Module: 11 — Reverse Engineering Cross-Cut
Points: 20
Time estimate: 90 min lab + 4 hr independent
Deliverable: lab-13-report.md + lab13/ directory (Lua dissector + state machine diagram + Suricata rule)
Objectives
- Capture traffic from an instructor-provided binary protocol.
- Annotate at least 5 byte-level frames in Wireshark.
- Produce a state machine diagram (3 states + named transitions).
- Write a Wireshark Lua dissector that parses at least 2 message types.
- Write one Suricata rule that detects the protocol.
The Lab 13 Protocol
The instructor provides a simple TCP-based key-value store protocol running on port 9876. This is a practice protocol -- you have not seen it before, and you should not look up its implementation. The RE methodology is the objective, not finding a protocol specification online.
What you are told:
- Transport: TCP, port 9876
- There are 4 message types
- There are at least 3 distinct protocol states
- There is a binary header before each message
What you are NOT told (you must discover these):
- Magic number (if any)
- Message type values
- Field layout within each message type
- State transition conditions
Start the Protocol Server
The instructor provides a Docker image for the protocol server:
# Pull and run the lab protocol server
docker pull virtus-academy/net301-lab-protocol:latest
docker run -d --name lab13-server -p 9876:9876 virtus-academy/net301-lab-protocol:latest
# Verify the server is listening
nc -zv localhost 9876
If the Docker image is unavailable: use the Python stub below as a minimal stand-in:
# lab13/stub_server.py -- simplified 2-message-type stub for offline practice
import socket, struct
MAGIC = 0xABCD
MSG_CONNECT = 0x01
MSG_CONNECT_ACK = 0x81
MSG_GET = 0x03
MSG_DATA = 0x83
MSG_CLOSE = 0x05
def handle(conn):
state = "INIT"
while True:
try:
header = conn.recv(5)
if len(header) < 5:
break
magic, mtype, plen = struct.unpack(">HBH", header)
if magic != MAGIC:
break
payload = conn.recv(plen) if plen else b''
if state == "INIT" and mtype == MSG_CONNECT:
response = struct.pack(">HBH", MAGIC, MSG_CONNECT_ACK, 0)
conn.sendall(response)
state = "ESTABLISHED"
elif state == "ESTABLISHED" and mtype == MSG_GET:
key = payload.decode(errors='replace')
val = f"value-for-{key}".encode()
response = struct.pack(">HBH", MAGIC, MSG_DATA, len(val)) + val
conn.sendall(response)
elif mtype == MSG_CLOSE:
break
except Exception:
break
conn.close()
with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 9876))
s.listen(5)
print("Lab protocol server listening on port 9876")
while True:
conn, addr = s.accept()
import threading
threading.Thread(target=handle, args=(conn,), daemon=True).start()
Part A: Capture and Initial Observation (30 min)
Generate varied traffic against the protocol server:
mkdir lab13
# Start capture
sudo tcpdump -i lo -w lab13/lab13_protocol.pcap &
TCPDUMP_PID=$!
# Generate connection attempts (what happens on first connect?)
nc localhost 9876 < /dev/null
sleep 1
# Generate a normal session with multiple requests
python3 -c "
import socket, struct, time
MAGIC = 0xABCD # guess at startup; refine after observation
def send_raw(s, data):
s.sendall(data)
return s.recv(4096)
with socket.socket() as s:
s.connect(('localhost', 9876))
# Try: what does the server expect first?
# Hypothesis 1: send a 5-byte frame with type 0x01 (CONNECT-like)
pkt = struct.pack('>HBH', MAGIC, 0x01, 0)
resp = send_raw(s, pkt)
print('0x01 response:', resp.hex())
time.sleep(0.5)
# Hypothesis 2: send type 0x03 (GET-like) with a key
key = b'test_key'
pkt = struct.pack('>HBH', MAGIC, 0x03, len(key)) + key
resp = send_raw(s, pkt)
print('0x03 response:', resp.hex())
time.sleep(0.5)
# Hypothesis 3: close gracefully
pkt = struct.pack('>HBH', MAGIC, 0x05, 0)
resp = send_raw(s, pkt)
print('0x05 response:', resp.hex())
" 2>&1 | tee lab13/initial_probe.log
sleep 2 && sudo kill $TCPDUMP_PID
Open lab13/lab13_protocol.pcap in Wireshark. Apply filter tcp.port == 9876. Follow the TCP stream (Right-click → Follow → TCP Stream) to see the raw byte exchange.
Part B: Frame Annotation (30 min)
In Wireshark, annotate at least 5 frames. For each frame, record in your report:
| Frame # | Direction | Bytes (hex) | Hypothesized meaning |
|---|---|---|---|
| (fill in) | Client→Server | (first 10 bytes hex) | (e.g., magic=0xABCD, type=0x01, len=0) |
| ... |
Tips for annotation:
- Use
tshark -r lab13/lab13_protocol.pcap -T fields -e frame.number -e tcp.payloadto extract raw payloads for all frames - If the protocol has a 2-byte magic number, it will appear as the same 2 bytes at the start of every client-initiated frame
- Look for response patterns: does the server always send a response immediately after a request? What is the response byte sequence?
Part C: State Machine Diagram (30 min)
Using your observations from Part B, draw the state machine. Include:
Minimum requirements (Gate 5 standard):
- 3 named states
- Named transitions between states (include message type that triggers the transition)
- Confidence level for each transition (CONFIRMED / INFERRED / HYPOTHESIZED)
- Direction of each transition (client-initiated or server-initiated)
Draw the diagram in any format (ASCII art, Mermaid diagram, hand-drawn + photo, draw.io). Store as lab13/state_machine.md or lab13/state_machine.png.
ASCII art example format:
[INIT]
|
| CONNECT (0x01) from client [CONFIRMED]
↓
[ESTABLISHED]
|
| GET (0x03) from client [INFERRED]
↓ (loops back to ESTABLISHED after server DATA response)
[ESTABLISHED] ←──────────────────────────────┐
| |
| GET (0x03) + server DATA (0x83) |
└──────────────────────────────────────┘
|
| CLOSE (0x05) from client [CONFIRMED]
↓
[CLOSING]
Part D: Lua Dissector (25 min)
Based on your field annotations, write lab13/dissect_lab13.lua. The dissector must:
- Register on TCP port 9876
- Parse at least 2 message types (e.g., the CONNECT type and the GET/DATA type)
- Display the message type as a named field (not just a raw hex number)
- Display the payload length field
- Display the payload bytes
Load and test in Wireshark:
wireshark -r lab13/lab13_protocol.pcap -X lua_script:lab13/dissect_lab13.lua
Verify in the Wireshark packet detail pane: your protocol name should appear as the dissector, with the fields you defined showing their parsed values.
Part E: Suricata Detection Rule (15 min)
Write a Suricata rule that detects this protocol on port 9876. The rule should match on the magic number bytes that appear in every frame.
# lab13/lab13_detect.rules
# Replace 0xABCD with the actual magic bytes you discovered
alert tcp any any -> any 9876 (
msg:"NET-301 Lab13 Protocol CONNECT message";
content:"|AB CD 01|"; # magic bytes + CONNECT type byte
depth:3;
classtype:protocol-command-decode;
sid:1301001; rev:1;
)
alert tcp any 9876 -> any any (
msg:"NET-301 Lab13 Protocol server response";
content:"|AB CD 8|"; # magic + high-bit response type (0x81, 0x83...)
depth:3;
classtype:protocol-command-decode;
sid:1301002; rev:1;
)
Test against the capture:
suricata -r lab13/lab13_protocol.pcap -S lab13/lab13_detect.rules \
-l /tmp/lab13-suricata/ --no-random-seed
cat /tmp/lab13-suricata/fast.log
Lab Report
Create lab-13-report.md with:
-
Frame annotation table (Part B): at least 5 rows; hex values and hypothesized meanings.
-
State machine (Part C): diagram + confidence table:
| Transition | From state | To state | Trigger | Confidence | Verification method |
|---|---|---|---|---|---|
| (fill in) | CONFIRMED/INFERRED | Scapy test / observation |
-
Lua dissector (Part D): paste the complete script. Screenshot or tshark output confirming it parses the magic and type fields correctly.
-
Suricata rule (Part E): paste the rules and the
fast.logoutput. -
Gate 5 readiness self-assessment: For each of the five Gate 5 criteria from Week 11's "Capstone Gate 5 Walkthrough" table, rate yourself: Full Credit / Partial Credit / Not Yet. For any "Not Yet" item, name the specific gap and what you will do before the capstone grading session.
Grading
| Component | Points |
|---|---|
| Part B: frame annotation table; 5+ frames; hex values correct | 4 |
| Part C: state machine -- 3 states, named transitions, confidence levels | 5 |
| Part D: Lua dissector -- syntactically valid; parses 2+ message types in Wireshark | 5 |
| Part E: Suricata rule fires against the capture | 3 |
| Report: Gate 5 self-assessment with specific gap identification | 3 |
| Total | 20 |