前言
知识来源:《加密与解密》
第一章启动函数
Windows程序在按顺序执行时不是直接从WinMain函数开始的,而是执行别的函数进行初始化,之后调用WinMain函数
(选择WinMain,交叉引用就可以找到上一级)
第二章函数
1. 识别函数
- 编译器都会使用call与ret指令来进行函数的调用与返回。
- call可以直接跳到被调用函数的地址段。call与跳转指令有着分别不同的是,call保存返回的地址到栈中,当执行到ret的时候ret就会使用call保存在栈里的地址。
- call用于函数的开始,ret用于函数的结束(不是所有ret都代表这函数的结束)。
int Add(int x,int y);
main( )
{
int a=5,b=6;
Add(a,b);
return 0;
}
Add(int x,int y)
{
return(x+y);
}
int Add(int x, int y);
void main()
{
003517A0 push ebp
003517A1 mov ebp,esp
003517A3 sub esp,0D8h
003517A9 push ebx
003517AA push esi
003517AB push edi
003517AC lea edi,[ebp-18h]
003517AF mov ecx,6
003517B4 mov eax,0CCCCCCCCh
003517B9 rep stos dword ptr es:[edi]
003517BB mov ecx,offset _99047FA2_源@cpp (035C000h)
003517C0 call @__CheckForDebuggerJustMyCode@4 (035130Ch)
int a = 5, b = 6;
003517C5 mov dword ptr [a],5
003517CC mov dword ptr [b],6
Add(a,b);
003517D3 mov eax,dword ptr [b]
003517D6 push eax
003517D7 mov ecx,dword ptr [a]
003517DA push ecx
003517DB call Add (03511B3h) //函数的开始
003517E0 add esp,8
}
int Add(int x, int y)
{
00351740 push ebp
00351741 mov ebp,esp
00351743 sub esp,0C0h
00351749 push ebx
0035174A push esi
0035174B push edi
0035174C mov edi,ebp
0035174E xor ecx,ecx
00351750 mov eax,0CCCCCCCCh
00351755 rep stos dword ptr es:[edi]
00351757 mov ecx,offset _99047FA2_源@cpp (035C000h)
0035175C call @__CheckForDebuggerJustMyCode@4 (035130Ch)
return (x + y);
00351761 mov eax,dword ptr [x]
00351764 add eax,dword ptr [y]
}
00351767 pop edi
00351768 pop esi
00351769 pop ebx
0035176A add esp,0C0h
00351770 cmp ebp,esp
00351772 call __RTC_CheckEsp (0351235h)
00351777 mov esp,ebp
00351779 pop ebp
0035177A ret //函数的结束
2. 函数的参数
- 函数传参的三种方式,栈方式,寄存器方式,全局变量
3. 函数的返回值
- return返回值
- 一般情况下函数的返回值会放在eax中,如果处理的大小超过eax寄存器的容量,edx寄存器中。
- 通过参数按传引方式返回值
传递两种方式分别是,传值,引用 - 传值,引用
示例代码
#include <stdio.h>
void max(int *a, int *b);
void main()
{
int a = 5;
int b = 6;
max(&a, &b); //获取变量的内存地址
printf("a,b中较大的数是:%d",a);
}
void max(int *a, int *b)
{ //if 不带{} 就会影响下一行缩进的代码,如果下列多行代码缩进只会影响if下一行的代码,剩下的无条件执行
if (*a < *b) //*a 代表间接变量去到&a的内存地址 *a 就是取到这个内存地址为保存的的值,然后今进行比较
*a = *b; // 把*b的值赋值到*a位置
}
00B818D5 mov ecx,offset _99047FA2_源@cpp (0B8C003h)
00B818DA call @__CheckForDebuggerJustMyCode@4 (0B8131Bh)
int a = 5;
00B818DF mov dword ptr [a],5 //a赋值
int b = 6;
00B818E6 mov dword ptr [b],6 //b赋值
max(&a, &b); //获取变量的内存地址
00B818ED lea eax,[b] //第二个参数 lea取的是地址
00B818F0 push eax
00B818F1 lea ecx,[a] //第一个参数 lea取的是地址
00B818F4 push ecx
00B818F5 call max (0B81280h) //调用 max 子程序
00B818FA add esp,8 (_cdecl)
max()
00B81787 mov ecx,offset _99047FA2_源@cpp (0B8C003h)
00B8178C call @__CheckForDebuggerJustMyCode@4 (0B8131Bh)
if (*a < *b)
00B81791 mov eax,dword ptr [a] //获取a在内存中的十六进制
00B81794 mov ecx,dword ptr [b] //获取b在内存中的十六进制
00B81797 mov edx,dword ptr [eax] //edx = eax
00B81799 cmp edx,dword ptr [ecx] //判断 *a<*b 是否满足条件
00B8179B jge __$EncStackInitStart+2Bh (0B817A7h) //大于等于跳转
*a = *b;
00B8179D mov eax,dword ptr [a] //a
00B817A0 mov ecx,dword ptr [b] //b
00B817A3 mov edx,dword ptr [ecx] // edx=b
00B817A5 mov dword ptr [eax],edx // a = b 赋值 并且作为返回值
}
00B817A7 pop edi //跳到这什么都不改
00B817A8 pop esi
00B817A9 pop ebx
00B817AA add esp,0C0h
00B817B0 cmp ebp,esp
00B817B2 call __RTC_CheckEsp (0B8123Fh)
00B817B7 mov esp,ebp
00B817B9 pop ebp
00B817BA ret
第三章数据结构
1.局部函数
- 利用栈存放局部变量
程序sub esp,8
这句话就是为局部变量分配分配内存空间,用[ebp-xxxx]
寻址调用这些变量,而参数调用相对于ebp偏移量正的,即[ebp+xxxx]
。编译在优化模式下。 当函数退出时,用add esp, 8
指令平衡栈,以释放局部变量占用的内存,有的编译器给esp 添加负值进行内存的分配,编译器也有可能会用push reg代替sub esp,4。 - 局部变量分配与清除栈的方式
形式一 | 形式二 | 形式三 |
---|---|---|
add esp,n …. sub esp,n |
add esp,-n …. sub esp,-n |
push reg … pop reg |
- 利用寄存器存放局部变量
int add(int x,int y);
int main(void)
{
int a=5,b=6;
add(a,b);
return 0;
}
int add(int x,int y)
{
int z;
z=x+y;
return(z);
}
.text:004117B0 push ebp
.text:004117B1 mov ebp, esp
.text:004117B3 sub esp, 0D8h //局部变量申请的栈内存空间
.text:004117B9 push ebx
.text:004117BA push esi
.text:004117BB push edi
.text:004117BC lea edi, [ebp+var_18]
.text:004117BF mov ecx, 6
.text:004117C4 mov eax, 0CCCCCCCCh
.text:004117C9 rep stosd
.text:004117CB mov ecx, offset unk_41C000
.text:004117D0 call j_@__CheckForDebuggerJustMyCode@4 ; __CheckForDebuggerJustMyCode(x)
.text:004117D5 mov [ebp+var_8], 5 //局部变量放入var_8中
.text:004117DC mov [ebp+var_14], 6 //局部变量放入var_14中
.text:004117E3 mov eax, [ebp+var_14] //取出var_14放入eax
.text:004117E6 push eax
.text:004117E7 mov ecx, [ebp+var_8] //取出var_8放入ecx
.text:004117EA push ecx
.text:004117EB call sub_4112AD
.text:004117F0 add esp, 8 //平栈 _cdecl
.text:004117F3 xor eax, eax
.text:004117F5 pop edi
.text:004117F6 pop esi
.text:004117F7 pop ebx
.text:004117F8 add esp, 0D8h //回收申请的栈内存空间
.text:004117FE cmp ebp, esp
.text:00411800 call j___RTC_CheckEsp
.text:00411805 mov esp, ebp
.text:00411807 pop ebp
.text:00411808 retn
.text:00411808 _main_0 endp
.text:00411808
- 利用寄存器存放局部变量
除了esp,ebp栈寄存器,其余6个通用寄存器尽可能的存放变量,这样可以减少代码提高程序的执行效率,逆向分析中
局部变量生命周期短,必须及时确定当前寄存器的变量是那个变量
2.全局变量
全局变量通常位于.data段,程序访问全局变量时,一般会用固定的硬编码地址对内存地址进行寻址
如果某个函数的改变了全局变量的值,就能影响其他函数,可以利用全局变量传递参数或者函数的返回值。
全局变量在程序的执行过程中占用内存单元,不像局部变量那样需要的时候才开辟存储单元。
源码示例
int z;
int add(int x,int y);
int main(void)
{
int a=5,b=6;
z=7;
add(a,b);
return 0;
}
int add(int x,int y)
{
return(x+y+z);
}
main
push ebp
mov ebp, esp
sub esp, 0D8h //局部变量申请空间
.......
mov ecx, 6
mov eax, 0CCCCCCCCh
rep stosd
mov ecx, offset unk_41C000
call j_@__CheckForDebuggerJustMyCode@4 ; __CheckForDebuggerJustMyCode(x)
mov [ebp+var_8], 5 //局部变量
mov [ebp+var_14], 6 //局部变量
mov dword_41A138, 7 //全局变量
mov eax, [ebp+var_14]
push eax
......
retn
add()
push ebp
mov ebp, esp
sub esp, 0C0h
......
mov eax, 0CCCCCCCCh
rep stosd
mov ecx, offset unk_41C000
call j_@__CheckForDebuggerJustMyCode@4 ; __CheckForDebuggerJustMyCode(x)
mov eax, [ebp+arg_0]
add eax, [ebp+arg_4]
add eax, dword_41A138 //全局变量
pop edi
......
call j___RTC_CheckEsp
mov esp, ebp
pop ebp
retn
charGPT回答
可以通过查看汇编代码中的内存地址来判断变量是局部变量还是全局变量。在汇编中,局部变量通常存储在堆栈中,而全局变量则存储在数据段中。通过查看变量的内存地址,如果它在堆栈范围内,则是局部变量,否则是全局变量。
3.数组
数组在内存中是连续存放的,在汇编访问状态下访问数组一般是通过基址加变址寻址的方式实现的。
#include <stdio.h>
int main(void)
{
static int a[3]={0x11,0x22,0x33};
int i,s=0,b[3];
for(i=0;i<3;i++)
{
s=s+a[i];
b[i]=s;
}
for(i=0;i<3;i++)
{
printf("%d\n",b[i]);
}
return 0;
}
.text:00401009 mov edi, dword_407030[eax] ; ;dword_407030是数组的空间位置, eax 现在是0 指向第一个元素
.text:0040100F add eax, 4 ; eax = 4 这是偏移下一次的地址指向了dword_407034
.text:00401012 add ecx, edi ; ;ecx = dword_407030 数组列表
.text:00401014 cmp eax, 0Ch ; ;if eax == 12
.text:00401017 mov [esp+eax+14h+var_10], ecx ; [esp+eax+14h+var_10] = dword_407030
.text:0040101B jl short loc_401009 ; 等于12的时候就不跳转了
.text:0040101D lea esi, [esp+14h+var_C] ; 数组列表现在到esi中了
.text:00401021 mov edi, 3 ; edi = 3 覆盖了数组元素
.text:00401026
.text:00401026 loc_401026: ; CODE XREF: _main+3A↓j
.text:00401026 mov eax, [esi] ; 第二个循环
.text:00401028 push eax
.text:00401029 push offset aD ; "%d"
.text:0040102E call printf ; 打印一次
.text:00401033 add esp, 8 ; 栈平衡
.text:00401036 add esi, 4 ; 数组指向下一个元素
.text:00401039 dec edi ; edi = 3-1计数器
.text:0040103A jnz short loc_401026 ; 第二个循环
.text:0040103C pop edi
.text:0040103D xor eax, eax
.text:0040103F pop esi
.text:00401040 add esp, 0Ch
.text:00401043 retn
.text:00401043 _main endp
dword_407030
.data:0040702C align 10h ;[0]第一个元素
.data:00407030 dword_407030 dd 11h ; DATA XREF: _main:loc_401009↑r
.data:00407034 db 22h ; "
.data:00407035 db 0
.data:00407036 db 0
.data:00407037 db 0
.data:00407038 db 33h ;最后指向的元素
.data:00407039 db 0
.data:0040703A db 0
.data:0040703B db 0
.data:0040703C aD db '%d' ; DATA XREF: _main+29↑o
.data:0040703E db 0Ah
.data:0040703F db 0
第四章虚函数
虚函数是在程序运行之前定义的函数,虚函数的地址不能在编译时确定,只能在调用即将进行时确定。所有的虚函数引用通常放在一个专用的数组————虚函数表,数组中的每个元素存放的就是类中的虚函数地址。
- 调用虚函数时,程序先从虚函数表指针,得到函数地址,再根据取出的函数地址调用函数
- 对象实例 –> 函数表 –> 要调用的函数的地址
#include <stdio.h>
class CSum
{
public:
virtual int Add(int a, int b)
{
return (a + b);
}
virtual int Sub(int a, int b )
{
return (a - b);
}
};
void main()
{
CSum* pCSum = new CSum ;
pCSum->Add(1,2);
pCSum->Sub(1,2);
}
.text:00401000 push esi
.text:00401001 push 4 ; Size
.text:00401003 call ??2@YAPAXI@Z ; new()函数,这里决定了虚表的位置
.text:00401008 add esp, 4
.text:0040100B test eax, eax
.text:0040100D jz short loc_401019
.text:0040100F mov dword ptr [eax], offset off_4050A0 ; 这是指向虚表的的指针,eax的地址存放的内容是004050A0
.text:00401015 mov esi, eax ; esi=eax的内存地址
.text:00401017 jmp short loc_40101B ; eax = esi = eax存放在该地址的内容 = 004050A0,[]取的是内容
.text:00401019 ; ---------------------------------------------------------------------------
.text:00401019
.text:00401019 loc_401019: ; CODE XREF: _main+D↑j
.text:00401019 xor esi, esi
.text:0040101B
.text:0040101B loc_40101B: ; CODE XREF: _main+17↑j
.text:0040101B mov eax, [esi] ; eax = esi = eax存放在该地址的内容 = 004050A0,[]取的是内容
.text:0040101D push 2
.text:0040101F push 1
.text:00401021 mov ecx, esi ; ecx = esi = eax的内存地址
.text:00401023 call dword ptr [eax] ; 第一次调用函数
.text:00401025 mov edx, [esi] ; edx = 004050A0
.text:00401027 push 2
.text:00401029 push 1
.text:0040102B mov ecx, esi
.text:0040102D call dword ptr [edx+4] ; 因为在内存中是连续的,所以第二次调用+4的偏移。004050A4 = 4 + (edx = 004050A0)
.text:00401030 pop esi
.text:00401031 retn
.text:00401031 _main endp
现在可以看看004050A0这位置存放的是什么
.rdata:004050A0 off_4050A0 dd offset sub_401040 ; DATA XREF: _main+F↑o //add函数
.rdata:004050A4 dd offset sub_401050 //sub函数
004050A0、004050A4就是不同函数的地址
第五章控制流程语句
1. if判断
2. switch
#include <stdio.h>
int main(void)
{
int a;
scanf("%d",&a);
switch(a)
{
case 1 :printf("a=1");
break;
case 2 :printf("a=2");
break;
case 10:printf("a=10");
break;
default :printf("a=default");
break;
}
return 0;
}
.text:0040100A push offset Format ; "%d"
.text:0040100F call _scanf ; scanf
.text:00401014 add esp, 8 ; 堆栈平衡
.text:00401017 mov ecx, [ebp+var_4]
.text:0040101A mov [ebp+var_8], ecx ; switch(a)
.text:0040101D cmp [ebp+var_8], 1 ; case 1:
.text:00401021 jz short loc_401031 ; print
.text:00401023 cmp [ebp+var_8], 2 ; case 2:
.text:00401027 jz short loc_401040
.text:00401029 cmp [ebp+var_8], 0Ah ; case 10:
.text:0040102D jz short loc_40104F
.text:0040102F jmp short loc_40105E
优化后的
.text:00401006 push offset Format ; "%d"
.text:0040100B call _scanf
.text:00401010 mov eax, [esp+0Ch+var_4]
.text:00401014 add esp, 8
.text:00401017 dec eax
.text:00401018 jz short loc_401055
.text:0040101A dec eax
.text:0040101B jz short loc_401044
.text:0040101D sub eax, 8
.text:00401020 jz short loc_401033
.text:00401022 push offset aADefault ; "a=default"
.text:00401027 call sub_401070
.text:0040102C add esp, 4
.text:0040102F xor eax, eax
最大的区别就是原先使用的时内存寻址现在使用的是寄存器,调用寄存器确实比寻址快,就是寄存器少。
2.2 switch跳转表
这个switch逆向有点东西,这个因为case多会生成switch跳转表再由跳转表进行跳转
#include <stdio.h>
int main(void)
{
int a;
scanf("%d",&a);
switch(a)
{
case 1 :printf("a=1");
break;
case 2 :printf("a=2");
break;
case 3:printf("a=3");
break;
case 4:printf("a=4");
break;
case 5:printf("a=5");
break;
case 6:printf("a=6");
break;
case 7:printf("a=7");
break;
default :printf("a=default");
break;
}
return 0;
}
假如我们输入的的是7
.text:00401000 push ecx
.text:00401001 lea eax, [esp+4+var_4]
.text:00401005 push eax
.text:00401006 push offset Format ; "%d"
.text:0040100B call _scanf
.text:00401010 mov ecx, [esp+0Ch+var_4] ; ecx是存放的是从缓冲区读取的内容
.text:00401014 add esp, 8
.text:00401017 lea eax, [ecx-1] ; switch 7 cases
.text:0040101A cmp eax, 6 ; 判断eax是否在六case之内。大于6进行ja跳转就是default分支
.text:0040101D ja short def_40101F ; jumptable 0040101F default case
.text:0040101F jmp ds:jpt_40101F[eax*4] ; switch jump 要在4010B0的地址偏移量(eax * 4 =要跳转的分支)
走到00401017
就会变成6,然后到0040101A
进行比较相等不会触发ja
跳转, 到0040101F
的时候就是(eax=6)*4 = 24
然后转换成16进制得到的偏移量就是18
得到的地址就是0040108C
.text:0040108C
.text:0040108C loc_40108C: ; CODE XREF: _main+1F↑j
.text:0040108C ; DATA XREF: .text:jpt_40101F↓o
.text:0040108C push offset aA7 ; jumptable 0040101F case 7 这是字符串
.text:00401091 call printf
.text:00401096 add esp, 4
.text:00401099 xor eax, eax
.text:0040109B pop ecx
.text:0040109C retn
- 转移指令机器码计算(跳过)
- 条件设置指令 (跳过)
第六章循环语句
第七章数学运算符
1. 加法
#include <stdio.h>
int main(void)
{
int a, b;
printf("%d",a+b+0x78);
return 0;
}
lea 是一条纯计算指令
.text:00401000 push ecx
.text:00401001 mov eax, [esp+4+var_4] ; int a
.text:00401005 mov ecx, [esp+4+var_4] ; int b
.text:00401009 lea edx, [ecx+eax+78h] ; 计算a+b+0x78,结果放到edx中
.text:0040100D push edx
.text:0040100E push offset aD ; "%d"
.text:00401013 call printf
- 除法(这个可以看C++反汇编大揭秘书上讲的比较好)
第八章文本字符串
1. 字符串的存储格式
- C字符串:也称ASCII字符串 “Z"表示其以”\0"为结束标识符。“\0”代表ASCII码为0的字符。ASCII为0的字符的时候显示空操作符
- DOS字符串: 以$结尾作为终止符,基本已被淘汰
- PASCAL字符串:这个字符串没有终止符,而是长度符号字符串的头部定义1个字节,用于指示当前字符串的长度,字符串的长度不能过255字符。
- Delphi字符串: 为了解决PASCAL 255长度限制设计的增加了对长度的支持,表示长度扩展到2字节,使字符串最大长度达到65535
- 四字节的Delphi: 少见,表示长度扩展到4字节字符串长度可以到达4GB
- 字符寻址指令
mov eax,[00401000] ;直接寻址,把00401000地址的双字节数据放到eax中
mov eax, [ecx] ;间接寻址, 即把ecx中的地址所指的内容放到eax
lea eax,[00401000] ;把00401000装到eax中
...