0%

ARM pwn 进阶知识

之前已经写过一篇关于 ARM 的博客了:ARM pwn 环境搭建+基础入门 | Pwn进你的心 (ywhkkx.github.io)

主要简述了以下各方面的知识:(主要是32位)

ARM 的七种工作模式

  • 用户模式(USR):正常程序执行模式,不能直接切换到其他模式
  • 系统模式(SYS):运行操作系统的特权任务,与用户模式类似,但具有可以直接切换到其他模式等特权
  • 快中断模式(FIQ):支持高速数据传输及通道处理,FIQ异常响应时进入此模式
  • 中断模式(IRQ):用于通用中断处理,IRQ异常响应时进入此模式
  • 管理模式(SVC):操作系统保护模式,系统复位和软件中断响应时进入此模式(由系统调用执行软中断SWI命令触发)
  • 中止模式(ABT):用于支持虚拟内存和/或存储器保护,在ARM7TDMI没有大用处
  • 未定义模式(UND):支持硬件协处理器的软件仿真,未定义指令异常响应时进入此模式

在所有的寄存器中,有些是各模式共用同一个物理寄存器,有些寄存器是各个模式自己拥有独立的物理寄存器

CPSR 和 SPSR 都是程序状态寄存器,其中 SPSR 是用来保存中断前的 CPSR 中的值,以便在中断返回之后恢复处理器程序状态

ARM 的 NEON 寄存器

常规寄存器

32位 64位 别名 目的
R0-R6 X0-X7 一般用途
X8 保存子程序返回值
R7 持有系统调用号
X9-X15 临时寄存器 子程序使用时不需要保存
R8-R10 X19-X28 临时寄存器 子程序使用时必须保存
X18 记录平台信息
R11 X29 FP 帧指针
R12 X16-X17 IP 程序内呼叫
R13 X31 SP 栈指针
R14 X30 LR 链接注册
R15 PC 程序计数器
CPSR CPSR 当前程序状态寄存器
SPSR SPSR 程序状态保存寄存器

NEON 寄存器

ARM NEON 技术本质上是一种高级的单指令多数据(SIMD)架构扩展,这种扩展仅在一些 ARMv7-A 和 ARMv7-R 架构以及 ARMv8 架构上支持:

  • 从 ARMv5 架构开始引入 VFP(vector-floating-point) 指令扩展,可以通过使用短向量指令来加速浮点计算
  • 从 ARMv7 架构开始引入 NEON 技术,NEON 技术同样是依靠向量指令来加速计算,VFP 向量指令加速的模式被弃用,因此 VFP 单元有时也称之为 FPU(Floating Point Unit)单元

NEON 寄存器主要是用来存放包含相同数据类型元素的向量:

  • 在 ARMv7 架构中(32位), 一共有16个128位寄存器,一个128位寄存器又可以分为两个64位寄存器,以此类推
  • 在 ARMv8 架构中(64位),所有寄存器的总数相比 ARMv7 架构翻倍
  • Q 寄存器:128位寄存器
  • D 寄存器:64位寄存器
  • S 寄存器:32位寄存器
  • H 寄存器:16位寄存器
  • B 寄存器:8位寄存器

32位下 NEON 寄存器:

  • 32个S寄存器,S0~S31(单字,32bit)
  • 32个D寄存器,D0~D31(双字,64bit)
  • 16个Q寄存器,Q0~Q15(四字,128bit)

64位下 NEON 寄存器:

  • 32个B寄存器,B0~B31(字节,8bit)
  • 32个H寄存器,H0~H31(半字,16bit)
  • 32个S寄存器,S0~S31(单字,32bit)
  • 32个D寄存器,D0~D31(双字,64bit)
  • 32个Q寄存器,V0~V31(四字,128bit)

ARM 的函数调用

测试案例一如下:(64位:循环+选择)

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [xsp+2Ch] [xbp+2Ch]

for ( i = 0; i <= 9; ++i )
{
if ( (i & 1) == 0 )
puts("a");
}
return 0;
}

从 main 函数开始到 for 循环之间的汇编代码:

1
2
3
4
5
6
7
; __unwind {
STP X29, X30, [SP,#var_30]! /* 将X29,X30写入[SP+var_30] */
MOV X29, SP /* 将SP写入X29 */
STR W0, [SP,#0x30+argc] /* 将W0写入[SP+0x30+argc] */
STR X1, [SP,#0x30+argv]
STR WZR, [SP,#0x30+i]
B loc_794
  • ARM 的 MOV 只支持在寄存器之间转换数据,从内存到 CPU 之间的移动只能通过 LDR/STR 指令来完成
  • STP 相当于两个 STR
  • W0 就是 X0 的低32位(类似于 RAX 和 EAX 之间的关系)
  • ARMv8 有两个0寄存器,分别是 WZR 和 XZR(零寄存器内容为 “0”)
  • B 指令是相对跳转指令,根据当前PC寄存器的值加上偏移来实现跳转

从 for 循环到 if 语句的汇编代码:

1
2
3
4
5
6
7
loc_794
LDR W0, [SP,#0x30+i] /* 读取计数器i */
CMP W0, #9 /* 对比计数器i是否到达目标值 */
B.LE loc_76C /* 判断上面cmp结果是否小于等于,跳转标号 */
MOV W0, #0
LDP X29, X30, [SP+0x30+var_30],#0x30 /* 将[SP+var_30]写回X29,X30 */
RET
  • X0 通常作为计数器(地位相当于 x86 中的 RAX)
  • B,B.LE 这两个跳转构成了循环
1
2
3
4
5
6
7
8
9
10
11
loc_76C
LDR W0, [SP,#0x30+i]
AND W0, W0, #1
CMP W0, #0 /* 执行判断语句 */
B.NE loc_788 /* 判断上面cmp结果是否不等于,跳转标号 */
ADRL X0, aA ; s /* 装载.puts的参数 */
BL .puts /* 转移并连接(调用子程序) */
loc_788
LDR W0, [SP,#0x30+i] /* 从栈中获取i */
ADD W0, W0, #1 /* 更新计数器i */
STR W0, [SP,#0x30+i] /* 把i放回栈中 */
  • B.NE 构成了选择

测试案例二如下:(64位:函数调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [xsp+24h] [xbp+24h]

for ( i = 0; i <= 9; ++i )
{
if ( (i & 1) == 0 )
{
A(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, i + 10, i + 20);
puts("a");
}
}
return 0;
}

函数 A 调用之前:

1
2
3
4
5
6
7
8
9
  0x5500000814 <main+64>     ldr    w7, [sp, #0x2c]
0x5500000818 <main+68> ldr w6, [sp, #0x28]
0x550000081c <main+72> mov w5, #0x60
0x5500000820 <main+76> mov w4, #0x50
0x5500000824 <main+80> mov w3, #0x40
0x5500000828 <main+84> mov w2, #0x30
0x550000082c <main+88> mov w1, #0x20
0x5500000830 <main+92> mov w0, #0x10
0x5500000834 <main+96> bl #A <A>
  • 前6个参数分别放入 X0-X5 中,后续的参数从右往左依次入栈

函数 A 结束之前:

1
2
3
  0x55000007c8 <A+116>       nop    
0x55000007cc <A+120> ldp x29, x30, [sp], #0x40
0x55000007d0 <A+124> ret
1
2
3
pwndbg> telescope 0x5501811f50-0x40
00:00000x5501811f10 —▸ 0x5501811f50 —▸ 0x5501811f80 —▸ 0x5501812090 ◂— 0x0
01:00080x5501811f18 —▸ 0x5500000838 (main+100) ◂— adrp x0, #0x5500000000
  • ldp x29, x30, [sp] 会恢复调用者 main 函数的 X29(FP),X30(LD)
  • 后面的 #0x40 会使 sp+0x40

函数 A 结束之后:

1
2
X29  0x5501811f50 —▸ 0x5501811f80 —▸ 0x5501812090 ◂— 0x0
X30 0x5500000838 (main+100) ◂— adrp x0, #0x5500000000

ARM 的系统调用

先看两个 libc 函数触发 ARM 系统调用的源码:

1
int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....);

32位 arm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ENTRY(__clone)
@ sanity check args
cmp r0, #0 /* 检查第一个参数 */
@ align sp
and r1, r1, #-8 /* 使child_stack对齐 */
ite ne /* If-Then-Else接下来的两条指令条件执行(条件为:不相等) */
cmpne r1, #0 /* If Then如果ne成立,执行cmp,不成立则该条不执行 */
moveq r0, #-EINVAL /* Else:如果eq成立,执行mov,不成立则该条不执行 */
beq PLTJMP(syscall_error) /* 如果eq成立,执行beq,不成立则该条不执行 */

@ insert the args onto the new stack
str r3, [r1, #-4]!
str r0, [r1, #-4]!

@ do the system call
@ get flags
mov r0, r2
mov ip, r2
@ new sp is already in r1
push {r4, r7}
cfi_adjust_cfa_offset (8) /* 发出一个操作码,告诉调试器CFA与假定的CFA(sp)的偏移量为8 */
cfi_rel_offset (r4, 0) /* 告诉调试器R4的原始值可以在新调整的CFA的偏移量0处找到 */
cfi_rel_offset (r7, 4) /* 告诉调试器R7的原始值可以在新调整的CFA的偏移量4处找到 */
ldr r2, [sp, #8]
ldr r3, [sp, #12]
ldr r4, [sp, #16]
ldr r7, =SYS_ify(clone) /* R7负责持有系统调用号 */
swi 0x0 /* 触发异步异常 */
cfi_endproc /* 定义函数结束 */
cmp r0, #0
beq 1f
pop {r4, r7}
blt PLTJMP(C_SYMBOL_NAME(__syscall_error))
RETINSTR(, lr)

cfi_startproc
PSEUDO_END (__clone)
  • 系统调用会引起处理器模式切换 USER 切换到 SVC,而 SVC 下的 SP 和 USER 模式的 SP 是不同的
  • 因此系统调用无法使用栈传入参数,需要将所有的参数通过寄存器 R0-R5 传递

64位 aarch64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ENTRY(__clone)
/* Save args for the child. */
mov x10, x0
mov x11, x2
mov x12, x3

/* Sanity check args. */
mov x0, #-EINVAL
cbz x10, .Lsyscall_error /* 如果X10的值为'0',则跳转Lsyscall_error */
cbz x1, .Lsyscall_error /* 如果X1的值为'0',则跳转Lsyscall_error */

/* Do the system call. */
/* X0:flags, x1:newsp, x2:parenttidptr, x3:newtls, x4:childtid. */
mov x0, x2 /* flags */
/* New sp is already in x1. */
mov x2, x4 /* ptid */
mov x3, x5 /* tls */
mov x4, x6 /* ctid */
mov x8, #SYS_ify(clone)
svc 0x0

cmp x0, #0
beq thread_start
blt .Lsyscall_error
RET
PSEUDO_END (__clone)
  • 参数传递依靠 X0-X6

svc 和 swi 都是 supervisor call 指令,都是系统调用:

  • 在 armv7 之前,用的都是 swi,触发异步异常,进入 vector_swi 异常向量表
    • 当异步异常产生后,程序会抛出一个信号使异常处理程序进入 CPU 的等待队列,然后由 CPU 在合适的时机去运行异常处理程序
  • 在 armv8-arch64 架构下,抛弃 swi 改用 svc,触发的是同步异常,进入同步异常向量表 el1_sync
    • 当同步异常产生后,指令流会立刻进入异常处理流程,CPU 立刻执行异常处理程序
  • 如果不纠结 CPU 的处理过程,就可以把 swi 和 svc 当成是一回事,只是用的向量表不同而已

当通过系统调用进入内核的时候,内核会进行一系列初始化操作,在内核栈上形成如下的用户空间现场:

  • pt_regs:封装了需要在内核入口中保存的最少的状态信息,在系统崩溃时将会提供 debug 信息
  • thread_info:存储当前进程的基本信息,包括进程描述符 task_struct

执行完具体的系统调用代码后,程序会依靠 pt_regs 的数据恢复用户态进程的上下文