協議分析實戰
協議分析是逆向技術中的一個重要技能,本篇文章先分享3個app。
第一個app
找到登錄界面,我輸入的手機號是13905376666,密碼是:666666666。

先抓包:

這個是請求包:
http://m.xxxx.com.cn/v2/member?modules=cloudlogin%3A1&password=666666666&siteid=10001&sign=d01f5e4445af0a30d148d4dc451b41cf&clientid=1&system_name=android&type=android&time=1659366738640&ip=10.0.2.15&device_id=2a%3Aa5%3A33%3A2d%3Ade%3Ac9&account=13905376666 HTTP/1.1
第一步,也是最重要的一步,得先定位到關鍵代碼的位置:
(這里插一句,分析協議字段的時候一般都是最后分析sign
字段,這個sign字段經過我這幾天的學習總結出來的一般是這樣生成的:
先將其他字段加密之后,然后有的還可能帶一些鹽,最后將各個字段排序之后,然后加密,MD5一般是)
回到分析過程中,這里發現,只有sign字段看著像加密的,其他的基本都是明文,然后找到兩個方法,分別下斷點,然后看看他走哪個就完事了。
這里我打算搜索post請求中的v2/member,和"system_name"。
最終定位到這個兩個方法中比較像:


然后動態調試的時候我怕段不下來,就遇見sign都下了斷點(這也是一種方法吧哈哈,但是他會在一些奇奇怪怪的方法中段下來,但是只要不是你想要的斷點處,直接讓程序繼續運行就好了),然后發現我的判斷沒有錯:(當點擊登錄按鈕的時候程序從這里斷了下來)

這個協議分析我也是剛剛開始自己動手,然后就打算把步驟寫的詳細一點:
首先看這個函數的兩個參數:
第一個參數表示一個集合,在這個集合中保存發往服務器的數據包中的各個參數,然后第二個參數的翻譯是上下文,所以這個參數應該就是連接上下函數的一個變量。

然后看這個device_id就是表示手機的串號:

是這樣聲明的:

就是返回手機的型號,沒有什么好說的。
然后解釋完第一個,其他的就不用過多解釋啦:


這個clientid就是定值1:ClientID(客戶端標識號)用于標識連接到API服務的客戶端。
然后下一個就是本機IP地址,還有時間timestamp,siteid站點標識符定值10001,系統名稱system_name,型號type是Android的。
然后重點就是這個sign了,這個是個簽名校驗,這個校驗對于我們來說就是我們自己分析算出來的sign值和發往服務器的sign值一樣,就說明分析正確了,一般的校驗都是看這個發往服務器的sign值和服務器那邊計算出來的sign值是否一樣:

所以接下來就是要分析這個函數了 m8098a(params.getURLHashMap(), timestamp + ""));
他是這樣聲明的:

然后就要分析這個關鍵函數了:
public static String m8098a(HashMap<String, String> paramsMap, String time) {//傳入兩個參數,一個是map集合,一個是剛剛獲取的time LinkedHashMap<String, String> sortParams = new LinkedHashMap<>();//聲名一個新的集合 Object[] key_arr = paramsMap.keySet().toArray();//將map中的屬性取出來,存放在key_arr數組里面 Arrays.sort(key_arr);//進行排序 for (Object key : key_arr) { try { sortParams.put(key.toString(), URLEncoder.encode(paramsMap.get(key).toString(), "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }//將剛剛key_arr數組里面的值轉化成string類型,然后將map中的數據進行url編碼,然后將這兩個數組組合成一個鍵值對放在新聲名的sortParams中 StringBuilder result = new StringBuilder(); for (Map.Entry<String, String> entry : sortParams.entrySet()) {//把剛剛處理完的sortParams中的值利用迭代器取出來放在entry變量中 if (result.length() > 0) { result.append("&"); }//在每一個鍵值對后面用 & 連接 result.append(entry.getKey()); result.append("="); result.append(entry.getValue());//將鍵和值之間用 = 連接 }//這樣就像發往服務器的包的結構了 String replace = result.toString().replace("*", "%2A").replace("%7E", "~").replace("+", "%20");//將處理完的結果result在to string之后進行字符替換為replace String resultMD5 = MD5.md5(replace);//將replace進行md5加密之后為resultMD5 String str = resultMD5 + "1fa50ba25ed527f3fd1eb9467686f2bb" + time;//進行字符串拼接之后(加鹽)轉換為str String md5Result = MD5.md5(str);//將str在進行md5加密之后作為函數的返回值md5Result return md5Result;//返回的md5Result即為sign值 }
這樣的話第一個app的協議字段到此就分析完了。
第二個app:
我輸入的用戶名是kanxue,密碼是kanxue123。

data值明文傳輸:
POST http://xxxx.xx8xx88xx.com/v2_2/user/login HTTP/1.1nonce=b104cc74ade1441a9c61759fe330883c&codeSign=2086B76A137CBA8B84DD1CBCAC3F7B45×tamp=1659843481869&data=%7B%22params%22%3A%7B%22username%22%3A%22kanxue%22%2C%22password%22%3A%22kanxue123%22%7D%7D&version=2.2.1&product_version=220&platform=HD1910&network=1&device=864282012982996&access_token=62cb31ab6f2ffdaef382236aba9b98f4&screen_width=1280&screen_height=720&bbsnopic=0&system=2&system_version=19&theme=4
根據上一個app的分析,這回需要重點分析的應該是codeSign,nonce,access_token(這個介紹一下吧):
accesstoken一個訪問令牌包含了此登陸會話的安全信息。登錄一次,服務器生成一個token返回給你,你只需每次請求附帶這個token就能對網站標識自己的身份。換句話說,就是一個電子版的令牌。(這個字段可以通過解碼,變成人可以看的字段)
然后剩下的字段看著直接random就好了。
然后就該定位了,還是把看著像的地方都下上斷點,然后看看從哪里段下來就好了:(這個挺好的,就三處斷點,上一個app我下了9個,不要嫌麻煩,靜下心來就好):
然后發現程序斷在了這里:

這里我插一句,之前我說過JEB誰用誰說好...今天出bug了哈哈,這個JEB他有的時候不顯示寄存器的值:

然后用readvar v0 string還報錯,這就得上Android stdio。
定位到這個關鍵函數:

直接上代碼分析過程了:
public void mo17350a(String str, JSONObject jSONObject, AbstractC5417d<T> dVar) { String replaceAll = UUID.randomUUID().toString().replaceAll("-", "");//這里就是隨機一個UUID,然后賦值給nonce字段 long currentTimeMillis = System.currentTimeMillis();//獲取系統時間,在計算codesign字段時會用到的·1 JSONObject a = mo17349a(jSONObject);//從函數的傳入的參數中取出的JSON_object中應該就是那個data字段的值 if (MyApplication.getInstance().isLogin()) {//如果點擊登錄按鈕,就走下面這個分支,就是進行鍵值對的拼接 C5445g.m20674a(str, new C5445g.C5461f[]{new C5445g.C5461f(ReportActivity.USER_ID, "" + MyApplication.getInstance().getUserDataEntity().getUid()), new C5445g.C5461f("login_token", "" + MyApplication.getInstance().getUserDataEntity().getLogin_token()), new C5445g.C5461f("nonce", replaceAll), new C5445g.C5461f("codeSign", C6485v.m23485a(replaceAll, a, MyApplication.getInstance().getUserDataEntity().getUid() + "", currentTimeMillis)), new C5445g.C5461f("timestamp", currentTimeMillis + ""), new C5445g.C5461f("data", a.toString()), new C5445g.C5461f("version", C5414a.f16227f + ""), new C5445g.C5461f("product_version", "220"), new C5445g.C5461f(C1380c.PLATFORM, Build.PRODUCT + ""), new C5445g.C5461f(CandidatePacketExtension.NETWORK_ATTR_NAME, MyApplication.getNetworkType() + ""), new C5445g.C5461f("device", "" + MyApplication.getDeviceId()), new C5445g.C5461f("access_token", "" + C5414a.f16229h), new C5445g.C5461f("screen_width", "" + C5414a.f16230i), new C5445g.C5461f("screen_height", "" + C5414a.f16231j), new C5445g.C5461f("bbsnopic", MyApplication.isForumNoIMG() + ""), new C5445g.C5461f(C7748d.C7755c.f24519a, MessageService.MSG_DB_NOTIFY_CLICK), new C5445g.C5461f("system_version", Build.VERSION.SDK_INT + ""), new C5445g.C5461f("theme", C5414a.f16223b + "")}, (ResultCallback) dVar); return; } C5445g.m20674a(str, new C5445g.C5461f[]{new C5445g.C5461f("nonce", replaceAll), new C5445g.C5461f("codeSign", C6485v.m23484a(replaceAll, a, currentTimeMillis)), new C5445g.C5461f("timestamp", currentTimeMillis + ""), new C5445g.C5461f("data", a.toString()), new C5445g.C5461f("version", C5414a.f16227f + ""), new C5445g.C5461f("product_version", "220"), new C5445g.C5461f(C1380c.PLATFORM, Build.PRODUCT + ""), new C5445g.C5461f(CandidatePacketExtension.NETWORK_ATTR_NAME, MyApplication.getNetworkType() + ""), new C5445g.C5461f("device", "" + MyApplication.getDeviceId()), new C5445g.C5461f("access_token", "" + C5414a.f16229h), new C5445g.C5461f("screen_width", "" + C5414a.f16230i), new C5445g.C5461f("screen_height", "" + C5414a.f16231j), new C5445g.C5461f("bbsnopic", MyApplication.isForumNoIMG() + ""), new C5445g.C5461f(C7748d.C7755c.f24519a, MessageService.MSG_DB_NOTIFY_CLICK), new C5445g.C5461f("system_version", Build.VERSION.SDK_INT + ""), new C5445g.C5461f("theme", C5414a.f16223b + "")}, (ResultCallback) dVar); }
接下來這個函數new C5445g.C5461f()就是鍵值對的拼接了:

然后就要一個個看看各個字段對應的值是怎么生成的了。
nonce:先生成一個隨機數,然后進行字符的替換。

然后這幾個就是明文,沒有加密過程:

其他的幾個也是直接明文傳輸,就不一一展示了。
然后來看看這個加密的字段access_token:

然后就要分析這個東西了 C5414a.f16229h 跳過去看看:

接著跟進去分析,終于找到了定義他的地方了:
static { String str = f16227f + Build.PRODUCT + MyApplication.getNetworkType() + MyApplication.getDeviceId(); crack.log(str); f16229h = C6448r.m23455a(str); }
這個字段f16227f是獲取versionname:

后面的幾個是deviceid,還有網絡信息:

最后再看看這個函數C6448r.m23455a()干了什么:

就是個md5加密就完了。
最后重點看這個codesign字段:

第一個參數relaceall是隨機生成的數字,然后進行了字符串的替換。
第二個參數a是之前傳入的

jsonobject中的data中的數據,就是明文的用戶名和密碼。
第三個參數是當前的時間。
然后就要跟進分析這個函數了:

這個函數的三個參數都分析完了,只有這個參數我們還不知道是什么。m23483a():

public static final int forum_key = 2131230990;
然后就要看這個函數了C6420af.m23337b()。

這個是獲取資源文件的函數。
所以要想得到這個字段的值就要從資源文件中看看了:?
<string name="forum_key">94ac5cfb69e87bd7</string>
這樣所有字段的值都得到了,最后直接看看這個函數就好了。
C6448r.m23455a()還是剛剛那個md5加密的函數:

這樣這款app就分析完了。
分析完上面兩款app大家也可以發現,這些工具沒有誰最好,只是都得使用,各有個的好處,上次說那個JEB yyds就打臉了哈哈,這個jadx也挺好的,一些代碼直接翻譯成人能看懂的了。
第三個app
這款app我用的真機,模擬器出了點問題,先登錄:

然后抓包,登錄包中只有這兩個數據:

這里如果只在key上面下斷點是段不下來程序的(分析到后面就知道了,這個傳輸的字段叫key_id),所以這里我打算從post字段上下手。
POST http://passport.xxxx.com.cn/login_jsonp_active.do HTTP/1.1Content-Type: application/x-www-form-urlencoded; charset=utf-8Content-Length: 302Host: passport.xxxx.com.cnConnection: Keep-AliveAccept-Encoding: gzipCookie: JSESSIONID=aaapcYHe7KtGb7NDScYhyUser-Agent: okhttp/3.1.2 key=dTdlMmtGQjZIQk9CNmdudi95QURUbUNrZ2xKWFRNc0t0Z3g4NnpKRkZDYjRGc25RU05CL0wzSjQ2ZFYrMmxqd1ZaU2JtTVJvaURudWJhVnpFZGRsRmZGQldGQzBxbE0xVFVNVER5TDRpNkFpc1E4eVJVK0VnZWxBUUdaR0lvZ0Y5NTdFKzJKRVFtNlR0SDN5SGtCY1FOZnFIYnpNdmZqa3FPVnc2SkNGUzJ1SWUrb2xBby9wbGUrSUh1bU1wK2pTbW1XYkhzajNsYmM9P2tleUlkPTE
這里我搜索的是login_jsonp_active這個字段,然后定位到這里:

然后看那個API_LOGIN的交叉引用定位到了這里:

然后向下找找看看有沒有突破點:這個函數中有解密字段,還有用戶名和結果,看著比較像:

然后繼續看交叉引用,這里有解密的標志,所以這里看著比較像:

最終定位到了這里,發現了是key_id字段,不是key字段.....難怪下這么多斷點都找不到它:

然后還發現了這個:發現他是des加密:
sb.append(DESedeCoder.encode(json, KEYS.get("1"))).append("?keyId=").append("1");
然后就要分析這個函數是怎么加密key_id字段了:
public String encrypt(Map params) {
這個傳入的參數中的map數據,在fiddler中得數據包中可以發現。

然后通過搜索sign字段也可以定位到在app中的位置:
public static Map<String, String> m264a(Map<String, String> map) { map.put("t", String.valueOf(System.currentTimeMillis())); StringBuilder sb = new StringBuilder(128); sb.append(m265b(map.get("appkey"))).append("&").append(m265b(map.get("domain"))).append("&").append(m265b(map.get("appName"))).append("&").append(m265b(map.get(SdkConstants.APP_VERSION))).append("&").append(m265b(map.get("bssid"))).append("&").append(m265b(map.get("channel"))).append("&").append(m265b(map.get(LeService.KEY_DEVICE_ID))).append("&").append(m265b(map.get("lat"))).append("&").append(m265b(map.get("lng"))).append("&").append(m265b(map.get("machine"))).append("&").append(m265b(map.get("netType"))).append("&").append(m265b(map.get("lng"))).append("&").append(m265b(map.get("platform"))).append("&").append(m265b(map.get("platformVersion"))).append("&").append(m265b(map.get("preIp"))).append("&").append(m265b(map.get("sid"))).append("&").append(m265b(map.get("t"))).append("&").append(m265b(map.get("v"))); map.put("sign", m263a(sb.toString())); return map; }
分析完參數是啥,就要看看函數的具體操作了:

先聲名一個stringbuffer變量,在將map中的數據轉化為string類型。
然后就是對key字段進行的加密了:那個 KEYS.get("1")在這個可以找到了值:應該是密鑰。

然后就要分析這個函數了:DESedeCoder.encode(json, KEYS.get("1"))
第一個參數是map中的數據,第二個參數是密鑰。

先將傳入的字符串轉化為byte類型,然后傳入encrypt函數:繼續跟進分析:

這款app的加密字段就分析完了。總結一下:這個協議分析和找flag基本上是一樣的,就是定位到關鍵字段和函數,然后分析加密過程,這次就先分享三個java層的吧,下次再分析so的。