feat: support behinder shell generate

This commit is contained in:
ReaJason
2024-12-22 00:05:01 +08:00
parent 100b4e5d9b
commit 01fd2ecb80
142 changed files with 2661 additions and 289 deletions
+30
View File
@@ -0,0 +1,30 @@
plugins {
id "io.freefair.lombok" version "8.11"
}
group = 'com.reajason.javaweb'
version = ''
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation 'net.bytebuddy:byte-buddy:1.+'
implementation 'commons-io:commons-io:2.+'
implementation 'org.apache.commons:commons-lang3:3.+'
implementation 'commons-codec:commons-codec:1.+'
implementation 'org.jetbrains:annotations:26.0.1'
testImplementation platform('org.junit:junit-bom:5.+')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation "org.mockito:mockito-core:5.+"
}
test {
useJUnitPlatform()
}
@@ -0,0 +1,56 @@
package com.reajason.javaweb.buddy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.DynamicType;
import java.lang.reflect.Field;
import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer;
/**
* JDK9 引入的 module 系统,只有主动声明 exports 的才能被外部访问。当前用于打破 module 的限制,使我们能像低版本一样任意反射获取方法
*
* @author ReaJason
*/
public class ByPassJavaModuleInterceptor {
@Advice.OnMethodEnter
public static void enter(@Advice.Origin Class<?> clazz) {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
java.lang.reflect.Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Object unsafe = unsafeField.get(null);
Object module = Class.class.getMethod("getModule").invoke(Object.class, (Object[]) null);
java.lang.reflect.Method objectFieldOffsetM = unsafe.getClass().getMethod("objectFieldOffset", Field.class);
Long offset = (Long) objectFieldOffsetM.invoke(unsafe, Class.class.getDeclaredField("module"));
java.lang.reflect.Method getAndSetObjectM = unsafe.getClass().getMethod("getAndSetObject", Object.class, long.class, Object.class);
getAndSetObjectM.invoke(unsafe, clazz, offset, module);
} catch (Exception ignored) {
}
}
/**
* Reference1: <a href="https://stackoverflow.com/questions/62664427/can-i-create-a-bytebuddy-instrumented-type-with-a-private-static-final-methodhan">stackoverflow</a>
* Reference2: <a href="https://github.com/raphw/byte-buddy/issues/1153">issue</a>
* <br>
* 在静态代码块中执行 byPassJdkModule 代码
* 值得注意的一点,builder 是不可变类型,所以都是需要重新赋值,例如以下代码示例
* # code that not work
* builder = new Bytebuddy().redefine(class);
* builder.visit(something);
* builder.make();
* <br>
* # code that work
* <br>
* builder = new Bytebuddy().redefine(class);
* builder = builder.visit(something);
* builder.make();
*
* @param builder bytebuddy builder
* @return new builder with bypass
*/
public static DynamicType.Builder<?> extend(DynamicType.Builder<?> builder) {
return builder.visit(Advice.to(ByPassJavaModuleInterceptor.class)
.on(isTypeInitializer()));
}
}
@@ -0,0 +1,67 @@
package com.reajason.javaweb.buddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.OpenedClassReader;
import org.jetbrains.annotations.NotNull;
/**
* 移除静态代码块
*
* @author ReaJason
*/
public enum ClinitRemovingAsmVisitorWrapper implements AsmVisitorWrapper {
/**
* The singleton instance.
*/
INSTANCE;
private static final String CLINIT = "<clinit>";
@Override
public int mergeWriter(int flags) {
return flags;
}
@Override
public int mergeReader(int flags) {
return flags;
}
@NotNull
@Override
public ClassVisitor wrap(@NotNull TypeDescription instrumentedType,
@NotNull ClassVisitor classVisitor,
@NotNull Implementation.Context implementationContext,
@NotNull TypePool typePool,
@NotNull FieldList<FieldDescription.InDefinedShape> fields,
@NotNull MethodList<?> methods,
int writerFlags,
int readerFlags) {
return new ClinitRemovingClassVisitor(classVisitor);
}
protected static class ClinitRemovingClassVisitor extends ClassVisitor {
protected ClinitRemovingClassVisitor(ClassVisitor classVisitor) {
super(OpenedClassReader.ASM_API, classVisitor);
}
@Override
public MethodVisitor visitMethod(
int modifiers, String name, String descriptor, String signature, String[] exception) {
MethodVisitor methodVisitor =
super.visitMethod(modifiers, name, descriptor, signature, exception);
return name.equals(CLINIT)
? null
: methodVisitor;
}
}
}
@@ -0,0 +1,66 @@
package com.reajason.javaweb.buddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import static net.bytebuddy.jar.asm.Opcodes.INVOKEVIRTUAL;
import static net.bytebuddy.jar.asm.Opcodes.POP;
/**
* Debug 信息打印移除器
* 目前仅支持移除以下几种
* <br />
* 1. System.out.println() - printf 还不支持)
* 2. e.printStackTrace()
* 3. Logger.info (java.util)
*
* @author ReaJason
*/
public class LogRemoveMethodVisitor implements AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper {
public static final LogRemoveMethodVisitor INSTANCE = new LogRemoveMethodVisitor();
public static DynamicType.Builder<?> extend(DynamicType.Builder<?> builder) {
return builder.visit(
new AsmVisitorWrapper.ForDeclaredMethods()
.method(ElementMatchers.any(), LogRemoveMethodVisitor.INSTANCE));
}
@NotNull
@Override
public MethodVisitor wrap(@NotNull TypeDescription instrumentedType,
@NotNull MethodDescription instrumentedMethod,
@NotNull MethodVisitor methodVisitor,
@NotNull Implementation.Context implementationContext,
@NotNull TypePool typePool,
int writerFlags,
int readerFlags) {
return new MethodVisitor(Opcodes.ASM9, methodVisitor) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if ((opcode == INVOKEVIRTUAL && owner.equals("java/io/PrintStream") && name.equals("println"))
|| (opcode == INVOKEVIRTUAL && owner.endsWith("Exception") && name.equals("printStackTrace"))
|| (opcode == INVOKEVIRTUAL && owner.equals("java/util/logging/Logger") && (name.equals("info") || name.equals("warning")))
) {
String[] args = descriptor.substring(1, descriptor.indexOf(')')).split(";");
for (String arg : args) {
if (StringUtils.isNotBlank(arg)) {
super.visitInsn(POP);
}
}
super.visitInsn(POP);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
};
}
}
@@ -0,0 +1,76 @@
package com.reajason.javaweb.buddy;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import java.nio.file.Files;
import java.nio.file.Paths;
import static net.bytebuddy.matcher.ElementMatchers.named;
public class MethodSubstitutionExample {
public static class MethodReplacementMethodVisitor extends MethodVisitor {
private final String targetClassName;
public MethodReplacementMethodVisitor(MethodVisitor mv, String targetClassName) {
super(Opcodes.ASM9, mv);
this.targetClassName = targetClassName;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == Opcodes.INVOKESTATIC
&& owner.endsWith("ExternalClass")
&& name.equals("replacementMethod")) {
super.visitMethodInsn(Opcodes.INVOKESTATIC,
targetClassName.replace(".", "/"),
name,
descriptor,
false);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
public static class ExternalClass {
public static String externalMethod(String input) {
return "External: " + input;
}
public static String replacementMethod(String input) {
return "Replaced: " + input;
}
}
public static class TargetClass {
public String targetMethod(String input) {
System.out.println("targetMethod");
return ExternalClass.replacementMethod(input);
}
public static String replacementMethod(String input) {
return "Replaced: " + input;
}
}
public static void main(String[] args) throws Exception {
String oldClassName = TargetClass.class.getName();
String newClassName = oldClassName + "Redefinition";
DynamicType.Unloaded<TargetClass> dynamicType = new ByteBuddy()
.redefine(TargetClass.class)
.name(newClassName)
.visit(new AsmVisitorWrapper.ForDeclaredMethods().method(named("targetMethod"), (typeDescription, methodDescription, methodVisitor, context, typePool, i, i1) -> new MethodReplacementMethodVisitor(methodVisitor, newClassName)))
.make();
Files.write(Paths.get("xixi.class"), dynamicType.getBytes());
Class<?> redefinedClass = dynamicType.load(MethodSubstitutionExample.class.getClassLoader())
.getLoaded();
}
}
@@ -0,0 +1,57 @@
package com.reajason.javaweb.buddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.commons.ClassRemapper;
import net.bytebuddy.jar.asm.commons.Remapper;
import net.bytebuddy.pool.TypePool;
import org.jetbrains.annotations.NotNull;
/**
* Servlet 包名替换,扫描包中所有 javax/servlet 将其替换成 jakarta/servlet。
*
* @author ReaJason
* @since 2024/11/23
*/
public class ServletRenameVisitorWrapper implements AsmVisitorWrapper {
public static ServletRenameVisitorWrapper INSTANCE = new ServletRenameVisitorWrapper();
@Override
public int mergeReader(int flags) {
return flags;
}
@Override
public int mergeWriter(int flags) {
return flags;
}
@NotNull
@Override
public ClassVisitor wrap(@NotNull TypeDescription instrumentedType,
@NotNull ClassVisitor classVisitor,
@NotNull Implementation.Context implementationContext,
@NotNull TypePool typePool,
@NotNull FieldList<FieldDescription.InDefinedShape> fields,
@NotNull MethodList<?> methods,
int writerFlags,
int readerFlags) {
return new ClassRemapper(
classVisitor,
new Remapper() {
@Override
public String map(String typeName) {
if (typeName.startsWith("javax/servlet/")) {
return typeName.replaceFirst("javax", "jakarta");
} else {
return typeName;
}
}
});
}
}
@@ -0,0 +1,58 @@
package com.reajason.javaweb.buddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.nullability.MaybeNull;
import org.jetbrains.annotations.NotNull;
/**
* 通过 classVisitor 将 classFileVersion 改为指定 JDK 版本,用于 JDK8 的环境能生成任意 JDK 版本的字节码,默认使用 JDK6
*
* @author ReaJason
*/
public class TargetJreVersionVisitorWrapper implements AsmVisitorWrapper {
public static final TargetJreVersionVisitorWrapper DEFAULT = new TargetJreVersionVisitorWrapper();
private final int targetJdkVersion;
public TargetJreVersionVisitorWrapper() {
targetJdkVersion = Opcodes.V1_6;
}
public TargetJreVersionVisitorWrapper(int targetJdkVersion) {
this.targetJdkVersion = targetJdkVersion;
}
@Override
public int mergeWriter(int flags) {
return flags;
}
@Override
public int mergeReader(int flags) {
return flags;
}
@NotNull
@Override
public ClassVisitor wrap(@NotNull TypeDescription instrumentedType,
@NotNull ClassVisitor classVisitor, @NotNull Implementation.Context implementationContext,
@NotNull TypePool typePool, @NotNull FieldList<FieldDescription.InDefinedShape> fields,
@NotNull MethodList<?> methods, int writerFlags, int readerFlags) {
return new ClassVisitor(Opcodes.ASM9, classVisitor) {
@Override
public void visit(int version, int modifiers, String name, @MaybeNull String signature, @MaybeNull String superClassName, @MaybeNull String[] interfaceName) {
super.visit(targetJdkVersion, modifiers, name, signature, superClassName, interfaceName);
}
};
}
}
@@ -0,0 +1,13 @@
package com.reajason.javaweb.util;
/**
* @author ReaJason
*/
public class ClassDefiner extends ClassLoader {
private ClassDefiner() {
}
public static Class<?> defineClass(byte[] code) {
return new ClassDefiner().defineClass(null, code, 0, code.length);
}
}
@@ -0,0 +1,38 @@
package com.reajason.javaweb.util;
import lombok.SneakyThrows;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* @author ReaJason
* @since 2024/11/23
*/
public class ClassUtils {
@SneakyThrows
public static Class<?> defineClass(byte[] bytes) {
return ClassDefiner.defineClass(bytes);
}
@SneakyThrows
public static Object newInstance(byte[] bytes) {
Class<?> clazz = defineClass(bytes);
return clazz.newInstance();
}
@SneakyThrows
public static Object getFieldValue(Object object, String fieldName) {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
@SneakyThrows
public static Object invokeMethod(Object object, String methodName, Class<?>[] parameterTypes, Object[] parameters) {
Method method = object.getClass().getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method.invoke(object, parameters);
}
}
@@ -0,0 +1,33 @@
package com.reajason.javaweb.buddy;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnJre;
import java.lang.reflect.InaccessibleObjectException;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.condition.JRE.JAVA_17;
/**
* @author ReaJason
* @since 2024/12/7
*/
class ByPassJavaModuleInterceptorTest {
@Test
@SneakyThrows
@EnabledOnJre(JAVA_17)
void testByPassModule() {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
assertThrows(InaccessibleObjectException.class, () -> {
defineClass.setAccessible(true);
});
ByPassJavaModuleInterceptor.enter(this.getClass());
assertDoesNotThrow(() -> {
defineClass.setAccessible(true);
});
}
}
@@ -0,0 +1,119 @@
package com.reajason.javaweb.buddy;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import org.junit.jupiter.api.Test;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.*;
/**
* @author ReaJason
* @since 2024/12/4
*/
@Slf4j
class LogRemoveVisitorWrapperTest {
@Test
void testExtend() {
DynamicType.Builder<?> builder = new ByteBuddy().subclass(Object.class);
DynamicType.Builder<?> extendedBuilder = LogRemoveMethodVisitor.extend(builder);
assertNotNull(extendedBuilder);
assertNotEquals(builder, extendedBuilder);
}
@Test
void testWrap() {
LogRemoveMethodVisitor visitor = LogRemoveMethodVisitor.INSTANCE;
TypeDescription instrumentedType = mock(TypeDescription.class);
MethodDescription instrumentedMethod = mock(MethodDescription.class);
MethodVisitor methodVisitor = mock(MethodVisitor.class);
Implementation.Context implementationContext = mock(Implementation.Context.class);
TypePool typePool = mock(TypePool.class);
MethodVisitor wrappedVisitor = visitor.wrap(instrumentedType, instrumentedMethod, methodVisitor,
implementationContext, typePool, 0, 0);
assertNotNull(wrappedVisitor);
assertNotEquals(methodVisitor, wrappedVisitor);
}
@Test
void testVisitMethodInsn_RemoveSystemOutPrintln() {
MethodVisitor methodVisitor = mock(MethodVisitor.class);
LogRemoveMethodVisitor.INSTANCE.wrap(null, null, methodVisitor, null, null, 0, 0)
.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
verify(methodVisitor, never()).visitMethodInsn(anyInt(), anyString(), anyString(), anyString(), anyBoolean());
}
@Test
void testVisitMethodInsn_RemovePrintStackTrace() {
MethodVisitor methodVisitor = mock(MethodVisitor.class);
LogRemoveMethodVisitor.INSTANCE.wrap(null, null, methodVisitor, null, null, 0, 0)
.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "()V", false);
verify(methodVisitor, never()).visitMethodInsn(anyInt(), anyString(), anyString(), anyString(), anyBoolean());
}
@Test
void testVisitMethodInsn_KeepOtherMethodCalls() {
MethodVisitor methodVisitor = mock(MethodVisitor.class);
MethodVisitor wrappedVisitor = LogRemoveMethodVisitor.INSTANCE.wrap(null, null, methodVisitor, null, null, 0, 0);
wrappedVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false);
verify(methodVisitor).visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false);
}
@Test
void testIntegration() throws Exception {
// Use ByteBuddy to create a new class with log statements removed
DynamicType.Unloaded<TestClass> make = new ByteBuddy()
.redefine(TestClass.class)
.name("com.reajason.javaweb.buddy.TestClass1")
.visit(new AsmVisitorWrapper.ForDeclaredMethods()
.method(ElementMatchers.any(), LogRemoveMethodVisitor.INSTANCE))
.make();
byte[] bytes = make.getBytes();
// Files.write(Paths.get("xx.class"), bytes);
Class<?> modifiedClass = make.load(getClass().getClassLoader()).getLoaded();
Object instance = modifiedClass.getDeclaredConstructor().newInstance();
modifiedClass.getMethod("methodWithLogs").invoke(instance);
}
public static class TestClass {
static Logger logger = Logger.getLogger(TestClass.class.getName());
public TestClass() {
}
public static void methodWithLogs() {
System.out.println("This should be removed");
String test = "test";
int length = test.length();
logger.info(test);
try {
System.out.println("hello");
throw new RuntimeException("hello");
} catch (Exception e) {
e.printStackTrace();
}
logger.warning("wa");
}
public static void main(String[] args) {
methodWithLogs();
}
}
}