본문 바로가기

리버싱/Lena's Reversing for Newbies

Reversing With Lena Tutorial 17

Lena의 리버싱 튜토리얼 17번째다. 다른 크랙미에서도 많이 볼 수 있는 KeygenMe 형태의 문제로 입력한 이름에 기반하여 시리얼을 생성하며 해당 시리얼이 무엇인지 추측하는 문제다. 먼저 프로그램을 실행시켜보면 다음과 같다.

간단하게 폼 두개와 버튼 두개로 이루어져 있다. About 버튼을 눌러보면 간단한 프로그램 안내 메시지가 뜨고 Check 버튼을 누르면 다음과 같은 메시지 박스가 나타난다.

아무래도 필드가 비어있으면 오류 메시지가 나타나는 것 같다. 위아래 필드에 아무 값이나 채우고 다시 한번 클릭해보니 다음과 같은 메시지 박스가 나타났다.

디버거를 하드 드라이브에서 지우라고 하고 있지만 그냥 하는 말이고 이것은 입력한 이름과 시리얼이 일치하지 않았을 때 나타나는 오류 메시지 박스일 것이다. 더이상 얻을 수 있는 정보는 없으므로 디버거를 열어 해당 메시지 박스의 문자열을 탐색해보자.

위와 같은 여러 문자열들을 찾을 수 있었다. 실패 메시지 박스에서 본 맨 마지막 문자열을 따라가보면 다음과 같은 성공, 실패 메시지 박스 분기를 찾을 수 있었다.

0x0040133C의 jne를 통해 성공, 실패 분기를 타는데 그 위의 cmp 명령어에 의해 여부가 결정되는 것 같다. 키젠에서 생성된 시리얼을 비교하는 것이기 때문에 둘 중 하나는 사용자가 입력한 시리얼, 하나는 사용자가 입력한 이름으로 생성된 문자열이라 추측할 수 있는데 이 생성 과정은 바로 위의 코드에 자세히 나와있다.

튜토리얼인만큼 코드는 복잡하지 않으며 다이얼로그의 텍스트 필드에서 이전에도 봤던 GetDlgItemTextA 함수를 이용해 값을 가져와 연산에 사용하고 있다. 해당 함수는 텍스트 필드에 입력된 문자열의 길이를 반환하기 때문에 0과 비교해서 0이라면, 즉 아무것도 입력되지 않았다면 위에서 본 메시지 박스를 출력하는 분기(0x004012DF)로 점프하고 있다. 문제가 없다면 0x004012F6으로 이동하여 0x00403038에 있는 문자열(현재는 잘려서 안보이지만 첫번째 GetDlgItemTextA 호출 시 사용된 매개변수다)의 길이를 lstrlen 함수를 이용하여 구하는 것을 볼 수 있다. 이때 이 문자열을 확인해보면 사용자가 입력한 이름(위쪽 필드)임을 확인할 수 있다.

이후 esi 레지스터끼리 xor 연산하여 0으로 초기화시킨 후 ecx 레지스터에 lstrlen 함수의 결과값인 이름 문자열("asdf")의 길이를 복사하고 eax 레지스터에는 1을 저장함으로써 반복문을 준비한다. 이후 코드를 실행시켜보면서 알게 되겠지만 eax 레지스터에 저장한 1은 첫번째 문자를 다루고 있다는 것이고 ecx 레지스터에 저장된 문자열의 길이 즉 4는 4번째 문자까지 반복하기 위해서 저장해두는 것이다. 0x00401333에서 'dec ecx'로 ecx 레지스터에 저장된 값을 감소시키는데 4번 감소시키면, 즉 이 시리얼 생성 코드를 4번 실행시키면 0이 되기 때문에 ZF가 세워져서 jne를 타지 않고 그대로 진행하여 cmp 명령어를 수행하게 된다. 그렇다면 0x00401309부터 0x00401334까지 어떤 일이 일어나고 있는지 살펴보도록 하자.

  • mov edx, dword ptr ds:[403038]: edx 레지스터에 0x00403038에 있는 데이터를 dword(4바이트)만큼 저장한다.

  • mov dl, byte ptr ds:[eax+403037]: 0x00403037 + eax에 있는 데이터를 byte(1바이트)만큼 dl 레지스터에 저장한다.

    • dl 레지스터는 edx 레지스터의 하위 1바이트기 때문에 레지스터 AABBCCDD의 DD 부분에 값이 저장된다.

    • 0x00403038에는 사용자가 입력한 이름 문자열이 저장되어 있다. eax 레지스터에는 1이 저장되어 있으므로 0x00403037 + 1은 0x00403038이며 이는 이름 문자열이 첫 글자(byte)를 참조하게 된다.

    • eax 레지스터의 값은 아래쪽의 'inc eax' 명령어에 의해 1씩 증가하기 때문에 첫 글자부터 순서대로 이름 문자열을 , 두번째 글자, 세번째 글자 등 이름 문자열을 순서대로 참조하게 된다.

  • and edx, FF: edx 레지스터와 0xFF를 AND 연산한다.

    • 바로 위의 명령어에서 dl 레지스터에 [eax+403037] 값이 복사되었는데 0xFF와 AND 연산하면 0x000000FF와 AND 연산하는 것과 동일하게 때문에 최하위 1바이트, 즉 dl 레지스터를 제외하고는 0과 AND 연산하는 것으로 0x00으로 초기화된다.

    • dl 레지스터에는 이름 문자열의 첫 글자가 저장되어 있으므로 edx 레지스터에는 해당 글자값만 남게 된다.

  • mov ebx, edx: ebx 레지스터에 edx 레지스터 값을 복사한다.

  • imul ebx, edx: ebx 레지스터에 ebx 레지스터와 edx 레지스터 값을 곱한 결과를 저장한다.

    • ebx, edx 레지스터에는 같은 값이 저장되어 있기 때문에 제곱한 결과를 저장하는 것과 다름없다.

  • add esi, ebx: 곱해진 ebx 레지스터 값을 esi 레지스터에 더한다.

    • 이 esi 레지스터는 계속 어떤 값이 더해지면서 매 반복마다 값이 누적되어간다.

    • 이렇게 누적된 esi 레지스터의 값은 0x00401336에서 사용자가 입력한 시리얼과 비교된다.

  • mov ebx, edx: edx 레지스터 값을 ebx 레지스터로 복사한다.

    • ebx 레지스터에는 제곱되기 전 원래 값이 저장된다.

  • sar ebx, 1: sar는 비트이동명령어로써 ebx 레지스터 값을 오른쪽으로 1비트 이동시킨다.

  • add ebx, 3: 비트이동된 ebx 레지스터에 3을 더한다.

  • imul ebx, edx: ebx 레지스터에 edx 레지스터 값을 곱한 결과를 저장한다.

  • sub ebx, edx: ebx 레지스터에서 edx 레지스터 값을 뺀다.

  • add esi, ebx: esi 레지스터에 ebx 레지스터 값을 더한다.

  • add esi, esi: esi 레지스터에 esi 레지스터 값을 더한다. 즉 값을 두배로 곱한다.

  • inc eax: eax 레지스터의 값을 증가시킨다.

    • [eax+403037]에서 사용되며 해당 명령어에서 다음 글자를 참조하게 된다.

  • dec ecx: ecx 값을 감소시킨다.

    • ecx 레지스터에는 lstrlen 함수로 구한 이름 문자열의 길이가 저장되어 있다.

    • ZF가 세워질 때까지 감소시키면서 카운터 역할을 수행한다.

위와같은 과정을 거치면서 esi 레지스터에는 곱하고 더해진 값들이 계속 쌓이게 되는데 이는 다른 키젠에서 이름 한 글자로 시리얼 한 글자를 생성하던 것과 달리 16진수로 연산하며 값을 누적시키기 때문에 아무 이름이나 입력했다가는 키보드로 입력할 수 없는 값이 시리얼로 생성될 수 있다. 위의 예시와 같이 이름을 "asdf"로 입력했다면 다음과 같은 시리얼이 생성된다.

 

esi 레지스터에는 0x00075584가 저장되어 있으며 1바이트씩 분할해보면 0x84, 0x55, 0x07을 입력해야 한다. 그러나 아스키 테이블을 보면 hex 값으로 84는 존재하지 않으며 55는 영문자 U, 7은 [BELL]이라는 예약어로 지정되어 있다. 즉 0x55 하나 말고는 전부 입력할 수 없는 값이기 때문에 사용자가 직접 복사해서 입력해넣거나 아니면 이름 문자열을 조정할 필요가 있다.

 

튜토리얼이기 때문에 꽤나 불완전한 키젠이라 이전과는 달리 어떤 이름에 대한 시리얼을 찾아냈다고는 할 수 없지만 키젠 로직을 파악한 것에 의의를 가질 수 있을 것이다. 그리고 해답을 보면 다음과 같이 해당 이름에 대한 키젠을 메시지 박스로 호출하는 것을 볼 수 있는데 이를 이전에 배웠던 코드 케이빙을 통해 구현해 보는 것을 최종 목표로 실습해볼 수 있다.

하지만 저렇게 메시지 박스로 출력되면 복사할 수도 없는 데다가 공백같은 걸 파악할 수 없기 때문에 조금 더 응용해서 SetDlgItemTextA 함수를 호출 및 필드 검증 로직을 없애서 시리얼 필드에 이름에 대한 시리얼 문자열이 나타나도록 구현했다.

생성된 시리얼을 원래 문제에 입력해보니 다음과 같이 성공 메시지 박스를 얻을 수 있었다.

 

 

이번 문제는 조금 당혹스러웠던 것이 키젠에서 생성한 시리얼을 필드에 입력할 방법이 떠오르지 않았기 때문이다. 실제로 튜토리얼을 따라해봐도 디버거에서 직접 값을 참조하여 아스키로 변환된 값을 필드에 복사하는 방식으로 입력하고 있었다. 그리고 입력 방식의 차이인지 다른 블로그에서도 사용했던 "lena151"에 대한 시리얼("R>")이 내 환경에서는 받아들여지지 않아 난감했었다. 다행히 "lena1515"에 대한 시리얼은 잘 입력되서 깔끔하게 마무리할 수 있었다. 아무래도 16진수 연산을 통해 시리얼이 계산되기 때문에 사용자 키보드에서 입력할 수 있는 범위를 자주 넘는 것 같다.

 

흥미로웠던 것은 GetDlgItemTextA 함수에 대응되는 SetDlgItemTextA 함수가 실제로 있었다는 것과 내가 이 함수를 따로 호출해서 사용할 수 있었던 점이었다.

프로그램 실행 후 마우스 오른쪽 클릭 -> 다음을 찾기 -> 현재 모듈 -> Names 메뉴를 클릭하니 위의 사진처럼 여러 모듈들과 함수 이름들이 제공되었는데 여기서 SetDlgItemTextA 함수 위치를 찾아서 프로그램 코드에서 GetDlgItemTextA 함수를 호출할 때 스택에 삽입하던 매개변수를 기반으로 똑같이 스택에 매개변수를 넣어주고 호출하니 잘 실행되었다. 물론 처음에는 시행착오가 있어서 함수의 매개변수에서 요구하는 LPCSTR, 즉 문자열이 위치한 주소를 넘겨줘야 하는데 esi 레지스터에 저장된 시리얼 값을 그대로 넘겨줘서 오류가 발생, 원인을 몰라서 그냥 포기할까 생각도 했었다. 하지만 GetDlgItemTextA 함수가 어떤 매개변수를 사용하는지 천천히 살펴보고 esi 레지스터에 저장된 값을 "mov dword ptr ds:[0x004031A8], esi" 처럼 메모리 공간에 복사해줌으로써 문자열이 위치한 주소를 넘겨줘서 정상적으로 함수를 호출할 수 있었다.

 

위의 메뉴를 찾은것도 순전히 운이고 esi 레지스터 값을 그대로 넘겨주는 게 아니라 값이 위치한 주소를 넘겨줘야 한다는 생각을 하지 못했다면 아마 키젠 로직만 파악하고 끝났을 것이다. 하지만 조금 더 살펴본 결과 실제로 함수를 사용할 수 있었기 때문에 단순히 튜토리얼 그 이상의 가치가 있었던 시간이었다. 리버싱에 지식이 부족해서 코드 케이빙을 통해 메시지 박스를 다시 호출하던 것처럼 프로그래밍 시 코드에서 사용한 함수만 다시 사용할 수 있을 줄 알았는데 이렇게 모듈에서 직접 호출해서 사용할 수 있다는 것도 새로 알게 되었다.