![[Insomni'hack 2025] write-up CTF - passvault 3](/images/passvault-main_hu_67d970349796833.jpg)
[Insomni'hack 2025] write-up CTF - passvault 3
- Ctf , Hardware , Write up
- April 1, 2025
Table of Contents
Initial Analysis and Hardware Identification
The challenge starts by analyzing a software capture obtained from DSView in .dsl
format. The capture includes data exchanged over an I²C bus. The first step involves:
- Opening the
.dsl
file with DSView. - Identifying the two signals: SDA (data) and SCL (clock) on the I²C bus.
- Exporting the data to CSV format for automated analysis.
Secure Element Chip Identification
After extracting the data, we quickly identify typical smart card transactions. The presence of commands such as:
INITIALIZE UPDATE
(CLA:0x80
, INS:0x50
)EXTERNAL AUTHENTICATE
(CLA:0x84
, INS:0x82
)- and other secure commands (INS:
0x03
)
indicates the use of the GlobalPlatform SCP03 protocol, common in NXP JCOP4 secure elements.
A detailed APDU analysis clearly shows the chip model:
00 a0 00 00 03 96 04 03 e8 00 fe 02 0b 03 e8 08 01 00 00 00 00 64 00 00 0a 4a 43 4f 50 34 20 41 54 50 4f
Here, JCOP4 ATPO explicitly reveals the chip is an NXP JCOP4.
Searching for Default Keys
Given the identified chip (JCOP4), a quick search through NXP’s official documentation or public resources shows default keys for SCP03 transactions are widely documented:
- S-ENC (encryption key):
BD1DE20A81EAB2BF3B709A9D69A31254
- S-MAC (MAC key):
9A761B8DBA6BEDF22741E45D8D4236F5
- S-DEK (derivation key):
9B993B600F1C64F5ADC063192A96C947
These keys can be found in official datasheets or via GitHub searches (e.g., “JCOP4 SCP03 default keys”).
CSV Data Processing and Conversion
The CSV file exported from DSView provides data in decimal bytes (0-255). We wrote a Python script to:
- Read and parse the CSV.
- Convert decimal data to hexadecimal (Note: DSView can do this directly, realized after the fact).
- Group the transactions into logical command/response sequences (APDU).
This facilitates cryptanalysis.
Cryptanalysis of SCP03 Transactions
The challenge requires decrypting SCP03-protected exchanges using AES-CMAC and AES-CBC with default keys. The Python script performs the following steps:
INITIALIZE UPDATE (INS: 0x50):
- Extract challenges exchanged between card and reader.
- Generate session keys (
S_ENC
,S_CMAC
,S_RMAC
) from static keys and challenges (KDF as per SCP03 standard SP800-108).
EXTERNAL AUTHENTICATE (INS: 0x82):
- Verify cryptograms exchanged, authenticating secure transactions.
SECURE COMMAND (INS: 0x03):
- Decrypt payloads using session keys.
- Verify integrity (MAC).
These steps enable full decryption of secure communications.
Python Script
from fastcrc import crc16
from construct import *
from Crypto.Hash import CMAC
from Crypto.Cipher import AES
from Crypto.Protocol import KDF
import csv
import re
# === Fonctions utilitaires ===
def crc(data):
return (crc16.iso_iec_14443_3_a(data, initial=0xffff) ^ 0xffff).to_bytes(2, "little")
def is_crc_valid(data):
return crc(data[:-2]) == data[-2:]
# === Extraction depuis le CSV ===
csv_filename = "decoder--250315-005946.csv"
output_txt = csv_filename.replace(".csv", ".txt")
with open(output_txt, "wt") as outfile:
with open(csv_filename, 'rt') as csvfile:
reader = csv.DictReader(csvfile)
current_dir = None
for row in reader:
status = row['1:I²C: Address/Data']
if 'Address write' in status:
current_dir = 'write'
elif 'Address read' in status:
current_dir = 'read'
elif 'Data write' in status and current_dir == 'write':
data = status.split(':')[-1].strip()
outfile.write(f"write: {data}\n")
elif 'Data read' in status and current_dir == 'read':
data = status.split(':')[-1].strip()
outfile.write(f"read: {data}\n")
# === Parsing write/read en transactions ===
transactions = {}
i = 0
curr = ""
for line in open(output_txt, "rt").readlines():
if line.startswith("write: "):
if curr == "r":
i += 1
transactions.setdefault(i, {})["cmd"] = transactions[i].get("cmd", b"") + bytes.fromhex(line.split(":")[1].strip())
curr = "w"
elif line.startswith("read: "):
transactions.setdefault(i, {})["response"] = transactions[i].get("response", b"") + bytes.fromhex(line.split(":")[1].strip())
curr = "r"
# === Structures GP et SCP03 ===
pkt_f = Struct("DAD_SAD" / Int8ub, "PCB" / Int8ub, "len" / Int8ub, "payload" / Bytes(this.len), "crc" / Int16ul)
gp_f = Struct("CLA" / Int8ub, "INS" / Int8ub, "P1" / Int8ub, "P2" / Int8ub, "Lc" / Int8ub, "payload" / Bytes(this.Lc))
kdf_data = Struct("ctr" / Int32ub, "label" / Bytes(12), "sep" / Int8ub, "context" / Bytes(16), "len" / Int32ub)
def prf(key, data):
k = kdf_data.parse(data)
return CMAC.new(key, k.label + b'\x00' + k.len.to_bytes(2, "big") + b'\x01' + k.context, ciphermod=AES).digest()
consts = {"CardCrypto": 0x00, "HostCrypto": 0x01, "CardChallenge": 0x02, "S_ENC": 0x04, "S_CMAC": 0x06, "S_RMAC": 0x07}
# === Clés statiques ===
session_ctx = {
'static_enc': bytes.fromhex('BD1DE20A81EAB2BF3B709A9D69A31254'),
'static_mac': bytes.fromhex('9A761B8DBA6BEDF22741E45D8D4236F5'),
'static_dek': bytes.fromhex('9B993B600F1C64F5ADC063192A96C947')
}
# === Analyse des transactions ===
for x in transactions:
cmd = transactions[x].get("cmd", b"")
rsp = transactions[x].get("response", b"")
print(f"\n====================\nTransaction {x}")
print(f"WRITE : {cmd.hex()} | ASCII : {cmd.decode(errors='ignore')}")
print(f"READ : {rsp.hex()} | ASCII : {rsp.decode(errors='ignore')}")
try:
payload = pkt_f.parse(cmd).payload
p = gp_f.parse(payload)
if p.CLA == 0x80 and p.INS == 0x50:
print("[>] INITIALIZE UPDATE trouvé.")
host_challenge = p.payload
response_payload = pkt_f.parse(rsp).payload
card_challenge = response_payload[-18:-10]
card_cryptogram = response_payload[-10:-2]
session_ctx['s_enc'] = KDF.SP800_108_Counter(session_ctx['static_enc'], 16, prf, label=consts["S_ENC"].to_bytes(12,"big"), context=host_challenge + card_challenge)
session_ctx['s_cmac'] = KDF.SP800_108_Counter(session_ctx['static_mac'], 16, prf, label=consts["S_CMAC"].to_bytes(12,"big"), context=host_challenge + card_challenge)
session_ctx['s_rmac'] = KDF.SP800_108_Counter(session_ctx['static_mac'], 16, prf, label=consts["S_RMAC"].to_bytes(12,"big"), context=host_challenge + card_challenge)
print(f" S_ENC : {session_ctx['s_enc'].hex()}")
print(f" S_CMAC : {session_ctx['s_cmac'].hex()}")
print(f" S_RMAC : {session_ctx['s_rmac'].hex()}")
elif p.CLA == 0x84 and p.INS == 0x03:
print("[>] SECURE COMMAND trouvé.")
cipher = AES.new(session_ctx['s_enc'], AES.MODE_ECB)
ICV = cipher.encrypt((1).to_bytes(16, "big"))
cipher = AES.new(session_ctx['s_enc'], AES.MODE_CBC, ICV)
decrypted_data = cipher.decrypt(p.payload[:-8])
print(f" Decrypted Payload (hex): {decrypted_data.hex()}")
print(f" Decrypted Payload (ascii): {decrypted_data.decode(errors='ignore')}")
response_payload = pkt_f.parse(rsp).payload
cipher = AES.new(session_ctx['s_enc'], AES.MODE_ECB)
ICV_r = cipher.encrypt((1 | 0x80000000000000000000000000000000).to_bytes(16, 'big'))
cipher = AES.new(session_ctx['s_enc'], AES.MODE_CBC, ICV_r)
response_decrypted = cipher.decrypt(response_payload[:-10])
print(f" Response Decrypted (ascii): {response_decrypted.decode(errors='ignore')}")
except Exception as e:
print(f"[!] Erreur transaction {x} : {e}")
Obtaining the Flag
Decrypted transactions clearly reveal an ASCII-readable text containing the flag:
- Decrypted payload:
p4s5w0r*K3Y$l0
Conclusion
The challenge highlights the importance of securely managing SCP03 keys. Default keys allow attackers to compromise system security.
Final Flag:
p4s5w0r*K3Y$l0