In this chapter, you’ll put the pieces together: send the Client Hello, wait for the handshake marker, send the malformed heartbeat, and interpret the response. The fixture returns a tiny fake leak so you can see the signal without ever testing a real service.
This is the “recipe for broken hearts” part of the old workshop, but with the dangerous edge moved into a local fixture. We’re learning the exploit shape, not practising surprise checks against the internet.
Concepts Covered
Heartbeat request and response records
Length checks and why they failed in vulnerable OpenSSL releases
Hex dumping received bytes
Interpreting a proof-of-concept result safely
Why This Matters
The exploit lesson asks you to distrust external length fields, parse protocol data carefully, and keep dangerous techniques inside authorised labs instead of memorising one old packet.
Objective
Run the local Heartbleed-style proof-of-concept against a fixture socket and explain what the returned bytes mean.
Sending the Heartbeat
The helper heartbeat_request() returns an intentionally malformed heartbeat record used only by the local fixture. A legitimate heartbeat response should echo the heartbeat payload; a vulnerable-style response returns more bytes than the client actually sent.
The heartbeat packet shape is small but important: it says “heartbeat request” and then claims a payload length. In the historical bug, the vulnerable server trusted that claim too much. Here the fixture does that on purpose so we can practise reading the result safely.
Start each read with the fixed-size TLS record header.
2
Extract the content type, version, and payload length.
3
Read the number of payload bytes promised by the header.
4
Return a small tuple so the calling code can decide how to interpret the record.
5
Use the safe local fixture instead of a live host.
6
Send the malformed heartbeat only after the fixture handshake marker has been seen.
True
0000: 02 40 00 73 65 73 73 69 6f 6e 3d 64 65 6d 6f 3b
0010: 20 74 6f 6b 65 6e 3d 6e 6f 74 2d 72 65 61 6c 3b
0020: 20 70 61 73 73 77 6f 72 64 3d 66 69 78 74 75 72
0030: 65 2d 6f 6e 6c 79
In the fixture response, bytes after the first three payload bytes represent fake leaked memory. The giveaway is that the response is longer than the tiny payload we sent.
You might get a bit of deja vu here: we read five bytes for the header, unpack it, then read the remaining payload just like we did with the handshake record. Protocol code often rewards this kind of repeated careful shape.
Interpreting the Result
A proof-of-concept needs a clear success condition. Here, success means we received a heartbeat record and found extra bytes after the echoed heartbeat payload. Those extra bytes are harmless fixture data, but the shape mirrors the original bug.
Dumping the contents of the payload is useful whether an exploit succeeds or fails, because it gives you evidence instead of vibes. In this notebook, hex_preview() gives us a small readable view without dragging in another dependency.
content_type, _version, payload = recordleaked_bytes = payload[3:]if content_type == TLS_HEARTBEAT_CONTENT_TYPE and leaked_bytes:print("Fixture result: vulnerable-style response")print(leaked_bytes.decode("utf-8"))else:print("Fixture result: no extra bytes returned")
1
Skip the three-byte heartbeat payload that the fixture intentionally echoes first.
2
Treat the result as vulnerable-style only when the record type is heartbeat and extra bytes are available to inspect.
Here’s what’s happening: the code isn’t declaring victory just because some bytes arrived. It checks that the response is the right TLS record type and that there’s extra payload beyond the echo. Small success conditions like this keep proof-of-concept code honest.
Checkpoint
Run the fixture proof-of-concept and explain the two checks that make the result meaningful: the TLS record type and the presence of extra payload bytes.
Then say, out loud or in notes, why this notebook uses a fixture instead of a host and port, because that boundary matters.
Extension Activity
Change the fixture leak value by creating HeartbleedFixtureSocket(leak=b"your demo text"). Re-run the final interpretation cell and explain which output changed and which parsing logic stayed the same.
For extra curiosity, change the leak to bytes that aren’t valid UTF-8 and decide how you’d display them safely, because hex dumps exist for a reason.