Technical Article

Delphi PDF-aláíró megerősítése rosszindulatú PKCS#12 fájlok ellen

Amikor aláír egy PDF-et, általában úgy gondol az aláíró kulcsra, mint valami olyanra, amit Ön irányít. Egy Ön által létrehozott .pfx fájlban él, amelyet egy Ön által választott jelszó véd. Az a kód, amely beolvassa ezt a fájlt, inkább tűnik csővezetéknek, mintsem határvonalnak. Ez az intuíció téves abban a pillanatban, amikor a tanúsítvány már nem az Öné. Egy asztali eszköz, amely lehetővé teszi a felhasználónak, hogy bármilyen .pfx-et válasszon, egy feltöltött hitelesítő adatot fogadó szerver, vagy a hálózaton keresztül tanúsítványokkal táplált kötegelt aláíró, mind támadó által befolyásolt bájtokat adnak át egy elemzőnek (parser), mielőtt egyetlen aláírásbájt is létrejönne. A PKCS#12 olvasó ugyanolyan támadási felület, mint egy képdekóder vagy egy betöltő program.

Ez a cikk két olyan valós hibát mutat be, amelyek ebben az olvasóban léteztek, mindkettő az aláíró hitelesítő adatok importálásának útvonalán. Egyik sem egzotikus. Mindkettő ugyanabból a kiváltó okból fakad, amely szinte minden olyan bináris elemzőt érint, amelyet rögzített szélességű egészeket használó nyelven írtak: a fájlból származó hosszúságot vagy darabszámot egy lépéssel jobban megbízzák a kelleténél. Az egyik határontúli olvasáshoz (out-of-bounds read) vezet, a másik pedig egy olyan folyamathoz, amely addig lóg, amíg le nem lövi.

Hová utaznak a bájtok

Egy .pfx importálása egy dokumentum aláírásához nem egyetlen művelet, hanem egy rövid csővezeték, és mindegyik szakasz olyasmit elemez, amit egy támadó írhatott. A konténer egy RFC 7292-ben meghatározott PKCS#12 struktúra, amely az AuthenticatedSafe tasakok egy fészke, egy titkosított burokba csomagolva, amely a privát kulcsot tartalmazza. Beolvasása az ASN.1 bejárását, a jelszóból kulcs származtatását, a visszafejtést, majd a visszanyert RSA kulcs átadását jelenti az aláírást felépítő kódnak.

A HotPDF-ben ezek a szakaszok különálló egységekre (units) képeződnek le. A PKCS#12 konténerlogika a HPDFPFX-ben él. Minden címkét, hosszt és értéket, amelyhez hozzáér, a HPDFASN1-ben található ASN.1 olvasó dekódol. A kulcsszármaztatás és a PBES2 visszafejtés a HPDFCrypt-ben található a PBKDF2HMACSHA256 mellett. Amikor a kulcs visszanyerésre kerül, a HPDFRSA és a HPDFCMS-ben lévő CMS SignedData készítő alakítja át azt a PDF-be ágyazott különálló (detached) aláírássá. A teljes láncot indító nyilvános belépési pont egyetlen hívás.

// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // signature embedded
else
  // signing did not complete
;

A signer.pfx minden bájtja átáramlik a HPDFASN1 és a HPDFPFX egységeken, mielőtt bármilyen kriptográfia történne. Ha ez a két egység nem óvatos azzal kapcsolatban, amit a fájl állít, a downstream kriptográfiának már esélye sem lesz számítani.

Első hiba: egy ASN.1 hossz, amely túlcsordul a védelmen

A DER és BER formátumú ASN.1 minden elemet tag-ként, hosszúságként és ennyi tartalom-bájtként kódol. A hossz az a mező, amelyet meg kell bízni, de ellenőrizni is kell, mert ez mondja meg az elemzőnek, meddig kell olvasnia, és ezt az írta, aki a fájlt előállította. Az X.690 §8.1.3 két kódolást határoz meg. A rövid forma a 0 és 127 közötti hosszt egyetlen bájtba csomagolja. A hosszabb forma, amelyet minden nagyobb értékhez használnak, egy vezető bájtot szán rá, amelynek alsó hét bitje a követő hosszbájtok számát adja meg, majd ennyi big-endian bájt hordozza a tényleges értéket. Négy hosszbájt így akár négy gigabájtot megközelítő tartalomméretet is deklarálhat.

Egy ilyen érték dekódolása után az elemzőnek ellenőriznie kell, hogy a tartalom valóban befér-e a pufferbe, mielőtt megbízna benne. A természetes ellenőrzés annak megerősítése, hogy a jelenlegi pozíció plusz a tartalom hossza nem nyúlik-e túl az adatok végén. Ha ezt a kézenfekvő módon írják le, ahol a pozíciót, a tartalom hosszát és a végösszeget mind 32 bites előjeles egészekként tárolják, a védelem megbukik:

// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

A probléma az összeadással van, nem az összehasonlítással. Amikor a ContentLen közel van a MaxInt-hez (2147483647), a Pos + ContentLen túlcsordul a 32 bites előjeles tartományon, és egy negatív számba fordul át. Egy negatív összeg soha nem nagyobb, mint a Total, így a védelem azt jelzi, hogy minden rendben van, és engedi az elemzőt folytatni egy nagyjából két gigabájtos tartalomhosszal, amelyet a puffer nem tartalmaz. A kár ezután következik be: az olvasó puffert foglal a kért hosszhoz, és másol bele, egy SetLength-et egy Move követ, amely a forrásból olvas. A forrásnak már csak néhány száz bájtja maradt, így a másolás messze túlnyúlik a bemenet végén, ami egy határontúli olvasás (out-of-bounds read), amely a legjobb esetben összeomlást okoz, a legrosszabb esetben pedig a szomszédos folyamatmemóriát szivárogtatja ki az elemzésbe.

Az egyetlen helyes védelem az összehasonlítás előtt kiszélesíti a köztes összeget, így az összeadás nem csordulhat túl abban a típusban, amelyben számítják. A javítás mindkét operandust Int64-re emeli:

// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
if ContentLen < 0 then
  raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
  raise EHPDFASN1Error.Create('content overruns buffer');

Egy Int64 adat típus veszteség nélkül megtartja két 32 bites érték összegét, így az összehasonlítás a valós számot látja, és elutasítja a hamisított hosszt. A különálló, nem-negatív ellenőrzés a ContentLen-en lezárja azt az esetet is, amikor a dekódolt érték önmagában negatívként jelenik meg. A HotPDF-ben ez a védelem a HPDFASN1ParseNode függvényben él, amely létrehozza azt a csomópontot, amelyre az összes többi segédfüggvény épít. Mivel a HPDFASN1Content a méretét (a SetLength és Move műveleteknél) közvetlenül a csomópont tartalomhosszából veszi, a hibás védelmen átjutó csomópont minden belőle származó olvasást megmérgezett volna. A határ javítása a dekódolás helyén az, ami biztonságossá teszi a felette lévő segédfüggvényeket.

Második hiba: fegyverként használt PBKDF2 iterációs szám

A második hiba nem memóriahiba, hanem az, hogy a fájl megmondja a CPU-nak, milyen keményen dolgozzon. A PKCS#12 az RFC 8018-ban meghatározott PBES2 jelszóalapú sémával védi a kulcsanyagát. A PBES2 futtat egy kulcsszármaztató függvényt (itt PBKDF2 HMAC-SHA-256-tal), majd egy rejtjelezőt (itt AES-256-CBC). A PBKDF2 egy iterációs számot vár, és ez a szám a fájlban hordozott paraméter. Ennek a célja az, hogy lassú legyen: a több iteráció azt jelenti, hogy minden jelszótipp többe kerül, ami jó az offline támadókkal szemben. Az RFC 8018 §4.2 kimondja, hogy a nagyobb szám jobb a biztonság szempontjából, és szándékosan nem szab felső határt.

Ez a nyitottság rendben van, ha Ön generálta a fájlt. Viszont fegyver, ha a támadó tette. Az iterációs szám egy támadó által ellenőrzött munkafaktor, a támadó által ellenőrzött munkafaktor pedig egy algoritmus-bonyolultsági szolgáltatásmegtagadásos (DoS) támadás. Egy hamisított .pfx milliárdos nagyságrendű iterációs számot kódolhat; az elemző kötelességtudóan beolvassa azt, és meghívja a PBKDF2-t ennyi HMAC-SHA-256 körre, a folyamat pedig eltűnik egy olyan ciklusban, amely percekig vagy órákig nem tér vissza egyetlen megadott fájl miatt. Egy olyan aláíró szerveren, amely kérésenként egy hitelesítő adatot kezel, egyetlen szerkesztett feltöltés leállít egy munkamenetet.

A darabszám rosszabbá teszi a túlcsordulást, még mielőtt a CPU pörögni kezdene. Az iterációs érték a fájlban ASN.1 INTEGER-ként él, amelynek nincs rögzített szélessége, míg a PBKDF2 by által végül elfogyasztott mező egy 32 bites Integer. Ha a INTEGER-t közvetlenül ebbe a mezőbe dekódoljuk, a nagy érték csonkolódik, az előjelbitre irányított érték pedig negatívként vagy valamilyen nem összefüggő kis számként tér vissza, így még a munka mérete sem az lesz, amit a fájl látszólag kért. A javítás teljes szélességben olvassa be az értéket, és korlátozza azt a szűkítés előtt:

// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node);          // returns Int64
if (LIter < 1) or (LIter > 100000000) then
  raise EHPDFPFXError.CreateFmt(
    'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
    [LIter]);
Iterations := Integer(LIter);                    // safe: already bounded

Az Int64-be történő beolvasás azt jelenti, hogy a dekódolt érték a valós érték, nem pedig annak csonkolt szelleme. Az alsó határ elutasítja a nulla és negatív értékeket, amelyeknek nincs értelmük a kulcsszármaztatásnál. A felső határ, a százmillió, jóval a jogos PKCS#12 fájlok felett van, amelyek ma tízezer és százezer közötti iterációt használnak, miközben a legrosszabb esetet egy korlátozott, túlélhető munkamennyiségre korlátozza. Csak azután, hogy az érték átment ezen a tartományon, szűkül le a 32 bites mezőre, így a csonkolás már nem okozhat meglepetést senkinek. A HotPDF-ben ez a korlátozás a ParsePBES2Params függvényben él, ahol a PBKDF2 paraméterek dekódolódnak a PBKDF2HMACSHA256 felé vezető úton.

Miért ugyanaz a két javítás

A két hiba különbözőnek tűnik (az egyik puffertúlcsordulás, a másik egy lefagyott folyamat), de ugyanaz a hiba. Mindkét esetben egy nem megbízható fájlból származó szám egy lépéssel túl korán került be egy rögzített szélességű típusba, mielőtt összevetették volna a valósággal. A hossz 32 biten lett összeadva a határvizsgálat előtt; az iterációs számot 32 bitre szűkítették a tartományvizsgálat előtt. Mindkettő ugyanarra a fegyelemre épül: dekódolás teljes szélességben, ellenőrzés a valós korlát ellen, és csak ezután történő szűkítés. A köztes Int64 nem stílusbeli választás, ez az egyetlen szélesség, amelyben a védelem láthatja azt az értéket, amelyet a támadó valójában beírt. A túlcsorduló határ nem határ, a felső határ nélküli szám pedig nem paraméter, hanem egy távoli fojtószelep az Ön saját CPU-ján.

Gyakorlati útmutató aláíró csővezetékekhez

A szűkebb tanulság az, hogy a nem megbízható tanúsítványbemenetet ugyanúgy kell ellenőrizni, mint bármely nem megbízható feltöltést. Korlátozza az elfogadott .pfx méretét, mivel egy legális fájl kilobájtos, nem pedig megabájtos nagyságrendű. Kezelje az elemzési hibát rutinszerűen elutasított bemenetként, nem pedig olyan hibaként, amely megér egy veremkövetést (stack trace) a felhasználónak. Ha szerveren végez aláírást, futtassa az importálást ott, ahol egy leállt folyamat nem tudja magával rántani a szolgáltatást, és tegyen időtúllépést (timeout) a művelet köré, hogy a váratlanul drága fájlt a valós idő és az iterációs korlát is korlátozza.

A tágabb tanulság túlmutat a tanúsítványokon. Az elemző megerősítése nem egyetlen egység egyszeri ellenőrzése, hanem minden olyan hely jellemzője, ahol a könyvtár olyan bájtokat olvas be, amelyeket nem ő írt. Egy PDF-könyvtár rengeteg dolgot elemez nem megbízható forrásokból: a dokumentumba ágyazott betűtípusokat, fél tucat kodekben lévő képeket, adatfolyam-szűrőket és az aláírási útvonalon tanúsítványokat. Mindegyik támadási felület, és mindegyik megérdemli ugyanazt a gyanakvást minden hosszúsággal és számmal kapcsolatban. A HotPDF az importálási és aláírási útvonalat az itt leírt megerősített HPDFASN1, HPDFPFX, HPDFCrypt és HPDFCMS egységekre építi, így a hitelesítő adatot, amelyet átad neki, bárhonnan is származik, védelmi szemlélettel elemzi, mielőtt valaha is megbízna benne.

Az aláírási munkafolyamat, amelyet ezek az ellenőrzések védenek, elejétől a végéig megtalálható a PAdES digitális aláírásokról szóló útmutatónkban Delphiben, és ugyanez a védelmi hozzáállás a dokumentumok titkosítására alkalmazva, beleértve a kódbázison osztozó AES-256 kulcsútvonalat, le van írva az AES-256 titkosításról és biztonságról szóló cikkben. Mindez a Delphihez és C++Builderhez készült HotPDF Component részeként érhető el, a blogon máshol tárgyalt betöltési, szerkesztési, titkosítási és aláírási API-k mellett.