ARM汇编入门

ARM入门

一、ARM简介

​ CTF比赛中,大部分题的都是x86、x86_64的程序,这类程序是属于Intel处理器支持的。但其实,在生活中配置ARM处理器的设备要多得多,比如:Android、网络设备、智能家居等

​ Intel和ARM之间的区别主要是指令集:

  • CISC 复杂指令集
  • RISC 精简指令集

​ 精简指令集通过减少每条指令的时钟周期来缩短执行时间,可以更快的执行指令,但因为指令较少,因此在实现功能时,会显得比Intel冗长。其次,在x86上,大多数指令都可以直接对内存中的数据进行操作,而在ARM上,必须先将内存中的数据从内存移到寄存器中,然后再进行操作

​ ARM和x86之间的更多区别是:

  • 在ARM中,大多数指令可用于条件执行
  • Intel x86和x86-64系列处理器使用little-endian格式
  • 在v3之前,ARM体系结构为little-endian字节序,此后,ARM处理器成为BI-endian,并具允许可切换字节序

​ ARM汇编程序的编译:

  • as program.s –o program.o
  • ld program.o –o program

二、ARM数据类型和寄存器

​ 与高级语言类似,ARM支持对不同数据类型的操作,通常是与ldr、str这类存储加载指令一起使用:
在这里插入图片描述

  • 带符号的数据类型可以同时包含正值和负值,因此范围较小

  • 无符号数据类型可以容纳较大的正值(包括“零”),但不能容纳负值,因此范围更广

2.1 字节序

​ 有两种查看内存中字节的基本方法:小端(LE)或大端(BE)。这两者的区别在于对象的每个字节存储在内存中的字节顺序

​ 在像Intel x86这样的低位字节机器上,最低有效字节存储在最低地址上,在big-endian计算机上,最高有效字节存储在最低地址。ARM体系结构在版本3之前是低端字节序的,因为那时它是双向字节序的,这意味着它具有允许可切换字节序的设置

2.2 寄存器

​ 寄存器的数量取决于ARM版本,ARM32有30个通用寄存器(基于ARMv6-M和基于ARMv7-M的处理器除外),前16个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用

​ 其中,r0-15寄存器可在任何特权模式下访问。这16个寄存器可以分为两组:通用寄存器(R0-R11)和专用寄存器(R12-R15)

2.2.1 32位寄存器

在这里插入图片描述在这里插入图片描述
R0-R12:可在常规操作期间用于存储临时值,指针(到存储器的位置)等,例如:

  • R0在算术操作期间可称为累加器,或用于存储先前调用的函数的结果
  • R7在处理系统调用时非常有用,因为它存储系统调用号
  • R11帮助我们跟踪用作帧指针的堆栈的边界
  • ARM上的函数调用约定指定函数的前四个参数存储在寄存器r0-r3中
  • R13:SP(堆栈指针)。堆栈指针指向堆栈的顶部。堆栈是用于函数特定存储的内存区域,函数返回时将对其进行回收。因此,通过从堆栈指针中减去我们要分配的值(以字节为单位),堆栈指针可用于在堆栈上分配空间。换句话说,如果我们要分配一个32位值,则从堆栈指针中减去4
  • R14:LR(链接寄存器)。进行功能调用时,链接寄存器将使用一个内存地址进行更新,该内存地址引用了从其开始该功能的下一条指令。这样做可以使程序返回到“父”函数,该子函数在“子”函数完成后启动“子”函数调用
  • R15:PC(程序计数器)。程序计数器自动增加执行指令的大小。在ARM状态下,此大小始终为4个字节,在THUMB模式下,此大小始终为2个字节。当执行转移指令时,PC保留目标地址。在执行期间,PC在ARM状态下存储当前指令的地址加8(两个ARM指令),在Thumb(v1)状态下存储当前指令的地址加4(两个Thumb指令)。这与x86不同,x86中PC始终指向要执行的下一条指令

TIPS:

  1. 当参数少于4个时,子程序间通过寄存器R0~R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值
  2. 在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存
  3. R13用作数据帧指针,记作SP;R14用作链接寄存器,记作LR,用于保存子程序返回时的地址;R15是程序计数器,记作PC
  4. ATPCS规定堆栈是满递减堆栈FD
  5. 子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位
2.2.2 64位寄存器

ARM64位参数调用规则遵循AAPCS64,规定堆栈为满递减堆栈,寄存器调用规则如下:
在这里插入图片描述

  • 子程序调用时必须要保存的寄存器:X19~X29和SP(X31)
  • 不需要保存的寄存器:X0X7,X9X15
2.2.3 32位与64位寄存器的差异
  • 栈arm32下,前4个参数是通过r0~r3传递,第4个参数需要通过sp访问,第5个参数需要通过sp + 4 访问,第n个参数需要通过sp + 4*(n-4)访问
  • arm64下,前8个参数是通过x0~x7传递,第8个参数需要通过sp访问,第9个参数需要通过sp + 8 访问,第n个参数需要通过sp + 8*(n-8)访问
  • ARM指令在32位下和在64位下并不是完全一致的,但大部分指令是通用的,特别的,” mov r2, r1, lsl #2”仅在ARM32下支持,它等同于ARM64的” lsl r2, r1, #2”
  • 还有一些32位存在的指令在64位下是不存在的,比如vswp指令,条件执行指令subgt,addle等

三、ARM指令集

3.1 ARM指令集简介

​ ARM处理器具有两种可以运行的主要状态(此处不包括Jazelle):ARM和Thumb

​ 这两种状态之间的主要区别是指令集,其中ARM状态下的指令始终为32位,Thumb状态下的指令始终为16位(但可以为32位)

​ 现在,ARM引入了增强的Thumb指令集(Thumbv2),该指令集允许32位Thumb指令甚至条件执行,而在此之前的版本中是不可能的,为了在Thumb状态下使用条件执行,引入了“ it”指令。但是,这个指令在后来的版本中被删除并替换成了其他的

​ 在编写ARM shellcode时,我们需要摆脱NULL字节,并使用16位Thumb指令而不是32位ARM指令来减少使用它们的机会

Thumb和ARM一样也有不同的版本:

  • Thumb-1(16位指令):在ARMv6和更早的体系结构中使用
  • Thumb-2(16位和32位指令):通过添加更多指令并使它们的宽度为16位或32位(ARMv6T2,ARMv7)来扩展Thumb-1
  • ThumbEE:包括一些针对动态生成的代码的更改和添加

ARM和Thumb之间的区别:

  • 条件执行:ARM状态下的所有指令均支持条件执行。某些ARM处理器版本允许使用“it”指令在Thumb中有条件执行
  • 32位ARM和Thumb指令:32位Thumb指令带有.w后缀
  • 桶式移位器(barrel shifter)是ARM模式的另一个独特功能。它可以用于将多个指令缩小为一个。例如,您可以使用左移,而不是使用两条指令,将寄存器乘以2并使用mov将结果存储到另一个寄存器中:mov r1, r0, lsl

要切换处理器执行的状态,必须满足以下两个条件之一:

  • 我们可以使用分支指令BX(分支和交换)或BLX(分支,链接和交换)并将目标寄存器的最低有效位设置为1。这可以通过在偏移量上加上1来实现,例如0x5530 + 1。可能会认为这会导致对齐问题,因为指令是2字节或4字节对齐的。这不是问题,因为处理器将忽略最低有效位
  • 我们知道如果当前程序状态寄存器中的T位置1,则我们处于Thumb模式

3.2 ARM指令简介

​ 汇编语言由指令构成,而指令是主要的构建块。ARM指令通常后跟一个或两个操作数,并且通常使用以下模板:

MNEMONIC {S} {condition} {Rd},Operand1,Operand2

在这里插入图片描述

注意,由于ARM指令集的灵活性,并非所有指令都使用模板中提供的所有字段。其中,条件字段与CPSR寄存器的值紧密相关,或者确切地说,与寄存器内特定位的值紧密相关

Operand2被称为灵活操作数,因为我们可以以多种形式使用它,例如,我们可以将这些表达式用作Operand2:
在这里插入图片描述

​ 下面以一些常见指令为例:

在这里插入图片描述

3.2.1 RAM32的指令:
指令描述指令描述
MOV移动数据EOR按位异或
MVN移动并取反LDR加载
ADDSTR存储
SUBLDM加载多个
MULSTM存储多个
LSL逻辑左移PUSH入栈
LSR逻辑右移POP出栈
ASR算术右移B跳转
ROR右旋BLLink跳转
CMP比较BX分支跳转
AND按位与BLX使用Link分支跳转
ORR按位或SWI/SVC系统调用
3.2.2 LDR 和 STR:

​ ARM使用加载存储模型进行内存访问,这意味着只有加载/存储(LDR和STR)指令才能访问内存
​ 通常,LDR用于将某些内容从内存加载到寄存器中,而STR用于将某些内容从寄存器存储到内存地址中
在这里插入图片描述
在这里插入图片描述

  • LDR操作:将R0中的地址的值加载到R2寄存器中
  • STR操作:将R2中的值存储到R1中的内存地址处
3.2.3 LDM 和 STM:

​ 在执行压栈和出栈的指令时,通常使用LDMIA/STMDB

​ 但事实上在汇编的过程中,可以看到LDMIA和STMDB指令已转换为PUSH和POP,那是因为 PUSH和STMDB sp!, reglist,POP和LDMIA sp! Reglist是等价的

在这里插入图片描述

3.2.4 条件执行:

在这里插入图片描述

条件码应用举例:

​ 比较两个值大小,并进行相应加1处理,C语言代码为:

if  ( a > b )  
    a++;
else  
    b++;

​ 对应的ARM指令如下(其中R0中保存a 的值,R1中保存b的值):

CMP  R0, R1  ; R0与R1比较,做R0-R1的操作
ADDHI  R0, R0, #1  ;若R0 > R1, 则R0 = R0 + 1
ADDLS  R1, R1, #1  ; 若R0 <= R1, 则R1 = R1 + 1
3.2.5 分支:

分支指令分为三种:

  • 支(B)
    • 简单跳转到功能
  • 分支链接(BL)
    • 将(PC + 4)保存为LR并跳转至功能
  • 分支交换(BX)和分支链接交换(BLX)
    • 与B / BL +exchange指令集相同(ARM <-> Thumb)
    • 需要一个寄存器作为第一个操作数:BX / BLX reg
    • BX / BLX用于将指令集从ARM交换到Thumb
3.2.6 有条件分支:

BEQ的条件分支:将值移入寄存器并在寄存器等于指定值的情况下跳转到另一个函数
在这里插入图片描述
在这里插入图片描述

3.2.7 ARM32与ARM64常用指令对应关系

在这里插入图片描述

四、ARM 堆栈和函数

​ 栈是一种先进后出的数据结构,栈底是第一个进栈的数据所处位置,栈顶是最后一个数据进栈所处的位置。在创建进程时会在栈中分配相应内存,我们使用堆栈来保存局部变量、参数传递、保存寄存器的值

​ ARM中主要使用PUSH和POP指令与堆栈进行交互

​ 注意,这里的PUSH和POP是其他一些与内存相关的指令的别名,而不是真实的指令

在这里插入图片描述

4.1 四种堆栈:ARM采用的满降栈

  • 满/空栈
    • 根据SP指针指向的位置,栈可以分为满栈和空栈
    • 满栈:当堆栈指针总是指向最后压入堆栈的数据
    • 空栈:当堆栈指针总是指向下一个将要放入数据的空位置
  • 升/降栈
    • 根据SP指针移动的方向,栈可以分为升栈和降栈
    • 升栈:随着数据的入栈,SP指针从低地址->高地址移动
    • 降栈:随着数据的入栈,SP指针从高地址->低地址移动
      在这里插入图片描述

这是不同的栈使用的压栈/出栈(存储多个/加载多个)指令:

在这里插入图片描述

这个例子是在满降的栈中,在执行push和pop操作时,sp的变化:

  • 最开始,sp指向0x01的内存,然后向r0中写入立即数2,sp不变
  • 执行push操作,r0中的立即数2入栈,sp指向0x02的内存
  • 再次将立即数3写入r0,sp不变,整个栈是从低地址向高地址生长的
  • 最后执行pop,将栈顶(低位)的立即数2弹出到r0,sp重新指向0x01,r0从3变为2

在这里插入图片描述

4.2 栈帧

​ 栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。栈帧的两个边界分别由fp(r11)和sp(r13)来限定

在这里插入图片描述

​ 前面描述的是ARM的栈帧布局方式。main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长

​ FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量

​ 如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。从main函数进入到func1函数,main函数的上边界和下边界保存在被它调用的栈帧里面

​ ARM也可以用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动

4.3 函数体

​ ARM中的函数主要由Prologue、Body、Epilogue组成

​ Prologue的目的是保存程序的先前状态,并为函数的局部变量设置堆栈

在这里插入图片描述
​ Body主要负责实现函数功能,简单的例子:
在这里插入图片描述

注意:当要传递的参数超过4个时,ARM会另外用堆栈来存储其余参数

​ Epilogue用于将程序的状态恢复到其初始状态

在这里插入图片描述

4.4 叶函数和非叶函数

​ 叶函数是指本身不会调用其他函数。非叶函数是指除了它自己的逻辑外,还会调用到其他的函数。这两种函数的实现是相似的,但也存在一些不同

​ 他们Prologue和Epilogue的实现方式不同:

  • 非叶函数中Prologue会将更多的寄存器保存到堆栈中。因为在执行非叶函数时LR会被修改,因此需要保留该寄存器的值,便于以后恢复
  • 在跳转到主函数之前,BL指令将函数main的下一条指令的地址保存到LR寄存器中。由于叶函数不会在执行过程中更改LR寄存器的值,因此该寄存器现在可用于返回父(主)函数
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

五、搭建ARM PWN环境

  • Ubuntu 18.04

  • 安装 gdb、gdb-multiarch

    • $ sudo apt-get install gdb gdb-multiarch
  • 安装 gdb plugin(peda、pwndbg、gef)

    • peda-arm:https://github.com/alset0326/peda-arm
    • pwndbg:https://github.com/pwndbg/pwndbg
    • gef:https://github.com/hugsy/gef
  • 安装 pwntools

    • $ sudo pip install pwntools

qemu是一款可执行硬件虚拟化的虚拟机,与他类似的还有Bochs、PearPC,但qemu具有高速(配合KVM)、跨平台的特性

qemu主要有两种运行模式:

  • qemu-user

  • qemu-system

安装 qemu-user:

$ sudo apt-get install qemu qemu-user qemu-user-static

此时可以运行静态链接的arm程序,而要运行动态链接的程序,需要安装对应架构的动态链接库:

$ apt search "libc6-" | grep "arm“

安装 qemu-system:

$ sudo apt-get install qemu qemu-user-static qemu-system uml-utilities bridge-utils

配置qemu-system网络:

qemu-system模式配置网络常见的方法是tap桥接,安装网络配置的依赖文件:

$ sudo apt install uml-utilities bridge-utils

修改Ubuntu主机网络接口配置文件:

$ sudo vim /etc/network/interfaces

在这里插入图片描述

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值