Android逆向:通过Xposed解密柠某直播本地数据

声明:案例分析仅供学习交流使用,勿用于任何非法用途。如学习者进一步逆向并对版权方造成损失,请自行承担法律后果,本人概不负责。

简介

很多软件在保护文件安全、用户隐私时,都会对本地文件数据进行加密处理。本次逆向的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 "";
}

执行后即可获得解密后文本:
在这里插入图片描述
 

相关资料

Android studio 3.0编写Xposed HOOK登录框

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值