算法分析
本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关;
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文涉及的技术而导致的任何意外,作者均不负责;
1. 概述
包名:com.hpbr.bosszhipin
版本:13.141
- 我们的目标是搜索,需要提前登录;算法整体难度算中等;
- 总体涉及md5、rc4、base64、LZ压缩这几个东西,需要提前了解一下这些算法;
2. 抓包&定位
- 首先进行抓包,确定目标参数;搜索之后找/api/batch/batchRunV2这个接口,主要目标是sp、sig、还有返回值看起来是加密的,接下来就逐个分析;

- 估计它们不一定在同一个位置,先看看吧,定位采用什么方式都可以,能定位到的方法都是好方法;
- 直接搜索定位看看,有没有比较显眼的位置;


- sig和sp都在,有可能是这里,继续跟一下就可以发下它们的native函数,我不放图了,把对应的结果写下来吧;
sig:private static native byte[] nativeSignature(byte[] bArr, String str);
sp: private static native String nativeEncodeRequest(byte[] bArr, String str);
private static final String LIB_NAME = "yzwg";
function showByteArray(byteArray, name) {
if (byteArray == null) return;
var result = Java.use('java.lang.String').$new(Java.array('byte', byteArray)).toString();
console.log(name + ": " + result);
}
function hook_sig() {
Java.perform(function () {
let com_twl_signer_YZWG = Java.use("com.twl.signer.YZWG");
com_twl_signer_YZWG["nativeSignature"].implementation = function (bArr, str) {
console.log(`[->] chook_sig ->str= ${str}`);
showByteArray(bArr, "hook_sig bArr");
var retval = this["nativeSignature"](bArr, str);
showByteArray(retval, "hook_sig 返回值")
return retval;
};
});
}
function hook_sp() {
Java.perform(function () {
let com_twl_signer_YZWG = Java.use("com.twl.signer.YZWG");
com_twl_signer_YZWG["nativeEncodeRequest"].implementation = function (bArr, str) {
console.log(`[->] hook_sp ->str= ${str}`);
showByteArray(bArr, "hook_sp bArr");
var retval = this["nativeEncodeRequest"](bArr, str);
console.log(`[<-] hook_sp retval= ${retval}`);
return retval;
};
});
}
hook_sig();
hook_sp();
- hook结果如下,我只展示和目标接口对应的部分,因为接口触发可能比较多;
[->] hook_sp ->str= null
hook_sp bArr: batch_method_feed=%5B%22method%3Dzpgeek.app.geek.search.cardlist%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26expectId%3D1261694622%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26isFromJd%3D0%26isSupplySearch%3Dfalse%26jids%3D%26maskComType%3D0%26naturalLanguageParam%3D%26noCorrect%3D0%26page%3D1%26prefix%3Dh%26pushId%3D%26pushType%3D%26query%3Dpython%26queryId%3D%26queryTitle%3D%25E6%2590%259C%25E7%25B4%25A2%25E5%258F%2591%25E7%258E%25B0%26searchType%3D3%26sort%3D-1%26source%3D1%26sugAbKey%3DM69-GroupA%26sugSessionId%3D1VpO529c1Ho%26userIntent%3D%22%2C+%22method%3Dzpgeek.app.search.listad.query%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26page%3D1%26query%3Dpython%26sort%3D-1%22%5D&client_info=%7B%22version%22%3A%2212%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221771739823352%22%2C%22resume_time%22%3A%221771739823352%22%2C%22channel%22%3A%2228%22%2C%22model%22%3A%22google%7C%7CPixel+6%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%223880525a-bdb9-41fb-b8bc-b38d1addd229%22%2C%22oaid%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22oaid_honor%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22did%22%3A%22DUYlEEPTkR_1kujGVuif0etPK8a41fRRHAa0RFVZbEVFUFRrUl8xa3VqR1Z1aWYwZXRQSzhhNDFmUlJIQWEwc2h1%22%2C%22tinker_id%22%3A%22Prod-arm64-v8a-release-13.141.1314110_0812-10-06-09%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22wifi%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A1%7D&curidentity=0&req_time=1771741084302&uniqid=3880525a-bdb9-41fb-b8bc-b38d1addd229&v=13.141
[<-] hook_sp retval= zwp_NCTRJTSwi3DeUsbkHIGNvU2MDIbyc_aI7pY7DTCAI7RtvzRtOHFgg3UWfOUMTO-UsQF6fAyw-rVGYJBSi6J1KX-FB70JplcewoyOd-dpWuhTl7gbHswG3N8n30lJuDL2_nFt0ONQG0-n42W9F8pD_iIs5mq-AKEi0RN8II4pDgJnH9pbEYKs0O0saFB8dqc6YRZJEJ0Iie7ZpgbwE2wwiQ_MU0Ko9XgrB6fBA_1dNFmp8z_--LNoBZBVyly3CrZasgjpOc5CqKwRiUrLgeb7BHBrymEAHlZDe_BQ8P1lQpGLHh-drdx6NqVHe86G_duGsfA0DoPuKyRaOSH75-PTb9Yub10pIs6LahfPutgSjC2BC09B1CSPjQPp8V2iQL20_x_unCd8sM5VGCTCr3fp9uIE18Rr7h2AMpAfS2xulAGlUjQhDFlyLkNvAWwbXzxVw8ax66GA9lv2bMdJVgs1HyvLF0rTX_ogaeu1FtgtPcGwzQcZ6ePWCAz2ciJdGH_u06080i88ensJkyDQDsnuuq-II60b4YmmKcHvUajXdbx0cDa1ycsUdnY-MDgKcLCbmgrDR5yX2dwcK7j8CzlC0SIQciE6Uc0SmPvpotX-eMKVGNlvblAg-XhXapAGzh7FwgfkDhGobrDNJ7lrCGu7IsKm-LAmuBPJUwV5QX0rWlkMM2xbo4UF1CskcpwwdPGEmg5eNxblPz3twdT_J6UqTMf63ORXTdROYuRT5D12saAMLm6gcjeaSk06KWura_DeVO_10mNKdk7yPf1rtwWuiG2WVwGKZLm7Oab_6NREKqkHMiXBc0d0r7hIWoC0Hi1aAB-h1boc91JBaZYLMN1tq3QWhDr_E0MrsjIHAO_jLhC56IcCQIYg44lmg3nvwrcbpqr6LzM6484_mTRummQWwOr5QFbqFtucP_elAVo5oNqbL_UrpbNjpcd_IdRn1wN4ak9HgBKWyXZvz1t-DNAGzA-EY-P_oP5etPcmoeO5C4Mt8eU-w_HO4aJljV0CQU_KnxyKB9Jfo_7o3E61W38LSlX6vxfaMQtmVDNjzERTXoMK73Q0KnWdd9RQEwn2bL1f9ruh2ngZvUG4PRaFp5w9dMq2-u-VU-0yjIJN81kbWcvHDoEVtT5MdubBYy3uNhetDoKY1NiiOFK7RWsDSB_K_a1ULaxaW2mun3Q4Qt8bkz4QyNwTF146dPhJxzrDQpx0ExNMvmBQRi7I9H-DY-ZJBLSIBn8tOSeb-2nWWN2cP_dl2Li9vk9jk5megfnb8LplyyA6WfD2MXhjYkVvUTgJ403oy0bYGYE7An3wOGOkmmK2Ip84EYPnKfcuMsihH-QUfUPzDGX-QWFzdj9X8GoEwevatOO0itUY7aMfLcW9icX0pziGDXIGv1rCo5xZSteXjc7besqHQpAxSkVKeqa2KQt33XmCjeJgE-Oh5X_ENG4bH7q5I9HLaLPBox7hSSUbq9NHay29dAZCPwujW9JnLsrm8BdACCpjO62Zl7IkKgoQ3ZgmQIkk1PYqUWSoOYMXQHCxL9SYRE4QcMs3UcUdw6_FWXokQalqSgqEwQfI0ZV4VqUkgf7qek59tggzKAA7N1eInXA_HXqX0wVNVznyrL0hkKXx50fmKxV7WrwitKRg2qXLr1I22-mhekVR8QUh-BiAog0Lqhf2mej1esVdBiqRq2CPmMJeN8I~
[->] chook_sig ->str= null
hook_sig bArr: /api/batch/batchRunV2batch_method_feed=%5B%22method%3Dzpgeek.app.geek.search.cardlist%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26expectId%3D1261694622%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26isFromJd%3D0%26isSupplySearch%3Dfalse%26jids%3D%26maskComType%3D0%26naturalLanguageParam%3D%26noCorrect%3D0%26page%3D1%26prefix%3Dh%26pushId%3D%26pushType%3D%26query%3Dpython%26queryId%3D%26queryTitle%3D%25E6%2590%259C%25E7%25B4%25A2%25E5%258F%2591%25E7%258E%25B0%26searchType%3D3%26sort%3D-1%26source%3D1%26sugAbKey%3DM69-GroupA%26sugSessionId%3D1VpO529c1Ho%26userIntent%3D%22%2C+%22method%3Dzpgeek.app.search.listad.query%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26page%3D1%26query%3Dpython%26sort%3D-1%22%5D&client_info=%7B%22version%22%3A%2212%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221771739823352%22%2C%22resume_time%22%3A%221771739823352%22%2C%22channel%22%3A%2228%22%2C%22model%22%3A%22google%7C%7CPixel+6%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%223880525a-bdb9-41fb-b8bc-b38d1addd229%22%2C%22oaid%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22oaid_honor%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22did%22%3A%22DUYlEEPTkR_1kujGVuif0etPK8a41fRRHAa0RFVZbEVFUFRrUl8xa3VqR1Z1aWYwZXRQSzhhNDFmUlJIQWEwc2h1%22%2C%22tinker_id%22%3A%22Prod-arm64-v8a-release-13.141.1314110_0812-10-06-09%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22wifi%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A1%7D&curidentity=0&req_time=1771741084302&uniqid=3880525a-bdb9-41fb-b8bc-b38d1addd229&v=13.141
hook_sig 返回值: V3.0f28a35d5baff6f9c44f199a292bece55
- 参数非常长,而且有url编码,解码一下,这里需要多次解码;
[->] hook_sp ->str= null
hook_sp bArr: batch_method_feed=["method=zpgeek.app.geek.search.cardlist&encryptExpectId=4617621d9b2479b31nZ82tu0FFRSwA~~&expectId=1261694622&filterParams={"cityCode":"101010100","chattedJob":0,"switchCity":0,"labelFilter":{"tags":[{"code":"-1","tag":"不限"}],"type":2}}&isFromJd=0&isSupplySearch=false&jids=&maskComType=0&naturalLanguageParam=&noCorrect=0&page=1&prefix=h&pushId=&pushType=&query=python&queryId=&queryTitle=搜索发现&searchType=3&sort=-1&source=1&sugAbKey=M69-GroupA&sugSessionId=1VpO529c1Ho&userIntent=", "method=zpgeek.app.search.listad.query&encryptExpectId=4617621d9b2479b31nZ82tu0FFRSwA~~&filterParams={"cityCode":"101010100","chattedJob":0,"switchCity":0,"labelFilter":{"tags":[{"code":"-1","tag":"不限"}],"type":2}}&page=1&query=python&sort=-1"]&client_info={"version":"12","os":"Android","start_time":"1771739823352","resume_time":"1771739823352","channel":"28","model":"google||Pixel 6","dzt":0,"loc_per":0,"uniqid":"3880525a-bdb9-41fb-b8bc-b38d1addd229","oaid":"00000000-0000-0000-0000-000000000000","oaid_honor":"00000000-0000-0000-0000-000000000000","did":"DUYlEEPTkR_1kujGVuif0etPK8a41fRRHAa0RFVZbEVFUFRrUl8xa3VqR1Z1aWYwZXRQSzhhNDFmUlJIQWEwc2h1","tinker_id":"Prod-arm64-v8a-release-13.141.1314110_0812-10-06-09","is_bg_req":0,"network":"wifi","operator":"UNKNOWN","abi":1}&curidentity=0&req_time=1771741084302&uniqid=3880525a-bdb9-41fb-b8bc-b38d1addd229&v=13.141
[<-] hook_sp retval= zwp_NCTRJTSwi3DeUsbkHIGNvU2MDIbyc_aI7pY7DTCAI7RtvzRtOHFgg3UWfOUMTO-UsQF6fAyw-rVGYJBSi6J1KX-FB70JplcewoyOd-dpWuhTl7gbHswG3N8n30lJuDL2_nFt0ONQG0-n42W9F8pD_iIs5mq-AKEi0RN8II4pDgJnH9pbEYKs0O0saFB8dqc6YRZJEJ0Iie7ZpgbwE2wwiQ_MU0Ko9XgrB6fBA_1dNFmp8z_--LNoBZBVyly3CrZasgjpOc5CqKwRiUrLgeb7BHBrymEAHlZDe_BQ8P1lQpGLHh-drdx6NqVHe86G_duGsfA0DoPuKyRaOSH75-PTb9Yub10pIs6LahfPutgSjC2BC09B1CSPjQPp8V2iQL20_x_unCd8sM5VGCTCr3fp9uIE18Rr7h2AMpAfS2xulAGlUjQhDFlyLkNvAWwbXzxVw8ax66GA9lv2bMdJVgs1HyvLF0rTX_ogaeu1FtgtPcGwzQcZ6ePWCAz2ciJdGH_u06080i88ensJkyDQDsnuuq-II60b4YmmKcHvUajXdbx0cDa1ycsUdnY-MDgKcLCbmgrDR5yX2dwcK7j8CzlC0SIQciE6Uc0SmPvpotX-eMKVGNlvblAg-XhXapAGzh7FwgfkDhGobrDNJ7lrCGu7IsKm-LAmuBPJUwV5QX0rWlkMM2xbo4UF1CskcpwwdPGEmg5eNxblPz3twdT_J6UqTMf63ORXTdROYuRT5D12saAMLm6gcjeaSk06KWura_DeVO_10mNKdk7yPf1rtwWuiG2WVwGKZLm7Oab_6NREKqkHMiXBc0d0r7hIWoC0Hi1aAB-h1boc91JBaZYLMN1tq3QWhDr_E0MrsjIHAO_jLhC56IcCQIYg44lmg3nvwrcbpqr6LzM6484_mTRummQWwOr5QFbqFtucP_elAVo5oNqbL_UrpbNjpcd_IdRn1wN4ak9HgBKWyXZvz1t-DNAGzA-EY-P_oP5etPcmoeO5C4Mt8eU-w_HO4aJljV0CQU_KnxyKB9Jfo_7o3E61W38LSlX6vxfaMQtmVDNjzERTXoMK73Q0KnWdd9RQEwn2bL1f9ruh2ngZvUG4PRaFp5w9dMq2-u-VU-0yjIJN81kbWcvHDoEVtT5MdubBYy3uNhetDoKY1NiiOFK7RWsDSB_K_a1ULaxaW2mun3Q4Qt8bkz4QyNwTF146dPhJxzrDQpx0ExNMvmBQRi7I9H-DY-ZJBLSIBn8tOSeb-2nWWN2cP_dl2Li9vk9jk5megfnb8LplyyA6WfD2MXhjYkVvUTgJ403oy0bYGYE7An3wOGOkmmK2Ip84EYPnKfcuMsihH-QUfUPzDGX-QWFzdj9X8GoEwevatOO0itUY7aMfLcW9icX0pziGDXIGv1rCo5xZSteXjc7besqHQpAxSkVKeqa2KQt33XmCjeJgE-Oh5X_ENG4bH7q5I9HLaLPBox7hSSUbq9NHay29dAZCPwujW9JnLsrm8BdACCpjO62Zl7IkKgoQ3ZgmQIkk1PYqUWSoOYMXQHCxL9SYRE4QcMs3UcUdw6_FWXokQalqSgqEwQfI0ZV4VqUkgf7qek59tggzKAA7N1eInXA_HXqX0wVNVznyrL0hkKXx50fmKxV7WrwitKRg2qXLr1I22-mhekVR8QUh-BiAog0Lqhf2mej1esVdBiqRq2CPmMJeN8I~
[->] hook_sig ->str= null
hook_sig bArr: /api/batch/batchRunV2batch_method_feed=["method=zpgeek.app.geek.search.cardlist&encryptExpectId=4617621d9b2479b31nZ82tu0FFRSwA~~&expectId=1261694622&filterParams={"cityCode":"101010100","chattedJob":0,"switchCity":0,"labelFilter":{"tags":[{"code":"-1","tag":"不限"}],"type":2}}&isFromJd=0&isSupplySearch=false&jids=&maskComType=0&naturalLanguageParam=&noCorrect=0&page=1&prefix=h&pushId=&pushType=&query=python&queryId=&queryTitle=搜索发现&searchType=3&sort=-1&source=1&sugAbKey=M69-GroupA&sugSessionId=1VpO529c1Ho&userIntent=", "method=zpgeek.app.search.listad.query&encryptExpectId=4617621d9b2479b31nZ82tu0FFRSwA~~&filterParams={"cityCode":"101010100","chattedJob":0,"switchCity":0,"labelFilter":{"tags":[{"code":"-1","tag":"不限"}],"type":2}}&page=1&query=python&sort=-1"]&client_info={"version":"12","os":"Android","start_time":"1771739823352","resume_time":"1771739823352","channel":"28","model":"google||Pixel 6","dzt":0,"loc_per":0,"uniqid":"3880525a-bdb9-41fb-b8bc-b38d1addd229","oaid":"00000000-0000-0000-0000-000000000000","oaid_honor":"00000000-0000-0000-0000-000000000000","did":"DUYlEEPTkR_1kujGVuif0etPK8a41fRRHAa0RFVZbEVFUFRrUl8xa3VqR1Z1aWYwZXRQSzhhNDFmUlJIQWEwc2h1","tinker_id":"Prod-arm64-v8a-release-13.141.1314110_0812-10-06-09","is_bg_req":0,"network":"wifi","operator":"UNKNOWN","abi":1}&curidentity=0&req_time=1771741084302&uniqid=3880525a-bdb9-41fb-b8bc-b38d1addd229&v=13.141
hook_sig 返回值: V3.0f28a35d5baff6f9c44f199a292bece55
- 简单对比可以发现,两个方法的bArr其实是一样的,只是sig的多了一个当前接口拼在最前面,而且两个字符串都是null,所以这两个参数描述的东西是一样的,只是形式不一样;
- 后续unidbg主动调用的时候记得传原始的参数,这里只是为了方便看而已,相对来说,响应的解密应当也是这个位置才对;
- 我们顺便也把它的函数找出来,我是比较偏向于它们都在同一个so的,而且附近也有类似的函数;

- 我猜测是选中的这两个函数之一,我们都hook上试试,看看是不是他们之一;
[->] nativeDecodeContent is called!
->bArr= -49,10,127,52,36,-47,37,52,-80,-117,112,-34,-61,-15,-28···省略部分数据···,123,-113,-3,-62
->str= null
->i11= 0
->i12= 1
->i13= 2
nativeDecodeContent retval: {"code":0,"message":"Success","zpData":{"zpgeek.app.geek.search.cardlist":{"code":0,"message":"Success","zpData":{"askAiEntrance":false,"cardList":[{"brandCardStyle":0,"hasMore":true,"jobCardStyle":0,"positionSearchCardList":[{"afterNameIcons":[{"height":48,"url":"https://img.bosszhipin.com/beijin/icon/20250107/b0d69dde1b861aef40e116820c982b289df0490b3b48e06fecc269e8d551a6fe1adc516614a77077.png","width":48}],"areaBusinessName":"望京","areaDistrict":"朝阳区","bossOnline":{"detailOnlin····省略部分数据····"zpData":{"listAds":[]}}}}
- 可以发现,确实走了,而且是参数比较多的那一个函数,到这里三个函数我们都定位好了;后续开始分析具体的算法,先把unidbg跑通;这里在unidbg调用的时候注意这几个int参数,有些时候第三个并不是2,自己注意就好了;
- 对了,bArr这个参数就是返回值,可以去cyberchef测试一下,是一样的;
3. 模拟执行
package com.Samples.boss;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.api.ClassLoader;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
public class YZWG extends AbstractJni implements IOResolver {
public static AndroidEmulator emulator;
public static Memory memory;
public static VM vm;
public static Module module;
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("sana file open-->>" + pathname);
return null;
}
public YZWG() {
emulator = AndroidEmulatorBuilder
.for64Bit()
.setProcessName("com.hpbr.bosszhipin")
.addBackendFactory(new Unicorn2Factory(false))
.build();
// 文件访问 注意这里的处理,加上这一句
emulator.getSyscallHandler().addIOResolver(this);
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("src/test/java/com/Samples/boss/files/BOSS直聘v13.141.apk"));
// 虚拟模块 添加这个就可以
// new AndroidModule(emulator, vm).register(memory);
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/Samples/boss/files/libyzwg.so"), true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
YZWG demo = new YZWG();
}
}
java.lang.UnsupportedOperationException: com/twl/signer/YZWG->gContext:Landroid/content/Context;
at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103)
at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:53)
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature){
case "com/twl/signer/YZWG->gContext:Landroid/content/Context;":{
return vm.resolveClass("android/content/Context").newObject(signature);
}
}
return super.getStaticObjectField(vm, dvmClass, signature);
}
java.lang.UnsupportedOperationException: android/content/pm/PackageManager->getPackagesForUid(I)[Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:937)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:871)
- 这个也直接补,实际上是获取包名,直接返回也未尝不可;
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "android/content/pm/PackageManager->getPackagesForUid(I)[Ljava/lang/String;":{
System.out.println("[+]sana getPackagesForUid uid-->>" + varArg.getIntArg(0));
return new ArrayObject(new StringObject(vm, vm.getPackageName()));
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
java.lang.UnsupportedOperationException: java/lang/String->hashCode()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:969)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:942)
- 这个获取一个hashcode,后续可能会用上,可以留意一下;
@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/lang/String->hashCode()I":{
int value = dvmObject.getValue().hashCode();
return value;
}
}
return super.callIntMethod(vm, dvmObject, signature, varArg);
}
RegisterNative(com/twl/signer/YZWG, nativeEncodePassword(Ljava/lang/String;)Ljava/lang/String;, RX@0x4001f650[libyzwg.so]0x1f650)
RegisterNative(com/twl/signer/YZWG, nativeEncodeData([BLjava/lang/String;)Ljava/lang/String;, RX@0x400200c0[libyzwg.so]0x200c0)
RegisterNative(com/twl/signer/YZWG, nativeEncodeRequest([BLjava/lang/String;)Ljava/lang/String;, RX@0x400209a4[libyzwg.so]0x209a4)
RegisterNative(com/twl/signer/YZWG, nativeSignature([BLjava/lang/String;)[B, RX@0x40021864[libyzwg.so]0x21864)
RegisterNative(com/twl/signer/YZWG, nativeDecodeContent(Ljava/lang/String;Ljava/lang/String;)[B, RX@0x40022ad0[libyzwg.so]0x22ad0)
RegisterNative(com/twl/signer/YZWG, nativeDecodeContent([BLjava/lang/String;III)[B, RX@0x40024dc8[libyzwg.so]0x24dc8)
RegisterNative(com/twl/signer/YZWG, nativeDecodePassword(Ljava/lang/String;)[B, RX@0x4001fb4c[libyzwg.so]0x1fb4c)
- 参数就用之前给出的那一个吧;一共两个参数,这里用api来调用,比较方便;
public void call_sig() {
byte[] bArr = "/api/batch/batchRunV2batch_method_feed=%5B%22method%3Dzpgeek.app.geek.search.cardlist%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26expectId%3D1261694622%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26isFromJd%3D0%26isSupplySearch%3Dfalse%26jids%3D%26maskComType%3D0%26naturalLanguageParam%3D%26noCorrect%3D0%26page%3D1%26prefix%3Dh%26pushId%3D%26pushType%3D%26query%3Dpython%26queryId%3D%26queryTitle%3D%25E6%2590%259C%25E7%25B4%25A2%25E5%258F%2591%25E7%258E%25B0%26searchType%3D3%26sort%3D-1%26source%3D1%26sugAbKey%3DM69-GroupA%26sugSessionId%3D1VpO529c1Ho%26userIntent%3D%22%2C+%22method%3Dzpgeek.app.search.listad.query%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26page%3D1%26query%3Dpython%26sort%3D-1%22%5D&client_info=%7B%22version%22%3A%2212%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221771739823352%22%2C%22resume_time%22%3A%221771739823352%22%2C%22channel%22%3A%2228%22%2C%22model%22%3A%22google%7C%7CPixel+6%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%223880525a-bdb9-41fb-b8bc-b38d1addd229%22%2C%22oaid%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22oaid_honor%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22did%22%3A%22DUYlEEPTkR_1kujGVuif0etPK8a41fRRHAa0RFVZbEVFUFRrUl8xa3VqR1Z1aWYwZXRQSzhhNDFmUlJIQWEwc2h1%22%2C%22tinker_id%22%3A%22Prod-arm64-v8a-release-13.141.1314110_0812-10-06-09%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22wifi%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A1%7D&curidentity=0&req_time=1771741084302&uniqid=3880525a-bdb9-41fb-b8bc-b38d1addd229&v=13.141".getBytes();
// 调用静态native方法
DvmObject<?> result = YZWG.callStaticJniMethodObject(
emulator,
"nativeSignature([BLjava/lang/String;)[B",
bArr,
null
);
//获取并返回结果
byte[] res = (byte[]) result.getValue();
System.out.println("[+]call_sig result:" + new String(res, StandardCharsets.UTF_8));
}
[+]call_sig result:
V3.0f28a35d5baff6f9c44f199a292bece55
抓包:V3.0f28a35d5baff6f9c44f199a292bece55
public void call_sp(){
byte[] bArr1 = "batch_method_feed=%5B%22method%3Dzpgeek.app.geek.search.cardlist%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26expectId%3D1261694622%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26isFromJd%3D0%26isSupplySearch%3Dfalse%26jids%3D%26maskComType%3D0%26naturalLanguageParam%3D%26noCorrect%3D0%26page%3D1%26prefix%3Dh%26pushId%3D%26pushType%3D%26query%3Dpython%26queryId%3D%26queryTitle%3D%25E6%2590%259C%25E7%25B4%25A2%25E5%258F%2591%25E7%258E%25B0%26searchType%3D3%26sort%3D-1%26source%3D1%26sugAbKey%3DM69-GroupA%26sugSessionId%3D1VpO529c1Ho%26userIntent%3D%22%2C+%22method%3Dzpgeek.app.search.listad.query%26encryptExpectId%3D4617621d9b2479b31nZ82tu0FFRSwA%257E%257E%26filterParams%3D%257B%2522cityCode%2522%253A%2522101010100%2522%252C%2522chattedJob%2522%253A0%252C%2522switchCity%2522%253A0%252C%2522labelFilter%2522%253A%257B%2522tags%2522%253A%255B%257B%2522code%2522%253A%2522-1%2522%252C%2522tag%2522%253A%2522%25E4%25B8%258D%25E9%2599%2590%2522%257D%255D%252C%2522type%2522%253A2%257D%257D%26page%3D1%26query%3Dpython%26sort%3D-1%22%5D&client_info=%7B%22version%22%3A%2212%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221771739823352%22%2C%22resume_time%22%3A%221771739823352%22%2C%22channel%22%3A%2228%22%2C%22model%22%3A%22google%7C%7CPixel+6%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%223880525a-bdb9-41fb-b8bc-b38d1addd229%22%2C%22oaid%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22oaid_honor%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22did%22%3A%22DUYlEEPTkR_1kujGVuif0etPK8a41fRRHAa0RFVZbEVFUFRrUl8xa3VqR1Z1aWYwZXRQSzhhNDFmUlJIQWEwc2h1%22%2C%22tinker_id%22%3A%22Prod-arm64-v8a-release-13.141.1314110_0812-10-06-09%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22wifi%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A1%7D&curidentity=0&req_time=1771741084302&uniqid=3880525a-bdb9-41fb-b8bc-b38d1addd229&v=13.141".getBytes();
//调用静态native方法
DvmObject<?> result = YZWG.callStaticJniMethodObject(
emulator,
"nativeEncodeRequest([BLjava/lang/String;)Ljava/lang/String;",
bArr1,
null
);
//获取并返回结果
String res = (String) result.getValue();
System.out.println("[+]call_sp result:" + res);
}
- 太长了,不写出结果了,也是不用再补环境,直接就有结果,而且和抓包对得上;

- 顺便把返回值的调用也写了,这个也不需要补环境,但是数据太占篇幅了,不写了;不过这里需要注意几个点,首先是原始返回值太长了,我们需要寻找短一点的来调用;并且不同的长度最后一个int参数是不一样的;
public void decode_response() {
byte[] bArr = {-10, 114, 76, 25, 44, -37, 100, 101, -128, -89, 82, -77, -46, -79, -105, 125, -105, -32, -97, 119, 58, 83, -13, -111, -30, -102, -103, -4, -64, 116, 71, 21, -99, 2, -95, 113, -79, 114, 8, 37, 105, 120};
int i11 = 0;
int i12 = 1;
int i13 = 0; // 短的是0 长的是2
//调用静态native方法
DvmObject<?> result = YZWG.callStaticJniMethodObject(
emulator,
"nativeDecodeContent([BLjava/lang/String;III)[B",
bArr,
null,
i11,
i12,
i13
);
//获取并返回结果
byte[] res3 = (byte[]) result.getValue();
System.out.println("result: " + new String(res3));
}

4. 算法分析
4.1 sig
- 先来分析sig参数,把其他两个调用先停了,老规矩,打开memcpy的hook;并且把密文结果转成hexdump,方便分析;
00000000 66 32 38 61 33 35 64 35 62 61 66 66 36 66 39 63 |f28a35d5baff6f9c|
00000010 34 34 66 31 39 39 61 32 39 32 62 65 63 65 35 35 |44f199a292bece55|
>-----------------------------------------------------------------------------<
[16:19:06 757]目标: 40713060 来源(主要追这里): bfffef1a lr 40578dc4, md5=3667f6a0c97490758d7dc9659d01ea34, hex=6632
size: 2
0000: 66 32 f2
^-----------------------------------------------------------------------------^
>-----------------------------------------------------------------------------<
[16:19:06 758]目标: 40713062 来源(主要追这里): bfffef1a lr 40578dc4, md5=8edd87c407c4a1732e4924598236e319, hex=3861
size: 2
0000: 38 61 8a
^-----------------------------------------------------------------------------^
>-----------------------------------------------------------------------------<
[16:19:06 758]目标: 40713064 来源(主要追这里): bfffef1a lr 40578dc4, md5=1c383cd30b7c298ab50293adfecb7b18, hex=3335
size: 2
0000: 33 35 35
^-----------------------------------------------------------------------------^
- 可以发现一些蛛丝马迹,那可以去找一下写入位置的信息;
emulator.traceWrite(0xbfffef1aL, 0xbfffef1aL + 0x10);

- pc是libc的函数,这种一般就不太好继续追了,可以打开unidbg的libc看看是什么函数,它实际上是一个_vfprintf函数;
- 这里需要另辟蹊径,回去看最后的日志;
JNIEnv->SetByteArrayRegion([B@0x000000000000000000000000000000000000000000000000000000000000000000000000, 0, 36, RW@0x40713090) was called from RX@0x40021fdc[libyzwg.so]0x21fdc
[+]call_sig result:V3.0f28a35d5baff6f9c44f199a292bece55

- 第一眼确实看不出来什么,但这里我们追的是SetByteArrayRegion,这里并没有看到,那么v8基本上就是jnienv,+1664实际上是偏移,我们改一下类型就可以看出来了;

- 最后一个参数是源数据指针,所以去找它,v29又等于v27,所以去hook sub_1C38C这个函数;他一共有两个参数;


- 参数1看起来应该是前缀,参数2则是结果,那还需要继续追,也就是去hook 0x1C714这个函数;它也是两个参数,并且我们需要去看它的返回值;
mx0 0x8a6
>-----------------------------------------------------------------------------<
[16:30:02 318]x0=RW@0x40719000, hex=2f617069·····261746368
size: 2214
0000: 2F 61 70 69 2F 62 61 74 63 68 2F 62 61 74 63 68 /api/batch/batch
·····这一部分数据省略了
·····这一部分数据省略了
0810: 4E 4B 4E 4F 57 4E 25 32 32 25 32 43 25 32 32 61 NKNOWN%22%2C%22a
0820: 62 69 25 32 32 25 33 41 31 25 37 44 26 63 75 72 bi%22%3A1%7D&cur
0830: 69 64 65 6E 74 69 74 79 3D 30 26 72 65 71 5F 74 identity=0&req_t
0840: 69 6D 65 3D 31 37 37 31 37 34 31 30 38 34 33 30 ime=177174108430
0850: 32 26 75 6E 69 71 69 64 3D 33 38 38 30 35 32 35 2&uniqid=3880525
0860: 61 2D 62 64 62 39 2D 34 31 66 62 2D 62 38 62 63 a-bdb9-41fb-b8bc
0870: 2D 62 33 38 64 31 61 64 64 64 32 32 39 26 76 3D -b38d1addd229&v=
0880: 31 33 2E 31 34 31 61 33 30 38 66 33 36 32 38 62 13.141a308f3628b
0890: 33 66 33 39 66 37 64 33 35 63 64 65 62 65 62 36 3f39f7d35cdebeb6
08A0: 39 32 30 65 32 31 920e21

- 是结果无疑,并且参数是我们传递的东西,但是结尾附加了一点东西,a308f3628b3f39f7d35cdebeb6920e21;
- 看起来是一个盐,先不讨论他是否固定,先分析一下算法,32位的结果我很难不往那方面想;去测试一下;

- 我们猜的不错,sig是一个加盐的md5算法,往深处稍微跟一下也可以看到关键的md5_transform函数;
- 我分析了一下,应该是固定写死的,具体的位置是这块;

4.2 sp
- 接下来分析sp,在分析我们最好是做一点简单的拆解,这么长的密文,大概率不会是哈希,极有可能是对称加密,要么是分组要么是流加密;无论哪种,我们都不希望分析的时候有非常多的分组,所以我在开始分析前将明文改成短一点的形式,改成什么无所谓,就用我前面的 call 就好,短一点比较好分析,而且我们验证过了unidbg跑出来结果是没问题的;
- 执行结果:
JNIEnv->NewStringUTF("zwp_NCTRJTSwi3DepMLkHOGFvU0aAIbycf2I7pY7DTCAI7RtvzRtOHFggw~~") was called from RX@0x40021020[libyzwg.so]0x21020
[+]call_sp result:zwp_NCTRJTSwi3DepMLkHOGFvU0aAIbycf2I7pY7DTCAI7RtvzRtOHFggw~~


- 先hook 1CEB8看看;它有两个参数,第二个是一个长度;


- 返回值就是最终的结果,那么这个函数只是把base64数据做了一点处理;
/ -> _
+ ->-
= -> ~
- 那它前面的sub_29E90函数大概率就是base64函数了;继续hook看看,它有三个参数,参数1是缓冲区,参数3是长度,应该是参数2的长度;



- 确实是base64的结果,我们去看看是不是标准的base64;

- 是标准base64无疑,所以接下来的问题就在于这段明文是什么?
0000: CF 0A 7F 34 24 D1 25 34 B0 8B 70 DE A4 C2 E4 1C ...4$.%4..p.....
0010: E1 85 BD 4D 1A 00 86 F2 71 FD 88 EE 96 3B 0D 30 ...M....q....;.0
0020: 80 23 B4 6D BF 34 6D 38 71 60 83 .#.m.4m8q`.

- hook看看情况,他一共有四个参数,参数二和三是一样的;参数四是0x2b;



- 结果确实是我们需要找的base64的明文那一部分,没有什么可看的了,进这个函数看看吧;

- 总体不长,稍微熟悉一点的朋友应该能看出来是什么算法;
*(data2 + v11) = (~v9 & 0x7B | v9 & 0x84) ^ (~*(data + v11) & 0x7B | *(data + v11) & 0x84)
*(data2 + v11) = v9 ^ *(data + v11)
- 不仅是异或,还有256,循环退出条件也是明文长度,他应该是一个rc4算法;
void rc4_enc(unsigned char *S_Box, char *data, int data_len) {
int i = 0, j = 0, t = 0;
for (int k = 0; k < data_len; k++) {
i = (i + 1) % 256;
j = (j + S_Box[i]) % 256;
swap(S_Box[i], S_Box[j]);
t = (S_Box[i] + S_Box[j]) % 256;
data[k] = data[k] ^ S_Box[t];
}
}
- 其实是比较相似的了,参数1应该就是S盒,参数2、3就应该是明文;
- 还应该有一个打乱s盒的函数,想必就是上面这个sub_2E680函数了;

- hook看看,有三个参数,参数1不知道,参数2应该是key、参数3是key的长度;

- 是否有些眼熟,这个key是前面sig的盐;那我们测试一下结果是否是我们所想的那样;

- 确实是标准的rc4算法,所以base64的明文就来自于rc4算法的加密结果;这个key想必也是对照的,那么,rc4的明文又是什么?
- 虽然看起来和我们的明文差不多,但是还是有区别的;
0000: 42 5A 50 42 6C 6F 63 6B 00 00 00 00 13 00 00 00 BZPBlock........
0010: 11 00 00 00 02 00 00 00 F0 02 62 61 74 63 68 5F ..........batch_
0020: 6D 65 74 68 6F 64 5F 66 65 65 64 method_feed
- 明文是:batch_method_feed,看起来差不多;找一下这一部分怎么来的,去看看交叉引用;


- 有个j_LZ4_compressBound函数,这是LZ4压缩的标志,搜索一下这个函数是什么;
int LZ4_compressBound(int inputSize);
- 作用:计算给定输入字节数
inputSize 经 LZ4 压缩后可能产生的最大输出尺寸;
- hook一下sub_1D444函数看看,毕竟有目标;他一共有四个参数;后面两个都是长度,参数3应该是明文的长度;


- x1是缓冲区,运行结束后确实是我们要找的那一部分数据,进去函数内部看看;

- 一进来就可以看到一个LZ4的函数,结合前面的符号我们猜测可能是一个LZ4压缩,我们尝试去压缩一下看看是否是标准的;

- 看起来并不一致,熟悉压缩算法的朋友应该知道,压缩算法都有一个魔数,用于分辨是哪一个压缩算法;
- LZ4是4字节,小端,值为0x184D2204,可以和标准的对比看看,确实是这样;但是我们的结果很明显不一样,这里我们让ai描述一下LZ4压缩后的组成;
- 结果属于是完全不对了,应该是魔改了这部分,看看代码部分吧,我写了一点点注释;

-
压缩成功后的结果并没有放在a2的位置,而是往后挪了24个字节,然后自定义了24 字节头部;
-
解析一下这部分自定义,其实就在下方高亮处;先是8 字节魔数(非标准),也就是固定的0x6B636F6C42505A42,这里是小端序的,然后是4 字节保留字段(置 0),压缩后长度(4 字节),然后是原始数据长度(4 字节),再就是原始长度 ^ 压缩长度(4 字节校验);
-
汇总一下:
- 偏移 0:8 字节魔数 42 5a 50 42 6c 6f 63 6b(固定);
- 偏移 8:4 字节保留字段(置 0);
- 偏移 12:压缩后长度v12(4 字节);
- 偏移 16:原始数据长度v5(4 字节);
- 偏移 20:
原始长度 ^ 压缩长度(4 字节);
-
实际上这个压缩只是改了头部那一部分,其他的还是标准的,所以我们如果需要解压缩的话需要去掉对应的24字节,然后再解压缩;这一部分我踩坑了,传参出问题了导致一直出错,搞得我还以为不是标准的压缩;
-
稍微总结一下sp的算法:
- 首先是LZ4压缩明文,但是这里改了一点头部拼接,就在上面有具体改了什么;
- lz4压缩后进行RC4加密,秘钥是sig那个盐:a308f3628b3f39f7d35cdebeb6920e21;
- 最后进行base64编码,码表稍微有点改动,也可以标准编码后替换那几个符号;
-
这个需要了解LZ4压缩的细节,我也不是特别熟悉,放一个解密sp的demo在这,可以自行测试;
import base64
import struct
import lz4.block
from Crypto.Cipher import ARC4
def base64_decode_custom(b64_str: str) -> bytes:
"""
自定义Base64解码(码表: A-Za-z0-9-_~)
"""
BASE64_ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_~"
cleaned = ''.join(c for c in b64_str if c in BASE64_ALLOWED).replace('~', '=')
cleaned += '=' * ((4 - len(cleaned) % 4) % 4)
return base64.urlsafe_b64decode(cleaned.encode())
def rc4_decrypt(data_bytes: bytes, key_hex: str = "a308f3628b3f39f7d35cdebeb6920e21") -> bytes:
key_bytes = key_hex.encode()
return ARC4.new(key_bytes).decrypt(data_bytes)
def lz4_decompress_custom(packed_data: hex) -> bytes:
"""
还原 sub_1D444:解析头部 + LZ4解压
:param packed_data: 封装后的压缩数据
:return: 原始明文
"""
if len(packed_data) < 24:
raise ValueError("数据太短")
magic, _, comp_len, orig_len, chk = struct.unpack('<QIIII', bytes.fromhex(packed_data)[:24])
if magic != 0x6B636F6C42505A42 or chk != (orig_len ^ comp_len):
raise ValueError("校验失败:非目标格式或数据损坏")
compressed = bytes.fromhex(packed_data)[24:24 + comp_len]
return lz4.block.decompress(compressed, uncompressed_size=orig_len)
if __name__ == "__main__":
B64_STRING = "xxx"
decoded = base64_decode_custom(B64_STRING)
decrypted = rc4_decrypt(decoded)
result = lz4_decompress_custom(decrypted.hex())
print(result.decode())
- 所以sp=base64( RC4 (魔改LZ4压缩 (urlpath+请求参数) ) ),总体就是这个压缩可能费点时间,剩下的就是响应的解密了;
4.3 响应解密

- 信息不多,但是还是有一点,这个0xf6724c192c是什么?感觉应该是密文的十六进制,转一下看看;


- 进来还是自己把类型改正一下,正好落在整个解密函数里,大概有几百行,我们稍微看看吧;把鼠标放在参数1上,看看交叉引用;

- 我个人觉得获取长度和它本身是比较敏感的操作,也就是下面这三个地方;


- 不断地找交叉引用配合变量名的修改就好,找到这样一个位置有函数调用;

- 看起来sub_23F2C和sub_242A0可能会是目标,进sub_23F2C看看;

- 记忆力好一点的朋友应该能看出来这是哪里,先不表,hook看看是不是走了;他一共有4个参数,参数1是1,参数2、3应该是地址;


- 参数2很显然是我们的明文,参数3是rc4的key,那这个函数是不是rc4呢?答案是显然的;


- 所以响应的算法实际上和sp是反着来的,sp加密响应解密,但是响应是没有base64的过程的;其实在分析之前我就知道大概是一样的算法了,我们抓包的响应结果的开头是:


- 而且在分析sp的时候多次有这种开头的hex数据,如下图:

- 这都是可以猜测的,这里就聊这么多,算法还没有完全结束;
- 这个数据只是经过了rc4解密,但是没有lz4解压缩的内容,别忘了我们的结果其实很长的;可以把参数换成长的再调用一下;
- idea显示方法太长了,我这会又没写加密函数,直接去看另一个方法吧;

- 实际上就是在解密前解一次压缩,其余没啥区别,这里也没有base64;
- 就用前面那个sp解密的脚本也可以解开长的密文;

- 到这里三个位置的算法都讲清楚了,响应解密和sp是类似的逆过程;
5. 总结
- 除了这一种密文以外,我还发现了有多种密文,我以sp和响应对照着说一下:
本文讲的:
sp:zwp_N开头
响应:cf 0a 7f 34开头 响应还有一种短的 开头不记得了
sp:wxJRBAfMjnn开头
响应:c3 12 51 04开头
sp:wxJRBAfMjnn开头
响应:fa 6a 62 29开头 这个属于短的 和前面的理论一样 没有压缩 只有rc4解密
- 大概就是四种,有可能更多,毕竟我也只是随便看了几个接口;后面这种其实算法什么都是一样的,就是rc4的key不一样,也不能说不一样,多了点东西而已,感兴趣的自己看看就好;
- 总结一下这几个参数:
- sig:md5加盐,盐是rc4的key;
- sp:base64 + rc4 + lz4压缩;
- 响应:同sp;
- by:2026-02-23;