35C3 CTF
Post

35C3 CTF

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 out NOPE. if the decrypted data doesn’t starts with some predefined bytes (in this case, it is the flag prefix 35C3)

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

corebot-1

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 be 0x25c3. The flag is 35C3_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.

0pack-1

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 the cpuid 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 :)

This post is licensed under CC BY 4.0 by the author.