Reversing.kr의 세 번째 문제인 Easy Unpack이다. 이전 문제인 Easy Keygen보다 약간 적은 3000명 정도의 사람들이 푼 문제로 언패킹에 관련된 문제다. 언패킹은 보통 OEP(Original Entry Point)를 찾아서 덤프를 뜨고 IAT를 복구하는 것이 목적인데 언패킹된 파일을 워게임 사이트에 업로드할 수도 없는 노릇이기 때문에 이번 문제에서는 이 프로그램의 OEP를 찾아 사이트에 인증하도록 설명하고 있다.
일단 프로그램을 실행시켜보면 다음과 같은 단순한 창이 뜨는데 뭘 눌러도 아무런 반응이 없기 때문에 정보를 얻어낼 수 없었다.
DIE로 확인해보아도 별다른 패커 정보를 찾을 수 없었다.
그렇기때문에 일단 프로그램을 x32dbg로 실행시켜보았다.
0x0040A04B에서 프로그램이 시작되는 것을 볼 수 있었다. 패킹된 프로그램인 만큼 이곳이 OEP는 아닐 것인데 일단 하나씩 실행시켜보면서 분석하기로 했다.
얼마되지않아 반복되는 코드 블록을 찾을 수 있었는데 0x0040A099부터 0x0040A0C1까지 중간에 조건에 따라 탈출하는 반복문을 수행하는 것을 확인할 수 있었다. xor 명령어를 통해 ecx 레지스터 값에 각각 특정 값을 연산하고 ecx 레지스터를 증가시킨 후 ecx, edx 레지스터를 비교하여 탈출 조건을 검사하는데 아마 암호화된 코드를 복호화하는 과정이 아닐까 생각한다. 5 바이트씩 0x10, 0x20, 0x30, 0x40, 0x50으로 각각 xor 연산하여 복호화하다가 암호화된 코드를 다 복호화했다면 중간에 0x0040A0C3으로 점프하여 탈출하고 그렇지 않다면 다시 0x0040A099로 돌아가서 반복하는 것이다. 레지스터를 살펴보면 0x004094EE까지 도달해야 탈출하기 때문에 일일히 확인하기에는 너무 오래 걸린다. 그래서 0x0040A0C3에 중단점을 걸고 실행하여 스킵한 후 다음 코드를 분석했다.
조금 아래에서는 VirtualProtect란 함수를 호출하는 것을 확인할 수 있는데 MSDN에 따르면 이 함수는 현재 프로세스의 메모리 영역(region of pages)의 보호 상태를 설정하는 함수로 다음과 같은 매개변수를 가지고 있으며 의미는 다음과 같다.
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
-
lpAddress: 보호 상태를 변경할 영역의 시작 주소. 0x00405000이 전달되었다.
-
dwSize: 보호 상태를 변경할 영역의 바이트 크기. 0x1000이 전달되었다.
-
flNewProtect: 보호 상태 옵션. 옵션 상수 값은 MSDN에 나와있다. 0x4가 전달되었다.
-
lpflOldProtect: 보호 상태가 변경된 영역의 이전 보호 상태를 저장할 변수의 위치(포인터). 0x0040A678이 전달되었다.
그렇다면 함수 실행 전후로 매개변수로 전달된 공간의 변화를 살펴보자. 먼저 lpAddress로 전달된 0x00405000은 데이터의 별다른 변화를 볼 수 없었다. 메모리 영역의 보호 상태만 변경하는 것이기 때문에 그런 것 같은데 그렇다면 이전 보호 상태를 저장하는 0x0040A678에는 어떤 값이 저장됐는지 확인해보았다.
0x02 값이 저장되었는데 이는 MSDN을 확인해보면 PAGE_READONLY, 읽기만 가능한 영역이었던 것을 알 수 있다. 매개변수로 전달된 0x4은 PAGE_READWRITE로 이 함수에서는 0x00405000 메모리 영역을 읽기만 가능한 상태에서 읽고 쓰기가 가능한 상태로 변경하는 작업을 수행함을 확인할 수 있다. 이 메모리 영역을 읽고 쓸 수 있는 상태로 변경하는 이유는 언패킹 시 복호화된 소스 코드가 쓰여질 공간이 아닐까 추측할 뿐이었다. 아직까지는 알 수 없기 때문에 좀 더 진행해보기로 했다.
그 다음에도 위처럼 반복되는 코드 블록을 찾을 수 있었는데 레지스터 간 연산을 통해 값을 증가시키고 비교하며 0x0040A109, 0x0040A10F, 0x0040A115 총 3단계에 걸쳐 복호화 과정을 수행하고 있다. 지금으로선 의미를 파악할 수 없기 때문에 0x0040A117에 중단점을 걸고 스킵하여 넘어가기로 했다.
아래쪽에서도 LoadLibraryA()라는 함수를 호출하는 부분을 발견했는데 이때 스택에 삽입되는 edx 레지스터에는 "USER32.dll" 문자열의 주소가 담겨 있었다. 이 "USER32.dll"이 저장되는 곳의 주변을 살펴보니 해당 모듈에서 사용하는 듯한 여러 함수의 이름이 적혀있는 것을 확인할 수 있었다. 혹시 이 부분도 패킹되면서 암호화되었다가 언패킹되면서 복호화된 데이터가 아닐까 생각해서 프로그램을 다시 실행시키고 이번에는 edx 레지스터가 가리키고 있는 0x0040912B를 지켜보았다.
확인결과 맨 처음에 봤던 복호화 로직이 이 모듈 이름과 함수 이름을 복호화하는 부분이었다는 것을 확인할 수 있었다. 이렇게 복호화된 문자열은 추후 여러 용도로 사용될 텐데 진행해보면서 확인하기로 했다.
그 다음으로는 작은 반복문에서 edx 레지스터 값을 0과 비교하고 그렇지 않으면 inc 명령어로 1씩 증가시키고 있는데 이는 0x00, 즉 null로 종결되는 문자열의 끝을 파악하기 위함이라 추측된다. 그래서 "USER32.dll"이란 문자열 다음까지 edx 레지스터를 증가시킨 후 0x0040A13E로 이동한다. 이때 edx 레지스터는 애매한 값을 가리키고 있는데 이는 다음 로직에서 교정되어 사용된다.
edx 레지스터에 대해 inc, add 연산을 사용한 후 가리키는 값을 보면 "DefWindowProcA" 문자열을 가리키고 있다. 왠지 함수 이름 같은데 이를 어떻게 사용하고 있는 것일까? 아래에서 호출되는 GetProcAddress()라는 함수를 살펴보면 이를 알 수 있다.
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
-
hModule: 해당 함수나 변수를 포함하고 있는 DLL에 대한 핸들. user32.75D30000이 전달되었다.
-
lpProcName: 해당 함수나 변수의 이름, 혹은 순서(ordinal) 값. "DefWindowProcA"가 전달되었다.
함수 매개변수에 비해 많은 값이 스택에 삽입되었지만 스택의 제일 위에 있는, 즉 가장 늦게 삽입된 값인 edx 레지스터와 ebx 레지스터가 가리키는 값을 보면 각각 0x0040913A, "DefWindowProcA" 문자열의 위치와 user32.75D30000인 것을 볼 수 있다. GetProcAddress() 함수는 해당 DLL에서 특정 이름의 함수나 변수의 주소를 얻어오는 함수로 위에서 전달된 매개변수로 볼 때 USER32.dll에서 DefWindowProcA() 함수의 주소를 얻어오는 것이라 추측할 수 있다.
이후 스택에서 사용되지 않은 나머지 값들은 pop으로 인출하면서 레지스터를 함수 호출 이전으로 복구시킨다. 함수의 반환값은 0x76F36640이며 eax 레지스터에 저장되었는데 이 주소값은 ecx 레지스터가 가리키는 곳(0x004050A4)에 저장된다. 왠지 이곳에 다른 함수들의 주소도 저장될 것이라 추측하여 별도의 덤프창에서 이를 감시해보면 좋을 것이다. 그 다음에는 아까 "USER32.dll" 문자열에서 종결 널 문자까지 이동하도록 레지스터 값을 증가시켰던 것처럼 edx 레지스터의 값을 증가시키고 널 문자와 비교하여 점프를 통해 다음 명령어로 이동한다.
다음 명령어에서는 다시 edx 레지스터를 증가시키고 0x004094EC와 비교하여 분기를 타는데 현재 edx 레지스터에서는 함수 이름의 널 종결 문자 다음을 가리키고 있다. 그렇다면 cmp 명령어로 비교되는 0x004094EC는 무엇일까? 확인해보니 사용되는 함수 이름 문자열 목록중에서 마지막 문자열의 널 종결 문자 다음을 가리키는 것을 알 수 있었다. 즉 해당 DLL에서 사용하는 모든 함수의 이름에 대해 GetProcAddress() 함수를 호출하여 주소값을 얻어와서 저장하는 과정이 반복문을 통해 수행된다고 추측해볼 수 있었다. 현재 분기에서는 0x0040A11D로 이동하는데 이는 위에서 라이브러리 모듈을 불러오는 LoadLibraryA() 함수가 호출된 부근의 코드로써 즉 다음과 같은 반복문이 진행중이라 추측할 수 있겠다.
while( isLastFunction == false )
{
DLL = LoadLibraryA(...);
FunctionAddr = GetProcAddress(DLL, FunctionName);
Save(FunctionAddr);
GetNextName(FunctionName);
}
아까 불러왔던 DefWindowProcA() 함수의 주소값이 저장된 곳 주변을 감시해보면서 반복문을 계속 수행해보자.
빈 공간이던 0x004050A4 부근의 메모리에 함수의 주소값이 하나하나 쓰여지는 것을 확인할 수 있었다. 아까 VirtualProtect() 함수에서 0x00405000부터 0x1000만큼 메모리 영역에 읽기, 쓰기 권한을 부여했기 때문에 사용자 코드가 진행되면서 이곳에 값을 쓸 수 있게 된 것이다. 그런데 함수의 수에 비해 사용된 공간이 적은데 나머지는 어디로 간 것일까? 확인 결과 다른 DLL에서 불러오는 함수는 조금 다른 공간에 저장되는 것을 알 수 있었다.
위처럼 GDI32.dll에서 불러오는 함수는 0x00405000에 저장되었으며 그 다음으로 KERNEL32.dll의 GetStringTypeA(), MultiByteToWideChar() 등 다른 함수들도 근처 위치에 저장되는 것을 알 수 있었다.
이렇게 함수의 주소를 모두 저장한 후에는 0x0040A163의 jne 점프 분기를 타지 않고 진행하는데 위에서 본 것과 비슷한 구조의 코드 블록을 두 개 정도 더 볼 수 있다.
이번에는 0x00401000부터 0x4000만큼의 메모리 영역에 읽기, 쓰기 권한을 부여하고 있다. 아래에서는 위에처럼 0x10, 0x20, 0x30, 0x40, 0x50으로 xor 연산을 하고 있는데 반복문을 탈출할 경우 도달하는 0x0040A1B0에 중단점을 걸고 스킵해서 그 결과를 확인해볼 수 있다.
0x00401000부터 어떤 값들이 쓰여졌는데 그냥 16진수로는 의미를 알 수 없다. VirtualProtect 함수에서 0x4000만큼이나 메모리 보호 영역을 조정했기 때문에 이 넓은 공간이 프로그램 실행 코드가 저장될 공간이라 추측하고 디스어셈블러(x96dbg 기준)에서 열어본 결과 위처럼 복호화된 원래 프로그램 코드가 저장되어 있는 것을 확인할 수 있었다.
비슷하게 다음 코드 블록에서도 0x00406000부터 0x3000만큼 읽기, 쓰기 권한을 부여하고 있는데 이 부분은 복호화된 코드 같지도 않고 어떤 영역인지 추측할 수 없었다. 어쨌든 이렇게 프로그램 실행에 필요한 코드들이 모두 복호화됐다면 이제 프로그램 실행 코드의 원래 진입점, 즉 OEP로 진입해서 프로그램을 실행해야 할 텐데 이 마지막 코드 블록에서 코드 실행을 마친 후 0x0040A1FB로 점프, 다시 특정 위치로 점프하고 있기 때문에 해당 위치를 확인해 보았다.
그곳을 확인한 결과 push ebp / mov ebp, esp 같은 전형적인 함수의 프롤로그와 이런저런 함수들을 호출해서 맨 처음에 본 빈 윈도우를 띄우는 코드를 확인할 수 있었다. 즉 이곳이 언패킹된 코드의 OEP임을 확인할 수 있었다.
사실 이번 문제는 언패킹에 대해 아예 지식이 없던 때에 그냥 무작정 풀다가 풀게되서 나 자신도 어이가 없었던 문제로 어떤 언패킹 관련 기법이나 그런 장애 요소가 하나도 없기 때문에 가능했던 일인 것 같다. 그렇지만 일단 언패킹을 위한 OEP를 수동으로 찾는 MUP(Manual UnPacking)을 실습해볼 수 있었고 인터넷 검색하면 여기저기서 나오는 UPX 패커가 아닌 일종의 커스텀 패커로 패킹된 프로그램을 언패킹해 볼 수 있어 좋은 경험이 되었다. 어쨌든 프로그램 실행 코드를 복호화하고 그곳으로 점프한다는 기본적인 규칙은 습득하게 되었기에 다른 언패킹 문제를 푸는데도 도움이 됐으면 좋겠다.
'챌린지 > Reversing.kr' 카테고리의 다른 글
Challenge - Replace (0) | 2020.12.05 |
---|---|
Challenge - Music Player (0) | 2020.12.03 |
Challenge - Easy Keygen (0) | 2020.11.11 |
Challenge - Easy Crack (0) | 2020.11.02 |