diff --git a/EDR_telem_linux.json b/EDR_telem_linux.json index 0cf6163..d20f292 100644 --- a/EDR_telem_linux.json +++ b/EDR_telem_linux.json @@ -417,6 +417,25 @@ "Sysmon":"No", "Uptycs":"Via EnablingTelemetry" }, + { + "Telemetry Feature Category":"Anonymous File Activity", + "Sub-Category":"Memory-Backed File Creation", + "Auditd":"Pending Response", + "BitDefender":"Pending Response", + "Cortex XDR":"Pending Response", + "Carbon Black Cloud":"Pending Response", + "C-Prot":"Pending Response", + "CrowdStrike":"Pending Response", + "ESET Inspect":"Pending Response", + "Elastic":"Yes", + "Harfanglab":"Pending Response", + "LimaCharlie":"Pending Response", + "MDE":"Pending Response", + "Qualys":"Pending Response", + "SentinelOne":"Pending Response", + "Sysmon":"Pending Response", + "Uptycs":"Pending Response" + }, { "Telemetry Feature Category":"Service Activity", "Sub-Category":"Service Creation", @@ -568,5 +587,62 @@ "SentinelOne":"No", "Sysmon":"Yes", "Uptycs":"Yes" + }, + { + "Telemetry Feature Category":"File Metadata", + "Sub-Category":"File Entropy", + "Auditd":"Pending Response", + "BitDefender":"Pending Response", + "Cortex XDR":"Pending Response", + "Carbon Black Cloud":"Pending Response", + "C-Prot":"Pending Response", + "CrowdStrike":"Pending Response", + "ESET Inspect":"Pending Response", + "Elastic":"Via EnablingTelemetry", + "Harfanglab":"Pending Response", + "LimaCharlie":"Pending Response", + "MDE":"Pending Response", + "Qualys":"Pending Response", + "SentinelOne":"Pending Response", + "Sysmon":"Pending Response", + "Uptycs":"Pending Response" + }, + { + "Telemetry Feature Category":null, + "Sub-Category":"File Header Bytes", + "Auditd":"Pending Response", + "BitDefender":"Pending Response", + "Cortex XDR":"Pending Response", + "Carbon Black Cloud":"Pending Response", + "C-Prot":"Pending Response", + "CrowdStrike":"Pending Response", + "ESET Inspect":"Pending Response", + "Elastic":"Via EnablingTelemetry", + "Harfanglab":"Pending Response", + "LimaCharlie":"Pending Response", + "MDE":"Pending Response", + "Qualys":"Pending Response", + "SentinelOne":"Pending Response", + "Sysmon":"Pending Response", + "Uptycs":"Pending Response" + }, + { + "Telemetry Feature Category":null, + "Sub-Category":"File Size", + "Auditd":"Pending Response", + "BitDefender":"Pending Response", + "Cortex XDR":"Pending Response", + "Carbon Black Cloud":"Pending Response", + "C-Prot":"Pending Response", + "CrowdStrike":"Pending Response", + "ESET Inspect":"Pending Response", + "Elastic":"Yes", + "Harfanglab":"Pending Response", + "LimaCharlie":"Pending Response", + "MDE":"Pending Response", + "Qualys":"Pending Response", + "SentinelOne":"Pending Response", + "Sysmon":"Pending Response", + "Uptycs":"Pending Response" } ] diff --git a/Tools/Telemetry-Generator/Linux/LINUX_TELEMETRY_GENERATOR_GUIDE.md b/Tools/Telemetry-Generator/Linux/LINUX_TELEMETRY_GENERATOR_GUIDE.md index 07a8c10..ff242fc 100644 --- a/Tools/Telemetry-Generator/Linux/LINUX_TELEMETRY_GENERATOR_GUIDE.md +++ b/Tools/Telemetry-Generator/Linux/LINUX_TELEMETRY_GENERATOR_GUIDE.md @@ -24,6 +24,8 @@ The script includes the following functionalities: 11. **User Account Events**: Create, modify, and delete user accounts using libuser 12. **eBPF Events**: Utilize pamspy for credential dumping using eBPF 13. **File Operations**: Create, modify, and delete files. +14. **Memory-Backed File Creation**: Exercise `memfd_create()` with a representative set of flag combinations and stage an in-memory ELF for execution from `/proc/self/fd/`. +15. **File Metadata**: Drop a small set of files with known magic-byte headers and varying entropy (random, repeating ASCII, ELF/ZIP/XZ/GZIP/PDF stubs) and then modify/rename them, so EDRs that collect `file.Ext.entropy` and `file.Ext.header_bytes` have deterministic input. ## Usage @@ -62,6 +64,8 @@ This command will run the `FileCreated`, `DnsQuery`, and `NetworkConnect` events - `NetworkListen` - `NetworkRawSocket` - `eBPFProgram` +- `MemfdCreate` +- `FileMetadata` ## Disclaimers diff --git a/Tools/Telemetry-Generator/Linux/complex/file_metadata.py b/Tools/Telemetry-Generator/Linux/complex/file_metadata.py new file mode 100644 index 0000000..39acbad --- /dev/null +++ b/Tools/Telemetry-Generator/Linux/complex/file_metadata.py @@ -0,0 +1,80 @@ +import os +import secrets +import tempfile +from typing import Optional + +# Sample magic-byte headers so EDRs can populate file.Ext.header_bytes / +# file.Ext.entropy with deterministic, recognisable inputs. +_FIXTURES = [ + # (file_name, header_bytes, body_bytes, expected_extension) + ("telemgen_high_entropy.bin", b"", None, "bin"), # random body, no header + ("telemgen_low_entropy.txt", b"", b"A" * 8192, "txt"), # repeating ASCII + ("telemgen_elf_stub.elf", b"\x7fELF\x02\x01\x01\x00", None, "elf"), + ("telemgen_zip_stub.zip", b"PK\x03\x04", None, "zip"), + ("telemgen_xz_stub.xz", b"\xfd7zXZ\x00", None, "xz"), + ("telemgen_gzip_stub.gz", b"\x1f\x8b\x08", None, "gz"), + ("telemgen_pdf_stub.pdf", b"%PDF-1.7\n", None, "pdf"), +] + + +def _write_fixture(out_dir: str, name: str, header: bytes, body: Optional[bytes]) -> str: + path = os.path.join(out_dir, name) + with open(path, "wb") as fh: + if header: + fh.write(header) + if body is None: + # Pad with random bytes so EDR-side entropy calculation has signal + fh.write(secrets.token_bytes(4096)) + else: + fh.write(body) + return path + + +def _modify_fixture(path: str) -> None: + """Append data to the file to trigger a file.modification event with a + fresh entropy/header sample on EDRs that recompute on every change.""" + with open(path, "ab") as fh: + fh.write(b"\n# telemgen modification\n") + fh.write(secrets.token_bytes(512)) + + +def _rename_fixture(path: str) -> str: + """Rename to a path with a different extension so EDRs that key entropy + collection on extension still emit the field.""" + base, ext = os.path.splitext(path) + new_path = f"{base}.renamed{ext}" + os.rename(path, new_path) + return new_path + + +def run_file_metadata(): + """Entry point invoked by lnx_telem_gen.py. + + Generates a small set of files with known headers and varying entropy + so EDRs that collect file.Ext.header_bytes and file.Ext.entropy on file + creation/modification events have something to populate. + """ + out_dir = tempfile.mkdtemp(prefix="telemgen-file-metadata-") + print(f"[+] Writing file-metadata fixtures into {out_dir}") + written = [] + for name, header, body, _ in _FIXTURES: + try: + path = _write_fixture(out_dir, name, header, body) + print(f"[+] created {path} (header={header!r}, size={os.path.getsize(path)})") + written.append(path) + except Exception as exc: + print(f"[!] failed to create {name}: {exc}") + + for path in written: + try: + _modify_fixture(path) + new_path = _rename_fixture(path) + print(f"[+] modified+renamed {path} -> {new_path}") + except Exception as exc: + print(f"[!] post-creation activity failed for {path}: {exc}") + + return f"file metadata fixtures written under {out_dir}" + + +if __name__ == "__main__": + run_file_metadata() diff --git a/Tools/Telemetry-Generator/Linux/complex/memfd_create_exec.py b/Tools/Telemetry-Generator/Linux/complex/memfd_create_exec.py new file mode 100644 index 0000000..231a289 --- /dev/null +++ b/Tools/Telemetry-Generator/Linux/complex/memfd_create_exec.py @@ -0,0 +1,133 @@ +import ctypes +import ctypes.util +import os +import shutil +import time + +# memfd_create flags from / +MFD_CLOEXEC = 0x0001 +MFD_ALLOW_SEALING = 0x0002 +MFD_HUGETLB = 0x0004 +MFD_NOEXEC_SEAL = 0x0008 +MFD_EXEC = 0x0010 + + +def _libc(): + libc_path = ctypes.util.find_library("c") or "libc.so.6" + libc = ctypes.CDLL(libc_path, use_errno=True) + # int memfd_create(const char *name, unsigned int flags); + libc.memfd_create.argtypes = [ctypes.c_char_p, ctypes.c_uint] + libc.memfd_create.restype = ctypes.c_int + return libc + + +def _memfd_create(libc, name: bytes, flags: int) -> int: + fd = libc.memfd_create(name, flags) + if fd < 0: + errno = ctypes.get_errno() + raise OSError(errno, os.strerror(errno), name.decode("utf-8", "replace")) + return fd + + +def _flags_to_str(flags: int) -> str: + parts = [] + for value, label in ( + (MFD_CLOEXEC, "MFD_CLOEXEC"), + (MFD_ALLOW_SEALING, "MFD_ALLOW_SEALING"), + (MFD_HUGETLB, "MFD_HUGETLB"), + (MFD_NOEXEC_SEAL, "MFD_NOEXEC_SEAL"), + (MFD_EXEC, "MFD_EXEC"), + ): + if flags & value: + parts.append(label) + return "|".join(parts) if parts else "0" + + +def _create_variants(libc): + """Exercise memfd_create with several flag combinations to surface + process.Ext.memfd.flag.* fields in EDR telemetry.""" + variants = [ + (b"telemgen-payload-default", 0), + (b"telemgen-payload-cloexec", MFD_CLOEXEC), + (b"telemgen-payload-sealing", MFD_CLOEXEC | MFD_ALLOW_SEALING), + (b"telemgen-payload-noexec", MFD_CLOEXEC | MFD_NOEXEC_SEAL), + (b"telemgen-payload-exec", MFD_CLOEXEC | MFD_EXEC), + ] + fds = [] + for name, flags in variants: + try: + fd = _memfd_create(libc, name, flags) + except OSError as e: + print(f"[!] memfd_create({name!r}, {_flags_to_str(flags)}) failed: {e}") + continue + print(f"[+] memfd_create({name.decode()}, {_flags_to_str(flags)}) -> fd={fd}") + fds.append((fd, name.decode())) + return fds + + +def _copy_into_fd(src_path: str, dst_fd: int) -> None: + with open(src_path, "rb") as src: + while True: + chunk = src.read(64 * 1024) + if not chunk: + break + os.write(dst_fd, chunk) + + +def _exec_from_memfd(libc): + """Stage /bin/true (or /bin/sh) into an exec-flagged memfd and exec it + from /proc/self/fd/. Confirms memfd-backed execution telemetry.""" + src = shutil.which("true") or "/bin/true" + if not os.path.isfile(src): + print(f"[!] Skipping memfd-backed exec: {src} not present") + return + + name = b"telemgen-loader" + flags = MFD_CLOEXEC | MFD_EXEC + try: + fd = _memfd_create(libc, name, flags) + except OSError as e: + # Older kernels (pre-6.3) reject MFD_EXEC. Fall back to plain CLOEXEC. + try: + fd = _memfd_create(libc, name, MFD_CLOEXEC) + except OSError as e2: + print(f"[!] memfd_create for exec failed: {e2}") + return + + try: + _copy_into_fd(src, fd) + proc_path = f"/proc/self/fd/{fd}" + print(f"[+] Forking child to execve {proc_path} (memfd-backed exec of {src})") + pid = os.fork() + if pid == 0: + try: + os.execv(proc_path, ["telemgen-loader"]) + except Exception as e: + print(f"[child] execv failed: {e}") + os._exit(127) + else: + _, status = os.waitpid(pid, 0) + print(f"[+] memfd-backed child exited with status {status}") + finally: + try: + os.close(fd) + except OSError: + pass + + +def run_memfd_create(): + """Entry point invoked by lnx_telem_gen.py.""" + libc = _libc() + fds = _create_variants(libc) + time.sleep(0.5) + _exec_from_memfd(libc) + for fd, _ in fds: + try: + os.close(fd) + except OSError: + pass + return "memfd_create variants emitted" + + +if __name__ == "__main__": + run_memfd_create() diff --git a/Tools/Telemetry-Generator/Linux/lnx_telem_gen.py b/Tools/Telemetry-Generator/Linux/lnx_telem_gen.py index 63222eb..3257699 100644 --- a/Tools/Telemetry-Generator/Linux/lnx_telem_gen.py +++ b/Tools/Telemetry-Generator/Linux/lnx_telem_gen.py @@ -15,6 +15,8 @@ from complex.scheduled_task import run_task from complex.process_hijack_demo import start_hijacking from complex.eBPF_exec import run_pamspy +from complex.memfd_create_exec import run_memfd_create +from complex.file_metadata import run_file_metadata from prettytable import PrettyTable scheduler = sched.scheduler(time.time, time.sleep) @@ -351,7 +353,9 @@ def raw_access_read(): 'NetworkListen': NetworkSocketManager.network_listen, 'NetworkRawSocket': NetworkSocketManager.network_raw_socket, 'eBPFProgram': run_pamspy, - 'ProcessAccess': start_hijacking + 'ProcessAccess': start_hijacking, + 'MemfdCreate': run_memfd_create, + 'FileMetadata': run_file_metadata } def log_to_csv(function_name, output, error=None): diff --git a/Tools/compare.py b/Tools/compare.py index b9cc40c..2135ebf 100644 --- a/Tools/compare.py +++ b/Tools/compare.py @@ -94,6 +94,7 @@ "Raw Access Read": 1, "Process Access": 1, "Process Tampering": 1, + "Memory-Backed File Creation": 1, "Service Creation": 1, "Service Modification": 0.7, "Service Deletion": 0.6, @@ -101,7 +102,10 @@ "Agent Stop": 0.8, "MD5": 1, "SHA": 1, - "Fuzzy Hash": 1 + "Fuzzy Hash": 1, + "File Entropy": 0.5, + "File Header Bytes": 0.5, + "File Size": 0.2 } # macOS-specific categories diff --git a/partially_value_explanations_linux.json b/partially_value_explanations_linux.json index 9427d44..89e4991 100644 --- a/partially_value_explanations_linux.json +++ b/partially_value_explanations_linux.json @@ -329,6 +329,21 @@ "Auditd":"", "Carbon Black Cloud":"" }, + { + "Telemetry Feature Category":"Anonymous File Activity", + "Sub-Category":"Memory-Backed File Creation", + "SentinelOne (Complete)":"", + "Qualys":"", + "Uptycs":"", + "CrowdStrike":"", + "C-Prot":"", + "Sysmon":"", + "LimaCharlie":"", + "MDE":"", + "Elastic":"", + "Auditd":"", + "Carbon Black Cloud":"" + }, { "Telemetry Feature Category":"Service Activity", "Sub-Category":"Service Creation", @@ -445,5 +460,50 @@ "Elastic":"", "Auditd":"", "Carbon Black Cloud":"" + }, + { + "Telemetry Feature Category":"File Metadata", + "Sub-Category":"File Entropy", + "SentinelOne (Complete)":"", + "Qualys":"", + "Uptycs":"", + "CrowdStrike":"", + "C-Prot":"", + "Sysmon":"", + "LimaCharlie":"", + "MDE":"", + "Elastic":"", + "Auditd":"", + "Carbon Black Cloud":"" + }, + { + "Telemetry Feature Category":"", + "Sub-Category":"File Header Bytes", + "SentinelOne (Complete)":"", + "Qualys":"", + "Uptycs":"", + "CrowdStrike":"", + "C-Prot":"", + "Sysmon":"", + "LimaCharlie":"", + "MDE":"", + "Elastic":"", + "Auditd":"", + "Carbon Black Cloud":"" + }, + { + "Telemetry Feature Category":"", + "Sub-Category":"File Size", + "SentinelOne (Complete)":"", + "Qualys":"", + "Uptycs":"", + "CrowdStrike":"", + "C-Prot":"", + "Sysmon":"", + "LimaCharlie":"", + "MDE":"", + "Elastic":"", + "Auditd":"", + "Carbon Black Cloud":"" } ] \ No newline at end of file