最近时间在学习这类的知识,为了自己的学习也为了和大家交流写了这个笔记,希望大家多提意见....
书分为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语言的练习
...