This document gives a 60-second tour of the modules and threads in the educational backdoor project. It is meant to be the first thing a new student reads after the README.
Backdoor_Project/
+-- README.md <- Start here. Includes the ethics section.
+-- LICENSE
+-- .gitignore
+-- requirements.txt <- Runtime deps (Pillow for the screenshot command).
+-- requirements-dev.txt <- Plus pytest and cryptography for tests + cert gen.
+-- pytest.ini <- Test discovery.
+-- protocol.py <- Binary framing, FrameType enum, ProtocolError.
+-- auth.py <- HMAC challenge-response handshake.
+-- tls_utils.py <- ssl.SSLContext helpers (server + client).
+-- backdoor_server.py <- Operator-side CLI.
+-- backdoor_client.py <- Target-side CLI.
+-- scripts/
| +-- gen_selfsigned_cert.py
+-- tests/
| +-- test_protocol.py <- Framing unit tests.
| +-- test_auth.py <- Handshake unit tests.
| +-- test_client_handlers.py <- dispatch() and per-command handlers.
| +-- test_client_main.py <- run_client() and main() CLI tests.
| +-- test_integration.py <- Full roundtrip over socketpair.
| +-- test_server.py <- SessionRegistry, accept_loop, REPL, main().
| +-- test_tls_utils.py <- SSLContext helpers + live TLS handshake.
+-- docs/
+-- PROTOCOL.md <- Wire format spec.
+-- ARCHITECTURE.md <- This file.
+-- LEARNING_RESOURCES.md <- Where to go next if you want more.
+--------------+
| protocol.py |
+------+-------+
^
+--------------+--------------+
| |
+----+-----+ +----+------+
| auth.py | | tls_utils |
+----+-----+ +-----+-----+
^ ^
+----------------+------------------------------+----------------+
| |
+----+-----------+ +-----+----------+
| backdoor_ |<---- TCP (+ optional TLS) framing ---------->| backdoor_ |
| server.py | | client.py |
+----------------+ +----------------+
protocol.pyhas no dependencies on the rest of the project. It is usable as-is in any other TCP socket exercise.auth.pydepends only onprotocol.py. Same property: reusable on its own.tls_utils.pydepends only on the standard library. The server and client import it conditionally when--tlsis passed.backdoor_server.pyandbackdoor_client.pyare the two entry points. They import the three helpers above. They do not import each other.
The server is the interesting one because it has multiple threads.
main thread
|
|-- argparse, logging setup, cert loading
|
|-- creates SessionRegistry
|
|-- creates a shutdown_event (threading.Event)
|
|-- spawns the "accept" thread
|
+-- runs the operator REPL on the main thread
|
accept thread |
| |
|-- loop: |
| raw, addr = listener.accept() |
| if TLS: wrap_socket(raw) |
| auth.server_handshake(sock, secret) |
| session = registry.add(sock, addr) |
| send_text(sock, COMMAND, "infos"); recv_frame(sock) |
| (runs without session.lock — no other thread knows |
| about the session yet; safe to access directly) |
+-- exits when shutdown_event is set |
|
operator REPL thread (== main) |
| |
|-- loop: |
| raw = input("[N] addr:port plat cwd > ") |
| if "clients" / "use" / "quit" / "help": handle locally |
| else: _dispatch_to_client(active_session, raw) |
| which does send_text(COMMAND, ...) then recv_frame |
|-- exits on EOF, KeyboardInterrupt, or "quit" |
|
on exit:
-- sets shutdown_event
-- closes the listener
-- closes every session in the registry
-- joins the accept thread (2-second timeout; thread is a daemon
so the process exits regardless if the join times out)
Key invariant: every ClientSession has its own threading.Lock. Any
code that wants to send_frame or recv_frame on a session MUST hold
the session's lock for the entire request-response cycle, otherwise a
future status-poller thread and the REPL thread could interleave their
frames on the same socket.
Zero threads. The client is a single loop:
connect loop:
connect to (host, port), optionally TLS-wrap
client_handshake(sock, secret)
run_client(sock, secret):
while True:
frame = recv_frame(sock) # waits for operator
response_type, response_bytes = dispatch(frame.payload)
send_frame(sock, response_type, response_bytes)
on disconnect or error: sleep retry_delay, reconnect
This simplicity is deliberate. Real C2 clients have worker pools, asynchronous beaconing, and jitter; an educational client has a single loop so a student can read it top to bottom in two minutes.
ProtocolErroris fatal for the current connection. The server closes the session, removes it from the registry, and the next operator input goes to another session (or prints(no clients)).OSErrorfrom the socket is fatal for the current connection. Same handling.subprocess.TimeoutExpiredinside a shell handler becomes aRESPONSE_ERRORframe. The connection stays alive.KeyboardInterruptat the operator REPL triggers graceful shutdown of all sessions.- Uncaught exceptions inside a client-side handler are caught at the top
of
run_clientand reported asRESPONSE_ERRORso one buggy command does not crash the whole client.
- Protocol: unit tests that build frames with
Frame.encodeand exerciserecv_frameagainst asocket.socketpair. Cover valid frames, every malformed header shape, short-read reassembly, and size limits. - Auth: server and client each run in their own thread, exchange handshake frames over a socketpair, assert the winner and the loser.
- Client handlers: pure-function tests on each command handler and
the
dispatchrouter. No sockets required. - Integration: one full end-to-end roundtrip over a socketpair, covering auth + command + response for each of the command types.
All tests run in well under ten seconds and require only pytest
(no external services or network). Over a hundred test cases achieve
100% branch coverage on all production modules.