리버싱 핵심원리 1부 6장 정리
Note
본 게시글의 내용은 리버싱 핵심원리를 보며 복습 겸 정리하였습니다.
책의 내용과 일부 상이할 수 있고, 이해를 돕기 위해 강좌 형식의 말투와 적절한(?) 예시를 추가하였습니다.
본 게시글에서 사용되는 소스 코드와 파일, 프로그램은 리버싱 핵심원리에서 제공하는 파일과 언급되는 것들을 기반으로 하며, 일부 상이할 수 있습니다.
abex' crackme
abex' crackme는 리버스 엔지니어링의 패치 및 크랙 기술을 연습하기 위해 만들어진 프로그램입니다. 특정한 목표를 수행하도록 설계되어 있습니다. 국내 외 해외에서도 유명한 프로그램이기 때문에 다양한 풀이 방법이 존재합니다.
abex' 1st crackme
프로그램을 실행하면 "Make me think your HD is CD-Rom."
문자열이 작성된 메시지 박스가 나타납니다. 내 컴퓨터의 하드디스크(HD)를 CD-Rom으로 인식하도록 만들라고 하네요. 확인 버튼도 눌러봅시다.
"Nah... This is not a CD-ROM Drive!"
문자열이 작성된 메시지 박스가 나타나네요? 이 프로그램을 분석하기 위해 x64dbg로 열어봅시다.
디버깅 시작
한 눈에 봐도 작성된 어셈블리 명령의 수가 적습니다. 이는 abex' 1st crackme가 VC++과 같은 도구가 아닌 직접 어셈블리어를 통해 만들었기 때문에 그렇습니다. 덕분에 Stub Code가 보이지도 않죠.
어셈블리어로 만들어진 프로그램은 그 자체가 디스어셈된 코드라 분석하기에도 용이합니다.
이미지 1-3을 자세히 보시면 "Ok, I really think that your HD is a CD-ROM! :p"
라는 문자열이 확인됩니다. 내 하드디스크를 CD-ROM으로 인식시켰을 때 나타나는 성공 메시지 박스죠.
이미지 1-4에서 호출되는 API 목록을 확인할 수 있습니다. Win32 API로, 윈도우즈 프로그래밍을 좀 해보신 분이라면 어떤 함수인지 아실겁니다.
GetDriveTypeA()
함수로 드라이브의 유형을 취득한 후 이를 CD-ROM으로 인식하도록 수정하면 우리가 원하는 성공 메시지 박스가 분명 나타날겁니다.
코드 분석
abex' 1st crackme 프로그램에 작성된 어셈블리 명령은 크게 어렵지 않게 되어있습니다.
MessageBoxA()
push
명령을 통해 MessageBoxA()
함수에 전달될 매개변수를 스택 메모리에 삽입(push)하고 있습니다. 함수 호츌 규약에 따라 매개변수는 역순으로 쌓입니다.
0x40100C
주소에 작성된 push 0
은 HWND hWnd
매개변수를 나타냅니다. 여기서 0의 값이 삽입되고 있는데 NULL
을 전달하는 것으로, 부모 창이 없음을 나타냅니다.
0x401007
주소에 작성된 push 402012
는 LPCTSTR lpText
매개변수를 나타냅니다. 메시지 박스의 내용으로 출력될 문자열의 메모리 주소를 의미합니다.
0x401002
주소에 작성된 push 402000
은 LPCTSTR lpCaption
매개변수를 나타냅니다. 메시지 박스의 제목으로 출력될 문자열의 메모리 주소를 의미합니다.
0x401000
주소에 작성된 push 0
은 UINT uType
매개변수를 나타냅니다. 메시지 박스의 아이콘이나 버튼 유형을 나타내는데, 0의 값은 MB_OK
로 확인 버튼만 출력하도록 합니다.
MessageBox.cpp | |
---|---|
어셈블리 명령을 C++ 언어로 복원하면 대략 위와 같다고 볼 수 있습니다.
GetDriveTypeA()
push
명령으로 GetDriveTypeA()
함수에 전달될 매개변수 LPCTSTR lpRootPathName
을 스택 메모리에 삽입하고 있습니다. "C:\\"
의 문자열이 확인되는 것으로 보아 C 드라이브를 대상으로 하고 있음을 알 수 있습니다.
GetDriveTypeA()
함수 호출 시 반환값으로 DRIVE_FIXED
가 eax
레지스터에 저장됩니다. DRIVE_FIXED
는 3이라는 상수 값을 갖고 있으며, 고정식 미디어 장치임을 의미합니다. 설정에 따라 달라질 수 있지만 대부분의 하드디스크의 경우 이 값이 반환됩니다.
기타
값을 1씩 증가 또는 감소시키는 inc
와 dec
명령이 확인되고, 중간에 jmp
명령이 확왼됩니다.
inc esi
명령이 3개 작성되어 있고, dec eax
명령은 2개 작성되어 있습니다. 위 명령을 모두 실행하면 esi
의 값은 3이 되고, eax
의 값은 1이됩니다.(1)
- 제 PC의 경우
esi
레지스터의 값으로401003
이 최종적으로 저장되었습니다. PC에 따라 값이 다를 수 있습니다.
중간에 jmp
명령이 끼어있는데 무시하셔도 되는 명령입니다. 왜냐하면 바로 그 아래로 이동하라는 점프(이동) 명령인데 있으나 마나한 분석을 방해하는 코드이기 때문입니다. 위와 같은 jmp
명령의 경우 코드 분석을 방해하기 위해 추가된 반동분자 같은 코드라 보시면 되겠습니다.
조건
cmp
는 Compare의 약자로, eax
와 esi
레지스터의 값을 서로 비교하는 명령입니다. 그 아래에 je
명령이 작성되어 있는데, eax
와 esi
레지스터의 값을 서로 비교하여 두 값이 같으면(Jump if Equal) 0x40103D
주소로 이동하라는 뜻입니다. 값이 서로 같지 않으면 그 아래에 있는 명령을 실행합니다.
esi
레지스터에 3, eax
레지스터에 1이 저장되어 있고 두 값은 서로 다르기 때문에 0x40103D
주소로 이동하지 않고 그 아래에 있는 명령을 실행합니다.
실패 MessageBoxA()
MessageBoxA와 같이 push
명령을 통해 매개변수를 스택 메모리에 삽입한 후 호출하여 전달하고 있습니다. 여기서 호출되는 MessageBoxA()
는 CD-ROM으로 인식 실패 시 나타나는 메시지 박스입니다.
0x40103B
주소에 있는 jmp
는 실패 메시지 박스를 띄운 후 0x401050
으로 이동하여 ExitProcess()
호출 후 프로그램을 종료합니다.
성공 MessageBoxA()
MessageBoxA와 같이 push
명령을 통해 매개변수를 스택 메모리에 삽입한 후 호출하여 전달하고 있습니다. 여기서 호출되는 MessageBoxA()
는 CD-ROM으로 인식 성공 시 나타나는 메시지 박스입니다.
성공 메시지 박스를 띄운 후 ExitProcess()
를 호출하여 프로그램을 종료합니다.
패치
방법
성공 메시지 박스를 호출하도록 하는 방법은 여러가지가 있습니다.
단순히 jmp
명령을 이용해 바로 성공 메시지 박스를 호출하거나, 일부 명령을 nop
으로 바꾸어 실행하지 않거나 등 매우 다양한 방법들이 존재합니다. 이중 어느 방법을 사용할지는 본인의 선택입니다. 본 게시글에선 jmp
명령을 이용해 바로 성공 메시지 박스를 나타내도록 하겠습니다.
jmp
0x401026
주소에 있는 je 40103D
명령을 jmp 40103D
로 변경합니다.
이렇게 변경하면 GetDriveTypeA()
로 함수의 결과를 받든 말든, cmp
로 비교를 하든 말든 상관없이 바로 성공 메시지 박스를 나타나도록 할 수 있습니다.