之前写过一个利用远程线程注入DLL的工具,大致过程如下:
1.使用OpenProcessToken、LookupPrivilegeValue、AdjustTokenPrivileges函数修改当前进程的的访问令牌,获得调试权限。
2.OpenProcess打开目标进程,用VirtualAllocEx在目标进程申请一段内存空间,WriteProcessMemory将要注入DLL的路径字符串写入申请的内存里。
3.最后调用GetProcAddress获取LoadLibraryA(W)的地址,CreateRemoteThread执行远程线程,成功在目标进程注入DLL。
远程注入的好处是动态的,它不修改文件本身,所有的改动都是在内存里,申请分配写入销毁,这就是说我们在注入DLL的时候目标程序已经在运行中了,但有时又需要注入代码执行在程序之前,有什么办法呢?很自然的想到通过修改本地PE文件的相关数据从而达成我们的目的。在Windows上所有EXE、DLL、OCX甚至SYS都是PE文件,所谓PE是微软制定的一种可移植的文件格式,其结构如图:
DOS头的0x3C偏移处的DWORD值指向PE头,对于Win32程序,结构如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
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;
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD 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;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中OptionalHeader.AddressOfEntryPoint是程序入口点,是系统装载程序完毕后跳转执行第一条指令的地址,这是一个内存偏移地址,程序基址由OptionalHeader.BaseOfCode指明。我们要做的就是修改OEP,先执行我们的代码,然后再跳转回原OEP,那么这些代码应该写入在哪里?PE文件中数据都是按节存放的,节中数据具有相同的属性,如可读可写可执行等等,PE格式规定节的物理大小必须是OptionalHeader.FileAlignment(一般为512)的倍数,不足的用0补齐,这些用0对齐的数据就是代码写入的地方。FileHeader.NumberOfSections表示节的数目,PE文头后面紧跟着节头信息,结构如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8];
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;
SizeOfRawData是对齐后的物理大小,Misc.VirtualSize是节的实际大小,两者一减,就是磁盘上对齐0的数量,PointerToRawData是节的文件偏移,Characteristics是节属性。好了,有了这些信息就能遍历节表,找到代码节,计算节剩余空间(即对齐的0数据),代码如下:
// 节的数目
int Sections = lpNtHead->FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER *lpSections = (IMAGE_SECTION_HEADER*)((DWORD)&(lpNtHead->OptionalHeader) + lpNtHead->FileHeader.SizeOfOptionalHeader);
DWORD lpCodeOffset = 0;
DWORD dwCodeSize = 0;
DWORD dwFreeSize = 0;
// 遍历所有节头
for(int i=0;i<Sections;i++) {
// 查找代码节
if((lpSections[i].Characteristics & IMAGE_SCN_CNT_CODE) &&
(lpSections[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) &&
(lpSections[i].Characteristics & IMAGE_SCN_MEM_READ)) {
// 获得代码节的相对虚拟地址并转换成文件偏移地址
lpCodeOffset = (DWORD)lpSections[i].VirtualAddress;
lpCodeOffset = RVAToFileOffset(lpDosHead,lpCodeOffset);
lpCodeOffset += (DWORD)lpDosHead;
// 计算节的剩余空间
dwCodeSize = lpSections[i].Misc.VirtualSize;
if(lpSections[i].SizeOfRawData > dwCodeSize) {
dwFreeSize = lpSections[i].SizeOfRawData - dwCodeSize;
} else {
dwFreeSize = 0;
continue;
}
break;
}
}
找到可用的代码节,再判断一下剩余空间能否容下写入代码,如果符合条件就可以在lpCodeOffset + dwCodeSize处写入代码了:调用LoadLibraryA(W)函数,载入注入的DLL文件,然后跳转回原OEP程序继续执行,类似下面的:
__asm {
pushad
pushfd
lea eax,szDllName
push eax
call lpLoadLibraryA
popfd
popad
jmp oldOEP
}
szDllName存放着DLL名字,可以在代码节剩余空间里分配,压入栈的是字符串的虚拟地址(RVA+映像基址),这个我们转换一下就行了,不过还有一个小问题:EXE和DLL不同,总是能成功映射首选基址不存在基址冲突的,但从Vista以后,系统出现一种新的“地址空间布局随机化”机制,如果一个EXE程序存在基址重定位信息并且没有IMAGE_FILE_RELOCS_STRIPPED标志,EXE基址会被随机化,如此一来之前压入的szDllName地址就不正确了,解决这问题最简单的办法就是强制EXE程序必须映射到首选基址上,FileHeader.Characteristics |= IMAGE_FILE_RELOCS_STRIPPED成功搞定!
好了,就剩最后一步了,LoadLibraryA(W)的地址怎么找?从导入表里获取,于是循环导入表查找kernel32.dll然后……,此处省略N字还是直接上代码简洁明了:
IMAGE_DATA_DIRECTORY *pImportDirectory = &(lpNtHead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);
DWORD lpBase = pImportDirectory->VirtualAddress;
lpBase = (DWORD)lpDosHead + RVAToFileOffset(lpDosHead,lpBase);
IMAGE_IMPORT_DESCRIPTOR *pImport = (IMAGE_IMPORT_DESCRIPTOR*)lpBase;
char *buf;
DWORD *dwAddress;
DWORD lpLoadLibraryA,lpLoadLibraryW,lpLoadLibraryExA,lpLoadLibraryExW;
lpLoadLibraryA = lpLoadLibraryW = lpLoadLibraryExA = lpLoadLibraryExW = NULL;
for(int i=0;;i++) {
if(0 == pImport[i].Characteristics &&
0 == pImport[i].Name) {
break;
}
buf = (char*)((DWORD)lpDosHead + RVAToFileOffset(lpDosHead,pImport[i].Name));
if(0 == stricmp(buf,"kernel32.dll")) {
lpBase = pImport[i].FirstThunk;
lpBase = (DWORD)lpDosHead + RVAToFileOffset(lpDosHead,lpBase);
dwAddress = (DWORD*)lpBase;
int pos = 0;
while(0 != (*dwAddress)) {
// 使用名称导入而非序号
if(!((*dwAddress) & 0x80000000)) {
// 2字节序号 + ASCII字符串(函数名)
buf = (char*)((DWORD)lpDosHead + RVAToFileOffset(lpDosHead,(*dwAddress)));
buf += 2;
if(0 == strcmp(buf,"LoadLibraryA")) {
lpLoadLibraryA = pImport[i].FirstThunk+pos*4;
}
if(0 == strcmp(buf,"LoadLibraryW")) {
lpLoadLibraryW = pImport[i].FirstThunk+pos*4;
}
if(0 == strcmp(buf,"LoadLibraryExA")) {
lpLoadLibraryExA = pImport[i].FirstThunk+pos*4;
}
if(0 == strcmp(buf,"LoadLibraryExW")) {
lpLoadLibraryExW = pImport[i].FirstThunk+pos*4;
}
}
dwAddress++;
pos++;
}
break;
}
}
终于写完了,好像感觉有点虎头蛇尾,点击这里下载实例工具。如果代码节剩余空间不足可以使用本工具先添加一个任意大小的新节。
最后,以此纪念我第一篇CSDN博文。