Skip to content

Commit ebf145c

Browse files
authored
Add tcp bind transport (#2140)
* Add chain tcp eldritch function * Add bind tcp transport
1 parent 0acd949 commit ebf145c

File tree

53 files changed

+2317
-1928
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2317
-1928
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,13 @@ The `docs/_docs/` folder contains three guides:
9797

9898
## Eldritch Standard Library
9999

100-
The Eldritch DSL provides 12 modules:
100+
The Eldritch DSL provides 13 modules:
101101

102102
| Module | Purpose |
103103
|-----------|--------------------------------------|
104104
| `agent` | Agent metadata and control |
105105
| `assets` | Embedded file access |
106+
| `chain` | Multi-agent chaining |
106107
| `crypto` | Encryption, decryption, hashing |
107108
| `file` | File system operations |
108109
| `http` | HTTP/HTTPS requests |

docs/_docs/user-guide/eldritch.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,37 @@ The **assets.read** method returns a UTF-8 string representation of the asset fi
459459

460460
---
461461

462+
## Chain
463+
464+
The `chain` library enables multi-agent chaining by allowing one agent (Agent A) to proxy C2 traffic for another agent (Agent B). This is useful for establishing communication through intermediary agents in restricted networks.
465+
466+
### chain.tcp
467+
468+
`chain.tcp(addr: str) -> int`
469+
470+
The **chain.tcp** method establishes a chain proxy over TCP, allowing Agent A to forward C2 messages to/from Agent B. Agent A connects to Agent B's bind TCP transport listener at the specified address and proxies gRPC traffic over HTTP/2.
471+
472+
**Parameters:**
473+
- `addr`: The address and port where Agent B is listening for chain connections (e.g., `"192.168.1.100:8443"`)
474+
475+
**Returns:**
476+
- `0` on successful initialization (the proxy runs asynchronously in the background)
477+
478+
**Example:**
479+
480+
```python
481+
# Agent A connects to Agent B's bind TCP listener and starts proxying traffic
482+
chain.tcp("192.168.1.100:8443")
483+
484+
# Now Agent B's C2 messages flow through Agent A to Tavern
485+
```
486+
487+
**Usage Pattern:**
488+
489+
Agent A must have one of the standard transports (grpc, http1, dns) configured for its upstream connection to Tavern. Agent B is configured with a TCP bind transport to accept connections from Agent A on a TCP port.
490+
491+
---
492+
462493
## Crypto
463494

464495
The `crypto` library offers functionalities to encrypt, decrypt, and hash data. It includes support for algorithms like AES, MD5, SHA1, and SHA256, as well as helpers for base64 encoding and JSON parsing.

docs/_docs/user-guide/imix.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,37 @@ This transport doesn't support eldritch functions that require bi-directional st
211211

212212
*Note*: TXT records provide the best performance.
213213

214+
### tcp_bind
215+
216+
The TCP Bind transport inverts the traditional C2 communication model: instead of the agent connecting outbound to the server, the agent binds to a local TCP port and waits for an upstream agent (or redirector) to connect inward.
217+
218+
**Use Cases:**
219+
- Agent chaining: An upstream agent (Agent A) connects to a downstream agent's (Agent B) TCP bind port to proxy its C2 traffic
220+
- Network egress restrictions: When agents can't initiate outbound connections but can receive inbound connections
221+
- Multi-stage deployments: Establishing secure communication tunnels between agent stages
222+
223+
**Configuration:**
224+
225+
```yaml
226+
transports:
227+
- type: tcp_bind
228+
uri: tcp://0.0.0.0:8443 # Bind address and port
229+
```
230+
231+
**Parameters:**
232+
- `uri`: The local address and port to bind on (e.g., `tcp://0.0.0.0:8443`). Use `0.0.0.0` to listen on all interfaces, or specify a specific IP for local-only binding.
233+
234+
**Important Notes:**
235+
236+
- **Inverted Nature**: The agent binds and listens; upstream agents or redirectors must initiate the connection. This reverses the typical agent-to-server model.
237+
- **Secure Channel**: TCP Bind is treated as a trusted local channel. Messages are sent as plain protobuf over the TCP connection; encryption (ChaCha20) is applied by the upstream agent when forwarding to Tavern.
238+
- **Agent Chaining**: Use with `chain.tcp()` in Eldritch to have one agent proxy traffic for another. For example:
239+
- Agent B binds on `tcp://0.0.0.0:8443` with TCP Bind transport
240+
- Agent A uses Eldritch to call `chain.tcp("192.168.1.100:8443")` to connect to Agent B
241+
- Agent A proxies all of Agent B's C2 traffic upstream to Tavern
242+
- **Connection Persistence**: The TCP connection is maintained and reused across multiple C2 cycles. If the connection drops, a new upstream connection must be initiated.
243+
- **Not Suitable for Wide-Area Networks**: This transport is designed for local or trusted network chaining. For remote communication, use standard grpc, http1, or dns transports.
244+
214245
## Logging
215246

216247
At runtime, you may use the `IMIX_LOG` environment variable to control log levels and verbosity. See [these docs](https://docs.rs/pretty_env_logger/latest/pretty_env_logger/) for more information. **When building a release version of imix, logging is disabled** and is not included in the released binary.

implants/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ members = [
2626
"lib/eldritch/eldritch",
2727
"lib/eldritch/eldritch-wasm",
2828
"lib/portals/portal-stream", "lib/eldritch/testutils/eldritch-mockagent",
29+
"lib/eldritch/stdlib/eldritch-libchain",
2930
]
3031
exclude = [
3132
"lib/eldritch/stdlib/tests", # Excluded to prevent fake_bindings from polluting workspace builds
@@ -59,6 +60,11 @@ eldritch-libsys = {path = "lib/eldritch/stdlib/eldritch-libsys",default-features
5960
eldritch-libtime = {path = "lib/eldritch/stdlib/eldritch-libtime",default-features = false }
6061
portal-stream = { path = "lib/portals/portal-stream" }
6162

63+
http-body-util = "0.1"
64+
hyper-util = { version = "0.1", features = ["full"] }
65+
66+
eldritch-libchain = {path = "lib/eldritch/stdlib/eldritch-libchain",default-features = false }
67+
6268
aes = "0.8.3"
6369
allocative = "0.3.2"
6470
allocative_derive = "0.3.2"

implants/golem/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ pub struct ParsedTome {
3939
// Build a new runtime
4040
fn new_runtime(assetlib: impl ForeignValue + 'static) -> Interpreter {
4141
// Maybe change the printer here?
42-
let mut interp = Interpreter::new_with_printer(Arc::new(StdoutPrinter)).with_default_libs();
4342
// Register the libraries that we need. Basically the same as interp.with_task_context but
4443
// with our custom assets library
4544
let agent = Arc::new(AgentFake {});
45+
let mut interp = Interpreter::new_with_printer(Arc::new(StdoutPrinter)).with_default_libs();
4646
let task_context = TaskContext {
4747
task_id: 0,
4848
jwt: String::new(),

implants/imix/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ edition = "2024"
77
crate-type = ["cdylib"]
88

99
[features]
10-
default = ["install", "grpc", "http1", "dns", "doh"]
10+
default = ["install", "grpc", "http1", "dns", "doh", "tcp-bind"]
1111
grpc = ["transport/grpc"]
1212
http1 = ["transport/http1"]
1313
dns = ["transport/dns"]
1414
doh = ["transport/doh"]
15+
tcp-bind = ["transport/tcp-bind"]
1516
win_service = []
1617
install = []
1718
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
@@ -43,6 +44,7 @@ rust-embed = { workspace = true }
4344
console-subscriber = { workspace = true, optional = true }
4445
rand = { workspace = true }
4546
async-trait = { workspace = true }
47+
eldritch-libchain = { workspace = true, features = ["stdlib"] }
4648

4749
[target.'cfg(target_os = "windows")'.dependencies]
4850
windows-service = { workspace = true }

implants/imix/src/agent.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ pub struct ImixAgent {
3434
pub process_list_tx: std::sync::mpsc::SyncSender<c2::ReportProcessListRequest>,
3535
pub process_list_rx: Arc<Mutex<std::sync::mpsc::Receiver<c2::ReportProcessListRequest>>>,
3636
pub shell_manager_tx: tokio::sync::mpsc::Sender<ShellManagerMessage>,
37+
pub pending_forwards: Arc<
38+
tokio::sync::Mutex<
39+
Vec<(
40+
String,
41+
tokio::sync::mpsc::Receiver<Vec<u8>>,
42+
tokio::sync::mpsc::Sender<Vec<u8>>,
43+
)>,
44+
>,
45+
>,
3746
}
3847

3948
impl ImixAgent {
@@ -57,6 +66,7 @@ impl ImixAgent {
5766
process_list_tx,
5867
process_list_rx: Arc::new(Mutex::new(process_list_rx)),
5968
shell_manager_tx,
69+
pending_forwards: Arc::new(tokio::sync::Mutex::new(Vec::new())),
6070
}
6171
}
6272

@@ -326,6 +336,31 @@ impl ImixAgent {
326336
}
327337

328338
pub async fn process_job_request(&self) -> Result<()> {
339+
// Dispatch any pending forward_raw requests before checking in.
340+
// Each forward is spawned so the beacon cycle is not blocked by long-running
341+
// streaming calls (e.g. ReportFile).
342+
let pending: Vec<_> = {
343+
let mut forwards = self.pending_forwards.lock().await;
344+
forwards.drain(..).collect()
345+
};
346+
for (path, rx, tx) in pending {
347+
let agent = self.clone();
348+
self.runtime_handle.spawn(async move {
349+
if let Ok(mut t) = agent.get_usable_transport().await {
350+
if let Err(_e) = t.forward_raw(path.clone(), rx, tx).await {
351+
#[cfg(debug_assertions)]
352+
log::error!("Deferred forward_raw to {} failed: {}", path, _e);
353+
}
354+
} else {
355+
#[cfg(debug_assertions)]
356+
log::error!(
357+
"Failed to get transport for deferred forward_raw to {}",
358+
path
359+
);
360+
}
361+
});
362+
}
363+
329364
let resp = self.claim_tasks().await?;
330365

331366
let mut has_work = false;
@@ -415,6 +450,7 @@ impl ImixAgent {
415450
}
416451

417452
// Implement the Eldritch Agent Trait
453+
#[async_trait::async_trait]
418454
impl Agent for ImixAgent {
419455
fn fetch_asset(&self, req: c2::FetchAssetRequest) -> Result<Vec<u8>, String> {
420456
self.with_transport(|mut t| async move {
@@ -502,6 +538,17 @@ impl Agent for ImixAgent {
502538
self.with_transport(|mut t| async move { t.claim_tasks(req).await })
503539
}
504540

541+
async fn forward_raw(
542+
&self,
543+
path: String,
544+
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
545+
tx: tokio::sync::mpsc::Sender<Vec<u8>>,
546+
) -> Result<(), String> {
547+
let mut forwards = self.pending_forwards.lock().await;
548+
forwards.push((path, rx, tx));
549+
Ok(())
550+
}
551+
505552
fn get_config(&self) -> Result<BTreeMap<String, String>, String> {
506553
let mut map = BTreeMap::new();
507554
// Blocks on read, but it's fast

implants/imix/src/run.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,16 @@ async fn run_agent_cycle(agent: Arc<ImixAgent>, registry: Arc<TaskRegistry>) {
9696
// Create new active transport
9797
let config = agent.get_transport_config().await;
9898

99-
let transport = match transport::create_transport(config) {
100-
Ok(t) => t,
101-
Err(_e) => {
102-
#[cfg(debug_assertions)]
103-
log::error!("Failed to create transport: {_e:#}");
104-
agent.rotate_callback_uri().await;
105-
return;
106-
}
107-
};
99+
let transport: Box<dyn transport::Transport + Send + Sync> =
100+
match transport::create_transport(config) {
101+
Ok(t) => t,
102+
Err(_e) => {
103+
#[cfg(debug_assertions)]
104+
log::error!("Failed to create transport: {_e:#}");
105+
agent.rotate_callback_uri().await;
106+
return;
107+
}
108+
};
108109

109110
// Set transport
110111
agent.update_transport(transport).await;

implants/imix/src/shell/manager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ impl ShellManager {
261261
let mut interpreter = Interpreter::new_with_printer(printer)
262262
.with_default_libs()
263263
.with_context(
264-
agent.clone(),
264+
agent.clone() as std::sync::Arc<dyn eldritch_agent::Agent>,
265265
Context::ShellTask(shell_task_context),
266266
Vec::new(),
267267
backend,

implants/imix/src/shell/repl.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,12 @@ pub async fn run_repl_reverse_shell(
276276
let backend = Arc::new(EmptyAssets {});
277277
let mut interpreter = Interpreter::new_with_printer(printer)
278278
.with_default_libs()
279-
.with_context(Arc::new(agent), context.clone(), Vec::new(), backend); // Changed to with_context
279+
.with_context(
280+
Arc::new(agent.clone()) as Arc<dyn eldritch::agent::agent::Agent>,
281+
context.clone(),
282+
Vec::new(),
283+
backend,
284+
);
280285

281286
let mut repl = Repl::new();
282287
let stdout = VtWriter {

0 commit comments

Comments
 (0)