Assembly
Assembly
前置知识
寄存器:CPU中可以存储数据的器件
汇编语言由以下三类组成
汇编指令
伪指令(即指示性语句)
其他符号
在内存或者磁盘上 数据和指令地位相同
数据总线的宽度决定了CPU和外界的数据传输速度.
cpu内部总线实现cpu内部各个器件之间的联系
cpu外部总线实现cpu和主板上其他器件的联系
编译器的作用:将汇编指令转换为机器码
汇编语言发展至今,有3类指令
- 汇编指令,比如mov
- 伪指令,没有对应的机器码,只是给编译器看的
- 其他符号,比如+-*/,也没有机器码,只是给编译器看的
对于CPU来讲,系统中所有存储器的存储单元都处于一个统一的逻辑存储器中,比如包含显存,主板的ROM等等。
寄存器
8086CPU共有14个寄存器
8086CPU所有的寄存器都是16位的,可以存放两个字节
一个字为两个字节
8086上一代CPU中的寄存器都是8位,为了兼容性,AX,BX,CX,DX都可当做独立的两个寄存器使用.
通用寄存器8个
AX,BX,CD,DX通常存放一般数据,被称为通用寄存器
AX可分为第八位AL和高八位AH
汇编指令不区分大小写
16位CPU有以下特征
- 运算器一次可以处理16位数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路为16位
8086有20位地址总线.8086内部为16位结构,只能传送16位数据,那么如何合成20位数据?
地址加法器合成物理地址的方法:
物理地址=段地址*16+偏移地址
段的概念
以后在编程需要时,可以将若干地址连续的内存单元看作一个段.
注意,由于段地址*16必然是16的倍数,所以一个段的起始地址一定是16的倍数
由于偏移地址为16位,16位的寻址能力为64k,所以一个段的长度最大为64k
cpu可以用不同的段地址和偏移地址形成用一个物理地址
8086给出物理地址的方法:
如图,段地址与偏移地址均为16位,后经过地址加法器转换为20位的物理地址。
物理地址=段地址*16+偏移地址,更一般的说,是物理地址=基础地址+偏移地址
段寄存器
段寄存器就是提供段地址的;
8086有4个段寄存器:
CS:代码段寄存器,存放内存代码段区域的入口地址
DS:数据段寄存器
当
mov 寄存器,内存数据
时,8086CPU自动读取DS中的数据作为内存单元的段地址SS:堆栈段寄存器
ES:附加段寄存器
当8086CPU要访问内存时,由这四个段寄存器提供内存单元的段地址
CS和IP是8086cpu中最关键的寄存器,他们指示了cpu当前要读取指令的地址
ip为指令指针寄存器
cpu刚开始工作时,cs和ip被设置为cs=FFFFH,ip=0000H;
如果说,内存中的一段信息曾经被cpu执行过的话,那么他所在的内存单元必然被cs:ip指向过
mov指令不能用于设置cs ip的值
同时修改cs ip中的内容:
jmp 2AE3:3
若只修改ip内容:
jmp ax
代码段:
cpu只认被cs:ip指向的内存单元中的内容为指令
注意:8086CPU并不支持将数据直接送入段寄存器(硬件方面的设计缺陷),必须用寄存器中转,例如
mov ds,1000h
非法,比如用其它寄存器中转,即mov ax,1000h;mov ds,ax
寄存器(内存访问)
任何两个地址连续的内存单元,N号单元和N+1号单元,可以将他们看做两个内存单元,也可以看做一个地址为N的字单元中的高位字节单元和低位字节单元.
已知的mov指令可以完成两种传送过程:
- 将数据直接送进寄存器
- 将一个寄存器中的内容送入另一个寄存器中
- 将一个字节单元中的内容送入一个寄存器:
mov al [偏移地址]
会自动取DS中的数据作为偏移地址 - 8086cpu不支持将数据直接送入段寄存器的操作
mov,add,sub指令
这一章以8086CPU为基础,8086CPU的所有寄存器都是16位的。
数据传送类指令
任何传送类指令不影响标志位
通用数据传送指令
一般数据传送指令MOV
MOV AX 09H是可以的
堆栈操作指令PUSH POP
一次的操作是两个字节
交换指令XCHG
操作数不能两个同时是存储器操作数
段寄存器不能是操作数
查表转换指令XLAT
采用隐含寻址
[BX+AL]->AL
字位拓展指令CBW CWD
永远当做有符号数进行拓展
采用隐含寻址
若AL最高位为1,执行CBW后AH为0FFH
若AL最高位为0,则执行CBW后AH为00H
CWD将AX的符号位拓展到DX
输入输出指令IN OUT
若端口号大于FFH,则端口号必须由DX寄存器指定
IN ACC PORT,这里ACC为AL或者AX,若为16位,则是AX,若8位,则为AL
目标地址传送指令LEA LDS LES
对于LEA,原操作数必为存储器操作数,且目标操作数必须为16位通用寄存器
LDS Reg16 Mem32,读取的是数,不是地址,高16位放在DS,低16位放在指定寄存器
标志传送指令LAHF SAHF PUSHF POPF
标志寄存器:OF DF SF符号标志位(负数时为1) ZF为0位 AF辅助进位位 PF奇偶位(低8位1的个数为偶数时为1) CF
对于LAHF,把标志寄存器的低8位放在AH,SAHF则相反
注意,这里的奇偶校验位是校验的低八位
如何判断溢出?有三种思路
第一种是正+正=负 负+负=正
第二种是最高有效位和符号位进位异或为1
第三种是双符号位,若不同则溢出
算数运算类指令
算数运算类指令,对于加法和减法,除了自增自减不影响CF外,其余均影响CF
对于乘除法,只记乘法即可,除法对标志位均无影响
加法ADD ADC INC
减法SUB SBB DEC NEG CMP
对于NEG,只有操作数为0时,进位标志CF被置0,其余情况均有借位
已知x求-x的补码? 连同符号位全部变反,然后末尾加1 0000 0010 2的补码 1111 1110 (-2)补码 加起来为0
当字节操作数为80H或者8000H时,结果将无变化,但是溢出标志OF置为1
注意,JE和JZ是一样的,JNE和JNZ是一样的
JP和JPE是一样的,JNP和JPO是一样的
对于CMP A,B
若为无符号数,
- A>B JA JNBE
- A<B JB JNAE
- A>=B JAE JNB
- A<=B JBE JNA
若为有符号数
- A>B JG JNLE
- A<B JL JNGE
- A>=B JGE JNL
- A<=B JBE JNA
如何比较AB的大小?
若为无符号数 是比较好判断的 首先是ZF若为1 则A=B,若ZF为0,CF为1,则A<B,否则A>B
若为有符号数,则比较复杂,SF异或OF为0且ZF为0时,A>B
SF异或OF为1且ZF为0时,A<B
乘法MUL IMUL
若两个数都是8位,则其中一个默认在al
中,结果存放在ax
若两个数都是16位,则其中一个默认在ax
中,结果存放在dx
,ax
MUL对CF和OF的标志位产生有效影响,但是其他标志位不确定
注意!!!若结果的高半部分为全0,则CF=OF=0,否则为1
什么是IMUL?
IMUL是有符号数的乘法,对标志位的影响类似MUL,唯一的区别是当高半部分是低半部分的符号拓展时CF=OF=0,否则为1
除法DIV IDIV
除法对标志位均无影响
除法要求被除数的字长是除数字长的两倍
采用隐含寻址,被除数可以是AX或者DX+AX
若除数为8位,ax / 除数 = al ... ah
若除数为16位,dx,ax / 除数 = ax ... dx
逻辑运算
逻辑与AND
主要应用:让某一位清零
会影响六个标志位,但是CF和OF始终为0
逻辑或OR
会影响六个标志位,但是CF和OF始终为0
主要应用:让某一位变成1
逻辑非NOT
对所有标志位均无影响
异或XOR
让某些位取反
会影响六个标志位,但是CF和OF始终为0
测试指令TEST
两操作数按位与,但是结果不送回目的操作数
会影响六个标志位,但是CF和OF始终为0
移位指令
任何移位 出来的永远是到CF
非循环移位
算数移位SAL SAR
SAL OPRD CL,当移1位时,CL可以为1,否则必须是CL
若移位后操作数的最高位和CF不同,则OF=1,溢出
左移补的是0 右移补的是CF
逻辑移位SHL SHR
和算术移位类似,但是OF=1不代表溢出
左移和右移补的是0
循环移位
带CF RCL RCR
不带CF ROL ROR
串操作指令
源串DS:SI,其中DS可改为SS ES
目的串:ES:DI,都不可更改
若DF=0时 按照自增方式,DF=1,按照自减方式
重复前缀不影响标志位
传操作指令时8086指令系统中唯一源操作数和目的操作数都为存储器操作数
重复前缀由REP REPZ REPNZ
串操作指令可能影响标志位
串传送MOVSB MOVSW
串比较CMPSB CMPSW
串比较指令影响标志位
串扫描SCASB SCASW
串扫描指令影响标志位
串装入LODSB LODSW
串存储STOSB STOSW
程序控制指令
无条件转移
影响标志位
无条件转移分为段间转移和段内转移,段内转移分为段内短转移和段内近转移,
段间转移:jmp far ptr 标号,又称为远转移
段内短转移:jmp short 标号
段内近转移:jmp near ptr 标号
段内转移还可以是jmp word ptr 内存单元地址
段间转移还可以是jmp dword ptr 内存单元地址
条件转移
影响标志位
采用直接寻址方式的短转移,即只能以当前IP为中心的-128-127字节
循环控制指令LOOP LOOPZ LOOPE LOOPNP LOOPNE
所有的循环指令都是短转移
也是以当前IP为中心的-128-127字节
子程序调用和返回
中断向量的高16位作为CS 低16位作为IP
call不能实现短转移
call 标号相当于push ip,jmp near ptr 标号
call far ptr 标号实现的是段间转移
中断类型指令INT IRET
对于INT
和调用子程序不同,这里需要改变标志位
首先是把IF置为0,TF置为0
然后压栈CS 压栈IP
然后得到中断服务程序的入口地址
对于IRET
中断返回指令位于中断服务子程序的最后一条,用于返回被中断的程序
和RET不用:除了返回 还要返回标志寄存器
处理器控制指令
CLC CLD CLI STI STD STC CMC
汇编语言程序设计
标号
标号可以看做指令地址的别名
变量
在指令性语句中直接引用变量名代表一个数
当变量出现在寄存器间接寻址的操作数中表示该变量的偏移地址
在指示性语句中代表一个偏移地址
ADR3 DD NUM中低16位存偏移地址 高16位存段地址
表达式
任何表达式的值在程序被汇编的过程中计算确定 而不是到程序运行时计算
伪指令
操作数的值不能超过伪指令所定义的范围
STRING1 DB 'ABCDEFG';正确
STRING2 DW 'AB','CD';
DW/DD不允许2个以上字符
属性定义伪指令
1 |
|
SUB1和SUB2入口地址相同
表达式运算符
算术运算符 +-*/ mod
逻辑运算符 and or not xor
关系运算符eq等于 ne不等于 lt小于 le小于等于 gt大于 gr大于等于
若比较常量 则按照无符号数比较
若比较变量 则比较偏移地址
取值运算符 OFFSET
属性运算符 PTR
TYPE运算符 取变量或者标号的类型属性 NEAR-1 FAR-2 BYTE1 WORD2 DWORD4 QWORD8 TQWORD10
LENGTH运算符 若无DUP则为1 若有DUP则按照DUP取值
SIZE只能用于变量 取值为LENGTH和TYPE的乘积
HIGH/LOW运算符
取高字节和低字节
符号定义伪指令
count equ 5
段定义伪指令
1 |
|
定位类型有四种
对于PARA和PAGE类型,起始地址一定是0
PAGE 页型
一个页面256个字节
表示该段从一个页面的边界开始存放数据
且段地址一定以00H结尾
PARA 小节型(默认值)
且地址一定是以0H结尾
WORD 字型
起始地址00B结尾
BYTE 字节型
起始地址任意
组合类型定义段与段之间的关系
NONE
这个是默认情况 即使具有相同的段名 也分别放入内存
PUBLIC
相同名字的段会放在一起
STACK
这个比较重要
COMMON
保留最长的
MEMORY
表示本段在存储器应定位在其它所有段之后的最高地址上
AT
根据表达式的值确定段地址
先按照组合类型进行组合,优先级比较高,然后按照类别名进行组合,优先级比较低
设置段寄存器伪指令
CS段不用初始化
1 |
|
那么操作系统怎么识别CS呢
1 |
|
过程定义伪指令
1 |
|
当前位置计数器$与定位伪指令ORG
DOS功能调用
带显示的键盘输入(1号功能)
1 |
|
最后的回车不算进length,但是算进max,也就是最多输入max-1个字符
回车键最后对应的Ascii是0dH
换行是0aH
不带显示的键盘输入(8号功能)
字符串输入(0AH号功能)
字符显示(2号功能)
字符串显示(9号功能)
返回DOS系统
函数调用分析
call指令后面可以跟上不同的数据源,比如寄存器,存储单元。而不同的数据源不一定会将CS入栈。
1 |
|
ret指令相当于进行如下操作:
1 |
|
retf指令相当于进行如下操作:
1 |
|
leave指令等价于:
1 |
|
如何使用objdump得到反汇编代码?
1 |
|
8086CPU格式
1 |
|
64位Intel格式
1 |
|
32位Intel格式
1 |
|
我们对下面的代码进行分析:
1 |
|
我们注意到,在执行call之前,做了一件事:将参数压入栈
对于add(a, b)
,首先压栈的是参数b,然后是参数a;
然后我们执行call指令
这里,我们需要注意一下call之后的指令地址:0x565561fe
栈溢出的一个例子
1 |
|
我们的目标是执行exploit
函数,通过前面我们对函数调用的分析,我们知道,在栈中曾经暂存了IP寄存器的数值,而这最终要pop到IP寄存器中,我们是否可以通过覆盖掉此值实现跳转到任意函数地址呢?答案是可以的,因为CPU并不会检查栈溢出。
我们通过gdb直接进入func
函数。
通过对汇编代码的分析,我们可以构建如下的payload:
1 |
|
字节对齐
编译如下代码:
1 |
|
观察反编译代码,我们可以看到有三行比较特殊,负责16字节对齐,对齐前后的效果只是esp的位置发生了改变。