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


export PATH=/home/super7dd/app/adt-bundle-linux-x86_64-20131030/android-ndk-r9b/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86_64/bin:$PATH
. ./build/envsetup.sh
lunch aosp_hammerhead-userdebug



make -C kernel O=../out/target/product/hammerhead/obj/KERNEL_OBJ ARCH=arm CROSS_COMPILE=arm-eabi- hammerhead_defconfig
make -C /media/super7dd/bonus/src/android_n5/kernel O=../out/target/product/hammerhead/obj/KERNEL_OBJ ARCH=arm CROSS_COMPILE=arm-eabi- hammerhead_defconfig
make -C kernel O=../out/target/product/hammerhead/obj/KERNEL_OBJ ARCH=arm CROSS_COMPILE=arm-eabi- zImage-dtb


/media/super7dd/bonus/src/android_n5/out/host/linux-x86/bin/mkbootfs /media/super7dd/bonus/src/android_n5/out/target/product/hammerhead/root  > root.fs


/media/super7dd/bonus/src/android_n5/out/host/linux-x86/bin/mkbootimg \
    --kernel ./out/target/product/hammerhead/obj/KERNEL_OBJ/arch/arm/boot/zImage-dtb \
    --ramdisk ./root.fs \
    --cmdline "console=ttyHSL0,115200,n8 androidboot.hardware=hammerhead user_debug=31 maxcpus=2 msm_watchdog_v2.enable=1" \
    --base 0x0 \
    --ramdisk_offset 0x02900000 \
    --kernel_offset 0x8000 \
    --tags_offset 0x2700000 \
    -o ./boot.img
abootimg -i ./boot.img

adb push boot.img /data/local/tmp

adb shell "dd if=/data/local/tmp/boot.img of=/dev/block/platform/msm_sdcc.1/by-name/boot"

adb reboot

这篇文章获取Context方法是受限的,如果你是注入或者被加载的jar包就不能使用
http://blog.csdn.net/hyx1990/article/details/7584789

这时候需要更本质上的方法,Client端大多数结构都是放在ActivityThread中的,反射去取

    public static Context getGlobalApplicationContext()
{
    // ActivityThread at = ActivityThread.currentActivityThread();
    //Class clazz  = ReflectionHelper.getClass("android.app.ActivityThread");


    Class[] type = null;
    Object[] args = null;

    Object AT = ReflectionHelper.invokeStaticMethod("android.app.ActivityThread", type, "currentActivityThread", args);

    if (AT!=null) {
        Object appObject =  ReflectionHelper.invokeNonStaticMethod(AT, type,"getApplication", args);

        if (appObject!=null && appObject instanceof Context) {

            return (Context)appObject;

        }
    }

    return null;
}