吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 500|回复: 0
上一主题 下一主题
收起左侧

[Android 原创] 某招聘软件算法分析

[复制链接]
跳转到指定楼层
楼主
xiayutianz 发表于 2026-5-13 16:15 回帖奖励
本帖最后由 xiayutianz 于 2026-5-13 16:34 编辑

算法分析

本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关;

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文涉及的技术而导致的任何意外,作者均不负责;

1. 概述

包名:com.hpbr.bosszhipin

版本:13.141

  • 我们的目标是搜索,需要提前登录;算法整体难度算中等;
  • 总体涉及md5、rc4、base64、LZ压缩这几个东西,需要提前了解一下这些算法;

2. 抓包&定位

  • 首先进行抓包,确定目标参数;搜索之后找/api/batch/batchRunV2这个接口,主要目标是sp、sig、还有返回值看起来是加密的,接下来就逐个分析;

image-20260222140023559

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

image-20260222140718910

  • 最后一个显然是要合理一点的,进去看看;

image-20260222140902294

  • sig和sp都在,有可能是这里,继续跟一下就可以发下它们的native函数,我不放图了,把对应的结果写下来吧;
sig:private static native byte[] nativeSignature(byte[] bArr, String str);
sp: private static native String nativeEncodeRequest(byte[] bArr, String str);
  • 对应的so应该是libyzwg.so;
private static final String LIB_NAME = "yzwg";
  • hook验证一下吧;
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的,而且附近也有类似的函数;

image-20260222144235757

  • 我猜测是选中的这两个函数之一,我们都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);
}
  • 到这里就没有环境问题了,可以开始call了;
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
  • 再继续call sp参数吧;
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);
}
  • 太长了,不写出结果了,也是不用再补环境,直接就有结果,而且和抓包对得上;

image-20260222153412877

  • 顺便把返回值的调用也写了,这个也不需要补环境,但是数据太占篇幅了,不写了;不过这里需要注意几个点,首先是原始返回值太长了,我们需要寻找短一点的来调用;并且不同的长度最后一个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));
}

image-20260222155857978

  • 到这里基础的准备就都做好了,可以开始算法分析了;

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|
  • memcpy的一部分日志:
>-----------------------------------------------------------------------------<
[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);

image-20260222162036178

  • 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
  • 去追一下0x21fdc这个位置;

image-20260222162316895

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

image-20260222162438241

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

image-20260222162818439

image-20260222162837494

  • 参数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
  • 返回值需要下一个blr断点去看当时的x0;

image-20260222163112601

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

image-20260222163314781

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

image-20260222173322025

  • 在内存里dump确实是 a308f3628b3f39f7d35cdebeb6920e21 这个值,交叉引用发现整个函数是在jni_onload被调用的,大概率是ollvm的字符串解密函数,这里我先不管了,有兴趣的可以跟一下;

  • 所以sig= V3.0+md5(urlpath+请求参数),请求参数有url编码,好像还是两层,复现的时候需要注意;

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

image-20260222175717326

  • 和前面是是一个意思,自己改一下jnienv类型;

image-20260222175907256

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

image-20260222175955195

  • 这是一个base64的数据,看看返回值是什么;

image-20260222180032394

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

image-20260222180410308

image-20260222180418948

  • 看看返回值;

image-20260222180458756

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

image-20260222180557955

  • 是标准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`.
  • 往上看就可以看到sub_2E91C有相关的内容;

image-20260222181119446

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

image-20260222190355886

image-20260222190450021

  • 看起来都不是特别眼熟,再看看结果吧;

image-20260222190534729

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

image-20260222190705402

  • 总体不长,稍微熟悉一点的朋友应该能看出来是什么算法;
*(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函数了;

image-20260222191719641

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

image-20260222192205612

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

image-20260222192509690

  • 确实是标准的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,看起来差不多;找一下这一部分怎么来的,去看看交叉引用;

image-20260222193558911

  • 前面两个看起来比较有可能一点,进去看看;

image-20260222193626304

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

image-20260222194428893

image-20260222194441475

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

image-20260222195227457

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

image-20260222195210056

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

image-20260222201744917

  • 压缩成功后的结果并没有放在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 响应解密
  • 直接调用函数,看看有没有什么信息吧;

image-20260223103724960

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

image-20260223104008891

  • 确实是这样,那去看一下0x25440这个地址;

image-20260223104129621

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

image-20260223105450501

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

image-20260223105623976

  • 可以都去看看,有没有什么关键的信息;

image-20260223105655418

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

image-20260223110042146

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

image-20260223110136082

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

image-20260223110316139

image-20260223110324019

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

image-20260223110450069

  • 去解一下吧;

image-20260223110533067

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

image-20260222140023559

  • 把它前面几个字节base64一下;

image-20260223110925918

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

image-20260223111014766

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

image-20260223121247024

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

image-20260223121529072

  • 到这里三个位置的算法都讲清楚了,响应解密和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;

免费评分

参与人数 4吾爱币 +2 热心值 +2 收起 理由
mfpss95134 + 1 热心回复!
user_0628 + 1 热心回复!
allenpu + 1 我很赞同!
glz220 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-14 02:43

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表