逆向——寄存器和常用汇编指令学习
更详细的见:https://www.showdoc.com.cn/fengxin1225/7019061226778425
9.汇编学习环境塔建
这里直接是用OD就好,因为习惯用了。
首先这里用一张截图概括吧!
10.通用寄存器
寄存器
存储数据:
1.速度方面 cpu>内存>硬盘
2.存储格式
32位cpu: 8 16 32
64位cpu: 8 16 32 64
通用寄存器
EAX WCX WDX EBX
ESP WBP WSI EDI
==//不能多于32位 不然多来的会被丢弃==
11.通用寄存器
12.内存
每个进程都有其4G的内存
==进程 即运行的程序
4G是虚拟的,需要多少映射到物理==
这里我觉得需要写出来
1Byte=8Bit
1KB=1024Byte
1MB=1024KB
1GB=1024MB
内存地址
1.内存是一个线性地址,每个数据都占有一定的内存,而他们存的位置就是他们的地址.成为内存地址,我们可以往内里存储数据或读取数据都会用到它
2.32位的话可 存房00000000到 FFFFFFFF(16进制)
也是上面每个进程都是4G的虚拟空间
mov ax,1 //放入寄存器 ax即说明了放入ax中,16位的
mov eax,1 //eax 即说明放入到了 32位的
而放入内存里,就得先说明放入多大//宽度的内存里
==mov byte ptr ds:[0018FFF0],1== 这个是固定语法 ptr表示pointer,byte表示位宽,[]表示内存地址
//不是每个内存地址都能使用的,使用前得提前申请
但可以测试时使用堆栈区的地址,那的都可以使用
立即数 写入到寄存器
寄存器的值 写入到内存
内存的值 写到 寄存器
另外 寄存器可到寄存器
内存目前的指令不可以到内存(有指令是能实现的)
1
|
mov dword PTR ds:[18fffc],12345678
|
注意宽度:
Byte:8字节
word:16字节
dword:32字节
13.内存地址的五种形式
1.形式一:[立即数]
略过
2.形式二:[reg], reg(寄存器)可以是8个通用寄存器中的任一个
读取内存的值
1
|
mov ecx,0x13FFD0
|
向内存中写入数据
1
|
mov EDx,0x13FFD8
|
3.形式3:[reg+立即数]
读取内存的值
1
|
mov ecx,0x13FFD0
|
向内存中写入数据
1
|
mov EDx,0x13FFD8
|
4.形式4:[reg+reg*(1,2,4,8)]
读取内存的值
1
|
mov eax,0x13FFC4
|
向内存中写入数据
1
|
mov Eax,0x13FFC4
|
5.形式5:[reg+reg*(1,2,4,8)+立即数]
读取内存的值
1
|
mov eax,0x13FFC4
|
向内存中写入数据
1
|
mov Eax,0x13FFC4
|
//数据是如何存放的:小端存储(大小端百度)
存储模式(大、小端)
要在计算机中存储数据,需要遵循其的存储模式,存储模式分为两种:大、小端。
大端模式:数据高位在低位地址中,数据低位在高位地址中;
小端模式:数据低位在低位地址中,数据高位在高位地址中。
如下图中所示,内存地址从小到大;我们知道每一个内存地址可以存储8位,也就是一个字节,当我们使用MOV指令写入数据到内存中时指定宽度为BYTE存储数据会存储在一个内存地址中,而当我们指定数据宽度为WORD、DWORD呢?
汇编指令:
mov word ptr ds:[
0x00000000
],
0x1A2B
这里存储的是2个字节的数据,那么按照大端模式去存储,0x1A2B数据高位就是1A,低位就是2B,内存地址中从小到大,小的则是低位,高的则是高位,所以内存地址0x00000000存储1A,0x00000001存储2B;小端模式则内存地址0x00000000存储2B,0x00000001存储1A。
我们可以尝试使用汇编指令写入数据到内存中:
mov word ptr ds:[
0x0012ffc4
],
0x1A2B
在DTDebug中我们可以在内存窗口使用db/dw/dd 内存地址的格式(byte、word、dword)来查看内存,在这使用db 0012ffc4才可以清晰的看出数据存储位置:
在这里我们可以看见2B存储在了内存地址0012ffc4,1A存储在了内存地址0012ffc5。(在这里内存地址是依次是从左到右的)
也就表示我们的数据存储模式是小端模式,而一般来说在80x86处理器上,绝大部分的应用数据存储都是采用小端模式,在ARM上则是大端模式。
15.汇编常用指令//参考资料Intel白皮书
mov指令
add指令
sub指令
and指令
做个实验:
1
|
and dword PTR ds:[18FFA0],EAX
|
or指令
xor指令
not指令
1
|
not Dword PTR ds:[18FFA0] //本来里面全为0
|
16.汇编常用指令(二)
前面提到:
寄存器可到寄存器,
内存当时说不可以直接到内存,而接下来这个指令可以实现
==movs==
小提示:
程序会在两个地方停下来:
1.刚加载时
2.程序入口点
1.movs指令:移动数据 内存–》内存
1
|
byte/word/dword
|
了解下 标志寄存器 elf
elf :00000246
DF:方向标志位:从右向左 坐标为10 //注意 有0坐标哦
if DF为0
di si 自动+1 4 8
if DF为1
di si 自动-1 4 8
试验下:
开始 换成dword
ESi从 0018FF8C 到0018FF88 减了16
Edi从 0018FFA0 到0018FF9C 减了16 //原因是 elf中的D方向标志为1;
STOS指令:讲AL/AX/EAX的值存储到[EDI]指定的内存单元
1
|
STOS
|
REP指令:按计数寄存器(ECX)中的指定的次数重复执行字符串指令
1
|
mov eax,10;//这里的值是 16进制 10即代表 会执行16次
|
堆栈相关指令
在学习堆栈相关指令之前,我们需要知道什么是堆栈,实际上我们之前就提到过这个东西,堆栈就是前面我们提到的程序的独立4GB内存空间,与数据结构的堆栈无关。
有心人会在「汇编学习环境搭建」章节看见DTDebug的图,堆栈的位置在软件的右下方。
要想知道堆栈可以使用的地址范围,我们可以查看DTDebug的寄存器窗口,找到如下图红框部分:
然后再到DTDebug的内存窗口,输入dd 7FFDF000,回车即可看到堆栈的地址范围了:
栈顶:0x00130000,栈底:0x0012D000,也就是说我们可以从0x0012D000用到0x00130000。
聊到堆栈,我们又不得不提一下之前所学到的通用寄存器:ESP、EBP,其分别表示栈顶指针(表示当前正在使用的堆栈地址)、栈底指针(表示当前使用的堆栈的第一个地址),当前使用的堆栈地址范围就可以是ESP - EBP。
堆栈的地址使用是从大用到小的(高位地址到低位地址),这个我们也可以从DTDebug的堆栈窗口中得出:
堆栈讲究先入后出的概念,也就是当我们先放A数据到堆栈中,再放B数据到堆栈中,当我们不使用这些数据时候就要“清理”,那么数据B就是先处理的,数据A就是后处理的。
我们想要往堆栈中压入数据就需要使用一个当前没有使用的堆栈地址避免覆盖了其他数据,也就是我们先存入数据,然后告诉堆栈当前用到了哪(提升栈顶指针ESP),在之前的章节中我们学习了使用MOV指令向内存中存储数据,在这里就可以借助MOV指令去压入数据:
mov dword ptr ds:[esp-
4
],
0x11223344
sub esp,
0x4
// 由于堆栈地址使用是从大到小的,所以使用sub指令提升esp栈顶指针(4表示我们之前存储的数据宽度为4字节,所以栈顶指针ESP需要提升4)
而当我们不需要使用这个数据的时候,就要释放,同样我们使用汇编指令就可以完成:
add esp,
0x4
// 直接下降栈顶指针ESP
但是有一个问题,当我们把EAX的值压入堆栈中,然后再想把EAX的值放回去使用这种方法完全不行,我们还需要一个MOV指令将值放回去,这样很繁琐,所以我们可以使用堆栈指令:P USH(压入)和POP(释放)。
PUSH指令
表示压入数据,其格式为:
// PUSH 通用寄存器/内存地址/立即数
// 含义:向堆栈中压入数据,压入数据后会提升(sub)栈顶指针(ESP),提升多少取决于压入数据的数据宽度
PUSH r16/r32
PUSH m16/m32
PUSH imm8/imm16/imm32
POP指令
表示释放数据,其格式为:
// POP 通用寄存器/内存地址
// 含义:释放压入堆栈中的数据,释放数据后会下降(add)栈顶指针(ESP),下降多少取决于释放数据的数据宽度
POP r16/r32
POP m16/m32
修改EIP的指令
EIP也是寄存器,但它不叫通用寄存器,它里面存放的值是CPU下次要执行的指令地址;当我们想去修改它的值就不能使用修改通用寄存器那些指令了,修改EIP有其特有的指令,接下来让我们来了解一下吧。
JMP指令
表示跳转,其格式为:
// JMP 寄存器/内存/立即数
// 含义:JMP指令会修改EIP的值为指定的指令地址,也就修改了程序下一次执行的指令地址,我们也可以称之为跳转到某条指令地址。
CALL指令
也可以修改EIP,跟JMP指令的功能是一样的,其格式为:
// CALL 寄存器/内存/立即数
// 含义:跟JMP指令的功能是一样的,同样也可以修改EIP的值,不同的点是,CALL指令执行后会将其下一条指令地址压入堆栈,ESP栈顶指针的值减4
// 注意:在我们使用DTDebug调试的时候,要跟进CALL指令不能使用F8要是用F7,这里涉及调试器相关的知识,暂时不深入了解
RET指令
表示返回,其格式为:
RET
// 含义:将当前栈顶指针的值赋给EIP,然后让栈顶指针加4
汇编眼中的函数
什么是函数?函数就是一系列指令的集合,为了完成某个会重复使用的特定功能。
那么在汇编中如何定义、使用函数呢?既然我们知道函数就是一系列指令的集合,那么只要我们随便写一段汇编代码即可:
mov eax,
0x1
我们将其写在了执行地址0x00460A32中,想要调用这个函数,需要使用JMP、CALL指令来调用。
但是我们一般不会使用JMP来调用函数,因为它执行完后没办法返回到原来要执行的地址,所以我们选择CALL指令,CALL指令需要搭配RET指令一起使用。
一般我们在函数指令集合的最后写入RET指令,以此来实现函数执行完后返回原来要执行的地址继续执行。
那假设我们需要做一个任意两个数的加法函数该怎么办?这时候就需要想办法将我们的任意两个数传入函数中,这也就是参数;加法函数计算结果就称之为返回值。
返回值在汇编中一般使用EAX存储,我们可以使用ECX、EDX作为传递参数,接下来我们编写加法函数:
add eax,ecx
add eax,edx
ret
实际场景:
在这里我们使用的是寄存器传递参数,但实际上还可以使用堆栈传参数,下一章节我们会介绍。
ESP寻址
之前我们了解了函数,以及堆栈传参,那其实我们获取参数就是借助的ESP去获取对应参数的地址,这种行为我们称之为ESP寻址。
需要注意的是我们获取参数的值,指令应为:
mov eax, dword ptr ss:[esp+
4
]
你会发现原来指令中的ds变为了ss,这是因为当你的内存地址是esp或ebp组成的需要使用ss,暂时先不用管原因。
ESP寻址弊端:当函数比较复杂时,使用的时候要使用很多寄存器,需要把寄存器的值保存在堆栈中备份,寻址计算会复杂一些。
EBP寻址
之前都是借用ESP去寻址确定一些参数 ,但如果存到堆栈里面的值过多,那么就得不断地调整ESP的指向,这是ESP寻址的缺点。
那么EBP寻址的思路是什么呢?先把EBP的值保存起来,然后将EBP指向ESP的位置,接着在原来的堆栈基础上将ESP上移,重新变成一块新的堆栈;之后新的程序再使用堆栈的时候,只影响ESP但不会影响EBP,那我们寻址的时候使用EBP去寻址,EBP的位置相对固定,程序不管如何操作ESP都会不停浮动,但是EBP相对稳定。
标志寄存器
在DTDebug中标志寄存器就是这一部分:
16位标志寄存器是:FLAGS
-
条件标志位:SF、ZF、OF、CF、AF、PF
CPU在执行完一条指令之后进行自动设置,反映了算数、逻辑运算等指令执行完毕之后,运算结果的特征。
-
控制标志位:DF、IF、TF
控制CPU的运行方式和工作状态。
条件标志位
-
进位标志:【CF】—运算结果的最高有效位有进位(加法)或者借位(减法)。用于表示两个无符号数高低。
-
零标志:【ZF】—如果运算结果位0,则ZF=1,否则ZF=0
-
溢出标志位:【OF】—当将操作数作为有符号数的时候,使用该标志位判断运算结果是否溢出。加法:若相同符号数相加,结果的符号与之相反则OF=1,否则OF=0。
减法:若被减数与减数符号不相同,而结果的符号与减数相同则OF=1,否则OF=0发生溢出,说明运算的结果已经不可信
-
标志符号:【SF】—运算结果最高位为1,SF=1,否则SF=0。有符号数用最高有效位表示数据的符号,最高有效位是标志符号的状态。
-
奇偶标志位:【PF】—当运算结果(指的是低8位)中1的个数为偶数时,PF=1,否则PF=0。该标志位主要用于检测数据在传输过程中是否出错
-
辅助进位标志位:【AF】—一个字节运算的时候低4位向高4位的进位和错位。
注意
1. 进位针对的是无符号数运算,溢出针对的是有符号数运算
2. 进位了,可以根据CF标志位得到正确的结果,溢出了,结果已经不正确了。
3. 汇编中的数据类型由程序员决定,也就是没有类型,程序员说是什么类型就是什么类型。所以当看到无符号数,则关注CF标志,看成有符号数,关注OF标志。
如下汇编代码:
我们往下走两步完成赋值
现在去跑ADD指令:
因为此处ADD指令是进行了运算结果不是0,固ZF位=0,在汇编中没有像C语言这样高级语言中有if语句、while语句,那它是怎么实现这些功能呢?
-
标志寄存器
-
JCC指令
如何操作标志寄存器
LAHF(Load AH with flags)指令:用于将标志寄存器的低八位送入AH,即将标志寄存器FLAGS中的SF、ZF、AF、PF、CF五个标志位分别传送到AH的对应位(八位中有三位是无效的)
SAHF(store AH into flags)指令:用于将AH寄存器送入标志寄存器
PUSHF(push the flags)指令:用于将标志寄存器送入栈 → push eflags
POPF(pop the flags)指令:用于将标志寄存器送出栈 → pop eflags
我们来做一个实验,首先将EAX置为0,然后CF、PF位为1,这时候我们调用LAHF将标志寄存器送入AH会得到怎样的数据呢?
这时候我们F8运行这一条指令,我们就会发现AH=0x700,这里我们来算一下
那么转换过来就是011100000000 → 0x700,CF和PF位之间的下标为1的空,默认为1。
JCC
JCC指令与标志寄存器是挂钩的,有太多了,不需要去背诵。
记住知识点:
1、可以改变EIP的指令:JMP、CALL、RET、JCC的所有指令
2、JCC指令的跳只与标志寄存器有关
—————————————————
A(above):高于 B(below):低于 E(equal):等于 N(not):不 G(greater):最大的 L(less):更少的
标志位的简写:
C:CF标志位 Z:OF标志位 S:SF位(符号位) P:PF标志位 O:OF标志位
Jump If Parity Even:按偶校验转移
JCC指令 | 中文含义 | 英文原意 | 检查符号位 | 典型C应用 |
---|---|---|---|---|
JZ/JE | 若为0则跳转;若相等则跳转 | jump if zero;jump if equal | ZF=1 | if (i == j);if (i == 0); |
JNZ/JNE | 若不为0则跳转;若不相等则跳转 | jump if not zero;jump if not equal | ZF=0 | if (i != j);if (i != 0); |
JS | 若为负则跳转 | jump if sign | SF=1 | if (i < 0); |
JNS | 若为正则跳转 | jump if not sign | SF=0 | if (i > 0); |
JP/JPE | 若1出现次数为偶数则跳转 | jump if Parity (Even) | PF=1 | (null) |
JNP/JPO | 若1出现次数为奇数则跳转 | jump if not parity (odd) | PF=0 | (null) |
JO | 若溢出则跳转 | jump if overflow | OF=1 | (null) |
JNO | 若无溢出则跳转 | jump if not overflow | OF=0 | (null) |
JC/JB/JNAE | 若进位则跳转;若低于则跳转;若不高于等于则跳转 | jump if carry;jump if below;jump if not above equal | CF=1 | if (i < j); |
JNC/JNB/JAE | 若无进位则跳转;若不低于则跳转;若高于等于则跳转; | jump if not carry;jump if not below;jump if above equal | CF=0 | if (i >= j); |
JBE/JNA | 若低于等于则跳转;若不高于则跳转 | jump if below equal;jump if not above | ZF=1或CF=1 | if (i <= j); |
JNBE/JA | 若不低于等于则跳转;若高于则跳转 | jump if not below equal;jump if above | ZF=0或CF=0 | if (i > j); |
JL/JNGE | 若小于则跳转;若不大于等于则跳转 | jump if less;jump if not greater equal | SF != OF | if (si < sj); |
JNL/JGE | 若不小于则跳转;若大于等于则跳转; | jump if not less;jump if greater equal | SF = OF | if (si >= sj); |
JLE/JNG | 若小于等于则跳转;若不大于则跳转 | jump if less equal;jump if not greater | ZF != OF 或 ZF=1 | if (si <= sj); |
JNLE/JG | 若不小于等于则跳转;若大于则跳转 | jump if not less equal;jump if greater | SF=0F 且 ZF=0 | if(si>sj) |