逆向笔记

0.1. IDA 说明

0.1.1. 字符串窗口(Strings window)

  • .data 表示数据在数据段:可以进行读写;
  • .rodata 表示在只读数据段:只读,不能进行修改;
  • .note

# 开头的一些数据为立即数,表示数值本身,如 #0

0.1.2. IDA 调试界面

  1. 把汇编窗口

    C 将当前地址处的数据解析成代码 P

  2. 十六进制窗口

    编辑内存数据和代码

  3. 寄存器窗口

    修改寄存器的值

  4. 模块窗口

    模块路径和地址

  5. 线程窗口
  6. 栈窗口
  7. 输出信息窗口

IDA 调试常用功能:

  1. 断点和运行

    设置断点 F2 设置断点不可用 Disable breakpoint 编辑断点 Edit breakpoint 删除断点 Delete breakpoint 继续运行 F9 查看当前所有断点 Ctrl+Alt+B

  2. 单步调试

    单步进入 F7 单步步过 F8 运行到函数的返回地址 Ctrl + F7 运行到光标处 F4

  3. IDC 脚本

0.1.3. ARM 汇编指集

  • B 无条件跳转
  • BL 带链接无条件跳转指令,即调用子函数或子程序,相当于 x86call 指令;
  • 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, [...]

nil

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)来访问不同的位数,如下图所示:

nil

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 内部进行置位,在程序中不能将某个数赋值给它。

nil

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>}

其中,尖括号是必需的,花括号是可选的,各关键字的含义见下表:

Table 1: ARM 指令关键字及其含义
标识符 含义
opcode 操作码域,也就是指令编码助词符,说明指令需要执行的操作类型
cond 条件码域,指令允许执行的条件编码
S 条件码设置域,这是一个可选项,当在指令中设置该域时,指令执行的结果将会影响程序状态寄存器 CPSR 中相应的状态标志
Rd/Xt 目标寄存器,ARM64 指令可以选择 X0 ~ X30 或 W0 ~ W30
Rn/Xn 第一个操作数的寄存器,和 Rd 一样,不同指令有不同要求
shift_op2 第二个操作数,可以是立即数或寄存器移位方式

常见的 ARM64 指令:

  • 常用算术指令
Table 2: 常用算术指令
指令 示例 含义
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 中的条件标志位,后面出现的其他指令也同理
  • 常用跳转指令
    • 条件跳转
Table 3: 条件跳转指令
指令 示例 含义
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
  • 无条件跳转
Table 4: 无条件跳转指令
指令 示例 含义
B B label 无条件跳转
BL BL label 无条件跳转,返回地址保存到 X30 (LR)寄存器
BLR BLR label 无条件跳转到 Xn 寄存器的地址,返回地址保存到 X30 (LR)寄存器
BR BR label 无条件跳转到 Xn 寄存器的地址
RET RET {Xn} 子程序返回指令,返回地址默认保存在 LR (X30)
  • 常用逻辑指令
Table 5: 常用逻辑指令
指令 示例 含义
AND AND X0,X1,X2 X0 = X1 & X2
EOR EOR X0,X1,X2 X0 = X1 ^ X2
ORR ORR X0,X1,X2 X0 = X1 &vert; X2
TST TST W0,#0x40 测试 W0[3] 是否为 1
  • 常用数据传输指令
Table 6: 常用数据传输指令
指令 示例 含义
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
  • 常用地址偏移指令
Table 7: 常用地址偏移指令
指令 示例 含义
ADR ADR Xn,label Xn = PC + label
ADRP ADRP Xn,label base = PC[11:0] = ZERO(12); Xd = base + label;
  • 常用移位运算指令
Table 8: 常用移位运算指令
指令 示例 含义
ASR ASR Xd,Xn,#uimm 算术右移,结果带符号
LSL LSL Xd,Xn,#uimm 逻辑左移,移位后寄存器空出的低位补 0
LSR LSR Xd,Xn,#uimm 逻辑右移,移位后寄存器空出的高位补 0
ROR LSR Xd,Xn,#uimm 循环右移,从右端移除的位将被插入左端空出的位,可理解为“首尾相连”
  • 常用加载/存储指令
Table 9: 常用加载/存储指令
指令 示例 含义
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 可以同时操作两个寄存器。

Table 10: 加载/存储指令的扩展
扩展 含义
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. 砸壳工具

$ 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 调试

相关命令

Table 11: 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 命令

chiselLLDB (如 sbt 恢复符号)插件

yololib

optool

砸壳工具:

恢复符号:

iOS Crash 的监听:https://www.jianshu.com/p/5e1c60de9c32

程序堆是由底部往上增长,栈是由上面往下申请。当堆和栈和撞到一起的时候表示内存用完了,即堆栈溢出。

1. ida 调试

adb forward tcp:23946 tcp:23946

参考:

iOS 应用逆向与安全之道,罗巍

反调试及绕过