Android逆向:某薇直播通过ClassLoader加载的jar包解密

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

简介

热修复和插件化是目前比较热门的技术,它们都是通过ClassLoader来查找和加载Class文件到虚拟机来实现的。本次逆向的apk就是基于这种技术来加载关键代码的。
 

目标

破解ClassLoader动态加载的流程,拿到关键代码。
 

逆向流程

突破口——android.jar

首先对apk的本体包反编译,并没有查到核心业务逻辑,尝试查看本地文件,里面有个android.jar是被加密的,比较可疑,推测是解密后ClassLoader加载。
在这里插入图片描述
搜索关键字“android.jar”没有什么收获,猜测其来源有二:1.资产文件中复制进data,2.网络下载。虽然assets资产中确实有类似的jar包,但作为一个动态加载的jar,仅存在于资产文件中就失去了热更新的作用,那么资产文件中的应该是作为本地备用jar,网络获取到的才是最新的jar。于是进应用抓包:
在这里插入图片描述
在这里插入图片描述
果然config.json接口中有一个加密的jar。
在这里插入图片描述
从apk本体代码中跟踪config.json接口。
在这里插入图片描述
在这里插入图片描述
本地备用的猜想得到证明,于是继续追踪downloadFromNet。
在这里插入图片描述
该方法所在的com.vst.dev.common.util.Utils类中几个主要方法都做了高强度混淆,包括downloadFromNet。只能通过伪代码分析,发现这个方法并没有特别之处,仅仅是下载文件后保存,参数1传递文件,参数2为下载地址。下载保存的过程中没有做手脚,那说明至此应用只是将加密jar下载到本地,那么想要得到解密jar流程,就要从读取文件入手。

    public static boolean downLoafFileFromNet(File arg10, String arg11) {
        FileOutputStream v6_1;
        InputStream v5_1;
        FileOutputStream v7;
        byte[] v1;
        URLConnection v2;
        boolean v8 = false;
        Closeable v6 = null;
        Closeable v5 = null;
        try {
            v2 = new URL(arg11).openConnection();
            ((HttpURLConnection)v2).setConnectTimeout(5000);
            ((HttpURLConnection)v2).setReadTimeout(5000);
            ((HttpURLConnection)v2).connect();
            v1 = new byte[0x800];
            v7 = new FileOutputStream(arg10);
            goto label_19;
        }
        catch(Throwable v9) {
        }
        catch(IOException v4) {
            goto label_42;
            try {
            label_19:
                v5_1 = ((HttpURLConnection)v2).getInputStream();
                while(true) {
                    int v3 = v5_1.read(v1);
                    if(v3 == -1) {
                        break;
                    }

                    ((OutputStream)v7).write(v1, 0, v3);
                }
            }
            catch(Throwable v9) {
                goto label_55;
            }
            catch(IOException v4) {
                goto label_58;
            }
            catch(MalformedURLException v4_1) {
                goto label_27;
            }

            v8 = true;
            if(v2 != null) {
                ((HttpURLConnection)v2).disconnect();
            }

            Utils.closeIO(((Closeable)v7));
            Utils.closeIO(((Closeable)v5_1));
            return v8;
        label_27:
            v6_1 = v7;
            try {
            label_28:
                v4_1.printStackTrace();
                if(v2 != null) {
                    goto label_30;
                }

                goto label_31;
            }
            catch(Throwable v9) {
                goto label_49;
            }

        label_30:
            ((HttpURLConnection)v2).disconnect();
        label_31:
            Utils.closeIO(v6);
            Utils.closeIO(v5);
            return v8;
        label_58:
            v6_1 = v7;
            try {
            label_42:
                v4.printStackTrace();
                if(v2 == null) {
                    goto label_45;
                }
            }
            catch(Throwable v9) {
                goto label_49;
            }
        }
        catch(MalformedURLException v4_1) {
            goto label_28;
        }

        ((HttpURLConnection)v2).disconnect();
    label_45:
        Utils.closeIO(v6);
        Utils.closeIO(v5);
        return v8;
    label_55:
        v6_1 = v7;
    label_49:
        if(v2 != null) {
            ((HttpURLConnection)v2).disconnect();
        }

        Utils.closeIO(v6);
        Utils.closeIO(v5);
        throw v9;
    }

回溯到建立文件的地方,不难看出getASCIIForInt(t_def)就是文件名:

File sourceFile = new File(ctx.getCacheDir(), getASCIIForInt(t_def));

而t-def为整型数组:

private static int[] t_def = new int[]{97, 110, 100, 114, 111, 105, 100, 46, 106, 97, 114};

那么getASCIIForInt方法意图很明显,就是把t-def中整型转字符然后拼出文件名——android.jar。以这种方式保存文件名,难怪之前搜不到关键字。

    private static String getASCIIForInt(int[] t) {
        if (t == null || t.length <= 0) {
            return null;
        }
        StringBuffer sb = new StringBuffer();
        for (int i : t) {
            sb.append((char) i);
        }
        return sb.toString();
    }

查找与t_def相关引用,getSoManager方法浮出水面。
在这里插入图片描述

DexClassLoader加载

getSoManager伪代码如下,可以发现解密后是通过DexClassLoader加载其com.vst.so.parser.SoMananger类。

    private static Class getSoManager(Context arg24) {
        VSTInputStream v16_1;
        FileInputStream v8_1;
        VSTInputStream v0;
        FileInputStream v9;
        File v15;
        File v14;
        if(SoManagerUtil.sSoManager == null) {
            Class v19 = SoManagerUtil.class;
            __monitor_enter(v19);
            try {
                if(SoManagerUtil.sSoManager != null) {
                    goto label_166;
                }

                v14 = new File(arg24.getCacheDir(), SoManagerUtil.getASCIIForInt(SoManagerUtil.t_def));
                Log.d(SoManagerUtil.TAG, "getSoManager=" + v14.exists());
                if(!v14.exists()) {
                    SoManagerUtil.downloadFromAsset(arg24);
                    v14 = new File(arg24.getCacheDir(), SoManagerUtil.getASCIIForInt(SoManagerUtil.t_def));
                }

                v15 = new File(arg24.getCacheDir(), SoManagerUtil.getASCIIForInt(SoManagerUtil.t_name));
                if(v15.exists()) {
                    v15.delete();
                }
            }
            catch(Throwable v18) {
                goto label_204;
            }

            Closeable v8 = null;
            Closeable v10 = null;
            Closeable v16 = null;
            boolean v13 = true;
            try {
                if(SoManagerUtil.IS_ENABLE_ENCRYPT_GZIP) {
                    v9 = new FileInputStream(v14);
                }
                else {
                    goto label_169;
                }
            }
            catch(Throwable v5) {
                goto label_179;
            }
            catch(Throwable v18) {
                goto label_207;
            }

            try {
                v0 = new VSTInputStream(((InputStream)v9));
            }
            catch(Throwable v18) {
                v8_1 = v9;
                goto label_207;
            }
            catch(Throwable v5) {
                v8_1 = v9;
                goto label_179;
            }

            VSTInputStream v17 = v0;
            try {
                v13 = VSTInputStream.uncompress(v15, VSTInputStream.input2byte(((InputStream)v17)));
                v16_1 = v17;
                v8_1 = v9;
                goto label_61;
            }
            catch(Throwable v18) {
                v16_1 = v17;
                v8_1 = v9;
                goto label_207;
            }
            catch(Throwable v5) {
                v16_1 = v17;
                v8_1 = v9;
                goto label_179;
            }

        label_169:
            v15 = v14;
            try {
            label_61:
                LogUtil.d(SoManagerUtil.TAG, "ret = " + v13);
                if(v13) {
                    // 这里是通过DexClassLoader加载com.vst.so.parser.SoMananger
                    Log.d(SoManagerUtil.TAG, "source=" + v14.length() + ",temp=" + v15.length());
                    ClassLoader v11 = Build$VERSION.SDK_INT < 28 ? ClassLoader.getSystemClassLoader() : SoManagerUtil.class.getClassLoader();
                    SoManagerUtil.sSoManager = new DexClassLoader(v15.getAbsolutePath(), arg24.getDir("dex", 0).getAbsolutePath(), null, v11).loadClass("com.vst.so.parser.SoMananger");
                    Log.d(SoManagerUtil.TAG, "sSoManager=" + SoManagerUtil.sSoManager);
                    SoManagerUtil.sSoManObj = SoManagerUtil.sSoManager.getConstructor(Context.class).newInstance(arg24);
                }
                else {
                    if(!v14.exists()) {
                        goto label_148;
                    }

                    v14.delete();
                }

                goto label_148;
            }
            catch(Throwable v18) {
            }
            catch(Throwable v5) {
                try {
                label_179:
                    v5.printStackTrace();
                    if(v14.exists()) {
                        v14.delete();
                    }

                    SoManagerUtil.getSoManager(arg24);
                }
                catch(Throwable v18) {
                    goto label_207;
                }

                try {
                    Utils.closeIO(v10);
                    Utils.closeIO(v16);
                    Utils.closeIO(((Closeable)v8_1));
                    LogUtil.d(SoManagerUtil.TAG, "initSoTime = " + (System.currentTimeMillis() - SoManagerUtil.startInitSo));
                    goto label_166;
                }
                catch(Throwable v18) {
                    goto label_204;
                }
            }

            try {
            label_207:
                Utils.closeIO(v10);
                Utils.closeIO(v16);
                Utils.closeIO(((Closeable)v8_1));
                LogUtil.d(SoManagerUtil.TAG, "initSoTime = " + (System.currentTimeMillis() - SoManagerUtil.startInitSo));
                throw v18;
            label_148:
                Utils.closeIO(v10);
                Utils.closeIO(v16);
                Utils.closeIO(((Closeable)v8_1));
                LogUtil.d(SoManagerUtil.TAG, "initSoTime = " + (System.currentTimeMillis() - SoManagerUtil.startInitSo));
            label_166:
                __monitor_exit(v19);
                goto label_167;
            label_204:
                __monitor_exit(v19);
            }
            catch(Throwable v18) {
                goto label_204;
            }

            throw v18;
        }

    label_167:
        return SoManagerUtil.sSoManager;
    }

动了手脚的InputStream继承类

其中的关键就在与继承了InputStream的内部类VSTInputStream:

    class VSTInputStream extends InputStream {
        private int i;
        private InputStream is;

        public VSTInputStream(InputStream arg2) {
            super();
            this.i = 0;
            this.is = null;
            this.is = arg2;
        }

        public void close() throws IOException {
            try {
                if(this.is == null) {
                    goto label_4;
                }

                this.is.close();
            }
            catch(Throwable v0) {
                v0.printStackTrace();
            }

        label_4:
            super.close();
        }

        public static final byte[] input2byte(InputStream arg8) {
            ByteArrayOutputStream v4_1;
            byte[] v2;
            int v6_1;
            ByteArrayOutputStream v5;
            Closeable v4 = null;
            try {
                v5 = new ByteArrayOutputStream();
                v6_1 = 100;
                goto label_4;
            }
            catch(Throwable v6) {
            }
            catch(Throwable v1) {
                goto label_15;
                try {
                label_4:
                    byte[] v0 = new byte[v6_1];
                    while(true) {
                        int v3 = arg8.read(v0, 0, 100);
                        if(v3 <= 0) {
                            break;
                        }

                        v5.write(v0, 0, v3);
                    }

                    v2 = v5.toByteArray();
                }
                catch(Throwable v1) {
                    goto label_14;
                }
                catch(Throwable v6) {
                    goto label_30;
                }

                Utils.closeIO(((Closeable)v5));
                Utils.closeIO(((Closeable)arg8));
                return v2;
            label_14:
                v4_1 = v5;
                try {
                label_15:
                    v1.printStackTrace();
                }
                catch(Throwable v6) {
                    goto label_26;
                }
            }

            Utils.closeIO(((Closeable)v4_1));
            Utils.closeIO(((Closeable)arg8));
            return null;
        label_30:
            v4_1 = v5;
        label_26:
            Utils.closeIO(((Closeable)v4_1));
            Utils.closeIO(((Closeable)arg8));
            throw v6;
        }

        public int read() throws IOException {
            if(SoManagerUtil.IS_ENABLE_ENCRYPT_GZIP) {
                if(this.i == 0) {
                    int v1 = this.is.read();
                    int v0;
                    for(v0 = 0; v0 < v1; ++v0) {
                        this.is.read();
                    }
                }

                ++this.i;
            }

            return this.is.read();
        }

        public static boolean uncompress(File arg11, byte[] arg12) throws IOException {
            GZIPInputStream v4_1;
            ByteArrayInputStream v6_1;
            int v10;
            GZIPInputStream v5;
            FileOutputStream v2_1;
            ByteArrayInputStream v7;
            FileOutputStream v3;
            boolean v9 = false;
            if(arg12 == null) {
                return v9;
            }

            if(arg12.length == 0) {
                return v9;
            }

            Closeable v2 = null;
            Closeable v6 = null;
            Closeable v4 = null;
            try {
                v3 = new FileOutputStream(arg11);
                goto label_10;
            }
            catch(Throwable v9_1) {
            }
            catch(Exception v1) {
                goto label_25;
                try {
                label_10:
                    v7 = new ByteArrayInputStream(arg12);
                }
                catch(Throwable v9_1) {
                    v2_1 = v3;
                    goto label_36;
                }
                catch(Exception v1) {
                    v2_1 = v3;
                    goto label_25;
                }

                try {
                    v5 = new GZIPInputStream(((InputStream)v7));
                    v10 = 0x400;
                }
                catch(Throwable v9_1) {
                    v6_1 = v7;
                    v2_1 = v3;
                    goto label_36;
                }
                catch(Exception v1) {
                    v6_1 = v7;
                    v2_1 = v3;
                    goto label_25;
                }

                try {
                    byte[] v0 = new byte[v10];
                    while(true) {
                        int v8 = v5.read(v0);
                        if(v8 < 0) {
                            break;
                        }

                        v3.write(v0, 0, v8);
                    }
                }
                catch(Exception v1) {
                    goto label_22;
                }
                catch(Throwable v9_1) {
                    goto label_48;
                }

                v9 = true;
                Utils.closeIO(((Closeable)v3));
                Utils.closeIO(((Closeable)v7));
                Utils.closeIO(((Closeable)v5));
                return v9;
            label_22:
                v4_1 = v5;
                v6_1 = v7;
                v2_1 = v3;
                try {
                label_25:
                    v1.printStackTrace();
                }
                catch(Throwable v9_1) {
                    goto label_36;
                }
            }

            Utils.closeIO(v2);
            Utils.closeIO(((Closeable)v6_1));
            Utils.closeIO(v4);
            return v9;
        label_48:
            v4_1 = v5;
            v6_1 = v7;
            v2_1 = v3;
        label_36:
            Utils.closeIO(v2);
            Utils.closeIO(((Closeable)v6_1));
            Utils.closeIO(v4);
            throw v9_1;
            return v9;
        }
    }

流程梳理

解密代码流程:
在这里插入图片描述
简单整理翻译一下:

	/**
	 * 用于解密jar的操作类
	 */
	class VstJarDec extends InputStream {
		private int vstPoint = 0;
		private InputStream vstInputStream;

		public VstJarDec(File file) {
			try {
				this.vstInputStream = new FileInputStream(file);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
		}

		/**
		 * 读取加密jar
		 * 
		 * @param inputStream
		 * @return
		 */
		public byte[] readEncJar(InputStream inputStream) throws IOException {
			ByteArrayOutputStream byteArrayOutputStream = null;
			byte[] bArr = null;
			try {
				byteArrayOutputStream = new ByteArrayOutputStream();

				byte[] buffer = new byte[100];
				while (true) {
					int read = inputStream.read(buffer, 0, 100);
					if (read <= 0) {
						break;
					}
					byteArrayOutputStream.write(buffer, 0, read);
					byteArrayOutputStream.flush();
				}
				bArr = byteArrayOutputStream.toByteArray();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				FileUtils.closeStream(byteArrayOutputStream);
				FileUtils.closeStream(inputStream);
			}
			return bArr;
		}

		/**
		 * 写出解密jar
		 * 
		 * @param outPath
		 * @param data
		 * @return
		 */
		public boolean writeDecJar(String outPath, byte[] data) throws IOException {
			GZIPInputStream gzipInputStream = null;
			ByteArrayInputStream byteArrayInputStream = null;
			FileOutputStream fileOutputStream = null;
			try {
				if (data != null && data.length > 0 && StringUtils.isNotBlank(outPath)) {
					fileOutputStream = new FileOutputStream(outPath);
					byteArrayInputStream = new ByteArrayInputStream(data);
					gzipInputStream = new GZIPInputStream(byteArrayInputStream);
					byte[] buffer = new byte[1024];
					while (true) {
						int read = gzipInputStream.read(buffer);
						if (read < 0) {
							break;
						}
						fileOutputStream.write(buffer, 0, read);
						fileOutputStream.flush();
					}
					return true;
				}
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				FileUtils.closeStream(gzipInputStream);
				FileUtils.closeStream(byteArrayInputStream);
				FileUtils.closeStream(fileOutputStream);
			}
			return false;
		}

		@Override
		public int read() throws IOException {
			if (this.vstPoint == 0) {
				int read = this.vstInputStream.read();
				for (int i = 0; i < read; ++i) {
					this.vstInputStream.read();
				}
			}
			++this.vstPoint;
			return this.vstInputStream.read();
		}
	}

用上面的方法就能解密出jar了。
在这里插入图片描述
 

相关资料

Android中的类装载器DexClassLoader
DexClassLoader和PathClassLoader的区别
Android插件化开发之DexClassLoader动态加载dex、jar小Demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值