파일 보안 (파일포맷분석) > 일반자료실

본문 바로가기

사이트 내 전체검색

일반자료실

보안관련 파일 보안 (파일포맷분석)

페이지 정보

작성자 작성일 25-07-19 20:01 조회 3회 댓글 0건

본문

파일 보안 (파일포맷분석)


Cryptex는 패스워드를 이용해서 파일을 암호화 할 수 있는 커맨드 라인 데이터 암호화 툴이다.
이번장에서는Cryptex 파일포맷을 분석할 것이다.
Cryptex는Zip 등과 같이 여러 개의 암호화된 파일을 포함 할 수 있는 아카이브파일(확장자가.crx)을 생성한다.
Cryptex에서는 하나의 단일 아카이브 파일에 들어갈 수 있는 파일의 개수에 제한이 없으며 각 개별 파일과 아카이브 파일자체의 크기도 제한이 없다.
Cryptex는 3DES 암호 알고리즘을 이용해서 파일을 암호화 한다.
3DES는 1976년 IBM에서 만들어낸 원래의 DES 알고리즘을 강화 시킨것이다.
DES 알고리즘은 데이터를 암호화 하기 위해서 56비트의 키를 사용한다. 그런데 현대의 컴퓨터는 무작위 대입 공격 방법을 이용하면 56비트의 키 값을 쉽게 찾아 낼 수 있다. 따라서 키의 크기가 좀 더 길어져야만 했다.
3DES 알고리즘은 단순히 서로 다른세개의 56비트의 키를 이용하고 원래의 DES 알고리즘을 이용해서 평문을 세번 암호화한다. 물론 세번의 암호화 과정에서 사용되는 키는 각기 틀린 키를 사용한다.
3DES(triple-DES)는 결국 168비트(56*3)의 키를 이용하게 된다.
Crtptex에서는 프로그램동작 중에 제공 되는 패스워드 문자를 이용해서 키 값을 만들어낸다.
프로그램을 이용함으로써 얻을 수 있는 보안레벨은 어떤패스워드를 사용하느냐에 따라 좌우된다.
"12345" 이나 성명과 같이 아주 평볌한 패스워드를 이용해서 파일을 암호화한다면 상당히 낮은 보안등급의 부여 될 것이다.
사전 기반 무작위 대입 공격을 구현하는 것은 그리 어려운일이 아니며 그것을 이용해서 복호화 키를 알아내는 것도 어려운 일이 아니기 때문이다.
반대로 "j8&1`#;#mAkQ)d*" 처럼 길고 예측하기 힘든 패스워드를 사용하면 Cryptex는 상당히 높은 보안성을 제공할 것이다.
Cryptex
Cryptex 사용
Cryptex는 네개의 명령 옵션만을 제공하며 사용법이 매우 직관적이다.
사용자가 입력한 패스워드를 이용해서 파일을 암호화 하고 아카이브 파일에서 특정파일을 지우거나 추출하는 기능을 제공한다.
*.doc처럼 와일드 카드문자를 이용해서 한번에 여러 파일을 처리할 수도 있다.
Cryptex 리버싱
파일 포맷 리버싱의 시작을 어떻게 할 수 있을까? 대부분 간단하고 작은 파일을 만드는 것으로 시작한다.
Cryptex의 경우에는 식별하기 쉬운 내용을 담고있는 단일파일로 이뤄진 간단한 아카이브파일을 만듦으로써 시작한다.
이런 식의 접근방식은 상당히 유용하다. 하지만 늘 그런것 만은 아니다.
예를 들어 해당 파일포맷의 파일을 읽을 수 있지만 그런파일의 파일을 만들 수 없는 경우가 있다.
이런경우에는리버싱과정이상당히복잡해진다.
즉, 해당 파일 포맷을 읽어들이는 코드를 분석하는데 상당히 많은 양의 시간을 투자 해야만 한다.
대부분의 경우 그렇게 코드를 철저하게 분석하면 원하는 답을 얻을 수 있다.
다행스럽게도Cryptex의경우에는자유롭게파일을만들어서분석할수있다.
그럼 우선"***************" 처럼 하나의문자로 이뤄진 긴 문자열을 포함하는 간단한 텍스트파일을 만들고 그것을 암호화하는 것으로 시작하자.
추가적으로 패스워드가 파일의 어느부분에 저장되는지 볼 수 있게 길고 반복적인 문자로 이뤄진 패스워드를 사용하길 권장한다.
Cryptex a Test1 6666666666 asterisks.txt
또한 Cryptex가 파일 이름이나 파일 내용을 실제로 암호하해서 저장하는지 확인하기 발나다.
여기서 흥미로운 사실은 Test1.crx 파일의 크기가 기대보다 훨씬 큰8,248바이트라는것이다.
이는 Cryptex 파일 포맷 이상 당한 오버헤드를 가지는것으로 보인다.
파일의 내용을 자세히 살펴 보기 전에 먼저 Cryptex가 파일 포맷 내용을 어떤식으로 바라보는지 알아보자.
이는 Cryptex 파일안의 내용을 보여주는 명령을 이용하면 된다.
Cryptex l Test1.crx 66666666666
(참고로 모든 명령어에는 패스워드필요)
위 출력결과에서 특별히 놀랄만한 내용은 없지만 한가지 흥미로운 점이 있다.
원래asterisks.txt 파일이원래크키는(약1K 였는데위의결과출력결과에서는3K인것으로나왔다. 왜2K가 증가 된것일까? 이에 대해서는 나중에 살펴보자.
Cryptex l Test1.crx 66666666665
여기서 한가지 더 실험해보자 만약 번호를 잘못 입력 했을시에는 어떻게 뜰까?
실험결과 Cryptex 는포함된 파일이 나열하기전에 입력된 패스워드를 실제로 검사한다는 사실이 확인됐다. 이미 항상 패스워드가 필요하다고 확실히 명시됐기때문에 이런실험은 어떻게 보면 쓸데없는것처럼 보일 수 있다. 하지만 패스워드가 틀렸다는 메시지내용을 정확히 알고 있으면 나중에 프
로그램 코드에서 메시지를 찾을때나 입력 된 패스워드가 올바른것이지 판단하는방법을 알아내고자 할 때 유용하게 사용 할 수 있다.
이제 Cryptex가 만들어 낸 파일의 내용을 살펴보자. 파일의 내용을 보기 위해서 어떤종류의헥사(hex) 덤프툴을 써도 상관 없으며 무료로 다운로드 할 수 있는 툴이 상당히 많다. 하지만 약간의 돈을 투자할 용의가 있다면 강력한 데이터리버싱툴 중 하나인Hex Workshop 을 추천한다.
파일 포맷 분석
2015년 10월 24일 토요일 오후 10:08
파일 포맷 분석 페이지 1
이제 Cryptex가 만들어 낸 파일의 내용을 살펴보자. 파일의 내용을 보기 위해서 어떤종류의헥사(hex) 덤프툴을 써도 상관 없으며 무료로 다운로드 할 수 있는 툴이 상당히 많다. 하지만 약간의 돈을 투자할 용의가 있다면 강력한 데이터리버싱툴 중 하나인Hex Workshop 을 추천한다.
다음은 Test1.crx 파일의 시작부분에 있는 64바이트 내용이다.
다음 파일포맷과 마찬가지로 .crx 파일도 CrYpTeX9 라는 시그니처로 시작한다.
그 이후에는 몇 개 의데이터 필드가 있고 0x18 위치로 부터는 랜덤바이트 값이 위치하는 것으로 보인다.
그리고 파일이 나머지 부분을 살펴보면 판독하기 어려운 내용으로 가득차 있다.
이는 파일 테이블 뿐만 아니라 파일의 전체 내용이 모두 암호화 됐다는 것을 의미 한다.
예상 한대로 파일의어느위치에도입력한패스워드나asterisks.txt 라는파일이름, 파일의 내용 인 별표 문자열 등이 보이지 않는다.
Hex WorkShop의 Character Distribution 기능을 이용해서 파일 내의 데이터를 살펴보면 파일이 암호화 됐다는 것을 더 확실히 알 수 있다.
흥미로운 사실은 파일안의 내용은 외관상으로 랜덤한 데이터로 이뤄져 있고 파일의 내용에 존재하는 256개의 문자는 모두 각 기 동일하게 0.4%의 분포도를 갖는다는 것이다. 이는 Cryptex에서 사용한 암호 알고리즘 이 암호화 된 데이터와 패스워드,파일이름, 파일 내용 사이의 유사성을
완벽히 제거 하기 때문으로 보인다.
이 시점에서 .crx 파일 포맷을 제대로 분석하려면 프로그램 코드에 대한 분석이 필요하다는 것이 명백해졌다.
따라서 전통적인 코드 리버싱과 데이터 리버싱이 동시에 수행되어야 한다. 데이터가 어떻게 처리되는지 알아내려면 프로그램 내부를 살펴봐야 하기 때문이다. Cryptex의 데이터는 암호화되므로 리버싱을 위한 좋은 예라고 할 수 있다.
하지만 의도적으로 파일 포맷의 내용을 숨기려는 프로그램의 경웨는 데이터를 단순히 관찰하는 것만으로는 파일의 포맷을 알아내기 힘들다.
Cryptex가 어떻게 동작하는지 알아내기 위한 첫 번째 단계는 임포트해서 사용하는 함수들이 어떤 것인지 알아보는 것이다.
이는 4장에서 소개한 실행 이미지 덤프 툴 중 하나를 이용하면 쉽게 알 수 있다. (나는 PE View로 사용할 것이다.)
임포트 함수 목록을 통해서 Cryptex가 작업을 어떻게 수행하는지 대략적으로 짐작 할 수 있다. 예를 들어 Crytpex가 어떤 방법으로 파일을 읽고 쓰는지 살펴볼 수 있다.
파일을 읽고 쓸 때 세션 객체를 사용하는지, 런타임 라이브러리 파일 I/O 함수를 사용하는지, Win32 파일 I/O API를 직접 호출하는 지 등을 알 수 있다.
Cryptex의 파일 I/O 작업을 분석하려면 먼저 어떤 방법으로 파일 I/O 작업을 수행하는 지 알아야 한다. 그래야 해당 함수 호출 부분에 브레이크 포인트를 지정해 실행을 추적할 수 있기 떄문이다.
우선 알아둬야 할 것은 리스트의 모든 함수들이 Cryptex에 의해서 직접 호출되어 사용되는 것은 아니라는 점이다.
대부분의 프로그램은 라이브러리를 정적으로 링크(런타임 라이브러리 처럼) 해서 사용하며 라이브러리는 운영체제나 다른 DLL들을 호출한다.
KERNEL32.dll 항목의 함수 목록을 보면 Cryptex는 파일 I/O를 위해 CreateFile,ReadFile,WriteFile 등과 같은 Win32 파일 I/O API를 직접 사용한다는 것을 알 수 있다.
KERNEL32.dll 다음에는 Cryptex가 사용하는 ADVAPI32.dll의 함수 이름들이 나열되어 있다. 함수 이름만 봐도 중요한 정보를 알 수 있다.
그것은 윈도우 Crypto API(CryptEncrypt, CryptDecrypt 라는 함수 이름만으로 쉽게 알 수 있다.) 를 사용한다는 것이다.
윈도우 Crypto API는 일반적인 암호화 라이브러리로서 설치 가능한 CSP(cryptographic service providers)를 지원하며 다양한 암호 알고리즘을 이용해서 데이터를 암호화 하거나 복호화할 수 있다.
마이크로소프트는 윈도우 자체에 내장되어 있지 않은 CSP를 여러 개 제공하고 있으며 DES,RSA,AES 같이 다양한 대칭형, 비대칭형 알고리즘을 지원한다.
Cryptex가 Crypto API를 사용한다는 것을 안다는 것은 그것이 사용하는 암호 알고리즘과 암호화 키 생성 방법을 판단하기가 그만큼 쉬어 졌다는 것을 의미하므로 상당한 희 소식이다.
ADVAPI32.dll 이후에는 Visual C++ 런타임 라이브러리 DLL인 MSVCR71.DLL의 함수들이 나열되어 있다. 이 목록에서 Cryptex가 호출해 사용하는 런타임 라이브러리 함수들을 볼 수 있다. 여기에는 콘솔 윈도우에 메시지를 출력하는 printf 함수외에는 특별한 것이 없
다.
Printf 함수는 Cryptex가 특정한 메시지를 콘솔 윈도우에 출력하려는 순간을 잡아내고자 할 떄 사용할 수 있다.
Cryptex의 패스워드 검증 과정을 분석하기 위한 가장 쉬운 방법은 먼저 Cryptex를 이용해서 파일을 생성 (앞에서 이미 Test1.crx 파일을 생성했다.)
그리고 잘못된 패스워드를 입력해서 Cryptex를 디버거로 실행시키는 것이다. 다음에는 Cryptex가 사용자에게 잘못된 패스워드를 입력했다는 메시지를 출력하는 부분의 코드를 찾는 것이다.
아까 익스포트 메뉴를 통해 printf 함수를 사용한다는 것을 이미 알았으므로 해당 코드를 찾는 것은 어려운 일이 아니다.
"Bad Password" 라는 메시지를 출력하는 printf 함수를 찾아낸 후 그것을 호출한 부분을 거꾸로 찾아가서 왜 해당 패스워드가 잘못된 것으로 판단됐는지 분석하면 된다.
WinDbg나 OllyDbg와 같은 유저 모드 디버거로 Cryptex를 로드하고 MSVCR71.dll 의 printf 함수에 브레이크 포인트를 건다.
Cryptex를 실행시키기 전에 먼저 Cryptex에 전달될 파라미터를 설정해야 한다.그래야 Cryptex가 어떤 아카이브 파일을 열어야 할지 알 수 있기 때문이다. 그리고 Cryptex가 잘못된 패스워드를 입력했다는 메시지를 출력하게 만드려면 틀린 패스워드를 입력
해야 하는 것을 잊어서는 안 된다. Cryptex에 요청하는 명령은 아카이브 파일 목록을 구하는 명령이 적당하다. 요청한 명령으로 인해서 아카이브 파일 변경되면 안되기 때문이다.
(Cryptex는 패스워드가 틀리면 요청한 명령에 상관없이 아카이브 파일에 대한 어떤 수정도 가하지 않는다.)
여기서는 Cryptex 1 test1 6666666665 명령을 이용, MSVCR71.DLL의 printf 함수에 브레이크 포인터를 설정했다.
(OllyDbg의 Executalbe Module 창에서 MSVCR71.DLL를 찾고, 익스포트 함수목록을 보기위해 Names 창을 이용, printf 함수를 찾아 브레이크 걸기)
프로그램이 실행되면서 printf 함수가 세 번 호출된다. 첫 번째는 "Cryptex 1.0…" 메시지 두번째는 "Listing Aall file …" 메시지다. 그리고 마지막으로 우리가 찾는 메시지 "ERROR : Invalid password.." 메시지다.
이때 할 일은 해당 printf 함수를 호출한 곳으로 이동해서 입력된 패스워드를 찾는 작업이다. 세 번째 printf 함수가 호출되면 OllyDbg에서 Ctrl+F9 키를 눌러서 RET 명령 바로 전까지 프로그램이 실행되게 만든다.
그러면 printf 함수를 호출한 함수로 이동할 수 있다.(가장 편한 방법은 Serch -> text String 으로 해당 문자열을 클릭하면 된다.)
"Bad Password" 메시지 잡아내기
패스워드 검증 과정
파일 포맷 분석 페이지 2
그러면 printf 함수를 호출한 함수로 이동할 수 있다.(가장 편한 방법은 Serch -> text String 으로 해당 문자열을 클릭하면 된다.)
Cryptex 아카이브 파일 헤더를 읽어 들여서 입력된 패스워드가 올바른지 검사하는 Cryptex의 헤더 검증 함수
먼저 파일 포인터를 파일의 시작 위치로 이동 (SetFilePointer API를 이용) 시키고 ReadFile API를 이용해서 파일의 처음 0x28(40) 바이트를 00406058 위치의 데이터 구조체로 읽어 들인다.
00406058이라는 주소는 실행되는 코드 주소와 상당히 가깝기 때문에 그것이 전역 변수의 주소라는 것을 쉽게 알 수 있다.
OllyDbg의 Executable Modules 창을 보면 프로그램 실행 이미지, 즉 Cryptex.exe는 0040000 위치에 로드되고, 00406058은 Cryptex.exe 모듈 영역 안의 주소(아마도 데이터 섹션)라는 것을 알 수있다.
(이는 실행 이미지 덤프 툴을 이용해서 모듈의 데이터 섹션 RVA를 구해보면 확인할 수 있다.)
계속해서 함수는 헤더의 처음 두 DWORD 값을 각기 하드 코딩된 값인 70597243,39586554와 비교한다.
비교 결과 같지 않으면 40123C 위치로 점프하고 "ERROR: Invalid Cryptex9 signature in file header!" 라는 메시지를 출력한다.
70597243은 CrYp를 나타내는 16진수 값이고, 39586554는 TeX9를 나타내는 16진수 값이다. 즉 ,Cryptex는 파일 헤더의 시그니처를 확인해서 마지 않으면 에러 메시지를 출력하는 것이다.
다음에 이어지는 코드는 패스워드 입력이 잘못됐다는 메시지를 출력할 것인지, 함수가 1 값을 반환할 것인지 결정하는 부분이다.
코드는 메모리상의 두 16바이트 데이터를 비교해서 서로 같지 않으면 에러 메시지를 출력한다.
첫 번째 비교 대상 데이터의 위치는 00405038이며 이는 또 다른 전역 변수로서 그 내용은 현재 알지 못한다. 두 번째 비교 대상 데이터의 위치는 00406070이며 파일 헤더가 저장된 전역 변수(00406058)의 일부분이다.
앞에서 ReadFile API를 이용해서 파일헤더의 0x28(40) 바이트를 00406058 위치로 읽어 들였으므로 00406070은 00406058 위치에서 시작하는 전역 변수 구조체 영역 안에 존재하는 것이 확실하다.
그리고 00406070은 00406058 위치로부터 0x18 만큼(24) 떨어져 있으므로 해당 구조체에서 00406070 이후에는 0x10바이트(10진수로 16바이트) 크기의 데이터가 존재한다.
실질적인 비교 작업은 REPE CMPS 명령을 이용해서 수행한다.
이는 EDI 레지스터와 ESI 레지스터가 가리키는 DWORD 쌍을 비교하고 나면 두 레지스터의 인덱스도 증가한다.
비교하는 횟수는 ECX 레지스터의 값에 따라 결정되며, 코드의 경우에 ECX 값은 4다.
따라서 네 개의 DWORD(16바이트) 값을 비교하고 서로 같다면 00401234 위치로 점프하고, 같지 않다면 0040121E의 코드가 실행된다.
이 시점에서 가질 수 있는 의문은 비교를 수행한 두 버퍼가 과연 무엇인가이다.
패스워드 일까? OllyDBG를 이용하여 두 버퍼의 내용을 확인할 수 있다.
다음은 00405038 위치에 전역 변수에 저장된 내용이다.
다음은 비교 작업에 사용되는 파일 헤더의 내용이다.
위 두 버퍼의 내용은 확실히 다르다. 또한 둘 모두 확실히 평문 패스워드가 아니다.
따라서 Cryptex는 패스워드를 어떤 식으로든 변환해서 파일 내부에 저장하고 입력한 패스워드를 그와 같은 방식(완벽히 동일한 알고리즘을 이용)으로 변환해서 서로 비교하는 것으로 보인다.
여기서는 패스워드가 어떻게 변환되는지, 변환 방법은 완전한지 의문을 가질 수 있다.
즉, 변환된 패스워드만으로 원래의 패스워드를 얻을 수 있다면 파일 헤더에서 패스워드를 추출하는 것이 가능하다.
패스워드 변환 알고리즘
평문의 패스워드를 16바이트 데이터로 변환하는 코드의 위치를 찾는 가장 쉬운 방법은 사용자가 입력한 패스워드가 저장되는 전역 변수 메모리에 브레이크 포인터를 설정하는 것이다.
그 위치는 바로 00405038이며,파일 헤더 데이터와 비교하는 데 사용된 메모리 주소다. OllyDbg에서 메모리에 브레이크 포인트를 설정하려면 Dump 창으로 00405038 주소를 연다.
그리고 해당 주소에서 마우스 우측 버튼을 클릭해서 Breakpoint -> Hardware, On write -> DWORD 메뉴를 선택한다.
브레이크 포인터를 설정하려면 프로그램을 다시 실행시켜야 한다.
패스워드가 틀렸다는 메시지가 출력된 시점에는 이미 변환된 패스워드 값으로 00405038이 이미 초기화된 상태이기 때문이다.
프로그램을 다시 실행(동일한 커맨드라인 파라미터를 사용해서)해서 00405038 위치에 하드웨어 브레이크 포인트를 설정하고 프로그램이 계속 실행되게 한다.
그러면 디버거는 설정한 브레이크 포인트에 의해서 RSAENH.DLL 안의 어느 한 위치의 코드에서 멈춘다. 어째서 Microsoft Enhanced Crytographic Provider 가 Ctyptex.exe의 전역 변수에 데이터를 쓰는 것일까? 그것은 아마도 Cryptex.exe가 해당 전역
변수를 제공했기 때문일 것이다. 스택을 조사하면 Cryptex의 어느 부분에서 이 부분의 코드를 호출했는지 거꾸로 찾아낼 수 있다. Stack Window 차응ㄹ 통해서 스택을 거슬러 올라가면 현재의 코드가 CryptGetHashParam API 안의 코드임을 알 수 있다. 이
API는 Cryptex가 내부적으로 호출해서 사용하는 API이다.
파일 포맷 분석 페이지 3
CSP(Cryptographic Service Provider)를 호출해서 사용하는 Cryptex내의 함수, 16바이트의 패스워드 식별자가 이 함수 안에서 설정한다.
이 코드를 이해라면 먼저 리스트 안에서 사용된 해시 API들을 이해해야 한다.(msdn.microsoft.com 사이트)
해시는 MSDN에서 "임의의 크기의 데이터에 수학적 함수(해시 알고리즘)를 적용해서 얻은 고정된 길이의 결과 값"이라고 정의되어 있다.
CrypteCreateHash 함수로 해시 객체를 생성하고 CryptHashData 함수로 생성한 생성한 해시 객체에 해싱을 수행할 데이터를 전달한다.
CryptGetHashParam 함수를 이용해서 수행된 해시 값도 얻어낸다. 이와 같이 해시 함수에 대한 기본적인 이해를 바탕으로 이 함수를 분석해서 과연 어떤 작업을 수행하는지 알아보자.
코드는 CryptCreateHash 함수로 해시 객체를 생성하는 것으로 시작한다.
이 함수에 전달되는 두 번째 파라미터에 주목하기 바란다.
두 번째 파라미터는 어떤 알고리즘으로 해시를 수행할 것이지 결정하는 것이다.
코드에서는 0x8003 값을 사용했다. 0x8003 값이 무엇을 원하는지는 CALG_MD5 와 같이 유명한 해시 알고리즘 식별자를 찾아보거나 헤더 파일인 WinCrypt.h 파일을 통해서 알아낼 수 있다.
0x8003 값에는 알고리즘 클래스가 ALG_CLASS_HASH이고 알고리즘 타입이 ALG_TYPE_ANY이며 마지막으로 ALG_SID_MD5 알고리즘을 사용한다는 의미가 포함되어 있다.
따라서 결국 0x8003 값이 의미하는 바는 해싱에 사용되는 알고리즘은 ALG_SID_MD5라는 것이다.
MD5(MD는 Message Digest의 약자임)는 널리 사용되는 해시 알고리즘으로서 길이가 유동적인 데이터에서 128비트의 해시 값이나 체크섬 값을 산출해 낸다.
산출해 낸 해시 값은 특정 데이터에 대한 식별자로 사용될 수 있다. MD5와 기타 다른 해시 알고리즘은 두 가지 기본적인 특징을 가진다. 그것은 서로 다른 두 데이터가 동일한 해시 값을 가질 수 없으며 특정한 해시 값을 갖는 데이터를 만들어 내는 것은 불
가능 하다는 것이다.
Cryptex가 해시 값을 산출하기 위해서 사용하는 원래의 데이터는 CryptHashData 함수 호출 부분을 조사하면 알아낼 수 있다. MSDN에 따르면 CryptHashData 함수에 전달되는 두 번째 파라미터가 바로 해싱될 데이터를 의미한다.
위의 함수를 보면 Cryptex는 두 번째 파라미터로 EDX 레지스터 값을 전달한다. 이는 [ESP+C] 위치의 값이 EDX에 레지스터에 복사된 것이다.
세번째 파라미터 (0x14)만큼
세 번째 파라미터는 전달되는 버퍼의 크기를 나타내며 0x14(20) 바이트가 전달된다. [ESP+C]가 가리키는 버퍼의 내용은 다음과 같다.
현재는 버퍼의 내용이 문자열인지 아닌지 알 수 없다.
Cryptex는 계속해서 CrypGetHashParam 함수를 호출하는 데 두 번째 파라미터 값으로 2를 전달한다.
WinCrypt.h 파일을 보면 2가 HP_HASHVAL을 의미한다는 것을 알 수 있다. 이는 Cryptex가 CryptGetHashParam 함수를 이용해서 해시 값(0x0018F5B0 위치의 20바이트 데이터에 대한 MD5 값)을 요청하는 것이다.
해시 값은 CryptGetHashParam 함수의 세 번째 파라미터를 통해서 전달받는다.
세 번째 파라미터가 무엇인지 알겠는가? 그것은 바로 입력된 패스워드가 올바른지 검사하기 위한 비교 작업에서 전역된 변수인 00405038이다.
정리하면 Cryptex는 MD5 해시 알고리즘을 이용해서 데이터를 해싱하고 결과 값을 전역 변수에 저장한다.
전역 변수에 저장되는 내용은 나중에 Cryptex 아카이브 파일에 저장된 내용과 비교한다.
비교 결과 두 내용이 동일하지 않다면 Cryptex는 패스워드가 틀렷다는 메시지를 출력한다.
위 함수가 해시를 수행하는 데이터는 분명 사용자가 입력한 패스워드와 관련이 있다. 다만 어떻게 연결되는지 모를 뿐이다.
그리고 해싱이 수행되는 데이터는 함수의 파라미터로 전달된다.
패스워드 해싱
이 시점에서는 전의 해시 작업에 사용된 버퍼가 어디에서 전달됐는지 궁금할 것이다. 이를 알기 위한 가장 좋은 방법은 단순히 해당 버퍼에 관련된 코드를 찾을 떄 까지 프로그램 코드를 거꾸로 추적해 나가는 것이다.
전의 함수에 전달된 버퍼는 402300의 함수가 전달한 것이며 Cryptex의 키 생성 함수이다.
파일 포맷 분석 페이지 4
이 함수는 전에 패스워드 변환 알고리즘 함수와 상당히 유사한 구조를 가지고 있다. 코드를 보면 먼저 해시 객체를 생성 (CryptCreateHash) 한 후 어떤 데이터에 대한 해싱 작업을 수행하고 있다.
그런데 해시 객체를 생성할 때 전달하는 파라미터 값이 패스워드 변환 알고리즘와 다르다. 패스워드 변환 알고리즘에서는 해시 알고리즘 0x8003을 사용했지만 Crypt 키 생성 함수에서는 CALG_SHA 해시 알고리즘을 의미하는 0x8004 (0040231A 행)를 사용했
다. SHA 또한 해시 알고리즘 중 하나로서 MD5와 유사한 특징을 가지고 있다. 차이점이라고 한다면 MD5의 해시 값은 128비트인데 반해 SHA의 해시 값은 160비트라는 것이다.
160비트는 20바이트로서 패스워드 변환 알고리즘에서 해싱을 수행한 데이터의 길이와 일치한다. 그렇다면 이 SHA 해싱 값 20바이트가 패스워드 변환 알고리즘에서 해싱되는 값일까? 이는 이후에 확실히 알게 될 것이다.
해시 객체를 생성한 이후에는 CryptHashData 함수를 호출한다. 하지만 그전에 어떤 데이터에 대한 처리 작업이 수행된다. 여기서 브레이크 포인트를 설정하고 프로그램을 다시 실행하면 그 데이터가 바로 패스워드로 입력한 6666666665 문자열이라는 것을
쉽게 알수 있다.
다음은 패스워드 데이터를 처리하는 코드이다.
위 코드는 아주 간단하다. 문자열에서 각 문자를 읽어서 그것이 0인지 아닌지 확인하는 작업을 수행한다. 0이라면 루프를 종료한다. 루프가 종료 됐을떄 EAX 레지스터는 문자열의 끝을 나타내는 NULL 문자를 가리키고, ESI 레지스터는 문자열의 두번째 문자를 가리킨다. 최종
적으로 다음 명령에 의해서 문자열에 대한 처리가 완료 된다.
NULL 문자를 가리키는 포인터 값에서 두 번째 문자를 가리키는 포인터 값을 뺀다. 뺀 결과 값은 NULL 문자를 제외(ESI 레지스터가 첫 번째 문자가 아닌 두 번쨰 문자를 가리키기 떄문)한 문자열의 길이가 된다.
이는 C 런타임 라이브러리 함수인 Strlen 함수와 기능이 같다. 그렇다면 왜 Strlen 런타임 라이브러리의 함수를 사용하지 않고 그와 동일한 기능의 코드를 구현했는지 의아할 것이다. 그것은 소스코드에서는 Strlen 런타임 라이브러리 함수를 호출했지만 컴
파일러가 그것을 내부적으로 위의 코드로 코드로 교체하는 경우가 있다. 이렇게 하면 함수 호출로 인한 부하게 제거되므로 성능이 향상될 수 있다.
패스워드 문자열의 길이를 계싼한 다음에 CryptHashData 함수를 이용해서 패스워드 문자열에 대한 해싱 작업을 수행한다. 그리고 CryptGetHashParam 함수를 이용해서 패스워드 문자열에 대한 해시 값을 구한다. 해시 값은 다시 알고리즘 변환 함수(402280)
에 파라미터로 전달된다. 이는 좀 이상하게 보일 수 있다. 알고리즘 변환 함수는 자신에게 전달된 데이터를 MD5 알고리즘으로 다시 해싱하기 떄문이다. 왜 이미 해싱된 값을 또 다른 알고리즘으로 다시 해싱을 수행하는 것일까? 현재는 그 이유를 명확히 알 수 없다.
MD5 해싱이 수행된 이후(0이 아닌 값을 반환했을 떄 )에 함수는 CryptDeriveKey(004023F4)라고 흥미로운 API를 호출한다. 마이크로 소프트 문서를 보면 CryptDeriveKey는 베이스 데이터로부터 세션 암호키를 생성하는 API라고 설명되어 있다. 여기서 베이
스 데이터는 바로 패스워드 문자열을 SHA 알고리즘으로 해싱한 160비트의 데이터다. 키를 생성하기 위해서는 베이스 데이터 뿐만 아니라 어떤 암호 알고리즘을 사용할 것인지 명시해야 한다.(이는 CryptDeriveKey 함수에 두 번째 파라미터로 전달된다.)
키 생성 함수를 보면 Cryptex는 사용할 암호 알고리즘을 전달하기 위해서 0x6603 (004023E6) 값을 사용했다. winCrypt.h 헤더 파일을 살펴보면 0x6603이 CALG_3DES를 나타낸다. 는 것을 알 수 있다. 이는 Cryptex가 데이터를 암호화하기 위해서 3DES 알
고리즘을 사용한다는 것을 의미한다.
좀 더 깊이 생각해보면 Cryptex가 왜 MD5 해시를 수행하는지 알 수 있다.
기본적으로 Cryptex는 데이터를 암호화 하고 복호화하기 위한 키로 SHA 해시 값을 사용한다.(3DES는 대칭형 알고리즘이므로 동일한 키를 이용해서 암호화하거나 복호화 할 수 있다.) 또한 Cryptex는 입력된 패스워드가 올바른 것인지 간단히 확인할 수 있는
방법이 필요하다. 이를 위해 Cryptex는 SHA 해시 결과 값을 다시 해싱(MD5 알고리즘을 이용해서)해서 그 결과 값을 파일 헤더에 저장한다. 따라서 파일 헤더에 저장된 값은 두번 (한번은 SHA 다른 한번은 MD5) 해싱된 것이고 그 값을 이용해서 패스워드가
올바른지 확인하는 것이다. Cryptex가 MD5를 수행하지 않고 SHA만을 수행한 해시 값을 파일 헤더에 곧바로 저장하지 않은 이유는 무엇일까? 왜 추가적인 해시 작업을 수행한 것일까? 그 이유는 SHA 해시 값이 암호화 키로 사용되기 떄문이다. 파일 헤더에
키 값을 저장해 놓으면 그것을 이용해서 Cryptex 아카이브 파일 쉽게 복호화 할 수 있다. 물론 SHA 해시 값을 이용해서 원래의 패스워드 문자열을 추출하는 것은 불가능하다.
하지만 패스워드를 알아내지 않아도 된다. 파일의 데이터를 복호화하는 데에는 SHA 해시 값만 있으면 되기 떄무이다. 이런 이유로 Cryptex는 SHA 해시 값을 다시 한 번 해싱하고 결과 값을 고유한 패스워드 식별자로 이용하는 것이다.
마지막으로 패스워드 문자열을 곧바로 MD5하지 않고 왜 SHA 결과 값을 MD5하는 것일까? 그것은 아마도 누군가 MD5 해시 값에서 그에 사응하는 SHA 해시 값을 알아내고 결국에는 복호화 키를 알아낼 수 있는 가능성 때문일 것이다. 하지만 그것은 수학적으로
불가능하다. 그렇다면 왜 그런 것일까? 파일 헤더에 저장된 MD5 값을 이용해서 원래의 키 값(SHA 해싱된 키 값)을 알아내는 것은 확실히 불가능하다. 파지만 보안 관련 기술을 개발 할 떄는 지나치다 싶을 정도로 보완성에 집착할 필요가 있다.
파일 포맷 분석 페이지 5
(Cryptex의 키 생성 과정과 패스워드 검증 과정)
지금 까지 Cryptex가 패스워드와 암호화 키를 어떻게 처리하는지 살펴봤으므로 이제는 Cryptex의 디렉토리 구조를 분석해볼 차례다. 이는 Cryptex의 보안 레벨 분석과는 관련성이 없지만 Cryptex와 호환되는 파일을 생성하거나 읽기 위해서는 상당히 중요한 부분
이다. 지금까지 데이터 리버스 엔지니어링을 학습한 이루로 디렉토리의 구조는 가장 복잡한 데이터 구조체가 될 것이다.
디렉토리 처리 코드 분석
디렉토리 구조를 분석하려먼 Cryptex의 코드에서 암호화된 elfprexh리 구조 데이터를 읽고 그것을 복호화하는 부분을 찾아야 한다. 이는 단순히 ReadFile API에 브레이크를 설정한 후 그 위치의 코드에서 데이터를 어떻게 처리하는 지 추적해 나가면 된다.
그렇다면 OllyDbg로 ReadFile API에 브레이크 포인트를 설정하고 프로그램을 다시 실행해보자. (프로그램 커맨드라인 옵션으로 올바른 패스워들 입력해야 한다는 것을 잊으면 안된다.) 브레이크 포인터에 의해서 프로그램을 멈추는 첫 번재 시점은
ADVAPI32.DLL이 시스템 내부적으로 ReadFile API를 호출할 때다. 디버거로 프로그램이 계속 실행되게 하면 또다시 시스템 내부적으로 ReadFile API를 사용할 떄 멈춘다. 얼마 지나지 않아 시스템 내부적으로 ReadFile API가 상당히 많이 사용된다는 것을
알 수 있을 것이다. 이런 상황에서 취할 수 있는 방법은 다양하며 어떤 애플리케이션인가에 따라서 방법이 달라진다. 그 중 한가지 방법은 아카이브 파일에 대한 ReadFile 인 경우에만 브레이크 포인트가 활성화 되게 제한하는 것이다. 먼저 아카이브 파일을
생성하거나 열기 위해서 호출하는 API(아마 CretaeFile API 일 것이다) 에 브레이크 포인트를 설정해서 아카이브 파일의 핸들을 얻는다. 그리고 해당 파일 핸들을 이용하는 ReadFile API인 경우에만 브레이크 포인터가 활성화 되게 설정한다.(대부분의 디버거가
이런 브레이크 포인트 설정을 지원한다.) 이렇게 하면 시스템 내부적으로 호출되는 수백 개의 ReadFile API를 건너뛸 수 있고 Cryptex가 아카이브 파일을 일기 위해서 호출하는 ReadFile API 만을 잡아낼 수있다.
다른 방법으로 Cryptex는 상당히 간단한 프로그램이므로 키 생성함수가 호출될 때까지 프로그램이 실행되게 하는 것이다. 그리고 그 시점부터 내부 적으로 디렉토리 데이터 구조체를 해석하는 부분의 코드가 실행될 때까지 코드 단계를 따라간다. 하지만 대부
분의 프로그램에서는 브레이크 포인트를 설정할 위치에 대한 좋은 아이디어를 생각해내야만 한다. 단순히 코드 실행을 따라가는 것은 상당히 길고 지루한 작업이기 때문이다.
키 생성 함수의 끝인 00402416 위치에 브레이크 포인트를 설정함으로써 분석을 시작할 수 있다. 그 위치까지 코드가 실행되면 키 생성 함수를 호출한 함수로 코드 실행흐름을 따라간다. 그리고 계속해서 코드 실행을 따라가면서 아카이브 파일을 열어
00401670 위치의 함수를 호출하는 부분에 이르게 된다. 그 다음에는 바로 원하는 004019F0 위치의 함수를 호출한다.
리스트 6.6이 바로 004019F0 위치에 있는 함수의 디스어셈 블린 코드이다.
위 함수의 시작부분에서는 Cryptex 파일의 헤더를 메모리로 읽어 들이는 작업을 수행한다. 즉 ,파일의 오프셋 0에서부터 0x28바이트를 읽어들인다. 그 다음에는 00401030 위치의 함수를 호출한다. 리스트 6.7 00401030 위치에서 함수를 디스어셈블한 코드
다. 리스트 6.7 = Cryptex의 클러스터 복호화 함수
디렉토리 구조
파일 포맷 분석 페이지 6
위 함수는 아카이브 파일에서 4,104바이트의 데이터를 읽어 들인다. 그런데 흥미로운 것은 읽을 주소를 계산하는 방식이다.
함수의 파라미터로 전달받은 값에 4,104를 곱하고 거기에 0x28을 더한다. 그래서 edje은 값을 파일에서 데이터를 읽기 위한 파일 오프셋 값으로 사용한다.
이 사실에서 내부적으로 사용되는 데이터 블록의 크기가 4,104라는 것을 짐작할 수 있다. 그리고 0x28을 더한 것은 단순히 파일 헤더를 건너뛰기 위한 것이라고 생각할 수 있다.
따라서 위 함수에 전달되는 두 번째 파라미터는 함수가 파일에서 읽어야 하는 데이터 블록의 번호일 것이다.
파일의 내용을 메로리로 읽어 들인 후에는 CryptDecrypt 함수를 사용해서 읽어들인 데이터 블록을 복호화 한다. CryptDecrypt 함수로 전달되는 여섯 번째 파라미터(복호화 할 데이터의 길이) 값은 4104로 하드 코딩되어 있다.
CryptDecrypt 함수가 실패했을 떄 출력되는 에러 메시지를 보면 고정된 크기의 데이터 블록을 클러스터라 고 부르며, 함수는 클러스터를 읽어서 복호화한다는 것을 알 수있다. CryptDecrypt 함수가 성공하는 경우에는 복호화된 데이터 블록의 주소를 반환한다.
앞서 파일에서 읽은 데이터 블록은 아카이브 파일의 디렉토리나 디렉토리 헤더의 다른 부분일 것이라는 가정하에 분석을 수행횄다. 이제는 복호화된 데이터 블록을 이용해서 그것이 어던 구조로 이뤄졌는지 살펴볼 차례다.
다음은 Test1.crx 아카이브 파일에 포함된 파일 목록을 추출할 떄 Cryptex가 아카이브 파일에서 읽어 들여서 복호화한 메모리 블록의 내용이다.
위 메모리 블록에는 앞서 아카이브 파일에 암호화 해 포함시킨 asterisks.txt 파일 이름이 존재한다.
그렇다면 파일 이름 앞의 28바이트는 무엇일까? 이를 알아내기 위해서 즉시 수행할 수 있는 것은 해당 데이터를 다른 방법으로 바라보는 것이다.
앞에서는 파일 이름 문자열을 찾아보기 위해 아스키 뷰를 이용했지만 파일 이름 이외의 부분은 32비트 뷰를 이용해서 판독해 보자. 다음은 처음 28 바이트의 데이터를 32비트의 16진수 숫자로 표현한 것이다.
처음 3개의 DWORD는 확실히 어떤 구조체의 32비트 필드일 것이다. 그리고 나머지 네 개의 DWORD는 확실하지는 않지만 임의의 16바이트 데이터 일 것이다.
위 DWORD 값은 파일의 오프셋 값이나 어떤 포인터 값은 아니다. 그러기에는 값이 너무 크다. 파일 포맷 핵석과정을 좀 더 단순화 하기 위해서는 해당 데이터를 해석하는 코드를 추적해야 한다.
리스트 6.6의 코드는 파일 테이블을 실제로 읽어서 그 안에 포함된 파일 목록을 화면에 출력하므로 코드 내부적인 의미를 따로 분석하지 않아도 쉽게 의미를 파악 할 수 있다. 그럼 리스트 6.6의 코드로 다시 돌아가서 Cryptrex가 파일 엔트리를 읽어 어던 작업을 수행하는 지
살펴보자.
00401A60 MOV ESI, DWORD PTR SS:[ESP+10]
00401A64 ADD ESI,8
00401A67 MOV DWORD PTR SS:[ESP+14],1A
00401A6F NOP
00401A70 MOV EAX,DWORD PTR DS:[ESI]
00401A72 TEST EAX,EAX
00401A74 JE SHORT cryptex.00401A9A
00401A76 MOV EDX,EAX
00401A78 SHL EDX,0A
00401A7B SUB EDX,EAX
00401A7D ADD EDX,EDX
00401A7F LEA ECX,DWORD PTR DS:[ESI+14]
00401A82 ADD EDX,EDX
00401A84 PUSH ECX
00401A85 SHR EDX,0A
우선 복호화 된 데이터 블록의 주소를 ESI 레지스터에 저장하고 저장된 ESI 레지스터 값에 8을 더한다. 그리고 ESI 레지스터에 저장된 주소 위치의 32비트 값을 EAX 레지스터에 저장한다.
앞의 복호화된 데이터 블록을 보면 오프셋 +8 위치의 DWORD 값(세 번쨰 DWORD 값)이 00000001 이라는 것을 알 수 있다. EAX 레지스터에 읽어 들인 값이 0이 아닌지 확인하고 0이 아니면 흥미로운 산술 연산을 수행한다.
먼저 EDX 레지스터의 값을 왼쪽으로 0XA(10)비트 쉬프트 시킨 값에서 원래의 값(EAX 레지스터의 값)을 뺀다. 그리고 EDX 레지스터의 값에 자기 자신의 값(EDX 레지스터의 값, 이는 EDX 레지스터의 값에 2를 곱하는 것과 같다.)을 더한다.
00401A82 에서 한번 더 자신의 값을 더한 후에 오른쪽으로 0XA(10)비트 쉬프트 시킨다. 지금까지의 연산을 하나하나 다시 정리해 보면 다음과 같다.
1.EDX 레지스터의 값을 왼쪽으로 0XA(10) 비트 쉬프트한다. edx = edx x1,024(2의10승)
2.EDX 레지스터의 값에서 원래의 값인 EAX 레지스터의 값을 뺸다. edx=edxx1,024-edx=>edx=edxx1,023
3.EDX 레지스터의 값에 자기 자신의 값(EDX 레지스터의 값을) 을 더하는 연산을 두번 수행하는 것은 edx=edxx4와 같고 결국 edx=edx x 4,092(1,023x4) 연산이 된다.
4.EDX 레지스터의 값을 오른쪽으로 0xA(10)비트 쉬프트한다. 이는 1,024fh 나누는 것과 같으며, 최종적으로 연산식은 edx=edx x4092 / 1024가 된다.
여기서 Cryprex는 4,092를 곱하기 위해서 MUL 명령을 사용하지 않았고, 1,024를 나누기 위해서 DIV 명령을 사용하지 않았는지 의아하게 생각할 수 있다.
그것은 속도 때문 일 것이다.
MUL과 DIV 명령은 ADD,SUB,쉬프트 명령보다 상대적으로 실행 속도가 느린 명령이다. 따라서 Cryptex는 코드 실행 속도를 최적화하는 옵션으로 컴파일 됐다는 것을 알 수 있다.
MUL이나 DIV 와 같이 산술 명령을 직접 사용하면 코드의 크기는 작아지지만 그 만큼 실행 속도가 느려진다. 반면에 그런 산술 명령을 직접 사용하지 않으면 코드의 속도가 향상되지만 코드의 크기가 그만큼 증가한다.
위의 산술 연산 수행 결과는 현재의 파일 엔트리 정보를 출력하기 위해서 호출되는 printf 함수에 전달된다. 여기서 pritnf 함수가 어떤 내용을 출력할 것인지 알 수 있다. 그것은 바로 파일 크기다. 파일 크기는 화면에 킬로바이트(Kbyte) 단위로 출력된다.
따라서 산술 연산의 마지막에 1,024로 나눈 것은 단순히 파일 크기를 킬로 바이트 단위로 변환하기 위한 것임을 알 수 있다.
여기서 의문점은 데이터 블록에서 읽어들인 값은 무엇이고 왜 그값에 4,092를 곱했는가이다. Cryptex는 정해진 크기의 블록 크기 단위로 파일 크기를 관리하기 때문이고, 정해진 크기의 블록은 앞서 버퍼를 복호화하는 부분에서 본 클러스터이고 데이터 블록에서 읽어 들인
값은 클러스터의 개수 일 것이다. 그런데 앞서 본 클러스터의 크기는 4,104바이트 였는데 여기는 4,092 바이트로 크기가 동일하지않는다.
지금은 그 이유를 정확히 알 수 없다.
혀냊의 파일 엔트리 구조체의 오프셋 +8 위치에서 읽어 들인 것은 클러스터의 개수다.
따라서 오프셋 +8의 값은 클러스터의 단위의 파일 크기다. 그렇다면 파일의 실제 크기는 어디에 저장되는 것일까? 원래의 파일 크기를 모르면 암호화된 파일을 정확히 복원하는 것은 불가능할 것이다.
파일 엔트리 분석
파일 포맷 분석 페이지 7
따라서 오프셋 +8의 값은 클러스터의 단위의 파일 크기다. 그렇다면 파일의 실제 크기는 어디에 저장되는 것일까? 원래의 파일 크기를 모르면 암호화된 파일을 정확히 복원하는 것은 불가능할 것이다.
따라서 Cryptex는 아카이브 파일 어딘가에 실제 파일 크기를 저장해 놓아야만 한다.
Pritnf 함수는 파일의 크기뿐만 아니라 파일의 이름도 출력한다. 파일의 이름은 ESI 레지스터 값에 +14를 더한 위치에서 쉽게 얻을 수 있다. ESI 레지스터의 값은 앞에서 8이 더해졋다는 것을 기억하기 바란다.
따라서 파일 이름이 존재하는 위치는 데이터 블록의 오프셋 +1C가 된다.
파일이름과 크기를 출력한 후에 프로그램은 다시 다음 파일 엔트리 정보를 출력하기 위해서 루프를 돈다. 다음 파일 엔트리 정보의 위치를 계산하기 위해서 Cryptex는 ESI 레지스터의 값에 0x98(10진수로 152)을 더한다.
이는 각 파일 엔트리 정보의 크기가 152바이트임을 나타낸다.
그리고 파일 엔트리 정보 구조체 안에 파일의 이름을 저장할 수 있는 공간의 크기는 일정하다는 것을 알 수 있다. 파일 이름은 구조체 안에서 오프셋 +14 위치에 저장된다. 따라서 오프셋 +14 이후의 공간에는 추가적으로
다른 데이터 엔트리가 없을 것이라고 예상할 수 있다. 결구 저장될 수 있는 파일 이름의 최대 크기는 152-20, 즉 132바이트가 된다.
루프가 종료되면 흥미로운 작업이 수행된다. 읽은 데이터 블록의 첫 번째 DWORD 값이 0인지 확인하고 0이 아니면 Cryptex는 리스트 00401030 위치의 함수에 전달되는 두 번째 파라미터(00401030 위치의 함수는 이 값에 4104를 곱한다.)
는 데이터 블록의 첫번째 값을 이용한다. 이는 파일 목록 정보가 몇 개의 클러스터에 저장되고 그 클러스터는 링크드 리스트 구조로 이어진다는 것을 보여준다.
그렇다면 첫 번째 클러스터는 번호는 항상 1로 하드 코딩되는것일까?
이를 확인하기 위해 첫 번째 파일 목록 클러스터의 내용을 읽는 리스트 6.6의 코드를 읽어보자.
00401A1E MOV EDX,DWORD PTR DS:[406064]
00401A24 PUSH ECX
00401A25 PUSH EDX
00401A26 PUSH ESI
00401A27 CALL cryptex.401030
코드를 보면 첫 번째 클러스터의 인덱스 전역변수에서 가져온다. 00406064는 Cryptex의 헤더을 읽어서 저장하는 00406058 전역 변수의 일부분이다. 따라서 첫 번재 파일 목록 클러스터의 인덱스는 Cryptex 헤더의 오프셋 +0C 위치에 저장된다는 사실을 알
수 있다.
00401030 위치의 함수가 반환되면 코드는 ESI 레지스터 값이 0이 아닌지 검사(이미 앞에서 eSI 레지스터의 값을 확인했고 그 이후에 그 값이 바뀔 수 없음에도 불구하고)한다.
ESI 레지스터의 값이 0이 아니면 파일 테이블을 읽는 코드 부분으로 다시 돌아간다. 이제는 파일 테이블의 첫 번째 맴버는 다른 파일 테이블 엔트리 정보를 포함하고 있는 클러스터의 인덱스 번호라는 것을 알 수 잇다.
각 파일 엔트리의 크기는 고정되어 있으므로 하나의 클러스터에 포함될 수 있는 파일 엔트리의 개수도 한정된다. 지역 변수로 사용되는 [ESP+4]는 현재 클러스터에 남은 파일 엔트리의 개수를 나타내기 위해서 사용된다.
00401A67 위치의 코드를 보면 [ESP+4]의 값이 0X1A(26) 값으로 초기화 되는 것을 볼 수 있다. 이는 하나의 클러스터 안에 최대 26개의 파일 엔트리가 포함도릴 수 있다는 것을 의미한다.
00401A70 MOV EAX,DWORD PTR DS:[ESI]
00401A72 TEST EAX,EAX
00401A74 JE SHORT.cryptex.00401A9A
마지막으로 리스트 6.6에서 아직 살펴보지 않은 세 라인의 코드를 살펴보자.
위 코드는 파일 엔트리의 오프셋 +8 위치의 값이 0 인 경우의 처리 코드다. 앞에서도 살펴봤듯이 오프셋 +8 위치에는 클러스터 단위의 파일 크기가 저장되므로 파일 크기가 0인지 확인하는 것이다. 파일 크기가 0이면 다음 파일 엔트리로 건너뛴다.
Cryptex는 파일이 삭제되면 단순히 해당 파일 엔트리의 파일 크기를 0으로 만들어 버린다. 따라서 파일 크기가 0이라는 것은 해당 파일이 지워진 파일 이라는 것을 의미한다.
사용 보안 제품에서는 이런식으로 처리하지 않는다. Cryptex는 파일 정보가 들어있는 데이터 블록을 완전히 지우지 않으므로 자체적으로 보안 위협을 만들어 내고 있다.
물론 삭제됐다고 표시된 클러스터의 데이터는 여전히 암호화되어 있다.
하지만 여전히 민감한 정보를 포함하고 있다.
Cryptex을 이용해서 파일을 누군가에게 전달한다고 가정해 보자. 파일을 전달 받는 사람은 파일의 내용을 볼 수 있어야 하므로 패스워들 알 고 있을 것이다.
그런데 전달한 Cryptex 아카이브 파일 안에 이전에 지운 파일 정보가 포함되어 있다면 그것은 추가적으로 다른 파일들 까지 전달한 셈이 되는 것이다.
그렇다면 Cryptex 아카이브 파일에서 파일 목록을 추출하려면 어떻게 해야 할까?
그렇게 복잡하지는 않다. 다음은 Cryptex 아카이브 파일 안에 포함된 파일 목록을 추출하기 위해서 수행해야 하는 작업을 단계별로 설명하고 있다.
1.Crypto API를 초기화 하고 아카이브 파일을 연다.
2.헤더에서 8바이트의 시그너처를 검사한다.
3.입력한 패스워드를 SHA 해싱하고 그것을 다시 MD5 해싱한다.
4.MD5 해시 값이 Cryptex 헤더(오프셋 +18)에 저장된 MD5 해시 값과 동일한지 확인한다.
5.SHA 해시 객체를 이용해서 3DES 키를 만들어 낸다.
6.첫 번째 파일 목록 클러스터(클러스터 인덱스는 Cryptex 헤더의 오프셋 +0C 위치에 저장되어 있다.)를 읽어 들인다.(4,104 바이트를 읽어서 3DES 키를 이용해 복호화 한다.)
7.루프를 돌면서 각 152바이트의 파일 엔트리에서 오프셋 +8(파일 크기)의 값이 0이 아니면 각 엔트리의 이름(파일 이름)을 구한다.
8.다른 추가적인 파일 목록 클러스터가 존재하면 해당 클러스터를 읽어서 복호화한다. 그리고 그 안의 파일 정보를 추출한다.
디렉토리 구조 덤프
Cryptex의 장점은 아카이브 파일 안에 파일들을 암호화해서 저장하고 그 파일들을 다시 복호화해서 추출할 수 있다는 것이다.
Cryptex에 대한 커맨드라인 명령 옵션으로 x를 이용하면 아카이브 파일에서 해당 파일을 추출(파일 내용을 복호화해서)한다. 파일 추출 과정을 리버싱 하면 파일 목록 데이터 구조와 실제 아카이브 파일 안에 어떻게 저장되는지 알 수 있다.
리스트 6.8은 파일을 추출하는 루틴이다.
파일 추출 과정
파일 포맷 분석 페이지 8
파일 포맷 분석 페이지 9
리스트 6.8 함수가 수행하는 작업 중에서 중요한 작업만 간단히 살펴보자. 파일 추출 함수는 00401670 위치의 함수를 호출해서 아카이브 파일을 여는 것으로 시작한다.
00401670 위치의 함수는 아카이브 파일을 열어서 004011C0 위치의 함수를 호출한다.
004011C0 위치의 함수는 헤더를 검사하고 입력한 패스워드가 올바른지 확인한다. 00401670 위치의 함수가 반환되면 패스워들 검사하기 위해서 사용한 것과 동일한 종류의 해시 객체를 생성한다.
해시 객체를 생성하기 위해 사용한 알고리즘 타입은 0x8003(ALG_SID_MD5) 이다. 현재는 이 해시 객체의 용도를 알 수 없다.
그 다음에는 Cryptex의 헤더를 전역 변수인 00406058로 읽어 들이고 파일 목록에서 원하는 파일 엔트리를 검색한다.
파일 목록 검색은 004017B0 위치의 함수를 호출함으로써 이뤄지며, 004017B0 위치의 함수는 목록의 각 파일 이름과 추출할 파일 이름을 비교해서 원하는 파일 찾는다.
일단 원하는 파일 엔트리를 찾으면 해당 파일 엔트리에서 파일 내용을 추출하기 위한 정보를 몇 개 추출한다. 다음은 원하는 파일 엔트리를 찾았을떄 실행되는 코드다.
먼저 최적화된 산술 연산 작업을 수행하는 코드의 시작부분을 살펴보자. 좀 헷갈리는 부분이 LEA 명령을 사용하는 부분이다.
LEA 명령은 주소를 처리하는 명령이 아니다.
00401885 위치의 LEA 명령은 ESI 레지스터의 값에 5를 곱해서 그 결과 값을 EAX레지스터에 저장하는 것이다. 위 함수의 시작부분을 살펴보면 ESI 레지스터가 카운터로 사용됨을 알 수 있다.
처음에는 0으로 초기화되고 각 파일 엔트리를 거칠떄마다 1씩 증가한다. 그리고 현재 클러스터의 모든 파일 엔트리(한 클러스터에 최대 0x1A개의 엔트리가 존재할 수 있다)를 조사하면 ESI 레지스터의 값은 다시 0으로 설정된다.
이는 ESI 레지스터 값이 현재의 파일 엔트리에 대한 인덱스로서 사용된다는 것을 의미한다.
다시 산술 연산을 수행하는 코드로 돌아가서 그것이 무엇을 수행할여는 것인지 알아보자. 첫 번쨰 LEA 명령은 ESI 레지스터 값에 5를 곱하는 것임을 알았다.
그 다음에는 두 개의 ADD 명형이 뒤따른다. 이 또한 ESI 레지스터에 곱하기 연산을 수행하는 것이다. 결국에는 ESI 레지스터 값에 20을 곱하는 것이 되고, 그 다음 명령에서는 20을 곱한 결과 값에 원래의 ESI 레지스터 값을 빼고 있다. 따라서 최종적
인 연산 결과는 ESI 레지스터 값에 19를 곱한 값이 되고 0040188E의 명령에서 결과 값(EAX 레지스터의 값)을 사용한다. 즉, 현재 파일 엔트리의 인덱스인 ESI 레지스터의 값에 19X8 =152를 곱한 것이다.
뭔가 익숙한 숫자가 나오지 않았는가? 생각대로 152는 바로 파일 엔트리 구조체의 크기다. Creptex는 [ECX+EAX*8+8] 연산을 통해서 현재 파일 엔트리의 오프셋 +8 위치에 저장된 값을 구하고 있다.
오프셋 +8 위치의 값이 클러스터 단위의 파일 크기라는 것을 이미 알고 있다.
그리고 그 값은 전달받은 파라미터를 이용해서 함수 호출자에 전달된다. Cryptex는 파일 크기를 알아야만 파일 내용을 추출할 수 있기 떄문이다.파일 크기를 구한 다음에는 [ESP+28]의 파라미터를 통해서 다른 정보를 반환한다.
[ESP+28]로 전달되는 파라미터가 0이 아니면 Creptex는 파일 엔트리의 오프셋 +C 위치에서 총 16바이트를 [ESP+28] 파라미터로 복사한다. 이 16바이트는 파일 목록을 검색하는 것과 연관성이 없어 보인다. 마지막으로 위 코드는 파일 엔트리의 오프셋 +
4 위치의 값을 EAX 레지스터에 저장한 후 반환한다.
지금까지의 과정을 요약해보면 파일 목록을 검색해서 찾고자 하는 파일 이름을 포함하고 있는 파일 엔트리를 찾는다. 그리고 찾은 파일 엔트리에서 세 가지 종류의 정보를 함수 호출자에게 전달한다. 첫번째 는 파일 크기이고, 두 번째는 16바이트의 알 수 없는
데이터, 마지막으로는 파일 엔트리의 오프셋 +4 위치의 DWORD 값이다. 그럼 이제는 이 세가지 데이터가 파일 추출 루틴에서 어떻게 사용되는 지 분석해보자.
파일 목록 검색
004017B0 위치의 함수가 반화하면 Creptex는 파일 경롤르 제외한 파일 이름만을 추출한다. 이는 strchr C 런타임 라이브러리 함수를 이용해서 이뤄진다. Strchr 함수는 문자열에서 특정 문자를 찾아 그 주소를 반환한다. 여기서 루프를 돌면서 마짐가
백 슬래시 () 문자의 위치를 찾는다. 그리고 그 위치를 [ESP+20]에 저장한다. 이것이 바로 파일의 경로를 제외한 파일 이름의 주소가 된다. 이 과정에서 주의르 ㄹ끄는 명령이 있는데, 그 것은 바로 00401C9E 위치의 명령이다.
00401C9E MOV EDI,EDI
5장에서 위와 비슷한 명령을 본 기억이 있을 것이다. 그것은 윈도우의 시스템 API를 추적하기 용이하게 하기 위해서 구조적으로 사용된 경우였다. 이번 경우는 그런 용도로 사용된 것은 아니다. 그렇다면 컴파일러는 왜 아무런 작요도 하지 않는 이 명
령을 함수 중간 부분에 추가 한 것일까? 이유는 간단하다. 이 명령이 시작되는 위치의 주소가 32비트로 딱 나눠 떨어지는 주소가 아니기 떄문이다. 32비트 프로세서에서는 32비트로 나눠 떨어지지 않는 주소에서의 명령 실행이나 메모리 접근 경꼐의 주
소가 되게 조정하는 것이다. 위의 경우는 반복문이 수행되기 시작하는 코드를 32비트 경계 주소로 맞추기 위해서 삽입된 것이다. 물론 컴파일러는 NOP 명령을 이용해서 위 명령과 동일한 효과를 낼 수도 있다.
파일 경로에서 파일 이름을 추출한 다음에는 추출한 파일의 데이터를 저장하기 위한 파일을 생성한다. 그 다음에는 004017B0 위치의 함수가 실제로 원하는 파일을 찾아냈는지 확인하기 위해서 004017B0 위치에 있는 함수의 반환 값이 저장되어 있는
EBP 레지스터 값을 검사하여 0이면 Creptex는 파일을 찾지 못했다는 에러 메시지를 출력하고 실행을 종료한다. EBP 레지스터의 값이 0 이 아니면 Cryptex는 00401030dnlcldml 함수를 호출해서 해당 파일의 데이터를 읽고 복호ㅗ하를 수행한다. 00401030 위
치에 있는 함수의 두번째 파라미터에는 00401030 위치의 함수가 읽고 복호화를 수행할 클러스터 번호를 전달하는데, 여기서 EBP 레지스터의 값(004017B0 위치에 있는 함수의 반환 값)이 00401030 위치의 함수에 대한 두번째 파라미터로 전다로디고 있
다.
결국 004017B0 위치의 함수가 반환하는 값이 클러스터 번호라는 것을 알 수 있다. 또한 그 클러스터 번호가 해당 파일 데이터가 존재하는 클러스터의 인덱스나 첫 번재 인덱스(대부분의 경우 파일의 데이터는 하나 이상의 클러스터에 저장된다)일 것이
라는 예상을 어럽지 않게 할 수 있다. 파일 검색 함수의 코드를 다시 살펴보면 파일 엔트리의 오프셋 +4 위치의 값을 반환 값으로 전달하는 것을 볼 수 있을 것이다. (004018BC 위치의 코드를 보면 알 수있다.) 따라서 지금까지의 상황을 종합해보면
파일 엔트리의 오프셋 +4 위치 값은 파일의 데이터가 젖아된 클러스터의 인덱스라고 판단 할 수 있다.
파일 복호화
파일 포맷 분석 페이지 10
파일 엔트리의 오프셋 +4 위치 값은 파일의 데이터가 젖아된 클러스터의 인덱스라고 판단 할 수 있다.
디버거로 확인해 보면 이 함수의 세 번째 파라미터는 복호화된 파일데이터가 들어있는 버퍼에 대한 포인터라는 것을 알 수 있다.
그리고 함수가 반환되면 그 버퍼에는 파일의 데이터, 즉 * 문자들이 들어있는것을 확인할 수 있다. 그런데 여기서 주목해야 할 점은 버퍼의 * 문자들 앞에 4바이트 값인 0000046E 가 있다는 것이다.
이것은 10진수로는 1134이며, ASTERISKS.TXT 가 암호화되기 전의 실제 파일 크기와 동일하다.
보동 소수점 연산
리스트 6.8의 코드를 보면 첫 번째 파일 데이터 클러스터를 읽은 후에 상당히 낯선 명령들이 실행되는 것을 볼 수 있다. 그 명령들이 파일의 내용을 추출하는 과정에서 특별히 중요한 것은 아니지만(사실 가장 중요하지 않은 부분이다.) 그런 종류의 코드도 해석 할 수
있어야 하므로 자세히 볼 필요가 있다. 다음의 코드를 살펴보자.
00401D28 FILD DWORD PTR SS:[ESP+2C]
00401D2C JGE SHORT cryptex.00401D34
00401D2E FADD DWORD PTR DS:[403BA0]
00401D34 FDIVR QWORD PTR DS:[403B98]
00401D3A MOV EAX,DWORD PTR SS:[ESP+24]
00401D3E XORPS XMM0,XMM0
00401D41 MOV EBP,DWORD PTR DS:[MSVCR71.pritnf]
00401D47 PSUH EAX
00401D48 PUSH cryptex.00403308
00401D4D MOVSS DWORD PTR SS:[ESP+24],XMM0
00401D53 FSTP DWORD PTR SS:[ESP+34]
00401D57 CALL EBP
위 코드에는 지금까지 보지 못했던 명령이 몇 개 포함 되어 있다. (IA-32 Instruction Set Refernec) 를 보면 부동소수점 연산을 위한 명령이라는 것을 알 수 있다.
위의 FILD 명령은 [ESP+2C](파일의 전체 클러스터 수가 저장된 위치)에서 일반적인 32비트 정수를 복사해서 80비의 배정도 확장 부동소수점 수로 변환해 특별한 부동 소수점 스택에 저장한다. 부동 소수점 스택은 프로세서가 현재 사용중인 부동소수
점 수를 저장하는 레지스터들이며, 단순히 CPU가 관리하는 하나의 레지스터 그룹이라고 생각하면 된다.
다음으로 등장하는 부동 소수점 명령은 FADD다.
FADD는 [ESP+2C]의 값이 음수인 경우에만 실행된다. FADD 명령은 00403BA0에 저장된 부동 소수점 수를 현재 부동소수점 스택에 저장된 수에 더한다.
FILD 명령은 정수를 부동소수점 스택에 저장하는 반면에 FADD 명령은 메모리상의 부동 소수점 수를 이용한다. 따라서 00403BA0 위치의 값을 단순히 32비트 수로 덤프하면 부동소수점 수가 아닌 4F80000으로 보인다.
하지만 FADD 명령은 32비트 부동소수점 수를 인자로 사용하므로 이는 적절하지 않는다. OLLYDBG을 이용하여 00403BA0 위치의 값을 32 비트 부동소수점 수로 표현 하면 4.294967e+09가 된다.
이는 상당히 논리적이지 않은 것처럼 보이지만 그렇지 않다. 노련한 사람이라면 그 값이 2의 32승 값과 비슷하다는 것을 눈치 챌 것이다. 사실은 2의 32승 값과 비슷한 것이 아니라 똑같은 것이다.
원리는 간단하다. FILD 명령은 항상 정수를 부호 있는 정수로 처리한다. 하지만 부동 소수점 수로 변환될 정수가 원래의 프로그램에서는 부호 없는 정수로 선언되어 있을 수 있다. 이런 경우를 위해서 컴파일러는 변수의 최상위 비트의 값이 1이면 그 변수에 2의 32승 값
을 더하는 명령을 만들어서 CPU가 항상 부호 있는 정수를 제대로 처리하게끔 만들어 준다. 부동 소수점 스택안의 음수에 2의 32승 값을 더하면 올바른 양수 값으로 변환된다.
부동 소수점 수에 대한 보정 작업을 수행한 후에 Cryptex는 FDIVR 명령을 사용해서 00403B98 위치의 상수 값을 부동 소수점 스택 안의 수로 나눈다. 이번에는 64비트 부동소수점 수다. 이번에도 디버거로 확인하면
100.0을 전체 클러스터 수로 나누는 작업을 수행하는 것이다.
다음 명령에서는 ESP+24 에 있는 파일 이름 문자열 주소를 EAX 레지스터에 저장한다. 그리고 다른 생소한 명령인 XORPS 명령이 XMM0 인자와 함께 설명된다. 이 명령은 대부분의 IA-32 프로세서에 구현된 SSE2 라는 완전히 개별적인 명령 셋에 포함된 것이다.
SSE2 명령 셋은 동시에 여러 개의 오퍼랜드를 처리할 수 있는 SIMD 명령들을 포함한다. 이를 이용하면 수학적인 계산이 많이 요구되는 멀티미디어와 컨텐츠 생성 애플리케이션의 성능을 상당히 향상 시킬 수 있다. XMM0은 8개의 특별한 128비트 레지스터 중 첫 번째
레지스터다.
XMM0부터 XMM7까지 있으며, 이 레지스터들은 SSE 명령을 사용해야만 접근 가능하다. 그리고 그 레지스터의 내용은 일반적으로 여러 개의 작은 내용이 합쳐진 경우가 많다. 여기서 XORPS 명령은 첫 번째 SSE 레지스터의 내용에 대한 XOR 연산을 수행한다.
XMM0 레지스터 자신의 값에 대한 XOR 연산이므로 결구 XMM0 레지스터의 내용은 0으로 설정된다.
FSTP 명령은 부동 소수점 스택의 값을 [ESP+34] 위치에 저장하는 작업을 수행한다. 코드에 DWORD PTR이 명시되어 있으므로 메모리 주소는 32비트 데이터를 저장하는 공간으로 취급한다.
따라서 32비트 부동 소수점 형태로 변환한 후 메모리에 값을 저장한다. 다시 한번 언급하지만 현재 부동 소수점 스택에 저장된 값은 앞의 나누기 연산의 결과 값이다.
복호화 루프
이제 살펴볼 부분은 00401030 위치의 함수를 이용해서 파일 데이터가 저장된 클러스터들을 모 두 읽고 복호화하고, CryptHashData 함수를 사용해서 그 데이터를 해싱하고, 파일 데이터를 writeFile API를 이용해서 파일에 저장하는 부분이다.
여기서 부동 소수점 연산이 왜 수행됐는지 알 수 있다. 각 클러스터가 복호화될 때마다 Cryptex는 몇 %의 파일 데이터가 새로운 파일에 쓰여졌는지 소수점 숫자로 출력한다.
100.0을 전체 클러스터 수로 나눈 값이 바로 하나의 클러스터가 파일에 쓰여질 때마다 더해지는 %값인 것이다. 그리고 더해진 값을 현재의 진행률로 화면에 출력해 주는 것이다.
여기서 흥미로운 사실은 Cryptex는 어떻게 다음에 읽을 클러스터를 아는가이다.
Cryptex는 아카이브 파일에서 특정 파일을 지우는 기능을 제공하므로 파일 데이터가 항상 연속적으로 저장되어 있다고 보장할 수 없다. 따라서 Cryptex는 항상 00405050에서 다음에 읽을 클러스터의 인덱스를 읽어온다.
그리고 그 클러스터를 읽을 때 00401030 위치의 함수에 해당 클러스터의 인덱스를 파라미터로 전달한다. 00405050은 현재 활성화된 클러스터 버퍼의 시작 주소다. 파일 목록과 마찬가지로 파일 데이터가 저장된 클러스터의 첫 번째 DWORD 값에도 다음
클러스터의 인덱스 값이 저장되어 있다. ?

첨부파일

댓글목록

등록된 댓글이 없습니다.

Copyright © 7942.or.kr. All rights reserved. Master : Park Ki Young