前言 只做学习记录的备份,来自于
[原创]Android漏洞之战(11)——整体加壳原理和脱壳技巧详解
《安卓逆向这档事》
Android App启动流程
Zygote
进程fork的第一个进程是:SystemServer
进程,SystemServer
进程主要进行以下的工作
Android APP安装 1 2 3 4 · 系统启动时安装,没有安装界面 · 第三方应用安装,有安装界面,也是我们最熟悉的方式 · ADB命令安装,没有安装界面 · 通过各类应用市场安装,没有安装界面
有这四种安装模式
但是都是通过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
的实例供用户使用
应用程序在安装时涉及到如下几个重要目录:
App的安装流程是由PackageManagerService
完成的,此前SystemServer
就已经启动了一个更重要的服务ActivityManagerService
,ActivityManagerService
其中一个重要的作用就是在启动完PackageManagerService
之后将Launcher
进程启动起来
启动Launcher
的入口为ActivityManagerService
的systemReady
函数
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
函数中,会调用ActivityManagerService
的systemReady
函数:
1 2 3 4 5 6 7 8 public void systemReady (final Runnable goingCallback) {... synchronized (this ) { ... mStackSupervisor.resumeFocusedStackTopActivityLocked(); mUserController.sendUserSwitchBroadcastsLocked(-1 , currentUserId); } }
systemReady
函数中调用了ActivityStackSupervisor
的resumeFocusedStackTopActivityLocked
函数:
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); } final ActivityRecord r = mFocusedStack.topRunningActivityLocked(); if (r == null || r.state != RESUMED) { mFocusedStack.resumeTopActivityUncheckedLocked(null , null ); } return false ; }
在注释1处会调用ActivityStack
的resumeTopActivityUncheckedLocked
函数,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) { return false ; } boolean result = false ; try { mStackSupervisor.inResumeTopActivity = true ; if (mService.mLockScreenShown == ActivityManagerService.LOCK_SCREEN_LEAVING) { mService.mLockScreenShown = ActivityManagerService.LOCK_SCREEN_HIDDEN; mService.updateSleepIfNeededLocked(); } result = resumeTopActivityInnerLocked(prev, options); } 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" ); ... }
调用ActivityStackSupervisor
的resumeHomeStackTask
函数,
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); }
调用了ActivityManagerService
的startHomeActivityLocked
函数
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 ) { return false ; } Intent intent = getHomeIntent(); 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 ) { intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); mActivityStarter.startHomeActivityLocked(intent, aInfo, reason); } } else { Slog.wtf(TAG, "No home screen found for " + intent, new Throwable ()); } return true ; }
注释1处的mFactoryTest
代表系统的运行模式 ,系统的运行模式分为三种,分别是非工厂模式、低级工厂模式和高级工厂模式 ,mTopAction
则用来描述第一个 被启动Activity组件的Action
,它的值为Intent.ACTION_MAIN
。
因此注释1的代码意思就是mFactoryTest
为FactoryTest.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
,并将mTopAction
和mTopData
传入。mTopAction
的值为Intent.ACTION_MAIN
,并且如果系统运行模式不是低级工厂模式则将intent
的Category
设置为Intent.CATEGORY_HOME
我们再回到ActivityManagerService
的startHomeActivityLocked
函数
假设系统的运行模式不是低级工厂模式,在注释3处判断符合Action
为Intent.ACTION_MAIN
,Category
为Intent.CATEGORY_HOME
的应用程序是否已经启动,如果没启动则调用注释4的方法启动该应用程序。
这个被启动的应用程序就是Launcher
,因为Launcher
的Manifest
文件中的intent-filter
标签匹配了Action
为Intent.ACTION_MAIN
,Category
为Intent.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
返回系统中已经安装的应用程序的信息,并将这些信息封装成一个快捷图标列表显示在系统屏幕上,这样用户可以通过点击这些快捷图标来启动相应的应用程序
点击桌面APP图标时,Launcher的startActivity()
方法,通过Binder
通信,调用system_server进程中AMS服务的startActivity
方法,发起启动请求
system_server
进程接收到请求后,向Zygote
进程发送创建进程的请求
Zygote进程fork出App进程,并执行ActivityThread
的main方法,创建ActivityThread线程,初始化MainLooper
,主线程Handler,同时初始化ApplicationThread用于和AMS通信交互
App进程,通过Binder向sytem_server进程发起attachApplication
请求,这里实际上就是APP进程通过Binder调用sytem_server进程中AMS的attachApplication方法 ,AMS的attachApplication方法的作用是将ApplicationThread对象与AMS绑定
system_server进程在收到attachApplication的请求,进行一些准备工作后,再通过binder IPC向App进程发送handleBindApplication请求 (初始化Application并调用onCreate方法)和scheduleLaunchActivity请求(创建启动Activity)
App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送BIND_APPLICATION和LAUNCH_ACTIVITY消息 ,这里注意的是AMS和主线程并不直接通信,而是AMS和主线程的内部类ApplicationThread
通过Binder通信,ApplicationThread
再和主线程通过Handler消息交互。
主线程在收到Message后,创建Application并调用onCreate方法 ,再通过反射机制创建目标Activity,并回调Activity.onCreate()
等方法
到此,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一样遵循双亲委派模式
双亲委派模式 代码解释:
首先检查该类是否已被加载
若未加载,则委托给父类的Loader
加载,如果没有父类则委托给BootClassLoader
尝试加载
若BootClassLoader
无法加载,则PathClassLoader
尝试从应用的dex文件中加载
若需要动态加载外部dex,则使用DexClassLoader
双亲委派模式加载
我们要加载一个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
出来,天然保留了加载过的类缓存
Android中的ClassLoader
类型分为系统ClassLoader 和自定义ClassLoader 。其中系统ClassLoader 包括3种是BootClassLoader
、DexClassLoader
、PathClassLoader
BootClassLoader
:Android平台上所有Android系统启动时会使用BootClassLoader来预加载常用的类
BaseDexClassLoader
:实际应用层类文件的加载,而真正的加载委托给pathList来完成
DexClassLoader
:可以加载dex文件以及包含dex的压缩文件(apk,dex,jar,zip),可以安装一个未安装的apk文件,一般为自定义类加载器
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文件加载:
通过Class.forName()
方法动态加载
通过ClassLoader.loadClass()
方法动态加载
类的加载分为3个步骤:1.装载(Load),2.链接(Link),3.初始化(Intialize)
类加载时机:
1.隐式加载:
创建类的实例,也就是new一个对象
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射Class.forName("android.app.ActivityThread")
初始化一个类的子类(会首先初始化子类的父类)
2.显示加载:
使用LoadClass()
加载
使用forName()
加载
Class.forName 和 ClassLoader.loadClass加载有何不同:
ClassLoader.loadClass
也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作)
Class.forName
这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作
PathClassLoader 主要用于系统和app的类加载器,其中optimizedDirectory
为null
, 采用默认目录/data/dalvik-cache/
1 PathClassLoader 是作为应用程序的系统类加载器,也是在 Zygote 进程启动的时候初始化的(基本流程为:ZygoteInit.main() -> ZygoteInit.forkSystemServer() -> ZygoteInit.handleSystemServerProcess() -> ZygoteInit.createPathClassLoader()。在预加载基本类之后执行),所以每一个 APP 进程从 Zygote 中 fork 出来之后都自动携带了一个 PathClassLoader,它通常用于加载 apk 里面的 .dex 文件
DexClassLoader 我们可以发现DexClassLoader
与PathClassLoader
都继承于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; 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类加载详细流程:
假设我们要加载一个类com.example.MyActivity:
应用通过Custom ClassLoader请求加载类(图中步骤1)
Custom ClassLoader调用loadClass(),遵循双亲委派,先委托给PathClassLoader(步骤3)
PathClassLoader委托给BootClassLoader(步骤5)
BootClassLoader无法加载该类,返回给PathClassLoader(步骤8)
PathClassLoader调用findClass()方法(步骤9)
findClass()调用DexPathList的findClass()方法
DexPathList遍历dexElements数组:
对每个Element调用findClass()
Element调用DexFile的loadClassBinaryName()方法
如果在某个DexFile中找到类,立即返回
如果PathClassLoader未找到,返回给Custom ClassLoader(步骤11)
Custom ClassLoader调用自己的findClass()(步骤12)
同样通过DexPathList查找类
如果找到,返回类对象(步骤14)
如果未找到,抛出ClassNotFoundException(步骤15)
整体加壳原理 Dex整体加壳 可以理解为在加密的源Apk程序外面有套上了一层外壳,简单过程为:
加壳例子:
我们很明显看见,除了一个代理类Application
,其他相关的代码信息都无法发现
在代理类中反射调用了一些方法,很显然我们解析出的结果都无法查找,很明显就说明在Application.attchBaseContext()
和Application.onCreate()
中必须要完成对源加密的dex
的动态加载和解密
App加载应用解析时流程如下:
BootClassLoader
加载系统核心库
PathClassLoader
加载APP自身dex
进入APP自身组件,解析AndroidManifest.xml
,然后查找Application
代理
调用声明Application
的attachBaseContext()
对源程序进行动态加载或解密
调用声明Application
的onCreate()
对源程序进行动态加载或解密
进入MainActivity
中的attachBaseContext()
,然后进入onCreate()
函数,执行源程序代码
上面我们已经很清晰的了解了壳加载的流程,我们很明显的意识到一个问题,我们从头到尾都是用PathClassLoader
来加载dex
简单整体加壳 我们要想动态加载dex文件必须使用自定义的DexClassLoader
,但是直接使用DexClassLoader
进行加载,会报异常,是因为DexClassLoader
加载的类是没有组件生命周期 的,即DexClassLoader
即使通过对APK的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常
所以我们要想使用DexClassLoader
进行动态加载dex,我们需要进行类加载器的修正
主要有两种方案:
替换系统组件类加载器为我们的DexClassLoader
,同时设置DexClassLoader
的parent
为系统组件加载器
打破原有的双亲委派关系,在系统组件类加载器PathClassLoader
和BootClassLoader
的中间插入我们自己的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 { Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread" ); Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread" ); Object activityThreadObj = currentActivityThread.invoke(null ); Field mPackagesField = ActivityThread.getDeclaredField("mPackages" ); mPackagesField.setAccessible(true ); ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj); String packagename = context.getPackageName(); WeakReference wr = (WeakReference) mPackagesObj.get(packagename); Object LoadApkObj = wr.get(); 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()); 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) { 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
代理的onCreate
和attachBaseContext
函数即可
脱壳方法 常用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模式 :
基础语法
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 function hookTest1 () { var utils = Java.use("类名" ); utils.method.implementation = function(a, b){ a = 123 ; b = 456 ; var retval = this .method(a, b); console.log(a, b, retval); return retval; } }
1 2 3 4 5 6 7 8 9 10 11 12 function hookTest2 ( ){ var utils = Java .use ("com.zj.wuaipojie.Demo" ); 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" ); 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" ); utils.staticField.value = "我是被修改的静态变量" ; console.log(utils.staticField.value); Java.choose("com.zj.wuaipojie.Demo" , { onMatch: function(obj){ 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){ 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" ); 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" ,{ onMatch:function(instance){ ret=instance.privateFunc("aaaaaaa" ); }, onComplete:function(){ } }); })
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检测
检测/data/local/tmp
路径下的是否有frida特征文件(重命名文件)
检测maps(hook strstr/strcmp
函数)/(重定向maps
文件)/(用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user
向用户态函数地址写内容直接修改参数)
检测status(线程名)(hook strstr strcmp
函数)
检测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加密就成了Encrypt
的value
了,可以看到我们请求包中的参数的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
因此整个流程是:先将equtype
、loginImei
、timeStamp
、userPwd
、username
和key
拼接起来,只有userPwd
和username
是用户传入的然后剩下是时间戳或固定的设备参数或者硬编码,之后将其MD5后拼接到sign
的value
,作为新的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 hashlibfrom Crypto.Cipher import DESfrom Crypto.Util.Padding import padimport base64sign_string='equtype=ANDROID&loginImei=Android352689080920725&timeStamp=1759501463192&userPwd=12345678 &username=12345678901&key=sdlkjsdljf0j2fsjk' ; md5_sign=hashlib.md5(sign_string.encode()).hexdigest().upper() 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" )); 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 from fastapi import FastAPIimport uvicornimport fridaimport timeimport jsonimport sysimport osdef 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已正确安装" ) 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 : bridge_path = find_java_bridge() with open (bridge_path, 'r' , encoding='utf-8' ) as f: java_bridge = f.read() 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 ) 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 : bridge_path = find_java_bridge() with open (bridge_path, 'r' , encoding='utf-8' ) as f: java_bridge = f.read() 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 )
读取Java桥接器: 从frida_tools/bridges/java.js
读取Java桥接器代码
注册Java对象: 添加Object.defineProperty(globalThis, 'Java', { value: bridge });
,这行代码让Java对象在全局可用
包装你的脚本: 将你的JavaScript
代码包装成Frida
格式
组合脚本: 将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" }
结果相等~~~