通过直接刷题逆向学习pwn:ciscn_2019_n_1 - 浮点数溢出与IEEE 754编码实战

通过直接刷题逆向学习pwn:ciscn_2019_n_1 - 浮点数溢出与IEEE 754编码实战
Xiaozhi_z查看文件详情
使用checksec查看文件保护
1 | checksec --file=ciscn_2019_n_1 |
- No canary found:没有栈溢出保护,可以直接栈溢出
- No PIE:代码段地址固定,可以直接硬编码地址
- Partial RELRO:GOT表可写,便于GOT劫持攻击
这个题开启了NX (No-eXecute)
其实之前解题也没有直接传shellcode影响不大(
作用: 防止在栈或堆上执行代码
工作原理:
- 将内存页标记为不可执行
- 即使注入shellcode也无法执行
NX保护主要防止的是直接在栈上执行代码,但我们之前的攻击策略是:
攻击方式叫做ROP(Return-Oriented Programming)
1 | 栈溢出 → 覆盖返回地址 → 跳转到已有的system("/bin/sh")代码 |
特性 | 传统shellcode注入 | ROP攻击 |
---|---|---|
执行位置 | 栈上执行shellcode | 代码段执行已有函数 |
NX保护影响 | 被NX阻止 | 可绕过NX |
所需条件 | 栈可执行 | 程序中有可用gadgets |
攻击复杂度 | 简单直接 | 相对复杂 |
payload结构 | padding + shellcode + 返回地址 |
padding + ROP链地 |
padding是填充数据的意思
查看文件 查到为64Bit 等一会使用IDA64打开
IDA分析
使用IDA64打开软件 mian函数伪代码如下
代码中主要内容为func函数,双击func函数打开
func函数伪代码如下
1 | int func() |
含义如下
1 | int func() |
正常程序运行逻辑
- 程序启动 → 执行
main
函数 - I/O初始化 → 设置无缓冲模式
- 调用func → 进入核心功能函数
- 变量声明 → 分配44字节缓冲区和浮点变量
v2=0.0
- 显示提示 → 输出”Let’s guess the number.”
- 等待输入 → 用户通过
gets(v1)
输入数据 - 安全检查 → 检查
if (v2 == 11.28125)
- 条件为假 → 由于
v2
仍然是初始值 0.0 - 显示错误 → 输出”Its value should be 11.28125”
- 程序退出 → 返回主函数并结束
注入攻击逻辑
- 程序启动 → 执行
main
函数 - I/O初始化 → 设置无缓冲模式
- 调用func → 进入核心功能函数
- 变量声明 → 分配44字节缓冲区和浮点变量
v2=0.0
- 显示提示 → 输出”Let’s guess the number.”
- 恶意输入 → 用户输入精心构造的payload:
- 前44字节:任意填充数据
- 后续4字节:浮点数 11.28125 的二进制表示
- 缓冲区溢出 →
gets()
不检查边界,数据溢出覆盖v2
变量 - 安全检查 → 检查
if (v2 == 11.28125)
- 条件为真 → 因为
v2
被溢出数据修改为 11.28125 - 获取flag → 执行
system("cat /flag")
显示flag内容
攻击结果:成功利用缓冲区溢出漏洞获取flag
需要覆盖多少的值?
查看代码可以发现
1 | char v1[44]; // 声明了44个字节的数组 |
内存布局:
v1
占用了前44个”储物柜”v2
紧挨着占用后面4个”储物柜”
攻击方法:
输入超过44个字符 + 4字节(将v2的值改写为11.28125即可获得flag)
1 | [A][A][A][A]...[A][A][A][A] | [特][殊][数][字] |
将IEEE 754浮点数转换为十六进制
打开在线网站输入11.28125转换为十六进制
补充:这个转换过程就像是给数字”11.28125”制作一个计算机能识别的身份证——IEEE 754标准规定了统一的编码格式,确保在任何支持该标准的计算机系统中,相同的浮点数都有唯一的二进制表示。
人类语言 | → | 计算机语言 |
---|---|---|
11.28125 | → | 0x41348000 |
理解内存中的字节顺序
1 | char v1[44]; // [rsp+0h] [rbp-30h] BYREF |
rbp
寄存器表明这是 x86-64 架构- x86 和 x86-64 架构都是小端序
- 这是行业标准,就像开车靠右行驶一样
为了便于理解小端序,想象你要在信封上写地址:
大端序(人类习惯):
1 | 省 市 区 街道 门牌号 |
小端序(计算机习惯):
1 | 门牌号 街道 区 市 省 |
数字 0x41348000
在内存中的存放:
大端序的顺序:
1 | 字节0: 0x41 |
小端序实际存储:
1 | 字节0: 0x00 ← 最低位在前 |
这个东西转换也是有在线工具的:https://www.toolhelper.cn/Digit/LittleBigEndianConvert(这道题自己转换就够了
最后转换为\x00\x80\x34\x41
的格式就可以啦
补充:为什么最终要写成 \x00\x80\x34\x41
这样的格式?
简单来说,\x
就像是给计算机的”喂食指令”。当我们把十六进制数 00 80 34 41
前面都加上 \x
,就相当于告诉计算机:”请直接把这些数字当作字节数据吃掉,不要当成普通文字!”
格式 | 用途 | 好比 |
---|---|---|
0x41348000 |
给人看的数字表示 | 菜单上的菜名 |
\x00\x80\x34\x41 |
计算机实际处理的字节 | 端上桌的菜品 |
在Python等编程语言中,\x
开头的表示法就是在说:”我后面这两个字符是一个真实的字节”,这样计算机就能准确地把我们构造的数字”喂”给目标程序了。
使用Linux自带命令尝试溢出
注入原理
1 | v1数组(44字节) v2变量(4字节) |
使用echo语句就能完成啦(先用本地文件进行测试)
1 | echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x80\x34\x41' | ./ciscn_2019_n_1 |
命令分解:
部分 | 作用 |
---|---|
echo -ne |
输出不换行的原始字节数据 |
44个A |
填满v1数组的44字节空间 |
\x00\x80\x34\x41 |
覆盖v2变量为11.28125 |
| | 管道符 |
./ciscn_2019_n_1 | 打开对应文件 |
成功执行了cat flag命令! 也就是这个注入方式是没有问题的,只不过本地不是靶机并没有存放flag,下一步构建新的命令使用nc连接远端服务器就好啦!
起初我把命令改成了这样(错误示范)
直接就失败了(
1 | echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x80\x34\x41' | nc node5.buuoj.cn 25389 |
问题原因: echo
命令发送完payload后立即关闭连接,程序还没机会输出flag!
linux管道工作原理(想象版本):
1 | echo → 发送数据 → 立即结束 → 关闭管道 → nc连接断开 → 程序被终止 |
我们在命令里面加一个cat命令就可以实现以下流程:
1 | (echo发送payload ; cat保持输入流打开) → 持续连接 → 程序输出flag → cat转发给我们 |
正确的shell命令
1 | (echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x80\x34\x41'; cat) | nc node5.buuoj.cn 25389 |
成功拿到Flag!