ARM64-PWN笔记01

  • Author:ZERO-A-ONE
  • Date:2021-01-08

所用使用到程序和代码均存放在开源平台Github和Gitee

  • Github:https://github.com/ZERO-A-ONE/arm-pwn-notes
  • Gitee:https://gitee.com/zeroaone/arm-pwn-notes

一、前言

​ 随着Apple M1处理器、华为鲲鹏920与微软高通SQ1等ARM处理器逐步投入消费级层面,很多汽车如特斯拉等平台也采用了ARM架构的处理器,ARM处理器架构不再局限于手机等普通的低功耗移动设备领域,在未来更多通用计算层面ARM架构处理器的身影必将常常出现

​ 本人最近在研究ARM架构处理器的PWN和逆向相关领域,加上最近华为CTF出现了很多基于ARM和鸿蒙的题目,市面上很多关于ARM架构的文章还是在讨论基于ARMv7也就是32位架构的,但是目前市面上绝大多数设备以及步入了ARMv8架构,也就是AARCH64架构,是64位架构,我们将优先讨论在64位环境下的情况

​ ARM处理架构有多种指令集包括:

  • "应用"配置: Cortex-A 系列
  • "嵌入式"配置: Cortex-R 系列
  • "微处理器"配置: ARM Cortex-M 系列

​ 我们一般题目和手机、服务器和电脑等通用应用环境下都是采用的Cortex-A系列

​ 我使用的实验环境如下图所示:

  • CPU:Huawei Cloud Kunpeng 920
    • 架构:TaiShan V110 - ARMv8.2-A 64bits
  • RAM:4GB
  • OS:Ubuntu Server 18.04 with ARM

二、ARM汇编

​ 本段文章ARMv7部分主要参考《汇编语言程序设计——基于ARM体系结构(第三版)》,ARMv8部分主要参考《Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile》

​ 个人认为学习入门一门汇编语言需要的就是了解这门汇编语言的语言指令结构、常用指令、寄存器结构、函数调用与栈等的利用

2.1 ARM

2.1.1 CPU模式

​ CPU ARM架构指定了以下的CPU模式。在任何时刻,CPU只可处于某一种模式,但可由于外部事件(中断)或编程方式进行模式切换

  • 用户模式:仅非特权模式
  • 系统模式:仅无需例外进入的特权模式。仅以执行明确写入CPSR的模式位的指令进入
  • Supervisor (svc) 模式:在CPU被重置或者SWI指令被执行时进入的特权模式
  • Abort 模式:预读取中断或数据中断异常发生时进入的特权模式
  • 未定义模式:未定义指令异常发生时进入的特权模式
  • 干预模式:处理器接受一条IRQ干预时进入的特权模式
  • 快速干预模式:处理器接受一条IRQ干预时进入的特权模式
  • Hyp 模式:armv-7a为cortex-A15处理器提供硬件虚拟化引进的管理模式

2.2 ARMv7

​ ARM 是 Load/Store 结构的处理器,能且仅能在寄存器中进行操作,而不能在内存中做运算。使用时要从存储器读值送寄存器,算完再放回去

​ 指令可以分为这么几类:数据处理、Load/Store(寄存器、内存数据传输)、跳转、CPSR处理、异常产生、协处理器

2.2.1 指令格式

​ ARM指令通常后跟一个或两个操作数,并且通常使用以下模板:

MNEMONIC {S} {condition} {Rd},Operand1,Operand2
  • MNEMONIC:指令简称
  • {S}:可选的后缀,如果指定了S,则根据操作结果更新条件标志
  • {condition}:执行指令需要满足的条件
  • {Rd}:用于存储指令结果的寄存器
  • Operand1:第一个操作数,寄存器或立即数
  • Operand2:第二个操作数[可选],可以是立即数或带有可移位的寄存器

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

Operand2被称为灵活操作数,因为我们可以以多种形式使用它,例如,我们可以将这些表达式用作Operand2:

指令意义
#123立即数
Rx寄存器(如R1,R2,R3…)
Rx, ASR n寄存器算数右移n位
Rx, LSL n寄存器逻辑左移n位
Rx, LSR n寄存器逻辑右移n位
Rx, ROR n寄存器向右旋转n位
Rx, RRX寄存器向右旋转1位,扩展

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

ADD R0,R1,R2
  • MNEMONIC:ADD
  • Rd:R0
  • Operand1:R1
  • Operand2:R2
  • 含义:将R1的内容和R2的内容相加并将结果存储到R0
ADD R0,R1,#2
  • MNEMONIC:ADD
  • Rd:R0
  • Operand1:R1
  • Operand2:#2
  • 含义:将R1的内容和立即数2相加并将结果存储到R0
MOVLE R0,#5
  • MNEMONIC:MOV
  • {condition}:LE
  • Rd:R0
  • Operand1:#5
  • 含义:仅在满足条件LE(小于或等于)的情况下,将立即数5移动到R0
MOV R0,R1,LSL #1
  • MNEMONIC:MOV
  • Rd:R0
  • Operand1:R1
  • Operand2:LSL #1
  • 含义:将R1的内容逻辑左移1位到R0
2.2.2 寄存器

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

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

​ ARMv7上的函数调用约定指定函数的前四个参数存储在寄存器r0-r3中
在这里插入图片描述

  • R0-R12:可在常规操作期间用于存储临时值,指针(到存储器的位置)等,例如:
    • R0:在算术操作期间可称为累加器,或用于存储先前调用的函数的结果
    • R7:在处理系统调用时非常有用,因为它存储系统调用号
    • R11:帮助我们跟踪用作帧指针的堆栈的边界
  • 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.3 常见指令
指令描述指令描述
MOV移动数据EOR按位异或
MVN移动并取反LDR加载
ADDSTR存储
SUBLDM加载多个
MULSTM存储多个
LSL逻辑左移PUSH入栈
LSR逻辑右移POP出栈
ASR算术右移B跳转
ROR右旋BLLink跳转
CMP比较BX分支跳转
AND按位与BLX使用Link分支跳转
ORR按位或SWI/SVC系统调用
2.2.4 访存

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

LDR R2,[R0]
  • 加载指令,读取内存的值到寄存器
  • 将R0中的地址的值加载到R2寄存器中
STR R2,[R1]
  • 存储指令,从寄存器中读取值写入内存
  • 将R2中的值存储到R1中的内存地址处

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

LDR:

指令意义
LDR加载word
LDR h加载无符号half word
LDR sh加载有符号half word
LDR b加载无符号byte
LDR sb加载有符号byte

STR:

指令意义
STR存储word
STR h存储无符号half word
STR sh存储有符号half word
STR b存储无符号byte
STR sb存储有符号byte
  • 带符号的数据类型可以同时包含正值和负值,因此范围较小
  • 无符号数据类型可以容纳较大的正值(包括“零”),但不能容纳负值,因此范围更广
2.2.5 访栈

​ 堆栈的操作包括建栈、进栈、出栈3种基本操作

2.2.5.1 建栈

​ 建栈就是规定堆栈底部在RAM寄存器种的位置,用户可以通过LDR命令设置SP的值来建立堆栈

LDR R13,=0x90010
LDR SP,=0x90010
2.2.5.2 压栈和出栈

​ 在ARM体系结构中使用多寄存器指令来完成堆栈操作:

  • 出栈使用:LDM
  • 入栈使用:STM

​ LDM和STM指令往往结合下面一些参数来实现堆栈的操作:

  • FD:满递减堆栈
  • ED:空递减堆栈
  • FA:满递增堆栈
  • EA:空递增堆栈

​ 从这里可以看出ARM的堆栈指令有两个属性,空(满)栈和递增(减)栈

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

​ 在ARM制定了ARM-Thumb过程调用标准(ATPCS)中堆栈被定义为满递减式堆栈,也就是和普通x86的堆栈方式是一样的,从高地址往低地址增长。因此LDMFD和STMFD指令分别被用来支持POP操作(出栈)和PUSH操作(进栈)

下列指令说明了进栈和出栈的过程,设指令执行之前:

  • SP=0x00090010(R13)
  • R4=0x00000003
  • R3=0x00000002
  • R2=0x00000001
STMFD SP!,{R2-R4}
LDMFD SP!,{R6-R8}

分析:

​ 第一条指令将R2~R4的数据入栈,指令执行前SP的值为0x00090010,指令执行时SP指向下一个地址(SP-4)存放R4,然后依次存放R3,R2,数据入栈后SP的值为0x00090004,指向堆栈的满位置,如果有数据继续入栈则下一地址为0x90000

​ 第二条指令实现退栈操作,在第一条指令的基础上,表示将刚才入栈的数据分别退栈到R6~R8,退栈后SP指向0x00090010。实际上STMFD指令相当于STMDB指令,LDMFD指令相当于LDMIA指令

2.2.6 条件执行
条件码含义标志位状态
EQ等于Z==1
NE不等Z==0
GT大于(Z==0)&&(N==V)
LT小于N!=V
GE大于或等于N==V
LE小于或等于(Z==1)||(N!=V)
CS or HS无符号大于或等于C==1
CC or LO无符号小于C==0
MI负数N==1
PL正数N==0
AL无条件执行-
NV不执行-
VS有溢出V==1
VC无溢出V==0
HI无符号大于或等于(C==1)&&(Z==0)
LS无符号小于或等于(C==0)||(Z==0)

条件码应用举例:

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

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

对应的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
2.2.7 分支

分支指令分为三种:

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

例如以下代码:

.text
.global _start
_start
	mov r0,#2
	mov r1,#2
	add r0,r0,r1
	cmp r0,#4
	beq fun1
	add r1,#5
	b fun2
func1:
	mov r1,r0
	bx lr
func2:
	mov r0,r1
	bx lr

流程图如下:
在这里插入图片描述

程序的意思就是当R0等于4的时候跳转到FUNC1,否则执行另外一条路径

2.2.8 函数

​ 首先需要简单介绍一下ARM的栈帧情况

​ 栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈

​ 在X86架构中一个栈帧是由EBP(栈基地址寄存器)和ESP(栈顶寄存器)来限定的。在ARM架构中栈帧的两个边界分别由fp(r11)和sp(r13)来限定

​ 这是一个简单的ARM中栈帧情况的示意图:
在这里插入图片描述

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

  • FP:栈基址,它指向函数的栈帧起始地址
  • SP:函数的栈指针,它指向栈顶的位置

​ ARM的规则是:Procedure Call Standard for the ARM Architecture简称就是AAPCS了

利用到的寄存器主要是:

  • PC寄存器:R15
  • LR寄存器:R14
  • SP寄存器:R13
  • FP寄存器:R11

​ ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量

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

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

​ 下面来介绍一下ARM的函数。我们先用一个简单的例子看看:

#include<stdio.h>
void Fun1(){
    printf("Hello World!");
}
int main(){
    int a = 1;
    Fun1();
    return 0;
}

​ 编译指令:

$ arm-none-linux-gnueabihf-gcc -g -z execstack 01.c -o 32test

​ 编译成的汇编代码:

; Attributes: bp-based frame fpd=8
; int __cdecl main(int argc, const char **argv, const char **envp)
EXPORT main
main

a = -4

PUSH            {R11,LR}
SUB             SP, SP, #8
ADD             R11, SP, #0
MOVS            R3, #1
STR             R3, [R11,#8+a]
BL              Fun1
MOVS            R3, #0
MOV             R0, R3
ADDS            R11, #8
MOV             SP, R11
POP             {R11,PC}
; End of function main

; Attributes: bp-based frame
; void Fun1()
EXPORT Fun1
Fun1
PUSH            {R11,LR}
ADD             R11, SP, #0
MOV             R0, #aHelloWorld ; format
BLX             printf
NOP
POP             {R11,PC}
; End of function Fun1

函数调用的共识:

  • 函数不对cpsr的内容进行任何假设。条件代码N,Z,C和V是未知的
  • 函数可以自由修改寄存器r0,r1,r2和r3
  • 函数不能在r0,r1,r2和r3的内容上假设任何内容,除非它们扮演参数的角色
  • 一个函数可以自由修改lr,但是在离开该函数时将需要输入该函数时的值(因此,该值必须保留在某处)
  • 一个函数可以修改所有剩余的寄存器,只要它们的值在离开函数时就被恢复即可。 这包括sp和寄存器r4至r11
  • 是函数自己平衡的堆栈

所以调用函数后,(仅)寄存器r0,r1,r2,r3和lr被覆盖

结果返回规则:

  • 结果为一个32位的整数时,可以通过寄存器R0返回
  • 结果为一个64位整数时,可以通过R0和R1返回,依此类推
  • 结果为一个浮点数时,可以通过浮点运算部件的寄存器f0,d0或者s0来返回
  • 结果为一个复合的浮点数时,可以通过寄存器f0-fN或者d0~dN来返回
  • 对于位数更多的结果,需要通过调用内存来传递

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

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

PUSH            {R11,LR}	;将帧指针和LR保存到堆栈中
SUB             SP, SP, #8  ;在堆栈上分配一些缓冲区,同时会为栈帧分配空间
ADD             R11, SP, #0 ;设置堆栈框架的底部

​ Body主要负责实现函数功能,简单的例子:

MOV             R0, #aHelloWorld ; format ;设置局部变量,printf的第一个参数
BLX             printf	;调用printf

​ 还有一个例子:

MOV				R0,#1	;设置局部变量,function的第一个参数
MOV				R1,#2	;设置局部变量,function的第二个参数
BL				function	;调用function

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

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

MOV             SP, R11		;重新调整堆栈指针
POP             {R11,PC}	;从堆栈中恢复帧指针,通过直接加载到PC跳到先前保存的LR

​ 或者:

SUB				SP,R11,#0	;重新调整堆栈指针
POP				{R11,PC}	;从堆栈中恢复帧指针,通过直接加载到PC跳到先前保存的LR

叶函数和非叶函数:

  • 叶函数:本身不会调用其他函数
  • 非叶函数:除了它自己的逻辑外,还会调用到其他的函数

​ 这两种函数的实现是相似的,但也存在一些不同。他们Prologue和Epilogue的实现方式不同:

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

对于Prologue:

;叶子函数 Prologue
PUSH		{R11,LR}	;将帧指针和LR保存到堆栈中
ADD			R11,SP,#0	;设置堆栈框架的底部
SUB			SP,SP,#16	;在堆栈上分配一些缓冲区
;非叶子函数 Prologue
PUSH		{R11}		;将帧指针保存到堆栈中
ADD			R11,SP,#0	;设置堆栈框架的底部
SUB			SP,SP,#12	;在堆栈上分配一些缓冲区

对于Epilogue:

;叶子函数 Epilogue
ADD			SP,R11,#0	;重新调整堆栈指针
POP			{R11}		;恢复帧指针
BX			LR			;通过LR寄存器跳回main
;非叶子函数 Epilogue
SUB			SP,R11,#0	;重新调整堆栈指针
POP			{R11,PC}	;从堆栈指针中恢复帧指针,通过直接加载到PC
2.2.9 Thumb

​ 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模式

2.3 ARMv8

​ ARMv8是ARMv7之后的一个重要架构更新。其中一个主要的变化是引入了64位的架构,即AArch64。AArch64状态只有在ARMv8架构中才有。而且在AArch64状态下执行的代码只能使用A64指令集。当然ARM为了维持整个生态参与者的利益,ARMv8还是保持与现有32位体系结构兼容性的AArch32,即ARMv8之前的ARMv7配置文件定义的那套设计规范

​ 目前的主流手机处理器与树莓派3之后的ARM架构处理器都是基于ARMv8的

  • 栈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等
2.3.1 寄存器
寄存器描述
r0 - r30通用整型寄存器,64 位,当使用 x0 - x30 访问时,代表的是 64 位的数;当使用 w0 - w30 访问的时候,访问的是这些寄存器的低 32 位
fp(x29)保存栈帧地址(栈底指针)
lr(x30)通常称x30为程序链接寄存器,保存子程序结束后需要执行的下一条指令
SP(x31)保存栈指针,使用 sp/wsp 来进行对 sp 寄存器的访问
PCPC寄存器存的是当前执行的指令的地址。在 arm64 中,软件是不能改写 pc 寄存器的
SPRs状态寄存器,存放状态标识,可分为 CPSR (The Current Program Status Register) 和 SPSRs(The Save Program Status Registers)。一般都是使用 CPSR,当发生异常时,CPSR 会存入 SPSR。当异常恢复,再拷贝回 CPSR
zr零寄存器,里面存的是 0 (zero register)一般使用 wzr/xzr ,w 代表 32位,x 代表 64 位
v0 - v31向量寄存器,也可以说是浮点型寄存器,每个寄存器大小是 128 位,可以用 Bn Hn Sn Dn Qn 来访问不同的位数(8 16 32 64 128)
  • 在A64指令中既可以操作32位的寄存器也可以操作64位的寄存器,视使用的寄存器标识而定,即Wn和Xn(其中n的范围0~30),W表示32位,X表示64位
  • 如果你在指令中使用32位形式的寄存器,那么源寄存器高32位部分会被忽略掉,目标寄存器高32位将被置0
2.3.2 常用的运算指令
运算指令描述含义
mov x1,x0将寄存器x0的值赋值给x1数据传送
add x0,x1,x2x0 = x1 + x2加法
sub x0,x1,x2x0 = x1 - x2减法
mul x0,x1,x2x0 = x1 * x2乘法
sdiv x0,x1,x2x0 = x1 / x2除法
and x0,x0,#0xFx0 = x0 & #0xF与操作
orr x0,x0,#9x0 = x0 或 #9或操作
eor x0,x0,#0xFx0 = x0 ^ #0xF异或操作
lsl x0, #1x0<<1逻辑左移
add x0, x1, x2 		;把 x1 + x2 = x0 这样一个操作。
sub sp, sp, 0x30	;把 sp - 30 存入sp.
cmp x11, #4			;相当于 subs xzr, x11, #4.  
					;如果 x11 - 4 == 0, 那么状态寄存器NZCV.Z = 1
					;如果 x11 - 4 < 0, 那么 NZCV.N = 1

​ NZCV是状态寄存器中存的几个状态值,分别代表运算过程中产生的状态,其中:

  • N, negative condition flag,一般代表运算结果是负数
  • Z, zero condition flag, 运算结果为0
  • C, carry condition flag, 无符号运算有溢出时,C=1
  • V, oVerflow condition flag 有符号运算有溢出时,V=1
2.3.3 寻址指令

分为两种,存和取

L 打头的基本都是取值指令,如 LDR(Load Register)、LDP(Load Pair)

S 打头的基本都是存值指令,如 STR(Store Register)、STP(Store Pair)

ldr    x0,[x1]               ;从 x1 指向的地址里面取出一个64位大小的数存入x0
ldp    x1,x2,[x10, #0x10]    ;从 x10+0x10 指向的地址里面取出2个64位的数,分别存入x1、x2
str    x5,[sp, #24]          ;往内存中写数据(偏移值为正), 把 x5 的值(64位的数值)存到 sp+24 指向的地址内存上
stur   w0,[x29, #0x8]        ;往内存中写数据(偏移值为负),将 w0 的值存储到 x29 - 0x8 这个地址里
stp    x29,x30,[sp, #-16]!   ;把 x29、x30 的值存到 sp-16 的地址上,并且把sp-=16  Note:后面有个感叹号的,然后没有stup这个指令哈
ldp    x29,x30,[sp],#16      ;从 sp 地址取出16 byte数据,分别存入x29、x30,然后 sp+=16

注:ldr既可以当读取地址的伪指令,也可以是内存访问指令。当第二个参数前面有”=”时,表示伪指令。否则表示内存访问指令。操作数都是32bits的

其中寻址的格式由分为下面这3种类型:

[x10, #0x10]      ;signed offset。 意思是从 x10 + 0x10的地址取值
[sp, #-16]!       ;pre-index。  意思是从 sp-16地址取值,取值完后在把 sp-16  writeback 回 sp
[sp], #16         ;post-index。 意思是从 sp 地址取值,取值完后在把 sp+16 writeback 回 sp
2.3.4 跳转指令
  • BL:是有返回的跳转
  • B:是无返回的跳转

​ 存了LR也就意味着可以返回到本方法继续执行。一般用于不同方法直接的调用
​ B相关的跳转没有LR,一般是本方法内的跳转,如while循环,if else等

​ 跳转相关的指令还会有种逻辑运算,就是condition code。配合状态寄存器中的状态标示,以实心点.开头的都是表示条件,如 b.ne ,一般用于 if else 。常见的条件码有以下这些:
在这里插入图片描述

2.3.5 代码实例

​ 我们继续采用之前的示例代码,在64位环境下进行编译:

#include<stdio.h>
void Fun1(){
    printf("Hello World!");
}
int main(){
    int a = 1;
    Fun1();
    return 0;
}
编译指令:
$ gcc -g -z execstack 01.c -o test

​ 查看一下反汇编代码:

; Attributes: bp-based frame fpd=0x20

; int __cdecl main(int argc, const char **argv, const char **envp)
EXPORT main
main

var_20= -0x20
a= -4

STP             X29, X30, [SP,#var_20]!
MOV             X29, SP
MOV             W0, #1
STR             W0, [X29,#0x20+a]
BL              Fun1
MOV             W0, #0
LDP             X29, X30, [SP+0x20+var_20],#0x20
RET
; End of function main

; Attributes: bp-based frame

; void __cdecl Fun1()
EXPORT Fun1
Fun1

var_s0=  0

STP             X29, X30, [SP,#-0x10+var_s0]!
MOV             X29, SP
ADRP            X0, #aHelloWorld@PAGE ; "Hello World!"
ADD             X0, X0, #aHelloWorld@PAGEOFF ; "Hello World!"
BL              .printf
NOP
LDP             X29, X30, [SP+var_s0],#0x10
RET
; End of function Fun1

让我们和之前32位环境下对比一下:

; Attributes: bp-based frame fpd=8
; int __cdecl main(int argc, const char **argv, const char **envp)
EXPORT main
main

a = -4

PUSH            {R11,LR}
SUB             SP, SP, #8
ADD             R11, SP, #0
MOVS            R3, #1
STR             R3, [R11,#8+a]
BL              Fun1
MOVS            R3, #0
MOV             R0, R3
ADDS            R11, #8
MOV             SP, R11
POP             {R11,PC}
; End of function main

; Attributes: bp-based frame
; void Fun1()
EXPORT Fun1
Fun1
PUSH            {R11,LR}
ADD             R11, SP, #0
MOV             R0, #aHelloWorld ; format
BLX             printf
NOP
POP             {R11,PC}
; End of function Fun1

一开始也是一样的

STP             X29, X30, [SP,#var_20]!	;保存FP、LR寄存器,在堆栈上分配一些缓冲区,同时会为栈帧分配空间
MOV             X29, SP					;设置堆栈框架的底部
  • FP(x29) :保存栈帧地址(栈底指针)
  • LR(x30) :程序链接寄存器,保存子程序结束后需要执行的下一条指令
  • [SP,#var_20]!: 意思是从 sp-#var_20(20)地址取值,取值完后在把 sp-0x20 写回 sp

​ 函数调用的时候也是:

MOV             R0, #aHelloWorld ; format
BLX             printf

​ 函数推出的时候也是一样的:

ADDS            R11, #8		;重新调整堆栈指针
MOV             SP, R11		;重新调整堆栈指针
POP             {R11,PC}	;从堆栈指针中恢复帧指针,通过直接加载到PC

三、环境配置

​ 这里我本人是在真实的ARM机器上进行调试,但是考虑到绝大多数朋友都是在使用QEMU环境下进行调试,故这一部分提供两种环境下的调试环境配置

3.1 真机环境配置

我们需要以下两个软件:

  • pwntools:用来编写EXP
  • GEF:调试插件(ARM环境推荐使用GEF,PWN-DBG有些问题)
3.1.1 更改Python源

​ 为了提高安装速度,强烈建议将Python源更换为国内的镜像源,下面以清华源为例子

​ 升级 pip 到最新的版本 (>=10.0.0) 后进行配置:

pip install pip -U
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

​ 如果您到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U
3.1.2 安装pwntools

​ 根据Github上的官方教程:

apt-get update
apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools
3.1.3 安装GEF

​ 因为总所周知的原因,Github上的RAW下载被墙了,国内的机器没办法直接使用Github上的官方教程就行安装

​ 我的做法是首先通过本地从Github中下载ZIP包,上传到服务器,然后解压代码包,关键是其中的gef.py

​ 然后我接下来就是按照官方教程一般将下载获得的gef.py改名放入到工作目录中,然后将其添加到.gdbinit文件中:

cp gef.py ~/.gdbinit-gef.py
echo source ~/.gdbinit-gef.py >> ~/.gdbinit

​ 然后运行GDB进行测试,会发现提示这些信息:

GEF for linux ready, type `gef' to start, `gef config' to configure
88 commands loaded for GDB 8.1.1 using Python engine 3.6
[*] 3 commands could not be loaded, run `gef missing` to know why.

​ 提示缺少一些模块,这时候我们需要查看缺少哪些模块

gef➤  gef missing 
[*] Command `set-permission` is missing, reason  →  Missing `keystone-engine` package, install with: `pip install keystone-engine`.
[*] Command `ropper` is missing, reason  →  Missing `ropper` package for Python, install with: `pip install ropper`.
[*] Command `assemble` is missing, reason  →  Missing `keystone-engine` package for Python, install with: `pip install keystone-engine`.

​ 根据提示将缺少的包手动安装即可,然后重启GDB即可

3.1.4 安装32位编译环境
3.1.4.1 下载交叉编译工具链

​ 首先访问ARM官方提供的工具链环境,配置交叉编译器

https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads

​ 然后下滑到AArch64 Linux hosted cross compilers,因为我们的服务器是AARCH64架构的,所以就选择这个工具链包即可,下面直接放一下下载链接,我们要下载的32位是基于AArch32 target with hard float (arm-none-linux-gnueabihf)

https://developer.arm.com/-/media/Files/downloads/gnu-a/10.2-2020.11/binrel/gcc-arm-10.2-2020.11-aarch64-arm-none-linux-gnueabihf.tar.xz?revision=26cbedef-a2fa-4687-b6bb-2fefc0d3c5d6&la=en&hash=9137A71DC9572E22A40BE62B1BF0C5800EB52E6A

​ 这里简单介绍以下ARM编译器工具链的情况,从授权上,分为免费授权版和付费授权版。免费版目前有三大主流工具商提供,第一是GNU(提供源码,自行编译制作),第二是 Codesourcery,第三是Linora

  • arm-none-linux-gnueabi-gcc:是 Codesourcery 公司(目前已经被Mentor收购)基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM(32位)系统中所有环节的代码,包括裸机程序、u-boot、Linux kernel、filesystem和App应用程序
  • arm-linux-gnueabihf-gcc:是由 Linaro 公司基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM(32位)系统中所有环节的代码,包括裸机程序、u-boot、Linux kernel、filesystem和App应用程序
  • aarch64-linux-gnu-gcc:是由 Linaro 公司基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARMv8 64位目标中的裸机程序、u-boot、Linux kernel、filesystem和App应用程序
  • arm-none-elf-gcc:是 Codesourcery 公司(目前已经被Mentor收购)基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM MCU(32位)芯片,如ARM7、ARM9、Cortex-M/R芯片程序
  • arm-none-eabi-gcc:是 GNU 推出的的ARM交叉编译工具。可用于交叉编译ARM MCU(32位)芯片,如ARM7、ARM9、Cortex-M/R芯片程序

​ 交叉编译工具链的命名规则为:arch [-vendor] [-os] [-(gnu)eabi]

  • arch - 体系架构,如ARM,MIPS
  • vendor - 工具链提供商
  • os - 目标操作系统
  • eabi - 嵌入式应用二进制接口(Embedded Application Binary Interface)

​ 根据对操作系统的支持与否,ARM GCC可分为支持和不支持操作系统,如:

  • arm-none-eabi:这个是没有操作系统的,自然不可能支持那些跟操作系统关系密切的函数,比如fork(2)。他使用的是newlib这个专用于嵌入式系统的C库。
  • arm-none-linux-eabi:用于Linux的,使用Glibc

ARM64为了保证前向兼容,提供了一个 32 位的兼容模式,所以我们用 arm-linux-gnueabi-gcc 编译的应用程序也是可以直接在ARM64 的系统上运行的,但是 Linux Kernel 和 U-Boot 就不行,除非你提前把 CPU 切换到 32 位模式

3.1.4.2 解压配置环境变量

解压:

$ xz -d gcc-arm-10.2-2020.11-aarch64-arm-none-linux-gnueabihf.tar.xz
$ tar -xvf gcc-arm-10.2-2020.11-aarch64-arm-none-linux-gnueabihf.tar

添加环境变量:

$ vim /etc/bash.bashrc

填入以下语句,具体路径由你自己的路径决定:

export PATH=$PATH:/root/Download/gcc-arm-10.2-2020.11-aarch64-arm-none-linux-gnueabihf/bin

使环境变量生效:

$ source /etc/profile

检查是否将路径加入到PATH:

$ echo $PATH

查看环境是否搭建成功:

root@ecs-kc1-large-2-linux-20201025105424:~# arm-none-linux-gnueabihf-gcc -v
Using built-in specs.
COLLECT_GCC=arm-none-linux-gnueabihf-gcc
COLLECT_LTO_WRAPPER=/root/Download/gcc-arm-10.2-2020.11-aarch64-arm-none-linux-gnueabihf/bin/../libexec/gcc/arm-none-linux-gnueabihf/10.2.1/lto-wrapper
Target: arm-none-linux-gnueabihf
Configured with: /tmp/dgboter/bbs/dsggnu-softiron-7--aarch64/buildbot/aarch64-none-linux-gnu--arm-none-linux-gnueabihf/build/src/gcc/configure --target=arm-none-linux-gnueabihf --prefix= --with-sysroot=/arm-none-linux-gnueabihf/libc --with-build-sysroot=/tmp/dgboter/bbs/dsggnu-softiron-7--aarch64/buildbot/aarch64-none-linux-gnu--arm-none-linux-gnueabihf/build/build-arm-none-linux-gnueabihf/install//arm-none-linux-gnueabihf/libc --with-bugurl=https://bugs.linaro.org/ --enable-gnu-indirect-function --enable-shared --disable-libssp --disable-libmudflap --enable-checking=release --enable-languages=c,c++,fortran --with-gmp=/tmp/dgboter/bbs/dsggnu-softiron-7--aarch64/buildbot/aarch64-none-linux-gnu--arm-none-linux-gnueabihf/build/build-arm-none-linux-gnueabihf/host-tools --with-mpfr=/tmp/dgboter/bbs/dsggnu-softiron-7--aarch64/buildbot/aarch64-none-linux-gnu--arm-none-linux-gnueabihf/build/build-arm-none-linux-gnueabihf/host-tools --with-mpc=/tmp/dgboter/bbs/dsggnu-softiron-7--aarch64/buildbot/aarch64-none-linux-gnu--arm-none-linux-gnueabihf/build/build-arm-none-linux-gnueabihf/host-tools --with-isl=/tmp/dgboter/bbs/dsggnu-softiron-7--aarch64/buildbot/aarch64-none-linux-gnu--arm-none-linux-gnueabihf/build/build-arm-none-linux-gnueabihf/host-tools --with-arch=armv7-a --with-fpu=neon --with-float=hard --with-mode=thumb --with-arch=armv7-a --with-pkgversion='GNU Toolchain for the A-profile Architecture 10.2-2020.11 (arm-10.16)'
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 10.2.1 20201103 (GNU Toolchain for the A-profile Architecture 10.2-2020.11 (arm-10.16)) 

3.2 QEMU环境配置

​ 这里主要提供使用QEMU模拟配合gdb-multiarch的remote功能进行调试,关于编译环境还请读者自行查阅如何配置交叉编译

3.2.1 安装QEMU

​ 我们对版本的要求不是很严格, 直接通过 apt 等包管理安装

$sudo apt install qemu-system
$sudo apt install qemu-user

​ 此时已经可以去运行静态链接的ARM架构的程序了,会自动调用对应架构的QEMU。但是如果是动态链接的程序,QEMU找不到对应的动态链接库,就会报错,从而无法运行

​ 这就需要我们安装对应架构的共享库,使用apt来搜索一下

apt search "libc6-" | grep "AARCH64"

​ 然后 apt 安装对应的共享库就可以了,动态链接程序用QEMU运行需要指定动态链接库的路径

qemu-aarch64 -L /usr/aarch64-linux-gnu ./test_arm

配置qemu-system网络:

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

$ sudo apt install uml-utilities bridge-utils
1

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

$ sudo vim /etc/network/interfaces
3.2.2 安装gdb-multiarch

​ 比较简单的方法就是通过apt进行安装

apt install gdb-multiarch
3.2.3 进行调试

可以使用 qemu 的 -g 指定端口

qemu-aarch64 -g 1235 -L /usr/aarch64-linux-gnu ./test_arm

使用gdb-multiarch的remote功能进行调试

pwndbg> target remote localhost:1235

参考文章

本文参考了以下文章,感谢以下文章作者的辛苦付出,在此表示感谢

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐