Joe1sn's Cabinet

【免杀】PE文件分析

最近代码能力飞速提升,顺便复习一下

一般就是

  • DOS头:IMAGE_DOS_HEADER结构体
  • PE头:IMAGE_NT_HEADERS结构体
  • Section头:IMAGE_SECTION_HEADER结构体

OK,如果要分析PE文件的话,首先把文件读取出来

那么如何解析呢?这就是C/C++非常方便的一点:直接使用结构体转换,例如把地址值解析为数值
总体就是解释内存中的值

DOS 头

先看DOSHeader吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

需要的是e_lfanew

NT头

文件起始地址+e_lfanew = NT头位置

得到NT头位置后

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

重要的是IMAGE_OPTIONAL_HEADER,其实都一样,可以从FileHeader得到32位还是64位,

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
  • 0x8664:adm64位
  • 0x014C:intel32位

接下来以64位举例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

到这里我写了部分代码,结合IDA就很好理解了

image-20240225161644926

如果还要继续看的话就是DataDirectory

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

打印虚拟地址

img

其实就是导入导出表之类的东西

image-20240225163401598

Nt->ImageBase+这里的偏移,就是运行时的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Directory Entries

#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

节表相关

再回到NT头中的FileHeader,提取得到节数量

DOS头和NT头的大小加起来后面就是第一个Section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BOOL FilePE::bGetSectionList() {
if (this->FileContent.length() == 0) {
Error("File content is zero", DEBUG);
return FALSE;
}
if (this->pHeaderNT == NULL) {
Error("NT head not initialize", DEBUG);
return FALSE;
}
DWORD SectionNum = this->pHeaderNT->FileHeader.NumberOfSections;
this->SecNum = SectionNum;
this->SectionList = (PIMAGE_SECTION_HEADER*)malloc((SectionNum + 1) * sizeof(PIMAGE_SECTION_HEADER));
if (this->SectionList == NULL) {
Error("Allcate section list memory failed\n", DEBUG);
return FALSE;
}
for (size_t i = 0; i < SectionNum; i++)
{
SectionList[i] = PIMAGE_SECTION_HEADER(
&this->FileContent[0] + this->pHeaderDOS->e_lfanew
+ sizeof(IMAGE_NT_HEADERS) + i * sizeof(IMAGE_SECTION_HEADER));
}
return TRUE;
}

IAT表

I. 如何找到文件的IAT表

IAT叫Import Address Table,当我们的程序需要外部库的函数时,就会从这里开始调用

IAT表的地址并不在任何头中,不过我们之前分析Directory的时候就发现会有一个IMAGE_DIRECTORY_ENTRY_IAT但它不是导入表,真正的IAT描述:

1
pHeaderNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]

按道理来说VirtualAddress+ImageBase就是运行起来的地址,这里我没有加上

突然想起来我们在分析节表的时候也存在类似的 基地址+偏移

对于这个结构体之前的只能用这块儿的目录

image-20240225200226287

对应文件中地址为28fc-2000+1400 = 1cfc

image-20240225201254439

有点意思,查阅资料发现对应的结构体:_IMAGE_IMPORT_DESCRIPTOR,MSDN中并没有对应说明

1
2
3
4
5
6
7
8
9
10
11
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;
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;

有个问题就是这块地址是运行时写入IAT相关信息,我们的静态分析到这一步就算断了

这里我设计了一个BOOL bGetIAT();函数,直接去读程序的内存,为了简单,就先关闭ASLR

image-20240225191521687

image-20240225205332584

已经成功读取一个信息,接下来办法全部找出来,自然想到的就是Name为0就退出,但是由于总数不知道,所以需要一个动态容器,我用的vector

1
vector<PIMAGE_IMPORT_DESCRIPTOR> pIATList;

按照一样的方法全部得到后,再次尝试在运行中读取

image-20240225211740589

但是很多PE分析工具静态也能看得到,利用的是RVA->RAW的转换

2d52-2000+1400=2152

image-20240225212317511

1
2
3
4
5
6
7
8
9
10
11
12
void FilePE::PrintIATStatic() {
if (this->pIATList.size() == 0) {
Error("IAT List empty", DEBUG);
return;
}
DWORD VA = 0;
for (size_t i = 0; i < this->pIATList.size(); i++)
{
VA = this->dwVAToRaw(this->pIATList[i]->Name);
cout << "DLL : " << &this->FileContent[0] + VA << endl;
}
}

image-20240225213837289

II. 找到导入函数

通过OriginalFirstThunk,基本就是OriginalFirstThunk进行转化找到RAW,RAW地方的地址再转为RAW就找到了,最终的位置是一个_IMAGE_IMPORT_BY_NAME结构体

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

image-20240225215424856

1
2
3
4
5
6
7
8
9
void FilePE::PrintFunction(PIMAGE_IMPORT_DESCRIPTOR pIAT) {
DWORD64 VA = *PDWORD64(&this->FileContent[0] + this->dwVAToRaw(pIAT->OriginalFirstThunk));
for (size_t i = 1; VA != 0; i++)
{
PIMAGE_IMPORT_BY_NAME IatName = PIMAGE_IMPORT_BY_NAME(&this->FileContent[0] + this->dwVAToRaw(VA));
cout << " Function " << IatName->Name << endl;
VA = *PDWORD64(&this->FileContent[0] + this->dwVAToRaw(pIAT->OriginalFirstThunk) + i * sizeof(DWORD64));
}
}

image-20240225223834556

找到入口函数

很简单,在上面的nt->option_header.AddressOfEntryPoint里面,可以直接ImageBase+AddressOfEntryPoint,二进制数据都对的上