ret2text
Return-to-text (ret2text) 是一种利用技术,它通过劫持程序的控制流并重定向到已存在的可执行文本(即程序代码)的某个位置来执行任意代码。
漏洞源码
|
|
ASLR
ASLR(Address Space Layout Randomization)会随机化进程在内存中的关键区域位置。每次进程启动时,堆、栈、共享库和可执行文件的加载地址都会被随机化。这使得攻击者难以预测内存地址,从而增加了利用漏洞的难度。一般情况下都关闭。
临时关闭
|
|
永久关闭
|
|
x86
|
|
-
-m32 生成32位程序
-
-fno-stack-protector 禁用栈保护机制(Stack Protector)
栈保护机制通过在栈上放置一个 “canary” 值来检测栈溢出漏洞
-
-no-pie 禁用位置无关可执行文件(Position Independent Executable, PIE)
位置无关可执行文件使得代码在不同的内存地址运行,每次加载都会随机化地址空间
查看保护机制
dofunc栈帧
esp -> 0xffffd330
ebp -> 0xffffd358
ebp和esp的差值是0x18中,0x18=0x4(保存ebx) + 0x14(开辟栈帧)
栈帧开辟具体细节
- call dofunc 即 push eip,jmp &dofunc 保存eip并跳到dofunc的首地址
- push ebp 保存上一个栈帧的ebp
- mov ebp,esp ebp和esp指向同一地址,此时栈帧空间为0
- sub esp.0x20 esp向低地址移动开辟新的栈帧
栈帧清除具体细节
- leave 即 mov esp,ebp pop ebp 新的栈帧关闭,ebp变为0xffffd358
- ret 即 pop eip 返回之前eip的地址
反编译dofunc
查看栈布局
从数组的起始地址到保存的eip的地址的差值为0x10+0x4=0x14
尝试篡改eip的地址,输入8个a
栈溢出的原因是没有对输入的数据长度做限制,过长的输入会覆盖内存中返回地址的数据,如果能输到修改eip的地址值就可以劫持函数流程
这里要输入20个a来填充数据,16个a填充栈帧,4个a覆盖ebp,后面加上func的起始地址0x8049186即可
0x8049186本质上就是4个通过ASCII表映射的字符,其中0x8 0x04 0x91 0x86在ASCII中都是不可打印字符,需要通过pwntools输入
exp
|
|
x86_seq
|
|
相比于上面的程序这里省略了帧指针,增大了调试的难度
可以看到dofunc直接省略了对ebp的操作
省略了帧指针后ebp被当作通用寄存器进行使用,不再用于维护栈帧的边界
exp和上面相同
x64
|
|
exp
|
|
x64堆栈平衡
结果显示
|
|
这是由于64位机器在函数调用约定上要求栈在调用指令之前必须是16字节对齐的
16 Bytes Stack Alignment 的 MOVAPS 問題 - Hack543 — 16 Bytes Stack Alignment 的 MOVAPS 問題 - Hack543
ret2text涉及到的堆栈平衡问题_ret2text pie nx-CSDN博客
解决方案
-
使用
execv()
代替system()
在 64 位架构下,执行命令的函数通常需要注意堆栈平衡,以确保函数调用遵循 ABI 规范,并保证程序的正确性和稳定性。然而,有一种特殊的情况,即使用
syscall
指令直接调用系统调用(如execve
),可以不考虑堆栈平衡。这是因为syscall
指令直接与内核交互,而不是通过常规的用户态函数调用机制。 -
手动进行16字节对齐
查找
ret
gadget地址写入payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
from pwn import * context.log_level = 'debug' context.arch = 'amd64' context.os = 'linux' filePath = './ret2text_x64' io = process(filePath) padding = 0x10 retAddr = 0x401016 # 查找到gadget地址 funcAddr = 0x401146 # 这样payload总共32字节,可以进行16字节对齐 payload = b'a' * padding # 16字节 payload += p64(retAddr) # 8字节 payload += p64(funcAddr) # 8字节 # gdb.attach(io) io.sendafter(b'input:', payload) io.interactive()
函数传参
函数调用约定
-
x86(32bit)
cdecl:参数从右到左入栈,调用者清理栈。 常用于 c/c++ 程序
stdcall:参数从右到左入栈,被调用者清理栈。 常用于 windows api
fastcall:前两个参数使用 ECX 和 EDX 寄存器,其余参数从右到左入栈。
-
x86-64(64bit)
System V AMD64 ABI(大多数UNIX系统):前六个整数或指针类型的参数使用 RDI、RSI、RDX、RCX、R8 和 R9 寄存器传递,其余参数入栈。
Microsoft x64:前四个参数使用 RCX、RDX、R8 和 R9 寄存器传递,其余参数入栈。
-
arm(32bit 64bit)
AAPCS:
前四个参数使用 R0-R3 寄存器传递,其余参数入栈 (32-bit)。
前八个参数使用 X0-X7 寄存器传递,其余参数入栈**(64-bit)**。
x86
源码变种
|
|
如果有这样的函数
|
|
对于cdecl栈区的布局会像是这样的**(参数从右往左进栈)**
编译&调试
|
|
输了8个a,需要覆盖掉0xffffd34c(func地址)和0xffffd350(参数地址),总共要0x14个a
exp
|
|
如果是call_func那么payload为padding * b'a' + call_func_addr + bash_addr
多个函数调用如何写 payload ?
x64
x64相比于x86在cdecl上优先使用寄存器来传递参数,劫持的关键在于控制寄存器的内容
涉及到的寄存器有rdi, rsi, rdx, rcx, r8, r9
寄存器无法传递的参数用栈传递
设置返回地址为func的地址,寄存器rdi的值为"/bin/sh"的内存地址,理想情况下能运行一个shell,但是在do_system
中遇到了16字节对齐,如果没有进行对齐CPU会触发一个一般保护故障,后续就无法取得shell
解决方案:pwn技术分享—执行system前为何要执行retn指令_哔哩哔哩_bilibili
ROP
ROP(Return-Oriented Programming)编程是一种用于绕过程序安全保护的技术,特别是在存在栈溢出或缓冲区溢出漏洞时。
ROP的核心思想就是把内存中的汇编代码片段组装成一系列的汇编代码块,每个代码片段的最后一个指令都要求能控制rip的值,这样的指令有call jmp pop,在栈上写入一系列gadget地址就可进行ROP,ROP的运行过程和链表非常类似。
在ret2text_x64中寻找的gadget
|
|
栈布局
这里的pop rdi;ret
相当于把栈顶的值给rdi,此时间接改变了rdi的值,成功给func传参
|
|
减少溢出空间
如果溢出空间有限呢?
|
|
x86下payload
|
|