MacOS逆向工具介绍 & 环境搭建

环境搭建是逆向的第一步,本文讲解在 MacOS 逆向中需要的工具以及环境的搭建。

从宏观上来看,逆向分析工具分为以下几类:

  • 静态分析工具
    • class-dump
    • 反汇编工具
  • 动态调试工具
    • UI 调试
    • 反汇编调试
  • 注入工具

接下来详细的介绍每一类工具。

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
//
// Generated by class-dump 3.5 (64 bit) (Debug version compiled Feb 10 2023 15:13:52).
//
// Copyright (C) 1997-2019 Steve Nygard.
//

#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; // @synthesize extAsXml;
@property(readonly, nonatomic) BOOL hasExtAsXml; // @synthesize hasExtAsXml;
@property(retain, nonatomic, setter=SetSyyb:) YYBStruct *syyb; // @synthesize syyb;
@property(readonly, nonatomic) BOOL hasSyyb; // @synthesize hasSyyb;
@property(retain, nonatomic, setter=SetGooglePlayDownloadUrl:) NSString *googlePlayDownloadUrl; // @synthesize googlePlayDownloadUrl;
@property(readonly, nonatomic) BOOL hasGooglePlayDownloadUrl; // @synthesize hasGooglePlayDownloadUrl;
@property(retain, nonatomic, setter=SetAndroidPackageName:) NSString *androidPackageName; // @synthesize androidPackageName;
@property(readonly, nonatomic) BOOL hasAndroidPackageName; // @synthesize hasAndroidPackageName;
@property(retain, nonatomic, setter=SetAppSnsDesc:) NSString *appSnsDesc; // @synthesize appSnsDesc;
@property(readonly, nonatomic) BOOL hasAppSnsDesc; // @synthesize hasAppSnsDesc;
@property(retain, nonatomic, setter=SetAppIconUrl:) NSString *appIconUrl; // @synthesize appIconUrl;
@property(readonly, nonatomic) BOOL hasAppIconUrl; // @synthesize hasAppIconUrl;
@property(retain, nonatomic, setter=SetAppName:) NSString *appName; // @synthesize appName;
@property(readonly, nonatomic) BOOL hasAppName; // @synthesize hasAppName;
@property(retain, nonatomic, setter=SetAndroidApkMd5:) NSString *androidApkMd5; // @synthesize androidApkMd5;
@property(readonly, nonatomic) BOOL hasAndroidApkMd5; // @synthesize hasAndroidApkMd5;
@property(retain, nonatomic, setter=SetAppDownloadUrl:) NSString *appDownloadUrl; // @synthesize appDownloadUrl;
@property(readonly, nonatomic) BOOL hasAppDownloadUrl; // @synthesize hasAppDownloadUrl;
@property(retain, nonatomic, setter=SetAppCoverUrl:) NSString *appCoverUrl; // @synthesize appCoverUrl;
@property(readonly, nonatomic) BOOL hasAppCoverUrl; // @synthesize hasAppCoverUrl;
@property(retain, nonatomic, setter=SetAppId:) NSString *appId; // @synthesize appId;
@property(readonly, nonatomic) BOOL hasAppId; // @synthesize 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 的可执行文件翻译过来的汇编代码也是海量的。

image.png

如果你水平很高,面对汇编代码的你的效率也是低下的;即使你水平很高,计算机专家,仅仅分析汇编也是低效的。

所以,我们还依赖反汇编工具的两项项重要功能:

  • 翻译汇编代码为伪 C/C++ 代码
  • 函数调用关系

由于汇编语言到高级语言不是一一对应的,这个过程就很考验反汇编工具的水平了,按照我使用经验,Hopper 基本够用,ida 技高一筹!

我用的最多的是 Hopper,原因是它比较便宜,license 在我买的起的范畴。

这就是 Hooper 分析完成的界面,左边一栏是符号,右边是汇编代码。

image.png

这段汇编代码对应的伪 C 代码如下。

image.png

函数调用关系图如下。

image.png

借助工具,大大提高逆向分析的效率。

2. 动态调试工具

接下来我介绍的逆向调试工具,本质上都是一个 – lldb,无论是 Xcode 还是 Hopper,负责调试的模块都是 lldb.

2.1 用 Xcode 调试 UI

UI 分析是我们逆向分析一个程序的重要切入点。

逆向分析某个功能的时候,往往最先试图去找和这个功能关联的界面,比如我们感兴趣的功能可以通过一个按钮的点击触发,那我们如果可以找到这个按钮所在界面,然后找到其对应的 ViewController,紧接着再去分析 ViewController 找到响应按钮点击的方法,最后在响应点击的方法里面找到相应的方法(函数)调用,这就完成了一个功能的逆向分析。

所以可以看出,通过 UI 元素顺藤摸瓜,是我们的一个重要的逆向分析手段。

对于原生应用来说,我使用 Xcode 来分析 UI.

step1 新建一个 MacOS 应用

在 Xcode 中新建一个 MacOS 应用,这里选择 App 模板,应用命名为 HelloWorld。

image.png

step2 attatch 目标程序

Edit Scheme > Excutable > Other > 选择/Application/xxx.app.

xxx.app 是我们要分析的目标程序。

image.png

完成之后,目标程序的 icon 会在 Xcode Debug 窗口显示出来。

image.png

step3 Run Project

Cmd + R 运行程序,程序运行时候会执行我们上面勾选的 xxx.app,调试器会 attach xxx.app 进程,我们就可以调试目标应用了。

注意:如果我们调试的目标应用不支持多开,我们调试前需要关闭正在运行的进程。

如我们所预期,程序运行起来了,并且调试器 attach 了进程,我们可以在调试窗口对程序进行调试,在控制台窗口看到程序输出的日志。

image.png

UI 调试

点击调试窗口的 UI 调试按钮,我们就可以很容易的看出应用当前界面的 UI 层级,通过 UI 层级我们就可以很容易的找到我们感兴趣的类。

image.png

比如在这个示例中,我们如果要分析二维码登录功能,我们很快的就定位到了和这个功能相关的两个类:MMMainWindowControllerMMLoginQRCodeViewController.

管中窥豹,可见一斑,至此我们对 UI 逆向分析的威力有所理解。

2.2 用 Xcode 进行反调试

和 UI 调试一样,我们如果可不可以对目标进程进行代码调试呢?

答案是可以的,我们可以借助符号断点来做。

比如,我可以对 viewDidLoad 这个函数添加符号断点,运行程序,程序会在相应位置停下来。

image.png

此时我们就可以向调试正向代码一样,对程序进行调试。

Xcode 可以逆向调试,但功能不太完善(毕竟它是设计了给正向开发用的),这些不完善体现在:

  • 不能在任意位置下断点,通过符号下断点有一定的局限
  • 可以看到寄存器的值,但是栈上的变量、以及调试过程中栈上值的变化看不到
  • 内存调试不好使

Xcode 可以进行反汇编调试,但不是第一选择。

2.3 用 Hopper 进行反调试

通过前面我们知道,利用 Hopper 可以对二进制进行反汇编,它的功能不限于此,我们还可以用它来进行动态调试。

在反汇编代码展示窗口,我们可以在任意反汇编代码处下断点。

image.png

下面是我实际开发中,应用 Hopper 进行调试的截图,在左边的调试窗口中我们可以看到函数调用堆栈、内存、日志等我们感兴趣的调试信息。

image

这样就将调试可执行文件变成了调试汇编代码,从体验上就和正向开发调试源代码没有差异了。

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 前置知识

  1. Mach-O Header 的文件结构,以及如何进行动态注入
  2. MacOS 应用启动流程
  3. __constructor & category,了解如何让我们写的代码运行起来

4.2 新建 Framework project

新建 Framework project,模板选择 MacOS > Framework。

image.png

项目命名为 WeComPlugin。

image.png

给新建的项目添加一个新的文件,命名为 Hook.m 。

image.png

Edit Scheme > Run > Excutable,选择企业微信。

image.png

4.3 添加注入脚本

在项目中新建一个脚本文件,命名为 dylib_insert.sh,文件放在项目根目录下。然后将 insert_dylib 放在同级目录里面。

image.png

注入脚本做两件事儿:

  1. 将可执行文件备份,因为注入会破坏原本的可执行文件
  2. 调用 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

# dylib_insert.sh
# WeComPlugin
#
# Created by xieshoutan on 2023/2/13.
#
echo "======== start run dylib_insert.sh ========"

app_name="企业微信" # 要注入的的app名称
shell_path=`pwd`
framework_name="WeComPlugin" # 此framework名字
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="${shell_path}/Framework/${framework_name}.framework"
framework_path="${BUILT_PRODUCTS_DIR}/${framework_name}.framework"
# 备份WeChat原始可执行文件
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.

image.png

添加如下脚本, 每次 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
//
// Hook.m
// WeComPlugin
//
// Created by xieshoutan on 2023/2/13.
//

#import <Foundation/Foundation.h>

static void __attribute__ ((constructor)) tweak(void) {
NSLog(@"hook ==> %s",__func__);
}

Cmd+R,运行程序,企业微信程序运行起来,并且我们的代码有执行。

image.png

至此,我们走出了逆向第一步,知道逆向的基本流程,并且完成了开发环境的搭建。

参考文档

Apple M1,逆向环境设置与我自己遇到了很多坑逐一解决,给后来的同学一个参考吧

Building a class-dump in 2019 2020


MacOS逆向工具介绍 & 环境搭建
http://example.com/2024/05/28/MacOS逆向工具介绍-&-环境搭建/
作者
guanzhendong
发布于
2024年5月28日
许可协议