Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
NullGate — 간접 syscall 사용을 용이하게 하는 라이브러리. PoC로서 상당히 흥미로운 AV/EDR 우회 기법. | Kitploit
도구/GitHubGitHub/0xsch1zo/nullgate
Encryption/Decryption ToolsShellcodePost-ExploitationRed TeamingPayload DevelopmentAdversarial Attack
GitHub0xsch1zo/nullgate

NullGate

간접 syscall 사용을 용이하게 하는 라이브러리. PoC로서 상당히 흥미로운 AV/EDR 우회 기법.

저장소 보기
168191년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

NullGate

이 프로젝트는 동적 syscall 번호 검색을 위한 약간의 변형을 가미한 FreshyCalls 방식과 함께 간접 syscall을 사용하여 NTAPI 함수를 편리하고 현대적인 방식으로 사용할 수 있게 해줍니다. 또한 Windows Defender의 메모리 검색을 우회하기 위해 언급된 적 없는 기술을 사용합니다. 이 프로젝트는 라이브러리를 사용하는 고전적인 PoC 프로세스 인젝터를 구현합니다. 암호화를 위한 편리한 함수도 제공됩니다.

데모

샘플 데모

빌드

샘플을 빌드하려면 -DNULLGATE_BUILD_SAMPLE=ON을 사용하세요. nullgate를 직접 빌드했다면 <build_dir>/sample.exe에서, 의존성으로 빌드했다면 <build_dir>/_deps/nullgate-build/sample.exe에서 접근할 수 있습니다. Windows에서는 빌드 대상 위치가 이상하기 때문에, 샘플 위치와 동일한 기본 디렉토리에 있겠지만 아마도 훨씬 더 중첩되어 있을 것입니다. 셸코드를 주입하려는 PID를 인자로 받습니다.

[!WARNING] Linux를 사용하고 있다면 mingw 크로스 컴파일러가 설치되어 있어야 합니다. 예를 들어 Arch에서는 pacman -S mingw-w64-gcc를 실행하면 됩니다. 그런 다음 -DNULLGATE_CROSSCOMPILE=ON 옵션을 사용하여 mingw를 기본 컴파일러로 설정하세요.

[!TIP] 또한 탐지 가능성을 줄이기 위해 결과 바이너리를 스트립하는 것이 좋습니다.

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를 사용합니다.

시스템 콜

사용법은 매우 간단합니다. 주요 기능을 보여주는 코드 조각은 다음과 같습니다:

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 안의 어떤 함수에서 syscall 번호를 가져올지 확인하는 데 사용합니다.

일반적인 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 명령이 실행될 때마다)마다 무작위로 생성됩니다! 이로 인해 시그니처가 탐지될 가능성이 훨씬 더 줄어듭니다.

Windows Defender 메모리 검사 우회

핵심 문제는 NtCreateRemoteThreadEx 또는 NtCreateProcess를 호출할 때 메모리 검사가 트리거되고, 그로 인해 시그니처가 엄청나게 박힌 msfvenom 페이로드가 탐지된다는 것입니다.

이를 어떻게 우회할까요?

알려진 해결책은 먼저 NtAllocateVirtualMemory를 호출할 때 페이지 권한을 PAGE_NOACCESS로 설정한 다음, 스레드를 일시 중단된 상태로 생성하는 것입니다. Windows Defender가 우리 프로세스의 메모리를 검사하려고 할 때 그렇게 할 수 없게 됩니다. 그런 다음 NtResumeThread로 스레드 실행을 재개할 수 있습니다. 이 방법은 작동하지만, 더 유능한 보안 솔루션이 사용되고 있다면 어떻게 될까요? 물론 VirtualProtect를 사용하여 페이지 권한을 변경한 다음 msfvenom을 탐지할 것입니다. 이를 우회하기 위해 전략을 약간 바꿨습니다. 페이지를 PAGE_NOACCESS로 설정하는 대신, 프로세스 메모리에 첫 번째 쓰기를 할 때 그냥 정크 데이터를 넣을 수 있습니다(네, 필수입니다. 아니면 제가 이것 없이 작동하게 만들 방법을 찾지 못한 것일 수도 있습니다). 그런 다음 스레드를 일시 중단 상태로 만듭니다. 그 후 프로세스에 원하는 셸코드를 쓰고 마지막으로 NtResumeThread를 사용하여 스레드를 재개합니다. 이 기법을 사용하면 NtCreateThreadEx 호출 이후 메모리에 아무것도 없기 때문에 메모리가 접근될 걱정을 할 필요가 없습니다. 그 후에야 복호화된 셸코드가 쓰이고 실행이 재개됩니다.

감사의 말

  • 동적 syscall 번호 검색의 훌륭한 방법과 제가 큰 영감을 받은 전체 프로젝트를 제공한 @ElephantSe4l과 @MarioBartolome에게 감사드립니다.
  • 직접 및 간접 syscall을 다룬 멋진 블로그 게시물과 동영상을 작성해 주신 @cr-0w에게 감사드립니다.
  • 우회의 초기 방법을 설명한 기사를 제공한 bordergate에게 감사드립니다.

면책 조항

이 라이브러리는 학술 목적으로만 제작되었습니다. 저자는 이 라이브러리에 제공되는 내용에 대해 책임을 지지 않으며, 따라서 이 라이브러리의 오용으로 인해 발생하는 모든 책임에서 면제됩니다.

도구 다운로드