Skip to content

Commit 29b7957

Browse files
authored
Merge pull request #3 from postmanlabs/elf
Reimplement ELF support
2 parents d674299 + a9d8f3d commit 29b7957

File tree

5 files changed

+60
-202
lines changed

5 files changed

+60
-202
lines changed

README.markdown

+2-49
Original file line numberDiff line numberDiff line change
@@ -65,53 +65,6 @@ The run-time lookup uses APIs from `<mach-o/getsect.h>`.
6565

6666
### Linux
6767

68-
For ELF executables, the resources are added as sections. Unfortunately
69-
there is no guaranteed metadata about sections in the ELF format, the
70-
section header table (SHT) is optional and can be stripped after linking
71-
(`llvm-strip --strip-sections`). So while the resources are added as
72-
sections, the run-time lookup requires a bespoke implementation, which
73-
makes it the most complex and non-standard out of the platforms. There
74-
are N+1 sections used for the implementation, where the extra section
75-
serves as our own version of the SHT (which can't be stripped). Finding
76-
the SHT section at run-time is done via a static pointer in the code
77-
which is looked up by its symbol name and has its value updated after
78-
the sections are injected.
79-
80-
The build-time equivalent is somewhat involved since our version of
81-
the SHT has to be manually constructed after adding the sections, and
82-
the section updated with the new content. There's also not a standard
83-
tool that can change the value of a static variable after linking, so
84-
instead the SHT is found by a symbol which is added via
85-
`objcopy --binary-architecture`. The following instructions show how
86-
to embed the binary data at build-time for ELF executables, with the
87-
section holding the data name "foobar":
68+
For ELF executables, the resources are added as notes.
8869

89-
```sh
90-
# The binary data should be on disk in a file named foobar - the name
91-
# of the file is important as it is used in the added symbols
92-
$ objcopy --input binary --output elf64-x86-64 \
93-
--binary-architecture i386:x86-64 \
94-
--rename-section .data=foobar,CONTENTS,ALLOC,LOAD,READONLY,DATA \
95-
foobar foobar.o
96-
# Also create an empty section for the SHT
97-
$ objcopy --input binary --output elf64-x86-64 \
98-
--binary-architecture i386:x86-64 \
99-
--rename-section .data=foobar,CONTENTS,ALLOC,LOAD,READONLY,DATA \
100-
postject_sht postject_sht.o
101-
# Link the created .o files at build-time. Also define
102-
# `__POSTJECT_NO_SHT_PTR` so that the run-time code uses the
103-
# symbol added by `--binary-architecture` to find the SHT
104-
$ clang++ -D__POSTJECT_NO_SHT_PTR test.cpp foobar.o postject_sht.o
105-
# Dump the sections with readelf, and grab the virtual address and
106-
# size for the "foobar" section
107-
$ readelf -S a.out
108-
# Manually create the SHT - replace 0xAA with the virtual address
109-
# and 0xBB with the size from the readelf output
110-
$ python3 -c "import struct; print((struct.pack('<I', 1) + bytes('foobar', 'ascii') + bytes([0]) + struct.pack('<QI', 0xAA, 0xBB)).decode('ascii'), end='');" > postject_sht
111-
# Update the SHT section with the correct content
112-
$ objcopy --update-section postject_sht=postject_sht a.out
113-
```
114-
115-
The run-time lookup finds our version of the SHT through the static
116-
pointer, and then parses the contents of our SHT to look for the
117-
section with the requested name.
70+
The build-time equivalent is to use a linker script.

examples/test.c

-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33

44
#include "../postject-api.h"
55

6-
#if defined(__linux__) && !defined(__POSTJECT_NO_SHT_PTR)
7-
volatile void *_binary_postject_sht_start = (void *)POSTJECT_SHT_PTR_SENTINEL;
8-
#endif
9-
106
int main()
117
{
128
size_t size;

examples/test.cpp

-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33

44
#include "../postject-api.h"
55

6-
#if defined(__linux__) && !defined(__POSTJECT_NO_SHT_PTR)
7-
volatile void *_binary_postject_sht_start = (void *)POSTJECT_SHT_PTR_SENTINEL;
8-
#endif
9-
106
int main()
117
{
128
size_t size;

postject-api.h

+49-54
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,14 @@
1111
#include <mach-o/dyld.h>
1212
#include <mach-o/getsect.h>
1313
#elif defined(__linux__)
14+
#include <elf.h>
1415
#include <link.h>
16+
#include <sys/param.h>
17+
#include <sys/auxv.h>
1518
#elif defined(_WIN32)
1619
#include <windows.h>
1720
#endif
1821

19-
#if defined(__linux__)
20-
// NOTE - This needs to be a sentinel value, if it's initialized to
21-
// NULL then it won't have a spot in the executable to change
22-
#define POSTJECT_SHT_PTR_SENTINEL 0000000001
23-
extern volatile void* _binary_postject_sht_start;
24-
25-
struct postject_elf_section {
26-
uint64_t virtual_address; // Don't use a pointer here, standardize size
27-
uint32_t size;
28-
};
29-
#endif
30-
3122
struct postject_options {
3223
const char* elf_section_name;
3324
const char* macho_framework_name;
@@ -101,49 +92,53 @@ static const void* postject_find_resource(const char* name,
10192

10293
return ptr;
10394
#elif defined(__linux__)
104-
void* ptr = NULL;
105-
106-
// This executable might be a Position Independent Executable (PIE), so
107-
// the virtual address values need to be added to the relocation address
108-
uintptr_t relocation_addr = _r_debug.r_map->l_addr;
109-
110-
if (_binary_postject_sht_start != (void*)POSTJECT_SHT_PTR_SENTINEL) {
111-
#if defined(__POSTJECT_NO_SHT_PTR)
112-
void* sht_ptr = (void*)&_binary_postject_sht_start;
113-
#else
114-
void* sht_ptr =
115-
(void*)(relocation_addr + (uintptr_t)_binary_postject_sht_start);
116-
#endif
95+
void *ptr = NULL;
96+
97+
if (options != NULL && options->elf_section_name != NULL) {
98+
name = options->elf_section_name;
99+
}
100+
101+
uintptr_t p = getauxval(AT_PHDR);
102+
size_t n = getauxval(AT_PHNUM);
103+
uintptr_t base_addr = p - sizeof(ElfW(Ehdr));
104+
105+
// iterate program header
106+
for (; n > 0; n--, p += sizeof(ElfW(Phdr))) {
107+
ElfW(Phdr) *phdr = (ElfW(Phdr) *)p;
108+
109+
// skip everything but notes
110+
if (phdr->p_type != PT_NOTE) {
111+
continue;
112+
}
113+
114+
// note segment starts at base address + segment virtual address
115+
uintptr_t pos = (base_addr + phdr->p_vaddr);
116+
uintptr_t end = (pos + phdr->p_memsz);
117+
118+
// iterate through segment until we reach the end
119+
while (pos < end) {
120+
if (pos + sizeof(ElfW(Nhdr)) > end) {
121+
break; // invalid
122+
}
123+
124+
ElfW(Nhdr) *note = (ElfW(Nhdr) *)(uintptr_t)pos;
125+
if (note->n_namesz != 0 && note->n_descsz != 0 &&
126+
strncmp((char *)(pos + sizeof(ElfW(Nhdr))),
127+
(char *)name, sizeof(name)) == 0) {
128+
*size = note->n_descsz;
129+
// advance past note header and aligned name
130+
// to get to description data
131+
return (void *)((uintptr_t)note +
132+
sizeof(ElfW(Nhdr)) +
133+
roundup(note->n_namesz, 4));
134+
}
135+
136+
pos += (sizeof(ElfW(Nhdr)) + roundup(note->n_namesz, 4) +
137+
roundup(note->n_descsz, 4));
138+
}
139+
}
140+
return NULL;
117141

118-
// First read the section count
119-
uint32_t section_count = *((uint32_t*)sht_ptr);
120-
sht_ptr = (uint32_t*)sht_ptr + 1;
121-
122-
uint32_t i;
123-
for (i = 0; i < section_count; i++) {
124-
// Read the section name as a null-terminated string
125-
const char* section_name = (const char*)sht_ptr;
126-
sht_ptr = (char*)sht_ptr + strlen(section_name) + 1;
127-
128-
// Then read the virtual_address (8 bytes)
129-
uint64_t virtual_address = *((uint64_t*)sht_ptr);
130-
sht_ptr = (uint64_t*)sht_ptr + 1;
131-
132-
// Finally read the section size (4 bytes)
133-
uint32_t section_size = *((uint32_t*)sht_ptr);
134-
sht_ptr = (uint32_t*)sht_ptr + 1;
135-
136-
if (strcmp(section_name, name) == 0) {
137-
if (size != NULL) {
138-
*size = (size_t)section_size;
139-
}
140-
ptr = (void*)(relocation_addr + (uintptr_t)virtual_address);
141-
break;
142-
}
143-
}
144-
}
145-
146-
return ptr;
147142
#elif defined(_WIN32)
148143
void* ptr = NULL;
149144
char* resource_name = NULL;

postject.py

+9-91
Original file line numberDiff line numberDiff line change
@@ -59,103 +59,21 @@ def get_executable_format(filename):
5959
def inject_into_elf(filename, section_name, data, overwrite=False):
6060
app = lief.ELF.parse(filename)
6161

62-
struct_endian_char = ">"
63-
64-
if app.header.identity_data == lief.ELF.ELF_DATA.LSB:
65-
struct_endian_char = "<"
66-
67-
existing_section = app.get_section(section_name)
62+
existing_section = None
63+
for note in app.notes:
64+
if note.name == section_name:
65+
existing_section = note
6866

6967
if existing_section:
7068
if not overwrite:
7169
return False
7270

73-
app.remove_section(section_name, clear=True)
74-
75-
# Create the new section we're injecting
76-
section = lief.ELF.Section()
77-
section.name = section_name
78-
section.content = data
79-
section.add(lief.ELF.SECTION_FLAGS.ALLOC) # Ensure it's loaded into memory
80-
81-
# Important to use the return value to get the updated
82-
# information like the virtual address, LIEF is returning
83-
# a separate object instead of updating the existing one
84-
section = app.add(section)
85-
86-
sections = [section]
87-
88-
# The contents of our SHT section are laid out as:
89-
# * section count (uint32)
90-
# * N sections:
91-
# * name as a null-terminated string
92-
# * virtual address (uint64)
93-
# * section size (uint32)
94-
postject_sht = app.get_section("postject_sht")
95-
96-
if postject_sht:
97-
contents = postject_sht.content
98-
99-
section_count = struct.unpack(f"{struct_endian_char}I", contents[:4])[0]
100-
idx = 4
101-
102-
for _ in range(section_count):
103-
name = ""
104-
105-
while True:
106-
ch = contents[idx]
107-
idx += 1
108-
109-
if ch != 0:
110-
name += chr(ch)
111-
else:
112-
break
113-
114-
# We're already overwriting this section
115-
if name == section_name:
116-
continue
71+
app.remove(note)
11772

118-
section = app.get_section(name)
119-
120-
if not section:
121-
raise RuntimeError("Couldn't find section listed in our SHT")
122-
else:
123-
sections.append(section)
124-
125-
idx += 12 # Skip over the other info
126-
127-
app.remove_section("postject_sht", clear=True)
128-
129-
section_count = struct.pack(f"{struct_endian_char}I", len(sections))
130-
content_bytes = section_count
131-
132-
for section in sections:
133-
content_bytes += bytes(section.name, "ascii") + bytes([0])
134-
content_bytes += struct.pack(f"{struct_endian_char}QI", section.virtual_address, section.size)
135-
136-
postject_sht = lief.ELF.Section()
137-
postject_sht.name = "postject_sht"
138-
postject_sht.add(lief.ELF.SECTION_FLAGS.ALLOC) # Ensure it's loaded into memory
139-
postject_sht.content = list(content_bytes)
140-
141-
postject_sht = app.add(postject_sht)
142-
143-
# TODO - How do we determine the size of void* from the ELF?
144-
# TODO - Why does it appear to only be 4 bytes when sizeof(void*) is showing 8?
145-
# TODO - Do we need to care or just let LIEF patch the address and assume it's fine?
146-
147-
symbol_found = False
148-
149-
# Find the symbol for our SHT pointer and update the value
150-
for symbol in app.symbols:
151-
if symbol.demangled_name == "_binary_postject_sht_start":
152-
symbol_found = True
153-
app.patch_address(symbol.value, postject_sht.virtual_address)
154-
break
155-
156-
if not symbol_found:
157-
print("ERROR: Couldn't find symbol")
158-
sys.exit(1)
73+
note = lief.ELF.Note()
74+
note.name = section_name
75+
note.description = data
76+
note = app.add(note)
15977

16078
app.write(filename)
16179

0 commit comments

Comments
 (0)