编写 LLDB 调试器脚本
LLDB 调试器提供对 Python 脚本的支持,可以执行一些自动化的操作,提供工作效率。本文从 HelloWorld 开始学习如何编写 LLDB 脚本。
编写第一个 LLDB 脚本
下面我们来实际操作编写 LLDB 调试器脚本。在计算机上建立一个目录用于存脚本文件,比如我们在 /Users/exchen/lldb 这个目录下操作,新建一个 HelloWorld.py 的文件
1 2 3 4 5 6 7 8 9 |
def test(debugger, command, result, internal_dict): """ This is my first lldb script """ print "hello, world" def __lldb_init_module(debugger, internal_dict): debugger.HandleCommand("command script add -f HelloWorld.test mycmd") |
test 函数是我们准备要导出的功能,里面的 4 个参数是固定的,含义如下:
- debugger 是当前调试器对象,类型是 lldb.SDBDebugger。
-
command 是命令的参数,类型是字符串。
-
result 是执行命令后返回的参数,类型是 lldb.SBCommandReturnObject
-
internal_dict 当前脚本的所有变量和函数,类型是字典。
lldb_init_module 函数是在加载脚本时会执行的,command script add -f 是用于将 Python 代码导出成自定义的命令,其中 HelloWorld 是脚本的名称,这个要和脚本的文件名保持一致,test 是函数的名称,mycmd 是导出的命令名称。在 lldb 加载这一段脚本,输入 mycmd 命令就可以执行 test 函数里的代码。
执行 command script import 将脚本引入到调试器,输入 mycmd 命令可以看到打印出 hello, world,证明 test 函数的代码得到执行。输入 help mycmd 可以查看到相应的提示信息。
1 2 3 4 5 6 7 8 |
(lldb)command script import /Users/exchen/lldb/HelloWorld.py (lldb)mycmd hello, world (lldb)help mycmd Syntax: This is my frist lldb script |
了解 LLDB 配置文件
LLDB打开时会自动加载~/.lldbinit配置文件,通过这个配置文件可以设置加载的脚本路径等。上面我们编写了第一个脚本,在启动 LLDB 需要手动执行 command script import 才能引入脚本,如果将 command script import 添加到 ~/.lldbinit 每次启动 LLDB 时就会自动会引入脚本。默认这个配置文件是不存在的,我们手动创建一个,然后添加下面的命令,这样每次 LLDB 启动都会自动引入 HelloWorld.py。
1 2 |
command script import /Users/exchen/lldb/HelloWorld.py |
除了引入自定义脚本,lldbinit配置文件还可以设置命令的别名、命令提示符文字等等,比如我们添加下面的信息
1 2 3 |
setting set prompt "(exchen lldb)" command alias connect process connect connect://127.0.0.1:12345 |
此时再打开 LLDB 会发现命令行的提示文字显示的是 "(exchen lldb)",输入connect 命令别名就可以连接程序进行调试,省去了每次烦琐地输入一大串命令。
1 2 3 4 5 6 7 8 9 |
$ lldb (exchen lldb)connect Process 5702 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00000002334200f4 libsystem_kernel.dylib`mach_msg_trap + 8 libsystem_kernel.dylib`mach_msg_trap: -> 0x2334200f4 <+8>: ret |
编写实际功能的脚本
在上面我们已经编写了一个 HelloWorld 脚本,通过这个脚本我们了解到 LLDB 脚本的大概框架,但是这个脚本并没有任何实际功能,接下面我们要编写一个有以实际功能的脚本,这个脚本的功能是导出三个命令,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#!/usr/bin/env python # -*- coding: utf-8 -*- import lldb import re import os import shlex def goto_main(debugger, command, result, internal_dict): """ goto main """ interpreter = lldb.debugger.GetCommandInterpreter() return_object = lldb.SBCommandReturnObject() # 用来保存结果 interpreter.HandleCommand('dis', return_object) # 执行dis命令 output = return_object.GetOutput(); #获取反汇编后的结果 br_index = output.rfind('br x16') #查找最后的 bx x16 br_index = br_index - 20 #位置减去20 addr_index = output.index('0x', br_index) #查找0x开头的字符串 br_addr = output[br_index:br_index+11] #找到之后偏移11位 debugger.HandleCommand('b ' + br_addr) #添加断点 debugger.HandleCommand('continue') #运行 debugger.HandleCommand('si') #单步步入 def get_aslr(): interpreter = lldb.debugger.GetCommandInterpreter() return_object = lldb.SBCommandReturnObject() interpreter.HandleCommand('image list -o -f', return_object) #执行image list -o -f命令 output = return_object.GetOutput(); #获取命令的返回值 match = re.match(r'.+(0x[0-9a-fA-F]+)', output) #正则匹配(0x开头) if match: return match.group(1) else: return None def aslr(debugger, command, result, internal_dict): """ get ASLR offset """ aslr = get_aslr() print >>result, "ASLR offset is:", aslr def breakpoint_address(debugger, command, result, internal_dict): """ breakpoint aslr address """ fileoff = shlex.split(command)[0] #获取输入的参数 if not fileoff: print >>result, 'Please input the address!' return aslr = get_aslr() if aslr: #如果找到了ASLR偏移,就设置断点 debugger.HandleCommand('br set -a "%s+%s"' % (aslr, fileoff)) else: print >>result, 'ASLR not found!' def __lldb_init_module(debugger, internal_dict): debugger.HandleCommand('command script add -f myscript.aslr aslr') debugger.HandleCommand('command script add -f myscript.goto_main gm') debugger.HandleCommand('command script add -f myscript.breakpoint_address ba') |
从上面的代码 lldb_init_module 可以看出导出了三个命令,第一个命令是 aslr,用于获取 aslr 的地址,原理是调用 image list -f -o 命令,从返回的结果中匹配出括号中有 0x 开头的就是第一个模块的 alsr 地址。
第二个命令是 gm,用于跳转到 main 函数的入口点,原理是 lldb 连接启动的进程时,第一条指令是在 dyld 里的 _dyld_start 函数,该函数最后一条指令是 br x16,会跳转到 main 函数,使用 dis 命令查看 _dyld_start 的反汇编代码,从结果中查找字符串 br x16 得到这条指令所在的地址,然后调用添加断点,再调用 continue 命令让程序运行起来,断点在 br x16 处会触发,然后再执行 si 单步步入即可达到 main 函数,操作过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
(exchen lldb)command script import ~/lldb/myscript.py (exchen lldb)connect Process 5693 stopped * thread #1, stop reason = signal SIGSTOP frame #0: 0x0000000100a3d000 dyld`_dyld_start dyld`_dyld_start: -> 0x100a3d000 <+0>: mov x28, sp 0x100a3d004 <+4>: and sp, x28, #0xfffffffffffffff0 0x100a3d008 <+8>: mov x0, #0x0 0x100a3d00c <+12>: mov x1, #0x0 0x100a3d010 <+16>: stp x1, x0, [sp, #-0x10]! 0x100a3d014 <+20>: mov x29, sp 0x100a3d018 <+24>: sub sp, sp, #0x10 ; =0x10 0x100a3d01c <+28>: ldr x0, [x28] Target 0: (AppStore) stopped. (exchen lldb)aslr ASLR offset is: 0x0000000100360000 (exchen lldb)gm Breakpoint 1: where = dyld`_dyld_start + 132, address = 0x0000000100a3d084 Process 5693 resuming Process 5693 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100a3d084 dyld`_dyld_start + 132 dyld`_dyld_start: -> 0x100a3d084 <+132>: br x16 dyld`dyldbootstrap::start: 0x100a3d088 <+0>: sub sp, sp, #0x80 ; =0x80 0x100a3d08c <+4>: stp x28, x27, [sp, #0x20] 0x100a3d090 <+8>: stp x26, x25, [sp, #0x30] 0x100a3d094 <+12>: stp x24, x23, [sp, #0x40] 0x100a3d098 <+16>: stp x22, x21, [sp, #0x50] 0x100a3d09c <+20>: stp x20, x19, [sp, #0x60] 0x100a3d0a0 <+24>: stp x29, x30, [sp, #0x70] Target 0: (AppStore) stopped. Process 5693 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into frame #0: 0x0000000100373ce0 AppStore`_mh_execute_header + 81120 AppStore`_mh_execute_header: -> 0x100373ce0 <+81120>: stp x22, x21, [sp, #-0x30]! 0x100373ce4 <+81124>: stp x20, x19, [sp, #0x10] 0x100373ce8 <+81128>: stp x29, x30, [sp, #0x20] 0x100373cec <+81132>: add x29, sp, #0x20 ; =0x20 0x100373cf0 <+81136>: mov x19, x1 0x100373cf4 <+81140>: mov x20, x0 0x100373cf8 <+81144>: mov x0, #0x0 0x100373cfc <+81148>: bl 0x1004ca278 ; AppStore.__TEXT.__text + 1469760 Target 0: (AppStore) stopped. |
第三个命令是 ba,功能在调试器里给 IDA 里看到的地址添加断点,省去了每次从 IDA 里看到地址,需要手动加上 aslr 地址才能和调试器的地址对应上。
关于更多脚本编写的说明可以参考 LLDB 官网的文档 http://lldb.llvm.org/use/python-reference.html
转载请注明:exchen's blog » 编写 LLDB 调试器脚本