★pwn random随机数保姆级教程★

★pwn random随机数保姆级教程★

🏀前言

本文章为glibc中random随机数的详解,讲解了随机数的诞生。学完本文章,你将会有以下收获:加深对random随机数的理解,略微提升计算机组成原理的水平。

⚽️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作为种子,得出来的序列如下图:
在这里插入图片描述
如果你也是用1作为种子,那么得出来的序列和我这个会是一样的,这就是伪随机。
通常情况下,会采用“当前时间”作为种子
在这里插入图片描述
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]%d\n", i, r[i]);
	}
}

首先是运行这样一段代码,定义一个r[]数组,将种子传给r[0],然后r[1] = 16807LL * r[0],我这里种子是1,所以就是16807LL * 1,然后再跟2147483647取余。下面的if判断是给种子为负数的情况用的,这里不用管。看一下运行效果:
在这里插入图片描述
如果将16807LL改成16807会怎么样呢?
在这里插入图片描述
前两个相同,后面的就不同了。写一个程序看看为什么
在这里插入图片描述
在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]%d\n", i, r[i]);
	}
	for (i = 31; i < 34; i++) {
		r[i] = r[i - 31];
		printf("r[%d]%d\n", i, r[i]);
	}
}

随后的31-33是这样的代码,将0-2复制了一下
在这里插入图片描述

随后就是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]%d\n", i, r[i]);
	}
	for (i = 31; i < 34; i++) {
		r[i] = r[i - 31];
		printf("r[%d]%d\n", i, r[i]);
	}
	for (i = 34; i < 344; i++) {
		r[i] = r[i - 31] + r[i - 3];
		printf("r[%d]%d\n", 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
在这里插入图片描述
最后就是完整的代码,从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]%d\n", i - 344, (unsigned int)r[i] >> 1);
	}
}

在这里插入图片描述
熟悉的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

这里讲另一题:
百度网盘(提取码yxxx)

在这里插入图片描述
checksec查看。64位,开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移。
在这里插入图片描述
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的时候这一位就被抹去了。

在这里插入图片描述
这里打印出的都是真实值
在这里插入图片描述

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]:
在这里插入图片描述

可以看到这里其实与运算没有太太作用了,主要还是防止最高位进位的情况,同时幸运的是,最低位也没有发生进位,因此我们算的打印值和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 = [libc.rand() for _ in range(11000)]

success(f'random =>> {a}\n')

j, k = 0, 0

for i in range(10000):
    idx_1, idx_2, idx_3 = i + 31, i, i + 28
    result = (int(a[idx_2]) + int(a[idx_3])) & 0x7FFFFFFF

    success(f'random[{idx_1}], [{idx_2}], [{idx_3}] is =>> {a[idx_1]}, {a[idx_2]}, {a[idx_3]}')
    success(f'random[{idx_1}] should be =>> ({int(a[idx_2])} + {int(a[idx_3])}) & 0x7FFFFFFF')
    success(f'random[{idx_1}]\'s result is =>> {result}')

    if result == a[idx_1]:
        success("That's right !!!")
        j += 1
    else:
        k += 1

    print(f'true:false = {j}:{k}')

在这里插入图片描述
发现正确与错误的比例确实是3:1

  • 49
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YX-hueimie

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值