Apple高级调试与逆向工程

(十)自定义LLDB命令 基础

2020-03-08  本文已影响0人  收纳箱

1. 自定义LLDB命令

我们已经学了一些基础的LLDB命令。现在是时候吧这些只是组合起来创造一些强力的复杂调试脚本了。LLDB允许你通过Python来进行大部分调试,辅助你解开那些隐藏在背后的秘密。

1.1 脚本桥接

LLDB有几种方法可以自定的命令。之前我们学习了command aliascommand regex。下面,权衡了便利与复杂的就是脚本桥接(script bridging)。用它你可以做到几乎你所有想做的事情。它是一个PythonLLDB调试器的接口,用来扩展调试功能,完成更复杂的调试需求。

首先必须要提到一个脚本:

/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/Resources/Python/lldb/macosx/heap.py

这个脚本做了这些事情:可以找到调用栈中所有malloc的对象(malloc_info -s);可以获取所有NSObject特定子类的所有示例(obj_refs -O);可以找到所有指向特定内存地址的指针(ptr_refs);找到内存中的所有C字符串(cstr_ref)。

你可以通过下面的方式来加载这个脚本。

command script import lldb.macosx.heap

然而,这个脚本因为编译器改变而代码没有改变,导致它有一些功能没法使用了。

Python 101

LLDB脚本桥接是Python到调试器的接口。你可以在LLDB中加载并执行Python脚本。在这些Python脚本中,你需要导入lldb模块来和调试器进行交互。

我们先来看看LLDBPython版本。

 ~> lldb
(lldb) script import sys
(lldb) script print (sys.version)
3.7.3 (default, Dec 13 2019, 19:58:14)
[Clang 11.0.0 (clang-1100.0.33.17)]

~> python3 --version
Python 3.7.3
Python中玩一玩
 ~> python3
Python 3.7.3 (default, Dec 13 2019, 19:58:14)
[Clang 11.0.0 (clang-1100.0.33.17)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> h = "hellow world"
>>> h
'hellow world'
>>> h.split(" ")
['hellow', 'world']

如果你想查看Python类的类型,加上.__class__就行了。

>>> h.split(" ").__class__
<class 'list'>
>>> h.__class__
<class 'str'>

如果你想查看帮助文档,利用help命令就可以了。

>>> help (str)
>>> help (str.split)

如果你想定义一个函数怎么做呢?

>>> def test(a):
...

省略号表示你开始创建一个函数了。输入两个空格,然后输入print(a + " world!")Python是通过缩进来判断作用域的,如果你的缩进不对,Python的函数是会报错的。再次点击回车来退出函数的编写。

>>> def test(a):
...   print(a + " world!")
...
>>> test("hello")
hello world!
创建你的第一个LLDB Python脚本

首先我们创建一个文件夹~/lldb

~> mkdir ~/lldb

你喜欢用什么编辑器都可以,创建~/lldb/helloworld.py

 def your_first_command(debugger, command, result, internal_dict): 
     print ("hello world!")

函数中的参数,你可以先不管,就是由LLDB传过来的参数。

~> lldb
(lldb) command script import ~/lldb/helloworld.py

我们在LLDB中导入这个文件,如果没有问题的话,什么输出都不会有。因为它只是把文件桥接过来了。如果你要调用里面函数怎么办呢?你首先需要导入对应的模块。

(lldb) script import helloworld

你可以通过列出模块汇总所有函数来验证是否导入成功。

(lldb) script dir(helloworld)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'your_first_command']

在里面看到了我们定义的your_first_command方法。

那怎么在LLDB中调用这个函数呢?

(lldb) command script add -f helloworld.your_first_command yay
(lldb) yay
hello world!

我们通过command script addhelloworld模块中的your_first_command方法定义为LLDB命令yay了。-f表示你要添加的是一个python方法。

更有效地设置命令

如果你写了很多自定义脚本了,你肯定不希望每次LLDB启动的时候,都靠自己来进行导入。幸好,LLDB有一个叫__lldb_init_module的模块,来帮助你进行加载。

我们在helloworld.py追加一下代码。

def your_first_command(debugger, command, result, internal_dict): 
    print ("hello world!")

#LLDB加载时会自动执行
def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f helloworld.your_first_command yay')

你传入了一个debugger就是SBDebugger的一个实例,然后你调用了它的HandleCommand方法。这个方法和在LLDB进行输入的效果差不多。

保存helloworld.py。然后在~/.lldbinit中添加导入代码。

command script import ~/lldb/helloworld.py

然后在终端新的tab中启动lldb

~> lldb
(lldb) yay
hello world!

1.2 调试脚本桥接

用pdb调试你的调试脚本

helloworld.py脚本your_first_command改成下面这样。

def your_first_command(debugger, command, result, internal_dict): 
    import pdb; pdb.set_trace()
    print ("hello world!")

然后启动LLDB

~> lldb
(lldb) yay woot
> /Users/xxx/lldb/helloworld.py(3)your_first_command()
-> print ("hello world!")
(Pdb)

我们可以看到pdb已经断在了your_first_commandprint这一行。当用Python来创建一个LLDB命令时,会传入几个特别的参数:debuggercommandresult

我们先来试试command。这个命令会列出你传给yay命令的所有命令。因为这里没有处理任何命令的逻辑,所以yay会自动忽略这些输入。

-> print ("hello world!")
(Pdb) command
'woot'

我们再来看看result。输出是一个SBCommandReturnObject实例对象。你可以通过它知道代码在LLDB命令中执行是否成功。另外,你可以添加一些信息。这些信息会在命令执行完毕时显示出来。

(Pdb) result
<lldb.SBCommandReturnObject; proxy of <Swig Object of type 'lldb::SBCommandReturnObject *' at 0x10cd4ce10> >
result.AppendMessage("2nd hello world!")
(Pdb) result.AppendMessage("2nd hello world!")

我们先还是保持断住的。先保持这样,我们来看看debugger。输出是一个SBDebugger的实例对象。

(Pdb) debugger
<lldb.SBDebugger; proxy of <Swig Object of type 'lldb::SBDebugger *' at 0x10cf42a20> >

输入cpdb恢复运行。

(Pdb) c
hello world!
2nd hello world!
pdb死后调试

根据错误类型,pdb有一个很吸引人的选项,让你可以探究问题发生时的调用栈,但它仅在发生异常时有效。

这里我们重新建一个python文件findclass.py

import lldb 

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f findclass.findclass findclass')


def findclass(debugger, command, result, internal_dict):
    """
    The findclass command will dump all the Objective-C runtime classes it knows about.
    Alternatively, if you supply an argument for it, it will do a case sensitive search
    looking only for the classes which contain the input. 

    Usage: findclass  # All Classes
    Usage: findclass UIViewController # Only classes that contain UIViewController in name
    """ 


    codeString = r'''
    @import Foundation;
    int numClasses;
    Class * classes = NULL;
    classes = NULL;
    numClasses = objc_getClassList(NULL, 0);
    NSMutableString *returnString = [NSMutableString string];
    classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    for (int i = 0; i < numClasses; i++) {
      Class c = classes[i];
      [returnString appendFormat:@"%s,", class_getName(c)];
    }
    free(classes);
    
    returnString;
    '''

    res = lldb.SBCommandReturnObject()
    debugger.GetCommandInterpreter().HandleCommand("expression -lobjc -O -- " + codeString, res)
    if res.GetError(): 
        raise AssertionError("Uhoh... something went wrong, can you figure it out? :]")
    elif not res.HasResult():
        raise AssertionError("There's no result. Womp womp....")
        
    returnVal = res.GetOutput()
    resultArray = returnVal.split(",")
    if not command: # No input supplied 
        print (returnVal.replace(",", "\n").replace("\n\n\n", ""))
    else: 
        filteredArray = filter(lambda className: command in className, resultArray)
        filteredResult = "\n".join(filteredArray)
        result.AppendMessage(filteredResult)

下面我们用LLDB调试Photos

~> lldb -n Photos
(lldb) command script import ~/lldb/findclass.py
(lldb) help findclass
     For more information run 'help findclass'  Expects 'raw' input (see 'help
     raw-input'.)

Syntax: findclass

    The findclass command will dump all the Objective-C runtime classes it
    knows about.
    Alternatively, if you supply an argument for it, it will do a case
    sensitive search
    looking only for the classes which contain the input.

    Usage: findclass  # All Classes
    Usage: findclass UIViewController # Only classes that contain
    UIViewController in name

(lldb) findclass
Traceback (most recent call last):
  File "/Users/ycpeng/lldb/findclass.py", line 40, in findclass
    raise AssertionError("Uhoh... something went wrong, can you figure it out? :]")
AssertionError: Uhoh... something went wrong, can you figure it out? :]

这个脚本的作者提供信息没啥用,但至少它抛出了一个异常。我们就可以用pdb查看错误发生时的调用栈。

(lldb) script import pdb
(lldb) findclass
Traceback (most recent call last):
  File "/Users/ycpeng/lldb/findclass.py", line 40, in findclass
    raise AssertionError("Uhoh... something went wrong, can you figure it out? :]")
AssertionError: Uhoh... something went wrong, can you figure it out? :]
(lldb) script pdb.pm()
> /Users/ycpeng/lldb/findclass.py(40)findclass()
-> raise AssertionError("Uhoh... something went wrong, can you figure it out? :]")

我们甚至可以在pdb中查看源代码。

# 表示列出1~50行代码
(Pdb) l 1, 50

其中18~35行是一个长字符串,就是这个命令的核心逻辑。

# 直接打印,可能不太好看
(Pdb) codeString
'\n    @import Foundation;\n    int numClasses;\n    Class * classes = NULL;\n    classes = NULL;\n    numClasses = objc_getClassList(NULL, 0);\n    NSMutableString *returnString = [NSMutableString string];\n    classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);\n    numClasses = objc_getClassList(classes, numClasses);\n\n    for (int i = 0; i < numClasses; i++) {\n      Class c = classes[i];\n      [returnString appendFormat:@"%s,", class_getName(c)];\n    }\n    free(classes);\n    \n    returnString;\n    '

# 我们用print打印,就会好看很多
(Pdb) print(codeString)

    @import Foundation;
    int numClasses;
    Class * classes = NULL;
    classes = NULL;
    numClasses = objc_getClassList(NULL, 0);
    NSMutableString *returnString = [NSMutableString string];
    classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    for (int i = 0; i < numClasses; i++) {
      Class c = classes[i];
      [returnString appendFormat:@"%s,", class_getName(c)];
    }
    free(classes);

    returnString;

我们可以看到这是一段OC代码。通过运行时获取所有的类。

我们来看一下报错的地方。我们还能看到40行的断点->

 37         res = lldb.SBCommandReturnObject()
 38         debugger.GetCommandInterpreter().HandleCommand("expression -lobjc -O -- " + codeString, res)
 39         if res.GetError():
 40  ->         raise AssertionError("Uhoh... something went wrong, can you figure it out? :]")
 41         elif not res.HasResult():
 42             raise AssertionError("There's no result. Womp womp....")

我们来看看这里res.GetError()到底报了什么错。

(Pdb) print(res.GetError())
error: warning: got name from symbols: classes
error: 'objc_getClassList' has unknown return type; cast the call to its declared return type
error: 'objc_getClassList' has unknown return type; cast the call to its declared return type
error: 'class_getName' has unknown return type; cast the call to its declared return type

这个错误看起来就就像平时LLDB打印出来错误的样子了。我们可以看到objc_getClassListclass_getName返回了未知类型。

我们查看一下文档:

int objc_getClassList(Class *buffer, int bufferCount);
const char * class_getName(Class cls);

然后对应强转一下函数的返回类型:

    codeString = r'''
    @import Foundation;
    int numClasses;
    Class * classes = NULL;
    classes = NULL;
    numClasses = (int)objc_getClassList(NULL, 0);
    NSMutableString *returnString = [NSMutableString string];
    classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
    numClasses = (int)objc_getClassList(classes, numClasses);

    for (int i = 0; i < numClasses; i++) {
      Class c = classes[i];
      [returnString appendFormat:@"%s,", (char *)class_getName(c)];
    }
    free(classes);
    
    returnString;
    '''

保存代码之后,Ctrl + D退出pdb

(lldb) command script import ~/lldb/findclass.py
(lldb) findclass
//打印了很多OC类

我们也可以限制一下我们关心的类型。比如下面我们打印包含ViewController的类。

(lldb) findclass ViewController
NSServiceViewControllerUnifyingProxy
IPXFeedViewControllerSpec
IPXFeedViewControllerMacSpec
...
expression的调试选项

expression有个调试选项--debug或者-g
findclass.py中38行

debugger.GetCommandInterpreter().HandleCommand("expression -lobjc -O -- " + codeString, res)

加入调试选项-g

debugger.GetCommandInterpreter().HandleCommand("expression -lobjc -g -O -- " + codeString, res)

然后再执行一下

(lldb) command script import ~/lldb/findclass.py
(lldb) findclass
Traceback (most recent call last):
  File "/Users/ycpeng/lldb/findclass.py", line 40, in findclass
    raise AssertionError("Uhoh... something went wrong, can you figure it out? :]")
AssertionError: Uhoh... something went wrong, can you figure it out? :]
Process 14327 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal 2147483647
    frame #0: 0x000000010b342000 $__lldb_expr1`$__lldb_expr($__lldb_arg=0x0000000000000000) at lldb-566369.expr:42
   39
   40   void
   41   $__lldb_expr(void *$__lldb_arg)
-> 42   {
   43       ;
   44       /*LLDB_BODY_START*/
   45
Target 0: (Photos) stopped.

现在你就可以用LLDB进行调试了。你需要查看源代码需要用到命令source list,或者list,甚至更简单l

(lldb) l
   46       @import Foundation;
   47       int numClasses;
   48       Class * classes = NULL;
   49       classes = NULL;
   50       numClasses = (int)objc_getClassList(NULL, 0);
   51       NSMutableString *returnString = [NSMutableString string];
   52       classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);

或者你还可以使用LLDBgui命令。

(lldb) gui
LLDB GUI

接下来你就可以点击N键进行单步调试,用S键进行step into了。如果你调试完毕,记得把-g删掉。

1.3 lldb模块中重要的类

通过LLDB来探索lldb模块

我们每次修改~/.lldbinit都要输入command source ~/.lldbinit比较麻烦。我们在~/.lldbinit添加一个命令。

command alias reload_script command source ~/.lldbinit

我们新建一个tvOS的项目Meh,语言选Swift。设置一个断点,并输入lldb.debugger

lldb.debugger
LLDB有几个可以方便访问的全局变量:
lldb.SBDebugge -> lldb.debugger
lldb.SBTarget -> lldb.target
lldb.SBProcess -> lldb.process
lldb.SBThread -> lldb.thread
lldb.SBFrame -> lldb.frame

我们可以看看我们当前的target

//直接打印这个类可能看不出什么
(lldb) script lldb.target
<lldb.SBTarget; proxy of <Swig Object of type 'lldb::SBTarget *' at 0x11556d420> >
//我们来print一下
(lldb) script print(lldb.target)
Meh

print命令可以打印一个实例的概况。就像po命令调用了OC中NSObjectdescription方法。

再看看其他几个命令的打印效果。

// 打印当前进程的信息
(lldb) script print(lldb.process)
SBProcess: pid = 14955, state = stopped, threads = 8, executable = Meh
// 打印当前线程的信息,还有我们的断点
(lldb) script print(lldb.thread)
thread #1: tid = 0xc525e, 0x000000010e81fad0 Meh`ViewController.viewDidLoad(self=0x00007fe7dfe04b10) at ViewController.swift:12, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
// 打印当前栈帧的信息
(lldb) script print(lldb.frame)
frame #0: 0x000000010e81fad0 Meh`ViewController.viewDidLoad(self=0x00007fe7dfe04b10) at ViewController.swift:12
学习和查看脚本桥接类的文档

通过help命令可以查看帮助文档。

(lldb) script help(lldb.target)
(lldb) script help(lldb.SBTarget)

为了方便阅读,我们可以在~/.lldbinit添加:

command regex gdocumentation 's/(.+)/script import os; os.system("open https:" + unichr(47) + unichr(47) + "lldb.llvm.org" + unichr(47) + "python_reference" + unichr(47) + "lldb.%1-class.html")/'

输入我们要查询的东西,然后直接就可以跳转到对应的网址了

(lldb) gdocumentation SBTarget
创建breakAfterRegex命令

如何设计一个命令,在函数之后立即停止,打印出返回值,然后继续?

我们来创建一个~/lldb/BreakAfterRegex.py

  1. 使用LLDB创建正则断点。
  2. 添加一个断点操作动作执行到当前帧完成。
  3. 利用寄存器的知识打印出正确的寄存器中的返回值。
import lldb

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f BreakAfterRegex.breakAfterRegex bar')

def breakAfterRegex(debugger, command, result, internal_dict):
    print ("yay. basic script setup with input: {}".format(command))

这里添加了一个名为bar的命令。该命令由模块BreakAfterRegex中的breakAfterRegex实现。

~/.lldbinit添加。

command script import ~/lldb/BreakAfterRegex.py

然后我们在项目中,输入

(lldb) reload_script
(lldb) bar UIViewController test -a -b
yay. basic script setup with input: UIViewController test -a -b

LLDB脚本中的输出是提供给它的参数。基本骨架已经准备好了。现在是编写基于输入创建断点的代码的时候了。返回BreakAfterRegex.py并找到def breakAfterRegex(debugger, command, result, internal_dict):。删除print语句并将其替换为以下逻辑:

def breakAfterRegex(debugger, command, result, internal_dict):
    #1 使用传入的正则参数创建断点。这个断点对象将是SBBreakpoint类型。
    target = debugger.GetSelectedTarget()
    breakpoint = target.BreakpointCreateByRegex(command)
    #2 如果断点创建失败,脚本将警告您它找不到任何可以中断的地方。否则,将打印出断点对象。
    if not breakpoint.IsValid() or breakpoint.num_locations == 0:
        result.AppendWarning("Breakpoint isn't valid or hasn't found any hits")
    else:
        result.AppendMessage("{}".format(breakpoint))
    #3 设置断点,以便每当断点命中时的回调函数。
    breakpoint.SetScriptCallbackFunction("BreakAfterRegex.breakpointHandler")

def breakpointHandler(frame, bp_loc, dict):
    # 获取函数名,并打印
    function_name = frame.GetFunctionName()
    print("stopped in: {}".format(function_name))
    return True

注意在函数的末尾返回True。返回True将导致程序停止执行。返回False,甚至省略return语句都会导致程序在执行此方法后继续运行。

为断点创建回调函数时,要实现的方法签名不同。刚刚的回调就包括SBFrameSBBreakpointLocationPython字典。

SBFrame表示已停在其中的栈帧。SBBreakpointLocation是在SBBreakpoint中找到一个断点的实例。这是非常有意义的。因为对于一个断点,可能有很多点击。特别是如果尝试中断一个经常实现的函数,例如main,或者使用了会匹配很多结果的正则表达式。

下面显示了当在特定函数上停止时,类的简化交互:


断点时类的交互

在断点回调函数中,SBFrameSBBreakpointLocation是大多数重要lldb类的线索。可以通过SBFrameSBFrameSBModule的引用来获取所有主要类实例。

请记住,不要在脚本中使用lldb.frame或其他全局变量。因为它们在脚本中执行时,信息可能比较落后。因此必须遍历以framebc_loc开头的变量才能获取到所需类的实例。

我们来试一下。

(lldb) reload_script
(lldb) bar NSObject.init\]
SBBreakpoint: id = 3, regex = 'NSObject.init\]', locations = 2

继续执行,并使用tvOS模拟器的远程遥控器点击一下触发断点。如果在触发断点时遇到问题,一种可靠的方法是导航到模拟器的主屏幕(⌘ + Shift + H)

断点

我们已经成功地创建了一个正则断点命令。现在,我们已经停在了NSObject其中一个init方法,可能是类方法或实例方法。而且这很可能是NSObject的一个子类。我们将使用LLDBPython脚本中手动地重现这个操作。

我们结束执行这个方法。因为我们使用的是tvOS模拟器,它的架构是x64,所以我们需要使用RAX寄存器打印出LLDBNSObjectinit的返回值。

(lldb) finish
(lldb) po $rax
<UIViewControllerBuiltinTransitionViewAnimator: 0x600001c82520>

打开BreakAfterRegex.py并重写breakpointHandler函数。

def breakpointHandler(frame, bp_loc, dict):
    #1 文档信息
    '''The function called when the regularexpression breakpoint gets triggered'''
    #2 从SBFrame出发,通过引用获取到SBDebugger和SBThread的实例
    thread = frame.GetThread()
    process = thread.GetProcess()
    debugger = process.GetTarget().GetDebugger()
    #3 获取父函数的名称
    function_name = frame.GetFunctionName()
    #4 让调试器不要异步执行
    debugger.SetAsync(False)
    #5 退出这个方法,将不再处于当前的栈帧中
    thread.StepOut()
    #6 调用evaluateReturnedObject方法获取适当的输出信息
    output = evaluateReturnedObject(debugger, thread, function_name)
    if output is not None:
        print(output)
    return False

下面我们来实现evaluateReturnedObject方法。

def evaluateReturnedObject(debugger, thread, function_name): 
    '''Grabs the reference from the return register and returns a string from the evaluated value.
    TODO ObjC only
    '''
    #1 实例化SBCommandReturnObject
    res = lldb.SBCommandReturnObject()
    #2 获取一些后面要用的实例
    interpreter = debugger.GetCommandInterpreter()
    target = debugger.GetSelectedTarget()
    frame = thread.GetSelectedFrame()
    parent_function_name = frame.GetFunctionName()
    #3 创建要执行的表达式,该表达式将输出返回值
    expression = 'expression -lobjc -O -- {}'.format(getRegisterString(target))
    #4 通过SBCommandInterpreter执行表达式
    #  它允许我们控制输出的位置,而不是立即将其传递到stderr或stdout。
    interpreter.HandleCommand(expression, res)
    #5 查看执行表达式之后是否有返回值
    if res.HasResult():
        #6 把断住的函数名、寄存器获取的对象和前一帧的函数名,格式化为字符串并返回该字符串。
        output = '{}\nbreakpoint: {}\nobject: {}\nstopped: {}'.format(
            '*' * 80, 
            function_name, 
            res.GetOutput().replace('\n', ''), 
            parent_function_name)
        return output
    else:
        #7 如果不需要输出,返回None
        return None

还剩寄存器读取没有完成。

# 根据架构返回需要读取的寄存器名
def getRegisterString(target): 
    triple_name = target.GetTriple() 
    if "x86_64" in triple_name:
        return "$rax"
    elif "i386" in triple_name:
        return "$eax"
    elif "arm64" in triple_name:
        return "$x0"
    elif "arm" in triple_name:
        return "$r0"
    raise Exception('Unknown hardware. Womp womp')

我们现在来试试!reload_script重新加载脚本,再删除现在的所有断点,重新执行bar NSObject.init\]。然后continue几次,可以看到一些有用的信息了。

(lldb) reload_script
(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] 
All breakpoints removed. (3 breakpoints)
(lldb) bar NSObject.init\]
SBBreakpoint: id = 4, regex = 'NSObject.init\]', locations = 2
********************************************************************************
breakpoint: -[NSObject init]
object: <RBSXPCMessageReply: 0x6000012e1240>
stopped: -[RBSXPCMessageReply _initWithMessage:]
********************************************************************************
breakpoint: -[NSObject init]
object: 105553124551936
stopped: -[BSXPCCoder initWithMessage:]
********************************************************************************
breakpoint: -[NSObject init]
object: 105553124551936
stopped: objc_object::sidetable_retain()
上一篇下一篇

猜你喜欢

热点阅读