Inline Jump Tables on ARM & Function Detection Using The .eh_frame Section

Our recent USENIX Sec’16 paper on x86/x64 disassembly has been getting a fair amount of attention on twitter and reddit, which is great to see! I’ve also talked to a few people who had some interesting additional insights which are not included in the paper. I thought it might be interesting to share them here.

Inline jump tables on ARM

One of our main findings is that on x86/x64, both gcc v5.1 and clang v3.6 are extremely well-behaved when it comes to jump tables. Rather than placing these inline in the .text section, both compilers place jump tables in .rodata. They emit no inline data at all, which means that linear disassembly produces 100% correct results.

It seems things are not quite so convenient on ARM. Apparently, arm-linux-gcc does produce inline jump tables (just like Visual Studio does for x86/x64). A quick check confirms this, as illustrated in the listing below, which shows a snippet of objdump output for lighttpd cross-compiled with arm-linux-gcc.

Indeed, we do see some inline data (the .word lines), and it looks like a jump table. You can see that it contains an array of valid addresses, presumably pointing to the case blocks of a switch. The DWARF information tells us the inline data is produced from somewhere near line 95 in server.c, shown in the following listing.

Clearly, this is indeed a switch statement, and the inline data is a jump table containing the addresses of the case blocks. Fortunately, if the binary is not stripped, then objdump can use symbols to differentiate the data from the code. This is the reason why objdump, in our test, is able to accurately mark the data as .word lines. However, if the binary is stripped, this is no longer possible, and the inline data will cause disassembly errors.

(Thanks to Ammar Ben Khadra from Uni Kaiserslautern for bringing this to my attention.)

Detecting functions using the .eh_frame section

We also show in our paper that function detection (i.e., accurately identifying the start address and size of each function in the binary) is currently the most problematic primitive for disassemblers. False positive and false negative rates in excess of 20% are not an uncommon sight, despite the fact that function detection is one of the most used and important primitives for virtually all areas of binary analysis.

It seems there is an interesting way to get around this problem, based on the .eh_frame section. This section contains information needed for DWARF-based stack unwinding. It’s primarily used for C++ exception handling, but also for various other applications such as backtrace(), and gcc intrinsics such as __attribute__((__cleanup__(f))) and __builtin_return_address(n) (more information in this StackOverflow post). Due to its many uses, .eh_frame is present by default not only in C++ binaries that use exception handling, but in all binaries produced by gcc, including plain C binaries.

The point of all this is that .eh_frame contains function boundary information that identifies all functions, and can thus be used to circumvent the function detection problem entirely. Here’s what a dump of the section looks like for 470.lbm (one of the SPEC CPU2006 benchmarks) compiled with gcc v5.1 at optimization level O0 for x64.

As far as I know, this method was first described here by Ryan O’Neill (a.k.a. ElfMaster). He also provides code to parse the .eh_frame section into a set of function addresses and sizes.

Note that the strip command will not strip the .eh_frame section. If you want to get rid of it (for anti-reversing or binary size reasons), you need to prevent it from being generated in the first place by passing -fno-asynchronous-unwind-tables to gcc.

(Thanks to Mariano Graziano from Cisco for telling me about this.)