1:案例分析
1.1 Tomcat:正统的类加载器架构
一个功能鉴权的Web服务器,需要解决的问题:
- 部署在同一服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。
- 服务器需要尽可能的保证自身的安全不受部署的Web应用程序影响。
- 支持Jsp应用的Web服务器,大多数都要支持HotSwap功能。
对于Tomcat5.X的版本,存在common、server、shared、和应用的WEB-INF下目录四个目录,四种不同的类加载器。Tomcat6.X版本common、server、shared三个目录合并为lib目录,这是一项改进。
1.2 OSGI:灵活的类加载器架构
OSGI中每个模块(Bundle)与普通的Java类库区别并不是很大,两者都已jar的形式封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以说明它所依赖的Java Package(通过Import-Package描述),也可以申明它允许倒出发布的Java Package(通过Export-Package描述)。在OSGI里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变成平层模块之间的依赖(至少外观上如此),而且类库的可见性能得到非常精确的控制,一个模块里只有被Export过的Package才可能由外界访问,其他的Package和Class都会被隐藏起来。OSGI还可以实现模块的热插拔的功能。
OSGI之所以能有上述“诱人”的特点,归功于他灵活的类加载器架构。OSGI的Bundle类加载器之间只有规则,没有固定的委派关系。在OSGI中,加载器之间的关系不再是双亲委派模型的树形结构,而是进一步发展成了一种更为复杂的、运行时才能确定的网状结构。
1.3 字节码生成技术与动态代理的实现
java.lang.reflect.Proxy或java.lang.reflect.InvocationHandle接口可以用来实现动态代理。典型的Spring,如果Bean是面向接口编程的,那么Spring则是通过动态代理来实现对Bean进行增强的。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 动态代理的实现
*/
public class DynamicProxyTest {
interface IHello{
void sayHello();
}
static class Hello implements IHello{
@Override
public void sayHello() {
System.out.println("Hello World");
}
}
static class DynamicProxy implements InvocationHandler{
Object originalObj;
Object bind(Object originalObj){
this.originalObj = originalObj;
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),
originalObj.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("welcome");
return method.invoke(originalObj,args);
}
}
public static void main(String[] args) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
上面的代码是最简单的实现动态代理的方法。
1.4 Retrotranslator:跨越JDK版本
为了跨JDK版本运行程序,Java逆向移植工具(Java Backporting Tools)应运而生,Retrotanslator是其中较为出色的。
2:实战:自己动手实现远程执行功能
2.1:思路
- 如何编译提交到服务器的Java代码:一种方法时使用tools.jar包中的com.sun.tools.javac.Main类来编译Java文件,和直接使用javac编译是一样的。另一种思路是直接在客户端编译好,把字节码而不是Java代码 传到服务端,
- 如何执行编译以后的Java代码:一段程序往往不是编写、运行一次就能达到效果,同一个类可能要反复地修改、提交、执行。另外提交上去的类要能访问服务端的其他类库才可以。既然提交的代码是零时的,那提交的Java类在执行完后就应当能卸载和回收。
- 如何收集Java代码的执行结果:直接在执行的类中,将System.out的符号引用替换为我们准备的PrintStream的符号引用。
2.2:实现
- 下面这段代码用来实现“同一个类的代码可以多次加载的需求”
/**
- 为了多次载入执行类而加入的加载器
- 把defineClass方法开放出来,只有外部调用的时候才会使用到loadByte方法
- 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行加载类
*/
public class HotSwapClassLoader extends ClassLoader{
public HotSwapClassLoader(){
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classByte){
return defineClass(null,classByte,0,classByte.length);
}
}
- 下面的类实现将java.lang.System替换为我们自己的HackSystem类的过程,它直接修改符合Class文件格式的byte[]数组中的常量池部分,将常量池中指定的内容替换为新的内容。
/**
* 修改Class文件,暂时只提供修改常量池常量的功能
* @author zzm
*/
public class ClassModifier {
/**
* Class文件中常量池的起始偏移
*/
private static final int CONSTANT_POOL_COUNT_INDEX = 8;
/**
* CONSTANT_Utf8_info常量的tag标志
*/
private static final int CONSTANT_Utf8_info = 1;
/**
* 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的
*/
private static final int[] CONSTANT_ITEM_LENGTH = { -1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5 };
private static final int u1 = 1;
private static final int u2 = 2;
private byte[] classByte;
public ClassModifier(byte[] classByte) {
this.classByte = classByte;
}
/**
* 修改常量池中CONSTANT_Utf8_info常量的内容
* @param oldStr 修改前的字符串
* @param newStr 修改后的字符串
* @return 修改结果
*/
public byte[] modifyUTF8Constant(String oldStr, String newStr) {
int cpc = getConstantPoolCount();
int offset = CONSTANT_POOL_COUNT_INDEX + u2;
for (int i = 0; i < cpc; i++) {
int tag = ByteUtils.bytes2Int(classByte, offset, u1);
if (tag == CONSTANT_Utf8_info) {
int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
offset += (u1 + u2);
String str = ByteUtils.bytes2String(classByte, offset, len);
if (str.equalsIgnoreCase(oldStr)) {
byte[] strBytes = ByteUtils.string2Bytes(newStr);
byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen);
classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes);
return classByte;
} else {
offset += len;
}
} else {
offset += CONSTANT_ITEM_LENGTH[tag];
}
}
return classByte;
}
/**
* 获取常量池中常量的数量
* @return 常量池数量
*/
public int getConstantPoolCount() {
return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
}
}
/**
* Bytes数组处理工具
* @author
*/
public class ByteUtils {
public static int bytes2Int(byte[] b, int start, int len) {
int sum = 0;
int end = start + len;
for (int i = start; i < end; i++) {
int n = ((int) b[i]) & 0xff;
n <<= (--len) * 8;
sum = n + sum;
}
return sum;
}
public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i = 0; i < len; i++) {
b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff);
}
return b;
}
public static String bytes2String(byte[] b, int start, int len) {
return new String(b, start, len);
}
public static byte[] string2Bytes(String str) {
return str.getBytes();
}
public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
System.arraycopy(originalBytes, 0, newBytes, 0, offset);
System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length);
System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length, originalBytes.length - offset - len);
return newBytes;
}
}
- 最后一个类就是HackSystem,主要实现的功能就是把out和err两个静态变量改成试用 ByteArrayOutputStream作为打印目标的同一个PrintStream对象,以及增加读取、清理ByteArrayOutputStream中的内容的getBufferString()和clearBuffer()方法。其余的方法都来自System类的方法。
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
/**
* 为JavaClass劫持java.lang.System提供支持
* 除了out和err外,其余的都直接转发给System处理
* */
public class HackSystem {
public final static InputStream in = System.in;
private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public final static PrintStream out = new PrintStream(buffer);
public final static PrintStream err = out;
public static String getBufferString() {
return buffer.toString();
}
public static void clearBuffer() {
buffer.reset();
}
public static void setSecurityManager(final SecurityManager s) {
System.setSecurityManager(s);
}
public static SecurityManager getSecurityManager() {
return System.getSecurityManager();
}
public static long currentTimeMillis() {
return System.currentTimeMillis();
}
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) {
System.arraycopy(src, srcPos, dest, destPos, length);
}
public static int identityHashCode(Object x) {
return System.identityHashCode(x);
}
// 下面所有的方法都与java.lang.System的名称一样
// 实现都是字节转调System的对应方法
// 因版面原因,省略了其他方法
}
* 类JavaClassExecuter,提供给外部调用的入口,调用前面几个支持类组装逻辑,完成类加载工作。
import java.lang.reflect.Method;
/**
* JavaClass执行工具
*
* @author zzm
*/
public class JavaClassExecuter {
/**
* 执行外部传过来的代表一个Java类的Byte数组<br>
* 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类
* 执行方法为该类的static main(String[] args)方法,输出结果为该类向System.out/err输出的信息
* @param classByte 代表一个Java类的Byte数组
* @return 执行结果
*/
public static String execute(byte[] classByte) {
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "org/fenixsoft/classloading/execute/HackSystem");
HotSwapClassLoader loader = new HotSwapClassLoader();
Class clazz = loader.loadByte(modiBytes);
try {
Method method = clazz.getMethod("main", new Class[] { String[].class });
method.invoke(null, new String[] { null });
} catch (Throwable e) {
e.printStackTrace(HackSystem.out);
}
return HackSystem.getBufferString();
}
}
2.3:验证
通过下面的代码,演示和测试JavaExecuter。
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<%
InputStream is = new FileInputStream("c:/TestClass.class");
byte[] b = new byte[is.available()];
is.read(b);
is.close();
out.println("<textarea style='width:1000;height=800'>");
out.println(JavaClassExecuter.execute(b));
out.println("</textarea>");
%>