浮点数的存储方式和CTF实战
浮点数的存储方式和CTF实战
浮点数的存储方式是计算机科学中的一个重要概念,特别是在处理精度要求较高的计算时。本文将详细介绍浮点数的存储方式,并通过一个CTF(Capture The Flag)题目实例展示其实际应用。
浮点数的二进制表示
- 整数和小数的二进制转换
整数部分:采用除以2取余数的方式,直到商为0。例如,十进制的9转换为二进制为1001。
小数部分:采用乘以2取整数部分的方式,直到小数部分为0或达到所需精度。例如,0.625转换为二进制为101。
因此,6.625用二进制表示为110.101。验证方法如下:
整数部分:(2^2 + 2^1 + 0*2^0 = 6)
小数部分:(12^{-1} + 02^{-2} + 1*2^{-3} = 0.5 + 0 + 1/8 = 0.625)
IEEE 754标准下的浮点数存储
内存中的数据都是以二进制形式存储的,因此需要通过人为抽象存储格式来表示浮点数。目前国际通用的浮点数存储规则是IEEE 754,其存储格式如下:
以4字节的float为例,存储格式为:
- 最高位31位存储符号位s
- 23~30这8位存储指数E
- 剩下23位存储尾数M
需要注意的是:
- 指数E采用偏置表示法,即实际指数值为存储值减去127。例如,指数为2时,E=127+2=129=1000 0001。
- 尾数M中小数点左边的1可以省略,只存储小数点右边的位数。
- M只有23位,如果十进制的小数部分乘以2始终不为1,那么只取23位,其余的舍弃,这是部分浮点数有误差、无法精确表示的根本原因。
因此,6.625这个浮点数在内存中的存储形式如下:
CTF实战:浮点数存储的应用
接下来通过一个2018护网杯CTF题目“getting_start”来展示浮点数存储的实际应用。
题目代码如下:
int main() {
double v7 = 0.1;
double v8 = 0.1;
char buf[32];
read(0, buf, 40);
if (v7 == v8) {
system("/bin/sh");
}
}
要使system("/bin/sh")
被执行,需要让if
条件不成立,即v7 != v8
。由于v7
已经固定为0.1,只能通过控制buf
的值来改变v8
的值。
分析栈布局可知:
buf
距离v8
有32字节read
会读取40字节的数据
因此,可以通过控制输入数据来覆盖v8
的值。具体payload构造如下:
payload = b"a" * 24 + p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A)
其中,0x3FB999999999999A
是0.1在内存中的存储形式,接下来详细解释其计算过程:
将0.1转换为二进制形式
通过计算可知,0.1的二进制表示为:
从第6步开始出现循环,因此二进制形式为:
0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011...
提取s、M、E三个要素
小数点向右移4位才遇到第一个1,因此表达式变为:
1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011*2^(-4)
这里三个要素明确为:
s=0
M=1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001
E=(2^10-1)-4=1023-4=1019=011 1111 1011
注意:这里的M只保留52位时,最后一位是1,这位不能直接去掉,要在末尾+1(小数存放的误差就是这么来的),所以真正的M = 1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
拼接s、M、E
按照标准把s、M、E首尾拼接:
0 || 011 1111 1011 || 1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
即:
0011 1111 1011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
转换为十六进制为:
0x3FB999999999999A
参考资料
- 浮点数进制转换(讲的很详细,强烈建议看完)
- 浮点数(float或double)10进制和2进制之间转换的工具