本文來自i春秋作者: penguin_wwy

零、問題出現

對dex文件進行加密,解密后動態加載是一種常用的加殼方式(一代殼以這種方式為主)。但這種在解密之后往往會產生一個解密后的完整dex。過程一般是這樣的 打開文件

File file = new File("classes.dex");

讀取字節碼

byte[] buffer = new FileInputStream(file).read();

解密字節碼

decrypt(buffer)

重寫到文件

File newFile = new File("classes_decrypt.dex"); new FileOutputStream(newFile).write(buffer);

加載dex

DexClassLoader dexClassLoader = new DexClassLoader("classes_decrypt.dex"...);

可見在重寫到文件這一步,就有可能被截獲到解密后的dex,那加密dex的意義就完全不存在了。 當然也有過許多辦法,比如加載完后刪除文件、或者隱藏文件等等,但都沒法從根本上解決問題。而最有實際意義的方法就是今天要說的,不落地加載dex。

一、理論基礎

不落地的含義就是說在解密后直接由字節碼進行加載,不需要變成dex文件。Dalvik中的兩種類加載器DexClassLoader和PathClassLoader顯然都不具備這個能力。我們需要自己定義一個類加載器。 那如何自己定義呢?我們先分析一下DexClassLoader加載的過程(詳細分析請看我的博客)。這里簡單說明一下,首先是DexClassLoader的構造函數 源碼位置 libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java

public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {[url=home.php?mod=space&uid=74926]@Code[/url] DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}

實質上是對它的父類,BaseDexClassLoader的構造 源碼位置 libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

libcore\dalvik\src\main\java\dalvik\system\DexPathList.java

public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
?
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
?
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
?
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
?
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

重點在函數makeDexElements

private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
?
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
?
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
?
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* IOException might get thrown "legitimately" by
* the DexFile constructor if the zip file turns
* out to be resource-only (that is, no
* classes.dex file in it). Safe to just ignore
* the exception here, and let dex == null.
*/
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
?
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
?
return elements.toArray(new Element[elements.size()]);
}

根據文件后綴名的判斷選擇分支,然后調用loadDex函數

private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}

DexFile.loadDex這個函數的內部也只是構造一個DexFile對象,所以直接看DexFile的構造函數就好

private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
?
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}

重點的重點在openDexFile,這個函數負責最終的dex文件加載

運行流程

DexClassLoader ——> BaseDexClassLoader ——> DexPathList ——> makeDexElements ——> loadDex ——> DexFile

這個openDexFile函數是一個native函數,在libdvm.so中,看對應的函數表

const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { 
{ "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I", 
Dalvik_dalvik_system_DexFile_openDexFile }, 
{ "openDexFile", "([B)I", 
Dalvik_dalvik_system_DexFile_openDexFile_bytearray }, 
{ "closeDexFile", "(I)V", 
Dalvik_dalvik_system_DexFile_closeDexFile }, 
{ "defineClass", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;", 
Dalvik_dalvik_system_DexFile_defineClass }, 
{ "getClassNameList", "(I)[Ljava/lang/String;", 
Dalvik_dalvik_system_DexFile_getClassNameList }, 
{ "isDexOptNeeded", "(Ljava/lang/String;)Z", 
Dalvik_dalvik_system_DexFile_isDexOptNeeded }, 
{ NULL, NULL, NULL }, 
};

調用表中第一個openDexFile所對應的Dalvik_dalvik_system_DexFile_openDexFile ,這個就是實際執行的函數,函數參數 "(Ljava/lang/String;Ljava/lang/String;I)I" 兩個字符串一個整型。 而意外的發現在它的下一個位置Dalvik_dalvik_system_DexFile_openDexFile_bytearray,它的參數 ([B)I 一個byte數組和一個整型,也就是說如果我們直接調用這個函數的話,就可以將字節碼以一個byte數組的形式傳入。了解到這里,我們的目標就清晰了。

(1)構造一個我們自己的類加載器

(2)通過Dalvik_dalvik_system_DexFile_openDexFile_bytearray,來加載dex文件的字節碼

二、開工實踐

下面我們就來嘗試實現一下,首先我們需要一個正常的Apk,越簡單越好,最好不需要太多資源文件,加載了dex能直接運行,畢竟只是實驗一下。上一篇當中的TestApk就很合適。解壓出它的classes.dex,放到手機/data/local/tmp文件夾下

然后新建一個Apk,就叫DexFile, 準備一個java類,負責native函數

public class JNITool {
static {
System.loadLibrary("JNITool");
}
?
public static native int loadDex(byte[] dex,long dexlen);
}

這個loadDex就負責通過我們前面所述的函數加載dex。在JNITool.so,我們要加載libdvm.so并且找到Dalvik_dalvik_system_DexFile_openDexFile_bytearray函數 所以需要定義JNI_OnLoad函數

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
?
void *ldvm = (void*) dlopen("libdvm.so", RTLD_LAZY);
dvm_dalvik_system_DexFile = (JNINativeMethod*) dlsym(ldvm, "dvm_dalvik_system_DexFile");
?
//openDexFile
if(0 == lookup(dvm_dalvik_system_DexFile, "openDexFile", "([B)I",&openDexFile)) {
openDexFile = NULL;
LOGI("openDexFile method does not found ");
}else{
LOGI("openDexFile method found ! HAVE_BIG_ENDIAN");
}
?
LOGI("ENDIANNESS is %c" ,ENDIANNESS );
void *venv;
LOGI("dufresne----->JNI_OnLoad!");
if ((*vm)->GetEnv(vm, (void**) &venv, JNI_VERSION_1_4) != JNI_OK) {
LOGI("dufresne--->ERROR: GetEnv failed");
return -1;
}
return JNI_VERSION_1_4;
}

dlopen函數鏈接libdvm.so,dlsym找到并返回dvm_dalvik_system_DexFile。dvm_dalvik_system_DexFile就是我們之前看到的函數表

const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { 
{ "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I", 
Dalvik_dalvik_system_DexFile_openDexFile }, 
{ "openDexFile", "([B)I", 
Dalvik_dalvik_system_DexFile_openDexFile_bytearray }, 
{ "closeDexFile", "(I)V", 
Dalvik_dalvik_system_DexFile_closeDexFile }, 
{ "defineClass", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;", 
Dalvik_dalvik_system_DexFile_defineClass }, 
{ "getClassNameList", "(I)[Ljava/lang/String;", 
Dalvik_dalvik_system_DexFile_getClassNameList }, 
{ "isDexOptNeeded", "(Ljava/lang/String;)Z", 
Dalvik_dalvik_system_DexFile_isDexOptNeeded }, 
{ NULL, NULL, NULL }, 
};

lookup從函數表中尋找我們要的Dalvik_dalvik_system_DexFile_openDexFile_bytearray

int lookup(JNINativeMethod *table, const char *name, const char *sig,
void (**fnPtrout)(u4 const *, union JValue *)) {
int i = 0;
while (table.name != NULL)
{
LOGI("lookup %d %s" ,i,table.name);
if ((strcmp(name, table.name) == 0)
&& (strcmp(sig, table.signature) == 0))
{
*fnPtrout = table.fnPtr;
return 1;
}
i++;
}
return 0;
}

找到之后就用全局的函數指針

void (*openDexFile)(const u4* args, union JValue* pResult);

來保存這個函數

JNIEXPORT jint JNICALL Java_cn_wjdiankong_dexfiledynamicload_NativeTool_loadDex(JNIEnv* env, jclass jv, jbyteArray dexArray, jlong dexLen)
{
// header+dex content
u1 * olddata = (u1*)(*env)-> GetByteArrayElements(env,dexArray,NULL);
char* arr;
arr = (char*)malloc(16 + dexLen);
ArrayObject *ao=(ArrayObject*)arr;
ao->length = dexLen;
memcpy(arr+16,olddata,dexLen);
u4 args[] = { (u4) ao };
union JValue pResult;
jint result;
if(openDexFile != NULL) {
openDexFile(args,&pResult);
}else{
result = -1;
}
?
result = (jint) pResult.l;
LOGI("Java_cn_wjdiankong_dexfiledynamicload_NativeTool_loadDex %d" , result);
return result;
}

loadDex函數最終會通過這個函數指針來調用dvm_dalvik_system_DexFile,最終加載dex

那么回到Java層,我們需要定義一個自己的類加載器

public class DynamicDexClassLoder extends DexClassLoader {
?
private static final String TAG = "dexlog";
private int cookie;
private Context mContext;

構造函數

public DynamicDexClassLoder(Context context, byte[] dexBytes,
String libraryPath, ClassLoader parent, String oriPath,
String fakePath) {
super(oriPath, fakePath, libraryPath, parent);
setContext(context);
?
int cookie = JNITool.loadDex(dexBytes, dexBytes.length);
?
setCookie(cookie);
?
}

cookie這個變量代表了加載完成后的dex的句柄 然后實現findClass函數

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Log.d(TAG, "findClass-" + name);
Class<?> cls = null;
String as[] = getClassNameList(cookie);
Class obj_class = Class.forName(DexFile.class.getName());
Method method = obj_class.getDeclaredMethod("defineClassNative",
new Class[]{String.class, ClassLoader.class, int.class});
method.setAccessible(true);
for (int z = 0; z < as.length; z++) {
Log.i(TAG, "classname:"+as[z]);
if (as[z].equals(name)) {
cls = (Class) method.invoke(null, 
new Object[]{as[z].replace('.', '/'), mContext.getClassLoader(), cookie});
} else {
//加載其他類
method.invoke(null, 
new Object[]{as[z].replace('.', '/'), mContext.getClassLoader(), cookie});
}
}
?
if (null == cls) {
cls = super.findClass(name);
}
?
return cls;
}

然后在MainActivity中我們就可以通過以下代碼,啟動TestApk的MainActivity

DynamicDexClassLoder dLoader = new DynamicDexClassLoder(
getApplicationContext(),
dexContent,
null,
clzLoader,
getPackageResourcePath(),getDir(".dex", MODE_PRIVATE).getAbsolutePath()
);
Class clazz = dLoader.findClass("com.example.testapk.MainActivity");
Intent intent = new Intent(this, clazz);
startActivity(intent);

三、小結

以上的代碼在Android5.0以下的Android系統上可以正確執行(少數真機可能會出問題),我測試的時候在原生的Android4.4上成功。至于Android5.0?不好意思,從Android5.0開始,谷歌已經放棄了Dalvik虛擬機,轉而支持ART,沒有了libdvm,所以。。。。之后我會考慮研究一下怎么在ART虛擬機中實現。

這種不落地的加載方式是現在加殼方式的一部分。現在的加殼方法往往是多種方法捏合在一起的(還有那種喪心病狂的VMP),大家可以試試將上篇的方法和這篇結合起來,對一個加密的dex,解密后不落地加載,之后再修復dex中的錯誤指令。之后我也會介紹越來越多的加殼、抗反編譯方法,都可以嘗試結合在一起。

原文地址:http://bbs.ichunqiu.com/thread-12734-1-1.html?from=paper


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/59/