編輯:關於Android編程
為了練練手,增長逆向分析的知識,本次博客打算分析一下某酒店APP的登陸請求。
這次的逆向分析還是以網絡請求為例,通過分析其登陸請求數據的加解密原理,將請求數據從密文轉換成明文,順便把返回的結果也轉成明文。
好了,既然明確了需求,那麼准備開始分析了,分析的步驟還是很簡單:反編譯-->找關鍵代碼-->分析請求代碼加解密原理-->驗證密文到明文准確性。
第一步,反編譯:
這一步中,一般來說我們可以結合smali文件和jd-gui查看器來分析,所以APKTool和dex2jar都可以用上。
因為APKTool和dex2jar的反編譯都有在前面的博客裡介紹過,所以在此就不詳細說明了,不明的可以回去看。
第二步,找關鍵代碼:
找關鍵代碼的方法有很多,我主要用到兩種方法,一是,利用wireshark抓包的關鍵字符串,結合sublime text工具在smali文件夾中使用“find in folder”功能找到關鍵字符串所在位置,然後再順籐摸瓜;二是直接通過jd-gui查看其代碼,並找到登陸界面的點擊事件,並定位到其關鍵代碼。
首先講第一種方法:
先抓包(打開wireshark監控,然後按下登陸按鈕進行登陸),並查看其TCP流,找出其關鍵字符串
可以看到圖中紅色方框中的關鍵詞"APPSIGN","sign","data"等,其中data的數據是我們想要知道的,但現在是密文。
接下來,我們利用找到的關鍵詞,利用sublime text工具的find in folder功能在smali文件夾中查找該關鍵詞出現的地方。
通過關鍵詞可以找到了一個HttpUtils的類,再看看其smali裡的表達式,可以看到com/loopj/android/http包,該包實際上就是第三方框架AsyncHttpClient的東西。
到這,我們基本上就可以定位到關鍵代碼位置了。
那麼,第二種方法又是怎樣的呢?
實際上第二種方法並不建議,這種方法就是直接找,因為fragment,Activity這些類是無法被混淆的,所以我們可以直接在jd-gui上浏覽一下目錄,找到其關鍵的地方,比如我們現在找登陸界面,那麼可以根據登陸的英文login去找,如下圖
可以看到在com.htinns.UI.fragment.My包下可以找到了LoginFragment,沒錯,這就是登陸頁面,在文件數量很大的情況不建議用這種方法,費時。
在LoginFragment的代碼中可以看到有多個HttpUtils類的靜態方法被調用,基本上已經確定HttpUtils確實是登陸請求的一個工具類了。
第三步,分析加解密原理,實際上這跟第二步是緊密聯系的,因為要一邊順籐摸瓜,一邊推斷其加解密的原理,一步步地接近真相~
根據上面找到的關鍵代碼,我們可以在HttpUtils中找到一個靜態方法a(Context, RequestInfo),該方法中的參數RequestInfo實例是包含了賬號密碼的,就是該實例的字段c,所以我們可以根據RequestInfo.c這個內容去順籐摸瓜,找到其加密的位置。
可以看到,圖中紅色方框中的分別是加密的key和調用加密函數的入口a函數,那麼我們繼續順著這個a函數找:
public static k a(Context paramContext, JSONObject paramJSONObject, String paramString) { k localk = new k(); if (paramJSONObject == null); try { paramJSONObject = new JSONObject(); if (TextUtils.isEmpty(b)) b = av.e(paramContext); paramJSONObject.put("devNo", b); paramJSONObject.put("brand", Build.BRAND); paramJSONObject.put("manufacturer", Build.MANUFACTURER); paramJSONObject.put("model", Build.MODEL); if (TextUtils.isEmpty(g)) g = av.f(paramContext); paramJSONObject.put("MAC", g); paramJSONObject.put("os", Build.VERSION.RELEASE); paramJSONObject.put("CHANNEL_ID", h.a("push_channelid", "")); paramJSONObject.put("PUSH_TOKEN", h.a("push_userid", "")); paramJSONObject.put("Jpush_CHANNEL_ID", h.a("jpush_channelid", "")); paramJSONObject.put("Jpush_PUSH_TOKEN", h.a("jpush_userid", "")); if (c == null) c = av.c(paramContext); paramJSONObject.put("access_mode", c); if (i == null) i = av.d(paramContext); paramJSONObject.put("ver", i); if (d == null) d = av.b(paramContext); paramJSONObject.put("channel", d); paramJSONObject.put("platform", ag.a()); paramJSONObject.put("LATITUDE", e); paramJSONObject.put("LONGITUDE", f); GuestInfo localGuestInfo = GuestInfo.GetInstance(); if (localGuestInfo != null) paramJSONObject.put("Token", localGuestInfo.TOKEN); Calendar localCalendar = Calendar.getInstance(); String str1 = new SimpleDateFormat("yyyyMMddHHmmss").format(localCalendar.getTime()); paramJSONObject.put("resultKey", paramString); String str2 = paramJSONObject.toString(); localk.a("data", al.b(str2, "@!#$#%$%&^%&DFGFHF%&%&^%&%")); localk.a("time", str1); String str3 = Base64.encodeToString(av.c(str2 + str1 + paramString), 2); localk.a("sign", str3); localk.a("APPSIGN", a(str3)); return localk; } catch (Exception localException) { localException.printStackTrace(); MobclickAgent.onEvent(MyApplication.a(), "BUILD_PARAM_EXCEPTION", localException.getMessage()); } return localk; }
從紅色方框中可以看到localk.a(String, String)就是一個拼接函數,裡面的 al.b(str2, "@!#$#%$%&^%&DFGFHF%&%&^%&%")就是一個加密方法。至於str2是不是data的明文信息,我們可以利用hook技術驗證一下。
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (loadPackageParam.packageName.equals("com.htinns")) { XposedBridge.log("Load Pakage:"+loadPackageParam.packageName); XposedHelpers.findAndHookMethod("com.htinns.Common.al", loadPackageParam.classLoader, "b", String.class, String.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { //查看加密前的String Log.d("zz", (String)param.args[0]); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { } }); } }上面是一段Xposed模塊中的hook函數,該函數暴露其明文,Xposed使用在前面有介紹過,所以在這就不說了,看看hook後的日志。
很好,就是明文,裡面還有密碼呢,那麼接下來看一下加密的地方,我把al類貼出來,該類就是加密類,因為混淆過,它很多函數都是調用a, 或b,順籐摸瓜的時候很煩~
public class al { private static byte[] a(String paramString) { int i = 0; byte[] arrayOfByte1 = paramString.getBytes(); byte[] arrayOfByte2 = new byte[256]; for (int j = 0; j < 256; j++) arrayOfByte2[j] = ((byte)j); if ((arrayOfByte1 == null) || (arrayOfByte1.length == 0)) arrayOfByte2 = null; while (true) { return arrayOfByte2; int k = 0; int m = 0; while (i < 256) { k = 0xFF & k + ((0xFF & arrayOfByte1[m]) + (0xFF & arrayOfByte2[i])); int n = arrayOfByte2[i]; arrayOfByte2[i] = arrayOfByte2[k]; arrayOfByte2[k] = n; m = (m + 1) % arrayOfByte1.length; i++; } } } public static byte[] a(String paramString1, String paramString2) { if ((paramString1 == null) || (paramString2 == null)) return null; return b(paramString1.getBytes(), paramString2); } public static byte[] a(byte[] paramArrayOfByte, String paramString) { if ((paramArrayOfByte == null) || (paramString == null)) return null; return b(paramArrayOfByte, paramString); } public static String b(String paramString1, String paramString2) { if ((paramString1 == null) || (paramString2 == null)) return null; return e.b(a(paramString1, paramString2), false); } private static byte[] b(byte[] paramArrayOfByte, String paramString) { int i = 0; byte[] arrayOfByte1 = a(paramString); byte[] arrayOfByte2 = new byte[paramArrayOfByte.length]; int j = 0; int k = 0; while (i < paramArrayOfByte.length) { k = 0xFF & k + 1; j = 0xFF & j + (0xFF & arrayOfByte1[k]); int m = arrayOfByte1[k]; arrayOfByte1[k] = arrayOfByte1[j]; arrayOfByte1[j] = m; int n = 0xFF & (0xFF & arrayOfByte1[k]) + (0xFF & arrayOfByte1[j]); arrayOfByte2[i] = ((byte)(paramArrayOfByte[i] ^ arrayOfByte1[n])); i++; } return arrayOfByte2; } }
由上圖可以看到,加密的過程關鍵有3處:
一是,對key字符串“@!#$#%$%&^%&DFGFHF%&%&^%&%”進行特殊的String轉byte[]處理,這裡我根據smali文件將其算法還原出來,並用java實現:
public static byte[] getKeyBytes(String key){ //73 byte[] keyBytes=key.getBytes(); //74 byte[] resultBytes=new byte[256]; //76 for(int i=0; i<256; i++){ //77 resultBytes[i]=(byte)i; } //81 if(keyBytes==null || keyBytes.length==0){ //91 return null; }else{ int j=0, k=0; //84 for(int i=0; i<256; i++){ //85 int m=(keyBytes[k]&0xff)+(resultBytes[i]&0xff); j=j+m & 0xff; //86 int x=resultBytes[i]; //87, 88 resultBytes[i]=resultBytes[j]; resultBytes[j]=(byte) x; k=k+1; k=k%keyBytes.length; } return resultBytes; }代碼中的注釋是smali上面顯示的函數,不用鳥它。
二是,對明文的byte[]和特殊處理key後的byte[]進行異或:
public static byte[] xorEncode(byte[] data, byte[] key){
byte[] encodeBytes=new byte[data.length];
int j=0, k=0;
for(int i=0; i
三是,對異或後的byte[]進行Base64編碼,java中已自帶該編碼,所以我就不貼出來了。到此為止,我們已經將明文到密文的加密原理給分析清楚了,現在我們就驗證一下是否正確。第四步,也是最後一步,我們可以正向驗證,也可以逆向驗證,這裡的話我們采取逆向驗證,即從抓包的數據中還原出明文。打開wireshark,將過濾器設置為frame contain "APPSIGN",然後在手機上登陸一下這APP,該過濾條件能很容易地將包區別出來:
然後隨便選擇一個打開,這裡我們可以打開第一個包的TCP流,並將data後的數據(藍色部分)復制出來作為String驗證:
這裡藍色選定的數據實際上已被wireshark通過URLEncode過,所以在逆向驗證時,需要將數據URLDecode一下才進行下一步。 public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub String str="vr%2BCsMVfUcBzHDadnFFNAjdhONP0Aivd9j3BdYn35E3EkHG2bOmXSnhH%2FlqJmnDw%2FpVJpr%2B9sSHI8WoV5deh51b9tiltTcFjQWWeSPhIQg1tgLD51hfaX9IkwIiYRAocGrBzsCuKotTJA25RWcFwyyR48pV%2BiJt2abLRLIEsaA%2FaM541c4FREfRUC2qDqw8SJnGVPIKiAS6iHGaHW%2BoCfFGhUzyNcsMarcxEmK9RBbSTscsGcCivhJ%2F1RBAjW%2BndZQHLFeIm0jJWQdlpCpKZHgTiUCq%2BzL1UGM8Iqv41xHRq%2Bf0yimbAzPW%2B%2Ft6LrRKYcA9VpmARgQTvjUmgZsQ8j%2F65EAvzYuCeeO80T017rJHmOeV17DX0IKRgWdhg3LomDnyMqsv%2BQA8I755162jgEcxplO1lgaTvRpNMORlwgByUUXh%2BbJqmvyjDq1%2FXYQBgD%2F9J1WyWW3OTnxdwmuYsrOBIBBNgMYYCELGqW2IknlSwkzfZEBUjWBpYHgrc9DIOzv8cVaMCQxCb1VV7E8MToa0fmI7FcvsJIM2J"; String key="@!#$#%$%&^%&DFGFHF%&%&^%&%"; String data=new String(xorEncode(Base64.getDecoder().decode(URLDecoder.decode(str, "utf8")), getKeyBytes(key))); System.out.println(data); }
在static main中,分幾步來逆向,其中異或的異或等於本身,所以只需要再執行一次異或就能過掉哪個異或加密算法:
最後在後台輸出字符串:
很顯然,這就是明文,也就是說驗證成功!至於Response的數據解密在這裡就不說了,其原理也差不多,只是比Request的數據加密逆向驗證多了一層gzip格式的解壓編碼,其他Base64和異或都和Request共用一套加解密算法。
前言 在微信剛流行的時候,在搖一搖還能用來那啥的時候,我也曾深更半夜的拿著手機晃一晃。當時想的最多的就是,我靠,為神馬搖一下需要用這麼大的力度,當時我想可能騰訊覺
搜索框上下滑動變透明度是現在APP中很常見的效果,先看看效果:首先來看下布局骨架:<RelativeLayout xmlns:android=http://sche
最近到4412最基本的都調了 然後覺得沒事做了 所以增加一個HDMI的設置 不能閒著 以下使用的是廣州斯道的icool210開發板 源碼修改如下 最開始我是先在設置裡加上
今天給大家詳解一下Android中Activity的生命周期,我希望我的講解不像網上大多數文章一樣,基本都是翻譯Android API,過於籠統,相信大家看了,會有一點點