XX客戶端APP簽名分析之算法分析篇
前言
將脫殼后的dex文件在jadx打開,由于脫殼后會產生多個dex,因此先用腳本合并一下比較方便。當然最新版jadx支持增加文件,也可以直接添加。
import os, sys
path = r'xxxx'# 文件夾目錄files = os.listdir(path) out_path =r'xxxxx' #路徑s = []for file in files: # 遍歷文件夾 if file.find("dex") > 0: ## 查找dex 文件 sh = f'jadx -j 1 -r -d {out_path} {path}\\{file}' print(sh) os.system(sh)`
同時使用抓包軟件開始對app進行抓包,對于移動端的app抓包,證書的安裝一直是一個很繁瑣的問題,在這里推薦一款Magisk的模塊Move Certificates,可以很方便的將用戶證書移動到系統層,實現https的抓包。
這里主要的功能側重點在bbsapi.domo.cn和class.domo.cn上,因為該app主要的功能點就是在這兩個地址下。

查看一下數據包的內容,可以看到path中是包含sign值的

/newapi/live/square/live-count?noncestr=84140992&sign=47e714f977952ad75eb6eeb3165083dae93502b3×tamp=1627008983764<
目標就是要找到這個sign值的生成算法。首先到jad中進行代碼定位,為了降低逆向難度,可以選擇從noncestr入手,進行搜索可能會簡單一點,當然這只能是經驗之談,一切還是要以實際分析情況為準。

可以看一下如果搜索sign,會有多少結果。

繼續看noncestr,看到搜索到了14個相關代碼,根據類名來看,選擇一個最最有可能的先看一下。


可以看到是有getSign函數在這個類下面的,采用直接hook這個函數也不是很現實,由于jadx的問題,是無法顯示該函數下的代碼,這個時候,可以采用直接hook該函數的系統加密函數的方法,來查看是否是直接采用了比如MD5,SHA1,Base64等等常規加密的方法,對于此,直接hook后查看輸入和輸出。
編寫frida腳本進行測試一下。


update:appSignKey=ac6190d7dfaa77df726f0a82244d3eda68675ccd4e95de802f5042e91d15edc7bae3026d8f0fb2a8287446bb289563970264&noncestr=12520893×tamp=1627023153533digest:e2931f94fdf5cbad61d95bf23b192841a12f1a8c
可以看到上傳數據的構成是appSignKey+noncestr和timestamp構成。多次抓包發現appSignKey是一個固定值(只是class.domo.cn下的固定值,該app不同的網址采用不同的Key值)并且結果跟抓包數據完全一致。這時候主要的問題就是找到使用的默認算法。可以看到第一個調用的函數就是java.security.MessageDigest.digest(Native Method),該類提供了消息摘要算法,如 MD5 或 SHA,的功能。因此可以模擬測試一下,看一下采用的是MD5還是SHA算法。

測試發現采用的是SHA1算法。noncestr就是隨機8位數字組成,每次可以采用上一次所生成的,或者是自己生成即可。noncestr算法還原。
Random random=new Random();
for(int i=0;i<8;i++){ str.append(random.nextInt(10));}
int num=Integer.parseInt(str.toString());System.out.println(num);
此外,對于bbsapi.domo.cn,它的update是
appSignKey=Wj8BI3VUZ6BuojAkqzBM3HWHNHv08xdZEtaksbRg6snnuLsvivwa8IvR6PvQ76H0IQQsqkIsa5OKJtg6QcBMfCblMMywgZaA8co&noncestr=63606047×tamp=1627023164959采用了不同的key值來進行拼接。
總結
該app的簽名算法還原后即為,key+noncestr+timestamp。之后進行SHA1加密,生成sign。最后附上frida hook代碼
base64DecodeChars = new Array((-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), 62, (-1), (-1), (-1), 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, (-1), (-1), (-1), (-1), (-1), (-1), (-1), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, (-1), (-1), (-1), (-1), (-1), (-1), 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, (-1), (-1), (-1), (-1), (-1));var stringToBase64 = function (e) { var r, a, c, h, o, t; for (c = e.length, a = 0, r = ''; a < c;) { if (h = 255 & e.charCodeAt(a++), a == c) { r += base64EncodeChars.charAt(h >> 2), r += base64EncodeChars.charAt((3 & h) << 4), r += '=='; break } if (o = e.charCodeAt(a++), a == c) { r += base64EncodeChars.charAt(h >> 2), r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4), r += base64EncodeChars.charAt((15 & o) << 2), r += '='; break } t = e.charCodeAt(a++), r += base64EncodeChars.charAt(h >> 2), r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4), r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6), r += base64EncodeChars.charAt(63 & t) } return r}var base64ToString = function (e) { var r, a, c, h, o, t, d; for (t = e.length, o = 0, d = ''; o < t;) { do r = base64DecodeChars[255 & e.charCodeAt(o++)]; while (o < t && r == -1); if (r == -1) break; do a = base64DecodeChars[255 & e.charCodeAt(o++)]; while (o < t && a == -1); if (a == -1) break; d += String.fromCharCode(r << 2 | (48 & a) >> 4); do { if (c = 255 & e.charCodeAt(o++), 61 == c) return d; c = base64DecodeChars[c] } while (o < t && c == -1); if (c == -1) break; d += String.fromCharCode((15 & a) << 4 | (60 & c) >> 2); do { if (h = 255 & e.charCodeAt(o++), 61 == h) return d; h = base64DecodeChars[h] } while (o < t && h == -1); if (h == -1) break; d += String.fromCharCode((3 & c) << 6 | h) } return d}
var hexToBytes = function (str) { var pos = 0; var len = str.length; if (len % 2 != 0) { return null; } len /= 2; var hexA = new Array(); for (var i = 0; i < len; i++) { var s = str.substr(pos, 2); var v = parseInt(s, 16); hexA.push(v); pos += 2; } return hexA;}var bytesToHex = function (arr) { var str = ''; var k, j; for (var i = 0; i < arr.length; i++) { k = arr[i]; j = k; if (k < 0) { j = k + 256; } if (j < 16) { str += "0"; } str += j.toString(16); } return str;}var stringToHex = function (str) { var val = ""; for (var i = 0; i < str.length; i++) { if (val == "") val = str.charCodeAt(i).toString(16); else val += str.charCodeAt(i).toString(16); } return val}var stringToBytes = function (str) { var ch, st, re = []; for (var i = 0; i < str.length; i++) { ch = str.charCodeAt(i); st = [];
do { st.push(ch & 0xFF); ch = ch >> 8; } while (ch); re = re.concat(st.reverse()); } return re;}
var bytesToString = function (arr) { var str = ''; arr = new Uint8Array(arr); for (var i in arr) { str += String.fromCharCode(arr[i]); } return str;}var bytesToBase64 = function (e) { var r, a, c, h, o, t; for (c = e.length, a = 0, r = ''; a < c;) { if (h = 255 & e[a++], a == c) { r += base64EncodeChars.charAt(h >> 2), r += base64EncodeChars.charAt((3 & h) << 4), r += '=='; break } if (o = e[a++], a == c) { r += base64EncodeChars.charAt(h >> 2), r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4), r += base64EncodeChars.charAt((15 & o) << 2), r += '='; break } t = e[a++], r += base64EncodeChars.charAt(h >> 2), r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4), r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6), r += base64EncodeChars.charAt(63 & t) } return r}var base64ToBytes = function (e) { var r, a, c, h, o, t, d; for (t = e.length, o = 0, d = []; o < t;) { do r = base64DecodeChars[255 & e.charCodeAt(o++)]; while (o < t && r == -1); if (r == -1) break; do a = base64DecodeChars[255 & e.charCodeAt(o++)]; while (o < t && a == -1); if (a == -1) break; d.push(r << 2 | (48 & a) >> 4); do { if (c = 255 & e.charCodeAt(o++), 61 == c) return d; c = base64DecodeChars[c] } while (o < t && c == -1); if (c == -1) break; d.push((15 & a) << 4 | (60 & c) >> 2); do { if (h = 255 & e.charCodeAt(o++), 61 == h) return d; h = base64DecodeChars[h] } while (o < t && h == -1); if (h == -1) break; d.push((3 & c) << 6 | h) } return d}
Java.perform(function () {
function showStacks() { console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); }
var secretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec'); secretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function (a, b) { showStacks(); var result = this.$init(a, b); console.log("======================================"); console.log("算法名:" + b + "|Dec密鑰:" + bytesToString(a)); console.log("算法名:" + b + "|Hex密鑰:" + bytesToHex(a)); return result; }
var mac = Java.use('javax.crypto.Mac'); mac.getInstance.overload('java.lang.String').implementation = function (a) { showStacks(); var result = this.getInstance(a); console.log("======================================"); console.log("算法名:" + a); return result; } mac.update.overload('[B').implementation = function (a) { showStacks(); this.update(a); console.log("======================================"); console.log("update:" + bytesToString(a)) } mac.update.overload('[B', 'int', 'int').implementation = function (a, b, c) { showStacks(); this.update(a, b, c) console.log("======================================"); console.log("update:" + bytesToString(a) + "|" + b + "|" + c); } mac.doFinal.overload().implementation = function () { showStacks(); var result = this.doFinal(); console.log("======================================"); console.log("doFinal結果:" + bytesToHex(result)); console.log("doFinal結果:" + bytesToBase64(result)); return result; } mac.doFinal.overload('[B').implementation = function (a) { showStacks(); var result = this.doFinal(a); console.log("======================================"); console.log("doFinal參數:" + bytesToString(a)); console.log("doFinal結果:" + bytesToHex(result)); console.log("doFinal結果:" + bytesToBase64(result)); return result; }
var md = Java.use('java.security.MessageDigest'); md.getInstance.overload('java.lang.String', 'java.lang.String').implementation = function (a, b) { showStacks(); console.log("======================================"); console.log("算法名:" + a); return this.getInstance(a, b); } md.getInstance.overload('java.lang.String').implementation = function (a) { showStacks(); console.log("======================================"); console.log("算法名:" + a); return this.getInstance(a); }
md.update.overload('[B').implementation = function (a) { showStacks(); console.log("======================================"); console.log("update:" + bytesToString(a)) return this.update(a); } md.update.overload('[B', 'int', 'int').implementation = function (a, b, c) { showStacks(); console.log("======================================"); console.log("update:" + bytesToString(a) + "|" + b + "|" + c); return this.update(a, b, c); }
md.digest.overload().implementation = function () { showStacks(); console.log("======================================"); var result = this.digest(); console.log("digest結果:" + bytesToHex(result)); console.log("digest結果:" + bytesToBase64(result)); return result; } md.digest.overload('[B').implementation = function (a) { showStacks(); console.log("======================================"); console.log("digest參數:" + bytesToString(a)); var result = this.digest(a); console.log("digest結果:" + bytesToHex(result)); console.log("digest結果:" + bytesToBase64(result)); return result; }
var ivParameterSpec = Java.use('javax.crypto.spec.IvParameterSpec'); ivParameterSpec.$init.overload('[B').implementation = function (a) { showStacks(); var result = this.$init(a); console.log("======================================"); console.log("iv向量:" + bytesToString(a)); console.log("iv向量:" + bytesToHex(a)); return result; }
var cipher = Java.use('javax.crypto.Cipher'); cipher.getInstance.overload('java.lang.String').implementation = function (a) { showStacks(); var result = this.getInstance(a); console.log("======================================"); console.log("模式填充:" + a); return result; } cipher.update.overload('[B').implementation = function (a) { showStacks(); var result = this.update(a); console.log("======================================"); console.log("update:" + bytesToString(a)); return result; } cipher.update.overload('[B', 'int', 'int').implementation = function (a, b, c) { showStacks(); var result = this.update(a, b, c); console.log("======================================"); console.log("update:" + bytesToString(a) + "|" + b + "|" + c); return result; } cipher.doFinal.overload().implementation = function () { showStacks(); var result = this.doFinal(); console.log("======================================"); console.log("doFinal結果:" + bytesToHex(result)); console.log("doFinal結果:" + bytesToBase64(result)); return result; } cipher.doFinal.overload('[B').implementation = function (a) { showStacks(); var result = this.doFinal(a); console.log("======================================"); console.log("doFinal參數:" + bytesToString(a)); console.log("doFinal結果:" + bytesToHex(result)); console.log("doFinal結果:" + bytesToBase64(result)); return result; }
var x509EncodedKeySpec = Java.use('java.security.spec.X509EncodedKeySpec'); x509EncodedKeySpec.$init.overload('[B').implementation = function (a) { showStacks(); var result = this.$init(a); console.log("======================================"); console.log("RSA密鑰:" + bytesToBase64(a)); return result; }
var rSAPublicKeySpec = Java.use('java.security.spec.RSAPublicKeySpec'); rSAPublicKeySpec.$init.overload('java.math.BigInteger', 'java.math.BigInteger').implementation = function (a, b) { showStacks(); var result = this.$init(a, b); console.log("======================================"); //console.log("RSA密鑰:" + bytesToBase64(a)); console.log("RSA密鑰N:" + a.toString(16)); console.log("RSA密鑰E:" + b.toString(16)); return result; }
});
算法還原之后,下一篇將進行完結,打造bp插件,實現無縫銜接測試。