爲了練練手,增長逆向分析的知識,本次博客打算分析一下某酒店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;
}
從這個a函數中可以知道,這個函數就是拼接JsonObject的地方,還可以看到抓包時的關鍵詞“APPSIGN”,“data”等。既然我們需要解密的是data的數據,那麼我們再看看data數據在這裏是怎樣被加密的。
從紅色方框中可以看到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<data.length; i++){
k=0xFF & k +1;
j=0xFF & j + (0xFF & key[k]);
int m=key[k];
key[k]=key[j];
key[j]=(byte)m;
int n=0xFF & ((0xFF & key[k])+(0xFF & key[j]));
encodeBytes[i]=((byte)(data[i]^key[n]));
}
return encodeBytes;
}
到此爲止,我們已經將明文到密文的加密原理給分析清楚了,現在我們就驗證一下是否正確。
第四步,也是最後一步,我們可以正向驗證,也可以逆向驗證,這裏的話我們採取逆向驗證,即從抓包的數據中還原出明文。
打開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共用一套加解密算法。