iOS逆向学习之八(动态调试)

3,111 阅读8分钟

什么是动态调试?

动态调试就是在我们的程序运行之时,通过下断点、打印等一系列方式查看参数、返回值、函数调用流程等等。不仅是在iOS开放中需要动态调试,在任何语言的开发过程中都需要用到动态调试

Xcode如何进行动态调试?

Xcode编译器和调试器

  • Xcode最早使用的是GUN开发的GCC编译器,但是从xcode5之后开始使用自研的LLVM编译器,可以点击查看GCCLLVM的介绍
  • Xcode调试器早期也是使用的GUN开发的GDB调试器,之后也替换成了自研的LLDB调试器,可以点击查看GDBLLDB的介绍。

Xcode调试App的流程

  • 首先在Xcode中会自带一个叫做==debugserver==的工具,存放在/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/De viceSupport/9.1/DeveloperDiskImage.dmg/usr/bin/debugserver目录下,当我们使用Xcode在iPhone上运行我们的程序时,Xcode会将==debugserver==安装到我们的iPhone上,具体安装路径为/Developer/usr/bin/debugserver
  • Xcode连接上手机后,通过自带的==LLDB==编译器向iPhone上的==debugserver==传输指令,==debugserver==接收到指令之后将指令运行到App中,App执行指令之后将结果返回为==debugserver==,然后==debugserver==会将信息反馈给==LLDB==,最后==LLDB==会将信息打印到Xcode上。
  • 但是Xcode这种调试方式有很大局限性,因为只能调试通过Xcod安装的App

如何不通过Xcode动态调试任意App?

可以使用终端取代Xcode来对App进行动态调试

debugserver环境搭建

方法一、通过ldid进行签名

  • 获取到iPhone下/Developer/usr/bin/debugserver目录中的debugserver工具,复制到Mac上
  • 由于通过Xcode安装的==debugserver==权限不足,只能调试Xocde安装的App,所以我们要给==debugserver==增加更多的权限。
  • 通过ldid -e指令导出==debugserver==的权限信息
ldid -e debugserver > debugserver.entitlements
  • 在debugserver.entitlements中增加以下两个权限
    • get-task-allow
    • task_for_pid-allow

  • 通过ldid对==debugserver==进行重新签名
ldid -Sdebugserver.entitlements debugserver
  • 由于/Developer/usr/bin/目录是只读的,所以我们将重新签名过的==debugserver==放在/usr/bin/下,然后对==debugserver==增加运行权限,就可以在终端使用==debugserver==了
chmod +x /usr/bin/debugserver

方法二、通过codesign对debugserver进行签名

#查看权限基本信息
codesign -d --entitlements - debugserver

#签名权限
codesign -f -s - --entitlements debugserver.entitlements debugserver

#也可以简写为
codesign -fs - --entitlements debugserver.entitlements debugserver

让debugserver附加到某个进程

debugserver *:端口号 -a 进程

*:端口号:表示使用iPhone上的某个端口启动debugserver服务(注意:不能使用保留端口号)
-a 进程:指定进程id或者进程名称

使用debugserver启动App

debugserver -x auto *:端口号 App可执行文件路径

在Mac上启动LLDB,远程连接iPhone上的debugserver

在之前的学习中,我们知道可以使用iPhone的ip地址来连接手机,但是这样需要保证手机和电脑在同一个wifi下,并且使用这种方式传输数据十分缓慢。所以,通常的做法是通过usb连接iPhone,将iPhone上的某个端口映射到Mac上的某个端口,然后然LLDB和Mac上的端口通信即可

debugserver attaching

  • 通过以下指令对iPhone的10089进行映射
python ./usbmuxd/tcprelay.py -t 22:10088 9999:10089

此处的10089端口可以任意定义,只要不使用保留端口号即可。使用10088端口映射22端口,是为了和iPhone进行SSH通信

  • 映射成功之后,使用9999端口启动==debugserver==服务。让==debugserver==附加到腾讯视频App进程,如下:
debugserver *:9999 -a live4iphone
  • 如果出现以下效果,表明==debugserver==已经成功attach到了腾讯视频App上

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

  • 首先在Mac上启动LLDB
➜  ~ lldb
(lldb)
  • 通过Mac的10089端口连接==debugserver==服务
process connect connect://localhost:10089

  • 由于连接上==debugserver==服务之后,程序是默认在断点状态,使用LLDB的c命令让程序继续运行
(lldb) c
Process 635 resuming

常用的LLDB指令

LLDB指令的基本格式

 <command> [<subcommand> [<subcommand>...]] <action> [-options [option- value]] [argument [argument...]]

对应着

命令 子命令 命令操作 命令选项 命令参数

例如给test这个函数设置断点:

breakpoint set -n test

help指令

help指令可以帮助我们快速查找LLDB指令的使用方法

help breakpoint
help breakpoint set

expression -- 指令

expression指令被用来执行一个表达式

expression self.view.backgroundColor = [UIColor redColor]
//或者
expression -- self.view.backgroundColor = [UIColor redColor]
  • expression、expression --和指令print、p、call效果等同

  • expression -O -- 和指令po效果等同

expression后的 -- 表示命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略。如果expression之后有命令选项,则--不能省略。

thread backtrace

==thread backtrace==指令的作用是打印线程的堆栈信息,效果和 ==bt== 的效果相同。

(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000102d4d61d TestFont`-[ViewController touchesBegan:withEvent:](self=0x00007fd2f86066d0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000036d4ab0) at ViewController.m:26
    frame #1: 0x0000000106f6f8e8 UIKitCore`forwardTouchMethod + 353
    ......
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000102d4d61d TestFont`-[ViewController touchesBegan:withEvent:](self=0x00007fd2f86066d0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000036d4ab0) at ViewController.m:26
    frame #1: 0x0000000106f6f8e8 UIKitCore`forwardTouchMethod + 353
    ......

thread return []

让函数返回某个值,不会执行之后的代码。如果函数有返回值,在后面跟上返回值,如果函数没有返回值,就直接使用thread return即可

frame variable []

打印当前栈帧的变量

thread相关指令

以下指令从左到右依次表示:指令全称、指令简称、极简指令

  • 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是汇编指令级别。每一句OC代码会有一条或多条汇编指令构成,s、n指令表示一步一步执行每一句OC代码,而si、ni表示一步一步执行汇编指令。

breakpoint相关指令

breakpoint set

设置断点

  • breakpoint set -a 函数地址
  • breakpoint set -n 函数名
    • 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

给指定的内存下断点,当内存中的数据发生改变时会触发

watchpoint set variable 变量

对指定的变量设置内存断点,当变量值改变的时候会触发

watchpoint set variable self->_age

注意:此处不能使用self.age

watchpoint set expression 内存地址

对指定内存地址设置断点,作用和watchpoint set variable相同

watchpoint list

列出所有的内存断点

watchpoint disable 断点编号

禁用内存断点

watchpoint enable 断点编号

启用内存断点

watchpoint delete 断点编号

删除内存断点

watchpoint command add 断点编号

给指定断点编号的内存断点预先设置需要执行的命令,到触发内存断点时,就会按顺序执行预先设置的命令

watchpoint command list 断点编号

查看某个编号的内存断点所有预先设置的命令

watchpoint command delete 断点编号

删除指定编号内存断点的所有预设命令

image模块查询指令

image lookup

模块查询指令

  • image lookup -t 类型
    查找某个类型的信息

  • image lookup -a 内存地址
    根据内存地址查找在模块中的位置

  • image lookup -n 符号或函数名
    查找某个符号或者函数的位置

image list

列出所有所加载的模块信息

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

LLDB小技巧

  • 每次敲Enter键,都会自动执行上次的命令
  • 绝大部分的指令都可以使用缩写
(lldb) breakpoint list
(lldb) br li
(lldb) br l

(lldb) breakpoint set -n test
(lldb) br s -n test