DLL의 번지 계산법과 재배치
DLL은 Image Base 값을 PE Header에 포함하고 있고 기본적으로 메모리 상에 그 주소에 로드된다.
DLL의 코드를 찾기 위해서는 이 Image Base 값에 Base of code 값을 더해주자.
Base of code도 마찬가지로 PE Header에 기입되어 있다.
하지만 다른 DLL이 먼저 그 주소에 로드되어 있다면 이후에 로드되는 DLL은 그 DLL을 피해서 다른 주소에 로드된다.
이 과정은 OS가 자동으로 처리해준다.
또 체크해야할 것! - 실행코드의 jmp문이나 특정 데이터 영역 메모리를 호출하는 부분 또한 달라진 Image Base에 맞게 자동으로 변환된다.
예를 들어 프로그래머가 지정한 Image Base가 0x1000000이고 push dll.10019000
이라는 코드가 있을 때 만약 re-allocation에 의해 해당 DLL이 0x180B0000에 로드되었다면 해당 코드는 자동으로 push dll.180C9000
이라는 코드로 변환된다.
DLL이 로드되는 주소를 고정시키는 방법도 있다. linker 옵션 중 다음과 같은 옵션을 사용한다.
#pragma comment(linker, "/base:0x23400000 /fixed")
이렇게 하면 해당 DLL은 항상 0x23400000에 로드된다. 다만 그 위치에 이미 다른 DLL이 로드되어 있을 경우 이 DLL은 정상적으로 작동하지 않을 수도 있다.
Disassembler / Debugger 에서 로드된 DLL의 번지 수 찾기
DLL에는 외부에서 호출이 가능한 export 함수가 있다. 이러한 함수들은 DllMain() 과 함께 DLL의 entry point 역할을 한다.
export 함수가 있는 경우에는 PE Tools를 이용해 이 함수들의 이름과 entry point가 제공되기 때문에 disassembler나 debugger 등에서 쉽게 찾아볼 수 있다.
이제 메모리 상에서 DllMain()의 위치를 찾아보자!
사실 패킹되지 않은 경우에는 DLL을 찾는 것은 문제가 되지 않는다.
IDA와 같은 Disassembler를 이용하면 DllMain()의 offset 값을 쉽게 구할 수 있다.
그렇다면 패킹되어 있는 경우에는?
우선 DllMain()의 구조에 대해 학습할 필요가 있다.
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpRes){ switch(fdwReason) { case DLL_PROCESS_ATTACH: lpBuffer = (LPBYTE)malloc(sizeof(LPBYTE)); break; case DLL_PROCESS_DETACH: free(lpBuffer); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; } return TRUE; }
이 코드가 빌드되어 어셈블리 형태가 되면 다음과 같이 보인다.
00BB1030 mov eax, dword ptr ss:[esp+8] 00BB1034 sub eax, 0 ; Switch (cases 0,1) 00BB1037 je short Dll.00BB1053 00BB1039 dec eax 00BB103A jnz short Dll.00BB1061 00BB103C push 4 ; Case 1 of switch 00BB1034 00BB103E call Dll.00BB10B1 ; malloc 00BB1043 mov dword ptr ds:[BBAD08], eax 00BB1048 add esp, 4 00BB104B mov eax, 1 00BB1050 retn 0C 00BB1053 mov eax, dword ptr ds:[BBAD08] ; Case 0 of switch 00BB1034 00BB1058 push eax 00BB1059 call Dll.00BB11EB ; free 00BB105E add esp, 4 00BB1061 mov eax, 1 ; Default case of switch 00BB1034 00BB1066 retn 0C
정리하자면, 함수의 인자로 전달받은 fdwReason의 값을 1씩 빼가면서 0과 비교해서 switch문의 각 case를 실행시키고 있다.
여기서 핵심은,
함수가 어쨌든 다음과 같은 코드로 시작한다는 것이다.
8B 44 24 08 mov eax, [esp+8] 83 E8 00 sub eax, 0 74 2A jz ??? ; DLL이 load되는 번지수와 switch-case문의 구조에 따라 달라짐
이때, 2A는 switch-case문의 구조에 따라 값이 달라질 것이다.
그렇다면 DllMain()이 언제나 8B 44 24 08 83 E8 00 74
로 시작한다는 사실은 불변이다!
OllyDBG에서 Search for - Binary String 메뉴를 이용해서 이 opcode를 검색한다면 DllMain()의 위치를 찾을 수 있다.
+) DisableThreadLibraryCalls()라는 함수를 이용해서 DllMain()을 찾을 수도 있다.
이 함수는 스레드가 생성되거나 호출될 때 DllMain()이 호출되지 않도록 하는 함수인데 주로 DLL_THREAD_ATTACH에 넣어지는 편이다.
'리버스 엔지니어링 바이블' 카테고리의 다른 글
06 흔히 사용하는 패턴 (0) | 2018.11.10 |
---|---|
05 PE 헤더 (PE Header) (0) | 2018.11.10 |
03 C++ 클래스와 리버스 엔지니어링 (0) | 2018.10.14 |
02 C 문법과 디스어셈블링 (0) | 2018.10.14 |
01 리버스 엔지니어링만을 위한 어셈블리 (0) | 2018.10.14 |