声明:案例分析仅供学习交流使用,勿用于任何非法用途。如学习者进一步逆向并对版权方造成损失,请自行承担法律后果,本人概不负责。
简介
很多软件在保护文件安全、用户隐私时,都会对本地文件数据进行加密处理。本次逆向的apk就对关键数据进行了加密,并对加解密代码提供了一定程度的保护。
目标
写一个小工具解密数据。
逆向流程
找到解密函数
首先从data目录找到文件:data2.8.0。内容是经过加密的。
通过jadx搜索“data2.8.0”没有任何线索,进一步扩大范围搜索“data”,本来有些担心这个词语过于普遍导致结果太多,但没想到还真找到一个很可疑的“data+版本号”格式的字符。
追踪进com.vv.test.ChannelDatas,恰好可以看到第一个函数貌似就是在读取加密文件。
经过分析,该方法功能确实是读取文件后解密。
public ChannelDatas(Context context) {
this.mySettings = new MySettings(context);
//这里是在生成之后AES解密用的key
//pmd5(string)是取md5
//SplashActivity.sKey是动态生成的密匙
//getSettingStr("rand")是从SharedPreferences取"rand"
String substring = AES.pmd5(SplashActivity.sKey + new MySettings(PlayerActivity.context).getSettingStr("rand")).substring(7, 7 + 16);
try {
//读取文件
this.jsonString = ChannelManager.readfile(context, FILE_DATA);
//文件内容做变换
this.jsonString = this.jsonString.substring(64);
this.jsonString = this.jsonString.substring(64);
this.jsonString = this.jsonString.replace("f", "#");
this.jsonString = this.jsonString.replace("b", "f");
this.jsonString = this.jsonString.replace("#", "b");
this.jsonString = this.jsonString.replace("t", "%");
this.jsonString = this.jsonString.replace("y", "t");
this.jsonString = this.jsonString.replace("%", "y");
//变化后使用AES解密
this.jsonString = AES.Decrypt(this.jsonString, substring);
//解密后Base64解密一道再解压一次得到
this.jsonString = new String(gzuncompress(Base64.decode(this.jsonString, 0)), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
new MySettings(PlayerActivity.context).saveVersion(0);
Toast.makeText(PlayerActivity.context, "解析节目数据失败!请重新启动软件!", 1).show();
}
}
/**
* AES解密工具
* str为要解密的内容,str2为密匙
*/
public static String Decrypt(String str, String str2) throws Exception {
if (str2 == null) {
try {
System.out.print("Key为空null");
return null;
} catch (Exception e) {
System.out.println(e.toString());
return null;
}
} else if (str2.length() != 16) {
System.out.print("Key长度不是16位");
return null;
} else {
SecretKeySpec secretKeySpec = new SecretKeySpec(str2.getBytes("utf-8"), "AES");
Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
instance.init(2, secretKeySpec);
try {
return new String(instance.doFinal(Base64.decode(str, 0)), "utf-8");
} catch (Exception e2) {
System.out.println(e2.toString());
return null;
}
}
}
/**
* 解压方法
*/
public static byte[] gzuncompress(byte[] bArr) {
byte[] bArr2 = null;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(bArr.length);
Inflater inflater = new Inflater();
try {
inflater.setInput(bArr);
byte[] bArr3 = new byte[256];
while (!inflater.finished()) {
byteArrayOutputStream.write(bArr3, 0, inflater.inflate(bArr3));
}
bArr2 = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable th) {
inflater.end();
throw th;
}
inflater.end();
return bArr2;
}
分析关键参数
当然,到这里还没结束,那就是SplashActivity.sKey和getSettingStr(“rand”)还没拿到,"rand"好办,直接从SharedPreferences.xml文件里可以读到,SplashActivity.sKey还挺费工夫的。由于混淆的厉害,就拿Dalvik看吧。
摘出真正在生成sKey的部分:
L_0x0106:
...
java.lang.StringBuilder r9 = new java.lang.StringBuilder
r9.<init>()
int r10 = sig
android.support.v4.media.TransportController.m88(r9, r10) //r9.append(r10)
java.lang.String r10 = appname
okhttp3.Route.m286(r9, r10) //r9.append(r10)
java.lang.String r10 = packagename
okhttp3.Route.m286(r9, r10)
com.setting.MySettings r10 = r1.mySettings
java.lang.String r10 = com.vv.test.ChannelData.m226(r10) //r10.getSign();
okhttp3.Route.m286(r9, r10)
java.lang.String r9 = okhttp3.internal.connection.RouteDatabase.m303(r9) //r9.toString();
java.lang.String r9 = android.support.v4.app.NotificationCompat.CarExtender.UnreadConversation.Builder.m44(r9) //md5(r9)
sKey = r9
java.lang.StringBuilder r9 = new java.lang.StringBuilder
r9.<init>()
java.lang.String r10 = sKey
android.support.v4.media.MediaMetadataCompatApi21.Builder.m82(r9, r10) //r9.append(r10)
java.lang.String r10 = appname
okhttp3.Route.m286(r9, r10)
java.lang.String r10 = packagename
android.support.v4.media.MediaMetadataCompatApi21.Builder.m82(r9, r10)
java.lang.String r9 = android.support.v4.media.session.PlaybackStateCompat.Builder.m106(r9) //r9.toString();
java.lang.String r9 = android.support.v4.app.NotificationCompat.CarExtender.UnreadConversation.Builder.m44(r9)
sKey = r9
...
其中关键的是sig和getSign()。sig生成也比较麻烦:
getSign()是native方法,使用ida静态分析可以看到逻辑如下:
开始Hook
上面两个函数我就不一点点分析了,有兴趣的盆友可以进一步挖掘,我在这里就直接开挂(Xposed Hook)给出答案了:
public class XposedInit implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
XposedBridge.log("hook init ---"+lpparam.packageName);
if (lpparam.packageName.equals("com.vv.com")) {
XposedBridge.log("hook start com.vv.com");
//拿到com.vv.test.SplashActivity类
final Class<?> splashActivityClass = XposedHelpers.findClass("com.vv.test.SplashActivity", lpparam.classLoader);
//Hook com.vv.test.SplashActivity的onCreate()方法
XposedHelpers.findAndHookMethod("com.vv.test.SplashActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
//onCreate()方法执行完后
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
//打印SplashActivity.skey
String skey = String.valueOf(XposedHelpers.getStaticObjectField(splashActivityClass,"sKey"));
XposedBridge.log("skey:" + skey); //d3ba4001c159be6c4087fd3679a4a74d
}
});
return;
}
}
}
获得sKey之后就能写出文件解密工具了:
public static String runDecrypt() {
String rand = "13ae41c997b396765bd57e3f8786dbf7";
String sKey = "d3ba4001c159be6c4087fd3679a4a74d";
String substring = MyTool.md5(sKey + rand).substring(7, 7 + 16);
String resData = FileUtils.loadFile("./files/data2.8.0", "utf-8");
resData = resData.substring(64);
resData = resData.substring(64);
resData = resData.replace("f", "#");
resData = resData.replace("b", "f");
resData = resData.replace("#", "b");
resData = resData.replace("t", "%");
resData = resData.replace("y", "t");
resData = resData.replace("%", "y");
try {
resData = Decrypt(resData, substring);
resData = new String(gzuncompress(Base64.decode(resData, 0)), "UTF-8");
if (StringUtils.isNotBlank(resData)) {
FileUtils.forBufferedWriter(resData, "./files/data.txt", false);
}
return resData;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
执行后即可获得解密后文本: