본문 바로가기

챌린지/CrackMe

Abex's 5th Crackme

Abex의 크랙미 중 마지막 작품이다. 따로 설명이 필요할 정도로 복잡한 프로그램이 아니며 이전과 비슷하게 시리얼 키를 입력해서 비교하는 방식으로 작동한다.

프로그램을 실행시키면 간단하게 텍스트 필드와 버튼이 하나 뜬다. 다행히 비주얼 베이직으로 만든 프로그램은 아니고 델파이와 터보 링커를 활용하여 개발된 프로그램이다. 일단 바로 Check 버튼을 눌러보면 시리얼이 틀렸다는 메시지와 함께 프로그램이 곧바로 종료된다.

시리얼에 아무런 값을 입력하지 않아도 동일하게 메시지 박스가 나타난 후 프로그램이 종료된다. 더이상 얻을 정보가 없으므로 일단 디버거를 켜서 확인해보도록 하겠다.

엔트리 포인트에서 머지않아 금방 다이얼로그 박스를 그리는 함수를 호출하는 것 같다. 아직은 별다른 중단점을 설정하지 않았기에 Check 버튼을 누르면 금방 프로그램이 꺼져버린다. 일단 아까 메시지박스에서 찾은 "The serial you entered is not correct!" 라는 문자열을 탐색해보자.

실패, 성공 문자열과 이상한 문자열 두 개가 보인다. 저건 무엇일까? 일단 성공, 실패 문자열을 확인해보기로 했다.

해당 문자열 근처의 코드를 확인해 보니 cmp 구문으로 비교 후 je를 통해 성공, 실패로 갈리는 것을 알 수 있었다. 틀린 시리얼 코드를 입력하면 eax 레지스터에는 1값이 들어가서 je가 작동하지 않아 실패 메시지 박스를 띄우고 종료한다. 이전처럼 단순히 je를 jne나 jmp로 어셈블하면 성공 메시지 박스를 띄우고 종료하겠지만 그런건 별로 의미가 없고 이 시리얼 키가 어떻게 생성되는지를 분석하는 게 중점이 될 것이다. 우선 "4562-ABEX", "L2C-5781" 문자열이 사용되는 코드를 조사해보았다.

생각보다 가까운 곳에서 사용되고 있었는데 아까 성공, 실패를 판단하는 코드 바로 위쪽이었다. 즉 사용자가 시리얼을 입력하고 버튼을 누르면 프로그램은 사용자가 입력한 내용을 확인한 후 어떤 로직에 따라 시리얼을 생성하고 입력값과 해당 시리얼을 비교, 성공 또는 실패를 판단할 것이라 추측할 수 있었다. 이를 확인해보기 위해 사용되는 함수를 조사한 결과 다음과 같은 목록을 얻을 수 있었다.

  • GetDlgItemTextA

  • GetVolumeInformationA

  • lstrcatA

  • lstrcmpiA

  • MessageBoxA

  • EndDialog

마지막 두 함수는 성공, 실패 문자열을 포함한 메시지 박스를 띄우고 프로그램을 종료하는 역할을 수행할 것이다. 그렇다면 나머지 4개 함수들은 어떤 동작을 수행하고 있을까? 제일 먼저 호출되는 GetDlgItemTextA의 경우 사용자가 입력한 시리얼을 읽어서 매개변수로 받은 위치에 저장하고 있었다. 즉 함수 이름대로 다이얼로그의 아이템(텍스트 박스)의 텍스트를 가져오는 것이다.

함수 실행 결과 매개변수로 전달되었던 0x00402324에 사용자가 입력한 문자열이 저장된 것을 볼 수 있다.

 

그 다음으로 실행되는 GetVolumeInformationA 함수는 여러 상수와 포인터를 매개변수로 받아 해당 포인터가 가리키는 곳에 어떤 값을 넣고 있었다.

하지만 값을 살펴봐도 별다른 정보를 찾을 수 없었는데 좀 더 자세한 확인을 위해 함수 자체를 검색해보니 지정된 루트 디렉토리의 파일 시스템과 볼륨에 대한 정보를 얻어오는 Win32 API 함수인 것을 알 수 있었다. 문서를 기반으로 함수의 매개변수 및 역할을 확인한 결과 다음과 같은 정보를 찾을 수 있었다.

BOOL GetVolumeInformationA(
  LPCSTR  lpRootPathName,
  LPSTR   lpVolumeNameBuffer,
  DWORD   nVolumeNameSize,
  LPDWORD lpVolumeSerialNumber,
  LPDWORD lpMaximumComponentLength,
  LPDWORD lpFileSystemFlags,
  LPSTR   lpFileSystemNameBuffer,
  DWORD   nFileSystemNameSize
);
  • lpRootPathName: 정보를 확인할 볼륨의 최상단 디렉토리를 나타내는 문자열의 포인터. NULL 값을 전달하면 현재 디렉토리의 최상위 디렉토리가 해당되며 대부분의 경우(프로그램을 D 드라이브에서 실행시키지 않는이상) C 드라이브가 지정될 것이다. 현재 함수 호출에서는 0x00, NULL 값이 전달되어 C 드라이브를 확인하고 있다.

  • lpVolumeNameBuffer: lpRootPathName에서 지정된 볼륨의 이름을 저장할 버퍼의 위치(포인터)를 지정한다. 버퍼의 크기는 nVolumeNameSize 파라미터로 지정된다. 현재 함수 호출에서는 0x0040225C가 이에 해당한다.

  • nVolumeNameSize: 볼륨의 이름을 저장할 버퍼의 크기를 지정한다. 현재 함수 호출에서는 0x32를 전달하여 50 TCHAR만큼 사용하고 있다.

  • lpVolumeSerialNumber: 볼륨의 시리얼 넘버를 저장할 변수의 포인터를 지정한다. 이때 시리얼 넘버는 제조사에서 부여하는 게 아닌 운영체제가 하드디스크를 포맷할 때 부여하는 번호다. 시리얼 넘버가 필요하지 않다면 NULL을 저장할 수 있다. 현재 함수 호출에서는 0x00402194가 이에 해당한다.

  • lpMaximumComponentLength: 지정된 파일 시스템에서 지원하는 파일 이름 컴포넌트(file name component)를 저장할 변수의 크기를 지정한다. 이는 특정 파일 시스템에서 지원하는 파일 이름의 최대 크기를 나타내는 데 예를 들어 FAT 시스템에서는 파일 이름이 최대 255글자까지 가능했다면 NTFS에서는 더 긴 파일 이름을 붙여줄 수도 있다. 현재 함수 호출에서는 0x00402190가 이에 해당한다.

  • lpFileSystemFlags: 지정된 파일 시스템과 관련된 플래그들이 저장될 변수의 포인터를 지정한다. 현재 함수 호출에서는 0x004020C8가 이에 해당한ㄷ.

  • lpFileSystemNameBuffer: 지정된 파일 시스템의 이름을 저장할 변수의 포인터를 지정한다. 현재 함수 호출에서는 0x00으로 NULL을 넘기고 있다.

  • nFileSystemNameSize: lpFileSystemNameBuffer의 최대 크기를 지정하며 현재 함수 호출에서는 0x00으로 NULL을 넘기고 있다.

함수의 반환값은 함수가 성공적으로 실행되었다면 0이 아닌 값이 반환된다. 그러므로 함수의 반환값을 살펴보기보다는 NULL로 전달되지 않은 매개변수를 살펴보는 편이 좋을 듯 하지만 일단 전체적인 흐름을 파악한 후에 해도 늦지 않을 것이다.

 

그 다음으로 호출되는 함수는 lstrcatA로 역시 Win32 API이며 문서를 참조한 결과 첫 번째 매개변수로 전달된 문자열에 두 번째 매개변수로 전달된 문자열을 붙인다(concatenate).

전달된 매개변수는 간단하게 빈 문자열 하나(0x0040225C)와 "4562-ABEX"라는 미리 생성된 문자열(0x004023F3)이 전달된다. 이때 0x0040225C는 위의 GetVolumeInformationA 함수에서 매개변수로 전달된 lpVolumeNameBuffer로 드라이브의 이름이 들어있어야 한다. 그러나 현재 이 크랙미를 실행하고 있는 컴퓨터의 C드라이브는 이름이 없기 때문에 빈 문자열이 된 것이다. 결과적으로 0x0040225C에는 "4562-ABEX"라는 문자열이 저장된다.

 

그 다음으로 눈여겨볼 것이 간단한 루프가 하나 있는데 이는 다음과 같다.

이번에도 0x0040225C, 즉 lpVolumeNameBuffer를 참조하고 있는데 해당 포인터에서 C, D, E, F를 참조하여 한 글자씩 4개를 참조하고 있다. 디버거의 도움으로 살펴본 결과 "4562-ABEX" 문자열의 첫 네 글자를 의미하는 것을 알 수 있다. 여기에 add 명령어를 통해 값을 1씩 증가시키고 있는데 dl 레지스터에 2가 저장되어 있고 4개의 add 명령어 다음에 dec dl 명령어를 통해 dl 레지스터를 1씩 감소시키기 때문에 이 블록은 총 두번 실행된다. 아스키 코드 특성상 영숫자는 연속적으로 표현되어 있기 때문에 4, 5, 6, 2 각 글자를 1씩 증가시키면 그 다음 글자인 5, 6, 7, 3이 되며 한번 더 증가시키면 6, 7, 8, 4가 된다. 두 번의 반복을 마쳤으면 0x004010AF로 점프하지 않고 다음 코드를 실행한다.

 

다음 코드도 위의 lstrcatA와 동일하게 문자열을 이어붙인다. 이번에는 0x00402000은 딱히 함수에서 사용된 적도, 어떤 값을 담고 있지도 않으며 0x004023FD는 "L2C-5781"이라는 문자열을 가지고 있다. 실행하면 별 문제없이 0x00402000에 "L2C-5781"이라는 문자열이 저장된다.

 

다음 코드도 위의 lstrcatA와 동일하게 문자열을 이어붙인다. 이번에는 0x00402000, 즉 "L2C-5781"에 0x0040225C, 즉 "6784-ABEX"를 이어붙인다. 결과적으로 "L2C-57816784-ABEX" 라는 문자열이 0x00402000에 저장된다.

 

최종적으로는 lstrcmpiA 함수를 이용해 문자열을 비교하는데 지금까지 이어붙였던 0x00402000, 즉 "L2C-57816784-ABEX"와 초반에 GetDlgItemTextA 함수로 얻어온 0x00402324, 즉 사용자가 입력한 시리얼을 비교하는 것을 알 수 있다. 현재는 두 문자열이 일치하지 않기 때문에 lstrcmpiA 함수가 1을 반환하고 이는 아래의 cmp eax, 0를 거쳐 성공, 실패 분기에서 실패 분기를 진행하게 된다.

 

그렇다면 디버거에서 보이는 올바른 시리얼을 입력해보면 어떻게 될까?

올바른 시리얼을 입력했다는 성공 메시지 박스와 함께 프로그램이 종료되는 것을 알수 있다. 지금 이 크랙미를 실행하는 컴퓨터에는 C 드라이브 하나밖에 없기 때문에 다른 드라이브에서 실행해볼 때 어떻게 되는지 확인해볼 방법이 없지만 위에서 확인한 함수들을 활용하는 키젠만 잘 작성한다면 문제없이 동작할 것이라 생각한다.

GetVolumeInformationA() 함수를 사용하기 위해 부득이하게 콘솔 C++로 키젠을 제작하였다.

실험삼아 키젠을 작성한 결과 올바른 시리얼 키를 파일로 출력하는 것을 확인할 수 있었다.

 

 

이렇게 Abex의 모든 Crackme에 대한 분석이 끝났다. 예전에도 몇번 리버싱을 공부하려다가 학교 공부때문에 그만둔 적이 있었는데 그때도 꼭 다루는게 이 Abex Crackme였다. 그만큼 유명하고 잘 만들어진 크랙미여서 그런지는 몰라도 아직까지 많이 언급된다는게 놀랍긴 하지만 2000년대즈음에 만들어졌기 때문에 너무 오래된 감이 없지않아 있기 때문에 여기에 안주하지 않고 얼른 다른 크랙미도 풀어봐야 할 것 같다. 그래도 두 번째, 네 번째 크랙미를 풀면서 비주얼 베이직에 조금 익숙해진 것 같아서 뿌듯하다.

'챌린지 > CrackMe' 카테고리의 다른 글

Abex's 4th Crackme  (0) 2020.10.25
Abex's 3rd Crackme  (0) 2020.10.23
Abex's 2nd Crackme  (0) 2020.09.29
Abex's 1st Crackme  (0) 2020.08.29