Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable/Avoid an advice execution in AspectJ

Tags:

java

aop

aspectj

Suppose I have an aspect

public aspect Hack {

pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

boolean around(String user, String pass): authHack(user,pass) {
    out("$$$ " + user + ":" + pass + " $$$");
    return false;
}

}

The Authenticator.authenticate method is important. The hack intercepts calls to this method.

Is it possible to write a second aspect that cancels/disables the authHack advice of Hack aspect?

I can catch the execution of the around authHack advice, but if I want to continue the authentication i need to call Authenticator.authenticate again and this creates an infinite loop..

like image 428
emesx Avatar asked Feb 03 '23 06:02

emesx


2 Answers

In order simulate your situation, I had written the following Authenticator code:

public class Authenticator {

    public boolean authenticate(String user, String pass) {
        System.out.println("User: '" + user + "', pass: '" + pass + "'");
        return true;
    }

}

This is my Main class:

public class Main {

    public static void main(String[] args) {

        Authenticator authenticator = new Authenticator();

        boolean status = authenticator.authenticate("Yaneeve", "12345");
        System.out.println("Status: '" + status + "'");
    }

}

output is:

User: 'Yaneeve', pass: '12345'
Status: 'true'

I added your Hack aspect:

public aspect Hack {

    pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

    boolean around(String user, String pass): authHack(user,pass) {
        System.out.println("$$$ " + user + ":" + pass + " $$$");
        return false;
    }
}

Now the output is:

$$$ Yaneeve:12345 $$$
Status: 'false'

Now for the solution:

I had created the following HackTheHack aspect:

public aspect HackTheHack {

    declare precedence: "HackTheHack", "Hack";

    pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

    boolean around(String user, String pass): authHack(user,pass) {
        boolean status = false;
        try {
            Class<?> klass = Class.forName("Authenticator");
            Object newInstance = klass.newInstance();
            Method authMethod = klass.getDeclaredMethod("authenticate", String.class, String.class);
            status = (Boolean) authMethod.invoke(newInstance, user, pass);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return status;
    }
}

Output is again:

User: 'Yaneeve', pass: '12345'
Status: 'true'

This only works if the original pointcut in Hack aspect was 'call' and not 'execution' as execution actually catches reflection.

Explanation:

I used Aspect precedence to invoke HackTheHack before Hack:

declare precedence: "HackTheHack", "Hack";

I then used reflection (note can and should be optimized to reduce the repeating lookup of the method) to simply invoke the original method without the Hack around advice. This had been made possible due to 2 things:

  1. the authHack pointcut: pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass); uses (in both aspects) call() instead of execution()
  2. I did not call proceed() in HackTheHack

I would like to refer you to Manning's AspectJ in Action, Second Edition which had put me on the right track with:

6.3.1 Ordering of advice

As you’ve just seen, with multiple aspects present in a system, pieces of advice in the different aspects can often apply to a single join point. When this happens, AspectJ uses the following precedence rules to determine the order in which the advice is applied. Later, you’ll see how to control precedence:

1 The aspect with higher precedence executes its before advice on a join point before the aspect with lower precedence.

2 The aspect with higher precedence executes its after advice on a join point after the aspect with lower precedence.

3 The around advice in the higher-precedence aspect encloses the around advice in the lower-precedence aspect. This kind of arrangement allows the higher- precedence aspect to control whether the lower-precedence advice will run by controlling the call to proceed(). If the higher-precedence aspect doesn’t call proceed() in its advice body, not only will the lower-precedence aspects not execute, but the advised join point also won’t execute.

like image 90
Yaneeve Avatar answered Feb 12 '23 09:02

Yaneeve


Actually user @Yaneeve has presented a nice solution, but it has a few shortcomings, e.g. it

  • only works for call(), not for execution(),
  • needs reflection,
  • needs declare precedence,
  • needs to know the hack's class and package name beforehand (okay, that can be circumvented by using * in precedence declaration).

I have a more stable solution for you. I have modified the source code to be a little bit more realistic:

Authenticator:

The authenticator has a user database (hard-coded for simplicity) and actually compares users and passwords.

package de.scrum_master.app;

import java.util.HashMap;
import java.util.Map;

public class Authenticator {
    private static final Map<String, String> userDB = new HashMap<>();

    static {
        userDB.put("alice", "aaa");
        userDB.put("bob", "bbb");
        userDB.put("dave", "ddd");
        userDB.put("erin", "eee");
    }

    public boolean authenticate(String user, String pass) {
        return userDB.containsKey(user) && userDB.get(user).equals(pass);
    }
}

Application:

The application has an entry point and tries to authenticate a few users, printing the results:

package de.scrum_master.app;

public class Application {
    public static void main(String[] args) {
        Authenticator authenticator = new Authenticator();
        System.out.println("Status: " + authenticator.authenticate("alice",  "aaa"));
        System.out.println("Status: " + authenticator.authenticate("bob",    "xxx"));
        System.out.println("Status: " + authenticator.authenticate("dave",   "ddd"));
        System.out.println("Status: " + authenticator.authenticate("erin",   "xxx"));
        System.out.println("Status: " + authenticator.authenticate("hacker", "xxx"));
    }
}

The application's output is as follows:

Status: true
Status: false
Status: true
Status: false
Status: false

Authentication logger aspect:

I want to add a legal aspect with an around() advice on the authentication method, just like the hacking aspect later.

package de.scrum_master.aspect;

import de.scrum_master.app.Authenticator;

public aspect AuthenticationLogger {
    pointcut authentication(String user) :
        execution(boolean Authenticator.authenticate(String, String)) && args(user, *);

    boolean around(String user): authentication(user) {
        boolean result = proceed(user);
        System.out.println("[INFO] Authentication result for '" + user + "' = " + result);
        return result;
    }
}

The output becomes:

[INFO] Authentication result for 'alice' = true
Status: true
[INFO] Authentication result for 'bob' = false
Status: false
[INFO] Authentication result for 'dave' = true
Status: true
[INFO] Authentication result for 'erin' = false
Status: false
[INFO] Authentication result for 'hacker' = false
Status: false

As you can see, "status" and "authentication result" are the same as long as the system was not hacked. No surprise here.

Hacker aspect:

Now let us hack the system. We can either always return true (positive authentication result) or always true for a certain user - whatever we like. We can even proceed() to the original call if we want to have its side effects, but we can still always return true, which is what we do in this example:

package de.scrum_master.hack;

import de.scrum_master.app.Authenticator;

public aspect Hack {
    declare precedence : *, Hack;
    pointcut authentication() :
        execution(boolean Authenticator.authenticate(String, String));

    boolean around(): authentication() {
        System.out.println("Hack is active!");
        proceed();
        return true;
    }
}

The output changes to:

Hack is active!
[INFO] Authentication result for 'alice' = true
Status: true
Hack is active!
[INFO] Authentication result for 'bob' = true
Status: true
Hack is active!
[INFO] Authentication result for 'dave' = true
Status: true
Hack is active!
[INFO] Authentication result for 'erin' = true
Status: true
Hack is active!
[INFO] Authentication result for 'hacker' = true
Status: true

Because the hacker aspect declares itself to be the last one in advice precedence (i.e. the innermost shell in a nested series of proceed() calls on the same joinpoint, its return value will be propagated up the call chain to the logger aspect, which is why the logger prints the already manipulated authentication result after it has received it from the inner aspect.

If we change the declaration to declare precedence : Hack, *; the output is as follows:

Hack is active!
[INFO] Authentication result for 'alice' = true
Status: true
Hack is active!
[INFO] Authentication result for 'bob' = false
Status: true
Hack is active!
[INFO] Authentication result for 'dave' = true
Status: true
Hack is active!
[INFO] Authentication result for 'erin' = false
Status: true
Hack is active!
[INFO] Authentication result for 'hacker' = false
Status: true

I.e. the logger now logs the original result and propagates it up the call chain to the hacker aspect which can manipulate it at the very end because it is first in precedence and thus in control of the whole call chain. Having the final say is what a hacker would usually want, but in this case it would show a mismatch between what is logged (some authentications true, some false) and how the application actually behaves (always true because it was hacked).

Anti hacker aspect:

Now, last but not least we want to intercept advice executions and determine if they might come from possible hacker aspects. The good news is: AspectJ has a pointcut called adviceexecution() - nomen est omen. :-)

Advice execution join points have arguments which can be determined via thisJoinPoint.getArgs(). Unfortunately AspectJ cannot bind them to parameters via args(). If the intercepted advice is of around() type, the first adviceexecution() parameter will be an AroundClosure object. If you call the run() method upon this closure object and specify the correct arguments (which can be determined via getState()), the effect is that the actual advice body will not be executed but only implicitly proceed() will be called. This effectively disables the intercepted advice!

package de.scrum_master.aspect;

import org.aspectj.lang.SoftException;
import org.aspectj.runtime.internal.AroundClosure;

public aspect AntiHack {
    pointcut catchHack() :
        adviceexecution() && ! within(AntiHack) && !within(AuthenticationLogger);

    Object around() : catchHack() {
        Object[] adviceArgs = thisJoinPoint.getArgs();
        if (adviceArgs[0] instanceof AroundClosure) {
            AroundClosure aroundClosure = (AroundClosure) adviceArgs[0];
            Object[] closureState = aroundClosure.getState();
            System.out.println("[WARN] Disabling probable authentication hack: " + thisJoinPointStaticPart);
            try {
                return aroundClosure.run(closureState);
            } catch (Throwable t) {
                throw new SoftException(t);
            }
        }
        return proceed();
    }
}

The resulting output is:

[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'alice' = true
Status: true
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'bob' = false
Status: false
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'dave' = true
Status: true
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'erin' = false
Status: false
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'hacker' = false
Status: false

As you can see,

  • the result is now the same as without the hacker aspect, i.e. we effectively disabled it,
  • it was not necessary to know the hacker aspect's class or package name, but in our catchHack() pointcut we specify a white list of known aspects which should not be disabled, i.e. run unchanged,
  • we are only targeting around() advice because before() and after() advice have signatures without AroundClosures.

Anti hacker advice with target method heuristics:

Unfortunately I found no way to determine the method targeted by the around closure, so there is no exact way to limit the anti hacker advice's scope to advice specifically targeting the method we want to protect against hacking. In this example we can narrow down the scope by heuristically checking the content of the array returned by AroundClosure.getState() which consists of

  • the advice's target object as the first parameter (we need to check if it is an Authenticator instance),
  • the target method call's parameters (for Authenticator.authenticate() there must be two Strings).

This knowledge is undocumented (just like the contents of advice execution's arguments), I found out by trial and error. Anyway, this modification enables the heuristics:

package de.scrum_master.aspect;

import org.aspectj.lang.SoftException;
import org.aspectj.runtime.internal.AroundClosure;

import de.scrum_master.app.Authenticator;

public aspect AntiHack {
    pointcut catchHack() :
        adviceexecution() && ! within(AntiHack) && !within(AuthenticationLogger);

    Object around() : catchHack() {
        Object[] adviceArgs = thisJoinPoint.getArgs();
        if (adviceArgs[0] instanceof AroundClosure) {
            AroundClosure aroundClosure = (AroundClosure) adviceArgs[0];
            Object[] closureState = aroundClosure.getState();
            if (closureState.length == 3
                    && closureState[0] instanceof Authenticator
                    && closureState[1] instanceof String
                    && closureState[2] instanceof String
            ) {
                System.out.println("[WARN] Disabling probable authentication hack: " + thisJoinPointStaticPart);
                try {
                    return aroundClosure.run(closureState);
                } catch (Throwable t) {
                    throw new SoftException(t);
                }
            }
        }
        return proceed();
    }
}

The output stays the same as above, but if there are multiple advice in the hacker aspect or even multiple hacker aspects you will see the difference. This version is narrowing down the scope. If you want this or not is up to you. I suggest you use the simpler version. In that case you only need to be careful to update the pointcut to always have an up to date whitelist.

Sorry for the lenghty answer, but I found the problem fascinating and tried to explain my solution as good as possible.

like image 38
kriegaex Avatar answered Feb 12 '23 07:02

kriegaex