Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
NullGate — 简化间接系统调用使用的库。作为 PoC 的 AV/EDR 绕过相当有趣。 | Kitploit
工具/GitHubGitHub/0xsch1zo/nullgate
加密/解密工具Shellcode后渗透利用红队Payload 开发对抗性攻击
GitHub0xsch1zo/nullgate

NullGate

简化间接系统调用使用的库。作为 PoC 的 AV/EDR 绕过相当有趣。

查看仓库
168191年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

NullGate

本项目实现了一种便捷且现代的方式来使用 NTAPI 函数,通过间接系统调用(indirect syscalls),并结合了 FreshyCalls 方法,在动态获取系统调用编号方面做了一点改动。 它还使用了一种我尚未见他人提及的技术来绕过 Windows Defender 的内存扫描。 该项目使用该库实现了一个经典的 PoC 进程注入器。 同时提供了一些便捷的加密函数。

演示

示例演示

构建

要构建示例,请使用 -DNULLGATE_BUILD_SAMPLE=ON。 如果你直接构建 nullgate,产物会位于 <build_dir>/sample.exe;如果是作为依赖构建,则在 <build_dir>/_deps/nullgate-build/sample.exe。 在 Windows 上,由于构建输出路径比较奇怪,它大概会位于样例所在的基础目录下,但嵌套层级可能会更深。 它接受一个你想注入 shellcode 的进程 PID 作为参数。

[!WARNING] 如果你使用的是 Linux,则需要安装 mingw 交叉编译器。例如在 Arch 上可以执行 pacman -S mingw-w64-gcc。然后使用 -DNULLGATE_CROSSCOMPILE=ON 选项将 mingw 设为默认编译器。

[!TIP] 还建议对生成的二进制文件进行 strip,以降低被检测的可能性。

root@kitploit:~
git clone https://github.com/0xsch1zo/NullGate
cd NullGate
cmake . -B build -DNULLGATE_BUILD_SAMPLE=ON
cmake --build build/

弃用的 hashser(1.1.3 及以下版本)

它可以通过 -DNULLGATE_DEPRECATED_HASHER 标志进行构建。

用法

将 nullgate 添加到你的项目

支持 CMake FetchContent。以下是一个简单 CMakeLists.txt 的示例:

root@kitploit:~
cmake_minimum_required(VERSION 3.25)

include(FetchContent)

FetchContent_Declare(nullgate
    GIT_REPOSITORY https://github.com/0xsch1zo/NullGate
    GIT_TAG 1.2.0 
)

FetchContent_MakeAvailable(nullgate)

project(test)

add_executable(test
    main.cpp
)

target_link_libraries(test
    PRIVATE nullgate
)

链接是静态完成的,因此你无需担心符号可见性问题。

[!NOTE] 以下示例将使用 namespace ng = nullgate

系统调用(Syscalls)

用法非常直接,下面是一段演示主要功能的代码片段:

root@kitploit:~
ng::syscalls syscalls;
typedef NTSTATUS NTAPI NtAllocateVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _Inout_ _At_(*BaseAddress,
                 _Readable_bytes_(*RegionSize) _Writable_bytes_(*RegionSize)
                     _Post_readable_byte_size_(*RegionSize)) PVOID *BaseAddress,
    _In_ ULONG_PTR ZeroBits, _Inout_ PSIZE_T RegionSize,
    _In_ ULONG AllocationType, _In_ ULONG PageProtection);

NTSTATUS status = syscalls.SCall<NtAllocateVirtualMemory>(
      ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"), processHandle,
      &buf, 0, &regionSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

内置了类型安全。 你只需提供想要调用的 NT 函数的定义即可!你可以轻松地从 ntdoc 获取这些定义。 这是使用该库的推荐方式。

之前的非弃用接口

对于不喜欢 C++ 模板黑魔法之类东西的人,之前的接口仍然可用:

root@kitploit:~
NTSTATUS status = syscalls.Call(ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"),
                         processHandle, (PVOID)&buf, (ULONG_PTR)0, &regionSize,
                         (ULONG)(MEM_RESERVE | MEM_COMMIT), (ULONG)PAGE_EXECUTE_READWRITE);

使用这个接口时,你必须将参数转换为正确的类型,否则可能会导致问题。

加密/哈希

对 NTAPI 调用进行哈希

root@kitploit:~
constexpr uint64_t hash = ng::obfuscation::fnv1Const("NtAllocateVirtualMemory");

前面演示的 fnv1Const 方法将现代 C++ 的乐趣带入了 maldev 世界。它是一个 consteval 函数,因此保证会在编译期求值,将可读的函数名替换为 fnv1 哈希。

还有一个运行时等价版本 fnv1Runtime,但当然它没有混淆函数名的好处。实现内部使用它来确定要获取 ntdll 中哪个函数的系统调用编号。

通用 XOR 加密

有三个用于 XOR“加密”的例程:

xorConst
root@kitploit:~
constexpr ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
std::cout << xored.string();

可能的输出:

root@kitploit:~
<Y:-E&X0

与 fnv1Const 函数类似,该例程在编译期求值,因此 "some string" 永远不会出现在二进制中,因为该字符串会以加密形式存储。 但嘿,我们得真正用上这些数据!ConstData 是一个围绕原始字节的轻量封装,其大小是恒定的。 它有两个方法:raw() 和 string()。 raw() 返回 std::vector<unsigned char>,而 string 顾名思义返回 std::string。

[!NOTE] 如果你需要用字符串字面量直接构造 ConstData,请使用 std::to_array 构造一个中间数组传给 ConstData。 但请注意,这种方式下 ConstData 会额外存储字面量的结尾空字符。

当然,我们需要一种解密并使用它的方法,这就是接下来要介绍的函数。

xorRuntime

xorRuntime 是可用的第二个例程。 顾名思义,它是 xorConst 的运行时等价版本。 请注意,它不仅可用于解密,而是双向可用;不过用它加密并不能获得在编译期混淆字符串的好处。

root@kitploit:~
ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
ng::obfuscation::ConstData data = ng::obfuscation::xorRuntime(xored);
assert(data.string() == "some string");

std::string dynamicString = "some data of dynamic size";
auto dynamicData = ng::obfuscation::DynamicData(dynamicString);
auto xoredDynamic = ng::obfuscation::xorRuntime(dynamicData);
auto unxoredDynamic = ng::obfuscation::xorRuntime(xoredDynamic);
assert(unxoredDynamic.string() == dynamicString);

DynamicData 拥有与 ConstData 相同的方法,并额外增加了一些用于互操作的构造函数;顾名思义,它的大小可以在编译期未知。 xorRuntime 同时接受 DynamicData 和 ConstData。

xorRuntimeDecrypted

虽然我们总是可以先调用 xorConst,再把结果传给 xorRuntime 在运行时解密,但那有点繁琐。 这个问题的解决方案来了,锦上添花的 xorRuntimeDecrypted 就是一个方便的函数,适用于那些不需要在加密状态下操作的字符串字面量。 它在编译期加密字符串字面量,然后在运行时解密,一次调用全部搞定。是不是很酷!。

root@kitploit:~
ng::obfuscation::ConstData text = ng::obfuscation::xorRuntimeDecrypted<"some string">();
assert(data.string() == "some string");
密钥

现在可能有人会说,一切都很好,但密钥在哪里?在 nullgate 1.2 中,每次全新构建(即每次运行 cmake 命令)时都会随机生成密钥! 这进一步降低了被特征化检测(signatured)的几率。

绕过 Windows Defender 内存扫描

问题的核心在于,当我们调用 NtCreateRemoteThreadEx 或 NtCreateProcess 时,会触发内存扫描,然后我们那个特征明显到爆的 msfvenom 载荷就会被检测出来。

如何绕过?

一个已知的解决方案是:在调用 NtAllocateVirtualMemory 时先将页面权限设置为 PAGE_NOACCESS,然后以挂起状态创建线程。 当 Windows Defender 扫描我们进程的内存时,它将无法扫描成功。 然后我们可以用 NtResumeThread 恢复线程执行。 这种方法有效,但如果遇到更强大的安全解决方案呢?它会怎么做? 它当然会直接用 VirtualProtect 修改我们页面的权限,然后检测到 msfvenom。 为了绕过这一点,我稍微改变了策略。我们不是将页面设置为 PAGE_NOACCESS,而是在第一次向进程内存写入时,仅将一些垃圾数据放入进程(是的,这是必需的,或者只是我太笨,找不到不用这一步就能让它工作的方法)。 然后我们创建一个挂起状态的线程。 之后,我们向进程写入所需的 shellcode,最后使用 NtResumeThread 恢复线程。 使用这一技术,我们不必担心调用 NtCreateThreadEx 之后内存被访问,因为那里什么也没有。 只有在事后,解密后的 shellcode 被写入,执行才恢复。

致谢:

  • 感谢 @ElephantSe4l 和 @MarioBartolome 提供了一种出色的动态获取系统调用编号的方法,以及整个项目,我从其中获得了极大灵感。
  • 感谢 @cr-0w 撰写的精彩博文和视频,其中讨论了直接与间接系统调用。
  • 感谢 bordergate 撰写的文章,该文描述了最初的绕过方法。

免责声明

此库仅用于学术目的。作者不对用于此库的任何内容负责,因此我们免于承担因滥用此库而产生的任何责任。

下载工具