Énoncé (résumé)
Un beacon de tracking s’annonce en USB mais refuse de livrer son « provisioning record » sans code d’activation à 8 chiffres hex. Chaque unité recalcule son code à partir de ce qu’elle sait d’elle-même. Fichier : beacon_flash.bin. Un faux flag est visible avec strings.
Reconnaissance
filene reconnaît rien, mais l’en-tête commence parE9(magic image ESP) et l’octet à l’offset 12 vaut0x0d: ESP32-C6, donc RISC-V 32 bits (avec extension compressée). Chaînev5.5.5: ESP-IDF 5.5.5.- C’est un dump complet de flash : table de partitions en 0x8000 (nvs, otadata, app0 en 0x10000, app1, spiffs, coredump). Le dump est tronqué à 0x52d60, la NVS est vide (0xFF).
- Segments de l’app0 : DROM
0x42030020(fichier 0x10020), IROM0x42000020(fichier 0x30020), IRAM en0x40800000. stringsdonneBA26{n0t_th15_0n3_k33p_d1gg1ng}: leurre. À côté, dans la DROM :BA26-BEACON-01(0x42039d20),LilyGO T7-C6(0x42039d30), puis 31 octets non imprimables (0x42039d40), le record chiffré.- Chaînes « Provisioning record is SEALED / Enter the 8-digit hex activation code » : la routine de vérification est proche.
Point clé
Désassemblage RISC-V avec capstone ; recherche des références lui/auipc + addi vers 0x42039d20–0x42039d40. Elles tombent dans une fonction du code applicatif (≈ 0x42000124) :
h1 = FNV-1a32("LilyGO T7-C6"),h2 = FNV-1a32("BA26-BEACON-01")(offset0x811c9dc5, prime0x01000193).- Le code saisi (
strtoul, base 16) doit valoirrotl32(h1, 7) ^ h2. Il est donc entièrement dérivable du firmware, sans l’appareil. - Si OK, un générateur de type xorshift est initialisé avec
(a4, a5) = (h1, h2)et déchiffre 31 octets par XOR :a1 = (a5>>19)|(a4<<13); a6 = a5<<13; a4 ^= a1; a5 ^= a6 a1 = (a5>>7)|(a4<<25); a6 = a4>>7; a5 ^= a1; a4 ^= a6 a1 = (a5>>15)|(a4<<17); a4 ^= a1 a5 ^= a5<<17 out[i] = ct[i] ^ (a5 & 0xff)
Résolution reproductible
python3 -m venv .venv && .venv/bin/pip install capstone # uniquement pour le désassemblage
python3 -I solve.py files/beacon_flash.bin
Sortie : code d’activation b80fef82 puis le flag.
Script : solve.py (extrait les chaînes et le chiffré du dump, recalcule le code, déchiffre). Outils d’analyse : parse_parts.py (partitions), xref.py (références), dis.py (désassemblage).
Note
Le code n’a pas été testé sur un appareil réel ; le déchiffrement produit directement un flag lisible au bon format.
Code
#!/usr/bin/env python3
# python -I solve.py files/beacon_flash.bin
import sys
d = open(sys.argv[1] if len(sys.argv) > 1 else 'files/beacon_flash.bin', 'rb').read()
M = 0xffffffff
# DROM: vaddr 0x42030020 <-> file 0x10020 ; code ref'd 0x42039d20/30/40
def drom(va): return 0x10020 + (va - 0x42030020)
def cstr(va):
o = drom(va); return d[o:d.index(b'\0', o)]
s_id, s_board = cstr(0x42039d20), cstr(0x42039d30)
ct = d[drom(0x42039d40):drom(0x42039d40) + 31]
def fnv(b):
h = 0x811c9dc5
for c in b: h = ((h ^ c) * 0x01000193) & M
return h
rotl = lambda x, n: ((x << n) | (x >> (32 - n))) & M
h1, h2 = fnv(s_board), fnv(s_id) # a4, a5
code = rotl(h1, 7) ^ h2
print('id=%r board=%r' % (s_id, s_board))
print('activation code: %08x' % code)
a4, a5 = h1, h2
out = bytearray()
for c in ct:
a1 = ((a5 >> 19) | (a4 << 13)) & M; a6 = (a5 << 13) & M
a4 ^= a1; a5 ^= a6
a1 = ((a5 >> 7) | (a4 << 25)) & M; a6 = a4 >> 7
a5 ^= a1; a4 ^= a6
a1 = ((a5 >> 15) | (a4 << 17)) & M
a4 ^= a1
a5 ^= (a5 << 17) & M
out.append(c ^ (a5 & 0xff))
print(out.decode(errors='replace')) import struct,sys
d=open(sys.argv[1],'rb').read()
print(len(d),hex(len(d)))
o=0x8000
while True:
e=d[o:o+32]
if e[:2]!=b'\xaa\x50': break
t,s,off,sz=struct.unpack('<BBII',e[2:12]); print(e[12:28].rstrip(b'\0'),hex(t),hex(s),hex(off),hex(sz)); o+=32