Skip to content

Code Generation

German Vekhorev edited this page May 28, 2021 · 10 revisions

Code Generation in Access Warden

Contrary to Lombok that works as an annotation processor and is based on a bug in the Java compiler that lets it modify the existing AST (abstract syntax tree) of your code before actual compilation (conversion of .java source to .class bytecode), Access Warden does not interfere with the compilation phase of your application. Instead, it uses ASM to modify the existing, already compiled JVM (Java Virtual Machine) bytecode.

Let's discover how it all works under the hood on the example of the @RestrictedAccess annotation.

Workflow Explained

This is what the workflow of Access Warden Core looks like:

  1. Wait for an input application JAR to be passed (that is, you first build your application executable).
  2. Create (basically, compile) a new class file in a special dedicated package, we'll refer to it as to checker class. This class is an utility-class — that is, it is final (cannot be inherited from) and has a private default constructor (cannot be instantiated).
  3. Walk through all compiled classes (.class files) within the application executable archive (.jar).
  4. Analyze bytecode in each compiled class (that is, in each already existing class of your application, including all of the library classes shaded in your JAR).
  5. For each method in the analyzed class:
    1. If the method is annotated with @RestrictedAccess, proceed to step 5.ii, else continue to the next method (step 5).
    2. Attempt to form a RestrictedAccess.Configuration object based on the annotation parameters.
    3. If an UnexpectedSetupException is caught, it is logged, and the method is not transformed (continue to step 5). Otherwise (if the configuration seems to be valid), proceed to step 5.iv.
    4. Delete the annotation from the method (if the preserveThisAnnotation parameter is set to false).
    5. Generate a new method with a random name in the checker class, with access flags public, static, and synthetic (to outline the fact the method is generated and should never be manually called). We'll refer to this method as to checker method.
    6. Generate bytecode instructions in the checker method that will resolve the current context at runtime, remove the checker class from this context, and check if the context matches the RestrictedAccess.Configuration object created earlier from the annotation parameters.
    7. Insert bytecode instructions in the beginning of the currently analyzed method that will call the checker method. That is, now, whenever this (analyzed) method will be invoked, the special checker method dedicated to this particular method located in the checker class will be called to check if the current call stack and environment are valid (otherwise a java.lang.SecurityException will be thrown).
    8. Continue to the next method (step 5), if there are still any, or to the next class (step 4), if there are still any, or to step 6 otherwise.
  6. Insert the generated checker class with all the generated special checker methods in the input JAR.
  7. Replace all changed classes (classes with at least one transformed method) in the input JAR (delete old .class files, insert new .class files).
  8. Save the updated input JAR.
  9. Now the build JAR you provided that previously contained "hint" to Access Warden (annotations and other stuff) is working and runnable. And it is now protected against arbitrary unwanted access to critical parts.

Optimizations

Access Warden attempts to generate as minimal code as possible. For example, it will omit configuration builder methods with empty lists (keeping them null internally implies Collections.emptyList() already). Another example is int generation on stack: for a few smallest values, elementary instructions iconst_0, iconst_1, ..., iconst_5 are used; for values in range [Byte.MIN_VALUE; Byte.MAX_VALUE] bipush is used; for values in range [Short.MIN_VALUE; Short.MAX_VALUE] sipush is used; finally, for all other values, ldc is used.

What the Generated Code Looks Like

Consider the following code from the Demo module:

    @RestrictedCall (
            preserveThisAnnotation      = true,
            prohibitReflectionTraces    = true,
            prohibitNativeTraces        = true,
            prohibitArbitraryInvocation = true,
            permittedSources            = "me.darksidecode.accesswarden.demo.AccessWardenDemo#first"
    )
    private static void test(int x) {
        System.out.println(">>> Successful test call, x=" + x);
    }

After transforming the output JAR (in case of the Demo module we're currently looking at, that happens automatically after ./gradlew access-warden-demo:build), this method will looks somewhat like this (decompiled with Procyon):

    @RestrictedCall(preserveThisAnnotation = true, prohibitReflectionTraces = true, prohibitNativeTraces = true, prohibitArbitraryInvocation = true, permittedSources = { "me.darksidecode.accesswarden.demo.AccessWardenDemo#first" })
    private static void test(final int x) {
        __CheckerClass__.__check__334aaca99fc517c0__();
        System.out.println(">>> Successful test call, x=" + x);
    }

Note that the annotation was kept because preserveThisAnnotation was explicitly set to true — it may just sometimes be useful to keep these annotations. In most cases, however, you'll want to be removing them (simply don't set this option to true then). You may notice a method call inserted at the beginning of our test method. This is the call to the checker method generated specifically for this test method.

Now, here's the new class itself (the checker class) that Access Warden created in our application JAR (decompiled with Procyon):

package __access__warden__generated__;

import java.util.Arrays;
import me.darksidecode.accesswarden.api.FilteredContext;
import me.darksidecode.accesswarden.api.UnexpectedSetupException;
import me.darksidecode.accesswarden.api.ContextResolution;
import java.util.Collections;
import me.darksidecode.accesswarden.api.RestrictedCall;

public final class __CheckerClass__
{
    private __CheckerClass__() {
        super();
    }
    
    public static /* synthetic */ void __check__334aaca99fc517c0__() {
        try {
            final RestrictedCall.Configuration conf = RestrictedCall.Configuration.newBuilder().prohibitReflectionTraces(true).prohibitNativeTraces(true).prohibitArbitraryInvocation(true).permittedSources(Collections.singletonList("me.darksidecode.accesswarden.demo.AccessWardenDemo#first")).build();
            final FilteredContext ctx = ContextResolution.resolve(conf.contextResolutionOptions());
            if (ctx.filteredCallStack().size() <= 1) {
                throw new UnexpectedSetupException("filtered call stack is unexpectedly small");
            }
            ctx.filteredCallStack().remove(0);
            ContextResolution.ensureCallPermitted(ctx, conf);
        }
        catch (UnexpectedSetupException ex) {
            throw new SecurityException("unexpected setup: " + ex.getMessage());
        }
    }
    
    public static /* synthetic */ void __check__3ee730bef7c9dfb0__() {
        try {
            final RestrictedCall.Configuration conf = RestrictedCall.Configuration.newBuilder().prohibitedSources(Arrays.asList("me.darksidecode.accesswarden.demo.AccessWardenDemo#prohibitTest*", "me.darksidecode.accesswarden.demo.AccessWardenDemo#otherProhibitedTest")).build();
            final FilteredContext ctx = ContextResolution.resolve(conf.contextResolutionOptions());
            if (ctx.filteredCallStack().size() <= 1) {
                throw new UnexpectedSetupException("filtered call stack is unexpectedly small");
            }
            ctx.filteredCallStack().remove(0);
            ContextResolution.ensureCallPermitted(ctx, conf);
        }
        catch (UnexpectedSetupException ex) {
            throw new SecurityException("unexpected setup: " + ex.getMessage());
        }
    }
}

This class contains checker methods for all methods that we annotated with @RestrictedAccess (in our Demo application, there are two such methods — hence the two checker methods). These methods make use of the low-level API module to inspect the current call stack and environment at runtime and decide whether the current invocation should be allowed (passed), or prohibited, according to the configuration we provided for this particular method in @RestrictedAccess parameters.

Obfuscation

Access Warden can work fine with obfuscated applications. To avoid messing with class names transformations and make everything as easy as possible, it is recommended that you apply Access Warden after you obfuscate your program. Since Access Warden itself generates random, meaningless names, it will not make your code any more vulnerable or disclose any private information.

However, if for some reason you have to use Access Warden before obfuscating your application executable, make sure that your obfuscation software can handle class and method renaming in Strings, or set it up the way it is capable of doing so. If you don't ensure that, Access Warden will be unable to inspect the call stacks properly at runtime, since classes that it was configured to work with, say, "com.mycompany.MyApp" turn into "a.b.c" at runtime (which means that Access Warden will check strack traces for classes that no longer exist because they were renamed by the obfuscator).