【APP逆向百例】某品会 app 逆向分析

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!

逆向目标

  • 目标:某品会 APP
  • apk 版本:v9.42.8
  • 逆向参数:authorization
  • 下载地址:aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzMxNTgzL2hpc3Rvcnlfdjk0MjA4

抓包分析

打开 app 随便搜索一样东西,然后通过 charles 配合 SocksDroid 进行抓包,结果如下:

python 重放请求,发现请求头需要 authorization 参数。

java 层分析

这个版本有 frida 检测,还是老方法,hook dlopen 查看在哪个 app 闪退了:

javascript 复制代码
function hook_dlopen() {
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
    Interceptor.attach(android_dlopen_ext, {
        onEnter: function (args) {
            var path_ptr = args[0];
            var path = ptr(path_ptr).readCString();
            console.log("[android_dlopen_ext -> enter", path);
        },
        onLeave: function (retval) {
            console.log("android_dlopen_ext -> leave")

        }
    });
}
setImmediate(hook_dlopen)
复制代码
frida -U -f com.achievo.vipshop -l .\demo.js

上面代码启动,发现还是 libmsaoaidsec.so 检测,关于该检测,往期文章中有详细的介绍:

某瓣 app 逆向分析:mp.weixin.qq.com/s/i_k_QOfgA...

把该 so 文件丢到 ida 工具,直接通过交叉引用,找到上层的调用函数 hook 掉就行:

hook 检测代码如下:

javascript 复制代码
function hook_dlopen() {
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
    Interceptor.attach(android_dlopen_ext, {
        onEnter: function (args) {
            var path_ptr = args[0];
            var path = ptr(path_ptr).readCString();
            console.log("[android_dlopen_ext -> enter", path);
        },
        onLeave: function (retval) {
            console.log("android_dlopen_ext -> leave")

        }
    });
}

function hook_call_constructors() {
    var linker64_base_addr = Module.getBaseAddress("linker64")
    var call_constructors_func_off = 0x000000000004a174
    var call_constructors_func_addr = linker64_base_addr.add(call_constructors_func_off)
    var listener = Interceptor.attach(call_constructors_func_addr, {
        onEnter: function (args) {
            console.log("call_constructors -> enter")
            var module = Process.findModuleByName("libmsaoaidsec.so")
            if (module != null) {
                Interceptor.replace(module.base.add(0x000000000001B924), new NativeCallback(function () {
                    console.log("replace sub_1BEC4")
                }, "void", []))
                listener.detach()
            }
        },
    })
}

setImmediate(hook_dlopen)

注意 call_constructors_func_off 的地址需要改成大伙自己的,这个地址可以在手机执行以下命令得到:

bash 复制代码
readelf -s /apex/com.android.runtime/bin/linker64 | grep call_constructors

这个参数在头部,因此我们优先选择 hook 头部,而不是 hook HashMap,hashMap 输出内容较多,当我们hook 头部没有相关信息时,可以再考虑 hook hashMap,hook 代码如下:

javascript 复制代码
function showStacks() {
    Java.perform(function () {
        console.log("打印堆栈")
        console.log(Java.use("android.util.Log").getStackTraceString(
            Java.use("java.lang.Throwable").$new()
        ));
    })
}

function hook_Header(){
    var Builder = Java.use("okhttp3.Request$Builder");
    Builder["addHeader"].implementation = function (str, str2) {
        console.log("key: " + str)
        console.log("val: " + str2)
        // showStacks()
        if (str ==="Authorization"){
            showStacks()
        }
        var result = this["addHeader"](str, str2);
        return result;
    };
}
复制代码
frida -U -f com.achievo.vipshop -l .\demo.js -o 1.txt

找到我们需要的参数的值:

apk 文件放入 jadx,根据第二层堆栈找到如下位置:

值是 str,str 又通过 b.b 方法得到的,我们这里是 post 方法就走 if 逻辑,hook 一下:

java 复制代码
let b = Java.use("a9.b");
b["b"].implementation = function (context, treeMap, str, str2) {
    console.log(`b.b is called: context=${context}, treeMap=${treeMap}, str=${str}, str2=${str2}`);
    let result = this["b"](context, treeMap, str, str2);
    console.log(`b.b result=${result}`);
    return result;
};

位置没错,进入方法:

一直走,最后定位到了这里:

进入 getSignHash 方法然后调用了 gs 方法,但是 gs 方法是报错,按道理 gs 函数应该有返回值,返回我们的加密结果,我们可以直接把代码丢到 GPT 让他帮忙分析一下:

gpt 给了我们答案,说是通过反射调用了一个隐藏的方法,那我们先了解一下 java 反射是什么:

上面也说了 java 的反射创建类不通过 new,而是通过 newInstance 方法创建,那我们向下翻,会看到 initInstance 方法:

根据上面了解到的反射的相关知识,这里很明显是在创建对象,我们直接点击 KeyInfo 这个类,找到真正的加密函数 gs:

最后调用了 native 方法,so 的名称为 keyinfo:

我们可以 hook 验证一下:

javascript 复制代码
let KeyInfo = Java.use("com.vip.vcsp.KeyInfo");
KeyInfo["gsNav"].implementation = function (context, map, str, z10) {
    console.log(`KeyInfo.gsNav is called: context=${context}, map=${map}, str=${str}, z10=${z10}`);
    let result = this["gsNav"](context, map, str, z10);
    console.log(`KeyInfo.gsNav result=${result}`);
    return result;
};

最后改成主动调用的形式,方便我们后续分析:

javascript 复制代码
function hook_zhu() {
    Java.perform(function () {
        const KeyInfo = Java.use("com.vip.vcsp.KeyInfo");
        const TreeMap = Java.use("java.util.TreeMap");
        const String = Java.use('java.lang.String');
        // 构造 context
        const currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        const context = currentApplication.getApplicationContext();
        let map = TreeMap.$new();
        map.put(String.$new("api_key"), String.$new("23e7f28019e8407b98b84cd05b5aef2c"));
        map.put(String.$new("app_name"), String.$new("shop_android"));
        map.put(String.$new("app_version"), String.$new("9.42.8"));
        map.put(String.$new("bigSaleTagIds"), String.$new(""));
        map.put(String.$new("brandIds"), String.$new(""));
        map.put(String.$new("brandStoreSns"), String.$new(""));
        map.put(String.$new("categoryId"), String.$new(""));
        map.put(String.$new("channelId"), String.$new("1"));
        map.put(String.$new("channel_flag"), String.$new("0_1"));
        map.put(String.$new("clickFrom"), String.$new("list_userword"));
        map.put(String.$new("client"), String.$new("android"));
        map.put(String.$new("client_type"), String.$new("android"));
        map.put(String.$new("couponIds"), String.$new(""));
        map.put(String.$new("darkmode"), String.$new("0"));
        map.put(String.$new("deeplink_cps"), String.$new(""));
        map.put(String.$new("device_model"), String.$new("Google Pixel 3"));
        map.put(String.$new("did"), String.$new("0.0.488b303f75e2323522a8ed6c9da1d86f.2e28ab"));
        map.put(String.$new("elder"), String.$new("0"));
        map.put(String.$new("evgid"), String.$new("itg0e6C7desc9WOI7whauxkrce1Tnmectkj0f9gbLh7l8Yy8fZS8ASg24uPgOT1UB+J9Pn37EebSRw/foKbqomTNnj0y5EDKLAXFfooxVlQ="));
        map.put(String.$new("extParams"), String.$new("{\"priceVer\":\"2\",\"video_playable\":\"1\",\"mclabel\":\"1\",\"cmpStyle\":\"1\",\"statusVer\":\"2\",\"ic2label\":\"1\",\"video\":\"2\",\"uiVer\":\"2\",\"preheatTipsVer\":\"4\",\"floatwin\":\"1\",\"superHot\":\"1\",\"exclusivePrice\":\"1\",\"router\":\"1\",\"coupons\":\"5\",\"needVideoExplain\":\"1\",\"rank\":\"2\",\"needVideoGive\":\"1\",\"attr\":\"2\",\"bigBrand\":\"2\",\"couponVer\":\"v2\",\"videoExplainUrl\":\"1\",\"live\":\"1\",\"sellpoint\":\"1\",\"reco\":\"1\",\"vreimg\":\"1\",\"search_tag\":\"2\",\"tpl\":\"1\",\"ads\":\"2\",\"stdSizeVids\":\"\",\"labelVer\":\"2\",\"preheatView\":\"1\"}"));
        map.put(String.$new("fdc_area_id"), String.$new("104104"));
        map.put(String.$new("functions"), String.$new("RTRecomm,flagshipInfo,couponBarV2,lowPriceTabs,feedbackV2,otdAds,zoneCode,slotOp,survey,outfit,aiRealtime,floaterParams,tabGroupV2,bsAndSeason,propAndOpTag,parallelCall"));
        map.put(String.$new("harmony_app"), String.$new("0"));
        map.put(String.$new("harmony_os"), String.$new("0"));
        map.put(String.$new("height"), String.$new("2028"));
        map.put(String.$new("isMultiTab"), String.$new("0"));
        map.put(String.$new("is_default_area"), String.$new("1"));
        map.put(String.$new("keyword"), String.$new("纸"));
        map.put(String.$new("lastPageProperty"), String.$new("{\"isBgToFront\":\"0\",\"suggest_text\":\"纸\",\"scene_entry_id\":\"-99\",\"refer_page_id\":\"page_te_globle_classify_search_1749796272747\",\"isSimple\":\"0\",\"text\":\"纸\",\"tag\":\"1\",\"module_name\":\"com.achievo.vipshop.search\",\"type\":\"all\",\"typename\":\"全部\",\"is_back_page\":\"1\"}"));
        map.put(String.$new("maker"), String.$new("GOOGLE"));
        map.put(String.$new("mars_cid"), String.$new("2760c2a5-07c5-3a1c-a409-66e37ebaf574"));
        map.put(String.$new("mobile_channel"), String.$new("rjx5hknt:::"));
        map.put(String.$new("mobile_platform"), String.$new("3"));
        map.put(String.$new("net"), String.$new("WIFI"));
        map.put(String.$new("operator"), String.$new(""));
        map.put(String.$new("os"), String.$new("Android"));
        map.put(String.$new("osv"), String.$new("11"));
        map.put(String.$new("otddid"), String.$new(""));
        map.put(String.$new("other_cps"), String.$new(""));
        map.put(String.$new("page_id"), String.$new("page_te_commodity_search_1749796274004"));
        map.put(String.$new("page_init_ts"), String.$new("1749796253199"));
        map.put(String.$new("phone_brand"), String.$new("google"));
        map.put(String.$new("phone_model"), String.$new("pixel 3"));
        map.put(String.$new("priceMax"), String.$new(""));
        map.put(String.$new("priceMin"), String.$new(""));
        map.put(String.$new("props"), String.$new(""));
        map.put(String.$new("province_id"), String.$new("104104"));
        map.put(String.$new("referer"), String.$new("com.achievo.vipshop.search.activity.SearchActivity"));
        map.put(String.$new("rom"), String.$new("Dalvik/2.1.0 (Linux; U; Android 11; Pixel 3 Build/RQ1D.210205.004)"));
        map.put(String.$new("sd_tuijian"), String.$new("0"));
        map.put(String.$new("service_provider"), String.$new(""));
        map.put(String.$new("session_id"), String.$new("2760c2a5-07c5-3a1c-a409-66e37ebaf574_shop_android_1749796345730"));
        map.put(String.$new("skey"), String.$new("6692c461c3810ab150c9a980d0c275ec"));
        map.put(String.$new("sort"), String.$new("0"));
        map.put(String.$new("source"), String.$new("app"));
        map.put(String.$new("source_app"), String.$new("android"));
        map.put(String.$new("standby_id"), String.$new("rjx5hknt:::"));
        map.put(String.$new("sys_version"), String.$new("30"));
        map.put(String.$new("tabFields"), String.$new("gender,tabs,priceTabs,discountTabs,tabGroupV2"));
        map.put(String.$new("tfs_fp_token"), String.$new("Ba1bX4G5LVCvZX/keQ+jKakGAtkmcd5uF3mqx1MgFxED+ki6Tt8pN7kHPc01QhDUUEKPZujxXveye0E3jIKnUvA=="));
        map.put(String.$new("timestamp"), String.$new("1749796274"));
        map.put(String.$new("union_mark"), String.$new("blank&_&blank&_&rjx5hknt:::&_&blank&_&blank&_&blank"));
        map.put(String.$new("vipService"), String.$new(""));
        map.put(String.$new("warehouse"), String.$new("VIP_NH"));
        map.put(String.$new("width"), String.$new("1080"));

        let result = KeyInfo["gsNav"](context, map, null, false);
        console.log("java---->", result);
    })
}

so 层分析

老样子,keyinfo.so 文件拖到 ida 里面去,在 Exports 表搜索 java 看看是不是静态注册的:

是静态注册,点进去:

返回 v7,点进去 Function_gs,并按 y 修改 a1 类型:

代码没有什么混淆,基本上全是明文,遇到这种,我们可以找根据名字定位,也可以 frida trace 看看调用堆栈,因为这代码没多少行,我们可以直接丢给 ai 帮我们分析,这里用的元宝的 deepseek:

根据 ai 说的,我们只需要关心 getByteHash 函数做了什么操作,先找到 getByteHash 函数看看,按住 alt + T 搜索 getByteHash,有两处,点进去:

很明显的 sha1 算法,还是直接先 hook 一下 getByteHash 方法,看看传递了什么参数:

我们重点看第三个参数和第五个参数就行,hook 代码如下:

javascript 复制代码
function hook_so_byhash() {
    var addr = Module.findBaseAddress('libkeyinfo.so');
    var func = addr.add(0xF2260)
    // console.log(func)
    let number = 0;
    Interceptor.attach(func, {
        onEnter: function (args) {
            number += 1;
            console.log(`\x1b[31m第${number} 次 hook getByteHash-----------------------------------------------\x1b[0m`);
            this.arg5 = args[4]
            console.log(hexdump(args[2]))
            console.log(args[2].readCString())
        },
        onLeave: function (retval) {
            console.log("this.arg5",hexdump(this.arg5));
        }
    })
}

setImmediate(hook_so_byhash)

在 frida 里面执行我们的主动调用:

打印结果如下:

第一次,输出的参数暂且不知道是什么,返回的结果为 1ed562e1e90b23ae3f9a40f8b2a65382b95a4752

第二次入参为我们的 TreeMap 数据,前面盐值为 aee4c425dbb2288b80c71347cc37d04b,经分析,该值写死即可,返回值为 d98aee7e972029d163709d41538d5bdcb2fe290f

我们到 K 哥工具站(www.kgtools.cn),对比发现,并没有魔改,标准算法:

第三次的入参为我们前面的盐加上第二次 sha1 算法加密的返回的结果。

第三次返回的结果为 0ae492780a1e7aefafa23efae2357faafc98af51

和我们主动调用的结果一样,最后用 python 改写发包即可,至此,加密参数分析完毕,相关代码,会放到知识星球中,仅供学习交流。

结果验证

相关推荐
失败又激情的man3 小时前
Scrapy进阶封装(第四阶段:中间件设置,动态UA,ip代理池)
爬虫·scrapy·中间件
dme.8 小时前
Javascript之DOM操作
开发语言·javascript·爬虫·python·ecmascript
巴里巴气13 小时前
selenium基础知识 和 模拟登录selenium版本
爬虫·python·selenium·爬虫模拟登录
凛铄linshuo1 天前
爬虫简单实操2——以贴吧为例爬取“某吧”前10页的网页代码
爬虫·python·学习
weixin_416639972 天前
爬虫工程师Chrome开发者工具简单介绍
前端·chrome·爬虫
科技苑2 天前
简单 Python 爬虫程序设计
爬虫
q567315232 天前
R语言初学者爬虫简单模板
开发语言·爬虫·r语言·iphone
泡泡以安3 天前
安卓高版本HTTPS抓包:终极解决方案
爬虫·https·安卓逆向·安卓抓包
q567315233 天前
Java Selenium反爬虫技术方案
java·爬虫·selenium