[Assembly/C] srand와 rand 어셈블리 분석, 구현

https://learn.microsoft.com/ko-kr/cpp/c-runtime-library/reference/rand?view=msvc-170

rand

의사 난수를 생성합니다. 이 함수의 보다 프로그래밍 방식으로 안전한 버전을 사용할 수 있습니다. 를 참조하세요 rand_s. 생성된 rand 숫자는 암호화된 보안이 아닙니다. 보다 암호화적으로 안전한 난수 생성을 위해 C++ 표준 라이브러리에 선언된 함수를 사용합니다rand_s.

위에서 말하는 rand가 안전하지 않은 이유는 뭘까? 어셈블리어를 분석해보고 구현해보면 그 답을 알 수 있다.

기본 설명

rand는 0~32767의 값을 갖는다. 이는 signed 16비트의 양수범위를 의미함. 그리고 srand를 사용하여 호출 rand하기 전에 의사 숫자 생성기를 시드한다.. 즉 초기 시드값를 통해 랜덤을 반환하는 구성인것이다. 살펴보자.

살펴볼점

먼저 srand()로 같은 시드값을 설정하면 rand()를 여러번 호출한 결과 리스트는 프로그램을 몇번 실행하든 결과 리스트는 동일하다. 즉 시드값을 알면 n번째의 랜덤값을 유추 가능한것이다.

따라서 암호화적으로 안전하지 않다는 것을 의미한다. 이를 어셈블리어로 분석해보자

코드

#include <iostream>

int main() {
    srand(10);  // 시드값 설정

    // 5개의 난수 출력
    for (int i = 0; i < 5; i++) {
        printf("%d\n", rand());
    }

    return 0;
}

srand

00651090 55                   push        ebp  
00651091 8B EC                mov         ebp,esp  
00651093 51                   push        ecx  
    srand(10);  // 시드값 설정
00651094 6A 0A                push        0Ah  
00651096 FF 15 C4 20 65 00    call        dword ptr [__imp__srand (06520C4h)]  
0065109C 83 C4 04             add         esp,4  //ret ip지점

call 바로 전 스택에 10이 채워진 상태 <- esp

__imp__srand (06520C4h)

761C2520 8B FF                mov         edi,edi  
761C2522 55                   push        ebp  
761C2523 8B EC                mov         ebp,esp  
// mem 1
761C2525 E8 92 E7 FE FF       call        __acrt_getptd (761B0CBCh)  
761C252A 8B 4D 08             mov         ecx,dword ptr [ebp+8]  
761C252D 89 48 18             mov         dword ptr [eax+18h],ecx  
761C2530 5D                   pop         ebp  
761C2531 C3                   ret  

mem 1

0x00CFFA0C  1c fa cf 00  .??.	// <- ebp,esp (값은 old ebp)
0x00CFFA10  9c 10 65 00  ?.e.	// ret ip
0x00CFFA14  0a 00 00 00  // 10 시드값 --> 즉 [ebp+8]은 시드값을 의미

함수가 호출되어 위처럼 진행된 상태임 이후 mov의 결과는 시드값을 ecx와 eax+18h 두 곳에 저장

rand

다음 rand을 실행

 call        dword ptr [__imp__rand (06520C8h)]  
 call        __acrt_getptd (761B0CBCh)  
 // 1
 imul        ecx,dword ptr [eax+18h],343FDh  
 add         ecx,269EC3h  
 mov         dword ptr [eax+18h],ecx  
 shr         ecx,10h  
 and         ecx,7FFFh  
 mov         eax,ecx  
 ret

1 __acrt_getptd (761B0CBCh) 실행을 하면 ecx는 다른값으로 대체된다. eax+18h에는 그대로 시드값이 남아있다.

이후 중요한점은 계산 중 중간값을 eax+18h에 다시 넣는다는 점이다! 대충 코드로 변환한다면,

 seed = (seed * 343FDh) + 269ec3h
 [eax+18h] = seed	// 계산값을 다시 저장
 seed >> 16		// 16비트 우측 시프트
 seed = seed & 0x7FFF	// 7FFF 하위 15비트 마스킹 (반환값 범위)
 return seed

두번째 rand와 결론

똑같은 rand 함수를 실행한다

 call        dword ptr [__imp__rand (06520C8h)]  

이번에 계산이 되는 시드값은 이전 랜드함수 중간의 계산값이였다! 계속 이런식으로 rand를 진행하게된다.

결과적으로 즉 초기 시드값을 안다면 rand()함수의 n번째 랜덤값 유추가 가능한 것

코드로 구현한다면?

#include <iostream>

unsigned int state_seed;

void mysrand(unsigned int seed) {
    state_seed = seed;
    return;
}

unsigned int myrand() {
    unsigned int result;
    result = state_seed * 0x343FD + 0x269EC3;
    state_seed = result;
    result = result >> 16;
    result = result & 0x7FFF;
    return result;
}

int main() {
    srand(10);
    mysrand(10);  // 시드값 설정
    
    // 5개의 난수 출력
    for (int i = 0; i < 5; i++) {
        printf("rand: %d\n", rand());
    }
    // 5개의 난수 출력
    for (int i = 0; i < 5; i++) {
        printf("myrand: %d\n", myrand());
    }

    return 0;
}
rand: 71
rand: 16899
rand: 3272
rand: 13694
rand: 13697
myrand: 71
myrand: 16899
myrand: 3272
myrand: 13694
myrand: 13697

몇번을 실행하든 결과는 같다. 직접 구현해보니 이제 rand를 쓸때마다 친숙한 느낌이 들겠다:)