Reverse Engineering Course. Part I

Reverse Engineering Tools. File Information
RE_TUTORIALRETOOLSCLI

Before starting reversing complex programs you have to learn the foundations and one of these foundations are the tools available for you to do your work. Nowadays there are complex graphical solutions to do almost everything, however it is very useful to know how to use the more basic and generic CLI tools.

As I just said, you can find a tool that does everything for you and happily be stuck with it. Tools like Ghidra or IDA are powerful and will save you a lot of time, but if you just learn to use a specific tool you will miss a little bit of what is going behind the hood. And that is really where all the fun is.

There are a few extra advantages about using the classical CLI tools:

  • They are evergreen. CLI tools have been around almost from the beginning of the times (what UNIX people know as epoch), and they are still available. Other tools become popular, then another tool that looks nicer appears and the old one get obsolete so you have to learn the new one.
  • Awareness. Modern advanced tools provide powerful functions available on a mouse click. You never know if what it does is a very basic action or a complex algorithm composed of multiple steps. This lack of knowledge gets you completely unarmed when the “magical” tool option fails. Those options are great, but they are greater when you know what they do under the hood so you can react when something goes wrong.
  • Architectures. Even when the more powerful tools support more and more platforms every day, the reality is that new platforms or niche platforms, when supported, are usually only supported by the open source toolchains. The fact that this is open source makes much easier for the companies to add the support for those platforms but that will always come later, when the platform is known to be popular. Some niche platforms may never make it into those tools.
  • Flexibility. Least but not less, CLI tools are built to be combined together to produce powerful output as well as making the integration in report generation scripts or automation much more easy. Top-tier tools also provides those functionalities, however each one has its own details, which means that there is something new to learn every time somebody decides that a change is needed.

At this point, you can stop reading and keep going with Ghidra, IDA, Binary Ninja or whatever tool you like to use, or take a small break and dive into the very basic tools available on all systems and, hopefully you will come back to those tools with new knowledge that will allow you to take the most out of them.

If that resonates with you. Let’s get started!

Basic File Information

The first step when you face a new unknown binary to analyse is to get as much information from it as you can. Usually each analysis starts with the execution of this sequence of commands. I will use RE Challenge 1 as example to illustrate most of the tools in this chapter, so go a grab a copy.

The first command you will usually execute is file:

$ file challenge01.x86_64.bin
code/challenge01/challenge01.x86_64.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e41a3c906ff47b5b259e49871f1d1ba4d6772154, for GNU/Linux 3.2.0, not stripped

This command tells us a lot about the binary. In this case it says:

  • It is a 64 bits Little Endian (LSB) PIE (Position Independent Executable) for x86_64 architecture using version 1 of the System V ABI
  • It tells us that it is a dynamic binary and which dynamic loader uses
  • It gives us a hash value for the specific build of this binary.
  • The minimal Linux ABI supported (in this case it requires kernel 3.2 or newer)
  • And last but not least, it let us know If the binary was stripped, i.e. all symbols and debug information was removed.

However note that many of this data is for information only and it is added to the binary during the build process. In other words, there is nothing in the system (at least at the time of this writing) that will ensure that data is honored. This means that you can change the build hash or the ABI supported version without any consequences and the binary will run fine, as far as the system is compatible at binary level.

This information is stored in PT_NOTE Program headers and, for non stripped binaries organized in sections of type SHT_NOTE that are mapped to those PT_NOTE program headers and can be easily found inside the file. We’ll come back to this after a bit.

A special comment about the buildId is required. This hash is produced by the linker when producing the executable and it is not a hash of the final binary. Think about it, the final binary has to contain the hash so calculating the hash and then storing it will change the hash. For some weak hash algorithms (as md5) this can be done, using what is called a hash collision, but that is something you do not want to happen in your systems. So, the linker basically hashes some parts of the binary (code, data segments, etc…). The order and parts of the ELF file being hashed depends on the used linker and there is no easy way to calculate it afterwards. The BuildID hash is used to identify a specific build and it is intended for internal use by the company or organization developing the software. It was never intended to be an integrity check of the file.

So, as I said, all this information is store in SHT_NOTE ELF sections mapped into one or more corresponding PT_NOTE program headers. They can all be shown using the command:

$ readelf -n challenge01.x86_64.bin
Displaying notes found in: .note.gnu.property
  Owner                Data size    Description
  GNU                  0x00000010   NT_GNU_PROPERTY_TYPE_0
      Properties: x86 ISA needed: x86-64-baseline

Displaying notes found in: .note.gnu.build-id
  Owner                Data size    Description
  GNU                  0x00000014   NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: e41a3c906ff47b5b259e49871f1d1ba4d6772154

Displaying notes found in: .note.ABI-tag
  Owner                Data size    Description
  GNU                  0x00000010   NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 3.2.0

The listing above shows the contents of the different SHT_NOTE that you can list using the following command:

$ readelf -SW challenge01.x86_64.bin | grep note
  [ 1] .note.gnu.property NOTE            0000000000000350 000350 000020 00   A  0   0  8
  [ 2] .note.gnu.build-id NOTE            0000000000000370 000370 000024 00   A  0   0  4
  [19] .note.ABI-tag     NOTE            0000000000002154 002154 000020 00   A  0   0  4

Heavily stripped programs like the ones produced by the tool sstrip from ELFKickers will get rid of the whole section table. In that case, readelf -n, is still able to show the notes but won’t reference the section they belong to, because the section no longer exist.

It is relatively easy to add as many notes as you want during the build process (either using compiler attributes or a linker script). It is also relatively easy to overwrite existing note sections with extra values to inject code. However, adding new notes once the binary is created, even when possible, it’s a bit more tricky. But enough about notes for now.

Hex Dumping (xxd)

There are several tools for produce hexdacimal dumping of a file. hexdump or od are two of the oldest and more commonly available, however, xxd has become the de-facto standard when it comes to hexadecimal dumping files. But, in case you don’t have it in the target system and you cannot install it, od or hd. By the way, od stands for Octal Dump, and hd is an alias for hexdump.

If you want to use od you’d likely do it like this:

$ od -A x -x challenge01.x86.bin

This forces od to dump offsets and data in hexadecimal. Default is octal, of course.

xxd does more or less the same thing, but has a few interesting option that make its use more convenient in some situations. Personally, I normally use xxd together with dd but in reality xxd offer command-line options to specify which part of the file to dump. For example, let’s dump the buildId note that we’ve just recently see. For that we need the file offset that can be obtained from the section list, for example:

$ readelf -SW challenge01.x86_64.bin | grep build
  [ 2] .note.gnu.build-id NOTE            0000000000000370 000370 000024 00   A  0   0  4
$  xxd -s $((0x370)) -l $((0x24)) challenge01.x86_64.bin
00000370: 0400 0000 1400 0000 0300 0000 474e 5500  ............GNU.
00000380: e41a 3c90 6ff4 7b5b 259e 4987 1f1d 1ba4  ..<.o.{[%.I.....
00000390: d677 2154                                .w!T

Notes have a very specific format. Let’s break it up:

04 00 00 00 -> Size of the Note name 
14 00 00 00 -> Size of the Desc name (data 20 bytes in this case)
03 00 00 00 -> Type of note (NT_GNU_BUILD_ID) in this case

After that we find the note name that is 4 bytes long, according to the previous dump:

47 4e 55 00 -> "GNU"

And after that, the buildID hexadecimal values follow. You can just compare it with the value we obtained from file, at the beginning of this chapter.

xxd has a bunch of useful command-line flags, but two of the most interesting ones are the following:

  • -p processes hexadecimal sequences without any formatting, just prints the data as is
  • -r does the reverse process, given a hexdump generates the binary data associated.

Additional options to group bytes (-g) or deal with endinaness are also available for us to tweak xxd output yo match our needs.

xxd and hd dumps ASCII representation in addition to the hexadecimal values and therefore they will show any plain string stored in the file, like symbols, literals and so forth. However, for extracting strings there are better options.

Strings

The usual next step after getting the very basic information about the binary we’ve just seen is look for strings in the program. The strings utility scans the whole file looking for printable strings which, are surprisingly useful during reverse engineering.

To deceive strings a program needs to encode any text it uses to interact with the user or to access other program functionalities. This is pretty easy to do but it is an extra step during development that may be a bit annoying, so, sometimes, programs just obfuscate some strings and left other alone. In any case, you should also pass the program through the strings tools just in case. You may be surprised about what you will find.

Among other things you will see any plain text string used by the program, including format strings, as well as names of symbols, compiler version, section names and other information, it all depends on the effort put by the developer to hide things.

strings is easy to use and straightforward, but for big dynamic programs, it may produce a massive output. In those cases we can use the tool objdump to dump specific sections of the program. For example, for our simple challenge01 program, strings produces 90 lines.

$ strings challege01.x86.bin | wc -l
90

But we can just dump the .rodata section and avoid all the symbol names using objdump like this:

$ objdump -j .rodata -s challenge01.x96.bin

This will produce a hexadecimal dump instead of a list of strings one in each line, but it would be much shorter. and will provide us with the offsets where those strings will be found.

strings provides a few interesting parameters that should be considered further. The first one is -d that will just print strings from initialized, loaded data sections in the file.

$ strings -d challege01.x86.bin | wc -l
25

As you can see the reduction is considerable. Also the strings tool looks for sequences of printable characters with a minimal length of 4. This value may be changed using the flag -n if we want to look for longer strings. There is one last thing to consider about strings. Some platforms like Windows, applications using UTF-16 encoding, or applications using wide chars may puzzle strings. The -e flag allows us to define the character encoding strings should use. The possible values that will follow this flag are listed below:

s | Single 7-bit-byte (default)
S | Single 8-bit-byte
b | 16-bit Big Endian
l | 16-bit Little Endian
B | 32-bits Big Endian
L | 32-bits Little Enfina

Take a look to the man page for strings to check all the possible flag, but these are the more relevant while reverse engineering.

To conclude this section, I’d like to mention my own tool maca. You can download the source code from github and study it. It is a small and simple application, so it is very easy to follow. One of the options that provides maca is to dump strings. It just dump regular ASCII strings and only allows to specify the minimal size, but for many cases that is good enough. The advantage of maca is that it shows the section and offset of each string it found. For example, when run on challenge01 it will produce lines like:

$ maca -s challenge01.x86.bin
[           .rodata] 0x002007 [00b]: SuperSecret
[           .rodata] 0x002017 [020]: Reverse Engineering Challenge #1

The output is colored so you can easily identify strings in the .rodata section, or in the symbol table.

Dynamic Binaries

One of the information provided by file was if the binary was statically or dynamically linked. Overall, dynamically linked binaries are smaller as they make extensive use of system libraries, while static binaries have to include all the code they need in the binary, making them much bigger.

In addition to the use of file (which us enough for getting this information) we can used readelf or maca.

$ readelf -h  challenge01.x86_64.bin | grep Type
  Type:                              DYN (Position-Independent Executable file)  

The flag -h shows the ELF header of the indicated binary. In this case we’re just showing the Type field, but you could skip the grep and take an look to the whole header. We’ll do this in detail a bit later. An alternative, is to use maca that provides a more compact output of the ELF header:

$ maca -h  challenge01.x86_64.bin | grep Description
Description      : [64 bits] [AMD x86-64] [LSB] [System V]  | ABI Version [0] [ET_DYN : Shared object]

It just show in a single line, the type the architecture, the endianess and the ABI, but, as I’ve just said you would usually look to the whole header instead of filtering fields, unless you want to write some kind of script that requires this information separately.

For static binaries, the type will be DT_EXEC or just EXEC in readelf, and that is it, but for dynamically linked binaries we can use a few extra tools to get extra information from the binary before executing it or even looking into its ASM. The first one to check is ldd.

$ ldd challenge01
    linux-vdso.so.1 (0x00007fee7f230000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fee7f013000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fee7f232000)

The ldd tool shows the dynamic libraries used by the program. A static program won’t show anything. The three libraries shown above are the very minimal for C programs:

  • linux-vdso.so.1. This is the Virtual Dynamic Shared Object library. It is a ELF shared library that the kernel maps into every process address and allows a process to get kernel services without making a system call that takes quite some time. One of the initial uses of this was to substitute the gettimeofday system call which just returns a number.
  • libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6. This is the standard C library used by the program. As this is usually a symbolic link, ldd shows the actual location of the library.
  • /lib64/ld-linux-x86-64.so.2. This is the dynamic loader required to load the program and to perform task like lazy symbol resolution at run-time.

Programs written using other programming languages will show different sets of libraries. Some examples:

$ ldd hello-cpp
    linux-vdso.so.1 (0x00007efd10236000)
    libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007efd0fe00000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efd0fc0c000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007efd1011d000)
    /lib64/ld-linux-x86-64.so.2 (0x00007efd10238000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007efd100f0000)

As you can see, in addition to the libraries used by vanilla C programs, a very minimal C++ program will also include at least the libstdc++.so.6 that is the Standard C++ Library.

As a curiosity, ldd has been pretty controverted because it was, somehow executing the program in order to list the libraries it uses. This was exploited in the past to get code executed. Nowadays it is pretty safe but you can still see the nature of this utility running your application like this:

$ LD_TRACE_LOADED_OBJECTS=1 challenge01.x86.bin

It just produces the same output as ldd. Talking about dynamic loader tweaking there are some environmental variables honored by ld that triggers different behaviours. A few interesting ones are:

LD_DEBUG=libs ./binary : Shows all libraries loaded by the dynamic loader
LD_DEBUG=all ./binary  : Shows all debug, including symbol resolution
LD_DEBUG_OUTPUT=ld.log LD_DEBUG=libs ./bin: Redirect output to a file ld.log.pid
LD_SHOW_AUCV=1 ./bin   : Shows the Auxiliary vector sent to the binary

There is also a LD_AUDIT which is pretty powerful but not for this current topic. Something for you to research

Finally, we can also get a list of dynamic object used by a running process checking the /proc/PID/mappings folder. This is interesting during dynamic analysis. Note that a binary can use libraries dynamicall using the dlopen, dlsym and dlclose functions. In that case, the library is loaded at run-time and there is no detail in the binary itself (beyond the name of the library that may be obfuscated). In other words, none of the tools we have just discussed would show a dynamically loaded library. This can only be determined either by statically analyzing the binary or during dynamic analysis.

Other tools worth mentioning

There are a few more tools that I’d mention as they may be useful in certain circumstances.

The first one is floss (FLARE Obfuscated String Solver) that, sometimes will be able to extract obfuscated strings. Over all, it may worth to use it instead of strings as it will find all the strings strings does and likely a few more.

Other small tool that gives us information about the binary, but not that useful for reversing it is size. This little tool can give us information about the size of code and data of a given binary. As we will see in the next chapter, we will get this very same information from other tools anyways so, beyond size comparison between binaries it is not that useful.

Another interesting tool to take into consideration is exiftool, able to extract metadata from files. Even when this tools is more interesting when used on multimedia files, it can also be used on binaries and will give us some general information. Nothing we cannot get using other tools but it is good to remember it exists.

Entropy calculation

Entropy, putting aside the thermodynamics term, is a measurement of the amount of information in a given message and it value indicates how many bits (when using basis 2 logarithms for calculation) per symbol are required to store such message. For binaries symbols are bytes and therefore we have 256 possible symbols of value, therefore, an entropy of 8 will indicate that we need 8 bits to encode a byte, or in other words, the message is composed of random bytes and it is not possible to compress it. Therefore we have to use the maximum amount of bits that allows us to encode all the messages.

In general, there is some relation between symbols in a message and the message can be compressed up to certain step. That’s the other way to interpret entropy. From this point of view, a message (or a binary fragment) with an entropy of 4 means that we just need 4 bits to encode all the symbols in that fragment, which means that we can compress the fragment to roughtly half of its size.

Entropy is calculated using the following formula:

E = SUM_i p_i * log (1/p_i) = - SUM_i p_i * log(p_i)

The formula may look intimidating but it is just a weighted average of the inverse of probabilities of each symbol. If you think about it, the probability of a symbol in a message somehow measures how often that symbol shows up. Actually, in the real world that is the only way to calculate probabilities counting the times a symbol appears and dividing by the total number of symbols, that is the normalized value of how often the symbol appears. So, if we take the inverse we are measuring how rare is to see a given symbol. Why is this relevant?, because the symbols that appear less frequently are the ones that gives us more information.

The meteorological station at Atacama desert

The Atacama desert in South America is one of the dries places in the world. In average a few mm of rain per year and some parts of the desert haven’t received any rain for years. Now imagine that you setup a meteorological station there to report rain over a satellite link (actually that doesn’t really matter is just to add realism).

Our station will sample a sensor and send a message every minute using two symbols: R meaning rain, D meaning Dry. OK, let’s rename them as 1 and 0. According to what we’ve said, the sequence of transmitted information from the station will look like:

0000000000000000... many months later 1100000000000000 ....

So, if you have to guess what is the next value you will receive from the station, which one would you pick?. Yes, zero. It is most likely that is not raining in Atacama at any time in the year. So the probability of getting a 0 very, very high, almost 1 and therefore, from an Entropy point of view it gives us little to no information. So, in order to measure how much information is in our message we should consider the inverse, in other words, that number very close to 1, when inverted will become a number very close to zero. On the other hand, if we suddenly receive a 1 that is a lot of information, because that is a value that we cannot predict easily because it is very unlikely and then it should contribute much more to the information in the message than the 0s we get all day long.

So, this is the conceptual explanation of why the Entropy formula considers the inverse of the probability. Note however that, as we are using a logarithm, the effect in the final result is just a change if sign. By the way, the use of the logarithm is just to zoom in the data. All our values are between 0 and 1 (they are probabilities), the logarithm function between 0 and 1 goes from 0 at 1 to minus infinite, making the smaller values look much bigger and making the difference much obvious. This is commonly used in engineering.

Back to reverse engineering, why is the entropy interesting for us. As you have likely deduced of all this discussion a high entropy value means that data looks random, or if you prefer that we have .5 probability of guessing the next symbol. In computer parlance this may mean two things:

  • The first is that the data has been compressed. Compressing data basically consist on that, remove any redundancy on the message so we are not wasting bits unnecessarily. And that means that we cannot guess what will come next. In information theory, the value of entropy indicated the theoretical maximal compression we can achieve. If entropy is 4 that means that we could compress that message to half its size (for each symbol -1 byte- we just need 4 bits, so we could pack 2 symbols in one byte).
  • The second is that the data has been encrypted. Again, when we encrypt data, in general, we want it to look random so there is no chance to guess any of the values in the message. Cryptographic algorithms usually do this and produce sequences with maximal entropy. This is the reason why you should compress your data before crypt it. The other way around you won’t compress the data much.

Both cases are common, specially in malware, where part of the program is compressed or crypted so it is not directly accessible to the researcher right away. The programs doing this are usually know as crypters, when they just crypt part of the program, or packers when they compress or pack together other files within the program. In both cases, from the malware point of view this defeats static analysis and signature detection security solutions. Overall, this kind of binaries are much easier to analyse using dynamic analysis.

So, there are several tools that would calculate the entropy of a binary and give us hints about the use of crypters or packers.

  • binwalk -E. This will produce a nice graphic showing the calculated entropy for different offsets in the file.
  • ent. This tool will calculate the entropy together with other statistical values that may be interesting. It may be to be used together with objdump or similar tools if you want to calculate the entropy per ELF section.
  • maca. This is my own tool and I added the functionality to calculate the entropy per section and per program header.

For example this is the output of maca for the challenge01.x86.bin. I decided to show the entropy in maca as a percentage. Basically it is the entropy divided by 8, so an entropy of 8 (maximum) will show 100 percent.

$ maca -l code/challenge01/challenge01.x86_64.bin
Analysing binary...
+ Analysing 31 sections...
PROGRAM HEADERS
[  ]             TYPE PERM    VADDR     PADDR       OFFSET FILESIZE  MEMSIZE    ALIGN   ENT
[00]             PHDR [4] R         40       40       40 00000310      310        8     20.900
[01]           INTERP [4] R        394      394      394 0000001c       1c        1     51.297
[02]             LOAD [4] R          0        0        0 00000758      758     1000     32.708
[03]             LOAD [5] R E     1000     1000     1000 00000231      231     1000     63.477
[04]             LOAD [4] R       2000     2000     2000 00000174      174     1000     56.466
[05]             LOAD [6] RW      3dd0     3dd0     2dd0 00000270      280     1000     18.398
[06]          DYNAMIC [6] RW      3de0     3de0     2de0 000001e0      1e0        8     18.171
[07]             NOTE [4] R        350      350      350 00000020       20        8     26.123
[08]             NOTE [4] R        370      370      370 00000024       24        4     54.062
[09]             NOTE [4] R       2154     2154     2154 00000020       20        4     21.931
[10]     GNU_PROPERTY [4] R        350      350      350 00000020       20        8     25.961
[11]     GNU_EH_FRAME [4] R       2078     2078     2078 0000002c       2c        4     39.789
[12]        GNU_STACK [6] RW         0        0        0 00000000        0       10     0.000
[13]         GNU_RELO [4] R       3dd0     3dd0     2dd0 00000230      230        1     17.321

This is pretty normal. The segment with the higher entropy are the code and read only data. However those values are kind of normal.

If we pack the binary with upx we’ll get this:

$ upx -9 challenge01.x86_64.bin -o challenge01.pack
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2024
UPX 4.2.4       Markus Oberhumer, Laszlo Molnar & John Reiser    May 9th 2024

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     16248 ->      6304   38.80%   linux/amd64   challenge01.pack

Packed 1 file.
$ maca -l challenge01.pack
Analysing binary...
+ Analysing 0 sections...
PROGRAM HEADERS
[  ]             TYPE PERM    VADDR     PADDR       OFFSET FILESIZE  MEMSIZE    ALIGN   ENT
[00]             LOAD [6] RW         0        0        0 00001000     4050     1000     88.207
[01]             LOAD [5] R E     5000     5000        0 000012d5     12d5     1000     90.136
[02]        GNU_STACK [6] RW         0        0        0 00000000        0       10     0.000

Despite the fact that the program header table looks pretty weird, you can see the values of 88%% amd 90%% that are pretty high.

UPX is a very well-known and popular packer and you can easily identify it using strings or maca and looking for UPX:

$ maca -s challenge01.pack | grep UPX
0x0000eb [004]: UPX!
0x000947 [04e]: $Info: This file is packed with the UPX executable packer http://upx.sf.net $\0x0a
0x000996 [04b]: $Id: UPX 4.24 Copyright (C) 1996-2024 the UPX Team. All Rights Reserved. $\0x0a
0x000c89 [005]: UPX!u
0x001873 [004]: UPX!
0x00187b [004]: UPX!

Pretty obvious in this case. We’ll see how to deal with crypters and packers later. For now, we learned how to identify them.

Summary

We have gone through the very basic tools to extract information from binary files without going into the ELF format itself (well, not too much) or the assembly code inside them. In the next chapter we’ll learn about tools to do that. Things will get much interesting.

Related Posts

Reverse Engineering Challenge 1

Get started with the simplest challenge

Read

RE_CHALLENGERELAB

Reverse Enginnering

All reverse engineering information in a single place

Read

REVERSE ENGINEERING RE RE CHALLENGES

Return to Home Page