Featured image of post Hunter 设备指纹与 Hook 检测逆向学习实录

Hunter 设备指纹与 Hook 检测逆向学习实录

本文围绕 Hunter 设备指纹与 Hook 检测开展一次完整逆向实战:先在 Java 层通过系统属性、系统服务与服务绑定等关键 API 建立观测链路,再在排除高频全局 Hook 噪音后收敛到业务入口,确认核心逻辑下沉至 Native 层;随后结合 RegisterNatives、dlopen 与静态 JNI 导出符号定位,最终完成 NativeEngine.getZhenxiInfo 系列方法映射,还原设备指纹采集字段与环境风险探测逻辑。

0x00 写在前面

本文记录了一次完整的 Hunter 设备指纹与 Hook 检测逆向实践。为避免分析发散,我先把目标拆成三个可验证问题,并围绕它们逐步推进:

  1. Hunter 具体采集了哪些设备信号与环境特征?
  2. 这些采集到的信号如何映射到页面展示的指纹字段中?
  3. 其风险判定逻辑是单点触发,还是多信号融合决策?

0x01 研究对象与初始现象

通过对指纹页面的初步分析,可以看到三个核心区域:

  1. DeviceBaseInfo(设备基础信息)
  2. JavaDeviceFingerprint(Java 层设备指纹)
  3. NativeDeviceFingerprint(Native 层设备指纹)
  • 其中 NativeDeviceFingerprint 区域呈现出特征性内容:
    • oaid_xxx开放设备标识符(OAID),用于替代 IMEI 的非敏感设备标识
    • ifaa_error_Bind service failed:IFAA(互联网金融身份认证联盟)服务绑定失败,是一个错误日志
    • soter_xxx:腾讯 Soter 安全组件生成的密钥标识
    • 后面的 a_/b_/c_/e_/f_ 开头字符串:是 Native 层采集的各类硬件 / 系统特征值,用于生成更稳定的设备指纹

0x02 动态分析迭代路线

0x02.1 第一阶段:定位核心采集入口

根据上面页面字段先做了一轮关键词反推(如 ro.securero.debuggableifaasoterbiometric),可以先确定两类最可能的入口,再进入后续 Hook:

  1. android.os.SystemProperties.get 用来验证基础系统属性的读取来源。页面里的构建标签、安全状态、调试开关这类信息,通常都会经过这里。
  2. android.app.ContextImpl.getSystemService 用来验证系统能力探测入口。像 IFAA、生物识别、指纹、Keyguard 这类能力查询,常见都会从这个 API 往下走。

这样做的目的,是先把“谁在采集什么”这条主线跑通,再决定下一步补 bindService、业务方法还是 JNI 路线,避免一开始就大面积 Hook 导致噪音失控。

点击展开:完整代码(v1)
 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
Java.perform(function () {
    var TARGET_PKG = "com.zhenxi.hunter";

    function getFirstBizFrame() {
        try {
            var Exception = Java.use("java.lang.Exception");
            var Log = Java.use("android.util.Log");
            var stack = Log.getStackTraceString(Exception.$new());
            if (!stack) return "";
    
            var lines = stack.split("\n");
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();
                if (line.indexOf(TARGET_PKG) !== -1) return line;
            }
        } catch (e) {}
        return "";
    }
    
    function logHit(api, key, value) {
        var frame = getFirstBizFrame();
        var tag = frame ? "[HIT]" : "[HIT_EARLY]";
        var showFrame = frame ? frame : "<no-biz-frame>";
        console.log(tag + " " + api + " | key=" + key + " | ret=" + value + " | frame=" + showFrame);
    }
    
    function hookSystemProperties() {
        var SP = Java.use("android.os.SystemProperties");
    
        var get1 = SP.get.overload("java.lang.String");
        get1.implementation = function (key) {
            var ret = get1.call(this, key);
            var k = (key || "").toLowerCase();
            if (k.indexOf("ro.") === 0) {
                logHit("SystemProperties.get(String)", key, ret);
            }
            return ret;
        };
    
        var get2 = SP.get.overload("java.lang.String", "java.lang.String");
        get2.implementation = function (key, defValue) {
            var ret = get2.call(this, key, defValue);
            var k = (key || "").toLowerCase();
            if (k.indexOf("ro.") === 0) {
                logHit("SystemProperties.get(String,String)", key, ret);
            }
            return ret;
        };
    }
    
    function hookGetSystemService() {
        var Ctx = Java.use("android.app.ContextImpl");
        var getSvc = Ctx.getSystemService.overload("java.lang.String");
    
        getSvc.implementation = function (name) {
            var ret = getSvc.call(this, name);
            var n = (name || "").toLowerCase();
            if (n.indexOf("ifaa") !== -1 || n.indexOf("finger") !== -1 || n.indexOf("biometric") !== -1 || n.indexOf("keyguard") !== -1) {
                logHit("Context.getSystemService", name, ret);
            }
            return ret;
        };
    }
    
    hookSystemProperties();
    hookGetSystemService();
    console.log("[*] hookhunter v1 installed");
});

这段代码的价值很明确:

  • 先做观测,不篡改返回值。
  • 先建立“调用来源地图”,再谈下一步。

阶段结果(实测):

  1. 启动期稳定观测到属性读取(如 ro.miui.notch)。
  2. 观测到 biometrickeyguard 等系统能力查询。
  3. 首个业务栈帧可回溯到 MainActivity.onCreate,验证入口归因可用。

日志说明:下面这组日志证明了 v1 入口观察已生效,且能看到属性读取与系统能力查询。

点击展开:日志输出

[+] hook SystemProperties.get(String) [+] hook SystemProperties.get(String,String) [+] hook ContextImpl.getSystemService(String) [*] hookhunter v1 installed [HIT] SystemProperties.get(String,String) | key=ro.miui.notch | ret=1 | frame=at com.zhenxi.hunter.MainActivity.onCreate(Unknown Source:203) [HIT_EARLY] Context.getSystemService | key=biometric | ret=android.hardware.biometrics.BiometricManager@245b6fa | frame= [HIT_EARLY] Context.getSystemService | key=keyguard | ret=android.app.KeyguardManager@bc6e0d6 | frame=

0x02.2 第二阶段:验证 IFAA/Soter 服务绑定链路

新增重点:ContextWrapper.bindService(Intent, ServiceConnection, int)

点击展开:完整代码(v1.2)
  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
Java.perform(function () {
    var TARGET_PKG = "com.zhenxi.hunter";

    function getFirstBizFrame() {
        try {
            var Exception = Java.use("java.lang.Exception");
            var Log = Java.use("android.util.Log");
            var stack = Log.getStackTraceString(Exception.$new());
            if (!stack) return "";

            var lines = stack.split("\n");
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();
                if (line.indexOf(TARGET_PKG) !== -1) return line;
            }
        } catch (e) {}
        return "";
    }

    function logHit(api, key, value) {
        var frame = getFirstBizFrame();
        var tag = frame ? "[HIT]" : "[HIT_EARLY]";
        var showFrame = frame ? frame : "<no-biz-frame>";
        console.log(tag + " " + api + " | key=" + key + " | ret=" + value + " | frame=" + showFrame);
    }

    function containsKeyword(s) {
        var text = (s || "").toLowerCase();
        return (
            text.indexOf("ifaa") !== -1 ||
            text.indexOf("soter") !== -1 ||
            text.indexOf("bind service failed") !== -1 ||
            text.indexOf("biometric") !== -1 ||
            text.indexOf("finger") !== -1
        );
    }

    function hookSystemProperties() {
        var SP = Java.use("android.os.SystemProperties");

        var get1 = SP.get.overload("java.lang.String");
        get1.implementation = function (key) {
            var ret = get1.call(this, key);
            var k = (key || "").toLowerCase();
            if (k.indexOf("ro.") === 0) logHit("SystemProperties.get(String)", key, ret);
            return ret;
        };

        var get2 = SP.get.overload("java.lang.String", "java.lang.String");
        get2.implementation = function (key, defValue) {
            var ret = get2.call(this, key, defValue);
            var k = (key || "").toLowerCase();
            if (k.indexOf("ro.") === 0) logHit("SystemProperties.get(String,String)", key, ret);
            return ret;
        };
    }

    function hookGetSystemService() {
        var Ctx = Java.use("android.app.ContextImpl");
        var getSvc = Ctx.getSystemService.overload("java.lang.String");
        getSvc.implementation = function (name) {
            var ret = getSvc.call(this, name);
            var n = (name || "").toLowerCase();
            if (n.indexOf("ifaa") !== -1 || n.indexOf("finger") !== -1 || n.indexOf("biometric") !== -1 || n.indexOf("keyguard") !== -1) {
                logHit("Context.getSystemService", name, ret);
            }
            return ret;
        };
    }

    function hookBindService() {
        var CtxWrapper = Java.use("android.content.ContextWrapper");
        function intentSummary(intentObj) {
            try {
                if (!intentObj) return "intent=<null>";
                var action = intentObj.getAction();
                var comp = intentObj.getComponent();
                var pkg = intentObj.getPackage();
                return "action=" + action + ", pkg=" + pkg + ", comp=" + comp;
            } catch (e) {
                return "intent=<error:" + e + ">";
            }
        }

        var b1 = CtxWrapper.bindService.overload("android.content.Intent", "android.content.ServiceConnection", "int");
        b1.implementation = function (intent, conn, flags) {
            var ret = b1.call(this, intent, conn, flags);
            var summary = intentSummary(intent);
            if (containsKeyword(summary)) logHit("ContextWrapper.bindService", summary + ", flags=" + flags, ret);
            return ret;
        };
    }

    function hookAndroidLog() {
        var ALog = Java.use("android.util.Log");

        var e2 = ALog.e.overload("java.lang.String", "java.lang.String");
        e2.implementation = function (tag, msg) {
            var ret = e2.call(this, tag, msg);
            var content = "tag=" + tag + ", msg=" + msg;
            if (containsKeyword(content)) logHit("Log.e(String,String)", content, ret);
            return ret;
        };

        var e3 = ALog.e.overload("java.lang.String", "java.lang.String", "java.lang.Throwable");
        e3.implementation = function (tag, msg, tr) {
            var ret = e3.call(this, tag, msg, tr);
            var content = "tag=" + tag + ", msg=" + msg + ", tr=" + tr;
            if (containsKeyword(content)) logHit("Log.e(String,String,Throwable)", content, ret);
            return ret;
        };

        var w2 = ALog.w.overload("java.lang.String", "java.lang.String");
        w2.implementation = function (tag, msg) {
            var ret = w2.call(this, tag, msg);
            var content = "tag=" + tag + ", msg=" + msg;
            if (containsKeyword(content)) logHit("Log.w(String,String)", content, ret);
            return ret;
        };

        var i2 = ALog.i.overload("java.lang.String", "java.lang.String");
        i2.implementation = function (tag, msg) {
            var ret = i2.call(this, tag, msg);
            var content = "tag=" + tag + ", msg=" + msg;
            if (containsKeyword(content)) logHit("Log.i(String,String)", content, ret);
            return ret;
        };
    }

    hookSystemProperties();
    hookGetSystemService();
    hookBindService();
    hookAndroidLog();
    console.log("[*] hookhunter v1.2 installed");
});
  1. 关键结论:日志可稳定复现以下结果,且与页面字段高度一致:
    1. IFAA 服务绑定返回 false → 对应页面 ifaa_error_Bind service failed
    2. Soter 服务绑定返回 true → 对应页面 soter_xxx 标识存在。

日志说明:下面这组日志直接坐实了 IFAA 失败、Soter 成功两条分支。

点击展开:日志输出

[+] hook SystemProperties.get(String) [+] hook SystemProperties.get(String,String) [+] hook ContextImpl.getSystemService(String) [+] hook ContextWrapper.bindService(Intent,ServiceConnection,int) [+] hook android.util.Log e/w/i [*] hookhunter v1.2 installed [HIT] SystemProperties.get(String,String) | key=ro.miui.notch | ret=1 | frame=at com.zhenxi.hunter.MainActivity.onCreate(Unknown Source:203) [HIT] ContextWrapper.bindService | key=action=org.ifaa.aidl.manager.IfaaManagerService, pkg=org.ifaa.aidl.manager, comp=null, flags=1 | ret=false | frame=at com.zhenxi.hunter.MainActivity.v(SourceFile:0) [HIT] ContextWrapper.bindService | key=action=null, pkg=null, comp=ComponentInfo{com.tencent.soter.soterserver/com.tencent.soter.soterserver.SoterService}, flags=1 | ret=true | frame=at com.zhenxi.hunter.MainActivity.v(SourceFile:0) [HIT_EARLY] Context.getSystemService | key=biometric | ret=android.hardware.biometrics.BiometricManager@4f78087 | frame= [HIT_EARLY] Context.getSystemService | key=keyguard | ret=android.app.KeyguardManager@bbc2ee5 | frame=

0x02.3 第三阶段:一次失败的全局热点 Hook

尝试:全局 Hook 高频基础类方法 StringBuilder.toString + ArrayList.add,试图捕获聚合指纹生成过程

点击展开:完整代码(v1.3,失败样本)
 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
Java.perform(function () {
    var TARGET_PKG = "com.zhenxi.hunter";

    function getFirstBizFrame() {
        try {
            var Exception = Java.use("java.lang.Exception");
            var Log = Java.use("android.util.Log");
            var stack = Log.getStackTraceString(Exception.$new());
            if (!stack) return "";
            var lines = stack.split("\n");
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();
                if (line.indexOf(TARGET_PKG) !== -1) return line;
            }
        } catch (e) {}
        return "";
    }

    function logHit(api, key, value) {
        var frame = getFirstBizFrame();
        var tag = frame ? "[HIT]" : "[HIT_EARLY]";
        var showFrame = frame ? frame : "<no-biz-frame>";
        console.log(tag + " " + api + " | key=" + key + " | ret=" + value + " | frame=" + showFrame);
    }

    function containsKeyword(s) {
        var text = (s || "").toLowerCase();
        return (
            text.indexOf("ifaa") !== -1 ||
            text.indexOf("soter") !== -1 ||
            text.indexOf("oaid") !== -1 ||
            text.indexOf("bind service failed") !== -1 ||
            text.indexOf("error_") !== -1 ||
            text.indexOf("ifaa_error") !== -1
        );
    }

    function hookStringBuilders() {
        var SB = Java.use("java.lang.StringBuilder");
        var sbToString = SB.toString.overload();
        sbToString.implementation = function () {
            var ret = sbToString.call(this);
            if (containsKeyword(ret)) {
                logHit("StringBuilder.toString", "builder", ret);
            }
            return ret;
        };

        var SBuf = Java.use("java.lang.StringBuffer");
        var sbufToString = SBuf.toString.overload();
        sbufToString.implementation = function () {
            var ret = sbufToString.call(this);
            if (containsKeyword(ret)) {
                logHit("StringBuffer.toString", "buffer", ret);
            }
            return ret;
        };
    }

    function hookArrayListAdd() {
        var AL = Java.use("java.util.ArrayList");
        var addObj = AL.add.overload("java.lang.Object");
        addObj.implementation = function (obj) {
            var ret = addObj.call(this, obj);
            try {
                var text = obj ? obj.toString() : "<null>";
                if (containsKeyword(text)) {
                    logHit("ArrayList.add(Object)", "size=" + this.size(), text);
                }
            } catch (e) {}
            return ret;
        };
    }

    hookStringBuilders();
    hookArrayListAdd();
    console.log("[*] hookhunter v1.3 installed");
});

执行结果

  1. 日志噪音极高,有效信息被海量无效调用淹没;
  2. 触发应用进程异常中止。

核心经验

  • 高频基础类方法不适合作为全局主路径 Hook 目标;
  • 逆向分析应回归“业务方法 + 关键 API”,避免无差别全局 Hook。

日志说明:下面这组日志说明全局热点 Hook 的噪音和性能问题都很严重。

点击展开:日志输出

[+] hook StringBuilder/StringBuffer.toString [+] hook ArrayList.add(Object) [*] hookhunter v1.3 installed [HIT_EARLY] StringBuilder.toString | key=builder | ret=google/rpc/error_details.proto | frame= Process terminated

0x02.4 第四阶段:回到可控策略

调整:

  1. 移除高频基础类的全局 Hook;
  2. 聚焦 MainActivity 的核心方法(v / onCreate)。
点击展开:完整代码(v1.3_safe)
 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
Java.perform(function () {
    var TARGET_PKG = "com.zhenxi.hunter";

    function getFirstBizFrame() {
        try {
            var Exception = Java.use("java.lang.Exception");
            var Log = Java.use("android.util.Log");
            var stack = Log.getStackTraceString(Exception.$new());
            if (!stack) return "";
            var lines = stack.split("\n");
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();
                if (line.indexOf(TARGET_PKG) !== -1) return line;
            }
        } catch (e) {}
        return "";
    }

    function logHit(api, key, value) {
        var frame = getFirstBizFrame();
        var tag = frame ? "[HIT]" : "[HIT_EARLY]";
        var showFrame = frame ? frame : "<no-biz-frame>";
        console.log(tag + " " + api + " | key=" + key + " | ret=" + value + " | frame=" + showFrame);
    }

    function containsKeyword(s) {
        var text = (s || "").toLowerCase();
        return (
            text.indexOf("ifaa") !== -1 ||
            text.indexOf("soter") !== -1 ||
            text.indexOf("oaid") !== -1 ||
            text.indexOf("bind service failed") !== -1 ||
            text.indexOf("ifaa_error") !== -1 ||
            text.indexOf("error_") !== -1
        );
    }

    function safeToText(v) {
        try {
            if (v === null || v === undefined) return "<null>";
            return String(v);
        } catch (e) {
            return "<toString_error:" + e + ">";
        }
    }

    function argsToText(args) {
        var out = [];
        for (var i = 0; i < args.length; i++) out.push("a" + i + "=" + safeToText(args[i]));
        return out.join(", ");
    }

    function hookMainActivityFocus() {
        try {
            var Main = Java.use("com.zhenxi.hunter.MainActivity");
            var methodNames = {};
            var declared = Main.class.getDeclaredMethods();
            for (var i = 0; i < declared.length; i++) {
                try { methodNames[declared[i].getName()] = true; } catch (e) {}
            }

            var focus = ["v", "onCreate"];
            for (var j = 0; j < focus.length; j++) {
                var name = focus[j];
                if (!methodNames[name] || !Main[name]) continue;

                var overloads = Main[name].overloads;
                for (var k = 0; k < overloads.length; k++) {
                    (function (mName, ov) {
                        ov.implementation = function () {
                            var argText = argsToText(arguments);
                            var ret;
                            try { ret = ov.apply(this, arguments); }
                            catch (err) {
                                logHit("MainActivity." + mName + "(throw)", argText, err);
                                throw err;
                            }
                            var retText = safeToText(ret);
                            if (mName === "v" || containsKeyword(argText) || containsKeyword(retText)) {
                                logHit("MainActivity." + mName, argText, retText);
                            }
                            return ret;
                        };
                    })(name, overloads[k]);
                }
                console.log("[+] hook MainActivity." + name + " overloads=" + overloads.length);
            }
        } catch (e) {
            console.log("[-] hook MainActivity focus failed: " + e);
        }
    }

    hookMainActivityFocus();
    console.log("[*] hookhunter v1.3_safe installed");
});

关键发现:

  • MainActivity.v 显示为 Native Method,说明 Hunter 关键逻辑下沉到 JNI/Native 层。

日志说明:下面这组日志把 MainActivity.v 的 Native Method 属性直接打了出来。

点击展开:日志输出

[+] hook MainActivity.v overloads=1 [+] hook MainActivity.onCreate overloads=1 [HIT] ContextWrapper.bindService | key=action=org.ifaa.aidl.manager.IfaaManagerService, pkg=org.ifaa.aidl.manager, comp=null, flags=1 | ret=false | frame=at com.zhenxi.hunter.MainActivity.v(SourceFile:0) [HIT] ContextWrapper.bindService | key=action=null, pkg=null, comp=ComponentInfo{com.tencent.soter.soterserver/com.tencent.soter.soterserver.SoterService}, flags=1 | ret=true | frame=at com.zhenxi.hunter.MainActivity.v(SourceFile:0) [HIT] MainActivity.v | key=a0= | ret= | frame=at com.zhenxi.hunter.MainActivity.v(Native Method) [HIT] MainActivity.v | key=a0=[object Object] | ret= | frame=at com.zhenxi.hunter.MainActivity.v(Native Method)

0x02.5 第五阶段:动态注册 JNI 路线验证

尝试 Hook RegisterNativesCheckJNI 相关方法,验证动态注册 JNI 路线

点击展开:完整代码(v1.4c)
 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
'use strict';

if (globalThis.__hunter_v14c_installed) {
    console.log('[*] v1.4c already installed, skip duplicate install');
} else {
    globalThis.__hunter_v14c_installed = true;
    var MAX_PRINT_CALLS = 120;
    var printedCalls = 0;

    function getClassNameSafe(jclassObj) {
        try {
            if (!Java.available) return '<java-unavailable>';
            var env = Java.vm.tryGetEnv();
            if (!env) return '<env-unavailable>';
            return env.getClassName(jclassObj) || '<unknown-class>';
        } catch (e) {
            return '<class-resolve-failed:' + e + '>';
        }
    }

    function hookOneAddress(symbolName, addr) {
        Interceptor.attach(addr, {
            onEnter: function (args) {
                this.shouldPrint = false;
                if (printedCalls >= MAX_PRINT_CALLS) return;
                var jclassObj = args[1];
                var methods = args[2];
                var count = args[3].toInt32();
                var className = getClassNameSafe(jclassObj);
                var lower = String(className).toLowerCase();
                var suspicious = lower.indexOf('zhenxi') !== -1 || lower.indexOf('hunter') !== -1 || lower.indexOf('mainactivity') !== -1;
                if (!(suspicious || count <= 6)) return;
                this.shouldPrint = true;
            }
        });
        console.log('[+] hook ' + symbolName + ' @ ' + addr);
    }

    function installHooks() {
        try {
            var art = Process.getModuleByName('libart.so');
            var symbols = art.enumerateSymbols();
            var hooked = {};
            var countHooks = 0;
            for (var i = 0; i < symbols.length; i++) {
                var n = symbols[i].name;
                if (n.indexOf('RegisterNatives') === -1) continue;
                if (n.indexOf('JNI') === -1 && n.indexOf('CheckJNI') === -1) continue;
                var addr = symbols[i].address;
                var key = addr.toString();
                if (hooked[key]) continue;
                hooked[key] = true;
                hookOneAddress(n, addr);
                countHooks++;
            }
            console.log('[*] v1.4c installed, hooked symbols=' + countHooks);
        } catch (e) {
            console.log('[-] installHooks failed: ' + e);
        }
    }

    installHooks();
}

执行现象:Hook 可成功安装,但有效命中次数极少。

日志说明:下面这组日志证明 Hook 安装成功,但没有有效 RegisterNatives 命中。

点击展开:日志输出

[+] hook _ZN3art12_GLOBAL__N_18CheckJNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi @ 0x… [+] hook _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi @ 0x… [*] v1.4c installed, hooked symbols=2 Spawned com.zhenxi.hunter. Resuming main thread!

关键推断:Hunter 的核心 JNI 逻辑并非以动态注册为主,更可能采用静态 JNI 导出方式。

0x02.6 第六阶段:静态 JNI 路线突破

dlopen/android_dlopen_ext 命中目标 so 后,定向扫描导出。

点击展开:完整代码(v1.5_safe)
 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
'use strict';

var scannedModules = {};
var hookedFns = {};

function getExport(name) {
    try {
        if (Module.findGlobalExportByName) {
            var p = Module.findGlobalExportByName(name);
            if (p) return p;
        }
    } catch (e) {}
    try {
        if (Module.findExportByName) return Module.findExportByName(null, name);
    } catch (e2) {}
    return null;
}

function shouldTrackSymbol(name) {
    var n = (name || '').toLowerCase();
    return n.indexOf('java_com_zhenxi_hunter_') !== -1 || (n.indexOf('java_') === 0 && n.indexOf('mainactivity') !== -1) || (n.indexOf('java_') === 0 && n.indexOf('hunter') !== -1 && n.indexOf('_v') !== -1);
}

function attachJni(address, name, modName) {
    var key = address.toString();
    if (hookedFns[key]) return;
    hookedFns[key] = true;
    Interceptor.attach(address, {
        onEnter: function () { console.log('[JNI_STATIC_ENTER] ' + name + ' @ ' + address + ' (' + modName + ')'); },
        onLeave: function (retval) { console.log('[JNI_STATIC_LEAVE] ' + name + ' ret=' + retval); }
    });
    console.log('[+] attach static JNI => ' + name + ' @ ' + address + ' (' + modName + ')');
}

function scanOneModule(mod) {
    if (!mod) return;
    var mk = mod.name + '@' + mod.base;
    if (scannedModules[mk]) return;
    scannedModules[mk] = true;
    var exports;
    try { exports = mod.enumerateExports(); } catch (e) { return; }
    for (var i = 0; i < exports.length; i++) {
        var ex = exports[i];
        if (ex.type !== 'function') continue;
        var name = ex.name || '';
        if (!shouldTrackSymbol(name)) continue;
        console.log('[JNI_STATIC_EXPORT] ' + mod.name + ' :: ' + name + ' @ ' + ex.address);
        attachJni(ex.address, name, mod.name);
    }
}

function hookDlopen() {
    function hookOne(sym) {
        var fn = getExport(sym);
        if (!fn) return;
        Interceptor.attach(fn, {
            onEnter: function (args) {
                this.path = '';
                try { if (args[0]) this.path = args[0].readCString() || ''; } catch (e) {}
            },
            onLeave: function (retval) {
                if (retval.isNull()) return;
                if (!this.path || this.path.toLowerCase().indexOf('hunter') === -1) return;
                console.log('[dlopen-hit] ' + sym + ' => ' + this.path);
                var mods = Process.enumerateModules();
                for (var i = 0; i < mods.length; i++) {
                    var m = mods[i];
                    if (m.path === this.path || m.path.indexOf(this.path) !== -1 || this.path.indexOf(m.path) !== -1) {
                        scanOneModule(m);
                    }
                }
            }
        });
        console.log('[+] hook ' + sym + ' @ ' + fn);
    }

    hookOne('dlopen');
    hookOne('android_dlopen_ext');
}

setImmediate(function () {
    hookDlopen();
    console.log('[*] hookhunter v1.5_safe installed');
});

关键发现:定位到核心 JNI 入口符号

  1. Java_com_zhenxi_hunter_NativeEngine_getZhenxiInfoMethodCode
  2. Java_com_zhenxi_hunter_NativeEngine_getZhenxiInfoReleaseNative

核心意义:将 JNI 入口从“猜测”转化为“可精准定位的符号”,为后续 Native 层分析奠定基础。

日志说明:下面这组日志是静态 JNI 路线的关键突破证据。

点击展开:日志输出

[dlopen-hit] android_dlopen_ext => …/lib/arm64/libhunter.so [JNI_STATIC_EXPORT] libhunter.so :: Java_com_zhenxi_hunter_NativeEngine_getZhenxiInfoMethodCode @ 0x780ebd1e24 [+] attach static JNI => Java_com_zhenxi_hunter_NativeEngine_getZhenxiInfoMethodCode @ 0x780ebd1e24 (libhunter.so) [JNI_STATIC_EXPORT] libhunter.so :: Java_com_zhenxi_hunter_NativeEngine_getZhenxiInfoReleaseNative @ 0x780ebd1d5c [+] attach static JNI => Java_com_zhenxi_hunter_NativeEngine_getZhenxiInfoReleaseNative @ 0x780ebd1d5c (libhunter.so) [*] module scan done: libhunter.so, hits=2

0x02.7 第七阶段:NativeEngine 全量映射

批量 Hook NativeEngine.getZhenxiInfo* 系列方法,建立 “方法 - 返回值” 映射关系

点击展开:完整代码(v1.6)
 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
'use strict';

(function () {
    function hookJavaNativeEngine() {
        if (!Java.available) return;
        Java.perform(function () {
            try {
                var NE = Java.use('com.zhenxi.hunter.NativeEngine');
                var clsMethods = NE.class.getDeclaredMethods();
                var seen = {};
                for (var i = 0; i < clsMethods.length; i++) {
                    var name = '';
                    try { name = clsMethods[i].getName(); } catch (e0) { continue; }
                    if (seen[name]) continue;
                    seen[name] = true;
                    if (name.indexOf('getZhenxiInfo') === -1) continue;
                    if (!NE[name]) continue;
                    var ovs = NE[name].overloads;
                    for (var k = 0; k < ovs.length; k++) {
                        (function (mName, ov) {
                            ov.implementation = function () {
                                var argsText = [];
                                for (var a = 0; a < arguments.length; a++) {
                                    try { argsText.push('a' + a + '=' + String(arguments[a])); }
                                    catch (e1) { argsText.push('a' + a + '=<err>'); }
                                }
                                var ret = ov.apply(this, arguments);
                                var retText = '<null>';
                                try { retText = String(ret); } catch (e2) {}
                                console.log('[JAVA_NE] ' + mName + '(' + argsText.join(', ') + ') => ' + retText);
                                return ret;
                            };
                        })(name, ovs[k]);
                    }
                    console.log('[+] hook NativeEngine.' + name + ' overloads=' + ovs.length);
                }
            } catch (e) {
                console.log('[-] hook NativeEngine failed: ' + e);
            }
        });
    }

    hookJavaNativeEngine();
    console.log('[*] hookhunter v1.6 nativeengine focus installed');
})();

方法调用频次统计

  1. getZhenxiInfoH 高达 21 次
  2. getZhenxiInfoZ 高达 13 次
  3. getZhenxiInfoK 6 次

结论:设备属性读取与环境风险探测是 Hunter 的核心主干流量。

日志说明:下面这组日志说明 v1.6 已成功进入 NativeEngine 全量方法映射阶段。

点击展开:日志输出

[-] libhunter.so not loaded yet, skip native attach in this phase [+] hook dlopen @ 0x78ff6f2014 [+] hook android_dlopen_ext @ 0x78ff6f20ac [*] hookhunter v1.6 nativeengine focus installed [+] hook NativeEngine.getZhenxiInfo1 overloads=1 [+] hook NativeEngine.getZhenxiInfo3 overloads=1 [+] hook NativeEngine.getZhenxiInfo4 overloads=1 [+] hook NativeEngine.getZhenxiInfoF overloads=1 [JAVA_NE] getZhenxiInfo1() => a_NSw6KjQ+MS,b_NTA5MUM8RT,c_29048414,e_ehwrEnRaXE,f_MzowNTQxMj

0x02.8 第八阶段:核心方法精准追踪

聚焦 9 个核心方法:

  1. getZhenxiInfo1
  2. getZhenxiInfo3
  3. getZhenxiInfo4
  4. getZhenxiInfoF
  5. getZhenxiInfoH
  6. getZhenxiInfoZ
  7. getZhenxiInfoK
  8. getZhenxiInfoOFLI
  9. getZhenxiInfoLLLI

并对超长返回做摘要处理,解决日志爆炸问题。

阶段结果(实测):

  1. getZhenxiInfo1 输出 a_/b_/c_/e_/f_ 聚合串。
  2. getZhenxiInfo3 命中 zygisk/su/magisk 痕迹。
  3. getZhenxiInfo4 命中 sh 进程。
  4. getZhenxiInfoF 命中 Hook/完整性风险。
点击展开:完整代码(v1.7)
 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
Java.perform(function () {
    var TARGET_CLASS = 'com.zhenxi.hunter.NativeEngine';
    var FOCUS = {
        getZhenxiInfo1: true,
        getZhenxiInfo3: true,
        getZhenxiInfo4: true,
        getZhenxiInfoF: true,
        getZhenxiInfoH: true,
        getZhenxiInfoZ: true,
        getZhenxiInfoK: true,
        getZhenxiInfoOFLI: true,
        getZhenxiInfoLLLI: true
    };

    var KEY_RO = {
        'ro.secure': true,
        'ro.debuggable': true,
        'ro.build.display.id': true,
        'ro.boot.verifiedbootstate': true,
        'ro.build.tags': true
    };

    function now() { try { return Date.now(); } catch (e) { return 0; } }
    function safeStr(v) {
        try {
            if (v === null || v === undefined) return '<null>';
            return String(v);
        } catch (e) {
            return '<toString_error:' + e + '>';
        }
    }

    function shrink(text, maxLen) {
        if (!text) return text;
        if (text.length <= maxLen) return text;
        return text.substring(0, maxLen) + '...<len=' + text.length + '>';
    }

    function summarizeRet(name, retText) {
        if (name === 'getZhenxiInfoOFLI' || name === 'getZhenxiInfoLLLI') return shrink(retText, 260);
        if (name === 'getZhenxiInfo3' || name === 'getZhenxiInfo4' || name === 'getZhenxiInfoF') return shrink(retText, 320);
        return shrink(retText, 180);
    }

    function shouldPrint(name, argsArr) {
        if (name === 'getZhenxiInfoH' || name === 'getZhenxiInfoZ') {
            var k = argsArr.length > 0 ? safeStr(argsArr[0]) : '';
            return !!KEY_RO[k];
        }
        return true;
    }

    function hookNativeEngine() {
        var NE = Java.use(TARGET_CLASS);
        var methods = NE.class.getDeclaredMethods();
        var seen = {};
        for (var i = 0; i < methods.length; i++) {
            var mName;
            try { mName = methods[i].getName(); } catch (e0) { continue; }
            if (seen[mName]) continue;
            seen[mName] = true;
            if (!FOCUS[mName]) continue;
            if (!NE[mName]) continue;
            var ovs = NE[mName].overloads;
            for (var j = 0; j < ovs.length; j++) {
                (function (name, ov) {
                    ov.implementation = function () {
                        var t0 = now();
                        var argsArr = [];
                        for (var a = 0; a < arguments.length; a++) argsArr.push(arguments[a]);
                        var ret = ov.apply(this, arguments);
                        if (!shouldPrint(name, argsArr)) return ret;
                        var argText = [];
                        for (var k = 0; k < argsArr.length; k++) argText.push('a' + k + '=' + safeStr(argsArr[k]));
                        var retText = summarizeRet(name, safeStr(ret));
                        var cost = now() - t0;
                        console.log('[V1.7] ' + name + '(' + argText.join(', ') + ') => ' + retText + ' | costMs=' + cost);
                        return ret;
                    };
                })(mName, ovs[j]);
            }
            console.log('[+] hook focus method: ' + mName + ' overloads=' + ovs.length);
        }
    }

    try {
        hookNativeEngine();
        console.log('[*] hookhunter v1.7 focus installed');
    } catch (e) {
        console.log('[-] v1.7 hook failed: ' + e);
    }
});

日志说明:下面这组日志体现了最终高价值方法追踪的核心命中结果。

点击展开:日志输出

[V1.7] getZhenxiInfoH(a0=ro.build.display.id) => QKQ1.190828.002 test-keys | costMs=11 [V1.7] getZhenxiInfo3() => {title=Check Find Root File risk=[com.zhenxi.hunter:hunter_main_process, /system/lib64/libzygisk.so, /sbin/su, /sbin/.magisk]} | costMs=3 [V1.7] getZhenxiInfo1() => a_NSw6KjQ+MS,b_NTA5MUM8RT,c_29048414,e_ehwrEnRaXE,f_MzowNTQxMj | costMs=86 [V1.7] getZhenxiInfoF() => {title=检测到libart.so被修改 risk=[检测当前程序被Hook,当前手机存在hook框架]} | costMs=674

0x03 核心证据(节选)

0x03.1 最终脚本实跑结果

最终版本脚本采用“中文结构化输出”,每次命中都会直接打印方法用途、参数、返回值、耗时和提炼后的关键点。

点击展开:最终脚本完整代码(较长)
  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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
 * Hunter 最终学习版脚本(中文输出 + 详细注释)
 *
 * 适用目标:com.zhenxi.hunter
 * 设计目标:
 * 1) 用最小必要 Hook 点覆盖“指纹采集 + 风险判断”的关键路径。
 * 2) 输出尽量中文化,便于直接作为博客素材。
 * 3) 对超长返回做摘要,避免日志被列表淹没。
 * 4) 不篡改返回值,只做观测,保证结果可重复、可解释。
 *
 * 推荐命令:
 * frida -U -f com.zhenxi.hunter -l D:\Desktop\练手\hunter\hookhunter_final_cn_blog.js -q
 */

Java.perform(function () {
    'use strict';

    // ========== 一、可调参数区域 ==========

    // 目标类:已在前序分析中确认 NativeEngine 是核心入口。
    var TARGET_CLASS = 'com.zhenxi.hunter.NativeEngine';

    // 仅追踪高价值方法,减少无关噪音。
    var FOCUS_METHODS = {
        getZhenxiInfo1: true,     // 核心聚合 a_/b_/c_/e_/f_
        getZhenxiInfo3: true,     // Root 文件/路径风险
        getZhenxiInfo4: true,     // 进程风险
        getZhenxiInfoF: true,     // Hook/完整性风险(libart)
        getZhenxiInfoH: true,     // 高频属性读取
        getZhenxiInfoZ: true,     // 属性补充读取
        getZhenxiInfoK: true,     // 容器环境探测
        getZhenxiInfoOFLI: true,  // 文件描述符枚举
        getZhenxiInfoLLLI: true   // so/oat 枚举
    };

    // H/Z 为高频属性读取,按关键键值过滤,避免刷屏。
    var KEY_RO = {
        'ro.secure': true,
        'ro.debuggable': true,
        'ro.build.display.id': true,
        'ro.boot.verifiedbootstate': true,
        'ro.build.tags': true
    };

    // ========== 二、通用工具函数 ==========

    function nowMs() {
        try { return Date.now(); } catch (e) { return 0; }
    }

    function safeStr(v) {
        try {
            if (v === null || v === undefined) return '<null>';
            return String(v);
        } catch (e) {
            return '<toString_error:' + e + '>';
        }
    }

    function shrink(text, maxLen) {
        if (!text) return text;
        if (text.length <= maxLen) return text;
        return text.substring(0, maxLen) + '...<len=' + text.length + '>';
    }

    // 按方法类型设置不同摘要长度,平衡信息量与可读性。
    function summarizeReturn(methodName, retText) {
        if (methodName === 'getZhenxiInfoOFLI' || methodName === 'getZhenxiInfoLLLI') {
            return shrink(retText, 320);
        }
        if (methodName === 'getZhenxiInfo3' || methodName === 'getZhenxiInfo4' || methodName === 'getZhenxiInfoF') {
            return shrink(retText, 420);
        }
        return shrink(retText, 220);
    }

    function shouldPrint(methodName, argsArr) {
        // 高频属性通道做键过滤,保留关键安全属性即可。
        if (methodName === 'getZhenxiInfoH' || methodName === 'getZhenxiInfoZ') {
            var key = argsArr.length > 0 ? safeStr(argsArr[0]) : '';
            return !!KEY_RO[key];
        }
        return true;
    }

    function methodDesc(methodName) {
        var desc = {
            getZhenxiInfo1: '设备指纹聚合(a_/b_/c_/e_/f_)',
            getZhenxiInfo3: 'Root 文件/路径风险检测',
            getZhenxiInfo4: '可疑进程检测',
            getZhenxiInfoF: 'Hook/完整性检测(libart)',
            getZhenxiInfoH: '系统属性主通道',
            getZhenxiInfoZ: '系统属性补充通道',
            getZhenxiInfoK: '容器环境检测(docker/lxc)',
            getZhenxiInfoOFLI: '文件描述符枚举',
            getZhenxiInfoLLLI: '加载库枚举'
        };
        return desc[methodName] || '未知用途';
    }

    // 从返回值里提取“可直接读懂的重点”。
    function extractHighlights(methodName, retText, argsArr) {
        var out = [];
        var lower = (retText || '').toLowerCase();

        if (methodName === 'getZhenxiInfo1') {
            if (retText.indexOf('a_') !== -1 || retText.indexOf('b_') !== -1) {
                out.push('命中 a_/b_/c_/e_/f_ 聚合字段,疑似 Native 指纹核心输出');
            }
        }

        if (methodName === 'getZhenxiInfo3') {
            if (lower.indexOf('zygisk') !== -1) out.push('命中 zygisk 相关路径');
            if (lower.indexOf('/sbin/su') !== -1) out.push('命中 su 路径');
            if (lower.indexOf('magisk') !== -1) out.push('命中 magisk 痕迹');
        }

        if (methodName === 'getZhenxiInfo4') {
            if (lower.indexOf('pid name: sh') !== -1) out.push('命中 sh 进程');
            if (lower.indexOf('pid name: grep') !== -1) out.push('命中 grep 进程');
            if (lower.indexOf('pid name: logcat') !== -1) out.push('命中 logcat 进程');
        }

        if (methodName === 'getZhenxiInfoF') {
            if (retText.indexOf('libart') !== -1) out.push('命中 libart 完整性风险');
            if (retText.indexOf('Hook') !== -1 || retText.indexOf('hook') !== -1) out.push('命中 Hook 框架风险提示');
        }

        if (methodName === 'getZhenxiInfoK') {
            var path = argsArr.length > 0 ? safeStr(argsArr[0]) : '';
            if (retText === '<null>' || retText === 'null') {
                out.push('路径 ' + path + ' 未命中容器关键词');
            } else {
                out.push('路径 ' + path + ' 存在容器相关命中');
            }
        }

        if (methodName === 'getZhenxiInfoH' || methodName === 'getZhenxiInfoZ') {
            var key = argsArr.length > 0 ? safeStr(argsArr[0]) : '';
            out.push('属性读取:' + key + ' = ' + retText);
        }

        if (methodName === 'getZhenxiInfoOFLI') {
            out.push('返回 fd 列表,用于观察可疑句柄/映射关系');
        }

        if (methodName === 'getZhenxiInfoLLLI') {
            out.push('返回 so/oat 列表,可用于识别注入/异常模块');
            if (lower.indexOf('frida-agent') !== -1) {
                out.push('列表中出现 frida-agent 相关痕迹');
            }
        }

        return out;
    }

    // ========== 三、关键 Hook:NativeEngine 方法级追踪 ==========

    function hookNativeEngineFocus() {
        var NE = Java.use(TARGET_CLASS);
        var methods = NE.class.getDeclaredMethods();
        var installed = {};

        for (var i = 0; i < methods.length; i++) {
            var mName = '';
            try { mName = methods[i].getName(); } catch (e0) { continue; }

            if (installed[mName]) continue;
            installed[mName] = true;

            if (!FOCUS_METHODS[mName]) continue;
            if (!NE[mName]) continue;

            var overloads = NE[mName].overloads;

            for (var j = 0; j < overloads.length; j++) {
                (function (methodName, ov) {
                    ov.implementation = function () {
                        var t0 = nowMs();

                        // 先保存参数,避免调用后对象状态变化导致打印失真。
                        var argsArr = [];
                        for (var a = 0; a < arguments.length; a++) {
                            argsArr.push(arguments[a]);
                        }

                        // 只观测,不修改返回值。
                        var ret = ov.apply(this, arguments);

                        // 高频属性方法做过滤,减少重复噪音。
                        if (!shouldPrint(methodName, argsArr)) {
                            return ret;
                        }

                        var cost = nowMs() - t0;

                        var argText = [];
                        for (var k = 0; k < argsArr.length; k++) {
                            argText.push('a' + k + '=' + safeStr(argsArr[k]));
                        }

                        var rawRet = safeStr(ret);
                        var retText = summarizeReturn(methodName, rawRet);

                        // 主日志:结构化 + 中文语义
                        console.log('');
                        console.log('【核心命中】' + methodName + '(' + methodDesc(methodName) + ')');
                        console.log('  参数: ' + (argText.length > 0 ? argText.join(', ') : '<无参数>'));
                        console.log('  返回: ' + retText);
                        console.log('  耗时: ' + cost + ' ms');

                        // 附加重点:从返回里抽“可读结论”。
                        var points = extractHighlights(methodName, rawRet, argsArr);
                        for (var p = 0; p < points.length; p++) {
                            console.log('  关键点: ' + points[p]);
                        }

                        return ret;
                    };
                })(mName, overloads[j]);
            }

            console.log('已安装 Hook -> ' + mName + ',重载数=' + overloads.length + ',用途=' + methodDesc(mName));
        }
    }

    // ========== 四、辅助 Hook:IFAA/Soter 绑定观测 ==========

    // 这个辅助点用于把早期结论串起来:IFAA bind 失败,Soter bind 成功。
    function hookBindServiceLite() {
        try {
            var CtxWrapper = Java.use('android.content.ContextWrapper');
            var bind3 = CtxWrapper.bindService.overload('android.content.Intent', 'android.content.ServiceConnection', 'int');

            bind3.implementation = function (intent, conn, flags) {
                var ret = bind3.call(this, intent, conn, flags);

                var action = '';
                var pkg = '';
                var comp = '';
                try { action = safeStr(intent ? intent.getAction() : null); } catch (e1) {}
                try { pkg = safeStr(intent ? intent.getPackage() : null); } catch (e2) {}
                try { comp = safeStr(intent ? intent.getComponent() : null); } catch (e3) {}

                var s = (action + ' ' + pkg + ' ' + comp).toLowerCase();
                if (s.indexOf('ifaa') !== -1 || s.indexOf('soter') !== -1 || s.indexOf('biometric') !== -1 || s.indexOf('finger') !== -1) {
                    console.log('');
                    console.log('【服务绑定】bindService 观测');
                    console.log('  action=' + action);
                    console.log('  pkg=' + pkg);
                    console.log('  comp=' + comp);
                    console.log('  flags=' + flags + ', ret=' + ret);
                }

                return ret;
            };

            console.log('已安装 Hook -> ContextWrapper.bindService(轻量观测)');
        } catch (e) {
            console.log('安装 bindService 观测失败: ' + e);
        }
    }

    // ========== 五、启动入口 ==========

    try {
        console.log('========== Hunter 最终学习脚本启动 ==========' );
        console.log('目标类: ' + TARGET_CLASS);
        console.log('策略: 高价值方法追踪 + 中文结构化输出 + 只观测不篡改');

        hookNativeEngineFocus();
        hookBindServiceLite();

        console.log('========== Hook 安装完成,等待目标方法触发 ==========' );
    } catch (e) {
        console.log('脚本初始化失败: ' + e);
    }
});
点击展开:完整实跑终端输出(较长)

(venv38) PS E:\code\vscode\venv38> frida -U -f com.zhenxi.hunter -l D:\Desktop\练手\hunter\hookhunter_final.js


​ / _ | Frida 17.3.2 - A world-class dynamic instrumentation toolkit | (| | ​ > _ | Commands: // || help -> Displays the help system . . . . object? -> Display information about ‘object’ . . . . exit/quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to MI 8 (id=6ca0abb6) Spawned com.zhenxi.hunter. Resuming main thread! [MI 8::com.zhenxi.hunter ]-> ========== Hunter 最终学习脚本启动 ========== 目标类: com.zhenxi.hunter.NativeEngine 策略: 高价值方法追踪 + 中文结构化输出 + 只观测不篡改 已安装 Hook -> getZhenxiInfo1,重载数=1,用途=设备指纹聚合(a/b_/c_/e_/f_) 已安装 Hook -> getZhenxiInfo3,重载数=1,用途=Root 文件/路径风险检测 已安装 Hook -> getZhenxiInfo4,重载数=1,用途=可疑进程检测 已安装 Hook -> getZhenxiInfoF,重载数=1,用途=Hook/完整性检测(libart) 已安装 Hook -> getZhenxiInfoH,重载数=1,用途=系统属性主通道 已安装 Hook -> getZhenxiInfoK,重载数=1,用途=容器环境检测(docker/lxc) 已安装 Hook -> getZhenxiInfoLLLI,重载数=1,用途=加载库枚举 已安装 Hook -> getZhenxiInfoOFLI,重载数=1,用途=文件描述符枚举 已安装 Hook -> getZhenxiInfoZ,重载数=1,用途=系统属性补充通道 已安装 Hook -> ContextWrapper.bindService(轻量观测) ========== Hook 安装完成,等待目标方法触发 ==========

【核心命中】getZhenxiInfoH(系统属性主通道) 参数: a0=ro.build.display.id 返回: QKQ1.190828.002 test-keys 耗时: 11 ms 关键点: 属性读取:ro.build.display.id = QKQ1.190828.002 test-keys

【服务绑定】bindService 观测 action=org.ifaa.aidl.manager.IfaaManagerService pkg=org.ifaa.aidl.manager comp= flags=1, ret=false

【服务绑定】bindService 观测 action= pkg= comp=ComponentInfo{com.tencent.soter.soterserver/com.tencent.soter.soterserver.SoterService} flags=1, ret=true

【核心命中】getZhenxiInfo3(Root 文件/路径风险检测) 参数: <无参数> 返回: {title=Check Find Root File risk=[com.zhenxi.hunter:hunter_main_process, /system/lib64/libzygisk.so, /sbin/su, /sbin/.magisk] com.zhenxi.hunter:hunter_main_process /system/lib64/libzygisk.so /sbin/su /sbin/.magisk }

耗时: 44 ms 关键点: 命中 zygisk 相关路径 关键点: 命中 su 路径 关键点: 命中 magisk 痕迹

【核心命中】getZhenxiInfo1(设备指纹聚合(a_/b_/c_/e_/f_)) 参数: <无参数> 返回: a_NSw6KjQ+MS,b_NTA5MUM8RT,c_29048414,e_ehwrEnRaXE,f_MzowNTQxMj 耗时: 86 ms 关键点: 命中 a_/b_/c_/e_/f_ 聚合字段,疑似 Native 指纹核心输出

【核心命中】getZhenxiInfo4(可疑进程检测) 参数: a0=YouAreLoser.ug@c56afde 返回: {title=Find Others Processmay be risks main pid (7087) risk=[other pid -> 7427(pid name: ls), other pid -> 7432(pid name: sh)] other pid -> 7427(pid name: ls) other pid -> 7432(pid name: sh) }

耗时: 8 ms 关键点: 命中 sh 进程

【核心命中】getZhenxiInfoLLLI(加载库枚举) 参数: <无参数> 返回: [/system/bin/linker64, /system/bin/app_process64, [vdso], /system/lib64/libandroid_runtime.so, /system/lib64/libbase.so, /system/lib64/libbinder.so, /system/lib64/libcutils.so, /system/lib64/libhidlbase.so, /system/lib64/liblog.so, /apex/com.android.runtime/lib64/libnativeloader.so, /system/lib64/libutils.so, /system/l…<len=10999> 耗时: 2 ms 关键点: 返回 so/oat 列表,可用于识别注入/异常模块 关键点: 列表中出现 frida-agent 相关痕迹

【核心命中】getZhenxiInfoOFLI(文件描述符枚举) 参数: <无参数> 返回: [1//dev/null/character device/8630/1/0/0/0, 2//dev/null/character device/8630/1/0/0/0, 3/socket:[2096764]/socket/49663/1/10277/10277/0, 4//dev/pmsg0/, 5//sys/kernel/debug/tracing/trace_marker/, 6//dev/null/character device/8630/1/0/0/0, 7//dev/null/character device/8630/1/0/0/0, 8//dev/null/character device/8630/1/0/0/…<len=6268> 耗时: 9 ms 关键点: 返回 fd 列表,用于观察可疑句柄/映射关系 关键点: 路径 /proc/mounts 未命中容器关键词

【核心命中】getZhenxiInfoK(容器环境检测(docker/lxc)) 参数: a0=/proc/self/mountstats, a1=docker,lxc_volumns 返回: 耗时: 0 ms 关键点: 路径 /proc/self/mountstats 未命中容器关键词

【核心命中】getZhenxiInfoK(容器环境检测(docker/lxc)) 参数: a0=/proc/self/mountinfo, a1=docker,lxc_volumns 返回: 耗时: 364 ms 关键点: 路径 /proc/self/mountinfo 未命中容器关键词

【核心命中】getZhenxiInfoH(系统属性主通道) 参数: a0=ro.boot.verifiedbootstate 返回: green 耗时: 32 ms 关键点: 属性读取:ro.boot.verifiedbootstate = green [节选说明] 中间存在多轮 getZhenxiInfoH/Zro.build.display.idro.debuggablero.secure 的重复轮询日志,这里不再全文贴出。

【核心命中】getZhenxiInfoF(Hook/完整性检测(libart)) 参数: <无参数> 返回: {title=检测到libart.so被修改 risk=[检测当前程序被Hook,当前手机存在hook框架]} 耗时: 433 ms 关键点: 命中 libart 完整性风险

  1. getZhenxiInfo1()

日志说明:该返回值用于证明 a_/b_/c_/e_/f_ 聚合字段已经被命中。

点击展开:日志输出

a_NSw6KjQ+MS,b_NTA5MUM8RT,c_29048414,e_ehwrEnRaXE,f_MzowNTQxMj

该返回与 NativeDeviceFingerprint 中的 a_/b_/c_/e_/f_ 聚合字段高度对应。

  1. getZhenxiInfo3()

日志说明:该风险列表用于证明 root/hook 路径检测命中。

点击展开:日志输出

risk=[com.zhenxi.hunter:hunter_main_process, /system/lib64/libzygisk.so, /sbin/su, /sbin/.magisk]

说明 root/hook 相关痕迹可通过文件与路径信号直接采集。

  1. getZhenxiInfo4(...)

日志说明:该输出用于证明存在进程维度的风险画像。

点击展开:日志输出

risk=[other pid -> … (pid name: sh/cat/grep/ls/logcat)…]

说明存在进程维度的实时风险画像能力。

  1. getZhenxiInfoH/Z

日志说明:该组日志用于证明关键系统属性被持续读取并参与判断。

点击展开:日志输出

ro.build.display.id => QKQ1.190828.002 test-keys ro.boot.verifiedbootstate => green ro.debuggable => 0 ro.secure => 1

说明基础系统属性属于高频输入,且存在重复校验行为。

  1. getZhenxiInfoK(...)

日志说明:该结果用于证明当前样本环境未触发容器特征命中。

点击展开:日志输出

/proc/mounts + docker,lxc_volumns => /proc/self/mountstats + docker,lxc_volumns => /proc/self/mountinfo + docker,lxc_volumns =>

该样本环境下容器特征未命中。

  1. getZhenxiInfoF()

日志说明:该输出用于证明链路内置了完整性与 Hook 框架检测。

点击展开:日志输出

{title=检测到libart.so被修改 risk=[检测当前程序被Hook,当前手机存在hook框架]}

这是最关键的一条:采集链路内置完整性与 Hook 框架检测。

0x04 最终映射表(方法 -> 字段 -> 风险意义)

方法 主要输入 输出形态 对应字段/语义 风险意义
getZhenxiInfo1 a_/b_/c_/e_/f_ 聚合串 NativeDeviceFingerprint 关键聚合值
getZhenxiInfo3 root 风险列表 root 文件/路径痕迹
getZhenxiInfo4 进程上下文 进程风险列表 可疑进程态势
getZhenxiInfoF 完整性风险 map libart/hook 框架检测 极高
getZhenxiInfoH/Z ro.* 属性键 属性值 DeviceBaseInfo 与安全属性 中高
getZhenxiInfoK /proc/* + docker,lxc null/命中项 容器环境检测
getZhenxiInfoOFLI fd 列表 打开文件/句柄画像
getZhenxiInfoLLLI so/oat 列表 运行时加载画像

0x05 关键结论(经日志支持)

  1. Hunter 并非单点 root 检测,而是多信号融合判断。
  2. 指纹采集与安全检测在同一链路并行执行,而非“先采集后检测”的串行模型。
  3. 仅隐藏单个信号(如 su)意义有限,进程、加载库、完整性校验仍会补充风险证据。
  4. 从工程实践看,“先宽后窄”的脚本迭代策略显著提升了定位效率与可解释性。

0x06 学习总结

1. 逆向分析方法论

本次 Hunter 设备指纹逆向遵循由浅入深、由 Java 到 Native、由观测到验证的主线:

  • 初始阶段:通过页面特征反推关键 API,优先 Hook 系统级 API(SystemProperties.get/Context.getSystemService)建立观测基线。
  • 迭代阶段:逐步收敛 Hook 范围,从全局 Hook 转向业务方法(MainActivity.v),再下钻到 JNI 层。
  • 突破阶段:通过静态 JNI 导出符号扫描定位核心 Native 方法,最终还原指纹生成链路。

2. 关键技术点总结

阶段 核心技术 解决的问题 核心经验
Java 层观测 Frida Java API Hook 定位数据采集入口 先观测不篡改,建立调用链路地图
系统服务探测 bindService Hook/Logcat 监控 验证 IFAA/Soter 服务状态 结合日志上下文分析错误码含义
JNI 层定位 dlopen Hook + 导出符号扫描 找到 Native 层核心入口 静态 JNI 导出比动态注册更常见
Native 层分析 指令级 Hook / 系统调用追踪 还原指纹生成算法 关注哈希函数、字符串拼接操作
对抗绕过 Anti-Anti-Debug 突破调试 / 检测限制 优先修复核心校验点而非全量绕过

3. 避坑指南

  • 避免无差别全局 Hook:高频基础类(StringBuilder/ArrayList)Hook 会产生海量噪音,甚至导致进程崩溃。
  • JNI 层分析优先静态符号:动态注册 JNI 的检测成本更高,很多商业应用更偏向静态导出。
  • 日志输出需做摘要:超长返回值(如完整指纹串)应截断,避免日志爆炸。
  • 坚持分层分析:Java 层链路未理清前,不要急于深入 Native,避免方向失控。

4. Frida 逆向基础

  • Java 层 Hook 核心 API:
    • Java.use():获取类的引用;
    • overload():指定方法重载版本;
    • implementation:替换方法实现;
    • Java.perform():在 Java 虚拟机上下文执行代码。
  • Native 层 Hook 核心 API:
    • Interceptor.attach():附加到函数地址;
    • Module.findExportByName():查找导出符号;
    • Thread.backtrace():获取调用栈;
    • NativeCallback/NativeFunction:Native 函数封装。

5. Android 设备指纹核心维度

维度 采集内容 技术实现
基础硬件 CPU / 内存 / 屏幕 / 传感器 读取/sys//proc文件、系统属性
系统信息 系统版本 / ROM / 安全补丁 ro.build.*系列属性
唯一标识 OAID/IMEI/ 序列号 系统服务调用(TelephonyManager)
环境安全 Root/Hook/ 调试状态 进程扫描、文件检测、指令校验
服务能力 IFAA/Soter/ 生物识别 bindService/getSystemService

6. Native 层逆向关键技巧

  • 符号命名规律:JNI 静态导出符号遵循Java_包名_类名_方法名规则;
  • 指令特征:哈希计算(MD5/SHA)会出现固定的指令序列;
  • 数据流转:通过read/write系统调用追踪数据读取与输出;
  • 对抗检测:关注ptrace/seccomp/mmap等系统调用的异常使用。

0x07 延伸阅读

  1. 《聊聊大厂设备指纹其二&Hunter环境检测思路详解》

  2. 《自动化采集Android系统级设备指纹对抗&如何四两拨千斤?》

前途似海,来日方长。


<