美文网首页
Android逆向三部曲之AndroidManifest.xml

Android逆向三部曲之AndroidManifest.xml

作者: 北京朝阳区精神病院院长 | 来源:发表于2020-09-02 15:32 被阅读0次

    前言

    Apktool命令大全一文中提过,Android中的apk程序本质上是一个包含资源和汇编Java代码的压缩(.zip)包,其中最核心的三个文件 分别是AndroidManifest.xml、classes.dex、resources.arsc

    做过逆向相关的工作的人都知道,开源工具Apktool 工作原理就是解析这三个文件格式。apk解压后AndroidManifest.xml(清单文件)用普通文本格式打开的话是乱码的。

    这是因为在打包的过程中,清单文件被编译成二进制数据,存储在apk安装包中。所以我们需要了解清单文件的二进制结构,这样才能读取我们需要的原始信息。如何解析清单文件呢?让我们带着这个问题往下看。

    1.清单文件格式解析

    AndroidManifest.xml文件官方文档并没有找到公开格式描述,在网上找到前辈大佬们一些学习资料,最有分析价值的还是看雪论坛大佬MindMac的结构图,请看下图, 原图链接点击直达

    MindMac清单文件结构图.png

    虽然是14年的结构图的放在现在但是依旧经典,除了这个结构图,还给大家推荐一款工具十六进制编辑器
    010 editor(v10.0版本) 这对于我们分析清单文件结构非常有用。

    对于上面的清单文件结构图 没有过这方面知识和基础看的肯定一脸懵逼,完全不知道什么意思。我第一次看的时候我也如此 ,但是一点点看下来,分析一下结构还是比较清晰明朗的。结合实践, 带大家一步步解惑。本文中涉及的清单文件均来源网络。

    用010 editor 编译器,清单文件整体结构非常清晰,如图所示,不理解什么意思耐心往下看

    header.png

    结合上面的两个结构图,可以大体分为四大部分:

    1. Header :头文件

    2. String Chunk : 存储字符串资源的程序块

    3. ResourceId Chunk :存储资源id的程序块

    4. XmlContent Chunk : 存储xml内容程序块,其中包含了五个部分,Start Namespace Chunk 、End Namespace Chunk 、Start Tag Chunk 、End Tag Chunk 、Text Chunk

    1.1解析头文件

    任何一个文件格式都会有头文件信息,AndroidManifest.xml 是一系列的模块组合而成,我们可以理解为清单文件整体就是一个模块,这个整体的模块包含了多个子模块。下图所示就是010 editor 模板分析出的头文件结构

    header_file.png

    头文件有固定的格式头部信息,根据010 editor分析得知其中包含的字段如下:

    1. magicnumber :魔数, 固定值 0x0008003(16进制),四个字节 ,(524291这个值是10进制 )

    2. filesize :文件总字节数 ,四个字节

    这里涉及到一个知识点,大小端模式,清单文件用的是小端模式表示的,filesize从010 editor上正常看是 7c180000,如果是用小端模式表示十六进制应该是 0000187c, 转换成十进制 结果就是 6268字节。进一步转换下文件大小就是6.12kb (6268/1024),我们用代码做下解析即可验证结果。

    header_parser.png
       /**
        *  解析头文件
        * @throws IOException 
        */
      private  void   getHeaderChunk() throws IOException {
        
            String  magicNumber = byteReader.readHexString(4); //读取成16进制字符串
            System.out.println("ParserSources  Header Start=================================");
            System.out.println("magin number hex :"+magicNumber);
            int fileSize=byteReader.readInt();   //16进制转换成 10进制字节码
            System.out.println("file Size  decimal :"+fileSize);
            System.out.println("ParserSources  Header End=================================");
        
      }
     
    

    上面的代码是经过封装处理过的,直接调用即可。简述下代码逻辑: 将清单文件读到一个byte数组中解析,因为是小端模式 ,获取magic number 的时候进行过一次反转byte数组,将byte数组转化成16进制的字符串。得到的结果进行拼串得出的结果是0x00080003,fileSize是对byte[]进行了 十进制的转换 ,得到的值就是6268。

    //结果输出
    ParserSources  Header Start=================================
    magin number hex :0x00080003
    file Size  decimal :6268
    ParserSources  Header END=================================
    
    

    1.1.1大小端模式

    举个例子,直观的认识下大小端模式,0x12345678在内存中的存储方式

    • 大端模式

      低地址 -----> 高地址
      0x12 | 0x34 | 0x56 | 0x78

    • 小端模式

      低地址 -----> 高地址
      0x78| 0x56 | 0x34 | 0x12

    1.2 解析String Chunk

    String Chunk 主要用于存放清单文件所有字符串信息,做解析的时候最终获取的就是strItem[0]-strItem[n]里的字符串值

    如图所示,010 editor 模板解析出来的字符串结构,正好能和上面的看雪大佬的结构图String Chunk对应上,我会对字符串信息做下说明,让大家知道什么意思。

    String Pool.png
    • ChunkType :StringChunk类型,4个字节 ,固定值 0x001c0001

    • ChunkSize :StringChunk大小 ,4个字节

    • StringCount :StringChunk字符串的个数,4个字节

    • StyleCount :StringChunk样式的个数,4个字节,固定值 0x00000000

    • Unkown : 位置区域,4个字节,固定值 0x00000000解析时候需要略过4个字节

    • StringPoolOffset :字符串池偏移量,4个字节,偏移量相对StringChunk头部位置

    • StylePoolOffset :样式池偏移量,4个字节,偏移量相对于StringChunk头部位置
      固定值 0x00000000 ,这个字段基本没用到过,可忽略掉

    • StringOffsets :每个字符串在字符串池中的相对偏移量,int数组,它的大小是StringCount*4个字节

    • StyleOffsets :每个样式的偏移量,int数组,它的大小是StyleCount*4个字节

    • String Pool :字符串池,存储了所有的字符串

    • Style Pool :样式池,存储了所有的样式

      private  void   getStringChunk() throws IOException {
          System.out.println("");
          
          System.out.println("ParserSources  String Chunk Start=================================");
          
          
          String chunkType  = byteReader.readHexString(4); //输出的值是 用16进制 的 0x001C0001  ,转换成10进制 就是1835009
          System.out.println("String  chunkType :"+chunkType);
          
          int chunkSize = byteReader.readInt();
          System.out.println("String  chunkSize :"+chunkSize);
          
          
          int stringCount = byteReader.readInt();
          System.out.println("String  StringCount :"+stringCount);
          
                
          
          int styleCount = byteReader.readInt(); 
          System.out.println("String  StyleCount :"+styleCount);
          
          byteReader.jump(4);//跳过 unknown的 4个字节
          
          int stringPoolOffset = byteReader.readInt();
          System.out.println("String  StringPoolOffset  :"+stringPoolOffset);
          
          
          int stylePoolOffset = byteReader.readInt();
          System.out.println("String  StylePoolOffset   :"+stylePoolOffset);
          
          
    
          // 每个 string 的偏移量,stringCount*4个字节
          List<Integer> stringPoolOffsets = new ArrayList<>(stringCount);
          
          for (int i = 0; i < stringCount; i++) {
              stringPoolOffsets.add(byteReader.readInt());
          }
          
          
          // 每个 style 的偏移量 ,StringCount*4个字节
          List<Integer> stylePoolOffsets = new ArrayList<>(styleCount);
          for (int i = 0; i < styleCount; i++) {
              stylePoolOffsets.add(byteReader.readInt());
          }
    
          System.out.println("string pool start" );
          
          //偏移值开始的两个字节是字符串的长度,接着是字符串的内容,后面跟着两个字符串的结束符00
          
          for (int i = 1; i <= stringCount; i++) { // 没有读最后一个字符串
              String string;
              if (i == stringCount) {
                  int lastStringLength = byteReader.readShort() * 2; //一个字符对应着2个字节,所以要*2
                  string = new String(moveBlank(byteReader.readOrigin(lastStringLength)));
                  byteReader.jump(2);
              } else {
                  byteReader.jump(2); // 字符长度          
                  byte[] content = byteReader.readOrigin(stringPoolOffsets.get(i) - stringPoolOffsets.get(i - 1) - 4);  // 根据偏移量读取字符串
                  byteReader.jump(2); // 跳过结尾的 0000  2个字节
                  string = new String(moveBlank(content));
    
              }
              
              System.out.println(string);
              stringChunkList.add(string); //把所有字符串都添加到ArrayList中
          }
          
           //Style Pool 
          for (int i = 1; i < styleCount; i++) {
              byteReader.jump(2);
              byte[] content = byteReader.readOrigin(stylePoolOffsets.get(i) - stylePoolOffsets.get(i - 1) - 4);
              byteReader.jump(2);
              String string = new String(content);
              System.out.println("Style Pool "+string);
          }
          byteReader.moveTo(chunkSize+8); //ResourceIdChunk 之前可能存在 0000,是为了对齐
          
    
          System.out.println("ParserSources  String Chunk End=================================");
      }
    
    
       /**
        * 移除多余的空的字节
        */
      public static byte[] moveBlank(byte[] data) {
          List<Byte> byteList = new ArrayList<>();
          for (Byte b : data) {
              if (b != 0) byteList.add(b);
          }
          byte[] result = new byte[byteList.size()];
          for (int i = 0; i < result.length; i++)
              result[i] = byteList.get(i);
          return result;
      }
    
    //解析结果输出
    ParserSources  String Chunk Start=================================
    String  chunkType :0x001C0001
    String  ChunkSize :3172
    String  StringCount :69
    String  StyleCount :0
    String  StringPoolOffset  :304
    String  StylePoolOffset   :0
    
    string pool start
    
    compileSdkVersion
    compileSdkVersionCodename
    installLocation
    versionCode
    versionName
    minSdkVersion
    targetSdkVersion
    name
    glEsVersion
    required
    allowBackup
    banner
    resizeableActivity
    icon
    isGame
    label
    usesCleartextTraffic
    theme
    value
    configChanges
    hardwareAccelerated
    launchMode
    screenOrientation
    android
    http://schemas.android.com/apk/res/android
    
    package
    platformBuildVersionCode
    platformBuildVersionName
    manifest
    6.0-2438415
    com.xdqbf.cw
    23
    2.6.1
    uses-sdk
    uses-permission
    com.android.vending.BILLING
    android.permission.WRITE_EXTERNAL_STORAGE
    uses-feature
    android.permission.INTERNET
    android.permission.ACCESS_NETWORK_STATE
    android.hardware.touchscreen
    android.hardware.touchscreen.multitouch
    android.hardware.touchscreen.multitouch.distinct
    com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE
    application
    com.hikergames.HikerGamesApplication
    meta-data
    Y2GAME_CHANNEL
    Y2PUBLISHER_CHANNEL
    android.max_aspect
    activity
    com.hikergames.MainActivity
    intent-filter
    action
    android.intent.action.MAIN
    category
    android.intent.category.LAUNCHER
    android.intent.category.LEANBACK_LAUNCHER
    unityplayer.UnityActivity
    unityplayer.ForwardNativeEventsToDalvik
    unity.build-id
    93406909-c0b3-40de-b7c5-75971379027e
    unity.splash-mode
    unity.splash-enable
    com.unity.udp.udpsandbox.LoginActivity
    com.unity.udp.udpsandbox.UDPPurchasing$LoginHelperActivity
    CHANNEL_NAME
    UDP
    
    string pool end
    
    ParserSources  String Chunk End=================================
    

    这里做个说明: 除了解析字符串内容外,其他的没什么可说的,封装好的方法复用。

    • unknown的这个字段上面解释过,没有什么实际意义,在解析中跳过忽略

    • 一个字符串对应的是2个字节,上面有个方法moveBlank(byte[] data),逻辑就是过滤一下空的字符串,在java语言中空字符串就是 00 ,如果不过滤会出现宽字符(例:v e r s i o n),过滤后就正常了展示。

    • 样式池在解析过程中一般都为空,样式数量也为 0

    • 解析字符串有个格式,偏移值开始的两个字节是字符串的长度,接着是字符串的内容,后面跟着两个字符串的结束符00 (0x0000),下图中有三个属性分别是 size, oneChar,end,对应这字符串长度,字符串内容和结束符,字符长度这里是00 11 (十进制 值是17 )。字符内容16进制有点多忽略,结束符的是 00 00

    StringChunk字符串格式.png

    1.2.1 偏移量的概念

    把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移。更通俗一点讲,内存中存储数据的方式是:一个存储数据的“实际地址”=段首地址+偏移量,你也可以这样理解:就像我们现实中的“家庭地址”=“小区地址”+“门牌号”

    1.3ResourceId Chunk

    ResourceId Chunk主要 用来存放清单文件中所引用到的系统属性值和当前应用所对应的资源ID,看一下 010 edtior属性块,对应这看雪大神结构图ResourceId Chunk。

    ResourceldChunk.png
    • ChunkType :ResourceldChunk的类型,4个字节 ,固定值 0x00080180

    • ChunkSize :ResourceldChunk的大小 ,4个字节

    • ResourceIds :int数组,大小为(chunkSize - 8) / 4 ,减 8是减去头部大小的8个字节(ChunkType和ChunkSize)

      /**
        *获取资源清单文件资源id  
        * @throws IOException
        */
      
      private  void   getResourceIdChunk() throws IOException {
           System.out.println();
           System.out.println("ParserSources  ResourceId Chunk Start=================================");
            String chunkType = byteReader.readHexString(4);
            System.out.println("chunkType:"+chunkType);
          
            int chunkSize = byteReader.readInt();
            System.out.println("chunkSize:"+chunkSize);
           
           // ResourceIds 大小为(chunkSize - 8) / 4 
            int resourcesIdChunkCount = (chunkSize - 8) / 4;
            
            for (int i = 0; i < resourcesIdChunkCount; i++) {//遍历ResourceIds
                String resourcesId = byteReader.readHexString(4);
                System.out.println("resource id:"+resourcesId);
           
            }
          
            System.out.println("ParserSources  ResourceId Chunk End=================================");
      }
    
    //解析结果输出
    ParserSources  ResourceId Chunk Start=================================
    chunkType:0x00080180
    chunkSize:100
    resource id:0x01010572
    resource id:0x01010573
    resource id:0x010102B7
    resource id:0x0101021B
    resource id:0x0101021C
    resource id:0x0101020C
    resource id:0x01010270
    resource id:0x01010003
    resource id:0x01010281
    resource id:0x0101028E
    resource id:0x01010280
    resource id:0x010103F2
    resource id:0x010104F6
    resource id:0x01010002
    resource id:0x010103F4
    resource id:0x01010001
    resource id:0x010104EC
    resource id:0x01010000
    resource id:0x01010024
    resource id:0x0101001F
    resource id:0x010102D3
    resource id:0x0101001D
    resource id:0x0101001E
    ParserSources  ResourceId Chunk End=================================
    

    说明:这一部分没什么好说,都是封装好的方法,输出的resource Id 是16进制的,010 edtor上显示的数值是转换后的10进制。

    1.3.1解析出来的ID是什么?

    接着上面的说,上一章节1.3 解析ResourceId Chunk结果 会得到一个十进制的或者 16进制的一个Id值,那这个id值是怎么来的呢?

    编译器在编译安卓资源的时候,至少会涉及两个包,其中一个是系统资源包,另一个就是当前正在编译应用的资源包。每一个包都有定义自己的资源,同时它也可以引用其它包的资源,一个包通过什么方式来引用其它包的资源呢?这就是我们熟悉的资源ID 了。资源ID是一个4字节的无符号整数,其中,最高字节表示Package ID,次高字节表示Type ID,最低两字节表示Entry ID。

    Package ID相当于是一个命名空间,限定资源的来源。Android系统当前定义了两个资源命令空间,其中一个系统资源命令空间,它的Package ID等于0x01,另外一个是应用程序资源命令空间,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之间的Package ID都是合法的,而在这个范围之外的都是非法的Package ID。这一点可以通过生成的R.java文件来验证,引用系统资源都是0x01开头的。

    Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。

    Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。

    关于系统对应的资源ID更多描述,以及系统资源的引用关系,可以参考系统源码,我这里用的是安卓8.0系统, 存放路径:frameworks/base/core/res/res/values/public.xml点击可以直接看源码

    public.xml

    1.4 XmlContentChunk

    XmlContentChunk 这部分表示的是,存储了清单文件的详细信息,包含的5项,其中Start Namespace Chunk 和End Namespace Chunk ,这两个可以合并一个来说明, 因为他们的结构是完全一致,解析过程也是一样的。至于EndTagChunk一共有6个数据,也就是 Start Tag Chunk 的前 6 项,这里不做单独解析和说明。EndTagChunk这个跟清单文件标签一样的,就是给解析出来的标签加上结束标签一样。

    Text Chunk这个模块在010 edtor模板里并没有用到过,我就不做说明了,如果哪位大佬遇到过这个模块,可以私聊或者评论,一起研究一下。

    1.4.1 Start Namespace Chunk

    这个Chunk主要包含是一个清单文件的命令空间内容,老规矩010 edtor 分析一下结构 ,属性如下图所示:

    Start Namespace Chunk.png
    • ChunkType :Chunk的类型,4个字节 ,固定值 0x00100100

    • ChunkSize :Chunk的大小 ,4个字节

    • LineNumber :清单文件中的行号, 4个字节

    • Unknown :未知区域, 4个字节

    • Prefix :命名空间的前缀, 4个字节

    • Uri :命名空间的URI, 4个字节

      private void getStartNamespaceChunk() throws IOException{
           
            System.out.println();
            
            System.out.println("ParserSources StartNamespaceChunk Start=================================");
            
            
            int chunkSize = byteReader.readInt();
            System.out.println("NamespaceChunk  ChunkType  :"+chunkSize);
         
    
            int lineNumber = byteReader.readInt();
           
            System.out.println("NamespaceChunk  LineNumber  :"+lineNumber);
            
            byteReader.jump(4);  //这里需要注意的是行号后面的四个字节为0xffffffff,过滤
    
            int prefix = byteReader.readInt();
          
            System.out.println("NamespaceChunk  Prefix   :"+prefix);
            
            int uri = byteReader.readInt();
          
            System.out.println("NamespaceChunk  Uri   :"+uri);
    
            StartNameSpaceChunk startNameSpaceChunk = new StartNameSpaceChunk(chunkSize, lineNumber, prefix, uri);
            chunkList.add(startNameSpaceChunk);
            nameSpaceMap.put(stringChunkList.get(prefix), stringChunkList.get(uri));
            
            
            System.out.println("ParserSources StartNamespaceChunk End=================================");
    }
    
    //解析结果输出
    ParserSources StartNamespaceChunk Start=================================
    NamespaceChunk  ChunkType  :24
    NamespaceChunk  LineNumber  :1
    NamespaceChunk  Prefix   :23
    NamespaceChunk  Uri   :24
    ParserSources StartNamespaceChunk End=================================
    
    

    我还是拿个例子图,让大家更直观的了解什么意思,重点是prefix和url 两个属性。

    首先如下图所示,Prefix的值是23 ,Uri的值是24,根据这2个数字值,在字符串池能找到对应的字符值,如图stringChunk_attribute.png,23对应着字符串值android ,24对应着字符串http://schemas.android.com/apk/res/android,也可以把Namespace Chunk 当成字符串索引值,这样能对这个Namespace Chunk更好的理解了

    namespace_attribute.png stringChunk_attribute.png

    说明:这一部分也没什么好说,都是封装好的方法,需要注意的是Unknown 四个字节为FFFF需要过滤,命名空间的后缀和 uri 的对应关系保存在了 map 中,供后面解析的时候使用。

    1.4.2 Start Tag Chunk

    这个TagChunk主要存放清单文件中的标签信息,最核心也是最麻烦的内容。老规矩010 edtor 分析一下结构 ,属性如下图所示:

    Tag Chunk.png
    • ChunkType :Chunk的类型,4个字节 ,固定值 0x00100102

    • ChunkSize :Chunk的大小 ,4个字节

    • LineNumber :清单文件中的行号, 4个字节

    • Unknown :未知区域, 4个字节

    • Namespace Uri :命名空间用到的url在字符串的索引,值为 -1 表示没有用到命名空间 uri。
      标签的一般都没有使用到命名空间,此值为 -1,4个字节

    • Name :标签名称(在字符串中的索引值),4个字节

    • Flags :标签类型例如开始标签还是结束标签,固定值0x00140014,4个字节,

    • Attribute Count :标签包含的属性个数,4个字节

    • Class Attribute :标签包含的类属性,此项值常为 0,4个字节

    • Attributes :属性内容集合,每个属性固定 20 个字节,包含 5 个字段,每个字段都是 4 字节无符号 int,解析的时候需要注意Type这个值需要做一次处理需要又移24位。各个字段含义如下:

      • NamespaceUri :属性的命名空间uri 在字符串池中的索引

      • Name :属性名称在字符串池中的索引

      • ValueStr :属性值

      • Type :属性类型

      • Data :属性数据

     private void getStartTagChunk()throws IOException{
           System.out.println();
            
            System.out.println("ParserSources StartTagChunk Start=================================");
            
            
        //   System.out.println("Chunk Type:  "+START_TAG_CHUNK_TYPE);//因为是固定值 ,所以直接写的静态常量
            
           int chunkSize = byteReader.readInt();
           System.out.println("StartTag  ChunkSize   :"+chunkSize);
    
           int lineNumber = byteReader.readInt();
           System.out.println("StartTag  LineNumber   :"+lineNumber);
    
           byteReader.jump(4); //   Unknown 需要跳过4个字节   0xffffffff
    
           int namespaceUri = byteReader.readInt();
           
           if (namespaceUri == -1)
               System.out.println("namespace uri  is null");
               
           else
               System.out.println("namespace uri :"+stringChunkList.get(namespaceUri));
              
    
           int name = byteReader.readInt();
           System.out.println("Name :"+stringChunkList.get(name));
         
    
           byteReader.jump(4); // Flag 固定值需要做忽略处理  0x00140014 
    
           int attributeCount = byteReader.readInt();
           
           System.out.println("AttributeCount:"+attributeCount);
           
    
           int classAttribute = byteReader.readInt();
           
           System.out.println("ClassAttribute:"+classAttribute);
          
    
           List<Attribute> attributes = new ArrayList<>();
           
           for (int i = 0; i < attributeCount; i++) {
               
               System.out.println("Attribute["+i+"]");
    
               
    
               int namespaceUriAttr = byteReader.readInt();
               if (namespaceUriAttr == -1)
                   
                   System.out.println("Namespace Uri: null");
               else
                   System.out.println("Namespace Uri:"+stringChunkList.get(namespaceUriAttr));            
    
               int nameAttr = byteReader.readInt();
               if (nameAttr == -1)
                   System.out.println("Name: null");
                  
               else
                   System.out.println("Name:"+stringChunkList.get(nameAttr));
                 
    
               int valueStr = byteReader.readInt();
               if (valueStr == -1)
                   
                   System.out.println("ValueStr: null");
               
               else
                   System.out.println("ValueStr:"+stringChunkList.get(valueStr));
                 
    
               int type = byteReader.readInt() >> 24;
               System.out.println("Type:"+type);
               
    
               int data = byteReader.readInt();
               String dataString = type == TypedValue.TYPE_STRING ? stringChunkList.get(data) : TypedValue.coerceToString(type, data);
               System.out.println("Data:"+dataString);
           
    
               Attribute attribute = new Attribute(namespaceUriAttr == -1 ? null : stringChunkList.get(namespaceUriAttr),
               stringChunkList.get(nameAttr), valueStr, type, dataString);
               attributes.add(attribute);
           }
           
           StartTagChunk startTagChunk = new StartTagChunk(namespaceUri, stringChunkList.get(name), attributes);
           chunkList.add(startTagChunk);
           
           System.out.println("ParserSources StartTagChunk End=================================");
       }
    
    // 解析结果输出
    ParserSources StartTagChunk Start=================================
    StartTag  ChunkSize   :196
    StartTag  LineNumber   :1
    namespace uri  is null
    Name :manifest
    AttributeCount:8
    ClassAttribute:0
    Attribute[0]
    Namespace Uri:http://schemas.android.com/apk/res/android
    Name:versionCode
    ValueStr: null
    Type:16
    Data:41
    Attribute[1]
    Namespace Uri:http://schemas.android.com/apk/res/android
    Name:versionName
    ValueStr:2.6.1
    Type:3
    Data:2.6.1
    Attribute[2]
    Namespace Uri:http://schemas.android.com/apk/res/android
    Name:installLocation
    ValueStr: null
    Type:16
    Data:2
    Attribute[3]
    Namespace Uri:http://schemas.android.com/apk/res/android
    Name:compileSdkVersion
    ValueStr: null
    Type:16
    Data:23
    Attribute[4]
    Namespace Uri:http://schemas.android.com/apk/res/android
    Name:compileSdkVersionCodename
    ValueStr:6.0-2438415
    Type:3
    Data:6.0-2438415
    Attribute[5]
    Namespace Uri: null
    Name:package
    ValueStr:com.xdqbf.cw
    Type:3
    Data:com.xdqbf.cw
    Attribute[6]
    Namespace Uri: null
    Name:platformBuildVersionCode
    ValueStr:23
    Type:16
    Data:23
    Attribute[7]
    Namespace Uri: null
    Name:platformBuildVersionName
    ValueStr:6.0-2438415
    Type:3
    Data:6.0-2438415
    ParserSources StartTagChunk End=================================
    
      /**
       *根据上面的输出结果再做进一步处理一下 ,看着是不是很熟悉了。
      */
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     android:compileSdkVersion="23" 
     android:compileSdkVersionCodename="6.0-2438415"
     android:installLocation="preferExternal" 
     package="com.xdqbf.cw" 
     platformBuildVersionCode="23" 
     platformBuildVersionName="6.0-2438415">
    
    

    解析说明:

    • 解析属性的需要注意的是每个属性单元都是由5个元素组成(Attributes 字段),每个元素占用4个字节,所以获取type时候需要做右移24位。

    • platformBuildVersionCode和platformBuildVersionName 这两个是系统在编译的时候自动添加的两个属性,在解析中会体现出来。

    • 当没有android这样的前缀时,NamespaceUri 是 null

    • 当dataTtpe不同,对应的data值也不同,当前代码引用的系统的工具TypedValue,这个类就是用来进行转义的。
    • 每个属性理论上都会含有一个NamespaceUri,这个决定了属性前缀Prefix 默认都是Android,但清单文件也会存在自定义控件的情况,所以就需要导入NamespaceUri和Prefix ,所以一个XML可能有多个Namespace,每个属性都会包含NamespaceUri

    1.4.3 展示出完整的解析结果

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
            android:versionCode="41"
            android:versionName="2.6.1"
            android:installLocation="2"
            android:compileSdkVersion="23"
            android:compileSdkVersionCodename="6.0-2438415"
            package="com.xdqbf.cw"
            platformBuildVersionCode="23"
            platformBuildVersionName="6.0-2438415">
            <uses-sdk
                android:minSdkVersion="16"
                android:targetSdkVersion="29">
            </uses-sdk>
            <uses-permission
                android:name="com.android.vending.BILLING">
            </uses-permission>
            <uses-permission
                android:name="android.permission.WRITE_EXTERNAL_STORAGE">
            </uses-permission>
            <uses-feature
                android:glEsVersion="0x00020000">
            </uses-feature>
            <uses-permission
                android:name="android.permission.INTERNET">
            </uses-permission>
            <uses-permission
                android:name="android.permission.ACCESS_NETWORK_STATE">
            </uses-permission>
            <uses-feature
                android:name="android.hardware.touchscreen"
                android:required="false">
            </uses-feature>
            <uses-feature
                android:name="android.hardware.touchscreen.multitouch"
                android:required="false">
            </uses-feature>
            <uses-feature
                android:name="android.hardware.touchscreen.multitouch.distinct"
                android:required="false">
            </uses-feature>
            <uses-permission
                android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE">
            </uses-permission>
            <application
                android:theme="@2131165186"
                android:label="@2131099648"
                android:icon="@2130837506"
                android:name="com.hikergames.HikerGamesApplication"
                android:allowBackup="true"
                android:banner="@2130837505"
                android:isGame="true"
                android:usesCleartextTraffic="true"
                android:resizeableActivity="true">
                <meta-data
                    android:name="Y2GAME_CHANNEL"
                    android:value="0">
                </meta-data>
                <meta-data
                    android:name="Y2PUBLISHER_CHANNEL"
                    android:value="0">
                </meta-data>
                <meta-data
                    android:name="android.max_aspect"
                    android:value="2.1">
                </meta-data>
                <activity
                    android:label="@2131099648"
                    android:icon="@2130837506"
                    android:name="com.hikergames.MainActivity"
                    android:launchMode="2"
                    android:screenOrientation="6"
                    android:configChanges="0x40002FFF"
                    android:hardwareAccelerated="false">
                    <intent-filter>
                        <action
                            android:name="android.intent.action.MAIN">
                        </action>
                        <category
                            android:name="android.intent.category.LAUNCHER">
                        </category>
                        <category
                            android:name="android.intent.category.LEANBACK_LAUNCHER">
                        </category>
                    </intent-filter>
                    <meta-data
                        android:name="unityplayer.UnityActivity"
                        android:value="true">
                    </meta-data>
                    <meta-data
                        android:name="unityplayer.ForwardNativeEventsToDalvik"
                        android:value="false">
                    </meta-data>
                </activity>
                <meta-data
                    android:name="unity.build-id"
                    android:value="93406909-c0b3-40de-b7c5-75971379027e">
                </meta-data>
                <meta-data
                    android:name="unity.splash-mode"
                    android:value="0">
                </meta-data>
                <meta-data
                    android:name="unity.splash-enable"
                    android:value="true">
                </meta-data>
                <activity
                    android:name="com.unity.udp.udpsandbox.LoginActivity">
                </activity>
                <activity
                    android:theme="@16973841"
                    android:name="com.unity.udp.udpsandbox.UDPPurchasing$LoginHelperActivity"
                    android:configChanges="0x40000FFF">
                </activity>
                <meta-data
                    android:name="CHANNEL_NAME"
                    android:value="UDP">
                </meta-data>
            </application>
    </manifest>
    
    

    做下说明:

    其中 android:theme="@2131165186"或者 android:label="@2131099648" 这个数字是不是跟平时写的不一样? 这是解析时候做了处理 ,展示的是10进制(android:theme="@2131165186),转换成16进制 就是0x7f070002。这个在1.3.1章节中说过,引用的是应用程序资源,再往下说就涉及到.arsc文件了,解释到此为止下次可能会出对应文章针对arsc文件做解读。

    android:screenOrientation="6" 这个值是sensorLandscape,这个sensorLandscape在系统定义的 int值 数字6 。在打包时候会有对应映射关系 ,用apktool解包后自然展示成 android:screenOrientation="sensorLandscape"

    技术总结

    在逆向应用中,用工具apktool 反编译应用,尤其是一些大厂的应用,经常出现一些异常信息,不让我们轻松的反编译。因为apktool本就是开源的,所以一些大企业会针对这个开源项目去找它的漏洞,通过一些漏洞对自己的apk进行处理,增加反编译的难度。我觉得非常有必要了解apktool原理和解析源码,这篇文章就是针对apktool中清单文件的解读。只要你了解了这个原理,对逆向和反逆向有很好的帮助。

    声明

    本文中涉及的代码均改自秉心说,代码封装的很好,我的代码就不发出来了,有需要去下载大佬的代码--点击即可进入

    文章参考

    秉心说文章链接: AndroidManifest.xml 文件格式解析

    姜维:安卓应用安全防护和逆向分析

    结语

    记录下自己的学习和工作经验,分享给有需要的人。如果有那里写的不对,说的不理解,欢迎大家的指正。

    相关文章

      网友评论

          本文标题:Android逆向三部曲之AndroidManifest.xml

          本文链接:https://www.haomeiwen.com/subject/vaofjktx.html