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

为了练练手,增长逆向分析的知识,本次博客打算分析一下某酒店APP的登陆请求。

这次的逆向分析还是以网络请求为例,通过分析其登陆请求数据的加解密原理,将请求数据从密文转换成明文,顺便把返回的结果也转成明文。

好了,既然明确了需求,那么准备开始分析了,分析的步骤还是很简单:反编译-->找关键代码-->分析请求代码加解密原理-->验证密文到明文准确性


第一步,反编译:

这一步中,一般来说我们可以结合smali文件和jd-gui查看器来分析,所以APKTool和dex2jar都可以用上。

因为APKTool和dex2jar的反编译都有在前面的博客里介绍过,所以在此就不详细说明了,不明的可以回去看。


第二步,找关键代码:

找关键代码的方法有很多,我主要用到两种方法,一是,利用wireshark抓包的关键字符串,结合sublime text工具在smali文件夹中使用“find in folder”功能找到关键字符串所在位置,然后再顺藤摸瓜;二是直接通过jd-gui查看其代码,并找到登陆界面的点击事件,并定位到其关键代码。

首先讲第一种方法:

先抓包(打开wireshark监控,然后按下登陆按钮进行登陆),并查看其TCP流,找出其关键字符串

Android逆向分析案例——某酒店APP的登陆请求分析_第1张图片

可以看到图中红色方框中的关键词"APPSIGN","sign","data"等,其中data的数据是我们想要知道的,但现在是密文。

接下来,我们利用找到的关键词,利用sublime text工具的find in folder功能在smali文件夹中查找该关键词出现的地方。

Android逆向分析案例——某酒店APP的登陆请求分析_第2张图片

通过关键词可以找到了一个HttpUtils的类,再看看其smali里的表达式,可以看到com/loopj/android/http包,该包实际上就是第三方框架AsyncHttpClient的东西。

到这,我们基本上就可以定位到关键代码位置了。

那么,第二种方法又是怎样的呢?

实际上第二种方法并不建议,这种方法就是直接找,因为fragment,Activity这些类是无法被混淆的,所以我们可以直接在jd-gui上浏览一下目录,找到其关键的地方,比如我们现在找登陆界面,那么可以根据登陆的英文login去找,如下图Android逆向分析案例——某酒店APP的登陆请求分析_第3张图片


可以看到在com.htinns.UI.fragment.My包下可以找到了LoginFragment,没错,这就是登陆页面,在文件数量很大的情况不建议用这种方法,费时。

在LoginFragment的代码中可以看到有多个HttpUtils类的静态方法被调用,基本上已经确定HttpUtils确实是登陆请求的一个工具类了。


第三步,分析加解密原理,实际上这跟第二步是紧密联系的,因为要一边顺藤摸瓜,一边推断其加解密的原理,一步步地接近真相~

根据上面找到的关键代码,我们可以在HttpUtils中找到一个静态方法a(Context, RequestInfo),该方法中的参数RequestInfo实例是包含了账号密码的,就是该实例的字段c,所以我们可以根据RequestInfo.c这个内容去顺藤摸瓜,找到其加密的位置。

Android逆向分析案例——某酒店APP的登陆请求分析_第4张图片

可以看到,图中红色方框中的分别是加密的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数据在这里是怎样被加密的。

Android逆向分析案例——某酒店APP的登陆请求分析_第5张图片

从红色方框中可以看到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后的日志。
Android逆向分析案例——某酒店APP的登陆请求分析_第6张图片

很好,就是明文,里面还有密码呢,那么接下来看一下加密的地方,我把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;
  }
}

经过跟踪,最后来看看梳理后的加密流程图:

Android逆向分析案例——某酒店APP的登陆请求分析_第7张图片

由上图可以看到,加密的过程关键有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,该过滤条件能很容易地将包区别出来:

Android逆向分析案例——某酒店APP的登陆请求分析_第8张图片

然后随便选择一个打开,这里我们可以打开第一个包的TCP流,并将data后的数据(蓝色部分)复制出来作为String验证:

Android逆向分析案例——某酒店APP的登陆请求分析_第9张图片

这里蓝色选定的数据实际上已被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中,分几步来逆向,其中 异或的异或等于本身,所以只需要再执行一次异或就能过掉哪个异或加密算法:

Android逆向分析案例——某酒店APP的登陆请求分析_第10张图片

最后在后台输出字符串:


很显然,这就是明文,也就是说验证成功!


至于Response的数据解密在这里就不说了,其原理也差不多,只是比Request的数据加密逆向验证多了一层gzip格式的解压编码,其他Base64和异或都和Request共用一套加解密算法。

你可能感兴趣的:(Android逆向分析案例)