staticer 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- staticer/__init__.py +1 -0
- staticer/common/__init__.py +0 -0
- staticer/common/depgraph.py +116 -0
- staticer/elf_embedder.py +2142 -0
- staticer/pe_embedder.py +1097 -0
- staticer/staticer.py +72 -0
- staticer/stubs/__init__.py +0 -0
- staticer/stubs/linux/__init__.py +0 -0
- staticer/stubs/linux/elf_stub.py +374 -0
- staticer/stubs/windows/__init__.py +0 -0
- staticer/stubs/windows/pe_stub.py +228 -0
- staticer-0.0.1.dist-info/METADATA +170 -0
- staticer-0.0.1.dist-info/RECORD +16 -0
- staticer-0.0.1.dist-info/WHEEL +5 -0
- staticer-0.0.1.dist-info/entry_points.txt +2 -0
- staticer-0.0.1.dist-info/top_level.txt +1 -0
staticer/elf_embedder.py
ADDED
|
@@ -0,0 +1,2142 @@
|
|
|
1
|
+
"""ELF (Linux) dependency embedding.
|
|
2
|
+
|
|
3
|
+
Implements the reloc-bearing ("PIC path") case -- a dependency .so is
|
|
4
|
+
appended as an in-place extension of the host ELF executable's own
|
|
5
|
+
highest-vaddr PT_LOAD segment (deliberately *not* a new PT_LOAD segment of
|
|
6
|
+
its own -- see ``ElfEmbedder.embed()``'s docstring for why that breaks
|
|
7
|
+
AT_PHDR), its own relocations are applied against the new addresses, the
|
|
8
|
+
host's references to it (GOT/PLT slots) are patched to the embedded
|
|
9
|
+
addresses, and its DT_NEEDED entry is neutralized -- and the fixed-base
|
|
10
|
+
("non-PIC path") case, where a dependency's own ELF header says it can
|
|
11
|
+
only ever be loaded at one specific address and gets its own, separate
|
|
12
|
+
PT_LOAD segment reserved at exactly that address instead (see the base-
|
|
13
|
+
address-reservation comment inside ``embed()``, by ``fixed_base_segments``,
|
|
14
|
+
for the full mechanism and its one real limitation). Unwind-metadata
|
|
15
|
+
preservation and the real generated entrypoint stub are later tasks; see
|
|
16
|
+
``ElfEmbedder.embed()``'s docstring for exactly what's simplified here.
|
|
17
|
+
|
|
18
|
+
``pyelftools`` is read-only, so every byte this module *writes* is built
|
|
19
|
+
by hand from the fixed, standardized ELF32/ELF64 struct layouts (ELF
|
|
20
|
+
headers, program headers, dynamic-tag entries, and REL/RELA relocation
|
|
21
|
+
entries are all fixed-size, unlike PE's variable-length optional header).
|
|
22
|
+
pyelftools is used only for the read side: walking the PT_DYNAMIC segment
|
|
23
|
+
to get DT_NEEDED/relocation tables/dynamic symbols without needing (and
|
|
24
|
+
without disturbing) section headers at all.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import io
|
|
28
|
+
import shutil
|
|
29
|
+
import struct
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
32
|
+
|
|
33
|
+
from elftools.elf.elffile import ELFFile
|
|
34
|
+
|
|
35
|
+
from .common.depgraph import build_dependency_graph, embed_order
|
|
36
|
+
from .stubs.linux.elf_stub import build_linux_init_array_stub, build_linux_stub
|
|
37
|
+
|
|
38
|
+
# -- ELF constants pyelftools doesn't expose as plain ints for our needs --
|
|
39
|
+
# (it hands back Enum-mapped strings for e.g. p_type/d_tag, which is fine
|
|
40
|
+
# for reading but useless for re-packing bytes we write ourselves).
|
|
41
|
+
|
|
42
|
+
PT_LOAD = 1
|
|
43
|
+
PT_DYNAMIC = 2
|
|
44
|
+
PT_PHDR = 6
|
|
45
|
+
PT_GNU_EH_FRAME = 0x6474E550
|
|
46
|
+
PF_X, PF_W, PF_R = 1, 2, 4
|
|
47
|
+
|
|
48
|
+
DT_NULL = 0
|
|
49
|
+
DT_NEEDED = 1
|
|
50
|
+
|
|
51
|
+
DT_DEBUG = 21 # harmless/ignored by every runtime linker; see _remove_needed
|
|
52
|
+
|
|
53
|
+
# Relocation type numbers below happen to share the same values on
|
|
54
|
+
# EM_386 and EM_X86_64 for every type this module handles.
|
|
55
|
+
R_NONE = 0
|
|
56
|
+
R_ABS_PLUS_ADDEND = 1 # R_X86_64_64 / R_386_32: value = S (+A for RELA)
|
|
57
|
+
R_COPY = 5 # R_X86_64_COPY / R_386_COPY: copy `st_size` bytes from the definition
|
|
58
|
+
R_GLOB_DAT = 6
|
|
59
|
+
R_JUMP_SLOT = 7
|
|
60
|
+
R_RELATIVE = 8
|
|
61
|
+
|
|
62
|
+
_SUPPORTED_MACHINES = ("EM_X86_64", "EM_386")
|
|
63
|
+
|
|
64
|
+
_PAGE = 0x1000
|
|
65
|
+
|
|
66
|
+
# -- DWARF exception-handling pointer-encoding bits (DW_EH_PE_*), just the
|
|
67
|
+
# ones GCC's own default .eh_frame_hdr layout actually uses -- see
|
|
68
|
+
# _parse_eh_frame_hdr/_build_eh_frame_hdr.
|
|
69
|
+
_DW_EH_PE_UDATA4 = 0x03
|
|
70
|
+
_DW_EH_PE_SDATA4 = 0x0B
|
|
71
|
+
_DW_EH_PE_PCREL = 0x10
|
|
72
|
+
_DW_EH_PE_DATAREL = 0x30
|
|
73
|
+
_DW_EH_PE_OMIT = 0xFF
|
|
74
|
+
|
|
75
|
+
STB_GLOBAL = 1
|
|
76
|
+
STB_WEAK = 2
|
|
77
|
+
STT_FUNC = 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def list_elf_imports(path: Path) -> List[str]:
|
|
81
|
+
"""Names from the DT_NEEDED entries of an ELF file's dynamic section,
|
|
82
|
+
read via its PT_DYNAMIC segment (so this works even without section
|
|
83
|
+
headers); empty list if it has none."""
|
|
84
|
+
with open(path, "rb") as f:
|
|
85
|
+
elf = ELFFile(f)
|
|
86
|
+
dyn = _dynamic_segment(elf)
|
|
87
|
+
if dyn is None:
|
|
88
|
+
return []
|
|
89
|
+
return [tag.needed for tag in dyn.iter_tags("DT_NEEDED")]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_elf(name: str, search_dirs: List[Path]) -> Optional[Path]:
|
|
93
|
+
"""Scan search_dirs in order for a file that `name` denotes.
|
|
94
|
+
|
|
95
|
+
`name` comes straight from the ELF file being processed's own
|
|
96
|
+
DT_NEEDED entries (an attacker-controlled dependency name, if the
|
|
97
|
+
input binary is untrusted) -- it must never be allowed to escape the
|
|
98
|
+
`search_dirs` entry it's being matched against via path-traversal
|
|
99
|
+
components (``..``) or an absolute path (CWE-22), or `--lib-dir`'s
|
|
100
|
+
whole purpose (scoping embedding to one chosen folder) is defeated.
|
|
101
|
+
|
|
102
|
+
A DT_NEEDED entry is *usually* a bare filename (e.g. "libc.so.6"),
|
|
103
|
+
but it legitimately isn't always one: a .so with no DT_SONAME of its
|
|
104
|
+
own that was linked by passing its full path on the linker command
|
|
105
|
+
line gets that literal path recorded as the NEEDED string by ld --
|
|
106
|
+
this project's own test fixtures do exactly that (`build_exe` passes
|
|
107
|
+
`str(lib)`, an absolute `tmp_path`-rooted path, with no
|
|
108
|
+
`-Wl,-soname=...` ever set). An earlier version of this check
|
|
109
|
+
rejected any such non-bare-filename name outright, which (correctly)
|
|
110
|
+
blocked a genuine traversal/absolute escape attempt but also
|
|
111
|
+
(incorrectly) blocked this entirely legitimate same-directory case,
|
|
112
|
+
since ``directory / name`` only ever *joins* when `name` is relative
|
|
113
|
+
-- for an absolute `name` it discards `directory` and evaluates to
|
|
114
|
+
`name` itself, so the real containment check below is what has to
|
|
115
|
+
carry the whole security property, not a cheap name-shape guard in
|
|
116
|
+
front of it.
|
|
117
|
+
|
|
118
|
+
So: no shape-based rejection up front. Every `name` -- bare,
|
|
119
|
+
relative-with-components, or absolute -- is joined against each
|
|
120
|
+
`directory` in turn (a no-op join for an absolute `name`, same as
|
|
121
|
+
Path's own semantics) and the resulting candidate is accepted only
|
|
122
|
+
if it both exists as a file and *resolves* (symlinks and all) to
|
|
123
|
+
somewhere inside that same `directory`'s own resolved form. A
|
|
124
|
+
genuine traversal (``../../etc/passwd``) or an absolute escape
|
|
125
|
+
(``/etc/passwd``) still fails that resolved-containment check against
|
|
126
|
+
every `search_dirs` entry, exactly as before -- only a `name` that
|
|
127
|
+
truly denotes a file inside one of them, however it's spelled, is
|
|
128
|
+
ever accepted. Either way a rejection behaves exactly like "not found
|
|
129
|
+
in this directory" -- falling through to the next search_dirs entry,
|
|
130
|
+
or returning None if none match -- never a hard error."""
|
|
131
|
+
if not name:
|
|
132
|
+
return None
|
|
133
|
+
for directory in search_dirs:
|
|
134
|
+
candidate = directory / name
|
|
135
|
+
try:
|
|
136
|
+
resolved = candidate.resolve()
|
|
137
|
+
if not resolved.is_relative_to(directory.resolve()):
|
|
138
|
+
continue
|
|
139
|
+
except (OSError, ValueError):
|
|
140
|
+
continue
|
|
141
|
+
if candidate.is_file():
|
|
142
|
+
return candidate
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _dynamic_segment(elf: ELFFile):
|
|
147
|
+
return next(elf.iter_segments(type="PT_DYNAMIC"), None)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _real_symtab_count(dyn, reloc_tables: dict) -> int:
|
|
151
|
+
"""The true number of entries in `dyn`'s own DT_SYMTAB, more reliable
|
|
152
|
+
than pyelftools' own ``DynamicSegment.num_symbols()`` for a host that
|
|
153
|
+
exports no symbols at all (the common case for an ordinary
|
|
154
|
+
executable, which usually only *imports*).
|
|
155
|
+
|
|
156
|
+
``num_symbols()`` derives its count from the DT_GNU_HASH table (or
|
|
157
|
+
DT_HASH), which only indexes *exported/defined* symbols -- GNU_HASH's
|
|
158
|
+
own lookup degrades, for a host with nothing hashed at all, to
|
|
159
|
+
returning its own `symoffset` bias value as if that were the total
|
|
160
|
+
symbol count (see pyelftools' ``GNUHashTable.get_number_of_symbols``:
|
|
161
|
+
``if max_idx < self.params['symoffset']: return self.params['symoffset']``),
|
|
162
|
+
silently undercounting every purely-undefined symbol past that bias.
|
|
163
|
+
Confirmed empirically against a real `-fno-plt`/`-no-pie` host on
|
|
164
|
+
real WSL Linux: with `__cxa_finalize`/`_ITM_registerTMCloneTable`/
|
|
165
|
+
`_ITM_deregisterTMCloneTable` at dynsym indices 1-3 (all undefined,
|
|
166
|
+
all referenced by real relocations), `num_symbols()` reported 1 --
|
|
167
|
+
appending new symbol-table growth past that wrong, too-small index
|
|
168
|
+
silently overwrote/discarded those three real, already-occupied
|
|
169
|
+
entries, corrupting what their own pre-existing relocations' symbol
|
|
170
|
+
indices resolved to and segfaulting at process startup.
|
|
171
|
+
|
|
172
|
+
Falls back to one past the highest symbol index any of the host's
|
|
173
|
+
OWN existing relocations (main table + JMPREL, if present) already
|
|
174
|
+
reference, whenever that's larger than what `num_symbols()` reports
|
|
175
|
+
-- every such index is, by construction, a real, already-occupied
|
|
176
|
+
dynsym slot that must not be overwritten."""
|
|
177
|
+
count = dyn.num_symbols()
|
|
178
|
+
for table in reloc_tables.values():
|
|
179
|
+
for reloc in table.iter_relocations():
|
|
180
|
+
count = max(count, reloc["r_info_sym"] + 1)
|
|
181
|
+
return count
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
_EI_CLASS_OFFSET = 4 # fixed byte offset in e_ident, identical for ELF32/ELF64
|
|
185
|
+
_ELFCLASS64 = 2
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _peek_elfclass64(path: Path) -> bool:
|
|
189
|
+
"""Whether `path` is a 64-bit ELF file (ELFCLASS64), read directly
|
|
190
|
+
from the single e_ident[EI_CLASS] byte rather than a full ELF parse
|
|
191
|
+
-- used only to pick a same-bitness-first multiarch search-dir order
|
|
192
|
+
(see `ElfEmbedder._search_dirs`); any malformed/unreadable input
|
|
193
|
+
here is caught properly, with a real diagnostic, by the full parse
|
|
194
|
+
`embed()` already does right after, so this deliberately doesn't
|
|
195
|
+
duplicate that error handling -- a bool default of False (32-bit)
|
|
196
|
+
on a read failure just means the search order picked here doesn't
|
|
197
|
+
matter, since `embed()` is about to fail loudly on the same file
|
|
198
|
+
for an unrelated, more informative reason moments later."""
|
|
199
|
+
try:
|
|
200
|
+
with open(path, "rb") as f:
|
|
201
|
+
f.seek(_EI_CLASS_OFFSET)
|
|
202
|
+
return f.read(1) == bytes([_ELFCLASS64])
|
|
203
|
+
except OSError:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _align_up(value: int, alignment: int) -> int:
|
|
208
|
+
if alignment <= 1:
|
|
209
|
+
return value
|
|
210
|
+
return (value + alignment - 1) // alignment * alignment
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# -- raw Phdr packing (pyelftools doesn't write; layouts are fixed by ABI) --
|
|
214
|
+
|
|
215
|
+
def _pack_phdr(is64: bool, p_type: int, p_flags: int, p_offset: int, p_vaddr: int,
|
|
216
|
+
p_paddr: int, p_filesz: int, p_memsz: int, p_align: int) -> bytes:
|
|
217
|
+
if is64:
|
|
218
|
+
return struct.pack("<IIQQQQQQ", p_type, p_flags, p_offset, p_vaddr,
|
|
219
|
+
p_paddr, p_filesz, p_memsz, p_align)
|
|
220
|
+
# Elf32_Phdr has a different field order: p_flags comes *after*
|
|
221
|
+
# p_filesz/p_memsz, unlike Elf64_Phdr where it's right after p_type.
|
|
222
|
+
return struct.pack("<IIIIIIII", p_type, p_offset, p_vaddr, p_paddr,
|
|
223
|
+
p_filesz, p_memsz, p_flags, p_align)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _remove_needed(dyn, raw: bytearray, ptr_size: int, needed_name: str) -> bool:
|
|
227
|
+
"""Convert the DT_NEEDED entry for `needed_name` into a DT_DEBUG entry,
|
|
228
|
+
in place. DT_DEBUG is ignored by every real-world runtime linker, and
|
|
229
|
+
(crucially) this doesn't shrink/grow the dynamic array or disturb its
|
|
230
|
+
DT_NULL terminator's position -- unlike turning it into DT_NULL
|
|
231
|
+
directly, which would truncate the whole table at that point for any
|
|
232
|
+
tag scanner that stops at the first DT_NULL (every real one does)."""
|
|
233
|
+
dyn_entry_size = 2 * ptr_size
|
|
234
|
+
for n in range(dyn.num_tags()):
|
|
235
|
+
tag = dyn.get_tag(n)
|
|
236
|
+
if tag.entry.d_tag == "DT_NEEDED" and tag.needed == needed_name:
|
|
237
|
+
entry_off = dyn["p_offset"] + n * dyn_entry_size
|
|
238
|
+
fmt = "<Q" if ptr_size == 8 else "<I"
|
|
239
|
+
struct.pack_into(fmt, raw, entry_off, DT_DEBUG)
|
|
240
|
+
return True
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
_VERNEED_ENTRY_SIZE = 16 # Elf32_Verneed/Elf64_Verneed share one layout (see _VERNEED_FMT)
|
|
245
|
+
_VERNEED_FMT = "<HHIII" # vn_version, vn_cnt, vn_file, vn_aux, vn_next (all fixed-width, class-independent)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _remove_verneed_entry(dyn, raw: bytearray, ptr_size: int, vaddr_to_offset, needed_name: str) -> None:
|
|
249
|
+
"""Remove `needed_name`'s own top-level DT_VERNEED (symbol-versioning
|
|
250
|
+
"needed version" list) entry, if the host has one, splicing the
|
|
251
|
+
linked list around it in place -- the version-requirement analogue of
|
|
252
|
+
_remove_needed, needed for the same reason but discovered only
|
|
253
|
+
against real system binaries (none of this project's own small
|
|
254
|
+
fixtures use GNU symbol versioning, so nothing exercised this path
|
|
255
|
+
before Task 10b): ld.so's _dl_check_map_versions walks the WHOLE
|
|
256
|
+
top-level Verneed list unconditionally at load time and asserts a
|
|
257
|
+
loaded module exists for *every* entry's own vn_file SONAME --
|
|
258
|
+
unconditionally, regardless of whether any relocation still actually
|
|
259
|
+
references a version from it. Leaving a stale entry for an embedded
|
|
260
|
+
(DT_NEEDED-neutralized) dependency in place makes that assertion fail
|
|
261
|
+
at process startup ("Inconsistency detected by ld.so: dl-version.c:
|
|
262
|
+
...: _dl_check_map_versions: Assertion `needed != NULL' failed"),
|
|
263
|
+
confirmed against a real `bash`/`libtinfo.so.6` embed on real Linux
|
|
264
|
+
(WSL) before this fix -- it ran `embed()` successfully and loaded via
|
|
265
|
+
a plain run but crashed immediately in the dynamic linker itself,
|
|
266
|
+
before any user code ran.
|
|
267
|
+
|
|
268
|
+
The corresponding Vernaux (per-version) entries hanging off the
|
|
269
|
+
removed node are simply left as dead bytes in place, unchanged -- the
|
|
270
|
+
same leave-it-dead-in-place pattern _remove_needed already uses for a
|
|
271
|
+
neutralized DT_NEEDED entry -- since nothing reaches them anymore
|
|
272
|
+
once their own Verneed node is spliced out of the list; a Verneed
|
|
273
|
+
entry's own vn_file (the only thing this function reads from each
|
|
274
|
+
entry) must always name one of the host's own *direct* DT_NEEDED
|
|
275
|
+
entries per the ELF ABI (a version can only ever be required from a
|
|
276
|
+
library the host itself directly depends on), so this is only ever
|
|
277
|
+
called for a direct import -- see this method's one call site."""
|
|
278
|
+
verneed_tag = next(dyn.iter_tags("DT_VERNEED"), None)
|
|
279
|
+
verneednum_tag = next(dyn.iter_tags("DT_VERNEEDNUM"), None)
|
|
280
|
+
strtab_tag = next(dyn.iter_tags("DT_STRTAB"), None)
|
|
281
|
+
if verneed_tag is None or verneednum_tag is None or strtab_tag is None:
|
|
282
|
+
return
|
|
283
|
+
count = verneednum_tag.entry.d_val
|
|
284
|
+
if count == 0:
|
|
285
|
+
return
|
|
286
|
+
strtab_vaddr = strtab_tag.entry.d_val
|
|
287
|
+
|
|
288
|
+
def _cstr_at(vaddr: int) -> str:
|
|
289
|
+
off = vaddr_to_offset(vaddr, 1)
|
|
290
|
+
end = raw.index(b"\x00", off)
|
|
291
|
+
return raw[off:end].decode("ascii")
|
|
292
|
+
|
|
293
|
+
# Walk the chain, recording each entry's own (vaddr, file offset,
|
|
294
|
+
# resolved vn_file name) -- vn_next entries are threaded by byte
|
|
295
|
+
# offset *from each entry's own vaddr*, not a flat array, so this
|
|
296
|
+
# can't just iterate `count` fixed-size slots directly.
|
|
297
|
+
entries = []
|
|
298
|
+
cur_vaddr = verneed_tag.entry.d_val
|
|
299
|
+
for _ in range(count):
|
|
300
|
+
off = vaddr_to_offset(cur_vaddr, _VERNEED_ENTRY_SIZE)
|
|
301
|
+
_, _, vn_file, _, vn_next = struct.unpack_from(_VERNEED_FMT, raw, off)
|
|
302
|
+
name = _cstr_at(strtab_vaddr + vn_file)
|
|
303
|
+
entries.append({"vaddr": cur_vaddr, "offset": off, "name": name})
|
|
304
|
+
if vn_next == 0:
|
|
305
|
+
break
|
|
306
|
+
cur_vaddr += vn_next
|
|
307
|
+
|
|
308
|
+
kept = [e for e in entries if e["name"] != needed_name]
|
|
309
|
+
if len(kept) == len(entries):
|
|
310
|
+
return # this host has no Verneed entry for `needed_name` at all
|
|
311
|
+
|
|
312
|
+
# Re-thread vn_next across only the kept entries (last one's becomes
|
|
313
|
+
# 0, the "no more entries" terminator) and update DT_VERNEEDNUM/the
|
|
314
|
+
# head DT_VERNEED tag's own d_val in place -- same append-nothing,
|
|
315
|
+
# edit-existing-fields-in-place pattern as _remove_needed/
|
|
316
|
+
# _set_dyn_tag_value, since nothing here needs new file content, just
|
|
317
|
+
# different field values at already-existing offsets.
|
|
318
|
+
for i, e in enumerate(kept):
|
|
319
|
+
new_next = 0 if i == len(kept) - 1 else kept[i + 1]["vaddr"] - e["vaddr"]
|
|
320
|
+
struct.pack_into("<I", raw, e["offset"] + 12, new_next & 0xFFFFFFFF)
|
|
321
|
+
if kept:
|
|
322
|
+
_set_dyn_tag_value(dyn, raw, ptr_size, "DT_VERNEED", kept[0]["vaddr"])
|
|
323
|
+
_set_dyn_tag_value(dyn, raw, ptr_size, "DT_VERNEEDNUM", len(kept))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _find_dyn_tag_file_offset(dyn, ptr_size: int, tag_name: str) -> Optional[int]:
|
|
327
|
+
dyn_entry_size = 2 * ptr_size
|
|
328
|
+
for n in range(dyn.num_tags()):
|
|
329
|
+
if dyn.get_tag(n).entry.d_tag == tag_name:
|
|
330
|
+
return dyn["p_offset"] + n * dyn_entry_size
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _set_dyn_tag_value(dyn, raw: bytearray, ptr_size: int, tag_name: str, value: int) -> None:
|
|
335
|
+
entry_off = _find_dyn_tag_file_offset(dyn, ptr_size, tag_name)
|
|
336
|
+
if entry_off is None:
|
|
337
|
+
raise ValueError(f"expected dynamic tag {tag_name!r} not found")
|
|
338
|
+
fmt = "<Q" if ptr_size == 8 else "<I"
|
|
339
|
+
struct.pack_into(fmt, raw, entry_off + ptr_size, value)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _read_int(buf, offset: int, size: int) -> int:
|
|
343
|
+
return int.from_bytes(buf[offset:offset + size], "little")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _write_int(buf: bytearray, offset: int, size: int, value: int) -> None:
|
|
347
|
+
buf[offset:offset + size] = (value & ((1 << (size * 8)) - 1)).to_bytes(size, "little")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _pack_relative_entries(entries: List[Tuple[int, int]], is64: bool, is_rela: bool) -> bytes:
|
|
351
|
+
"""Pack (vaddr, value) pairs as new R_*_RELATIVE REL/RELA entries
|
|
352
|
+
(symbol index 0, so r_info reduces to just the type).
|
|
353
|
+
|
|
354
|
+
`value` is packed as an *unsigned* bit pattern (struct format 'Q'/'I',
|
|
355
|
+
not 'q'/'i'): the ELF ABI only cares about the addend's raw bits, and
|
|
356
|
+
`value` is already masked into unsigned range by every caller (see
|
|
357
|
+
`new_reloc_entries.append(...)` call sites) -- a signed format would
|
|
358
|
+
raise OverflowError for any legitimate 32-bit address >= 2**31 (common
|
|
359
|
+
for e.g. mmap-assigned bases), even though the bit pattern itself is
|
|
360
|
+
perfectly valid."""
|
|
361
|
+
out = bytearray()
|
|
362
|
+
for vaddr, value in entries:
|
|
363
|
+
if is64:
|
|
364
|
+
if is_rela:
|
|
365
|
+
out += struct.pack("<QQQ", vaddr, R_RELATIVE, value & 0xFFFFFFFFFFFFFFFF)
|
|
366
|
+
else:
|
|
367
|
+
out += struct.pack("<QQ", vaddr, R_RELATIVE)
|
|
368
|
+
else:
|
|
369
|
+
if is_rela:
|
|
370
|
+
out += struct.pack("<III", vaddr, R_RELATIVE, value & 0xFFFFFFFF)
|
|
371
|
+
else:
|
|
372
|
+
out += struct.pack("<II", vaddr, R_RELATIVE)
|
|
373
|
+
return bytes(out)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _pack_glob_dat_entries(entries: List[Tuple[int, int]], is64: bool, is_rela: bool) -> bytes:
|
|
377
|
+
"""Pack (slot_vaddr, sym_index) pairs as new R_*_GLOB_DAT REL/RELA
|
|
378
|
+
entries -- used to forward an embedded dependency's remaining
|
|
379
|
+
external symbol references (functions/data it calls via its own,
|
|
380
|
+
now-dead PT_DYNAMIC that no embedded dependency provides -- e.g.
|
|
381
|
+
`malloc`, `__cxa_throw`) into the host's own dynamic section, so
|
|
382
|
+
ld.so resolves them normally, by name, at load time -- exactly as it
|
|
383
|
+
would if the host had referenced them directly -- rather than the
|
|
384
|
+
value being something *this module* computes (unlike
|
|
385
|
+
_pack_relative_entries' RELATIVE entries, which always encode an
|
|
386
|
+
address this module already resolved itself; a GLOB_DAT entry here
|
|
387
|
+
instead carries a *symbol index* for ld.so's own lookup to resolve,
|
|
388
|
+
since the real address -- e.g. malloc's -- isn't known until load
|
|
389
|
+
time and isn't ours to compute). Addend is always 0: the whole
|
|
390
|
+
resolved value replaces the slot's content outright, there's nothing
|
|
391
|
+
to add it to."""
|
|
392
|
+
out = bytearray()
|
|
393
|
+
for vaddr, sym_idx in entries:
|
|
394
|
+
if is64:
|
|
395
|
+
r_info = (sym_idx << 32) | R_GLOB_DAT
|
|
396
|
+
if is_rela:
|
|
397
|
+
out += struct.pack("<QQq", vaddr, r_info, 0)
|
|
398
|
+
else:
|
|
399
|
+
out += struct.pack("<QQ", vaddr, r_info)
|
|
400
|
+
else:
|
|
401
|
+
r_info = (sym_idx << 8) | R_GLOB_DAT
|
|
402
|
+
if is_rela:
|
|
403
|
+
out += struct.pack("<IIi", vaddr, r_info, 0)
|
|
404
|
+
else:
|
|
405
|
+
out += struct.pack("<II", vaddr, r_info)
|
|
406
|
+
return bytes(out)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _pack_undef_sym(is64: bool, name_off: int, weak: bool = False) -> bytes:
|
|
410
|
+
"""A new Elf32_Sym/Elf64_Sym entry for a symbol this module itself
|
|
411
|
+
never defines (SHN_UNDEF, st_value/st_size 0) -- used only so ld.so's
|
|
412
|
+
relocation processing has a name to look up for a forwarded external
|
|
413
|
+
reference (see _pack_glob_dat_entries); STT_FUNC matches what a real
|
|
414
|
+
external function import's own symbol table entry would normally
|
|
415
|
+
carry (ld.so's lookup doesn't actually care about the type for OUR
|
|
416
|
+
copy of it -- only the *name* and the fact that it's undefined here
|
|
417
|
+
-- but matching convention costs nothing).
|
|
418
|
+
|
|
419
|
+
`weak` preserves the ORIGINAL reference's own binding (STB_WEAK vs
|
|
420
|
+
STB_GLOBAL) rather than always hardcoding STB_GLOBAL -- this is what
|
|
421
|
+
makes a forwarded weak-undefined reference behave like real ld.so
|
|
422
|
+
semantics instead of this module's own: ld.so's symbol lookup
|
|
423
|
+
tolerates an unresolved STB_WEAK reference (silently leaves it at 0)
|
|
424
|
+
but raises a fatal "undefined symbol" error for an unresolved
|
|
425
|
+
STB_GLOBAL one, exactly matching what a dependency's own weak
|
|
426
|
+
reference (e.g. crtstuff's __cxa_finalize/__gmon_start__-style
|
|
427
|
+
optional hooks) would have done had it never been embedded at all."""
|
|
428
|
+
st_info = ((STB_WEAK if weak else STB_GLOBAL) << 4) | STT_FUNC
|
|
429
|
+
if is64:
|
|
430
|
+
# st_name, st_info, st_other, st_shndx, st_value, st_size
|
|
431
|
+
return struct.pack("<IBBHQQ", name_off, st_info, 0, 0, 0, 0)
|
|
432
|
+
# Elf32_Sym has a different field order: st_value/st_size come right
|
|
433
|
+
# after st_name, with st_info/st_other/st_shndx packed at the end.
|
|
434
|
+
return struct.pack("<IIIBBH", name_off, 0, 0, st_info, 0, 0)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _decode_eh_pe_sdata4(encoding: int, raw: int, field_vaddr: int, hdr_vaddr: int) -> int:
|
|
438
|
+
"""Decode one DW_EH_PE-encoded 4-byte field from .eh_frame_hdr, as
|
|
439
|
+
used by every encoding GCC's own default layout actually emits for
|
|
440
|
+
this structure's fields: `sdata4`/`udata4` format (this module
|
|
441
|
+
doesn't need to tell those apart -- both are 4 bytes, and the
|
|
442
|
+
unsigned-vs-signed distinction only matters for a value that could
|
|
443
|
+
legitimately be negative *before* adding a base, which none of
|
|
444
|
+
these are once decoded) combined with either a `pcrel` base
|
|
445
|
+
(relative to this field's own vaddr) or a `datarel` base (relative
|
|
446
|
+
to the start of .eh_frame_hdr itself). Raises for anything else --
|
|
447
|
+
failing loudly rather than silently misinterpreting unwind data
|
|
448
|
+
matches this module's existing architecture-mismatch/unsupported-
|
|
449
|
+
relocation-type precedent elsewhere."""
|
|
450
|
+
width_bits = encoding & 0x0F
|
|
451
|
+
if width_bits not in (_DW_EH_PE_UDATA4, _DW_EH_PE_SDATA4):
|
|
452
|
+
raise ValueError(f"unsupported .eh_frame_hdr value encoding {hex(encoding)}")
|
|
453
|
+
base_bits = encoding & 0x70
|
|
454
|
+
if base_bits == _DW_EH_PE_PCREL:
|
|
455
|
+
return field_vaddr + raw
|
|
456
|
+
if base_bits == _DW_EH_PE_DATAREL:
|
|
457
|
+
return hdr_vaddr + raw
|
|
458
|
+
raise ValueError(f"unsupported .eh_frame_hdr base encoding {hex(encoding)}")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _parse_eh_frame_hdr(hdr_bytes: bytes, hdr_vaddr: int) -> List[Tuple[int, int]]:
|
|
462
|
+
"""-> [(initial_loc, fde_addr), ...] -- absolute vaddrs, decoded from
|
|
463
|
+
a .eh_frame_hdr's binary-search table per whatever DW_EH_PE_*
|
|
464
|
+
encodings its own 4-byte encoding header declares (see
|
|
465
|
+
_decode_eh_pe_sdata4), not any one hardcoded scheme. This is the
|
|
466
|
+
*only* thing ever read from a dependency's .eh_frame/.eh_frame_hdr --
|
|
467
|
+
the FDE/CIE bytes themselves never need touching at all (see
|
|
468
|
+
ElfEmbedder.embed()'s docstring for why: every PC-relative offset a
|
|
469
|
+
FDE encodes -- to its own function, to its CIE, to its LSDA -- stays
|
|
470
|
+
numerically correct under a uniform shift, and a dependency's whole
|
|
471
|
+
mapped image, .eh_frame included, is always relocated as one single
|
|
472
|
+
uniform shift by this module)."""
|
|
473
|
+
version, eh_frame_ptr_enc, fde_count_enc, table_enc = hdr_bytes[0:4]
|
|
474
|
+
if version != 1:
|
|
475
|
+
raise ValueError(f"unsupported .eh_frame_hdr version {version}")
|
|
476
|
+
pos = 4
|
|
477
|
+
if eh_frame_ptr_enc != _DW_EH_PE_OMIT:
|
|
478
|
+
pos += 4 # eh_frame_ptr itself is never needed by this module
|
|
479
|
+
if fde_count_enc == _DW_EH_PE_OMIT:
|
|
480
|
+
return []
|
|
481
|
+
if (fde_count_enc & 0x0F) != _DW_EH_PE_UDATA4:
|
|
482
|
+
raise ValueError(f"unsupported .eh_frame_hdr fde_count encoding {hex(fde_count_enc)}")
|
|
483
|
+
fde_count = struct.unpack_from("<I", hdr_bytes, pos)[0]
|
|
484
|
+
pos += 4
|
|
485
|
+
entries = []
|
|
486
|
+
for i in range(fde_count):
|
|
487
|
+
off = pos + i * 8
|
|
488
|
+
iloc_raw, fde_raw = struct.unpack_from("<ii", hdr_bytes, off)
|
|
489
|
+
initial_loc = _decode_eh_pe_sdata4(table_enc, iloc_raw, hdr_vaddr + off, hdr_vaddr)
|
|
490
|
+
fde_addr = _decode_eh_pe_sdata4(table_enc, fde_raw, hdr_vaddr + off + 4, hdr_vaddr)
|
|
491
|
+
entries.append((initial_loc, fde_addr))
|
|
492
|
+
return entries
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _build_eh_frame_hdr(hdr_vaddr: int, eh_frame_vaddr: int, entries: List[Tuple[int, int]]) -> bytes:
|
|
496
|
+
"""Inverse of _parse_eh_frame_hdr, always emitting GCC's own default
|
|
497
|
+
encoding scheme (`pcrel`/`sdata4` for the eh_frame_ptr field,
|
|
498
|
+
`udata4` for fde_count, `datarel`/`sdata4` for the table -- the same
|
|
499
|
+
scheme _parse_eh_frame_hdr already knows how to read, so a hdr this
|
|
500
|
+
function builds is always itself re-parseable) regardless of what
|
|
501
|
+
encoding scheme the *input* entries were originally read with.
|
|
502
|
+
`entries` must already be sorted ascending by initial_loc -- the ABI
|
|
503
|
+
requires this for the binary search the table exists to support to
|
|
504
|
+
actually work."""
|
|
505
|
+
eh_frame_ptr_field_vaddr = hdr_vaddr + 4
|
|
506
|
+
out = bytearray([1, 0x1B, 0x03, 0x3B])
|
|
507
|
+
out += struct.pack("<i", eh_frame_vaddr - eh_frame_ptr_field_vaddr)
|
|
508
|
+
out += struct.pack("<I", len(entries))
|
|
509
|
+
for initial_loc, fde_addr in entries:
|
|
510
|
+
out += struct.pack("<ii", initial_loc - hdr_vaddr, fde_addr - hdr_vaddr)
|
|
511
|
+
return bytes(out)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class ElfEmbedder:
|
|
515
|
+
def __init__(self, input_path: Path, output_path: Path, exclude: Set[str], lib_dir: Optional[Path]):
|
|
516
|
+
self.input_path = Path(input_path)
|
|
517
|
+
self.output_path = Path(output_path)
|
|
518
|
+
self.exclude = exclude
|
|
519
|
+
self.lib_dir = lib_dir
|
|
520
|
+
|
|
521
|
+
def _search_dirs(self) -> List[Path]:
|
|
522
|
+
if self.lib_dir is not None:
|
|
523
|
+
return [self.lib_dir]
|
|
524
|
+
dirs = [
|
|
525
|
+
self.input_path.parent,
|
|
526
|
+
Path("/lib"), Path("/usr/lib"), Path("/lib64"), Path("/usr/lib64"),
|
|
527
|
+
]
|
|
528
|
+
# Debian/Ubuntu-style "multiarch" layout: real shared objects live
|
|
529
|
+
# in an architecture-qualified subdirectory (e.g. the real
|
|
530
|
+
# libc.so.6 at /usr/lib/x86_64-linux-gnu/libc.so.6), never directly
|
|
531
|
+
# under /usr/lib itself -- discovered against real system binaries
|
|
532
|
+
# (Task 10b): without this, lib_dir=None (every resolvable import
|
|
533
|
+
# is a candidate, per this method's own docstring) silently
|
|
534
|
+
# resolved *zero* real dependencies on any such system, even
|
|
535
|
+
# libc.so.6, making the whole lib_dir=None/"full closure" path
|
|
536
|
+
# dead code there. /lib is a symlink to /usr/lib on these systems,
|
|
537
|
+
# so checking both costs nothing and also covers non-symlinked
|
|
538
|
+
# layouts (e.g. a system where only one of the two is qualified).
|
|
539
|
+
#
|
|
540
|
+
# Deliberately only the two *exact* triplets this module's own
|
|
541
|
+
# `_SUPPORTED_MACHINES` ever allows (EM_X86_64/EM_386) -- NOT a
|
|
542
|
+
# wildcard glob over every "*-linux-gnu*" directory present. An
|
|
543
|
+
# earlier version of this fix used such a glob and broke for
|
|
544
|
+
# real on a host that also had a foreign-architecture multiarch
|
|
545
|
+
# directory installed (e.g. aarch64-linux-gnu, from an unrelated
|
|
546
|
+
# cross-compilation/container toolchain already on the system):
|
|
547
|
+
# resolve_elf's own plain by-name file search has no architecture
|
|
548
|
+
# awareness at all, so a same-named file (e.g. "libc.so.6") in
|
|
549
|
+
# the *wrong*-architecture directory could get resolved first
|
|
550
|
+
# purely by directory sort order, pulling in a whole
|
|
551
|
+
# foreign-architecture dependency subtree that then failed this
|
|
552
|
+
# method's own (correct) architecture-mismatch check several
|
|
553
|
+
# steps later -- confirmed for real against a real multi-
|
|
554
|
+
# toolchain WSL box while building this fix. Hardcoding exactly
|
|
555
|
+
# the two supported triplets sidesteps that regardless of
|
|
556
|
+
# whatever *other* architectures' multiarch directories happen
|
|
557
|
+
# to also be installed.
|
|
558
|
+
#
|
|
559
|
+
# Moderate final-review finding: an earlier version of this
|
|
560
|
+
# method always listed "x86_64-linux-gnu" before
|
|
561
|
+
# "i386-linux-gnu" unconditionally, regardless of the HOST's own
|
|
562
|
+
# bitness -- resolve_elf's by-name search (see its own docstring)
|
|
563
|
+
# has no architecture awareness either, so for a genuinely
|
|
564
|
+
# 32-bit host/full-closure embed, a same-named file (e.g.
|
|
565
|
+
# "libc.so.6") sitting in the wrong-architecture (64-bit)
|
|
566
|
+
# multiarch directory would get resolved FIRST, pulled into the
|
|
567
|
+
# dependency graph, and only THEN rejected by this method's own
|
|
568
|
+
# (correct) architecture-mismatch check -- making 32-bit
|
|
569
|
+
# lib_dir=None/"full closure" embedding unusable in practice
|
|
570
|
+
# whenever both multiarch directories happen to carry a
|
|
571
|
+
# same-named file (the ordinary case on any real multiarch-
|
|
572
|
+
# enabled distro). Peeking at the host's own ELFCLASS (a single
|
|
573
|
+
# byte, e_ident[EI_CLASS] at a fixed offset -- no need for a full
|
|
574
|
+
# ELF parse just to pick a search order) and listing the
|
|
575
|
+
# matching-bitness triplet first fixes this: a 32-bit host now
|
|
576
|
+
# finds its own real i386-linux-gnu libc.so.6 before the
|
|
577
|
+
# wrong-architecture x86_64-linux-gnu one could ever shadow it.
|
|
578
|
+
host_is64 = _peek_elfclass64(self.input_path)
|
|
579
|
+
triplets = ("x86_64-linux-gnu", "i386-linux-gnu") if host_is64 else ("i386-linux-gnu", "x86_64-linux-gnu")
|
|
580
|
+
for triplet in triplets:
|
|
581
|
+
for base in (Path("/usr/lib"), Path("/lib")):
|
|
582
|
+
candidate = base / triplet
|
|
583
|
+
if candidate.is_dir():
|
|
584
|
+
dirs.append(candidate)
|
|
585
|
+
return dirs
|
|
586
|
+
|
|
587
|
+
def embed(self) -> None:
|
|
588
|
+
"""Embed every embeddable dependency (per Task 1's dependency
|
|
589
|
+
graph) into the host, reloc-bearing (PIC) path only.
|
|
590
|
+
|
|
591
|
+
Mechanism, per dependency (in dependencies-first order):
|
|
592
|
+
1. Copy its mapped image (every PT_LOAD segment, laid out at its
|
|
593
|
+
own vaddr-relative offset) into a new blob at a freshly chosen
|
|
594
|
+
vaddr `X`.
|
|
595
|
+
2. Apply the dependency's own .rela.dyn/.rel.dyn relocations
|
|
596
|
+
against that new placement (delta = X - dep_base, where
|
|
597
|
+
dep_base is the lowest vaddr among the dependency's own
|
|
598
|
+
PT_LOAD segments -- the ELF analogue of PE's ImageBase, except
|
|
599
|
+
ELF vaddrs are already absolute-in-their-own-space, with no
|
|
600
|
+
separate base field to add back in).
|
|
601
|
+
3. For each of the host's relocations against this dependency's
|
|
602
|
+
exported symbols (GOT/PLT slots -- R_*_JUMP_SLOT/R_*_GLOB_DAT
|
|
603
|
+
entries, whichever the compiler used), resolve the matching
|
|
604
|
+
export and patch the slot in place with the resolved absolute
|
|
605
|
+
address, exactly as eager (`-z now`) binding would; record
|
|
606
|
+
that slot (and every relocation applied in step 2) as a *new*
|
|
607
|
+
host R_*_RELATIVE entry, so the whole thing stays correct if
|
|
608
|
+
the final image is itself relocated at load time (ASLR).
|
|
609
|
+
Neutralize (zero out -- R_*_NONE) the original relocation
|
|
610
|
+
entry in place, so ld.so never tries to re-resolve a symbol
|
|
611
|
+
that no longer has a provider.
|
|
612
|
+
4. Convert the dependency's DT_NEEDED entry to a harmless
|
|
613
|
+
DT_DEBUG entry in place (see _remove_needed).
|
|
614
|
+
|
|
615
|
+
Deliberately NOT done: calling the dependency's own DT_INIT/
|
|
616
|
+
DT_INIT_ARRAY entries (the ELF analogue of PE's DllMain(...,
|
|
617
|
+
DLL_PROCESS_ATTACH, ...) call, which pe_embedder's docstring lists
|
|
618
|
+
as its own step 6). An earlier version of this method did exactly
|
|
619
|
+
that -- collect those function pointers (already relocated to
|
|
620
|
+
their new addresses, since init_array slots are themselves
|
|
621
|
+
R_*_RELATIVE targets) and call them from a generated stub
|
|
622
|
+
installed as the new e_entry, mirroring pe_embedder's approach.
|
|
623
|
+
Verified by actually building and running the embedded output on
|
|
624
|
+
real Linux (not just reasoning about it): on x86 (EM_386), this
|
|
625
|
+
reliably segfaults. crtstuff's own init_array entries
|
|
626
|
+
(`frame_dummy`/`register_tm_clones`, injected into *every* GCC-
|
|
627
|
+
built shared object regardless of user code -- the same
|
|
628
|
+
toolchain-boilerplate pattern as the weak GLOB_DAT symbols
|
|
629
|
+
documented below) are compiled as 32-bit PIC code, which computes
|
|
630
|
+
its own GOT-relative addressing via a `call __x86.get_pc_thunk.bx`
|
|
631
|
+
/ `add ebx, <offset>` sequence at entry; calling them "cold" from
|
|
632
|
+
a plain `mov eax, imm32; call eax` (no attempt to establish
|
|
633
|
+
whatever calling-convention precondition that sequence depends
|
|
634
|
+
on) corrupts something, with a concrete observed crash signature
|
|
635
|
+
of `SIGSEGV, error 6 (write, not-present), si_addr=1` a few
|
|
636
|
+
instructions into glibc's own post-relocation startup path. x86-64
|
|
637
|
+
(EM_X86_64) uses RIP-relative addressing instead and happened not
|
|
638
|
+
to crash in testing, but calling crtstuff-internal functions
|
|
639
|
+
directly is risky enough on its own merits (undocumented
|
|
640
|
+
preconditions, no argc/argv/envp despite glibc's own _dl_init
|
|
641
|
+
passing them by convention -- see glibc's elf/dl-init.c) that
|
|
642
|
+
shipping it on the strength of "didn't crash on one machine" would
|
|
643
|
+
just be trading a *known* bug for an *unknown* one. The task
|
|
644
|
+
brief's own instructions for this method don't actually list an
|
|
645
|
+
entrypoint stub as a required step (unlike Task 3's PE brief,
|
|
646
|
+
which explicitly does) -- so, given it's proven actively unsafe as
|
|
647
|
+
a naive port of PE's approach, it's left for Task 8 to design
|
|
648
|
+
properly (which will need to either establish the x86-32 PIC
|
|
649
|
+
calling convention precondition correctly, or find a safer hook
|
|
650
|
+
point than "call directly from a synthetic entrypoint stub").
|
|
651
|
+
e_entry is therefore never touched by this method.
|
|
652
|
+
|
|
653
|
+
Simplifications for this task (explicitly deferred -- see Task 8):
|
|
654
|
+
- The dependency's *own* further external requirements aren't
|
|
655
|
+
supported: if it has any PLT/JMPREL relocations at all, or any
|
|
656
|
+
RELA/REL entry referencing a symbol that's undefined within
|
|
657
|
+
itself, embedding fails loudly rather than silently leaving an
|
|
658
|
+
unresolvable reference in the output. Unlike PE's import-table
|
|
659
|
+
merge (step 5 in pe_embedder's docstring), nothing here merges
|
|
660
|
+
a dependency's remaining imports into the host's own dynamic
|
|
661
|
+
section -- the brief for this task doesn't ask for it, and the
|
|
662
|
+
fixtures this task is scoped to (single-function .so's with no
|
|
663
|
+
further library needs) never exercise it.
|
|
664
|
+
- Unlike pe_embedder, this doesn't treat "the dependency has no
|
|
665
|
+
relocation data at all" as the fixed-base/non-PIC signal: in
|
|
666
|
+
PE, a DLL with no base-relocation directory at all reliably
|
|
667
|
+
means a fixed-base build. In ELF, a *simple* PIC .so with no
|
|
668
|
+
absolute self-pointers (e.g. this task's own `lib.c` fixture)
|
|
669
|
+
just as reliably produces an *empty* .rela.dyn -- that's the
|
|
670
|
+
normal, fully-position-independent case, not a warning sign.
|
|
671
|
+
The actual fixed-base signal this method keys off instead is
|
|
672
|
+
the dependency's own `e_type` (see the base-address-reservation
|
|
673
|
+
comment further down, by `fixed_base_segments`, for the full
|
|
674
|
+
mechanism and its one real limitation).
|
|
675
|
+
- step 3's new-relocation-entries go into a brand new, bigger
|
|
676
|
+
REL(A) table at a new location (growing the existing one in
|
|
677
|
+
place isn't generally safe -- same reasoning as pe_embedder's
|
|
678
|
+
import-directory rebuild); the original entries are preserved
|
|
679
|
+
byte-for-byte as that new table's prefix.
|
|
680
|
+
- The host's extended PT_LOAD segment (dependency blobs +
|
|
681
|
+
combined relocation table, all appended as a single in-place
|
|
682
|
+
extension of it -- see below) has RWX ORed into its existing
|
|
683
|
+
flags, mirroring pe_embedder's blanket `_SCN_CODE_RWX` section
|
|
684
|
+
characteristics -- a real W^X split is out of scope for this
|
|
685
|
+
placeholder.
|
|
686
|
+
"""
|
|
687
|
+
search_dirs = self._search_dirs()
|
|
688
|
+
graph = build_dependency_graph(
|
|
689
|
+
self.input_path, list_elf_imports, resolve_elf, search_dirs, self.lib_dir, self.exclude
|
|
690
|
+
)
|
|
691
|
+
order = embed_order(graph)
|
|
692
|
+
# Names the *host* itself directly lists in its own DT_NEEDED --
|
|
693
|
+
# as opposed to a node that's only reachable *transitively*
|
|
694
|
+
# (needed by some other embedded dependency, not by the host).
|
|
695
|
+
# Only a direct name has anything to neutralize in the host's own
|
|
696
|
+
# dynamic section at all; see its one use, further down, for why
|
|
697
|
+
# treating every node in `order` as if it had one is wrong.
|
|
698
|
+
host_direct_import_names = set(list_elf_imports(self.input_path))
|
|
699
|
+
|
|
700
|
+
if not order:
|
|
701
|
+
self.output_path.write_bytes(self.input_path.read_bytes())
|
|
702
|
+
shutil.copymode(self.input_path, self.output_path)
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
raw = bytearray(self.input_path.read_bytes())
|
|
706
|
+
# A read-only snapshot of the *original* bytes: every vaddr/offset
|
|
707
|
+
# we look up via `elf` below stays valid against `raw` for the rest
|
|
708
|
+
# of this method, since we only ever append to `raw` or edit
|
|
709
|
+
# existing bytes in place -- never move or delete anything.
|
|
710
|
+
elf = ELFFile(io.BytesIO(bytes(raw)))
|
|
711
|
+
|
|
712
|
+
host_machine = elf["e_machine"]
|
|
713
|
+
if host_machine not in _SUPPORTED_MACHINES:
|
|
714
|
+
raise ValueError(
|
|
715
|
+
f"unsupported host architecture {host_machine!r}; only "
|
|
716
|
+
f"{_SUPPORTED_MACHINES} are supported"
|
|
717
|
+
)
|
|
718
|
+
is64 = elf.elfclass == 64
|
|
719
|
+
ptr_size = 8 if is64 else 4
|
|
720
|
+
|
|
721
|
+
host_dyn = _dynamic_segment(elf)
|
|
722
|
+
if host_dyn is None:
|
|
723
|
+
raise ValueError(f"'{self.input_path}' has no PT_DYNAMIC segment to embed into")
|
|
724
|
+
|
|
725
|
+
host_load_segments = list(elf.iter_segments(type="PT_LOAD"))
|
|
726
|
+
if not host_load_segments:
|
|
727
|
+
raise ValueError(f"'{self.input_path}' has no PT_LOAD segments")
|
|
728
|
+
|
|
729
|
+
host_reloc_tables = host_dyn.get_relocation_tables()
|
|
730
|
+
dyn_table_key = "RELA" if "RELA" in host_reloc_tables else ("REL" if "REL" in host_reloc_tables else None)
|
|
731
|
+
if dyn_table_key is None:
|
|
732
|
+
raise ValueError(
|
|
733
|
+
f"'{self.input_path}' has no DT_RELA/DT_REL table to extend; embedding "
|
|
734
|
+
"into a host with no dynamic-relocation table isn't supported"
|
|
735
|
+
)
|
|
736
|
+
dyn_tag_name = "DT_RELA" if dyn_table_key == "RELA" else "DT_REL"
|
|
737
|
+
dyn_sz_tag_name = "DT_RELASZ" if dyn_table_key == "RELA" else "DT_RELSZ"
|
|
738
|
+
dyn_table = host_reloc_tables[dyn_table_key]
|
|
739
|
+
dyn_is_rela = dyn_table.is_RELA()
|
|
740
|
+
ptr_mask = (1 << (ptr_size * 8)) - 1
|
|
741
|
+
|
|
742
|
+
def _host_dyn_tag_val(tag_name: str) -> Optional[int]:
|
|
743
|
+
tag = next(host_dyn.iter_tags(tag_name), None)
|
|
744
|
+
return tag.entry.d_val if tag is not None else None
|
|
745
|
+
|
|
746
|
+
# Needed only if some embedded dependency turns out to have a
|
|
747
|
+
# remaining external symbol reference to forward (see
|
|
748
|
+
# `external_symbol_index`, below) -- read unconditionally here
|
|
749
|
+
# since they're cheap dynamic-tag lookups, not real work.
|
|
750
|
+
host_symtab_count = _real_symtab_count(host_dyn, host_reloc_tables)
|
|
751
|
+
host_symtab_vaddr = _host_dyn_tag_val("DT_SYMTAB")
|
|
752
|
+
host_strtab_vaddr = _host_dyn_tag_val("DT_STRTAB")
|
|
753
|
+
host_strsz = _host_dyn_tag_val("DT_STRSZ")
|
|
754
|
+
host_versym_vaddr = _host_dyn_tag_val("DT_VERSYM")
|
|
755
|
+
|
|
756
|
+
def vaddr_to_offset(vaddr: int, size: int) -> int:
|
|
757
|
+
off = next(elf.address_offsets(vaddr, size), None)
|
|
758
|
+
if off is None:
|
|
759
|
+
raise ValueError(
|
|
760
|
+
f"address {hex(vaddr)} isn't covered by any PT_LOAD segment of '{self.input_path}'"
|
|
761
|
+
)
|
|
762
|
+
return off
|
|
763
|
+
|
|
764
|
+
new_sections: List[Tuple[int, bytes]] = [] # (vaddr, data), contiguous in placement order
|
|
765
|
+
new_reloc_entries: List[Tuple[int, int]] = [] # (host vaddr, value) -> new R_*_RELATIVE entries
|
|
766
|
+
combined_exports: Dict[str, Tuple[int, int, int]] = {} # export name -> (X, offset-within-dep, st_size)
|
|
767
|
+
# Each embedded dependency's own blob bytes, keyed by its own `X`
|
|
768
|
+
# (the same key `combined_exports` values' first element uses) --
|
|
769
|
+
# needed only for a host R_*_COPY relocation's data (see the
|
|
770
|
+
# R_COPY handling in the patch_specs loop, further down): at that
|
|
771
|
+
# point `raw` doesn't contain any embedded dependency's bytes yet
|
|
772
|
+
# (everything in `new_sections`/`fixed_base_segments` is only
|
|
773
|
+
# actually appended into `raw` by the "lay everything out" block,
|
|
774
|
+
# well after this), so a COPY relocation's source bytes have to
|
|
775
|
+
# come from here instead.
|
|
776
|
+
dep_blob_by_base: Dict[int, bytes] = {}
|
|
777
|
+
# (host vaddr, bytes-to-write) pairs for R_*_COPY relocations (see
|
|
778
|
+
# the R_COPY handling in the patch_specs loop) -- writing has to
|
|
779
|
+
# be deferred until after the "lay everything out" block, further
|
|
780
|
+
# down: a COPY relocation's target is frequently inside the
|
|
781
|
+
# host's own *BSS* (confirmed for real against bash/libtinfo.so.6
|
|
782
|
+
# on real Linux, WSL -- its `UP` symbol lands there), which has no
|
|
783
|
+
# file bytes at all yet at the point the patch_specs loop runs
|
|
784
|
+
# (the host's own `ext` PT_LOAD segment's filesz only grows to
|
|
785
|
+
# cover its full memsz -- backfilling that BSS gap with explicit
|
|
786
|
+
# zero bytes -- once "lay everything out" actually extends `raw`;
|
|
787
|
+
# see `ext_skew`'s own declaration, and this list's one other use
|
|
788
|
+
# further down, for exactly where/why).
|
|
789
|
+
deferred_copy_writes: List[Tuple[int, bytes]] = []
|
|
790
|
+
# A symbol an embedded dependency itself still references
|
|
791
|
+
# externally (through its own, now-dead PT_DYNAMIC/PLT) that no
|
|
792
|
+
# embedded dependency's own exports satisfy -- e.g. `malloc`,
|
|
793
|
+
# `__cxa_throw` (C++ exceptions always need at least the former,
|
|
794
|
+
# even in the simplest possible throw/catch: __cxa_allocate_
|
|
795
|
+
# exception calls it internally -- there's no way to support
|
|
796
|
+
# unwind-metadata preservation's own flagship scenario, an
|
|
797
|
+
# exception crossing the embedding boundary, without this).
|
|
798
|
+
# Forwarded into the host's own dynamic section (new DT_SYMTAB/
|
|
799
|
+
# DT_STRTAB/DT_VERSYM entries, further down, past the per-
|
|
800
|
+
# dependency loop) as a brand new, genuinely-external,
|
|
801
|
+
# R_*_GLOB_DAT-resolved import -- exactly as if the *host* had
|
|
802
|
+
# referenced it directly -- rather than something this module
|
|
803
|
+
# tries to resolve itself (unlike `combined_exports`, there's no
|
|
804
|
+
# static address to resolve here at all: the real provider, e.g.
|
|
805
|
+
# libc, is loaded and relocated by ld.so at a load-time-chosen
|
|
806
|
+
# address this module can't know). Keyed by name; value is this
|
|
807
|
+
# symbol's index in the *grown* host .dynsym (assigned in
|
|
808
|
+
# insertion order, starting right after the host's existing
|
|
809
|
+
# entries -- see where it's grown, further down).
|
|
810
|
+
external_symbol_index: Dict[str, int] = {}
|
|
811
|
+
# Whether every occurrence seen so far of a given forwarded
|
|
812
|
+
# name was itself a *weak* undefined reference (STB_WEAK) --
|
|
813
|
+
# if ANY occurrence is a strong (STB_GLOBAL) reference, the
|
|
814
|
+
# forwarded symbol must behave like a strong one (ld.so must
|
|
815
|
+
# raise if nothing resolves it), so this is ANDed across every
|
|
816
|
+
# occurrence rather than just keyed off the first one seen. See
|
|
817
|
+
# _pack_undef_sym's own docstring for why this distinction is
|
|
818
|
+
# preserved at all, instead of hard-zeroing every weak-undefined
|
|
819
|
+
# reference the way an earlier version of this method did.
|
|
820
|
+
external_symbol_is_weak: Dict[str, bool] = {}
|
|
821
|
+
new_glob_dat_entries: List[Tuple[int, int]] = [] # (slot vaddr, symbol index) -> new R_*_GLOB_DAT entries
|
|
822
|
+
# (initial_loc, fde_addr) pairs, absolute vaddrs already shifted
|
|
823
|
+
# by each dependency's own delta -- collected so the host's own
|
|
824
|
+
# .eh_frame_hdr binary-search table can be grown to cover them
|
|
825
|
+
# too (see the merge block past the per-dependency loop, and
|
|
826
|
+
# ElfEmbedder.embed()'s docstring, for why nothing here ever
|
|
827
|
+
# needs to touch the FDE/CIE bytes themselves).
|
|
828
|
+
collected_eh_entries: List[Tuple[int, int]] = []
|
|
829
|
+
# Resolved absolute VAs of every embedded dependency's own
|
|
830
|
+
# DT_INIT_ARRAY entries (its .init_array constructors -- already
|
|
831
|
+
# relocated to their final addresses by the time they're read,
|
|
832
|
+
# since .init_array slots are themselves normal R_*_RELATIVE
|
|
833
|
+
# targets processed like any other), flattened in
|
|
834
|
+
# dependencies-first order, for the generated stub to call.
|
|
835
|
+
|
|
836
|
+
init_array_calls: List[int] = []
|
|
837
|
+
|
|
838
|
+
# -- base-address reservation for fixed-base (non-PIC) dependencies --
|
|
839
|
+
# A dependency whose own ELF header says ET_EXEC (not ET_DYN) was
|
|
840
|
+
# built assuming it will *always* load at exactly its own PT_LOAD
|
|
841
|
+
# segments' own p_vaddr -- unlike a PIC .so (ET_DYN), which is
|
|
842
|
+
# designed to tolerate loading at any delta and records every
|
|
843
|
+
# absolute self-pointer that needs fixing up for that delta in its
|
|
844
|
+
# own .rela.dyn/.rel.dyn, a fixed-base dependency's code may contain
|
|
845
|
+
# absolute address references baked directly into its instructions
|
|
846
|
+
# with *no* corresponding relocation entry at all (real-world
|
|
847
|
+
# example, confirmed against this project's own toolchain: an
|
|
848
|
+
# internal-data access compiled with `-fno-pic` for a plain ET_EXEC
|
|
849
|
+
# target produces a bare `mov 0x<absolute addr>, %eax`-style
|
|
850
|
+
# instruction with zero relocation tracking, whereas the *same*
|
|
851
|
+
# access compiled into a `-shared` object -- PIC or not -- always
|
|
852
|
+
# gets at least an R_*_RELATIVE entry, because any `.so` must
|
|
853
|
+
# support loading away from its preferred base, and the linker
|
|
854
|
+
# arranges for that regardless of the `-f{,no-}pic` codegen flag).
|
|
855
|
+
# This is the ELF analogue of why a PE DLL with no base-relocation
|
|
856
|
+
# directory can only be loaded at exactly its declared ImageBase:
|
|
857
|
+
# there's no delta the loader could apply to such a dependency's
|
|
858
|
+
# untracked absolute references and have them stay correct, so the
|
|
859
|
+
# only place such a dependency can go is its own already-fixed
|
|
860
|
+
# address.
|
|
861
|
+
#
|
|
862
|
+
# So, for these nodes, `X` below is forced to the dependency's own
|
|
863
|
+
# `dep_base` (delta = X - dep_base = 0, a no-op for whatever
|
|
864
|
+
# relocations it *does* still have) rather than freshly allocated
|
|
865
|
+
# from `next_vaddr`, and that fixed range has to be reserved up
|
|
866
|
+
# front against both the host's own existing PT_LOAD ranges and
|
|
867
|
+
# every other such dependency's own reservation -- mirroring
|
|
868
|
+
# pe_embedder's same-named mechanism for a reloc-table-less DLL.
|
|
869
|
+
# Because that fixed address generally has nothing to do with where
|
|
870
|
+
# the host's own content ends, it can't just extend `ext` like
|
|
871
|
+
# everything else in this method does (see `fixed_base_segments`'s
|
|
872
|
+
# use, further down, for the brand-new-PT_LOAD-segment mechanism
|
|
873
|
+
# that needs instead, and why growing the program header array to
|
|
874
|
+
# add it is still AT_PHDR-safe).
|
|
875
|
+
#
|
|
876
|
+
# Soundness note (also true of pe_embedder's mechanism, and not
|
|
877
|
+
# solved here -- doesn't seem to be solvable at all without a way
|
|
878
|
+
# to enumerate a dependency's *untracked* absolute references,
|
|
879
|
+
# which by construction there's no record of): this only stays
|
|
880
|
+
# correct if the final combined image itself never gets relocated
|
|
881
|
+
# as a whole at load time (e.g. ASLR). If the host is non-PIE
|
|
882
|
+
# (ET_EXEC itself -- the realistic pairing for a genuinely
|
|
883
|
+
# fixed-base dependency, and what this task's own tests use), the
|
|
884
|
+
# kernel always maps every PT_LOAD segment at exactly its file
|
|
885
|
+
# p_vaddr and this is completely sound; if the host is PIE, the
|
|
886
|
+
# whole image (including whatever's reserved here) moves together
|
|
887
|
+
# under ASLR and a fixed-base dependency's untracked references
|
|
888
|
+
# silently go stale by the same amount the rest of the image moved.
|
|
889
|
+
fixed_base_segments: List[Tuple[int, bytes]] = [] # (X, blob) -- each gets its own new PT_LOAD entry
|
|
890
|
+
reserved_ranges: List[Tuple[int, int, str]] = [
|
|
891
|
+
(seg["p_vaddr"], seg["p_vaddr"] + seg["p_memsz"], f"the host image ('{self.input_path}')")
|
|
892
|
+
for seg in host_load_segments
|
|
893
|
+
]
|
|
894
|
+
|
|
895
|
+
# Every newly-built blob/reloc-table is appended as an
|
|
896
|
+
# *extension* of the host's own highest-vaddr PT_LOAD segment,
|
|
897
|
+
# rather than as a new PT_LOAD segment of its own -- see the large
|
|
898
|
+
# comment further down, right before this same `ext`/`ext_idx` pair
|
|
899
|
+
# is used to actually grow it, for why a brand new segment (with a
|
|
900
|
+
# freshly chosen vaddr/offset pair, with no particular reason to
|
|
901
|
+
# share whatever segment-specific skew AT_PHDR's derivation
|
|
902
|
+
# actually keys off) risks landing AT_PHDR on a bogus address.
|
|
903
|
+
all_host_segments = [elf.get_segment(i) for i in range(elf.num_segments())]
|
|
904
|
+
host_load_indices = [i for i, s in enumerate(all_host_segments) if s["p_type"] == "PT_LOAD"]
|
|
905
|
+
ext_idx = max(host_load_indices, key=lambda i: all_host_segments[i]["p_vaddr"] + all_host_segments[i]["p_memsz"])
|
|
906
|
+
ext = all_host_segments[ext_idx]
|
|
907
|
+
max_file_end = max(s["p_offset"] + s["p_filesz"] for s in all_host_segments)
|
|
908
|
+
if ext["p_offset"] + ext["p_filesz"] != max_file_end:
|
|
909
|
+
raise ValueError(
|
|
910
|
+
f"'{self.input_path}': its highest-vaddr PT_LOAD segment isn't also "
|
|
911
|
+
"the last one in the file; extending it in place isn't supported"
|
|
912
|
+
)
|
|
913
|
+
ext_align = ext["p_align"] if ext["p_align"] >= _PAGE else _PAGE
|
|
914
|
+
# The skew this segment's new (extended) content must keep -- any
|
|
915
|
+
# vaddr V within it is at file offset V - ext_skew, and vice versa.
|
|
916
|
+
ext_skew = ext["p_vaddr"] - ext["p_offset"]
|
|
917
|
+
# New content must start at a vaddr that (a) doesn't collide with
|
|
918
|
+
# `ext`'s own existing memsz, and (b) whose corresponding file
|
|
919
|
+
# offset (vaddr - ext_skew) is past the *actual* end of the file
|
|
920
|
+
# -- which, for any ordinary unstripped binary, extends further
|
|
921
|
+
# than `ext`'s own rounded-up memsz: .comment/.symtab/.strtab/
|
|
922
|
+
# .shstrtab/the section header table are normal trailing content
|
|
923
|
+
# that no PT_* segment covers at all (the check above only
|
|
924
|
+
# confirms `ext` is the last *segment-covered* content, which
|
|
925
|
+
# isn't the same as the last *byte in the file*). Taking the max
|
|
926
|
+
# of both lower bounds before rounding -- rather than assuming
|
|
927
|
+
# `ext`'s own page-rounding slack always happens to cover
|
|
928
|
+
# whatever trailing content exists -- means this never
|
|
929
|
+
# spuriously rejects a completely normal binary just because its
|
|
930
|
+
# one page of rounding slack happens to be smaller than its
|
|
931
|
+
# trailing symbol-table/debug-info content.
|
|
932
|
+
min_vaddr_for_memsz = ext["p_vaddr"] + ext["p_memsz"]
|
|
933
|
+
min_vaddr_for_eof = len(raw) + ext_skew
|
|
934
|
+
next_vaddr = _align_up(max(min_vaddr_for_memsz, min_vaddr_for_eof), ext_align)
|
|
935
|
+
|
|
936
|
+
# The host's own PT_DYNAMIC entry -- needed only if some embedded
|
|
937
|
+
# dependency turns out to have its own further, non-embedded
|
|
938
|
+
# DT_NEEDED import to forward into the host's own dynamic section
|
|
939
|
+
# (see the B2-fix block past the per-dependency loop, by
|
|
940
|
+
# `extra_needed_names`). Always exists (host_dyn above is None-
|
|
941
|
+
# checked already), found the same way as `ext_idx`/`phdr_idx`.
|
|
942
|
+
host_dynamic_idx = next(
|
|
943
|
+
i for i, s in enumerate(all_host_segments) if s["p_type"] == "PT_DYNAMIC"
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
# The host's own PT_GNU_EH_FRAME entry (if any), found the same
|
|
947
|
+
# way as `ext_idx`/`phdr_idx` -- needed only if some embedded
|
|
948
|
+
# dependency turns out to have unwind info to merge (see the
|
|
949
|
+
# merge block past the per-dependency loop).
|
|
950
|
+
host_gnu_eh_frame_idx = next(
|
|
951
|
+
(i for i, s in enumerate(all_host_segments) if s["p_type"] == "PT_GNU_EH_FRAME"), None
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# Every symbol name the HOST's own dynamic relocation table already
|
|
955
|
+
# has an R_*_COPY entry for, mapped to that entry's own target
|
|
956
|
+
# vaddr -- pre-scanned up front, before any dependency is
|
|
957
|
+
# processed, because of what an R_*_COPY relocation actually
|
|
958
|
+
# *means* per the ELF ABI: once ld.so processes it, the copied-to
|
|
959
|
+
# address in the *executable* becomes THE one true storage for
|
|
960
|
+
# that global, and every other module's own access to it --
|
|
961
|
+
# including the *defining* shared object's own internal GOT-
|
|
962
|
+
# indirect accesses to its own global, exactly the same
|
|
963
|
+
# same-module-but-interposable pattern this method's own
|
|
964
|
+
# `_resolve_dep_reference` docstring already discusses for a
|
|
965
|
+
# plain exported mutable data symbol -- has to be redirected
|
|
966
|
+
# there too, not left pointing at the shared object's own
|
|
967
|
+
# original (now dead) storage. Confirmed for real (a `bash`/
|
|
968
|
+
# `libtinfo.so.6` embed on real Linux, WSL, crashing deep inside
|
|
969
|
+
# bash's own `parse_and_execute` with a garbage pointer) before
|
|
970
|
+
# this fix: libtinfo's own internal reference to its "UP" global
|
|
971
|
+
# (copy-relocated into bash itself, since old-style termcap
|
|
972
|
+
# globals like UP/PC/BC are always accessed this way) was being
|
|
973
|
+
# resolved to libtinfo's own embedded-blob-local copy instead,
|
|
974
|
+
# silently diverging from the value anything else (bash itself,
|
|
975
|
+
# or libtinfo's own tgetent()-equivalent setup code) reads/writes
|
|
976
|
+
# through the host's copy -- needed *before* the per-dependency
|
|
977
|
+
# loop below so a dependency's own self-reference to such a
|
|
978
|
+
# symbol (processed inline, during that very loop) already
|
|
979
|
+
# redirects correctly, not just cross-dependency references
|
|
980
|
+
# resolved via `combined_exports`.
|
|
981
|
+
host_copy_targets: Dict[str, int] = {}
|
|
982
|
+
for copy_reloc in host_reloc_tables[dyn_table_key].iter_relocations():
|
|
983
|
+
if copy_reloc["r_info_type"] != R_COPY:
|
|
984
|
+
continue
|
|
985
|
+
copy_sym = host_dyn.get_symbol(copy_reloc["r_info_sym"])
|
|
986
|
+
if copy_sym.name:
|
|
987
|
+
host_copy_targets[copy_sym.name] = copy_reloc["r_offset"]
|
|
988
|
+
|
|
989
|
+
def _resolve_dep_reference(sym, blob: bytearray, blob_offset: int, X: int, dep_base: int,
|
|
990
|
+
node_name: str, addend: int) -> None:
|
|
991
|
+
"""Resolve a dependency's own reference to `sym` (anything
|
|
992
|
+
that isn't a plain load-bias-only R_*_RELATIVE entry, which
|
|
993
|
+
callers handle inline themselves without needing this) and
|
|
994
|
+
record whatever new *host*-level relocation entry keeps it
|
|
995
|
+
correct, exactly mirroring how the embedded dependency's own
|
|
996
|
+
relocation would have been processed had it still been
|
|
997
|
+
independently loaded:
|
|
998
|
+
- a self-reference, or a reference to an *earlier*
|
|
999
|
+
dependency's own export (dependencies-first order means
|
|
1000
|
+
those are already in `combined_exports` by the time a
|
|
1001
|
+
later dependency is processed) -- resolve directly, same
|
|
1002
|
+
as step 3's host GOT/PLT patch further down (a new
|
|
1003
|
+
R_*_RELATIVE entry).
|
|
1004
|
+
- a *named*, still-unresolved reference (weak or strong)
|
|
1005
|
+
-- forward it into the host's own dynamic section (see
|
|
1006
|
+
`external_symbol_index`'s own declaration for why; a new
|
|
1007
|
+
R_*_GLOB_DAT entry, not RELATIVE, since the real address
|
|
1008
|
+
is only known at load time, via ld.so's own by-name
|
|
1009
|
+
lookup, not something this module resolves itself),
|
|
1010
|
+
preserving its original STB_WEAK/STB_GLOBAL binding (see
|
|
1011
|
+
`_pack_undef_sym`) so ld.so's own real weak-symbol
|
|
1012
|
+
semantics apply -- a weak reference that's genuinely
|
|
1013
|
+
unresolved anywhere else, at the real process's load
|
|
1014
|
+
time, stays silently 0 exactly as it would have if this
|
|
1015
|
+
dependency had never been embedded, while a strong one
|
|
1016
|
+
still raises a fatal "undefined symbol" error.
|
|
1017
|
+
- an *unnamed* weak reference -- can't be forwarded (no
|
|
1018
|
+
name to look up by), so it's zeroed directly, exactly as
|
|
1019
|
+
ld.so itself would resolve a weak reference with nothing
|
|
1020
|
+
defining it.
|
|
1021
|
+
- an unnamed STRONG reference -- fails loudly; nothing
|
|
1022
|
+
can resolve it and there's no name to even try."""
|
|
1023
|
+
if sym.name and sym.name in host_copy_targets:
|
|
1024
|
+
# Takes priority over everything else below, including a
|
|
1025
|
+
# same-dependency self-reference to its own defined
|
|
1026
|
+
# symbol (the `new_val = X + ...` fallback at the bottom)
|
|
1027
|
+
# -- see `host_copy_targets`'s own declaration for why.
|
|
1028
|
+
new_val = (host_copy_targets[sym.name] + addend) & ptr_mask
|
|
1029
|
+
_write_int(blob, blob_offset, ptr_size, new_val)
|
|
1030
|
+
new_reloc_entries.append((X + blob_offset, new_val))
|
|
1031
|
+
return
|
|
1032
|
+
if sym.name and sym.name in combined_exports:
|
|
1033
|
+
exp_X, exp_off, _ = combined_exports[sym.name]
|
|
1034
|
+
new_val = (exp_X + exp_off + addend) & ptr_mask
|
|
1035
|
+
_write_int(blob, blob_offset, ptr_size, new_val)
|
|
1036
|
+
new_reloc_entries.append((X + blob_offset, new_val))
|
|
1037
|
+
return
|
|
1038
|
+
if sym["st_shndx"] == "SHN_UNDEF":
|
|
1039
|
+
is_weak = sym["st_info"]["bind"] == "STB_WEAK"
|
|
1040
|
+
if not sym.name:
|
|
1041
|
+
if is_weak:
|
|
1042
|
+
_write_int(blob, blob_offset, ptr_size, 0)
|
|
1043
|
+
return
|
|
1044
|
+
raise ValueError(
|
|
1045
|
+
f"dependency '{node_name}' has an unresolved, unnamed external "
|
|
1046
|
+
"symbol reference; can't forward it to the host"
|
|
1047
|
+
)
|
|
1048
|
+
if sym.name not in external_symbol_index:
|
|
1049
|
+
external_symbol_index[sym.name] = host_symtab_count + len(external_symbol_index)
|
|
1050
|
+
external_symbol_is_weak[sym.name] = is_weak
|
|
1051
|
+
else:
|
|
1052
|
+
external_symbol_is_weak[sym.name] = external_symbol_is_weak[sym.name] and is_weak
|
|
1053
|
+
_write_int(blob, blob_offset, ptr_size, 0)
|
|
1054
|
+
new_glob_dat_entries.append((X + blob_offset, external_symbol_index[sym.name]))
|
|
1055
|
+
return
|
|
1056
|
+
new_val = (X + (sym["st_value"] - dep_base) + addend) & ptr_mask
|
|
1057
|
+
_write_int(blob, blob_offset, ptr_size, new_val)
|
|
1058
|
+
new_reloc_entries.append((X + blob_offset, new_val))
|
|
1059
|
+
|
|
1060
|
+
# -- per-dependency: copy, relocate, and remove its DT_NEEDED -----
|
|
1061
|
+
for node in order:
|
|
1062
|
+
with open(node.path, "rb") as depf:
|
|
1063
|
+
dep = ELFFile(depf)
|
|
1064
|
+
if dep["e_machine"] != host_machine:
|
|
1065
|
+
raise ValueError(
|
|
1066
|
+
f"architecture mismatch embedding '{node.name}': host "
|
|
1067
|
+
f"'{self.input_path}' is {host_machine}, dependency "
|
|
1068
|
+
f"'{node.path}' is {dep['e_machine']}"
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
dep_load_segments = list(dep.iter_segments(type="PT_LOAD"))
|
|
1072
|
+
if not dep_load_segments:
|
|
1073
|
+
raise ValueError(f"dependency '{node.name}' has no PT_LOAD segments")
|
|
1074
|
+
dep_base = min(seg["p_vaddr"] for seg in dep_load_segments)
|
|
1075
|
+
blob_size = _align_up(
|
|
1076
|
+
max(seg["p_vaddr"] + seg["p_memsz"] for seg in dep_load_segments) - dep_base, _PAGE
|
|
1077
|
+
)
|
|
1078
|
+
# Minor final-review finding: p_memsz is an attacker-
|
|
1079
|
+
# controlled field (it's read directly from a dependency
|
|
1080
|
+
# file this module doesn't otherwise trust) with no
|
|
1081
|
+
# inherent upper bound of its own -- a crafted/malformed
|
|
1082
|
+
# dependency could declare a multi-exabyte p_memsz,
|
|
1083
|
+
# turning `bytearray(blob_size)` just below into an
|
|
1084
|
+
# unbounded-allocation DoS before any other validation
|
|
1085
|
+
# ever runs. Real shared objects never need anywhere
|
|
1086
|
+
# close to this; failing loudly on an obviously-absurd
|
|
1087
|
+
# size (comfortably above any legitimate real-world .so,
|
|
1088
|
+
# by many orders of magnitude) costs nothing for a
|
|
1089
|
+
# genuine dependency and avoids attempting the
|
|
1090
|
+
# allocation at all for an adversarial one.
|
|
1091
|
+
_MAX_SANE_BLOB_SIZE = 1 << 34 # 16 GiB
|
|
1092
|
+
if blob_size > _MAX_SANE_BLOB_SIZE:
|
|
1093
|
+
raise ValueError(
|
|
1094
|
+
f"dependency '{node.name}' declares an implausibly large mapped "
|
|
1095
|
+
f"size ({blob_size} bytes); refusing to allocate it"
|
|
1096
|
+
)
|
|
1097
|
+
blob = bytearray(blob_size)
|
|
1098
|
+
for seg in dep_load_segments:
|
|
1099
|
+
data = seg.data()
|
|
1100
|
+
start = seg["p_vaddr"] - dep_base
|
|
1101
|
+
blob[start:start + len(data)] = data
|
|
1102
|
+
|
|
1103
|
+
is_fixed_base = dep["e_type"] == "ET_EXEC"
|
|
1104
|
+
if is_fixed_base and elf["e_type"] == "ET_DYN":
|
|
1105
|
+
# Moderate final-review finding: a fixed-base
|
|
1106
|
+
# dependency's own untracked absolute references
|
|
1107
|
+
# (see the big comment above, by `fixed_base_segments`,
|
|
1108
|
+
# for exactly what those are and why) only stay
|
|
1109
|
+
# correct if the final combined image's load address
|
|
1110
|
+
# never shifts as a whole -- true for a non-PIE
|
|
1111
|
+
# (ET_EXEC) host (the kernel always maps every PT_LOAD
|
|
1112
|
+
# segment at exactly its declared p_vaddr), but NOT for
|
|
1113
|
+
# an ET_DYN host (a PIE executable, or this method's
|
|
1114
|
+
# own B6-ELF .so-input case): under ASLR, the whole
|
|
1115
|
+
# image -- including the fixed-base dependency's own
|
|
1116
|
+
# untracked references -- moves together by the same
|
|
1117
|
+
# load bias, silently going stale by exactly that
|
|
1118
|
+
# amount. Unlike PE (pe_embedder.py can clear
|
|
1119
|
+
# DYNAMIC_BASE/HIGH_ENTROPY_VA on its own output to
|
|
1120
|
+
# neutralize this for a fixed-base DLL dependency),
|
|
1121
|
+
# ELF has no per-image "disable ASLR" flag this module
|
|
1122
|
+
# can set on its own output -- PIE-ness is baked into
|
|
1123
|
+
# e_type/the kernel's own ASLR policy, not a flag this
|
|
1124
|
+
# module could flip afterward. Rather than silently
|
|
1125
|
+
# producing an output that corrupts itself the moment
|
|
1126
|
+
# ASLR picks a non-zero load address (the exact
|
|
1127
|
+
# silent-corruption case the design spec's anti-goal
|
|
1128
|
+
# forbids), fail loudly here instead.
|
|
1129
|
+
raise ValueError(
|
|
1130
|
+
f"dependency '{node.name}' is a fixed-base (non-PIC) ELF object, but "
|
|
1131
|
+
f"'{self.input_path}' is a PIE/ET_DYN image; a fixed-base dependency's "
|
|
1132
|
+
"own untracked absolute references would silently go stale under ASLR "
|
|
1133
|
+
"once the combined image's load address shifts -- embedding a fixed-base "
|
|
1134
|
+
"dependency is only supported into a non-PIE (ET_EXEC) host"
|
|
1135
|
+
)
|
|
1136
|
+
if is_fixed_base:
|
|
1137
|
+
X = dep_base
|
|
1138
|
+
lo, hi = X, X + blob_size
|
|
1139
|
+
for r_lo, r_hi, label in reserved_ranges:
|
|
1140
|
+
if lo < r_hi and r_lo < hi:
|
|
1141
|
+
raise ValueError(
|
|
1142
|
+
f"base address conflict at {hex(max(lo, r_lo))} between "
|
|
1143
|
+
f"dependency '{node.name}' (fixed base {hex(X)}) and {label}"
|
|
1144
|
+
)
|
|
1145
|
+
reserved_ranges.append((lo, hi, f"dependency '{node.name}' (fixed base {hex(X)})"))
|
|
1146
|
+
else:
|
|
1147
|
+
X = next_vaddr
|
|
1148
|
+
delta = X - dep_base
|
|
1149
|
+
|
|
1150
|
+
dep_dyn = _dynamic_segment(dep)
|
|
1151
|
+
if dep_dyn is None:
|
|
1152
|
+
raise ValueError(f"dependency '{node.name}' has no PT_DYNAMIC segment")
|
|
1153
|
+
|
|
1154
|
+
dep_reloc_tables = dep_dyn.get_relocation_tables()
|
|
1155
|
+
|
|
1156
|
+
for key in ("RELA", "REL"):
|
|
1157
|
+
table = dep_reloc_tables.get(key)
|
|
1158
|
+
if table is None:
|
|
1159
|
+
continue
|
|
1160
|
+
is_rela = table.is_RELA()
|
|
1161
|
+
for reloc in table.iter_relocations():
|
|
1162
|
+
rtype = reloc["r_info_type"]
|
|
1163
|
+
if rtype == R_NONE:
|
|
1164
|
+
continue
|
|
1165
|
+
target_vaddr = reloc["r_offset"]
|
|
1166
|
+
blob_offset = target_vaddr - dep_base
|
|
1167
|
+
if blob_offset < 0 or blob_offset + ptr_size > blob_size:
|
|
1168
|
+
raise ValueError(
|
|
1169
|
+
f"relocation target {hex(target_vaddr)} in dependency "
|
|
1170
|
+
f"'{node.name}' falls outside its own PT_LOAD segments"
|
|
1171
|
+
)
|
|
1172
|
+
if rtype == R_RELATIVE:
|
|
1173
|
+
base_value = (
|
|
1174
|
+
reloc["r_addend"] if is_rela else _read_int(blob, blob_offset, ptr_size)
|
|
1175
|
+
)
|
|
1176
|
+
new_val = (base_value + delta) & ptr_mask
|
|
1177
|
+
_write_int(blob, blob_offset, ptr_size, new_val)
|
|
1178
|
+
new_reloc_entries.append((X + blob_offset, new_val))
|
|
1179
|
+
continue
|
|
1180
|
+
if rtype not in (R_ABS_PLUS_ADDEND, R_GLOB_DAT):
|
|
1181
|
+
raise ValueError(
|
|
1182
|
+
f"unsupported relocation type {rtype} in dependency '{node.name}'"
|
|
1183
|
+
)
|
|
1184
|
+
sym = dep_dyn.get_symbol(reloc["r_info_sym"])
|
|
1185
|
+
addend = reloc["r_addend"] if is_rela else 0
|
|
1186
|
+
# See _resolve_dep_reference's own docstring for
|
|
1187
|
+
# the self/earlier-dependency-export vs. weak vs.
|
|
1188
|
+
# genuinely-external cases it handles -- R_*_GLOB_DAT
|
|
1189
|
+
# here (rather than the more usual
|
|
1190
|
+
# R_*_ABS_PLUS_ADDEND) for a symbol *defined* within
|
|
1191
|
+
# this same dependency means it's one of its own
|
|
1192
|
+
# exports (default/global visibility): GCC can't
|
|
1193
|
+
# assume a reference to a global-bound, exported
|
|
1194
|
+
# mutable data symbol is necessarily *its own*
|
|
1195
|
+
# definition (another loaded object could preempt/
|
|
1196
|
+
# interpose it), so even a same-module reference
|
|
1197
|
+
# goes through the GOT, resolved via symbol lookup
|
|
1198
|
+
# rather than a plain load-bias-only fixup. Confirmed
|
|
1199
|
+
# by a real crash (ValueError, not a silent
|
|
1200
|
+
# miscompute) building this task's own
|
|
1201
|
+
# `lib_ctor_b.c` fixture, whose non-`static`
|
|
1202
|
+
# `b_ctor_ran` global triggers exactly this -- with
|
|
1203
|
+
# no actual interposition happening here (this
|
|
1204
|
+
# dependency's own copy is the only one ever
|
|
1205
|
+
# loaded), resolving it to its own address is
|
|
1206
|
+
# correct either way.
|
|
1207
|
+
_resolve_dep_reference(sym, blob, blob_offset, X, dep_base, node.name, addend)
|
|
1208
|
+
|
|
1209
|
+
# A dependency's own PLT entries (DT_JMPREL) are always
|
|
1210
|
+
# R_*_JUMP_SLOT, always against an undefined (external)
|
|
1211
|
+
# symbol by construction -- *unless* it's a symbol some
|
|
1212
|
+
# earlier-processed (dependencies-first order) embedded
|
|
1213
|
+
# dependency itself exports, which _resolve_dep_reference
|
|
1214
|
+
# also handles. An earlier version of this method rejected
|
|
1215
|
+
# a dependency with *any* JMPREL entries outright ("further
|
|
1216
|
+
# external requirements aren't supported yet"); unwind-
|
|
1217
|
+
# metadata preservation's own flagship scenario -- a C++
|
|
1218
|
+
# exception crossing the embedding boundary -- makes that
|
|
1219
|
+
# genuinely unworkable to keep: even the simplest possible
|
|
1220
|
+
# `throw` calls `__cxa_throw`, which calls `malloc`
|
|
1221
|
+
# internally, through the dependency's own PLT. Verified by
|
|
1222
|
+
# actually forwarding a synthetic external import this way
|
|
1223
|
+
# and running the result on real Linux (WSL) before wiring
|
|
1224
|
+
# this in, for both 32- and 64-bit -- including the
|
|
1225
|
+
# .gnu.version/DT_VERSYM-array-growth part, which silent
|
|
1226
|
+
# static reasoning alone couldn't settle (ld.so's relocation
|
|
1227
|
+
# processing indexes it by the *same* symbol-table index as
|
|
1228
|
+
# the new symbol, so leaving it ungrown risks an out-of-
|
|
1229
|
+
# bounds read deciding a bogus required symbol version).
|
|
1230
|
+
jmprel = dep_reloc_tables.get("JMPREL")
|
|
1231
|
+
if jmprel is not None:
|
|
1232
|
+
is_rela = jmprel.is_RELA()
|
|
1233
|
+
for reloc in jmprel.iter_relocations():
|
|
1234
|
+
rtype = reloc["r_info_type"]
|
|
1235
|
+
if rtype == R_NONE:
|
|
1236
|
+
continue
|
|
1237
|
+
if rtype not in (R_JUMP_SLOT, R_ABS_PLUS_ADDEND, R_GLOB_DAT):
|
|
1238
|
+
raise ValueError(
|
|
1239
|
+
f"unsupported PLT relocation type {rtype} in dependency '{node.name}'"
|
|
1240
|
+
)
|
|
1241
|
+
target_vaddr = reloc["r_offset"]
|
|
1242
|
+
blob_offset = target_vaddr - dep_base
|
|
1243
|
+
if blob_offset < 0 or blob_offset + ptr_size > blob_size:
|
|
1244
|
+
raise ValueError(
|
|
1245
|
+
f"PLT relocation target {hex(target_vaddr)} in dependency "
|
|
1246
|
+
f"'{node.name}' falls outside its own PT_LOAD segments"
|
|
1247
|
+
)
|
|
1248
|
+
sym = dep_dyn.get_symbol(reloc["r_info_sym"])
|
|
1249
|
+
addend = reloc["r_addend"] if is_rela else 0
|
|
1250
|
+
_resolve_dep_reference(sym, blob, blob_offset, X, dep_base, node.name, addend)
|
|
1251
|
+
|
|
1252
|
+
for i in range(dep_dyn.num_symbols()):
|
|
1253
|
+
sym = dep_dyn.get_symbol(i)
|
|
1254
|
+
if (
|
|
1255
|
+
sym.name
|
|
1256
|
+
and sym["st_shndx"] != "SHN_UNDEF"
|
|
1257
|
+
and sym["st_info"]["type"] in ("STT_FUNC", "STT_OBJECT")
|
|
1258
|
+
and sym["st_info"]["bind"] in ("STB_GLOBAL", "STB_WEAK")
|
|
1259
|
+
):
|
|
1260
|
+
combined_exports[sym.name] = (X, sym["st_value"] - dep_base, sym["st_size"])
|
|
1261
|
+
|
|
1262
|
+
# This dependency's own .init_array constructors -- already
|
|
1263
|
+
# relocated to their final absolute addresses in `blob` by
|
|
1264
|
+
# the RELA/REL loop above, since each slot is itself a
|
|
1265
|
+
# normal R_*_RELATIVE target -- for the generated stub
|
|
1266
|
+
# (build_linux_stub) to call. Deliberately *not* collecting/
|
|
1267
|
+
# calling DT_INIT (legacy `_init`, a single crtstuff-
|
|
1268
|
+
# provided entry point, separate from DT_INIT_ARRAY): on a
|
|
1269
|
+
# modern GCC/glibc toolchain it's boilerplate with an empty
|
|
1270
|
+
# body for a plain `-shared` build (DT_INIT_ARRAY -- what
|
|
1271
|
+
# `__attribute__((constructor))` actually populates, and
|
|
1272
|
+
# what this task's own fixtures exercise -- carries every
|
|
1273
|
+
# real constructor instead), and Task 4's own docstring
|
|
1274
|
+
# already found calling *other* crtstuff-provided init
|
|
1275
|
+
# machinery directly, cold, to be unsafe on x86 -- no
|
|
1276
|
+
# reason to take on that same risk for a part no fixture
|
|
1277
|
+
# here needs.
|
|
1278
|
+
dep_init_array_vaddr = next(
|
|
1279
|
+
(t.entry.d_val for t in dep_dyn.iter_tags("DT_INIT_ARRAY")), None
|
|
1280
|
+
)
|
|
1281
|
+
dep_init_array_sz = next(
|
|
1282
|
+
(t.entry.d_val for t in dep_dyn.iter_tags("DT_INIT_ARRAYSZ")), None
|
|
1283
|
+
)
|
|
1284
|
+
if dep_init_array_vaddr is not None and dep_init_array_sz:
|
|
1285
|
+
for i in range(dep_init_array_sz // ptr_size):
|
|
1286
|
+
off = (dep_init_array_vaddr - dep_base) + i * ptr_size
|
|
1287
|
+
init_array_calls.append(_read_int(blob, off, ptr_size))
|
|
1288
|
+
|
|
1289
|
+
# This dependency's own unwind info, if it has any -- see
|
|
1290
|
+
# _parse_eh_frame_hdr's docstring for why only these two
|
|
1291
|
+
# numbers per FDE (never the FDE/CIE bytes themselves) ever
|
|
1292
|
+
# need reading, and the merge block past this loop for how
|
|
1293
|
+
# they get folded into the host's own .eh_frame_hdr.
|
|
1294
|
+
dep_eh_hdr_seg = next(dep.iter_segments(type="PT_GNU_EH_FRAME"), None)
|
|
1295
|
+
if dep_eh_hdr_seg is not None:
|
|
1296
|
+
hdr_vaddr = dep_eh_hdr_seg["p_vaddr"]
|
|
1297
|
+
hdr_off_in_blob = hdr_vaddr - dep_base
|
|
1298
|
+
hdr_bytes = bytes(blob[hdr_off_in_blob: hdr_off_in_blob + dep_eh_hdr_seg["p_filesz"]])
|
|
1299
|
+
for initial_loc, fde_addr in _parse_eh_frame_hdr(hdr_bytes, hdr_vaddr):
|
|
1300
|
+
collected_eh_entries.append((initial_loc + delta, fde_addr + delta))
|
|
1301
|
+
|
|
1302
|
+
# Only a node the *host* directly lists in its own
|
|
1303
|
+
# DT_NEEDED has anything to neutralize there at all -- a
|
|
1304
|
+
# transitive-only node (needed by some other embedded
|
|
1305
|
+
# dependency, e.g. this task's own lib_ctor_a->lib_ctor_b
|
|
1306
|
+
# fixture) has no entry of its own in the host's dynamic
|
|
1307
|
+
# section to begin with; its DT_NEEDED edge lives inside
|
|
1308
|
+
# *that other dependency's* own (already-copied-into-
|
|
1309
|
+
# `blob`, now-inert) PT_DYNAMIC segment instead, which
|
|
1310
|
+
# nothing ever reads again once that dependency is no
|
|
1311
|
+
# longer independently loaded as a module -- so there's
|
|
1312
|
+
# nothing to do here for it, and treating its absence as
|
|
1313
|
+
# an error (as an earlier version of this method did)
|
|
1314
|
+
# was itself the bug: it assumed every embedded node is
|
|
1315
|
+
# always a *direct* host import, which a real transitive
|
|
1316
|
+
# chain disproves.
|
|
1317
|
+
if node.name in host_direct_import_names:
|
|
1318
|
+
if not _remove_needed(host_dyn, raw, ptr_size, node.name):
|
|
1319
|
+
raise ValueError(f"expected DT_NEEDED entry for '{node.name}' not found in host")
|
|
1320
|
+
_remove_verneed_entry(host_dyn, raw, ptr_size, vaddr_to_offset, node.name)
|
|
1321
|
+
|
|
1322
|
+
dep_blob_by_base[X] = bytes(blob)
|
|
1323
|
+
if is_fixed_base:
|
|
1324
|
+
fixed_base_segments.append((X, bytes(blob)))
|
|
1325
|
+
else:
|
|
1326
|
+
new_sections.append((X, bytes(blob)))
|
|
1327
|
+
next_vaddr = _align_up(X + blob_size, _PAGE)
|
|
1328
|
+
|
|
1329
|
+
# -- B2 fix: every embedded dependency's OWN DT_NEEDED entries that
|
|
1330
|
+
# aren't themselves embedded need to become new entries in the
|
|
1331
|
+
# HOST's own DT_NEEDED, or ld.so never loads them at all --
|
|
1332
|
+
# even though `_resolve_dep_reference` already forwards the
|
|
1333
|
+
# dependency's remaining symbol references into the host's
|
|
1334
|
+
# dynamic section as plain by-name GLOB_DAT lookups (see its
|
|
1335
|
+
# own docstring), that only works if the library that actually
|
|
1336
|
+
# *defines* those symbols is also loaded. An embedded
|
|
1337
|
+
# dependency's own DT_NEEDED entry has no further use once it's
|
|
1338
|
+
# no longer independently loaded as a module (see the
|
|
1339
|
+
# `_remove_needed`-call comment just above) for an *embedded*
|
|
1340
|
+
# target -- but when that target was never resolved into the
|
|
1341
|
+
# graph at all (excluded, or simply outside `lib_dir`/
|
|
1342
|
+
# unresolvable), it's a real, still-needed runtime dependency
|
|
1343
|
+
# that this method must now take over advertising, since the
|
|
1344
|
+
# dependency that used to advertise it is gone. Order names are
|
|
1345
|
+
# deduplicated and kept in a deterministic (dependency, then
|
|
1346
|
+
# that dependency's own DT_NEEDED) order, skipping anything
|
|
1347
|
+
# that's itself embedded or that the host already has its own,
|
|
1348
|
+
# still-active DT_NEEDED entry for (no embedded node overlaps
|
|
1349
|
+
# this set, since those get neutralized above).
|
|
1350
|
+
order_names = {node.name for node in order}
|
|
1351
|
+
host_still_needed_names = host_direct_import_names - order_names
|
|
1352
|
+
extra_needed_names: List[str] = []
|
|
1353
|
+
_seen_extra_needed: Set[str] = set()
|
|
1354
|
+
for node in order:
|
|
1355
|
+
for needed in list_elf_imports(node.path):
|
|
1356
|
+
if (
|
|
1357
|
+
needed in order_names
|
|
1358
|
+
or needed in host_still_needed_names
|
|
1359
|
+
or needed in _seen_extra_needed
|
|
1360
|
+
):
|
|
1361
|
+
continue
|
|
1362
|
+
_seen_extra_needed.add(needed)
|
|
1363
|
+
extra_needed_names.append(needed)
|
|
1364
|
+
|
|
1365
|
+
# -- forward embedded dependencies' remaining external symbol
|
|
1366
|
+
# references into the host's own dynamic section (see
|
|
1367
|
+
# `external_symbol_index`'s own declaration for the full "why")
|
|
1368
|
+
# -- grow .dynsym/.dynstr (and .gnu.version/DT_VERSYM, if the
|
|
1369
|
+
# host has one, kept in lockstep since ld.so's relocation
|
|
1370
|
+
# processing indexes it by the very same symbol-table index) as
|
|
1371
|
+
# brand new, bigger combined tables, same append-past-the-end-
|
|
1372
|
+
# and-repoint-the-DT_*-tag pattern already used for the combined
|
|
1373
|
+
# relocation table just below (and PE's own import-directory
|
|
1374
|
+
# rebuild) -- growing any of these three in place isn't
|
|
1375
|
+
# generally safe either, for the same reason. `extra_needed_names`'
|
|
1376
|
+
# own strings (see the B2-fix block just above) are folded into
|
|
1377
|
+
# this same .dynstr growth, since both need "append new strings
|
|
1378
|
+
# past the end of a brand-new, bigger .dynstr" -- even if
|
|
1379
|
+
# `external_symbol_index` itself is empty, needing room for a
|
|
1380
|
+
# new DT_NEEDED's own name string is reason enough to grow
|
|
1381
|
+
# .dynstr (but NOT .dynsym/.gnu.version, which a plain
|
|
1382
|
+
# DT_NEEDED name has no entry in at all).
|
|
1383
|
+
needed_name_offsets: Dict[str, int] = {}
|
|
1384
|
+
dynstr_vaddr = host_strtab_vaddr
|
|
1385
|
+
if external_symbol_index or extra_needed_names:
|
|
1386
|
+
if host_strtab_vaddr is None or host_strsz is None:
|
|
1387
|
+
raise ValueError(
|
|
1388
|
+
f"'{self.input_path}' is missing DT_STRTAB/DT_STRSZ; can't forward an "
|
|
1389
|
+
"embedded dependency's external symbol/DT_NEEDED references into it"
|
|
1390
|
+
)
|
|
1391
|
+
old_dynstr_off = vaddr_to_offset(host_strtab_vaddr, host_strsz)
|
|
1392
|
+
old_dynstr_bytes = bytes(raw[old_dynstr_off: old_dynstr_off + host_strsz])
|
|
1393
|
+
new_dynstr_bytes = bytearray(old_dynstr_bytes)
|
|
1394
|
+
|
|
1395
|
+
if external_symbol_index:
|
|
1396
|
+
if host_symtab_vaddr is None:
|
|
1397
|
+
raise ValueError(
|
|
1398
|
+
f"'{self.input_path}' is missing DT_SYMTAB; can't forward an "
|
|
1399
|
+
"embedded dependency's external symbol references into it"
|
|
1400
|
+
)
|
|
1401
|
+
sym_entry_size = 24 if is64 else 16
|
|
1402
|
+
old_dynsym_off = vaddr_to_offset(host_symtab_vaddr, host_symtab_count * sym_entry_size)
|
|
1403
|
+
old_dynsym_bytes = bytes(
|
|
1404
|
+
raw[old_dynsym_off: old_dynsym_off + host_symtab_count * sym_entry_size]
|
|
1405
|
+
)
|
|
1406
|
+
new_dynsym_bytes = bytearray(old_dynsym_bytes)
|
|
1407
|
+
for name in external_symbol_index: # dict preserves insertion order == assigned index order
|
|
1408
|
+
name_off = len(new_dynstr_bytes)
|
|
1409
|
+
new_dynstr_bytes += name.encode("ascii") + b"\x00"
|
|
1410
|
+
new_dynsym_bytes += _pack_undef_sym(
|
|
1411
|
+
is64, name_off, weak=external_symbol_is_weak.get(name, False)
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
dynsym_vaddr = next_vaddr
|
|
1415
|
+
new_sections.append((dynsym_vaddr, bytes(new_dynsym_bytes)))
|
|
1416
|
+
next_vaddr = _align_up(dynsym_vaddr + len(new_dynsym_bytes), ptr_size)
|
|
1417
|
+
|
|
1418
|
+
for name in extra_needed_names:
|
|
1419
|
+
needed_name_offsets[name] = len(new_dynstr_bytes)
|
|
1420
|
+
new_dynstr_bytes += name.encode("ascii") + b"\x00"
|
|
1421
|
+
|
|
1422
|
+
dynstr_vaddr = next_vaddr
|
|
1423
|
+
new_sections.append((dynstr_vaddr, bytes(new_dynstr_bytes)))
|
|
1424
|
+
next_vaddr = _align_up(dynstr_vaddr + len(new_dynstr_bytes), _PAGE)
|
|
1425
|
+
|
|
1426
|
+
if external_symbol_index:
|
|
1427
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, "DT_SYMTAB", dynsym_vaddr)
|
|
1428
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, "DT_STRTAB", dynstr_vaddr)
|
|
1429
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, "DT_STRSZ", len(new_dynstr_bytes))
|
|
1430
|
+
|
|
1431
|
+
if external_symbol_index and host_versym_vaddr is not None:
|
|
1432
|
+
old_versym_off = vaddr_to_offset(host_versym_vaddr, host_symtab_count * 2)
|
|
1433
|
+
old_versym_bytes = bytes(raw[old_versym_off: old_versym_off + host_symtab_count * 2])
|
|
1434
|
+
# 0 (VER_NDX_LOCAL) per new entry: "no version requirement"
|
|
1435
|
+
# for this (our own, newly forwarded) reference -- an
|
|
1436
|
+
# ordinary unversioned lookup, verified end-to-end against
|
|
1437
|
+
# a real `malloc` import on real Linux (WSL, both 32- and
|
|
1438
|
+
# 64-bit) before wiring this in; see this method's docstring.
|
|
1439
|
+
# Only grown when `external_symbol_index` itself is
|
|
1440
|
+
# non-empty -- a plain new DT_NEEDED (see
|
|
1441
|
+
# `extra_needed_names`) adds no new .dynsym entry at all,
|
|
1442
|
+
# so .gnu.version (indexed 1:1 by .dynsym entry) needs no
|
|
1443
|
+
# corresponding growth for it.
|
|
1444
|
+
new_versym_bytes = old_versym_bytes + b"\x00\x00" * len(external_symbol_index)
|
|
1445
|
+
versym_vaddr = next_vaddr
|
|
1446
|
+
new_sections.append((versym_vaddr, new_versym_bytes))
|
|
1447
|
+
next_vaddr = _align_up(versym_vaddr + len(new_versym_bytes), _PAGE)
|
|
1448
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, "DT_VERSYM", versym_vaddr)
|
|
1449
|
+
|
|
1450
|
+
# -- patch the host's GOT/PLT references to embedded exports -----
|
|
1451
|
+
#
|
|
1452
|
+
# B5: whether the HOST itself was built with eager (`-z now`/
|
|
1453
|
+
# BIND_NOW) binding matters only for how a *PLT* (DT_JMPREL)
|
|
1454
|
+
# relocation entry gets neutralized below -- see the JMPREL-
|
|
1455
|
+
# specific branch, further down, for why. Checked via either of
|
|
1456
|
+
# the two real-world ways a linker records it (DF_BIND_NOW in
|
|
1457
|
+
# DT_FLAGS, or the more modern DF_1_NOW in DT_FLAGS_1 -- confirmed
|
|
1458
|
+
# against real `readelf -d` output on this project's own toolchain,
|
|
1459
|
+
# which emits the latter); the legacy, presence-only DT_BIND_NOW
|
|
1460
|
+
# tag is deliberately not checked too -- no modern linker emits it
|
|
1461
|
+
# (GNU ld hasn't since the mid-2000s), and a host that genuinely
|
|
1462
|
+
# used only that legacy mechanism would, worst case, just take the
|
|
1463
|
+
# (already-correct-today) eager-binding code path below instead of
|
|
1464
|
+
# the lazy one, not silently corrupt anything.
|
|
1465
|
+
DF_BIND_NOW = 0x8
|
|
1466
|
+
DF_1_NOW = 0x1
|
|
1467
|
+
host_bind_now = bool((_host_dyn_tag_val("DT_FLAGS") or 0) & DF_BIND_NOW) or bool(
|
|
1468
|
+
(_host_dyn_tag_val("DT_FLAGS_1") or 0) & DF_1_NOW
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
patch_specs = [(dyn_table_key, dyn_tag_name)]
|
|
1472
|
+
if "JMPREL" in host_reloc_tables:
|
|
1473
|
+
patch_specs.append(("JMPREL", "DT_JMPREL"))
|
|
1474
|
+
|
|
1475
|
+
for key, tag_name in patch_specs:
|
|
1476
|
+
table = host_reloc_tables[key]
|
|
1477
|
+
is_rela = table.is_RELA()
|
|
1478
|
+
# File offset of the *table itself* (what the DT_* tag's value
|
|
1479
|
+
# points at) -- not to be confused with the DT_* tag entry's
|
|
1480
|
+
# own location inside .dynamic (that's what
|
|
1481
|
+
# _find_dyn_tag_file_offset is for, used elsewhere to edit a
|
|
1482
|
+
# tag's d_val in place).
|
|
1483
|
+
_, table_file_offset = host_dyn.get_table_offset(tag_name)
|
|
1484
|
+
entry_size = table.entry_size
|
|
1485
|
+
for idx, reloc in enumerate(table.iter_relocations()):
|
|
1486
|
+
rtype = reloc["r_info_type"]
|
|
1487
|
+
if rtype == R_COPY:
|
|
1488
|
+
# A direct-data-symbol import (e.g. ncurses/termcap's
|
|
1489
|
+
# legacy `extern char *UP;`-style globals, confirmed
|
|
1490
|
+
# against a real `bash`/`libtinfo.so.6` embed on real
|
|
1491
|
+
# Linux (WSL) -- none of this project's own fixtures
|
|
1492
|
+
# exercise a copy relocation at all): the *value*
|
|
1493
|
+
# (`st_size` raw bytes, copied verbatim from the
|
|
1494
|
+
# definition) has to be written into the host's own
|
|
1495
|
+
# reserved space at `r_offset` right now, since ld.so
|
|
1496
|
+
# itself would normally do this copy at load time by
|
|
1497
|
+
# looking up the defining module by name -- which no
|
|
1498
|
+
# longer exists once that dependency is embedded,
|
|
1499
|
+
# otherwise reproducing exactly the undefined-symbol
|
|
1500
|
+
# load failure this fix was written against.
|
|
1501
|
+
sym = host_dyn.get_symbol(reloc["r_info_sym"])
|
|
1502
|
+
if not sym.name or sym.name not in combined_exports:
|
|
1503
|
+
continue
|
|
1504
|
+
X, offset_in_dep, dep_size = combined_exports[sym.name]
|
|
1505
|
+
size = sym["st_size"] or dep_size
|
|
1506
|
+
src = dep_blob_by_base[X][offset_in_dep:offset_in_dep + size]
|
|
1507
|
+
if len(src) != size:
|
|
1508
|
+
raise ValueError(
|
|
1509
|
+
f"COPY relocation for '{sym.name}' needs {size} bytes "
|
|
1510
|
+
f"at offset {offset_in_dep} in its defining dependency, "
|
|
1511
|
+
f"but only {len(src)} are available -- symbol size/offset "
|
|
1512
|
+
f"mismatch, refusing a silent partial copy"
|
|
1513
|
+
)
|
|
1514
|
+
target_vaddr = reloc["r_offset"]
|
|
1515
|
+
if not (ext["p_vaddr"] <= target_vaddr < ext["p_vaddr"] + ext["p_memsz"]):
|
|
1516
|
+
raise ValueError(
|
|
1517
|
+
f"R_*_COPY target {hex(target_vaddr)} in '{self.input_path}' falls "
|
|
1518
|
+
"outside its own extendable PT_LOAD segment; not supported"
|
|
1519
|
+
)
|
|
1520
|
+
deferred_copy_writes.append((target_vaddr, bytes(src)))
|
|
1521
|
+
# If the copied value is itself a pointer-sized field
|
|
1522
|
+
# whose value lands inside this very same dependency's
|
|
1523
|
+
# own embedded blob range, it's one of *our* addresses
|
|
1524
|
+
# (e.g. a `char *` into the dependency's own relocated
|
|
1525
|
+
# .rodata) and needs the same ASLR-safety new
|
|
1526
|
+
# R_*_RELATIVE entry as every other such address this
|
|
1527
|
+
# method introduces -- unlike a NULL or an address
|
|
1528
|
+
# pointing somewhere else entirely (e.g. still-external
|
|
1529
|
+
# data), which needs no fixup at all.
|
|
1530
|
+
#
|
|
1531
|
+
# Known architectural limitation (documented, not
|
|
1532
|
+
# fixed -- see PLAN.md's "Known problems" entry for
|
|
1533
|
+
# this): this only ever checks the copied value AS A
|
|
1534
|
+
# WHOLE, and only when it's exactly one pointer wide.
|
|
1535
|
+
# A larger COPY-relocated object (e.g. a struct with
|
|
1536
|
+
# its OWN interior pointer field, not just scalar
|
|
1537
|
+
# data) could have such a field pointing into the
|
|
1538
|
+
# dependency's own blob too, needing the exact same
|
|
1539
|
+
# fixup at that field's own offset -- but nothing
|
|
1540
|
+
# here can tell, from `size`/`src`'s raw bytes alone,
|
|
1541
|
+
# which sub-offsets of a >ptr_size object are even
|
|
1542
|
+
# pointers at all. ELF's own COPY-relocation symbol
|
|
1543
|
+
# info carries only `st_size` (a byte count), no
|
|
1544
|
+
# type/layout metadata; doing this correctly in
|
|
1545
|
+
# general needs DWARF type info this module doesn't
|
|
1546
|
+
# use anywhere else. Every real COPY-relocated symbol
|
|
1547
|
+
# this module has actually been tested against (this
|
|
1548
|
+
# project's own fixtures, and bash/libtinfo.so.6's
|
|
1549
|
+
# termcap globals in Task 10b) happens to be
|
|
1550
|
+
# pointer-sized or smaller, so this gap is currently
|
|
1551
|
+
# unexercised in practice.
|
|
1552
|
+
if size == ptr_size:
|
|
1553
|
+
copied_value = _read_int(src, 0, ptr_size)
|
|
1554
|
+
if X <= copied_value < X + len(dep_blob_by_base[X]):
|
|
1555
|
+
new_reloc_entries.append((reloc["r_offset"], copied_value))
|
|
1556
|
+
|
|
1557
|
+
entry_off = table_file_offset + idx * entry_size
|
|
1558
|
+
raw[entry_off:entry_off + entry_size] = b"\x00" * entry_size
|
|
1559
|
+
continue
|
|
1560
|
+
|
|
1561
|
+
if rtype not in (R_JUMP_SLOT, R_GLOB_DAT, R_ABS_PLUS_ADDEND):
|
|
1562
|
+
continue
|
|
1563
|
+
sym = host_dyn.get_symbol(reloc["r_info_sym"])
|
|
1564
|
+
if sym["st_shndx"] != "SHN_UNDEF" or not sym.name or sym.name not in combined_exports:
|
|
1565
|
+
continue
|
|
1566
|
+
X, offset_in_dep, _ = combined_exports[sym.name]
|
|
1567
|
+
addend = reloc["r_addend"] if is_rela else 0
|
|
1568
|
+
resolved = (X + offset_in_dep + addend) & ((1 << (ptr_size * 8)) - 1)
|
|
1569
|
+
target_off = vaddr_to_offset(reloc["r_offset"], ptr_size)
|
|
1570
|
+
_write_int(raw, target_off, ptr_size, resolved)
|
|
1571
|
+
|
|
1572
|
+
if key == "JMPREL" and rtype == R_JUMP_SLOT and not host_bind_now:
|
|
1573
|
+
# B5 fix: a lazy-bound host (the ABI default -- most
|
|
1574
|
+
# real-world ELF binaries, though this project's own
|
|
1575
|
+
# toolchain happens to default to BIND_NOW) still has
|
|
1576
|
+
# ld.so walk *every* DT_JMPREL entry at load time, via
|
|
1577
|
+
# a completely separate code path from the one that
|
|
1578
|
+
# processes the main DT_RELA/DT_REL table
|
|
1579
|
+
# (`elf_machine_lazy_rel`, not the generic relocation
|
|
1580
|
+
# dispatch `_remove_needed`'s own R_*_NONE-zeroing
|
|
1581
|
+
# trick already relies on elsewhere) -- its only job
|
|
1582
|
+
# for a R_*_JUMP_SLOT entry is to add the image's own
|
|
1583
|
+
# load bias to whatever's *already* sitting in the GOT
|
|
1584
|
+
# slot (deferring the real by-name symbol lookup to
|
|
1585
|
+
# the PLT0 trampoline on first call, under genuine
|
|
1586
|
+
# laziness) -- it does NOT consult the entry's own
|
|
1587
|
+
# symbol index to do this. Confirmed empirically (real
|
|
1588
|
+
# WSL Linux, both bitnesses): zeroing this entry to
|
|
1589
|
+
# R_*_NONE -- correct for the eager/BIND_NOW path,
|
|
1590
|
+
# where DT_JMPREL is instead processed through the
|
|
1591
|
+
# *generic* dispatch, which does tolerate R_*_NONE --
|
|
1592
|
+
# crashes a lazy-bound host at process startup with
|
|
1593
|
+
# "unexpected PLT reloc type 0x00", since
|
|
1594
|
+
# elf_machine_lazy_rel's own switch has no such
|
|
1595
|
+
# tolerance. The fix: leave this entry's bytes
|
|
1596
|
+
# completely untouched (still nominally R_*_JUMP_SLOT,
|
|
1597
|
+
# against its original, now-dead symbol -- never read
|
|
1598
|
+
# by elf_machine_lazy_rel for this type) and skip
|
|
1599
|
+
# adding a new host R_*_RELATIVE entry for the GOT slot
|
|
1600
|
+
# too, since `elf_machine_lazy_rel`'s own load-bias add
|
|
1601
|
+
# already does exactly that job for this specific slot
|
|
1602
|
+
# -- adding a second, independent bias-add via a new
|
|
1603
|
+
# RELATIVE entry in the *main* table would double-apply
|
|
1604
|
+
# the load bias on a PIE host (every fixture this
|
|
1605
|
+
# module is tested against), corrupting the address.
|
|
1606
|
+
continue
|
|
1607
|
+
|
|
1608
|
+
new_reloc_entries.append((reloc["r_offset"], resolved))
|
|
1609
|
+
|
|
1610
|
+
entry_off = table_file_offset + idx * entry_size
|
|
1611
|
+
raw[entry_off:entry_off + entry_size] = b"\x00" * entry_size
|
|
1612
|
+
|
|
1613
|
+
# -- B6-ELF: is the INPUT itself a shared object, rather than an
|
|
1614
|
+
# executable? -- ld.so never calls e_entry for anything but the
|
|
1615
|
+
# process's own main executable; a .so loaded via dlopen (or as
|
|
1616
|
+
# an ordinary DT_NEEDED dependency) has e_entry completely
|
|
1617
|
+
# ignored, so installing the generated init-call stub as the
|
|
1618
|
+
# new e_entry (as the EXE-input branch just below does) would
|
|
1619
|
+
# silently never run it at all. ET_EXEC always means "the
|
|
1620
|
+
# input is an ordinary non-PIE executable" -- no ambiguity. An
|
|
1621
|
+
# ET_DYN input, though, is ambiguous by itself: a modern PIE
|
|
1622
|
+
# executable is ALSO ET_DYN, indistinguishable from a plain
|
|
1623
|
+
# shared library by e_type alone. The real-world disambiguator
|
|
1624
|
+
# GNU ld actually emits is DF_1_PIE in DT_FLAGS_1 (confirmed
|
|
1625
|
+
# against this project's own toolchain's `readelf -d` output on
|
|
1626
|
+
# one of its own PIE fixtures: "Flags: NOW PIE") -- set only
|
|
1627
|
+
# for an ET_DYN that's actually meant to be executed directly,
|
|
1628
|
+
# never for an ordinary .so. Its absence on an ET_DYN input is
|
|
1629
|
+
# this method's signal that the input is a genuine shared
|
|
1630
|
+
# object, and the e_entry-based mechanism isn't applicable.
|
|
1631
|
+
DF_1_PIE = 0x08000000
|
|
1632
|
+
host_is_pie_exe = (elf["e_type"] == "ET_DYN") and bool((_host_dyn_tag_val("DT_FLAGS_1") or 0) & DF_1_PIE)
|
|
1633
|
+
host_is_so_input = (elf["e_type"] == "ET_DYN") and not host_is_pie_exe
|
|
1634
|
+
|
|
1635
|
+
# -- generated entrypoint/init-array stub (calls each embedded
|
|
1636
|
+
# dependency's own .init_array constructors, in dependencies-
|
|
1637
|
+
# first order) -- see build_linux_stub's/
|
|
1638
|
+
# build_linux_init_array_stub's own docstrings for exactly what
|
|
1639
|
+
# each does and doesn't do (the dlopen/dlsym hook
|
|
1640
|
+
# `hooked_names` implies isn't implemented yet, for the
|
|
1641
|
+
# EXE-input stub). Only installed when there's actually
|
|
1642
|
+
# something for it to do (`init_array_calls` non-empty) --
|
|
1643
|
+
# e_entry/DT_INIT_ARRAY are otherwise left completely
|
|
1644
|
+
# untouched, exactly as before this task, rather than
|
|
1645
|
+
# installing a stub that would just immediately return/
|
|
1646
|
+
# tail-jump with no other effect. Must run *before* the
|
|
1647
|
+
# combined relocation table just below, since the stub's own
|
|
1648
|
+
# embedded absolute-VA immediates need new R_*_RELATIVE entries
|
|
1649
|
+
# of their own (for ASLR-safety -- see build_linux_stub's
|
|
1650
|
+
# docstring), and that table is built once, from whatever's in
|
|
1651
|
+
# `new_reloc_entries` at that point.
|
|
1652
|
+
entry_stub_vaddr = None # only set in the EXE/PIE-exe branch below
|
|
1653
|
+
if init_array_calls and host_is_so_input:
|
|
1654
|
+
init_array_stub_vaddr = next_vaddr
|
|
1655
|
+
stub_bytes, stub_reloc_offsets = build_linux_init_array_stub(is64=is64, init_calls=init_array_calls)
|
|
1656
|
+
new_sections.append((init_array_stub_vaddr, stub_bytes))
|
|
1657
|
+
next_vaddr = _align_up(init_array_stub_vaddr + len(stub_bytes), _PAGE)
|
|
1658
|
+
for off in stub_reloc_offsets:
|
|
1659
|
+
new_reloc_entries.append((init_array_stub_vaddr + off, _read_int(stub_bytes, off, ptr_size)))
|
|
1660
|
+
|
|
1661
|
+
# Inject it as a brand-new, prepended entry in the host's own
|
|
1662
|
+
# DT_INIT_ARRAY -- the dynamic linker's normal, built-in
|
|
1663
|
+
# mechanism for calling a shared object's own constructors
|
|
1664
|
+
# when it's loaded (unlike e_entry, which it never touches
|
|
1665
|
+
# for anything but the main executable). Prepended (runs
|
|
1666
|
+
# before the host's own pre-existing constructors) to match
|
|
1667
|
+
# this method's own "dependencies-first" ordering convention
|
|
1668
|
+
# elsewhere. Only the array's *content* needs to grow at a
|
|
1669
|
+
# new location (re-pointing the already-existing DT_INIT_ARRAY/
|
|
1670
|
+
# DT_INIT_ARRAYSZ tags' own d_val fields in place, same
|
|
1671
|
+
# pattern as DT_SYMTAB/DT_STRTAB/DT_RELA growth elsewhere in
|
|
1672
|
+
# this method) -- no new *tag* needs adding to the dynamic
|
|
1673
|
+
# array itself, unlike B2's DT_NEEDED fix, since every
|
|
1674
|
+
# GCC-built .so already carries crtstuff's own DT_INIT_ARRAY/
|
|
1675
|
+
# DT_INIT_ARRAYSZ tags (frame_dummy/register_tm_clones) even
|
|
1676
|
+
# with no user-written constructor at all. A .so genuinely
|
|
1677
|
+
# built with neither tag at all (e.g. assembled by hand, or
|
|
1678
|
+
# by a toolchain that omits crtstuff) is explicitly out of
|
|
1679
|
+
# scope -- failing loudly here rather than also implementing
|
|
1680
|
+
# "add two brand-new dynamic tags from scratch" for a case
|
|
1681
|
+
# this project's own fixtures (and, empirically, every real
|
|
1682
|
+
# GCC-built .so) never hits.
|
|
1683
|
+
host_init_array_vaddr = _host_dyn_tag_val("DT_INIT_ARRAY")
|
|
1684
|
+
host_init_array_sz = _host_dyn_tag_val("DT_INIT_ARRAYSZ")
|
|
1685
|
+
if host_init_array_vaddr is None or not host_init_array_sz:
|
|
1686
|
+
raise ValueError(
|
|
1687
|
+
f"'{self.input_path}' is a shared object with no existing DT_INIT_ARRAY "
|
|
1688
|
+
"of its own; injecting an embedded dependency's constructor call into "
|
|
1689
|
+
"a .so input with no constructors of its own isn't supported"
|
|
1690
|
+
)
|
|
1691
|
+
old_ia_off = vaddr_to_offset(host_init_array_vaddr, host_init_array_sz)
|
|
1692
|
+
old_ia_bytes = bytes(raw[old_ia_off: old_ia_off + host_init_array_sz])
|
|
1693
|
+
ptr_fmt = "<Q" if is64 else "<I"
|
|
1694
|
+
new_ia_vaddr = next_vaddr
|
|
1695
|
+
new_ia_bytes = struct.pack(ptr_fmt, init_array_stub_vaddr) + old_ia_bytes
|
|
1696
|
+
new_sections.append((new_ia_vaddr, new_ia_bytes))
|
|
1697
|
+
next_vaddr = _align_up(new_ia_vaddr + len(new_ia_bytes), ptr_size)
|
|
1698
|
+
|
|
1699
|
+
# Every slot in the new array -- our own new one AND each of
|
|
1700
|
+
# the host's own original entries, now copied to a new
|
|
1701
|
+
# location -- is itself a pointer value that needs the
|
|
1702
|
+
# image's own load bias applied at load time, exactly like
|
|
1703
|
+
# every other absolute-VA this method writes; the host's own
|
|
1704
|
+
# original entries already had their OWN R_*_RELATIVE entries
|
|
1705
|
+
# for their OLD location (left stale/dead in place, same
|
|
1706
|
+
# pattern as every other abandoned old location in this
|
|
1707
|
+
# method), which doesn't help them at their NEW one.
|
|
1708
|
+
new_reloc_entries.append((new_ia_vaddr, init_array_stub_vaddr))
|
|
1709
|
+
for i in range(host_init_array_sz // ptr_size):
|
|
1710
|
+
old_val = _read_int(old_ia_bytes, i * ptr_size, ptr_size)
|
|
1711
|
+
new_reloc_entries.append((new_ia_vaddr + ptr_size + i * ptr_size, old_val))
|
|
1712
|
+
|
|
1713
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, "DT_INIT_ARRAY", new_ia_vaddr)
|
|
1714
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, "DT_INIT_ARRAYSZ", len(new_ia_bytes))
|
|
1715
|
+
elif init_array_calls:
|
|
1716
|
+
original_entry_va = elf["e_entry"]
|
|
1717
|
+
entry_stub_vaddr = next_vaddr
|
|
1718
|
+
stub_bytes, stub_reloc_offsets = build_linux_stub(
|
|
1719
|
+
[node.name for node in order],
|
|
1720
|
+
set(),
|
|
1721
|
+
is64=is64,
|
|
1722
|
+
init_calls=init_array_calls,
|
|
1723
|
+
original_entry=original_entry_va,
|
|
1724
|
+
)
|
|
1725
|
+
new_sections.append((entry_stub_vaddr, stub_bytes))
|
|
1726
|
+
next_vaddr = _align_up(entry_stub_vaddr + len(stub_bytes), _PAGE)
|
|
1727
|
+
for off in stub_reloc_offsets:
|
|
1728
|
+
new_reloc_entries.append((entry_stub_vaddr + off, _read_int(stub_bytes, off, ptr_size)))
|
|
1729
|
+
|
|
1730
|
+
# -- combined relocation table -------------------------------------
|
|
1731
|
+
if new_reloc_entries or new_glob_dat_entries:
|
|
1732
|
+
_, orig_table_offset = host_dyn.get_table_offset(dyn_tag_name)
|
|
1733
|
+
orig_size_tag = next(host_dyn.iter_tags(dyn_sz_tag_name), None)
|
|
1734
|
+
orig_table_size = orig_size_tag.entry.d_val if orig_size_tag is not None else 0
|
|
1735
|
+
orig_table_bytes = bytes(raw[orig_table_offset:orig_table_offset + orig_table_size])
|
|
1736
|
+
|
|
1737
|
+
new_entries_bytes = _pack_relative_entries(new_reloc_entries, is64, dyn_is_rela)
|
|
1738
|
+
new_glob_dat_bytes = _pack_glob_dat_entries(new_glob_dat_entries, is64, dyn_is_rela)
|
|
1739
|
+
combined_bytes = orig_table_bytes + new_entries_bytes + new_glob_dat_bytes
|
|
1740
|
+
|
|
1741
|
+
reloc_table_vaddr = next_vaddr
|
|
1742
|
+
new_sections.append((reloc_table_vaddr, combined_bytes))
|
|
1743
|
+
next_vaddr = _align_up(reloc_table_vaddr + len(combined_bytes), _PAGE)
|
|
1744
|
+
|
|
1745
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, dyn_tag_name, reloc_table_vaddr)
|
|
1746
|
+
_set_dyn_tag_value(host_dyn, raw, ptr_size, dyn_sz_tag_name, len(combined_bytes))
|
|
1747
|
+
|
|
1748
|
+
# -- B2 fix, continued: grow the host's own dynamic array itself
|
|
1749
|
+
# with one new DT_NEEDED entry per name in `extra_needed_names`
|
|
1750
|
+
# (see that variable's own declaration, earlier, for the full
|
|
1751
|
+
# "why") --
|
|
1752
|
+
#
|
|
1753
|
+
# Must run *after* every other `_set_dyn_tag_value` call above
|
|
1754
|
+
# (the external-symbol-forwarding block's DT_SYMTAB/DT_STRTAB/
|
|
1755
|
+
# DT_STRSZ/DT_VERSYM, and this combined-relocation-table block's
|
|
1756
|
+
# own DT_RELA/DT_RELASZ): those all edit the dynamic array's
|
|
1757
|
+
# entries *in place*, at their ORIGINAL file location (via
|
|
1758
|
+
# `host_dyn`, parsed once against the original bytes) -- reading
|
|
1759
|
+
# that same, by-now-fully-updated array's current byte content
|
|
1760
|
+
# here and copying it into a brand new, bigger array (rather than
|
|
1761
|
+
# growing it in place, same reasoning as every other table this
|
|
1762
|
+
# method grows) preserves every one of those edits. Doing this
|
|
1763
|
+
# any earlier would mean a later `_set_dyn_tag_value` call writes
|
|
1764
|
+
# into the OLD array's now-abandoned bytes instead of the new one
|
|
1765
|
+
# ld.so will actually read via the updated PT_DYNAMIC program
|
|
1766
|
+
# header entry below.
|
|
1767
|
+
#
|
|
1768
|
+
# New entries are spliced in just before the array's own
|
|
1769
|
+
# terminating DT_NULL (which must stay last -- every real-world
|
|
1770
|
+
# loader stops scanning at the first one), rather than appended
|
|
1771
|
+
# past it, mirroring `_remove_needed`'s own "never disturb the
|
|
1772
|
+
# DT_NULL terminator's position" rule for the same reason.
|
|
1773
|
+
if extra_needed_names:
|
|
1774
|
+
dyn_entry_size = 2 * ptr_size
|
|
1775
|
+
old_dyn_count = host_dyn.num_tags()
|
|
1776
|
+
old_dyn_bytes = bytes(raw[host_dyn["p_offset"]: host_dyn["p_offset"] + old_dyn_count * dyn_entry_size])
|
|
1777
|
+
null_index = next(
|
|
1778
|
+
(n for n in range(old_dyn_count) if host_dyn.get_tag(n).entry.d_tag == "DT_NULL"), None
|
|
1779
|
+
)
|
|
1780
|
+
if null_index is None:
|
|
1781
|
+
raise ValueError(f"'{self.input_path}': its dynamic section has no DT_NULL terminator")
|
|
1782
|
+
|
|
1783
|
+
# d_val for a DT_NEEDED entry is a plain .dynstr *offset* (not
|
|
1784
|
+
# an absolute vaddr), exactly like `needed_name_offsets` was
|
|
1785
|
+
# already recorded as.
|
|
1786
|
+
needed_fmt = "<QQ" if is64 else "<II"
|
|
1787
|
+
new_entries = bytearray()
|
|
1788
|
+
for name in extra_needed_names:
|
|
1789
|
+
new_entries += struct.pack(needed_fmt, DT_NEEDED, needed_name_offsets[name])
|
|
1790
|
+
|
|
1791
|
+
head = old_dyn_bytes[: null_index * dyn_entry_size]
|
|
1792
|
+
tail = old_dyn_bytes[null_index * dyn_entry_size:]
|
|
1793
|
+
new_dyn_bytes = bytes(head) + bytes(new_entries) + bytes(tail)
|
|
1794
|
+
|
|
1795
|
+
dynamic_vaddr = next_vaddr
|
|
1796
|
+
new_sections.append((dynamic_vaddr, new_dyn_bytes))
|
|
1797
|
+
next_vaddr = _align_up(dynamic_vaddr + len(new_dyn_bytes), _PAGE)
|
|
1798
|
+
|
|
1799
|
+
host_dynamic_seg = all_host_segments[host_dynamic_idx]
|
|
1800
|
+
dynamic_phdr_update = _pack_phdr(
|
|
1801
|
+
is64, PT_DYNAMIC, host_dynamic_seg["p_flags"], dynamic_vaddr - ext_skew, dynamic_vaddr,
|
|
1802
|
+
dynamic_vaddr, len(new_dyn_bytes), len(new_dyn_bytes), host_dynamic_seg["p_align"],
|
|
1803
|
+
)
|
|
1804
|
+
else:
|
|
1805
|
+
dynamic_phdr_update = None
|
|
1806
|
+
|
|
1807
|
+
# -- merge embedded dependencies' unwind info into the host's own
|
|
1808
|
+
# .eh_frame_hdr binary-search table --
|
|
1809
|
+
#
|
|
1810
|
+
# Once a dependency is embedded, it no longer has a PT_GNU_EH_FRAME
|
|
1811
|
+
# segment -- or module -- of its own: dl_iterate_phdr (what
|
|
1812
|
+
# _Unwind_Find_FDE actually walks to find unwind info for a given
|
|
1813
|
+
# PC) only ever sees the *host's* program headers now, so an
|
|
1814
|
+
# unwind through a PC that's now inside embedded dependency code
|
|
1815
|
+
# only succeeds if the *host's* own .eh_frame_hdr table has an
|
|
1816
|
+
# entry covering it. The dependency's FDEs themselves need no
|
|
1817
|
+
# changes at all to stay valid (see _parse_eh_frame_hdr's
|
|
1818
|
+
# docstring) -- only the table of (initial_loc, fde_addr) pairs
|
|
1819
|
+
# that lets the unwinder *find* them needs to grow to list them,
|
|
1820
|
+
# each already shifted by its own dependency's `delta` (collected
|
|
1821
|
+
# into `collected_eh_entries` during the per-dependency loop
|
|
1822
|
+
# above). A brand new, bigger table is built here (same append-
|
|
1823
|
+
# and-repoint pattern as everything else in this method) rather
|
|
1824
|
+
# than growing the existing one in place, and the host's *existing*
|
|
1825
|
+
# PT_GNU_EH_FRAME program-header entry is overwritten in place to
|
|
1826
|
+
# point at it -- unlike the phdr-array-growth case further down,
|
|
1827
|
+
# this only ever *edits* an already-existing entry, never adds a
|
|
1828
|
+
# new one, so it's needed in both the fixed_base_segments and
|
|
1829
|
+
# non-fixed_base_segments branches below alike.
|
|
1830
|
+
eh_hdr_update: Optional[bytes] = None
|
|
1831
|
+
if collected_eh_entries:
|
|
1832
|
+
if host_gnu_eh_frame_idx is None:
|
|
1833
|
+
raise ValueError(
|
|
1834
|
+
f"'{self.input_path}' has no PT_GNU_EH_FRAME segment to merge "
|
|
1835
|
+
"an embedded dependency's unwind info into"
|
|
1836
|
+
)
|
|
1837
|
+
host_eh_hdr_seg = all_host_segments[host_gnu_eh_frame_idx]
|
|
1838
|
+
host_hdr_vaddr = host_eh_hdr_seg["p_vaddr"]
|
|
1839
|
+
host_hdr_size = host_eh_hdr_seg["p_filesz"]
|
|
1840
|
+
host_hdr_file_off = vaddr_to_offset(host_hdr_vaddr, host_hdr_size)
|
|
1841
|
+
host_hdr_bytes = bytes(raw[host_hdr_file_off: host_hdr_file_off + host_hdr_size])
|
|
1842
|
+
host_eh_entries = _parse_eh_frame_hdr(host_hdr_bytes, host_hdr_vaddr)
|
|
1843
|
+
# The host's own .eh_frame start never moves (nothing in this
|
|
1844
|
+
# method ever relocates the host's *own*, pre-existing
|
|
1845
|
+
# content) -- re-derive it from the host's own hdr rather than
|
|
1846
|
+
# hardcoding anything, and keep using it as the regenerated
|
|
1847
|
+
# hdr's own eh_frame_ptr field too (that field is otherwise
|
|
1848
|
+
# unused by the dl_iterate_phdr/hdr-table-based unwind path
|
|
1849
|
+
# this relies on; see _build_eh_frame_hdr).
|
|
1850
|
+
eh_frame_ptr_enc = host_hdr_bytes[1]
|
|
1851
|
+
if eh_frame_ptr_enc == _DW_EH_PE_OMIT:
|
|
1852
|
+
raise ValueError(
|
|
1853
|
+
f"'{self.input_path}': its .eh_frame_hdr has no eh_frame_ptr field"
|
|
1854
|
+
)
|
|
1855
|
+
host_eh_frame_vaddr = _decode_eh_pe_sdata4(
|
|
1856
|
+
eh_frame_ptr_enc, struct.unpack_from("<i", host_hdr_bytes, 4)[0], host_hdr_vaddr + 4, host_hdr_vaddr
|
|
1857
|
+
)
|
|
1858
|
+
|
|
1859
|
+
combined_entries = sorted(host_eh_entries + collected_eh_entries, key=lambda e: e[0])
|
|
1860
|
+
new_hdr_vaddr = next_vaddr
|
|
1861
|
+
new_hdr_bytes = _build_eh_frame_hdr(new_hdr_vaddr, host_eh_frame_vaddr, combined_entries)
|
|
1862
|
+
new_sections.append((new_hdr_vaddr, new_hdr_bytes))
|
|
1863
|
+
next_vaddr = _align_up(new_hdr_vaddr + len(new_hdr_bytes), _PAGE)
|
|
1864
|
+
|
|
1865
|
+
eh_hdr_update = _pack_phdr(
|
|
1866
|
+
is64, PT_GNU_EH_FRAME, host_eh_hdr_seg["p_flags"], new_hdr_vaddr - ext_skew, new_hdr_vaddr,
|
|
1867
|
+
new_hdr_vaddr, len(new_hdr_bytes), len(new_hdr_bytes), host_eh_hdr_seg["p_align"],
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
# -- reserve room for a grown program header array, if any fixed-
|
|
1871
|
+
# base dependency needs its own new PT_LOAD entry (see the large
|
|
1872
|
+
# comment above, by `fixed_base_segments`, for why those can't
|
|
1873
|
+
# just extend `ext` like everything above this point does) --
|
|
1874
|
+
# placed as one more piece of `ext`'s own extension, same as the
|
|
1875
|
+
# relocation table just above, so it shares the extension's skew
|
|
1876
|
+
# and keeps AT_PHDR's computation correct even though e_phoff is
|
|
1877
|
+
# about to move (empirically confirmed via LD_SHOW_AUXV=1
|
|
1878
|
+
# against a real host while building this mechanism -- see the
|
|
1879
|
+
# comment further down, by `data_start_vaddr`, for exactly what
|
|
1880
|
+
# that showed and how it corrects this file's own older claim
|
|
1881
|
+
# about how the kernel derives AT_PHDR). Its *content*
|
|
1882
|
+
# can't be filled in yet -- it needs to record `ext`'s own
|
|
1883
|
+
# final, grown p_filesz/p_memsz, which isn't known until after
|
|
1884
|
+
# this whole extension (including this array's own reserved
|
|
1885
|
+
# bytes) is actually laid out below -- so a same-sized
|
|
1886
|
+
# zero-filled placeholder goes here for now and gets overwritten
|
|
1887
|
+
# in place, once that size is known, right before this method
|
|
1888
|
+
# returns.
|
|
1889
|
+
if fixed_base_segments:
|
|
1890
|
+
old_phnum = elf.num_segments()
|
|
1891
|
+
new_phnum = old_phnum + len(fixed_base_segments)
|
|
1892
|
+
phdr_array_size = new_phnum * elf["e_phentsize"]
|
|
1893
|
+
phdr_array_vaddr = next_vaddr
|
|
1894
|
+
new_sections.append((phdr_array_vaddr, b"\x00" * phdr_array_size))
|
|
1895
|
+
next_vaddr = _align_up(phdr_array_vaddr + phdr_array_size, _PAGE)
|
|
1896
|
+
|
|
1897
|
+
# -- lay everything out as an in-place extension of the host's own
|
|
1898
|
+
# `ext` PT_LOAD segment (see where ext/ext_idx/ext_skew/ext_align
|
|
1899
|
+
# were derived, above the per-dependency loop) --
|
|
1900
|
+
#
|
|
1901
|
+
# The straightforward-looking alternative -- append everything at
|
|
1902
|
+
# the end of the file in a brand new PT_LOAD segment of its own --
|
|
1903
|
+
# is risky: AT_PHDR's value is derived from e_phoff through a
|
|
1904
|
+
# translation that depends on *some* segment's own p_vaddr/
|
|
1905
|
+
# p_offset skew. An earlier version of this comment claimed that
|
|
1906
|
+
# skew always comes from the *first* PT_LOAD segment specifically,
|
|
1907
|
+
# with the kernel never re-deriving one per segment -- empirical
|
|
1908
|
+
# testing while building the fixed-base mechanism further up in
|
|
1909
|
+
# this method (LD_SHOW_AUXV=1 against a real host) contradicts
|
|
1910
|
+
# that: `ext` (the segment e_phoff gets moved into, there) does
|
|
1911
|
+
# *not* share the first PT_LOAD segment's skew for that host, yet
|
|
1912
|
+
# AT_PHDR still came out correct relative to *ext's own* skew. So
|
|
1913
|
+
# the precise mechanism this comment used to assert isn't right --
|
|
1914
|
+
# what's still true, and still the reason to prefer extending an
|
|
1915
|
+
# existing segment over adding a new one, is that *some*
|
|
1916
|
+
# segment-specific skew is involved, and there's no particular
|
|
1917
|
+
# reason a brand new segment, placed at a freshly chosen vaddr/
|
|
1918
|
+
# offset pair, would happen to share whatever skew that
|
|
1919
|
+
# translation actually keys off. Moving e_phoff into one without
|
|
1920
|
+
# knowing that risks AT_PHDR landing on a bogus address the moment
|
|
1921
|
+
# it happens. glibc's `_dl_aux_init`/`__libc_start_main` walk that
|
|
1922
|
+
# (potentially garbage) phdr array looking for PT_TLS/
|
|
1923
|
+
# PT_GNU_RELRO/PT_GNU_STACK for the main executable -- silently
|
|
1924
|
+
# skipping TLS/RELRO/stack-flag setup, or segfaulting outright if
|
|
1925
|
+
# the bogus address isn't even mapped.
|
|
1926
|
+
#
|
|
1927
|
+
# Extending an *existing* segment in place sidesteps this
|
|
1928
|
+
# completely: e_phoff/e_phnum (and every other segment's entry)
|
|
1929
|
+
# stay byte-for-byte untouched, so AT_PHDR's computation doesn't
|
|
1930
|
+
# change at all. Growing `ext`'s p_filesz/p_memsz all the way from
|
|
1931
|
+
# its own original p_offset up to the new end of file is always
|
|
1932
|
+
# safe *placement*-wise (nothing one of our new blobs writes can
|
|
1933
|
+
# collide with some other segment's bytes), *as long as* `ext` is
|
|
1934
|
+
# confirmed above to also be the last segment's content in the
|
|
1935
|
+
# file -- but it is NOT safe *content*-wise to leave whatever was
|
|
1936
|
+
# already sitting past `ext`'s own original filesz unchanged: an
|
|
1937
|
+
# earlier version of this comment claimed those bytes (e.g. a
|
|
1938
|
+
# stale section header table/.comment/.note content -- normal
|
|
1939
|
+
# for any unstripped-of-everything binary) were "inert... that
|
|
1940
|
+
# nothing else maps", which was true only in the *original* file,
|
|
1941
|
+
# where `ext`'s filesz genuinely stopped short of them. The
|
|
1942
|
+
# moment this method grows `ext`'s filesz to cover that same
|
|
1943
|
+
# span, the kernel DOES map it -- as real, non-zero file content,
|
|
1944
|
+
# not the zero-filled BSS the ABI promises for anything between
|
|
1945
|
+
# p_filesz and p_memsz. Confirmed for real (a `bash`/
|
|
1946
|
+
# `libtinfo.so.6` embed on real Linux, WSL) before this fix: a
|
|
1947
|
+
# handful of bash's own ordinary BSS globals happened to fall
|
|
1948
|
+
# inside that exact span (p_filesz..p_memsz, with real leftover
|
|
1949
|
+
# trailing bytes already occupying the first ~2KB of it on a
|
|
1950
|
+
# real, not-fully-stripped `/bin/bash`) and came up initialized
|
|
1951
|
+
# to that garbage instead of zero, crashing deep inside bash's
|
|
1952
|
+
# own `parse_and_execute` on first use. Zeroing that whole span
|
|
1953
|
+
# -- from `ext`'s own original filesz end through whatever the
|
|
1954
|
+
# file's actual length already is -- before growing filesz over
|
|
1955
|
+
# it restores the zero-BSS guarantee for it; losing those inert
|
|
1956
|
+
# bytes' own original content costs nothing, since nothing in
|
|
1957
|
+
# the original file's own PT_LOAD segments ever mapped them
|
|
1958
|
+
# either.
|
|
1959
|
+
ext_filesz_end_offset = ext["p_offset"] + ext["p_filesz"]
|
|
1960
|
+
if ext_filesz_end_offset < len(raw):
|
|
1961
|
+
raw[ext_filesz_end_offset:len(raw)] = b"\x00" * (len(raw) - ext_filesz_end_offset)
|
|
1962
|
+
|
|
1963
|
+
data_start_vaddr = new_sections[0][0]
|
|
1964
|
+
data_start_offset = data_start_vaddr - ext_skew
|
|
1965
|
+
if data_start_offset < len(raw):
|
|
1966
|
+
raise ValueError("internal error: new ELF content would overlap existing file content")
|
|
1967
|
+
raw.extend(b"\x00" * (data_start_offset - len(raw)))
|
|
1968
|
+
|
|
1969
|
+
# Now that `raw` covers `ext`'s whole original memsz as real file
|
|
1970
|
+
# bytes (the backfill just above) -- not just its original,
|
|
1971
|
+
# possibly-smaller filesz -- any deferred R_*_COPY write's target
|
|
1972
|
+
# (validated above to be within `ext`) is guaranteed in range;
|
|
1973
|
+
# see `deferred_copy_writes`'s own declaration for why this can't
|
|
1974
|
+
# happen any earlier.
|
|
1975
|
+
for copy_target_vaddr, copy_src in deferred_copy_writes:
|
|
1976
|
+
copy_target_off = copy_target_vaddr - ext_skew
|
|
1977
|
+
raw[copy_target_off:copy_target_off + len(copy_src)] = copy_src
|
|
1978
|
+
|
|
1979
|
+
cursor_vaddr = data_start_vaddr
|
|
1980
|
+
for vaddr, data in new_sections:
|
|
1981
|
+
pad = vaddr - cursor_vaddr
|
|
1982
|
+
if pad < 0:
|
|
1983
|
+
raise ValueError("internal error: overlapping generated ELF segments")
|
|
1984
|
+
raw.extend(b"\x00" * pad)
|
|
1985
|
+
raw.extend(data)
|
|
1986
|
+
cursor_vaddr = vaddr + len(data)
|
|
1987
|
+
|
|
1988
|
+
new_filesz = len(raw) - ext["p_offset"]
|
|
1989
|
+
extended_entry = _pack_phdr(
|
|
1990
|
+
is64, PT_LOAD, ext["p_flags"] | PF_R | PF_W | PF_X, ext["p_offset"], ext["p_vaddr"],
|
|
1991
|
+
ext["p_paddr"], new_filesz, new_filesz, ext["p_align"],
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
# `reserved_ranges` (seeded above, before the per-dependency loop)
|
|
1995
|
+
# only ever checked a fixed-base node's reservation against the
|
|
1996
|
+
# host's own *original* PT_LOAD ranges and other fixed-base
|
|
1997
|
+
# nodes' reservations -- never against `ext`, since fixed-base
|
|
1998
|
+
# nodes don't participate in `next_vaddr`'s progression and
|
|
1999
|
+
# `ext`'s own eventual grown extent (every ordinary dependency's
|
|
2000
|
+
# blob, the combined relocation table, the phdr-array placeholder
|
|
2001
|
+
# -- everything laid out just above) isn't known until right now,
|
|
2002
|
+
# well after every fixed-base node already passed its own check.
|
|
2003
|
+
# A fixed-base reservation that looked clear of the host at the
|
|
2004
|
+
# time could still end up virtually overlapping wherever `ext`
|
|
2005
|
+
# actually grew to by the time this method is done -- the same
|
|
2006
|
+
# silent-overlap anti-goal `reserved_ranges`'s own checks exist to
|
|
2007
|
+
# catch everywhere else, just caught late instead of never (same
|
|
2008
|
+
# class of gap as pe_embedder's alignment-gap-filler fix).
|
|
2009
|
+
if fixed_base_segments:
|
|
2010
|
+
ext_final_lo, ext_final_hi = ext["p_vaddr"], ext["p_vaddr"] + new_filesz
|
|
2011
|
+
for X, blob in fixed_base_segments:
|
|
2012
|
+
lo, hi = X, X + len(blob)
|
|
2013
|
+
if lo < ext_final_hi and ext_final_lo < hi:
|
|
2014
|
+
raise ValueError(
|
|
2015
|
+
f"base address conflict at {hex(max(lo, ext_final_lo))} between a "
|
|
2016
|
+
f"fixed-base dependency (base {hex(X)}) and the host's own extended "
|
|
2017
|
+
f"image (grown to {hex(ext_final_lo)}-{hex(ext_final_hi)})"
|
|
2018
|
+
)
|
|
2019
|
+
|
|
2020
|
+
if not fixed_base_segments:
|
|
2021
|
+
entry_off = elf["e_phoff"] + ext_idx * elf["e_phentsize"]
|
|
2022
|
+
raw[entry_off:entry_off + len(extended_entry)] = extended_entry
|
|
2023
|
+
if eh_hdr_update is not None:
|
|
2024
|
+
eh_entry_off = elf["e_phoff"] + host_gnu_eh_frame_idx * elf["e_phentsize"]
|
|
2025
|
+
raw[eh_entry_off:eh_entry_off + len(eh_hdr_update)] = eh_hdr_update
|
|
2026
|
+
if dynamic_phdr_update is not None:
|
|
2027
|
+
dyn_entry_off = elf["e_phoff"] + host_dynamic_idx * elf["e_phentsize"]
|
|
2028
|
+
raw[dyn_entry_off:dyn_entry_off + len(dynamic_phdr_update)] = dynamic_phdr_update
|
|
2029
|
+
else:
|
|
2030
|
+
# Grow the program header array itself: copy the *original*
|
|
2031
|
+
# array byte-for-byte (still untouched, at its original
|
|
2032
|
+
# location -- only `ext`'s own entry within the copy gets
|
|
2033
|
+
# updated), append one brand-new PT_LOAD entry per fixed-base
|
|
2034
|
+
# dependency, and write the result into the placeholder
|
|
2035
|
+
# reserved for it above (inside `ext`'s own just-finished
|
|
2036
|
+
# extension, so it shares whatever segment-specific skew
|
|
2037
|
+
# AT_PHDR's derivation actually keys off -- see the comment
|
|
2038
|
+
# by that placeholder for what that is and why it's what
|
|
2039
|
+
# keeps AT_PHDR correct here). The *original* array's bytes are
|
|
2040
|
+
# simply abandoned in place (dead, harmless bytes nothing reads
|
|
2041
|
+
# anymore once e_phoff is repointed) -- the same
|
|
2042
|
+
# leave-it-dead-in-place pattern _remove_needed already uses
|
|
2043
|
+
# for a neutralized DT_NEEDED entry.
|
|
2044
|
+
entsize = elf["e_phentsize"]
|
|
2045
|
+
old_phnum = elf.num_segments()
|
|
2046
|
+
old_array_bytes = bytes(raw[elf["e_phoff"]: elf["e_phoff"] + old_phnum * entsize])
|
|
2047
|
+
new_array = bytearray(old_array_bytes)
|
|
2048
|
+
new_array[ext_idx * entsize:(ext_idx + 1) * entsize] = extended_entry
|
|
2049
|
+
if eh_hdr_update is not None:
|
|
2050
|
+
new_array[host_gnu_eh_frame_idx * entsize:(host_gnu_eh_frame_idx + 1) * entsize] = eh_hdr_update
|
|
2051
|
+
if dynamic_phdr_update is not None:
|
|
2052
|
+
new_array[host_dynamic_idx * entsize:(host_dynamic_idx + 1) * entsize] = dynamic_phdr_update
|
|
2053
|
+
|
|
2054
|
+
placeholder_file_offset = phdr_array_vaddr - ext_skew
|
|
2055
|
+
|
|
2056
|
+
# The PT_PHDR entry (if any -- not every ELF file has one, but
|
|
2057
|
+
# every ordinary executable does) is the *other* place the
|
|
2058
|
+
# array's own location/size is recorded, independently of
|
|
2059
|
+
# e_phoff/e_phnum, and it's just as important to keep correct:
|
|
2060
|
+
# glibc's ld.so derives the main executable's load bias not
|
|
2061
|
+
# from e_phoff but from `AT_PHDR - PT_PHDR.p_vaddr` (AT_PHDR
|
|
2062
|
+
# being where the kernel *actually* put the array, which is
|
|
2063
|
+
# correct automatically -- see the e_phoff/e_phnum comment
|
|
2064
|
+
# below). Leaving PT_PHDR's own copy of the array's old
|
|
2065
|
+
# location in place would make that subtraction produce a
|
|
2066
|
+
# bogus nonzero "load bias" out of what should be a
|
|
2067
|
+
# non-relocated (ET_EXEC) image, corrupting every other
|
|
2068
|
+
# address ld.so derives from it -- confirmed by actually
|
|
2069
|
+
# running the result on real Linux (WSL): it crashes inside
|
|
2070
|
+
# ld.so itself, before any user code runs, with this entry
|
|
2071
|
+
# stale, and runs correctly once it's updated here too.
|
|
2072
|
+
phdr_idx = next((i for i, s in enumerate(all_host_segments) if s["p_type"] == "PT_PHDR"), None)
|
|
2073
|
+
if phdr_idx is not None:
|
|
2074
|
+
phdr_seg = all_host_segments[phdr_idx]
|
|
2075
|
+
updated_phdr_entry = _pack_phdr(
|
|
2076
|
+
is64, PT_PHDR, phdr_seg["p_flags"], placeholder_file_offset, phdr_array_vaddr,
|
|
2077
|
+
phdr_array_vaddr, phdr_array_size, phdr_array_size, phdr_seg["p_align"],
|
|
2078
|
+
)
|
|
2079
|
+
new_array[phdr_idx * entsize:(phdr_idx + 1) * entsize] = updated_phdr_entry
|
|
2080
|
+
|
|
2081
|
+
# Each fixed-base dependency's own blob is appended past the
|
|
2082
|
+
# very end of the file as it now stands (which already
|
|
2083
|
+
# includes `ext`'s whole extension, placeholder and all) --
|
|
2084
|
+
# unlike everything in `new_sections`, it doesn't need to be
|
|
2085
|
+
# contiguous with `ext` at all (it gets its own PT_LOAD entry,
|
|
2086
|
+
# with its own independent p_offset/p_vaddr pair), only
|
|
2087
|
+
# page-aligned consistently with its own fixed vaddr, which an
|
|
2088
|
+
# at-most-one-page padding gap is always enough to arrange
|
|
2089
|
+
# (the ELF ABI only requires p_vaddr and p_offset to agree
|
|
2090
|
+
# modulo p_align, not to be numerically equal).
|
|
2091
|
+
for X, blob in fixed_base_segments:
|
|
2092
|
+
offset = len(raw) + ((X - len(raw)) % _PAGE)
|
|
2093
|
+
raw.extend(b"\x00" * (offset - len(raw)))
|
|
2094
|
+
raw.extend(blob)
|
|
2095
|
+
new_array += _pack_phdr(is64, PT_LOAD, PF_R | PF_W | PF_X, offset, X, X, len(blob), len(blob), _PAGE)
|
|
2096
|
+
|
|
2097
|
+
if len(new_array) != phdr_array_size:
|
|
2098
|
+
raise ValueError("internal error: grown ELF program header array size mismatch")
|
|
2099
|
+
raw[placeholder_file_offset:placeholder_file_offset + len(new_array)] = new_array
|
|
2100
|
+
|
|
2101
|
+
# e_phoff/e_phnum are fixed-size fields at a fixed byte offset
|
|
2102
|
+
# in every ELF32/ELF64 header alike (this module writes every
|
|
2103
|
+
# byte it touches from the ABI's own fixed, hand-verified
|
|
2104
|
+
# struct layouts rather than through pyelftools, which is
|
|
2105
|
+
# read-only -- see this module's own top-of-file docstring);
|
|
2106
|
+
# e_phoff is ptr-sized (4 bytes for ELF32, 8 for ELF64) and
|
|
2107
|
+
# immediately follows e_ident/e_type/e_machine/e_version/
|
|
2108
|
+
# e_entry, while e_phnum is a fixed 2-byte field three
|
|
2109
|
+
# ptr-sized fields (e_phoff/e_shoff) plus one 4-byte field
|
|
2110
|
+
# (e_flags) and two 2-byte fields (e_ehsize/e_phentsize)
|
|
2111
|
+
# further along.
|
|
2112
|
+
e_phoff_off = 16 + 2 + 2 + 4 + ptr_size
|
|
2113
|
+
e_phnum_off = e_phoff_off + ptr_size * 2 + 4 + 2 + 2
|
|
2114
|
+
_write_int(raw, e_phoff_off, ptr_size, placeholder_file_offset)
|
|
2115
|
+
_write_int(raw, e_phnum_off, 2, old_phnum + len(fixed_base_segments))
|
|
2116
|
+
|
|
2117
|
+
# e_entry is overwritten to point at the generated stub only when
|
|
2118
|
+
# one was actually built as an EXE/PIE-exe entrypoint stub
|
|
2119
|
+
# (`entry_stub_vaddr` set above, by the init_array_calls-gated
|
|
2120
|
+
# block's EXE/PIE-exe branch); otherwise -- including the B6-ELF
|
|
2121
|
+
# .so-input case, where the init-array stub was installed via
|
|
2122
|
+
# DT_INIT_ARRAY instead, never touching e_entry at all -- it's
|
|
2123
|
+
# left completely untouched. e_entry is a fixed, ptr-sized field
|
|
2124
|
+
# immediately following e_ident/e_type/e_machine/e_version in
|
|
2125
|
+
# every ELF32/ELF64 header alike.
|
|
2126
|
+
if entry_stub_vaddr is not None:
|
|
2127
|
+
e_entry_off = 16 + 2 + 2 + 4
|
|
2128
|
+
_write_int(raw, e_entry_off, ptr_size, entry_stub_vaddr)
|
|
2129
|
+
|
|
2130
|
+
self.output_path.write_bytes(bytes(raw))
|
|
2131
|
+
# Moderate final-review finding: write_bytes() alone creates the
|
|
2132
|
+
# output file with the process's own default (umask-governed)
|
|
2133
|
+
# permissions, losing the input's executable bit -- every test in
|
|
2134
|
+
# this suite has had to work around this by calling
|
|
2135
|
+
# `out.chmod(0o755)` itself right after `embed()` returns,
|
|
2136
|
+
# strongly suggesting this was never actually handled here.
|
|
2137
|
+
# Preserve the input's own full permission bits (not just force
|
|
2138
|
+
# a fixed 0o755) so an input with unusual permissions (e.g. a
|
|
2139
|
+
# setuid/setgid bit, or a more restrictive executable-only mode)
|
|
2140
|
+
# round-trips the same way, matching the CLI's documented
|
|
2141
|
+
# "produces a working replacement for the original binary" goal.
|
|
2142
|
+
shutil.copymode(self.input_path, self.output_path)
|