这将是本网站的第一篇博文。我选择 DLL Hollowing 作为我的第一篇博文,因为我之前没有看到任何关于使用 rust-lang 进行 DLL Hollowing 的文章,所以我决定讨论一下这个主题。这个项目与其他项目略有不同,因为我们没有创建线程。我们只是更改活动模块的权限,并将我们的 shellcode 注入到该模块中,而无需更改头文件。当执行此 shellcode 时,它将加载你的 DLL 并创建真正的目标进程。
开始之前,您可以在这里访问我的代码。
要求
对于此设置,您将需要以下内容:
-
Rust 编程语言(https://www.rust-lang.org/)
-
DLL Hollowing 项目(https://github.com/kuzeyardabulut/rust-dll-hollowing)
设置
首先,您应该克隆这个存储库,然后编译如下代码:
cd rust-dll-hollowingcargo build --release
现在我们编译这个项目,但我们需要一个调用LoadLibrary函数的 shellcode。为了创建 shellcode,我们将使用msfvenom。在 bash 终端中执行以下命令。
msfvenom -p windows/x64/loadlibrary DLL=C:\Users\Public\in.dll PrependMigrate=true PrependMigrateProc=explorer.exe -f rust ‐-bad-chars 'x00x0ax0d'
如果您成功完成所有这些步骤,那么您就准备好了!
注射方法
当你阅读那些广为人知的 Windows 注入方法时,你会发现许多恶意软件开发和游戏作弊的方法。你可以用很多方法运行你的 DLL,但在加载或运行这些 DLL 时,你的注入器应该是无法检测到的。通常,许多基本的和已知的注入方法都可以被反病毒软件检测到。
图1:常见的注射方法
因此,攻击者通常会尝试通过对已知方法进行细微修改来使其无法被检测到,这给网络安全专家的工作带来了困难。为此,我将描述一种经过我修改后得以区分的方法,以便网络安全专家能够更好地理解攻击者的视角。
模块踩踏概述
Module Stomping 是一种常见的注入方法。在这种技术中,你将 Shell 代码注入到 Legit DLL 下。这样,你的 Shell 代码就会在 Legit DLL 下运行。
首先,注入器使用VirtualAllocEx函数分配空间,然后使用WriteProcessMemory函数将 DLL 的路径写入分配的空间,以便将合法 DLL(例如:kernel32.dll、amsi.dll)加载到目标进程。之后,注入器调用CreateRemoteThread函数创建线程。如果注入器成功创建线程,它将挂起该线程。之后,注入器将计算目标合法 DLL 的AddressOfEntryPoint (指向.text段的开头),并将 Shellcode 写入该计算出的地址。
-
VirtualAllocEx:它在指定进程的虚拟地址空间内的内存区域下分配空间。如果目标地址为null,则该函数确定在何处分配区域。
-
WriteProcessMemory:要写入的整个字段必须是可访问且空闲的,否则操作将失败。
-
入口点地址 (AddressOfEntryPoint ):入口点地址是可执行文件中一个特定的内存地址,程序加载到内存后,从这里开始执行。它是操作系统开始执行程序指令的入口点。我们将在本文的代码审查部分中,根据 NT 头文件计算这个地址。
-
CreateRemoteThread:CreateRemoteThread 是 Windows 操作系统中的一项功能,它允许一个进程在另一个进程中创建一个线程,从而使其能够在该远程进程的上下文中执行代码。
-
ResumeThread:ResumeThread 是 Windows 操作系统中的一项功能,允许暂停的线程恢复执行。
至此,我们已经完成了所有注入过程,只需使用ResumeThread函数更改线程的 Suspend 状态即可。完成后,您的 Shellcode 将在合法 DLL 下运行。这些合法 DLL 的头文件将保持不变,从而使恶意 Shellcode 难以被检测到。
图 2:Shellcode 在 amsi.dll 下运行
模块踩踏的利与弊
与任何方法一样,此方法也有其优缺点。与任何方法一样,此方法也有其优缺点。这些技术的优缺点通常由网络安全专家进行审查,并编写相应的检测器。
优点:
-
你的 Shellcode 在合法的 DLL 下运行。这使得它不太可能被检测到。
-
您不需要再次使用VirtualProtectEx,因为您已经在分配空间时授予了适当的权限来写入 shellcode。
-
由于您在线程挂起时执行注入操作,因此检测的可能性可能会降低。
缺点:
-
您将合法的 DLL 加载到合法的进程中。乍一看这似乎很正常,但我们不要忘记,我们的注入器(一个非合法进程)正在尝试执行所有这些操作,并且在注入器执行这些操作时会使用许多函数(VirtualAllocEx、CreateRemoteThread等)。
-
在此过程中会创建一个额外的、不寻常的线程。
AddressOfEntryPoint 注入概述
AddressOfEntryPoint 注入与 Module Stomping 略有不同,因为执行该注入方法时,无需从内存中分配任何空间,也无需创建或暂停线程。您可以通过更改现有模块的权限来直接编写 Shellcode。
您可以在我们文章的代码审查部分查看此技术代码。https://kuzey.rs/posts/RustStomping/#code-review
首先,你必须设置一个目标模块。如果这个模块不作为线程运行可能会更好。其次,你应该检查模块的权限。如果被检查模块的权限不是PAGE_EXECUTE_READWRITE ,则使用VirtualProtectEx函数更改模块的权限。其次,使用ReadProcessMemory函数读取进程内存,然后根据返回值计算NTHeaders 。然后根据NTHeaders计算AddressOfEntryPoint。最后,使用WriteProcessMemory函数将你的 shellcode 写入AddressOfEntryPoint地址( .text部分的开头),你的注入过程就完成了。现在,当进程尝试使用你注入的 shellcode 的模块时,你的 shellcode 将会运行。
图 3:注入 urlmon.dll 的 Shellcode
AddressOfEntryPoint 注入的优缺点
这种方法与模块踩踏相比也有优点和缺点。
优点:
-
你的 Shellcode 在合法的 DLL 下运行。这使得它不太可能被检测到。
-
您不需要使用VirtualAllocate。
-
您不需要先加载 Legit DLL。
-
您不需要创建线程。
缺点:
-
您需要更改现有模块的权限。
-
您将要更改正在运行的模块的权限。
代码审查
在讨论了技术的相似之处之后,我们现在可以转到代码的审查部分。
当我们第一次查看 Github 中的 Workspace 时,我们会看到两个不同的项目,分别名为“encrypt shellcode”和“injector”。“encrypt shellcode”项目会加密我们的 shellcode 并打印出来。您可以自行查看。“injector”项目是我们的AddressOfEntryPoint Injector。因此,本文我们将重点关注“injection”项目。
main.rs
在主文件中,我们完成了所有注入操作。现在我将逐一解释代码片段。开始吧!
在文件的开头,导入了必要的库,确定了 const 变量,并写入了一个名为print_permission的宏。我们先来解释一下这些变量。这些变量名为BUF和TARGET_PROCESS_NAME。TARGET_PROCESS_NAME包含将注入 Shellcode 的进程的名称。BUF是 Shellcode 的加密版本。如果我们查看宏,会发现这个宏尝试使用VirtualProtectEx函数更改目标模块的权限。
图 4:main.rs 的头部
现在,我们有一个名为everything的函数。在这个函数中,我们使用我们的函数来解码你的 AES 加密的 shellcode。
图 5:一切
现在我们开始注入!首先,我们通过进程名称获取目标进程 ID。然后,我们获取模块的基址,并使用OpenProcess函数打开目标进程。然后,我们保存进程句柄并计算MEMORY_BASIC_INFORMATION。
图 6:计算 MemoryInfo
我们使用VirtualQueryEx函数从模块基址获取MEMORY_BASIC_INFORMATION。然后,我们尝试控制内存保护机制,并使用打印权限函数(也就是我们开头提到的函数)来修改内存保护机制。之后,我们使用ReadProcessMemory函数获取内存信息,并计算AddressOfEntryPoint。
图 7:更改权限并获取 MemoryInfo
我们读取了内存并获得了DOS_HEADERS。如果我们想获取AddressOfEntity ,我们需要计算NT_HEADERS。如果我们想计算NT_HEADERS,我们应该使用DOS_HEADER下的e_lfanew。
-
e_lfanew:这是 PE(可移植可执行文件)文件中 DOS 头结构的成员。该成员是一个 32 位有符号整数,位于 DOS 头的偏移量 0x3C(十进制 60)处。“e_lfanew”指定 NT(新技术)头在文件中的起始偏移量。PE 加载器使用“e_lfanew”中的值来定位 NT 头的起始位置,然后解析文件头以了解可执行文件的结构。这个值对加载器来说很重要,因为它决定了在何处查找文件头。
因此,我们将使用e_lfanew收集基址,并找到NT_HEADERS的起始地址。找到NT_HEADERS后,我们将计算AddressOfEntryPoint。之后,我们将基址和AddressOfEntryPoint相加,就能得到.text段的起始地址。
我们将打印出我们发现的重要地址,并使用WriteProcessMemory函数将我们的 shellcode 写入内存( .text部分开始的位置)。
图 8:编写 Shellcode
当你的模块被使用时,shellcode 将在目标机器上运行。这样,shellcode 将在合法 DLL 下运行,使其更难被检测到。让我们查看security.rs来了解安全保护措施。
security.rs
现在,我们将回顾一下我们的安全保护措施。首先,我们导入库。然后,我们编写一个名为anti_debugger的函数。此函数检查调试器是否正在运行,以确保二进制文件不被调试器检查。
图 9:检查调试器
检查完调试器之后,我们会主动检查系统中正在运行的进程,如果有一些不需要的进程正在运行,我们就关闭那些没有注入的程序。
图 10:检查进程
您可以在下方看到不需要的进程列表。此外,您还可以将某些进程名称添加为不需要的进程。
图 11:不需要的进程列表
encrypt.rs
使用msfvenom创建并嵌入到项目中的Shellcode很容易被杀毒软件解析。但是,如果这些 Shellcode 经过编码,则几乎不可能通过静态分析找到它们。
因此,我们将使用encrypt_shellcode项目加密我们的 shellcode 。然后,我们将加密的 shellcode 嵌入到Injector项目中。程序启动时,主函数将使用密钥调用解码器。之后,解码器将接收加密的 shellcode 并检查 shellcode_len。如果 shellcode_len 大于516字节,则解码器将接收前516字节并解码这516字节。之后,解码器将减去516字节,也就是我们之前计算文件大小时得到的剩余空间。如果剩余空间大于 516 字节,则以相同的方式继续执行。当剩余空间小于 516 字节时,所有剩余空间(无论大小)都将被读取并解码。
图 12:Shellcode 解码器
结论
因此,在本文中,我们研究了 DLL 注入方法,并分析了我们用 Rust 开发的AddressOfEntryPoint 注入器。我们解释了main.rs中的重要代码片段。您可以从我的Github上查看其他代码https://github.com/kuzeyardabulut/rust-dll-hollowing/。如果您有任何不明白的代码,可以通过我的社交媒体地址联系我。
谢谢阅读
原文始发于微信公众号(Ots安全):Rust 语言的 DLL Hollowing:隐形注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论