☰
ISCC 2023 Pwn方向赛题复盘:从栈溢出到堆利用的完整实战
2026/10/1 9:08:42 网站建设 项目流程

不玩虚的,直接进入正题。ISCC 2023 的 pwn 方向我完整跟了下来,从初赛到决赛的题目都动过手。这篇文章是我自己赛后整理的一份复盘笔记,包含各题型的解题思路、关键利用手法和本地调试到远程打通的完整过程。如果你正准备入门 pwn,或者想看看 ISCC 这类国内主流赛事在 pwn 方向到底考什么、怎么考,这篇内容应该能帮你省下不少试错的成本。

先给没参加过 ISCC 的朋友交代一下背景。ISCC(全国大学生信息安全竞赛)是国内老牌的信息安全赛事之一,pwn 方向的题目设置一直比较贴近实战,覆盖栈溢出、格式化字符串、堆利用这些 CTF 经典考点。2023 年的题目整体难度分布比较科学,既有适合新手的签到题,也有需要深入理解 glibc 堆管理机制的进阶题。它和很多纯线上赛不一样的地方在于,比赛环境使用动态容器部署,每个人连接的靶机都是独立实例,这意味着你在本地调试好的 exploit 拿到远程去打的时候,libc 版本、ASLR 布局、连接超时这些变量都得重新考虑。这也是我认为 ISCC 的 pwn 题特别适合拿来练手的原因——它逼着你把本地到远程的整套流程走通。

下面我开始按题型拆解。

1. 赛前准备与 ISCC 2023 pwn 赛题概览

1.1 今年 pwn 方向的整体难度与题型分布

先说结论:ISCC 2023 pwn 方向的题目设定,可以概括为“基础题送分、中档题考细节、难题考综合”。从实际题目来看,主要覆盖了以下几个方向:

  • 栈溢出基础题(ret2text、ret2shellcode)
  • 格式化字符串漏洞利用
  • ROP 链构造(ret2libc、ret2syscall)
  • 堆利用(tcache 相关手法为主)
  • 个别题目涉及 C++ 逆向与异常处理机制

这个题型分布其实很典型,基本就是国内大学生赛事的标准配置。没有特别偏门的考法,但也正是这种“看似常规”的题目,最能拉开差距。因为常规题拼的不是谁见过更多偏门技巧,而是谁的基础功更扎实、调试更熟练、脚本写得更快更稳。

另外注意到一个趋势:动态容器在 ISCC 这类赛事中已经成为标配。每道题都会给你一个独立的端口,容器内部跑着题目二进制和对应版本的 libc。这个机制带来的直接影响是,你没法通过libc-database或者libc.rip简单匹配远程 libc,因为不同赛题的容器可能基于不同的基础镜像。所以赛前准备阶段,学会如何从远程泄露地址、如何通过/proc/self/maps获取内存布局,反而比单纯背 libc 偏移更重要。

1.2 参赛前的工具链准备

工欲善其事,必先利其器。我在赛前把常用的 pwn 工具链完整过了一遍,这里列出我认为最核心的几个,以及它们各自在比赛中的定位。

  • pwntools:Python 的 CTF 利用框架,所有 exploit 脚本的骨架。重点掌握remote()、sendlineafter()、recvuntil()、ELF()、ROP()这些常用接口。
  • gdb + pwndbg / gef:动态调试的标配。pwndbg 对堆结构的展示比较友好,适合堆题调试;gef 的 ROP 辅助功能更强。我个人习惯用 pwndbg,但建议两个都装一下,遇到具体题目可以切换。
  • checksec:查看二进制保护机制的脚本,一般在 pwntools 里直接调用checksec("./pwn")即可。它会告诉你 NX、PIE、Canary、RELRO 分别是什么状态,这直接决定了你能用什么利用手法。
  • one_gadget / ROPgadget:one_gadget 用于查找满足特定约束条件的execve("/bin/sh")地址;ROPgadget 用于搜索二进制和库文件中的 gadget。
  • LibcSearcher / LibcSearcher2:通过泄露的某个函数地址反查 libc 版本的工具。注意在动态容器环境下,它的准确率取决于你泄露的地址数量,建议至少泄露两个函数地址交叉验证。
  • patchelf:本地调试时替换二进制依赖的 libc 和动态链接器,保证本地环境和远程容器一致。

注意:在所有工具准备到位之后,第一件事不是刷题,而是把“从本地打通到远程打通”的全流程用一个最简单的栈溢出题完整走一遍。因为比赛中最常见的翻车现场不是你不会利用,而是你本地打得好好的,一打远程就断连、超时或者崩溃。这中间的变量就是动态容器和网络环境。

2. 基础题解:从签到题到栈溢出入门

2.1 签到题的整体分析流程

ISCC 的签到题一般不会太难,它的目的是让你快速进入状态。2023 年的签到题是一道非常典型的栈溢出,二进制开启了 NX 但没有开启 PIE,也没有 Canary。这意味着我们可以直接构造 ROP 链调用后门函数。

拿到题目后的第一步永远是信息收集。按照我的习惯,顺序是这样的:

  1. checksec查看保护机制
  2. file查看文件类型和架构(32 位还是 64 位)
  3. strings快速浏览二进制中的字符串,找找有没有/bin/sh、flag、system等关键词
  4. 用 IDA 或 Ghidra 反编译,定位漏洞函数

对于这道签到题,通过反编译可以看到main函数中调用了vuln函数,vuln里存在一个明显的read(0, buf, 0x100)调用,而buf只有 0x20 字节的栈空间。典型的栈溢出,而且一眼就能看到溢出的偏移是 0x20 + 8(64 位下返回地址前的 8 字节为 rbp 寄存器占用),所以从buf到返回地址的偏移是 0x28。

再看反编译结果里的后门函数,通常命名为win、backdoor或shell之类的,函数内部直接调用了system("/bin/sh")。这样问题就变得很简单了,只要覆盖返回地址跳到后门函数。

2.2 一场简单的 ret2text 实战

基于上面的分析,我们构造一个最简单的 ret2text 脚本。这里用cyclic来确定偏移也是一种常用的验证方法,但既然反编译已经给出了明确的栈大小,直接用计算出来的偏移即可。

from pwn import * context.arch = "amd64" context.log_level = "debug" # 本地调试 # p = process("./pwn") # 远程连接 p = remote("target.example.com", 10001) elf = ELF("./pwn") backdoor_addr = elf.sym["backdoor"] # 假设后门函数名是 backdoor offset = 0x28 payload = b"A" * offset + p64(backdoor_addr) p.sendlineafter(b"Input:", payload) p.interactive()

这里有几个细节需要初学者特别注意:

  • elf.sym["backdoor"]函数名要和反编译结果一致,如果函数名记错了会直接报错。
  • 64 位程序在返回地址之前一定要填满 8 字节对齐,所以偏移是0x20 + 0x8,这一点在调试时最容易踩坑。很多人一开始写 0x20,结果返回地址定位不对,gdb 里一查栈发现完全不是自己预期的布局。
  • 如果程序没有开 PIE,函数地址是固定的,可以直接写死在 payload 里;如果开了 PIE,就需要先泄露地址,后面我会讲。
  • 远程连接时,不要用process()的 payload 原封不动去打远程。由于容器环境的问题,远程程序加载的地址和本地可能不同(尤其是开了 PIE 的情况),需要根据远程泄露的信息动态调整。

在实际打这道题的时候,我还发现了一个很多人会忽略的点:接收输入时的提示字符串到底有没有换行。sendlineafter(b"Input:", payload)中的b"Input:"必须和程序实际输出的提示完全匹配,如果程序输出的是"Input> "而你没写对,脚本就会卡死在等待接收的位置。所以建议先用nc手动连一次远程,看看真实的交互格式再写脚本。

2.3 格式化字符串漏洞的常见利用方式

ISCC 2023 在基础题里还安排了一道格式化字符串漏洞的题目。这类漏洞的核心原理是printf家族函数的格式化参数与可变参数不匹配,导致我们可以通过%x、%s、%n等格式符从栈上读取或写入数据。

最简单且最直接的利用方式是:

  • 泄露栈上数据:用%p或%x逐个输出栈上的值,确定格式化字符串参数在栈上的偏移位置。假设输入的内容是AAAA.%p.%p.%p...,通过数第几个%p输出的是0x41414141,就知道我们的输入在格式化参数列表中的位置是第几个,这个偏移是后续构造攻击 payload 的基础。
  • 任意地址读取:用%s配合目标地址,读取某个内存地址的内容。这通常用来泄露 GOT 表中某个函数(如puts)的实际运行地址,再通过这个地址推算 libc 基址。
  • 任意地址写:用%n配合目标地址,写入一个值。这一步通常用来劫持 GOT 表项或栈上返回地址,实现控制流劫持。

格式化字符串利用最难的一点是定位偏移,但也是最有规律可循的环节。我自己习惯的做法是先用AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p去试,数到输出0x41414141的那个位置,记录下偏移。之后的所有 payload 都固定在这个偏移基础上构造,不再额外猜测。这里分享一个小技巧:%n写入的数据量是按字节计算的,如果要写一个很大的地址(比如 0x7fffffff),直接写完会输出大量字符导致程序卡死,这时候要用%hn(写两个字节)甚至%hhn(写一个字节)分多次写入,每次写一小段。

3. ROP 进阶:绕过保护机制的核心手法

3.1 ret2libc 与 PLT/GOT 劫持的完整拆解

进入中档题之后,最常见的就是 ret2libc。它针对的场景是:程序开了 NX(栈不可执行),又没有现成的后门函数可用,我们必须通过 ROP 调 libc 中的system函数拿到 shell。

ret2libc 的完整利用链路是:

  1. 通过格式化字符串漏洞或栈溢出,泄露某个已经加载的 libc 函数的真实地址。
  2. 根据泄露的地址,使用 LibcSearcher 或本地 libc 文件计算出 libc 基址。
  3. 根据基址计算出system和/bin/sh的地址。
  4. 构造 ROP 链,调用system("/bin/sh")实现 getshell。

以典型题目为例,假设程序存在两次输入机会,第一次输入用来泄露 libc 地址,第二次输入用来执行 ROP 链。脚本框架大致是这样:

from pwn import * context.arch = "amd64" elf = ELF("./pwn") libc = ELF("./libc-2.31.so") # 假设已知远程 libc # 第一次输入:泄露 puts@got 对应的实际地址 payload_leak = b"A" * offset payload_leak += p64(elf.plt["puts"]) payload_leak += p64(elf.sym["main"]) # 泄露后回到 main 重新执行 p.sendlineafter(b"Input:", payload_leak) # 从程序输出中解析出 puts 的真实地址 puts_addr = u64(p.recvline().strip().ljust(8, b"\x00")) # 计算 libc 基址 libc_base = puts_addr - libc.sym["puts"] system_addr = libc_base + libc.sym["system"] binsh_addr = libc_base + next(libc.search(b"/bin/sh")) # 第二次输入:构造 system("/bin/sh") 的 ROP 链 payload_rop = b"A" * offset payload_rop += p64(ret_gadget) # 用于栈对齐 payload_rop += p64(system_addr) payload_rop += p64(0x0) # 返回地址随便填 payload_rop += p64(binsh_addr) p.sendlineafter(b"Input:", payload_rop) p.interactive()

3.2 动态容器环境下的 libc 版本适配问题

这一段是我特别想仔细说的,因为它在 ISCC 2023 的实战里坑了非常多的人。

在本地做题的时候,大家通常直接用自己系统的 libc 或者题目附件提供的 libc。但动态容器环境下,远程 libc 版本很可能和你本地不同。ISCC 2023 好几道题的容器都是基于 Ubuntu 20.04 或 22.04 的镜像构建的,对应的 libc 分别是 2.31 和 2.35。如果你不知道远程 libc 的版本,用本地的偏移去打远程,会导致两个现象:一是system地址算不准,程序直接崩;二是就算地址碰巧对了,偏移不对也会段错误。

解决这个问题有两条路:

第一条路:泄露多个函数地址,用 LibcSearcher 反查 libc 版本。这种方法需要泄露至少两个函数地址(比如puts和printf),这样匹配的精度会高很多。注意,ISCC 的动态容器都是全新创建的,所以每个实例的 libc 加载基址可能会变(ASLR),但函数之间的相对偏移是不变的。所以你泄露之后,在本地用libc.sym计算相对偏移,远程只要拿到基址就能算出所有目标地址。

第二条路:使用/proc/self/maps直接读取远程内存布局。这个方法更加直接粗暴,前提是你已经拿到了远程代码执行的权限,或者至少能读取文件内容。比如你有了任意文件读取漏洞,可以直接发送/proc/self/maps的内容,从里面找到 libc 的加载地址,这样就连基址都省得算了。

我在实际比赛中更推荐先走第一条路,因为简单快速;如果反查失败,再想办法读取远程 maps。

还有一个很隐蔽的坑:同一个 libc 版本,在不同容器里编译时可能带有不同的 patch 选项,导致某些符号偏移有细微差异。这种情况虽然少见,但我确实遇到过。排查方法是在远程用%p泄露同一个函数的地址,和本地libc.sym中该函数的偏移对比,如果差值大于 0x1000,说明 libc 版本不匹配,需要重新匹配。

3.3 栈迁移与 SROP 的灵活运用

除开标准的 ret2libc,ISCC 2023 的某道题还考察了栈迁移(stack pivot)的技巧。这个技巧在以下场景中极其有用:程序给你的溢出空间非常小,不足以容纳完整的 ROP 链。此时我们可以通过leave; ret这个 gadget,将rsp指向我们控制的另一块内存区域(比如 bss 段或堆上),从而“无中生有”地获得更大的 ROP 空间。

栈迁移的利用核心是利用两条指令:

  • leave等价于mov rsp, rbp; pop rbp
  • ret等价于pop rip

具体构造思路是:

  1. 通过第一次写入,在 bss 段上布置完整的 ROP 链。
  2. 通过第二次溢出,覆盖栈上的rbp为 bss 地址,并把返回地址改为leave; ret。
  3. 当程序执行到leave; ret时,rsp被设置为 bss 地址,ret从 bss 地址开始取指令,完成 ROP 链的执行。

这个手法看着简单,但细节很多,尤其是 bss 地址的选择要避开.bss中程序自己使用的部分,否则可能覆盖到关键数据导致崩溃。我的习惯是先用readelf -S ./pwn查一下.bss段的地址范围,然后选一个靠后的偏移位置存放 ROP 链。

SROP(Sigreturn-Oriented Programming)在 ISCC 2023 中虽然不是主考内容,但它在某些题目里可以作为一种新颖的解法。SROP 的核心是:通过设置rax为 15(SYS_rt_sigreturn),触发内核恢复一个伪造的sigframe,从而完全控制所有寄存器的值。这个技术对 gadget 的需求非常少,适合在 ROP 链构造困难时作为备选方案。

4. 堆利用专题:从 tcache 到 fastbin

4.1 堆题前置知识:glibc 堆管理机制速览

ISCC 2023 在堆题上的考察集中在 glibc 2.31 和 2.35 版本的tcache机制上。很多第一次接触堆题的选手会在这里卡住,因为堆题不像栈题那样有清晰的溢出偏移,而是需要你理解堆块之间的组织方式。

简单梳理一下核心数据结构:

  • tcache(per-thread cache):每个线程维护一个小型缓存,用于加速小块内存的分配和释放。每个 tcache 条目默认最多放 7 个 chunk,每个 chunk 大小相同。
  • fastbin:用于管理 64 字节到 128 字节之间(64 位系统)的快速释放块。释放的 fastbin chunk 会形成一个单向链表。
  • unsorted bin:释放较大 chunk 或 tcache 满时,chunk 会进入 unsorted bin,它是一个双向链表。

理解这些 bin 的组织方式之后,堆利用的常见套路就很好理解了:通过 UAF(Use After Free)或 double free 等漏洞,在你释放的堆块上伪造数据,让 glibc 分配器在下次分配时把这个堆块分配到你想要的位置。

ISCC 的堆题一般都会给你菜单式的交互界面:add、delete、edit、show之类的功能。这四类功能对应的漏洞点分别是:

  • add:分配堆块,通常要检查 size 是否合法
  • delete:释放堆块,漏洞点往往在这里,比如没有清空指针(导致 UAF),或者释放两次(double free)
  • edit:编辑堆块内容,这里可能存在堆溢出
  • show:输出堆块内容,通常用来泄露 libc 地址

拿到题目,第一步不是试图立刻构造利用,而是把所有功能交互一遍,记录每个操作的输入限制和输出内容。我会画一个简单的表,记录每个功能对应的大小范围、能否重复调用、释放后指针是否置空等信息。这些细节直接决定了后续用什么利用手法。

4.2 tcache poisoning 实战拆解

tcache poisoning 是 ISCC 2023 堆题最核心的利用手法。它的原理非常简单:tcache 链表在分配时不会检查 chunk 指针是否指向合法的堆内存,所以如果我们能改写 tcache 链上某个 chunk 的fd指针,那么在下次分配时,glibc 会直接返回这个伪造的地址。

举个例子,假设程序的delete功能存在 UAF,即释放堆块后没有清空对应的指针,导致我们可以通过edit功能修改已经释放的堆块内容。

第一步,申请两个相同大小的 chunk 并释放,让它们进入 tcache 链表。

tcache bin 链表状态:head -> chunk2 -> chunk1 -> NULL

第二步,通过 UAF 修改 chunk2 的fd指针为目标地址,比如target_addr。

tcache bin 链表状态:head -> chunk2 -> target_addr -> ...

第三步,连续两次add,第一次 add 会返回 chunk2,第二次 add 会返回target_addr指向的内存。此时我们就获得了任意地址写的能力。

如果目标地址是__free_hook或__malloc_hook,我们可以把这两个函数指针改成system的地址。那么当我们执行delete一个内容为/bin/sh的块时,就会触发system("/bin/sh")。

需要注意,在 glibc 2.34 及更高版本中,__free_hook和__malloc_hook已经被彻底移除,不能再通过这个方式拿到执行权限。ISCC 2023 如果用了 Ubuntu 22.04 的容器,就需要换用exit_hook或者 FSOP(File Stream Oriented Programming)这类新手法。这也反映了堆题的一个趋势:glibc 版本越高,堆利用的门槛就越高,考点也从“改函数指针”往“攻击 IO 结构体”的方向偏移。我在复盘时尤其关注了这一点,因为很多教程还在教老版本的 hook 攻击,直接拿去打新版本题目是会落空的。

4.3 fastbin attack 与 unsorted bin 泄露的配合

在 tcache 被禁用或者目标堆块大小超过 tcache 范围时,fastbin attack 和 unsorted bin 泄露就派上用场了。

unsorted bin 泄露是堆题最常用的泄露手段。操作流程是:申请一个较大的 chunk(比如 0x100 以上),然后释放它。此时它会被放入 unsorted bin,其fd和bk指针会指向main_arena中的某个地址。由于main_arena在 libc 中偏移是固定的,我们通过 UAF 或show功能把这个地址读出来,再减去偏移,就能得到 libc 基址。

这一步泄露 libc 的思路和栈题的 ret2libc 泄露思路本质相同,只是在具体实现上要借助 UAF 或 double free 提供的读取能力。

fastbin attack则主要用来构造任意地址分配。当 tcache 不可用时,fastbin 的利用方式是:释放两个相邻的 fastbin chunk,修改第一个 chunk 的fd指针指向伪造的 chunk 位置(比如__malloc_hook附近),然后在适当偏移处伪造一个合法的 chunk 结构,使得分配器在检查时能通过校验。

这里有一个经常踩的坑:glibc 从 2.29 开始,对 fastbin 的 size 字段检查变得更加严格。你伪造的 chunk 不仅要保证地址对齐,还要保证size字段落在合法的 fastbin 范围内。否则即使你成功把堆块分配到了目标地址,在分配或释放时也会触发 assertion 错误,直接导致程序崩溃。

5. 实战踩坑与排查技巧实录

5.1 本地环境与远程容器环境不一致的老大难问题

ISCC 2023 的动态容器机制,让我在比赛过程中反复吃到了本地和远程环境不一致的亏。总结起来主要有三类问题:

第一类是libc 版本不一致,这个前面已经详细说过,解决方案是通过泄露多个函数地址来匹配 libc,或者使用patchelf在本地把二进制的 libc 替换成远程对应版本,保证本地调试环境和远程一致。

第二类是ASLR(地址空间布局随机化)。本地调试时大家经常习惯性地设置set disable-randomization on关闭 ASLR,方便下断点。但远程环境一定是开启了完全随机化的。所以 exploit 脚本里不能出现任何硬编码的地址,每次远程连接都要动态解析地址。

第三类是网络与交互时序问题。动态容器环境下的网络延迟通常比本地高很多,如果你的脚本在接收数据时使用了不当的超时设置或者没有处理粘包,就可能导致数据接收不完整。ISCC 2023 里有一道题,本地运行程序时puts的输出很正常,但远程因为缓冲区设置的问题,输出会多一个换行或者少一个换行。我的解决方法是,在脚本里先用sleep(0.1)强制等待一小段时间再发送下一个输入,或者用recvuntil接收一个确定的字符串而不是盲目接收整行。

5.2 调试技巧:pwndbg 插件与核心转储分析

想要高效解 pwn 题,调试工具的使用水平很大程度上决定了你的效率。我自己的调试流程是:

  1. 先用checksec和反编译结果确定漏洞类型。
  2. 用cyclic 200生成一串随机字符串作为溢出输入,程序崩溃后,用 gdb 查看崩溃时的$rsp或返回地址,从而确定溢出偏移。
  3. 在关键函数上下断点,比如read、printf、system,观察输入和输出在栈上的位置关系。
  4. 对于堆题,主要用 pwndbg 的heap命令查看堆块的布局,用bins命令查看各个 bin 链表的状态。

如果你本地调试时遇到程序崩溃但不知道崩溃在哪一步,可以启用 core dump 来定位。命令如下:

ulimit -c unlimited gdb ./pwn core

gdb 加载 core 文件后会自动停在崩溃点,你只需要bt查看调用栈,就能清楚地知道程序死在了哪个函数。

在使用 pwndbg 时会碰到一个常见问题:gdb 在开启 ASLR 时无法稳定复现某个条件。这时可以用set disable-randomization on先关闭随机化,把利用雏形调通,再在最终脚本里通过代码解析地址来适配远程的随机化环境。本地调试和远程利用本来就该是两个阶段,不要试图在不关闭 ASLR 的情况下本地一把梭,那样调试效率太低。

5.3 pwntools 脚本编写的高效模板

最后分享一个我自己常用的 pwntools 脚本模板。它不一定最万能,但覆盖了大部分 pwn 题的基础需求,可以帮你快速进入编写逻辑的阶段,减少重复劳动。

#!/usr/bin/env python3 from pwn import * context.arch = "amd64" context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] # 是否开启本地调试模式 LOCAL = True if LOCAL: io = process("./pwn") # 如果题目提供了特定的 libc,用 patchelf 替换后启动 # 例如:io = process(["./ld-2.31.so", "./pwn"], env={"LD_LIBRARY_PATH": "./"}) else: io = remote("target.example.com", 10001) def add(size, content): io.sendlineafter(b"> ", b"1") io.sendlineafter(b"Size:", str(size).encode()) io.sendlineafter(b"Content:", content) def delete(idx): io.sendlineafter(b"> ", b"2") io.sendlineafter(b"Index:", str(idx).encode()) def edit(idx, content): io.sendlineafter(b"> ", b"3") io.sendlineafter(b"Index:", str(idx).encode()) io.sendlineafter(b"Content:", content) def show(idx): io.sendlineafter(b"> ", b"4") io.sendlineafter(b"Index:", str(idx).encode()) return io.recvline() # 示例:泄露 libc 地址 # leak = show(0) # leak_addr = u64(leak[:6].ljust(8, b"\x00")) # log.success(f"leak addr: {hex(leak_addr)}") io.interactive()

这个模板里的两个细节值得说下。第一,context.terminal设置为 tmux 分屏后,配合gdb.attach(io, "b *0x400000+0x1234")可以很方便地在调试过程中一键调起 gdb 附着到当前进程,比手动开 gdb 再 attach 省事得多。第二,io.sendlineafter的第二个参数,我建议统一用菜单的提示符b"> "而不是具体的数字,这样即使输入对应的功能号变了,脚本的兼容性也更好。

5.4 常见错误与规避建议速查表

最后放一个我在比赛过程中遇到的常见错误速查表,每一行都是真实踩过的坑,不是网上抄来的空话。

问题现象可能原因解决办法
本地能打通,远程直接没回显远程 libc 版本与本地不一致泄露多个函数地址匹配 libc,或读取远程/proc/self/maps
打远程时 pwntools 一直卡在sendlineafter提示字符串与实际不匹配先用nc手动连接一次,确认交互格式
栈溢出 payload 打过去程序报错退出忽略栈对齐导致system崩溃在system前加一段retgadget 保证 16 字节对齐
堆题edit后程序显示 malloc(): invalid pointertcache fd 指针被修改后指向了非法地址检查伪造 chunk 的 size 字段和对齐属性
泄露地址后用 LibcSearcher 匹配到错误 libc只泄露了一个函数地址,匹配不唯一泄露两个以上函数地址,交叉验证
远程脚本每次结果不一致硬编码了本地地址每次连接后动态解析基址和返回值
调用 system 后没有 shell 交互stdout 缓冲区未刷新在 ROP 链前调用setvbuf或直接发送cat flag的命令
gdb 调试时无法定位崩溃点没有开启 core dump运行ulimit -c unlimited后重新加载 core 文件

我没有再往下写更进阶的 2023 ISCC 高难度题目拆解,因为那些题目在复现时需要完整的附件和容器环境,脱离实际文件写讲解很容易变成纸上谈兵。但上面提到的这些思路和手法,已经足够覆盖 ISCC pwn 方向七成以上的题目解题需求。从栈溢出到 ROP,从格式化字符串到堆利用,每一类题的核心都是先搞清楚程序给了你什么能力,再决定怎么把它转化成控制流劫持。

最后再分享一点个人体会:参加 pwn 比赛,最重要的能力不是背下多少利用模板,而是调试时能不能快速定位到漏洞点,写脚本时能不能把漏洞点转化成稳定的利用原语。这个能力没有捷径,只有多刷题、多调试、多踩坑。ISCC 2023 的 pwn 题目难度层次分明,非常适合作为系统练习 pwn 方向的起点:把每一道题都完整地做一遍,从信息收集到写出稳定的远程 exploit,这个过程本身就是最好的成长路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询