本帖最后由 zwo 于 2019-12-5 22:41 编辑
0x0 前言
在iOS以及Mac上,Safari提供了功能强大的基于webkit的调试器功能,方便开发者调试APP的内置浏览器。在某聊天软件盛行的今天,很多网页都必须在该软件的内置浏览器里才能运行,作为开发者的我们,如果想一窥这类网页的实现细节该怎么办呢?在浩瀚的网络里搜了一下,安卓可以通过Chrome来调试,iOS系统越狱后,通过FrIDA也能做到,唯独Mac系统甚少提及。这篇文章里,我尝试通过逆向的手段,达到对任意APP的内置浏览器实现调试。
0x1 原理分析
从现有的资料,可以查到,Safari判断是否可以进行调试依赖于守护进程 webinspectord ,而实际的逻辑判断实现在动态库WebInspector中,其路径为/System/Library/PrivateFrameworks/WebInspector.framework/Versions/A/WebInspector
而其中的关键函数为签名如下的ObjC方法:
[Objective-C] 纯文本查看 复制代码 -[RWIRelayDelegateMac _allowApplication:bundleIdentifier:];
放进IDA分析这个方法的实现过程,整个流程,可以注释如下:
分析可得,要使这个方法返回真,有以下几个条件- 为内部程序,即isInternal为真,且debugEverything为真
- 是代{过}{滤}理应用(包名包含com.apple.WebKit.WebContent),即isProxyApplication函数返回1.
- 应用签名包含com.apple.security.get-task-allow权限
- 应用签名包含com.apple.webinspector.allow权限
以上条件实现任一即可。对于普通开发者来说,最可操作的是第3点,只要用Mac开发证书重签名应用即可达成。但是重签也是有局限性的,会造成某些重要entitlement丢失从而导致运行不正常。根据以上的分析,我们来尝试一下如何用技术的手段实现。
在开始之前,需要做的第一件事情是关闭系统的SIP以及taskgated。因为我们需要修改系统的实现,就需要获得目标进程的task,获得了task,也就得到了该进程的控制权,系统的SIP正是保护这一机制不被滥用,拦截不信任的应用调用task_for_pid()以获得task。另一个安全机制taskgated对调用敏感内核函数的程序进行签名校验,也要关闭。关于关闭的方法网上有大量资料,不再累述。
0x2 尝试
由于我们要修改的为一个ObjC方法,首先想到的就是最常用最方便的method swizzling技术,我们参考iOS的版本,直接用以下的Frida脚本验证一下:
[JavaScript] 纯文本查看 复制代码 Interceptor.attach(ObjC.classes.RWIRelayDelegateMac['- _allowApplication:bundleIdentifier:'].implementation, {
onEnter: function(args) {
this.bundleId = new ObjC.Object(args[3]);
},
onLeave: function(retVal) {
const allow = !retVal.equals(NULL)
console.log(this.bundleId + (allow ? ' allows' : ' does not allow') + ' WebInspect')
if (!allow) {
console.log('now patch it');
retVal.replace(ptr(1));
}
}
});
打开Safari后,在命令行输入frida webinspectord进入frida界面提示,输入以上的JavaScript脚本。然后在某聊天软件打开任意文章以拉起webview,这时可以看到frida给出进程终止的提示。
frida不起作用,而我对frida的注入机制也不了解,只能靠自己来实现对webinspectord的注入了。对于在Mac上可以实现进程注入的技术手段,据我所知有以下三种:其中线程劫持这种远程注入的难度太大,因为要从进程中取出某个线程,保存当前线程的状态,运行劫持代码,再恢复原来的状态,非常容易造成线程出错。我在尝试了另外两种方法后,都以webinspectord的crash而失败告终。打开系统的日志,可以看到如下的输出:
通过命令行输入$security error -67050可以查看这个出错的详细说明,英文就不写出来了,用人话来说,就是“不符合某些要求”,但是没指名是什么要求。这个错误是amfid进程发出的,查阅相关资料,有点头绪了,我们来分析一下。
我是首先生成一个动态库dylib,内容很简单,在初始化函数中利用method swizzling令-[RWIRelayDelegateMac _allowApplication:bundleIdentifier:]始终返回真。而注入程序首先是通过task_for_pid()获取webinspectord的task,并在webinspectord的进程空间里通过mach_vm_allocate函数分配代码空间和栈空间。然后将dlopen()加载动态库所在路径的相关汇编代码通过mach_vm_write()函数写入到刚才开辟的代码空间中。最后通过结构体x86_thread_state64_t设置线程的栈指针RSP和栈基指针RBP指向栈空间底部,通过thread_create_running创建并运行线程。一切看起来如此正常,然而即使dylib和注入程序都用开发者证书签名,依然过不了amfid的拦截。
amfid (Apple Mobile File Integrity daemon) 苹果可移动文件完整性守护进程,对系统的保护简单而高效。在Mac系统里,当一个程序被签名的时候,签名工具codesign会对该程序的二进制文件的每个页(page)计算一个哈希值,以及所有哈希值一起计算出一个总的哈希值。对于Mac系统以及Linux系统,内核通过page fault的机制来分配内存, 当我们访问一个内存地址时,如果该地址非法,或者我们对其没有访问权限,或者该地址对应的物理内存还未分配,内核都会生成一个page fault,进而执行操作系统的page fault handler。 在Mac上,还有一种情况会产生page fault,当某个文件(例如dylib)被内核以mmap函数映射进进程的虚拟内存中。这个时候amfid会校验新进来的内存页是否有签名,签名是否与当前执行的二进制程序一致,如果没有签名或者签名的哈希值不一致,那么加载过程将中断,对于某些苹果签名的二进制,系统为了安全起见会直接kill掉该二进制,webinspectord就是个例子。故事到了这里,如果要继续发展下去,就需要patch amfid了,了解过iOS越狱的朋友会知道,越狱其中关键的一步就是patch amfid,可见这条路一定是荆棘丛丛,先暂不考虑。我想到的另一个办法是内存补丁,既然系统不允许我们加载我们的代码,那就直接改内部实现好了,也就是我接下来要说的内存补丁。
0x3 内存补丁
回顾图1的流程图,已知其中一个可以实现调试的条件是函数isProxyApplication返回1,观察了一下判断过程,惊喜的发现,非常容易扭转状态,见下图:
首先是给bl寄存器赋值1,然后判断存放isProxyApplication返回值的al寄存器是否不为零,如果不为零则跳转到结束并把bl的值1返回。很容易想到把test al, al改为test bl, bl即可。用机器码来表示就是84 C0改为84 DB。
现在面临的最大问题也是整个实现过程最大的挑战,已知这段代码在虚拟内存中的位置是0x6DAAF,如何确定在进程的虚拟内存中的位置?这段代码是以系统共享动态库的形式加载进webinspectord的内存空间。我首先想到的方法是通过mach_vm_region_recurse函数枚举出所有进程中活跃的内存块(region)并搜索mach-o的文件头以找到动态库的开始位置。然而实际的输出内存区间却没有我想象中那样可以对齐到动态库的起始头位置,对比一下vmmap的输出如下:
尝试了若干方法后,始终没办法获得准确的偏移量,百般无奈下,我只好采取最没技术含量的方法,直接调用命令行vmmap获取偏移量。在这里我也问一下论坛里的大牛,如何通过代码计算某个动态库加载后的偏移量?
获得vmmap的输出后,字符串查找代码所在的动态库的名称WebInspect从而定位到所在行,系统从dyld cache中映射共享动态库到进程的虚拟内存空间中,起始位置的16位数字一定是以00007ff开头(根据shared_region.h中的定义,起始偏移 SHARED_REGION_BASE_X86_64 = 0x00007FFF7000000),通过正则表达匹配出开始和结束位置,从而获得偏移值。以上图为例子,需要打补丁的位置就在0x7fff90432000 + 0x6daaf。
先打开Safari,然后运行程序,在聊天软件中打开一篇公众号文章,可以看到该文章的地址已经出现在Safari的开发调试列表中。
点击文章的URL即可进入调试界面,开始愉快的调试了。
代码在Mojave、High Sierra两个版本的系统上测试过。由于是内存补丁,重启Safari后就会失效。
完整代码在这里 https://github.com/zwo/patch_webinspect |