ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 25장 PE 패치를 이용한 DLL 로딩
    Reverse Engineering 2021. 1. 10. 19:50

    이전 실습까지는 DLL을 실행 중인 프로세스에 인젝션 하는 방법에 대해 알아보았다. 이번 실습에서는 프로세스가 메모리에 로딩되기 이전 상태인 파일 자체를 수정하여 프로세스가 실행될 때마다 원하는 DLL을 로딩하도록 하는 실습을 진행한다. 예제 파일인 TextView.exe를 패치하여 myhack3.dll이 프로그램 실행시마다 로딩될 수 있도록 수정해보자.

     

    1. 소스코드 (myhack3.dll)

    더보기
    #include "stdio.h"
    #include "Windows.h"
    #include "shlobj.h"
    #include "Wininet.h"
    #include "tchar.h"
    
    #pragma comment(lib, "Wininet.lib")
    
    #define DEF_BUF_SIZE (4096)
    #define DEF_URL L"http://www.google.com/index.html"
    #define DEF_INDEX_FILE L"index.html"
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    	__declspec(dllexport) void dummy() {
    		return;
    	}
    #ifdef __cplusplus
    }
    #endif
    
    BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile);
    BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam);
    HWND GetWindowHandleFromPID(DWORD dwPID);
    BOOL DropFile(LPCTSTR wcsFile);
    
    HWND g_hWnd = NULL;
    
    DWORD WINAPI ThreadProc(LPVOID lParam) {
    	TCHAR szPath[MAX_PATH] = { 0, };
    	TCHAR* p = NULL;
    
    	GetModuleFileName(NULL, szPath, sizeof(szPath));
    
    	if (p = _tcsrchr(szPath, L'\\')) {
    		_tcscpy_s(p + 1, wcslen(DEF_INDEX_FILE) + 1, DEF_INDEX_FILE);
    		if (DownloadURL(DEF_URL, szPath))
    			DropFile(szPath);
    	}
    	return 0;
    }
    
    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
    	switch (fdwReason) {
    	case DLL_PROCESS_ATTACH:
    		CloseHandle(CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL));
    		break;
    	}
    	return TRUE;
    }
    
    BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile) {
    	BOOL bRet = FALSE;
    	HINTERNET hInternet = NULL, hURL = NULL;
    	BYTE pBuf[DEF_BUF_SIZE] = { 0, };
    	DWORD dwBytesRead = 0;
    	FILE* pFile = NULL;
    	errno_t err = 0;
    
    	hInternet = InternetOpen(L"ReverseCore", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
    	if (NULL == hInternet) {
    		OutputDebugString(L"InternetOpen() failed!");
    		return FALSE;
    	}
    
    	hURL = InternetOpenUrl(hInternet, szURL, NULL, 0, INTERNET_FLAG_RELOAD, 0);
    	if (NULL == hURL) {
    		OutputDebugString(L"InternetOpenUrl() failed!");
    		goto _DownloadURL_EXIT;
    	}
    
    	if (err = _tfopen_s(&pFile, szFile, L"wt")) {
    		OutputDebugString(L"fopen() failed!");
    		goto _DownloadURL_EXIT;
    	}
    
    	while (InternetReadFile(hURL, pBuf, DEF_BUF_SIZE, &dwBytesRead)) {
    		if (!dwBytesRead)
    			break;
    		fwrite(pBuf, dwBytesRead, 1, pFile);
    	}
    	bRet = TRUE;
    
    _DownloadURL_EXIT:
    	if (pFile)
    		fclose(pFile);
    	if (hURL)
    		InternetCloseHandle(hURL);
    	if (hInternet)
    		InternetCloseHandle(hInternet);
    
    	return bRet;
    }
    
    BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam) {
    	DWORD dwPID = 0;
    
    	GetWindowThreadProcessId(hWnd, &dwPID);
    
    	if (dwPID == (DWORD)lParam) {
    		g_hWnd = hWnd;
    		return FALSE;
    	}
    
    	return TRUE;
    }
    
    HWND GetWindowHandleFromPID(DWORD dwPID) {
    	EnumWindows(EnumWindowsProc, dwPID);
    	
    	return g_hWnd;
    }
    
    BOOL DropFile(LPCTSTR wcsFile) {
    	HWND hWnd = NULL;
    	DWORD dwBufSize = 0;
    	BYTE* pBuf = NULL;
    	DROPFILES* pDrop = NULL;
    	char szFile[MAX_PATH] = { 0, };
    	HANDLE hMem = 0;
    
    	WideCharToMultiByte(CP_ACP, 0, wcsFile, -1, szFile, MAX_PATH, NULL, NULL);
    	dwBufSize = sizeof(DROPFILES) + strlen(szFile) + 1;
    
    	if (!(hMem = GlobalAlloc(GMEM_ZEROINIT, dwBufSize))) {
    		OutputDebugString(L"GlobalAlloc() failed!!!");
    		return FALSE;
    	}
    	
    	pBuf = (LPBYTE)GlobalLock(hMem);
    
    	pDrop = (DROPFILES*)pBuf;
    	pDrop->pFiles = sizeof(DROPFILES);
    	strcpy_s((char*)(pBuf + sizeof(DROPFILES)), strlen(szFile) + 1, szFile);
    
    	GlobalUnlock(hMem);
    
    	if (!(hWnd = GetWindowHandleFromPID(GetCurrentProcessId()))) {
    		OutputDebugString(L"GetWndhandleFromPID() failed!!!");
    		return FALSE;
    	}
    
    	PostMessage(hWnd, WM_DROPFILES, (WPARAM)pBuf, NULL);
    
    	return TRUE;
    }

    DllMain()에서 스레드 함수 ThreadProc을 실행시키는데 ThreadProc에서 DownloadURL()과 DropFile() 함수를 호출하여 google의 index.html 파일을 다운로드하고 해당 내용을 TextView 프로그램에 나타나게 해 준다. 해당 코드에서 유심히 살펴봐야 할 부분이 있는데 바로 dummy() 함수 호출 부분이다. 해당 함수는 myhack3.dll 파일에서 외부로 export 하는 함수로 아무런 기능이 없지만 다른 파일에서 import table에 추가할 수 있도록 형식적인 완전성을 제공한다.

     

    2. TextView.exe 파일 패치

    PEView를 이용해 TextView.exe의 IDT 주소를 확인해보자.

    IDT의 RVA

    위와 같이 84CC의 RVA를 갖는다. 직접 해당 위치를 PEView에서 봐보자.

     

    IDT

    위와 같이 IDT는 TextView.exe의 .rdata 섹션에 존재하고, IDT는 이전에 배운 IMAGE_IMPORT_DESCRIPTOR 구조체 배열로 이루어져 있다. (끝은 NULL 구조체) IID 구조체는 아래와 같다.

    //IMAGE_IMPORT_DESCRIPTOR
    
    typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    	union {
        	DWORD Characteristics;
            DWORD OriginalFirstThink;	//INT
        };
        DWORD TimeDateStamp;
        DWORD ForwarderChain;
        DWORD Name;	//DLL Name
        DWORD FirstThunk;	//IAT
    } IMAGE_IMPORT_DESCRIPTOR;

    구조체 하나 당 0x14 byte이므로 전체 IID 영역은 84CC ~ 852F이다. (dll 4개 + NULL 구조체 1개=> 5개의 IID 구조체)

     

    PEView의 주소 보기를 File Offset으로 변경하여 IDT의 파일 오프셋을 확인해보면 다음과 같다.

    IID in file offset

    해당 위치를 HxD를 통해 찾아가면 다음과 같다.

    IID in HxD

    위 그림에서 IDT가 끝나는 부분(드래그 된 부분의 마지막 영역) 뒤로는 다른 데이터가 존재해서 myhack3.dll을 위한 IID 구조체를 바로 이어서 덧붙이기는 어려움을 알 수 있다. 이런 경우 IDT 전체를 다른 넓은 위치로 이동한 후 새로운 IID를 추가하는 방법을 이용해야 하는데 다음과 같은 방법이 있다.

     

    (1) 파일의 다른 빈 영역을 찾는다.

    (2) 파일 마지막 섹션의 크기를 늘린다.

    (3) 파일 끝에 새로운 섹션을 추가한다.

     

    실습에서는 (1)로 파일을 패치한다. PEView를 통해. rdata 섹션을 보면 다음과 같다.

    .rdata 섹션

    위와 같이 공간이 NULL로 가득차 있다. 적당한 위치에 IDT를 옮기면 되지만, 해당 영역이 메모리에 올라갈 때도 NULL로 올라가는지 반드시 확인해야 한다. PEView를 이용해. rdata 섹션 헤더를 살펴보자.

    .rdata 섹션헤더

    위와 같이 Virtual Size와 Size of Raw Data가 다른 것을 확인할 수 있다. 위 예에서는 2E00 - 2C56 = 1AA 만큼의 file 영역은 사용되지 않는 것을 알 수 있다. 앞의 .rdata 섹션에서 7E80 영역에 IDT를 복사하는 것은 문제가 되지 않으므로 해당 부분에 새로운 IDT를 생성해보자.

     

    우선 TextView의 IMAGE_OPTIONAL_HEADER의 IMPORT Table 구조체 멤버는 아래와 같다.

    IMAGE_OPTIONAL_HEADER의 IMPORT Table

    우선 RVA 값을 새롭게 만들 IDT의 위치인 8C80으로 수정하고, Size 값은 기존 0x64에 0x14를 더한 0x78로 변경한다.

    RVA와 Size 수정

    이제 IMPORT Table이 해당 위치에 해당 size를 갖는 것으로 간주된다. 실습에서는 또한 BOUND IMPORT Table을 제거하는데 BOUND IMPORT Table은 optional한 테이블로 DLL 로딩 속도를 향상하기 위한 기법이라고 한다. Optional 한 테이블이지만 잘못 세팅되어 있다면 문제가 될 수 있으므로 패치 시에는 해당 부분을 모두 0으로 바꿔주는 것이 좋다고 한다.

     

    새롭게 설정해준 위치에 아래와 같이 기존 IDT를 복사해서 붙여넣는다.

    새로운 IDT

    이 상태에서 myhack3.dll을 위한 새로운 IID를 구성해서 위의 드래그된 부분 끝에 추가해주면 된다. 각 멤버 값은 다음과 같이 세팅한다.

     

    - DWORD OriginalFirstThunk(INT) : 00008D00 =>RVA to INT 

    - DWORD TimeDateStamp : 0

    - DWORD ForwarderChain : 0

    - DWORD Name : 00008D10=>RVA to DLL Name

    - DWORD FirstThunk(IAT) : 00008D20=>RVA to IAT

     

    새롭게 추가된 IID

    위의 값들은 RVA 값으로 RAW로 변환하게 되면 INT: 7F00 / Name: 7F10 / IAT : 7F20의 값을 갖고, 이 값은 새로 생성된 IDT 바로 밑에 위치한다. 해당 위치로 가서 아래와 같이 값을 추가해주자.

    INT, Name, IAT 구성

    위와 같이 7F00에는 8D30(RVA)을 나타내고 있고, 이는 RAW로 7F40이다. 7F40에는 IMAGE_IMPORT_BY_NAME 구조체가 있는데 0 값을 갖는 2 bytes ordinal이 있고, 그 뒤로 dummy라는 이름이 나타난다. (myhack3.dll 소스코드에서 작성한 함수다.) 7F10에는 import 할 dll 이름이 문자 배열로 나타난다. 7F20에는 7F00과 마찬가지의 값을 나타낸다. 

     

    마지막으로 IAT 섹션의 Characteristics를 변경해주어야 한다. PEView로 .rdata 섹션 헤더를 살펴보면 다음과 같다.

    .rdata 섹션 헤더

    IAT는 PE 로더에 의해 메모리에 로딩될 때 실제 함수 주소로 덮어써 지기 때문에 해당 섹션은 반드시 WRITE 속성을 갖고 있어야 한다. IMAGE_SCN_MEM_WRITE(80000000) 속성을 bit OR 한 값인 C0000040 값으로 수정해주자.

    쓰기 속성 추가

    이때 기존에는 IAT가 존재했음에도 불구하고 쓰기 속성은 없었는데 프로그램은 잘 실행된다. 이는 IMAGE_OPTIONAL_HEADER 구조체의 Data Directory 배열 중 IMPORT Address Table에서 명시된 영역 RVA ~ RVA+Size 내부에 IAT가 존재하면 쓰기 속성이 없어도 된다. 만약에 쓰기 속성을 추가하고 싶지 않으면 해당 영역을 8byte(ordinal 2byte + 'dummy' 6byte) 늘린 후, 뒤에 dummy()를 위한 IAT를 추가하면 된다.

     

    3. 검증

    마지막으로 패치된 TextView 파일을 PEView로 열어보자.

    PEView

    위와 같이 myhack3.dll이 새롭게 추가된 것을 확인할 수 있다. 프로그램을 실행시켜보자.

     

    TextView

    위와 같이 myhack3.dll에 의해 google의 index.html이 TextView 작업 영역에 나타난 것을 확인할 수 있다. 

    반응형

    댓글

Designed by Tistory.