[Practical Binary Analysis] Part.1 (Chapter.1)
CHPTER 1 - ANATOMY OF BINARY
=================
What exactly is a binary?
Modern computers perform their computations using the binary numerical system, which express all numbers as strings of one and zeros.
The machine code that these systems execute is called binary code.
Every program consists of a collection of:
-
Binary code the machine instructions
-
Data variables, constants and the like
To keep track of all the different programs on a given system is needed a way to store all the code and data belonging to each program in a self-contained file.
These files, containing executable binary programs, are called binary executable files or simply binary
1.1 The C Compilation Process
Binaries are produced through compilation:
COMPILATION : Is the process of translating human-readable source code into machine code that the processor can execute. |
Compiling C code involves four phases (modern compilers often merge some or all of these phases):
-
Processing
-
Compilation
-
Assembly
-
Linking
1.1.1 The Processing Phase
–
Below a typical compilation process for C code:
The compilation process starts with a number of source files that have to be compiled ( file-1.c through file-n.c )
It is possible to have one large file but typically, large programs are composed of many files, this for the following reasons:
a) It makes the project easier to manage
b) It speeds up compilation because if one file changes, only that file has to be recompiled instead of all the code.
C source files contain:
-
Macros (#define)
-
Directives (#include): are used to include header files (with the extension .h), on which the source code depends.
The Processing phase expands any “#define” macros or “#include” directives in the source file, so all that is left is pure C code ready to be compiled.
Let take the following code as an example:
|
|
Let us compile the file “compilation_example.c” using gcc (into x86-64 code) using the following parameters:
-
-E : tells gcc to stop after processing
-
-P : causes the compiler to omit the debugging information, so that the output is a bit cleaner.
Below the output of the C processor of the compilation_example.c gcc -E -P compilation_example.c
|
|
The “stdio.h” header is included in its entirety, with all its type definitions, global variables, and function prototypes “copied in” to the source file.
-
Because this happens for every #include directive, preprocessor output can be quite verbose.
-
The preprocessor also fully expands all of any #define macros used. In the above example both arguments to “printf”* are evaluated and replaced by the constant strings they represent:
|
|
to:
|
|
1.1.2 The Compiling Phase
After the processing part is completed, the source is now ready to be compiled-
The compilation phase takes the preprocessed code and translate it into assembly language. |
Most compilers also perform heavy optimization in this phase, typically configurable as an optimization level through command line switches such as option –Oo3 through –O3 in gcc. The degree of optimization during compilation can have a profound effect on disassembly.
The main reason why the compilation phase produces assembly language instead of machine code is that writing a compiler that emits, at the same time, machine code for many different languages would be an extremely demanding and time-consuming task.
It is better to instead emit assembly code, which is an already challenging task, and have a single dedicated assembler that can handle the final translation of assembly to machine code for every language.
Therefore, the output of the compilation phase is:
- Assembly, in a reasonably human-readable form, with symbolic information intact.
As mentioned, gcc normally calls all compilation phases automatically, so:
To see the emitted assembly from the compilation stage, the parameters and options to use in order to tell gcc to stop after stage and store the assembly to the disk are:
-
-S : This flag is for the conventional extension for assembly files “*.s”
-
-masm=intel : With this option “gcc” emits assembly in Intel syntax rather than the default AT&T syntax
Below the output of the C processor of the compilation_example.c gcc -S -masm=intel compilation_example.c
|
|
Below the output of $ cat compilation_example.s
|
|
The above code looks relatively easy to read because the symbols and functions have been preserved:
- ln.12 - The auto generated name LC0 for the nameless “Hello, world!” string.
- ln.22 - The explicit label for the mainfunction.
- ln.44 - The symbolic reference to code and data to the LC0(the“Hello, world!” string).
Constants and variables have symbolic names rather than just address:
- Unfortunately, with stripped binaries it’s not possible to gain all these extensive information.
1.1.3 The Assembly Phase
The Assembly phase: when some real machine code is finally generated!
The input of the assembly phase is the set of assembly language files generated in the compilation phase, and the output is a set of object files, sometimes also referred to as module. Object files contain machine instructions that are in principle executable by the processor. Typically, each source file corresponds to one assembly file, and each assembly file corresponds to one object file.
Below the process of generating an object file with gcc .The parameter to generate an object file, is -c : Flag to generate an object file.
|
|
The “file <file.o>” utility to confirm that the produced file is indeed and object file
$ file compilation\_example.o | |
compilation\_example.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped |
Let’s analyse the output, the first part of the file output shows that the file conforms to the elf specification for binary executables:
ELF 64-bit LSB relocatable: |
|
not stripped: |
|
Object files are compiled independently from each other, so the assembler has no way of knowing the memory addresses of other objects files when assembling an object file.
That’s why objects files need to be relocatable; so that you can link them together in any order to form a complete binary executable. (If objects files were not relocatable, that action would be impossible)
1.1.4 The Linking Phase
The linking phase is the *final phase of the compilation process*. As the name implies, this phase links together all the object files **into a single binary executable.** |
In modern systems, *the linking phase* sometimes incorporate an
**additional optimization pass**, called:
|
The program that performs the linking phase is called a ***linke**r*, or ***link editor*** and ***it is typically separate from the compiler**, which usually implements all the preceding phases*. |
**Objects file are relocatable** because they are **compiled independently from each other**, preventing the compiler from assuming that an object **will end up at any particular address**. **Moreover, “*object file”* may reference functions or variables in other *objects files* or in libraries that are external to the program**. |
**Before** the linking phase, **the address** at which the referenced
code and data will be placed **are not yet known**:
|
**When an object file references *one of its own functions or variables
by absolute address*, the reference will also be symbolic**.
|
Now that the arrangement of all modules in the executable is known, the
linker can also resolve most symbolic refences.
References to libraries may or may not be completely resolved, depending on the type of library.
|
To produce a complete binary executable, only **gcc with no special switches is required. ** |
The below example shows the generated binary executable with **gcc (with -o it’s possible to name the file)** |
$ gcc compilation_example.c | ||||||||||
$ ls -la | ||||||||||
total | 40 | |||||||||
drwxr-xr-x | 2 | binary | binary | 4096 | dec | 10 | 17:38 | . | ||
drwxrwxr-x | 16 | binary | binary | 4096 | apr | 19 | 2018 | .. | ||
-rwxrwxr-x | 1 | binary | binary | 8616 | dec | 10 | 17:38 | a.out | (with no switches, by default the executable is called ‘a.out’) | |
-rw-r--r-- | 1 | binary | binary | 172 | apr | 19 | 2018 | compilation_example.c | ||
-rw-r--r-- | 1 | binary | binary | 10240 | apr | 19 | 2018 | hello.exe | ||
-rw-r--r-- | 1 | binary | binary | 723 | apr | 19 | 2018 | Makefile | ||
Let’s run file commmand angainst the generated binary file a.out
$ file a.out | ||||||||||
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID\[sha1\]=c21ccd6d27df9e553a574b2be2d6d58501fa8a0b, not stripped |
Let’s analyse the output generated by the “file” utility ($ file a.out):
ELF 64-bit LSB executable: |
|
dynamically linked: |
|
interpreter /lib64/ld-linux-x86-64.so.2 : |
|
not stripped : |
|
▶ Symbols and Stripped Binaries
=============================
High-level source code, such as C code, centers around functions and variables with meaningful, human-readable names.
When compiling a program, compiler emit symbols, which:
Keep track of such symbolic names.
Record which binary code and data correspond to each symbol.
For instance, function symbols provide a mapping from symbolic, high-level function names to the first address and the size of each function.
This information is normally used by the linker when combining object files (for instance, to resolve function and variable references between modules) and also aids debugging.
◌ Viewing Symbolic Information
The below example shows some symbolic information for the file “a.out” using “readelf –syms”:
- ‘-s’ or ‘–syms’ or ‘–symbols’ (An alias for ‘–syms’) : Display the symbol table
$ readelf –syms a.out | |||||||
Symbol table '.dynsym' contains 4 entries: | |||||||
Num | Value (the address) | Size | Type | ind | Vis | Ndx | Name |
0: | 0000000000000000 | 0 | NOTYPE | LOCAL | DEFAULT | UND | |
1: | 0000000000000000 | 0 | FUNC | GLOBAL | DEFAULT | UND | puts@GLIBC_2.2.5(2) |
2: | 0000000000000000 | 0 | FUNC | GLOBAL | DEFAULT | UND | __libc_start_main@GLIBC_2.2.5(2) |
3 | 0000000000000000 | 0 | NOTYPE | WEAK | DEFAULT | UND | __gmon_start__ |
Symbol table '.symtab' contains 67 entries: | |||||||
Num | Value (the address) | Size | Type | ind | Vis | Ndx | Name |
... | |||||||
56 | 0000000000601030 | 0 | OBJECT/td> | GLOBAL | HIDDEN | 25 | __dso_handle |
57: | 00000000004005d0 | 4 | OBJECT | GLOBAL | DEFAULT | 16 | _IO_stdin_used |
58: | 0000000000400550 | 101 | FUNC | GLOBAL | DEFAULT | 14 | __libc_csu_init |
59: | 0000000000601040 | 0 | NOTYPE | GLOBAL | DEFAULT | 26 | _end |
60: | 0000000000400430 | 42 | FUNC | GLOBAL | DEFAULT | 14 | _start |
61: | 0000000000601038 | 0 | NOTYPE | GLOBAL | DEFAULT | 26 | __bss_start |
62: | 0000000000400526 | 32 | FUNC | GLOBAL | DEFAULT | 14 | main |
63: | 0000000000000000 | 0 | NOTYPE | WEAK | DEFAULT | UND | _Jv_RegisterClasses |
64: | 0000000000601038 | 0 | OBJECT | GLOBAL | HIDDEN | 25 | __TMC_END__ |
65: | 0000000000000000 | 0 | NOTYPE | WEAK | DEFAULT | UND | _ITM_registerTMCloneTable |
66: | 00000000004003c8 | 0 | FUNC | GLOBAL | DEFAULT | 11 | _init |
In the above listing, with the utility readelf is possible to display the symbol table, among many unfamiliar symbols we can find a symbol for:
$ readelf –syms a.out | |||||||
Symbol table '.dynsym' contains 4 entries: | |||||||
Num | Value (the address) | Size | Type | ind | Vis | Ndx | Name |
.... | |||||||
62: | 0000000000400526 | 32 | FUNC | GLOBAL | DEFAULT | 14 | main |
.... |
-
The main function: the value 0x400526represents the address at which “main” will reside when the binary is loaded into memory.
-
The output also shows the code size of main (32 bytes) and indicates that you are dealing with a function symbol (type FUNC)
Symbolic information can be emitted either
a) As part of the binary (as shown above).
b) Or in the form of a separate symbol file, and it comes on various flavours.
The linking phase is the *final phase of the compilation process*. As the name implies, this phase links together all the object files **into a single binary executable.** |
In modern systems, *the linking phase* sometimes incorporate an
**additional optimization pass**, called:
|
The linker needs only basic symbols, but far more extensive information can be emitted for debugging purposes. **Debugging symbols** information go as far as providing a full mapping between source lines and binary-level instructions, and they **even describe** function parameters, stack frame information, and more. |
|
As you might imagine, symbolic information is extremely useful for binary analysis. Having a set of well-defined function symbols at your disposal makes disassembly much easier because you can use each function symbol as a starting point for disassembly. |
This makes it much less likely that you will *accidentally disassemble data as code*, for instance (which would lead to bogus instructions in the disassembly output) |
Knowing which part of a binary belong to which function, and what the function is called, also makes it much easier for human reverse engineer to compartmentalize and understand what the code is doing. Even just basic linker symbols are already a tremendous help in many binary analysis applications. |
**Unfortunately, extensive debugging information typically isn’t included in production-ready binaries, and even symbolic information is stripped to reduce the size and prevent reverse engineering, especially in the case of malware or proprietary software. ** |
◌ Another Binary Turns to the Dark Side:
▹ Stripping a Binary
Apparently, the default behaviour of gcc is not to automatically strip newly compiled binaries.
The below example shows how binary with symbols end up stripped by using
the command :
$ strip –strip-all a.out
$ strip --strip-all a.out | ||||||||||
$ file a.out*/td> | ||||||||||
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c21ccd6d27df9e553a574b2be2d6d58501fa8a0b, stripped |
Using the command “strip –strip-all a.out”, the binary is now stripped.
Only symbols in ‘.dynsym’ table are left, these are used to resolve dynamic dependencies when the binary is loaded into memory, but they are not much use when disassembling.
$ readelf –syms a.out | |||||||
Symbol table '.dynsym' contains 4 entries: | |||||||
Num | Value (the address) | Size | Type | ind | Vis | Ndx | Name |
0: | 0000000000000000 | 0 | NOTYPE | LOCAL | DEFAULT | UND | |
1: | 0000000000000000 | 0 | FUNC | GLOBAL | DEFAULT | UND | puts@GLIBC_2.2.5(2) |
2: | 0000000000000000 | 0 | FUNC | GLOBAL | DEFAULT | UND | __libc_start_main@GLIBC_2.2.5(2) |
3 | 0000000000000000 | 0 | NOTYPE | WEAK | DEFAULT | UND | __gmon_start__ |
1.3 Disassembling a Binary
======================
Now that we have seen how to compile a binary, let’s take a look at the contents of the object file produced in the assembly phase of compilation.
1.3.1 Looking inside an Object File.
Let’s use the objdump utility to show how to do all the disassembling
Below the disassembled version of the object file compilation_example.o using options -s and -j
$ objdump -sj .rodata compilation_example.o |
compilation_example.o: file format elf64-x86-64 |
Contents of section .rodata: |
$ 0000 48656c6c 6f2c2077 6f726c64 2100 Hello, world!. |
In the above example the options used to show only the ‘.rodata’ section are:
-
-s, –full-contents Display the full contents of all sections requested
-
-j, –section=NAME Only display information for section NAME
Contents of section .rodata: |
$ 0000 48656c6c 6f2c2077 6f726c64 2100 Hello, world!. |
The content of .rodata section consists of :
-
An ASCII encoding of the string (on the left side of the output) **0000 48656c6c 6f2c2077 6f726c64 2100 **
-
The human-readable representation of those same bytes (on the right side of the output) Hello, world!.
.rodata stands for “read-only”; it’s part of the binary where all constants are stored (including the “Hello, world!” string)
Below the disassembled version of the object file compilation_example.o using option/switch -M and -d
$ objdump -M intel -d compilation_example.o | |||
compilation_example.o: file format elf64-x86-64 | |||
Disassembly of section .text: | |||
0000000000000000 <main> | |||
0: | 55 | mov rbp | |
1: | 48 89 e5 | mov rbp,rsp | |
4: | 48 83 ec 10 | sub rsp,0x10 | |
8: | 89 7d fc | mov DWORD PTR [rbp-0x4],edi | |
b: | 48 89 75 f0 | mov QWORD PTR [rbp-0x10],rsi | |
f: | bf 00 00 00 00 | mov edi,0x0 | |
14: | e8 00 00 00 00 | call 19 <main+0x19> | |
19: | b8 00 00 00 00 | mov eax,0x0 | |
1e: | c9 | leave | |
1f: | c3 | ret | |
In the above example the options used are:
-
-M, –disassembler-options=OPT Pass text OPT on to the disassembler
- Intel, for usewith the -M switch This option to display instruction in Intel syntax
-
-d, –disassemble Display assembler contents of executable sections
The utility objdump here has been used to disassemble all the code in the object file compilation_example.o, in the Intel syntax.
$ objdump -M intel -d compilation_example.o |
It contains only the code of the main function because that’s the only function defined in the source file.
$ objdump -M intel -d compilation_example.o | |||
compilation_example.o: file format elf64-x86-64 | |||
Disassembly of section .text: | |||
0000000000000000 <main> | |||
0: | 55 | mov rbp | |
1: | 48 89 e5 | mov rbp,rsp | |
4: | 48 83 ec 10 | sub rsp,0x10 | |
8: | 89 7d fc | mov DWORD PTR [rbp-0x4],edi | |
b: | 48 89 75 f0 | mov QWORD PTR [rbp-0x10],rsi | |
f: | bf 00 00 00 00 | mov edi,0x0 | |
14: | e8 00 00 00 00 | call 19 <main+0x19> | |
19: | b8 00 00 00 00 | mov eax,0x0 | |
1e: | c9 | leave | |
1f: | c3 | ret | |
For the most part, the output conforms pretty closely to the assembly code previously produced by the compilation phase (give or take few assembly level macros)
What is interesting here is that the pointer to the “Hello, world!” string is set to zero.
$ objdump -M intel -d compilation_example.o | |||
compilation_example.o: file format elf64-x86-64 | |||
Disassembly of section .text: | |||
0000000000000000 <main> | |||
0: | 55 | mov rbp | |
1: | 48 89 e5 | mov rbp,rsp | |
4: | 48 83 ec 10 | sub rsp,0x10 | |
8: | 89 7d fc | mov DWORD PTR [rbp-0x4],edi | |
b: | 48 89 75 f0 | mov QWORD PTR [rbp-0x10],rsi | |
f: | bf 00 00 00 00 | mov edi,0x0 | |
14: | e8 00 00 00 00 | call 19 <main+0x19> | |
19: | b8 00 00 00 00 | mov eax,0x0 | |
1e: | c9 | leave | |
1f: | c3 | ret | |
The subsequent call that should print the string to the screen using puts also points to non sensical location (offset 19, in the middle of the main).
$ objdump -M intel -d compilation_example.o | |||
compilation_example.o: file format elf64-x86-64 | |||
Disassembly of section .text: | |||
0000000000000000 <main> | |||
0: | 55 | mov rbp | |
1: | 48 89 e5 | mov rbp,rsp | |
4: | 48 83 ec 10 | sub rsp,0x10 | |
8: | 89 7d fc | mov DWORD PTR [rbp-0x4],edi | |
b: | 48 89 75 f0 | mov QWORD PTR [rbp-0x10],rsi | |
f: | bf 00 00 00 00 | mov edi,0x0 | |
14: | e8 00 00 00 00 | call 19 <main+0x19> | |
19: | b8 00 00 00 00 | mov eax,0x0 | |
1e: | c9 | leave | |
1f: | c3 | ret | |
But why does the call, that should reference puts, point instead into the middle main?
Let’s having in mind that data and code references from object files are not yet fully resolved because the compiler does not know at what base address the file will eventually be loaded. That’s why the call to puts is not yet correctly resolved in the object file.
The object file is waiting for the linker to fill in the correct value for this reference.
Let’s use the command readelf to show all the relocation symbols:
$ readelf --relocs compilation_example.o | |||||||
Relocation section '.rela.text' at offset 0x210 contains 2 entries: | |||||||
Offset | Info | Type | Sym. Value | Sym. Name | + | Addend | |
000000000010 | 00050000000a | R_X86_64_32 | 0000000000000000 | .rodata | + | 0 | |
000000000015 | 000a00000002 | R_X86_64_PC32 | 0000000000000000 | puts | - | 4 | |
The relocation symbol at offset 000000000010 tells the linker that it should resolve the reference to the string to point to whatever address it ends up at in the .rodata section.
$ readelf --relocs compilation_example.o | |||||||
Relocation section '.rela.text' at offset 0x210 contains 2 entries: | |||||||
Offset | Info | Type | Sym. Value | Sym. Name | + | Addend | |
000000000010 | 00050000000a | R_X86_64_32 | 0000000000000000 | .rodata | + | 0 | |
000000000015 | 000a00000002 | R_X86_64_PC32 | 0000000000000000 | puts | - | 4 | |
Similarly, the relocation symbol at offset 000000000015 tells the linker how to resolve the call to puts.
The value in the offset “column” is the offset in the object file where the resolved reference must be filled in. If we go back and check the offset generated with the utility objdump we can see that the offset is 0x14.
$ objdump -M intel -d compilation_example.o | |||
/* …. */ | |||
f: | bf 00 00 00 00 | mov edi,0x0 | |
14: | e8 00 00 00 00 | call 19 <main+0x19> | |
/* …. */ | |||
1f: | c3 | ret |
|
|
We can notice that the relocation symbol points to offset 0x15 instead, this is because only the operand (at position 0x15) of the instruction needs to be overwritten, not the opcode of the instruction at 0x14 (e8).
It is just to happen that for both instructions that need fixing up, the opcode is 1 byte long, so to point to the instruction’s operand, the relocation symbol needs to skip past the opcode byte.
1.3.2 Examining a complete binary executable
It’s now time to examine a complete binary executable.
Let’s start with a binary with symbols (a.out) and then move on to the stripped equivalent (a.out.stripped) to see the difference in disassembly output.
$ ls -la | |||||||||
...... | |||||||||
-rwxrwxr-x | 1 | binary | binary | 8616 | dec | 12 | 16:49 | a.out | |
-rwxrwxr-x | 1 | binary | binary | 6312 | dec | 12 | 16:48 | a.out.stripped | |
...... |
Let’s disassemble the non-stripped version of the binary file “a.out”
$ objdump -M intel -d a.out | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
a.out: file format elf64-x86-64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
00000000004003c8 .init:1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003c8: | 48 83 ec 08 | sub rsp,0x8 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003cc: | 48 8b 05 25 0c 20 00 | mov rax,QWORD PTR [rip+0x200c25] | # 600ff8 <_DYNAMIC+0x1d0> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003d3: | 48 85 c0 | test rax,rax | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003d6: | 74 05 | je 4003dd <_init+0x15> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003d8: | e8 43 00 00 00 | call 400420 <__libc_start_main@plt+0x10> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003dd: | 48 83 c4 08 | add rsp,0x8 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003e1: | c3 | ret | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Disassembly of section.plt :2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
00000000004003f0 <puts@plt-0x10>: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003f0: | ff 35 12 0c 20 00 | push QWORD PTR [rip+0x200c12] | # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003f6: | ff 25 14 0c 20 00 | jmp QWORD PTR [rip+0x200c14] | # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003fc: | 0f 1f 40 00 | nop DWORD PTR [rax+0x0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
00000000004003f0 <puts@plt>: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
400400: | ff 25 12 0c 20 00 | jmp QWORD PTR [rip+0x200c12] | # 601018 <_GLOBAL_OFFSET_TABLE_+0x18> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
400406: | 68 00 00 00 00 | push 0x0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
40040b: | e9 e0 ff ff ff | jmp 4003f0 <_init+0x28> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
...... | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Disassembly of section .fini: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
00000000004005c4 <_fini>: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4005c4: | 48 83 ec 08 | sub rsp,0x8 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4005c8: | 48 83 c4 08 | add rsp,0x8 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4005cc: | c3 | ret |
We can see that the binary has a lot more code than the object file.
It’s no longer just the main function or even just a single code section.
There are multiple sections now, with names like :
-
.init
-
.plt
-
.text
These sections contain code serving different function, such as program initialization or stubs for calling shared libraries.
The .text section (3) is the main code section, and it contains:
-
The main (4) function.
-
Other functions, such, as _start, that are responsible for tasks such as setting up the command line arguments and runtime environment for main and cleaning up after main. (These extra functions are standards functions, present in any ELF file produced by gcc).
We can also see that the previously incomplete code and data references have now been resolved by the linker.
$ objdump -M intel -d compilation_example.o | ||||||||||
compilation_example.o: file format elf64-x86-64 | ||||||||||
Disassembly of section .text: | ||||||||||
0000000000000000 <main> | ||||||||||
0: | 55 | mov | rbp | |||||||
1: | 48 89 e5 | mov | rbp,rsp | |||||||
4: | 48 83 ec 10 | sub | rsp,0x10 | |||||||
8: | 89 7d fc | mov | DWORD PTR [rbp-0x4],edi | |||||||
b: | 48 89 75 f0 | mov | QWORD PTR [rbp-0x10],rsi | |||||||
f: | bf 00 00 00 00 | mov | edi,0x0 | |||||||
14: | e8 00 00 00 00 | call | 19 <main+0x19> | |||||||
19: | b8 00 00 00 00 | mov | eax,0x0 | |||||||
1e: | c9 | leave | ||||||||
1f: | c3 | ret |
$ readelf --relocs compilation_example.o | |||||||||
Relocation section '.rela.text' at offset 0x210 contains 2 entries: | |||||||||
Offset | Info | Type | Sym. Value | Sym. Name | + | Addend | |||
000000000010 | 00050000000a | R_X86_64_32 | 0000000000000000 | .rodata | + | 0 | |||
000000000015 | 000a00000002 | R_X86_64_PC32 | 0000000000000000 | puts | - | 4 | |||
For instance, the call to “puts” now points to the proper stub (in the .plt section) for the shared library that contains puts.
Disassembly of section .plt: | ||||
00000000004003f0 <puts@plt-0x10>: | ||||
4003f0: | ff 35 12 0c 20 00 | push QWORD PTR [rip+0x200c12] | # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> | |
...... | ||||
0000000000400400 <**puts@plt**>: | ||||
400400: | ff 25 12 0c 20 00 | jmp QWORD PTR [rip+0x200c12] | # 601008 <_GLOBAL_OFFSET_TABLE_+0x18> | |
400406: | 68 00 00 00 00 | jmp 0x0 | ||
40040b: | e9 e0 ff ff ff | jmp 4003f0 <_init+0x28> | ||
...... | ||||
0000000000000000 <main> | ||||
...... | ||||
40053a: | e8 c1 fe ff ff | call 400400 <puts@plt> | ||
...... |
Let’s try now to disassemble the stripped filed version a.out.stripped
$ objdump -M intel -d a.out.stripped | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
a.out.stripped: file format elf64-x86-64 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Disassembly of section .init: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
00000000004003c8 <.init> : 1 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003c8: | 48 83 ec 08 | sub rsp,0x8 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003cc: | 48 8b 05 25 0c 20 00 | mov rax,QWORD PTR [rip+0x200c25] | # 600ff8 <__libc_start_main@plt+0x200be8> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003d3: | 48 85 c0 | test rax,rax | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003d6: | 74 05 | je 4003dd <puts@plt-0x23> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003d8: | e8 43 00 00 00 | call 400420 <__libc_start_main@plt+0x10> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003dd: | 48 83 c4 08 | add rsp,0x8 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4003e1: | c3 | ret | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Disassembly of section .plt: 2 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
....... | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Disassembly of section text: 3 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
- While the difference sections are still clearly distinguishable ( 1.init, 2.plt ,3.tex t) BUT the functions are not!
Now all the functions have been merged into one big blob of code:
-
The <_start> function begins at address 400430 and ends at 40045a
-
The **<*deregister_tm_clones> ***function begins at address 40046
-
The <main> function begins at address 400526 and ends at 400545
As we can see above, there is not anything special to indicate that the instructions at these markers represent function starts.
The only exception are the functions in the .plt section, which still have their names as before.
$ objdump -M intel -d a.out.stripped | ||||
a.out.stripped: file format elf64-x86-64 | ||||
Disassembly of section .init: | ||||
00000000004003c8 <.init>: | ||||
4003c8: | 48 83 ec 08 | sub rsp,0x8 | ||
....... | ||||
Disassembly of section .plt: | ||||
4003f0: | ff 35 12 0c 20 00 | push QWORD PTR [rip+0x200c12] | # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> | |
4003f6: | ff 25 14 0c 20 00 | jmp QWORD PTR [rip+0x200c14] | # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> | |
4003fc: | 0f 1f 40 00 | nop DWORD PTR [rax+0x0] | ||
0000000000400400 <puts@plt>: | ||||
....... | ||||
0000000000400410 <__libc_start_main@plt>: | ||||
400410: | ff 25 0a 0c 20 00 | jmp QWORD PTR [rip+0x200c0a] | # 601020 <_GLOBAL_OFFSET_TABLE_+0x20> | |
....... | ||||
....... | ||||
400454: | e8 b7 ff ff ff | call 400410 <__libc_start_main@plt> | function <main> | |
400459: | f4 | hlt g | ||
1.4 Loading and Executing a Binary
==============================
What really happens when a binary file is loaded and executed?
The figure below shows how a loaded ELF binary is represented in memory on a Linux-based platform.
(At a high level, loading a PE binary on Windows is quite similar).
Loading a binary is a complicated process that involves a lot of work by the operating system. It is also important to note a binary’s representation in memory does not necessarily correspond one to one with its on-disk representation.
For instance, large amount of zero-initialized data may be collapsed in the on-disk binary (to save disk space), while all those zeros will be expanded in memory or not loaded into memory at all.
When we decide to run a binary, the operating system starts by setting up a new process for the program to run in, including a virtual address space.
Subsequently, the operating system maps an interpreter into the process’s virtual memory
This is a user space program that knows how to load the binary and perform the necessary relocations:
-
On Linux the interpreter is typically a shared library called ld-linux.so.
-
On Windows the interpreter functionality is implemented as part of ***ntdll.dll. ***
After loading the interpreter, the kernel transfers control to it, and the interpreter starts its work in user space.
Linux ELF binaries comes with a special section called .interp that specifies the path to the interpreter that is to be used to load the binary.
The command below shows the .interp section of the file a.out:
-
-p –string-dump=<number|name> Dump the contents of section <number|name> as strings.
-
.interp Is the section name.
$ readelf -p .interp a.out |
String dump of section '.interp': |
[0] /lib64/ld-linux-x86-64.so.2 |
-
Here the interpreter loads the binary into its virtual address space (the same space in which the interpreter is loaded).
-
It then parses the binary to find put (among other things) which dynamic libraries the binary uses.
-
The interpreter maps these into the virtual address space ( using mmap or equivalent function) and then performs any necessary last-minute relocations in the binary’s code sections to fill in the correct address for references to the dynamic libraries.
-
In reality, instead of resolving these references immediately at load time, the interpreter resolves references only when they are invoked for the first time. This is known as lazy bindings**.**
-
After relocation is complete, the interpreter looks up the entry point of the binary and transfers control to it, beginning normal execution of the binary.
1.5 Summary
ELF binaries are divides into sections. Some sections contain code, and others contain data. |
**Why do you think the distinction between code and data sections exists?** |
**Symbols are organized into sections** – code lives in one section (.text), and data in another (.data, .rodata) |
|
Separating them allows them to be given different permissions. Variables (i.e. data) need to be writeable, but executable sections (i.e. code) should normally be read-only in order to prevent an attacker from overwriting them and changing their behavior. |
**How do you think the loading process differs for code and data
sections?**
At a high level: |
|
Linux loads the .text section into memory only once, no matter how many times an application is loaded. This reduces memory usage and launch time and is safe because the code doesn't change. For that reason, the .rodata section, which contains read-only initialized data, is packed into the same segment that contains the .text section. The .data section contains information that could be changed during application execution, so this section must be copied for every instance. |
The main code is stored in .text (flagged read-only and executable). Constants are stored in .rodata/.rdata (read-only), although Windows compilers often mix them into .text. Default values of initialized variables are stored in .data (writeable). Uninitialized variables have space reserved by .bss (writeable) when the process is set up, but do not take up any bytes in the binary on disk. |
**Is it necessary to copy all sections into memory when a binary is loaded for executions?** |
Loading a binary is a process that involves a lot of work by the operating system |
When we decide to run a binary, the operating system starts by setting up a new process for the program to run in, including a virtual address space.
-
Here the interpreter loads the binary into its virtual address space (the same space in which the interpreter is loaded).
-
It then parses the binary to find put (among other things) which dynamic libraries the binary uses.
-
The interpreter maps these into the virtual address space ( using mmap or equivalent function) and then performs any necessary last-minute relocations in the binary’s code sections to fill in the correct address for references to the dynamic libraries. In reality, instead of resolving these references immediately at load time, the interpreter resolves references only when they are invoked for the first time. This is known as lazy bindings**.**
-
After relocation is complete, the interpreter looks up the entry point of the binary and transfers control to it, beginning normal execution of the binary.
No. The division of the binary into sections is a convenient organization set up to be parseable by the linker. Some sections contain only data intended for the linker at link time, such as symbolic or relocation information. Much linking occurs before execution time, so some such sections do not need to be loaded into the process’s virtual memory for execution - although with lazy binding, many relocations can occur during execution (the .plt and .got.plt sections are used for this, and do need to be loaded). Sections are organized into groups called segments. Segments of type PT_LOAD are intended to be loaded into memory. The executable’s program headers describe the segments.