CInsects 19 WriteUp bufcore
18 July 2019 by Johannes
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.
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.
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:
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?
- The
checkpw
function setr0
was set to1
to indicate that we entered the wrong password. However,1
is also the syscall number for writing. - 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. - 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 ifr1
were 0mem[:r2]
should still contain the flag.
Combined, this means that we will dump the entire memory mem[r1:r1+r2]
, including the flag.
Patching the service
While the service could be patched by replacing
with
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.