I played with Wanna.One
and got the 100$ price for being the first team to clear Rev chals
Description:
My machine is running very simple, can you understand it?
Given files:
magicbox.zip (Extract password: TetCTF-7c71accd8175c365688a233a14fc4a1a
)
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)
...
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).
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}
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.
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.
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
.
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
TetCTF{<3h4ppy_n3w_ye4r<3}
Description:
Just a simple crackme, but we spiced it up a little bit.
Given files:
crackme-pls.zip
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.
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
.
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.
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
.
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:
hO7nMqKeFYv9VVAS7qlX
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}