概述
这是一篇关于编写我所称的“现代”位置无关植入程序的小文章。“现代”的含义是:易于编写、易于维护、灵活且模块化。这些现代植入程序为位置无关代码增加了对全局变量、原始字符串和编译时哈希的支持,而位置无关代码通常存在一些限制,例如无法使用全局变量和字符串字面量(除了构造在栈上的字符串)。
反射式加载器:一个已有十年历史的设计
首先,让我们聊聊反射式 DLL 注入或加载。这项技术已经存在十多年了(ReflectiveDLLInjection),它要求 DLL 导出一个位置无关的函数,通常被称为反射式加载器(Reflective Loader)。这个加载器函数可以通过将 DLL 注入内存并调用该函数来执行,从而使 DLL 自我加载。另一种方法是将反射式加载器直接加在我们想要注入的目标 DLL 前面。不管是哪种方式,最终目的都是一样的:手动将 DLL 映射到当前进程的内存中。
本篇博客不会深入探讨反射式加载器的实现细节,仅限于这种表面级的使用模式。
许多新旧的商业 C2(Command and Control)工具,其核心植入程序都是以反射式 DLL 的形式设计的。原因在于它开发简单、稳定且易于维护,因为它本质上就是一个带导出函数的简单 PE 文件。但这也意味着它遵循一个非常特定且常见的模式:找到导出的反射加载器函数的偏移并调用它,DLL 随后会分配一段新的内存(通过 VirtualAlloc/NtAllocateVirtualMemory 这类虚拟内存分配方式,或模块覆盖等技术),解析自己的 PE 格式,并手动将自身映射到新内存中。
而我对反射式加载器设计的质疑也正是从这里开始的。多年前我在学习这类技术时非常着迷,尝试各种奇怪方式将 DLL 加载到内存中并执行,确实很有趣。但我始终注意到,这种手动将 DLL 映射到内存的过程非常“嘈杂”——整个过程涉及“分配新内存区域”、解析 PE 头、复制节区、设置节区权限、重定位、解析导入表等步骤,仅仅是为了让一个 Payload 在内存中运行,代价就太高了。
于是我开始研究一种新方法,我写了一个反射式加载器,它的节区在编译时就已经对齐好。这样我就不需要再分配新的内存区域,只需进行重定位(需要写权限)、解析导入表,然后设置节区的权限即可。这个方式省去了分配内存的步骤,但我仍然不满足。我开始思考,是否可以完全写出一种不依赖加载器 stub、不依赖特定元数据头(比如 IMAGE_NT_HEADER 或自定义的 PE 信息头部,如重定位信息)的植入方式,从而让程序“就这样”直接执行,而无需大量内存修补。
我不喜欢反射式加载器的另一个原因是:如果没有做好清理,它会在内存中留下很多容易被签名检测识别的数据(比如 NT 头、反射式加载器本身等)。这些鬼东西的签名真是令人头疼。
总之,我已经在一年前放弃使用反射式加载器,转而完全写位置无关的植入程序。但这过程中还是遇到了一些让我头疼的问题,比如需要全局变量实例(用于记录配置、Win32 API 的解析、已加载模块等),而有些情况下又无法通过 context struct 传递,比如在 COFF 加载场景中(更具体地说是加载 beacon 对象文件时),我需要一种方式将 implant 实例传递给 Beacon API(例如 BeaconPrintf、BeaconOutput 或其他需要将数据从对象文件传回服务器的 beacon API)。
于是我设计出了一个解决方案,让我可以在植入程序中的任意位置访问我的全局变量。
接下来,让我们直接进入我称之为 “Stardust” 的设计。
Stardust 设计
Stardust 设计其实非常简单,它的核心思想是将代码和数据的特定部分划分到独立的段(section)和页(page)中。
这是整个植入程序的结构概览:
[ .text$A ] 进行栈对齐、执行入口点、获取基地址的工具函数等。 [ .text$B ] C 层的入口点,植入准备、通信、命令处理、对抗技术等。 [ .rdata* ] 字符串字面量和数据(可能还包括配置信息) [ page align ] 按 0x1000 对齐内存页,使 ".global" 段位于独立页中 [ .global ] 全局变量 [ .text$E ] 用于获取植入代码结尾 RIP 的代码
基本上,这就是整个植入程序的构建方式。我会逐个讲解这些段中包含了什么内容,以及它们各自的作用。我们可以先看看用于划分这些段并正确对齐代码的链接器脚本(linker script):
LINK_BASE = 0x0000; ENTRY( Start ) SECTIONS { . = LINK_BASE; .text : { . = LINK_BASE; *( .text$A ); *( .text$B ); *( .rdata* ); FILL( 0x00 ) . = ALIGN( 0x1000 ); __Instance_offset = .; *( .global ); *( .text$E ); *( .text$P ); } .eh_frame : { *( .eh_frame ) } }
请忽略我写得比较粗糙的链接器脚本,这是我第一次写 linker script。我知道肯定有更好的写法,但这个已经足够让我让这个设计跑起来了,我也欢迎任何反馈和批评。
不管怎样,这段脚本在链接阶段告诉链接器要做以下几件事:
-
按照指定顺序组织各段;
-
包含
.rdata
段,它存放字符串和其他只读数据; -
使用
ALIGN(0x1000)
将下一个段(即.global
)对齐到一个完整的内存页,以便我们后续可以单独修改该页的权限(因为全局变量通常需要写权限); -
并将
.global
段的偏移保存到__Instance_offset
符号中。
接下来我们从第一个段开始讲解,也就是.text$A
,它是我们 PIC(位置无关代码)植入程序的入口点。
;; ;; 主 Shellcode 入口点 ;; [SECTION .text$A] ;; ;; shellcode 的入口点 ;; 将栈对齐到 16 字节,避免调用 Win32 API 时 ;; 由于栈未对齐而崩溃,然后执行真正的 C 层入口点 ;; Start: push rsi ; 保存 rsi mov rsi, rsp ; 把当前栈指针存入 rsi and rsp, 0FFFFFFFFFFFFFFF0h ; 将栈地址向下对齐到 16 字节边界 sub rsp, 020h ; 为函数调用保留栈空间 call PreMain ; 调用 C 层入口点 PreMain(在 .text$B 中) mov rsp, rsi ; 恢复原始栈指针 pop rsi ; 恢复 rsi ret ;; ;; 获取 agent 起始地址的 rip(指令指针) ;; StRipStart: call StRipPtrStart ; 调用获取 rip 的函数 ret ;; ;; 获取 StRipStart 返回地址,并将其放入 rax 寄存器中 ;; StRipPtrStart: mov rax, [rsp] ; 获取返回地址(即调用该函数的下一条指令的地址) sub rax, 0x1b ; 减去指令大小,得到真正的基地址 ret ; 返回到 StRipStart
这段代码的作用是:
-
栈对齐:通过
and rsp, 0xFFFFFFFFFFFFFFF0
语句将栈指针向下对齐到 16 字节,以符合 Windows x64 调用约定要求。如果栈没有正确对齐,调用 Win32 API 时可能会出错或崩溃。 -
调用 PreMain:这是真正的 C 层入口点,位于
.text$B
段中,会负责设置植入程序的运行环境并执行主逻辑。 -
获取 RIP(基地址):
StRipStart
和StRipPtrStart
的组合是用来动态获取当前植入程序在内存中的位置,也就是基地址(position-independent code 的常见技巧)。
正如之前所说,PreMain
位于.text$B
段,这一段包含了用 C/C++ 编写的植入程序核心逻辑。
以下是对这段 C 函数的翻译和解释:
中文翻译:
EXTERN_C FUNC VOID PreMain( PVOID Param ) { INSTANCE Stardust = { 0 }; // 初始化全局实例结构体 PVOID Heap = { 0 }; // 堆指针 PVOID MmAddr = { 0 }; // 内存地址(预留) SIZE_T MmSize = { 0 }; // 内存大小(预留) ULONG Protect = { 0 }; // 内存保护属性(预留) MmZero( &Stardust, sizeof( Stardust ) ); // 清空 Stardust 结构体内存 // // 从 PEB 获取进程堆的句柄 // Heap = NtCurrentPeb()->ProcessHeap; // // 获取当前 implant(植入程序)在内存中的基地址和结束地址。 // 用结束地址减去起始地址,就可以得到 implant 在内存中的大小。 // Stardust.Base.Buffer = StRipStart(); Stardust.Base.Length = U_PTR( StRipEnd() ) - U_PTR( Stardust.Base.Buffer ); // // 设置全局实例变量 // ... // // 清理工作 // ... // // 现在执行 implant 的主入口点 // Main( Param ); }
解释:
这个PreMain
函数负责完成以下关键任务:
-
初始化变量:
-
声明并初始化了
Stardust
结构体,这是一个包含全局配置信息的结构体。 -
还声明了一些用于内存管理的变量,比如
Heap
,MmAddr
,MmSize
,Protect
,虽然它们目前未使用,但可能为后续内存操作做准备。
-
-
获取堆句柄:
-
使用
NtCurrentPeb()
访问 PEB(进程环境块),并从中提取当前进程的堆指针保存到Heap
变量中。 -
这个堆之后会被用来分配
INSTANCE
结构体。
-
-
获取 implant 在内存中的起始地址和大小:
-
StRipStart()
返回 implant 的起始地址。 -
StRipEnd()
返回 implant 的结束地址。 -
这两个地址的差值就是 implant 在内存中的完整长度。
-
这对于后续访问全局变量
.global
段是必需的。
-
-
设置全局变量实例(省略的部分):
-
这一部分通常会将
Stardust
的指针保存在某个已知位置,供 implant 中其他地方引用,实现模块化访问。
-
-
调用
Main
函数开始 implant 主逻辑执行。
PreMain
函数负责获取 implant(植入程序)在内存中的正确基地址和大小,并定位全局的Instance
变量,它是一个指向INSTANCE
结构体的指针。这个结构体可以包含我们在运行时需要访问的任何内容,比如变量、配置、指针或其他数据,并且能在 implant 代码的任何地方使用。
我们现在逐行来看PreMain
函数的代码含义。
函数前几行的作用很基础,是在做准备工作:
-
清空
Stardust
结构体; -
从 PEB(进程环境块)中提取进程的堆句柄(
ProcessHeap
); -
将堆地址保存到变量
Heap
中(后续我们要在堆上分配内存来存储Instance
结构体的数据)。
这些准备工作完成之后,就要进入真正关键的部分:
获取 implant 在内存中的基地址。
因为后续我们访问全局变量.global
段时必须依赖这个基地址。我们使用StRipStart
函数来完成这件事,它的实现之前已经展示过,它位于.text$A
段,具体是这样工作的:
-
利用
call
指令将返回地址压入栈中; -
然后通过读取
rsp
上的返回地址,减去当前指令长度,得到实际的 RIP(当前代码段的真实内存位置); -
最终这就是 implant 在内存中的“运行时地址基址”。
这种技术是编写**位置无关代码(Position Independent Code, PIC)**时的标准技巧。
;; ;; 获取代理(implant)起始地址的 RIP(运行时指令指针) ;; StRipStart: call StRipPtrStart ret ;; ;; 获取 StRipStart 的返回地址,并存入 RAX 寄存器 ;; StRipPtrStart: mov rax, [rsp] ;; 从栈顶取出返回地址(call 指令压入的) sub rax, 0x1b ;; 减去 call + ret 所占的指令大小,得到真正的起始地址 ret ;; 返回到 StRipStart
背后原理:
call StRipPtrStart
-
call
会将下一条指令地址(即ret
位置)压入栈顶; -
然后跳转到
StRipPtrStart
。
mov rax, [rsp]
-
从栈顶取出返回地址,即
StRipStart
的下一条指令的地址; -
这个地址在内存中表示的是某段位置,而不是代码的真正起始地址。
sub rax, 0x1b
-
0x1b
是从代码起始位置到当前地址的偏移量; -
减去它即可获得整个 implant 的运行时基地址。
ret
-
返回到
StRipStart
的ret
,回到上一层调用。
你可能会问,为什么使用mov rax, [rsp]
来读取返回地址,而不是使用更小/更快的替代方法,比如我之前在旧的仓库(ShellcodeTemplate)中使用的那种方式:
GetRIP: call retptr retptr: pop rax sub rax, 5 ret
解释:
-
call retptr
将下一条指令地址(即retptr
的地址)压入栈中,然后跳转到retptr
。 -
pop rax
将栈顶的返回地址弹出到rax
中。此时rax
的值等于retptr
的地址。 -
sub rax, 5
减去 5 是为了回退到call retptr
指令的起始地址,即获取当前代码的真实位置(GetRIP
的地址)。
为什么是 5?因为call
指令在 x86_64 下通常占用 5 个字节,所以回退这 5 个字节,就得到了GetRIP:
所在的地址。 -
ret
返回到call retptr
的调用点之外(执行完这个函数)。
原因是因为 x86matthew 指出,我应该通过确保每个ret
指令与正确的call
指令匹配,从而使代码兼容 CET(控制流保护,Control-flow Enforcement Technology)。
接下来,我将逐行描述并解释每一行的原因。在调用StRipStart
后,函数会调用另一个子函数,将自己的返回地址压入栈中。接着,StRipPtrStart
会从栈中读取返回地址,并将其存储在rax
寄存器中,稍后会减去它自身的指令大小、StRipStart
的指令大小以及Start
的指令大小,最终得到基地址。我们可以简单地通过内存中的指令大小来计算,但我喜欢使用 radare2,因此我用它来获取Start
函数的大小,方法如下:
通过在radare2
中使用命令pdf
来查看反汇编代码后,结合afi ~.size
获取的函数大小信息,我们可以看到Start
函数的反汇编代码及其大小。
解释:
-
反汇编输出 (
pdf
):
0x00000000 56 push rsi ; arg2
-
在
radare2
中,使用pdf
命令可以查看指定地址的反汇编输出。输出显示了Start
函数的指令:
0x00000001 4889e6 mov rsi, rsp ; int64_t arg2
0x00000004 4883e4f0 and rsp, 0xfffffffffffffff0
0x00000008 4883ec20 sub rsp, 0x20
0x0000000c e8df010000 call fcn.000001f0
0x00000011 4889f4 mov rsp, rsi
0x00000014 5e pop rsi
0x00000015 c3 ret
```
* 每一行代码的含义逐一解释: * `push rsi`和 `mov rsi, rsp`是准备栈对齐; * `and rsp, 0xfffffffffffffff0`将栈指针对齐到 16 字节边界; * `sub rsp, 0x20`分配 32 字节空间; * `call fcn.000001f0`调用另一个函数; * `mov rsp, rsi`恢复栈指针; * `pop rsi`恢复 `rsi`寄存器; * `ret`返回。
-
获取函数大小 (
afi ~.size
):-
通过
afi ~.size
命令,你可以查看当前函数的大小,这里显示为 22 字节。这是Start
函数的大小,可以帮助你进一步理解指令的大小。
-
无论如何,现在我们可以通过简单地从返回地址中减去Start
(0x16)和StRipStart
(只有call
指令大小,即 0x5)的指令大小,从rax
寄存器中获得基地址,从而得到植入物在内存中的基地址。在获取基地址后,我们现在需要获得植入物的结束地址,这样才能计算植入物在内存中的精确大小。
获取植入物结束地址的方式与之前在.text$A
中展示的StRipStart
相同,唯一的区别是这次它位于.text$E
中,.text$E
是植入物的最后一个代码段,包含以下代码:
;;
;; end of the implant code
;;
[SECTION .text$E]
;; ;; get end of the implant ;; StRipEnd: call StRetPtrEnd ret ;; ;; get the return address of StRipEnd and put it into the rax register ;; StRetPtrEnd: mov rax, [rsp] ;; get the return address add rax, 0xa ;; get implant end address ret ;; return to StRipEnd 这是植入物代码的结尾部分,位于 `.text$E` 段中。它的目的是获取植入物的结束地址。具体代码如下:
代码解析:
-
StRipEnd
:
StRipEnd: call StRetPtrEnd ret
-
这一部分的作用是通过调用
StRetPtrEnd
来获取返回地址,并将控制返回到StRipEnd
,从而获得植入物的结束地址。
-
StRetPtrEnd
:
StRetPtrEnd: mov rax, [rsp] ;; 获取返回地址 add rax, 0xa ;; 获取植入物结束地址 ret ;; 返回到 StRipEnd
-
这部分通过从栈中获取返回地址并存储到
rax
寄存器中,然后将返回地址加上 0xA(即指令的长度)来得到植入物结束的内存地址。
这种获取植入物在内存中结束地址的方法与获取基地址的方法非常相似,唯一的关键区别是它将加上StRipEnd
函数的指令大小(由于ret
指令,它只有一个字节)和StRetPtrEnd
的指令大小(大小为 0x9 字节)。现在,我们总共有 10 个字节的指令需要加到返回地址上,以获得植入物的结束地址。
在执行StRipStart
和StRipEnd
这两个函数后,你应该能够得到植入物所在内存区域的完整范围:
//
// get the base address of the current implant in memory and the end.
// subtract the implant end address with the start address you will
// get the size of the implant in memory
//
Stardust.Base.Buffer = StRipStart();
Stardust.Base.Length = U_PTR( StRipEnd() ) - U_PTR( Stardust.Base.Buffer );
这段代码的作用是获取当前植入物在内存中的基地址和结束地址,并通过计算基地址与结束地址之间的差值来得到植入物在内存中的大小。
代码解析:
// 获取当前植入物的基地址和结束地址。 // 通过从结束地址减去起始地址,我们可以得到植入物在内存中的大小。 Stardust.Base.Buffer = StRipStart(); // 获取植入物的基地址 Stardust.Base.Length = U_PTR( StRipEnd() ) - U_PTR( Stardust.Base.Buffer ); // 计算植入物的大小
解释:
-
StRipStart()
:此函数返回植入物的基地址,即植入物在内存中的起始位置。 -
StRipEnd()
:此函数返回植入物的结束地址。 -
U_PTR()
:这是一个宏或函数,它将返回值转换为一个无符号指针,确保计算是以正确的内存地址单位进行的。 -
计算大小:通过计算结束地址与基地址之间的差值,得到植入物在内存中的大小。
全局实例
接下来的几行代码用于准备全局实例,通过获取.global
部分的偏移地址,来确定我们的Instance
指针将存储的位置。
//
// get the offset and address of our global instance structure
//
MmAddr = Stardust.Base.Buffer + InstanceOffset();
MmSize = sizeof( PVOID );
这段代码的工作原理是通过InstanceOffset()
获取实例的偏移量。InstanceOffset()
是一个宏,它包含了实例位置的偏移量,这个偏移量是在链接时插入的,如我们在上面的链接脚本中指定的那样,链接器会将偏移量保存到__Instance_offset
中,以便我们在 C 代码中直接使用。
#define InstanceOffset() ( U_PTR( & __Instance_offset ) )
全局实例(Global Instance)
接下来的几行代码是为了初始化全局实例,具体做法是获取.global
段的偏移地址,该段中将存放我们的Instance
指针。
//
// get the offset and address of our global instance structure
//
MmAddr = Stardust.Base.Buffer + InstanceOffset();
MmSize = sizeof( PVOID );
这是通过调用InstanceOffset()
宏来获取实例的偏移地址来实现的。这个宏包含了实例位置的偏移量,该偏移量在链接时已插入(正如我们在上面的链接脚本中指定的那样),它会被保存到__Instance_offset
中,因此我们可以直接在 C 代码中使用它。该宏的定义如下:
#define InstanceOffset() ( U_PTR( & __Instance_offset ) )
我们将__Instance_offset
和__Instance
一起定义为外部变量,因为它们将分别保存我们的实例偏移地址和实例指针。
//
// stardust instances
//
EXTERN_C ULONG __Instance_offset;
EXTERN_C PVOID __Instance;
在获取到全局实例的偏移地址之后,我们可以将该偏移值加到 implant 的基地址上,从而获得 implant 中指向实例的指针。拿到这个全局实例在内存中的指针后,我们将其保存到MmAddr
中,稍后我们会使用它来修改对应页面的内存保护权限,以便将堆上的指针写入这个全局空间,从而存储所有的实例数据。
接下来的几行代码是为了解析我们需要用到的函数,以便能更改.global
段所在页面的权限并在堆上分配内存。在这个场景中,我们会解析ntdll!NtProtectVirtualMemory
和ntdll!RtlAllocateHeap
,解析方式是使用LdrModulePeb
(通过遍历 PEB 的InLoadOrderModuleList
链表来获取模块)以及LdrFunction
(用于从指定模块中解析导出的函数指针)。
//
// resolve ntdll!RtlAllocateHeap and ntdll!NtProtectVirtualMemory for
// updating/patching the Instance in the current memory
//
if ( ( Stardust.Modules.Ntdll = LdrModulePeb( H_MODULE_NTDLL ) ) ) {
if ( ! ( Stardust.Win32.RtlAllocateHeap = LdrFunction( Stardust.Modules.Ntdll, HASH_STR( "RtlAllocateHeap" ) ) ) ||
! ( Stardust.Win32.NtProtectVirtualMemory = LdrFunction( Stardust.Modules.Ntdll, HASH_STR( "NtProtectVirtualMemory" ) ) )
) {
return;
}
}
在从栈和代码的其他部分清理掉一些数据之后,我们现在就可以执行植入程序的主入口点了,也就是我们的主要载荷。这个载荷将负责解析其他的 Win32 函数和模块,进行通信、任务管理、执行命令等操作。
//
// copy the local instance into the heap,
// zero out the instance from stack and
// remove RtRipEnd code/instructions as
// they are not needed anymore
//
MmCopy( C_DEF( MmAddr ), &Stardust, sizeof( INSTANCE ) );
MmZero( & Stardust, sizeof( INSTANCE ) );
MmZero( C_PTR( U_PTR( MmAddr ) + sizeof( PVOID ) ), 0x18 );
//
// now execute the implant entrypoint
//
Main( Param );
在继续深入讲解主函数的实现之前,我们先来聊聊编译时哈希(compile-time hashing)以及它是如何实现的。
编译时哈希(Compile-time Hashing)
其实关于这个没什么太多好说的,我们只是利用了 C++ 的constexpr
特性,在编译时执行一个函数,把字符串转换为 djb2 哈希值。
#define HASH_STR( x ) ExprHashStringA( ( x ) )
constexpr ULONG ExprHashStringA(
InPCHAR String
) {
ULONG Hash = { 0 };
CHAR Char = { 0 };
Hash = H_MAGIC_KEY; if ( ! String ) { return 0; } while ( ( Char = *String++ ) ) { /* turn current character to uppercase */ if ( Char >= 'a' ) { Char -= 0x20; } Hash = ( ( Hash << H_MAGIC_SEED ) + Hash ) + Char; } return Hash;
}
主载荷(Main Payload)
好了,我想现在一切都已经解释清楚了,那我们就开始编写我们的主载荷吧。我们先保持简单,尝试弹出一个消息框,显示当前运行进程的文件路径,标题为 “Stardust Message”。这应该足以展示这个植入物的基本能力,以及它是如何被用来进一步开发一个完整功能的植入物的。
FUNC VOID Main(
InPVOID Param
) {
STARDUST_INSTANCE
PVOID Message = { 0 }; // // resolve kernel32.dll related functions // if ( ( Instance()->Modules.Kernel32 = LdrModulePeb( H_MODULE_KERNEL32 ) ) ) { if ( ! ( Instance()->Win32.LoadLibraryW = LdrFunction( Instance()->Modules.Kernel32, HASH_STR( "LoadLibraryW" ) ) ) ) { return; } } // // resolve user32.dll related functions // if ( ( Instance()->Modules.User32 = Instance()->Win32.LoadLibraryW( L"User32" ) ) ) { if ( ! ( Instance()->Win32.MessageBoxW = LdrFunction( Instance()->Modules.User32, HASH_STR( "MessageBoxW" ) ) ) ) { return; } } Message = NtCurrentPeb()->ProcessParameters->ImagePathName.Buffer; // // pop da message // Instance()->Win32.MessageBoxW( NULL, Message, L"Stardust MessageBox", MB_OK );
}
在开发和设计一个位置无关的植入物时,我们需要考虑一些事项,并了解如何使用 Stardust 设计。每个函数应该以FUNC
宏开始,因为它告诉链接器脚本将该函数存储在哪里,在本例中是存储在.text$B
部分,因为这一部分将包含所有植入物的核心 C 函数。
#define D_SEC( x ) attribute( ( section( ".text$" #x "" ) ) )
#define FUNC D_SEC( B )
如果函数需要访问全局实例,则函数的开头应该以STARDUST_INSTANCE
开始。
//
// instance related macros
//
#define InstanceOffset() ( U_PTR( & __Instance_offset ) )
#define InstancePtr() ( ( PINSTANCE ) C_DEF( C_PTR( U_PTR( StRipStart() ) + InstanceOffset() ) ) )
#define Instance() ( ( PINSTANCE ) __LocalInstance )
#define STARDUST_INSTANCE PINSTANCE __LocalInstance = InstancePtr();
这个宏声明了一个指向植入程序全局实例的变量。指定宏后,可以通过使用Instance()
宏轻松访问全局实例。
编写完主植入负载后,我们需要编译它。该项目包含一个默认的 Makefile,目前只编译 Stardust 项目为 64 位二进制文件,因为我不想再支持 x86。然而,添加对 x86 的支持应该相对简单,因为只需要将汇编部分重写为 x86。
$ make
[] compile assembly files
[+] compile x64 executable
/usr/lib/gcc/x86_64-w64-mingw32/13.1.0/../../../../x86_64-w64-mingw32/bin/ld: bin/stardust.x64.exe:.text: section below image base
/usr/lib/gcc/x86_64-w64-mingw32/13.1.0/../../../../x86_64-w64-mingw32/bin/ld: bin/stardust.x64.exe:.eh_fram: section below image base
[] payload len : 4128 bytes
[] size : 8192 bytes
[] padding : 4064 bytes
[*] page count : 2.0 pages
$ ls -l bin
total 12
drwxr-xr-x 2 spider spider 4096 Jan 27 17:53 obj
-rw-r--r-- 1 spider spider 8192 Jan 27 17:53 stardust.x64.bin
现在我们成功构建了植入程序二进制文件,它可以被包含在任何类型的加载器中,或者可以使用项目中附带的测试加载器,位于scripts/loader.x64.exe
(一个简单的 VirtualAlloc 和执行加载器)。
原文链接:https://5pider.net/blog/2024/01/27/modern-shellcode-implant-design/
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)