Reverse-engineering part 8: baby's first steps with delinking
Previously in this series of articles, we used Ghidra to craft a specially-designed executable artifact from our case study, one which still has all of the information required in its Ghidra database to delink it. In this article we will delink this executable back into a relocatable object file, using nothing but the information present in 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.
Reversing the linker, poorly
As with last time, recall the build workflow of our case study, shown all the way back in part 2:
We want to take that executable and turn it back into a relocatable object file. An object file contains three pieces of information:
- Relocatable section bytes ;
- Symbols ;
- Relocations.
Normally, an executable contains just relocated sections bytes, it has no symbols and no relocations. However, in the previous part we’ve linked an executable in a special way that preserves all that data. Let’s take advantage of that.
We are going to delink the following address ranges from that executable:
0x10110-0x104d3
: the entire.text
and.text.startup
sections, minus the functionprint_number()
;0x104d4-0x10643
: the entire.rodata
section.
Jython to the rescue
Unlike last time we are not going to craft an ELF file by hand within the Listing view of Ghidra, instead we will take advantage of Ghidra’s scripting capabilities to achieve our goals.
Click on Window > Python
and the scripting console will appear:
From here, we can run snippets written in Jython, an implementation of Python for the JVM that allows us to manipulate Java objects.
We interface with Ghidra through the variable currentProgram
of type ProgramDB.
This variable grants us access to the whole database program.
With it, we can access Ghidra’s API to perform all the operations we need on the program.
We will be only using the interactive scripting console here, but it is also possible to use pre-made scripts with the script manager, available by clicking on Window > Script Manager
.
Section bytes
First, we’ll import some modules we’ll need in our ELF object file crafting quest:
import jarray, struct
Then, we’ll define some constants about the sections we are going to work with:
_TEXT_START = currentProgram.parseAddress("0x10110")[0]
_TEXT_END = currentProgram.parseAddress("0x104d3")[0]
_TEXT_SET = currentProgram.getAddressFactory().getAddressSet(_TEXT_START, _TEXT_END)
_RODATA_START = _TEXT_END.next()
_RODATA_END = currentProgram.parseAddress("0x10643")[0]
_RODATA_SET = currentProgram.getAddressFactory().getAddressSet(_RODATA_START, _RODATA_END)
_EXTERNAL_START = currentProgram.parseAddress("0x10084")[0]
_EXTERNAL_END = _TEXT_START.previous()
_EXTERNAL_SET = currentProgram.getAddressFactory().getAddressSet(_EXTERNAL_START, _EXTERNAL_END)
Finally, we’ll grab the bytes of these sections:
_text_bytes = jarray.zeros(_TEXT_SET.getNumAddresses(), "b")
_rodata_bytes = jarray.zeros(_RODATA_SET.getNumAddresses(), "b")
currentProgram.getMemory().getBytes(_TEXT_START, _text_bytes)
currentProgram.getMemory().getBytes(_RODATA_START, _rodata_bytes)
These bytes are the raw relocated program bytes, but we want the unrelocated ones, so we’ll need to patch the original bytes from the relocations into the byte arrays we’ve fetched:
def patch_original_bytes(program, section_bytes, section_set):
for relocation in program.getRelocationTable().getRelocations(section_set):
offset = relocation.getAddress().subtract(section_set.getMinAddress())
for i in range(len(relocation.getBytes())):
section_bytes[offset + i] = relocation.getBytes()[i]
patch_original_bytes(currentProgram, _text_bytes, _TEXT_SET)
patch_original_bytes(currentProgram, _rodata_bytes, _RODATA_SET)
We have our unrelocated section bytes, now we need to build the symbol table for our object file.
Symbol table
We’ll define a helper function to retrieve a list of symbols from a set of addresses:
def get_symbols(program, symbol_set):
symbols = []
for symbol in program.getSymbolTable().getAllSymbols(False):
if symbol_set.contains(symbol.getAddress()):
symbols.append(symbol)
return symbols
We can then retrieve all the symbols for our symbol table:
symbols = [None]
FIRST_NONLOCAL_SYMBOL_IDX=len(symbols)
symbols += get_symbols(currentProgram, _TEXT_SET.subtract(_EXTERNAL_SET))
symbols += get_symbols(currentProgram, _RODATA_SET.subtract(_EXTERNAL_SET))
FIRST_UNDEFINED_SYMBOL_IDX=len(symbols)
symbols += get_symbols(currentProgram, _EXTERNAL_SET)
ELF symbol tables start with a null symbol, so we make room for one with a None
value.
They also need to specify the index to the first non-local symbol in their section header, we’ll record it in the FIRST_NONLOCAL_SYMBOL_IDX
variable for later use.
Furthermore, we need to know which symbols are undefined to annotate them correctly, we’ll record the index to the first one in the FIRST_UNDEFINED_SYMBOL_IDX
variable.
We can take a look at the symbols to ensure we didn’t miss anything:
>>> symbols
[None, print_ascii_entry, isalnum, isalpha, iscntrl, isdigit, isgraph, islower, isprint, ispunct, isspace, isupper, isxdigit, write, exit, __start, main, COLUMNS, s_ascii_properties, NUM_ASCII_PROPERTIES, _ctype_, print_number]
Everything is set up correctly, we can proceed with the relocation tables.
Relocation tables
We have our symbol table, now we need to build the relocation tables for our object file:
_text_rel = [i for i in currentProgram.getRelocationTable().getRelocations(_TEXT_SET.subtract(_EXTERNAL_SET))]
_rodata_rel = [i for i in currentProgram.getRelocationTable().getRelocations(_RODATA_SET.subtract(_EXTERNAL_SET))]
We have the basic data required for our ELF object file, it’s time to craft it.
Squeezing a working object file out of Ghidra’s database
To be able to create an ELF object file, we need the following:
- A valid ELF header of type
ET_REL
; - A list of ELF section headers ;
- The actual bytes referenced by the section headers.
We’ll build our ELF object file with the following section headers layout:
Index | Name | Type | Link field | Info field | Description |
---|---|---|---|---|---|
0 | (null) |
NULL |
0 | 0 | Null section (empty) |
1 | .shstrtab |
STRTAB |
0 | 0 | String table for section names |
2 | .strtab |
STRTAB |
0 | 0 | String table for symbol table |
3 | .symtab |
SYMTAB |
2 (.strtab ) |
0 | Symbol table |
4 | .text |
PROGBITS |
0 | 0 | .text section |
5 | .rodata |
PROGBITS |
0 | 0 | .rodata section |
6 | .text.rel |
REL |
3 (.symtab ) |
4 (.text ) |
Relocation table for .text section |
7 | .rodata.rel |
REL |
3 (.symtab ) |
5 (.rodata ) |
Relocation table for .rodata section |
8 | .reginfo |
MIPS_REGINFO |
4 (.text ) |
0 | MIPS-specific section |
Now that we have a plan for the section headers, let’s create the actual bytes for every one of those.
Section bytes for the string tables
We have the bytes for the .text
and .rodata
sections, but we’ll also need the bytes for the other sections.
Let’s start with the string tables by defining a function to build one:
def craft_string_table(strings):
data = ""
offsets = dict()
for string in strings:
offsets[string] = len(data)
data += (string + "\0").encode("ascii")
return data, offsets
We can then build the string table for the section names (.shstrtab
):
_shstrtab_bytes, shstrtab = craft_string_table(["", ".shstrtab", ".strtab", ".symtab", ".text", ".rodata", ".text.rel", ".rodata.rel", ".reginfo"])
Let’s check the contents of this section:
>>> _shstrtab_bytes
'\x00.shstrtab\x00.strtab\x00.symtab\x00.text\x00.rodata\x00.text.rel\x00.rodata.rel\x00.reginfo\x00''
>>> shstrtab
{'': 0, '.symtab': 19, '.text': 27, '.strtab': 11, '.reginfo': 63, '.rodata': 33, '.rodata.rel': 51, '.text.rel': 41, '.shstrtab': 1}
The first variable returned by craft_string_table
is the section bytes of the string table, the second variable is the dictionary containing the offsets of the strings within it.
Let’s also build the string table for the symbol table (.strtab
), taking into account the None
value present at the start:
_strtab_bytes, strtab = craft_string_table(map(lambda symbol: symbol.getName() if symbol != None else "", symbols))
We have the section bytes for the string tables, now it’s time for the symbol table.
Section bytes for the symbol table
The symbol table for a 32-bit ELF file is an array of the following structure:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
The equivalent representation using the struct
Python module is:
ELF32LE_SYM = "<IIIBBH"
ELF32LE_SYM_ENTSIZE = struct.calcsize(ELF32LE_SYM)
Like with the string tables, we’ll define a function to craft a symbol table:
STB_LOCAL = 0 << 4
STB_GLOBAL = 1 << 4
STT_NOTYPE = 0
def craft_symbol_table(symbols, first_undefined_symbol_idx, strings, sections):
data = bytearray()
offsets = dict()
for idx, symbol in enumerate(symbols):
if symbol == None:
name = ""
name_offset = strings[name]
info = STT_NOTYPE|STB_LOCAL
others = 0
section_idx = 0
value = 0
size = 0
elif idx < first_undefined_symbol_idx:
name = symbol.getName()
name_offset = strings[name]
info = STT_NOTYPE|STB_GLOBAL
others = 0
for address_set, section in sections.items():
if address_set.contains(symbol.getAddress()):
section_idx = section
value = symbol.getAddress().subtract(address_set.getMinAddress())
size = 0
else:
name = symbol.getName()
name_offset = strings[name]
info = STT_NOTYPE|STB_GLOBAL
others = 0
section_idx = 0
value = 0
size = 0
data += struct.pack(ELF32LE_SYM, name_offset, value, size, info, others, section_idx)
offsets[name] = idx
return data, offsets
We have three cases to handle:
- If the symbol is null, we emit a null symbol ;
- If the symbol is contained within the object file, we emit a global defined symbol ;
- If the symbol is not contained within the object file, we emit a global undefined symbol.
With this function, we can then craft our symbol table:
SECTIONS={_TEXT_SET: 4, _RODATA_SET: 5}
_symtab_bytes, symtab = craft_symbol_table(symbols, FIRST_UNDEFINED_SYMBOL_IDX, strtab, SECTIONS)
As with craft_string_table
, the first variable returned by craft_symbol_table
is the section bytes of the symbol table, the second variable is the dictionary containing the indexes of the symbols by name within it.
We have the string table and the symbol table, we can now deal with the relocation tables.
Section bytes for the relocation tables
The relocation table for a 32-bit ELF file is an array of the following structure:
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
The equivalent representation using the struct
Python module is:
ELF32LE_REL = "<II"
ELF32LE_REL_ENTSIZE = struct.calcsize(ELF32LE_REL)
Like with the symbol tables, we’ll define a function to craft a relocation table:
def craft_relocation_table(relocations, symbols, section_set):
data = bytearray()
for relocation in relocations:
offset = relocation.getAddress().subtract(section_set.getMinAddress())
symbol_idx = symbols[relocation.getSymbolName()]
data += struct.pack(ELF32LE_REL, offset, symbol_idx << 8 | relocation.getType())
return data
We can then craft our relocation tables:
_text_rel_bytes = craft_relocation_table(_text_rel, symtab, _TEXT_SET)
_rodata_rel_bytes = craft_relocation_table(_rodata_rel, symtab, _RODATA_SET)
We have the bytes for all the standard ELF sections we need, we’re only missing one MIPS-specific ELF section now.
Section bytes for the .reginfo section
The .reginfo
section is a MIPS-specific section required for our ELF object file.
We’ll just hardcode a value that will work for us:
_reginfo_bytes = bytearray("\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1C")
Finally, we have the section bytes for every section in our planned object file. We can now start putting it together.
Putting everything together
The object file we are building will consist of the ELF header, followed by the ELF section headers, followed by the contents of the section in numerical order.
Before we start scripting, we’ll need some constants in our ELF file structure packings:
ELFCLASS32 = 1
ELFDATA2LSB = 1
EV_CURRENT = 1
ELFOSABI_SYSV = 0
ELF_ET_REL = 1
ELF_EM_MIPS = 8
ELF_EV_CURRENT = 1
ELF32LE_HDR = "<4c12BHHIIIIIHHHHHH"
ELF32LE_HDR_SIZE = struct.calcsize(ELF32LE_HDR)
SHT_NULL = 0
SHT_PROGBITS = 1
SHT_SYMTAB = 2
SHT_STRTAB = 3
SHT_NOBITS = 8
SHT_REL = 9
SHT_MIPS_REGINFO = 0x70000006
SHF_WRITE = 0x1
SHF_ALLOC = 0x2
SHF_EXECINSTR = 0x4
ELF32LE_SHDR = "<IIIIIIIIII"
ELF32LE_SHDR_SIZE = struct.calcsize(ELF32LE_SHDR)
We’ll start with the ELF section headers:
# Name Type Flags Bytes Link Info Alignment Entry size
sections = [
("", SHT_NULL, 0, "", 0, 0, 0, 0),
(".shstrtab", SHT_STRTAB, 0, _shstrtab_bytes, 0, 0, 0, 0),
(".strtab", SHT_STRTAB, 0, _strtab_bytes, 0, 0, 0, 0),
(".symtab", SHT_SYMTAB, 0, _symtab_bytes, 2, FIRST_NONLOCAL_SYMBOL_IDX, 0, ELF32LE_SYM_ENTSIZE),
(".text", SHT_PROGBITS, SHF_ALLOC|SHF_EXECINSTR, _text_bytes, 0, 0, 4, 0),
(".rodata", SHT_PROGBITS, SHF_ALLOC, _rodata_bytes, 0, 0, 4, 0),
(".text.rel", SHT_REL, 0, _text_rel_bytes, 3, 4, 0, ELF32LE_REL_ENTSIZE),
(".rodata.rel", SHT_REL, 0, _rodata_rel_bytes, 3, 5, 0, ELF32LE_REL_ENTSIZE),
(".reginfo", SHT_MIPS_REGINFO, 0, _reginfo_bytes, 4, 0, 0, 0),
]
file_offset = ELF32LE_HDR_SIZE + ELF32LE_SHDR_SIZE * len(sections)
elf_section_headers_bytes = ""
for section in sections:
elf_section_headers_bytes += struct.pack(ELF32LE_SHDR,
shstrtab[section[0]], section[1], section[2], 0, file_offset, len(section[3]), section[4], section[5], section[6], section[7]
)
file_offset += len(section[3])
Once the section headers are packed, we’ll continue with the ELF header:
elf_header_bytes = struct.pack(ELF32LE_HDR,
'\x7f', 'E', 'L', 'F', ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_SYSV, 0, 0, 0, 0, 0, 0, 0, 0,
ELF_ET_REL, ELF_EM_MIPS, ELF_EV_CURRENT, 0, 0, ELF32LE_HDR_SIZE, 0x1000, ELF32LE_HDR_SIZE, 0, 0, ELF32LE_SHDR_SIZE, len(sections), 1
)
With the ELF header packed, we have all the bytes of our file. Time to write out all of its parts:
with open("ascii-table.relocatable.delinked.o", "w") as fp:
fp.write(elf_header_bytes)
fp.write(elf_section_headers_bytes)
for section in sections:
fp.write(bytearray(section[3]))
Finally, we have ascii-table.relocatable.delinked.o
in Ghidra’s current working directory, an object file crafted from bits and pieces of our executable file.
It’s high time we start playing with it.
It’s linkable, it’s linkable!
Like before, we can take a peek at this file using the toolchain:
$ mips-linux-gnu-objdump --wide --file-headers ascii-table.relocatable.delinked.o
ascii-table.relocatable.delinked.o: file format elf32-tradlittlemips
architecture: mips:3000, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
$ mips-linux-gnu-objdump --wide --section-headers ascii-table.relocatable.delinked.o
ascii-table.relocatable.delinked.o: file format elf32-tradlittlemips
Sections:
Idx Name Size VMA LMA File off Algn Flags
0 .text 000003c4 00000000 00000000 0000040d 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .rodata 00000170 00000000 00000000 000007d1 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
2 .reginfo 00000018 00000000 00000000 00000aa1 2**0 CONTENTS, READONLY, LINK_ONCE_SAME_SIZE
$ mips-linux-gnu-nm --format sysv --line-numbers --no-sort ascii-table.relocatable.delinked.o
Symbols from ascii-table.relocatable.delinked.o:
Name Value Class Type Size Line Section
print_ascii_entry |00000000| T | NOTYPE| | |.text
isalnum |00000104| T | NOTYPE| | |.text
isalpha |00000130| T | NOTYPE| | |.text
iscntrl |0000015c| T | NOTYPE| | |.text
isdigit |00000188| T | NOTYPE| | |.text
isgraph |000001b4| T | NOTYPE| | |.text
islower |000001e0| T | NOTYPE| | |.text
isprint |0000020c| T | NOTYPE| | |.text
ispunct |00000238| T | NOTYPE| | |.text
isspace |00000264| T | NOTYPE| | |.text
isupper |00000290| T | NOTYPE| | |.text
isxdigit |000002bc| T | NOTYPE| | |.text
write |000002f4| T | NOTYPE| | |.text
exit |00000304| T | NOTYPE| | |.text
__start |0000030c| T | NOTYPE| | |.text
main |00000324| T | NOTYPE| | |.text
COLUMNS |00000000| R | NOTYPE| | |.rodata
s_ascii_properties |00000004| R | NOTYPE| | |.rodata
NUM_ASCII_PROPERTIES|00000054| R | NOTYPE| | |.rodata
_ctype_ |00000060| R | NOTYPE| | |.rodata
print_number | | U | NOTYPE| | |*UND*
$ mips-linux-gnu-objdump --wide --reloc ascii-table.relocatable.delinked.o
ascii-table.relocatable.delinked.o: file format elf32-tradlittlemips
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000028 R_MIPS_26 print_number
0000003c R_MIPS_26 write
00000044 R_MIPS_26 isgraph
00000064 R_MIPS_26 write
00000080 R_MIPS_26 write
000000e4 R_MIPS_26 write
00000110 R_MIPS_HI16 _ctype_
00000118 R_MIPS_LO16 _ctype_
0000013c R_MIPS_HI16 _ctype_
00000144 R_MIPS_LO16 _ctype_
00000168 R_MIPS_HI16 _ctype_
00000170 R_MIPS_LO16 _ctype_
00000194 R_MIPS_HI16 _ctype_
0000019c R_MIPS_LO16 _ctype_
000001c0 R_MIPS_HI16 _ctype_
000001c8 R_MIPS_LO16 _ctype_
000001ec R_MIPS_HI16 _ctype_
000001f4 R_MIPS_LO16 _ctype_
00000218 R_MIPS_HI16 _ctype_
00000220 R_MIPS_LO16 _ctype_
00000244 R_MIPS_HI16 _ctype_
0000024c R_MIPS_LO16 _ctype_
00000270 R_MIPS_HI16 _ctype_
00000278 R_MIPS_LO16 _ctype_
0000029c R_MIPS_HI16 _ctype_
000002a4 R_MIPS_LO16 _ctype_
000002c8 R_MIPS_HI16 _ctype_
000002d0 R_MIPS_LO16 _ctype_
00000314 R_MIPS_26 main
0000031c R_MIPS_26 exit
0000032c R_MIPS_HI16 s_ascii_properties
0000033c R_MIPS_LO16 s_ascii_properties
0000036c R_MIPS_26 print_ascii_entry
00000390 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
At first glance it looks like a normal, everyday object file stripped of its debugging symbols. Careful examination does reveal differences with object files emitted by the toolchain so far, like symbols being untyped and without size, or sections appearing in a different order than usual. At any rate, despite its unconventional origins this is a valid ELF object file, as we’ll demonstrate next.
Back to our long-running theme, we’ll modify the print_number()
function so that it prints an integer in octal.
We’ll prepare a file named print_number_octal.c
with the following contents:
#include "stdio.h"
void print_number(int num) {
for (int n = 3; n >= 0; n--) {
int digit = (num >> (3 * n)) % 8;
putchar('0' + digit);
}
}
We will then make an executable from the reconstructed object file and this source file:
$ make CC=mips-linux-gnu-gcc print_number_octal.o
mips-linux-gnu-gcc -Os -g -EL -ffreestanding -fno-pic -fno-plt -nostdinc -I. -mno-abicalls -mno-check-zero-division -fverbose-asm -S -o print_number_octal.S print_number_octal.c
mips-linux-gnu-gcc -Os -g -EL -ffreestanding -fno-pic -fno-plt -nostdinc -I. -mno-abicalls -mno-check-zero-division -c -o print_number_octal.o print_number_octal.S
$ mips-linux-gnu-gcc -EL -static -no-pie -nostdlib -o ascii-table.relocatable.delinked.elf ascii-table.relocatable.delinked.o print_number_octal.o
At long last we have crafted the object of our quest, ascii-table.relocatable.delinked.elf
.
Of course, the final thing to do is ensure that our dark magic ritual actually worked:
$ qemu-mipsel ascii-table.relocatable.delinked.elf
0000 c 0040 p s 0100 @ gp ! 0140 ` gp !
0001 c 0041 ! gp ! 0101 A gp Aa U 0141 a gp Aa l
0002 c 0042 " gp ! 0102 B gp Aa U 0142 b gp Aa l
0003 c 0043 # gp ! 0103 C gp Aa U 0143 c gp Aa l
0004 c 0044 $ gp ! 0104 D gp Aa U 0144 d gp Aa l
0005 c 0045 % gp ! 0105 E gp Aa U 0145 e gp Aa l
0006 c 0046 & gp ! 0106 F gp Aa U 0146 f gp Aa l
0007 c 0047 ' gp ! 0107 G gp Aa U 0147 g gp Aa l
0010 c 0050 ( gp ! 0110 H gp Aa U 0150 h gp Aa l
0011 cs 0051 ) gp ! 0111 I gp Aa U 0151 i gp Aa l
0012 cs 0052 * gp ! 0112 J gp Aa U 0152 j gp Aa l
0013 cs 0053 + gp ! 0113 K gp Aa U 0153 k gp Aa l
0014 cs 0054 , gp ! 0114 L gp Aa U 0154 l gp Aa l
0015 cs 0055 - gp ! 0115 M gp Aa U 0155 m gp Aa l
0016 c 0056 . gp ! 0116 N gp Aa U 0156 n gp Aa l
0017 c 0057 / gp ! 0117 O gp Aa U 0157 o gp Aa l
0020 c 0060 0 gp A d 0120 P gp Aa U 0160 p gp Aa l
0021 c 0061 1 gp A d 0121 Q gp Aa U 0161 q gp Aa l
0022 c 0062 2 gp A d 0122 R gp Aa U 0162 r gp Aa l
0023 c 0063 3 gp A d 0123 S gp Aa U 0163 s gp Aa l
0024 c 0064 4 gp A d 0124 T gp Aa U 0164 t gp Aa l
0025 c 0065 5 gp A d 0125 U gp Aa U 0165 u gp Aa l
0026 c 0066 6 gp A d 0126 V gp Aa U 0166 v gp Aa l
0027 c 0067 7 gp A d 0127 W gp Aa U 0167 w gp Aa l
0030 c 0070 8 gp A d 0130 X gp Aa U 0170 x gp Aa l
0031 c 0071 9 gp A d 0131 Y gp Aa U 0171 y gp Aa l
0032 c 0072 : gp ! 0132 Z gp Aa U 0172 z gp Aa l
0033 c 0073 ; gp ! 0133 [ gp ! 0173 { gp !
0034 c 0074 < gp ! 0134 \ gp ! 0174 | gp !
0035 c 0075 = gp ! 0135 ] gp ! 0175 } gp !
0036 c 0076 > gp ! 0136 ^ gp ! 0176 ~ gp !
0037 c 0077 ? gp ! 0137 _ gp ! 0177 c
Once more, we have successfully modified our case study to print the ASCII table in octal. Only this time we’ve violated the natural order of the toolchain building flow in the process, by turning bits and pieces of an executable file back into an object file.
The files for this case study can be found here: case-study.tar.gz
Conclusion
We have successfully delinked parts of a specially-crafted executable back into a relocatable object file with lots of Python script snippets and used it to make a new, modified executable. Next time, no more tricks up our sleeves: we’ll remove the training wheels and we’ll learn how to identify and reconstruct relocations, so that we can delink normal executables too.