吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3337|回复: 32
上一主题 下一主题
收起左侧

[Android 原创] 从零开始绕过 DexProtector 加固的 Frida 检测

[复制链接]
跳转到指定楼层
楼主
fyr666 发表于 2025-11-20 22:32 回帖奖励
本帖最后由 fyr666 于 2025-11-21 22:54 编辑

从零开始绕过 DexProtector 加固的 Frida 检测

一个可复盘、可扩展、可工程化的对抗实录

(本文由 id:小佳、fyrlove、roysue 共同完成)

为保证结论稳健且可迁移,文中所有实验均在以下环境中完成(LineageOS 21 / Nexus 5X,Magisk 29.0.0,LSPosed,Zygisk Frida Gadget开源模块(https://github.com/sucsand/sucsand) ,Frida Server 16.5.2)

合规:本文仅用于安全研究与对抗评估,旨在帮助甲方团队识别自身加固薄弱点、完善自检与回归策略;不针对具体业务落地攻击,不提供可直接用于对第三方应用的利用脚本。

你在这篇文章里会学到这些硬技能(都能复现):

1.入口卡位:不死盯 System.loadLibrary,而是改从 __loader_android_dlopen_ext 抓“真实装载面”,提早拿到证据和时序。

2.匿名段定位与转储:用 JNI_OnLoad → 函数指针 → 匿名可执行段 这条线,结合 /proc/<pid>/maps 找段,Frida 直接 dump,只修 text 段也能在 IDA 里反汇到可用程度。

3.最小化修复与类型库引入:在 IDA 里手动补区段、引 android_arm64 / gnulnx_arm64 类型库,把 JNIEnv/动态注册链条(RegisterNatives/FindClass/...)梳顺。

4.校验链路拆解:识别 xxHash / SHA256 / HMAC 的落点(含内联 SHA 指令),用“等式化替换 + 调用点定位(靠 LR 定位调用者)”做最小侵入的绕过。

5.二分法定位:从“可卸载点”开始逐段排除,把“必崩区间”缩到少量函数,再精确打补丁。

6.入口完整性绕过:遇到对“当前段基址”的校验,复制一份干净 text,参数基址替换为干净副本过检。

7.线程面处理:顺着 /proc/self/maps 的反向引用追到 pthread_create,定位监控线程入口。

工程化习惯:每一步都留“能回头验证”的观测点,避免“一刀切”,降低误伤和回归压力。

分析过程

01.样本获取与安装

安装相同的版本,确保app可安装、可启动,并且能够完整复线整个过程。

1.1 样本获取

样本:Hyatt6.8.0.apkm
下载地址:下载地址:https://www.apkmirror.com/apk/hyatt-corporation/world-of-hyatt/world-of-hyatt-6-8-0-release/world-of-hyatt-6-8-0-android-apk-download/#google_vignette

1.2 样本安装

安装方法:通过 APKMirror Installer / MT 管理器均可。

02.设备与运行环境

该基线用于复现与对比。不同 SoC / API level / ART 实现可能造成“加载顺序、符号可见性、maps 标记”差异。

2.1 设备环境

设备:Nexus 5X(LineageOS 21)。

Root/框架:Magisk 29.0.0、LSPosed、Zygisk Frida Gadget 模块(https://github.com/sucsand/sucsand

Frida:frida-server 16.5.2。

PC环境: 肉丝(r0ysue)大佬提供的r0env kali虚拟机(安装了逆向需要的工具和环境配置,能省掉很多安装和环境的问题)。

2.2 运行结果

不运行 frida-server 时可进入主页但提示要升级;一旦 attach/spawn,进程迅速崩溃。

APP点开也不会崩溃,正常进入到主页,只是强制提示必须升级到最新版才可用。说明壳可能没有root检测,或者没有检测到Magisk、LSPosed。

frida不管是attach模式还是spawn模式,均会迅速的进程崩溃退出。

03.Java 层入口识别(Application & System.loadLibrary)

使用jadx对apk进行分析。
通过 AndroidManifest 与 Application 类,定位主壳入口与native 装载点,判断逻辑是否下沉到 so。

3.1 AndroidManifest.xml文件简介

Manifest 是干嘛的?

1.给系统登记应用身份证与四大组件(Activity/Service/BroadcastReceiver/ContentProvider)。

2.声明权限、最低/目标系统版本、硬件能力等。

3.配置应用级开关(如是否可调试、是否允许备份、网络安全配置等)。

其中还包含了应用的包名。这只是简述了一个大概,想了解完整的朋友请自行搜索。

3.2 样本apk的AndroidManifest.xml分析

六十几兆的apkm文件后缀名改成zip后解压,里面有个一百多兆的base.apk,其实大部分内容都在这个base.apk里。jadx-gui打开瞅瞅。
1.搜application,找到application节点,这个节点中的name就是主壳入口点,这里是ProtectedTopHyattApplication类。

<application
        android:theme="@style/AppThemeV4.HorizontalAnimation"
        android:label="@string/app_name_value"
        android:icon="@mipmap/ic_launcher"
        android:name="com.Hyatt.hyt.ProtectedTopHyattApplication"/>

2.查找启动Activity,安卓中,启动入口的Activity会有一个intent-filter标签

<intent-filter>
      <action android:name="android.intent.action.MAIN"/>
      <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

搜索一下可以发现,入口Activity是SplashActivity:

<activity android:theme="@style/SplashTheme" android:label="@string/app_name_value" android:name="com.hyt.auth.activities.SplashActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
</activity>

3.查看ProtectedTopHyattApplication类,可以看到有一些加载so库的操作,一般都清楚,主要逻辑一般肯定在so库里。

protected void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        try {
            s.a((Context) this);
            System.loadLibrary("dpboot");
            wxyrq();
        } catch (Throwable th) {
            b.fooldg(this, th);
        }
    }
public static void a(Context context, String str) {
            System.loadLibrary(c);
            boolean z = !r.a();
            b = z;
            if (z) {
                a = new File(context.getFilesDir().getAbsolutePath());
                r.a(context);
                r.a(context.getFilesDir().getAbsolutePath());
            }
        }

还有很多native函数更加验证了其逻辑在java层几乎没有。

public static native InputStream BC(Object obj, String str);
public static native boolean JaucCymn(String str, int i, List list);
private static native byte[] iIbBs();
public static native String s(String str);
private static native void ttghdCr(Object obj);
private static native void wxyrq();

基本可以确定,Java层只做了引导,核心检测链条在native中。

04.壳的native层入口点

那就想先hook这个System.loadLibrary函数,但很明显一hook就会崩。
那就是要找更早的时机,hook这个System.loadLibrary最底层的函数,那最底层的函数是哪个呢?
是linker里的__loader_android_dlopen_ext函数。(参考文章:SystemLoadLibrary :: 郑欢的学习总结

这个函数是全局的,直接取符号就可以。(以下都是frida16.5.2,切记先别上frida17噢)

4.1 启动frida-server,并进行端口转发

启动frida-server并指定14725,不使用默认端口(27042),很多检测会检测这个默认端口,所以一开始就处理一下。

adb shell                                                                                                                            
bullhead:/ $ su
bullhead:/ # cd /data/local/tmp                                  
bullhead:/data/local/tmp # ./frida-server  -l 0.0.0.0:14725 

进行端口转发:

adb forward tcp:14725 tcp:14725

4.2 hook dlopen,定位so加载时机

执行脚本:frida -H 127.0.0.1:14725 -f com.Hyatt.hyt -l hook_dexprotect.js。
根据结果分析:hook 日志显示先后加载:libalice.so、libdpboot.so、libdexprotector.so;都输出了"结束",随后崩溃。
崩溃时机在“加载完成之后”(非 .init_array 内)。
所以入口库可以基本确定为 libdexprotector.so;
检测不在 .init_array,后续应聚焦 JNI_OnLoad 或动态注册链。

function hook_dlopen() {
    var android_dlopen_ext = Module.findExportByName(null, "__loader_android_dlopen_ext")
    Interceptor.attach(android_dlopen_ext, {
        onEnter: function (args) {
            var pathptr = args[0];
            console.log("path is => ", pathptr.readCString())
        },
        onLeave: function () {
            console.log("结束")
        }
    })
}

hook_dlopen()

运行如下:

> frida -H 127.0.0.1:14725 -f com.Hyatt.hyt -l hook_dexprotect.js                       
     ____
    / _  |   Frida 16.5.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 127.0.0.1:14725 (id=socket@127.0.0.1:14725)
Spawned `com.Hyatt.hyt`. Resuming main thread!                          
[Remote::com.Hyatt.hyt ]-> path is =>  libframework-connectivity-tiramisu-jni.so
结束
path is =>  /system/framework/oat/arm64/org.apache.http.legacy.odex
结束
path is =>  /data/app/~~IvxV7LJ9cvtqbZUadvpHsQ==/com.Hyatt.hyt-q8YukchM306gv-ilc-P6Ig==/oat/arm64/base.odex
结束
path is =>  /data/app/~~IvxV7LJ9cvtqbZUadvpHsQ==/com.Hyatt.hyt-q8YukchM306gv-ilc-P6Ig==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libalice.so
结束
path is =>  /data/app/~~IvxV7LJ9cvtqbZUadvpHsQ==/com.Hyatt.hyt-q8YukchM306gv-ilc-P6Ig==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libdpboot.so
结束
path is =>  /data/app/~~IvxV7LJ9cvtqbZUadvpHsQ==/com.Hyatt.hyt-q8YukchM306gv-ilc-P6Ig==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libdexprotector.so
结束
Process crashed: java.lang.RuntimeException: DP: 786 01120321050d0580a011078020058060078080010580e003078020058080040780200580a0100780400580c0030780200580400115032105090580a0560780200580a06e078020058020078020058080a8020780400580a0010116032105050580e0320780600580200780200580c029                                                                                   

***
FATAL EXCEPTION: main
Process: com.Hyatt.hyt, PID: 8020
java.lang.RuntimeException: Unable to create application com.Hyatt.hyt.ProtectedTopHyattApplication: com.Hyatt.hyt.MessageGuardException_RFA6IDc4NiAwMTEyMDMyMTA1MGQwNTgwYTAxMTA3ODAyMDA1ODA2MDA3ODA4MDAxMDU4MGUwMDMwNzgwMjAwNTgwODAwNDA3ODAyMDA1ODBhMDEwMDc4MDQwMDU4MGMwMDMwNzgwMjAwNTgwNDAwMTE1MDMyMTA1MDkwNTgwYTA1NjA3ODAyMDA1ODBhMDZlMDc4MDIwMDU4MDIwMDc4MDIwMDU4MDgwYTgwMjA3ODA0MDA1ODBhMDAxMDExNjAzMjEwNTA1MDU4MGUwMzIwNzgwNjAwNTgwMjAwNzgwMjAwNTgwYzAyOSBbMjAyNTA0MjEtMjAyNTA1MjIxOTAzIGI3OmI3IDM0IGdvb2dsZS9idWxsaGVhZC9idWxsaGVhZDo4LjEuMC9PUE0zLjE3MTAxOS4wMTQvNDUwMzk5ODp1c2VyL3JlbGVhc2Uta2V5cyBibG9ja2VkXSAwMTk5ZjdjOS1hYjI5LTQ0ZGUtYmMzYi04OWI5ODk5N2RiNTg: DP: 786 01120321050d0580a011078020058060078080010580e003078020058080040780200580a0100780400580c0030780200580400115032105090580a0560780200580a06e078020058020078020058080a8020780400580a0010116032105050580e0320780600580200780200580c029
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7403)
        at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2379)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loopOnce(Looper.java:232)
        at android.os.Looper.loop(Looper.java:317)
        at android.app.ActivityThread.main(ActivityThread.java:8592)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
Caused by: com.Hyatt.hyt.MessageGuardException_RFA6IDc4NiAwMTEyMDMyMTA1MGQwNTgwYTAxMTA3ODAyMDA1ODA2MDA3ODA4MDAxMDU4MGUwMDMwNzgwMjAwNTgwODAwNDA3ODAyMDA1ODBhMDEwMDc4MDQwMDU4MGMwMDMwNzgwMjAwNTgwNDAwMTE1MDMyMTA1MDkwNTgwYTA1NjA3ODAyMDA1ODBhMDZlMDc4MDIwMDU4MDIwMDc4MDIwMDU4MDgwYTgwMjA3ODA0MDA1ODBhMDAxMDExNjAzMjEwNTA1MDU4MGUwMzIwNzgwNjAwNTgwMjAwNzgwMjAwNTgwYzAyOSBbMjAyNTA0MjEtMjAyNTA1MjIxOTAzIGI3OmI3IDM0IGdvb2dsZS9idWxsaGVhZC9idWxsaGVhZDo4LjEuMC9PUE0zLjE3MTAxOS4wMTQvNDUwMzk5ODp1c2VyL3JlbGVhc2Uta2V5cyBibG9ja2VkXSAwMTk5ZjdjOS1hYjI5LTQ0ZGUtYmMzYi04OWI5ODk5N2RiNTg: DP: 786 01120321050d0580a011078020058060078080010580e003078020058080040780200580a0100780400580c0030780200580400115032105090580a0560780200580a06e078020058020078020058080a8020780400580a0010116032105050580e0320780600580200780200580c029
        at com.Hyatt.hyt.ProtectedTopHyattApplication$b.qC(Unknown Source:9)
        at com.Hyatt.hyt.ProtectedTopHyattApplication$b.xDzqsetu(Unknown Source:0)
        at com.Hyatt.hyt.ProtectedTopHyattApplication$b.EHo(Unknown Source:6)
        at com.Hyatt.hyt.ProtectedTopHyattApplication$b.fooldg(Unknown Source:1)
        at com.Hyatt.hyt.ProtectedTopHyattApplication.onCreate(Unknown Source:49)
        at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1386)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7398)
        ... 9 more
Caused by: java.lang.RuntimeException: DP: 786 01120321050d0580a011078020058060078080010580e003078020058080040780200580a0100780400580c0030780200580400115032105090580a0560780200580a06e078020058020078020058080a8020780400580a0010116032105050580e0320780600580200780200580c029
        at com.Hyatt.hyt.ProtectedTopHyattApplication.ttghdCr(Native Method)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.Hyatt.hyt.ProtectedTopHyattApplication$k.zeHo(Unknown Source:13)
        at com.Hyatt.hyt.ProtectedTopHyattApplication$k.wolzmlnlx(Unknown Source:472)
        at com.Hyatt.hyt.ProtectedTopHyattApplication.ttghdCr(Native Method)
        at com.Hyatt.hyt.ProtectedTopHyattApplication.onCreate(Unknown Source:44)
        ... 11 more
***
[Remote::com.Hyatt.hyt ]->

Thank you for using Frida!

4.3 hook JNI_OnLoad函数

通过再去hook它的JNI_OnLoad函数。
发现它的JNI_OnLoad函数也很顺利执行了,但是还是死掉了。
我们在onLeave这里加上休眠,它是不会死的,,它可能是在JNI_OnLoad注册了JNI函数,然后外部调用的

 var libdexprotector = Process.findModuleByName("libdexprotector.so")
            Interceptor.attach(libdexprotector.findExportByName("JNI_OnLoad"), {
                onEnter: function (args) {                    
                     console.log("JNI_OnLoad onEnter")
                },onLeave:function(ret){
                    console.log("JNI_OnLoad  结束")

                    // Thread.sleep(60)
                }
            })

结果:

05.定位检测函数位置,追到MMAP匿名内存段里

检测函数应该不在init_array里面,那还有可能在JNI_OnLoad里面,或者其他native函数里面,刚刚壳的Java层看到了很多native函数,可能是Java层调过来检测的也未必。

5.1 使用IDA分析libdexprotector.so

先把libdexprotector.so拖到IDA里分析看下,有日志可以,这个so在split_config.arm64_v8a.apk里面,前面安装包里解压出来有,再给它解压就得到libdexprotector.so

看下JNI_OnLoad函数,发现啥也没干,常见的动态注册也没有,也不是没有,它是把vm传给off_C838这个指针函数了,让它去做事情。

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  int v3; // w0

  if ( dword_C830 )
    return -dword_C830;
  v3 = off_C838(vm, 0LL);
  off_C838 = 0LL;
  if ( v3 )
    return -v3;
  else
    return 65540;
}

frida读下这个off_C838指针函数的值:

var loader_android_dlopen_ext = Module.findExportByName(null,"__loader_android_dlopen_ext")
console.log("dlopen address is => ", loader_android_dlopen_ext)
Interceptor.attach(loader_android_dlopen_ext, {
    onEnter: function (args) {
        var pathptr = args[0]
        console.log("path is => ", pathptr.readCString())
        if (pathptr.readCString().indexOf("libdexprotector.so") >= 0) {
            this.match = true
        }
    }, onLeave: function (ret) {    
        console.log("结束")    
        if (this.match) {
            var libdexprotector = Process.findModuleByName("libdexprotector.so")
            Interceptor.attach(libdexprotector.findExportByName("JNI_OnLoad"), {
                onEnter: function (args) {                    
                    console.log("off_C838 is => ", libdexprotector.base.add(0xC838).readPointer())
                },onLeave:function(ret){}
            })
        }        
    }
})

跑一下结果是:

off_C838 is =>  0x7e25b9a984

再看它属于哪个so:

console.log("off_C838 module is => ", Process.findModuleByAddress(libdexprotector.base.add(0xC838).readPointer()))

跑出来结果是null,地址不属于任何已知 module:

off_C838 module is =>  null

那么再看这个地址属于哪段内存(findRangeByAddress 显示该地址落在的匿名可执行段),为了防止进程崩溃看不到内存,加上线程休眠60秒:

console.log("off_C838 mem is => ",JSON.stringify(Process.findRangeByAddress(libdexprotector.base.add(0xC838).readPointer())))
Thread.sleep(60)

跑出来内存段是:

off_C838 mem is =>  {"base":"0x7e25afc000","size":507904,"protection":"r-x"}

输出cat maps:

console.log(`cat /proc/${Process.id}/maps | grep ${Process.findRangeByAddress(libdexprotector.base.add(0xc838).readPointer()).base}`)

结果是:

cat /proc/8467/maps | grep 0x7e24570000

把上面的结果拿去手机APP进程的maps里看下(注意去掉0x7e24570000这里的0x):

bullhead:/ # cat /proc/8467/maps | grep 7e24570000                                                             
7e24570000-7e245ec000 r-xp 00000000 00:00 0                              [anon:15f1e]

GPT解释说,[anon:15f1e] 表示这是一段可执行的可读、私有匿名内存映射,内核为其分配的内部标识符(伪 inode)是 0x15f1e。

执行的函数位于这段匿名内存里,那得把它dump下来分析看看。

5.2 IDA分析结论

分析到这里,可以知道,DexProtector 将关键逻辑放入运行时生成的匿名映射段;分析需转向内存态 dump与单段反汇。

06. 匿名可执行段定位与 /proc/<pid>/maps 交叉验证

通过 Frida 拿到 off_C838 所在内存区 base/size/prot,再交叉 cat /proc/<pid>/maps 验证段属性与范围。
Frida转储dump匿名内存:
使用frida直接输出一下进程号pid:

console.log("off_C838 pid is => ",Process.id)

这样就直接有了内存段的起始地址,长度,进程号,再使用线程休眠,把APP卡主使其不闪退:

off_C838 mem is =>  {"base":"0x7e25afc000","size":507904,"protection":"r-x"}
off_C838 pid is =>  8601

如果手速较慢,可以把休眠时间设置为更长。配合以下dump脚本,把匿名内存脱下来:

import frida
js_script = """
function dump_anon() {
    console.log("开始dump")
    const base = ptr(0x7e25afc000);
    const module_size = 507904;
    Memory.protect(base, module_size, 'rwx');
    const soMemory = Memory.readByteArray(base, module_size);
    send({name: "libanon.so", base: base, size: module_size}, soMemory);
}
dump_anon()
"""
def on_message(message, data):
    if message['type'] == 'send':
        payload = message['payload']
        so_name = payload['name']
        base_address = payload['base']
        size = payload['size']
        print(f"Dumping {so_name} (Base: {base_address}, Size: {size})")
        # 保存dump的.so文件
        with open(so_name, "wb") as f:
            f.write(data)
        print(f"{so_name} dumped successfully!")
    else:
        print(f"Error: {message}")
def main():
    # 附加到目标进程
    device = frida.get_usb_device()
    session = device.attach(8601)
    # 加载Frida脚本
    script = session.create_script(js_script)
    # 设置消息处理函数
    script.on("message", on_message)
    # 加载并执行脚本
    script.load()
if __name__ == "__main__":
    main()

执行结果如下:

>dumpso.py
开始dump
Dumping libanon.so (Base: 0x7e25afc000, Size: 507904)
libanon.so dumped successfully!

脱下来的libanon.so位于当前执行脚本的目录下。

07.IDA手修匿名内存SO

拖到010 editor里去可以看到前面一大片都是0,也就是没有ELF的文件头,果然是匿名内存段的风格。大概率也sofix没法修。
拖到IDA里,IDA也无法判断这是什么汇编格式,手动选择一个处理器类型:ARM Little-endian ,点击确定。


一路点击确定,按照ARM默认设置来分析这段匿名内存so。此时IDA左边已经有了一堆sub_符号,说明IDA是可以正常分析里面的函数的。
说明哪怕只有一个text段,IDA也是可以正常反汇编的,哪怕没有导入导出表,没有文件头没有符号表,没有其他区段,也没有关系。
接下来开始做基础修复,视图→打开子视图→类型库,导入基础库,右键,加载类型库,导入android_arm64库,和gnulnx_arm64,前者是安卓的,后者是C++的。导进去才能识别JNI的东西,才能有JNIEnv和jclass这些,后续要改参数类型识别JNI里面的函数如RegisterNatives、FindClass那些。

08.IDA手修匿名内存SO(2),匿名指针函数代码追踪

以 ARM64 Little-endian 打开;导入 android_arm64 / gnulnx_arm64 类型库以恢复 JNI/GLIBC 符号语义;

手工增加 rodata 等伪区段解决“无效内存访问”。

8.1 寻找真实执行的函数

到这里还没找到前面实际执行的off_C838函数在哪里,可以把函数指针地址减去基地址即可得到真实的偏移。

console.log("off_C838 real offset => ",libdexprotector.base.add(0xC838).readPointer().sub(Process.findRangeByAddress(libdexprotector.base.add(0xC838).readPointer()).base))

执行一下,也就是sub_4e984。

off_C838 real offset =>  0x4e984

8.2 无效内存访问修复

在IDA里按g跳到0x4e984的地址,就是sub_4e984的函数头,按F5汇编即可开始分析。
首先有些报红字的,无效内存访问报错:

MEMORY[0x8A800] = v1;
MEMORY[0x8A808] = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v7 + 168LL))(v7, v6);

其实就是dump下来的so里没有数据段的原因,视图→打开子视图→区段,右键→添加区段,区段名称可以取rodata,开始地址可以填0x8A800,结束地址0x8B800,给大一些。后面再遇到红字的,可以继续扩大一些,覆盖到红字指向地址的范围即可。
再回到sub_4e984函数再按一下F5,红字就消失了,变成了:

unk_8A800 = v1;
unk_8A808 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v7 + 168LL))(v7, v6);

点在unk_8A800上按x,就可以追踪交叉引用了,可以看到有四处引用到这个地址:


前面JNIOnLoad里面就一个vm参数传到off_C838了,那这里sub_4e984参数类型就是JavaVM*,按y修改。修改成功后,一些指针也会从这样:

   if ( (*(unsigned int (__fastcall **)(__int64, __int64 *, __int64))(*(_QWORD *)a1 + 48LL))(a1, &v7, 65540) )

变成这样:

if ( (*a1)->GetEnv(a1, (void **)&v8, 65540LL) )

完整的结果:


识别出来了env里的指针,这也是前面导入库发挥了作用。
接下来即可进行基本的逐行手撕了,以及不停地做一些尝试绕过检测的hook了。在逆向中也是这样不断地做尝试的。
接下来理论上要进行全量分析了,逆向到最后,归根结底都是体力活罢了。

09.JNIEnv 恢复与动态注册链梳理

逐行手撕定位敏感函数

9.1 JNIEnv 恢复

继续分析sub_4e984,a1获取env传给v7了:

(*a1)->GetEnv(a1, (void **)&v7, 65540) 

那v7的参数类型就是JNIEnv*,在定义那里__int64 v8;按y修改一下参数类型。原本的几个不明意义的指针:

if ( v3 )
  {
    (*(void (__fastcall **)(__int64))(*(_QWORD *)v7 + 136LL))(v7);
    return (unsigned int)(v3 + 1000);
  }
  else
  {
    v4 = sub_4EBE0(v7);
    if ( v4 )
    {
      v5 = v4;
      if ( !(*(unsigned __int8 (__fastcall **)(__int64))(*(_QWORD *)v7 + 1824LL))(v7) )
        sub_4EFC4(v7, v5);
    }
    if ( (*(unsigned __int8 (__fastcall **)(__int64))(*(_QWORD *)v7 + 1824LL))(v7) )
    {
      v6 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v7 + 120LL))(v7);
      (*(void (__fastcall **)(__int64))(*(_QWORD *)v7 + 136LL))(v7);
      unk_8A808 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v7 + 168LL))(v7, v6);
    }
    return 0;
  }

修改成功后,又变得有意义了:

  if ( v3 )
  {
    (*v7)->ExceptionClear(v7);
    return (unsigned int)(v3 + 1000);
  }
  else
  {
    v4 = sub_4EBE0(v7);
    if ( v4 )
    {
      v5 = v4;
      if ( !(*v7)->ExceptionCheck(v7) )
        sub_4EFC4(v7, v5);
    }
    if ( (*v7)->ExceptionCheck(v7) )
    {
      v6 = (__int64)(*v7)->ExceptionOccurred(v7);
      (*v7)->ExceptionClear(v7);
      unk_8A808 = (*v7)->NewGlobalRef(v7, (jobject)v6);
    }
    return 0;
  }

9.2 动态注册链路梳理,函数分析,发现重点可疑目标

从上往下看,接下来它把v8这个jnienv依次传给了sub_4EAA0sub_4EBE0sub_4EFC4这几个函数,这些函数都要跟进去逐行查看,每一行到底做了什么。
先看第一个sub_4EAA0,唯一的参数a1修改类型为JNIEnv*,下面立刻动态注册的API出来了。

jint (*RegisterNatives)(JNIEnv *, jclass, const JNINativeMethod *, jint); 

动态注册的函数列表保存在v6参数中,

    v6[0] = v9;
    v6[1] = sub_40814(&unk_681F);
    v6[2] = sub_4F0F4;
    v6[3] = v8;
    v6[4] = sub_40814(&unk_346B);
    v6[5] = sub_4F254;
    v6[6] = v7;
    v4 = sub_40814(&unk_5523);
    RegisterNatives = (*a1)->RegisterNatives;
    v6[7] = v4;
    v6[8] = MEMORY[0x82988];
    if ( RegisterNatives(a1, (jclass)v3, (const JNINativeMethod *)v6, 3) )

其中sub_4F0F4开辟了一段字节数组,像是初始化函数,先不看。
sub_4F254特别长,业务逻辑十分丰富,还夹杂着很多Java类方法的使用,大概五六百行,一眼看不出逻辑,需要细细拆分研究。这里是接下来分析的重点。

10.关键函数 sub_4F254 的行为探测(返回值与入栈点)

直接hook这个sub_4F254,观察一下结果,并首次尝试修改结果过检测:

Interceptor.attach(libanon.base.add(0x4F254),{
     onEnter: function(){
        console.log("onEnter  0x4F254 ")
     },
     onLeave: function(retval){
        console.log("onLeave  0x4F254  ",retval.toInt32())   
     }
})

结果是:

onEnter  0x4F254 
onEnter  0x4F254 
onLeave  0x4F254   0
onLeave  0x4F254   1

尝试修改返回值为0,测试是否能过掉检测:

Interceptor.attach(libanon.base.add(0x4F254),{
     onEnter: function(){
        console.log("onEnter  0x4F254 ")
     },
     onLeave: function(retval){
        retval.replace(0)
        console.log("onLeave  0x4F254  ",retval.toInt32())   
     }
})

返回值修改为0,成功。但是app依然崩溃了,果然,一切不会这么简单。继续分析中间的逻辑,检测大概率就是在中间的逻辑完成。

onEnter  0x4F254 
onEnter  0x4F254 
onLeave  0x4F254   0
onLeave  0x4F254   0

必须深入其内部校验路径(而非仅“短路返回”)——尤其是完整性/哈希相关分支。

11.浮点运算特征联合GPT定位CRC哈希算法

进这个sub_4F254函数,从上往下看,第一个可疑的地方:

if ( v34 == sub_161E8(unk_82948, unk_82990 - unk_82948, &v75) )
    sub_14D24(byte_872AC, 64, v32, 32, v33);

点进sub_161E8函数,可以看到大量浮点数寄存器,vaddq_s64veorq_s8vorrq_s8,一般这种不是哈希就是加解密,再结合函数开头就有一些初始常量,大概率是哈希:

  v12 = v10 ^ 0x7465646279746573LL;
  v13 = v10 ^ 0x646F72616E646F6DLL;
  v14 = a2 << 56;
  v15 = *a3 ^ 0x6C7967656E657261LL;
  v16 = *a3 ^ 0x736F6D6570736575LL;

其实猜算法这件事情,最拿手的应该是GPT,直接整个F5全部复制黏贴过去问GPT,GPT告诉我这很像是SipHash哈希算法的实现,具体是 SipHash-2-4 变种(2轮压缩,4轮最终化),真假先不做评价,先hook看下。

var libanon = Process.findRangeByAddress(libdexprotector.base.add(0xC838).readPointer()).base
Interceptor.attach(libanon.base.add(0x161E8),{
    onEnter:function(args){
        console.log("sub_161E8 enter")
    },onLeave:function(ret){
        console.log("sub_161E8 leave",ret)
    }
})

单纯hook结果没什么特别:

sub_161E8 enter
sub_161E8 leave 0xff4414fe9bf00ecd
sub_161E8 enter
sub_161E8 leave 0xff4414fe9bf00ecd

但从结果来看这里其实是做crc哈希校验的地方。sub_161E8 属于完整性哈希链路;但仅满足判等未必足以阻断后续自校验/熔断。

12.HMAC-SHA256 路径确认(内联 SHA 指令识别)

全局hex搜挂哈希魔术定位CRC校验

12.1 分析定位 HMAC-SHA256

问了下GPT,0xff4414fe9bf00ecd这个值看起来像是一个64位的哈希值(16个十六进制字符 * 4位/字符 = 64位)。根据其长度和格式,它最有可能来自以下几种哈希算法:

  • xxHash: 一个极快的非加密哈希算法,64位版本会生成一个16字符的十六进制值。这是一个非常强有力的候选。
  • MurmurHash3: 另一个经典的非加密哈希函数,其128位版本更常见,但它也有64位的变体。
  • CityHash, FarmHash: 由Google发布的哈希函数系列,用于类似的目的,能产生64位哈希。
  • SipHash: 虽然也是64位输出,但它更注重防止哈希洪水攻击,常用在编程语言的字典实现中(如Python、Ruby)。
    当然如果是结果导向的话,逆完之后发现是xxHash算法。
  • 继续回到sub_4F254里面,如果返回值与v34相等,则会进入sub_14D24,那再进入sub_14D24看下做了什么。

    v34 = unk_8A810;
    if ( v34 == sub_161E8(unk_82948, unk_82990 - unk_82948, &v75) )
    sub_14D24(byte_872AC, 64, v32, 32, v33);

    sub_14D24里面可以看到,sub_2FA44做了一些初始化,sub_304A8sub_30B14里面则演都不演了,直接内联汇编了SHA256的算法,也可能是编译优化的产物,为了加快运行速度。


    两次SHA256的计算,那合理猜测是做的HMAC算法,合理猜测sub_14D24就是HMAC的入口。

12.2 通过010editor,快速查找使用了sha256的函数

打开IDA的选项→常规,操作码字节数改成8,点确定,随便找个SHA256里面已经优化好的汇编,看下操作码,比如这一行:

ROM:0000000000030708 83 28 28 5E   SHA256SU0       V3.4S, V4.4S

就是8328285E,把linanon.so文件拖到010 editor里面去,全局搜这个二进制,可以得到如图12处结果。
复制行号,到IDA里去看了下,前六处属于刚刚的sub_304A8函数,后六处属于sub_30B14函数,也就是一共就俩函数进行SHA256校验。

13.等式化替换策略:让 CRC/Hash“必相等”

基于 lr 获取调用点(0x4f5a4 / 0x4f6cc / 0x5c494),对 sub_161E8 的返回值在不同入栈点替换为预期内存值(0x8A810/0x8AB20)以通过等式校验。

13.1 HMAC-SHA256入口函数分析

再回到sub_14D24可以发现,先调用了sub_304A8函数之后,又立即调用了sub_30B14,也就是sub_14D24应该是HMAC-SHA256的入口。
那同样hook看下有没有经过sub_14D24函数。

Interceptor.attach(libanon.base.add(0x14D24),{
    onEnter:function(args){
        console.log("sub_14D24 enter",this.context.lr.sub(libanon.base))
    },onLeave:function(ret){
        console.log("sub_14D24 leave",ret)
    }
})

输出是:

sub_14D24 enter 0x4ee38
sub_14D24 leave 0x7ff213aa70

而此处调用的汇编地址是0x4F5C8,也就是sub_4F254+354处,很明显不是0x4ee38,也就是此处没有进入执行sub_14D24的逻辑。

13.2 HOOK绕过CRC校验使其相等

那如何让它进入呢?只要v34sub_161E8运行的结果相等即可进入。这其实就是一段CRC的内存校验,可以写个脚本来使其相等。
首先看下有哪些地方对sub_161E8进行了校验,前面的代码加上一句lr返回值地址的输出:

console.log("sub_161E8 enter",this.context.lr.sub(libanon.base))

跑一下:

sub_161E8 enter 0x4f5a4
sub_161E8 leave 0x7799165d5282bf95
sub_161E8 enter 0x4f6cc
sub_161E8 leave 0x7799165d5282bf95

两处进行了校验,0x4f5a40x4f6cc处,那就把这两处的返回值都看一下,要等于哪处内存的值,才能进入相等后的逻辑:
通过x查找引用,发现还有一个地方也使用sub_161E8做了比较0x5C494

var hash_crc = {
        "0x4f5a4" : 0x8A810,
        "0x4f6cc" : 0x8A810,
        "0x5c494" : 0x8AB20
}

最终完整的相等逻辑代码是:

Interceptor.attach(libanon.add(0x161E8),{
    onEnter:function(args){
        console.log("sub_161E8 enter",this.context.lr.sub(libanon))
        this.lr = this.context.lr.sub(libanon)
    },onLeave:function(ret){
        if(hash_crc[this.lr.toString()]){
            ret.replace(libanon.add(hash_crc[this.lr.toString()]).readU64())
            console.log("sub_161E8 leave",ret)
        }                            
    }
})

为何lr寄存器要使用onEnter时机的而不能是onLeave时机的?因为frida在hook替换的时候已经把lr修改的面目全非了,这涉及到native hook的调用顺序和核心原理,可以问问GPT,这里不再赘述。所以得保留原来的lr才是正确的。
跑一下:

sub_14D24 enter 0x4ee38
sub_14D24 leave 0x7ff213aa70
sub_161E8 enter 0x4f5a4
sub_161E8 leave 0x40a05af38cb96159
sub_14D24 enter 0x4f5c8
sub_14D24 leave 0x7ff2139460
sub_161E8 enter 0x4f6cc
sub_161E8 leave 0x40a05af38cb96159

很明显替换成功了,sub_161E8返回值由0x7799165d5282bf95替换成了0x40a05af38cb96159,且进入了0x4f5c8处的sub_14D24函数计算逻辑。
只是很不幸,还是没能绕过,进程还是崩溃了。胜败乃兵家常事,英雄请重新来过。

SO里的每一行汇编都要扒光,要让它没有秘密。全扒光就拥有了维多利亚的秘密。

该策略仅能跨过“第一道门”,后续仍有追加校验/副通道检测(如 maps 轮询 / 线程监控)。

14.字符串解密链与 /proc/self/maps 检测点确认

分析发现字符串处理,关键信息

14.1 发现字符串检测

sub_50130->修改a1类型为JNIEnv之后,发现调用了字符串的方法:


v4是一个类,传入了v19,那么猜测sub_55650可能是一个字符串解密的函数,下面的sub_40814似乎也是一个字符串揭秘函数,
那么接下来直接hook它们

sub_55650(&unk_87419, v19, 257);
  v4 = (*a1)->FindClass(a1, v19);
  if ( (*a1)->ExceptionCheck(a1) )
    return 46;
  v5 = (void *)sub_3CF88();
  sub_55650(&unk_87218, v18, 65);
  GetStaticMethodID = (*a1)->GetStaticMethodID;
  v7 = sub_40814(dword_346B, &v16, 22);
  v8 = GetStaticMethodID(a1, v5, v18, v7);
  if ( (*a1)->ExceptionCheck(a1) )
  {
    return 46;
  }
  else
  {
    v11 = (*a1)->ToReflectedMethod(a1, v5, v8, 1);
    sub_55650(&unk_872F5, v17, 65);
    v12 = (*a1)->GetStaticMethodID;
    v13 = sub_40814(dword_3EC3, &v15, 24);
    v14 = v12(a1, v4, v17, v13);
    v9 = 46;
    if ( !(*a1)->ExceptionCheck(a1) )
    {
      (*a1)->CallStaticVoidMethod(a1, v4, v14, v11, a2);
      if ( (*a1)->ExceptionCheck(a1) )
        return 46;
      else
        return 0;
    }
  }
Interceptor.attach(libanon.base.add(0x55650),{
    onEnter: function(){
        this.lr = this.context.lr.sub(libanon.base)
    },
    onLeave: function(retval){
        console.log("[0x55650]",retval.readCString()," lr => ",this.lr);
    }
})
//Interceptor.attach(libanon.base.add(0x40814),{ //0x40814这个函数内部做了inlinehook,直接这样hook,就崩溃了
//那么这个我们可以偏移4条指令进行hook 
Interceptor.attach(libanon.base.add(0x40814+ 4*4),{ 
    onEnter: function(){
        if(libanon.base){
            this.lr = this.context.lr.sub(libanon.base)
        }  
    },
    onLeave: function(retval){
        if(this.lr){
            console.log("[0x40814]",retval.readCString()," lr => ",this.lr);
        }else{
            console.log("[0x40814]",retval.readCString());
        }

    }
})

结果是:

14.2 通过输出的libart.so的地址,找到调用函数,并修改返回值

分析这个结果,发现检测了分段maps,这里通过libart.so的0x61a78去找到函数进行hook,检测一般都是通过这里进行。
在ida中g这个0x61a78,找到调用的函数为sub_61974,hook这个看一下结果:

Interceptor.attach(libanon.base.add(0x61974),{ 
        onEnter: function(){
            this.lr = this.context.lr.sub(libanon.base)
        },
        onLeave: function(retval){      
            console.log("[0x61974]",retval.toInt32()," lr => ",this.lr);
        }
    })

运行结果,发现返回值是786。在ida中分析sub_61974,返回值786是出现了异常,正常返回应该是v1=0,那么我们这里hook替换一下返回值:

0x61974 =>  786  lr =>  0x4f630
retval.replace(0);

这里修改返回之后,运行依然崩溃了,但是日志输出要比之前多一些了。
说明 maps 检测非唯一触发,且与其他面耦合。

15.二分排除法缩窗(定位“必崩区间”)

注意,这里开始,我们切换Zygisk Frida Gadget,使用方法在这里(https://github.com/sucsand/sucsand) ,绕过一部分基于ptrace的检测,并开始使用二分排除法,来定位出问题的地方。
将“必崩窗口”缩至少量函数,有利于后续精确打补丁。

15.1 使用Zygisk Frida Gadget模块

注意:从frida-server换到Zygisk Frida Gadget模块后,重启一下手机。使用Zygisk Frida Gadget的时候,要先正常启动过一次app。
在sucsand中,勾选酒店app,并设置200延迟。

配置完成后,在桌面的酒店应用图标长按,然后点强行停止,然后再重新打开app,此时app启动会被阻塞:


现在开始使用frida -H 192.168.0.101:9999 -F -l dexprotect2.js 来执行脚本。9999是模块内置的端口号。

15.2 继续分析检测点

继续往下分析sub_4F254,现在都是体力活儿,只能挨着往下分析,看着确实无聊。

现在来到了sub_27398,这里看着像是在做检测:

      v41 = sub_27398(v39, &v70, 32, &v69, 8, &v68, 4);
      if ( (unsigned __int8)v68 != unk_872EC
        || BYTE1(v68) != unk_872ED
        || BYTE2(v68) != unk_872EE
        || HIBYTE(v68) != unk_872EF )
      {
        sub_62F48(11);
        v41 = sub_62F74(&v68, 4);
      }
      v24 = v74;
      if ( !v74 )
Interceptor.attach(libanon.base.add(0x27398),{     //fail
    onEnter: function(){
        this.lr = this.context.lr.sub(libanon.base)
        console.log("onEnter  0x27398 ",this.lr)
        Interceptor.detachAll()
    },
    onLeave: function(retval){
        console.log("[0x27398] ",retval.toInt32(),"lr",this.lr)
    }
})

我们在这里执行全部卸载,发现还是崩溃了,说明在这里之前就已经对整个内存进行检测了,那么我们要找一个没有检测的点,来缩小排查范围。
找这个点的原则是,这个函数只调用了一次,并且卸载后能保证app顺利运行。
4F254 ,可以
593FC ,可以
27398 ,不行
那么就可以确定, 问题出在593FC~27398之间。那么我们又从这个中间的函数开始进行排查,一步一步缩小位置:
3EA0C ,可以
15128 ,不行
那么现在进一步缩小到了3EA0C~15128之间,分析的范围大大减小了:


进一步分析,sub_14F7C,没有看出什么问题,暂时排除。

目前这些函数都没有分析出什么有效的信息。

16.入口校验绕过(复制 text → 替换参数基址)

在没有什么有效信息的情况下,当一次侥幸哥,进行暴力拆解

16.1 发现对入口函数进行了检测

上面分析出来的sha256的sub_304A8,来hook一下:

Interceptor.attach(libanon.base.add(0x304A8), {
    onEnter: function (args) {
        console.log("onEnter  0x304A8 ",args[0],args[1],args[2],"base ",rangeDetails.base)
    }
})

结果是:


通过分析结果,发现304A8,对入口的函数也做了检测

16.2 处理对入口函数检测

针对 sub_304A8 的入口校验:

先把匿名段 text 拷贝一份 origin;

若发现其校验针对当前段基址,则把参数中的基址替换为 origin(干净副本)以规避校验。

    var origin
    var size=0x7a2a0

    origin = Memory.alloc(size)
    origin.writeByteArray(libanon.base.readByteArray(size))

    Interceptor.attach(libanon.base.add(0x304A8), {
        onEnter: function (args) {
            if (args[1].toString() === libanon.base.toString()) {
                var rangeDetails = Process.findRangeByAddress(args[0]);
                console.log("onEnter  0x304A8 ",args[0],args[1],args[2],"base ",rangeDetails.base)
                args[1] = origin
                console.log("[0x304A8] 替换成功")
            }
        }
    })

此时已经过掉了检测,并进入了首页。但是输出看到,不停在刷,maps检测明显开了线程。

17. 线程面收敛:定位 pthread_create 入口并“即刻返回”

虽然已经完成了过掉frida检测,为了完美一点,我们再来找到线程并处理掉它。

17.1 定位pthread_create

根据 "/proc/self/maps" 调用的反向引用,追到 pthread_create

我们使用[0x40814] /proc/self/maps  lr =>  0x3b66c的0x3b66c,在ids中按x,一步一步向上查找。最终发现了sub_7A230,这个看起来很像是pthread_create函数:

pthread_create函数原型:

#include <pthread.h>

int pthread_create(
    pthread_t *thread,                  // [out] 新线程ID
    const pthread_attr_t *attr,         // [in]  线程属性(可为NULL)
    void *(*start_routine)(void *),     // [in]  线程入口函数
    void *arg                           // [in]  传给入口函数的参数
);

17.2 查找所有使用pthread_create的地方,并处理掉

找到被创建的线程入口地址,在运行态用 Arm64Writer 写入 RET,实现就地空返回。

在ida中,我们通过x键,查找sub_7A230的引用,然后去把线程入口函数给处理掉。

function retFunc(parg2) {
    // 修改内存保护,使其可写
    Memory.protect(parg2, 4, 'rwx');
    // 使用 Arm64Writer 写入 'ret' 指令
    var writer = new Arm64Writer(parg2);
    writer.putRet();
    writer.flush();
    writer.dispose();
    console.log("ret " + parg2 + " success");
}
retFunc(libanon.base.add(0x5A708))
retFunc(libanon.base.add(0x5BE28))
retFunc(libanon.base.add(0x5D1A4))
retFunc(libanon.base.add(0x5D710))

最终结果,没有那么多线程检测一直刷了,看着比较舒服。


结论回顾(方法 > 结论)

入口选择决定成败:相对直接 Hook System.loadLibrary,从 __loader_android_dlopen_ext 切入能更早获得“真实装载面”的证据;

匿名映射段是关键战场:JNI_OnLoad → 函数指针 → 匿名段 这一跳,要求在内存态完成 dump 与“仅 text 段”的最小可用反汇编;

校验链路是主线:识别 xxHash/SHA256/HMAC 的组合与落点,用等式化替换与调用点定位做最小侵入的试探;

系统性缩小问题空间:从可卸载点开始做二分排除,定位到刷 maps 的线程入口,补丁而非大面积禁用,以减小误伤面与回归压力。

限制与风险

结论对ROM/内核/ART 版本敏感:不同 SoC 与 API Level 下,maps 命名、权限组合、linker 细节均可能影响可见性与时序。

某些对抗属于“只对当下样本有效”:比如栈上/堆上指针偏移、匿名段大小、HMAC 初始材料位置等,需在发布后持续校验与回归。

工具链差异:Zygisk Frida Gadget 与纯 frida-server 的可见性与时序差异


img_21.png (233.98 KB, 下载次数: 0)

img_21.png

img_11.png (186.36 KB, 下载次数: 0)

img_11.png

libart的maps检测.png (114.61 KB, 下载次数: 0)

libart的maps检测.png

img.png (114.61 KB, 下载次数: 0)

img.png

img_24.png (94.64 KB, 下载次数: 0)

img_24.png

过掉的frida命令.png (396.75 KB, 下载次数: 0)

过掉的frida命令.png

小工具配置.png (156.44 KB, 下载次数: 0)

小工具配置.png

img_2.png (193.9 KB, 下载次数: 1)

img_2.png

img_1.png (128.86 KB, 下载次数: 0)

img_1.png

frida过掉的结果.png (148.07 KB, 下载次数: 0)

frida过掉的结果.png

frida检测过掉之后进入首页.png (88.71 KB, 下载次数: 0)

frida检测过掉之后进入首页.png

免费评分

参与人数 16吾爱币 +18 热心值 +16 收起 理由
junjia215 + 1 + 1 用心讨论,共获提升!
Yao2903 + 1 + 1 谢谢@Thanks!
Quinn01 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
chenjingyes + 1 + 1 谢谢@Thanks!
kent_up + 1 + 1 我很赞同!
klop + 1 + 1 感谢分享,看似简单,其实一点都不容易
wqx520 + 1 + 1 谢谢@Thanks!
max2012 + 1 + 1 我很赞同!
pk8900 + 3 + 1 用心讨论,共获提升!
xuanle + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Eviden + 1 + 1 用心讨论,共获提升!
无敌小车 + 1 + 1 热心回复!
jbczzz + 2 + 1 牛的
HelloF + 1 + 1 论坛禁止求脱求破,求助软件分析思路,务必在主题帖中描述清楚你的分析思路 ...
buluo533 + 1 + 1 用心讨论,共获提升!
唐小样儿 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
正己 发表于 2025-11-21 10:04
表哥,看看把md的部分转化一下格式,现在阅读体验不是最佳,论坛的编辑器可以转换
推荐
zglhappy 发表于 2025-11-21 12:38
实战派狂喜!这篇文章最打动我的是不回避问题从 hook System.loadLibrary 崩,到改 hook __loader_android_dlopen_ext,再到发现检测逻辑藏在匿名段,每一次碰壁后的转向都特别真实,完全还原了实际逆向中的试错过程。
印象最深的是哈希校验绕过的思路:没有硬刚算法实现,而是通过 LR 寄存器定位调用点,用内存中预存的预期值做等式替换,这种 “借力打力” 的技巧太巧妙了!还有二分法缩窗定位必崩区间,从 593FC~27398 到 3EA0C~15128,把复杂问题拆解成可落地的小目标,这种排查思路不仅适用于逆向,在日常调试中也超实用。
另外,文中对环境基线的强调(LineageOS 21/Nexus 5X/Magisk 29.0.0)特别良心,很多新手做逆向时忽略环境一致性导致复现失败,这点真的要重点学习。最后想问下大佬们,后续有没有计划补充 “不同 API Level 下 maps 标记差异” 的适配技巧?比如在 Android 14 上匿名段的命名规则是否有变化,hook 时机是否需要调整?期待更多延伸分享!
评论 3(简洁致敬 + 价值肯定)
3#
zepp7289 发表于 2025-11-21 10:13
常规思路一般是直接过pthread_create,有时候暴力出奇迹啊
4#
唐小样儿 发表于 2025-11-21 10:23
这帖子起码写一天吧
5#
cyberjoo 发表于 2025-11-21 10:47
最近工作有用到frida,前来学习一下
6#
 楼主| fyr666 发表于 2025-11-21 11:04 |楼主
正己 发表于 2025-11-21 10:04
表哥,看看把md的部分转化一下格式,现在阅读体验不是最佳,论坛的编辑器可以转换

奇怪,我是用论坛的MD,把我文章复制进去的。格式怎么带出来。我研究一下
7#
 楼主| fyr666 发表于 2025-11-21 11:21 |楼主
唐小样儿 发表于 2025-11-21 10:23
这帖子起码写一天吧

绝对不止一天,自己复现过程就好几天
8#
mscsky 发表于 2025-11-21 12:02
太硬核了
10#
li083m 发表于 2025-11-21 13:46
感谢分享,很有收获
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-1-24 04:59

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表