Java内部类与匿名类反汇编的小知识

今天看到一个testcase,很常见很普通的用法,JEB翻译出来的是错的,于是想通过阅读字节码来手动写一段Java模拟一下,却发现怎么都写不出来。所以本文讲以下Java在编译过程中对匿名类和内部类的处理,应该属于小tips。

一、问题发现

先看这段用JEB翻译出来的代码,一个执行 shell 命令的 Service ,常见的可攻击的demo。 Thread 的构造过程使用了 Intent arg3 作为参数,但它肯定是没有这个构造方法的。而且下面的 val$it 肯定是没有被初始化过的,但却被当作一个 Intent 类进行处理,显然 val$it 就是 arg3 。以前看的时候大脑完成了自动替换的功能,今天突然想探究一下这玩意到底是咋回事。

 

先看一眼smali, $1 这样的肯定是匿名类,继承 Thread ,只有一个构造方法,2个参数分别是 Service 本身和 Intent ,将来在初始化过程中传 service 和 intent 对象,逻辑上没有任何问题。

这时候突然有一个想法,既然是匿名类,开发者肯定没有主动去写构造方法来传参,我不信开发者故意命名一个叫 $1 的类出来,累不累啊。那可能就是编译器干的了,这时候掏出了jadx,出现了下方的反汇编代码。

注意这里有个 final ,这便是问题所在了!因为在内部里本来是无法访问这个变量的,必须要加 final,这时写样例测试一下。

二、验证假设

写两类样例,分别是使用内部类,使用匿名类,均去尝试访问主类的局部变量、成员变量,观察一下行为有没有变化。

1、FinalService

源码的角度看,必须要将 intent, variableInt, variableObj 标记为 final,不然 Thread 里面是访问不到它们的。

标记后,发现有三个参数,是 this, Intent, Integer 类,显然缺少一个数字 10 没有被传入,主类的对象、2个 final对象都作为参数传递了进去。

翻译出来的java代码里, Thread 构造方法错误翻译出了2个参数。由于优化的存在,数字 10 并没有被作为参数传到 TAG2 的地方,而是直接替换了它出现的所有位置并且和附近的 const-String 进行了合并。因为 final修饰的对象是不可以被第二次定义的,对于int这种基本数据类型,这么做肯定是没有问题的,对于非基本数据类型的对象,就会偷偷在构造方法里添加并且传递,在开发者这里是看不到的。

为了证明优化的存在,这里又写了一个testcase,源码如下:

优化后是:

普通的int被优化掉了,而Integer没有被优化掉。

2、InnerService

源码方面,我们只能访问到已经被传递进去的参数,和主类里的变量,访问不到方法里的局部变量,这已经不是 final的问题了,本来就不该被访问到。

smali代码里看到,内部类有三个参数,但不对啊,我们在源码里定义只有2个参数,这里自动补了一个 this 进去,于是有2个 InnerService ,前者是编译器加的,后者是我们手写进去的,也是符合逻辑的。

JEB翻译出来的代码基本是正确的,参数格式、代码结构都没有问题,注意 Inner 的构造方法,我们代码里写的是第一句调用 super 方法,但被编译器偷偷在开头插入一条先对内部类的this进行赋值。

smali代码也验证了这一点,会偷偷加一个 this$0 的属性进去。

三、结论

对于匿名类

  • 构造方法第一个参数一定是this
  • 如果有用到final修饰的基本数据类型,编译时做推断并替换掉它的数值
  • 如果有用到final修饰的对象,在构造方法里加若干个参数进去,创建对象时传递进去

对于内部类

  • 构造方法第一个参数一定是this,其余参数均为用户指定
  • 构造方法的最开始会被插入一条对this的赋值,之后再执行用户写的构造方法

为什么都要传一个this进去呢?

这个也是很合理的,当然是为了访问主类里的各种变量和方法啦,如果不传递this进去,内部类和外部类还有什么区别呢。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code