Exploiting Perl 6 Code From Down The Dependency Chain

Read this article on Perl6.Party

DISCLAIMER: data theft is a serious crime in many jurisdictions. The author does not condone or encourage anyone to break laws. The information provided here is for educational purposes only.

Back when I wrote about exploiting operators made of invisible Unicode characters, a bunch of folks pointed out the module containing rogue code would actually have to be imported by the victim and it's not that easy to convince them. Fair enough. Today, we'll play a new game and crank it up a notch!

The Game Plan

We all worked on codebases that relied on a dozen of open-sourced modules, each of which relied on a few more, which in turn relied on... well, you get the point. Somewhere, a gazillion levels deep, there's some type of a leftpad in use that you never reviewed for sanity. Right?

So here's our setup:

+--------------+     +-------------+    +------------+
| Rightpad.pm6 |  -> | GoodGuy.pm6 | -> | Target.pm6 |
+--------------+     +-------------+    +------------+

Target.pm6 is some code we want to steal stuff from. It uses GoodGuy.pm6 to bring in some functionality, and somewhere down GoodGuy.pm6's dependency chain there's Rightpad.pm6 that we'll use to inject rogue code into and hope no one will notice.

In particular, we have these goals:

  1. Inject code that will steal data that's being operated on by Target.pm6. For our purposes, a simple print out of the hijacked data will be our "exploit." The code can be anything we want really, something that logs to a file or connects to a server to send the data to a remote endpoint.
  2. The exploit has to be as invisible as possible. We're free to modify Rightpad.pm6 any way we want, but Target.pm6's and GoodGuy.pm6's code has to remain exactly the same, functioning, and with as few signs of foul play as possible.

Augmented Reality

The first thing one might think about when trying to change stuff in the entire app is core type augmentation. Let's try it out:

use MONKEY-TYPING;
augment class Str { method uc {} }

# OUTPUT:
# ===SORRY!=== Error while compiling /home/zoffix/CPANPRC/temp.p6
# Package 'Str' already has a method 'uc' (did you mean to declare
# a multi-method?)
# at /home/zoffix/CPANPRC/temp.p6:2

We enable the MONKEY-TYPING pragma to acknowledge we're doing Dangerous Things™ and then try to replace an already-existing uc method on Str type.

Oops! We get an error saying it already exists and we can't replace it. However, we do get a hint for how we can cheat: a multi. We just have to get creative and make a multi that'll hijack the dispatch to the method we're trying to... um... hijack.

use MONKEY-TYPING;
augment class Str {
    multi method uc (Str $str where * :) {
        say "We're in! Someone's trying to uppercase $str";
        nextsame;
    }
}

say 'foo'.uc;

# OUTPUT:
# We're in! Someone's trying to uppercase foo
# FOO

It worked! We prefix the method definition with multi to indicate it's a multi-dispatch method. For the invocant, we add a type constraint with where in the signature, except the condition is just the Whatever Star, which will always match any Str. The benefit is the dispatch will actually pick this method first, as the type constraint indicates that it should be narrower than normal Str.

Lastly, to retain the original functionality of the method we're hijacking, we simply use nextsame to re-dispatch to the next multi, which would be the original method that will make the target code behave as it was meant to.

Mixing It Up

Augmentation is great, but it's a blunt tool. We can get more precision by mixing in roles into core objects:

'foo' does role {
    method uc {
        say "We're in! Someone's trying to uppercase {self}";
        nextsame;
    }
};

say 'foo'.uc;

# OUTPUT:
# We're in! Someone's trying to uppercase foo
# FOO

Here, we're doing a runtime mixin with an anonymous role that plugs in its own version of the uc method. Since this isn't a compile-time composition, the role doesn't get "inserted" into the class, but rather shadows the original uc method, so we can still use nextsame to provide the original behaviour.

Now, the problem with the above is (a) we already know the content of the object is 'foo'; and (b) this won't actually cross the module boundary as the 'foo' in Target.pm6 will be a different beast, so we can't use it for our game. However, there are objects we can mix a role into in a useful way; for example, subroutines.

&say does role {
    method CALL-ME (|c){
        put "We're in! Someone's trying to print {c}";
        put |c;
    }
};

say 'foo';

# OUTPUT:
# We're in! Someone's trying to print BE7457CAF9032DF8A21D7E50B29D75AE5E581F84 /tmp/perl6.party/Rightpad.pm6
# We're in! Someone's trying to print foo
# foo

We intercept the call to say by defining a CALL-ME method on the appropriate subroutine object and use a Capture for the signature. (The first line in the output is from precompilation, if anyone was curious).

Since most of the core subs don't use CALL-MEs but are natively invokable, we can't nextsame to get the original functionality. In the above version we attempted to replicate it, but a sharp eye will notice that put is way different than say. Is there a way to not re-invent the wheel?

Perl 6's but operator creates a copy of an object and mixes a role into it. Let's use that feature to our advantage:

my &original-say = &say but role {};
&say does role {
    method CALL-ME (|c){
        original-say "We're in! Someone's trying to print {c}";
        original-say |c;
    }
};

say -∞..∞;

# OUTPUT:
# We're in! Someone's trying to print *..*
# -Inf..Inf

We use the but operator to mix in an empty role into the sub object handling say and store it in a variable. Since but creates a copy, we can then mangle the say without affecting the saved core version. We do it the same way as last time, with a mixin, and this time we call the saved say to perform the core function. And there is it! Hijacked core sub without a need to re-implement it.

Wrapping It Up

While you can do some damage with mixins on core objects, when it comes to subs, there's a smaller hammer for the job: wrap.

&say.wrap: sub (|c) {
    put "We're in! Someone's trying to print {c}";
    nextsame;
};

say -∞..∞;

# OUTPUT:
# We're in! Someone's trying to print *..*
# -Inf..Inf

That looks a lot simpler than the mixin version! We call .wrap on the Sub we want to wrap into our malicious wrapper and we get the benefit of being able to call nextcase to perform the original function. Note that we can't use say inside our wrapper, or we'll cause an infinite loop of it attempting to call the wrapper once again.

Putting the bits together

And so, we have this tree of modules and scripts:

.
├── Rightpad.pm6
├── GoodGuy.pm6
└── test.p6

And here's the code in them:

# Rightpad.pm6
unit class Rightpad;
use MONKEY-TYPING;

&say.wrap: sub (|c) {
    put "We're in! Someone's trying to print {c}";
    nextsame;
};

augment class Str {
    multi method uc (Str $str where * :) {
        put "We're in! Someone's trying to uppercase $str";
        nextsame;
    }
}

# GoodGuy.pm6
unit class GoodGuy;
use Rightpad;

# test.p6
use GoodGuy;
say 'foo'.uc;

And here's the invocation and output:

$ perl6 -I. test.p6
We're in! Someone's trying to uppercase foo
We're in! Someone's trying to print FOO
FOO

Rightpad.pm6 doesn't need to be so close in the dependency chain, it can be hidden behind dozens and dozens of dependencies. Our script uses GoodGuy.pm6 and doesn't have anything but a core function call and a core method call. However, due to the code we enjected in Rightpad.pm6, we were able to intercept the data in both of those calls.

Conclusion

While this exercise was a lot of fun, it did bring to light the reality that we can't trust dependencies too much. Even if the malicious code is several dependencies away, it can still affect our code and steal our data.

Why would such code exist? Revenge. Plans for world domination. Or even good ol' carelessness and ignorance.

Footnotes

* while writing the blog post, I came across a bug with .wrap where it caused a segfault when trying to wrap across two module boundaries. This can be worked around by putting no precompilation in the module with the wrap and may be already fixed when you read this.

1 Comment

Very interesting--and scary! Have you any ideas for hardening Perl 6 to prevent such exploits?

Leave a comment

About Zoffix Znet

user-pic I blog about Perl.