Sigurnosni problemi pri korištenju pseudoslučajnih brojeva

Kada je potrebno generirati nekakav tajni ključ, lozinku ili slično, često želimo da taj podatak bude nasumično odabran. Računala mogu proizvesti nasumične nizove koristeći algoritme generatora pseudonasumičnih brojeva. Kao što im ime kaže, takvi algoritmi ne generiraju istinski nasumične brojeve, brojevi koje generiraju ovise o inicijalnoj vrijednosti (engl. seed). Inicijalna vrijednost može biti nešto poput trenutnog vremena, vrijednosti izvedene iz pokreta miša, temperature računala i slično.

Pri korištenju generatora pseudonasumičnih brojeva u kriptografske svrhe, sigurnosni problemi mogu nastati u sljedećim slučajevima:

a) Koristi se inicijalna vrijednost koju napadač može pogoditi

b) Koristi se algoritam za generiranje slučajnih brojeva koji nije kriptografski siguran, pa se promatranjem dovoljnog broja generiranih nasumičnih vrijednosti može predvidjeti buduće vrijednosti

PRIMJER - Predvidljiva inicijalna vrijednost - Zadatak s Hacknite platforme - “Blog”

Marko je odlučio napraviti blog na kojem će dijeliti svoje programske projekte. 
Daje ti dopuštenje da testiraš sigurnost, ali s obzirom na to da nema previše funkcionalnosti vjerojatno nećeš pronaći nikakvu ranjivost.

Flag je u formatu CTF2022[brojevi]
http://chal.platforma.hacknite.hr:8077 

Na početnoj stranici vidimo da je Marko podijelio C kod generatora lozinki te spominje da je koristio taj kod i kako bi generirao lozinku za tu stranicu.

U kodu je vidljiva linija

srand(time(NULL));

koja inicijalizira generator pseudoslučajnih brojeva s trenutnim UNIX vremenom (Unix vrijeme je broj sekundi koji je prošao od 1.1.1970.) To upućuje na to da ako otkrijemo kada je Marko pokrenuo ovaj generator lozinki, možemo saznati njegovu lozinku.

Klikom na poveznicu “Marko Markić” vidimo kada je njegov korisnički račun stvoren

Možemo pretvoriti to vrijeme u UNIX vrijeme i izračunati lozinku. Međutim, budući da nam je dostupna samo minuta kad je Marko stvorio korisnički račun, a ne i sekunda kad je Marko pokrenuo generator lozinki, moramo generirati lozinke za sve sekunde u toj minuti.

#include <time.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
	
	for(long int candidate = 1660734360;candidate < (1660734360+60);candidate++){
	
	    srand(candidate);
	    for(int i=0;i<12;i++){
		int random = (rand() % (126 - 48 + 1)) + 48;
		printf("%c",random);
	    }
	    printf("\n");
	}
}

Pokretanjem programa dobijemo 60 potencijalnih lozinki. Isprobavajući ih sve možemo otkriti pravu

dW|w><pK6vWz

, prijaviti se i dobiti flag.

Ovakve ranjivosti su prisutne i u stvarnom svijetu. Kaspersky Password Manager je u prošlosti imao vrlo sličnu ranjivost [1]

Kako bi se ovakva ranjivost popravila, potrebno je koristiti siguran izvor nasumičnosti. Popularni operacijski sustavi često imaju ugrađen sigurni izvor nasumičnosti (npr. /dev/urandom na Linuxu).

PRIMJER - Korištenje algoritma neprikladnog za kriptografske svrhe - Zadatak s Hacknite platforme - Twister

Ana je sve svoje zadaće pohranjivala na svoj web poslužitelj. Međutim, netko je kompromitirao njezin poslužitelj i izvršio program koji je šifrirao sve njezine datoteke. Napadač nije bio pažljiv i ostavio je dio svog ransomware koda na poslužitelju. Možeš li pomoći Ani vratiti sadržaj njezinih datoteka?

U zip datoteci encrypted.zip se nalaze šifrirane datoteke, a ransomware skripta se nalazi u datoteci twister.py

Flag je u formatu CTF2023[brojevi].

U nastavku se nalazi kod ransomware skripte

import random
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend

def gen_filename():
    name = ""
    for i in range(0,12):
        name+=str(random.getrandbits(32))
        name+="_"
    return name

def gen_key():
    return random.getrandbits(128)

def enc_content(enc_filename,key):
    padder = padding.PKCS7(128).padder()
    f = open(enc_filename,"rb")
    content = f.read()
    f.close()
    backend = default_backend()
    key = bytes(str(key).encode())[0:32]
    iv = b"somethingrandom!"
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
    encryptor = cipher.encryptor()
    pad_content = padder.update(content)+padder.finalize()
    ct = encryptor.update(pad_content) + encryptor.finalize()
    f = open(enc_filename,"wb")
    f.write(ct)
    f.close()

def iterate_over_files():
    files = [f for f in os.listdir('.') if os.path.isfile(f)]
    counter = 0
    for f in files:
        if f.endswith(".hacknite"):
            counter+=1
            enc_filename = str(counter)+"_"+gen_filename()+str(".hahaitsencryptednow")
            os.rename(f,enc_filename)

    files = [f for f in os.listdir('.') if os.path.isfile(f)]
    key = gen_key()
    print(key)
    for f in files:
        if f.endswith(".hahaitsencryptednow"):
            enc_content(f,key)

Vidimo da skripta mijenja imena datoteka u nasumično generirano ime, a nakon toga ih šifrira nasumično generiranim AES ključem. Za generiranje nasumičnih nizova, skripta koristi Python modul “random”. Taj modul u pozadini koristi algoritam koji se zove “Mersenne Twister” koji nije prikladan za kriptografske svrhe. Naime, ako napadač uspije promatrati 624 32-bitna izlaza koje taj algoritam generira, može znati sve buduće izlaze.

Kako će nam to pomoći u ovom slučaju? Ransomware skripta prvo preimenuje datoteke u nasumično generirano ime, a tek onda generira ključ za šifriranje. Svako ime sadrži dvanaest 32-bitnih izlaza “Mersenne Twistera”, a ima 53 šifriranih datoteka, što znači da imamo 636 izlaza, što je više nego dovoljno da saznamo ključ.

Možemo koristiti alat randcrack[2] kako bi riješili zadatak:

from randcrack import RandCrack
from natsort import natsorted
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
import os

rc = RandCrack()

f_list = natsorted(os.listdir())
print(f_list)
random_list = []
for filename in f_list:
    if filename.endswith("hahaitsencryptednow"):
        splitted = filename.split("_")
        splitted.pop(0)
        splitted.pop(len(splitted)-1)
        random_list+=splitted
#int_conversion = [int(x) for x in random_list]

for x in range(0,624):
    print(random_list[x])
    rc.submit(int(random_list[x]))

for i in range(0,12):
    rc.predict_getrandbits(32)

aes_key = rc.predict_getrandbits(128)

for filename in f_list:
    if filename.endswith("hahaitsencryptednow") is False:
        continue
    f = open(filename,"rb")
    content = f.read()
    iv = b"somethingrandom!"
    backend = default_backend()
    key = bytes(str(aes_key).encode())[0:32]
    cipher = Cipher(algorithms.AES(key),modes.CBC(iv),backend=backend)
    decryptor = cipher.decryptor()
    print(decryptor.update(content) + decryptor.finalize())
    f.close()

Pokretanjem skripte u direktoriju gdje se nalaze i šifrirane datoteke dobivamo flag.

Ovaj zadatak je također inspiriran stvarnim slučajem, NemucodAES ransomware je koristio Mersenne twister algoritam za generiranje kriptografskog ključa [3].

Iako je dobro da su ransomware developeri nepažljivi i koriste loše metode kriptografije, korištenje nesigurnih generatora slučajnih brojeva može ugroziti i legitimne softvere (npr. legitimni softver za šifriranje diska).

Izvori

[1] https://www.ledger.com/blog/kaspersky-password-manager [2] https://github.com/tna0y/Python-random-module-cracker [3] https://web.archive.org/web/20220601204212/https://adamcaudill.com/2017/07/12/breaking-nemucodaes-ransomware/