Exploiting the OW2 ASM Libary

As an obfuscator developer it is my job to attempt to prevent reverse engineers from analyzing customer's applications.

One common tool used by Java reverse engineers is The OW2 ASM Library, a library that can read, manipulate and write classfiles generated by the javac compiler. This makes ASM a target for obfuscation — hindering the functionality of ASM on obfuscated classes can be very valuable.

In this post I'll go over how I found and exploited a design flaw in the ASM library in order to create classfiles that render ASM useless.

Well Known Attributes

The JVM classfile specification defines the format of binary classfiles that can be loaded and executed by the Java Virtual Machine. The classfile format includes lists of the fields, methods and bytecode in a classfile for example.

Many of the structures in the classfile format, for example fields and methods, contain a list of attributes; attributes are simply a section of arbitrary bytes with a user defined name, similar to adding a custom section in an ELF file. For example, a custom attribute could be used to provide extra metadata to debuggers. While some attributes can be user defined, other attributes are “Well Known”, meaning that the name and structure of the attribute is known to and hardcoded within the JVM.

An example of this is the Code attribute, which contains information about the bytecode of a method. When the JVM encounters an attribute within a method with the name Code it will be parsed according to the definition of the code attribute within the JVM specification.

What is interesting however is when a new Java release, and therefore a new version of the classfile specification, results in the addition of a new well known attribute. This happened in Java 11, when the introduction of Nest-Based Access Control (JEP 181: Allow separate classes to access each other's private members) resulted in the NestMembers attribute being defined in the JVM specification.

There is an issue here — since compilers are allowed to insert user defined attributes into classfiles, what would happen if a Java 10 classfile with a custom attribute happening to be called NestMembers is loaded onto a Java 11 virtual machine? Well luckily the JVM developers thought of this and the JVM will not parse a well known attribute if the classfile version is less than the version in which the attribute was defined.

The exploit

While the JVM developers took this issue into consideration, the ASM library developers did not. The library will always try to parse well known attributes, no matter the classfile version. What this means is that we could for example add an attribute with the name NestMembers and a length of 0 to a version 8 classfile. ASM would attempt to parse the NestMembers attribute and would basically buffer overflow as it attempts to read data from a 0 length attribute. Sadly (or maybe luckily!) since ASM is written in Java, a memory safe language, all a buffer overflow means here is that an exception is thrown.

I've written a example classfile to demonstrate this behaviour which can be viewed here, which you can test by running the provided test.sh script. The classfile simply prints “Hello World!” but the key part is the attribute defined at the bottom of it. Essentially this classfile will run absolutely fine on any JVM, however ASM will crash upon attempting to parse the file with the following stacktrace:

java.lang.ArrayIndexOutOfBoundsException: Index 317 out of bounds for length 317
  at org.objectweb.asm.ClassReader.readUnsignedShort(ClassReader.java:3561)
  at org.objectweb.asm.ClassReader.accept(ClassReader.java:660)
  at org.objectweb.asm.ClassReader.accept(ClassReader.java:394)

Looking at the relevant code we can see the fault:

  // - The offset of the NestMembers attribute, or 0.
  int nestMembersOffset = 0;
  ...
  int currentAttributeOffset = getFirstAttributeOffset();
  for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) {
    // Read the attribute_info's attribute_name and attribute_length fields.
    String attributeName = readUTF8(currentAttributeOffset, charBuffer);
    int attributeLength = readInt(currentAttributeOffset + 2);
    currentAttributeOffset += 6;

    if (Constants.SOURCE_FILE.equals(attributeName)) {
      sourceFile = readUTF8(currentAttributeOffset, charBuffer);
    ...
    } else if (Constants.NEST_MEMBERS.equals(attributeName)) {
      nestMembersOffset = currentAttributeOffset;
    ...
    }
    currentAttributeOffset += attributeLength;
  }
  ...
  // Visit the NestedMembers attribute.
  if (nestMembersOffset != 0) {
    int numberOfNestMembers = readUnsignedShort(nestMembersOffset);
    int currentNestMemberOffset = nestMembersOffset + 2;
    while (numberOfNestMembers-- > 0) {
      classVisitor.visitNestMember(readClass(currentNestMemberOffset, charBuffer));
      currentNestMemberOffset += 2;
    }
  }
  ...

The problem is easily visible here — the NestMembers attribute is always parsed if present, no matter the classfile version. This can be compared to the behaviour of the Open JDK's classfile parser, which clearly avoids this mistake with a if (_major_version >= JAVA_11_VERSION):

  // Iterate over attributes
  while (attributes_count--) {
    cfs->guarantee_more(6, CHECK);  // attribute_name_index, attribute_length
    const u2 attribute_name_index = cfs->get_u2_fast();
    const u4 attribute_length = cfs->get_u4_fast();
    check_property(
        valid_symbol_at(attribute_name_index),
        "Attribute name has bad constant pool index %u in class file %s",
        attribute_name_index, CHECK);
    const Symbol* const tag = cp->symbol_at(attribute_name_index);
    if (tag == vmSymbols::tag_source_file()) {
    ...
    } else if (_major_version >= JAVA_11_VERSION) {
      if (tag == vmSymbols::tag_nest_members()) {
        // Check for NestMembers tag
        if (parsed_nest_members_attribute) {
          classfile_parse_error("Multiple NestMembers attributes in class file %s", THREAD);
          return;
        } else {
          parsed_nest_members_attribute = true;
        }
        if (parsed_nest_host_attribute) {
          classfile_parse_error("Conflicting NestHost and NestMembers attributes in class file %s", THREAD);
          return;
        }
        nest_members_attribute_start = cfs->current();
        nest_members_attribute_length = attribute_length;
        cfs->skip_u1(nest_members_attribute_length, CHECK);
      } else if (tag == vmSymbols::tag_nest_host()) {

I discovered this exploit while analysing ASM and included it within my obfuscator last year, however since it has now been copied by some of my competitors I thought it would be best to release it as a writeup. Hopefully this helps other people writing classfile parsers to know which pitfalls to avoid.

Tags: #jvm #ow2asm