BUUCTF get_started_3dsctf_2016 wp

1.使用checksec命令查看文件的保护机制开启情况:

bash 复制代码
checksec --file=./get_started_3dsctf_2016

显示除了NX保护均未开启或完全启用 ,说明此题难度系数不高,而NX开启说明数据区域(如栈、堆)不可执行,是防止将 shellcode 写入数据区后跳转执行,所以可优先考虑不利用shellcode注入且简单直接的方式寻找漏洞。

2.利用IDA静态分析:

main函数中出现了危险函数gets ,提示此题为栈溢出类型。且v4偏移量为 56 + 4 (真的吗?)

get_flag函数中出现了目标文件**"flag.txt"** ,并与 fopen 搭配使用。而根据对伪代码的逻辑分析可知,后续代码逻辑就是通过循环逐字符打印 flag.txt 中的字符串(即flag)。 所以,只需要满足进入核心逻辑的判断条件a1 == 814536271 && a2 == 425138641 即可:由函数定义 void __cdecl get_flag(int a1, int a2) 可知,a1、a2是作为两个参数在函数调用时传入,那么便可以在后续构造payload时,讲符合判断条件的a1、a2连同函数起始地址一同传入进行覆盖。

从get_flag函数的汇编详情界面可获得函数的起始地址0x080489A0 ,以及判断条件指定值对应的16进制数:a1--814536271--308CD64F,a2--425138641--195719D1

3.编写python脚本:

python 复制代码
from pwn import *
r = remote('node5.buuoj.cn', 29558)

offset = 56 + 4
get_flag_addr = 0x080489A0

payload = b'A' * offset + p32(get_flag_addr) # 实现栈溢出覆盖
payload += p32(0)                            # 填充返回地址
payload += p32(0x308CD64F) + p32(0x195719D1) # 注入满足判断条件的参数值

r.sendline(payload)
r.interactive()

详细解读:

1. 为什么需要在p32(get_flag_addr)后传入一个返回地址(p32(0))?

这是由x86架构的函数调用约定栈帧结构决定的。当发生栈溢出并覆盖返回地址时,栈的布局必须模拟一次正常的函数调用过程。

  • 正常函数调用(call指令)​​:

    当程序执行call get_flag时,会先将返回地址 ​(即call下一条指令的地址)压入栈顶,然后跳转到get_flag函数。函数内部通过ret指令结束时,会从栈顶弹出这个返回地址,并跳转回去继续执行。

  • 溢出攻击中的模拟​:

    payload通过溢出覆盖了原返回地址,将其改为get_flag_addr。但get_flag函数执行完毕后,同样需要执行ret指令,此时它会从栈顶读取一个值作为返回地址。因此,你必须在get_flag_addr之后立即放置一个有效的返回地址,否则程序会跳转到非法地址导致崩溃。(此题由于当get_flag函数执行完毕后,flag已被打印出来,并不需要关注函数后续跳转到哪里)------这对吗?

2. 为什么a1a2的值需要用16进制且由p32()打包?

这与数据表示方式架构要求相关,并非必须用16进制,但需要正确转换为字节序列。

  • 参数值的本质​:

    a1 == 814536271a2 == 425138641是十进制整数,但在内存中参数以二进制字节形式存储。16进制(如0x308CD64F)只是更便捷的表示方式,本质上与十进制等价。

  • ​**p32()的作用**​:

    p32()是pwntools提供的函数,用于将32位整数打包为小端序的4字节序列。因为x86架构使用小端序(低位字节在前),直接传入十进制数会导致字节顺序错误。例如:

    • 814536271的十六进制是0x308CD64Fp32(0x308CD64F)会输出字节序列\x4f\xd6\x8c\x30

    • 如果直接传入十进制数814536271,打包结果相同,但16进制更易读且便于调试。

  • 为什么不能直接传入字符串?​

    参数是整型(int),而非字符串。栈上的参数必须按内存布局直接写入原始字节,不能以字符串形式传递(否则会被解释为ASCII码,导致值错误)

3.x86架构(32位)下的参数传递与Payload顺序

在x86架构中,​所有函数参数都通过栈传递 ,且压栈顺序是从右向左​(即先压入最右边的参数)。

  • 正常函数调用时的栈布局 ​(以get_flag(a1, a2)为例):

    1. 先将参数a2(第二个参数)压栈。

    2. 再将参数a1(第一个参数)压栈。

    3. 执行call get_flag指令,该指令会将返回地址压栈。

      由于栈是从高地址向低地址增长的,压栈完成后,栈上的布局从低地址到高地址 依次为:​返回地址 ​ → ​a1 ​ → ​a2 。也就是说,第一个参数a1在内存中的位置反而更靠近低地址(在返回地址之后),第二个参数a2在更高地址。

  • 栈溢出攻击中的Payload布局​:

    通过溢出覆盖返回地址后,需要模拟上述正常的栈帧结构。因此,在覆盖了新的返回地址(即get_flag_addr)之后,栈上的布局必须是:

    [get_flag_addr] + [返回地址(如exit_addr)] + [a1] + [a2]

    这里的关键是:

    • 虽然压栈顺序是从右向左 ​(先a2,后a1),但在内存中的存储顺序从左向右 ​(先a1,后a2)。

    • 所以Payload中a1a2之前,正是为了符合函数内部通过ebp+8访问第一个参数a1,通过ebp+12访问第二个参数a2的内存布局要求。

4.运行脚本:

????????为什么会报错呢????????


错误信息解读:

  • timeout: 远程服务在等待你的exploit执行完成时超时。

  • the monitored command dumped core: 你发送的payload导致目标程序出现严重错误(如段错误),操作系统终止了进程并生成了core dump文件。

这个错误尤其常见于64位架构的pwn题 。你的exploit可能在本地测试成功,但在远程打不通,主要原因往往与栈对齐问题有关。

But,此题是32位架构,为什么会出现此错误呢?------看来是payload出了问题。。。

python 复制代码
payload = b'A' * offset + p32(get_flag_addr) # 实现栈溢出覆盖
payload += p32(0)                            # 填充返回地址
payload += p32(0x308CD64F) 
payload += p32(0x195719D1)                   # 注入满足判断条件的参数值

注入满足判断条件的参数值这一步肯定是没有问题的,由以上种种分析可知,由于栈帧规则等限定,该步骤两个参数的传入形式和顺序都是正确的。那么,对于第一步实现栈溢出的覆盖,如果有错误的话,一定是偏移量offset的问题;对于第二步填充返回地址,有可能此题不能随便填一个地址,可能有约束条件。


错误分析:

1.对于第二步填充返回地址,重新对伪代码进行分析:
1. fopen函数:

此程序中的特殊之处就是使用了fopen ,与文件产生链接,对于fopen函数:

数据的存储位置:

当你使用 fopen打开一个文件时,数据实际上涉及两个主要的存储位置:

  1. 最终归宿:磁盘

    文件本身的内容,也就是你希望长期保存的数据,始终存储在硬盘等外部存储设备上。fopen的作用是建立一条从你的程序到磁盘上这个文件的通道。

  2. 高速中转站:内存缓冲区

    为了提升读写效率,C标准库在内存中开辟了一块区域作为I/O缓冲区 。当你用 fprintffwrite等函数写入数据时,数据通常先被放入这个缓冲区,而不是立即写入硬盘。当缓冲区满了,或遇到特定条件(如程序正常结束、主动刷新)时,数据才会被一次性批量写入磁盘,这大大减少了直接操作磁盘的次数。

  3. 控制中心:FILE结构体

    fopen返回的 FILE*指针,指向一个 FILE类型的结构体。这个结构体可以理解为文件流的"控制中心",它记录了文件描述符、缓冲区的位置和状态、当前读写位置、错误标志等关键信息。这个 FILE结构体本身是在 fopen函数内部通过 malloc等函数在 上动态分配内存的,因此你需要用 fclose来释放这块内存。

因此,fopen函数读入数据丢失的风险主要来自于数据还滞留在"中转站"(内存缓冲区)而未到达"最终归宿"(磁盘)。

主要原因包括:​

  • 程序异常终止​:如果程序因为崩溃、被强制杀死(如按下Ctrl+C)或断电而突然结束,缓冲区中的数据将来不及写入磁盘,从而丢失。

  • 未关闭文件 ​:fclose函数在执行时,会先将缓冲区中的剩余数据写入磁盘,再释放资源。如果忘记调用 fclose,不仅可能导致数据丢失,还会造成内存泄漏。

  • 缓冲区未刷新​:对于需要实时确保数据落盘的场景(如记录关键日志),如果仅写入缓冲区而没有主动刷新,在下次定时刷新之前发生异常,数据就会丢失。

综上:正确关闭文件会自动刷新 该文件对应的缓冲区 + 程序正常终止时,会自动清理并刷新所有已打开的文件缓冲区,然后关闭文件 ------> 可防止数据丢失,保证正常回显。

核心概念 关键操作 作用与说明
文件缓冲区 fclose(fp) 最推荐、最根本的方法 。关闭文件会自动刷新该文件对应的缓冲区,确保数据写入磁盘。这是任何文件操作完成后都应执行的步骤。
fflush(fp) 强制刷新指定文件的缓冲区,将数据立即写入磁盘,但文件保持打开状态。适用于需要实时持久化数据又不想关闭文件的场景。
程序退出 exit()函数 程序正常终止时,会自动清理并刷新所有已打开的文件缓冲区,然后关闭文件。
return(从main函数) 效果与调用exit()类似,属于正常退出,也会刷新缓冲区。
异常终止 (如崩溃) 缓冲区不会被刷新,数据可能丢失。

再次对伪代码进行分析,可以发现,虽然函数末尾存在 fclose ,但其上游代码do-while循环的终止条件是 v5 != 255 ,这意味着getc函数读取到字符值等于255时,循环便会退出 。而在C语言中,getc函数在遇到文件结束或发生错误时会返回EOFEOF是一个宏,其值通常被定义为​-1​ (这取决于编译器,但常见实现是-1)。那么这里将v5getc的返回值,类型是int) 与255进行比较:如果EOF的值确实是-10xFFFFFFFF),那么它不可能等于255 ​ (0xFF)。如果文件顺利读完,循环会在遇到EOF后继续尝试读取,因为-1 != 255,这会导致无限循环 。所以,函数末尾的fclose大概率因无限循环无法跳出而失效, 导致运行脚本后的 timeout: the monitored command dumped core 超时报错。

那么,便需要让程序正常终止 ,但对于正常逻辑 return(从main函数退出) 这种退出方法,对于此题并不适用,因为 payload 注入后,main函数返回地址已被覆盖为 get_flag 的地址,遭到破坏。

注入payload后的栈空间结构(从高地址到低地址)

地址方向 内存内容 大小 值(示例) 说明
高地址 a2参数 4字节 0x195719D1 get_flag的第二个参数,值对应十进制425138641。在x86 cdecl约定中,参数从右向左压栈,因此a2先压栈,位于高地址。
a1参数 4字节 0x308CD64F get_flag的第一个参数,值对应十进制814536271。位于a2之下(较低地址)。
返回地址(用于get_flag 4字节 ------- 模拟get_flag函数执行后的返回地址。由于直接跳转(非正常调用),需人工填充一个无害值(如退出地址或垃圾数据),避免崩溃。
main的返回地址(被覆盖为get_flag地址)​ 4字节 0x08048576 原为main返回到libc的地址,现被覆盖为get_flag函数的起始地址。当main执行ret指令时,EIP跳转至此。
main的保存的EBP(被覆盖为垃圾数据) 4字节 0x42424242 原为调用main函数的帧指针,溢出后被覆盖为任意值(如'B'*4),不影响控制流。
低地址 v4缓冲区填充(垃圾数据) 56字节 0x41414141...(如'A'*56 覆盖main的局部变量v4,填充无用数据以占满空间。

因此,想要程序正常终止,我们只能尝试**exit()**函数。

很幸运,我们发现程序中给出了exit函数,并获得了起始地址0x804E6A0。

但是,经过搜索,程序中还有一个 _exit函数,这又是什么呢?(最终尝试使用_exit函数的地址进行payload构造,结果失败;而exit函数成功)

2.exit() 与 _exit() 的根本区别

它们最核心的区别在于对标准I/O缓冲区的处理方式不同。

特性 exit() _exit()
缓冲区处理 刷新所有标准I/O缓冲区 不刷新任何标准I/O缓冲区
终止处理函数 调用atexit()注册的函数 不调用任何终止处理函数
头文件 stdlib.h unistd.h
本质 C标准库函数,是_exit()的高级封装 直接调用同名系统调用,立即进入内核

当程序使用printf等标准I/O函数输出时,数据通常先存放在内存的缓冲区 里,直到缓冲区满、遇到换行符\n或文件关闭时,才真正写入目标(如屏幕)。exit()会在进程终止前执行清理工作,包括将缓冲区中的数据"刷新"到目的地 ;而_exit()则直接关闭进程,​丢弃缓冲区中的所有数据

------为什么CTF中必须使用 exit()

已此题为例,成功执行get_flag()函数后,是通过putchar这个标准I/O函数将flag内容打印到标准输出。如果payload中让程序跳转到_exit(),会发生以下情况:

  1. get_flag()函数成功读取并准备输出flag。

  2. 程序流程跳转到_exit()

  3. _exit()立即终止进程,存在于缓冲区中的flag内容将被丢弃,无法显示在终端上

  4. 攻击者看不到flag,攻击"成功"但"无效"。

而使用exit()时:

  1. 同样成功执行get_flag()

  2. 程序流程跳转到exit()

  3. exit()首先刷新stdout等所有打开的I/O缓冲区,确保flag内容被发送到终端。

  4. 然后才终止进程。

  5. 攻击者能在屏幕上看到flag,攻击真正成功。


2.对于第一步实现栈溢出的覆盖中的偏移量重新进行计算验证:

使用GDB( 搭配增强插件pwndbg**) + pwntools(** 提供cyclic等命令**)的动态调试**精确计算程序运行时v4的实际偏移量:

1.生成模式字符串

使用pwntools的cyclic生成一个唯一且长度足够的字符串(确保能覆盖返回地址):

bash 复制代码
cyclic 100

输出示例:

bash 复制代码
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab

关键​:长度需大于缓冲区大小(这里64字节),建议为缓冲区大小+至少40字节以确保覆盖关键数据。

2.GDB动态调试触发崩溃
  1. 启动GDB调试程序​:

    bash 复制代码
    gdb ./get_started_3dsctf_2016
  2. 运行程序并输入模式字符串​:

    在GDB中执行:

    复制代码
    run

    程序等待输入时,​粘贴步骤1生成的模式字符串,回车后程序会因溢出而崩溃。

  3. 记录崩溃时的关键值​:

    这里的0x6161616l就是覆盖EIP/RIP 的值(32位系统看EIP,64位看RIP)。记下这个值(示例中为0x6161616c)。

3.计算精确偏移量

使用cyclic-l选项反推偏移:

bash 复制代码
cyclic -l 0x6161616c

56就是精确的实际的偏移量,表示从缓冲区起始到返回地址的距离。

------这跟先前静态分析的结果(56 + 4)并不一致,是为什么呢?

------差了ebp本身的4位(突破口:)

【新知识点------内平栈与外平栈】

此知识点详解文章请见:https://blog.csdn.net/ankanglcy/article/details/152230540?spm=1001.2014.3001.5501

可以发现,无论是main函数还是get_flag函数,在retn返回语句前都调用了 add esp, Xadd esp, X是 x86 汇编语言中一条用于调整栈顶位置 的指令,直接操作栈指针寄存器 ESP 。它的核心作用是提升栈顶指针 ESP 的值,从而缩小栈空间 ,通常是为了"清理"栈上不再需要的数据。简单来说,add esp, X就像是在栈上用完一些东西后进行的"打扫",把栈顶指针移回合适的位置。------这是明显的外平栈标志,因此,相应偏移量计算并不需要加上ebp本身的4位。


5.重构脚本:

经过以上分析,将修复这两处错误:

python 复制代码
from pwn import *

r = remote('node5.buuoj.cn', 26078)

offset = 56
get_flag_addr = 0x080489A0
exit_addr = 0x0804E6A0
a1_value = 0x308CD64F
a2_value = 0x195719D1

payload = b'A' * offset + p32(get_flag_addr)
payload += p32(exit_addr) 
payload += p32(a1_value)
payload += p32(a2_value)

r.sendline(payload)
r.interactive()

6.获取flag:

此次运行脚本后,终于成功了!

Congratulations!

相关推荐
暗流者2 个月前
学习pwn需要的基本汇编语言知识
汇编·学习·网络安全·pwn
TAMOXL4 个月前
ctf.show pwn入门 堆利用-前置基础 pwn142
pwn·
kali-Myon4 个月前
栈迁移与onegadget利用[GHCTF 2025]ret2libc2
c语言·安全·pwn·ctf·栈溢出·栈迁移·onegadget
TAMOXL4 个月前
NSSCTF [NISACTF 2022]ezheap
pwn·
TAMOXL4 个月前
NSSCTF [GFCTF 2021]where_is_shell
pwn
Mr_Fmnwon9 个月前
【我的 PWN 学习手札】IO_FILE 之 FSOP
pwn·ctf·io_file
风间琉璃""9 个月前
PWN的知识之栈溢出
数据结构·算法·网络安全·pwn·二进制安全·栈溢出
CH13hh10 个月前
常回家看看之Tcache Stashing Unlink Attack
pwn·ctf
想拿 0day 的脚步小子10 个月前
从ctfwiki开始的pwn之旅 5.ret2csu
pwn