2009-03-12

polyglot: bash me harder

reminder: this article is part of a serie.

remember when we inserted the opening pascal comment in previous episode? we used characters that are also valid in bash... (uh, sorry, i meant this bash :-) ) indeed, ( starts a subshell, and * is of course the globbing operator. there definitely is potential to sneak in some bashism! especially since bash is interpreted, and thus we can exit in the middle of the file without even needing to have the rest of the file be bash-compliant!

ok, so how can we achieve that? well, *foo will either report an error if no program matching *foo exist in the current directory, or run the program matching *foo if there is at least one. this is not what we want, but we have an asset: we can discard everything produced, including the errors, by trapping stdout and stderr! and then, we can run our real commands...

but before doing that, we need to be cautious with the c program. so the first thing that we're going to do is to somehow comment a portion of code wrt c (which will be understood as command arguments by bash). note that we cannot use a cpp instruction here, since the hash sign is also used to comment in bash. so, let's open a plain c comment, without forgetting to close it before the closing parens:


(*foo /* */
);
[...]


however, this breaks perl. we need to come up with something... well, let's take things in order: / is the division in perl. dividing a glob doesn't make much sense, but we just want to quiet the syntax errors - we don't care about the semantics. * is the glob operator, but it is missing a varname - let's fix that:


(*foo /*bar */
);
[...]


but now, perl continues to parse, using the * as a multiplication operator, and the / as a regex opening. but then, a closing paren is not valid, since a regex needs to have balanced parens. that's not good, so let's cheat and do some hocus pocus comment thingy:


(*foo /*bar
); # */ );
[...]


we're not using any more the same closing paren in perl and c - but who cares? perl compiles correctly, but we have an "illegal division by zero" during runtime. sigh. indeed, *bar is understood as $bar (scalar context). and $bar in numeric context is 0, since it's undef. man, this is getting annoying... let's use perl builtin optimiser to really skip this instruction:


(*foo /*bar
) if 0; # */ );
[...]


there! finally, we're back to a working c + pascal + perl program. and this time, we may have some room to grow bash instructions... but before that, we need to close the subshell and discard its output - knowing that we should hide it from perl:


(*foo /*bar
#) 2>/dev/null;exit
) if 0; # */ );
[...]


alas, this does not work since # also hides the line to bash. so let's just start an instruction (which includes the bang), and this time the closing paren will be seen. however, we need to have something also valid from the perl point of view - remember that we're inside a mathematical expression. so we must have a math operator be part of the command. and which operator is better than the division, since it's the path separator in unix? there, we'll divide by the leet number for perl while calling non-existant program /1337# (hmm, there should exist such a program):


(*foo /*bar
/1337#) 2>/dev/null;exit
) if 0; # */ );
[...]


our tests pass, and bash doesn't throw errors anymore! let's just insert the bash fibonacci instructions between the /dev/null redirection and the exit. (who said we're limited to 80 columns? :-) ) our program is now complete:


(*foo /*bar
/1337#) 2>/dev/null;i=0; a=1; b=1;echo $a;while test $i -lt 9;do c=$((a+b));a=$b;b=$c;echo $a;i=$((i+1));done;exit
) if 0; # */ );

#include <stdio.h>
#include <stdlib.h>

#define $ /*
"*/
main () /*"; # */
{
int $ i;
int $ n1;
int $ n2;
int $ n3;
$ i = 0;
$ n1 = 1;
$ n2 = 1;
printf( "%d\n", $ n1 );
while ( $ i < 9 ) {
$ n3 = $ n1 + $ n2;
$ n1 = $ n2;
$ n2 = $ n3;
printf( "%d\n", $ n1 );
$ i++;
}
}

#define foo /*
__END__
*)

program foo;
var
i : integer;
n1 : integer;
n2 : integer;
n3 : integer;

begin
i := 0;
n1 := 1;
n2 := 1;
writeln(n1);
while i < 9 do
begin
n3 := n1 + n2;
n1 := n2;
n2 := n3;
writeln(n1);
i := i + 1;
end;
end.

(* */
#define bar *)


and it passes our internal tests, including the new bash one:


$ prove -l t
t/bash......ok
t/c.........fibonacci.c:1: warning: data definition has no type or storage class
t/c.........ok
t/pascal....ok
t/perl......ok
All tests successful.
Files=4, Tests=4, 1 wallclock secs ( 0.02 usr 0.00 sys + 0.20 cusr 0.04 csys = 0.26 CPU)
Result: PASS


pfeww, is it me or is it getting harder? :-) but don't worry, we're not finished yet! but this will have to wait a bit...

No comments:

Post a Comment