The cartography subsystem¶
How the unit's map data is organised, and what it would take to supply your own.
The map container is a gzipped tar of tiles, and the map engine ships unstripped, so its function and type names are available. Every claim here comes from files in the update packages; the evidence is named as it goes.
tools/cartography.py reads every layer that is fully understood — the name pools, the
.inf sidecars, the %03dSCC.DST record tables and the CCT.DAT table — so the claims on
this page can be checked against your own package rather than taken on trust:
uv run tools/cartography.py names <pool> # a NUL-separated string pool
uv run tools/cartography.py inf MAPPE/005/DESCRI.DAT.inf # the .inf checksum + fields
uv run tools/cartography.py scc <module>/005SCC.DST # the record table
uv run tools/cartography.py cct CCT.DAT --image app_nav.bin # decrypt via the firmware key
It does not read the tiles, because they are not decoded — the bit-packing below is the reason. Where a claim is an inference rather than a reading, the page says so.
Where the truth comes from¶
Two PowerPC ELF files carry the whole subsystem, and neither is stripped:
| file | where | symbols |
|---|---|---|
db_dwnl_ppc.out |
the map package (M49RG20-Q0123-2001/db_dwnl_ppc.out) |
1,829 symbols, 1,205 functions |
db_dwnl_gl.out |
the firmware package (NAV/DB_DWNL/) |
1,900 symbols, 1,278 functions |
They are the cartography module — db_dwnl = "database download" — for the PC build and the
head-unit ("GL") build respectively. tools/elfsyms.py reads them.
Check these first, always
This is the same lesson as tools/elfsyms.py's note in AGENTS.md: the package ships
unstripped binaries, so before reverse-engineering anything, check whether it already
has a name. The map subsystem cost nothing to identify because the vendor shipped the
function and type names.
The file tree¶
Both builds contain the filename templates verbatim. Reconstructed, the runtime layout under
<root>/MAPPE/ is:
MAPPE/ per-country root, %03d = country id (001 = Italy)
%03d.DEG country-level geometry
%03d/%03d.DEG parcel geometry
%03d/%03d%s.DEG .DPL .DRL per-parcel geometry variants
%03d/%03d%s.DST street / road attribute data
%03d/%03dPOI.DAT the POI records
%03d/%03dcat.poi CAT.POI POI categories
%03d/%03dDSP.POI POI spatial index
%03d/%03dDPA.LZW (LZW-compressed members)
%03d/%03dSIG.LZW
%03d/%03d_NOMSERV.LZW .DAT service names
%03d/%03d_MFT.DAT MFT index - see the loader below
%03d/%03d.TIT %03d/%03d_NV.dat %03d/%03d_EPC.LET
%03d/%s/TOP/LZW%s.TOP topology
%03d/%s/TOP/NAMETOP.LZW place names
%03d/%s/LET/%sTOP.LET lettering / labels
%03d/%s/LET/%sKWD.LET %sKWD_CH.LET %sFLPY.LET
%03d/%s/IND/%sCOM.IND indices, one per family
%03d/%s/CAT/%sDCP.CAT categories
%03d/%s/KWD/LZW%s.KWD keywords
%03d/%s/FLPY/LZW%s.FLP phonetic / flypy
%03d/%s/INSCIV/LZW%s.S_C civil numbering
%03d/BCR_TABLE.DAT BCR_OFFSET.IND SEG_TO_BCR.IND
%03d/LANE.DAT %03d/JUNCTION.DAT
%03d/NAMECITY.DAT %03d/SCITTANAME.DAT %03d/EPC_COORD.DAT %03d/fonemi.lzw
%03d/gen_det.dat %03d/divieti.dat %03d/SCC.IMP
%03d/JV_FILES/ (path built by Get_path_jv_files)
%03d/RDSTMC/CCODE%X/… TMC traffic tables, per code and location
MAPPE/%03d/FILES_VER.DAT per-country file version
MAPPE/%03d/GRUPPO_4_CID.DAT group/brand tables
MAPPE/%03d/%03d_GRUPPO_BCR.DAT
MAPPE/POI_USER/CURR_VERS_POI.DAT the *user* POI store, separate from the map
GRUPPO_4_ROOT.DAT root group table
%s is a sub-family tag, not a country: the same parcel is queried as LZW%s.TOP,
%sCOM.IND, %sDCP.CAT, LZW%s.KWD, %sFLPY.LET — one file per data family within a
parcel directory.
The data model¶
The type names survive in the mangled function names, and they are the whole vocabulary:
| type | meaning |
|---|---|
TYPE_GEO_COORD |
a coordinate pair |
EXPORT_ID_PARC, EXPORT_ID_PARC_DB4, TYPE_ID_PARC_DB4 |
a parcel — the tile id |
TYPE_DRAW_LEVEL |
level of detail |
TYPE_ID_SEG, TYPE_ID_SEG_DB4 |
a road segment id |
TYPE_DRAW_SEG_PARC, TYPE_DRAW_LINK_PARC |
segments and topological links per parcel |
TYPE_DRAW_AREA, TYPE_DRAW_AREA_PTR |
polygons |
TYPE_DRAW_SERVICE |
services — POIs |
TYPE_DRAW_FACT |
facts — city centres |
TYPE_STREET_PTR, TYPE_ROAD_CODE |
street and road-code handles |
TYPE_SEG_BUFF, TYPE_SEG_LANE_TRAV |
segment buffers, lane traversal |
TYPE_ARCO_EXP, ARCO_EX_RID |
routing arcs (arco = edge) |
TYPE_EL_PARC_TABLE, TYPE_DATI_DEG_XY |
parcel tables and DEG geometry data |
TYPE_NET_MAP_USAGE, TYPE_DATI_DEG_XY |
routing network usage, geometry |
TYPE_ADM_CODE, TYPE_STR_TOP |
administrative codes, toponyms |
TYPE_CHAMPERARD, TYPE_INF_POI_USER |
guide data, user POI info |
CFilterFacility |
a POI filter class |
So the model is: parcels (tiles) × draw levels, each yielding segments, links, areas,
services and facts — plus a separate routing graph (TYPE_ARCO_EXP) with cost functions.
The engine API¶
CdCartogr has 362 named methods in the PPC build. The ones that matter for writing data:
Opening and locating
CdCartogr::Open(..., mem_part, ...) the main entry, takes a partition
CdCartogr::Initialize_db_seg / Close_db_seg
CdCartogr::Set_parcel_list(TYPE_DRAW_LEVEL, EXPORT_ID_PARC*, int*)
CdCartogr::Set_parcel_list_path(EXPORT_ID_PARC*)
CdCartogr::Open_db_rds / Open_db_lgr / Open_db_fac
Loading, per parcel and draw level
Load_segs_by_parc_cache(EXPORT_ID_PARC, …, TYPE_DRAW_LEVEL, …, TYPE_DRAW_SEG_PARC*, …)
Load_poly_by_parc_cache(…, TYPE_DRAW_AREA*, …)
Load_poi_by_parc_cache(…, CFilterFacility, …, TYPE_DRAW_SERVICE*, …)
Load_city_center_by_parc_cache(…, TYPE_DRAW_FACT*, …)
Get_segs_ptr_by_parc_cache / Get_links_ptr_by_parc_cache / Get_poly_ptr_by_parc_cache
Queries — this is what a navigation unit actually calls
Get_seg_id_of_map(TYPE_GEO_COORD, TYPE_GEO_COORD, …) coords → segment
Get_seg_by_coord(TYPE_TRANSITABLE_FILTER, …, TYPE_GEO_COORD, TYPE_ID_SEG*)
Get_road_name_by_seg / Get_road_name_data_by_seg / Get_road_phoneme_by_seg
Get_adm_name_by_seg / Get_road_code_by_seg / Get_number_seg
Get_coord_by_house_number(TYPE_ROAD_CODE, long, TYPE_GEO_COORD*) address → position
Get_coord_and_dir_by_house_number(…)
Get_fac_near(TYPE_GEO_COORD, CFilterFacility, …, TYPE_FAC_BUFF*, …)
Get_fac_by_point / Get_fac_on_path find POIs by point or along a route
Is_speed_trap_DB(int*) speeds cameras are a first-class concept
Is_category_on_DB / Is_guide_DB
Get_segs_to_or_from_node(TYPE_ID_SEG, …, TYPE_TRANSITABLE_SIDES, TYPE_TRANSITABLE_FILTER, …)
Calcola_speed_sum_from_det(…) / Calcola_eco_sum_from_det(…) routing cost
Reading — the loaders whose decompilation gives the struct layouts
Read_parc_deg(TYPE_EL_PARC_TABLE*, void*)
Load_parc_deg / Load_parc_deg_full
Read_parc_seg_lgr / Read_parc_deg_lgr
Load_seg_mft_data_tsk(…) MFT index: header, offset list, index, chain
Get_aree_det_pointer_without_wait(EXPORT_ID_PARC_DB4, …)
Get_poly_det_by_parc / Get_poly_and_put_info_in_buf_by_parc
Create_buff_output_ptr_scc_by_parc
Load_data_file_cache / Load_compress_data_file_cache
Load_first_data_file_with_filter / …_compress
I/O and compression¶
Everything goes through a small I/O shim, and the strings name it:
open_osx open_lzw Open_misc
OSX_open OSX_read OSX_seek OSX_size OSX_close
fopen fread opendir
LZW_start LZW:Read LZW:Seek
So: fopen/fread underneath, an OSX_* layer on top, and LZW as the only compression
for the members that carry it. Not encryption — LZW, a long-since-standard algorithm.
Versions are checked rather than assumed: Check_map_ver, Load_info_file_version,
FILES_VER.DAT, CD_VER.LA, and WRONG map version:map_ver=%d,map_ver_read=%s.
Providers appear by name. The phoneme tables carry \SAMPA=NAVTEQ;%s and
\SAMPA=TELEATLAS;%s — SAMPA being a phonetic alphabet, so the TTS street-name phonemes come
from Navteq (now HERE, matching PROVIDER:HERE in DVD_VER.NAV) or Tele Atlas
(TomTom) depending on the dataset.
So: can you build your own?¶
It is a bounded engineering problem. What is known:
- the container (gzipped tar per country, ~790 members, 512 MB unpacked);
- the destination layout (every filename template, quoted verbatim from the binary);
- the compression (LZW, not a bespoke scheme);
- the data model (parcels × draw levels → segments, links, areas, services, facts);
- the engine's API, including the loaders that read each file type.
What is still missing is the binary layout of each file type — the bytes inside
001.DEG, 001%s.DST, LZW%s.TOP and the rest. But that is a finite, enumerable list of
structs, and the loaders that parse them are named and available to decompile.
A realistic order of attack:
- Decompile the loaders, one family at a time, starting with the smallest complete unit:
Read_parc_degandLoad_parc_degfor geometry,Read_parc_seg_lgrfor segments,Load_seg_mft_data_tskfor the MFT index. Ghidra's PowerPC big-endian default language is right for this core — see Toolchain. - Recover each struct into a documented layout. This is where the actual work is: 15-odd file families, each with its own record format.
- Write an emitter from an OpenStreetMap extract into those layouts.
- Satisfy the checks —
FILES_VER.DAT,Check_map_ver, theGRUPPO_4_*group tables.
The honest scale. That is a project, not a patch: it is strictly larger than everything
else in this repository. But it is legible — a list of formats with named readers — rather
than a 372 MB blob. And the cheap routes still stand: the
user POI store needs none
of this, and Is_speed_trap_DB shows the engine already treats speed traps as a category.
Worked example: the POI record, partly cracked¶
001POI.DAT is the most tractable file in the set, so it is the place to start. Parsing it as
[address][name][binary][ff ff ff] works cleanly from byte zero:
"VIA LUIGI COLOMBO" "CARABINIERI, LAVENA PONTE TRESA"
7d ca 00 d0 01 4d 00 17 30 ba 00 00 00 00 01 12 3c f8 00 30 23 55 10 20 ff ff ff
"VIA ROMA" "INTESA SANPAOLO"
c1 bb 00 d0 01 4e 00 1b d5 a7 00 00 00 00 01 34 ae c4 00 30 23 44 60 56 ff ff ff
"VIA GUGLIELMO MARCONI" "BANCA POPOLARE DI SONDRIO"
cb aa 00 d0 01 4e 00 12 b7 69 00 00 00 00 01 18 2e 9c 00 30 13 99 13 73 ff ff ff
"VIA GUGLIELMO MARCONI, 2" "BANCA POPOLARE DI SONDRIO"
cb aa 00 d0 01 4f 00 12 b7 69 00 00 00 00 01 18 5e 94 00 ff ff ff
Confirmed by reading the bytes:
- the record is
[address][name][binary]and ends with a literalff ff ff; - the binary block is variable length — 22 bytes in one of the records above, 27 in its neighbour — so the tail carries an optional field group;
00 d0at+2and00 00 00 00 01at+10are constant across every record sampled;- the block is address-linked: the two
VIA GUGLIELMO MARCONIrecords sharecb aaat+0andb7 69at+8, and differ only where the house number differs.
The coordinates are not yet decoded, and the reason is a hypothesis worth stating: the
first field is small and varies with position but matches no absolute encoding. Testing
Lavena Ponte Tresa (≈45.96 N, 8.86 E) against the block for its Carabinieri record found
nothing under lat/lon scaled by 1e-5, 1e-6 or 1e-7, nor the +90/+180 variants, at any
offset or field width. That is what a parcel-relative delta looks like — and it fits the
rest of the design: the engine is organised by parcel (Set_parcel_list, EXPORT_ID_PARC),
and TYPE_DATI_DEG_XY names geometry data with X/Y.
So finishing this one file means: decompile Load_poi_by_parc_cache and the POI reader to
recover the record struct, and take the parcel origin from the parcel table. That is the same
shape of work as every other family.
PoC: OpenStreetMap in, name-pool out¶
The string-pool layer is fully understood and has been driven end to end.
The format. NAMECITY.DAT and %03d_NV.dat are plain NUL-separated strings — no
header, no footer, no compression. Verified by round-trip: reading 005_NV.DAT and writing it
back reproduces the file byte for byte (29,912 fields, 29,834 of them non-empty).
The 78 empty fields are part of the format: dropping them breaks the round trip.
The convention. NAMECITY.DAT holds NAME\TOWN, and some entries carry a postcode
district:
Of the 1,863,417 strings in the UK table, 398 end in \MANCHESTER, and 341 of those carry
a postcode district.
The PoC, measured. Overpass returns 2,443 ways in a central-Manchester bounding box,
yielding 644 distinct street names. Emitted in the unit's own format
(<STREET>\MANCHESTER) they produce a 16,832-byte pool that re-reads identically through the
same reader. So OpenStreetMap → the unit's on-disk name format works, for this layer, and
the round-trip against the real vendor file is the evidence that the format was understood
rather than guessed.
ODbL — a licence question, not a technical one
OpenStreetMap data is ODbL. Cartography built from it would be a derivative database, which carries attribution and share-alike obligations. That is a different kind of constraint from everything else on this page, and it applies to anything distributed. Worth deciding deliberately rather than discovering later.
What the PoC does not do. The name pool is the easy layer and carries no geometry — it puts nothing on a screen. The coordinates are the hard part and they are not cracked.
The coordinate problem, stated precisely¶
%03dSCC.DST is a settlement table and is the best-behaved binary in the set:
- records are a fixed 92 bytes (19,875 of them, the dominant gap);
- a record is
[name][name][10 bytes], the name appearing twice inside an 82-byte area; - every 10-byte tail begins with
0x15, and byte 3 is always0x01.
ST AGNES (Cornwall) 15 f4 f7 01 04 03 1d 01 96 03
ST IVES 15 59 1b 01 20 02 05 00 0d 04
DOVER 15 e7 4e 01 21 02 62 00 55 05
LOWESTOFT 15 b7 bd 01 22 00 ed 01 e1 05
MANCHESTER 15 b4 b5 01 22 00 ea 01 f1 02
NORWICH 15 fb cb 01 22 03 84 03 1e 06
CARLISLE 15 ff fb 01 3d 01 82 02 0a 02
Byte 4 tracks latitude loosely — 0x04 at Land's End, 0x22 across the Manchester/Lowestoft
band, 0x3d at Carlisle — but it is not monotonic in latitude (ST IVES at 50.21 gives
0x20 while ST AGNES at 50.31 gives 0x04), so it is not a scaled coordinate. Nor do any
offset, width or endianness of the remaining bytes reproduce 50–55 N / −6 to +2 E under
1e-3…1e-7 scaling.
Two candidate readings remain, and both need the loader:
- a grid or parcel cell id in byte 4, with a sub-cell offset beside it — plausible because the table is spatially ordered, starting at the south-west extreme;
- a reference into another file (an offset or record id) whose table holds the geometry.
Settling it means decompiling Load_city_center_by_parc_cache and its reader, which is the
same step every other family needs. The PoC above deliberately did not depend on it.
The coordinates, from the loader outward¶
The runtime module names the reader, so the coordinate path was followed with Ghidra rather than guessed at. Three layers came out.
Search_elem_in_buf_city_centers gives the in-memory record. It is a sorted insert, and
its struct offsets are visible:
strcmp(rec, name) // +0x00 the name, inline and NUL-terminated
rec[0x98] == code // +0x98 an id
param_2 < rec[0xa4] // +0xa4 a 4-byte SORT KEY, binary-searched
memcpy(dst, src, 0x52) // 0x52 = 82 bytes — the SCC.DST name area, exactly
so TYPE_CITY_CENTER is 176 bytes (0xb0) in memory, and its first 82 bytes are the
same name area the on-disk records carry.
The records are built from a 60-byte source (0x3c), and the coordinates are plain
integers in it. Load_city_centers_where_file_parc walks that buffer and reads:
iVar12 = Fly_dist(here.x, here.y, rec[0x30], rec[0x34]); // note: +0x30 and +0x34
iVar12 = iVar12 / 10;
Search_elem_in_buf_city_centers(rec, iVar12, rec[0x2c]); // key = distance, id = +0x2c
so at this layer the coordinates are two consecutive 32-bit integers at +0x30 and
+0x34, and the search key is a distance divided by ten.
And Fly_dist says what they are. It is not a great-circle calculation at all:
d = (y2 - y1)^2 + (x2 - x1)^2; // int -> float, via the 2^52 trick
sqrt(d);
return d * SCALE + ROUND; // clamped at SENTINEL
with the constants read straight out of the module:
| address | value | meaning |
|---|---|---|
0x1237fc |
4.50360e+15 |
2^52, the int→double conversion constant |
0x123800 |
10.0 |
the scale applied to the squared distance |
0x123804 |
2.0e+09 |
the sentinel — Fly_dist returns it when the distance is too large |
0x123808 |
0.5 |
the rounding offset |
So distances are Euclidean in the coordinate's own integer units, scaled by ten — which is why the caller divides by ten to get the key back. There is no projection and no latitude correction in the distance function, so the coordinate space is a planar grid, and the meters-per-unit factor lives in whatever produced the grid, not here.
What that leaves, precisely. The in-memory layouts and the distance maths are known. What
is not decoded is the on-disk packing of the SCC payload: the ten bytes that follow each name
area in %03dSCC.DST. They do not read as two plain 32-bit integers (for ST AGNES the tail
is 15 f4 f7 01 04 03 1d 01 96 03), so there is a compression step between the file and the
60-byte buffer that this path starts from. Reading Create_buff_output_ptr_scc_by_parc is the
next step, and it is a small function with a known signature.
So the coordinate type is a planar integer grid and the distance function is arithmetic; one packing layer is unread.
The packing layer is bit-packed¶
Following the loader one more step answers why the ten bytes would not read as integers:
the fields are not byte-aligned. Load_facs_where_parc_with_imp_by_parc unpacks them with
masks and shifts rather than loads:
uStack_194 = uStack_194 & 0xffffff00 | (uint)(*(ushort *)(rec + 0x57) >> 8);
uStack_190 = (rec[0x5b] & 0x80) | (rec[0x5b] & 0x7f) | (uint)*(ushort *)(rec + 0x57) << 0x18;
uStack_27c = uStack_190 >> 0x18 | (uStack_194 & 0xff) << 8;
uStack_278 = uStack_190 >> 8 & 0xffff;
Three things follow, and they are the useful part:
- Values are assembled from piece of one byte plus part of a 16-bit at another offset. So a field can straddle a byte boundary, and no byte-aligned read of the file will ever line up — which is exactly the symptom the data showed.
- There is a 7-bit field —
rec[0x5b] & 0x7f, compared against a filter at*(uint *)(this + 0x68)— with the top bit of the same byte belonging to a different value. Two fields share one byte. - A 24-bit value is reconstructed from
rec[0x5b]and the high half of theu16atrec[0x57], split into a low byte and a 16-bit remainder.
So the payload is a packed bitstream with sub-byte fields, not an array of integers. Recovering the full field map means walking each extraction site — they are uniform in shape, which makes it mechanical — rather than inferring a layout from the bytes.
This is worth stating as the general lesson for the remaining families too: %03d.DST,
LZW%s.TOP and the rest should be expected to be bit-packed. The three files that did read
as plain data — the name pools — are the exception that made them the right place to start.
What a map update actually has to get past¶
Cracking the formats is only the first requirement. A map package also has to be accepted, and the strings show what accepts it.
CCT.DAT is a licence token, and it is VIN-locked. The application image opens
/bd0/CCT.DAT, decrypts it, and reads an activation key, a code, and the vehicle's VIN:
/bd0/CCT.DAT
Opening %s file....
Decrypted CCT table present into file %s:
Activation Key=%s
n Code=%s
GetUncryptedVIN : GetKeyInt uncrypted VIN faile…
(C_BCM_UPGRADE) TestGetMapCode : %s
with /Licence, /CCT.DAT.inf and C_MEDIA_MANAGER alongside. The file itself is 456 bytes
at ~6.0 bits/byte of entropy — encoded, not a plain certificate.
There is a plain-file shortcut for the key prompt. Before asking for the activation key
on screen, the map updater looks for SMEG_PLUS_UPG/DATA/Licence; if that text file exists
and holds the 16-character key, the prompt is skipped. It needs no CRC sidecar, unlike
everything else in the package. (read, bousqi/SMEG_PLUS, where it is reported "tested and
approved"; this project has not verified it. See Sources and prior art.)
There are region gates too. The updater carries CheckEuropeContinent,
ReadContinentFromGruppoRoot, CONTINENT_ID, and a Crimea_Manager with
CheckIf_RUSSIA__UKRAINE_Key — so the package declares a continent (MEDIA_MAP.INI says
CONTINENT_ID:1 … EUROPE) and the updater validates it. The map updater also carries
Untar__7C_UNTGZPCcT1ii, which confirms independently that the *.BIN files really are
tarballs.
And the delivery path is not the firmware one. Cartography has its own updater
(C_SDHC_UPGRADE), a separate system from C_UPGRADE. The map package is laid out for it —
DATA/MAPPE/NNN/, DESCRI.DAT, CD_VER.*.INF, GRUPPO_4_* — but which mechanism presents it
to the unit has not been established here.
It covers the map data, and the container is re-sealable
Decrypting CCT.DAT (next section) yields a per-country checksum keyed to that country's
DESCRI.DAT, which is the manifest over the country's payloads. So it does bind the
map data, rather than merely authorising a region.
That is not the dead end it sounds like, and it is weaker than it sounds. The check turns
out to be an eight-byte string comparison against each country's .inf sidecar, and the
cipher enclosing the table is a byte subtraction whose key vector ships in the
application image. So the token is reproducible — what remains is a different and
narrower question, the algorithm behind the .inf checksum field itself.
There is still a licensing dimension, and it is not the same question. CCT.DAT is how
HERE's cartography is licensed per vehicle, and OpenStreetMap brings its own ODbL
obligations. Both are decisions for whoever ships a map.
What CCT.DAT is made of¶
It is a fixed-block file. Test_Read_CCT_table (0x0169f330) reads it 76 bytes at a
time — li r5, 0x4c on the read and cmpwi r3, 0x4c on the return — and 456 = 6 × 76.
The six records are visible directly:
rec 0: 30 34 32 | 0b 0b 01 0d 05 0f 0b 0f 06 0c | 0a | 37 64 46 3f … | 04 | 26 24 26 21 …
rec 1: 34 31 41 | 05 0c 0e 04 06 04 07 0b 08 03 0d | 35 38 73 3f …
Each record opens with three printable bytes — 042, 341, 427, 624 in four of the
six — then a run of values in 0x00–0x0F, then bytes in 0x20–0x6B. So the file
interleaves a small-valued key stream with a data stream, and 0x0A occurs inside the
small-valued runs, which is why the file looks line-structured to cat.
The key table is in the application image, not the file: Test_Read_CCT_table loads
0x035E4CD8 (lis r11, 0x35e ; addi r17, r11, 0x4cd8) and indexes it with lbzx. That
region is initialised data — readable with tools/ppcdis.py or by slicing the inflated image —
and it holds small values in the same 0x00–0x0F range, starting 00 04 01 0b 0b 01 0d 05
0f 0b 0f 06 0c, which is the same sequence as the first record's key run. That overlap is
not a coincidence and is probably the thread to pull.
It decrypts, and the key ships in the firmware. Ghidra on Test_Read_CCT_table
(0x0169f330) gives the loop directly:
iVar4 = iVar6 - ((iVar6 / 0x380) * 0x400 + (iVar6 / 0x380 & 0x1ffffffU) * -0x80); // = iVar6 % 896
iVar6 = iVar4 + 1;
cVar5 = acStack_b2[iVar3] - ChipherCCT_Vect[iVar4];
acStack_b2[iVar3] = cVar5;
so the transform is a byte subtraction against a key vector applied cyclically with period
896, over a counter that runs continuously across all six 76-byte blocks. The
0x92492493 constant is a divide by 896 (7 × 128), not a divide by 7.
ChipherCCT_Vect is a 896-byte data symbol at 0x035E4CD8, inside the application image.
Applying it reproduces the plaintext exactly.
What CCT.DAT actually says. Decrypted, it is a per-country table — six records here,
one per country in the package:
001 … 4c75a293 … /MAPPE/001/DESCRI.DAT
002 … 05e1dd3e … /MAPPE/002/DESCRI.DAT
003 … e3eb4cb5 … /MAPPE/003/DESCRI.DAT
004 … 30d2c5af … /MAPPE/004/DESCRI.DAT
005 … 1b526603 … /MAPPE/005/DESCRI.DAT
012 … 4deaa03f … /MAPPE/012/DESCRI.DAT
So it covers the cartography. It is a
per-country integrity value keyed to that country's DESCRI.DAT — and DESCRI.DAT is itself
the manifest carrying each payload's size and CRC, so the chain commits to the map data.
And it is a checksum, not a signature. The value is 32 bits, stored as ASCII hex, and
the key it is enclosed by is a plain subtraction table sitting in the firmware. That is the
same shape as contract.dat, not the shape of a signed token.
And it is a copy, not a commitment — the check is a string comparison. Ghidra on
C_MEDIA_MANAGER::CheckCCTFileRow (0x0169f9f0) gives the verifier, and it is much simpler
than the 32-bit value suggested:
cVar1 = row[0xd]; // CheckType: 1 or 2
GetFullPath(this, path, "/bd0/SMEG_PLUS_UPG/DATA/", row + 0x1a); // the stick's package
if (stat(path, &st) == 0 && (st.st_mode & 0xf000) == 0x8000) { // must be a regular file
if (cVar1 == 1) {
strcat(path, ".inf");
f = fopen(path, "rb");
fread(buf, 8, 1, f); // first 8 bytes of the sidecar
cmp(buf, row + 0xe); // compare with the record's value field
}
if (cVar1 == 2) { // otherwise: size, parsed from the field
if (st.st_size == atoi(row + 0xe)) return 0;
}
}
So for CheckType = 1 — which is what all six records here are — the check is: read the
first 8 bytes of <path>.inf and compare them with the record's value field.
Verified against every record: the value equals the first 8 bytes of each country's
MAPPE/<cid>/DESCRI.DAT.inf, all six matching. And the path is resolved against
/bd0/SMEG_PLUS_UPG/DATA/ — the update stick, not the unit's installed copy.
What that means for deliverability. CCT.DAT does not independently summarise the map
data. It copies the .inf sidecar's own checksum field and cross-checks it by
eight-byte string comparison. So:
- the gate is mechanically reproducible — put the right 8 characters in each record and
re-encrypt with
ChipherCCT_Vect, which ships in the firmware; - the real integrity work is not in
CCT.DATat all, but in producing correct.infsidecar checksums, which is the same unknown the updater's CRC cascade depends on.
The one field still unidentified is that .inf value itself. It is not crc32 of its
own file (for MAPPE/001/DESCRI.DAT, the sidecar says 4c75a293 while crc32 is
6cb1bd24), nor of the .inf, nor adler32/md5/sha1/sha256 truncations — and it is
not crc32 of the country payload gzipped or inflated either. Identifying that is the
remaining task.
The checksums are a parameter-recovery problem¶
The negative results below bound the work.
DESCRI.DAT uses a 16-bit CRC, not 32. Its records carry four hex digits per payload —
CD_VER,001,\DATA\MAPPE\001\CD_VER.LA.INF,CRC,15af — so the descriptor layer is CRC16.
That is a different family from the .inf field, which is eight hex digits.
Neither matches a standard variant. For DESCRI.DAT's 15af and the .inf's
4c75a293, every common parameterisation was tried and missed: CRC16 as
CCITT-FALSE/XMODEM/ARC/MODBUS/USB/KERMIT/X25/DNP, and CRC32 as ISO-HDLC/JAMCRC/BZIP2/MPEG-2/
POSIX/XFER/Castagnoli/Koopman/Q. So the polynomial, or what bytes are covered, is not one of
the published ones — and on this evidence the value is not computed over the file as the
package ships it.
That turns it into a bounded, offline problem rather than a mystery: recover the CRC
parameters from samples — the standard approach is CRC RevEng, which recovers poly/init/
reflect/xorout from a handful of (message, checksum) pairs. Three payloads and several
countries supply the samples. Worth noting the likely wrinkle: the updater Untars the
payloads, so the bytes the vendor checksummed may not be the bytes in the package.
The recovery is built, and it says the checksum is not a CRC¶
tools/crc_recover.py does the recovery, and — this is the part that makes its answer worth
anything — it is checked against published parameterisations. Given synthetic samples it
recovers all eight of CRC-16/MODBUS, CCITT-FALSE, XMODEM, ARC, USB, DNP, KERMIT
and X25 from scratch, without being told the polynomial. A recovery tool that fails quietly
reports "not a CRC", so testing it against knowns is the difference between a finding and a
false negative.
Run over the package's 36 real samples across 13 lengths:
No polynomial, either input reflection, and any init/xorout reproduces them. That is a much
stronger statement than "not the variants we tried": it rules out the entire standard-form CRC
class, over the file bytes as shipped. Five other message definitions were tried and all
failed — the whole file, the file with the first line stripped, with the first 8 bytes
stripped, with the first 10 bytes stripped, and with trailing CRLF stripped. Simple checksums
were ruled out earlier too: sum8, xor8, word-wise sums and XORs.
So the remaining explanation is that the vendor checksums bytes that are not the file
contents as shipped. The updater Untars the payloads, and
DESCRI.DAT describes the installed layout under \DATA\, so the checksummed byte range is
probably produced during packaging rather than shipped verbatim.
Two caveats the tool documents rather than hides. It needs at least two samples of the
same length, because init and xorout cancel in the differential only then — with one
length they are not separately identifiable at all. And even with several lengths the pair is
recovered up to an equivalence: CRC-16/MODBUS comes back as init=0x7ffc, xorout=0xc001,
which is exactly equivalent to its published 0xffff/0x0000 at every length, which is why
the tool checks published values before calling a result non-standard. The polynomial, input
reflection and output reflection are unique.
Performance. init enters the CRC linearly, so the register after n zero bytes is
linear in it; precomputing the images of the basis vectors gets Z_n(init) for any init with a
few XORs, rather than running a whole CRC per candidate.
And there is a third layer above both. The package ships
UHD6E2P01200REU_MEDIA_CONTENT.md5, headed MD5 Created with MD5_ISO Creator Ver. 2.10,
listing MD5s for every file including the .inf sidecars — CCT.DAT, CCT.DAT.inf,
MAPPE\001\DESCRI.DAT and so on, 205 lines. That is a distribution integrity layer, not a
unit one: it is how the media image is validated as a whole. It also confirms the update
layout, since its paths carry the DATA\ prefix that CheckCCTFileRow resolves against.
So there are three distinct integrity schemes in play — the .inf field, DESCRI.DAT's
CRC16, and the MD5 manifest — and the unit's own gate (CCT.DAT) is a string comparison
against the first of them.
It is also a licence. CCT.DAT is how HERE's cartography is licensed per vehicle, and
OpenStreetMap carries ODbL attribution and share-alike. Both are decisions for whoever ships a
map. This page documents the mechanism because that is what the repository does; it does not
provide a tool for forging the token.
Caveats¶
- No map data has been written to a unit. The findings are read from symbol tables,
string tables and decompiles of the shipped binaries, plus offline runs (unpacking the
tars, the name-pool round trip, decrypting
CCT.DAT,crc_recover.py). - The map package is vendor data and must never be committed — the same rule as the firmware. Nothing in this repository reproduces it.
%sin the templates is a family tag whose exact values were not enumerated here.- Only country
001(Italy) was unpacked; the member counts and extensions are from that one part.