-
-
[原创]记一次某街区app百度加固分析
-
发表于: 2天前 837
-
java层分析
这种app一般都是有壳存在的,第一步先分析这个壳,先看xml文件第一个启动的activity是com.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;
}把全局变量保存下来一遍后面读取asset、shared_prefs和patch真实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;
}简单来说这就是一个为了给x86、x86_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) {
}
}
}
}按设备ABI从asset释放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.mInitialApplication、mAllApplications、ContextImpl.outerContext、LoadedApk.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.so的art::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还是太权威了。
如有侵权联系我删除,读者因违反相关法律法规而引发的任何危害网络安全、侵犯他人权益的行为,均与本文作者无关,相关法律责任由行为人自行承担。