iOS 逆向 - LLDB

4,037 阅读8分钟

前言

在进入逆向正式实战之前 , 动态调试和静态分析都是我们必不可少的能力 .

LLDB 是不管在正向开发还是逆向开发中 , 都是帮助我们调试必不可少的手段 . 而在逆向开发中不能像正向开发一样页面断点 , 可视化数据展示 , 源代码调试等方式的情况下 , LLDB 的作用就会尤其重要 .

  • 考虑到并不是所有同学都会亲自建一个工程来实战演练一下 LLDB , 本篇文章我会结合实战来介绍 LLDB 常用指令的实际效果 , 以此来更好地理解和记忆 LLDB ( 毕竟面试还是经常会问到的 ) , 熟悉的同学可以自行跳过 .

  • 另外笔者写指令时会尽量写全 , 全称指令有助于理解 , 并且以后再换为简写也很简单 , 反过来就不行了 . 因此大佬勿喷 .

LLDB

概述

默认内置于 Xcode 中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要。

查看指令和指令帮助

  • 任意工程进入断点模式 或者暂停后 , 直接输入 help
  • 再输入具体指令加 help 可以查看某一条指令的说明 : breakpoint help.

代码准备

#import "ViewController.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touch screen");
    testFunc();
}

void testFunc(){
    printf("testFunc");
}

- (IBAction)btnAction1:(id)sender {
    
}
- (IBAction)btnAction2:(id)sender {
    
}
- (IBAction)btnAction3:(id)sender {
    
}
@end

常用断点指令

一、过掉这个断点

  • 指令 : continue , 简写为 c .
  • 单步走为 step , 简写为 s , 遇到嵌套子函数会进去.
  • 单步运行 next , 简写为 n , 遇到嵌套子函数会当做整体一步执行 .

二、查看当前断点列表

  • 指令 : breakpoint list , 简写为 b l

    ( 我们在 Xcode 可视化加的断点也可以被查看到 , 因为本身都在寄存器中 ) .

三、方法名加断点

  • 指令 : breakpoint set --name testFunc

  • 结果 :

  • 说明 :

    该指令直接通过方法名称下断点 . --name 可简写为 -n

  • 查看断点 :

  • 结果验证 : continue 过掉断点 , 点击屏幕 , 来到我们下的断点 .

四、一次通过多个方法名加断点

  • 指令 :

    breakpoint set --name "-[ViewController btnAction1:]" --name "-[ViewController btnAction2:]" --name "-[ViewController btnAction3:]"
    
  • 结果 :

  • 说明 :

    该指令直接通过一次添加多个方法名称下断点 . --name 可简写为 -n , 添加结果是一组断点 , 但是为多个位置 .

  • 查看断点 :

五、禁用 / 启用断点

  • 指令 : breakpoint disable 5 / breakpoint enable 5

  • 结果 :

  • 说明 :

    通过 breakpoint list 先查看断点 ID , 然后禁用或者启用某一个或者某一组断点.

  • 查看断点 :

六、通过 sel 加断点

  • 指令 : breakpoint set --selector touchesBegan:withEvent:

  • 结果 :

  • 说明 :

    • 通过 selector 添加断点 , 不局限于某个类 , 当再添加 --file 指令时 , 会具体到该类中寻找这个 sel 下断点 .

    • 例: breakpoint set --file ViewController.m --selector touchesBegan:withEvent:

  • 查看断点 :

七、不完整 sel 加断点

  • 指令 : breakpoint set --func-regex btnActi

  • 结果 :

  • 说明 :

    • 通过 并没有写完整的selector 添加断点 , 但是并非是模糊搜索的模式 , 用的正则来处理的 , 因此输入的名称不能错 , 只能是未写完 .
    • 也可结合 --file 指令来针对某个类处理
  • 查看断点 :

八、删除断点

  • 指令 : breakpoint delete 10

  • 结果 :

  • 说明 :

    不传断点 ID 则为删除所有断点 , 会提示确认操作.

  • 查看断点 :

提示

  • LLDB 指令对断点做的操作与 Xcode 可视化页面中对断点所做的添加 / 删除 / 禁用 / 启用 操作都是同步的.

  • 当删除断点时 , 不能删除某一组中的一个 , 删除会变成 disable , 只能删除组

  • breakpoint set 可直接简写为 b

  • breakpoint list 可简写为 break l , breakpoint disable 可简写为 break dis 等等以此类推 , 可以简写到只要系统可以区分你到底敲的是哪一条命令都可以

其他常用命令

执行代码 expression / p

  • 指令 : expression self.view.subviews , 简写 p

  • 结果 :

  • 说明 :

    pexpression 的简写 , 并非很多同学理解的 print 哦 , poexpression -O ( --object-description NSObjectdescription 方法 ) 的简写.

  • 使用举例 :

    • 执行代码 : p self.view.backgroundColor = [UIColor orangeColor];
    • 过掉断点 : c
    • 页面效果 :

同样的 , p 可以写多句代码 , 使用 ; 即可 . 该方法常用与逆向中 Cycript 检查是否是我们要找的 view .

函数调用栈

  • 指令 : thread backtrace , 简写 bt ,
  • 结果 :
  • 说明 :
    • bt查看函数调用流程 , 可加上要查看的数量 , 例如 bt 3
    • 通过 up / down 方法可以跳转到 前一个 / 后一个 方法中
    • 通过 frame select 5 也可以直接通过编号跳转对应方法
    • 通过 frame variable 可以查看方法参数
    • 通过 thread return 可以线程回滚 , 而且可以修改参数 , 但是执行完该函数会直接 return .

还有一些其他的指令这里就不一一列举了 , 通过 help 指令可以查看自行练习.

image 相关

image list
  • 指令 : image list
  • 说明 : 查看当前进程加载了哪些库 .
  • 结果 :
image lookup
  • 指令 : image lookup -t LBPerson
  • 说明 : 查看某个类信息 .
  • 结果 :

逆向常用命令

刚刚我们看了很多断点调试指令 , 那么逆向过程中呢 ? 我们并没有源码 , 结果就是 : 很多通过方法名称下断点等方式都不能用 😶 .

  • 为什么 ?

    • 我们就一个 Mach-O , 没有符号 , release 模式的情况下 , 符号是会被编译器自动去掉的 .
  • 怎么办 ?

    • 内存断点

代码准备

#import "ViewController.h"

@interface LBPerson : NSObject
@property(nonatomic , assign) int age;
@property(nonatomic , copy) NSString * name;
@end

@implementation LBPerson

@end

@interface ViewController ()
@property(nonatomic, strong) LBPerson * person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LBPerson alloc] init];
    self.person.age = 20;
    self.person.name = @"lb";
    NSLog(@"haha");
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name = @"test";
}

NSLog 处加个断点 , 运行 .

添加内存断点

来到断点后输入指令 : watchpoint set variable self->_person->_name

过掉断点 . 点击屏幕 , 修改了 personname , 监控到内存断点 , 并打印如下 :

Watchpoint 1 hit:
old value: 0x0000000107f72078
new value: 0x0000000107f720b8

查看内容 .

通过函数调用栈可以看到其实是在属性的 set 方法时调用的 .

通过内存地址下断点

逆向中我们往往只有内存地址 , 甚至不一定拿得到变量或者属性 . 那么就需要通过内存地址下断点 .

同样刚刚的代码 , 重新运行来到 NSLog 断点.

  • p 查看 name 内存地址 , p &self->_person->_name
  • watchpoint set expression 0x0000600000db2f70
  • 过掉断点 , 点击屏幕
  • 检测到改变

内存断点的删除 禁用 查看列表

同上述 breakpoint , 不多赘述.

LLDB高级用法

特定断点添加指令

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touch screen");
    [self test];
}

- (void)test{
    NSLog(@"Add Command test");
}
1. 给 testFunc 添加断点

指令 : b testFunc

2. 给这个断点添加指令

指令 : breakpoint command add 1

3. 输入要添加的指令

输入 :

>po self
>p self.view.backgroundColor = [UIColor orangeColor];
>DONE

过掉断点 , 点击屏幕.

结果 :

再过掉断点 , 查看页面 .

这种方法适用场景较多 , 例如某个断点一断住就查看形参 等等 , 自由发挥 .

同样的 查看断点指令列表

breakpoint command list 1

删除断点指令

breakpoint command list 1

stop-hook

  • 指令 : target stop-hook add --one "frame variable"

  • 说明 :

    • --one 代表添加一条指令 , 可简写为 -o
    • 可视化页面中 Pause paogram execution 以及 Debug view 不属于 stop 范畴.
  • 结果: 在 TouchBegan 添加一个断点 , 点击屏幕 , 打印如下 :

  • 相较于上一个 特定断点添加指令 , 显然是更通用 .

那么同样 , 查看断点指令列表 : target stop-hook list

删除断点指令 : target stop-hook delete , 使用 undisplay 1 , 也是一样的 .

禁用 / 启用 同理 .

自动启用加载指令

以上我们说的这些 , 建立在工程运行起来之后 , 进入 lldb 模式 , 添加指令等操作的前提下 . 那么我们思考一个问题 , 每次运行工程都要重新添加 , 可否自动处理呢 ?

答案是肯定的 .

打开 终端 / iTerm2 . 来到家目录下 , ls -a 查看文件.

  • 如果你跟我一样有这个 .lldbinit 文件 , 直接 vim 添加 target stop-hook add -o "frame variable" .
  • 如果你没有这个文件 . 没关系 直接 vim 创建 然后写上 target stop-hook add -o "frame variable".
  • 保存并退出 .

来到工程中 , 重新 run 一下 , 点击屏幕方法添加一个断点 , 点击屏幕 :

其原因是 lldb 在启动时会加载这个文件 , 对每个工程都有效 .

最后

最后简单说一下逆向过程中如何下一个内存断点

  • 使用 MachOView / Hopper , 找到方法地址 , 减去 PageZero 的虚拟内存 (也就是最前面的那个 1 , / 64 位下是 4 个 G, ) 得到方法基于 Mach-O首地址的真实偏移地址.
  • lldb 查看 Mach-O 地址 .
  • 得到的地址和方法偏移地址相加 , 得到方法的真实地址 .
  • 根据地址添加断点 , 即可添加成功.

这个是由于 ASLR 的原因 . 说白了就是地址空间配置随机加载 , 因此我们需要在加载完后再获取 Mach-O 的地址即可.

物理地址 = ALSR + 虚拟地址