pwn liunx的glibc中的random随机数详解

admin 2023年12月21日07:49:16评论20 views字数 5491阅读18分18秒阅读模式


⚽️glibc

要了解这个函数首先需要知道什么是glibc。lib是library的缩写,意思是“库”,libc在后面加了个c,意思是“标准C语言库”,这个库是用来干什么的呢?为什么需要这个库呢?

在我们编写C语言的时候,经常会用到一个函数printf(),而我们都知道C语言中函数必须先声明再调用,但是我们并没有创建printf()这个函数,那么我们是怎么调用这个函数的呢?这就需要用到库了。

printf()这个函数很复杂,有上千行汇编代码,如果是给你复制粘贴的话会消耗大量的代码,使得程序很难看,所以C语言的开发者们就想了一个办法解决,就是将这个函数的代码放到一个库中,然后让你在运行程序的时候链接上这个库,然后就可以使用printf()这个函数了。

glibc的意思是GNU libc,是GNU旗下的C标准库,后来被liunx运用了,因此在liunx中自带的libc就是glibc,这个libc和windows上的是不同的。

⚽️random

要想使用random,首先要种下一颗“种子”,需要利用函数srand,意思是seed rand,不同的种子,得出来的random随机数的序列会不一样,我这里用1作为种子,得出来的序列如下图:

pwn liunx的glibc中的random随机数详解

如果你也是用1作为种子,那么得出来的序列和我这个会是一样的,这就是伪随机。通常情况下,会采用“当前时间”作为种子

pwn liunx的glibc中的random随机数详解

time()函数的作用是获取当前的系统时间距离1970年1月1日00:00过了多少秒,然后作为种子开启random,因为时间每秒都不同,所以就做到了每秒随机,如果你在一秒内连续运行这个程序,会发现随机数是一样的。

这里再用1作为种子,接下来说明1804289383这个数是怎么得来的

#include <stdio.h>
#define seed 1
void main() { int r[500]; int i;
r[0] = seed; for (i = 1; i < 31; i++) { r[i] = (16807LL * r[i - 1]) % 2147483647; if (r[i] < 0) { r[i] += 2147483647; } printf("r[%d]%dn", i, r[i]); }}


首先是运行这样一段代码,定义一个r[]数组,将种子传给r[0],然后r[1] = 16807LL * r[0],我这里种子是1,所以就是16807LL * 1,然后再跟2147483647取余。下面的if判断是给种子为负数的情况用的,这里不用管。看一下运行效果:

pwn liunx的glibc中的random随机数详解

如果将16807LL改成16807会怎么样呢?

pwn liunx的glibc中的random随机数详解

前两个相同,后面的就不同了。写一个程序看看为什么

pwn liunx的glibc中的random随机数详解

在a的计算中,r[2]和16807都是int类型,所以最后的结果也会是int类型,而b的计算中,由于16807是long long int类型,r[2]会小转大,也会变成long long int类型,所以最后的结果是long long int类型,最后这两个不一样的数取余2147483647就会不一样。

#include <stdio.h>
#define seed 1
void main() { int r[500]; int i;
r[0] = seed; for (i = 1; i < 31; i++) { r[i] = (16807LL * r[i - 1]) % 2147483647; if (r[i] < 0) { r[i] += 2147483647; } printf("r[%d]%dn", i, r[i]); } for (i = 31; i < 34; i++) { r[i] = r[i - 31]; printf("r[%d]%dn", i, r[i]); }}

随后的31-33是这样的代码,将0-2复制了一下

pwn liunx的glibc中的random随机数详解

随后就是34-344了,是这样的代码:

#include <stdio.h>
#define seed 1
void main() { int r[500]; int i;
r[0] = seed; for (i = 1; i < 31; i++) { r[i] = (16807LL * r[i - 1]) % 2147483647; if (r[i] < 0) { r[i] += 2147483647; } printf("r[%d]%dn", i, r[i]); } for (i = 31; i < 34; i++) { r[i] = r[i - 31]; printf("r[%d]%dn", i, r[i]); } for (i = 34; i < 344; i++) { r[i] = r[i - 31] + r[i - 3]; printf("r[%d]%dn", i, r[i]); }}

例如

r[34] = r[3] + r[31] = 1622650073+ 1 = 1622650074

r[39] = r[8] + r[36] = 1457850878 + 1426584179 = 2884435057,符号位为1,为负数,将这个数先转换为二进制来看:2884435057= 10101011111011001111110001110001,因为计算机中存放的是补码(这里就不细讲补码了,属于计算机组成原理的内容,与主题偏的有些大),所以除了符号位的1不变以外,其他31位取反再加1,变成11010100000100110000001110001111,拿掉符号位,变成1010100000100110000001110001111 = 1410532239,加上符号位就是-1410532239

pwn liunx的glibc中的random随机数详解

最后就是完整的代码,从344到无穷大,这里为了观看方便就只设置到380

#include <stdio.h>
#define seed 1
void main() { int r[500]; int i;
r[0] = seed; for (i = 1; i < 31; i++) { r[i] = (16807LL * r[i - 1]) % 2147483647; if (r[i] < 0) { r[i] += 2147483647; } } for (i = 31; i < 34; i++) { r[i] = r[i - 31]; } for (i = 34; i < 344; i++) { r[i] = r[i - 31] + r[i - 3]; } for (i = 344; i < 380; i++) { r[i] = r[i - 31] + r[i - 3]; printf("r[%d]%dn", i - 344, (unsigned int)r[i] >> 1); }}

pwn liunx的glibc中的random随机数详解

熟悉的1804289383这个“随机数”就是这样诞生的,观看代码可以发现,最后输出的是r[i]向右移了一个bit,这里演示一下计算流程:r[344] = r[313] + r[341] = 1040273694 + (-1726662223) ,将-1726662223转换成补码计算,-1726662223 = 11100110111010101100011001001111,减1再取反得(标准是取反再加1,都是一样的)10011001000101010011100110110001(符号位不动) = 2568305073,1040273694 + 2568305073 = 3608578767 = 11010111000101101000101011001111 >> 1 = 01101011100010110100010101100111 = 1804289383

最后向右移1bit导致符号位必定会补成0,因此可以直接用%d输出不会出现负号。

同时,最后用到了(unsigned int)将r[]类型转换为无符号整形,如果不转化,右移的时候是这样的:11010111000101101000101011001111 >> 1 = 11101011100010110100010101100111(第一位补符号位),去掉符号位为1101011100010110100010101100111,这是补码,显示给你看的是原码,转换成原码为0010100011101001011101010011001,加上符号位为10010100011101001011101010011001 = -343194265

以上就是random随机数的全部原理,建议读者还是要自行去尝试跑一遍,会有更深刻的印象。

⚽️例题

这是之前写的一个wp,是一道基础random题:NewStarCTF 2023 WEEK1 random

这里讲另一题:

pwn liunx的glibc中的random随机数详解

checksec查看。64位,开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移。

pwn liunx的glibc中的random随机数详解

IDA查看。这里不演示进入vulfunc函数后发生的事,仅将如何进入vulfunc函数。首先是打开了liunx自带的随机数库,从里面拿了一个随机数作为种子,这个urandom的计算非常复杂,很难破解,因此无法知道种子是什么。随后进入循环,让你输入一个数,然后调用random,如果输入的数与random相同,则成功。如果不同,则他会把本次random的数告诉你,然后重新开始输入。

根据上面对于random函数的分析可以得知,r[i] = r[i - 31] + r[i - 3],也就是说r[31] = r[0] + r[28],失败后可以接收random的数值,也就是可以知道r[0]和r[28]的值,然后就可以预测出r[31]的值,随后在第32次random的时候输入即可。

但是,将获得的值相加并不一定就真的是r[31]的值,因为只能接收无符号整数,他在相加的时候却是用有符号整数运算的。举一个例子:

假设此时第一个获取的random值为1804289383,而这是打印出来的值,实际上在r[0]中的值是-686388529,打印的计算过程上面已经演示过一遍了,现在要逆向一遍。

1804289383 = 01101011100010110100010101100111 << 1 = 1101011100010110100010101100111? ,可以看到最后一位我标了一个?,因为向右移可以自动在首位补0,但是向左移的时候并不知道最后一位是1还是0,在-686388529转变到1804289383的时候这一位就被抹去了。

pwn liunx的glibc中的random随机数详解

这里打印出的都是真实值

pwn liunx的glibc中的random随机数详解

3608578767是补码,然后和后面的那个数的补码相加后得到一个值,由于r[]是int类型,超过32位的去掉。然后右移一位,打印出打印值。然后下面是通过题目能够获取到的打印值,将两个打印值相加,相加再进行与运算,仍然可以得到上面的打印值。

什么时候会不准确呢?看我鼠标标记的地方,当第一个数的最后1bit为1,第二个数也为1时,相加后,就会影响到真实值的倒数第2个bit,第二个bit会由于进位变成1(如果原来是0的话),右移后,就是打印值的第一个bit,但是用获取的打印值计算,无法得知后面是否发生了进位,如果原来是0,那么还是0,不会进为1。这就导致了值不同。很容易可以看出,这个不同的概率是1/4

&0x7FFFFFFF是因为,如果在用真实值计算时发生了最高位进位后,转换为int类型会少掉最高位的1,变成32位,然后再右移一位,变成31位,所以打印值的最终结果一定是一个31位的值,像上述的计算中打印值就成了32位,这反向说明了在用真实值计算时就发生了进位。因此需要与运算将其变成31位,下面给了一个在用真实值计算时最高位没有发生进位的例子,用的是r[36] = r[5] + r[33]:

pwn liunx的glibc中的random随机数详解

可以看到这里其实与运算没有太太作用了,主要还是防止最高位进位的情况,同时幸运的是,最低位也没有发生进位,因此我们算的打印值和random随机出的值是一样的(不要忘记了,random随机出来,显示给你看的值就是上面的打印值,下面的是用打印值计算出来的值,由于最低位可能进位所以有1/4的概率不等于打印值)。

掌握了原理后,写一个脚本,先错31次,接收r[0]和r[28],然后计算出r[31],第32次输入的时候填入就可以了,有3/4的概率成功,如果不成功,那就算r[32],总会成功的。

测试一下理论是否正确:

from pwn import *from ctypes import *
libc = cdll.LoadLibrary('libc.so.6')
libc.srand(libc.time())
a = []j = 0k = 0
for i in range(11000): a.append(libc.rand())
success('random =>> ' + str(a))print('n')
for i in range(10000): success('random[' + str(i + 31) + '], ' + '[' + str(i) + '], ' + '[' + str(i + 28) + '] is =>> ' + str(a[i + 31]) + ', ' + str(a[i]) + ', ' + str(a[i + 28])) success('random[' + str(i + 31) + ']' + ' should be =>> ' + '(' + str(int(a[i])) + ' + ' + str(int(a[i + 28])) + ')' + ' & 0x7FFFFFFF') success('random[' + str(i + 31) + ']' + ''s result is =>> ' + str(int(a[i] + a[i + 28]) & 0x7FFFFFFF)) if (int(a[i] + a[i + 28]) & 0x7FFFFFFF) == a[i + 31]: success('That 's right !!!') j += 1 else : k += 1 print('ture:false = ' + str(j) + ':' + str(k))

pwn liunx的glibc中的random随机数详解

跑多次后,发现正确和错误的比例确实接近3:1。

原文标题:★pwn liunx的glibc中的random随机数详解★-CSDN博客

作者:UKFC安全——YX-hueimie

如果还有什么问题的话欢迎提问,有缺漏错误欢迎指出,感谢阅读。

原文始发于微信公众号(UKFC安全):pwn liunx的glibc中的random随机数详解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月21日07:49:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   pwn liunx的glibc中的random随机数详解http://cn-sec.com/archives/2320019.html

发表评论

匿名网友 填写信息