CTF 中的随机数问题
随机数问题
前言
在 CTF 比赛中,有些时候可能会考一些关于 随机数的问题,其实考察的是两个函数,即 mt_rand()
和 mt_srand()
。
前者用于生成随机数,后者则用于给随机数发生器播种。
函数的认识
mt_rand()
mt_rand — 通过梅森旋转(Mersenne Twister)随机数生成器生成随机值
参数 | 作用 |
---|---|
min | 可选的、返回的最小值(默认:0) |
max | 可选的、返回的最大值(默认:mt_getrandmax()) |
具体参考:https://www.php.net/manual/zh/function.mt-rand.php
示例:
1 |
|
程序运行结果:
可以看到,每次程序运行得到的结果都不一样,那么有没有方法,让其每次运行得到的结果一样呢?答案是有的,那就是通过随机数种子,计算机本身无法实现真正的随机,而是通过某个种子进行计算之后得到的所谓伪随机值。
在 PHP 中使用的种子函数为:mt_srand()
。
mt_srand()
mt_srand — 播下一个更好的随机数发生器种子
参数 | 作用 |
---|---|
seed | 用线性同余生成器生成的值填充状态,该生成器使用解释为无符号 32 位整数的 seed 进行播种。如果省略 seed 或为 **null **,则将使用随机无符号 32 位整数。 |
other | 其他参数省略 |
详细请参考:https://www.php.net/manual/zh/function.mt-sran
示例:
1 |
|
程序运行结果:
由于种子为 1234
,每次程序运行使用相同的算法对 1234
进行运算。故而每次程序运行的结果都是一样的。
结论:
PHP 版本相近、种子一致,得到的随机数也一致。
例题-1
以下例题来自:Ctfshow web入门 暴破 web24
1 |
|
mt_srand()
函数的作用是播下一个更好的随机数发生器种子,当有了随机数种子的时候,那么每次运行得到的随机数也是固定的。
比如种子 372619038
得到固定随机数为 1155388967
(当然可能PHP的版本有所差异,题目是PHP7.3的版本)
这里我们本地使用 PHP7.2 进行测试得到的随机数为 1155388967
,测试代码如下:
1 |
|
例题-2
以下例题来自:Ctfshow web入门 暴破 web25
1 |
|
源码解读如下:
由于种子是由 FLAG 的md5值,截取前8位转为十进制生成的,我们并不知道随机数种子是多少,因此这里需要使用php_mt_seed
进行种子爆破。
首先我们先传入?r=0
会得到一个负的随机数
1 | # 当我们传入 ?r=0 的时候,此时获得的随机数 192346317 |
我们通过生成的随机数暴破出随机数种子,这里需要使用到 php_mt_seed
工具。
工具的安装地址:https://github.com/openwall/php_mt_seed
通过在服务器的响应头中发现服务器的PHP版本为:php7.3
接着使用 php_mt_seed
工具根据第一次生成的随机数枚举出 种子,我这里第一次生成的随机数为 192346317
,
由于服务器的PHP版本为 php7.3 因此我们这里得到的种子十进制为 1142351452
,十六进制为 0x4416e65c
,这里得到了随机数的种子。
这里我们准一个 PHP7+ 的环境,运行如下代码(模拟服务器生成随机数):
1 |
|
以上源码运行之后得到 2274592836
,因此题目的mt_rand()+mt_rand()
的结果为 2274592836
。
题目源码:
1 | if ($_COOKIE['token'] == (mt_rand() + mt_rand())) { |
添加Cookie属性 token
字段值为:2274592836
,并传入参数r
其值为 192346317
这里为什么传入r=192346317
呢,观察如下代码:
1 |
|
例题-3
以下例题来自 GWCTF 2019 web 枯燥的抽奖 题目地址
DockerFile 项目地址:https://github.com/gwht/2019GWCTF/
访问题目出现如下页面:
这里我们随便输入,点击提交,接着页面返回 “没抽中哦,再试试吧”。
这里有个 小细节,点击提交,页面并没有刷新,这种情况只有两种可能,第一种 纯前端操作 第二种 通过 Ajax 发送请求。
我们右键查看前端代码:
1 | $(document).ready(function() { |
通过查看 JS 代码可以发现,这里是显然是通过 Ajax 发送 POST 请求到 check.php 文件。其传参值为:num=猜数字输入的值。
直接访问 check.php 文件,得到文件源码。
对关键代码进行审计:
1 | session_start(); |
综上所述,我们知道了前10位,取到的随机值为 bF01Kk2aBz,那么我们编写脚本,获取每次生成的随机值:
1 | str1 = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
但是这种数据,php_mt_seed 是不认识的,故而我们需要将其转为 php_mt_seed 能识别的随机数数据:
1 | str1 = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
运行得到如下数据:
1 | 1 1 0 61 41 41 0 61 26 26 0 61 27 27 0 61 46 46 0 61 10 10 0 61 28 28 0 61 0 0 0 61 37 37 0 61 25 25 0 61 |
将其使用 php_mt_seed 暴破出随机种子即可。
最终暴破出了 服务器的 PHP 版本为 7.1.0 +,其随机种子为:7937658,最后使用相同的代码,即可生成正确的字符串。
1 |
|
运行得到:bF01Kk2aBzArx0IjGi2a,传入即可得到 FLAG。
例题-4
考点:无需暴破还原 mt_rand()
种子
题目源码如下:
1 |
|
根据代码的功能可以发现,种子由 rand()
函数随机生成存储到$_SESSION['key']
,并且每3秒进行更新,重新生成。
以下代码的作用,就是生成 N 个 的随机数
1 | if (isset($_GET['num']) && is_numeric($_GET['num'])) { |
那么虽然我们知道其随机数,理论上来讲可以通过 php_mt_seed
工具进行暴破(**但是由于 php_mt_seed 是通过第一次 mt_rand() 生成的随机值进行暴破随机 **),而本道题进行了两次 mt_rand()
才输出了第三次随机的随机数。如下:
因此我们拿到的第一个值,则是第三次随机的数,又因为每3秒刷新一次,使用 php_mt_seed
暴破则需要时间,故而该方法不可行。
这里的知识点涉及到:无需暴破还原 mt_srand() 种子
直接来看详细请移步文章:
1、从mt_rand()
获取2个值:R000
以及R227
,中间间隔226
个值;此外还需知道R000
之前已生成的值的个数(用i
表示);
2、根据这些值算出加扰状态值;
3、异或这些状态值,推测出原始的状态值(s228
);
4、根据s228
,推测出s0
,获取种子。
无需暴破还原 mt_srand() 种子脚本:https://github.com/ambionics/mt_rand-reverse
接着我们使用 reverse_mt_rand.py
进行计算种子:
1 | python3 reverse_mt_rand.py 824793744 1480482850 123 1 |
最终计算出种子为 11570033
。知道用法之后,我们来看题。
先传入 ?num=228
生成 228 个随机数:
接着取第一个作为 R000
最后一个 作为 R227
中间间隔为 226,刚好 228 个。最后使用脚本进行计算:
最终得到种子为:1909862726,其中的 2 表示在 1070335843
产生之前,已生成随机值的个数,可以从下图中看到:
通过响应头,得到 PHP 版本为,因此拼接为 0。
但是由于,每秒刷新一次随机种子,故而需要编写脚本:
1 | import requests |
脚本运行结果:
最终得到 FLAG。