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에 넣어지는 편이다.

+ Recent posts