前言

只做学习记录的备份,来自于

[原创]Android漏洞之战(11)——整体加壳原理和脱壳技巧详解

《安卓逆向这档事》

Android App启动流程

image-20250928213056553

Zygote进程fork的第一个进程是:SystemServer进程,SystemServer进程主要进行以下的工作

image-20250928213032187

Android APP安装

1
2
3
4
· 系统启动时安装,没有安装界面
· 第三方应用安装,有安装界面,也是我们最熟悉的方式
· ADB命令安装,没有安装界面
· 通过各类应用市场安装,没有安装界面

有这四种安装模式

image-20250928215538686

但是都是通过PackgeManagerService服务来完成应用程序的安装的,而PackgeManagerService服务会与installed服务通信,发送具体的指令来执行应用程序的安装、卸载等工作

1
2
3
4
5
6
public static final IPackageManager main(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
PackageManagerService m = new PackageManagerService(context, installer, factoryTest, onlyCore);
ServiceManager.addService("package", m);
return m;
}

这里的PackgeManagerService其实就是上述的PackageManager服务注册的

IPackageManager函数是Android系统中包管理服务的初始化入口,初始化PackageManagerService,将服务加入到系统服务管理器中,然后返回PackgeManagerService的实例供用户使用

应用程序在安装时涉及到如下几个重要目录:

image-20250928220334806

App的安装流程是由PackageManagerService完成的,此前SystemServer就已经启动了一个更重要的服务ActivityManagerServiceActivityManagerService其中一个重要的作用就是在启动完PackageManagerService之后将Launcher进程启动起来

Launcher启动流程

启动Launcher的入口为ActivityManagerServicesystemReady函数

1
2
3
4
5
6
7
8
9
10
11
12
13
private void startOtherServices() {
...
mActivityManagerService.systemReady(new Runnable() {
@Override
public void run() {
Slog.i(TAG, "Making services ready");
mSystemServiceManager.startBootPhase(
SystemService.PHASE_ACTIVITY_MANAGER_READY);

...
}
...
}

startOtherServices函数中,会调用ActivityManagerServicesystemReady函数:

1
2
3
4
5
6
7
8
public void systemReady(final Runnable goingCallback) {
...
synchronized (this) {
...
mStackSupervisor.resumeFocusedStackTopActivityLocked();
mUserController.sendUserSwitchBroadcastsLocked(-1, currentUserId);
}
}

systemReady函数中调用了ActivityStackSupervisorresumeFocusedStackTopActivityLocked函数:

1
2
3
4
5
6
7
8
9
10
11
boolean resumeFocusedStackTopActivityLocked(
ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {
if (targetStack != null && isFocusedStack(targetStack)) {
return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions); //1
}
final ActivityRecord r = mFocusedStack.topRunningActivityLocked();
if (r == null || r.state != RESUMED) {
mFocusedStack.resumeTopActivityUncheckedLocked(null, null);
}
return false;
}

在注释1处会调用ActivityStackresumeTopActivityUncheckedLocked函数,ActivityStack对象是用来描述Activity堆栈的,resumeTopActivityUncheckedLocked函数如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {
if (mStackSupervisor.inResumeTopActivity) {
// Don't even start recursing.
return false;
}
boolean result = false;
try {
// Protect against recursion.
mStackSupervisor.inResumeTopActivity = true;
if (mService.mLockScreenShown == ActivityManagerService.LOCK_SCREEN_LEAVING) {
mService.mLockScreenShown = ActivityManagerService.LOCK_SCREEN_HIDDEN;
mService.updateSleepIfNeededLocked();
}
result = resumeTopActivityInnerLocked(prev, options);//1
} finally {
mStackSupervisor.inResumeTopActivity = false;
}
return result;
}

调用了resumeTopActivityInnerLocked函数:

1
2
3
4
5
6
private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
...
return isOnHomeDisplay() &&
mStackSupervisor.resumeHomeStackTask(returnTaskType, prev, "prevFinished");
...
}

调用ActivityStackSupervisorresumeHomeStackTask函数,

1
2
3
4
5
6
7
8
boolean resumeHomeStackTask(int homeStackTaskType, ActivityRecord prev, String reason) {
...
if (r != null && !r.finishing) {
mService.setFocusedActivityLocked(r, myReason);
return resumeFocusedStackTopActivityLocked(mHomeStack, prev, null);
}
return mService.startHomeActivityLocked(mCurrentUser, myReason);//1
}

调用了ActivityManagerServicestartHomeActivityLocked函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
boolean startHomeActivityLocked(int userId, String reason) {
if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL
&& mTopAction == null) {//1
return false;
}
Intent intent = getHomeIntent();//2
ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);
if (aInfo != null) {
intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
aInfo = new ActivityInfo(aInfo);
aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);
ProcessRecord app = getProcessRecordLocked(aInfo.processName,
aInfo.applicationInfo.uid, true);
if (app == null || app.instrumentationClass == null) {//3
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
mActivityStarter.startHomeActivityLocked(intent, aInfo, reason);//4
}
} else {
Slog.wtf(TAG, "No home screen found for " + intent, new Throwable());
}

return true;
}

注释1处的mFactoryTest代表系统的运行模式,系统的运行模式分为三种,分别是非工厂模式、低级工厂模式和高级工厂模式mTopAction则用来描述第一个被启动Activity组件的Action,它的值为Intent.ACTION_MAIN

因此注释1的代码意思就是mFactoryTestFactoryTest.FACTORY_TEST_LOW_LEVEL(低级工厂模式)并且mTopAction=null时,直接返回false。注释2处的getHomeIntent函数如下所示。

1
2
3
4
5
6
7
8
9
Intent getHomeIntent() {
Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
intent.setComponent(mTopComponent);
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
intent.addCategory(Intent.CATEGORY_HOME);
}
return intent;
}

getHomeIntent函数中创建了Intent,并将mTopActionmTopData传入。mTopAction的值为Intent.ACTION_MAIN,并且如果系统运行模式不是低级工厂模式则将intentCategory设置为Intent.CATEGORY_HOME

我们再回到ActivityManagerServicestartHomeActivityLocked函数

假设系统的运行模式不是低级工厂模式,在注释3处判断符合ActionIntent.ACTION_MAINCategoryIntent.CATEGORY_HOME的应用程序是否已经启动,如果没启动则调用注释4的方法启动该应用程序。

这个被启动的应用程序就是Launcher,因为LauncherManifest文件中的intent-filter标签匹配了ActionIntent.ACTION_MAINCategoryIntent.CATEGORY_HOME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.launcher3">
<uses-sdk android:targetSdkVersion="23" android:minSdkVersion="16"/>
...
<application
...
<activity
android:name="com.android.launcher3.Launcher"
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
android:stateNotNeeded="true"
android:theme="@style/Theme"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="nosensor"
android:configChanges="keyboard|keyboardHidden|navigation"
android:resumeWhilePausing="true"
android:taskAffinity=""
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MONKEY"/>
</intent-filter>
</activity>
...
</application>
</manifest>

之后Launcher就会开始执行onCreate

App启动流程

应用程序Launcher在启动过程中会请求PackageManagerService返回系统中已经安装的应用程序的信息,并将这些信息封装成一个快捷图标列表显示在系统屏幕上,这样用户可以通过点击这些快捷图标来启动相应的应用程序

image-20250928223841904

  1. 点击桌面APP图标时,Launcher的startActivity()方法,通过Binder通信,调用system_server进程中AMS服务的startActivity方法,发起启动请求
  2. system_server进程接收到请求后,向Zygote进程发送创建进程的请求
  3. Zygote进程fork出App进程,并执行ActivityThread的main方法,创建ActivityThread线程,初始化MainLooper,主线程Handler,同时初始化ApplicationThread用于和AMS通信交互
  4. App进程,通过Binder向sytem_server进程发起attachApplication请求,这里实际上就是APP进程通过Binder调用sytem_server进程中AMS的attachApplication方法,AMS的attachApplication方法的作用是将ApplicationThread对象与AMS绑定
  5. system_server进程在收到attachApplication的请求,进行一些准备工作后,再通过binder IPC向App进程发送handleBindApplication请求(初始化Application并调用onCreate方法)和scheduleLaunchActivity请求(创建启动Activity)
  6. App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送BIND_APPLICATION和LAUNCH_ACTIVITY消息,这里注意的是AMS和主线程并不直接通信,而是AMS和主线程的内部类ApplicationThread通过Binder通信,ApplicationThread再和主线程通过Handler消息交互。
  7. 主线程在收到Message后,创建Application并调用onCreate方法,再通过反射机制创建目标Activity,并回调Activity.onCreate()等方法
  8. 到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染后显示APP主界面

这里我们就进入了加壳中十分重要的地方ActivityTread,之后就是调用各种函数来启动APP,过程如下

1
2
App的运行流程是
初始化————>Application的构造函数————>Application.attachBaseContext()————>Application.onCreate()函数

最后才会进入MainActivity中的attachBaseContext函数、onCreate函数

所以加壳厂商要在程序正式执行前,也就是上面的流程中进行动态加载和类加载器的修正,这样才能对加密的dex进行释放,而一般的厂商往往选择在Application中的attachBaseContext或onCreate函数进行

整体加壳原理详解

深入理解类加载器和动态加载

在学习如何对App加上一层外壳之前,需要详细了解类加载器和动态加载的机制

类加载器

Android中的类加载器机制与JVM一样遵循双亲委派模式

双亲委派模式

代码解释:

  1. 首先检查该类是否已被加载

  2. 若未加载,则委托给父类的Loader加载,如果没有父类则委托给BootClassLoader尝试加载

  3. BootClassLoader无法加载,则PathClassLoader尝试从应用的dex文件中加载

  4. 若需要动态加载外部dex,则使用DexClassLoader

image-20250929131650468

双亲委派模式加载

image-20250929132247135

我们要加载一个class文件,我们定义了一个CustomerClassLoader类加载器:
(1)首先会判断自己的CustomerClassLoader否加载过,如果加载过直接返回,
(2)如果没有加载过则会调用父类PathClassLoader去加载,该父类同样会判断自己是否加载过,如果没有加载过则委托给父类BootClassLoader去加载,
(3)这个BootClassLoader是顶级classLoader,同样会去判断自己有没有加载过,如果也没有加载过则会调用自己的findClass(name)去加载,
(4)如果顶级BootClassLoader加载失败了,则会把加载这个动作向下交还PathClassLoader,
(5)这个PathClassLoader也会尝试去调用findClass(name);去加载,如果加载失败了,则会继续向下交还给CustomClassLoader来完成加载,这整个过程感觉是一个递归的过程,逐渐往上然后有逐渐往下,直到加载成功
其实这个String.class在系统启动的时候已经被加载了,我们自己定义一个CustomerClassLoader去加载,其实也是父类加载的

Android中类加载机制

Android的类加载机制和JVM一样遵循双亲委派模式,在dalvik/art启动时将所有Java基本类和Android系统框架的基本类加载进来,预加载的类记录在/frameworks/base/config/preloaded-classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
android.R$styleable
android.accessibilityservice.AccessibilityServiceInfo$1
android.accessibilityservice.AccessibilityServiceInfo
android.accessibilityservice.IAccessibilityServiceClient$Stub$Proxy
android.accessibilityservice.IAccessibilityServiceClient$Stub
android.accessibilityservice.IAccessibilityServiceClient
android.accounts.AbstractAccountAuthenticator$Transport
android.accounts.AbstractAccountAuthenticator
android.accounts.Account$1
android.accounts.Account
...

java.lang.Short
java.lang.StackOverflowError
java.lang.StackTraceElement
java.lang.StrictMath
java.lang.String$1
java.lang.String$CaseInsensitiveComparator
java.lang.String
java.lang.StringBuffer
java.lang.StringBuilder
java.lang.StringFactory
java.lang.StringIndexOutOfBoundsException
java.lang.System$PropertiesWithNonOverrideableDefaults
java.lang.System
java.lang.Thread$1
...

这些类只需要在Zygote进程启动时加载一遍就可以了,后续每一个APP或Android运行时环境的进程,都是从Zygote中fork出来,天然保留了加载过的类缓存

image-20250929133634773

image-20250929134222740

Android中的ClassLoader类型分为系统ClassLoader自定义ClassLoader。其中系统ClassLoader包括3种是BootClassLoaderDexClassLoaderPathClassLoader

  1. BootClassLoader:Android平台上所有Android系统启动时会使用BootClassLoader来预加载常用的类
  2. BaseDexClassLoader:实际应用层类文件的加载,而真正的加载委托给pathList来完成
  3. DexClassLoader:可以加载dex文件以及包含dex的压缩文件(apk,dex,jar,zip),可以安装一个未安装的apk文件,一般为自定义类加载器
  4. PathClassLoader:可以加载系统类和应用程序的类,通常用来加载已安装的apk的dex文件

Android 提供的原生加载器叫做基础类加载器,包括:BootClassLoader,PathClassLoader,DexClassLoader,InMemoryDexClassLoader(Android 8.0 引入)

DelegateLastClassLoader(Android 8.1 引入)

BootClassLoader

启动类加载器,用于加载 Zygote 进程已经预加载的基本类,可以推测它只需从缓存中加载。这是基类 ClassLoader 的一个内部类,是包访问权限,所以应用程序无权直接访问

BootClassLoader没有父加载器,在缓存取不到类是直接调用自己的findClass()方法
findClass()方法调用Class.classForName()方法,而ZygoteInit.preloadClasses()中,加载基本类是Class.forName()

无论是系统类加载器(PathClassLoader)还是自定义的类加载器(DexClassLoader),最顶层的祖先加载器默认是 BootClassLoader,与 JVM 一样,保证了基本类的类型安全

Class文件加载:
  1. 通过Class.forName()方法动态加载
  2. 通过ClassLoader.loadClass()方法动态加载

类的加载分为3个步骤:1.装载(Load),2.链接(Link),3.初始化(Intialize)

image-20250929140247689

类加载时机:

1.隐式加载:

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射Class.forName("android.app.ActivityThread")
  5. 初始化一个类的子类(会首先初始化子类的父类)

2.显示加载:

  1. 使用LoadClass()加载
  2. 使用forName()加载

Class.forName 和 ClassLoader.loadClass加载有何不同:

  1. ClassLoader.loadClass也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作)
  2. Class.forName这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作
PathClassLoader

主要用于系统和app的类加载器,其中optimizedDirectorynull, 采用默认目录/data/dalvik-cache/

1
PathClassLoader 是作为应用程序的系统类加载器,也是在 Zygote 进程启动的时候初始化的(基本流程为:ZygoteInit.main() -> ZygoteInit.forkSystemServer() -> ZygoteInit.handleSystemServerProcess() -> ZygoteInit.createPathClassLoader()。在预加载基本类之后执行),所以每一个 APP 进程从 Zygote 中 fork 出来之后都自动携带了一个 PathClassLoader,它通常用于加载 apk 里面的 .dex 文件
DexClassLoader

我们可以发现DexClassLoaderPathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了自己的构造函数,没有额外的实现

区别:
DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件的地方,所以可以利用DexClassLoader实现动态加载

BaseDexClassLoader
1
2
3
4
5
6
7
8
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList; //记录dex文件路径信息

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
}
  • dexPath: 包含目标类或资源的apk/jar列表;当有多个路径则采用:分割;
  • optimizedDirectory: 优化后dex文件存在的目录, 可以为null;
  • libraryPath: native库所在路径列表;当有多个路径则采用:分割;
  • ClassLoader:父类的类加载器

BaseDexClassLoader会初始化dexPathList,收集dex文件和Native文件动态库

………(https://bbs.kanxue.com/thread-271538.htm)

最后,Android类加载详细流程:

image-20250929143026114

假设我们要加载一个类com.example.MyActivity:

  1. 应用通过Custom ClassLoader请求加载类(图中步骤1)
  2. Custom ClassLoader调用loadClass(),遵循双亲委派,先委托给PathClassLoader(步骤3)
  3. PathClassLoader委托给BootClassLoader(步骤5)
  4. BootClassLoader无法加载该类,返回给PathClassLoader(步骤8)
  5. PathClassLoader调用findClass()方法(步骤9)
  6. findClass()调用DexPathList的findClass()方法
  7. DexPathList遍历dexElements数组:
  8. 对每个Element调用findClass()
  9. Element调用DexFile的loadClassBinaryName()方法
  10. 如果在某个DexFile中找到类,立即返回
  11. 如果PathClassLoader未找到,返回给Custom ClassLoader(步骤11)
  12. Custom ClassLoader调用自己的findClass()(步骤12)
  13. 同样通过DexPathList查找类
  14. 如果找到,返回类对象(步骤14)
  15. 如果未找到,抛出ClassNotFoundException(步骤15)

整体加壳原理

Dex整体加壳可以理解为在加密的源Apk程序外面有套上了一层外壳,简单过程为:

image-20250929130703014

image-20250929130743024

加壳例子:

image-20250929143926144

我们很明显看见,除了一个代理类Application,其他相关的代码信息都无法发现

在代理类中反射调用了一些方法,很显然我们解析出的结果都无法查找,很明显就说明在Application.attchBaseContext()Application.onCreate()中必须要完成对源加密的dex动态加载和解密

App加载应用解析时流程如下:

  1. BootClassLoader加载系统核心库
  2. PathClassLoader加载APP自身dex
  3. 进入APP自身组件,解析AndroidManifest.xml,然后查找Application代理
  4. 调用声明ApplicationattachBaseContext()对源程序进行动态加载或解密
  5. 调用声明ApplicationonCreate()对源程序进行动态加载或解密
  6. 进入MainActivity中的attachBaseContext(),然后进入onCreate()函数,执行源程序代码

上面我们已经很清晰的了解了壳加载的流程,我们很明显的意识到一个问题,我们从头到尾都是用PathClassLoader来加载dex

简单整体加壳

我们要想动态加载dex文件必须使用自定义的DexClassLoader,但是直接使用DexClassLoader进行加载,会报异常,是因为DexClassLoader加载的类是没有组件生命周期的,即DexClassLoader即使通过对APK的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常

所以我们要想使用DexClassLoader进行动态加载dex,我们需要进行类加载器的修正

主要有两种方案:

  1. 替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoaderparent为系统组件加载器
  2. 打破原有的双亲委派关系,在系统组件类加载器PathClassLoaderBootClassLoader的中间插入我们自己的DexClassLoader
类加载器替换

LoadedApk主要负责加载一个Apk程序,其中有一个字段mclassLoader,我们通过反射来使用我们的DexClassLoader将其替换,就能让我们的DexClassLoader拥有生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void replaceClassLoader(Context context,ClassLoader dexClassLoader){
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
try {
//1.获取ActivityThread实例
Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread");
Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");
Object activityThreadObj = currentActivityThread.invoke(null);
//2.通过反射获得类加载器
//final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
Field mPackagesField = ActivityThread.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
//3.拿到LoadedApk
ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);
String packagename = context.getPackageName();
WeakReference wr = (WeakReference) mPackagesObj.get(packagename);
Object LoadApkObj = wr.get();
//4.拿到mclassLoader
Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
Object mClassLoader =mClassLoaderField.get(LoadApkObj);
Log.e("mClassLoader",mClassLoader.toString());
//5.将系统组件ClassLoader给替换
mClassLoaderField.set(LoadApkObj,dexClassLoader);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
类加载器插入

类加载器刚拿到类,并不会直接进行加载,而是先判断自己是否加载,如果没有加载则给自己的父类,父类再给父类,所以我们让DexClassLoader成为PathClassLoader的父类

这样就可以解决DexClassLoader生命周期的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void replaceClassLoader(Context context, ClassLoader dexClassLoader){
//将pathClassLoader父节点设置为DexClassLoader
ClassLoader pathClassLoaderobj = context.getClassLoader();
Class<ClassLoader> ClassLoaderClass = ClassLoader.class;
try {
Field parent = ClassLoaderClass.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(pathClassLoaderobj,dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

}

完成壳加载器的修正后,我们就可以正常的加载dex了

之后只需要编写Application代理的onCreateattachBaseContext函数即可

脱壳方法

常用fart、frida-dexdump(要先过frida)

以后补,还在学

Frida基础知识

操作模式:

操作模式 描述 优点 主要用途
CLI(命令行)模式 通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 便于直接注入和操作 在较小规模的操作或者需求比较简单的场景中使用
RPC模式 使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗 在大规模调用中更加普遍,特别是对于复杂数据处理的需求

注入模式与启动命令:

注入模式 描述 命令或参数 优点 主要用途
Spawn模式 将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App 在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App 适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为 当需要监控App从启动开始的所有行为时使用
Attach模式 在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作 在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App 适合于已经运行的App,不会重新启动App,对用户体验影响较小 在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用

Spawn模式

1
frida -U -f 进程名 -l hook.js

attach模式 :

1
frida -U 进程名 -l hook.js

基础语法

API名称 描述
Java.use(className) 获取指定的Java类并使其在JavaScript代码中可用。
Java.perform(callback) 确保回调函数在Java的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个Java对象转换成另一个Java类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有Java类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有Java类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

日志输出语法区别

日志方法 描述 区别
console.log() 使用JavaScript直接进行日志打印 多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。
send() Frida的专有方法,用于发送数据或日志到外部Python脚本 多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。

Frida常用API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义一个名为hookTest1的函数
function hookTest1(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("类名");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.method.implementation = function(a, b){
//将参数a和b的值改为123和456。
a = 123;
b = 456;
//调用修改过的"method"方法,并将返回值存储在`retval`变量中
var retval = this.method(a, b);
//在控制台上打印参数a,b的值以及"method"方法的返回值
console.log(a, b, retval);
//返回"method"方法的返回值
return retval;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// .overload()
// .overload('自定义参数')
// .overload('int')
function hookTest2(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//overload定义重载函数,根据函数的参数类型填
utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){
b = "aaaaaaaaaa";
this.Inner(a,b);
console.log(b);
}
}
1
2
3
4
5
6
7
8
9
function hookTest3(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的构造函数的实现,$init表示构造函数
utils.$init.overload('java.lang.String').implementation = function(str){
console.log(str);
str = "52";
this.$init(str);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hookTest5(){
Java.perform(function(){
//静态字段的修改
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的静态字段"flag"的值
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
//非静态字段的修改
//使用`Java.choose()`枚举类的所有实例
Java.choose("com.zj.wuaipojie.Demo", {
onMatch: function(obj){ //通过onMatch回调函数访问每个找到的实例,obj代表Frida找到的每一个实例对象
//修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){

}
});
});

}
1
2
3
4
5
6
7
8
9
10
11
function hookTest6(){
Java.perform(function(){
//内部类
var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("eeeeeeee");
}

});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest7(){
Java.perform(function(){
//枚举所有的类与类的所有方法,异步枚举
Java.enumerateLoadedClasses({
onMatch: function(name,handle){ //name和handle参数是由Frida框架自动传入的,分别代表类名和类句柄
//过滤类名
if(name.indexOf("com.zj.wuaipojie.Demo") !=-1){
console.log(name);
var clazz =Java.use(name);
console.log(clazz);
var methods = clazz.class.getDeclaredMethods();
console.log(methods);
}
},
onComplete: function(){}
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest8(){
Java.perform(function(){
var Demo = Java.use("com.zj.wuaipojie.Demo");
//getDeclaredMethods枚举所有方法
var methods =Demo.class.getDeclaredMethods();
for(var j=0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
for(var k=0; k<Demo[methodName].overloads.length;k++){
Demo[methodName].overloads[k].implementation = function(){
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
return this[methodName].apply(this,arguments);
}
}
}
})
}
1
2
var ClassName=Java.use("com.zj.wuaipojie.Demo"); 
ClassName.privateFunc("传参");
1
2
3
4
5
6
7
8
9
10
11
12
var ret = null;
Java.perform(function () {
Java.choose("com.zj.wuaipojie.Demo",{ //要hook的类
onMatch:function(instance){
ret=instance.privateFunc("aaaaaaa"); //要hook的方法
},
onComplete:function(){
//console.log("result: " + ret);
}
});
})
//return ret;

Process、Module、Memory基础

1.Process

Process 对象代表当前被Hook的进程,能获取进程的信息,枚举模块,枚举范围等

API 含义
Process.id 返回附加目标进程的 PID
Process.isDebuggerAttached() 检测当前是否对目标程序已经附加
Process.enumerateModules() 枚举当前加载的模块,返回模块对象的数组
Process.enumerateThreads() 枚举当前所有的线程,返回包含 id, state, context 等属性的对象数组

2.Module

Module 对象代表一个加载到进程的模块(例如,在 Windows 上的 DLL,或在 Linux/Android 上的 .so 文件),能查询模块的信息,如模块的基址、名称、导入/导出的函数等

API 含义
Module.load() 加载指定so文件,返回一个Module对象
enumerateImports() 枚举所有Import库函数,返回Module数组对象
enumerateExports() 枚举所有Export库函数,返回Module数组对象
enumerateSymbols() 枚举所有Symbol库函数,返回Module数组对象
Module.findExportByName(exportName)、Module.getExportByName(exportName) 寻找指定so中export库中的函数地址
Module.findBaseAddress(name)、Module.getBaseAddress(name) 返回so的基地址

3.Memory

Memory是一个工具对象,提供直接读取和修改进程内存的功能,能够读取特定地址的值、写入数据、分配内存等

方法 功能
Memory.copy() 复制内存
Memory.scan() 搜索内存中特定模式的数据
Memory.scanSync() 同上,但返回多个匹配的数据
Memory.alloc() 在目标进程的堆上申请指定大小的内存,返回一个NativePointer
Memory.writeByteArray() 将字节数组写入一个指定内存
Memory.readByteArray() 读取内存

Frida检测

  1. 检测/data/local/tmp路径下的是否有frida特征文件(重命名文件)
  2. 检测maps(hook strstr/strcmp函数)/(重定向maps文件)/(用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user向用户态函数地址写内容直接修改参数)
  3. 检测status(线程名)(hook strstr strcmp函数)
  4. 检测inlinehook(hook memcmp函数)

实际的hook还需要去逆向对应的frida检测代码,然后做出相应的change

也可以刷入魔改后的frida-server客户端

https://github.com/hzzheyang/strongR-frida-android?tab=readme-ov-file

https://github.com/Ylarod/Florida

App1

一个登录界面,尝试登录请求抓包:

账号:1234567890 密码:123456789

1
2
3
4
5
6
7
8
9
POST /api/user/login HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Pixel Build/PQ2A.190405.003)
Host: api.dodovip.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 262

{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT\/FXC+BMD\nbmIP0xrILxokBXoh7OoVgMbtuNMHBgOkhfune+JRPpa3O6XOpZbvNSV4zGRVpm6sKZ74RrRZvf5Y\nR1tTPSGZkERXdKPxddJZKfJiqwKHHMTk61D\/Z4zcZYYsTWqycwQ+ZGFjPIugKPjPFFzcf+vHE3CR\nGqsmzGc=\n"}

但是返回了一个404的包,说明该接口已经被弃用了,只能纯逆向了(因为是一个非常老的apk了)

反编译apk然后分析算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Map;
private Map para;
private void login(String userName, String pwd) {
this.DEFAULT_TYPE = new TypeToken() {
}.getType();
this.para.clear();
this.para.put("username", userName);
this.para.put("userPwd", pwd);
if(TextUtils.isEmpty(DodonewOnlineApplication.devId)) {
DodonewOnlineApplication.devId = Utils.getDevId(DodonewOnlineApplication.getAppContext());
}

this.para.put("equtype", "ANDROID");
this.para.put("loginImei", "Android" + DodonewOnlineApplication.devId);
this.requestNetwork("user/login", this.para, this.DEFAULT_TYPE);
}

将userName、pwd、equtype和loginImei打包成一个Map类型的值类似json,然后通过this.requestNetwork,传递给user/login接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void requestNetwork(String cmd, Map para, Type type) {
this.showProgress();
this.request = new JsonRequest(this, "http://api.dodovip.com/api/" + cmd, "", new Listener() {
public void onResponse(RequestResult requestResult) {
if(!requestResult.code.equals("1")) {
LoginActivity.this.showToast(requestResult.message);
}
else if(cmd.equals("user/login")) {
DodonewOnlineApplication.loginUser = (User)requestResult.data;
DodonewOnlineApplication.loginLabel = "mobile";
Utils.saveJson(LoginActivity.this, DodonewOnlineApplication.loginLabel, "LOGINLABEL");
LoginActivity.this.intentMainActivity();
}

LoginActivity.this.dissProgress();
}
}, this, type);
this.request.addRequestMap(para, 0);
DodonewOnlineApplication.addRequest(this.request, this);
}
  • 创建JsonRequest对象,构造完整API URL为http://api.dodovip.com/api/ + cmd
  • 设置响应监听器(Listener)处理请求结果
  • 通过addRequestMap方法将参数(para)添加到请求中
  • 最后通过DodonewOnlineApplication.addRequest将请求添加到队列中执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void addRequestMap(Map map0, int a) {
String s = System.currentTimeMillis() + "";
if(map0 == null) {
map0 = new HashMap();
}

map0.put("timeStamp", s);
String s1 = RequestUtil.encodeDesMap(RequestUtil.paraMap(map0, "sdlkjsdljf0j2fsjk", "sign"), this.desKey, this.desIV);
JSONObject obj = new JSONObject();
try {
obj.put("Encrypt", s1);
this.mRequestBody = obj + "";
}
catch(JSONException e) {
e.printStackTrace();
}
}

可以看到再次加入一个timeStamp参数,然后通过Des加密就成了Encryptvalue了,可以看到我们请求包中的参数的key就是Encrypt

1
String s1 = RequestUtil.encodeDesMap(RequestUtil.paraMap(map0, "sdlkjsdljf0j2fsjk", "sign"), this.desKey, this.desIV);

这里可以通过frida hook来获取这三个参数的值

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function(){
var RequestUtil = Java.use("com.dodonew.online.http.RequestUtil")
console.log(RequestUtil);
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a,b,c){
console.log("data: "+a);
console.log("desKey: "+b);
console.log("desIV: "+c);
var returnValue = this.encodeDesMap(a,b,c);
console.log("returnValue: "+returnValue);
return returnValue;
}
});
1
2
3
4
5
6
7
[Pixel::PID::18572 ]-> data: {"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"D890E5736DE831FE31CFEAB80EA79808","timeStamp":"1759499341310","userPwd":"12345678 ","username":"12345678901"}
desKey: 65102933
desIV: 32028092
returnValue: NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT/FXC+BMD
bmIP0xrILw+btCAOv/RTB58iDYS9BneDRhHyJkzHZpgn+2fSdkhXpYri+OFeZ+MGhIR+Vqny4/K5
nBpbSsQk3dnxQYVkLC64nN947cLFIUOcvnABk93q8ih6kqT53Hj0yxQbl3ksduKCO4P/pZPpQzAG
6DUPk6s=
1
{"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"D890E5736DE831FE31CFEAB80EA79808","timeStamp":"1759499341310","userPwd":"12345678 ","username":"12345678901"}

这是RequestUtil.paraMap(map0, "sdlkjsdljf0j2fsjk", "sign")的返回值,然后Key和IV是硬编码

1
2
3
4
5
6
7
8
9
POST /api/user/login HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Pixel Build/PQ2A.190405.003)
Host: api.dodovip.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 264

{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT\/FXC+BMD\nbmIP0xrILw+btCAOv\/RTB58iDYS9BneDRhHyJkzHZpgn+2fSdkhXpYri+OFeZ+MGhIR+Vqny4\/K5\nnBpbSsQk3dnxQYVkLC64nN947cLFIUOcvnABk93q8ih6kqT53Hj0yxQbl3ksduKCO4P\/pZPpQzAG\n6DUPk6s=\n"}

返回值和抓包得到Encrypt的value是一样的

1
{"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"D890E5736DE831FE31CFEAB80EA79808","timeStamp":"1759499341310","userPwd":"12345678 ","username":"12345678901"}

timeStamp是时间戳,通过String s = System.currentTimeMillis() + "";获取

其他字段都是明文,重点看sign字段是如何得到的

HookparaMap函数

1
2
3
4
5
timeStamp : 1759500042670
loginImei : Android352689080920725
equtype : ANDROID
userPwd : 12345678
username : 12345678901

map里的内容是以上的明文字段,因此sign是在当前函数产生的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static String paraMap(Map map0, String append, String sign) {
try {
Set set0 = map0.keySet();
StringBuilder builder = new StringBuilder();
ArrayList list = new ArrayList();
for(Object object0: set0) {
list.add(((String)object0) + "=" + ((String)map0.get(((String)object0))));
}

Collections.sort(list);
for(int i = 0; i < list.size(); ++i) {
builder.append(((String)list.get(i)));
builder.append("&");
}

builder.append("key=" + append);
map0.put("sign", Utils.md5(builder.toString()).toUpperCase());
String s2 = new Gson().toJson(RequestUtil.sortMapByKey(map0));
Log.w("yang", s2 + " result");
return s2;
}
catch(Exception e) {
e.printStackTrace();
return "";
}
}

从map0中获取所有的key,然后按照key=value的格式放到一个list里,之后进行字典排序,然后用&将每个键值对拼接起来,最后将其MD5就获得了sign,因此我们hookmd5函数就能得到参数是什么了

1
2
3
md5: equtype=ANDROID&loginImei=Android352689080920725&timeStamp=1759500651664&userPwd=12345678 &username=12345678901&key=sdlkjsdljf0j2fsjk
returnValue: 7514354356ba3ea708a5df7bcd139bbd
returnValue: {"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"7514354356BA3EA708A5DF7BCD139BBD","timeStamp":"1759500651664","userPwd":"12345678 ","username":"12345678901"}

可以看到是通过首字符升序排序的,key也是一个固定的值

然后详细看加密算法

1
2
3
4
5
6
7
8
9
public static String encodeDesMap(String data, String desKey, String desIV) {
try {
return new DesSecurity(desKey, desIV).encrypt64(data.getBytes("UTF-8"));
}
catch(Exception e) {
e.printStackTrace();
return "";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public DesSecurity(String key, String iv) throws Exception {
if(key == null) {
throw new NullPointerException("Parameter is null!");
}

this.InitCipher(key.getBytes(), iv.getBytes());
}

private void InitCipher(byte[] secKey, byte[] secIv) throws Exception {
MessageDigest messageDigest0 = MessageDigest.getInstance("MD5");
messageDigest0.update(secKey);
DESKeySpec dsk = new DESKeySpec(messageDigest0.digest());
SecretKey secretKey0 = SecretKeyFactory.getInstance("DES").generateSecret(dsk);
IvParameterSpec iv = new IvParameterSpec(secIv);
this.enCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
this.deCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
this.enCipher.init(1, secretKey0, iv);
this.deCipher.init(2, secretKey0, iv);
}

public byte[] decrypt64(String data) throws Exception {
return this.deCipher.doFinal(Base64.decode(data, 0));
}

在初始化Cipher函数中,会获取一个MD5,然后将secKey推进去,之后调用DES的获取密钥的函数将dsk截断,之后使用MD5后的Key和原始IV进行DES加密,加密模式为DES/CBC/PKCS5Padding

因此整个流程是:先将equtypeloginImeitimeStampuserPwdusernamekey拼接起来,只有userPwdusername是用户传入的然后剩下是时间戳或固定的设备参数或者硬编码,之后将其MD5后拼接到signvalue,作为新的map,之后将Key进行一次MD5然后将其和原本的IV作为密钥对新的map进行加密,得到的就是最后的Encrypt

算法还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import hashlib
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad
import base64

sign_string='equtype=ANDROID&loginImei=Android352689080920725&timeStamp=1759501463192&userPwd=12345678 &username=12345678901&key=sdlkjsdljf0j2fsjk';
md5_sign=hashlib.md5(sign_string.encode()).hexdigest().upper()
# print(md5_string)

user_string='{"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"'+str(md5_sign)+'","timeStamp":"1759501463192","userPwd":"12345678 ","username":"12345678901"}'
print(f"user_string={user_string}")
key_string = "65102933"
key_md5 = hashlib.md5(key_string.encode()).digest()
print(f"key_md5={key_md5}")
key = key_md5[:8]
print(f'key={key}')

iv = "32028092".encode()
if len(iv) < 8:
iv = iv + b'\0' * (8 - len(iv))
elif len(iv) > 8:
iv = iv[:8]
data = user_string.encode()
padded_data = pad(data, DES.block_size)
cipher = DES.new(key, DES.MODE_CBC, iv)
encrypted_data = cipher.encrypt(padded_data)
encrypted_base64 = base64.b64encode(encrypted_data).decode()

print(f"加密后的结果: {encrypted_base64}")

因为使用Python写加解密算法比较方便,因此使用python进行算法还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user_string={"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"914CCB500B828858A16496959DE4CC7A","timeStamp":"1759501463192","userPwd":"12345678 ","username":"12345678901"}
key_md5=b'\xc3\xd2m\xca\x86%\x97\x82\xbd\x91\x86H\x1f\x89*\xe6'
key=b'\xc3\xd2m\xca\x86%\x97\x82'
加密后的结果:
NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT/FXC+BMD
bmIP0xrIL5+cfhEzMXyKHy4xuh0h19w1UoI+CUlznYRWafwBDARtKnpa1J/s5fecfmKDlU6dbU70
CC9YUEGfzovqMpwa+LSpI8uI0g8zWl8z9CGsfPmQirjlSp41/YR/AvN6QLOGCxJbcAk8Zlh2O2nR
Z9vurSE=

returnValue:
NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT/FXC+BMD
bmIP0xrIL5+cfhEzMXyKHy4xuh0h19w1UoI+CUlznYRWafwBDARtKnpa1J/s5fecfmKDlU6dbU70
CC9YUEGfzovqMpwa+LSpI8uI0g8zWl8z9CGsfPmQirjlSp41/YR/AvN6QLOGCxJbcAk8Zlh2O2nR
Z9vurSE=

一毛一样!!!

hook.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Java.perform(function(){
var RequestUtil = Java.use("com.dodonew.online.http.RequestUtil")
console.log(RequestUtil);
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a,b,c){
console.log("data: "+a);
console.log("desKey: "+b);
console.log("desIV: "+c);
var returnValue = this.encodeDesMap(a,b,c);
console.log("returnValue: "+returnValue);
return returnValue;
}

RequestUtil.paraMap.overload('java.util.Map', 'java.lang.String', 'java.lang.String').implementation = function(a,b,c){
console.log("map0 类型: " + (a ? a.$className : "null"));

// 打印Map内容
try {
if (a != null) {
console.log("map内容:");
var keySet = a.keySet();
var keyArray = keySet.toArray();
for (var i = 0; i < keyArray.length; i++) {
var key = keyArray[i];
var value = a.get(key);
console.log(key + " : " + value);
}
}
} catch (e) {
console.log("打印Map内容时出错: " + e);
}

console.log("append: "+b);
console.log("sign: "+c);
var returnValue = this.paraMap(a,b,c);
console.log("returnValue: "+returnValue);
return returnValue;
}

var Utils = Java.use("com.dodonew.online.util.Utils")
Utils.md5.implementation = function(a){
console.log("md5: "+a);
var returnValue = this.md5(a);
console.log("returnValue: "+returnValue);
return returnValue;
}


});

算法转发

直接使用该App内部的Java算法来进行一个加密的操作,我们就不用进行算法的还原了,只需要调用该App的算法函数,然后提供需要的原料,通过Rpc映射到本地的一个端口上,就能实现一个Rpc远程调用该算法

使用小肩膀课程中的代码会发现会报一个错误:

1
{"error":"ReferenceError: 'Java' is not defined\n    at hookTest (/script1.js:5)\n    at call (native)\n    at handleRpcMessage (/frida/runtime/message-dispatcher.js:39)\n    at handleMessage (/frida/runtime/message-dispatcher.js:25)","item_id":"1"}

这是因为https://frida.re/news/2025/05/17/frida-17-0-0-released/在这里说明frida17的版本,桥接器`frida-{objc,swift,java}-bridge`不再默认加载,需要手动导入,使用AI去重写一个 Python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# -*- coding: UTF-8 -*-

from fastapi import FastAPI
import uvicorn
import frida
import time
import json
import sys
import os


def wrap_user_script(name, script):
if script.startswith("📦\n"):
return script
return f"Script.evaluate({json.dumps(name)}, {json.dumps(script)});"


def build_final_script(raw_fragments):
fragments = []
next_script_id = 1
for raw_fragment in raw_fragments:
if raw_fragment.startswith("📦\n"):
fragments.append(raw_fragment[2:])
else:
script_id = next_script_id
next_script_id += 1
size = len(raw_fragment.encode("utf-8"))
fragments.append(f"{size} /frida/repl-{script_id}.js\n✄\n{raw_fragment}")

return "📦\n" + "\n✄\n".join(fragments)


def find_java_bridge():
"""查找Java桥接器文件"""
for path in reversed(sys.path):
if path.endswith('site-packages'):
bridge_path = os.path.join(path, 'frida_tools', 'bridges', 'java.js')
if os.path.exists(bridge_path):
return bridge_path

# 如果找不到,尝试其他可能的路径
import frida_tools
frida_tools_path = os.path.dirname(frida_tools.__file__)
bridge_path = os.path.join(frida_tools_path, 'bridges', 'java.js')
if os.path.exists(bridge_path):
return bridge_path

raise FileNotFoundError("找不到Java桥接器文件,请确保frida-tools已正确安装")


# 你的JavaScript代码
user_js_code = """
function hookTest(username, passward){
var result;
Java.perform(function(){
var time = new Date().getTime();
time = '1759643230427';

var string = Java.use('java.lang.String');
var signData = string.$new('equtype=ANDROID&loginImei=Android352689080920725&timeStamp=' +
time + '&userPwd=' + passward + '&username=' + username + '&key=sdlkjsdljf0j2fsjk');

var Utils = Java.use('com.dodonew.online.util.Utils');
var sign = Utils.md5(signData).toUpperCase();
console.log('sign: ', sign);

var encryptData = '{"equtype":"ANDROID","loginImei":"Android352689080920725","sign":"'+
sign +'","timeStamp":"'+ time +'","userPwd":"' + passward + '","username":"' + username + '"}';

var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
var Encrypt = RequestUtil.encodeDesMap(encryptData, '65102933', '32028092');
console.log('Encrypt: ', Encrypt);
result = Encrypt;
});
return result;
}

rpc.exports = {
s1nec1o: hookTest
};
"""


def on_message(message, data):
if message['type'] == 'send':
print(f"[+] {message['payload']}")
else:
print(f"[-] ERROR: {message}")


# 创建完整的脚本
try:
# 读取Java桥接器
bridge_path = find_java_bridge()
with open(bridge_path, 'r', encoding='utf-8') as f:
java_bridge = f.read()

# 添加Java到全局对象
java_bridge += "\n\nObject.defineProperty(globalThis, 'Java', { value: bridge });"

# 包装用户脚本
wrapped_script = wrap_user_script("hookTest", user_js_code)

# 构建最终脚本
raw_fragments = [java_bridge, wrapped_script]
final_script = build_final_script(raw_fragments)

print("脚本构建成功")

except Exception as e:
print(f"脚本构建失败: {e}")
exit(1)

# Frida连接
devices = frida.get_usb_device(1000)
pid = devices.spawn('com.dodonew.online')
print(f"pid: {pid}")

process = devices.attach(pid)
script = process.create_script(final_script)
script.on('message', on_message)
script.load()

devices.resume(pid)
print("应用已恢复执行")

# 等待应用完全启动
print("等待应用完全启动...")
time.sleep(15)

app = FastAPI()


@app.get("/get_data")
async def getEchoApi(item_id: str, item_user: str, item_pass: str):
try:
print(f"开始处理请求: user={item_user}, pass={item_pass}")
result = script.exports_sync.s1nec1o(item_user, item_pass)
return {"item_id": item_id, "item_retval": result}
except Exception as e:
error_msg = str(e)
print(f"RPC调用错误: {error_msg}")
return {"error": error_msg, "item_id": item_id}


@app.get("/status")
async def check_status():
return {"status": "ready", "frida_version": frida.__version__}


if __name__ == '__main__':
uvicorn.run(app=app, host="127.0.0.1", port=8000)

增加部分的详细说明:

1
2
3
4
def wrap_user_script(name, script):
if script.startswith("📦\n"):
return script
return f"Script.evaluate({json.dumps(name)}, {json.dumps(script)});"

将Js代码包装成frida能识别的格式。Script.evaluate()是Frida内部用来执行脚本的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
def build_final_script(raw_fragments):
fragments = []
next_script_id = 1
for raw_fragment in raw_fragments:
if raw_fragment.startswith("📦\n"):
fragments.append(raw_fragment[2:])
else:
script_id = next_script_id
next_script_id += 1
size = len(raw_fragment.encode("utf-8"))
fragments.append(f"{size} /frida/repl-{script_id}.js\n✄\n{raw_fragment}")

return "📦\n" + "\n✄\n".join(fragments)

将多个JavaScript片段(Java桥接器 + 你的代码)组合成一个完整的Frida脚本包。📦和✄是Frida的内部标记符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def find_java_bridge():
"""查找Java桥接器文件"""
for path in reversed(sys.path):
if path.endswith('site-packages'):
bridge_path = os.path.join(path, 'frida_tools', 'bridges', 'java.js')
if os.path.exists(bridge_path):
return bridge_path

# 如果找不到,尝试其他可能的路径
import frida_tools
frida_tools_path = os.path.dirname(frida_tools.__file__)
bridge_path = os.path.join(frida_tools_path, 'bridges', 'java.js')
if os.path.exists(bridge_path):
return bridge_path

raise FileNotFoundError("找不到Java桥接器文件,请确保frida-tools已正确安装")

自动查找系统中安装的Java桥接器文件。这个文件包含了让Java.perform()工作所需的所有代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建完整的脚本
try:
# 读取Java桥接器
bridge_path = find_java_bridge()
with open(bridge_path, 'r', encoding='utf-8') as f:
java_bridge = f.read()

# 添加Java到全局对象
java_bridge += "\n\nObject.defineProperty(globalThis, 'Java', { value: bridge });"

# 包装用户脚本
wrapped_script = wrap_user_script("hookTest", user_js_code)

# 构建最终脚本
raw_fragments = [java_bridge, wrapped_script]
final_script = build_final_script(raw_fragments)

print("脚本构建成功")

except Exception as e:
print(f"脚本构建失败: {e}")
exit(1)
  1. 读取Java桥接器: 从frida_tools/bridges/java.js读取Java桥接器代码
  2. 注册Java对象: 添加Object.defineProperty(globalThis, 'Java', { value: bridge });,这行代码让Java对象在全局可用
  3. 包装你的脚本: 将你的JavaScript代码包装成Frida格式
  4. 组合脚本: 将Java桥接器和你的代码组合成最终脚本

http://127.0.0.1:8000/get_data?item_id=1&item_user=12345678901&item_pass=123456789

1
{"item_id":"1","item_retval":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT/FXC+BMD\nbmIP0xrIL98xsKTOSbgIWuEhruHgPtMk8UaM2X3bIN17yS34xTmxUQlfDaRFr4d1eIV2+Kn2vl2z\n8dbq+kB0AY2H+3lKAmUDzuRngtxIqaZHS9MIBv9sErGLqpABUb9MgTOmjljk+33RaMn5gSx6PQKy\nQN2xZw4=\n"}

抓包结果:

1
{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii02ggCULFNaeuVKT\/FXC+BMD\nbmIP0xrIL98xsKTOSbgIWuEhruHgPtMk8UaM2X3bIN17yS34xTmxUQlfDaRFr4d1eIV2+Kn2vl2z\n8dbq+kB0AY2H+3lKAmUDzuRngtxIqaZHS9MIBv9sErGLqpABUb9MgTOmjljk+33RaMn5gSx6PQKy\nQN2xZw4=\n"}

结果相等~~~