Skip to content

SessionKey handshake for V1-initial S7-1200 PLCs#775

Open
gijzelaerr wants to merge 13 commits into
masterfrom
fix-tls-layering-v2
Open

SessionKey handshake for V1-initial S7-1200 PLCs#775
gijzelaerr wants to merge 13 commits into
masterfrom
fix-tls-layering-v2

Conversation

@gijzelaerr

Copy link
Copy Markdown
Owner

Replaces #761, rebased onto the s7commplus/ package split (#774).

Summary

Adds the full session_auth package for V1-initial S7-1200 PLCs that require a SessionKey handshake before data operations:

  • s7commplus/session_auth/: Public-key-based session authentication (Family 0 key derivation, fingerprint matching, SecurityKeyEncryptedKey blob generation, legitimation challenge solver, HMAC key derivation)
  • V3 HMAC framing for post-auth data operations
  • TIA-style CreateObject attributes (required by V1-initial firmware)
  • PAOM string stripping in ServerSessionVersion echo
  • Post-auth legitimation flow (challenge read + solved blob write)
  • Connection.connect(password=) parameter for auth flow

Test plan

Known limitations

🤖 Generated with Claude Code

Rebased onto the s7commplus/ package split (#774). All session_auth
code now lives under s7commplus/session_auth/ instead of s7/session_auth/.

Adds the full session_auth package for V1-initial S7-1200 PLCs that
require a SessionKey handshake before data operations:

- s7commplus/session_auth/: Public-key-based session authentication
  (Family 0 key derivation, fingerprint matching, SecurityKeyEncryptedKey
  blob generation, legitimation challenge solver, HMAC key derivation)
- V3 HMAC framing for post-auth data operations
- TIA-style CreateObject attributes (required by V1-initial firmware)
- PAOM string stripping in ServerSessionVersion echo
- Post-auth legitimation flow (challenge read + solved blob write)
- Connection.connect(password=) parameter for auth flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gijzelaerr and others added 3 commits July 16, 2026 19:49
…ation

- Add type annotations for hashmod parameters in HKDF helpers
- Fix frame_version type annotation (int vs ProtocolVersion)
- Remove duplicated _skip_typed_value type-dispatch block (dead code after return)
- Guard auth_result unpacking with explicit None check for mypy
- Add None guards for session_key and challenge in _post_auth_legitimation
- Add assert/guard for Optional connection in substreamed client methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Analysis of TIA Portal V19 pcap captures (xBiggs, GH-710) revealed the
root cause of the ConnectionReset after SessionKey auth:

TIA Portal's post-SetupSession sequence:
  1. SET_VARIABLE (0x04F2) — write attribute 323 on session
  2. GET_VAR_SUBSTREAMED — read from InObj=50, addr 7920 (finalize)
  3. Data reads work immediately (no password needed)
  4. Legitimation (addr 303/1846) happens later, optionally

Our code was skipping steps 1-2 and jumping straight to legitimation
(read challenge → write blob → finalize), which caused the PLC to
return V254 SYSTEM_EVENT error 0xE9 and RST subsequent requests.

Changes:
- Add _session_activate() that mirrors TIA Portal's activation:
  SET_VARIABLE attr 323 + GET_VAR_SUBSTREAMED InObj=50 addr 7920
- Only call _post_auth_legitimation() when a password is provided
  (previously ran unconditionally, even with empty password)
- Reorder: activate first, then legitimation (if password given)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace duplicate _skip_typed_value (73 lines) with codec.skip_typed_value
  import; fixes DWORD bug (was VLQ instead of fixed 4 bytes)
- Initialize _session_auth_public_key/_session_auth_family in __init__
  instead of using hasattr() guards
- Remove redundant lazy imports (DataType, LegitimationId already at
  top level)
- Make SessionKey and TLS auth mutually exclusive in client.connect()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gijzelaerr
gijzelaerr force-pushed the fix-tls-layering-v2 branch 2 times, most recently from 3a5660f to 6a3b019 Compare July 16, 2026 18:15
gijzelaerr and others added 2 commits July 16, 2026 20:33
Move the 95K+ lines of machine-transpiled monolith transforms, constant
tables, and binary data into family0/_generated/ to clearly separate
opaque generated code from the human-readable orchestration layer.

Add ARCHITECTURE.md documenting:
- When SessionKey auth is needed (V1-initial vs TLS)
- The full authentication flow (diagram)
- Module map: which file does what
- How the 180-byte SecurityKeyEncryptedKey blob is built
- References to upstream HarpoS7, Black Hat papers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Data2Collection table 1 had 2111 entries (4222 bytes) but its 33
operations require 2112 entries (4224 bytes). The missing final word
0x4C28 caused IndexError during session key renewal when a challenge
selected the last lookup block.

Backported from HarpoS7 commit 22b9dc0 (bonk-dev/HarpoS7#24).

Adds:
- The missing 0x4C28 entry in fp_data2.bin
- Lookup table size invariant test
- Regression test with captured renewal challenge 5B15B469...

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gijzelaerr and others added 2 commits July 16, 2026 21:14
Analysis of all 4 TIA Portal pcaps (GH-710 + GH-728) shows TIA
consistently sends 4 requests after SetupSession, not 2:

1. SET_VARIABLE attr 323 = USINT(5)
2. GET_VAR_SUBSTREAMED InObj=50, addr 7920
3. GET_VAR_SUBSTREAMED InObj=session, addr 1842
4. GET_VAR_SUBSTREAMED InObj=50, addr 7920 (again)

Previously we only did steps 1-2. Add steps 3-4 and extract a
_build_get_var_substreamed helper to reduce boilerplate in both
_session_activate and _post_auth_legitimation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move _parse_create_object_response (120 lines, connection.py) into
  codec.py as parse_create_object_attributes — a reusable function that
  returns ServerSessionVersion, public key fingerprint, and session
  challenge in a CreateObjectAttributes dataclass. Both sync connection
  and async_client can use it.
- Keep parse_server_session_version as a thin wrapper for backward compat.
- Fix: add session_auth/**/*.bin to pyproject.toml package-data so
  binary data files (fp_data, transform12 metadata) ship in wheels.

Net: -69 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bonk-dev

Copy link
Copy Markdown

Hi, I'm the creator of HarpoS7. I see that you're porting it to Python which is awesome! However, could you please include the original license text somewhere in your repository?: LICENSE-HarpoS7

@gijzelaerr

Copy link
Copy Markdown
Owner Author

hey! thanks for making HarpoS7!

But i don't fully understand your request. the licenses we put on our software is the same, it's both MIT right?

https://github.com/gijzelaerr/python-snap7/blob/master/LICENSE

Or are you talking about credits? I think the parts that are ported from HaroS7 should indicate it comes from your source code, but i happily do another iteration and check credit is given where applicable. I'll aso add you to the credits file/section.

Also, i'm not 100% convinced yet that the SessionKey handshake for V1 s7commplus should land in this package, since it is very reverse-engineery with tons of binary blobs and magic. I've been trying to polish it, but there is still quite some magic. Maybe my cleanups can be helpful on your side again :)

Let me know if i can help you out in another way.

gijzelaerr and others added 5 commits July 17, 2026 11:38
The session_auth package is a Python port of HarpoS7 by bonk-dev.
Include the original MIT license text as requested in #775, and add
credits in the changelog and docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GH-710)

The 4-step activation sequence (SET_VARIABLE attr 323 + finalize reads)
was modeled after TIA Portal pcaps but is TIA-specific behavior — the
HarpoS7 reference implementation skips it entirely and goes straight
from SetupSession to data operations with V3+HMAC framing.

V1-initial PLCs (FW < 4.5) RST the connection when they receive the
SET_VARIABLE request, and the finalize read to addr 7920 in
_post_auth_legitimation caused V254 SYSTEM_EVENT error 0xE9.

Remove both and align with HarpoS7's proven flow: SetupSession →
optional legitimation (read 303, write 1846) → data ops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-710)

Real S7-1200 testing by @xBiggs showed that the PLC requires the
legitimation handshake after SetupSession even when no password is
configured. Without it, db_read fails with access-denied error
0xE03388000081FFE0. The legitimation solver already handles an empty
password correctly (SHA-1 of ""), so the conditional guard was simply
wrong.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…H-710)

The GET_VAR_SUBSTREAMED response for the legitimation challenge (addr
303) has the format: UInt64Vlq ReturnValue | byte | PValue(datatype +
count_vlq + length_vlq + data) | UInt32Vlq IntegrityId. The code was
skipping only the 1-byte return value VLQ, then taking the next 20
bytes as the challenge — but those first 4 bytes (00 10 02 14) are
BLOB metadata (unknown byte, datatype tag, count, length). The actual
challenge starts 4 bytes later.

This fed wrong input to the legitimation solver, producing a wrong
DEADBEEF blob that the PLC correctly rejected (ReturnValue
0xE605CC0004C6FEA1 = negative = error). The code ignored this error
and logged "completed", then all subsequent data reads failed with
V254 SystemEvent error 0xE9.

Fix both issues:
- Parse the PValue BLOB header properly to extract the challenge at
  the correct offset (matching thomas-v2 GetVarSubstreamedResponse)
- Check the SET_VAR_SUBSTREAMED response return value and raise on
  negative (= legitimation rejected)

Root-caused from @xBiggs's real S7-1200 (6ES7 215-1BG40-0XB0, FW
V4.2) debug traces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
V1-initial PLCs accept GET_VAR_SUBSTREAMED (0x0586) for session-level
operations (legitimation challenge/response), but reject it for DB
data reads with V254 SystemEvent error 0xE9. This matches the
thomas-v2 S7CommPlusDriver which uses GetMultiVariables (0x054C) for
all data reads, even over TLS/V2.

Change requires_substreamed to return False so data operations route
through GetMultiVariables. V3+HMAC framing is controlled separately
by session_key presence in send_request, so HMAC signing is preserved.

Tested by @xBiggs on S7-1200 (6ES7 215-1BG40-0XB0, FW V4.2) —
legitimation now succeeds (positive return value) after the challenge
extraction fix in 1c0cae5, but db_read still hit V254/0xE9 because
GET_VAR_SUBSTREAMED was used for the data read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants