-
-
[原创]Windows内核模糊测试之Interface-aware fuzzing
-
2022-3-2 11:41 5527
-
一.前言
Interface-aware fuzzing是通过将内核API函数的参数进行随机化,随后调用内核API来实现的。通过这种方式,可以主动地对内核模块进行测试,且覆盖面和深度都更加优异,性能可以得到显著的提高。这种模糊测试方法要解决如下的问题:
内核API数量那么多,要怎么样主动调用这些API才可以完成测试
每个API的参数个数各不相同,应当如何生成合适的值来完成测试
通过https://github.com/FSecureLABS/KernelFuzzer这个模糊测试器(附件是作者的PPT),看看它是如何解决上述问题的。
二.Fuzzing输入点
对于API的调用,显然不能通过常规的方式从相应的dll文件中慢慢调用进去,这样不仅运行效率低,参数不合法的时候都进不到内核,最关键的是找全这些函数的地址太耗时间的。
由于每个dll中的API,在将参数压入栈中以后,会通过ntdll中的相应存根函数进入内核。而这些存根函数是具有固定格式的,比如下面就是函数ReadProcessMemory在ntdll中执行的存根函数:
.text:7C92D9E0 public NtReadVirtualMemory .text:7C92D9E0 NtReadVirtualMemory proc near ; CODE XREF: .text:7C93FFD1↓p .text:7C92D9E0 ; LdrCreateOutOfProcessImage+7C↓p ... .text:7C92D9E0 mov eax, 0BAh ; 服务号赋给eax .text:7C92D9E5 mov edx, 7FFE0300h .text:7C92D9EA call dword ptr [edx] .text:7C92D9EC retn 14h .text:7C92D9EC NtReadVirtualMemory endp
此时的esp指向的栈顶指针保存了该函数的参数,这里的eax保存的是服务号,进入内核以后,内核通过对该值进行运算,去SSDT表中找到相应的内核函数进行调用。0x7FFE0300则保存了进入内核的函数,所有的函数都是通过这种格式,从用户层发起调用进入内核的。因此,就可以参考这种格式来完成模糊测试。
KernelFuzzer中实现的相应调用的代码在bughut_syscall.asm和bughunt_syscall_x64.asm中完成的,其中bughut_syscall.asm代码如下:
.686P .MODEL FLAT, C .STACK 1000h .CODE bughunt_syscall PROC push ebp mov ebp, esp sub esp, 84h // 一共会从栈中取出32个参数入栈,这里省略到前面一部分 mov ecx, [ebp + 18h] ; // 参数四入栈 push ecx mov ecx, [ebp + 14h] ; // 参数三入栈 push ecx mov ecx, [ebp + 10h] ; // 参数二入栈 push ecx mov ecx, [ebp + 0Ch] ; // 参数一入栈 push ecx mov eax, [ebp + 08h] ; // 将服务号赋值给eax mov edx, 7FFE0300h call dword ptr [edx] // 调用函数进入内核 mov esp, ebp pop ebp ret bughunt_syscall ENDP END
三.Fuzzing数据
1.调用号和参数个数
有了输入点,就需要相应的输入数据才能完成测试,这里有两项输入数据,分别是调用号和参数。在https://github.com/tinysec/windows-syscall-table中,作者收集了各个版本的内核函数的调用号和参数个数。
如下是win7 sp0系统的内核函数的调用号和参数个数,id32和id64分别代表32位和64位系统的内核函数调用号,argc32和argc64则分别代表了32位系统和64位系统的内核函数的参数个数。
但是KernelFuzzer看起来并没有去获取内核函数的调用号与参数个数,对于调用号,只要按顺序调用下去就可以。而对于参数个数,只要在栈中压入足够多的参数也可以完成测试。
2.参数的生成
内核函数的参数可以分为以下三类:
不同类型的具体数值,比如字符型或者整型1,2,3
指向某块地址的指针
各种各样的句柄
对于第一类只需要随机生成相应大小的数值就好,第二类也只需要随机生成4字节(32位系统)大小的数值就可以,因为所有的指针都是4个字节,指向了某个地址。
句柄不可以随机生成一个数值,因此句柄值需要在句柄表中有相应的表项才可以作为合法的输入,因此在KerFuzzer的主函数中会在开启测试前先调用make_HANDLES函数随机生成一些合法的句柄,以下是关键的代码
int main (int argc, char* argv[]) { // 生成句柄值 make_HANDLES(); // 开始测试 for (subprocess_idx = 0; subprocess_idx < subprocess_count; subprocess_idx += 1) { // 创建线程进行测试 hThreadArray[subprocess_idx] = CreateThread(NULL, // default security attributes 0, // use default stack size (LPTHREAD_START_ROUTINE) bughunt_thread, // thread function name seed, // argument to thread function 0, // use default creation flags &dwThreadIdArray[subprocess_idx]); // returns the thread identifier if (hThreadArray[subprocess_idx] == NULL) { printf ("Error creating thread, exiting."); return (0xDEADBEEF); // Error. } } return 0; }
make_HANDLES函数则是随机生成一些合法的句柄保存到全局变量HANDLES中,供测试函数使用
void make_HANDLES (void) { unsigned int handle_idx = 0; const POINT ptZero = { 0, 0 }; //to get a handle to the primary monitor BITMAP bmp = { 0, 8, 8, 2, 1, 1 }; BYTE bits [8][2] = { 0xFF, 0, 0x0C, 0, 0x0C, 0, 0x0C, 0, 0xFF, 0, 0xC0, 0, 0xC0, 0, 0xC0, 0 }; HKEY keyCurrentUser; HANDLE tempHandle; unsigned int tempUINT1, tempUINT2; INT NumberOfNotepadHandles = 0; tempUINT1 = 0; tempUINT2 = 0; // 初始化句柄值 for (handle_idx = 0; handle_idx < HANDLES_N; handle_idx += 1) { HANDLES[handle_idx] = 0x0000000000000000; } // 随机生成合法的句柄 for (handle_idx = 0; handle_idx < 64; handle_idx += 1) { while(HANDLES[handle_idx] == 0x0000000000000000) { if (!tempUINT1) { tempHandle = GetDesktopWindow(); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "GetDesktopWindow"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "GetDesktopWindow"; tempHandle = -1; tempUINT1 = 1; break; } } if (!tempUINT2) { tempHandle = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "MonitorFromPoint"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "MonitorFromPoint"; tempHandle = -1; tempUINT2 = 1; break; } } switch(rand() % 8) { case 0: tempHandle = CreateFile(TEXT("C:\\boot.ini"), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateFile"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "CreateFile"; tempHandle = -1; } break; case 1: tempHandle = CreateSolidBrush(RGB(0, 255, 0)); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateSolidBrush"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "CreateSolidBrush"; tempHandle = -1; } break; case 2: tempHandle = FindWindow(NULL, TEXT("Explorer")); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "FindWindow"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "FindWindow"; tempHandle = -1; } break; case 3: tempHandle = CreateFont(46, 28, 215, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN, "Times New Roman"); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateFont"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "CreateFont"; tempHandle = -1; } break; case 4: tempHandle = CreateBitmapIndirect(&bmp); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "CreateBitmapIndirect"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "CreateBitmapIndirect"; tempHandle = -1; } break; case 5: tempHandle = GlobalAlloc(GMEM_FIXED, 10); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "GlobalAlloc"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "GlobalAlloc"; tempHandle = -1; } break; case 6: RegOpenCurrentUser(KEY_READ, &tempHandle); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "RegOpenCurrentUser"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "RegOpenCurrentUser"; tempHandle = -1; } break; case 7: tempHandle = OpenNotepad(); if (tempHandle == NULL || tempHandle == -1 || tempHandle == INVALID_HANDLE_VALUE) { logger("//[Handler_Function]: make_HANDLES : Ignoring invalid handle."); } else { logger("//[Handler_Function]: make_HANDLES : n = %u, handle = 0x%08X, HANDLE_CREATOR[n] = %s", handle_idx, tempHandle, "OpenNotepad"); HANDLES[handle_idx] = tempHandle; HANDLE_CREATOR[handle_idx] = "OpenNotepad"; tempHandle = -1; } break; } } } // 生成句柄的数量 HANDLES_ARRAY_AVAILABLE_SLOT_INDEX = 64; }
在测试函数bughunt_thread中是通过SYSCALL结构体来传递参数的,该结构体定义如下:
typedef enum { _BOOL = 1, _BOOL_PTR = 2, _CHAR8 = 3, _CHAR8_PTR = 4, _CHAR16 = 5, _CHAR16_PTR = 6, _INT8 = 7, _INT8_PTR = 8, _INT16 = 9, _INT16_PTR = 10, _INT32 = 11, _INT32_PTR = 12, _INT64 = 13, _INT64_PTR = 14, _UINT8 = 15, _UINT8_PTR = 16, _UINT16 = 17, _UINT16_PTR = 18, _UINT32 = 19, _UINT32_PTR = 20, _UINT64 = 21, _UINT64_PTR = 22, _REAL32 = 23, _REAL32_PTR = 24, _REAL64 = 25, _REAL64_PTR = 26, _HANDLE = 28, _VOID_PTR = 27, NIL = 0 } DATATYPE; /* SYSCALL DEFINITIONS... */ #define SYSCALL_ARGUMENT_N ((size_t)33) typedef struct { DWORD uid; // 函数的调用号 DATATYPE argument_datatypes[SYSCALL_ARGUMENT_N]; // 参数的类型 DATATYPE return_datatype; // 返回值的类型 } SYSCALL;
在bughunt_thread中,会按如下步骤进行测试:
随机获取syscall,此时结构中保存了调用号和参数个数及对应类型
为每个参数生成随机的数值
调用bughunt_syscall完成测试
部分代码如下:
DWORD bughunt_thread(unsigned int seed) { unsigned int syscall_idx = 0; SYSCALL* syscall = NULL; unsigned int syscall_argument_datatype_idx = 0; unsigned int syscall_arguments[SYSCALL_ARGUMENT_N - 1]; // DWORD syscall_arguments[32]; BH_Handle syscall_handle_argument; for (syscall_idx = 0; syscall_idx < syscall_count; syscall_idx += 1) { // 随机获取syscall syscall = random_SYSCALL (); syscall_argument_datatype_idx = 0; while ((syscall_argument_datatype_idx < (SYSCALL_ARGUMENT_N - 1)) && (syscall->argument_datatypes[syscall_argument_datatype_idx] != NIL)) { // 根据数据类型随机生成参数值,这里删除了一部分 switch (syscall->argument_datatypes[syscall_argument_datatype_idx]) { // Something to check is whether the 0x%08x format string specifier is okay in all cases, e.g. 64-bit. case _BOOL: syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_bool()); break; case _CHAR8: syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_char8()); break; case _INT32: syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_int32()); break; case _UINT32: syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_uint32()); break; case _REAL32: syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_real32()); break; case _REAL64: syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)get_fuzzed_real64()); break; case _HANDLE: // 随机获取句柄 syscall_handle_argument = get_random_HANDLE(); syscall_arguments[syscall_argument_datatype_idx] = ((DWORD)syscall_handle_argument.value); break; } syscall_argument_datatype_idx += 1; } // 调用函数完成测试 bughunt_syscall ( syscall->uid, syscall_arguments[0], syscall_arguments[1], syscall_arguments[2], syscall_arguments[3], syscall_arguments[4], syscall_arguments[5], syscall_arguments[6], syscall_arguments[7], syscall_arguments[8], syscall_arguments[9], syscall_arguments[10], syscall_arguments[11], syscall_arguments[12], syscall_arguments[13], syscall_arguments[14], syscall_arguments[15], syscall_arguments[16], syscall_arguments[17], syscall_arguments[18], syscall_arguments[19], syscall_arguments[20], syscall_arguments[21], syscall_arguments[22], syscall_arguments[23], syscall_arguments[24], syscall_arguments[25], syscall_arguments[26], syscall_arguments[27], syscall_arguments[28], syscall_arguments[29], syscall_arguments[30], syscall_arguments[31] ); } return 0; }
这里的random_SYSCALLl函数是从全局数组SYSCALLS中随机获取数据,具体实现如下:
double random_double_0_to_1 (void) { return ((double)rand() / (double)RAND_MAX); } DWORD random_DWORD_0_to_N (DWORD n) { return ((DWORD)(random_double_0_to_1 () * n)); } SYSCALL* random_SYSCALL (void) { unsigned int n = sizeof (SYSCALLS) / sizeof (SYSCALLS[0]); return (&(SYSCALLS[random_DWORD_0_to_N (n)])); }
而全局数组SYSCALLS的存储格式如下:
SYSCALL SYSCALLS[] = { //Windows 7 x64 user32 syscalls. { ((DWORD)0x12F5), { NIL }, _BOOL }, { ((DWORD)0x12D4), { _VOID_PTR, _VOID_PTR, _VOID_PTR, _HANDLE, NIL }, _BOOL }, // End of Windows 7 x64 gdi32 syscalls. };
除了句柄以外的参数,根据参数类型随机生成符合要求的数据,比如bool类型的数据生成如下:
bool_t get_fuzzed_bool (void) { bool_t bool_BH[] = {0, 1}; bool_t n; n = bool_BH[rand() % sizeof(bool_BH) / sizeof(bool_BH[0])]; return n; }
句柄的获取是通过get_random_HANDLE函数获取的,该函数就是从前面随机生成保存到全局数组HANDLES中随机获取句柄值,具体实现如下:
BH_Handle get_random_HANDLE (void) { BH_Handle temp_handle; unsigned int n; if (HANDLE_ARRAY_FULLY_POPULATED) { n = sizeof (HANDLES) / sizeof (HANDLES[0]); n = rand() % n; } else { n = rand() % HANDLES_ARRAY_AVAILABLE_SLOT_INDEX; } temp_handle.index = n; temp_handle.value = HANDLES[n]; return temp_handle; }
其中BH_Handle结构体的定义如下:
typedef struct { HANDLE value; // 句柄值 int index; // 在数组中对应的下标 } BH_Handle;
[招生]科锐逆向工程师培训46期预科班将于 2023年02月09日 正式开班