Skip to content

Latest commit

 

History

History
170 lines (151 loc) · 7.59 KB

File metadata and controls

170 lines (151 loc) · 7.59 KB

Architecture overview

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.

File layout

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.

Module boundaries

                              +--------------+
                              |  protocol.py |
                              +------+-------+
                                     ^
                      +--------------+--------------+
                      |                             |
                 +----+-----+                  +----+------+
                 | auth.py  |                  | tls_utils |
                 +----+-----+                  +-----+-----+
                      ^                              ^
     +----------------+------------------------------+----------------+
     |                                                                |
+----+-----------+                                              +-----+----------+
| backdoor_      |<---- TCP (+ optional TLS) framing ---------->| backdoor_      |
| server.py      |                                              | client.py      |
+----------------+                                              +----------------+
  • protocol.py has no dependencies on the rest of the project. It is usable as-is in any other TCP socket exercise.
  • auth.py depends only on protocol.py. Same property: reusable on its own.
  • tls_utils.py depends only on the standard library. The server and client import it conditionally when --tls is passed.
  • backdoor_server.py and backdoor_client.py are the two entry points. They import the three helpers above. They do not import each other.

Threads and ownership (server side)

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.

Threads on the client side

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.

Error handling posture

  • ProtocolError is 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)).
  • OSError from the socket is fatal for the current connection. Same handling.
  • subprocess.TimeoutExpired inside a shell handler becomes a RESPONSE_ERROR frame. The connection stays alive.
  • KeyboardInterrupt at the operator REPL triggers graceful shutdown of all sessions.
  • Uncaught exceptions inside a client-side handler are caught at the top of run_client and reported as RESPONSE_ERROR so one buggy command does not crash the whole client.

Testing strategy

  • Protocol: unit tests that build frames with Frame.encode and exercise recv_frame against a socket.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 dispatch router. 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.