Windows系统大量使用dll作为组件复用,应用程序也会通过dll实现功能模块的拆分。DLL注入技术是向一个正在运行的进程插入自有DLL的过程。

Window下的代码注入

常见的Windows代码注入方法如下:

  • 注册表注入

    编译注册表中的AppInit_DLLs选项,凡是使用GUI的进程,都会读取AppInit_DLLs内容,加载这些Dll。

  • Windows Hook注入

    使用 SetWindowsHookEx、UnHkkkWindowsHookEx 来进行,为目标进程安装钩子,在注入dll中监听目标进程消息。

  • 远程线程注入

    使用 CreateRemoteThread 函数在目标进程中创建线程,在该线程中加载注入dll。

  • DLL函数转发

    使用伪造的dll来替换目标dll,两个dll的导出符号完全相同,在自定义DLL中,先利用函数转发器将请求转发到真实dll中,然后进行自己的一些处理。

在本篇文章中,主要介绍 Windows Hook注入 这一种方式。在具体介绍之前,先介绍下Dll的加载顺序、加载过程。

DLL加载顺序

系统在搜索加载指定DLL之前,按照如下顺序做检查:

  1. 如果同名DLL已在内存中加载,则直接使用
  2. 如果DLL在系统DLL列表中,系统直接使用已知DLL的拷贝
  3. 如果DLL依赖其他DLL,系统会按照名字搜索并加载依赖的DLL,待依赖的DLL加载完毕后再加载自身。

系统已知DLL列表配置位于注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs选项中,本机上的内容如下:

标准的DLL搜索顺序取决于系统安全DLL搜索模式,该模式默认使能。按照如下顺序搜索DLL:

  1. 进程对应的应用程序目录,可通过 GetModuleFileName 获得,程序启动后为固定值。
  2. Windows系统目录,一般为 C:\Windows\system32
  3. Windows目录,一般为 C:\Windows
  4. 当前进程目录,通过 GetCurrentDirectory 获得,程序可通过 SetCurrentDirectory 进行修改。
  5. PATH环境变量中的目录

DLL加载过程

DLL加载分为隐式加载和显示加载。

隐式加载既为在编译链接选项中增加导入库,在程序运行目录存放待加载的dll。双击程序启动时,由系统加载程序根据exe中的导入表加载对应dll到进程空间,若dll有依赖其他dll的,会递归加载直到加载完成所有必需的dll,然后进行exe的导入函数地址重定位,使得能够调用dll的导出函数。

显示加载指的是由应用程序按需加载,具体为调用 LoadLibraryFreeLibraryGetProcAddress这三个API函数,加载并获取dll的导出函数地址。

加载器执行流程

  1. 为进程创建虚拟地址空间
  2. 把可执行模块映射到进程地址空间中
  3. 检查可执行模块的导入段,根据DLL搜索规则,找到所需的DLL并加载。如在此阶段,未找到需要的dll,弹出"无法启动,因为计算机中缺失XXX.dll"
  4. 检查dll的导入表(IAT),如果该dll还依赖其他的dll,那么继续去定位所需的dll并加载.
  5. 修复导入符号的引用符号。具体做法,遍历所有模块的导入段,针对每个导入符号,加载程序在导出段中检查是否存在匹配的导入符号,若存在,则取导出符号的RVA,加上对应dll的基址,得到导出符号的真实地址,填入对应的导入表中。如在此阶段未找到对应的导出符号,弹出"程序入口点XXX无法定位到动态链接库xxx.dll上"
  6. 可执行模块运行期间如果调用到某个dll的导出函数,则会跳转到IAT,得到导出函数的地址,然后进行调用。

使用CreateRemoteThread注入DLL

使用CreateRemoteThread可使得目前进程创建线程,但要加载注入dll,需要在目标线程的虚拟地址空间申请内存,用于保存目标dll的名称,通过 GetProcAddress 函数在远程线程中 kernel32.dll 模块的LoadLibrary函数地址,用于远程线程的入口函数,主要流程如下:

  1. 获取 LoadLibrary 函数的地址
  2. 调用 VirtualAllocEx 函数在远程进程中申请一段虚拟内存
  3. 调用 WriteProcessMemory 将 待加载的dll名称写入虚拟内存 ReadProcessMemory 读写进程地址内容
  4. 调用 CreateRemoteThread 创建远程线程,回调函数为 LoadLibrary,参数为对应字符串地址
  5. 调用 VirtualFreeEx 释放远程虚拟内存

执行完自己的函数后,就要远程卸载dll,思路与注入类似,函数变为FreeLibrary,传入参数对对应dll的句柄。

获取已加载的模块句柄(通过EnumProcessModule实现)

注入代码示例:


// 获得指定进程名称的进程PID
int GetProcessId(const char* pName)
{
PROCESSENTRY32 pe;
DWORD id = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
pe.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hSnapshot, &pe))
return 0; while (1)
{
pe.dwSize = sizeof(PROCESSENTRY32);
if (Process32Next(hSnapshot, &pe) == FALSE)
break; if (strcmp(pe.szExeFile, pName) == 0)
{
id = pe.th32ProcessID;
break;
}
} CloseHandle(hSnapshot);
return id;
} //利用远程线程来注入dll
bool RemoteThreadDllInject()
{
////提权代码,在Windows Vista 及以上的版本需要将进程的权限提升,否则打开进程会失败
if (!SetDebugPrivilege(TRUE))
{
printf("提升权限失败\n");
return false;
} int nPid = GetProcessId(INJECT_EXE_NAME); // 打开目标进程
HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
if (NULL == hRemoteProcess)
{
printf("OpenProcess failed! %d", GetLastError());
return false;
} // 获得 LoadLibraryW 在 kernel32.dll 中的地址
typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR);
// 这里注意要载入宽字节版本还是普通版本的 LoadLibrary
pfnLoadLibrary pfnThreadRtn2 = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "LoadLibraryA");
// 在远程进程中申请内存空间,用于保存远程线程的参数
LPVOID lpRemoteMemory = VirtualAllocEx(hRemoteProcess, 0, MAX_PATH, MEM_COMMIT, PAGE_READWRITE); string strInjectDllName(INJECT_DLL_NAME);
DWORD nWritten = 0;
BOOL bRet = WriteProcessMemory(hRemoteProcess, lpRemoteMemory, strInjectDllName.c_str(), strInjectDllName.length() + 1, &nWritten); HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pfnThreadRtn2, lpRemoteMemory, 0, NULL);
WaitForSingleObject(hRemoteThread, INFINITE);
VirtualFreeEx(hRemoteProcess, lpRemoteMemory, 0, MEM_RELEASE); CloseHandle(hRemoteThread);
CloseHandle(hRemoteProcess); return true;
}

远程卸载dll示例代码:

// 获得指定进程内指定模块信息
bool GetProcessModule(DWORD dwPid, string strModuleName, LPMODULEENTRY32 lpMe32, DWORD cbMe32)
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
if (INVALID_HANDLE_VALUE == hSnapshot)
{
printf("CreateToolhelp32Snapshot Error");
return false;
} bool bFind = false;
MODULEENTRY32 moduleInfo = {0};
moduleInfo.dwSize = sizeof(MODULEENTRY32);
if (Module32First(hSnapshot, &moduleInfo))
{
do
{
if (strModuleName == string(moduleInfo.szModule))
{
memcpy(lpMe32, &moduleInfo, cbMe32);
bFind = true;
break;
}
} while (!bFind && Module32Next(hSnapshot, &moduleInfo));
} CloseHandle(hSnapshot); return bFind;
} bool RemoteDllUnLoad()
{
//在远程线程执行结束后,注入的 dll 仍然存在与目标进程中,我们需要再次使用 CreateRemoteThread,执行 FreeLibrary ,将之前注入的 dll 卸载掉
// 实现思路:枚举进程的模块,根据模块名称找到对应模块的句柄。
int nPid = GetProcessId(INJECT_EXE_NAME); ////提权代码,在Windows Vista 及以上的版本需要将进程的权限提升,否则打开进程会失败
if (!SetDebugPrivilege(TRUE))
{
printf("提升权限失败\n");
return false;
} MODULEENTRY32 moduleInfo = {0};
if (!GetProcessModule(nPid, INJECT_DLL_NAME, &moduleInfo, sizeof(moduleInfo)))
{
printf("can't find %s dll in %s", INJECT_DLL_NAME, INJECT_EXE_NAME);
return false;
} typedef BOOL(*pfnFreeLibrary)(HMODULE);
pfnFreeLibrary pFreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle("kernel32.dll"), "FreeLibrary"); HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
if (hRemoteProcess == NULL)
{
printf("OpenProcess Error");
return false;
} HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFreeLibrary, moduleInfo.hModule, 0, NULL);
WaitForSingleObject(hRemoteThread, INFINITE); CloseHandle(hRemoteThread);
CloseHandle(hRemoteProcess);
return true;
}

注意事项

  • 远程进程可能以Unicode编码编译,也可能是MBSC编码编译,因此,要区分 LoadLibraryW 还是 LoadLibraryA 版的函数,FreeLibrary函数无需区分不同版本。
  • 在获得目标进程的句柄时,需要对自身程序进程提权操作。一般需要当前登陆用户属于Administrator组成员,才有能力提升至调试权限。Administrator组成员的access token中会含有一些可以执行系统级操作,注意,非Administrator组成员创建的进程 无法提升自身的权限。如果提权失败,建议通过管理权权限打开VS开发环境。
  • 在注入时,需要将待注入的dll放在远程进程同一目录下。
  • 验证注入,可通过在dllMain函数的加载和卸载分支中弹框提示,也可以通过 procexp64.exe 程序来查看程序当前加载的dll信息。

工程实践

如何获得运行过程中不符合预期的dll

  1. 通过读取exe的PE头,获得依赖的dll列表,遍历读取dll依赖的dll,得到集合1
  2. 在程序运行过程中,调用 EnumProcessModules 得到当前加载所有dll,得到集合2
  3. 从集合2中排除掉集合1的内容,得到生成集合3,即为不符合预期的dll。

一般来说,输入法(搜狗输入法的皮肤组件PicFace.dll、资源组件Resource.dll等)、监控软件的dll会自动加载到所有进程的dll中去。

模块基址重定位

每个模块(exe和dll)在编译输出时,都有一个首选基址,它指示加载器将模块映射到进程地址空间中的首选位置,一般exe的基址设定为 0x00400000,dll模块为 0x10000000.当一个exe依赖于多个dll时,第一个dll被正确的加载到 0x10000000上,随后的dll就不能在加载到0x10000000上,加载程序会对随后的dll进行基址重定位,把它放到别的地方。基址重定位会增加程序初始化时间,因此,如果将多个模块载入同一进程空间,可以给不同模块指定不同的基址。在VS开发环境中,基址配置方式:DLL工程的配置属性-->链接器-->高级-->基址。

在所有dll编译完成后,使用 Rebase.exe 工具,对需要载入进程地址空间的所有模块进行基址重定位,将结果写回到dll文件中。

一旦一个dll模块的基址已知,那么可直接推算出exe在使用该DLL的导出函数的真实地址。这种预先将exe和所依赖的dll绑定在一起的做法,可以提高应用程序的启动速度。

VS提供 Bind.exe程序提供了绑定可执行文件与dll的功能。工作原理为,读取所有dll的基址和导出符号的RVA,计算后填充到可执行文件的导入表中。

参考文档:多种DLL注入技术原理介绍

DLL注入实践的更多相关文章

  1. DLL注入新姿势:反射式DLL注入研究

    在分析koadic渗透利器时,发现它有一个注入模块,其DLL注入实现方式和一般的注入方式不一样.搜索了一下发现是由HarmanySecurity的Stephen Fewer提出的ReflectiveD ...

  2. Ring3下的DLL注入(NtCreateThreadEx + LdrLoadDll方式实现,可以注入系统进程)

    工具介绍及使用请移步:http://blog.csdn.net/sunflover454/article/details/50441014 本文首发在零日安全论坛:http://www.jmpoep. ...

  3. DLL注入

    最近的项目涉及了软件破解方面的知识,记录一下. 将dll注入另一个进程. // Inject.cpp : Defines the exported functions for the DLL appl ...

  4. [转]Dll注入经典方法完整版

    Pnig0s1992:算是复习了,最经典的教科书式的Dll注入. 总结一下基本的注入过程,分注入和卸载 注入Dll: 1,OpenProcess获得要注入进程的句柄 2,VirtualAllocEx在 ...

  5. 第22章 DLL注入和API拦截(2)

    22.4 使用远程线程来注入DLL 22.4.1 概述 (1)远程线程注入是指一个进程在另一个进程中创建线程,然后载入我们编写的DLL,并执行该DLL代码的技术.其基本思路是通过CreateRemot ...

  6. 第22章 DLL注入和API拦截(1)

    22.1 注入的一个例子(跨进程子类化窗口) ①子类化窗口可以改变窗口的行为,让发往该窗口的消息重新发到我们指定的过程来处理.但这种行为只能在本进程中(如A),对于从一个进程(如B)去子类化另一个进程 ...

  7. DLL注入之注册表

    0x00 唠叨 编写本系列文章纯属为了系统学习DLL注入的方法,所以很多方法可能已经过时,希望各位看官勿喷.当然若有更好的方法,希望不腻赐教.若本文有任何错的地方,也希望各位指正.谢谢! 0x01 适 ...

  8. c++实现dll注入其它进程

    DLL注入技术才具有强大的功能和使用性,同时简单易用,因为DLL中可以实现复杂的功能和很多的技术. 技术要点: 1.宿主进程调用LoadLibrary,就可以完成DLL的远程注入.可以通过Create ...

  9. DLL注入_拦截技术之Hook方式

    后卫大师教你进程注入 首先提一下,由于文章完全是我手写,所以打不了太多,请包含,由于我已经提供了源代码,所以我在这里详细讲一下理论,至于想看代码的下载代码就可以了.代码中关于注入的部分做了详细的注释. ...

随机推荐

  1. Java动态代理--CGLib实现

    上一篇说了通过JDK实现实现了动态代理,但是用JDK实现代理,要求所代理的类必须实现一个接口.如果我们类不实现接口,这就在无法使用JDK提供的Proxy来实现动态代理,这个时候我们应该怎么办呢?我们找 ...

  2. python基础知识-列表的排序问题

    def main(): f=['orange','zoo','apple','internationalization','blueberry'] #python 内置的排序方式默认为升序(从小到大) ...

  3. QT工程中添加资源(简单明了)

    1. 在工程文件下右击添加新文件 2. 在QT目录下选择QT Resource File 3. 填写资源名称 4. 点击完成就可以看到自己建立的资源了 5. 点击右键添加现有文件,找到自己要添加的资源 ...

  4. http编程体系结构URL loading system

    https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadi ...

  5. websocket 浅学

    websocket ,可以实现客户端与服务器的即时通讯,比如即时聊天,获取项目中的消息提醒等(小铃铛上的标红数字提醒) 话不多说,上马奔腾,走起 只写demo,不在项目中使用: 1.初始化项目, np ...

  6. kuma 学习四 策略

    通过策略我们可以构建灵活的service mesh 应用策略 我们可以通过kumactl 以及kubectl 应用策略 kumactl 格式 echo " type: .. spec: .. ...

  7. artillery强大灵活的负载测试套件

    artillery是基于nodejs 编写的负载测试套件支持http&&socket.io&&websockets&&kinesis&& ...

  8. Mean Average Precision(mAP),Precision,Recall,Accuracy,F1_score,PR曲线、ROC曲线,AUC值,决定系数R^2 的含义与计算

    背景   之前在研究Object Detection的时候,只是知道Precision这个指标,但是mAP(mean Average Precision)具体是如何计算的,暂时还不知道.最近做OD的任 ...

  9. ELK实时日志分析平台环境部署,以及可视化展示

    ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是开源软件.新增了一个FileBeat,它是一个轻量级的日志收集处理工具(Agent) ...

  10. 分享一个Python脚本--统计redis key类型数据大小分布

    概述 今天主要介绍怎么统计redis key类型数据大小分布. 原理:使用redis命令: scan.pipline.type 和 debug object 来得到 redis key 信息. 脚本 ...