首页
社区
课程
招聘
[原创]企业壳学习记录-IDA分析(动态调试后续补上)
发表于: 2025-10-12 14:06 1134

[原创]企业壳学习记录-IDA分析(动态调试后续补上)

2025-10-12 14:06
1134

一:apktool环境搭建

参考网络教程

0d3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4q4I4i4K6g2X3y4o6p5^5y4U0j5&6z5o6S2Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5J5y4K6f1^5z5e0V1H3x3l9`.`.

二:使用apktool进行解包

三:找到目标so文件

找到目标so

静态分析(1)sub_3C798()-SDK版本获取

(2)sub_3CCE8()-ART检测


反编译语义还原

// 近似重构(省略了栈保护尾部检查等样板)
bool isArtRuntime() {
    char buf[80] = {0};           // v0
    // v3 = __stack_chk_guard:AArch64 上的栈保护 canary,来自 TPIDR_EL0 线程局部存储偏移 0x28
    // v2 = 0; v1 = 0; (未使用/占位)

    // 优先读取老属性,如果取不到,或 SDK > 20(Android 5.0 时代之后),再读新属性
    if ( _system_property_get("persist.sys.dalvik.vm.lib", buf) == 0 || *sdk > 20 ) {
        _system_property_get("persist.sys.dalvik.vm.lib.2", buf);
    }

    // 检查字符串中是否包含 "art"
    return strstr(buf, "art") != NULL;
}

关键点说明:

  1. TPIDR_EL0 + 40(0x28)v3 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);AArch64 的栈溢出保护(stack canary) 读出操作。编译器把 canary 放在 TLS(线程本地存储)里,函数入口读出并在函数尾对比,异常则调用 __stack_chk_fail

  2. 判定 ART/Dalvik:strstr(v0, "art"):只要属性值里包含 “art” 子串(如 libart.so),即可判定 当前运行时是 ART;否则多半是 Dalvik(libdvm.so

    典型值:

    • Dalvik:libdvm.so
    • ART:libart.so / libartd.so(带 art
  3. **sdk 的含义:**这是外部全局/静态变量,保存了当前系统的 SDK_INT。比较阈值 20(Android 4.4W)用于决定优先读取哪个属性。21(Android 5.0)起系统默认 ART。


常见用途

环境探测 / 分支加载:根据是 Dalvik 还是 ART,决定后续加载的 符号名、偏移、hook 方式(如 libdvm.so vs libart.so),决定使用 不同的反调试、反注入 手段。

兼容性判断:某些行为(如字节码注入/解释器入口 hook)在 Dalvik 与 ART 上完全不同,先做环境分流可避免崩溃。

绕过


Frida hook

var f = Module.findExportByName(null, "__system_property_get") || Module.findExportByName(null, "_system_property_get");
Interceptor.attach(f, {
  onEnter(args) {
    this.key = Memory.readUtf8String(args[0]);
    this.buf = args[1];
  },
  onLeave(retval) {
    if (this.key && (this.key.indexOf("dalvik.vm.lib") >= 0)) {
      Memory.writeUtf8String(this.buf, "libart.so");  // 或 "libdvm.so"
      retval.replace( (1) ); // 非 0 表示成功
    }
  }
});

手工 patch:把 strstr(...,"art") 的比较结果逻辑翻转(如把 BNEBEQ),强制走另一分支。

  1. 留意栈保护

    由于有 stack canary,盲目改局部栈数据可能触发 __stack_chk_fail 崩溃。做 inline hook/patch 时注意 保持函数序言/结尾完整 或使用 thumb/aarch64 安全补丁

  2. SDK 版本来源

    sdk 的定义处(交叉引用),确认它是通过 Build.VERSION.SDK_INT 还是 __system_property_get("ro.build.version.sdk") 初始化,避免误判。


函数的核心作用是:通过读取 persist.sys.dalvik.vm.lib(.2) 属性并搜索 “art” 关键字,来判断当前运行时是 Dalvik 还是 ART(兼容 4.x→5.x 过渡),以便后续执行不同的逻辑;同时包含 AArch64 常规的栈 canary 读取用于栈溢出保护。

(3)sub_3C8C8()-补丁检测


void buding_jc() {
  // 栈 canary(AArch64):从 TPIDR_EL0+0x28 取 canary,用于函数尾部栈溢出校验,非业务逻辑
  v7 = *(_ReadStatusReg(TPIDR_EL0) + 40);

  // 垫变量(混淆用)
  v6 = 0; v5 = 0; memset(s, 0, sizeof(s));

  // 读取系统属性:Android 版本/代号
  _system_property_get("ro.build.version.release_or_codename", s);

  // v1 = 是否包含字符 'S'(ASCII 83)
  v1 = strchr(s, 83) != 0;  // 'S' —— Android 12 的 codename 是 "S"
  if (v1 || (v3 = strstr(s, "12")) != 0) {
    // 命中 Android 12("S"/"12")分支
    n1066837414 = -1370028921;  // 典型“状态常量/不透明谓词”值(opaque constant)
    while (*sdk_bb > 32) ;      // 若 SDK_INT > 32(Android 13+),死等(busy-wait)=> 挂死
  }

  // 记录 SDK,并进入一段“状态机循环”
  n32 = *sdk_bb;
  do {
    if (n32 == 32) {
      // 仅当 Android 12L/32 时才执行的块:
      v6 = 0; v5 = 0; memset(s, 0, sizeof(s));
      _system_property_get("ro.build.version.security_patch", s);
      strstr(s, "2022-02");     // 检索补丁级别是否为 2022-02(结果未直接使用)
    }
  } while (n1066837414 != 1066837414 && n1066837414 != 670287947);
}
  1. 版本判定

    命中 Android 12("S""12")→ 置状态常量 n1066837414 = -1370028921。同时SDK_INT > 32(Android 13+)就原地 busy-wait,使程序“卡住”。这是非常典型的 反环境/反版本 行为(对 13+ 直接卡死)。

  2. 只针对 SDK_INT == 32 的次级检查

    读取 ro.build.version.security_patch,搜索 "2022-02"。(某 CVE 在 2022-02 之前可利用)。

  3. do … while (n1066837414 != 1066837414 && n1066837414 != 670287947)

    这是扁平化状态机的“主循环退出条件”。1066837414670287947 是“好状态/出环状态”常量。

    n1066837414 只在 if(命中 12) 分支被写为 1370028921,没有后续赋值,自然会无限循环

这是典型 flattening:把原本的 if/else/return 改写成“状态变量 + 大 while 循环 + 若干 case 写状态”的形式。

结合上面 1/2/3 点:目的是

识别 Android 12/12L/13+;

在 13+ 直接卡死,阻断分析/运行;

在 12/12L 再按安全补丁级别细分(例如 2022-02 前/后走不同利用或规避路径);

通过“状态常量 + 循环”隐藏真实控制流


如果系统是 Android 12(S/12):
    如果 SDK > 32(即 13+):卡死(busy-wait)
    否则(12 / 12L):进入状态机,在特定补丁级别前可能走可利用路径
否则(非 12):进入状态机的其他 case(未在片段中给出),最终也许跳出或直接 return

混淆/不透明谓词(opaque predicates)识别

13700289211066837414670287947:典型的状态码/哈希常量,用于把 if/else 平铺为“写状态→循环判断状态→跳转到下一个块”。恢复办法:动态执行静态常量传播,把“状态写→下一次判断位置”连起来,还原为 if/else/switch。


  1. 绕过:改变系统属性返回值,把版本伪装成 11 或更低,避免进入“12/13 专属杀开关”。

Frida Hook _system_property_get

const getProp = Module.findExportByName(null, "__system_property_get")
             || Module.findExportByName(null, "_system_property_get");
Interceptor.attach(getProp, {
  onEnter(args){ this.k = Memory.readUtf8String(args[0]); this.buf = args[1]; },
  onLeave(ret){
    if (!this.k) return;
    if (this.k === "ro.build.version.release_or_codename") {
      // 伪装为 "11"
      Memory.writeUtf8String(this.buf, "11");
      ret.replace(2); // 非0表示成功
    }
    if (this.k === "ro.build.version.security_patch") {
      // 如需触发“安全”路径,可写入高补丁级别
      Memory.writeUtf8String(this.buf, "2025-08-01");
      ret.replace(2);
    }
  }
});

不改代码段、不碰 canary。

  1. 定点消除“挂死”条件

去掉 while (*sdk_bb > 32) ;:把比较条件改为永不成立或把整个循环改为 NOP / BR 跳过。或在进入 if 分支后立即把 sdk_bb 写成 32 或更小(Frida 内存写)。

  1. 强制退出“状态机循环”

    在进入 do { … } while(…) 前,n1066837414 写成 1066837414670287947,直接一次性满  足退出条件:

// 先定位 n1066837414 的栈/寄存器保存位置(IDA 看函数栈布局/寄存器分配)
// 然后在 loop 上方插一个 Interceptor/inline hook 把该局部写好
  1. 更干净的“整函数短路”

直接 Inline Hook buding_jc() 的入口,立即 return(如果它无返回值/副作用可接受)。如果怕缺少副作用导致后面崩溃,保留必要初始化(如全局标志位)后再 return。



sub_724FF47BB8()-总控开关 Magisk 检测


__int64 sub_724FF41074()

  StatusReg = _ReadStatusReg(TPIDR_EL0);
  v118 = *(StatusReg + 40);
  MAGISKTMP_1 = &v40;
  u:r:zygote:s0_1 = u:r:zygote:s0_3;
  _proc_%d_attr_prev_1 = &v36;
  v55 = &v34;
  s_8 = &v33;
  s_1 = &v32;
  s_5 = &v31;
  v59 = &v30;
  v60 = &v29;
  s_4 = &v28;
  v62 = &v27;
  v63 = &v26;
  v64 = &v25;
  v65 = &v24;
  v66 = &_proc_self_fd_%d__data_dalvik_cache_x86_pure_virtual_method_cal;
  _proc_self_fd_%d__data_dalvik_cache_x86_pure_virtual_method_cal = proc_self_fd__d__data_dalvik_cache_x86_pure_virtual_method_cal;// /proc/self/fd/%d,/data/dalvik-cache/x86,pure virtual method called,deleted virtual method called,St17bad_function_call,bad_function_call,%s: __pos (which is %zu) > this->size() (which is %zu),basic_string::at: __n (which is %zu) >= this->size() (which is %zu),basic_string::copy,basic_string::compare,
  %lx_%lx_%s_%_lx_%s_%ld_%s_2 = %lx_%lx_%s_%_lx_%s_%ld_%s_1;
  *&%lx_%lx_%s_%_lx_%s_%ld_%s_1[9] = *(&xmmword_724FF52E14 + 9);// Ķ ...........dat
                                                // a/app/./system/.
                                                // /apex/./vendor/.
                                                // /data/dalvik-cac
                                                // he/..........ꇌ  
  *%lx_%lx_%s_%_lx_%s_%ld_%s_1 = xmmword_724FF52E14;
  v68 = &v40;
  v40 = 0xEACDE2F4D0EEE6D4LL;
  n249 = 249;
  u:r:zygote:s0_4 = u:r:zygote:s0_3;
  *&u:r:zygote:s0_3[6] = 0xA9DA9DFCDDC8FELL;
  *u:r:zygote:s0_3 = 0xC8FED0DDA3DB9DECLL;
  v70 = &v36;
  v36 = xmmword_724FF52E45;
  v38 = 0;
  v37 = -8254;
  v71 = &v34;
  n169 = 169;
  v34 = 0xA700000099LL;
  _proc_self_maps = &_proc_self_fd_%d__data_dalvik_cache_x86_pure_virtual_method_cal;// /proc/self/maps
  v73 = &v34;
  i_2 = strlen(&_proc_self_fd_%d__data_dalvik_cache_x86_pure_virtual_method_cal);
  for ( i = 0; ; i = i_1 + 1 )
  {
    i_1 = i;
    if ( i >= i_2 )
      break;
    v112 = i_1 % 3;
    v113 = i_1 % 3;
    v114 = *(&v34 + v113);
    *(&_proc_self_fd_%d__data_dalvik_cache_x86_pure_virtual_method_cal + i_1) ^= v114;
  }
  %lx_%lx_%s_%_lx_%s_%ld_%s = %lx_%lx_%s_%_lx_%s_%ld_%s_1;// %lx-%lx %s % lx %s %ld %s
  j_1 = strlen(%lx_%lx_%s_%_lx_%s_%ld_%s_1);
  v9 = v73;
  for ( j = 0; ; j = i_1 + 1 )
  {
    i_1 = j;
    if ( j >= j_1 )
      break;
    v112 = i_1 % 3;
    v113 = i_1 % 3;
    v114 = *(v9 + v113);
    %lx_%lx_%s_%_lx_%s_%ld_%s_1[i_1] ^= v114;
  }
  MAGISKTMP = MAGISKTMP_1;                      // MAGISKTMP
  k_2 = strlen(MAGISKTMP_1);
  v11 = v73;
  MAGISKTMP_2 = MAGISKTMP_1;
  k_1 = k_2;
  for ( k = 0; ; k = i_1 + 1 )
  {
    i_1 = k;
    if ( k >= k_1 )
      break;
    v112 = i_1 % 3;
    v113 = i_1 % 3;
    v114 = *(v11 + v113);
    MAGISKTMP_2[i_1] ^= v114;
  }
  u:r:zygote:s0 = u:r:zygote:s0_1;              // u:r:zygote:s0
  m_1 = strlen(u:r:zygote:s0_1);
  v16 = v73;
  u:r:zygote:s0_2 = u:r:zygote:s0_1;
  for ( m = 0; ; m = i_1 + 1 )
  {
    i_1 = m;
    if ( m >= m_1 )
      break;
    v112 = i_1 % 3;
    v113 = i_1 % 3;
    v114 = *(v16 + v113);
    u:r:zygote:s0_2[i_1] ^= v114;
  }
  _proc_%d_attr_prev = _proc_%d_attr_prev_1;    // /proc/%d/attr/prev
  n_1 = strlen(_proc_%d_attr_prev_1);
  v20 = v73;
  _proc_%d_attr_prev_2 = _proc_%d_attr_prev_1;
  for ( n = 0; ; n = i_1 + 1 )
  {
    i_1 = n;
    if ( n >= n_1 )
      break;
    v112 = i_1 % 3;
    v113 = i_1 % 3;
    v114 = *(v20 + v113);
    _proc_%d_attr_prev_2[i_1] ^= v114;
  }
  s = s_8;
  memset(s, 0, 0x7D0u);
  s_9 = s_1;
  memset(s_1, 0, 0x7D0u);
  s_2 = s_8;
  pid = getpid();                               // 9736
  sprintf(s_2, "/proc/%d/cmdline", pid);        // /proc/9736/cmdline
  stream = fopen(s_2, R);
  if ( stream )
  {
    fscanf(stream, "%s", s_1);
    fclose(stream);
  }
  s_3 = s_1;
  if ( strchr(s_3, ':') )                       // 函数检测 Magisk root:如果 cmdline 无 ':',解析 maps 找栈区域,扫描栈 env 字符串找 "MAGISKTMP";
  {
    v47 = 0;
  }
  else
  {
    s_10 = s_5;
    v91 = v59;
    v0 = v59;
    *(v59 + 2) = 0;
    *v0 = 0;
    v92 = v60;
    v1 = v60;
    *(v60 + 2) = 0;
    *v1 = 0;
    s_11 = s_4;
    memset(s_4, 0, 0x1000u);
    v94 = v62;
    v95 = v63;
    v96 = v64;
    v97 = v65;
    _proc_self_maps_1 = fopen(_proc_self_maps, R);// /proc/self/maps
    v42 = 0;
    v43 = 0;
    _proc_self_maps_2 = _proc_self_maps_1;
    if ( _proc_self_maps_1 )
    {
      while ( 1 )
      {
        v110 = v42;
        _________ = feof(_proc_self_maps_2);    // 是否已到达文件末尾
        v48 = v110;
        if ( _________ )
          break;
        s_7 = s_5;
        _12c00000_52c0000_0_rw_p_00000000__00:00_0_________ = fgets(s_5, 4096, _proc_self_maps_2);// 12c00000-52c0000
                                                // 0 rw-p 00000000 
                                                // 00:00 0         
        v48 = v110;
        if ( !_12c00000_52c0000_0_rw_p_00000000__00:00_0_________ )
          break;
        v99 = v59;
        v100 = v60;
        s_6 = s_4;
        if ( sscanf(s_7, %lx_%lx_%s_%_lx_%s_%ld_%s, v62, v63, v99, v65, v100, v64, s_6) == 7 )
        {
          v108 = v62;
          v109 = v62 > *v62;
          if ( v109 && (v106 = v108 < *v63) )
          {
            n3 = 3;
            v2 = 1;
          }
          else
          {
            n3 = 0;
            v2 = v110;
          }
          v49 = v2;
        }
        else
        {
          v49 = v110;
          n3 = 2;
        }
        if ( n3 == 3 )
        {
          v42 = v49;
          v48 = v49;
          break;
        }
        v42 = v49;
        v48 = v49;
      }
      fclose(_proc_self_maps_2);
      v43 = v48 & 1;
    }
    if ( (v43 & 1) != 0 )
    {
      v88 = *v63;
      v89 = v88 - 512;
      ____________null_______1 = (v88 - 512);
      while ( 1 )                               // 字符串解密
      {
        haystack = ____________null_______1;
        ____________null_______2 = *v63 - 6;
        if ( ____________null_______1 >= ____________null_______2 )
          break;
        if ( strstr(haystack, MAGISKTMP) )      // 栈内存扫描(主要检测)
          goto LABEL_20;                        // 如果找到,设置 v51 = 1(检测到 Magisk)
        v83 = strlen(haystack);
        v84 = &haystack[v83];
        ____________null______ = &haystack[v83 + 1];// 如果未找到,跳到下一个 null 终止字符串
        ____________null_______1 = ____________null______;
        if ( ____________null______ > ____________null_______2 )// 检测完了木有,未检出为0
          goto LABEL_19;
      }
      pid_1 = getpid();                         // 备选检查(如果栈扫描未检测到)
      sprintf(s_2, _proc_%d_attr_prev, pid_1);  // sprintf v81 = v78 (格式如 "/proc/%d/environ") + pid。
                                                // /proc/9736/cmdline
                                                // /proc/9736/attr/prev
      stream_1 = sub_724FF42758(s_2, R);
      if ( !stream_1
        || (memset(s_3, 0, 0x7D0u), fscanf(stream_1, "%s", s_3), fclose(stream_1), strcmp(s_3, u:r:zygote:s0)) )
      {
LABEL_19:
        v51 = 0;
        goto LABEL_18;
      }
LABEL_20:
      v51 = 1;
LABEL_18:
      v44 = v51;
    }
    else
    {
      v44 = 0;
    }
    v47 = v44;
  }
  return v47 & 1;
}

sub_724FF47BB8总控开关:只有当 sub_724FEE4B08() 返回 218 时,才会进入真正的 Magisk 检测 sub_724FF41074();最终返回 0/1 表示是否检出。

sub_724FEE4B08门闩函数:内部是无意义“忙等/不透明谓词”,最后仅返回一个全局整型 dword_724FF60850。也就是说,是否做检测完全取决于这个全局值(是否等于 218)。

sub_724FF41074 才是实质检测:先用3 字节滚动 XOR解密若干字符串(/proc/self/mapsMAGISKTMPu:r:zygote:s0/proc/%d/attr/prev 等),然后:解析 /proc/self/maps 定位栈映射,在栈顶附近 [end-512, end-6] 扫描以 \\0 分隔的短串,查找包含 MAGISKTMP 的子串;如果未命中,备选检查 /proc/<pid>/attr/prev 是否等于 u:r:zygote:s0

两者任一为真则返回 1(认定为 Magisk/root 特征),否则 0。


1) __int64 sub_724FF47BB8()

作用:总控

流程:读 TPIDR_EL0(AArch64 TLS/stack canary,防栈溢出的样板,与业务无关)。调 sub_724FEE4B08(),若其返回 218,则继续调用 sub_724FF41074();否则认定为未检出(返回 0)。

返回值为 _____Magisk____root & 1(布尔化)。


2) __int64 sub_724FEE4B08()

作用:门闩 / 不透明谓词(opaque predicate)

特征:先读 TPIDR_EL0

一个看似循环的结构:

for (i = -536524096; i == -536524096; i = 1981835013) ;

由于循环条件一开始为真,循环体为空,随后 i 被改成另一个常量,循环立即结束 —— 纯混淆。返回 dword_724FF60850(一个全局整型)。

绕过点:把该全局值改成非 218,或 hook 该函数返回值。


3) __int64 sub_724FF41074()

作用:核心 Magisk/root 检测

关键步骤:

(a) 字符串解密:代码把很多可读常量(路径/格式串/关键字)存放在已混淆的数据块中,通过3 字节滚动 XOR逐字节恢复:

for (i = 0; i < len; ++i) {
  dst[i] ^= key[i % 3];  // key 来源于 v34 附近内存的 3 个字节
}

被还原的关键字/路径包括:"/proc/self/maps","%lx-%lx %s %lx %s %ld %s"(maps 行解析格式:起始-结束、权限、偏移、设备、inode、路径)"MAGISKTMP""u:r:zygote:s0","/proc/%d/attr/prev"以及构造 "/proc/%d/cmdline" 的模板

解密目的:混淆静态扫描,防止通过字符串特征直接识别。

(b) 读取 /proc/<pid>/cmdline:getpid()sprintf("/proc/%d/cmdline")fopen → 读第一段命令行字符串。若命令行包含 ':'(Android 常见多进程命名:com.xx:worker),则直接跳过后续检测,返回 0。

(c) 解析 /proc/self/maps 定位栈段:逐行 fgets,用解析格式串 %lx-%lx %s %lx %s %ld %s 提取字段:start, end, perms, off, dev, ino, path,通过与某指针区间比对判断是否为栈映射;本质是找栈的 [start,end))。一旦定位到栈映射,记下 end(代码里 v63),退出循环。

(d) 在栈顶附近扫描环境/短串:设 scan_begin = end - 512scan_end = end - 6,以 \\0 分隔为单位向前扫描:对每个以 \\0 结束的串 haystack,执行 strstr(haystack, "MAGISKTMP")。命中则 v51 = 1检出)并跳到 LABEL_20 收尾。

这一步是主要检测点:Magisk 的一些实现会在环境/临时目录相关变量里暴露 MAGISKTMP

通过从栈末尾向回扫 \\0 终止的 C 字符串段,能找到这类痕迹。

(e) 备选检查:/proc/<pid>/attr/prev:若 (d) 未命中:构造 /proc/%d/attr/prev 路径并读取其内容,与解密出的 "u:r:zygote:s0" 比较:strcmp == 0v51 = 1检出),否则 v51 = 0

/proc/<pid>/attr/prev 是 SELinux 相关的进程属性文件。正常应用进程通常不会是 u:r:zygote:s0;这个检查较为“私货”。



关键绕过点

  1. 门闩dword_724FF60850 ≠ 218 → 完全不进入检测

    Hook sub_724FEE4B08() 的返回值;或直接把该全局变量改写为非 218。

  2. 实质检测函数sub_724FF41074()

    入口把前两条指令改为 MOV W0,#0; RET → 恒返回 0(未检出)。或仅改两处“置 1”的赋值点(命中 MAGISKTMP / 命中 u:r:zygote:s0),把它们 NOP 掉或改成置 0。也可在运行时 hook strstr:当 needle=="MAGISKTMP" 时强制返回 NULL,从而让栈扫描“看不见”。

  3. 字符串还原

    若要静态还原所有常量,记住3 字节滚动 XOR的 key 来自函数内固定 3 字节(i%3 取 key),对每个密文重复异或即可。



sub_724FF2ACEC()-入口函数

image.png

image.png

// The function seems has been flattened
void sub_724FF2A478()
{
  int n544480486; // w8
  int n18972516; // w8
  signed int v2; // w0
  signed int v3; // w9
  int v4; // w10
  signed int v5; // w0
  signed int v6; // w9
  int v7; // w10
  void *handle; // [xsp+40h] [xbp-D0h]
  __int64 (__fastcall *pthread_create)(__int64 *, _QWORD, void (__fastcall __noreturn *)(), void *); // [xsp+48h] [xbp-C8h]
  bool ______________; // [xsp+5Fh] [xbp-B1h]
  int _______; // [xsp+60h] [xbp-B0h]
  int _________________libc.so_handle; // [xsp+64h] [xbp-ACh]
  _BYTE *v13; // [xsp+68h] [xbp-A8h]
  char *v14; // [xsp+68h] [xbp-A8h]
  char s[16]; // [xsp+84h] [xbp-8Ch] BYREF
  __int64 _____libc.so_; // [xsp+94h] [xbp-7Ch] BYREF
  __int64 ________; // [xsp+9Ch] [xbp-74h]
  int n236; // [xsp+A4h] [xbp-6Ch]
  __int64 com.cmi.jegotrip; // [xsp+A8h] [xbp-68h] v10 → "libc.so"
                            //                     v9 → "pthread_create"

  com.cmi.jegotrip = *(_ReadStatusReg(TPIDR_EL0) + 40);// com.cmi.jegotrip
  n236 = 236;
  ________ = 0xA700000099LL;                    // 加密用的异或密钥
  _____libc.so_ = 0xF69F89FA8ECEF5LL;           // 加密的 "libc.so"
  do
  {
    *&s[7] = 0xC2ED8DC2EB8FF8LL;
    *s = 0xF8FD8DC2EB84D3E9LL;                  // 加密的 "pthread_create"
    v2 = strlen(&_____libc.so_);
    while ( v4 == -1746935943 )
    {
      v3 = 0;
LABEL_13:
      v4 = -1552211836;
    }
    if ( v3 < v2 )
    {
      v13 = &_____libc.so_ + v3;
      *v13 ^= *(&________ + 4 * (v3 % 3));      // libc.so
      ++v3;
      goto LABEL_13;
    }
    v5 = strlen(s);
    while ( v7 == -1746935943 )
    {
      v6 = 0;
LABEL_29:
      v7 = -1552211836;
    }
    if ( v6 < v5 )
    {
      v14 = &s[v6];
      *v14 ^= *(&________ + 4 * (v6 % 3));      // pthread_create
      ++v6;
      goto LABEL_29;
    }
    handle = dlopen(&_____libc.so_, 2);         // 动态加载 "libc.so"
                                                // 使用 dlsym 获取 pthread_create 函数指针。
    if ( !handle )
      goto LABEL_2;
    pthread_create = dlsym(handle, s);
    ______________ = sub_724FEE4824() == 248;   // 用于条件判断是否激活反调试。
    if ( ______________ && (sub_724FF06B28() & 1) == 0 )// 检查是否存在libfrida-agent.so
                                                // sub_724FF06B28() == 0(未检测到 Frida)
      sub_724FF2EFA0(pthread_create);           // 创建线程(可能用于反调试钩子初始化)。
    _______ = sub_724FEE48A8();                 // 第二个检测分支
    if ( _______ != 249 )
    {
      sub_724FF287D4(handle, pthread_create);   // 线程检测
      break;
    }
    pthread_create(&qword_724FF61660, 0, fanxtiaoshi, 0);// 无限循环反调试线程
  }
  while ( n18972516 == 18972516 );
  _________________libc.so_handle = sub_724FEE47A4();
  if ( _________________libc.so_handle == 167 )
  {
    pthread_create(qword_724FF61668, 0, sub_724FF22A94, 0);// 在检测到设备处于 ADB 模式时,高频轮询电源插入状态;一旦发现设备接通电源(尤其是 USB 供电),就立刻自杀退出
    goto LABEL_18;
  }
  do
  {
    dlclose(handle);
LABEL_2:
    n544480486 = 544480486;
LABEL_18:
    ;
  }
  while ( n544480486 == 128074723 );
}
/* Orphan comments:
备用反调试初始化函数,使用传入的 libc.so handle 和 pthread_create 函数指针 a2(实际未使用 a2),通过 fork + ptrace 自跟踪子进程来防止外部调试器附加。
*/

if (!(sub_724FF47BB8() & 1)) {        // A. 环境安全?(未检出 Magisk/root)
    // 读取 /proc/<pid>/cmdline -> s_1(进程名)
    if (!strchr(s_1, ':')) {           // B. 仅对“主进程”生效(子进程名常含 ':')
        sub_724FF2ACEC();              // C. 进入反调试初始化入口
    }
}

sub_724FF47BB8()仅当它返回 1(阳性) 才会阻止进入反调试。其内部逻辑:

门闩函数 sub_724FEE4B08()只有当返回 218 时才会去做“Magisk/root 检测” sub_724FF41074()sub_724FEE4B08() 本身就是个不透明谓词外壳,最后返回某个全局 dworddword_724FF60850)。

是否做 Magisk 检测完全取决于这个全局值是否为 218

sub_724FF41074() 真正检测两点:① 栈末“\0 分隔短串”中是否含 "MAGISKTMP";② /proc/<pid>/attr/prev 是否等于 "u:r:zygote:s0"。命中任一 → 返回 1。

主/子进程分流

子进程名经常是 com.pkg:xxx,这里 只在主进程才初始化反调试

sub_724FF2ACEC()sub_724FF2A478()(核心反调试初始化)

异或解密 "libc.so""pthread_create"dlopen("libc.so")dlsym("pthread_create") 得到函数指针;若 sub_724FEE4824() == 248未检测到 Fridasub_724FF06B28() & 1 == 0),调用 sub_724FF2EFA0(pthread_create)(初始化一批反调试线程/钩子);若 sub_724FEE48A8() != 249,则调用 sub_724FF287D4(handle, pthread_create)(另一套线程检测/自守护策略);

sub_724FEE47A4() == 167,使用 pthread_create(..., sub_724FF22A94, 0) 启动“电源/USB 插入 → 触发自杀退出”的高频轮询线程;另有 pthread_create(..., fanxtiaoshi, 0) 这类“无限循环”线程推进周期自检/心跳。


字符串混淆与解析

  1. 字符串异或解密(3×4B 轮转密钥)

关键密文:"libc.so" → 存在 _____libc.so_"pthread_create" → 存在 s;解密使用 ________ = 0xA700000099 这一“12 字节”键的3 组 4 字节循环((&________ + 4 * (i % 3)))过程:对每个字节:plain[i] = cipher[i] ^ key[(i%3)*4 .. (i%3)*4+3] 的第(i%4)字节

反编译因类型塌陷显示为 4B 粗粒度取值,但可以按 i%3 取对应 4B,再用 i%4 取其中一字节完成还原。

  1. /proc/self/maps 解析

解出格式串:"%lx-%lx %s %lx %s %ld %s",读出:start、end、perms、off、dev、ino、path

通过与当前栈指针邻近关系定位栈段,取 end 并在 [end-512, end-6] 范围按 \\0 串扫描,strstr(haystack, "MAGISKTMP") 即判阳性。


反调试动作

sub_724FF2EFA0(pthread_create)sub_724FF287D4(handle, pthread_create)

周期性 ptrace(PTRACE_TRACEME) 或“self-ptrace 父子互指”策略,阻止外部调试器附加;prctl(PR_SET_DUMPABLE, 0)prctl(PR_SET_PTRACER, ...) 等;

signal(SIGTRAP/SIGILL/…) 处理器设置;open/stat /proc/self/status|maps|mounts 与关键字符串扫描(frida, gadget, agent 等);

侦测 libfrida-agent.sosub_724FF06B28() 就在 /proc/self/maps 查找此名)。

sub_724FF22A94:“ADB 模式时高频轮询电源插入状态(尤其 USB),一旦接通立即自杀退出”。此类线程常用 uevent/sysfs/battery 节点或 BroadcastReceiver/binder,但这里是 native,因此大概率通过读取 /sys/class/power_supply/battery_service 的 fd。


常量出现场景语义
218sub_724FEE4B08() == 218 才做 Magisk 检测构建/策略开关:允许/禁止做 Root 检测
248sub_724FEE4824() == 248是否启用第一组反调试线程初始化
249sub_724FEE48A8() != 249是否再启用第二组线程检测
167sub_724FEE47A4() == 167是否启用“电源/USB 触发自杀”线程

绕过位点

  1. 优先掐“门闩”(不进入反调试初始化)

目标:让 sub_724FF47BB8() 返回 1(即“环境不安全”),从而根本不走 sub_724FF2ACEC()

两种打法:

最外层:Hook sub_724FF47BB8,让它恒返回 1

内层门闩:把 sub_724FEE4B08() 返回改为 非 218(或把其全局 dword 改掉),使“Magisk 检测”分支不被执行,从而**sub_724FF47BB8() 返回 0

⇒ 最稳是直接让 sub_724FF47BB8() 返回 1(假称“已检出 Root”,于是不上反调试)。

  1. 若必须进入 sub_724FF2A478()卡住线程创建

该函数通过 dlopen("libc.so")dlsym("pthread_create") 获取指针再创建线程。

三种轻量拦截:

Hook dlsym:当符号为 "pthread_create"返回一个我们自定义的空实现地址(NativeCallback,直接返回 0,不真正创建线程);

直接 Hook pthread_create 导出符号:所有线程创建都“成功返回”但不执行

或在 sub_724FF2A478 内部拿到的 pthread_create 指针被调用点上打 NOP

  1. 关掉分支门闩(248/249/167)

Hook sub_724FEE4824 令其返回非 248,则不会跑 sub_724FF2EFA0

Hook sub_724FEE48A8 令其返回 249,使 if (_______ != 249) 为假,不跑 sub_724FF287D4

Hook sub_724FEE47A4 令其返回非 167,不创建“USB 插电自杀”线程。

4)反 Frida

sub_724FF06B28() 被用来判断是否存在 libfrida-agent.so。当前逻辑:

if ( sub_724FEE4824()==248 && (sub_724FF06B28() & 1) == 0 ) {
    sub_724FF2EFA0(...);  // 只有“未检测到 Frida”才启动一套反调试
}

让该函数返回 1(假称“检测到 Frida”) 反而不触发这条初始化路径;



Frida 切点

最外一刀

// Interceptor.replace(sub_724FF47BB8, new NativeCallback(()=>ptr(1), 'pointer', []));

阻断 pthread_create

Interceptor.replace(Module.getExportByName("libc.so","pthread_create"), stub);或

Hook dlsym:needle 等于 "pthread_create" 时返回 stub

关门闩

Interceptor.attach(addr_sub_724FEE4824, { onLeave(r){ r.replace(0); } }) 等。

maps/字符串层

Hook strstr:needle 为 "MAGISKTMP" 时返回 NULL(但这属于更内层的 sub_724FF41074,用于 Root 检测)。


pthread_create-线程创建

image.png

sub_724FF22A94()-电源/ADB 触发“自杀”的监控线程 + JNIEnv 获取 + ADB 判定

image.png

image.png

image.png


1) 线程主体:sub_724FF22A94(无限循环监控)

先用 JNIEnv_hq() 拿到 JNIEnv*(必要时通过 JNI_GetCreatedJavaVMs + GetEnv 间接获取)。调 usb_jc(env, …)判断是否接电/USB。若为真 → 立刻 exit_jc()(自杀退出)。

do { sleep(3); } while (!ADB_jc());

ADB 未开时:每 3 秒睡一次(低频轮询)。ADB 打开后:跳出 do…while,进入 while(1) 的下一轮——注意下一轮会不再 sleep,持续高速检测电源;一旦 usb_jc() 为真,就 exit_jc()

⇒ 组合逻辑是:ADB 打开 + 有电/USB 供电 ⇒ 立刻退出;ADB 未开时只是低频轮询。


2) JNIEnv_hq():跨 ART/Dalvik 拿 JNIEnv*

art_bj[0](布尔开关):决定 libart.so 还是 libdvm.sodlopen + dlsym("JNI_GetCreatedJavaVMs"),拿到 JavaVM*;调 JavaVM->GetEnv(jni**, version):先用 0x00010004 (JNI 1.4);若失败再用 0(ret 非 0 仍失败则返回 0)。成功则返回 JNIEnv*。所以它不需要 Java 层 attach,纯 native 自拿 JNIEnv。


3) ADB_jc():是否开启 ADB

先读系统属性 sys.usb.config 到缓存;若包含 "adb"返回 true。否则读取 sdk_bb(SDK INT)。若 ≤33(Android 13 及以下)直接返回 false(不再做二次检查)。若 >33(Android 14+):再读 persist.sys.usb.config,若包含 "adb"true,否则 false。


4) 行为与触发条件

退出条件usb_jc(...) == true(判断在每轮顶部)。加速轮询条件ADB_jc() == true(一旦 ADB 打开,后面每轮不再 sleep,等同 “忙等 + 高频检查 USB 供电”)。结合前面的“主流程”:只有 主进程(cmdline 不含 :)才会起这些线程。



5) 绕过方案

方案 A(最稳):sub_724FF47BB8() 恒返回 1

效果:上层认为“环境不安全”(疑似 root),于是不启动反调试线程(也许会降低功能,但通常只是不启反调试)。如果你只想顺利动态分析/抓包。

方案 B:定点让 ADB_jc() 恒返回 0 + usb_jc() 恒返回 0

效果:线程永远看不到 “ADB 开”/“有电”,所以不会 exit_jc(),且一直带着 sleep(3) 的低频节奏。

Hook _system_property_get,针对 sys.usb.config/persist.sys.usb.config 写入不含 "adb" 的值;Hook usb_jc 返回 0。

方案 C:入口短路屏蔽 exit_jc()

sub_724FF22A94 入口改成 RET:彻底不跑该线程逻辑;或将 exit_jc() 改为空实现(不退出)。


7) Frida 切点

(1) 让 ADB_jc() 恒返回 false:

// 假设 ADB_jc 在 libtarget.so + 0xAABBCC
const ADB_OFF = 0x00AABBCC, LIB = "libtarget.so";
const mod = Process.findModuleByName(LIB);
const ADB = mod.base.add(ADB_OFF);
Interceptor.replace(ADB, new NativeCallback(function(){ return 0; }, 'int', []));

(2) 让 usb_jc() 恒返回 false:

const USB_OFF = 0x00DDEEFF;
const USB = mod.base.add(USB_OFF);
Interceptor.replace(USB, new NativeCallback(function(){ return 0; }, 'int', ['pointer','int','uint','uint','uint']));

(3) 阻断 exit_jc()

const EXIT_OFF = 0x00123456;
const EXIT = mod.base.add(EXIT_OFF);
Interceptor.replace(EXIT, new NativeCallback(function(){ /* no-op */ }, 'void', []));

(4) 伪造 getprop(不含 adb):

['__system_property_get','_system_property_get'].forEach(name=>{
  const f = Module.findExportByName(null, name);
  if (!f) return;
  Interceptor.attach(f, {
    onEnter(args){ this.key = Memory.readUtf8String(args[0]); this.buf = args[1]; },
    onLeave(ret){
      if (this.key === 'sys.usb.config' || this.key === 'persist.sys.usb.config') {
        Memory.writeUtf8String(this.buf, 'mtp'); // 不含 "adb"
        ret.replace(3); // 非0表示成功写入
      }
    }
  });
});

(5) 直接让监控线程函数返回(入口短路):

const MON_OFF = 0x00778899; // sub_724FF22A94
const MON = mod.base.add(MON_OFF);
Memory.protect(MON, 4, 'rwx');
Memory.writeU32(MON, 0xD65F03C0); // RET

(6) 最外层:让 sub_724FF47BB8() 恒返回 1(不上反调试):

const GATE_OFF = 0x00ABCDEF; // sub_724FF47BB8
const GATE = mod.base.add(GATE_OFF);
Interceptor.replace(GATE, new NativeCallback(function(){ return ptr(1); }, 'pointer', []));


exit_jc()-退出判断函数分析

image.png


一、执行路径

exit_jc()
 ├─ sub_724FF1FB9C()      // JNI 环境 sanity check(不依赖返回值,更多是反 hook/时序干扰)
 ├─ sub_724FF13860()      // 无实质逻辑(恒 1)
 └─ dongtai_exit(0)       // ★ 动态生成并执行“直接系统退出”的机器码

核心:dongtai_exit(0)。前两个调用主要起“反分析/反 hook :

sub_724FF1FB9C() 通过 dlopen + dlsym("JNI_GetCreatedJavaVMs") → JavaVM->GetEnvlibart/libdvm 上“手搓”拿 JNIEnv*sub_724FF13860() 几乎是空函数(返回 1)。


二、dongtai_exit(unsigned int a1):动态生成“退出”shellcode

关键逻辑:

  1. shengchen_jqm(94)返回一个可执行函数指针(内存来自 mmap(PROT_EXEC),并对其进行 XOR 解码 + __clear_cache)。
  2. 立刻调用这个函数指针:fn(a1)
  3. 之后 munmap(addr, sysconf(40)) 清理这页内存(40 对应 _SC_PAGESIZE,即页面大小);
  4. 返回。

参数 94 的含义

AArch64 Linux 系统调用号:

__NR_exit = 93(退出单个线程),__NR_exit_group = 94(退出整个进程)

// 伪汇编(arm64)
mov     w0, <a1>           ; 退出码(这里传 0)
mov     x8, #94            ; __NR_exit_group
svc     #0                 ; 系统调用:整个进程立即退出
// (或备选:触发 SIGABRT / tgkill 自杀,但 94 的指向性很强)

避开了 exit() / abort() / pthread_exit() 这类容易被在 PLT / GOT / libc 层 hook 的调用;直接用系统调用号执行进程级退出,难以从用户空间框架层拦截。


三、sub_724FF1FB9C() 的要点(JNI 环境校验)

依据 art_bj[0] 选择 libart.solibdvm.sodlsym("JNI_GetCreatedJavaVMs") → 调用拿 JavaVM*;走 JavaVM->GetEnv(env**, 0x00010004 /*JNI 1.4*/),失败时再试 0;成功则拿到 JNIEnv*。中间穿插多个“门闩常量”(如 1446174508 / -663006137 / 1582186095 等)驱动扁平化分支;返回值未被 exit_jc() 使用,所以这一步不影响是否退出


绕过方法:

直接把 dongtai_exit 变成空操作(或恒返回):

Frida:

// 替换 dongtai_exit 为 no-op:返回 0,不生成/不执行 shellcode
const LIB = "libtarget.so";
const OFF_dongtai_exit = 0xXXXXXXXX; // 你的偏移
const fn = Module.findBaseAddress(LIB).add(OFF_dongtai_exit);
Interceptor.replace(fn, new NativeCallback(function(a1){ return ptr(0); }, 'pointer', ['uint']));

不会造成不可预期的 SIGSEGV(不碰 mmap / 执行权限)。

或者把 shengchen_jqm 换成“返回空指针”或“返回安全 stub”:让 dongtai_exit 拿到的函数指针为 NULL → 代码会不调用,直接 munmap 跳过。或者返回一个可调用但空操作的函数指针ret):

const OFF_shengchen_jqm = 0xYYYYYYYY;
const gen = Module.findBaseAddress(LIB).add(OFF_shengchen_jqm);
const stub = new NativeCallback(function(){ /* no-op */ }, 'void', ['uint']);
Interceptor.replace(gen, new NativeCallback(function(sysno){ return stub; }, 'pointer', ['int']));

再者把“异常触发点”掐断,让 exit_jc() 压根不被调用

Hook usb_jc() 恒返回 0;Hook ADB_jc() 恒返回 0;或直接把 exit_jc 的所有调用者处改成 NOP(风险更高,要逐点 patch)。



Frida 迷你版

逻辑:优先拦 dongtai_exit;如果没时间找偏移,就退而拦 shengchen_jqm。

'use strict';

// === 填你的 ===
const LIB = "libtarget.so";
const OFF_dongtai_exit  = 0xAAAAAAAA;  // dongtai_exit 偏移
const OFF_shengchen_jqm = 0xBBBBBBBB;  // shengchen_jqm 偏移

function base() {
  const m = Process.findModuleByName(LIB);
  if (!m) throw new Error("module not found: " + LIB);
  return m.base;
}

// 1) 首选:直接把 dongtai_exit 变 no-op
function patchDongtaiExit() {
  const p = base().add(OFF_dongtai_exit);
  Interceptor.replace(p, new NativeCallback(function(a1){
    // console.log("[dongtai_exit] blocked, code=", a1);
    return ptr(0);
  }, 'pointer', ['uint']));
  console.log("[+] dongtai_exit patched");
}

// 2) 备选:把 shengchen_jqm 改成返回一个空操作函数指针
function patchGenerator() {
  const gen = base().add(OFF_shengchen_jqm);
  const stub = new NativeCallback(function(/* code */){ /* no-op */ }, 'void', ['uint']);
  Interceptor.replace(gen, new NativeCallback(function(sysno){
    // console.log("[shengchen_jqm] intercepted, sysno=", sysno);
    return stub;
  }, 'pointer', ['int']));
  console.log("[+] shengchen_jqm patched");
}

setImmediate(() => {
  try { patchDongtaiExit(); } catch(e) {
    console.error(e);
    try { patchGenerator(); } catch(e2) {
      console.error(e2);
    }
  }
});

sub_724FF287D4(handle, pthread_create)-线程检测分析


一套自-ptrace 反调试

  1. 动态解密 "ptrace"dlsym 出 libc 里的 ptrace 指针;
  2. fork()子进程自跟踪PTRACE_TRACEME),父进程 waitpid 驻守并依据状态 WIFSTOPPED/EXITED/SIGNALED 做分支;还会在不同阶段对被追踪子进程 PTRACE_LISTEN/CONT
  3. 并行起线程 sub_724FF26ECC(a1=pid) 检查僵尸进程等异常;
  4. 多处监看 TracerPid(自身是否被外部调试器附加);
  5. 任何一步“不对劲”就走 exit_jc()(动态生成 syscall=94 的 shellcode 执行 exit_group,直接把整个进程杀掉)。

1) 入口门闩 & 早期退出

读 TLS canary(模板代码)。多个状态常量 n1141574616 驱动扁平状态机

会检查:TracerPid()/proc/self/status 中的 TracerPid),若非 0 且 TracerPid_jc(TracerPid)!=0直接 exit_jc()prctl(4,1,0,0,0)PR_SET_DUMPABLE=1(有些策略用 0,这里设 1,主要影响 core dump/ptrace 许可)。

2) 字符串解密 & dlsym

用 3×4B 轮换 XOR 方式解密两串:

s:格式/其他字符串(用于后续)ptrace:字面 "ptrace"

handle = dlsym(handle, "ptrace") 拿到函数指针(之前的 handle 来自 libc.so)。保存到 fork 变量的是另一个通过 dlsym 拿到的 fork() 指针(注意不是系统调用号)。

3) fork() + 自-ptrace 布局

pid = fork()

子进程路径:调用 ptrace(PTRACE_TRACEME, 0, 0, 0),让自己被父进程跟踪(从而阻断外部再附加);raise(SIGSTOP)(或让 kernel 以 WIFSTOPPED 形式把状态送给父进程,反编译里通过 waitpid 层次可以看到对 0xFF00>>8 的解析)。

父进程路径waitpid(child, &status, …) 循环;解析 statusstatus & 0xFF00(>>8 后是停止信号),保存到 pid_13/pid_12;判断 n6(一个“事件阶段计数器/状态计数器”)分多步推进:1→…→6,对应不同 PTRACE_*wait 分支;在若干阶段对子进程ptrace(PTRACE_LISTEN)ptrace(PTRACE_CONT);如果 waitpidptrace 出错、或者收到了非预期的状态(比如不是 SIGSTOP/SIGTRAP 等),就判失败。期间多次设置/检查全局 dword_724FF61688(计数器/心跳)该 wait/ptrace 主循环被编译器强烈扁平化

4) 僵尸进程探测线程

pthread_create(ptrace_6, 0, sub_724FF26ECC, new int(ptrace_zhizhen))

这里的 ptrace_zhizhen 实际是 pid;线程函数从注释看是“检查指定 pid 是否成为僵尸”,如果是,就触发异常路径(通常也会间接引发 exit_jc())。

5) 失败/异常路径 → exit_jc()

在多处分支(比如 TracerPid 非零校验后、wait/ptrace 过程异常、僵尸判定异常)直接跳到 LABEL_308handle = exit_jc();(动态 exit_group(0))或者 exit(1)(极少数 fallback)。


片段分析理解

PTRACE_LISTEN / PTRACE_CONT:能看到显式赋值分支:PTRACE_LISTEN 出现在“初次建立跟踪会话后”;PTRACE_CONT 出现在收到停止后继续时;::ptrace(PTRACE_CONT, v90, 0, pid_1);(父进程发给子进程)。

wait status 解析v79 = v87 & 0xFF00; pid_13 = v79 >> 8;v82 = v87 & 0xFF00; pid_12 = v82 >> 8;这是典型的 WSTOPSIG(status) 提取停止信号。

TracerPid 双重校验handle = TracerPid(); handle_6 = handle; v67 = handle == 0;随后 handle = TracerPid_jc(handle_6); if (handle & 1) ...(读取 /proc/self/status 确认未被附加)。


函数具体作用

阻止外部附加(GDB/LLDB/Frida):借助 子进程 PTRACE_TRACEME 与父进程 wait + ptrace占坑机制,Linux 下 ptrace 是互斥的。

抗 patch/Hookptrace 函数指针来自 dlsym(且字符串解密),不走 PLT 重定位;换库路径也没用(直接吐“ptrace”名字去找)。抗静态分析:扁平化 + 常量跳枢 + 线程/进程多路并行。


如何绕过

1:把“外部已调试/异常”的门闩关掉

TracerPid() 恒返回 0;或让 TracerPid_jc(x) 恒返回 0(假称“合法”)。优点:状态机继续往下走,但不会立刻 exit_jc()

2:dlsym("ptrace") 返回“假 ptrace”

Hook dlsym:当 symbol == "ptrace" 时,返回我们自制的 stub

PTRACE_TRACEMEPTRACE_LISTENPTRACE_CONT 直接返回 0(成功),但不做系统调用(不占坑、不改变真实 ptrace 关系);父进程 waitpid 仍然能收到 SIGSTOP(由子进程自己的 raise(SIGSTOP) 触发),而我们的 ptrace 不会影响外部调试器;这样既不破坏它的状态机,也不给 kernel 建立真正的 ptrace 关系。注意:要确保“它期待的语义”能被满足(例如收到 WIFSTOPPEDPTRACE_LISTEN/CONT 也返回 0)。

3:入口 RET / 直接 return 0

4:自-ptrace 分支“空转”

允许 fork() 成功;在子进程路径里让 ptrace(PTRACE_TRACEME) 的调用变成空操作(Stub,返回 0);在父进程路径里让针对 childptrace(PTRACE_LISTEN/CONT) 也变 stub(返回 0);保持 waitpid 能拿到“期望”的几个状态(SIGSTOP、继续、正常退出);



Frida 思路

优先dlsym("ptrace") → 返回我们自己的 ptrace_stub

const real_dlsym = Module.findExportByName(null, "dlsym");
const ptrace_stub = new NativeCallback(function(req, pid, addr, data){
  // 模拟成功,不触发真正的 sys_ptrace
  return 0;
}, 'int', ['int','int','pointer','pointer']);

Interceptor.attach(real_dlsym, {
  onEnter(args){ this.sym = Memory.readUtf8String(args[1]); },
  onLeave(ret){
    if (this.sym === "ptrace") { ret.replace(ptrace_stub); }
  }
});

这样父/子路径都“以为” ptrace 成功,但实际没有建立 ptrace 关系,外部调试器/Frida 仍可附加。如果想“一刀切”:exit_jcdongtai_exit 换空实现


fanxtiaoshi()-轮询哨兵分析

image.png

void __noreturn fanxtiaoshi() {
  // 1) 取一个全局的 usleep 间隔
  unsigned int delay = sub_724FEE48E8();        // 返回轮询周期(微秒)
  if (delay < 0x64) delay = 2000000;            // 太小则强行设成 2s,避免忙等
  // 2) 无穷循环:每次休眠 delay 后检查 TracerPid
  for (;;) {
    unsigned int tpid = TracerPid();            // 读 /proc/self/status 的 TracerPid
    char ok;
    if (tpid) {                                 // 有追踪者
      if (tpid == (unsigned)-1)                 // 读取出错(用 -1 表示)
        ok = 0;                                 // 视为异常
      else
        ok = TracerPid_jc(tpid);                // 二次校验“是不是自己认可的追踪者”
    } else {
      ok = 1;                                   // 无追踪者也视为安全
    }
    if ((ok & 1) == 0)                          // 低位为 0 => 判定异常
      exit_jc();                                // 立刻触发“动态退出”路径(mmap+代码执行)
    usleep(delay);
  }
}

TracerPid()-实时检测 ptrace 附加函数

image.png

image.png

TracerPid_jc-PID检测函数分析

image.png

image.png


1) TracerPid() — 读取 /proc/self/status 的 TracerPid

动态解密出 "/proc/%d/status""TracerPid:" 两个字符串;打开并逐行读 /proc/<pid>/status;找到 TracerPid: 行后,取后面数字(跳过 10 个字符 "TracerPid:\\t"),atoi 得到追踪者 PID。

伪代码

int TracerPid(void) {
    char path[1024];     // "/proc/%d/status" (XOR 解码)
    char key1[] = "TracerPid:"; // 同样 XOR 解码
    int pid = getpid();

    // 解码 "/proc/%d/status" 到 path
    sprintf(path, "/proc/%d/status", pid);
    FILE *fp = fopen(path, "r");
    if (!fp) return -1;                  // 文件打不开,返回 -1(异常)

    char line[1024] = {0};
    while (fgets(line, sizeof(line), fp)) {
        char *p = strstr(line, "TracerPid:");
        if (p) {
            int v = atoi(p + 10);        // 取数值部分
            fclose(fp);
            if (v) return v;             // >0:被某 PID 跟踪
            else   return 0;             // ==0:未被跟踪
        }
    }
    fclose(fp);
    return 0;                            // 没找到字段也当作未被跟踪
}

1:文件打开失败/读取异常;0:未被 ptrace>0:被 PID = 返回值 的进程跟踪。


2) TracerPid_jc(handle) — 校验“是谁在跟踪我”

用传入的 handle 作为 Tracer PID;动态解密出 "/proc/%d/cmdline" 和另一个标识串(形如 "Pid:" 或用于匹配 cmdline 的关键字;代码里是 s__1,通过 XOR 解密);读取 /proc/<handle>/cmdline,查找该标识;结合自身逻辑做“白名单/合法性”判定;返回 1(最低位为 1)表示“合法追踪者”(通常是自跟踪的友方进程/线程),否则 0

伪代码

int TracerPid_jc(unsigned int tracerPid) {
    if (tracerPid == (unsigned)-1) return 0;    // 上游异常直接失败

    char path[1024];      // "/proc/%d/cmdline"(XOR 解密)
    char mark[16];        // s__1(XOR 解密,可能是 "Pid:" 或用于匹配 cmdline 的关键串)
    sprintf(path, "/proc/%u/cmdline", tracerPid);

    FILE *fp = fopen(path, "r");
    if (!fp) return 0;                          // 打不开:判异常

    char line[1024] = {0};
    while (fgets(line, sizeof(line), fp)) {
        // 条件 1:cmdline 中含有期望的“友方特征”(或不含已知调试器特征)
        // 条件 2:某些分支会校验 “Pid: <self>” 等(你的反编译里有 pid 比对的影子)
        if (strstr(line, mark)) {
            // 满足某个白名单规则(自跟踪/授权追踪者)
            fclose(fp);
            return 1;
        }
    }
    fclose(fp);
    return 0;                                   // 没匹配到,视为非法跟踪者
}

TracerPid_jc() 在不同版本里常见两类策略:

  • 白名单:只允许“自己 fork/ptrace 的子(父)进程”,或限定进程名/路径;
  • 黑名单:若命中 gdbfrida-serverlldbandroid_server 等调试器特征字符串则判定为 0

3) exit_jc() — 动态退出(反 Hook)

调用 sub_724FF1FB9C() 做 JNI 环境探测(可作为额外稳定/指纹信号);调用 sub_724FF13860()(占位/可能埋点);调 dongtai_exit(0):通过 XOR 解码一段指令 → mmap(PROT_EXEC)__clear_cache → 执行 → munmap。等价 “exit(0)/abort()”,但规避对常见 libc 退栈函数的 Hook。


4) 三者是如何配合

轮询线程 fanxtiaoshi()

delay = sub_...48E8();  // 微秒级,<0x64 强制 2s
for (;;) {
  tpid = TracerPid();
  ok   = (tpid==0) ? 1 : (tpid==(unsigned)-1 ? 0 : TracerPid_jc(tpid));
  if ((ok & 1) == 0) exit_jc();
  usleep(delay);
}

无人跟踪 ⇒ 放行有人跟踪 ⇒ 必须被认定为“自己人/合法”;任何异常(读失败/-1/不在白名单)均触发动态自杀


5) 触发条件

TracerPid() 结果TracerPid_jc() 结果解释行为
-1/proc/self/status 读失败退出
0无人跟踪继续运行
>01有人跟踪,且校验通过(自跟踪)继续运行
>00有人跟踪,但非白名单/含黑词退出


整体关系

链路与此前的:JNI/ART 探测Frida/agent 扫描/proc/self/maps 栈搜索 MAGISKTMPADB/供电状态线程自 ptrace 父子进程。共同组成了一个多路传感 + 多因子触发的防分析框架。TracerPid 只是其中“实时态”最直接的信号源之一。


三处函数的调用图


总览:反调试轮询线程(fanxtiaoshi

+--------------------------- 进 程 ----------------------------+
|                                                              |
|  thread: fanxtiaoshi (守护轮询)                              |
|  -------------------------------                             |
|  [取采样周期] ← sub_724FEE48E8()                             |
|      |                                                       |
|  +---v------------------------------+   (循环)               |
|  | tpid = TracerPid()               |--------------------+   |
|  +---+------------------------------+                    |   |
|      | tpid == -1  (异常)                                |   |
|      |---------------------------> [EXIT] exit_jc() <----+   |
|      | tpid == 0   (无人跟踪) -> [OK] 睡眠 → 下一轮           |
|      | tpid  > 0   (被跟踪)     |                          |
|      +---------------------------v----------------------+   |
|                              TracerPid_jc(tpid)         |   |
|                                   |  返回 1(合法) -> [OK]  |
|                                   |  返回 0(非法) -> [EXIT]|
+--------------------------------------------------------------+

  1. TracerPid() 内部流程(读取 /proc/self/status
+----------------------- TracerPid() ------------------------+
|  [解码路径模板 "/proc/%d/status"]                          |
|  pid = getpid()                                            |
|  path = sprintf("/proc/%d/status", pid)                    |
|  fp = fopen(path, "r")                                     |
|      |                                                     |
|      |-- 打不开 --> return -1  (读取异常)                  |
|      v                                                     |
|  循环: fgets(line)                                         |
|      |                                                     |
|      |-- strstr(line, "TracerPid:") ? -----------------+   |
|      |                                                |    |
|      |  YES: v = atoi(p + 10)  // 跳过 "TracerPid:\\t" |    |
|      |       fclose(fp)                                |   |
|      |       return (v==0 ? 0 : v)                     |   |
|      |                                                |    |
|      |  NO : 继续读下一行                              |    |
|      +------------------------------------------------+    |
|  (读到文件末尾仍未找到) -> fclose(fp) -> return 0           |
+------------------------------------------------------------+

  1. TracerPid_jc(tpid) 内部流程(校验“跟踪者是否合法”)
+----------------------- TracerPid_jc(tpid) ------------------------+
|  if (tpid == (unsigned)-1) return 0  // 上游异常直接失败           |
|                                                                    |
|  [解码路径模板 "/proc/%u/cmdline"]                                 |
|  [解码特征 mark(白名单关键字/格式片段,如 "Pid:" 或友方标识)]     |
|  path = sprintf("/proc/%u/cmdline", tpid)                          |
|  fp = fopen(path, "r")                                             |
|      |                                                             |
|      |-- 打不开 --> return 0  // 无法验证,判非法                  |
|      v                                                             |
|  ok = 0                                                            |
|  循环: fgets(line)                                                 |
|      |                                                             |
|      |-- strstr(line, mark) ? -------- YES ---------> ok = 1  -----+
|      |                              NO: 继续读下一行                |
|      +-------------------------------------------------------------+
|  fclose(fp)                                                        |
|  return ok    // 1=合法(友方自跟踪),0=非法(外部调试/特征不符)   |
+--------------------------------------------------------------------+

  1. exit_jc() 动态退出(绕过常规 libc 钩子)
+------------------------- exit_jc() --------------------------+
|  sub_724FF1FB9C()   // JNI/VM 环境探测(稳定性/指纹信号)      |
|  sub_724FF13860()   // 可能为埋点/占位                          |
|  dongtai_exit(0)    // XOR 解码 → mmap(PROT_EXEC) → 写入指令    |
|                     // __clear_cache → 跳转执行 → munmap        |
|                     // 等价 exit(0)/abort(), 但更难 Hook        |
+--------------------------------------------------------------+

  1. 失败/成功(判定)
            +------------------ TracerPid() ------------------+
            | -1 : 读取异常 → EXIT (exit_jc)                  |
            |  0 : 无人跟踪 → OK                               |
            | >0 : 有跟踪者 tpid                               |
            +-------------------------+------------------------+
                                      |
                                      v
                        +------ TracerPid_jc(tpid) ------+
                        |  return 1 : 合法自跟踪 → OK    |
                        |  return 0 : 非法/不匹配 → EXIT |
                        +--------------------------------+

  1. 线程/组件关系
+------------------- APP 进程 --------------------+
| main thread                                      |
|  └─ 业务逻辑                                     |
|                                                 |
| guard threads                                   |
|  ├─ fanxtiaoshi():                              |
|  |    TracerPid() → TracerPid_jc() → exit_jc()  |
|  |    (循环, usleep(delay))                      |
|  ├─ ADB/USB 监测线程 (sub_724FF22A94)            |
|  |    ADB_jc() / usb_jc() → exit_jc()           |
|  ├─ 自 ptrace 父/子 监控 (sub_724FF287D4 等)      |
|  |    fork + ptrace + waitpid 组合               |
|  └─ 其它:JNI/ART/Frida/maps/MAGISKTMP 扫描……     |
+--------------------------------------------------+

动态分析(后续发视频)

四:修改目标so,替换原来的so

总体初始化

sub_724FF2A478()
    |
    |--[XOR 解密]--> "libc.so" / "pthread_create"
    |
    |-- dlopen("libc.so") --> handle
    |       |
    |       +-- dlsym("pthread_create") --> pthread_create
    |
    |-- sw1 = (sub_724FEE4824() == 248)
    |-- frida = (sub_724FF06B28() & 1)
    |       |
    |       +-- if (sw1 && !frida) --> sub_724FF2EFA0(pthread_create)
    |
    |-- sw2 = sub_724FEE48A8()
    |       |
    |       +-- if (sw2 != 249) --> sub_724FF287D4(handle, pthread_create)
    |       |                           (fork+ptrace 自跟踪/父子互监)
    |       |
    |       +-- else               --> pthread_create(..., fanxtiaoshi, ...)
    |
    |-- sw3 = sub_724FEE47A4()
    |       |
    |       +-- if (sw3 == 167) --> pthread_create(..., sub_724FF22A94, ...)
    |
    +-- dlclose(handle)

线程与功能关系

+-------------------- 进 程 --------------------+
| main: sub_724FF2A478()                        |
|  ├─ (可选) sub_724FF2EFA0()                   |  // 反调试预处理
|  ├─ (二选一)                                  |
|  |    ├─ sub_724FF287D4()   // 自 ptrace + 父子监控状态机
|  |    └─ fanxtiaoshi()      // 实时 TracerPid 轮询线程
|  └─ (可选) sub_724FF22A94()  // ADB/USB 供电联动自杀
+-----------------------------------------------+



修改检查点

根据具体情况将上述的检查点进行修改

替换so

五:重打包

apktool.bat b F:\\apktool\\b -o F:\\apktool\\b.apk

六:签名,测试安装app

keytool -genkey -alias abc.keystore -keyalg RSA -validity 20000 -keystore abc.keystore

jarsigner -verbose -keystore abc.keystore -signedjar wy_ok.apk wy.apk abc.keystore

image.png

第一个可能遇见的问题:xml文件配置问题

AndroidManifest.xml文件,安装错误日志("INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2"),问题根源:

在<application>标签中,设置了android:extractNativeLibs="false"。这会导致系统在安装APK时无法提取本地库文件(native libraries,如.so文件),特别是在Android Q(API 29)及以上版本、某些虚拟设备或特定架构(如ARM64)上。

这个设置通常是为了优化APK大小(不提取库文件,直接从APK中加载),但在某些场景下会触发兼容性问题,尤其如果APK包含本地库且设备不支持未提取模式。

其他潜在因素:APK可能包含未对齐或压缩的本地库;设备存储不足;或minSdkVersion与设备不匹配。但从manifest看,compileSdkVersion="34"(Android 14),这可能加剧问题。

解决

将android:extractNativeLibs改为"true"。这会强制系统在安装时提取本地库,避免提取失败。

第二个可能遇见的问题:对齐问题

错误消息为“Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary]”,这是一个常见的Android APK安装失败问题,通常在使用adb install命令时出现。该错误表示APK针对Android 11(API 30)或更高版本,但APK内部的resources.arsc文件被压缩了,或者未按4字节边界对齐。根据Android官方要求,对于API 30+的APK,resources.arsc必须以未压缩形式存储,并对齐到4字节边界,否则安装解析会失败

对齐APK(确保4字节边界对齐)

使用zipalign(位于Android SDK的build-tools文件夹中)对齐重建后的APK。这会修复潜在的对齐问题。

zipalign -p -f -v 4 F:\\\\apktool\\\\unsigned_b.apk F:\\\\apktool\\\\aligned_b.apk

七:补充后续,过了反调试后干了什么

image.png

sub_724FF09D08()-解析/解封装一段内存数据块


先做环境/完整性与反分析检查;随后解析/解封装一段内存数据块,按特征头“NAOP”定位与拷贝,再把得到的载荷交给后续装载/校验逻辑(sub_724FED1194),整个过程中若检测到异常(JNI/Frida/校验失败/TracerPid 等)则提前返回。


sub_724FF09D08()
|
|-- n203 = sub_724FEE4FB4()
|   |
|   +-- if (n203 != 203)  OR  CRC32_check_ok():
|           ↳ 继续;否则同样进入“继续”,这块是容错/白名单分支(见注)
|
|-- sub_724FF49260()                 // 预初始化/环境布置
|
|-- loop {
|     // --- 环境/反分析检查 ---
|     if ( JNI_env_bad() || suspiciousA() || suspiciousB() ) return;
|
|     if (sdk > 20) {
|         JNIEnv *env = sub_724FEE50B8();
|         if (!*env)                return;      // 无有效 JNIEnv
|         if ( frida_jc(env) != 0 ) return;      // 命中 Frida -> 直接返回
|     }
|
|     // --- 解析目标数据区的起始与长度 ---
|     base = off_724FF5FF38 - sub_724FEE456C();
|     off  = sub_724FEE45AC();
|     len  = sub_724FEE45EC();
|     buf  = base + off;
|
|     // --- 若 magic==179 ,尝试解封装(解压/解密) ---
|     if ( magic(buf) == 179 ) {
|         (buf2, len2) = sub_724FF3EA24(buf, len);   // 解码/解压
|         if (buf2) { buf = buf2; len = len2; }
|     }
|
|     // --- 在 buf 中查找“NAOP”头,最多回退 0x2000 字节 ---
|     ptr = locate_NAOP(buf, len);   // 小步回退扫描 + memcmp(...,"NAOP",4)
|     if (!ptr) { dest=NULL; goto LOAD; }
|
|     // --- 拷贝并二次校验 ---
|     dest = malloc(len);
|     if (dest) {
|         memcpy(dest, ptr, len);
|         if ( magic(dest) != 179 ) { /*保留原dest*/ }
|     }
|
| LOAD:
|     // 把得到的 payload 递交给后续装载/校验
|     so = so_mz();                              // 可能返回固定标签/库名
|     ok = sub_724FED1194(&qword_724FF61258, so, dest, sub_724FED0D6C());
|     if (!ok)  continue;                        // 本轮失败 -> 再试
| }

“magic==179”是该样本里常见的自定义魔数(是否代表“已封装/需解封装”的状态位),由 sub_724FEE47E4() 判定。

"NAOP"结构/块头标记,用来从(可能是对齐/拼接的)缓冲中定位真正头部(回退扫描 0x2000 范围)。CRC32_jiaoyan()base+off 起的区段做完整性校验,和 sub_724FEE4F30() 返回的参考值比对。

sub_724FF3EA24():解压/解密型的“解封装”函数。sub_724FED1194():把载荷与上下文 (&qword_724FF61258, v28=sub_724FED0D6C()) 交给装载/注册/验签流程,失败则下一轮。

JNI_env_bad() ≈ (sub_724FF1FB9C() & 1)==0suspiciousA/Bsub_724FF44FEC() / sub_724FF47AD0() 风险探测。


[定位区段]
base = B = off_724FF5FF38 - sub_724FEE456C()
off  = O = sub_724FEE45AC()
len  = L = sub_724FEE45EC()
buf  = B + O

[可选解封装]
if magic(buf)==179:
    (buf2, L2) = sub_724FF3EA24(buf, L)
    if buf2: buf=buf2; L=L2

[回退扫描 NAOP]
ptr = buf
step back ≤ 0x2000 bytes by small steps:
    if memcmp(ptr, "NAOP", 4)==0: break
    ptr -= 1
if not found: dest=NULL; goto LOAD

[拷贝+二次 magic]
dest = malloc(L)
if dest:
    memcpy(dest, ptr, L)
    if magic(dest)!=179:
        // 保留 dest(或视为半成品),后续交给装载函数检验

分支点/调用条件(为真)动作
sub_724FF1FB9C()JNI/VM 异常return(直接结束)
sub_724FF44FEC()风险 Areturn
sub_724FF47AD0()风险 Breturn
frida_jc(env)命中 Fridareturn
magic(buf)==179缓冲是“封装态”sub_724FF3EA24 解封装
locate_NAOP()未找到 “NAOP” 头dest=NULL,仍尝试走装载
sub_724FED1194(...)返回 0(校验/装载失败)continue 再轮询
CRC32_jiaoyan(...)==OK完整性校验通过允许继续后续步骤


伪代码

if (!precheck_ok()) return;                 // JNI / 风险位 / Frida
(B,O,L) = region_layout();
buf = B + O;
if (magic(buf)==179) (buf,L) = maybe_unpack(buf,L);
ptr = scan_back_for(buf,"NAOP",0x2000);
dest = ptr ? memcpy(malloc(L), ptr, L) : NULL;
if (dest && magic(dest)!=179) {/*仍尝试*/}
ok = deliver(&ctx, so_mz(), dest, extra_ctx);
if (!ok) retry;

sub_724FED1194()-受互斥保护的载荷注册/装载入口


sub_724FED1194()受互斥保护的载荷注册/装载入口

pthread_once 完成全局上下文初始化,并取出一个全局互斥量;在持锁状态下调用 sub_724FEEF7F8(...) 按给定参数(a2, dest, a4...构造/解析一个对象句柄 v27;如有配置(v18 非空)且对象头部魔数匹配,则走 sub_724FEFF31C(...)签名/完整性校验;校验失败就调用 sub_724FEEE508 清理;成功时把得到的对象指针写回 a1,返回 1;否则返回 0;全程进入/退出时分别调用 sub_724FED1950() / sub_724FED1C34()一次性初始化与收尾


参数

a1(X0传入,函数内作 a1 = v27):输出位址,用于回传成功构建/加载后的对象指针。

a2标识/名字/上下文指针(在 sub_724FED1950() 被存到 once 区域 712 偏移处,还会被传给 sub_724FEEF7F8,像是“模块名/来源”)。

dest载荷缓冲区(上游 sub_724FF09D08() 解析/解封装后的 payload)。

a4参数块/上下文结构体(大量以偏移使用:

a4 → 传入 sub_724FEEF7F8(可能是 flag/版本/种子);

a4+66a4+2 → 同传入(可能是辅助指针/长度/环境);

a4+75(解读为 v18)→ 公钥/证书/策略指针(是否启用验签的开关+材料);

a4+76(解读为 v17)→ 公钥长度/序列号/策略位。)


调用关系

sub_724FED1194(a1, a2, dest, a4)
│
├─ sub_724FED1950(tmp, a4)             // 进场:根据 a4 做一次性初始化标记
│   └─ pthread_once_01()
│       ├─ pthread_once(_bss_start__, sub_724FEE4418)
│       └─ return qword_724FF61108     // 单例,全局上下文基址
│
├─ pthread_once_01() → mutex_base
│   └─ (后续 +40 偏移处用作 mutex 指针)
│
├─ pthread_mutex_lock(mutex)
│
├─ v27 = sub_724FEEF7F8(mutexa, a2, dest, 2, *a4, a4+66, a4+2)
│    // 构造/解析对象,成功返回对象指针 v27
│
├─ if (v27)
│    ├─ if (* (a4+75) != 0)                         // 配了验签材料才进
│    │    ├─ if (*(v27+12) == -839965817)           // 头部魔数/版本匹配
│    │    │    ├─ v4 = *(v27+16)                    // 指向签名/元数据
│    │    │    └─ ok = sub_724FEFF31C(v4, *(a4+75), *(a4+76), a4+2)
│    │    │         ├─ 返回 1:验签通过
│    │    │         └─ 返回 0:验签失败 → sub_724FEEE508(mutexa, v27) 清理
│    │    └─ else(魔数不符):跳过验签分支(走 LABEL_3)
│    └─ if (通过 or 未启用验签):*a1 = v27, ret=1
│
├─ pthread_mutex_unlock(mutex)
│
└─ sub_724FED1C34(tmp, …)              // 出场收尾(清理临时痕迹/计数等)
   return ret (0/1)


细节分析

  1. 一次性初始化(sub_724FED1950

    传入 a2,将 sub_724FED6098 写到 pthread_once_01() 返回区域的 +704 偏移,将 a2 存到 +712;并把 result[0]=1。说明 once 区域扮演“全局控制块/回调注册”角色,sub_724FED6098 可能是析构/通知/日志回调

  2. 互斥保护

    通过 pthread_once_01()+40 拿到一个 pthread_mutex_t*,配合 lock/unlock 包裹“构造+验签+发布”。这保证对象注册线程安全可见性

  3. 对象构造(sub_724FEEF7F8

    关键参数:(mutexa, a2, dest, 2, *a4, a4+66, a4+2)。风格像“把内存载荷 dest 解析为内部对象”,同时用 a2 作为名称/域,a4 等作为策略/模式,a4+66/a4+2 作为额外上下文/资源池。成功返回对象指针 v27

  4. 验签/完整性

    只有在 (a4+75) 非空时启用(理解为“提供了公钥/policy,就执行验签”)。先检查对象头 (v27+12) == -839965817(魔数/版本),再取 (v27+16) 作为签名/索引传给 sub_724FEFF31Csub_724FEFF31C(v4, pubkey, keylen_or_flags, a4+2):高度像签名校验,返回 1=通过;否则清理并标记失败。

  5. 发布/返回

    通过或未启用验签:把 v27 写到 a1ret=1;否则 ret=0。始终在尾部 unlock 并调用 sub_724FED1C34(...) 做收尾



返回值与边界

情况*a1返回
sub_724FEEF7F8() 失败无效0
成功构造 + 未启用验签指向对象1
成功构造 + 验签通过指向对象1
成功构造 + 验签失败无效(已清理)0

互斥保证多线程同时装载时不会踩踏。所有路径在末尾都会解锁收尾


伪代码

u64 sub_724FED1194(out_handle* a1, const void* name_or_ctx, void* payload, Ctx* a4) {
    Tmp tmp; sub_724FED1950(&tmp, a4);     // once 初始化/登记回调
    Mutex* m = pthread_once_01()+40;
    lock(m);

    Obj* obj = sub_724FEEF7F8(m, name_or_ctx, payload, 2, a4->flag0, a4->p66, a4->p2);
    u32 ok = 0;
    if (obj) {
        if (a4->pubkey && obj->magic == MAGIC_OBJ) {
            void* sig = obj->sig_ptr;      // *(v27 + 16)
            if (sub_724FEFF31C(sig, a4->pubkey, a4->pubkey_len, a4->p2) != 1) {
                sub_724FEEE508(pthread_once_01()+40, obj);   // 验签失败回收
                obj = NULL;
            }
        }
        if (obj) { *a1 = obj; ok = 1; }
    }

    unlock(m);
    sub_724FED1C34(&tmp, ...);             // 出场收尾
    return ok;                              // 1=成功 0=失败
}

上游负责解封装/对齐/定位 NAOP;本函数在持锁状态下做对象化 + 可选验签 + 发布;如果失败,上游会循环重试或走其他路径(与反调试守护线程并行存在)。

sub_724FED1194()once 初始化 → 互斥锁保护 → 对象构造 → 可选验签 → 发布/清理 → 收尾 各阶段展开


  1. 顶层时序
Caller
  |
  |  sub_724FED1194(a1_out, a2_nameCtx, dest_payload, a4_ctx)
  v
+-------------------- sub_724FED1194 ---------------------+
|  +----------------------------------------------+       |
|  | sub_724FED1950(tmp, a4_ctx)                  |       |
|  |   └─ pthread_once_01() -------------------+  |       |
|  |       └─ pthread_once(_bss_start__,      |  |       |
|  |                        sub_724FEE4418)   |  |       |
|  |       └─ return qword_724FF61108 (once)  |  |       |
|  +------------------------------------------+  |       |
|                                                |       |
|  once_base = pthread_once_01()                 |       |
|  mutex = (once_base + 40)                      |       |
|  pthread_mutex_lock(mutex)                     |       |
|                                                |       |
|  v27 = sub_724FEEF7F8(mutex, a2, dest, 2,      |       |
|                       *a4, (a4+66), (a4+2))    |       |
|          |                                     |       |
|          |-- 构造/解析对象(成功返回对象指针)--|       |
|                                                |       |
|  if (v27) {                                    |       |
|     if (*(a4+75) != 0) {        // 配置了公钥/策略  |       |
|        if (*(v27+12) == -839965817) {          |       |
|           v4 = *(v27+16);       // 签名/元数据指针  |       |
|           ok = sub_724FEFF31C(v4, *(a4+75),    |       |
|                                *(a4+76),       |       |
|                                (a4+2));        |       |
|           if (ok != 1) {                       |       |
|              sub_724FEEE508(once_base+40, v27);// 清理 |
|              v27 = NULL;                       |       |
|           }                                     |       |
|        } // else: 魔数不符 -> 跳过验签分支            |
|     } // else: 未启用验签 -> 直接通过               |
|                                                |       |
|     if (v27) { *a1_out = v27; ret=1; }         |       |
|     else      { ret=0; }                       |       |
|  } else {                                       |       |
|     ret = 0;                                    |       |
|  }                                              |       |
|                                                |       |
|  pthread_mutex_unlock(mutex)                    |       |
|  sub_724FED1C34(tmp, …)   // 出场收尾/擦痕          |       |
|  return ret (0/1)                               |       |
+--------------------------------------------------------+

  1. 进入阶段(once 初始化 + 取互斥量)
sub_724FED1194
  |
  +--> sub_724FED1950(tmp, a4)
  |      |
  |      +--> if (a4 && *(a4+616)) {
  |      |         once = pthread_once_01()
  |      |         *(once + 704) = sub_724FED6098   // once 回调/钩子
  |      |         *(once + 712) = a4              // 记住当前上下文
  |      |         tmp[0] = 1                      // 打开“已初始化”标志
  |      |    } else { tmp[0] = 0; }
  |      |
  |      +--> return tmp
  |
  +--> once_base = pthread_once_01()
  +--> mutex = (once_base + 40)
  +--> pthread_mutex_lock(mutex)

  1. 中段(对象构造 / 可选验签)
... 持锁中 ...
  v27 = sub_724FEEF7F8(mutex, a2, dest, 2, *a4, (a4+66), (a4+2))

  if v27 == NULL:
      ret = 0
  else:
      if *(a4+75) != 0:              // 有“验签材料”
          if *(v27+12) == -839965817 // 头部魔数/版本 OK
              v4 = *(v27+16)         // 指向签名/索引/证书描述
              ok = sub_724FEFF31C(v4, *(a4+75), *(a4+76), (a4+2))
              if ok != 1:            // 验签失败
                  sub_724FEEE508(once_base+40, v27)   // 回收对象/撤销注册
                  v27 = NULL
          else
              // 魔数不符:跳过验签分支(视为“不满足验签条件”,不会强退)
      // else 没有公钥/策略:不验签,直接通过

(a4+75) 极可能是公钥/策略指针(a4+76)长度/策略位(a4+2) 另一个上下文参数。(v27+12)839965817魔数/版本戳(v27+16)签名块地址


  1. 出段(发布/清理 + 收尾)
if v27 != NULL:
    *a1_out = v27
    ret = 1
else:
    ret = 0

pthread_mutex_unlock(mutex)
sub_724FED1C34(tmp, ...)    // 统一收尾(清理 tmp/日志/计数等)
return ret

  1. 分支视图
A) 构造失败
   sub_724FEEF7F8 → NULL
   └─ ret=0 → unlock → 收尾 → return 0

B) 构造成功 + 未启用验签
   v27 != NULL, *(a4+75) == 0
   └─ 直接 *a1=v27, ret=1 → unlock → 收尾 → return 1

C) 构造成功 + 启用验签 + 头部魔数匹配 + 验签通过
   v27 != NULL, *(a4+75)!=0, *(v27+12)==MAGIC, sub_724FEFF31C==1
   └─ *a1=v27, ret=1 → unlock → 收尾 → return 1

D) 构造成功 + 启用验签 + 魔数匹配 + 验签失败
   v27 != NULL, *(a4+75)!=0, *(v27+12)==MAGIC, sub_724FEFF31C!=1
   └─ sub_724FEEE508 清理, v27=NULL
      ret=0 → unlock → 收尾 → return 0





[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2025-10-12 17:02 被烬奇小云编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (8)
雪    币: 1907
活跃值: (1288)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
2
学习
2025-10-12 22:19
0
雪    币: 20
活跃值: (2731)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
有人和我一样,看到很多图片挂了吗?是论坛问题,还是我网问题,还是作者发图问题
2025-10-13 09:37
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
666
2025-10-13 09:45
0
雪    币: 1487
活跃值: (3478)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
5
123
2025-10-13 17:09
0
雪    币: 104
活跃值: (6838)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
tql
2025-10-13 17:20
0
雪    币: 104
活跃值: (6838)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
图是不是裂了?
2025-10-13 17:22
0
雪    币: 547
活跃值: (2469)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8
Imxz 图是不是裂了?
这个其实不影响,贴的是代码图片,如果影响观感,我后面补上
2025-10-14 16:50
0
雪    币: 547
活跃值: (2469)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
9
doduhuang 有人和我一样,看到很多图片挂了吗?是论坛问题,还是我网问题,还是作者发图问题[em_022]
我的问题,后面我补上吧,soory
2025-10-14 16:51
0
游客
登录 | 注册 方可回帖
返回