这次分析的原因是因为群里有人想让我帮他做个签到脚本,我晚上无聊看了一下,结果发现这个软件对响应体进行了加密,于是便有了这篇文章
1.准备工作
APP样本
Xposed
反射大师
MT管理器
模拟器
我本人使用的是IOS,所以用模拟器来分析安卓版本的加密过程。
2. 脱壳
安装软件之后,使用MT管理器
查看软件的加固方式,可以看到,下面的图片显示:腾讯御安全
。
使用反射大师
进行脱壳(具体过程不做阐述,站内有教程,只是写出dex记得长按,就能写出所有dex)。
3.抓包
脱壳之后,先不要急着看源码,首先进行抓包
。
我用的是IOS上的Thor
。
我用登陆的接口作为演示,首先是登陆的请求头
信息,如下图所示(个人觉得没什么有用的信息)
接着看登陆接口的返回
信息,看起来像是AES/DES的某种加密,这样就有思路了,搜索的范围也可以稍微小一点。
4.代码分析
我们使用MT管理器
搜索AES、DES、descrypt等关键字,因为这几个关键字有可能是某个类/方法/字符串,能够极大提高我们找到关键信息的成功率。
我首先使用AES
搜索,这里需要注意,搜索结果会很多,我们一般去看和APP包名
一样的packages
名的就可以了,如下图所示。
接着我们点击进去,转换成java(需要更换引擎,几个都试试),这个类中的代码如下:
package com.xbiao.utils;
import android.annotation.SuppressLint;
import android.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class AESedeUtil {
private static final String INIT_VECTOR = "6di50aH901duea7d";
@SuppressLint({"NewApi"})//解密方法
public static String decrypt(String var0, String var1) {
try {
IvParameterSpec var3 = new IvParameterSpec("6di50aH901duea7d".getBytes("UTF-8"));
SecretKeySpec var2 = new SecretKeySpec(var0.getBytes("UTF-8"), "AES");
Cipher var5 = Cipher.getInstance("AES/CBC/NOPADDING");
var5.init(2, var2, var3);
var0 = new String(var5.doFinal(Base64.decode(var1, 0)));
return var0;
} catch (Exception var4) {
var4.printStackTrace();
return null;
}
}
@SuppressLint({"NewApi"})//加密方法
public static String encrypt(String var0, String var1) {
try {
IvParameterSpec var2 = new IvParameterSpec("6di50aH901duea7d".getBytes("UTF-8"));
SecretKeySpec var3 = new SecretKeySpec(var0.getBytes("UTF-8"), "AES");
Cipher var5 = Cipher.getInstance("AES/CBC/NOPADDING");
var5.init(1, var3, var2);
byte[] var6 = var5.doFinal(var1.getBytes());
var0 = java.util.Base64.getEncoder().encodeToString(var6);
return var0;
} catch (Exception var4) {
var4.printStackTrace();
return null;
}
}
}
我没学过java,只学过类似的c#,上面的encrypt
和decrypt
的算法,还不知道是不是用于解密响应消息
的,从代码中我们能得到的信息是:使用了AES的CBC模式,还有一个NOPADDING参数
,同时我们从开头的定义还知道了,那个变量的值应该是偏移iv
的值,所以也需要留意一下,既然已经知道了方法名,我们就回去搜索一下decrypt
,看看哪里调用了这个方法,解密的是什么内容。
我们在一个叫做NetContent
的类中找到了如下图所示的字段
其中有个方法名引起了我的注意:decryptResponse
,翻译过来就是解密响应信息,我觉得八九不离十,就是这里了,打开进行反编译,代码段内容如下:
private static String decryptResponse(String var0, String var1) {
String var2 = LocalCookieManager.getInstance().getSecretKey();
if (!TextUtils.isEmpty(var0) && !TextUtils.isEmpty(var2)) {
var0 = AESedeUtil.decrypt(var2, var0);//调用了decrypt的方法
} else {
var0 = "";
}
label20: {
try {
var2 = changeCharset(var0, "utf-8");
} catch (UnsupportedEncodingException var3) {
var3.printStackTrace();
break label20;
}
var0 = var2;
}
var2 = var0.trim();
var0 = var2;
if (TextUtils.isEmpty(var2)) {
var0 = "";
}
StringBuilder var4 = new StringBuilder();
var4.append("url:");
var4.append(var1.replace(NewsAccessor.BASE_HEAD_URL, ""));
var4.append(" 最终结果response***************:");
var4.append(var0);
Log.i("liuguangyou", var4.toString());
return var0;
}
从上面的代码中,可以看到是调用了我们之前找的那个类里面的方法了的。
所以我们要分析一下参数var2
和var0
是怎么来的
我们看到var2
是调用了一个类中的方法来生成的:
String var2 = LocalCookieManager.getInstance().getSecretKey();
由于本身这个decryptResponse
也是要被调用的,所以着重分析var2
的生成方式
我们看到这里的类叫做LocalCookieManager
所以可能就是请求头里面携带的Cookie内容,这样我们的范围就能缩小了一点,再看后面的getSecretKey
方法,翻译过来就是密钥,所以var2
就是这个算法的解密密钥,我们去MT
搜索getSecretKey
。
搜索到了LocalCookieManager
类中,反编译代码的时候,发现如下一串代码:
private static LocalCookieManager infoManager;
private String searchCookiePublicKey;
private String secretKey;
private LocalCookieManager() {
this.searchCookiePublicKey = "publicKey=";
}
因为这个类是Cookie的类,于是回去寻找抓包信息中的Cookie中,是否有这个KEY
果然,在之前的抓包结果中,是有如上的KEY的,一切都证明我们的方向是正确的
在这个类中,一直看到最后,看到了一个方法名subCookieSecretKey
,字面意思是分割Cookie密钥,于是猜测,可能是从publicKey
的值中生成密钥,因为变量的名字也很明确,searchCookiePublicKey
.搜索的就是这个KEY,于是反编译代码:
public void subCookieSecretKey(String s) {
if (!TextUtils.isEmpty((CharSequence)s)) {
s = URLDecoder.decode(s.replaceAll(":eq:", "; "));
final int index = s.indexOf(this.searchCookiePublicKey);
if (index != -1) {
int n;
if ((n = s.indexOf(";", index)) == -1) {
n = s.length();
}
final String replace = s.substring(index, n).trim().replace(this.searchCookiePublicKey, "");
s = replace.substring(0, 5);
final String substring = replace.substring(replace.length() - 11, replace.length());
final StringBuilder sb = new StringBuilder();
sb.append(s);
sb.append(substring);
this.secretKey = sb.toString();
if (TextUtils.isEmpty((CharSequence)this.secretKey)) {
this.secretKey = SharedPreferencesPublicKeyUtil.getStringValueFromSP("secretKey");
}
SharedPreferencesPublicKeyUtil.setStringDataIntoSP("secretKey", this.secretKey);
}
}
前面有一端是判断值是否存在的,所以只要看一部分就好
final String replace = s.substring(index, n).trim().replace(this.searchCookiePublicKey, "");
上面这段可以看出,是把有publicKey=
的这一段给替换去除,所以也就是这个KEY的值了,接下来都按照这个思路去走。
s = replace.substring(0, 5);
final String substring = replace.substring(replace.length() - 11, replace.length());
final StringBuilder sb = new StringBuilder();
sb.append(s);
sb.append(substring);
this.secretKey = sb.toString();
上面的代码中,可以看到,用publicKey
的值,取其索引第一位
到第四位
。
为了方便讲解,我把我的KEY值贴上来。
我的KEY值是:5BJ3e+kjDdAIy9gEvnJasIRXqZFnjfTG3Om4ER/Tcz4=
所以此时s=5BJ3e,接着再取KEY值长度
-11的位置到KEY值
末尾的索引
这个时候substring=Om4ER/Tcz4=,后面把这两个值进行拼接,赋值给变量secretKey
.
所以最后secretKey
的值就是5BJ3eOm4ER/Tcz4=
我们逆推,看看secretKey
是在哪里,我们在源码中找到如下代码:
public String getSecretKey() {
return this.secretKey;
}
所以这就是在NetContent
类中被调用的方法。
回到AES的解密中,看到这段代码:
IvParameterSpec var3 = new IvParameterSpec("6di50aH901duea7d".getBytes("UTF-8"));
SecretKeySpec var2 = new SecretKeySpec(var0.getBytes("UTF-8"), "AES");
Cipher var5 = Cipher.getInstance("AES/CBC/NOPADDING");
var5.init(2, var2, var3);
推测判断,var3
为iv偏移量
,var2
为解密的密钥,参数var0
为密文数据,通过var5
解密返回原始数据。
为了验证这个猜想,我找到了一个AES解密的网站,分别填入密文
、解密KEY
、iv偏移
,得到结果如下:
处理了一下,最终内容如下:
5.结语
后面的内容我没有继续深入下去,因为这个APP其实定时发送GET进行签到正常没事的,只是加密的内容你不知道是什么而已,我也很感谢那个提供APP的人,让我又学习到了新的东西。
在这过程中,踩了不少坑,比如手机上的iv偏移值不支持输入字符串,不支持选择填充,所以解密数据是不完整的,好在找到了一个不错的AES解密网站。
后期的脚本没有写,本来打算写JS版本的,想想还是算了,最后奉上在这次过程中,用于调试的python代码:
Cookie="5BJ3e+kjDdAIy9gEvnJasIRXqZFnjfTG3Om4ER/Tcz4="
Front_Str = Cookie[0:5]
After_Str = Cookie[len(Cookie)-11:len(Cookie)]
Full_SecKey = Front_Str+After_Str
print(Full_SecKey)
Iv_Str = "6di50aH901duea7d"
'''
Iv_Bytes_Str = ""
for Byte in Iv_Str:
Iv_Bytes_Str+=str(ord(Byte))
SecKey_Byte_Str = ""
for Key in Full_SecKey:
SecKey_Byte_Str+=str(ord(Key))
print(SecKey_Byte_Str)
'''
到这里就分析结束啦!也希望能帮到一些和我一样的新手们