对于应用层开发人员而言,仅仅掌握Objective-C和系统框架便可较好的完成开发,但在涉及到应用加固、逆向分析等内容时仅有应用层开发技能就会显得很是的无力,所以掌握汇编对于突破iOS开发水平的瓶颈十分有效。html
以反调试为例,咱们知道,经过调用ptrace函数能够阻止调试器依附。ios
ptrace(31, 0, 0, 0)
复制代码
这种方式可以被函数hook轻易破解,例如使用facebook的fishhook。 为了防止函数被hook,咱们能够将函数调用转为经过汇编发起系统调用,即便用下面的代码。git
mov x0, #31
mov x1, #0
mov x2, #0
mov x3, #0
mov x16, #26
svc #0x80
复制代码
其中x0-x3存储的为函数入参,x16存储的为函数编号,经过Apple提供的System Call Table 能够查出ptrace的编号为26,最后一句指令发起了系统调用。 经过使用__asm__指令可以将汇编代码嵌入咱们的函数中,构成反调试方法。github
// 使用inline方式将函数在调用处强制展开,防止被hook和追踪符号
static __attribute__((always_inline)) void anti_debug() {
// 判断是不是ARM64处理器指令集
#ifdef __arm64__
// volatile修饰符可以防止汇编指令被编译器忽略
__asm__ __volatile__(
"mov x0, #31\n"
"mov x1, #0\n"
"mov x2, #0\n"
"mov x3, #0\n"
"mov x16, #26\n"
"svc #0x80\n"
);
#endif
}
复制代码
虽然上面的反调试机制并不完善,可是比直接调用ptrace要好上不少倍,从这一点来看,掌握汇编技能对于iOS应用安全和底层研究很是有利。shell
iOS设备主要使用的为ARM64汇编,所以本文主要介绍ARM64汇编的入门技巧。 汇编入门最难的地方在于对栈的理解,汇编的全部指令操做都是围绕栈实现的,在汇编中,没有变量的概念,只有寄存器和内存。安全
汇编中的栈是由高地址向低地址生长的数据结构,sp指针永远指向栈顶,须要记住的是,在某位置进行存储时,是向高地址进行的,下面以一个简单的例子讲解汇编的栈操做。 咱们以一段简单的C代码为例。数据结构
// hello.c
#include <stdio.h>
int test(int a, int b) {
int res = a + b;
return res;
}
int main() {
int res = test(1, 2);
return 0;
}
复制代码
使用clang能够将其编译为特定指令集的汇编代码,这里咱们将其编译为ARM64指令集的汇编代码。框架
clang -S -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` hello.c
复制代码
完整的汇编代码以下。iphone
.section __TEXT,__text,regular,pure_instructions
.ios_version_min 11, 2
.globl _test
.p2align 2
_test: ; @test
; BB#0:
sub sp, sp, #16 ; =16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
.globl _main
.p2align 2
_main: ; @main
; BB#0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 8-byte Folded Spill
add x29, sp, #16 ; =16
orr w0, wzr, #0x1
orr w1, wzr, #0x2
stur wzr, [x29, #-4]
bl _test
mov w1, #0
str w0, [sp, #8]
mov x0, x1
ldp x29, x30, [sp, #16] ; 8-byte Folded Reload
add sp, sp, #32 ; =32
ret
.subsections_via_symbols
复制代码
本节咱们只讨论栈操做,所以忽略main函数和printf调用部分,咱们只看对test函数的调用,节选这一段汇编代码以下。jsp
sub sp, sp, #16 ; =16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
复制代码
首先介绍一下基本指令和指令的学习方式,要查询某个指令如何使用,最好的方式是去查询ARM公司提供的官方文档,在官方文档页面能够直接搜索指令并查看用法和例程,本文会简单讲解上面的汇编代码中出现的指令。
sub用于对寄存器实施减法,sub a, b, c
等价于a = b - c
,在ARM汇编中,目的操做数通常出现最前方,例如mov ra, rb
表明将rb寄存器的值复制到ra寄存器。add和sub同理,只是将减法变成了加法。
str和ldr是一对指令,str的全称是store register,即将寄存器的值存储到内存中,ldr的全称是load register,即将内存中的值读到寄存器,所以他们的第一个参数都是寄存器,第二个参数都是内存地址。[sp, #12]
表明sp+12
这个地址,同理[sp, #-12]
表明sp-12
这个地址。注意这里的数字都是以字节为单位的偏移量,以str w0, [sp, #12]
为例,w是4字节的寄存器,这个指令表明将w0寄存器的值存储在sp+12这个地址上,因为w0有4个字节,因此存储后会占据sp+12~sp+16
这个内存区域。
下面将分段讲解这段汇编代码,在编译器生成汇编时,首先会计算须要的栈空间大小,并利用sp指针向低地址开辟相应的空间,咱们再来看一下test函数。
int test(int a, int b) {
int res = a + b;
return res;
}
复制代码
这里涉及了3个int变量,分别是a、b、res,int变量占据4个字节,所以一共须要12个字节,但ARM64汇编为了提升访问效率要求按照16字节进行对齐,所以须要16byte的空间,也就是须要在栈上开辟16字节的空间,咱们来看汇编的第一句,正是将sp指针下移16字节。
sub sp, sp, #16
复制代码
sp下移16后,留下了4个4字节的内存空格,共计16字节,咱们继续看下面的句子。
str w0, [sp, #12]
str w1, [sp, #8]
复制代码
这两句的含义是将w0存储在sp+12的格子中,w1存储在sp+8的格子中,上面的例子中提到 x0, x1等寄存器将顺序存放函数的入参,x0和w0是同一个寄存器的不一样尺寸形式,x0为8字节,w0为x0的前4个字节,所以w0是函数的第一个入参a,w1是函数的第二个入参b,因为存储是从低地址到高地址的,因此a将占据sp+12~sp+16
,同理b将占据sp+8~sp+12
,则栈的结构变为下图。
按照“上帝视角”,接下来test函数应该将a和b相加,须要注意的是,只有寄存器才能参与运算,所以接下来的汇编代码又将变量的值从内存中读出,进行相加运算。
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
复制代码
因而可知先存储再读取后运算实际上是多余的,这是没有进行编译优化的结果,学习不进行编译优化的汇编更能让咱们理解其工做机制。
接下来的代码将w0存入了sp+4,也就是res变量的内存区域。
str w0, [sp, #4]
复制代码
接下来就要进行返回了,在例子中咱们提到,函数的返回值通常存储在x0寄存器中返回,所以咱们须要将res的值载入x0寄存器。
ldr w0, [sp, #4]
复制代码
这里之因此使用w寄存器,是由于int为4字节,这也就是类型转换时带来信息丢失的缘由,例如从long到int的转换就相似于将x寄存器的值以w的形式进行存储。最后的代码为将栈还原,并返回到函数调用处继续向下执行。
add sp, sp, #16
ret
复制代码
显然,通过这样的操做,栈被彻底还原到了函数调用之前的样子,须要注意的细节是,栈空间中的内存单元并未被清空,这也就致使下一次使用低地址的栈时,未初始化单元的值是不肯定的,这也就是局部变量不初始化值随机的根本缘由。
经过上面的例子,咱们对栈有了基本的认识,汇编的操做基本都是对栈进行的,只要理解了栈机制,只须要学习各类指令,便可掌握足够使用的汇编技能。
在了解了栈之后,就能够看一些较为复杂的汇编片断来进行学习了,初级阶段能够尝试看着函数写汇编代码,高级阶段要求可以看着汇编还原成函数逻辑,本文仅仅介绍入门基础,下面推荐一些大牛的博客供你们深刻学习汇编技能。
1.知兵的知乎专栏
掌握ARM汇编可以帮助开发者更好地了解编译器和CPU的工做原理,除了可以指导编码外,还可以扩宽视野,经过反编译分析一些闭源代码的逻辑或是进行一些安全加固,所以在汇编上付出时间是十分值得的。
2.ARM官方文档
3.反调试和绕过