DLL隐藏

属于是看着别人的文章,用着别人的工具在这儿玩儿得不亦乐乎了。

1.何为DLL?

1
2
动态链接库英文为DLL,是Dynamic Link Library的缩写。DLL是一个包含可由多个程序,同时使用的代码和数据的库。例如,在 Windows 操作系统中,Comdlg32.dll 执行与对话框有关的常见函数。因此,每个程序都可以使用该 DLL 中包含的功能来实现“打开”对话框。这有助于避免代码重用和促进内存的有效使用。
通过使用 DLL,程序可以实现模块化,由相对独立的组件组成。例如,一个计帐程序可以按模块来销售。可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。

​ 总而言之就是DLL这种技术可以使得程序模块化,同时让程序加载速度更快。

详细解释见下面的链接:

https://encyclopedia.thefreedictionary.com/Dynamic-link+library

2.为啥要隐藏DLL?

​ 一句话总结:没做啥坏事,干嘛隐藏自己呢?所以隐藏了自己,那有很大的可能性是要做坏事:

​ 1.木马以DLL形式注入进正常进程并隐藏自己,导致无法追踪溯源,如键盘记录木马,感染型木马

​ 2.游戏外挂通过该种方式躲避游戏自身的“模块检测”,如DXF的第三方木马检测

​ 除了坏事儿之外,在一些常见的攻防对抗中,如若用到windows木马,不仅要注意免杀工作,有时也需要通过该技术实现持久性隐藏攻击。

3.DLL隐藏常见手法

​ 一.手动载入

​ 所谓手动载入就是自己去实现LoadLibrary函数,从理论上讲,这种方法隐藏效果是最棒的,但是实现一个基本的PEloader并不是一件容易的事情,如果还要考虑兼容性完美,就必须还得妥善处理TLS,资源,线程等问题。(不太易上手)

​ 二.痕迹消除

​ 这个就比较好说了,我们在注入DLL的时候,通常要选择去调用 LoadLibrary函数,然后再擦除痕迹,这样不用去处理那些TLS,资源,线程等为了解决兼容性产生的问题。但是Windows大家都清楚,表面风平浪静,背地里风起云涌,说不准现在你电脑的XXX安全卫士正在对你的电脑做些什么呢,这些都是你不通过技术手段看不到的东西,实际上没有什么好的方法把痕迹真正的消除。

​ 以DLL擦除痕迹为例,网上大多数的方法都是通过PEB双向断链,需要很多的硬编码,麻烦不说,通过内存暴力搜索还是会露出尾巴来

​ 攻防无绝对,攻防本身就是一个提升本身技术的过程,只要能够通过一个比较合适的方法,实现相应的功能,便具有可行性。

4.DLL编写与注入测试

​ 一.DLL编写:

​ ①.于vs中创建一个空的项目。

​ ②.添加C语言代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Windows.h>

BOOL WINAPI DllMain(HMODULE hDll, DWORD dwReason, LPVOID lpReserved)
{
DisableThreadLibraryCalls(hDll);

if (dwReason == DLL_PROCESS_ATTACH)
{
//================================== OPTIONAL =========================================
MessageBoxA(0, "Message Test","Message Title", 0);
}
return 1;
}

​ ③.同时在项目属性中设置:

image-20231007111516385

image-20231007111556604

​ ④.而后对文件进行编译生成即可。

​ 二.DLL注入测试:

​ 见演示。

image-20240606170519264

由图可见进程中已经注入进入了我们的DLL,且内存地址为0x78EB0000

​ 三.手写代码实现注入

​ 谈完DLL编写和利用工具注入测试,我们来大致了解一下代码实现注入,方法有很多,但原理大致相同,我在这里以线程注入dll为例:

​ 代码编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <Windows.h>

void Inject(int pID, char* Path)
{
//获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pID);

//申请一块内存给DLL路径
LPVOID pReturnAddress = VirtualAllocEx(hProcess, NULL, strlen(Path) + 1, MEM_COMMIT, PAGE_READWRITE);

//写入路径到上一行代码申请的内存中
WriteProcessMemory(hProcess, pReturnAddress, Path, strlen(Path) + 1, NULL);


//获取LoadLibraryA函数的地址
HMODULE hModule = LoadLibrary("KERNEL32.DLL");
LPTHREAD_START_ROUTINE lpStartAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA");


//创建远程线程-并获取线程的句柄
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, lpStartAddress, pReturnAddress, 0, NULL);

//等待线程事件
WaitForSingleObject(hThread, 2000);


//防止内存泄露
CloseHandle(hThread);
CloseHandle(hProcess);

}

int main()
{
//传dll路径
const char* a = "(传入dll所在路径)";

//传入进程ID
Inject(传入进程id, (char*)a);

return 0;
}

代码解析:

​ 首先,通过调用OpenProcess函数获取目标进程的句柄(hProcess)。OpenProcess函数使用了PROCESS_ALL_ACCESS参数,表示打开进程时拥有最高权限。然后,通过VirtualAllocEx函数在目标进程中申请一块内存(pReturnAddress),用于存储DLL文件路径。接着,使用WriteProcessMemory函数将DLL文件路径写入到刚才申请的内存空间中。

之后,通过调用LoadLibrary函数加载KERNEL32.DLL库,并使用GetProcAddress函数获取LoadLibraryA函数的地址(lpStartAddress)。

接下来,使用CreateRemoteThread函数在目标进程中创建一个远程线程,该线程将在目标进程的上下文中执行lpStartAddress函数,并将pReturnAddress作为参数传递给它。这样,目标进程就会执行LoadLibraryA函数,将DLL文件路径作为参数加载到自己的地址空间中。

最后,通过调用WaitForSingleObject函数等待远程线程事件的发生,直到线程执行完毕或超时。

整个过程完成后,关闭线程和进程句柄,释放相关资源,避免内存泄漏。

根据传入参数,记录我们要传入的数据

进程ID:xxxx

路径:xxxxxx

image-20240606170551892

​ 而后将两个参数写入到程序对应的位置当中去,生成exe后丢到虚拟机中即可实现dll注入(此步还未复现)

成功注入之后,应该会弹出信息框,通过分析源代码,我们总结了一下注入流程:

1.在别人的程序里开辟内存空间A

2.将 LoadLibrary 函数参数写入A

3.获取LoadLibrary函数地址

4.在别人的程序里远程执行 LoadLibrary实现加载外部DLL

由此可见 LoadLibrary很重要,这个玩意竟然实现了注入!