r/haskell • • Jul 16 '11

Reverse Engineering of Compiled Haskell

A client of mine is considering use of Haskell for some software that will be deployed in the field. One of his concerns (possibly a concern upstream from him) is that the "lower levels" of the code "block users from easily seeing how it works." By that I'm assuming they mean looking at byte code or disassembled machine code. Would Haskell's compiled output be any better or worse than C or C++ in this respect?

edit: Thanks all for the very helpful responses.

It sounds like a good summary of this topic is: GHC's output, stripped, is more difficult to reverse engineer than that of a similar program written in C or C++. That is due to the increased indirection, different idioms, and lack of interest and tools. Interest in reversing GHC's output could increase in the future and reversing could become easier.

Of course any useful code that can run on a CPU can be reverse engineered. This question was more about comparing the reversing effort to C and C++.

19 Upvotes

19 comments sorted by

17

u/godofpumpkins Jul 16 '11 edited Jul 16 '11

GHC by default puts a crapload of (moderately human-readable) symbols into binaries, and stripping them will typically reduce the binary size (and reversibility) significantly. Once the symbols are gone, I think Haskell is mostly pretty reverser-proof right now because it looks like nothing most reversers are used to reading, and there's so much indirection for everything. People get frustrated with C++ because of the virtual function calls, and GHC's output has way more of those indirect function calls. Also, most tools that reversers use make assumptions about generated code (stack layouts, function preludes, etc.) that GHC doesn't follow.

I don't think it'd be impossible for an experienced reverser to figure out, but they might run away screaming before they get down to understanding it. Also, the intersection between the binary analysis community and the people who have heard of Haskell (and especially those who use it regularly) is tiny, sadly.

15

u/godofpumpkins Jul 17 '11

Just to add to that, I've done a lot of reverse engineering in the past and am still fairly involved in at least a couple of related communities. What I've gathered from it is that most reversers are pretty mediocre, and do most of their reversing by extrapolating from bits and pieces of human-readable information left in programs. Even experienced reversers will be hugely relieved if the information is still in the program.

For example, many companies (including certain fruity ones) obviously use macros that include the path, function name, and line number of an error message. So because of this, even if they strip binaries of symbols, the error messages (even ignoring the actual message, which itself is also hugely valuable) contain a treasure trove of information on the original structure of the program. We can then go into IDA and write a quick script to harvest the strings with a regex and name all functions with error calls automatically. Once you have names, things generally make a lot more sense.

One of the reasons the jailbroken iOS hombrew development community is so active (to the point of several people actually making very good livings writing "tweaks" for it) is that it's really easy to reverse the vast majority of programs on iOS, due to their use of Objective-C. As a smalltalk-like messaging language that uses string-based messages (called selectors), most programs are basically forced to leave all their function (fine, method) names in their programs, because dispatch can be dynamic. Even better, due to highly dynamic nature of Objective-C (and some questionable early design decisions that nobody at all except for the reversers probably takes advantage of now), all these nicely named methods also contain type signatures (often fairly detailed, sometimes even including struct definitions). Again, the binary and runtime structure of Objective-C prevents any of this from being removed from the program, so it's more information for a reverser to incorporate into his knowledge about the program. They even have tools that dump complete and valid header files for any Objective-C-compiled binary, using this information.

The approach employed by some clever obfuscators on Objective-C is to post-process (even just by hashing) method names to be garbage. The garbage method names will still be in the binary, but any human trying to read it will have nothing to rest on. Even with other strings and error messages left in the program, I suspect that simple step would kill 90% of the developers writing software for jailbroken iOS right now.

I've experimented with obfuscating C with a few simple functional techniques, like doing pseudo-CPS on my functions (nested functions don't work on Apple's ARM compiler, but they have newfangled block things that do, and make it fairly pleasant to program in that style). The general reaction from many experienced reversers I know contained many expletives. Even the kinds of reversers who know what to do when there are no strings in a program generally haven't come across functional languages and concepts like the lazy thunks (almost no constant function branches/calls anywhere in the program) or CPS used by Haskell compilers.

Anyway, I guess this long rant is to show you that there are two main categories of reversers in my experience, and the larger one would be mostly clueless if there were no strings in the program (and given the indirect representation of everything in Haskell, even strings might be hard to track back to their use location). The other one doesn't intersect much with the PL crowd (I've recently gotten a few of my reversing buddies into Haskell and they love it, so that may change), and is thus ignorant of how functional languages work and are compiled.

tl;dr: I wouldn't worry too much about people reversing compiled Haskell for now. A few people can probably do it, but they're few and far between, and probably have better things to do.

7

u/ezyang Jul 16 '11

It's different. GHC has its own calling convention, and uses a very different set of idioms than you would expect from a traditional library. I don't recommend looking at GHC-produced assembly unless you also have Core, STG and C-- at hand. Fortunately, these are pretty easy to produce during compilation.

5

u/[deleted] Jul 16 '11

The rule is: If they have access to your binary, they will figure out what it does if they really want to.

3

u/drb226 Jul 16 '11

If they have access to your binary

In other words, don't distribute binaries; execute the code on your own servers if you are super worried about this. The only way to truly obfuscate your code is to make it so even the machine can't understand it; but that sort of defeats the purpose of programming.

1

u/f2u Jul 16 '11

You have to be a bit careful about identifiers ending up in the binary, or that the business logic ends up in tables which do not actually need disassembly. (That's relevant to C++, too.) GHC does not use the hardware stack, so a stock disassembler/decompiler will be very confused. That and the different evaluation model are not major obstacles, merely deterrents, but it's not as trivial as decompiling Java.

One thing that makes me wary of putting Haskell code in the field is the lack of crash dumps.

2

u/darrint Jul 16 '11

By lack of crash dumps do you mean the equivalent of a core file for a C program? That I could use that core file and gdb to diagnose a problem?

1

u/clockfort Jul 17 '11

I must admit I've never used the functionality, but the GHC man page does seem to make mention of core files' existence.

0

u/[deleted] Jul 17 '11

2

u/f2u Jul 17 '11

The core file is just a bit difficult to interpret, even for bugs which do not involve memory corruption or race conditions. There aren't any useful debugging symbols, either. That's the flip side of confusing disassemblers.

2

u/MatrixFrog Jul 16 '11

I really think if you don't want people reverse-engineering your code, the solution should be legal, not technical.

1

u/mosha48 Jul 16 '11

jhc's assembly output might be easier to read than ghc's.

0

u/[deleted] Jul 16 '11

3

u/clockfort Jul 16 '11

Whoah, small world. Reversing binaries -- malware, in particular, is a pretty big part of my job too :-P And I love Haskell!

GHC moving to LLVM stuff aside, I think if I wanted to reverse GHC's normal platform-specific binaries, I would take a look at GHC's mangler perl script for those platforms it supports, and read up on the internal binary structures and calling syntax, to get a good delta from what is already well known (i.e. GCC-styled C binaries).

10

u/[deleted] Jul 16 '11

I would take a look at GHC's mangler perl script

God be with you.

→ More replies (3)

0

u/[deleted] Jul 16 '11

6

u/[deleted] Jul 16 '11

Sorry, but your assertions about what the compiler will do are wrong. GHC does not compute the result of a function at compile time (except for some type-level functions).

5

u/augustss Jul 16 '11

Well, it might compute a call at compile time if it decides to inline the function. Just like a C++ compiler might.