您的位置:首页 > 移动开发

Android逆向分析案例——某酒店APP的登陆请求分析

2016-07-20 12:44 429 查看
为了练练手,增长逆向分析的知识,本次博客打算分析一下某酒店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
));
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
));
}
return encodeBytes;
}

三是,对异或后的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共用一套加解密算法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息