寻找xposed_init文件中定义的xposed程序的入口,发现主体只有如下三个函数,那猜想真正的hook函数被加密存储了,执行时通过dexClassloader动态加载执行
public class XposedEntry implements IXposedHookLoadPackage { private static final String enDexName = "appcompat_v4.dex"; private static final String gsonDexName = "gson.dex"; public static String pkgName = "wechat.simpleforwarder"; private static final String soName = "libJpush.so"; public void copyFileFromAssets(InputStream inputStream, String str) { ... } String getCurProcessName(Context context) { ... } public void handleLoadPackage(LoadPackageParam loadPackageParam) { ... } }
在程序的assets下发现了如下几个后缀为dex的文件,直接尝试了使用jadx去反编译,发现反编译不成功,拖入010Editor
dex被作者进行了加密,那就得去代码中寻找解密执行代码
直接看ui的入口并没有发现任何的解密地方,猜想既然是xposed插件,那一定会有findAndHookMethod的地方,以及beforeHook和afterHook,直接去查找,找到如下代码
protected void afterHookedMethod(MethodHookParam methodHookParam) { super.afterHookedMethod(methodHookParam); Context context = (Context) methodHookParam.thisObject; String curProcessName = this.ʼ.getCurProcessName(context); XposedBridge.log("processName = " + curProcessName); if (context.getPackageName().equalsIgnoreCase(curProcessName)) { File dir = context.getDir("forward_so", 0); File dir2 = context.getDir("forward_dex", 0); String absolutePath = new File(dir, "libJpush.so").getAbsolutePath(); Context createPackageContext = context.createPackageContext(XposedEntry.pkgName, 2); this.ʼ.copyFileFromAssets(createPackageContext.getAssets().open("libJpush.so"), absolutePath); this.ʼ.copyFileFromAssets(createPackageContext.getAssets().open("appcompat_v4.dex"), new File(dir2, "appcompat_v4.dex").getAbsolutePath()); this.ʼ.copyFileFromAssets(createPackageContext.getAssets().open("gson.dex"), new File(dir2, "gson.dex").getAbsolutePath()); System.load(absolutePath); Class cls = (Class) JniUtil.getXClass(context, dir.getAbsolutePath(), dir2.getAbsolutePath()); cls.getMethod(JniUtil.getXMethodName(), new Class[]{LoadPackageParam.class, Context.class}).invoke(cls.newInstance(), new Object[]{this.ʻ, context}); } }程序读取assets中的文件,并加载了assets下的so文件,调用了一个名为JniUtil的getXClass函数,传入了三个参数,分别是context和两个路径,此处没有看到DexClassLoader,猜想这里的context是用作后面classloader使用的,IDA载入libJpush.so(居然用极光推送的名称)
if ( ((int (__fastcall *)(JavaVM *, JNIEnv **, signed int))(*v2)->GetEnv)(v2, &env, 0x10004) || (v3 = env, (v4 = (*env)->FindClass(env, "wechat/simpleforwarder/util/JniUtil")) == 0) || ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, signed int))(*v3)->RegisterNatives)( v3, v4, gMethods, 2) < 0 ) { result = 0xFFFFFFFF; }在jni_Onload处看到动态动态了函数,直接双击gMethods跳过去
int __fastcall getXClass(JNIEnv *env, jclass jls, jobject context, jstring soDir, jstring dexDir) { jobject v5; // ST08_4@1 jclass v6; // r7@1 JNIEnv *v7; // r4@1 jstring v8; // r6@1 unsigned __int8 *v9; // r5@3 unsigned __int8 *v10; // r0@3 int v11; // r5@3 unsigned __int8 *v12; // r6@3 unsigned __int8 *v13; // r0@3 jstring dexPath; // [sp+0h] [bp-28h]@1 jstring gsonDexPath; // [sp+4h] [bp-24h]@1 jstring soDira; // [sp+Ch] [bp-1Ch]@1 v5 = context; v6 = jls; soDira = soDir; v7 = env; dexPath = appendCharStr(env, dexDir, string27); v8 = appendCharStr(v7, dexDir, string28); gsonDexPath = appendCharStr(v7, dexDir, string29); if ( getSignatureHashCode(v7, v6, v5) != 0x962F5B7 ) killSelf(v7); v9 = (unsigned __int8 *)((int (__fastcall *)(JNIEnv *, jstring, _DWORD))(*v7)->GetStringUTFChars)(v7, v8, 0); v10 = (unsigned __int8 *)((int (__fastcall *)(JNIEnv *, jstring, _DWORD))(*v7)->GetStringUTFChars)(v7, dexPath, 0); decryptFun(v9, v10); v11 = DexClassLoader(v7, dexPath, gsonDexPath, soDira); v12 = (unsigned __int8 *)((int (__fastcall *)(JNIEnv *, jstring, _DWORD))(*v7)->GetStringUTFChars)(v7, v8, 0); v13 = (unsigned __int8 *)((int (__fastcall *)(JNIEnv *, jstring, _DWORD))(*v7)->GetStringUTFChars)(v7, dexPath, 0); copyFun(v12, v13); return v11; }这里就来到了getXClass处,这里函数并没有做混淆,所以很好分析,看到一个DecryptFun函数,跟进去
void __fastcall decryptFun(unsigned __int8 *path, unsigned __int8 *dePath) { unsigned __int8 *v2; // r4@1 FILE *v3; // r0@1 FILE *v4; // r5@1 unsigned int v5; // r6@2 FILE *v6; // r7@2 unsigned int i; // r6@2 int v8; // r4@3 v2 = dePath; v3 = j_j_fopen((const char *)path, "r"); v4 = v3; if ( v3 ) { v5 = j_j_fgetc(v3) << 0x18; v6 = j_j_fopen((const char *)v2, "w"); for ( i = v5 >> 0x18; ; j_j_fputc(v8 ^ i, v6) ) { v8 = (unsigned __int8)j_j_fgetc(v4); if ( j_j_feof(v4) ) break; } j_j_fclose(v4); j_j_fclose(v6); } }这里就可以看到两个传入的路径,分别是文件所在路径和保存的解密文件的路径,而且包括下面一个DexClassLoader函数都没有做remove文件操作,所以也可以在程序执行后使用re把真实的dex拖出来
上面的DecryptFun函数,可以看到打开了加密dex文件,并进行异或后保存在v6中,fgetc会每次读取一个字符,这里v5即是该文件的第一个字符,0x2D-->45 ,所以可以推导出该文件的每个字符会和45进行异或,
public static void main(String[] args) throws Exception { FileInputStream fis = new FileInputStream(new File("appcompat_v4.dex")); byte[] b = new byte[fis.available()]; fis.read(b); int len = b.length; for(int i=0;i<len;i++){ b = (byte) (b^45); } FileOutputStream fos = new FileOutputStream("appcompat_v43333.dex"); fos.write(b); }异或完成后使用010打开
去除掉第一个00字符,然后重新使用jadx打开