逆向分析TLV544协议的第一步,往往是从Java层寻找突破口。与大多数安卓应用协议不同,QQ客户端的代码混淆程度较低,这给我们提供了天然的分析便利。我曾在分析某电商App时,光是解混淆就花了三天时间,相比之下QQ的代码简直就像敞开的书本。
用JADX打开APK后,直接搜索"tlv544"关键词是最快捷的方式。实际操作中你会发现类似tlv_t544这样的类名,它们通常位于com.tencent.mobileqq包路径下。这里有个小技巧:不要只盯着类名看,注意观察类中的常量字符串。比如我最近分析的一个版本中,就发现了"qsec-t544-encrypt"这样的特征字符串,这就像犯罪现场留下的指纹一样明显。
通过代码追踪,你会发现调用链大致是这样的:
code复制TLV编码入口类 → 加密控制器 → JNI桥接类
具体到代码层面,通常会定位到com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion这个类。这个类名很有意思,直译是"蒲公英",可能暗示着加密数据像蒲公英种子一样扩散传播。其中的fly()方法是个关键转折点,它会通过JNI调用native层的energy()函数——这就是我们要找的加密函数入口。
很多逆向工程师第一次遇到动态注册的JNI函数时都会懵圈,因为它们在编译后的so文件中不会留下明显的函数符号。我清楚地记得第一次碰到这种情况时,对着IDA反编译结果发呆了两小时,直到发现RegisterNatives这个关键线索。
虽然我们知道目标很可能是动态注册,但先排除静态注册的可能性是严谨的做法。使用Frida hook dlsym函数是个有效方案:
javascript复制function hook_dlsym(){
let dlsymAddr = Module.findExportByName("libdl.so","dlsym");
Interceptor.attach(dlsymAddr,{
onEnter:function(args){
this.symbol = args[1];
},
onLeave:function(retval){
let module = Process.findModuleByAddress(retval);
if(module) console.log(`符号:${this.symbol.readCString()} 模块:${module.name} 地址:${retval}`);
}
})
}
这个脚本会打印出所有通过dlsym解析的函数符号。如果运气好,你可能会直接看到energy这样的关键函数名。但根据我的经验,在TLV544的场景下,这个方法往往无功而返。
当静态注册的路走不通时,就该祭出动态注册分析的大杀器了。Android系统中,所有JNI函数的动态注册最终都会调用RegisterNatives这个函数。不过要注意的是,不同Android版本这个函数的实现位置可能不同:
libdvm.solibart.so我整理了个兼容性更好的Hook方案:
javascript复制function findRegisterNatives() {
const libs = ['libart.so', 'libdvm.so'];
for (let lib of libs) {
let module = Process.findModuleByName(lib);
if (!module) continue;
let symbols = module.enumerateSymbols();
for (let symbol of symbols) {
if (symbol.name.includes('RegisterNatives')) {
return symbol.address;
}
}
}
throw new Error("RegisterNatives not found!");
}
let RegisterNatives_addr = findRegisterNatives();
Interceptor.attach(RegisterNatives_addr, {
onEnter: function(args) {
let jclass = args[1];
let className = Java.vm.tryGetEnv().getClassName(jclass);
let methods = args[2];
let count = args[3].toInt32();
for (let i = 0; i < count; i++) {
let methodPtr = methods.add(i * Process.pointerSize * 3);
let namePtr = methodPtr.readPointer();
let sigPtr = methodPtr.add(Process.pointerSize).readPointer();
let fnPtr = methodPtr.add(Process.pointerSize * 2).readPointer();
console.log(`类:${className} 方法:${namePtr.readCString()} 签名:${sigPtr.readCString()}`);
console.log(`函数地址:${fnPtr} 模块:${Process.findModuleByAddress(fnPtr)?.name || '未知'}`);
}
}
});
这个脚本不仅能捕获注册的native方法名,还能定位到对应的native函数地址。在实际分析中,我建议重点关注以下特征的方法:
encrypt、encode、energy等关键词[B(字节数组)参数或返回值libfekit.so)当通过上述方法找到目标函数地址后,真正的挑战才刚刚开始。以我最近分析的一个案例为例,energy函数位于libfekit.so的0x79134偏移处。这个so有几个特点需要注意:
用IDA分析时,我习惯先做这几件事:
sub_79134改为energy_impl)对于TLV544的加密逻辑,通常会在函数中看到以下特征:
在多次逆向TLV544协议后,我总结了一些实用技巧:
技巧1:字符串追踪法
在IDA中搜索qsec、t544等关键词,往往能快速定位到关键代码区域。最近一次分析中,我就是通过qsec_dandelion_encrypt这个字符串找到了加密函数的调用链。
技巧2:调用栈分析
在Frida中可以使用以下代码打印完整调用栈:
javascript复制console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n'));
技巧3:参数监控
对于疑似加密函数,可以记录其输入输出:
javascript复制Interceptor.attach(targetFunction, {
onEnter: function(args) {
this.input = args[1].readByteArray(args[2].toInt32());
},
onLeave: function(retval) {
console.log("输入:", bytesToHex(this.input));
console.log("输出:", bytesToHex(retval.readByteArray(16)));
}
});
常见坑点:
记得有次分析时,我花了半天时间Hook的函数其实是外层校验逻辑,真正的加密在更深层的函数里。后来发现是因为没注意到函数内部有个goto跳转到了关键代码段。