tryerlang.org is an Interactive Erlang Shell which allows you to try the power of Erlang directly in your browser, without installing anything in your machine. In the first months of his existence, tryerlang.org has been subjected to a countless number of attacks, aiming at bringing the Erlang node down. Studying the tryerlang.org’s logs has been so far an highly interesting and constructive experience.
In this blog post I will present one of the attempted attacks. The attack was based on the concept of External Term Representation for Erlang, which is introduced in the next section.
External Term Representation
In the distribution mechanism of Erlang, Erlang terms are converted into binaries before being sent to a remote host and then converted back into Erlang terms at the destination. The built-in functions
binary_to_term are used for the conversions.
More information about the External Term Representation are available in the official Erlang Documentation. How the External Term Representation has been used to perform the attack is explained in the next section.
Halting the Erlang Node
The user wants to bring the Erlang node down. He then tries to use the
erlang:halt/0 function. This function, documented here, halts the Erlang runtime system and indicates normal exit to the calling environment. It has no return value. The function has been disabled in tryerlang.org for security reasons, so the only result the user get is the following message:
“This functionality has been disabled for security reasons in tryerlang.org.”.
So far, so good. The Erlang node is still up. The user thinks for a while and then he notices that tryerlang.org allows you to define funs. Here is where the External Term Representation can help. According to the documentation, it is possible to encode an external fun – something like
fun M:F/A – in the following way:
113 | Module | Function | Arity
The encoding for the atoms looks like:
100 | Len | AtomName
Len is the length of
AtomName, expressed in two bytes.
Finally, the encoding for the small integers looks like:
97 | Int
The last thing we have to consider is that, in the External Term Representation, the byte
131 needs to be prepended to the term. Now that we have all the required knowledge to do it, let’ try to encode the
erlang:halt/0 function using the External Term Representation and let see if we can fool tryerlang.org!
We could write the binaries for the atoms erlang and halt by hand, but it’s actually more handy to use the
term_to_binary/1 BIF to do it for us. Since the function is blacklisted in tryerlang.org, let’s use our own shell.
Eshell V5.8.1 (abort with ^G)
Skipping the initial
131 byte (see above) and concatenating the other two obtained terms, we have:
131 (prefix for all terms) and
113 (prefix for external funs) bytes and appending the arity integer, we have:
We should have the binary representation of the external fun
erlang:halt/0. Let’s check it!
Let’s now take this binary from our shell and let’s paste it in tryerlang.org. Oh, I forgot that the online shell doesn’t allow copy and paste (what a shame!), so we have to fill the whole binary sequence by hand…
After a couple of minutes of struggling against typos and errors, here’s our wonderful fun which we can bind to a new variable:
>B = <<131,113,100,0,6,101,114,108,
We now need to convert the binary into an Erlang term. Originally, tryerlang.org was allowing the binary_to_term function in safe mode. This function has been now completely disabled after this attack. If you want to try what follows you will need to do it in your own Erlang shell.
>F = binary_to_term(B, [safe]).
Let’s now try to launch the fun as:
Well, that didn’t work as expected. tryerlang.org actually realized that the
erlang:halt/0 function was going to be called and he managed to block it. We need something different.
What would happens if we embed the
halt function in another function call, say a
list:map/2? The only problem would be that we need a parameter in our halt function. Fortunately an alternative version of
erlang:halt/0 exists, taking exactly one argument. Let’s create an external representation for it. We just need to change the last element from 0 to 1. The BIF
f/1 forgets the value of a bounded variable.
> B = <<131,113,100,0,6,101,114,
Now we should be able to do…
>F = binary_to_term(B, [safe]).
And the node dies.
Actually the node is almost immediately brought back by heart but, hey, I have to pay a beer to this guy! :)
Please note that the hacker had the advantage to look at the source code for tryerlang.org while performing the attack.
I wanted to share this experience with all of you. I consider it highly constructive, since it leads to reflect on several aspects of Erlang. Comments and feedback are more than welcome.