콘텐츠로 이동

리버싱 핵심원리 2부 1장 정리 2

Note

본 게시글의 내용은 리버싱 핵심원리를 보며 복습 겸 정리하였습니다.

책의 내용과 일부 상이할 수 있고, 이해를 돕기 위해 강좌 형식의 말투와 적절한(?) 예시를 추가하였습니다.

본 게시글에서 사용되는 소스 코드와 파일, 프로그램은 리버싱 핵심원리에서 제공하는 파일과 언급되는 것들을 기반으로 하며, 일부 상이할 수 있습니다.

IAT

IATImport Address Table는 PE 파일이 외부 동적 링크 라이브러리(DLL)에서 제공하는 함수들을 호출할 수 있도록 도와주는 테이블입니다. 실행 파일은 운영체제에서 제공하는 여러 함수를 호출하는데, 이 함수들이 주로 DLL에 포함되어 있습니다. IAT는 이러한 외부 함수의 호출을 처리할 수 있도록 도와줍니다.

여기서 잠깐! DLL이란?

DLLDynamic Link Library은 프로그램이 동적으로 링크할 수 있는 라이브러리를 말합니다. Windows 운영체제에선 .dll 확장자를 갖는 파일로 제공되고 실행 파일과 함께 사용됩니다. 프로그램의 기능을 여러 DLL로 분리하여 관리할 수 있고 유지보수가 매우 용이해진다는 장점이 있습니다.

16비트 DOS 시절에는 동적 링크와 같은 개념이 없었기 때문에 프로그램에서 외부 함수를 호출하려면 정적 링크(Static Linking)를 이용했습니다. 즉, 외부 함수나 라이브러리가 필요하면 실행 파일의 바이너리에 포함되는 방식이죠. 동일한 함수가 중복되기도 하고 실행 파일의 크기가 커지기 때문에 여러모로 단점이 많았습니다. 그래서 더 효율적으로 사용하고 유지보수를 용이하게 하기 위해 DLL과 같은 방식이 도입되었습니다.

DLL을 불러와 사용하는 방식은 크게 두 가지로 나뉩니다. 묵시적인 방법과 명시적인 방법입니다.

묵시적인 방식(Implicit Linking)은 실행 파일이 불러와질 때 프로그램에서 참조하는 모든 DLL을 자동으로 불러오는 것입니다. 이때 IAT를 통해 이루어집니다.

명시적인 방식(Explicit Linking)은 개발자가 직접 필요한 DLL을 불러와 사용하고 해제하는 방법입니다. 특정 DLL이 필요할 때 메모리에 적재하고 사용한 다음 필요가 없어지면 해제합니다.

IMAGE_IMPORT_DESCRIPTOR

winnt.h
/* Import name entry */
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    char    Name[1];
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

#include <pshpack8.h>
/* Import thunk */
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;
        ULONGLONG Function;
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;
    } u1;
} IMAGE_THUNK_DATA64,*PIMAGE_THUNK_DATA64;
#include <poppack.h>

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;
        DWORD Function;
        DWORD Ordinal;
        DWORD AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;

/* Import module directory */

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics; /* 0 for terminating null import descriptor  */
        DWORD   OriginalFirstThunk; /* RVA to original unbound IAT */
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;  /* 0 if not bound,
                 * -1 if bound, and real date\time stamp
                 *    in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
                 * (new BIND)
                 * otherwise date/time stamp of DLL bound to
                 * (Old BIND)
                 */
    DWORD   ForwarderChain; /* -1 if no forwarders */
    DWORD   Name;
    /* RVA to IAT (if bound this IAT has actual addresses) */
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

IMAGE_IMPORT_DESCRIPTOR 구조체는 PE 파일에서 어떤 외부 라이브러리(DLL)에서 어떤 함수를 가져오는 지에 대한 정보를 담고 있습니다.

PE 파일은 여러 개의 IMAGE_IMPORT_DESCRIPTOR 구조체를 가질 수 있고, 각 구조체는 하나의 DLL에 대한 가져오기 정보를 나타냅니다. 이 구조체 배열의 마지막은 모든 필드가 0 또는 NULL이 할당된 구조체로 끝납니다.

OriginalFirstThunk는 Import Name Table(INT)의 주소를 가리키는 RVA입니다. 이 테이블은 각각 가져온 함수에 대한 정보를 담고 있는 IMAGE_THUNK_DATA 구조체의 배열입니다. CharacteristicsOriginalFirstThunk이나 같은 거고 보통 OriginalFirstThunk로 사용하는 편입니다.

Name은 가져오는 DLL의 이름을 가리키는 RVA입니다. 이 RVA는 PE 파일에서 DLL 이름 문자열의 위치입니다.

FirstThunk는 Import Address Table(IAT)의 주소를 가리키는 RVA입니다.

실습

Windows 10 64Bit 환경을 기준으로 notepad.exe를 실습 대상으로 합니다. 책에선 자세한 설명으로 작성되어 있지 않아서 제가 대체해서 작성합니다.

IMAGE1-1
이미지 1-1

Pepper라는 프로그램을 이용해 C:\Windows\notepad.exe를 열어주세요. notepad.exe는 메모장 프로그램입니다. Pepper 프로그램으로 열어주시면 이미지 1-1처럼 64비트의 파일임을 확인할 수 있습니다.

IMAGE_IMPORT_DESCRIPTOR 구조체를 찾아보도록 합시다. 이 구조체를 찾기 위해선 OptionalHeader의 DataDirectory의 정보가 필요합니다.

IMAGE1-2
이미지 1-2

프로그램의 좌측 트리맵을 잘 보시면 Optional Header의 자식으로 Data Directories가 있습니다. 클릭해주세요.

IMAGE_IMPORT_DESCRIPTOR 구조체에 대한 실마리는 Import Table에서 확인할 수 있습니다. 프로그램에서 Import Directory에 해당하는 부분을 클릭해주세요.

winnt.h
1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

DataDirectory는 IMAGE_DATA_DIRECTORY 구조체 배열입니다. 그리고 이 구조체는 VirtualAddressSize 필드를 가집니다.

Import Directory의 VirtualAddressSize는 프로그램에서 확인되듯 0x2D0C80x244 값을 가집니다. VirtualAddress는 RVA입니다. VirtualAddress의 값을 RAW로 변환해보도록 합시다.

IMAGE1-3
이미지 1-3

RVA는 0x2D0C8로 이 RVA가 해당하는 섹션은 .rdata입니다. 위 프로그램에서 확인되는 값을 통해 RAW 값을 알아내면 아래와 같습니다.

0x24A00 + (0x2D0C8 - 0x26000) = 0x2BAC8

0x2BAC8이라는 값이 도출되는데요, 이 위치가 바로 우리가 찾는 IMAGE_IMPORT_DESCRIPTOR입니다. 헥스에디터를 통해 이동해봅시다.

IMAGE1-4
이미지 1-4

이미지 1-4에서 드래그된 부분이 IMAGE_IMPORT_DESCRIPTOR 구조체 배열의 첫 번째 부분입니다.

이미지 1-2를 통해 Size0x244로 확인됐기 때문에 IMAGE_IMPORT_DESCRIPTOR의 영역은 0x2BAC8에서 0x2BD0B까지입니다. 마지막 IMAGE_IMPORT_DESCRIPTOR는(0x2BCF8 ~ 0x2BD0B) 0x00으로 되어있는 걸 확인할 수 있습니다.

첫 번째 IMAGE_IMPORT_DESCRIPTOR의 각 필드 값은 아래 표와 같습니다:

File Offset Field Name Value(RVA) RAW
0x2BAC8 OriginalFirstThunk 0x2D3E0 0x24A00 + (0x2D3E0 - 0x26000) = 0x2BDE0
0x2BACC TimeDateStamp 0
0x2BAD0 ForwarderChain 0
0x2BAD4 Name 0x2E1F8 0x24A00 + (0x2E1F8 - 0x26000) = 0x2CBF8
0x2BAD8 FirstThunk 0x268B8 0x24A00 + (0x268B8 - 0x26000) = 0x252B8

Name은 어떤 외부 라이브러리에서 함수를 가져오는 지 라이브러리의 이름을 확인할 수 있습니다. RAW가 0x2CBF8로 산출되었기 때문에 헥스에디터로 해당 위치로 이동하면 kernel32.dll에서 가져온다는 걸 확인할 수 있습니다.

FirstThunk는 특수한 경우가 아닌 이상 PE 파일이 로딩되기 전까진 OriginalFirstThunk와 동일한 데이터를 가리킵니다. 이 필드는 IAT(Import Address Table)을 가리키는데 실행 시점에 함수 주소가 저장됩니다.

OriginalFirstThunk는 가져오는 함수의 정보가 담긴 구조체 포인터 배열입니다. 이 정보를 얻어야 프로세스 메모리에 로딩된 라이브러리에서 해당 함수의 시작 주소를 얻을 수 있죠. RAW를 구하면 0x2BDE0으로 나오는데 헥스에디터로 이동하면 IMAGE_THUNK_DATA 구조체를 만날 수 있습니다. notepad.exe는 64비트 프로그램이기 때문에 정확히는 _IMAGE_THUNK_DATA64 구조체입니다. 첫 번째 _IMAGE_THUNK_DATA64의 필드를 표로 나타내면 아래와 같습니다:

File Offset Field Name Value(RVA) RAW
0x2BDE0 Ordinal 0x2DE3C 0x24A00 + (0x2DE3C - 0x26000) = 0x2C83C

OrdinalIMAGE_IMPORT_BY_NAME의 위치를 가리킵니다. 0x2C83C가 산출되었으니 헥스 에디터로 확인해봅시다.

IMAGE1-5
이미지 1-5

위 영역을 표로 나타내면 아래와 같습니다:

File Offset Field Name Value
0x2BDE0 Hint 0x2B8
0x2C83E Name GetProcAddress

IMAGE_IMPORT_BY_NAMEName은 크기가 1인데 어떻게 긴 문자열 값을 가지냐구요? C 언어에서 char Name[1]로 선언된 건 가변 길이를 가진다는 걸 의미합니다. 실제로는 문자열 데이터가 그 뒤에 연속적으로 저장된다는 걸 의미합니다. 현대적인 방식으로 쓴다면 char Name[]이나 char* Name이겠죠?

NameGetProcAddress로 확인됩니다. 즉 첫 번째 IMAGE_IMPORT_DESCRIPTORkernel32.dllGetProcAddress 함수를 가져와 사용한다는 걸 알 수 있습니다.

EAT

EATExport Address Table는 DLL 파일이 외부로 제공하는 함수들의 주소를 관리하는 테이블입니다. EAT는 다른 프로그램이나 DLL이 해당 DLL의 함수의 주소를 찾을 때 사용되고, 실행 파일이 DLL을 불러오고 필요한 함수를 정확히 호출할 수 있도록 도와줍니다.

EAT는 PE 파일 중 실행 파일이 아닌 DLL 파일에 주로 작성됩니다. 실행 파일에선 일반적으로 사용되진 않습니다.

IMAGE_EXPORT_DIRECTORY

winnt.h
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD Characteristics;
    DWORD TimeDateStamp;
    WORD  MajorVersion;
    WORD  MinorVersion;
    DWORD Name;
    DWORD Base;
    DWORD NumberOfFunctions;
    DWORD NumberOfNames
    DWORD AddressOfFunctions;
    DWORD AddressOfNames;
    DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

IAT와 마찬가지로 EAT에 대한 정보는 PE 파일 내 IMAGE_EXPORT_DIRECTORY 구조체에 저장되어 있습니다. IAT와 다르게 IMAGE_EXPORT_DIRECTORY는 배열이 아닌 단일 구조체로서 하나만 존재합니다.

Name은 DLL의 이름을 가리키는 RVA입니다.

Base는 내보내진(Export) 함수의 Ordinal의 시작 값입니다. 1인 경우 첫 번째 함수의 Ordinal이 1이라는 걸 의미합니다.

NumberOfFunctions는 내보내진 함수의 총 개수를 나타냅니다. NumberOfNames는 이름으로 내보내진 함수의 개수를 나타냅니다.

AddressOfFunctions는 내보내진 함수의 주소 배열(EAT)을 가리키는 RVA입니다. AddressOfNames는 내보내진 함수의 이름 배열을 가리키는 RVA입니다. AddressOfNameOrdinals는 내보내진 함수의 Ordinal 배열을 가리키는 RVA입니다.

실습

IMAGE1-6
이미지 1-6

C:\Windows\System32\kernel32.dll을 실습 대상으로 하겠습니다. 이 DLL 파일의 EAT 정보를 찾아 CreateProcessW 함수의 주소를 찾아보도록 합시다.

Pepper 프로그램을 통해 kernel32.dll 파일을 열어주세요. System32 폴더에 들어있었으니 32비트 파일임을 알 수 있습니다.

IMAGE_EXPORT_DIRECTORY 구조체는 OptionalHeader의 DataDirectory의 정보가 필요합니다. DataDirectory[0]에 IMAGE_EXPORT_DIRECTORY 구조체의 시작 주소가 담겨있습니다.

0x9D2C0이라는 값이 담겨있는 걸 확인할 수 있습니다. 0x9D2C0은 .rdata 섹션 범위에 해당하는 걸 확인할 수 있고 RVA를 RAW로 변환하면 0x80600 + (0x9D2C0 - 0x82000) = 0x9B8C0이라는 값이 산출됩니다. 헥스에디터로 0x9B8C0 위치로 이동해봅시다.

IMAGE1-7
이미지 1-7
File Offset Field Name Value(RVA) RAW
0x9B8C0 Characteristics 0
0x9B8C4 TimeDateStamp 0x413E8087
0x9B8C8 MajorVersion 0
0x9B8CA MinorVersion 0
0x9B8CC Name 0xA12BC 0x80600 + (0xA12BC - 0x82000) = 0x9F8BC
0x9B8D0 Base 1
0x9B8D4 NumberOfFunctions 0x662
0x9B8D8 NumberOfNames 0x662
0x9B8DC AddressOfFunctions 0x9D2E8 0x80600 + (0x9D2E8 - 0x82000) = 0x9B8E8
0x9B8E0 AddressOfNames 0x9EC70 0x80600 + (0x9EC70 - 0x82000) = 0x9D270
0x9B8E4 AddressOfNameOrdinals 0xA05F8 0x80600 + (0xA05F8 - 0x82000) = 0x9EBF8

IMAGE_EXPORT_DIRECTORY 구조체의 정보를 표로 확인해보면 위와 같습니다.

C++에서 GetProcAddress 함수를 통해 라이브러리의 함수 주소를 얻어낼 수 있습니다. 이 함수가 EAT의 정보를 참조해서 원하는 함수의 주소를 얻어오죠. 이 함수는 대략 아래와 같은 과정을 통해 주소를 얻어냅니다.

  1. AddressOfNames 멤버를 이용해 원하는 함수의 이름을 찾는다. 이때 문자열 비교를 통해 찾게 되고 인덱스(Index)가 반환된다.
  2. AddressOfNameOrdinals 멤버를 이용해 인덱스(Index)에 해당하는 값을 찾는다.
  3. 2번에서 얻은 값을 인덱스로 하여 AddressOfFunctions 멤버에서 함수의 주소를 찾는다.

자, CreateProcessW의 함수 주소를 찾으러 떠나볼까요?

IMAGE1-8
이미지 1-8

우리가 찾는 CreateProcessW 함수의 이름은 배열에서 233번째에 있습니다. 어떻게 아나구요? Pepper로 확인이 됩니다. 처음에 일일이 뒤지다가 이건 아닌 거 같아서 Pepper로 봤습니다.

헥스에디터로 AddressOfNames의 위치로 이동한 후 233번째 위치로 가면 0xA2AEA라는 값이 나타납니다. 이를 RAW로 변환하면 0x80600 + (0xA2AEA - 0x82000) = 0xA10EA입니다. 헥스에디터로 이동해봅시다.

IMAGE1-9
이미지 1-9

CreateProcessW 문자열이 작성된 걸 확인할 수 있습니다.

이제 Ordinal 값을 구한 후 AddressOfFunctions에 인덱스로 사용하면 함수의 주소를 찾을 수 있습니다. 인덱스가 233인 걸 알았으니 AddressOfNameOrdinals에 인덱스로 사용하여 Ordinal 값을 찾아봅시다.

헥스에디터로 0x9EBF8로 이동하신 후 233번째에 해당하는 위치로 이동합니다. 참고로 AddressOfNameOrdinals 배열의 원소의 크기는 2Bytes입니다.

Ordinal 값은 0xE8로 나타납니다. 이를 AddressOfFunctions에 인덱스로 사용하면 함수의 주소를 알아낼 수 있습니다.

IMAGE1-10
이미지 1-10

함수의 주소(RVA)는 0x1CEA0로 나타납니다.

책에선 kernel32.dll의 ImageBase가 0x7C7D0000으로 나타난있지만 제 환경에선 ASLR 기법이 적용되었는 지 PE에 기재된 ImageBase와는 다르게 나타나 디버거에서 실제 함수를 찾는 과정은 생략합니다.