Skip to content

Commit 803e2bc

Browse files
committed
Add ELF loader
1 parent a4a7b6b commit 803e2bc

13 files changed

Lines changed: 1335 additions & 2 deletions

dissect/executable/elf/c_elf.pyi

Lines changed: 920 additions & 0 deletions
Large diffs are not rendered by default.

dissect/executable/elf/tools/__init__.py

Whitespace-only changes.
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import contextlib
5+
import ctypes
6+
import ctypes.util
7+
import enum
8+
import json
9+
import logging
10+
import os
11+
import platform
12+
import random
13+
import struct
14+
import sys
15+
from io import BytesIO
16+
from pathlib import Path
17+
from typing import TYPE_CHECKING, Any, BinaryIO
18+
19+
from dissect.executable.elf.c_elf import PF, PT
20+
from dissect.executable.elf.elf import ELF
21+
22+
if TYPE_CHECKING:
23+
from collections.abc import Callable
24+
25+
from dissect.executable.elf.elf import Segment
26+
27+
log = logging.getLogger(__name__)
28+
log.setLevel(os.getenv("DISSECT_LOG_EXECUTABLE", "CRITICAL"))
29+
30+
31+
class PROT(enum.IntFlag):
32+
READ = 0x1
33+
WRITE = 0x2
34+
EXEC = 0x4
35+
36+
@classmethod
37+
def from_PF(cls, flags: PF) -> PROT:
38+
mapping = {
39+
PF.R: PROT.READ,
40+
PF.X: PROT.EXEC,
41+
PF.W: PROT.WRITE,
42+
}
43+
44+
result = 0
45+
for pf, prot in mapping.items():
46+
if flags & pf:
47+
result |= prot
48+
49+
return result
50+
51+
52+
class MAP(enum.IntFlag):
53+
SHARED = 0x01
54+
PRIVATE = 0x02
55+
FIXED = 0x10
56+
ANONYMOUS = 0x20
57+
58+
59+
class AT(enum.IntEnum):
60+
NULL = 0
61+
IGNORE = 1
62+
EXECFD = 2
63+
PHDR = 3
64+
PHENT = 4
65+
PHNUM = 5
66+
PAGESZ = 6
67+
BASE = 7
68+
FLAGS = 8
69+
ENTRY = 9
70+
NOTELF = 10
71+
UID = 11
72+
EUID = 12
73+
GID = 13
74+
EGID = 14
75+
PLATFORM = 15
76+
HWCAP = 16
77+
CLKTCK = 17
78+
SECURE = 23
79+
BASE_PLATFORM = 24
80+
RANDOM = 25
81+
EXECFN = 31
82+
SYSINFO = 32
83+
SYSINFO_EHDR = 33
84+
85+
86+
PAGE_SIZE = 4096
87+
ALIGN = PAGE_SIZE - 1
88+
89+
90+
libc = None
91+
loader = None
92+
93+
94+
def _setup_libc(path: str | None = None) -> None:
95+
"""Set up the libc functions.
96+
97+
Args:
98+
path: The optional path to the libc library to use.
99+
"""
100+
path = path or ctypes.util.find_library("c")
101+
if path is None:
102+
raise ValueError("Unable to find path to libc")
103+
104+
global libc
105+
if libc is not None:
106+
return
107+
108+
libc = ctypes.CDLL(path, use_errno=True)
109+
110+
libc.mmap.argtypes = [
111+
# addr
112+
ctypes.c_size_t,
113+
# length
114+
ctypes.c_size_t,
115+
# prot
116+
ctypes.c_int,
117+
# flags
118+
ctypes.c_int,
119+
# fd
120+
ctypes.c_int,
121+
# offset
122+
ctypes.c_size_t,
123+
]
124+
libc.mmap.restype = ctypes.c_size_t
125+
126+
libc.mprotect.argtypes = [
127+
# addr
128+
ctypes.c_size_t,
129+
# len
130+
ctypes.c_size_t,
131+
# prot
132+
ctypes.c_int,
133+
]
134+
libc.mprotect.restype = ctypes.c_int
135+
136+
libc.getauxval.argtypes = [
137+
# type
138+
ctypes.c_ulong
139+
]
140+
libc.getauxval.restype = ctypes.c_size_t
141+
142+
if log.level <= logging.DEBUG:
143+
libc.mmap = _ctype_log(libc.mmap)
144+
libc.mprotect = _ctype_log(libc.mprotect)
145+
libc.getauxval = _ctype_log(libc.getauxval)
146+
147+
148+
def _setup_loader() -> None:
149+
"""Set up the loader function."""
150+
global loader
151+
if loader is not None:
152+
return
153+
154+
machine = platform.machine()
155+
if machine == "x86_64":
156+
asm = bytes.fromhex(
157+
# rdi = pointer to initial stack
158+
# rsi = size of stack
159+
# rdx = entry point
160+
#
161+
# Reserve space for the stack and align it
162+
" 48 29 f4" # sub rsp, rsi
163+
"48 83 e4 f0" # and rsp, 0fffffffffffffff0h
164+
# Copy the stack over (dst=rdi, src=rsi, count=rcx)
165+
" 48 89 f1" # mov rcx, rsi
166+
" 48 89 fe" # mov rsi, rdi
167+
" 48 89 e7" # mov rdi, rsp
168+
" fc" # cld
169+
" f3 a4" # rep movsb
170+
# Jump to the entry point
171+
" ff e2" # jmp rdx
172+
" f4" # hlt
173+
)
174+
elif machine == "aarch64":
175+
asm = bytes.fromhex(
176+
# x0 = pointer to initial stack
177+
# x1 = size of stack
178+
# x2 = entry point
179+
#
180+
# Reserve space for the stack and align it
181+
"eb 63 21 cb" # sub x11, sp, x1
182+
"6b ed 7c 92" # and x11, x11, #~0xF
183+
"7f 01 00 91" # mov sp, x11
184+
# Copy the stack over (dst=x3, src=x0, count=x1)
185+
"e3 03 00 91" # mov x3, sp
186+
"a1 00 00 b4" # loop: cbz x1, end
187+
"06 14 40 38" # ldrb w6, [x0], #1
188+
"66 14 00 38" # strb w6, [x3], #1
189+
"21 04 00 f1" # subs x1, x1, #1
190+
"81 ff ff 54" # b.ne loop
191+
# Jump to the entry point
192+
"40 00 1f d6" # end: br x2
193+
)
194+
else:
195+
raise NotImplementedError(f"Unsupported architecture: {machine}")
196+
197+
buf = libc.mmap(0, len(asm), PROT.WRITE, MAP.PRIVATE | MAP.ANONYMOUS, -1, 0)
198+
ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char * len(asm)))
199+
ptr.contents[:] = asm
200+
libc.mprotect(buf, len(asm), PROT.READ | PROT.EXEC)
201+
202+
loader = ctypes.cast(
203+
buf,
204+
ctypes.CFUNCTYPE(
205+
ctypes.c_void_p,
206+
ctypes.c_char_p,
207+
ctypes.c_size_t,
208+
ctypes.c_size_t,
209+
),
210+
)
211+
212+
if log.level <= logging.DEBUG:
213+
loader = _ctype_log(loader)
214+
215+
216+
def _ctype_log(function: Callable) -> Callable:
217+
def fmt(arg: Any) -> str:
218+
if isinstance(arg, enum.Enum):
219+
return f"{arg.__class__.__name__}_{arg.name}"
220+
if isinstance(arg, int):
221+
return hex(arg)
222+
return repr(arg)
223+
224+
def wrapper(*args, **kwargs) -> Any:
225+
result = function(*args, **kwargs)
226+
arg_str = ", ".join(f"{fmt(arg)}" for arg in args)
227+
log.debug("%s(%s) -> %s", function.__name__, arg_str, fmt(result))
228+
return result
229+
230+
return wrapper
231+
232+
233+
def load(
234+
elf: Path | bytes, argv: list[str] | None = None, envp: dict[str, str] | None = None, *, libc: str | None = None
235+
) -> None:
236+
"""Load an ELF executable into memory and execute it.
237+
238+
Args:
239+
elf: The path to the ELF executable or the ELF data to load.
240+
argv: The arguments to pass to the executable.
241+
envp: The environment variables to pass to the executable.
242+
libc: The optional path to the libc library to use.
243+
"""
244+
if os.name != "posix" and sys.platform != "darwin":
245+
raise TypeError("This loader only supports POSIX systems")
246+
247+
_setup_libc(libc)
248+
_setup_loader()
249+
250+
argv = argv or []
251+
envp = envp or {}
252+
253+
fh = contextlib.nullcontext(BytesIO(elf)) if isinstance(elf, bytes) else elf.open("rb")
254+
with fh:
255+
elf = ELF(fh)
256+
257+
if elf.dynamic:
258+
raise NotImplementedError("Dynamic loading is not yet implemented, only static executables are supported")
259+
260+
segments = elf.segments.by_type(PT.LOAD)
261+
base = truncate(min([s.virtual_address for s in segments]))
262+
263+
for segment in segments:
264+
map_segment(segment, MAP.FIXED | MAP.PRIVATE | MAP.ANONYMOUS)
265+
266+
stack = create_stack(elf, base, argv, envp)
267+
loader(stack, len(stack), elf.header.e_entry)
268+
269+
270+
def map_segment(segment: Segment, flags: MAP) -> None:
271+
"""Map segment to memory.
272+
273+
Args:
274+
segment: The segment to map.
275+
flags: The flags to pass to mmap.
276+
"""
277+
alignment = segment.alignment - 1
278+
279+
offset = segment.virtual_address & alignment
280+
start = truncate(segment.virtual_address, alignment)
281+
size = round(segment.memory_size + offset, alignment)
282+
283+
pointer = libc.mmap(start, size, PROT.WRITE, flags, -1, 0)
284+
285+
if pointer == 0xFFFF_FFFF_FFFF_FFFF:
286+
raise ValueError("mmap failed to allocate memory.")
287+
288+
ptr = ctypes.cast(pointer, ctypes.POINTER(ctypes.c_char * size))
289+
ptr.contents[offset : offset + segment.size] = segment.data
290+
291+
pflags = PROT.from_PF(segment.flags)
292+
libc.mprotect(pointer, size, pflags)
293+
294+
295+
def create_stack(elf: ELF, base: int, argv: list[str], envp: dict[str, str]) -> bytes:
296+
"""Create the initial process stack for the executable.
297+
298+
The stack is created according to the System V AMD64 ABI specification, which can be found here:
299+
https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf
300+
301+
Args:
302+
elf: The ELF executable to create the stack for.
303+
base: The base address of the executable.
304+
argv: The arguments to pass to the executable.
305+
envp: The environment variables to pass to the executable.
306+
"""
307+
stack = BytesIO()
308+
309+
stack.write(struct.pack("<Q", len(argv)))
310+
311+
c_argv = (ctypes.c_char_p * len(argv))(*[s.encode() for s in argv])
312+
stack.write(c_argv)
313+
stack.write(ctypes.c_void_p())
314+
315+
c_envp = (ctypes.c_char_p * len(envp))(*[f"{k}={v}".encode() for k, v in envp.items()])
316+
stack.write(c_envp)
317+
stack.write(ctypes.c_void_p())
318+
319+
copy_aux(stack, AT.SYSINFO_EHDR)
320+
copy_aux(stack, AT.HWCAP)
321+
copy_aux(stack, AT.PAGESZ)
322+
copy_aux(stack, AT.CLKTCK)
323+
write_aux(stack, AT.PHDR, ctypes.c_size_t(base + elf.header.e_phoff))
324+
write_aux(stack, AT.PHENT, ctypes.c_size_t(elf.header.e_phentsize))
325+
write_aux(stack, AT.PHNUM, ctypes.c_size_t(elf.header.e_phnum))
326+
write_aux(stack, AT.BASE, ctypes.c_size_t(0))
327+
write_aux(stack, AT.FLAGS, ctypes.c_size_t(0))
328+
write_aux(stack, AT.ENTRY, ctypes.c_size_t(elf.header.e_entry))
329+
copy_aux(stack, AT.UID)
330+
copy_aux(stack, AT.EUID)
331+
copy_aux(stack, AT.GID)
332+
copy_aux(stack, AT.EGID)
333+
copy_aux(stack, AT.SECURE)
334+
write_aux(stack, AT.RANDOM, ctypes.c_char_p(random.randbytes(16)))
335+
write_aux(stack, AT.EXECFN, ctypes.c_char_p(c_argv[0]))
336+
write_aux(stack, AT.PLATFORM, ctypes.c_char_p(platform.machine().encode() + b"\x00"))
337+
write_aux(stack, AT.NULL, ctypes.c_size_t(0))
338+
339+
return stack.getvalue()
340+
341+
342+
def copy_aux(fh: BinaryIO, type: AT) -> None:
343+
"""Copy auxiliary value from the host process to the stack."""
344+
write_aux(fh, type, ctypes.c_uint64(libc.getauxval(type)))
345+
346+
347+
def write_aux(fh: BinaryIO, type: AT, value: bytes) -> None:
348+
"""Write auxiliary value to the stack."""
349+
fh.write(struct.pack("<Q", type))
350+
fh.write(value)
351+
352+
353+
def truncate(value: int, align: int = ALIGN) -> int:
354+
"""Truncate value to alignment."""
355+
return value & ~(align)
356+
357+
358+
def round(value: int, align: int = ALIGN) -> int:
359+
"""Round value up to alignment."""
360+
return truncate(value + align, align)
361+
362+
363+
def main() -> None:
364+
parser = argparse.ArgumentParser()
365+
parser.add_argument("path", type=Path, help="the ELF executable to load")
366+
parser.add_argument("--libc", type=str, help="the path of the libc library to use")
367+
parser.add_argument(
368+
"--env", "-e", type=json.loads, default=None, help="environment variables to pass to the executable"
369+
)
370+
parser.add_argument("-v", "--verbose", action="store_true", help="enable verbose logging")
371+
args, rest = parser.parse_known_args()
372+
373+
if args.verbose:
374+
logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s")
375+
log.setLevel(logging.DEBUG)
376+
377+
arguments = [args.path.name, *rest]
378+
load(args.path, arguments, args.env, libc=args.libc)
379+
380+
381+
if __name__ == "__main__":
382+
main()
625 KB
Binary file not shown.
65.9 KB
Binary file not shown.
68.8 KB
Binary file not shown.
767 KB
Binary file not shown.
14.1 KB
Binary file not shown.

tests/_data/elf/hello_world.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// gcc hello_world.c -Wl,-Ttext-segment=0x1000000 [-static] -o hello_world.bin
2+
#include <stdio.h>
3+
4+
int main() {
5+
setbuf(stdout, NULL);
6+
printf("Kusjes van SRT <3\n");
7+
}

0 commit comments

Comments
 (0)