环境搭建是逆向的第一步,本文讲解在 MacOS 逆向中需要的工具以及环境的搭建。
从宏观上来看,逆向分析工具分为以下几类:
接下来详细的介绍每一类工具。
1. 静态分析工具
1.1 class-dump
由于 OC 的动态特性,对于 OC 程序的逆向分析,第一步是 dump 出目标应用的所有头文件。
相对与 Windows、Linux 平台来说,MacOS 平台的逆向是最简单的,其核心原因就是我们可以利用 OC 动态性 dump 出头文件,这是一个重要的切入点。
class-dump
是一个命名行工具,在电脑上安装好之后,使用方法如下。
1
| class-dump -H ../Application/xxx.app/Contents/MacOS/xxx -o Headers
|
命令执行成功之后,所有头文件会 dump 到当前 ./Headers
目录下。
我 dump 了一下某社交软件,发现其头文件有 5438 个。
1 2
| ➜ Headers ls -l |grep "^-"|wc -l 5438
|
dump 出来的头文件,虽然不能完全还原成源代码中的头文件,但也能提供重要的成员变量、成员函数信息,下面是一个 dump 出来头文件的样式。
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
|
#import "PBGeneratedMessage.h"
@class NSString, YYBStruct;
@interface AdAppList : PBGeneratedMessage { unsigned int hasAppId:1; unsigned int hasAppCoverUrl:1; unsigned int hasAppDownloadUrl:1; unsigned int hasAndroidApkMd5:1; unsigned int hasAppName:1; unsigned int hasAppIconUrl:1; unsigned int hasAppSnsDesc:1; unsigned int hasAndroidPackageName:1; unsigned int hasGooglePlayDownloadUrl:1; unsigned int hasSyyb:1; unsigned int hasExtAsXml:1; NSString *appId; NSString *appCoverUrl; NSString *appDownloadUrl; NSString *androidApkMd5; NSString *appName; NSString *appIconUrl; NSString *appSnsDesc; NSString *androidPackageName; NSString *googlePlayDownloadUrl; YYBStruct *syyb; NSString *extAsXml; }
+ (id)parseFromData:(id)arg1; @property(retain, nonatomic, setter=SetExtAsXml:) NSString *extAsXml; @property(readonly, nonatomic) BOOL hasExtAsXml; @property(retain, nonatomic, setter=SetSyyb:) YYBStruct *syyb; @property(readonly, nonatomic) BOOL hasSyyb; @property(retain, nonatomic, setter=SetGooglePlayDownloadUrl:) NSString *googlePlayDownloadUrl; @property(readonly, nonatomic) BOOL hasGooglePlayDownloadUrl; @property(retain, nonatomic, setter=SetAndroidPackageName:) NSString *androidPackageName; @property(readonly, nonatomic) BOOL hasAndroidPackageName; @property(retain, nonatomic, setter=SetAppSnsDesc:) NSString *appSnsDesc; @property(readonly, nonatomic) BOOL hasAppSnsDesc; @property(retain, nonatomic, setter=SetAppIconUrl:) NSString *appIconUrl; @property(readonly, nonatomic) BOOL hasAppIconUrl; @property(retain, nonatomic, setter=SetAppName:) NSString *appName; @property(readonly, nonatomic) BOOL hasAppName; @property(retain, nonatomic, setter=SetAndroidApkMd5:) NSString *androidApkMd5; @property(readonly, nonatomic) BOOL hasAndroidApkMd5; @property(retain, nonatomic, setter=SetAppDownloadUrl:) NSString *appDownloadUrl; @property(readonly, nonatomic) BOOL hasAppDownloadUrl; @property(retain, nonatomic, setter=SetAppCoverUrl:) NSString *appCoverUrl; @property(readonly, nonatomic) BOOL hasAppCoverUrl; @property(retain, nonatomic, setter=SetAppId:) NSString *appId; @property(readonly, nonatomic) BOOL hasAppId; - (void).cxx_destruct; - (id)mergeFromCodedInputStream:(id)arg1; - (int)serializedSize; - (void)writeToCodedOutputStream:(id)arg1; - (BOOL)isInitialized; - (id)init;
@end
|
dump 头文件这个过程虽然简单,但作用很大。通过头文件我们可以分析到:
由于绝大数程序员写出来的类名、函数名都是具备语义的,我们可以从中分析出大量的信息。
分析 OC 头文件为我们逆向分析划开了了一道口子。
1.2 反编译(反汇编)工具
逆向分析的对象是可执行文件(二进制文件),也就是二进制的机器码,机器码很晦涩没有语义,我们必须将其转换成我们能看懂的“符号” – 汇编代码。
机器码和汇编代码是一一对应的,在这个层面上,其实所有的反汇编工具没有什么差异,他们完成的工作就是“翻译”,一一对应的翻译,我用过的工具有:
- 朴素的命令行工具
objdump
- 重剑
ida
- Mac 平台的后起之秀
hopper
但是,我们的诉求不仅仅是将机器码翻译成汇编代码!
我们面对的往往是大型商用软件,接近 200M 的可执行文件翻译过来的汇编代码也是海量的。
如果你水平很高,面对汇编代码的你的效率也是低下的;即使你水平很高,计算机专家,仅仅分析汇编也是低效的。
所以,我们还依赖反汇编工具的两项项重要功能:
由于汇编语言到高级语言不是一一对应的,这个过程就很考验反汇编工具的水平了,按照我使用经验,Hopper 基本够用,ida 技高一筹!
我用的最多的是 Hopper,原因是它比较便宜,license 在我买的起的范畴。
这就是 Hooper 分析完成的界面,左边一栏是符号,右边是汇编代码。
这段汇编代码对应的伪 C 代码如下。
函数调用关系图如下。
借助工具,大大提高逆向分析的效率。
2. 动态调试工具
接下来我介绍的逆向调试工具,本质上都是一个 – lldb,无论是 Xcode 还是 Hopper,负责调试的模块都是 lldb.
2.1 用 Xcode 调试 UI
UI 分析是我们逆向分析一个程序的重要切入点。
逆向分析某个功能的时候,往往最先试图去找和这个功能关联的界面,比如我们感兴趣的功能可以通过一个按钮的点击触发,那我们如果可以找到这个按钮所在界面,然后找到其对应的 ViewController,紧接着再去分析 ViewController 找到响应按钮点击的方法,最后在响应点击的方法里面找到相应的方法(函数)调用,这就完成了一个功能的逆向分析。
所以可以看出,通过 UI 元素顺藤摸瓜,是我们的一个重要的逆向分析手段。
对于原生应用来说,我使用 Xcode 来分析 UI.
step1 新建一个 MacOS 应用
在 Xcode 中新建一个 MacOS 应用,这里选择 App 模板,应用命名为 HelloWorld。
step2 attatch 目标程序
Edit Scheme > Excutable > Other > 选择/Application/xxx.app
.
xxx.app 是我们要分析的目标程序。
完成之后,目标程序的 icon 会在 Xcode Debug 窗口显示出来。
step3 Run Project
Cmd + R 运行程序,程序运行时候会执行我们上面勾选的 xxx.app,调试器会 attach xxx.app 进程,我们就可以调试目标应用了。
注意:如果我们调试的目标应用不支持多开,我们调试前需要关闭正在运行的进程。
如我们所预期,程序运行起来了,并且调试器 attach 了进程,我们可以在调试窗口对程序进行调试,在控制台窗口看到程序输出的日志。
UI 调试
点击调试窗口的 UI 调试按钮,我们就可以很容易的看出应用当前界面的 UI 层级,通过 UI 层级我们就可以很容易的找到我们感兴趣的类。
比如在这个示例中,我们如果要分析二维码登录功能,我们很快的就定位到了和这个功能相关的两个类:MMMainWindowController
和 MMLoginQRCodeViewController
.
管中窥豹,可见一斑,至此我们对 UI 逆向分析的威力有所理解。
2.2 用 Xcode 进行反调试
和 UI 调试一样,我们如果可不可以对目标进程进行代码调试呢?
答案是可以的,我们可以借助符号断点来做。
比如,我可以对 viewDidLoad
这个函数添加符号断点,运行程序,程序会在相应位置停下来。
此时我们就可以向调试正向代码一样,对程序进行调试。
Xcode 可以逆向调试,但功能不太完善(毕竟它是设计了给正向开发用的),这些不完善体现在:
- 不能在任意位置下断点,通过符号下断点有一定的局限
- 可以看到寄存器的值,但是栈上的变量、以及调试过程中栈上值的变化看不到
- 内存调试不好使
Xcode 可以进行反汇编调试,但不是第一选择。
2.3 用 Hopper 进行反调试
通过前面我们知道,利用 Hopper 可以对二进制进行反汇编,它的功能不限于此,我们还可以用它来进行动态调试。
在反汇编代码展示窗口,我们可以在任意反汇编代码处下断点。
下面是我实际开发中,应用 Hopper 进行调试的截图,在左边的调试窗口中我们可以看到函数调用堆栈、内存、日志等我们感兴趣的调试信息。
这样就将调试可执行文件变成了调试汇编代码,从体验上就和正向开发调试源代码没有差异了。
2.4 用 Frida 进行反调试
Frida 的使用,单独写一篇文章来描述。
3. 注入工具 insert_dylib
insert_dylib
insert_dylib
是一个命名行工具,用来将动态库注入到目标可执行文件(Mach-O)中。
它的执行原理是给 Mach-O Header 里面插入 LC_LOAD_DYLIB
load command,让可执行文件可以加载我们的“外挂”库。这个修改之后,牵一发而动全身,我们还需要做以下修改:
- 修改 Mach-O Header 的 ncmds 和 sizeofcmds 字段
- 如果可执行文件有签名的话,移除其签名
1 2
| Usage: insert_dylib dylib_path binary_path [new_binary_path] Option flags: --inplace --weak --overwrite --strip-codesig --no-strip-codesig --all-yes
|
4. 开发环境搭建
4.1 前置知识
- Mach-O Header 的文件结构,以及如何进行动态注入
- MacOS 应用启动流程
__constructor
& category,了解如何让我们写的代码运行起来
4.2 新建 Framework project
新建 Framework project,模板选择 MacOS > Framework。
项目命名为 WeComPlugin。
给新建的项目添加一个新的文件,命名为 Hook.m 。
Edit Scheme > Run > Excutable,选择企业微信。
4.3 添加注入脚本
在项目中新建一个脚本文件,命名为 dylib_insert.sh
,文件放在项目根目录下。然后将 insert_dylib
放在同级目录里面。
注入脚本做两件事儿:
- 将可执行文件备份,因为注入会破坏原本的可执行文件
- 调用
insert_dylib
,将我们编写的 WeComPlugin.framework 注入到企业微信中去
完整的代码如下。
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
| #!/bin/sh
echo "======== start run dylib_insert.sh ========"
app_name="企业微信" shell_path=`pwd` framework_name="WeComPlugin" app_bundle_path="/Users/xieshoutan/Workspace/ReverseDev/4.0/test/企业微信.app/Contents/MacOS"
app_executable_path="${app_bundle_path}/${app_name}" app_executable_backup_path="${app_executable_path}_backup"
framework_path="${BUILT_PRODUCTS_DIR}/${framework_name}.framework"
if [ ! -f "$app_executable_backup_path" ] then cp "$app_executable_path" "$app_executable_backup_path" fi cp -r "${framework_path}" ${app_bundle_path}
echo "framework_path:${framework_path}/${framework_name}" echo "app_executable_backup_path:${app_executable_backup_path}" echo "app_executable_path:${app_executable_path}"
./insert_dylib --all-yes "${framework_path}/${framework_name}" "$app_executable_backup_path" "$app_executable_path"
echo "======== end run dylib_insert.sh ========"
|
在 Build Phases 中添加 Run Script.
添加如下脚本, 每次 Xcode 调试的时候,将本次编译的 WeComPlugin.framework 注入到企业微信中去。
1
| sh ${SRCROOT}/dylib_insert.sh
|
4.4 编写 Hook 代码
在 Hook.m 中,添加如下代码。
1 2 3 4 5 6 7 8 9 10 11 12
|
#import <Foundation/Foundation.h>
static void __attribute__ ((constructor)) tweak(void) { NSLog(@"hook ==> %s",__func__); }
|
Cmd+R,运行程序,企业微信程序运行起来,并且我们的代码有执行。
至此,我们走出了逆向第一步,知道逆向的基本流程,并且完成了开发环境的搭建。
参考文档
Apple M1,逆向环境设置与我自己遇到了很多坑逐一解决,给后来的同学一个参考吧
Building a class-dump in 2019 2020