티스토리 뷰

1.요약

API hooking 하는 방법 중에 Dll을 바꿔버리는 방법을 알아보고 구현시 주의점을 알아봅니다.

2.본문

[API hooking은 무엇인가?]

특정 API를 주시하고 있다가 누군가 그 API를 호출하면 중간에 개입하고자 함입니다.

메시지가 나올때나 윈도우가 생성될 때, 통지받을 수 있는 Windows Hook 과는 달리 공식적으로 문서화된 방법은 없습니다.

[ 왜 API hooking을 하는가? ]
다른 프로그램이 API를 어떻게 사용하고 있는가 지켜보려고..
내 프로그램의 디버깅을 목적으로.. (Deadlock detection이 좋은 예가 되겠네요..)


[ API 는 어떻게 hooking 하는가? ]

1. Import Address Table을 바꿔치는 방법.

이 방법은 이 글에서 다룰 내용이 아니므로, 간단히 설명하겠습니다. 읽으시는 분도 대충 읽으시길..

이 경우는 import library를 사용하는 경우에 해당됩니다. 대부분 GDI32.dll, User32.dll, Kernel32.dll 같은 경우는 import library를 사용해서 링크하고 있으니까, 위 dll에 들어있는 API는 좋은 hooking 대상이 되겠죠.

exe 파일은 프로세스가 생성될 때 자신이 원하는 위치에 로드될 수 있습니다. 가장 먼저 로드되는 모듈이기 때문이죠. 하지만 dll의 경우 에는 자신이 선호하는 위치를 이미 다른 dll이 차지하고 있을 수 있기 때문에, 최종적으로 어느 주소에 로드될지는 아무도 알 수가 없습니다.

다시 말해서, dll 안의 함수의 주소도 실행시간이 되어서야 결정된다는 얘기죠.

그렇다면, dll 안의 함수를 호출하는 클라이언트들은 어셈블리 코드를 어떻게 생산해내야 될까요? 단순히 "몇 번지로 call 해라" 라고는 할 수 없습니다.

결국은 Import Address Table 이라는 것을 중간에 두어서, 클라이언트 의 코드는 "IAT의 어디어디에 있는 주소로 call 해라" 처럼 될것이고, 실행 시간에 dll 이 로드되면 IAT는 적당히 초기화 되겠죠.

(C++의 v-table 개념과 매우 흡사하군요 :)

이런 작동방식 때문에 IAT에서 원하는 API의 주소를, 우리가 만든 함수의 주소로 바꿔버리면 간단하게 hooking이 되겠죠.
(뭐.. 그리 간단하지는 않습니다. -_-;;)

또 하나, 잘 생각해보면 프로세스의 중요한 영역을 함부로 바꾸는 일이다 보니까, 내 프로세스는 그렇다 치더라도, 다른 프로세스까지 hooking을 하려면 내가 만든 dll을 다른 프로세스에 주입하는 기술도 필요하겠죠..

dll을 주입하는 방법에는 dll을 레지스트리에 등록하기, Windows Hook 사용하기, CreateRemoteThread 사용하기, CreateProcess로 아예 디버깅 해버리기 등등 다양한 방법이 있습니다.

2. Dll 바꿔치우기~

자.. 이제 우리가 관심을 가지고 있던 부분입니다.

dll 바꿔치우기의 가장 큰 장점은 import library를 사용했건 LoadLibrary를 사용해서 실행시간에 동적으로 로드했건 다 먹힌다는 점입니다.

물론 시스템의 dll을 내 것으로 바꾸기 때문에 어느 정도의 위험부담은 있겠죠..

구현하기도 아주 쉽습니다. 보통은 다음과 같은 방식이죠..

하나, MSDN 도움말보고 똑같은 원형의 함수를 가진dll을 만든다. 두울, 기존의 dll을 다른 이름으로 바꿔서 보관한다.

세엣, 아무것도 모르는 순진한 클라이언트가 내 dll에 있는 함수를 호출하면, 적당히 체크하고, 원래 dll로 포워딩 시킨다..

끝입니다.. 뭐, 굳이 설명할 것도 없군요..

하나만 예를 들어서..

UINT WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR pCaption, UINT uType)
이 API를 후킹한다면 다음과 같이 나만의 MessageBox를 구현할 수 있겠죠.

extern "C" 
UINT WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR pCaption, UINT uType) 
{ 
    typedef UINT (WINAPI *FUNC_PTR)(HWND, LPCSTR, LPCSTR, UINT); 
    OutputDebugString("후후.. 순진하기는.. \n"); 

    UINT uRet; 
    // 진짜 user32.dll의 이름을 ~user32.dll로 바꿨다고 가정하고.. 
    HMODULE hMod = ::LoadLibrary("~User32.dll"); 
    if (hMod) 
    {
        FUNC_PTR pfnOrig = (FUNC_PTR)::GetProcAddress(hMod, "MessageBoxA"); 
        if (pfnOrig) 
        { 
            uRet = pfnOrig(hWnd, lpText, pCaption, uType); 
        } 
        ::FreeLibrary(hMod); 
    } 
    return uRet; 
} 

물론 매번 LoadLibrary와 FreeLibrary를 호출하는 것은 비효율적이니까, 적당히 바꿔주면 되겠죠.

자, 이 문서에서 말하려고 하는 것은 이제 시작입니다.

운좋게도 dll에 있는 API가 헤더 파일에도 포함되어 있고, 도움말까지 있다면 걱정이 없겠지만, 때로는 문서화되지 않은 API를 몰래 쓰는 프로그램도 있습니다.

물론 dumpbin이나 dendancy walker같은 툴로 dll을 살펴보면 이런 API의 이름은 알 수 있습니다. 하지만 이름이 전부라는 게 문제죠.. signature는 전혀 알 길이 없는 것 같습니다..( *signature : 반환값, 호출관행, 매개변수 리스트 등등)

그래서 우리의 목표를 이렇게 정하기로 합니다.

"좋다.. 매개변수는 몰라도 좋다.. 하지만 적어도 이 함수가 언제 호출이 되는가는 알고 싶다.."

이런 목표라도 이루고 싶다면 다음과 같은 방법으로 함수를 구현할 수 있습니다.

예를 들어

XXX.dll 에 YYY 라는 함수가 있다라고 치면..
// g_pfnOriginYYY에는 이미 원래 함수의 주소가 들어있다고 가정합니다 :) 
extern PROC g_pfnOriginYYY; 
__declspec( naked ) void YYY() 
{ 
    __asm pushad 

    // 여기서 마음껏 필요한 일을 합니다. 
    ::OutputDebugString("걸렸어~ YYY\n"); 

    __asm popad 
    __asm jmp    g_pfnOriginYYY 
} 
우선 이 함수 구현에 쓰인 알고리즘을 추상적으로 설명한다면 다음과 같습니다.

-> 함수가 호출 되었다 -> 함수가 호출되었음을 체크한다 -> 원래 함수의 주소로 그냥 점프한다.

이제 한 줄씩 설명을 해보죠..

__declspec( naked ) 는 함수의 prolog와 epilog 를 생성하지 않도록 해줍니다. prolog와 epilog란 스택 프레임을 만들고 제거하는 작업을 말합니다. 다시, 스택 프레임이란 ebp 레지스터에 함수 시작 당시의 esp 를 보관함으로써 함수의 인자나 지역변수로의 접근을 편하게 하는 것을 말합니다.

우리가 만든 함수는 정상적으로 리턴하지 않고 그냥 원래 API의 주소로 jmp 해버립니다.
그런데, prolog 때문에 스택이나 레지스터가 지저분해진다면 문제가 생기겠죠.

__asm 은 어셈블리 언어를 C++ 소스에서 사용할 수 있게 해줍니다.

pushad 는 모든 레지스터를 저장하는 명령입니다. 함수가 호출되었을 때 아무것도 하지않고 원래 API로 점프해버린다면 필요없는 명령이지만, 중간에 체크하는 과정에서 레지스터가 변경될 것이 분명한 일이므로, 다시 복구하기 위해서 저장해 둡니다.

popad는 모든 레지스터를 복구하는 명령입니다.

jmp 명령을 통해서 원래 API 의 주소로 제어를 옮깁니다. call 이 아니라는 점에 주의해야겠죠?

다시 한 번 개념적으로 정리해 보겠습니다.

일단은 YYY라는 함수의 구현이 필요합니다. 하지만 우리 역시 함수의 signature를 다 적어줄 필요는 없습니다. 함수의 signature라는 것은 C++ 컴파일러의 마법일 뿐이죠..

그렇더라도 C++ 컴파일러를 사용하는 한은 컴파일러의 마법을 피해나갈 방법은 없습니다. 가장 좋은 방법은 assembly 로 함수를 만든 다음에 이진파일을 함께 링크하는 방법이겠죠. 하지만.. 다행이도 VC++ 에는 __declspec( naked ) 라는 서비스가 있었고, 이는 순수하게 inline assembly 만을 사용해서 프로그래밍 하는 것을 가능하게 해줍니다..


3.예제

예제는 직접 열어보셔야 합니다. 이 항목에서는 예제를 사용하는 법을 설명드립니다.

예제의 Output 폴더를 보시면

PotentialClientExe.exe
MyHookingDll.dll
SystemDll.dll

세 개의 이진파일이 있습니다.

여기서 SystemDll.dll 이 실제 시스템에 설치된 dll 이라고 생각하시면 됩니다.
MyHookingDll.dll 이 바꿔치기할 용도로 만든 dll 이구요.
PotentailClientExe.exe 는 SystemDll 내의 API를 사용하는 잠재적인 어플리케이션 이라고 생각하시면 되겠죠.

우선, PotentailClientExe.exe 를 실행시켜봅니다.

그러면 단순히 메시지 박스가 하나 뜨죠?

이제 후킹을 실행하기 위해서 다음의 절차를 밟습니다.

SystemDll.dll 의 이름을 ~SystemDll.dll 로 바꿉니다.
MyHookingDll.dll 의 이름을 SystemDll.dll 로 바꿉니다.
다시 PotentailClientExe.exe를 실행합니다.

이번에도 마찬가지로 메시지 박스가 뜨지만, 달라진 점이 있습니다.
바로 콘솔창에 후킹되었다는 메시지가 나오는.. 그것이죠~


4.참고

Advanced Windows, Jeffrey Richter Ch.18 "Breaking through Process Boundary Walls"
Debuggin Application, John Robbins Ch.12 "Multithreaded Deadlocks"
Under the Hood, Matt Pietrek , Feb 2000 MSJ
MSDN Library




- 2001.08.19 Smile Seo -

'Reverse Engineering' 카테고리의 다른 글

Detours Library (Hooking)  (0) 2008.12.24
Ollydbg - 디버거  (0) 2008.01.15
PumaEngine - 디스 어셈블러 겸 메모리 제어  (0) 2008.01.15
W32Dasm - 디스 어셈블러  (0) 2008.01.15
RecStudio - 디 컴파일러  (0) 2008.01.15
댓글