逆向之汇编笔记

一. 通用寄存器

数据寄存器 EAX, EBX, ECX, EDX (Data Register)

数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器的时间。   

32位CPU有4个32位的通用寄存器EAX、EBX、ECX和EDX。对低16位数据的存取,不会影响高16位的数据。这些低16位寄存器分别命名为:AX、BX、CX和DX,它和先前的CPU中的寄存器相一致。4个16位寄存器又可分割成8个独立的8位寄存器:

  • AX可分为AH和AL.

  • BX可分为BH和BL.

  • CX可分为CH和CL.

  • DX可分为DH和DL.

每个寄存器都有自己的名称,可独立存取。程序员可利用数据寄存器的这种“可分可合”的特性,灵活地处理字/字节的信息。

寄存器AX通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、除、输入/输出等操作,它们的使用频率很高;

寄存器BX称为基地址寄存器(Base Register)。它可作为存储器指针来使用;

寄存器CX称为计数寄存器(Count Register)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数;

寄存器DX称为数据寄存器(Data Register)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址。

指针寄存器 EBP, ESP (Pointer Register)32位CPU有2个32位通用寄存器EBP和ESP。其低16位对应先前CPU中的BP和SP,对低16位数据的存取,不影响高16位的数据。

指针寄存器主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。

寄存器BP称为基址指针寄存器(Base Pointer);

寄存器SP称为堆栈指针寄存器(Stack Pointer)。

变址寄存器 ESI, EDI (Index Register)32位CPU有2个32位通用寄存器ESI和EDI。其低16位对应先前CPU中的SI和DI,对低16位数据的存取,不影响高16位的数据。

变址寄存器主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。 变址寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。 寄存器SI称为源变址寄存器 (Source Index);

寄存器DI称为目的变址寄存器(Destination Index)。

附表:

MOV指令

语法:MOV r/m8,r8

MOV r/m16,r16 r 通用寄存器

MOV r/m32,r三二 m 代表内存

MOV r8,r/m8 imm代表立即数

MOV r16,r/m16 r8代表8位通用寄存器

MOV r三二,/r/m32 m8 代表8位内存

MOV r8,imm8 imm8代表8位立即数

MOV r16,imm16

MOV r三二,imm32

ADD指令:加法

SUB指令:减法

AND指令:与运算

OR指令: 或运算

XOR指令:异或运算

NOT指令:非运算 该运算的语法操作对象只是源操作数

二. 内存读写

BYTE 字节 = 8 (BIT) WORD 字 = 16(BIT) DWORD 双字 = 32(BIT)

  1. 内存格式

    每个内存单元的宽度是8 [编号]称为地址

  2. 从指定内存中写入读取数据

    mov dword ptr ds:[0x0012FF34],0x12345678

    mov eax,dword ptr ds:[0x12FF34]

    dword:要读写多少, 此时是4字节

    ptr:point 代表后面是一个指针

    ds:段寄存器

    [编号] 内存地址 必须为32位

三. 内存寻址

1.寻址公式一: (立即数)

读取内存的值:

MOV EAX,DWORD PTR DS:[0X13FFC四] 从地址中读取DWORD(四个字节的值):所以其实是 0X13FFC四,0X13FFC50X13FF,C6,0X13FFC7的值取到EAX

MOV EAX,DWORD PTR DS:[0X13FFC8]

写数据于内存:

MOV DWORD PTR DS:[0X13FFC四],EAX

获取内存编号:

LEA EAX,DWORD PTR DS:[0x13ffc四]:

2.寻址公式二:[reg]reg代表寄存器 可以是8个通用寄存器中的任意一个

读取内存的值:

MOV ECX,0X13FFD0 :先将地址给ECX

MOV EAX,DWORD PTR DS:[ECX] :将ECX中的值给EAX

向内存写数据

MOV EDX,0X13FFD8

MOV DWORD PTR DS:[EDX],0X87654321

获取内存编号

LEA EAX ,DOWRD PTR DS:[EDX] 将EDX的值(是一个地址)给EAX

MOV EAX,DWORD PTR DS:[EDX]

3.寻址公式三. [reg+立即数]

读内存的值

MOV ECX,0x13FFD0

MOV EAX,DWORD PTR DS:[ECX+4]

向内存中写数据

MOV EDX,0x13FFD8

MOV DWORD PTR DS:[EDX+0xC],0x87654321

获取内存编号

LEA EAX,DWORD PTR DS:[EDX+4] :获取EDX中的地址值+4后给EAX

MOV EAX,DWORD PTR DS:[EDX+4]

还有其他的几种情况

四. 堆栈

压入数据

设置栈顶和栈底:

MOV EBX,0x13FFDC BASE

MOV EDX,0x13FFDC TOP

方式一:

MOV DWORD PTR DS:[EDX-4],0xAAAAAAAA

SUB EDX,4

方式二:

SUB EDX,4

MOV DWORD PTR DS:[EDX],0XBBBBBBBB

方式三:

MOV DWORD PRT DS:[EDX-4],0xDDDDDDDD

LEA EDX,DWORD PTR DS:[EDX-4]

方式四:

LEA EDX,DWORD PTR DS:[EDX-4]

MOV DWORD PTR DS:[EDX],0XEEEEEEEE

使用PUSH压栈 POP出栈

PUSH 将后面的所跟的内容入栈 当跟的是内存的时候是将内存的值入栈

例如 PUSH DWORD PTR DS:[0XFFDA] 将0XFFDA内的值入栈

PUSHAD:将8个通用寄存器全部入栈,这样可以随便使用之后的寄存器

POPAD:将入栈了的8个寄存器全部出栈,恢复现场

五. 第一个CRACKME

需要的知识:PE结构,下断点,WIN32 API,知道什么是函数调用,熟悉堆栈,call,JCC,标志寄存器

六. 寄存器

1.进位寄存器CF(Carry Flag):一般用于无符号数,如果运算结果的最高位发生进位或借位,则值为1没否则为0

如:MOV AL,0xEF ADD AL,2

2.奇偶标志PF(Parity Falg)用于反应运算结果中"1"的个数的奇偶性,若“1”的个数是偶数则PF的值为1,否则为0

MOV AL,3 ADD AL,3 ADD AL,2 运行观察情况

3.辅助进位标志AF,当发生下列情况的时候AF置1否则为0

(1) 在字操作时,发生低字节向高字节进位或借位时

(2) 在字节操作时,发生低四位向高四位进位或借位时

MOV EAX,0x55EEFFFF(32位情况)

MOV AX,5EFE ADD AX,2 (16位情况)

4.零标志位ZF,ZF用来反映运算结果是否是0,若运算结果是0,则其值位1,否则为0

XOR(异或:对应的位相同为0) EAX,EAX 可以用来EAX清零

MOV EAX,2 SUB EAX,2

5.符号标志位SF:符号标志位用于反应运算结果的符号位,他与运算结果的最高位相同

MOV AL,7F (7F:0111 1111)最高位是0

ADD AL,2F

6.溢出标志位OF:OF用于反应有符号数加减运算得到的结果是否溢出,如果运算的结果超过当前运算位数所能表示的范围则称为溢出,OF置1

最高位进位与溢出的区别:

进位标志表示无符号数运算结果是否超出范围

溢出标志表示有符号数运算结果是否超出范围

首先:溢出主要是给有符号运算使用的,在有符号运算中,有如下规律:

正 + 正 = 正 如果结果是负数,则说明有溢出

负 + 负 = 负 如果结果是正数,则说明有溢出

正 + 负 永远不会溢出

ADC指令:带进位加法

格式:ADC R/M,R/M/IMM 两边不能同时为内存,数据宽度要一样

ADC BYTE PTR DS:[0x12FFC四],2

SBB指令,带借位减法

SBB BYTE PRE DS:[12FFC四],2

XCHG指令:交换数据

XCHG R/M,R/M

MOVS指令:移动数据 内存-内存(该指令是少有的可以源操作数和目的操作数同都可以是内存的指令)

MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] 数据宽度为字节,将ESI这个内存中的数据移动到EDI 简写为 MOVSB

MOVS WORD PTR ES:[EDI],WORD PTR DS:[ESI] 简写为WOVSW

MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[EDI] 简写为WOVSD

STOS指令:将AL/AX/EAX的值存储到[EDI]所指定的内存单元 (注意此时如果数据宽度是DWORD 则EDI的值也根据 数据宽度+4或者-4)

` STOS BYTE PTE ES:[EDI]简写为STOSB

STOS WORD PTE ES:[EDI]简写为STOSW

STOS WORD PTE ES:[EDI]简写为STOSD

到底是选AL还是AX还是EAX取决于选择的数据宽度是BYTE还是WORD还是DWORD

REP指令:按照计数寄存器(ECX)中指定的次数重复执行字符串指令

MOV ECX,10

REP MOVSD 重复执行十次将ESI指定内存中的数据移动到EDI

JMP指令:无条件修改EIP(存放下条要执行的指令)的值,

MOV EIP,寄存器/立即数 简写为JMP 寄存器/立即数

CALL指令:也是修改EIP为指定的值,同时将原来指令的下一条指令入栈

RET指令:和CALL成对出现,用于返回到CALL之前的指令,本质就是POP EIP

CMP指令:比较两个操作数,实际上相当于SUB指令,但是相减的结构并不保存到第一个操作数中,只是根据相减的结果来改变零标志位,即只是比较操作数但是不改变两个寄存器的值,当两个操作数相等的的时候,零标志位置1.

CMP R/M,R/M/IMM 注意相比较的时候的数据宽度要一样

TEST指令:该指令一定程度上和CMP指令类似,两个数值执行与操作,结果不保存(即不改变原来两个寄存器的值),但是会改变相应的标志位.常用于判断某寄存器的值是否为0

TEST R/M,R/M/IMM

七. JCC条件跳转指令

八. 重点! Win堆栈图

windows堆栈的特点:1.先进后出,2.向低地址扩展 3.堆栈平衡:函数用完堆栈之后堆栈要回复原状

https://www.cnblogs.com/Tkitn/p/12354131.html

OD:F8单步步过 F7单步步入

九. C语言

1. 窥探

VC6的使用以及调试方法

简单函数的堆栈汇编分析

对于一般的函数,编译器会做很多的处理,例如划分堆栈,保存现场等等

C语言裸函数 void __declspec(nacked) Function(){},对于裸函数,编译器是不会做任何的处理,但是在裸函数中需要写ret返回语句确保整个程序不出错

C语言中添加汇编代码的操作是 __asm{汇编代码}

//例如一个两数相加的裸函数
int __declspec(nacked) Plus(int x,int y){
__asm{
//参数
//局部变量
//返回值 
//保留调用前的堆栈
push ebp
//提升堆栈,其目的是位函数分配空间
mov ebp,esp
sub esp,0x40
//保留现场
push ebx
push esi
push edi
//开始填充缓冲区
mov eax,0xCCCCCCCC
mov ecx,0x10
lea edi,dword ptr ds:[ebp-40]
rep stosd 
//函数的核心功能
mov eax,dword ptr ds:[ebp+8]
add eax,dword ptr ds:[ebp+0xc]
//回复现场
pop dei
pop edi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp         
ret
}
}
注意局部变量是从ebp-4的位置开始存储的

fastcall方法中参数超过两个效果就不是很显著了。fastcall快的原因是其参数直接在寄存器(ECX/EDX传送前两个)传递而不是堆栈

2. 数据类型

float在内存中的存储方式

各个字母字符的存储:ASCII表 拓展ASCII表

中文的GB2312

3. 内存与变量

1.全局变量的识别:MOV 寄存器,byte/word/dword ptr ds:[0x123113];通过寄存器的宽度或者byte/word/dword来判断全局变量的宽度。全局变量就是基址

2.局部变量的反汇编识别:[ebp-4] [ebp-8] [ebp-0xC]

3.判断一个函数到底有几个参数,以及分别是什么:

一般情况:

步骤一:观察调用除的代码:

push 3

push 2

push 1

call 0x0040100f

步骤二:找到平衡堆栈的代码继续论证:

call 0040100f

add esp,0Ch

或者函数内部做堆栈平衡:ret 4(一个参数)/8(两个)/0xC(三个)/0x10(四个)

观察步骤:

  1. 先不考虑ebp,esp

  2. 只找给别人赋值的寄存器eax/ecx/edx/ebx/esi/edi 但是注意源操作数是立即数的指令一定不是,因为参数是在函数调用之后从寄存器调用的

  3. 找到后追查来源,如果该寄存器中的值不是在函数内存赋值的,那一定是传进来的参数

  4. 公式1:寄存器 + ret 4 = 参数个数

  5. 公式2:寄存器 + [ebp+8] + [ebp+0x] = 参数个数

简单的一个小代码分析函数的变脸和参数情况

IF_BEGIN:

先执行各类影响标志位的指令

jxx ElSE BEGIN

IF_END:

jmp END

ELSE_BEGIN

.........

ELSE_END

END

一维数组 int arr[12] = {1,2,3,4,5,6,7,8,9,10,11}的反汇编

二维数组反汇编int arr[3][4]= {{....},{....},{....}}:和上面一样没有区别

int arr[3][4]={

{1,2},

{5},

{9}

}的反汇编是这样的:

编译器对于多维数组和一维数组其实是一样的

1.小于32位的局部变量,空间在分配的时候,按照32位分配

2.使用的时候按照实际的宽度使用

3.不要定义Char/short类型的局部i变量,因为没有意义,反而会有多余的堆栈操作

4.参数和局部变量没有本质的区别,都在栈中

5.完全可以把参数当作局部变量使用

参数和局部变量的区别就是:参数是函数调用的时候分配在寄存器或者堆栈中,局部变量在函数执行的时候分配

注意:经过学习其实可以知道数组越界的代码是可以执行的,例如int arr[5] = {1,2,3,4,5}

此时代码在有一个arr[6] = (int) Helloworld;此时会执行helloworld这个函数,这是因为汇编中此时helloworld函数的地址为[ebp+4],而[ebp+4]得值会到EIP中,EIP存放下一步要执行得指令地址。so~~~~

对齐原则:

原则1:数据成员对齐规则,结构的数据成员,第一个数据成员放在offset为0的地方,之后每个成员存储的起始位置要从该成员的整数倍开始(比如32位机位4字节,则从4的整数倍地址开始存储)

原则2:结构体的总大小,也就是sizeof的结果必须是内部最大成员的整数倍,不足的对齐

原则3:如果一个结构里面由某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里存有struct b,b中由char,int,double等元素,那b应该从8的整数倍开始)

原则4:对齐参数如果比结构体成员的sizeof值小,改成员的偏移量应该以此值为标准。即结构体成员的偏移量应该取二者最小值

tips:由上可知,在代码编写的过程中根据数据类型由小到大的顺序进行书写可以节省空间。

为现有的类型定义一个新的名字:例如在定义typedef unsigned char BYTE;之后

再定义unsigned char i = 1;的时候可以直接写成BYTE i;

或者typedef int vector[10]; 之后定义的时候直接可以:vector v; v[0]=1;v[1]=2;.......

或者直接定义一个结构体:

typedef struct student{
int x;
int y;
}stu;
//之后可以直接使用stu代替student
switch(x){
case 1:
printf("1");
break;
case 2;
printf("2");
break;
case 3:
printf("3");
break;
}

tips:分支少于4的时候,用switch没有意义,编译器会生成类似if-else之类的反汇编

case后面的常量可以是无序的,并不影响大表的生成

switch的本质是生成一个大表,当参数传递进来的时候通过比对寻找到指定分支的地址

https://blog.csdn.net/apollon_krj/article/details/76793914

1.带*类型的变量赋值的时候只能是"完整写法" :char* i; i=(char*)8;

即例如char** a; a = (char**)100; a++; 实际上声明时*有两个,-1之后剩一个*,则该变量大小是char* a 的大小,即4个字节。而原本的char* 减去一*之后成了char类型,变量大小就是1字节

注意该类型的加法减法都可以使用该结论,但是带*类型不能乘除运算

4.两个类型相同的带*类型的变量可以进行减法操作

5.相减的结果要除以去掉一个*的数据的宽度

6.带*类型的变量可以做比较操作

重点是注意不同*的数量的变量操作之时,变量的大小情况。

1.算数移位运算:

SAL:算数左移 SAL AL,1 左移一位,最高位移到CF位,最低位补0

SAR:算数右移 SAR AL,1 右移一位,最低位移到CF位,最高位补符号位

2.逻辑移位指令:

SHL:逻辑右移 最低位到CF,最低位补0

SHR:逻辑左移

3.循环移位指令:

ROL:循环左移 将最高位的数补到最低位 ,CF中是最高位拿出来的数

ROR:循环右移 将最低位的数补到最高位,CF中是最低位拿出来的数

4.带进位的循环移位指令:

RCL:带进位循环左移,将最高位的值放在CF,原来CF的值放到最低位

RCR:带进位循环右移,将最低位的值放在CF,原来CF的值放到最高位

上一篇:windows-进程相关操作


下一篇:汇编层探索与讨论c++引用