Dynamic API Hooking Architecture

Written by X-Type

User Level (ring 3)

 

목차

머리말

API Hooking…?

Dynamic API Hooking…?

API 후킹 방식

동적 API 후킹의 구현

문제 해결하기

코딩하기

결론

참고 자료

 

머리말

글에서는 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 상황에 따라 다르게 후킹하도록 것이다.

 

Static API Hooking(정적 API 후킹)

Dynamic API Hooking(동적 API 후킹)

구현이 쉽다.

구현이 다소 어렵다.

후킹할 API 관리하기가 까다롭다.

(코드를 작성하고 다시 컴파일해야 )

유저에 의해 후킹할 API 관리된다.

(, 런타임상에서 관리된다)

API 따라서 가로챈 후의 동작이 다양하게 구현되는 경우가 많다.

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)이라는 것을 수정하는 방법이다. 프로그램이 특정 API1) 호출하는 코드를 작성했다면, 보통 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 후킹을 사용하면 에러가 난다. 기존의 디버거를 이용한 방법은 모든 프로세스에 중단점이 존재하지만 디버그하는 프로세스는 하나이다. 만약에 디버그 중이 아닌 프로세스에서 해당 API 호출하게 되면, 중단점에서 프로그램이 튕겨버린다. 중단점도 인터럽트1) 중의 하나이기 때문에, 핸들링되어있지2) 않으면 기본 핸들러3) 프로세스를 강제 종료시켜 버린다. 7) 이런 이유로, 필자가 아는 바로는 9x에서 안전한 동작을 보장할 있는 API 후킹 방법은 Hook Imported Function 하나밖에 없다.

 

참고로, 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 호출을 하나의 프로시저로 연결시키는 것이다.

 

참고 자료

1. API Hooking 기초강좌

2. API Hooking Revealed (한글 번역)

3. Remote Library (한글 번역)

4. IA-32 Intel Architecture Software Developer’s Manual Volume 2A: Instruction Set Reference, A-M; CHAPTER 2, Instruction Format (한글 번역)


출처 : 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

+ Recent posts