[TOC]
前沿概述
正式比赛时间:2025.11.15 8:00--2025.11.16 23.59
BJTUCTF-PWN:
出题主要考察入门PWN~栈溢出进阶的内容,涉及栈溢出基本原理、libc泄露、ROP构造攻击链、沙盒逃逸等。
出题环境:Ubuntu18.04、Ubuntu24.04、备用Kali2024
程序源代码类型:C语言
已公布WP。
BJTUCTF——WP
|
|
前言:请各位不要完全照抄WP的exp,都是作者在本地复现,要远程连接靶机复现就稍改一下,加个远程端口和IP就好😄。
pwn1
这题是让大家测试自己的环境是否部署成功,包括Linux虚拟机、nc、pwntools、python2和3等。
直接运行exp:
|
|
这题用意在于你是否能理解每一行代码:
|
|
PWN的解题思路大致为:checksec指令看保护信息→使用正确的IDA去分析功能函数→看汇编代码和C语言伪代码→寻找漏洞包括后门函数
system("/bin/sh");、execve("/bin/sh");、system("sh");等→编写EXP在此之前需要特别注意的是程序架构,有x86和x64,两个架构的参数传递顺序、字节长度、运行流程都有所差别。
pwn2
checksec一下看看:

- Arch:程序架构为
i386 - 32 - little,即 32 位小端序的 Intel x86 架构。 - RELRO:开启了
Partial RELRO,意味着部分重定位只读,有一定的防重定向攻击能力,但不是完全的。 - Stack:
No canary found,表示栈中没有启用栈保护(金丝雀),这使得栈溢出攻击更容易实施。 - NX:
NX enabled,表示启用了不可执行(No - eXecute)保护,即栈上的代码默认不可执行,能防范一些基于栈的代码注入攻击。 - PIE:
No PIE,即没有启用地址空间布局随机化(Position - Independent Executable),程序的内存地址是固定的,这对利用内存漏洞(如缓冲区溢出等)有帮助,因为攻击者可以预先知道关键地址。
IDA32位分析:

双击跟进BJTUCTF函数:

现在开始读C语言代码了,看看在讲什么,定义了容量为18大小的buf数组,然后有一个read函数,会读取0x32大小到buf上。乍一看很正常,其实这已经是一个漏洞了,我们可以用借助read的作用,使程序发生栈溢出,填满了buf接着溢出后,把return对应的地址给覆盖了,这时候return会指向我们覆盖的代码,那我们可以设计把这个覆盖的代码写成设计好的一个地址,这样return返回到那个地址,去执行我想要的效果。
行,本题基本思路已达成,那我们找找有无可利用的东西,在左边的函数目录上看到了两个函数很可疑:hint & SASCTF


看到这里就想,根本不是system("/bin/sh");,比较相似有个sh啊,其实在开篇题目运行程序时,就已经提示过了,这题就是要用sh去拿后台程序的flag,没用常见的后门函数,那我们就自己构建一个…。
先把exp框架搭出来:
|
|
差payload:
一开始先把缓冲区(buf)填满先,用0x16,待会还要加4,也就是(0x16+4),为什么加4?
在 32 位程序的函数调用栈帧中,结构大致如下(从高地址到低地址):
|
|
当进行缓冲区溢出时:
- 首先需要填充
0x16字节来填满整个缓冲区(局部变量区域) - 接着需要再填充
4字节来覆盖ebp(因为 32 位系统中ebp是 4 字节寄存器)
覆盖 ebp 本身对漏洞利用通常没有直接作用,但这是到达返回地址的必经之路 —— 只有先覆盖掉 ebp,后续的数据才能写入到返回地址的位置。
所以 0x16+4 的完整含义是:0x16 字节填满缓冲区 + 4 字节覆盖 ebp,之后就可以开始写入我们想要的返回地址(system 函数地址)了。
system函数:
|
|
可知道system函数只有一个变量参数要代入。我们没找到/bin/sh,构造不出system("/bin/sh");,但我们可以构造出system("sh");
用Alt+T搜寻sh字符串地址:

payload:
|
|
其实这里的system函数地址可以用IDA去找的,我这里用ELF去找也行,更方便。
exp:
|
|
pwn3
这题来到一个ROPgadget的使用,给各位简单介绍一下:
ROPgadget 是一款在二进制漏洞利用领域广泛使用的工具,主要用于寻找和分析二进制文件中的返回导向编程(Return - Oriented Programming,ROP)小工具(gadget)。
ROPgadget在漏洞利用中的作用
- 绕过防御机制:在现代操作系统中,存在多种内存保护机制,如栈保护(Canary)、地址空间布局随机化(ASLR)、不可执行栈(NX)等。ROP 技术利用程序中已有的代码片段(ROPgadget)来构建新的执行逻辑,能够在不执行攻击者注入的代码(绕过 NX 保护)的情况下,实现任意代码执行的效果。例如,攻击者可以利用 ROPgadget 来构建系统调用,以获取 shell。
- 构造复杂攻击链:通过组合不同功能的 ROPgadget,攻击者可以实现一系列复杂的操作,如设置函数参数、调用特定函数等,从而完成对目标程序的攻击。
工作原理
- 机器码扫描:ROPgadget 会对二进制文件(如 ELF、PE 等格式)进行扫描,它不是基于高级语言的语法来分析,而是直接针对机器码。通过搜索特定的指令序列模式,找到以
ret指令结尾的指令片段,这些片段就是 ROPgadget。- 地址解析:在找到 ROP 小工具后,还会解析出它们在二进制文件中的虚拟地址,方便在漏洞利用代码编写时使用。
主要功能
- 查找 ROPgadget:用户可以指定二进制文件,ROPgadget 会列出文件中所有符合条件的 ROP 小工具。例如,它可以找到能够用于设置寄存器值、执行系统调用等功能的小工具。
- 过滤与筛选:支持根据不同的条件对 ROP 小工具进行过滤,比如按照指令的具体内容(如寻找包含特定寄存器操作的 gadget)、操作数的值等进行筛选,帮助使用者更精准地找到需要的小工具。
题解过程
checksec:

IDA64位分析:


找到了一个backdoor函数,那我们就可以通过栈溢出(0xA+8,64位架构的寄存器ebp是8字节大小)覆盖返回地址,使指针指向backdoor的system函数从而得到交互权限拿flag。
先写出大致exp框架:
|
|
那payload就应该是:
|
|
看到这里你可以去尝试一下是否能打通,发现是报EOF崩溃的(如SIGSEGV)。
这里涉及到一个细节:
64 位程序调用
system需考虑堆栈平衡,本质是 64 位 ABI 对 “栈对齐” 的硬性要求 —— 若不满足,函数执行时访问栈内存会崩溃;而 32 位 ABI 无此要求,仅需调用后清理参数栈即可,因此无需额外关注平衡。对于这个阶段的PWN学习,要进一步判断是否堆栈平衡,需要用到gdb调试,16字节求余等方法去判断,比较难理解,等各位后续学到调用约定、程序架构、栈堆就可慢慢理解,对于当前阶段而言只需记得堆栈不平衡就加上ret的地址进行调平即可。
ret就相当于return,执行之后就会返回“起点”,相当于一次校正,怕你走歪,不然你后续执行程序就会引发崩溃。
所以说 exp的payload 需要考虑到堆栈平衡加上 ret 指令片段的返回地址:
这里我们用:
|
|

所以:
|
|
根据崩溃情况反推:如果去掉ret后程序崩溃(如报SIGSEGV),但加上ret后正常执行,那就说明需要堆栈平衡,这就是64架构的不同之处。
完整版exp:
|
|
pwn4
这题初步考察了格式化字符串。
checksec一下:

32位关闭PIE,部分开启RELRO,开启了canary保护,意味着不能用栈溢出那一套方法了。😄
IDA
32位分析:

从main函数可以看到当Expl0rer = 6 的时候即可获的一个shell。
双击BJTUCTF函数跟进查看:

可以看到这里的ptintf(s)明显的存在格式化字符串漏洞,第一次接触,不知道为啥这里就存在漏洞?别急后面会逐步讲解。
首先将数组s初始化为0,清除数组中的内容,然后读取0x50的数据到字符数组s中。
printf(s); 使用用户输入的内容作为格式字符串,进行 printf 输出。这里存在格式字符串漏洞,我们可以先简单尝试一下,先正常输入字符,看起来没有问题:

但是当我们输入特殊的格式字符时候会输出特定的内容:

运行了多次,发现下面这串地址是随机化的。
在 C 语言中,
%x是printf等格式化输出函数的格式控制符,用于将整数以十六进制(小写字母) 的形式输出。输入
%x.%x.%x,可能会输出类似bffff3a0.8048520.1的结果,这些就是栈上不同位置的数据(具体值取决于程序运行时的栈状态)。这种特性使得
%x成为格式化字符串漏洞中泄露内存信息的常用工具。
多搞几个%x看看:

我试了好几次都不行,再来一次,换成AAAA更明显😔:

终于找到了,偏移量为7,因为是小端序,两位字母为一个单位从右到左排的,所以比较难辨认,当然用BJTU也行:

“BJTU” 对应的十六进制(按字符依次转换)为
0x42 0x4A 0x54 0x55,细心一点就能看出来了。
这个偏移量是7为巧合?可以进行验证一下,分别用指令%7$p、%7$x:


我们需要让Expl0rer = 6,现在又有格式化字符串漏洞,我们就可以使用其任意地址写功能将Expl0rer的值修改为6即可获得shell。
这里使用pwntools模块中的fmtstr模块直接进行改写:
|
|
完整exp:
|
|
pwn5
这题对于PWN入门手来说最大一个弱点是过度依赖IDA,其实这是一个缓冲区是一个动态分配,我们需要搭配pwngdb、pwndbg去调试,得出真实的缓冲区。
checksec:

32位关canary、PIE保护,开NX保护。
IDA32位分析:

main函数这很明显有个栈溢出漏洞,0x64+4就能覆盖。
在汇编代码块这看到了个_system:


全局搜索了下,没有看到/bin/sh。
顺便搜索了system:

接着我们需要通过gets函数手动写入/bin/sh字符串到一个可写可执行区域,通常在bss段,在ida找到一个地址。

为什么要写入bss内存字段?直接给system函数压进一个字符串
/bin/sh作为变量不就可以了吗?这我当初也这么想,其实不然:BSS 段的特性:BSS(Block Started by Symbol)段通常用于存储未初始化或初始化为 0 的全局变量和静态变量。它在程序加载时就已经分配好了固定的内存空间, 并且在程序的整个生命周期内,这块内存区域的地址是相对稳定的。将
/bin/sh写入 BSS 段,可以保证字符串在需要使用时,其内容不会被意外修改,从而确保system函数能够正确获取到参数。变量有效性:不管是system、常见的printf、read这些函数都是要有参数的,不是简简单单放一个字符串就可以了,变量需要在程序中有地址,这样程序才能找到它,单一个字符串是没有意义的,因为没有分配内存地址。
exp:
|
|
对于缓冲区大小,不是0x64+4吗?这里是一个坑,这里我们具体要用gdb+cyclic来分析:
详细步骤:
第一步先生成个200个随机字符,毕竟大部分参数容量大小不会有200这么大吧。
复制,接着给gets或者main函数断点,再输入r(run)运行程序:
这时候就会出现彩色的调试界面,有些人是在一个终端窗口出现的,我的是在另外一个弹出终端窗口调试界面的。
输入n(next),一步步执行程序,直到指针指向gets函数需要我们输入字符串的时候:
输入上次用cyclic生成的200个随机字符串。
然后再下一步n,直到崩溃:
出现了
SIGSEGV崩溃标识,右边是我们注入的200个随机字符串的结果。去调试界面,看EIP状况:
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 30 31 32 33──────────────────────────[ REGISTERS ]────────────────────────── EAX 0x0 EBX 0x0 ECX 0xf7fac5c0 (_IO_2_1_stdin_) ◂— 0xfbad2288 EDX 0xf7fad89c (_IO_stdfile_0_lock) ◂— 0x0 EDI 0x0 ESI 0xf7fac000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c EBP 0x62616163 ('caab') ESP 0xffffcf10 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' EIP 0x62616164 ('daab') ───────────────────────────[ DISASM ]──────────────────────────── Invalid address 0x62616164 ────────────────────────────[ STACK ]──────────────────────────── 00:0000│ esp 0xffffcf10 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 01:0004│ 0xffffcf14 ◂— 'faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 02:0008│ 0xffffcf18 ◂— 'gaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 03:000c│ 0xffffcf1c ◂— 'haabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 04:0010│ 0xffffcf20 ◂— 'iaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 05:0014│ 0xffffcf24 ◂— 'jaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 06:0018│ 0xffffcf28 ◂— 'kaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' 07:001c│ 0xffffcf2c ◂— 'laabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab' ──────────────────────────[ BACKTRACE ]────────────────────────── ► f 0 62616164 f 1 62616165 f 2 62616166 f 3 62616167 f 4 62616168 f 5 62616169 f 6 6261616a f 7 6261616b在利用
cyclic工具进行缓冲区溢出漏洞测试时,关注EIP寄存器中存储的字符串(实际上是特定模式的字符序列),是为了精确定位覆盖EIP所需的偏移量,这是漏洞利用的关键步骤。
这时,EIP被“daab”所覆盖,这里用cyclic工具查此时的偏移量。
|
|

112的十六进制是0x70,我们程序需要覆盖的大小应该是0x70。
对于payload:
p32(gets_addr):gets_addr是gets函数在程序 PLT(过程链接表)中的地址(整数形式)。通过p32,将这个32 位的地址整数转换成小端字节序的 32 位二进制数据,这样当 payload 被程序接收并处理时,内存中就能正确存储gets函数的地址,为后续调用gets函数做准备。p32(sys_addr):sys_addr是system函数在 PLT 中的地址(整数形式)。同样,p32把这个地址转换成小端字节序的 32 位二进制数据,目的是让程序后续能正确跳转到system函数执行。- 两个
p32(bss_addr):bss_addr是程序.bss段的一个地址(整数形式)。第一个p32(bss_addr)是作为system函数的返回地址(不过在这个利用逻辑里,更关键的是第二个p32(bss_addr));第二个p32(bss_addr)是作为gets函数的参数,意思是让gets函数把用户输入(这里是/bin/sh)写入到bss_addr对应的内存位置。之后调用system函数时,就会以bss_addr处的/bin/sh作为参数,执行system("/bin/sh")来获取 shell。
小结:总的来说这题对新手不算友好,但在PWN手来说算基本操作。
pwn6
checksec保护信息:

IDA 分析(依据函数功能修改对应函数名):

看到了陌生的mmap函数:
1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代 I/O 读写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。
那这里的mmap函数作用就是把从 0x123000 开始的地址,大小为 0x1000 的长度,权限改为可写可执行。
跟进seccomp函数:

seccomp 主要用于配置和加载 Seccomp(Secure Computing Mode,安全计算模式) 策略,来限制程序可以执行的系统调用,以增强程序的安全性。
PWN经典题目——沙箱逃逸(对立是沙盒过滤):
|
|

可以看到,只有 read,write,open,exit 可以使用,使用 open–>read–>write 这样的 orw 的方式。
ORW 指的是 Open-Read-Write 技术,是一种利用系统调用读取文件内容(如 flag 文件)的攻击方法。
ORW 通过以下三个系统调用实现:
open:打开目标文件,获取文件描述符。
read:通过文件描述符读取文件内容到缓冲区。
write:将缓冲区的内容写入标准输出。
跟进BJTUCTF函数:

看到了明显的栈溢出漏洞,在上面我提及过 mmap 函数,在 main 函数已经执行过一次了,在 0x123000 这里已经有可写可执行权限了。到这里,攻击思路就比较清晰了,我们想办法往 mmap 给的这个地址段里面写 shellcode (ORW),然后跳转到这里执行。
那我们的 orw_shellcode:
|
|
shellcode 已经编好了,现在编 payload 让程序听我们话,将 shellcode 注入进内存中去。
shellcraft.read 的作用是执行 read 函数的后续流程 —— 读取 0x100 长度的字符(也就是后续的 orw_shellcode),文件描述符 fd=3(0、1、2 已被标准文件占用)
shellcraft.write(1, mmap, 0x100) 的 1 是 Linux 标准输出(stdout)的固定文件描述符,其功能是将 mmap 区域中存储的 \flag 内容输出到屏幕,当成 printf 也行。
payload:
|
|
我在汇编代码分析的过程中找到关键的 jmp_rsp 后门地址,可以调用这行汇编代码来实现跳转回起始地址:

ayload = asm (shellcraft.read (0, mmap, 0x100)) 的意思是启动 read 函数,开启 “读取模式” 处于待读取状态,等待后面的 orw_shellcode 的出现,并存到 mmap 的可读可写区域(2+4);
最正确理解:执行后因缺少输入数据(orw_shellcode)才进入 “等待状态”。
asm (“mov rax, 0x123000; jmp rax”) 的意思是跳到 mmap=0x123000 区域,也就是指向这个区域,等待后续 orw_shellcode 注入执行。
payload = asm (shellcraft.read (0, mmap, 0x100)) + asm (“mov rax, 0x123000; jmp rax”) 加起来不足 0x28 个字节(0x20+8),让 buf 刚好充满,以便后续将 rsp 调整到 buf 缓冲区起始地址(buf 复原)。
python3 的 exp:
|
|
结尾
有疑问解决不了,可以在下方评论区相互交流,作者一直都在….





