首页
社区
课程
招聘
[原创]记一次某街区app百度加固分析
发表于: 2天前 837

[原创]记一次某街区app百度加固分析

2天前
837
java层分析

这种app一般都是有壳存在的,第一步先分析这个壳,先看xml文件第一个启动的activitycom.sagittarius.v6.StubApplication

关键的如下

public class StubApplication extends Application {
    private static final String[] lllIIl;
    private ArrayList mActivityCallbacks;
    public static Context mContext;
    public static int mInstallProviderCounter;
    public static Application mRealApplication;
    public static boolean mRealApplicationPatched;
    public static boolean skipLoad;
    static {
        StubApplication.lllIIl();
        StubApplication.mContext = null;
        StubApplication.mRealApplication = null;
        StubApplication.mRealApplicationPatched = false;
        StubApplication.mInstallProviderCounter = 0;
        StubApplication.skipLoad = false;
        f.a().init();
    }
    public StubApplication() {
        this.mActivityCallbacks = null;
    }
    private static String IllIIl(String s, String s1) {
        byte[] arr_b = a.a(s);
        StringBuilder stringBuilder0 = new StringBuilder();
        char[] arr_c = s1.toCharArray();
        int v1 = 0;
        for(int v = 0; v < arr_b.length; ++v) {
            stringBuilder0.append(((char)(arr_b[v] ^ arr_c[v1 % arr_c.length])));
            ++v1;
        }
        return stringBuilder0.toString();
    }
    @Override  // android.content.ContextWrapper
    protected void attachBaseContext(Context context0) {
        if((AppInfo.FLAGS & 0x100000) != 0 && t.a().contains(StubApplication.lllIIl[0])) {
            StubApplication.skipLoad = true;
        }
        if(StubApplication.skipLoad) {
            super.attachBaseContext(context0);
        }
        else {
            t.a(this, context0);
            StubApplication.mContext = context0;
            AppInfo.PKGNAME = "com.kurogame.kjq";
            AppInfo.APKPATH = context0.getApplicationInfo().sourceDir;
            AppInfo.DATAPATH = t.a(context0.getApplicationInfo().dataDir);
            if(Debug.isDebuggerConnected()) {
                Process.killProcess(Process.myPid());
            }
            else {
                if(Build.VERSION.SDK_INT > 27) {
                    r.a();
                }
                t.b();
                A.n1(context0, "com.kurogame.kjq", "", "", Build.VERSION.SDK_INT);
            }
            StubApplication.mRealApplication = g.a("com.kurogame.kjq.app.AppApplication");
            super.attachBaseContext(context0);
            Application application0 = StubApplication.mRealApplication;
            if(application0 != null) {
                g.a(context0, application0);
                if(this.mActivityCallbacks != null && !this.mActivityCallbacks.isEmpty()) {
                    for(Object object0: this.mActivityCallbacks) {
                        StubApplication.mRealApplication.registerActivityLifecycleCallbacks(((Application.ActivityLifecycleCallbacks)object0));
                    }
                }
            }
            t.c();
        }
        f.a().attachBaseContext(context0, this, StubApplication.mRealApplication);
    }
    private static void lllIIl() {
        String[] arr_s = new String[4];
        StubApplication.lllIIl = arr_s;
        arr_s[0] = StubApplication.IllIIl("ADosbQ8MNygvB000MTMLByZvKhEMOSA3Bwc=", "cUACb");
        StubApplication.lllIIl[1] = StubApplication.IllIIl("EQ0UAh8ZB14RAABNMRMEGRUZBAkkCwIVERQ=", "pcppp");
        StubApplication.lllIIl[2] = StubApplication.IllIIl("ACIJLTkFICorNx8lHjwq", "iLzYX");
        StubApplication.lllIIl[3] = StubApplication.IllIIl("", "jdjZq");
    }
    @Override  // android.app.Application
    public void onCreate() {
        if(StubApplication.skipLoad) {
            super.onCreate();
        }
        else {
            t.a(this);
            super.onCreate();
            t.a(this);
            if(StubApplication.mRealApplication != null) {
                t.b(this);
                StubApplication.mRealApplication.onCreate();
            }
            A.n2(this);
        }
        f.a().onCreate(this, StubApplication.mRealApplication);
    }
public class StubApplication extends Application {
    private static final String[] lllIIl;
    private ArrayList mActivityCallbacks;
    public static Context mContext;
    public static int mInstallProviderCounter;
    public static Application mRealApplication;
    public static boolean mRealApplicationPatched;
    public static boolean skipLoad;
    static {
        StubApplication.lllIIl();
        StubApplication.mContext = null;	//壳保留的全局 Context
        StubApplication.mRealApplication = null;	//真实业务 Application 实例
        StubApplication.mRealApplicationPatched = false;	//是否已经把系统内部引用切到真实 App
        StubApplication.mInstallProviderCounter = 0;	//Provider 安装阶段的计数器
        StubApplication.skipLoad = false;	//是否跳过壳逻辑
        f.a().init();	//初始化壳的扩展模块
    }

这个方法的用途就是把敏感字符串藏起来,避免明文出现在 dex 里。 这个函数本身不重要,它解出来的字符串才重要。

    private static String IllIIl(String s, String s1) {	//先做base64,再按key逐字节xor
        byte[] arr_b = a.a(s);
        StringBuilder stringBuilder0 = new StringBuilder();
        char[] arr_c = s1.toCharArray();
        int v1 = 0;
        for(int v = 0; v < arr_b.length; ++v) {
            stringBuilder0.append(((char)(arr_b[v] ^ arr_c[v1 % arr_c.length])));
            ++v1;
        }
        return stringBuilder0.toString();
    }

简单写个解密即可看到到底是什么。

import base64
def decode(s, key):
    data = base64.b64decode(s)
    key = key.encode()
    return ''.join(
        chr(data[i] ^ key[i % len(key)])
        for i in range(len(data))
    )
arr = [
    ("ADosbQ8MNygvB000MTMLByZvKhEMOSA3Bwc=", "cUACb"),
    ("EQ0UAh8ZB14RAABNMRMEGRUZBAkkCwIVERQ=", "pcppp"),
    ("ACIJLTkFICorNx8lHjwq", "iLzYX"),
    ("", "jdjZq"),
    ("QBsaOSI=", "nwuZI"),
    ("ShUJIgBKFQkiAEo=", "eqhVa"),
    ("QA4SATlADhIBOUA=", "ojsuX"),
    ("MyQRJzs7Lls0JCJkNDYgOzwcIS0GIgcwNTY=", "RJuUT"),
    ("EjQQOQgfNTI5AhIkETgjECwH", "qAbKm"),
    ("JTcfKCooNj07LC0jCj8BJy8I", "FBmZO"),
    ("", "wmRSO"),
    ("WSAYAihZIw8BLVk9Cx04", "vPjmK"),
    ("bQcTPyYnGUUuOyxbBiU8KREYemY=", "BtjLR"),
    ("bAkpHj4mF38PIy1VPAQkKB8i", "CzPmJ"),
    ("EWtf", "iSiSJ"),
    ("QmQUIDwBIxc=", "mJuRQ"),
    ("PgYv", "RoMUM"),
    ("Ew==", "LiAmI"),
    ("NgQE", "Zmfkx"),
    ("TTkM", "cJcoV"),
    ("AgUH", "nleis"),
    ("Qz8r", "mLDft"),
    ("FTEJ", "yXkOm"),
    ("LChCcgZFZA==", "sPzDY"),
    ("Dz8z", "cVQSz"),
    ("HjJXWw==", "AJomu"),
    ("fVkrUXw+HjE=", "RwSiJ"),
    ("EC0VMxwx", "XXtDy"),
    ("LAcZBR8=", "dHWJM"),
    ("RjwONiYMKzknJgwpFQ==", "iOfWT"),
    ("YTQvCQ==", "OLBeT"),
    ("", "QAdCY"),
    ("NT0WKDA9FQ==", "XqyIT"),
    ("OxksLBAlGSAMPyQdASc1PBMp", "WvMHV"),
    ("GCAEBhUGIAgmOgck", "tOebS"),
    ("FzMmETATOjMnMBQbNyE9HzI=", "pVRUU"),
    ("HhUjADMRWjwPKQ4RIlgMNyY6GC4TGSo=", "ztOvZ"),
    ("JA8OATItHhM+Ig==", "CjzSG"),
    ("JCQQOTAzJQEfGCcoIQk8OjEQGDY5Mg==", "WAdqY"),
    ("Ag==", "NSSgw")

]
for s, k in arr:
    print(decode(s, k))
输出
'''
com.mobile.appids.isolated
android.app.ActivityThread
installProvider

.lock
/data/data/
/data/data/
android.app.ActivityThread
currentProcessName
currentPackageName

/proc/self/maps
/system/bin/linker64
/system/bin/linker
x86
/.armlib
lib
_
lib
.so
lib
.so
lib
_x86_64
lib
_x86
/.x86lib
Huawei
HONOR
/shared_prefs
.xml

mLoaded
loadFromDiskLocked
loadFromDisk
getDeclaredMethod
dalvik.system.VMRuntime
getRuntime
setHiddenApiExemptions
L
'''

AppInfo.FLAGS为0,如果这个值被修改了那么就代表出现异常,后面的t.a()看下面分析。

@Override  // android.content.ContextWrapper
    protected void attachBaseContext(Context context0) {
        if((AppInfo.FLAGS & 0x100000) != 0 && t.a().contains(StubApplication.lllIIl[0])) {	//lllIIl[0]是com.mobile.appids.isolated
            StubApplication.skipLoad = true;
        }
public class AppInfo {
    public static String APKPATH = "";
    public static String APPNAME = "com.kurogame.kjq.app.AppApplication";
    public static String DATAPATH = "";
    public static int FLAGS = 0;
    public static String LIBNAME = "baiduprotect";
    public static String PKGNAME = "com.kurogame.kjq";
    public static String SUPPORT_ARCH = "arm64-v8a:armeabi-v7a";
    public static String h = "sagittarius";

}
public final class t {
    private static Context a;
    private static Application b;
    private static final String[] c;
    static {
        t.g();
        t.b = null;
        t.a = null;
    }
    public static String a() {
        String s;
        Class class0;
        try {
            class0 = Class.forName(t.c[3]);	//这里的t.c[3]的解密和前面是一致的,所以套用脚本即可,解出来是android.app.ActivityThread
            if(Build.VERSION.SDK_INT >= 18) {	//SDK版本大于等于18
                s = t.c[4];	//取值currentProcessName
                return (String)class0.getDeclaredMethod(s).invoke(null);	//return ActivityThread.currentProcessName();
            }
            else {
                goto label_4;
            }
            return (String)class0.getDeclaredMethod(s).invoke(null);
        }
        catch(Exception unused_ex) {
            return t.c[6];	//这里为空
        }
        s = t.c[4];
        return (String)class0.getDeclaredMethod(s).invoke(null);
    label_4:
        s = t.c[5];	//取值currentPackageName
        try {
            return (String)class0.getDeclaredMethod(s).invoke(null);	//return ActivityThread.currentPackageName();
        }
        catch(Exception unused_ex) {
            return t.c[6];	//取值为空
        }
    }

这里大概得意思就是根据SDK的版本来获取对应的API,最终如下

t.a().contains(StubApplication.lllIIl[0])
大概就是
ActivityThread.currentProcessName().contains("isolated")
ActivityThread.currentPackageName().contains("isolated")

所以如果被标记为skipLoad着不会走加壳的链路。

        if(StubApplication.skipLoad) {	//跳过了加壳逻辑,则只执行系统默认初始化
            super.attachBaseContext(context0);
        }
        else {
            t.a(this, context0);	//保存壳运行环境
            StubApplication.mContext = context0;	//初始化数据
            AppInfo.PKGNAME = "com.kurogame.kjq";	//初始化包名
            AppInfo.APKPATH = context0.getApplicationInfo().sourceDir;	//初始化APK真实路径
            AppInfo.DATAPATH = t.a(context0.getApplicationInfo().dataDir);	//初始化APK数据
            if(Debug.isDebuggerConnected()) {	//java层反调试
                Process.killProcess(Process.myPid());
            }
            else {
                if(Build.VERSION.SDK_INT > 27) {	//SDK>17额外操作
                    r.a();	//处理高版本安卓的特性
                }
                t.b();	//加载baiduprotect.so
                A.n1(context0, "com.kurogame.kjq", "", "", Build.VERSION.SDK_INT);	//native实现
            }
            StubApplication.mRealApplication = g.a("com.kurogame.kjq.app.AppApplication");	//说明com.kurogame.kjq.app.AppApplication才是真正的入口
            super.attachBaseContext(context0);	//先把壳Application自己attach完
            Application application0 = StubApplication.mRealApplication;	//取出真实Application
            if(application0 != null) {
                g.a(context0, application0);	//再开始把真实Application接进去
                if(this.mActivityCallbacks != null && !this.mActivityCallbacks.isEmpty()) {	//补发生命周期回调
                    for(Object object0: this.mActivityCallbacks) {
                        StubApplication.mRealApplication.registerActivityLifecycleCallbacks(((Application.ActivityLifecycleCallbacks)object0));
                    }
                }
            }
            t.c();	//是运行兼容修补,针对不同厂商
        }
        f.a().attachBaseContext(context0, this, StubApplication.mRealApplication);	//分发给壳的插件层,通知其在attachBaseContext 阶段继续处理壳App和真实App
    }
public static void a(Application application0, Context context0) {
        t.b = application0;
        t.a = context0;
    }

把全局变量保存下来一遍后面读取assetshared_prefspatch真实Application来使用。

public static String a(String s) {
        if(Build.VERSION.SDK_INT < 17) {
            return t.c[1] + "com.kurogame.kjq";	//c[1]:data/data
        }

        try {
            new File(s).listFiles();
            return s;
        }
        catch(Exception unused_ex) {
            return t.c[2] + "com.kurogame.kjq";	//c[2]:data/data
        }
    }

Android < 17:直接返回/data/data/com.kurogame.kjq

Android >= 17:优先用系统给的dataDir

如果这个 dataDir 访问异常,再回退/data/data/com.kurogame.kjq

public static void a() {
        r.a(new String[]{r.c[4]});	//r.c[4]的字符串加密规则同上,解出来是"L"
    }
private static boolean a(String[] arr_s) {
        Object object0 = r.b;
        if(object0 != null) {
            Method method0 = r.a;
            if(method0 != null) {
                try {
                    method0.invoke(object0, arr_s);
                    return true;
                }
                catch(Exception unused_ex) {
                    return false;
                }
            }
        }
        return false;
    }

实际上就是执行了一下命令

 VMRuntime.getRuntime().setHiddenApiExemptions(new String[]{"L"});

这里的"L"是签名/描述符前缀。

Ljava/lang/String;

Landroid/app/ActivityThread;

Ldalvik/system/BaseDexClassLoader;

这是高版本Android的特性,反射调用隐藏API

public static void b() {
        if(Build.VERSION.SDK_INT < 30 && (AppInfo.SUPPORT_ARCH == null || !"arm64-v8a:armeabi-v7a".contains(t.c[10])) && t.e()) {	//t.c[10]:x86	
            String s = t.c[16] + "baiduprotect" + t.c[17];	//t.c[16]:lib  t.c[17]:.so 实际上s = libbaiduprotect.so
            String s1 = t.d() ? t.c[18] + "baiduprotect" + t.c[19] : t.c[20] + "baiduprotect" + t.c[21];	//实际上s2 = libbaiduprotect_x86_64/libbaiduprotect_x86
            String s2 = "" + t.c[22] + File.separator + s;	//实际上s2 = DATAPATH + "/.x86lib/" + s
            t.a(s1, s2);	//把 assets 里的文件释放到磁盘
            System.load(s2);
            return;
        }
        if((AppInfo.FLAGS & 0x10000) != 0) {
            t.f();
            return;
        }
        System.loadLibrary("baiduprotect");	//正常arm环境就加载baiduprotect.so
    }
private static boolean e() {
        try {
            byte[] arr_b = new byte[20];
            FileInputStream fileInputStream0 = new FileInputStream(t.c[9]);	//t.c[9]:/system/bin/linker
            fileInputStream0.read(arr_b, 0, 20);
            if(arr_b[18] == 3) {	//读系统linker的ELF头,arr_b[18]对应ELF头里的e_machine低字节:3 == EM_386,也就是x86
                fileInputStream0.close();
                return true;
            }
            fileInputStream0.close();
        }
        catch(Exception unused_ex) {
        }
        return false;
    }

那么也就是说如果SDK版本地狱30,APK声明自己不支持x86,但运行环境偏偏是 x86,那么不要直接loadLibrary("baiduprotect"),而是走一条x86兼容加载分支。

private static boolean d() {
        try {
            BufferedReader bufferedReader0 = new BufferedReader(new InputStreamReader(new FileInputStream(t.c[7])));	//t.c[7]:/proc/self/maps
            for(String s = bufferedReader0.readLine(); s != null; s = bufferedReader0.readLine()) {
                if(s.endsWith(t.c[8])) {	//t.c[8]:/system/bin/linker64
                    bufferedReader0.close();
                    return true;
                }
            }
        }
        catch(Exception unused_ex) {
        }

        return false;
    }

这个是读当前进程的内存映射表,查找有无linker64,那么和前面完美闭环,前面是看是否是x86,后面是看是否是x86_64。大概的意思就是:

if (isx86Runtime()) {
      if (is64BitLinker()) {
          // 用 x86_64 那份壳 so
      } else {
          // 用 x86 那份壳 so
      }
  }
private static boolean a(String s, String s1) {
        boolean z1;
        FileOutputStream fileOutputStream1;
        FileOutputStream fileOutputStream0;
        InputStream inputStream0;
        AssetManager assetManager0;
        boolean z;
        long v;
        FileLock fileLock0;
        File file0 = new File(s1);
        File file1 = file0.getParentFile();
        if(!file1.exists()) {
            file1.mkdirs();
        }
        try {
            fileLock0 = new FileOutputStream(new File(s1 + t.c[0])).getChannel().lock();
            v = new File("").lastModified();
            if(!file0.exists()) {
                goto label_13;
            }
            else if(file0.lastModified() != v) {
                file0.delete();
                goto label_13;
            }
            else {
                z = true;
            }
            goto label_55;
        }
        catch(Exception unused_ex) {
            return false;
        }
        try {
        label_13:
            assetManager0 = t.a.getAssets();
        }
        catch(Exception unused_ex) {
            return false;
        }
        try {
            inputStream0 = assetManager0.open(s);
        }
        catch(Exception unused_ex) {
            fileOutputStream0 = null;
            inputStream0 = null;
            goto label_37;
        }
        catch(Throwable throwable0) {
            fileOutputStream0 = null;
            inputStream0 = null;
            goto label_42;
        }
        try {
            fileOutputStream0 = null;
            fileOutputStream0 = new FileOutputStream(s1);
            byte[] arr_b = new byte[0x400];
            int v1;
            while((v1 = inputStream0.read(arr_b)) > 0) {
                fileOutputStream0.write(arr_b, 0, v1);
            }

            fileOutputStream0.flush();
            File file2 = new File(s1);
            if(file2.exists()) {
                file2.setLastModified(v);
            }
            goto label_46;
        }
        catch(Exception unused_ex) {
        }
        catch(Throwable throwable0) {
            goto label_42;
        }
        try {
        label_37:
            t.a(inputStream0);
            fileOutputStream1 = fileOutputStream0;
            z1 = false;
            goto label_51;
        label_42:
            t.a(inputStream0);
            t.a(fileOutputStream0);
            throw throwable0;
        }
        catch(Exception unused_ex) {
            return false;
        }
        try {
        label_46:
            t.a(inputStream0);
            fileOutputStream1 = fileOutputStream0;
            z1 = true;
        }
        catch(Exception unused_ex) {
            return true;
        }
        try {
        label_51:
            t.a(fileOutputStream1);
            z = z1;
        }
        catch(Exception unused_ex) {
            return z1;
        }
        try {
        label_55:
            fileLock0.release();
        }
        catch(Exception unused_ex) {
        }
        return z;
    }

简单来说这就是一个为了给x86x86_64而外准备的一套so加载逻辑,他是从assets下加载的,同时我们在assets也能看到。

private static void f() {
        String[] arr_s = Build.VERSION.SDK_INT < 21 ? new String[]{Build.CPU_ABI, Build.CPU_ABI2} : Build.SUPPORTED_ABIS;
        String s = "" + t.c[11];	//t.c[11]:/.armlib
        for(int v = 0; v < arr_s.length; ++v) {
            String s1 = arr_s[v];
            String s2 = s + File.separator + (t.c[14] + "baiduprotect" + t.c[15]);
            if(t.a((t.c[12] + "baiduprotect" + t.c[13] + s1), s2)) {
                try {
                    System.load(s2);
                    return;
                }
                catch(Exception unused_ex) {
                }
            }
        }
    }

按设备ABIasset释放ARM版壳 so 再加载。

public static Application a(String s) {
        if(s != null && s.length() > 0) {
            try {
                return (Application)Class.forName(s).getConstructor().newInstance();
            }
            catch(Throwable throwable0) {
                throw new IllegalStateException(throwable0);
            }
        }

        return null;
    }

根据真实Application类名反射创建实例,供后续attachBaseContext和系统接管使用。

public static void a(Context context0, Application application0, Application application1) {   
    // context0:当前基础 Context
    // application0:壳 StubApplication
    // application1:真实 AppApplication
        Field field3;
        Class class1;
        Class class0;
        try {
            class0 = Class.forName(g.a[5]);	//g.a[5]:android.app.ActivityThread,即class0 = android.app.ActivityThread
        }
        catch(Throwable unused_ex) {
            return;
        }
        try {
            Object object0 = g.a(context0, class0);
            Field field0 = class0.getDeclaredField(g.a[6]);	//g.a[6]:mInitialApplication
            field0.setAccessible(true);
            Application application2 = (Application)field0.get(object0);
            if(!Build.BRAND.equals(g.a[7]) && application1 != null && application2 == application0) {	//g.a[7]:samsung
                field0.set(object0, application1);	//如果当前 mInitialApplication 还指向壳 App,就把它改成真实App
            }
            if(application1 != null) {	//系统维护了一份当前进程里所有Application的列表,如果列表里还是壳App,就替换成真实App
                Field field1 = class0.getDeclaredField(g.a[8]);	//g.a[8]:mAllApplications
                field1.setAccessible(true);
                List list0 = (List)field1.get(object0);
                int v1 = 0;
                while(true) {
                    if(v1 >= list0.size()) {
                        goto label_20;
                    }
                    if(list0.get(v1) == application0) {
                        list0.set(v1, application1);
                    }
                    ++v1;
                }
            }
            goto label_23;
        }
        catch(Throwable unused_ex) {
            return;
        }
        try {
        label_20:	//把ContextImpl的outerContext指向真实App
            Method method0 = StubApplication.mContext.getClass().getDeclaredMethod(g.a[9], Context.class);	//g.a[9]:setOuterContext
            method0.setAccessible(true);
            method0.invoke(StubApplication.mContext, application1);
        }
        catch(Throwable unused_ex) {
        }
        try {
            try {
            label_23:
                class1 = Class.forName(g.a[10]);	//g.a[10]:android.app.LoadedApk
            }
            catch(ClassNotFoundException unused_ex) {
                class1 = Class.forName(g.a[11]);	//g.a[11]:android.app.ActivityThread$PackageInfo
            }

            Field field2 = class1.getDeclaredField(g.a[12]);	//g.a[12]:mApplication
            field2.setAccessible(true);
            class1.getDeclaredField(g.a[13]).setAccessible(true);	//g.a[13]:mResDir
            try {
                field3 = null;
                field3 = Application.class.getDeclaredField(g.a[14]);	//g.a[14]:mLoadedApk
            }
            catch(NoSuchFieldException unused_ex) {
            }
            for(int v = 0; v < 2; ++v) {
                Field field4 = class0.getDeclaredField(new String[]{g.a[15], g.a[16]}[v]);	//g.a[15]:mPackages	g.a[16]:mResourcePackages
                field4.setAccessible(true);
                for(Object object1: ((Map)field4.get(object0)).entrySet()) {
                    Object object2 = ((WeakReference)((Map.Entry)object1).getValue()).get();
                    if(object2 != null && field2.get(object2) == application0) {
                        if(application1 != null) {
                            field2.set(object2, application1);	//替换LoadedApk.mApplication
                        }

                        if(application1 != null && field3 != null) {
                            field3.set(application1, object2);	//回填realApp.mLoadedApk
                        }
                    }
                }
            }
        }
        catch(Throwable unused_ex) {
        }
    }

总的来说就是将Android框架内部仍指向壳Application的关键引用批量切换到真实Application:包括 ActivityThread.mInitialApplicationmAllApplicationsContextImpl.outerContextLoadedApk.mApplication,以及mPackages/mResourcePackages中的LoadedApk

public void onCreate() {
        if(StubApplication.skipLoad) {	//如果前面被判定为是不走壳的链路,那么直接执行壳的onCreate()
            super.onCreate();
        }
        else {
            t.a(this);	//如果还没完成切换,这里再补一次“壳 Application -> 真实Application”的转变
            super.onCreate();	//壳Application自己完成系统层onCreate
            t.a(this);	//若尚未完成切换,则只执行一次壳 App -> 真实App的系统引用替换
            if(StubApplication.mRealApplication != null) {	/// 如果前面已经成功实例化出真实Application
                t.b(this);	//执行厂商ROM相关的补充修正,把特定环境中残留的壳App引用再补换成真实App
                StubApplication.mRealApplication.onCreate();	//真正把执行权交给业务层Application,真实业务初始化从这里开始
            }
            A.n2(this);	//native实现
        }
        f.a().onCreate(this, StubApplication.mRealApplication);	//分发给壳的插件层,通知其在attachBaseContext 阶段继续处理壳App和真实App
    }
    }
 public static void a(Application application0) {
        if(!StubApplication.mRealApplicationPatched && StubApplication.mRealApplication != null) {
            StubApplication.mRealApplicationPatched = true;
            g.a(t.a, application0, StubApplication.mRealApplication);
        }
    }
 public static void b(Application application0) {
        if(StubApplication.mRealApplication != null) {
            g.b(t.a, application0, StubApplication.mRealApplication);
        }
    }

至此java层分析完毕,在native层实现的就是A.n2()A.n1()了。

Native层分析

进来只有三个导出函数

其中JNI_OnLoad还被高度混淆了,看一下字符串有什么有用的信息,也是啥也没有,再去看看.init_array

一个一个来看

unsigned __int64 sub_4FE58()
{
  unsigned __int64 result; // x0
  char *end; // [xsp+8h] [xbp-18h]
  char *start; // [xsp+10h] [xbp-10h]

  if ( qword_78008 == 0x87654321LL )
  {
    qword_78008 = &qword_78008;
    qword_78010 = 0;
  }
  start = &qword_78008 - qword_78008;           // start = 0x78008 - 0x1A0A8 = 0x5DF60
  end = &qword_78008 + qword_78010 - qword_78008;// end = start + 0x4F48
  mprotect_1(&qword_78008 - qword_78008, qword_78010, 7u);// 修改内存权限为可RWX
  xor(start, qword_78010);                      // 对每个字节做 XOR,8 字节循环一次
  mprotect_1(start, qword_78010, 5u);           // 把权限改为RW
  result = sub_4C6B4(start, end);
  qword_78008 = 0;
  qword_78010 = 0;
  return result;
}

写一个idapython脚本还原

import ida_bytes
key = [0x67, 0x69, 0xBD, 0xE7, 0x11, 0xD1, 0x54, 0x3C]
delta = ida_bytes.get_qword(0x78008)
size = ida_bytes.get_qword(0x78010)
start = 0x78008 - delta
for i in range(size):
    b = ida_bytes.get_byte(start + i)
    ida_bytes.patch_byte(start + i, b ^ key[i & 7])
unsigned __int64 __fastcall xor8(unsigned __int64 n7_1, unsigned __int64 a2)
{
  unsigned __int8 n7_2; // [xsp+7h] [xbp-29h]
  unsigned __int64 i; // [xsp+8h] [xbp-28h]
  _BYTE *v4; // [xsp+10h] [xbp-20h]
  unsigned __int64 n7; // [xsp+20h] [xbp-10h]

  n7 = n7_1;
  for ( i = 0; i < a2; ++i )
  {
    v4 = (n7 + i);
    n7_2 = i & 7;
    n7_1 = i & 7;
    if ( (i & 7) != 0 )
    {
      n7_1 = n7_2;
      if ( n7_2 == 1 )
      {
        *v4 ^= 0x69u;
      }
      else
      {
        n7_1 = n7_2;
        if ( n7_2 == 2 )
        {
          *v4 ^= 0xBDu;
        }
        else
        {
          n7_1 = n7_2;
          if ( n7_2 == 3 )
          {
            *v4 ^= 0xE7u;
          }
          else
          {
            n7_1 = n7_2;
            if ( n7_2 == 4 )
            {
              *v4 ^= 0x11u;
            }
            else
            {
              n7_1 = n7_2;
              if ( n7_2 == 5 )
              {
                *v4 ^= 0xD1u;
              }
              else
              {
                n7_1 = n7_2;
                if ( n7_2 == 6 )
                {
                  *v4 ^= 0x54u;
                }
                else
                {
                  n7_1 = n7_2;
                  if ( n7_2 == 7 )
                    *v4 ^= 0x3Cu;
                }
              }
            }
          }
        }
      }
    }
    else
    {
      *v4 ^= 0x67u;
    }
  }
  return n7_1;
}

还原出很多有用的字符串

第二层还原

// Raw init stage 2: flattened self-modifying routine that restores transformed text region; later JNI_OnLoad lies inside restored range
int *init_transform_text()
{
  _DWORD *v0; // x28
  int **v1; // x7
  int **v2; // x30
  _DWORD *v3; // x23
  _DWORD *v4; // x24
  _DWORD *v5; // x7
  _DWORD *v6; // x24
  int *v7; // x10
  int *v8; // x6
  int *v9; // x11
  _DWORD *v10; // x12
  int *v11; // x4
  int *v12; // x21
  int *v13; // x22
  int *v14; // x26
  int *v15; // x23
  _DWORD *v16; // x25
  int *v17; // x7
  int *v18; // x19
  int *result; // x0
  _DWORD *v20; // x14
  _QWORD *v21; // x2
  _DWORD *v22; // x5
  _DWORD *v23; // x24
  _DWORD *v24; // x3
  int *v25; // x16
  _DWORD *v26; // x17
  _BYTE *v27; // x27
  int **v28; // x30
  _DWORD *v29; // x13
  int *v30; // x20
  int **v31; // x15
  int *v32; // x18
  _DWORD *v33; // x1
  int **v34; // x11
  int **v35; // x15
  _DWORD *v36; // x30
  int **v37; // x3
  _DWORD *v38; // x12
  _DWORD *v39; // x18
  int *v40; // x23
  int *v41; // x16
  _DWORD *v42; // x17
  int **v43; // x15
  int **v44; // x20
  _DWORD *v45; // x19
  _DWORD *v46; // x14
  int *v47; // x26
  int *v48; // x25
  int *v49; // x22
  int *v50; // x21
  int *v51; // x28
  _DWORD *v52; // x27
  int *v53; // x14
  _DWORD *v54; // x27
  int **v55; // x26
  int **v56; // x15
  __int64 *v57; // x3
  _DWORD *v58; // x12
  _DWORD *v59; // x14
  _DWORD *v60; // x25
  int **v61; // x30
  _DWORD *v62; // x27
  _DWORD *v63; // x13
  int *v64; // x26
  int *v65; // x24
  int *v66; // x23
  _DWORD *v67; // x26
  int *v68; // x19
  __int64 *v69; // x25
  int *v70; // x22
  int *v71; // x21
  int *v72; // x26
  int *v73; // x23
  _DWORD *v74; // x27
  __int64 v75; // x0
  int **v76; // x11
  int *v77; // x27
  int **v78; // x21
  _DWORD *v79; // x25
  _DWORD *v80; // x26
  int v81; // w0
  int **v82; // x23
  int **v83; // x26
  int **v84; // x30
  int *v85; // x20
  _DWORD *v86; // x17
  int **v87; // x13
  int **v88; // x14
  int **v89; // x15
  _QWORD *v90; // x16
  int **v91; // x21
  _QWORD *v92; // x13
  int *v93; // x23
  int **v94; // x8
  _DWORD *v95; // x27
  int **v96; // x19
  _QWORD *v97; // x25
  int **v98; // [xsp+0h] [xbp-210h] BYREF
  int *v99; // [xsp+8h] [xbp-208h]
  int **v100; // [xsp+10h] [xbp-200h]
  int **v101; // [xsp+18h] [xbp-1F8h]
  int *v102; // [xsp+20h] [xbp-1F0h]
  int *v103; // [xsp+28h] [xbp-1E8h]
  int **v104; // [xsp+30h] [xbp-1E0h]
  int **v105; // [xsp+38h] [xbp-1D8h]
  int **v106; // [xsp+40h] [xbp-1D0h]
  int **v107; // [xsp+48h] [xbp-1C8h]
  int *v108; // [xsp+50h] [xbp-1C0h]
  int *v109; // [xsp+58h] [xbp-1B8h]
  int ***v110; // [xsp+60h] [xbp-1B0h]
  int *v111; // [xsp+68h] [xbp-1A8h]
  _DWORD *v112; // [xsp+70h] [xbp-1A0h]
  int ***v113; // [xsp+78h] [xbp-198h]
  int *v114; // [xsp+80h] [xbp-190h]
  int *v115; // [xsp+88h] [xbp-188h]
  int *v116; // [xsp+90h] [xbp-180h]
  int *v117; // [xsp+98h] [xbp-178h]
  _DWORD *v118; // [xsp+A0h] [xbp-170h]
  int **v119; // [xsp+A8h] [xbp-168h]
  int **v120; // [xsp+B0h] [xbp-160h]
  int ***v121; // [xsp+B8h] [xbp-158h]
  int **v122; // [xsp+C0h] [xbp-150h]
  int ***v123; // [xsp+C8h] [xbp-148h]
  int **v124; // [xsp+D0h] [xbp-140h]
  int **v125; // [xsp+D8h] [xbp-138h]
  int *v126; // [xsp+E0h] [xbp-130h]
  int *v127; // [xsp+E8h] [xbp-128h]
  int **v128; // [xsp+F0h] [xbp-120h]
  int **v129; // [xsp+F8h] [xbp-118h]
  int **v130; // [xsp+100h] [xbp-110h]
  int *v131; // [xsp+108h] [xbp-108h]
  int *v132; // [xsp+110h] [xbp-100h]
  _DWORD *v133; // [xsp+118h] [xbp-F8h]
  int *v134; // [xsp+120h] [xbp-F0h]
  _DWORD *v135; // [xsp+128h] [xbp-E8h]
  int *v136; // [xsp+130h] [xbp-E0h]
  int **v137; // [xsp+138h] [xbp-D8h]
  int **v138; // [xsp+140h] [xbp-D0h]
  _DWORD *v139; // [xsp+148h] [xbp-C8h]
  int ***v140; // [xsp+150h] [xbp-C0h]
  int *v141; // [xsp+158h] [xbp-B8h]
  int **v142; // [xsp+160h] [xbp-B0h]
  int **v143; // [xsp+168h] [xbp-A8h]
  int *v144; // [xsp+170h] [xbp-A0h]
  int **v145; // [xsp+178h] [xbp-98h]
  int **v146; // [xsp+180h] [xbp-90h]
  int *v147; // [xsp+188h] [xbp-88h]
  int *v148; // [xsp+190h] [xbp-80h]
  int **v149; // [xsp+198h] [xbp-78h]
  int **v150; // [xsp+1A0h] [xbp-70h]
  int *v151; // [xsp+1A8h] [xbp-68h]
  int **v152; // [xsp+1B8h] [xbp-58h]

  v122 = (&v98 - 2);
  v129 = (&v98 - 2);
  v143 = (&v98 - 2);
  v120 = (&v98 - 2);
  v145 = (&v98 - 2);
  v119 = (&v98 - 2);
  v146 = (&v98 - 2);
  v138 = (&v98 - 2);
  v128 = (&v98 - 2);
  v124 = (&v98 - 2);
  v107 = (&v98 - 2);
  v106 = (&v98 - 2);
  v105 = (&v98 - 2);
  v104 = (&v98 - 2);
  v139 = &v98 - 2;
  v130 = (&v98 - 2);
  v150 = (&v98 - 2);
  v137 = (&v98 - 2);
  v126 = (&v98 - 2);
  v112 = &v98 - 2;
  v132 = (&v98 - 2);
  v116 = (&v98 - 2);
  v147 = (&v98 - 2);
  v118 = &v98 - 2;
  v141 = (&v98 - 2);
  v125 = (&v98 - 2);
  v111 = (&v98 - 2);
  v148 = (&v98 - 2);
  v115 = (&v98 - 2);
  v151 = (&v98 - 2);
  v117 = (&v98 - 2);
  v0 = &v98 - 2;
  v142 = (&v98 - 2);
  v152 = (&v98 - 2);
  *(&v98 - 2) = 0;
  *v129 = 0;
  *v143 = 0;
  *(&v98 - 2) = 0;
  *(&v98 - 2) = 0;
  *(&v98 - 2) = 0;
  *(&v98 - 16) = 0;
  v1 = v128;
  *v138 = 0;
  *v1 = 0;
  *v124 = 0;
  *v107 = 0;
  v2 = v152;
  *v106 = 0;
  *v105 = 0;
  *v104 = 0;
  v3 = v137;
  v4 = v150;
  v5 = v130;
  *v139 = 7;
  *v5 = -1767537072;
  *v4 = 1668330147;
  v6 = v125;
  *v3 = 1139357055;
  *v126 = 22;
  *v112 = 4;
  *v132 = 888793072;
  *v116 = 1951630882;
  *(&v98 - 4) = 5;
  *v118 = 13;
  *v141 = 17;
  *v6 = 1654894664;
  *v111 = 341302425;
  *(&v98 - 4) = 4;
  *v148 = -1517093216;
  *v115 = -1224375452;
  *v151 = 25;
  v7 = v117;
  *v117 = -1138722582;
  *(&v98 - 4) = 13;
  v149 = (&v98 - 2);
  v98 = v2;
  v100 = (&v98 - 2);
  v101 = (&v98 - 2);
  v102 = &v98;
  v140 = &v98;
  v110 = &v98;
  v144 = &v98;
  v113 = &v98;
  v131 = &v98;
  v123 = &v98;
  v127 = &v98;
  v109 = &v98;
  v8 = &v98;
  v9 = v7;
  v10 = v118;
  v11 = v116;
  v12 = v111;
  v13 = v115;
  v14 = &v98;
  v136 = &v98;
  v108 = &v98;
  v15 = v141;
  v16 = &v98;
  v121 = &v98;
  v135 = &v98;
  v17 = &v98;
  v103 = &v98;
  v18 = &v98;
  v99 = &v98;
  v134 = &v98;
  v114 = &v98;
  v133 = &v98;
  result = &v98;
  v20 = &v98;
  v21 = v122;
  v22 = &v98;
  v23 = &v98;
  v24 = &v98;
  v25 = &v98;
  v26 = v125;
  v28 = v145;
  v27 = v146;
  v29 = &v98 - 2;
  v30 = v150;
  v31 = v138;
  while ( 2 )
  {
    v32 = v9;
    v33 = v10;
    switch ( *v0 )
    {
      case 1:
        *v13 = (*v132 >> 4) + (*v11 >> 8) * ((*v147 >> 4) + *v20);
        *v0 = 24;
        v24 = v140;
        v16 = v121;
        v29 = v149;
        v23 = v110;
        goto LABEL_48;
      case 2:
        *v17 = *v18 - *v17 * (*v132 >> 4) - *v9 * (*v13 - *v144);
        *v0 = 28;
        v29 = v149;
        v30 = v150;
        v14 = v136;
        v28 = v145;
        v27 = v146;
        goto LABEL_48;
      case 3:
        *v23 = *v29 * (*v10 + (*v14 >> 4)) - *v11 * (*v133 - 16 * *v23);
        *v0 = 27;
        v30 = v150;
        v28 = v145;
        v27 = v146;
        goto LABEL_48;
      case 4:
        v34 = v31;
        v35 = v120;
        v36 = v139;
        v37 = v143;
        *v134 = *v25 + *v132 * (*v15 >> 4) - (*result >> 4) * *v8;
        *v35 = (*v34 - *v37);
        *v36 = 0;
        *v0 = 18;
        v20 = v123;
        v24 = v140;
        v29 = v149;
        v30 = v150;
        v31 = v34;
        v14 = v136;
        v28 = v145;
        v27 = v146;
        goto LABEL_48;
      case 5:
        *v9 = *v147 - *v13 * (*v17 - (*v148 >> 4)) + *v29 * (*v131 - *v22);
        *v0 = 25;
        v24 = v140;
        v16 = v121;
        v14 = v136;
        v28 = v145;
        v27 = v146;
        goto LABEL_48;
      case 6:
        v22 = v113;
        if ( *v126 < *v112 )
          *v0 = 17;
        else
          *v0 = 29;
        goto LABEL_48;
      case 7:
        v38 = v135;
        *v33 = *v151 * (*result - *v114) + (*v135 + *v17) * ((*v17 >> 4) + *v16);
        *v0 = 4;
        v24 = v140;
        v135 = v38;
        v31 = v138;
        v28 = v145;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 8:
        v39 = v26;
        v40 = v25;
        v41 = v127;
        v42 = v137;
        *v18 = *v22 + (*v127 >> 8) - *v13 + *v132 * (*v18 - *v148);
        *v21 = &qword_78018 - qword_78018;      // 恢复待处理 .text 区间起始地址:start = &qword_78018 - qword_78018 = 0x7CC8
        *v42 = 4;
        *v0 = 30;
        v137 = v42;
        v127 = v41;
        v25 = v40;
        v26 = v39;
        v32 = v9;
        v8 = v108;
        v24 = v140;
        v15 = v141;
        v28 = v145;
        v27 = v146;
        v20 = v123;
        v29 = v149;
        v30 = v150;
        v14 = v136;
        goto LABEL_48;
      case 9:
        *v135 = *v13 + *v17 * (*v18 - *v15) - *v12 * ((*v134 >> 4) + *v24);
        *v0 = 6;
        v16 = v121;
        v30 = v150;
        v28 = v145;
        v27 = v146;
        goto LABEL_48;
      case 0xA:
        *v119 = (off_78020 + *v21 - *v126 - 1); // 按倒序方式计算当前要交换/处理的目标字节地址:off_78020 + start - index - 1
        *v0 = 26;
        v14 = v136;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 0xB:
        *v29 = *v8 * (*v11 - *v147) + *v15 * (*v131 >> 8);
        *v0 = 19;
        v30 = v150;
        v23 = v110;
        v22 = v113;
        goto LABEL_48;
      case 0xC:
        v43 = v143;
        v44 = v120;
        v45 = v130;
        v46 = v133;
        v47 = v134;
        v48 = v127;
        *v133 = *v12 + (*v11 >> 4) * *v23 - (*v134 + (*v8 >> 4)) * *v127;
        v49 = v12;
        v50 = v11;
        v51 = v8;
        v52 = v46;
        sub_5DEC4(226, *v43, *v44, 7);          // 第一次 mprotect/syscall(226):将目标 .text 区间所在页临时设为 RWX
        v8 = v51;
        v0 = v142;
        v11 = v50;
        v12 = v49;
        *v45 = 1;
        *v0 = 5;
        v127 = v48;
        v133 = v52;
        v134 = v47;
        v130 = v45;
        v120 = v44;
        v28 = v145;
        v31 = v138;
        v29 = v149;
        v30 = v150;
        v33 = v118;
        v13 = v115;
        v14 = v136;
        v17 = v103;
        v18 = v99;
        result = v109;
        v20 = v123;
        v21 = v122;
        v22 = v113;
        v24 = v140;
        v15 = v141;
        v25 = v102;
        v26 = v125;
        v32 = v117;
        v27 = v146;
        v16 = v121;
        goto LABEL_48;
      case 0xD:
        *v15 = (*v9 >> 8) * (*v151 + *v18) - *v15 * (*v26 + (*v114 >> 4));
        *v0 = 15;
        v31 = v138;
        v28 = v145;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 0xE:
        v53 = v148;
        v54 = v112;
        *v14 = (*v9 + (*v15 >> 4)) * *v148 - *v29 * (*v13 >> 4);
        *v54 = off_78020 / *v30;                // 根据目标区间长度和页内处理参数计算循环次数/分块次数
        *v0 = 9;
        v148 = v53;
        v20 = v123;
        v24 = v140;
        v28 = v145;
        v27 = v146;
        goto LABEL_48;
      case 0xF:
        *v22 = *v14 * *v16 + *v135 * (*v12 - *v10 + (*v22 << 8));
        v29 = v149;
        if ( *v144 != 1 )
        {
          v27 = v146;
          *v0 = 8;
LABEL_48:
          v10 = v33;
          v9 = v32;
          continue;
        }
        return result;
      case 0x10:
        ++*v126;
        *v0 = 6;
        v29 = v149;
        goto LABEL_48;
      case 0x11:
        *v28 = (*v21 + *v126);
        *v0 = 10;
        v29 = v149;
        goto LABEL_48;
      case 0x12:
        *v24 = (*v25 - *v10) * (*v151 >> 4) - *v18 * ((*result >> 4) - *v16);
        *v0 = 22;
        v31 = v138;
        v28 = v145;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 0x13:
        v55 = v31;
        v56 = v129;
        v57 = v143;
        *v9 = ((*v10 >> 4) + *v17) * *v15 - *v26 * (*v9 - *v127);
        *v57 = *v21 & ~(*v56 - 1);
        *v0 = 3;
        v20 = v123;
        v24 = v140;
        v27 = v146;
        v31 = v55;
        v14 = v136;
        v30 = v150;
        goto LABEL_48;
      case 0x14:
        v58 = v20;
        v59 = v101;
        v60 = v128;
        v61 = v130;
        v62 = v139;
        *v25 = *v133 * (*v151 - (*v13 >> 8)) + *v58 * (*v29 + (*v15 >> 4));
        *v60 = *v62;
        *v59 = *v61;
        v28 = v145;
        v27 = v146;
        v24 = v140;
        v16 = v121;
        v63 = v128;
        v31 = v138;
        v30 = v150;
        v14 = v136;
        while ( *v59 <= *v137 )
          *v63 += (*v59)++;
        v20 = v58;
        *v30 = *v0 / *v63;
        *v0 = 2;
        v29 = v149;
        goto LABEL_48;
      case 0x15:
        v125 = v26;
        v122 = v21;
        v64 = v151;
        v65 = result;
        *v144 = 1;
        v66 = v64;
        v67 = v10;
        (qword_E0B0[0])();
        v33 = v67;
        *v66 = *v12 * *v18 - (*v65 >> 4) + *v67;
        *v0 = 13;
        v151 = v66;
        v21 = v122;
        v28 = v145;
        v27 = v146;
        v31 = v138;
        v30 = v150;
        v11 = v116;
        v26 = v125;
        v13 = v115;
        v14 = v136;
        v16 = v121;
        v17 = v103;
        result = v65;
        v29 = v149;
        v22 = v113;
        v23 = v110;
        v25 = v102;
        v32 = v117;
        v8 = v108;
        v24 = v140;
        v15 = v141;
        v20 = v123;
        goto LABEL_48;
      case 0x16:
        *v15 = *v16 - (*v15 >> 4) * (*v12 >> 8) + (*v18 >> 4) * *v23;
        *v0 = 12;
        v29 = v149;
        goto LABEL_48;
      case 0x17:
        *v114 = (*v18 - (*v114 >> 4)) * (*v10 >> 4) - *v26;
        *v31 = (off_78020 + *v21);
        *v0 = 7;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 0x18:
        v149 = v29;
        v138 = v31;
        v68 = v126;
        v69 = v129;
        *v12 = *v12 * *v15 - *v11 * (*v26 + (*v25 >> 4));
        v70 = v12;
        v71 = v11;
        v72 = v15;
        v73 = v25;
        v74 = v26;
        v75 = sysconf(39);                      // 读取系统页大小 sysconf(_SC_PAGESIZE),供后续页对齐和 mprotect 使用
        v26 = v74;
        v25 = v73;
        v15 = v72;
        v11 = v71;
        v12 = v70;
        *v69 = v75;
        *v68 = 0;
        *v0 = 11;
        v126 = v68;
        v28 = v145;
        v31 = v138;
        v29 = v149;
        v30 = v150;
        v33 = v118;
        v13 = v115;
        v14 = v136;
        v17 = v103;
        v18 = v99;
        result = v109;
        v21 = v122;
        v22 = v113;
        v32 = v117;
        v8 = v108;
        v27 = v146;
        v20 = v123;
        v24 = v140;
        v16 = v121;
        goto LABEL_48;
      case 0x19:
        *v26 = *v9 + *v147 * (*v29 >> 4) - (*v15 + *v17) * *v13;
        *v0 = 20;
        v28 = v145;
        v27 = v146;
        v30 = v150;
        goto LABEL_48;
      case 0x1A:
        v76 = v119;
        *v27 = **v28;                           // 核心字节交换逻辑:取两个地址处的字节并交换,属于 .text 恢复过程的关键动作
        **v28 = **v76;
        **v76 = *v27;
        *v0 = 16;
        v119 = v76;
        v29 = v149;
        goto LABEL_48;
      case 0x1B:
        *v144 = (*v144 - *v16) * (*v25 >> 8) - *result * *v16 + *v12 * (*v147 - *v8);
        *v0 = 23;
        v14 = v136;
        v28 = v145;
        v27 = v146;
        v30 = v150;
        goto LABEL_48;
      case 0x1C:
        *v11 = *v11 + (*v144 >> 4) * (*v23 - *v25) - (*v151 >> 8) * (*v148 - *v11);
        *v0 = 14;
        v16 = v121;
        v31 = v138;
        v14 = v136;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 0x1D:
        *result = *v16 * (*result - *v14) + *v17 * (*v26 >> 4) - *result * *v147;
        *v0 = 31;
        v29 = v149;
        v30 = v150;
        goto LABEL_48;
      case 0x1E:
        v77 = v17;
        v78 = v124;
        *v20 = (*v141 >> 8) * *v22 + (*v20 >> 4) * (*v20 - *v18);
        *v78 = (init_transform_text - qword_78028);// 计算当前函数所在页基址:init_transform_text - qword_78028;当前 qword_78028 = 0,因此等价于页对齐后的函数区地址
        v79 = v22;
        v80 = v20;
        v81 = sub_5DEC4(226, *v78, 4096, 7);    // 第二次 mprotect/syscall(226):先把当前代码页设为 RWX,允许后续自修改/清零局部指令数据
        v20 = v80;
        v22 = v79;
        v31 = v138;
        v29 = v149;
        v30 = v150;
        v11 = v116;
        v33 = v118;
        v14 = v136;
        v17 = v77;
        v21 = v122;
        v25 = v102;
        v26 = v125;
        v32 = v117;
        v27 = v146;
        v24 = v140;
        v16 = v121;
        if ( !v81 )
        {
          v82 = v149;
          v83 = v150;
          v84 = v138;
          v85 = v102;
          v86 = v118;
          v87 = v124;
          v88 = v107;
          v89 = v106;
          v90 = v104;
          v91 = v100;
          *v107 = *v124;                        // 开始处理当前代码页内容:根据页内偏移和表项长度清零一段数据/指令区域
          *v89 = (*v87 + *(*v88 + 4));
          *v90 = *(*v88 + 28) * *(*v88 + 27);
          *v91 = 0;
          v92 = v105;
          v33 = v86;
          while ( *v91 <= *v90 )
            *(*v89 + (*v91)++) = 0;
          v25 = v85;
          v31 = v84;
          *v92 = *(*v88 + 26);                  // 读取另一段长度字段,并继续清零当前代码页中的目标区域
          *v98 = 0;
          v30 = v83;
          v14 = v136;
          while ( *v98 < *v92 )
            *(*v88 + (*v98)++) = 0;
          v26 = v125;
          v20 = v123;
          v24 = v140;
          v29 = v82;
        }
        v28 = v145;
        v12 = v111;
        result = v109;
        v8 = v108;
        v15 = v141;
        *v0 = 1;
        goto LABEL_48;
      case 0x1F:
        v93 = v25;
        v94 = v27;
        v95 = v16;
        v150 = v30;
        v146 = v94;
        v96 = v31;
        v97 = v21;
        flush_dcache_icache_range(*v21, *v31);  // 恢复完成后刷新数据缓存和指令缓存,使修改后的 .text 对 CPU 可见
        v21 = v97;
        v31 = v96;
        *v0 = 21;
        v30 = v150;
        v11 = v116;
        v33 = v118;
        v16 = v95;
        v27 = v146;
        v17 = v103;
        v18 = v99;
        result = v109;
        v22 = v113;
        v23 = v110;
        v25 = v93;
        v26 = v125;
        v32 = v117;
        v8 = v108;
        v24 = v140;
        v15 = v141;
        v20 = v123;
        v29 = v149;
        goto LABEL_48;
      default:
        goto LABEL_48;
    }
  }
}

经典ollvm混淆,看不懂吧,看不懂就上AI!!

for ea in [0x7CC8, 0x3487C) step 4:
      patch bytes [b0 b1 b2 b3] -> [b3 b2 b1 b0]
import ida_auto
import ida_bytes
import ida_funcs
import ida_kernwin
import ida_name
import ida_ua

TEXT_DELTA_EA = 0x78018
TEXT_SIZE_EA = 0x78020

FUNC_RANGES = [
    (0x100EC, 0x10130, "java_vm_get_env"),
    (0x10130, 0x102A4, "register_protection_modules"),
    (0x25DE8, 0x25EC4, "dispatch_feature_event_to_registered_modules"),
    (0xE51C, 0xF8E4, "JNI_OnLoad"),
]

def patch_range(start_ea, data):
    for i, b in enumerate(data):
        ida_bytes.patch_byte(start_ea + i, b)

def make_code_range(start_ea, end_ea):
    ida_bytes.del_items(start_ea, ida_bytes.DELIT_SIMPLE, end_ea - start_ea)
    cur = start_ea
    while cur < end_ea:
        size = ida_ua.create_insn(cur)
        if not size:
            cur += 4
            continue
        cur += size

def make_function(start_ea, end_ea, new_name):
    ida_funcs.del_func(start_ea)
    ok = ida_funcs.add_func(start_ea, end_ea)
    if ok:
        ida_name.set_name(start_ea, new_name, ida_name.SN_FORCE)
    return ok

def restore_text():
    delta = ida_bytes.get_qword(TEXT_DELTA_EA)
    size = ida_bytes.get_qword(TEXT_SIZE_EA)
    start = TEXT_DELTA_EA - delta
    data = ida_bytes.get_bytes(start, size)
    if not data:
        raise RuntimeError("failed to read .text range")

    # 当前样本里可直接用整段反转恢复关键入口区域
    patch_range(start, data[::-1])
    print("[+] text restored: start=0x%X size=0x%X end=0x%X" % (start, size, start + size))

def rebuild_key_functions():
    for start_ea, end_ea, name in FUNC_RANGES:
        make_code_range(start_ea, end_ea)
        ok = make_function(start_ea, end_ea, name)
        print("[+] make_func %-40s 0x%X-0x%X ok=%s" % (name, start_ea, end_ea, ok))

def add_comments():
    ida_bytes.set_cmt(0x50250, "第二阶段初始化:恢复被变换的 .text 区间", 0)
    ida_bytes.set_cmt(0x100EC, "JavaVM->GetEnv 包装函数", 0)
    ida_bytes.set_cmt(0x10130, "注册 com/sagittarius/v6/A 的 3 个 native 方法", 0)
    ida_bytes.set_cmt(0x25DE8, "向已注册 feature 模块广播事件", 0)
    ida_bytes.set_cmt(0xE51C, "JNI_OnLoad 主入口;修正了函数尾部,避免出现 JUMPOUT(0xF8E0)", 0)

def main():
    restore_text()
    rebuild_key_functions()
    add_comments()
    ida_auto.auto_wait()
    ida_kernwin.refresh_idaview_anyway()
    print("[+] text recovery done")


if __name__ == "__main__":
    main()

这样JNI_OnLoad就能够还原出来了,注册了n1 / n2 / n3(顺手恢复一下函数名)

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  void *___feature_dispatcher___; // x0
  struct timeval tv_; // [xsp-50h] [xbp-90h] BYREF
  __int64 v5[4]; // [xsp-40h] [xbp-80h] BYREF
  JavaVM *vm_1; // [xsp-20h] [xbp-60h]

  vm_1 = vm;
  v5[2] = reserved;
  tv_.tv_sec = 0;
  if ( (byte_783C8 & 1) != 0 )                  // 如果已经初始化过,则直接返回 JNI_VERSION_1_6,避免重复执行初始化逻辑
    return 65540;
  byte_783C8 = 1;                               // 设置已初始化标志,后续再次进入 JNI_OnLoad 时会直接返回
  gettimeofday(&::tv_, 0);                      // 记录初始化起始时间,用于统计壳初始化耗时
  if ( java_vm_get_env(vm_1, v5, 65540) )       // 调用 JavaVM->GetEnv(&env, JNI_VERSION_1_6);失败则返回 -1
    return -1;
  ::vm = vm_1;                                  // 保存全局 JavaVM 指针,供后续 worker 线程或 JNI 辅助逻辑复用
  ___feature_dispatcher___ = sub_25D20();       // 获取 feature dispatcher 单例
  dispatch_feature_event_to_registered_modules(___feature_dispatcher___, 2, v5[0], 0, 0, 0);// 广播 event=2,通知各保护模块执行 JNI_OnLoad 阶段初始化
  if ( register_protection_modules(v5[0]) )     // 注册 com/sagittarius/v6/A 上的 3 个 native 方法;失败则返回 -1
    return -1;
  gettimeofday(&tv_, 0);                        // 记录初始化结束时间
  qword_783B8 = 1000000 * (tv_.tv_sec - ::tv_) + tv_.tv_usec - qword_783B0;
  return 65540;                                 // 返回 JNI_VERSION_1_6 (0x10004)
}

然后看register_protection_modules

__int64 __fastcall register_protection_modules(__int64 a1)
{
  __int64 v1; // x0
  __int64 v3; // [xsp+8h] [xbp-78h]
  _QWORD v6[9]; // [xsp+20h] [xbp-60h] BYREF

  v6[0] = (decode_hex_xor_k1)("0F892D685939526D");      // 解码第 1 个 native 方法名	n1
  v6[1] = (decode_hex_xor_k2)(
            "4F25DC8975A33B550346DE887FA531521346FE887FA531441352F18D70A735130B08D3803E82204E0E07DADC5DBB354A0646D1867FB6"
            "7B6F131BD48976EA1856061FDCC87DB03A5B483AC99578BF33072E40EB");                                  // 解码第 1 个 native 方法签名,并绑定 native_feature_entry_1 (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V
  v6[2] = &native_feature_entry_1;
  v6[3] = (decode_hex_xor_k3)("CAD6059A10DC32C3");      // 解码第 2 个 native 方法名	n2
  v6[4] = (decode_hex_xor_k4)("29B16CA41B46FCCE65D26EA51140F6C975D24EA51140F6DF75C6249C");// 解码第 2 个 native 方法签名,并绑定 native_feature_entry_2	(Landroid/content/Context;)V
  v6[5] = &loc_10448[3] + 4;
  v6[6] = (decode_hex_xor_k5)("2E0C218B92E17D7E");      // 解码第 3 个 native 方法名	n3
  v6[7] = (decode_hex_xor_k6)("4C21048D5373F3960042068C5975F9911042268C5975F98710564CB5");// 解码第 3 个 native 方法签名,并绑定 native_feature_entry_3  	(Landroid/content/Context;)V
  v6[8] = &loc_10448[32] + 4;
  v1 = ((&loc_105A8[17] + 4))(&byte_78870);     // 取出内嵌字符串对象中的类名:com/sagittarius/v6/A
  v3 = (&loc_105A8[9])(a1, v1);                 // env->FindClass("com/sagittarius/v6/A")
  if ( v3 )
  {                                             // env->RegisterNatives(clazz, methods, 3)
    if ( ((&loc_105A8[25] + 4))(a1, v3, v6, 3) < 0 )
      return -1;
    else
      return 0;
  }
  else
  {
    return -1;
  }
}

有点难看,大概就是以下的意思

methods[0].name      = decode_hex_xor_k1(...);   // n1
methods[0].signature = decode_hex_xor_k2(...);
methods[0].fnPtr     = native_feature_entry_1;	 //(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V

methods[1].name      = decode_hex_xor_k3(...);   // n2
methods[1].signature = decode_hex_xor_k4(...);
methods[1].fnPtr     = native_feature_entry_2;	 //(Landroid/content/Context;)V

methods[2].name      = decode_hex_xor_k5(...);   // n3
methods[2].signature = decode_hex_xor_k6(...);
methods[2].fnPtr     = native_feature_entry_3;	 //(Landroid/content/Context;)V

其中这里还有一些解密字符串的操作,其中就是xor的数组不一样而已,写个脚本即可。

_BYTE *__fastcall sub_1C630(_BYTE *p__0F892D685939526D)
{
  _BYTE *v2; // x8
  unsigned __int64 v4; // x21
  _BYTE *result; // x0
  int n0x3A; // w9
  int n8; // w14
  unsigned __int8 *v8; // x8
  _BYTE *v9; // x15
  unsigned int n0x3A_1; // w17
  char v11; // w16
  char v12; // w18

  v2 = p__0F892D685939526D - 1;
  while ( *++v2 )
    ;
  v4 = (v2 - p__0F892D685939526D) >> 1;
  result = malloc(v4 + 1);
  result[v4] = 0;
  LOBYTE(n0x3A) = *p__0F892D685939526D;
  if ( *p__0F892D685939526D )
  {
    n8 = 0;
    v8 = p__0F892D685939526D + 2;
    v9 = result;
    do
    {
      n0x3A_1 = *(v8 - 1);
      if ( n0x3A >= 0x3Au )
        v11 = -112;
      else
        v11 = 0;
      if ( n0x3A_1 >= 0x3A )
        v12 = -55;
      else
        v12 = -48;
      if ( n8 == 8 )
        n8 = 0;
      *v9++ = byte_5F2F2[n8] ^ (v11 + 16 * n0x3A + v12 + n0x3A_1);
      n0x3A = *v8;
      ++n8;
      v8 += 2;
    }
    while ( n0x3A );
  }
  return result;
}
KEYS = {
    "k0": bytes([0x31, 0x48, 0xB6, 0x9D, 0x4B, 0x47, 0xB3, 0x31]),
    "k1": bytes([0x61, 0xB8, 0x2D, 0x68, 0x59, 0x39, 0x52, 0x6D]),
    "k2": bytes([0x67, 0x69, 0xBD, 0xE7, 0x11, 0xD1, 0x54, 0x3C]),
    "k3": bytes([0xA4, 0xE4, 0x05, 0x9A, 0x10, 0xDC, 0x32, 0xC3]),
    "k4": bytes([0x01, 0xFD, 0x0D, 0xCA, 0x7F, 0x34, 0x93, 0xA7]),
    "k5": bytes([0x40, 0x3F, 0x21, 0x8B, 0x92, 0xE1, 0x7D, 0x7E]),
    "k6": bytes([0x64, 0x6D, 0x65, 0xE3, 0x37, 0x01, 0x9C, 0xFF]),
}

def decode_hex_xor(hex_str, key_name):
    key = KEYS[key_name]
    data = bytes.fromhex(hex_str)
    out = bytes(b ^ key[i & 7] for i, b in enumerate(data))
    return out.rstrip(b"\x00")


if __name__ == "__main__":
    samples = [
        ("k0", "143B99B32923DF5E5223"),
        ("k1", "0F892D685939526D"),
        ("k2", "4F25DC8975A33B550346DE887FA531521346FE887FA531441352F18D70A735130B08D3803E82204E0E07DADC5DBB354A0646D1867FB67B6F131BD48976EA1856061FDCC87DB03A5B483AC99578BF33072E40EB"),
        ("k3", "CAD6059A10DC32C3"),
        ("k4", "29B16CA41B46FCCE65D26EA51140F6C975D24EA51140F6DF75C6249C"),
        ("k5", "2E0C218B92E17D7E"),
        ("k6", "4C21048D5373F3960042068C5975F9911042268C5975F98710564CB5"),
    ]

    for key_name, hex_str in samples:
        plain = decode_hex_xor(hex_str, key_name)
        try:
            text = plain.decode("utf-8")
        except UnicodeDecodeError:
            text = plain.decode("latin1")
        print(f"{key_name}: {text}")
输出
k0: %s/.bdlock
k1: n1
k2: (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V
k3: n2
k4: (Landroid/content/Context;)V
k5: n3
k6: (Landroid/content/Context;)V

接下来就看看native_feature_entry_1他们了。(只有这个是事关壳的)

// n1 是壳在 attachBaseContext 阶段的主初始化入口:保存 pkg/apk/data 路径,构造 dataPath/block,同步等待真实 bootstrap 回调后再继续执行壳逻辑。
void __fastcall native_feature_entry_1(
        JNIEnv *env,
        jobject thiz,
        jobject context,
        jstring pkg_name_jstr,
        jstring apk_path_jstr,
        jstring data_path_jstr,
        jint sdk_or_mode)
{                                               
  const char *pkg_name_cstr; // x0
  const char *apk_path_cstr; // x0
  const char *data_path_cstr; // x0
  const char *block_path_fmt; // x20
  __int64 data_path_saved_cstr; // x0
  char *bootstrap_cb; // [xsp+8h] [xbp-278h]
  char block_lock_ctx[16]; // [xsp+48h] [xbp-238h] BYREF
  char path_buf[512]; // [xsp+58h] [xbp-228h] BYREF

  if ( (byte_783CB & 1) == 0 )
  {
    byte_783CB = 1;                             
    dword_78748 = sub_1BD04();                  
    pkg_name_cstr = jni_get_string_utf8(env, path_buf, pkg_name_jstr, 0x200u);
    sub_106B8(&obj__1, pkg_name_cstr);          // 保存 pkgName 到全局字符串对象,后续路径构造和壳逻辑会继续使用。
    apk_path_cstr = jni_get_string_utf8(env, path_buf, apk_path_jstr, 0x200u);
    sub_106B8(&obj__3, apk_path_cstr);          // 保存 apkPath 到全局字符串对象。
    data_path_cstr = jni_get_string_utf8(env, path_buf, data_path_jstr, 0x200u);
    sub_106B8(&obj__4, data_path_cstr);         // 保存 dataPath 到全局字符串对象。
    block_path_fmt = decode_hex_xor_k0("143B99B32923DF5E5223");// "%s/block"。
    data_path_saved_cstr = sub_10634(&obj__4);  // 取出之前保存的 dataPath C 字符串。
    snprintf(path_buf, 0x200u, block_path_fmt, data_path_saved_cstr);// 拼出 block 路径:<dataPath>/block。
    bootstrap_cb = wait_and_get_bootstrap_callback();// 等待异步初始化完成,并取回真正的 bootstrap 回调入口。
    sub_107A0(block_lock_ctx, path_buf);        // 围绕 block 路径建立同步/互斥上下文,通常会打开并处理该文件。
    (bootstrap_cb)(env, context);               // 调用真正的壳 bootstrap 入口,后续核心初始化逻辑从这里展开。
    sub_10850(block_lock_ctx);                  // 清理 block 文件相关句柄/同步上下文。
  }
}

接着看wait_and_get_bootstrap_callback()

// 等待前置异步初始化完成:只有当 3 个全局就绪标志都被置位后,才通过 基址(off_78330[0]) + 偏移(qword_791F0) 计算出真正的 bootstrap 回调入口。
char *wait_and_get_bootstrap_callback()
{
  do
  {                                             // 忙等第 1 个就绪标志:通常表示前置模块/线程已启动。
    while ( !g_bootstrap_ready_stage0 )
      ;
  }
  while ( !g_bootstrap_ready_stage1 || !g_bootstrap_ready_stage2 );// 检查第 3 个就绪标志;三个标志都就绪后才允许继续。
  __dmb(0xBu);                                  // 读取 bootstrap 回调相对基址的偏移值。
  return g_bootstrap_base[0] + n4096;           // 返回真实 bootstrap 入口 = off_78330[0] + qword_791F0。
}

真是入口如下

// 真实 bootstrap 入口:不直接做解包/加载,而是取出 feature dispatcher 后向所有已注册保护模块广播 event 3;真正的壳初始化逻辑在各模块的 event 3 处理函数中展开。
__int64 __fastcall bootstrap_dispatch_event_3(__int64 env_or_reserved, __int64 context_obj)
{
  void *feature_dispatcher; // x0
  __int64 env_or_reserved_1; // [xsp+0h] [xbp-30h]

  env_or_reserved_1 = env_or_reserved;
  *(&env_or_reserved_1 - 2) = context_obj;
  feature_dispatcher = get_feature_dispatcher_singleton();// 获取全局 feature dispatcher 单例。所有保护模块都挂在这个调度器下面。
  dispatch_feature_event_to_registered_modules(
    feature_dispatcher,
    3u,
    *(&env_or_reserved_1 - 2),
    *(&env_or_reserved_1 - 2),
    0,
    0);                                         // 当前 bootstrap 分发函数本身不返回业务结果,只负责完成 event 3 广播后返回。
  return 0;
}

在继续看dispatch_feature_event_to_registered_modules

// 事件分发总入口:遍历 feature dispatcher 中的模块桶,把 event_id 连同 4 个附加参数广播给所有已注册保护模块。真正的 attachBaseContext/onCreate/反调试等逻辑都在各模块的虚函数回调里实现。
__int64 __fastcall dispatch_feature_event_to_registered_modules(
        __int64 feature_dispatcher,
        unsigned int event_id,
        __int64 arg1,
        __int64 arg2,
        __int64 arg3,
        __int64 arg4)
{
  __int64 feature_dispatcher_1; // x24
  __int64 bucket_index; // x26
  __int64 bucket_desc; // x28
  __int64 (__fastcall ****module_iter)(_QWORD, _QWORD, __int64, __int64, __int64, __int64); // x25
  __int64 (__fastcall ****_______end__________[begin__end)____1)(_QWORD, _QWORD, __int64, __int64, __int64, __int64); // x8
  _QWORD *bucket_end_ptr; // x28
  __int64 (__fastcall ****_______end__________[begin__end)___)(_QWORD, _QWORD, __int64, __int64, __int64, __int64); // t1
  __int64 (__fastcall ***module_obj)(_QWORD, _QWORD, __int64, __int64, __int64, __int64); // t1

  feature_dispatcher_1 = feature_dispatcher;
  for ( bucket_index = 1; bucket_index != 19; ++bucket_index )// 切到下一个模块桶继续广播。
  {
    bucket_desc = feature_dispatcher_1 + 24 * bucket_index;
    module_iter = *bucket_desc;                 // 读取当前桶的 begin 指针。桶内元素是模块对象指针数组。
    _______end__________[begin__end)___ = *(bucket_desc + 8);// 读取当前桶的 end 指针;内层循环在 [begin, end) 范围内遍历所有模块对象。
    bucket_end_ptr = (bucket_desc + 8);
    for ( _______end__________[begin__end)____1 = _______end__________[begin__end)___;
          module_iter != _______end__________[begin__end)____1;
          _______end__________[begin__end)____1 = *bucket_end_ptr )
    {
      module_obj = *module_iter++;              // 取出一个模块对象指针。
      feature_dispatcher = (**module_obj)(module_obj, event_id, arg1, arg2, arg3, arg4);// 调用模块回调:module->on_event(event_id, arg1, arg2, arg3, arg4)。不同 event_id 对应 attachBaseContext、onCreate、等阶段。
    }
  }
  return feature_dispatcher;
}

可以看到这里就是主逻辑在广播event 3,在这里正式进入壳的初始化阶段。

feature_classloader_on_event只响应event 3,并且调用run_classloader_bootstrap_strategy,这里是classloader 的入口。

// protect_classloader 模块的统一事件入口。当前样本里只处理 event 3:进入 attachBaseContext 阶段的 classloader/壳主分支初始化;成功后会置位 byte_783A0。
__int64 __fastcall feature_classloader_on_event(__int64 a1, int n3, __int64 a3, __int64 a4)
{
  __int64 result; // x0

  if ( n3 != 3 )
    return 0;
  run_classloader_bootstrap_strategy(a3, a4);
  result = 1;
  byte_783A0 = 1;
  return result;
}

run_classloader_bootstrap_strategy:先尝试modern ART / dex verifier路线,如果失败,再按 SDK 版本回退到默认兼容路线。

// event 3 的核心 classloader 启动分流点:先尝试 modern ART / dex verifier 路线;如果该路线不可用或执行失败,再回退到默认 classloader 兼容路线。这里非常接近壳主加载链。
void __fastcall run_classloader_bootstrap_strategy(__int64 a1, __int64 a2)
{
  _QWORD *__SDK___________classloader______; // [xsp+0h] [xbp-30h]
  _QWORD *____SDK________modern_ART________; // [xsp+8h] [xbp-28h]
  char v4; // [xsp+17h] [xbp-19h]

  v4 = 0;
  if ( sub_198DC(0x20000) )                     // 检查全局能力标志 0x20000;命中时允许优先走 modern ART 路线。
  {
    ____SDK________modern_ART________ = hook_art_dexfile_verifier_modern_0(n25);// 按当前 SDK/环境尝试构造 modern ART 路线的实现对象。
    if ( ____SDK________modern_ART________ )
    {                                           // modern ART 路线执行成功,置成功标志,后续不再走兜底分支。
      if ( (**____SDK________modern_ART________)(____SDK________modern_ART________, a1, a2) )
        v4 = 1;
      else
        sub_49510(____SDK________modern_ART________);// modern ART 路线执行失败,则释放该实现对象,准备回退到默认 classloader 兼容路线。
    }
  }
  if ( (v4 & 1) == 0 )                          // 如果优先分支没有成功完成,则进入默认 classloader 策略。
  {
    __SDK___________classloader______ = select_default_classloader_strategy(n25);// 按 SDK 版本选择默认/兼容 classloader 实现对象。
    (**__SDK___________classloader______)(__SDK___________classloader______, a1, a2);// 调用默认策略对象的虚函数入口,继续 event 3 的 classloader 主链。
  }
}

hook_art_dexfile_verifier_modern解析libdexfile.soart::dex::Verify

// ART modern verifier hook: resolves libdexfile.so art::dex::Verify and then calls load_dex_daemon_or_parallel.
__int64 __fastcall hook_art_dexfile_verifier_modern(__int64 a1, JNIEnv *env)
{
  char load_ok; // w19
  __int64 verify_ctx; // [xsp+0h] [xbp-90h] BYREF
  _QWORD symbol_ctx[5]; // [xsp+8h] [xbp-88h] BYREF
  _QWORD *p_1; // [xsp+30h] [xbp-60h]
  __int64 tmp_meta; // [xsp+38h] [xbp-58h] BYREF
  __int64 *p; // [xsp+60h] [xbp-30h]

  sub_2B4B4();                                  // 初始化 modern ART verifier / dex 装载相关的运行时上下文。
  sub_2DDDC(symbol_ctx);                        // 初始化符号解析容器 symbol_ctx。
  sub_2DE2C(
    symbol_ctx,
    "libdexfile.so",
    "_ZN3art3dex6VerifyEPKNS_7DexFileEPKhmPKcbPNSt3__112basic_stringIcNS8_11char_traitsIcEENS8_9allocatorIcEEEE");// 向 symbol_ctx 中写入目标 so 与符号名:libdexfile.so / art::dex::Verify。
  sub_2DFC8(symbol_ctx);                        // 完成符号解析或校验结果,确认 modern ART verifier 路线可继续执行。
  load_ok = load_dex_daemon_or_parallel(&verify_ctx, env);// 真正进入 modern ART 路线的 dex 装载总控:load_dex_daemon_or_parallel。
  sub_2E0E8();                                  // 清理 verifier / symbol 解析相关上下文。
  if ( p != &tmp_meta && p )                    // 释放第一组临时缓冲区/对象。
  {
    if ( (tmp_meta - p) < 0x101 )
      sub_4B940();
    else
      sub_49510(p);
  }
  if ( p_1 != symbol_ctx && p_1 )               // 释放第二组临时缓冲区/对象。
  {
    if ( symbol_ctx[0] - p_1 < 0x101u )
      sub_4B940();
    else
      sub_49510(p_1);
  }
  return load_ok & 1;                           // 只返回布尔结果:modern ART 路线是否执行成功。
}

load_dex_daemon_or_parallel

  • 如果只有 1 个 dex -> load_dex_main_thread
  • 如果有多个 dex -> parallel_load_one_dex_worker
  • 总之都会把单个dex处理成 ByteBuffer
// 真正的 dex 装载总控:单 dex 直接走主线程;多 dex 则创建最多 4 个 worker 并行处理。每个 dex 最终都会被包装成 DirectByteBuffer 并交给 Java 层 com/sagittarius/v6/A.a1(ByteBuffer)。
__int64 __fastcall load_dex_daemon_or_parallel(__int64 verify_ctx, JNIEnv *env)
{
  int dex_package_count; // w20
  jint dexCount; // w1
  JNIEnv *env_1; // x0
  int worker_count; // w0
  __int64 *dex_load_thread_pool; // x22
  jclass bridge_class; // x21
  __int64 dex_package_count_1; // x23
  const char *format; // x19
  int *errnop; // x0
  char *errstr; // x0

  dex_package_count = get_dex_package_count();  // 获取当前需要处理的 dex 包数量。
  if ( dex_package_count == 1 )                 // 如果只有 1 个 dex,则不走线程池,直接转入主线程加载路径 load_dex_main_thread。
  {
    dexCount = 1;
    env_1 = env;
    return load_dex_main_thread(env_1, dexCount);
  }
  if ( dex_package_count <= 4 )
    worker_count = dex_package_count;           // 并行模式下最多只开 4 个 worker 线程,多余 dex 任务排队处理。
  else
    worker_count = 4;
  dex_load_thread_pool = create_dex_load_thread_pool(worker_count);// 创建 dex 加载线程池。
  bridge_class = env->functions->FindClass(env, *(&xmmword_78890 + 1));// 查找 Java 桥接类 com/sagittarius/v6/A;后续需要在这个类上调用静态方法 a1(ByteBuffer)。
  if ( !bridge_class )
  {
    __android_log_print(7, "XOX", "state=%d", 427);
    n427 = 427;
    format = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
    errnop = __errno();
    errstr = strerror(*errnop);
    snprintf(s_, 0x40u, format, "load_dex_daemon_thread", 114, errstr);
    sub_2EFC4(427);
    while ( n427 < 1 )
      MEMORY[0xDEADDEADDEAD01AB] = 3735928559LL;
    sub_198A0(427);
  }
  bridge_class_global = (*&env->functions->_pad_findclass_to_deletelocalref[112])(env, bridge_class);// 创建 bridge 类的全局引用,供并行 worker 线程跨线程安全使用。
  __Java______A_a1_ByteBuffer____method_ID_nativ = env->functions->GetStaticMethodID(
                                                     env,
                                                     bridge_class,
                                                     "a1",
                                                     "(Ljava/nio/ByteBuffer;)V");// 获取 bridge 方法 ID:A.a1(ByteBuffer)。native 侧把单个 dex 包装成 DirectByteBuffer 后,就通过这个入口交给 Java 层处理。
  n401 = 0;
  if ( dex_package_count >= 1 )
  {
    dex_package_count_1 = 0;
    do
      enqueue_dex_load_task(dex_load_thread_pool, parallel_load_one_dex_worker, dex_package_count_1++);// 为每个 dex 创建一个并行加载任务,worker 入口为 parallel_load_one_dex_worker。
    while ( dex_package_count != dex_package_count_1 );
  }
  wait_dex_load_thread_pool_idle(dex_load_thread_pool);// 等待线程池中的所有 dex 加载任务完成。
  destroy_dex_load_thread_pool(dex_load_thread_pool);// 销毁 dex 加载线程池。
  env->functions->DeleteLocalRef(env, bridge_class);// 释放桥接类的本地引用。
  (*&env->functions->_pad_findclass_to_deletelocalref[120])(env, bridge_class_global);// 释放 bridge 类的全局引用。
  if ( n401 )                                   // 如果并行路径中设置了回退标志 n401,则改为主线程串行重新加载全部 dex。
  {
    env_1 = env;
    dexCount = dex_package_count;               // 回退到 load_dex_main_thread,按主线程路径串行处理所有 dex。
    return load_dex_main_thread(env_1, dexCount);
  }
  return 1;
}

dex_source_read_one_dex

  • dex_source单例里按索引取一个 dex
  • dex_source底层不是简单读文件,而是会扫描/proc/self/maps
  • 说明它是从内存映射中定位/组织真实dex数据
// dex_source 的统一读取接口:本函数本身不直接解析 dex,而是转发到 dex_source 对象内部的虚函数 read(index, &address, &capacity)。返回指定索引 dex 的原始内存缓冲区和大小。
__int64 __fastcall dex_source_read_one_dex(
        __int64 dex_source_singleton,
        __int64 dex_index,
        __int64 p_address,
        __int64 p_capacity)
{
  return (*(**(dex_source_singleton + 184) + 16LL))(*(dex_source_singleton + 184), dex_index, p_address, p_capacity);
}

native -> Java 桥接

调用com.sagittarius.v6.A.a1(ByteBuffer) - >h.a(context, ByteBuffer) ->p.a(classLoader0, byteBuffer0) -> p.a(classLoader, ByteBuffer[])

p.a(classLoader, ByteBuffer[])

  • 反射拿 pathList
  • ByteBuffer[]转成新的dexElements
  • 写回pathList.dexElements
  • 这里就是真正的dex注入点
    public static void a1(ByteBuffer byteBuffer0) {
        h.a(StubApplication.mContext, byteBuffer0);
    }
    public static void a(Context context0, ByteBuffer byteBuffer0) {
        ClassLoader classLoader0 = h.a(context0);
        if(classLoader0 == null) {
            throw new RuntimeException(h.h[11]);
        }

        if(Build.VERSION.SDK_INT >= 26) {
            try {
                p.a(classLoader0, byteBuffer0);
                return;
            }
            catch(Exception exception0) {
                throw new RuntimeException(exception0);
            }
        }

        throw new RuntimeException(h.h[10]);
    }
    static void a(ClassLoader classLoader0, ByteBuffer byteBuffer0) {
        p.a(classLoader0, new ByteBuffer[]{byteBuffer0});
    }

除了classloader外还有一条个关于event 3

// protect_environment 模块的统一事件入口。当前样本里它只关心 event 3,用来准备后续真实 dex 读取所需的环境对象。
__int64 __fastcall feature_protect_environment_on_event(__int64 a1, int n3)
{                                               // 只处理 event 3;其他事件直接忽略。说明这个模块属于 attachBaseContext 阶段的主加载准备逻辑。
  _BYTE *v2; // x19
  void (*v3)(void); // x8

  if ( n3 != 3 )
    return 0;
  v2 = qword_78B98;                             // 取全局 dex_source 单例。后面所有 dex 包数量查询、bundle 读取、真实 dex 输出都围绕这个对象展开。
  if ( !qword_78B98 )                           // 如果 dex_source 还没初始化,这里走首次构造路径。
  {
    v2 = sub_4A07C(0xD0u);                      // 分配 0xD0 大小的 dex_source 对象。它本身更像一个“读取上下文/调度器”,不是直接的 dex 数据。
    v2[200] = 0;                                // 清零内部状态字段,避免沿用旧状态。这里清的是若干路径列表、provider 指针和标志位。
    *(v2 + 21) = 0;
    *(v2 + 22) = 0;
    *(v2 + 20) = 0;
    v3 = off_78408;                             // 取默认构造入口 off_78408,对新分配的 dex_source 做基础初始化。
    *(v2 + 18) = 0;
    *(v2 + 19) = 0;
    *(v2 + 17) = 0;
    v3();                                       // 执行 dex_source 基础构造,建立默认空对象和内部成员初始布局。
    qword_78B98 = v2;                           // 把初始化完成的 dex_source 保存到全局,后续 event 3 / dex 加载流程都复用它。
  }
  (init_dex_source_paths)(v2);                  // 成功后,后续 classloader 路线就可以通过 dex_source 读取并还原真实 dex。 
  return 1;
}

init_dex_source_paths简单来说就是把后面恢复真实dex需要的路径表和读取器都装配好。

// 初始化 dex_source 的路径与后端 provider。这里会先准备若干 APK/data 目录相关的候选路径,再按 dex 序号生成 bundle 文件名,最后挂上 DexBundleFileV1 / VmpBundleFileV1 两个读取后端。
__int64 __fastcall init_dex_source_paths(__int64 a1)
{
  void *v1; // x19
  _BYTE *v2; // x0
  void (__fastcall *v3)(char *, __int64, _BYTE *, __int64); // x22
  _BYTE *v4; // x19
  __int64 runtime_data_path; // x0
  void (__fastcall *v6)(char *, __int64, _BYTE *, __int64); // x23
  _BYTE *v7; // x19
  __int64 runtime_data_path_1; // x0
  size_t n; // x0
  char *dest_1; // x8
  char *dest_2; // x9
  size_t n_1; // x19
  char *v13; // x19
  char *dest_5; // x10
  char *dest_4; // x8
  unsigned int dex_package_count_1; // w21
  void *v17; // x24
  _BYTE *v18; // x0
  void *v19; // x24
  _BYTE *v20; // x0
  void (__fastcall ***bundle_provider)(_QWORD, _QWORD); // x0
  void (__fastcall ***v22)(_QWORD, _QWORD); // x0
  unsigned int v23; // w19
  int dex_package_count; // [xsp+1Ch] [xbp-224h]
  _QWORD v27[5]; // [xsp+20h] [xbp-220h] BYREF
  _QWORD *p_1; // [xsp+48h] [xbp-1F8h]
  _QWORD v29[5]; // [xsp+50h] [xbp-1F0h] BYREF
  _QWORD *p; // [xsp+78h] [xbp-1C8h]
  _QWORD v31[4]; // [xsp+80h] [xbp-1C0h] BYREF
  char *dest_3; // [xsp+A0h] [xbp-1A0h]
  char *dest; // [xsp+A8h] [xbp-198h]
  char src[306]; // [xsp+B6h] [xbp-18Ah] BYREF

  v1 = off_784B8;
  v2 = decode_hex_xor_k0("143B99B37A47B331");   // %s/.1
  (v1)(src, 306, v2, *(&xmmword_78860 + 1));
  if ( access(src, 0) != -1 )
    (loc_1B228)(src);
  dex_package_count = get_dex_package_count();	
  v3 = off_784B8;
  dest_3 = v31;
  dest = v31;
  LOBYTE(v31[0]) = 0;
  v4 = decode_hex_xor_k1("44CB2D685939526D");   // %s
  runtime_data_path = get_runtime_data_path();
  v3(src, 306, v4, runtime_data_path);
  off_78488(src, 448);                          // 清理上一轮临时字符串对象,避免路径拼接过程中产生的临时缓冲区泄漏。
  v6 = off_784B8;
  v7 = decode_hex_xor_k2("421A92D611D1543C");   // %s/1
  runtime_data_path_1 = get_runtime_data_path();
  v6(src, 306, v7, runtime_data_path_1);
  n = strlen(src);                              // 把刚生成的路径内容拷入临时字符串容器,供后续按序号追加文件名后写入 dex_source。
  dest_2 = dest_3;
  dest_1 = dest;
  n_1 = n;
  if ( n <= dest_3 - dest )
  {
    if ( n )
    {
      memmove(dest, src, n);
      dest_2 = dest_3;
      dest_1 = dest;
    }
    dest_4 = &dest_1[n_1];
    if ( dest_4 != dest_2 )
    {
      *dest_4 = *dest_2;
      dest_3 += dest_4 - dest_2;
    }
  }
  else
  {
    v13 = &src[n];
    dest_5 = dest_3;
    if ( dest_3 != dest )
    {
      memmove(dest, src, dest_3 - dest);
      dest_5 = dest_3;
      dest_2 = dest;
    }
    sub_108C0(v31, &src[dest_5 - dest_2], v13);
  }
  off_78488(dest, 448);
  if ( dex_package_count >= 1 )
  {
    dex_package_count_1 = 0;                    // 如果探测结果表明至少存在 1 个 bundle,则开始按 1..N 循环构造每个 dex 的候选文件路径。
    do
    {
      v17 = off_784B8;
      ++dex_package_count_1;
      v18 = decode_hex_xor_k3("8BC161B47ABD40C3");
      (v17)(src, 306, v18, dex_package_count_1);
      build_path_from_template(v29, v31, src);  // 把“根路径 + 第 i 个文件名模板”拼成完整路径,并插入 dex_source 第一组路径记录。
      (sub_24A8C)(a1 + 136, v29);               // dex_source + 0x88 起始的一组路径记录,后面插入的是第一类 bundle 文件路径。
      if ( p != v29 && p )
      {
        if ( v29[0] - p < 0x101u )
          sub_4B940();
        else
          sub_49510(p);
      }
      v19 = off_784B8;
      v20 = decode_hex_xor_k4("2ED869E41050F6DF");// /%d.odex
      (v19)(src, 306, v20, dex_package_count_1);
      build_path_from_template(v27, v31, src);  // 把第二类完整路径插入 dex_source 的另一组路径记录中。
      (sub_24A8C)(a1 + 160, v27);               // 解码按序号生成的第一类文件名模板;随后会把 index 拼进去,再与前面的根路径拼成完整 bundle 路径。
      if ( p_1 != v27 && p_1 )
      {
        if ( v27[0] - p_1 < 0x101u )
          sub_4B940();
        else
          sub_49510(p_1);
      }
    }
    while ( dex_package_count != dex_package_count_1 );
  }
  bundle_provider = create_bundle_provider(1);  // 创建类型 1 的 bundle provider,也就是 DexBundleFileV1;后续 dex_source_read_one_dex 默认优先走这一路读取真实 dex bundle。
  *(a1 + 184) = bundle_provider;                // 把 DexBundleFileV1 provider 挂到 dex_source + 0xB8。上层 read_one_dex 的虚调用最终会落到这里。
  if ( bundle_provider
    && ((**bundle_provider)(bundle_provider, *(&xmmword_78830 + 1)),
        v22 = create_bundle_provider(2),
        (*(a1 + 192) = v22) != 0) )             // 把 VmpBundleFileV1 provider 挂到 dex_source + 0xC0,作为第二套后端实现。
  {
    (**v22)(v22, *(&xmmword_78830 + 1));
    v23 = 1;
  }
  else
  {
    v23 = 0;
  }
  if ( dest != v31 && dest )
  {
    if ( v31[0] - dest < 0x101u )
      sub_4B940();
    else
      sub_49510(dest);
  }
  return v23;
}

大概就是:

已知
第1个 -> xxx1.jar
第2个 -> xxx2.jar
第3个 -> xxx3.jar

组合
/data/xxx/ + xxx1.jar
/data/xxx/ + xxx2.jar

存入dex_source,其实就是在建立索引

create_bundle_provider

// 根据类型创建 bundle provider:1 -> DexBundleFileV1,2 -> VmpBundleFileV1。两者都实现 read(index, &buf, &size) 虚函数。
_QWORD *__fastcall create_bundle_provider(int n2)
{
  _QWORD *v1; // x19

  if ( n2 == 2 )
  {
    v1 = sub_4A07C(8u);	// 分配一个 8 字节的小对象,主要用来保存虚表指针
    construct_vmp_bundle_file_v1(v1);
  }
  else if ( n2 == 1 )
  {
    v1 = sub_4A07C(8u);
    (construct_dex_bundle_file_v1)();
  }
  else
  {
    return 0;
  }
  return v1;
}
// DexBundleFileV1 的按索引读取函数:
// 1. 根据 dex 序号拼出对应的 asset bundle 名;
// 2. 从 APK 中读出该 bundle 的原始字节;
// 3. 调用 dex_bundle_decrypt_uncompress 做真正的解密 + 解压;
// 4. 成功时 a3/a4 返回真实 dex 缓冲区地址与大小。
__int64 __fastcall dex_bundle_read_by_index(__int64 this_, unsigned int dex_index, void **out_buf, _QWORD *out_size)
{
  __int64 read_ok;
  int ret;
  __int64 bundle_size = 0;
  __int64 bundle_buf = 0;
  char asset_path[306];

  format_asset_index_jar_path(dex_index, asset_path, 0x132u);      // 生成第 dex_index 个 bundle 的 asset 名
  read_ok = read_apk_asset_entry_to_memory(asset_path, &bundle_buf, &bundle_size); // 从 APK 读取原始加密 bundle

  if ( (read_ok & 1) == 0 )                                        // 读取失败:进入保护性错误处理,通常不会正常返回
  {
    ...
  }

  ret = dex_bundle_decrypt_uncompress(read_ok, dex_index, bundle_buf, bundle_size, out_buf, out_size); // 还原真实 dex

  if ( bundle_buf )
    off_78460(bundle_buf);                                         // 释放原始加密 bundle 缓冲区

  if ( ret )                                                       // 还原失败:进入保护性错误处理,通常不会正常返回
  {
    ...
  }

  return 1;                                                        // 成功返回,out_buf/out_size 即真实 dex
}

dex_bundle_decrypt_uncompress

// DexBundleFileV1 的核心还原函数:先从 metadata 解析出的 runtime key blob 派生 AES-CTR 材料,解开 bundle 前 0x100 字节,再按预先记录的目标大小做 zlib 解压,最终得到真实 dex 内存。
__int64 __fastcall dex_bundle_decrypt_uncompress(__int64 a1, int a2, __int64 a3, __int64 a4, void **a5, _QWORD *a6)
{
  __int64 minic0_runtime_key_blob; // x0
  __int64 v12; // x0
  void *p; // x23
  unsigned int v14; // w24
  unsigned int v15; // w20
  __int64 v17; // [xsp+0h] [xbp-190h] BYREF
  __int128 dest_[20]; // [xsp+8h] [xbp-188h] BYREF

  minic0_runtime_key_blob = get_minic0_runtime_key_blob();// 取运行时 minic0 key blob。注意这份密钥材料不是硬编码常量,而是 init_metadata_from_assets_md 解密 metadata.md 后解析出来的。
  derive_minic0_aes_material(dest_, minic0_runtime_key_blob + 16, 16);// 从 key blob + 0x10 的 16 字节种子派生当前 bundle 解密所需的 AES 材料(内部是 HKDF-SHA256 -> AES-CTR key/counter 材料)。
  decrypt_minic0_blob(dest_, a3, a3, 256);      // 只对原始 bundle 前 0x100 字节做 AES-CTR 流式解密;这一步完成后,整个输入缓冲区才变成可继续 zlib 解压的格式。
  LODWORD(v12) = get_dex_bundle_uncompressed_size(a2);// 按 dex 索引查询该 bundle 解压后的真实大小;这个大小表来自 metadata.md。
  p = *a5;
  v14 = v12;
  if ( *a5 )
  {
    v12 = v12;
    if ( *a6 >= v12 )
      goto LABEL_6;
  }
  else
  {
    v12 = v12;
  }
  p = off_783F8(v12);                           // 如果调用方没有传入可复用缓冲区,或者现有缓冲区容量不够,这里新申请一块输出内存。
  if ( !p )
  {
    v15 = -1;
    goto LABEL_12;
  }
LABEL_6:
  v17 = v14;                                    // 把预期输出大小写入 v17,作为 uncompress 的目标长度参数。
  if ( uncompress(p, &v17, a3, a4) )            // 对“已修正头部后的 bundle 数据”执行 zlib 解压,输出真实 dex 字节流。
  {
    free(p);                                    // 解压失败时释放本次新申请的输出缓冲区,并返回 -1。
    v15 = -1;
  }
  else
  {
    if ( p != *a5 )
      *a5 = p;                                  // 如果输出缓冲区是本函数新申请的,就把地址回填给调用方。
    v15 = 0;
    *a6 = v14;                                  // 把真实 dex 大小回填给调用方;上层随后会用这块内存创建 DirectByteBuffer 并交给 Java 注入。
  }
LABEL_12:
  sub_29764(dest_);                             // 清理派生出来的临时 AES 材料,避免敏感数据继续留在栈上。
  return v15;
}

现在又多出一个问题,就是metadata怎么来的。注意到他是有初始化的。

init_metadata_from_assets_md

// metadata 初始化入口:从 assets 中读取 metadata.md,先解密 metadata 本体,再解析 dex/vmp 数量、每个 bundle 的解压后大小表,以及后续解密真正 dex 所需的 minic0 runtime key blob。
__int64 init_metadata_from_assets_md()
{
  __int64 v0; // x0
  const char *format; // x19
  int *v2; // x0
  char *v3; // x0
  __int64 v4; // x0
  __int64 v5; // x0
  const char *format_1; // x19
  int *v7; // x0
  char *v8; // x0
  const char *format_2; // x19
  int *v11; // x0
  char *v12; // x0
  const char *format_4; // x19
  int *v14; // x0
  char *v15; // x0
  __int64 v17; // x19
  const char *v18; // x0
  size_t v19; // x0
  int v20; // w0
  const char *format_3; // x19
  int *v22; // x0
  char *v23; // x0
  unsigned int g_dex_bundle_count_1; // [xsp+20h] [xbp-210h]
  unsigned int g_vmp_bundle_count; // [xsp+20h] [xbp-210h]
  unsigned int g_dex_bundle_count; // [xsp+24h] [xbp-20Ch]
  __int64 v28; // [xsp+28h] [xbp-208h]
  __int64 v29; // [xsp+28h] [xbp-208h]
  __int64 v30; // [xsp+28h] [xbp-208h]
  __int64 v31; // [xsp+30h] [xbp-200h]
  __int64 v32; // [xsp+58h] [xbp-1D8h]
  __int64 v33; // [xsp+78h] [xbp-1B8h] BYREF
  _BYTE v34[72]; // [xsp+80h] [xbp-1B0h] BYREF
  char dest_[320]; // [xsp+C8h] [xbp-168h] BYREF

  sub_26AD0(v34);
  v0 = sub_10634(&obj__3);
  if ( (sub_10D58)(v34, v0, 0xFFFFFFFFLL) )
  {
    __android_log_print(7, "XOX", "state=%d", 518);
    n427 = 518;
    format = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
    v2 = __errno();
    v3 = strerror(*v2);
    snprintf(s_, 0x40u, format, "init_meta_data", 82, v3);
    sub_2EFC4(518);
    while ( n427 <= 0 )
      MEMORY[0xDEADDEADDEAD0206] = 3735928559LL;
    sub_198A0(518);
  }
  v4 = format_asset_metadata_md_path();         // 拼出 metadata.md 的 asset 路径。这个文件是整个壳的配置/密钥/尺寸表入口。
  v5 = (sub_1159C)(v34, v4);                    // 在 APK/zip 中定位 metadata.md 条目。
  v32 = v5;
  if ( !v5 )
  {
    __android_log_print(7, "XOX", "state=%d", 517);
    n427 = 517;
    format_1 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
    v7 = __errno();
    v8 = strerror(*v7);
    snprintf(s_, 0x40u, format_1, "init_meta_data", 88, v8);
    sub_2EFC4(517);
    while ( n427 <= 0 )
      MEMORY[0xDEADDEADDEAD0205] = 3735928559LL;
    sub_198A0(517);
  }
  v33 = 0;
  if ( !(sub_11740)(v34, v5, 0, &v33, 0, 0, 0, 0) )// 查询 metadata.md 原始长度。
  {
    __android_log_print(7, "XOX", "state=%d", 517);
    n427 = 517;
    format_2 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
    v11 = __errno();
    v12 = strerror(*v11);
    snprintf(s_, 0x40u, format_2, "init_meta_data", 95, v12);
    sub_2EFC4(517);
    while ( n427 <= 0 )
      MEMORY[0xDEADDEADDEAD0205] = 3735928559LL;
    sub_198A0(517);
  }
  v31 = off_783F8(v33);                         // 分配缓冲区并把 metadata.md 整块读入内存。此时还是加密态 metadata。
  if ( v31 )
  {
    if ( !(sub_11B64)(v34, v32, v31) )
    {
      __android_log_print(7, "XOX", "state=%d", 519);
      n427 = 519;
      format_3 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
      v22 = __errno();
      v23 = strerror(*v22);
      snprintf(s_, 0x40u, format_3, "init_meta_data", 146, v23);
      sub_2EFC4(519);
      while ( n427 <= 0 )
        MEMORY[0xDEADDEADDEAD0207] = 3735928559LL;
      sub_198A0(519);
    }
    v17 = format_asset_metadata_md_path();
    v18 = format_asset_metadata_md_path();
    v19 = strlen(v18);
    derive_minic0_aes_material(dest_, v17, v19);// 用 metadata 文件路径本身作为输入材料之一,派生解 metadata 所需的 AES 材料。
    decrypt_minic0_blob(dest_, v31, v31, v33);  // 对整个 metadata 缓冲区做解密;解完之后,后面才能按固定偏移解析 bundle 数量、大小表和 key blob。
    g_dex_bundle_count = (sub_13CC8)(v31 + 68, 4);// 解析 dex bundle 数量。
    v28 = v31 + 72;
    g_dex_bundle_count_1 = 0;
    ::g_dex_bundle_count = g_dex_bundle_count;
    g_dex_bundle_size_table = off_783F8(4LL * g_dex_bundle_count);// 为 dex bundle 的解压后大小表分配数组。后面按索引查大小就靠这张表。
    while ( g_dex_bundle_count_1 < g_dex_bundle_count )
    {
      *(g_dex_bundle_size_table + 4LL * g_dex_bundle_count_1) = (sub_13CC8)(v28, 4);// 逐项解析每个 dex bundle 的解压后大小。
      v28 += 4;
      ++g_dex_bundle_count_1;
    }
    ::g_vmp_bundle_count = (sub_13CC8)(v28, 4); // 解析 VMP bundle 数量。
    v29 = v28 + 4;
    g_vmp_bundle_size_table = off_783F8(4LL * ::g_vmp_bundle_count);// 为 VMP bundle 的解压后大小表分配数组。
    for ( g_vmp_bundle_count = 0; g_vmp_bundle_count < ::g_vmp_bundle_count; ++g_vmp_bundle_count )
    {
      *(g_vmp_bundle_size_table + 4LL * g_vmp_bundle_count) = (sub_13CC8)(v29, 4);// 逐项解析每个 VMP bundle 的解压后大小。
      v29 += 4;
    }
    v20 = (sub_13CC8)(v29, 1);
    qword_78C18 = v29 + 1;
    v30 = v29 + 1 + v20 + 1;
    (sub_13CC8)(v30, 1);
    qword_78C20 = v30 + 1;
    sub_29764(dest_);
    qword_78C40 = v31;                          // 保存整个已解密 metadata 缓冲区,供后续继续引用。
    g_minic0_runtime_key_blob = v31 + 4;        // 关键:把 metadata 缓冲区 + 4 处记录为 g_minic0_runtime_key_blob。后续 dex/vmp 还原都会先从这里取运行时 key blob。
  }
  else
  {
    n427 = 411;
    format_4 = decode_hex_xor_k0("6A6DC5A76E32EE0B143B");
    v14 = __errno();
    v15 = strerror(*v14);
    snprintf(s_, 0x40u, format_4, "init_meta_data", 104, v15);
    sub_2ED60(411);
  }
  return sub_10BE8(v34);
}

通俗来说就是

metadata 初始化入口:
1. 从 APK 的 assets 中读取 metadata.md;
2. 派生 AES 材料并解密 metadata 本体;
3. 从解密后的 metadata 中解析出:
   - dex bundle 数量
   - dex bundle 解压后大小表
   - vmp bundle 数量
   - vmp bundle 解压后大小表
   - runtime key blob(后续还原 dex/vmp 时使用)
4. 将这些结果保存到全局变量,供后续 bundle 读取与解密流程复用。

算法流程就不在这里讲了,分析到这里应该就很明朗了。大概就是

对 dex/vmp bundle 的还原并非“整块 AES 解密”,而是先从已解密 metadata.md 中取出 minic0_runtime_key_blob,再通过 HKDF-SHA256(salt="83d4e5534f4e330e", info="97f3a59f2ae61f3a")派生 32 字节材料。派生结果的一部分用于构造 AES-128 key schedule,另一部分作为 CTR 初始状态材料。随后程序仅对 bundle 前导区域执行 AES-CTR 流式解密(dex 为 0x100 字节,vmp 为 0x200 字节),最后再结合metadata中记录的目标大小执行zlib解压,得到真实dex。

这里还有一个有意思的

// stub i.dex 安装链:遍历 dex_source 中登记的磁盘 jar 目标路径,检查 data 目录里的占位 jar 是否存在/是否与 asset 里的 i.dex 尺寸和时间戳一致;若缺失或不匹配,则从 assets/<prefix><index+1>.i.dex 重新写入。注意这条链只负责磁盘占位 stub,不负责真实 dex 解密。
__int64 __fastcall install_stub_idex_files_to_data_dir(__int64 a1)
{
  const char *format; // x19
  int *v3; // x0
  char *v4; // x0
  unsigned __int64 v5; // x20
  __int64 n40; // x21
  int *v7; // x28
  __int64 v8; // x27
  int n103; // w8
  unsigned int fd; // w26
  int *v11; // x22
  __int64 v12; // x28
  __int64 v13; // x0
  const char *format_6; // x27
  int *v15; // x0
  char *v16; // x0
  const char *format_7; // x27
  int *v18; // x0
  char *v19; // x0
  __int64 v20; // x26
  int v21; // w0
  const char *format_8; // x26
  int *v23; // x0
  char *v24; // x0
  const char *format_1; // x19
  int *v27; // x0
  char *v28; // x0
  const char *format_2; // x19
  int *v30; // x0
  char *v31; // x0
  const char *format_3; // x19
  int *v33; // x0
  char *v34; // x0
  const char *format_4; // x19
  int *v36; // x0
  char *v37; // x0
  const char *format_5; // x19
  int *v39; // x0
  char *v40; // x0
  _BYTE v41[48]; // [xsp+38h] [xbp-268h] BYREF
  __int64 v42; // [xsp+68h] [xbp-238h]
  __int64 v43; // [xsp+90h] [xbp-210h]
  __int64 v44; // [xsp+B8h] [xbp-1E8h] BYREF
  __int64 v45; // [xsp+C0h] [xbp-1E0h] BYREF
  int v46; // [xsp+C8h] [xbp-1D8h] BYREF
  __int64 v47; // [xsp+D0h] [xbp-1D0h]
  __int64 v48; // [xsp+D8h] [xbp-1C8h]
  __int64 v49; // [xsp+E0h] [xbp-1C0h]
  int v50; // [xsp+E8h] [xbp-1B8h]
  __int64 v51; // [xsp+F0h] [xbp-1B0h]
  int v52; // [xsp+F8h] [xbp-1A8h]
  __int64 v53; // [xsp+100h] [xbp-1A0h]
  char s_[306]; // [xsp+116h] [xbp-18Ah] BYREF

  v46 = -1;
  v47 = 0;
  v48 = -1;
  v49 = 0;
  v50 = -1;
  v51 = -1;
  v52 = -1;
  v53 = 0;
  if ( (sub_10D58)(&v46, *(&xmmword_78830 + 1), 0xFFFFFFFFLL) )
  {
    __android_log_print(7, "XOX", "state=%d", 418);
    n427 = 418;
    format = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
    v3 = __errno();
    v4 = strerror(*v3);
    snprintf(::s_, 0x40u, format, "install_stub_dex_files", 52, v4);
    sub_2EFC4(418);
    while ( n427 < 1 )
      MEMORY[0xDEADDEADDEAD01A2] = 3735928559LL;
    sub_198A0(418);
  }
  if ( *(a1 + 144) != *(a1 + 136) )             // 取 dex_source + 0x88 路径记录表的起止位置;如果没有任何目标路径需要维护,就直接返回。
  {
    v5 = 0;
    n40 = 40;
    v7 = &v46;
    while ( 1 )
    {
      v44 = 0;
      v45 = 0;
      format_asset_index_idex_path(v5, s_, 0x132u);// 按当前索引生成 assets/<prefix><index+1>.i.dex 路径,例如 assets/baiduprotect1.i.dex。
      v8 = (sub_1159C)(v7, s_);                 // 在 APK/zip 中定位该 i.dex asset 条目。找不到就走错误处理。
      if ( !v8 )
      {
        __android_log_print(7, "XOX", "state=%d", 424);
        n427 = 424;
        format_1 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
        v27 = __errno();
        v28 = strerror(*v27);
        snprintf(::s_, 0x40u, format_1, "install_stub_dex_files", 66, v28);
        sub_2EFC4(424);
        while ( n427 < 1 )
          MEMORY[0xDEADDEADDEAD01A8] = 3735928559LL;
        sub_198A0(424);
      }
      if ( ((sub_11740)(v7, v8, 0, &v45, 0, 0, &v44, 0) & 1) == 0 )// 读取 i.dex 条目的 size / mtime 等元信息,后面会拿来和 data 目录里的目标文件做一致性比较。
      {
        __android_log_print(7, "XOX", "state=%d", 425);
        n427 = 425;
        format_2 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
        v30 = __errno();
        v31 = strerror(*v30);
        snprintf(::s_, 0x40u, format_2, "install_stub_dex_files", 70, v31);
        sub_2EFC4(425);
        while ( n427 < 1 )
          MEMORY[0xDEADDEADDEAD01A9] = 3735928559LL;
        sub_198A0(425);
      }
      if ( off_784A8(*(*(a1 + 136) + n40), v41) && *__errno() == 2 )
        break;                                  // 取当前索引对应的 data 目录目标 jar 路径(来自 dex_source + 0x88 路径表),检查目标文件是否存在。若 errno=2 则说明文件缺失,需要重建。
      if ( v45 != v42 || v44 != v43 )           // 若目标文件存在,则继续比较它与 asset i.dex 的关键属性;若 size 或 mtime 不一致,也视为需要重建。
      {
        n103 = 103;
        goto LABEL_17;
      }
      n427 = 101;
LABEL_26:
      ++v5;                                     // 当前索引处理完成,继续下一个 i.dex / 目标 jar 映射。
      n40 += 48;
      if ( v5 >= 0xAAAAAAAAAAAAAAABLL * ((*(a1 + 144) - *(a1 + 136)) >> 4) )
        return sub_10BE8(&v46);
    }
    n103 = 102;
LABEL_17:
    n427 = n103;                                // 进入重建/刷新分支:置 dirty 标志,说明磁盘占位 jar 已发生变化。
    *(a1 + 200) = 1;
    off_784A0(*(*(a1 + 136) + n40));            // 删除旧的目标 jar 路径。
    off_784A0(*(*(a1 + 160) + n40));            // 删除与该 jar 配套的另一条缓存/odex 路径(来自 dex_source + 0xA0 路径表),避免旧优化产物残留。
    fd = off_78490(*(*(a1 + 136) + n40), 66, 420);// 以可写方式新建目标 jar 文件,准备把 asset i.dex 内容写入 data 目录。
    if ( (fd & 0x80000000) != 0 )
    {
      __android_log_print(7, "XOX", "state=%d", 417);
      n427 = 417;
      format_3 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
      v33 = __errno();
      v34 = strerror(*v33);
      snprintf(::s_, 0x40u, format_3, "install_stub_dex_files", 105, v34);
      sub_2EFC4(417);
      while ( n427 < 1 )
        MEMORY[0xDEADDEADDEAD01A1] = 3735928559LL;
      sub_198A0(417);
    }
    v11 = v7;
    v12 = off_783F8(v45);                       // 按 i.dex 条目长度分配临时缓冲区。
    if ( !v12 )
    {
      __android_log_print(7, "XOX", "state=%d", 411);
      n427 = 411;
      format_4 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
      v36 = __errno();
      v37 = strerror(*v36);
      snprintf(::s_, 0x40u, format_4, "install_stub_dex_files", 111, v37);
      sub_2EFC4(411);
      while ( n427 < 1 )
        MEMORY[0xDEADDEADDEAD019B] = 3735928559LL;
      sub_198A0(411);
    }
    if ( ((sub_11B64)(v11, v8, v12) & 1) == 0 ) // 把 asset 里的 i.dex 原样读入内存缓冲区。这里仍然不是解密真实 dex,只是在搬运 stub 文件。
    {
      __android_log_print(7, "XOX", "state=%d", 402);
      n427 = 402;
      format_5 = decode_hex_xor_k1("3A9D5E527C4C0F5744CB");
      v39 = __errno();
      v40 = strerror(*v39);
      snprintf(::s_, 0x40u, format_5, "install_stub_dex_files", 126, v40);
      sub_2EFC4(402);
      while ( n427 < 1 )
        MEMORY[0xDEADDEADDEAD0192] = 3735928559LL;
      sub_198A0(402);
    }
    off_785A0(fd, 0);
    v13 = off_78570(fd, v12, v45);              // 把缓冲区中的 i.dex 内容完整写入目标 jar 文件。
    if ( v45 != v13 )
    {
      n427 = 409;
      format_6 = decode_hex_xor_k0("6A6DC5A76E32EE0B143B");
      v15 = __errno();
      v16 = strerror(*v15);
      snprintf(::s_, 0x40u, format_6, "install_stub_dex_files", 120, v16);
      sub_2ED60(409);
    }
    off_78460(v12);
    if ( fsync(fd) )                            // fsync 目标文件,确保 stub 已落盘。
    {
      n427 = 410;
      format_7 = decode_hex_xor_k0("6A6DC5A76E32EE0B143B");
      v18 = __errno();
      v19 = strerror(*v18);
      snprintf(::s_, 0x40u, format_7, "install_stub_dex_files", 135, v19);
      sub_2ED60(410);
    }
    off_78498(fd);                              // 关闭目标文件句柄。
    v20 = *(*(a1 + 136) + n40);                 // 把目标文件的时间戳同步为 asset 记录的时间值,后续可据此快速判断是否需要重建。
    v21 = off_784B0(0);
    v7 = v11;
    if ( (loc_1AB98)(v20, v21, v44) )
    {
      n427 = 416;
      format_8 = decode_hex_xor_k0("6A6DC5A76E32EE0B143B");
      v23 = __errno();
      v24 = strerror(*v23);
      snprintf(::s_, 0x40u, format_8, "install_stub_dex_files", 142, v24);
      sub_2ED60(416);
    }
    goto LABEL_26;
  }
  return sub_10BE8(&v46);
}
// 格式化 i.dex asset 路径:assets/<prefix><index+1>.i.dex。注意这里生成的是磁盘占位 stub 资源名,不是真实 dex payload 的 .jar bundle 名。 
char *__fastcall format_asset_index_idex_path(int a1, char *s, size_t maxlen)
{
  const char *format; // x0

  format = decode_hex_xor_k1("00CB5E0D2D4A7D48129D49463017360819");
  snprintf(s, maxlen, format, *(&xmmword_789E0 + 1), (a1 + 1));
  return s;
}

简单来说他是个占位壳,是一个空壳,主要用于校验兼容等。

最后附上解密的代码(ai牛逼)

#!/usr/bin/env python3
"""
Extract real dex payloads from Baidu/Sagittarius protect assets.

Expected layout:
  assets/baiduprotect.md
  assets/baiduprotect1.jar ... baiduprotectN.jar
  assets/baiduprotect1.i.dex ... baiduprotectN.i.dex

The native code does this at runtime:
  1. Decrypt assets/<prefix>.md with key material derived from its asset path.
  2. Parse real dex bundle count and uncompressed sizes from metadata.
  3. For each assets/<prefix>N.jar:
       decrypt only the first 256 bytes with minic0 AES-CTR,
       then zlib-uncompress the whole buffer into a real dex.
  4. Install assets/<prefix>N.i.dex to data-dir N.jar as a sparse stub.
"""

from __future__ import annotations

import argparse
import hashlib
import hmac
import struct
import zlib
import zipfile
from pathlib import Path

from Crypto.Cipher import AES


SALT = b"83d4e5534f4e330e"
INFO = b"97f3a59f2ae61f3a"


def hkdf_sha256(ikm: bytes, length: int = 32) -> bytes:
    """Native derive_minic0_aes_material uses HKDF-SHA256 with fixed salt/info."""
    prk = hmac.new(SALT, ikm, hashlib.sha256).digest()
    out = b""
    t = b""
    counter = 1
    while len(out) < length:
        t = hmac.new(prk, t + INFO + bytes([counter]), hashlib.sha256).digest()
        out += t
        counter += 1
    return out[:length]


def minic0_crypt_prefix(data: bytes, ikm: bytes, prefix_len: int) -> bytes:
    """
    Mirror decrypt_minic0_blob.

    Important detail: the native CTR block uses HKDF output[16:28] plus four
    zero bytes, not the full output[16:32].
    """
    material = hkdf_sha256(ikm, 32)
    key = material[:16]
    counter_block = material[16:28] + b"\x00\x00\x00\x00"
    cipher = AES.new(
        key,
        AES.MODE_CTR,
        nonce=b"",
        initial_value=int.from_bytes(counter_block, "big"),
    )
    return cipher.decrypt(data[:prefix_len]) + data[prefix_len:]


def parse_metadata(blob: bytes) -> tuple[list[int], list[int], bytes]:
    """
    Parse the decrypted assets/<prefix>.md layout recovered from init_meta_data.

    Layout used here:
      +0x00  magic "md01"
      +0x04  key blob area
      +0x44  u32 dex_count
      +0x48  u32[dex_count] dex uncompressed sizes
             u32 vmp_count
             u32[vmp_count] vmp uncompressed sizes

    Bundle decryption later uses get_minic0_runtime_key_blob()+0x10,
    i.e. decrypted_md[0x14:0x24], as HKDF input.
    """
    if blob[:4] != b"md01":
        raise ValueError(f"unexpected metadata magic: {blob[:4]!r}")

    pos = 0x44
    dex_count = struct.unpack_from("<I", blob, pos)[0]
    pos += 4
    dex_sizes = list(struct.unpack_from("<" + "I" * dex_count, blob, pos))
    pos += 4 * dex_count

    vmp_count = struct.unpack_from("<I", blob, pos)[0]
    pos += 4
    vmp_sizes = list(struct.unpack_from("<" + "I" * vmp_count, blob, pos))

    bundle_ikm = blob[0x14:0x24]
    return dex_sizes, vmp_sizes, bundle_ikm


def read_stub_info(path: Path) -> tuple[int, int]:
    """Return (classes.dex size, nonzero byte count) for an i.dex zip."""
    with zipfile.ZipFile(path) as zf:
        data = zf.read("classes.dex")
    return len(data), sum(1 for byte in data if byte)


def extract(assets_dir: Path, prefix: str, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)

    metadata_path = f"assets/{prefix}.md".encode()
    metadata_enc = (assets_dir / f"{prefix}.md").read_bytes()
    metadata = minic0_crypt_prefix(metadata_enc, metadata_path, len(metadata_enc))
    dex_sizes, vmp_sizes, bundle_ikm = parse_metadata(metadata)

    (out_dir / f"{prefix}.md.dec").write_bytes(metadata)
    print(f"[metadata] magic={metadata[:4]!r} dex_count={len(dex_sizes)} vmp_count={len(vmp_sizes)}")
    print(f"[metadata] dex_sizes={dex_sizes}")
    print(f"[metadata] bundle_ikm={bundle_ikm.hex()}")

    for index, expected_size in enumerate(dex_sizes, start=1):
        asset = assets_dir / f"{prefix}{index}.jar"
        encrypted = asset.read_bytes()
        compressed = minic0_crypt_prefix(encrypted, bundle_ikm, 256)
        dex = zlib.decompress(compressed)
        if len(dex) != expected_size:
            raise ValueError(
                f"{asset.name}: decompressed size {len(dex)} != metadata {expected_size}"
            )
        if not dex.startswith(b"dex\n035\x00"):
            raise ValueError(f"{asset.name}: output is not a dex035 file")

        out_path = out_dir / f"{prefix}{index}.dex"
        out_path.write_bytes(dex)
        digest = hashlib.sha256(dex).hexdigest()
        print(f"[dex {index}] {asset.name} -> {out_path.name} size={len(dex)} sha256={digest}")

        stub = assets_dir / f"{prefix}{index}.i.dex"
        if stub.exists():
            stub_size, nonzero = read_stub_info(stub)
            print(f"[stub {index}] {stub.name} classes.dex size={stub_size} nonzero={nonzero}")

def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--assets",
        type=Path,
        default=Path(__file__).resolve().parents[2] / "assets",
        help="APK assets directory",
    )
    parser.add_argument("--prefix", default="baiduprotect")
    parser.add_argument(
        "--out",
        type=Path,
        default=Path("extracted_baiduprotect_dex"),
        help="Output directory for decrypted dex files",
    )
    args = parser.parse_args()
    extract(args.assets, args.prefix, args.out)
if __name__ == "__main__":
    main()


里面还有一些反调试之类的就不分析了。

这个分析过程AI帮助了比较多,但是在这种大型的分析过程中发现,上下文容易爆,一旦压缩上下文立马变蠢,还需要人为来给方向,但不可否认AI还是太权威了。

如有侵权联系我删除,读者因违反相关法律法规而引发的任何危害网络安全、侵犯他人权益的行为,均与本文作者无关,相关法律责任由行为人自行承担。



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2天前 被ROSE0编辑 ,原因:
收藏
免费 1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回