【iOS】iOS逆向系列八 - 动态调试

什么叫动态调试?

将程序运行起来,通过下断点、打印等⽅式,查看参数、返回值、函数调⽤流程等。

一、Xcode的动态调试原理

Xcode内置了LLDB调试器,每次通过Xcode调试APP时,会启动iPhone中的debugserver(iPhone手机官方调试工具。用来协助调试APP)。

LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器。

如果iPhone手机从未连接过Xcode,debugserver是不在手机上的。debugserver一开始存放在Mac的Xcode⾥面(/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/De viceSupport/10.0/DeveloperDiskImage.dmg/usr/bin/debugserver)。当Xcode识别到⼿机设备时,Xcode会⾃动将debugserver安装到iPhone上(/Developer/usr/bin/debugserver)。

当Xcode把APP运行起来开始调试时,LLDB会发送指令给debugserver,debugserver监听到指令后会把指令执行到APP,APP再把指令信息反馈给debugserver,debugserver反馈给LLDB,最终LLDB会把反馈信息打印到Xcode的控制台。

LLDB是通过数据线或TCP(同一个局域网)和debugserver进行交互的。

关于GCC、LLVM、GDB、LLDB:

  • Xcode的编译器发展历程:GCCLLVM
  • Xcode的调试器发展历程:GDBLLDB

Xcode调试的局限性:⼀般情况下,只能调试通过Xcode安装的APP。

二、动态调试任意APP

既然Xcode有一定的局限性,我们则可以通过终端运行LLDB调试APP。但是我们需要解决的问题就是建立LLDB和debugserver的连接通道,并且能让debugserver和APP进行通信。

2.1. 环境搭建

2.1.1. debugserver的权限问题

默认情况下,/Developer/usr/bin/debugserver缺少一定的权限,只能调试通过Xcode安装的APP,⽆法调试其他APP(⽐如来自App Store的APP)。

如果希望调试其他APP,需要对debugserver重新签名,签上2个调试相关的权限(get-task-allow、task_for_pid-allow)。

2.1.2. debugserver重签权限

  1. iPhone上的/Developer⽬录是只读的,无法直接对/Developer/usr/bin/debugserver⽂件签名,需要先把debugserver复制到Mac,通过ldid命令导出文件以前的签名权限。
1
$ ldid -e debugserver > debugserver.entitlements
  1. debugserver.plist⽂件加上get-task-allowtask_for_pid-allow权限。

  1. 通过ldid命令重新签名。
1
$ ldid -Sdebugserver.entitlements debugserver
  1. 将已经签好权限的debugserver放到/usr/bin目录,便于找到debugserver指令(更重要的是以前的目录文件是只读的,无法进行覆盖)。

  2. 默认情况下,需要开启/usr/bin/debugserver的权限。

1
$ chmod +x /usr/bin/debugserver

关于权限的签名,也可以使用codesign

1
2
3
4
5
6
# 查看权限信息
$ codesign -d --entitlements - debugserver
# 签名权限
$ codesign -f -s - --entitlements debugserver.entitlements debugserver
# 或者简写为
$ codesign -fs- --entitlements debugserver.entitlements debugserver

2.1.3. 让debugserver附加到某个APP进程

1
$ debugserver *:端口号 -a 进程
  • *:端⼝号*代表主机地址。端口号意思是使用iPhone的某个端口启动debugserver服务(只要不是保留端口号就⾏)。
  • -a:进程:输⼊APP的进程信息(进程ID或者进程名称)。

上面介绍的是debugserver连接运行时的APP,其实我们还可以通过debugserver启动APP:

1
$ debugserver -x auto *:端⼝口号 APP的可执⾏行行⽂文件路路径

2.2. 建立连接

Mac上启动LLDB,远程连接iPhone上的debugserver服务。

  1. 启动LLDB

    1
    $ lldb
  2. 连接debugserver服务(可以通过usbmuxd把debugserver的端口映射到Mac,然后填写映射的IP地址和端口号,最终使用USB进行传输)

    1
    (lldb) process connect connect://⼿机IP地址:debugserver服务端口号
  3. 默认情况下,只要debugserver服务连接成功,对应的APP就会处于暂停状态(类似下了一个断点)。可以使用LLDB的c命令让程序继续运行

    1
    (lldb) c

三、常见的LLDB指令

  • 指令的格式:

    1
    <command> [<subcommand> [<subcommand>...]] <action> [-options [option- value]] [argument [argument...]]
    • command:命令
    • subcommand:子命令
    • action:命令操作
    • options:命令选项
    • argument:命令参数
    • ⽐如给test函数设置断点
      1
      breakpoint set -n test
      • breakpointcommand
      • setsubcommand
      • -noptions
      • testargument
  • **help <command>**:查看指令的⽤法(⽐如help breakpointhelp breakpoint set

  • **expression <cmd-options> -- <expr>**:执行⼀个表达式

    • cmd-options:命令选项
    • --:命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略
    • expr:需要执行的表达式
    • 比如设置view的背景色:
      1
      expression self.view.backgroundColor = [UIColor redColor]
      • expressionexpression --和指令printpcall的效果⼀样
      • expression -O ---O就是--object description的指令缩写,类似NSLog(@"%@", object))和指令po的效果⼀样
  • thread backtrace:打印线程的堆栈信息(和指令bt的效果一样)

  • **thread return []**:让函数直接返回某个值,不会执⾏断点后面的代码

  • **frame variable []**:打印当前栈帧的变量

  • thread continue、continue、c:程序继续运⾏

  • thread step-over、next、n:单步运行,把⼦函数当做整体⼀步执⾏

  • thread step-in、step、s:单步运行,遇到子函数会进⼊子函数

  • thread step-out、finish:直接执行完当前函数的所有代码,返回到上⼀个函数

  • thread step-inst-over、nexti、ni

  • thread step-inst、stepi、si

  • si、ni和s、n类似

    • s、n是源码级别
    • si、ni是汇编指令级别
  • breakpoint set:设置断点

    • breakpoint set -a 函数地址
    • breakpoint set -n 函数名
      1
      2
      3
      breakpoint set -n test
      breakpoint set -n touchesBegan:withEvent:
      breakpoint set -n "-[ViewController touchesBegan:withEvent:]"
    • breakpoint set -r 正则表达式
    • breakpoint set -s 动态库 -n 函数名
  • breakpoint list:列出所有的断点(每个断点都有⾃己的编号)

  • breakpoint disable 断点编号:禁⽤断点

  • breakpoint enable 断点编号:启⽤断点

  • breakpoint delete 断点编号:删除断点

  • breakpoint command add 断点编号:给断点预先设置需要执行的命令,到触发断点时,就会按顺序执⾏

  • breakpoint command list 断点编号:查看某个断点设置的命令

  • breakpoint command delete 断点编号:删除某个断点设置的命令

  • 内存断点:在内存数据发生改变的时候触发

    • watchpoint set variable 变量watchpoint set variable self->age
    • watchpoint set expression 地址watchpoint set expression &(self->_age)
    • watchpoint list
    • watchpoint disable 断点编号
    • watchpoint enable 断点编号
    • watchpoint delete 断点编号
    • watchpoint command add 断点编号
    • watchpoint command list 断点编号
    • watchpoint command delete 断点编号
  • image lookup:模块查找(image不是图片的意思)

    • image lookup -t 类型:查找某个类型的信息
    • image lookup -a 地址:根据内存地址查找在模块中的位置
    • image lookup -n 符号或者函数名:查找某个符号或者函数的位置
  • image list:列出所加载的模块信息

    • image list -o -f:打印出模块的偏移地址、全路径

四、ASLR

iOS4.3开始引入了ASLR技术。

什么是ASLR?Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。

在调试其他APP时,是无法通过函数名下断点的(breakpoint set -n 函数名),而且通过函数地址下断点时(breakpoint set -a 函数地址),函数地址需要经过计算才能得出。

4.1. 未使用ASLR

代码编译以后,虚拟内存地址基本都是固定的,__PAGEZERO起始内存地址是0,其他人员可以非法入侵任意APP。

__PAGEZERO是空指针指向的内存区域(安全),更多用途可自行查阅资料。

4.2. 使用了ASLR

ASLR控制的是__PAGEZERO前面的一部分内存,随机产生的Offset,让__PAGEZERO的起始内存地址动态变化。

函数的内存地址(VM Address) = File Offset + ASLR Offset + __PAGEZERO Size

Hopper、IDA等工具中的地址都是未使用ASLR的VM Address。