简介
通过分析resources.arsc文件信息,找到存储字符串信息与资源文件直接的关系,然后对资源目录和文件进行混淆。
第一:可以增加反编译难度。
第二:可以减小应用包大小。
微信资源混淆插件
- 支持7zip压缩,进一步减小应用包大小。
- 资源混淆
- 等等
原理
在文章【安全与逆向】- resources.arsc分析,资源替换讲解了用于存储资源信息“resources.arsc”文件的结构和读取方法。那么把读取到数据打印出来,在控制台查看,便可以看到到resources.arsc详细的存储内容。这里我们借助Android Studio可视化界面查看resources.arsc。
截屏2020-02-05下午7.22.38.png
通过读取resources.arsc,我可以拿到资源的名称,路径等信息。信息的第一列是记录资源的id,在应用里面我们可以使用id找到对应的资源文件。比如图片资源,我们从resources.arsc读取到的资源信息是像“res/drawable-xhdpi-v4/unknown_image.png”这样的字符串,然后解析。通过id拿到也是这个字符串,然后去找对应的资源。
到这里,我们便可以得到资源混淆的方法了,只要保证上面读取到字符串路径和应用实际的资源路径一一对应,那么我们在使用资源的时候便不会有问题,所以我们可以先对resources.arsc资源路径进行修改混淆,然后在相应的修改应用的路径名和资源文件名。
读取resources.arsc
根据“resources.arsc”文件存储格式图,依次读取存储信息。
- RES_TABLE_TYPE(0x0002)
读取package数量,一个应用一般只有一个,所以值应该等于1,否则应用存在问题。public static Header read(ByteBuffer buffer) throws IOException { short type = buffer.getShort(); short headSize = buffer.getShort(); int chunkSize = buffer.getInt(); int packageNum = buffer.getInt(); }
int packageNum = buffer.getInt(); - RES_STRING_POOL_TYPE(0x0001)
public static StringPoolChunk read(ByteBuffer buffer) throws IOException { StringPoolChunk block = new StringPoolChunk(); block.mHeader = Header.read(buffer); int stringCount = buffer.getInt(); int styleCount = buffer.getInt(); int flags = buffer.getInt(); int stringsOffset = buffer.getInt(); int stylesOffset = buffer.getInt(); //flags == 0x00000100 则是utf-8编码 block.isUTF8 = (flags & UTF8_FLAG) != 0; //条目的值 指向字符串数据(byte数组)下标 block.mStringOffsets = new int[stringCount]; for (int i = 0; i < stringCount; i++) { block.mStringOffsets[i] = buffer.getInt(); } if (styleCount != 0) { block.mStyleOffsets = new int[stringCount]; for (int i = 0; i < stringCount; i++) { block.mStyleOffsets[i] = buffer.getInt(); } } //字符串长度,在stylesOffset == 0情况下,块大小 - 字符串的起始地址得到字符串占用的空间大小。 int size = ((stylesOffset == 0) ? block.mHeader.chunkSize : stylesOffset) - stringsOffset; if ((size % 4) != 0) { throw new RuntimeException("字符串字节长度必须能被4整除"); } block.mStrings = new byte[size]; buffer.get(block.mStrings); if (stylesOffset != 0) { size = (block.mHeader.chunkSize - stylesOffset); if ((size % 4) != 0) { throw new RuntimeException("资源类型字符串字节长度必须能被4整除"); } block.mStyles = new byte[size]; buffer.get(block.mStyles); } return block; }
- RES_TABLE_PACKAGE_TYPE(0x0200)
public static PackageHeader read(ByteBuffer buffer) throws IOException { PackageHeader header = new PackageHeader(); header.mHeader = Header.read(buffer); //包id header.mPackageId = buffer.getInt(); //包名 buffer.get(header.mPackageName); //类型字符串池偏移 (anim layout drawable等) header.mTypeStrOffset = buffer.getInt(); //lastpublictype 类型字符串资源池的个数 header.mTypeNameCount = buffer.getInt(); //资源项名称字符串池 (app_name activity_main 等) header.mSpecNameStrOffset = buffer.getInt(); //lastpublickey 资源名称字符串资源池的个数 header.mSpecNameCount = buffer.getInt(); //源码中 ResTable_package 还有个 uint32_t typeIdOffset; header.mTypeIdOffset = buffer.getInt(); return header; }
- 资源类型字符串池,资源项名称字符串池
这个和RES_STRING_POOL_TYPE结构相同。
混淆
- 修改resources.arsc
private void obfuscate(ARSC arsc) throws UnsupportedEncodingException { SimpleNameFactory typeNameFactory = new SimpleNameFactory(); SimpleNameFactory specNameFactory = new SimpleNameFactory(); int strCount = arsc.mTableStrings.getStrCount(); for (int i = 0; i < strCount; i++) { String rawStr = arsc.mTableStrings.getString(i); if (rawStr.startsWith("res")) { String[] names = rawStr.split("/"); //不符合 if (names == null || names.length != 3) { continue; } String newTypeName; String newSpecName; newTypeName = mTypeMap.get(names[1]); if (null == newTypeName) { newTypeName = typeNameFactory.nextName(); mTypeMap.put(names[1], newTypeName); } int index = names[2].indexOf('.'); if (index > 0) { names[2] = names[2].substring(0, index); } newSpecName = mSpecMap.get(names[2]); if (null == newSpecName) { newSpecName = specNameFactory.nextName(); mSpecMap.put(names[2], newSpecName); } } } StringPoolChunk mSpecNames = arsc.mSpecNames; strCount = mSpecNames.getStrCount(); for (int i = 0; i < strCount; i++) { String specName = mSpecNames.getString(i); if (!mSpecMap.containsKey(specName)) { String newSpecName = specNameFactory.nextName(); mSpecMap.put(specName, newSpecName); } } }
- 修改res下目录与文件名
private void obfuscate(File src, File dst) throws Exception { File[] files = src.listFiles(); for (File file : files) { if (file.getName().equals("resources.arsc")) { continue; } if (file.isFile() || !file.getName().equals("res")) { FileUtils.cpFiles(file, new File(dst, file.getName())); } } File resDir = new File(src, "res"); for (File type : resDir.listFiles()) { for (File spec : type.listFiles()) { String newName = getTableString("res/" + spec.getParentFile().getName() + "/" + spec.getName()); FileUtils.cpFiles(spec, new File(dst, newName)); } } }
总结
经过上面的步骤,混淆基本完成,剩下的就是重新打包,签名。这里只是简单介绍资源混淆的原理和简单实现资源混淆。如果想要使用到项目中,可以直接集成微信的资源混淆工具。