API 후킹 방식
- DLL Injection을 통해 타깃 프로세스의 IAT에 저장된 원함수 주소를 후킹 함수 주소로 조작
- 후킹하고자 하는 API가 대상 프로세스의 IAT에 존재하지 않으면 후킹 불가
타겟 API 파악하기
- 실행 중인 calc.exe에 Ollydbg attach, calc 모듈에서 호출하는 API들 목록 중 의심스러운 API들에 전부 bp
ex) DrawTextW, DispatchMessageW, LoadStringW, PostMessageW, SetWindowTextW 등등
- 입력값에 대한 힌트는 'SetWindowTextW' 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 메모리 매핑 후 실제 함수 주소)
'security > 리버싱핵심원리' 카테고리의 다른 글
API 코드 패치를 이용한 API hooking - notepad.exe 프로세스 은폐하기 (1) (0) | 2023.04.01 |
---|---|
[API 후킹] IAT 조작을 이용한 메모장 암호화/복호화 (완) (0) | 2023.04.01 |
[미완][API 후킹] 디버그(Debug) 기법을 이용한 후킹 - 메모장 암호화/복호화 (0) | 2023.03.25 |
[미완][API 후킹] 디버그(Debug) 기법을 이용한 메모장 WriteFile() 후킹 (0) | 2023.03.25 |
API 후킹 Tech Map (0) | 2023.03.25 |