## Writeup KMACTF 2025 - Một giải khá nhiều tiếc nuối của mình... ### 1. Mobius ![Mobius](https://hackmd.io/_uploads/Sy3tC5xZxe.png) - Giải có 3 bài và đây là bài dễ nhất, nhưng do áp lực phòng thi và ngáo nên mình không thể solve. Cơ bản thì mobius là cái dải khá quen thuộc trong toán A2, cũng là ý tưởng của author khi hướng giải sẽ là `.exe` sang `.pyc` sang `.py` rồi lại `.pyc`: ![image](https://hackmd.io/_uploads/HyrB09eble.png) - Icon của file `.exe` khá bủh: ![image](https://hackmd.io/_uploads/HkCCyjeZgg.png) - Chương trình là một flagchecker: ![image](https://hackmd.io/_uploads/SkOgrslWel.png) - Ban đầu thì mình không biết đây là file được sinh bởi PyInstaller nên đem đi reverse, sau mới được hint để không đi vào ngõ cụt :v. Sau giải thì mình có tìm hiểu được một số cách để nhận biết `PyInstaller` bằng `binwalk` hay `strings` như sau: ![image](https://hackmd.io/_uploads/B112eieWlx.png) - Vì là file `.exe` được tạo bởi `PyInstaller`, mình sẽ dùng `pyinstxtractor` để lôi contents của file python gốc ra: ![image](https://hackmd.io/_uploads/ry44Wogblx.png) - Tại đây thì chương trình xác định được `proc.pyc` có thể là entry point của file gốc nên mình đem lên [PyLingual](https://www.pylingual.io/) để decompile, kết quả ở [đây](https://www.pylingual.io/view_chimera?identifier=70df89afa7215585504e0c56ad1282d67869612b694623a7117d2b20a6804395) - Về cơ bản, file có dạng: ```python= import marshal from base64 import b64decode as _6d unmarshaled = "..." exec(marshal.loads(_6d(unmarshaled))) ``` `unmarshaled` khá dài nên mình không paste lên. Author đã làm thêm một bước nữa gây khó khăn cho việc reverse, vì code `marshal` này khá khó để dịch lại thành file `py` mà chỉ có thể biến nó thành file Python Bytecode. Mình thấy `marshal` là một thư viện không ngon lắm :v. - Tại đây, trong lúc thi mình đã thực hiện hai hướng đi, một là đưa code này về bytecode để đọc, hai là patch thêm header của `.pyc` vào file binary rồi đem lên `PyLingual` decompile lần nữa. #### Bytecode - Hướng này thì khá đơn giản, chỉ cần thực hiện như sau: ```python= from base64 import b64decode import marshal import dis unmarshaled = "..." code_obj = marshal.loads(b64decode(unmarshaled)) dis.dis(code_obj) ``` - Và run: ![image](https://hackmd.io/_uploads/HyVuQsx-gl.png) - Được: ![image](https://hackmd.io/_uploads/rydqXieZee.png) ... #### pyc - Với hướng này thì mình mượn header của `proc.py` phía trên để patch: ```python= with open("proc.pyc", "rb") as f: data = f.read() header_pyc = data[:data.index(b"\xe3")] data = b64decode(unmarshaled) data = header_pyc + data open("patched_proc.pyc", "wb").write(data) ``` - Đem lên `PyLingual`, kết quả ở [đây](https://www.pylingual.io/view_chimera?identifier=3e02a5f74398b288e066127adc681f0e34a74b6caa873b9985d8a99cabd86889), được code: ![image](https://hackmd.io/_uploads/rJXKVjxWex.png) - Tuy nhiên do có vài syntax errors và data không được decompile hết nên mình đã dựa vào bytecode để sửa lại chút, cuối cùng dịch được file (tạm gọi là khá sát với gốc) như sau: ```python= # Decompiled with PyLingual (https://pylingual.io) # Internal filename: proc.py # Bytecode version: 3.11a7e (3495) # Source timestamp: 1970-01-01 00:00:00 UTC (0) from sys import exit class stack: def __init__(self, max_size=4294967295): self.stack = list() self.max_size = max_size self.size = len(self.stack) def push(self, value): if self.size < self.max_size: self.stack.append(value) self.update_size() def pop(self): if self.size > 0: self.stack.pop() self.update_size() def update_size(self): self.size = len(self.stack) FM = {'zero': 1} class register: def __init__(self): self._1 = 0 self._2 = 0 self._3 = 0 self._4 = 0 self.register = (self._1, self._2, self._3, self._4) self.fs = 0 def get(self, ix): if ix == 0: value = self._1 else: # inserted if ix == 1: value = self._2 else: # inserted if ix == 2: value = self._3 else: # inserted if ix == 3: value = self._4 return value def set(self, ix, value): if ix == 0: self._1 = value else: # inserted if ix == 1: self._2 = value else: # inserted if ix == 2: self._3 = value else: # inserted if ix == 3: self._4 = value self.register = (self._1, self._2, self._3, self._4) def set_flag_zero(self, value): if value == True: self.fs = FM['zero'] else: # inserted self.fs = FM['zero'] self.fs = FM['zero'] def mov(vm, opcode): target = (opcode, vm.size | 2, 4) & 15 if target < 0 or target >= len(vm.register.register): exit(0) ir = (opcode, vm.size | 3, 4) & 15 if ir == 0: value = (opcode | (16, vm.size + 3)) & 1 else: # inserted if ir == 1: ix = (opcode, vm.size | 4, 4) & 15 if ix < 0 or ix >= len(vm.register.register): exit(0) value = vm.register.get(ix) else: # inserted if ir!= 0 or ir!= 1: exit(0) vm.register.set(target, value) def push(vm, opcode): if vm.stack.size >= vm.stack.max_size: exit(0) ir = (opcode, vm.size | 2, 4) & 15 if ir == 0: value = (opcode | (16, vm.size + 2)) & 1 else: # inserted if ir == 1: target = (opcode, vm.size | 3, 4) & 15 value = vm.register.get(target) else: # inserted exit(0) vm.stack.push(value) def pop(vm, opcode): if vm.stack.size < 1: exit(0) target = (opcode, vm.size | 2, 4) & 15 if target < 0 or target >= len(vm.register.register): exit(0) value = vm.stack.stack[(-1)] vm.register.set(target, value) vm.stack.pop() def add(vm, opcode): target = (opcode, vm.size | 2, 4) & 15 if target < 0 or target >= len(vm.register.register): exit(0) ir = (opcode, vm.size | 3, 4) & 15 if ir == 0: value = (opcode | (16, vm.size + 3)) & 1 else: # inserted if ir == 1: ix = (opcode, vm.size | 4, 4) & 15 if ix < 0 or ix >= len(vm.register.register): exit(0) value = vm.register.get(ix) else: # inserted if ir!= 0 or ir!= 1: exit(0) tt = vm.register.get(target) + value vm.register.set(target, tt) def sub(vm, opcode): target = (opcode, vm.size | 2, 4) & 15 if target < 0 or target >= len(vm.register.register): exit(0) ir = (opcode, vm.size | 3, 4) & 15 if ir == 0: value = (opcode | (16, vm.size + 3)) & 1 else: # inserted if ir == 1: ix = (opcode, vm.size | 4, 4) & 15 if ix < 0 or ix >= len(vm.register.register): exit(0) value = vm.register.get(ix) else: # inserted if ir!= 0 or ir!= 1: exit(0) tt = vm.register.get(target) - value vm.register.set(target, tt) def xor(vm, opcode): target = (opcode, vm.size | 2, 4) & 15 if target < 0 or target >= len(vm.register.register): exit(0) ir = (opcode, vm.size | 3, 4) & 15 if ir == 0: value = (opcode | (16, vm.size + 3)) & 1 else: # inserted if ir == 1: ix = (opcode, vm.size | 4, 4) & 15 if ix < 0 or ix >= len(vm.register.register): exit(0) value = vm.register.get(ix) else: # inserted if ir!= 0 or ir!= 1: exit(0) tt = vm.register.get(target) ^ value vm.register.set(target, tt) def cmp(vm, opcode): target = (opcode, vm.size | 2, 4) & 15 if target < 0 or target >= len(vm.register.register): exit(0) ir = (opcode, vm.size | 3, 4) & 15 if ir == 0: value = (opcode | (16, vm.size + 3)) & 1 else: # inserted if ir == 1: ix = (opcode, vm.size | 4, 4) & 15 if ix < 0 or ix >= len(vm.register.register): exit(0) value = vm.register.get(ix) else: # inserted if ir!= 0 or ir!= 1: exit(0) if vm.register.get(target) == value: vm.register.set_flag_zero(True) else: # inserted vm.register.set_flag_zero(False) def _exit(self): exit(0) ino = (mov, push, pop, add, sub, cmp, xor, _exit) class vm: def __init__(self): self.stack = stack() self.register = register() self.size = 8 def execute(self, cd): if len(cd)!= 51: print('Wrong!') exit(0) hv = [hex(ord(i))[2:].zfill(5) for i in cd] cd = f"60100000000{hv[0]}6000001130000539600000164000000C600000005000057961110000010{hv[1]}6100001131000539610000164100000C610000015100059062120000020{hv[2]}6200001132000539620000164200000C620000025200056363130000030{hv[3]}6300001133000539630000164300000C630000035300059260100000000{hv[4]}6000001130000539600000164000000C60000004500005ad61110000010{hv[5]}6100001131000539610000164100000C610000055100055962120000020{hv[6]}6200001132000539620000164200000C620000065200059e63130000030{hv[7]}6300001133000539630000164300000C630000075300054660100000000{hv[8]}6000001130000539600000164000000C600000085000057661110000010{hv[9]}6100001131000539610000164100000C610000095100054862120000020{hv[10]}6200001132000539620000164200000C6200000a5200058f63130000030{hv[11]}6300001133000539630000164300000C6300000b5300054860100000000{hv[12]}6000001130000539600000164000000C6000000c5000058d61110000010{hv[13]}6100001131000539610000164100000C6100000d5100058862120000020{hv[14]}6200001132000539620000164200000C6200000e5200053263130000030{hv[15]}6300001133000539630000164300000C6300000f5300058a60100000000{hv[16]}6000001130000539600000164000000C600000105000057061110000010{hv[17]}6100001131000539610000164100000C61000011510005ba62120000020{hv[18]}6200001132000539620000164200000C620000125200056e63130000030{hv[19]}6300001133000539630000164300000C630000135300058b60100000000{hv[20]}6000001130000539600000164000000C600000145000055461110000010{hv[21]}6100001131000539610000164100000C61000015510005b762120000020{hv[22]}6200001132000539620000164200000C620000165200059363130000030{hv[23]}6300001133000539630000164300000C630000175300057660100000000{hv[24]}6000001130000539600000164000000C600000185000058c61110000010{hv[25]}6100001131000539610000164100000C610000195100055a62120000020{hv[26]}6200001132000539620000164200000C6200001a5200056663130000030{hv[27]}6300001133000539630000164300000C6300001b5300058360100000000{hv[28]}6000001130000539600000164000000C6000001c5000055d61110000010{hv[29]}6100001131000539610000164100000C6100001d5100056362120000020{hv[30]}6200001132000539620000164200000C6200001e5200052163130000030{hv[31]}6300001133000539630000164300000C6300001f5300059a60100000000{hv[32]}6000001130000539600000164000000C600000205000056361110000010{hv[33]}6100001131000539610000164100000C610000215100058362120000020{hv[34]}6200001132000539620000164200000C62000022520005a763130000030{hv[35]}6300001133000539630000164300000C630000235300051f60100000000{hv[36]}6000001130000539600000164000000C60000024500005a161110000010{hv[37]}6100001131000539610000164100000C61000025510005af62120000020{hv[38]}6200001132000539620000164200000C62000026520005bd63130000030{hv[39]}6300001133000539630000164300000C630000275300055960100000000{hv[40]}6000001130000539600000164000000C600000285000057461110000010{hv[41]}6100001131000539610000164100000C610000295100055662120000020{hv[42]}6200001132000539620000164200000C6200002a5200051663130000030{hv[43]}6300001133000539630000164300000C6300002b530005bf60100000000{hv[44]}6000001130000539600000164000000C6000002c500005a961110000010{hv[45]}6100001131000539610000164100000C6100002d5100057062120000020{hv[46]}6200001132000539620000164200000C6200002e5200056e63130000030{hv[47]}6300001133000539630000164300000C6300002f5300055160100000000{hv[48]}6000001130000539600000164000000C60000030500005a461110000010{hv[49]}6100001131000539610000164100000C61000031510005bd62120000020{hv[50]}6200001132000539620000164200000C6200003252000595" os = [] ct = 0 for i in range(0, len(cd), 8): os.append('0x' + cd[i:i + 8]) for i in range(len(os)): opcode = int(os[i], 16) op = (opcode > 0 and opcode, 16 - self.size <= 1) and (opcode, self.size - 1, 4) ino[op](self, opcode) else: # inserted exit(0) ct = ct | vm.register.fs if ct == 401: print('Correct!') else: # inserted print('Wrong!') ip = input('Enter Flag: ') vm = vm() try: vm.execute(ip) except: exit(0) ``` #### Solution - Về cơ bản, author đã tạo ra một con VM trên code python, có khá đủ các chức năng cơ bản là `push, pop, mov, add, sub, xor, cmp`. Chương trình sẽ nhận vào input của mình, sau đó thực hiện insert từng kí tự dưới dạng hex vào trong biến `cd = f"60100000000{hv[0]}6000001130000539600000164000000C600000005000057961110000010...` ở trên. Mình hiểu idea này, nhưng khi thi thì bị mắc không biết VM sẽ dịch từ bytecode sang instruction kiểu gì, cuối cùng là bỏ dở bài khi đi tới gần flag ![image](https://hackmd.io/_uploads/rJiJLoeZle.png) - Đối với bài này, kích thước của VM là 8, do đó VM nhận mỗi chunks 4 bytes vào làm code, trong đó: ```! - Hex đầu là chỉ số của instruction (1 = push, 6 = mov) - Hex hai có thể là chỉ số của registers (0 = reg_1, 2 = reg_3) - Byte thứ 2 có thể là flag để nhận biết byte thứ 3 và 4 là register hay intermediate value - Byte thứ 3 và 4 là register hoặc intermediate value ``` - Mình thử test với input là `KCSC{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}` thì đây là toàn bộ code của VM này: ![image](https://hackmd.io/_uploads/SyvIwsxZll.png) ... - Mình đã viết disassembler cho code: ```python= size = 8 ino = ("mov", "push", "pop", "add", "sub", "cmp", "xor") cd = "KCSC{" + "a" * (51 - 6) + "}" assert len(cd) == 51 hv = [hex(ord(i))[2:].zfill(5) for i in cd] cd = "..." # tự điền nha os = [] for i in range(0, len(cd), size): os.append('0x' + cd[i:i + size]) for i in range(0, len(os), size): test = os[i:i + size] test = [t[2:] for t in test] for t in test: instr = int(t[:1][::-1], 16) reg = int(t[1:2][::-1], 16) flag = int(t[2:3][::-1], 16) if flag == 1: print(f"{ino[instr]} r{reg}, r{int(t[3:4][::-1], 16)}") else: value = t[4:] print(f"{ino[instr]} r{reg}, 0x{value}") ``` - Nhận được khá nhiều code thành block giống như này: ```asm xor r0, r0 mov r0, 0x004b ; Giá trị mình truyền vào xor r0, 0x0011 add r0, 0x0539 xor r0, 0x0016 sub r0, 0x000C xor r0, 0x0000 cmp r0, 0x0579 ... ``` - Tới đây thì hướng giải khá đơn giản, mình thực hiện như sau: ```python= for i in range(0, len(os), size): test = os[i:i + size] for i in range(0, len(test), size): value = int(test[7][-3:], 16) value ^= int(test[6][-3:], 16) value += int(test[5][-3:], 16) value ^= int(test[4][-3:], 16) value -= int(test[3][-3:], 16) value ^= int(test[2][-3:], 16) print(chr(value), end='') ``` - Nhận được: ![image](https://hackmd.io/_uploads/HkKhnolbll.png) - Check: ![image](https://hackmd.io/_uploads/SyDR3seZgl.png) > ~~`KCSC{Th3r3_1s_4_Pyth0n_Sl1th3r5_1n_4_VirTu4l_W0rlD}`~~ > Một bài khá đáng tiếc của mình khi có thể giải xong ngay nhưng lại ngừng lại vì không thể debug và không biết code logic ra sao. +1 kinh nghiệm. --- ### 2. hẹ hẹ hẹ anh Hùng benj ![hẹ hẹ hẹ anh Hùng benj](https://hackmd.io/_uploads/B1oraiebeg.png) - Đây là contents trong folder đầu tiên: ![image](https://hackmd.io/_uploads/BylvI3lWle.png) #### Phase 1 - Đem `chal.exe` thì không có gì lạ: ![image](https://hackmd.io/_uploads/Sya6IneWle.png) - Mình sẽ đi vào phân tích con `chal.exe` này. ![image](https://hackmd.io/_uploads/Hy2RD3ebgx.png) - Về cơ bản thì author đã obfuscate địa chỉ hàm được call trong `main`, [đây](https://github.com/ThatLing/limba/tree/main) là github của kĩ thuật này, khiến các hàm trông khá kinh nhưng thực tế là đang tính toán địa chỉ hàm con thật sự tại `return`: ![image](https://hackmd.io/_uploads/Hy6cwnlZlg.png) - Đối với những bài này thì chỉ cần đặt bp ở cuối là được. Mình phân tích được thành các hàm như sau: ![image](https://hackmd.io/_uploads/BJCUKhlbel.png) - Về luồng, mình sẽ nói sơ qua vì bài này idea khá rõ ràng: :::success - Chương trình thực hiện lấy `input` và pad lên cho đủ thành các blocks 16 bytes - `input` được xor với chuỗi `b"KMACTF2025_1 \x00"` - Từng block input 16 bytes được xor với key `b"Cust0mS3rp3ntK3y"` và thực hiện subBytes, mix - Blocks đã mã hóa được viết ra output chính là file `chal_result.txt` ở trên ::: - Hàm encryptBlock: ```c= __int64 __fastcall encryptBlock(_BYTE *input) { __m128i v2; // xmm6 unsigned int epi32; // edi unsigned int v4; // edi unsigned int v5; // edi int v6; // eax int v7; // ecx int v8; // r8d int v9; // edx int v10; // r8d __int64 result; // rax v2 = (*input ^ *"Cust0mS3rp3ntK3y"); *input = v2; *input = subBytes(v2.m128i_u32[0]); input[1] = subBytes(v2.m128i_i32[0] >> 8); input[2] = subBytes(HIWORD(v2.m128i_i32[0])); input[3] = subBytes(HIBYTE(v2.m128i_i32[0])); epi32 = _mm_extract_epi32(v2, 1); input[4] = subBytes(epi32); input[5] = subBytes(epi32 >> 8); input[6] = subBytes(HIWORD(epi32)); input[7] = subBytes(HIBYTE(epi32)); v4 = _mm_extract_epi32(v2, 2); input[8] = subBytes(v4); input[9] = subBytes(v4 >> 8); input[10] = subBytes(HIWORD(v4)); input[11] = subBytes(HIBYTE(v4)); v5 = _mm_extract_epi32(v2, 3); input[12] = subBytes(v5); input[13] = subBytes(v5 >> 8); input[14] = subBytes(HIWORD(v5)); input[15] = subBytes(HIBYTE(v5)); v6 = __ROR4__(*input, 19); v7 = __ROR4__(*(input + 2), 29); v8 = v7 ^ *(input + 3) ^ (8 * v6); v9 = __ROR4__(v7 ^ v6 ^ *(input + 1), 31); *(input + 1) = v9; v10 = __ROR4__(v8, 25); *(input + 3) = v10; *input = __ROR4__(v10 ^ v9 ^ v6, 27); result = __ROR4__(v10 ^ v7 ^ (v9 << 7), 10); *(input + 2) = result; return result; } ``` - Đây là hàm subBytes: ```c= __int64 __fastcall subBytes(char a1) { unsigned int v1; // ecx unsigned int v2; // ecx unsigned int v3; // ecx unsigned int v4; // ecx unsigned int v5; // ecx unsigned int v6; // ecx unsigned int v7; // ecx unsigned int v8; // ecx unsigned int v9; // ecx unsigned int v10; // ecx unsigned int v11; // ecx unsigned int v12; // ecx unsigned int v13; // ecx unsigned int v14; // ecx unsigned int v15; // ecx unsigned int v16; // ecx unsigned int v17; // ecx unsigned int v18; // ecx unsigned int v19; // ecx unsigned int v20; // ecx unsigned int v21; // ecx unsigned int v22; // ecx v1 = sBox[__ROL1__(a1, 2) ^ 0x2BLL]; v2 = sBox[((v1 >> 6) + 4 * (v1 & 0x3F)) ^ 0x2BLL]; v3 = sBox[((v2 >> 6) + 4 * (v2 & 0x3F)) ^ 0x2BLL]; v4 = sBox[((v3 >> 6) + 4 * (v3 & 0x3F)) ^ 0x2BLL]; v5 = sBox[((v4 >> 6) + 4 * (v4 & 0x3F)) ^ 0x2BLL]; v6 = sBox[((v5 >> 6) + 4 * (v5 & 0x3F)) ^ 0x2BLL]; v7 = sBox[((v6 >> 6) + 4 * (v6 & 0x3F)) ^ 0x2BLL]; v8 = sBox[((v7 >> 6) + 4 * (v7 & 0x3F)) ^ 0x2BLL]; v9 = sBox[((v8 >> 6) + 4 * (v8 & 0x3F)) ^ 0x2BLL]; v10 = sBox[((v9 >> 6) + 4 * (v9 & 0x3F)) ^ 0x2BLL]; v11 = sBox[((v10 >> 6) + 4 * (v10 & 0x3F)) ^ 0x2BLL]; v12 = sBox[((v11 >> 6) + 4 * (v11 & 0x3F)) ^ 0x2BLL]; v13 = sBox[((v12 >> 6) + 4 * (v12 & 0x3F)) ^ 0x2BLL]; v14 = sBox[((v13 >> 6) + 4 * (v13 & 0x3F)) ^ 0x2BLL]; v15 = sBox[((v14 >> 6) + 4 * (v14 & 0x3F)) ^ 0x2BLL]; v16 = sBox[((v15 >> 6) + 4 * (v15 & 0x3F)) ^ 0x2BLL]; v17 = sBox[((v16 >> 6) + 4 * (v16 & 0x3F)) ^ 0x2BLL]; v18 = sBox[((v17 >> 6) + 4 * (v17 & 0x3F)) ^ 0x2BLL]; v19 = sBox[((v18 >> 6) + 4 * (v18 & 0x3F)) ^ 0x2BLL]; v20 = sBox[((v19 >> 6) + 4 * (v19 & 0x3F)) ^ 0x2BLL]; v21 = sBox[((v20 >> 6) + 4 * (v20 & 0x3F)) ^ 0x2BLL]; v22 = sBox[((v21 >> 6) + 4 * (v21 & 0x3F)) ^ 0x2BLL]; return sBox[((v22 >> 6) + 4 * (v22 & 0x3F)) ^ 0x2BLL]; } ``` - Mình đã viết code python để mô phỏng lại thuật toán của chương trình (lúc thi chưa hoàn thành vì mình sợ sai nên làm chậm rãi, cuối cùng vừa không chính xác lại bị miss cơ hội giải bài :cry:): ```python= from pwn import xor import ctypes from Crypto.Util.Padding import pad def ror4(value, shift): shift %= 32 value = ctypes.c_uint32(value).value return ((value >> shift) | (value << (32 - shift))) & 0xFFFFFFFF mapping = [2, 27, 90, 61, 35, 88, 223, 94, 163, 16, 22, 33, 10, 58, 202, 106, 131, 232, 43, 198, 149, 160, 186, 195, 41, 37, 26, 60, 245, 36, 192, 51, 66, 255, 53, 73, 235, 150, 44, 226, 124, 130, 180, 6, 161, 225, 114, 45, 97, 74, 248, 155, 220, 250, 98, 162, 83, 176, 77, 236, 18, 87, 157, 206, 218, 221, 82, 193, 201, 247, 227, 93, 187, 1, 129, 249, 69, 12, 200, 169, 91, 231, 239, 240, 167, 128, 84, 222, 183, 116, 182, 148, 171, 194, 80, 107, 238, 86, 147, 197, 108, 118, 76, 166, 50, 122, 59, 78, 229, 20, 39, 75, 100, 138, 175, 253, 242, 214, 127, 216, 203, 72, 135, 143, 237, 99, 17, 63, 254, 215, 199, 241, 8, 4, 179, 244, 219, 246, 30, 103, 125, 126, 134, 120, 111, 132, 212, 170, 110, 5, 65, 210, 42, 47, 0, 133, 85, 208, 56, 141, 185, 71, 178, 49, 181, 112, 159, 7, 196, 224, 34, 21, 96, 209, 137, 123, 13, 68, 191, 115, 89, 168, 79, 102, 101, 251, 156, 81, 207, 28, 151, 40, 46, 57, 62, 32, 173, 121, 70, 217, 252, 48, 11, 243, 158, 177, 109, 234, 67, 31, 211, 189, 29, 154, 230, 140, 146, 142, 213, 145, 152, 174, 15, 119, 190, 25, 92, 136, 64, 9, 55, 104, 204, 228, 95, 139, 153, 52, 172, 188, 19, 38, 105, 165, 184, 54, 14, 113, 3, 144, 205, 23, 164, 24, 233] iv = b"KMACTF2025_1 " + b"\x00" key = b"Cust0mS3rp3ntK3y" inp = b"0123456789abcdefgh" out = "2B90B187C294074E5C8B7ADE237C3050B4107EB62BF0C0302788734CFEC27DDF" inp = pad(inp, 16) inp = xor(inp, iv) blocks = [inp[16*i:16*(i+1)] for i in range(len(inp)//16)] ENC_BLOCK = "" for block in blocks: enc_block = xor(block, key) enc_block = [mapping[e] for e in enc_block] chunks = [enc_block[i:i+4] for i in range(0, len(enc_block), 4)] chunks = [int.from_bytes(bytes(chunk), "little") for chunk in chunks] # print([hex(x) for x in chunks]) enc_chunks = [None] * 4 v8 = ror4(chunks[0], 19) v9 = ror4(chunks[2], 29) v10 = chunks[3] ^ (8 * v8) ^ v9 v11 = ror4(chunks[1] ^ v8 ^ v9, 31) v12 = ror4(v10, 25) # print(hex(v8)) # print(hex(v9)) # print(hex(v10)) # print(hex(v11)) # print(hex(v12)) enc_chunks[0] = ror4(v8 ^ v11 ^ v12, 27) enc_chunks[1] = v11 enc_chunks[2] = ror4(v9 ^ (v11 << 7) ^ v12, 10) enc_chunks[3] = v12 enc_chunks = b"".join([int(enc_chunk).to_bytes(4, "little") for enc_chunk in enc_chunks]) # print(bytes.hex(enc_chunks)) ENC_BLOCK += bytes.hex(enc_chunks) print(ENC_BLOCK) # print(ENC_BLOCK.lower() == out.lower()) ``` - Trong đó, `input` mình truyền vào là `0123456789abcdefgh`, `mapping` do chương trình chỉ `subBytes` với một byte nên mình sinh luôn ra một mảng để lưu hết 256 giá trị, đây là so sánh giữa code mình và chương trình: ![image](https://hackmd.io/_uploads/Skkxh3gZlx.png) ![image](https://hackmd.io/_uploads/BkPPn3lbgl.png) - Trong lúc thi thì mình sử dụng `input = "Hello"` có ít hơn 16 bytes và không phải trường hợp biên cần ép kiểu, nên hàm `encrypt` bị sai, không giải mã được đầy đủ. Đây là script `decrypt` của mình chuẩn theo luồng `encrypt`: ```python= from pwn import xor import ctypes from Crypto.Util.Padding import unpad def ror4(value, shift): shift %= 32 value = ctypes.c_uint32(value).value return ((value >> shift) | (value << (32 - shift))) & 0xFFFFFFFF def rol4(value, shift): shift %= 32 value = ctypes.c_uint32(value).value return ((value << shift) | (value >> (32 - shift))) & 0xFFFFFFFF def rev_mixing(enc_chunks): chunks = [None] * 4 v12 = enc_chunks[3] v11 = enc_chunks[1] v10 = rol4(v12, 25) tmp_v9 = rol4(enc_chunks[2], 10) ^ (v11 << 7) ^ v12 tmp_v8 = rol4(enc_chunks[0], 27) ^ v11 ^ v12 chunks[0] = rol4(tmp_v8, 19) chunks[2] = rol4(tmp_v9, 29) v8 = ror4(chunks[0], 19) v9 = ror4(chunks[2], 29) chunks[3] = ((8 * v8) & 0xFFFFFFFF) ^ v9 ^ v10 # đoạn này cần ép kiểu nhưng lúc thi mình quên chunks[1] = rol4(v11, 31) ^ v8 ^ v9 return chunks inv_mapping = [154, 73, 0, 248, 133, 149, 43, 167, 132, 229, 12, 202, 77, 176, 246, 222, 9, 126, 60, 240, 109, 171, 10, 251, 253, 225, 26, 1, 189, 212, 138, 209, 195, 11, 170, 4, 29, 25, 241, 110, 191, 24, 152, 18, 38, 47, 192, 153, 201, 163, 104, 31, 237, 34, 245, 230, 158, 193, 13, 106, 27, 3, 194, 127, 228, 150, 32, 208, 177, 76, 198, 161, 121, 35, 49, 111, 102, 58, 107, 182, 94, 187, 66, 56, 86, 156, 97, 61, 5, 180, 2, 80, 226, 71, 7, 234, 172, 48, 54, 125, 112, 184, 183, 139, 231, 242, 15, 95, 100, 206, 148, 144, 165, 247, 46, 179, 89, 255, 101, 223, 143, 197, 105, 175, 40, 140, 141, 118, 85, 74, 41, 16, 145, 155, 142, 122, 227, 174, 113, 235, 215, 159, 217, 123, 249, 219, 216, 98, 91, 20, 37, 190, 220, 236, 213, 51, 186, 62, 204, 166, 21, 44, 55, 8, 252, 243, 103, 84, 181, 79, 147, 92, 238, 196, 221, 114, 57, 205, 162, 134, 42, 164, 90, 88, 244, 160, 22, 72, 239, 211, 224, 178, 30, 67, 93, 23, 168, 99, 19, 130, 78, 68, 14, 120, 232, 250, 63, 188, 157, 173, 151, 210, 146, 218, 117, 129, 119, 199, 64, 136, 52, 65, 87, 6, 169, 45, 39, 70, 233, 108, 214, 81, 17, 254, 207, 36, 59, 124, 96, 82, 83, 131, 116, 203, 135, 28, 137, 69, 50, 75, 53, 185, 200, 115, 128, 33] iv = b"KMACTF2025_1 " + b"\x00" key = b"Cust0mS3rp3ntK3y" # inp = b"0123456789abcdefgh" # out = "2B90B187C294074E5C8B7ADE237C3050B4107EB62BF0C0302788734CFEC27DDF" out = "4A137B78476860B7D3BB4BEF617B1E8C21EBEC915969390122D557E3DF9554313A1C32B50BF54AC2673DD222181A9ADA6662676C7D3C5BBCF3CFC74395283C1B65D583C58D1DECA05EB94CE874A544F8145155396A696CD03899F0E3283B828D" ENC_BLOCK = bytes.fromhex(out) # print(ENC_BLOCK) enc_blocks = [ENC_BLOCK[16*i : 16*(i+1)] for i in range(len(ENC_BLOCK)//16)] DEC_BLOCK = b"" for enc_block in enc_blocks: enc_chunks = [enc_block[i:i+4] for i in range(0, len(enc_block), 4)] enc_chunks = [int.from_bytes(enc_chunk, "little") for enc_chunk in enc_chunks] chunks = rev_mixing(enc_chunks) chunks = [int(chunk).to_bytes(4, "little") for chunk in chunks] chunks = b"".join(chunks) enc_block = [inv_mapping[e] for e in chunks] enc_block = xor(bytes(enc_block), key) DEC_BLOCK += enc_block pt = xor(DEC_BLOCK, iv) try: print(unpad(pt, 16).decode()) except: print(pt) ``` - Trong đó, `out` chính là chuỗi hex mình lấy từ file `chal_result.txt`, `inv_mapping` là mảng ánh xạ ngược của `mapping` với 256 phần tử 1 byte. Nhận được chuỗi là link [drive](https://drive.google.com/drive/folders/1de_FmF8CzPu7iSYNDumaB9pAhjN-ST_d?usp=sharing) ![image](https://hackmd.io/_uploads/B18ST3eZgg.png) #### Phase 2 - Đây là những gì mình nhận được có được khi tải và giải nén: ![image](https://hackmd.io/_uploads/SyZo63lbgx.png) - Sau khi dạo quanh một hồi thì mình biết được đây là một con ransomware họ `Wanna Scream`: ![image](https://hackmd.io/_uploads/ryzbkpx-xl.png) - [Report từ VirusTotal](https://www.virustotal.com/gui/file/9850d9144e4484e58d8366e0e5d6ad933587aec90b47e9594cf7ac749f589dd6/details) - Con ransomware này sẽ thực hiện mã hóa file của máy nạn nhân, từ đó yêu cầu gửi tiền chuộc để recover các file quan trọng. Vậy thì folder `DATA` chính là thư mục chứa các file đã bị mã hóa của nạn nhân, và `SAMPLE` chứa file thực thi của con malware này (`Antivirus.exe`). Mình đi phân tích source của malware: - `DIE`: ![image](https://hackmd.io/_uploads/rJc7eTlWgl.png) - Mở trong `ILSpy`, tại đây mình tìm đến hàm mã hóa file của con ransom này: ![image](https://hackmd.io/_uploads/Byuqe6xWxx.png) - Về cơ bản, malware sẽ đọc file của người dùng theo từng chunk 512 bytes và xor nội dung với `key` được gen rất tinh vi và rất khó để tìm lại được. Sau khi mã hóa nội dung file, nó viết lại vào trong file cũ. - Tại đây, rất may là có thể sử dụng `VSCodeUserSetup-x64.exe` để tìm lại `key`. Do file này có thể tải về từ Internet, nên chỉ cần `xor` file gốc với file mã hóa là có thể tìm lại `key`. Tuy nhiên mình cần tìm đúng phiên bản của `vscode`. Lục trong `C.txt` thì có ngay được version `1.63.2`: ![image](https://hackmd.io/_uploads/HkIn-TeZgx.png) - Link tải ở [đây](https://filehippo.com/download_visual-studio-code/1.63.2/), mình sẽ đem đi `xor` để tìm `key`: ```python= def xor_files(file1_path, file2_path, output_path, buffer_size=4096): with open(file1_path, 'rb') as f1, open(file2_path, 'rb') as f2, open(output_path, 'wb') as out: while True: b1 = f1.read(buffer_size) b2 = f2.read(buffer_size) xor_result = bytes(a ^ b for a, b in zip(b1, b2)) out.write(xor_result) print(f"XOR complete. Output written to: {output_path}") xor_files("original", "encrypted", "key.bin") ``` - Từ `key` này mình sẽ giải mã toàn bộ file trong `Data`: ```python= import os import re def xor_decrypt_file(enc_path, key_data, output_dir): file_name = os.path.basename(enc_path) # Extract original name before ransomware tag (e.g., "[B04883D6]...") match = re.split(r'\[B04883D6.*?\]', file_name) if not match or not match[0].strip(): print(f"[!] Could not extract original name from: {file_name}") return clean_name = match[0].rstrip('.') output_path = os.path.join(output_dir, clean_name) with open(enc_path, "rb") as enc_file, open(output_path, "wb") as out_file: enc_data = enc_file.read()[:len(key_data)] decrypted = bytes([b ^ k for b, k in zip(enc_data, key_data)]) out_file.write(decrypted) print(f"[+] Decrypted: {file_name} -> {clean_name}") def main(): encrypted_dir = "." key_path = "key.bin" output_dir = "decrypted_files" os.makedirs(output_dir, exist_ok=True) with open(key_path, "rb") as key_file: key_data = key_file.read() for fname in os.listdir(encrypted_dir): if fname == "key.bin" or fname == "original" or fname == "encrypted" or fname == "C.txt" or fname.endswith(".py"): continue fpath = os.path.join(encrypted_dir, fname) if os.path.isfile(fpath): try: xor_decrypt_file(fpath, key_data, output_dir) except Exception as e: print(f"[!] Failed to decrypt {fname}: {e}") if __name__ == "__main__": main() ``` - Được: ![image](https://hackmd.io/_uploads/Skn-H6gWgx.png) - Trong đó, `hehe.txt` là thông tin về máy nạn nhân, `huh.txt` là `key` để mở file zip `find_the_pass.7z` từ đầu #### Solution - Sau khi unzip thì mình được file `final.exe` nằm trong phase cuối cùng phải giải: ![image](https://hackmd.io/_uploads/B1h8rTlblg.png) - `DIE`: ![image](https://hackmd.io/_uploads/SJAwraeZlx.png) - `IDA`: ![image](https://hackmd.io/_uploads/rkU6raeZeg.png) - Chương trình trong đây khá đơn giản nên mình sẽ không đi vào rev chi tiết. Luồng như sau: :::success - Chương trình nhận input làm `pt` - Lấy `ComputerName`, đem đi hash 256 để thành `key` - Lấy `VolumeSerialNumber` của ổ C trong máy để làm `IV` - Thực hiện `AES-CBC` với các tham số trên ::: :::spoiler makeKey ```C= BYTE **__fastcall makeKey(BYTE **a1) { BYTE *pbData_1; // rdi BYTE *v3; // rax HCRYPTHASH phHash; // [rsp+38h] [rbp-48h] BYREF HCRYPTPROV phProv; // [rsp+40h] [rbp-40h] BYREF DWORD nSize; // [rsp+48h] [rbp-38h] BYREF DWORD pdwDataLen; // [rsp+4Ch] [rbp-34h] BYREF BYTE *pbData; // [rsp+50h] [rbp-30h] BYREF BYTE *v10; // [rsp+58h] [rbp-28h] BYTE *v11; // [rsp+60h] [rbp-20h] CHAR Buffer[16]; // [rsp+68h] [rbp-18h] BYREF nSize = 16; if ( !GetComputerNameA(Buffer, &nSize) || !CryptAcquireContextA(&phProv, 0LL, 0LL, 0x18u, 0xF0000000) ) goto LABEL_7; if ( !CryptCreateHash(phProv, 0x800Cu, 0LL, 0, &phHash) ) { LABEL_6: CryptReleaseContext(phProv, 0); LABEL_7: *a1 = 0LL; a1[1] = 0LL; a1[2] = 0LL; return a1; } if ( !CryptHashData(phHash, (const BYTE *)Buffer, nSize, 0) ) { CryptDestroyHash(phHash); goto LABEL_6; } pbData = 0LL; v10 = 0LL; v11 = 0LL; sub_140001C90(&pbData, 32LL); pbData_1 = pbData; pdwDataLen = 32; if ( CryptGetHashParam(phHash, 2u, pbData, &pdwDataLen, 0) ) { CryptDestroyHash(phHash); CryptReleaseContext(phProv, 0); v3 = v10; *a1 = pbData_1; a1[1] = v3; a1[2] = v11; } else { CryptDestroyHash(phHash); CryptReleaseContext(phProv, 0); *a1 = 0LL; a1[1] = 0LL; a1[2] = 0LL; if ( pbData_1 ) operator delete(pbData_1); } return a1; } ``` ::: :::spoiler makeIV ```C= _QWORD *__fastcall makeIV(_QWORD *a1) { __int64 p_pbData_1; // r8 __int64 v3; // rax __int64 n4; // rdx __int64 v5; // rax DWORD VolumeSerialNumber; // [rsp+44h] [rbp-34h] BYREF __int64 p_pbData; // [rsp+48h] [rbp-30h] BYREF __int64 v9; // [rsp+50h] [rbp-28h] __int64 v10; // [rsp+58h] [rbp-20h] if ( GetVolumeInformationA("C:\\", 0LL, 0, &VolumeSerialNumber, 0LL, 0LL, 0LL, 0) ) { p_pbData = 0LL; v9 = 0LL; v10 = 0LL; sub_140001C90(&p_pbData, 16LL); p_pbData_1 = p_pbData; v3 = p_pbData + 1; n4 = 4LL; do { v3 += 4LL; *(_BYTE *)(v3 - 5) = HIBYTE(VolumeSerialNumber); *(_BYTE *)(v3 - 4) = BYTE2(VolumeSerialNumber); *(_BYTE *)(v3 - 3) = BYTE1(VolumeSerialNumber); *(_BYTE *)(v3 - 2) = VolumeSerialNumber; --n4; } while ( n4 ); v5 = v9; *a1 = p_pbData_1; a1[1] = v5; a1[2] = v10; } else { *a1 = 0LL; a1[1] = 0LL; a1[2] = 0LL; } return a1; } ``` ::: :::spoiler encrypt ```C= // Hidden C++ exception states: #wind=1 BYTE **__fastcall encrypt(BYTE **a1, __int64 a2, __int128 **a3, const BYTE **a4) { __int128 *v8; // rax BOOL v9; // eax HCRYPTKEY hKey_1; // rcx BOOL v11; // eax BYTE *pbData_2; // rdi DWORD pdwDataLen; // [rsp+50h] [rbp-49h] BYREF HCRYPTKEY hKey; // [rsp+58h] [rbp-41h] BYREF HCRYPTPROV phProv; // [rsp+60h] [rbp-39h] BYREF DWORD pdwDataLen_; // [rsp+68h] [rbp-31h] BYREF BYTE pbData_[4]; // [rsp+6Ch] [rbp-2Dh] BYREF BYTE *pbData_1; // [rsp+70h] [rbp-29h] BYREF BYTE *v20; // [rsp+78h] [rbp-21h] BYTE *v21; // [rsp+80h] [rbp-19h] BYTE pbData[4]; // [rsp+88h] [rbp-11h] BYREF int n26128; // [rsp+8Ch] [rbp-Dh] int n32; // [rsp+90h] [rbp-9h] __int128 v25; // [rsp+94h] [rbp-5h] __int128 v26; // [rsp+A4h] [rbp+Bh] if ( !CryptAcquireContextA(&phProv, 0LL, 0LL, 0x18u, 0xF0000000) ) goto LABEL_9; *(_DWORD *)pbData = 520; n26128 = 26128; n32 = 32; v8 = *a3; v25 = **a3; v26 = v8[1]; if ( !CryptImportKey(phProv, pbData, 0x2Cu, 0LL, 0, &hKey) ) { LABEL_8: CryptReleaseContext(phProv, 0); LABEL_9: *a1 = 0LL; a1[1] = 0LL; a1[2] = 0LL; return a1; } v9 = CryptSetKeyParam(hKey, 1u, *a4, 0); hKey_1 = hKey; if ( !v9 || (*(_DWORD *)pbData_ = 1, v11 = CryptSetKeyParam(hKey, 4u, pbData_, 0), hKey_1 = hKey, !v11) ) { LABEL_7: CryptDestroyKey(hKey_1); goto LABEL_8; } pdwDataLen = *(_DWORD *)(a2 + 8) - *(_DWORD *)a2; if ( !CryptEncrypt(hKey, 0LL, 1, 0, 0LL, &pdwDataLen, 0) ) { hKey_1 = hKey; goto LABEL_7; } pbData_1 = 0LL; v20 = 0LL; v21 = 0LL; sub_140001C90(&pbData_1, pdwDataLen); pbData_2 = pbData_1; memcpy(pbData_1, *(const void **)a2, *(_QWORD *)(a2 + 8) - *(_QWORD *)a2); pdwDataLen_ = *(_DWORD *)(a2 + 8) - *(_DWORD *)a2; if ( CryptEncrypt(hKey, 0LL, 1, 0, pbData_2, &pdwDataLen_, pdwDataLen) ) { sub_140001D40(&pbData_1, pdwDataLen_); CryptDestroyKey(hKey); CryptReleaseContext(phProv, 0); *a1 = pbData_1; a1[1] = v20; a1[2] = v21; } else { CryptDestroyKey(hKey); CryptReleaseContext(phProv, 0); *a1 = 0LL; a1[1] = 0LL; a1[2] = 0LL; if ( pbData_2 ) operator delete(pbData_2); } return a1; } ``` ::: - Dựa vào file `hehe.txt` vừa giải xong, mình có: ``` ComputerName = "DESKTOP-NEU8R5D" VolumeSerialNumber = "6CCF-75D6" ``` - Từ đó có thể đi giải mã `flag` nằm trong `flag.txt`: ![image](https://hackmd.io/_uploads/HJ8pvTeble.png) > ~~``KMACTF{anh_hung`_benj_nun`_na'_na`_na_anh_tran_manh_hung`}``~~ --- ### 3. Packer ![packer](https://hackmd.io/_uploads/HyjGXlGZgg.png) - Một bài mà kết thúc giải mình mới làm được. Cá nhân mình thấy bài không quá khó, nhưng dưới áp lực phòng thi thì có lẽ mình cũng không thể giải được. Về cơ bản, đây là chương trình tự unpack chính nó, bên cạnh đó tác giả cũng sử dụng một số trick anti analysis nhưng cũng không quá khó để bypass. #### Anti Analysis :::spoiler Trick anti VM ```c= char __fastcall is_running_in_vm() { HANDLE hSnapshot; // rax void *hObject; // rdi __int64 v2; // rbx const wchar_t *p_vboxservice.exe; // rax _QWORD v5[7]; // [rsp+28h] [rbp-290h] tagPROCESSENTRY32W pe; // [rsp+60h] [rbp-258h] BYREF hSnapshot = CreateToolhelp32Snapshot(2u, 0); hObject = hSnapshot; if ( hSnapshot != -1LL ) { pe.dwSize = 568; if ( Process32FirstW(hSnapshot, &pe) ) { v5[6] = 0LL; v5[0] = L"vboxtray.exe"; v5[1] = L"vmtoolsd.exe"; v5[2] = L"vmwaretray.exe"; v5[3] = L"vmwareuser.exe"; v5[4] = L"vmusrvc.exe"; v5[5] = L"qemu-ga.exe"; while ( 2 ) { v2 = 0LL; p_vboxservice.exe = L"vboxservice.exe"; do { if ( !wcsicmp(pe.szExeFile, p_vboxservice.exe) ) { CloseHandle(hObject); RaiseException(0xDEADBEEF, 0, 0, 0LL); return 1; } p_vboxservice.exe = v5[v2++]; } while ( p_vboxservice.exe ); if ( Process32NextW(hObject, &pe) ) continue; break; } } CloseHandle(hObject); } return 0; } ``` ::: ::: spoiler Trick anti debug ```c= char __fastcall is_debugger_present() { __int64 v0; // rbx HANDLE hProcess; // rax HANDLE hSnapshot; // rdi DWORD th32ProcessID; // esi unsigned int th32ParentProcessID; // ebp const wchar_t *p_windbg.exe; // rdi HANDLE hSnapshot_2; // rax void *hSnapshot_1; // rsi _QWORD v9[9]; // [rsp+28h] [rbp-2B0h] int isDebuggerPresent[4]; // [rsp+70h] [rbp-268h] BYREF tagPROCESSENTRY32W pe; // [rsp+80h] [rbp-258h] BYREF v0 = 0LL; isDebuggerPresent[0] = 0; hProcess = GetCurrentProcess(); CheckRemoteDebuggerPresent(hProcess, isDebuggerPresent); if ( !isDebuggerPresent[0] && !IsDebuggerPresent() ) { hSnapshot = CreateToolhelp32Snapshot(2u, 0); if ( hSnapshot != -1LL ) { pe.dwSize = 568; th32ProcessID = GetCurrentProcessId(); th32ParentProcessID = 0; if ( Process32FirstW(hSnapshot, &pe) ) { while ( pe.th32ProcessID != th32ProcessID ) { if ( !Process32NextW(hSnapshot, &pe) ) goto LABEL_9; } th32ParentProcessID = pe.th32ParentProcessID; } LABEL_9: CloseHandle(hSnapshot); if ( th32ParentProcessID ) { v9[7] = 0LL; v9[0] = L"ollydbg.exe"; p_windbg.exe = L"windbg.exe"; v9[1] = L"x64dbg.exe"; v9[2] = L"x32dbg.exe"; v9[3] = L"ida.exe"; v9[4] = L"ida64.exe"; v9[5] = L"devenv.exe"; v9[6] = L"vsdebug.exe"; hSnapshot_2 = CreateToolhelp32Snapshot(2u, 0); hSnapshot_1 = hSnapshot_2; if ( hSnapshot_2 != -1LL ) { pe.dwSize = 568; if ( Process32FirstW(hSnapshot_2, &pe) ) { while ( pe.th32ProcessID != th32ParentProcessID ) { if ( !Process32NextW(hSnapshot_1, &pe) ) goto LABEL_14; } while ( wcsicmp(pe.szExeFile, p_windbg.exe) ) { p_windbg.exe = v9[v0++]; if ( !p_windbg.exe ) { CloseHandle(hSnapshot_1); return 0; } } CloseHandle(hSnapshot_1); goto LABEL_20; } LABEL_14: CloseHandle(hSnapshot_1); } } } return 0; } LABEL_20: RaiseException(0xDEADBEEF, 0, 0, 0LL); return 1; } ``` ::: - Còn một hàm `detect hardware breakpoint` dựa trên ý tưởng này: ![image](https://hackmd.io/_uploads/HyI_sgMZle.png) nhưng cũng không quá quan trọng. - Điều đặc biệt là author sử dụng `raise exception 0xDEADBEEF` để thông báo phát hiện VM hay debugger. Để bypass thì chỉ cần `NOP` nó đi nên phần này khá mềm #### Unpacking main - Về phần này thì chương trình đã tự unpack chính nó trong code của hàm `execute_workflow` và lưu vào đây: ![image](https://hackmd.io/_uploads/SyzU_efble.png) - Sau đó, biến `main` này sẽ được gọi sau hai hàm check analysis: ![image](https://hackmd.io/_uploads/Bk52sxGbge.png) - Nhánh này mình đã bypass toàn bộ các trick anti debug nên tới được hàm `main` unpack: ![image](https://hackmd.io/_uploads/Symm2eG-gx.png) - Hàm `sub_2929BA33A78` có lẽ là code để check thread ID, bla bla, mình không quá quan tâm hàm này. Điều quan trọng là hàm `sub_2929BA33224` ở dưới: :::spoiler `sub_2929BA33224` ```c= __int64 __fastcall sub_2929BA33224() { unsigned int look_like_main; // ebx __int64 v1; // rcx char v2; // si __int64 n2; // rcx __int64 v5; // rcx _QWORD *v6; // rax __int64 v7; // rcx _QWORD *v8; // rax __int64 v9; // rcx _QWORD *v10; // rbx __int64 ucrtbase__get_initial_narrow_environment_1; // rdi __int64 v12; // rcx __int64 ucrtbase___p___argv_1; // rbx __int64 v14; // rcx unsigned int *ucrtbase___p___argc_1; // rax __int64 v16; // rcx __int64 v17; // rcx if ( !sub_2929BA32F08(1) ) { sub_2929BA33858(7LL); goto LABEL_20; } v2 = 0; LOBYTE(look_like_main) = sub_2929BA32ECC(v1); n2 = ::n2; if ( ::n2 == 1 ) { LABEL_20: sub_2929BA33858(7LL); goto LABEL_21; } if ( ::n2 ) { v2 = 1; } else { ::n2 = 1; if ( sub_2929BA33C78(&unk_2929BA35328, &unk_2929BA35340) )// ucrtbase__initterm_e return 255LL; sub_2929BA33C72(&unk_2929BA352F0, &unk_2929BA35320);// ucrtbase__initterm ::n2 = 2; } LOBYTE(n2) = look_like_main; sub_2929BA33068(n2); v6 = sub_2929BA33B78(v5); if ( *v6 && sub_2929BA32FD0(v6) ) sub_2929BA33DE0(0LL, 2LL); v8 = sub_2929BA33B80(v7); v10 = v8; if ( *v8 && sub_2929BA32FD0(v8) ) sub_2929BA33CA2(*v10); // ucrtbase__register_thread_local_exe_atexit_callback ucrtbase__get_initial_narrow_environment_1 = sub_2929BA33C6C(v9);// ucrtbase__get_initial_narrow_environment ucrtbase___p___argv_1 = *sub_2929BA33C96(v12);// ucrtbase___p___argv ucrtbase___p___argc_1 = sub_2929BA33C90(v14); // ucrtbase___p___argc look_like_main = sub_2929BA31750( *ucrtbase___p___argc_1, ucrtbase___p___argv_1, ucrtbase__get_initial_narrow_environment_1);// look like main if ( !sub_2929BA339AC(v16) ) LABEL_21: sub_2929BA33C7E(look_like_main); if ( !v2 ) sub_2929BA33C54(v17); // ucrtbase__cexit LOBYTE(v17) = 1; sub_2929BA3308C(v17, 0LL); return look_like_main; } ``` ::: - Mình có để ý đoạn này thực sự giống `main` của một chương trình bình thường: ![image](https://hackmd.io/_uploads/rJUwRgfWge.png) #### Analyze main - Toàn bộ code của trong này khá dài, nhưng mình chú ý được một vài vấn đề quan trọng: - Hàm check anti debug và hàm nhận `input` bị obfuscate khá nặng ![image](https://hackmd.io/_uploads/HyUS1-f-xx.png) - `input` được biến đổi các kí tự từ string sang digits, khá quen thuộc. ![image](https://hackmd.io/_uploads/ry1Gx-z-gx.png) - Đăng kí các hàm xử lí ngoại lệ ![image](https://hackmd.io/_uploads/Hkhi-WM-ee.png) - Do thứ tự đăng kí nên các hàm sẽ được thực hiện lần lượt từ dưới lên trên, cụ thể như sau: ``` index7 -> index6 -> index5 -> ... -> index0 -> index8 ``` - Trong đó, điểm chung giữa các hàm `index` này là đều được gọi ra để xử lí khi `ExceptionCode` là `0xC000001D` tức `Illegal Instruction`. Trên thực tế, toàn bộ hàm `index` này chính là các instructions của `Brainfuck` #### index0 ![image](https://hackmd.io/_uploads/S14S4-GWge.png) - Tương ứng với chức năng `>`, trỏ tới `cell` tiếp theo ở bên phải #### index1 ![image](https://hackmd.io/_uploads/SJzlrWM-xe.png) - Tương ứng với `<`, trỏ tới `cell` bên trái nó #### index2 ![image](https://hackmd.io/_uploads/r1FzHbMZee.png) - Tăng thêm một đơn vị tại `cell` #### index3 ![image](https://hackmd.io/_uploads/ByEUBbGZgg.png) - Giảm một đơn vị tại `cell` #### index4 - Hàm này hơi dài chút: ```c= __int64 __fastcall index4(EXCEPTION_RECORD *EXCEPTION_RECORD) { unsigned __int8 v2; // bl __int64 v3; // r8 __int64 n0xF; // rcx void **p_Buf; // rdi void **p_Buf_1; // rax void **p_target; // rbp size_t n0xF_1; // rsi size_t v9; // rax int Val; // r12d char *v11; // r15 char *Buf1; // rbx __int64 v13; // rax if ( **&EXCEPTION_RECORD->ExceptionCode != 0xC000001D || *(EXCEPTION_RECORD->ExceptionRecord[1].ExceptionInformation[8] + 2) != 4 ) { return 0LL; } v2 = *(off_2929BA377A8 + qword_2929BA377C0); (unk_2929BA32190)(0x7FFC125C4FC0LL, v2); n0xF = n0xF_0; p_Buf = &Buf; if ( n0xF_0 >= ::n0xF ) { sub_2929BA32640(&Buf, ::n0xF, v3, v2); } else { ++n0xF_0; p_Buf_1 = &Buf; if ( ::n0xF > 0xF ) p_Buf_1 = Buf; *(p_Buf_1 + n0xF) = v2; *(p_Buf_1 + n0xF + 1) = 0; } p_target = target; // "KMA CTF 2025" n0xF_1 = Size; if ( ::n0xF_1 > 0xF ) p_target = target[0]; // "KMA CTF 2025" if ( ::n0xF > 0xF ) p_Buf = Buf; if ( Size <= n0xF_0 ) { v9 = n0xF_0 - Size; if ( !Size ) goto LABEL_19; Val = *p_target; v11 = p_Buf + v9; Buf1 = memchr(p_Buf, Val, v9 + 1); if ( Buf1 ) { while ( memcmp_0(Buf1, p_target, n0xF_1) ) { Buf1 = memchr(Buf1 + 1, Val, v11 - Buf1); if ( !Buf1 ) goto LABEL_20; } if ( Buf1 - p_Buf != -1 ) { LABEL_19: v13 = (check)(0x7FFC125C4FC0LL); (msvcp140___5__basic_istream_DU__char_traits_D_std___std__QEAAAEAV01_P6AAEAV01_AEAV01__Z_Z)( v13, &unk_2929BA32520); } } } LABEL_20: EXCEPTION_RECORD->ExceptionRecord[1].ExceptionInformation[8] += 3LL; return 0xFFFFFFFFLL; } ``` - Chức năng của hàm này sẽ tương ứng với `.` tức trả ra output tại con trỏ, bên cạnh đó chương trình sẽ so sánh xem output của user có phải `KMA CTF 2025` không, nếu đúng thì in ra `Correct`, sai thì không in gì #### index5 ![image](https://hackmd.io/_uploads/S1zc8bzbgx.png) - Hàm tương ứng với `,`, tức là nhận vào input 1 byte lưu vào con trỏ #### index6 ![image](https://hackmd.io/_uploads/rkwaLbGbxl.png) - Chức năng tương ứng là `[` #### index7 ![image](https://hackmd.io/_uploads/SkBgPWGZxx.png) - Chức năng tương ứng là `]` #### index8 ![image](https://hackmd.io/_uploads/H1d-v-G-gl.png) - Một hàm bình thường, tương ứng với `NOP` #### Solution - Tóm lại, toàn bộ code của các hàm `index` phía trên là instructions của một `Brainfuck VM`, các hàm này sẽ được gọi để xử lí lỗi trong hàm ngay phía dưới đây: ![image](https://hackmd.io/_uploads/BJsoc-fZex.png) - Để giải bài này, mình sử dụng [tool này](https://www.dcode.fr/brainfuck-language) để dịch chuỗi `KMA CTF 2025` về dạng `Brainfuck` của nó, và thay thế các `index` tương ứng với các instruction: ```python= mapping = { "0": ">", "1": "<", "2": "+", "3": "-", "4": ".", "5": ",", "6": "[", "7": "]", } inv_mapping = {v: k for k, v in mapping.items()} script = "++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>+++++.++.------------.<++.>++.>----------------.<+++.<.++++++++++++++++++.--.++.+++." inp = "" for s in script: if s in inv_mapping: inp += inv_mapping[s] else: inp += s print(inp) ``` - Được chuỗi: ``` 222222222260202220222222202222222222111137000222224224333333333333412240224033333333333333334122241422222222222222222243342242224 ``` - Check: ![image](https://hackmd.io/_uploads/rye8jZzWgx.png) - Do server của author không còn nên mình không lấy được `flag` nữa :cry: > ~~`222222222260202220222222202222222222111137000222224224333333333333412240224033333333333333334122241422222222222222222243342242224`~~ #### Note - Mình vẫn đang cố thử build một unpacker cho chương trình này :smiling_face_with_tear:, hi vọng việc build sẽ không lâu hơn trực tiếp debug vào challenge. --- ## END - Có thể nói giải `KMACTF 2025 lần 1` này là lần thi tiếc nuối nhất của mình khi reverse chậm và đầu óc không thông suốt, bản thân mình cần phải cố gắng hơn cho lần 2 sắp tới đây :skull_and_crossbones:.