当前位置: 首页 > news >正文

BUUCTF get_started_3dsctf_2016 wp

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

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脚本:

from pwn import *
r = remote('node5.buuoj.cn', 29558)offset = 56 + 4
get_flag_addr = 0x080489A0payload = 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出了问题。。。

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大概率因无限循环无法跳出而失效,导致运行脚本后的 timeoutthe 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生成一个唯一且长度足够的字符串(确保能覆盖返回地址):

cyclic 100

输出示例:

aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab

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

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

    gdb ./get_started_3dsctf_2016
  2. 运行程序并输入模式字符串​:

    在GDB中执行:

    run

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

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

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

3.计算精确偏移量

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

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.重构脚本:

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

from pwn import *r = remote('node5.buuoj.cn', 26078)offset = 56
get_flag_addr = 0x080489A0
exit_addr = 0x0804E6A0
a1_value = 0x308CD64F
a2_value = 0x195719D1payload = 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!

http://www.dtcms.com/a/423491.html

相关文章:

  • 成都网站制作设计网页设计实训报告心得体会
  • Linux 进程创建与控制详解
  • 万网x3主机l系统放两个网站手机制作ppt
  • XML语言解析
  • AJAX XML:深入解析与实际应用
  • 十大网站在线观看深圳互联网推广公司
  • 价值流智能时代:DevOps平台如何成为企业高效交付的核心引擎?
  • Vue Router 动态路由完全指南:灵活掌控前端路由
  • 电子商务网站域名注册方法wordpress 模板语言包
  • 网站空间和服务器有什么区别阜宁网站制作价格
  • 【每日一问】X电容和Y电容有什么区别?
  • AI 播客:从体验到原理,知识获取的新姿势
  • 异构计算实战:CPU/GPU/TPU在创意工作流中的调度策略
  • 打破“形似”桎梏,OmniHuman-1.5让数字人“由内而外”活起来。
  • 语言理解-阿里木江【基础课笔记】
  • 邮件系统建设篇:Coremail与Exchange并行方案介绍
  • 解码数据结构队列
  • 典型的四大综合门户网站wordpress excel导入
  • 六边形架构实现:领域驱动设计 + 端口适配器模式
  • 六安网站建设定制全国最大的源码平台
  • Qt Linux交叉编译字节数目不一样
  • 概率统计中的数学语言与术语1
  • 微服务项目->在线oj系统(Java-Spring)--增删改
  • 空间设计网站yahoo搜索引擎
  • 网站建设合同英文软件外包公司名单
  • Java基础(①Tomcat + Servlet + JSP)
  • 连云港百度推广总代理上海谷歌seo公司
  • ssl外贸网站网站空间托管
  • k8s kubelet 10250监控端口访问配置
  • 十二、伪分布式配置