## Mở đầu
Xin chào các hacker, là mình Syaoren đây! Bài viết hôm nay sẽ không tập trung vào bất kỳ kỹ thuật nào cả, thay vào đó mình muốn giới thiệu về hai khái niệm quan trọng là **Import Address Table (IAT)** và **Export Address Table (EAT)**. Lý do cho việc thực hiện bài viết này là vì **IAT** và **EAT** là những khái niệm quan trọng cần phải nắm bắt để hiểu rõ các kỹ thuật nâng cao mà mình sẽ giới thiệu trong các bài viết sau. Chính vì vậy, mình quyết định dành riêng một bài viết để giới thiệu về **IAT** và **EAT**. Bạn đọc cần phải có kiến thức về Portable Executable (PE) format trước khi đọc bài viết này, mình đã viết một bài về PE format [tại đây](https://hackmd.io/@LeNhutHoang2112/r1Pzr-M96), bạn có thể tham khảo nó trước khi bắt đầu. Không vòng vo nữa, vào vấn đề chính thôi! :smiling_imp:
<div style="width:100%;height:0;padding-bottom:55%;position:relative;"><iframe src="https://giphy.com/embed/jkSvCVEXWlOla" width="100%" height="100%" style="position:absolute" frameBorder="0" allowFullScreen></iframe></div>
## Import Address Table (IAT)
Khi nhìn vào hình dưới đây, ta có thể dễ dàng nhận thấy rằng **IAT** được sử dụng để làm gì rồi đúng không? Yah, nó được sử dụng để lưu trữ địa chỉ của các **API** mà chương trình gọi trong lúc thực thi. API trong Windows thật sự đóng vai trò rất quan trọng, nhờ có nó mà chương trình mới có thể truy cập vào các tài nguyên của hệ thống một cách dễ dàng. Vì lý do đó, địa chỉ của API được sắp xếp trong một bảng có cấu trúc là IAT, điều này giúp quản lý thuận tiện hơn so với việc lưu trữ các địa chỉ này một cách rời rạc. :dog:
Ngoài ra, như ta đã biết, các API mà chương trình sử dụng có thể được định nghĩa trong nhiều DLL khác nhau. Cho nên, để quản lý các địa chỉ một cách hiệu quả, chương trình sẽ tạo một IAT riêng cho mỗi DLL mà nó import vào. Ví dụ, chương trình của ta sẽ có một IAT dành riêng cho **kernel32.dll** và một IAT riêng cho **ntdll.dll**.

Một điều quan trọng cần nhớ là mỗi lần máy tính khởi động lại, địa chỉ của các DLL trong bộ nhớ sẽ thay đổi và dẫn đến việc địa chỉ của các API cũng thay đổi theo. Do đó, các **địa chỉ API trong IAT sẽ không cố định**. Vì vậy, nên windows loader cần phải có cơ chế để phân giải địa chỉ IAT cho chương trình mỗi lần mà nó thực thi. Vậy thì quá trình phân giải địa chỉ cho các entry của IAT được windows loader thực hiện như thế nào? :thinking_face:
Để giải thích rõ cơ chế này thì khái niệm ta cần phải đề cập đầu tiên là **import directory**, đây là một bảng gồm nhiều entry và entry cuối cùng trong bảng không được sử dụng. Mỗi entry trong import directory tương ứng với một DLL được nạp lên bởi process tại thời điểm chạy và thông tin của DLL này được biểu diễn bởi cấu trúc **IMAGE_IMPORT_DESCRIPTOR**.
Trong cấu trúc **IMAGE_IMPORT_DESCRIPTOR**, ta cần quan tâm đến ba trường là **OriginalFirstThunk**, **FirstThunk**, và **Name**. OriginalFirstThunk, FirstThunk, và Name lần lượt lưu trữ RVA của **import name table (INT)**, **import address table (IAT)**, và **tên của DLL tương ứng**. Dưới đây là mô tả chi tiết về **IMAGE_IMPORT_DESCRIPTOR**.
```cpp=
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
```
INT và IAT đều được biểu diễn bằng cấu trúc **IMAGE_THUNK_DATA**, nhìn vào cấu trúc IMAGE_THUNK_DATA bên dưới, ta có thể thấy nó được biểu diễn bởi một union gồm bốn thành phần là **ForwarderString**, **Function**, **Ordinal**, và **AddressOfData**. Cần lưu ý một điểm là **INT và IAT của chương trình khi chưa được thực thi là như nhau**. Chỉ khi windows loader nạp chương trình lên bộ nhớ để tạo thành một process thì lúc này thông tin của từng entry trong INT mới được sử dụng để phân giải địa chỉ cho từng entry tương ứng trong IAT. Khi quá trình này hoàn thành, IAT sẽ thật sự lưu trữ địa chỉ của các API được import. Vậy windows loader sẽ sử dụng INT để phân giải địa chỉ cho IAT như thế nào? :thinking_face:
Quá trình phân giải địa chỉ cho IAT có thể được thực hiện theo ba cách. Cách đầu tiên là sử dụng tên API, cách thứ hai là sử dụng giá trị ordinal, và cách thứ ba là sử dụng giá trị trường ForwarderString. Nếu Windows loader sử dụng giá trị ordinal để phân giải địa chỉ của API tương ứng, trường **Ordinal** sẽ được dùng. Còn nếu windows loader phân giải địa chỉ IAT bằng tên API, trường **AddressOfData** sẽ được dùng để trỏ đến một đối tượng có cấu trúc là **IMAGE_IMPORT_BY_NAME**. Cách cuối cùng là sử dụng trường **ForwarderString** khi API cần phân giải địa chỉ nằm trong một DLL khác so với DLL đang xét, trong đó giá trị của ForwarderString sẽ trỏ đến tên một API được chuyển tiếp từ một DLL khác.
```cpp=
typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;
```
Cấu trúc **IMAGE_IMPORT_BY_NAME** được biểu diễn bởi hai trường chính là **Hint** và **Name**. Windows loader có thể sử dụng trường Hint hoặc trường Name để phân giải địa chỉ cho IAT. Đối với trường Hint, windows loader sử dụng giá trị của trường này để làm index vào **export name array** trong **export directory**. Còn đối với trường Name, windows loader thực hiện phân giải địa chỉ bằng tên API tương ứng. Dễ thấy, **cách phân giải IAT bằng trường Name tốn nhiều thời gian hơn cách phân giải IAT bằng trường Hint** là vì cách sử dụng trường Hint không cần thiết phải thực hiện tìm kiếm nhị phân index trong export name array như cách sử dụng trường Name mà thay vào đó nó xem giá trị trường Hint như là index cần tìm. Sau khi phân giải địa chỉ cho các entry trong bảng IAT, giá trị địa chỉ API tương ứng sẽ được lưu trữ trong trường Function của cấu trúc **IMAGE_THUNK_DATA**.
Dưới đây là cấu trúc chi tiết của **IMAGE_IMPORT_BY_NAME**.
```cpp=
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
```
Khi tìm hiểu đến đây, cá nhân mình đã từng đặt ra một câu hỏi là vì sao INT lại cần thiết? **Rõ ràng INT và IAT giống hệt nhau trong chương trình khi chưa được thực thi**. Vậy tại sao không sử dụng trực tiếp các giá trị entry trong IAT để phân giải địa chỉ cho chính các entry đó mà phải thông qua INT? Nếu làm như thế này thì chẳng phải việc tồn tại INT trong chương trình là dư thừa hay sao? :thinking_face:
Qua quá trình tìm hiểu từ nhiều nguồn, mình đã hiểu lý do vì sao INT tồn tại trong chương trình. Việc phân giải IAT được thực hiện trong quá trình chạy chương trình (run-time) được gọi là **unbound import**, nghĩa là IAT được phân giải trong khi chương trình đang được nạp lên bộ nhớ. Trong trường hợp này, INT dường như không có tác dụng gì. Tuy nhiên, đây chưa phải là toàn bộ câu chuyện, vì còn một phương pháp khác để phân giải IAT, được gọi là **bound import**. Vậy bound import là gì? :thinking_face:
Cơ chế bound import sẽ phân giải IAT tại **linking-time** thay vì run-time như unbound import. Tức là lúc chương trình được biên dịch (compile) thì linker sẽ thực hiện quá trình phân giải IAT luôn mà không cần phải đợi đến lúc chương trình được thực thi. Lợi ích rõ ràng của việc này là quá trình nạp chương trình lên để thực thi sẽ nhanh hơn so với unbound import bởi vì IAT của chương trình lúc này đã được phân giải sẵn.
Nhưng cái gì cũng có cái giá của nó, nếu sử dụng cơ chế bound import thì thời gian tải chương trình sẽ nhanh hơn. Tuy nhiên, một vấn đề phát sinh của việc này là các DLL chứa API mà chương trình sử dụng có thể được chỉnh sửa hoặc cập nhật, việc này dẫn đến khả năng địa chỉ trong IAT sẽ đúng tại thời điểm chương trình được biên dịch nhưng có thể trở nên sai lệch trong tương lai và đây chính là lúc mà INT thể hiện sự cần thiết của nó. Khi địa chỉ trong IAT không còn chính xác, **Windows loader sẽ sử dụng các entry trong bảng INT để phân giải lại địa chỉ không hợp lệ và cập nhật lại IAT**. Có thể thấy, INT đóng vai trò rất quan trọng trong việc triển khai cơ chế bound import. Vì bound import không được sử dụng trong ngữ cảnh của malware, nên mình sẽ không đi sâu vào chi tiết cơ chế này. :alien:
Dưới đây là chi tiết các trường trong cấu trúc **IMAGE_BOUND_IMPORT_DESCRIPTOR**, cấu trúc này được sử dụng trong quá trình phân giải IAT tại linkting-time.
```cpp=
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
```
Đến đây, ta có thể tổng kết lại các bước cụ thể được sử dụng để phân giải IAT theo cơ chế unbound import dựa vào các khái niệm đã nêu trên:
1. Kiểm tra tên DLL đang duyệt có phải là DLL cần tìm kiếm hay không ? Nếu đúng thì chuyển sang bước 2.
2. Thực hiện API **LoadLibrary()** để tải DLL nếu như DLL chưa xuất hiện trong bộ nhớ hiện tại của tiến trình.
3. Truy xuất **INT** bằng RVA được lưu trong trường **OriginalFirstThunk** của cấu trúc **IMAGE_IMPORT_DESCRIPTOR**.
4. Duyệt qua từng entry trong INT để thực hiện phân giải IAT. Ở bước này, có thể sử dụng giá trị ordinal hoặc **Hint/Name** table để phân giải IAT.
5. Nếu sử dụng Hint/Name table để phân giải IAT thì tại bước này API **GetProcAddress()** sẽ được gọi để lấy địa chỉ của API tương ứng.
6. Truy xuất IAT bằng RVA được lưu trong trường **FirstThunk** của **IMAGE_IMPORT_DESCRIPTOR**.
7. Gán địa chỉ thu được ở **bước 5** vào entry tương ứng trong IAT.
Hình dưới đây sẽ mô tả tổng quan các bước mà mình vừa đề cập trên **kernel32.dll**.

Lý thuyết như vậy là đủ rồi, bây giờ là lúc để thực hành. Dưới đây là đoạn code mà mình đã viết để tải **wsock32.dll** vào bộ nhớ và liệt kê các DLL và API cùng với địa chỉ tương ứng được sử dụng bởi **wsock32.dll**. Quy trình này giống với các bước mình vừa đề cập trước đó. :smiling_face_with_smiling_eyes_and_hand_covering_mouth:
```cpp=
#include<stdio.h>
#include<Windows.h>
#include<winnt.h>
#define DLLName "wsock32.dll"
int main(int argc, const char* argv[]) {
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(argv);
// Get kernel32 module
UINT_PTR hWsock32 = (UINT_PTR)LoadLibraryA(DLLName);
if (!hWsock32) {
printf("[-] Failed to load wsock32.dll! Erorr 0x%x\n", GetLastError());
return 1;
}
// Get NT header
PIMAGE_NT_HEADERS ntHeader = hWsock32 + ((PIMAGE_DOS_HEADER)(hWsock32))->e_lfanew;
// Get import descriptor
PIMAGE_IMPORT_DESCRIPTOR impDes = hWsock32 + (&ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT])->VirtualAddress;
// Loop through import directory
while (impDes->Name) {
// Get import name table (INT)
PIMAGE_THUNK_DATA impNameTable = hWsock32 + impDes->OriginalFirstThunk;
// Get import address table (IAT)
PIMAGE_THUNK_DATA impAddressTable = hWsock32 + impDes->FirstThunk;
// Get current import DLL
LPCSTR impDLLName = hWsock32 + impDes->Name;
HMODULE hImpDLL = GetModuleHandleA(impDLLName);
printf("\n[*] %s (0x%p)\n", impDLLName, hImpDLL);
// Loop through import name table
while (impNameTable->u1.AddressOfData) {
// Resolve address via ordinal
if (IMAGE_SNAP_BY_ORDINAL(impNameTable->u1.Ordinal)) {
WORD APIOrdinal = IMAGE_ORDINAL(impNameTable->u1.Ordinal);
printf("\t[+] Ordinal: 0x%hx (0x%p)\n", APIOrdinal, (LPVOID)(impAddressTable->u1.Function));
}
// Resolve address via name
else {
LPCSTR APIName = &((PIMAGE_IMPORT_BY_NAME)(hWsock32 + impNameTable->u1.AddressOfData))->Name;
printf("\t[+] API: %s (0x%p)\n", APIName, (LPVOID)impAddressTable->u1.Function);
}
++impNameTable;
++impAddressTable;
}
++impDes;
}
return 0;
}
```
Khi chạy đoạn code trên, nó sẽ liệt kê tất cả các DLL mà **wsock32.dll** sử dụng cũng như các API thuộc về các DLL này mà wsock32.dll import. Danh sách này bao gồm cả địa chỉ tương ứng của từng DLL và API. Dựa vào hình minh họa, ta có thể thấy rằng wsock32.dll đã import 4 API từ **WS2_32.dll**, trong đó có 2 API được import dựa trên giá trị ordinal.

Để xác minh tính chính xác của chương trình, ta có thể sử dụng công cụ **CFF Explorer** để kiểm tra **wsock32.dll**. Kết quả kiểm tra cho thấy thông tin do chương trình cung cấp hoàn toàn khớp với dữ liệu được cung cấp bởi công cụ trong hình minh họa bên dưới.

## Export Address Table (EAT)
Nếu như IAT được dùng để lưu trữ địa chỉ của các API mà chương trình import trong quá trình thực thi, thì **EAT (Export Address Table)** lại được dùng để lưu trữ địa chỉ của các API mà DLL export ra bên ngoài. Các DLL trong Windows được tạo ra với mục đích để cung cấp các API cho các chương trình khác sử dụng, do đó chúng phải cung cấp thông tin về RVA cho các API mà chúng export ra ngoài. Nếu DLL không cung cấp thông tin này, các chương trình sẽ không biết được địa chỉ cụ thể của những API và sẽ không thể sử dụng chúng. :grimacing:

Nhắc lại về bước thứ 5 trong quá trình phân giải địa chỉ cho IAT ở phần trước thì ta có đề cập đến việc là nếu sử dụng **Hint/Name** table để phân giải IAT thì tại bước này API **GetProcAddress()** sẽ được gọi để lấy địa chỉ của API tương ứng. Vậy thì một câu hỏi đặt ra là **GetProcAddress()** đã thật sự tính toán địa chỉ API như thế nào? Liệu rằng nó có liên quan gì đến EAT hay không? :thinking_face:
Câu trả lời là có, thậm chí là liên quan một cách chặt chẽ nữa là đằng khác. Về bản chất, API **GetProcAddress()** lấy địa chỉ của API bất kỳ bằng cách truy cập vào EAT trong DLL định nghĩa API này. Vậy thì câu hỏi đặt ra ở đây là làm thế ta có thể truy cập vào EAT? :thinking_face:
Trước hết, ta phải nói về khái niệm export directory. Nó được biểu diễn bằng cấu trúc **IMAGE_EXPORT_DIRECTORY**, cấu trúc này lưu trữ các trường thông tin quan trọng của các hàm và biến trong PE file được export ra bên ngoài. Các hàm và biến được export từ PE file này có thể được truy xuất và sử dụng bởi các PE file khác. Ví dụ: trong **kernel32.dll** định nghĩa rất nhiều export function được sử dụng chung bởi nhiều file thực thi khác trong hệ thống. Dưới đây là ý nghĩa của các trường trong cấu trúc **IMAGE_EXPORT_DIRECTORY**:
* **TimeDateStamp:** trường này mô tả thời gian gần nhất mà module được cập nhật hoặc sửa đổi.
* **Name:** chứa địa chỉ trỏ đến tên của module.
* **Base:** chứa giá trị là một số nguyên và giá trị này được sử dụng trong quá trình tính toán RVA của export function bằng ordinal.
* **NumberOfFunctions:** chứa số lượng export function của module.
* **NumberOfNames:** chứa số lượng các export function có tên cụ thể. Giá trị này luôn bé hơn hoặc bằng **NumberOfFunctions**.
* **AddressOfFunctions:** chứa địa chỉ của EAT, là bảng chứa các RVA của export function. Số lượng phần tử trong bảng này luôn bằng giá trị của **NumberOfFunctions**.
* **AddressOfNames:** chứa địa chỉ của mảng lưu trữ RVA của các tên hàm. Phải nhớ là số lượng phần tử trong mảng này luôn bằng giá trị của **NumberOfNames**.
* **AddressOfNamesOrdinals:** là mảng chứa index của EAT.
Hình dưới mô tả chi tiết các trường trong cấu trúc **IMAGE_EXPORT_DIRECTORY**.
```cpp=
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
```
Sau khi nêu xong ý nghĩa của các trường có trong export directory thì tiếp theo ta cần đi làm rõ cơ chế để trích xuất RVA của API trong EAT. Dưới đây là các bước cụ thể để thực hiện việc này:
1. Duyệt qua từng phần tử của export name array để trích xuất các RVA. Giá trị RVA này trỏ đến tên của một API chứa trong **function name table**.
2. Duyệt qua từng tên API để tìm kiếm API mà ta mong muốn lấy địa chỉ và đồng thời trích xuất index của function name table ở nơi mà API này được tìm thấy.
3. Tìm kiếm địa chỉ của ordinal array bằng trường **AddressOfNamesOrdinals**.
4. Trích xuất giá trị một phần tử trong ordinal array bằng giá trị index được tìm thấy ở **bước 2**.
5. Tìm kiếm địa chỉ của EAT bằng trường **AddressOfFunctions**.
6. Sử dụng giá trị ordinal ở **bước 4** để làm index trong EAT, giá trị của phần tử tại vị trí index này chính là RVA của API đang tìm kiếm.

Như ta đã đề cập trong phần IAT, địa chỉ API có thể được phân giải bằng tên của API đó hoặc giá trị ordinal đại diện cho API đó. Thì tất cả 6 bước vừa được liệt kê ở trên được sử dụng để phân giải địa chỉ API bằng tên của API đó. Vậy còn nếu API được phân giải địa chỉ bằng giá trị ordinal thì quá trình này được diễn ra như thế nào? :thinking_face:
Nếu giá trị ordinal được sử dụng làm tham số trong API **GetProcAddress()**, trước tiên nó sẽ được trừ đi giá trị trường **Base** trong export directory, và kết quả này sau đó được sử dụng làm index để truy cập vào ordinal array như ở bước 4. Nói một cách đơn giản hơn, nếu ta sử dụng ordinal để phân giải địa chỉ API, **ta chỉ cần thực hiện từ bước 3 đến bước 6 của quy trình đã nêu trên**. :smiling_imp:
Việc chỉ giải thích bằng lời thôi thì có thể khó hiểu, vì vậy để hiểu rõ hơn về các phương pháp phân giải địa chỉ API, ta có thể xem xét đoạn code dưới đây. Nó được sử dụng để mô phỏng cách thức hoạt động của API **GetProcAddress()**. Đoạn code này có thể thực hiện phân giải địa chỉ cho API bằng cả ba phương pháp đã được đề cập trong phần Import Address Table (IAT). Bạn có thể tham khảo thêm bài viết của tác giả đoạn code này [tại đây](http://www.rohitab.com/discuss/topic/38615-getprocaddress-replacement/). :alien:
```cpp=
#include <windows.h>
#include <stdlib.h>
void *get_proc_address(HMODULE module, const char *proc_name)
{
char *modb = (char *)module;
IMAGE_DOS_HEADER *dos_header = (IMAGE_DOS_HEADER *)modb;
IMAGE_NT_HEADERS *nt_headers = (IMAGE_NT_HEADERS *)(modb + dos_header->e_lfanew);
IMAGE_OPTIONAL_HEADER *opt_header = &nt_headers->OptionalHeader;
IMAGE_DATA_DIRECTORY *exp_entry = (IMAGE_DATA_DIRECTORY *)
(&opt_header->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
IMAGE_EXPORT_DIRECTORY *exp_dir = (IMAGE_EXPORT_DIRECTORY *)(modb + exp_entry->VirtualAddress);
void **func_table = (void **)(modb + exp_dir->AddressOfFunctions);
WORD *ord_table = (WORD *)(modb + exp_dir->AddressOfNameOrdinals);
char **name_table = (char **)(modb + exp_dir->AddressOfNames);
void *address = NULL;
DWORD i;
/* is ordinal? */
if (((DWORD)proc_name >> 16) == 0) {
WORD ordinal = LOWORD(proc_name);
DWORD ord_base = exp_dir->Base;
/* is valid ordinal? */
if (ordinal < ord_base || ordinal > ord_base + exp_dir->NumberOfFunctions)
return NULL;
/* taking ordinal base into consideration */
address = (void *)(modb + (DWORD)func_table[ordinal - ord_base]);
} else {
/* import by name */
for (i = 0; i < exp_dir->NumberOfNames; i++) {
/* name table pointers are rvas */
if (strcmp(proc_name, modb + (DWORD)name_table[i]) == 0)
address = (void *)(modb + (DWORD)func_table[ord_table[i]]);
}
}
/* is forwarded? */
if ((char *)address >= (char *)exp_dir &&
(char *)address < (char *)exp_dir + exp_entry->Size) {
char *dll_name, *func_name;
HMODULE frwd_module;
dll_name = strdup((char *)address);
if (!dll_name)
return NULL;
address = NULL;
func_name = strchr(dll_name, '.');
*func_name++ = 0;
/* is already loaded? */
frwd_module = GetModuleHandle(dll_name);
if (!frwd_module)
frwd_module = LoadLibrary(dll_name);
if (frwd_module)
address = get_proc_address(frwd_module, func_name);
free(dll_name);
}
return address;
}
```
Để chứng minh rằng mình đã hiểu cách hoạt động của EAT, mình đã viết một đoạn code nhỏ dưới đây để liệt kê các API được xuất ra bởi **kernel32.dll** theo tên của API đó, dựa trên 6 bước đã đề cập. Tuy nhiên, mình vẫn chưa biết cách liệt kê các API theo giá trị ordinal. Nếu ai biết cách thực hiện việc này, có thể chia sẻ với mình nhé. :yum:
```cpp=
#include<stdio.h>
#include<Windows.h>
#define DLLName "kernel32"
#define DEREF_32( name )*(DWORD *)(name)
#define DEREF_16( name )*(WORD *)(name)
int main(int argc, const char* argv[]) {
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(argv);
// Get kernel32 module
UINT_PTR hKernel32 = (UINT_PTR)GetModuleHandleA(DLLName);
if (!hKernel32) {
printf("[-] Failed to get kernel32.dll module! Error 0x%x\n", GetLastError());
return 1;
}
// Get NT header
PIMAGE_NT_HEADERS ntHeader = hKernel32 + ((PIMAGE_DOS_HEADER)hKernel32)->e_lfanew;
PIMAGE_DATA_DIRECTORY expDataDir = &ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
// Get export directory
PIMAGE_EXPORT_DIRECTORY expDir = hKernel32 + expDataDir->VirtualAddress;
// Get export name array
UINT_PTR expNameArray = hKernel32 + expDir->AddressOfNames;
// Get export address table
UINT_PTR expAddressTable = hKernel32 + expDir->AddressOfNames;
// Get export ordinal array
UINT_PTR expOrdinal = hKernel32 + expDir->AddressOfNameOrdinals;
// Number of named APIs
UINT_PTR expNumName = expDir->NumberOfNames;
// Loop through export name array
for (UINT_PTR i = 0; i < expNumName; i++) {
// Get API name
LPCSTR APIName = hKernel32 + DEREF_32(expNameArray);
// Get index in export address table
UINT_PTR expAddressIndex = expAddressTable + DEREF_16(expOrdinal) * sizeof(DWORD);
// Get API address
UINT_PTR APIAddress = hKernel32 + DEREF_32(expAddressIndex);
printf("%llu %s (0x%p)\n", i, APIName, (LPVOID)APIAddress);
expNameArray += sizeof(DWORD);
expOrdinal += sizeof(WORD);
}
return 0;
}
```
Khi ta thực thi đoạn mã trên, nó sẽ liệt kê tất cả các API được export theo tên bởi **kernel32.dll**. Hình dưới đây minh họa một phần kết quả chi tiết của quá trình này.

## Kết luận
Trong bài viết này, mình đã làm rõ về cơ chế hoạt động của IAT và EAT. Đây là hai khái niệm cần phải hiểu rõ trước khi thực hiện các kỹ thuật nâng cao hơn như là **Reflective DLL Injection**, **API Hashing**, hay **Shellcode Reflective DLL Injection**. Mình hi vọng bài viết này có thể hỗ trợ được cho những ai đang nghiên cứu về lĩnh vực bảo mật, đặc biệt là trong ngữ cảnh về malware. :yum:
:::warning
:zap: Lưu ý rằng, bài viết chỉ mang tính chất giáo dục và không khuyến khích việc sử dụng thông tin để thực hiện các hoạt động xấu hay bất hợp pháp. Nếu có thắc mắc hay ý kiến, đừng ngần ngại chia sẻ với mình để làm cho bài viết trở nên tốt hơn.
:::
## Tham khảo
1. https://0xrick.github.io/win-internals/pe6/
2. https://www.ired.team/offensive-security/code-injection-process-injection/import-adress-table-iat-hooking
3. http://www.rohitab.com/discuss/topic/38615-getprocaddress-replacement/