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 function print_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_END = currentProgram.parseAddress("0x10643")[0]
_RODATA_SET = currentProgram.getAddressFactory().getAddressSet(_RODATA_START, _RODATA_END)

_EXTERNAL_START = currentProgram.parseAddress("0x10084")[0]
_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 an 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()):
    return symbols

We can then retrieve all the symbols for our symbol table:

symbols = [None]
symbols += get_symbols(currentProgram, _TEXT_SET.subtract(_EXTERNAL_SET))
symbols += get_symbols(currentProgram, _RODATA_SET.subtract(_EXTERNAL_SET))
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
>>> 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_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

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
            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:

_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_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:


ELF32LE_HDR_SIZE = struct.calcsize(ELF32LE_HDR)

SHT_MIPS_REGINFO = 0x70000006


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:
    for section in sections:

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:
start address 0x00000000

$ mips-linux-gnu-objdump --wide --section-headers ascii-table.relocatable.delinked.o

ascii-table.relocatable.delinked.o:     file format elf32-tradlittlemips

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

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

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


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.