Van een Cronos-dump naar CSV: Herstellen van een verouderd databaseformaat

Iedereen die met het CronosPro-formaat heeft gewerkt, weet dat de ruwe databestanden berucht moeilijk om te zetten zijn naar machineleesbare formaten zoals CSV. Er is veel werk verricht aan de Cronos-dumpconverter cronodump (alephdata/cronodump), maar vanwege versie-specifieke nuances in de databasestructuren werkt deze niet altijd 100% van de tijd. Om dit probleem aan te pakken, hebben we Codex gebruikt voor het analyseren van de dumpstructuur en de codebase van Cronodump verbeterd om het parsen van de vermeende defecte database te herstellen.

De Cronodump-converter kon een groot deel van het formaat lezen, maar decodeerde deze specifieke dump niet correct. We moesten begrijpen hoe de bestanden in elkaar grepen, een geobfusceerd schema herstellen, verschillende aannames in de parser corrigeren en valideren dat de geparsede waarden nog steeds onder de juiste schemacolommen verschenen.

Dit artikel legt dat proces stap voor stap uit, zonder dat voorafgaande kennis van Cronos vereist is.

Wat is Cronos?

Cronos, ook bekend als CronosPro, is een propriëtair desktop-database- en informatiebeheersysteem. Het is historisch gezien gebruikt door organisaties in Rusland en andere post-Sovjetlanden voor het bouwen van registers, doorzoekbare archieven, documentcollecties en interne informatiesystemen.

Een Cronos-installatie gebruikt een andere terminologie dan een conventionele database:

Conventionele termCronos-term
DatabaseBank
Table (Tabel)Base
RecordRecord
Field (Veld)Field
Record IDSysteemnummer

Een Cronos-database bestaat normaal gesproken uit verschillende gerelateerde binaire bestanden:

BestandenDoel
CroStru.dat en CroStru.tadDatabasestructuur: tabellen, velden, formulieren en andere definities
CroBank.dat en CroBank.tadDe feitelijke records
CroIndex.dat en CroIndex.tadZoekindexen
CroSys.dat en CroSys.tadInformatie over databases die bekend zijn bij een Cronos-installatie
Aanvullende bestandenFormulieren, formules, woordenboeken en ingebedde documenten

De .dat-bestanden bevatten de data. De bijbehorende .tad-bestanden fungeren als directories die de software vertellen waar records zich binnen elk .dat-bestand bevinden.

Een handig mentaal model is:

  • CroStru → legt uit hoe de database eruitziet.
  • CroBank → bevat de feitelijke rijen.
  • CroIndex → helpt Cronos deze rijen te vinden.

Dit onderscheid werd centraal in het herstelproces. De recorddata zelf was leesbaar, maar het schema dat nodig was om deze te interpreteren, was beschermd.

Wat bedoelen we met "normalisatie"?

In dit artikel betekent normalisatie niet de relationele databasenormalisatie naar eerste, tweede of derde normaalvorm. Hier betekent het het omzetten van een propriëtaire binaire database naar een draagbare en controleerbare representatie:

Cronos binaire bestandengedecodeerde tabellen en veldencorrect getypeerde en uitgelijnde waardenUTF-8 CSV-bestanden

Het doel was niet enkel het extraheren van strings. Het resultaat moest de relatie behouden tussen:

  • Tabellen en records.
  • Kolomnamen en waarden.
  • Datums en hun betekenis.
  • Tekst en de oorspronkelijke karaktercodering.
  • Interne veldposities.
  • Verwijzingen naar ingebedde bestanden.

Een CSV met leesbare waarden onder de verkeerde headers zou erger zijn dan een duidelijke fout, omdat het geldig zou kunnen lijken terwijl het semantisch corrupt is.

Keuze voor een bestaande parser

We begonnen met het open-source cronodump-pakket, dat twee hoofdcommando's biedt:

  • crodump inspecteert Cronos-bestanden en print hun interne structuren.
  • croconvert exporteert een database naar CSV, PostgreSQL SQL of HTML.

Voor een typische dump kan conversie zo simpel zijn als: croconvert --csv /path/to/cronos-dump

Intern voert het pakket verschillende taken uit:

  1. Het leest de .tad-bestanden om records te lokaliseren.
  2. Het haalt de bijbehorende bytes uit de .dat-bestanden.
  3. Het decomprimeert records indien nodig.
  4. Het decodeert beschermde records met behulp van een KOD-tabel.
  5. Het leest het schema uit CroStru.
  6. Het gebruikt dat schema om CroBank-records op te delen in velden.
  7. Het schrijft de resulterende tabellen naar CSV.

De documentatie van de repository vermeldt expliciet dat delen van het Cronos-formaat nog onvolledig zijn teruggeengineerd. De parser ondersteunt veel databases, maar ongebruikelijke versie-specifieke details kunnen nog steeds onderzoek vereisen.

De eerste fout

De converter faalde tijdens het lezen van CroStru.dat. Dit betekende dat we nog geen antwoord konden geven op basisvragen zoals:

  • Hoeveel tabellen zijn er aanwezig?
  • Wat zijn hun namen?
  • Hoeveel velden bevat elke tabel?
  • Welk veld is een naam, datum, identificatie of bestandsverwijzing?
  • Waar hoort elke waarde in de output?

Het grote CroBank.dat-bestand bevatte de feitelijke records, maar zonder een geldig schema was het in feite een stroom van waarden gescheiden door binaire markers.

De situatie was als volgt: CroStru.datdecodering misluktgeen betrouwbare kolomdefinitiesCroBank.datwaarden kunnen niet veilig worden geïnterpreteerd.

Voordat we probeerden iets te "cracken", inspecteerden we de headers van alle belangrijke bestanden. De dump gebruikte het Cronos 01.11-formaat, geassocieerd met Cronos v4. De component-vlaggen onthulden een belangrijk detail:

  • CroStru was KOD-gecodeerd.
  • CroBank was gecomprimeerd, maar niet KOD-gecodeerd.
  • CroIndex was gecomprimeerd, maar niet KOD-gecodeerd.

Dit beperkte de omvang van het probleem aanzienlijk. We hoefden geen tientallen gigabytes aan beschermde recorddata te decoderen; we hoefden alleen het relatief kleine schemabestand te herstellen. Zodra het schema leesbaar was, kon de gewone parser het veel grotere recordbestand interpreteren.

Compressie en protectie zijn verschillende lagen

Het is nuttig om twee concepten te scheiden die anders verwarrend kunnen worden.

Compressie verandert data zodat deze minder ruimte in beslag neemt: originele bytescompressiekleinere gecodeerde bytes

KOD-protectie verandert byte-waarden met behulp van een substitutietabel: originele bytespositie-afhankelijke substitutiebeschermde bytes

Een bestand kan:

  • Noch gecomprimeerd, noch KOD-gecodeerd zijn.
  • Alleen gecomprimeerd zijn.
  • Alleen KOD-gecodeerd zijn.
  • Zowel gecomprimeerd als KOD-gecodeerd zijn.

De bestanden in deze dump gebruikten niet allemaal dezelfde combinatie. Dat was belangrijk, omdat een van de cracking-methoden van cronodump ervan uitgaat dat het de KOD-tabel kan afleiden uit voorspelbare bytes in gecomprimeerde CroBank- en CroIndex-records. Hier waren die bestanden helemaal niet KOD-gecodeerd, dus was die aanname niet van toepassing.

Wat is een KOD-tabel?

Cronos v4 en later kunnen databases beschermen door een substitutietabel van 256 entries aan te passen, genaamd de KOD-tabel of KOD S-box.

Op het eenvoudigste niveau zegt een substitutietabel:

  • versleutelde byte 0x00 → gedecodeerde waarde X
  • versleutelde byte 0x01 → gedecodeerde waarde Y
  • ...
  • versleutelde byte 0xFF → gedecodeerde waarde Z

Cronos voegt echter een complicatie toe: de decodering hangt ook af van de positie van de byte en het recordnummer. Conceptueel is het algoritme:

plaintext[i] = KOD[ciphertext[i]] - i - record_number (mod 256)

Waarbij:

  • ciphertext[i] de opgeslagen byte is.
  • KOD[...] een substitutie uitvoert.
  • i de positie binnen het record is.
  • record_number een extra verschuiving bijdraagt.
  • De rekenkunde loopt rond bij 256.

Het praktische gevolg is dat een afwijking van slechts één byte de decodering van alles wat volgt verandert. Dit verklaarde later waarom één schema-record onleesbaar bleef, zelfs nadat we een plausibele KOD-tabel hadden hersteld.

Wat de bestaande cracking-tools doen

cronodump bevat twee relevante herstelopties:

  • crodump --strucrack ...
  • crodump --dbcrack ...

Deze maken gebruik van statistische eigenschappen van Cronos-bestanden in plaats van het brute-forcen van een wachtwoord.

strucrack

Binaire schema's bevatten veel herhaalde waarden, vooral nul-bytes die worden gebruikt voor lengtes, vlaggen, padding en integers. strucrack groepeert versleutelde bytes op basis van hun effectieve positie en neemt aan dat het meest voorkomende resultaat waarschijnlijk de plaintext-nul representeert. Met voldoende schemadata kan dit een groot deel van de KOD-tabel onthullen.

dbcrack

Gecomprimeerde records hebben herkenbare headers. In geschikte databases kunnen voorspelbare bytes in die headers worden gebruikt om KOD-mappings af te leiden uit CroBank en CroIndex.

Beide zijn heuristieken. Ze werken wanneer de bron genoeg van de patronen bevat die ze verwachten. In ons geval:

  • CroStru bevatte te weinig bewijs voor strucrack om elke entry betrouwbaar te herstellen.
  • CroBank en CroIndex waren niet KOD-gecodeerd, dus was dbcrack niet toepasbaar.

Er was ook een kleine softwarebug: de cracking-paden maakten een tijdelijk argument-object aan zonder een vereiste compact-eigenschap. Door deze toe te voegen (cargs.compact = args.compact), konden de heuristieken draaien. Ze herstelden nog steeds niet automatisch een geldige tabel, maar we waren verschoven van een programmafout naar een daadwerkelijk data-herstelprobleem.

Gerelateerd upstream werk

De cronodump-repository bevat een niet-samengevoegde branch (erdgeist-strucrack-ambigous-kods) die dezelfde zwakte aanpakt: strucrack heeft mogelijk niet genoeg bewijs om elke KOD-entry met zekerheid te bepalen.

De pull request voegt een interactieve herstelworkflow toe:

  • Bijhouden van het betrouwbaarheidsniveau voor elke afgeleide KOD-entry.
  • Markeren van entries die onopgelost blijven.
  • Detecteren van dubbele mappings.
  • Printen van gedeeltelijk gedecodeerde schemadata.
  • Zoeken naar waarschijnlijke strings zoals BankName en USERINFO.
  • De operator in staat stellen individuele correcties door te geven.
  • De operator in staat stellen bekende tekst op een specifieke recordpositie op te geven.
  • Verbeteren van de manier waarop strucrack wordt aangeroepen vanuit croconvert.

Onze situatie vereiste nog steeds aanvullende wijzigingen omdat KOD-ambiguïteit slechts het eerste probleem was. De upstream branch leek niet in te gaan op:

  • Een Cronos v4 inline-record-header die de decodering verschoof.
  • Verborgen fysieke velden binnen records.
  • Onjuiste afhandeling van gedocumenteerde tekstveldtypen.

We hebben daarom hetzelfde algemene idee gebruikt, maar de toewijzing op een andere manier geautomatiseerd.

Het bouwen van een betere statistische referentie

Naast de hoofd-dump was een kleiner leesbaar Cronos-component beschikbaar. De cronodump-repository bevatte ook een testdatabase met bekende veldtypen. Deze gaven ons voorbeelden van hoe geldige gedecodeerde Cronos-structuren eruitzien.

We hebben hun schema's niet simpelweg gekopieerd met de aanname dat de onbekende database identiek was. In plaats daarvan gebruikten we ze om algemene byte-frequenties in Cronos-schemarecords te schatten. Geldige schemadata bevatten bijvoorbeeld vaak:

  • Veel nul-bytes.
  • Kleine binaire integers.
  • Namen met een lengte-prefix.
  • Herhaalde structuurmarkers.
  • ASCII-eigenschapsnamen.
  • Windows-1251 tekst.
  • Herhaalde velddefinitie-layouts.

Voor elke versleutelde byte in het onbekende schema wisten we de ciphertext-waarde, de byte-positie en het recordnummer. Voor elke mogelijke KOD-mapping konden we berekenen welke plaintext-byte dit zou produceren en scoren hoe plausibel die byte was volgens de referentieverdeling.

Waarom dit een toewijzingsprobleem is

Een KOD-tabel moet een permutatie zijn. Dat betekent:

  • Elke ciphertext-byte heeft precies één mapping.
  • Geen twee ciphertext-bytes kunnen naar dezelfde KOD-waarde mappen.
  • Elke waarde van 0x00 tot 0xFF moet precies één keer voorkomen.

We konden niet simpelweg de hoogste score onafhankelijk kiezen voor elke byte, omdat meerdere ciphertext-bytes dezelfde output zouden kunnen selecteren, wat zou leiden tot een ongeldige tabel. We moesten de beste set mappings in zijn geheel kiezen terwijl de uniciteit werd afgedwongen.

Dit is een klassiek toewijzingsprobleem (assignment problem). We hebben dit opgelost met de implementatie van het Hongaarse algoritme in SciPy:

from scipy.optimize import linear_sum_assignment
rows, columns = linear_sum_assignment(-scores)

linearsumassignment minimaliseert kosten, dus de scores werden negatief gemaakt om de toewijzing met de hoogste waarschijnlijkheid te selecteren. Het resultaat was een complete KOD-permutatie van 256 bytes. Op dit punt hadden we een kandidaat-sleutel, maar nog geen bewezen sleutel.

Structurele validatie in plaats van "het ziet er leesbaar uit"

Alleen leesbare output is zwak bewijs. Een verkeerde substitutie kan per toeval letters, cijfers of bekende fragmenten produceren. We valideerden de kandidaat-KOD-tabel tegen de verwachte Cronos-structuur:

  • Begonnen records met geldige type-markers?
  • Konden namen met een lengte-prefix worden gelezen zonder buiten het record te treden?
  • Verschenen bekende sleutels zoals Bank, BankId en BankName?
  • Verwezen BaseNNN-entries naar geldige tabeldefinities?
  • Waren tabel-ID's en veldaantallen plausibel?
  • Decodeerden gerefereerde schema-records consistent?
  • Decodeerde Windows-1251 tekst naar zinvolle veldnamen?
  • Was de uiteindelijke KOD-tabel een echte 256-byte permutatie?

Alle schema-records behalve één doorstonden deze controles, wat leidde tot de volgende ontdekking.

Het eerste schema-record had een extra header

Het eerste CroStru-record gebruikte een Cronos v4-vlagwaarde van 0x08. De opgeslagen data begon met een 12-byte extent-header:

  • uint64 extent_offset
  • uint32 extent_length

De parser behandelde elke niet-nul vlag alsof de record-bytes direct gedecodeerd konden worden. Hij stuurde dus de header en de payload samen naar de KOD-decoder. Omdat KOD-decodering afhankelijk is van de byte-positie (plaintext[i] = KOD[ciphertext[i]] - i - record_number), zorgde het toevoegen van twaalf header-bytes er niet alleen voor dat er twaalf ongewenste tekens ontstonden, maar veranderde het de positie i voor elke payload-byte.

Het hele record werd bijgevolg gedecodeerd met een positie-offset van twaalf. De situatie was:

Opgeslagen record: [12-byte extent header][versleutelde schema payload] Aanname parser: [ versleutelde schema payload ]decodering start hier Correcte interpretatie: [skip deze header][versleutelde schema payload]decodering start hier

De parser werd aangepast om de v4 0x08-representatie te herkennen, de extent-header te lezen en alleen de opgegeven payload te decoderen:

elif not flags or (self.isv4() and flags == 0x08):
    if self.use64bit:
        next_offset, extent_length = struct.unpack("<QL", data[:12])
        payload_offset = 12

Na het verwijderen van de header begon het record met: 03 04 42 61 6e 6b. Op byte-niveau betekent dit:

  • 03 → schema-record marker
  • 04 → lengte van de volgende naam
  • 42 61 6e 6b → ASCII-tekst "Bank"

Dit was sterk structureel bewijs dat zowel de extent-afhandeling als de herstelde KOD-tabel correct waren. De belangrijkste les was dat een decoderingsfout niet altijd wijst op een slechte sleutel; soms wordt de juiste decoder toegepast op de verkeerde grens.

Het herstellen van de tabellen

Zodra CroStru correct decodeerde, kon de parser de logische databasestructuur reconstrueren: database definitietabel definitieveld naam / type / fysieke positie / maximale lengte.

Het herstelde schema bevatte een gewone datatabel en een gerelateerde bestandstabel. Hierdoor kon de converter overstappen van anonieme byte-sequenties naar records met benoemde kolommen. De eerste CSV-export werd gestart, maar sampling onthulde een ander probleem.

Tekst werd geëxporteerd als hexadecimaal

De CSV had het juiste aantal kolommen, maar veel tekstwaarden zagen er zo uit: c1 c8 d7 20 c2 ce cb ce c4 c8 cc c8 d0

Dit waren geen corrupte bytes, maar Windows-1251 tekst weergegeven als hexadecimaal. Cronos slaat Cyrillische tekst gewoonlijk op met Windows-1251. Een eerdere veiligheidsaanpassing in de parser bewaarde onbekende veldtypen als hex in plaats van willekeurige binaire data als tekst te decoderen. Dat was verstandig voor ongedocumenteerde typen, maar het beïnvloedde ook bekende tekstuele typen.

De oplossing was om de gedocumenteerde typen 1, 2 en 3 expliciet af te handelen:

elif self.typ in (1, 2, 3):
    self.content = data.rstrip(b"\x00").decode("cp1251", "ignore")

Het conversiepad werd daarmee: Cronos CP1251 bytesPython Unicode tekstUTF-8 CSV

Onbekende binaire typen bleven verliesvrij worden geëxporteerd als hexadecimaal. Na deze fix waren namen en andere tekstuele waarden leesbaar, maar sommige verschenen onder de verkeerde headers.

Waarom leesbare waarden nog steeds niet genoeg waren

Stel een vereenvoudigde tabeldefinitie voor:

  • Veld A heeft fysieke positie 2
  • Veld B heeft fysieke positie 4
  • Veld C heeft fysieke positie 7

De oorspronkelijke parser nam aan dat omdat dit de drie zichtbare definities waren, ze overeenkwamen met de eerste drie opgeslagen waarden:

  • Veld A $\leftarrow$ opgeslagen waarde 1
  • Veld B $\leftarrow$ opgeslagen waarde 2
  • Veld C $\leftarrow$ opgeslagen waarde 3

Deze aanname was onjuist. Cronos-records kunnen interne of verborgen velden bevatten die geen gewone zichtbare definities hebben. De idx2-waarde van de velddefinitie specificeert de werkelijke positie in het geserialiseerde record.

De correcte mapping was:

  • Veld A $\leftarrow$ opgeslagen waarde 2
  • Veld B $\leftarrow$ opgeslagen waarde 4
  • Veld C $\leftarrow$ opgeslagen waarde 7

Waarden 1, 3, 5 en 6 moesten nog steeds worden verwerkt (geconsumeerd), ook al werden ze niet geëxporteerd. Zonder rekening te houden met die gaten leek de CSV oppervlakkig geldig: namen waren leesbaar, datums leken op datums en identifiers leken op identifiers, maar de waarden waren verschoven naar niet-gerelateerde kolommen. Dit is precies het soort fout dat sampling van de output essentieel maakt.

Rekening houden met fysieke veldposities

De record-parser werd aangepast om de huidige fysieke veldpositie bij te houden. Voordat een zichtbare definitie wordt gelezen, worden verborgen velden geconsumeerd totdat de idx2-positie van de definitie is bereikt:

source_index = 1
for field_definition in table_definition[1:]:
    while source_index < field_definition.idx2 and not reader.eof():
        read_field_data(reader)
        source_index += 1
    value = read_field_data(reader) if not reader.eof() else b""
    source_index += 1

Cronos ondersteunt ook complexe veldwaarden met een prefix van 0x1b, dus de veldconsumptie moest dit gedrag behouden:

def read_field_data(reader):
    if reader.testbyte(0x1b):
        reader.readbyte()
        size = reader.readdword()
        return reader.readbytes(size)
    return reader.readtoseperator(b"\x1e")

Na deze wijziging toonden de samples de verwachte relaties: de kolom voor identifiers bevatte identifier-achtige waarden, de naamkolom bevatte menselijke namen, en zo verder.

Resultaat

De uiteindelijke CSV-conversie was een combinatie van:

  • Het open-source cronodump-pakket.
  • Ideeën gerelateerd aan het ambiguous-KOD werk in pull request #22.
  • Statistische KOD-reconstructie.
  • Globale toewijzing met behulp van het Hongaarse algoritme.
  • Correcte afhandeling van Cronos v4 inline-extents.
  • Conversie van Windows-1251 naar Unicode.
  • Tracking van fysieke veldposities.
  • Begrensde semantische validatie.