본문 바로가기

Window Programming

다시 보는 후킹 기법


10여 년 전 후킹 기법은 고급 테크닉에 속하는 생소한 개념이었고, 그만큼 단순한 후킹도 고급 기술로 취급받았다. 하지만 이제는 누구나 알고 있는 기술 중 하나가 됐다. 그럼에도 불구하고 이 기술을 잘 사용하기란 그리 쉽지 않다. 인터넷에 있는 여러 예제 코드 중 하나를 가져와 사용할 수는 있겠지만 이로 인해 발생하는 다양한 문제점을 이해하고 수정하는 데는 시스템에 대한 전반적인 지식이 필요하다. 결국 후킹 기술은 많이 알려졌지만 이를 잘 사용하는 방법들은 오히려 더 찾아보기 힘들어진 셈이다. 이 글에서는 후킹 기술에 대한 실질적인 이해와 활용을 위한 고급 비법들을 소개한다.

권용휘 rodream@gmail.com|http://rodream.net에서 악성코드 제거기 ‘울타리’와 컴퓨터 최적화 프로그램인 ‘클릭 투 트윅’을 배포하고 있다. 2008년부터 비주얼 C++ 분야에서 마이크로소프트 MVP로 활용하고 있으며 데브피아 비주얼 C++ 분야의 시샵도 맡고 있다.

지금은 후킹 기술이 꽤 많은 개발자들에게 알려져 있다. 그만큼 후킹 기술이 우리가 사용하는 여러 프로그램에 널리 사용된다는 뜻이다. 보안 프로그램을 비롯해 많은 프로그램들이 이 기술을 사용하고 있지만, 후킹이 실제 어떤 방식으로 이뤄지는지 또 어떻게 구현되는지는 아직 잘 알려져 있지 않다.

후킹의 분류
후킹은 기본적으로 ‘함수 호출을 가로채 변형한 다음 특정 프로그램이나 운영체제의 기능을 변형시키는 기술’을 의미한다. 여기서 ‘함수 호출을 가로챈다’는 의미는 ‘객체지향 언어에서 상속을 하면 함수를 재정의할 수 있다’는 의미와 같다. 

예를 들면 <리스트 1>에서 main 함수가 func1을 호출하기 전 특정 함수를 호출하도록 지정하는 것이다. 이렇게 특정 함수가 호출되는 과정에서 지정된 함수를 추가하는 방법을 사용하면 기능을 추가하거나 제한할 수 있다.

<리스트 1> 아주 간단한 프로그램
void func1()
{
    printf("This is the func1()\n");}
void main()
{
    printf("This is the main()\n");
    func1();}



이 기법을 일반적으로 ‘API 후킹’ 또는 ‘함수 후킹’이라고 부른다. API 후킹과 함께 잘 거론되는 대표적인 후킹 방법 중 하나가 메시지 후킹이다. 메시지 후킹은 윈도우들끼리 전달하는 메시지들의 전송 과정에 끼어들 수 있는 방법이다. 예를 들어 프로그램 A에서 WM_PAINT 메시지를 받는다고 가정했을 때, 이 메시지를 수신하기 전이나 수신 후에 자신이 원하는 추가적인 기능을 넣을 수 있게 해준다. 

메시지 후킹과 함수 후킹의 가장 큰 차이점은 일반적으로 함수 후킹은 가로채는 함수의 원본 함수로 호출 여부를 결정할 수 있지만, 대부분의 메시지 후킹은 메시지가 도착하는 시점만 확인할 수 있을 뿐 메시지 자체를 무효화시킬 수는 없다. 물론 메시지 후킹에서도 CBT(Computer-based Training), 키보드 훅 등을 사용해 원래 창에서의 메시지 전달을 막을 수는 있다. 하지만 다른 많은 메시지들의 전달 방지는 불가능하기 때문에 메시지 후킹을 기능 확장이나 제한에 사용하기는 다소 어렵다. 

앞서 설명한 것처럼 후킹은 크게 메시지 후킹과 함수/API 후킹으로 구분된다. 함수/API 후킹은 기본적으로 바이너리 실행 코드의 일부분을 가로채는 것이므로, 여기에서는 편의상 ‘코드 후킹’ 이라고 언급하겠다. 코드 후킹은 대상에 따라 여러 가지로 나눌 수 있는데, 먼저 앞서 살펴본 API 후킹과 함수 후킹이 있고 일반 객체(Class) 후킹과 COM 객체 후킹 그리고 마지막으로 특정 코드를 후킹 하는 것이 있다. 이를 한눈에 볼 수 있도록 나타낸 것이 <그림 1>이다.

<그림 1> 후킹의 분류

이 글에서는 코드 후킹을 중심으로 API, 함수, 일반 객체, COM 객체 그리고 특정 코드 후킹까지 전반적인 후킹 기법들을 모두 다룰 예정이다. 메시지 후킹은 MSDN이나 이미 연재된 기사에서 아주 잘 다루고 있기 때문에 이를 참고하면 된다.

코드와 데이터
본격적인 내용에 들어가기에 앞서 코드와 데이터의 개념을 먼저 알아보자. 후킹 기술은 기본적으로 코드를 변경하는 기술이다. 프로그램은 코드와 데이터로 이루어져 있다. 간단히 말하면 코드는 명령어의 집합이며 데이터는 현재 상태를 저장하고 있는 저장소다. 

행동을 변경하는 후킹은 기본적으로 코드를 변경하는 것부터 시작하지만 명령어의 집합인 코드는 실행 중인 데이터를 읽어온 다음 참고한다. 그래서 COM 객체 후킹이나 일반 객체 후킹 등에서는 데이터를 변경한다. 데이터와 코드에 대한 내용은 많은 기본 서적들에서 다루고 있지만, 너무 추상적으로 설명하고 있어 이에 대한 개념이 부족한 개발자들이 많이 있다. <그림 2>는 간단한 예제 프로그램에서 데이터와 코드 개념을 실제 디버거를 사용해 확인한 모습이다. 디버거는 가장 잘 알려져 있고 사용하기 편리한 디버거 중 하나인 OllyDbg(http://www.ollydbg.de/)를 사용했다.

<그림 2> OllyDebugger로본 응용프로그램의 구조

디버거 사용방법을 설명하면, 실행 후 File - Open 메뉴로 프로그램을 불러온 다음 F9 또는 Debug - Run 메뉴를 사용해 프로그램을 실행시킨다. <그림 2>에 나오는 것과 같이 OK 버튼에 대한 이벤트 핸들러에 강제적으로 중단점(Breakpoint)을 삽입했다. 

어셈블리 코드가 처음인 독자들은 다소 혼란스러워 보일 수 있을지 모른다. 하지만 이번 연재를 읽다보면 충분히 익숙해질 수 있을 것이다. 이해를 돕기 위해 한 화면에 소스 코드와 디버거 화면을 함께 넣었다. 화살표를 따라가 보면 함수의 시작 부분과 new char[1024] 부분, __asm int 3h 부분 그리고 Message BoxA 부분이 소스 코드와 어셈블리 코드가 서로 연결돼 있다는 것을 알 수 있다. 대부분의 함수 호출이 CALL 명령어로 이뤄지며 화살표가 가리키는 코드들도 모두 CALL로 돼 있다. 

여기서 한 가지 의문점이 든다. strcpy 함수에 대한 CALL 명령이 없다는 것이다. 이는 컴파일러 최적화에 따른 것으로, new char[1024] 부분과 __asm int 3h 사이에 수많은 MOV 코드들이 strcpy 함수 역할을 하고 있다. MOV 코드는 주로 데이터를 복사하는 데 사용된다. 함수를 호출하면 함수 인자들과 돌아올 주소(Return Address)를 스택에 저장하고, 함수가 종료될 때 반환값(Return Value)을 스택에서 다시 가져와야 한다. 이런 작업들이 진행되는 동안 성능 저하를 최소화하기 위해 컴파일러가 함수 호출을 하지 않고 데이터를 복사하는 형태로 프로그램을 컴파일한다. 

일반적으로 비주얼 스튜디오를 사용해 컴파일한 프로그램도 함수와 함수 사이가 0xCC로 채워져 있다. <그림 2>에서도 첫 번째 화살표가 OnBnClickedOk 함수의 시작을 가리키고 있는데, 그 위에 있는 코드가 0xCC로 채워져 있다. 이를 통해 서로 다른 함수를 대략 구분할 수 있다. 물론 가장 근본적인 함수의 확인은 함수의 시작(Prologue)과 종료(Epilogue) 코드를 보고 구분하는 것이다. 일반적으로 시작 코드는 push ebp, mov ebp, esp 과 같이 프레임 포인터(ebp)를 저장하는 코드다. 하지만 <그림 2>에서와 같이 최적화 시 삭제되는 경우도 많고 프레임 포인터를 사용하지 않는 호출 규약(Calling Convention)을 가진 함수들도 많으므로, 정석적인 방법은 실질적인 프로그램을 대상으로 하는 경우에는 잘 맞지 않는다. 

다시 <그림 2>로 돌아가서 어셈블리 코드 좌측에 나오는 숫자가 의미하는 것은 해당 코드가 있는 주소다. <그림 3>을 보면 현재 코드의 주소는 002017D5이며, 메시지 박스의 타이틀 텍스트가 되는 ‘Title’은 002037D8에 존재하는 것을 알 수 있다(<그림 3>과 같이 디버거를 사용해 해당 명령어를 마우스로 선택하면 하단 창에 실제 주소가 나타난다).

<그림 3> 현재 코드의 주소와 데이터의 주소

<그림 4>는 이 주소들이 실제 메모리에서 어떻게 위치하고 있는지를 찾아본 것이다. 

<그림 4> 코드의 주소와 데이터의 주소가 위치하고 있는 메모리

<그림 4>와 같이 M 버튼을 누르면 메모리 주소와 해당 메모리가 어떤 용도로 쓰이는지 알 수 있다. 기본적으로 32비트 윈도우 프로그램은 0x00000000에서 0xFFFFFFFF까지의 4GB 메모리 중 2GB 가량 운영체제에서 예약한 부분을 제외하고 자유롭게 사용할 수 있다. 

가장 먼저 지역 변수들은 스택(Stack)에 저장되고, new로 할당한 변수들은 힙(Heap) 메모리에 저장된다. 스택은 1번 영역에 저장되고, 힙 메모리는 <그림 4>에서 생략된 부분인 다른 메모리 영역에 지정돼 있다. <그림 4>에서 2번 영역으로 표시된 .text 섹션에는 code 영역을 포함하고 있다. 이 안에는 프로그래머가 작성한 프로그램 코드가 기계어로 번역돼 쌓여있다. 마지막으로 3번 영역으로 표시된 .rdata의 경우에는 추후에 알아보게 될 Import Address Table과 하드코딩 된 텍스트 형태의 정보들이 저장돼 있다. 

이렇게 소스 코드에는 화면과 함수가 같이 있는 것처럼 보이지만, 실제 프로그램에서는 데이터와 코드들이 컴파일된 후에는 서로 다른 위치에 있다. 즉 데이터는 데이터끼리, 코드는 코드끼리 인접한 형태로 존재한다. 이는 프로그램을 이해하고 프로그램의 세밀한 부분을 다루는 후킹이라는 기술을 이해하는 데 가장 중요한 개념 중 하나이다.

후킹 방식
<그림 5>와 같이 후킹 방식에도 여러 가지 기법들이 있다.

<그림 5> 후킹 기법의 종류

Debugging API를 사용하는 방식
Debugging API를 사용하는 방식은 필자가 <그림 2>에서 설명한 것 같이 중단점(Breakpoint)을 함수 시작에 직접 주입하는 방식이다. 이는 OllyDebugger 같은 디버거로 구현한다고 생각하면 이해하기 쉽다. 윈도우에서 제공하는 Debugging API를 사용해 다른 프로그램을 실행 시킨 후, 함수 시작 부분의 코드를 INT 3(0xCC)로 변경한다. 해당 함수가 실행되면 디버깅 이벤트가 발생되는데, 이 이벤트에 원하는 내용을 처리한 다음 함수 시작 부분의 코드를 원래대로 변경하고 프로그램을 다시 실행하면 된다. 

<그림 6> 코드 주소와 데이터 주소가 위치하고 있는 메모리

<그림 6>은 원래 있던 명령어(PUSH ESI)를 INT3으로 변경해 구현하는 방식이다. 이 방법은 1990년대부터 소개돼 많은 책에서 다루어진 방법이다. 하지만 디버거를 사용해본 개발자들은 알겠지만, 실제 프로그램을 실행하는 것과 비교했을 때 너무 느려진다는 문제가 있다. 이는 디버그 모드로 프로그램을 실행시키는 데에 함수가 실행할 때 마다 매우 느린 예외처리(Exception Handling)를 수행하기 때문이다. 현실적으로 이 방법은 후킹을 이해하기 위해서만 사용되지 상용 제품에서는 사용할 수 없다.

Import Address Table 후킹 방식
또 다른 많이 알려진 방식 중 하나가 Import Address Table(IAT) 후킹 방식이다. 이는 실행 파일이 자신이 사용하고 있는 함수들의 주소를 기록해 두고, 이를 참조하는 Table에서 기록된 함수 주소를 임의로 변경하는 방법이다. IAT 후킹 방식은 <그림 7>과 같은 함수 호출 방식으로 성립할 수 있다. 

<그림 7> IAT를 통한 함수 호출 구조

<그림 7>을 간단히 설명하면 CALL 명령으로 MessageBoxA 함수를 호출한다. 네모 박스로 표시된 내용이 어셈블리 코드에 대한 실제 기계어 코드이다. 여기서 CALL 명령은 FF15로 나타나며, 그 뒤에 있는 B8301701이라는 값은 이동할 함수의 주소를 담고 있는 메모리 영역 주소이다. x86 CPU에서는 데이터를 Little Endian에서 가지고 있기 때문에 이 값을 거꾸로 해석해야 한다. 즉 B8301701은 011730B8이 된다. 코드 영역 밑에 표시된 네모 박스를 보니 이 값이 77CBEA11(MessageBoxA)을 가리키는 것을 알 수 있다. <그림 8>은 실제로 이 값을 찾아본 결과이다.

<그림 8> 직접 확인해본 IAT 영역의 데이터

만일 이 값을 변경하고 싶다면 MessageBoxA 대신 다른 함수가 호출되게 하면 된다. 간단한 테스트를 해보자. <그림 9>와 같이 E 버튼을 누르면 현재 실행 파일의 시작 주소가 01170000이라는 것을 알 수 있으며, <그림 8>에서 확인했던 MessageBoxA 주소는 011730B8에 저장돼 있음을 확인할 수 있다. 결국 이 두 주소의 차이를 구하면 MessageBoxA 주소는 모듈이 로드된 시작 주소로부터 30B8만큼 떨어진 곳에 있는 것을 알 수 있다. 이를 기반으로 해 간단한 테스트 코드를 작성한 것이 <리스트 2>다.

<그림 9. 실행 파일의 모듈시작 주소

<리스트 2> IAT 후킹 동작 방식을 알아보는 예제
typedef int (WINAPI *PFNMESSAGEBOXA)(HWND, LPCSTR, LPCSTR, UINT);
PFNMESSAGEBOXA g_pfnOrgMessageBoxA = NULL;

int WINAPI NewMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
  if( lpText ) {
    char* pszText = new char[strlen(lpText) + 255];
    sprintf( pszText, "[This API has been hooked!]\n\n%s", lpText);
    int nRet = g_pfnOrgMessageBoxA(hWnd, pszText, "[Hooked]", uType);
    delete[] pszText;
    return nRet;
  }
  return g_pfnOrgMessageBoxA(hWnd, lpText, lpCaption, uType);
}

void CSecretsHookDlg::OnBnClickedOk()
{
  LPVOID* lpAddr = (LPVOID*)::GetModuleHandleA( NULL );
  // Offset: 0x30C4
  lpAddr += (0x30C4 / sizeof(LPVOID*));

  // 1. Check if it has been hooked
  if( g_pfnOrgMessageBoxA == NULL ) {
    // 2. Replace it with our new function.
    DWORD dwOldProt = 0;
    if( VirtualProtect( (LPVOID)lpAddr, 4, PAGE_EXECUTE_READWRITE, &dwOldProt ) ) {
      g_pfnOrgMessageBoxA = (PFNMESSAGEBOXA)*lpAddr;
      *lpAddr = NewMessageBoxA;
      VirtualProtect( (LPVOID)lpAddr, 4, dwOldProt, &dwOldProt );
    }
  }

  // Test function call
  MessageBoxA( GetSafeHwnd(), "Test Message", "Title", MB_ICONINFORMATION|MB_OK );
}


<리스트 2>에서 NewMessageBoxA 함수는 실제 Message BoxA 함수와 동일한 형태의 함수여야 한다. 이는 함수의 호출 방식(Calling Convention)마다 스택을 다루는 방식이 다르기 때문이다. 만일 스택이 깨지면 프로그래머가 의도하지 않은 다른 변수들을 수정하게 돼 반환 주소나 반환값 등이 모든 데이터에 잘못 접근하기 때문이다. 특정 함수의 선언 방법(원형)을 알기 위한 가장 간단한 방법 중 하나는 비주얼 스튜디오 에디터에서 해당 함수 위에 마우스를 위치한 다음, 오른쪽 마우스 버튼을 눌러 나오는 팝업 메뉴에서 Go to Definition을 누르거나 함수를 선택한 후 F12 버튼을 누르는 것이다. 

또한 <리스트 2>에서 사용한 오프셋인 0x30C4는 <그림 7>에서 확인했던 30B8과 다르다. 왜냐하면 <리스트 2>를 작성하면서 <그림 7>에 있는 프로그램보다 많은 API를 사용해 해당 함수들의 정보를 추가했기 때문이다. 결과적으로 <리스트 2>에서는 GetModuleHandle 함수를 사용해 현재 실행 파일이 로드된 주소를 찾은 다음  MessageBoxA 함수 주소를 저장하고 있는 IAT 값에 접근해 변경한다. IAT 영역의 메모리는 읽기 전용이므로 VirtualProtect 함수를 사용해 값을 변경할 수 있게 수정했다. 

이 방법은 효과적이고 깔끔한 방법이지만, 현실적으로 사용하기 힘들다는 단점들이 있다. 가장 먼저 이 방법은 IAT를 사용해 호출되지 않는 함수들은 적용하지 못한다. IAT를 사용하지 않는 함수의 대표적인 예로는 GetProcAddress를 사용해 직접 함수값을 저장해 호출하는 경우다. 일반적으로 이 경우 호환성을 염두에 두고 운영체제 별로 다른 API를 호출할 때 많이 쓰인다. 
실제로 윈도우 비스타와 7에서의 워드패드 프로그램은 많은 수의 함수들을 직접 불러와 사용하고 있다. 특히 워드패드의 경우 GetProcAddress를 사용해 함수 주소를 저장해두고 사용하기 때문에, 변형된 IAT 값을 다시 원래대로 복원해도 후킹 된 함수 주소를 계속 가지게 된다. 이 경우 후킹 된 함수를 메모리에서 제거할 수 없는데, 이 문제는 특히 전역 후킹을 사용하는 프로그램의 경우 큰 문제가 된다. 

결과적으로 IAT 후킹 방식은 간단하고 성능 저하도 적지만 적용되지 않는 경우가 많다. 또한 후킹된 함수를 원래 함수로 복원하는 경우에도 문제가 발생할 수 있어 실제로 사용하기는 다소 어려운 기법이다.

에뮬레이션 및 가상화 방법
에뮬레이션 및 가상화 방법은 한 응용프로그램을 완전히 가상화시켜 각 어셈블리 명령들이 실행되기 전 명령어를 마음대로 수정하는 것이다. 이를 구현하는 라이브러리에는 QEMU, Pin 같은 것들이 있다. 자세한 내용 다루기에는 지면이 부족한 관계로 간단하게만 설명한다. 관심이 있는 독자는 http://qemu.org와 http://pintool.org를 참고하기 바란다. 

기본적으로 에뮬레이션 및 가상화 방법은 어셈블리 코드를 CPU가 직접 읽어서 실행하는 과정 사이에 특별한 계층을 하나 생성해, 이곳에서 원본 코드를 번역해 실행한다. 번역 과정 중에서 프로그래머는 자신이 원하는 특정한 명령어가 실행될 때의 기능을 추가하거나 특정 명령어가 실행되지 않게 할 수 있다. <그림 10>은 이런 내용을 그림으로 표현한 것이다. 왼쪽에 있는 것이 일반적인 실행 환경이며 여기에서는 CPU가 직접 프로그램의 명령을 실행한다. 오른쪽에 있는 것이 가상화된 실행 환경이며 중간에 코드를 번역(해석 및 변경)하는 과정에서 여러 가지 추가 작업들이 이뤄질 수 있다. 

<그림 10> 에뮬레이션 및 가상화 기법에 대한 기본적인 구조도

이런 기법들은 단순히 함수뿐만 아니라 명령어(어셈블리 코드 ; x86 Instruction)자체를 재 정의할 수 있기 때문에 매우 효과적이다. 하지만 번역과정에서 자체적으로 설계된 디스 어셈블러가 필요하기 때문에, 새로운 CPU만 지원하는 최신 명령어를 사용하는 프로그램의 경우에는 지원하지 못할 수 있다. 

또 최소 한번은 명령어를 실행하기 전에 미리 읽어 둬야 하므로 이로 인한 성능저하도 무시할 수 없다. 결과적으로 이 방법은 효과적이지만 실제 범용 프로그램들에 적용하기에는 다소 문제가 있다. 또한 추후 CPU에 대한 지원을 고려해 보았을 경우 상용 제품에서 사용하기에는 조금 어렵다.

Code Overwrite
마지막으로 소개할 Code overwrite 기법은 실제 상용 프로그램에서 가장 많이 사용되고 있는 기법이며, 가장 호환성과 효율성이 좋은 방법이다. 다만 이 기법은 다소 복잡하고 어셈블리 언어에 대한 해박한 지식 습득이 선행돼야 한다. 간략하게 이 방법을 설명하면 Debugging API를 사용하는 방식에서 <그림 6>과 같이 코드를 변경한다. 다만 이때 변경하는 코드가 중단점을 의미하는 INT3(0xCC)가 아닌 자신이 원하는 함수로 이동하는 jmp 명령인 게 다르다. 

jmp 명령은 C 언어에서 goto 문과 같다고 할 수 있다. 이 명령은 어떠한 변화도 없이 단순하게 실행 위치를 변경하는 역할만을 수행한다. 그렇지만 jmp로 코드를 변경하려면 일반적으로 5바이트가 필요하다. 이는 jmp 명령은 명령어를 나타내기 위한 1바이트와 주소값을 지정하기 위한 4바이트(32비트)로 구성돼 있기 때문이다. 5바이트를 함수 처음 부분에 쓰기 전 원본 명령어를 다른 곳에 복사해 두고 원본 함수를 호출할 경우에만 사용한다. <그림 11>은 Function A라는 함수가 이러한 방식으로 후킹된 후에 실제로 코드가 어떻게 나누어지는지를 표현한 것이다.

<그림 11> Function A를 후킹한 후의 코드상태

<그림 11>과 같이 후킹 된 이후에는 Function A를 호출하는 모든 경우에 NewFunctionA로 이동하게 된다. 원본 함수를 호출하려면 Original Function Code로 표시된 주소를 호출해 원본 함수와 동일하게 처리한다. 물론 Original Function Code에 있는 내용, 다시 말해  Function A의 시작 부분에 상대 주소를 가지고 있는 명령어가 있다면 명령어가 실행되는 위치에 따라 코드 의미가 바뀌게 되므로 문제가 될 수 있다. 

예를 들면 어셈블리 명령어 중 ‘현재 위치(실행중인 주소; EIP)로부터 0x10떨어진 곳으로 이동’과 같은 형태의 명령어가 있다면, 이 명령어가 실행되는 위치가 달라지면서 이동 후 실행될 위치도 달라질 것이다. 하지만 대부분의 API 함수는 위와 같이 함수의 프레임을 단순하게 만드는 코드를 가지고 있어 현실적으로 큰 문제가 되지 않는다.

다만 Code Overwrite 방식을 구현하기 위해서는 간단한 디스 어셈블러가 필요하다. 왜냐하면 함수 초입에 들어가는 어셈블리 명령어들을 적절하게 백업하기 위해서다. <그림 11>과 같은 경우에도 PUSH EBP부터 AND ESP, FFFFFFF8까지 세 개의 명령을 합치면 8바이트가 된다. 그러나 이 경우 필요한 공간인 5바이트보다 3바이트가 더 남는다. 따라서 적절한 디스 어셈블러가 없다면 AND ESP, FFFFFFF8 명령의 중간부터 백업받게 돼 문제가 발생하게 된다. 이로 인해 구현이 다소 복잡해진다는 문제가 있으며 함수가 호출되는 중에 이 작업을 수행하면 그 또한 문제를 발생시킬 수 있다. 

이러한 여러 가지 상황에 대응하기 위한 세밀한 처리들이 필요하다. 하지만 이러한 여러 가지 어려움에도 불구하고, Code Overwrite 방식은 현존하는 후킹 방법 중 가장 정확하고 확실한 방법이다. 그렇기 때문에 많은 상용 제품들이 이러한 문제점에 적절하게 대응하는 라이브러리를 사용하고 있다.

이런 라이브러리 중 가장 오래되고 신뢰할 수 있는 라이브러리는 Detours 라이브러리다. 하지만 이 라이브러리는 상용 제품에 사용하려면 고액을 지불해 구매해야 한다는 단점이 있다. 또한 무료 버전이 32비트만 지원한다는 문제도 있다. Detours이외에 madCodeHook과 같은 라이브러리들도 있지만 필자가 소개하고자 하는 것은 EasyHook이라는 오픈소스 라이브러리이다. 

EasyHook은 Detours와 유사한 형태의 라이브러리이지만 무료라는 완전히 다른 장점이 있다. 물론 EasyHook을 사용하게 되면 Detours와는 달리 추가적인 DLL을 배포해야 하며, 이를 사용한 제품은 반드시 ‘Powered by EasyHook’라는 로고를 명시해야 한다. 이에 대한 자세한 내용은 EasyHook 웹사이트의 첫 화면에서 찾아볼 수 있다. 이러한 제약에도 불구하고 Detours를 구매하는 것이 부담스러운 독자들에게는 EasyHook이 Detours를 대체할 수 있는 아주 좋은 라이브러리라고 생각한다. 다음 시간부터 본격적인 후킹 구현에 필자는 EasyHook과 Detours 두 가지 모두를 사용할 것이다. 관심 있는 독자들은 웹사이트(http://easyhook.codeplex.com/)에 미리 들려 간단하게나마 EasyHook을 구경하는 것도 좋을 듯싶다. 

마지막으로 <그림 12>는 EasyHook을 사용해 후킹한 경우 코드가 어떻게 변경되는지를 보여준다. 좌측은 후킹 이전이며 우측은 후킹 이후의 함수 코드다. 결과적으로 <그림 11>과 같은 방법을 사용해 코드가 변경됐다는 것을 확인할 수 있다. 또한 <그림 12>에서 보면 함수의 시작 부분에 의미 없는 코드인 MOV EDI, EDI 라는 명령어가 있는데, 이는 윈도우즈 XP 이후에 이뤄진 Hot Patching과 후킹 방식을 사용해 원활한 윈도우 업데이트하고자 추가된 내용이다. 만약 이 코드가 없다면 MOV EBP, ESP 아래의 CMP 명령어까지 영향을 받게 되지만, MOV EDI, EDI 코드가 있기 때문에 필요한 5바이트를 안전하고 정확하게 얻어낼 수 있다.

<그림 12> Function A를 후킹한 후의 코드 상태

가장 중요한 것은 기본
이번 시간에는 후킹을 위해 반드시 필요한 기본 내용들을 알아봤다. 무엇을 하던 가장 중요한 것은 기본기다. 그러나 현실에서는 바쁘다는 이유로 기본기를 채 쌓지도 못하고 실무를 시작하는 경우가 허다하다. 필자 또한 후킹 기술을 사용하기 이전에 반드시 알아야 할 어셈블러나 시스템에 대한 기본적인 지식과 PE 파일 등에 대한 지식 없이 후킹을 하는 프로그래머들을 많이 봤다. 심지어 상용 프로그램을 만드는 경우에도 이런 모습을 많이 볼 수 있었다. 그러나 이 경우 매우 단순한 오류가 나도 해결할 방법이 없다. 후킹 기술은 매우 안정된 기술이며, 좋은 솔루션을 작성할 수 있는 첩경이다.

원본 :
http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=39157  

'Window Programming' 카테고리의 다른 글

Key Logger  (0) 2014.04.29
InjectDll  (0) 2012.02.08
WIN 32 API 시작하기전에 간단히 알아두기  (0) 2012.01.13