이전 글

Detours Hooking Framework 1 - Hooking Function

Detours Hooking Framework 2 - Hooking API

Detours Hooking Framework 3-1 Hooking class member function

 

잠시 복습

지난 글에서 class member function을 호출했을 때 컴파일 된 assembly에 대해 살펴보았다. 나머지는 전부 제쳐두고 다음을 기억하면 됐다.


    int result1 = a.TestFunction(10);
008E1612  push        0Ah  
008E1614  lea         ecx,[a]  
008E1617  call        TestClass::TestFunction (8E111Dh)  

class의 instance의 주소를 ecx에 넣고 해당 함수를 콜 한다는 규칙이었다. 자, 이것을 기억해두고 class member function을 후킹해보자.

 

 

Hooking class member function


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;

public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

int HookingFunction(int value){
    std::cout<<"Hooked" <<std::endl;
    //여기서­ 진짜 멤버 함수를 호출해야한다.
    return 0;
}
int main(int argc, char** argv){
    TestClass a(10),b(20);

    int result1 = a.TestFunction(10);

    return 0;
}


위 코드에서 TestClass의 TestFunction함수를 후킹해보자. Detours를 사용하기 위해 TestClass::TestFunction과 HookingFunction의 address를 넘겨야 한다. 이때 main()함수의 코드는 다음과 같게 된다.


int HookingFunction(int value){
    std::cout<<"Hooked" <<std::endl;
    //여기서 진짜 멤버 함수를 호출해야 한다.
    return 0;
}

PVOID functionAddress ;

int main(int argc, char** argv){
    TestClass a(10),b(20);
    functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);

    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&functionAddress, HookingFunction);
    
    error = DetourTransactionCommit();

    int result1 = a.TestFunction(10);

    return 0;
}

위 작업으로 a.TestFunction(10)을 실행했을 때 HookingFunction이 호출되게 된다. 기존 TestClass::TestFunction이 존재하던 Address에 jmp HookingFunction이란 코드가 대치되었기 때문이다. 그런데 HookingFunction코드 내에서 어떻게 TestClass::TestFunction을 실행할 수 있을까?

 

TestClass::TestFunction 을 실행하기 위한 조건은 다음과 같았다. 먼저 TestClass의 instance, a의 주소를 ecx에 넘겨 주어야 하고 TestClass::TestFunction의 address를 call해야 한다. 그런데 문제가 있다. a의 주소를 HookingFunction 내에서 알아낼 방법이 없다는 것이다. a의 주소는 ecx로 넘어 오기 때문에 알 수 있지 않냐고 생각할 수 있겠지만 ecx를 일반함수 HookingFunction에서 보존한다는 보장이 없기 때문이다. 만약 ecx값을 보존한다면 다음과 같이 코딩하면 되겠지만 이는 실패한다.


int HookingFunction(int value){
    std::cout<<"Hooked" <<std::endl;
    int result;
    __asm push value;
    __asm call [functionAddress];
    __asm mov [result], eax;
    return result;
}


그럼 어떻게 해야할까? 답은 의외로 간단하다. 일반 함수로 Hooking하는 것이 아니라 클래스의 멤버함수로 Hooking하면 된다. 다음과 같이 말이다.


class HookingClass{
public:
    int HookingFunction(int value){
        std::cout<<"Hooked" <<std::endl;
        int result;
        __asm push value;
        __asm mov ecx, [this];
        __asm call [functionAddress];
        __asm mov [result], eax;
        return result;
    }
};


int main(int argc, char** argv){
    TestClass a(10),b(20);
    functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);

    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&functionAddress, (PVOID)(&(PVOID&)HookingClass::HookingFunction));
    
    error = DetourTransactionCommit();

    int result1 = a.TestFunction(10);

    return 0;
}


위 코드를 실행시키면 HookingClass::HookingFunction(int value)내에서 TestClass::TestFunction을 실행할 수 있음을 알 수 있다. __asm mov ecx, [this];  에서 [this]가 곧 instance a의 주소이다. 이때 __asm으로 어셈블리 코드를 직접 사용하는 것이 싫으면 다음과 같이 하면 된다.


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;

public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

class HookingClass{
public:
    int HookingFunction(int value);
    static int (HookingClass::*Real_Target)(int);
};

int HookingClass::HookingFunction(int value)
{
    std::cout<<"Hooked" <<std::endl;
    return (this->*Real_Target)(value);
}
int (HookingClass::* HookingClass::Real_Target)(int) = (int (HookingClass::*)(int))&TestClass::TestFunction;

int main(int argc, char** argv){
    TestClass a(10),b(20);

    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach(&(PVOID&)HookingClass::Real_Target, (PVOID)(&(PVOID&)HookingClass::HookingFunction));
    
    error = DetourTransactionCommit();

    int result1 = a.TestFunction(10);

    return 0;
}


굉장히 아름다운 이 방법은 detours의 sample에 포함된 소스코드를 참조한 것이다. 붉은 색으로 된 코드를 보고 그 안에 담긴 의미를 음미해보라. 다음 마지막이 될 다음 포스트에는 virtual function을 후킹하는 법에 대하 알아보도록 한다.

 

신고

이전 글

Detours Hooking Framework 1 - Hooking Function

Detours Hooking Framework 2 - Hooking API

 

잠시 복습

첫번째 Detours Hooking Framework 글에선 일반 Function을 Hooking하는 법을 살펴보았고 두번째 글에서는 Detours로 API를 후킹하는 법에 대해 알아보았다. Detours가 훌륭한 것은 동일한 방식으로 다른 분류의 Hooking을 할 수 있다는 사실이다. 사용자의 입장에선 내부적으로 어떤 일이 벌어지는지 신경쓸 필요가 없다. 조금만 신경쓰면 된다. 잠시 이전에 Detours가 어떻게 Hooking을 했는지 복습해보자. 일반 함수의 경우 함수 심볼 위치에 있는 명령어를 사용자가 원하는 명령어로 변경해줌으로써 이 일이 가능했다.

위를 보면 TestFunction: 심볼 값에 위치한 명령이 jmp TestFunction에서 jmp HookingFunction으로 변경된 것을 확인할 수 있다. 또한 Dynamic Link된 API를 Hooking할 때는 좀더 복잡한 Code Patch방식을 사용한다는 것이 확인되었다. 만약 해당 API Function의 주소가 A라면 해당 주소에 jmp HookingFunction이라는 명령어가 들어가는 식이었다. 하지만 근본적인 방식은 특정 메모리 주소에 Jmp HookingFunction이라는 명령어를 쓰는 방식으로 동일했다. 그리고 변경되기 전 메모리 주소의 명령어들은 다른 메모리 주소로 옮겨 진다. 이것은 현대의 컴퓨터가 폰노이만 머신, 즉 Code와 Data가 동시에 같은 메모리상에 존재하기 때문에 가능한 것이었다.(VirtualProtect함수로 해당 메모리 주소에 WRITE속성을 추가하고 memcpy로 명령어를 쓰는 것으로 이루어진다.) 당연히 후킹을 위해선 반드시 후킹하고 싶은 함수 또는 API의 메모리 주소를 알아야한다.

 

Class member function call을 디벼보자.

class member function을 후킹하려고 한다. 먼저 후킹이 가능하려면 해당 member function의 주소를 알아내야 한다. 그런데 그것만으로 될까?


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;
public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

int main(int argc, char** argv){
    TestClass a(10),b(20);
    int result1 = a.TestFunction(10);
    int result2 = b.TestFunction(10);
    return 0;
}


위와 같은 초 단순한 예를 한번 생각해보자. 후킹을 하기 위해서 해당 함수의 메모리 주소를 알아야한다. 그렇다면 여기서 a.TestFunction 함수와 b.TestFunction 함수의 메모리 주소는 같을까 다를까? 같다면 하나의 작업으로 여러 Instance의 TestFunction을 후킹할 수 있을 것이고 다르다면 Instance마다 TestFunction을 후킹해야한다. 이를 확인하기 위해 a.TestFunction 함수 콜과 b.TestFunction 함수 콜이 Assembly로 어떻게 컴파일 되는지 살펴보자.


    int result1 = a.TestFunction(10);
008E1612  push        0Ah  
008E1614  lea         ecx,[a]  
008E1617  call        TestClass::TestFunction (8E111Dh)  
008E161C  mov         dword ptr [result1],eax  
    int result2 = b.TestFunction(10);
008E161F  push        0Ah  
008E1621  lea         ecx,[b]  
008E1624  call        TestClass::TestFunction (8E111Dh)  
008E1629  mov         dword ptr [result2],eax  


위 어셈블리를 통해 알 수 있듯이 a.TestFunction 함수와 b.TestFunction은 같은 주소 값을 가진다. 이것은 동일한 클래스의 Instance가 여러개 존재하더라도 해당 클래스의 Function은 같은 메모리 주소에 있다는 것을 의미한다. 그렇다면 해당 클래스 멤버 함수는 자신이 어떤 Instance에 속해 실행되는지 어떻게 알 수 있을까? 옳지, call 명령 전의 lea ecx, [a]에 해답이 있을 것 같다. lea ecx, [a]명령이 완료되면 a변수의 주소가 ECX 레지스터에 들어가게 된다. 아하, TestClass::TestFunction은 이 ECX 레지스터에 있는 a의 주소를 통해 정확한 mInt의 값을 얻어 내는 것이구나! a.TestFunction(10)으로 들어가 확인해보자.


int TestClass::TestFunction(int iValue){
008E1590  push        ebp      //Stack Frame을 사용하기 위해 ebp를 Stack에 저장한다. 
008E1591  mov         ebp,esp  //현 esp 값을 ebp에 저장한다. 
008E1593  sub         esp,0CCh //함수내에 사용될 스택의 크기를 0x0CC만큼 확보한다. 
008E1599  push        ebx      //ebx 레지스터의 값을 스택에 저장한다. 
008E159A  push        esi      //esi 레지스터의 값을 스택에 저장한다. 
008E159B  push        edi      //edi 레지스터의 값을 스택에 저장한다. 
008E159C  push        ecx      //ecx 레지스터의 값을 스택에 저장한다. 
008E159D  lea         edi,[ebp-0CCh]  //여기서 부터 rep stos까지의 명령은 하나의 명령 set으로 보면된다. 
008E15A3  mov         ecx,33h         //ecx에 0x33 값을 넣는다.  
008E15A8  mov         eax,0CCCCCCCCh  //eax에 0xcccccccc값을 넣는다. 
008E15AD  rep stos    dword ptr es:[edi]  
//rep stos dword ptr es:[edi]는 edi, ecx, eax와 연관된다. 
//보통 함수의 첫 시작에 불리는 이 명령어 집합은 스택에 초기값을 넣는 것에 활용된다. 
//이 명령은 edi의 주소 값에 eax값을 넣는다. 그리고 그 다음에는 edi+4의 주소 값에 eax값을 넣는다. 
//다음에는 edi+8의 주소 값에 eax값을 넣는다. 이것을 총 ecx내의 수 만큼 반복한다. 즉 0x33번 반복한다.(0x33*4 = 0xcc)
008E15AF  pop         ecx  //ecx값을 복구한다.
008E15B0  mov         dword ptr [ebp-8],ecx  //ecx값을 ebp-8의 주소에 넣는다. 
    return mInt + iValue;
008E15B3  mov         eax,dword ptr [this] //this 주소 값을 eax에 넣는다.   
008E15B6  mov         eax,dword ptr [eax]  //eax의 주소의 value값을 eax에 넣는다. 
008E15B8  add         eax,dword ptr [iValue]  //eax에 iValue값을 더해 넣는다. 
}
008E15BB  pop         edi  //edi값 복구  
008E15BC  pop         esi  //esi값 복구
008E15BD  pop         ebx  //ebx값 복구
008E15BE  mov         esp,ebp  //esp = ebp
008E15C0  pop         ebp  //ebp값 복구
008E15C1  ret         4  //esp = esp +4, 함수 호출했던 주소로 가기 위해 또한번 pop. 


 

Stack

음... Stack관련 명령만 대해 먼저 정리하자. 먼저 알아야 할 것은 함수가 불리기 전의 ESP값과 함수가 불린 후의 ESP 값이 동일해야 한다는 사실이다. 그리고 Stack은 함수 내 변수 공간을 위해 활용된다. 이 Stack의 변화를 보기 위해 Stack관련 명령어들만 정리한 다음 그림을 보기 바란다.

이 과정으로 ESP가 함수 호출이 끝나면서 자연스럽게 복귀됨을 알 수 있다. 이 assembly와 같이 EBP 레지스터를 이용해 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 Stack Frame기법이라 한다. 보통 이 과정은

push EBP
mov EBP, ESP
...
mov ESP, EBP, 
pop EBP
ret 

로 압축된다. 이 stack frame을 이용하면 함수 호출 깊이가 깊어져도 스택을 완벽하게 관리할 수 있다. 더 관심있으신 분들은 검색해보시길~ 

 

 

ECX, 그리고 EBP-8 == this

하여간 여기서 중요한 것은 Stack Frame이 어떻게 동작하는가가 아니다. 우리가 궁금한 것은 어떻게 TestClass::TestFunction이 자신의 Instance의 mInt변수값을 알아 낼 수 있는가였다. 이 부분에 해당하는 코드만 따로 보자.


main함수 내에서

    int result1 = a.TestFunction(10);
01041612  push        0Ah  
01041614  lea         ecx,[a]  
01041617  call        TestClass::TestFunction (104111Dh)  
0104161C  mov         dword ptr [result1],eax  

 

TestClass::TestFunction 내에서

010415B0  mov         dword ptr [ebp-8],ecx  
    return mInt + iValue;
010415B3  mov         eax,dword ptr [this]  
010415B6  mov         eax,dword ptr [eax]  
010415B8  add         eax,dword ptr [iValue]  
}


위 부분이 궁금한 부분을 압축한 코드라 할 수 있다. 보자. 함수 호출전에 ecx값에 a변수의 주소가 들어갔었다. TestFunction내에서 ebp-8의 주소에 ecx값을 넣고 있다. 이때 ebp 에는 -12값이 들어있으므로 -20에 ecx값, 곧 a의 변수 값을 넣고 있다. 이 다음에 나오는 코드가 mov eax, dword ptr [this]이다. 이것은 무엇일까?

놀랍게도 이것은 mov eax, dword [ebp-8]과 동일한 의미를 지닌다. 중요한 포인트인데, 바로 클래스 멤버 함수의 ebp-8에는 해당 클래스의 instance의 주소가 들어가게 된다는 사실이다. 이것은 일종의 약속이다. 그리고 해당 instance의 주소에는 클래스 멤버 변수들의 값이 들어있다. 이 경우 해당 instance의 주소에 mInt값이 들어있으므로 mov eax, dword ptr[eax]로 해당 멤버 변수의 값을 eax에 로드할 수 있는 것이다. 그렇다면 만약 TestClass에 int mInt2라는 변수가 추가되어있다면 어떨까? 그리고 이 변수는 어떻게 클래스 함수에서 접근할까? 딩동댕! 다음과 같은 꼴일 것이다.

mov eax, dword ptr[this+4]

 

 

Class Member Function Pointer

비밀들이 벗겨지고 있다. 위에서 설명한 이유 때문에 일반 Function Pointer로 함수 멤버 함수를 호출할 수 없는 것이다. 예를 들어 TestClass::TestFunction의 주소를 얻어와 이것을 일반 Function Pointer로 만든 후 해당 Function Pointer를 호출해보자.


TestClass a(10),b(20);
typedef int (*FPointer)(int value);
PVOID functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);
FPointer pointer = (FPointer)functionAddress;
pointer(10);


위 코드는 정상적으로 빌드가 되고 pointer에 TestClass::TestFunction의 주소 값이 정상적으로 들어가지만 Run-time Exception을 발생시킨다. 그리고 이것은 너무나 당연한 결과이다.


00B41621  push        0Ah  
00B41623  call        dword ptr [pointer]  


class의 Instance를 넘겨주는 명령이 빠져있는 것을 볼 수 있다. 이와 반대로 Class Member Function Pointer를 활용해보자.


typedef int (TestClass::*PFClassMember)(int);
PFClassMember classMemberFP = &TestClass::TestFunction;
(a.*classMemberFP)(10);


    int result3 = (a.*classMemberFP)(10);
0107161B  push        0Ah  
0107161D  lea         ecx,[a]  
01071620  call        dword ptr [classMemberFP]  

ecx에 변수 instance a의 주소값을 넘겨주고 있다. 이로써 해당 함수 콜이 정상적으로 동작할 수 있다.

 

 

결론

일반 함수 콜과 클래스 멤버 함수 콜이 어떻게 다른지 기초적인 부분을 알아보았다. 다음 포스트에서는 이 TestFunction 클래스 함수를 후킹해보자! 

 

**혹시 이 포스트에서 제가 잘못 알고 있는 부분이 있다면 꼭 태클해주세요. 감사합니다.

신고

Detours Hooking Framework -2. Hooking API

프로그래밍 2011.07.12 16:44 Posted by 아일레프

지난 글에서 Detours로 Function을 후킹하는 방법을 살펴보았다. 이 포스트에는 API를 Hooking하는 법을 알아본다. 물론, 매우 간단하고 쉽다.

 

API Hooking

API를 후킹하는 방법은 지난 글에서 말했던 방식과 완전히 동일하다. 단, 내부에서 Detours가 Hooking하는 방식은 다르다.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

int main(int, char **){
    ::MessageBoxA(NULL, "messageBox", "Hello", MB_OK);
}

위 코드에서 MessageBoxA를 후킹해 MessageBox 창의 caption을 "Hooked"로 바꿔보자. 코드는 다음과 같을 것이다.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

int (WINAPI *pfTrueMessageBox)(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType) = MessageBoxA;

int WINAPI HookingMessageBox(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType){
        
        return pfTrueMessageBox(hWnd, lpText, "Hooked", uType);
}

int main(int, char **){

    DWORD error;
    ::DetourTransactionBegin();
    ::DetourUpdateThread(::GetCurrentThread());
    ::DetourAttach((PVOID*)&pfTrueMessageBox, HookingMessageBox);
    error = DetourTransactionCommit();

    if(error != NO_ERROR){
        printf("fail to attach\n");
    }

    ::MessageBoxA(NULL, "messageBox", "Hello", MB_OK);

}

이 프로그램을 실행시키면 다음 결과를 확인할 수 있다.

Caption이 바뀌었음을 확인 할 수 있다. 자, 이제 내부에서 어떤 일이 벌어졌는지 확인해보자.

먼저 Hooking되기 이전의 MessageBoxA 함수를 호출하는 명령은 다음과 같다.

::MessageBoxA(NULL, "messageBox", "Hello", MB_OK);
00EC165C  mov         esi,esp 
00EC165E  push        0 
00EC1660  push        offset string "Hello" (0EC7854h) 
00EC1665  push        offset string "messageBox" (0EC7844h) 
00EC166A  push        0 
00EC166C  call        dword ptr [__imp__MessageBoxA@16 (0ECC404h)] 
00EC1672  cmp         esi,esp 
00EC1674  call        @ILT+445(__RTC_CheckEsp) (0EC11C2h) 

명령어를 통해 알 수 있듯이 __imp__MessageBoxA의 심볼 값 - 0xECC404h주소의 value 를 call하라는 명령어 이다. 그리고 0xECC404h주소에는 0x7670fd1e 값이 들어있다.

 

그리고 해당 주소에는 MessageBoxA 함수의 코드가 위치해 있다.

7670FD1E  mov         edi,edi 
7670FD20  push        ebp 
7670FD21  mov         ebp,esp 
7670FD23  push        0 
7670FD25  push        dword ptr [ebp+14h] 
7670FD28  push        dword ptr [ebp+10h] 
7670FD2B  push        dword ptr [ebp+0Ch] 
7670FD2E  push        dword ptr [ebp+8] 
7670FD31  call        7670FCD6 

또한 MessageBoxA의 function Pointer를 저장한 pfTrueMessageBox 의 값은 0x7670FD1E이다.

코드를 진행해 Detours로 Attach한 뒤의 Assembly 명령어를 살펴보면 다음과 같은 변화가 있다는 것을 알 수 있다.

7670FD1E  jmp         HookingMessageBox (0EC12BCh) 
7670FD23  push        0 
7670FD25  push        dword ptr [ebp+14h] 
7670FD28  push        dword ptr [ebp+10h] 
7670FD2B  push        dword ptr [ebp+0Ch] 
7670FD2E  push        dword ptr [ebp+8] 
7670FD31  call        7670FCD6 

먼저 위와 같이 0x7670FD1E주소의 명령어가 변경되었다. 그리고, pfTrueMessageBox의 값이 0x6fff0060으로 변경되었다.

그리고 0x6fff0060의 위치에는 다음 명령어가 존재함을 확인 할 수 있다.

6FFF0060  mov         edi,edi 
6FFF0062  push        ebp 
6FFF0063  mov         ebp,esp 
6FFF0065  jmp         7670FD23

6FFF0060 - 6FFF0063은 Detour로 Attach하기 전 0x7670FD1E - 0x7670FD21의 명령어였다.  

정리해보자. 이전 포스트에서 Detours는 단순히 함수 심볼 값의 명령어를 변경할 뿐이었다. 하지만 이번에는 새로운 명령어가 기존의 위치에 추가되고, 기존의 명령어가 다른 위치로 이동됨으로써 Hooking 동작이 이루어졌다. 물론 다른 방식으로 이 API후킹이 이루어질 수도 있다. 0xECC404h 주소의 값에 HookingMessageBox 주소(0x0EC12BCh)를 적어 놓는 것도 하나의 방법일 것이다. 하지만 Detour는 기존의 코드를 변경하는 방식을 택했다는 것을 확인해주기 바란다. 만약 Detours를 통하지 않고 직접 API를 후킹하고 싶다면 www.reversecore.com/63을 확인하면 된다. 정말 자세히 잘 설명 되어있다. 다음에는LoadLibrary, GetProcAddress를 통해 얻어진 API를 후킹하는 법에 대해 살펴볼 것이다.

 

 

** 2011. 07. 12 추가 내용

LoadLibrary, GetProcAddress로 얻어진 함수를 후킹하는 방법을 새 글로 다루려고 했는데 사실 이것은 이 포스트의 내용과 다르지 않기에 여기에 추가한다.

위에서 MessageBoxA함수를 호출할 수 있는 것은 User32.dll이 동적 링크 되었기 때문이다. 그런데 User32.dll을 동적 링크하라는 명령이 보다시피 프로그램내에서 존재하지 않고 있다. 이는 User32.dll이 암시적으로 동적로딩되었기 때문이다. (PE Loader가 프로그램이 시작할 때 이 일을 한다.) 하지만 때때로 프로그래머는 명시적으로 dll을 로딩하고 해당 dll의 exported된 함수를 사용하고 싶을 때가 있는데 이때 사용하는 명령어가 LoadLibrary, GetProcAddress이다. 위 프로그램과 같은 프로그램을 만들되, 명시적 Dynamic Link를 사용해보자. 프로그램은 다음과 같을 것이다.


typedef int (WINAPI *PFMessageBox)(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType);

PFMessageBox pfTrueMessageBox;
int main(int, char **){
    HMODULE user32Module = LoadLibrary(TEXT("user32.dll"));

PFMessageBox messageBox = (PFMessageBox)::GetProcAddress(user32Module,
"MessageBoxA"); messageBox(NULL, "messageBox", "Hello", MB_OK); }


위 프로그램을 Detours로 후킹하면 다음과 같다.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

typedef int (WINAPI *PFMessageBox)(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType);

PFMessageBox pfTrueMessageBox;

int WINAPI HookingMessageBox(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType){
        
        return pfTrueMessageBox(hWnd, lpText, "Hooked", uType);
}

int main(int, char **){
    HMODULE user32Module = LoadLibrary(TEXT("user32.dll"));
    pfTrueMessageBox = (PFMessageBox)::GetProcAddress(user32Module, "MessageBoxA");
    DWORD error;
    ::DetourTransactionBegin();
    ::DetourUpdateThread(::GetCurrentThread());
    ::DetourAttach((PVOID*)&pfTrueMessageBox, HookingMessageBox);
    error = DetourTransactionCommit();

    if(error != NO_ERROR){
        printf("fail to attach\n");
    }
    
    PFMessageBox messageBox = (PFMessageBox)::GetProcAddress(user32Module, "MessageBoxA");
    messageBox(NULL, "messageBox", "Hello", MB_OK);

}


pfTrueMessageBox와 messageBox 함수 포인터가 가리키는 주소가 다르다는 것에 주의해야 한다. pfTrueMessageBox에는 기존의 MessageBoxA를 호출하는 명령 있으며 messageBox에는 HookingMessageBox를 호출하는 명령이 있다. 그리고 이 일을 하기 위해 Detours가 하는 일은 암시적 동적 링크된 API를 후킹하는 방식과 완전히 동일하다.

신고

Detours Hooking Framework - 1. hooking function

프로그래밍 2011.06.16 00:37 Posted by 아일레프

최근 Hooking을 해야 할 일이 생겼다. 그런데 이 Hooking할 함수가 dll의 일반 export함수도 아니고, 클래스 멤버의 함수도 아니고, COM객체의 virtual 함수라 이리저리 많은 삽질을 해야했다. 그러다 Detours란 Hooking Framework를 만나게 되었는데 와, 이거 정말 훌륭하더라. 물론 Virtual 함수를 Hooking하는 것은 이리저리 많은 삽질을 거쳐야 했지만, 프로그래밍이란게 한번 알고 나면 너무 간단해 허무한 것이다. 고로 지금 허무하다.

 

설치.

Detours Hooking Framework 이 사이트에서 Detours Express 2.1을 다운로드 받고 설치하자. default 경로는 C:\Program Files (x86)\Microsoft Research\Detours Express 2.1 이다. 해당 폴더에 Command Prompt로 이동해 nmake all 명령을 실행하자. 본인은 nmake all 명령이 실패했는데 나의 문제는 Visual Studio의 Command Prompt를 이용하고 set DETOURS_TARGET_PROCESSOR =x86 을 설정함으로써 해결할 수 있었다.

 

Function Hooking

이제 Detours를 사용해보자. Visual Studio에 DetoursTest를 만들고 C/C++, Linker 환경을 설정하자. C/C++의 Additional Include Directories에 Detours Express의 Include 디렉토리를 추가하고, Linker의 Additional Library Directories에 Detours Express의 lib 디렉토리를 추가한다. 그리고 Linker의 Additional Dependencies에 detoured.lib, detours.lib를 추가한다. 그후 main.cpp란 파일을 추가 한 후 다음 코드를 넣자.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

void TestFunction(){
    printf("TestFunction Called\n");
}

void HookingFunction(){
    printf("before - TestFunction call");
    TestFunction();
    printf("after - TestFunction call");
}

int main(int, char**){
    TestFunction();
    getchar();
}

main에서 TestFunction을 call했으므로 당연히 CMD 창에 "TestFunction Called" 문장이 나타날 것이다. 이제 TestFunction을 Hooking해 TestFunction()을 호출하면 HookingFunction()이 호출되게 할 것이다. 그리고 이 일은 굉장히 간단하다. 프로그램을 다음과 같이 바꿔보자.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

void TestFunction(){
    printf("TestFunction Called\n");
}
void (*pfTrueTestFunction)() = TestFunction;
void HookingFunction(){
    printf("before - TestFunction call\n");
    TestFunction();
    //pfTrueTestFunction();
    printf("after - TestFunction call\n");
}

int main(int, char**){
    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&pfTrueTestFunction, HookingFunction);
    error = DetourTransactionCommit();

    if (error == NO_ERROR) {
        printf("\n");
    }
    else{
        printf("fail to attach\n");
    }
    TestFunction();
    getchar();
}

프로그램을 실행하면 어떤 일이 벌어질까? before - TestFunction Call 문장이 반복되어 출력될 것이다. 원하는 동작을 원한다면 HookingFunction의 TestFunction()을 주석처리하고 pfTrueTestFunction function pointer로 기존 메소드를 출력하면 된다.

어떻게 이 일이 가능하게 되었을까? DetourAttach 함수 콜에 Breakpoint를 설정하고 TestFunction과 pfTrueTestFunction의 값을 확인해보자.

TestFunction 주소 값과 pfTrueTestFunction 값이 다르지만 pfTrueTestFunction 주소 값의 Assembly 명령어를 확인하면 Jmp 명령어로 TestFunction으로 점프하는 코드가 있음을 확인 할 수 있다. 그리고 그 명령어 위에 'TestFunction:'이 있는데 이것이 함수 symbol값이다. 

이제 DetourAttach명령과 DetourTransactionCommit()까지 프로그램을 진행시켜보자. Watch창으로 TestFunction과 pfTrueTestFunction을 다시 확인해보자. 재미있게도 pfTrueTestFunction의 주소 값이 바뀌었다.

pfTrueTestFunction의 값이 변경되었음을 알 수 있다. 0x010c0060메모리 주소를 보면 TestFunction의 메모리로 점프하는 코드가 있다.

 

더 재미있는 것은 0x010e12bc의 메모리주소의 값이 변경되었다는 사실에 있다.

TestFunction: 심볼 값에 Jmp TestFunction 명령이 있었는데 Jmp HookingFunction으로 변경되었다. 이것이 DetourAttach명령과 DetourTransactionCommit이 하는 일이다. 이것으로 후킹이 완료되었다. 

 

마치며

Detours로 특정 함수 심볼 값을 변경해 후킹을 해보았다. 이런 방식으로 기존의 exe파일이나 dll파일을 후킹할 수도 있다. detours의 bin 폴더 내에는 setdll이란 명령어가 있는데 이를 이용하면 된다. 이 setdll명령은 기존의 exe파일이나 dll파일에 외부의 dll을 로드하게하는 명령어이다. 예를 들어
setdll /d:dllInject.dll notepad.exe
명령을 실행시키면 이후에 notepad.exe를 실행했을 때 프로그램 내부에서 dllInject.dll이 로드되게 된다. 즉 LoadLibrary("dllInject.dll")이 실행되게 되는 것이다. 이 dll이 로드되면 dll내부의 DllMain함수가 실행되게 되고, 만약 DllMain함수 내에서 DetourAttach 등으로 Hooking동작을 하는 코드가 있다면 notepad.exe내에서 호출하는 함수, API들을 Hooking할 수 있다.
 

 2로 계속됩니다.

** 2001. 07. 01 수정.
setdll /d:dllInject.dll executable.exe를 실행시키면 dllInject.dll이 executable.exe에 Inject된다고 위에 설명했었다. 이것은 사실이다. 그런데 난 멋도 모르고 어떻게 추측했었냐면, .text의 EntryPoint에 LoadLibrary("dllInject.dll") 코드를 명시적으로 실행시켜 dllInject.dll의 DllMain 함수가 실행되게 할 줄 알았다. 하지만 사실은 이와 달라 정정한다. Dll Loading의 방식에는 2가지가 있는데 그 중 하나는 명시적 로딩, 즉 코드 내에서 LoadLibrary("dllInject.dll")이라고 명시적으로 코딩을 하는 방법이고, 또 하나는 암시적 로딩으로써 프로그램이 시작하는 순간에 같이 로딩되는 방법이 있다. 암시적 로딩을 위해 PE 파일은 어떤 DLL파일을 로드 해야 하는지 명시해 PE Loader에게 알려야 한다. Detours 는 PE파일의 이 부분을 변경한다. 즉, dllInject.dll이 필요하다고 PE Loader에게 알리는 것이다. 자세한 부분은 www.reversecore.com/23에 잘 설명되어 있다. 

신고