@@ -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