1. 问题
问题是这样的,有某个运行场景,编译完成的二进制代码无法进行链接操作,只能维持在 .o(Linux) 或 .obj(windows) 状态,但是在运行时需要使用这些二进制文件中的代码。所以需要编写一个加载器来手动加载这些未经链接的二进制文件。
2. COFF文件
这个状态的文件有个名字COFF(Common Object Format File)。该文件的与PE文件非常类似(目前我只看了windows平台上的情况),可以说PE文件就是它的子集。有关COFF文件的详细介绍可以查看微软官方文档
3. 加载过程
COFF文件加载核心是重定位,既有函数的也有变量的。当然这种重定位只能局限在当前文件内,当前文件外的内容也没有办法。这里只是分享应当如何被加载,希望达到的效果就是与IDA打开之后相同的内容,不过呈现的地方在内存中罢了。
3.1 结构体
解析COFF文件的结构体都包含在了winnt.h头文件中,可以不用自己来创建相关的结构体进行解析。需要用到大概这4个结构体:IMAGE_FILE_HEADER、IMAGE_SECTION_HEADER、IMAGE_SYMBOL和IMAGE_RELOCATION。他们的定义如下:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER;
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;
typedef struct _IMAGE_SYMBOL {
union {
BYTE ShortName[8];
struct {
DWORD Short; // if 0, use LongName
DWORD Long; // offset into string table
} Name;
DWORD LongName[2]; // PBYTE [2]
} N;
DWORD Value;
SHORT SectionNumber;
WORD Type;
BYTE StorageClass;
BYTE NumberOfAuxSymbols;
} IMAGE_SYMBOL;
typedef struct _IMAGE_RELOCATION {
union {
DWORD VirtualAddress;
DWORD RelocCount; // Set to the real count when IMAGE_SCN_LNK_NRELOC_OVFL is set
} DUMMYUNIONNAME;
DWORD SymbolTableIndex;
WORD Type;
} IMAGE_RELOCATION;
3.2 节偏移
COFF文件中包含有很多节,节在文件中存储时是紧挨着的,当加载到内存中时需要以8对齐,这就是地址能够被8整除。所以需要事先更具每个节的大小计算出每个节在内存中的偏移地址。
偏移的计算是从第1个节开始,第0节的偏移是0,不需要计算。将上一个节的大小向上与8对齐,然后加上上一个节的偏移就能够得到当前节的内存偏移。这里的计算与IDA加载时有些不同,当某个节的大小为0时我不会为其分配8字节,而IDA会保证每个节至少有8字节。
//save the offset of section in the memory
for (int i = 1; i < FileHeader->NumberOfSections; i++) {
int SectionSize = Section[i - 1].SizeOfRawData;
//make the section align with LOADER_ALIGN
if (SectionSize % LOADER_ALIGN) {
SectionSize = (SectionSize / LOADER_ALIGN + 1) * LOADER_ALIGN;
}
SectionMemOffset[i] = SectionMemOffset[i - 1] + SectionSize;
DBPRINT("SectionMemOffset %d is 0x%x\n", i, SectionMemOffset[i]);
}
3.3 重定位
3.3.1 重定位地址(VirtualAddress)
重定位地址,就是需要写入数据的地址。这个地址可以通过遍历Reloc_Table来获得,其中的VirtualAddress就是相对于本节的重定位地址。
进行重定位前要对Type进行确认,只有当其为IMAGE_REL_AMD64_REL32才有需要进行重定位,其他类型可以暂时不考虑。
SymbolTableIndex可以用来寻找重定位数据所在的位置。
typedef struct _IMAGE_RELOCATION {
union {
DWORD VirtualAddress;
DWORD RelocCount;
} DUMMYUNIONNAME;
DWORD SymbolTableIndex;
WORD Type;
} IMAGE_RELOCATION;
3.3.2 重定位数据(Value)
重定位数据,就是数据所在的地址。这个地址可以通过重定位表中的SymbolTableIndex找到对应的符号表项,其中的Value就是相对于本节的地址。
SectionNumber是以1开始计数的节索引,可以用来得到该符号所在的节。
StorageClass需要在重定位前进行判断,判断是否为IMAGE_SYM_CLASS_STATIC类型,如果是则需要重新考虑Value的计算方式。
typedef struct _IMAGE_SYMBOL {
...
DWORD Value;
SHORT SectionNumber;
WORD Type;
BYTE StorageClass;
...
} IMAGE_SYMBOL;
3.3.3 .rdata节数据
对于.rdata节有关的数据进行重定位需要进行特殊对待。
此时符号表中的StorageClass为IMAGE_SYM_CLASS_STATIC类型,且符号表中的Value为0,而修正的数据在存放在VirtualAddress所在的位置。
3.3.4 结果计算
假设VirtualAddress所在的节为i,Value所在符号表项为Clause,节偏移表为SectionMemOffset。
需要注意地址的计算起始是下一条指令,而偏移本身为4字节,所以需要加上一个4进行修正。
int Address = Value - (VirtualAddress + 4) - (SectionMemOffset[i] - SectionMemOffset[Clause->SectionNumber - 1]);
3.4 整体代码
#include <Windows.h>
#include <stdio.h>
#include <stdint.h>
//#define DEBUG
#ifdef DEBUG
#define DBPRINT(str, ...) printf(str, ##__VA_ARGS__);
#define SETCOLOR(color) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
#define DBPRINTCOLOR(color, str, ...) SETCOLOR(color); printf(str, ##__VA_ARGS__); SETCOLOR(CMD_WHITE);
#define CMD_RED FOREGROUND_RED
#define CMD_WHITE FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
#define CMD_YELLOW FOREGROUND_RED |FOREGROUND_GREEN
#define CMD_GREEN FOREGROUND_GREEN
#define CMD_BLUE FOREGROUND_BLUE
#define CMD_PINK FOREGROUND_RED | FOREGROUND_BLUE
#define CMD_CYAN FOREGROUND_GREEN | FOREGROUND_BLUE
#else
#define DBPRINT(str, ...)
#define SETCOLOR(color)
#define DBPRINTCOLOR(color, str, ...)
#endif // DEBUG
#define LOADER_ALIGN 0x8
INT64* Loader(char* FileName) {
FILE* file = fopen(FileName, "rb");
if (file == NULL) {
printf("open %s file error\n", FileName);
return NULL;
}
//get the size of the file
fseek(file, 0, SEEK_END);
int FSize = ftell(file);
fseek(file, 0, SEEK_SET);
//read the file
char* buf = (char*)malloc(FSize);
char* oribuf = buf;
int readsize = fread(buf, 1, FSize, file);
if (readsize != FSize) {
return NULL;
}
SETCOLOR(CMD_YELLOW);
DBPRINT("Loader %s\n", FileName);
SETCOLOR(CMD_WHITE);
IMAGE_FILE_HEADER* FileHeader = (IMAGE_FILE_HEADER*)buf;
IMAGE_SECTION_HEADER* Section = (IMAGE_SECTION_HEADER*)(buf + sizeof(IMAGE_FILE_HEADER));
IMAGE_SYMBOL* SymbolTable = (IMAGE_SYMBOL*)(buf + FileHeader->PointerToSymbolTable);
IMAGE_RELOCATION* RelocTable = NULL;
char* StringTable = (char*)&SymbolTable[FileHeader->NumberOfSymbols]; //this is for the debug
int* SectionMemOffset = (int*)malloc(FileHeader->NumberOfSections * sizeof(int));
memset(SectionMemOffset, 0, FileHeader->NumberOfSections * sizeof(int));
char* RawData = NULL;
//save the offset of section in the memory
for (int i = 1; i < FileHeader->NumberOfSections; i++) {
int SectionSize = Section[i - 1].SizeOfRawData;
//make the section align with LOADER_ALIGN
if (SectionSize % LOADER_ALIGN) {
SectionSize = (SectionSize / LOADER_ALIGN + 1) * LOADER_ALIGN;
}
SectionMemOffset[i] = SectionMemOffset[i - 1] + SectionSize;
DBPRINT("SectionMemOffset %d is 0x%x\n", i, SectionMemOffset[i]);
}
DBPRINT("\n");
//relocate the data
for (int i = 0; i < FileHeader->NumberOfSections; i++) {
char* FileAddress = NULL;
IMAGE_SYMBOL* Clause = NULL;
int RVAOffset = 0;
int BaseOffset = 0;
//1. when relocate the function, the Value field of SymbolTable is the Value.
//2. however, when it comes to relocate data referring rdata, the Value become zero
//the Value needed is saved in the VirtualAddress position.
int Value = 0;
RelocTable = (IMAGE_RELOCATION*)(buf + Section[i].PointerToRelocations);
RawData = buf + Section[i].PointerToRawData;
for (int j = 0; j < Section[i].NumberOfRelocations; j++) {
if (RelocTable[j].Type != IMAGE_REL_AMD64_REL32) {
continue;
}
Clause = &SymbolTable[RelocTable[j].SymbolTableIndex];
FileAddress = RawData + RelocTable[j].VirtualAddress;
//the rdata
if (Clause->StorageClass == IMAGE_SYM_CLASS_STATIC) {
Value = *(int*)FileAddress;
}
//the function
else {
Value = Clause->Value;
}
RVAOffset = Value - (RelocTable[j].VirtualAddress + 4);
BaseOffset = SectionMemOffset[i] - SectionMemOffset[Clause->SectionNumber - 1];
*(int*)FileAddress = RVAOffset - BaseOffset;
//Below is the Debug Information
//the Name is less than 8 bytes
if (Clause->N.Name.Short) {
SETCOLOR(CMD_CYAN);
DBPRINT("Name : %s\n", Clause->N.ShortName);
SETCOLOR(CMD_WHITE);
}
//the Name is more than 8 bytes
else {
SETCOLOR(CMD_CYAN);
DBPRINT("Name : %s\n", StringTable + Clause->N.Name.Long);
SETCOLOR(CMD_WHITE);
}
DBPRINT("Value 0x%x\n", Value);
DBPRINT("VirtualAddress 0x%x\n", RelocTable[j].VirtualAddress);
DBPRINT("BaseOffset 0x%x\n", BaseOffset);
DBPRINT("Relocation Offset 0x%x\n", *(int*)FileAddress);
DBPRINT("\n");
}
}
//write to the execute memroy
int TotalSize = SectionMemOffset[FileHeader->NumberOfSections - 1] + Section[FileHeader->NumberOfSections - 1].SizeOfRawData;
LPVOID pAddr = VirtualAlloc(NULL, TotalSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memset(pAddr, 0x90, TotalSize);
//loader to the executable memory
for (int i = 0; i < FileHeader->NumberOfSections; i++) {
memcpy((char*)pAddr + SectionMemOffset[i], buf + Section[i].PointerToRawData, Section[i].SizeOfRawData);
}
DBPRINT("\n");
fclose(file);
free(oribuf);
free(SectionMemOffset);
return (INT64*)pAddr;
}
4. 探索历程
这一章是我的私货内容,记录一下上述代码的探索历程。
刚接触这个问题的时候也是一脸茫然的,进行搜索时没有找到相应的代码,还在至少找到了微软对于COFF文件的官方文档。探索的过程主要使用了两个工具010 editor和IDA,这两个工具使用起来太方便的。
通过010 editor可以看到COFF文件的具体二进制形式,同时相应的模板也使用了对应的结构体对于文件进行了分析。
其中可以清晰的得到每个字段的内容。
IDA中则是已经加载好了COFF文件内容,当COFF被加载到内存中是应当与其相似。
一开始我并不知道如何计算重定位所需要的内容,IDA中是已经加载好的内容,而在010 editor中重定位地址处的值为0(.rdata节数据除外)。通过010 editor中看到的数据进行手动尝试,不断的计算与IDA中地址进行比较,最后得到了3.3.4中的计算公式。
如果某位读者想尝试手动计算,也可以试试,通过010获取相应的数据按照给出的公式进行计算,然后使用IDA中的结果进行验证。