Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

First few words

I played with Wanna.One and got the 100$ price for being the first team to clear Rev chals

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
. 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 (Extract password: TetCTF-7c71accd8175c365688a233a14fc4a1a)

Look through the code

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:

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.

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 (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

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.

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

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:

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

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

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.

.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:

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:

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}