一个 MethodHandle 常量能否被使用以绕过访问控制?

huangapple go评论135阅读模式
英文:

Can a MethodHandle constant be used in such a way as to bypass access control?

问题

我正在使用 JDK 15。在其中一个生成的类中,我正在对我已经成功存储在生成的类中的 MethodHandle 常量调用 invokeExact()。它是通过 MethodHandles.Lookup#findSetter 获取的“字段设置器”。

(在接下来的内容中,我知道 MethodHandles.privateLookupIn() 方法。)

我注意到,所讨论的“字段设置器” MethodHandle 在表示 private 字段时会失败。在大多数情况下,这并不让我感到意外:直接的 MethodHandle 是直接的。虽然我并不自称对所有这些东西的内部机制都了解很多,但在我看来,它肯定只是包装了一些不包含访问检查的低级字节码。

但是鉴于存在 privateLookupIn(),显示在某些情况下可以绕过访问检查,在某种情况下是否有可能从类 A 中“收集”一个可以读取 private 字段的“字段设置器” MethodHandle,然后将其存储为类 B 中的常量,以便对其调用 invokeExact() 会成功呢?

我相信我过去做过类似的事情(必须检查一下),涉及到 private 方法,但在那些情况下我 没有 使用 MethodHandle 常量,即我是在类初始化期间在 <clinit> 时使用 privateLookupIn() 获取 MethodHandle,并将生成的 MethodHandle 存储在 private static final 字段中,然后调用 invokeExact() 调用该字段的内容。如果我必须继续走这条路,我会这样做,但是 MethodHandle 常量在这里似乎很有吸引力,如果可能的话,我想使用它们。

因此,另一种表达我的问题的方式是:MethodHandle 所表示的常量形式是否能够存储其权限?还是在将 MethodHandle 存储为常量的情况下,是否有某种一次性方法可以提升权限?或者事实上,给定一个存储为常量的 MethodHandle,它是否永远无法访问除常规可访问的 Java 构造之外的任何内容?在 JVM 规范的相关章节中,我没有看到任何非常明显的信息。

英文:

I am using JDK 15. (I am using ByteBuddy 1.10.16 to generate some classes but it's mostly irrelevant here, I think, except as background information.)

In one of these generated classes, I am calling invokeExact() on a MethodHandle constant I've managed to store in the generated class. It is a "field setter" acquired via MethodHandles.Lookup#findSetter.

(In what follows I am aware of the MethodHandles.privateLookupIn() method.)

I've noticed that the "field setter" MethodHandle in question fails when it represents a private field. At most levels this does not surprise me: a direct MethodHandle is, well, direct: while I don't pretend to know much about the innards of all this stuff, it seems to me that surely it must just wrap some low-level bytecode devoid of access checks.

But given the existence of privateLookupIn() which shows that bypassing access checks is possible in certain situations, is there a path where I can "harvest" a "field setter" MethodHandle from class A that can read a private field, and then store it as a constant in another class B such that invokeExact() on it will succeed?

I believe I have done something similar in the past (have to check) involving private methods, but in those cases I was not using MethodHandle constants, i.e. I was acquiring the MethodHandle at class initialization time during <clinit> time using privateLookupIn() and storing the resulting MethodHandle in a private static final field, and then calling invokeExact() on the contents of that field. If I have to continue to go this route, I will, but MethodHandle constants seem appealing here and it would be nice to use them if I can.

So another way of phrasing my question is: is the constant form in which a MethodHandle is represented capable of storing its privileges? Or is there some one-time way of "upping" the privileges given a MethodHandle stored as a constant? Or does the fact that a given MethodHandle is stored as a constant prevent it for all time from accessing anything other than conventionally accessible Java constructs? I didn't see anything super obvious in the JVM specification in the relevant section.

答案1

得分: 3

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
        Type mhLookup = Type.getType(MethodHandles.Lookup.class);
        Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);

        Type targetType = Type.getType(DynConstant.class);

        String myBootstrapName = "privateLookup";
        String myBootstrapDesc = Type.getMethodDescriptor(mHandle, mhLookup, string, clazz, clazz, mType);

        String generatedClassName = DynConstant.class.getPackageName().replace('.', '/') + "/Test";

        Handle myBootStrap = new Handle(H_INVOKESTATIC, generatedClassName,
            myBootstrapName, myBootstrapDesc, true);
        ConstantDynamic theHandle = new ConstantDynamic("inacessibleMethod",
            mHandle.getDescriptor(), myBootStrap, targetType, Type.getMethodType("()V"));

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE | ACC_ABSTRACT, generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        mv = cw.visitMethod(ACC_PRIVATE | ACC_STATIC, myBootstrapName, myBootstrapDesc, null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 0); // MethodHandles.lookup() generated as JVM arg
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "privateLookupIn",
            Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 1); // invoked name, i.e. "inacessibleMethod"
        mv.visitVarInsn(ALOAD, 4); // bootstrap argument, i.e. MethodType ()V
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",
            Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(4, 5);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName + ".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-p", "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for (Path p = classFile; ; p = p.getParent()) {
                    Files.delete(p);
                    if (p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}
英文:

The specification you’ve linked states:

> To resolve MH, all symbolic references to classes, interfaces, fields, and methods in MH's bytecode behavior are resolved, using the following four steps:
>
> R is resolved. This occurs as if by field resolution (§5.4.3.2) when MH's bytecode behavior is kind 1, 2, 3, or 4, and as if by method resolution (§5.4.3.3) when MH's bytecode behavior is kind 5, 6, 7, or 8, and as if by interface method resolution (§5.4.3.4) when MH's bytecode behavior is kind 9.

The linked chapters, i.e. §5.4.3.2 for fields, describe the ordinary resolution process, including access control. Even without that explicit statement, you could derive the existence of access control from the preceding description, that states that these symbolic method handle references are supposed to be equivalent to specific listed bytecode behavior.

So a direct method handle acquired via a CONSTANT_MethodHandle_info entry of the class file’s constant pool can not access classes or members that wouldn’t be also accessible directly by bytecode instructions.

But since JDK 11, you can use Dynamic Constants to load constants of arbitrary type defined by an arbitrary bootstrapping process. So when you can express how to get the constant in terms of Java code, like the use of privateLookupIn, you can also define it as bootstrapping of a dynamic constant and load that constant at places where you would otherwise load the direct method handle.

Consider the following starting point:

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        // express the constant
        Handle theHandle = new Handle(H_INVOKESTATIC,
            Type.getInternalName(DynConstant.class), "inacessibleMethod",
            Type.getMethodDescriptor(Type.VOID_TYPE), false);

        String generatedClassName
                = DynConstant.class.getPackageName().replace('.', '/')+"/Test";

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT,
                generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(
                ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL,
                "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName+".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for(Path p = classFile;;p=p.getParent()) {
                    Files.delete(p);
                    if(p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        }
        catch(Throwable t) {
            t.printStackTrace();
        }
    }
}

It tries to define a new runtime class that attempts to load a MethodHandle constant pointing to inacessibleMethod() via a CONSTANT_MethodHandle_info. The program prints

interface instexamples.Test {
public static void test();
Code:
0: ldc           #12                 // MethodHandle REF_invokeStatic instexamples/DynConstant.inacessibleMethod:()V
2: invokevirtual #17                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
5: return
}
java.lang.IllegalAccessError: class instexamples.Test tried to access private method 'void instexamples.DynConstant.inacessibleMethod()' (instexamples.Test and instexamples.DynConstant are in unnamed module of loader 'app')
at instexamples.Test.test(Unknown Source)
at instexamples.DynConstant.main(DynConstant.java:100)

Now, let’s change the constant to a dynamic constant that will perform the equivalent to

MethodHandles.Lookup l = MethodHandles.lookup();
l = MethodHandles.privateLookupIn(DynConstant.class, l);
MethodHandle mh = l.findStatic(
        DynConstant.class, "inacessibleMethod", MethodType.methodType(void.class));

when the constant is resolved the first time. The definition of the constant is “a bit” more involved. Since the code contains three method invocations, the definition requires three method handles, further, another handle to the already existing bootstrap method ConstantBootstraps.invoke(…) that allows to use arbitrary method invocations for the bootstrapping. These handles can be used to define dynamic constants, whereas dynamic constants are allowed as constant input to another dynamic constant.

So we replace the definition after the // express the constant comment with:

Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
Type oArray = Type.getType(Object[].class), object = oArray.getElementType();
Type mhLookup = Type.getType(MethodHandles.Lookup.class);
Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);
Type targetType = Type.getType(DynConstant.class);

String methodHandles = Type.getInternalName(MethodHandles.class);

Handle methodHandlesLookup = new Handle(H_INVOKESTATIC, methodHandles,
    "lookup", Type.getMethodDescriptor(mhLookup), false);
Handle privateLookupIn = new Handle(H_INVOKESTATIC, methodHandles,
    "privateLookupIn", Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
Handle findStatic = new Handle(H_INVOKEVIRTUAL, mhLookup.getInternalName(),
    "findStatic", Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
Handle invoke = new Handle(H_INVOKESTATIC,
    Type.getInternalName(ConstantBootstraps.class), "invoke",
    Type.getMethodDescriptor(object, mhLookup, string, clazz, mHandle, oArray), false);

ConstantDynamic methodHandlesLookupC = new ConstantDynamic("lookup",
    mhLookup.getDescriptor(), invoke, methodHandlesLookup);
ConstantDynamic privateLookupInC = new ConstantDynamic("privateLookupIn",
    mhLookup.getDescriptor(), invoke, privateLookupIn, targetType, methodHandlesLookupC);
ConstantDynamic theHandle = new ConstantDynamic("findStatic",
    mHandle.getDescriptor(), invoke, findStatic,
    privateLookupInC, targetType, "inacessibleMethod", Type.getMethodType("()V"));

To avoid repeating the very long constant method descriptor strings, I use ASM’s Type abstraction. In principle, we could use constant strings for all type names and signatures.

This program prints:

interface instexamples.Test {
public static void test();
Code:
0: ldc           #45                 // Dynamic #2:findStatic:Ljava/lang/invoke/MethodHandle;
2: invokevirtual #50                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
5: return
}
java.lang.Exception: inacessibleMethod() called
at instexamples.DynConstant.inacessibleMethod(DynConstant.java:23)
at instexamples.Test.test(Unknown Source)
at instexamples.DynConstant.main(DynConstant.java:89)

The complexity of a dynamic constant composed of three constants created by method invocations will result in quite a big constant pool. We may generate a custom bootstrap method instead and get a significantly smaller class file, despite we have an additional method:

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
        Type mhLookup = Type.getType(MethodHandles.Lookup.class);
        Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);

        Type targetType = Type.getType(DynConstant.class);

        String myBootstrapName = "privateLookup";
        String myBootstrapDesc = Type.getMethodDescriptor(mHandle, mhLookup, string, clazz, clazz, mType);

        String generatedClassName = DynConstant.class.getPackageName().replace('.', '/')+"/Test";

        Handle myBootStrap = new Handle(H_INVOKESTATIC, generatedClassName,
            myBootstrapName, myBootstrapDesc, true);
        ConstantDynamic theHandle = new ConstantDynamic("inacessibleMethod",
            mHandle.getDescriptor(), myBootStrap, targetType, Type.getMethodType("()V"));

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT, generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        mv = cw.visitMethod(ACC_PRIVATE|ACC_STATIC, myBootstrapName, myBootstrapDesc, null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 0); // MethodHandles.lookup() generated as JVM arg
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "privateLookupIn",
            Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 1); // invoked name, i.e. "inacessibleMethod"
        mv.visitVarInsn(ALOAD, 4); // bootstrap argument, i.e. MethodType ()V
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",
            Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(4, 5);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName+".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-p", "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for(Path p = classFile;;p=p.getParent()) {
                    Files.delete(p);
                    if(p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        }
        catch(Throwable t) {
            t.printStackTrace();
        }
    }
}
interface instexamples.custombootstrap.Test {
public static void test();
Code:
0: ldc           #18                 // Dynamic #0:inacessibleMethod:Ljava/lang/invoke/MethodHandle;
2: invokevirtual #23                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
5: return
private static java.lang.invoke.MethodHandle privateLookup(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.Class, java.lang.Class, java.lang.invoke.MethodType);
Code:
0: aload_3
1: aload_0
2: invokestatic  #29                 // Method java/lang/invoke/MethodHandles.privateLookupIn:(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup;
5: aload_3
6: aload_1
7: aload         4
9: invokevirtual #35                 // Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
12: areturn
}
java.lang.Exception: inacessibleMethod() called
at instexamples.custombootstrap.DynConstant.inacessibleMethod(DynConstant.java:22)
at instexamples.custombootstrap.Test.test(Unknown Source)
at instexamples.custombootstrap.DynConstant.main(DynConstant.java:91)

The bootstrap method has been designed to be reusable. It receives all necessary information as constant arguments, so different ldc instructions can use it to get handles to different members. The JVM does already pass the caller’s lookup context as first argument, so we can use this and don’t need to call MethodHandles.lookup(). The class to search for the member is the first additional argument, which is used as first argument to both, privateLookupIn and findStatic. Since every dynamic constant has a standard name argument, we can use it to denote the member’s name. The last argument denotes the MethodType for the method to look up. When we retrofit this for field lookups, we could remove that parameter, as the third standard argument, the expected constant type could be matched with the expected field’s type.

Basically, the custom bootstrap method does the privateLookupIn based lookup you described in your question, but using it with ldc allows to have lazy initialization (rather than the class initialization time of static final fields) while still getting optimized like static final fields once the instruction has been linked. Also, these dynamic constants are permitted as constant input to other bootstrap methods for other dynamic constants or invokedynamic instructions (though, you can also adapt an existing static final field to a dynamic constant using this bootstrap method).

huangapple
  • 本文由 发表于 2020年10月11日 08:58:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/64299711.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定