[보안] PHP 난수 생성의 모든 것 > php

본문 바로가기
사이트 내 전체검색

php

[보안] PHP 난수 생성의 모든 것

페이지 정보

작성자 서방님 댓글 0건 조회 623회 작성일 16-12-20 19:57

본문

출처 : http://www.phpschool.com/gnuboard4/bbs/board.php?bo_table=tipntech&wr_id=79087


[다른 사이트에 퍼가실 때는 반드시 출처를 밝히고 PHP스쿨로 링크를 걸어 주시기 바랍니다.] 


[그냥 모범답안을 찾고 계신 분은 맨 밑으로 스크롤하셔도 됩니다.] 


아이디/비번찾기 기능을 통해 임시비번을 발급하거나, 솔트를 포함한 강력한 알고리듬으로 비번을 암호화하거나, mcrypt를 이용해 쿠키나 그 밖의 데이터를 암호화할 때 반드시 필요한 것이 난수(random number)입니다. 


(실제로는 5NhzMZJ827oGezCC와 같이 랜덤으로 생성한 문자열이 필요할 때도 많지만, 컴퓨터의 입장에서는 모든 것이 숫자일 뿐이므로 그냥 숫자와 문자열을 통칭하여 "난수"라고 하겠습니다.) 


그런데 PHP는 난수를 생성하는 방법이 너무 다양하고, 그 함수들을 어떻게 써야 하는지에 대해서도 지나치게 오래된 정보와 잘못된 정보가 난무하고 있습니다. 공식 영문 사이트의 댓글들을 봐도 누군가는 "반드시 이렇게 해라"라고 하고, 다른 누군가는 "절대 그렇게 하지 마라"라고 하는 등... 그래서 인터넷에 굴러다니는 함수를 아무 거나 복붙하다가는 보안성이 떨어지거나 성능에 악영향을 끼치는 수가 있습니다. 


그래서 오늘은 PHP에서 난수를 생성하는 다양한 방법들과 각각의 장단점을 알아보고, 임시비번이나 솔트, 암호키 등을 생성할 때 어떻게 하는 것이 가장 안전한지를 소개해 드리겠습니다. 



1. rand() 함수 


가장 역사가 깊은 함수죠. 그냥 적당히 랜덤한 숫자를 만들어 줍니다. 


그냥 rand() 하면 0에서 2,147,483,647 사이의 정수를 반환합니다. 

rand(100, 200) 하면 100에서 200 사이의 정수를 반환합니다. 

여섯 자리 숫자를 얻고 싶다면 rand(100000, 999999) 하면 됩니다. 

만약 문자열이 필요하다면 ord(), dechex(), pack() 등의 함수를 동원하여 가공을 해야 합니다. 꽤 귀찮습니다. 


사용 가능한 환경: PHP만 있으면 어디서나 사용 가능 

사용 난이도: 숫자는 쉬움, 문자열은 복잡함 

보안성: 중~하 



2. mt_rand() 함수 


기본 rand() 함수보다 좀더 괜찮은 Mersenne Twister 알고리듬으로 난수를 생성하자는 요구에 따라 만들어진 함수입니다. 처음 생겼을 때는 선풍적인 인기를 끌었고, 그래서 요즘도 무조건 rand()보다는 mt_rand()를 쓰고 보는 분들이 많습니다. 그러나 Mersenne Twister 알고리듬도 그다지 안전한 것이 아니라는 사실이 알려졌기 때문에 실제 보안성은 rand()보다 못하면 못했지, 더 좋지는 않다는 설도 있습니다. 


사용법은 rand()와 동일합니다. 


사용 가능한 환경: PHP만 있으면 어디서나 사용 가능 

사용 난이도: 숫자는 쉬움, 문자열은 복잡함 

보안성: 하 



※ 여기서 잠깐! ※ 


rand() 또는 mt_rand() 함수를 사용할 때 srand() 또는 mt_srand() 함수를 사용해서 난수 생성기를 초기화해줄 필요가 있을까요? 지금도 많은 프로그램들이 별 생각 없이 microtime(), uniqid() 등의 결과값을 사용하여 난수 생성기를 초기화해 주고 있습니다. 그러나 PHP 4.2부터는 따로 초기화를 해주지 않아도 자동으로 초기화가 됩니다! 무려 12년 전부터 그랬어요! 즉, 초기화해 줄 필요가 없습니다. microtime()처럼 누구나 쉽게 예측 가능한 값으로 초기화하느니 차라리 그냥 운영체제를 믿는 것이 훨씬 낫습니다. 



3. microtime() 함수 


말 나온 김에 잠깐 짚고 넘어갑시다. 이건 난수 생성 함수가 아닙니다. 현재 시간을 100만분의 1초 단위까지 정확하게 측정하여 반환하는 함수입니다. (실제로 시스템 시계가 100만분의 1초까지 정확하지는 않으므로, 꽤 오차가 있기는 해요.) 만약 이 함수를 난수 생성에 사용한다면 누구나 예측할 수 있는 결과가 나와버립니다. 


보안성: 안 쓰는 것만 못함 



4. uniqid() 함수 


이거 의외로 많이 씁니다. 그러나 실제로 PHP 소스코드를 뒤져보면 microtime() 함수의 결과를 포맷만 살짝 바꾸어 반환하고 있다는 것을 알 수 있습니다. 즉, microtime()보다 조금도 나을 게 없습니다. 


보안성: 안 쓰는 것만 못함 



※ 여기서 잠깐! ※ 


md5(uniqid()); 이런거 많이 쓰시죠? 아무리 뻔한 현재 시각이라도 md5를 한 번 거치면 좀더 난수다운 뽀대가 납니다. 그러나 뽀대가 나는 것과 실제로 보안이 뛰어난 것은 전혀 별개의 문제죠. 예측 가능한 숫자를 md5에 넣어봤자 예측 가능한 해쉬값이 나오는 것은 마찬가지니까요. 해쉬 함수는 이미 강력한 보안을 갖춘 난수를 원하는 형태와 길이로 포맷하거나, 두 개 이상의 난수를 하나로 합치는 데는 쓸모가 있으나, 원래부터 보안이 약한 난수에 사용하면 거의 도움이 되지 못합니다. 



5. mcrypt_create_iv() 함수 


mcrypt를 사용하여 암호화를 할 때 빠지지 않고 들어가는 함수입니다. 흔히 초기화벡터(IV)를 생성하기 위해 사용하지만, 그 외에도 난수가 필요할 때면 언제든지 사용할 수 있습니다. 보안성은 두 번째 인자가 무엇이냐에 따라 다릅니다. MCRYPT_DEV_URANDOM으로 하면 아래에서 다룰 /dev/urandom과 동일한 보안성을 얻을 수 있고, MCRYPT_RAND로 하면 운영체제의 난수 생성 API를 그대로 사용합니다. 그러나 PHP 5.2까지는 윈도우 환경에서 MCRYPT_DEV_URANDOM을 사용할 수 없었기 때문에, 대부분의 프로그램들은 MCRYPT_RAND를 기본값으로 사용하고 있습니다. 기본값 그대로 사용하면 보안성이 다소 떨어집니다. 


바이너리 문자열을 반환하므로, 화면에 표시 가능한 문자열이나 정수로 변환하는 과정이 필요합니다. 

    abs(current(unpack('i', mcrypt_create_iv(4))));  // 정수 생성 

    bin2hex(mcrypt_create_iv(16));  // 32자리 문자열 생성 


사용 가능한 환경: mcrypt 모듈 필요 

사용 난이도: 중간 

보안성: 중 



※ 여기서 잠깐! ※ 


자꾸 보안성, 보안성 하는데 보안성의 기준이 대체 뭡니까? 라고 질문하시는 분들께... 보안성의 기준에는 여러 가지가 있겠지만, 난수 생성을 얘기할 때는 무엇보다도 예측하기 어렵다는 점이 제일 중요하겠죠. 같은 함수로 생성된 난수를 여러 개 수집하더라도 패턴을 파악하여 다음 난수를 예측할 수 없어야 하고, 예전에 생성된 난수의 값을 알 수 있어서도 안 됩니다. 


예를 들어 mt_rand() 함수로 생성한 난수는 누구든지 600여개만 수집하면 앞으로 생성될 난수를 모두 예측할 수 있습니다. 임시비번 생성 링크를 자꾸 생성하다 보면 다른 사람의 임시비번까지 알 수 있게 된다는 거죠. 반면, /dev/urandom의 값을 예측할 수 있다는 사람은 아직 한 번도 본 적이 없습니다. 



6. openssl_random_pseudo_bytes() 함수 


PHP 5.3에서 새로 생겼습니다. OpenSSL 모듈이 필요합니다. 보안성이 매우 뛰어난 난수를 반환하거나, 환경에 따라서는 가끔 좀 어정쩡한 난수를 반환하기도 합니다 ㅡ.ㅡ;; (두 번째 인자를 잘 사용하면 어떤 경우인지 판단할 수 있습니다.) 한동안 PHP 난수 생성의 정석으로 평가받던 함수이지만, 지난 봄 Heartbleed 취약점을 비롯하여 OpenSSL에서 자꾸 보안 취약점이 발견되는 바람에 이미지가 다소 훼손된 것 같습니다. 


바이너리 문자열을 반환하므로, 위의 mcrypt_create_iv() 함수와 동일한 변환 과정이 필요합니다. 


사용 가능한 환경: PHP 5.3 이상, OpenSSL 모듈 필요 

사용 난이도: 중간 

보안성: 중~상 



7. 리눅스를 비롯한 유닉스 운영체제의 /dev/urandom 


위의 함수들 중 보안성이 높은 경우는 대개 내부적으로 /dev/urandom과 연동하고 있습니다. 리눅스, BSD, OS X 등에서 일상적인 난수 생성에 가장 적합한 방법으로, 커널에서 보안성을 보장합니다 ^^ 


단, /dev/urandom은 함수가 아니라 시스템 파일입니다. fopen()으로 열어서 직접 읽어야 합니다. 

근데 이게 open_basedir 등의 서버 보안설정에 따라 에러가 날 수도 있어요. 이럴 때는 또 별도 조치가 필요함. 

    $fp = fopen('/dev/urandom'); 

    $random = bin2hex(fread($fp, 16)); 

    fclose($fp); 


반환값은 위의 함수들과 마찬가지로 바이너리입니다. 알아서 정수나 문자열로 변환하세요. 


사용 가능한 환경: 윈도우만 아니면 됨 

사용 난이도: 다소 어려움 

보안성: 상 



※ 여기서 잠깐! ※ 


/dev/random의 보안성이 더 높다고 알려져 있지만, 이건 너무 오래 걸리는 경향이 있어서 웹 어플리케이션에는 적합하지 않습니다. 게다가 요즘 운영체제들은 난수 생성 알고리듬이 많이 발전해서 /dev/random이나 /dev/urandom이나 거의 동일한 보안성을 제공합니다. 심지어 BSD에서는 /dev/urandom을 /dev/random에 심볼릭 링크로 연결해 놓았더군요. 둘이 완벽하게 똑같다는 뜻이지요. 리눅스에서도 /dev/urandom의 보안이 취약한 경우는 오직 운영체제를 처음 설치한 후 몇 분간뿐입니다. (이 때는 엔트로피가 부족함...) 따라서 평소에는 안심하고 /dev/urandom을 사용해도 됩니다. 



※ 여기서 잠깐! ※ 


/dev/urandom이나 그 밖의 함수들을 사용할 때 엄청나게 많은 양의 데이터를 마구 읽는 프로그램들이 종종 있습니다. 막 1KB씩 읽고 그럽니다 ㅎㄷㄷ 그러나 현존하는 어떤 난수 생성기도 256비트 이상의 보안성을 보장하지 못하므로, 256 ÷ 8 = 32바이트 이상 읽는 것은 낭비입니다. 입출력 양이 불필요하게 많아서 성능만 나빠져요. 만약 32바이트 이상의 난수가 필요하다면 SHA-512와 같은 해쉬 함수를 반복 사용하여 쭈~욱 늘려주면 됩니다. 



8. CAPICOM.Utilities.getRandom() 


예전에 윈도우에서 종종 쓰던 방법인데, 요즘은 거의 지원조차 되지 않으므로 건너뛰겠습니다. 참고로 이거 쓰던 시절에도 보안성은 그저 그랬어요. 게다가 결과를 base64로 인코딩해서 돌려주는 바람에 다른 용도로 쓰려면 꼭 한 단계 더 가공을 거쳐야 했죠 -_- 



9. 그래서 어쩌라고? 


리눅스 환경에서는 /dev/urandom이 갑입니다. 다른 것들은 감히 비교가 안 돼요. 


윈도우 환경에서는 PHP 5.3 이상을 설치하여 mcrypt_create_iv() 함수에 MCRYPT_DEV_URANDOM 인자를 넣어 사용하거나, openssl_random_pseudo_bytes() 함수를 사용하는 것이 가장 좋습니다. PHP 버전이 낮다면 mcrypt_create_iv() 함수를 기본값 그대로 사용하거나, 그것도 안 되면 rand() 함수를 쓰세요. 



10. 아놔 귀찮아... 


위에서 추천드린 방법들은 모두 바이너리를 반환한다는 특성이 있습니다. 정수가 필요하거나, 화면에 표시할 수 있는 알파벳과 숫자로 이루어진 문자열이 필요하다면 일일이 변환을 해줘야 하죠. 게다가 모범답안이 리눅스 따로, 윈도우 따로, PHP 버전에 따라 제각각이기 때문에 설치 환경을 완벽하게 컨트롤할 수 없다면 어쩔 수 없이 보안성을 희생하며 최대공약수를 선택할 수밖에 없습니다. 


요즘 해외에서는 PHP 보안 전문가인 Anthony Ferrera (@ircmaxell) 가 개발한 RandomLib이라는 라이브러리가 대세입니다. (링크1 참조) 그러나 이건 PHP 5.3 이상에서만 동작하고, 쓰는 방법도 꽤 복잡합니다. 


    $factory = new RandomLib\Factory; 

    $generator = $factory->getMediumStrengthGenerator(); 

    $generator->generate(32); 


이게 뭡니까 대체~~~ 난수 하나 생성하는데 팩토리 패턴까지 등장! 그야말로 자바스럽습니다~~~ 


그래서 제가 이번에 좀 단순화된 버전을 만들어 보았습니다. 이름은 PHPRandom입니다. (링크2 참조) 


사용 환경에 따라 최적의 난수를 자동으로 생성하고, 만약 어쩔 수 없이 보안성이 낮은 함수에 의존하게 되면 해쉬 함수 등을 사용하여 걸러줌으로써 예측을 더 어렵게 만듭니다. (mixing이라고 합니다. 이것도 국제 표준이 있음.) 정수, 16진수, 대소문자+숫자, 그냥 바이너리, 원하시는 포맷과 길이로 깔끔하게 가공해 드립니다. 


    include 'phprandom.php'; 

    PHPRandom::getInteger();    // 정수 생성 

    PHPRandom::getInteger(1000, 9999);  // 4자리 숫자 생성 

    PHPRandom::getString(32);  // 32자리 글자 생성 


이제는 고인이 되신 밥아저씨 말씀처럼... 참~ 쉽죠?



댓글목록

등록된 댓글이 없습니다.

Total 612건 12 페이지
게시물 검색

회원로그인

접속자집계

오늘
210
어제
163
최대
1,347
전체
154,744
Latest Crypto Fear & Greed Index

그누보드5
Copyright © 서방님.kr All rights reserved.