HOME

从逆向角度学Intel_x86_汇编

x32dbg使用方法

  • 打开一个exe可执行程序后断点在系统函数。通过点击run命令自动执行到用户程序的入口函数。

  • x32dbg的栈窗口会跟随栈顶值变化

    数字电路基础知识

    理解补码,反码,原码关键在于这是三种编码方式,而非数值。原码只是恰好和二进制对应的十进制数的规则相同。
    计算电路就是按照原码来设计的,在需要计算负数时,通过改变编码方式实现,而不是通过改变电路。
    已知有符号数的补码求对应的十进制数:
    某种意义上将寄存器名称和内存地址对汇编程序员来说是等价的,(从键值对角度理解,内存地址与值,寄存器名称与值)

  1. 先将所有位取反

  2. 取反后的二进制数加1

  3. 转换成对应的十进制,加上负号。 已知一个负十进制数,求对应的有符号数的表示(即补码)

  4. 将绝对值转成二进制

  5. 将所有位取反包括最高位

  6. 将取反后的二进制加一

    MOV指令

    指令格式:操作码 (目标)操作数,(源)操作数
    两个操作数的数据宽度必须一致。

    寄存器

    一共8个32位通用寄存器。EAX(accumulator),ECX(counter),EDX(data),EBX(base address),EBP(base address pointer),ESP(stack top pointer),ESI(source index),EDI(destination index)。
    8个16位寄存器。
    8个8位寄存器。
    需要注意8位寄存器并不是直接截断全部8个的32位寄存器,而是拆分的前4个16位寄存器。

    指令语法

    • mov r/m8, r8

    • mov r/m16, r16

    • mov r/m32, r32

    • mov r8, r/m8

    • mov r16, r/m16

    • mov r32, r/m32

    • mov r8, imm8

    • mov r16, imm16

    • mov r32, imm32

      ADD指令

      见intel编程手册第二卷第三章
      可以操作的对象只可能是寄存器,内存地址,立即数。

      SUB指令

      AND指令

      OR指令

      XOR指令

      NOT指令

      向内存中写数据

      前提条件:认识运行中的内存。
      每个32位x86程序运行时,操作系统会为其分配4G的虚拟空间。但是不能全部使用,其中有正在运行的系统库指令,程序的数据区,程序的指令区。因此不是所有的地址都具有读写权限。

      内存寻址的5种方式,配合mov和lea指令使用

      立即数直接寻址

      值的宽度 ptr ds:[立即数]

      LEA 指令

      LEA R16/32,M ;将某个内存的地址赋予一个寄存器。
      lea esi, dword ptr ss:[esp+4] ;含义将ESP中值所表示的内存的地址加4,然后将此地址赋值给esi.
      将指定的内存地址写到某个寄存器(目标操作数只能是寄存器),用法:
      lea eax, DWORD PTR DS:[12FFC4]
      结果:12FFC4
      lea eax, DWORD ptr ds:[ecx]
      结果:eax的值会等于ecx的值。

      寄存器直接寻址[reg]

      值的宽度 ptr ds:[reg8/16/32]

      寄存器表达式寻址[reg+立即数], [reg + reg{1,2,4,8}]只能是{1,2,4,8},[reg+reg{1,2,4,8}+立即数]

      引入栈结构

      【栈结构更像气球,而不是竹筒】
      需求:

  • 需要临时存储一些数据,但是数量比较多,不能全放在寄存器中。(用内存)
  • 能够记录存了多少数据。(使用两个寄存器保存一段连续的内存地址两端,简单计算即可得到一共记录的数据个数)
  • 能够快速找到某个数据(大的内存地址减去偏移值即可定位到目标值或者小的地址加上偏移值) 这时需要利用内存设计某种机制,来满足2,3需求。
    windows分配栈的时候从高地址向低地址分配。
    模拟栈结构的操作:
mov ebx,EFFC68  ;初始化栈结构,即给栈顶和栈底赋值。
  mov edx,EFFC68

  mov dword ptr ds:[edx-4],AAAAAAAA  ;使用栈存值,分两步,先写值再更改栈顶指针
  sub edx,4

  nop
  lea edx,dword ptr ds:[edx-4]       ;存值,先修改栈顶指针,再存值
  mov dword ptr ds:[edx],BBBBBBBB

  mov esi,dword ptr ds:[ebx-8]    ;取出想要保存的值,可以从栈底开始也可以从栈顶开始加偏移值。

栈使用的一般流程:

  • 使用两个寄存器分别存栈底和栈顶地址(初始状态栈顶和栈底地址相同)

  • 连续存储若干个数

  • 使用栈顶或栈底偏移的方式读取这些数,读取到寄存器中

  • 移动(表示一步一步的过程,而非直接设置)栈顶指针到栈底,

    ESP,EBP

    在需要使用栈结构时ESP(stack top pointer)存储栈顶值,EBP(base address pointer)。

    push指令

    详见intel编程手册卷二第三章。
    push可以看成做两个动作:向上移动(减法)栈顶指针,将数据写到原来的栈顶处。二者没有先后关系。
    push imm8/16/32,ESP总是减少4个字节的地址长度。
    push r16/32,push 不能操作八位寄存器。
    push m16/32, m16时ESP减少2个字节的地址长度。

    pop指令

    pop可以看成做了两个动作:向下移动(加法)栈顶指针,将原来栈顶位置的数,读取并写到指令的寄存器。

    pushad和popad指令

    对八个通用寄存器的整体操作,但是不影响原来存在栈中的数据。

    标志寄存器

    详见intel编程手册第一卷第三章。

    CF(carry flag)

    如果(算术和逻辑运算)运算结果最高位产生了一个进位或借位,其值为1,否则其值为0。

    PF(parity flag)

    (算术和逻辑运算)结果里面1的个数,偶数为1,奇数为0

    AF(Auxiliary Carry flag)

    (算术和逻辑运算)辅助进位标志,在下列情况被置为1,其余为0:

  • 在字操作时,发生低字节向高字节进位或借位。

  • 在字节操作时,发生低4位向高4位进位或借位。 ` mov eax, 0x55eeffff add eax,2

mov ax, 5efe add ax,2

mov al,4e add al,2 `

ZF(zero flag)

(算术和逻辑)运算结果是否为0,无论数据宽度。

SF (sign flag)

用来表示运算结果的符号位(和运算结果的符号位相同,即与运算结果的最高位相同),
mov al,7f add al,2 溢出标志位OF和SF与CF区别:
正数与负数相加永远不会溢出,7f(最大正数)+ff(最大负数)= 17e

Of(overflow flag)

溢出标志位。
什么是溢出?溢出标志位和CF位很像,CF是做无符号运算时应该关注的寄存器。OF是做有符号运算关注的,只要涉及有符号运算只需要关注OF位是否溢出即可。

溢出的概念只存在于有符号运算中(即当你认为两个操作数按照有符号数编码时)

  • 正数+ 正数 = 正(数学上的规律)(0到7f)不溢出,(7f到ff溢出)如果结果大于7f(为负数),(最大为ff),溢出。

  • 负数+负数 = 负数(数学上的规律)(在7f到ff之间)不溢出,(0到7f溢出)如果结果小于7f(最小为0),溢出。

  • 正 + 负数(数学上结果可能为正也可能为负) 永远不会溢出。 以上通过16进制数圆盘理解是一种体会。也可以从十进制角度理解,会有另一种体会。

    例子

  • 无符号,有符号都不溢出 mov al,8 add al,8

  • 无符号溢出,有符号不溢出 mov al, 0ff mov al, 2

  • 无符号不溢出,有符号溢出 mov al,7f add al,2

  • 无符号,有符号都溢出 mov al, 0fe add al,80

    ADC指令

    带进位的加法

;CF位:为1
mov al,0x1
mov cl,0x2
adc al,cl ;结果al为0x4

SBB指令

带借位的减法

;CF位为1
mov al,0x4
mov cl,0x2
sbb al, cl ;结果al为0x1

XCHG指令

交换源操作数和目标操作数
xchg r/m8/16/32,r/m8/16/32 ;但是不能直接交换两个内存的值

MOVS指令

movs/movsb/w/d m8/16/32,m8/16/32 ;移动数据内存间移动数据。
MOV BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] ;从DS:[ESI]中取出一个字节,放入DS[EDI]中。

标志寄存器方向标志位DF

当DF值为1时,movs 系列指令在在移动完数据后,ESI,EDI的值减去移动距离。当为0时加上移动距离(向下复制)。
movs系列指令一般用于字符串复制。

STOS指令

将AL/AX/EAX的值存储到[EDI]指定的内存单元。受方向标志位DF影响。

STOS BYTE PTR ES:[EDI] ;简写为STOSB,将AL值放入内存
STOS WORD PTR ES:[EDI] ;简写STOSW,将AX值放入内存
STOS DWORD PTR ES:[EDI] ;简写STOSD,将EAX值放入内存

REP指令

根据计数寄存器(ECX)中的值,重复执行字符串命令( MOVS系列指令,STOS系列指令 )

MOV ECX,10
REP MOVSD
REP STOSD

JUMP指令

JUMP 跳转 128字节内会自动加short jump指令执行前和执行后,cpu内寄存器状态只有EIP变化。内存种数据状态不变。

可以修改eip的指令

call

CALL指令

执行前后CPU寄存器和内存中数据的变化。ESP和EIP变了,内存值发生变化了,栈内存的值改了4个字节。、

函数调用

  1. 在调用处,通过push,call对栈内存进行入栈。

  2. 通过push,mov 指令将原来栈基地址保存,并提升栈基址。

  3. 在Debug模式下,才会在栈内存内存添加保护区域(在上一个栈帧地址和局部变量地址之前)(在给保护区域填充内容并没有使用push或pop指令esp没有变化),并填充0xcc,堆内存填充0xcd。方便程序越界(栈帧的界)造成覆盖返回地址等错误。

  4. 在调试模式下执行子程序过程,

  5. 通过pop指令,将栈基址下移。ret指令修改eip到返回地址。 在即将执行子程序第一条功能语句之前,(unoptimazie) 栈空间情况:
    上一个栈帧的基址
    调用子程序的指令的下一个指令地址(返回地址)
    子程序的参数(最上面是最左面的参数)
    一个函数的栈帧从上到下(地址从小到大): 局部变量
    上一个栈帧的基址
    子程序的返回地址
    子程序参数
    当开始执行子程序的功能代码时,栈基址和栈顶指针指向上一个栈帧的基址处。

    局部变量是栈空间的一块内存

    一般通过[EBP-0x]方式确定其位置。存放局部变量时没有使用push指令,所以不会改变栈顶指针esp的值。

Ret指令

ret numnber
执行后esp+4+number

裸函数

void __declspec(naked) Plus(){
  __asm
  {
    ret
  }
}
// c函数完整的外平栈补全指令
		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-0x40]
		rep stosd

		pop edi
		pop esi
		pop ebx
		mov esp, ebp
		pop ebp
		ret

调用约定

约定2方面内容: 1. 参数压栈顺序 2. 堆栈平衡谁来做 - __cdecl 外平栈 - __stdcall 内平栈 多用于win32api操作系统内部函数 - __fastcall

C语言的数据类型

关注数据类型的三点即可 - 存储数据的宽度 - 存储数据的格式 - 作用范围

整数类型

  • char byte 8bit 1字节
  • short word 16bit 2字节
  • int dword 32bit 4字节
  • long 注意:
char i = 0x12345678;
00D11795 C6 45 FB 78          mov         byte ptr [ebp-5],78h
  • 多余部分高位舍弃

  • 局部变量char类型是从ebp-5(vs2019)开始,shrot, int 从ebp-8开始。【可能和内存对齐,cpu寻址时间有关】

    整数类型又分为有符号和无符号数,默认是有符号编码

    unsigned {char|short|int|long},单独的unsigned表示unsigned int。

	char i = 0xff;
	unsigned char x = 0xff;
	printf("%d  %d", i, x);
  // -1  255

在内存中都是0xff,但是通过%d(以一个十进制数形式表示,至于是否有符号取决于数变量的定义,decimal integer)打印出来时,区分是否有符号。
是否有符号在类型转换,比较大小和数学运算表现不同。
【在无符号数比较时,经过msvc编译器生成的指令不同,详见201501260141】

浮点类型

int i = 1;
000C1795 C7 45 F8 01 00 00 00 mov         dword ptr [ebp-8],1

msvc 2019会把第一个局部变量存放在ebp-8的位置,vc6会存放在ebp-4。