0.1. IDA 说明
0.1.1. 字符串窗口(Strings window)
- .data 表示数据在数据段:可以进行读写;
- .rodata 表示在只读数据段:只读,不能进行修改;
- .note
#
开头的一些数据为立即数,表示数值本身,如 #0
。
0.1.2. IDA 调试界面
-
把汇编窗口
C 将当前地址处的数据解析成代码 P
-
十六进制窗口
编辑内存数据和代码
-
寄存器窗口
修改寄存器的值
-
模块窗口
模块路径和地址
- 线程窗口
- 栈窗口
- 输出信息窗口
IDA 调试常用功能:
-
断点和运行
设置断点 F2 设置断点不可用 Disable breakpoint 编辑断点 Edit breakpoint 删除断点 Delete breakpoint 继续运行 F9 查看当前所有断点 Ctrl+Alt+B
-
单步调试
单步进入 F7 单步步过 F8 运行到函数的返回地址 Ctrl + F7 运行到光标处 F4
- IDC 脚本
0.1.3. ARM 汇编指集
- B 无条件跳转
- BL 带链接无条件跳转指令,即调用子函数或子程序,相当于
x86
的call
指令; - MOV 数据传送指令(<–),方向为从右向左,把右边的立即数或者寄存器里面的值放入左边的寄存器里面;
堆栈操作
- STMFD (寄存器和存储器之间的访问) 和 LDMFD 操作多个寄存器,这两个命令针对的是都是堆栈操作;
- STMFD 入栈,方向 <–
- LDMFD 出栈,方向 –>
BEQ
BNE
分析和修改汇编指令:
赋值(MOV)、跳转、算术运算、移位运算、堆栈操作、内存读写指令、函数调用约定
Thumb 16 、Thumb2 32、ARM32 、ARM64
用户模式(usr):
不分组寄存器:R0-R7 分组寄存器:R8-R14
传递参数与返回值:R0-R3 ,如果参数大于四个多余的参数则使用堆栈进行存储(顺序相反,从后向前,即最后的参数是R0)
保存栈顶地址:R13/SP
保存函数的返回地址:R14/LR
程序计数器:R15 (PC)控制程序执行流程
状态寄存器:CPSR (指定当前指令是ARM 指令还是Thumb 指令)
ARM 处理器:
ARM 状态(执行32位对齐指令的 ARM 指令)
Thumb 状态(执行16位对齐的 Thumb 指令)
0.1.4. 工具
-
objdump
objdump -S exec > exec.txt objdump -s exec > exec.txt
-
readelf
ELF 文件结构
Linux ELF 文件;Windows PE 文件; 安卓操作系统内核采用 Linux 内核框架实现,即 Android ELF 文件。
ELF 文件整体结构:
ELF Header -> ELF 文件头的位置是固定的 Segment Header Table -> ELF 程序头描述的是段的相关信息 .init .text .rodata .data .symtab 符号表 .line .strtab 字符串表 section Header Table -> ELF 节头表描述的是节区的信息 动态用段,静态用节
readelf 的使用: -a -h -l -s -e -s
程序在运行的时候只会加载 PT_LOAD
段,其他段不会加载到内存,这会导致一些 so 文件在动态调试的时候抠出来的内存里的东西放到 IDA 里面只能看得到 PT_LOAD
段的信息,其他段看不到,这种情况下就要进行 so 文件的修复(补全其他段的信息)。
iOS APP 证书双向验证机制
参考: https://blog.csdn.net/feibabeibei_beibei/article/details/64126064
0.2. iOS
0.2.1. 汇编基础(部分摘自《iOS 应用逆向与安全之道》,罗巍著)
0.2.1.1. 寄存器
0.2.1.1.1. 通用寄存器
ARM64 有 31 个通用寄存器,每个寄存器可以存取一个 64 位的数据。当使用 X0 ~ X30 时,它就是一个 64 位的数;当使用 W0 ~ W30 时,实际访问的是这些寄存器的低 32 位,写入时会将高 32 位清零。在指令编码中,0b11111 (31)用来表示 ZR (0 寄存器),仅表示参数 0 ,并不表示 ZR 是一个物理寄存器,如下图所示。
zero register(XZR/WZR) 效果和软件层面的"/dev/zero"类似,作为源寄存器产生0,作为目标寄存器丢弃传入的数据。效果和软件层面的"/dev/zero"类似,作为源寄存器产生0,作为目标寄存器丢弃传入的数据。另外,wzr是32位的零寄存器,用于给int清零;xzr是64位的零寄存器,用于给long清零。
mov r0, #0
str r0, [...]
有了zero register,一条指令就可以解决问题:
str wzr, [...]
Figure 1: 通用寄存器
除了 X0 ~ X30 寄存器外,还有一个 SP 寄存器也非常重要。下面介绍 iOS 逆向工程中关注频率最高的一些寄存器。
- X0 ~ X7:用来传递函数的参数,如果有更多的参数则使用栈来传递;X0 也用来存放函数的返回值;
- SP (Stack Pointer):栈指针寄存器。指向栈的顶部,可以通过 WSP 寄存器访问栈指针的最低有效 32 位;
- FP (Frame Pointer):即 X29 ,栈指针寄存器。指向栈的顶部;
- LR (Link Register):即 X30 ,链接寄存器。存储着函数调用完成时的返回地址,用来做函数调用栈跟踪,程序在崩溃时能够用栈打印出来就是借助 LR 寄存器来实现的;
- PC (Program Counter):保存的是将要执行的下一条指令的内存地址。通常在调试状态下看到的 PC 值都是当前断点处的地址,所以很多人认为 PC 值就是当前正在执行的指令内存地址,其实这是错误的。
0.2.1.1.2. 浮点寄存器
因为浮点数的存储及运算的特殊性,所以 CPU 中专门提供了 FPU 以及相应的浮点数寄存器来进行处理。ARM64 有 32 个浮点寄存器(V0 ~ V31),每个寄存器大小是 128 位。开发者可以通过 Bn (Byte)、Hn (Half Word)、Sn (Single Word)、Dn (Double Word)、Qn (Quad Word)来访问不同的位数,如下图所示:
Figure 2: 浮点寄存器
0.2.1.1.3. 状态寄存器
状态寄存器用来保存指令运行结果的一些信息,比如相加的结果是否溢出、是否为0及是否为负数等。CPU 的某些指令会根据运行的结果来设置状态寄存器的标志位,而某些指令则是根据这些状态寄存器中的值来进行处理。ARM64 体系的 CPU 提供了一个 32 位的 CPSR (Current Program Status Registerp)寄存器来作为状态寄存器,低 8 位(包括 M[0:4] 、T 、F 和 I)称为控制位,程序无法修改,第 28 ~ 31 位的 V 、C 、Z 和 N 均为条件码标志位,它们的内容可被算术或逻辑运算的结果改变,如下图所示。状态寄存器的内容由 CPU 内部进行置位,在程序中不能将某个数赋值给它。
Figure 3: 状态寄存器
下面介绍 4 个条件代码标志位及其含义:
- N ( Negative):当两个有符号整数进行运算时,N = 1 表示结果为负数,N = 0 表示运行结果为正数或零;
- Z (Zero):Z = 1 表示运算结果为零;Z = 0 表示运算结果为非零;
- C (Carry):当加法运算结果产生进位时(无符号数溢出),C = 1 ,否则 C = 0;当减法运行结果产生错位(无符号数溢出)时,C = 0,否则 C = 1;
- V (Overflow):在加/减法运算中,当操作数和运算结果为二进制补码表示的带符号数时,V = 1 表示符号位溢出。
0.2.1.1.4. 指令集
指令的基本格式如下:
<opcode> {<cond>}{S} <Rd>, <Rn> {, <shift_op2>}
其中,尖括号是必需的,花括号是可选的,各关键字的含义见下表:
标识符 | 含义 |
---|---|
opcode | 操作码域,也就是指令编码助词符,说明指令需要执行的操作类型 |
cond | 条件码域,指令允许执行的条件编码 |
S | 条件码设置域,这是一个可选项,当在指令中设置该域时,指令执行的结果将会影响程序状态寄存器 CPSR 中相应的状态标志 |
Rd/Xt | 目标寄存器,ARM64 指令可以选择 X0 ~ X30 或 W0 ~ W30 |
Rn/Xn | 第一个操作数的寄存器,和 Rd 一样,不同指令有不同要求 |
shift_op2 | 第二个操作数,可以是立即数或寄存器移位方式 |
常见的 ARM64 指令:
- 常用算术指令
指令 | 示例 | 含义 |
---|---|---|
ADD | ADD X0,X1,X2 | X0 = X1 + X2 |
SUB | SUB X0,X1,X2 | X0 = X1 - X2 |
MUL | MUL X0,X0,X8 | X = X0 x X8 |
SDIV | SDIV X0,X0,X1 | X0 = X0 / X1 (有符号除法运算) |
UDIV | UDIV X0,X0,X1 | X0 = X0 / X1 (无符号除法运算 |
CMP | CMP X28,X0 | X28 与 X0 相减,不存储结果,只更新 CPSR 中的条件标志位 |
CMN | CMN X28,X0 | X28 与 X0 相加,并根据结果更新 CPSR 中的条件标志位 |
ADDS/SUBS | ADDS X0,X1,X2 | 带 S 的指令运算结果会影响 CPSR 中的条件标志位,后面出现的其他指令也同理 |
- 常用跳转指令
- 条件跳转
指令 | 示例 | 含义 |
---|---|---|
B.cond | B.cond label | 若 cond 为真,则跳转到 label |
CBNZ | CBNZ Xn,label | 若 Xn != 0,则跳转到 label |
CBZ | CBZ Xn,label | 若 Xn == 0,则跳转到 label |
TBNZ | TBNZ Xn,#uimm6,label | 若 Xn[uimm6] != 0,则跳转到 label |
TBZ | TBZ Xn,#uimm6,label | 若 Xn[uimm6] == 0,则跳转到 label |
- 无条件跳转
指令 | 示例 | 含义 |
---|---|---|
B | B label | 无条件跳转 |
BL | BL label | 无条件跳转,返回地址保存到 X30 (LR)寄存器 |
BLR | BLR label | 无条件跳转到 Xn 寄存器的地址,返回地址保存到 X30 (LR)寄存器 |
BR | BR label | 无条件跳转到 Xn 寄存器的地址 |
RET | RET {Xn} | 子程序返回指令,返回地址默认保存在 LR (X30) |
- 常用逻辑指令
指令 | 示例 | 含义 |
---|---|---|
AND | AND X0,X1,X2 | X0 = X1 & X2 |
EOR | EOR X0,X1,X2 | X0 = X1 ^ X2 |
ORR | ORR X0,X1,X2 | X0 = X1 | X2 |
TST | TST W0,#0x40 | 测试 W0[3] 是否为 1 |
- 常用数据传输指令
指令 | 示例 | 含义 |
---|---|---|
MOV | MOV X19,X1 | X19 = X1 |
MOVZ | MOVZ Xn,#uimm16{,LSL #pos} | Xn = LSL(uimm16,pos) |
MOVN | MOVN Xn,#uimm16{,LSL #pos} | Xn = NOT(LSL(uimm16,pos)) |
MOVK | MOVK Xn,#uimm16{,LSL #pos} | Xn < pos+15:pos>=uimm16 |
- 常用地址偏移指令
指令 | 示例 | 含义 |
---|---|---|
ADR | ADR Xn,label | Xn = PC + label |
ADRP | ADRP Xn,label | base = PC[11:0] = ZERO(12); Xd = base + label; |
- 常用移位运算指令
指令 | 示例 | 含义 |
---|---|---|
ASR | ASR Xd,Xn,#uimm | 算术右移,结果带符号 |
LSL | LSL Xd,Xn,#uimm | 逻辑左移,移位后寄存器空出的低位补 0 |
LSR | LSR Xd,Xn,#uimm | 逻辑右移,移位后寄存器空出的高位补 0 |
ROR | LSR Xd,Xn,#uimm | 循环右移,从右端移除的位将被插入左端空出的位,可理解为“首尾相连” |
- 常用加载/存储指令
指令 | 示例 | 含义 |
---|---|---|
LDR | LDR Xn/Wn,addr | 从内存地址 addr 读取 8/4 字节内容到 Xn/Wn 中 |
STR | STR Xn/Wn,addr | 将 Xn/Wn 写入内存地址 addr |
LDUR | LDUR Xn/Wn [base,#simm9] | 从 base + #simm9 地址中读取数据到 Xn/Wn 中,U (Unscaled)表示不需要按字节对齐,取多少就是多少 |
STUR | STUR Xn/Wn,[base,#simm9] | 将 Xn/Wn 写入 base + #simm9 的内存地址 |
STP | STP Xn1,Xn2,addr | 将 Xn1 和 Xn2 写入到内存地址 addr 中。P (pair)表示一对,即同时操作两个寄存器 |
LDP | LDP Xn1,Xn2,addr | 从内存地址 addr 读取数据到 Xn1 和 Xn2 中 |
加载/存储指令都是成对出现,有时也会遇到这些指令的一些扩展,如 LDRB 、LDRSB 等,其含义见下表:
注意:ARM64 开始取消 32 位的 LDM 、STM 、PUSH 、POP 指令。取而代之的是 LDR/LDP 、STR/STP 。ARM64 中对栈的操作是 16 字节对齐的。
内存读写都是往高地址读/写:
STR :将数据从寄存器中读出来,存到内存中,即压栈; LDR :将数据从内存中读出来,放到寄存器中,即出栈。
此 LDR 和 STR 的变种 LDP 和 STP 可以同时操作两个寄存器。
扩展 | 含义 |
---|---|
B | 无符号 8bit |
SB | 有符号 8bit |
H | 无符号 16bit |
SH | 有符号 16bit |
W | 无符号 32bit |
SW | 有符号 32bit |
ARM 指令的一个重要特点是可以条件执行,每条 ARM 指令的条件码域包含 4 位条件码,共 16 种。几乎所有指令均根据 CPSR 中条件码的状态和指令条件码域的设置有条件地执行。当指令执行条件满足时,指令被执行,否则被忽略。指令条件码及其助词符后缀见下表:
条件码 | 助记符后缀 | 标志 | 含义 |
---|---|---|---|
0000 | EQ | Z 置位 | 相等 |
0001 | NE | Z 清零 | 不相等 |
0010 | CS | C 置位 | 无符号数大于或等于 |
0011 | CC | C 清零 | 无符号小于 |
0100 | MI | N 置位 | 负数 |
0101 | PL | N 清零 | 正数或零 |
0110 | VS | V 置位 | 溢出 |
0111 | VC | V 清零 | 未溢出 |
1000 | HI | C 置位、Z 清零 | 无符号数大于 |
1001 | LS | C 清零、Z 置位 | 无符号数小于或等于 |
1010 | GE | N 等于 V | 带符号数大于或等于 |
1011 | LT | N 不等于 V | 带符号数小于 |
1100 | GT | Z 清零且 N==V | 带符号数大于 |
1101 | LE | Z 置位或 N != V | 带符号数小于或等于 |
1110 | AL | 忽略 | 无条件执行 |
0.2.2. Objective-C
Objective-C 中的方法调用是通过消息机制来实现的(即[object method:arg]),X0 寄存器保存对象本身,X1 寄存器保存方法名,从 X2 ~ X7 开始便是参数,其他的参数通过栈传递。
0.2.3. 工具
0.2.4. otool 检查应用是否加壳
otool -l Portal | grep crypt
cryptoff 16384
cryptsize 97026048
cryptid 0
如以上输出中 cryptid 字段,cryptid = 1 表示已加壳,cryptid = 0 表示未加壳。
0.2.5. 砸壳工具
- Clutch (新系统无法使用)
- bfinject
-
CrackerXI
安装方法:在 Cydia 中添加源地址 http://cydia.iphonecake.com/ 后搜索安装即可。
- dumpdecrypted
- frida-ios-dump 现在这个比较好用。
- LLDB 手动脱壳
- 将应用程序执行文件复制到本地 (可使用scp)
$ scp -P2222 localhost:/var/containers/Bundle/Application/165833F7-4B85-49BB-9EE3-D6EDF4C41995/Charles.app/Charles ./
Charles
- 使用 otool 工具查看该二进制文件的
LC_ENCRYPTION_INFO
信息:
$ otool -arch arm64 -l ./Charles | grep -A 4 LC_ENCRYPTION_INFO
cmd LC_ENCRYPTION_INFO_64
cmdsize 24
cryptoff 28672
cryptsize 4096
cryptid 1
记住如上的 cryptoff 和 cryptsize 的值。
-
在设备启动 debugserver ,使用 LLDB 附加进程,然后获取应用的内存加载地址:
iOS 端:
debugserver -x auto 192.168.199.121:12345 /var/containers/Bundle/Application/165833F7-4B85-49BB-9EE3-D6EDF4C41995/Charles.app/Charles
电脑端:
lldb
(lldb) process connect connect://192.168.199.123:12345
Process 2686 stopped
thread #1, stop reason = signal SIGSTOP
frame #0: 0x0000000102a99000 cy-y9Ee3S.dylib`_dyld_start
cy-y9Ee3S.dylib`_dyld_start:
-> 0x102a99000 <+0>: mov x28, sp
0x102a99004 <+4>: and sp, x28, #0xfffffffffffffff0
0x102a99008 <+8>: mov x0, #0x0
0x102a9900c <+12>: mov x1, #0x0
Target 0: (Charles) stopped.
(lldb) image list -f -o Charles
[ 0] /private/var/containers/Bundle/Application/165833F7-4B85-49BB-9EE3-D6EDF4C41995/Charles.app/Charles 0x0000000002694000(0x0000000102694000)
(lldb) memory read 0x0000000102694000+28672 -c 4096 --force --binary -outfile ./CharlesDecrypted
4096 bytes written to 'CharlesDecrypted'
(lldb)
-
修复文件
因为 dump 出来的数据是没有 Mach-O 头部信息,所以需要修复才能使用。将 dump 出来的数据重新写回脱壳前的文件以替换加密的数据即可:
$ dd seek=28672 bs=1 conv=notrunc if=./CharlesDecrypted of=Charles-new
4096+0 records in
4096+0 records out
4096 bytes transferred in 0.009814 secs (417363 bytes/sec)
seek 值指明需要在 Charles 的何处开始写入解密后的数据,目标程序只有一个 ARM64 架构,因此架构偏移值是 0 ,28672 是加密数据的偏移值 cryptoff ,两者相加即 seek 的值。
如果目标程序有多个架构,就要先使用 otool -hf
查看待修复架构的 offset 字段,然后和 cryptoff 相加,即得到 seek 值。
最后一步就是修改加密标记:使用 MachOView 打开修复后的文件,定位到 “LC_ENCRYPTION_INFO_64”,修改 cryptid 的值为 0 即可。
0.2.6. lipo 分离架构
lipo -info WeChat # 查看目标文件架构
lipo WeChat -thin arm64 -output WeChat_arm64
0.2.7. class-dump 将二进制文件中的类、方法及属性导出为头文件
class-dump --arch arm64 -a -A -H Portal -o ./Headers/
0.2.8. Cycript
0.2.9. Reveal 界面分析工具
0.2.10. FLEX 应用内部调试工具
为 APP 添加一个悬浮的工具栏,通过其查看和修改视图的层次结构、动态修改类的属性、动态调用补全和方法、动态查看类和框架以及动态修改 UI 等。
0.2.11. Frida 轻量级 Hook 框架
提供了精简的 Python 接口和功能丰富的 JS 接口,除了使用自身的控制台交互以外,还可以利用 Python 将 JS 脚本库注入目标进程。使用 Frida 可以获取进程详细信息、拦截和调用指定函数、注入代码、修改参数、从 iOS 应用程序中 dump 类和类方法信息等。
0.2.12. LLDB 调试
相关命令
命令 | 示例 | 说明 |
---|---|---|
po | po $x0 | 打印 Objective-C 对象 |
x/s | x/s $x1 | 将 x1 寄存器的 C 字符串打印出来 |
p/x | p/x $x7 | 以 16 进制显示寄存器 x7 中的数据 |
p/d | p/d $x7 | 以 10 进制显示寄存器 x7 中的数据 |
p/t | p/t $x7 | 以 2 进制显示寄存器 x7 中的数据 |
register read | register read $x4 | 读取寄存器 x4 中的内容 |
register write | register write $x4 0 | 写寄存器 x4 |
memory read | memory read $x0 |
0.2.13. framework 注入
load
两个重要概念 MachO
MachO 介绍:
源码:https://github.com/apple/darwin-xnu/tree/main/EXTERNAL_HEADERS/mach-o 格式说明文档:https://github.com/aidansteele/osx-abi-macho-file-format-reference/blob/master/Mach-O_File_Format.pdf
dyld
代码注入(注意路径):https://www.fuqionglin.com/%E6%8A%80%E6%9C%AF/2018/03/16/%E4%BB%A3%E7%A0%81%E6%B3%A8%E5%85%A5.html
hook
- fishhook
- cydia substrate
crash 分析
lldb image 命令
yololib
optool
砸壳工具:
恢复符号:
- restore-symbol
- restore block symbol https://github.com/4ch12dy/xia0LLDB
iOS Crash 的监听:https://www.jianshu.com/p/5e1c60de9c32
程序堆是由底部往上增长,栈是由上面往下申请。当堆和栈和撞到一起的时候表示内存用完了,即堆栈溢出。