In this article, we will look into the process of solving two reverse engineering challenges from the TryHackMe CTF event. Step-by-step breakdown of how to approach and solve these puzzles.

The original binaries are saved here.
Recommended knowledge
First get familiar with C (often you will end-up in de-compiler), it is very useful to learn little bit of assembly, registers and calling conventions if you need to use GDB.
Most important is to learn how to google build-in functions and more.
Few tips how to approach these types challenges
Find your de-compiler (Ghidra, IDA, Binary ninja, cutter, radare2 and more), there are lot’s of options just find what you are comfortable.
I stick with Ghidra and cutter (switching to the cutter if Ghidra does not decompile built-in functions or cannot find the main, just to check).
Get the bigger picture do not look into one part of the code too long if you look at a function from a different part, you can spot what is it for and decide if you need to understand it.
When looking for the main function, look for common patterns (the main function is usually loaded in the same manner from the entry point of the binary).
Plot graph of the execution flow for bigger binaries, it can help with clarity.
Make comments (the same reason).
Computemagic
When opening a binary usually you would see weird names and types.
After some renaming and figuring out what does what. I end up with this code:
int main(int argc,char **argv) {
int error_res;
ssize_t read_return;
long in_FS_OFFSET;
undefined4 option_value;
socklen_t address_len;
int socket;
int socket_filedesc;
int check;
size_t length;
sockaddr socketAddr;
char buffer [24];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 40);
option_value = 1;
address_len = 16;
while( true ) {
/* int socket(int domain, int type, int protocol); */
socket = ::socket(2,1,0);
if (socket == 0) {
perror("socket failed");
/* WARNING: Subroutine does not return */
exit(1);
}
error_res = setsockopt(socket,1,15,&option_value,4);
if (error_res != 0) {
perror("setsockopt failed");
/* WARNING: Subroutine does not return */
exit(1);
}
socketAddr.sa_family = 2;
socketAddr.sa_data[2] = '\0';
socketAddr.sa_data[3] = '\0';
socketAddr.sa_data[4] = '\0';
socketAddr.sa_data[5] = '\0';
socketAddr.sa_data._0_2_ = htons(9003);
error_res = bind(socket,&socketAddr,16);
if (error_res < 0) {
perror("bind failed");
/* WARNING: Subroutine does not return */
exit(1);
}
error_res = listen(socket,3);
if (error_res < 0) break;
socket_filedesc = accept(socket,&socketAddr,&address_len);
if (socket_filedesc < 0) {
perror("accept failed");
/* WARNING: Subroutine does not return */
exit(1);
}
sendBanner(socket_filedesc);
read_return = read(socket_filedesc,buffer,16);
if (read_return < 0) {
perror("read failed");
/* WARNING: Subroutine does not return */
exit(1);
}
length = strlen(buffer);
if ((length != 0) && (socketAddr.sa_data[length + 13] == '\n')) {
socketAddr.sa_data[length + 13] = '\0';
}
check = checkSpell(buffer,socket_filedesc);
if (check == 1) {
puts("\nSpell check successful.");
}
else {
puts("\nSpell check failed.");
}
close(socket_filedesc);
close(socket);
}
perror("listen failed");
/* WARNING: Subroutine does not return */
exit(1);
}
The binary starting a socket and waiting for connection.
Here is the most interesting parts of the code:
socket_filedesc = accept(socket,&socketAddr,&address_len);
read_return = read(socket_filedesc,buffer,16);
length = strlen(buffer);
if ((length != 0) && (socketAddr.sa_data[length + 13] == '\n')) {
socketAddr.sa_data[length + 13] = '\0';
}
check = checkSpell(buffer,socket_filedesc);
if (check == 1) {
puts("\nSpell check successful.");
}
else {
puts("\nSpell check failed.");
}
Now we know how large the input string must be and we know what function is used to validate if the input is correct.
int checkSpell(char *buffer,int filedesc) {
int return_val;
size_t length;
length = strlen(buffer);
if ((int)length < 1) {
puts("Empty string.");
return_val = 0;
}
else {
switch(*buffer) {
case 'A':
func_1(buffer,filedesc);
break;
...
...
case 'Z':
func_26(buffer,filedesc);
}
return_val = 1;
}
return return_val;
}
Switch case, that is calling different functions depending on the first letter.
It can look complex, but I realized it has to take parameters in order to check the actual string and I end up look into each function that has parameters and found:
void func_13(char *buf,int filedsc) {
int result;
result = check_other(buf);
if (result != 0) {
read_flag(filedsc);
}
return;
}
Where the real validation happens:
bool check_other(char *buf) {
int res_strcmp;
long in_FS_OFFSET;
int i;
char hardcoded [16];
long canary;
canary = *(long *)(in_FS_OFFSET + 40);
// all what we need to reverse:
for (i = 0; i < 16; i = i + 1) {
hardcoded[i] = buf[i] + 0x4U ^ 13;
}
res_strcmp = strcmp(hardcoded,"AhhF1ag1571GHFDS");
if (res_strcmp != 0) {
printf("Incorrect: %s\n",buf);
}
else {
printf("Correct: %s\n",buf);
}
if (canary != *(long *)(in_FS_OFFSET + 40)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return res_strcmp == 0;
}
Which I reverse by using python (the 0x4U means unsigned 4):
a = "AhhF1ag1571GHFDS"
buf = [""] * len(a)
for i in range(len(buf)):
buf[i] = chr(ord(a[i]) + 4 ^ 13)
print("".join(buf)) # HaaG8hf8468FAGEZ
Oldauth
Here is the same process rename everything and type it:
int main(int argc,char **argv,char **envp) {
bool bVar1;
int iVar2;
char *fgets_check;
size_t newline_index;
undefined7 extraout_var;
undefined7 extraout_var_00;
undefined7 extraout_var_01;
undefined7 extraout_var_02;
undefined4 in_register_0000003c;
long in_FS_OFFSET;
char username [112];
char key [1000];
long canary_bit;
canary_bit = *(long *)(in_FS_OFFSET + 40);
setup(CONCAT44(in_register_0000003c,argc));
printf("Enter the key: ");
fgets_check = fgets(key,1000,stdin);
if (fgets_check == (char *)0) {
puts("Error!");
iVar2 = -1;
}
else {
newline_index = strcspn(key,"\n");
key[newline_index] = '\0';
/* xor by 82 every char
*/
change(key,newline_index,82);
if (newline_index != 16) {
/*
*/
fail();
}
if (key[2] != 'Q') {
fail();
}
if (key[13] != '4') {
fail();
}
/* suma of chars and modulo (4: how many chars, 3: modulo) */
bVar1 = check(key,4,3);
if ((int)CONCAT71(extraout_var,bVar1) == 0) {
fail();
}
bVar1 = check(key + 4,5,8);
if ((int)CONCAT71(extraout_var_00,bVar1) == 0) {
fail();
}
bVar1 = check(key + 8,4,5);
if ((int)CONCAT71(extraout_var_01,bVar1) == 0) {
fail();
}
bVar1 = check(key + 12,4,3);
if ((int)CONCAT71(extraout_var_02,bVar1) == 0) {
fail();
}
printf("Enter the username: ");
fgets_check = fgets(username,100,stdin);
if (fgets_check == (char *)0) {
puts("Error!");
iVar2 = -1;
}
else {
newline_index = strcspn(username,"\n");
iVar2 = compare_with_target(username,newline_index);
if (iVar2 == 0) {
fail();
}
succeed();
iVar2 = 0;
}
}
if (canary_bit != *(long *)(in_FS_OFFSET + 40)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return iVar2;
}
Find interesting parts of code:
(I’ve named the first input as key)

Next there is called four times on an input check function.
bVar1 = check(key,4,3);
if ((int)CONCAT71(extraout_var,bVar1) == 0) {
fail();
}
bVar1 = check(key + 4,5,8);
if ((int)CONCAT71(extraout_var_00,bVar1) == 0) {
fail();
}
bVar1 = check(key + 8,4,5);
if ((int)CONCAT71(extraout_var_01,bVar1) == 0) {
fail();
}
bVar1 = check(key + 12,4,3);
if ((int)CONCAT71(extraout_var_02,bVar1) == 0) {
fail();
}
After some adjustments for the function:
bool check(char *key,int for_end,int modulo) {
int suma;
int i;
suma = 0;
for (i = 0; i < for_end; i = i + 1) {
suma = suma + key[i];
}
return suma % modulo == 0;
}
I also noticed a second fgets meaning there are two parts (password and username). I started with reversing the check function using python:
key = ["", "", "Q", "", "", "", "", "", "", "", "", "", "", "4", "", ""]
def rev_check(start, end, modulo):
suma = 0
def_letter = "d"
for i in range(start, start+end):
if not key[i]:
key[i] = def_letter
suma += ord(key[i])
if suma % modulo != 0:
tmp = suma % modulo
key[i] = chr(ord(def_letter) - tmp)
# from the binary
rev_check(0, 4, 3)
rev_check(4, 5, 8)
rev_check(8, 4, 5)
rev_check(12, 4, 3)
print("REV Check:", key)
for c in range(len(key)):
key[c] = chr(ord(key[c]) ^ 82)
print("XOR:", key)
with open("rev_out", "w") as f:
f.write("".join(key))
print("password:", "".join(key).encode())
Not the prettiest code, but hey it works.
2nd part is username:
printf("Enter the username: ");
fgets_check = fgets(username,100,stdin);
if (fgets_check == (char *)0) {
puts("Error!");
iVar2 = -1;
}
else {
newline_index = strcspn(username,"\n");
iVar2 = compare_with_target(username,newline_index);
if (iVar2 == 0) {
fail();
}
succeed();
iVar2 = 0;
}
Noticed that there is called the compare_with_target function, to check if the username is correct.

Don’t get confused with the return “!=” it has to return the “1”.
This function is a straight forward checking hard-coded variable with a small ASCII shift (+2).
The reverse of this function in python would look like this:
# username
username = ""
for c in "elb4rt0pwn":
username += chr(ord(c) - 2)
print("username:", username)
After sending it all together into the binary you would get a flag.
Final words
Thanks for reading, if someone is interested in collaboration write me on discord (bitr13x) or email me (sw33tbit@protonmail.com).