前言

知识来源:《加密与解密》

第一章启动函数

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. 函数的参数

  • 函数传参的三种方式,栈方式,寄存器方式,全局变量
    • 利用栈方式
      栈的基础知识
      常用的调用约定
    • 利用寄存器传参 Fastcall特点就快(因为寄存器传参的原因)
      不同编译器的Fastcall也是不一样的
      • Visual C++,函数参数左边2个不大于4字节(DWORD)参数放在eax,edx寄存器, 寄存器用完用栈,从从右到左压栈,子程序处理堆栈平衡。浮点值,远指针,_int64 总是栈来传递
      • Borland Delphi/C++ 的 Fastcall 左边3个不大于4字节(DWORD),分别放在eax,edx,ecx寄存器中其余参数按照PASCAL方式压入。

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. 转移指令机器码计算(跳过)
  2. 条件设置指令 (跳过)

第六章循环语句

for循环

第七章数学运算符

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
  1. 除法(这个可以看C++反汇编大揭秘书上讲的比较好)

第八章文本字符串

1. 字符串的存储格式

  • C字符串:也称ASCII字符串 “Z"表示其以”\0"为结束标识符。“\0”代表ASCII码为0的字符。ASCII为0的字符的时候显示空操作符
  • DOS字符串: 以$结尾作为终止符,基本已被淘汰
  • PASCAL字符串:这个字符串没有终止符,而是长度符号字符串的头部定义1个字节,用于指示当前字符串的长度,字符串的长度不能过255字符。
  • Delphi字符串: 为了解决PASCAL 255长度限制设计的增加了对长度的支持,表示长度扩展到2字节,使字符串最大长度达到65535
  • 四字节的Delphi: 少见,表示长度扩展到4字节字符串长度可以到达4GB
  1. 字符寻址指令
mov eax,[00401000] ;直接寻址,把00401000地址的双字节数据放到eax中
mov eax, [ecx] ;间接寻址, 即把ecx中的地址所指的内容放到eax
lea eax,[00401000] ;把00401000装到eax中