x86_汇编(持续补充)
进制转换
二进制
八进制
十六进制
寄存器
通用寄存器
32位|16位|8位|二进制编号
eax |ax |al |000
ecx |cx |cl |001
edx |dx |dl |010
ebx |bx |bl |011
esp |sp |ah |100
ebp |bp |ch |101
esi |si |dh |110 //源寄存器
edi |di |bh |111 //目标寄存器
寄存器的结构 32位寄存器由两个16位寄存器组成,低位16位寄存器由两个8位寄存器组成。
常用的汇编指令(对不同位数寄存器的操作不同,汇编指令对应的硬编码不同 查表)
mov指令
使用mov指令 拷贝源操作数到目标操作数
1.源操作数可以是立即数,通用寄存器,段寄存器,内存地址
2.目标操作数可以是通用寄存器,段寄存器,内存地址
3.数据宽度使用时必须一致,如:mov r8/m8,r8 源操作数必须使用8位的数据宽度
4.mov指令的源操作数和目标操作数不能同时为内存地址
使用mov指令对内存地址进行操作
add指令(+)
目标操作数与源操作数进行加法运算,结果保存到目标操作数
sub指令(-)
目标操作数与源操作数进行减法运算,结果保存到目标操作数
or指令(0|1 结果为1)
目标操作数与源操作数进行或运算,结果保存到目标操作数
xor指令(0^1 结果为1)
目标操作数与源操作数进行异或运算,结果保存到目标操作数
and指令(1&0 结果为0 1&1 结果为1)
目标操作数与源操作数进行和运算,结果保存到目标操作数
not指令(二进制位反转)
目标操作数进行非运算,结果保存到目标操作数
算数移动指令
shl指令
目标操作数与源操作数进行逻辑左移运算,结果保存到目标操作数
目标操作数向左位移一位也就是一个数*2^1
shr指令
目标操作数与源操作数进行逻辑右移运算,结果保存到目标操作数
目标操作数向右位移一位也就是一个数/2^1
sal指令(原本标志位的值将被丢弃)
算数左移指令,每一次移动会将目标操作数中的每一位移动到下一位的最高位上,最低位使用0进行填充;最高位移入CF标志寄存器
sar指令(原本标志位的值将被丢弃)
算数右移指令,每一次移动运行将目标操作数中的每一位移动到下一位的最高位上,最低位使用0进行填充;最高位移入CF标志寄存器
有符号数除法使用sar指令,可以将有符号操作数/2的幂
ax符号拓展到EAX
ax为有符号数 将符号位拓展到eax,先将eax向左位移16位,后将eax向右位移16位
CMP指令
进行逻辑运算,比较目标操作数和源操作数(从目标操作数中减去源操作数,但是不修改任何操作数),对ZF和CF标志位进行修改。当进行数学运算时,目标操作数小于源操作数时,则会出现借位导致cf标志位发生改变(cf标志位此时为1)。当目标操作数等于源操作数时在,则cmp指令返回值为1,zf寄存器发生修改(zf寄存器此时为1)。cmp指令常用于if等逻辑判断时进行使用。
test指令
进行逻辑运算,与运算(与and指令的操作不同在于and会对目标操作数进行操作将运算后的值放入目标操作数或目标寄存器但是test指令不会进行值得存储,而是对标志位进行修改),test指令会对sf标志位和pf标志位产生影响。
INC指令
自增指令 INC EAX 表示EAX+1
LOOP指令
LOOP循环指令
执行会使cx(存放循环次数的寄存器)cx=cx-1
对cx中存放的值进行判断,不为0则跳转到标号位置继续执行。等于0则执行下面的指令。
使用loop指令时首先需要定义一个标号,在使用时使用loop 标号 这样就可以对标号位置的汇编代码重复进行执行。下列代码可以对s标号的代码进行重复执行。
1 | mov cx,10 |
内存操作
32位
内存格式
1.内存的宽度为8
2.[0x12345678]表示一个地址存储的值 []内部写入一个编号表示这个值为地址,可以理解为内存中存放的值
3.地址的作用作为编号可以从指定的内存中读取或者从指定内存进行写入
内存大小单位
bit 表示数据宽度 位
byte 表示数据宽度 单字节(8位)
Word 表示数据宽度 单字,两字节
Dword表示数据宽度 双字,四字节
指定内存写入/读取
写入:
1.使用mov指令进行内存 公式: mov dword ptr ds:[地址],立即数
不要将内存写入没有访问权限的位置 存入没有访问权限的位置会导致程序无法运行而且报错异常程序跳到异常部分
2.ptr 表示后面的内容为指针(表示存放的内容不是普通的数而是内存地址)
3.ds是段寄存器
读取:
使用mov指令 将指定读取的内存位置的值读取到一个寄存器(容器)中,查看寄存器的值
mov eax,dword ptr ds:[地址]
内存地址 堆栈
内存放数据的端序
内存单元中存放的数据为小端序存储方式,0x12345678 在内存中的存储方式为 78 56 34 12
大端序是生活中的正常顺序,低位数据在低位,高位数据存放在高位。小端序,存放数据顺序与大端序相反,高位数据在低位,低位数据存放在高位。
寻址方式
寻址公式1使用立即数对内存进行读取/写入
mov eax,dword ptr ds:[地址]
lea eax,dword ptr ds:[寄存器]//一般用于间接寻址 如果使用lea对地址直接寻址 就会输出寻址的地址
lea指令
获得内存地址
lea eax,dword ptr ds:[地址]
lea eax, dword ptr ds:[寄存器]
第二种寻址方式
lea eax, dword ptr ds:[寄存器] lea指令会将寄存器的的值作为地址进行读取,也就是说寄存器的值会作为地址被存储到eax。
mov eax, dword ptr ds:[寄存器] mov指令会将寄存器的值作为地址进行读取,也就是说这条指令读取了地址内存储的值
第三种寻址方式
mov eax,dword ptr ds:[寄存器+4]
第四种寻址方式
mov eax,dword ptr ds:[寄存器1+寄存器2+4]
堆栈
栈结构
堆栈读取和存储数据时,每次对四个字节进行操作。读取某个数据时使用栈底的的地址作为基础地址,添加数据时,栈底指针不懂,栈顶进行移动,每一次移动4个字节(栈顶指针的地址-4,栈顶指针的地址跟随存入的数据进行改变)
esp 作为寄存器储存了栈顶的值
ebp 作为寄存器存储可栈底的值
缓冲区
程序中汇编代码中会存在
1 | push ebp |
再现有得堆栈中开一个新栈,这个部分被称为缓冲区,并且在缓冲区之后一般会为了存储当前得堆栈得寄存器中得数据所以会push一些寄存器
下列代码会将cccccccc存入10个内存单元(),代码常用于向缓冲区中存入大量cccccccc(int 3的作用为了防止缓冲区溢出)
1 | mov ecx,10 |
观察当前内存的存储的数据,发现充斥着无用的垃圾数据(可能为上一个函数的使用的内存),为了初始化这段内存将,cccccccc存入这段内存。
mov eax, dword ptr ss:[ebp+8]
add eax, dowrd ptr ss: [ebp+c]
函数调用后,使用了push存入的参数
堆栈平衡
在函数执行完毕后,堆栈应该保持原有的状态,也就是说esp,ebp应该与原来(未执行时)保持一直。如果函数内部,执行后做到了堆栈平衡叫做内栈平衡,在函数外部执行一些语句叫做外栈平衡。在一些debug版本的程序会在函数调用结束的时候进行堆栈平衡的判断,如果堆栈在此时平衡则不会继续执行堆栈平衡代码,代码会正常返回。
函数
函数是一个程序段(子程序),有入口(允许传入参数的位置)和出口(执行程序后返回值)。传入参数后可以执行,执行后返回值(不断进行栈的提升在执行函数功能之后,将栈恢复,重复进行这个过程不断的执行函数功能).
汇编中函数结构:
提升堆栈,为函数执行分配空间
1 | push ebp |
保存寄存器,程序在执行时候会使用一些寄存器,所以需要将一些寄存器保存到堆栈
1 | push ebx |
为分配的空间填充数据
1 | lea edi,dword ptr ss:[ebp-40] |
函数功能实现部分
1 | mov eax,dowrd ptr ss:[ebp+8] |
回复寄存器的初始状态
1 | pop edi |
降低堆栈
1 | mov esp,ebp |
回复栈底
1 | pop ebp |
返回入口的下一条指令
1 | retn |
入/出栈的常用汇编指令
pop指令
pop reg16
pop reg32
pop m16
pop m32
push指令
push reg16
push reg32
push m16
push m32
push imm8/imm16/imm32
栈内压入数据的方式 push(目的传入参数并且对栈的指针进行修改)
方式1和2,3和4的区别的时传入数据的对指针的操作方式不同,先进行数据传入和先进行指针移动。方式1和3,方式2和4区别使用的指令不同,lea指令使用相对地址进行赋值,sub直接对寄存器的值进行修改。
1 | 1. |
读入数据使用push reg/data push指令实际上是两条指令的结合
栈内数据弹出 pop(栈内压入数据的逆过程,弹出数据不是将内存地址的值)
1 | 1. |
弹出数据使用pop reg pop指令实际上是两条指令的结合
栈内数据读取
1.base加偏移值
读取第一个压入的数据
mov esi,dword ptr ds:[ebp-4]
读取第四个压入的数据
mov esi,dword ptr ds:[ebp-0x10]
2.top加偏移值
mov esi,dword ptr ds:[esp+4]
读取第四个压入的数据
mov esi,dword ptr ds:[esp+0x10]
弹出数据和读出输入是不同的,弹出数据虽然不会销毁数据,但是也就以为这个数据被放弃了。读出数据不会销毁数据,也不会放弃数据。
标志寄存器
popad和pushad的作用主要是用于堆栈平衡,作用是为保存当前代码的寄存器和恢复当前代码的寄存器。
efl 寄存器存放了标志寄存器的状态,通过查看数据位就可以查看标志寄存器的状态。
pushad指令
PUSHAD
将eax ecx ebx 等8个32位通用寄存器依次存入堆栈,压入的值为当前的值。
popad指令
POPAD
将eax ecx ebx 等8个32位寄存器依次弹出存入堆栈,恢复原来寄存器的值。
Carry flag
进位寄存器(0位),运算结果的最高位产生进位或者借位(确定数据宽度是必要的)
parity flag
奇偶标志寄存器(2位),运算结果的二进制形式有奇数个1,寄存器为0。运算结果的二进制表示形式有偶数个1,寄存器为1
PF的值只于最低有效字节最后一个字节有关 804 二进制 1000 0000 0100 只跟最后一个字节0000 0100有关 PF位为0
Auxiliary Carry flag
辅助进位标志寄存器(4位),32位数据宽度时,低四位(低16位)的最高位发生进位或者借位时,寄存器为1。
Zero flag
零标志,运算结果位0时,寄存器为1.运算结果为1,则为0.(mov的赋值过程不是运算,不对寄存器进行影响,类似于xor,cmp结果才时运算)
Sign flag
符号标志,存放运算结果的符号位,和运算结果的最高位相同(二进制位的最高位根据寄存器的数据宽度确定)。
Overflow Flag
溢出标志位,使用于有符号的运算结果。无符号使用cf寄存器。正数+正数=正数 结果如果是负数那么就是溢出。负数+负数=整数,那么就是溢出。正数+负数 不会溢出
1100 0000
1100 0000
11000 0000
最高位进位和最高有效位异或
Direction Flag
方向标志位 为1时,移动向小地址的方向进行移动为0时移动向大地址移动
ADC指令
带进位加法(进位寄存器cf存放的值也会被加入计算) 格式 ADC r/m,r/m/imm 两边不能同时为内存 宽度相同
ADC al,cl
SBB指令
带借位减法 SBB r/m,r/m/imm
XCHG指令
交换指令 交换两个寄存器的值或者交换内存和寄存器的值 XCHG r/m,r/m 两把不能同时为内存
MOVS指令
移动数据 内存-内存 字符串赋值常用
将第二个内存存储的值复制到第一个内存的位置
movsb 移动一个字节//movs byte ptr es:[edi],byte ptr ds:[esi]
movsw 移动一个字//movs word ptr es:[edi],word ptr ds:[esi]
movsd 移动一个双字//movs dword ptr es:[edi],dword ptr ds:[esi]
存储内存的寄存器(edi)的值 地址会根据移动的数据宽度和DF标志寄存器的值进行增加减少(增加减少的大小根据数据宽度 增加还是减少根据方向标志位)
对有符号位数据进行拓展的指令
MOVZ指令
移动数据 内存-内存
对无符号位数据进行拓展的指令
stos指令
将值al/ax/eax的值存储到edi寄存器指定的内存地址
mov eax,12345678
mov edi,12ffc4
stosb//stos byte ptr es:[edi]
stosw//stos word ptr es:[edi]
stosd//stos dword ptr es:[edi]
edi增加的值为数据宽度 方向由df决定
rep指令
按计数器ecx中指定的次数循环执行代码
mov ecx,10
rep movsd
rep stosd
JCC(修改eip)
jmp指令(无条件修改eip)
jmp addr(reg/imm) 跳转 (修改eip进行跳转)//mov eip ,reg/imm
short 和 long区别在于是否跳转超过128字节(32个内存单元)
call指令(跳转后返回到下一条地址)
push addr//把返回地址压入栈中(修改esp 压入的地址为call+指令的长度),把下一条跳转的地址压入栈中
mov eip,addr/reg
将下一条指令得地址(返回地址)压入堆栈,并且修改eip为下一条指令。
ret指令
ret//功能是pop eip
lea esp,[esp+4]//将栈顶地址esp向下移动一个内存单元
mov eip,[esp-4]//将栈外的返回地址存入eip
call指令会在最后调用ret指令, 将压入的返回地址弹出到eip,返回到执行的位置。
变址寄存器和堆栈寄存器
变址寄存器
变址寄存器常用于存取储存器寻址时提供地址
SI是源变址寄存器 DI是目标变址寄存器(通常在大幅修改或者是赋值数据时提到)
堆栈相关寄存器
SS堆栈段寄存器
SP堆栈指针寄存器
BP堆栈基地址寄存器
堆栈中数据表示方式
SS:SP 表示栈顶的地址
SS:BP 表示栈底的地址
win32汇编
win32文件头
.386 表示i386架构
.model 指定了语言模式,子程序的调用方式。win32 API调用约定固定使用stdcall调用约定。
option casemap:none 定义了程序中变量名和子程序名是否区分大小写,winAPI的API名称区分大小写。
include 文件头 .inc 文件中存在使用的方法的声明。
includelib 文件头lib lib表示library .lib是实现方法的导入库,存放了实现方法的dll的具体位置。
win32段
win32汇编中弱化了段的概念,使用内存属性进行分节(section)。win32汇编程序将不同类型数据或代码进行分类后,放入不同文件属性的内存页中(内存进行分页)。
x32dbg/x64dbg 查看内存布局可以直接看内存页的属性。
数据段
数据段使用pE解析器可以直接查看(未加密时)
.data 已经定义变量,数据可读可写
.data? 未定义变量,数据可读可写
.const 常量,只读(一般存放有实际意义的字符串)
数据变量定义
对应段内 dd db对数据进行定义
代码段
.code 代码段可执行 对PE文件头进行修改可以对文件的段文件属性进行修改。UPX壳等需要操作代码段存储内容进行加解密或压缩操作,需要将代码段属性修改为可写。
堆栈段
程序中不需要定义,由系统进行分配。内存属性为可读可写可执行。
程序入口
end 段名 定义程序开始的地方
注释换行
\表示换行 ;表示注释
函数调用(代码段内编写)
函数编写
格式为函数名开始后proc作为起始,endp表示函数结束
函数名 proc
函数名 endp
调用API/方法
invoke 调用函数名,\ ;表示调用这个函数
offest szText,
offset szCaption ;表示数据
call 方法名
获取方法的返回值
mov 变量/寄存器,eax
设置局部变量和结构体
local @自设置的变量名:对应的系统设置的变量或者是结构体
结构体内变量可以使用 .结构体成员 进行引用
winAPI
调用win32API时,会自动导入winapi的dll
KERNEL32.dll 系统服务功能 内存管理 任务管理 动态链接
GDI32.dll 图形设备结构
USER32.dll 用户服务接口
Wsock32.dll 网络通信
Tapi32.dll 电话服务
编码方式
winAPI 中使用的string类型数据有ascii和unicode编码,因此需要string类型数据的函数存在A和W版本区分的winAPI函数,对不同的编码的字符串进行使用。
下断点方法(纯经验)
1.不明确功能,直接APIbreakpointer打想要找的功能的断点,或者摸奖直接打断点慢慢磨
2.明确功能后直接打相关功能的API断点(APIBreakPointer)
3.明确功能后,找到关键代码部分,下相关功能API的断点
窗口程序运行过程
1.获得程序的句柄(GetModuleHandle),获取实例句柄。模块句柄是程序初始化的基地址。
2.注册窗口类(RegisterClassEx)
3.建立窗口(CreateWindow)
4.显示窗口(ShowWindow)
5.刷新窗口客户区(UpdateWindow)
6.进入无限的消息获取和处理(GetMessage DispatchMessage)