Skip to content

Commit 1e3b9f5

Browse files
committed
allow accepting elf from stdin
bump fee to 0.2.0
1 parent fa46ed1 commit 1e3b9f5

File tree

3 files changed

+93
-33
lines changed

3 files changed

+93
-33
lines changed

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,25 @@ If you want to customise the arguments, use the `--argv` flag (`-a`):
7777
$ fee -a "killall sshd" ./busybox > output.py
7878
```
7979

80+
**If you don't wish to include the binary in the generated output**, you can instruct `fee` to generate a script which accepts the ELF from stdin at runtime. For this, use `-` for the filename. You can combine all of these options for clever one-liners:
81+
```console
82+
$ ssh user@target "$(fee -c -a "echo hi from stdin" -t "libc" -)" < ./busybox
83+
84+
hi from stdin
85+
```
86+
8087
__NB!__ By default, the script parses the encoded ELF's header to determine the target architecture. This is required to use the correct syscall number when calling `memfd_create`. If this fails, you can use the `--target-architecture` (`-t`) flag to explicitly generate a syscall number. Alternatively, you can use the `libc` target to resolve the symbol automatically at runtime, although this only works when generating Python code.
8188
For more exotic platforms, you should specify the syscall number manually. You need to search for `memfd_create` in your target's architecture's syscall table. This is located in various places in the Linux kernel sources. Just Googling `[architecture] syscall table` is perhaps the easiest. You can then specify the syscall number using the `--syscall` flag (`-s`).
8289

8390
Full help text:
8491
```
85-
usage: fee [-h] [-t ARCH | -s NUM] [-a ARGV] [-l LANG] [-c] [-p PATH] [-w CHARS] [-z LEVEL]
92+
usage: fee.py [-h] [-t ARCH | -s NUM] [-a ARGV] [-l LANG] [-c] [-p PATH] [-w CHARS] [-z LEVEL]
8693
path
8794
8895
Print code to stdout to execute an ELF without dropping files.
8996
9097
positional arguments:
91-
path path to the ELF file
98+
path path to the ELF file (use '-' to read from stdin at runtime)
9299
93100
optional arguments:
94101
-h, --help show this help message and exit

fee.py

+83-30
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,18 @@ def _get_e_machine(header: bytes) -> int:
3838
return machine
3939

4040

41+
def _err_stdin_flag(name, flag) -> int:
42+
print_err(f"error: {name} ('-{flag}') is required when using stdin\n")
43+
print_err("Use --help for more information.\n")
44+
return 1
45+
46+
4147
class CodeGenerator:
4248
def __init__(self) -> None:
4349
self.compression_level = 9
4450
self.wrap = 0
4551
self.syscall = None
52+
self.use_stdin = False
4653
self._meta = self._Python
4754
self._generator = None
4855

@@ -85,9 +92,7 @@ def with_command(self, **kwargs) -> str:
8592
kwargs.pop("path", None)
8693

8794
# let's try to hide our tracks a bit better
88-
return (
89-
f" set +o history; unset HISTFILE; {self._generator.with_command(**kwargs)}"
90-
)
95+
return f" unset HISTFILE; {self._generator.with_command(**kwargs)}"
9196

9297
class _Perl:
9398
# Perl generator metaclass
@@ -100,6 +105,7 @@ def __init__(self, outer) -> None:
100105
"Perl generator cannot resolve the syscall using libc, specify a target architecture"
101106
)
102107
self.syscall = outer.syscall
108+
self.use_stdin = outer.use_stdin
103109

104110
def with_command(self, path="/usr/bin/env perl") -> str:
105111
escaped = self.output.replace('"', '\\"')
@@ -110,10 +116,21 @@ def add(self, line: str) -> None:
110116
self.output += f"{line};\n"
111117

112118
def add_header(self) -> None:
113-
self.add("use MIME::Base64")
114-
self.add("use Compress::Zlib")
119+
if not self.use_stdin:
120+
self.add("use MIME::Base64")
121+
self.add("use Compress::Zlib")
115122

116123
def add_elf(self, elf: bytes) -> None:
124+
if self.use_stdin:
125+
self.add("open F, '</proc/self/fd/0'")
126+
self.add("binmode(F)")
127+
self.add("binmode(F)")
128+
self.add("{\nlocal $/")
129+
self.add("$e = <F>")
130+
self.add("}")
131+
self.add("close(F)")
132+
return
133+
117134
# prepare elf
118135
encoded = self.prep_elf(elf).decode("ascii")
119136

@@ -150,6 +167,7 @@ def __init__(self, outer) -> None:
150167
self.prep_elf = outer._prepare_elf
151168
self.wrap = outer.wrap
152169
self.syscall = outer.syscall
170+
self.use_stdin = outer.use_stdin
153171

154172
def with_command(self, path="/usr/bin/env python") -> str:
155173
escaped = self.output.replace('"', '\\"')
@@ -159,14 +177,25 @@ def add(self, line: str) -> None:
159177
self.output += f"{line}\n"
160178

161179
def add_header(self) -> None:
162-
self.add("import ctypes, os, base64, zlib")
180+
imports = "ctypes, os"
181+
if not self.use_stdin:
182+
imports += ", base64, zlib"
183+
self.add(f"import {imports}")
163184
self.add("l = ctypes.CDLL(None)")
164185
if self.syscall:
165186
self.add("s = l.syscall") # we specify the syscall manually
166187
else:
167188
self.add("s = l.memfd_create") # dynamic
168189

169190
def add_elf(self, elf: bytes) -> None:
191+
if self.use_stdin:
192+
self.add("from sys import stdin, version_info")
193+
self.add("if version_info >= (3, 0):")
194+
self.add(" e = stdin.buffer.read()")
195+
self.add("else:")
196+
self.add(" e = stdin.read()")
197+
return
198+
170199
# prepare elf
171200
encoded = f"{self.prep_elf(elf)}"
172201

@@ -207,6 +236,7 @@ def __init__(self, outer) -> None:
207236
"Ruby generator cannot resolve the syscall using libc, specify a target architecture"
208237
)
209238
self.syscall = outer.syscall
239+
self.use_stdin = outer.use_stdin
210240

211241
def with_command(self, path="/usr/bin/env ruby") -> str:
212242
escaped = self.output.replace('"', '\\"')
@@ -217,10 +247,16 @@ def add(self, line: str) -> None:
217247
self.output += f"{line}\n"
218248

219249
def add_header(self) -> None:
220-
self.add("require 'base64'")
221-
self.add("require 'zlib'")
250+
if not self.use_stdin:
251+
self.add("require 'base64'")
252+
self.add("require 'zlib'")
222253

223254
def add_elf(self, elf: bytes) -> None:
255+
if self.use_stdin:
256+
self.add("$stdin.binmode")
257+
self.add("e = $stdin.read")
258+
return
259+
224260
# prepare elf
225261
encoded = self.prep_elf(elf).decode("ascii")
226262

@@ -287,7 +323,9 @@ def patched_help_call(
287323
description="Print code to stdout to execute an ELF without dropping files."
288324
)
289325
parser.add_argument(
290-
"path", type=argparse.FileType("rb"), help="path to the ELF file"
326+
"path",
327+
type=str,
328+
help="path to the ELF file (use '-' to read from stdin at runtime)",
291329
)
292330
arch_or_syscall_group = parser.add_mutually_exclusive_group()
293331
arch_or_syscall_group.add_argument(
@@ -348,34 +386,49 @@ def patched_help_call(
348386
args = parser.parse_args()
349387

350388
argv = args.argv
351-
if not argv:
352-
# argv not specified, so let's just call it with the basename on the host
353-
argv = os.path.basename(args.path.name)
354-
355-
if args.interpreter_path and not args.with_command:
356-
print_err("note: '-p' flag meaningless without '-c'\n")
357389

358-
# read the elf
359-
elf = args.path.read()
360-
args.path.close()
390+
try:
391+
use_stdin = False
392+
if args.path == "-":
393+
use_stdin = True
394+
395+
if use_stdin:
396+
if not argv:
397+
return _err_stdin_flag("argv", "a")
398+
if not args.target_architecture or args.target_architecture == "autodetect":
399+
return _err_stdin_flag("arch", "t")
400+
401+
if not argv:
402+
# argv not specified, so let's just call it with the basename on the host
403+
argv = os.path.basename(args.path)
404+
405+
if args.interpreter_path and not args.with_command:
406+
print_err("note: '-p' flag meaningless without '-c'\n")
407+
408+
# read the elf
409+
if not use_stdin:
410+
with open(args.path, "rb") as elf_file:
411+
elf = elf_file.read()
412+
else:
413+
elf = None
361414

362-
code_generator = CodeGenerator()
415+
code_generator = CodeGenerator()
363416

364-
code_generator.compression_level = args.compression_level # defaults to 9
365-
code_generator.wrap = args.wrap # defaults to 0, no wrap
417+
code_generator.compression_level = args.compression_level # defaults to 9
418+
code_generator.wrap = args.wrap # defaults to 0, no wrap
419+
code_generator.use_stdin = use_stdin # defaults to False
366420

367-
if args.target_architecture == "autodetect":
368-
args.target_architecture = _get_e_machine(elf[:20])
421+
if args.target_architecture == "autodetect" and not use_stdin:
422+
args.target_architecture = _get_e_machine(elf[:20])
369423

370-
if args.target_architecture != "libc":
371-
# map to syscall number
372-
syscall = syscall_numbers.get(args.target_architecture)
373-
else:
374-
syscall = args.syscall # None if not specified
424+
if args.target_architecture != "libc":
425+
# map to syscall number
426+
syscall = syscall_numbers.get(args.target_architecture)
427+
else:
428+
syscall = args.syscall # None if not specified
375429

376-
code_generator.syscall = syscall
430+
code_generator.syscall = syscall
377431

378-
try:
379432
if args.language:
380433
code_generator.set_lang(args.language)
381434

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "fee"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
description = "Execute ELF files without dropping them on disk"
55
authors = ["Rasmus Moorats <[email protected]>"]
66
license = "GPLv3"

0 commit comments

Comments
 (0)