I love swampCTF. swampCTF 2024 was one of the first CTF's ive played and the first one where I felt immense satisfaction solving a challenge. A lot of high quality challenges all around, though it seems like this years was easier than last years.
In the main function, We allocate 3 pointers and 1 variable, change a variable until its non-zero, do a lot more assignments to variables from constants loaded within memory, and perform a hashing function for each of them (there are like 20 of thes variables). Another loop of a pointer, and then we run the 2nd function
The second function is a lot more interesting. It constructs a list, with the first element as a function. Then, it calls that function.
Debugging
So, the function data is dynamicaly loaded. We debug to see whats going on.
Enter libc_start_main
Continue until we enter main which is here:
Step into that call, then keep moving until we call rax. Our entry is at 0x5555555550c0. This is what it looks like:
It iterates through a loop, and it runs in it for __ times.
Afterwards, it gradually pushes the word Incorrect\n onto the memory locations
Then, it writes Correct!\n to the memory space afterwards
Afterwards, save some data to the heap
Enter a loop that repeats 181 times,
Write the data, then jump into the next call.
Then, it tries to mmap, and it will jump out if it fails.
It will then move some string contents into registers
Moves some more things around
Moves in some constants, then calls rbp
Inside rbp: string constants and data is being loaded
Enters a loop that runs 38 times
It should have a conditional move that occurs when dil is equal to r8b
This is the key.
Initially, before the start of the loop, rbx is Correct!\n
After the loop, if esi is 0, then the value at r14 (which is Incorrect) is moved into rbx, then it prints rbx.
If this comparison is not equal, then we will have to cmov
so, this comparison must be equal all 36 times
Setting the first character to s passes the first check, so we know that it is decoding now character by character. Also, changing characters does not modify the r8b values, so these are constant.
Now, we know that it loads the character into dil, xors it with al and compares it to be r8b. So, we should be able to find the character from the expected r8b values and al values.
I have a script to help me calculate the next character, since I did not want to reverse engineer the encryption process
def find_a(b_hex, c_hex):
b_int = int(b_hex, 16) # Convert b from hex to integer
c_int = int(c_hex, 16) # Convert c from hex to integer
a_int = c_int ^ b_int # Perform the XOR operation
a_hex = hex(a_int) # Convert a back to hexadecimal
return a_hex
b_decimal = int(input('b_decimal: '))
b_hex = hex(b_decimal & 0xFF) #ensure 8 bit representation.
c_hex = input("c_hex: ")
result_hex = int(find_a(b_hex, c_hex), 16)
print(f"a = {result_hex}, chr(a) = {chr(result_hex)}")