2014. 6. 25. 17:34

PE 파일을 disassembly하여 CALL 명령에 연결된 Win32 API 찾기 (IAT, Import Address Table)

 본 블로그글 읽기에 앞서,...


본 블로그를 읽기 위해선,

- PE 파일 분석

- Disassembly 분석

- Win32 API

등에 대한 기본적인 지식이 있어야 될 겁니다.

참고로, PE 파일 분석이나 Disassembly 분석은 향후 기초부터 다룰 예정입니다.

필요하다면, PE 파일 분석등은 "PE IAT" 등의 keyword로 검색하면 훌륭한 글들이 많이 있으니, 참고하시기 바랍니다.


 샘플 코드


다음과 같은 샘플 코드를 작성하고, sample_pe_win32.exe를 생성합니다.

(해당 .exe는 분석이 목적이므로, 실행할 이유가 없기에, 논리적 오류등은 무시합니다.)


#include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { HANDLE h = INVALID_HANDLE_VALUE; h = ::CreateFileW(L"abcdabcd", 

GENERIC_READ, 0, NULL, OPEN_EXISTING, 

FILE_ATTRIBUTE_NORMAL, NULL);

::CloseHandle(h); return 0; }

즉, CreateFileW와 CloseHandle 이라는 Win32 API를 호출하고 있습니다.


sample_pe_win32.exe


 PE 분석


주어진 sample_pe_win32.exe를 분석해 봅니다.

우선 AddressOfEntryPoint는 0x12d8(=4824)로 해당 되는 section은 0번째 section 입니다.

해당 section의 정보는 다음과 같습니다.


 Name

 .text

 VirtualSize(PhysicalAddress)

 0x81e

 VirtualAddress

 0x1000

 SizeOfRawData

 0xa00

 PointerToRawData

 0x400

 PointerToRelocations

 0

 PointerToLinenumbers

 0

 NumberOfRelocations

 0

 NumberOfLinenumbers

 0

 Characteristics

 0x60000020


즉, 역시 EntryPoint는 .text에 포함되어 있습니다. 0x1000 번지부터 .text가 시작인데, Entry Point는 0x12d8로 되어 있습니다. 프로그램은 Entry Point에서 시작하며, 보통, 우리가 설계한 winmain은 그 상위 번지에 포함될 수 있습니다. 즉, 0x1000~0x12d7에 winmain이 포함될 수 있습니다. 아마도 entry point에서 시작하여 상위 0x1000~0x12d7 번지로 CALL될 것입니다.


PE 분석시 Raw Address가 있는데, 이는 실제 PE 파일의 Seek 값을 의미합니다. Raw Address 변환 공식을 이용하여 아래 값을 계산합니다. (변환 공식은 차후 설명할 예정이며, 검색을 통해 다른 블로그글을 참고하세요)


RAW(0x1000) = 0x400

RAW(0x12d8) = 0x6d8


일단 winmain에 있는 코드가 포함될 가능성이 있는 0x400 위치의 sample_pe_win32.exe hex값은 다음과 같습니다.


 ...

 00000400 6A 00 68 80 00 00 00 6A 03 6A 00 6A 00 68 00 00

 00000410 00 80 68 F4 20 40 00 FF 15 00 20 40 00 50 FF 15

 00000420 04 20 40 00 33 C0 C3 3B 0D 00 30 40 00 75 02 F3

 00000430 C3 E9 AC 02 00 00 68 20 15 40 00 E8 A3 04 00 00

 00000440 A1 64 33 40 00 C7 04 24 2C 30 40 00 FF 35 60 33

 00000450 40 00 A3 2C 30 40 00 68 1C 30 40 00 68 20 30 40

 ...

이를 disassemble 하면 다음과 같습니다.

(disassemble은 u86dis open source를 사용할 수 있습니다. 해당 부분은 차후 설명할 예정입니다.)


...
[0x00000400] push 0x0
[0x00000402] push 0x80
[0x00000407] push 0x3
[0x00000409] push 0x0
[0x0000040b] push 0x0
[0x0000040d] push 0x80000000
[0x00000412] push 0x4020f4
[0x00000417] call dword [0x402000]
[0x0000041d] push eax
[0x0000041e] call dword [0x402004]
[0x00000424] xor eax, eax
[0x00000426] ret
[0x00000427] cmp ecx, [0x403000]
[0x0000042d] jnz 0x31
[0x0000042f] repe ret
[0x00000431] jmp 0x2e2
[0x00000436] push 0x401520
[0x0000043b] call 0x4e3
[0x00000440] mov eax, [0x403364]
[0x00000445] mov dword [esp], 0x40302c
[0x0000044c] push dword [0x403360]
...

(주의 : 위 disasssemble의 왼쪽 [0x...] 부분은 File의 Seek point로 실제 PE 파일이 메모리에 올라왔을때의 Virtual Address는 아님을 유의하세요)


결과론적이지만, call dword [0x40200]이 CreateFileW, call dword [0x402004]가 CloseHandle에 해당됩니다. 우선 disassemble한 상황에서는 Win32 API로 binding되지 않기 때문에, 정확하게 어떤 Win32 API가 사용되는지는 확인할 방법이 없습니다. 그래서 본 블로그에서는 해당 부분, 즉, Win32 API로 연결하는 방법에 대해 알아보도록 합니다.


 Import Address Table (IAT)


이제 0x40200이 CreateFileW와 연관성이 있다는 증거를 찾아야 할 때 입니다. 이를 위해서는 우선 PE 파일의 IAT를 구해야 합니다. IAT에 대해서는 많은 글들이 있으니, 검색을 통해 확인해 보시기 바랍니다. 다만, 이 곳에서는 보통의 다른 문서에서 잘 다루지 않는 부분으로 확인하고자 합니다. 즉, ollydbg와 같이 처음부터 disassemble된 상태에서 Win32 API를 직접 바인딩하여 표기하는 "직접 실행된 디버그 방식"이 아닌 실행과 상관없이 "정적"인 방법으로 disassemble하고 Win32 API로 바인딩하는 방법을 알려드리고자 합니다.


우선,IAT를 구하기 앞서, IMAGE_IMPORT_DESCRIPTOR를 구해야 하는데, 해당 PE 파일은 총 2개가 존재합니다. 이는 흔히 dependency walker로 확인 가능합니다. (다시 말해 의존성이 있는 모듈의 개수임. 단, 지연 로드(delay load) 모듈은 제외)


우선, CreateFileW는 KERNEL32.DLL과 연관이 있으므로, 첫번째 IMAGE_IMPORT_DESCRIPTOR를 확인해 봐야 겠습니다.


 Characteristics

 0x00002270

 OriginalFirstThunk

 0x00002270

 TimeDateStamp

 0x00000000

 ForwarderChain

 0x00000000

 Name

 0x00002338

 FirstThunk

 0x00002000


Name이 0x2338인데,

RAW(0x2338) = 0x1138

입니다. 즉, PE 파일의 0x1138 위치에 DLL 이름이 등장하는데, 다음과 같습니다.

 ...

 00001130 65 48 61 6E 64 6C 65 00 4B 45 52 4E 45 4C 33 32    eHandle.KERNEL32

 00001140 2E 64 6C 6C 00 00 15 ...                           .dll...

 ...


즉, 해당 IMAGE_IMPORT_DESCRIPTOR는 KERNEL32.DLL을 나타내는 것이 확인되었습니다.


사용중인 KERNEL32.DLL 함수들은 이제 배열로 표현되는데, 해당 배열의 순서는 꽤나 중요합니다. 우선, 어떤 함수가 사용되는지 확인해 필요가 있는데, 이는 OriginalFirstThunk를 이용합니다. 해당 주소로 이동하면, IMAGE_THUNK_DATA32 배열 구할 수 있습니다. 해당 값이 0으로 표현되면 배열의 끝을 의미합니다. 일단 해당 값의 주소로 다시 이동하면, IMAGE_IMPORT_BY_NAME를 구할 수 있습니다(참고로 이렇게 얻어진 주소는 모두 RAW 주소로 변환하여 접근해야 합니다). 이런 방식으로 여러개의 IMAGE_IMPORT_BY_NAME를 구할 수 있습니다.

이러한 과정은 향후 보다 자세히 다룰예정이며, 급하다면 검색을 통해 방법을 습득할 수 있습니다.

일단, 계산된 그 결과를 정리하면 다음과 같습니다.

 Index

 Hint

 함수명

 0

 0x007F

 CreateFileW

 1

 0x0043

 CloseHandle

 2

 0x01aa

 GetCurrentProcessId

 ...

 ...

 ...


dependency walker에서는 다음과 같이 표현됩니다.

(순서가 중요하기 때문에 PI 부분으로 정렬하여 로드하시기 바랍니다)

즉, dependency walker와 동일한 순서로 되어 있는 것이 확인되었습니다.


이와 같이 배열을 구하기 위해, OriginalFirstThunk를 이용하였습니다. 즉, 해당 값은 위 배열의 시작 위치를 가리키는 값이라 볼 수 있습니다. 이와 유사하게, FirstThunk값은 실제 함수 포인터가 저장된 위치를 가리키는 값을 가리킵니다. 즉, 위에서는 0x2000인데 해당 값 부터 배열 표현 방식으로 각 함수들의 실행 포인터가 저장됩니다. 그러나 중요한건, 해당 0x2000값은 Relative Vitual Address이기 때문에, 실제 프로그램이 로드될 위치인 ImageBase 값을 더해 줘야 합니다. 위 샘플 파일의 ImageBase는 일반적인 0x400000입니다. 그리고 FirstThunk도 IMAGE_THUNK_DATA32의 배열로 구성되기 때문에, 해당값의 크기만큼 계속 더해 줘야 합니다. 그렇게 되면, 실제 함수의 Address를 구할 수 있습니다.


 Index

 Hint

 함수명

 Address

 0

 0x007F

 CreateFileW

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * 0 = 0x40200

 1

 0x0043

 CloseHandle

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * 1 = 0x40204

 2

 0x01aa

 GetCurrentProcessId

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * 2 = 0x40208

 ...

 ...

 ...

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * n = ...

(만일 X64 바이너인 경우에는 sizeof(IMAGE_THUNK_DATA64)를 사용해야 합니다.)


여기서 확인해 볼 것은, 0x40200위치의 값을 확인해 보는 것인데, 해당 값은 OriginalFirstThunk가 가리키던 값이랑 동일한 위치를 가리키게 됩니다. 만일 PE 파일이 실제 PE Loader에 의해 실행하게 된다면, PE Loader는 PE 파일을 메모리에 로드하고(실제로 MMF), 의존성 있는 DLL을 로드하게 되는데, 위 각 배열의 함수의 포인터를 FirstThunk에 해당되는 값, 즉, 0x40200 위치에 Overwrite하게 됩니다.


대략 이런 과정을 그림으로 표현하면 다음과 같습니다.


우선, PE 파일이 실행되기 전 파일 상태로 있을 때의 모습입니다.

즉, 각각의 분리된 Thunk 배열이 있고, 해당 배열이 가리키는 위치는 동일하게 되어 있습니다. 그리고, 그 배열의 끝은 "CreateFileW"와 같은 함수 이름이 들어 있습니다(여기서 주의할 것은 함수 이름 대신 Ordinal로 표현되는 경우도 있습니다). "(따옴표)를 표시한 것은, 함수의 이름이 Hardcode되어 저장되어 있다고 표시하기 위함입니다.


이런 PE 파일이 PE Loader에 의해 실행되면, PE Loader는 시작 싯점에 KERNEL32.DLL을 로드하고 "CreateFileW"의 함수 위치를 First Thunk에 해당되는 배열값으로 Overwrite하게 됩니다.

즉, 위 그림은 PE 파일이 PE Loader에 의해 실제 Bind된 상태를 표현한 것입니다. PE Loader는 FirstThunk에 해당되는 배열 영역에 실제 해당 함수의 함수 포인터를 Overwrite하게 됩니다.


그렇다면, 위 샘플 파일인 case로 좀더 확대해서, 메모리를 살펴 보면 아래와 같습니다.

즉, 메모리 상에서 ImageBase(=0x40000) + First Thunk(=0x2000)부터 Win32 API의 함수 포인터가 저장됨을 확인할 수 있습니다.


 Find Win32 API


앞서서 생성한 자료 2개를 다시 아래에 표시합니다.

...
[0x00000400] push 0x0
[0x00000402] push 0x80
[0x00000407] push 0x3
[0x00000409] push 0x0
[0x0000040b] push 0x0
[0x0000040d] push 0x80000000
[0x00000412] push 0x4020f4
[0x00000417] call dword [0x402000]
[0x0000041d] push eax
[0x0000041e] call dword [0x402004]
[0x00000424] xor eax, eax
[0x00000426] ret
[0x00000427] cmp ecx, [0x403000]
[0x0000042d] jnz 0x31
[0x0000042f] repe ret
[0x00000431] jmp 0x2e2
[0x00000436] push 0x401520
[0x0000043b] call 0x4e3
[0x00000440] mov eax, [0x403364]
[0x00000445] mov dword [esp], 0x40302c
[0x0000044c] push dword [0x403360]
...

[disassemble code]


 Index

 Hint

 함수명

 Address

 0

 0x007F

 CreateFileW

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * 0 = 0x40200

 1

 0x0043

 CloseHandle

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * 1 = 0x40204

 2

 0x01aa

 GetCurrentProcessId

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * 2 = 0x40208

 ...

 ...

 ...

 0x2000 + 0x40000 + sizeof(IMAGE_THUNK_DATA32) * n = ...

[Import Address Table]


이것을 기반으로 다음과 같이 표현할 수 있습니다.

... [0x00000412] push 0x4020f4 [0x00000417] call CreateFileW ; call dword [0x402000] [0x0000041d] push eax [0x0000041e] call CloseHandle ; call dword [0x402004] ...


즉, dword [0x402000] 위치는 CreateFileW의 함수 포인터가 저장되었기 때문에, call CreateFileW로 표시할 수 있기 때문입니다.


 ollydbg 실험


여태껏 실행하지 않고 정적으로 disassemble하고 Win32 API를 매칭해 보았습니다. 이제 범용적인 ollydbg로 한번 확인해 보도록 하겠습니다. (참고로 XP에서 실험을 추천합니다. Vista 이상에서는 ImageBase가 random하게 선택되므로 분석이 어렵기 때문입니다.)


앞서 0x12d8이 EntryPoint였고, 0x400000이 ImageBase여서, 0x4012d8 위치에서 시작하게 됩니다.

스크롤을 올려, winmain이 있는 함수 위치로 올라가 보겠습니다.


즉, FF15 00204000은 CALL DWORD PTR DS:[<&KEREL32.CreateFileW>]로 disassemble 됩니다.

아래부분은 DS:[00402000]=7C81070로 표기되는데, 0x402000위치의 값은 PE Loader에 의해 CreateFileW의 함수 포인터인 0x7C8107F0로 Overwrite되었습니다.


해당 위치(0x7C8107F0)는 위 Memory map에서 나타나듯 kernel32.dll의 .text 코드 영역에 포함되어 있으니, CreateFileW의 함수 포인터임을 어림짐작할 수 있으리라 봅니다.


 마무리


이상으로 PE 파일을 분석하고 disassemble한뒤 Win32 API로 직접 연결하는 방법을 알아보았습니다. 위 과정은 하나의 예일 뿐이며, 다른 조건들에 의해 조금은 다를 수있음을 유의하시기 바랍니다.