심플스 - 프로그램과 책 이야기로 가득한 곳, (Simples.Kr)

  


   심플스 배너



유저 레벨에서 API Hook 1부

Windows Research 조회 수 13267 추천 수 0 2010.09.03 16:50:13
 

이 문서에는 유저 레벨 즉, Ring 3권한에서 API Hook를 하는 방법에 대해서 설명합니다.
( Ring 0권한에서 API Hook를 하는 방법에 대해서는 다음 문서에서 설명하겠습니다.
혹시나 CPU에서 제공하는 Ring에 대해서 모르신다면 관련 문서를 먼저 읽어보기를 권합니다. )

API Hook과 관련하여 인터넷에서 자료들이 많이 있는 것이 사실이지만 설명이 너무 어렵거나
관련 소스가 있어도 너무 불안정하게 돌아가는 경우가 많습니다. 따라서 간단한 API Hook를 구현해보고
실제로 사용할 수 있는 Hook 코드에 대해서 설명하려고 합니다.

API Hook이란, Win32 애플리케이션에서 사용하는 API를 내가 만든 프로그램에서 가로챈 후
가로챈 데이터의 내용을 보거나 또는 그 API를 호출하지 못 하도록 하는 것을 말합니다.
API Hook을 하는 방법에는 다음과 크게 다음과 같이 두 가지 방법이 있습니다.

1. IAT( 임포트 어드레스 테이블 )
애플리케이션에서 Win32 API를 호출하기 위해서는 API가 실제로 어느 위치에 해당하는지 알아야 합니다.
이러한 API주소들은 IAT라는 곳에 저장되게 되고 이 테이블의 값을 통해서 애플리케이션에서는 API를 호출 할 수 있게 됩니다.
이 때 이 테이블의 값을 수정해서 내가 원하는 주소로 점프하도록 하여 API함수를 가로챌 수 있습니다.
이 방법은 API Hook중에서 가장 쉬운 방법이나 IAT를 참조하지 않고 API를 호출하는 애플리케이션이 있기 때문에
실제로 사용하는건 불가능합니다. 대표적으로 델파이로 만들어진 프로그램은 IAT를 사용하지 않고 자체적으로
테이블을 갖고 있고 그러한 테이블을 사용하기 때문에 델파이로 만들어진 애플리케이션은 훅이 불가능하게 됩니다.

2. Trampoline Hook ( 트램폴라인 훅 )
Trampoline Hook은 API 코드 자체를 변경하는 것을 의미합니다.
코드 자체를 변경한다고 해서 많은 변경이 일어나는 것은 아니고 이전 문서에서 크랙미를 크랙했던 것과 비슷하게
함수에 시작 코드앞에 5byte Jmp기계어 코드를 삽입하여 내가 만든 함수로 점프하도록 하는 것 입니다.
이 방식은 매우 강력하며 하이퍼 스냅이나 스네그잇 같은 경우에는 TextOut계열의 API를 Hook하여 텍스트 캡쳐
기능을 개발 할 수 있고, BitBlt를 Hook하여 화면 캡쳐 방지 모듈을 개발 할 수도 있습니다.
즉, 상용 애플리케이션도 간혹 쓰이며 우리가 사용할 방법은 이 방식을 이용해 API Hook를 하게 될 것 입니다.

본론으로 들어가기전에 Trampoline Hook이 어떠한 방식인지 이해하기 위해
Trampoline Hook를 이용하는 애플리케이션이 어떠한 방식으로 API를 Hook는지 살펴보겠습니다.
대상 애플리케이션은 하이퍼 스냅이며 우선 하이퍼 스냅을 띄웁니다.
일단 이 상태에서 TextOutA에 API코드를 보면 다음과 같습니다.


b0072382_4971b1f3078e5.png 


( 델파이 IDE에서 디버깅해보는 화면입니다. VC2005에서는 함수에 이름이 나오지 않기 때문에 사용하지 않았습니다. )
다음과 같은 코드로 이루어져 있다는 것을 보실 수 있으며 첫 부분은 mov edi, edi로 시작하게 됩니다. ( Windows xp sp2 기준. )
그렇다면 TextOutA가 하이퍼 스냅에 의해 훅이 되었을 경우 어떻게 바뀌는 보겠습니다.
( 참고 : TextOutA가 훅이 되는 시점은 사용자가 하이퍼 스냅에게 텍스트 캡쳐 명령을 내린 후 훅이 되고 바로 훅이 해제 되게 됩니다.
만약 테스트를 하려면 OnPaint 시점에 TextOutA를 호출하도록 하고 Text From an Object under Cursor메뉴를 이용해야 합니다. )


b0072382_4971b1f3078e5.png 


위와 같이 0xE9 xxxxx ( xx 는 내 함수가 있는 주소. 이 이미지에서 기계어 코드를 보시면 0xE9다음에 나오는 주소가
조금 이상하게 보일 수 있는데 0xE9에 경우는 상대주소를 사용하기 때문입니다. 이에 대해서는 다음에 설명하게 됩니다. )
형태로 하이퍼 스냅이 만들어 놓은 코드로 점프시키도록 합니다.
이 부분은 하이퍼 스냅에서 사용하는 Hook 함수 코드입니다.


b0072382_4971b1f3078e5.png 


또한 실제로, 이 주소에 모듈이 올라와 있는 것을 확인 할 수 있습니다. ( 주소는 시스템에 따라서 다를 수 있습니다. )


b0072382_4971b1f3078e5.png 


하이퍼스냅에서 사용하는 Trampoline Hook에 대해서 살펴보았는데 이 방식에 대해서 정리하자면 다음과 같습니다.

1. Hooking하려는 대상에 API의 시작 코드가 jmp코드로 변경되었다. ( 0xE9, 0xXX, 0xXX, 0xXX, 0xXX )
2. 훅킹하려는 대상에 프로세스에 특정한 dll이 로드되었다. ( 이것을 Dll 인젝션이라고 부릅니다. )
3. 1번과 2번에 의해서 Hooking 된 API가 호출이 되면 jmp코드로 인해서 인젝션 된 dll에 코드가 실행된다.


API Hooking에 기본 흐름을 위에 3가지로 요약될 수 있습니다. ( 물론 원본 API호출까지 고려하자면 개수가 조금 늘어나게 됩니다. )

그렇다면 이제 본론으로 들어가서 API Hooking에 흐름을 파악하기 위해 코드를 작성해보겠습니다.
여기서 하게 될 프로그램은 현재 실행중인 모든 윈도우에서 ExtTextOutW API를 Hooking하여 호출이 되지 않도록 하는 것 입니다.

위에 코드는 마이크로소프트 비주얼 스튜디오 2008로 컴파일 작성되었으며 메인 콘솔 프로그램과 인젝션용 Dll로 나누어집니다.

따라서 그 이하 버전에서는 컴파일이 안될 수 있으니 폴더에 "APIHookExample"는 콘솔 프로그램으로
"APIHookExampleDLL"은 Dll 프로그램으로 하여 새 프로젝트를 생성해주시면 됩니다.

( 여담이지만 저 같은 경우에는 메인UI는 델파이로, Dll에 경우에는 C++, C로 작성하는 스타일이라서 델파이 소스는 없습니다.
물론 어려운 부분이 없기 때문에 델파이 사용자라도 충분히 컨버팅이 가능하다고 봅니다.
루트킷 개발하시는 분들도 이런 스타일로 개발하시는 분들이 많습니다. 너무 하나에 언어를 중심으로 개발하는 것은 좋지 않으며
상황에 맞게 최적의 컴파일러를 사용하는 것이 좋습니다. 물론 우리나라에 실무 환경에서는 다 C++이기 때문에 힘들겠지만... )

우선 받은 소스에서 APIHookExample.cpp를 열어봅니다.

// APIHookExample.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include

extern "C"
{
__declspec(dllexport) void InstallHook();
}

extern "C"
{
__declspec(dllexport) void UninstallHook();
}

int _tmain(int argc, _TCHAR* argv[])
{
printf("API Hook 예제가 실행되었습니다.\n종료하려면 엔터를 입력하세요.");

InstallHook();
while (TRUE)
{
::Sleep(30);
if (GetKeyState(VK_RETURN) < 0)
{
break;
}
}

return 0;
}

콘솔 애플리케이션으로써 InstallHook(), UninstallHook()함수를 임포트하고 있습니다.
우선 InstallHook()함수만 호출하고 ::Sleep() 함수를 사용하여 기다리는 작업밖에 하지 않습니다.
( 테스트 프로그램으로써 UninstallHook()를 호출해주지 않았습니다. )
코드가 너무 간단하므로 이제 Dll에 소스코드를 봅니다.
APIHookExampleDLL.cpp파일을 엽니다.

// APIHookExampleDLL.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"

#include
#include

#ifdef _MANAGED
#pragma managed(push, off)
#endif

/////////////////////////////////////////////////////
// 모든 프로세스에서 공유 될 변수를 선언합니다.
/////////////////////////////////////////////////////
#pragma data_seg(".APIHookSection")
HHOOK hHook = NULL;
#pragma data_seg ()

/////////////////////////////////////////////////////
// 전역으로 사용할 변수를 선언하였습니다.
HINSTANCE g_hInst = 0;
// API Hook을 한 후 훅을 해제시 원래의 코드로 되돌리기 위해
// 선언하였습니다.
BYTE cbOldCode[5] = { 0x00, };
/////////////////////////////////////////////////////

LRESULT CALLBACK GetMessageProc(int nCode, WPARAM wParam, LPARAM lParam);


/////////////////////////////////////////////////////
// 기본적인 전역훅 루틴입니다.
/////////////////////////////////////////////////////
LRESULT CALLBACK GetMessageProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx( hHook, nCode, wParam, lParam);
}

extern "C" __declspec(dllexport) void InstallHook()
{
hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMessageProc, g_hInst, 0);
}

extern "C" __declspec(dllexport) void UninstallHook()
{
UnhookWindowsHookEx(hHook);
}
/////////////////////////////////////////////////////

BOOL WINAPI MyExtTextOutW( __in HDC hdc, __in int x, __in int y, __in UINT options,
__in_opt CONST RECT * lprect, __in_ecount(c) LPCWSTR lpString, __in UINT cbCount,
__in_ecount_opt(c) CONST INT * lpDx)
{
//if (cbCount == 0)
//{
// return FALSE;
//}
/*HDC hDC;

hDC = ::GetDC(0);

::TextOutA(hDC, 0, 0, "A", 1);
//::TextOutW(hDC, 0, 0, _T("A"), 1);

::ReleaseDC(0, hDC);*/

return TRUE;
}

BOOL InstallAPIHook_ExtTextOutW()
{
BYTE *pDrawTextWAddress;

DWORD dwOldProtect;
DWORD dw;

pDrawTextWAddress = (BYTE *)GetProcAddress(GetModuleHandle(_T("GDI32.Dll")), "ExtTextOutW");
if (pDrawTextWAddress == NULL)
{

::OutputDebugString(_T("ExtTextOutW API의 주소를 얻어오는데 실패하였습니다."));

return FALSE;
}
VirtualProtect(pDrawTextWAddress, 5, PAGE_WRITECOPY, &dwOldProtect);

memcpy(cbOldCode, pDrawTextWAddress, 5);

*pDrawTextWAddress = 0xE9;
pDrawTextWAddress++;
*((DWORD *)(pDrawTextWAddress)) = ((DWORD)MyExtTextOutW) - ((DWORD)pDrawTextWAddress +4);

VirtualProtect(pDrawTextWAddress, 5, dwOldProtect, &dw);

return TRUE;
}

BOOL UnInstallAPIHook_ExtTextOutW()
{
BYTE *pDrawTextWAddress;

DWORD dwOldProtect;
DWORD dw;

pDrawTextWAddress = (BYTE *)GetProcAddress(GetModuleHandle(_T("GDI32.Dll")), "ExtTextOutW");
if (pDrawTextWAddress == NULL)
{
::OutputDebugString(_T("ExtTextOutW API의 주소를 얻어오는데 실패하였습니다."));

return FALSE;
}
VirtualProtect(pDrawTextWAddress, 5, PAGE_WRITECOPY, &dwOldProtect);

memcpy(pDrawTextWAddress, cbOldCode, 5);

VirtualProtect(pDrawTextWAddress, 5, dwOldProtect, &dw);

return TRUE;
}


BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
/////////////////////////////////////////////////////
// DLL이 해당 프로세스로 Attach되었습니다.
/////////////////////////////////////////////////////
g_hInst = hModule;

InstallAPIHook_ExtTextOutW();

break;

case DLL_PROCESS_DETACH:
/////////////////////////////////////////////////////
// DLL이 해당 프로세스로 Detach되었습니다.
/////////////////////////////////////////////////////
UnInstallAPIHook_ExtTextOutW();

break;
}

return TRUE;
}

#ifdef _MANAGED
#pragma managed(pop)
#endif



( 이 예제코드에서는 콘솔 프로그램에서 이 Dll파일을 임포트 함으로 DllMain함수가 실행되어 DLL_PROCESS_ATTACH인해 InstallAPIHook_ExtTextOutW()함수를 호출함으로써
자기자신까지도 API Hooking를 해버리는데 이 부분은 무시하도록 하겠습니다. )

소스코드를 보시면 생각했던것보다 너무나도 API Hooking 예제코드가 간단하다는 것을 볼 수 있습니다.
한 페이지반밖에 되지 않는 예제 소스 코드입니다.

그렇다면 하나하나씩 분석해보도록 하겠습니다.
우선 메인 콘솔 애플리케이션에서 InstallHook함수를 호출하게 됩니다.
이 함수는 SetWindowsHookEx를 호출하는데 WH_GETMESSAGE훅킹을 하게 됩니다.
여기서 말씀드리고 싶은 것은 SetWindowsHookEx함수를 호출하게 되면
훅킹하려는 대상 프로세스에 SetWindowsHookEx를 호출한 Dll를 시스템에서 자동으로 Dll 인젝션시킨다는 것 입니다.


이렇게 함으로써 Dll 인젝션을 쉽게 구현 하였는데 사실 이 방법말고도 CreateRemoteThread API를 이용하여 특정 프로세스에
깔끔하게 Dll 인젝션을 할 수도 있습니다.

Dll 인젝션이 되면 바로 DllMain에서 DLL_PROCESS_ATTACH 루틴을 타게 됩니다.
그리고 이 루틴은 다시 InstallAPIHook_ExtTextOutW()함수를 호출하게 됩니다. 이 부분이 가장 중요한 부분이 되겠습니다.

GetProcAddress()함수를 사용하여 GDI32.Dll에 있는 ExtTextOutW함수에 주소를 얻어옵니다.
VirtualProtect(pDrawTextWAddress, 5, PAGE_WRITECOPY, &dwOldProtect); 를 통해 Copy-On-Write를 구현합니다.
Copy-On-Write는 ExtTextOut()함수의 시작부분에서 5Byte만큼 구현하도록 하였습니다.
즉, 이 5Byte부분은 우리가 바꿔야할 기계어 코드 메모리 영역인 것 입니다.

Copy-On-Write는 매우 중요한 부분으로 Dll에 경우에는 모든 프로세스가 코드를 공유하게 되므로 메모리상에
Dll은 오직 하나만 올라오게 됩니다.
그러나 부득이하게 Dll에 코드를 런타임시에 변경해야 하는 경우가 생기게 되는데
하나의 Dll에서 이 코드를 변경하게 되면 Dll이 메모리상에 하나만 올라가있으므로 ( 모든 프로세스가 공유하므로 )
모든 프로세스가 변경 된 코드에 영향을 받게 됩니다.
이러한 경우 특정한 프로세스에 한해서 변경 된 코드 영역이 다른 물리 메모리를 가르키게 함으로써
다른 프로세스에서는 영향을 받지 않고 변경한 프로세스 자신만 영향을 받도록 합니다.
이러한 방법은 운영체제에서 페이징 기능을 이용하여 제공하는 매커니즘이며 이와 관련 된 부분은
다른 추가적인 서적이나 문서를 참고할 것을 권장합니다.


( 여담이지만 이러한 운영체제 매커니즘을 가장 잘 이해하는 방법은 운영체제 서적을 보거나
운영체제 소스를 보는 것보다 직접 간단한 커널을 구현하여 멀티 태스킹 및 페이징 기능을 만들어보는게 가장 확실하게 이해 할 수 있습니다. )

갑자기 Copy-On-Write에 대해서 설명하다보니 내용이 많이 어려워졌는데 계속 해서 다음줄에 코드를 설명하겠습니다.

memcpy(cbOldCode, pDrawTextWAddress, 5); 부분은 우리가 변경해야 할 기계어 코드에 5Byte를 백업두는 코드입니다.
이것은 API Hooking 해제할 때 원래의 코드로 되돌리기 위함입니다.

*pDrawTextWAddress = 0xE9; 부분은 Jmp기계어 코드를 사용한다는 의미입니다.

그 다음에 *((DWORD *)(pDrawTextWAddress)) = ((DWORD)MyExtTextOutW) - ((DWORD)pDrawTextWAddress +4);
구문은 Jmp를 해야 할 주소를 지정하게 됩니다.
여기서 주소 지정을 할 때 보시면 MyExtTextOutW를 지정하는것이 아니라 MyExtTextOutW함수에 주소에서
(pDrawTextWAddress +4) 주소를 빼주는 것을 볼 수 있습니다.
이것은 Jmp명령어가 절대 주소로 지정하는것이 아니라 상대 주소로 지정해야 한다는 것을 알 수 있습니다.

다음 줄을 보시면 VirtualProtect(pDrawTextWAddress, 5, dwOldProtect, &dw);를 호출하는데 원래의 메모리 속성으로 되돌리게 합니다.
( 메모리 속성이이라고 하니 메모리에 속성이라는 것이 있는가라는 의문을 가질 수 있는데 이것은 페이징이 제공하는 메모리 보호기능을 의미합니다. )

이 부분까지 완료가 되었다면 이제부터 ExtTextOutW함수를 호출하게 되면 우리의 MyExtTextOutW함수가 호출되게 됩니다.

프로그램을 실행하면 다음과 같은 화면이 뜹니다. 여기서 엔터키를 입력하면 종료하게 되는데 윈도우즈 전역으로 엔터키를 감지함으로
다른 프로그램에서 엔터키를 입력하여도 종료하게 됩니다.


b0072382_4971b1f3078e5.png 


윈도우즈 탐색기를 띄워보시면 MyExtTextOutW에서 아무것도 하지 않고 return TRUE;를 함으로 어떠한 문자열도 출력이 되지 않는 것을 볼 수 있습니다.


b0072382_4971b1f3078e5.png 

당연히 파이어 폭스에 경우에도 마찬가지입니다.


b0072382_4971b1f3078e5.png 

간혹 출력되는 문자열이 있을 수 있는데 이것은 다른 Text출력 API함수를 사용하여서 그렇습니다.


profile

esniper

2010.09.05 21:14:22

그림과 예제를 이용하여 자세히 설명해주셔서 감사합니다.

공부하시는 분들께 많은 도움이 되겠네요~

List of Articles
번호 제목 글쓴이 날짜 조회 수
163 문서자료 json 문법 체크 사이트 Lyn 2012-02-28 16572
162 Windows Research SCSI Miniport - IOCTL_SCSI_PASS_THROUGH_DIRECT ( Windows ... lain 2011-06-11 12683
161 Windows Research 커널레벨에서 IAT Hook lain 2011-06-11 14200
160 Windows Research DEVICE_OBJECT에 대한 보안 디스크립터 변경 lain 2011-06-11 11424
159 Windows Research IE Cache 경로 변경하기 lain 2011-06-09 29447
158 Windows Research 32Bit 윈도우즈에서 실제 물리메모리크기 얻어오기 lain 2011-06-09 11875
157 Windows Research VMWare 탐지 기법 우회 - 2 [2] lain 2011-04-04 25887
156 Linux Tip 우분투에 IRC서버 설치후 닉네임 길이와 동접자 조절. [1] 오랑캐꽃 2010-11-26 14914
155 Linux Tip VirtualBox에 우분투 설치 후에 내부 네트워크 접속 설정하기 file esniper 2010-09-10 32497
154 Linux Tip VirtualBox에서 오른쪽 CTRL키 사용하기(VitualBox 호스트키) file [2] esniper 2010-09-10 17711
153 Linux Tip 우분투 설치 후 putty에서 한글 안 깨지도록 설정하기 file esniper 2010-09-10 15961
152 Linux Tip 우분투 ssh or mysql 서버 접속지연이 있는 경우 해결책 esniper 2010-09-10 15186
151 문서자료 고품질의 무료 아이콘들 [3] esniper 2010-09-08 17948
150 Windows Research Sysinternals - Filemon source code file [5] lain 2010-09-04 14167
149 Windows Research Sysinternals - Regmon source code file [1] lain 2010-09-04 13761
148 Windows Research PostQueuedCompletionStatus 함수에 실패 여부도 고려해야 할까? lain 2010-09-04 12336
147 Windows Research SetEvent 함수는 언제 실패할까? lain 2010-09-04 14289
146 Windows Research 커널모드 드라이버에서 사용하는 시간관련 매크로 lain 2010-09-04 11848
145 Windows Research 화면캡쳐방지는 어떻게 구현될까? [3] lain 2010-09-04 25387
144 Windows Research IRP Completion Hook file lain 2010-09-04 12574

  • 이용약관
  • 개인정보취급방침
  • 사이트맵