【问题标题】:Can a MethodHandle constant be used in such a way as to bypass access control?可以使用 MethodHandle 常量绕过访问控制吗?
【发布时间】:2021-01-25 16:42:14
【问题描述】:

我正在使用 JDK 15。(我正在使用 ByteBuddy 1.10.16 来生成一些类,但我认为它在这里几乎无关紧要,除了作为背景信息。)

在其中一个生成的类中,我在MethodHandle 上调用invokeExact() 常量我已设法将其存储在生成的类中。它是通过MethodHandles.Lookup#findSetter获得的“字段设置器”。

(下面我知道MethodHandles.privateLookupIn() 方法。)

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

但鉴于privateLookupIn() 的存在表明在某些情况下可以绕过访问检查,是否有一条路径可以让我从A 类中“收获”一个“字段设置器”MethodHandle 可以读取@987654337 @字段,然后将其作为常量存储在另一个B类中,这样invokeExact()就可以成功?

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

所以另一种表达我的问题的方式是:表示MethodHandle 的常量形式是否能够存储其权限?或者是否有一些一次性的方法来“提高”给定MethodHandle 存储为常量的特权?还是将给定的MethodHandle 存储为常量这一事实是否会一直阻止它访问除传统可访问的 Java 构造之外的任何内容? I didn't see anything super obvious in the JVM specification in the relevant section.

【问题讨论】:

  • 我不知道 ByteBuddy 的具体细节,但一般来说,访问检查是在解析常量时执行的。您可以通过预先解析 MethodHandle 然后使用常量池修补将活动对象放入常量池中来解决此问题,但目前没有可用的公共 API 可以做到这一点(尽管我相信是/计划中的:@987654324 @)。
  • @JornVernee Afaik,没有计划将常量池补丁引入公共 API 由于 defineHiddenClass 应该取代旧的 Unsafe.defineAnonymousClass,常量池补丁即将消失而无需替换。您可以使用动态常量,通过ldc 指令或类似指令加载此类方法句柄。不过,static final 字段没有任何好处。
  • @Holger 请注意,还有一个defineHiddenClassWithClassData 方法,目前是包私有的,可以用作常量池修补的替代品。 (虽然你是对的,但它们并不完全相同)。 Last I heard这是替换CP补丁的计划,不过最近没问...

标签: java methodhandle


【解决方案1】:

specification you’ve linked 声明:

要解析MH,对MH的字节码行为中的类、接口、字段和方法的所有符号引用都会使用以下四个步骤进行解析:

R 已解决。当 MH 的字节码行为是种类 1、2、3 或 4 时,这好像通过字段解析 (§5.4.3.2) 发生,并且当 MH 的字节码行为时,好像通过方法解析 (§5.4.3.3)是种类 5、6、7 或 8,并且当 MH 的字节码行为是种类 9 时,就好像通过接口方法解析 (§5.4.3.4)。

链接的章节,即字段的§5.4.3.2,描述了普通的解析过程,包括访问控制。即使没有该明确声明,您也可以从前面的描述中推导出访问控制的存在,即这些符号方法句柄引用应该等同于列出的特定字节码行为。

因此,通过类文件常量池的CONSTANT_MethodHandle_info 条目获取的直接方法句柄无法访问字节码指令也无法直接访问的类或成员。

但从 JDK 11 开始,您可以使用 Dynamic Constants 加载由任意引导进程定义的任意类型的常量。因此,当您可以用 Java 代码表达如何获取常量时,例如使用 privateLookupIn,您还可以将其定义为动态常量的引导,并将该常量加载到您将加载直接方法句柄的位置.

考虑以下起点:

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();
        }
    }
}

它尝试定义一个新的运行时类,该类尝试通过CONSTANT_MethodHandle_info 加载指向inacessibleMethod()MethodHandle 常量。程序打印

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)

现在,让我们将常量更改为动态常量,该常量将执行等价于

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

当第一次解析常量时。常数的定义“有点”复杂。由于代码包含三个方法调用,因此该定义需要三个方法句柄,此外,另一个句柄是已经存在的引导方法ConstantBootstraps.invoke(…),它允许使用任意方法调用进行引导。这些句柄可用于定义动态常量,而动态常量可以作为常量输入到另一个动态常量。

所以我们将// express the constant评论后的定义替换为:

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"));

为了避免重复很长的常量方法描述符字符串,我使用了 ASM 的 Type 抽象。原则上,我们可以为所有类型名称和签名使用常量字符串。

这个程序打印:

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)

由方法调用创建的三个常量组成的动态常量的复杂性将导致相当大的常量池。尽管我们有一个额外的方法,但我们可能会生成一个自定义引导方法并获得一个小得多的类文件:

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)

引导方法被设计为可重复使用。它接收所有必要的信息作为常量参数,因此不同的ldc 指令可以使用它来获取不同成员的句柄。 JVM 确实已经将调用者的查找上下文作为第一个参数传递了,所以我们可以使用它而无需调用 MethodHandles.lookup()。搜索成员的类是第一个附加参数,用作privateLookupInfindStatic 的第一个参数。由于每个动态常量都有一个标准名称参数,我们可以使用它来表示成员的名称。最后一个参数表示要查找的方法的MethodType。当我们为字段查找进行改造时,我们可以删除该参数,作为第三个标准参数,预期的常量类型可以与预期的字段类型匹配。

基本上,自定义引导方法执行您在问题中描述的基于privateLookupIn 的查找,但是将其与ldc 一起使用允许延迟初始化(而不是static final 字段的类初始化时间),同时仍然获得链接指令后,像static final 字段一样优化。此外,这些动态常量允许作为其他动态常量或 invokedynamic 指令的其他引导方法的常量输入(不过,您也可以使用 this bootstrap method 将现有的 static final 字段调整为动态常量)。

【讨论】:

  • 谢谢你;这是我前一段时间最终采用的确切方法。确实很强大。
  • 在您的示例中,我将简单地将引导方法放入DynConstant - 使其更具可读性。
  • @JohannesKuhn 依赖于现有的引导方法是否可行取决于实际用例。例如,重要的是让引导方法与目标方法不在同一个类中,以演示如何获取不可访问方法的句柄。还有其他可以想象的场景,其中声明类包含一个引导方法,允许某些friend 类以这种方式访问​​它们……
  • 很少需要用字节码编写实际的引导类。是的,如果您将引导类移动到 DynConstant,则引导类可以在您的示例中使用它自己的查找。但这不是重点 - 重点是:您可能更容易用纯 java 编写引导方法。
  • 我并不是要预测未来更有可能使用什么。我试图给出一个完整的画面。由于答案解释了引导方法的原理并提供了普通的 java 等价物,因此应该有足够的信息,以允许读者使用他们喜欢的任何方法编写自己的引导方法。
猜你喜欢
  • 1970-01-01
  • 2012-07-27
  • 2011-11-25
相关资源
最近更新 更多