You're Gonna Carry That Weight

This commit is contained in:
sibouzitoun
2026-06-22 12:38:04 +01:00
commit 5e26626615
162 changed files with 10836 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Integration Tests
This directory documents the automated testing infrastructure for validating the SindriKit reflective loading pipeline.
## Table of Contents
- [test_runner.md](test_runner.md)
Documentation for the data-driven test runner, the specification matrix, and the test categories (functional, arch-mismatch, Heaven's Gate, Corkami fuzz).
- [pe_mutator.md](pe_mutator.md)
Documentation for the PE mutation engine, covering both benign edge-case mutations and breaking stressor mutations.
- [test_payloads.md](test_payloads.md)
Documentation for the test payload source files, detailing the purpose and validation logic of each fixture.
+67
View File
@@ -0,0 +1,67 @@
# PE Mutation Engine (`tests/loader/pe_mutator.py`)
**Location:** `tests/loader/pe_mutator.py`
The PE mutation engine applies targeted structural modifications to valid PE files to produce variants that stress-test the reflective loader's parser and loading pipeline. Every mutation is designed to exercise a specific safety boundary or parsing assumption.
## Mutation Categories
Mutations are divided into two strict categories:
### Benign Edge-Cases
These produce PE files that are unusual or hyper-optimized but structurally valid — Windows can still load and run them. The reflective loader **must** adapt and succeed.
| Mutation | Description | What It Tests |
|---|---|---|
| `unaligned_sections` | Forces `SectionAlignment` and `FileAlignment` to `0x200` | Loaders that hardcode `0x1000` page alignment assumptions |
| `strip_relocs_from_exe` | Strips the `.reloc` directory and sets `IMAGE_FILE_RELOCS_STRIPPED` | Loaders that aggressively demand relocations for EXEs |
| `garbage_dos_stub` | Overwrites the DOS stub (bytes 2`e_lfanew`) with deterministic garbage | Parsers that read linearly through the DOS stub instead of seeking via `e_lfanew` |
| `append_overlay` | Appends 4 KB of junk data after the last section | Loaders that map beyond declared section boundaries |
| `null_section_names` | Zeroes out every section name string | Loaders that match section names unsafely or log via unchecked `printf` |
### Breaking Stressors
These produce structurally corrupted PE files. The parser **must** detect the violation and reject the input gracefully — **never crash**.
| Mutation | Description | What It Tests |
|---|---|---|
| `corrupt_reloc_block_size` | Sets first reloc block's `SizeOfBlock` to `0xFFFFFFFF` | Unbounded `while` loops in relocation parsing |
| `massive_size_of_headers` | Sets `SizeOfHeaders` to `0xFFFFFFFF` | Unchecked `memcpy` using header-sourced sizes |
| `section_truncated_raw_data` | Inflates a section's `SizeOfRawData` beyond the physical file | Blind section mapping without file bounds checks |
| `section_integer_overflow` | Sets `VirtualSize` and `SizeOfRawData` to `0xFFFFFFFF` | Integer overflow in allocation arithmetic (`VA + ALIGN(VirtualSize)`) |
| `section_overlap_corruption` | Collapses all section file offsets to zero | File alignment normalization bugs |
| `corrupt_pe_signature` | Replaces `PE\0\0` with `0xDEADBEEF` | NT signature validation |
| `invalid_e_lfanew` | Points `e_lfanew` 1 MB past EOF | Offset bounds checking before NT headers access |
| `zero_size_of_image` | Sets `SizeOfImage` to 0 | Allocation size validation |
| `corrupt_import_rva` | Points import directory to `0xDEAD0000` | RVA bounds checking before import traversal |
| `corrupt_export_rva` | Points export directory to `0xDEAD0000` | RVA bounds checking before export parsing |
| `huge_sections_count` | Sets `NumberOfSections` to `0xFFFF` | Section table overflow protection |
| `unterminated_imports` | Overwrites the null-terminating import descriptor with `0xFF` | Import loop termination without boundary checks |
| `massive_export_count` | Sets `NumberOfFunctions` and `NumberOfNames` to `0xFFFFFFFF` | Export iteration bounds checking |
| `oob_entry_point` | Points `AddressOfEntryPoint` past `SizeOfImage` | Entry point RVA validation before execution transfer |
## API
### `Mutation` Dataclass
| Field | Type | Description |
|---|---|---|
| `name` | `str` | Short string identifier (used in output filenames) |
| `description` | `str` | Human-readable label |
| `apply` | `Callable` | The mutation function (`(src, dst) -> None`) |
| `expect_loadable` | `bool` | `True` = loader must succeed; `False` = loader must reject gracefully |
| `applies_to` | `str` | `"both"`, `"exe"`, or `"dll"` |
### `mutate_pe(src_path, mutation, output_dir) -> str`
Applies the specified mutation to the source PE file and writes the mutated output to `output_dir`. Returns the path to the generated file. Raises `MutationError` on failure.
## Dependency
Requires the `pefile` Python package for structured PE modifications:
```
pip install pefile
```
Raw byte-level mutations (`garbage_dos_stub`, `corrupt_pe_signature`, `invalid_e_lfanew`, `huge_sections_count`) operate directly on the byte buffer without `pefile`.
+69
View File
@@ -0,0 +1,69 @@
# Test Payloads (`tests/loader/src/`)
**Location:** `tests/loader/src/`
These C source files are compiled into DLL and EXE payloads that the test runner feeds to the reflective loaders. Each payload is designed to validate a specific capability of the loading pipeline from basic FFI argument passing to TLS callback execution.
All DLL payloads export functions that return sentinel values (`0xFEEDC0DE`, `0x1337C0DE`, etc.) to signal success or specific failure modes. The test runner validates these return values across architecture boundaries.
---
## `test_dll.c`
The primary DLL test payload. Exercises three critical loader capabilities:
### Exports
- **`SayHello(s1, s2, n3)`**: Validates that the FFI bridge correctly passes three arguments. Returns `0xFEEDC0DE` if the arguments exactly match `("bonjour", "hello", 12)`, otherwise `0xDEADBEEF`.
- **`VerifyInit()`**: Validates two things simultaneously:
1. **DllMain execution**: `DllMain(DLL_PROCESS_ATTACH)` writes `0xA77AC4ED` into a global variable. `VerifyInit` reads it back.
2. **Relocation correctness**: The global lives in `.data`. If relocations were not applied, the pointer reference will read garbage.
Returns `0xC001D00D` on success.
---
## `test_dll_advanced.c`
Exercises complex loader capabilities: multi-DLL imports, heap allocation, and dynamic loading.
### Exports
- **`AdvancedExport(input_str)`**: Calls `GetSystemInfo` (kernel32), allocates heap memory via `malloc`, copies the input string, dynamically loads `user32.dll` via `LoadLibraryA`, and resolves `MessageBoxA` via `GetProcAddress`. Returns `0x1337C0DE` if all operations succeed with input `"advanced_test"`.
- **`VerifyImports()`**: Stress-tests IAT resolution by calling a diverse set of kernel32 APIs (`GetCurrentProcessId`, `GetCurrentThreadId`, `VirtualAlloc/Free`, `GetSystemTimeAsFileTime`). Also verifies `DllMain` was called exactly once. Returns `0xCA11AB1E` on success.
---
## `test_dll_empty.c`
A completely empty DLL with no export or custom imports beyond what the compiler forces for `DllMain`. Tests that the loader gracefully handles a missing Export Directory without crashing. The test runner expects the loader to report `"Error: Requested export was not found"`.
---
## `test_dll_tls.c`
Registers a TLS callback via the `.CRT$XLB` section using `#pragma section` and `__declspec(allocate)`. The callback writes a sentinel (`0x7153CA11`) into a global variable during `DLL_PROCESS_ATTACH`.
### Exports
- **`VerifyTLS()`**: Reads the sentinel back. Returns `0x71500C01` if the TLS callback fired, `0x715FA110` if it did not.
This validates that the reflective loader correctly parses `IMAGE_TLS_DIRECTORY` and iterates the callback array before invoking `DllMain`.
---
## `test_exe.c`
A basic EXE payload that validates relocations and imports:
1. Checks that a `.data` global (`g_reloc_canary = 0xCAFEBABE`) survived relocation.
2. Calls `GetCurrentProcessId()` through the resolved IAT.
Exits with code `0x7A` (122) on success.
---
## `test_exe_advanced.c`
An advanced EXE payload that validates CRT initialization:
1. Performs `memcpy`, `strlen`, `strcmp` to verify the CRT was bootstrapped.
2. Calls `malloc` and `printf` to exercise heap and stdio.
3. Calls `GetCurrentProcessId` and `GetCurrentThreadId` for multi-API IAT verification.
Exits with code `0x1337` on success. The test runner matches `"Successfully allocated and printed"` in stdout.
+62
View File
@@ -0,0 +1,62 @@
# Test Runner (`tests/loader/test_runner.py`)
**Location:** `tests/loader/test_runner.py`
The test runner is a data-driven integration test harness that validates the reflective loading pipeline end-to-end. It auto-generates concrete test cases from compact specifications and executes them against both loader variants across both architectures.
## Usage
```
python tests/loader/test_runner.py [--corkami]
```
| Flag | Description |
|---|---|
| *(none)* | Runs the core functional test matrix |
| `--corkami` | Enables the Corkami PE fuzz corpus tests (requires extracting `tests/fixtures/pe/corkami_fixtures.zip`) |
## Architecture
### The Spec → TestCase Expansion
Test definitions are written as compact `Spec` objects that are loader- and architecture-agnostic. Each `Spec` declares:
| Field | Description |
|---|---|
| `loaders` | Tuple of loader variants to test against (e.g., `("nowinapi", "winapi")`) |
| `payload` | Test payload name (e.g., `"test_dll"`, `"test_exe_advanced"`) |
| `export` | Optional DLL export name to invoke via the FFI bridge |
| `args` | Arguments passed to the export function |
| `expect_stdout` | Expected substring in the process stdout |
| `expect_retval` | Expected FFI return value (auto-formatted per architecture pointer width) |
| `expect_rc` | Expected process exit code |
| `expect_fail` | If `True`, the test expects the loader to reject the payload gracefully |
At runtime, each `Spec` is expanded across all `(loader × architecture)` combinations. With 9 specs × 2 loaders × 2 architectures, the core matrix produces 36 test cases from a single declarative table.
### Test Categories
#### 1. Functional Tests (from SPECS)
Validates the end-to-end loading pipeline:
- **DLL loading with correct arguments** — verifies FFI bridge argument passing (`SayHello` with 3 args).
- **DLL loading with wrong arguments** — validates the payload's own input validation.
- **Missing export parameter** — tests the CLI error path.
- **Advanced DLL** — exercises `kernel32.dll` imports, heap allocations, dynamic `LoadLibraryA`, and multi-API IAT resolution.
- **Empty DLL** — verifies graceful handling of a DLL with no export directory.
- **DllMain + Relocations** — confirms `DLL_PROCESS_ATTACH` fired and global variable relocations were applied.
- **TLS callbacks** — confirms the loader parsed `IMAGE_TLS_DIRECTORY` and fired callbacks before `DllMain`.
- **EXE loading** — validates entry point execution and exit code capture.
- **Advanced EXE** — exercises CRT initialization, heap allocation, and `printf`.
#### 2. Architecture Mismatch Tests
Feeds a 32-bit payload to a 64-bit loader (and vice versa) to verify the compatibility guard rejects it with a clear error message.
#### 3. Heaven's Gate Tests
- Runs the 32-bit PoC and expects WoW64 detection success.
- Runs the 64-bit PoC and expects a non-WoW64 rejection message.
#### 4. Corkami Fuzz Tests (opt-in via `--corkami`)
Iterates every `.exe` in `tests/fixtures/pe/corkami/` (a public corpus of exotic, standards-pushing PE files) and feeds each to the 64-bit `loader_winapi`. These tests verify that the parser does not crash on any real-world edge-case PE file.
#### 5. PE Mutation Tests (via `pe_mutator.py`)
Generates structurally mutated PE variants at runtime and validates that the loader either successfully loads benign mutations or gracefully rejects breaking mutations without crashing.