文章总结: 本文详细分析了安居客安卓应用的反调试机制,包括Frida检测的实现原理和绕过方法。作者通过逆向工程发现了libmsaoaidsec.so中的检测线程,提供了两种绕过方案:阻止库加载或直接替换检测函数。文章还深入解析了nsign参数的生成算法,揭示了其基于特定字符串、API路径和参数的MD5计算,并对结果进行特殊处理的过程。这些技术细节对于移动应用安全研究和爬虫开发具有实用价值。
综合评分: 85
文章分类: 逆向分析,移动安全,爬虫
【安卓逆向】安居客反调试与参数分析
原创
可乐还是百事好
爬虫逆向小林哥
2025年8月16日 09:00
江苏
集美语录
❝
“婆婆住院,老公让我拿彩礼钱应急,我没同意后老公记恨我怎么办”
介绍
ssl
本来想用frida脚本过下ssl的,发现有frida检测,直接用truastme吧
好了
frida检测
ida看下这个线程
void __noreturn sub_11E1C()
{
unsigned int v0; // r0
__useconds_t v1; // r4
int v2; // r0
v0 = sub_81FC();
if ( v0 < 0x64 )
v1 = 2000000;
else
v1 = v0;
while ( 1 )
{
v2 = sub_117F0();
if ( v2 == -1 || v2 && !sub_11630(v2, v2 + 1) || sub_11CF8() == 777 )
sub_BA20();
usleep(v1);
}
}
很像一个监测点啊
1 死循环 + usleep
2 检测函数三连
sub_117F0() // 读取 /proc/self/maps 或 /proc/self/task 等
sub_11630(...) // 字符串匹配(搜 “frida”、“gum-js-loop” 等)
sub_11CF8() == 777 // 第二个检测函数返回特定魔数 → 触发退出
解决方案可以看之前的文章,这里也可以直接把libmsaoaidsec android_dlopen_ext进入时候把path改空阻止加载
function hook_dlopen(soName) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(soName) >= 0) {
ptr(pathptr).writeUtf8String("");
}
}
}
});
}
hook_dlopen('libmsaoaidsec.so')
当然找到了这个循环检测的函数后,也可以把它Nop了,同时把闪退的函数BA20也杀了,也可以过检测
const TARGET_SO = 'libmsaoaidsec.so';
Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'), {
onEnter: function (args) {
this.path = Memory.readUtf8String(args[0]);
},
onLeave: function () {
if (this.path && this.path.indexOf(TARGET_SO) >= 0) {
const base = Module.findBaseAddress(TARGET_SO);
console.log(`[+] ${TARGET_SO} loaded @ ${base}`);
const loopOffset = 0x11E1C;
const loopAddr = base.add(loopOffset);
const exitOffset = 0xBA20;
const exitAddr = base.add(exitOffset);
Interceptor.replace(loopAddr, new NativeCallback(() => {
}, 'void', []));
Interceptor.replace(exitAddr, new NativeCallback(() => {
}, 'void', []));
}
}
});
done
nsign
请求头的nsign是我们分析的目标
Get:
c = SignUtil.c(path, null, b2, uuid);
参数
path=/mobile/v5/sale/property/list
uuid = UUID.randomUUID().toString();
b2 = params
Post:
c = SignUtil.c(path, c2, b2, uuid);
newBuilder2.add("body_md5", SignUtil.a(c2));
c2 = byte[](payload)
参数
str, a(bArr), hashMap, str2, bArr.length
str = path
a(bArr) = md5(byte[]) // get 为md5('')
hashMap = params key字符串 值是value的bytes
str2 = uuid
bArr.length byte[]长度
hook下静态注册,发现实在libsignutil.so里面
看下参数对不上,前两个字符串好像是从a2 jclass中读取的
v42 = a2;
v23 = (void *)v42;
v24 = (*a1)->GetStringUTFChars(a1, v42, 0);
v25 = (*a1)->GetStringUTFLength(a1, v23);
v26 = (void *)HIDWORD(v42);
v27 = (*a1)->GetStringUTFChars(a1, HIDWORD(v42), 0);
v28 = (*a1)->GetStringUTFLength(a1, v26);
o 函数是__fastcall,前2个32位参数通过寄存器传递
// 常规参数传递
R0(JNIEnv*), R1(jclass), [栈:str, str2, map, str3, i]
// __fastcall参数打包
R0(JNIEnv*), R1(高32位=str2, 低32位=str1), [栈:str, str2, map, str3, i]
从后往前看
v33 = a0123456789abcd[(unsigned __int8)a5 >> 4];
v34 = a0123456789abcd[a5 & 0xF];
v35 = a0123456789abcd[(a5 >> 8) & 0xF];
v36 = a0123456789abcd[(unsigned __int16)a5 >> 12];
v37 = a0123456789abcd[(unsigned __int8)v48 >> 4];
v49[36] = a0123456789abcd[(unsigned __int16)v48 >> 12];
v49[37] = a0123456789abcd[(v48 >> 8) & 0xF];
v49[38] = v37;
v49[39] = a0123456789abcd[v48 & 0xF];
v38 = a0123456789abcd[v30 & 0xF];
v39 = a0123456789abcd[(unsigned __int8)v30 >> 4];
v40 = a0123456789abcd[((unsigned int)v30 >> 8) & 0xF];
v49[40] = a0123456789abcd[(unsigned __int16)v30 >> 12];
v49[41] = v40;
v49[42] = v39;
v49[43] = v38;
v49[44] = v36;
v49[45] = v35;
v49[46] = v33;
v49[47] = v34;
*(_QWORD *)&v49[48] = *(_QWORD *)v47;
下面主要看看这个里的j_get_sign((int)v24, v29, (int)v27, v28, base, v43, (int)v49);
// 低32位读取java参数str
v24 = (*a1)->GetStringUTFChars(a1, v42, 0);
// str 的长度
v25 = (*a1)->GetStringUTFLength(a1, v23);
// 高32位读取java参数str2
v27 = (*a1)->GetStringUTFChars(a1, HIDWORD(v42), 0);
// str2 的长度
v28 = (*a1)->GetStringUTFLength(a1, v26);
// base 包含Map键值对数据的结构体指针
v7 = (*a1)->FindClass(a1, "java/util/HashMap");
v8 = (*a1)->GetMethodID(a1, v7, "size", "()I");
v43 = (*a1)->CallIntMethod(a1, (jobject)a3, v8);
base = &v42 - 2 * v43;
// 键值对的size
v43
//
v49 是一个 57位字节数组
进入md5函数内部,先hook下看看
Java.perform(function() {
var moduleName = "libsignutil.so";
var targetAddr = Module.findExportByName(moduleName, "SIGNMD5");
if (targetAddr) {
console.log("找到SIGNMD5函数: " + targetAddr);
Interceptor.attach(targetAddr, {
onEnter: function(args) {
// 记录参数
this.a1 = args[0];
this.a2 = args[1];
this.a3 = args[2]; // 这是存储MD5结果的缓冲区
console.log("[SIGNMD5] 调用参数:");
console.log(" a1: " + hexdump(this.a1, { length: parseInt(this.a2) }));
console.log(" a2: " + this.a2 + " (数据长度)");
console.log(" a3: " + this.a3 + " (结果缓冲区)");
},
onLeave: function(retval) {
console.log(" md5: " + hexdump(this.a3, { length: 32}));
}
});
} else {
console.log("未找到SIGNMD5函数");
}
});
参数: “bcb2e93c6b527180099601a2dd8ef8b1” + api + java传的str1 + map
标准的md5
主要看操作a7的地方
*(_DWORD *)a7 = 808464433 = 0x30303031; // 四字节
j_SIGNMD5(v18, v17, a7 + 4); // 将指针从首地址向后移动 4 字节
// 取刚刚md5生成结果的第一个字节的值
result = *(unsigned __int8 *)(a7 + 4);
*(_BYTE *)(a7 + 4 + (result & 0xF)) = result;
下面生成的md5首字节进行对应的调整
后面就算十六进制编码转换了
最后的v49[36] —> v49[47]
String str, String str2, Map<String, byte[]> map, String str3, int i
a5 = i = 0
v33 = a0123456789abcd[(unsigned __int8)a5 >> 4]; = '0'
v34 = a0123456789abcd[a5 & 0xF]; = '0'
v35 = a0123456789abcd[(a5 >> 8) & 0xF]; = '0'
v36 = a0123456789abcd[(unsigned __int16)a5 >> 12]; = '0'
v48
遍历HashMap中的每个键值对:
获取键的UTF-8长度 → v19
获取值的字节数组长度 → v20
累加到 v48
最终 v48= 所有键和值的总数据长度
v37 = a0123456789abcd[(unsigned __int8)v48 >> 4];
END
❝
有需要用于爬虫、逆向、抓包的测试机又不想花时间折腾Root、刷机、环境配置可以来看下下面联系方式,或者B站视频评论区见
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:爬虫逆向小林哥 可乐还是百事好《【安卓逆向】安居客反调试与参数分析》