Skip to content

Commit 0b9a088

Browse files
committed
fix: read legitimation challenge from address 303, fix SET_VAR_SUBSTREAMED format
Two issues with the legitimation flow: 1. Challenge source: should come from GET_VAR_SUBSTREAMED to address 303 (ServerSessionRequest) on the session object, not from the DEADBEEF blob or the session challenge from CreateObject 2. SET_VAR_SUBSTREAMED payload: needs 0x20 0x04 prefix and extra 0x00 in BLOB encoding, matching the HarpoS7 PoC template
1 parent cddb4db commit 0b9a088

1 file changed

Lines changed: 46 additions & 58 deletions

File tree

s7/connection.py

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,53 +1165,40 @@ def _post_auth_legitimation(self) -> None:
11651165
"""Perform the post-SessionKey legitimation handshake.
11661166
11671167
V1-initial PLCs require this exchange after the SessionKey blob
1168-
is accepted before they'll allow data operations. Matches TIA
1169-
Portal's frames 18-30 in the ProgramBlocks pcap.
1170-
1171-
Steps:
1172-
1. SET_VARIABLE: write USINT(5) to address 323 on the session
1173-
2. GET_VAR_SUBSTREAMED: read legitimation blob from object 50, address 7920
1174-
3. GET_VAR_SUBSTREAMED: read from session object, address 1842
1175-
4. GET_VAR_SUBSTREAMED: read legitimation blob again from object 50
1168+
is accepted before they'll allow data operations.
1169+
1170+
Matches the HarpoS7 PoC flow:
1171+
1. GET_VAR_SUBSTREAMED: read 20-byte challenge from address 303
1172+
2. Solve the challenge cryptographically
1173+
3. SET_VAR_SUBSTREAMED: write solved 248-byte blob to address 1846
11761174
"""
11771175
oq = encode_object_qualifier()
11781176

1179-
# Step 1: SET_VARIABLE to session, address 323, value USINT=5
1180-
sv_payload = struct.pack(">I", self._session_id)
1181-
sv_payload += encode_uint32_vlq(1) # ItemCount
1182-
sv_payload += encode_uint32_vlq(323) # Address
1183-
sv_payload += bytes([0x00, 0x02, 0x05]) # flags=0, USINT, value=5
1184-
sv_payload += oq
1185-
sv_payload += bytes([0x00]) # separator
1186-
sv_payload += encode_uint32_vlq(self._sequence_number)
1187-
sv_payload += struct.pack(">I", 0)
1188-
1189-
logger.debug("Post-auth legitimation: SET_VARIABLE to address 323")
1190-
self.send_request(FunctionCode.SET_VARIABLE, sv_payload)
1191-
1192-
# Step 2: GET_VAR_SUBSTREAMED from object 50, address 7920
1193-
gvs1 = struct.pack(">I", 50) # object 50
1194-
gvs1 += bytes([0x20, 0x04])
1195-
gvs1 += encode_uint32_vlq(1)
1196-
gvs1 += encode_uint32_vlq(7920) # address
1197-
gvs1 += oq + bytes([0x00])
1198-
gvs1 += encode_uint32_vlq(1) + encode_uint32_vlq(1)
1199-
gvs1 += struct.pack(">I", 0)
1200-
1201-
logger.debug("Post-auth legitimation: GET_VAR_SUBSTREAMED from object 50, address 7920")
1202-
legit_resp = self.send_request(FunctionCode.GET_VAR_SUBSTREAMED, gvs1)
1203-
1204-
# Extract the 20-byte challenge from the legitimation response.
1205-
# The response starts with VLQ return code, then a BLOB with
1206-
# DEADBEEF-prefixed data. The challenge is the 20 bytes at offset 2
1207-
# of the first DEADBEEF fragment's encrypted seed output.
1208-
legit_challenge = self._session_challenge # reuse the session challenge
1209-
if len(legit_resp) >= 20:
1210-
# The challenge for legitimation comes from the response blob.
1211-
# For no-password PLCs, we use the original session challenge.
1212-
logger.debug(f"Legitimation response: {len(legit_resp)} bytes")
1213-
1214-
# Step 3: Solve the challenge and write the response blob
1177+
# Step 1: Read legitimation challenge from session, address 303
1178+
gvs = struct.pack(">I", self._session_id)
1179+
gvs += bytes([0x20, 0x04])
1180+
gvs += encode_uint32_vlq(1)
1181+
gvs += encode_uint32_vlq(LegitimationId.SERVER_SESSION_REQUEST) # 303
1182+
gvs += oq + bytes([0x00])
1183+
gvs += encode_uint32_vlq(1) + encode_uint32_vlq(1)
1184+
gvs += struct.pack(">I", 0)
1185+
1186+
logger.debug("Post-auth legitimation: reading challenge from address 303")
1187+
challenge_resp = self.send_request(FunctionCode.GET_VAR_SUBSTREAMED, gvs)
1188+
1189+
# Extract the 20-byte challenge from the response.
1190+
# Response format: VLQ return code + BLOB data. The challenge
1191+
# is the first 20 bytes after the return code and BLOB header.
1192+
legit_challenge = self._session_challenge
1193+
if len(challenge_resp) >= 22:
1194+
offset = 0
1195+
retval, c = decode_uint32_vlq(challenge_resp, offset)
1196+
offset += c
1197+
if offset + 20 <= len(challenge_resp):
1198+
legit_challenge = bytes(challenge_resp[offset : offset + 20])
1199+
logger.info(f"Legitimation challenge: {legit_challenge.hex()}")
1200+
1201+
# Step 2: Solve the challenge
12151202
from .session_auth.legitimate import solve_legitimate_challenge_real_plc
12161203

12171204
legit_blob = solve_legitimate_challenge_real_plc(
@@ -1223,20 +1210,21 @@ def _post_auth_legitimation(self) -> None:
12231210
)
12241211
logger.info(f"Legitimation blob generated ({len(legit_blob)} bytes)")
12251212

1226-
# Write the solved blob via SET_VAR_SUBSTREAMED to session, address 1846
1227-
svs_payload = struct.pack(">I", self._session_id)
1228-
svs_payload += encode_uint32_vlq(1) # ItemCount
1229-
svs_payload += encode_uint32_vlq(1) # AddressCount
1230-
svs_payload += encode_uint32_vlq(LegitimationId.LEGITIMATE) # 1846
1231-
svs_payload += encode_uint32_vlq(1) # ItemNumber
1232-
svs_payload += bytes([0x00, DataType.BLOB])
1233-
svs_payload += encode_uint32_vlq(len(legit_blob))
1234-
svs_payload += legit_blob
1235-
svs_payload += oq
1236-
svs_payload += struct.pack(">I", 0)
1237-
1238-
logger.debug("Post-auth legitimation: SET_VAR_SUBSTREAMED with solved blob")
1239-
self.send_request(FunctionCode.SET_VAR_SUBSTREAMED, svs_payload)
1213+
# Step 3: Write solved blob via SET_VAR_SUBSTREAMED to address 1846
1214+
# Format matches HarpoS7 PoC SetVarSubStreamedRequest template
1215+
svs = struct.pack(">I", self._session_id)
1216+
svs += bytes([0x20, 0x04])
1217+
svs += encode_uint32_vlq(1)
1218+
svs += encode_uint32_vlq(LegitimationId.LEGITIMATE) # 1846
1219+
svs += oq + bytes([0x00])
1220+
svs += encode_uint32_vlq(1)
1221+
svs += bytes([0x00, DataType.BLOB, 0x00]) # extra 0x00 before VLQ length
1222+
svs += encode_uint32_vlq(len(legit_blob))
1223+
svs += legit_blob
1224+
svs += struct.pack(">I", 0) + bytes([0x00])
1225+
1226+
logger.debug("Post-auth legitimation: writing solved blob to address 1846")
1227+
self.send_request(FunctionCode.SET_VAR_SUBSTREAMED, svs)
12401228

12411229
logger.info("Post-auth legitimation completed")
12421230

0 commit comments

Comments
 (0)