首页
论坛
课程
招聘
[原创]Windows内核模糊测试之Interface-aware fuzzing
2022-3-2 11:41 4647

[原创]Windows内核模糊测试之Interface-aware fuzzing

2022-3-2 11:41
4647

一.前言

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中,会按如下步骤进行测试:

  1. 随机获取syscall,此时结构中保存了调用号和参数个数及对应类型

  2. 为每个参数生成随机的数值

  3. 调用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;

恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

最后于 2022-3-11 21:15 被1900编辑 ,原因:
上传的附件:
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回