伪随机数逆向:生成机制与安全漏洞

一、伪随机数介绍:

伪随机数是通过算法生成的数字序列,与真正的随机数相比,它们具有确定性的特点。这意味着,给定相同的初始种子,伪随机数生成器(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约束器求解

random.rar

实例分析:前期信息探查
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例题分析:

题目部署.zip

可以看到题目附件有两个:

先打开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个有关

  1. 攻击条件:
    • 需要知道624个output
    • 因为绿色箭头部分的操作是可逆的,所以只要知道624个output就可以逆推出一整个state块的值从而获得到下一块的state和output

 

猜你喜欢

转载自blog.csdn.net/weixin_46175201/article/details/133237666