# Intro At 6-8 January 2024, i'm playing IrisCTF with Social Engineering Expert, and solved several Reverse Engineering challenges. Here are my writeup for challenge The Johnson's, Secure Computing, The Maze and CloudVM. # Writeup ## The Johnson's ![image](https://hackmd.io/_uploads/HyvCxcFua.png) To solve this challenge we need to guess each person's favorite color and food ### Reversing Decompile the binary using IDA #### Main ![image](https://hackmd.io/_uploads/HkVCEcKOT.png) ![image](https://hackmd.io/_uploads/BkFyHqKOa.png) In main function, the program will ask the user to input each person's favorite color and food. each person has unique favorite color and food, and there are only 4 options to choose from per category. Then Our string will be mapped into a number (red=>1 , blue=>2 , green=>3 , yellow=>4 , pizza=>1 , pasta=>2 , steak=>3 , chicken=>4) After that, check() function is called. #### Check ![image](https://hackmd.io/_uploads/BywqU5td6.png) To get the correct value, we need to make the if statement return false. that means: - v2 = True - color[0] != 3 - color[1] != 3 - v1 = True - v0 = True - food[2] != 2 - food[3] != 2 - color[1] != 1 - food[0] = 4 - food[3] != 3 - color[2] != 4 - color[3] = 2 ### Getting Flag After process of elimination, we can determine the correct order of food and color, which is food = [(~~1~~,~~2~~,~~3~~,4),(~~1~~,2,~~3~~,~~4~~),(~~1~~,~~2~~,3,~~4~~),(1,~~2~~,~~3~~,~~4~~)] = [4,2,3,1] = [chicken,pasta,steak,pizza] color = [(1,~~2~~,~~3~~,~~4~~),(~~1~~,~~2~~,~~3~~,4),(~~1~~,~~2~~,3,~~4~~),(~~1~~,2,~~3~~,~~4~~)] = [1,4,3,2] = [red,yellow,green,blue] Sending this to the server will give the flag ![image](https://hackmd.io/_uploads/r1T_o9Y_6.png) ``FLAG : irisctf{m0r3_th4n_0n3_l0g1c_puzzl3_h3r3}`` ## Secure Computing ![image](https://hackmd.io/_uploads/rkljnqFuT.png) We are given a challenge binary and the code snippet. from the code snippet, we know that the flag is irisctf{<48-bytes flag>} ### Reversing Decompile the binary in IDA Main function is the same as the code snippet. ![image](https://hackmd.io/_uploads/rJrwa9Kda.png) At the start function, we discover that there is an additional function call in init array ![image](https://hackmd.io/_uploads/S1fZA5KOp.png) ![image](https://hackmd.io/_uploads/r1cXC9FO6.png) At this function, seccomp syscall was called with SECCOMP_SET_MODE_FILTER flag. [More information about seccomp](https://www.kernel.org/doc/html/v4.19/userspace-api/seccomp_filter.html) ![image](https://hackmd.io/_uploads/BJFIkiKOp.png) Basically after adding a filter using seccomp, every syscall that we want to execute will be checked using this filter. because the while loop is executed 8 (64/8) times, that means there are 8 sets of filter being applied. Lets extract those filters. ![image](https://hackmd.io/_uploads/HkjD0ojuT.png) ![image](https://hackmd.io/_uploads/ByVORojuT.png) After extracting all of the filters, we can disassemble the filters by using [seccomp-tools](https://github.com/david942j/seccomp-tools) ```seccomp-tools disasm --no-bpf export_results1.txt > chunk1``` ![image](https://hackmd.io/_uploads/SJyhgiYdp.png) ![image](https://hackmd.io/_uploads/HJ5AAoj_p.png) There are 3 types of return value, but after reading the disassembly a little bit, we can assume that we need the filter to do return ERRNO(0). ### Gettting Flag Looks like inserting these equations into Z3 is possible, but we need to know several static values - sys_number = 0x1337 - X = 0 - arch = 0xc000003e ([const for x86_64](https://github.com/david942j/seccomp-tools/blob/2c51ad9a1be7b6c00675289cd9868b088b81d362/lib/seccomp-tools/const.rb#L153)) Solve script ```python= from z3 import * import re s=Solver() for numfile in range(1,9): #seccomp-tools disasm --no-bpf export_results{i}.txt > chunk{i} x=open(f'chunk{numfile}').readlines() stop_line=len(x) mem=[BitVecVal(0,32) for i in range(16)] X=BitVecVal(0,32) A=BitVecVal(0x1337,32) arch=BitVecVal(0xc000003e,32) sys_number=BitVecVal(0x1337,32) reg=r'([0-9]+?)\)' reg_hex=r'(0x[0-9a-f]+?)\)' reg_first_part='A = (args\[.+?\]) >> 32' reg_second_part='A = (args\[.+?\])' args=[BitVec(f"x{i}",64) for i in range(6)] for i in args: for j in range(0,64,8): s.add(Extract(j+7,j,i)>=0x20) s.add(Extract(j+7,j,i)<0x7f) for i in range(3,stop_line): num,cmd=x[i].strip().split(': ') if("KILL" in cmd or "ERRNO(0)" in cmd): continue if("A = args" in cmd): cmd=re.sub(reg_first_part,"A = Extract(63,32,\\1)",cmd) cmd=re.sub(reg_second_part,"A = Extract(31,0,\\1)",cmd) if("goto" in cmd): try: val=eval(re.findall(reg,cmd)[0]) except Exception as e : val=eval(re.findall(reg_hex,cmd)[0]) s.add(A==val) continue exec(cmd) #simplifying the equation if(type(A)==BitVecRef): A=simplify(A) else: A=A%2**32 if(type(X)==BitVecRef): X=simplify(X) else: X=X%2**32 print(s.check()) m=s.model() for i in args: print(int.to_bytes(m[i].as_long(),8,"little").decode(),end='') ``` ![image](https://hackmd.io/_uploads/SJ9NzstOT.png) ``FLAG : irisctf{1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_1nstruct10ns!}`` ## The Maze ![image](https://hackmd.io/_uploads/rJboXoKOT.png) We need to somehow get flag from this maze game. After viewing the html source, we can see that one .js file is used to run the game. ![image](https://hackmd.io/_uploads/ByPeVsFda.png) ![image](https://hackmd.io/_uploads/Sk-84sKup.png) The code snippet looks intimidating, but the most interesting part is in function y(), because it looks like the function is doing a decryption process. Its possible that we can get the flag by decrypting some data through this function. Our analysis will be focusing on this function. To analyze it, we can debug the javascript by adding a breakpoint in the javascript code then observe the variable value, or try to call some functions manually. ![image](https://hackmd.io/_uploads/HkjUNhF_6.png) ![image](https://hackmd.io/_uploads/ryW2dhtda.png) ### Reversing #### global variable init These are the variable that is initialized at the start of the game ![image](https://hackmd.io/_uploads/rk-Er2td6.png) #### y() If we put a breakpoint on the first line after the if statement, it will be called everytime we try to move a step in the maze. ![image](https://hackmd.io/_uploads/HkjUNhF_6.png) Lets see the most interesting part first, the decryption process ![image](https://hackmd.io/_uploads/SJ1qwjtd6.png) This code snippet is doing a one-liner to process data and call a function to decrypt something. To understand this code, we need to understand how the one-liner code works. ``` <operation-1> && <operation-2> -> only execute operation-2 if operation-1 is true <operation-1> || <operation-2> -> only execute operation-2 if operation-1 is false <operation-1> , <operation-2> -> execute operation-2 after operation-1 ``` ![image](https://hackmd.io/_uploads/HyaXznF_T.png) After understanding how the one-liner works, this is the deobfuscated version. ![image](https://hackmd.io/_uploads/r1P-EnF_a.png) After debugging the code, this is what we found. ``` i.g -> (x,y) position of player i.f -> maze metadata (maze size, grid information ) i.b -> maze level f(N) -> function to generate level N maze n (local var) -> iv e (local var) -> base64 encoded secret t, r, c (local var) -> key i.e -> unknown (needed to generate key) a (local var) -> base64 decoded secret i.l -> string that will be displayed to the screen ``` To successfully call the decrypt function, we must: - finish 4 levels of maze by reaching the bottom right of the maze - i.e size is 4 and all value in i.e is not -1 The only unknown value is in **i.e**. Lets check how the array is generated. ![image](https://hackmd.io/_uploads/HJPKtnFda.png) **i.e** is generated by using another one-liner code. Lets make this more readable. ![image](https://hackmd.io/_uploads/HJ-26hYOT.png) Looks like for every step we take, the program will update **i.k** value using the player's current (x,y) coordinate, and if **i.i** length is 16 and **i.j** is 1, some kind of process is done to an array in **t**. the processed data is used to determine what value to push into **i.e**, **i.k** if succesful and -1 if not. Lets find out what process is happening once the if statement is true ![image](https://hackmd.io/_uploads/rJOeGTtOT.png) Turns out that series of operations were a matrix multiplication of **i.i** with a matrix in **a**[**i.b**]. To return true, the multiplication result must result in identity matrix, which means we just need to inverse the matrix in **a**[**i.b**] to get the correct value for **i.i** for each level. ### Getting Flag we will use sage to do the matrix operations ![image](https://hackmd.io/_uploads/BkJh7aYdT.png) ![image](https://hackmd.io/_uploads/rJ1CQaYup.png) We now have the value of the correct **i.i** for each level, now we just need to translate this into a maze movement to generate **i.k** ![image](https://hackmd.io/_uploads/BJL6E6K_a.png) **i.d[i.c.findIndex((e => 1 === e))]** is our step direction in the maze translated into a value. we can see what value is each direction by adding a code to console.log() the value. ![image](https://hackmd.io/_uploads/Hyz6U6tua.png) ![image](https://hackmd.io/_uploads/S1I2HaYd6.png) ``` down -> 40 & 3 = 00 right -> 39 & 3 = 11 left -> 37 & 3 = 01 up -> 38 & 3 = 10 ``` The program will take the first 2 bits of each direction value and append it to **i.j**. if 4 moves are completed (8 bits), it will be pushed into **i.i** after subtracting it with 32. So we just need to convert the matrix we inversed before into binary representation, split it into 2 bits, then translate it into movement direction. From the movement direction, we can generate each coordinate that the player visited, which results in the correct **i.k** value for each level. We now have the complete **i.e** ```python= from sage.all import * keys=[ [[-0.035712653158569425, -0.03287257405769594, 0.02334270751557671, -0.02540569009549741], [0.006411543162643718, 0.02983176158440242, 0.02174280291361509, 0.0067315240830360425], [-0.010545458159016606, -0.00814858244477348, -0.042581311921773606, 0.020546352515626396], [-0.033596009182061196, -0.000705547992169411, 0.02680088640677326, -0.051391718257793324]], [[0.17638922081966382, 0.09011377379943657, -0.2507513705723452, -0.14779730737755375], [0.055901970582484195, 0.015708171290764118, -0.08718795489603928, -0.034523214634888215], [-0.08558020292437048, -0.07923610054967736, 0.17700480152953701, 0.08977339387750669], [-0.1294747286013282, -0.05801667137404857, 0.20491595512778732, 0.13903433491935893]], [[-1.4436222005842259, 0.33729308666017527, 0.5668938656280429, -1.2835443037974683], [5.040019474196689, -1.136222005842259, -1.895520934761441, 4.410126582278481], [2.833203505355404, -0.6585199610516066, -1.0631937682570594, 2.4658227848101264], [-1.0833495618305744, 0.23018500486854918, 0.4296007789678676, -0.9417721518987342]], [[-0.30391992395012024, 0.37225297768830734, 0.10770005032712632, 0.18833529049935693], [0.2496784655818375, -0.27545713806408323, -0.055359838953195774, -0.15288262595761337], [-0.027456243359615277, 0.10879047139741654, 0.055359838953195774, 0.09732707040205782], [0.20058155790415477, -0.2713470894145278, -0.11726220432813286, -0.1713079460940558]] ] def hass(movement): start=[-7,-7] ik=0 for move in movement: if(move=="down"): start[1]+=1 elif(move=="up"): start[1]-=1 elif(move=="right"): start[0]+=1 elif(move=="left"): start[0]-=1 ik = ik + 211 * (start[0] + 9) * (start[1] + 9) * 239 & 4294967295 return ik for r in keys: key=Matrix(r) ans=key.inverse() ans=list(ans.apply_map(lambda x: int(bin(round(x)+32)[2:]))) ans=''.join(list(map(lambda x: ''.join([str(i).zfill(6) for i in x]) ,ans))) ans=[['down','left','up','right'][int(ans[i:i+2],2)] for i in range(0,len(ans),2)] print(hass(ans),end=',') ``` use this to decrypt the secret ```js= async function aesGcmDecrypt(key, ciphertext, nonce) { const algorithm = { name: 'AES-GCM', iv: nonce}; const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']); const decryptedData = await crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext); return new Uint8Array(decryptedData); } cipher="Dugd8DbBCXnrEF1kKd2Hg4lsRQ1eV/6gQ+NfwsVhtr4UgeXQFq1m6WctmIljEG7PZg=="; aa = new Uint8Array(atob(cipher).split("").map((e => e.charCodeAt(0)))); nn = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); w=[40645774,130661539,116339703,150379278].map((e => e.toString(16).padStart(8, "0"))).join("") key=(new TextEncoder).encode(w) aesGcmDecrypt(key,aa,nn).then(e => console.log(new TextDecoder().decode(e))) ``` `FLAG: irisctf{thankfully_no_ghost_girl}` ## CloudVM We are only given a VM bytecode and a service that executes those bytecodes. ![image](https://hackmd.io/_uploads/Syq3satdT.png) ![image](https://hackmd.io/_uploads/Hye-J0F_6.png) ### Discovering Opcode Thankfully, we can debug the opcodes because the service will dump the register and memory content, and also the reason why the code fails, such as invalid opcode or memory access error if an error in emulation occurs. ![image](https://hackmd.io/_uploads/HJ-u2Tt_p.png) After some trial and errors while finding out how the opcode works, most of the opcode used in michaelpaint.bin has been disassembled using the following code ```python= x=open('michaelpaint.bin','rb').read() idx=0 header=x[:idx+4];idx+=4 function_num=int.from_bytes(x[idx:idx+4],"little");idx+=4 addr2fun={} fun2addr={} for i in range(function_num): addr=int.from_bytes(x[idx:idx+4],"little");idx+=4 fun_name_len=int.from_bytes(x[idx:idx+2],"little");idx+=2 fun_name=x[idx:idx+fun_name_len];idx+=fun_name_len addr2fun[addr]=fun_name.decode() fun2addr[fun_name.decode()]=addr #read_strings addr2string={} string2addr={} while(idx!=fun2addr["main"]): addr=idx str_len=int.from_bytes(x[idx:idx+2],"little");idx+=2 str_val=x[idx:idx+str_len];idx+=str_len addr2string[addr]=str_val.decode() string2addr[str_val.decode()]=addr while idx!=len(x): if(idx in addr2fun.keys()): print("=== "+addr2fun[idx]+" ===") print(str(idx).ljust(6)+":",end='') if(x[idx]==0xc0): r_x=x[idx+1] r_y=x[idx+2] print(f"reg[{r_x}]=reg[{r_y}]") idx+=3 elif(x[idx]==0xc1): r_x=x[idx+1] val=int.from_bytes(x[idx+2:idx+2+4],'little') print(f"reg[{r_x}]={val}") idx+=7-1 elif(x[idx]==0xc2): r_x=x[idx+1] print(f"reg[8] += reg[{r_x}]") idx+=2 elif(x[idx]==0xc3): r_x=x[idx+1] print(f"reg[8] -= reg[{r_x}]") idx+=2 elif(x[idx]==0xc4): r_x=x[idx+1] print(f"reg[8] *= reg[{r_x}]") idx+=2 elif(x[idx]==0xc5): r_x=x[idx+1] print(f"reg[7] = reg[8] % reg[{r_x}]") idx+=2 elif(x[idx]==0xc6): r_x=x[idx+1] print(f"reg[8] &= reg[{r_x}]") idx+=2 elif(x[idx]==0xc7): r_x=x[idx+1] print(f"reg[8] |= reg[{r_x}]") idx+=2 elif(x[idx]==0xc8): r_x=x[idx+1] print(f"reg[8] ^= reg[{r_x}]") idx+=2 elif(x[idx]==0xc9): r_x=x[idx+1] print(f"reg[8] <<= reg[{r_x}]") idx+=2 elif(x[idx]==0xca): r_x=x[idx+1] print(f"reg[8] >>= reg[{r_x}]") idx+=2 elif(x[idx]==0xD0): addr=int.from_bytes(x[idx+1:idx+1+4],'little') print(f"print({repr(addr2string[addr])})") idx+=5 elif(x[idx]==0xD3): r_x=x[idx+1] r_y=x[idx+2] print(f"mem[reg[{r_x}]:reg[{r_x}]+reg[{r_y}]]] = bytearray(input().strip()[:reg[{r_y}]].encode())") idx+=3 elif(x[idx]==0xD4): r_x=x[idx+1] print(f"A = readint(mem[{r_x}])") idx+=2 elif(x[idx]==0xD5): r_x=x[idx+1] print(f"unknown {hex(x[idx])} {hex(x[idx+1])}") idx+=2 elif(x[idx]==0xE2): addr=int.from_bytes(x[idx+1:idx+1+4],'little') r_x=x[idx+1+4] print(f"cmp reg[{r_x}],reg[8] ; je {addr}") idx+=6 elif(x[idx]==0xE0): addr=int.from_bytes(x[idx+1:idx+1+4],'little') print(f"jmp {addr}") idx+=5 elif(x[idx]==0xE1): addr=int.from_bytes(x[idx+1:idx+1+4],'little') r_x=x[idx+1+4] print(f"cmp reg[{r_x}],reg[8] ; jne {addr}") idx+=6 elif(x[idx]==0xE5): addr=int.from_bytes(x[idx+1:idx+1+4],'little') r_x=x[idx+1+4] print(f"cmp reg[{r_x}],reg[8] ; jle {addr}") idx+=6 elif(x[idx]==0xF0): straddr=int.from_bytes(x[idx+1:idx+1+4],'little') fun_name=addr2string[straddr] print(f"call {fun_name}") idx+=5 elif(x[idx]==0xF2 or x[idx]==0xF1): print(f"return {hex(x[idx])}") idx+=1 else: print(x[idx:idx+6].hex().upper()) break ``` one of the string said that the flag content is the name of the object painted in the canvas ![image](https://hackmd.io/_uploads/r1JV0pYu6.png) ### Breaking Down Functions #### main The main function ![image](https://hackmd.io/_uploads/SJrjxAFdT.png) #### paint Function to do painting activity ![image](https://hackmd.io/_uploads/SJKTe0Yua.png) #### render displaying color to the screen based on value in memory ![image](https://hackmd.io/_uploads/H1DQZCFdp.png) #### suSsY Interesting function that is used to determine if the image is correct or not. ![image](https://hackmd.io/_uploads/S1wSbCY_p.png) ![image](https://hackmd.io/_uploads/B1cDbRK_p.png) #### SUssY contains operation that calculates a value using reg[0] and reg[1] ![image](https://hackmd.io/_uploads/rJV0WRKua.png) ### Reversing we need to know how the memory is translated into color. For that we need to look at **render** function ![image](https://hackmd.io/_uploads/H1vXrRt_p.png) there are 8 colors to choose from, and each color is mapped to number 0-7. that means the only possible byte value in the memory is from 0b000 to 0b111. ### Getting Flag to get the correct value, we need to satisfy all conditions in suSsY function. we can use z3 to solve this. ```python= from z3 import * def get_val(x,res): reg=[0 for i in range(8)] reg[1]=x A = BitVec("A",32) reg[2]=A reg[3]=8 reg[4]=A reg[5]=A A=reg[2] reg[3]=12 A >>= reg[3] reg[6]=A reg[7]=A reg[3]=255 A=reg[4] A &= reg[3] reg[4]=A A=reg[6] A &= reg[3] reg[6]=A reg[3]=65280 A=reg[5] A &= reg[3] reg[5]=A A=reg[7] A &= reg[3] reg[7]=A A=0 A |= reg[4] A |= reg[5] A |= reg[6] A |= reg[7] A ^= reg[1] s=Solver() s.add(A==res) s.add(Extract(7,0,BitVec("A",32))<0b1000) s.add(Extract(15,8,BitVec("A",32))<0b1000) s.add(Extract(23,16,BitVec("A",32))<0b1000) s.add(Extract(31,24,BitVec("A",32))<0b1000) s.check() return bytearray(int.to_bytes(s.model()[BitVec("A",32)].as_long(),4,"little")) image=[0 for i in range(512)] image=bytearray(image) iddx=35 key=34216 res=38072 image[iddx:iddx+4]=get_val(key,res) iddx=39 key=57981 res=62316 image[iddx:iddx+4]=get_val(key,res) iddx=43 key=22478 res=22223 image[iddx:iddx+4]=get_val(key,res) iddx=51 key=6088 res=1753 image[iddx:iddx+4]=get_val(key,res) iddx=55 key=5918 res=1551 image[iddx:iddx+4]=get_val(key,res) . . . <SNIP> . . . iddx=243 key=62403 res=62403 image[iddx:iddx+4]=get_val(key,res) iddx=247 key=43710 res=43710 image[iddx:iddx+4]=get_val(key,res) iddx=251 key=46028 res=46028 image[iddx:iddx+4]=get_val(key,res) iddx=259 key=9729 res=9729 image[iddx:iddx+4]=get_val(key,res) iddx=263 key=49732 res=49732 image[iddx:iddx+4]=get_val(key,res) iddx=267 key=15739 res=15739 image[iddx:iddx+4]=get_val(key,res) color=[ '\x1b[40m ', '\x1b[41m ', '\x1b[42m ', '\x1b[43m ', '\x1b[44m ', '\x1b[45m ', '\x1b[46m ', '\x1b[47m ' ] res=['\x1b[40m ' for i in range(512)] for i in range(len(image)): res[i]=color[image[i]] for i,data in enumerate(res[:256]): if(i%16==0): print('\x1b[0m ') print(data,end='') ``` ![image](https://hackmd.io/_uploads/HJb8DAY_6.png) `FLAG : irisctf{gameboy}`