多读书多实践,勤思考善领悟

Java逆向基础之二十一.AspectJ

本文于2125天之前发表,文中内容可能已经过时。

AspectJ是一个面向切面编程的框架,它扩展了Java语言。AspectJ定义了AOP语法所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。

AspectJ目前支持以下三种编织的方式

  1. 编译时编织:把aspect类(aop的切面)和目标类(被aop的类)放在一起用ajc编译。

  2. 后编译时编织:目标类可能已经被打成了一个jar包,这时候也可以用ajc命令将jar再编织一次

  3. 加载时编织Load-time weaving (LTW):在jvm加载类的时候,做字节码修改或替换

逆向工程中可以用到后两个:后编译时编织和加载时编织。

1. 安装AspectJ

下载地址:http://www.eclipse.org/aspectj/downloads.php

选择里面的1.9版本下载,下载完成之后是个jar包

安装命令

1
java -jar aspectj-1.9.1.jar

根据提示设置好java主目录和aspectj的安装目录,这里采用默认的C:\aspectj1.9

安装完成后,设置如下环境变量

设置ASPECTJ_HOME

1
ASPECTJ_HOME = C:\aspectj1.9

添加/修改CLASSPATH,把aspectjrt.jar添加进去

1
CLASSPATH = .;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\lib\dt.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar

PATH变量新增值

1
%ASPECTJ_HOME%\bin

环境变量设置完成之后,验证命令

1
ajc -version

出现如下结果说明环境变量配置成功

QQ截图20180428182104.png

2. 看两个简单的例子

1. 第一个项目helloworld

Java文件HelloWorld.java

1
2
3
4
5
6
7
8
9
10
11
//HelloWorld.java
public class HelloWorld {
public void sayHello() {
System.out.println("Hello, world!");
}

public static void main(String[] argv) {
HelloWorld hw = new HelloWorld();
hw.sayHello();
}
}

Aspectj文件MyAspect.aj

1
2
3
4
5
6
7
8
9
10
11
12
//MyAspect.aj
public aspect MyAspect {
pointcut say():call(void HelloWorld.sayHello());

before():say() {
System.out.println("before say hello....");
}

after():say() {
System.out.println("after say hello....");
}
}

Load-time weaving (LTW) 加载时编织命令

编译

1
2
ajc -outjar myjar.jar HelloWorld.java
ajc -outjar MyAspect.jar -outxml MyAspect.aj -classpath "myjar.jar;%CLASSPATH%"

运行

1
aj5 -classpath "MyAspect.jar;myjar.jar;%CLASSPATH%" HelloWorld

运行结果

QQ截图20180428182845.png

可以看到在helloworld字符串上有上下两行文字

2. 第二个项目,输出参数和返回值

java文件Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Main.java
package com.vvvtimes;

public class Main {

public int add(int x, int y) {
return x + y;
}

public int add(int x, int y, int z) {
return x + y + z;
}

public static void main(String[] args) {
Main m = new Main();
System.out.println(m.add(1, 2));
System.out.println(m.add(1, 2, 3));
}
}

Aspectj文件Tracing.aj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//Tracing.aj
public aspect Tracing {
private pointcut mainMethod():
execution(public static void main(String[]));

before(): mainMethod() {
System.out.println("> " + thisJoinPoint);
}

after(): mainMethod() {
System.out.println("< " + thisJoinPoint);
}

private pointcut addMethod():
execution(public int add(..));

before(): addMethod() {
System.out.println("> " + thisJoinPoint);
Object[] args = thisJoinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "]: " + thisJoinPoint.getArgs()[i].toString());
}

}

after(): addMethod() {
System.out.println("< " + thisJoinPoint);
}

after() returning(Object o) :addMethod(){
System.out.println("Return value: " + o.toString());
}
}

编译

1
2
ajc -outjar myjar.jar com/vvvtimes/Main.java
ajc -outjar Tracing.jar -outxml Tracing.aj -classpath "myjar.jar;%CLASSPATH%"

运行

1
aj5 -classpath "Tracing.jar;myjar.jar;%CLASSPATH%" com.vvvtimes.Main

运行结果

QQ截图20180428183817.png

可以看到,add方法的参数和返回值都打印出来了

顺便说一下三个编织期的区别,从命令行角度比较如下:

  1. Compile-time weaving 编译时编织
1
ajc -outjar mytarget.jar HelloWorld.java MyAspect.aj

运行

1
aj5 -classpath "mytarget.jar;%CLASSPATH%" HelloWorld
  1. Post-compile weaving 后编译时编织
1
2
ajc -outjar myjar.jar HelloWorld.java
ajc -inpath myjar.jar MyAspect.aj -outjar mytarget.jar

运行

1
aj5 -classpath "mytarget.jar;%CLASSPATH%" HelloWorld
  1. Load-time weaving (LTW) 加载时编织

编译

1
2
ajc -outjar myjar.jar HelloWorld.java
ajc -outjar MyAspect.jar -outxml MyAspect.aj -classpath "myjar.jar;%CLASSPATH%"

运行

1
aj5 -classpath "MyAspect.jar;myjar.jar;%CLASSPATH%" HelloWorld

3. ajc与aj5命令

acj命令是用于编译java文件和aj文件的编译器,相当于eclipse的编译器(ECJ)+aspectj运行时扩展

aj5命令在jdk1.5上使用-javaagent:pathto/aspectjweaver.jar加载aspectj程序,达到修改字节码的目的

这两个命令可以用java命令和javac来替代,我们以上文的命令为例

1. Load-time weaving (LTW) 加载时编织

编译

1
2
ajc -outjar myjar.jar HelloWorld.java
ajc -outjar MyAspect.jar -outxml MyAspect.aj -classpath "myjar.jar;%CLASSPATH%"

运行

1
aj5 -classpath "MyAspect.jar;myjar.jar;%CLASSPATH%" HelloWorld

命令翻译

若已经在classpath环境变量里设置过aspectjrt.jar,可在 -classpath里省略%ASPECTJ_HOME%\lib\aspectjrt.jar;下同

编译

1
2
3
javac HelloWorld.java
jar cvf myjar.jar HelloWorld.class
java -classpath "myjar.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar;%ASPECTJ_HOME%\lib\aspectjtools.jar;%CLASSPATH%" org.aspectj.tools.ajc.Main -outxml MyAspect.aj -outjar MyAspect.jar

运行

1
java -javaagent:%ASPECTJ_HOME%\lib\aspectjweaver.jar -classpath "MyAspect.jar;myjar.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar;%CLASSPATH%" HelloWorld

2. Compile-time weaving 编译时编织

编译

1
ajc -outjar mytarget.jar HelloWorld.java MyAspect.aj

运行

1
aj5 -classpath "mytarget.jar;%CLASSPATH%" HelloWorld

命令翻译

编译

1
java -classpath "myjar.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar;%ASPECTJ_HOME%\lib\aspectjtools.jar;%CLASSPATH%" org.aspectj.tools.ajc.Main -outjar mytarget.jar HelloWorld.java MyAspect.aj

运行

1
java -classpath "mytarget.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar;%CLASSPATH%" HelloWorld

3. Post-compile weaving 后编译时编织

编译

1
2
ajc -outjar myjar.jar HelloWorld.java
ajc -inpath myjar.jar MyAspect.aj -outjar mytarget.jar

运行

1
aj5 -classpath "mytarget.jar;%CLASSPATH%" HelloWorld

命令翻译

编译

1
2
3
javac HelloWorld.java
jar cvf myjar.jar HelloWorld.class
java -classpath "myjar.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar;%ASPECTJ_HOME%\lib\aspectjtools.jar;%CLASSPATH%" org.aspectj.tools.ajc.Main -inpath myjar.jar MyAspect.aj -outjar mytarget.jar

运行

1
java -classpath "mytarget.jar;%ASPECTJ_HOME%\lib\aspectjrt.jar;%CLASSPATH%" HelloWorld

翻译成java命令的好处是,可以在Load-time weaving (LTW) 加载时编织直接指定多个-javaagent

1
java -javaagent:aspectjweaver.jar -javaagent:ZKMAgent.jar -cp Tracing.jar;ZKM.jar com.zelix.ZKM

4. Eclipse插件AJDT

AJDT即Eclipse AspectJ Development Tools.是一个Eclipse插件,可以编写AspectJ项目

安装

help–>Install New Software

填写在线安装地址:http://download.eclipse.org/tools/ajdt/47_aj9/dev/update

安装完成之后,新建Aspectj项目

QQ截图20180429214612.png

项目结构如下

QQ截图20180429214703.png

把前面博文的aj文件和java文件内容复制过来

其中aj文件的新建方法

File–>New–>Project选择AspectJ Project

QQ截图20180429214855.png

运行的时候,选中java文件,右键Run As–>AspectJ/Java Application

image.png

运行结果

QQ截图20180429215113.png

下面看运行时加载项目的配置

新建项目结构如图

QQ截图20180429215259.png

其中aj文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
//Tracing.aj
public aspect Tracing {
private pointcut mainMethod():
execution(public static void main(String[]));

before(): mainMethod() {
System.out.println("> " + thisJoinPoint);
}

after(): mainMethod() {
System.out.println("< " + thisJoinPoint);
}
}

Run–>Run configurations

运行项目类型选:Aspect Load-Time Weaving Application(默认的是下面的那个Aspect/Java Application 注意别选错了)

Project:AspectJDemo3

Main class:com.zelix.ZKM

LTW Aspect path:添加j外部jar选择ZKM.jar

QQ截图20180429215513.png

QQ截图20180429215701.png

最后点击配置里的Run命令,运行结果如下

QQ截图20180429215841.png

这里面的导出jar没啥用,都是导出编译期的,不能导出加载期的,也许是不会用。。。

5. 获取成员变量的值

注意:由于JVM优化的原因,方法里面的局部变量是不能通过AspectJ拦截并获取其中的值的,但是成员变量可以

在逆向中,我们经常要跟踪某些类的成员变量的值,这里以获取ZKM9中的qs类的成员变量g为例进行说明

在StackOverFlow上有这么一篇提问:AspectJ: How to get accessed field’s value in a get() pointcut

将其中内容改写为qs类的代码如下:

1
2
3
4
5
6
7
8
private pointcut qsfiledMethod() :
get(* com.zelix.qs.*);

after() returning(Object field) :qsfiledMethod(){
System.out.println(thisJoinPoint.toLongString());
System.out.println(" " + thisJoinPoint.getSignature().getName());
System.out.println(" " + field);
}

但是这个方法有缺陷,只能获取公共变量,运行之后获取到的都是qs的成员变量j和k

运行结果如下

QQ截图20180510163734.png

所以此路不通,那么就需要再找一条路:反射

qs类中的某个方法调用了jj.a方法,所以用call找出调用者,然后通过反射方式获取filed,talk is cheap,show you code?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private pointcut jjaMethod() :
call(String com.zelix.jj.a(String, String, String, Object, int));

before() : jjaMethod() {
System.out.println("> " + thisJoinPoint);
if (thisJoinPoint.getThis() != null) {
System.out.println("this "+thisJoinPoint.getThis().getClass().getName() + " " + thisJoinPoint.getSourceLocation());
Object obj = thisJoinPoint.getThis();
Class clazz = obj.getClass();

//遍历成员
Field[] fileds = clazz.getDeclaredFields();
for (Field field : fileds) {
System.out.println(field);
}
try {
//获取单个成员private final java.lang.String[] com.zelix.qs.g
//并输出它的值
Field filed = clazz.getDeclaredField("g");
System.out.println(filed);
filed.setAccessible(true);
String[] g= (String[]) filed.get(obj);
for (int i = 0; i < g.length; i++) {
System.out.println("g["+i+"] ="+g[i]);
}
} catch (Exception e) {
e.printStackTrace();
}
}else if (thisJoinPoint.getTarget() != null) {
System.out.println("target "+thisJoinPoint.getTarget().getClass().getName() + " " + thisJoinPoint.getSourceLocation());
}
}

运行结果如下

QQ截图20180510164305.png

before方法里的功能如下

  1. 打印出调用者的名称和位置

  2. 遍历打印qs类的所有成员名称

  3. 获取成员g的值,由于这个成员是数组类型,遍历这个数组打印值

6. Around方法修改方法体

在逆向中,我们往往通过修改某个方法达到目的,在javaassist中有insertBefore,insertAfter,setBody,在AspectJ中也可以通过Around实现类似的功能。

看一个简单的例子

java文件Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Main.java
package com.vvvtimes;

public class Main {

public int add(int x, int y) {
return x + y;
}

public int add(int x, int y, int z) {
return x + y + z;
}

public static void main(String[] args) {
Main m = new Main();
System.out.println(m.add(1, 2));
System.out.println(m.add(1, 2, 3));
}
}

aj文件Tracing.aj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//Tracing.aj
public aspect Tracing {
private pointcut mainMethod():
execution(public static void main(String[]));

before(): mainMethod() {
System.out.println("> " + thisJoinPoint);
}

after(): mainMethod() {
System.out.println("< " + thisJoinPoint);
}

pointcut addMethodOne() : call (public int add(int,int));

int around() : addMethodOne() {
System.out.println("> " + thisJoinPoint);
Object[] args = thisJoinPoint.getArgs();
for (int i = 0; i < args.length; i++) { //输出参数
if( args[i]!=null) {
System.out.println("args[" + i + "]: " + args[i].toString());
}
}
int result = proceed();//这里执行原有方法体
System.out.println("original return value: " + result);//输出原有返回值
return 777; //指定新值返回
}



pointcut addMethodTwo(int a, int b, int c) : call (public int add(int,int,int)) && args (a, b, c);

int around(int a, int b, int c) : addMethodTwo (a, b, c){
System.out.println("> " + thisJoinPoint);
System.out.println("1st passed value: " + a);
System.out.println("2nd passed value: " + b);
System.out.println("3rd passed value: " + c);
a = 6;
b = 6;
c = 6;
int result = proceed(a, b, c);//修改传入的参数值
return result;
}

}

运行后的结果如下

QQ截图20180512153730.png

proceed用于执行原有方法体,return方法用于改变返回值

  • 在第一个修改中我们输出了其中的参数,执行了原有方法之后,直接指定了一个返回值777

  • 在第二个修改中,我们通过&& args 指定了参数,修改其参数之后执行原有方法体并返回。