미친해커

[Reversing] EAT Hooking Step 1 본문

Reversing/Hooking

[Reversing] EAT Hooking Step 1

미친해커 2022. 7. 28. 02:31
반응형
 

[Reversing] EAT Hooking Step 0

EAT Hooking EAT(Export Address Table) Hooking 이란 EAT의 API 주소를 조작하여 후킹하는 기법을 말한다. EAT에는 라이브러리가 외부로 Export 하는 함수명 또는 변수명과 주소가 기록되어 있다. 응용 프로그램..

crazyhacker.tistory.com

이전 포스팅에서 EAT 구조에 대해서 간단히 배웠었다. 이번 포스팅에서는 C언어를 활용해 실제 EAT에 등록되어 있는 함수들의 이름과 주소를 불러오는 프로그램을 만들어볼 예정이다.

Get Library ImageBase

EAT 주소를 구하기 위해서는 IMAGE_DOS_HEADER(Dos Header)를 구해야 한다. Dos Header의 주소는 해당 라이브러리의 ImageBase의 주소와 동일하다.

ULONGLONG ImageBase = GetModuleHandleA("user32.dll");

GetModuleHandleA

GetModuleHandleA API는 특정 라이브러리의 ImageBase를 반환하는 함수이다.

 

LPCSTR(const char *) 자료형을 인자로 받으며 라이브러리의 이름을 전달하면 된다. NULL이 전달될 경우 해당 함수는 현재 프로그램의 ImageBase를 반환한다.

HMODULE GetModuleHandleA(LPCSTR lpModuleName);
 

GetModuleHandleA function (libloaderapi.h) - Win32 apps

Retrieves a module handle for the specified module. The module must have been loaded by the calling process. (ANSI)

docs.microsoft.com

실제 함수 원형을 살펴보면 HMODULE이라는 Handle(핸들)을 반환하지만 이는 과거 MS-DOS 시절의 호환성을 위한 것일 뿐, 신경 쓰지 않아도 된다. 실제 반환되는 값을 확인해보면 ImageBase의 주소인 것을 확인할 수 있다.

How to get EAT Address From DOS Header

EAT 주소를 구하기 전 NT Header의 주소를 구해야 한다. NT Header 주소는 Dos Header를 이용해 구할 수 있다.

First, Get NT Header From DOS Header

Dos Header의 e_lfanew 멤버 변수와 ImageBase를 더하는 방법으로 NT Header 주소를 구할 수 있다.

IMAGE_DOS_HEADER *DOS = ImageBase;
printf("Image Dos Header : 0x%p\n", DOS);

IMAGE_NT_HEADERS *NT = ImageBase + DOS->e_lfanew;
printf("Image Nt Header : 0x%p\n", NT);​

위에서 구했던 ImageBase 주소를 DOS Header의 주소로 설정하고 참조 연산자를 이용해 e_lfanew에 접근하여 NT Header 주소를 구할 수 있다.

 

NT Header는 x86, x64 이렇게 나뉘는데 해당 포스팅에서는 x64를 기준으로 설명한다. 하지만 x86, x64 간의 큰 차이는 없다.

Second, Get EAT Address From NT Header

NT Header의 Optional Header에 있는 DataDirectory 0번째 인덱스(IMAGE_DIRECTORY_ENTRY_EXPORT를 사용하면 된다)에 EAT의 RVA가 기록되어 있다.

IMAGE_EXPORT_DIRECTORY *EXPORT = ImageBase + NT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
printf("Export Table : 0x%p\n", EXPORT);

printf("Number Of Functions : %d\n", EXPORT->NumberOfFunctions);
printf("Number Of Names : %d\n", EXPORT->NumberOfNames);

VA = ImageBase + RVA 공식이 성립하기 때문에 ImageBase에 RVA를 더하여 실제 EAT 주소를 구할 수 있다.

 

위 코드를 보면 IMAGE_DIRECTORY_ENTRY_EXPORT를 사용해 인덱스를 구하는데 헤더파일을 보면 DataDirectory 각각의 위치가 정의 되어 있다.

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8
#define IMAGE_DIRECTORY_ENTRY_TLS             9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11
#define IMAGE_DIRECTORY_ENTRY_IAT            12
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14

Print a Function Name and Address

함수 주소는 Ordinal을 이용해 구할 수 있다. Ordinal이란 AddressOfFunctions의 인덱스로 사용된다.

 

AddressOfNameOrdinals에서 현재 인덱스의 해당되는 함수명의 함수 주소의 Ordinal을 구할 수 있다.

for (int i = 0; i < EXPORT->NumberOfNames; i++)
{
    printf("%s : %p\n", ImageBase + *(DWORD *)(ImageBase + EXPORT->AddressOfNames + i * 4), ImageBase + *(DWORD *)(ImageBase + EXPORT->AddressOfFunctions + *(WORD *)(ImageBase + EXPORT->AddressOfNameOrdinals + i * 2) * 4));
}

Export 되는 함수들이 반드시 함수명을 포함해서 Export 되는 것은 아니다. 그렇기 때문에 예시에서는 NumberOfNames로 이름이 존재하는 함수들의 대한 정보만 출력했다.

 

필자도 알고 있다. 위 예시코드는 내가 봐도 복잡하고 이해하기 어렵다. 그렇기 때문에 보다 이해하기 쉽게 코드를 바꿔보면 아래와 처럼도 사용할 수 있다.

DWORD *AddressOfFunctions = ImageBase + EXPORT->AddressOfFunctions;
DWORD *AddressOfNames = ImageBase + EXPORT->AddressOfNames;
WORD *AddressOfNameOrdinals = ImageBase + EXPORT->AddressOfNameOrdinals;

for (int i = 0; i < EXPORT->NumberOfNames; i++)
{
    printf("%s : %p\n", ImageBase + AddressOfNames[i], ImageBase + AddressOfFunctions[AddressOfNameOrdinals[i]]);
}

위 코드가 훨씬도 가독성이 좋고 예쁘지만 필자는 쓸 때 없는 메모리 사용을 싫어하기 때문에 변수를 사용하지 않고 라인 하나로 처리하는 것을 좋아한다.(이거 쓰면서 생각났는데 그럼 register 변수를 선언하면 되는 거였다...)

Compile and Troubleshooting

왜 제목을 컴파일 그리고 트러블 슈팅이라고 했을까? 그렇다.. 위에 예시 코드 그대로 이어서 컴파일하면 무조건 런타임 에러가 발생할 것이다. 그래도 우선 컴파일하고 실행해보자 아래는 완성된 코드다.

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
    ULONGLONG ImageBase = LoadLibraryA("user32.dll");

    IMAGE_DOS_HEADER *DOS = ImageBase;
    printf("Image Dos Header : 0x%p\n", DOS);

    IMAGE_NT_HEADERS *NT = ImageBase + DOS->e_lfanew;
    printf("Image Nt Header : 0x%p\n", NT);

    IMAGE_EXPORT_DIRECTORY *EXPORT = ImageBase + NT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    printf("Export Table : 0x%p\n", EXPORT);

    printf("Number Of Functions : %d\n", EXPORT->NumberOfFunctions);
    printf("Number Of Names : %d\n", EXPORT->NumberOfNames);

    DWORD *AddressOfFunctions = ImageBase + EXPORT->AddressOfFunctions;
    DWORD *AddressOfNames = ImageBase + EXPORT->AddressOfNames;
    WORD *AddressOfNameOrdinals = ImageBase + EXPORT->AddressOfNameOrdinals;

    for (int i = 0; i < EXPORT->NumberOfNames; i++)
    {
        printf("%s : %p\n", ImageBase + AddressOfNames[i], ImageBase + AddressOfFunctions[AddressOfNameOrdinals[i]]);
    }
}

아마 아래와 같이 출력되고 프로그램이 강제 종료될 것이다.

Image Dos Header : 0x0000000000000000

출력된 결과를 보면 GetModuleHandleA의 반환 값이 NULL이라는 것을 알 수 있는데 왜 NULL을 반환하는 걸까?

Why dose GetModuleHandleA Return NULL?

GetModuleHandleA가 NULL을 반환하는 이유는 MSDN을 확인해보면 알 수 있다.

파라미터 항목에 분명하게 "로드된 모듈의 이름" 이라고 적혀있다. 런타임 에러가 발생하는 원인은 해당 프로세스에 user32.dll이 로드되어 있지 않기 때문이다.

Solution

해결방법은 간단하다. 로드되어 있지 않는 것이 문제라면 로드시키면 된다. 모듈을 로드를 하기 위해서는 다음 함수를 사용하면 된다.

HMODULE LoadLibraryA(LPCSTR lpLibFileName);
 

LoadLibraryA function (libloaderapi.h) - Win32 apps

Loads the specified module into the address space of the calling process. (LoadLibraryA)

docs.microsoft.com

LoadLibraryA는 GetModuleHandleA와 똑같이 해당 모듈의 ImageBase를 반환한다.

 

차이점은 GetModuleHandleA 모듈이 로드되어 있어야 하는 것이라면 LoadLibraryA는 모듈이 로드되어 있지 않다면 해당 모듈을 로드하고 ImageBase를 반환한다. 즉 모듈이 존재하지 않는 이상 NULL이 반환되지 않는다.

 

이제 코드를 수정해보자

Modified Code

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
    ULONGLONG ImageBase = GetModuleHandleA("user32.dll");
    
    if (ImageBase == NULL)
        ImageBase = LoadLibrary("user32.dll");

    IMAGE_DOS_HEADER *DOS = ImageBase;
    printf("Image Dos Header : 0x%p\n", DOS);

    IMAGE_NT_HEADERS *NT = ImageBase + DOS->e_lfanew;
    printf("Image Nt Header : 0x%p\n", NT);

    IMAGE_EXPORT_DIRECTORY *EXPORT = ImageBase + NT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    printf("Export Table : 0x%p\n", EXPORT);

    printf("Number Of Functions : %d\n", EXPORT->NumberOfFunctions);
    printf("Number Of Names : %d\n", EXPORT->NumberOfNames);

    DWORD *AddressOfFunctions = ImageBase + EXPORT->AddressOfFunctions;
    DWORD *AddressOfNames = ImageBase + EXPORT->AddressOfNames;
    WORD *AddressOfNameOrdinals = ImageBase + EXPORT->AddressOfNameOrdinals;

    for (int i = 0; i < EXPORT->NumberOfNames; i++)
    {
        printf("%s : %p\n", ImageBase + AddressOfNames[i], ImageBase + AddressOfFunctions[AddressOfNameOrdinals[i]]);
    }
}

수정된 코드를 다시 컴파일해 실행해보면 정상적으로 실행되는 것을 볼 수 있다.

Image Dos Header : 0x00007FFA16120000
Image Nt Header : 0x00007FFA161200F8
Export Table : 0x00007FFA161C1D50
Number Of Functions : 1215
Number Of Names : 1005
ActivateKeyboardLayout : 00007FFA1614C660
AddClipboardFormatListener : 00007FFA1614CE40
------------중략------------
wsprintfW : 00007FFA161499A0
wvsprintfA : 00007FFA161473F0
wvsprintfW : 00007FFA161499D0

다음 포스팅에서는 실제 EAT 후킹을 해볼 생각이다.

반응형
Comments