Classroom Glossary Public page

Lab 13: Protocol RE Practice (Capstone Gate 5 Preparation)

720 words

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

  1. Capture traffic from an instructor-provided binary protocol.
  2. Annotate at least 5 byte-level frames in Wireshark.
  3. Produce a state machine diagram (3 states + named transitions).
  4. Write a Wireshark Lua dissector that parses at least 2 message types.
  5. 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.payload to 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:

  1. Frame annotation table (Part B): at least 5 rows; hex values and hypothesized meanings.

  2. State machine (Part C): diagram + confidence table:

Transition From state To state Trigger Confidence Verification method
(fill in) CONFIRMED/INFERRED Scapy test / observation
  1. Lua dissector (Part D): paste the complete script. Screenshot or tshark output confirming it parses the magic and type fields correctly.

  2. Suricata rule (Part E): paste the rules and the fast.log output.

  3. 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