SHCTF 2024
3※ No stack overflow2
-
考點:ret2libc,libc 庫查詢,整數溢位
-
題目附件:No stack overflow2
-
在 linux 下使用
checksec
檢視該程式開啟的保護,發現Arch
為amd64-64-little
,這說明這是一個 64位 的程式,並且採用了 小端 儲存,即低位對應低地址,高位對應高地址。 -
下方的
RELRO
,這是一種透過設定 重定位相關表 的許可權為 只讀 來防止其被修改的安全機制,我們只關注其對 got 表的影響。Partial RELRO
指的是部分開啟,此時 got 表被設定為:每個表項只有在未解析過該函式地址前是可寫,載入地址後改為只讀: -
不過我們暫時不關心,把 vuln 檔案拖進 IDA64 開啟,點選左側
main
,按 F5 反編譯,發現程式主要功能是先讀入一個長度,接著檢測長度小於等於 256 時再讀入最多這麼長的位元組: -
我們看 IDA64 提示的儲存地址,發現
nbytes_4
長度也是0x100
即 256 個位元組,難道無法溢位嗎: -
我們發現關鍵在於傳入引數時,將長度轉為了
unsigned int
無符號整數,這就使得首位的符號位被當做了資料,那麼如果原先輸入長度為 -1,二進位制對應的0xFFFFFFFF
,那麼轉為無符號整數後就變為了 2147483647,就可以繞過上方的長度檢測了: -
那麼接下來就是要構造我們的 payload1 了,但是一個個檢視左側函式發現沒有
system
相關的,按 Shift+F12 也沒有發現/bin/sh
字串。 -
但是發現左側有
puts
函式,那麼我們可以考慮使用和上一題相同的方法:將puts
函式的 got 表地址洩露出來,然後查詢其在動態庫中的位置,二者相減得到偏移量。 接著就可以先查詢動態庫中的system
函式和字串/bin/sh
的位置,來計算出它們在程式中的位置,然後便可以構造 payload 模擬執行system("/bin/sh")
了。 -
那麼我們先開啟 ELF 檔案,查詢
puts
函式 plt 表,got表所在的地址,以及puts
結束後返回的main
函式的地址: -
這裡的
process('./vuln')
指的是先不連線伺服器,而是用本地檔案來模擬,方便我們除錯: -
然後在第一處輸入 -1 繞過長度檢測:
-
但是我們忽然發現一個問題,本題與上一題不同的是,上一題是 32位 程式,所有引數均透過棧來傳遞,我們只需要將函式的傳參壓入棧中即可模擬函式執行。但是這題是 64位 程式,函式的前 6個 引數是依次透過 6個 暫存器來傳遞:
rdi
,rsi
,rdx
,rcx
,r8
,r9
,之後的更多的引數才用棧來傳遞。這樣使得程式執行的速度提升了不少,但是對於我們棧溢位攻擊就不能直接透過棧來傳參了,需要修改暫存器的值。
-
我們可以想到,
pop rdi
指令可以將棧內寫入的內容傳給rdi
這樣就可以修改它了,那麼如果我們將pop rdi
指令所在的地址,放在函式返回的ret
處,就可以修改rip
下一條指令執行pop rdi
了! -
可是僅僅是這樣,
rip
跳轉之後就回不來了,程式流程整個被打亂,所以我們需要找一個後面緊跟著ret
指令的pop rdi
,這樣下一句還會執行ret
,將此時rsp
自加過後的棧頂的內容給rip
,不就仍然等同於繼續進行棧溢位攻擊了嗎。 -
同理,如果想要修改
rsi
,rdx
,也需要在程式中找到pop rsi
隨後ret
的程式碼片段的地址,以此來傳遞引數模擬函式執行。這個過程就是所謂的構造 ROP 鏈。
-
我們可以透過使用
ropper
工具或是ROPgadget
工具,在 linux 下快速查詢一個檔案中出現指定字串的位置,我們透過使用管道符來查詢所有pop
開頭到ret
結尾的字串,再要求其中含有rdi
,寫出以下命令查詢:ropper --file vuln --search "pop|ret" | grep "rdi"
,發現成功找到一個: -
記錄下這段程式所在的地址
pop_rdi_ret = 0x401223
,接下來就可以使用了。由於puts
函式只需要一個引數,即輸出的字串的地址,我們只需要rdi
來傳參,所以現在可以開始構造 payload1 了: -
先填充
0x100
即 256個位元組 給nbytes_4
,然後因為這是 64位 程式,要填充 8個位元組 給s
即rbp
,接下來在r
處填入我們的pop rdi;ret
程式的地址:p64(pop_rdi_ret)
,然後填入要修改的rid
資料,即puts
的傳參,也就是要輸出的字串的地址:puts_got
。 -
然後填充
pop rdi;ret
返回回來後要執行的程式的地址,我們要輸出puts
函式的 got 表裡的內容,所以這裡填呼叫的輸出函式puts
的地址:puts_plt
。 -
最後填充
puts
函式輸出完返回回來後要執行的下一個程式的地址,由於我們需要再次利用這裡的棧溢位來執行system("/bin/sh")
,所以填main
函式的首地址。 -
可以看到此時已經將地址輸出出來了,不過都是
\x
開頭的 16 進位制bytes
資料。由於 64位 程式的地址都是以\x7f
開頭的,並且由於這是個 小端程式,字串地位置儲存在低位置,所以輸出出來是倒序的,所以我們可以用.recvuntil(b'\x7f')
讀到\x7f
為止。 -
又由於雖然我們是 64位 程式雖然應該有 8 位,但使用時的編碼都是以
\x7f
開頭的 6位 編碼地址,所以我們只要讀進來的最後 6個位元組: -
然後我們要對這個 16進位制 的
bytes
資料用u64()
進行解包,但是如果直接使用的話程式會報錯: -
這是因為
u64()
每次解包需要輸入 \(8個位元組\),而剛才的地址不足 8位,我們需要在左側用.ljust(8,b'\x00')
補\x00
將其補滿 8位: -
此時再輸出發現就是正確的一個整數地址了:
-
那麼拿到了一個
puts
函式的 got 表的所在地址,接下來我們就可以透過查詢動態庫中puts
函式的地址然後計算出偏移量啦。 -
不過這道題題目並沒有把動態庫檔案直接給我們,我們需要根據洩露出來的
puts
函式的 got 表的所在地址來查詢到系統所使用的動態庫版本。
-
我們可以下載使用
LibcSearcher
這個 python 庫來開啟對應的動態庫,只需要提供某個已知函式的具體地址即可: -
接下來就和上一題一樣了,查詢
puts
函式在動態庫的地址,計算出偏移量,然後查詢system
函式和/bin/sh
字串在動態庫的地址,計算出在程式內的地址。不過要注意的是此時使用LibcSearcher
指令,需要用.dump("xxx")
來查詢某個函式的地址,用.dump("str_bin_sh")
來查詢字串/bin/sh
的位置: -
此時執行時我們會發現,查詢到多個匹配的動態庫,程式詢問我們要使用哪一個版本的,這是因為先前我們用的是
process('./vuln')
在本地除錯。而如果連線到伺服器上的時候就不需要我們進行選擇了。 -
不過現在我們需要根據自己的 ubuntu 版本來選擇對應的動態庫,我們可以先按 Ctrl+C 退出程式,在 linux 中輸入
ldd --version
來查詢版本: -
可以看到第二行,我的是
2.39-0ubuntu8.3
,那麼再次執行程式,這次就選擇這個版本的動態庫,填入程式提示的版本前方的編號 0 按回車,可以看到下方有一句該版本be choosed
就成功選擇了: -
那麼完事具備,我們現在已經執行到第二次
main
函式要求我們輸入長度的位置了,再次輸入-1
,然後開始構造我們第二次的 payload2: -
先填充前面
0x108
個字元到r
處與 payload1 一樣,然後透過pop di;ret
來傳遞system
的引數:bin_sh_addr
, -
然後填充要執行的
system
函式的地址:system_addr
-
最後的返回地址在哪裡都無所謂,因為馬上要得到系統許可權進入互動模式了,並不會返回回來用上,直接不填。
-
但是!此時執行會發現並沒有得到系統許可權,反而報錯退出了。這是因為這是採用了新的高版本的 gcc 編譯器的 64位 系統,其在呼叫動態庫中的
system
函式時,對rsp
有額外的要求: -
在準備進入
system
函式時,會對此時的rsp
也就是棧頂進行一次檢驗,要求此時指向的地址必須能被 16 整除,也就是必須以 0 結尾,否則報錯退出不予呼叫。 -
我們進入 IDA64 的
main
函式,點選nbytes_4
檢視棧空間,發現我們填充到r
的位置以 8 結尾: -
所以此時放在
r
中的pop rdi;ret
的地址以 8 結尾,接下來/bin/sh
字串的地址以 0 結尾,而system
函式的地址以 8 結尾,就無法透過高版本的rsp
檢驗。 -
那麼我們需要再呼叫
system
函式之前額外填充一個 某段程式的地址,這樣在執行system
函式時rsp
就以 0 結尾了。 -
最簡單的就是找一個只有一句
ret
指令的地址,rip
執行原先函式的ret
跳轉到這裡後,下一句還是將執行ret
,沒有區別,但是此時rsp
已經自加了一次。 -
所以我們用
ropper
指令尋找一個只有一句ret
的程式,在 linux 下輸入ropper --file vuln --search "ret"
查詢: -
記錄下程式的位置
ret = 0x40101a
,接下來只需要在呼叫system
之前多填充一個ret
的地址即可: -
此時執行完程式,在選擇動態庫版本輸入 0 後,我們發現已經進入了互動模式,輸入
ls
可以看到當前目錄下的檔案,大功告成: -
最後調整為遠端連線伺服器,
ls
一下發現有flag
,cat flag
獲取 flag: -
最後放上完整 exp(調整了一下順序):
-
除了使用 LibcSearcher 線上查詢動態庫之外,我們還可以使用一個線上網站將伺服器所使用的動態庫下載下來:
https://libc.rip/
,使用的時候只需要輸入,洩露的函式的名稱,和洩露出來的函式的地址的後三位(16進位制)即可: -
當然,如果用
puts
函式查不到對應的版本的話,可以試著用別的函式查詢,網站的內容有時候明沒有更新到最新(這裡就是,我換成了read
函式 ): -
然後可以下載下來,本地進行除錯(當然我們不知道伺服器用的是哪一個,這隻限於本地除錯程式碼用的下載)。
3※ No stack overflow2 pro
-
考點:libc 靜態連結
-
題目附件:No stack overflow2 pro
-
這題題目首先提示了,使用了靜態連結,也就是將動態連結直接寫入了程式中,這樣就沒有 plt 表和 got 表供我們使用了。
-
首先在 linux 下用
checksec vuln
檢視檔案保護情況: -
發現是 64位 小端程式,開了
Partial RELRO
,開了NX
保護,這個就是不允許執行存放在資料段的程式碼,也就是為什麼我們之前,都要費盡心思往棧裡面寫別的程式的地址的原因:程式碼直接放在棧裡面不允許執行。 -
接著是
Stack:Canary found
,這是指開啟了Canary
保護:在進入函式前生成一個校驗碼壓入棧中,在函式返回時檢測校驗碼是否被修改,若被修改則判斷棧發生了改變收到了溢位攻擊,自動結束程式。這是對棧溢位攻擊的防護。 -
那麼接下來我們拖入 IDA64 中,發現左邊亂七八糟一大堆,這正是因為靜態連結引起的,將所有動態庫裡的函式全寫進來了,如果檢視過這個檔案的大小的話,會發現它遠遠大於我們之前使用動態庫連線技術的檔案的大小:
-
我們找到加黑了的
main
,點選進入,F5 反編譯,發現和上一題的程式碼一模一樣,都是輸入一個長度,然後轉化為有符號的int
來進行判斷大小,接著往 v9 中存入不超過先前讀入的長度的位元組。很明顯這裡存在著和前幾題一樣的棧溢位: -
那麼我們記得先前有提到
Canary
保護,點開 v9 檢視棧結構找找 校驗值 存在哪裡,但是發現 v9 下面直接就是s
和r
了,並沒有找到Canary
保護的校驗值儲存的位置,那麼就不需要理會了,直接正常溢位即可執行我們想執行的程式,也就是所謂的劫持程式。 -
我們需要模擬
system("/bin/sh")
,這在動態庫裡本質是輸入指令syscall
,所以我們就需要一個寫著syscall
指令的地址,用ropper --file vuln --search "syscall"
進行查詢: -
發現很多個,我們隨便選哪個地址都可以,因為執行完
syscall
指令後我們會獲得系統許可權進入互動模式,就不用管syscall
指令之後還有什麼了,可以選最後一個syscall_addr = 0x41cbf6
: -
接下來我們要找字串
/bin/sh
,按 Shift+F12,按 alt+T 查詢字串/bin/sh
,發現並沒有跳轉,不存在現成的字串: -
所以我們只能自己找一個地址,往裡面寫入字串
/bin/sh
。首先我們需要找一個有讀和寫許可權的段,因為既要寫進去也要讀出來使用。我們按 Shift+F7 開啟段檢視,一般使用.bss
段,BSS
段通常是指用來存放程式中 未初始化 的或者 \(初始化為0\) 的 全域性變數 和 靜態變數 也就是說,只要初始值為 0 的型別,都會先放在這裡,等到再次賦值時才會被取出。所以寫在這裡面可以全域性使用。 -
我們點開
.bss
段,隨便複製一個起始位置,bss_addr = 0x4E72C0
: -
那麼接下來我們要往裡面寫資料,可以呼叫
read
函式,在左側下方輸入read
查詢函式位置: -
點進去,複製函式入口位置,
read_addr = 0x44FD90
: -
我們發現
read
函式需要三個引數,由於這是 64位 程式透過暫存器傳參,所以和上一題一樣我們要去尋找pop rdi;ret
,pop rsi;ret
,pop rdx;ret
的程式的存放位置,來改變暫存器的值為read
函式傳參: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rdi"
來找與rdi
相關的指令,在一大堆結果中找到緊挨著ret
的程式,pop_rdi_ret = 0x4022bf
: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rsi"
來找與rsi
相關的指令,同理找緊挨著ret
的程式,pop_rsi_ret = 0x40a32e
: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rdx"
來找與rsi
相關的指令時,發現沒有緊挨著ret
的程式,我們找一個離ret
最近的程式,中間仍然多了一個pop rbx
,不過也可以用,每次多傳一個 0 給rbx
即可,pop_rdx_rbx_ret = 0x49D06B
: -
最後在程式內找到
main
的起始地址,因為第一次溢位後我們輸入字串/bin/sh
還需要第二次溢位來執行system("/bin/sh")
,main_addr = 0x401B7A
: -
那麼我們可以開始構造第一次溢位的 payload1 了,需要注意的是,這一個程式輸入長度的時候用
(unsigned int)
輸入,判斷的時候轉為(int)
判斷,所以我們需要輸入2147483679
,對應的二進位制轉化為(int)
就是 -1: -
然後構造 payload1,先用 0x100 + 0x08 個位元組填充到
r
處,然後用暫存器為read
函式傳參: -
第一個參數列示讀取的檔案,為 0 表示從控制檯讀入,我們
pop_rdi_ret
後傳 0 -
第二個引數為存放的地址,我們
pop_rsi_ret
後傳bss_addr
, -
第三個引數為最大的寫入長度,可以大一點,我們
pop_rdx_rbx_ret
後傳0x100
,然後傳 0 給多的pop rbx
-
最後填充函式結束後返回的地址
main_addr
: -
且慢,這是 64 程式,需要檢驗一下呼叫
main
函式之前,rsp
是否指向的地址末尾為 0,可以簡單數一下main
函式是第 9 條指令,第一條指令以 8 結尾,此時main
也以 8 結尾,無法透過檢驗。我們需要再填充一條指令進去,這就是所謂的平衡棧操作。 -
同上一題,我們再用
ropper --file vuln --search "ret"
找一下ret
指令的位置,取單獨的指令,ret = 0x454257
: -
那麼我們此時在進入
main
之前加一個ret
指令的地址來平衡棧,構造出 payload1: -
執行可以看到此時程式成功執行了
read
函式,在輸入一串字元後重新開始執行main
函式了: -
接下來我們可以往這個
.bss
段裡面寫入/bin/sh
字串了,需要注意的是字串需要一個結束識別符號\x00
,所以我們往裡面寫的應該是b'/bin/sh\x00'
: -
接著重新再來一遍
main
函式,還是先輸入2147483649
繞過長度判斷,然後開始構造 payload2 來執行我們的system("/bin/sh")
:
-
syscall
的本質是系統透過呼叫這條指令時rax
裡面的值,來執行不同的函式,我們執行system("/bin/sh")
時需要令rax = 0x3b
來執行execve
語句。(在 32位 系統中,是用int 80h
代替syscall
,同時令eax = 0x0b
),所以我們需要找到修改rax
暫存器的程式碼段pop rax;ret
,和之前的那些一樣,都是所謂的 gadget。 -
在 linux 中用
ropper --file vuln --search "pop|ret" | grep "rax"
來查詢,pop_rax_ret = 0x4507f7
: -
那麼接下來就可以繼續構造我們的 payload2 了,先填充
0x108
個字元到r
,然後在pop_rax_ret
後傳0x3b
修改rax
, -
接著為
syscall
傳引數,一共有三個引數:第一個引數是字串地址,
pop_rdi_ret
後傳bss_addr
;第二和第三個引數涉及系統核心取參方式,系統空間與使用者空間之間的協議,都設為 0 預設即可。
-
pop_rsi_ret
後傳 0,pop_rdx_rbx_ret
後傳 0 ,再傳 0 給rbx
。 -
最後填入
syscall_addr
的地址,來呼叫系統的syscall
功能。 -
此時算一下是否棧平衡,我們數一下
syscall
的地址為第 10 個指令,此時rsp
末位為 0,無需調整。 -
執行成功獲得系統許可權,進入互動模式,輸入
ls
成功輸出當前目錄下的內容: -
轉為遠端連線伺服器再次執行,輸入
cat flag
獲得 flag: -
最後附上完整 exp(調整了下順序):
MoeCTF 2024
2※ leak_sth
-
考點:格式化字串漏洞
-
題目連結:leak_sth
-
拖進 IDA64,點左側
main
,F5 反彙編,發現存在printf
格式化字串漏洞: -
那麼可以先輸出來看看偏移量是多少,由於
buf
最多隻允許輸入 32個位元組,構造輸入為b'DDDD'+b'.%x'*9
: -
執行數一下,發現偏移 8 個的時候剛好為
buf
的開頭: -
繼續分析題目,要求我們接下來輸入的數字等於 v3,那麼我們只需要利用格式化輸出,將 v3 裡面的值輸出來就好了,檢視一下
main
的棧裡 v3 與buf
的位置關係,發現就在正上方一個位元組: -
那麼我們構造 payload 來格式化
%d
輸出偏移量為 7 的位置: -
執行,發現已經把 v3 輸出出來了,我們直接複製輸入即可獲得系統許可權,
cat flag
一下獲得 flag: