重载内核全程分析笔记

转自:http://blog.csdn.net/whatday/article/details/14160875


标 题: 【原创】重载内核全程分析笔记
作 者: Speeday
时 间: 2013-08-20,20:19:46
链 接: http://bbs.pediy.com/showthread.php?t=177555

还记得七夕的那几天,老V率先把AGP的源码发布出来,然后是EasyDebugger的源码出土,后面陆续有很多大牛把珍藏已久的代码拿出来晒太阳,那段疯狂的日子,让看雪论坛都面临崩溃的边缘。折腾了大半天,终于把代码都下载下来了,可是下载下来做什么呢,自己连看都看不懂。
于是,带着激动而又郁闷的心情继续学习内核编程。
由于是初学者,很多时候也有不清楚的地方,若下文中有什么地方理解有误,还请大牛多多指正,
文中所写代码参考自       
看雪论坛大牛  和  梦织未来论坛大牛  
好了,进入正题吧,继续看我的废话。

重载内核内容:
1、  将内核文件加载到内存
2、  进行基址重定位
3、  重定位ssdt结构
4、  Hook KiFastCallEntry,让RING3进程调用走新内核

下面一步一步的进行分析
1、  加载内核文件
我们要加载哪一个文件呢?答案是:ntkrnlpa.exe,我测试的系统的XP sp3,该文件所在目录为C:\WINDOWS\system32\ntkrnlpa.exe。加载文件要一部分一部分的加载,因为硬盘上的文件对齐方式与在内存中文件的对齐方式不同,所以如果你直接申请一块内存,然后把整个磁盘文件加载进去,那肯定是不对的,我们要按照PE结构一块块的进行加载。具体加载顺序是:IMAGE_DOS_HEADER,IMAGE_NT_HEADER,IMAGE_SECTION_HEADER,最后是把各区段的内容加载进去。加载过程中用到的内核函数有:打开文件:ZwCreateFile,初始化ZwCreateFile中的一个参数:IninializeObjectAttributes,读取文件内容:ZwReadFile,分配内存空间:ExAllocatePool,释放内存空间:ExFreePool,关闭句柄:ZwClose。
由于这段代码比较简单,又有很多地方是做重复性的工作,这里就不贴上来讲解了,具体请大家下载源码进行分析吧。如有不懂的请回帖交流。
这段代码写完了,有什么用呢?加载PE文件这事,不像写helloworld,写Helloworld,写完了一运行,至少能看到一句话吧,可是LoadPe这事,在哪去看效果呢?如果代码中有错误,我们怎么知道有没有正确运行,光凭代码中的Kdprint打印信息是不够的。既然是内核程序,我们就可以通过windbg来查看,方法是:
随便找一个不是ssdt的函数(ssdt后面再说),如:IoCreateFile,打开windbg,输入lkd>u IoCreateFile l a,会看到一段汇编代码,获取第一句汇编代码的地址,如我这里是0x8056cc7e,再用xuetr看到的模块加载地址是0x804D8000,偏移=0x8056cc7e-0x804D8000=0x94C7E;再将debugview上打印出来的我们新加载的内核的首地址找到,加上这个偏移,我得到的是0x85BB0000,用windbg查看 lkd>u 85C44C7E l a
效果如图: 
重载内核全程分析笔记_第1张图片
如果看到与图中相似的画面,也就是说和原来内核的代码几乎一样,那就说明加载内核文件成功了。
为什么说几乎一样呢?请看图中用红色线框标记的部分,原来的内核直接能够识别出一个全局变量名,而我们新载入的内核却只能看到一个数字。这是什么原因呢?要解决这个问题,就需要进行重载内核第二步,基址重定位。
2、  基址重定位
基址重定位的目的我们已经知道了,那具体该怎么做呢?微软在PE结构中已经有一个表定义了需要重定位的一些数据,就是重定位表,我们只需要修改里面的数据就可以了。现在的问题是如何修改。我们修改的目的是让新内核使用到正确的数据,来看看重定位表的数据结构:
typedef struct _IMAGE_BASE_RELOCATION {
    ULONG   VirtualAddress;
    ULONG   SizeOfBlock;
  USHORT  TypeOffset[1];
} IMAGE_BASE_RELOCATION;
第一个数据是相对地址,第二个数据是整个数据结构的大小,第三个数据就是要修改的地方了。
第三个数据是一个USHORT结构,包含两字节,高四位用于表示该数据的属性,我们这里要判断是否等于3,因为只有等于3的数据才是需要重定位的地址。我们将后12位取出来,再与新地址的首地址相加,得到一个实际的地址。得到这个实际地址后,这个地址的数据还并不是想要得到的数据,我们还要加上一个偏移,这个偏移就得用原内核的模块首地址减去imagebase,这样就可以正确定位了。
参考代码如下:

代码:
void FixBaseRelocTable(PVOID pNewImage)
{

  //将新内核地址作为一个PE文件头,依次向下,目的是寻找重定位表结构
  pImageDosHeader=(PIMAGE_DOS_HEADER)pNewImage;
  //定位到IMAGE_NT_HEADER
  pImageNtHeader=(PIMAGE_NT_HEADERS)((ULONG)pNewImage+pImageDosHeader->e_lfanew);
  //获取内核文件的imagebase,以便后面做偏移修改。
  OriginalImageBase=pImageNtHeader->OptionalHeader.ImageBase;
  //定位到数据目录
  ImageDataDirectory = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
  //定位到重定位表结构
  pImageBaseRelocation = (PIMAGE_BASE_RELOCATION)(ImageDataDirectory.VirtualAddress + (ULONG)pNewImage);
  if (pImageBaseRelocation==NULL)
  {
    return;  
  }
  while (pImageBaseRelocation->SizeOfBlock)
  {   //计算需要修改的地址的个数
    uRelocTableSize=(pImageBaseRelocation->SizeOfBlock-8)/2;
    //循环遍历
    for (uIndex=0;uIndex<uRelocTableSize;uIndex++)
    {//判断高4位是否等于3
      Type=pImageBaseRelocation->TypeOffset[uIndex]>>12;
      if (Type==3)
      {
        //修改地址,相对地址加上一个新内核地址,使其成为一个实际地址
        uRelocAddress=(ULONG *)((ULONG)(pImageBaseRelocation->TypeOffset[uIndex]&0x0fff)+pImageBaseRelocation->VirtualAddress+(ULONG)pNewImage);
        //再加上内核首地址到imagebase的偏移
        *uRelocAddress=*uRelocAddress+(OrigImage-OriginalImageBase);
      }
    }
    //进行下一个重定位表的修改
    pImageBaseRelocation=(IMAGE_BASE_RELOCATION *)((ULONG)pImageBaseRelocation+pImageBaseRelocation->SizeOfBlock);
  }
}
这一步完成过后,我们又可以去内核里面看看了。按照上面所说的方法,如果正确修正基址后,结果如下图所示:
  重载内核全程分析笔记_第2张图片
从图中可以看到,虽然是不同的地址,但是汇编代码却完全一样。这样,基址重定位就完成了。接下来就该进行ssdt重定位了。
3、  ssdt重定位
这个重定位相对于基址重定位来说就要简单得多了,因为PE结构的东西有点复杂(对于像我这样的新手来说)。
因为SSDT结构有多层,所以要分别进行运算,设置NewSSDT方法如下:
1、首先确定新SSDT在哪 个位置,因为用导出KeServiceDescriptorTable的方法是导出的老内核的SSDT结构,所以我们无法再用这个方法 。正确的方法是用原来的SSDT地址+相对加载地址。
2、如何得到KeServiceDescriptorTable.ServiceTableBase的地址?方法是用原来的(ServiceTableBase地址-老内核加载地址)+ 新内核加载地址。
3、修正SSDT函数中的地址。方法是在原来的函数地址上+ 相对加载地址。
相对加载地址=新内核加载地址 - 老内核加载地址

参考代码如下 :
代码:
VOID SetNewSSDT(PVOID pNewImage)
{
  ULONG              uIndex;
  ULONG              uNewKernelInc,uOffset;
  //新内核地址-老内核地址,得到相对偏移
  uNewKernelInc = (ULONG)pNewImage -OrigImage;
  //老内核的ssdt指针加上相对偏移,得到新内核的ssdt指针
  pNewSSDT = (ServiceDescriptorTableEntry_t *)((ULONG)&KeServiceDescriptorTable + uNewKernelInc);

  if (!MmIsAddressValid(pNewSSDT))
  {
    KdPrint(("pNewSSDT is unaviable!"));
    return;
  }
  //由于数量是一个数值,因此不必作相对偏移
  pNewSSDT->NumberOfServices = KeServiceDescriptorTable.NumberOfServices;
  //计算相对函数地址
  uOffset = (ULONG)KeServiceDescriptorTable.ServiceTableBase -OrigImage;
  //得到新的ssdt函数表地址
  pNewSSDT->ServiceTableBase = (unsigned int*)((ULONG)pNewImage + uOffset);
  if (!MmIsAddressValid(pNewSSDT->ServiceTableBase))
  {
    KdPrint(("pNewSSDT->ServiceTableBase: %X",pNewSSDT->ServiceTableBase));
    return;
  }
  //依次遍历
  for (uIndex = 0;uIndex<pNewSSDT->NumberOfServices;uIndex++)
  {//新的函数地址再加上相对加载地址,得到现在的ssdt函数地址
    pNewSSDT->ServiceTableBase[uIndex] += uNewKernelInc;
  }
}
好了,这一步完成后,重载内核差不多就完成了,但是只是这样做,我们就什么事都没干,重载内核就这么无聊么?以前不是听说很多过游戏保护的方式都是重载内核么?是啊,做到这一步如果不继续往下学习,那么重载内核是一件多么无聊的事啊。所以我们继续进行下一步吧。
4、  HOOK KiFastCallEntry,让RING3进程调用走新内核
谈到Hook KiFastCallEntry是不是兴奋劲又来了,国内的360安全卫士就是hook 了这个函数,起到了很强大的作用。这里找hook点的方法我用的是堆栈回溯法,因为我现在也只会用这个方法。
Hook KiFastCallEntry要用到两个技术,一个是ssdt hook ,一个是inline hook。
我们先用ssdt hook 随便 hook 一个ssdt函数,然后当RING3下调用这个函数时,就会进入到我们定义的函数内,而在我们的函数中,就可以通过mov eax,[ebp+4],得到的eax值就在KiFastCallEntry函数的代码范围内了。具体原理如下:
这是一段取自KiFastCallEntry的汇编代码:
代码:
8053e71c 8b3f            mov     edi,dword ptr [edi]
8053e71e 8b1c87          mov     ebx,dword ptr [edi+eax*4]
8053e721 2be1            sub     esp,ecx
8053e723 c1e902          shr     ecx,2
8053e726 8bfc            mov     edi,esp
8053e728 3b35549b5580    cmp     esi,dword ptr [nt!MmUserProbeAddress (80559b54)]
8053e72e 0f83a8010000    jae     nt!KiSystemCallExit2+0x9f (8053e8dc)
8053e734 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
8053e736 ffd3            call    ebx
8053e738 8be5            mov     esp,ebp
其中:8053e736 ffd3            call    ebx这一句就是调用ssdt函数的call,而ebx就是ssdt中对应函数的地址。所以这里call进去,就会到达我们自己定义的函数内。而调用这个call ebx之前,会把下一条指令mov esp,ebp的地址放入堆栈中,便于调用完子程序后还能返回到程序的下一条指令,继续执行后面的代码。所以我们刚才说的mov eax,[ebp+4]就可以得到 这个栈中的地址。
下一步,我们就在KiFastCallEntry的代码里找一个合适的地方,用Inline hook 的方法,hook KiFastCallEntry吧。这里我们就用360软件hook 的地方吧,因为这个地方好处多多。
那么360到底hook 了哪 个点呢?答案就是:
8053e721 2be1            sub     esp,ecx
8053e723 c1e902          shr     ecx,2
这里就啰嗦几句吧。
我的电脑里没有装360,因此我hook 这个地方是没有问题的。昨天我也看了下,360目前仍然是hook的这个地方,因此如果你电脑里安装了360安全卫士,那么你再hook这个点,是不会成功的。
  关于ssdt hook 我之前发表过一篇文章,如果有不懂的可以去看看,为了论坛的文章质量,我这里就不重复叙述了。
新手学ssdt_hook
  讲讲Inline hook 吧。
  Inline hook 就是将想要hook 的地方用jmp 或者call指令修改,让它跳到自己的代码处,然后在自己的代码里面,执行完自己想要的功能后,再把这5个字节码还原。这里面涉及到的重点问题就是jmp_code的机器码获取。5个字节,第一个固定为指令jmp 是E9,call 是E8。。其它的自己查查看。后面四个字节该怎样获得呢,方法是:后四字节内容=新函数的地址-旧函数的地址-5。为什么要这样做呢?因为机器码跳转的位置不是通过硬编码得到的,而是通过计算两个位置间的距离,那为什么还要减5呢,因为计算两条指令之前的距离是在这条指令之后到下一条指令开始的距离,而jmp这类指令占5个字节,所以就减5,如果是je这种小跳转,那就是减2。
Inline hook 做好后,现在就该 在我们自己的函数里面做文章了。通过分析KiFastCallEntry 的汇编代码发现,360hook的那个地方简直是风水宝地,因为在那里即能得到ssdt的地址,还能得到ssdt地址总表,更能得到ssdt索引号。所以这个地方集天地之灵气,360现在都还舍不得离开,可见发现这个地方的人是怎样一位大牛。
好了,回到正题 上来吧。既然这个地方能够得到这么多有用信息,那我们怎样利用呢?
首先,我们通过PsGetCurrentProcess函数得到一个EPROCESS,再通过偏移得到当前进程的进程名,然后我们判断,如果这个进程名(假设我们这里要让CE走新内核)是cheatengine-i38,这里为什么我没把进程名写全呢?因为EPROCESS这个结构的这一项只能放这么多个字符,所以只有这样写才能够正确判断,如果想写全的话,可以把CE进程名改短点。得到这个进程名后,我们就进行判断,如果是CE的进程,我们就让它走新内核,怎样给它指路呢?只需要返回新内核对应的ssdt函数地址就行了。这里得到了函数索引号,所以这一步就完成了。
下一步就是在我们和程序返回之前,修改堆栈中的ebx的值,用什么值修改呢?当然是用上面返回的新内核的ssdt地址修改了,修改方法是mov [esp+0x14],eax。
这里大家估计有疑问了:为什么修改ebx?我们看看KiFastCallEntry的代码就知道了。
8053e734 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
8053e736 ffd3            call    ebx
8053e738 8be5            mov     esp,ebp
看到了吧,因为下面call 的寄存器就是ebx,但是这并不是一层不变的,呵呵,别担心,一层不变不是说每一次开机后这个就会变,变是地方是xp和win7系统的变化,在xp中,是ebx,在win7中就是edx了,所以如果想把本代码放在win7上用,这里是必须要改的。
好了,废话也说了这么多了,再来看看代码吧。
代码:
ULONG display(ULONG ServiceTableBase,ULONG FuncIndex,ULONG OrigFuncAddress)
{
  if (ServiceTableBase==(ULONG)KeServiceDescriptorTable.ServiceTableBase)
  {//比较当前调用的进程是不是ce
    if (!strcmp((char*)PsGetCurrentProcess()+0x174,"cheatengine-i38"))
    {
      return pNewSSDT->ServiceTableBase[FuncIndex];
    }
  }
  return OrigFuncAddress;
}
__declspec(naked)
  void MyKiFastCallEntry()
{
  __asm
  {
    pushad
      pushfd
      push  ebx
      push  eax
      push  edi
      call  display
      //再返回前修改堆栈里的数据
      mov    [esp+0x14],eax
      popfd
      popad
      //恢复以前的代码,以便内核正常运行
      sub esp,ecx
      shr ecx,2
      jmp jmp_ret
  }
}

void hook_KiFastCallEntry()
{
  UCHAR jmp_code[5];
  jmp_code[0]=0xe9;
  //计算jmp_code
  *(ULONG *)&jmp_code[1]=(ULONG)MyKiFastCallEntry-5-addr_hookaddr;
  //计算返回jmp
  jmp_ret = addr_hookaddr + 5;
  PageProtectOff();
  //inline hook
  RtlCopyMemory((PVOID)addr_hookaddr,jmp_code,5);
  PageProtectOn();
}
到这里,重载内核的内容和目的我们都达成了,来个图看看效果。
  重载内核全程分析笔记_第3张图片
上面的图片我加载了两个驱动程序,一个是ssdt hook,一个是ReloadKernel,
通过上图可以发现OD走的老内核,由于NtOpenProcess函数被hook了,所以进程列表中连计算器的进程都找不到,而CE走的新内核,不仅能找到,还能正常打开进程,并进行数据搜索功能。
结束了,送一句结束语吧。
写驱动,犹如走夜路,步步惊心,或许下一秒,电脑就蓝了!

你可能感兴趣的:(重载内核全程分析笔记)