Categories
Capture The Flag

VishwaCTF’22 Writeups: Reverse Engineering

Below are the Reverse Engineering writeups of all the challenges that were being asked in the VishwaCTF'22, organised by Cyber cell VIIT, Pune.

Aggresive vault

First dry run: No output

After analizing the binary found:
  [ExE argument]:
    -> Exe accpets an argument
    -> The string is opened as a file
    -> The file needs to exist
    -> The argument is used to work upon the arra of bytes
  
  [Array of bytes]:
    -> Are being processed upon and finally written to a file. 
    -> Output is in an image file
   
  [An ascii string just there]
    -> Points to VishwaCTF website
    -> The site only has one image
  
  Steps to solve:
  
    -> Dowload img on site, keep exe and img in same directory
    -> Open run exe with img name
    [ Output ]
      program leaks memory, before exiting abnormally
      the img file is changed (possibly corrupted or empty)
      
    -> After more analyzing, you find the memory leak, block that function call, remake the exe
    -> Open with the img name with img in same directory, img is changed to the flag
    
    
    [Incase img not found]
    -> Experienced solvers may realize its an encrypted img right away, after reversing the encryption steps, 
       Keys can be tried out to get the expected headder bytes, correct format can be identified from them by looking 
       at the meaningful key string
       creating a file with that name, blocking the mem leak call, the file will be built with your flag in it! 
    
--- More about encryption ---     

  -> It's just exoring bits of a bmp image with the key string

Confusion

Simple keygen that takes a username and creates a password the user must enter, the Username has already been given to the user, all the user had to do was reverse engineer the program, and based on the Username find the suitable Password. There are multiple debugger checks that will force the application to exit if it is being debugged.

Analysis

The main function is found by finding the _cexit function. This is the function that returns control from the user code back to the C runtime environment. The function call at instruction 14000310b seen below is the main function.

       1400030f6 e8 93 0a        CALL       API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL::__p___argv
                 00 00
       1400030fb 48 8b 18        MOV        RBX,qword ptr [RAX]
       1400030fe e8 85 0a        CALL       API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL::__p___argc
                 00 00
       140003103 4c 8b c7        MOV        R8,RDI
       140003106 48 8b d3        MOV        RDX,RBX
       140003109 8b 08           MOV        ECX,dword ptr [RAX]
       14000310b e8 f0 e1        CALL       main				<-----------------
                 ff ff
       140003110 8b d8           MOV        EBX,EAX
       140003112 e8 05 07        CALL       FUN_14000381c
                 00 00
       140003117 84 c0           TEST       AL,AL
       140003119 74 55           JZ         LAB_140003170
       14000311b 40 84 f6        TEST       SIL,SIL
       14000311e 75 05           JNZ        LAB_140003125
       140003120 e8 6f 0a        CALL       API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL::_cexit
                 00 00

Inside main there are three debugger checking functions. These are found at 14000138c, 14000139f, and 1400013ef. If the user wants to bypass the logic checks for the presence of a debugger, the zflag needs to be set to 1, 1, and 0 respectively for the logic checks at addresses 14000138c, 14000139f, and 1400013ef.

The program then prompts the user to either enter 'continue' or 'rules' to either continue or see a list of rules. Entering continue will take the user to a function called at address 140001591. The function starts at address 140002120 and is the function that generates the key, prompts the user for their name and password, and checks if the password is valid. Another debugger check occurs at 140002157. This requires the zflag set to 1 to bypass. The console screen is cleared and the username is prompted for their username. Data is encountered at 1400021dc to 140002201 that is the base data used to generate the password.

       1400021dc c7 45 db        MOV        dword ptr [RBP + -0x25]=>local_88[4],0xff9cffa0
                 a0 ff 9c ff
       1400021e3 c7 45 e7        MOV        dword ptr [RBP + -0x19]=>local_78[0],0xffb2ffe2
                 e2 ff b2 ff
       1400021ea c7 45 d7        MOV        dword ptr [RBP + -0x29]=>local_88[0],0xff98ffb1
                 b1 ff 98 ff
       1400021f1 c7 45 e3        MOV        dword ptr [RBP + -0x1d]=>local_88[12],0xffe0ffa2
                 a2 ff e0 ff
       1400021f8 b8 ff ff        MOV        EAX,0xffff
                 00 00
       1400021fd 66 89 45 eb     MOV        word ptr [RBP + -0x15]=>local_78[4],AX
       140002201 c7 45 df        MOV        dword ptr [RBP + -0x21]=>local_88[8],0xff99ffb8
                 b8 ff 99 ff

A loop is then entered that will begin the process of transforming the data from the assembly above.

                             encrypt_sub 
       140002210 0f b7 0a        MOVZX      ECX,word ptr [RDX]=>local_88
       140002213 66 2b c8        SUB        CX,AX
       140002216 66 f7 d1        NOT        CX
       140002219 66 33 c8        XOR        CX,AX
       14000221c 66 89 0a        MOV        word ptr [RDX]=>local_88,CX
       14000221f ff c0           INC        EAX
       140002221 48 8d 52 02     LEA        RDX=>local_88[2],[RDX + 0x2]
       140002225 83 f8 0b        CMP        EAX,0xb
       140002228 72 e6           JC         encrypt_sub

The loop takes 2 byte chunks from the data "buffer" and subtracts it from the index of the loop, toggles every bit, xor's it with the index of the loop, and then places the value back in the same portion of the data "buffer." This is done 11 times.

A loop is then entered that prompts the user for the password. The input stream accepts an integer value as input. This is done 11 times.

The final transform is taking the values from the "encryption" loop and xor'ing each 2 byte zero extended chunk by the length of the username entered. A comparison is performed with the input password integers, if the two values are not equal a counter is incremented.

Note, the 2 byte chunks start at index 0 and incremented to the next 2 byte chunk. The input password integers are indexed by their order in the password prompting above. The counter is checked after all the comparisons are performed and if it is zero the success message is output to the user.

Is this even Hashing?

#include <iostream>
#include <chrono>
#include <thread>
#define ll long long
using namespace std;

ll hashIt(ll n, ll x, ll m){
    ll ans = 0;
    for (int i=0; i<x; i++){
        ans += n;
        ans %=m;
    }
    return ans;
}

ll hashHash(ll n){
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    return (n << 32) + (n >> 32);
}

int main(int argc, char *argv[]){
    if (argc == 1) cout << "Enter atleast 1 argument \n";
    else {
        for (int i=1; i<argc; i++){
            // cout << hashHash(hashIt(stoll(argv[i]), 961798719, 1000000000)) << "\n";
            cout << hashIt(hashHash(stoll(argv[i])), 279954879, 1000000000) << "\n";
        }
    }
    return 0;
}

One way enigma

from string import ascii_uppercase, digits

class rotors:

    saved = {
            1:('{K8UMNZBGS20D3IR_5CEFYOQPX4JH1VA}96TW7L',[9]),
            2:('EFXRSBQ1MC3GYW_9{47AVT8JP5ON6U20ZK}HDIL',[23]),
            3:('WUXQGVJHT2YO043MK17LC9ANZBP{_S6D}I5EFR8',[7]),
            4:('W0K2NBVRPJTZY1XHD9478L{QUMG6I_O3FAS5}CE',[30]),
            5:('V3F_1QA}L4WOZIM8SY7P9XG20CDNKJUH5B6ERT{',[13])
            }

    rotorSequence = {}

    def __init__(self, **kwargs):

        # default values
        self.preset = 1
        self.offset = 0
        self.position = 0

        # switching to given values
        if "rotor_num" in kwargs: self.rotor_num = kwargs["rotor_num"]
        if "preset" in kwargs: self.preset = kwargs["preset"] 
        if "offset" in kwargs: self.offset = kwargs["offset"] 
        if "position" in kwargs: self.position = kwargs["position"]
        if "notches" in kwargs: self.notches = kwargs["notches"]
        else: self.notches = rotors.saved[self.preset][1]
        
        # setting up remaining properties
        rotors.rotorSequence[self.rotor_num] = self 
        self.mapper(self.preset) 
        self.notch_found = False

    def __repr__(self):
        return f'<Rotor no. {self.rotor_num}, Preset: {self.preset}, Position: {self.position}, Offset: {self.offset}>'

    def mapper(self, preset=1):

        '''takes preset number as an argument and maps the\n
        rotor wiring accordingly'''

        statorKey = ascii_uppercase + digits + "_{}"
        rotorKey = rotors.saved[preset][0]
        map = {}
        rotorMap = {}
        for i,char in enumerate(statorKey):
            map[char]=i
            map[i]=char
        
        for i in range(len(statorKey)):
            rotorMap[i] = [map[rotorKey[i]]]
        for i in range(len(statorKey)):
            temp = rotorMap[map[rotorKey[i]]]
            rotorMap[map[rotorKey[i]]] = (temp[0], i)
        rotorMap["wiring"] = rotorKey
        
        self.wiring = rotorMap    


    def rotate(self):

        '''rotates the rotorset once (for each keypress)'''

        # should this rotor make the next rotor rotate?
        if self.position in self.notches and self.rotor_num < len(rotors.rotorSequence):
                rotors.rotorSequence[self.rotor_num + 1].notch_found = True

        # to rotate or not to rotate
        if self.rotor_num == 1: 
            self.position +=1

        elif self.notch_found:
            self.position += 1
            self.notch_found = False
        
        # Completion of rotation >>> Reset
        self.position %= 39

    def passLeft(self, contact):

        '''takes contact from right and sends to left\n
        meanwhile taking care of rotation in the\n
        FORWARD CYCLE'''

        self.rotate() # Rotation prior to connection

        LSCN = (self.wiring[(self.position-self.offset+contact)%39][0] - (self.position-self.offset))%39

        return LSCN

    def passRight(self, contact):

            '''Hmmm...'''
            
            RSCN = (self.wiring[(self.position-self.offset+contact)%39][1] - (self.position-self.offset))%39

            return RSCN

            # pass

class steckerbrett:

    def __init__(self):

        self.mapper()
            
    def __repr__(self):
        return f'Steckerbrett : {self.swaps}'

    def mapper(self):
        statorKey = ascii_uppercase + digits + "_{}"
        self.pairs = {}
        self.map = {}
        for i,char in enumerate(statorKey):
            self.map[char]=i
            self.map[i]=char
            self.pairs[char]= char

    def connect(self, char):
        return self.map[self.pairs[char]]

    def disconnect(self, contact):
        return self.pairs[self.map[contact]]


def getSettings():

    num = int(input('How many rotors do you want? >>>   '))

    settings['rotors']['sequence'] = []
    for i in range(num):
        rotor_num = int(input(f'Enter number of rotor {i+1} from 1-5 >>>   '))
        settings['rotors']['sequence'].append(rotor_num)
    
    settings['rotors']['positions'] = []
    for i in range(num):
        position = int(input(f'Enter position of rotor {i+1} from 0-38 >>>   '))
        settings['rotors']['positions'].append(position)

    return settings


def Enigma(settings):

    plugboard = steckerbrett()
    sequence = settings['rotors']['sequence']
    positions = settings['rotors']['positions']
    rotorSet = {}
    for i in range(len(sequence)):
        rotorSet[i] = rotors(rotor_num=i+1, preset=sequence[i], position=positions[i])

    return plugboard, rotorSet


def Encrypt(message, enigma):

    plugboard, rotorSet = enigma
    encrypted = ''
    for char in message:
        contact = plugboard.connect(char)
        for i in range(1, len(rotors.rotorSequence)+1):
            contact = rotors.rotorSequence[i].passLeft(contact)
        char = plugboard.disconnect(contact)
        encrypted += char
    return encrypted

def Decrypt(message, enigma):
    
    plugboard, rotorSet = enigma
    decrypted = ''
    for char in message:
        for i in range(1, len(rotors.rotorSequence)+1):
            rotors.rotorSequence[i].rotate()
        contact = plugboard.connect(char)
        for i in range(len(rotors.rotorSequence), 0, -1):
            contact = rotors.rotorSequence[i].passRight(contact)
        char = plugboard.disconnect(contact)
        decrypted += char
    return decrypted

# Default Settings for The Enigma
settings = {'version':1,
            'phrase':"Anyth1nG RanD0M Go3S HeR=E 69 And HWa+t CtuaLFck39875/",
            'rotors':{'sequence':[1,1,1],
                    'positions':[11,22,33],
                    'offset':[15,2,31]}}

Overcook_

python -c "print('a'*50+'b'*12+'\x3c\x62\x55\x56')"
114 051 118 101 114 115 049 110 103 095 100 117 100 051

VishwaCTF{r3vers1ng_dud3}

Uzamaki Game

  1. Extract code from exe file
  2. Among garbage find spiralOrder function
  3. You will get the matrix which you have to access all elements in a
    spiral order
  4. For each pair just multiply 1st no. by 26 and add second no and
    convert this integer ascii value to character
  5. Do this for all elements to get flag

Bet Game

We are given an ELF and a .py file named game and sage.py respectively.

On running game we see game instructions:

![[Pasted image 20220323130725.png]] While playing, we notice it is almost impossible to earn the flag amount. So we give the player a hint in the form of sage.py

when we run sage.py it says “The biggest clue lies in your input” and asks for input.

Let’s try some random input.

![[Pasted image 20220323130738.png]]

It’s saying “t@k3_f3w_5t3p5_b@<k”.

If we look into code we can see a weird looking string and some functions. Let’s try to input that string.

![[Pasted image 20220323130745.png]] Hmm, so the output was same.

We reversed those functions and tried to print the output given by functions we get:-

![[Pasted image 20220323130754.png]]

So we got the hint which is “try_thinking_negatively”.

The flag amount can be earned directly in one bet if the player bets negative amount (hence the hint try_thinking_negatively) only this time we have to guess incorrectly (which is pretty easy).

![[Pasted image 20220323130800.png]]

Flag

$tr!k3_!t_lu<ky

Brutal taskmaster

Compiling task.ll in ELF format using clang with the following command


_clang task.ll -mllvm -W -g -W1,-pie -o task_

After compiling the task.ll with clang, we run executable file and type string as 0123456789, but receive fail notification as below:

![[Pasted image 20220323130931.png]] Let’s open task![[Pasted image 20220323130941.png]]in Ghidra.

Pseudo code for check() function.

![[Pasted image 20220323130948.png]] We easy conclude that the return value( initially, its value is 1) of this function depends input string and what string:

· if it's 1 then all elements of input must satisfy above equation( choose this case)

· if it's 0, we will have a lot's of potential input which can happen( ignore this case)

main() function

![[Pasted image 20220323130954.png]] ![[Pasted image 20220323130959.png]]

  • As mentioning above, I guess that the return value of check() function is 1, so I will pay my attention to the second branch of program: each element of input will be XOR-ed secret string before printing to screen.
  • Because there are no additional hints about original flag, so I brute-forced all possible characters of flag( in range 32-127, in ASCII). We also noticed that, when I have the first character of input, I easily can get its original form( xor with secret[i]) and second character's original form( xor with what[i])

This is the sample script which will do the brute forcing.

secret = 'B\n|"\x06\x1bg7#\F\n)\t0Q8_{Y\x13\x18\rP'_

what = "\x17/'\x17\x1dJy\x03,\x11\x1e&\nexjONacA-&\x01LANH'.&\x12>#'Z\x0fO\x0b%:(&HI\x0cJylL'\x1emtdC"

alphabet =[]

for i in range(30, 127):

alphabet.append(i)

flag = [0]*56

for i in alphabet:

print( 'i = ', chr(i))

flag[0] = i

for j in range(0, len(flag) – 1):

#flag[i] ^ secret

tmp = flag[j] ^ ord(secret[j %len(secret)])

#ori_flag[i] ^ what --> ord_flag[i + 1]

ori_flag_i_1 = tmp ^ ord(what[j])

flag[j + 1] = ori_flag_i_1 ^ ord(secret[(j + 1) % len(secret)])

print('flag{', end='')

for k in flag:

print(chr(k), end='')

print('}')

print('\n**********************************************************\n')

print()

Sensible xor is found as i=7.

Flag

7h15_f14g_15_v3ry_v3ry_l0ng_4nd_1_h0p3_th3r3_4r3_n0_7yp0

Take your time

#include <stdio.h> 
#include <stdlib.h> 
#include<time.h> 

int main(void)
{ 

	char str[80];

	int T = time(0);

    srand(T); 
  
    int num1 = rand();
    uint num2 = (num1 - 3) * 3;
  
    if(num2 & 1 != 0)
    {
    	num2 = num2 - 1;
    }

    int num3 = (int) num2 /2 + 7;

    sprintf(str,"%d %d %d %d",T,num1,num2,num3);

    puts(str);
  
    return 0; 
} 

Taking over the world

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <map>
#include <algorithm>
#include <climits>
#include "offsets.h"

using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::vector;
using std::queue;
using std::multimap;
using std::min;

struct Node {
	int to, cap, rev;
	Node(int t, int c, int r) : to(t), cap(c), rev(r) {}
};

void die() {
	puts("Incorrect!");
	exit(1);
}

int vid(const int v, const bool o) {
	return v * 2 + (o ? 1 : 0);
}

void add_edge(const int i, 
				const int j, 
				const int c, 
				vector<vector<Node>> *adj) {
	(*adj)[i].emplace_back(j, c, (*adj)[j].size());
	(*adj)[j].emplace_back(i, 0, (*adj)[i].size() - 1);
}

// Time:  O(MlogN)
// Space: O(N)
vector<int> dijkstra(const vector<bool>& guard,
					 const vector<vector<int>>& A,
					 const int s) {
	vector<int> dst(A.size(), INT_MAX);
	multimap<int, int> que;
	que.emplace(0, s);
	dst[s] = 0;

	while (!que.empty()) {
		const int c = que.begin()->first;
		const int v = que.begin()->second;
		que.erase(que.begin());
		if (dst[v] == c) {
			for (int tv = 0; tv < A[v].size(); ++tv) {
				if (A[v][tv]) {
					const int tc = dst[v] + 1 + (guard[v] ? 1 : 0);
					if (tc < dst[tv]) {
						dst[tv] = tc;
						que.emplace(tc, tv);
					}
				}
			}
		}
	}
	return dst;
}

bool levelize(const int V, const int S, const int T,
			  vector<vector<Node>> *adj,
			  vector<int> *lev) {
	*lev = vector<int>(V, -1);
	queue<int> que;
	(*lev)[S] = 0;
	que.emplace(S);
	while (!que.empty()) {
		int v = que.front();
		que.pop();
		for (int i = 0; i < (*adj)[v].size(); ++i) {
			const Node &e = (*adj)[v][i];
			if (e.cap && (*lev)[e.to] == -1) {
				(*lev)[e.to] = (*lev)[v] + 1;
				que.emplace(e.to);
			}
		}
	}
	return (*lev)[T] != -1;
}

int augment(const int S, const int T,
			const int v, const int f,
			const vector<int>& lev,
			vector<vector<Node>> *adj,
			vector<int> *done) {
	if (v == T || !f) {
		return f;
	}
	for (; (*done)[v] < (*adj)[v].size(); ++(*done)[v]) {
		Node &e = (*adj)[v][(*done)[v]];
		if (lev[e.to] > lev[v]) {
			const int t = augment(S, T, e.to, min(f, e.cap), lev, adj, done);
			if (t > 0) {
				e.cap -= t;
				(*adj)[e.to][e.rev].cap += t;
				return t;
			}
		}
	}
	return 0;
}

// Time:  O(N * M^2)
// Space: O(N)
int max_flow(const int V, const int S, const int T,
			 vector<vector<Node>> *adj) {
	int f = 0, t;
	vector<int> lev;
	while (levelize(V, S, T, adj, &lev)) {
		vector<int> done(V);
		while ((t = augment(S, T, S, INT_MAX, lev, adj, &done))) {
			f += t;
		}
	}
	return f;
}

vector<bool> min_cut(const int V, const int S,
					 const vector<vector<Node>>& adj) {
	vector<bool> vis(V);
	queue<int> que;
	que.emplace(S);
	vis[S] = true;
	while (!que.empty()) {
		int v = que.front();
		que.pop();
		for (const Node &e : adj[v]) {
			if (e.cap && !vis[e.to]) {
				que.emplace(e.to);
				vis[e.to] = true;
			}
		}
	}
	return vis;
}

int taking_over_the_world() {
	int N, M, K; cin >> N >> M >> K;
	vector<vector<int>> A(N, vector<int>(N));
	for (int i = 0; i < M; ++i) {
		int u, v;
		cin >> u >> v;
		A[u][v] = A[v][u] = true;
	}

	const int GUARD = 1000;
	vector<bool> guard(N);
	while (true) {
		const int V = N * 2;
		const int S = vid(0, false);
		const int T = vid(N - 1, false);

		vector<vector<Node>> adj(V);
		for (int v = 0; v < N; ++v) {
			add_edge(vid(v, false), vid(v, true), guard[v] ? GUARD : 1,
					 &adj);
		}

		const vector<int> ds = dijkstra(guard, A, 0);
		const vector<int> dt = dijkstra(guard, A, N - 1);
		for (int u = 0; u < N; ++u) {
			for (int v = 0; v < N; ++v) {
				if (A[u][v]) {
					if (ds[u] != -1 && dt[v] != -1) {
						// Edge (u, v)
						int td = ds[u] +
								 (guard[u] ? 1 : 0) + 1 +
								 (v == N - 1 ? 0 : (guard[v] ? 1 : 0) + dt[v]);
						if (td == ds[N - 1]) {
							add_edge(vid(u, true), vid(v, false), GUARD, &adj);
						}
					}
				}
			}
		}

		if (max_flow(V, S, T, &adj) <= K) {
			const vector<bool> mc = min_cut(V, S, adj);
			for (int v = 0; v < N; ++v) {
				if (mc[vid(v, false)] && !mc[vid(v, true)]) {
					guard[v] = true;
					--K;  // At most K loops
				}
			}
		} else {
			break;
		}
	}
	return dijkstra(guard, A, 0)[N - 1];
}

/*
int array_to_num(int arr[],int n){
	char str[10][2];
	int i;
	char number[13] = {'\n'};
	for(i=0;i<n;i++) sprintf(str[i],"%d",arr[i]);
	for(i=0;i<n;i++)strcat(number,str[i]);
	i = atoi(number);
	return i;
} 
*/

/*
int num[6] = {13,20,6,4,3,55};
int real_num;
real_num = array_to_num(num,6);
*/

int main() {
	int T = 5;

	int decrypt[10] = {123421, 212, 3234, 23234, 55755, 4566, 464517, 464688, 2579, 152629};
	/*
	FILE* fp = fopen(argv[0], "r");
	if (!fp){
		return 1;
	}
	*/

	matrix newMatrix;
	int *magic_numbers = newMatrix.magic_numbers_();
	int *num_array1 = newMatrix.num_array_1_();
	int *num_array2 = newMatrix.num_array_2_();

	// fread(decrypt, sizeof(char), sizeof(decrypt), fp);
	int add_by = 1;
	for (int i = 0; i < 10; i++)
	{
		if (magic_numbers[i] == 0)
		{
			magic_numbers[i] = add_by;
		}
		add_by++;
	}

	for (int i = 0; i < 10; i++){
		magic_numbers[i] = magic_numbers[i] ^ decrypt[i];
	}

	for (int test = 0; test < T; ++test) {

		int x = taking_over_the_world();
		if(num_array1[test] != x){
			die();
		}
		if(num_array2[test] != x){
			die();
		}
	}

	// false flag
	cout << "VishwaCTF{";
	for(int i = 0; i < T*2; i++){
		cout << magic_numbers[i];
	}
	cout << "}";
}

Get the latest tech news and updatesethical hacking tutorials and cybersecurity tips and tricks. Check out MeuSec for more.

Sometimes we include links to online retail stores and/or online campaigns. If you click on one and make a purchase we may receive a small commission.

Comments:

Leave a Reply

Your email address will not be published. Required fields are marked *