安卓協議逆向 cxdx 分析與實現
一、Kit
app 版本:5.0.0
設備:K40 刷 piexl 11 rom
抓包工具:Charles
反匯編工具:JEB、JADX、IDA
inject:frida
二、抓包
POST /v1/api/app/login/doLogin HTTP/1.1X-OsVersion: 30User-Agent: Mozilla/5.0 (Linux; Android 11; M2012K11AC Build/RQ3A.211001.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/108.0.5359.128 Mobile Safari/537.36 CSDNApp/5.0.0(Android)wToken/0.0.1X-RandomNum: 54736X-Access-Token: 00871d5df0d4f51efb5883b3b2fd2359platform: androidX-Ca-Signature-Headers: X-Ca-Timestamp,X-Ca-Key,X-Ca-NonceAuthorization:X-OS: Androidc_appVersion: 5.0.0X-App-ID: CSDN-APPX-App-Theme: daycontent-type: application/json; charset=UTF-8X-Ca-Signature: BqhPpXbobBOndykiyCtOVK06GHLkfLbs1y4B3Ek0gnY=X-ConnectionType: WIFIUserToken:X-TimeStamp: 1671939318488Cookie: UserName=;UserToken=X-Ca-Key: 203789067Accept: application/jsonX-Device-ID: aid0f0fef992b53479187546b3c621157f0wToken: e447_5rU5WWYQegPo5EMOZSnKzF4YPtJSetwo+lGrEUrtZaKEe73GkiTOoE83PLp0yivsi4pZV7HySc+lbsebppMqHlXmQwVLx0vrlQpYC0b99vOYBRZWnVbeMhLZ4WUuAAKH/V07WkNSNXORUsgHumLj1BZZ7x1riK8beahdp9ctmSwP3AdA/sZA4OkzEVF4rJ+G6nwyyGcI/JLoRH0/1hPUT91sdBKKA64yj1QKRAZuJjsX9WRcqo9xYgfcJDnpqVnhObGQD96CfSok8z8d9otv+Fl6ULZrddvcnvzs6cJhjuW3ryBn151Xat/2CU/9EXUEKG3e0g4/K9rEaDRb2JhDGEDwIj+Qd5RU1uaBKxS/7jlSVq8wQ7x3qVVN1tHqS4AXhVow6eMABT6PArcEfkFm42bwXOFWsAUd5C7uGvHGIlTGytU6Vx/CJPwPvCuSffef5mQL7daszEzN+zQJ9VjxgOrjKXkDVkt6O6UpyA+1u3lowOqUaSPK6u2vND/Xqus7&ff4b_85475962D8E15A4E7AE60ED42FF3568E8EDB86EE620E495591X-DeviceModel: Redmi M2012K11ACversion: 5.0.0X-Ca-Nonce: be0eca5c-e959-4b0f-b4e7-22e00118157eX-Ca-Timestamp: 1671939318489X-Sign: 70B21B02FD0EFD2353F0D7F4F2E7CDB6FC1C3C42Host: passport.csdn.netConnection: Keep-AliveAccept-Encoding: gzipContent-Length: 95{"pwdOrVerifyCode":"123456","loginType":"1","userIdentification":"17750659921","checkAli":true}
意料之中一大堆參數,反復幾次總結需分析的參數應該為以下幾個:
X-Sign、wToken、X-Ca-Signature、X-Access-Token、X-Ca-Timestamp
三、分析
先搜索 X-Sign,就一處。
跟進得:
public static Map z(String url, Map requestMap) { String str; String a2 = wo3.a(CSDNApp.csdnApp); HashMap hashMap = new HashMap(); hashMap.put("platform", "android"); hashMap.put("version", xn3.u()); hashMap.put("c_appVersion", xn3.u()); if (!TextUtils.isEmpty(it3.g())) { hashMap.put("JWT-TOKEN", it3.g()); no3.a("==JWT-TOKEN==", it3.g()); } hashMap.put("Authorization", StringUtils.isEmpty(it3.g()) ? "" : "Bearer " + it3.g()); hashMap.put("X-Device-ID", a2); hashMap.put("X-OS", "Android"); hashMap.put("X-App-ID", "CSDN-APP"); hashMap.put("X-Access-Token", MD5.md5(a2 + "AndroidCSDN-APPb85fF96d-7Aa4-4Ec1-bf1D-2133c1A45656")); hashMap.put("X-OsVersion", Build.VERSION.SDK_INT + ""); String str2 = Build.BRAND + Operators.SPACE_STR + Build.MODEL; int length = str2.length(); for (int i = 0; i < length; i++) { char charAt = str2.charAt(i); if ((charAt <= 31 && charAt != '\t') || charAt >= 127) { str2.replace(charAt, ' '); } } hashMap.put("X-DeviceModel", str2); hashMap.put("X-ConnectionType", yp3.b(CSDNApp.csdnApp)); hashMap.put("UserToken", StringUtils.isEmpty(it3.q()) ? "" : it3.q()); hashMap.put("X-App-Theme", CSDNApp.isDayMode ? "day" : "night"); int c2 = xn3.c(10000, 99999); String str3 = new Date().getTime() + ""; try { str = mq3.a(a2 + c2 + str3 + zf1.o); } catch (DigestException e2) { e2.printStackTrace(); str = ""; } hashMap.put("X-Sign", str); hashMap.put("X-RandomNum", c2 + ""); hashMap.put("X-TimeStamp", str3); StringBuilder sb = new StringBuilder(); sb.append("UserName="); sb.append(it3.p()); sb.append(";UserToken="); sb.append(StringUtils.isEmpty(it3.q()) ? "" : it3.q()); hashMap.put(IWebview.COOKIE, sb.toString()); if (!StringUtils.isEmpty(url) && requestMap != null && requestMap.containsKey("category")) { hashMap.put("X-PageKey", "blog." + requestMap.get("category")); hashMap.put("X-Path", "app.csdn.net/blog/" + requestMap.get("category")); } if (!StringUtils.isEmpty(url) && url.equals(s22.G0)) { hashMap.put("X-PageKey", vr3.Q6); hashMap.put("X-Path", "app.csdn.net/blog/detail"); if (requestMap != null && requestMap.containsKey("from")) { hashMap.put("X-Referer", "blog." + requestMap.get("from")); } } hashMap.put("User-Agent", CSDNApp.csdnApp.userAgent + " CSDNApp/" + xn3.u() + "(Android)wToken/0.0.1"); try { hashMap.put("wToken", TigerTallyAPI.vmpSign(1, str3.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e3) { e3.printStackTrace(); } return hashMap;}
pretty nice,很多參數都在這里,那就從上往下分析:
hashMap.put("platform", "android"); // 固定值hashMap.put("version", xn3.u()); // 固定值hashMap.put("c_appVersion", xn3.u()); // 固定值hashMap.put("Authorization", StringUtils.isEmpty(it3.g()) ? "" : "Bearer " + it3.g()); // 無用,請求頭中為空hashMap.put("X-Device-ID", a2); // a2 在 String a2 = wo3.a(CSDNApp.csdnApp);
hook wo3.a看看:
function main() { Java.perform(function () { var wo3 = Java.use("wo3"); wo3["a"].implementation = function (context) { console.log('a is called' + ', ' + 'context: ' + context); var ret = this.a(context); console.log('a ret value is ' + ret); return ret; }; });}setImmediate(main)
結果:
a is called, context: net.csdn.csdnplus.CSDNApp@dc96acba ret value is aid0f0fef992b53479187546b3c621157f0
多次 hook 該值并沒有改變,查看不同數據包的內容也是一樣的,但在 Java 層僅分析到 aid 復制點,后面數據同一設備都是一樣的,懷疑是消息散列值,有可能是 DeviceID 或者 UUID,有在 Java 層看到,但 hook 不到,往下分析:
hashMap.put("X-OS", "Android"); // 固定值hashMap.put("X-App-ID", "CSDN-APP"); // 固定值hashMap.put("X-Access-Token", MD5.md5(a2 + "AndroidCSDN-APPb85fF96d-7Aa4-4Ec1-bf1D-2133c1A45656")); // a2 就是上面的 X-Device-ID 再進行加鹽處理后進行 MD5 加密復現一下:
from hashlib import md5 def encrypt_md5(mes): new_md5 = md5() # 這里必須用encode()函數對字符串進行編碼,不然會報 TypeError: Unicode-objects must be encoded before hashing new_md5.update(mes.encode(encoding='utf-8')) # 加密 return new_md5.hexdigest() if __name__ == '__main__': print(encrypt_md5('aid0f0fef992b53479187546b3c621157f0AndroidCSDN-APPb85fF96d-7Aa4-4Ec1-bf1D-2133c1A45656'))
結果 00871d5df0d4f51efb5883b3b2fd2359,校驗無誤,繼續往下:
hashMap.put("X-OsVersion", Build.VERSION.SDK_INT + ""); // sdk 版本吧,可隨機的樣子hashMap.put("X-DeviceModel", str2); // 通過上面獲取來的,分析一下就是手機 + 手機名稱hashMap.put("X-ConnectionType", yp3.b(CSDNApp.csdnApp)); // 網絡連接類型hashMap.put("UserToken", StringUtils.isEmpty(it3.q()) ? "" : it3.q()); // 抓包為空值,放棄hashMap.put("X-App-Theme", CSDNApp.isDayMode ? "day" : "night"); // 主題模式hashMap.put("X-Sign", str);
str 是上面計算來的,拿來分析下:
int c2 = xn3.c(10000, 99999);String str3 = new Date().getTime() + "";try { str = mq3.a(a2 + c2 + str3 + zf1.o);} catch (DigestException e2) { e2.printStackTrace(); str = "";}hashMap.put("X-Sign", str);
c2:10000 - 99999 之間的隨機值;str3:時間戳轉字符串;a2:上面分析過為 X-Device-ID 值;zf1.o:跟進查看為定值:public static final String o = "F403F982CA92F73AC142D50FFA69853D";
參數搞定,看 mq3.a 方法:
public static String a(String decrypt) throws DigestException { try { MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); messageDigest.update(decrypt.getBytes()); byte[] digest = messageDigest.digest(); StringBuffer stringBuffer = new StringBuffer(); for (byte b : digest) { String hexString = Integer.toHexString(b & 255); if (hexString.length() < 2) { stringBuffer.append(0); } stringBuffer.append(hexString); } return stringBuffer.toString().toUpperCase(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new DigestException("簽名錯誤!"); }}
SHA1算法,先 hook 再還原:
function main() { Java.perform(function () { var mq3 = Java.use("mq3"); mq3["a"].implementation = function (decrypt) { console.log('a is called' + ', ' + 'decrypt: ' + decrypt); var ret = this.a(decrypt); console.log('a ret value is ' + ret); return ret; }; });}setImmediate(main)// frida -FU -l CSDN/csdn.js
結果:
a is called, decrypt:aid0f0fef992b53479187546b3c621157f0709751671957807054F403F982CA92F73AC142D50FFA69853Da ret value is E11539C9183644EEB69C7FEBAC1D58A2D874895C
還原:
import hashlib # 使用sha1加密算法,返回str加密后的字符串def sha1_secret_str(s: str): sha = hashlib.sha1(s.encode('utf-8')) encrypts = sha.hexdigest() return encrypts.upper() if __name__ == '__main__': # 待加密的字符串 s = 'aid0f0fef992b53479187546b3c621157f0709751671957807054F403F982CA92F73AC142D50FFA69853D' res = sha1_secret_str(s) print(res)
結果:E11539C9183644EEB69C7FEBAC1D58A2D874895C 校驗無誤,繼續往下分析:
hashMap.put("X-RandomNum", c2 + ""); // 10000 - 99999 之間的隨機值;hashMap.put("X-TimeStamp", str3); // str3:時間戳轉字符串;hashMap.put(IWebview.COOKIE, sb.toString()); // 看抓包結果應該是 Cookie 值,對照結果 UserName=;UserToken= 啥操作沒做,應該是要登錄后才有值也是固定值hashMap.put("User-Agent", CSDNApp.csdnApp.userAgent + " CSDNApp/" + xn3.u() + "(Android)wToken/0.0.1");
對照其抓包內容:CSDNApp.csdnApp.userAgent 獲取設備 header,xn3.u() 獲取 app 版本號。
// Mozilla/5.0 (Linux; Android 11; M2012K11AC Build/RQ3A.211001.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/108.0.5359.128 Mobile Safari/537.36 CSDNApp/5.0.0(Android)wToken/0.0.1hashMap.put("wToken", TigerTallyAPI.vmpSign(1, str3.getBytes("UTF-8")));
看到 wToken 就有點不好的預感,應該是阿里安全的,前段時間很火的某box就是用的這個,跟進看看,最終定位到:
private static native String _genericNt3(int i, byte[] bArr);
so 層了,放著先看看別的參數吧,這塊代碼,能解決的參數都解決了,但 X-Ca-Signature 沒在這出現,jadx 再搜跟到以下代碼:
private static Map<String, String> c(StringBuilder strBuilder, Map<String, String> headerParams) { if (ft3.b() != 1) { headerParams.put("X-Ca-Stage", ft3.b() == 2 ? "TEST" : "PRE"); } try { Mac mac = Mac.getInstance("HmacSHA256"); byte[] bytes = y12.c.getBytes("UTF-8"); mac.init(new SecretKeySpec(bytes, 0, bytes.length, "HmacSHA256")); String str = new String(Base64.encodeBase64(mac.doFinal(strBuilder.toString().getBytes("UTF-8")))); headerParams.put("X-Ca-Signature", str); no3.a("==HmacSHA256==", str); } catch (UnsupportedEncodingException e2) { e2.printStackTrace(); } catch (InvalidKeyException e3) { e3.printStackTrace(); } catch (NoSuchAlgorithmException e4) { e4.printStackTrace(); } return headerParams;}
不難看出該值是通過 HmacSHA256 加密后再進行 Base64 編碼后得到的結果,其 key 值為:
public static final String c = "0u94vkvsewic9kkgsp1r3nuq3ir0lv3n";hook 看看要加密 strBuilder 有啥:function main() { Java.perform(function () { var r12 = Java.use("r12"); r12["c"].implementation = function (strBuilder, headerParams) { console.log('strBuilder: ' + strBuilder); var ret = this.c(strBuilder, headerParams); var keyset = ret.keySet(); var result = ""; var it = keyset.iterator(); while (it.hasNext()) { var keystr = it.next().toString(); var valuestr = ret.get(keystr).toString(); console.log(keystr) console.log(valuestr) result += valuestr; } return ret; }; });}setImmediate(main)// frida -FU -l CSDN/csdn.js
結果:
strBuilder:POSTapplication/jsonapplication/json; charset=UTF-8X-Ca-Key:203789067X-Ca-Nonce:3339aae3-e295-410c-8345-52c9ebc56b5aX-Ca-Timestamp:1671963564343/v1/api/app/login/doLogin
其中 X-Ca-Key 是個定值:
headerParams.put("X-Ca-Key", y12.b);public static final String b = "203789067";X-Ca-Nonce 為 UUID 值:headerParams.put("X-Ca-Nonce", UUID.randomUUID().toString());X-Ca-Timestamp 為時間戳。
python還原算法,與抓包結果一致:
from hashlib import sha256import hmac, base64 def get_sign(data, key): key = key.encode('utf-8') message = data.encode('utf-8') sign = base64.b64encode(hmac.new(key, message, digestmod=sha256).digest()).decode() print(sign) return signif __name__ == '__main__': # 待加密的字符串 s = "POST" + "" + \ "application/json" + "" + \ "" + \ "application/json; charset=UTF-8" + "" + \ "" + \ "X-Ca-Key:203789067" + "" + \ "X-Ca-Nonce:3339aae3-e295-410c-8345-52c9ebc56b5a" + "" + \ "X-Ca-Timestamp:1671963564343" + "" + \ "/v1/api/app/login/doLogin" k = '0u94vkvsewic9kkgsp1r3nuq3ir0lv3n' res = get_sign(s, k) print(res)
抓包結果:QJYeguZxkE+ZojwTP0rIJ+IzjaSHI82uR2y0xOIG35U=
到這幾乎參數都解決了,剩個 so 層的 wtoken,在 Java 層能夠定位到:com/aliyun/TigerTally/TigerTallyAPI,其中,要找的就是 libtiger_tally.so 了:
static { System.loadLibrary("tiger_tally");}
換思路做吧,不想折騰,想了想我直接 rpc 調用不就好了。
四、rpc 遠程調用
frida rpc 腳本:
var response = null;Java.enumerateClassLoaders({ onMatch: function (loader) { try { if (loader.findClass("com.aliyun.TigerTally.TigerTallyAPI")) { Java.classFactory.loader = loader; response = Java.use("com.aliyun.TigerTally.TigerTallyAPI") } else { } } catch (error) { } }, onComplete: function () { }}); function stringToByte (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;} function getwwoken(data){ var result = response._genericNt3(1, stringToByte(data)); return result;} rpc.exports = { getwtoken:getwwoken}
補環境:
import timeimport hmacimport uuidimport randomimport base64from hashlib import sha256, md5, sha1 def get_x_osversion(): return "30" def get_x_os(): return "android" def get_x_appid(): return "CSDN_APP" def get_x_app_theme(): return "day" def get_x_connection_type(): return "WIFI" def get_x_timestramp(): return str(int(time.time() * 1000)) def get_x_device_model(): return "Redmi M2012K11AC" def get_x_ca_Signature_Headers(): return "X-Ca-Timestamp,X-Ca-Key,X-Ca-Nonce" def get_User_Agent(): return "Mozilla/5.0 (Linux; Android 11; M2012K11AC Build/RQ3A.211001.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/108.0.5359.128 Mobile Safari/537.36 CSDNApp/5.0.0(Android)wToken/0.0.1" def get_x_ca_nonce(): return str(uuid.uuid4()) def get_x_device_id(): return "aid0f0fef992b53479187546b3c621157f0" def get_access_token(): X_Access_Token = "aid0f0fef992b53479187546b3c621157f0AndroidCSDN-APPb85fF96d-7Aa4-4Ec1-bf1D-2133c1A45656" md5_mes = md5(X_Access_Token.encode()) return md5_mes.hexdigest() x_timestramp = get_x_timestramp()x_ca_nonce = get_x_ca_nonce()def get_x_randomnum(): return str(random.randint(10000, 99999)) def get_x_ca_signature(): data = "POST" + "" + \ "application/json" + "" + \ "" + \ "application/json; charset=UTF-8" + "" + \ "" + \ "X-Ca-Key:203789067" + "" + \ "X-Ca-Nonce:3339aae3-e295-410c-8345-52c9ebc56b5a" + "" + \ "X-Ca-Timestamp:1671963564343" + "" + \ "/v1/api/app/login/doLogin" key = "0u94vkvsewic9kkgsp1r3nuq3ir0lv3n".encode("utf-8") message = data.encode("utf-8") sign = base64.b64encode(hmac.new(key, message, digestmod=sha256).digest()) return str(sign, 'utf-8'), x_timestramp, x_ca_nonce def get_xsign(): xsign_mes = get_x_device_id() + get_x_randomnum() + x_timestramp + "F403F982CA92F73AC142D50FFA69853D" return sha1(xsign_mes.encode("utf-8")).hexdigest().upper()
遠程調用:
import fridafrom login_env import *import requests def on_message(message, data): print("[%s] => %s" % (message, data)) def inject_hook(): session = frida.get_usb_device().attach('net.csdn.csdnplus') with open('CSDN/rpc.js', 'r') as f: js_code = f.read() script = session.create_script(js_code) script.on('message', on_message) script.load() return script def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) def req(): url = "https://passport.csdn.net/v1/api/app/login/doLogin" headers = { "X-OsVersion": "30", "User-Agent": "Mozilla/5.0 (Linux; Android 11; M2012K11AC Build/RQ3A.211001.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/108.0.5359.128 Mobile Safari/537.36 CSDNApp/5.0.0(Android)wToken/0.0.1", "X-RandomNum": get_x_randomnum(), "X-Access-Token": get_access_token(), "platform": "android", "X-Ca-Signature-Headers": "X-Ca-Timestamp,X-Ca-Key,X-Ca-Nonce", "Authorization": "", "X-OS": "Android", "c_appVersion": "5.0.0", "X-App-ID": "CSDN-APP", "X-App-Theme": "day", "content-type": "application/json; charset=UTF-8", "X-Ca-Signature": get_x_ca_signature()[0], "X-ConnectionType": "WIFI", "UserToken": "", "X-TimeStamp": get_x_ca_signature()[1], "X-Ca-Key": "203789067", "Accept": "application/json", "X-Device-ID": get_x_device_id(), "wToken": res, "X-DeviceModel": "Redmi M2012K11AC", "version": "5.0.0", "X-Ca-Nonce": get_x_ca_signature()[2], "X-Ca-Timestamp": get_x_ca_signature()[1], "X-Sign": get_xsign(), "Host": "passport.csdn.net", "Connection": "Keep-Alive", "Accept-Encoding": "gzip", } data = {"pwdOrVerifyCode":"123456","loginType":"1","userIdentification":"17750659921","checkAli":"true"} response = requests.post(url, headers=headers,json=data) print(response.text) if __name__ == '__main__': rpc_script = inject_hook() res = rpc_script.exports.getwtoken(get_x_ca_signature()[1]) req()'''
結果,校驗一致:
{"message":"用戶名或密碼錯誤","status":false,"code":"1039"}!