pwn的基础题目1
一.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 | from pwn import * |
二.ret2shellcode
ret2shellcode 是利用栈溢出覆盖返回地址,跳转到注入的 shellcode 执行以获取权限的漏洞利用技术。其核心是确保栈可执行(NX 保护关闭),并准确控制返回地址到 shellcode 起始位置。
exp:
1 | from pwn import * |
补充:
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 滑梯的核心逻辑:
- 利用「指令默认低地址→高地址执行」,在 shellcode 低地址侧放置长 nop 序列;
- 溢出覆盖返回地址时,目标设为 nop 滑梯的地址范围(而非精确的 shellcode 地址);
- 借助 nop 「空操作 + PC 自增」的特性,让 CPU 从滑梯任意位置滑到 shellcode,破解 ASLR 导致的地址不可预测问题。
关键前提:栈可执行(NX 关闭),且 nop 滑梯长度足够覆盖 ASLR 随机化范围。
三.ret2syscall(一般是静态链接程序)
ret2syscall 是 栈溢出漏洞 的经典利用技术,核心思路是:通过溢出覆盖栈上的返回地址,劫持程序执行流,最终构造并触发 syscall(系统调用),实现任意操作(如执行 /bin/sh 获取 shell、读取 flag 文件等)。
它无需依赖目标程序的 system、execve 等库函数,仅通过寄存器传递系统调用参数 + 触发 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 的核心步骤
- 利用栈溢出,覆盖返回地址为一系列 ROP Gadget(小指令片段);
- 通过 Gadget 依次设置
rax(系统调用号)、rdi/rsi/rdx(参数); - 最后调用
syscall指令,触发目标系统调用。
最常用的目标是 execve("/bin/sh", NULL, NULL)(获取交互式 shell),对应的系统调用参数:
rax = 0x3b(x86_64 中execve的系统调用号);rdi = 指向 "/bin/sh" 字符串的地址;rsi = 0(NULL);rdx = 0(NULL)。
(2)关键前提
- 程序存在 栈溢出漏洞(可控制栈上返回地址及后续数据);
- 关闭或绕过 栈保护(如
NX可开启,ret2syscall 不依赖栈可执行;但Canary必须绕过或关闭); - 目标程序 / 内存中存在所需的 ROP Gadget(核心是
pop 寄存器; ret类 Gadget,用于设置参数,以及syscallGadget); - 内存中存在
/bin/sh字符串(或可通过栈 / 数据段写入该字符串)。
(3)实战步骤(以 x86_64 为例)
步骤 1:确认漏洞与环境
假设目标程序 ret2syscall 存在栈溢出(如读取输入时未限制长度,覆盖了 rbp 和返回地址),且:
Canary关闭(checksec查看Stack canary: No);NX开启(NX enabled: Yes,栈不可执行,需用 ROP);- 无
PIE(PIE: No,地址固定,便于利用)。
步骤 2:查找关键 ROP Gadget
需通过 ropgadget 命令(Pwndbg 内置)查找以下 4 类 Gadget:
pop rax; ret:用于设置系统调用号;pop rdi; ret:用于设置第 1 参数;pop rsi; ret:用于设置第 2 参数;pop rdx; ret:用于设置第 3 参数;syscall:触发系统调用的指令。
查找命令示例(Pwndbg 中执行):
bash
运行
1 | # 1. 查找 pop rax; ret |
步骤 3:查找 / 构造 /bin/sh 字符串
execve 需要一个指向 /bin/sh 字符串的地址,有 3 种获取方式:
方式 1:程序中已存在(优先)
用 search 命令搜索程序内存中的 /bin/sh:
bash
运行
1 | search "/bin/sh" # Pwndbg 命令 |
方式 2:栈中写入(无现成字符串时)
若程序无 /bin/sh,可通过栈溢出将字符串写入栈中,再使用栈地址作为 rdi 参数。
步骤 4:计算栈溢出偏移
通过 pattern 命令计算覆盖返回地址的偏移(核心步骤):
生成 cyclic 字符串并作为输入,让程序崩溃:
bash
运行
1
2pattern create 200 # 生成 200 字节 cyclic 串
# 输出:'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaad'运行程序,输入上述字符串,程序崩溃后查看栈顶(
rsp)的值:bash
运行
1
2
3run < <(echo "上述 cyclic 串") # 触发崩溃
reg rsp # 查看崩溃时 rsp 的值(示例:0x7fffffffe458)
x/x $rsp # 查看 rsp 指向的内容(示例:0x616161616161616b)计算偏移:
bash
运行
1
2pattern offset 0x616161616161616b # 输入 rsp 指向的 cyclic 值
# 输出示例:Offset: 40(表示输入第 41 字节开始覆盖返回地址)即:前 40 字节填充垃圾数据(
padding),第 41 字节开始写入 ROP 链。
步骤 5:构造 ROP 链(核心)
根据前面获取的 Gadget 地址、/bin/sh 地址、偏移,构造 ROP 链,顺序如下:
plaintext
1 | padding(40 字节) + |
示例(假设各地址如下):
- 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 | from pwn import * |
(4)、常见变体与注意事项
1. 32 位系统差异(x86)
32 位 Linux 中系统调用方式不同,需调整:
- 系统调用号:
execve是0xb(而非 0x3b); - 传参寄存器:
eax(系统调用号)、ebx(第 1 参数)、ecx(第 2)、edx(第 3); - 触发指令:
int 0x80(而非syscall)。
Gadget 需查找 pop ebx; ret、pop ecx; ret、pop edx; ret、int 0x80。
2. 无 /bin/sh 时的处理
若程序内存中无 /bin/sh,可通过bss段写入字符串:
(具体见[[zzu大二上联合招新赛(pwn复现)]]中的ret2shellcode)
1 | from pwn import * |
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 链手动构造系统调用参数并触发执行,核心依赖:
- 栈溢出漏洞(控制返回地址);
- 足够的 ROP Gadget(设置寄存器);
- 系统调用的参数与指令匹配。
四、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.偷懒构造法,






