[color=]本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除。
前言
本人刚学习app方面的逆向几个星期,最近花了四天搞了一个简单的app,写一篇文章分享记录自己的逆向过程,方便自己后续回顾,也方便刚学习app的伙伴们入门练习,同时文章也还遗留了一些问题,如果大佬们有相关的文章和视频可以告送我,我再去学习学习。第一次写文章,大佬们不喜勿喷,欢迎技术指点,万分感谢!
正文
基本信息

image1.png (56.78 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
建议从谷歌商店下载,小米商城下载的好像有frida检测。
登录注册接口是某验4的一个无感,app端和web端好像都是一样的,本人已搞完了web的,不难,这一部分就不写出教程了,大佬们可以去web端玩玩。

image2.png (23.38 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
通过抓包可以看见,基本大多数的请求头中都会携带一个 x-signature ,因此接下来的文章内容以找到该参数的加密方式为主。

image3.png (20.34 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
查找参数
1. Frida检测
本人原先的流程是用jadx反编译apk,查看java层代码,写了一点hook脚本,发现无法注入才知道有frida检测的,为了方便文章查看,因此先写了frida检测这部分,后面再讲编写hook脚本部分。(本人没解决该app的frida检测,如果有大佬有类似的文章或者教程可以推荐给我)
首先第一步可以查看加载了哪写so文件,导致断开了frida
var dlopen = Module.findExportByName(null, "dlopen");//低版本
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");//高版本
Interceptor.attach(dlopen, {
onEnter: function (args) {
var path = args[0].readCString();
console.log("[dlopen:]", path);
},
onLeave: function (retval) {
}
});
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var path = args[0].readCString();
console.log("[dlopen_ext:]", path);
},
onLeave: function (retval) {
}
});
执行该脚本,可以看到加载了常见的 libmsaoaidsec.so

image4.png (279.25 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
然后我学习类似过该so文件的教程,查看检测的线程和查找偏移
function check_pthread_create(name = null) {
var pthread_create_addr = Module.findExportByName(null, 'pthread_create');
var pthread_create = new NativeFunction(pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]);
Interceptor.replace(pthread_create_addr, new NativeCallback(function (parg0, parg1, parg2, parg3) {
var module = Process.findModuleByAddress(parg2)
var so_base = module.base;
var off = "0x" + parg2.sub(so_base).toString(16)
var so_name = module.name;
console.log('[pthread_create] ',so_name, off, parg3)
return pthread_create(parg0, parg1, parg2, parg3);
}, "int", ["pointer", "pointer", "pointer", "pointer"]))
}
setImmediate(check_pthread_create)

image5.png (232.62 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
可是我后续 nop 掉相关的地址,还是没用。留着以后再解决吧......
用谷歌商店下载的无frida检测。
2. 参数查找
首先打开jadx,可以搜索查找相关关键词,如我搜索了 x-signature

image7.png (35.79 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
builder.add("X-Signature", KLookSignatureToolUtil.sign(bArr2, "kMtbID/p1?eWAsQ+5A3g="));
这里搜索到的大小写有点差异,不过问题不大,我们可以复制frida脚本,试试究竟会不会走这对应的函数。这两个都是走的KLookSignatureToolUtil.sign,双击进入

image8.png (20.39 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
String sign = KLookSignatureTool.sign(AppContextHolderKt.getAppContext(), data, key);
这里走的 KLookSignatureTool.sign,一样双击进入

image9.png (37.63 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
到此就可以先编写hook脚本查看是否是这几个进行的调用:
function hookSign(){
console.log("开始注入")
let SignatureHandlerImpl = Java.use("com.klooklib.flutter.bridgeimpl.SignatureHandlerImpl");
SignatureHandlerImpl["sign"].implementation = function (argument) {
console.log(`SignatureHandlerImpl.sign 调用: argument=${argument}`);
let result = this["sign"](argument);
console.log(`SignatureHandlerImpl.sign result=${result}`);
return result;
};
let KLookSignatureToolUtil = Java.use("com.klook.base_platform.security.KLookSignatureToolUtil");
KLookSignatureToolUtil["sign"].implementation = function (data, key) {
console.log(`KLookSignatureToolUtil.sign is 调用: data=${data}, key=${key}`);
let result = this["sign"](data, key);
console.log(`KLookSignatureToolUtil.sign result=${result}`);
return result;
};
let KLookSignatureTool = Java.use("com.klook.base_platform.security.KLookSignatureTool");
KLookSignatureTool["sign"].implementation = function (context, bArr, str) {
console.log(`KLookSignatureTool.sign is called: context=${context}, bArr=${bArr}, str=${str}`);
let result = this["sign"](context, bArr, str);
console.log(`KLookSignatureTool.sign result=${result}`);
return result;
};
}
function main(){
Java.perform(function (){
hookSign()
})
}
setImmediate(main);
把上述脚本进行注入,可以看到日志内容如下:

image10.png (90.86 KB, 下载次数: 0)
下载附件
2025-10-27 20:05 上传
通过日志可以看到是 KLookSignatureTool.sign 被成功调用的,说明该函数确实是最后生成的地方,因此可以确定到是 libklooktool.so 这个文件实现了这个方法。
使用ida打开该so文件,左侧搜索一下sign可以看到对应的函数方法:

image11.png (19.89 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
双击后按F5看相关代码,然后导入jni.h文件,再切换参数可以看到部分代码如下:

image12.png (54.23 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
分析(看不懂可以直接丢给AI):
[ol]
[/ol]
因此,我们可以跳过v8的返回值,直接去hook 函数 klook_api_sign_v2 的入参,就可以知道有没有拼接这段盐值。
进入函数 klook_api_sign_v2 可以看到:

image13.png (19.49 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
到这后可以先编写hook脚本,尝试打印 klook_api_sign_v2 的入参和返回值:
var targetSo = 'libklooktool.so'
var pollIntervalMs = 500;
var maxWaitMs = 60 * 1000;
function toHex(byteArray) {
if (!byteArray) return '';
var u8 = new Uint8Array(byteArray);
var out = [];
for (var i = 0; i ', klook_api_sign_v2_addr);
} catch (e) {}
Interceptor.attach(klook_api_sign_v2_addr, {
onEnter: function (args) {
// ARM64 convention: x0.. -> args[0], args[1], ...
this.input_ptr = args[0];
// 可能是 size_t,尽量兼容
try { this.len = args[1].toInt32(); } catch (e) { try { this.len = args[1].toInt64(); } catch (e2) { this.len = 0; } }
this.out_ptr = args[2];
try { this.out_len = args[3].toInt32(); } catch (e) { try { this.out_len = args[3].toInt64(); } catch (e2) { this.out_len = 0; } }
console.log('\n---- klook_api_sign_v2 ENTER ----');
console.log(' addr:', klook_api_sign_v2_addr, ' input_ptr=', this.input_ptr, ' len=', this.len, ' out_ptr=', this.out_ptr, ' out_len=', this.out_len);
// 读取 input,按 len 读取(限制上限防止 OOM)
try {
if (this.len > 0) {
var raw = Memory.readByteArray(this.input_ptr, this.len);
try {
var sutf = Memory.readUtf8String(this.input_ptr, this.len);
console.log(' input (utf8):', sutf);
} catch (e) {
console.log(' input (hex):', toHex(raw));
}
} else {
// 长度为 0 时尝试读取预览(64 bytes)
var pre = Memory.readByteArray(this.input_ptr, 64);
console.log(' input (preview hex):', toHex(pre));
}
} catch (e) {
console.log('[!] 读取 input 失败:', e);
}
},
onLeave: function (retval) {
console.log('---- klook_api_sign_v2 LEAVE ----');
// retval 一般是 int
try {
var r = retval.toInt32();
console.log(' retval (int):', r);
} catch (e) {
console.log('[!] 读取 retval 失败:', e);
}
// 读取 out buffer(优先 out_len,再 fallback 32)
try {
var outbytes = Memory.readByteArray(this.out_ptr, this.out_len);
var hex = toHex(outbytes);
console.log(' out bytes (hex len=' + this.out_len + '):', hex);
} catch (e) {
console.log('[!] 读取 out buffer 失败:', e);
}
console.log('---- klook_api_sign_v2 END ----\n');
}
});
console.log(' klook_api_sign_v2 hook 安装成功 :', klook_api_sign_v2_addr);
return true;
} catch (e) {
console.log('[!] tryHookModule 异常:', e);
return false;
}
}
function pollForModule() {
var start = Date.now();
var timer = setInterval(function () {
var mods = Process.enumerateModulesSync();
for (var i = 0; i maxWaitMs) {
clearInterval(timer);
console.log('[!] 超时未发现目标模块(' + targetSo + ')。');
}
}, pollIntervalMs);
}
setImmediate(function () {
console.log(' wait_and_hook 启动, targetSoHint =', targetSo);
// 先尝试一次立即查找
var mods = Process.enumerateModulesSync();
for (var i = 0; i
这里我做了一个定时循环的操作,我不能很精准的把控hook时机,大佬们如果有好的写法也可以告送我一声。
看到结果如下,传入参数似乎是 时间戳 + android + 设备id + 设备id + url(不同的接口组成方式好像不同)

image15.png (76.11 KB, 下载次数: 0)
下载附件
2025-10-27 20:18 上传
然后现在回过头去看 klook_api_sign_v2 的具体实现方式:
分析(看不懂就丢给AI):
[ol]
[/ol]
unk_2DF20肯定是常量,取了其中512个字节,然后传入给了klook_api_sign_base。想拿到完整的unk_2DF20,然后再取512字节对我来说挺难的,所以我直接去hook klook_api_sign_base的入参似乎也能达到相同的效果。在刚刚的代码块中新增代码:
var klook_api_sign_base_addr = null;
var dump_context_len = 512; // klook_api_sign_v2 里 dest 长度
try {
klook_api_sign_base_addr = Module.findExportByName(mod.name, 'klook_api_sign_base');
if (klook_api_sign_base_addr) console.log(' findExportByName 找到 klook_api_sign_base ->', klook_api_sign_base_addr);
} catch (e) {}
Interceptor.attach(klook_api_sign_base_addr, {
onEnter: function(args) {
// args[0] 应该是 context (512 bytes copy of unk_2DF20)
this.ctx_ptr = args[0];
this.input_ptr = args[1];
// len may be args[2] or args[2] is some jint, we will try both when reading
try { this.len = args[2].toInt32(); } catch(e) { try { this.len = args[2].toInt64(); } catch(e2) { this.len = 0; } }
this.out_ptr = args[3];
try { this.out_len = args[4].toInt32(); } catch (e) {
console.log("入参第四个参数转换错误:",e)
console.log("args[4]:",args[4])
this.out_len = 32;
}
console.log('---- klook_api_sign_base ENTER ----');
console.log(' ctx_ptr=', this.ctx_ptr, ' input_ptr=', this.input_ptr, ' len=', this.len, ' out_ptr=', this.out_ptr, ' out_len=', this.out_len);
try {
var fullctx = Memory.readByteArray(this.ctx_ptr, dump_context_len);
console.log('dest | 512 (hex):', toHex(fullctx));
} catch (e) {
console.log('[!] 读取 dest 失败:', e);
}
},
onLeave: function(retval) {
console.log('---- klook_api_sign_base LEAVE ----');
var outBuf = null;
try { console.log(' retval (int):', retval.toInt32()); } catch (e) { /* ignore */ }
try {
outBuf = Memory.readByteArray(this.out_ptr, this.out_len);
if (outBuf) {
var outHex = toHex(outBuf);
console.log(' out (hex full):',outHex);
} else {
console.log('[!] out 读取到空 buf');
}
} catch (e) {
console.log('[!] 读取 out buffer 失败:', e);
}
console.log('---- klook_api_sign_base END ----');
}
});
console.log(' klook_api_sign_base hook 安装成功 :', klook_api_sign_base_addr);
利用上述代码,可以看到dest的值都是相同的

image16.png (192.2 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
但是klook_api_sign_base并不只是传入了这一个参数,同时可以看看别的参数具体是什么,所以可以再次补充代码:
// 读取 input
try {
if (this.input_ptr && this.len && this.len > 0 ) {
try {
var s = Memory.readUtf8String(this.input_ptr, this.len);
console.log('完整 input (utf8):', s);
var raw = Memory.readByteArray(this.input_ptr, this.len);
var hex = toHex(raw);
console.log('完整 input (hex):', hex);
} catch (e) {
// 如果不是纯 UTF-8 文本(可能是二进制)
var raw = Memory.readByteArray(this.input_ptr, this.len);
hex = toHex(raw);
console.log('完整 input (hex):', hex);
console.log('klook_api_sign_base onEnter input Error :',e)
}
} else {
console.log('[!] input_ptr 或 len 无效, input_ptr=', this.input_ptr, ' len=', this.len);
}
} catch (e) {
console.log('[!] 读取 input 失败:', e);
}
得到日志内容如下。可以看到除了刚刚的dest,同时也把klook_api_sign_v2的参数一同传入。

image17.png (118.55 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
现在进入klook_api_sign_base内部,查看实现逻辑:

image18.png (110.9 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
分析:
[ol]
[/ol]
分析到这,可以对klook_api_sign_base的hook脚本进行补充,打印v8和v16的值分别是什么?同时去打印hmac_sha256的入参,可以确保分析的是否正确。
var klook_api_sign_base_addr = null;
var dump_context_len = 512; // klook_api_sign_v2 里 dest 长度
try {
klook_api_sign_base_addr = Module.findExportByName(mod.name, 'klook_api_sign_base');
if (klook_api_sign_base_addr) console.log(' findExportByName 找到 klook_api_sign_base ->', klook_api_sign_base_addr);
} catch (e) {}
Interceptor.attach(klook_api_sign_base_addr, {
onEnter: function(args) {
// args[0] 应该是 context (512 bytes copy of unk_2DF20)
this.ctx_ptr = args[0];
this.input_ptr = args[1];
// len may be args[2] or args[2] is some jint, we will try both when reading
try { this.len = args[2].toInt32(); } catch(e) { try { this.len = args[2].toInt64(); } catch(e2) { this.len = 0; } }
this.out_ptr = args[3];
try { this.out_len = args[4].toInt32(); } catch (e) {
console.log("入参第四个参数转换错误:",e)
console.log("args[4]:",args[4])
this.out_len = 32;
}
console.log('---- klook_api_sign_base ENTER ----');
console.log(' ctx_ptr=', this.ctx_ptr, ' input_ptr=', this.input_ptr, ' len=', this.len, ' out_ptr=', this.out_ptr, ' out_len=', this.out_len);
try {
var fullctx = Memory.readByteArray(this.ctx_ptr, dump_context_len);
console.log('dest | 512 (hex):', toHex(fullctx));
} catch (e) {
console.log('[!] 读取 dest 失败:', e);
}
// 读取 input
try {
if (this.input_ptr && this.len && this.len > 0 ) {
try {
var s = Memory.readUtf8String(this.input_ptr, this.len);
console.log('完整 input (utf8):', s);
var raw = Memory.readByteArray(this.input_ptr, this.len);
var hex = toHex(raw);
console.log('完整 input (hex):', hex);
} catch (e) {
// 如果不是纯 UTF-8 文本(可能是二进制)
var raw = Memory.readByteArray(this.input_ptr, this.len);
hex = toHex(raw);
console.log('完整 input (hex):', hex);
console.log('klook_api_sign_base onEnter input Error :',e)
}
} else {
console.log('[!] input_ptr 或 len 无效, input_ptr=', this.input_ptr, ' len=', this.len);
}
} catch (e) {
console.log('[!] 读取 input 失败:', e);
}
// 这里解析出 v8 和 v16
var v8 = 0;
for (var i = 0; i ('00000000' + x.toString(16)).slice(-8)).join(' '));
// 打印完整 32 字节 hex(小端顺序拼接)
var keyHex = v16.map(x => {
var b0 = (x & 0xFF).toString(16).padStart(2,'0');
var b1 = ((x >> 8) & 0xFF).toString(16).padStart(2,'0');
var b2 = ((x >> 16) & 0xFF).toString(16).padStart(2,'0');
var b3 = ((x >> 24) & 0xFF).toString(16).padStart(2,'0');
return b0 + b1 + b2 + b3;
}).join('');
console.log('v16 hex (32 bytes):', keyHex);
},
onLeave: function(retval) {
console.log('---- klook_api_sign_base LEAVE ----');
var outBuf = null;
try { console.log(' retval (int):', retval.toInt32()); } catch (e) { /* ignore */ }
try {
outBuf = Memory.readByteArray(this.out_ptr, this.out_len);
if (outBuf) {
var outHex = toHex(outBuf);
console.log(' out (hex full):',outHex);
} else {
console.log('[!] out 读取到空 buf');
}
} catch (e) {
console.log('[!] 读取 out buffer 失败:', e);
}
console.log('---- klook_api_sign_base END ----');
}
});
console.log(' klook_api_sign_base hook 安装成功 :', klook_api_sign_base_addr);
var hmac_sha256_addr = null;
try {
hmac_sha256_addr = Module.findExportByName(mod.name, 'hmac_sha256');
if (hmac_sha256_addr) console.log(' findExportByName 找到 hmac_sha256 ->', hmac_sha256_addr);
} catch (e) {}
Interceptor.attach(hmac_sha256_addr, {
onEnter: function(args) {
try {
console.log('---- hmac_sha256 ENTER ----');
this.a0 = args[0];
this.a1 = args[1].toInt32 ? args[1].toInt32() : parseInt(args[1]);
this.a2 = args[2];
this.a3 = args[3].toInt32 ? args[3].toInt32() : parseInt(args[3]);
this.a4 = args[4];
this.a5 = args[5].toInt32 ? args[5].toInt32() : parseInt(args[5]);
console.log(' arg0(ptr)=', this.a0, ' len=', this.a1);
console.log(' arg2(ptr)=', this.a2, ' len=', this.a3);
console.log(' arg4(ptr)=', this.a4, ' len=', this.a5);
var data0 = Memory.readByteArray(this.a0, this.a1);
console.log(' arg0(hex)=', toHex(data0));
var data2 = Memory.readByteArray(this.a2, this.a3);
console.log(' arg2(hex)=', toHex(data2));
var s = Memory.readUtf8String(this.a2, this.a3);
console.log(' arg2(utf8):', s);
var data4 = Memory.readByteArray(this.a4, this.a5);
console.log(' arg4(hex)=', toHex(data4));
} catch (e) {
console.log('[!] print args failed:', e);
}
},
onLeave: function(retval) {
try {
console.log('---- hmac_sha256 LEAVE ----');
try { console.log(' retval (int):', retval.toInt32()); } catch (e) { /* ignore */ }
// 尝试读取输出缓冲区
var outData = Memory.readByteArray(this.a4, this.a5);
console.log('hmac_sha256 output(hex)=', toHex(outData));
console.log('---- hmac_sha256 END ----');
} catch (e) {
console.log('hmac_sha256 ERROR :',e);
console.log('hmac_sha256 onLeave retval (raw) =', retval);
}
}
});
console.log(' hmac_sha256 hook 安装成功 :', hmac_sha256_addr);
查看日志:

image19.png (290.53 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
分析日志:
查看klook_api_sign_base部分的v8和v16跟刚刚的分析相同,32字节的v16也是传入到了hmac_sha256中,现在可以去一些在线的加密网站中去验证加密是否正确,防止对算法进行了一些魔改什么的。

image20.png (53.96 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
完美复现,是标准的加密,然后就可以使用python去还原加密流程了

image21.png (45.71 KB, 下载次数: 0)
下载附件
2025-10-27 20:06 上传
算法还原部分就不提供了,拿到完整的dest后可以让AI帮忙生成对应的代码就行了,后续接口请求也不再多说了。
结尾
第一次写文章,写的可能不是很好,大佬们见谅,如果有写的不对的地方可以告诉我一声,我再学学。中间提到的那个设备id,我也不知道是不是唯一的,不知道会不会有影响,也拜托各位不要用我这里显示的设备id去发请求,我怕.....
感谢!

