July 5, 2026
Necropolis C2: decentralised command and control mesh network.
Necropolis is a fork of the original Arachne command-and-control (C2) system. Arachne used the libp2p protocol, pairing a distributed hash…

By Lewis Desmond
7 min read
Necropolis is a fork of the original Arachne command-and-control (C2) system. Arachne used the libp2p protocol, pairing a distributed hash table (DHT) for peer discovery with GossipSub for topic-based message distribution. The result is a mesh network with no central server. That architecture grabbed my attention. Arachne proved a true mesh C2 could work in the wild. Whereas Most C2 frameworks still rely on HTTPS or DNS to communicate with a single server or domain, which means a domain seizure can knock them offline. Unless you add relays or bounce servers, a single takedown can bring the whole operation down. So why the change in name and differences over the original Arachne, which served its purpose?
Necropolis meaning
The name comes from ancient Greek. A necropolis is a city of the dead. The metaphor fits. When nodes drop off the mesh, the network routes around them as if nothing had happened. If the operator goes silent, the dead man switch activates, and the implant keeps moving. In Necropolis, death is not failure. It is just another path forward.
Reading the book "How to Hack Like a Ghost" by Sparc Flow really piqued my interest in how APTs keep their operations alive after gaining initial access and establishing persistence. So coming across portbuster1337's project inspired me to learn this approach. Why spend so much time building an infrastructure when there's one already in use by so many legitimate services? It's a very clever way to implement a similar setup, just using Libp2p instead.
What actually is libp2p
The libp2p protocol powers IPFS, the InterPlanetary File System. Unlike traditional client-server models, libp2p treats every machine as a peer. There's no server and no client. Every machine is an equal.
Each peer in the mesh network has a unique peer ID. If someone tries to flood the network and impersonate the operator, connections still fail without the right private key. Every 30 seconds, the operator broadcasts this key to the DHT, along with their current IP address. As long as the operator holds the key, they stay in control, even if their IP changes.
Blockchain and cryptocurrency networks frequently employ decentralised communication protocols. Web3, Ethereum 2.0, and certain features in the Brave browser use similar peer-to-peer communication methods between nodes. In this context, the implant becomes an additional peer within the network.
What is the DHT
DHT stands for Distributed Hash Table, the network's phone book. With libp2p, you know only a peer ID, not its IP.
Every 30 seconds, the operator announces, "I am peer ABC123 at 1.2.3.4." The DHT stores this for a short time. Before it expires, the operator refreshes the record to keep it up to date.
On startup, your implant queries the DHT: "Who has seen peer ABC123?" The DHT responds with the latest IP address. The implant connects, performs the Noise protocol handshake, and verifies the private key. Only valid connections succeed. Invalid ones fail, and the implant keeps searching.
If your VPN drops and you receive a new IP, the operator updates: "I am ABC123, now at 5.6.7.8." The implant polls every 15 seconds and reconnects quickly. No domain or static IP needed. Just broadcast and connect.
BitTorrent uses the same mechanism to find peers, just searching for file hashes instead of peer IDs. Necropolis does the same, but asks for a peer ID. It's the same phone book, just a different question.
What is GossipSub
GossipSub is like a group chat for machines. Rather than two peers talking directly, there's a topic. Think of it as a chat room. Peers subscribe to topics they care about. When a message is published, it spreads through the network like gossip: each peer relays it to others until all interested nodes receive it. In short, GossipSub lets machines have efficient, topic-based conversations across the mesh.
Your operator subscribes to a beacon topic. Your implant publishes to it. Even if the direct connection between them breaks, the message still finds a path through the network. The operator does not need to do anything. GossipSub handles the routing.
Necropolis uses GossipSub as a fallback. If direct streams drop, GossipSub picks up the slack. Even without a relay, GossipSub still works. As long as peers are on the mesh, it is a zero-configuration safety net.
These three form the foundation: libp2p provides identity and a persistent private key. DHT keeps the phone book updated every 30 seconds. GossipSub is the fallback when nothing else works. Everything in Necropolis builds on these core elements.
Commands journey
Envelope {
ID int64 // crypto/rand — not a timestamp
Type uint32 // opaque number (Z1=beacon, Z14=exec)
Data bytes // NaCl box encrypted
Signature bytes // Ed25519 — covers all 6 fields
SenderKey bytes // sender’s Ed25519 public key
Token bytes // 32 byte operator auth token
}Envelope {
ID int64 // crypto/rand — not a timestamp
Type uint32 // opaque number (Z1=beacon, Z14=exec)
Data bytes // NaCl box encrypted
Signature bytes // Ed25519 — covers all 6 fields
SenderKey bytes // sender’s Ed25519 public key
Token bytes // 32 byte operator auth token
}Every message between the operator and the implant travels in an envelope.
Think of it like a sealed letter. The command is the letter inside. The envelope has a stamp (signature) proving who sent it, a seal (encryption) to prevent anyone from reading it, and a wax mark (auth token) to verify that the implant came from the right operator.
When the implant sends something back, it creates its own envelope. Same structure, just going the other direction. The implant uses the operator's box public key (baked in at build time) to seal it. The operator uses its own private key to open it.
Both sides do the same: seal, sign, stamp, send. Verify, open, read, act.
That is why there is no server and no client. Just two peers wrapping envelopes and opening them. Clever architecture. Think of a more secure torrenting setup :)) with IDs, not hashes.
The Difference
Arachne's networking foundation was solid, so I kept it. But the rest of the system needed updates.
Stealth Improvements
Arachne offered no evasion at all. Its implants used standard Windows APIs openly, so any monitoring tool could hook them right away. I addressed this by:
1. Reflective loading — DLL loaded from memory, no disk
2. Module stomping — hides inside a signed Microsoft DLL
3. Hell's Gate — reads real syscall numbers from ntdll
4. Indirect syscalls — calls through ntdll
5. Call stack spoofing — fakes legitimate Windows frames
6. AMSI/ETW blinding — hardware breakpoint, no memory patches
7. Sleep encryption — XOR-scrambles itself when idle
8. Pure Go fallback — if the DLL won't load, still works1. Reflective loading — DLL loaded from memory, no disk
2. Module stomping — hides inside a signed Microsoft DLL
3. Hell's Gate — reads real syscall numbers from ntdll
4. Indirect syscalls — calls through ntdll
5. Call stack spoofing — fakes legitimate Windows frames
6. AMSI/ETW blinding — hardware breakpoint, no memory patches
7. Sleep encryption — XOR-scrambles itself when idle
8. Pure Go fallback — if the DLL won't load, still worksWhy Zig
Go has three background systems that fight evasion. The garbage collector manages memory constantly. You cannot encrypt Go's own code during sleep because the runtime needs to read it. The goroutine scheduler walks call stacks. Push a fake return address, and it sees a corrupted frame and panics. Call raw Windows syscalls, and the runtime's assumptions about memory layout break. Three systems are running in the background, whether you want them to or not.
Zig has none of this. No GC. No scheduler. No runtime. It loads into memory, does exactly what you tell it, and nothing else interferes.
What Zig gives you that C and Go cannot is compile-time execution and first-class cross-compilation. The compiler ships with every target built in. Build a Windows DLL from Linux in one command. No Visual Studio. No MinGW. No extra toolchain. The structures that define Windows types and PE headers are resolved during compilation, not at runtime. Windows syscall numbers still get resolved at runtime through Hell's Gate, but the scaffolding around them, such as the syscall dispatcher, the API resolution chain, and the call stack spoofing, is built at compile time with zero runtime overhead. Less logic at runtime means a smaller footprint. Less for an EDR to inspect.
No runtime. No GC. Direct access to the Windows kernel. Nothing in the implant is running that an EDR can hook. Just your code doing what you told it to.
Encryption Upgrades
Arachne encrypted implant-to-operator only. Now, both directions are NaCl box-encrypted. Each implant generates a key pair at startup and sends the public key during registration.
Transport Trade-Offs: Mesh, CDN, and Beyond
Arachne relied on two paths: direct streams and GossipSub. I added new transport options, each with distinct strengths and weaknesses. Operators can mix and match according to their threat model and operational needs:
- WSS: Traffic can be routed through a CDN, making it appear as ordinary HTTPS WebSocket traffic. This boosts stealth and helps bypass network blocks or avoid anomaly alerts. The trade-off is centralisation. A CDN like Cloudflare can be monitored or taken down, so this option is best kept as a fallback for situations where direct peer-to-peer connectivity is not possible.
- Circuit Relay: Now, every implant can act as a relay, not just the operator. This lets machines behind NAT or firewalls stay connected without relying on central infrastructure. It keeps the network resilient and decentralised, at the cost of a bit more latency and routing complexity.
- DHT Dead-Drop: The operator can drop signed messages into the distributed hash table, and implants check for them every 30 seconds. This allows for indirect, asynchronous communication. Even if both the operator and the implant are sometimes offline, messages will still be delivered. It keeps the system distributed and reliable, especially in tough network conditions.
Operational Flexibility and Threat Model
- Authentication: A 32-byte token in every envelope; wrong tokens are dropped immediately.
- Dead Man Switch: Cross-platform autonomous action if the operator goes dark. Commands delivered after the connection is dropped can still be acted on by the implant using dead-man flags with each command sent.
- Store-and-Forward: Implants catch up on messages after brief disconnects.
- Rate Limiting: 100ms throttle per implant.
Codebase Cleanup
I removed 3,500 lines of dead weight to streamline the codebase since portbuster1337 built a solid foundation, but it was unfinished upon inspection:
- Unused gRPC services and non-imported config types.
- Dangling crypto wrappers.
- Unwired CLI commands (e.g., Ping).
- Parsers replaced with standard library alternatives.
The libp2p core is still there, but everything built on top has been reworked. The foundation's the same, but the system itself is fundamentally different now.
I hope you found this as interesting as I did. Still a lot for me to try and learn from. My next write-up on this will demonstrate the implant in use against dynamic analysis, and see how far we get.
Github: https://github.com/Yenn503