# PicoCTF 2024 Reverse Engineering Challenges Writeup Below is a writeup of my solution upon solving the reverse engineering category from picoCTF 2024. There was a total of 7 reverse engineering challenges. I could say they were pretty straight forward and interesting challenges. Let's dive into it! But there is a note from PicoCTF, but anyways all I hope they put it in the GYM sooner 😭 ![image](https://hackmd.io/_uploads/ry2zUseyC.png) ## Packer (100 Points) ### Description ![image](https://hackmd.io/_uploads/BkWV5uYAT.png) ### Solution Provided with an executable, the first thing I check is to see if the binary is packed as the challenge name hints: ![image](https://hackmd.io/_uploads/HyFddxkCT.png) With the command `strings out | tail` I was able to see how the last string looks like and it's actually packed with UPX. What follows next is to decompress the packed binary to an original binary using the command `upx -d out -o original` will decompress the packed binary to a new binary known as "original": ![image](https://hackmd.io/_uploads/r1N4YxyRT.png) As seen above we are able to successfuly decompress the packed binary this will allow us to get a clear output of strings declares in variables and disassembling of the binary. With the command `rabin2 -z original | grep -i "flag"` I was able to get a weird string encoded in hex: ![image](https://hackmd.io/_uploads/Hyv6Yly0p.png) Decoding it will provide us with the flag: ![image](https://hackmd.io/_uploads/BkeZqeJ0a.png) FLAG : picoCTF{U9X_UnP4ck1N6_B1n4Ri3S_1a5a3f39} ## FactCheck (200 Points) ### Description ![image](https://hackmd.io/_uploads/Hy1T5xk0p.png) ### Solution Provided with a binary we are required to understand the inner workings of the binary to be able to get the complete flag from it! Starting off by checking the strings: ![image](https://hackmd.io/_uploads/SydWbtKC6.png) We have the first part of the flag and then we have a weird string looks like hex but when decoded it gives nothing readable. Now let's disassemble our binary: ![image](https://hackmd.io/_uploads/B1oufFY0p.png) I notice that there are empty variables that are first initiated and then given the values of an index of the string "759304aedb268" then it has a certain condition check then appends the output from the condition checks to a certain variable and in the end after appending everything necessary it will lastly add `"}"` ![image](https://hackmd.io/_uploads/SypzXKKA6.png) Now taking a reference of the same function on **Ghidra**, we get the same logic except there are different conditions implemented: ![image](https://hackmd.io/_uploads/HycdmiK0T.png) Now then let's break down what I understood from my analysis; First it initiates a variable `var_248` which will carry the value of `picoCTF{wELF_d0N3_mate_` as seen here: ![image](https://hackmd.io/_uploads/Hyo7EjF0a.png) Then the variable `var_228` is assigned the value of the string `"759304aedb268"` this string now will now then be fetched as an x index of different variables where as the x stands for a number as seen below: ![image](https://hackmd.io/_uploads/BymzHjKAT.png) Then after assigning each variable with a single character from the string it then runs a conditional check of if statements as seen above it'll first compare a certain variable if it's less than `B` then compares if it's not equal to `A` and lastly it substracts certain variables to check if the answer is equal to 3 and then for each conditional check from the if statements it then appends a certain variable to the variable `var_249` where in the end variable `var_249` will have a complete string of the flag. I decided to do this by replicating the same thing that is being done by the program but with python, First I rename the variables to a better way that I can understand in my case I'll notate them with ABC(s) ![image](https://hackmd.io/_uploads/BJIROitCT.png) Well at least now I can keep a good track on the variables, let me reconstruct this logic in python: ![image](https://hackmd.io/_uploads/SJHig3K0T.png) ![image](https://hackmd.io/_uploads/Hkd2g3tC6.png) I was then able to get the flag! FLAG : picoCTF{wELF_d0N3_mate_2394045a} ## WinAntiDbg0x100 (200 Points) ### Description ![image](https://hackmd.io/_uploads/SJfVbnK0T.png) ### Solution Given the description of the challenge, it's straight forward that the challenge wants us to bypass this anti-debugging process and debug the executable to get the flag. I first load my executable in IDA then I saw something interesting from the IDA output in the graph representation of the main function: ![image](https://hackmd.io/_uploads/rJCySnFA6.png) basically the function **isDebuggerPresent** will check if a debugger is present in the execution of the program, [**reference here**](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent). When I saw this I was like "this should be an interesting one!" I then decided to run the executable through IDA and see what the log panel outputs: ![image](https://hackmd.io/_uploads/H1RZ82KC6.png) Now from just running the program we can see that I have been able to through well except that when I reached the function **IsDebuggerPresent** I wasn't able to pass that check since I am actually using a debugger to run the program and it ended up giving the output `"### Oops! The debugger was detected. Try to bypass this check to get the flag!"` ![image](https://hackmd.io/_uploads/S1esIntAp.png) Since we need to reach the function `loc_20165A` which prints the flag, we first need to tamper around with the value of the test instruction from the function `loc_2015E7` by setting a breakpoint at the `test` instruction: ![image](https://hackmd.io/_uploads/S1xuS2nFRp.png) This will allow us to know the value that is then the output of `EAX` register before it hits the `jz` instruction and throws us the debugging error. ![image](https://hackmd.io/_uploads/rJpa23KCT.png) In our case after executing the program, I saw that the `EAX` register had the value of **1**, I change that to **0** and I was able to get the flag: ![image](https://hackmd.io/_uploads/Hkd-63tAT.png) And I got the flag: ![image](https://hackmd.io/_uploads/rJgLTntRT.png) FLAG : picoCTF{d3bug_f0r_th3_Win_0x100_e70398c9} ## Classic Crackme 0x100 (200 Points) ### Description ![image](https://hackmd.io/_uploads/HkYqTnt0a.png) ### Solution We are provided with a binary, I first load it up in my decompiler so that I get to understand what I am dealing with: ![image](https://hackmd.io/_uploads/BynJkTtCp.png) Given the decompiled output I was able to understand what is actually going on! Now to breakdown what's happening; The binary when executed it takes an input for the password which is then passed through an encryption algo that will encrypt your input and checks if the output is equal to the string `mp......` I had quiet a tough time trying to reverse the encryption logic and decrypt the string `mp.......` to get the initial string that was encrypted and resulted to that final string and I ended up having this script: ```c #include <stdlib.h> #include <stdio.h> #include <stdint.h> #include <string.h> int main() { //char encrypted[50] = "mrmrptlppkfnkfelvxzqenvxibnssxeqfdbycqqwoxomvnvbhs"; char encrypted[50]; scanf("%s", &encrypted); char decrypted[50]; strcpy(decrypted, encrypted); int encrypted_len = strlen(encrypted); int a = 0x55; int b = 0x33; int c = 0xf; char magic_byte = 0x61; for (int i = 1; i >= 0; i--) { for (int j = encrypted_len - 1; j >= 0; j--) { int calc_a = ((j % 0xff) >> 1 & a) + ((j % 0xff) & a); int calc_b = (calc_a >> 2 & b) + (b & calc_a); int decrypted_char = ((int)decrypted[j] - magic_byte - (c & calc_b) + 0x1a) % 0x1a; if (decrypted_char < 0) decrypted_char += 0x1a; int intermediate = (decrypted_char - (calc_b >> 4 & c) + 0x1a) % 0x1a; if (intermediate < 0) intermediate += 0x1a; decrypted[j] = magic_byte + intermediate; } } printf("Decrypted: %s\n", decrypted); return 0; } ``` But the output from running the script isn't the right string that was used to get the final secret string in our case. Running it in radare2 as I perform a dynamic analysis through the program and trying to bruteforce for each character until they complete the whole password of bytes with 40 length. First starting off by setting a breakpoint at the memcmp call instruction: ![image](https://hackmd.io/_uploads/S1QzQaFCp.png) using the command `db 0x0040136a` I am able to set a breakpoint to that call. Then running it : ![image](https://hackmd.io/_uploads/rJrpNpF0a.png) I can tell now that we need to see if we can actually match the index of each character in the string from `RSI` and `RDI` and for this we need to bruteforce. Now doing this all manually would be a pain! Below is an automated script which uses `r2pipe` to perform the dynamic analysis in a loop as well as comparing each character in the end: ```python #!/usr/bin/env python3 import r2pipe import sys import string def solve(b: str): key = list('a' * 0x32) index = 0 while True: for c in string.ascii_lowercase: # update key[index] = c with open('profile.rr2', 'w+') as f: f.write('#!/path/to/rarun2\nstdin="%s"' % ''.join(key)) r = r2pipe.open(b, flags=['-2', '-d', '-e', 'dbg.profile=profile.rr2']) BP1 = 0x0040136a # auto analyze r.cmd('aaaa') # seek to main r.cmd('s main') # set breakpiont after random function call r.cmd(f"db {BP1}") # run program r.cmd("ood") r.cmd("dc") key_out = r.cmd('psz @ rsi') key_in = r.cmd('psz @ rdi') if key_out[index] != key_in[index]: continue else: break index += 1 print(''.join(key)) if index >= 0x32: # print(key) print("THE COMPLETE FLAG IS : ") print(''.join(key)) break def main(): if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} binary") sys.exit(1) binary = str(sys.argv[1]) solve(b=binary) if __name__ == "__main__": main() ``` Credits to @Kharon for the automation, I tweaked the script a bit for better output :P Now before running the script make sure to turn off `ASLR` using the commands: ```shell echo 0 | sudo tee /proc/sys/kernel/randomize_va_space ``` then run the script: ![image](https://hackmd.io/_uploads/HJC0RRq0p.png) The last string of the output is the password, now that I am able to get the password I try to prove that it's really the password: ![image](https://hackmd.io/_uploads/r1pz1Ji06.png) Now what's left is to connect to the instance that can be spawned from picoCTF website and then input the password and you got the flag! To turn `ASLR` back on run the command: ```shell echo 2 | sudo tee /proc/sys/kernel/randomize_va_space ``` ## WeirdSnake (300 Points) ### Description ![image](https://hackmd.io/_uploads/HkvIlJoAT.png) ### Solution we are provided with a python bytecode as follows: ``` 1 0 LOAD_CONST 0 (4) 2 LOAD_CONST 1 (54) 4 LOAD_CONST 2 (41) 6 LOAD_CONST 3 (0) 8 LOAD_CONST 4 (112) 10 LOAD_CONST 5 (32) 12 LOAD_CONST 6 (25) 14 LOAD_CONST 7 (49) 16 LOAD_CONST 8 (33) 18 LOAD_CONST 9 (3) 20 LOAD_CONST 3 (0) 22 LOAD_CONST 3 (0) 24 LOAD_CONST 10 (57) 26 LOAD_CONST 5 (32) 28 LOAD_CONST 11 (108) 30 LOAD_CONST 12 (23) 32 LOAD_CONST 13 (48) 34 LOAD_CONST 0 (4) 36 LOAD_CONST 14 (9) 38 LOAD_CONST 15 (70) 40 LOAD_CONST 16 (7) 42 LOAD_CONST 17 (110) 44 LOAD_CONST 18 (36) 46 LOAD_CONST 19 (8) 48 LOAD_CONST 11 (108) 50 LOAD_CONST 16 (7) 52 LOAD_CONST 7 (49) 54 LOAD_CONST 20 (10) 56 LOAD_CONST 0 (4) 58 LOAD_CONST 21 (86) 60 LOAD_CONST 22 (43) 62 LOAD_CONST 23 (106) 64 LOAD_CONST 24 (123) 66 LOAD_CONST 25 (89) 68 LOAD_CONST 26 (87) 70 LOAD_CONST 27 (18) 72 LOAD_CONST 28 (62) 74 LOAD_CONST 29 (47) 76 LOAD_CONST 20 (10) 78 LOAD_CONST 30 (78) 80 BUILD_LIST 40 82 STORE_NAME 0 (input_list) 2 84 LOAD_CONST 31 ('J') 86 STORE_NAME 1 (key_str) 3 88 LOAD_CONST 32 ('_') 90 LOAD_NAME 1 (key_str) 92 BINARY_ADD 94 STORE_NAME 1 (key_str) 4 96 LOAD_NAME 1 (key_str) 98 LOAD_CONST 33 ('o') 100 BINARY_ADD 102 STORE_NAME 1 (key_str) 5 104 LOAD_NAME 1 (key_str) 106 LOAD_CONST 34 ('3') 108 BINARY_ADD 110 STORE_NAME 1 (key_str) 6 112 LOAD_CONST 35 ('t') 114 LOAD_NAME 1 (key_str) 116 BINARY_ADD 118 STORE_NAME 1 (key_str) 9 120 LOAD_CONST 36 (<code object <listcomp> at 0x7f704e8a4d40, file "snake.py", line 9>) 122 LOAD_CONST 37 ('<listcomp>') 124 MAKE_FUNCTION 0 126 LOAD_NAME 1 (key_str) 128 GET_ITER 130 CALL_FUNCTION 1 132 STORE_NAME 2 (key_list) 11 >> 134 LOAD_NAME 3 (len) 136 LOAD_NAME 2 (key_list) 138 CALL_FUNCTION 1 140 LOAD_NAME 3 (len) 142 LOAD_NAME 0 (input_list) 144 CALL_FUNCTION 1 146 COMPARE_OP 0 (<) 148 POP_JUMP_IF_FALSE 162 12 150 LOAD_NAME 2 (key_list) 152 LOAD_METHOD 4 (extend) 154 LOAD_NAME 2 (key_list) 156 CALL_METHOD 1 158 POP_TOP 160 JUMP_ABSOLUTE 134 15 >> 162 LOAD_CONST 38 (<code object <listcomp> at 0x7f704e8a4df0, file "snake.py", line 15>) 164 LOAD_CONST 37 ('<listcomp>') 166 MAKE_FUNCTION 0 168 LOAD_NAME 5 (zip) 170 LOAD_NAME 0 (input_list) 172 LOAD_NAME 2 (key_list) 174 CALL_FUNCTION 2 176 GET_ITER 178 CALL_FUNCTION 1 180 STORE_NAME 6 (result) 18 182 LOAD_CONST 39 ('') 184 LOAD_METHOD 7 (join) 186 LOAD_NAME 8 (map) 188 LOAD_NAME 9 (chr) 190 LOAD_NAME 6 (result) 192 CALL_FUNCTION 2 194 CALL_METHOD 1 196 STORE_NAME 10 (result_text) 198 LOAD_CONST 40 (None) 200 RETURN_VALUE Disassembly of <code object <listcomp> at 0x7f704e8a4d40, file "snake.py", line 9>: 9 0 BUILD_LIST 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 12 (to 18) 6 STORE_FAST 1 (char) 8 LOAD_GLOBAL 0 (ord) 10 LOAD_FAST 1 (char) 12 CALL_FUNCTION 1 14 LIST_APPEND 2 16 JUMP_ABSOLUTE 4 >> 18 RETURN_VALUE Disassembly of <code object <listcomp> at 0x7f704e8a4df0, file "snake.py", line 15>: 15 0 BUILD_LIST 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 16 (to 22) 6 UNPACK_SEQUENCE 2 8 STORE_FAST 1 (a) 10 STORE_FAST 2 (b) 12 LOAD_FAST 1 (a) 14 LOAD_FAST 2 (b) 16 BINARY_XOR 18 LIST_APPEND 2 20 JUMP_ABSOLUTE 4 >> 22 RETURN_VALUE ``` first we can see that it's assigning some integer values to the list `input_list`: ![image](https://hackmd.io/_uploads/S1ebnJjRp.png) then we have another string assigned to a variable `key_str` and that is `J_o3t`: ![image](https://hackmd.io/_uploads/ByF8nxjAa.png) Then it runs through every index of the input_list xorring it with the key. So basically what we have here is the encrypted output in decimal but to note is when recreating the key, the key is t_Jo3 now let's get to scripting: ```python value = [4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 106, 123, 89, 87, 18, 62, 47, 10, 78] key = "t_Jo3" key_list = [ord(char) for char in key] while len(key_list) < len(value): key_list.extend(key_list) res = [a ^ b for a, b in zip(value, key_list)] out = ''.join(map(chr, res)) print(out) ``` with the script above we will be able to get the flag: ![image](https://hackmd.io/_uploads/HJVbP-s0T.png) FLAG : picoCTF{N0t_sO_coNfus1ng_sn@ke_516dfaee} ## WinAntiDbg0x200 (300 Points) ### Description ![image](https://hackmd.io/_uploads/r1C_wZoCp.png) ### Solution Provided with another interesting windows anti-debug bypass challenge. This is the same as the first challenge except we have something new as they stated on the description. Let's get into digging! First thing is first we are required to run this executable as administrator! ![image](https://hackmd.io/_uploads/S1FpO-sCa.png) I first open it up in IDA, **REMEMBER TO OPEN IDA AS ADMINSTRATOR**. My next step is to understand how this program works, Well from what I am first seeing here it looks like if the executable is not run with admin privs it'll through the error we saw earlier and if it's run as administrator it'll run a function to create a mutex: ![image](https://hackmd.io/_uploads/BJUCt-jRa.png) Let's try to run the program and see what we end up getting on the output console: ![image](https://hackmd.io/_uploads/S1CL9WjAT.png) Looks like we are able to pass the mutex creation function as well but we ended up getting an error that debugger was detected now let's look for that string and see where it's called off from: ![image](https://hackmd.io/_uploads/HJY1sWsCT.png) Now that we know where our `IsDebuggerPresent` function call is at, we have another catch! before the `IsDebuggerPresent` function is even reached the function `local_41807` also has a `test` instruction and `jnz` instructon which jumps to print the output `Oops! Debugger was detected`. So let's set a breakpoint here: ![image](https://hackmd.io/_uploads/HkKanbsAa.png) then let's run our program and see what happens if we tweak the values of the **EDX** register: ![image](https://hackmd.io/_uploads/ryqZ6Zj0a.png) As seen above the value of **EDX** is 1 let's change that to 0 and our next call will be moved to the `IsDebuggerPresent` check and this returns us back to printing the error. So let's set a breakpoint at the `test` instruction for the **EAX** register and tweak the value of **EDX** to 0 and **EAX** to 0 then we see what we get as the output: ![image](https://hackmd.io/_uploads/BJrCp-s0T.png) And we got the flag! FLAG : picoCTF{0x200_debug_f0r_Win_c6db2768} ## WinAntiDbg0x300 (400 Points) ### Description ![image](https://hackmd.io/_uploads/BkncAbiRT.png) ### Solution I first try to load the executable in IDA , and I found out that it's compressed with UPX: ![image](https://hackmd.io/_uploads/HkfW-fjRp.png) I unpack it using the command ```shell upx -d WinAntiDbg0x300.exe -o original.exe ``` ![image](https://hackmd.io/_uploads/ryqoZMiRT.png) And then I load the new executable I decompressed: ![image](https://hackmd.io/_uploads/H1TA-fsCa.png) First thing first, I run the executable through the debugger to see what output I get from it: ![image](https://hackmd.io/_uploads/rkXY4xJ10.png) Now I find where this error is being called from: ![image](https://hackmd.io/_uploads/HkskBx1J0.png) this is interesting we can see that it hit that error after the `test eax,eax` instruction: ![image](https://hackmd.io/_uploads/BJzF8xy10.png) I set a breakpoint at the test instruction to see what the value of the **EAX** register becomes and then we modify it and then continue from the breakpoint to see what we get, but before that we first need to understand the logic flow of the program and what to expect. So let's start breaking down what's happening: ![image](https://hackmd.io/_uploads/ByDNZWJJC.png) Starting with number 1, this function `loc_4027C6` will perform some basic DLL calls then will call the `test eax,eax` instruction, then after that the **JZ** jump instruction is called which in our case is `jz short loc_402802` we know that **JZ** mnemonic stands for jump if zero so in this case if the value of **EAX** register is equal to 0 it'll jump to the function specified and that is **loc_402802** and the reason to why we end up getting the error "Oops!...." is because the value of the **EAX** register is equal to 1 and not 0 thus setting a breakpoint and the test instruction will allow us to modify to the right value for us to get the right jump. ![image](https://hackmd.io/_uploads/HkWSSWkkC.png) And as seen above if we do manage to jump into the function `loc_402802` the program will then create a thread after it creates the thread we can see it moves the value of **EAX** to **EBP** and then does a comparison to check if value of **EBP+hObject** is equal to 0 if it's True it'll perform the jump and go to the function `loc_402889` and if it's False it'll abort the challenge as seen below: ![image](https://hackmd.io/_uploads/r1KhwWyk0.png) Now I think we are noticing another useful place to set a breakpoint to and see what the values in case we end up hitting the "Error creating the thread...." error! Now let's get started with this, I first set a breakpoint at the `test` instruction from the function `loc_4027C6`: ![image](https://hackmd.io/_uploads/ByAXpZJ1A.png) Running it we can see that the value of **EAX** is equal to 1 changing that to 0, it terminated the program including IDA πŸ’€ . Now from what happened it seems like it failed to create a thread and terminated! I open IDA again and try to see how I work around with this again. I notice that we have another function known as `StartAddress` that is being called before creating a thread: ![image](https://hackmd.io/_uploads/H1zH8fyJ0.png) Opening that function up, it seems like this is where the magic actually happens and to get the flag we actually need to play around with this function: ![image](https://hackmd.io/_uploads/H1858Myy0.png) Taking a better look at this whole logic it's more of an infinite loop: ![image](https://hackmd.io/_uploads/Byr08GJy0.png) Let's start breaking down this function, so it all starts at the function `loc_4037C0` which calls the `test` instruction for **EAX** register and check if it's equal to zero then does a jump to `loc_4038E0`: ![image](https://hackmd.io/_uploads/SJfDufyk0.png) Let's take a look at what will happen if we do jump to `loc_4038E0`: ![image](https://hackmd.io/_uploads/SJtWqf1J0.png) As seen above, it'll end up calling the function `loc_403929` which will give us the flag! Well let's set a breakpoint at the test instruction from the function `loc_4037C0`, this means we have two breakpoints set out, the first breakpoint is on `loc_4027C6` and the second is on `loc_4037C0`: ![image](https://hackmd.io/_uploads/HJpdszkJ0.png) All we have to do is to modify the values of the register **EAX** to 0 so as the **JZ** instruction jumps us to the function to get the flag. Now I start debugging I run the executable and change the value of **EAX** from the first breakpoint to 0: ![image](https://hackmd.io/_uploads/BytlnGykR.png) Then I change the value of **EAX** from the second breakpoint: ![image](https://hackmd.io/_uploads/HkBN2zJyR.png) And I got the flag!!! ![image](https://hackmd.io/_uploads/SJD_nMy1A.png) FLAG : picoCTF{Wind0ws_antid3bg_0x300_09b94ee8} ## Thoughts! I enjoyed the CTF, the challenges were pretty straight up and actually needed some out of the box thinking to be solved! The reverse engineering challenges on the other hand were nice :) very nice challenges! --- <div style="width:100%;height:0;padding-bottom:100%;position:relative;"><iframe src="https://giphy.com/embed/DfbpTbQ9TvSX6" width="100%" height="100%" style="position:absolute" frameBorder="0" class="giphy-embed" allowFullScreen></iframe></div>