分类目录归档:技术相关

iOS开发调试 – LLDB使用概览

前言


LLDB是个开源的内置于XCode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安装C++或者Python插件。在日常的开发和调试过程中给开发人员带来了非常多的帮助。

(lldb)po std.name
Noskthing

了解并熟练掌握LLDB的使用是非常有必要的。这篇文章将会为大家总结日常高频使用的一些技巧。文章分节的主要依据是功能的相关性,并且省略了很多Xcode已经集成并且可视化的操作。

  • 一些基础
  • 断点相关
  • 参数检查
  • expression指令
  • hook的概念
  • 流程控制
  • script
  • image
  • register
  • 结语

一些基础


LLDB的基本语法如下

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

其中内置了非常多的功能,选择去硬背每一条指令并不是一个明智的选择。我们只需要记住一些常用的指令,在需要的时候通过help命令来查看相关的描述即可。

(lldb)help
Debugger commands:

  apropos           -- List debugger commands related to a word or subject.
  breakpoint        -- Commands for operating on breakpoints (see 'help b' for shorthand.)
  ...

还可以通过apropos来获取具体命令的合法参数信息以及含义

(lldb) apropos breakpoint
The following commands may relate to 'breakpoint':
  _regexp-break                         -- Set a breakpoint using one of
                                           several shorthand formats.
  _regexp-tbreak                        -- Set a one-shot breakpoint using one
                                           of several shorthand formats.
  ...

断点相关


Xcode本身已经将大部分的操作用UI展示了出来,比如说

  • Breakpoint Navigator (⌘ + 7)
  • Debug Navigator (⌘ + 6)
  • Debug Area (⌘ + Shift + Y)
  • Debug menu item

    iOS开发调试 - LLDB使用概览
    断点列表

日常开发中大部分有关断点的操作我们都可以不使用命令行直接通过Xcode的可视化操作来实现,命令行的操作似乎是一种多余。但是使用(lldb)help breakpoint查看一下LLDB提供的所有帮助,你会发现在命令行中使用LLDB能够给予我们更多更详细的调试信息以及更广阔的操作空间。

(lldb)help breakpoint

举一个简单的例子,我们需要为某一个函数设置一个断点。比如说给ViewController的VviewDidLoad方法设置一个断点。这对于Xcode而言非常的简单。

iOS开发调试 - LLDB使用概览
添加断点

编辑每一个断点的各个选项也因为可视化的操作而变得非常的简单。但是如果我们需要在系统调用的某个函数里设置断点呢,抑或某个函数我们只能在crash log茫茫碌的堆栈信息里才能看到一点它的痕迹,这个时候如何操作呢?

iOS开发调试 - LLDB使用概览
crash log

假设我们现在需要给objc_msgSend函数设置断点。首先先想办法获取objc_msgSend的地址。我们在Appdelegate.m文件给函数- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions打一个断点,运行程序如下图所示。

iOS开发调试 - LLDB使用概览

我们可以通过(lldb)br set -a 0x0000000103c04ac0来为objc_msgSend()设置一个断点。输入continue继续执行你会发现如果程序再次调用objc_msgSend()会暂停。

iOS开发调试 - LLDB使用概览
断点设置

Tips
* 图片是用模拟器运行所以是x86,移动设备是arm。两者的指令有所不同。图中的callq指令对应arm中的bl
* 由于 ASLR(地址空间配置随机载入) 的原因地址是不固定的,所以图中objc_msgSend()的地址在你的机器上是不可用的。

断点相关的指令很多很杂,这里为大家列举一些常用的。如果以后遇到一些特殊的需求,可以借助help()指令来自行查找相关指令。

设置断点

  • 给所有名为xx的函数设置一个断点
(lldb)breakpoint set —name xx
(lldb)br s -n xx
(lldb)b xx
  • 在文件F指定行L设置断点
(lldb)breakpoint set —file F —line L
(lldb)br s -f F -l L
(lldb)b F:L
  • 给所有名为xx的C++函数设置一个断点(希望没有同名的C函数)
(lldb)breakpoint set —method xx
(lldb)br s -M xx
  • 给一个OC函数[objc msgSend:]设置一个断点
(lldb)breakpoint set —name “[objc msgSend:]”
(lldb)b -n “[objc msgSend:]”
  • 给所有名为xx的OC方法设置一个断点(希望没有名为xx的C或者C++函数)
(lldb)breakpoint set —selector xx
(lldb)br s -S count
  • 给所有函数名正则匹配成功的函数设置一个断点
(lldb)breakpoint set --func-regex regular-expression
(lldb)br s -r regular-expression
  • 给指定函数地址func_addr的位置设置一个断点
(lldb)br set -a func_addr

断点查看

(lldb)breakpoint list
(lldb)br l

断点删除

(lldb)breakpoint delete index
(lldb)br del index

index指明断点的序号,如果为空则删除所有断点

watchpoint

iOS开发当中有一个重要的概念KVO,我们会给一个重要的变量设置一个观察者,用以在它发生变化的时候做出相应的操作。在调试过程中我们也可以借助LLDB来监视某个变量或某一块内存的读写情况。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSString * str = @"First";
    [self printString:str];
    str = @"Second";
    [self printString:str];
}

- (void)printString:(NSString *)str
{
    NSLog(@"%@",str);
}

我们利用watchpoint指令来监视变量str。需要重点说明的是-w选项,下例中并没有写出,缺省值是write,这意味着只有在str被写入的时候程序会暂停。

(lldb) watchpoint set variable str
Watchpoint created: Watchpoint 1: addr = 0x7fff5997f9e8 size = 8 state = enabled type = w
    declare @ '/Users/noskthing/Desktop/LLDBTest/LLDBTest/ViewController.m:22'
    watchpoint spec = 'str'
    new value: 0x0000000106280078
2017-07-22 17:35:13.534 LLDBTest[4585:521823] First

Watchpoint 1 hit:
old value: 0x0000000106280078
new value: 0x0000000106280098

(lldb) image lookup -a 0x0000000106280098
      Address: LLDBTest[0x0000000100003098] (LLDBTest.__DATA.__cfstring + 32)
      Summary: @"Second"
(lldb) image lookup -a 0x0000000106280078
      Address: LLDBTest[0x0000000100003078] (LLDBTest.__DATA.__cfstring + 0)
      Summary: @"First"

当你输入watchpoint list查看设置的watchpoint时系统会提示你当前测试的机器允许设置的最大个数。

(lldb) watchpoint list
Number of supported hardware watchpoints: 4
No watchpoints currently set.

参数检查


当我们调试程序遇到断点的时候Xcode会自动的将当前作用域下的局部变量以及全局变量展示出来

iOS开发调试 - LLDB使用概览
当前作用

借助命令行我们也能够轻松的获取这些参数的信息

  • 展示当前作用域下的参数和局部变量
(lldb)frame variable
(lldb)fr v
  • 展示当前作用域下的局部变量
(lldb)frame variable --no-args
(lldb)fr v -a
  • 展示指定变量var的具体内容
(lldb)frame variable *var*
(lldb)fr v *var*
(lldb)p *var*
  • 展示当前对象的全局变量
(lldb)target variable
(lldb)ta v

细心的朋友应该能够有所发现,这些操作都有一个局限:我们查看的各个变量都是当前作用域的。这意味着程序遇到断点的时候暂停,所有的操作都是局限于当前函数以及当前函数所在线程的内部。可视化的操作并没有给我们太多操作的空间,但是借助命令行我们可以打破这样一个局限。

命令行输入(lldb)thread backtrace可以获取当前线程函数的调用栈

(lldb)thread backtrace
* frame #0: 0x0000000100057204 test`-[ViewController viewDidLoad](self=0x000000014fe0ad10, _cmd=<unavailable>) at ViewController.m:99 [opt]
  frame #1: 0x000000018e1cfec0 UIKit`-[UIViewController loadViewIfRequired] + 1036
  frame #2: 0x000000018e1cfa9c UIKit`-[UIViewController view] + 28
  frame #3: 0x000000018e1d631c UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 76
  frame #4: 0x000000018e1d37b8 UIKit`-[UIWindow _setHidden:forced:] + 272
  frame #5: 0x000000018e245224 UIKit`-[UIWindow makeKeyAndVisible] + 48

输入frame select指令我们可以任意的去选择一个作用域去查看。

(lldb)frame select 2

类比frame的操作我们可以轻松看出线程选择相关的操作

(lldb) thread list
Process 21035 stopped
* thread #1: tid = 0x27361a, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  thread #2: tid = 0x273639, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #3: tid = 0x27363a, 0x00000001893c2ca8 libsystem_pthread.dylib`start_wqthread
  thread #4: tid = 0x27363e, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #5: tid = 0x27363f, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'com.apple.uikit.eventfetch-thread'
  thread #6: tid = 0x273640, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #7: tid = 0x273641, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #8: tid = 0x273642, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #10: tid = 0x273646, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'com.apple.NSURLConnectionLoader'
  thread #11: tid = 0x273644, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'AFNetworking'
  thread #12: tid = 0x27364a, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #13: tid = 0x27364b, 0x00000001892fd23c libsystem_kernel.dylib`__select + 8, name = 'com.apple.CFSocket.private'
(lldb) thread select 2

以上提到的几个指令意味着借助命令行,我们可以在断点发生的时候跳转到当前存在的任一线程里的任一作用域去进行操作。

除却frame的操作,我们很多时候习惯借助NSLog去打印某些关键的信息。如果运行到一半的时候发现漏写了某个地方的NSLog,加入相关代码并重新运行也许不是一个让人省心的方法。我们可以在需要打印的地方设置一个断点,然后运行p object或者po object指令来查看指定对象。

(lldb) p userInfo
(__NSDictionaryM *) $0 = 0x0000000174242010 4 key/value pairs
(lldb) po userInfo
{
    macAddressString = "60:01:94:80:37:6c";
    payload =     {
        TimerAction = 0;
        TimerStat = 0;
        brightness = 70;
        colortemp = 93;
        remaining = "-1";
        switch = 1;
    };
    serialNumberString = 60019480376C;
    tcpPortString = "192.168.199.124";
}

两个指令实际都是expression指令的缩写。p打印的是当前对象的地址而po则会调用对象的description方法,做法和NSLog是一致的。

expression指令


expression命令是执行一个表达式,并将表达式返回的结果输出。包括上文提到的p指令在内,以下几个都是expression指令的别名。

(lldb)expression userInfo
(__NSDictionaryM *) $5 = 0x0000000174242010 4 key/value pairs 
(lldb) p userInfo
(__NSDictionaryM *) $2 = 0x0000000174242010 4 key/value pairs
(lldb) print userInfo
(__NSDictionaryM *) $3 = 0x0000000174242010 4 key/value pairs
(lldb) e userInfo
(__NSDictionaryM *) $4 = 0x0000000174242010 4 key/value pairs
(lldb) call userInfo
(__NSDictionaryM *) $5 = 0x0000000174242010 4 key/value pairs

打印对象的时候我们也可以指定特定格式,详细的格式查阅参见这里。

(lldb) p 16
16
(lldb)p/x 16
0x10
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

但是expression指令真正强大的部分应该是它的写入能力。我们可以通过expression来执行一个表达式动态的修改我们程序中变量的值。

(lldb) p count
(NSUInteger) $4 = 12
(lldb)e count = 42
(lldb) p count
(NSUInteger) $5 = 42

在断点处我们首先打印count变量的值,之后通过执行expression指令来修改count变量,再次打印可以发现此时count已经被修改。这对于调试时模拟一些极端情况非常的有帮助。这里有一个特殊一点的情况需要指明,如果你尝试通过expression来修改UI可能会失效。

(lldb)expression -- self.view.backgroundColor = [UIColor redColor]

因为执行断点会打断更新UI的进程导致你的修改没有及时渲染出来,执行flush命令可以让机器渲染出你修改后的界面。

实际上一些复杂的调试操作单单靠每次命令行去手动输入指令是非常的繁琐的,仅仅依靠单条指令和它提供的参数选项在一些针对界面的调试上并不能给予我们足够多的支持。令人兴奋的是facebook开源的Chisel为我们提供了更多实用的功能。整个开源库是用Python实现的,基于LLDB 内建的,完整的 Python 支持。这一部分我们后面聊到script指令再细细探讨。

hook的概念


hook翻译成中文是钩子的意思。这个名词在我从事iOS开发的过程中确实没有太多的接触,第一次碰到是在学习Flask框架时遇到的请求钩子。我并不觉得钩子的中文翻译对于我们理解有所帮助,在我初学的阶段甚至给我产生了一定的误解,所以我后续还是以hook来描述。

简单来说hook一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,此时hook函数先得到控制权。这时hook函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。这意味着借助hook函数我们可以在指定在某些特殊的情况下做出一些包括但不限于参数验证,消息拦截等操作来查验当前情况和修改后续程序的运行。

在LLDB中常见的操作有以下这些。本身指令并不复杂,但配合上其它的指令确实在某些情况下能节省我们很多的精力。

  • 设置一个stop-hook用以在每次断点被触发时执行
(lldb)target stop-hook add --one-liner stop-hook
  • 设置一个stop-hook用以在指定函数func内的断点被触发时执行
(lldb) target stop-hook add --name func --one-liner stop-hook
  • 设置一个stop-hook用以在名为className的C类的断点被触发时执行
(lldb)target stop-hook add -- className MyClass --one-liner stop-hook
iOS开发调试 - LLDB使用概览
stop-hook示例

流程控制


Xcode已经为我们提供了可视化的工具,但是如果你习惯了命令行操作不希望双手离开键盘降低你的效率,了解一下也是很有帮助的。

iOS开发调试 - LLDB使用概览
流程控制
  • 继续
(lldb)process continue
(lldb)continue
(lldb)c
  • 下一步
(lldb)thread step-over
(lldb)next
(lldb)n
  • 进入
(lldb)thread step-in
(lldb)step
(lldb)s
  • 跳出
(lldb)thread step-out
(lldb) finish
(lldb)f

除此以外我们还可以通过Thread return来控制流程。该指令有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。当然这也可能会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方一个方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    if ([self isEvenNumber:2])
    {
        NSLog(@"First");
    }
    else
    {
        NSLog(@"Second");
    }
}

- (BOOL)isEvenNumber:(NSInteger)num
{
    if (num % 2 == 0)
    {
        return YES;
    }
    else
    {
        return NO;
    }
}

我们在isEvenNumber:函数中设置断点利用thread return函数返回NO。

(lldb) thread return NO
(lldb) c
Process 4784 resuming
2017-07-22 18:48:14.654 LLDBTest[4784:569378] Second

script


LLDB 有内建的,完整的 Python支持。在LLDB中输入 script,会打开一个 Python REPL。你也可以输入一行 python 语句作为 script 命令的参数,这可以运行 python 语句而不进入REPL

(lldb) script print 'Hello World'
Hello World

借助LLDB提供的Python API我们可以实现很多复杂的功能。这里列举一个简单的例子,将以下内容写入~/myCommands.py文件

def caflushCommand(debugger, command, result, internal_dict): 
    debugger.HandleCommand("e (void)[CATransaction flush]")

在LLDB中执行

command script import ~/myCommands.py

或者将这条指令写入~ /.lldbinit中,每次进入LLDB都会自动执行这些函数。

如果没有~ /.lldbinit 终端执行touch ~ /.lldbinit生成文件
你可以在这里提前设置好一些指令,然后disable。调试过程中再设置enable打开。相信经过整理之后LLDB会让你的调试如鱼得水。

Facebook开源的Chisel就是基于此实现。我们通过brew安装Chisel

brew install Chisel
iOS开发调试 - LLDB使用概览
chise文件层次

fblldbbase.py文件中定义了各个基础类,fblldb.py负责遍历commands文件夹里的各个类来加载自定义的指令。在Chisel基础上我们也可以轻松的自定义指令。在commands文件夹内新建py文件,实现函数lldbcommands返回一个数组,包含对象的类都是FBCommand的子类。

def lldbcommands():
  return [
    FBPrintAccessibilityLabels()
  ]

class FBPrintAccessibilityLabels(fb.FBCommand):
  def name(self):
    return 'pa11y'

  def description(self):
    return 'Print accessibility labels of all views in hierarchy of <aView>'

  def args(self):
    return [ fb.FBCommandArgument(arg='aView', type='UIView*', help='The view to print the hierarchy of.', default='(id)[[UIApplication sharedApplication] keyWindow]') ]

  def run(self, arguments, options):
    forceStartAccessibilityServer();
    printAccessibilityHierarchy(arguments[0])

每一个类都继承自FBCommand,我们需要分别复写以下几个函数

  • def name()
    返回一个字符串表示指令的名称
  • def description()
    返回一个字符串表示指令的描述
  • def args()
    返回一个数组,其中的对象都是类FBCommandArgument构建的,每一个对象表示一个指令的选项参数。
  • def run()
    指令的具体操作

image


image指令是target module指令的缩写,借助它我们能够查看当前的Binary Images相关的信息。日常开发我们主要利用它寻址。

在日常开发的过程中,我们会收集到用户各式各样的crash log。log中会为我们提供崩溃前函数栈的运行情况,每一个函数都会对应一个函数地址。

iOS开发调试 - LLDB使用概览
crash log

要解决问题首先我们需要确定的是程序最后调用了什么函数。由于ALSR的原因crash log中的函数地址我们不能够直接的去使用,我们需要在测试的机器上自己去计算出对应的函数地址。一般情况下crash log中会附带一个Binary Images。我们要利用这个来计算出每一个函数地址相对于所在框架的偏移量。

iOS开发调试 - LLDB使用概览
Binary Images

之后利用image指令来查看本机的Binary Images

(lldb) image list
[  0] 48EA38EC-6E36-3E77-A680-A4D04D3D3868 0x00000001014ac000 /Users/noskthing/Library/Developer/Xcode/DerivedData/LLDBTest-dfmaxwkizubjskftkbnlfzumauje/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest 
[  1] 322C06B7-8878-311D-888C-C8FD2CA96FF3 0x0000000107c66000 /usr/lib/dyld 
[  2] 14AD0238-D077-378B-82A8-AC2D2ADC9DDF 0x00000001014b4000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/dyld_sim 
[  3] 61CD1144-BB93-3571-BDB3-9F9B56CECFFE 0x0000000101543000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/Foundation.framework/Foundation 
[  4] 5F0E622C-86EC-3969-ACFB-CAAA10E21A31 0x0000000101a76000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//usr/lib/libobjc.A.dylib 

有了本机的Binary Images我们就可以通过之前计算出的偏移量来获取本机对应函数的地址。通过image lookup指令查找对应地址的函数就可以确定崩溃前究竟执行了哪些函数

(lldb) image lookup -a 0x1025dd00a
      Address: UIKit[0x00000000001cb00a] (UIKit.__TEXT.__text + 1869978)
      Summary: UIKit`-[UIViewController loadViewIfRequired] + 1219

register


register指令能够获取和修改各个寄存器的信息。

我们需要明白一个典型的CPU是由运算器、控制器、寄存器等器件构成的,而寄存器进行的就是信息存储。我们利用汇编语言来操作寄存器。

iOS开发调试 - LLDB使用概览
汇编

这里是苹果官方文档,介绍的是armv6。需要注意的是自从iPhone 5s之后已经全部换到64-bit,在arm64下整数寄存器的个数已经增加到31个。我们可以通过register read来进行查看。

iOS开发调试 - LLDB使用概览
register

其中x0-x7八个寄存器是用来保存参数的。objc_msgSend会有两个默认参数,这也就意味着x0保存的是self,x1保存的是_cmd。fr对应frame point,lr对应link point,在汇编中分别为x29,x30。最近正在准备一篇从汇编的层面分析objc_msgSend的文章,会在那里结合官方文档详细介绍包括函数调用过程以及各个寄存器的作用。有兴趣的朋友可以先关注一下作者:)

利用runtime动态调用Objective-C任意对象的任意方法,需要为NSInvocation设置参数。参数的index就是从2开始的。具体的实现可以参考Github-Tools中的NSObject+Runtime这个Category的实现。

虽然我们更多的时候只是借助read指令来获取一下当前各个寄存器的信息,但是对于一些替换参数,模拟特殊输入的需求,write指令也是非常的有帮助。

实现一个简单的例子。

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString * str = [NSString stringWithFormat:@"First"];
    NSString * str1 = [NSString stringWithFormat:@"Second"];
    [self printString:str];
    [self printString:str1];
}

- (void)printString:(NSString *)str
{
    NSLog(@"%@",str);
}

函数非常的简单,会依次打印出First和Second。我们首先在第一次调用printString:之前打印一个断点,调用frame variable来查看一下当前两个参数的地址.

(lldb) frame variable
(ViewController *) self = 0x000000010090ac30
(SEL) _cmd = <variable not available>

(NSTaggedPointerString *) str = 0xa000074737269465 @"First"
(NSTaggedPointerString *) str1 = 0xa00646e6f6365536 @"Second"

如果你的参数是unused编译器会把它优化掉,这样你就无法获取它的地址。注意NSString的创建方式,字符串常量创建会把str分配到常量区,查看参数会得到<variable not available>的提示。

之后在printString:中的NSLog之前设置一个断点,continue。在printString:中遇到断点的时候我们执行register read指令。

(lldb) register read
General Purpose Registers:
        x0 = 0x000000010090ac30
        x1 = 0x000000010000995f  "printString:"
        x2 = 0xa000074737269465
        x3 = 0x000000016fdfd876
        x4 = 0x0000000000000000
        x5 = 0x0000000000000000
        x6 = 0x0000000000000064
        x7 = 0x0000000000000000
        x8 = 0x00000001ae36bc20  libsystem_pthread.dylib`_thread + 224
        x9 = 0x00000001ae364fec  runtimeLock + 28
       x10 = 0x00000001ae364ff0  runtimeLock + 32
       x11 = 0x003c6d01003c6d80
       x12 = 0x0000000000000000
       x13 = 0x00000000003c6d00
       x14 = 0x00000000003c6e00
       x15 = 0x00000000003c6dc0
       x16 = 0x00000000003c6d01
       x17 = 0x0000000100007250  test`-[ViewController printString:] at ViewController.m:103
       x18 = 0x0000000000000000
       x19 = 0x000000010090ac30
       x20 = 0xa00646e6f6365536
       x21 = 0xa000074737269465
       x22 = 0x000000010000995f  "printString:"
       x23 = 0x0000000000000000
       x24 = 0x0000000000000010
       x25 = 0x0000000000000258
       x26 = 0x000000018ed0e90e  "window"
       x27 = 0x0000000000000001
       x28 = 0x0000000000000000
        fp = 0x000000016fdfddc0
        lr = 0x000000010000721c  test`-[ViewController viewDidLoad] + 156 at ViewController.m:100
        sp = 0x000000016fdfddb0
        pc = 0x000000010000725c  test`-[ViewController printString:] + 12 at ViewController.m:106
      cpsr = 0x60000000

对比地址可以发现x0保存的是viewController的地址,x1注明了是函数printString:的地址,而x2就是str的地址。我们通过register write指令来修改x2的值。

(lldb) register write x2 0xa00646e6f6365536

contine之后你会发现打印出的不是First而是Second。

如果有朋友对汇编和函数调用感兴趣,我会在之后结合objc_msgSend汇编部分的代码在另一篇文章里来做个介绍。

结语


文章的目的是希望给大家展示LLDB强大的能力以及命令行的优点,但实际以上篇幅介绍的只是冰山一角。希望这篇文章能够给大家一些帮助,来更多的了解LLDB。

以下是一些有关LLDB的资料和文档

  • LLDB Documentation
  • Dancing in the Debugger — A Waltz with LLDB
  • WWDC 2012: Debugging with LLDB
  • WWDC 2014: Advanced Swift debugging in LLDB
  • WWDC 2014: Introduction to LLDB and the Swift REPL
  • WWDC 2015: What’s new in LLDB
    希望能够对你有所帮助

LLDB+debugserver调试第三方应用

前言

本文主要介绍越狱手机通过LLDB、debugserver调试第三方应用,关于LLDB、debugserver的内容,前面在学习中也有提到过:LLDB调试命令、ptrace反调试

一、简介

debugserver就像是远程服务的控制台应用,主要给gdb或者lldb调试,手机设备开启debugserver服务,本地Mac通过LLDB发送指令给debugserver,debugserver在真机调试的时候被安装到手机上,可以在手机上/Developer/usr/bin/debugserver找到。日常正向开发就是Xcode内部的LLDB调起debugserver进程来调试我们自己的App。

二、debugserver命令选项

iOS端调起debugserver

debugserver host:port [program-name program-arg1 program-arg2 …]

选项 含义
-a 进程 将debugserver依附到指定进程,通过PID或可执行文件名称
-d integer 指定等待时间
-i integer 指定等待时间间隔
-l filename 日志文件。将文件名设置为stdout以记录标准输出
-t 使用task ID代替PID
-v 日志模式

三、配置debugserver

debugserver默认只能调试自己开发的应用,调试其他应用会抛异常unable to start the exception thread。默认的debugserver缺少task_for_pid()权限,因此需要给debugserver赋予task_for_pid权限。

LLDB+debugserver调试第三方应用
unable to start the exception thread异常

(一)、方法一 使用ldid赋予权限

(1)Mac端找到debugserver文件
debugserver可以在/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/你越狱手机版本/DeveloperDiskImage.dmg里的usr/bin路径找到,拷贝出来。由于ldid不支持胖二进制文件,因此需要先瘦身:

lipo -thin armv7 debugserver -output ~/debugserver (注意,本文使用iPhone5测试,因此对应armv7,请自行对应架构)

LLDB+debugserver调试第三方应用
image.png

(2)新建一个xml文件

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.springboard.debugapplications</key>
        <true/>
        <key>get-task-allow</key>
        <true/>
        <key>task_for_pid-allow</key>
        <true/>
        <key>run-unsigned-code</key>
        <true/>
    </dict>
</plist>
LLDB+debugserver调试第三方应用
xml

(3)赋予权限

ldid -Sxml全路径 debugserver全路径
例如:ldid -S/Users/kinken_yuen/Desktop/ent.xml /Users/kinken_yuen/Desktop/debugserver

LLDB+debugserver调试第三方应用
赋予权限

(4)拷贝配置后的debugserver到手机

注意: 手机上的/Developer目录实际上是只读的,你不能直接将debugserver复制回去,放到别的地方使用,可以是:scp -P 2222 ./debugserver root@127.0.0.1:/usr/bin/debugserver
下面的方法二最后也需要如此操作

(二)、方法二 使用codesign赋予权限

(1)拷贝出debugserver
先将debugserver从手机复制到Mac

scp -P 2222 root@localhost:/Developer/usr/bin/debugserver ./

因为是从当前越狱手机拷贝,因此不需要再瘦身

LLDB+debugserver调试第三方应用
debugserver文件

(1)使用entitlements权限文件签名
新建entitlements.plist,写入内容

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.springboard.debugapplications</key>
    <true/>
    <key>run-unsigned-code</key>
    <true/>
    <key>get-task-allow</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
</dict> 
</plist>

(3)赋予权限

codesign -s - --entitlements entitlements.plist -f debugserver

LLDB+debugserver调试第三方应用
签名权限

(4)拷贝配置后的debugserver到手机
参考方法一

四、附加到进程

(一)、通过WIFI连接

(1)、手机端开启debugserver

LLDB+debugserver调试第三方应用

(2)、Mac端LLDB连接debugserver

LLDB+debugserver调试第三方应用
进入lldb

process connect connect://设备IP地址:1234(对应于手机开启的端口号)

LLDB+debugserver调试第三方应用
连接debugserver

(二)、通过USB连接

(1)手机端开启debugserver(同上)
(2)Mac端

  • 先做端口转发

iproxy 1234 1234

接着

process connect connect://127.0.0.1:1234
process connect connect://localhost:1234

LLDB+debugserver调试第三方应用
USB连接

五、小结

第三方应用动态调试的方式不止一种,如重签名后用Xcode调试非越狱Cycript调试越狱Cycript调试,以及上面的lldb + debugserver调试。

dumpdecrypted砸壳

一、原理

动态注入可执行文件Mach-o,从内存dump出解密的内容

github : dumpdecrypted

二、编译dumpdecrypted

cd到目录 make 得到一个dumpdecrypted.dylib

PS:后面注入过程出现一个问题:

dumpdecrypted砸壳
因为没有签名dylib,注入失败

解决方案:
列出可签名证书

security find-identity -v -p codesigning

为dumpecrypted.dylib签名

codesign --force --verify --verbose --sign "iPhone Developer: xxx xxxx (xxxxxxxxxx)" dumpdecrypted.dylib

三、砸壳(解密)

1.定位目标应用的可执行文件

手机退出其他应用,只打开目标应用(以微信为例),电脑ssh到手机,使用ps -e(ps命令需要手机安装插件adv-cmds)命令查看进程:

dumpdecrypted砸壳
定位微信进程

2.获取应用沙盒目录Documents

动态库注入解密需要写到同一目录下,而应用存在沙盒中,通过BundleID调用私有API获取Documents目录:
cat /var/containers/Bundle/Application/BE4C2082-083B-4DE4-924D-EDCA98EB1701/WeChat.app/Info.plist | grep CFBundleIdentifier -A 1

dumpdecrypted砸壳
获取bundleID

3.调用私有API获取沙盒Documents目录

新建iOS工程,在目标应用设备上运行

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSString *bundleID = @"com.tencent.xin";
    NSURL *dataURL = [[NSClassFromString(@"LSApplicationProxy") performSelector:@selector(applicationProxyForIdentifier:) withObject:bundleID] performSelector:@selector(dataContainerURL)];
    NSLog(@"%@",[dataURL.absoluteString stringByAppendingString:@"/Documents"]);
    return YES;
}

控制台输出:
2018-11-08 23:13:26.751557 decrypt[623:18364] file:///private/var/mobile/Containers/Data/Application/02A7FF42-8A6B-45BA-8C25-99760F0311C7/Documents
对应Documents目录:
/var/mobile/Containers/Data/Application/02A7FF42-8A6B-45BA-8C25-99760F0311C7/Documents

4.复制dylib到Documents目录并进行解密

  • 在dumpdecrypted.dylib目录下打开终端,复制dylib使用以下命令(自行修改不同参数):
    scp ./dumpdecrypted.dylib root@192.168.10.170:/var/mobile/Containers/Data/Application/02A7FF42-8A6B-45BA-8C25-99760F0311C7/Documents

  • ssh到手机端解密
    1.cd /var/mobile/Containers/Data/Application/02A7FF42-8A6B-45BA-8C25-99760F0311C7/Documents
    2.DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/containers/Bundle/Application/BE4C2082-083B-4DE4-924D-EDCA98EB1701/WeChat.app/WeChat

    dumpdecrypted砸壳
    dump出解密文件
  • 解密文件就在Documents目录,拷贝出来玩耍吧
    Mac端:
    scp root@192.168.10.170:/var/mobile/Containers/Data/Application/02A7FF42-8A6B-45BA-8C25-99760F0311C7/Documents/WeChat.decrypted ~/desktop

  • 查看解密文件

    dumpdecrypted砸壳
    cryptid为0表示未加密

上述操作比较原始,后面会有更快的砸壳方法,庆哥的改版dumpdecrypted(试过一遍没搞懂),庆哥推荐使用 frida-ios-dump,传送门:使用frida-ios-dump快速通过越狱设备砸壳

Clutch砸壳

一、准备工作

Clutch

git clone https://github.com/KJCracks/Clutch.git

设置生成所有架构(Build Active Architecture Only – NO),Xcode编译,我遇到了编译失败,有待解决:

Clutch砸壳
Xcode编译失败

按照github README处理
终端:

killall Xcode

cp /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist ~/

sudo /usr/libexec/PlistBuddy -c "Set :DefaultProperties:CODE_SIGNING_REQUIRED NO" /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist

sudo /usr/libexec/PlistBuddy -c "Set :DefaultProperties:AD_HOC_CODE_SIGNING_ALLOWED YES" /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist

接着到项目目录、终端:

xcodebuild clean build

会有build文件夹生成,里面有编译好的可执行的Clutch,拷贝手机端:
注意:命令采用USB ssh方式,并且做了端口转发

scp -P 2222 ./build/Clutch root@localhost:/usr/bin/Clutch

二、在手机端进行砸壳

我尝试了两次对Wechat砸壳,手机都死机了,终端提示以下:

Clutch砸壳
砸壳失败

接着我又直接在手机终端上砸,同样失败,终端闪退,手机黑屏死机,又要强制重启重新越狱…

对于失败的猜测,可能是自己编译Clutch可能架构不对,无法正确使用,那么我直接使用作者Release出来的,换了个App,成功。后续学习过程了解到,Clutch会对App内部的Framework也解密,所以可能会出错,像微信、支付宝就会出错。

Clutch砸壳
换个了App砸

再砸一次WeChat,失败!

学习对比了几个砸壳的方式,个人感觉frida-ios-dump最方便,直接在Mac端拿到解密的可执行文件。

frida-ios-dump砸壳

一、越狱设备端配置

  • 安装frida
    cydia添加源:http://build.frida.re/
frida-ios-dump砸壳
安装Frida

二、Mac端配置frida

  • Mac安装frida

pip install frida-tools

PS:有可能Mac没有安装pip

sudo easy_install pip

  • 克隆frida-ios-dump并且安装依赖

git clone https://github.com/AloneMonkey/frida-ios-dump.git
sudo pip install -r requirements.txt --upgrade(需要Python2.7,Python3请检出3.x分支,github README.md有说明)

配置过程可能出现异常:

frida-ios-dump砸壳
sudo pip install -r requirements.txt –upgrade提示异常

使用sudo -H pip install -r requirements.txt --upgrade解决

  • 使用usbmuxd做端口转发

iproxy 2222 22

  • 列出Display name(应用名称)

frida-ps -U

frida-ios-dump砸壳
target:微信

三、dump出脱壳的ipa

./dump.py 微信

参数用Display name或者Bundle ID都行

PS:这一步可能会提示认证失败,原因是没有将Mac公钥传到越狱设备上,庆哥github上面也有提示。

nice~

frida-ios-dump砸壳
得到砸壳的ipa
frida-ios-dump砸壳
验证是否加密