iOS 逆向 - lldb高级篇 Chisel 与 Cycript

5,365 阅读11分钟

前言

上一篇文章 iOS 逆向 - LLDB 中讲述了 lldb 的一些基础用法 , 并没有涉及太多其他内容 , 逆向过程中常用的动态调试方法其实还有一些 , 本文针对上篇文章和实际逆向中的运用进行一个补充 .

主要针对 Chisel 以及 Cycript 两个部分 .

如果篇幅不长 , 我们来讲一讲自定义 cy 指令.

逆向调试注意

  • 有部分同学反应重签名微信应用被封号的情况 . 这里说明一下 , 微信 / 抖音 等应用是有防护和监测操作的 , 网上流传部分破解微信防护的 logos 代码 , 笔者测试效果不是百分百 , 有需要的小伙伴可以评论留言 .

  • 调试别人应用本身是会造成类似问题的 , 因此尽量不要登录个人使用账号 , 如非必须登录 , 不要登录账号 .

  • 确实需要登录 , 登录一个小号 , 另外提前准备好另一个账户去解封 .

  • 在越狱环境下 , 使用插件的方式调试 , 无须重签名 , 被封号的几率是很低的 .

  • 最后再强调一次 : 玩逆向只是为了更好地防护 .

Chisel

概述

Chisel 也是 Facebook 发布的一个 lldb 的插件 , 能够做到帮助调试和提供用户自定义指令集的功能 . 接下来会具体阐述 .

安装

命令 : brew install chisel

( 还没有安装 HomeBrew 的自行安装 )

安装后使用 : brew list 查看安装结果

配置

如何配置 ?

1 -- 打开下载目录

  • 先找到下载的文件 : cd /usr/local/Cellar/chisel
  • open . , 找到 fblldb.py 脚本文件.
  • cmd + opt + c 拷贝文件路径.

2 -- lldb配置脚本路径加载

  • 如果你的用户家目录下没有 .lldbinit 文件 , 请查阅上一篇文章 iOS 逆向 - LLDB 中最后讲自动启用加载指令所述 , 使用 vim 自行创建即可 .
  • vim .lldbinit
  • s 进入编辑模式
  • 添加加载指令 : command script import /usr/local/Cellar/chisel/1.8.1/libexec/fblldb.py ( 路径换成你自己的 )
  • ESC , :wq 保存退出

使用

测试配置结果

先来简单试一下配置成功了没 .

注意 : 如果是正在运行的工程 , 那你需要使用 command source ~/.lldbinit , 来重新加载一下 lldb 配置文件 .

随便打开一个工程 , 进入断点模式 , 输入 pviews .

pviews 就是 Chisel 提供的一个查看视图层级的命令 . 以上看到 , 我们已经配置成功了 .

常用指令

为了模拟逆向过程中实际动态调试场景 . 我使用了我之前使用 MonkeyDev 重签名的微信应用来演示指令 , 对重签名不熟悉的同学可以阅读一下 重签应用调试与代码修改 (Hook) , 与 shell 脚本自动重签名与代码注入 , 这两篇文章 .

MonkeyDev 的安装和使用我们就不多赘述了 , 毕竟重签名原理理解了 , Monkey 其本质上也是利用脚本自动重签名 , 然后 代码注入 hook 部分集成了 Cydia Substrate 来做的.

我们所需要做的只是把砸过壳的 ipa 或者 app 包 放到指定文件夹下即可完成重签名和代码注入 . 非常方便.

如果同学们关于 MonkeyDev 使用有问题可以留言告知 .

运行工程 .

pviews 图层层级

打开注册页面 , 暂停 . 进入断点模式 ,

  • 指令 : pviews
  • 结果 :
  • 说明 : pviews 可以帮助我们很清楚的看出图层逻辑层级关系以及内存地址 .
pvc 视图层级
  • 指令 : pvc

  • 结果 :

  • 说明 : pvc 可以帮助我们很清楚的看出视图控制器层级关系以及内存地址 .

pactions 事件查找

使用 pviews , 随便找到一个按钮 , 复制其内存地址 .

  • 指令 : pactions 0x10b06e5e0
  • 结果 :
  • 说明 : pactions 可以拿到 buttontarget 以及 action , 在逆向时需要方法 hook 经常会使用.
presbonder 响应链

使用 pviews , 随便找到一个按钮 , 复制其内存地址 .

  • 指令 : presbonder 0x10b06e5e0
  • 结果 :
  • 说明 : presbonder 可以查看完整的响应链.
pclass 继承链

使用 pclass , 随便找到一个类 , 复制其内存地址 .

  • 指令 : pclass 0x10b910600
  • 结果 :
  • 说明 : pclass 可以查看完整的继承链.
pmethods 查看类方法 / 实例方法
  • 指令 : pmethods 0x106b57bc0
  • 结果 :
  • 说明 : pmethods 可以查看类完整的类方法和实例方法.
pinternals 查看成员变量
  • 指令 : pinternals 0x10b660df0
  • 结果 :
fv / fvc
  • 指令 : fvc -v 0x10b8fe000
  • 结果 :
  • 说明 :
    • 通过内存地址查看类名 ( po 也可以 ) . , 视图控制器用 fvc , 视图用 fv .
    • fv + 类名 反之是一样的 , 回去工程搜索这个类 打印其内存地址 .
重点 : taplog
  • 指令 : taplog
  • 说明 : taplog 输入后会退出断点模式 , 再点击屏幕上任何可响应视图 , 会自动进入 lldb 模式并打印按钮.
  • 结果 :
重点 : flicker
  • 指令 : flicker 0x11827c040
  • 说明 : 通过内存地址 , 在断电模式下调用该指令会闪烁 view , 非常方便调试 , 以及确定该内存地址是否为我们想找到的那个视图 .
重点 : vs
  • 指令 : vs 0x11827c040
  • 说明 : 通过内存地址 , 进入调试模式 , 当前view会被添加红色以便查看 .
  • 结果 :
(lldb) vs 0x1120f3390

Use the following and (q) to quit.
(w) move to superview    // 来到当前视图的父视图
(s) move to first subview // 来到当前视图的第一个子视图
(a) move to previous sibling  // 来到当前视图同级关系下的前一个视图
(d) move to next sibling     // 来到当前视图同级关系下的后一个视图
(p) print the hierarchy   // 打印当前视图的层级结构

<FixTitleColorButton: 0x1120f3390; baseClass = UIButton; frame = (20 112; 374 47); clipsToBounds = YES; opaque = NO; autoresize = W; layer = <CALayer: 0x1118c0900>>
  • 退出 vs 调试模式 , q 指令 .
提示

最后三条指令在逆向过程中非常常用 , 大家多加练习与掌握 .

lldb_commands 插件

这个 LLDB 插件叫 lldb_commands . 地址为 github.com/DerekSeland…

安装

  • 直接 Clone 或者下载 , 把 lldb_commands 文件夹保存起来 , 我这边是放到 /usr/local/Cellar

  • 来到家目录 , 找到 .lldbinit , 添加一条指令 :
command script import /usr/local/Cellar/lldb_commands/dslldb.py

路径换成自己的 lldb_commands 文件夹路径即可 .

工程来到 lldb 模式下 , 输入 search UIView , 即可测试有没有配置成功 .

常用指令

methods 快速定位方法

找到视图控制器地址 , 使用 methods 查看其所有的实例方法和属性 .

注意 : 由于逆向时 方法的符号是没有恢复的 , 因此根据类名和方法名下断点会失败 .

函数调用栈

由于符号并没有恢复 , 因此 bt 指令查看函数调用栈时 , 会出现以下情况 .

( 后续会讲如何利用工具恢复 Mach-O 的符号 )

那么此时 , 利用 lldb_commands 提供的 sbt 指令 , 会帮助恢复一些符号 , 以便于查看方法名称 .

Mach-O Section 查看
  • Section 指令可以让我们快速查看 Mach-O 有哪些 Section 段 .
  • Section 可添加其他指令来查看 Mach-O 具体内容 .
  # Dump the Mach-O segments to the main executable
  (lldb) section

  # Dump the Mach-O segments to UIKit
  (lldb) section UIKit

  # Dump the Mach-O sections of the __TEXT segment of UIKit
  (lldb) section UIKit __TEXT

  # Get the load address of all the hard-coded uint8_t * strings in the UIKit binary
  (lldb) section UIKit __TEXT.__cstring -l

  # Get the entitlements for the executable (simulator only, entitlements for actual app in __LINKEDIT)
  (lldb) section  __TEXT.__entitlements

  # Get all the load address to the lazy symbol stubs in the main executable
  (lldb) section  __DATA.__la_symbol_ptr -l

效果如下 , 大家可以结合 MachOView 来查看结果.

Cycript

概述

Cycript 是由 Cydia ( 熟悉越狱的同学应该都很清楚 ) 创始人 Saurik 推出的一款脚本语言,Cycript 混合了 OCJavaScript 语法的解释器,这意味着我们能够在一个命令中使用 OC 或者 JavaScript,甚至两者并用。

它能够挂钩 正在运行的进程,能够在运行时修改很多东西。

到官网点击 Download SDK 即可下载 .

安装

  • 将下载后的文件夹放入 /opt/ 里即可 , 也可以自行选择位置 .
  • 配置环境变量 .
  • 注意 : 这里根据你使用的是 zsh 还是 bash 去相应的资源文件配置 (家目录下的 .zshrc / .bash_profile) .
  • 笔者由于在 .zshrc 中也配置了 bash_profile 的引用 , 因此两处来配置这个环境变量都是可以的.

配置内容:

  • 添加 : export CY=/opt/cycript_0.9.594/ , 换成你自己的路径.
  • export PATH= 中添加 :$CY

重启 iTerm , 输入 cycript , 即可查看 .

如果有遇到 ruby 环境不对的同学 , 去下载对应版本的即可 . /System/Library/Frameworks/Ruby.framework/Versions

使用

在越狱环境下 , 是可以在 Cydia 直接安装 Cycript 插件的.

越狱手机截图上传有点麻烦 , 直接拍的.. 瑕疵请忽略 , 看个意思

那么非越狱环境下 , 就要借助 Cycript 提供的 iOS Framework , 注入进去了 . 而且在 MonkeyDev 中 , 是默认已经做好了 Cycript 的注入的 .

而且添加了 默认 6666 端口号的监听 .

也就是说只要用 MonkeyDev 来重签跑起来的程序进程 , 6666 端口就可以附加使用了 ( 当然 , 在 Monkey 里代码可以自己定义端口号 ) .

好了 说了这么多 , 开始使用.

  • 1 . 保证电脑和手机在同一个局域网内 ( 因为要做端口映射 )
  • 2 . 运行 MonkeyDev 重签名程序 / 或者直接打开以前重签好的工程 , 无须 Xcode 运行也可以
  • 3 . 终端输入 cycript -r 192.168.0.116:6666
    • 换成你自己的手机 ip 地址
    • 另外 , 请不要将应用程序放到后台 , 会影响附加.

出现以下 , Congratulations , you're good to go !

提示

使用 tab 键 , 写代码可以补全 .

UIWindow.keyWindow()

查看当前 Window .

UIApplication sharedApplication

指令 : [UIApplication sharedApplication ] 可简写为 UIApp

自定义变量

指令 : 自己 var 一个对象 , 而后我们就可以使用 .

另外注意 : 只要 APP 进程没有挂 , 这个变量是一直存在的 .

# + 地址可以直接使用

# + 对象地址 可以直接调用对象的方法

查看视图层级

UIWindow.keyWindow().recursiveDescription().toString()

随便找一个 , 例如注册页面有一个 .text+86label , 拿到其内存地址 ,

命令 :

#0x10b95d800.text = "hhh"

显示结果

基于这种直接修改进程内存的方式 , 大家可以自己去玩一玩 . 比如登录了修改一下钱包余额 , 然后改一改 frame , 练习一下 .

以下指令结果我就不一一贴图了 , 文章太长 , 不便阅读 , 大家自己尝试 .

获取页面上所有控件

choose(UIButton)

choose(UILabel)

隐藏 / 显示状态栏

[UIApp setStatusBarHidden:YES]

APP角标

[UIApp setApplicationIconBadgeNumber: 99]

获取Bundle ID

APPID 结果 :

@"com.libin.LBMonkeyApp"

页面层级

pviews()

pvcs()

根据按钮地址获取按钮 target & Action

pactions (#0x10b29da40)

结果 :

"<WCAccountRegisterViewController: 0x10b9d9800> onAgreementCheckBoxClick:"

根据按钮地址获取响应链

rp(#0x10b29da40)

退出cy 调试模式

control + d

注意:

pviews / pvc / pactions / rp 这些指令是 MonkeyMDConfig.plist 中额外封装了自定义的 cy 源的 . 也就是说使用越狱环境原本的 cycript 插件是没有这些指令可用的 .

那么我们闲着也是闲着 , 我们也来自己写一个 cy 源来玩一下 ?

自定义 cy 指令

  • 在我们的 monkey 工程主工程 target 中新建一个空文件, 我这里取名 lb.cy .

  • Build Phases - Copy Files , 引入这个文件

  • 在空文件添加我们想自己定义的指令 . 可以参照 Monkey 原本提供的那两个来写 raw.githubusercontent.com/AloneMonkey…

    这里我写了一些我常用的指令 , 比如获取 APPID / APPPATH , 当前跟视图 , 当前页面 这些 , 我贴在下面供大家参考 , 也可以拿去直接用 .

  • 重新运行工程 .

  • 输入我们自定义的指令 例如 : LBCurrentVC()

  • 提示没找到指令 , 因为我们还没有引入 , monkey 那两个在 config 中会自动引入.

  • @import lb

  • 再次输入 LBCurrentVC() , 得到结果.

//IIFE 匿名函数自执行表达式

(function(exports){

     APPID = [NSBundle mainBundle].bundleIdentifier,
     APPPATH = [NSBundle mainBundle].bundlePath,

     //如果有变化,就用function去定义!!
     LBRootvc = function(){
        return UIApp.keyWindow.rootViewController;
     };

     LBKeyWindow = function(){
        return UIApp.keyWindow;
     };

    LBGetCurrentVCFromRootVc = function(rootVC){
        var currentVC;
        if([rootVC presentedViewController]){
            rootVC = [rootVC presentedViewController];
        }

        if([rootVC isKindOfClass:[UITabBarController class]]){
            currentVC = LBGetCurrentVCFromRootVc(rootVC.selectedViewController);
        }else if([rootVC isKindOfClass:[UINavigationController class]]){
            currentVC = LBGetCurrentVCFromRootVc(rootVC.visibleViewController);
        }else{
            currentVC = rootVC;
        }

        return currentVC;
    };

    LBCurrentVC = function(){
        return LBGetCurrentVCFromRootVc(LBRootvc());
    };
 
})(exports);

小提示

  • 使用 cycript 某一个控件 / 对象 内存地址时 , 要注意其生命周期 , 例如用一个 label 的内存地址调试 , 你页面退出 , 又重进 , 是重新创建了对象的 , 以前的内存地址也会无法再使用 . 不要忘记这点 .

  • cycript 使用需要同局域网每次要连接 , 或者其他我们需要自定义的初始化动作 , 我们可以将其写到一个脚本中 , 配置到 zsh / bash 环境变量中即可 , 大家可以玩一玩 , 有问题大家一起探讨一下 .

  • 逆向过程中 , View Debugcycript 调试界面是非常常用的手段 , 因此 , 希望大家能熟练掌握这些技巧 .