반응형

API Hooking 에 대한 강좌입니다.
User 에서 API Hooking의 다양한 기법들에 대해서 자세히 설명하도록 하겠습니다.



후킹 (Hooking)

리버싱에서 후킹은 정보를 가로채고, 실행 흐름을 변경하고, 원래와는 다른 기능을 제공하게 하는 기술입니다.

후킹의 전체 프로세스는 아래와 같습니다.

- 디스어셈블러/디버거를 이용하여 프로그램의 구조와 동작원리를 파악
- 버그 수정 또는 기능 개선에 필요한 Hook 코드를 개발 [1]
- 실행 파일과 프로세스 메모리를 자유롭게 조작하여 Hook 코드 설치

위와 같은 일련의 작업들은 그야말로 리버스 엔지니어링 기술의 핵심(Core) 이라고 할 수 있습니다.

그래서 전 후킹을 "리버싱의 꽃" 이라고 부릅니다.

여러가지 후킹 기술이 있지만 그중에서도 API 를 후킹하는 기술을 API Hooking 이라고 하고, User mode 후킹 중에서 메시지 후킹[2]과 함께 가장 널리 사용되는 기술입니다.


[1] 프로그램 소스가 있다면 대부분의 경우 후킹은 필요하지 않을 것입니다. 하지만 특수한 상황(소스 코드가 없거나, 소스 코드의 수정이 여의치 않은 상황)에서는 후킹 기술이 요긴하게 사용될 수 있습니다.

[2] 메시지 후킹(Message Hooking) 에 대한 내용은 제 글을 참고하시기 바랍니다.

- Windows Message Hooking (윈도우 메시지 후킹)



API (Application Programming Interface)


API Hooking 을 설명하려면 먼저 API(Application Programming Interface) 에 대해서 짚고 넘어가야 합니다.

Windows OS 에서는 사용자 어플리케이션이 시스템 자원(메모리, 파일, 네트워크, 비디오, 사운드, 기타)을 사용하고 싶을 때 직접 할 수 있는 방법이 없습니다. 왜냐하면 그것들은 OS 가 직접 관리하며, 여러 가지 이유(안정성, 보안, 효율, 기타)로 사용자 어플리케이션의 직접적인 접근을 막아놓았기 때문입니다.

이럴 때 사용자 어플리케이션은 시스템 커널에게 요청해야 합니다. 요청 방법이 바로 MS 에서 제공한 Win32 API 를 이용하는 것입니다. (API 는 해당 OS 제작사에서 제공합니다.)

즉, API 함수 없이는 어떤 의미 있는 프로그램을 만들어 낼 수 없습니다.

아래 그림은 32 bit Windows OS 의 프로세스 메모리를 간략히 나타낸 것입니다.


<Fig. 1>

실제 어플리케이션 코드를 실행 시키기 위해 많은 시스템 라이브러리(DLL) 들이 로딩됩니다. [3] 모든 프로세스에는 기본적으로 kernel32.dll 이 로딩되구요, kernel32.dll 은 ntdll.dll 을 로딩합니다. (참고로 GUI 어플리케이션에서는 user32.dll 과 gdi32.dll 또한 필수 라이브러리 입니다.)

ntdll.dll 의 역할이 바로 유저 모드 어플리케이션의 코드에서 발생하는 시스템 자원에 대한 접근을 커널 모드에게 요청 하는 것입니다.


간단한 를 들어보겠습니다.
notepad.exe 에서 c:\abc.txt 라는 파일을 열고자 합니다.
코드에서는 msvcrt!fopen() API 를 호출합니다. 그 이후의 API 호출 흐름을 보면 아래와 같습니다.

msvcrt!fopen()
 
kernel32!CreateFileW()
    ntdll!ZwCreateFile()
      ntdll!KiFastSystemCall()
        SYSENTER                     // Intel IA-32 Op Code
          => 커널 모드 진입


일반적인 시스템 자원을 사용하는 API 는 kernel32.dll 과 ntdll.dll 을 타고 가다가 결국 SYSENTER 명령을 통해 커널 모드로 진입하게 됩니다.


[3] 'DLL 로딩(loading)' 이라는 용어보다는 'DLL 매핑(mapping)' 이라는 용어가 더 정확한 표현입니다. Windows 운영체제는 DLL 을 최초 한번만 메모리에 적재(loading) 하고, 그 이후부터는 프로세스에게 매핑(mapping) 시켜주는 메커니즘을 사용합니다.



API Hooking


API Hooking 이란 Win32 API 호출을 중간에서 가로채서 제어권을 얻어내는 것입니다.

API Hooking 의 이점은 다음과 같습니다.

- API 호출 전/후에 사용자의 훅 코드(Hook Code)를 실행시킬 수 있습니다.
- API 에 넘어온 파라미터 혹은 API 함수의 리턴값을 엿보거나 조작 할 수 있습니다.
- API 호출 자체를 취소시키거나 사용자 코드로 실행 흐름을 변경시킬 수 있습니다.

이해를 돕기 위해 아래 그림을 봐주시기 바랍니다.

먼저 정상적인 API 호출입니다.


<Fig. 2>

코드 영역 주소에서 CreateFile() API 를 호출하였습니다. [4]

CreateFile() API 는 kernel32.dll 에서 서비스(export) 하므로 kernel32.dll 영역의 CreateFile() API 가 실행되고 정상적으로 리턴합니다.

[4] 실제 kernel32 에서 서비스되는 API 이름은 CreateFileA() 와 CreateFileW() 입니다. 프로그래밍할 때 CreateFile() 만 써주면 컴파일 시에 적절히 CreateFileA()/CreateFileW() 중에서 결정됩니다. 여기서는 설명의 편의상 CreateFile() 로 하였습니다.


다음은 kernel32!CreateFile() 가 후킹된 경우입니다.


<Fig. 3>

사용자가 DLL Injection 기술로 hook.dll 을 프로세스 메모리 공간에 침투 시킵니다. 그리고 kernel32!CreateFile() 를 hook!MyCreateFile() 로 후킹하였습니다. (후킹 함수 설치 방법은 DLL Injection 말고도 더 있습니다.)

이제부터 해당 프로세스에서 CreateFile() API 가 호출 될 때마다 kernel32!CreateFile() 이 호출 되는 것이 아니라, hook!MyCreateFile() 이 호출 됩니다.

후킹 함수(MyCreateFile)와 원본 함수(CreateFile)의 호출 순서는 경우에 따라 달라집니다.
입력된 파라미터를 조작하고 싶을 때는 후킹 함수가 먼저 실행되고 나중에 원본 함수가 실행됩니다. 또한 API 리턴값을 조작하고 싶을 때는 원본 함수가 먼저 실행된 후 후킹 함수가 나중에 실행됩니다.

이 외에도 후킹 목적에 따라서 원본 함수 호출 전/후에 후킹 함수를 실행시키거나 원본 함수를 아예 호출하지 않는 등의 여러 가지 변형이 가능합니다. 따라서 후킹 목적과 상황에 따라서 적절히 사용해 주시면 되겠습니다.

이것이 바로 API Hooking 의 기본 개념입니다.

API Hooking 을 구현하는 방법은 다양합니다. 하지만 후킹의 기본 개념은 변하지 않습니다.

위의 개념을 잘 이해하시면 이후에 설명 드리는 구현 방법에 대해서도 쉽게 이해하실 수 있습니다.


+---+

다음 포스트로 이어집니다.

API Hooking - Tech Map


ReverseCore

반응형
반응형


리버스 엔지니어링 분야에 대해 질문이 있으시면 아래의 댓글로 올려주세요. 역시 댓글로 답변을 올려드리겠습니다. 공개하기 어려운 내용은 '비밀글' 에 체크해주세요.

"비도덕적, 불법적" 인 내용은 답변 드릴 수 없음을 이해해 주시기 바랍니다.


댓글, 방명록, 이메일 등으로 많은 분들께서 여러 가지 질문들을 해주십니다.
그중에 정말 좋은 질문들이 많아서 여러분들과 공유하면 좋겠다고 생각하였습니다.

이제부터 저에게 들어오는 모든 질문과 답변들을 이곳으로 모을 것입니다.
앞으로 질문과 답변은 이곳에서 해주세요~

질문에 대한 답변은 저 뿐만 아니라 제 블로그에 오시는 모든 분들께서 하실 수 있습니다.
문제 해결을 위한 방법은 다양합니다. 저 말고도 다른 분들의 답변은 언제나 환영입니다.

"질문은 좋은 것 입니다. 많이 해주세요. ^^ "

* 댓글로 질문하기 어려운 내용들(긴내용, 첨부파일 등)은 제 이메일(reversecore@gmail.com)로 문의해 주세요.

* 댓글이 너무 많이 달리면 제가 질문/답변 빈 포스트를 또 올릴 겁니다. 그쪽으로 계속 질문 댓글 달아주시면 됩니다.

감사합니다.


ReverseCore



반응형

'q&a' 카테고리의 다른 글

질문/답변 코너입니다. (3)  (381) 2011.04.13
질문/답변 코너입니다. (2)  (223) 2010.01.28
반응형

지금까지 우리가 원하는 DLL 을 "실행중인" 프로세스에 강제로 Injection 시키는 방법에 대해서 알아봤습니다.

이번에는 접근 방법을 바꿔서 아예 대상 프로그램 "파일을 직접 수정"하여 DLL 을 강제로 로딩하도록 해보겠습니다.
이 방법은 한 번 적용해 놓으면 (별도의 Injection 과정없이) 프로세스가 시작할 때마다 원하는 DLL 을 로딩하게 만들 수 있습니다. 일종의 크랙이라고 생각하시면 됩니다.

목표는 notepad.exe 파일을 직접 수정하여 실행 시 myhack3.dll 을 로딩하도록 만드는 것입니다.
이를 위해서는 PE Header 를 자유자재로 다룰 수 있는 실력이 필요합니다.

IDT(Import Directory Table)와 기타 PE File Format 에 대해서는 아래의 글을 참조하시기 바랍니다.

- PE(Portable Executable) File Format (6) - PE Header



myhack3.dll


먼저 로딩 시킬 DLL 의 소스를 보겠습니다.

// myhack2.cpp
// ReverseCore

include "windows.h"

#define DEF_CMD  "c:\\Program Files\\Internet Explorer\\iexplore.exe"
#define DEF_ADDR "www.naver.com"

#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) BOOL start()
{
    char szCmd[MAX_PATH] = {0,};
    STARTUPINFO si = {0,};
    PROCESS_INFORMATION pi = {0,};

    si.cb = sizeof(STARTUPINFO);
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;

    // IE 를 hidden window 로 실행시켜 naver 에 접속합니다.
    wsprintf(szCmd, "%s %s", DEF_CMD, DEF_ADDR);
    if( !CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd, 
                       NULL, NULL, FALSE,
                       NORMAL_PRIORITY_CLASS,
                       NULL, NULL, &si, &pi) )
        return FALSE;

    if( pi.hProcess != NULL )
        CloseHandle(pi.hProcess);

    return TRUE;
}
#ifdef __cplusplus
}
#endif

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH :
            start();
            break;
    }
  
    return TRUE;
}

myhack3.dll


기능은 지난번과 마찬가지로 IE 를 hidden 속성으로 실행시켜 Naver 초기화면에 접속시키는 것입니다.

가장 눈에 띄는 것은 start() EXPORT 함수입니다.

PE 파일에서 어떤 DLL 을 IMPORT 한다는 것은 코드내에서 그 DLL 의 EXPORT 함수를 호출한다는 의미입니다. 그래서 myhack3.dll 파일은 형식적인 완전성을 위해 EXPORT 함수를 제공해야 합니다. (실제로 notepad.exe 내부에는 myhack3.dll!start() 함수를 호출하는 코드 같은건 존재하지 않습니다.)

프로그래밍에서 DLL 의 IMPORT 는 컴파일러/링커가 해주지만, 저는 이 작업을 PE View 와 Hex Editor 만을 가지고 수동으로 해보겠습니다. (제 환경은 WinXP SP3 KOR 버전입니다. OS 와 SP 버전에 따라서 방법이 약간 틀려질 수 있습니다.)



파일 변경 아이디어 

notepad.exe 파일이 실행될 때 자동으로 myhack3.dll 파일을 로딩하기 위해서는 PE Header 의 IMPORT DIRECTORY TABLE 에 myhack3.dll 을 (IMPORT 하도록) 추가해야 합니다.

notepad.exe 원본과 제가 수정한 notepad_hack.exe 를 첨부합니다.

notepad.exe

notepad_hack.exe


PE View 를 이용하여 notepad.exe 의 IDT 의 주소를 확인해 보겠습니다. IMAGE_OPTIONAL_HEADER 의 IMPORT Table RVA 값이 바로 IDT 의 RVA 입니다.


<Fig. 1>

위 그림에서 IDT 주소는 RVA 로 7604 입니다. 이를 PE View 로 따라가 보겠습니다. (PE View 의 주소 보기 옵션을 "RVA" 로 해주세요.)



<Fig. 2>

IDT 는 <Fig. 2> 와 같이 IMAGE_IMPORT_DESCRIPTOR(이하 IID) 구조체 배열로 되어 있으며, 마지막은 NULL 구조체로 끝납니다. IMPORT 하는 DLL 파일 하나당 IID 구조체 하나가 필요합니다. (IID 구조체 하나의 크기는 14h bytes)

PE View 의 주소 보기 옵션을 "File Offset" 으로 변환하면 IDT 의 file offset 은 6A04 입니다.
Hex editor 를 이용해 6A04 주소를 찾으면 아래 그림과 같습니다.


<Fig. 3>

IDT 는 file offset 으로 6A04 ~ 6ACC 범위에 있으며, 전체 크기는 C8 입니다.
마지막 NULL 구조체를 포함하여 총 10개의 IID 구조체가 있습니다. 

처음에 전 IDT 마지막에 myhack3.dll 을 위한 IID 구조체를 추가하려 했습니다만, 더 이상 추가할 공간이 없습니다. (마지막 NULL 구조체는 꼭 필요하거든요.)

그래서 Hex editor 로 notepad.exe 의 빈 영역(사용되지 않는 영역)을 찾아 보았습니다.
<Fig. 4> 에서 보이는 것처럼 마침 파일 끝부분에 알맞은 크기가 비어 있네요.

* 만약 파일에 빈 영역이 보이지 않는다면 파일 끝에 새로운 섹션을 추가하거나, 또는 마지막 섹션의 크기를 늘리는 방법을 사용해야 합니다.


<Fig. 4>

바로 이 위치(file offset = 10710, RVA = 13310)에 새로운 IID 를 만들것입니다.

주의 하실 점은 저 영역(file:10710 ~ 107FF, RVA:13310 ~ 133FF)이 과연 메모리에 로딩되는 영역인지 확인하는 것입니다.
파일에 존재한다고 해서 반드시 메모리에 로딩되는 것은 아닙니다. 섹션 헤더에 명시된 영역만 메모리에 로딩됩니다.


notepad 의 마지막 섹션 헤더(".rsrc") 를 살펴보겠습니다.


<Fig. 5>

RVA = B000 이고 VirtualSize = 8304 이므로 메모리에 로딩되는 영역은 RVA = 13304 까지입니다.
하지만 <Fig. 4> 의 파란색 영역(RVA:13310 ~ 133FF)은 포함되지 않는 영역입니다.

그렇다면 이부분은 메모리에 로딩되지 않을까요?
실제로는 메모리에 로딩됩니다. 이유는 notepad 에서 IMAGE_OPTIONAL_HEADER 의 SectionAlignment 값이 1000 이기 때문에 마지막 섹션(".rsrc")헤더에 명시된 위치(RVA=13304)는 실제로 14000 까지 확장됩니다. (기본 단위의 배수)

디버거에서 확인해 보겠습니다.

<Fig. 6>

위 그림을 보시면 notepad 프로세스 메모리에서 마지막 섹션(".rsrc")의 VirtualSize 값이 9000 으로 확장된 것을 보실 수 있습니다. <Fig. 5> 의 실제 VirtualSize 값은 8304 였으나 SectionAlignment (1000) 의 배수인 9000 으로 자동 확장된 것입니다.

따라서 RVA = 13310 위치에 새로운 IID 를 만드는 것은 문제가 없습니다.



IDT(Import Directory Table) 변경


notepad.exe 를 복사하셔서 이름을 notepad_hack.exe 로 변경해주세요.
이제부터 notepad_hack.exe 파일을 가지고 변경 작업을 진행합니다.

#1. IMPORT Table 의 RVA 값 변경

PE View 를 기준으로 설명드리겠습니다.
IMAGE_OPTIONAL_HEADER 의 IMPORT Table 구조체 멤버는 IDT 의 위치와 크기를 알려줍니다. <Fig. 1> 을 보시면 현재 IMPORT Table 의 RVA 값은 7604 입니다. Hex editor 에서 이 값을 새로운 IDT 의 RVA 값인 13310 으로 변경합니다. 그리고 Size 값을 기존 C8 에 14 (IID 구조체 크기) 를 더한 값인 DC 로 변경합니다. (Size 멤버는 수정 하지 않아도 됩니다.)


<Fig. 7>

PE View 로 확인하면 아래 그림과 같습니다.

<Fig. 8>

이제 notepad 실행 시 PE Loader 는 IDT 가 RVA = 13310 에 존재한다고 간주합니다.

#2. BOUND IMPORT TABLE 사용 안함


PE HEADER 에서 BOUND IMPORT TABLE 은 DLL 로딩 속도를 조금 향상시킬 수 있는 기법입니다. 이 값을 그냥 놔둬도 문제 없는 경우가 많지만, 제가 테스트할 때는 myhack3.dll 이 정상동작하지 않았습니다. (편하게 작업하시려면 이 값을 무조건 0 으로 밀어버리시기 바랍니다.)

Hex editor 를 이용해서 BOUND IMPORT Table 영역(RVA, Size)을 0 으로 밀어버립니다.

<Fig. 9>

PE View 로 확인해 볼까요?

<Fig. 10>

#3. 새로운 IDT 생성

기존 IDT (file = 6A04 ~ 6ACB, RVA = 7604 ~ 76CB) 를 복사하여 새로운 위치(file = 10710, RVA = 13310)에 붙여넣고, 나머지 지저분한 데이타들은 깔끔하게 NULL 로 밀어버립니다.

<Fig. 11>

이 상태에서 아래와 같이 myhack3.dll 을 위한 IID 를 구성하여 새로운 IDT 끝에 추가시킵니다.
(각 멤버의 데이타에 대해서는 아래에서 따로 설명드리겠습니다.)

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            
        DWORD   OriginalFirstThunk;       // INT(Import Name Table) => 8750
    };

    DWORD   TimeDateStamp;                // => FFFFFFFF

    DWORD   ForwarderChain;               // => FFFFFFFF
    DWORD   Name;                         // library name => 8760
    DWORD   FirstThunk;                   // IAT(Import Address Table) => 1348
} IMAGE_IMPORT_DESCRIPTOR;


아래는 Hex editor 로 작업한 그림입니다. (IDT 마지막에 myhack3.dll 을 위한 IID 를 추가)

<Fig. 12>

#4. IID(IMAGE_IMPORT_DESCRIPTOR) 구성

앞 단계에서 추가한 IID 구조체 멤버들이 정상적으로 동작하기 위해서 적절한 세팅이 필요합니다.

먼저 INT(Import Name Table) 와 Name 멤버 설명입니다. 
이 값들은 각각 RVA = 8750 (file = 7B50), RVA = 8760 (file = 7B60) 입니다.

참고로 이 위치는 첫번째 섹션(".text")의 끝부분으로써 빈 영역입니다. 제가 편의상 이 영역을 선정한 것이며 다른 위치에 선정하셔도 상관 없습니다.

일단 Hex Editor 로 RVA = 8750 (file = 7B50) 위치로 가서 아래 그림과 같이 값을 입력합니다.

<Fig. 13>

위 그림에 나타난 값들의 의미를 설명드리겠습니다.

INT 는 쉽게 말해서 RVA 배열인데요, 배열의 각 원소는 Import 하는 함수의 Ordinal(2 bytes) + Func Name String 구조체의 주소(RVA) 를 나타내며 배열의 끝은 NULL 입니다. 

<Fig. 13> 에서 INT 에는 1 개의 원소가 있고 그 값은 RVA 8770 입니다. RVA 8770 으로 가보면 Import 하려는 함수의 Ordinal (2 bytes) 와 함수 이름 문자열이 나타납니다.

Name 은 Import 하는 함수를 제공하는 DLL 파일의 이름 문자열 입니다. "myhack3.dll" 이라고 보이시죠?

마지막으로 IID 의 IAT(Import Address Table) 멤버에 대해 설명드리겠습니다.

#3 에서 IAT 값을 RVA = 1348 (file = 748) 로 세팅하였습니다. 참고로 이 위치는 원래 notepad 의 IAT 영역의 끝 부분입니다.
(INT 데이타와 IAT 데이타는 각각 뭉쳐서 존재합니다.)


<Fig. 14>

IAT 역시 RVA 배열입니다. 각 원소는 INT 와 같은 값을 가져도 좋고 NULL 을 가져도 좋습니다. INT 가 정확하다면 IAT 는 다른 값을 가져도 상관없습니다. 어차피 실행 시에 PE Loader 에 의해 메모리 상에서 실제 함수 주소로 덮어써집니다. (간혹 NULL 을 넣었을때 에러는 발생하지 않지만 DLL 이 정상 동작하지 않을 때가 있습니다. 확실한 건 INT 와 같은 값을 써주면 정확히 동작합니다.)

* 반드시 IAT 를 기존 IAT 영역에 쓸 필요는 없습니다만, 프로세스에 따라서 혹은 DLL 에 따라서 정상 동작하지 않는 경우가 있습니다. 세심한 테스트가 필요한 부분입니다.

여기까지 제대로 따라 하셨으면 작업끝입니다.



검증 (Test)


새로 만든 notepad_hack.exe 파일을 PE View 로 열어보겠습니다.


<Fig. 15>

마지막 섹션(".rsrc")로 IDT 가 옮겨졌으며, myhack3.dll 을 IMPORT 시키기 위한 IID 구조체가 정상적으로 세팅된 것을 확인 하실 수 있습니다.


<Fig. 16>

<Fig. 16> 에서 추가된 INT 도 확인 하실 수 있습니다.

notepad_hack.exe 와 myhack3.dll 파일을 같은 폴더에 넣고 notepad_hack.exe 를 실행해 보겠습니다.
Process Explorer 로 확인하면 아래 그림과 같습니다.


<Fig. 17>

notepad_hack.exe 에 myhack3.dll 이 로딩되어서 IE 가 정확하게 실행되는 화면을 보실 수 있습니다.

성공입니다!



Epilogue


설명이 길어서 자칫 복잡하게 느끼실 수 있습니다.

Import Directory Table 에 내가 원하는 dll 을 추가시켜서 실행 시 자동으로 로딩하게 한다는 기본 원리만 확실히 이해하시고, 관련된 PE Header 를 참고하시면 쉽게 하실 수 있으실 겁니다.

단, OS 와 SP 버전에 따라서 PE Loader 의 구현이 조금씩 다를 수 있으니 처음 하시는 분들께서는 약간의 시행 착오를 거치셔야 합니다. 또한 대상 프로세스와 DLL 파일의 프로그래밍에 따라서 약간씩 방법을 달리 적용해야 하는 경우도 있습니다.

그러나 위의 원리를 이해하신다면 어렵지 않게 해내실 걸로 생각합니다.


+---+


다음번에는 프로세스에 Injection 된 DLL 을 강제로 꺼내는 기법인 DLL Ejection 에 대해서 알아보도록 하겠습니다.


ReverseCore



반응형
반응형

 

앞에서 DLL Injection 의 개념에 대해서 알아보았고 CreateRemoteThread() API 를 이용하여 DLL Injection 을 실제로 구현 해보았습니다.

이번에는 DLL Injection 의 또 다른 구현 방법들에 대해서 알아보도록 하겠습니다.




AppInit_DLLs


Windows 운영체제에서 기본으로 제공하는 레지스트리 키 중에서 AppInit_DLLs 란 것이 있습니다.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
  AppInit_DLLs


<Fig. 1>

AppInit_DLLs 값에 인젝션을 원하는 DLL 경로를 써준 후 재부팅하면, 이후 Windows 운영체제는 재부팅하면서 실행되는 모든 프로세스에 해당 DLL 을 인젝션 시켜줍니다. 너무 간단하면서도 너무 강력한 기능이지요.

간단한 실습을 해보겠습니다. 일단 아래 myhack2.cpp 소스를 보시죠.

// myhack2.cpp

#include "windows.h"

#define DEF_CMD  "c:\\Program Files\\Internet Explorer\\iexplore.exe"
#define DEF_ADDR "www.naver.com"
#define DEF_DST_PROC "notepad.exe"

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char szCmd[MAX_PATH]  = {0,};
    char szPath[MAX_PATH] = {0,};
    char *szProcess = NULL;
    STARTUPINFO si = {0,};
    PROCESS_INFORMATION pi = {0,};

    si.cb = sizeof(STARTUPINFO);
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;

    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH :
            if( !GetModuleFileName( NULL, szPath, MAX_PATH ) )
                break;
   
            if( !(szProcess = strrchr(szPath, '\\')) )
                break;

            szProcess++;
            if( stricmp(szProcess, DEF_DST_PROC) )
                break;

            wsprintf(szCmd, "%s %s", DEF_CMD, DEF_ADDR);
            if( !CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd,
                               NULL, NULL, FALSE, 
                               NORMAL_PRIORITY_CLASS,
                               NULL, NULL, &si, &pi) )
                break;

            if( pi.hProcess != NULL )
                CloseHandle(pi.hProcess);

            break;
    }
  
    return TRUE;
}

소스 코드 내용은 간단합니다.
현재 자신을 로딩한 프로세스 이름이 "notepad" 라면 IE(Internet Explorer) 를 숨김모드로 실행시켜 Naver 사이트에 접속하게 됩니다. 목적에 따라서 다양한 업무를 수행할 수 있겠죠?

차례대로 따라해보겠습니다.

#1. 파일 복사

myhack2.dll


첨부된 파일을 적절한 위치에 복사합니다. (제 경우엔 C:\work\myhack2.dll)

#2. 레지스트리 값(AppInit_DLLs) 입력

regedit.exe 를 실행하여 아래와 같이 입력합니다. (myhack2.dll 의 전체경로를 입력해주세요.)

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows

<Fig. 2>

#3. 재부팅

재부팅이 완료되었으면 Process Explorer 를 이용해서 과연 myhack.dll 이 모든 프로세스에 인젝션 되었는지 확인해 보겠습니다.


<Fig. 3>

myhack2.dll 이 모든 프로세스에 성공적으로 인젝션 되었습니다.
인젝션된 myhack2.dll 은 아무 동작을 하고 있지 않습니다. (notepad 프로세스만 대상으로 한다는 걸 기억하세요.)

notepad 를 실행하면 아래 그림과 같이 IE 가 (숨김 속성으로) 실행되는걸 확인할 수 있습니다.


<Fig. 4>


* 주의!
AppInit_DLLs 레지스트리 키는 너무나 강력해서 모든 프로세스에 DLL 을 인젝션 시켜버립니다.
만약 인젝션되는 DLL 에 문제(버그)가 있다면 자칫 Windows 부팅이 안되는 상황이 발생할 수 있습니다.
이 경우 Window Preinstalled Environment 부팅 CD (예:BartPE)가 없다면 복구할 수 없습니다.
따라서 AppInit_Dlls 를 사용할 때는 사전에 철저한 테스트가 필요합니다.




SetWindowsHookEx() - message hook


SetWindowsHookEx() API 를 이용하여 메시지 훅을 설치하면 OS 에서 hook procedure 를 담고 있는 DLL 을 (윈도우를 가진) 프로세스에 강제로 인젝션 시켜줍니다.

이 또한 DLL Injection 의 한 기법입니다.

자세한 설명은 제 글을 참고하시기 바랍니다.

- Windows Message Hooking (윈도우 메시지 후킹)


다음번에는 실행파일의 PE Header 정보를 조작하여 원하는 DLL 파일을 로딩시키는 방법에 대해서 알아보겠습니다. (엄밀히 말하면 DLL Injection 이라고 할 수 는 없습니다만, 리버싱에서 즐겨 사용되는 방법이기에 같이 소개합니다.)


Dll Injection - 다른 프로세스에 침투하기 (4)


ReverseCore


반응형
반응형

DLL Injection 구현 방법


몇 가지 구현 방법이 있습니다.

그 중에서 가장 유명한 방법이 CreateRemoteThread() API 를 이용하는 방법입니다.

이 방법은 윈도우즈 프로그래밍 서적의 바이블인 Jeffrey Richter Programming Applications for Microsoft Windows 에 소개된 내용입니다.

일단 소스 코드를 보겠습니다. (엔지니어에게는 백 마디 설명 보다는 역시 소스 코드를 한번 보는게 낫죠.)

먼저 Injection 시킬 myhack.dll 소스 코드입니다.

myhack.cpp


// myhack.cpp

#include "stdio.h"
#include "windows.h"

#pragma comment(lib, "urlmon.lib")

#define DEF_NAVER_ADDR ("http://www.naver.com/index.html")
#define DEF_INDEX_PATH ("c:\\index.html")

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    URLDownloadToFile(NULL, DEF_NAVER_ADDR, DEF_INDEX_PATH, 0, NULL);
    return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    HANDLE hThread = NULL;

    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH :
            hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
            CloseHandle(hThread);
            break;
    }
  
    return TRUE;
}

아주 간단한 코드입니다.

DllMain() 을 보시면 DLL 이 로딩(DLL_PROCESS_ATTACH)될 때 스레드(ThreadProc)를 실행합니다.

ThreadProc() 의 내용은 urlmon.dll 의 URLDownloadToFile() 함수를 실행시켜서 네이버 초기화면(index.html)을 다운받습니다.

프로세스에 DLL Injection 이 발생하면 해당 DLL 의 DllMain() 함수가 호출된다고 이전 포스트에서 설명드렸습니다. 따라서 notepad.exe 프로세스에 myhack.dll 이 Injection 되면 결국 URLDownloadToFile() 함수가 실행될 것입니다.

* DLLMain() 에서 직접 URLDownloadToFile() 을 호출하면 간혹 hang 이 걸리는 경우가 있어서, 별도의 스레드를 생성하여 호출하도록 프로그래밍 하였습니다.



이제 myhack.dll 을 notepad.exe 프로세스에 Injection 시켜줄 프로그램(InjectDll.exe)의 소스코드를 보시겠습니다.

InjectDll.cpp


// InjectDll.exe

#include "stdio.h"

#include "windows.h"
#include "tlhelp32.h"

#define DEF_PROC_NAME ("notepad.exe")
#define DEF_DLL_PATH ("c:\\myhack.dll")

DWORD FindProcessID(LPCTSTR szProcessName);
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllName);

int main(int argc, char* argv[])
{
    DWORD dwPID = 0xFFFFFFFF;
 
    // find process
    dwPID = FindProcessID(DEF_PROC_NAME);
    if( dwPID == 0xFFFFFFFF )
    {
        printf("There is no <%s> process!\n", DEF_PROC_NAME);
        return 1;
    }

    // inject dll
    InjectDll(dwPID, DEF_DLL_PATH); 

    return 0;
}

DWORD FindProcessID(LPCTSTR szProcessName)
{
    DWORD dwPID = 0xFFFFFFFF;
    HANDLE hSnapShot = INVALID_HANDLE_VALUE;
    PROCESSENTRY32 pe;

    // Get the snapshot of the system
    pe.dwSize = sizeof( PROCESSENTRY32 );
    hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );

    // find process
    Process32First(hSnapShot, &pe);
    do
    {
        if(!_stricmp(szProcessName, pe.szExeFile))
        {
            dwPID = pe.th32ProcessID;
            break;
        }
    }
    while(Process32Next(hSnapShot, &pe));

    CloseHandle(hSnapShot);

    return dwPID;
}

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllName)
{
    HANDLE hProcess, hThread;
    HMODULE hMod;
    LPVOID pRemoteBuf;
    DWORD dwBufSize = lstrlen(szDllName) + 1;
    LPTHREAD_START_ROUTINE pThreadProc;

    // #1. dwPID 를 이용하여 대상 프로세스(notepad.exe)의 HANDLE을 구함
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
        return FALSE;

    // #2. 대상 프로세스(notepad.exe) 메모리에 szDllName 크기만큼 메모리를 할당
    pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

    // #3. 할당 받은 메모리에 myhack.dll 경로("c:\\myhack.dll")를 씀
    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, dwBufSize, NULL);

    // #4. LoadLibraryA() API 주소를 구함
    hMod = GetModuleHandle("kernel32.dll");
    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryA");

    // #5. notepad.exe 프로세스에 스레드를 실행
    hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
    WaitForSingleObject(hThread, INFINITE); 

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

역시 코드가 간결합니다. (편의상 예외처리, 리턴값 체크 등의 코드는 생략하였습니다.)

main() 함수에서는 2개의 서브 함수를 호출하고 있습니다.

FindProcessID(DEF_PROC_NAME) 함수는 프로세스 이름으로 PID(Process ID) 를 구해주는 함수입니다. (설명은 생략합니다.) 그리고 InjectDll(dwPID, DEF_DLL_PATH) 함수가 바로 DLL Injection 을 해주는 핵심 함수입니다.

InjectDll() 함수를 자세히 살펴보겠습니다.

InjectDll() 함수는 대상 프로세스(notepad.exe)로 하여금 스스로 LoadLibrary("myhack.dll") API 를 호출하도록 명령하는 기능을 가지고 있습니다.

#1. 대상 프로세스 핸들 구하기

hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))

OpenProcess() API 를 이용해서 notepad.exe 의 프로세스 핸들을 구합니다. (이때 미리 구해놓은 PID 를 사용함)
이 프로세스 핸들(hProcess)을 이용해서 해당 프로세스(notepad.exe)를 제어할 수 있습니다.

#2-3. 대상 프로세스 메모리에 Injection 시킬 DLL 경로를 써주기

pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

대상 프로세스(notepad.exe)에게 로딩할 DLL 파일의 경로(문자열)를 알려줘야 합니다.
아무 메모리 공간에 쓸 수 없으므로 VirtualAllocEx() API 를 이용하여 대상 프로세스(notepad.exe) 메모리 공간에 버퍼를 할당합니다. 버퍼 크기는 DLL 경로 문자열 길이(NULL 포함)입니다. 

* 주의! 
VirtualAllocEx() 함수의 리턴값(pRemoteBuf)은 할당된 버퍼 주소입니다. 이 주소는 내 프로세스(Inject.exe)의 메모리 주소가 아니라 hProcess 핸들이 가리키는 대상 프로세스(notepad.exe)의 메모리 주소라는것을 꼭 기억하시기 바랍니다.


WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, dwBufSize, NULL);

할당 받은 버퍼 주소(pRemoteBuf)에 WriteProcessMemory() API 를 이용하여 DLL 경로 문자열("C:\\myhack.dll")을 써줍니다.

이로써 대상 프로세스(notepad.exe) 메모리 공간에 Injection 시킬 DLL 파일의 경로가 생겼습니다.

* 참고
Win32 프로그래밍을 처음 배울 때 분명 다른 프로세스의 메모리에 읽고 쓰는 일이 어렵다고(혹은 불가능하다고) 배웠습니다. 하지만 실제로는 다른 프로세스의 메모리 공간에 접근을 못하면 운영체제도 답답해 집니다. (예를 들어 다른 프로세스 메모리에 접근 할 수 없다면 디버거 제작이 불가능해지지요.) 그래서 Windows 운영체제는 Debug API 를 제공하여 다른 프로세스 메모리 공간에 접근 할 수 있도록 하였습니다. 대표적인 Debug API 가 바로 위에서 소개해 드린 VirtualAllocEx(), VirtualFreeEx(), WriteProcessMemory(), ReadProcessMemory() 등이 있습니다.

#4. LoadLibraryA() API 주소를 구하기

hMod = GetModuleHandle("kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryA");


LoadLibrary() API 를 호출시키기 위해 그 주소가 필요합니다.
(LoadLibraryA() 는 LoadLibrary() 의 ASCII 문자열 버전입니다.)

위 코드의 의미를 잘 생각해봐야 합니다.
우리는 분명 notepad.exe 에 로딩된 kernel32.dll 의 LoadLibraryA() API 의 시작 주소를 알아내야 합니다.
하지만 위 코드는 InjectDll.exe 에 로딩된 kernel32.dll 의 LoadLibraryA() API 의 시작 주소를 얻어내고 있습니다.

notepad.exe 에 로딩된 kernel32.dll 과 InjectDll.exe 에 로딩된 kernel32.dll 의 메모리 시작 위치(ImageBase)가 동일 하다면 위 코드는 문제가 없습니다.

일반적인 DLL 파일의 ImageBase 는 0x10000000 으로 설정되기 때문에 a.dll 과 b.dll 을 차례대로 로딩하면 a.dll 은 정상적으로 0x1000000 주소에 로딩이 되겠지만 b.dll 은 자신이 원하는 0x10000000 주소에 로딩되지 못하고 다른 비어 있는 주소 공간에 로딩됩니다. 즉, DLL Relocation 이 발생하는 것입니다. (a.dll 이 같은 주소에 이미 로딩되어 있기 때문입니다.)

만약 kernel32.dll 이 프로세스마다 다른 주소에 로딩된다면 위 코드는 잘못된 것입니다.
하지만 실제 Winodows 운영체제에서 kernel32.dll 은 프로세스마다 같은 주소에 로딩됩니다.

어째서 그런걸까요?

PE View 를 통해서 Windows 운영체제의 핵심 DLL 파일들의 ImageBase 값을 조사해 봤습니다.
(Windows XP SP3 KOR 버전입니다. Windows 업데이트 상태에 따라서 아래 값들은 달라질 수 있습니다.)

DLL file        ImageBase       SizeOfImage
--------------------------------------------
msvcrt.dll      77BC0000        00058000
user32.dll      77CF0000        00090000
gdi32.dll       77E20000        00049000
advapi32.dll    77F50000        000A8000
kernel32.dll    7C7D0000        00130000
shell32.dll     7D5A0000        007FD000
...


Microsoft 에서 친절하게 OS 핵심 DLL 파일들의 ImageBase 값을 이쁘게 정리해놨습니다.
즉, 자신들끼리 절대로 겹치지 않고 따라서 DLL Relocation 이 발생하지 않습니다.

Dll Injection 기법은 위와같이 OS 핵심 DLL 들은 자신만의 고유한 주소에 로딩된는 것을 보장해주는 Windows 특성을 이용한 것입니다. (이 특성이 Windows 보안 취약점으로 이용되기도 합니다.)

따라서 InjectDll.exe 프로세스에 import 된 LoadLibraryA() 주소와 notepad.exe 프로세스에 import 된 LoadLibraryA() 주소는 동일합니다.

* 참고!
모든 Windows 프로세스는 kernel32.dll 을 로딩합니다.
PE Header 를 조작하여 IAT 에서 kernel32.dll 항목을 제거해버려도 loader 가 강제로 kernel32.dll 을 로딩시켜버립니다. (XP 부터 해당됨. 2000 에서는 실행불가.)


#5. 대상 프로세스에 스레드를 실행 시킴

모든 준비는 끝났고 마지막으로 notepad.exe 로 하여금 LoadLibraryA() API 를 호출하도록 명령만 내리면 됩니다. 하지만 Windows 에서는 그런 API 를 제공하지 않습니다.

그래서 편법(?)으로 CreateRemoteThread() API 를 사용합니다.
(편법이라기 보다는 DLL Injection 의 정석이라고 말 할 수 있지요.)

CreateRemoteThread() API 는 다른 프로세스에게 스레드를 실행시켜주는 함수입니다.

HANDLE WINAPI CreateRemoteThread(
  __in   HANDLE                   hProcess,             // 프로세스 핸들
  __in   LPSECURITY_ATTRIBUTES    lpThreadAttributes,
  __in   SIZE_T                   dwStackSize,
  __in   LPTHREAD_START_ROUTINE   lpStartAddress,       // 스레드 함수 주소
  __in   LPVOID                   lpParameter,          // 스레드 파라미터 주소

  __in   DWORD                    dwCreationFlags,
  __out  LPDWORD                  lpThreadId
);


첫번째 파라미터인 hProcess 만 빼면 일반적으로 사용되는 CreateThread() 함수와 다 똑같습니다.

hProcess 파라미터가 바로 스레드를 실행시킬 프로세스의 핸들입니다.
lpStartAddress 와 lpParameter 파라미터는 각각 스레드 함수 주소와 스레드 파라미터 주소입니다.
중요한건 이 주소들이 대상 프로세스의 가상 메모리 공간의 주소이어야 한다는 것입니다. (그래야 그 프로세스에서 인식을 할 수 있겠죠.)

좀 어리둥절 하시죠?
다른 프로세스에 DLL 을 injection 시키는데 스레드가 무슨 상관일까요?

스레드 함수 ThreadProc() 과 LoadLibrary() API 를 보시면 힌트를 얻을 수 있습니다.

DWORD WINAPI ThreadProc(
  __in  LPVOID           lpParameter
);

HMODULE WINAPI LoadLibrary(
  __in  LPCTSTR          lpFileName
);


두 함수 모두 4 byte 파라미터를 받고, 4 byte 값을 리턴하지요.
바로 여기서 아이디어를 얻은 것입니다.

CreateRemoteThread() 를 호출해서 4 번째 파라미터 lpStartAddress 에 "LoadLibrary() 주소"를 주고, 5 번째 파라미터 lpParameter 에 원하는 "DLL 의 경로" 문자열을 주면 됩니다. (반드시 대상 프로세스의 가상 메모리 공간에의 주소이여야 합니다.)

우린 이미 위에서 다 준비해놨지요. 편안하게 호출해주면 됩니다.

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);

  pThreadProc = notepad.exe 의 LoadLibraryA() 주소
  pRemoteBuf = notepad.exe 의 "c:\\myhack.dll" 문자열 주소


CreateRemoteThread() 는 스레드를 생성하는 것이 아니라 실제로는 LoadLibraryA() 를 호출 시키는 것입니다.



+---+

CreateRemoteThread() 를 이용한 Dll Injection 기법에 대한 설명을 마치겠습니다.

처음에는 잘 이해가 되지 않을 수 있습니다. 설명을 다시 차근차근 읽어보시고 직접 실습해 보세요.

다음 번에는 다른 Dll Injection 기법들과 한번 Injection 된 Dll 을 꺼내는 (Ejection) 방법에 대해서 설명드리도록 하겠습니다.


Dll Injection - 다른 프로세스에 침투하기 (3)


ReverseCore

반응형

+ Recent posts