Reverse-engineering part 7: setting up the stage for delinking (training wheels edition)
Previously in this series of articles, we used Ghidra to reverse-engineer and perform a binary patch on our case study. In this article we will craft an executable artifact from our case study in an unusual manner, one in which all of the information needed to delink it is already inside the Ghidra database.
This article is written using an unmodified Ghidra 10.2 instance for demonstrative purposes only. Do not actually perform this at home with real artifacts ; instead, refer to this article that shows how to automate all of this sanely.
Reinventing the linker, poorly
Recall the build workflow of our case study, shown all the way back in part 2:
In the last step of the build, the linker takes the object files and then:
- lays out their sections in memory ;
- computes the final addresses of all symbols ;
- applies all the relocations within the sections according to the symbols’ addresses.
After this process, the relocations are discarded and the symbols are stripped, leaving only the relocated sections in the final executable. Or at least, that is usually what happens when using a traditional linker.
Ghidra, the poor man’s linker with training wheels
Ghidra works within a virtualized address space, similar to that of a conventional user-space process. When importing an ELF file, Ghidra must load it into this address space before it can be analyzed. In order to analyze relocatable and/or dynamically-linked executables, Ghidra can process relocations inside an ELF file and import shared libraries if instructed so, just like how a dynamic linker would.
That being said, Ghidra is not meant to be general-purpose linker ; our case study is made of three object files and Ghidra does not allow importing multiple ELF object files into one program. However, it can import and load one ELF object file, which we’ll (ab)use to our advantage here.
We can instruct the ELF toolchain to merge multiple object files into one:
$ mips-linux-gnu-ld -EL -r ascii-table.o ctype.o libstd.o -o ascii-table.relocatable.o
The output is an object file that contains all of the input object files, combined into one. Note that this object file has no undefined symbols and is therefore self-sufficient:
$ mips-linux-gnu-objdump --wide --file-headers ascii-table.relocatable.o
ascii-table.relocatable.o: file format elf32-tradlittlemips
architecture: mips:isa32r2, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
$ mips-linux-gnu-objdump --wide --section-headers ascii-table.relocatable.o
ascii-table.relocatable.o: file format elf32-tradlittlemips
Sections:
Idx Name Size VMA LMA File off Algn Flags
0 .MIPS.abiflags 00000018 00000000 00000000 00000038 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_SAME_SIZE
1 .reginfo 00000018 00000000 00000000 00000050 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_SAME_SIZE
2 .text 000003b0 00000000 00000000 00000070 2**4 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
3 .text.startup 000000a0 00000000 00000000 00000420 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
4 .rodata 00000170 00000000 00000000 000004c0 2**4 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
5 .data 00000000 00000000 00000000 00000630 2**4 CONTENTS, ALLOC, LOAD, DATA
6 .bss 00000000 00000000 00000000 00000630 2**4 ALLOC
7 .comment 00000078 00000000 00000000 00000630 2**0 CONTENTS, READONLY
8 .pdr 00000220 00000000 00000000 000006a8 2**2 CONTENTS, RELOC, READONLY
9 .note.GNU-stack 00000000 00000000 00000000 000008c8 2**0 CONTENTS, READONLY
10 .debug_aranges 00000068 00000000 00000000 000008c8 2**0 CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
11 .debug_info 00000805 00000000 00000000 00000930 2**0 CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
12 .debug_abbrev 0000033f 00000000 00000000 00001135 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS
13 .debug_line 0000040d 00000000 00000000 00001474 2**0 CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
14 .debug_frame 000001a8 00000000 00000000 00001884 2**2 CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
15 .debug_str 00000444 00000000 00000000 00001a2c 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS
16 .debug_loc 0000048d 00000000 00000000 00001e70 2**0 CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
17 .debug_ranges 000001b0 00000000 00000000 000022fd 2**0 CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
18 .gnu.attributes 00000010 00000000 00000000 000024ad 2**0 CONTENTS, READONLY
19 .mdebug.abi32 00000000 00000000 00000000 000024bd 2**0 CONTENTS, READONLY
$ mips-linux-gnu-nm --format sysv --line-numbers --no-sort ascii-table.relocatable.o
Symbols from ascii-table.relocatable.o:
Name Value Class Type Size Line Section
s_ascii_properties |00000004| R | OBJECT|00000050| |.rodata /home/jblbeurope/Documents/samples/ascii-table.c:11
NUM_ASCII_PROPERTIES|00000054| R | OBJECT|00000004| |.rodata /home/jblbeurope/Documents/samples/ascii-table.c:10
islower |0000026c| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:111
ispunct |000002c4| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:121
isspace |000002f0| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:126
isxdigit |00000348| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:136
write |00000380| T | FUNC|00000010| |.text /home/jblbeurope/Documents/samples/libstd.c:12
__start |00000398| T | FUNC|00000018| |.text /home/jblbeurope/Documents/samples/libstd.c:44
print_number |00000000| T | FUNC|0000008c| |.text /home/jblbeurope/Documents/samples/ascii-table.c:26
isupper |0000031c| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:131
isalpha |000001bc| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:91
main |00000000| T | FUNC|000000a0| |.text.startup /home/jblbeurope/Documents/samples/ascii-table.c:57
isgraph |00000240| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:106
isalnum |00000190| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:86
isprint |00000298| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:116
print_ascii_entry |0000008c| T | FUNC|000000fc| |.text /home/jblbeurope/Documents/samples/ascii-table.c:37
COLUMNS |00000000| R | OBJECT|00000004| |.rodata /home/jblbeurope/Documents/samples/ascii-table.c:24
isdigit |00000214| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:101
exit |00000390| T | FUNC|00000008| |.text /home/jblbeurope/Documents/samples/libstd.c:27
iscntrl |000001e8| T | FUNC|0000002c| |.text /home/jblbeurope/Documents/samples/ctype.c:96
_ctype_ |00000060| R | OBJECT|00000101| |.rodata /home/jblbeurope/Documents/samples/ctype.c:49
$ mips-linux-gnu-objdump --wide --reloc ascii-table.relocatable.o
ascii-table.relocatable.o: file format elf32-tradlittlemips
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000058 R_MIPS_26 write
000000b4 R_MIPS_26 print_number
000000c8 R_MIPS_26 write
000000d0 R_MIPS_26 isgraph
000000f0 R_MIPS_26 write
0000010c R_MIPS_26 write
00000170 R_MIPS_26 write
0000019c R_MIPS_HI16 _ctype_
000001a4 R_MIPS_LO16 _ctype_
000001c8 R_MIPS_HI16 _ctype_
000001d0 R_MIPS_LO16 _ctype_
000001f4 R_MIPS_HI16 _ctype_
000001fc R_MIPS_LO16 _ctype_
00000220 R_MIPS_HI16 _ctype_
00000228 R_MIPS_LO16 _ctype_
0000024c R_MIPS_HI16 _ctype_
00000254 R_MIPS_LO16 _ctype_
00000278 R_MIPS_HI16 _ctype_
00000280 R_MIPS_LO16 _ctype_
000002a4 R_MIPS_HI16 _ctype_
000002ac R_MIPS_LO16 _ctype_
000002d0 R_MIPS_HI16 _ctype_
000002d8 R_MIPS_LO16 _ctype_
000002fc R_MIPS_HI16 _ctype_
00000304 R_MIPS_LO16 _ctype_
00000328 R_MIPS_HI16 _ctype_
00000330 R_MIPS_LO16 _ctype_
00000354 R_MIPS_HI16 _ctype_
0000035c R_MIPS_LO16 _ctype_
000003a0 R_MIPS_26 main
000003a8 R_MIPS_26 exit
RELOCATION RECORDS FOR [.text.startup]:
OFFSET TYPE VALUE
00000008 R_MIPS_HI16 s_ascii_properties
00000018 R_MIPS_LO16 s_ascii_properties
00000048 R_MIPS_26 print_ascii_entry
0000006c R_MIPS_26 write
RELOCATION RECORDS FOR [.rodata]:
OFFSET TYPE VALUE
00000004 R_MIPS_32 isgraph
0000000c R_MIPS_32 isprint
00000014 R_MIPS_32 iscntrl
0000001c R_MIPS_32 isspace
00000024 R_MIPS_32 ispunct
0000002c R_MIPS_32 isalnum
00000034 R_MIPS_32 isalpha
0000003c R_MIPS_32 isdigit
00000044 R_MIPS_32 isupper
0000004c R_MIPS_32 islower
We then import that combined object file into Ghidra (with an image base of 0x10054
, for reasons that will be explained later), which will effectively link it into an executable.
However, unlike a traditional linker Ghidra will keep all of the information which is usually discarded.
For example, clicking Window > Relocation Table
allows us to take a peek at the relocation table:
We can see all the relocations that Ghidra processed as part of the loading process.
Crucially, Ghidra also recorded the original, unrelocated bytes of the artifact in this table, inside the Original Bytes
column of the relocations.
These are our training wheels for the delinking technique: despite the linking process done to this object file, all of the information from the original object file is still in the Ghidra database.
To be fair, this is a rather unusual way to link an executable. To convince ourselves that we indeed have a working executable in front of us, we can try to execute it. However, this will require a bit more work to achieve this.
Squeezing a working executable out of Ghidra’s linker
To be able to run an ELF executable, we need the following:
- A valid ELF header of type
ET_EXEC
with an entry point ; - A list of ELF program headers (or segments) ;
- The actual bytes referenced by the program headers.
We have the bytes since Ghidra loaded the object file into memory and linked it, but we’re missing the rest because Ghidra isn’t actually supposed to be used as a linker.
We’ll have to fill in the missing pieces by hand ; we’ll start with the ELF header.
Click Window > Memory Map
to display the memory map of the program:
Each entry in this table is a block of memory currently loaded into the program.
Most are sections taken straight from the ELF file, some like _elfHeader
or _elfSectionHeaders
are ELF file structures.
We’ll add a new block that we can use to hold the executable ELF header we are about to craft.
Click on the green cross with the tooltip Add a new block to memory
and fill in the dialog as follows:
We now have a place where we can write our own ELF header.
Double-click on elfHeader.executable
in the Program Trees panel and retype the whole fragment as Elf32_Ehdr
.
The block was initialized with zeroes, we’ll patch the following values by right-clicking them in the Listing panel and selecting Patch Data
(or hitting Ctrl-Shift-H
):
Field | Value |
---|---|
e_ident_magic_num |
0x7f |
e_ident_magic_str |
0x45 0x4c 0x46 (ELF) |
e_ident_class |
1 (ELFCLASS32) |
e_ident_data |
1 (ELFDATA2LSB) |
e_ident_version |
1 (EVCURRENT) |
e_type |
2 (ET_EXEC) |
e_machine |
8 (EM_MIPS) |
e_version |
1 (EVCURRENT) |
e_entry |
0x0001041c (__start) |
e_phoff |
52 (e_ehsize) |
e_ehsize |
52 |
e_phentsize |
32 |
e_phnum |
1 |
We’ve written by hand an ELF header for an executable file, which has one program header placed right after the ELF header. We need that program header (or segment) in order to instruct the program loader to correctly load our executable into memory, but we haven’t crafted it yet.
We’ll create another memory block named elfProgramHeaders.executable
of size 32 bytes to hold the program header.
Sadly there is no definition for a program header in the data type library (the object file doesn’t have program headers so Ghidra didn’t instantiate the type), but we can still type the individual fields and patch in the data:
Field | Offset | Size | Value |
---|---|---|---|
p_type |
0 | 4 | 1 (PT_LOAD) |
p_offset |
4 | 4 | 0 (start of file) |
p_vaddr |
8 | 4 | 0x0010000 (start of program) |
p_paddr |
12 | 4 | 0x0010000 (p_vaddr) |
p_filesz |
16 | 4 | 1604 (size of program) |
p_memsz |
20 | 4 | 1604 (p_filesz) |
p_flags |
24 | 4 | 0x5 (PF_R|PF_X) |
p_align |
28 | 4 | 0x10000 (align to 64 KiB) |
Note that this segment includes the ELF header and the program headers.
We used an image base of 0x10054
when importing the object file, to make room for the ELF header and the program headers at the beginning of the segment.
This isn’t strictly necessary, but segments must be page-aligned ; if the segment starts loading from file offset 0, then we do not need to worry about dealing with alignment and proper padding when concatenating the file.
Now that we have all of the pieces for our executable, we need to export and assemble these into an actual executable file.
Select the following address ranges (using Select > Bytes...
) and export the selection as a binary file by hitting the O
key (or clicking on File > Export Program...
):
Start | Size | File name |
---|---|---|
elfHeader.executable:00000000 |
0x36 |
ascii-table.relocatable.elfHeader.bin |
elfProgramHeaders.executable:00000000 |
0x20 |
ascii-table.relocatable.elfProgramHeaders.bin |
0010054 |
0x5f0 |
ascii-table.relocatable.data.bin |
Then, concatenate the parts into a valid executable file:
$ cat ascii-table.relocatable.elfHeader.bin \
ascii-table.relocatable.elfProgramHeaders.bin \
ascii-table.relocatable.data.bin \
> ascii-table.relocatable.elf
$ chmod +x ascii-table.relocatable.elf
It’s executable, it’s executable!
We can take a peek at this file using the toolchain:
$ mips-linux-gnu-objdump --wide --file-headers ascii-table.relocatable.elf
ascii-table.relocatable.elf: file format elf32-tradlittlemips
architecture: mips:3000, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x0001041c
$ mips-linux-gnu-objdump --wide --section-headers ascii-table.relocatable.elf
ascii-table.relocatable.elf: file format elf32-tradlittlemips
Sections:
Idx Name Size VMA LMA File off Algn Flags
$ mips-linux-gnu-nm --format sysv --line-numbers --no-sort ascii-table.relocatable.elf
Symbols from ascii-table.relocatable.elf:
Name Value Class Type Size Line Section
mips-linux-gnu-nm: ascii-table.relocatable.elf: no symbols
$ mips-linux-gnu-objdump --wide --reloc ascii-table.relocatable.elf
ascii-table.relocatable.elf: file format elf32-tradlittlemips
When compared to the original ascii-table.elf
artifact, there is a lot of information missing from this file.
This is because most of it isn’t actually necessary to run an executable: the program loader only cares about the ELF header and the program headers.
We simply did not synthesize that superfluous information when creating the ELF file structures by hand.
Nevertheless, this is a valid executable and as such can be run:
$ qemu-mipsel ascii-table.relocatable.elf
0000 c 0020 p s 0040 @ gp ! 0060 ` gp !
0001 c 0021 ! gp ! 0041 A gp Aa U 0061 a gp Aa l
0002 c 0022 " gp ! 0042 B gp Aa U 0062 b gp Aa l
0003 c 0023 # gp ! 0043 C gp Aa U 0063 c gp Aa l
0004 c 0024 $ gp ! 0044 D gp Aa U 0064 d gp Aa l
0005 c 0025 % gp ! 0045 E gp Aa U 0065 e gp Aa l
0006 c 0026 & gp ! 0046 F gp Aa U 0066 f gp Aa l
0007 c 0027 ' gp ! 0047 G gp Aa U 0067 g gp Aa l
0008 c 0028 ( gp ! 0048 H gp Aa U 0068 h gp Aa l
0009 cs 0029 ) gp ! 0049 I gp Aa U 0069 i gp Aa l
000a cs 002a * gp ! 004a J gp Aa U 006a j gp Aa l
000b cs 002b + gp ! 004b K gp Aa U 006b k gp Aa l
000c cs 002c , gp ! 004c L gp Aa U 006c l gp Aa l
000d cs 002d - gp ! 004d M gp Aa U 006d m gp Aa l
000e c 002e . gp ! 004e N gp Aa U 006e n gp Aa l
000f c 002f / gp ! 004f O gp Aa U 006f o gp Aa l
0010 c 0030 0 gp A d 0050 P gp Aa U 0070 p gp Aa l
0011 c 0031 1 gp A d 0051 Q gp Aa U 0071 q gp Aa l
0012 c 0032 2 gp A d 0052 R gp Aa U 0072 r gp Aa l
0013 c 0033 3 gp A d 0053 S gp Aa U 0073 s gp Aa l
0014 c 0034 4 gp A d 0054 T gp Aa U 0074 t gp Aa l
0015 c 0035 5 gp A d 0055 U gp Aa U 0075 u gp Aa l
0016 c 0036 6 gp A d 0056 V gp Aa U 0076 v gp Aa l
0017 c 0037 7 gp A d 0057 W gp Aa U 0077 w gp Aa l
0018 c 0038 8 gp A d 0058 X gp Aa U 0078 x gp Aa l
0019 c 0039 9 gp A d 0059 Y gp Aa U 0079 y gp Aa l
001a c 003a : gp ! 005a Z gp Aa U 007a z gp Aa l
001b c 003b ; gp ! 005b [ gp ! 007b { gp !
001c c 003c < gp ! 005c \ gp ! 007c | gp !
001d c 003d = gp ! 005d ] gp ! 007d } gp !
001e c 003e > gp ! 005e ^ gp ! 007e ~ gp !
001f c 003f ? gp ! 005f _ gp ! 007f c
We can observe that this Frankenstein’s monster of an executable file does work and we are treated to the familiar sight of our ASCII table of our case study, just like with the original ascii-table.elf
.
Conclusion
We have crafted an executable artifact based on our case study which still has all of its original, pre-linkage information in its Ghidra database. Next time, no more stalling: we will actually delink this executable back into a relocatable object file.