本文已同步到个人博客 怎么经过 Mapping 文件反混杂


写在前边

做过 Android 开发的应该或多或少都知道“混杂”这个技术点,它不仅能够协助咱们添加三方逆向的难度,还能够有用削减包体积,减肥 APK。

其实这些才能都来自于 Proguard 这个程序,Proguard 能利用字典文件,在编译时将咱们的类名,办法名,字段名都替换掉,最终生成一份十分反人类的编译产物。Proguard 在每次运转时都会创立一个 mapping.txt 文件,其间列出了经过混杂处理的类、办法和字段称号与原始称号的映射联系。此映射文件还包含用于将行号映射回原始源文件行号的信息。

这篇文章的意图便是要解析生成的 Mapping 文件。

正文

Mapping 文件的来历与用处

以下便是 Mapping 文件的生成进程

如何通过 Mapping 文件反混淆

这个 Mapping 文件是由 Proguard 程序主动生成的,会存放在 output 目录下,与 release 包放在一同。需求谨记的是,Mapping 文件或许在每次 Proguard 运转后都会不同,所以发布给用户的包必定要留存好 Mapping 文件,便利后续盯梢解决问题

了解 Mapping 文件的优点

了解 Mapping 文件最直观的优点在于咱们盯梢线上的经过混杂之后的 Crash 信息时,能够从 Mapping 文件逆向推出原始的仓库信息,更快更便利的定位问题,但不只这些,咱们还能够经过 Mapping 文件处理内存快照文件 Hprof 的反混杂,处理 Systrace 的文件的反混杂,还有 Nanoscope 文件的反混杂等

怎么解析 Mapping 文件

注意:Android 在新版中启用了 R8 编译器,没有运用 Proguard 东西,尽管兼容 Proguard 的装备和字典等,可是编译出来的 Mapping 文件格局仍是有一点不同。咱们会在最终一个末节讲一下其间的不同

下面咱们详细来看 Mapping 文件的格局

classline
    fieldline *
    methodline *

Mapping 文件的正式部分由多个 Class 块组成,每个 Class 块中包含混杂前后的类信息,字段信息,办法信息。每个 Class 块由顶格的类信息最初,后边跟着最初带有4个空格的字段信息与办法信息 每个 Class 块中详细格局如下:

类信息:

original class name -> obfuscated class name:

混杂之前的全限制类名与混杂后的全限制类名经过 -> 分隔符切割,以 : 标识当前类信息的结束,标识类内字段,办法信息的开端

补白:全限制类名,是指带有包名限制的类名,能够完全定位一个类

字段信息:

original field type original field name -> obfuscated field name

混杂之前的字段信息与混杂之后的字段信息相同经过 -> 分隔符切割,值得注意的是,混杂前的字段包含了字段类型和字段称号,而混杂之后只要字段称号

办法信息:

[startline:endline:]original return type
[original classname.]original method name(original argument type,...)
[:original startline[:original endline]] -> obfuscated method name

补白:标识着 * 的行,意味着或许呈现恣意多次; [] 表明内容是可选的; 表明或许会呈现恣意多个前边指定的item; :->都是分隔符

办法信息相同经过 -> 分隔符切割,可是办法信息比类信息和字段信息更杂乱一点,由于办法还额定包含了行号表,参数,回来类型等信息

  • original return type:原始回来类型,全限制类名,或许基本类型,或许无类型 void
  • original method name:原始办法称号
  • original class name:可选,当办法不属于所在的类块时,需求特别经过全限制类名引证
  • original argument type:原始参数类型,全限制类名或许基本类型,多个参数按顺序经过 , 切割
  • obfuscat edmethod name:混杂后的称号

剩下的行号信息,略微杂乱一点要根据办法有没有做内联优化分成两种状况:

无内联优化:

  • [startline:endline:]:和 Jvm 字节码中的 LineNumberTable 对应,表明原始代码的行号规模
  • [:original startline[:original endline]]:无内联优化,这个字段不存在

有内联优化

  • [startline:endline:]:是一个编译器给出的一个类似于源码中的行号,为什么说类似于源码行号,咱们后边经过示例来阐明
  • [:original startline[:original endline]]:这个字段中 originalendline 又是可选的,所以也分两种状况
    • [:original startline]:只要起始行号,表明这是反常的某个中间调用(???大问号?后边经过示例来阐明吧,这里不好了解)
    • [:original startline:original endline]:有行号规模,表明在源码中的实在行号规模,对应源码的办法规模

有一些要注意的点:

  1. 办法的行号仅有标识了这个办法,这个在咱们从仓库中反推原始信息时特别有用
  2. 假如办法的行号没有存在,那么咱们只能经过办法的签名(或许描述符)来反混杂代码,可是这种匹配不是绝对精确的,或许会呈现匹配到多个相同的办法或字段

Mapping 示例剖析

简略示例

com example.application.ArgumentWordReader -> com.example.a.a:
    java.lang.String[] arguments -> a
    int index -> a
    36:57:void <init>(java.lang.String[],java.io.File) -> <init>
    64:64:java.lang.String nextLine() -> a
    72:72:java.lang.String lineLocationDescription() -> b

com example.application.ArgumentWordReader 被混杂为 com.example.a.a ,其间字符 a 来历于混杂文件中所装备的字典文件 字段 arguments 和 index 会在混杂中丢掉类型信息,一起转换为混杂字符 a

假如多个办法或许字段的签名(或许说描述符)不同,那么混杂之后的称号或许是相同的

办法的实例构造函数 <init> 和静态类构造函数 <clinit> ,称号不会被混杂,只会丢掉其参数列表和回来类型

办法 nextLine 和 lineLocationDescription 都有自己的源码行号规模,可是回来类型和参数列表是相同的,假如在混杂的装备文件中装备保存了 LineNumberTable,那么在报错仓库中就能够看到行号,也就能够经过行号定位到详细的办法,而假如没有在混杂的装备文件中装备保存 LineNumberTable,那么报错仓库中也就不会打印出行号,仅仅经过回来类型和参数列表是无法区分二者的,所以这便是为什么这两个办法的混杂之后的称号是不同的

以上的示例比较简略,咱们来看一下杂乱的示例

杂乱示例

com.example.application.Main -> com.example.application.Main:
    com.example.application.Configuration configuration -> a
    50:66:void <init>(com.example.application.Configuration) -> <init>
    74:228:void execute() -> a
    2039:2056:void com.example.application.GPL.check():39:56 -> a
    2039:2056:void execute():76 -> a
    2236:2252:void printConfiguration():236:252 -> a
    2236:2252:void execute():80 -> a
    3040:3042:java.io.PrintWriter com.example.application.util.PrintWriterUtil.createPrintWriterOut(java.io.File):40:42 -> a
    3040:3042:void printConfiguration():243 -> a
    3040:3042:void execute():80 -> a
    3260:3268:void readInput():260:268 -> a
    3260:3268:void execute():97 -> a

com.example.application.Main 类装备了 keep 特点,所以类信息没有被混杂掉,一般咱们会把或许需求被反射运用的类保存,防止在 release 包中类名改变导致混杂运用犯错

configuration 字段信息同上,混杂后丢掉类型 实例构造函数 <init> 和办法 execute 同上解析办法

剩下的办法都比较古怪,最初的行号都是特别大的数字,且有几个办法行号是相同的,明显不是正常的行号, 这是由于经过了办法内联处理,在混杂处理的进程中,或许会内联办法到其他办法中,乃至进行递归的内联

办法内联

简略来说,便是将相互调用的多个办法合并为一个办法,这样削减程序办法调用的次数,从而削减程序调用进程中的栈帧的创立毁掉等额定的消耗,提升功能 例如

class A:
    def a():
        print("a")
        B.b()
class B:
    def b():
        print("from B")
        print("b")

做办法内联优化之后:

class A:
    def a():
        print("a")
        print("from B")
        print("b") // inner line from B()

了解了办法内联之后,咱们再来看办法内联对混杂的影响,办法内联之后,仓库中本来 B.b() 办法已经被内联到 A.a() 办法中,混杂之后的办法信息也天然指向了 A.a(),那么仓库中呈现的错误信息也是指向 A.a(),可是咱们源码中的调用是来自办法 B.b() 的,所以内联前后的优化信息咱们是需求知道的,便利在后续仓库信息追踪时反推源码信息。

下边咱们就看一下详细的解析办法

2039:2056:void com.example.application.GPL.check():39:56 -> a
2039:2056:void execute():76 -> a

办法最前边的行号规模假如相同,就代表一个内联链中的办法调用链,比如以上两句表明,办法 check 被内联到了 execute 办法中,内联的方位是原 execute 办法的第76行,假如结尾是行号规模,那么对应的便是最终的内联办法体

最初的行号是内联函数调用链最底层的行号规模和编译器给予的必定的偏移量加和的成果,偏移量是 1000的倍数,偏移量的意图是防止与其他的正常的代码规模产生冲突,所以2039:2056是来自 check 办法的源码行号规模 39:56 与 2000 的偏移量相加得出的成果

另外,由于 check 办法由于不属于类 com.example.application.Main,所以运用了类全限制符标识,标明 check 所在的类

2236:2252:void printConfiguration():236:252 -> a
2236:2252:void execute():80 -> a
3040:3042:java.io.PrintWriter com.example.application.util.PrintWriterUtil.createPrintWriterOut(java.io.File):40:42 -> a
3040:3042:void printConfiguration():243 -> a
3040:3042:void execute():80 -> a

以上 Mapping 文件的剖析办法和之前的共同,仅有需求阐明的是这其间的关联 execute 办法在80行内联了办法 printConfiguration,后者的行号规模是 236:252,其间,printConfiguration 又在 243 行内联了办法 createPrintWriterOut,后者的行号规模是 40:42。

至此,咱们剖析完了 Mapping 文件的所有状况的格局,最终的两行交由读者自己测验剖析一下。

R8 编译器

当运用 Android Gradle 插件 3.4.0 或更高版本构建项目时,该插件不再运用 ProGuard 来执行编译时代码优化,而是与 R8 编译器协同工作来处理编译时任务,所以能够经过 Gradle 插件版本来查看详细运用了 Proguard 仍是 R8 编译器。

R8 编译器必定程度上兼容 Proguard 规矩,可是仍是略有不同。

概况能够参看官网:developer.android.com/studio/buil…

注释

Mapping 文件以 # 最初的行作为注释,标识 R8 程序的格局,日期等信息,可是在 Proguard 中还未发现这样的规范

例如以下:

# compiler: R8
# compiler_version: 1.6.82
# min_api: 19
# pg_map_id: 6af58cc
# common_typos_disable

与 Proguard 区别

R8 中的行号的表明办法和 Proguard 还不太一样,以下的解析办法是根据 Proguard 新版的规范和源码相印证的成果,在 R8 的官方文档中,是直接导向 Proguard 官网的,并没有自己的格局的阐明(这点在 Hprof 格局也是),所以有谁找到对应的官方文档,能够帮助附到评论中,感谢。

androidx.appcompat.app.AppCompatActivity -> androidx.appcompat.app.AppCompatActivity:
    1:1:void <init>():77:77 -> <init>
    2:2:void <init>(int):92:92 -> <init>
    1:1:void addContentView(android.view.View,android.view.ViewGroup$LayoutParams):176:176 -> addContentView
    1:2:void attachBaseContext(android.content.Context):97:98 -> attachBaseContext
    1:4:void closeOptionsMenu():609:612 -> closeOptionsMenu
    1:2:boolean dispatchKeyEvent(android.view.KeyEvent):552:553 -> dispatchKeyEvent
    3:3:boolean dispatchKeyEvent(android.view.KeyEvent):555:555 -> dispatchKeyEvent
    4:4:boolean dispatchKeyEvent(android.view.KeyEvent):558:558 -> dispatchKeyEvent
    1:1:android.view.View findViewById(int):214:214 -> findViewById

这是一个 R8 编译完结的 Mapping 文件示例,由于都运用了 keep 特点,所以没有被混杂之后称号没有被字典中的字符替换掉,但这点关于剖析 Mapping 格局没有什么影响。

和 Proguard 规范有所不同的是:

  1. 许多办法并没有用一个连续的行号规模标识,而是被拆分成了不同的子块,每个子块都有自身对应的行号规模,办法前边的是虚拟行号,对应后边的实在行号规模

例如:

1:1:void <init>():77:77 -> <init>
2:2:void <init>(int):92:92 -> <init>
......
1:2:boolean dispatchKeyEvent(android.view.KeyEvent):552:553 -> dispatchKeyEvent
3:3:boolean dispatchKeyEvent(android.view.KeyEvent):555:555 -> dispatchKeyEvent
4:4:boolean dispatchKeyEvent(android.view.KeyEvent):558:558 -> dispatchKeyEvent
  1. 各个办法的虚拟行号的规模是有所重叠的,可是所对应的混杂之后的称号是不同的,所以在区分不同办法上来说是没有歧义的

例如:

1:2:void attachBaseContext(android.content.Context):97:98 -> attachBaseContext
1:4:void closeOptionsMenu():609:612 -> closeOptionsMenu

虚拟行号规模重叠了,但实践的行号规模是不一样的,而且混杂后的称号也是不同的

  1. R8 编译出的文件中并未找到内联办法相关的编译优化,不确定是没有开对应的优化项仍是说根本就没有这项优化,所以不会呈现 Proguard 之前的内联相关的调用栈的 Mapping 信息

跋文

根据以上的 Mapping 文件的解析规矩,咱们能够做很多事情,比如反混杂 Trace 文件,反混杂 Nanoscope 文件,反混杂 Hprof 文件等等,我根据这个规矩,开发了一个 ReProguard 的程序,能够供我们参阅,欢迎沟通提意见

项目地址:github.com/0xforee/ReP…

假如后续有时间,我会根据搜集的材料写一个 Hprof 文件格局的解析教程,欢迎评论沟通。^_^