一篇文章走进Mac逆向的世界

原文链接:http://www.alonemonkey.com/2017/05/31/get-start-with-mac-reverse/

前言

在玩了iOS逆向之后,看到Mac上面的应用莫名有了一种想要搞事情的冲动。其实在思想上iOS逆向和Mac逆向是差不多的,原理都差不多,走的流程可能不太一样,这篇文章的目的主要是让大家了解一下Mac上面玩逆向的大致流程和分析方法,所以在本文中去实现什么样的功能并不是重点,重点是让大家懂得怎么去分析找到实现。

目的

既然是逆向分析,总的有个目的,有目的才有动力,那么就以QQ撤回这个功能为例吧,网上也有很多文章讲解应该hook哪个函数,具体的分析过程却不是那么清晰,所以这里还是着重分析的过程,最终的实现大家自由发挥。

分析思路

在iOS逆向分析中,大致的思想是这样的(不同情况略有差异):

界面分析  ->  动态分析  ->  静态分析  ->  动态库注入  ->  ...  

那么在Mac逆向过程中,也可以按照这个套路来搞一波。

界面分析

要分析QQ撤回的动作,这个动作是有在界面上面体现的,别人撤回一条消息后,那条消息会变成被撤回的通知。所以先分析界面上负责处理该状态的类,然后再去看类的调用。

在Mac上面也有类似iOS Reveal的工具叫做Interface Inspector

下载下来后打开,发现是一个需要License的货,既然遇到了那就搞一搞吧。

image

首先从界面关键词Register入手吧,拖到hopper,在Strings里面搜索Register

image

找到之后按x查找引用关系,发现是在-[SMEnterLicenseViewController loadView]这个函数里面存在这个字符串。

image

继续找到action-[SMEnterLicenseViewController register:]

里面会调用 [r14 enterLicenseViewControllerDidSelectRegister:r15 withLicenseName:r12 code:rbx];

image

这里registerLicenseWithName:code:里面是通过[SMLicenseManager verifyLicenseWithName:code:]验证的。

那直接修改代码返回YES

Modify -> Assemble Instruction...

输入:

mov        eax, 0x1    //eax用来保存返回值
ret

生成一个新的可执行文件,File -> Produce New Executable...

选择Remove Signature

替换原可执行文件,然后打开,弹出了:

image

看来它还验证签名了。

那么找到启动时的回调:

 -[SMAppDelegate applicationWillFinishLaunching:]

可以找到对应的代码。

if ([rax codeSignState] != 0x2) goto loc_100024851;

如果codeSignState返回不为2就会弹出刚刚那个。

所以这里把:

jne        loc_100024851

对应的二进制0F 85 CD 03 00 00改成0F 84 CD 03 00 00

重新打开不显示那个签名问题,但是直接闪退了。

猜测某些地方还有检测,然后exit或者terminate了。

Strings中查找字符串找到terminate:的引用。

image

查看每一处调用,发现这么一段代码:

void ___24+[SMLicenseManager load]_block_invoke(void * _block) {
    if ([SMLicenseManager verifyLicenseWithName:@"Test User" code:@"GAWAE-8C69D-7LZ5H-9D8M3-HVEG7-KHNQC-CQ7RF-SEPQC-CRF82-G47U5-H6DKAB-8SKA7-EWSCM-7Q7SV-MYF4"] != 0x0) {
            [*_NSApp terminate:0x0];
    }
    return;
}

作者自己拿了假的License测试,如果通过就直接退出了。

所以这里je loc_10010ec26对应二进制74 1A改成75 1A

然后重新生成可执行文件,打开随便填入License注册即可。

额,好像有点扯远了,不过本文的目的就是为了学习Mac逆向的分析过程,所以这一部分并不是多余的。

当然这里还有坑,如何你发现attach process的时候报错的话,请参考Fix Bug for Interface Inspector on macOS Serria这篇文章重新编译mach_inject_bundle_stub.bundle

终于准备好了,附加到QQ,选中聊天界面。可以找到MQAIOChatViewController这样的一个ViewController

image

下面通过动态跟踪函数调用来看看这个类的调用流程。

动态跟踪

动态跟踪的方法有多种,除了iOS逆向中使用的Frida, lldb , Mac上面还可以使用Dtrace。

Frida

Frida是一个通过jsNative交互的跨平台工具,可以通过frida QQ的方式附加,查看当前加载的类,类的方法等等。除此外还可以加载自己的js脚本frida QQ -l trace.js

python中,有可以import frida来获取设置以及目标等相关信息,同时也能调用js并获取返回结果。

ObjC.classes.MQAIOChatViewController.$methods

frida还提供了一个非常有用的功能,可以用于此处,也就是frida-trace

可以通过这个工具来跟踪指定方法或者指定类的调用过程,这里感兴趣的是MQAIOChatViewController这个类的调用过程,所以可以通过如下命令来跟踪:

frida-trace -m "-[MQAIOChatViewController *]" QQ

然后撤回一条消息可以看到如下调用过程:

 25623 ms  -[MQAIOChatViewController revokeMessages:0x618000206220 ]
 25623 ms     | -[MQAIOChatViewController topMsgListViewController]
 25623 ms     | -[MQAIOChatViewController topMsgListViewController]
 25625 ms     | -[MQAIOChatViewController scrollViewDidScrollToBottom:0x7fc28b0dffb0 ]
 25632 ms     | -[MQAIOChatViewController scheduleRecognitionLogic]
 25633 ms     | -[MQAIOChatViewController topMsgListViewController]
 25639 ms  -[MQAIOChatViewController isSimplestModel]
 25639 ms  -[MQAIOChatViewController isSimplestModel]
 25639 ms  -[MQAIOChatViewController isSimplestModel]

这便找到了一个上层撤回的调用,后面还会继续深入。

Dtrace

lldb前,先讲下Dtrace这个Linux上的动态追踪神器,可以通过它来监控应用程序或者内核的调用,所以这里可以用于监控OC函数的调用。

新建文件trace.d,写入内容:

#!/usr/sbin/dtrace -s

objc$target:MQAIOChatViewController::entry{
	
}

然后找到QQ的进程id ps -e | grep QQ。再运行dtrace脚本

sudo ./trace.d -p 24105

撤回消息可得到如下输出结果:

2  30436           -revokeMessages::entry
2  44561  -topMsgListViewController:entry
2  44561  -topMsgListViewController:entry
2  30382 -scrollViewDidScrollToBottom::entry
2  44569  -scheduleRecognitionLogic:entry
2  44561  -topMsgListViewController:entry
6  44608           -isSimplestModel:entry
6  44608           -isSimplestModel:entry
0  44608           -isSimplestModel:entry

也可以直接运行:

sudo dtrace -n 'objc$target:MQAIOChatViewController::entry{}' -p 24105

只是在文件里面可以进行更多的操作。

lldb

lldb这部分的动态跟踪在后面Xcode调试会有讲到。

Xcode

找到了函数之后,可以写个动态库注入为了方便调试以及动态库注入这里创建一个dylib的Xcode项目,选择macOS Library,点击创建,Type选择Dynamic

为了Hook函数,先选择一个现成的库来做,可以选择:

这里你可能会想一个撤回一条命令一个文件哪有这么麻烦,而这些都是一般逆向中可能用到的,不针对功能。

我个人倾向于使用substitute,它有着和substrate一样的api接口。

那么首先要编译一个Mac平台的substitute动态库。

直接编译会报错syscall被弃用,到这里下载一个早一点的比如MacOSX10.11.sdk,然后放到/Applications/XCode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs下。

然后通过如下命令行编译:

./configure --xcode-sdk macosx10.11 && make -j8

out目录就可以看到生成的动态库。

查看动态库的加载地址,把其重命名为libsubstitute.0.dylib并拷贝到/usr/local/lib/

➜  out git:(master) ✗ otool -L libsubstitute.dylib
libsubstitute.dylib:
	/usr/local/lib/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)

然后把头文件和动态库引入Xcode项目, 然后在构造函数里面hook-[MQAIOChatViewController revokeMessages:]

#import "substrate.h"

@class MQAIOChatViewController;

static void (*origin_MQAIOChatViewController_revokeMessages)(MQAIOChatViewController*,SEL,id);
static void new_MQAIOChatViewController_revokeMessages(MQAIOChatViewController* self,SEL _cmd,id arrays){
    NSLog(@"revokeMessages:%@",arrays);
    return;
}

static void __attribute__((constructor)) initialize(void) {
    MSHookMessageEx(objc_getClass("MQAIOChatViewController"),  @selector(revokeMessages:), (IMP)&new_MQAIOChatViewController_revokeMessages, (IMP*)&origin_MQAIOChatViewController_revokeMessages);
}

编译生成动态库libMacQQRevoke.dylib

动态库注入

动态库注入可以通过DYLD_INSERT_LIBRARIES注入,也可以直接注入到可执行文件的Load Command

这里为了方便就直接通过环境变量注入了。

点击Build Phases左上角的+,然后点击New Run Script Phase

image

写入脚本:

cd ${TARGET_BUILD_DIR}
export DYLD_INSERT_LIBRARIES=./libMacQQRevoke.dylib && /Applications/QQ.app/Contents/MacOS/QQ

Command + B, 注入动态库,运行QQ。

然后撤回消息,发现消息还在,并且控制台有如下输出。

image

也就是Hook生效了,但是重启QQ后,那条消息还是变成了撤回状态。

这个时候需要继续往上层调用去探索。

符号还原+Xcode调试

为了能看到revokeMessages:被调用的堆栈,先使用restore-symbol还原符号,然后再使用unsign去掉签名。

./restore-symbol QQ -o QQ_symbol
mv QQ_symbol QQ
./unsign QQ
mv QQ.unsigned QQ

替换原可执行文件之后,在new_MQAIOChatViewController_revokeMessages断点,Command + B重新运行。

然后选择Debug -> Attach to Process

image

附加到QQ进程。

撤回消息,Xcode便断在了new_MQAIOChatViewController_revokeMessages

查看堆栈如下:

image

因为block的符号没恢复,所以看来是block里面调用过来的。

找到block内存地址0x103a69663减去加载基地址0x0000000003757000,对应到文件偏移0x0000000100312663

Hopper加载文件按g跳转到指定地址:

image

image

所以可以找到是从:

-[BHMessageChatModel revokeMessageModel:]
-[MQAIOChatViewController chatMessageModel]

调过来的。
revokeMessageModel下断点b -[BHMessageChatModel revokeMessageModel:],再撤回,可以看到调用堆栈。

image

这下应该就比较清楚了。

那么就在比较上层的调用中去处理,比如:

-[RecallProcessor solveRecallNotify:isOnline:]
-[QQMessageRevokeEngine handleRecallNotify:isOnline:]

测试直接hook-[QQMessageRevokeEngine handleRecallNotify:isOnline:]是不会再本地删除的。

关于hook中的参数类型可以使用class-dump dump出头文件,从头文件可以看到第一个参数是struct RecallModel类型。

lldb

补充一下上面说的lldb跟踪程序,有了符号直接可以直接通过符号断点来跟踪程序。比如:

rb \[MQAIOChatViewController *\]
br com add 1
po $rdi
x/s $rsi
c
DONE

撤回消息即可看到打印结果如下:

po $rdi
<MQAIOChatViewController: 0x7fca3a8def10>

 x/s $rsi
0x10fa1f89e: "revokeMessages:"
 c
Process 35302 resuming
Command #3 'c' continued the target.
 po $rdi
<MQAIOChatViewController: 0x7fca3a8def10>

 x/s $rsi
0x10fa07e45: "topMsgListViewController"
 c
Process 35302 resuming
Command #3 'c' continued the target.
 po $rdi
<MQAIOChatViewController: 0x7fca3a8def10>

 x/s $rsi
0x10fa07e45: "topMsgListViewController"
 c
Process 35302 resuming
Command #3 'c' continued the target.
 po $rdi
<MQAIOChatViewController: 0x7fca3a8def10>

 x/s $rsi
0x10fa1f973: "scheduleRecognitionLogic"
 c
Process 35302 resuming
Command #3 'c' continued the target.
 po $rdi
<MQAIOChatViewController: 0x7fca3a8def10>

 x/s $rsi
0x10fa07e45: "topMsgListViewController"
 c
Process 35302 resuming
Command #3 'c' continued the target.
 po $rdi
<MQAIOChatViewController: 0x7fca3a8def10>

 x/s $rsi
0x10f9e881c: "isSimplestModel"
 c
Process 35302 resuming
Command #3 'c' continued the target.

在x64汇编中,传递参数依次通过以下寄存器传递。

rdi, rsi, rdx, rcx, r8, r9

在很多动态分析的过程中可以借助lldb调试来帮助分析解决问题。

MachO注入

为了避免每次从环境变量注入,还可以直接使用insert_dylib注入可执行文件。

./insert_dylib --weak /path/to/dylib QQ QQ_patch
mv QQ_patch QQ

总结

总的来说,Mac上的逆向和iOS逆向差不太多,反而Mac上面操作更方便一些,不用连着手机,还有像Dtrace这样的神器。

这篇文章主要还是领大家走进Mac逆向的世界,其它靠自己慢慢去探索~

相关工具和代码尽在github

9 个赞

厉害了,最近正好也想学习一波 Mac 逆向。感谢楼主。

话说。。楼主的头像跟我好像是同一款。

想请教下,为什么不直接用frida去hook关键函数

想请教下,为什么不直接手写汇编去hook关键函数

高产啊,膜。

dtrace: failed to match pid58860:MQAIOMessageViewController::entry: No probe matches description
我不知道是我操作问题还是环境的问题 总是无法匹配

修改完会显示这个:

文章里面不是写了吗

好歹文章写了那么清楚,看清楚再操作

你操作过了吗?你确定我没看完和完成后续操作么

请问

jne loc_100024851
对应的二进制0F 85 CD 03 00 00改成0F 84 CD 03 00 00。

这一步怎么操作,操作这步原理是啥

http://ref.x86asm.net/coder64.html
你可以查一下 jne je,实际上就是条件判断反过来

大佬,不知道为什么我直接拿你的 MacQQRevoke build ,QQ 会直接崩溃
不过我自己拿 runtime 重新写的,一样配置就不会崩了。。
是 libsubstitute.dylib 这个库要自己编译再放进去么?

是的……
其实他的需求可能并不需要substitute

破解Interface Inspector 的时候 到
void ___24+[SMLicenseManager load]_block_invoke(void * _block) {
if ([SMLicenseManager verifyLicenseWithName:@“Test User” code:@“GAWAE-8C69D-7LZ5H-9D8M3-HVEG7-KHNQC-CQ7RF-SEPQC-CRF82-G47U5-H6DKAB-8SKA7-EWSCM-7Q7SV-MYF4”] != 0x0) {
[*_NSApp terminate:0x0];
}
return;
}
修改 je 这步 为止 都是正常的 修改 这步je 为 75 后 生成可执行文件 打开 出现崩溃

你在这个地方单步调试啊

为什么我一改je指令的数据,就不识别指令了呢,如果直接改指令是行的。修改数据的地方只能单个字节改
原来是74 38

很好的文章,收藏了,楼主很是厉害,向你学习。

你好,我也遇到这个问题,折腾了1整天了,还没解决,请问你解决了吗?