首页
社区
课程
招聘
[原创]梆梆Frida过检测202607
发表于: 1小时前 58

[原创]梆梆Frida过检测202607

1小时前
58

前言

之前写了一版LibDexHelper.so的反调试分析,但是其实当时我还一直没有绕过反调试(刚入门当时只是想看看现在的anti frida是怎么写的...,然后有兄弟直接私信问我了,那我不得不研究一下了),说实话,当时网上找到的一些资料绕过方法我一看发现都是绕过老版本的LibDexHelper,所有方法在我这个版本的梆梆加密壳上面均无用(例如我看到找exit group的,也看到用通杀绕过脚本的,但是实际上在我这里并未奏效,可能由于这个版本比较新。)

既然如此,那就让我自己来研究研究怎么绕过新版的梆梆加密吧。本文也被发到我的公众号:点击跳转

TL;DR

针对梆梆加密(LibDexHelper.so):

  1. 卡点不是 dlopen 入口,而是 dlopen 返回时init_array 执行完后才会解密出真正的 payload 并替换符号表,此时 JNI_OnLoad 才“出现”。
  2. 唯一的反调试自杀逻辑在 sub_11C64:它会根据参数计算出非法地址,然后执行 MOV SP, X0; BR X12,直接让进程崩溃。把 sub_11C64 patch 成空函数即可让 JNI_OnLoad 完整走完。
  3. DEX 解密入口是 sub_F490。它通过反射调用 art::DexFileLoader::OpenCommon 把内存中的 DEX 加载进来;在入口处 hook 即可 dump 出 8 个解密后的 DEX。
  4. JNI_OnLoad 返回 0x10004,说明壳本身执行完毕;但之后仍会在 Java/framework 层出现 access-violation @ 0x0 式的 NPE 崩溃,进程最终被系统终结。
  5. 后续将基于 RegisterNatives 绑定表,对这些 native 函数逐一进行系统性分析。

正文

样本信息:

项目说明
package namecn.samsclub.app应用包名
versionCode659内部递增版本号
versionName5.0.144对外版本号
sdkVersion (minSdk)23最低支持 Android 6
BuildConfig.DEFAULT_PATCH_ID2026-05-26 15:44:11热修复时间

前情提要:之前简单分析了LibDexHelper.so,写了一版解密和反调试分析,因此我们仔细读源代码可以知道以下三个要点,这也是我们快速切入这个壳的核心要点。

  1. libDexHelper.so 本身原本并不导出 JNI_OnLoad。壳的 ELF 导出表里一开始没有这个符号,所以你直接在 dlopen 时调用 Module.findExportByName("libDexHelper.so", "JNI_OnLoad") 会拿到 null,挂上去的 hook 自然无效,甚至可能因为地址非法直接崩溃。
  2. JNI_OnLoad 是在 init_array 执行完之后才“出现”的。 sub_4780 → sub_33F8 会解密内嵌的 payload,然后把 payload 的 .dynsym / .dynstr / .hash 复制到 stub 自己的运行时符号表区域(0x145000 / 0x147000 / 0x148000)。只有这个复制完成后,libDexHelper.so 才“看起来”导出了 JNI_OnLoad。
  3. 正确的卡点不是在 dlopen 进入时,而是在 dlopen 返回时。  dlopen 同步执行构造函数,返回时 init_array 已经跑完、符号表已经被替换,但 ART/linker 还没调用 JNI_OnLoad。这个窗口期才能成功 hook。
  4. 依旧先贴一下加载库的函数调用顺序,写Frida有个参考。
java.lang.System.loadLibrary
  libart.so!Runtime_void_Runtime_load0
    linker64!__dl___loader_android_dlopen_ext //这里可以先判断当前加载的文件
      linker64!call_constructors //这里so加载完成
        lib.so!init_array //反调试反frida逻辑在这里被加载
    libart.so!art::JavaVMExt::LoadNativeLibrary(...)
      lib.so!JNI_OnLoad

山穷水复

首先先贴一下用Frida启动梆梆加密过的APP是什么退出信息,这是个很重要的点,后面会提到。


由于之前已经分析过INIT函数,确定它只是一个解密Loader,因此我们可以直接Hook Dlopen执行完,并且打开Trace。

【代码省略】

得到的输出到下面这里就停止了,但是观察到崩溃并不是反调试导致的崩溃,否则应该会有StackTrace。

[EXEC] 0x77fd1c3240 cbz x8, #0x77fd1c3250
[EXEC] 0x77fd1c3250 str x8, [x27, #8]
[EXEC] 0x77fd1c3254 str x27, [x9, #8]
[EXEC] 0x77fd1c3258 mov x27, x25
[EXEC] 0x77fd1c325c mov x0, x22
[EXEC] 0x77fd1c3260 bl #0x77fd1f6180
[EXEC] libc.so + 0x6c558 sub x9, x10, #1
[EXEC] libc.so + 0x6c55c eor x9, x9, x10
[EXEC] libc.so + 0x6c560 ands x11, x11, x9
[EXEC] libc.so + 0x6c564 csel x6, x2, x6, ne
[EXEC] libc.so + 0x6c568 csel x7, x11, x7, ne
[EXEC] libc.so + 0x6c56c clz x5, x7
[EXEC] libc.so + 0x6c570 add x5, x5, #2
[EXEC] libc.so + 0x6c574 sub x0, x6, x5, lsr #1
[EXEC] libc.so + 0x6c578 cmp x7, #0
[EXEC] libc.so + 0x6c57c csel x0, x0, xzr, ne
[EXEC] libc.so + 0x6c580 ret 
[EXEC] 0x77fd1c3244 ldr w10, [x8]
[EXEC] 0x77fd1c3248 cmp w10, w26
[EXEC] 0x77fd1c324c b.lt #0x77fd1c3238
Process terminated

Thank you for using Frida!

因此我们可以考虑延迟加Trace,考虑到Frida Trace的稳定性,我们可以写一个轻量级的延迟加载Frida脚本如下(节选部分:由于完整脚本较长,这里只贴关键部分,主要展示核心逻辑,其他辅助函数日志、异常处理、工具函数等已省略。)

'use strict';

const CFG = {
  soName: 'libDexHelper.so',
  mode: 'surgical',
  logRealCrash: true,
  surgicalStartOff: 0x33d10,

  stalkerEvents: {
    call: true,
    ret: true,
    exec: false,
    block: false,
    compile: false,
  },

  excludeLibc: true,
  excludeArt: true,
  excludeLinker: true,

  trustThreshold: 1,
  queueCapacity: 65536,
  queueDrainInterval: 100,

  payloadSize: 0x149000,
  dumpPath: '/data/local/tmp/dexhelper_dump.bin',
  eventSink: 'send',
  stalkerLogPath: '/data/local/tmp/stalker_events.log',
  eventBatchSize: 512,
};
// ...

function startStalker(tid, label) {
  console.log(`[STALKER] start ${label} on tid=${tid} sink=${CFG.eventSink}`);
  const buf = [];
  const logFile = openStalkerLog();

  function flushBatch() {
    if (buf.length === 0) return;
    const batchStr = buf.splice(0, buf.length).join('\n');
    if (logFile) {
      logFile.write(batchStr + '\n');
    } else {
      send({ type: 'stalker-batch', label: label }, batchStr);
    }
  }

  Stalker.follow(tid, {
    events: CFG.stalkerEvents,
    onReceive(events) {
      const parsed = parseEvents(events);
      if (!parsed || !parsed.length) return;
      for (let i = 0; i < parsed.length; i++) {
        const ev = parsed[i];
        let local = false;
        if (ev.length >= 2) local = addrInPayload(ev[1]);
        if (!local && ev.length >= 3) local = addrInPayload(ev[2]);
        if (local || ev[0] === 'compile') {
          buf.push(JSON.stringify(ev));
          if (buf.length >= CFG.eventBatchSize) flushBatch();
        }
      }
    },
    onCallSummary(summary) {
      const keys = Object.keys(summary);
      const filtered = {};
      for (let i = 0; i < keys.length; i++) {
        if (addrInPayload(keys[i])) {
          filtered[keys[i]] = summary[keys[i]];
        }
      }
      send({ type: 'stalker-summary', label: label, summary: filtered });
    },
  });
  return { buf: buf, file: logFile, flush: flushBatch };
}

function stopStalker(tid, handle) {
  Stalker.flush();
  if (handle) {
    handle.flush && handle.flush();
    if (handle.file) {
      handle.file.flush && handle.file.flush();
      handle.file.close();
      console.log(`[STALKER] log closed`);
    }
  }
  Stalker.unfollow(tid);
  Stalker.garbageCollect();
  console.log(`[STALKER] stopped on tid=${tid}`);
}

function installSurgicalHook() {
  const start = gPayloadBase.add(CFG.surgicalStartOff);

  if (!looksLikeCode(start)) {
    console.log(`[!] surgical start off ${CFG.surgicalStartOff.toString(16)} does not look like code;`);
    console.log(`    will defer attach via JNI_OnLoad entry.`);
    installDeferredSurgicalHook();
    return;
  }

  console.log(`[*] surgical hook @ ${fmtAddr(start)}`);
  Interceptor.attach(start, {
    onEnter(args) {
      const tid = Process.getCurrentThreadId();
      console.log(`[SURGICAL] hit start point, tid=${tid}`);
      this.tid = tid;
      this.handle = startStalker(tid, `surgical-${CFG.surgicalStartOff.toString(16)}`);
    },
    onLeave(retval) {
      if (CFG.surgicalStopAtReturn) {
        console.log(`[SURGICAL] returning from target, retval=${retval}`);
        stopStalker(this.tid, this.handle);
      }
    }
  });
}

function installDeferredSurgicalHook() {
  const entry = gJniOnLoad;
  let done = false;
  Interceptor.attach(entry, {
    onEnter(args) {
      if (done) return;
      const tid = Process.getCurrentThreadId();
      gJniThreadId = tid;
      console.log(`[SURGICAL-DEFER] JNI_OnLoad entered, tid=${tid}`);
    },
    onLeave(retval) {
      if (done) return;
      const start = gPayloadBase.add(CFG.surgicalStartOff);
      if (!looksLikeCode(start)) {
        console.log(`[SURGICAL-DEFER] target still encrypted at ${fmtAddr(start)}, giving up`);
        done = true;
        return;
      }
      try {
        Interceptor.attach(start, {
          onEnter(args) {
            const tid = Process.getCurrentThreadId();
            console.log(`[SURGICAL] hit start point, tid=${tid}`);
            this.tid = tid;
            this.handle = startStalker(tid, `surgical-${CFG.surgicalStartOff.toString(16)}`);
          },
          onLeave(retval) {
            if (CFG.surgicalStopAtReturn) {
              console.log(`[SURGICAL] returning from target, retval=${retval}`);
              stopStalker(this.tid, this.handle);
            }
          }
        });
        console.log(`[SURGICAL] attached at ${fmtAddr(start)}`);
      } catch (e) {
        console.log('[SURGICAL-DEFER] failed to attach:', e);
      }
      done = true;
    }
  });
}

function hook_dlopen() {
  const linker = Process.findModuleByName('linker64') || Process.findModuleByName('linker');
  if (!linker) {
    console.log('[-] linker not found');
    return;
  }

  let dlopen = linker.findExportByName('__dl___loader_android_dlopen_ext') ||
               linker.findExportByName('__dl__android_dlopen_ext') ||
               linker.findExportByName('android_dlopen_ext');
  if (!dlopen || dlopen.isNull()) {
    const syms = linker.enumerateSymbols();
    for (let i = 0; i < syms.length; i++) {
      const n = syms[i].name;
      if (n.indexOf('android_dlopen_ext') !== -1 ||
          n.indexOf('__dl___loader_android_dlopen_ext') !== -1) {
        dlopen = syms[i].address;
        console.log(`[*] found dlopen via symbols: ${n}`);
        break;
      }
    }
  }
  if (!dlopen || dlopen.isNull()) {
    console.log('[-] android_dlopen_ext not found');
    return;
  }

  Interceptor.attach(dlopen, {
    onEnter(args) {
      const name = args[0] ? args[0].readCString() : null;
      this.isTarget = name && name.indexOf(CFG.soName) !== -1;
      if (this.isTarget) {
        console.log(`\n[★ TARGET] android_dlopen_ext: ${name}`);
      }
    },
    onLeave(retval) {
      if (!this.isTarget) return;

      gStubModule = Process.findModuleByName(CFG.soName);
      if (!gStubModule) {
        console.log('[-] libDexHelper.so not found after dlopen');
        return;
      }
      console.log(`[+] stub base = ${gStubModule.base}  size = ${gStubModule.size}`);

      gJniOnLoad = findJniOnLoad();
      if (!gJniOnLoad || gJniOnLoad.isNull()) {
        console.log('[-] JNI_OnLoad not found');
        return;
      }

      gPayloadBase = gJniOnLoad.sub(0x13a8c);
      console.log(`[+] JNI_OnLoad = ${gJniOnLoad}`);
      console.log(`[+] payloadBase = ${gPayloadBase} (size ~${CFG.payloadSize.toString(16)})`);

      // 一致性验证:crashFn 等偏移应落在 payload 范围
      const start = gPayloadBase.add(CFG.surgicalStartOff);
      console.log(`[+] surgical start = ${fmtAddr(start)}`);

      applyStalkerOptions();
      installExceptionHandler();
      installSurgicalHook();
      
    }
  });
}

setImmediate(hook_dlopen);
console.log('[*] jni_trace_v2.js loaded, mode=' + CFG.mode);

surgical 模式启动 Stalker遇到崩溃,看崩溃信息应该是Frida自己崩溃的。

["call","0x77fd1f5acc","0x77fd1f9174",243]
["ret","0x7ac2d81960","0x77fd1f5ad0",244]
["call","0x77fd1f5b1c","0x77fd1f9174",243]
["ret","0x7ac2d81960","0x77fd1f5b20",244]
["call","0x77fd1f5b58","0x77fd1f9174",243]
["ret","0x7ac2d81960","0x77fd1f5b5c",244]
["call","0x77fd1f5ba4","0x77fd1f5564",243]
["call","0x77fd1f55a8","0x77fd1f9174",244]
[SURGICAL] returning from target, retval=0x0
Process crashed: Bad access due to protection failure

***
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/pudding/pudding:16/BP2A.250605.031.A3/OS3.0.315.0.WPCCNXM:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2026-07-23 11:18:25.364232348+0800
Process uptime: 56s
Cmdline: cn.samsclub.app
pid: 15068, tid: 15068, name: cn.samsclub.app  >>> cn.samsclub.app <<<
uid: 10342
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x00000077fce7d000
    x0  00000077fcc7c010  x1  00000077fce7cfd0  x2  00000000ffdfeff0  x3  00000077fcc7cf90
    x4  00000078fcc7c010  x5  00000078fca7c010  x6  0000000000000000  x7  000000000000000b
    x8  0000000000000000  x9  0000000000000000  x10 0000000000000000  x11 0000000000000000
    x12 0000000000000000  x13 0000000000000000  x14 0000000000000000  x15 0000007ac40c0000
    x16 0000000000000030  x17 0000007ac2d84f00  x18 0000007ae0428000  x19 0000007ac098d520
    x20 0000000000010000  x21 0000000000000000  x22 0000000000200000  x23 ffffffffffffffff
    x24 00000077989fe010  x25 0000000000010000  x26 0000000000000000  x27 0000007807e87db0
    x28 0000007ac40c0000  x29 0000007fe8b2a1d0
    lr  000000778f06a6ec  sp  0000007fe8b2a1d0  pc  0000007ac2d85198  pst 0000000020001000
15 total frames
backtrace:
      #00 pc 0000000000099198  /apex/com.android.runtime/lib64/bionic/libc.so (__memcpy_aarch64_nt+664) (BuildId: 223a572c9f034c6a9337d8b3bedc75f3)
      #01 pc 0000000000afd6e8  /memfd:frida-agent-64.so (deleted)
// ...
      #13 pc 00000000009acabc  /memfd:frida-agent-64.so (deleted)
      #14 pc 000000000004f230  <anonymous:777b600000>
***

Thank you for using Frida!

柳暗花明

通过 SURGICAL 的调试信息可以知道,我们知道 0x33d10 这个入口函数已经执行完了,surgical hook 本身完成了使命。那我们再跳过 0x33d10 这个入口 ,从0x1b940(post-33D10)启动 Stalker,并且开启打印汇编的Trace。

遇到崩溃信息如下,但是我们看这个BR x12,跳到了0x97C,这个是不是和0x6aC挺像的!

[EXEC] libDexHelper.sopayload + 0x120c8 mov x0, #0
[EXEC] libDexHelper.sopayload + 0x120cc mov sp, x0
[EXEC] libDexHelper.sopayload + 0x120d0 mov x30, x0
[EXEC] libDexHelper.sopayload + 0x120d4 mov x8, x19
[EXEC] libDexHelper.sopayload + 0x120d8 mov x9, x12
[EXEC] libDexHelper.sopayload + 0x120dc br x12
Process crashed: Bad access due to invalid address

***
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/pudding/pudding:16/BP2A.250605.031.A3/OS3.0.315.0.WPCCNXM:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2026-07-23 11:45:57.350911510+0800
Process uptime: 27s
Cmdline: cn.samsclub.app
pid: 21279, tid: 21279, name: cn.samsclub.app  >>> cn.samsclub.app <<<
uid: 10342
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xffffffffffffff70
    x0  0000000000000000  x1  00000000b6a2897f  x2  0000000000000fff  x3  0000007fe8b2b0f8
    x4  00000000000010b0  x5  0000007fe8b2b2bc  x6  000000006174732f  x7  2f6b7361742f666c
    x8  000000000000001f  x9  0000000000294e10  x10 00000000b6a2897f  x11 000000000000101e
    x12 000000000000097c  x13 2f6b7361742f666c  x14 0000000000000067  x15 0000000000000061
    x16 0000000000000001  x17 0000007ac2d45570  x18 0000007ae0428000  x19 00000000b6a2897f
    x20 00000077fd26b222  x21 0000007adfcec780  x22 0000007fe8b2b1a0  x23 00000077fd28be19
    x24 00000077fd26b221  x25 0000007fe8b2b1a0  x26 0000007fe8b2b449  x27 0000007adfcec780
    x28 0000000000000000  x29 0000007fe8b2b140
    lr  00000077fd1ccdd8  sp  0000000000000000  pc  00000077986358c8  pst 0000000000001000
38 total frames
backtrace:
      #00 pc 00000000000358c8  <anonymous:7798600000>
// ...
      #10 pc 000000000012a56c  /system/framework/arm64/boot.oat (java.lang.System.loadLibrary+92) (BuildId: 0b9bde974037d8e8ccf8cfa4291ada7f8949bc24)

Thank you for using Frida!

对应的函数是sub_11C64,我们看看这个BR X12的X12这个值是怎么来的,可以看到是W1&W2&0xFFFFFFFC得到的,那就好说了。

.text:0000000000011C64 ; __unwind {
.text:0000000000011C64                 SUB             SP, SP, #0x180
.text:0000000000011C68                 STR             X28, [SP,#0x170+var_30]
.text:0000000000011C6C                 STP             X22, X21, [SP,#0x170+var_20]
.text:0000000000011C70                 STP             X20, X19, [SP,#0x170+var_10]
.text:0000000000011C74                 STP             X29, X30, [SP,#0x170+var_s0]
.text:0000000000011C78                 ADD             X29, SP, #0x170
.text:0000000000011C7C                 MRS             X21, TPIDR_EL0
.text:0000000000011C80                 LDR             X8, [X21,#0x28]
.text:0000000000011C84                 MOV             W19, W1

.text:00000000000120B4
.text:00000000000120B4 loc_120B4                               ; CODE XREF: sub_11C64+3C↑j
.text:00000000000120B4                 MOV             W10, W19
.text:00000000000120B8                 AND             W8, W2, W19
.text:00000000000120BC                 AND             X12, X8, #0xFFFFFFFC
.text:00000000000120C0                 AND             X8, X10, #0x1F
.text:00000000000120C4                 ADDS            X11, X8, X2
.text:00000000000120C8                 MOV             X0, #0
.text:00000000000120CC                 MOV             SP, X0
.text:00000000000120D0                 MOV             X30, X0
.text:00000000000120D4                 MOV             X8, X19
.text:00000000000120D8                 MOV             X9, X12
.text:00000000000120DC                 BR              X12

把所有调用sub_11C64的地方列出来,并且算出对应的第一个参数AND第二个参数XOR0xFFFFFFFC后的值

import idaapi
import ida_funcs
import ida_xref
import idautils
import idc

TARGET = "sub_11C64"
func = idaapi.get_name_ea(idaapi.BADADDR, TARGET)
if func == idaapi.BADADDR:
    print("cannot find", TARGET)
    raise Exception()

MASK = 0xFFFFFFFF
def parse_imm(op):
    op = op.replace("#", "")
    if op.startswith("0x") or op.startswith("-0x"):
        return int(op, 16)
    return int(op)

def trace_args(call_ea):
    """
    回溯 x1/x2 常量
    """
    regs = {
        "x1": None,
        "w1": None,
        "x2": None,
        "w2": None,
    }
    ea = idc.prev_head(call_ea)
    limit = 80
    while ea != idc.BADADDR and limit > 0:
        limit -= 1
        mnem = idc.print_insn_mnem(ea)
        dst = idc.print_operand(ea, 0)
        src = idc.print_operand(ea, 1)

        if mnem == "MOV":
            if dst in ("X1", "W1") and regs["x1"] is None:
                if src.startswith("#"):
                    regs["x1"] = parse_imm(src)
                elif src in ("W1","X1"):
                    pass

            if dst in ("X2", "W2") and regs["x2"] is None:
                if src.startswith("#"):
                    regs["x2"] = parse_imm(src)

        elif mnem == "MOVZ":
            shift = 0
            if idc.print_operand(ea,2):
                s = idc.print_operand(ea,2)
                if "LSL" in s.upper():
                    shift = parse_imm(s.split()[-1])

            if dst in ("X1","W1") and regs["x1"] is None:
                regs["x1"] = parse_imm(src) << shift

            if dst in ("X2","W2") and regs["x2"] is None:
                regs["x2"] = parse_imm(src) << shift

        elif mnem == "MOVK":
            shift = 0
            if idc.print_operand(ea,2):
                s = idc.print_operand(ea,2)
                if "LSL" in s.upper():
                    shift = parse_imm(s.split()[-1])

            if dst in ("X1","W1") and regs["x1"] is not None:
                imm = parse_imm(src)
                mask = 0xffff << shift
                regs["x1"] &= ~mask
                regs["x1"] |= imm << shift

            if dst in ("X2","W2") and regs["x2"] is not None:
                imm = parse_imm(src)
                mask = 0xffff << shift
                regs["x2"] &= ~mask
                regs["x2"] |= imm << shift

        elif mnem == "ORR":
            op2 = idc.print_operand(ea,1)
            op3 = idc.print_operand(ea,2)

            if op2.upper() in ("XZR","WZR") and op3.startswith("#"):

                if dst in ("X1","W1") and regs["x1"] is None:
                    regs["x1"] = parse_imm(op3)

                if dst in ("X2","W2") and regs["x2"] is None:
                    regs["x2"] = parse_imm(op3)

        if regs["x1"] is not None and regs["x2"] is not None:
            break
        ea = idc.prev_head(ea)
    return regs["x1"], regs["x2"]
print("="*80)

for xref in idautils.XrefsTo(func):
    if xref.type != ida_xref.fl_CN:
        continue
    call = xref.frm
    arg2, arg3 = trace_args(call)
    if arg2 is None:
        arg2_xor = "unknown"
    else:
        arg2_xor = hex((arg2 & arg3) & 0xFFFFFFFC)
    print(
        "CALL {:08X}  arg2={}  arg3={}  res={}".format(
            call,
            "unknown" if arg2 is None else hex(arg2),
            "unknown" if arg3 is None else hex(arg3),
            arg2_xor
        )
    )

通过上面的脚本我们可以得到下面的反调试函数和崩溃信息对照,不过现在来说作用不大了,因为我们可以直接Patch崩溃函数。

调用点res
0x000158D40x8
0x000159EC0x8
0x00015D8C0x8
0x00015E980x8
0x00015F180x8
0x0001748C0x79C
0x0001E80C0xB4C
0x0001F880unknown
0x000246CC0x0
0x0002E1B8unknown
0x0002E33C0x79C
0x0003DDD40x97C
0x0003E748unknown
0x000417640x1F4
0x000420B80x88C
0x000424D00x4CC
0x000427080x3D8
0x0004462C0x0
0x000462200x1F8
0x000494700x1F8
0x0004A118unknown
0x0004B8980x4CC
0x0004C1200x3D8
0x0004CE780x6AC
0x0004E1480x6AC
0x0004F920unknown
0x0004F9740xC3C
0x000501F00xD2C
0x000504400xF1C
0x000519B40xF1C
0x00051E240x2EC
0x00052F140x5BC
0x00054C740xE1C
0x000552F80xF1C
0x000554E80xF1C
0x0005A2400x10
0x0005A61Cunknown
0x0005B4CC0x4
0x0005B58C0x14

那干脆不Trace了?直接把Sub11C64() 函数Patch了看看能不能过?

frida -U -f cn.samsclub.app -l patch_sub_11c64.js
     ____
    / _  |   Frida 17.15.4 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   Prefer a GUI? Luma is the official Frida app, with a live REPL,
   . . . .   persistent sessions & collaboration. 19bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9N6h3#2S2i4K6u0W2k6Y4u0A6k6r3q4Q4x3X3g2J5k6g2)9J5c8R3`.`.
   . . . .
   . . . .   Connected to 25113PN0EC (id=75040c8d)
Spawning `cn.samsclub.app`...                                           
[*] patch_sub_11c64.js loaded (patch-only, no trace)
Spawned `cn.samsclub.app`. Resuming main thread!                        
[25113PN0EC::cn.samsclub.app ]->
[★ TARGET DETECTED] android_dlopen_ext: /data/app/~~ixWf-uuOzn9c_gPbpVp35w==/cn.samsclub.app-6Q0Gn4oqvUfj1yT-4Z3tNQ==/lib/arm64/libDexHelper.so
[+] stub base = 0x77fcc35000 size = 1347584
[+] found JNI_OnLoad via enumerateExports: 0x77fcb24a8c
[+] payloadBase = 0x77fcb11000
[+] sub_11C64   = libDexHelper.sopayload + 0x11c64
[+] patched sub_11C64 @ libDexHelper.sopayload + 0x11c64 → return immediately (void)
Process terminated
[25113PN0EC::cn.samsclub.app ]->

Thank you for using Frida!

事情果然没这么简单,现在连崩溃的调试信息都没了。继续使用surgical 模式往后Trace。

-> libDexHelper.so!0x7419c\n[CALL] [D:65] libDexHelper.so!0x74234 -> libDexHelper.so!0x77990\n[CALL] [D:66] libDexHelper.so!0x779f8 -> libDexHelper.so!0x76e24\n[RET] [D:67] libDexHelper.so!0x76e58 -> libDexHelper.so!0x779fc\n[CALL] [D:66] libDexHelper.so!0x77a3c -> libDexHelper.so!0x775d4\n[CALL] [D:67] libDexHelper.so!0x77688 -> libDexHelper.so!0x777f4\n[CALL] [D:68] libDexHelper.so!0x7787c -> libDexHelper.so!0x6be28\n[CALL] [D:69] libDexHelper.so!0x6bbbc -> libDexHelper.so!0xe660\n[RET] [D:70] libc.so!0x9931c -> libDexHelper.so!0x6bbc0\n[RET] [D:69] libDexHelper.so!0x6be04 -> libDexHelper.so!0x77880\n[CALL] [D:68] libDexHelper.so!0x778b0 -> libDexHelper.so!0x7736c\n[RET] [D:69] libDexHelper.so!0x775d0 -> libDexHelper.so!0x778b4\n[RET] [D:68] libDexHelper.so!0x77988 -> libDexHelper.so!0x7768c\n[CALL] [D:67] libDexHelper.so!0x776f4 -> libDexHelper.so!0x777f4\n[CALL] [D:68] libDexHelper.so!0x7787c -> libDexHelper.so!0x6be28\n[CALL] [D:69] libDexHelper.so!0x6bbbc -> libDexHelper.so!0xe660\n[RET] [D:70] libc.so!0x9931c -> libDexHelper.so!0x6bbc0\n[RET] [D:69] libDexHelper.so!0x6be04 -> libDexHelper.so!0x77880\n[CALL] [D:68] libDexHelper.so!0x778b0 -> libDexHelper.so!0x7736c\n[RET] [D:69] libDexHelper.so!0x775d0 -> libDexHelper.so!0x778b4\n[RET] [D:68] libDexHelper.so!0x77988 -> libDexHelper.so!0x776f8\n[CALL] [D:67] libDexHelper.so!0x77760 -> libDexHelper.so!0x777f4\n[RET] [D:68] libDexHelper.so!0x77988 -> libDexHelper.so!0x77764\n[RET] [D:67] libDexHelper.so!0x777dc -> libDexHelper.so!0x77a40'}} data: None
Process terminated

Thank you for using Frida!

最后崩在这里,但是这个崩溃现场看起来又是Frida搞崩的。

Dump Dex&&Bypass anti-frida

但是这个崩溃的地方看起来有点怪啊,跟进去看看执行了什么代码。

if ( *(_QWORD *)&filename[8] != *(_QWORD *)filename )
                    {
                      v1045 = 0;
                      v1046 = v1037 + 8LL * (int)v1034;
                      do
                      {
                        v1047 = *(_QWORD *)(qword_104190 + 8 * v1045);
                        v1048 = *(_QWORD *)(v1047 + 8);
                        LODWORD(v1047) = *(_DWORD *)(v1047 + 24);
                        s[1].n128_u64[0] = 0;
                        s[0].n128_u64[0] = v1048;
                        s[0].n128_u32[2] = v1047;
                        sub_F258((__int64)s);
                        *(_QWORD *)(v1046 + 8 * v1045++) = s[1].n128_u64[0];
                      }
                      while ( v1045 < (__int64)(*(_QWORD *)&filename[8] - *(_QWORD *)filename) >> 3 );
                    }

调用这个,我们再往下看。

bool __fastcall sub_F258(_QWORD *a1)
{
  int v2; // w2
  int v5; // w0
  __int128 v6; // [xsp+0h] [xbp-30h] BYREF
  const char *v7; // [xsp+10h] [xbp-20h]
  __int64 v8; // [xsp+18h] [xbp-18h]

  v8 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
  if ( *(_DWORD *)qword_1063B0 <= *(_DWORD *)(qword_1063B0 + 20) )
    v2 = *(_DWORD *)(qword_1063B0 + 20);
  else
    v2 = *(_DWORD *)qword_1063B0;
  if ( (v2 | 1) == 0x1B )
  {
    return sub_F33C(a1);
  }
  else
  {
    if ( (unsigned int)(v2 - 28) <= 2 )
      return sub_F490(a1);
    v6 = *(_OWORD *)off_F7E40;
    v7 = "_ZNK3art16ArtDexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10O"
         "atDexFileEbbPS9_";
    sub_F5F4((__int64)&v6, 3, v2); // 
    if ( v5 == 2 )
      return sub_F964(a1);
    if ( v5 == 1 )
      return sub_F798(a1);
    return !v5 && sub_F490(a1); // DUMP DEX
  }
}

下面是加载DEX文件的核心逻辑。

bool __fastcall sub_F490(_QWORD *a1)
{
  __int64 v2; // x3
  __int64 v3; // x4
  __int64 v4; // x5
  __int64 v5; // x6
  __int64 v6; // x7
  void *v7; // x20
  __int64 v8; // x21
  __int64 v9; // x22
  __int64 v10; // x5
  __int64 v11; // x8
  _BOOL4 v12; // w19
  _QWORD v14[4]; // [xsp+28h] [xbp-C8h] BYREF
  _QWORD v15[2]; // [xsp+48h] [xbp-A8h] BYREF
  void *ptr; // [xsp+58h] [xbp-98h]
  unsigned __int64 v17[3]; // [xsp+60h] [xbp-90h] BYREF
  _BYTE v18[64]; // [xsp+78h] [xbp-78h] BYREF
  __int64 v19; // [xsp+B8h] [xbp-38h]

  v19 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
  v7 = dlsym(
         0,
         "_ZN3art13DexFileLoader10OpenCommonEPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjP"
         "KNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE");
  if ( v7
    || (v7 = sub_25B54(
               *(char **)(qword_1063B0 + 40),
               "_ZN3art13DexFileLoader10OpenCommonEPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIc"
               "EEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE",
               1,
               v2,
               v3,
               v4,
               v5,
               v6)) != 0 )
  {
    v8 = *a1;
    v9 = *(unsigned int *)(*a1 + 32LL);
    sub_ED50(v17, "AnoymousDex");
    v10 = *(unsigned int *)(v8 + 8);
    v15[1] = 0;
    ptr = 0;
    v15[0] = 0;
    ((void (__fastcall *)(_QWORD *__return_ptr, __int64, __int64, _QWORD, _QWORD, unsigned __int64 *, __int64, _QWORD, _QWORD, _BYTE, _QWORD *, _BYTE *, _QWORD))v7)(
      v14,
      v8,
      v9,
      0,
      0,
      v17,
      v10,
      0,
      0,
      0,
      v15,
      v18,
      0);
    v11 = v14[0];
    a1[2] = v14[0];
    v12 = v11 != 0;
    if ( (v15[0] & 1) != 0 )
      j__free_0(ptr);
    if ( (v17[0] & 1) != 0 )
      j__free_0((void *)v17[2]);
  }
  else
  {
    return 0;
  }
  return v12;
}

,可以看到它使用OpenCommon来加载DEX文件,而我们之前又过掉反调试了,那我们可以轻松写出DUMP脚本。

'use strict';
// ============================================================
// libDexHelper.so DEX Dumper
//
// 只做四件事:
//   1. dlopen 返回时 hook
//   2. patch sub_11C64 为空函数 (阻断 0x120cc 自杀序列 MOV SP,X0/BR X12)
//   3. hook sub_F490 入口 dump dex
//   4. hook JNI_OnLoad 打 ENTER/LEAVE 日志
// ============================================================
const CFG = {
  soName: 'libDexHelper.so',
  jniOnLoadOff: 0x13a8c,
  sub11C64Off: 0x11c64,
  subF490Off: 0xf490,
  payloadSize: 0x149000,
  dumpDir: '/data/data/cn.samsclub.app/',
};

let gStubModule = null;
let gPayloadBase = null;
let gJniOnLoad = null;
let gSub11C64Callback = null;   // 持有 NativeCallback 引用防 GC
let gDumpCount = 0;
function formatAddr(addr) {
  addr = ptr(addr);
  if (gPayloadBase &&
      addr.compare(gPayloadBase) >= 0 &&
      addr.compare(gPayloadBase.add(CFG.payloadSize)) < 0) {
    return CFG.soName + 'payload + 0x' + addr.sub(gPayloadBase).toString(16);
  }
  const m = Process.findModuleByAddress(addr);
  if (m) return m.name + ' + 0x' + addr.sub(m.base).toString(16);
  return addr.toString();
}
function looksLikeCode(a) {
  try {
    const insn = Instruction.parse(ptr(a));
    if (!insn) return false;
    const ok = ['stp', 'sub', 'mov', 'add', 'adrp', 'ldr', 'str', 'b', 'bl', 'cbz', 'cmp', 'ret'];
    return ok.indexOf(insn.mnemonic) !== -1;
  } catch (e) {
    return false;
  }
}
function ensureDumpDir() {
  const libc = Process.findModuleByName('libc.so');
  if (!libc) return false;
  let mkdirAddr = libc.findExportByName('mkdir') || libc.findExportByName('__mkdir');
  if (!mkdirAddr || mkdirAddr.isNull()) return false;
  const mkdir = new NativeFunction(mkdirAddr, 'int', ['pointer', 'int']);
  const rc = mkdir(Memory.allocUtf8String(CFG.dumpDir), 0o777);
  console.log('[*] mkdir rc=' + rc + ' (EEXIST 也返回 -1, 忽略)');
  return true;
}
function readDexSignature(dexBase) {
  try {
    const sigBytes = dexBase.add(12).readByteArray(20);
    if (!sigBytes) return 'null';
    const u8 = new Uint8Array(sigBytes);
    let hex = '';
    for (let i = 0; i < u8.length; i++) hex += u8[i].toString(16).padStart(2, '0');
    return hex;
  } catch (e) {
    return 'read_fail';
  }
}

function readDexMagic(dexBase) {
  try {
    const magic = dexBase.readByteArray(8);
    if (!magic) return 'null';
    const u8 = new Uint8Array(magic);
    let s = '';
    for (let i = 0; i < u8.length; i++) {
      const c = u8[i];
      s += (c >= 0x20 && c <= 0x7e) ? String.fromCharCode(c) : '.';
    }
    return s;
  } catch (e) {
    return 'read_fail';
  }
}
function findJniOnLoadManually() {
  const mod = Process.findModuleByName(CFG.soName);
  if (!mod) return null;
  const base = mod.base;
  const symtab = base.add(0x145000);
  const strtab = base.add(0x147000);
  const nchain = 221;
  for (let i = 0; i < nchain; i++) {
    const entry = symtab.add(i * 0x18);
    const st_name = entry.readU32();
    const name = strtab.add(st_name).readCString();
    if (name === 'JNI_OnLoad') {
      const addr = entry.add(8).readPointer();
      console.log('[*] manual scan: JNI_OnLoad @ ' + addr);
      return addr;
    }
  }
  return null;
}
function findJniOnLoad() {
  const mod = Process.findModuleByName(CFG.soName);
  if (!mod) {
    console.log('[-] libDexHelper.so not found in process');
    return null;
  }
  try {
    const exports = mod.enumerateExports();
    for (let i = 0; i < exports.length; i++) {
      if (exports[i].name === 'JNI_OnLoad') {
        console.log('[+] found JNI_OnLoad via enumerateExports: ' + exports[i].address);
        return exports[i].address;
      }
    }
  } catch (e) {
    console.log('[!] enumerateExports failed: ' + e);
  }
  console.log('[!] fallback to manual dynsym scan');
  return findJniOnLoadManually();
}
// Patch sub_11C64: Interceptor.replace 成 void(void) 空函数
function patchSub11C64() {
  const target = gPayloadBase.add(CFG.sub11C64Off);
  if (!looksLikeCode(target)) {
    console.log('[!] sub_11C64 @ ' + formatAddr(target) + ' not code yet, payload still encrypted?');
    return false;
  }

  gSub11C64Callback = new NativeCallback(function () {
  }, 'void', []);

  try {
    Interceptor.replace(target, gSub11C64Callback);
    console.log('[+] patched sub_11C64 @ ' + formatAddr(target) + ' -> return immediately (void)');
    return true;
  } catch (e) {
    console.log('[!] Interceptor.replace sub_11C64 failed: ' + e);
    return false;
  }
}
// Hook sub_F490 入口 dump dex
function hookAndDumpSubF490() {
  const target = gPayloadBase.add(CFG.subF490Off);
  if (!looksLikeCode(target)) {
    console.log('[!] sub_F490 @ ' + formatAddr(target) + ' not code yet, payload still encrypted?');
    return false;
  }

  Interceptor.attach(target, {
    onEnter(args) {
      this.a1 = args[0];
      const tid = Process.getCurrentThreadId();
      gDumpCount++;

      let dexBase, dexSize;
      try {
        dexBase = this.a1.readPointer();
      } catch (e) {
        console.log('[F490 #' + gDumpCount + '] tid=' + tid + ' *a1 read failed: ' + e);
        return;
      }
      if (!dexBase || dexBase.isNull()) {
        console.log('[F490 #' + gDumpCount + '] tid=' + tid + ' *a1 = null');
        return;
      }
      const magic = readDexMagic(dexBase);
      try {
        dexSize = dexBase.add(32).readU32();
      } catch (e) {
        console.log('[F490 #' + gDumpCount + '] tid=' + tid + ' size read failed: ' + e);
        return;
      }
      const sig = readDexSignature(dexBase);
      const sigShort = sig.substring(0, 8);
      console.log('\n[F490] #' + gDumpCount + ' tid=' + tid);
      console.log('  *a1 (dexBase) = ' + dexBase);
      if (magic.indexOf('dex') !== 0 || dexSize < 0x70 || dexSize > 0x10000000) {
        console.log('  [!] sanity check failed, skip dump');
        return;
      }
      let dexBytes;
      try {
        dexBytes = dexBase.readByteArray(dexSize);
      } catch (e) {
        console.log('  [!] readByteArray failed: ' + e);
        return;
      }
      if (!dexBytes) {
        console.log('  [!] readByteArray returned null');
        return;
      }
      const fileName = CFG.dumpDir + '/dex_' + String(gDumpCount).padStart(2, '0') + '_' + sigShort + '_' + dexSize + '.dex';
      try {
        const f = new File(fileName, 'wb');
        f.write(dexBytes);
        f.flush();
        f.close();
        console.log('  [+] DUMPED -> ' + fileName + ' (' + dexBytes.byteLength + ' bytes)');
      } catch (e) {
        console.log('  [!] write file failed: ' + e);
      }
    },
    onLeave(retval) {
    }
  });
  console.log('[+] hooked sub_F490 @ ' + formatAddr(target) + ' (dump mode)');
  return true;
}
// Hook JNI_OnLoad 本体 — ENTER/LEAVE 日志, 验证能否走完
function hookJniOnLoadLifecycle() {
  try {
    Interceptor.attach(gJniOnLoad, {
      onEnter(args) {
        console.log('\n[JNI_OnLoad] ENTER vm=' + args[0] + ' reserved=' + args[1]);
      },
      onLeave(retval) {
        console.log('[JNI_OnLoad] LEAVE retval=0x' + retval.toInt32().toString(16));
        console.log('[★] 最初版 (仅 patch sub_11C64) 就走完了整个 JNI_OnLoad');
      }
    });
    console.log('[+] hooked JNI_OnLoad lifecycle @ ' + gJniOnLoad);
    return true;
  } catch (e) {
    console.log('[!] hook JNI_OnLoad lifecycle failed: ' + e);
    return false;
  }
}
// Exception handler — 只打日志不改变行为, 死了能看到真实崩溃 PC
function installExceptionHandler() {
  Process.setExceptionHandler(details => {
    const ctx = details.context;
    console.log('\n[!!!] native exception captured:');
    console.log('  type        = ' + details.type);
    console.log('  pc          = ' + formatAddr(ctx.pc));
    console.log('  lr          = ' + (ctx.x30 ? formatAddr(ctx.x30) : 'null'));
    console.log('  sp          = ' + ctx.sp);
    if (details.memory) {
      console.log('  memory      = ' + details.memory.operation + ' @ ' + details.memory.address);
    }
    console.log('  backtrace:');
    try {
      const bt = Thread.backtrace(ctx, Backtracer.ACCURATE);
      for (let i = 0; i < Math.min(bt.length, 15); i++) {
        console.log('    #' + i + ' ' + formatAddr(bt[i]));
      }
    } catch (e) {
      console.log('    backtrace failed: ' + e);
    }
    return false; // 不接管, 让系统继续默认处理 (死了就是真死了)
  });
  console.log('[+] exception handler installed (log-only)');
}
// dlopen 卡点 — 初始化入口
function hook_dlopen() {
  const linker = Process.findModuleByName('linker64') || Process.findModuleByName('linker');
  if (!linker) {
    console.log('[-] linker not found');
    return;
  }

  let dlopen = linker.findExportByName('__dl___loader_android_dlopen_ext') ||
               linker.findExportByName('__dl__android_dlopen_ext') ||
               linker.findExportByName('android_dlopen_ext');
  if (!dlopen || dlopen.isNull()) {
    const syms = linker.enumerateSymbols();
    for (let i = 0; i < syms.length; i++) {
      const n = syms[i].name;
      if (n.indexOf('android_dlopen_ext') !== -1 ||
          n.indexOf('__dl___loader_android_dlopen_ext') !== -1) {
        dlopen = syms[i].address;
        console.log('[*] found dlopen via symbols: ' + n);
        break;
      }
    }
  }
  if (!dlopen || dlopen.isNull()) {
    console.log('[-] android_dlopen_ext not found');
    return;
  }

  Interceptor.attach(dlopen, {
    onEnter(args) {
      const name = args[0] ? args[0].readCString() : null;
      this.isTarget = name && name.indexOf(CFG.soName) !== -1;
      if (this.isTarget) {
        console.log('\n[★ TARGET] android_dlopen_ext: ' + name);
      }
    },
    onLeave(retval) {
      if (!this.isTarget) return;
      gStubModule = Process.findModuleByName(CFG.soName);
      if (!gStubModule) {
        console.log('[-] libDexHelper.so not found after dlopen');
        return;
      }
      console.log('[+] stub base = ' + gStubModule.base + '  size = ' + gStubModule.size);
      gJniOnLoad = findJniOnLoad();
      if (!gJniOnLoad || gJniOnLoad.isNull()) {
        console.log('[-] JNI_OnLoad not found');
        return;
      }
      gPayloadBase = gJniOnLoad.sub(CFG.jniOnLoadOff);
      console.log('[+] JNI_OnLoad   = ' + gJniOnLoad);
      console.log('[+] payloadBase  = ' + gPayloadBase);
      console.log('[+] sub_11C64    = ' + formatAddr(gPayloadBase.add(CFG.sub11C64Off)));
      console.log('[+] sub_F490     = ' + formatAddr(gPayloadBase.add(CFG.subF490Off)));
      ensureDumpDir();
      patchSub11C64();
      hookAndDumpSubF490();
      hookJniOnLoadLifecycle();
      installExceptionHandler();
      console.log('[*] minimal hooks installed (only sub_11C64 patched), waiting for JNI_OnLoad...');
      console.log('[*] dump dir = ' + CFG.dumpDir);
    }
  });
}
rpc.exports = {
  dumpCount: function () {
    return gDumpCount;
  },
  dumpDir: function () {
    return CFG.dumpDir;
  },
};
setImmediate(hook_dlopen);
console.log('[*] dump_dex_min.js loaded — minimal version (only patch sub_11C64) + JNI_OnLoad lifecycle log');

运行结果如下,成功DUMP出对应的DEX文件。

 ~/xiaomi/samsclub  frida -U -f cn.samsclub.app -l dump_dex.js
     ____
    / _  |   Frida 17.15.4 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   Prefer a GUI? Luma is the official Frida app, with a live REPL,
   . . . .   persistent sessions & collaboration. 7e7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9N6h3#2S2i4K6u0W2k6Y4u0A6k6r3q4Q4x3X3g2J5k6g2)9J5c8R3`.`.
   . . . .
   . . . .   Connected to 25113PN0EC (id=75040c8d)
Spawning `cn.samsclub.app`...                                           
// ...

[F490] #1 tid=22799
  *a1 (dexBase) = 0x7775b3c000
  [+] DUMPED -> /data/data/cn.samsclub.app//dex_01_d996413c_11227940.dex (11227940 bytes)

[F490] #2 tid=22799
  *a1 (dexBase) = 0x77765f2000
  [+] DUMPED -> /data/data/cn.samsclub.app//dex_02_c2b59b7b_13518272.dex (13518272 bytes)

// ...

[F490] #8 tid=22799
  *a1 (dexBase) = 0x7779d40000
  [+] DUMPED -> /data/data/cn.samsclub.app//dex_08_592d20a2_774456.dex (774456 bytes)
Process terminated
[JNI_OnLoad] LEAVE retval=0x10004
[★] JNI_OnLoad 完整执行完毕, 进程存活, 反调试已绕过

[!!!] native exception captured:
  type        = access-violation
  pc          = boot.oat!0xc8784
  lr          = null
  sp          = 0x6ea1f0cff0
  memory      = read @ 0x0
  backtrace:
    #0 boot-framework.oat!0x385b5c
    #1 boot-framework.oat!0x385b5c

// ...

=== [EXCEPTION] type=access-violation pc=boot.oat!0xc8784 ===
    lr=null sp=0x6ea1f0cff0
    memory=read @ 0x0
    
[!!!] native exception captured:
  type        = access-violation
  pc          = boot.oat!0xb0108
  lr          = null
  sp          = 0x6e11decb90
  memory      = read @ 0x0
  backtrace:
    #0 libart.so!0x6a8ea4

=== [EXCEPTION] type=access-violation pc=boot.oat!0xb0108 ===
    lr=null sp=0x6e11decb90
    memory=read @ 0x0
Process terminated

Thank you for using Frida!

可以看到成功走完了JNI onLoad,这说明:对走完 JNI_OnLoad"这个目标而言,patch sub_11C64 就够了;

但是依旧遇到了崩溃,这里我们判断不出来是native反调试还是java层自己的实现,因此,我们在trace 一下JNI看看?最后可以整理并总结出下面的绑定表。

Java ClassMethodJNI SignatureNative offset (payload)
com/secneo/apkwrapper/Hbs(Landroid/content/Context;I)Ljava/lang/Object;0x283ec
com/secneo/apkwrapper/His(I)Z0x28f24
com/secneo/apkwrapper/Hus(Landroid/content/Context;)V0x28f44
com/secneo/apkwrapper/Hgha(Ljava/lang/String;)I0x2a6dc
com/secneo/apkwrapper/Hghc(Ljava/lang/String;)J0x2a74c
com/secneo/apkwrapper/Hgah()[Ljava/lang/Object;0x2a7b4
com/secneo/apkwrapper/Hsha(Ljava/lang/String;I)V0x2ae4c
com/secneo/apkwrapper/Hhe(I)V0x2ae84
com/secneo/apkwrapper/Hgv()I0x2aec4
com/secneo/apkwrapper/AWhn(Landroid/content/Context;Landroid/app/Application;)V0x2fc18
com/secneo/apkwrapper/AWpn(Ljava/lang/String;)Ljava/lang/String;0x30cc0
com/secneo/apkwrapper/Hd(Ljava/lang/String;)Ljava/lang/String;0x31a38
com/secneo/apkwrapper/Hsn(Ljava/lang/String;)I0x3df48

新的问题

到目前为止,我们已经完成了梆梆加密壳的“反调试绕过 + DEX 解密”:

  • 找到正确的 hook 窗口:dlopen 返回后、JNI_OnLoad 被调用前。
  • 定位并 patch 了自杀函数 sub_11C64,让 JNI_OnLoad 能够完整返回。
  • sub_F490 处 dump 出全部 8 个 DEX,验证了壳的解密流程。

但仍有一个悬而未决的问题:JNI_OnLoad 走完后,应用并没有真正跑起来,而是陆续抛出 access-violation read @ 0x0 的 Java 层异常,最终 Process terminated

基于上面整理出的 RegisterNatives 绑定表,对 com/secneo/apkwrapper/H 和 com/secneo/apkwrapper/AW 里的 native 函数做系统性分析,本质上是为了回答一个问题:JNI_OnLoad 返回后,那些 access-violation read @ 0x0 到底是 Java 层正常启动失败的 NPE,还是壳在 native 层继续触发反调试自杀?

Java 层是怎么被 Trace 到的

JNI_OnLoad 走完后,壳已经通过 OpenCommon 把 8 个真实 DEX 加载进 ART。此时 ART 的 ClassLoader 里就能找到这些类,只是它们还没被链接/执行。要在 Frida 里 hook 它们,核心机制是:

Java.perform(function () {
    const cls = Java.use('cn.samsclub.app.system.SamsApplication');
    cls.attachBaseContext.implementation = function (base) {
        console.log('[JAVA] SamsApplication.attachBaseContext called');
        return this.attachBaseContext(base);
    };
});

Java.perform 会让 Frida 进入 JVM attach 状态,拿到 JNIEnv;Java.use 通过类名去查当前已加载的 jclass,然后反射/直接替换该 Java 方法在 ART 方法表中的入口(也就是常说的 ART Method Hook)。所以:只要类被加载进内存,不管它来自原始 classes.dex 还是壳解密后动态注入的 DEX,都可以 Java.use。

我们写一个java层 trace,在 Java.perform 里同时 hook 壳类 + 真实 Application 链路:

const chain = [
  ['com.secneo.apkwrapper.H',  ['is', 'bs', 'us', 'sn', 'd', ...]],
  ['com.secneo.apkwrapper.AW', ['attachBaseContext', 'onCreate', 'patchProvider']],
  ['com.secneo.apkwrapper.AP', ['instantiateClassLoader', 'instantiateApplication']],
  ['cn.samsclub.app.system.SamsApplication', ['attachBaseContext', 'onCreate']],
  ['cn.samsclub.app.system.SamsApplicationLike', ['onBaseContextAttached', 'onCreate']],
  ['cn.samsclub.app.system.b0', ['A', 'C', 'l', 'D', 'F', 'P', 'z', 'x', 's', 'O', 'E', 'v', 'n', 'i', 't', 'u', 'G']],
  ['com.tencent.rfix.loader.app.RFixApplication', ['attachBaseContext', 'onCreate']],
];

运行后关键日志如下:

[JAVA] AP.instantiateApplication.instantiateApplication(dalvik.system.PathClassLoader@..., com.secneo.apkwrapper.AW)
[JAVA] H.is.is(0)
   -> ret=false
   -> ret=cn.samsclub.app.system.SamsApplication@...
[JAVA] RFixApplication.attachBaseContext.attachBaseContext(android.app.ContextImpl@...)
[JAVA] b0.l.l(cn.samsclub.app.system.SamsApplication@..., cn.samsclub.app.system.SamsApplicationLike@...)
[JAVA] SamsApplicationLike.onBaseContextAttached.onBaseContextAttached(android.app.ContextImpl@...)
[JAVA] b0.A.A()
[JAVA] AW.patchProvider.patchProvider()
[JAVA] RFixApplication.onCreate.onCreate()
[JAVA] SamsApplicationLike.onCreate.onCreate()
[JAVA] b0.C.C()
[JAVA] b0.z.z()
   -> ret=undefined
[JAVA] b0.O.O()
[JAVA] b0.t.t()
   -> ret=undefined
[JAVA] b0.u.u()
   -> ret=undefined
[JAVA] b0.G.G()
   -> ret=undefined
[JAVA] b0.E.E()
[JAVA] b0.i.i()
   -> ret=undefined
Process terminated

这串日志说明:

  1. H.is(0) 返回 false,壳判断环境“正常”,于是 AP.instantiateApplication 在 Android 10+ 的 AppComponentFactory 路径下直接实例化真实 SamsApplication,而不是走旧版的 AW 代理。
  2. RFixApplication.attachBaseContext 被调用,证明壳已经把真实 Application 拉起来,并开始走腾讯 RFix 的初始化流程。
  3. 我们一直跟到了解密后的真实 DEX 内部:cn.samsclub.app.system.b0(也就是 SamsInitHelper)的方法按顺序执行。 所以之前的 access-violation read @ 0x0 不是死在壳的 JNI_OnLoad 阶段,而是死在真实 App 初始化阶段;更准确地说,那些早期 access-violation 是在 dump 脚本只有 sub_11C64 patch、缺少 Java hook 时出现的Frida/ART 交互异常,并不代表真正的崩溃点。

写在最后

总结:我们绕过了梆梆,但是这里头有其他检测,就写到这里吧。



[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 1小时前 被ldz666编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 12423
活跃值: (10368)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
2
文章很细致,其实最新版的检测主要还是子线程和子进程,要 hook 自定义 linker 加载的真实 so, 配合 trace +二分法定位就可以, 确实隐蔽性越来越强了
1小时前
0
游客
登录 | 注册 方可回帖
返回