感谢杨如画大佬分析的文章, 这个文章是复现大佬的文章. 动手了才是自己的.
抓包
- Pixel3
- Reqable
咱们的任务需求主要是搞定这几个算法.
定位加密位置
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
💡 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();
}
}
发现没啥异常, 咱们在写一下主动调用的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());
}
到此,我们的逆向工作陷入僵局. 我们必须要解决了这个错误, 否则只能delete进入回收站了🥹
3.1.2 Unidbg异常分析处理(烂尾等大佬指导)
对于异常, 我们就不能再像前文,无脑Unidbg 黑盒执行. 打开IDA
跳转到异常的位置
从报错日志上面看
[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
龙哥,也说了句. 龙哥就是真理
然后就过了. 可以开始正常的补环境.
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的环境补起来相对容易, 唯一的难点在于 "异常的处理", 当然, 这只是对于我来说的难点. 对于各位大佬来说, 这种应该是信手拈来的东西.
接下来, 继续复现如画大佬的算法分析文章.