This is my first time participating in C3 CTF. Although I wasn’t able to solve many challenges within the time of the CTF, I still find the challenges really awesome and exciting. I wanted to solve pwnable challenges with the hope to learn more about exploit development of real applications, but ended up solving 2 RE ones. Here is my writeup for them :)
RE - Corebot
This challenge is a 32-bit Windows binary. When I tried running the binary for the first time, it was odd because it didn’t prompt for any input and just printed out NOPE.
However, the logic of the binary is pretty straight forward:
- There is a sequence of API calls to crypto-related functions such as:
CryptAcquireContext
CryptImportKey
CryptSetKeyParam
CryptDecrypt
- There is a API call to
GetVolumeInformation
to get information of the partition that the binary is on. Only the serial number of the volume is relevant to the rest of the challenge. - The binary performs some comparison at the end of the
_start
function after decrypting some data. It will print outNOPE.
if the decrypted data doesn’t starts with some predefined bytes (in this case, it is the flag prefix35C3
)
This reminds me a lot of level 4 of this year Flare-on challenge, which was also a windows binary involving some data decryption. Although this is much simpler, there are at least these 2 ways to solve this:
- Statically reverse engineer the encryption and figure out what is the correct input that satisfies the condition after some data is decrypted.
- Bruteforce the input.
The problem now is to find what data affect the comparison since it doesn’t prompt for any user input. It turned out that the binary use the serial number mentioned above to carry out the calculation. The serial number is a 32-bit integer. However, only the lower 16-bit (or 2 bytes) are used for the calculation
With only 2 bytes being used, I chose to bruteforce the data immediately. This time, I decided to use the radare2
scripting engine r2pipe
to solve this so that I can learn it at the same time. r2pipe
is really awesome because it can actually carry out commands as if you were interacting with radare2
to debug the binary. Since there is no user input, the data has to be modified during execution. The idea is to set a breakpoint right after the program get the serial number and modify the register accordingly.
I am certain that my script can be further optimized, but it was good enough to solve the challenge in-time. There were a few problems that I encountered while writing the script:
- The code is unmapped before it was execute so I have to figure out the mapped addresses to set the necessary breakpoints.
- The script run fairly slowly because I have to open a whole new
radare2
process for every trial. I think there is a way to redirect the execution so that it doesn’t have to run from scratch like that, but reuses the data it obtained before. I optimized it a little bit by running 2 threads and printing the output while it is running. I’ve tried with more threads but it bugged out after a while. While it was still running, I looked for the flag string in the output and figured out the flag and the correct bytes to be0x25c3
. The flag is35C3_MalwareAuthorKryptoChef
.
If you know of a way that I can further optimize this, I would love to know since this is my first time solving a challenge with r2pipe
.
Anyway, here’s the script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from __future__ import print_function
import r2pipe
from multiprocessing.dummy import Pool as ThreadPool
hook_1 = 0x19a
hook_2 = 0x13f
flags = []
def r2Breakpoint(r2, addr):
r2.cmd('db {}'.format(addr))
def solve(key):
r2 = r2pipe.open('corebot.exe', flags=['-2'])
r2.cmd('aaa')
r2.cmd('doo')
r2.cmd('dc')
# Figure out the mapped addresses
memmap = r2.cmd('dm').split('\n')
memmap = [x for x in memmap if 'corebot.exe' in x and '.text' in x][0]
memmap = memmap.split('-')[0]
memmap = int(memmap, 16)
# Then set the breakpoints
r2Breakpoint(r2, hex(memmap + hook_1))
r2Breakpoint(r2, hex(memmap + hook_2))
r2.cmd('dc')
# The breakpoint is hit. Modify the register here
eax = r2.cmd('dr?eax')
eax = int(eax, 16)
eax = (eax >> 16) << 16
eax += key
seteax = 'dr eax={}'.format(hex(eax))
r2.cmd(seteax)
eax = r2.cmd('dr?eax')
# Continue running and hit the breakpoint where the output is going to be printed out.
# Fetch the output then print it out.
r2.cmd('dc')
buffer = r2.cmd('pv @esp+4')
flag = r2.cmd('pvz @{}'.format(buffer))
r2.cmd('q')
print(key, flag)
return str(key), flag
def main():
pool = ThreadPool(2)
results = pool.map(solve, range(0x10000))
pool.close()
pool.join()
for r in results:
print(repr(r))
f = open('output.txt', 'wb')
f.write(''.join([' == '.join(x) for x in results]))
f.close()
if __name__ == '__main__':
main()
# 0x25c3
RE - 0pack
This is not a hard challenge, but it’s still a little bit tricky to solve. When I opened the binary in a disassembler, the functions were all 0
as described. Running the binary and giving it some dummy input resulted in the string Awwww ಠ_ಠ
to be printed.
As expected, this is some kind of binary packing. I thought the binary will be calling the main function at some point. I gave it a shot by placing a breakpoint at the call to __libc_start_main
with the hope that the address of the real main function would be revealed here. And it was. I realized this because of the reference to the prompt Input password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
➜ 0pack r2 -A -d 0pack.elf
Process with PID 28177 started...
= attach 28177 28177
bin.baddr 0x557b2e3d6000
Using 0x557b2e3d6000
asm.bits 64
[ WARNING : block size exceeding max block size at 0x557b2e3d6e7d
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d96cc
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d89e1
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d936b
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d7f52
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d9b93
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d67a8
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d752f
[+] Try changing it with e anal.bb.maxsize
WARNING : block size exceeding max block size at 0x557b2e3d6759
[+] Try changing it with e anal.bb.maxsize
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[TOFIX: afta can't run in debugger mode.ions (afta)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.
= attach 28177 28177
28177
-- Wrong argument
[0x7f9f17df2090]> db sym.imp.__
sym.imp.__stack_chk_fail sym.imp.__libc_start_main sym.imp.__cxa_finalize
[0x7f9f17df2090]> db sym.imp.__libc_start_main
[0x7f9f17df2090]> dc
hit breakpoint at: 557b2e3e8770
[0x557b2e3e8770]> dr
rax = 0x0000001c
rbx = 0x00000000
rcx = 0x557b2e3e8e90
rdx = 0x7fff8df34b88
r8 = 0x557b2e3e8f00
r9 = 0x7f9f17e019a0
r10 = 0x557b2e3e87b0
r11 = 0x00000000
r12 = 0x557b2e3d6000
r13 = 0x7fff8df34b80
r14 = 0x00000000
r15 = 0x557b2e3d6000
rsi = 0x00000001
rdi = 0x557b2e3e89a0
rsp = 0x7fff8df34b68
rbp = 0x00000000
rip = 0x557b2e3e8770
rflags = 0x00000202
orax = 0xffffffffffffffff
[0x557b2e3e8770]> sr rdi;pd 30
;-- rdi:
0x557b2e3e89a0 55 push rbp
0x557b2e3e89a1 4889e5 mov rbp, rsp
0x557b2e3e89a4 4881eca00000. sub rsp, 0xa0
0x557b2e3e89ab 89bd6cffffff mov dword [rbp - 0x94], edi
0x557b2e3e89b1 4889b560ffff. mov qword [rbp - 0xa0], rsi
0x557b2e3e89b8 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
0x557b2e3e89c1 488945f8 mov qword [rbp - 8], rax
0x557b2e3e89c5 31c0 xor eax, eax
0x557b2e3e89c7 c6857dffffff. mov byte [rbp - 0x83], 1
0x557b2e3e89ce 48b8496e7075. movabs rax, 0x6170207475706e49 ; 'Input pa'
0x557b2e3e89d8 48894590 mov qword [rbp - 0x70], rax
0x557b2e3e89dc 48b87373776f. movabs rax, 0x203a64726f777373 ; 'ssword: '
0x557b2e3e89e6 48894598 mov qword [rbp - 0x68], rax
0x557b2e3e89ea c645a000 mov byte [rbp - 0x60], 0
0x557b2e3e89ee 488d4590 lea rax, [rbp - 0x70]
0x557b2e3e89f2 4889c6 mov rsi, rax
0x557b2e3e89f5 488d3d180500. lea rdi, [0x557b2e3e8f14] ; "%s"
0x557b2e3e89fc b800000000 mov eax, 0
0x557b2e3e8a01 e85afdffff call sym.imp.printf ; int printf(const char *format)
0x557b2e3e8a06 488b15531620. mov rdx, qword [0x557b2e5ea060] ; section..bss ; [0x557b2e5ea060:8]=0x7f9f17deba00
0x557b2e3e8a0d 488d4580 lea rax, [rbp - 0x80]
0x557b2e3e8a11 be0f000000 mov esi, 0xf ; 15
0x557b2e3e8a16 4889c7 mov rdi, rax
0x557b2e3e8a19 e862fdffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
0x557b2e3e8a1e bf0a000000 mov edi, 0xa
0x557b2e3e8a23 e818fdffff call sym.imp.putchar ; int putchar(int c)
0x557b2e3e8a28 0fb64580 movzx eax, byte [rbp - 0x80]
0x557b2e3e8a2c 88857effffff mov byte [rbp - 0x82], al
0x557b2e3e8a32 4c89f8 mov rax, r15
0x557b2e3e8a35 480575240100 add rax, 0x12475
[0x557b2e3e89a0]>
After a quick look at this function, I made the following observations:
The function gets the input then perform byte-by-byte comparison of the input. This is an example of the comparison of 1 byte. If the comparison fails, it sets a variable to 0 but doesn’t immediately stop the process. There are 14 such comparisons so the password is 14-byte long.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0x557b2e3e8a28 0fb64580 movzx eax, byte [rbp - 0x80] 0x557b2e3e8a2c 88857effffff mov byte [rbp - 0x82], al 0x557b2e3e8a32 4c89f8 mov rax, r15 0x557b2e3e8a35 480575240100 add rax, 0x12475 0x557b2e3e8a3b 0fb600 movzx eax, byte [rax] 0x557b2e3e8a3e 88857fffffff mov byte [rbp - 0x81], al 0x557b2e3e8a44 0fb6857effff. movzx eax, byte [rbp - 0x82] 0x557b2e3e8a4b 3a857fffffff cmp al, byte [rbp - 0x81] ,=< 0x557b2e3e8a51 750e jne 0x557b2e3e8a61 | 0x557b2e3e8a53 b800000000 mov eax, 0 | 0x557b2e3e8a58 e8cafeffff call 0x557b2e3e8927 | 0x557b2e3e8a5d 84c0 test al, al ,==< 0x557b2e3e8a5f 7407 je 0x557b2e3e8a68 |`-> 0x557b2e3e8a61 c6857dffffff. mov byte [rbp - 0x83], 0 `--> 0x557b2e3e8a68 0fb64581 movzx eax, byte [rbp - 0x7f]
In each comparison, a function at
0x557b2e3e8927
is called. This function calls another function which involves thecpuid
instruction, so my guess was that it detects virtual machines.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[0x5555b0c259a0]> pd 24 @0x5555b0c258e0 | 0x5555b0c258e0 55 push rbp | 0x5555b0c258e1 4889e5 mov rbp, rsp | 0x5555b0c258e4 0f31 rdtsc | 0x5555b0c258e6 8945e8 mov dword [rbp - 0x18], eax | 0x5555b0c258e9 8955ec mov dword [rbp - 0x14], edx | 0x5555b0c258ec 8b45e8 mov eax, dword [rbp - 0x18] \ 0x5555b0c258ef 8b55ec mov edx, dword [rbp - 0x14] 0x5555b0c258f2 48c1e220 shl rdx, 0x20 0x5555b0c258f6 4809d0 or rax, rdx 0x5555b0c258f9 488945f0 mov qword [rbp - 0x10], rax 0x5555b0c258fd b800000000 mov eax, 0 0x5555b0c25902 0fa2 cpuid 0x5555b0c25904 0f31 rdtsc 0x5555b0c25906 8945e8 mov dword [rbp - 0x18], eax 0x5555b0c25909 8955ec mov dword [rbp - 0x14], edx 0x5555b0c2590c 8b45e8 mov eax, dword [rbp - 0x18] 0x5555b0c2590f 8b55ec mov edx, dword [rbp - 0x14] 0x5555b0c25912 48c1e220 shl rdx, 0x20 0x5555b0c25916 4809d0 or rax, rdx 0x5555b0c25919 488945f8 mov qword [rbp - 8], rax 0x5555b0c2591d 488b45f8 mov rax, qword [rbp - 8] 0x5555b0c25921 482b45f0 sub rax, qword [rbp - 0x10] 0x5555b0c25925 5d pop rbp 0x5555b0c25926 c3 ret
I was able to go through all such comparisons manually and extract the password: ThisIsATriumph
. I think my guess about the VM detection was correct since I was running the binary inside a VM and the extracted password didn’t result in a “winning-output”. I submitted the flag and it was correct :)