美文网首页Apple高级调试与逆向工程
(十一)自定义LLDB命令 选项和参数

(十一)自定义LLDB命令 选项和参数

作者: 收纳箱 | 来源:发表于2020-03-08 18:36 被阅读0次

    1. 脚本桥接之选项和参数

    创建自定义调试命令时,通常需要根据提供给命令的选项或参数稍微调整功能。一个自定义的LLDB命令只能用一种方式来完成一项工作,那就太无趣了。

    下面我们将探索如何将可选参数和必传参数传递给自定义命令,以更改自定义LLDB脚本中的功能或逻辑。我们将继续使用在前一篇文章中创建的bar命令。通过添加逻辑来处理脚本中的选项来丰富bar命令,令其具有处理以下可选参数的逻辑:

    • 非正则表达式搜索:使用-n--non_regex选项,使得bar命令使用非正则表达式进行断点搜索。此选项不接受任何其他参数。
    • 按模块筛选:使用-m--module选项,将只搜索特定模块中的断点。此选项需要一个指定模块名称的附加参数。
    • 按条件停止:使用-c--condition选项,bar命令将在当前函数执行完时判断给定条件。如果为真,将停止执行。如果为假,则将继续执行。此选项需要一个附加参数,且该参数是一条字符串代码,执行完后返回Objective-C BOOL类型。

    我们将使用RWDevCon项目。这个应用程序是RWDevcon会议的配套应用程序。每年都有一个传统,就是在雷·温德里奇生气之前,看看你能摸多少次他的肩膀。这里用到的项目是从84167c68这个提交派生出来。可以在这里获得更新的版本:https://github.com/raywenderlich/RWDevCon-App

    不需要探索任何源代码。借助bar命令,将能够使用智能断点查询探索我们感兴趣不同的项目。但在能够做到这一点之前,先来谈谈如何使这个bar命令更加强大。

    Python的optparse模块

    我们拥有Python及其模块的全部功能,可以随意使用。Python 2.7附带的三个有用的模块,在解析选项和参数时非常值得研究:getoptoptparseargparse

    getopt是一种底层的操作。optparse正逐渐退出历史舞台,因为它在Python 2.7之后就被弃用了。不幸的是,argparse主要设计为与Pythonsys.argv一起使用。然而,Python LLDB命令脚本无法使用sys.argv。这意味着optparse将是我们唯一的选项。FacebookChisel、苹果自己定制的LLDB脚本都使用这个模块。所以,它实际上是解析参数的标准方式。

    optparse模块将允许我们定义OptionParser类型的实例,OptionParser是一个负责解析所有参数的类。要使这个类工作,需要声明我们的命令支持哪些参数和选项。因为可选参数可能接受,也可能不接受该特定选项的附加值。比如

    some_command woot -b 34 -a "hello world"
    

    这个命令名为some_command。但是传递给这个命令的参数和选项是什么?如果没有为解析器提供任何上下文,则此语句是不明确的。解析器不知道-b-a选项是否应该接受该选项的参数。

    例如,解析器可能认为这个命令传递了三个参数:[woot34hello world],还有两个选项-b-a,没有参数。但是,如果解析器希望-b-a接受参数。那么解析器将为您提供参数[woot]、-b选项的34-ahello world

    添加不带参数的选项

    你需要告诉你的解析器需要什么参数。我们先来添加第一个选项,该选项将改变bar命令的功能,以便在不使用正则表达式的情况下应用SBBreakpoint,而使用普通表达式。

    此参数最终将由布尔值表示,因此此选项不需要参数。此选项的存在与否是确定布尔值所需要的所有信息。如果这个参数存在,那么它为真;否则为假。

    值得注意的是,一些脚本作者会设计一个鼓励你设置布尔值的选项。该选项显式传入布尔值。如果未提供该选项,则默认为TrueFalse

    //比如这个命令
    some_command -f
    //上面的命令等价于
    some_command -f1
    

    那并不是我的风格。但是如果你希望更多的人使用这个脚本,那么可能需要考虑这个设计。因为它为用户提供了更明确的意图。

    打开BreakAfterRegex.py,并导入optparseshlex模块。optparse是刚刚介绍的模块,包含OptionParser类,用于分析命令的任何额外输入。shlex模块有一个很好的Python函数,方便地分割为命令提供的参数,同时保持字符串参数的完整性。

    import shlex
    command = '"hello world" "2nd parameter" 34' shlex.split(command)
    ['hello world', '2nd parameter', '34']
    

    在BreakAfterRegex.py的最底部创建以下方法:

    def generateOptionParser():
        usage = "usage: %prog [options] breakpoint_query\n" +\
                "Use 'bar -h' for option desc"
        #1
        parser = optparse.OptionParser(usage=usage, prog='bar')
        #2
        parser.add_option("-n", "--non_regex",
                          #3
                          action="store_true",
                          #4
                          default=False,
                          #5
                          dest="non_regex",
                          #6
                          help="Use a non-regex breakpoint instead")
        #7
        return parser
    
    1. 创建OptionParser实例并为其提供usage参数和prog参数。如果使用错了,给解析器一个不知道如何处理的参数,就会显示用法。prog选项用于处理函数的名字。
      我们需要设置这个参数。因为它解决了一个奇怪的小问题,允许我们在运行-h--help选项时,可以获取自定义命令的所有支持选项。如果prog不在其中,-h命令将无法正常工作。
    2. 在解析器中添加--non_regex-n参数。
    3. action参数,指明了如果提供了该参数的行为。store_true意思是,如果提供了该参数,那么解析器就保存True
    4. 参数默值为False。即如果未提供此选项,则保存值为False
    5. dest参数,指明了OptionParser解析输入时的属性名称non_regex。例如
      command_args = shlex.split(command)
      (options, args) = parser.parse_args(command_args)
      options.non_regex
      
      parse_args方法生成一个Python元组,其中包含一个选项列表和一个参数列表。options变量将包含non_regex属性。
    6. help参数,将提供帮助文档。可以使用--help选项获取所有参数及其信息。例如,如果在bar命令中正确设置了此选项,则只需键入bar -h即可查看所有选项及其操作的列表。
    7. 将返回OptionParser的实例。

    来到breakAfterRegex函数的开头。删除以下两行:

     target = debugger.GetSelectedTarget()
    breakpoint = target.BreakpointCreateByRegex(command)
    

    然后在删除的地方加入:

        '''Creates a regular expression breakpoint and adds it. Once the breakpoint is hit, control will step out of the current function and print the return value. Useful for stopping on getter/accessor/initialization methods
        '''
        #1
        command = command.replace('\\', '\\\\')
        #2
        command_args = shlex.split(command, posix=False)
        #3
        parser = generateOptionParser()
        #4
        try:
            #5
            (options, args) = parser.parse_args(command_args)
        except:
            result.SetError(parser.usage)
            return
        target = debugger.GetSelectedTarget()
        #6
        clean_command = shlex.split(args[0])[0]
        #7
        if options.non_regex:
            breakpoint = target.BreakpointCreateByName(clean_command)
        else:
            breakpoint = target.BreakpointCreateByRegex(clean_command)
    
    1. 我们输入在终端输入\'时,实际表示'。我们需要对命令中的\再转义一次。
    2. 传递到自定义LLDB脚本中的命令参数是一个字符串,包含所有输入参数。我们把这个变量传递到shlex.split方法中获得字符串列表。posix=False有助于处理包含的特殊字符(如破折号);否则,OptionParse将错误地假设这是一个传入的选项。

      这非常重要。因为Objective-C在实例方法中有破折号,所以我们不希望破折号被错误地解释为一个选项!

    3. 使用generateOptionParser函数,创建一个解析器来处理命令的输入。
    4. 解析输入很可能出错。Python通常的错误处理方法是抛出异常。如果optparse发现一个错误,它就会抛出这个错误。如果在脚本中没有捕捉异常,LLDB将退出。因此,将解析包含在try-except块中,防止LLDB因输入错误而退出。
    5. 将command_args变量传递给OptionParser类的parse_args方法,并将接收一个元组作为返回值。这个元组由两个值组成:options可选参数,它包含所有的选项参数(目前只有non_regex选项);args必选参数,这些参数由解析器解析的任何其他输入组成。
    6. 获取第一个捕获的参数(断点查询),并将其分配给一个名为clean_command的变量。还记得第2条中提到的posix=False吗?该逻辑将保持捕获的参数周围的引号,从而保持精确的语法。如果没有posix=False,您可以只使用args[0],但是如果不能在正则表达式搜索中使用转义\,将丧失正则表达式中的很多功能。
    7. 检查options.non_regex的布尔值。如果为True,则在SBTarget中执行BreakpointCreateByName方法以实现非正则表达式断点。如果non_regexFalse(可能是默认值),则脚本将使用正则表达式搜索。所以,我们需要做的只是将-n添加到bar命令的输入中,就可以使non_regexTrue
    测试一下

    创建一个符号断点。符号为getenv,添加两个命令br dis 1bar -n "-[NSUserDefaults(NSUserDefaults) objectForKey:]",勾选自动继续。

    符号断点

    我们在getenvC函数上创建了一个符号断点。在LLDB中,如果想在自己的代码开始执行之前设置断点,或者在逆向别人的app之前设置断点,那么这是hook任何逻辑的好地方。

    我不喜欢使用main,因为许多可执行文件都包含main函数,并且主可执行文件的main符号可能在可执行文件的生产版本中被剥离。但我们知道getenv肯定会命中,而且会在我们的代码开始运行之前被命中。

    第一个动作是去掉getenv断点。我们不是在删除它,只是在禁用它。之所以使用1,是因为这是我们的第一个断点,其ID1

    NSUSerDefaultsobjectForKey:方法上创建一个非正则表达式断点。我们希望这个方法返回一个idnil,所以让我们看看这个RWDevCon应用程序正在读取(或写入)NSUserDefaults什么东西。

    运行这个app。如果没有深入研究这个应用程序,可能会得到很多nil。意味着这个方法肯定在被这个应用程序中的某些代码读取。

    测试
    添加带参数的选项

    我们来添加下一个选项--module,用于指定在哪个模块进行正则表达式的查询。

    BreakAfterRegex.py脚本中,回到generationParser函数,在返回parser之前添加以下代码:

        parser.add_option("-m", "--module", 
                          action="store",
                          default=None,
                          dest="module",
                          help="Filter a breakpoint by only searching within a specified Module")
    

    回到breakAfterRegex函数将下面两行替换

    if options.non_regex:
            breakpoint = target.BreakpointCreateByName(clean_command)
        else:
            breakpoint = target.BreakpointCreateByRegex(clean_command)
    //↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    if options.non_regex:
            breakpoint = target.BreakpointCreateByName(clean_command, options.module)
        else:
            breakpoint = target.BreakpointCreateByRegex(clean_command, options.module)
    

    我们来看看到底可以传入那些参数。

    (lldb) script help (lldb.SBTarget.BreakpointCreateByRegex)
    Help on function BreakpointCreateByRegex in module lldb:
    
    BreakpointCreateByRegex(self, *args)
        BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex, str const * module_name=None) -> SBBreakpoint
        BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex) -> SBBreakpoint
        BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex, lldb::LanguageType symbol_language, SBFileSpecList module_list, SBFileSpecList comp_unit_list) -> SBBreakpoint
    

    我们现在使用的便是这个函数。

    BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex, str const * module_name=None) -> SBBreakpoint
    

    注意最后一个参数:module_name=None。这是一个可选参数,意味着如果不提供参数,模块名默认值为None。当OptionParser实例解析选项时,怎么都可以将options.module提供给BreakpointCreateByRegex方法。因为options.module的默认值将为None`,与不使用额外参数效果相同。

    我们来试试。我们的第二个动作改为bar @objc.*.init -m RWDevCon。在所有继承自OC对象的Swift对象初始化的地方加上了一个断点。我们限制这个断点只查询RWDevCon模块。

    断点

    运行一下。我们会发现很多__ObjC.NSEntityDescription的命中。这意味着这个项目使用了Swift写了很多CoreData逻辑。清除屏幕,并点击包含研讨会(即没有午餐或派对日期)的项目。在控制台,我们得到了一个继承自OC对象的Swift对象列表。搜索名为Person的类。

    Person

    将地址复制到剪贴板中。在粘贴到地址之前,我们看一下Person类实现的所有方法。methods Person命令将列出OC运行时知道Person类实现的所有方法。这个类实现的Swift方法仍然有可能是OC运行时不知道的。

    (lldb) methods Person
    <Person: 0x105a35460>:
    in Person:
        Properties:
            @property (nonatomic, copy) NSString* first;  (@dynamic first;)
            @property (nonatomic, copy) NSString* last;  (@dynamic last;)
            @property (nonatomic, copy) NSString* bio;  (@dynamic bio;)
            @property (nonatomic, copy) NSString* twitter;  (@dynamic twitter;)
            @property (nonatomic, copy) NSString* identifier;  (@dynamic identifier;)
            @property (nonatomic) BOOL active;  (@dynamic active;)
            @property (nonatomic, retain) NSSet* sessions;  (@dynamic sessions;)
        Instance Methods:
            - (id) initWithEntity:(id)arg1 insertIntoManagedObjectContext:(id)arg2; (0x1059fe0a0)
    (NSManagedObject ...)
    
    向断点回调函数传递参数

    下面我们创建-c--condition参数的解析。在generateOptionParser返回值之前加入:

    parser.add_option("-c", "--condition",
                      action="store",
                      default=None,
                      dest="condition",
                      help="Only stop if the expression matches True. Can reference return value through 'obj'. Obj-C only.")
    

    那么我们怎么把这个参数传递给回调函数breakpointHandler呢?答案是我们将使用Python字典来传递这个选项。另外,断点的好处是,不管创建或删除多少个断点,每个断点在每次运行会话中都有一个唯一的标识ID。可以将断点ID设置为键,并将该断点的选项设置为值。来到BreakAfterRegex.py的顶部,并在import语句的正下方添加以下逻辑:

    #1
    class BarOptions(object):
        #2
        optdict = {}
        #3
        @staticmethod
        def addOptions(options, breakpoint):
            key = str(breakpoint.GetID())
            BarOptions.optdict[key] = options
    
    1. 声明一个名为BarOptions的类,该类继承自object。可以把object看作是Python中的NSObject
      2。声明一个名为optdict的类变量。如果要声明实例变量,它必须在init函数中。因为只使用这个类变量,所以不会为这个类设置任何初始化方法。
    2. 声明一个名为addOptions的类方法。通过断点的ID作为键来保存options

    来到breakAfterRegex并在回调函数上面加入:

    BarOptions.addOptions(options, breakpoint)
    

    BreakAfterRegex.py的最下面加入新的函数。

    def evaluateCondition(debugger, condition):
        '''Returns True or False based upon the supplied condition. You can reference the NSObject through "obj"'''
        #1
        res = lldb.SBCommandReturnObject()
        interpreter = debugger.GetCommandInterpreter()
        target = debugger.GetSelectedTarget()
        #2
        expression = 'expression -lobjc -O -- id obj = ((id){}); ((BOOL) {})'.format(getRegisterString(target), condition)
        interpreter.HandleCommand(expression, res)
        #3
        if res.GetError():
            print(condition)
            print('*' * 80 + '\n' + res.GetError() + '\ncondition:' + condition)
            return False
        elif res.HasResult():
            #4
            retval = res.GetOutput()
            #5
            if 'YES' in retval:
                return True
        #6
        return False
    
    1. 创建一个SBCommandReturnObject来处理传递过来的condition参数。
    2. 创建并执行传入的自定义表达式。

      注意:我们声明了实例变量obj,并将其从返回寄存器强制转换为类型id。这样,我们就可以方便地将返回值引用为obj,而不是硬件特定的寄存器。再将提供的表达式返回值转换为Objective-C BOOL,它将返回YESNO输出。

    3. 如果返回值包含错误,则打印出错误。

      注意:如果函数返回TrueSBBreakpoint回调函数断点处理程序将停止执行。如果返回的不是True(即FalseNoneno return),则执行不会停止。

    4. 把结果赋值给retval变量。
    5. 将输出与预期结果进行比较。如果表达式的计算结果为YES,则暂停执行。
    6. 如果执行返回NO,则通过返回False继续执行。

    最后breakpointHandler函数,在thread.StepOut()下面添加:

    #1
    key = str(bp_loc.GetBreakpoint().GetID())
    #2
    options = BarOptions.optdict[key]
    #3
    if options.condition:
        #4
        condition = shlex.split(options.condition)[0]
        #5
        return evaluateCondition(debugger, condition)
    
    1. bp_locSBBreakpointLocation类型。这个类允许您通过GetBreakpoint方法引用初始的SBBreakpoint,就可以拿到ID了。然后需要将此数字转换为字符串并将其分配给变量键。
    2. 从类属性optict中获取对应的值,并将其分配给变量options
    3. 检查options变量是否为空。
    4. 获取options.condition中的条件语句。
    5. 调用evaluateCondition函数。返回函数的返回值,该值将影响是否应停止执行。

    我们来试一试。并把第二个动作改为bar NSURL\(.*init -c '\[\[$obj absoluteString\] containsString:@\"amazon\"\]'

    断点

    现在断点只会停在满足条件的断点上了。

    相关文章

      网友评论

        本文标题:(十一)自定义LLDB命令 选项和参数

        本文链接:https://www.haomeiwen.com/subject/fndjdhtx.html