一、伪随机数介绍:
伪随机数是通过算法生成的数字序列,与真正的随机数相比,它们具有确定性的特点。这意味着,给定相同的初始种子,伪随机数生成器(PRNG)每次都会产生相同的数字序列。尽管这些数字看起来是随机的,但它们实际上是由初始种子和算法共同决定的。
1.伪随机数的运行机制:
假设有a1=f(seed)
,an+1=f(an)
,那么就可以得到一个序列a1,a2,a3...an
制作一个伪随机数也就是让其每次返回序列的下一个元素,如图:
对于java.utiol.Random
,比较老的C语言的rand()
库,和一部分php的rand()
,它们使用的就是这种线性的随机数方法。这种方法一开始一般会取一个48bit的值作为seed直接放到state0里,而每个新的state的产生方式则为: next_state=(state * multiplier + addend) mod (2 ^ precision)
(state为老的state,后面三个为固定的常量(multiplier
=25214903917,addend
= 11,precision
= 48))。而从state计算到output的过程如下图:
output:为原数据右移16位之后的32比特的数据
2.C语言中的随机数函数:(rand,srand)
rand和srand是用于产生伪随机数的两个函数,其返回值为[0,RAND_MAX]之间的数据
1.rand函数:
rand()函数是使用线性同余法做的,它并不是真的随机数,因为其周期特别长,所以在一定范围内可以看成随机的。
int rand(void)
头文件: stdlib.h
用户未设定随机数种子时,系统默认的随机数种子为1。
rand()产生的是伪随机数字,每次执行时是相同的;若要不同,用函数srand()初始化它。
2.srand函数:
srand()为初始化随机数发生器,用于设置rand()产生随机数时的种子
void srand(unsigned int seed)
头文件: stdlib.h
功能:初始化随机数发生器
srand()用来设置rand()产生随机数时的随机数种子。
参数seed必须是个整数,如果每次seed都设相同值,rand()所产生的随机数值每次就会一样。
tips:一般为了srand值相同导致的产生随机数值相同,随机数种子考虑使用时间(srand(time(NULL))
)
二、关于随机数的攻击方式:
对于随机的攻击来说可以分为两种情况考虑,一种是可以通过逆向算法来获取到随机数的情况,另一种是只能得到生成的随机数,需要想办法逆推出种子的情况。
情况一:根据算法推出种子
这种情况基本就是看它的随机数是怎么得出来的,目前见过的都是建立方程组然后用z3约束器求解
实例分析:前期信息探查
1.EXEinfo:
64位,无壳,ELF文件
2.运行一下:
IDA分析:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int i; // [rsp+Ch] [rbp-24h]
int j; // [rsp+10h] [rbp-20h]
unsigned int seed; // [rsp+14h] [rbp-1Ch]
int v7; // [rsp+18h] [rbp-18h]
int v8; // [rsp+1Ch] [rbp-14h]
void *v9; // [rsp+20h] [rbp-10h]
void *v10; // [rsp+28h] [rbp-8h]
v9 = malloc(0x3E8uLL);
v10 = malloc(0x3E8uLL);
seed = time(0LL);
srand(seed);
puts("Welcome to my reverse!!!\\n");
printf("Waiting");
fflush(stdout);
for ( i = 0; i <= 29; ++i )
{
v7 = rand();
sleep(v7 % (4 * i + 2));
putchar('-');
fflush(stdout); // 函数作用为清空缓冲区,这个参数的意思是键盘输入
}
putchar(10);
printf("username:");
__isoc99_scanf("%s", v9);
printf("password:");
__isoc99_scanf("%s", v10);
printf("Checking");
fflush(stdout);
for ( j = 0; j <= 29; ++j )
{
v8 = rand();
sleep(v8 % (2 * j + 1));
putchar(45);
fflush(stdout);
}
putchar(10);
sub_4008C1(v9);
sub_400901(v9);
sub_4009B2(v9);
sub_400A33(v9, v10);
return sub_400C23(v9, v10);
主函数接收了两个值,一个username,一个password,分别被40~44行的五个函数作为参数,而剩下的两个循环没啥用,纯粹浪费分析时间。
1.第一个子函数分析
__int64 __fastcall sub_4008C1(__int64 username)
{
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= 100 && *(_BYTE *)(i + username); ++i );
return sub_400866((unsigned int)i);
}
这个子函数就一个循环,然后跳转到另一个子函数,循环的终止条件有两个,一个是i>100,一个是username的地址加i个偏移等于’\0’,所以i也就是username的串长。接着看sub_400866函数:
__int64 __fastcall sub_400866(int a1)
{
__int64 result; // rax
if ( 4 * (a1 >> 2) != a1 || 4 * (a1 >> 4) == a1 >> 2 || !(a1 >> 3) || (result = (unsigned int)(a1 >> 4), (_DWORD)result) )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
return result;
}
这个函数作用大概是要求串长满足某种条件的,想知道哪些串长满足如下条件也很简单,就是穷举1~100,代入公式就行
result = 0
for i in range(100):
result = (i >> 4)
if ( 4 * (i >> 2) != i or 4 * (i >> 4) == i >> 2 or not(i >> 3) or result):
continue
else :
print(i)
可以看到满足条件的长度只有8和12
第一个函数的作用到这就分析出来了,就是用来判断username串的串长
2.第二个函数分析:
__int64 __fastcall sub_400901(int *username)
{
__int64 result; // rax
__int64 v2; // [rsp+18h] [rbp-18h]
__int64 v3; // [rsp+20h] [rbp-10h]
__int64 v4; // [rsp+28h] [rbp-8h]
v2 = *username;
v3 = username[1];
v4 = username[2];
if ( v2 - v3 + v4 != 1885764216 || v3 + 2 * (v4 + v2) != 0x22F241C1FLL || (result = 0x31CD156AC3A69DC4LL, v3 * v4 != 0x31CD156AC3A69DC4LL) )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
return result;
}
result变量在这里啥用也没有,直接忽略。
username被分成三段,分别放入v2、v3、v4,这三个值分别满足if里的三个等式,也就是要解一个三元一次方程组
from z3 import *
x = Real('x') #v2
y = Real('y') #v3
z = Real('z') #v4
s = Solver()
s.add(x - y + z == 1885764216)
s.add(y + 2*(x + z) == 9380830239)
s.add(y * z == 3588548026377346500)
if s.check() == sat:
result = s.model()
print (result)
else:
print('no result')
将这些值转成十六进制:
v2 = 0x6D73 7469
v3 = 0x6F72 6265
v4 = 0x7265 6874
这里需要注意的是这些的值是以小端的形式存在内存中,重新组合转换成字符应为:
v2 = 0x6974736d -> itsm
v3 = 0x6562726f -> ebro
v4 = 0x74686572 -> ther
由此得出username的值为“itsmebrother”
3.第三个函数分析:
__int64 __fastcall sub_4009B2(__int64 a1)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; ; ++i )
{
result = *(unsigned __int8 *)(i + a1);
if ( !(_BYTE)result )
break;
if ( (*(char *)(i + a1) <= 96 || *(char *)(i + a1) > 122) && *(_BYTE *)(i + a1) != 95 )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
}
return result;
}
函数作用:判断输入的字符是否为小写字母和‘_’
4.第四个函数分析:
__int64 __fastcall sub_400A33(_DWORD *username, char *password)
{
__int64 result; // rax
int i; // [rsp+18h] [rbp-28h]
int v4; // [rsp+1Ch] [rbp-24h]
int v5; // [rsp+20h] [rbp-20h]
int v6; // [rsp+24h] [rbp-1Ch]
int v7; // [rsp+28h] [rbp-18h]
int v8; // [rsp+2Ch] [rbp-14h]
for ( i = 0; password[i]; ++i ) // 判断password是否由,大小写字母和数字组成
{
if ( (password[i] <= 96 || password[i] > 122)
&& (password[i] <= 64 || password[i] > 90)
&& (password[i] <= 47 || password[i] > 57) )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
}
srand(username[1] + *username + username[2]);
v4 = *(_DWORD *)password;
if ( v4 - rand() != 385125110 )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
v5 = *((_DWORD *)password + 1);
if ( v5 - rand() != 537582108 )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
v6 = *((_DWORD *)password + 2);
if ( v6 - rand() != 58160324 )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
v7 = *((_DWORD *)password + 3);
if ( v7 - rand() != 1606651445 )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
v8 = *((_DWORD *)password + 4);
result = (unsigned int)(v8 - rand());
if ( (_DWORD)result != 183286517 )
{
puts("Wrong username or password!!!\\n");
exit(0);
}
return result;
}
这里调用的是rand函数,paseword的值减去生成的随机数等于一个值,而srand设置的随机数种子是已知的利用这个特点我们就可以解出password的值。
先利用已知的随机数种子求出生成的随机数:(操作系统不同rand函数的处理方式可能有差别)
一开始是在Windows的环境下跑的结果解不出来:
这是个linux程序还是使用linux的gcc:
生成可执行文件会报错,说做加法的时候出现溢出情况,不过不影响,因为源程序也会溢出。
0x5664ad74
0x283e9d1a
0x6ee9f96c
0x36fc1ff
0x665b8a42
编写exp来获取password的值:
rpassword = []
rpassword.append(0x16F48AF6)
rpassword.append(0x200ADA1C)
rpassword.append(0x37774C4)
rpassword.append(0x5FC38E35)
rpassword.append(0xAECBAF5)
rand = []
rand.append(0x5664ad74)
rand.append(0x283e9d1a)
rand.append(0x6ee9f96c)
rand.append(0x36fc1ff)
rand.append(0x665b8a42)
password = []
for i in range(len(rpassword)):
password.append(rpassword[i] + rand[i])
print(hex(password[i]))
但这里还是要注意处理小端数的问题
import binascii
password = ["6d59386a","48497736","72616e30","63335034","71484537"]
def big_small_end_convert(data):
return binascii.hexlify(binascii.unhexlify(data)[::-1])
di = b'6d59386a'
do = big_small_end_convert(di)
print(do)
di = b'48497736'
do = big_small_end_convert(di)
print(do)
di = b'72616e30'
do = big_small_end_convert(di)
print(do)
di = b'63335034'
do = big_small_end_convert(di)
print(do)
di = b'71484537'
do = big_small_end_convert(di)
print(do)
输出得到正常password串(这个代码有点笨,以后还得改一改)
6a38596d36774948306e61723450336337454871
将获得的password转成字符
i = "6a38596d36774948306e61723450336337454871"
for q in range(0, len(i), 2):
flag += chr(int(i[q:q+2], 16))
#j8Ym6wIH0nar4P3c7EHq
至此用户名和密码就都搞到手了
username:itsmebrother
password:j8Ym6wIH0nar4P3c7EHq
5.第五个函数分析:
int __fastcall sub_400C23(__int64 username, __int64 password)
{
int i; // [rsp+1Ch] [rbp-4h]
printf("flag is:flag{");
for ( i = 0; i <= 19; ++i )
putchar(byte_602090[i] ^ *(_BYTE *)(i + password));
return puts("}");
}
flag的值为password异或byte_602090
里的值,将byte_602090
里的值提取出来就可以写exp了。
EXP为:
rflag = [0x06,0x57,0x3E,0x0A,0x53,0x13,0x16,0x21,0x5E,0x31,0x0C,0x0B,0x6B,0x22,0x56,0x15,0x52,0x37,0x3B,0x14]
password = "j8Ym6wIH0nar4P3c7EHq"
flag = ""
for i in range(len(rflag)):
flag+=chr(ord(password[i]) ^ rflag[i])
print(flag)
flag{logged_in_my_reverse}
情况二:根据生成的随机数推出种子
ATTACK1:
当我们能够获得两个连续的output时:
假设获得到的output为0和1,state0右移十六位得出out0,state1右移十六位得出out1,设state的右16位为x,可设立方程out0<<16+x=state0
,将这个带着x的state0做线性变换就可以得出state1,再将带着x的state1右移16位则得到了out1,。通过建立方程得出state0后就可以得出所有的state(这里可能会有疑问明明是一元一次方程为啥还需要知道state1,因为右移的操作可能会导致有多个x的值加上左移16位后的out0等于state0,为了确定是哪个所以需要state1)
Random random = new Random();
long v1 = random.nextlnt();
long v2 = random.nextlnt();
for (int i = 0;i < 65535;i++)
{
long state = v1 * 65535 + i;
if (((state * multiplier + addend)& mask) >>> 16) == v2{
System.out.println("Seed found:" + state);
break;
}
}
- multiplier = 25214903917
- addend = 11
- next_state = (state * multiplier + addend) & 0xFFFFFFFFFFFF
- outout = state >> 16
ATTACK1例题分析:
可以看到题目附件有两个:
先打开py文件:
class Unbuffered(object):
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
sys.stdout = Unbuffered(sys.stdout)
import signal
signal.alarm(600)
import os
os.chdir("/root/level1")
flag=open("flag","r").read()
import subprocess
o = subprocess.check_output(["java", "Main"])
tmp=[]
for i in o.split("\\n")[0:3]:
tmp.append(int(i.strip()))
v1=tmp[0] % 0xffffffff
v2=tmp[1] % 0xffffffff
v3=tmp[2] % 0xffffffff
print v1
print v2
v3_get=int(raw_input())
if v3_get==v3:
print flag
19行到22行可以看到调用了Java语言的一个main文件,从里面读出了三行,然后下面30行的判断则是如果接受的值等于v3则输出flag。
接着查看main文件
import java.util.Random;
public class Main {
public Main() {
}
public static void main(String[] var0) {
Random var1 = new Random();
System.out.println(var1.nextInt());
System.out.println(var1.nextInt());
System.out.println(var1.nextInt());
}
}
用到的是Java里面的random函数,既然已知两个连续的output则可以构造方程,来解出state的值
exp:
from zio import *
import time
import random
target=("192.168.157.13",5001) #部署的服务器ip及端口号
io=zio(target)
v1=int(io.readline().strip()) #读取output
v2=int(io.readline().strip())
def liner(seed):
return ((seed*25214903917+11) & 0xffffffffffff)
for i in range(0xffff+1):
seed=v1*65536+i
if liner(seed)>>16 == v2:
print seed
print liner(liner(seed))>>16
io.writeline(str(liner(liner(seed))>>16))
print io.readline()
ATTACK2:
Ruby’s rand( ),Python’s random module,PHP’s mt_rand( )
1.这类的随机数基本原理为:
-
state以一次624个int为一组生成的,也就是说生成state时一次生成624个state,然后才会进行下一次state的生成
-
生成output的方法:
currentIndex++; //currentIndex作为state的索引 int tmp = state[currentIndex]; //tmp经过下面的运算后作为返回值传给output tmp ^= (tmp >>> 11); tmp ^= (tmp << 7) & 0x9d2c5680; tmp ^= (tmp << 15) & 0xefc60000; tmp ^= (tmp >>>18); return tmp;
这里的操作虽然很复杂,但和上面的
java.utiol.Random
不同它是用的循环右移,所以并不会破坏原来的值 -
新的624个state的生成方法:
int[] state; for(i = 0;i < 624;i++) { int y = (state[i] & 0x80000000) + (state[(i+1)%624] & 0x7fffffff); int next = y >>> 1; next ^= state[(i + 397) % 624]; if((y & 1L) == 1L) { next ^= 0x9908b0df; } state[i] = next; }
每一个新的state都与之前624个state中的第一个,第二个和第398个有关
- 攻击条件:
- 需要知道624个output
- 因为绿色箭头部分的操作是可逆的,所以只要知道624个output就可以逆推出一整个state块的值从而获得到下一块的state和output