Skip to content

Commit 60f0a1c

Browse files
committed
Crypto post
1 parent cc62423 commit 60f0a1c

2 files changed

Lines changed: 121 additions & 1 deletion

File tree

_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ share-links-active:
9898

9999
# How to display the link to your website in the footer
100100
# Remove this if you don't want a link in the footer
101-
url-pretty: "spellshift.github.io/dev-blog"
101+
url-pretty: "blog.realm.pub"
102102

103103
# Add the website title to the title of every page
104104
title-on-all-pages: true
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
layout: post
3+
title: Application Layer Crypto
4+
subtitle: Trust no-one hack everything!
5+
gh-repo: spellshift/realm
6+
gh-badge: [star, fork, follow]
7+
tags: [tavern, imix]
8+
comments: true
9+
mathjax: true
10+
author: Hulto
11+
---
12+
13+
## Why not just use TLS?
14+
15+
While TLS provides strong encryption for data in transit, relying solely on TLS for C2 communications introduces two key security concerns:
16+
17+
C2 traffic contains highly sensitive information that requires additional protection beyond standard transport encryption, including agent tasking and responses that may contain privileged credentials such as Domain Admin credentials, API keys, and session tokens
18+
19+
Many enterprise environments implement TLS inspection at network boundaries, where corporate proxies decrypt, inspect, and re-encrypt all HTTPS traffic. This breaks the end-to-end encryption model—your traffic is only encrypted to the inspection point, not to your actual C2 server. Network defenders can observe your C2 communications in plaintext, and third-party security vendors processing this traffic may log or analyze your operations indefinitely
20+
21+
## Security Requirements
22+
23+
Any application-layer encryption needs to maintain confidentiality and integrity even in adverse situations to do this we need to ensure:
24+
- Forward secrecy
25+
- Capturing an agent doesn't jeapordize sent data
26+
- Even with a full network capture data is protected
27+
28+
**Forward Secrecy** guarantees that compromise of long-term agent/server keys does not compromise past sessions. Each session uses ephemeral keys that are destroyed after use, implementing perfect forward secrecy to ensure that traffic captured now cannot be decrypted later even with full agent/server compromise.
29+
30+
**Capturing an agent doesn't jeapordize sent data** - We have to assume that defenders possess the agent binary and can perform static and dynamic analysis. The design ensures that possession of the binary does not enable traffic decryption, static analysis reveals no useful key material, and embedded cryptographic material is limited to public keys only. Even keys in memory are expired so that limited messages can be decrypted given a full memory dump.
31+
32+
**Even with a full network capture data is protected** - Ensure that network monitoring and full packet capture yield no useful plaintext. Historical traffic must remain encrypted even if current session keys are compromised, and no metadata or patterns should leak operational details that could aid an attacker.
33+
34+
35+
## Implementation
36+
37+
Our implementation combines modern cryptographic primitives to meet these security requirements. At its core is XChaCha20-Poly1305, an authenticated encryption algorithm that provides both confidentiality through stream encryption and integrity through AEAD (Authenticated Encryption with Associated Data). This means every message is encrypted and authenticated to prevent tampering.
38+
39+
The key exchange uses Ephemeral Diffie-Hellman, where the client generates a temporary keypairs that get destroyed when they're no longer needed. The agent embeds the server's long-term public key at compile time for trust establishment, while client keys stay ephemeral and regenerate for each session. This design ensures that capturing an agent binary only reveals the server's public key—nothing more. The key derivation follows NIST SP 800-56A Rev. 3, specifically section 6.2 covering "Schemes Using One Ephemeral Key Pair" (C(1e) Schemes). Following established standards ensures the implementation stays cryptographically sound.
40+
41+
42+
Here's where things got interesting on the server side. gRPC codecs don't let you pass state between marshal and unmarshal functions, which meant we couldn't directly share the encryption key between decrypt and encrypt operations. Our solution? A thread-safe LRU that indexes keys by Goroutine ID. This worked great until we hit streaming scenarios where the server spawns a new goroutine to handle the conversation. Since the new goroutine has a different ID, we perform a stack trace to find the parent's Goroutine ID and retrieve the correct key. Turns out we weren't alone in facing this challenge—others encountered similar issues ([grpc-go#3906](https://github.com/grpc/grpc-go/issues/3906), [grpc#9985](https://github.com/grpc/grpc/issues/9985)).
43+
44+
```go
45+
func (csvc *CryptoSvc) Decrypt(in_arr []byte) ([]byte, []byte) {
46+
// Read in pub key
47+
if len(in_arr) < x25519.Size {
48+
slog.Error(fmt.Sprintf("input bytes to short %d expected at least %d", len(in_arr), x25519.Size))
49+
return FAILURE_BYTES, FAILURE_BYTES
50+
}
51+
52+
client_pub_key_bytes := in_arr[:x25519.Size]
53+
54+
ids, err := goAllIds()
55+
if err != nil {
56+
slog.Error("failed to get goid")
57+
return FAILURE_BYTES, FAILURE_BYTES
58+
}
59+
session_pub_keys.Store(ids.Id, client_pub_key_bytes)
60+
61+
// ...
62+
}
63+
64+
func (csvc *CryptoSvc) Encrypt(in_arr []byte) []byte {
65+
ids, err := goAllIds()
66+
if err != nil {
67+
slog.Error(fmt.Sprintf("unable to find GOID %s", err))
68+
return FAILURE_BYTES
69+
}
70+
71+
var id int
72+
var client_pub_key_bytes []byte
73+
ok := false
74+
for idx, id := range []int{ids.Id, ids.ParentId} {
75+
client_pub_key_bytes, ok = session_pub_keys.Load(id)
76+
if ok {
77+
slog.Info(fmt.Sprintf("found public key for id: %d idx: %d", id, idx))
78+
break
79+
}
80+
}
81+
// ...
82+
}
83+
84+
type GoidTrace struct {
85+
Id int
86+
ParentId int
87+
Others []int
88+
}
89+
90+
func goAllIds() (GoidTrace, error) {
91+
buf := debug.Stack()
92+
// slog.Info(fmt.Sprintf("debug stack: %s", buf))
93+
var ids []int
94+
elems := bytes.Fields(buf)
95+
for i, elem := range elems {
96+
if bytes.Equal(elem, []byte("goroutine")) && i+1 < len(elems) {
97+
id, err := strconv.Atoi(string(elems[i+1]))
98+
if err != nil {
99+
return GoidTrace{}, err
100+
}
101+
ids = append(ids, id)
102+
}
103+
}
104+
res := GoidTrace{
105+
Id: ids[0],
106+
ParentId: ids[1],
107+
Others: ids[2:],
108+
}
109+
return res, nil
110+
}
111+
```
112+
113+
### Cryptographic Flow
114+
1. Agent embeds server's long-term public key at compile time
115+
2. For each session, client generates ephemeral Curve25519 keypair
116+
3. Client sends its ephemeral public key to server
117+
4. Both parties perform Diffie-Hellman exchange to derive shared secret
118+
5. Shared secret is used to derive XChaCha20-Poly1305 session keys
119+
6. All messages encrypted and authenticated with session keys
120+
7. Ephemeral keys discarded at session end

0 commit comments

Comments
 (0)