Basically the program receive input then it will validate the input. In this case we can't directly see the algorithm since the program call the function by jumping to some address. So, to get the correct algorithm one of the way we can do is debugging. Run the program then press ctrl+c
Use bt command to see the backtrace
Set breakpoint on 0x555555555203 then run again the program
Set hardware breakpoint on address that store our input. Then continue
So our input length should be 0x1d . Set breakpoint on 0x5555555554f4 then rerun the program with input that has length 0x1d . Step in to next instruction until found interesting instruction such as xoring our value with index (0x555555555453) and with static value 0x55 (0x55555555543b)
After calculated the input program will xored again with value on 0x55555555540b. r9d register filled with value from address on $r10+$rdi with $rdi == index.
Dump the values using command below
print-format--length0x1d--bitlen8 $r10
Continue the flow we will see instruction that compare our calculated input on 0x55555555557c . Dump the values then write script to get the flag, since the program only do xor so we can directly implement the same process to get the actual input (flag)
buf1 = [0xf6,0xf5,0x31,0xc8,0x81,0x15,0x14,0x68,0xf6,0x35,0xe5,0x3e,0x82,0x9,0xca,0xf1,0x8a,0xa9,0xdf,0xdf,0x33,0x2a,0x6d,0x81,0xf5,0xa6,0x85,0xdf,0x17]buf2 = [0xf0,0xe4,0x25,0xdd,0x9f,0xb,0x3c,0x50,0xde,0x4,0xca,0x3f,0xaf,0x30,0xf3,0xc7,0xaa,0xb2,0xfd,0xef,0x17,0x18,0x57,0xb4,0xd0,0x8f,0xb8,0xf4,0x23]flag =b""for i inrange(len(buf1)): flag +=bytes([buf1[i]^buf2[i]^0x55^i])print(flag)
Flag : SECCON{jump_table_everywhere}
Sickle (106 pts)
Description
Pickle infected with COVID-19
Solution
Given python script that load pickle data (serialized payload). Actually i've tried pickle reverse engineering challenge on LACTF. We can decompile pickle data using fickling https://github.com/trailofbits/fickling. But somehow in this case we can't decompile the data and got error.
To know the next decompiled code, i rewrite the library and add some line of code to skip the error and continue the decompile process. Open the source code, in my machine the code stored in /Users/kosong/.pyenv/versions/3.11.2/lib/python3.11/site-packages/fickling/pickle.py
So i add new variable tmp_val to store all pushed object and when there is error caused by pop from empty stack i just pop some random object from fake stack (tmp_val). Run the fickling again and we will get pseudocode of pickle data.
Although the produced code is not correct but we still guess some part of the code. Here is recovered algorithm we found
Our input should be 64 bytes length
Something sliced each 8 byte then converted to int
Something calculated using xor
RSA algorithm found with prime modulus
pow(something, 65537, 18446744073709551557)
Something compared with static value on array
So the first step i do is decrypting the known value using RSA Algorithm
from Crypto.Util.number import*ct = [8215359690687096682,1862662588367509514,8350772864914849965,11616510986494699232,3711648467207374797,9722127090168848805,16780197523811627561,18138828537077112905]p =18446744073709551557d =inverse(65537, p-1)print(long_to_bytes(pow(ct[0], d, p)))
We can't get the correct output, so the next step i do is replacing the builtins pow to know the actual value processed by the pow function. Here is the flow i used
Change pow to eval since eval not used and to keep pow function valid
change pow stirng to eval string
change 03 to 04 (length of next data)
Overwrite eval function to custom function to do pow and print value
Value processed on pow is 5982845580366531443 and our input should be 4774451407313060418 so there is algorithm that process our input. Using previous information i try to xor the processed value on pow with actual "BBBBBBBB" value and got 1244422970072434993. Xoring our decrypted output from RSA before, we got the part of flag.
Decrypting the next value using the same flow and value we got invalid output. So there is something wrong with the static value (xor part). Through previous output (from nice.py), we can analyze what is the actual xor value.
So basically the actual xor value for next process is the ciphertext value from RSA. Using this information i write the script to get the whole flag
Flag : SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
Perfect Blu (135 pts)
Description
No, I'm real!
Solution
Given ISO file, open it using VLC media viewer
So basically the program is flag checker and we can click on screen to give input. To get the code behind that ISO we try to findout what tools can be used. Then we found https://bdedit.pel.hu/ . Double click on ISO will automatically mounted it on windows. Load the mounted ISO on BDedit
Exploring the tools, we found out one of the way to get the logic. Click on CLIPINF tab then choose sequence under zzzzz.clpi then click "Read". After that double click on Stream Type IG on ProgramInfo.
We can see that there are many ID and all of the ID represent object on program for example button.
Deeping dive into each value on each id we get some valuable information like on table below
Key
Description
Opcode
Hexadecimal representation of machine code
Command Line
Instruction translation from opcode
X
Horizontal coordinate of object
Y
Vertical coordinate of object
Looking at Command Line value we got something weird, one of the object has different call object. For 00000.clpi , ID 21 has different value. Since we know the layout of virtual keyboard we can determine which object on ID 21.
list_y = [413,543,672,802]list_x = [315,453,590,727,865,1002,1139,1277,1414,1551]list_alphabet ="1234567890qwertyuiopasdfghjkl{zxcvbnm_-}".upper()dicti ={}k =0for i in list_y: tmp_dicti ={}for j in list_x: tmp_dicti[j]= list_alphabet[k] k +=1 dicti[i]= tmp_dictiprint(dicti[672][453])
Output from above code is S and looks like legit since we know the flag format is SECCON{}. So the next step we do is writing script to automatically parsing which button that has different call object and translate it to keyboard layout. We can using find function to validate which file that really contains opcode.
f =open("00000.m2ts", "rb").read()x_val =b"\x01\x3b"y_val =b"\x01\x9d"obj =b"\x21\x82\x00\x00"print(f.index(x_val))print(f.index(y_val))print(f.index(obj))
Okay, now last step just implement parsing for all *m2ts file, get different call object, and translate it. Here my implementation in python
from Crypto.Util.number import*deffind(target,data): list_i = []for i inrange(0, len(data)-len(target)):if(data[i:i+len(target)]== target): list_i.append(i-len(target))return list_iobj =b"\x21\x82\x00\x00"list_y = [413,543,672,802]list_x = [315,453,590,727,865,1002,1139,1277,1414,1551]list_alphabet ="1234567890qwertyuiopasdfghjkl{zxcvbnm_-}".upper()dicti ={}k =0for i in list_y: tmp_dicti ={}for j in list_x: tmp_dicti[j]= list_alphabet[k] k +=1 dicti[i]= tmp_dictiflaggg =""for num inrange(90): tmp_num =str(num).rjust(5, "0") f =open(f"{tmp_num}.m2ts", "rb").read() done =Falsefor y inrange(4): y_val =long_to_bytes(list_y[y]) arr_y =find(y_val, f)for i inrange(10): ind = arr_y[i] data = f[ind:ind+94]for j in list_x: val =long_to_bytes(j)if(val in data):if(obj in data): tmp_ind = data.index(obj) res =bytes_to_long(data[tmp_ind+4:tmp_ind+8])if(res == (num+1)): flaggg += dicti[list_y[y]][j]print(flaggg) done =Truebreakif(done):breakif(done):breakif(done):breakif(done):breakprint(flaggg)
Looks like there is missing one character on 5EL part, during the competition i just validate it manually by using BDedit to find out which object that different and i found that the missing part should be 85EL
Flag : SECCON{JWBH-85EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV}
optinimize (152 pts)
Description
Nim is good at bignum arithmetic.
Solution
TBU
import sympyimport stringpreprocess_value = [0x4A,0x55,0x6F,0x79,0x80,0x95,0x0AE,0x0BF,0x0C7,0x0D5,0x306,0x1AC8,0x24BA,0x3D00,0x4301,0x5626,0x6AD9,0x7103,0x901B,0x9E03,0x1E5FB6,0x26F764,0x30BD9E,0x407678,0x5B173B,0x6FE3B1,0x78EF25,0x858E5F,0x98C639,0x0AD6AF6,0x1080096,0x18E08CD,0x1BB6107,0x1F50FF1,0x25C6327,0x2A971B6,0x2D68493,0x362F0C0,0x3788EAD,0x3CAA8ED] xor_keys =[0x3C,0x0F4,0x1A,0x0D0,0x8A,0x17,0x7C,0x4C,0x0DF,0x21,0x0DF,0x0B0,0x12,0x0B8,0x4E,0x0FA,0x0D9,0x2D,0x66,0x0FA,0x0D4,0x95,0x0F0,0x66,0x6D,0x0CE,0x69,0x0,0x7D,0x95,0x0EA,0x0D9,0x0A,0x0EB,0x27,0x63,0x75,0x11,0x37,0xD4]keys = [367,433,601,659,709,857,1031,1151,1213,1301,5869,69001,97829,171403,189817,250057,316919,336683,439409,486053,32290399,42106697,53430277,71926391,103839599,129151843,140251081,155813837,179656963,205472089,320518241,494588603,554258797,630614177,768532223,872167577,933088907,1124023861,1153586989,1266126107]flag = []for i inrange(len(keys)): dicti ={} tmp = keys[i]for j inrange(21): tmp2 = (tmp&0xff)^xor_keys[i] tmp = sympy.prevprime(tmp)if(chr(tmp2)in string.printable[:-6]): dicti[j]=chr(tmp2) flag.append(dicti)# guess# for i in flag: # print(i)list_char ="0123456789abcdef"last_j =-1flagg =""for dicti2 in flag[7:-1]:for j in dicti2:if(dicti2[j]in list_char):if(last_j <= j): last_j = j flagg += dicti2[j]breakprint(flagg)
So basically the program encrypt our input then compared it with static values. My approach is reconstructing the whole encryption process. The first step i do is reconstructing the function inside encrypt function.
encrypt_block function call r function and inside r function there are some nested function called tnls and els. During the competition i wrote helper script to accelerate my process. Here is script i used to automate debugging process
#!/usr/bin/python3import stringclassSolverEquation(gdb.Command):def__init__ (self):super(SolverEquation, self).__init__ ("solve-equation",gdb.COMMAND_OBSCURE)definvoke (self,arg,from_tty): f =open("x.txt","w")# f.write("B"*16)# f.write("A"*12 + "B"*4)# f.write("B"*16)# f.write("A"*16)# f.write("A"*16) f.write("Q"*16) f.close() gdb.execute("pie del")# gdb.execute("pie b 0x1a6a") # xor# gdb.execute("pie b 0x1b8f") # mov# gdb.execute("pie b 0x2256")# gdb.execute("pie run < x.txt")# arch = gdb.selected_frame().architecture()# data = []# for i in range(32):# tmp = gdb.execute("x/wx 0x00007ffff7fb9000", to_string=True)# data.append(hex(parse(tmp)[0]))# gdb.execute("c")# print(data) gdb.execute("pie b 0x1e96") gdb.execute("pie run < x.txt") data_eax = [] data_edx = []for i inrange(31): eax =addr2num(gdb.selected_frame().read_register("eax"))# tmp = gdb.execute("x/wx $rdi+$r13", to_string=True)# tmp = gdb.execute("x/wx $r8+$rdx", to_string=True) data_eax.append((eax))# data_edx.append((parse(tmp)[0])) gdb.execute("c")print(data_eax)# print(data_edx)defparse(f): f = f.split("\n") result = []for i in f: tmp = i.split("\t")for j inrange(1,len(tmp)): result.append(int(tmp[j],16))return resultdefaddr2num(addr):try:returnint(addr)&0xffffffff# Python 3except:returnlong(addr)# Python 2SolverEquation()
The summary of each function are described in table below
Function name
Description
encrypt_block
process our input each 16 bytes (4 blocks of 4 bytes). Do xor and call function
r
call tnsl then els function
tnls
convert input to sbox values
els
process sbox values with rol and xor
Below is reconstructed encryption algorithm in python
ct we got for each block are last 16 bytes value in result variable. In this case we can reverse the whole process (create decrypt function). Below is the flow of decrypt function
# i = 0# encrypt, process 16 bytetmp = inp_block[2]^inp_block[1]^inp_block[3]^static_buf[0]res =r(tmp)^inp_block[0]inp_block.append(res)# res == inp_block[4]ct = inp_block[-4:]# get last 16 bytes# decrypt, process 16 bytesinp_block = cttmp = inp_block[0]^inp_block[1]^inp_block[2]^static_buf[0]pt = inp_block[3]^r(tmp)# get inp_block[0] in encrypt function
So we just need to iterate 32 times until get the plaintext. Here is the final script to get the flag using decrypt function