-
[리버싱 핵심원리 study] 34장 고급 글로벌 API 후킹 - IE 접속 제어Reverse Engineering 2021. 1. 29. 21:22
이번 실습에서는 IE를 후킹 하여 의도하지 않은 다른 사이트로 접속을 우회시키는 방법을 공부해본다. (32bit windows 7 환경에서 진행하였다.)
1. 후킹 대상 API
API 후킹을 할 때는 어떤 API를 후킹 할지 선정하는 것이 가장 중요하다. 우선 VirtualBox의 Windows 7 32bit의 IE를 실행시킨 후, Process Explorer로 IE를 관찰해보자.
위와 같이 iexplore.exe 프로세스에 로딩된 dll을 확인해보면 그중 wininet.dll이 로딩된 것을 확인할 수 있다. wininet.dll에는 InternetConnect() API라는 것이 있는데 해당 API는 웹사이트에 접속하려고 할 때 사용되는 API이다. API의 형태는 다음과 같다.
void InternetConnectA( HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext );
디버깅을 통해 정말 웹 사이트에 접속하려할 때 해당 API가 호출되는지 검증해보자.
아래와 같이 OllyDbg2.0으로 IE 프로세스를 attach 시킨 후, wininet.InternetConnectW() API에 BP를 설정해보자.
이제 IE에 maple19out.tistory.com을 입력하여 블로그 접속을 시도해보면 다음과 같이 BP에 걸린다.
이때, 스택 창을 자세히 살펴보자.
스택 창을 살펴보면 두 번째 argument로 "maple19out.tistory.com" 문자열의 주소가 넘어가는 것을 확인할 수 있다. InternetConnect() API의 두 번째 argument가 바로 lpszServerName으로 접속하려는 웹 사이트의 주소가 됨을 알 수 있다. 해당 스택 부분을 dump창에서 다음과 같이 수정해보자.
이제 F9를 눌러 다시 프로세스를 실행시켜보자.
신기하게도 위와 같이 분명 IE에서는 maple19out.tistory.com을 주소창에 입력하였는데 naver.com으로 이동하는 것을 확인할 수 있다. 이와 같이 웹사이트를 접속할 때에 InternetConnect() API가 사용되고 해당 부분을 후킹 하면 이번 실습 목표를 달성할 수 있을 것 같다.
2. IE 프로세스 구조
그러나 한 가지 문제점이 있는데, IE는 탭 별로 프로세스가 생성된다. IE 내부에서 5개의 탭을 생성한 후 Process Explorer를 통해 확인해 보겠다.
위와 같이 IE는 탭 별로 프로세스가 생성되어서 iexplore.exe를 대상으로 InternetConnect() API 후킹을 한다고 해도 새로운 탭을 통해 웹사이트를 접속하려고 하면 후킹을 할 수 없다는 문제점이 있다. 이를 해결하기 위해서 글로벌 후킹이 필요한데 실습에서는 ntdll.ZwResumeThread() API를 이용한다.
3. ntdll.ZwResumeThread() API
ZwResumeThread() API는 자식 프로세스를 생성하는 low level API이다. 대표적으로 프로세스를 생성할 때 kernel32.CreateProcess() API가 사용되는데 내부적으로 호출되는 API를 따라가다 보면 ZwResumeThread() API가 나타난다. 예제 파일 중 cptest.exe는 내부적으로 notepad.exe를 CreateProcess() API를 이용해 생성하는 프로그램이다. 이 cptest.exe를 OllyDbg로 디버깅해보면서 어떤 API가 호출되는지 확인해보자.
CreateProcessW() API가 먼저 호출된다. 해당 부분에서 F7을 눌러 step in 한 후 스크롤을 내려보자.
바로 밑에 CreateProcessInternalW() API가 호출되는 것을 확인할 수 있다. 다시 한번 step in 한 후 디버깅을 진행해보자.
위와 같이 CreateProcessInternalW() API 내부에서는 ZwCreateUserProcess() API가 호출되는 것을 확인할 수 있다. 해당 API가 실행된 이후 스크롤을 내리다 보면 다음과 같이 ZwResumeThread() API가 호출되는 것을 확인할 수 있다.
즉 다음과 같은 형태로 프로세스 생성을 위해 API가 호출되는 것을 확인할 수 있다.
CreateProcess() -> CreateProcessInternal() -> ZwCreateUserProcess() / ZwResumeThread()
ZwResumeThread() API까지 호출이 되면 notepad.exe가 실행된다. 실습에서는 가장 low-level에 있는 ZwResumeThread() API를 후킹하여 글로벌 후킹 목적을 달성한다.
4. 실습 예제 - IE 접속 제어
실습 예제 파일은 IE 프로세스가 naver.com / daum.net / Nate.com / Yahoo.co.kr 등의 포털 사이트에 접속할 때 다른 주소인 reversecore.com으로 우회하도록 후킹을 하였다. 실습 프로그램을 실행해보자. (이전 실습과 같은 방식으로 cmd 창에서 인젝터 프로그램을 실행시켜 dll을 인젝션 한다.)
IE를 통해 www.naver.com 사이트에 접속하려고 하였지만 리버싱 핵심원리 책 작가님 블로그인 reversecore.com으로 이동하는 것을 확인할 수 있다. 새로운 탭을 생성하여도 마찬가지로 작동한다.
5. 예제 소스코드 분석
DllMain()
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { char szCurProc[MAX_PATH] = {0,}; char *p = NULL; switch( fdwReason ) { case DLL_PROCESS_ATTACH : DebugLog("DllMain() : DLL_PROCESS_ATTACH\n"); GetModuleFileNameA(NULL, szCurProc, MAX_PATH); p = strrchr(szCurProc, '\\'); if( (p != NULL) && !_stricmp(p+1, "iexplore.exe") ) { DebugLog("DllMain() : current process is [iexplore.exe]\n"); // wininet!InternetConnectW() API 를 후킹하기 전에 // 미리 wininet.dll 을 로딩시킴 if( NULL == LoadLibrary(L"wininet.dll") ) { DebugLog("DllMain() : LoadLibrary() failed!!! [%d]\n", GetLastError()); } } // hook hook_by_code("ntdll.dll", "ZwResumeThread", (PROC)NewZwResumeThread, g_pZWRT); hook_by_code("wininet.dll", "InternetConnectW", (PROC)NewInternetConnectW, g_pICW); break; case DLL_PROCESS_DETACH : DebugLog("DllMain() : DLL_PROCESS_DETACH\n"); // unhook unhook_by_code("ntdll.dll", "ZwResumeThread", g_pZWRT); unhook_by_code("wininet.dll", "InternetConnectW", g_pICW); break; } return TRUE; }
이전의 다른 dll의 DllMain()과 비슷한 형태를 띠지만 ZwResumeThread() API (글로벌 후킹용)와 InternetConnectW API (인터넷 접속 우회용)를 후킹 하는 것을 확인할 수 있으며, 5 바이트 패치 기법을 이용한다. (ZwResumeThread() API는 7 바이트 여유 공간이 없어서 핫 패치가 불가능하다.)
NewInternetConnectW() API
HINTERNET WINAPI NewInternetConnectW ( HINTERNET hInternet, LPCWSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUsername, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext ) { HINTERNET hInt = NULL; FARPROC pFunc = NULL; HMODULE hMod = NULL; // unhook if( !unhook_by_code("wininet.dll", "InternetConnectW", g_pICW) ) { DebugLog("NewInternetConnectW() : unhook_by_code() failed!!!\n"); return NULL; } // call original API hMod = GetModuleHandle(L"wininet.dll"); if( hMod == NULL ) { DebugLog("NewInternetConnectW() : GetModuleHandle() failed!!! [%d]\n", GetLastError()); goto __INTERNETCONNECT_EXIT; } pFunc = GetProcAddress(hMod, "InternetConnectW"); if( pFunc == NULL ) { DebugLog("NewInternetConnectW() : GetProcAddress() failed!!! [%d]\n", GetLastError()); goto __INTERNETCONNECT_EXIT; } if( !_tcsicmp(lpszServerName, L"www.naver.com") || !_tcsicmp(lpszServerName, L"www.daum.net") || !_tcsicmp(lpszServerName, L"www.nate.com") || !_tcsicmp(lpszServerName, L"www.yahoo.com") ) { DebugLog("[redirect] naver, daum, nate, yahoo => reversecore\n"); hInt = ((PFINTERNETCONNECTW)pFunc)(hInternet, L"www.reversecore.com", nServerPort, lpszUsername, lpszPassword, dwService, dwFlags, dwContext); } else { DebugLog("[no redirect]\n"); hInt = ((PFINTERNETCONNECTW)pFunc)(hInternet, lpszServerName, nServerPort, lpszUsername, lpszPassword, dwService, dwFlags, dwContext); } __INTERNETCONNECT_EXIT: // hook if( !hook_by_code("wininet.dll", "InternetConnectW", (PROC)NewInternetConnectW, g_pICW) ) { DebugLog("NewInternetConnectW() : hook_by_code() failed!!!\n"); } return hInt; }
코드가 길지만 핵심은 중간의 if / else 부분만 보면 된다. NewInternetConnect() 함수는 InternetConnect() API와 똑같은 매개변수를 갖는데 만약 lpszServerName이 지정한 특정 포털의 문자열과 match가 일어나면 "www.reversecore.com" 문자열로 변경을 해주는 작업을 하고, 그렇지 않은 경우 그냥 InternetConnect() API를 호출한다.
NewZwResumeThread() API
NTSTATUS WINAPI NewZwResumeThread(HANDLE ThreadHandle, PULONG SuspendCount) { NTSTATUS status, statusThread; FARPROC pFunc = NULL, pFuncThread = NULL; DWORD dwPID = 0; static DWORD dwPrevPID = 0; THREAD_BASIC_INFORMATION tbi; HMODULE hMod = NULL; TCHAR szModPath[MAX_PATH] = {0,}; DebugLog("NewZwResumeThread() : start!!!\n"); hMod = GetModuleHandle(L"ntdll.dll"); if( hMod == NULL ) { DebugLog("NewZwResumeThread() : GetModuleHandle() failed!!! [%d]\n", GetLastError()); return NULL; } // call ntdll!ZwQueryInformationThread() pFuncThread = GetProcAddress(hMod, "ZwQueryInformationThread"); if( pFuncThread == NULL ) { DebugLog("NewZwResumeThread() : GetProcAddress() failed!!! [%d]\n", GetLastError()); return NULL; } statusThread = ((PFZWQUERYINFORMATIONTHREAD)pFuncThread) (ThreadHandle, 0, &tbi, sizeof(tbi), NULL); if( statusThread != STATUS_SUCCESS ) { DebugLog("NewZwResumeThread() : pFuncThread() failed!!! [%d]\n", GetLastError()); return NULL; } dwPID = (DWORD)tbi.ClientId.UniqueProcess; if ( (dwPID != GetCurrentProcessId()) && (dwPID != dwPrevPID) ) { DebugLog("NewZwResumeThread() => call InjectDll()\n"); dwPrevPID = dwPID; // change privilege if( !SetPrivilege(SE_DEBUG_NAME, TRUE) ) DebugLog("NewZwResumeThread() : SetPrivilege() failed!!!\n"); // get injection dll path GetModuleFileName(GetModuleHandle(STR_MODULE_NAME), szModPath, MAX_PATH); if( !InjectDll(dwPID, szModPath) ) DebugLog("NewZwResumeThread() : InjectDll(%d) failed!!!\n", dwPID); } // call ntdll!ZwResumeThread() if( !unhook_by_code("ntdll.dll", "ZwResumeThread", g_pZWRT) ) { DebugLog("NewZwResumeThread() : unhook_by_code() failed!!!\n"); return NULL; } pFunc = GetProcAddress(hMod, "ZwResumeThread"); if( pFunc == NULL ) { DebugLog("NewZwResumeThread() : GetProcAddress() failed!!! [%d]\n", GetLastError()); goto __NTRESUMETHREAD_END; } status = ((PFZWRESUMETHREAD)pFunc)(ThreadHandle, SuspendCount); if( status != STATUS_SUCCESS ) { DebugLog("NewZwResumeThread() : pFunc() failed!!! [%d]\n", GetLastError()); goto __NTRESUMETHREAD_END; } __NTRESUMETHREAD_END: if( !hook_by_code("ntdll.dll", "ZwResumeThread", (PROC)NewZwResumeThread, g_pZWRT) ) { DebugLog("NewZwResumeThread() : hook_by_code() failed!!!\n"); } DebugLog("NewZwResumeThread() : end!!!\n"); return status; }
초반의 ZwQueryInformationThread() API를 이용하여 생성될 자식 프로세스의 PID를 계산해내고, 이 PID를 이용해서 redirect.dll을 인젝션 해준다. 코드의 마지막 부분에서는 ZwResumeThread() API를 호출해주어 자식 프로세스의 메인 스레드를 Resume 함으로써 API가 후킹 된 채로 프로세스가 실행된다.
6. Comment
이로써 리버싱 핵심원리 서적의 API 후킹 챕터 공부가 끝났다. 기본적인 큰 흐름은 파악했지만 실제로 코드로 구현하기는 쉽지 않아 보인다. 나중에 실제로 64 비트 환경에서의 API 인젝션을 연습해볼 겸 코드를 작성해봐야겠다.
반응형'Reverse Engineering' 카테고리의 다른 글
[리버싱 핵심원리 study] 37장 x64 프로세서 이야기 (0) 2021.02.01 [리버싱 핵심원리 study] 36장 64비트 컴퓨팅 (0) 2021.02.01 [리버싱 핵심원리 study] 33장 '스텔스' 프로세스 (0) 2021.01.26 [리버싱 핵심원리 study] 32장 계산기, 한글을 배우다 (0) 2021.01.22 [리버싱 핵심원리 study] 30장 메모장 WriteFile() 후킹 (1) 2021.01.18