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支持对不同数据类型操作。
可以加载或存储的数据类型可以是有符号或无符号的一个字、半字、或字节。这三种数据类型的扩展名为:-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 | – | 一般用途 | - |
R11 | FP | 帧指针 | EBP |
R12 | IP | 程序内调用 | - |
R13 | SP | 堆栈指针 | ESP |
R14 | LR | 链接寄存器 | - |
R15 | PC | 程序计数器 | 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 位二进制补码表示的值,则启用 |
E | ARM 可以以小端或大端运行。对于小端模式,该位设置为 0,对于大端模式,该位设置为 1 |
T | 如果处于 Thumb 状态,则该位被设置;当处于 ARM 状态时,该位被禁用 |
M | 这些位指定当前特权模式(USR、SVC 等) |
J | 第三种执行状态,允许某些 ARM 处理器在硬件中执行 Java 字节码 |
假设现在比较寄存器r1和r0,其中 r1 = 4 且 r0 = 2, 执行cmp r1, r0
操作后标志的样子:
ARM和THUMB
ARM 处理器有两种主要的运行状态,ARM
和 Thumb
,这两种状态和处理器的权限级别没有关系,也就是说,不管是在用户模式还是系统模式下,代码都可能是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
:将寄存器R1
和R2
中的内容相加,并将结果存储在寄存器R0
中 -
ADD R0, R1, #2
: 将寄存器R1
中的内容与立即数2
相加,并将结果存储在寄存器R0
中 -
MOV R0, R1, LSL #1
:将寄存器R1
中的内容逻辑左移一位,然后将结果移动到寄存器R0
中 -
CMP R0, R1
:CMP
(比较)指令会将寄存器R0
和R1
的值进行比较,根据比较的结果,改变$CPSR
中的值- 如果
R2
小于R3
,CPSR中的N(负数)标志会被设置为1,表示结果为负 - 如果
R2
等于R3
,则CPSR中的Z(零)标志会被设置为1,表示结果为零
- 如果
-
MOVLE R0, #5
:MOVLE
是一种特殊的移动(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 | 系统调用 |
- arm32环境
RASPBERRY PI ON QEMU
在虚拟机 Ubuntu22.04.1
中进行QEMU下的ARM环境搭建
-
Raspbian 镜像:http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-04-10/
-
对应优化后的的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进行测试