感谢杨如画大佬分析的文章, 这个文章是复现大佬的文章. 动手了才是自己的.

抓包

  • Pixel3
  • Reqable

咱们的任务需求主要是搞定这几个算法.

image-20241213144552163

定位加密位置

1. 搜索法

我们将应用拖入JADX中,搜索关键词 x-sap-ri,发现无果. 所以搜索关键词的方法在此处显得较为无力. 在此案例中, 不适合使用. 但是并不意味着,搜索法不好. 这个要看实际的使用常见

2. 开发者思维

我们反编译虾皮的APK, 可以看到有okhttp3的主包, 那么我们就要思考. 我们应该如何给一个请求设置请求头?

https://square.github.io/okhttp/

使用addHeader, addHeader里面应该是用HashMap, 这里我没有去跟踪okhttp, 感兴趣的读者可以自己去跟踪一下.

最终可以写一个frida hook脚本.

💡对HashMap.put方法进行了拦截, 判断key是否为我们的目标, 如果为目标,则打印调用栈. 根据调用栈定位加密位置
Java.perform(function (){
    //     HashMap.put
    var hashMap = Java.use("java.util.HashMap");
    hashMap.put.implementation = function (a, b) {
        if(a!=null && a.equals("x-sap-ri")){
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
            console.log("hashMap.put: ", a, b);
        }
        return this.put(a, b);
    }
})

保证应用在前台, 我们开始Hook应用

frida -UF -l .\hook_shopee.js

堆栈非常的长, 我们看到又关键的方法com.shopee.shpssdk.SHPSSDK.uvwvvwvvw

java.lang.Throwable
        at java.util.HashMap.put(Native Method)
        at org.json.JSONObject.put(JSONObject.java:276)
        at org.json.JSONTokener.readObject(JSONTokener.java:394)
        at org.json.JSONTokener.nextValue(JSONTokener.java:104)
        at org.json.JSONObject.<init>(JSONObject.java:168)
        at org.json.JSONObject.<init>(JSONObject.java:185)
        at com.shopee.shpssdk.SHPSSDK.uvwvvwvvw(Unknown Source:58)
        at com.shopee.shpssdk.SHPSSDK.requestDefense(Unknown Source:95)
        at com.shopee.app.network.antifraud.b.intercept(SourceFile:47)
        at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:19)
        ....(省略)
hashMap.put:  x-sap-ri d3db5b67e52fdbdec805c31d01850c676f03f49ab5458cc0533f

在jadx中, 我们找到对应的位置, 怪不得我们在前文中,用搜索法无法定位到函数位置. 原来是因为该应用的字符串加密导致的.

💡以后在逆向工作中, 大家可以思考下, 如果我是开发者,我会怎么做? 有时候可能会带来意想不到的效果!

那么生成我们需要的x-sap-ri参数的函数就是vuwuuwvw方法

public Map<String, String> requestDefense(String str, byte[] bArr) {
    if (!ShPerfC.checkNotNull(perfEntry) || !ShPerfC.on(new Object[]{str, bArr}, this, perfEntry, false, 35729, new Class[]{String.class, byte[].class}, Map.class)) {
        Hashtable hashtable = new Hashtable();
        if (str == null) {
            return hashtable;
        }
        try {
            try {
                return uvwvvwvvw(wvvvuwwu.vuwuuwvw(str.getBytes(), bArr)); // vuwuuwvw方法生成结果
            } catch (Throwable unused) {
                hashtable.put(vvuwvuuuu.wwvvvwvwu("0F580414075A131E0F1A10", "wuwuwwuww"), vvuwvuuuu.wwvvvwvwu("130E3F013E1F1A123E1B261C3A34341C2F243F413C3232063C363D023E1F19103C1C3F4E", "vwuvwuuuw"));
                return hashtable;
            }
        } catch (Throwable unused2) {
        }
    } else {
        return (Map) ShPerfC.perf(new Object[]{str, bArr}, this, perfEntry, false, 35729, new Class[]{String.class, byte[].class}, Map.class);
    }
}

wwvvvwvwu 方法是一个native方法. 需要去so中分析了.

public static native String vuwuuwvw(byte[] bArr, byte[] bArr2);

但是目前, 我们已经成功的通过开发者思维定位到函数加密的位置.

frida主动调用与unidbg模拟

1. 确定目标SO

在同个文件下搜索loadLibrary发现, 不是太容易, 因为导入的SO字符串也被处理了. 所以,我们可以hook libart

public static /* synthetic */ void access$000(String str) {
    IAFz3z iAFz3z = perfEntry;
    if (iAFz3z == null || !((Boolean) ShPerfB.perf(new Object[]{str}, null, iAFz3z, true, 39835, new Class[]{String.class}, Void.TYPE)[0]).booleanValue()) {
        System.loadLibrary(str);
    }
}

hook registernative脚本

function hook_hashmap() {
    Java.perform(function () {
        //     HashMap.put
        var hashMap = Java.use("java.util.HashMap");
        hashMap.put.implementation = function (a, b) {
            if (a != null && a.equals("x-sap-ri")) {
                console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
                console.log("hashMap.put: ", a, b);
            }
            return this.put(a, b);
        }
    })
}

function find_RegisterNatives(params) {
    let symbols = Module.enumerateSymbolsSync("libart.so");
    let addrRegisterNatives = null;
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];

        //_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
        if (symbol.name.indexOf("art") >= 0 &&
            symbol.name.indexOf("JNI") >= 0 &&
            symbol.name.indexOf("RegisterNatives") >= 0 &&
            symbol.name.indexOf("CheckJNI") < 0) {
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
            hook_RegisterNatives(addrRegisterNatives)
        }
    }

}

function hook_RegisterNatives(addrRegisterNatives) {

    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                let java_class = args[1];
                let class_name = Java.vm.tryGetEnv().getClassName(java_class);
                //console.log(class_name);

                let methods_ptr = ptr(args[2]);

                let method_count = parseInt(args[3]);
                for (let i = 0; i < method_count; i++) {
                    let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

                    let name = Memory.readCString(name_ptr);
                    let sig = Memory.readCString(sig_ptr);
                    let symbol = DebugSymbol.fromAddress(fnPtr_ptr)
                    console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, " fnOffset:", symbol, " callee:", DebugSymbol.fromAddress(this.returnAddress));
                }
            }
        });
    }
}

setImmediate(find_RegisterNatives);
// setImmediate(hook_hashmap)

于是乎, 我们就定位到了目标SO及其偏移的信息

  • java_class: com.shopee.shpssdk.wvvvuwwu
  • name: vuwuuwvw
  • sig: ([B[B)Ljava/lang/String;
  • fnPtr: 0x7b8a6a45dc
  • fnOffset: 0x7b8a6a45dc libshpssdk.so!0x995dc
  • callee: 0x7b8a7c9b4c libshpssdk.so!0x1beb4c

image-20241213153128744

💡 xapk包的so文件不在主包内, so文件在config.arm64_v8a.apk类似的分包中. 所以SO文件应该去分包中寻找.

2. 主动调用SO

function hook_call() {
    Java.perform(function () {
        console.log('======================')
        let wvvvuwwu = Java.use("com.shopee.shpssdk.wvvvuwwu");
        wvvvuwwu["vuwuuwvw"].implementation = function (bArr, bArr2) {
            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
            if (bArr !== null) {
                try {
                    console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${ByteString.of(bArr).utf8()}, bArr2=${bArr2}`);
                } catch (e) {
                }
            }
            // console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${bArr}, bArr2=${bArr2}`);
            let result = this["vuwuuwvw"](bArr, bArr2);
            console.log(`wvvvuwwu.vuwuuwvw result=${result}`);
            return result;
        };
    })
}
setImmediate(hook_call)
wvvvuwwu.vuwuuwvw is called: bArr=https://mall.shopee.com.my/api/v4/search/search_page_common?by=relevancy&extra_param=%7B%22global_search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075545797-global%22%2C%22search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832-search%22%7D&keyword=ab&newest=0&order=desc&page=1&page_type=search&scenario=PAGE_GLOBAL_SEARCH&version=2&view_session_id=NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832&with_filter_config=true, bArr2=null
wvvvuwwu.vuwuuwvw result={"20d083aa": "ZsSf4b7VSHV5dFJGy8wSyiKg8GQ=", "253b8c85": "VbKH6pR9EMI6lDf7XrtJ8IElQaj=", "97603148": "EXje1Nwo3MpUhiKkDa3Ia1W28mjVYKt6pKeWIpNSR3J38KF67E0Th4Ip2RF4jqYyeoXtN3A0EO7Io6V5Z46at7j1dUpr8i2BP1+uQwWR5y7/9zvj/C2+4QfcbjtaGRHPxHh1iKemRFbZvxaxgaZhGX5ifAXERfKkLunz6NF7VStEohZzuK1oLtKQqFvenOAqXoVGU+JwZlb9PJE94Nk5W+24NxvfcMXffP0/k+rGLH8CrTNHlZS+x3rPsVQa7eNs9GeIediLGNYNF4hyThjGJJORVl8lvKjJ0Sup4vgdK2vvOUaJju2EPGOP3ZHoRuL/3XqejORazAxVoi169m+IgKdqrlmBo/J4sjg/O6U304Wi+DUMRkBk/hNf72rkP0St/v86BTjjkw/IwcY2uR3obxrrCnKuA7fM+udhIqCXlIdBsIXKWvct1K4+tnFnkhaYfWntXmk6wAowBHAe4iYweNOkEYOMMa9EBeN3XpIdrdpjsXIf40XV19n2IHNPjwMP/QVW70/kug+kBD5h67vrwVa2Wik2kla0pMVePOTsNUBde+RTG1Mj4XYuNMfqyp65dNNtVX6b3p4cFyA9R7gyCQcklwzSQ1Wen0ku/4ddrfWyJXDmxCXspr3q1wXvjNWVABF99K2qh8wLOmCxYiAX6SsNdUzwCqJI+IJUbqChkhQ3XNC91IuEKPnm+o69cXF5ZbA9Oj/t8Earf7wOdEYoxTUZd23d51dqGQ12nkNiHMRTbAPfSRlml78JPG44uFeuVrrHC1PCUQKaXwDhlMCJF0gx5ADtLiFVbtAYCJjgEOwJ/AD/0t5y1hD5o2PMcbbrEhraJPi8yER85WMQCahdBmj9czEtR48Sv2j/noXLEoT3AgL4MAdu/GxgeLpJ3/9cr57nbnoo1gt7NFM4KN4XcZqCq0U1gfoGwXCLWwmqqVlK0pjar+89/qUlfMg9oaFTp9Fa6WhnWTyNANSHeVUpW+Fd32U5BUoPlUedpIi9N3ov4AdJXxZrBglWxiulmQ+hmNAddvGIQgYXI7DLb31TQcUQLoExeIrNzRgzM+U26qLNaYA665puHPiFytl+oA/qOOhvYD2bZ9nCXkD7DLvcq8aPwiq6g9kjZnfEl+v/XPo11VoQIp8Q4bpOFDCb06iyzZRKmRmEyLdJ+cERBbPIXxUDj4nLNSOo0Qr+aDvG5M5K1DtHk5tyBzhr5uOnLQn2l9ESBB38f0ErzH9r162Hj/WavPpP4Di5emEVqcW1EAB6rNQq0yZvglaM/qzFDnJ/AxtOx67mRACdMwEyMtOVJUrONQc30GfTeBECnN5oyWShV0fAu1H5j6VSOX7z/ppqw5YQT2d6LptfWUODvFIlQ9weY3vnM07iNFrUUFWSTFGp/JgsW++Dl5AvsjtMF/EipgoBLVnJWpuicHNXvE/YN8PQZOI5sx17cn4MXrG5kkUYO1qxSAqqSR/JRQBgHa+6KuooNoiCMINacJiQXSfDFu/XZ726/vaPwjwXZrlwFbfi+lOnbCi4s5yCeZ6c9CTjAqQ5RivbqvU5CpE1", "x-sap-ri": "a8e45b6725cac1db36aa5e140103570eca090e0d4812dc66117b"}

确定了入参, 我们再主动调用一下

function call_func() {
    Java.perform(function () {
        let wvvvuwwu = Java.use("com.shopee.shpssdk.wvvvuwwu");
        let string = Java.use("java.lang.String");

        let arg1_str = "https://mall.shopee.com.my/api/v4/search/search_page_common?by=relevancy&extra_param=%7B%22global_search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075545797-global%22%2C%22search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832-search%22%7D&keyword=ab&newest=0&order=desc&page=1&page_type=search&scenario=PAGE_GLOBAL_SEARCH&version=2&view_session_id=NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832&with_filter_config=true"
        let arg1 = string.$new(arg1_str).getBytes();
        let arg2 = null;
        let result = wvvvuwwu.vuwuuwvw(arg1, arg2);
        console.log(`result=${result}`)
    })
}

相同参数, 我们主动多次call. 可以看到结果是不同的. 只有一个Key不同.

💡假设我们在什么资料都没有的前提下, 请大胆尝试,别害怕走弯路
{"253b8c85": "bun9+3sd76Kac6JsSIeVi0yIpTw=", "894c4df1": "rJgGEdeNFBrsLlBi5+M/X4E+7xt=", "c5cc14d6": "HkYJSSq7kRx4REH0ZOimt3ZDHYz+ScHUAHGBUOiSAZIiwkXbZGUI2JHjyX6sLhHPT8pbFuiT+RCcFQ17qwHuZJM7ZVmvoxZXBFBTO/67UGYgc1+IyCSnaCFmciYzHK9JRK387+QRKXQhiYkKnE5X+l0ICcTFl3EfPiabiI/5IwU0ta8sFw0v/ruZhxorrlsQlJEvKxF8QmsE1urvmiwFuliINZnriUr/prXgH413HJxuYswOa3u1Kz/QBhz7QJU+JwwIWH8ftxuSyYbXiBlhlN8qXrk9FySp/H9A0FCqPHOh8J6z4O69mvbIIRh85nyjeGwejj/mR6H+63EwEG7z4TajAsym+xQnKrZqQnU6RcH7AKxPcFa1ipzZBNe9bFZWIoRt0b5RiRRJDebMyQGkQ/W+VVxrCIT7WVlLbOqtuJkL2ZkW+jsEqrn+EoRl8/4lxHJAEw2g96hzaE2tcZhe+ncpXn43LDPn9ATIv5x1eYFE5fc3kB9GujqorP3h+O9ZKQVbzgz2/feg4iD1O5s+qNigkWpTdhaUMDKIEuGMV6mBFlWzLrbFXZrOyENfEIofsABuD11kdG6PiYYgRE1JMpRz8eEC7qJHFkIztesAQzC6QYx7eHLMUrAomwF7Z+AssKP8wHOrDxnAgit4TqcO7lPZwoLQs1JMiEMH31QEwdoVoH3B2bKJ/jGtrZRU1AHCFO/yTFgLaB9VDb3j1qahrKqd7D/o9tuxeomrypKoD7NrSVwNrUFCZ4b+ePa4hgkWniubdNf2c4fSmqYr", "x-sap-ri": "f3e55b67ada705f8ab60fe1d01e22a822ea4faa27c95af362d83"}

{"253b8c85": "hlGD6Ox0ErCcsv0i1q6GFgq+etT=", "73b486d9": "w+zOLbwZzWWjDCByolukIxboHc8eCDw5uSYuf7+/KIps2blzV5SC7hTiXVEo+juNcNfcasCv4YU0SBOKdcPqlnFYscZQKBqHUUJ0ajd9IWCvhc1hR8CGi0vE+PcMfaqvyGHXE8NWnBOJNjDNO1xH8OZJk0lvxO7PDeUcJMPY0ILWbJjE23DRQy1tqmVYfAKrMNBqBh5OFWow1ldWi6S+HzsBoEZV68bP3kcboEcirsQe8h1kaBxzbNkv5A+uwmgVvq0oLehC28nkxYz3AQZwoSZ17R4QzkASjYvJqrM01RHB2Uw73o2RP4myEHB337BrZVPW00vOJUGKzndna/W6AsjVmux89sGd4gpqlhtDgShe7c5OaIDUJWrA9vU8tiTCANqgZfbibSBieW+s0dXK0l7BuY379tJvq/D5gXuyucGCiHw6jWP+3uN8AvNHNkP87XGdyhEAQAXEjyrUiZ/4CXgCpl9kA37kEz1XNXqmFE3tep6/bc36+By6Me+c52+pNM/P2wMUGKdlq2sJdlAnUy5Lt2ZAxYgqRLFI8hRmmkekwmsct5GLDbxK95uaA/421mIHtdBqNwHmI7nMFVt+MA762jMRzzlj5Fi6/xXYIISbgO9wD7pF1LIi3pS/qR/9uVx6Lk2uXwZuxx8riMTgk54XNBminw4Er6hG3sZLeD+UqS5RAfA6goAR23zdudYvNph65wdFi84FxVBbKjRG5er2pfKQyRKIP1bJ4Uu34RfhaAXU/LKPqVhb9ObpHGJJI4WTnXxANMCUMqAt", "7a13af71": "++DZCfoP4+xyQmG0/UBwnd+3Ce/=", "x-sap-ri": "f4e55b675d1842b8a2f1d01301146b15925ce8c171ea82224c30"}

{"253b8c85": "1yh1TOnqTJ+r+s+JXa9dVHJM4vj=", "43832504": "NPfgWthJW7zy4B6geMdRyYzCCpyl7v9qxuXfnGx/UFWe/BSntV1POpgA1TJ6+WGIslQDL9nxNX4MGiTr1iLHEGVmGlkUfxcxGePWutKAMqOtFBDYSebvFZs8i6GTUdCXdcgpzlZS+1qVky9g/wgPLuAqM+ygkVAvSyZpEhfZEjMpe+QD/mCz939GYekpaoAOa9BNgjZUA1qOOLL23FCLaFwTcTF8MFmXDMDwsV01huY/gH8716OeHL65BkshtkMWp5ws+lecdgbc6roDYRbTTpftswe/lw3P6P2fo3zLkzrJmQTxPGTbGji7LqN6eSu2Css7DWYpmb5NWLVkrQbqT87veABVBArNeyPLwz/K2vcjCiOd4XnDJFaGYBfdpvSOHYfFe+2ScbT3N524vbXRNG/NLgJnvXvMUA+bCFoeU74FSI7Su7Qat0Or2h0o7uh0RzpQC94hmQBAuhxWydPpt/UwN9H87dpJTPLCFEJDTR40JWY4+nflgfXJQBGwvd8iPrG80nM0nHLO8SGB18qPUiXW6kqiVYyk1zGfuDcFGiFDrgpTcppnlAVwrBMQwjIvA6ZuuS9kl1x9YTzu/gIhNRKradFR73xOmvhVXMG2sCO0lZ+ledRTTbyFJttdKETHsbg1x5FLx0MRPTfyns6h+JAMjK8SDZiS4R+DNebpEKlFcbmZGf8ikQ3t84PFk6m/2gMok54+M/DNiBFQWe1GFy9dtmVLwKnI1jK/FMSxTwnk2JANICCojJ4vli5p0DkjilR2ks==", "d2af97d7": "YEU2NhTcZqCUxyfvdOqrfO642w8=", "x-sap-ri": "f5e55b6758116691ad6fc91a01758b5e219e68f71810bb2561c1"}

{"253b8c85": "zi140oa1FHUhL06mM+xyRNqqlRj=", "26ce4a25": "dM9LPZxE+p24ZvI/C/tL/98ohST=", "84fbe71c": "OXuDmkT5Kkkyeq8ENU+5jnjtMKGHNEApeBwsdwpmTURQNkBCDI4zfCwhLyqsO90ZxBpEJtx1pWLe/cWlwMYT+XXRUO5T531IWxY+LPQgOsPdfLNCbDZ9oSsuvhthremiznDDZkupmoi7CEEeAHbQUF9g3RsFI8sSoM8h9ETM6f3LecWt/kSoDww15UeKJcfZ0euFJgCoYyWHE4KeQwKaldoNAHpHGGzpouCKs4F72d8PGNMXsCORytxy3R474Gc1G0SP0nJRl3VsxpHKyya1phnl/HQ8MbPkMvLLF92EKoH9Ie6x2lai9v5Aek2lTjR+DSepW8EXb6dyEP0osUs203aRajJOpj8PZYxwj1VujpS24b8QBXGO6loZ9sA81UfDRgVkU0lgVDBmZL7Sm2A9OGq0f3q6jnN4RT4oTN316Y2o/DvD0o4MK4LxBmxgOiP3rc4WV9BAav/NxqdBuefgXAvC077CAO033kWZsxi19JRz1Lam5lxyZVbO5yERx/SyM0VUTy9D9Y2reK77iTXAAS9IC9GOOEmaBeFY8gGPxC8n6uEVtmq+m6gttfQtMeULv0hT1GbPZCFHkrHENwPDSYPOE5m+DMhYGvl5Fu333wRfkn+euLo5447pjgLf/hJ/swZs1griKrC5AthcjGCBo4hbLiuvVQGPCfPl5z9FQo1gakkShsUuEMqDHFplZPLYNhUmc4iC6jtwLv4QNxj34H7jlT9XUs/tYmL0BtRSRPsGZu/ATdRu3i6cgbbb9b10jFvh+U==", "x-sap-ri": "f6e55b6710834ac19107561c01c37a678416d4aea786680d9908"}

3. Unidbg模拟执行

Unidbg基础知识强推龙哥的文章.

3.1 补环境

3.1.1 基础框架搭建

基础框架直接复制代码即可, 固定模板

package com.shopee.shpssdk;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class wvvvuwwu  extends AbstractJni {
    private final AndroidEmulator emulator;
    //vm
    private final VM vm;
    //载入的模块
    private final Module module;
    private final DvmClass cWvvvuwwu;

    public  wvvvuwwu(){
        emulator = AndroidEmulatorBuilder.for64Bit().addBackendFactory(new DynarmicFactory(true)).build();

        Memory memory = emulator.getMemory();
        //作者支持19和23两个sdk
        memory.setLibraryResolver(new AndroidResolver(23));
        //创建DalvikVM,利用apk本身,可以为null
        //如果用apk文件加载so的话,会自动处理签名方面的jni,具体可看AbstractJni,这就是利用apk加载的好处
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\resources\\shopee\\com.shopee.my.apk"));
//        vm = emulator.createDalvikVM(null);
        vm.setJni(this);
        vm.setVerbose(true);
        //加载so,使用armv8-64速度会快很多
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\resources\\shopee\\libshpssdk.so"),true);
        //调用jni
        dm.callJNI_OnLoad(emulator);
        module = dm.getModule();
        //Jni调用的类,加载so
        cWvvvuwwu = vm.resolveClass("com/shopee/shpssdk/wvvvuwwu");
    }

    public static void main(String[] args) {
        new wvvvuwwu();

    }

}

image-20241213155723959

发现没啥异常, 咱们在写一下主动调用的call方法, 调用以后发现报错了.

public void callFunc(){
    // 参数1: url
    // 参数2: 目前是null
    byte[] arg1 = "https://mall.shopee.com.my/api/v4/search/search_page_common?by=relevancy&extra_param=%7B%22global_search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075545797-global%22%2C%22search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832-search%22%7D&keyword=ab&newest=0&order=desc&page=1&page_type=search&scenario=PAGE_GLOBAL_SEARCH&version=2&view_session_id=NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832&with_filter_config=true".getBytes();
    byte[] arg2 = null;
    // 主动调用
    StringObject result =   (StringObject)cWvvvuwwu.callStaticJniMethodObject(emulator,"vuwuuwvw([B[B)Ljava/lang/String;",arg1,arg2);
    System.out.println(result.getValue());

}

image-20241213160216353

到此,我们的逆向工作陷入僵局. 我们必须要解决了这个错误, 否则只能delete进入回收站了🥹

img

3.1.2 Unidbg异常分析处理(烂尾等大佬指导)

对于异常, 我们就不能再像前文,无脑Unidbg 黑盒执行. 打开IDA

跳转到异常的位置

image-20241213160850317

从报错日志上面看

[16:01:16 210]  WARN [com.github.unidbg.linux.ARM64SyscallHandler] (ARM64SyscallHandler:399) - handleInterrupt intno=2, NR=-127248, svcNumber=0x10a, PC=unidbg@0xfffe0134, LR=RX@0x40096dd8[libshpssdk.so]0x96dd8, syscall=null

intno为2,因为 JNI 也是通过SVC指令处理。

⚠️ 引用自龙哥星球

intno是异常类型,异常有很多种,比如未定义的指令,软中断、软断点等等,Unidbg 或者说 Unicorn 里对它们的定义如下。

int EXCP_UDEF = 1; /* undefined instruction */ 未定义的指令
int EXCP_SWI = 2; /* software interrupt */ 软中断
int EXCP_BKPT = 7; /* software breakpoint */ 软断点

SVC指令就是软中断,对应于定义里的EXCP_SWI,我们熟悉的系统调用就是通过它发起。因此如果into的值是 2,就说明这是一个软中断,也就是系统调用。

如果是其他中断类型,即未定义的指令或者软件中断,在 Unidbg 里会直接断下,交由用户处置,但很少碰到这两种情况。

if (intno == ARMEmulator.EXCP_BKPT) { // bkpt
    createBreaker(emulator).brk(pc, bkpt);
    return;
}
if (intno == ARMEmulator.EXCP_UDEF) {
    createBreaker(emulator).debug();
    return;
}

if (intno != ARMEmulator.EXCP_SWI) {
    throw new BackendException("intno=" + intno);
}

NR为-127248, 这显然不是任何一个合理的调用号,这实际上是因为 JNI 调用并不会对 R7、X8 做赋值,所以它是外部上下文中错乱的寄存器值。

NR 就是所谓的调用号,32 位存在 R7 寄存器,64 位存在 X8 寄存器。以 64 位为例,Unidbg 代码如下,从 X8 寄存器取出值。

int NR = backend.reg_read(Arm64Const.UC_ARM64_REG_X8).intValue();

系统调用表:https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#arm64-64_bit

svcNumber为0x10a, 而不是 0,所以它是 JNI 调用而非系统调用。

当陷入中断 Hook 时,解析SWI,如果这个立即数不为 0 ,那么就是一处 JNI 跳板函数而非系统调用,然后根据立即数的具体值确定是哪一个 JNI 函数。

换句话说,在 Unidbg 里,JNI 调用被“提升”到了和系统调用相同的级别,这就可以解释为什么 JNI 报错也发生在 syscallHandler 这件事了。

借助龙哥的文章, 我们可以确定, 当前的报错是JNI调用的报错. 也就是说有的东西它没实现.

这里不是特别理解. 大佬们指导指导!!

然后我就尝试性的把这个异常抛出屏蔽了. 学着上面函数返回了一个 0 .

屏蔽路径:unidbg-android\src\main\java\com\github\unidbg\linux\android\dvm\AbstractJni.java

image-20241213174234938

龙哥,也说了句. 龙哥就是真理

1734083220766

然后就过了. 可以开始正常的补环境.

image-20241213174333257

3.1.3 补环境

这一块就比较基础, 如画大佬文章中所说:

环境交给你相信也是信手拈来
package com.shopee.shpssdk;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
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.ApplicationInfo;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class wvvvuwwu  extends AbstractJni {
    private final AndroidEmulator emulator;
    //vm
    private final VM vm;
    //载入的模块
    private final Module module;
    private final DvmClass cWvvvuwwu;

    public  wvvvuwwu(){
        emulator = AndroidEmulatorBuilder.for64Bit().addBackendFactory(new DynarmicFactory(true)).build();

        Memory memory = emulator.getMemory();
        //作者支持19和23两个sdk
        memory.setLibraryResolver(new AndroidResolver(23));
        //创建DalvikVM,利用apk本身,可以为null
        //如果用apk文件加载so的话,会自动处理签名方面的jni,具体可看AbstractJni,这就是利用apk加载的好处
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\resources\\shopee\\com.shopee.my.apk"));
//        vm = emulator.createDalvikVM(null);
        vm.setJni(this);
        vm.setVerbose(true);
        //加载so,使用armv8-64速度会快很多
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\resources\\shopee\\libshpssdk.so"),true);
        //调用jni
        dm.callJNI_OnLoad(emulator);
        module = dm.getModule();
        //Jni调用的类,加载so
        cWvvvuwwu = vm.resolveClass("com/shopee/shpssdk/wvvvuwwu");
    }


    @Override
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature){
            case "android/app/ActivityThread->getApplication()Landroid/app/Application;":
                return vm.resolveClass("android/app/Application", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(signature);
            case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
                return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObjectArg(0).getValue().toString());
            case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":
                // 读取SharedPreferences
                String tableName = dvmObject.getValue().toString();
                System.out.println("tableName:"+tableName);
                if(tableName.equals("SPHelper_sp_main")){
                    String key = vaList.getObjectArg(0).getValue().toString();
                    System.out.println("get key:"+key);
                    switch (key){
                        case "E1YASQpPEEUQWR1CCUwVVVVw":{
                            // 每个人的值都不一定,不确定是什么.
                            return new StringObject(vm, "f0VMRgEAAAAIAAAA4Bw+NgAAAAACAAAAJAAAAAVUUwFVA1JSSlNfXgZKU1BUA0oGVQVXSlYFVVNRBFFQUldQAwMAAAAIAAAAAFRQAQAAAAA=");
                        }
                    }
                }
            case "android/content/pm/ApplicationInfo->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;":
                // loadLabal方法: 返回应用的名称
                return new StringObject(vm,"Shopee");
            case "android/content/pm/PackageManager->getInstallerPackageName(Ljava/lang/String;)Ljava/lang/String;":
                // getInstallerPackageName方法: 返回应用的安装包名
                return new StringObject(vm,"com.shopee.my");
            case "android/content/SharedPreferences->edit()Landroid/content/SharedPreferences$Editor;":
                return vm.resolveClass("android/content/SharedPreferences$Editor").newObject(null);
            case "android/content/SharedPreferences$Editor->putString(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;":
                // 写入SharedPreferences
                String key = vaList.getObjectArg(0).getValue().toString();
                String value = vaList.getObjectArg(1).getValue().toString();
                System.out.println("put key:"+key+" value:"+value);
                return dvmObject;
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

    @Override
    public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature){
            case "android/content/SharedPreferences$Editor->commit()Z":
                // 提交SharedPreferences
                return true;
        }
        return super.callBooleanMethodV(vm, dvmObject, signature, vaList);
    }

    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature){
            case "com/shopee/shpssdk/wvvvuwwu->vuwuuuvv(ILjava/lang/Object;)Ljava/lang/Object;":
                // 调用APK的函数 挺复杂的. 看着像是个解释器. 参数一是指令, 参数二是参数.
                int arg1 = vaList.getIntArg(0);
                DvmObject<?> arg2 = vaList.getObjectArg(1);
                System.out.println("arg1:"+arg1+" arg2:"+arg2.getValue());
                return new StringObject(vm,"y4MPaYVLCwOUmAqoxMo2WQ==|a8008WfrhvEuMxhlYeZHMe6FKbuu8TkZWbgxPYJQobY7Yc3ivrR4qzLKW2y95jU3TfzasbpK1dI10QZv/PHnLVK3ixS7rODIWW6glQ==|yZeBlOWh5ZZRdfLu|08|1");

        }
        return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }

    @Override
    public long getLongField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        switch (signature){
            case "android/content/pm/PackageInfo->firstInstallTime:J":{ // 首次安装时间
                return 1732349660249L;
            }
            case "android/content/pm/PackageInfo->lastUpdateTime:J":{ // 最后更新时间
                return 1732349661249L;
            }
        }
        return super.getLongField(vm, dvmObject, signature);
    }

    @Override
    public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        switch (signature){
            case "android/content/pm/PackageInfo->applicationInfo:Landroid/content/pm/ApplicationInfo;":
                return new ApplicationInfo(vm);
            case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;":
                // pm path com.shopee.my
                return new StringObject(vm,"/data/app/~~oONxdR2wXsaVN2qQTbgsSg==/com.shopee.my-jns-B-GoPqIjRs55vzq5WA==");
        }
        return super.getObjectField(vm, dvmObject, signature);
    }

    @Override
    public int getIntField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        switch (signature){
            case "android/content/pm/ApplicationInfo->flags:I": // 检查app是否开启调试
                return 1048576;
        }
        return super.getIntField(vm, dvmObject, signature);
    }

    public void callFunc(){
        // 参数1: url
        // 参数2: 目前是null
        byte[] arg1 = "https://mall.shopee.com.my/api/v4/search/search_page_common?by=relevancy&extra_param=%7B%22global_search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075545797-global%22%2C%22search_session_id%22%3A%22NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832-search%22%7D&keyword=ab&newest=0&order=desc&page=1&page_type=search&scenario=PAGE_GLOBAL_SEARCH&version=2&view_session_id=NqAYm66NYy45DbQoklaKEBuaQD%2BQgayKLEeqNuK3MhM%3D-1734075560832&with_filter_config=true".getBytes();
        byte[] arg2 = null;
        // 主动调用
        StringObject result = (StringObject)cWvvvuwwu.callStaticJniMethodObject(emulator,"vuwuuwvw([B[B)Ljava/lang/String;",arg1,arg2);
        System.out.println(result.getValue());

    }
    public static void main(String[] args) {
        wvvvuwwu obj = new wvvvuwwu();
        obj.callFunc();

    }

}

总结

shopee的环境补起来相对容易, 唯一的难点在于 "异常的处理", 当然, 这只是对于我来说的难点. 对于各位大佬来说, 这种应该是信手拈来的东西.

接下来, 继续复现如画大佬的算法分析文章.