本文是跟我學習爬蟲的小夥伴:彭良懷的投稿,稿費是500。本文寫得非常好,完全可以當著APP逆向抓取的教程來學。從逆向思路的分析,逆向工具的搭配使用,到逆向知識結構的掌握,顯示出了他紮實的爬蟲逆向基礎功底。
PS:他在北京,有看上的老闆可以私信我,為人也不錯。
一、前言
-
一部 root 後的安卓手機,模擬器也可以 -
抓包工具:Charles -
查殼工具:APK Messenger -
APK反編譯工具:jadx-gui 1.1 -
SO檔案分析工具:IDA_Pro_v7.0 -
Hook 框架:frida
二、抓包分析
-
signKey:密文,長度 32 位,可能為 MD5、HmacMD5 加密或隨機 UUID -
signKeyV1:密文,長度 64 位,可能為 SHA256、HmacSHA256 加密 -
t :13 位時間戳 -
traceId:等於 deviceId (固定的裝置ID)加兩個13位時間戳 -
currentPage:頁碼 -
lastStoreId:上一頁最後一家店鋪 ID
三、Java層分析
1. 查殼
2. 分析關鍵 Java 程式碼
-
t 為當前時間戳; -
subVersion 為當前APP版本號; -
signKey 是由 k 方法生成的; -
signKeyV1 等於 KEY_NEW_SIGN, KEY_NEW_SIGN 又是由 k2 方法生成的; -
傳入方法 k2 的引數為 formatQueryParaMap 方法的返回值; -
方法 k 和 k2 都在 native 層,載入的是 libjdpdj.so 檔案;
3. 分析 formatQueryParaMap 方法
def formatQueryParaMap(param: dict) -> str:
return '&'.join(param[k] for k in sorted(param.keys()) if k != 'functionId')
4. Hook formatQueryParaMap 方法
Java.perform(function () {
var util = Java.use('jd.net.ASCIISortUtil');
util.formatQueryParaMap.implementation = function (arg1, arg2) {
console.log('param1: ', arg1);
console.log('param2: ', arg2);
var result = this.formatQueryParaMap(arg1, arg2);
console.log('return: ', result);
return result;
};
})
5. 關於反除錯
四、Native層分析
1. 靜態註冊和動態註冊
-
靜態註冊: 靜態註冊是透過固定格式方法名進行關聯,命名規則如下: native 函式名 = Java + 包名 + 類名 + 方法名 例如,包名: com.example.test,類名:jd.net.z,方法名:k 如果是靜態註冊的話,那麼 native 中的函式名就該為:Java_com_example_test_jd_net_z_k -
動態註冊: 動態註冊是透過 RegisterNative() 這個 JNI 函式動態新增對映關係來進行關聯的,這種方式可以隨便命名函式名,比較靈活。其申明示例如下: -
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod* methods, jint nMethods)
第 1 個引數是 JNIEnv 指標,所有 JNI 函式第一個引數都是它; 第 2 個引數 clazz 是註冊方法對應 Java 層中的類,由 FindClass 函式獲取; 第 3 個引數 methods 是一個陣列,其中包含了註冊方法結構體資訊,我們可以從中找到註冊前後的方法名,所以我們注意這個引數就行了; 第 4 個引數 nMethods 是動態註冊方法的數量。
2. 找到 k、k2 對應的 native 函式
3. JNI 靜態除錯的一些技巧
(1) 批次還原 JNI 函式名
-
按 Ctrl + F9 ,選擇 jni.h 標頭檔案匯入 -
匯入成功後,滑鼠左鍵點選其中一個 JNI 函式的引數,然後右鍵選擇 Convert to Struct * -
在彈出的 Select a structure 視窗中 選擇 _JNIEnv,點選OK
(2) 強制調出函式引數
(3) 常用快捷鍵
-
shift + F12:檢視so檔案中所有常量字串的值; -
tab鍵:彙編和偽 C 程式碼之間相互切換; -
/ 鍵:新增註釋; -
N 鍵:變數重新命名; -
X 鍵:檢視某變數的所有引用; -
= 鍵:消除冗餘的中間變數; 由於 IDA 反編譯出來總是會有很多冗餘的中間變數,如: v2 = v1;
result = encrypt(v2);選中 v2,按鍵盤上的 = 鍵,再點選 OK,即可消除中間變數 v2: result = encrypt(v1);
(4) 靜態除錯思路
-
根據函式入參,至上而下分析 -
根據函式返回值,至下而上分析 -
尋找關鍵的函式進行分析,一般可以把函式分為以下幾種: ① 標準庫函式:如 strlen(),計算字串的長度,見名知意; ② JNI 函式:如 FindClass(),呼叫 Java中的類,JNI 函式一般也是見名知意; ③ 使用者自定義的函式:如 MD5::MD5(),一看就知道是 MD5 加密,這類需特別注意; ④ IDA命名的函式:如 sub_567C(),IDA 會對沒有名字的函式自動命名,命名規則就是 sub_ + 函式地址,這類函式也是重點。 從追求效率的角度來說,最好先找關鍵函式,看看有沒有常見的加密函式名,找到後直接用frida hook,一些簡單的往往能夠一擊中的,快速搞定。從學技術的角度來說,可以多嘗試一行一行程式碼地分析,鍛鍊看程式碼的能力。當然複雜點的還不得不分析 arm 指令,要是被混淆後就更加難了,難的我也不會,以後多練多學吧。
4. 靜態分析 gk 函式
var onload_addr = Module.getExportByName('libjdpdj.so', 'JNI_OnLoad');
-
方式一: -
// 獲取JNI_OnLoad的地址:
var onload_addr = Module.getExportByName('libjdpdj.so', 'JNI_OnLoad');
// 基地址 = JNI_OnLoad地址 - JNI_OnLoad偏移:
var base_addr = parseInt(onload_addr ) - parseInt('0x34D6C');
// MD5Update地址 = 基地址 + MD5Update偏移:
var md5_update_addr = ptr(base_addr + parseInt('0x34E18'));
-
方式二: -
var onload_addr = Module.getExportByName('libjdpdj.so', 'JNI_OnLoad');
var md5_update_addr = onload_addr.sub(0x34D6C).add(0x34E18);
-
方式三: -
var md5_update_addr = Module.findBaseAddress("libjdpdj.so").add(0x34E18 + 1);
方式一看註釋很好理解,方式二其實就是方式一的簡化,用 frida 提供的的 add() 和 sub() 函式進行地址的加減。方式三是進一步簡化,但是用這種方式一定要記得對地址 +1,為什麼要 +1 呢?我引用趙四的原話解釋吧: 因為thumb和arm指令的區分,地址最後一位的奇偶性來進行標誌 獲取未匯出函式地址的方式也完全適用於匯出函式,所以不管匯出還是未匯出,我都用方式三獲取,程式碼簡單優雅。
var pointer = Module.findBaseAddress("libjdpdj.so").add(0x34E18 + 1);
console.log('MD5Update pointer:', pointer);
Interceptor.attach(pointer, {
onEnter: function(args) {
console.log('引數1:', args[0]);
console.log('引數2:', Memory.readCString(args[1])); // Memory.readCString()就是讀取地址為字串
console.log('引數3:', parseInt(args[2]));
console.log('----------------');
},
onLeave: function(retval) {
}
})
MD5Update pointer: 0xaed5ae19
引數1: 0xbef0eb8c
引數2: {"city":"重慶市","latitude":29.57252,"longitude":106.53355,"address":"觀音橋", "coordType":"2","channelId":"4037","appVersion":"7.4.0","platform":"2","currentPage":1, "pageSize":10,"areaCode":4,"ref":"home","ctp":"channel"}923047ae3f8d11d8b19aeb9f3d1bc002
引數3: 259
5. 靜態分析 gk2 函式
var pointer = Module.findBaseAddress("libjdpdj.so").add(0x361B8 + 1);
console.log("hmac_sha256 pointer: ", pointer);
Interceptor.attach(pointer, {
onEnter: function(args) {
console.log("引數1:", Memory.readUtf8String(args[0]));
console.log("引數2:", parseInt(args[1]));
console.log("引數3:", Memory.readCString(args[2]));
console.log("引數4:", parseInt(args[3]));
console.log('---------------');
},
onLeave:function(retval){
}
});
-
先呼叫 java 層的 getsign 方法獲取基礎 key, -
對基礎 key 每個字元的 ASCII 碼進行修改,同時拼接到輸入引數的尾部, -
取出入參尾部的 32 位作為金鑰, -
最後對輸入引數進行 hmac_sha256 加密,透過指標返回加密結果。