浮点数的存储方式和ctf实战
1、(1)数字分两种:整数和小数。之前介绍了整数溢出,本文介绍小数(浮点数)的存储和表示方法;整数的表示方法很简单:按照一定的计算方式转成二进制即可,比如10进制的9转成二进制1001,内存中最小存储单元是字节,也就是8bit;如果用1byte存储9,那么转成二进制就是00001001,这个应该不难理解;那么小数或浮点数该怎么表示和存储在内存了?
(2)举个例子:6.625这个浮点数,整数部分是6,小数部分是0.625,要想存储在内存,必须先转成二进制的形式;整数部分和小数部分转换的方式刚好相反,如下:
(2.1)整数部分6:这个很常见,每次除以2,用商继续除以2,直到商为0为止。6的二进制表示就是110;
(2.2)小数部分0.625:这个该怎么表示成二进制了? 整数部分是除以2,小数部分刚好反着,就是乘以2,计算过程如下:小数部分就是101
(2.3)6.625用二进制小数“表示”就是:110.101;怎么验证110.101转成10进制后就是6.625了?整数部分很好验证:2^2+2^1+0*2^0=6,小数部分怎么验证了?形式和整数是一样的!
1*2^(-1)+0*2^(-2)+1*2^(-3)=0.5+0+1/8 = 0.625
通过上述互相转换验证了小数部分转成二进制方式是正确的!
(3)内存从硬件上看都是无数个小电容构成的。电容充电就是1,放电就是0;所以内存中所有的数都是0101这种二进制串,是没法直接表示小数点的,这种情况只能继续通过人为抽象存储格式来存浮点数了。目前国际通用的浮点数存储规则是IEEE754,浮点数存储格式如下:
按照上面的标准,110.101 = 1.10101*2^2( 二进制小数点左移两位,相当于翻2^2倍,和位运算中的移位是一样的!) ,此时进一步抽象出了几个关键的“描述符”:s=0,M=1.10101,E=2;到了这里上述问题还是存在:内存硬件上只能存01二进制串,怎么存(表)储(示)小数点了?
(4)第(3)步抽象出s、M、E这些“描述符”后,利用这些信息是不是已经完全能够还原110.101这个浮点数了?答案是肯定的!套用上述的公式,能得到1.10101*2^2,根据这个进一步还原得到110.101;
所以在内存中存储浮点数,等价于在内存中存储s、M、E这些关键信息;
(5)s、M、E这些关键信息又是按照什么标准或方式在内存中存放的了?格式如下,以4字节的float为例: 最高位31位放s,23~30这8bit存放E,剩下23bit存放M;
几点需要注意:
- E是指数,所以有可能是负数,这8bit是怎么表示负指数的了?这里就和有符号数表示有本质区别了。有符号数最高位1表示负数,0表示整数,但这里不这么干,而是指数+127;比如这里的指数是2,那么E=127+2=129=1000 0001(所以这里指数最大不能超过2^127-1,表示浮点数足够了!);如果指数是负数,比如-5,那么E=127-5=122=111 1010;
- M:所有M中小数点左边都是1,所以这1位可以省略,直接存剩下的位数;
- M:只有23位,如果10进制的小数部分乘以2始终不为1,那么只取23位,其余的舍弃,所以这是部分浮点数有误差、无法精确表示的根本原因!最低为舍弃时如果是1就不能直接舍弃,还要+1;
所以6.625这个浮点数经过一系列转换后,最终在内存中的存储形式如下:
cpu从内存中读取这4字节数据后,按照上面的规则反过来计算出10进制的浮点数!
2、这里来看一道2018护网杯CTF题目:getting_start;题目的代码是酱紫的:
这里需要执行system("bin/sh")这行代码,也就是说上面if条件不能成立;这里v7已经相等了,只能看v8了;v8已经有了初始值,怎么才能让v8=0.1了、让if条件不成立了?
这里能接受用户输入的地方有read,数据保存在buf中,所以这里只能通过控制buf的值来改变栈上的数据,当然也包括v8了!老规矩,先画个栈图,能直观理解:
buf距离v8有32字节,而read会读取0x28=40字节,所以如果buf全部读取40字节,连下面的v9都能覆盖!不过这里v9和本题无关,暂时放过它!buf、v5、v6随便写,那么前24字节随便写,这里用“a”替代;由于v7要保持原值,所以第25~28字节是0x7FFFFFFFFFFFFFFF;接下来就是最重点的部分了:29~32字节、也就是v8怎么填写二进制数据,才能让这里的值等于0.1?这里先偷个懒:下面参考这里链接有个工具,可以直接把double类型的小数转成二进制形式,如下:
那么最后4字节就是0x3FB999999999999A,所以最终的payload=b"a"*24+ p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A);
3、站在学习的角度,这里从头开始手搓一下0.1在内存中的存放形式;
(1)0.1是10进制,先转成小数二进制的形式;按照之前的方法,每个步骤如下:
这里出现了一个有趣的现象:从第6步开始出现了循环,所以没必要再挨个计算了,这里直接写出二进制的形式:0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011.....
(2)二进制的形式有了,现在要提取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
(3)再按照标准把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;
参考:
1、https://www.bilibili.com/video/BV1j7411H7Fc?p=8 浮点数进制转换(讲的很详细,强烈建议看完)
2、http://www.binaryconvert.com/result_double.html?decimal=048046049 非常好用的浮点数(float或double)10进制和2进制之间转换的工具