apkanalyzer(3)-走读dex/arsc解析命令

栏目: 编程语言 · XML · 发布时间: 5年前

内容简介:接下来选两个具体的命令,来走读一遍实现功能的流程、使用了哪些功能库。第一个选择解析dex的最后一个功能,打印指定类、方法的反编译代码;第二个选择资源文件解析的最后一个命令,输出指定xml二进制文件对应的可读版本。首先,一定是从cli库中找到对应的命令枚举类型实现。在DEX_CODE类型的执行函数部分,可以找到impl类中对应的方法dexCode——ApkAnalyzerImpl类的功能函数代码如下:

接下来选两个具体的命令,来走读一遍实现功能的流程、使用了哪些功能库。第一个选择解析dex的最后一个功能,打印指定类、方法的反编译代码;第二个选择资源文件解析的最后一个命令,输出指定xml二进制文件对应的可读版本。

1. 打印dex中某个类、方法的smali代码

首先,一定是从cli库中找到对应的命令枚举类型实现。

DEX_CODE("dex", "code", "Prints the bytecode of a class or method in smali format") {
          public ArgumentAcceptingOptionSpec<String> classSpec;
          public ArgumentAcceptingOptionSpec<String> methodSpec;
          public OptionParser parser;
      
      @Override
      public OptionParser getParser(){
        	// 参数解析
          if (this.parser == null) {
              this.parser = super.getParser();
              this.classSpec = (ArgumentAcceptingOptionSpec<String>)this.parser.accepts("class", "Fully qualified class name to decompile.").withRequiredArg().ofType((Class)String.class).required();
              this.methodSpec = (ArgumentAcceptingOptionSpec<String>)this.parser.accepts("method", "Method to decompile. Format: name(params)returnType, e.g. someMethod(Ljava/lang/String;I)V").withRequiredArg().ofType((Class)String.class);
          }
          return this.parser;
      }
      
      @Override
      public void execute(final PrintStream out, final PrintStream err, final ApkAnalyzerImpl impl, final String... args){
          final OptionParser parser = this.getParser();
          final OptionSet opts = parseOrPrintHelp(parser, err, args);
        	// 解析dex部分
          impl.dexCode(((File)opts.valueOf((OptionSpec)this.getFileSpec())).toPath(), (String)opts.valueOf((OptionSpec)this.classSpec), (String)opts.valueOf((OptionSpec)this.methodSpec));
      }
  },

在DEX_CODE类型的执行函数部分,可以找到impl类中对应的方法dexCode——ApkAnalyzerImpl类的功能函数代码如下:

public void dexCode(final Path apk, final String fqcn, String method){
  			//解压apk文件
        try (final Archive archive = Archives.open(apk)) {
            final Collection<Path> dexPaths = Files.list(archive.getContentRoot()).filter(path -> Files.isRegularFile(path, new LinkOption[0]) && path.getFileName().toString().endsWith(".dex")).collect((Collector<? super Path, ?, Collection<Path>>)Collectors.toList());
            boolean dexFound = false;
          	//遍历dex文件
            for (final Path dexPath : dexPaths) {
              	//核心文件解析类
                final DexBackedDexFile dexBackedDexFile = DexFiles.getDexFile(dexPath);
              	//核心文件反编译的管理类
                final DexDisassembler disassembler = new DexDisassembler(dexBackedDexFile);
                if (method == null) {
                    try {
                      	//打印该文件的所有反编译代码
                        this.out.println(disassembler.disassembleClass(fqcn));
                        dexFound = true;
                    }
                    catch (IllegalStateException ex) {}
                }
                else {
                    final Optional<? extends DexBackedClassDef> classDef = (Optional<? extends DexBackedClassDef>)dexBackedDexFile.getClasses().stream().filter(c -> fqcn.equals(SigUtils.signatureToName(c.getType()))).findFirst();
                  	//找到参数指定的方法
                    if (classDef.isPresent()) {
                        method = ((DexBackedClassDef)classDef.get()).getType() + "->" + method;
                    }
                    try {
                      	//打印方法对应的反编译代码段
                        this.out.println(disassembler.disassembleMethod(fqcn, method));
                        dexFound = true;
                    }
                    catch (IllegalStateException ex2) {}
                }
            }
          //异常情况处理
            if (!dexFound) {
                if (method == null) {
                    throw new IllegalArgumentException(String.format("The given class (%s) not found", fqcn));
                }
                throw new IllegalArgumentException(String.format("The given class (%s) or method (%s) not found", fqcn, method));
            }
        }
        catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

流程比较明确,加了一些简单的注释。

第一步,解压APK

Archives工具类在apkanalyzer.jar中,open这个函数基本上是所有命令的调用必经之路——毕竟解析内容第一步就是解压APK文件。可以看到里面根据zip和其他两种情况,使用了不同的 工具 类来做解压:

public static Archive open(final Path archive)throws IOException {
    if (archive.getFileName().toString().toLowerCase().endsWith(".zip")) {
        return ZipArtifact.fromZippedBundle(archive);
    }
    final FileSystem fileSystem = FileUtils.createZipFilesystem(archive);
    return new AndroidArtifact(archive, fileSystem);
}

open函数返回的Archive是个接口,具体如下:

public interface Archiveextends AutoCloseable
{
    PathgetPath();
    
    PathgetContentRoot();
    
    boolean isBinaryXml(final Path p0, final byte[] p1);
    
    void close()throws IOException;
}

结合Archives来看,无非是使用java nio的一些文件工具,来实现解压方法。具体到AndroidArtifact上,有一些特殊的分析功能。

第二步,解析dex文件

DexBackedDexFile,该类在dexlib2-2.2.1.jar包中,包名路径是org.jf.dexlib2.dexbacked。这个包在sdk同目录下,也可以在网络上找到它的信息—— dexlib2

dexlib2 is a library for reading/modifying/writing Android dex files

简单讲,这个库可以读、写、改dex文件,很多搞hook、修改dex的工具插件等黑科技都会使用到这个库。可以查询它的javadoc看它具体的功能接口。

apkanalyzer(3)-走读dex/arsc解析命令

呃……不是很友好,没有什么注释的样子。这个类走读的话,看下构造就好了:

protected DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnullbyte[] buf,int offset, boolean verifyMagic)
 {
   super(buf, offset);
   
   this.opcodes = opcodes;
   if (verifyMagic) {
     DexUtil.verifyDexHeader(buf, offset);
   }
   this.stringCount = readSmallUint(56);
   this.stringStartOffset = readSmallUint(60);
   this.typeCount = readSmallUint(64);
   this.typeStartOffset = readSmallUint(68);
   this.protoCount = readSmallUint(72);
   this.protoStartOffset = readSmallUint(76);
   this.fieldCount = readSmallUint(80);
   this.fieldStartOffset = readSmallUint(84);
   this.methodCount = readSmallUint(88);
   this.methodStartOffset = readSmallUint(92);
   this.classCount = readSmallUint(96);
   this.classStartOffset = readSmallUint(100);
 }

成员属性的初始化,各种数据是如何从dex文件流中解析出来的,找对应的read函数就好了。

DexDisassembler 该类是apkanalyzer.jar里面的类,包名目录是com.android.tools.apk.analyzer.dex。它只有不到70行的长度,构造参数要求传入DexBackedDexFile实体,其中只有两个公用方法:

  • public String disassembleMethod(final String fqcn, final String methodDescriptor) throws IOException
  • public String disassembleClass(final String fqcn) throws IOException

逻辑也比较简单,通过构造传入的DexBackedDexFile实体,获得dex文件的解析的class信息,然后根据方法需要来输方法或者类的反编译信息。

第三步,根据参数要求输出,并处理异常情况

后面的输出逻辑也是一目了然的。根据是否有method参数,走不通的逻辑。有的话,查找对应的method;没有就输出整个类。如果找不到或者发生其他解析问题,抛出异常。

2. 把二进制XML文件的转换成可读的XML文件打印出来

相比而言,这条命令同样是代码的还原,只不过针对的是资源文件的二进制文件。同样的,找到对应的命令枚举类型的实现定义:

RESOURCES_XML("resources", "xml", "Prints the human readable form of a binary XML") {
            public OptionParser parser;
            private ArgumentAcceptingOptionSpec<String> filePathSpec;
            
  					//解析参数
            @Override
            public OptionParser getParser(){
                if (this.parser == null) {
                    this.parser = super.getParser();
                    this.filePathSpec = (ArgumentAcceptingOptionSpec<String>)this.parser.accepts("file", "File path within the APK.").withRequiredArg().ofType((Class)String.class);
                }
                return this.parser;
            }
            
            @Override
            public void execute(final PrintStream out, final PrintStream err, final ApkAnalyzerImpl impl, final String... args){
                final OptionParser parser = this.getParser();
                final OptionSet opts = parseOrPrintHelp(parser, err, args);
                assert this.filePathSpec != null;
              	//执行解析
                impl.resXml(((File)opts.valueOf((OptionSpec)this.getFileSpec())).toPath(), (String)opts.valueOf((OptionSpec)this.filePathSpec));
            }
        };

同样具体功能函数会追溯到impl类中,找到对应的方法:

public void resXml(final Path apk, final String filePath){
  	//解压apk
    try (final Archive archive = Archives.open(apk)) {
        final Path path = archive.getContentRoot().resolve(filePath);
      	//读文件
        final byte[] bytes = Files.readAllBytes(path);
      	//校验
        if (!archive.isBinaryXml(path, bytes)) {
            throw new IOException("The supplied file is not a binary XML resource.");
        }
      	//解析xml二进制码
        this.out.write(BinaryXmlParser.decodeXml(path.getFileName().toString(), bytes));
    }
    catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

同样的open函数,解压好目标APK文件。在校验好文件类型后,调用了BinaryXmlParser的静态方法,该类在apkanalyzer.jar中,具体实现如下:

public static byte[] decodeXml(final String fileName, final byte[] bytes) {
  //文件解析
  final BinaryResourceFile file = new BinaryResourceFile(bytes);
    final List<Chunk> chunks = (List<Chunk>)file.getChunks();
    //各种情况处理
  	if (chunks.size() != 1) {
        return bytes;
    }
    
  	if (!(chunks.get(0) instanceof XmlChunk)) {
        return bytes;
    }
  	
    final XmlPrinter printer = new XmlPrinter();
    final XmlChunk xmlChunk = (XmlChunk)chunks.get(0);
    visitChunks(xmlChunk.getChunks(), printer);
    final String reconstructedXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + printer.getReconstructedXml();
    return reconstructedXml.getBytes(Charsets.UTF_8);
}

文件解析

核心的文件解析部分在binary-resources.jar中,即BinaryResourceFile、Chunk类,包名目录是com.google.devrel.gmscore.tools.apk.arsc。根据BinaryResourceFile的构造可以看出这一点:

public BinaryResourceFile(byte[] buf)
{
  ByteBuffer buffer = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
  while (buffer.remaining() > 0) {
    this.chunks.add(Chunk.newInstance(buffer));
  }
}

Chunk略上,贴一部分代码基本就可以看出,是个根据资源文件二进制的数据结构特点来处理的具体解析类:

public abstract class Chunk
  implements SerializableResource
{
  public static final int PAD_BOUNDARY = 4;
  public static final int METADATA_SIZE = 8;
  private static final int CHUNK_SIZE_OFFSET = 4;
  @Nullable
  private final Chunk parent;
  protected final int headerSize;
  protected final int chunkSize;
  protected final int offset;
  
  public static enum Type
  {
    NULL(0),  STRING_POOL(1),  TABLE(2),  XML(3),  XML_START_NAMESPACE(256),  XML_END_NAMESPACE(257),  XML_START_ELEMENT(258),  XML_END_ELEMENT(259),  XML_CDATA(260),  XML_RESOURCE_MAP(384),  TABLE_PACKAGE(512),  TABLE_TYPE(513),  TABLE_TYPE_SPEC(514),  TABLE_LIBRARY(515);
    
    private final short code;
    private static final Map<Short, Type> FROM_SHORT;
    ...
    }
    ...
}

各种情况处理

解析后有几种情况。具体情况需要分析BinaryResourceFile的具体实现,来看。这里不做深入。最后一种情况则是解析XmlChunk。对应的使用XmlPrinter,该类是BinaryXmlParser中的一个内部工具类。看代码基本可以推断是转化各种Chunk内容的一个内容管理类。通过visitChunks,把XmlChunk的各种属性、元素内容写入XmlPrinter,最终输出可读的xml内容。

这里面除了上面提到的binary-resources相关的工具类和数据Bean之外,还涉及到一个XmlBuilder类,包名目录是com.android.xml,在common-26.0.0-dev.jar中,用于组装xml的各种零部件。

小结

也仅仅是走读代码,读其大略而已。没有深入研究dex、arsc具体的文件格式。

  • sdk tools里面有很多工具jar,基本上可以包含所有的apk打包相关的各个环节
  • 了解一些反编译的细节之后,多少对打包这件事本身会有更进一步的掌控

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

嵌入式Linux应用程序开发详解

嵌入式Linux应用程序开发详解

孙琼 / 人民邮电出版社 / 2006-7 / 46.00元

《嵌入式Linux应用程序开发详解》主要分为3个部分,包括Linux基础、搭建嵌入式Linux环境和嵌入式Linux的应用开发。Linux基础部分从Linux的安装过程、基本操作命令讲起,为Linux初学者能快速入门提供了保证。接着系统地讲解了嵌入式Linux的环境搭建,以及嵌入式Linux的I/O与文件系统的开发、进程控制开发、进程间通信开发、网络应用开发、基于中断的开发、设备驱动程序的开发以及......一起来看看 《嵌入式Linux应用程序开发详解》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具