That’s radare2. No, our scripts don’t look anything like this :(

Python for Reverse Engineering #1: ELF Binaries

Building your own disassembly tooling for — that’s right — fun and profit

Anirudh
Anirudh
Feb 7, 2019 · 6 min read

While solving complex reversing challenges, we often use established tools like radare2 or IDA for disassembling and debugging. But there are times when you need to dig in a little deeper and understand how things work under the hood.

Rolling your own disassembly scripts can be immensely helpful when it comes to automating certain processes, and eventually build your own homebrew reversing toolchain of sorts. At least, that’s what I’m attempting anyway.

Setup

As the title suggests, you’re going to need a Python 3 interpreter before
anything else. Once you’ve confirmed beyond reasonable doubt that you do,
in fact, have a Python 3 interpreter installed on your system, run

where capstone is the disassembly engine we’ll be scripting with and pyelftools to help parse ELF files.

With that out of the way, let’s start with an example of a basic reversing
challenge.

Compile it with GCC/Clang:

Scripting

For starters, let’s look at the different sections present in the binary.

This script iterates through all the sections and also shows us where it’s loaded. This will be pretty useful later. Running it gives us

Most of these aren’t relevant to us, but a few sections here are to be noted. The .text section contains the instructions (opcodes) that we’re after. The .data section should have strings and constants initialized at compile time. Finally, the .plt which is the Procedure Linkage Table and the .got, the Global Offset Table. If you’re unsure about what these mean, read up on the ELF format and its internals.

Since we know that the .text section has the opcodes, let’s disassemble the binary starting at that address.

The code is fairly straightforward (I think). We should be seeing this, on running

The line in bold is fairly interesting to us. The address at [rip + 0x200916] is equivalent to [0x6ca + 0x200916], which in turn evaluates to 0x200fe0. The first call being made to a function at 0x200fe0? What could this function be?

For this, we will have to look at relocations. Quoting linuxbase.org

Relocation is the process of connecting symbolic references with symbolic definitions. For example, when a program calls a function, the associated call instruction must transfer control to the proper destination address at execution. Relocatable files must have “relocation entries’’ which are necessary because they contain information that describes how to modify their section contents, thus allowing executable and shared object files to hold the right information for a process’s program image.

To try and find these relocation entries, we write a third script.

Let’s run through this code real quick. We first loop through the sections, and check if it’s of the type RelocationSection. We then iterate through the relocations from the symbol table for each section. Finally, running this gives us

Remember the function call at 0x200fe0 from earlier? Yep, so that was a call to the well known __libc_start_main. Again, according to linuxbase.org

The __libc_start_main() function shall perform any necessary initialization of the execution environment, call the main function with appropriate arguments, and handle the return from main(). If the main() function returns, the return value shall be passed to the exit() function.

And its definition is like so

Looking back at our disassembly

but this time, at the lea or Load Effective Address instruction, which loads some address [rip + 0xe6] into the rdi register. [rip + 0xe6] evaluates to 0x7aa which happens to be the address of our main() function! How do I know that? Because __libc_start_main(), after doing whatever it does, eventually jumps to the function at rdi, which is generally the main() function. It looks something like this

To see the disassembly of main, seek to 0x7aa in the output of the script we’d written earlier (disas1.py).

From what we discovered earlier, each call instruction points to some function which we can see from the relocation entries. So following each call into their relocations gives us this

Putting all this together, things start falling into place. Let me highlight the key sections of the disassembly here. It’s pretty self-explanatory.

The loop to populate the *pw string

And this looks like our strcmp()

I’m not sure why it uses puts here? I might be missing something; perhaps printf calls puts. I could be wrong. I also confirmed with radare2 that those locations are actually the strings “haha yes!” and “nah dude”.

Conclusion

Wew, that took quite some time. But we’re done. If you’re a beginner, you might find this extremely confusing, or probably didn’t even understand what was going on. And that’s okay. Building an intuition for reading and grokking disassembly comes with practice. I’m no good at it either.

All the code used in this post is here: https://github.com/icyphox/asdf/tree/master/reversing-elf

Ciao for now, and I’ll see ya in #2 of this series — PE binaries. Whenever that is.


I’m Anirudh, a CS undergrad, focusing on offensive security and digital forensics. I go by “icyphox” on the Internet.

Feel free to shoot me a mail at icyph0x@pm.me.

Sector443

An infosec community based out of SRM KTR, Chennai

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store