本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78092365
Dalvik模式下的Android加固技術已經很成熟了,Dalvik虛擬機模式下的Android加固技術也在不斷的發展和加強,鑑於Art虛擬機比Dalvik虛擬機的設計更復雜,Art虛擬機模式下兼容性更嚴格,一些Dalvik虛擬機模式下的Android加固技術並不能馬上移植到Art模式下以及鑑於Art虛擬機模式下的設計複雜和兼容性考慮,暫時相對來說,Art模式下的Android加固並沒有Dalvik虛擬機模式下的粒度細和強。
本文給出的 Art模式下基於Xposed Hook開發脫殼工具的思路和流程不是我原創,主要是源自於看雪論壇的文章《一個基於xposed和inline hook的一代殼脫殼工具》,思路和流程有原作者smartdon提供,文中提到的Art模式下的dexdump脫殼工具源碼github下載地址:https://github.com/smartdone/dexdump。原作者提供的脫殼操作步驟稍微複雜了一些,在此基礎上我對原作者的代碼進行了修改,使脫殼更加方便,原作者的代碼是Android Studio的工程,順手將其轉化爲了Eclipse下的工程。作者smartdon的Art模式下脫殼思路如下圖所示:
要學習Android加固的脫殼還是需要先了解一下Dalvik模式下和Art模式下Android加固的流程和思路,熟悉一下 DexClassLoader 的代碼執行流程。雖然Dalvik模式下和Art模式下DexClassLoader的java層實現代碼是一樣的,但是從 OpenDexFileNative函數 之後Dalvik模式下和Art模式下Native層的代碼實現就不一樣了,後面有空花時間整理一下Android加固相關方面的知識。Art模式下基於Xposed Hook開發的脫殼工具只對整體dex加固的Android應用脫殼纔有效果,對於dex文件類方法抽離這類加固處理的Android應用就顯得比較蒼白了。
ART模式下基於Xposed Hook開發脫殼工具的思路整理。
1. Art模式下,Inline Hook時機 的選擇
Android加固的一般思路:在外殼Apk應用調用 android.app.Application類 的成員函數 attach 時,內存解密出被保護的原始dex文件使用DexClassLoader進行內存加載,Art虛擬機模式下DexClassLoader進行dex文件的加載過程中繞不開函數 const DexFile* DexFile::OpenMemory(const std::string& location, uint32_t location_checksum, MemMap* mem_map, std::string* error_msg),因此我們選擇在 art::DexFile::OpenMemory函數 處進行dex文件的內存dump處理。基於Art模式下的Xposed Hook實現在外殼apk應用調用 android.app.Application類 的成員函數 attach 時,在被保護的dex文件加載之前Inline Hook OpenMemory函數,對內存解密後的原始dex文件進行攔截。
Art模式下,Xposed Hook外殼apk應用 android.app.Application類(實現的代理子類) 的成員函數 attach。
2. Art模式下,Inline Hook函數點 的選擇
Art虛擬機模式下,對 art::DexFile::OpenMemory函數 進行Inline Hook操作所採用的Hook框架爲作者 Ele7enxxh 編寫的Android平臺的Inline Hook庫。作者Ele7enxxh關於該Inline Hook庫的介紹和描述可以參考作者的博文《Android Arm Inline Hook》,該Inline Hook庫的github下載地址爲:https://github.com/ele7enxxh/Android-Inline-Hook。由於Art虛擬機模式下,dex文件的加載DexClassLoader的代碼實現流程中繞不開art::DexFile::OpenMemory函數的執行,更重要的是該函數的傳入參數 base表示的是dex文件所在的內存地址, size表示的是dex文件的字節長度,因此選擇在art::DexFile::OpenMemory函數處進行dex文件的內存dump處理。
Android 5.0版以後ART模式下,OpenMemory函數的形式:http://androidxref.com/5.0.0_r2/xref/art/runtime/dex_file.cc#325
ART模式下基於Xposed Hook和Ele7enxxh Inline Hook開發的脫殼工具dexdump的代碼詳細分析。
1. 作者smartdon寫了個獲取當前安裝應用的列表界面,用以選擇需要脫殼的apk應用,然後根據選擇脫殼apk應用的包名,在sdcard文件夾下生成脫殼需要的配置文件dumdex.js,dumdex.js文件中保存着需要脫殼的apk應用的包名。
選擇脫殼apk應用列表界面的實現代碼 MainActivity.java:
package com.xposedhook.dexdump;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import com.example.com.xposedhook.dexdump.R;
public class MainActivity extends Activity {
// static {
//
// // 加載動態庫文件libhook.so
// System.loadLibrary("hook");
// }
private List<Appinfo> appinfos;
private ListView listView;
private AppAdapter adapter;
private List<String> selected;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 設置佈局文件
setContentView(R.layout.activity_main);
// 調用native層實現的dump函數
// 對函數art::DexFile::OpenMemory進行Hook處理
// Dumpper.dump();
// 讀取配置文件"/sdcard/dumdex.js"獲取需要脫殼的apk應用的包名列表
selected = Config.getConfig();
// Apk應用信息列表
appinfos = new ArrayList<>();
// 用於顯示apk應用的列表
listView = (ListView) findViewById(R.id.applist);
adapter = new AppAdapter(appinfos);
// 設置ListView控件的適配器
listView.setAdapter(adapter);
// 創建線程
new Thread(){
@Override
public void run() {
super.run();
// 獲取當前Android系統安裝的apk應用列表
getInstallAppList();
}
}.start();
}
private void getInstallAppList() {
try{
// 獲取當前安裝應用的PackageInfo列表
List<PackageInfo> packageInfos = getPackageManager().getInstalledPackages(0);
// 遍歷當前安裝應用的PackageInfo列表
for(PackageInfo packageInfo : packageInfos) {
Appinfo info = new Appinfo();
// 設置apk應用的名稱
info.setAppName(packageInfo.applicationInfo.loadLabel(getPackageManager()).toString());
// 設置apk應用的包名
info.setAppPackage(packageInfo.packageName);
// 根據當前遍歷到apk應用的包名是否在配置文件中設置選中與否的現實
if(Config.contains(selected, info.getAppPackage())) {
info.setChecked(true);
}else {
info.setChecked(false);
}
// 添加當前遍歷到apk應用的信息到apk應用的現實列表中
appinfos.addAll(info);
// 更新適配器的現實
adapter.notifyDataSetChanged();
}
}catch (Exception e) {
e.printStackTrace();
}
}
// ListView列表的適配器
@SuppressLint({ "ViewHolder", "InflateParams" })
class AppAdapter extends BaseAdapter{
private List<Appinfo> appinfos;
public AppAdapter(List<Appinfo> appinfos){
this.appinfos = appinfos;
}
@Override
public int getCount() {
return appinfos.size();
}
@Override
public Object getItem(int i) {
return appinfos.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
View v = LayoutInflater.from(MainActivity.this).inflate(R.layout.item, null);
final int posi = i;
final TextView appname = (TextView) v.findViewById(R.id.tv_appname);
appname.setText(appinfos.get(i).getAppName());
TextView appPackage = (TextView) v.findViewById(R.id.tv_apppackage);
appPackage.setText(appinfos.get(i).getAppPackage());
CheckBox checkBox = (CheckBox) v.findViewById(R.id.cb_select);
if(appinfos.get(i).isChecked()) {
checkBox.setChecked(true);
}else {
checkBox.setChecked(false);
}
// 監控apk應用列表的選中事件
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
if(b) {
// 添加選中的apk應用的包名到配置文件/sdcard/dumdex.js中
// 格式: ["apk包名字符串"]
Config.addOne(appinfos.get(posi).getAppPackage());
} else {
// 從配置文件/sdcard/dumdex.js中刪除指定包名的apk引用
Config.removeOne(appinfos.get(posi).getAppPackage());
}
}
});
return v;
}
}
}
根據用戶選擇的脫殼apk應用的包名,生成脫殼需要的配置文件dumdex.js文件的代碼 Config.java:
package com.xposedhook.dexdump;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* Created by smartdone on 2017/7/2.
*/
public class Config {
// sdcard的問價路徑最好還是通過函數來獲取
// File file=Environment.getExternalStorageDirectory();
// 直接寫死有兼容性的問題
private static final String FILENAME = "/sdcard/dumdex.js";
// 將JSONArray類型的數據寫入到配置文件"/sdcard/dumdex.js"中
public static void writeConfig(String s) {
try {
// 文件"/sdcard/dumdex.js"的文件寫入流
FileOutputStream fout = new FileOutputStream(FILENAME);
// 將字符串寫入到文件中
fout.write(s.getBytes("utf-8"));
// 刷新文件流
fout.flush();
fout.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 添加Apk應用的包名到配置文件/sdcard/dumdex.js
public static void addOne(String name) {
List<String> ori = getConfig();
if(ori == null) {
JSONArray jsonArray = new JSONArray();
jsonArray.put(name);
writeConfig(jsonArray.toString());
} else {
ori.add(name);
JSONArray jsonArray = new JSONArray();
for(String o : ori) {
jsonArray.put(o);
}
writeConfig(jsonArray.toString());
}
}
// 從配置文件/sdcard/dumdex.js中刪除指定包名的應用
public static void removeOne(String name) {
List<String> ori = getConfig();
if(ori != null) {
for(int i = 0; i < ori.size(); i++) {
if(ori.get(i).equals(name)) {
ori.remove(i);
}
}
JSONArray jsonArray = new JSONArray();
for(String s : ori) {
jsonArray.put(s);
}
writeConfig(jsonArray.toString());
}
}
// 讀取配置文件"/sdcard/dumdex.js"獲取需要脫殼的apk應用的包名列表
public static List<String> getConfig() {
// 打開文件"/sdcard/dumdex.js"
File file = new File(FILENAME);
// 判斷文件是否存在
if (file.exists()) {
try {
// 構建內存緩衝區讀取流
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
// 分行讀取文件數據
String line = br.readLine();
// 使用讀取的一行文件數據構建JSONArray對象
JSONArray jsonArray = new JSONArray(line);
List<String> apps = new ArrayList<>();
// 解析JSONArray數據將需要Hook的apk應用的包名添加到列表中
for(int i = 0; i < jsonArray.length(); i++) {
apps.add(jsonArray.getString(i));
}
br.close();
// Log.e("DEX_DUMP", "需要hook的列表: " + line);
return apps;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
// 判斷name是否在需要脫殼的apk應用的列表中
public static boolean contains(List<String> lists, String name) {
if(lists == null) {
return false;
}
for(String l : lists) {
if(l.equals(name)) {
return true;
}
}
return false;
}
}
2. 基於Art虛擬機模式下的Xposed框架 Hook外殼Apk應用 android.app.Application類 的成員函數 attach,這裏提到的Xposed Hook框架需要注意一下,不能使用支持Android 4.4.x版本之前的Xposed Hook框架(只支持Dalvik虛擬機模式,不支持Art虛擬機模式),需要使用支持Android 5.0版本以後的Xposed Hook框架(支持Art虛擬機模式),Xposed Hook框架的下載地址可以參考:http://repo.xposed.info/module/de.robv.android.xposed.installer。Art虛擬機模式下,Xposed框架 Hook外殼Apk應用 android.app.Application類 的成員函數 attach 的模塊代碼 com.xposedhook.dexdump.Main 編寫的實現:
package com.xposedhook.dexdump;
import android.content.Context;
import android.util.Log;
import java.util.List;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* Created by smartdone on 2017/7/1.
*/
// art模式下的Xposed Hook
public class Main implements IXposedHookLoadPackage {
private static final String TAG = "DEX_DUMP";
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
// 從配置文件"/sdcard/dumdex.js"中獲取需要脫殼的apk應用列表
List<String> hooklist = Config.getConfig();
// 判斷當前應用是否在需要脫殼的apk應用列表中
if(!Config.contains(hooklist, loadPackageParam.packageName))
return;
XposedBridge.log("對" + loadPackageParam.packageName + "進行處理");
Log.e(TAG, "開始處理: " + loadPackageParam.packageName);
try{
// 自定義加載動態庫文件libhook.so
// 可以試着使用兼容性好的Android系統函數來處理路徑問題
System.load("/data/data/com.xposedHook.dexdump/lib/libhook.so");
} catch (Exception e) {
Log.e(TAG, "加載動態庫失敗:" + e.getMessage());
}
Log.e(TAG, "加載動態庫成功");
// 對Android系統類android.app.Application的attach函數進行art模式下的Hook操作
XposedHelpers.findAndHookMethod("android.app.Application",
loadPackageParam.classLoader,
"attach",
Context.class,
new XC_MethodHook() {
private Context context;
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
// 在類android.app.Application的attach函數調用之前進行dex文件的內存dump操作
Dumpper.dump();
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 不處理
}
});
}
}
3.在需要脫殼的apk應用進程裏動態加載動態庫文件/data/data/com.xposedHook.dexdump/lib/libhook.so,實現Art模式下對 const DexFile* DexFile::OpenMemory(const std::string& location, uint32_t location_checksum, MemMap* mem_map, std::string* error_msg)函數 的Inline Hook操作,在Inline Hook操作的自定義實現函數裏進行dex文件的內存dump處理。使用Ele7enxxh Inline Hook框架對Art模式下的OpenMemory函數進行Hook操作的實現代碼如下:
//
// Created by 袁東明 on 2017/7/1.
//
extern "C" {
#include "include/inlineHook.h"
}
#include "dump.h"
#include <unistd.h>
#include <android/log.h>
#include <sys/system_properties.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>
#include <string>
#include <dlfcn.h>
#include <dlfcn.h>
#define TAG "DEX_DUMP"
int isArt();
void getProcessName(int pid, char *name, int len);
void dumpFileName(char *name, int len, const char *pname, int dexlen);
// 保存當前apk進程的進程名字
static char pname[256];
// 判斷當前所處環境是否是Android art虛擬機模式
int isArt() {
char version[10];
// 獲取ro.build.version.sdk的屬性值
__system_property_get("ro.build.version.sdk", version);
// 打印當前Android系統的api版本信息
__android_log_print(ANDROID_LOG_INFO, TAG, "api level %s", version);
// 將api版本轉換成int型版本號
int sdk = atoi(version);
// 判斷api版本是否是大於21(要求Android系統的版本爲 Android 5.0以上 纔可以)
if (sdk >= 21) {
// art虛擬機模式
return 1;
}
return 0;
}
// 讀取/proc/self/cmdline文件的數據,獲取當前apk進程的進程名字
void getProcessName(int pid, char *name, int len) {
int fp = open("/proc/self/cmdline", O_RDONLY);
memset(name, 0, len);
read(fp, name, len);
close(fp);
}
// 格式字符串構建dump的dex文件的路徑字符串
void dumpFileName(char *name, int len, const char *pname, int dexlen) {
time_t now;
struct tm *timenow;
time(&now);
// 獲取當前時間(值得借鑑和學習)
timenow = localtime(&now);
memset(name, 0, len);
// 格式化字符串得到當前dump的dex文件路徑字符串
sprintf(name, "/data/data/%s/dump_size_%u_time_%d_%d_%d_%d_%d_%d.dex", pname, dexlen,
timenow->tm_year + 1900,
timenow->tm_mon + 1,
timenow->tm_mday,
timenow->tm_hour,
timenow->tm_min,
timenow->tm_sec);
}
void writeToFile(const char *pname, u_int8_t *data, size_t length) {
char dname[1024];
// pname爲當前進程的名稱
// 格式字符串構建dump的dex文件的路徑字符串dname
dumpFileName(dname, sizeof(dname), pname, length);
__android_log_print(ANDROID_LOG_ERROR, TAG, "dump dex file name is : %s", dname);
__android_log_print(ANDROID_LOG_ERROR, TAG, "start dump");
// 根據dname創建新文件用於保存內存dump的dex文件
int dex = open(dname, O_CREAT | O_WRONLY, 0644);
if (dex < 0) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "open or create file error");
return;
}
// 將內存dex文件的數據寫入到新的dname文件中
int ret = write(dex, data, length);
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "write file error");
} else {
__android_log_print(ANDROID_LOG_ERROR, TAG, "dump dex file success `%s`", dname);
}
// 關閉文件
close(dex);
}
// 保存openmemory函數舊的地址
art::DexFile *(*old_openmemory)(const byte *base, size_t size, const std::string &location,
uint32_t location_checksum, art::MemMap *mem_map,
const art::OatDexFile *oat_dex_file, std::string *error_msg) = NULL;
art::DexFile *new_openmemory(const byte *base, size_t size, const std::string &location,
uint32_t location_checksum, art::MemMap *mem_map,
const art::OatDexFile *oat_dex_file, std::string *error_msg) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "art::DexFile::OpenMemory is called");
writeToFile(pname, (uint8_t *) base, size);
// 調用原art::DexFile::OpenMemory函數
return (*old_openmemory)(base, size, location, location_checksum, mem_map, oat_dex_file,
error_msg);
}
void hook() {
// 加載動態庫文件libart.so
void *handle = dlopen("libart.so", RTLD_GLOBAL | RTLD_LAZY);
if (handle == NULL) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "Error: unable to find the SO : libart.so");
return;
}
// 獲取導出函數const DexFile* DexFile::OpenMemory(const std::string& location, uint32_t location_checksum, MemMap* mem_map, std::string* error_msg)
// 的調用地址,http://androidxref.com/5.0.0_r2/xref/art/runtime/dex_file.cc#325
// 在不同的Android版本上OpenMemory函數的名稱粉碎稍有不同,需要根據實際Android系統版本進行修改
void *addr = dlsym(handle,
"_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_");
if (addr == NULL) {
__android_log_print(ANDROID_LOG_ERROR, TAG,
"Error: unable to find the Symbol : _ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_");
return;
}
// 使用ele7enxxh寫的inline Hook框架對art模式下的art::DexFile::OpenMemory函數進行inline Hook操作
// 進行art::DexFile::OpenMemory函數inline Hook操作的Hook註冊
if (registerInlineHook((uint32_t) addr, (uint32_t) new_openmemory,
(uint32_t **) &old_openmemory) != ELE7EN_OK) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "register inline hook failed");
return;
}
// 對art模式下的art::DexFile::OpenMemory函數進行inline Hook操作
if (inlineHook((uint32_t) addr) != ELE7EN_OK) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "inline hook failed");
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "inline hook success");
}
// java方法Dumpper.dump()的native層實現
// com.xposedhook.dexdump.Dumpper.dump
JNIEXPORT void JNICALL Java_com_xposedhook_dexdump_Dumpper_dump(JNIEnv *env, jclass clazz) {
// 獲取當前apk進程的進程名字
getProcessName(getpid(), pname, sizeof(pname));
// 判斷當前Android虛擬機是否是art模式
if (isArt()) {
// 當前Android系統運行在art模式下
// 執行inline Hook操作
hook();
}
}
4. 使用當前脫殼工具進行Art虛擬機模式下的Android加固脫殼需要注意的地方:
A. 移動設備的Android系統必須是 Api 21以上即Android 5.0以上版本的Android系統並且要運行在Art虛擬機模式下,不能運行在Dalvik虛擬機模式下。
B. 移動設備上安裝的Xposed Hook框架必須是支持Android 5.0以上版本的ART虛擬機模式下的Xposed Hook框架。
C. 第1次使用該脫殼工具com.xposedhook.dexdump.apk時,先使用apk應用列表界面選擇需要脫殼的apk應用,生成後面脫殼需要的配置文件"/sdcard/dumdex.js"。
D. 重啓移動設備使Xposed框架的Hook模塊com.xposedhook.dexdump.Main生效,運行需要脫殼的apk應用等待脫殼完成,脫殼後的dex文件在 /data/data/脫殼apk應用的包名/ 文件夾下。
註釋版完整的代碼下載地址:http://download.csdn.net/download/qq1084283172/9996172