--- title: TetCTF 2022 tags: CTF disqus: hackmd --- First few words === I played with `Wanna.One` and got the 100$ price for being the first team to clear Rev chals :tada:. All these chals are amazing and I want to share my write up about them. [RE] magicbox === **Description:** My machine is running very simple, can you understand it? **Given files:** [magicbox.zip](https://drive.google.com/file/d/1jqcAzT0frp2SLYlgRTOlSFSZMXARRxlT/view?usp=sharing) (Extract password: `TetCTF-7c71accd8175c365688a233a14fc4a1a`) Look through the code --- ```c= int sub_71000() { unsigned __int16 *mem; // ebx int result; // eax int id; // edx int arg1; // ecx int arg0; // esi int arg2; // edi mem = (unsigned __int16 *)lpAddress; for ( result = 1; *mem != 0xFFFF; result = 1 ) { if ( mem[4] == 1 ) { mem[4] = 0; printf("%c", mem[3]); mem = (unsigned __int16 *)lpAddress; } if ( mem[6] == 1 ) { mem[6] = 0; scanf("%c", mem + 5); mem = (unsigned __int16 *)lpAddress; } id = *mem; arg1 = mem[id + 1]; arg0 = mem[id]; arg2 = mem[id + 2]; *mem = id + 3; LOWORD(arg1) = ~(mem[arg0] | mem[arg1]); mem[arg2] = arg1; mem[1] = __ROL2__(arg1, 1); } return result; } ``` I regconize this is a VM-style chal right away. So I write a script to parse all these bytes: ```python= a = [64, 26, 64, 26, 7, 0, 7, 0, 7, 0, 3, ...] # Bytecode in the PE file li = [] mem = [0] * 300 mem[0] = 300 num = 0 def rol1(x): return ((x << 1) + (((1 << 15) & x) >> 15)) & 0xffff for i in range(0, len(a), 2): li.append((a[i+1] << 8) + a[i]) mem += li mem += [0] * 100000 k = 'a' * 26 for i in range(0, len(li), 3): if mem[4] == 1: mem[4] = 0 print(chr(mem[3])) if mem[6] == 1: mem[6] = 0 mem[5] = ord(k[num]) num += 1 print('mem[5] <-- inp') try: id = mem[0] arg1 = mem[id + 1] arg0 = mem[id] arg2 = mem[id + 2] mem[0] = id + 3 temp = arg1 arg1 = ~(mem[arg0] | mem[arg1]) & 0xffff mem[arg2] = arg1 mem[1] = rol1(arg1) print('mem[{}] <-- ~(mem[{}] ({}) | mem[{}]) ({}) (Val = {}, id = {}, {})'.format(hex(arg2), hex(arg0), hex(mem[arg0]), hex(temp), hex(mem[temp]), hex(arg1), id, hex(id))) except Exception as e: print(e) print(hex(arg0), hex(arg1), hex(arg2)) exit(0) ``` And I found something that's really interesting ``` ... mem[5] <-- inp # 26th input character mem[0x7] <-- ~(mem[0x5] (0x61) | mem[0x5]) (0x61) (Val = 0xff9e, id = 1009, 0x3f1) mem[0x1a3c] <-- ~(mem[0x7] (0xff9e) | mem[0x7]) (0xff9e) (Val = 0x61, id = 1012, 0x3f4) mem[0x7] <-- ~(mem[0x3fd] (0x400) | mem[0x3fd]) (0x400) (Val = 0xfbff, id = 1015, 0x3f7) mem[0x0] <-- ~(mem[0x7] (0xfbff) | mem[0x7]) (0xfbff) (Val = 0x400, id = 1018, 0x3fa) mem[0x3fe] <-- ~(mem[0x1a23] (0x61) | mem[0x1a23]) (0x61) (Val = 0xff9e, id = 1024, 0x400) mem[0x3ff] <-- ~(mem[0x1a0e] (0x57) | mem[0x1a0e]) (0x57) (Val = 0xffa8, id = 1027, 0x403) mem[0x7] <-- ~(mem[0x40c] (0x40f) | mem[0x40c]) (0x40f) (Val = 0xfbf0, id = 1030, 0x406) mem[0x0] <-- ~(mem[0x7] (0xfbf0) | mem[0x7]) (0xfbf0) (Val = 0x40f, id = 1033, 0x409) mem[0x40d] <-- ~(mem[0x1a23] (0x61) | mem[0x1a23]) (0x61) (Val = 0xff9e, id = 1039, 0x40f) mem[0x40e] <-- ~(mem[0x3ff] (0xffa8) | mem[0x3ff]) (0xffa8) (Val = 0x57, id = 1042, 0x412) mem[0x7] <-- ~(mem[0x40d] (0xff9e) | mem[0x40e]) (0x57) (Val = 0x20, id = 1045, 0x415) mem[0x7] <-- ~(mem[0x7] (0xffdf) | mem[0x7]) (0xffdf) (Val = 0xffdf, id = 1048, 0x418) mem[0x3ff] <-- ~(mem[0x7] (0xffdf) | mem[0x7]) (0xffdf) (Val = 0x20, id = 1051, 0x41b) mem[0x7] <-- ~(mem[0x424] (0x427) | mem[0x424]) (0x427) (Val = 0xfbd8, id = 1054, 0x41e) mem[0x0] <-- ~(mem[0x7] (0xfbd8) | mem[0x7]) (0xfbd8) (Val = 0x427, id = 1057, 0x421) mem[0x425] <-- ~(mem[0x1a0e] (0x57) | mem[0x1a0e]) (0x57) (Val = 0xffa8, id = 1063, 0x427) mem[0x426] <-- ~(mem[0x3fe] (0xff9e) | mem[0x3fe]) (0xff9e) (Val = 0x61, id = 1066, 0x42a) mem[0x7] <-- ~(mem[0x425] (0xffa8) | mem[0x426]) (0x61) (Val = 0x16, id = 1069, 0x42d) mem[0x7] <-- ~(mem[0x7] (0xffe9) | mem[0x7]) (0xffe9) (Val = 0xffe9, id = 1072, 0x430) mem[0x3fe] <-- ~(mem[0x7] (0xffe9) | mem[0x7]) (0xffe9) (Val = 0x16, id = 1075, 0x433) mem[0x7] <-- ~(mem[0x3fe] (0x16) | mem[0x3ff]) (0x20) (Val = 0xffc9, id = 1078, 0x436) mem[0x1a0d] <-- ~(mem[0x7] (0xffc9) | mem[0x7]) (0xffc9) (Val = 0x36, id = 1081, 0x439) mem[0x7] <-- ~(mem[0x1a0d] (0x36) | mem[0x1ee]) (0x0) (Val = 0xffc9, id = 1084, 0x43c) mem[0x1ee] <-- ~(mem[0x7] (0xffc9) | mem[0x7]) (0xffc9) (Val = 0x36, id = 1087, 0x43f) ... ``` First approach --- If we look at the parsed code closely, we could see that value at `0x1ee` acts like a flag that check our input. If the character is correct, it'll set the value at `0x1ee` to zero, or a random number in the other hand. So my first approach is that I'll reverse the logic of this piece of code and find the right character for my input. It sounds good, but there's a problem: Every character has a different check. At this time I knew that I have to change my approach (the right one to me). Seccond approach --- Just bruteforce. ```python= a = [64, 26, 64, 26, 7, 0, 7, 0, 7, 0, 3, ...] li = [] mem = [0] * 300 mem[0] = 300 num = 0 def rol1(x): return ((x << 1) + (((1 << 15) & x) >> 15)) & 0xffff for i in range(0, len(a), 2): li.append((a[i+1] << 8) + a[i]) mem += li mem += [0] * 100000 for t in range(256): mem = [0] * 300 mem[0] = 300 num = 0 for i in range(0, len(a), 2): li.append((a[i + 1] << 8) + a[i]) mem += li mem += [0] * 100000 # Change these values to bruteforce each char # =========================================== k = list("a" * 26) k[0] = chr(t) modifiedID = 0x43f # modifiedID is the line number that it modifies the value at 0x1ee. # For example this is the set that I bruteforce the first char. I'm too lazy to write a script that can bruteforce all character at one time :p # =========================================== for i in range(0, len(li), 3): if mem[4] == 1: mem[4] = 0 # print(chr(mem[3])) if mem[6] == 1: mem[6] = 0 mem[5] = ord(k[num]) num += 1 # print('mem[5] <-- inp') try: id = mem[0] arg1 = mem[id + 1] arg0 = mem[id] arg2 = mem[id + 2] mem[0] = id + 3 temp = arg1 arg1 = ~(mem[arg0] | mem[arg1]) & 0xffff mem[arg2] = arg1 mem[1] = rol1(arg1) # print('mem[{}] <-- ~(mem[{}] ({}) | mem[{}]) ({}) (Val = {}, id = {}, {})'.format(hex(arg2), hex(arg0), hex(mem[arg0]), hex(temp), hex(mem[temp]), hex(arg1), id, hex(id))) if id == modifiedID: if mem[0x1a0d] == 0: print(chr(t)) break except Exception as e: print(e) print(hex(arg0), hex(arg1), hex(arg2)) exit(0) ``` `TetCTF{WE1rd_v1R7UaL_M@chINE_Ev3R}` [RE] hidden === **Description:** The way that I decode the media file. **Given files:** [hidden.zip](https://drive.google.com/file/d/1IXCWXao5HEfa3P7e2-hO9EplD9tu4W67/view?usp=sharing) (Extract password: `TetCTF-60de12aee522b012b8f0a917e2f84297`) **Thanks IxZ - a teammate of mine, who found a key part of this chal, so that I could finish the rest and find the flag.** First glance --- There are lots of weird API calls related to **Media Foundation**, which I have never encountered before and had no clue although there're documents about it on the internet. All I did was assuming my theories and applying them, and this actually worked. The code --- ```c= if ( argc != 8 ) { if ( !RegOpenKeyExW( HKEY_LOCAL_MACHINE, L"Software\\Classes\\CLSID\\{26613686-ae12-4b5c-9139-4c8211b45a4b}\\Parameters", 0, 0x20019u, (PHKEY)&TokenHandle) ) { cbData = 16; v8 = RegQueryValueExW((HKEY)TokenHandle, L"1", 0, 0, arg2, &cbData); cbData = 4; v9 = RegQueryValueExW((HKEY)TokenHandle, L"2", 0, 0, arg3, &cbData) | v8; cbData = 4; v10 = RegQueryValueExW((HKEY)TokenHandle, L"3", 0, 0, arg4, &cbData) | v9; cbData = 4; v11 = RegQueryValueExW((HKEY)TokenHandle, L"4", 0, 0, &arg5[4], &cbData) | v10; cbData = 4; v12 = RegQueryValueExW((HKEY)TokenHandle, L"5", 0, 0, arg5, &cbData) | v11; cbData = 128; v13 = RegQueryValueExW((HKEY)TokenHandle, L"6", 0, 0, (LPBYTE)Data, &cbData) | v12; RegCloseKey((HKEY)TokenHandle); if ( v13 ) { print("Decode error!\n"); return 1; } } goto LABEL_38; } ``` It checks whether we provide enough arguments or not. If not, the program will read arguments in regkey. I immidiately ignore this code because I want to see what will it do if I pass enough number of arguments. ```c= v52 = argv[2]; v14 = LoadLibraryW(L"shell32.dll"); v15 = v14; if ( v14 ) { v16 = GetProcAddress(v14, (LPCSTR)0x2C0); if ( v16 ) { LABEL_33: ((void (__stdcall *)(const char *, BYTE *))v16)(v52, arg2); //GUIDFromString ... } *(_DWORD *)arg3 = stringToNumber((int)argv[3]); *(_DWORD *)arg4 = stringToNumber((int)argv[4]); *(_DWORD *)arg5 = stringToNumber((int)argv[5]); arg6 = stringToNumber((int)argv[6]); arg7 = argv[7]; *(_DWORD *)&arg5[4] = arg6; v20 = (char *)((char *)Data - arg7); do { v21 = *(_WORD *)arg7; arg7 += 2; *(_WORD *)&arg7[(_DWORD)v20 - 2] = v21; } while ( v21 ); ``` So an example of a set of arguments should look like this: `Video.avi {bd3940b2-1709-4911-8fe6-d813d87e1a42} 1234 5678 9012 3456 7890` I immidiately step into rabbit holes right after that. I try to understand every API calls that the binary have, which is really dump since it's not worth trying to understand an API call for hours. Then IxZ notice something: `C:\Users\Admin\AppData\Local\Temp\server.dll` That's not where a normal dll should be placed, definitely there's **dll injection** somewhere in the code. I then analyze the custom `server.dll`, especially `sub_10001630`. The comparison --- ```c= if ( v5 >= 0 ) { v5 = v18->lpVtbl->Compare(v18, v20, MF_ATTRIBUTES_MATCH_OUR_ITEMS, (BOOL *)&v19); if ( v5 >= 0 ) { v5 = 0; if ( v19 ) *(_BYTE *)(a1 + 89) = 1; else *(_BYTE *)(a1 + 89) = 0; } } ``` This code looks really suspicious since there's a compare function. If we read document about this: https://docs.microsoft.com/en-us/windows/win32/api/mfobjects/nf-mfobjects-imfattributes-compare, it actually check the input we pass to the program that match the expected input they want. And the expected set of input should look like this: `Video.avi {00000029-0000-0010-8000-00AA00389B71} 1000 800 120 1 ????` The final argument isn't in the above comparison, instead it's used for decrypting the `jpeg` file. So all we have to do is xor the first few bytes with the jpeg header: ```python= a = [int(i, 16) for i in '9E BF AE B9 52 74 1D 02 2A 35 61 66'.split(' ')] b = [int(i, 16) for i in 'FF D8 FF E0 00 10 4A 46 49 46 00 01'.split(' ')] for i in range(len(b)): print(chr(b[i] ^ a[i]), end = '') ``` And the final set of input that produce the final flag: `Video.avi {00000029-0000-0010-8000-00AA00389B71} 1000 800 120 1 agQYRdWDcs` ![pepega with a flag](https://i.imgur.com/OhBpNme.jpg) `TetCTF{<3h4ppy_n3w_ye4r<3}` [RE] crackme pls === **Description:** Just a simple crackme, but we spiced it up a little bit. **Given files:** [crackme-pls.zip](https://drive.google.com/file/d/1AY2c6Tgx9-njER4UTqtmWf_FaHWfpwW9/view?usp=sharing) ~~Deobfuscation~~ --- Look through the program, my first thought is that writing a tool that can deobfuscate all these obnoxious code. There're all kinds of obfuscation: `Instructions Substitution` to obfuscate API call, `Control Flow Flattening`, `String Encrypting`,... And I don't know how to write such tools that can make the code better, so maybe I should think of something that can help me reverse this binary better. Gather information --- Maybe look through the assembly code and debug it over and over again would help me have a better understanding of the binary. ```bash= .text:0000000000403D90 push rbp .text:0000000000403D91 push r15 .text:0000000000403D93 push r14 .text:0000000000403D95 push r13 .text:0000000000403D97 push r12 .text:0000000000403D99 push rbx .text:0000000000403D9A sub rsp, 0C8h .text:0000000000403DA1 mov rax, cs:off_40D330 .text:0000000000403DA8 mov [rsp+0F8h+var_D8], rax .text:0000000000403DAD mov rax, cs:off_40D338 .text:0000000000403DB4 mov [rsp+0F8h+var_D0], rax .text:0000000000403DB9 mov rax, cs:off_40D340 .text:0000000000403DC0 mov [rsp+0F8h+var_C8], rax .text:0000000000403DC5 mov rax, cs:off_40D348 .text:0000000000403DCC mov [rsp+0F8h+var_C0], rax .text:0000000000403DD1 mov rax, cs:off_40D350 .text:0000000000403DD8 mov [rsp+0F8h+var_B8], rax .text:0000000000403DDD mov rax, cs:off_40D358 .text:0000000000403DE4 mov [rsp+0F8h+var_B0], rax .text:0000000000403DE9 mov rax, cs:off_40D360 .text:0000000000403DF0 mov [rsp+0F8h+var_A8], rax .text:0000000000403DF5 mov rax, cs:off_40D368 .text:0000000000403DFC mov [rsp+0F8h+var_A0], rax .text:0000000000403E01 mov rax, cs:off_40D370 .text:0000000000403E08 mov [rsp+0F8h+var_98], rax .text:0000000000403E0D mov rax, cs:off_40D378 .text:0000000000403E14 mov [rsp+0F8h+var_90], rax .text:0000000000403E19 mov rax, cs:off_40D380 .text:0000000000403E20 mov [rsp+0F8h+var_88], rax .text:0000000000403E25 mov rax, cs:off_40D388 .text:0000000000403E2C mov [rsp+0F8h+var_80], rax .text:0000000000403E31 mov rax, cs:off_40D390 .text:0000000000403E38 mov [rsp+0F8h+var_78], rax .text:0000000000403E40 mov rax, cs:off_40D398 .text:0000000000403E47 mov [rsp+0F8h+var_70], rax .text:0000000000403E4F mov rax, cs:off_40D3A0 .text:0000000000403E56 mov [rsp+0F8h+var_68], rax .text:0000000000403E5E mov rax, cs:off_40D3A8 .text:0000000000403E65 mov [rsp+0F8h+var_60], rax .text:0000000000403E6D mov rax, cs:off_40D3B0 .text:0000000000403E74 mov [rsp+0F8h+var_58], rax .text:0000000000403E7C mov rax, cs:off_40D3B8 .text:0000000000403E83 mov [rsp+0F8h+var_50], rax .text:0000000000403E8B mov rax, cs:off_40D3C0 .text:0000000000403E92 mov [rsp+0F8h+var_48], rax .text:0000000000403E9A mov rax, cs:off_40D3C8 .text:0000000000403EA1 mov [rsp+0F8h+var_40], rax .text:0000000000403EA9 mov rax, cs:off_40D3D0 .text:0000000000403EB0 mov [rsp+0F8h+var_38], rax .text:0000000000403EB8 mov r14d, offset funcTable .text:0000000000403EBE mov eax, offset funcTable .text:0000000000403EC3 xor rax, 0FFFFFFFFFFFFFF4Fh .text:0000000000403EC9 mov ecx, offset funcTable .text:0000000000403ECE or rcx, 0FFFFFFFFFFFFFF4Fh .text:0000000000403ED5 lea rcx, [rcx+rcx*2] .text:0000000000403ED9 sub rcx, rax ... ``` This is the main function, and all core functions should look similar to this. `mov rax, cs:off_40D360` actually give us some hints: This function will use a small piece of code pointed by `off_40D360`. The strategy --- So everytime I encounter a core function that has a structure like this, I'll put a breakpoint at the entry of those pieces of code that looks important. This way I can ignore the `Control Flow Flattening` strat, which is really good. After spending sometime debugging, I figure out this is a nanomite chal, but both parent and child process look really fascinating. Child Process --- It'll read 60 characters as our input to check. After that it'll decrypt something looks similar to an **elf file**. And then xor first 4 bytes of our input to the encrypted header to create a valid elf file. This way I can find the first 4 bytes of our input: `h3ll`. Parent Process --- It'll trace the child's syscall by using `PTRACE_SYSCALL`, and will do something base on this. I actually ignore this since it gives me no helpful information about what does parent process actually do. I skip a bunch of trash code and finally come to the right place: - Read our input in the child process - Perform RC4 cipher (not encrypting, just extracting byte stream) with key `hO7nMqKeFYv9VVAS7qlX` - Check the next 48 input chars (skip the first 4 byte, which is `h3ll`) by doing some encryption algorithm. Here's the script that I used to recover that 48 chars: ```python= def encryption(inp0, inp1, oldInp): eax = inp0 ecx = inp1 ebp = ((~eax) & ecx) ecx = ((~ecx) & eax) + ebp eax = oldInp ^ ecx ebx = inp0 & eax eax ^= inp0 eax = (eax + ebx * 2) return eax li = [121, 148, 249, 108, 0, 198, 52, 253, 133, 156, 119, 148, 246, 218, 252, 218, 242, 64, 156, 123, 116, 220, 109, 215, 101, 245, 50, 197, 104, 74, 48, 87, 9, 41, 22, 25, 249, 249, 250, 232, 206, 112, 197, 216, 28, 102, 193, 101] res = [194, 70, 82, 103, 8, 95, 185, 247, 153, 22, 82, 34, 71, 77, 78, 240, 105, 6, 80, 79, 196, 187, 238, 159, 67, 233, 202, 140, 51, 199, 122, 92, 65, 126, 98, 133, 205, 109, 100, 10, 22, 81, 151, 71, 193, 116, 158, 67] oldVal = 0 for id in range(len(li)): if id != 0: oldVal = li[id - 1] for i in range(256): if (encryption(li[id], i, oldVal) & 0xff) == res[id]: print(chr(i), end = '') ``` Combining the `h3ll` one, we have `h3ll0_4nd_w3lc0m3_t0_th3_w0rld_0f_0bfusc4ti0n_gratzz`. But where's the final 8 bytes? I run the binary again and find some suspicious string `!!!!!111`. Append it to the string we've found so far and check it: ```bash= nguyenguyen753@ubuntu:~/CTF/tetCTF/crackmePls$ ./crackme-pls.bin Just a simple crack me, gimme your password Password: h3ll0_4nd_w3lc0m3_t0_th3_w0rld_0f_0bfusc4ti0n_gratzz!!!!!111 Nicely done, your flag is TetCTF{h3ll0_4nd_w3lc0m3_t0_th3_w0rld_0f_0bfusc4ti0n_gratzz!!!!!111} ``` `TetCTF{h3ll0_4nd_w3lc0m3_t0_th3_w0rld_0f_0bfusc4ti0n_gratzz!!!!!111}` <style> html, body, .ui-content { background-color: #333; color: #ddd; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { color: #ddd; } .markdown-body h1, .markdown-body h2 { border-bottom-color: #ffffff69; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: #fff; } .markdown-body img { background-color: transparent; } .ui-toc-dropdown .nav>.active:focus>a, .ui-toc-dropdown .nav>.active:hover>a, .ui-toc-dropdown .nav>.active>a { color: white; border-left: 2px solid white; } .expand-toggle:hover, .expand-toggle:focus, .back-to-top:hover, .back-to-top:focus, .go-to-bottom:hover, .go-to-bottom:focus { color: white; } .ui-toc-dropdown { background-color: #333; } .ui-toc-label.btn { background-color: #191919; color: white; } .ui-toc-dropdown .nav>li>a:focus, .ui-toc-dropdown .nav>li>a:hover { color: white; border-left: 1px solid white; } .markdown-body blockquote { color: #bcbcbc; } .markdown-body table tr { background-color: #5f5f5f; } .markdown-body table tr:nth-child(2n) { background-color: #4f4f4f; } .markdown-body code, .markdown-body tt { color: #eee; background-color: rgba(230, 230, 230, 0.36); } a, .open-files-container li.selected a { color: #5EB7E0; } </style>