ARM简介

ARM vs Intel

Intel和ARM之间主要区别在于指令集。Intel是一种CISC(复杂指令集计算)处理器,具有更大、功能更丰富的指令集,并允许多种复杂指令访问内存。因此与ARM相比,它具有更多的操作、寻址模式,但寄存器更少。CISC 处理器主要用于普通 PC、工作站和服务器。

ARM 是RISC(精简指令集计算)处理器,具有更简化的指令集和更通用的寄存器。ARM是一种加载/存储架构 ,这意味着没有数据处理指令直接对内存中的数据进行操作。数据必须首先被加载到寄存器中,修改,然后存储到内存中。

精简指令集有其优点和缺点。优点之一是指令可以更快地执行,从而可能实现更高的速度。缺点是,更少的指令意味着更加强调使用有限的可用指令来高效编写软件。

另一些区别:

  • 在 ARM 中,大多数指令都可用于条件执行。
  • Intel x86 和 x86-64 系列处理器使用 little-endian 格式
  • ARMv3之前,使用little-endian,之后,全部使用big-endian,并允许切换字节序。

不仅ARM和Intel之间存在差异,而且不同的ARM版本也存在差异。不同版本的命名规则也很乱,可以参考https://en.wikichip.org/wiki/arm/versions

汇编语言

如果希望构建自己的 ARM shellcode、制作 ARM ROP 链和调试 ARM 应用程序,则需要理解ARM汇编中的一些关键点。

汇编语言是一种比机器代码更易于人类阅读和编写的编程语言。它提供了一种方式,让我们可以用更接近人类语言的符号来编写程序,而不是直接使用计算机理解的二进制形式的机器代码。尽管汇编语言比机器代码更易读,但计算机本身不能直接执行汇编代码,需要将其转换为机器代码。这个转换过程通常由汇编器完成,如GNU Binutils项目中的GNU汇编器,它能够处理以".s"为扩展名的源文件,将汇编代码转换成计算机可以执行的机器代码。

而高级语言又在汇编语言的基础上,进一步抽象化编程过程,使得开发者能够快速构建复杂的应用程序,同时保持代码的可读性和可维护性。一个高级语言编写的程序转换为机器指令的过程如下图:

汇编之下

计算机的基础是电路,而电路的核心是电信号,信号通过电压的变化来传递,可以将电压设置为两个状态:0伏(表示“关闭”)或5伏(表示“开启”)。为了直观地表示这些电压变化,使用0和1这两个数字来代表电压的高低状态。这是一种非常基础的数学系统,叫做二进制系统,它只使用两个数字:0和1。

将这些0和1的序列进行分组,从而形成一个机器指令,机器指令时计算机CPU的最小工作单元,一个机器指令示例如下:

1110 0001 1010 0000 0010 0000 0000 0001

仅记忆0和1的序列,很难记得每个机器指令的意思,此时汇编语言产出,使用助记符来代码这些01序列,即机器指令,其中每个机器指令都有一个名称,通常由三个字母组成,接下来就可以使用这些助记符作为指令编写程序,写出来的程序称之为汇编程序。用于表示机器指令的助记符的集合称之为汇编语言。因此,汇编语言是人类在计算机进行编程的最底层。一个用助记符写的汇编代码如下:

	pushq	%rbp
	movq	%rsp, %rbp
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	call	puts@PLT
	movl	$0, %eax
	popq	%rbp

Reference

ARM assembler in Raspberry Pi.

Practical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation by Bruce Dang, Alexandre Gazet, Elias Bachaalany and Sebastien Josse.

ARM Assembler User Guide

数据类型和寄存器

数据类型

与高级语言类似,ARM支持对不同数据类型操作。

可以加载或存储的数据类型可以是有符号或无符号的一个字、半字、或字节。这三种数据类型的扩展名为:-h 或 -sh 表示半字,-b 或 -sb 表示字节,字没有扩展名。

将数据类型与“加载和存储”指令一起使用:

ldr = Load Word     # ldr为指令
ldrh = Load unsigned Half Word    #ldr指令后跟一个h
ldrsh = Load signed Half Word     #ldr指令后跟一个sh
ldrb = Load unsigned Byte         #ldr指令后跟一个b
ldrsb = Load signed Bytes

str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte

字节序

内存中数据存储的两种方式:小端序(LE)和大端序(BE)。Intel x86这种小端序机器上,最低有效字节存储在内存的最低地址上;在大端序机器上,最高有效字节存储在最低地址。ARM架构的计算机在版本3之前是固定使用小端序的。但从ARMv6版本开始,它变得灵活了,可以根据需要在小端序和大端序之间切换。这种切换是通过程序状态寄存器(CPSR)中的一个特殊位(E位)来控制的。这意味着ARM计算机可以根据程序的需要来决定数据的存储方式,无论是小端序还是大端序。

寄存器

寄存器的数量取决于ARM版本,根据ARM参考手册,除了基于ARMv6-M 和ARMv7-M的处理器外,其他处理器有30个通用32位寄存器。前16个寄存器可在用户级模式下访问,其他寄存器可在特权软件(内核)执行中使用,接下来的介绍,只使用前16个用户级模式下可访问的寄存器:r0-15。这 16 个寄存器可分为两组:通用寄存器和特殊用途寄存器。

寄存器别名 alias用途X86对应寄存器
R0一般用途EAX
R1一般用途EBX, ECX, EDX, ESI, EDIEBX、ECX、EDX、ESI、EDI
R2一般用途EBX, ECX, EDX, ESI, EDIEBX、ECX、EDX、ESI、EDI
R3一般用途EBX, ECX, EDX, ESI, EDIEBX、ECX、EDX、ESI、EDI
R4一般用途EBX, ECX, EDX, ESI, EDIEBX、ECX、EDX、ESI、EDI
R5一般用途EBX, ECX, EDX, ESI, EDIEBX、ECX、EDX、ESI、EDI
R6一般用途-
R7保存系统呼叫号码-
R8一般用途-
R9一般用途-
R10一般用途-
R11FP帧指针EBP
R12IP程序内调用-
R13SP堆栈指针ESP
R14LR链接寄存器-
R15PC程序计数器EIP
R16-当前计划状态寄存器EFLAGS

R0-R12: 在常用操作中用于存储临时值、指针等。例如,R0既可以在算术运算期间称为累加器,也可以用于存储先前调用的函数的结果。R7 存储了系统调用编号,因此在处理系统调用时变得很有用。而 R11作为帧指针,可以跟踪堆栈上的边界。此外,当调用一个函数时,它的前四个参数会先被放入r0、r1、r2和r3这四个寄存器中,然后函数通过读取这些寄存器来获取传入的参数值

R13: SP (堆栈指针):堆栈是一种特殊的内存区域,主要用于存储函数运行时需要的临时数据。当你调用一个函数时,计算机会在这个区域为这个函数分配一些空间来存储数据,比如函数的参数、局部变量和返回地址。当你的函数执行完毕并返回时,之前为这个函数分配的堆栈空间就会被释放,也就是“回收”。这样做是为了重新利用这些空间,为其他函数调用提供存储空间。堆栈指针是一个特殊的寄存器,它总是指向堆栈的顶部。当需要在堆栈上分配空间时,计算机会通过修改堆栈指针来实现。当你需要在堆栈上存储数据时,比如一个32位的值,计算机会从堆栈指针的当前位置减去相应的字节数(在这个例子中是4字节)。这样做会更新堆栈指针的位置,从而为这个值分配空间。这个过程是自动的,不需要程序员手动管理。

R14:LR(链路寄存器):当你调用一个函数时,链路寄存器会被更新为一个特殊的内存地址。这个地址实际上指向了调用函数的下一条指令,也就是你调用函数后应该继续执行的地方。假设你有一个主函数,它调用了一个子函数。在子函数执行的过程中,链路寄存器保存了主函数中紧随函数调用之后的那条指令的地址。当子函数完成它的任务后,程序会通过读取链路寄存器中的地址,来知道接下来应该执行哪条指令。这样,程序就可以顺利地从子函数返回到主函数,继续之前的工作。

R15:PC(程序计数器)。程序计数器(PC)是计算机中的一个特殊寄存器,它的作用是跟踪当前正在执行的指令的位置。你可以把它想象成阅读时的手指,它总是指向你接下来要阅读的单词。在ARM架构的处理器中,程序计数器的行为会根据处理器的工作模式而有所不同。ARM架构有两种主要的工作模式:ARM状态和THUMB模式。ARM状态是处理器的标准工作模式。在这个状态下,每条指令都是4个字节大小。因此,每当处理器执行一条指令后,程序计数器会自动增加4,指向下一条指令的位置。THUMB模式是一种更紧凑的指令集,每条指令只有2个字节大小。在这种模式下,程序计数器在执行完一条指令后会增加2,指向下一条指令。当处理器执行一个分支指令,也就是告诉它跳转到程序的另一个部分时,程序计数器会直接更新为这个新位置的地址。

这里有一个特别的地方需要注意:在ARM状态中,程序计数器在执行当前指令后,会预先增加8(也就是两条ARM指令的大小),这样做是为了准备下下条指令的地址。而在THUMB模式(对于v1版本)中,程序计数器在执行当前指令后,会预先增加4(也就是两条THUMB指令的大小)。这与x86架构的处理器不同,在x86中,程序计数器始终只是简单地指向下一条要执行的指令,而不会预先增加额外的量。

当前程序状态寄存器

使用gdb调试ARM二进制文件时,会看到一个名为 Flags 的东西:

寄存器$cpsr显示是ARM处理器中的一个特殊部件,它的全名是“当前程序状态寄存器”。它记录了处理器在任何时刻的状态和一些重要的信息。

上图是32 位寄存器 (CPSR) 的布局,其中左侧保存最高有效位,右侧保存最低有效位。每个单元格(除了 GE 和 M 部分以及空白单元格)的大小都是一位。这些一位部分定义了程序当前状态的各种属性。

Flag说明
N如果指令结果产生负数,则启用
Z如果指令结果产生零值则启用
C如果指令结果产生需要完全表示第 33 位的值,则启用
V如果指令结果产生无法用 32 位二进制补码表示的值,则启用
EARM 可以以小端或大端运行。对于小端模式,该位设置为 0,对于大端模式,该位设置为 1
T如果处于 Thumb 状态,则该位被设置;当处于 ARM 状态时,该位被禁用
M这些位指定当前特权模式(USR、SVC 等)
J第三种执行状态,允许某些 ARM 处理器在硬件中执行 Java 字节码

假设现在比较寄存器r1和r0,其中 r1 = 4 且 r0 = 2, 执行cmp r1, r0 操作后标志的样子:

ARM和THUMB

ARM 处理器有两种主要的运行状态,ARMThumb,这两种状态和处理器的权限级别没有关系,也就是说,不管是在用户模式还是系统模式下,代码都可能是ARM或Thumb状态。在ARM状态下,处理器使用的是标准的32位ARM指令集。这意味着每条指令都是32位,可以执行复杂的操作。

Thumb状态是ARM处理器的一种16位指令集,设计用来节省内存空间。虽然Thumb指令通常是16位宽,但也可以是32位,取决于具体的指令和操作。Thumb状态下的指令集是ARM状态的子集。以根据需要在这两种状态之间切换。例如,在编写需要嵌入到其他程序中的小型代码片段(如shellcode)时,使用Thumb状态可以更高效地利用内存。

ARMARM公司在不同时间推出了不同版本的Thumb指令集。每个版本可能支持不同的ARM和Thumb指令集变体。并不是所有版本的ARM都支持相同的Thumb指令集。研究的时候具体了解目标设备使用的ARM版本,以及这个版本特别支持的Thumb指令集就可以了。详细信息可以在ARM信息中心查询。

ARM指令简介

前期先了解汇编语言的最小部分如何运行、它们如何相互连接以及通过组合它们可以实现什么目标就可以了,后续在研究过程中逐渐学习即可。

汇编指令模板如下:

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

每个字段的含义如下:

  • MNEMONIC: 助记符,指令名,例如ADD(加法),MOV(移动)
  • {S}: 可选,如果指令中包含S,则表示在执行操作后,处理器的状态标志(如零标志、负数标志、进位标志等)将根据操作结果进行更新。
  • {condition}:可选,条件表达式,指定了指令执行所需的条件。ARM指令可以在满足特定条件时才执行。
  • {Rd}:这是指令执行结果的目标寄存器。指令执行的结果(比如加法操作的和)将被存储在这个寄存器中。Rd代表destination Register(目的寄存器)
  • Operand1:指令的第一个操作数。它可以是一个寄存器的名称或一个立即数(直接给出的数值)
  • Operand2:第二个操作数,可以是一个立即数,或者是一个带有可选移位的寄存器。

几个简单的指令如下:

  • ADD R0, R1, R2:将寄存器R1R2中的内容相加,并将结果存储在寄存器R0

  • ADD R0, R1, #2 : 将寄存器R1中的内容与立即数2相加,并将结果存储在寄存器R0

  • MOV R0, R1, LSL #1:将寄存器R1中的内容逻辑左移一位,然后将结果移动到寄存器R0

  • CMP R0, R1CMP(比较)指令会将寄存器R0R1的值进行比较,根据比较的结果,改变$CPSR中的值

    • 如果R2小于R3,CPSR中的N(负数)标志会被设置为1,表示结果为负
    • 如果R2等于R3,则CPSR中的Z(零)标志会被设置为1,表示结果为零
  • MOVLE R0, #5MOVLE是一种特殊的移动(move)指令,全称是"Move if Less Than or Equal",意思是“如果小于或等于,就移动”,此时会检查上一条指令比较的结果。是否满足“小于等于”,如果满足,那么数字5就会被放入寄存器R0中,如果不满足,R0中的值就不会改变。

一些基本的指令如下表

指令描述指令描述
MOV移动数据EOR按位异或
MVN将一个数取反并移动LDR加载,从内存中读取数据到寄存器
ADD加法STR存储,将数据从寄存器存储到内存
SUB减法LDM加载多个
MUL乘法STM存储多个
LSL逻辑左移PUSH压栈
LSR逻辑右移POP弹出堆栈
ASR算术右移B分支,根据条件,跳转到程序的另一个位置
ROR右旋BL执行一个子程序调用,并将返回地址存储在寄存器中
CMP比较BX跳转到另一个寄存器指向的地址,并可以交换处理器模式
AND按位与BLX用于调用子程序,并将链接寄存器更新为子程序的返回地址,同时可以改变处理器模式
ORR按位或SWI/SVC系统调用
  1. arm32环境

RASPBERRY PI ON QEMU

在虚拟机 Ubuntu22.04.1中进行QEMU下的ARM环境搭建

  1. Raspbian 镜像:http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-04-10/

  2. 对应优化后的的QEMU内核:https://github.com/dhruvvyas90/qemu-rpi-kernel/blob/master/kernel-qemu-4.4.34-jessie

创建文件夹,将下载的Raspbian 镜像,以及qemu-kernel放置其中

并下载qemu-system

解压后,进行分区调整

从中可以看出该镜像文件共有两个分区,特别注意第二个分区的起点扇区,每个扇区是512个字节,由此算出第二分区起点位置是512 * 92160 = 47185920 字节。

接下载我们把镜像文件挂载到一个目录下面,这样就可以通过这个目录访问这个镜像文件里面的内容了,在解压后的镜像文件目录下执行命令

使用#注释掉文件里面的所有内容, Ctrl-x --» Y 保存并退出。


修改/mnt/raspbian/etc/fstab

卸载镜像

sudo umount /mnt/raspbian

写一个qemu启动脚本

gedit qemu_run.sh

#!/bin/bash
sudo qemu-system-arm \
-kernel kernel-qemu-4.4.34-jessie \
-append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" \
-hda  2017-04-10-raspbian-jessie.img \
-vga std \
-cpu arm1176 -m 256 \
-M versatilepb \
-no-reboot \
-serial stdio \
-net nic \
-net user,hostfwd=::1234-:22 \

给脚本加上可执行权限,启动系统

chmod +x qemu_run.sh
./qemu_run.sh

进入终端,启动SSH服务

在Ubuntu上远程访问(默认用户名pi,默认密码raspberry)

ssh pi@127.0.0.1 -p 1234

配置网络

上面为了方便ssh连接,将树莓派的22端口映射到Ubuntu宿主机的1234端口

在Ubuntu上创建共享网络接口(tap0)

kka@kka-ubuntu:~$ sudo apt-get install uml-utilities
kka@kka-ubuntu:~$ sudo tunctl -t tap0 -u kka
Set 'tap0' persistent and owned by uid 1000
kka@kka-ubuntu:~$ sudo ifconfig tap0 172.16.0.1/24

完成配置后,查看tap0接口

kka@kka-ubuntu:~$ ifconfig tap0
tap0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.16.0.1  netmask 255.255.255.0  broadcast 172.16.0.255
        ether 6e:c0:7b:81:7a:0a  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

重新启动QEMU

sudo qemu-system-arm -kernel kernel-qemu-4.4.34-jessie -cpu arm1176 -m 256 -M versatilepb -serial stdio -append "root=/dev/sda2 rootfstype=ext4 rw" -hda 2017-04-10-raspbian-jessie.img -net nic -net tap,ifname=tap0,script=no,downscript=no -no-reboot

然后就可以从Ubuntu访问raspbian开放的端口,使用nc进行测试