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

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

一、问题发现

先看这段用JEB翻译出来的代码,一个执行shell 命令的Service ,常见的可攻击的demo。Thread 的构造过程使用了Intent arg3 作为参数,但它肯定是没有这个构造方法的。而且下面的 val$it 肯定是没有被初始化过的,但却被当作一个Intent 类进行处理,显然 val$it 就是 Intent 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,源码如下:

1
2
3
4
5
6
7
8
Log.d("TAG2", "" + "<<<" + variableInt + ">>>");
Log.d("TAG3", "" + "<<<" + variableObj + ">>>");
int aaa = variableInt;
aaa *= 999;
aaa -= 100;
int[] bbb = new int[10];
bbb[0] = variableInt;
Log.d("AAA", "" + aaa);

优化后是:

1
2
3
4
Log.d("TAG2", "<<<10>>>");
Log.d("TAG3", "<<<" + this.val$variableObj + ">>>");
new int[10][0] = 10;
Log.d("AAA", "" + 9890);

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

2、InnerService

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

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

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

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

三、结论

对于匿名类

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

对于内部类

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

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