小柏实战学习安卓图文教程-第十二课-Frida动态脱壳1
本节课主题:实战演示内存扫描脱壳CrackMe.apk
一、实战脱壳(上节课失败了,这次再来,Fighting👊再一决✊)
内存扫描搜索DEX:
先查询我们要找的PID 是哪一个?
frida-ps -U

这次我们发现两个进程,因为这次我注册了,脚本账号,只不过没有充钱登录不上去;
2341 com.liushi.nz
2560 球球英雄脚本



(记得第十课讲过了,雷电模拟器的frida 服务自己记得启动,这里不赘述了)
上节课我们看了一下 2560 , 这次先快速看一下2341,看一下是不是一样的问题:
frida-dexdump -U -p 2341 -d
dump下来了:

GDA快速试一下: (也不行)

好了,现在不应该再在各种静态反编译工具上浪费时间了,应该动态分析了;
一、动态分析
1:放弃“阅读源码”,改为“监听行为”
忘掉类名和方法名。直接监听它做了什么。
运行下面这个脚本,它会监听应用的启动、Activity跳转和网络请求:
先创建这个脚本
// monitor_basic.js Java.perform(function() { console.log("[*] 修复版悬浮窗监控脚本启动"); // 1. 修复无障碍服务监控 - 监控具体的无障碍服务实现 try { // 查找当前运行的无障碍服务并Hook Java.choose("android.accessibilityservice.AccessibilityService", { onMatch: function(instance) { console.log("[ACCESSIBILITY] 找到无障碍服务实例: " + instance.getClass().getName()); // Hook 具体的onAccessibilityEvent实现 var clazz = Java.use(instance.getClass().getName()); if (clazz.onAccessibilityEvent) { clazz.onAccessibilityEvent.implementation = function(event) { var eventType = event.getEventType(); var className = event.getClassName(); console.log("[ACCESSIBILITY] 事件类型: " + eventType + ", 组件: " + className); return this.onAccessibilityEvent(event); }; } }, onComplete: function() { console.log("[ACCESSIBILITY] 无障碍服务扫描完成"); } }); } catch (e) { console.log("[!] 无障碍服务Hook失败: " + e.message); } // 2. 修复窗口管理器监控 - 使用正确的实现类 try { var WindowManagerImpl = Java.use("android.view.WindowManagerImpl"); // 指定addView方法的重载 WindowManagerImpl.addView.overload('android.view.View', 'android.view.ViewGroup$LayoutParams').implementation = function(view, params) { console.log("[WINDOW] 添加视图: " + view.getClass().getName()); return this.addView(view, params); }; } catch (e) { console.log("[!] WindowManagerImpl Hook失败,尝试其他实现: " + e.message); // 备用方案:Hook View的添加 try { var ViewGroup = Java.use("android.view.ViewGroup"); ViewGroup.addView.overload('android.view.View', 'android.view.ViewGroup$LayoutParams').implementation = function(view, params) { // 过滤悬浮窗相关的视图添加 if (view.getClass().getName().toLowerCase().indexOf("window") !== -1 || view.getClass().getName().toLowerCase().indexOf("float") !== -1) { console.log("[WINDOW] 可能添加悬浮窗: " + view.getClass().getName()); } return this.addView(view, params); }; } catch (e2) { console.log("[!] ViewGroup Hook也失败: " + e2.message); } } // 3. 修复输入框监控 - 更安全的方法 try { var EditText = Java.use("android.widget.EditText"); // 指定setOnClickListener的重载 EditText.setOnClickListener.overload('android.view.View$OnClickListener').implementation = function(listener) { console.log("[UI] 输入框设置点击监听器: " + this.toString().substring(0, 100)); // 调用原始方法 return this.setOnClickListener(listener); }; } catch (e) { console.log("[!] 输入框Hook失败: " + e.message); } // 4. 修复Activity监控 - 指定onCreate的重载 try { var Activity = Java.use("android.app.Activity"); // 使用单参数版本的重载(更常见) Activity.onCreate.overload('android.os.Bundle').implementation = function(bundle) { console.log("[ACTIVITY] 启动: " + this.getClass().getName()); return this.onCreate(bundle); }; } catch (e) { console.log("[!] Activity.onCreate Hook失败: " + e.message); } // 5. 修复按键事件监控 try { var View = Java.use("android.view.View"); // 指定dispatchKeyEvent的重载 View.dispatchKeyEvent.overload('android.view.KeyEvent').implementation = function(event) { var keyCode = event.getKeyCode(); if (keyCode === 4 || keyCode === 3) { // BACK键或HOME键 console.log("[KEY] 按键事件: keyCode=" + keyCode + ", 在: " + this.getClass().getName()); } return this.dispatchKeyEvent(event); }; } catch (e) { console.log("[!] View.dispatchKeyEvent Hook失败: " + e.message); } console.log("[*] 修复版监控脚本注入完成"); });
再运行以下命令
frida -U -p 2341 -l monitor_basic.js
结果如下: 点一下登录,app 下方弹了一个悬浮气泡: 会员已到期; 监听到layout

很明显下一步我们要 精准定位验证逻辑
监控Toast和对话框创建,创建如下脚本
// login_monitor.js Java.perform(function() { console.log("[*] 登录验证专项监控启动"); // 1. 监控Toast显示("会员已到期"很可能是Toast) var Toast = Java.use("android.widget.Toast"); Toast.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function(context, text, duration) { var message = text.toString(); console.log("[TOAST] 显示提示: " + message); // 打印调用栈,找到是谁调用了这个Toast console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); return this.makeText(context, text, duration); }; // 2. 监控按钮点击事件 var Button = Java.use("android.widget.Button"); Button.performClick.implementation = function() { var buttonText = this.getText(); console.log("[BUTTON] 按钮被点击: " + buttonText); // 如果是"登录"按钮 if (buttonText.toString().indexOf("登录") !== -1) { console.log("[!!!] 登录按钮被点击!"); // 打印调用栈,找到登录处理逻辑 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); } return this.performClick(); }; // 3. 监控网络请求(登录验证可能调用API) var HttpURLConnection = Java.use("com.android.okhttp.internal.huc.HttpURLConnectionImpl"); HttpURLConnection.getInputStream.implementation = function() { var url = this.getURL().toString(); console.log("[NETWORK] 请求URL: " + url); return this.getInputStream(); }; // 4. 查找包含"会员"、"到期"、"vip"等关键词的类 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.toLowerCase().indexOf("vip") !== -1 || className.toLowerCase().indexOf("member") !== -1 || className.indexOf("到期") !== -1 || className.indexOf("会员") !== -1) { console.log("[CLASS] 发现相关类: " + className); } }, onComplete: function() {} }); });

上一个脚本的监听先停一下:
先Ctrl+c 在输入下面的退出命令
exit

重新执行 登录的监听:
frida -U -p 2341 -l login_monitor.js
结果如下:

再重新按一下登录按钮: (哦,发现报错了)

在完善一下脚本,再重新执行一下下面这个脚本
// expanded_monitor.js Java.perform(function() { console.log("[*] 扩大监控范围脚本启动"); // 1. 监控所有View的点击事件(不仅仅是Button) try { var View = Java.use("android.view.View"); View.performClick.implementation = function() { var viewClass = this.getClass().getName(); var viewId = this.getId(); // 尝试获取资源ID名称 try { var resources = this.getResources(); var entryName = resources.getResourceEntryName(viewId); console.log("[CLICK] 视图被点击: " + viewClass + " ID: " + entryName); } catch(e) { console.log("[CLICK] 视图被点击: " + viewClass + " ID: 0x" + viewId.toString(16)); } // 如果是登录相关视图 if (viewClass.toLowerCase().indexOf("login") !== -1 || viewClass.toLowerCase().indexOf("sign") !== -1) { console.log("[!!!] 可能登录相关视图被点击!"); } return this.performClick(); }; console.log("[+] View点击监控就绪"); } catch(e) { console.log("[-] View点击监控失败: " + e.message); } // 2. 监控触摸事件(备用方案) try { var View = Java.use("android.view.View"); View.onTouchEvent.implementation = function(event) { var action = event.getAction(); if (action === 1) { // ACTION_UP (手指抬起) var viewClass = this.getClass().getName(); console.log("[TOUCH] 触摸事件: " + viewClass); } return this.onTouchEvent(event); }; console.log("[+] 触摸事件监控就绪"); } catch(e) { console.log("[-] 触摸事件监控失败: " + e.message); } // 3. 监控对话框和弹出窗口 try { var Dialog = Java.use("android.app.Dialog"); Dialog.show.implementation = function() { console.log("[DIALOG] 显示对话框: " + this.getClass().getName()); return this.show(); }; console.log("[+] 对话框监控就绪"); } catch(e) { console.log("[-] 对话框监控失败: " + e.message); } // 4. 监控所有Activity生命周期 try { var Activity = Java.use("android.app.Activity"); var methods = ["onCreate", "onResume", "onPause", "onDestroy"]; methods.forEach(function(methodName) { if (Activity[methodName]) { Activity[methodName].overloads.forEach(function(overload, index) { overload.implementation = function() { console.log("[ACTIVITY] " + methodName + ": " + this.getClass().getName()); return overload.apply(this, arguments); }; }); } }); console.log("[+] Activity生命周期监控就绪"); } catch(e) { console.log("[-] Activity监控失败: " + e.message); } console.log("[*] 扩大监控脚本注入完成"); });
点一下登录按钮

当前情况分析
✅ 成功捕获:脚本检测到了TextView的点击事件
⚠️ 但缺少关键信息:没有看到登录验证逻辑的执行,也没有看到"会员已到期"的Toast提示
下一步行动:深入追踪验证逻辑
我们需要找到为什么点击事件被捕获了,但登录验证逻辑没有被触发。
方案一:增强Toast监控(最直接) (创建脚本并执行这个脚本,不赘述了,和上面的流程一样)
// toast_focused.js Java.perform(function() { console.log("[*] Toast专项监控启动"); // 1. 精准监控Toast显示 var Toast = Java.use("android.widget.Toast"); var showOverloads = Toast.show.overloads; showOverloads.forEach(function(overload, index) { overload.implementation = function() { // 获取Toast的文本内容 try { var view = this.getView(); if (view) { var textView = view.findViewById(0x102000b); // android.R.id.message if (textView) { var message = textView.getText().toString(); console.log("[TOAST!!!] 显示内容: " + message); // 如果是会员相关提示,打印完整调用栈 if (message.indexOf("会员") !== -1 || message.indexOf("到期") !== -1) { console.log("[!!!] 发现会员验证Toast,追踪调用栈:"); Java.perform(function() { var Thread = Java.use("java.lang.Thread"); var stackTrace = Thread.currentThread().getStackTrace(); for (var i = 0; i < Math.min(stackTrace.length, 12); i++) { var stackElement = stackTrace[i].toString(); // 过滤掉系统类,只显示应用相关调用 if (stackElement.indexOf("android.") === -1 && stackElement.indexOf("java.") === -1 && stackElement.indexOf("com.android.") === -1) { console.log(" [" + i + "] " + stackElement); } } }); } } } } catch(e) { // 备用方案:通过反射获取文本 try { var mNextView = this.getClass().getDeclaredField("mNextView"); mNextView.setAccessible(true); var view = mNextView.get(this); if (view) { var textView = view.findViewById(0x102000b); if (textView) { var message = textView.getText().toString(); console.log("[TOAST-REFLECT] 内容: " + message); } } } catch(e2) {} } return overload.apply(this, arguments); }; }); // 2. 监控可能触发Toast的方法 var Log = Java.use("android.util.Log"); Log.w.overload('java.lang.String', 'java.lang.String').implementation = function(tag, msg) { if (msg.indexOf("会员") !== -1 || msg.indexOf("到期") !== -1) { console.log("[LOG-W] 警告日志 - Tag: " + tag + ", Msg: " + msg); } return this.w(tag, msg); }; console.log("[*] Toast监控就绪"); });
执行后结果如下:

问题分析
从我们之前的监控结果看,点击事件被捕获了([CLICK] 视图被点击: android.widget.TextView),但没有Toast输出,这说明:
-
提示可能不是标准Toast:应用可能使用了自定义对话框、PopupWindow、Snackbar或其他UI组件
-
提示可能来自WebView或混合开发:如果是Web内容,不会触发原生Toast
-
可能有反检测机制:应用可能检测到Frida后避免了标准API调用
解决方案:扩大UI监控范围
以下是增强版脚本,监控所有可能的提示显示方式:
创建脚本
// enhanced_ui_monitor.js Java.perform(function() { console.log("[*] 增强版UI监控启动 - 覆盖所有提示类型"); // 1. 监控Dialog(对话框) try { var Dialog = Java.use("android.app.Dialog"); Dialog.show.implementation = function() { console.log("[DIALOG] 显示对话框: " + this.getClass().getName()); // 尝试获取对话框内容 try { var window = this.getWindow(); if (window) { var decorView = window.getDecorView(); scanViewText(decorView, "DIALOG"); } } catch(e) {} return this.show(); }; console.log("[+] Dialog监控就绪"); } catch(e) { console.log("[-] Dialog监控失败: " + e.message); } // 2. 监控PopupWindow(弹出窗口) try { var PopupWindow = Java.use("android.widget.PopupWindow"); PopupWindow.showAtLocation.overload('android.view.View', 'int', 'int', 'int').implementation = function(parent, gravity, x, y) { console.log("[POPUP] 显示弹出窗口: " + this.getClass().getName()); return this.showAtLocation(parent, gravity, x, y); }; console.log("[+] PopupWindow监控就绪"); } catch(e) { console.log("[-] PopupWindow监控失败: " + e.message); } // 3. 监控Snackbar(Material Design提示) try { var Snackbar = Java.use("com.google.android.material.snackbar.Snackbar"); Snackbar.show.implementation = function() { console.log("[SNACKBAR] 显示Snackbar提示"); return this.show(); }; console.log("[+] Snackbar监控就绪"); } catch(e) { console.log("[-] Snackbar监控失败(可能未使用): " + e.message); } // 4. 监控TextView文本变化(动态内容更新) try { var TextView = Java.use("android.widget.TextView"); TextView.setText.overload('java.lang.CharSequence').implementation = function(text) { var textStr = text.toString(); if (textStr.indexOf("会员") !== -1 || textStr.indexOf("到期") !== -1) { console.log("[!!!] TextView设置会员相关文本: " + textStr); // 打印调用栈找到来源 printFilteredStackTrace(); } return this.setText(text); }; console.log("[+] TextView文本监控就绪"); } catch(e) { console.log("[-] TextView监控失败: " + e.message); } // 5. 监控WebView(如果提示来自网页) try { var WebView = Java.use("android.webkit.WebView"); WebView.loadUrl.overload('java.lang.String').implementation = function(url) { if (url.indexOf("http") !== -1) { console.log("[WEBVIEW] 加载URL: " + url); } return this.loadUrl(url); }; console.log("[+] WebView监控就绪"); } catch(e) { console.log("[-] WebView监控失败: " + e.message); } // 6. 保留原有的Toast监控(备用) try { var Toast = Java.use("android.widget.Toast"); var showOverloads = Toast.show.overloads; showOverloads.forEach(function(overload) { overload.implementation = function() { console.log("[TOAST] 显示Toast"); return overload.apply(this, arguments); }; }); console.log("[+] Toast监控就绪"); } catch(e) { console.log("[-] Toast监控失败: " + e.message); } console.log("[*] 增强版UI监控注入完成"); }); // 辅助函数:扫描View中的文本内容 function scanViewText(view, prefix) { try { if (view.getClass().getName().indexOf("TextView") !== -1) { var text = view.getText().toString(); if (text.length > 0) { console.log("[" + prefix + "] 发现文本: " + text); if (text.indexOf("会员") !== -1 || text.indexOf("到期") !== -1) { console.log("[!!!] 发现会员相关提示: " + text); printFilteredStackTrace(); } } } // 如果是ViewGroup,递归扫描子View if (view.getClass().getName().indexOf("ViewGroup") !== -1) { var childCount = view.getChildCount(); for (var i = 0; i < childCount; i++) { try { var child = view.getChildAt(i); scanViewText(child, prefix); } catch(e) {} } } } catch(e) {} } // 辅助函数:打印过滤后的调用栈(只显示应用相关) function printFilteredStackTrace() { Java.perform(function() { var Thread = Java.use("java.lang.Thread"); var stackTrace = Thread.currentThread().getStackTrace(); console.log("[STACK] 调用栈追踪:"); for (var i = 0; i < Math.min(stackTrace.length, 15); i++) { var stackElement = stackTrace[i].toString(); // 过滤掉系统框架调用,只显示应用相关 if (stackElement.indexOf("android.") === -1 && stackElement.indexOf("java.") === -1 && stackElement.indexOf("com.android.") === -1 && stackElement.indexOf("dalvik.") === -1 && stackElement.indexOf("libcore.") === -1) { console.log(" [" + i + "] " + stackElement); } } }); }
执行:
frida -U -p 2341 -l enhanced_ui_monitor.js
结果:

于成功捕获到了关键信息:"会员已到期"的提示是通过TextView设置的,调用栈指向com.nx.assist.ToastUtil类。
这是一个重大突破!
当前情况分析
从调用栈可以看出:
-
提示来源:
com.nx.assist.ToastUtil(Toast工具类) -
调用路径:
ILil→IL1Iii→iI1i丨I.run(可能是异步任务,代码被混淆了) -
关键信息:提示显示在
ToastUtil.java的第21行左右
关键信息:
-
真实类名:
com.nx.assist.ILL丨Ii(混淆后的) -
方法名:
ILil、IL1Iii、run -
源文件:
ToastUtil.java(但类名已被混淆)
基于这个发现,我们就需要精准hook了:
创建专门的Hook脚本,针对ToastUtil进行监控:
// fixed_toast_util_hook.js Java.perform(function() { console.log("[*] 修正版ToastUtil Hook脚本启动"); // 1. 使用正确的混淆类名 try { var ToastUtilClass = Java.use("com.nx.assist.ILL丨Ii"); console.log("[+] 成功加载混淆后的ToastUtil类: com.nx.assist.ILL丨Ii"); // 获取类的所有方法 var methods = ToastUtilClass.class.getDeclaredMethods(); console.log("[*] 类方法列表:"); methods.forEach(function(method) { var methodName = method.getName(); var parameterTypes = method.getParameterTypes(); var params = []; for (var i = 0; i < parameterTypes.length; i++) { params.push(parameterTypes[i].getSimpleName()); } console.log(" - " + methodName + "(" + params.join(", ") + ")"); // Hook所有可能显示Toast的方法(基于方法名特征) if (methodName.toLowerCase().indexOf("show") !== -1 || methodName.toLowerCase().indexOf("toast") !== -1 || methodName.length <= 5) { // 混淆方法通常名字很短 try { // 获取方法的所有重载 var overloads = ToastUtilClass[methodName].overloads; overloads.forEach(function(overload, index) { overload.implementation = function() { console.log("[!!!] ToastUtil." + methodName + " 被调用!"); // 打印参数 for (var i = 0; i < arguments.length; i++) { try { var argValue = arguments[i] ? arguments[i].toString() : "null"; if (argValue.length > 100) argValue = argValue.substring(0, 100) + "..."; console.log(" 参数" + i + ": " + argValue); } catch(e) { console.log(" 参数" + i + ": [无法转换]"); } } // 调用原始方法 var result = overload.apply(this, arguments); console.log(" 返回: " + result); return result; }; }); console.log("[+] 已Hook方法: " + methodName); } catch(e) { console.log("[-] Hook方法失败: " + methodName + " - " + e.message); } } }); } catch(e) { console.log("[-] 直接Hook失败: " + e.message); // 备用方案:动态查找 dynamicFindToastClass(); } // 2. 也尝试另一个可能的类 try { var AnotherToastClass = Java.use("com.nx.assist.iI1i丨I"); console.log("[+] 成功加载另一个Toast相关类: com.nx.assist.iI1i丨I"); // Hook run方法(从调用栈中看到) if (AnotherToastClass.run) { AnotherToastClass.run.implementation = function() { console.log("[!!!] iI1i丨I.run 被调用"); return this.run(); }; } } catch(e) { console.log("[-] 第二个类Hook失败: " + e.message); } }); // 动态查找Toast相关类 function dynamicFindToastClass() { console.log("[*] 开始动态查找Toast相关类..."); Java.enumerateLoadedClasses({ onMatch: function(className) { // 查找包含关键字的类 if (className.indexOf("com.nx.assist") !== -1) { console.log("[CLASS] 发现辅助类: " + className); // 如果类名包含Toast相关关键词 if (className.toLowerCase().indexOf("toast") !== -1 || className.toLowerCase().indexOf("util") !== -1) { console.log("[!!!] 可能找到ToastUtil类: " + className); try { var clazz = Java.use(className); var methods = clazz.class.getDeclaredMethods(); methods.forEach(function(method) { var methodName = method.getName(); if (methodName.toLowerCase().indexOf("show") !== -1) { console.log(" [METHOD] 可能显示方法: " + methodName); } }); } catch(e) {} } } }, onComplete: function() { console.log("[*] 动态查找完成"); } }); }
运行这个脚本:
frida -U -p 2341 -l toast_util_hook.js
结果如下:

点一下登录按钮,监听结果如下:

记住可读性差,不是不可读,耐心慢慢读就行了;
完整的调用流程分析如下:
1. [!!!] iI1i丨I.run 被调用 ← 异步任务启动 2. [!!!] ToastUtil.ILil 被调用 ← 显示Toast 参数0: 会员已到期 ← 验证失败提示 参数1: null 参数2: null 参数3: 22 ← 显示时长(22毫秒) 3. [!!!] ToastUtil.ILil 被调用 ← 创建UI组件 参数0: com.nx.assist.ILL丨Ii@fa46306 返回: android.widget.RelativeLayout 4. [!!!] ToastUtil.I1I 被调用 ← 其他UI操作
绕过会员验证
直接修改Toast内容(最直接,不过可能没有用)
// bypass_toast.js Java.perform(function() { console.log("[*] 开始绕过会员验证 - Toast内容修改方案"); // Hook ToastUtil.ILil 方法(显示Toast的核心方法) var ToastUtil = Java.use("com.nx.assist.ILL丨Ii"); // 针对显示文本的ILil重载(参数为String, int, int, int) ToastUtil.ILil.overload('java.lang.String', 'int', 'int', 'int').implementation = function(message, duration, x, y) { console.log("[TOAST] 原始消息: " + message); // 如果检测到会员到期提示,修改为成功消息 if (message.indexOf("会员已到期") !== -1 || message.indexOf("会员") !== -1) { console.log("[!!!] 检测到会员验证失败,修改提示为成功!"); message = "登录成功!"; // 修改提示内容 // 或者完全禁止显示:return; } return this.ILil(message, duration, x, y); }; console.log("[+] Toast内容修改Hook就绪"); });
执行脚本:
frida -U -p 2341 -l bypass_toast.js
结果:文言描述改为我们想要的了,但是功能还是无法使用

结论: 说明仅仅修改Toast提示是不够的,我们需要绕过实际的验证逻辑
想一想,这种脚本大概率都是网络验证,所以我们有以下思路跳过验证:
1.Hook验证逻辑的源头,跳过验证逻辑,直接返回成功;
2.各种验证逻辑类:auth,valid,verify,check等 里面的验证方法强制修改成 true , 1 success
3.网络请求拦截: 如果发现验证请求,要么直接监控这个请求,并修改请求后的参数; 要么找到他的请求url本地修改host替换一个假的在线服务;
那就耐心点,一个个尝试呗!
本节课,因篇幅有限,先到这里,下节课继续,(我上个厕所0.o);
二、常见问题与解决方案 (没问题,可跳过)
注意:所有技术仅用于学习和安全研究,请遵守相关法律法规
浙公网安备 33010602011771号