1. 项目概述:为什么格式化字符串漏洞是CTF中的“常青树”?
在CTF(Capture The Flag)竞赛,尤其是Web安全或Pwn(二进制漏洞利用)类题目中,格式化字符串漏洞(Format String Vulnerability)绝对是一个绕不开的经典考点。它不像栈溢出那样直观,也不像SQL注入那样广为人知,但它却像一把精巧的瑞士军刀,一旦掌握,就能在看似固若金汤的程序防御上撬开一道缝隙。CTFshow平台上的相关题目,从入门到进阶,系统地覆盖了这个漏洞的方方面面,成为了无数安全爱好者实战演练的绝佳沙盒。
简单来说,格式化字符串漏洞源于程序员的一个常见疏忽:误用了像printf、sprintf、fprintf这类C语言格式化输出函数。这些函数的设计本意是接收一个格式字符串(如%s,%d)和一系列对应的参数,然后按照格式输出。但如果程序允许用户输入直接作为格式字符串参数,攻击者就可以注入特殊的格式化占位符(如%x,%p,%n),从而读取内存、修改数据,甚至执行任意代码。在CTFshow的题目里,你可能会遇到一个简单的、回显你输入的服务,而你的任务就是利用这个回显,一步步窥探和操控程序内部。
为什么它如此重要?首先,它揭示了“信任边界”的脆弱性——程序过于信任了用户的输入。其次,它的利用链条非常有趣,从信息泄露(内存读取)到任意地址写(%n),逻辑递进清晰,是学习漏洞利用思维的绝佳范例。最后,即使在现代编译器和操作系统拥有诸多保护机制(如ASLR, NX, Stack Canary)的今天,格式化字符串漏洞的某些利用技巧依然有效,或者能与其他漏洞形成组合拳。因此,无论你是刚接触二进制安全的萌新,还是想深化利用技巧的老手,系统性地攻克CTFshow上的格式化字符串题目,都能让你对程序内存布局、函数调用约定和漏洞利用艺术有质的理解。
2. 漏洞原理深度拆解:不只是“%s”和“%n”那么简单
要利用一个漏洞,必须先吃透它的原理。格式化字符串漏洞的核心在于对C语言可变参数函数(variadic functions)机制的滥用。我们以最常用的printf为例。
2.1 格式化函数如何工作
当一个正常的printf被调用时,例如printf("Number: %d, String: %s\n", 123, "hello");,函数的执行流程是这样的:
- 函数从栈上(或特定的寄存器,取决于调用约定,如x86-64下前几个参数用寄存器)获取格式字符串地址。
- 解析格式字符串,遇到
%d时,它知道要去栈上(或下一个寄存器)取一个4字节的数据解释为整数输出;遇到%s时,它知道要去取一个指针,然后解引用该指针指向的内存地址,将其作为字符串输出,直到遇到空字节。 - 关键点在于,函数本身并不知道你传入了多少个参数。它完全“信任”格式字符串的指示。格式字符串说有几个占位符,它就“认为”你在栈上准备了几个对应的参数。
2.2 漏洞如何产生
漏洞产生的典型代码模式如下:
char user_input[100]; fgets(user_input, sizeof(user_input), stdin); printf(user_input); // 危险!用户输入直接作为格式字符串或者更隐蔽的:printf("%s", user_input);这是安全的,而printf(user_input);这是危险的。
当攻击者输入的不是普通字符串,而是包含格式化占位符的字符串时,例如输入%p.%p.%p.%p,printf会忠实地执行:
- 解析格式字符串,发现第一个
%p,于是它去栈上“本该”是第一个参数的位置(实际上,在printf(user_input)的调用中,user_input本身就是第一个参数,所以第一个%p会读取栈上user_input之后的数据)读取一个指针大小的数据并输出。 - 接着解析第二个
%p,再去栈上更深处读取数据……如此循环。
这样一来,攻击者就像拥有了一台“内存望远镜”,可以逐字逐句地读取栈上的内容。这些内容可能包括:
- 函数的返回地址
- 栈上的局部变量
- 库函数的地址(可用于绕过ASLR)
- 甚至是程序本身的代码段地址
2.3 关键格式化符的利用含义
%x/%p: 内存泄露的起点。%x以十六进制输出一个unsigned int,%p输出一个指针值。这是探测内存布局、寻找有用信息(如canary值、libc地址)的主要工具。%s: 更危险的信息泄露。它试图将对应参数视为指针,并输出该指针指向的字符串。如果我们可以控制这个参数的值(通过栈布局或后续技巧),就能读取任意地址的内存(如got表中的函数地址,这是计算libc基址的关键)。%n: 漏洞利用的“质变点”。这是最特殊的占位符。它不输出内容,而是将截至目前已成功输出的字符数,写入到对应参数所指向的内存地址中。这个参数必须是一个指针(int *)。%n实现了任意地址写。你可以写入一个数字,通过控制输出字符的数量,就能控制写入的值。%hn(写入2字节)、%hhn(写入1字节)可以用于更精确的写入。%c: 用于精确控制输出字符数量,从而配合%n控制写入的数值。%[num]d/%[num]x: 用于控制输出字段宽度,同样是为了配合%n精确控制写入值。
理解这些占位符,就拿到了格式化字符串漏洞的“武器库清单”。
3. 实战环境搭建与基础利用:从“Hello, Memory”开始
理论讲再多,不如动手调一调。我们从一个最简单的CTFshow风格题目开始,搭建实验环境。
3.1 编写漏洞程序与编译选项
创建一个名为fmt_vuln.c的文件:
#include <stdio.h> #include <string.h> void vuln_func() { char buffer[100]; printf("Enter your payload: "); fgets(buffer, sizeof(buffer), stdin); // 移除末尾的换行符 buffer[strcspn(buffer, "\n")] = 0; printf(buffer); // 漏洞点! printf("\n"); } int main() { vuln_func(); return 0; }为了模拟CTF题目环境,我们关闭一些现代保护机制进行编译,但保留部分以学习绕过:
gcc -m32 -no-pie -fno-stack-protector -z execstack -o fmt_vuln fmt_vuln.c-m32: 编译为32位程序。32位环境下栈操作更直观,参数传递全在栈上,便于初学者理解。-no-pie: 关闭位置无关可执行文件。这样程序的代码段、数据段地址是固定的,便于计算偏移。-fno-stack-protector: 关闭栈溢出保护(Canary)。这样栈结构更干净,我们暂时不处理canary。-z execstack: 使栈可执行。这为后续可能的shellcode执行提供便利(虽然高级利用中不常用)。
注意:在实际CTF比赛中,保护机制通常是全开的(Canary, NX, ASLR, PIE)。我们的学习路径是从简单到复杂,先理解核心原理,再学习如何绕过这些保护。
3.2 基础信息泄露:窥探栈内存
运行程序并输入一串%p:
$ ./fmt_vuln Enter your payload: %p.%p.%p.%p.%p.%p 0xff8b1a10.0x1.0xf7e3e5a0.0x1.0xff8b1a9c.0xff8b1aa4你看到的是一串十六进制数。它们是什么?
- 第一个
%p(0xff8b1a10): 这通常是buffer数组本身在栈上的地址(或附近)。因为buffer是printf的参数,在32位下,第一个参数之后就是栈上的后续内容。 - 后续的
%p:依次是栈上更高地址处的内容。其中可能包含:- 局部变量
- 调用者的栈帧基址(ebp)
- 函数返回地址
main函数的参数(argc,argv)- 以及最重要的:libc函数的地址(如上面输出中的
0xf7e3e5a0,很可能是一个libc地址)。
如何定位关键数据?你需要多次尝试,结合调试器(如gdb或pwndbg)来分析。在gdb中运行程序,在printf处下断点,查看栈布局,与你输出的%p序列进行比对。你会发现,第n个%p对应的是栈上从格式字符串指针地址向高地址方向的第(n-1)个参数位置(因为第一个参数是格式字符串本身)。
3.3 利用%s进行任意地址读
仅仅泄露地址还不够,我们需要读取特定地址的内容。假设我们通过之前的泄露,知道了栈上某个位置存放着一个指向"Hello"字符串的指针,我们想读取它。 如果这个指针位于第6个参数的位置(即第6个%p输出的值是一个指针),我们可以构造payload:AAAA%6$s。
AAAA是四个字符的填充,也可以是任意字符,用于在内存中形成一个可识别的模式,有时在调试中更方便。%6$s是直接参数访问语法。6$表示使用第6个参数(从1开始计数)。printf会去栈上找到第6个参数的值,将其作为指针,然后输出该指针指向的字符串,直到遇到\x00。
如果第6个参数的值确实是一个可读地址的指针,程序就会输出该地址处的字符串。如果该值不是合法指针或指向不可读内存,程序会崩溃(段错误)。这是信息泄露更强大的形式,可以用来读取GOT表中的函数地址,从而计算libc基址。
实操心得:在CTF中,题目往往会将某个关键字符串(如
"/bin/sh")或函数的GOT表地址放在栈上。你的任务就是用%p遍历找到它,然后用%s读出来。使用类似%p,%p,%p,...或%{i}$p(i从1递增)的payload进行“爆破”式探测是常用手段。
4. 高级利用技巧:利用%n实现任意地址写与控制流劫持
信息泄露(读)只是铺垫,真正的攻击目标是获得控制权(写和执行)。%n家族格式化符是实现“写”的关键。
4.1%n的基本原理与危险
%n会将当前已输出的字符数量写入到对应参数指向的地址。例如:
int bytes_written; printf("Hello%n", &bytes_written); // 执行后,bytes_written的值将是5("Hello"的长度)。在漏洞利用中,我们无法直接传递一个地址作为参数(&bytes_written)。但是,我们可以利用栈布局,让某个栈上的位置恰好存放着我们想要写入的目标地址,然后通过直接参数访问(如%7$n)告诉printf:“把输出的字符数,写入到第7个参数所指向的地址”。如果第7个参数的值恰好是我们想要修改的地址(比如某个函数的GOT表项),那么我们就实现了向该地址写入数据。
4.2 利用步骤详解:以修改GOT表为例
假设我们的目标是修改printf的GOT表项,使其指向我们注入的shellcode(在栈可执行的情况下)或者指向system函数。大致的利用链条如下:
信息泄露:利用
%p或%s泄露栈地址和libc地址。- 泄露一个栈地址,用于计算我们输入的
buffer在内存中的位置。 - 泄露一个libc函数(如
__libc_start_main)的地址,通过查询libc数据库(如libc-database)计算出libc基址和system函数的真实地址。
- 泄露一个栈地址,用于计算我们输入的
构造写目标:我们需要将目标地址(如
printf@got.plt)放到栈上,并确保printf在解析格式化字符串时,能将其作为一个参数找到。- 在32位下,我们可以直接将目标地址作为payload的一部分写入
buffer。因为buffer在栈上,它的内容本身就是参数的一部分。例如,payload可以是:<addr_printf_got>%<offset>$n。这里<addr_printf_got>是4字节的地址,它会占据buffer的开头。我们需要计算这个地址在栈上作为第几个参数被printf看到,这个位置就是<offset>。
- 在32位下,我们可以直接将目标地址作为payload的一部分写入
计算偏移(Offset):这是最关键的一步。你需要确定你输入的
buffer起始地址,在printf的栈帧中,是第几个参数。- 通过泄露栈地址和gdb调试,可以精确计算。假设
buffer地址是0xffffd010,printf的格式字符串指针(指向buffer)在栈上的位置是0xffffd02c。在32位下,参数是连续4字节存放的。那么从0xffffd02c(第一个参数,即格式字符串地址)到0xffffd010(我们输入的buffer起始)的偏移是(0xffffd02c - 0xffffd010) / 4 = 7(字节)。这意味着,buffer起始处的内容,会被printf当作第8个参数(因为第一个参数是0xffffd02c本身指向的地址,即buffer的地址,所以从buffer内容开始算是第2个参数?这里容易混淆,需要仔细计算)。 - 更可靠的方法是使用模式字符串。输入
AAAA%p.%p.%p.%p.%p.%p.%p...,观察输出中AAAA(即0x41414141)出现在第几个%p的位置。假设出现在第7个,那么偏移就是7。那么%7$n就会将输出字符数写入到AAAA这个地址(0x41414141)指向的内存,这会导致崩溃。我们将AAAA替换成真正的目标地址(如0x0804c014,printf的GOT地址),%7$n就会向0x0804c014写入数据。
- 通过泄露栈地址和gdb调试,可以精确计算。假设
控制写入的值:
%n写入的是已输出的字符数。如果我们想写入一个特定的值(比如system函数的地址),就需要精确控制输出字符的数量。- 直接输出一个巨大的字符串不现实。这里用到
%[num]d或%[num]x。例如,%100d会输出一个宽度为100的整数(用空格填充)。这样就能快速增加输出字符数。 - 但
system的地址可能是一个很大的数(如0xf7e3c850)。我们无法一次性输出数亿个字符。解决方案是分字节写入。利用%hn(写2字节)或%hhn(写1字节),将目标地址拆分成高16位、低16位,或者甚至4个字节,分别写入目标地址、目标地址+1、目标地址+2、目标地址+3。 - 构造链式写入:这是格式化字符串利用中最精妙的部分。例如,我们要向地址
0x0804c014写入值0xdeadbeef。- 将地址拆分:
0xdead(高16位),0xbeef(低16位)。 - 假设我们需要先写低16位
0xbeef(48879十进制)到0x0804c014,再写高16位0xdead(57005十进制)到0x0804c016。 - Payload结构:
[addr_low][addr_high]%[value_low]d%[offset]$hn%[value_high_diff]d%[offset+1]$hn - 具体计算:
value_low = 0xbeef = 48879。value_high需要写入0xdead = 57005,但此时已输出了len(addr_low+addr_high) + 48879个字符。所以第二部分需要输出的字符数是57005 - (已输出字符数)。通过精心计算填充,可以精确控制。
- 将地址拆分:
- 直接输出一个巨大的字符串不现实。这里用到
劫持控制流:成功将GOT表中的
printf地址覆盖为system地址后,下一次程序调用printf时,实际上会跳转到system。如果我们能同时控制printf的参数(在漏洞利用中,我们通常可以),比如让这个参数是字符串"/bin/sh"的地址,那么system("/bin/sh")就会被执行,从而获得一个shell。
4.3 自动化利用与工具
手动计算偏移、构造payload非常繁琐且容易出错。在实际CTF解题中,我们通常使用Python脚本配合pwntools库来自动化这一过程。pwntools提供了强大的格式化字符串漏洞利用功能:
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte'):这个函数可以自动生成复杂的格式化字符串payload。你只需要提供:offset: 你控制的第一个格式化字符串参数在栈上的偏移。writes: 一个字典,{target_address: value_to_write}。numbwritten: 在生成payload前已经输出的字符数(通常为0)。write_size: 写入大小,可选'byte'(%hhn)、'short'(%hn)、'int'(%n)。
例如,要利用我们上面的漏洞程序,假设偏移是7,想将0x0804c014(printf_got)改为0xf7e3c850(system_addr),脚本大致如下:
from pwn import * context.arch = 'i386' context.log_level = 'debug' # 假设本地运行 p = process('./fmt_vuln') # 先泄露信息,计算偏移和地址(这里省略泄露过程) offset = 7 printf_got = 0x0804c014 system_addr = 0xf7e3c850 # 构造payload payload = fmtstr_payload(offset, {printf_got: system_addr}) p.sendline(payload) p.interactive()pwntools会帮你处理好所有复杂的地址拆分、字节序和长度计算。
5. 现代保护机制下的利用挑战与绕过技巧
真实的CTF题目和现实世界的程序都开启了各种保护机制,让利用变得困难。CTFshow的中高级题目正是训练这些绕过技巧的战场。
5.1 地址空间布局随机化(ASLR)与位置无关可执行文件(PIE)
- 挑战:ASLR使得栈、堆、libc的基址在每次程序运行时都随机变化。PIE使得程序本身的代码段、数据段(包括GOT)地址也随机化。你之前硬编码的地址(如
0x0804c014)全部失效。 - 绕过:格式化字符串漏洞本身是信息泄露的利器,而这正是绕过ASLR/PIE的关键。
- 泄露程序基址:通过格式化字符串泄露栈上或
.got.plt中的一个已知函数地址(如__libc_start_main或某个库函数)。因为程序内部的相对偏移是固定的,用泄露的地址减去该函数在libc中的已知偏移,就能得到libc的基址。得到libc基址后,就可以计算出system、/bin/sh等所有libc中符号的运行时地址。 - 泄露栈地址:同样可以泄露栈上的一个指针,通过计算与目标缓冲区(如我们输入的字符串)的固定偏移,可以定位到缓冲区在内存中的确切位置,从而将地址写入栈上正确的位置。
- 泄露PIE基址:如果程序是PIE的,可以通过泄露程序代码段中的一个地址(例如
main函数的返回地址,它指向__libc_start_main内的某个位置,但这个位置相对于程序基址的偏移是固定的),来计算程序的基址。有了程序基址,就能算出GOT表的运行时地址。
- 泄露程序基址:通过格式化字符串泄露栈上或
注意事项:在64位系统下,地址空间巨大,且地址通常带有高位
0x00字节(如0x00007ffff7a523c0)。这些空字节会被C字符串函数(如strcpy,fgets)视为字符串终止符,导致payload截断。因此,在构造payload时,需要将地址放在payload的末尾,或者利用格式化字符串本身的特性(如%*c配合%n)来避免在内存中直接出现空字节。这也是64位下格式化字符串利用的一个常见难点。
5.2 栈保护(Stack Canary)
- 挑战:Canary是一个放在栈上的随机值,位于返回地址之前。函数返回前会检查这个值是否被修改,如果被修改则立即终止程序,防止栈溢出覆盖返回地址。
- 格式化字符串如何绕过:Canary本身也存放在栈上。我们可以利用格式化字符串漏洞读取Canary的值。因为Canary在每次运行中是随机的,但在同一次运行中,对于同一个函数,它的值是不变的。一旦我们泄露了Canary,在后续真正的栈溢出攻击中(如果存在),就可以在覆盖缓冲区时,将正确的Canary值写回原处,从而通过检查。格式化字符串在这里扮演了“情报员”的角色。
5.3 数据执行保护(NX/DEP)
- 挑战:NX(No-eXecute)位将数据区(如栈、堆)标记为不可执行。即使你在栈上注入了shellcode,跳转过去也会引发段错误。
- 绕过:格式化字符串漏洞的经典利用方式(写GOT表)本身不依赖于执行栈上的代码。我们通过
%n覆盖GOT表项,将其指向已有的代码片段,比如libc中的system函数。这是一种面向返回编程(ROP)的雏形,不涉及执行自定义的shellcode,因此NX防护对此无效。这也是为什么格式化字符串漏洞在现代保护下依然强大的原因之一——它主要利用的是程序已有的代码。
5.4 完整利用链示例(带ASLR/PIE)
假设一个开启了ASLR、PIE、NX的64位程序存在格式化字符串漏洞。利用步骤高度概括如下:
- 多次交互:通常需要至少两次
printf调用。第一次用于信息泄露,第二次用于写入。 - 第一次泄露:发送
%p大礼包,泄露多个栈地址和libc地址。通过偏移计算,得到:- 一个libc函数的地址 -> 计算libc基址 -> 得到
system和/bin/sh地址。 - 一个指向栈的地址 -> 计算输入缓冲区在栈上的绝对地址。
- 程序代码段中的一个地址 -> 计算程序基址 -> 得到GOT表地址。
- 一个libc函数的地址 -> 计算libc基址 -> 得到
- 构造写payload:使用
pwntools的fmtstr_payload,传入计算出的偏移、目标GOT地址(如printf_got)和要写入的值(system_addr)。注意64位地址的空字节问题,pwntools会自动处理(通常将地址放在payload末尾)。 - 触发执行:发送写payload。当下一次程序调用
printf(或其他被覆盖的函数)时,控制流就被劫持到system,同时要确保此时该函数的参数(可能在栈上)指向/bin/sh字符串的地址。这可能需要通过之前的泄露和计算,精心布局栈上的数据。
6. CTFshow典型题目实战解析与排错指南
结合CTFshow平台的热门关键词,我们来分析几种典型的出题套路和解题思路。
6.1 场景一:无字母数字代码执行(结合格式化字符串)
这通常是Web类题目的考点,但原理相通。题目限制payload不能包含字母和数字,却要执行命令。格式化字符串中的%p、%x、%n等符号本身不是字母数字,可以用来构造攻击。
- 思路:利用格式化字符串泄露内存中的地址,这些地址是十六进制数字(0-9, a-f)。虽然不能直接输入字母数字,但泄露出的地址信息中可能包含有用的数据。更进一步,可以通过
%n向某个可写内存(如栈上的变量)写入一个值,这个值可能被后续代码当作函数指针或跳转地址。挑战在于如何用纯格式化字符串符号构造出有用的地址或数据。有时需要结合多次泄露和写入,进行“数据锻造”。
6.2 场景二:日志文件包含
题目可能模拟一个Web应用,将用户输入记录到日志文件,然后另一个功能点会读取并printf这个日志文件。这就间接造成了格式化字符串漏洞。
- 解题关键:
- 找到注入点:确定哪个用户输入会被最终放入日志并被
printf。 - 考虑过滤:题目可能会过滤一些字符,如
%、\n、\x00等。需要尝试绕过,例如使用%25(%的URL编码)如果后端解码,或者使用%c%c组合来输出%字符本身(%的ASCII码是37,可以用%37c输出)。 - 利用链可能较长:因为需要先写入日志,再触发读取,所以可能需要精心构造一个payload,使其在日志文件中保存时是安全的,在被
printf时才会被解析为格式化字符串。
- 找到注入点:确定哪个用户输入会被最终放入日志并被
6.3 场景三:多层嵌套解码
题目可能对输入进行多次解码(如base64、URL、html实体等),然后再传递给printf。
- 解题关键:
- 理清解码顺序:你需要构造一个payload,使其在经过所有解码步骤后,变成有效的格式化字符串攻击载荷。例如,原始输入是
%2570(%70的URL编码)。第一次URL解码后变成%70,第二次如果被printf解析,%70可能被解释为输出70个字符的%c(如果%70c是合法格式),或者引发其他行为。这需要仔细测试和理解每一层解码的效果。 - 利用编码绕过过滤:如果直接输入
%n被过滤,可以尝试输入%6E(n的ASCII码十六进制)配合%c或%x来动态生成字符n,但这非常复杂,通常需要结合其他漏洞。
- 理清解码顺序:你需要构造一个payload,使其在经过所有解码步骤后,变成有效的格式化字符串攻击载荷。例如,原始输入是
6.4 常见问题与调试技巧
程序崩溃(Segmentation Fault):
- 原因:最常见。可能是
%s读取了不可读地址,%n写入了不可写地址,或者构造的地址不对齐(如在64位下写入非8字节对齐地址)。 - 排查:使用
gdb调试,在printf处下断点,单步执行,观察栈内容和寄存器。检查你计算的偏移是否正确。检查你试图读写地址的合法性(vmmap命令查看内存映射)。
- 原因:最常见。可能是
输出乱码或无输出:
- 原因:
%s读取的地址可能包含空字节,导致字符串提前终止;或者输出的字符是非打印字符。 - 排查:尝试用
%p代替%s先看看地址值是什么。使用hexdump或pwntools的hexdump功能查看原始输出。
- 原因:
写入的值不对:
- 原因:
%n写入的是已输出的字符数,计算错误。特别是使用%[num]d时,num是输出字段的最小宽度,如果实际数字位数小于num,会用空格或0填充,这会影响字符计数。 - 排查:在脚本中精确计算payload长度。使用
len(payload)或在构造payload时打印中间结果。pwntools的fmtstr_payload能极大减少这类错误。
- 原因:
偏移计算不准:
- 原因:本地环境和远程环境可能存在差异(如环境变量导致栈偏移微调)。或者对于64位程序,参数传递前几个使用寄存器(rdi, rsi, rdx, rcx, r8, r9),格式化字符串参数超过6个后才用栈,计算方式与32位不同。
- 技巧:使用“试探法”。发送
AAAA%p-%p-%p...或AAAA%1$p-%2$p...,观察0x41414141出现在第几个%p。这个位置就是偏移。编写脚本时,可以写一个循环来自动探测偏移。
应对过滤:
- 过滤
%n:尝试使用%hn或%hhn,有时过滤不完整。或者尝试使用%c和%*c等组合来间接实现写入,但这非常复杂。 - 过滤
$(直接参数访问):这是最麻烦的。你只能使用顺序访问的%p、%x等。你需要通过大量填充字符(如%c)来移动格式化字符串的“指针”,使其对齐到你想要读写的栈位置。这需要精确计算已输出的字符数,极其繁琐,通常需要自动化脚本。
- 过滤
格式化字符串漏洞的学习曲线较陡,但一旦掌握,你对程序内存和漏洞利用的理解会上一个大台阶。CTFshow的题目梯度设置得很好,从简单的内存读到复杂的组合利用,建议按照平台题目顺序逐一攻克,每一步都配合调试器理解内存变化,积累下来的经验会成为你安全技能库中一件强大的武器。记住,关键不是记住payload,而是理解其背后的“为什么”——为什么这个偏移有效?为什么这个地址要这样写?理解了原理,万变不离其宗。