Previously in this series of articles, we delinked an executable back into a relocatable object file with Ghidra, partly by (ab)using Ghidra as a linker in order to preserve the data (most notably relocations and original section bytes) required to do so. In this article we will do the same thing, only with an executable file linked by a standard linker.

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.

No more training wheels

This time, no more cheating by (ab)using Ghidra as a linker, we will delink the original ascii-table.elf artifact that we built all the way back in part 2 fair and square. We have already learned in part 6 how to recreate symbols and type information by hand if necessary, so we’ll let Ghidra use the debugging symbols in this file to skip that step of the reverse-engineering process.

Let’s import the executable into Ghidra and…

…Oh, that’s right. We don’t have any relocations in the relocation table for this file. Since the standard linker threw that information out as part of its linking process, this means we don’t have the original, unrelocated section bytes required by our script snippets used in the previous article to work. Nevertheless, with some heuristics we can reconstruct that missing data and perform the delinking procedure anyway.

MIPS relocations 101

The MIPS® RISC Processor Supplement of the System V Application Binary Interface documents everything MIPS-specific for the ELF file format, including relocations. In general, ELF relocations describe a patching operation that involves the address of a symbol plus an addend that acts as an offset. Since the MIPS architecture uses the REL relocation type, the addend is stored implicitly, at the location to be modified by the relocation.

R_MIPS_32

For the MIPS architecture, ELF relocations for data are done with the R_MIPS_32 (0x2) relocation type. This relocation patches a 32-bit value with the address of a symbol, which is useful for generic pointers.

Bring up the symbol table with Ctrl-T (or click on Window > Symbol Table) and find the isgraph symbol. Right-click on it in the symbol table and select Symbol References:

We have one interesting data symbol reference at address 0x400584, double-click on the reference to display it in the listing view:

It’s the s_ascii_properties array, whose elements consists of a pointer to a character classification function and the character to display in the ASCII table.

We can observe that for the isgraph reference, the matches field of the structure contains the address of the isgraph symbol (0x00404010). This means there was originally a R_MIPS_32 reference at this field for the symbol isgraph and that the original bytes were 00 00 00 00 because the addend is zero (i.e. we want the address of isgraph without applying an offset). We’ll add that reference using the Python console:

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400584")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isgraph")

While we’re here, we’ll also annotate the rest of the references in this table:

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040058c")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isprint")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400594")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "iscntrl")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040059c")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isspace")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004005a4")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "ispunct")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004005ac")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isalnum")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004005b4")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isalpha")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004005bc")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isdigit")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004005c4")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "isupper")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004005cc")[0], 0x2, [], [0x00, 0x00, 0x00, 0x00], "islower")

As noted above, this article is written using an unmodified Ghidra 10.2 instance for demonstrative purposes only. There is no way to delete or modify a relocation after one is added. Furthermore, before Ghidra 10.3 the relocation window will not automatically refresh after adding a relocation.

R_MIPS_26

We have another interesting call symbol reference for isgraph located at address 0x4002a0, let’s study it:

        004002a0 04 01 10 0c     jal        isgraph                                          int isgraph(int _c)

Here, JAL is a MIPS instruction used for function calls. This instruction contains the immediate value 0x100104, which shifted by two bits to the left yields the value 0x400410, the address of the isgraph function. This is consistent with a R_MIPS_26 (0x4) relocation with an addend of 0x0, let’s create a relocation for it:

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004002a0")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "isgraph")

The values of the original bytes matches the encoding of the JAL instruction with an immediate value of 0x0, hence the 0x0c byte.

Let’s annotate the rest of the direct function calls (they can be found using Search > For Instructions Patterns):

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400178")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "print_ascii_entry")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040019c")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "write")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400228")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "write")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400284")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "print_number")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400298")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "write")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004002c0")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "write")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004002dc")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "write")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400340")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "write")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400570")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "main")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400578")[0], 0x4, [], [0x00, 0x00, 0x00, 0x0c], "exit")

R_MIPS_HI16/R_MIPS_LO16 without an addend

The MIPS instruction set can’t directly load a 32 bit constant (or at least not until MIPS R6 with its ALUIPC instruction when used for constant pools). Instead, MIPS loads 32 bit constants in two steps:

  • First, the LUI instruction loads a constant into the upper 16 bits of a 32 bit register, clearing the lower 16 bits to 0 ;
  • Then, another instruction (ADDIU, LW, SW…) provides the lower 16 bits of the constant.

The R_MIPS_HI16 (0x5) and R_MIPS_LO16 (0x6) relocations are designed to work together within this pattern.

Checking the references for the symbol s_ascii_properties, we have one interesting data reference at address 0x400174, inside the main function:

                             //
                             // .text 
                             // SHT_PROGBITS  [0x400130 - 0x40057f]
                             // ram:00400130-ram:0040057f
                             //
                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             int __stdcall main(void)
                               assume gp = 0x4186e0
             int               v0:4           <RETURN>
             undefined4        Stack[-0x4]:4  local_4                                 XREF[2]:     00400158(W), 
                                                                                                   004001ac(R)  
             undefined4        Stack[-0x8]:4  local_8                                 XREF[2]:     0040013c(W), 
                                                                                                   004001b4(R)  
             undefined4        Stack[-0xc]:4  local_c                                 XREF[2]:     00400144(W), 
                                                                                                   004001b8(R)  
             undefined4        Stack[-0x10]:4 local_10                                XREF[2]:     00400134(W), 
                                                                                                   004001bc(R)  
             undefined4        Stack[-0x14]:4 local_14                                XREF[2]:     0040015c(W), 
                                                                                                   004001c0(R)  
             undefined4        Stack[-0x18]:4 local_18                                XREF[2]:     0040014c(W), 
                                                                                                   004001c4(R)  
             char              Stack[-0x20]:1 _putch_data                             XREF[1]:     004001a0(W)  
                             _ftext                                          XREF[4]:     Entry Point(*), 
                             main                                                         __start:00400570(c), 
                                                                                          .debug_frame::00000078(*), 
                                                                                          _elfSectionHeaders::000000ac(*)  
        00400130 d0 ff bd 27     addiu      sp,sp,-0x30
             assume gp = <UNKNOWN>
        00400134 20 00 b2 af     sw         s2,local_10(sp)
        00400138 40 00 12 3c     lui        s2,0x40
        0040013c 28 00 b4 af     sw         s4,local_8(sp)
        00400140 80 00 14 24     li         s4,0x80
        00400144 24 00 b3 af     sw         s3,local_c(sp)
        00400148 84 05 52 26     addiu      s2,s2,0x584
        0040014c 18 00 b0 af     sw         s0,local_18(sp)
        00400150 09 00 13 24     li         s3,0x9
        00400154 25 80 00 00     or         s0,zero,zero
        00400158 2c 00 bf af     sw         ra,local_4(sp)
        0040015c 1c 00 b1 af     sw         s1,local_14(sp)
        00400160 03 00 11 32     andi       s1,s0,0x3
                             LAB_00400164                                    XREF[1]:     004001a4(j)  
        00400164 83 10 10 00     sra        v0,s0,0x2
        00400168 40 21 11 00     sll        a0,s1,0x5
        0040016c 0a 00 06 24     li         a2,0xa
        00400170 21 20 82 00     addu       a0,a0,v0
        00400174 25 28 40 02     or         a1=>s_ascii_properties,s2,zero

At address 0x400174, we have an OR instruction whose a1 register operand is marked as a reference to s_ascii_properties. If we trace backwards how the value of the register a1 at this address is calculated inside this function, we obtain this chain of instructions:

        00400138 40 00 12 3c     lui        s2,0x40
...
        00400148 84 05 52 26     addiu      s2,s2,0x584
...
        00400174 25 28 40 02     or         a1=>s_ascii_properties,s2,zero

Breaking this down, we have the following execution trace:

  • First, the LUI instruction loads the constant value 0x400000 to the register s2 ;
  • Then, the ADDIU instruction adds the constant value 0x584 to the register s2, which yields the value 0x400584 ;
  • Finally, the OR instruction moves the value inside the register s2 to the register a1, at which point Ghidra annotated this instruction as a reference.

The value 0x400584 is actually the address of s_ascii_properties. We can therefore conclude that there was originally a MIPS_HI16/MIPS_LO16 pair of relocations for the LUI/ADDIU instructions, targeting the symbol s_ascii_table, with an addend of 0x0. Let’s create two relocations using the Python console:

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400138")[0], 0x5, [], [0x00, 0x00, 0x12, 0x3c], "s_ascii_properties")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400148")[0], 0x6, [], [0x00, 0x00, 0x52, 0x26], "s_ascii_properties")

Like with R_MIPS_26, we need to be careful to clear out only the immediate fields, which are the lower 16 bits of the instructions.

R_MIPS_HI16/R_MIPS_LO16 with an addend

We have one last set of relocations to reconstruct. Let’s check out the isalnum function:

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             int __stdcall isalnum(int _c)
                               assume gp = 0x4186e0
             int               v0:4           <RETURN>
             int               a0:4           _c
                             isalnum                                         XREF[3]:     Entry Point(*), 004005ac(*), 
                                                                                          .debug_frame::000000b8(*)  
        00400360 ff ff 03 24     li         v1,-0x1
             assume gp = <UNKNOWN>
        00400364 07 00 83 10     beq        _c,v1,LAB_00400384
        00400368 25 10 00 00     _or        v0,zero,zero
        0040036c 40 00 02 3c     lui        v0,0x40
        00400370 ff 00 84 30     andi       _c,_c,0xff
        00400374 e1 05 42 24     addiu      v0,v0,0x5e1
        00400378 21 20 82 00     addu       _c,_c,v0
        0040037c 00 00 82 90     lbu        v0=>s__(((((_AAAAAA_BBBBBB_004005e0+1,0x0(_c)    = "         (((((               
        00400380 07 00 42 30     andi       v0,v0,0x7
                             LAB_00400384                                    XREF[1]:     00400364(j)  
        00400384 08 00 e0 03     jr         ra
        00400388 00 00 00 00     _nop

This time, Ghidra marked a reference for the v0 register. The address is 0x004005e1, which is the address of the symbol _ctype_ plus one. This means the addend for the reference is not 0x0 but 0x1, so we need to take it into account when recreating the relocations:

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040036c")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400374")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")

By setting the constant for the ADDIU instruction to 0x1, we have encoded the value of the addend. Let’s do the same for the rest of the is* functions:

currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400398")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004003a0")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004003c4")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004003cc")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004003f0")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004003f8")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040041c")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400424")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400448")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400450")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400474")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040047c")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004004a0")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004004a8")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004004cc")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004004d4")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x004004f8")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400500")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x00400524")[0], 0x5, [], [0x00, 0x00, 0x02, 0x3c], "_ctype_")
currentProgram.getRelocationTable().add(currentProgram.parseAddress("0x0040052c")[0], 0x6, [], [0x01, 0x00, 0x42, 0x24], "_ctype_")

By now, we should have all the relocations we need:

Squeezing a working object file out of Ghidra’s database, redux

Now that we have our relocations, we can craft a relocatable object file out of this executable. We will reuse the script snippets form the last part, but this time we’ll skip most of the explanations.

First, the section bytes:

import jarray, struct

# Define address sets
_TEXT_START = currentProgram.parseAddress("0x400130")[0]
_TEXT_END = currentProgram.parseAddress("0x40057f")[0]
_TEXT_SET = currentProgram.getAddressFactory().getAddressSet(_TEXT_START, _TEXT_END)

_RODATA_START = _TEXT_END.next()
_RODATA_END = currentProgram.parseAddress("0x4006ef")[0]
_RODATA_SET = currentProgram.getAddressFactory().getAddressSet(_RODATA_START, _RODATA_END)

_EXTERNAL_START = currentProgram.parseAddress("0x4001d0")[0]
_EXTERNAL_END = currentProgram.parseAddress("0x40025b")[0]
_EXTERNAL_SET = currentProgram.getAddressFactory().getAddressSet(_EXTERNAL_START, _EXTERNAL_END)

# Grab section bytes
_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)

# Patch in the original bytes
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)

# Create symbol table
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

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)

# Create relocation tables
_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))]

# Craft string tables
def craft_string_table(strings):
    data = ""
    offsets = dict()
    for string in strings:
        offsets[string] = len(data)
        data += (string + "\0").encode("ascii")
    return data, offsets

_shstrtab_bytes, shstrtab = craft_string_table(["", ".shstrtab", ".strtab", ".symtab", ".text", ".rodata", ".text.rel", ".rodata.rel", ".reginfo"])
_strtab_bytes, strtab = craft_string_table(map(lambda symbol: symbol.getName() if symbol != None else "", symbols))

# Craft symbol table
ELF32LE_SYM = "<IIIBBH"
ELF32LE_SYM_ENTSIZE = struct.calcsize(ELF32LE_SYM)

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

SECTIONS={_TEXT_SET: 4, _RODATA_SET: 5}
_symtab_bytes, symtab = craft_symbol_table(symbols, FIRST_UNDEFINED_SYMBOL_IDX, strtab, SECTIONS)

# Craft relocation tables
ELF32LE_REL = "<II"
ELF32LE_REL_ENTSIZE = struct.calcsize(ELF32LE_REL)

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

_text_rel_bytes = craft_relocation_table(_text_rel, symtab, _TEXT_SET)
_rodata_rel_bytes = craft_relocation_table(_rodata_rel, symtab, _RODATA_SET)

# Craft .reginfo section
_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")

This time, print_number is smack in the middle of the .text section that we want to export. Rather than adapt the script snippets to deal with fragmented sections, we’ll just export the entire .text section but put the print_number function inside _EXTERNAL_SET. While the bits of the print_number function will still be present in the object file, this will make the print_number symbol undefined, effectively hiding the function from the toolchain.

Then, the ELF file structures:

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)

# 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])

# 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
)

Finally, let’s write the file itself:

with open("ascii-table.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]))

The file ascii-table.delinked.o should appear in the current working directory of Ghidra.

It’s (still) linkable, it’s (still) linkable!

Once more, we can take a peek at this file using the toolchain:

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

ascii-table.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.delinked.o

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

Sections:
Idx Name          Size      VMA       LMA       File off  Algn  Flags
  0 .text         00000450  00000000  00000000  00000424  2**2  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .rodata       00000170  00000000  00000000  00000874  2**2  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  2 .reginfo      00000018  00000000  00000000  00000b44  2**0  CONTENTS, READONLY, LINK_ONCE_SAME_SIZE
$ mips-linux-gnu-nm --format sysv --line-numbers --no-sort ascii-table.delinked.o


Symbols from ascii-table.delinked.o:

Name                  Value   Class        Type         Size     Line  Section

_ftext              |00000000|   T  |            NOTYPE|        |     |.text
main                |00000000|   T  |            NOTYPE|        |     |.text
print_ascii_entry   |0000012c|   T  |            NOTYPE|        |     |.text
isalnum             |00000230|   T  |            NOTYPE|        |     |.text
isalpha             |0000025c|   T  |            NOTYPE|        |     |.text
iscntrl             |00000288|   T  |            NOTYPE|        |     |.text
isdigit             |000002b4|   T  |            NOTYPE|        |     |.text
isgraph             |000002e0|   T  |            NOTYPE|        |     |.text
islower             |0000030c|   T  |            NOTYPE|        |     |.text
isprint             |00000338|   T  |            NOTYPE|        |     |.text
ispunct             |00000364|   T  |            NOTYPE|        |     |.text
isspace             |00000390|   T  |            NOTYPE|        |     |.text
isupper             |000003bc|   T  |            NOTYPE|        |     |.text
isxdigit            |000003e8|   T  |            NOTYPE|        |     |.text
write               |00000420|   T  |            NOTYPE|        |     |.text
exit                |00000430|   T  |            NOTYPE|        |     |.text
__start             |00000438|   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.delinked.o

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

RELOCATION RECORDS FOR [.text]:
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
00000154 R_MIPS_26         print_number
00000168 R_MIPS_26         write
00000170 R_MIPS_26         isgraph
00000190 R_MIPS_26         write
000001ac R_MIPS_26         write
00000210 R_MIPS_26         write
0000023c R_MIPS_HI16       _ctype_
00000244 R_MIPS_LO16       _ctype_
00000268 R_MIPS_HI16       _ctype_
00000270 R_MIPS_LO16       _ctype_
00000294 R_MIPS_HI16       _ctype_
0000029c R_MIPS_LO16       _ctype_
000002c0 R_MIPS_HI16       _ctype_
000002c8 R_MIPS_LO16       _ctype_
000002ec R_MIPS_HI16       _ctype_
000002f4 R_MIPS_LO16       _ctype_
00000318 R_MIPS_HI16       _ctype_
00000320 R_MIPS_LO16       _ctype_
00000344 R_MIPS_HI16       _ctype_
0000034c R_MIPS_LO16       _ctype_
00000370 R_MIPS_HI16       _ctype_
00000378 R_MIPS_LO16       _ctype_
0000039c R_MIPS_HI16       _ctype_
000003a4 R_MIPS_LO16       _ctype_
000003c8 R_MIPS_HI16       _ctype_
000003d0 R_MIPS_LO16       _ctype_
000003f4 R_MIPS_HI16       _ctype_
000003fc R_MIPS_LO16       _ctype_
00000440 R_MIPS_26         main
00000448 R_MIPS_26         exit


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


Let’s link it with print_number_octal.o:

$ mips-linux-gnu-gcc -EL -static -no-pie -nostdlib -o ascii-table.delinked.elf ascii-table.delinked.o print_number_octal.o

We’ll make sure that our dark magic ritual actually worked:

$ qemu-mipsel ascii-table.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       

For the fourth time, we have successfully modified our case study to print the ASCII table in octal. Unlike last time, we’ve delinked an executable produced by the standard linker and not by (ab)using Ghidra as a linker, requiring us to reconstruct the missing data (relocations) to do so.

The files for this case study can be found here: case-study.tar.gz

Conclusion

We have successfully delinked parts of a normal executable back into a relocatable object file with lots of Python script snippets and used it to make a new, modified executable. Next time, we’ll use a Ghidra extension to automate this process.