0%

PHP中的精确度问题

好几次做题遇到了这种利用数字的精确度来绕过的。但是还是时常忘记这个方法,这里记录下加深一下印象。

理论

 在PHP中进行浮点数的运算时,经常会出现一些和预期结果不一样的值,这是由于浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。
 例如我们测试一下 floor((0.1+0.7)*10); 我们预期是输出8,可是实际结果是 7。这也证明了,以十进制可以精准表示的数,无论有多少尾数都不能被内部所使用的二进制精确表示,就是说不能在不丢失一点点精度的情况下转换为二进制的格式。
我们做的如下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$a=0.10000000000001; //14位小数 注意注意如果前面的整数部分不为0,则会只精确到13为小数
$b=0.100000000000001;
$c=1.10000000000001;
$d=0.100000000000005;
echo $a."\n";
echo $b."\n";
echo $c."\n";
echo $d."\n";
------------------------------
0.10000000000001
0.1
1.1
0.10000000000001

这里我们得出结论:
当浮点型数据转化为字符型数据时(这里的echo就会使它转为str型),如果整数部分为0的话就有14位有效小数且是四舍五入(15位>=5,14位进一)。当整数部分为非0时,这里有效小数位数就为13位,同时也是四舍五入。这里也需要注意下,转为字符串时如果后面全是0的话也会被省略,只保留一个小数位的0
我们继续我们的测试,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
$a=0.10000000000000002; //17位小数,
$b=0.10000000000000001; //
$c=0.100000000000000002; //18位
$d=0.100000000000000003;

echo $a."\n";
echo $b."\n";

if($a===$b)
echo "this is eq\n";
else
echo "this is nq\n";

echo $c."\n";
echo $d."\n";

if($c===$d)
echo "this is eq\n";
else
echo "this is nq\n";
----------------------------
0.1
0.1
this is nq
0.1
0.1
this is eq

这里我们可以看出,整数部分为0时 浮点逻辑运算时,会比较其17位小数 。 同时测试发现,如果整数部分非0会比较其16位小数。具体原因不太清楚应该是算法问题,如下有个奇怪的问题:

1
2
3
4
5
6
7
if(0.100000000000000000000000000000000000000000000000000000000000==0.10000000000000001)
echo "true";
//但是下面却不相等
if(0.100000000000000010000000000000000000000000000000000000000000==0.10000000000000002)
echo "true";
else
echo "false";

这边,以后使用的时候就尽量不要使用太长吧,实在需要就现场测试。

结论

所以说了这么多,我们应该重视的点是,浮点数当字符串处理时和进行逻辑运算时所保留的位数不同,这就及其可能造成漏洞点。
当字符串: 当整数部分为0时,保留14位有效小数且四舍五入。当整数部分不为0时,保留13位有效小数且四舍五入。
进行逻辑运算: 当整数部分为0时,保留17位有效小数。当整数部分不为0时,保留16位有效小数且四舍五入。
也就是说,我们赋值的时候可以赋值为整数部分为0的2个带有15位小数且最后一位不相等的数。来绕过类似逻辑:

1
2
if(md5($a)===md5($b) && $a!==$b)
echo "success!";

但然利用的payload只要通过上面的学习应该是可以千变万化的。
注意,在小数位的前4位没有赋值一个非0数,则会进行科学计数法。

而外补充整数

1
2
3
4
5
6
7
<?php
$a=1234567891234567391; // 15位开始科学计数法表示,也就是说整数其实也是只精确到17位(并不是15位哟),后面的直接忽略。
$b=1234567891234567362;
if($a===$b)
echo "Come";
----------------
Come

15位开始科学计数法表示,也就是说整数其实也是只精确到17位(并不是15位哟),后面的直接忽略。

实操

Test 1

1
2
3
4
5
<?php
$a=$_GET[a];
if(intval($a)===520)
if( strpos($a,"2"))
echo "give you flag";

这题我们怎么绕过呢?除了编码绕过外,我们也可以利用今天的知识,精准度问题来进行绕过:
payload:?a=519.9999999999999999999

Test 2

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
$flag = 'flag{1S_numer1c_Not_S4fe}';
$id = $_GET['id'];
is_numeric($id)?die("Sorry...."):NULL;
if($id>665){
echo $flag;
}
?>

这题就简单了,payload:?id=666mayi

test 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
echo "nctf{*****************}";
} else {
echo "false!!!";
}
}
else{
echo "please input a";
}
?>

这题我们首先发现,md5(‘QNKCDZO’)它的值是一个0e开头的且后面没有其它字母,那么它就是把其当作科学计数法来进行计算了,0 * 10^n 都是为0,所以我们只需要md5值找到0e开头的就可如:

1
2
3
4
5
QNKCDZO	    0e830400451993494058024219903391
240610708 0e462097431906509019562988736854
aabg7XSs 0e087386482136013740957780965295
aabC9RqS 0e041022518165728065344349536299
s878926199a 0e54599327451770903432885584102

payload: ?a=240610708

Test 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
show_source(__FILE__);
$flag = "xxxx";
if(isset($_GET['time'])){
if(!is_numeric($_GET['time'])){
echo 'The time must be number.';
}else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
echo 'This time is too short.';
}else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
echo 'This time is too long.';
}else{
sleep((int)$_GET['time']);
echo $flag;
}
}
?>

题目要求我们get传入一个time且它在(5184000,7776000)之间,随后又进行这么长的时间睡眠。这里注意有个 int 强制类型转换 ,我们利用上面说道的知识可以利用16进制或者科学计数法进行绕过。payload如:

1
2
?time=6e6  //需要我们等6秒
?time=0x4f1a01

Test 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);

这题咋整?以前用过异常类来绕过,但是这边限制了长度,就是说异常类和硬碰撞都没用了。
这时就有我们今天讲的精准度的用武之地了。记住函数调用时基本是将其先转换为str类型,当然具体情况具体分析。
我们直接看图吧,经过上面的学习应该是好理解的:


-------------本文结束感谢您的阅读-------------