一:apktool环境搭建 参考网络教程
febK9s2c8@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;
} 关键点说明:
TPIDR_EL0 + 40(0x28)v3 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);AArch64 的栈溢出保护(stack canary) 读出操作。编译器把 canary 放在 TLS(线程本地存储)里,函数入口读出并在函数尾对比,异常则调用 __stack_chk_fail。
判定 ART/Dalvik: strstr(v0, "art"):只要属性值里包含 “art” 子串(如 libart.so),即可判定 当前运行时是 ART ;否则多半是 Dalvik(libdvm.so ) 。
典型值:
Dalvik:libdvm.so ART:libart.so / libartd.so(带 art) **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") 的比较结果逻辑翻转(如把 BNE 改 BEQ),强制走另一分支。
留意栈保护
由于有 stack canary,盲目改局部栈数据可能触发 __stack_chk_fail 崩溃。做 inline hook/patch 时注意 保持函数序言/结尾完整 或使用 thumb/aarch64 安全补丁 。
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);
} 版本判定
命中 Android 12("S" 或 "12")→ 置状态常量 n1066837414 = -1370028921。同时若 SDK_INT > 32(Android 13+)就原地 busy-wait ,使程序“卡住”。这是非常典型的 反环境/反版本 行为(对 13+ 直接卡死)。
只针对 SDK_INT == 32 的次级检查
读取 ro.build.version.security_patch,搜索 "2022-02"。(某 CVE 在 2022-02 之前可利用)。
do … while (n1066837414 != 1066837414 && n1066837414 != 670287947)
这是扁平化状态机 的“主循环退出条件”。1066837414、670287947 是“好状态/出环状态”常量。
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)识别 1370028921、1066837414、670287947:典型的状态码/哈希常量 ,用于把 if/else 平铺为“写状态→循环判断状态→跳转到下一个块”。恢复办法:动态执行 或静态常量传播 ,把“状态写→下一次判断位置”连起来,还原为 if/else/switch。
绕过:改变系统属性返回值,把版本伪装成 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。
定点消除“挂死”条件 去掉 while (*sdk_bb > 32) ;:把比较条件改为永不成立 或把整个循环改为 NOP / BR 跳过。或在进入 if 分支后立即把 sdk_bb 写成 32 或更小(Frida 内存写)。
强制退出“状态机循环”
在进入 do { … } while(…) 前,把 n1066837414 写成 1066837414 或 670287947,直接一次性满 足退出条件:
// 先定位 n1066837414 的栈/寄存器保存位置(IDA 看函数栈布局/寄存器分配)
// 然后在 loop 上方插一个 Interceptor/inline hook 把该局部写好 更干净的“整函数短路” 直接 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/maps、MAGISKTMP、u: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 - 512,scan_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 == 0 → v51 = 1(检出 ),否则 v51 = 0
/proc/<pid>/attr/prev 是 SELinux 相关的进程属性文件。正常应用进程通常不会是 u:r:zygote:s0;这个检查较为“私货”。
关键绕过点 门闩 :dword_724FF60850 ≠ 218 → 完全不进入检测
Hook sub_724FEE4B08() 的返回值;或直接把该全局变量改写为非 218。
实质检测函数 :sub_724FF41074()
入口把前两条指令改为 MOV W0,#0; RET → 恒返回 0(未检出)。或仅改两处“置 1”的赋值点(命中 MAGISKTMP / 命中 u:r:zygote:s0),把它们 NOP 掉或改成置 0。也可在运行时 hook strstr :当 needle=="MAGISKTMP" 时强制返回 NULL,从而让栈扫描“看不见”。
字符串还原 :
若要静态还原所有常量,记住3 字节滚动 XOR 的 key 来自函数内固定 3 字节(i%3 取 key),对每个密文重复异或即可。
sub_724FF2ACEC()-入口函数
// 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() 本身就是个不透明谓词 外壳,最后返回某个全局 dword (dword_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 且 未检测到 Frida (sub_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) 这类“无限循环”线程推进周期自检/心跳。
字符串混淆与解析 字符串异或解密(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 取其中一字节完成还原。
/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.so (sub_724FF06B28() 就在 /proc/self/maps 查找此名)。
sub_724FF22A94:“ADB 模式时高频轮询电源插入状态(尤其 USB),一旦接通立即自杀退出 ”。此类线程常用 uevent/sysfs/battery 节点或 BroadcastReceiver/binder,但这里是 native,因此大概率通过读取 /sys/class/power_supply/ 或 battery_service 的 fd。
常量 出现场景 语义 218 sub_724FEE4B08() == 218 才做 Magisk 检测构建/策略开关:允许/禁止做 Root 检测 248 sub_724FEE4824() == 248是否启用第一组反调试线程初始化 249 sub_724FEE48A8() != 249是否再启用第二组线程检测 167 sub_724FEE47A4() == 167是否启用“电源/USB 触发自杀”线程
绕过位点 优先掐“门闩” (不进入反调试初始化)目标:让 sub_724FF47BB8() 返回 1 (即“环境不安全”),从而根本不走 sub_724FF2ACEC()。
两种打法:
最外层 :Hook sub_724FF47BB8,让它恒返回 1 。
内层门闩 :把 sub_724FEE4B08() 返回改为 非 218 (或把其全局 dword 改掉),使“Magisk 检测”分支不被执行,从而**sub_724FF47BB8() 返回 0
⇒ 最稳是直接让 sub_724FF47BB8() 返回 1 (假称“已检出 Root”,于是不上反调试 )。
若必须进入 sub_724FF2A478(),卡住线程创建 该函数通过 dlopen("libc.so") → dlsym("pthread_create") 获取指针再创建线程。
三种轻量拦截:
Hook dlsym :当符号为 "pthread_create" 时返回一个我们自定义的空实现地址 (NativeCallback,直接返回 0,不真正创建线程);
直接 Hook pthread_create 导出符号 :所有线程创建都“成功返回”但不执行 ;
或在 sub_724FF2A478 内部拿到的 pthread_create 指针被调用点上打 NOP 。
关掉分支门闩(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-线程创建
sub_724FF22A94()-电源/ADB 触发“自杀”的监控线程 + JNIEnv 获取 + ADB 判定
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.so 。dlopen + 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()-退出判断函数分析
一、执行路径 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->GetEnv 在 libart/libdvm 上“手搓”拿 JNIEnv*;sub_724FF13860() 几乎是空函数(返回 1)。
二、dongtai_exit(unsigned int a1):动态生成“退出”shellcode 关键逻辑:
shengchen_jqm(94) → 返回一个可执行函数指针 (内存来自 mmap(PROT_EXEC),并对其进行 XOR 解码 + __clear_cache)。立刻调用这个函数指针:fn(a1); 之后 munmap(addr, sysconf(40)) 清理这页内存(40 对应 _SC_PAGESIZE ,即页面大小); 返回。 参数 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.so 或 libdvm.so;dlsym("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 反调试 :
动态解密 "ptrace",dlsym 出 libc 里的 ptrace 指针; fork() 出子进程自跟踪 (PTRACE_TRACEME),父进程 waitpid 驻守并依据状态 WIFSTOPPED/EXITED/SIGNALED 做分支;还会在不同阶段对被追踪子进程 PTRACE_LISTEN/CONT;并行起线程 sub_724FF26ECC(a1=pid) 检查僵尸进程 等异常; 多处监看 TracerPid(自身是否被外部调试器附加); 任何一步“不对劲”就走 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, …) 循环;解析 status:status & 0xFF00(>>8 后是停止信号 ),保存到 pid_13/pid_12;判断 n6(一个“事件阶段计数器/状态计数器”)分多步推进:1→…→6,对应不同 PTRACE_* 与 wait 分支;在若干阶段对子进程 发 ptrace(PTRACE_LISTEN) 或 ptrace(PTRACE_CONT);如果 waitpid 或 ptrace 出错、或者收到了非预期的状态(比如不是 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_308:handle = 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/Hook :ptrace 函数指针来自 dlsym(且字符串解密),不走 PLT 重定位 ;换库路径也没用(直接吐“ptrace”名字去找)。抗静态分析 :扁平化 + 常量跳枢 + 线程/进程多路并行。
如何绕过 1:把“外部已调试/异常”的门闩关掉
让 TracerPid() 恒返回 0;或让 TracerPid_jc(x) 恒返回 0(假称“合法”)。优点:状态机继续往下走,但不会立刻 exit_jc()。
2:让 dlsym("ptrace") 返回 “假 ptrace”
Hook dlsym:当 symbol == "ptrace" 时,返回我们自制的 stub :
对 PTRACE_TRACEME、PTRACE_LISTEN、PTRACE_CONT 直接返回 0 (成功),但不做系统调用 (不占坑、不改变真实 ptrace 关系);父进程 waitpid 仍然能收到 SIGSTOP(由子进程自己的 raise(SIGSTOP) 触发),而我们的 ptrace 不会影响外部调试器;这样既不破坏它的状态机 ,也不给 kernel 建立真正的 ptrace 关系。注意:要确保“它期待的语义”能被满足(例如收到 WIFSTOPPED 后 PTRACE_LISTEN/CONT 也返回 0)。
3:入口 RET / 直接 return 0
4:自-ptrace 分支“空转”
允许 fork() 成功;在子进程路径 里让 ptrace(PTRACE_TRACEME) 的调用变成空操作 (Stub,返回 0);在父进程路径 里让针对 child 的 ptrace(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_jc 或 dongtai_exit 换空实现
fanxtiaoshi()-轮询哨兵分析
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 附加函数
TracerPid_jc-PID检测函数分析
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 的子(父)进程”,或限定进程名/路径;黑名单 :若命中 gdb、frida-server、lldb、android_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 栈搜索 MAGISKTMP 、ADB/供电状态线程 、自 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]|
+--------------------------------------------------------------+ 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 |
+------------------------------------------------------------+ 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=非法(外部调试/特征不符) |
+--------------------------------------------------------------------+ 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 |
+--------------------------------------------------------------+ 失败/成功(判定) +------------------ TracerPid() ------------------+
| -1 : 读取异常 → EXIT (exit_jc) |
| 0 : 无人跟踪 → OK |
| >0 : 有跟踪者 tpid |
+-------------------------+------------------------+
|
v
+------ TracerPid_jc(tpid) ------+
| return 1 : 合法自跟踪 → OK |
| return 0 : 非法/不匹配 → EXIT |
+--------------------------------+ 线程/组件关系 +------------------- 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
第一个可能遇见的问题: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 七:补充后续,过了反调试后干了什么
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)==0;suspiciousA/B≈ sub_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()风险 A returnsub_724FF47AD0()风险 B returnfrida_jc(env)命中 Frida returnmagic(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+66、a4+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) 细节分析 一次性初始化(sub_724FED1950)
传入 a2,将 sub_724FED6098 写到 pthread_once_01() 返回区域的 +704 偏移,将 a2 存到 +712;并把 result[0]=1。说明 once 区域扮演“全局控制块/回调注册 ”角色,sub_724FED6098 可能是析构/通知/日志回调 。
互斥保护
通过 pthread_once_01()+40 拿到一个 pthread_mutex_t*,配合 lock/unlock 包裹“构造+验签+发布”。这保证对象注册 的线程安全 与可见性 。
对象构造(sub_724FEEF7F8)
关键参数:(mutexa, a2, dest, 2, *a4, a4+66, a4+2)。风格像“把内存载荷 dest 解析为内部对象 ”,同时用 a2 作为名称/域,a4 等作为策略/模式,a4+66/a4+2 作为额外上下文/资源池。成功返回对象指针 v27。
验签/完整性
只有在 (a4+75) 非空时启用(理解为“提供了公钥/policy,就执行验签”)。先检查对象头 (v27+12) == -839965817(魔数/版本),再取 (v27+16) 作为签名/索引传给 sub_724FEFF31C。sub_724FEFF31C(v4, pubkey, keylen_or_flags, a4+2):高度像签名校验 ,返回 1=通过;否则清理并标记失败。
发布/返回
通过或未启用验签:把 v27 写到 a1,ret=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 初始化 → 互斥锁保护 → 对象构造 → 可选验签 → 发布/清理 → 收尾 各阶段展开顶层时序 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) | |
+--------------------------------------------------------+ 进入阶段(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) 中段(对象构造 / 可选验签) ... 持锁中 ...
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) 像签名块地址 。
出段(发布/清理 + 收尾) if v27 != NULL:
*a1_out = v27
ret = 1
else:
ret = 0
pthread_mutex_unlock(mutex)
sub_724FED1C34(tmp, ...) // 统一收尾(清理 tmp/日志/计数等)
return ret 分支视图 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
[培训]科锐软件逆向54期预科班、正式班开始火爆招生报名啦!!!
最后于 2025-10-12 17:02
被烬奇小云编辑
,原因: