纤程(Fiber)是一种执行单元,必须由应用程序手动调度,而不是依赖 Windows 内置的基于优先级的调度机制。纤程通常被称为轻量级线程。有关纤程概念及工作机制的更多详细信息,请查阅官方文档。 纤程允许在单个线程内部拥有多个执行流,每个执行流拥有自己的寄存器状态和栈。另一方面,纤程对内核是不可见的,这使得它们比创建新线程更隐蔽(也更廉价)地执行内存代码。
一个线程可以创建多个纤程,并通过调用 SwitchToFiber 函数随意切换。在此之前,当前线程本身必须先通过调用 ConvertThreadToFiber 变为纤程,因为只有纤程才能创建其他纤程。最后,要创建一个被调度时执行内存中代码(例如,反射加载一个 PE 或某些 shellcode 之后)的纤程,只需调用 CreateFiber 即可。
SwitchToFiber 函数是整个过程中最关键的部分,所有神奇之处都发生在这里。该函数允许调度一个或另一个纤程,这一切都发生在用户空间。根据官方文档,“SwitchToFiber 函数保存当前纤程的状态信息,并恢复指定纤程的状态”。这意味着当调用该函数时,寄存器的值和栈会从当前纤程的状态切换到目标纤程的状态,从而允许在进程完成后“隐藏”当前纤程的栈。这也允许目标纤程从中断执行的点继续执行(就像调度程序根据自身优先级逻辑在线程间切换时发生的情况一样)。
而这正是这个简单 PoC 所做的:
run() 函数。此纤程以下称为有效负载纤程。这个过程无限重复。
使用纤程可能对某些类型的有效负载(如 C2 信标)有利,原因如下:
JMP 或 CALL 指令。由于我们使用 LITCRYPT 插件来混淆字符串字面量,因此在编译代码之前需要设置环境变量 LITCRYPT_ENCRYPT_KEY:
C:\Users\User\Desktop\Fiber> set LITCRYPT_ENCRYPT_KEY="yoursupersecretkey"
之后,只需同时编译有效负载和加载器,然后运行后者:
C:\Users\User\Desktop\Fiber\payload> cargo build --release
C:\Users\User\Desktop\Fiber\loader> cargo build --release
C:\Users\User\Desktop\Fiber\loader\target\release> loader.exe
这个 PoC 的执行没有什么神秘之处。只需运行加载器,并使用任何工具(如 ProcessHacker)检查线程栈。由于有效负载在睡眠前会切换回控制纤程,因此有效负载纤程的栈大部分时间保持隐藏。你将在输出中看到两个纤程按照已经讨论的逻辑连续调度。
代码中有注释,说明如何使用、创建和调度纤程。你会注意到,作为示例提供的加载器和有效负载都“卡”在了一个无限循环中,这使得可以在纤程之间无限切换并继续执行。
如果想测试不同的有效负载,只需修改加载器文件 src::main.rs 第 32 行的路径。在这种情况下,新的 DLL 必须导出一个 run(PVOID) 函数,该函数将接收控制纤程的地址作为输入参数。此函数必须切换回控制纤程以便调用 Sleep 函数,但你可以根据需要修改此行为以满足你的要求。
另一种使用随机有效负载测试此工具的方法是执行 IAT 挂钩,将有效负载对 Sleep 函数(或任何其他导入函数)的调用重定向到加载器中的一个函数,从而允许在此调用发生时切换回控制纤程。取决于你。
在以下截图中,我们可以看到当前线程的栈如何在切换纤程时从一个私有内存区域移动到另一个私有内存区域:
