diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6407a2e..624c8c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,8 +7,24 @@ on: branches: [master] jobs: + detect-secrets: + name: Detect Secrets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Baseline check + run: | + pip install --quiet detect-secrets + detect-secrets scan --baseline .secrets.baseline + build: + name: Build (${{ matrix.configuration }}) runs-on: windows-latest + needs: detect-secrets + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] steps: - uses: actions/checkout@v4 @@ -22,12 +38,17 @@ jobs: run: dotnet restore EarlyWorm.sln - name: Build - run: dotnet build EarlyWorm.sln --no-restore --configuration Release + run: dotnet build EarlyWorm.sln --no-restore --configuration ${{ matrix.configuration }} test: - name: Test + name: Test (${{ matrix.configuration }}) runs-on: windows-latest needs: build + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] + steps: - uses: actions/checkout@v4 @@ -40,4 +61,4 @@ jobs: run: dotnet restore EarlyWorm.sln - name: Test - run: dotnet test "EarlyWorm.Tests/EarlyWorm.Tests.csproj" --no-restore --configuration Release --verbosity normal + run: dotnet test "EarlyWorm.Tests/EarlyWorm.Tests.csproj" --no-restore --configuration ${{ matrix.configuration }} --verbosity normal diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e546bc..06f6d7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,13 @@ repos: hooks: - id: gitleaks + # Baseline-tracked keyword/pattern scanning — catches secrets not caught by gitleaks + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ["--baseline", ".secrets.baseline"] + # C# formatting — enforces .editorconfig rules via dotnet format - repo: local hooks: diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..1567035 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,127 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": {}, + "generated_at": "2026-05-19T11:01:53Z" +} diff --git a/README.md b/README.md index 747966c..af360df 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,17 @@ EarlyWorm/ | [#8](https://github.com/incendiary/EarlyWorm/issues/8) | ✅ Closed | Enable branch protection and make repository public | | [#9](https://github.com/incendiary/EarlyWorm/issues/9) | ✅ Closed | Remove residual PDiddyVoke paths from git index | | [#10](https://github.com/incendiary/EarlyWorm/issues/10) | ✅ Closed | Consider integration test harness — manual smoke test checklist added to README | +| [#11](https://github.com/incendiary/EarlyWorm/issues/11) | ✅ Closed | Add xUnit tests for Win32 imports and DInvoke resolution | +| [#12](https://github.com/incendiary/EarlyWorm/issues/12) | ✅ Closed | Fix build: make Imports class public for test project access | +| [#13](https://github.com/incendiary/EarlyWorm/issues/13) | ✅ Done | detect-secrets baseline and pre-commit hook | +| [#14](https://github.com/incendiary/EarlyWorm/issues/14) | ✅ Done | CI matrix for Debug and Release configurations | +| [#15](https://github.com/incendiary/EarlyWorm/issues/15) | ✅ Done | [PInvoke vs DInvoke technique comparison](TECHNIQUES.md) | + +--- + +## Techniques + +See [TECHNIQUES.md](TECHNIQUES.md) for a comparison of PInvoke vs DInvoke injection approaches — IAT visibility, EDR hook bypass, and OPSEC tradeoffs. --- diff --git a/TECHNIQUES.md b/TECHNIQUES.md new file mode 100644 index 0000000..add6201 --- /dev/null +++ b/TECHNIQUES.md @@ -0,0 +1,60 @@ +# Injection Techniques: PInvoke vs DInvoke + +This project uses **DInvoke** for Win32 API resolution. This document explains the practical difference between PInvoke and DInvoke for offensive tooling and why DInvoke is preferred here. + +--- + +## PInvoke (Platform Invocation Services) + +PInvoke is the standard .NET mechanism for calling native Win32 functions: + +```csharp +[DllImport("kernel32.dll")] +public static extern IntPtr VirtualAllocEx(...); +``` + +**How it works:** The CLR resolves the function at load time and writes the DLL name + function name into the compiled assembly's Import Address Table (IAT). When the process starts, the OS loader populates the IAT with resolved addresses. + +**OPSEC concerns:** +- The IAT is statically visible in the PE header — `VirtualAllocEx`, `WriteProcessMemory`, `CreateRemoteThread` in the IAT are high-confidence injection indicators for EDRs and static scanners. +- Many EDRs instrument IAT entries at process load time by patching the resolved addresses to point to their own hooks. + +--- + +## DInvoke (Dynamic Invocation) + +DInvoke resolves function pointers at runtime without creating IAT entries: + +```csharp +var ptr = DInvoke.DynamicInvoke.Generic.GetLibraryAddress("kernel32.dll", "VirtualAllocEx"); +``` + +**How it works:** `GetLibraryAddress` walks the PEB's loaded module list to find the target DLL in memory, then parses its export directory to locate the function — no static IAT entry, no `LoadLibrary`/`GetProcAddress` call that EDRs commonly hook. + +**OPSEC advantages:** +- No IAT entries for sensitive APIs → harder for static analysis to flag. +- Bypasses some userland API hooks if the hook is applied at the IAT level rather than the function prologue. +- Function resolution happens at call-time, not load-time, reducing the static footprint. + +**Limitations:** +- Does not bypass inline hooks (prologue patching) on the underlying ntdll/kernel32 functions. +- Slightly more complex code; type-casting delegates requires care. +- Reflection-based approaches can still be detected behaviourally. + +--- + +## Summary + +| Aspect | PInvoke | DInvoke | +|--------|---------|---------| +| IAT visibility | ✗ Visible to static analysis | ✓ No IAT entry | +| EDR hook bypass | ✗ IAT hooks apply | ✓ Bypasses IAT-level hooks | +| Inline hook bypass | ✗ No | ✗ No | +| Code complexity | Low | Medium | +| Detection surface | Higher | Lower | + +**For authorized red team research**, DInvoke reduces the static artefact footprint and better reflects the techniques used by real-world adversaries, making it a more representative test of defensive coverage. + +--- + +*This document is for educational purposes. All use of this tool requires explicit written authorization from the target system owner.*