从微信扔骰子看iOS应用安全与逆向分析

有价值炮灰

共 30880字,需浏览 62分钟

 ·

2021-04-12 06:39

前言

在之前《免越狱调试与分析黑盒iOS应用》[1]以及前几篇文章中已经介绍了如何开始分析iOS应用,不过都是基于非越狱的机器,其本意是为了能够在自己的主力设备中进行简单的分析和调试。但是执着于免越狱其实在很多情况下需要额外的工作,如果想要在iOS上做进一步研究的话,完全的访问权限是很有必要的。

Jailbreak

本文主要讨论的是应用安全,因此关于越狱实现的部分不做深入介绍。关于XNU内核漏洞的分析和利用网上有很多相关的文章或书籍,比如:

An adventure into the XNU kernel[2]Project Zero - SockPuppet: A Walkthrough of a Kernel Exploit for iOS 12.4[3]Project Zero - A survey of recent iOS kernel exploit[4]Secfault Security - Writing an iOS Kernel Exploit from Scratch[5]《MacOS and iOS Internals》(*OS Internals)...

看一些公开的漏洞利用代码对加深印象也很有帮助,比如oob_timestamp[6]的利用。对于本文而言只需要站在巨人的肩膀上使用这些封装好的利用即可。iOS越狱和Android root的的一个很大不同是前者系统封闭性高,碎片化较低,因此提权的方法也相对单一,不支持刷机,大部分都是通过漏洞去获取更高的权限(tfp0)。既然是专注应用层安全,就抓大放小,使用现有的越狱工具[7],站在前人肩膀上即可。

砸壳

越狱之后总算可以通过 ssh 进入到系统中,也就是相当于安卓世界的 adb shell 而已。既然是逆向分析,下一步就是获取应用的安装包,这在安卓中是一条adb pull命令,但苹果里要复杂一些。

iOS 中大部分应用都从 Apple Store 即应用商店下载,而从应用商店下载的 app 是通过苹果签名和加密保护的,这是苹果 FairPlay DRM 数字版权保护的重要部分。为了能使用逆向工具进行分析,需要先对其进行解密,即俗称的砸壳。网上关于砸壳的资料和工具都很多,比如:

stefanesser/dumpdecrypted[8]:手动将动态库注入进程获取解密后的文件,不包括动态库conradev/dumpdecrypted[9]:更新版,支持对每个模块进行解密Clutch[10]:通过posix_spawnp创建进程并解密文件,支持重新打包为ipa (iOS12之后这类静态砸壳方法基本上都不行了)frida-ios-dump[11]:使用frida进行动态解密,并支持重新打包为ipabagbak[12]:和前者类似,不过是基于 nodejs 的 frida binding

这里面有些工具已经年代久远年久失修了,比如我就注意到 dumpdecrypted 在文件沙盒的处理上有点问题导致无法保存文件,需要经过简单 patch 。

上面的这些工具大部分都是基于内存 dump,也就是需要通过 exec 执行目标程序才实现砸壳,这可能会让一些 App 通过在初始化函数中自我检测来对抗砸壳。如果出现这种情况,可以通过基于 mremap_encrypted[13] 的方式进行解密,直接调用内核接口,无需启动目标程序实现静态砸壳。

抓手

狱也越了,壳也砸了,接下来呢?直接丢到 IDA 里分析?不是不可以,但 iOS 的应用主体是一个巨大的 mach-o[14] 文件,直接分析是很难的。还是以微信为例,主程序解密后单 arm64 架构的可执行文件就有 218 MB,即便在逆向工具里分析也会让人无从下手。这时候就需要一个入手点,用互联网的黑话来说,就是需要一个抓手。

一个最常见的入手点就是 UI 界面。在安卓应用分析中一般通过 Android Studio 提供的uiautomatorviewer可以进行目标应用的 UI 分析,并通过 UI 的 ID 在反编译的代码中查找引用。不过这个方法我一般不用,而是直接获取顶层 Activity 然后找对应的类去分析。

在 iOS 中,UI 分析却是一个有效的入手点,因为 iOS 应用都是基于 MVC 结构,View 中触发的事件由对应的 Controller 去实现。不管是基于 Springboard 的拖拖拽拽还是通过代码布局,MVC 的基调是不变的。在 Xcode 中有视图层级调试功能(Debug View Hierarchy),但需要目标开启调试。除此之外,更为常用的是一些视图调试框架,比如 Reveal 或者开源的 FLEX。

用 FLEX[15] 直接注入到进程中可实现 UI 的分析:

这里用到了 cycript,后文再细说。或者可以用 Reveal 在电脑端分析,当然还是需要将 RevealServer.dylib 注入到目标进程中并调用[IBARevealLoader startServer],参考官方的 lldb 脚本,启动后 PC 端界面如下:

Reveal

点击发送骰子的区域是一个 UIImageView,其本身是没有响应点击事件的,因此要找对应的 ViewController 或者 Gesture Recognizer。

动态分析

根据 UI 信息能知道的是当前界面的 Controller 类是 BaseMsgContentViewController,所以我们可以通过脚本去跟踪发送骰子时该类所有的函数调用,如下所示:

[-] 13:04:24.188 Hooked: -[BaseMsgContentViewController didRotateFromInterfaceOrientation:][-] 13:04:24.188 Hooked: -[BaseMsgContentViewController previewingContext:viewControllerForLocation:][-] 13:04:24.188 Hooked: -[BaseMsgContentViewController previewingContext:commitViewController:][-] 13:04:24.188 Hooked: -[BaseMsgContentViewController previewActionItems][-] 13:04:24.189 Hooked: -[BaseMsgContentViewController m_delegate][!] 13:04:24.291 Failed to hook: -[BaseMsgContentViewController willShow][-] 13:04:24.291 Hooked: -[BaseMsgContentViewController canPasteImage][-] 13:04:24.292 Hooked: -[BaseMsgContentViewController setTableFooterView:][-] 13:04:24.292 Hooked: -[BaseMsgContentViewController documentInteractionControllerViewControllerForPreview:][-] 13:04:24.292 Hooked: -[BaseMsgContentViewController willAppear][-] 13:04:24.292 Hooked: -[BaseMsgContentViewController showLoadingView][-] 13:04:24.293 Hooked: -[BaseMsgContentViewController initTableView][-] 13:04:24.293 Hooked: 614 methods for class BaseMsgContentViewController# 点击骰子图标发送表情[-] 13:04:35.075 ENTER -[BaseMsgContentViewController useTransparentNavibar][-] 13:04:35.076 ENTER -[BaseMsgContentViewController useTransparentNavibar][-] 13:04:35.078 ENTER -[BaseMsgContentViewController shouldInteractivePop][-] 13:04:35.078 ENTER -[BaseMsgContentViewController toolView][-] 13:04:35.132 ENTER -[BaseMsgContentViewController SendEmoticonMesssageToolView:][-] 13:04:35.148 ENTER -[BaseMsgContentViewController findNodeDataByLocalId:][-] 13:04:35.148 ENTER -[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:][-] 13:04:35.148 ENTER -[BaseMsgContentViewController findNodeDataByLocalId:][-] 13:04:35.148 ENTER -[BaseMsgContentViewController getCurContentSizeHeight]...

frida

其中SendEmoticonMesssageToolView是个值得关注的函数,可以直接用 frida 打印函数堆栈:

import { kObjC } from "../../agent/objc";import { kNative } from "../../agent/native";// kObjC.traceClass("BaseMsgContentViewController");kObjC.traceMethod("BaseMsgContentViewController", "- SendEmoticonMesssageToolView:", {  onEnter: function(args) {    log.i("SendEmoticonMesssageToolView", args[0], args[1]);    kNative.printStackTrace(this.context);  }});

输出结果如下:

[-] 13:10:32.079 Hooked: -[BaseMsgContentViewController SendEmoticonMesssageToolView:][+] 13:10:37.212 SendEmoticonMesssageToolView 0x11f8a1800 0x10bf6e8ba[-] 13:10:37.636 backtrace:0x104e06de4 WeChat!0x2676de40x10294cde8 WeChat!0x1bcde80x1041bb930 WeChat!0x1a2b9300x1b8331e34 UIKitCore!-[UICollectionView _selectItemAtIndexPath:animated:scrollPosition:notifyDelegate:deselectPrevious:]0x1b83464b0 UIKitCore!-[UICollectionView _cellForItemAtIndexPath:includePrefetchedCells:]0x1b8359e10 UIKitCore!-[UICollectionView touchesEnded:withEvent:]0x1b46ef730 libsystem_malloc.dylib!nanov2_calloc0x1b46f4100 libsystem_malloc.dylib!calloc0x1b48ddae4 CoreFoundation!-[__NSSetM addObject:]0x1b473926c libobjc.A.dylib!objc_autoreleasePoolPop0x1b4a14320 CoreFoundation!_CFAutoreleasePoolPop0x1b48dd594 CoreFoundation!-[__NSSetM enumerateObjectsWithOptions:usingBlock:]0x1b498bc7c CoreFoundation!__NSSetM_new0x1bb64e208 QuartzCore!CA::Layer::retain_parent(CA::Transaction*) const0x1b8b5d9e8 UIKitCore!forwardTouchMethod0x1b8b5dae4 UIKitCore!-[UIResponder touchesEnded:withEvent:]

在 WeChat 部分的回溯没有符号,因为已经去掉了。

lldb

在 lldb 中也可以看到类似的结果:

(lldbinit) bt* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1  * frame #0: 0x0000000105334f04 WeChat`___lldb_unnamed_symbol201841$$WeChat    frame #1: 0x0000000104b32f80 WeChat`___lldb_unnamed_symbol170264$$WeChat + 160    frame #2: 0x0000000104e06de4 WeChat`___lldb_unnamed_symbol182196$$WeChat + 2600    frame #3: 0x000000010294cde8 WeChat`___lldb_unnamed_symbol8796$$WeChat + 248    frame #4: 0x00000001041bb930 WeChat`___lldb_unnamed_symbol129751$$WeChat + 624    frame #5: 0x00000001b8331e34 UIKitCore`-[UICollectionView _selectItemAtIndexPath:animated:scrollPosition:notifyDelegate:deselectPrevious:] + 952    frame #6: 0x00000001b8359e10 UIKitCore`-[UICollectionView touchesEnded:withEvent:] + 572    frame #7: 0x00000001b8b5d9e8 UIKitCore`forwardTouchMethod + 332    frame #8: 0x00000001b8b5dae4 UIKitCore`-[UIResponder touchesEnded:withEvent:] + 64    frame #9: 0x00000001b8b5d9e8 UIKitCore`forwardTouchMethod + 332    frame #10: 0x00000001b8b5dae4 UIKitCore`-[UIResponder touchesEnded:withEvent:] + 64    frame #11: 0x00000001b8b5d9e8 UIKitCore`forwardTouchMethod + 332    frame #12: 0x00000001b8b5dae4 UIKitCore`-[UIResponder touchesEnded:withEvent:] + 64    frame #13: 0x00000001b86e6478 UIKitCore`_UIGestureEnvironmentUpdate + 6992    frame #14: 0x00000001b86e48dc UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 380    frame #15: 0x00000001b86e4698 UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 248    frame #16: 0x00000001b8b6d654 UIKitCore`-[UIWindow sendEvent:] + 3512    frame #17: 0x00000001b8b48840 UIKitCore`-[UIApplication sendEvent:] + 348    frame #18: 0x0000000103de4920 WeChat`___lldb_unnamed_symbol111457$$WeChat + 404    frame #19: 0x00000001ebaaa87c UIKit`-[UIApplicationAccessibility sendEvent:] + 100

有符号更好,没有也无所谓,我们还是可以通过地址在逆向工具中查看代码。不过考虑到 ASLR 的存在,需要确认一下镜像的加载地址:

(lldbinit) vmmap /WeChat000102790000 - 00010d6d4000  af44000 R-X /private/var/containers/Bundle/Application/2A5D623F-5F8E-4A99-96C8-8CBD00D8B6BE/WeChat.app/WeChat        0 __TEXT00010d6d4000 - 00010fc40000  256c000 RW- /private/var/containers/Bundle/Application/2A5D623F-5F8E-4A99-96C8-8CBD00D8B6BE/WeChat.app/WeChat  af44000 __DATA00010fc40000 - 000110ac4000   e84000 R-- /private/var/containers/Bundle/Application/2A5D623F-5F8E-4A99-96C8-8CBD00D8B6BE/WeChat.app/WeChat  cb68000 __LINKEDIT

vmmap 是我自己定义的一个 lldb 命令,参考了 gef 和 pwndbg 在 gdb 中的实现

这里加载地址是 0x000102790000,而在 IDA 中 text 段的加载地址是 0x100004000,可以在后者进行 rebase,但是因为函数不多,所以先手动查找。另外用 lldb 也可以直接查看对应函数地址的偏移:

(lldbinit) image lookup -a 0x0000000105334f04      Address: WeChat[0x0000000102ba4f04] (WeChat.__TEXT.__text + 45747972)      Summary: WeChat`___lldb_unnamed_symbol201841$$WeChat(lldbinit) image lookup -a 0x0000000104b32f80      Address: WeChat[0x00000001023a2f80] (WeChat.__TEXT.__text + 37351296)      Summary: WeChat`___lldb_unnamed_symbol170264$$WeChat + 160(lldbinit) image lookup -a 0x0000000104e06de4      Address: WeChat[0x0000000102676de4] (WeChat.__TEXT.__text + 40316388)      Summary: WeChat`___lldb_unnamed_symbol182196$$WeChat + 2600(lldbinit) image lookup -a 0x000000010294cde8      Address: WeChat[0x00000001001bcde8] (WeChat.__TEXT.__text + 1805800)      Summary: WeChat`___lldb_unnamed_symbol8796$$WeChat + 248(lldbinit) image lookup -a 0x00000001041bb930      Address: WeChat[0x0000000101a2b930] (WeChat.__TEXT.__text + 27425072)      Summary: WeChat`___lldb_unnamed_symbol129751$$WeChat + 624

符号恢复

根据 lldb 中每个函数地址在 TEXT 段中的偏移,可以在 IDA 中直接跳转到对应函数(返回值),整理对应的调用函数分别是:

-[BaseMsgContentViewController SendEmoticonMesssageToolView:] (45747972)-[MMInputToolView didSelectorSelfDefinedEmotcion:] (37351296)-[EmoticonBoardView onTapEmoticonWrap:atIndex:maxCountPerLine:fromSection:] (40316388)-[EmoticonBoardCrossCollectionController onEmoticonPageCellTapEmoticonWrap:atIndex:pid:maxCountPerLine:] (1805800)-[EmoticonBoardCrossCollectionEmoticonPageCell collectionView:didSelectItemAtIndexPath:] (27425072)

既然 IDA 可以识别出对应 OC 函数的符号,那么理论上这些符号也是可以还原的,如果经常需要进行动态分析,那么可以通过一些方法自动化恢复对应的符号,可以参考 iOS符号表恢复[16] 以及 restore-symbol[17] 等项目。

静态分析

通过动态分析找到了着手点,以及通过回溯调用栈找到了一些相关函数,其中并没有一目了然的设置骰子点数的函数 setDiceValue,所以要想改点数还需要对上面的函数进行进一步分析。到这里,逆向工程是不可避免了,一般而言我是能躺着绝不坐着,能坐着绝不站着,不到万不得已是不去逆向的。

花开两朵,各表一枝,在前面砸完壳的第一时间我就预感到需要逆向,所以早早地把 200MB 的 MachO 丢进了 IDA,在文章写到这里时已经过去了四个小时,期间看了两集动漫,但是 IDA 还是没有全部分析完。所以在等待的时间里,先来简单介绍些 ObjectiveC 的底层实现。

ObjectiveC 101

在学习 OC 的时候,总会看到说 OC 语言实现面向对象是通过发送消息,具体是怎么发送呢?因此在实际逆向之前,我们先来自己写一个程序来进行分析,以便了解 OC 的调用原理。

以下面简单的 helloworld 为例:

#import <Foundation/Foundation.h>@interface MyClass: NSObject - (void)insMethod:(const char *)a1 arg2:(int)a2; + (void)clsMethod:(const char *)a1 arg2:(int)a2;@end@implementation MyClass - (void)insMethod:(const char *)a1 arg2:(int)a2 {     NSLog(@"insMethod called, self=%p, a1=%s", self, a1); } + (void)clsMethod:(const char *)a1 arg2:(int)a2 {     NSLog(@"clsMethod called, self=%p, a1=%s", self, a1); }@endint main() {    MyClass *c = [[MyClass alloc] init];    [MyClass clsMethod:"hello" arg2:1];    [c insMethod:"world" arg2:2];    return 0;}

主要调用了两个方法,一个类方法和一个成员方法,编译后查看其汇编代码如下:

(lldbinit) disassemblemain @ test_hello:    0x100003e90: push   rbp    0x100003e91: mov    rbp, rsp    0x100003e94: sub    rsp, 0x10    0x100003e98: mov    dword ptr [rbp - 0x4], 0x0->  0x100003e9f: mov    rax, qword ptr [rip + 0x424a] ; (void *)0x0000000100008120: MyClass    0x100003ea6: mov    rdi, rax    0x100003ea9: call   0x100003f04               ; symbol stub for: objc_alloc_init    0x100003eae: mov    qword ptr [rbp - 0x10], rax    0x100003eb2: mov    rax, qword ptr [rip + 0x4237] ; (void *)0x0000000100008120: MyClass    0x100003eb9: mov    rsi, qword ptr [rip + 0x4220] ; "clsMethod:arg2:"    0x100003ec0: mov    rdi, rax    0x100003ec3: lea    rdx, [rip + 0xa8]         ; "hello"    0x100003eca: mov    ecx, 0x1    0x100003ecf: call   qword ptr [rip + 0x12b]   ; (void *)0x00007fff20439d00: objc_msgSend    0x100003ed5: mov    rax, qword ptr [rbp - 0x10]    0x100003ed9: mov    rsi, qword ptr [rip + 0x4208] ; "insMethod:arg2:"    0x100003ee0: mov    rdi, rax    0x100003ee3: lea    rdx, [rip + 0x8e]         ; "world"    0x100003eea: mov    ecx, 0x2    0x100003eef: call   qword ptr [rip + 0x10b]   ; (void *)0x00007fff20439d00: objc_msgSend    0x100003ef5: xor    eax, eax    0x100003ef7: add    rsp, 0x10    0x100003efb: pop    rbp    0x100003efc: ret

且不管初始化部分,后面两个函数调用最终都进入了 objc_msgSend[18] 函数,在苹果官网可以看到其函数原型:

id objc_msgSend(self, op, ...);

其中各个参数为:

1.self: 消息的接收方,对于类方法为指向类的指针,对于对象方法而言为对象指针;2.op: 为消息的标识符,也称为 selector,实际上是一个表示对应方法名称的字符串;3.... 其他方法参数;

回到上面的例子,第一次类方法调用的参数为:

(lldbinit) p/x $rdi(unsigned long) $29 = 0x0000000100008120(lldbinit) x/1s $rsi0x100003f86: "clsMethod:arg2:"(lldbinit) x/1s $rdx0x100003f72: "hello"(lldbinit) p/x $rcx(unsigned long) $32 = 0x0000000000000001

第二次成员方法的调用参数为:

(lldbinit) p/x $rdi(unsigned long) $54 = 0x0000000100208340(lldbinit) x/1s $rsi0x100003f96: "insMethod:arg2:"(lldbinit) x/1s $rdx0x100003f78: "world"(lldbinit) p/x $rcx(unsigned long) $57 = 0x0000000000000002

程序的输出如下:

2021-04-10 22:54:19.487148+0800 test_hello[99332:5120374] clsMethod called, self=0x100008120, a1=hello2021-04-10 22:54:54.565229+0800 test_hello[99332:5120374] insMethod called, self=0x100208340, a1=world

所以,ObjectiveC 在实现上还是挺接近 C 语言的,这对于我们逆向而言方便很多。

头铁逆向

了解了简单的 OC 逆向之后,IDA 也跑的差不多了。根据栈回溯对应 OC 函数的名称,逐级往上看。首先是 didSelectorSelfDefinedEmotcion这个函数,表示选中了某个自定义表情,里面只是一些发送操作,所以并不是我们想要的;

然后是 -[EmoticonBoardView onTapEmoticonWrap:atIndex:maxCountPerLine:fromSection:] ,该函数中主要是判断所选择的表情是否为自拍表情或者自定义表情,如果是的话就进行异步上传。其调用didSelectorSelfDefinedEmotcion 的参数为 a3,即一个对象指针,虽然还不知道是哪个对象,但知道其包含这些属性(方法):

m_emojiInfom_isAsyncUploadattachObject:forKey:

在 class-dump 导出的头文件中搜索,可以发现是CEmoticonWrap 类:

#import <objc/NSObject.h>#import "PBCoding-Protocol.h"@class EmojiInfoObj, NSData, NSString;@interface CEmoticonWrap : NSObject <PBCoding>{    _Bool m_bCanDelete;    _Bool m_isAsyncUpload;    _Bool m_isRemoteRecommed;    _Bool m_isLastSended;    unsigned int m_uiType;    unsigned int m_uiGameType;    unsigned int m_lastUsedTime;    unsigned int m_extFlag;    NSString *m_nsAppID;    NSString *m_nsThumbImgPath;    NSString *m_query;    EmojiInfoObj *m_emojiInfo;    NSData *_m_imageData;}

该函数主要是执行上传并记录一些信息,更新一些 emojiInfo 的字段,所以还是要继续往上走;

接着是-[EmoticonBoardView onTapEmoticonWrap:atIndex:maxCountPerLine:fromSection:],该函数主要是作为 delegate 进行转发,没有什么实际功能。但是从名字可以猜测自定义表情栏是通过不同的行实现,而每行中每个表情又对应一个 Cell。

按照回溯的堆栈都看完了,也没有发现和骰子相关的代码。不管怎样,骰子的点数总归是在设置骰子图片之前确定的,而根据上面的逆向可以知道,自定义表情的图片应该是定义在 [CEmoticonWrap m_emojiInfo]中,该类的属性如下:

@interface EmojiInfoObj : MMObject <PBCoding, NSCopying>{    _Bool _disableExtern;    NSString *md5;    NSString *url;    NSString *thumbUrl;    NSString *designerId;    NSString *encryptUrl;    NSString *aesKey;    NSString *productId;    NSString *externUrl;    NSString *attachedText;    NSString *externMd5;    NSString *activityId;    NSString *attachedTextColor;    NSString *lensId;    NSString *linkId;    NSString *_tpUrlString;    NSString *_authkey;}

接下来还是要通过结合动态分析来确定骰子图片加载的时机。

动静结合

还是回到SendEmoticonMesssageToolView函数,通过逆向可以得知其参数为CEmoticonWrap类型,修改前面打印栈 callback 的脚本,令其打印参数的各个字段,如下:

[+] 21:50:18.958 Called SendEmoticonMesssageToolView:[+] 21:50:18.958 emotion: <CEmoticonWrap: self.m_uiType=1, self.m_bCanDelete=1, self.m_uiGameType=2, self.m_nsAppID=(null), self.m_extFlag=0, self.m_nsThumbImgPath=(null), self.m_lastUsedTime=0, self.m_isAsyncUpload=0, self.m_isRemoteRecommed=0, self.m_isLastSended=0, self.m_query=(null), self.m_emojiInfo=<EmojiInfoObj: 0x2813c77b0> {    activityId = <nil>;    aesKey = <nil>;    attachedText = <nil>;    attachedTextColor = <nil>;    authkey = <nil>;    designerId = <nil>;    disableExtern = 0;    encryptUrl = <nil>;    externMd5 = <nil>;    externUrl = <nil>;    lensId = <nil>;    linkId = <nil>;    md5 = "dice_emoticon_md5";    productId = "custom_emoticon_pid";    thumbUrl = <nil>;    tpUrlString = <nil>;    url = <nil>}>

也就是说进入函数之时这些参数都是空的,m_emojiInfo 的 md5 为 “dice_emoticon_md5”,此时骰子图像并没有真正生成,所以印证了我们之前的猜想,即实际的操作还在后面。那么栈回溯是不是白看了?也不尽然,至少至少了参数格式和调用流程(逆向嘛,总是要学会安慰自己)。

回过头来继续看这个函数:

void __cdecl -[BaseMsgContentViewController SendEmoticonMesssageToolView:](BaseMsgContentViewController *self, SEL a2, id emotion){  id v4; // x19  id v5; // x20  id *v6; // x21  id v7; // x24  unsigned __int8 v8; // w23  id *v9; // x21  id v10; // x0  id *v11; // x21  void *v12; // x20  id v13; // x24  unsigned int v14; // w22  id *v15; // x21  v4 = objc_retain(emotion);  v5 = objc_loadWeakRetained((id *)&self->m_delegate);  if ( ((unsigned int)objc_msgSend(v5, "respondsToSelector:", "CanSendEmoticonMessage") & 1) == 0 )    goto LABEL_6;  v7 = objc_loadWeakRetained(v6);  v8 = (unsigned __int8)objc_msgSend(v7, "CanSendEmoticonMessage");  objc_release(v7);  objc_release(v5);  if ( (v8 & 1) != 0 )  {    v10 = objc_loadWeakRetained(v9);    if ( v10 )    {      v12 = v10;      v13 = objc_loadWeakRetained(v11);      v14 = (unsigned int)objc_msgSend(v13, "respondsToSelector:", "SendEmoticonMessage:");      objc_release(v13);      objc_release(v12);      if ( v14 )      {        v5 = objc_loadWeakRetained(v15);        objc_msgSend(v5, "SendEmoticonMessage:", v4);LABEL_6:        objc_release(v5);        goto LABEL_7;      }    }  }LABEL_7:  objc_release(v4);}

又是一个 delegate 模式,直接打印出实例:

kObjC.traceMethod("BaseMsgContentViewController", "- SendEmoticonMesssageToolView:", {  onEnter(args) {    log.i("Called", args[1].readCString());    const vc = new ObjC.Object(args[0]);    const emotion = new ObjC.Object(args[2]);    log.i("vc:", vc);    log.i("m_delegate:", vc.valueForKey_("m_delegate"));  }});

结果如下:

[+] 19:15:24.203 Called SendEmoticonMesssageToolView:[+] 19:15:24.204 vc: <BaseMsgContentViewController: 0x1200b8e00>[+] 19:15:24.206 m_delegate: <WeixinContentLogicController: 0x11f340a90>

是个 WeixinContentLogicController,那就继续找它的SendEmoticonMessage:方法,没有的话就在父类或者 Category 里找。因为程序太大,我的破电脑在 IDA 里搜索比较卡,所以有个小技巧是直接通过函数名跳转,对应的 IDA 函数为名为 -[WeixinContentLogicController SendEmoticonMessage:],当然实际并没有这个函数,而是在父类中定义的 -[BaseMsgContentLogicController SendEmoticonMessage:]

ida.png

在上面高亮的一行,IDA 反编译的结果有点问题,显示 V7 是未初始化的,遇到这种情况直接看汇编即可,汇编是不会骗人的:

asm

即第一个参数实际上是 PluginUtil 类的地址,这里调用的则是类方法+[PluginUtil isPluginUserName:]。其中 m_uiGameType 前面已经通过 frida 打印出来了,其值是 2,继续动态分析得知进入 else 分支。按照这种动静结合的策略,最终分析得到向前的调用过程:

-[BaseMsgContentLogicController SendEmoticonMessage:]+[GameController GameEmoticonMsgForEmoticonWrap:]+[GameController SetGameContentForMsgWrap:] (参数类型是 CMessageWrap)

最终设置内容如下:

random

看到这逻辑应该很明显了,如果游戏类型为 1,则有:

content = 1 + rand - rand / 3 * 3 = 1 + rand % 3

如果游戏类型为 2,则有:

content = 4 + rand - rand / 6 * 6 = 4 + rand % 6

由于我们选的是扔骰子,游戏类型为 2,骰子的 content 取值范围为 4 ~ 9,正好对应 6 个点数;有理由猜测前面一个 3 个点数的应该就对应剪刀石头布游戏。

这里有两个值得注意的点,一是 random 函数调用了两次,但是后面一次的结果并没有使用,因此目的可能通过减少随机数连续性增加破解伪随机序列的难度;另外一个点是随机后的内容经过了 md5,所以修改点数的时候可能要一并修改哈希,否则会导致校验失败,或者直接修改 random 函数的返回值也是可以的。

kNative.trace(null, "random", {  onLeave(retval) {    log.i("random:", retval.toInt32());    retval.replace(ptr(5));  }});

实现效果如下:

dice

至此,修改骰子点数的核心功能就分析完毕了。

再谈 Hook

说实话最近听 hook 这个词已经听到腻了,不管是 plt hook 还是 inline hook,其本质都是在运行时修改现有的代码,进而达到修改控制流的目的。对于二进制汇编代码的修改大同小异,不过对于 iOS 程序而言其实也有其他选择,下面逐一介绍:

Inline Hook

这是最为通用的 hook 方式,即在指定地址前增加跳转指令,保存现场并跳转到用户指令,执行完后还原现场并返回执行。inline hook 可以实现汇编指令级别的 trace,也是大部分动态跟踪工具实现的基础。下图为 frida-gum 实现 inline hook 的的大致流程:

frida

当然原理归原理,要基于公式实现核弹在工程上还要做很多工作,比如代码段通常是不可修改的,因此需要 mmap 拷贝一份到自己的地盘去进行修改;另外保存和恢复现场的操作大多只能用汇编写,因此要实现跨平台还要针对不同指令集进行测试等等。这一切繁重的工作别人都已经为我们做好了,我们需要做的就只是调用一下 Interceptor.attach ,不得不感慨生活真幸福。

Method Swizzling

前面我们说过,Objective-C 实现面向对象是基于消息的发送,那么针对 Objc 的调用,我们其实可以动态的修改某个 selector 对应的实现,这个过程就称之为 method swizzling。苹果官方也提供了对应的 API:

void method_exchangeImplementations(Method m1, Method m2);

该方法相当于下面指令的原子实现:

IMP imp1 = method_getImplementation(m1);IMP imp2 = method_getImplementation(m2);method_setImplementation(m1, imp2);method_setImplementation(m2, imp1);

还是那句老话,原理是简单的,实现是复杂的。现实中很少这样直接交换两个方法实现,而是在之前进行一些必要的检查,如下所示:

+ (void)load{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        Class aClass = [self class];                SEL originalSelector = @selector(method_original:);        SEL swizzledSelector = @selector(method_swizzle:);                Method originalMethod = class_getInstanceMethod(aClass, originalSelector);        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);        BOOL didAddMethod =        class_addMethod(aClass,                        originalSelector,                        method_getImplementation(swizzledMethod),                        method_getTypeEncoding(swizzledMethod));                if (didAddMethod) {            class_replaceMethod(aClass,                                               swizzledSelector,                                               method_getImplementation(originalMethod),                                               method_getTypeEncoding(originalMethod));        } else {            method_exchangeImplementations(originalMethod, swizzledMethod);        }    });}

直接交换方法是危险的,因为如果类中没有实现该方法,那么 class_getInstanceMethod 会返回某个父类中的 Method 对象,实际交换的则是父类的方法,这样其他父类和子类的调用就会出现意想不到的问题。因此上面的代码会判断 selector 方法不存在的情况。

另外还要注意的是 selector 命名冲突问题、重复 swizzling 问题以及 _cmd 被修改的问题等等,因此也就有了 RSSwizzle[19] 或者 jrswizzle[20] 这些完善的实现方案。对于开发人员来说这是个需要仔细考虑的问题,毕竟这影响了程序的稳定性。

值得一提的是,frida 中对于 Objc 也提供了基于 method swizzling 的 hook 方案:

const myMethod_ = ObjC.classes.MyClass.myMethod_;myMethod_.implementation = ObjC.implement(myMethod_, function (handle, selector, arg0) {  log.i(`self=${handle}, selector=${selector}, arg0=${arg0}`);});

NSProxy

在 Java 中,我们可以通过 java.lang.reflect.Proxy[21] 基于反射机制来实现方法的动态代理,这种方式在很多地方被应用于 AOP 即切面编程。而 Objective-C 也有类似的方式,即 NSProxy,该类的定义如下:

/*    NSProxy.h    Copyright (c) 1994-2019, Apple Inc. All rights reserved.*/#import <Foundation/NSObject.h>@class NSMethodSignature, NSInvocation;NS_ASSUME_NONNULL_BEGINNS_ROOT_CLASS@interface NSProxy <NSObject> {    __ptrauth_objc_isa_pointer Class    isa;}+ (id)alloc;+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;+ (Class)class;- (void)forwardInvocation:(NSInvocation *)invocation;- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");- (void)dealloc;- (void)finalize;@property (readonly, copy) NSString *description;@property (readonly, copy) NSString *debugDescription;+ (BOOL)respondsToSelector:(SEL)aSelector;- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);// - (id)forwardingTargetForSelector:(SEL)aSelector;@endNS_ASSUME_NONNULL_END

可以看到其遵守 NSObject protocol,并且第一个 ivar 是 isa 指针,因此可以当成是一个 NSObject 或者其派生类使用,但是它并不是一个 NSObject 的子类。使用上只需要实现两个方法:

- (void)forwardInvocation:(NSInvocation *)invocation;- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

NSInvocation 封装了一个方法调用的全部信息,包括调用参数和返回值。一个示例实现如下:

#import <Foundation/Foundation.h>@interface MyProxy : NSProxy {    id victim_;}+ (id)hookObject:(id)obj;@end@implementation MyProxy+ (id)hookObject:(id)obj {    MyProxy *p = [MyProxy alloc];    p->victim_ = obj;    return p;}- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {    return [victim_ methodSignatureForSelector:sel];}- (void)forwardInvocation:(NSInvocation *)invocation {    if ([victim_ respondsToSelector:invocation.selector]) {        NSString *selectorName = NSStringFromSelector(invocation.selector);        NSLog(@"ENTER %@", selectorName);        [invocation invokeWithTarget:victim_];        NSLog(@"LEAVE %@", selectorName);    }}@end  int main(int argc, char *argv[]) {    dispatch_semaphore_t sem = dispatch_semaphore_create(0);        NSURL *url = [MyProxy hookObject:[NSURL URLWithString:@"https://evilpan.com"]];    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {        dispatch_semaphore_signal(sem);    }];    [task resume];    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);    return 0;}

输出:

$ ./test_proxy2021-04-11 11:18:55.193 test_proxy[15011:5637371] ENTER absoluteURL2021-04-11 11:18:55.194 test_proxy[15011:5637371] LEAVE absoluteURL

直接用我们的 MyProxy 对象替换了原始的 NSURL 对象,这样 NSURL 对象中所调用的所有方法都由我们的 Proxy 对象来进行代理,因此可以在执行原始函数之前以及返回之前执行我们自己的操作,从而实现 hook 的功能。

其他

补充一些前面没有提到的东西。

瘦身

Mach-O程序是支持多架构的,例如同一个程序既能运行在arm32位也能运行在64位机器上。多架构(multiarch)的应用通常比原程序体积要大,但是比两个单架构的程序要小,因为多架构应用会共享资源。

iOS应用开发者为了保证兼容性通常会选择同时支持armv7和arm64,但对于逆向分析却不必要。因此,我们可以先对其进行瘦身(thin),只保留arm64架构即可。首先查看应用是否为多架构:

# 通过file命令查看$ file pp2048/Payload/2048.app/2048pp2048/Payload/2048.app/2048: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64]pp2048/Payload/2048.app/2048 (for architecture armv7):    Mach-O executable arm_v7pp2048/Payload/2048.app/2048 (for architecture arm64):    Mach-O 64-bit executable arm64# 或者通过lipo命令查看$ lipo -detailed_info pp2048/Payload/2048.app/2048Fat header in: pp2048/Payload/2048.app/2048fat_magic 0xcafebabenfat_arch 2architecture armv7    cputype CPU_TYPE_ARM    cpusubtype CPU_SUBTYPE_ARM_V7    offset 16384    size 8838944    align 2^14 (16384)architecture arm64    cputype CPU_TYPE_ARM64    cpusubtype CPU_SUBTYPE_ARM64_ALL    offset 8863744    size 10265808    align 2^14 (16384)

只保留arm64,可以使用lipo或者ditto,二者都是苹果自带的:

# 使用lipo工具$ lipo -thin arm64 pp2048/Payload/2048.app/2048 -output thin_2048# 或者使用ditto工具$ ditto --arch arm64 pp2048/Payload/2048.app/2048 thin_2048

输出的文件是一样的。

Cycript

前文中有用到 cycript 来注入动态库进行 UI 界面分析,这里再补充一下。cycript[22] 由 saurik 大神开发,在 JavaScript REPL 中支持 ObjectiveC 的混合语法,使用起来比较直观。比如:

cy# [[UIApplication sharedApplication] keyWindow]#"<iConsoleWindow: 0x11581f120; baseClass = UIWindow; frame = (0 0; 1194 834); gestureRecognizers = <NSArray: 0x2800d0570>; layer = <UIWindowLayer: 0x280eb1800>>"

支持直接调用 c 函数,并且直接使用 enum 常量:

cy# dlopen(extern "C" void *dlopen(char const*, int))cy# RTLD_NOW2cy# dlopen("/usr/lib/libFLEX.dylib", RTLD_NOW);(typedef void*)(0x11a23e180)

前面使用 FLEX 进行 UI 分析就是直接通过 dlopen 注入动态库实现的(其实注入 Reveal 也是)。cycript 的功能很强大,但是文档不多,主要是参考官方的 manual。一些参考网站如下:

http://www.cycript.org/manual/https://iphonedevwiki.net/index.php/Cycript_Trickshttps://iosre.com/t/cycript/7554

Tweak

在越狱 iOS 中有类似于 Andoid Xposed 的 patch 框架 Cydia Substrate[23],其提供了 C API[24] 给越狱 APP 的开发者来实现对特定方法的动态修改,还提供了基于 plist 的声明文件,让开发者可以选择指定的 APP 进行 hook,同时也很好地处理了同一个方法被 Hook 多次的问题。基于 Substrate 开发的 dylib 称为 Tweak,由 Substrate 根据 plist 在应用启动时选择进行注入。

一个 hook 示例如下:

NSString *(*oldDescription)(id self, SEL _cmd);// implicit self and _cmd are explicit with IMP ABINSString *newDescription(id self, SEL _cmd) {    NSString *description = (*oldDescription)(self, _cmd);    description = [description stringByAppendingString:@"!"];    return description;}MSHookMessageEx(    [NSObject class], @selector(description),    &newDescription, &oldDescription);

如果觉得这样写还是太麻烦,可以使用 Logos[25] 来简化代码,这是个基于预处理库的组件,包含一系列以%开头的宏方便编写 hook 代码,示例如下:

%hook SBApplicationController-(void)uninstallApplication:(SBApplication *)application {    NSLog(@"Hey, we're hooking uninstallApplication:!");    %orig; // Call the original implementation of this method    return;}%end

Logos 现在是 Theos[26] 组件的一部分,可以通过 NIC 工程模板系统快速地创建一个基于 Makefile 的 Tweak 项目。初学者可以通过参考一些 开源的 Tweaks[27] 来熟悉开发流程。

LLDB

LLDB是个功能十分强大的调试器,在有些场景下比GDB还要好用。关于GDB命令可以参考GDB调试笔记[28],而GDB命令对应的LLDB命令可以参考GDB and LLDB Command Examples[29],这里只介绍一些常用的例子:

在一个ObjectiveC类中的所有方法下设置断点(正则表达式):

(lldb) breakpoint set -r '\[ClassName .*\]$'

打印 Objective-C 对象:

(lldb) po addr

查看各个模块的映射地址(target module list):

(lldb) image list -h -f

设置zero flag,常用于修改跳转指令:

(lldb) register write rflags `$rflags|0x40`

更详细的功能以及 lldb 脚本的编写可以参考下面的链接:

https://lldb.llvm.org/use/tutorial.htmlhttps://lldb.llvm.org/use/python-reference.htmlhttps://github.com/llvm/llvm-project/tree/main/lldb/examples/python

后记

本文以微信扔骰子小游戏为例,介绍了一次完整的 iOS 逆向分析过程。以最初的越狱开始,分别介绍了砸壳、UI 分析、动态分析和静态分析的具体操作,最后还介绍了 iOS 中常见的 hook 方案以及一些越狱开发相关的工具。之前分析过 Android 中微信的扔骰子,里面大部分代码都经过了混淆,相比之下 iOS 中逆向代码就直观很多。现今很多应用都是同时支持两个平台,在一个平台中受阻可以切换到另一个平台说不定有意想不到的收获,比如对于 Unity3D 中 C# 代码的逆向,Android 平台就比 iOS 要方便点 (因为 IL2CPP)。因此,对于有志深造的逆向工程师而言,保持灵活和开放的心态也是至关重要的。

参考资料

RE-iOS-Apps[30]https://iphonedevwiki.net/index.php

引用链接

[1] 《免越狱调试与分析黑盒iOS应用》: https://evilpan.com/2019/04/07/ios-reverse-basics/
[2] An adventure into the XNU kernel: https://www.exploit-db.com/docs/english/43945-jailbreaking-ios-11.1.2-an-adventure-into-the-xnu-kernel.pdf
[3] Project Zero - SockPuppet: A Walkthrough of a Kernel Exploit for iOS 12.4: https://googleprojectzero.blogspot.com/2019/12/sockpuppet-walkthrough-of-kernel.html
[4] Project Zero - A survey of recent iOS kernel exploit: https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html
[5] Secfault Security - Writing an iOS Kernel Exploit from Scratch: https://secfault-security.com/blog/chain3.html
[6] oob_timestamp: https://bugs.chromium.org/p/project-zero/issues/detail?id=1986
[7] 现有的越狱工具: https://pangu8.com/jailbreak/
[8] stefanesser/dumpdecrypted: https://github.com/stefanesser/dumpdecrypted
[9] conradev/dumpdecrypted: https://github.com/conradev/dumpdecrypted
[10] Clutch: https://github.com/KJCracks/Clutch
[11] frida-ios-dump: https://github.com/AloneMonkey/frida-ios-dump
[12] bagbak: https://github.com/ChiChou/bagbak
[13] mremap_encrypted: https://www.linkedin.com/pulse/decrypting-apps-ios-john-coates/
[14] mach-o: https://evilpan.com/2020/09/06/macho-inside-out/
[15] FLEX: https://github.com/FLEXTool/FLEX
[16] iOS符号表恢复: http://blog.imjun.net/posts/restore-symbol-of-iOS-app/
[17] restore-symbol: https://github.com/tobefuturer/restore-symbol
[18] objc_msgSend: https://developer.apple.com/documentation/objectivec/1456712-objc_msgsend
[19] RSSwizzle: https://github.com/rabovik/RSSwizzle
[20] jrswizzle: https://github.com/rentzsch/jrswizzle
[21] java.lang.reflect.Proxy: https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html
[22] cycript: http://www.cycript.org/
[23] Cydia Substrate: https://iphonedevwiki.net/index.php/Cydia_Substrate
[24] C API: http://www.cydiasubstrate.com/api/c/MSHookMessageEx/
[25] Logos: http://iphonedevwiki.net/index.php/Logos
[26] Theos: https://github.com/theos/theos
[27] 开源的 Tweaks: https://github.com/LacertosusRepo/Open-Source-Tweaks
[28] GDB调试笔记: https://evilpan.com/2015/10/28/GDB-Tips/
[29] GDB and LLDB Command Examples: https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-command-examples.html
[30] RE-iOS-Apps: https://github.com/ivRodriguezCA/RE-iOS-Apps


浏览 87
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报