Reverse-engineering part 9: look Ma, no relocations!
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 to0
; - 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 value0x400000
to the registers2
; - Then, the
ADDIU
instruction adds the constant value0x584
to the registers2
, which yields the value0x400584
; - Finally, the
OR
instruction moves the value inside the registers2
to the registera1
, 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.