Dynamic API Hooking Architecture Written by X-Type User Level (ring 3) 목차 머리말 이 글에서는 Windows NT와 9x에 대해 정적(Static)이 아닌 동적(Dynamic)인 API Hooking을 하는 방법에 대해 자세히 알아보고자 한다. API Hooking…? 본론으로 들어가기 전에 먼저 API Hooking이 무엇인지 짚고 넘어가자. API Hooking은 어떤 특정한 프로그램 또는 시스템 전역에서 API가 호출되는 것을 가로채는 것을 말한다. (그러나 반드시 API를 대상으로 하지는 않는다) 개발자들이 이 문제에 도전하는 이유는, API Hooking을 해서 얻는 것이 많기 때문이다. API 호출을 가로챈다면, 주어진 인자 또는 리턴값을 수정하여 프로그램의 동작을 제어할 수 있게 된다. 또한 API 호출을 로그(Log)로 남긴다면, 프로그램이 어떤 식으로 동작하는지 알 수 있다. (의외로 그 효과는 크다) 그렇다면, 정적 API Hooking은 무엇이고 동적 API Hooking은 또 무엇인가? 둘의 차이는 API를 어느 정도 후킹할 수 있는지의 차이이다. 정적 API 후킹의 경우, 특정한 코드로 짜여져서 특정 API만 후킹하는 것이다. 동적 API 후킹은 런타임(Run-Time)에서 사용자의 요구에 따라 API를 상황에 따라 다르게 후킹하도록 된 것이다.
<동적 API 후킹과 정적 API 후킹의 차이> 동적 API 후킹은 불안전한 면을 다소 가지고 있다. 예를 들어, 유저가 인자 개수를 틀리게 작성해주는 경우가 있다. 이럴 경우에는 스택이 잘못 정리되므로, 후킹 도중에 프로그램이 튕기게 된다. (디스어셈블러를 이용하여 보정해 줄 수는 있다. 그러나 완벽하지는 않다.) Dynamic API Hooking…? 이번에는 동적 API 후킹이 무엇인지 알아보자. 그 정의에 관해서는 앞에서 말했기 때문에, 여기서는 구현 방식에 대해 간단하게 설명하도록 하겠다. 우선 정적 API 후킹 방식에 대해 이해해야 한다. 정적 API 후킹은 이런 방식으로 작성된다: typedef int (WINAPI *API_MESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT); int WINAPI MessageBoxAProc(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); API_MESSAGEBOX pfnOriginalMessageBoxA; LPVOID HookAPI(PROC Api, PROC ApiProc, DWORD Argc){ // API 후킹 구현 } void main(){ pfnOriginalMessageBoxA = HookAPI( GetProcAddress(LoadLibrary(“User32.dll”), “MessageBoxA”), (PROC)&MessageBoxAProc, 4 ); } int WINAPI MessageBoxAProc(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType){ … return pfnOriginalMessageBoxA(hWnd, lpText, lpCaption, uType); } 이런 식으로 API 후킹 프로시저(API 후킹을 가로챈 뒤에 어떠한 작업을 해주는 함수)를 선언, 정의하고 API 후킹을 시도하는 것을 볼 수 있다. 그렇다면 API 프로시저를 런타임에서 생성할 수 있다면 동적 API 후킹이 되는 것인가? 가능은 하겠지만 비효율적인 면이 있다. C/C++ 코드는 옮기면 깨질 가능성이 크기 때문에 어셈블리로 짜야 한다. 또한 스택 정리를 위해 함수가 약간씩 수정되어야 한다. (이 과정에서 디스어셈블러가 필요하다.) 1)API 프로시저의 크기가 크다면 이러한 동적 API 후킹 방식은 매우 비효율적인 방법이 될 수밖에 없다. 그렇다면 어떤 식으로 구현되어야 하는가? 필자는 동적 API 후킹을 하려면 함수의 동적인 생성(복사)은 필연적이라고 생각한다. 2) 만약에 함수를 런타임에서 생성시킨다면 그 크기를 최소화하는 것이 좋을 것이다. 그러나, API 후킹 프로시저를 복사하는 것은 앞에서도 말했듯이 비효율적이다. 그렇다면 어떻게 해야 할까? 필자는 API 프로시저를 하나만 두고도 여러 가지 API의 후킹이 가능하도록, 동적으로 생성된 여러 개의 작은 함수들과, 1개의 내부3) API 프로시저로 외부4)의 API 프로시저를 연결시켰다. 이 과정이 약간 어렵다. 왜냐하면 순수 C/C++로는 구현이 불가능하기 때문이다. 즉, 어셈블리를 써야 한다. 자세한 것은 다음에 말하도록 하겠다. 1) 이것을 구현하기에는 문제가 많다. C/C++에서 함수를 호출하면 보통 컴파일러가 절대 주소 참조를 하지 않고 상대 주소 참조를 한다. 이러한 코드는 코드의 위치를 옮기게 되면 모두 깨진다. Volatile 키워드가 붙은 함수 포인터를 사용하여 절대 주소 참조로 만들 수는 있다. (그러나 이 방법으로는 속도, 용량에 모두 손해를 본다.) 그리고 컴파일러에 따라서 같은 함수라도 어셈블리 상의 구현이 다를 수 있다. 즉, 함수의 크기가 일정하지 않기 때문에 주의를 요한다. 2) 다수의 API를 가로채어 같은 API 프로시저에 오게 한다면 API를 구분할 수 없다. API를 구분할 수 있게 하는 루틴이 API마다 필요하다. 3) 필자는 API 후킹이 쉽도록 API 후킹 객체를 작성하고 있다. 즉, 객체 내부에서 구현하고 있는 API 프로시저를 말한다. 4) 객체 바깥에서 구현한 API 프로시저를 말한다. API 후킹 방식 API를 후킹하는 방법에 대해 간단히 알아보자. 동적인 후킹이든 정적인 후킹이든 모두 일정한 API 후킹 방법을 기초로 하여 작성된 것들이기 때문에 알아둘 필요가 있다. DLL이 필요한 방법은 DLL을 이용하여 다른 프로세스의 메모리에 침투할 필요가 있는 방법들이다. a. Hook Imported Function 이 방법은 임포트 테이블(Import Table)이라는 것을 수정하는 방법이다. 프로그램이 특정 API를1) 호출하는 코드를 작성했다면, 보통 API의 주소를 담고 있는 테이블을 이용하여 API를 호출하는데 그 테이블을 수정하여 API 후킹을 시도하는 것이다. 이 방법은 특정 모듈2)에 대해서만 API 후킹이 가능하다. 또한 임포트 테이블을 통하지 않고 API의 주소를 알아내어 함수를 호출할 수 있으므로, 3) 모든 API 호출이 후킹되지 않는다. 그러나 이 방법은 다른 방법에 비해 비교적 간단하고 좀 더 안정적으로 동작한다. 1) API뿐만 아니라 외부 DLL에서 export한 모든 함수들이 그 대상이다. 2) 모듈(Module): 프로세스에 로드(load)된 프로그램(exe, dll, …)을 말한다. 3) LoadLibrary, GetProcAddress API를 이용하면 API의 주소를 알아낼 수 있다. 예를 들어 보자. GetModuleHandleA(NULL); 00404663 6A 00 push 0 00404665 FF 15 50 12 42 00 call dword ptr [__imp__GetModuleHandleA@4 (00421250)] 여기서 __imp__GetModuleHandleA@4 즉, 메모리 주소 0x421250의 값을 읽어서 호출하는 것을 볼 수 있다. 0x421250의 값을 수정한다면, 이 함수의 호출을 가로챌 수 있을 것이다. (자세한 구현 방법은 찾아보면 나온다. 찾기가 정 귀찮다면 이곳(1)을 참고하기 바란다.) Windows에서는 각각의 프로세스의 메모리가 분리되어 있기 때문에, 다른 프로세스의 임포트 테이블을 건드리려면 DLL을 대상 프로세스에 삽입(Inject)하여야 한다. DLL을 삽입하지 않게 할 수도 있지만 구현이 훨씬 복잡하게 된다. 여기서는 DLL 삽입(DLL Injection)에 관한 방법은 다루지 않는다. 이것에 관한 부분은 API Hooking Revealed(2)에서 찾아볼 수 있고, CreateRemoteThread(이 API는 NT 계열에서만 구현되어 있다. 그러나 9x에서도 가능하다)를 이용한 DLL Injection 기법은 Remote Library(3)에서 모든 Windows에 대해(9x, NT, …) 다루고 있다. b. Trampoline 이 방법은 API 함수 자체의 코드를 변경시키는 방법이다. 그래서 구현이 까다롭다. 이 방법은 API 함수의 첫 부분에 jmp 명령어를 넣어서 API 프로시저로 오게 하여 호출을 가로채는 방식이다. 원본 API를 호출하기 위해서 복잡한 과정을 필요로 한다. 이렇게 복잡하지만 프로세스 내의 모든 API 호출이 후킹되는 장점이 있다. 타겟 API는 jmp 명령어가 들어갈 수 있는 충분한 크기를 가져야 하기 때문에, 함수의 크기가 jmp 명령어의 크기인 5 바이트보다 작을 경우 후킹할 수 없다는 단점이 있다. 예를 들어 보자. // FindWindowA API 원본 (편의상 뒷부분을 생략했다) 77CFE8FF 33 C0 xor eax,eax 77CFE901 50 push eax 77CFE902 FF 74 24 0C push dword ptr [esp+0Ch] 77CFE906 FF 74 24 0C push dword ptr [esp+0Ch] // FindWindowA API 후킹 후 77CFE8FF E9 DC 2F C1 88 jmp 009118E0 77CFE904 90 nop 77CFE905 90 nop 77CFE906 FF 74 24 0C push dword ptr [esp+0Ch] // Trampoline 함수 (원본 API를 이것을 통해 호출) 00911920 33 C0 xor eax,eax 00911922 50 push eax 00911923 FF 74 24 0C push dword ptr [esp+0Ch] 00911927 E9 DA CF 3E 77 jmp 77CFE906 어떤 방식인지 감이 올 것이다. 주의할 점이 있다면 jmp 명령어를 삽입할 때 원본 명령어 크기를 고려하여 nop를 넣어주어야 한다는 것이다. Jmp 명렁어의 크기인 5바이트만을 복사하게 되면 위의 함수와 같은 경우에는 명령어가 깨질 수가 있다. 77CFE8FF E9 DC 2F C1 88 jmp 009118E0 77CFE904 24 0C and al, 0Ch (잘못됨) 77CFE906 FF 74 24 0C push dword ptr [esp+0Ch] 이 함수의 경우, 이렇게 잘못 수정되면 인수 하나가 넘겨지지 못하게 된다. Access Violation(엑세스 위반)이 발생할 것은 뻔하다. 그러나 디스어셈블러를 쓴다면 이 문제를 해결할 수 있다. 디스어셈블러 없이 구현하려면 원본 API 호출시 잠깐 API를 복원했다가 다시 패치해주는 방법이 있으나 API가 복원되었을 때, 다른 쓰레드에서 API를 호출한다면 후킹이 되지 않는 문제가 발생한다. 이런 방법은 멀티쓰레드 환경에 맞지 않다. 이 방법도 마찬가지로 이 방법으로 다른 프로세스의 API 호출을 잡아내려면 DLL을 삽입해야 한다. (이 방법의 자세한 구현 방법은 뒤에서 말할 것이다.) c. Debugging 이 방법은 디버거를 이용하는 것이다. 이 방법은 DLL을 사용하지 않는 방법이다. 우선, 대상 프로세스에 디버거를 활성(Active)시킨다. 그 다음에 함수 첫 부분에 브레이크 포인트(BreakPoint, 중단점)을 심어둔다. 그 함수가 호출되면 EXCEPTION_BREAKPOINT 디버그 이벤트가 발생하며 모든 쓰레드가 중지된다. 이 때 제어권이 넘어오게 되는데 이때 각종 작업을 해주는 것이다. 이 방법의 경우 프로세스 간 통신 구현이 따로 필요가 없다. 단점은 좀 많은 편이다. 우선 느리고, 디버거를 종료시키면 디버깅되는 프로세스도 같이 종료되어 버린다. 중단점에 이르면 모든 쓰레드가 멎어버리기 때문에 예기치 않은 상황이 발생할 가능성을 배제할 수 없다. 대상 프로세스를 디버그시킨다. -> 중단점(int 3)을 심는다. -> 중단점 디버그 메시지를 받는다. -> (작업) -> API 복원 -> 중단점(int 3)을 다시 심는다. 요약하자면 이정도가 된다. (참고) Win9x API Hooking Windows 9x(95, 98, Me)에서는 API 후킹이 더더욱 어렵다. System Dll(Kernel32.Dll, User32.dll, Gdi32.dll, …)들은 Windows 9x에만 존재하는 “항상 공유되는 메모리 영역”에 맵핑되어있기 때문에 문제가 발생한다. Windows 9x에서는 가상 메모리 0x80000000 ~ 0xC0000000 영역은 모든 프로세스에서 동일하다. 즉, System Dll의 API 코드를 수정하였을 경우에 문제가 된다. 모든 프로세스에 적용되기 때문에, 기존의 Trampoline API 후킹을 사용하면 에러가 난다. 참고로, Windows 9x에서는 공유 메모리 영역의 Protection을 변경할 수 없다. 공유 메모리에 위치한 System Dll의 코드는 Protection이 읽기 전용이기 때문에 수정이 불가능한 것처럼 보인다. 그러나 int 0x2E4), 즉 VxD Service5)를 이용하면 수정할 수 있다. (자세한 내용은 뒤에 언급하겠다) 1) 인터럽트(Interrupt): 예외 상황이 발생하더라도 계속해서 작업이 가능하도록 하는 운영체제의 기능. 예외 처리 외에도 다른 기능이 있다. 2) 핸들링(Handling): 예외 상황을 계속해서 작업이 가능하도록 유도하는 작업. 3) 핸들러(Handler): 예외 상황에 대응하는 루틴. 기본 핸들러(Default Handler)는 핸들링이 되어있지 않을 때에 호출된다. 4) Int 0x2E: 인터럽트 번호 0x2E의 인터럽트를 말한다. NT나 2000의 ring 06)의 서브루틴을 호출할 때 사용된다. (9x에서도 일부가 지원된다) 5) VxD Service: 운영체제 내부 구현이나 드라이버 구현에서 쓰인다. VxD(Virtual x Device)가 제공하는 서비스를 말한다. 6) Ring 0: Intel 계열의 CPU가 지원하는 “특권 레벨” 0를 말한다. 특권 레벨은 0부터 3까지 있으며, 0이 가장 높고, 3이 가장 낮다. Windows는 0과 3만을 사용하는데, 흔히 ring 0를 커널 모드, ring 3를 유저 모드라고 부른다. 7) 아직 시험해 본 적이 없어서 확신할 수 없다. 동적 API 후킹의 구현 이번에는 동적 API 후킹이 어떤 식으로 구현되는지 알아보자. 앞으로 말할 내용은 동적 API 후킹 방법 중에 한 가지 방법일 뿐이며, 다른 방법도 있다는 것을 명심하기 바란다. 필자는 Trampoline API 후킹 방식을 기초로 한다. (하지만 다른 후킹 방법을 쓸 수 있다) 다수의 API를 후킹하되 단 하나의 후킹 프로시저로 연결되도록 하는 것이 핵심이다. 그렇다면, 후킹 프로시저는 호출된 API가 무엇인지 구분할 수 있어야 한다. 따라서 동적 API 후킹에는 후킹된 API에 관한 정보를 저장해둘 필요가 있다. 하지만, 어떻게 자료를 넘겨야 할까? 필자는 API가 호출된 직후에 eax 레지스터를 이용하여 자료의 포인터를 넘기는 방법을 선택했다. (필자는 이 역할을 하는 코드를 API Specifier라고 명명했다) 그 다음에는 필자는 유저 프로시저에 직접 데이터의 포인터를 넘기는 방법을 택했다. (유저 프로시저를 C/C++ 코드로 작성하기 위해서는 이 방법이 가장 안전하다) 그러면 유저 프로시저를 연결시켜주는 과정에 대해 생각해 보자. 필자는 그 ‘과정’을 작성하기 전에 미리 몇 가지를 정해두었다. -1. 구현하는 데 필요한 어셈블리 코드를 최소화한다. - 2. 유저 프로시저에는 최소한의 데이터만을 넘긴다. - 3. 함수의 복사는 최대한 사용하지 않는다. 어셈블리 코드는 크기가 커지면 유지/보수가 곤란하다. 유저 프로시저에 데이터를 많이 넘기면 넘길수록 속도에 손해를 보게 되며, 함수의 복사는 안전하지 못하다. (구현하기도 어렵고 메모리를 쓸데없이 먹는다.) API Specifier는 유저 프로시저로 연결시켜주기 위해 특정한 함수(Connector)에 점프한다. Connector에서는 일단 데이터 포인터(eax)를 스택에 백업한다. Eax 레지스터는 차후 리턴값으로 쓰이게 되기 때문이다. 다음으로, 유저 프로시저를 실제로 호출하는 함수(Linker)를 따로 두게 되면 그 함수(Linker)의 구현을 좀더 자유롭게 할 수 있게 되고, Connector 함수의 코드의 양을 어느 정도 줄일 수가 있다. 따라서 Connector는 Linker 함수를 호출한다. Connector는 Linker에서 반환한 값으로 리턴을 한다. Connector는 API에 넘겨진 인수를 쓰지 않는다. User API Procedure가 API에 넘겨진 인수를 쉽게 조작할 수 있도록, 이를 위해 Connector는 첫 번째 인수의 포인터를 후킹 데이터와 함께 Linker에 넘겨주는 방법을 택했다. (어셈블리 코드를 줄이기 위해서) 문제는 스택 정리이다. API는 stdcall 호출 규약1)을 사용하며 API 마다 인수의 개수가 약간씩 다르다. 그러므로 각 API마다 잘 맞게 스택을 정리해줘야 한다. 유저 프로시저에 연결시켜주는 함수를 복사해서 스택 정리 부분만 수정시켜주는 방법은 곤란하다. (즉, Connector를 복사하는 방법) 메모리 효율이 떨어지기 때문이다. 필자는 스택을 정리하는 부분만을 API마다 따로 두고(필자는 API Specifier에 두었다) Connector는 사용한 스택을 모두 정리한 뒤, 데이터에 포인터로 기록되어 있는 스택을 정리하는 부분으로 점프한다. (이것 때문에 Connector는 C/C++로 구현될 수 없다) 이렇게 위의 조건을 최대한 따르면서 API 후킹을 구현해 보았다. 지금까지의 과정을 요약하면 다음과 같다. (구현 과정을 알아보기 쉽도록 C 코드와 어셈블리를 섞어 썼다.) 참고: cdecl 호출 규약1)을 가진 모든 함수에서의 스택 정리는 ret 0이나 ret로 하면 된다. API Specifier, Connector는 모두 어셈블리로 작성되며, Linker는 C/C++로 작성해도 무방하다. 참고로 리턴 과정은 이러하다. User Proc -> Linker -> Connector -> API Specifier -> (End) 위에서는 원본 API를 호출하는 것이 보이지 않는다. 그것은 User API Procedure의 몫이다. 데이터를 통해 Trampoline 함수의 포인터가 제공되며, User API Procedure에서는 그것을 이용하여 원본 API를 호출할 수 있다. (이 과정은 인라인 어셈블리를 필요로 하므로 따로 함수를 만들어 두는 것이 좋다.) 주의: User API Procedure에서 후킹된 API를 호출할 경우에도 위의 순서대로 수행되어 무한 루프에 빠질 수 있다. 해결책은 뒤에 언급하겠다. 1) 호출 규약: 함수를 어떻게 호출할 것인지 정해놓은 일종의 약속이다. stdcall 호출 규약은 함수에 인수를 넘기는 데 쓴 스택을 함수 내에서 정리하는 호출 규약을 말한다. 반대로 cdecl 호출 규약은 함수를 호출한 쪽에서 스택을 정리하는 것이다. 여기까지의 구현으로는 Windows NT에서만 잘 동작한다. Windows 9x에서 System DLL API의 호출을 가로채려면 좀 더 복잡한 구현이 필요하다. 그렇다면 Windows 9x에서는 어떻게 해야 하는지 알아보자. 우선, System DLL API를 수정하는 방법에 대해 알아보자. Windows 9x의 VxD 서비스 중에 RtlCopyMemory라는 것이 있다. 이것을 이용하면 읽기 전용의 메모리 영역이라도 메모리 입출력을 할 수 있다. push nSize // 원본, 대상의 크기 push pSrc // 원본 (Source) 메모리 주소 push pDest // 대상 (Destination) 메모리 주소 mov edx, esp // edx: 인수 포인터 mov eax, 0x10A // eax: RtlCopyMemory 서비스 번호 int 0x2E // RtlCopyMemory(pDest, pSrc, nSize) add esp, 12 // 스택 정리 간단한 어셈블리 코드이다. 우선 인수들을 넘기고 가장 첫번째 인수의 주소를 edx 레지스터에, 서비스 번호를 eax 레지스터에 넘기고 인터럽트 0x2E를 발생시키면 nSize만큼의 pSrc가 pDest에 복사된다. 참고로, 원본이나 대상의 메모리를 읽을 수 없을 경우 무서운 블루스크린이 뜨니 저 코드를 실행하기 전에 VirtualQuery함수로 해당 메모리가 존재하는지 체크하는 것이 좋다. 또한 저 코드는 NT에서 동작하지 않는다. (NT에서는 필요가 없겠지만) 하지만 공유 영역에 있는 System DLL API를 수정하면 모든 프로세스에 적용되기 때문에 추가로 구현해주어야 할 것이 있다. 일단은 현재 프로세스가 “후킹되었는지” 검사해야 한다. 만약 후킹되었으면 API Specifier로 jump하고, 후킹되지 않았으면 Trampoline 함수로 jump하면 된다. 일단 후킹된 프로세스 리스트(9x Stub Data), API Specifier, Trampoline 함수를 공유 영역에 할당할 필요가 있다. 주의: System DLL은 공유 메모리 영역에 위치하기 때문에 수정에 신중을 기하여야 한다. 잘못된 코드를 실행하게 되면 실행중인 모든 프로세스가 튕기는 현상이 발생한다. 또한 모든 작업을 마친 후 코드를 원래대로 돌려놓지 않을 경우에도 문제가 발생할 수 있다. 메모리를 직접 할당하는데 쓰이는 VirtualAlloc함수의 fAllocationType에는 VA_SHARED라는 Flag가 있어서 이것을 쓰면 공유 영역에 메모리를 할당할 수 있다. (NT에서는 당연히 안된다) VA_SHARED의 값은 0x8000000이다. 이렇게 할당해주면 될 것이다. #define VA_SHARED 0x8000000 LPVOID pSharedData = ::VirtualAlloc(NULL, dwSize, fAllocationType | VA_SHARED, flProtect); 이렇게 공유 영역에 할당한 데이터를 프로세스 검사 루틴에 넘겨주기 위해서는 마찬가지로 eax 레지스터를 통해 넘겨주면 된다. 1) 이렇게 넘겨진 데이터로 후킹되었는지를 검사해서 적절히 jump하면 된다. 주의할 점은 Trampoline 함수는 후킹되지 않은 프로세스에서도 쓰이므로 공유 영역에 할당3)하고, API Specifier는 프로세스별로 다르게 할당2)되어 있으므로 프로세스 리스트와 묶어서 관리해야 한다. 그리고 이렇게 할당한 데이터를 해제하기 위해서는 몇 개의 프로세스가 후킹되었는지 저장해둘 필요가 있다. 후킹을 해제할 때 맨 마지막 후킹된 프로세스일 경우에만 공유 메모리에 할당된 데이터를 최종적으로 해제해야 한다. 1) 필자는 이러한 역할을 수행하는 코드를 9x Specifier라고 이름 붙였다. 9x Specifier는 모든 프로세스에서 유효해야 하므로 공유 메모리에 할당되어야 한다. 또한 프로세스 검사 루틴 역시 공유 메모리에 위치해야 한다) 2) 이것도 공유 메모리에 할당할 필요는 없다. 공유 메모리에 할당하는 것은 좁은 공유 영역을 낭비하는 것이다. 3) Trampoline 함수, 9x Specifier 함수는 공유 영역에 위치하기 때문에 한번만 할당해도 여러 프로세스에서 써먹을 수 있다. 정리하자면, 공유 메모리에 할당되어야 할 것들은 9x Specifier, Trampoline 함수, 프로세스 검사 루틴, 프로세스 리스트이다. 프로세스 리스트에 포함될 데이터는 Trampoline 함수 주소 (전 프로세스 공통), 후킹된 프로세스 개수, API Specifier가 포함된 프로세스 리스트이다. 프로세스 검사 루틴은 공유 메모리에 할당되어야 하기 때문에 이 함수를 복사해야 한다. 안전한 복사를 하려면, 모두 어셈블리로 짜야 하고, 직접적인 jmp, call 구문을 쓰지 않아야 한다. 주소값을 담고 있는 포인터나 레지스터를 이용해서 jmp, call을 해야 한다. 프로세스 리스트에서, Windows 9x에서는 프로세스 ID 대신 프로세스 Database 포인터를 쓸 수 있다. 번거롭게 GetCurrentProcessId를 호출하는 것보다 훨씬 구현이 쉽다. LPVOID pCurrentProcessDatabase; __asm{ mov eax, fs:[0x30] // Current Process Database = fs:[0x30] mov [pCurrentProcessDatabase], eax } 이런 식으로 현재 프로세스의 데이터베이스 주소를 구할 수 있다. 지금까지의 과정을 그림으로 나타내 보면 이러하다. 위의 그림에서는 빠뜨렸는데, 프로세스 체크 함수도 공유되어야 한다. (위에도 언급했었지만) 주의: 프로그램이 예기치 못한 에러로 종료될 경우, API가 제대로 복원되지 않아 문제를 발생시킬 수 있다. (메모리 누수, 전체 프로세스 종료 현상) 따라서 SetUnhandledExceptionFilter를 써서 예외 상황에서 API를 복원해 주는 것이 좋다. 이 방식으로 예외 처리를 할 때, API 후킹을 객체로 짤 경우, 인스턴스를 한 개로 제한할 필요가 있다. 문제 해결하기 이번에는 위에서 언급했던 문제에 관해 알아보자. 첫번째로 API 코드 덮어쓰기에 문제가 있었다. 명령어가 깨지는 문제이다. 디스어셈블러를 이용하여 API를 명령어 단위로 분석하여 최소 5 바이트를 확보하면 이 문제는 해결된다. 디스어셈블러는 어떻게 써야 하는 것인가? ……물론 직접 만들거나 다른 디스어셈블러 소스를 쓰면 된다. 디스어셈블러를 만들려면 기계어의 해독 방법을 알아야 한다. 별로 어렵지는 않다. CPU 제조 회사 홈페이지에 찾아가면 어렵지 않게 관련 문서를 구할 수 있다. (모두 영어지만) 필자가 하나 번역해 둔 것(4)이 있으니 참고하기 바란다. 두번째로 User API Procedure에 의해 무한 루프에 빠져버리는 문제이다. 먼저 생각해 볼 수 있는 방법은 API가 후킹되었는지 확인하고, 후킹되었으면 Trampoline 함수를 호출하고, 그렇지 않으면 원본 API를 바로 호출하는 것이다. API 후킹 데이터와 일일이 비교하는 방법도 있으나, 필자의 후킹 방식을 쓸 경우에, API 첫 부분에 jmp가 있는지 확인하고, jmp가 있으면 그 주소를 추적한다. 그 주소가 Specifier라면, (mov로 시작하고, 다음 명령어가 jmp인지 체크해본다.) 데이터 주소를 얻어낼 수 있다. 이때 이 데이터를 이용하여 Trampoline 함수의 주소를 알아내고 그것을 호출하는 것이다. 물론 후킹된 API가 아니면 그대로 호출해주면 된다. 둘 모두 인라인 어셈블리를 필요로 한다. 그러나 이 방법으로는 겉으로 보이는 호출만 막을 수 있지 다른 라이브러리를 통한 API 호출은 막을 수 없다.1) 자세한 방법은 뒤에 언급할 것이다. 다른 방법으로는 리턴 주소를 체크하여 API 프로시저에서 호출한 것이라면 바로 원본 API를 호출하고 리턴하는 방법이 있다. 2) 1) 예를 들어서 malloc같은 함수가 있다. 내부적으로 HeapAlloc API를 호출한다. 만약에 API 후킹 프로시저에서 malloc를 호출하고 HeapAlloc 함수가 후킹되어 있다면 그 방법으로는 막을 수 없다. 2) 이 방법도 마찬가지다. 라이브러리를 정적 링크하고 DLL 전체에서 검사한다면 막을 수는 있다. 3) 그러나 위의 방법 모두 Win9x에서 디버깅되는 프로세스에서는 통하지 않는다. Win9x에서 디버그중인 프로세스에서는 직접적으로 System DLL의 API를 호출하지 않고 간접적으로 API를 호출하도록 Windows에서 조치를 취하기 때문이다. 코딩하기 자 이제 코딩을 해 보자. 일단 누구나 쉽게 API 후킹을 할 수 있도록, 객체로 짜는 것이 좋겠다. 우선 필요한 데이터들을 알아보자. (1 바이트 구조체 정렬을 했다) typedef struct _API{ api_hook *HookObj; // 00 Hooking Object const _APIHOOK *HookInfo; // 04 Hooking Information void *ApiCaller; // 08 Return Address }API; API 프로시저에 넘겨질 데이터이다. 후킹 객체, API 정보, 리턴 주소(함수 수행이 끝났을 때 리턴되는 곳)가 있다. 후킹 객체는 API 정보에도 담겨 있지만, 편의상 제공했다. typedef struct _APIHOOK{ PROC fnTrampoline; // 00 trampoline 함수 PROC fnApi; // 04 원본 api 함수 PROC fnSpecifer; // 08 API Specifier APIPROC fnProc; // 12 User API procedure DWORD fnArgc; // 16 API 인수 개수 DWORD fnApiOffset; // 20 API 코드에 덮어쓴 크기 LPVOID UserData; // 24 user data api_hook *HookObj; // 28 후킹 객체 _9XSTUB *pStub; // 32 stub data (9x) DWORD idxDat; // 36 pStub->Data[idxDat] PROC fn9xSpecifier; // 40 9x Specifier }APIHOOK; API 후킹 정보이다.
typedef struct __9XDATA{ LPVOID pPDB; // 00 Process DataBase LPVOID pSpec; // 04 API Specifier }_9XDATA; typedef struct __9XSTUB{ LPVOID pTrampoline; // 00 Trampoline Function (공유) DWORD nProcHooked; // 04 후킹된 프로세스 개수 _9XDATA Data[1]; // 08(+8n) 프로세스 리스트 본체 }_9XSTUB; 9x용 데이터이다. 9x data는 프로세스를 구분하는데 쓰일 프로세스 데이터베이스(PDB)와 API Specifier가 있다. 여기서 주의할 것은 API Specifier는 공유 메모리에 할당되지 않는다는 것이다. (즉, 프로세스마다 따로 할당) class api_hook{ private: static const int procchk_len; // Process Checker 길이 static const int stubdata_len; // Stub Data 크기 list<APIHOOK> m_data; // APIHOOK DATA disasm m_disObj; // Disassembler // Skeleton Codes static BYTE ApiPatchCode[]; // API에 덮어쓸 코드 static BYTE ApiSpecifer[]; // api specifier static BYTE Api9xSpecifier[]; // 9x api specifier PROC m_ProcChecker; // 9x process checker (공유) // 예외 처리 static LPTOP_LEVEL_EXCEPTION_FILTER pTopFilter; static api_hook *pInstance; static LONG __stdcall UnhandledExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo); public: static ah_osver osver; // Operating System Version Checker // simple VirtualProtect static inline DWORD VirtualProtect(void *lpAddress, DWORD dwSize, DWORD flNewProtect){ DWORD flOldProtect = 0; ::VirtualProtect(lpAddress, dwSize, flNewProtect, &flOldProtect); return flOldProtect; } // Intercept Function Calls (stdcall, cdecl) const APIHOOK *InterceptApi(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData = 0); const APIHOOK *InterceptCdecl(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData = 0); // stop Intercepting (stdcall, cdecl) int RestoreApi(PROC fnApi); // api hook data const list<APIHOOK> &GetData() const{return m_data;} // call trampoline function int CallTrampolineAPI(const API *api, LPVOID pFirstArg); // stdcall int CallTrampolineCdecl(const API *api, LPVOID pFirstArg); // cdecl // copy memory (9x, NT) static void _stdcall _RtlCopyMemory(void *pDest, const void *pSrc, unsigned int nSize); api_hook(); virtual ~api_hook(); }; 클래스 선언문이다. 지금까지 쭉 설명한 내용이 모두 들어가 있음을 알 수 있다. 이 부분은 별로 어렵지 않으니 넘어가도록 하겠다. 이제 connector, linker, Process Checker를 짜 보자. ////////////////////////////////////////////////////////////////////////// // 내부 함수 정의 부분 (헤더 파일에 포함) // connector에서 데이터를 받아 API 프로시저를 호출하는 linker 함수. static int __stdcall api_hook_linker(APIHOOK *pSpecApi, PVOID pStackObject){ // pStackObject -> 첫번째 인수의 주소이다. [첫번째 인수의 주소]-4는 리턴 주소를 나타낸다. API api = {pSpecApi->HookObj, pSpecApi, (PVOID)(*((DWORD *)((BYTE *)pStackObject-4)))}; return pSpecApi->fnProc(&api, pStackObject); } #define pos_spec 8 // APIHOOK에서의 Specifier 위치 #define pos_spec_2 10 // Specifier에서의 ret xx위치 // connector 함수 (Specifier->Linker) static int __declspec(naked) __stdcall api_hook_connector(){ __asm{ // API Hooking 데이터 백업 push eax // api_hook_linker(pSpecApi, pStackObject); lea ecx, [esp+8] push ecx push eax call api_hook_linker // Specifier의 ret xx 위치 추적 mov ecx, dword ptr [esp] mov ecx, dword ptr [ecx+pos_spec] add ecx, pos_spec_2 // 스택 정리 후 점프 add esp, 4 jmp ecx } } #undef pos_spec #undef pos_spec_2 // Process Checker 함수 // 지역 변수 #define pCurPDB ecx #define idx edx #define i edi #define pScanPDB esi #define pStub ebp-4 #define local_size 4 // 지역 변수 크기 static int __declspec(naked) __stdcall api_hook_proc_checker(){ __asm{ // EAX: APIHOOK::pStub // 레지스터 백업 push edx push edi push esi push ebp mov ebp, esp sub esp, local_size // pStub = APIHOOK::pStub; mov [pStub], eax // pCurPDB = fs:[0x30]; mov pCurPDB, fs:[0x30] // idx = -1; mov idx, -1 // i = 0; xor i, i // pScanPDB = &pStub->Data[0].pPDB; mov esi, [pStub] } MAIN_LOOP: __asm{ // to next pPDB add pScanPDB, 8 // if (pStub->Data[i].pPDB == pCurPDB) goto MATCH_PROC; cmp pCurPDB, dword ptr [pScanPDB] je MATCH_PROC // i++ inc i // if (i<1024) goto MAIN_LOOP; cmp i, 1024 jl MAIN_LOOP // else goto LOOP_END; jmp LOOP_END } MATCH_PROC: __asm{ // idx = I; mov idx, i } LOOP_END: __asm{ cmp idx, 0xFFFFFFFF // cmp idx, -1 je TRAMPOLINE jmp SPECIFIER // if (idx == -1) goto TRAMPOLINE // else goto SPECIFIER } TRAMPOLINE: __asm{ // eax = TRAMPOLINE FUNCTION mov eax, [pStub] mov eax, [eax] jmp END } SPECIFIER: __asm{ // eax = API SPECIFIER FUNCTION mov eax, [pScanPDB+4] } END: __asm{ add esp, local_size pop ebp pop esi pop edi pop edx // jump TRAMPOLINE or SPECIFIER jmp eax } } #undef pCurPDB #undef idx #undef i #undef pStub #undef pScanPDB #undef local_size 여기까지가 헤더 파일(.h)에 들어갈 내용들이다. 주석에 설명이 되어있으니 다음으로 넘어가자. 이제 .cpp를 작성해 보자. #define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr)+(DWORD)(addValue)) // 후킹될 수 있는 최대 프로세스 개수 (값 수정시 Process Checker 수정 필요) #define STUB_DATA_COUNT 1024 // 공유 메모리 영역에 메모리를 할당하도록 하는 Flag (VirtualAlloc) #define VA_SHARED 0x8000000 // 운영체제 버전 체크. IsWinxx 형식의 함수들이 있음. ah_osver api_hook::osver; // 9x Stub Data 크기 const int api_hook::stubdata_len = sizeof(_9XSTUB)+(sizeof(_9XDATA)*(STUB_DATA_COUNT)-1); // 9x Process Checker의 크기 (직접 측정. VC++의 Disassembly를 썼다.) const int api_hook::procchk_len = 75; // 예외 처리 LPTOP_LEVEL_EXCEPTION_FILTER api_hook::pTopFilter = NULL; api_hook *api_hook::pInstance = NULL; // Api에 쓰여질 코드. 명령어 길이를 맞추기 위해서 0x90 (nop-아무 동작도 수행하지 않음) 을 넣었다. BYTE api_hook::ApiPatchCode[]={ 0xE9, 0x00, 0x00, 0x00, 0x00, // jmp rel32 (Api Specifier) 0x90, 0x90, 0x90, 0x90, 0x90 }; // Api Specifier BYTE api_hook::ApiSpecifer[]={ 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, imm32 (data ptr) 0xE9, 0x00, 0x00, 0x00, 0x00, // jmp rel32 (connector) 0xC2, 0x00, 0x00 // ret xx }; // 9x Api Specifier BYTE api_hook::Api9xSpecifier[]={ 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, imm32 (9x stub ptr) 0xE9, 0x00, 0x00, 0x00, 0x00 // jmp rel32 (proc_checker) }; 여기까지가 선언문이다. 이제 본격적으로 코딩에 들어가 보자. // 예외 처리. 예외 발생시 파괴자 호출 후 프로세스 종료 LONG api_hook::UnhandledExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo){ pInstance->~api_hook(); return EXCEPTION_CONTINUE_SEARCH; } // 생성자 api_hook::api_hook(){ // 인스턴스 제한 ASSERT(!pInstance); // UnhandledExceptionFilter 설정 pInstance = this; pTopFilter = SetUnhandledExceptionFilter(&api_hook::UnhandledExceptionFilter); // Win9x: Process Checker를 공유 메모리에 할당한다. if (osver.IsWin9x()){ m_ProcChecker = (PROC) VirtualAlloc(NULL, procchk_len, MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE); memcpy(m_ProcChecker, &api_hook_proc_checker, procchk_len); }else{ // WinNT: Process Checker 없음 m_ProcChecker = 0; } } // 파괴자 api_hook::~api_hook(){ // UnhandledExceptionFilter 복원 SetUnhandledExceptionFilter(pTopFilter); list<APIHOOK>::iterator it = m_data.begin(); // 후킹된 API를 복원한다 while(m_data.size()){ const APIHOOK &data = *it; RestoreApi(data.fnApi); it = m_data.begin(); } // Win9x: Process Checker를 해제한다. if (m_ProcChecker){ VirtualFree(m_ProcChecker, NULL, MEM_RELEASE); m_ProcChecker = 0; } } // __stdcall 호출 규약의 함수 호출을 후킹한다. // fnApi: 후킹할 함수 주소, fnProc: API 후킹 프로시저, fnArgc: 후킹될 함수의 인수 개수, UserData: 사용자 정의 데이터 // 리턴값: API 후킹 데이터 const APIHOOK *api_hook::InterceptApi(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData){ // 함수가 모두 주어졌는가? if (!fnApi || !fnProc) return NULL; // Win9x: '진짜' API 함수 주소를 얻어 온다. // Windows 9x에서는 System DLL이 디버깅되는 것을 방지하기 위해, // 디버거가 실행중일 때에는 GetProcAddress()가 // 동적 할당된 코드의 주소를 리턴하며, 임포트 테이블의 내용도 모두 바뀐다. // 그 코드의 가장 첫 부분은 API 주소를 push 하는 명령으로 되어 있다. if (osver.IsWin9x()){ if (*(PBYTE)fnApi == 0x68) { fnApi = (PROC)*(DWORD *)((PBYTE)fnApi + 1); } } // 증분 링크 체크 // Microsoft Visual C++은 실행중에도 코드를 수정할 수 있게끔 // 증분 링크를 사용하여 함수의 주소가 실제 함수를 가리키지 않고, 실제 함수로 jmp 하는 // 명령의 주소로 되어 있다. // 참고: System DLL일 경우에 무시 if ((DWORD)fnApi < 0x80000000 && *(PBYTE)fnApi == 0xE9){ fnApi = (PROC)((int)(fnApi) + (*(int *)((PBYTE)fnApi+1)) + 5); } // 중복 후킹은 에러의 원인 중 하나다. (중복 후킹 막기) // fnApi가 이미 후킹되었는가? (데이터 검색) list<APIHOOK>::iterator it; list<APIHOOK>::iterator end = m_data.end(); for (it=m_data.begin(); it!=end; it++){ if (it->fnApi == fnApi) return 0; } // list에 추가할 데이터 APIHOOK data; // 데이터 셋팅 data.fnArgc = fnArgc; // 인수 개수 data.fnApi = fnApi; // API 주소 data.fnProc = fnProc; // API Procedure data.UserData = UserData; // 사용자 정의 data.HookObj = this; // 후킹 객체 // API에 덮어쓸 코드의 크기를 측정한다. // 디스어셈블러를 사용하여 명령 크기 단위로 5 바이트 이상의 공간을 확보한다. int WriteSize = 0; while (WriteSize < 5) WriteSize += m_disObj.GetLineSize(((BYTE *)fnApi)+WriteSize); // 0x90 (nop)가 모자랄 경우. // 이론상 에러는 나지 않지만 예외 상황을 대비하여 0을 리턴함. if (WriteSize > sizeof(ApiPatchCode)) return 0; // API 의 복원을 위해 덮어쓴 코드 크기를 저장해 둔다. data.fnApiOffset = WriteSize; BYTE *TrampCode = new BYTE [WriteSize+5]; // Trampoline 함수 할당 BYTE *SpecCode = new BYTE [sizeof(ApiSpecifer)]; // api specifier 할당 BYTE *PatchCode = new BYTE [WriteSize]; // API에 덮어쓸 코드 할당 // 메모리 할당 실패 if (!PatchCode || !TrampCode || !SpecCode){ delete [] PatchCode; delete [] TrampCode; delete [] SpecCode; return 0; } // PatchCode: jmp SpecCode memcpy(PatchCode, ApiPatchCode, WriteSize); *((int *)&PatchCode[1]) = SpecCode -((BYTE *)fnApi) -5; // SpecCode: mov eax, data // jmp api_hook_connector // ret argc*sizeof(int) memcpy(SpecCode, ApiSpecifer, sizeof(ApiSpecifer)); *((WORD *)(SpecCode+11)) = fnArgc * sizeof(int); *((int *)(SpecCode+6)) = ((BYTE *)(void *)&api_hook_connector) -(&SpecCode[6]) -4; // TrampCode: <원본 API 코드> (API 복원시에도 활용) // jmp <덮어써지지 않은 API 코드 시작 부분> memcpy(TrampCode, fnApi, WriteSize+5); TrampCode[WriteSize] = 0xE9; *((int *)&TrampCode[WriteSize+1]) = ((BYTE *)fnApi+WriteSize+1) -(&TrampCode[WriteSize+1]) -5; // 동적 할당한 함수들의 메모리 Protection 재설정 VirtualProtect(PatchCode, WriteSize, PAGE_EXECUTE_READWRITE); VirtualProtect(SpecCode, sizeof(SpecCode), PAGE_EXECUTE_READWRITE); VirtualProtect(TrampCode, WriteSize+5, PAGE_EXECUTE_READWRITE); // 함수가 System DLL에 속해 있지 않거나 WinNT인 경우 if (osver.IsWinNT() || (DWORD)fnApi < 0x80000000){ // 메모리 Protection 변경 후 코드를 덮어씀. (안정성을 위해 가장 마지막에 덮어씀) // DWORD oldProtect = VirtualProtect(fnApi, WriteSize, PAGE_EXECUTE_READWRITE); // memcpy(fnApi, PatchCode, WriteSize); // VirtualProtect(fnApi, WriteSize, oldProtect); // Win9x에서 함수가 System DLL에 속해 있는 경우 }else{ PVOID pfn9xSpec = 0; // 9x Specifier _9XSTUB *p9xStub = 0; // 9x Stub Data PVOID pfn9xTramp = 0; // 9x Shared Trampoline Function // System DLL의 함수는 모든 프로세스에 적용된다. // 프로세스 별로 API를 후킹하려면, Stub 데이터를 한번만 할당해도 된다. // 따라서 중복 할당을 막기 위해 Stub 데이터 주소를 추적한다. // 9x Specifier 추적 (API 첫부분에 jmp 코드가 있을 경우) if (*(PBYTE)fnApi == 0xE9){ // 9x Specifier pfn9xSpec = (PROC)((int)(fnApi) + (*(int *)((PBYTE)fnApi+1)) + 5); // 올바른 9x Specifier인가? (맨 첫 명령이 mov인가?) if (*(BYTE *)pfn9xSpec != 0xB8) pfn9xSpec = 0; // 9x Stub Data 추적 p9xStub = (_9XSTUB *)(*(DWORD *)((BYTE *)pfn9xSpec+1)); } // 9x Specifier가 존재하지 않는 경우, 아직 후킹되지 않은 API이다. if (!pfn9xSpec){ // 9x Specifier 할당 (공유) pfn9xSpec = VirtualAlloc(NULL, sizeof(Api9xSpecifier), MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE); memcpy(pfn9xSpec, Api9xSpecifier, sizeof(Api9xSpecifier)); // 9x stub data 할당 (공유) p9xStub = (_9XSTUB *)VirtualAlloc(NULL, stubdata_len, MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE); memset(p9xStub, 0, stubdata_len); // Trampoline 함수 할당 (공유) // 기존 힙에 할당한 코드를 해제하고 다시 공유 메모리에 할당한다. pfn9xTramp = VirtualAlloc(NULL, WriteSize+5, MEM_COMMIT | VA_SHARED, PAGE_EXECUTE_READWRITE); memcpy(pfn9xTramp, TrampCode, WriteSize+5); delete [] TrampCode; TrampCode = (BYTE *)pfn9xTramp; // Trampoline 코드가 옮겨졌으므로 jmp 명령어가 다시 설정되어야 한다. *((int *)&TrampCode[WriteSize+1]) = ((BYTE *)fnApi+WriteSize+1) -(&TrampCode[WriteSize+1]) -5; // 9x Stub Data 설정 *((DWORD *)(((BYTE *)pfn9xSpec)+1)) = (DWORD)p9xStub; // Process Checker 설정 *((DWORD *)(((BYTE *)pfn9xSpec)+6)) = (int)m_ProcChecker -(int)((BYTE *)pfn9xSpec+6) -4; } int i, idx=-1; // 프로세스 리스트에서 빈 공간 찾음 for (i=0; i<STUB_DATA_COUNT; i++){ if (p9xStub->Data[i].pPDB == 0){ idx = i; break; } } // 프로세스 리스트가 꽉 참 if (idx == -1) return 0; // 현재 Process DB를 읽어와서 항목에 셋팅한다. PVOID PDB; __asm{ mov eax, fs:[0x30] mov [PDB], eax } p9xStub->Data[i].pPDB = PDB; // API Specifier 설정 p9xStub->Data[i].pSpec = SpecCode; // API 후킹 데이터에 저장 data.fn9xSpecifier = (PROC)pfn9xSpec; data.pStub = p9xStub; // 첫번째 후킹 => TrampCode가 공유 메모리에 있음 (위에서 할당함) if (p9xStub->nProcHooked < 1){ data.pStub->pTrampoline = TrampCode; // 두번째 이상 => TrampCode가 힙에 할당됨. // 공유된 Trampoline 함수로 설정 }else{ delete [] TrampCode; TrampCode = (BYTE*)(data.pStub->pTrampoline); } // 프로세스 리스트의 index 저장 data.idxDat = idx; // 후킹 횟수 증가 p9xStub->nProcHooked++; } // API 후킹 데이터에 저장한다. data.fnTrampoline = (PROC)TrampCode; data.fnSpecifer = (PROC)SpecCode; // 데이터 추가 m_data.push_back(data); // API Specifier에 해당 데이터 적용 *((DWORD *)(SpecCode+1)) = (DWORD)&(m_data.back()); // API 코드를 패치해 준다. if (osver.IsWinNT() || (DWORD)fnApi < 0x80000000){ DWORD oldProtect = VirtualProtect(fnApi, WriteSize, PAGE_EXECUTE_READWRITE); memcpy(fnApi, PatchCode, WriteSize); VirtualProtect(fnApi, WriteSize, oldProtect); }else{ // Win9x: 첫번째 후킹일때만 패치시켜준다. if (data.pStub->nProcHooked <= 1){ *((int *)&PatchCode[1]) = (BYTE *)(data.fn9xSpecifier) -((BYTE *)fnApi) -5; _RtlCopyMemory(fnApi, PatchCode, WriteSize); } } // API 패치 후 PatchCode를 해제해 준다. delete [] PatchCode; // 새로 패치한 명령어가 적용되도록 명령어 캐쉬를 비운다. FlushInstructionCache(GetCurrentProcess(), NULL, NULL); // API Hooking 성공! return (APIHOOK *)(*((DWORD *)(SpecCode+1))); } // cdecl 호출 규약의 함수 후킹 // 아래와 같이 구현하면 안전하지 않다. // InterceptAPI와 똑같이 구현하되 Specifier의 마지막 스택 정리 부분을 ret 0으로 하면 된다. // 코드의 길이가 길어져서 생략했다. const APIHOOK *api_hook::InterceptCdecl(PROC fnApi, APIPROC fnProc, WORD fnArgc, LPVOID UserData){ APIHOOK *pHookDat = (APIHOOK *)InterceptApi(fnApi, fnProc, fnArgc, UserData); if (!pHookDat) return 0; // Specifier의 스택 정리 부분을 수정한다 -> ret 0 *((WORD *)((BYTE *)(pHookDat->fnSpecifer)+11)) = 0; return (APIHOOK *)(pHookDat); } // 후킹한 API 복원 int api_hook::RestoreApi(PROC fnApi){ list<APIHOOK>::iterator it_data; list<APIHOOK>::iterator it_end = m_data.end(); // list에서 맞는 데이터를 찾는다. for (it_data = m_data.begin(); it_data != it_end; it_data++){ const APIHOOK &data = *it_data; // 데이터가 주어진 함수의 주소와 맞는가? if (data.fnApi == fnApi){ // Win9x System API가 아니거나 NT일 경우 if (osver.IsWinNT() || (DWORD)data.fnApi < 0x80000000){ // API 코드 복원 DWORD oldp = VirtualProtect(data.fnApi, data.fnApiOffset, PAGE_EXECUTE_READWRITE); memcpy(data.fnApi, data.fnTrampoline, data.fnApiOffset); VirtualProtect(data.fnApi, data.fnApiOffset, oldp); // 메모리 해제 delete [] (BYTE *)data.fnTrampoline; delete [] (BYTE *)data.fnSpecifer; m_data.erase(it_data); }else{ // 다른 프로세스가 후킹하고 있는 중일 경우 if (data.pStub->nProcHooked > 1){ // 쓰고 있는 프로세스 리스트를 초기화시킨다. memset(&data.pStub->Data[data.idxDat], 0, sizeof(_9XDATA)); // 후킹 횟수를 줄인다. data.pStub->nProcHooked--; // 메모리를 해제한다. delete [] (BYTE *)data.fnSpecifer; m_data.erase(it_data); } // 다른 프로세스가 후킹중이지 않은 경우 else{ // API를 복원한다. _RtlCopyMemory(data.fnApi, data.fnTrampoline, data.fnApiOffset); // 공유 메모리를 해제한다. VirtualFree(data.pStub, NULL, MEM_RELEASE); VirtualFree(data.fn9xSpecifier, NULL, MEM_RELEASE); VirtualFree(data.fnTrampoline, NULL, MEM_RELEASE); // 메모리를 해제한다. delete [] (BYTE *)data.fnSpecifer; m_data.erase(it_data); } } break; } } return 1; } // Trampoline 함수 호출 (__stdcall) // API Procedure의 인수를 그대로 넘겨주면 된다. int api_hook::CallTrampolineAPI(const API *api, LPVOID pFirstArg){ // 후킹 데이터를 읽는다. const APIHOOK *data = api->HookInfo; // 인수를 차례로 push 한 후 Trampoline 함수를 호출한다. // (push 순서는 거꾸로다. 가장 마지막 인수 -> 가장 첫번째 인수로 push) int *pCurParam = ((int *)pFirstArg)+(data->fnArgc)-1; int _var, _proc, _ret; for (int i=0; i<data->fnArgc; i++){ _var = (*(pCurParam-i)); __asm push _var } _proc = (int)(data->fnTrampoline); __asm{ call _proc mov _ret, eax } // 원본 API 함수의 리턴값 return _ret; } // Trampoline 함수 호출 (cdecl, stdcall 공용) int api_hook::CallTrampolineCdecl(const API *api, LPVOID pFirstArg){ const APIHOOK *data = api->HookInfo; int *pCurParam = ((int *)pFirstArg)+(data->fnArgc)-1; int _var, _proc, _ret, _esp; __asm mov _esp, esp for (int i=0; i<data->fnArgc; i++){ _var = (*(pCurParam-i)); __asm push _var } _proc = (int)(data->fnTrampoline); __asm{ call _proc mov esp, _esp mov _ret, eax } return _ret; } // 참고: Hook Imported Function 방식의 API Hooking. // 수정한 부분의 주소를 반환한다. (즉, 함수의 주소가 기록되어 있는 메모리 주소) // System DLL의 Import Table은 수정할 수 없다. /*PDWORD api_hook::HookImportedFunction(HMODULE hModule, PROC fnApi, PROC fnProc){ PROC pfnOriginalProc; PIMAGE_DOS_HEADER pDosHeader; PIMAGE_NT_HEADERS pNTHeader; PIMAGE_IMPORT_DESCRIPTOR pImportDesc; PIMAGE_THUNK_DATA pThunk; PVOID pBaseAddr; // Verify that a valid pfn was passed if ( IsBadCodePtr(fnProc) ) return 0; // Get API Address to Hook pfnOriginalProc = fnApi; if(!pfnOriginalProc) return 0; // Approach the address of import table pBaseAddr = (PVOID)hModule; pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddr; pNTHeader = (PIMAGE_NT_HEADERS) ( (DWORD)pDosHeader + pDosHeader->e_lfanew); pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ( (DWORD)pBaseAddr + (DWORD) (pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)); // Verify that the module is valid if ( pDosHeader->e_magic != IMAGE_DOS_SIGNATURE ) // "MZ" return 0; if ( pNTHeader->Signature != IMAGE_NT_SIGNATURE ) // "PE\0\0" return 0; // the end of dll chain is always NULL while ( pImportDesc->Name ) //Name is a DWORD (RVA, to a DLL name) { PSTR pszModName = MakePtr(PSTR, pDosHeader, pImportDesc->Name); // if ( stricmp(pszModName, szDllName) == 0 ) // find dll { pThunk = (PIMAGE_THUNK_DATA) ( (DWORD)pBaseAddr + pImportDesc->FirstThunk); // the end of API address chain in dll is always NULL while ( pThunk->u1.Function ) { PDWORD pImpFunction = pThunk->u1.Function; if (osver.IsWin9x()){ if (*(PBYTE)pImpFunction == 0x68){ pImpFunction = (PDWORD)*(DWORD *)((PBYTE)pImpFunction + 1); } } if ( (DWORD)pImpFunction == (DWORD)pfnOriginalProc ) // find api { // FOUND! DWORD oldp = 0; ::VirtualProtect(&pThunk->u1.Function, sizeof(PDWORD), PAGE_EXECUTE_READWRITE, &oldp); pThunk->u1.Function = (PDWORD)fnProc; // intercept target api address field ::VirtualProtect(&pThunk->u1.Function, sizeof(PDWORD), oldp, &oldp); return (PDWORD)&pThunk->u1.Function; } pThunk++; } } pImportDesc++; } return 0; }*/ // 메모리 복사 // 메모리 Protection을 무시하고 복사한다. void _stdcall api_hook::_RtlCopyMemory(void *pDest, const void *pSrc, unsigned int nSize){ // 메모리가 실제로 존재하는 부분인가? MEMORY_BASIC_INFORMATION mbi={0}; if (VirtualQuery(pDest, &mbi, sizeof(mbi)) == sizeof(mbi)){ if (mbi.State == MEM_FREE) return; } if (VirtualQuery(pSrc, &mbi, sizeof(mbi)) == sizeof(mbi)){ if (mbi.State == MEM_FREE) return; } // WinNT: 메모리 Protection을 잠시 바꾸어서 복사 if (osver.IsWinNT()){ DWORD oldp = VirtualProtect(pDest, nSize, PAGE_EXECUTE_READWRITE); DWORD oldp_2 = VirtualProtect((void *)pSrc, nSize, PAGE_EXECUTE_READWRITE); memcpy(pDest, pSrc, nSize); VirtualProtect((void *)pSrc, nSize, oldp_2); VirtualProtect(pDest, nSize, oldp); return; } // Win9x: int 2E 이용 __asm{ push nSize push pSrc push pDest mov edx, esp mov eax, 0x10A // int2E_RtlCopyMemory int 0x2E // RtlCopyMemory(pDest, pSrc, nSize) add esp, 12 } } 결론 API 후킹은 API의 호출을 가로채는 것이고 동적 API 후킹은 API 후킹 기법을 활용하여 여러 API의 호출을 하나의 프로시저로 연결시키는 것이다. 참고 자료 | |||||||||
|
출처 : http://www.devpia.com/Maeul/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=7267&ref=7267#IDynAPIHook
'C/C++언어 > 후킹' 카테고리의 다른 글
Hooking 을 사용하는 프로그램 내의 구현 (0) | 2007.11.11 |
---|---|
Global Hooking in Win32 (0) | 2007.11.11 |
[본문스크랩] 메세지 후킹 (0) | 2007.09.06 |
[본문스크랩] Knowledge Base API (0) | 2007.09.06 |
API Hooking Revealed (0) | 2007.09.06 |