【安全与逆向】- 仿微信资源混淆工具

简介

通过分析resources.arsc文件信息,找到存储字符串信息与资源文件直接的关系,然后对资源目录和文件进行混淆。
第一:可以增加反编译难度。
第二:可以减小应用包大小。

微信资源混淆插件

AndResGuard

  • 支持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)
    public static Header read(ByteBuffer buffer) throws IOException {
       short type = buffer.getShort();
       short headSize = buffer.getShort();
       int chunkSize = buffer.getInt();
       int packageNum = buffer.getInt();     
    }
    
    读取package数量,一个应用一般只有一个,所以值应该等于1,否则应用存在问题。
    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));
             }
         }
    
     }
    

总结

经过上面的步骤,混淆基本完成,剩下的就是重新打包,签名。这里只是简单介绍资源混淆的原理和简单实现资源混淆。如果想要使用到项目中,可以直接集成微信的资源混淆工具。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容