-
[리버싱 핵심원리 study] 32장 계산기, 한글을 배우다Reverse Engineering 2021. 1. 22. 22:33
DLL 인젝션은 동작 원리와 구현이 비교적 간단하다는 장점이 있지만, 대상 프로세스의 IAT에 후킹을 원하는 API가 존재하지 않는 경우 사용할 수 없다는 단점이 있다. 이번 실습에서는 계산기에 DLL을 인젝션 하여 숫자 대신 한글이 출력되도록 API 후킹을 진행한다.
1. 대상 API 선정
PEView를 통해 calc.exe에서 임포트하는 API들을 확인해보자.
위와 같이 SetWindowText() API가 IAT에 있는 것을 확인할 수 있는데, 해당 API는 텍스트 에디터에 문자열을 출력해주는 기능을 한다. SetWindowText의() API의 구조는 다음과 같다.
BOOL SetWindowTextA( HWND hWnd, LPCSTR lpString );
API의 hWnd는 윈도우 핸들이고, lpString은 출력을 원하는 문자열의 주소를 가리킨다. 후킹을 진행할 때 해당 주소의 문자열을 살펴보고 숫자를 한글로 변경하는 작업을 해보자. 그전에 우선 OllyDbg로 계산기가 숫자 등의 결과물을 출력할 때 해당 API가 호출되는지 검증을 해보자.
우선 calc.exe를 OllyDbg로 연후, 'Search for All intermodular calls' 명령을 이용해 SetWindowText() API를 호출하는 부분을 찾고 BP를 설정한다.
그리고 난후, F9를 눌러 실행시켜보면 다음과 같이 BP가 적중한다.
이때, 스택에 SetWindowText() API의 두 번째 파라미터인 lpString에 0.이 저장되어 있는 것을 확인할 수 있다. 이는 계산기를 처음에 실행했을 때, 초기에 0이 출력되기 때문에 위와 같은 값이 저장된 것이다. 다시 F9를 눌러 실행시킨 후, 계산기 프로세스에서 9를 눌러보자.
마찬가지로 BP가 적중하며, 이 때의 lpString에는 9.이 저장되어 있는 것을 확인할 수 있다. 이제 lpString이 저장된 위치로 가서 '구'의 유니코드인 AD6C로 아래와 같이 값을 편집해보고 F9를 눌러 실행해보자.
위와 같이 숫자 '9' 대신 한글'구'가 출력된 것을 확인할 수 있다. 검증이 끝났으니 후킹 하는 소스코드를 작성하고 실습을 진행해보자.
2. IAT 후킹 동작 원리
프로세스의 IAT에는 프로그램에서 호출되는 API들의 주소가 저장되어있다. IAT 후킹은 IAT에 저장되어 있는 API의 주소를 바꾸는 방법이다. 아래와 같은 명령어를 통해 SetWindowText() API가 호출된다고 생각해보자.
CALL DWORD PTR [01001110]
01001110 주소가 IAT 영역이며, 해당 주소에는 SetWindowText() API의 주소가 저장되어 있다. 후킹을 통해 01001110에 새로 정의한 MySetWindowText() 함수를 호출하고, 내부적으로 수행할 작업을 마친 후 SetWindowText() API를 호출하는 코드를 삽입할 수 있다. 실습에서는 hookiat.dll이라는 dll을 인젝션 하여 후킹을 하였다.
3. 실습
책의 예제 파일인 InjectDll.exe를 적당한 폴더로 옮긴 후, Process Explorer로 알아낸 calc.exe의 프로세스 ID를 argument로 하여 hookiat.dll을 인젝션하게 되면 계산기는 다음과 같이 숫자 대신 한글을 출력하게 된다.
4. 소스코드 분석
(1) DllMain()
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"), "SetWindowTextW"); hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW); break; case DLL_PROCESS_DETACH: hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc); break; } return TRUE; }
DllMain의 코드는 간단하다. DLL_PROCESS_ATTACH 시에는 g_pOrgFunc에 GetProcAddress() API를 통해 후킹하려는 SetWindowTextW() API를 저장하고, hook_iat 함수를 호출하여 IAT의 SetWindowText() 자리를 MySetWindowTextW() 함수로 바꿔준다. 반대로 DLL_PROCESS_DETACH 시에는 SetWindowText() API로 돌려놓는 작업을 한다.
(2) MySetWindowTextW()
SetWindowText() API를 대체할 함수다.
//사용자 정의 후킹 함수 BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString) { wchar_t pNum[11] = L"영일이삼사오육칠팔구"; wchar_t temp[2] = { 0, }; int i = 0, nLen = 0, nIndex = 0; nLen = wcslen(lpString); for (i = 0; i < nLen; i++) { if (L'0' <= lpString[i] && lpString[i] <= L'9') { temp[0] = lpString[i]; nIndex = _wtoi(temp); lpString[i] = pNum[nIndex]; } } return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString); }
IAT가 후킹된 이후에 calc.exe는 SetWindowText() API가 호출될 때마다 MySetWindowText() 함수가 호출된다. 핵심 부분은 for loop로 만약 lpString 주소에 '1' ~ '9' 값을 갖는다면 한글 유니코드로 바꿔주는 역할을 한다. 마지막으로는 g_pOrgFunc에 담긴 SetWindowText() API를 호출함으로써 변경된 lpString을 화면에 출력하도록 한다.
(3) hook_iat()
//현재 프로세스의 IAT를 검색해서 pfnOrg 값을 pfnNew로 변경 BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew) { HMODULE hMod; LPCSTR szLibName; PIMAGE_IMPORT_DESCRIPTOR pImportDesc; PIMAGE_THUNK_DATA pThunk; DWORD dwOldProtect, dwRVA; PBYTE pAddr; //hMod, pAddr : ImageBase of calc.exe hMod = GetModuleHandle(NULL); pAddr = (PBYTE)hMod; //pAddr = VA to PE signature (IMAGE_NT_HEADERS) pAddr += *((DWORD*)&pAddr[0x3C]); //dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table dwRVA = *((DWORD*)&pAddr[0x80]); //pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod + dwRVA); for (; pImportDesc->Name; pImportDesc++) { //szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name); if (!_stricmp(szLibName, szDllName)) { pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod + pImportDesc->FirstThunk); for (; pThunk->u1.Function; pThunk++) { if (pThunk->u1.Function == (DWORD)pfnOrg) { VirtualProtect((LPVOID)&pThunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect); pThunk->u1.Function = (DWORD)pfnNew; VirtualProtect((LPVOID)&pThunk->u1.Function, 4, dwOldProtect, &dwOldProtect); return TRUE; } } } } return FALSE; }
실제로 IAT를 후킹하는 함수로, 코드는 짧지만 이해하는데 애를 먹었던 부분이다. 하나씩 천천히 살펴보자.
//hMod, pAddr : ImageBase of calc.exe hMod = GetModuleHandle(NULL); pAddr = (PBYTE)hMod; //pAddr = VA to PE signature (IMAGE_NT_HEADERS) pAddr += *((DWORD*)&pAddr[0x3C]); //dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table dwRVA = *((DWORD*)&pAddr[0x80]); //pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod + dwRVA);
우선 GetModuleHandle을 통해 pAddr에는 calc.exe의 imagebase가 저장된다. 이후에 (*(DWORD*)&pAddr[0x3C])를 통해 ImageBase로 부터 0x3C만큼 떨어진 곳에 저장된 값을 pAddr에 더하게 되는데 HxD로 해당 부분을 살펴보자.
위와 같이 0x3C에는 IMAGE_DOS_HEADER의 e_lfanew 멤버, 즉 NT header로의 offset이 저장되어 있고 해당 부분을 pAddr에 더해주면 PE signature까지의 Virtual Address가 pAddr에 저장되게 된다. 이후에 같은 방식으로 현재의 pAddr로부터 0x80에 저장되어 있는 값을 더해주게 되는데 이번에는 PEView로 해당 부분을 살펴보자.
0000F0에 80을 더한 00000170에는 위와 같이 IMPORT Table이 있는 것을 확인할 수 있다. 위와 같은 접근을 통해 pImportDesc 변수에는 calc.exe의 IID Table이 저장된다.
for (; pImportDesc->Name; pImportDesc++) { //szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name); if (!_stricmp(szLibName, szDllName)) { pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod + pImportDesc->FirstThunk); for (; pThunk->u1.Function; pThunk++) { if (pThunk->u1.Function == (DWORD)pfnOrg) { VirtualProtect((LPVOID)&pThunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect); pThunk->u1.Function = (DWORD)pfnNew; VirtualProtect((LPVOID)&pThunk->u1.Function, 4, dwOldProtect, &dwOldProtect); return TRUE; } } } }
이제 IID 배열을 순회하면서 목표로 하는 "user32.dll" (outer loop)를 찾고, "SetWindowText()" API를 찾는 작업(inner loop)을 수행한 후, VirtualProtect() 함수를 이용하여 해당 IAT의 메모리 영역을 읽기&쓰기 모드로 변경한 후 해당 위치에 새로 작성한 MySetWindowText()함수의 주소를 집어넣어주고 다시 원래대로 메모리 영역을 읽기 모드로 설정한다.
5. 인젝션된 DLL의 디버깅
OllyDbg를 이용해 후킹 된 IAT 메모리 영역을 확인해보자. calc.exe를 실행시킨 후, OllyDbg 2.0에 Attach 하자.
이후에 Option에서 Pause on new module을 체크한 후, Dll 인젝션을 하면 아래와 같이 hookiat.dll의 module entry point에서 멈추게 된다.
이제 해당 위치에서 부터 Option에서 Pause on new module 체크 해제를 한 후 디버깅을 진행하면 된다. 우선 hookiat.dll의 DllMain() 함수를 찾아가야 하는데 문자열 검색을 통해 찾아가 보자.
SetWindowText 문자열이 있는 주소로 이동해보자.
위 부분은 Dll Main에서 DLL_PROCESS_ATTACH 부분과 일치한다고 볼 수 있다. (GetProcAddress 호출 부분) 해당 위치부터 디버깅해보자.
위와 같이 CALL 727E1090 이전에 스택에 쌓인 argument들을 확인해보면 각각 MySetWindowText()와 SetWindowText() API 주소임을 확인할 수 있다. ("user32.dll" 문자열 주소는 코드 최적화에 의해 하드 코딩되며 따로 넘어가지 않는다.) F7을 통해 hook_iat 내부로 들어가 보자.
조금 내려보면 위와 같은 형태로 이중 반복문 구조를 나타내는데 소스코드와 같이 Outer loop에서는 IID.Name 항목 중 "user32.dll"을 갖는 부분을 찾고, inner loop에서는 SetWindowText() API 주소와 같은 값을 갖는 부분을 찾는다.
SetWindowText() API 주소를 발견한 후에는 위와 같이 Mov 명령어를 통해 IAT에서 SetWindowText() API가 있어야 할 자리에 MySetWindowText 함수 주소를 집어넣게 된다. 메모리 dump를 통해 위와 같이 다른 값이 들어 있는 것을 확인할 수 있다.
이제 API 후킹이 완료되었으므로 SetWindowText() API를 호출하는 코드에 BP를 설정한 후 F9를 눌러 실행시킨 후, 아무 숫자나 입력해보자. 그러면 아래와 같이 BP가 적중하는 것을 확인할 수 있다.
현재 상태에서 F7을 눌러 stepin을 하게 되면 SetWindowText 대신 새롭게 정의한 MySetWindowText 함수로 이동하게 된다.
또한 숫자를 한글로 바꿔주는 작업을 한 후, 자체적으로 SetWindowText API를 호출하여 계산기 UI에 해당 내용을 출력하는 것을 확인할 수 있다.
반응형'Reverse Engineering' 카테고리의 다른 글
[리버싱 핵심원리 study] 34장 고급 글로벌 API 후킹 - IE 접속 제어 (0) 2021.01.29 [리버싱 핵심원리 study] 33장 '스텔스' 프로세스 (0) 2021.01.26 [리버싱 핵심원리 study] 30장 메모장 WriteFile() 후킹 (1) 2021.01.18 [리버싱 핵심원리 study] 29장 API 후킹 : 리버싱의 '꽃' (0) 2021.01.18 [리버싱 핵심원리 study] 28장 어셈블리 언어를 이용한 Code 인젝션 (0) 2021.01.15