天书夜读(1)--Windows内核编程系列读书笔记(一)

时间:2021-04-11 01:14:56

最近时间在学习这类的知识,为了自己的学习也为了和大家交流写了这个笔记,希望大家多提意见....

书分为1:入手篇 熟悉汇编 2:基础篇 内核编程 3:探索篇 研究内核 4:深入篇 修干内核 5:实践篇 实际开发

五大部分

在下面的笔记中我将针对每一篇做单独的理解和提出疑问

第一篇 入手篇 熟悉汇编

分为如下章节 第一章:汇编指令与C语言 第二章:C语言的流程和处理 第三章:练习反汇编C语言程序

第一章
重点有三
(一)用vs查看汇编代码
(二)常用反汇编指令
(三)C函数的参数传递过程

 

(一)用vs查看汇编代码

          (1)VC必须处于调试状态才能看到汇编指令窗口,因此记得在返回前用F9建立断点;

          (2)按F5调试程序,打开菜单“Debug”(调试)下的“Windows”(窗口)子菜单,选择“Disassembly”(反汇编)

       如下图所示:


(二)常用反汇编指令

         (1)堆栈相关指令

                  push:把一个32位操作数加入堆栈中。esp(栈顶,栈顶为地址小的区域)减4。
    pop: 相反,esp加4,一个数据出栈。(pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中)

         (2)数据传送指令
   mov:数据移动。第一个参数为目的,第二个参数为源。在C语言中相当于赋值号。
   xor:异或。常用于对寄存器清零操作(例:xor eax,eax)。
   lea:取得地址(第二个参数)后放入前面的寄存器(第一个参数)中。(例:lea edi,[ebp-0cch])

         (3)跳转与比较指令
   jmp:无条件跳转。
   jg:大于的时候跳转。
   jl:小于的时候跳转。
   jge:大于等于的时候跳转。
   辅助指令:
   cmp:比较。往往是jg,jl,jge之类条件跳转指令的执行条件。
(4)其他常用指令
   sub:减法。(第一个参数是被减数所在寄存器,第二个参数是减数)
   add:加法。(用法同上)
   ret:返回。相当于跳转回 调用函数 的地方。(对应的call指令来调用函数,返回到call之后的下一条指令)
   call:调用函数。
   stos:串存储指令。(例:
    mov ecx,30h
    mov eax,0cccccccch
    rep stos dword ptr es:[edi]
    它的功能是将eax中的数据放到edi所指的地址中,同时,edi会增加4(字节数)。rep使指令重复执行ecx中填写的次数。)
   cmp:比较。往往是jg,jl,jge之类条件跳转指令的执行条件。
(三)C函数的参数传递过程
(1)C语言程序通过堆栈把参数外部传入到函数内部,此外,在堆栈中划分区域来容纳函数的内部变量。
(2)C语言默认的调用方式,堆栈总是 调用方 把参数反序(从右到左)压入堆栈中,被调用方 把堆栈复原。
(3)call和ret指令只是为了调用方便而已,绝不是函数存在的绝对证据。
(4)在Windows上的,常用的函数调用方式有:Pascal方式,WINAPI方式(_stdcall),C方式(_cdecl)。
   _cdecl C调用规则:
    (1)参数从右到左进入堆栈
    (2)在函数返回后,调用者要负责清除堆栈,所以这种调用常会生成较大的可执行程序。
   _stdcall 又称WINAPI,其调用规则:
    (1)参数从右到左进栈
    (2)被调用函数在返回之前自行清理堆栈,所以生成的代码比cdecl小
   Pascal 调用规则:(主要用于win16函数库中,现在基本不用)
    (1)参数从左到右入栈
    (2)被调用的函数在返回之前自行清理堆栈
    (3)不支持可变参数的函数的调用
   此外,在Windows内核中还常见的快速调用方式(_fastcall);在C++编译的代码中有this call方式。
(5)在Windows中,不管哪种调用方式都是返回值放到eax中,然后返回。
(6)标准的C函数调用方式的例子:
   过程(1)调用者把参数反序压入堆栈中(2)调用函数(3)调用者把堆栈清理复原
   该方式(_cdecl)下调用函数需要做一下一些事情:
   (1)保存ebp。ebp总是被我们用来保存这个函数执行之前的esp的值。执行完毕后,我们用ebp恢复esp;同时调用此函数的上层函数也用esp作同样的事情。所以先把ebp压入堆栈,返回之前弹出,避免ebp被我们改动。
   (2)保存esp到ebp。
    上面的两部代码如下:
    push ebp
    mov ebp,esp
   (3)在堆栈中腾出一个区域用来保存局部变量,这就是常说的所谓的局部变量时保存在栈空间中的,方法是:让esp减去一个数值,这样就等于压入了一堆变量。要恢复时,只要把esp恢复成ebp中保存的数据就可以了。
   (4)保存ebx、esi、edi到堆栈中,函数调用完后恢复。
    对应的代码如下:
    sub esp,0cch
    push ebx ;下面保存三个寄存器ebx、esi、edi
    push esi
    push edi
   (5)把局部变量初始化为全0cccccccch。occh实际是int 3指令的机器码,这是一个断点中断指令。因为局部变量不可能被执行,如果执行了,必然程序有错,这是发生中断来提示开发者。这是VC编译Debug版本的特有操作。
    相关代码如下:
    lea edi,[ebp-0cch] ;本来是要mov edi,ebp-0cch,但是mov不支持ebp-0cch这样的参数,所以对ebp-0cch取内容,而lea把内容的地址,也就是ebp-0cch加载到edi中。目的是把保存局部变量的区域(从ebp-0cch开始的区域)初始化成全0cccccccch
    mov ecx,33h
    mov eax,0cccccccch
    rep stos dword ptr [edi] ;串写入;写入int 3中断指令0cch
   (6)然后做函数里应该做的事情。参数的获取是ebp+12字节为第二个参数,ebp+8为第一个参数(注意是:倒序压入),依次增加。最后ebp+4字节处是要返回的地址。
   (7)恢复ebx、esi、edi、esp、ebp,最后返回。代码如下:
    pop edi       ;恢复ebx、esi、edi
    pop esi
    pop ebx
    mov esp,ebp   ;恢复原来的ebp和esp,让上一个调用的函数正常使用
    pop ebp
    ret
       

第二章 C语言的流程和处理
这章的重点是对C语言中常见的流程和处理等的反汇编的认识,分为四个重点
(一)C语言的循环的反汇编 
(二)C语言判断与分支的反汇编
(三)C语言的数组与结构
(四)C语言的共用体和枚举类型

 

(一)C语言的循环的反汇编
(1)for循环
例子:
int i;
for (i=0;i<50;i++)
004117D7 mov         dword ptr [i],0    ;初始化i 
004117DE jmp         myfunction+39h (4117E9h) 
004117E0 mov         eax,dword ptr [i] 
004117E3 add         eax,1 
004117E6 mov         dword ptr [i],eax 
004117E9 cmp         dword ptr [i],32h 
004117ED jge         myfunction+4Ah (4117FAh) 
{
   c=c+i;
004117EF mov         eax,dword ptr [c] 
004117F2 add         eax,dword ptr [i] 
004117F5 mov         dword ptr [c],eax 
}
004117F8 jmp         myfunction+30h (4117E0h) 
return c;
004117FA mov         eax,dword ptr [c] 
可见for循环主要用这么几条指令来实现:mov 进行初始化;jmp跳过修改循环变量的代码;cmp实现条件的判断;jge根据条件跳转。用jmp回到修改循环变量的代码进行下一次的循环。大体结构如下:
   mov <循环变量>,<初始值> ;给循环变量赋初始值
   jmp B    ;跳到第一次循环处
   A:(改动循环变量) ;修改循环变量
   ...
   B:cmp<循环变量>,<限制变量> ;检查循环条件
   jge 跳出循环
   (循环体)
   ...
   jmp A    ;跳回去修改循环变量
(2)do循环
            因为do循环没有修改循环变量的部分,所以比for循环要简单一些。
        int i=0;
004117D7 mov         dword ptr [i],0 
do{
   c=c+i;
004117DE mov         eax,dword ptr [c] 
004117E1 add         eax,dword ptr [i] 
004117E4 mov         dword ptr [c],eax 
}while(c<100);
004117E7 cmp         dword ptr [c],64h 
004117EB jl          myfunction+2Eh (4117DEh) 
return c;
004117ED mov         eax,dword ptr [c] 
上面的do循环使用一个简单的条件比较指令跳转回去。只有两条指令:
cmp <循环变量>,<限制变量>
jl <循环开始点>
(3)while循环
int i=0;
004117D7 mov         dword ptr [i],0 
while(c<100)
004117DE cmp         dword ptr [c],64h 
004117E2 jge         myfunction+3Fh (4117EFh) 
{
   c=c+i;
004117E4 mov         eax,dword ptr [c] 
004117E7 add         eax,dword ptr [i] 
004117EA mov         dword ptr [c],eax 
}
004117ED jmp         myfunction+2Eh (4117DEh) 
return c;
004117EF mov         eax,dword ptr [c] 
我们发现while循环更复杂。因为while除了开始的时候判断循环条件之外,后面还必须有一条无条件跳转回到循环开始的地方,共用3跳指令实现。
A: cmp <循环变量>,<限制循环>
    jge B
    (循环体)
    ...
    jmp A
B:(循环结束了)

(二)C语言判断与分支的反汇编
(1)if-else判断分支
if (c>0&&c<10)
004113C5 cmp         dword ptr [c],0 
004113C9 jle         wmain+4Ah (4113EAh) 
004113CB cmp         dword ptr [c],0Ah 
004113CF jge         wmain+4Ah (4113EAh) 
{
   printf("c>0 && c<10/n");
004113D1 mov         esi,esp 
004113D3 push        offset string "c>0 && c<10/n" (41575Ch) 
004113D8 call        dword ptr [__imp__printf (4182BCh)] 
004113DE add         esp,4 
004113E1 cmp         esi,esp 
004113E3 call        @ILT+320(__RTC_CheckEsp) (411145h) 
004113E8 jmp         wmain+86h (411426h) 
}
else if (c>10&&c<100)
004113EA cmp         dword ptr [c],0Ah 
004113EE jle         wmain+6Fh (41140Fh) 
004113F0 cmp         dword ptr [c],64h 
004113F4 jge         wmain+6Fh (41140Fh) 
{
   printf("c>10 && c<100/n");
004113F6 mov         esi,esp 
004113F8 push        offset string "c>10 && c<100/n" (415748h) 
004113FD call        dword ptr [__imp__printf (4182BCh)] 
00411403 add         esp,4 
00411406 cmp         esi,esp 
00411408 call        @ILT+320(__RTC_CheckEsp) (411145h) 
}
else
0041140D jmp         wmain+86h (411426h) 
   printf("c>=100/n");
0041140F mov         esi,esp 
00411411 push        offset string "c>=100/n" (41573Ch) 
00411416 call        dword ptr [__imp__printf (4182BCh)] 
0041141C add         esp,4 
0041141F cmp         esi,esp 
00411421 call        @ILT+320(__RTC_CheckEsp) (411145h) 
if判断都是使用cmp再加上条件跳转指令。
   cmp <条件>
   jle <下一个分支>
else if和else的特点是,在开始的地方,都有一条无条件跳转指令,跳转到判断结束处,阻止前面的分支执行结束后,直接进入这个分支的可能。这个分支能执行的唯一途径是前面的判断条件不足。
else则在jmp之后直接执行操作,而else if则开始重复if之后的操作,用cmp比较,然后用条件跳转到指令进行的跳转。

(2)switch-case判断分支
switch (c)
004113C5 mov         eax,dword ptr [c] 
004113C8 mov         dword ptr [ebp-0D0h],eax 
004113CE cmp         dword ptr [ebp-0D0h],0 
004113D5 je          wmain+42h (4113E2h) 
004113D7 cmp         dword ptr [ebp-0D0h],1 
004113DE je          wmain+59h (4113F9h) 
004113E0 jmp         wmain+72h (411412h) 
{
case 0:
   printf("c<0");
004113E2 mov         esi,esp 
004113E4 push        offset string "c>10 && c<100/n" (415748h) 
004113E9 call        dword ptr [__imp__printf (4182BCh)] 
004113EF add         esp,4 
004113F2 cmp         esi,esp 
004113F4 call        @ILT+320(__RTC_CheckEsp) (411145h) 
case 1:
   {
    printf("c>10&&c<100");
004113F9 mov         esi,esp 
004113FB push        offset string "c>=100/n" (41573Ch) 
00411400 call        dword ptr [__imp__printf (4182BCh)] 
00411406 add         esp,4 
00411409 cmp         esi,esp 
0041140B call        @ILT+320(__RTC_CheckEsp) (411145h) 
    break;
00411410 jmp         wmain+89h (411429h) 
   }
default:
   printf("c>10&&c<100");
00411412 mov         esi,esp 
00411414 push        offset string "c>=100/n" (41573Ch) 
00411419 call        dword ptr [__imp__printf (4182BCh)] 
0041141F add         esp,4 
00411422 cmp         esi,esp 
00411424 call        @ILT+320(__RTC_CheckEsp) (411145h) 
}
switch显然不用判断大于小于,所以都是je,分别跳到每个case处。最后一个事无条件跳转,直接跳到default处。至于case和default都非常简单。如果有break,则会增加一个无条件跳转。
(三)C语言的数组与结构
typedef struct 
{
int a;
int b;
int c;
}mystruct;
int myfunction(int a,int b)
{
00413570 push        ebp 
00413571 mov         ebp,esp 
00413573 sub         esp,270h 
00413579 push        ebx 
0041357A push        esi 
0041357B push        edi 
0041357C lea         edi,[ebp-270h] 
00413582 mov         ecx,9Ch 
00413587 mov         eax,0CCCCCCCCh 
0041358C rep stos    dword ptr es:[edi] 
unsigned char* buf[100];
mystruct *strs=(mystruct*)buf;
0041358E lea         eax,[buf] 
00413594 mov         dword ptr [strs],eax 
int i;
for (i=0;i<5;i++)
0041359A mov         dword ptr [i],0 
004135A4 jmp         myfunction+45h (4135B5h) 
004135A6 mov         eax,dword ptr [i] 
004135AC add         eax,1 
004135AF mov         dword ptr [i],eax 
004135B5 cmp         dword ptr [i],5 
004135BC jge         myfunction+94h (413604h) 
{
   strs[i].a=0;
004135BE mov         eax,dword ptr [i] 
004135C4 imul        eax,eax,0Ch 
004135C7 mov         ecx,dword ptr [strs] 
004135CD mov         dword ptr [ecx+eax],0 
   strs[i].b=1;
004135D4 mov         eax,dword ptr [i] 
004135DA imul        eax,eax,0Ch 
004135DD mov         ecx,dword ptr [strs] 
004135E3 mov         dword ptr [ecx+eax+4],1 
   strs[i].c=2;
004135EB mov         eax,dword ptr [i] 
004135F1 imul        eax,eax,0Ch 
004135F4 mov         ecx,dword ptr [strs] 
004135FA mov         dword ptr [ecx+eax+8],2 
}
00413602 jmp         myfunction+36h (4135A6h) 
return 0;
00413604 xor         eax,eax 
}
imul指令让人联想到结构体数组,这是一个特征。程序在访问一个结构体的数组时,往往需要得到数组中某个结构体元素的开始地址。很显然,某个元素的起始地址=下标*单个元素的长度+数组的起始地址。其中单个元素的长度往往是个常数,这个数由编译器生成(如上面的0ch)。此后,程序中会出现用imul指令,将元素下表去乘这个常数的代码。

(四)C语言的共用体和枚举类型
共用体和枚举类型都是在C语言中为了让内容更加易读而引入的。实际上,只要有结构体和基本的数据类型就足够了,所以在汇编中,这些多余的东西消失不见了。

第三章 练习反汇编C语言程序
(一)算法的反汇编
(二)发行版的反汇编
(三)汇编反C语言的练习

(一)算法的反汇编
算法反汇编阅读技巧:首先,把流程控制的代码与数值计算的代码分开时关键;然后,得到数值计算的代码部分后,必须判断输入与输出(一般被读的内部变量为输入,被写的内部变量为输出);最后,把前两步分析的结果还原成一个C语言的表达式。
任何一段中间不加任何跳转,连续的mov和加减乘除的指令一般都可以还原为一个C表达式。
数组的访问的代码:
mov eax,<我要取的数组元素的下标>
imul eax,eax,<结构的大小>
mov ecx,<结构数组的开始地址>
mov eax,dword ptr [ecx+eax] ;取得数组元素的内容,放到eax中

(二)发行版的反汇编
到非调试版本的时候,编译器将进行非常多的优化,使汇编代码变得精简的同时,阅读的难度也大大增大了。
例:优化后的for循环喜欢模仿相对简单的do循环方式,把判断和跳转放到最后。

(三)汇编反C语言的练习

    ...