This project implements a comfortable and modern way to use the NTAPI functions using indirect syscalls, coupled with the FreshyCalls method with a little twist for dynamic syscall number retrieval. It also uses a technique that I haven't seen being metioned to bypass windows defender's memory scanning. It also implements a classic PoC process injector.
The usage is pretty straight forward, here is a snippet demonstrating the main functionality:
nullgate::syscalls syscalls;
auto status = syscalls.Call(nullgate::obfuscation::fnv1Const("NtAllocateVirtualMemory"),
processHandle, &buf, 0, ®ionSize,
MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
The fnv1Const
method brings the joys of modern C++ to the maldev world. It is a consteval
function, so it is guaranteed that it will get evaluated at compile time, replacing the readable function name with a fnv1 hash.
There is also a runtime equivalent called fnv1Runtime
but of course it doesn't add the benefit of having our function names obfuscated. It is used by the implementation to check which function inside of ntdll to get the syscall number of.
There are routines that can xor encrypt/decrypt(multibyte key) and base64 encode/decode your payload or some message:
if (!NT_SUCCESS(status))
throw std::runtime_error(
nullgate::obfuscation::xorDecode("BQkEI1c0dkJ4LU4naSJhGCcIFSNWej5YeD5DNmkzM"
"x8lAwI8H3o3VzEmTjdpNCgELlxR") +
std::to_string(status));
The key for now is FfqO3ZQ6XJ+SICAp
. A hasher is also provided, after building the project, the binary will be accessible at <build_dir>/_deps/nullgate-build/src/hasher
. Just pipe something into it and it will spit out a base64 encoded and xored string.
To ease the encryption of shellcode a special functon is provided:
auto decryptedShellcode =
nullgate::obfuscation::hex2bin(nullgate::obfuscation::xorDecode(encryptedShellcode))
The hex2bin
function just turns a hex string into a vector of bytes, thanks to this you can just pipe the shellcode from msfvenom with the -f hex
flag straight into the hasher and not have worry about the special characters.
CMake FetchContent is supported. Here is an example of a simple CMakeLists.txt:
cmake_minimum_required(VERSION 3.25)
include(FetchContent)
FetchContent_Declare(nullgate
GIT_REPOSITORY https://github.com/0xsch1zo/NullGate
GIT_TAG 1.0.0
)
FetchContent_MakeAvailable(nullgate)
project(test)
add_executable(test
main.cpp
)
target_link_libraries(test
PRIVATE nullgate
)
The linking is done statically so you don't have to worry about symbols being visible.
To build the sample use -DNULLGATE_BUILD_SAMPLE=ON
. It will be accessible at <build_dir>/_deps/nullgate-build/sample.exe
. It takes a PID that you want to inject shellcode into as an argument.
Warning
If you are using linux you need to have the mingw crosscompiler installed. On Arch for example you can do pacman -S mingw-w64-gcc
. Then use the -DNULLGATE_CROSSCOMPILE=ON
option to set mingw as the default compiler for the relevant parts of the program.
git clone https://github.com/0xsch1zo/NullGate
cd NullGate
cmake . -B build -DNULLGATE_BUILD_SAMPLE=ON
cmake --build build/
The core of the issue is that when we call NtCreateRemoteThreadEx
or NtCreateProcess
, a memory scan gets triggered and our signatured as hell msfvenom payload gets detected.
A known solution is to first when calling NtAllocateVirtualMemory
set the page permissions as PAGE_NOACCESS
, then create the thread in a suspended state.
When windows defender will scan the memory of our process it will fail to do that.
We can then resume the execution of our thread with NtResumeThread
.
This works, but what if a more competent security solution is being used? What would it do?
It would of course just use VirtualProtect
to change the permissions of our page and detect msfvenom.
To bypass that I changed the strategy a bit. Instead of setting the page as PAGE_NOACCESS
, during our first write to the memory of the process we can just put some junk data into the process(Yes it is required, or I'm just too stupid to find a way to get it working wihout this).
Then we create a thread in suspended state.
After that we write to the process our desired shellcode and finally we resume the thread using NtResumeThread
.
With this technique we don't have to worry about our memory being accessed after the call to NtCreateThreadEx
because there is nothing in there.
Only after the fact the decrypted shellcode is written and the execution is resumed.
- To @ElephantSe4l and @MarioBartolome for a great method of dynamic syscall number retrieval and generally the whole project from which I've taken great inspiration of of.
- To @cr-0w for the amazing blog post and video discussing direct and indirect syscalls.
- To bordergate for the article that describes the initial method of bypass.