声明:案例分析仅供学习交流使用,勿用于任何非法用途。如学习者进一步逆向并对版权方造成损失,请自行承担法律后果,本人概不负责。
简介
热修复和插件化是目前比较热门的技术,它们都是通过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