小柏实战学习安卓图文教程-第十三课-Frida动态脱壳2

本节课主题:实战跳过CrackMe.apk 的登录验证;

 上节课总结:

1.Hook验证逻辑的源头,跳过验证逻辑,直接返回成功;

2.各种验证逻辑类:auth,valid,verify,check等 里面的验证方法强制修改成 true , 1 success

3.网络请求拦截: 如果发现验证请求,要么直接监控这个请求,并修改请求后的参数; 要么找到他的请求url本地修改host替换一个假的在线服务;

 

干!

方案一:Hook验证逻辑的源头

脚本如下,自己创建文件后执行文件:

// bypass_validation_source.js
Java.perform(function() {
    console.log("[*] 开始绕过验证逻辑源头");
    
    // 1. 根据调用栈,找到验证逻辑的入口
    // 之前的调用栈显示:iI1i丨I.run → IL1Iii → ILil
    // 我们需要Hook iI1i丨I.run 方法
    
    try {
        var ValidationTask = Java.use("com.nx.assist.iI1i丨I");
        console.log("[+] 找到验证任务类: com.nx.assist.iI1i丨I");
        
        // Hook run 方法(验证逻辑的入口)
        ValidationTask.run.implementation = function() {
            console.log("[!!!] 验证任务被触发,开始绕过...");
            
            // 方案A:直接返回,不执行验证
            console.log("[BYPASS] 跳过验证逻辑,直接返回成功");
            return; // 不执行原逻辑
            
            // 方案B:如果需要返回值,根据实际情况返回
            // 如果是布尔型验证:return true;
            // 如果是整型验证:return 1; (成功)
        };
        
    } catch(e) {
        console.log("[-] 直接Hook验证任务失败: " + e.message);
    }
    
    // 2. 同时Hook可能存在的验证方法
    try {
        // 搜索所有可能包含验证逻辑的类
        Java.enumerateLoadedClasses({
            onMatch: function(className) {
                if (className.indexOf("com.nx.") !== -1) {
                    // 检查类名是否包含验证相关关键词
                    var lowerClassName = className.toLowerCase();
                    if (lowerClassName.indexOf("check") !== -1 || 
                        lowerClassName.indexOf("verify") !== -1 ||
                        lowerClassName.indexOf("valid") !== -1 ||
                        lowerClassName.indexOf("auth") !== -1) {
                        
                        console.log("[CLASS] 发现验证相关类: " + className);
                        
                        try {
                            var clazz = Java.use(className);
                            var methods = clazz.class.getDeclaredMethods();
                            
                            methods.forEach(function(method) {
                                var methodName = method.getName();
                                var returnType = method.getReturnType().getName();
                                
                                // 重点关注返回布尔值或整型的方法(通常是验证结果)
                                if (returnType === "boolean" || returnType === "int" || returnType === "java.lang.String") {
                                    console.log("  [METHOD] 可能验证方法: " + methodName + " → " + returnType);
                                    
                                    // Hook这些方法
                                    try {
                                        clazz[methodName].overloads.forEach(function(overload) {
                                            overload.implementation = function() {
                                                console.log("[VERIFY] 调用验证: " + className + "." + methodName);
                                                
                                                // 根据返回类型强制返回成功
                                                if (returnType === "boolean") {
                                                    console.log("[BYPASS] 布尔验证 → 强制返回 true");
                                                    return true;
                                                } else if (returnType === "int") {
                                                    console.log("[BYPASS] 整型验证 → 强制返回 1");
                                                    return 1;
                                                } else if (returnType === "java.lang.String") {
                                                    console.log("[BYPASS] 字符串验证 → 强制返回 'success'");
                                                    return "success";
                                                }
                                                
                                                return overload.apply(this, arguments);
                                            };
                                        });
                                    } catch(e) {}
                                }
                            });
                        } catch(e) {}
                    }
                }
            },
            onComplete: function() {
                console.log("[*] 验证类扫描完成");
            }
        });
    } catch(e) {
        console.log("[-] 验证类扫描失败: " + e.message);
    }
});

 

方案二:网络请求拦截(如果验证涉及服务器)

// bypass_network_validation.js
Java.perform(function() {
    console.log("[*] 开始拦截网络验证请求");
    
    // 1. 监控HTTP请求
    try {
        var URL = Java.use("java.net.URL");
        URL.openConnection.implementation = function() {
            var url = this.toString();
            console.log("[NETWORK] 请求URL: " + url);
            
            // 如果是验证相关的请求,可以修改响应
            if (url.indexOf("verify") !== -1 || url.indexOf("check") !== -1 || url.indexOf("vip") !== -1) {
                console.log("[!!!] 发现验证请求: " + url);
            }
            
            return this.openConnection();
        };
    } catch(e) {}
    
    // 2. 监控OkHttp(如果使用)
    try {
        var OkHttpClient = Java.use("okhttp3.OkHttpClient");
        var RealCall = Java.use("okhttp3.RealCall");
        RealCall.execute.implementation = function() {
            var request = this.request();
            var url = request.url().toString();
            console.log("[OKHTTP] 请求: " + url);
            return this.execute();
        };
    } catch(e) {}
});

 

方案三:综合绕过脚本(推荐尝试)

// comprehensive_bypass.js
Java.perform(function() {
    console.log("[*] 综合绕过脚本启动");
    
    var bypassEnabled = true;
    
    // 1. 修改Toast显示(用户体验)
    var ToastUtil = Java.use("com.nx.assist.ILL丨Ii");
    ToastUtil.ILil.overload('java.lang.String', 'int', 'int', 'int').implementation = function(message, duration, x, y) {
        if (bypassEnabled && message && message.indexOf("会员") !== -1) {
            console.log("[UI] 修改提示: '" + message + "' → '验证通过'");
            message = "验证通过,欢迎使用!";
        }
        return this.ILil(message, duration, x, y);
    };
    
    // 2. 拦截验证任务
    try {
        var ValidationTask = Java.use("com.nx.assist.iI1i丨I");
        ValidationTask.run.implementation = function() {
            if (bypassEnabled) {
                console.log("[!!!] 拦截验证任务,直接返回成功");
                return; // 跳过验证
            }
            return this.run();
        };
    } catch(e) {
        console.log("[-] 验证任务Hook失败: " + e.message);
    }
    
    // 3. 修改系统属性读取(如果有本地验证)
    try {
        var System = Java.use("java.lang.System");
        System.getProperty.overload('java.lang.String').implementation = function(key) {
            if (bypassEnabled && key && (key.indexOf("vip") !== -1 || key.indexOf("member") !== -1)) {
                console.log("[SYSTEM] 读取属性: " + key + " → 返回 'true'");
                return "true";
            }
            return this.getProperty(key);
        };
    } catch(e) {}
    
    // 4. 修改SharedPreferences(本地存储的验证状态)
    try {
        var SharedPreferences = Java.use("android.content.SharedPreferences");
        SharedPreferences.getBoolean.overload('java.lang.String', 'boolean').implementation = function(key, defValue) {
            if (bypassEnabled && key && (key.indexOf("vip") !== -1 || key.indexOf("is_login") !== -1)) {
                console.log("[SP] 读取布尔值: " + key + " → 返回 true");
                return true;
            }
            return this.getBoolean(key, defValue);
        };
    } catch(e) {}
    
    console.log("[*] 综合绕过脚本就绪");
});

上面三个尝试都不行,都是假的通过,实际没有通过,

 

可能的原因:

1.验证逻辑不止一处:可能有多重验证机制,用户只绕过了一部分。

2.验证在Native层:验证逻辑可能在.so文件中,而不是Java层。

3.网络验证:应用可能依赖服务器端验证,本地绕过无效。

4.签名或完整性检查:应用可能检测到Frida或代码修改,拒绝运行。

5.混淆和反调试:验证逻辑被深度混淆,用户Hook的点不正确。

 

我们监听捕获到了Toast显示"会员已到期",并找到了相关的类和方法。但现在绕过Toast和验证任务后,功能仍无效,说明需要更深入的分析。

 

下一步建议:

1.动态分析验证流程:使用Frida跟踪整个验证过程,包括网络请求、文件读写、系统调用等。

2.检查网络请求:验证可能涉及服务器通信,需要监控网络活动。

3.Native层监控:如果验证在C/C++层,需要使用Frida的NativeAPI。

4.完整性检查:应用可能检查自身完整性,防止篡改,需要绕过这些检查。

5.多维度Hook:同时Hook多个点,确保覆盖所有验证路径。

 

综上所属我们接下来的具体行动方案:

步骤1:监控网络请求

-使用FridaHook网络库(如OkHttp、HttpURLConnection)来捕获所有网络流量。

-检查是否有验证请求发送到服务器,并分析响应。

步骤2:Native层分析

-使用Frida的NativeAPI来Hook .so文件中的函数。-特别是查找与验证、解密、签名相关的函数。

步骤3:全面方法跟踪

-使用Frida的Stalker功能来跟踪代码执行流,找到真正的验证点。

-或者使用Frida的API来跟踪所有方法调用,过滤出关键路径。

步骤4:反反调试-应用可能检测Frida,需要实施反反调试措施,如隐藏Frida痕迹。

步骤5:静态分析辅助

 

一:网络请求监控(关键!)

很多应用会向服务器验证会员状态,本地绕过无效。创建网络监控脚本:

// network_monitor.js
Java.perform(function() {
    console.log("[*] 修正版网络监控启动");

    // 1. 修复URL.openConnection监控 - 处理所有重载
    try {
        var URL = Java.use("java.net.URL");
        
        // Hook无参数重载
        URL.openConnection.overload().implementation = function() {
            var url = this.toString();
            if (url.indexOf("http") !== -1) {
                console.log("[NETWORK] 请求URL: " + url);
                
                if (isValidationRequest(url)) {
                    console.log("[!!!] 可能验证请求: " + url);
                    printFilteredStackTrace();
                }
            }
            return this.openConnection();
        };
        
        // Hook带Proxy参数的重载
        URL.openConnection.overload('java.net.Proxy').implementation = function(proxy) {
            var url = this.toString();
            if (url.indexOf("http") !== -1) {
                console.log("[NETWORK] 代理请求URL: " + url);
                
                if (isValidationRequest(url)) {
                    console.log("[!!!] 可能验证请求: " + url);
                    printFilteredStackTrace();
                }
            }
            return this.openConnection(proxy);
        };
        
        console.log("[+] URL.openConnection监控就绪");
    } catch(e) {
        console.log("[-] URL监控失败: " + e.message);
    }

    // 2. 监控HttpURLConnection的更具体方法
    try {
        var HttpURLConnection = Java.use("java.net.HttpURLConnection");
        
        // Hook连接建立
        HttpURLConnection.connect.implementation = function() {
            var url = this.getURL().toString();
            console.log("[HTTP] 连接: " + url);
            
            if (isValidationRequest(url)) {
                console.log("[!!!] HTTP验证请求: " + url);
                printFilteredStackTrace();
            }
            
            return this.connect();
        };
        
        // Hook获取输入流(实际数据请求)
        HttpURLConnection.getInputStream.implementation = function() {
            var url = this.getURL().toString();
            console.log("[HTTP] 获取输入流: " + url);
            
            if (isValidationRequest(url)) {
                console.log("[!!!] HTTP验证数据请求: " + url);
                printFilteredStackTrace();
            }
            
            return this.getInputStream();
        };
        
        console.log("[+] HttpURLConnection监控就绪");
    } catch(e) {
        console.log("[-] HttpURLConnection监控失败: " + e.message);
    }

    // 3. 监控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);
    }

    console.log("[*] 网络监控脚本注入完成");
});

// 辅助函数:判断是否是验证请求
function isValidationRequest(url) {
    return url && (url.indexOf("verify") !== -1 || 
                   url.indexOf("vip") !== -1 || 
                   url.indexOf("member") !== -1 || 
                   url.indexOf("auth") !== -1 ||
                   url.indexOf("check") !== -1 ||
                   url.indexOf("validate") !== -1);
}

// 辅助函数:打印过滤后的调用栈
function printFilteredStackTrace() {
    Java.perform(function() {
        var stackTrace = Java.use("android.util.Log").getStackTraceString(
            Java.use("java.lang.Exception").$new()
        );
        var lines = stackTrace.split("\n");
        
        console.log("[STACK] 过滤后的调用栈:");
        for (var i = 2; i < Math.min(lines.length, 10); i++) {
            var line = lines[i].trim();
            if (line.length > 0 && 
                line.indexOf("android.") === -1 && 
                line.indexOf("java.") === -1 &&
                line.indexOf("com.android.") === -1) {
                console.log("  => " + line);
            }
        }
    });
}

结果是点了登录没有反应:

image

 
点击登录按钮后网络监控脚本没有捕获到任何请求,

这确实表明验证逻辑可能不在Java层或网络层,而很可能在Native层(.so文件)中实现

因为许多加固应用会将核心验证逻辑放在Native代码中以增加反调试难度。

 

以下是针对Native层验证的完整解决方案:

为什么需要监控Native层?

  1. 核心逻辑隐藏:商业级加固常将关键验证放在Native层,避免Java层被轻易Hook

  2. 反调试机制:Native代码更容易检测调试器和Frida

  3. 性能考虑:敏感操作(如加密验证)在Native层执行更快

 

Native层监控方案:

方案一:基础Native模块分析

// native_basic_monitor.js
Java.perform(function() {
    console.log("[*] 开始Native层基础分析");
    
    // 1. 枚举所有已加载的Native模块
    console.log("[*] 已加载的Native模块:");
    Process.enumerateModules({
        onMatch: function(module) {
            // 过滤出可能包含验证逻辑的模块
            if (module.name.indexOf("nz") !== -1 || 
                module.name.indexOf("nx") !== -1 ||
                module.name.indexOf("liushi") !== -1 ||
                module.name.indexOf("vip") !== -1 ||
                module.name.indexOf("auth") !== -1) {
                console.log("[MODULE] 发现相关模块: " + module.name + 
                          " 基址: " + module.base + 
                          " 大小: " + module.size + " bytes");
                
                // 列出模块的导出函数
                module.enumerateExports().slice(0, 10).forEach(function(exp) {
                    console.log("  [EXPORT] " + exp.name + " @ " + exp.address);
                });
            }
        },
        onComplete: function() {
            console.log("[*] 模块枚举完成");
        }
    });
    
    // 2. 监控JNI调用(Java与Native的桥梁)
    setTimeout(function() {
        console.log("[*] 开始监控JNI调用...");
        
        // 查找可能的JNI函数
        var symbols = Process.findModuleByName("libart.so").enumerateSymbols();
        symbols.forEach(function(symbol) {
            if (symbol.name.indexOf("CheckJNI") !== -1 || 
                symbol.name.indexOf("JNI") !== -1) {
                console.log("[JNI] 发现JNI相关符号: " + symbol.name);
            }
        });
    }, 1000);
});

 

image

 

 

方案二:深度Native函数Hook

// native_deep_hook.js
Java.perform(function() {
    console.log("[*] Native层深度Hook启动");
    
    // 1. 首先查找应用相关的Native库
    var targetModules = [];
    Process.enumerateModules({
        onMatch: function(module) {
            // 寻找应用自定义的Native库
            if (module.path.indexOf("com.liushi.nz") !== -1 || 
                module.name.indexOf("nz") !== -1 ||
                module.name.indexOf("nx") !== -1) {
                console.log("[!!!] 发现目标Native库: " + module.name + " @ " + module.path);
                targetModules.push(module);
                
                // 尝试Hook这个库的导出函数
                hookModuleExports(module);
            }
        },
        onComplete: function() {
            console.log("[*] 共发现 " + targetModules.length + " 个目标模块");
        }
    });
    
    function hookModuleExports(module) {
        var exports = module.enumerateExports();
        console.log("[*] 模块 " + module.name + " 有 " + exports.length + " 个导出函数");
        
        // Hook前20个导出函数(避免过多)
        exports.slice(0, 20).forEach(function(exp) {
            try {
                // 只Hook可能相关的函数(根据命名模式)
                if (isSuspiciousFunction(exp.name)) {
                    console.log("[HOOK] 尝试Hook: " + exp.name + " @ " + exp.address);
                    
                    Interceptor.attach(exp.address, {
                        onEnter: function(args) {
                            console.log("[NATIVE] 调用: " + exp.name);
                            console.log("  [ARGS] 参数个数: " + this.context.argc);
                            
                            // 打印调用栈(Native层)
                            console.log("  [BACKTRACE]:");
                            this.backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE);
                            this.backtrace.forEach(function(frame, i) {
                                var module = Process.findModuleByAddress(frame);
                                if (module) {
                                    console.log("    " + i + ": " + module.name + "+" + (frame - module.base));
                                }
                            });
                        },
                        onLeave: function(retval) {
                            console.log("  [RETURN]: " + retval);
                        }
                    });
                }
            } catch(e) {
                console.log("[-] Hook失败: " + exp.name + " - " + e.message);
            }
        });
    }
    
    function isSuspiciousFunction(name) {
        // 根据常见验证函数命名模式过滤
        var patterns = ["verify", "check", "valid", "auth", "vip", "member", "license", "init"];
        return patterns.some(function(pattern) {
            return name.toLowerCase().indexOf(pattern) !== -1;
        });
    }
});

 

image

 重要线索:

[!!!] 发现目标Native库: libassist.so @ /data/app/com.liushi.nz-8v4srfVt64fAsUl9lmPT3w==/lib/x86/libassist.so
[*] 模块 libassist.so 有 7346 个导出函数

 

 

方案三:字符串内存扫描

// native_string_scan.js
Java.perform(function() {
    console.log("[*] 开始Native层字符串扫描");
    
    // 在内存中搜索中文字符串(如"会员已到期")
    var patterns = ["会员已到期", "会员", "验证失败", "登录成功", "vip", "auth"];
    
    patterns.forEach(function(pattern) {
        // 将字符串转换为字节序列
        var patternBytes = [];
        for (var i = 0; i < pattern.length; i++) {
            patternBytes.push(pattern.charCodeAt(i));
        }
        
        // 扫描所有可读内存区域
        Process.enumerateRanges('r--').forEach(function(range) {
            if (range.size > 1000) { // 只扫描大于1KB的区域
                try {
                    Memory.scan(range.base, range.size, patternBytes, {
                        onMatch: function(address, size) {
                            console.log("[!!!] 发现字符串 '" + pattern + "' 在: " + address);
                            console.log("      模块: " + (Process.findModuleByAddress(address) || {}).name);
                            console.log("      偏移: " + address.sub(range.base));
                            
                            // 尝试反汇编附近的代码
                            try {
                                var insn = Instruction.parse(address);
                                console.log("      指令: " + insn.toString());
                            } catch(e) {}
                        },
                        onComplete: function() {}
                    });
                } catch(e) {}
            }
        });
    });
});

 

 

 

上面的看了一圈,

这个 libassist.so库非常可疑:

  • 库名assist(辅助)暗示它可能包含核心功能逻辑

  • 函数数量:7346个导出函数说明这是一个功能丰富的核心库

  • 路径:位于应用私有目录,是应用的自定义Native库

 

所以验证逻辑很可能分布在Java层和Native层

  • Java层:处理UI交互和Toast显示(如之前捕获的"会员已到期")

  • Native层:在libassist.so中进行实际的身份验证和会员状态检查

  • 🔄 协同工作:Java层调用Native层进行验证,然后根据返回结果显示结果

 

深入分析libassist.so

以下是针对性的Native层深度分析脚本:

// libassist_deep_hook.js
Java.perform(function() {
    console.log("[*] 开始深度分析 libassist.so");
    
    // 1. 定位并Hook libassist.so
    var libassist = Process.findModuleByName("libassist.so");
    if (libassist) {
        console.log("[!!!] 发现目标库: " + libassist.name);
        console.log("     基址: " + libassist.base);
        console.log("     大小: " + libassist.size + " bytes");
        console.log("     路径: " + libassist.path);
        
        // 2. 枚举导出函数,寻找验证相关函数
        var exports = libassist.enumerateExports();
        console.log("[*] 扫描导出函数...");
        
        var suspiciousFunctions = [];
        var patterns = ["verify", "check", "valid", "auth", "vip", "member", "login", "init"];
        
        exports.forEach(function(exp) {
            var name = exp.name;
            patterns.forEach(function(pattern) {
                if (name.toLowerCase().indexOf(pattern) !== -1) {
                    suspiciousFunctions.push({
                        name: name,
                        address: exp.address,
                        type: exp.type
                    });
                }
            });
        });
        
        console.log("[*] 发现 " + suspiciousFunctions.length + " 个可疑函数");
        
        // 3. Hook可疑函数
        suspiciousFunctions.forEach(function(func) {
            console.log("[HOOK] 尝试Hook: " + func.name + " @ " + func.address);
            
            try {
                Interceptor.attach(func.address, {
                    onEnter: function(args) {
                        console.log("[!!!] libassist." + func.name + " 被调用");
                        
                        // 记录参数
                        for (var i = 0; i < Math.min(args.length, 6); i++) {
                            try {
                                var argValue = args[i];
                                if (argValue instanceof NativePointer) {
                                    try {
                                        var str = argValue.readCString();
                                        if (str && str.length < 100) {
                                            console.log("    参数" + i + " (字符串): " + str);
                                        } else {
                                            console.log("    参数" + i + " (指针): " + argValue);
                                        }
                                    } catch(e) {
                                        console.log("    参数" + i + " (指针): " + argValue);
                                    }
                                } else {
                                    console.log("    参数" + i + ": " + argValue);
                                }
                            } catch(e) {
                                console.log("    参数" + i + ": [读取失败]");
                            }
                        }
                        
                        // 记录调用栈(Native层)
                        console.log("    [BACKTRACE]:");
                        var backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE);
                        for (var i = 0; i < Math.min(backtrace.length, 8); i++) {
                            var module = Process.findModuleByAddress(backtrace[i]);
                            if (module) {
                                console.log("      " + i + ": " + module.name + "+" + (backtrace[i] - module.base));
                            } else {
                                console.log("      " + i + ": " + backtrace[i]);
                            }
                        }
                    },
                    onLeave: function(retval) {
                        console.log("    返回: " + retval);
                        
                        // 如果是验证函数,尝试修改返回值
                        if (func.name.indexOf("verify") !== -1 || func.name.indexOf("check") !== -1) {
                            console.log("[!!!] 尝试绕过验证,强制返回成功 (1)");
                            retval.replace(ptr(0x1)); // 返回1表示成功
                        }
                    }
                });
            } catch(e) {
                console.log("[-] Hook失败: " + func.name + " - " + e.message);
            }
        });
        
        // 4. 监控JNI调用(Java与Native的桥梁)
        console.log("[*] 监控JNI调用...");
        var JNIEnv = Java.vm.getEnv();
        
        // Hook RegisterNatives来发现动态注册的Native方法
        var RegisterNatives = Module.findExportByName("libart.so", "JNI_RegisterNatives");
        if (RegisterNatives) {
            Interceptor.attach(RegisterNatives, {
                onEnter: function(args) {
                    var clazz = args[1];
                    var methods = args[2];
                    var nMethods = args[3];
                    
                    console.log("[JNI] 注册 " + nMethods + " 个Native方法");
                    
                    // 尝试获取类名
                    try {
                        var className = Java.vm.tryGetEnv().getClassName(clazz);
                        console.log("     类: " + className);
                    } catch(e) {}
                }
            });
        }
    } else {
        console.log("[-] 未找到 libassist.so");
    }
    
    console.log("[*] libassist.so 分析脚本就绪");
});

运行脚本后,

点击登录按钮,观察输出。

重点关注:

  • 哪些libassist.so函数被调用

  • 函数的参数和返回值

  • 调用栈信息

image

 

一、脚本执行结果总结

从输出日志看,脚本成功识别了 libassist.so库(基址 0xd320c000,大小约 10MB),并枚举了 390 个"可疑函数"(名称包含 verifycheckinit等关键词)。脚本尝试 Hook 这些函数,但出现了以下关键问题:

  1. 部分 Hook 失败:一些函数无法拦截(如 CMS_Attributes_Verify_it),可能由于地址无效或内存保护。

  2. 运行时错误:当函数被调用时(如 BIO_set_init),脚本在访问参数时抛出 RangeError: invalid array index,表明在读取函数参数时索引越界。

  3. 进程崩溃:最终进程因空指针解引用(SIGSEGV)崩溃,调用栈显示崩溃发生在 libc.sostrlen函数中,源头是 libassist.so内的 Lua 相关函数(如 lua_pcallk)。这可能是由于 Hook 逻辑错误地修改了内存或参数。

二、问题根因分析

  1. 脚本逻辑缺陷

    • 批量 Hook 所有"可疑函数"过于激进,未验证函数签名。Native 函数参数数量和类型未知,直接通过索引访问 args[i]可能导致越界。

    • onLeave回调中强制修改返回值(如 retval.replace(ptr(0x1)))可能破坏函数约定,导致后续代码处理无效数据而崩溃。

  2. 库复杂性

    • libassist.so是一个大型库(7346 个导出函数),内含 OpenSSL、Lua 等第三方组件。盲目 Hook 可能干扰底层逻辑(如加密操作或脚本执行)。

    • 崩溃调用栈中频繁出现 lua_前缀函数(如 lua_pcallk),表明该库使用 Lua 脚本引擎。若 Hook 影响了 Lua 虚拟机状态,易导致崩溃。

  3. 反调试机制

    • 应用可能检测到 Frida 的 Hook 行为,主动触发崩溃以阻止分析。崩溃地址 0x1(无效指针)暗示可能是有意的反调试陷阱。

 

三、再改一版

// safe_libassist_monitor.js
Java.perform(function() {
    console.log("[*] 安全监控脚本启动 - 仅日志记录,不修改行为");

    var libassist = Process.findModuleByName("libassist.so");
    if (!libassist) {
        console.log("[-] 未找到 libassist.so");
        return;
    }

    // 1. 只 Hook 少数关键函数,避免批量操作
    var targetedFunctions = [
        "lua_pcallk",    // Lua 调用相关
        "BIO_set_init",  // 加密相关
        "EVP_DecodeInit" // Base64 解码
    ];

    targetedFunctions.forEach(funcName => {
        var funcAddr = Module.findExportByName("libassist.so", funcName);
        if (funcAddr) {
            Interceptor.attach(funcAddr, {
                onEnter: function(args) {
                    // 安全记录:不访问 args 数组,仅记录调用
                    console.log(`[CALL] ${funcName} 被调用`);
                    
                    // 打印调用栈(限制深度)
                    console.log("  [BACKTRACE]:");
                    var backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0, 5);
                    backtrace.forEach((addr, i) => {
                        var mod = Process.findModuleByAddress(addr);
                        console.log(`    ${i}: ${mod ? mod.name : "unknown"} + ${addr - (mod?.base || 0)}`);
                    });
                },
                onLeave: function(retval) {
                    // 不修改返回值,仅记录
                    console.log(`  [RET] ${funcName} 返回: ${retval}`);
                }
            });
            console.log(`[+] 已安全Hook: ${funcName}`);
        }
    });

    // 2. 监控 JNI 调用,定位 Java 与 Native 交互
    var JNIEnv = Java.vm.getEnv();
    if (JNIEnv) {
        console.log("[*] 监控 JNI 调用...");
        // 可添加 JNI 函数 Hook,如 CallStaticBooleanMethod 等
    }

    console.log("[*] 安全监控就绪");
});

 

前面异常奔溃了,
所以重新找一下app 的 pid:

frida-ps -U

再重新执行:

frida -U -p <PID> -l safe_libassist_monitor.js

 

输出如下:

image

点击登录,结果如下:

image

我们看到了非常重要的信息!验证逻辑确实在Native层执行,特别是通过Lua脚本引擎。

以下是详细分析

1. Lua脚本引擎活跃

[CALL] lua_pcallk 被调用
[RET] lua_pcallk 返回: 0x2
[CALL] lua_pcallk 被调用
[RET] lua_pcallk 返回: 0x0

小结:

lua_pcallk是Lua虚拟机执行函数的关键调用

返回值 0x2和 0x0表示不同的执行状态(Lua调用结果)

 

 2. 加密/解密操作

[CALL] EVP_DecodeInit 被调用
[CALL] BIO_set_init 被调用

小结:

EVP_DecodeInit用于Base64解码初始化

BIO_set_init是OpenSSL BIO系统初始化

 

3. 调用链分析

从调用栈可以看到验证流程:

 

接下来就是 深度Lua监控 : 

以下是针对性的Lua深度监控脚本:

// lua_deep_monitor_fixed.js
Java.perform(function() {
    console.log("[*] 修复版Lua深度监控启动");

    var libassist = Process.findModuleByName("libassist.so");
    if (!libassist) {
        console.log("[-] 未找到 libassist.so");
        return;
    }

    // 1. 安全地监控Lua函数
    var luaFunctions = [
        "lua_pcallk",       // Lua函数调用
        "lua_callk",        // Lua调用
        "luaL_loadstring",  // 加载Lua字符串
        "lua_getglobal",     // 获取全局变量
        "lua_getfield",      // 获取字段
        "lua_tolstring",     // 转换为字符串
        "lua_toboolean"      // 转换为布尔值
    ];

    luaFunctions.forEach(funcName => {
        var funcAddr = Module.findExportByName("libassist.so", funcName);
        if (funcAddr) {
            try {
                Interceptor.attach(funcAddr, {
                    onEnter: function(args) {
                        console.log(`\n[LUA] === ${funcName} 被调用 ===`);
                        
                        // 修复:安全地访问参数
                        try {
                            // 使用安全的参数访问方式
                            var argCount = 0;
                            for (let i = 0; i < 10; i++) { // 最大尝试10个参数
                                try {
                                    if (args[i] !== undefined && !args[i].isNull()) {
                                        argCount = i + 1;
                                    } else {
                                        break;
                                    }
                                } catch(e) {
                                    break;
                                }
                            }
                            
                            // 只访问有效的参数
                            for (let i = 0; i < argCount && i < 6; i++) {
                                try {
                                    if (args[i].isNull()) {
                                        console.log(`  参数${i}: NULL`);
                                        continue;
                                    }
                                    
                                    // 尝试读取指针内容
                                    try {
                                        let str = args[i].readCString();
                                        if (str && str.length > 0) {
                                            if (str.length < 100) {
                                                console.log(`  参数${i} (字符串): "${str}"`);
                                            } else {
                                                console.log(`  参数${i} (长字符串): 长度=${str.length}`);
                                                // 检查关键词
                                                if (str.indexOf("vip") !== -1 || str.indexOf("member") !== -1 || 
                                                    str.indexOf("会员") !== -1) {
                                                    console.log(`    [!!!] 包含关键词: ${str.substring(0, 50)}...`);
                                                }
                                            }
                                        } else {
                                            console.log(`  参数${i}: 指针 0x${args[i]}`);
                                        }
                                    } catch(e) {
                                        console.log(`  参数${i}: 指针 0x${args[i]} [非字符串]`);
                                    }
                                } catch(e) {
                                    console.log(`  参数${i}: [读取失败: ${e.message}]`);
                                }
                            }
                        } catch(e) {
                            console.log("  [参数访问错误]: " + e.message);
                        }

                        // 安全地获取调用栈
                        try {
                            console.log("  [调用栈]:");
                            let backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE);
                            for (let i = 0; i < Math.min(backtrace.length, 5); i++) {
                                let addr = backtrace[i];
                                let mod = Process.findModuleByAddress(addr);
                                if (mod && mod.name === "libassist.so") {
                                    let offset = addr - mod.base;
                                    console.log(`    ${i}: ${mod.name} + 0x${offset.toString(16)}`);
                                } else if (i < 2) {
                                    let mod = Process.findModuleByAddress(addr);
                                    if (mod) {
                                        console.log(`    ${i}: ${mod.name} + 0x${(addr - mod.base).toString(16)}`);
                                    } else {
                                        console.log(`    ${i}: 0x${addr}`);
                                    }
                                }
                            }
                        } catch(e) {
                            console.log("  [调用栈获取失败]");
                        }
                    },
                    onLeave: function(retval) {
                        console.log(`  [返回]: ${retval}`);
                        
                        // 对于lua_tolstring,安全地读取返回字符串
                        if (funcName === "lua_tolstring" && retval && !retval.isNull()) {
                            try {
                                let str = retval.readCString();
                                if (str && str.length > 0) {
                                    let displayStr = str.length > 200 ? str.substring(0, 200) + "..." : str;
                                    console.log(`  [LUA-STRING] 内容: "${displayStr}"`);
                                    
                                    // 检查关键内容
                                    if (str.indexOf("会员") !== -1 || str.indexOf("vip") !== -1 || 
                                        str.indexOf("成功") !== -1 || str.indexOf("失败") !== -1 ||
                                        str.indexOf("到期") !== -1) {
                                        console.log(`  [!!!] 发现关键字符串: ${str.substring(0, 100)}`);
                                    }
                                }
                            } catch(e) {
                                // 忽略读取错误
                            }
                        }
                    }
                });
                console.log(`[+] 已安全Hook: ${funcName}`);
            } catch(e) {
                console.log(`[-] Hook失败 ${funcName}: ${e.message}`);
            }
        } else {
            console.log(`[-] 未找到导出函数: ${funcName}`);
        }
    });

    console.log("[*] Lua深度监控就绪(已修复)");
});

点击登录后的结果如下:

image

关键发现(不容易呀!)

1.Lua脚本控制:应用使用Lua脚本处理登录验证逻辑。

2.网络请求构建:通过Lua函数构建了完整的HTTP请求参数。

3.服务器响应:服务器返回了JSON数据:{"code":201,"msg":"\u4f1a\u5458\u5df2\u5230\u671f","time":1768816973},其中msg字段解码后为“会员已到期”。

4.关键字符串:在最后一步,我们捕获到了“会员已到期”这个字符串。

 小结:

我们看到了完整的登录验证流程

1. 验证机制完全解密

  • 使用Lua脚本:应用通过Lua脚本处理验证逻辑

  • 网络验证:向服务器发送HTTP POST请求进行验证

  • 参数构造:构建完整的登录参数,包括签名验证

  • 服务器响应:服务器返回JSON格式的验证结果

2. 具体验证流程

1. 构建请求参数:
   - account=shaun89
   - password=Qq123456  
   - markcode=3325ded7708e34e8cd4a35ec564b5765
   - t=1768816975 (时间戳)
   - sign=e085be074e277fe063015ac161d2b9b4 (签名)

2. 发送请求:
   POST http://47.96.230.2:5147/api.php?act=user_logon&app=10000&account=shaun89...

3. 服务器返回:
   {"code":201,"msg":"\u4f1a\u5458\u5df2\u5230\u671f","time":1768816973}
   (解码后:"会员已到期")

 

所以,我们的绕过方案:

1.修改服务器响应

// bypass_server_response.js
Java.perform(function() {
    console.log("[*] 服务器响应修改脚本启动");
    
    var libassist = Process.findModuleByName("libassist.so");
    if (!libassist) {
        console.log("[-] 未找到 libassist.so");
        return;
    }
    
    // Hook lua_tolstring 函数,在返回服务器响应时修改内容
    var lua_tolstring = Module.findExportByName("libassist.so", "lua_tolstring");
    if (lua_tolstring) {
        Interceptor.attach(lua_tolstring, {
            onLeave: function(retval) {
                if (retval && !retval.isNull()) {
                    try {
                        var str = retval.readCString();
                        if (str && str.indexOf('"code":201') !== -1) {
                            console.log("[!!!] 发现服务器响应:会员已到期");
                            console.log("  原始响应: " + str);
                            
                            // 修改为成功响应
                            var newResponse = '{"code":200,"msg":"\\u767b\\u5f55\\u6210\\u529f","time":' + Math.floor(Date.now() / 1000) + '}';
                            console.log("[!!!] 修改为: " + newResponse);
                            
                            // 分配新内存并写入修改后的字符串
                            var newPtr = Memory.allocUtf8String(newResponse);
                            retval.replace(newPtr);
                        }
                    } catch(e) {
                        // 忽略读取错误
                    }
                }
            }
        });
        console.log("[+] 已Hook lua_tolstring,准备修改服务器响应");
    }
});

image

结论:还是没效果;

2.修改验证结果判断

// bypass_validation_logic.js
Java.perform(function() {
    console.log("[*] 验证逻辑绕过脚本启动");
    
    // 监控JSON解析,修改code字段
    var targetFunctions = ["lua_tolstring", "lua_tonumber", "lua_toboolean"];
    
    targetFunctions.forEach(funcName => {
        var funcAddr = Module.findExportByName("libassist.so", funcName);
        if (funcAddr) {
            Interceptor.attach(funcAddr, {
                onLeave: function(retval) {
                    if (funcName === "lua_tolstring" && retval && !retval.isNull()) {
                        try {
                            var str = retval.readCString();
                            if (str && (str === "201" || str === "会员已到期")) {
                                console.log("[!!!] 发现验证失败代码/消息,进行修改");
                                
                                if (str === "201") {
                                    // 修改错误代码为成功代码
                                    var newStr = "200";
                                    var newPtr = Memory.allocUtf8String(newStr);
                                    retval.replace(newPtr);
                                } else if (str === "会员已到期") {
                                    // 修改错误消息为成功消息
                                    var newStr = "登录成功";
                                    var newPtr = Memory.allocUtf8String(newStr);
                                    retval.replace(newPtr);
                                }
                            }
                        } catch(e) {}
                    }
                }
            });
        }
    });
});

image

还是不行;

深呼吸,分析一下:

这说明应用可能存在多重验证机制反调试检测

根据我们的分析流程,现在需要深入排查其他可能的验证环节和防御机制。

 这节课到这里,下节课继续

 

 

 

注意:所有技术仅用于学习和安全研究,请遵守相关法律法规

 

posted on 2026-01-19 18:25  shaun88  阅读(4)  评论(0)    收藏  举报

导航