一、前后知识串联
- C语言的学习是为了熟练使用指针
- 写一个游戏辅助(.exe):有自动打怪、加血、自动任务等功能。那么当点击自动打怪按钮时,怎么让游戏(.exe)去执行这个功能呢?游戏的打怪功能就是一个函数,传入相关的参数、调用打怪函数就可以实现打怪,只不过我们用鼠标或者键盘是一种快捷键的方式操作,而不是直接操控代码。
- 如果我们把①定位游戏打怪函数的call、②传入相关参数、③执行这部分代码等功能封装到一个DLL中,把这个DLL注入到游戏运行时的虚拟空间中,接着去调用执行这个DLL,不就实现了自动打怪的功能。(上述过程就是我们提过的DLL注入)
- 那么如何指挥这个DLL去做事呢?就需要知道进程通信,即游戏辅助(.exe)与游戏(.exe)之间进行通信。(所以需要学习进程通信相关API,即Win32API,以及线程相关概念)
- 而游戏里打怪相关数据有很多,不可能胡乱存,一般需要一种合适的数据结构来存储这些繁杂的数据,比如数组、链表、二叉树等(所以需要学习C++以及数据结构,可以更有开发效率的编程)
二、注入
1.注入的种类
- 在操作系统3环层面有8种常见注入方式:①注册表注入;②导入表注入;③特洛伊注入;④远程线程注入;⑤无DLL注入;⑥Apc注入;⑦Windows挂钩注入DLL;⑧输入法注入
今天主要讲导入表注入;后面等基础知识学的差不多了再学习其他注入方式
2.导入表注入原理
- 当exe被加载时,系统会根据exe的导入表信息来加载此程序需要用到的DLL
- 比如通过导入表的所有Name成员,知道要装载哪些DLL,接着操作系统会去程序所在当前文件夹下查找DLL去装载,找不到再去系统盘中找;或者如果导入表的某个结构中OriginalFirstThunk和FirstThunk都为0,系统就不会加载这个导入表结构对应的DLL,因为操作系统知道你没有使用这个DLL中的任何函数,加载没有意义。
- 所以在下面导入表注入实操时,一定要创建一个完整的“链”:新增一个导入表结构的同时,还需要新增对应的INT表以及IAT表、以及函数名称表,不能只创建一个导入表结构
- 导入表注入的原理就是:修改exe导入表,将自己写的DLL相关信息添加到exe的导入表中,这样exe运行时就可以将自己的DLL加载到exe的进程虚拟内存中
三、移动导入表
1.为什么要移动导入表
- 结合导入表注入的原理思考:由于一个导入表结构对应一个DLL,所以你把DLL添加到.exe进程空间中,就需要在.exe的导入表中新增一个导入表结构(还要新增对应INT表、IAT表和函数名称表),但是原来的导入表结构是一个紧挨着一个存储的,最后有sizeof(导入表结构体)个0结尾,再后面的数据肯定都是有用的。所以我们没办法直接在导入表个结构中插入一个新的导入表结构,也没办法在原有的导入表结尾处新增导入表结构,那就只能把导入表移走!再在它结尾处新增导入表结构和全0结尾即可
- 但是注意:只用把原来的导入表结构移走即可,不需要移走原来各个导入表结构对应的INT表和IAT表,也就不需要修改原来导入表结构中的OriginalFirstThunk和FirstThunk等字段
- 而且IAT表是一定不能移动的!
- 因为前面学过,当程序运行时,系统会把程序中调用DLL函数的call语句后面的地址也改了!改成什么?------程序的
call
后面跟的是间接寻址,即call [0x....]
,实际上这里的地址改成了IAT表中对应函数绝对地址值的地址!
- 即运行前:
call [0x....]
后面间接寻址的地址0x....
是INT表中某个元素的地址; - 而运行后:
call [0x....]
后面间接寻址的地址0x....
,改成了IAT表中某个元素的地址;
- 所以,要是移动IAT表,IAT表地址变了,那么你就要把程序中所有调用DLL函数的call语句后面的间接寻址的地址也改掉!但是这个工作量是非常大的!
2.导入表注入步骤
- 设计要注入的DLL
- 先找到可选PE头中最后一个成员数据目录,第二个结构就是导入表数据目录,通过VirtualAddress定位到导入表地址
- 依次移动原来导入表所有结构到新增节或者节与节之间的空白区或者扩大节中(哪种方法都行)
- 在移动后的导入表后面,新增一个导入表结构(别忘了还要在结尾留下sizeof(导入表结构体)个0)
- 再新增一个至少8字节的INT表和IAT表,并且修正新增导入表结构中的OriginalFirstThunk和FirstThunk
因为上面说过,导入表结构中的OriginalFirstThunk和FirstThunk字段不能为0,即必须要使用对应DLL中的至少一个函数才行,不然系统不会加载此DLL到程序虚拟内存中的。所以INT表和IAT表中至少要有一个函数信息还有4字节0表示结束(具体大小为多少,还是要看你的DLL有多少函数给程序使用)
- 还要新增至少一个
IMAGE_IMPORT_BY_NAME
结构,即函数名称表:前两个字节为0即可,后面是函数名称字符串 - 修改INT表和IAT表(运行前)中的元素值,指向
IMAGE_IMPORT_BY_NAME
结构中元素。 - 再在后面分配空间存储此DLL名称字符串,并将该字符串的起始RVA赋值给导入表结构中的Name
- 最后修正导入表数据目录中的VirtualAddress和Size
四、设计要注入的DLL
1.创建DLL
- 我们先自己编写一个DLL,用于后面导入表注入实验
- 先用VC6++创建一个DLL:名为InjectDll,接着在
InjectDll.h
头文件中声明三个函数,前两个DLL内部使用,最后一个为导出函数供别的程序使用
创建DLL和使用DLL都在day35.1-静态、动态链接库中详细讲过
// InjectDll.h: interface for the InjectDll class.
//
//
#if !defined(AFX_INJECTDLL_H__0713C052_B98E_4101_AB64_77B2AE5596C8__INCLUDED_)
#define AFX_INJECTDLL_H__0713C052_B98E_4101_AB64_77B2AE5596C8__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
void Init(); //这两个函数不用导出
void Destroy();
extern "C" _declspec(dllexport) void ExportFunction(); //这个函数要导出
#endif // !defined(AFX_INJECTDLL_H__0713C052_B98E_4101_AB64_77B2AE5596C8__INCLUDED_)
- 再在
InjectDll.cpp
中定义三个函数
// InjectDll.cpp: implementation of the InjectDll class.
#include "InjectDll.h"
#include <windows.h>
void Init(){
MessageBox(0,"Init","Init",MB_OK); //简单的一个弹窗功能
}
void Destroy(){
MessageBox(0,"Destroy","Destroy",MB_OK);
}
void ExportFunction(){
MessageBox(0,"ExportFunction","ExportFunction",MB_OK);
}
- 最后定义DLL入口函数:即当程序运行、装载此DLL时以及程序退出、卸载此DLL时会调用
DllMain()
函数,调用DllMain方法后,如果传入的ul_reason_for_call
参数为DLL_PROCESS_ATTACH
(这是一个宏),表示正在装载DLL,则会调用Init()
方法;如果传入的ul_reason_for_call
参数为DLL_PROCESS_DETACH
,表示正在卸载DLL,则会调用Destroy()
方法。
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved){ //定义DllMain方法
switch(ul_reason_for_call){
case DLL_PROCESS_ATTACH: //当程序运行,装载DLL时会调用一次DllMain方法,执行Init()
Init();
break;
case DLL_PROCESS_DETACH: //当程序结束,卸载DLL时会调用一次DllMain方法,执行Destroy()
Destroy();
break;
}
return TRUE;
}
2.使用DLL
- 创建好DLL后,我们来使用一下DLL中的功能,看看究竟这个DLL干了什么。所以再创建一个控制台项目:名为useDll(就是我们平时写C创建的工程),把InjectDll工程中的
Inject.dll
和Inject.lib
复制到useDll工程目录下面 - 最后在UseDll.cpp中使用隐式链接,调用一下InjectDll导出的函数
// UseDll.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include <windows.h>
#pragma comment(lib,"InjectDll.lib")
extern "C" _declspec(dllexport) void ExportFunction();
int main(int argc, char* argv[]){
ExportFunction(); //调用DLL中的导出函数
return 0;
}
- 查看一下功能:F7编译、F5运行UseDll.exe、再依次点确定、最后按F5:发现会先弹Init窗口、接着弹ExportFunction窗口、最后弹Destroy窗口
由于此DLL中定义了DllMain函数,所以当UseDll.exe程序最终执行时由于使用了此DLL中的函数,那么必然会装载DLL和卸载DLL,所以就会自动执行DllMain函数。所以会先因为DLL装载而执行
Init()
、再因为调用了导出函数而执行ExportFunction()
、最后因为程序退出而执行Destroy()
- 也可以使用显式链接、使用DLL中的函数
// UseDll.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include <windows.h>
typedef void (*Funcp)(); //声明函数指针
int main(int argc, char* argv[]){
HMODULE hModule = LoadLibrary("InjectDll.dll"); //加载DLL
Funcp myFunc = (Funcp)GetProcAddress(hModule,"ExportFunction"); //获取导出函数地址并赋给函数指针
myFunc(); //调用导出函数
FreeLibrary(hModule); //释放DLL
return 0;
}
同样,在LoadLibrary行设断点:加载DLL时弹Init框、调用导出函数时弹ExportFunction框、释放DLL时弹Destroy框。显式链接使用DLL更加灵活,可以主动加载DLL和释放DLL
五、作业
在了解上述DLL的功能之后,我们就需要以导入表注入的方式,将此DLL注入到一个程序中(这里以ipmsg.exe举例)
1.手动用工具实现注入
- 我们将
InjectDll.dll
注入到ipmsg_inject.exe
中:先创建一个ipmsg.exe程序的副本,改名为ipmsg_inject.exe
,接着把写好的InjectDll.dll
复制到ipmsg文件所在目录;再使用LordPE工具打开ipmsg_inject.exe
导入表;在导入表的空白处右键–Add Import;接着输入要注入的DLL名称:InjectDll.dll
,而且必须要输入至少一个API函数(前面解释过),由于我们只设计了一个ExportFunction()
导出函数,所以再API栏中输入再点击+
键。此时就完成了导入表DLL注入 - 我们双击运行
ipmsg_inject.exe
,查看注入后的效果:双击运行时由于加载了DLL,就会调用当中的DllMain()
函数,且由于是加载,所以又会调用Init()
函数;最后在任务栏中右键关闭ipmsg_inject.exe
,由于卸载了DLL,所以会调用当中的DllMain()
函数,且由于是退出,所以又会调用Destroy()
函数。但由于程序没有主动或人为的使用到DLL的ExportFunction()
函数,所以并不会有ExportFunction的弹窗 - 最后我们可以再用LordPE查看一下注入后
ipmsg_inject.exe
的PE文件结构有什么不同:会发现原来导入表的RVA是0x253C0,现在导入表RVA为0x3D027,且发现多了一个InjectDll.dll的导入表结构以及INT表和IAT表还有函数名称等,且RVA都是在0x3D000左右,所以可以发现这个工具是将原来的导入表移到了新节当中,并在新节中新增了所需要的结构
2.代码实现导入表注入
#include "stdafx.h"
#include <stdlib.h>
#include <string.h>
typedef unsigned short WORD;
typedef unsigned int DWORD;
typedef unsigned char BYTE;
#define MZ 0x5A4D
#define PE 0x4550
#define IMAGE_SIZEOF_SHORT_NAME 8
//DOS头
struct _IMAGE_DOS_HEADER {
WORD e_magic; //MZ标记
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
DWORD e_lfanew; //PE文件真正开始的偏移地址
};
//标准PE头
struct _IMAGE_FILE_HEADER {
WORD Machine; //文件运行平台
WORD NumberOfSections; //节数量
DWORD TimeDateStamp; //时间戳
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //可选PE头大小
WORD Characteristics; //特征值
};
//数据目录
struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress;
DWORD Size;
};
//可选PE头
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; //DOS、NT头和节表大小
DWORD CheckSum; //校验和
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve; //预留堆栈大小
DWORD SizeOfStackCommit; //实际分配堆栈大小
DWORD SizeOfHeapReserve; //预留堆大小
DWORD SizeOfHeapCommit; //实际分配堆大小
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //目录项数目
_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
};
//NT头
struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE签名
_IMAGE_FILE_HEADER FileHeader;
_IMAGE_OPTIONAL_HEADER OptionalHeader;
};
//节表
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; //该节特征属性
};
//导入表
struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组(INT表)
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字字符串存储地址
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组(IAT表)
};//导入表有很多个这种结构(成员全为0,表示结束)
//INT表中元素指向的函数名称表
struct _IMAGE_IMPORT_BY_NAME{
WORD Hint; //可能为空(编译器决定);如果不为空,表示函数在导出表中的索引
BYTE Name[1]; //函数名称,以0结尾
};
//INT表和运行前IAT表
struct _IMAGE_THUNK_DATA32{
union{
BYTE ForwarderString;
DWORD Function;
DWORD Ordinal; //序号
_IMAGE_IMPORT_BY_NAME* AddressOfData; //RVA,指向IMAGE_IMPORT_BY_NAME
};
};
/*计算文件大小函数
参数:文件绝对路径
返回值:返回文件大小(单位字节)
*/
int compute_file_size(char* filePath){
FILE* fp = fopen(filePath,"rb");
if(!fp){
printf("打开文件失败");
exit(0);
}
fseek(fp,0,2);
int size = ftell(fp);
//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
fclose(fp);
return size;
}
/*将文件读入FileBuffer函数
参数:文件绝对路径
返回值:FileBuffer起始地址
*/
char* to_FileBuffer(char* filePath){
FILE* fp = fopen(filePath,"rb");
if(!fp){
printf("打开文件失败");
exit(0);
}
int size = compute_file_size(filePath);
char* mp = (char*)malloc(sizeof(char) * size); //分配内存空间
if(!mp){
printf("分配空间失败");
fclose(fp);
exit(0);
}
int isSucceed = fread(mp,size,1,fp);
if(!isSucceed){
printf("读取数据失败");
free(mp);
fclose(fp);
exit(0);
}
fclose(fp);
return mp;
}
/*计算NewBuffer大小函数
参数:NewBuffer起始地址
返回值:unsigned int类型(单位:字节)
*/
DWORD compute_NewBuffer_size(char* newBufferp){
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)newBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(newBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
_IMAGE_SECTION_HEADER* last_image_section_header = _image_section_header + _image_file_header->NumberOfSections - 1;
DWORD sizeofNewBuffer = last_image_section_header->PointerToRawData + last_image_section_header->SizeOfRawData;
return sizeofNewBuffer;
}
/*NewBuffer存盘函数
参数:需要写出数据的内存首地址,保存绝对路径,写出的数据大小(单位:字节)
返回值:成功返回1,失败返回0
*/
int save_to_disk(char* newBufferp,char* storagePath,DWORD size){
FILE* fp = fopen(storagePath,"wb");
if(!fp){
printf("打开文件失败");
return 0;
}
int isSucceed = fwrite(newBufferp,size,1,fp);
if(!isSucceed){
free(newBufferp);
fclose(fp);
return 0;
}
fclose(fp);
return 1;
}
/*虚拟内存偏移地址->文件偏移地址函数
参数:FileBuffer起始地址,RVA地址值
返回值:RVA对应的FOA,返回0则表示非法RVA
*/
DWORD RVA_to_FOA(char* fileBufferp,DWORD RVA){
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header +
_image_file_header->SizeOfOptionalHeader);
bool flag = 0; //用来判断最终RVA值是否在节中的非空白区
if(RVA < _image_section_header->VirtualAddress)
return RVA;
for(int i = 0;i < _image_file_header->NumberOfSections;i++){
if(RVA >= _image_section_header->VirtualAddress
&& RVA < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize){
flag = 1;
break;
}else{
_image_section_header++;
}
}
if(!flag)
return 0;
DWORD mem_offset_from_section = RVA - _image_section_header->VirtualAddress;
return _image_section_header->PointerToRawData + mem_offset_from_section;
}
/*文件偏移地址函数->虚拟内存偏移地址
参数:FileBuffer起始地址,FOA地址值
返回值:FOA对应的RVA,返回0则表示非法FOA
*/
DWORD FOA_to_RVA(char* fileBufferp,DWORD FOA){
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header +
_image_file_header->SizeOfOptionalHeader);
bool flag = 0;//用来判断最终FOA值是否在节中的非空白区
if(FOA < _image_section_header->PointerToRawData)
return FOA;
for(DWORD i = 0;i < _image_file_header->NumberOfSections;i++){
DWORD tempSize = (_image_section_header->Misc.VirtualSize < _image_section_header->SizeOfRawData ? _image_section_header->Misc.VirtualSize : _image_section_header->SizeOfRawData);
if(FOA >= _image_section_header->PointerToRawData && FOA < _image_section_header->PointerToRawData + tempSize){
flag = 1;
break;
}
_image_section_header++;
}
if(!flag)
return 0;
DWORD file_offset_from_section = FOA - _image_section_header->PointerToRawData;
return _image_section_header->VirtualAddress + file_offset_from_section;
}
/*在文件最后新增节函数(直接在FileBuffer中操作)
参数:要新增节的可执行文件的FileBuffer起始地址
返回值:新增节后的NewFileBuffer内存的起始地址
*/
char* add_one_section_inFileBuffer(char* fileBufferp){
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
//定义原来的FileBuffer几个指针(因为要用到几个值用于创建新的NewFileBuffer内存)
_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header +
_image_file_header->SizeOfOptionalHeader);
//先计算添加新节后的NewFileBuffer的大小,并初始化内存
DWORD expand_value = 0x1000; //这里0x1000字节足够了,要视情况而定
DWORD fileBufferSize = (_image_section_header + _image_file_header->NumberOfSections - 1)->PointerToRawData + (_image_section_header + _image_file_header->NumberOfSections - 1)->SizeOfRawData;
char* newFileBufferp = (char*)malloc(fileBufferSize + expand_value);
for(DWORD i = 0;i < fileBufferSize + expand_value;i++){
*(newFileBufferp + i) = 0x00;
}
memcpy(newFileBufferp,fileBufferp,fileBufferSize);//不改变指针哦
_image_dos_header = (_IMAGE_DOS_HEADER*)newFileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(newFileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
//判断节表末尾是否有空间新增节表
if(_image_optional_header->SizeOfHeaders - _image_dos_header->e_lfanew - 4 - 20 - _image_file_header->SizeOfOptionalHeader -
_image_file_header->NumberOfSections * 40 < 80){
printf("空间不足,无法新增节表");
getchar();
exit(0);
}
for(i = 0;i < 80;i++){
if(*((char*)(_image_section_header + _image_file_header->NumberOfSections)+ i) != 0){
printf("节表后80字节数据不全为0x00,有文件数据,无法新增节表");
getchar();
exit(0);
}
}
//新增节
DWORD original_SizeOfImage = _image_optional_header->SizeOfImage; //记录下原来的SizeOfImage
_image_optional_header->SizeOfImage += expand_value;
//新增节表
_IMAGE_SECTION_HEADER* _image_section_header_new = (_IMAGE_SECTION_HEADER*)(char*)_image_section_header + _image_file_header->NumberOfSections;
for(i = 0;i < 40;i++){
*((char*)_image_section_header_new + i) = *((char*)_image_section_header + i);
}
_image_file_header->NumberOfSections++;
//修改新增节表信息
char* name = (char*)_image_section_header_new->Name;
char* newName = ".newsec";
strncpy(name,newName,IMAGE_SIZEOF_SHORT_NAME);
_image_section_header_new->Misc.VirtualSize = expand_value;
_image_section_header_new->VirtualAddress = original_SizeOfImage;
_image_section_header_new->SizeOfRawData = expand_value;
_image_section_header_new->PointerToRawData = fileBufferSize;
//修改节属性,保证可执行,这里最保险的办法就是把每个节的属性全包括(这里的错误找了好久!!前面学习过程中都是满足可读可执行就可以了,即0x60000020,但这里不行!必须把所有节的属性都包含,0xC0000040才行)
for(i = 0,_image_section_header_new->Characteristics = 0x00000000;i < _image_file_header->NumberOfSections;i++){
_image_section_header_new->Characteristics = _image_section_header_new->Characteristics | (_image_section_header + i)->Characteristics;
}
return newFileBufferp;
}
/*扩大最后一个节函数(直接在FileBuffer中操作)
参数:要扩大节的可执行文件的FileBuffer起始地址
返回值:扩大节后的NewFileBuffer内存的起始地址
*/
char* expand_last_section_inFileBuffer(char* fileBufferp){
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_SECTION_HEADER* _image_section_header = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
//计算扩大多少,并初始化NewFileBuffer
int expand_value = 0x1000;//这里为了方便直接把需要扩大的大小设为文件对齐粒度和内存对齐粒度的最小公倍数即可,如果不够再改
char* newFileBufferp = (char*)malloc(_image_optional_header->SizeOfImage + expand_value);
for(int i = 0;i < (int)_image_optional_header->SizeOfImage + expand_value;i++){
*(newFileBufferp + i) = 0x00;
}
memcpy(newFileBufferp,fileBufferp,_image_optional_header->SizeOfImage);//不改变指针哦
_image_dos_header = (_IMAGE_DOS_HEADER*)newFileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(newFileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + _image_file_header->SizeOfOptionalHeader);
//修改SizeofImage:原来的SizeOfImage + Ex(因为原来的SizeOfImage是内存对齐的整数倍,Ex也是通过内存对齐和文件对齐的最小公倍数算出来的,所以相加一定还是内存对齐的整数倍)
DWORD original_SizeOfImage = _image_optional_header->SizeOfImage; //记录下原来的SizeOfImage
_image_optional_header->SizeOfImage += expand_value;
//修改节表信息
_IMAGE_SECTION_HEADER* _image_section_header_last = _image_section_header + _image_file_header->NumberOfSections - 1;
_image_section_header_last->Misc.VirtualSize = original_SizeOfImage - _image_section_header_last->VirtualAddress + expand_value;
_image_section_header_last->SizeOfRawData = _image_section_header_last->Misc.VirtualSize;
//修改节属性,保证可执行,这里最保险的办法就是把每个节的属性全包括(这里的错误找了好久!!前面学习过程中都是满足可读可执行就可以了,即0x60000020,但这里不行!必须把所有节的属性都包含,0xC0000040才行)
for(i = 0;i < _image_file_header->NumberOfSections - 1;i++){
_image_section_header_last->Characteristics = _image_section_header_last->Characteristics | (_image_section_header + i)->Characteristics;
}
return newFileBufferp;
}
/*导入表注入函数
参数:需要注入的程序FileBuffer起始地址,要注入的DLL名字,要使用到的DLL函数名
返回值:返回注入后的NewFileBuffer起始地址
*/
char* importTable_injectDll(char* fileBufferp,char* dllName,char* API){
_IMAGE_DOS_HEADER* _image_dos_header = NULL;
_IMAGE_FILE_HEADER* _image_file_header = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
_IMAGE_IMPORT_DESCRIPTOR* _image_import_descriptor = NULL;
_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
_image_data_directory = (_IMAGE_DATA_DIRECTORY*)(_image_optional_header->DataDirectory + 1); //导入表数据目录
if(_image_data_directory->VirtualAddress == 0){
printf("此文件没有导入表");
getchar();
exit(0);
}
_image_import_descriptor = (_IMAGE_IMPORT_DESCRIPTOR*)(fileBufferp + RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress));
//先选择把导入表移动到新增节的方式,如果无法新增节,就选择扩大节再移动的方式
char* newFileBufferp = add_one_section_inFileBuffer(fileBufferp); //新增节(0x1000)
if(newFileBufferp == 0){
printf("程序无法新增节,采用扩大最后一个节的方式\n");
newFileBufferp = expand_last_section_inFileBuffer(fileBufferp); //如果程序没法新增节,就选择扩大最后一个节,并将导入表移动到最后一个节
if(newFileBufferp == 0){
printf("注入失败\n");
getchar();
exit(0);
}
}
_IMAGE_DOS_HEADER* _image_dos_header_new = NULL;
_IMAGE_FILE_HEADER* _image_file_header_new = NULL;
_IMAGE_OPTIONAL_HEADER* _image_optional_header_new = NULL;
_IMAGE_SECTION_HEADER* _image_section_header_new = NULL;
_IMAGE_DATA_DIRECTORY* _image_data_directory_new = NULL;
_IMAGE_IMPORT_DESCRIPTOR* _image_import_descriptor_new = NULL;
_IMAGE_THUNK_DATA32* _INT_new = NULL; //INT表
_IMAGE_THUNK_DATA32* _IAT_new = NULL; //IAT表
_IMAGE_IMPORT_BY_NAME* _image_import_by_name_new = NULL; //函数名称表
_image_dos_header_new = (_IMAGE_DOS_HEADER*)newFileBufferp;
_image_file_header_new = (_IMAGE_FILE_HEADER*)(newFileBufferp + _image_dos_header_new->e_lfanew + 4);
_image_optional_header_new = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header_new + 20);
_image_section_header_new = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header_new + _image_file_header_new->SizeOfOptionalHeader);
_image_data_directory_new = (_IMAGE_DATA_DIRECTORY*)(_image_optional_header_new->DataDirectory + 1); //导入表数据目录
_image_import_descriptor_new = (_IMAGE_IMPORT_DESCRIPTOR*)(newFileBufferp + RVA_to_FOA(newFileBufferp,_image_data_directory_new->VirtualAddress));
//由于新增节大小为0x1000,且扩大最后一个节大小也为0x1000,所以无论哪一种方法都可以从NewFileBuffer倒数第0x1000的位置开始存储
char* moveStartp = newFileBufferp + compute_NewBuffer_size(newFileBufferp) - 0x1000;
//char* moveStartp = newFileBufferp + (_image_section_header_new + _image_file_header_new->NumberOfSections - 1)->PointerToRawData; //如果是新增节,在新增节起始地址开始复制即可
DWORD moveStartAddress = (DWORD)moveStartp; //记录一下存储起始地址,后面计算VirtualAddress要用到
//移动导入表
while(1){
//根据导入表结构中是否为全0判断导入表是否结束
for(int i = 0;i < sizeof(*_image_import_descriptor_new);i++){
if(*((char*)_image_import_descriptor_new + i) != 0){
break;
}
}
if(i == sizeof(*_image_import_descriptor_new))
break;
memcpy(moveStartp,_image_import_descriptor_new,sizeof(*_image_import_descriptor_new));
moveStartp += sizeof(*_image_import_descriptor_new);
_image_import_descriptor_new++;
}
//新增一个导入表结构
_IMAGE_IMPORT_DESCRIPTOR* new_importTablep = (_IMAGE_IMPORT_DESCRIPTOR*)moveStartp;
moveStartp += (2 * sizeof(*_image_import_descriptor_new)); //别忘了全0结束
//新增一个8字节的INT表和IAT表
_INT_new = (_IMAGE_THUNK_DATA32*)moveStartp;
moveStartp += 8; //正常来说应该计算一下注入DLL中有多少个被使用的函数,再决定INT、IAT表有多大
_IAT_new = (_IMAGE_THUNK_DATA32*)moveStartp;
moveStartp += 8;
//修正新增导入表结构中的OriginalFirstThunk和FirstThunk
new_importTablep->OriginalFirstThunk = FOA_to_RVA(newFileBufferp,(DWORD)_INT_new - (DWORD)newFileBufferp);
new_importTablep->FirstThunk = FOA_to_RVA(newFileBufferp,(DWORD)_IAT_new - (DWORD)newFileBufferp);
//新增一个IMAGE_IMPORT_BY_NAME结构
_image_import_by_name_new = (_IMAGE_IMPORT_BY_NAME*)moveStartp;
_image_import_by_name_new->Hint = 0;
strncpy((char*)_image_import_by_name_new->Name,API,strlen(API) + 1);
moveStartp += (2 + strlen(API) + 1);
//修改INT表和IAT表(运行前)中的元素值
_INT_new->Function = FOA_to_RVA(newFileBufferp,(DWORD)_image_import_by_name_new - (DWORD)newFileBufferp);
_IAT_new->Function = FOA_to_RVA(newFileBufferp,(DWORD)_image_import_by_name_new - (DWORD)newFileBufferp);
//存储DLL名称字符串
char* dllNamep = moveStartp;
strncpy(dllNamep,dllName,strlen(dllName) + 1);
moveStartp += (strlen(dllName) + 1);
//DLL名称字符串RVA赋值给导入表结构中的Name
new_importTablep->Name = FOA_to_RVA(newFileBufferp,(DWORD)dllNamep - (DWORD)newFileBufferp);
//修正导入表数据目录中的VirtualAddress和Size
_image_data_directory_new->Size = _image_data_directory->Size + 20;
_image_data_directory_new->VirtualAddress = FOA_to_RVA(newFileBufferp,moveStartAddress - (DWORD)newFileBufferp);
return newFileBufferp;
}
int main(int argc, char* argv[])
{
char* filePath = "D:/IPMsg/ipmsg.exe";
char* fileBufferp = to_FileBuffer(filePath);
char* dllName = "InjectDll.dll";
char* API = "ExportFunction"; //如果有很多API,可以用数组传入,再去遍历,但这里可以偷个懒
char* newFileBufferp = importTable_injectDll(fileBufferp,dllName,API);
//将注入后的PE文件存盘
DWORD size = compute_NewBuffer_size(newFileBufferp);
char* storagePath = "D:/IPMsg/ipmsg_inject.exe";
int isSucceed = save_to_disk(newFileBufferp,storagePath,size);
if(!isSucceed){
printf("存盘失败");
getchar();
}else
printf("存盘成功");
free(fileBufferp);
free(newFileBufferp);
return 0;
}
说明:
- 还需要把要注入的DLL文件,存放到ipmsg_inject.exe所在目录下!
- 在调试过程中,本人由于在某些地方设置断点,但是每次调试后,都没有完整的执行完整个程序,所以导致
free(newFileBufferp)
每次都没有执行,即开辟的堆一直再增加没有释放,所以后面导致malloc一个过大的堆时失败!经过这次经验教训,以后调试时要主要堆空间的释放- 最后一点!在新增节或扩大节的修改节属性这一步中,最保险的办法就是把每个节的属性全包括(这里的错误找了好久!!前面学习过程中都是满足可读可执行、包含可执行代码就可以了,即0x60000020,但这里不行!必须把所有节的属性都包含,0xC0000040才行)这里的错误我找了好久!!代码调试了几遍,看内存数据都没问题,就是成功不了,后面发现正常ipmsg.exe的导入表的RVA、名字字符串RVA、或者其他的相关RVA所在的节都是很分散的,而且所在节的属性也都不太一样,那由于我们是在新节中或者最后一个节中新增导入表、INT、IAT表等,那么就需要保证这个集中存放的节的属性要满足正常情况下所在的各节的属性取并集
3.特洛伊注入说明
这里简单说一下特洛伊注入原理,后面学完线程再详细学习
- 又称DLL拦截
- 一个.exe根据导入表信息知道要装载哪个DLL,但是装载前必须要找到DLL文件在哪里?而操作系统会先在.exe当前所在目录(文件夹)下搜索DLL,如果搜不到再去系统目录(
C:\Windows\
)中去搜索 - 所以假如现在已知.exe会装载一个名叫
kernel32.dll
的DLL,接着我们自己也写一个名叫kernel32.dll
的DLL,且自己写的DLL中的函数也和kernel32.dll
中的函数一模一样(当然自己的DLL中还可以写一些其他功能的函数),然后再把自己写的DLL放到.exe程序所在目录中,那么.exe程序根据导入表装载kernel32.dll
时,会先在程序当前所在目录中查找,所以会把我们写的kernel32.dll
装载进去,但是为了防止程序可能奔溃,所以还要在自己写的DLL中把正常的kernel32.dll
加载进去、做一个函数转发:即如果系统要调用kernel32.dll
中的函数,肯定会先调用我们自己写的kernel32.dll
中同名函数,接着再跳到正常的kernel32.dll
中此函数中去。 - 按照系统如果调用原来的
kernel32.dll
中的函数会先调用自己写的DLL中的函数的原理,我们就可以做到DLL拦截:因为系统调用kernel32.dll
中任何函数时都会先经过自己写的DLL,所以我们就可以在自己写的DLL中做一些手脚,比如改改参数再传给正常的kernel32.dll
等