security/리버싱핵심원리

IAT 조작하여 API 후킹 - 한글이 출력되는 계산기

민사민서 2023. 3. 30. 21:03

API 후킹 방식

- DLL Injection을 통해 타깃 프로세스의 IAT에 저장된 원함수 주소를 후킹 함수 주소로 조작

- 후킹하고자 하는 API가 대상 프로세스의 IAT에 존재하지 않으면 후킹 불가

 

타겟 API 파악하기

- 실행 중인 calc.exe에 Ollydbg attach, calc 모듈에서 호출하는 API들 목록 중 의심스러운 API들에 전부 bp

ex) DrawTextW, DispatchMessageW, LoadStringW, PostMessageW, SetWindowTextW 등등

- 입력값에 대한 힌트는 'SetWindowTextW' API에서 찾을 수 있었다 => 후킹 대상

화면에 출력되는 모든 문자열 ("129 *", "2")에 대해 API 호출됨

구현 코드 - InjectDllForHook.exe

// InjectDllForHook.exe
#include "windows.h"
#include "tchar.h"
#include "tlhelp32.h"

// Debug API를 사용하기 위해 엑세스 토큰의 privilege 변경
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) {
	TOKEN_PRIVILEGES tp;
	HANDLE hToken;
	LUID luid;

	if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken)) {
		_tprintf(L"OpenProcessToken error: %u\n", GetLastError());
		return FALSE;
	}

	if(!LookupPrivilegeValue(NULL, lpszPrivilege, &luid)) {
		_tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError());
		return FALSE;
	}

	tp.PrivilegeCount = 1;
	tp.Privileges[0].Luid = luid;
	if(bEnablePrivilege)
		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	else
		tp.Privileges[0].Attributes = 0;

	if(!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) {
		_tprintf(L"AdjuestTokenPrivileges error: %u\n", GetLastError());
		return FALSE;
	}

	if(GetLastError()==ERROR_NOT_ALL_ASSIGNED) {
		_tprintf(L"The token does not have the specified privilege.\n");
		return FALSE;
	}

	return TRUE;
}

// CreateRemoteThread를 이용해 원격 프로세스에서 LoadLibraryW(szDllName) 실행
BOOL InjectDll(DWORD dwPID, LPCWSTR szDllName) {
	HANDLE hProcess = NULL, hThread = NULL;
	LPVOID pRemoteBuf = NULL;
	LPTHREAD_START_ROUTINE pThreadProc;
	TCHAR szExePath[MAX_PATH] = {0,};
	TCHAR* ptr;

	if( !(hProcess=OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) {
		wprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
		return FALSE;
	}

	// dll 이름만 건네준 경우 (동일 디렉토리 내에 있다는 가정 아래 full path로 만들어줌)
	if(!wcsrchr(szDllName, L'\\')) {
		GetModuleFileName(NULL, szExePath, MAX_PATH);
		ptr = wcsrchr(szExePath, L'\\');
		/*
		*(ptr+1) = L'\x0';
		wcscat(szExePath, szDllName);
		szDllName = szExePath;
		*/
		wcscpy(ptr+1, szDllName);
		szDllName = szExePath;
	}

	DWORD dwBufSize = (DWORD)(wcslen(szDllName)+1) * sizeof(TCHAR);

	pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
	WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, dwBufSize, NULL);

	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryW");
	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}

// CreateRemoteThread를 이용해 원격에서 FreeLibrary(szDllName) 실행
BOOL EjectDll(DWORD dwPID, LPCWSTR szDllName) {
	HANDLE hSnapshot, hProcess, hThread;
	MODULEENTRY32 me;
	LPTHREAD_START_ROUTINE pThreadProc;
	BOOL bFound=FALSE;

	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
	me.dwSize = sizeof(MODULEENTRY32);

	if(Module32First(hSnapshot, &me)) {
		do {
			// dll 모듈 경로 + 모듈 이름 둘 다 비교
			if(_wcsicmp((LPCWSTR)me.szExePath, szDllName)==0 || _wcsicmp((LPCWSTR)me.szModule,szDllName)==0) {
				bFound = TRUE;
				break;
			}
		} while(Module32Next(hSnapshot, &me));
	}

	if(!bFound) {
		CloseHandle(hSnapshot);
		return FALSE;
	}

	if( !(hProcess=OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) {
		wprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
		return FALSE;
	}

	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "FreeLibrary");

	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	CloseHandle(hProcess);
	CloseHandle(hSnapshot);

	return TRUE;
}

int _tmain(int argc, TCHAR* argv[]) {
	if(argc!=4) {
		wprintf(L"USAGE : %s [i,e] [PID] [Dll Name] \n", argv[0]);
		return 1;
	}
	if(!SetPrivilege(SE_DEBUG_NAME, TRUE)) {
		wprintf(L"Failed to set privilege! \n");
		return 1;
	}

	DWORD dwPID = _wtol(argv[2]);
	if(_wcsicmp(argv[1], L"i") == 0) {
		if(InjectDll(dwPID, argv[3])) {
			wprintf(L"InjectDll(\"%s\") success!!\n", argv[3]);
		} else {
			wprintf(L"InjectDll(\"%s\") failed!!\n", argv[3]);
		}
	} 
	else if(_wcsicmp(argv[1], L"e") == 0) {
		if(EjectDll(dwPID, argv[3])) {
			wprintf(L"EjectDll(\"%s\") success!!\n", argv[3]);
		} else {
			wprintf(L"EjectDll(\"%s\") failed!!\n", argv[3]);
		}
	} 
	else {
		wprintf(L"Invalid option! \n");
		return 1;
	}

	return 0;
}

- Debug API를 사용하기 때문에 SetPrivilege 함수 필요

- CreateRemoteThread를 이용해 LoadLibraryW, FreeLibrary를 호출하는 InjectDll()과 EjectDll()

- EjectDll()에는 szDllName의 값으로 전체경로 / 모듈이름 모두 받을 수 있음

if(!_wcsicmp((LPCWSTR)me.szExePath, szDllName) || !_wcsicmp((LPCWSTR)me.szModule,szDllName))

- InjectDll()에는 szDllName으로 전체 경로를 주어야 제대로 동작 → dll이름만 건네주어도 되도록 개선!

    => 현재 프로세스의 경로를 가져와 마지막 '\' 뒤에 모듈 이름을 이어붙임

	TCHAR szExePath[MAX_PATH] = {0,};
	TCHAR* ptr;

	// 동일 디렉토리 내에 있다는 가정 아래 full path로 만들어줌
	if(!wcsrchr(szDllName, L'\\')) {
		GetModuleFileName(NULL, szExePath, MAX_PATH);
		ptr = wcsrchr(szExePath, L'\\');
		/*
		*(ptr+1) = L'\x0';
		wcscat(szExePath, szDllName);
		szDllName = szExePath;
		*/
		wcscpy(ptr+1, szDllName);
		szDllName = szExePath;
	}
	DWORD dwBufSize = (DWORD)(wcslen(szDllName)+1) * sizeof(TCHAR);

 

코드 구현 - hookiat.dll

// hookiat.dll
#include "windows.h"
#include "stdio.h"
#include "tchar.h"

typedef BOOL(WINAPI *PFSETWINDOWTEXTW)(HWND, LPCWSTR);
FARPROC g_pOrgFunc;

BOOL hook_iat(LPCSTR szDllName, FARPROC pfnOrg, FARPROC pfnNew) {
	// szDllName의 IAT에서 pfnOrg를 pfnNew로 업데이트
	HANDLE hMod;
	LPCSTR szLibName;
	PBYTE pAddr;
	PIMAGE_THUNK_DATA pThunk;
	PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
	DWORD dwRVA, oldProtect;

	// get current file handle
	hMod = GetModuleHandle(NULL);
	pAddr = (PBYTE)hMod;

	// pAddr = VA to PE signature (PE header)
	pAddr += *((DWORD*)&pAddr[0x3C]); // &pAddr[0x3C] = pAddr+0x3C

	// dwRVA = RVA to IMAGE_DIRECTORY_TABLE (DataDirectory[1].RVA)
	dwRVA = *((DWORD*)&pAddr[0x80]); // &pAddr[0x80] = pAddr+0x80

	// pImportDesc = VA to IID array (IDT)
	pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod+dwRVA);
	for( ; pImportDesc->Name ; pImportDesc++ ) {
		// szLibName = VA to IID.Name
		szLibName = (LPCSTR)((DWORD)hMod+pImportDesc->Name);
		if( !_stricmp(szLibName, szDllName) ) {
			// pThunk = VA to IAT
			pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod+pImportDesc->FirstThunk);
			for( ; pThunk->u1.Function ; pThunk++) {
				if(pThunk->u1.Function == (DWORD)pfnOrg) {
					VirtualProtect((LPVOID)&(pThunk->u1.Function), sizeof(DWORD), PAGE_EXECUTE_READWRITE, &oldProtect);
					// IAT 값 변경 (API hooking)
					pThunk->u1.Function = (DWORD)pfnNew;
					VirtualProtect((LPVOID)&(pThunk->u1.Function), sizeof(DWORD), oldProtect, &oldProtect);

					return TRUE;
				}
			}
		}
	}

	return FALSE;
}

// Unicode 환경에서 TCHAR = wchar_t
BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString) {
	TCHAR* pNum = L"영일이삼사오육칠팔구";
	TCHAR temp[2] = {0,};
	int i, nLen, nIndex = 0;

	nLen = wcslen(lpString);
	for(i=0; i<nLen; i++) {
		if(lpString[i]>=L'0' && lpString[i]<=L'9') {
			temp[0] = lpString[i];
			nIndex = _wtoi(temp);
			lpString[i] = pNum[nIndex];
		}
	}

	return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdReason, LPVOID lpReserved) {
	switch(fdReason) {
	case DLL_PROCESS_ATTACH:
		g_pOrgFunc = GetProcAddress(GetModuleHandleA("user32.dll"), "SetWindowTextW");
		// hook
		hook_iat("user32.dll", g_pOrgFunc, (FARPROC)MySetWindowTextW);
		break;
	case DLL_PROCESS_DETACH:
		// unhook
		hook_iat("user32.dll", (FARPROC)MySetWindowTextW, g_pOrgFunc);
		break;
	}
	return TRUE;
}

- VirtualProtect에서 (LPVOID)&(pThunk->u1.Function) 앞의 &를 빠뜨렸더니 아예 DLL이 로드되지 않음

- 유효하지 않은 주소(여기선 실제 API 함수의 주소)의 protection을 바꾸려고 했고, 그 결과 DllMain이 FALSE를 리턴하면서 로드하지 않은 듯?

 

디버깅 해보기

- calc.exe에 Ollydbg attach 후 Pause on new DLL 체크하고 인젝션한다. F9 한두번 하면 아래와 같이 hookiat EP 만남

- 진행하다보면 아래와 같이 DllMain도 만나고

- hook_iat로 들어가보면 IID 순회하며 "user32.dll" 모듈에 해당하는 IID를 찾은후, IAT를 순회하며 ECX 값 (User32!SetWindowTextW 주소)와 같은 원소가 있는지 ESI를 4씩 증가시켜가며 비교한다

- VirtualProtect()를 통해 calc.exe의 SetWindowTextW 주소가 담긴 IAT의 protection을 바꾸고

- IAT 값을 덮어쓴다

- 그 뒤로 SetWindowTextW가 호출될 일이 있으면 MySetWindowTextW가 대신 호출된다

 

새로 알게 된 점

1. TCHAR = wchar_t

- "tchar.h" 헤더에 아래와 같이 정의되어있다

#ifdef _UNICODE
#define TCHAR wchar_t
~
#else
#define TCHAR char

- 멀티바이트 환경인지 / 유니코드 환경인지에 따라 해석이 달라진다

 

2. LPWSTR vs. LPCWSTR

- 둘 다 Wide character 문자열을 가리키는 포인터로, 윈도우 프로그래밍에서 자주 사용

- LPWSTR: Long Pointer to Wide STRing (wchar_t *), 문자열 수정 가능

- LPCWSTR: Long Pointer to Const Wide STRing, (const wchar_t *), 문자열 수정 불가

(난 왜 지금까지 C가 C언어의 C인줄 알았지 ㅋㅋㅋㅋ)

 

3. IMAGE_CHUNK_DATA

- 리버싱 핵심원리에는 IID.OriginalFirstThunk (INT)와 IID.FirstThunk (IAT)가 4Byte 포인터(IMAGE_IMPORT_BY_NAME 구조체 주소) 배열을 가리킨다라고 설명되어있었는데 => 더 정확히 말하면 IMAGE_CHUNK_DATA 를 가리킴

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
  • OriginalFirstThunk IMAGE_THUNK_DATA 구조체 RVA 주소를 가리킴. 이때 IMAGE_THUNK_DATA는 Import하는 함수 이름이나 서수(Ordinal)를 포함하는 구조체 IMAGE_IMPORT_BY_NAME을 가리킴
  • Name : Import한 DLL의 이름을 담고 있는 ASCII 문자열의 RVA 주소
  • FirstThunk : IMAGE_THUNK_DATA 구조체의 RVA 주소를 가리키지만 PE 파일이 메모리에 맵핑되고 나면 Import 한 DLL 내의 함수 주소가지고 있는 IMAGE_THUNK_DATA를 가리킴

=> IMAGE_THUNK_DATA는 공용체이므로 상황에 따라 쓰임이 다름

=> 어찌보면 4byte 포인터 배열이 맞다 (상황에 따라 AddressOfData / Function 으로 다르게 해석됨)

1) INT가 가리키는 IMAGE_THUNK_DATA 구조체에는 AddressOfData가 들어있음 (IMAGE_IMPORT_BY_NAME 구조체 주소)

2) IAT가 가리키는 IMAGE_THUNK_DATA 구조체에는 Function이 들어있음(PE 메모리 매핑 후 실제 함수 주소)