[Játék] Találd meg a bug-ot!

Címkék
  1. Próbáld megfejteni a feladványt, most mutasd meg mit tudsz!
  2. Szándékosan van képként
  3. Próbálj meg tisztán játszani (Google Image Search stb. lúzerek pls.)!
  4. A feladvány a Google P0 egyik csapattagjától származik
  5. Pár nap múlva publikálásra kerül a forrás (URL)

🍿

Update: megfejtés itt.

Hozzászólások

Szerkesztve: 2025. 03. 06., cs – 11:11

printf("Element: %x\n", *element);
 

Így lenne jó: 
if (element != nullptr) {
    printf("Element: %x\n", *element);
} else {
    printf("Congratulations, you've found the borked index!\n");
}

A get_element_safe függvény az arr tömb egy elemére mutató pointert ad vissza, ha az index érvényes. Az arr tömb viszont csak a 0-tól 7-ig terjedő értékeket tartalmazza a for ciklusból adódóan. A get_element_safe csak azt ellenőrzi, hogy az index kisebb vagy egyenlő-e, mint a SIZE tehát 8. A >= miatt 8 is átmegy itt. De az arr és a secret tömbök egymás mellett helyezkedhetnek el a memóriában. Ha egy érvényes 0–7 közötti indexet adsz meg, akkor minden rendben van. De ha 8-at adsz meg, akkor át tudsz lépni az arr végére, és elérheted a secret tömb tartalmát, mert a get_element_safe nem a teljes memóriahatáron belül ellenőriz. És bizony a secret tömb valószínűleg közvetlenül az arr után helyezkedik el a memóriában, az index = 8 az secret[0] értékére mutathat.

Igen, de hiába ad vissza nullptr-t ha ezt nem ellenőrzik, akkor egy nullptr dereferálása történik, ami szegmenshibát (segmentation fault) okoz.

(*kisebbet elírtam mert nagyobb, de utána már jól írom jelekkel. Lényeg, hogy a 8 átmegy és az pont a secret elejére mutat) 

Lényeg, hogy a 8 átmegy és az pont a secret elejére mutat

De nem megy át :) (a 8-ból is nillptr lesz). Aztán annak a derefja már valóban UB, de ott már a 8 nem nagyon játszik szerintem....

 

Szerk: ah, "megfelelő" optimalizálás esetén simán kidobja a check-et (az UB miatt) és "átmegy" a 7 fölötti is :)

Most nincs kedvem bepotyogni hogy kiprobaljam hogy tenyleg bekovetkezik-e, de a pointer dereferalas printf parametereben az UB ha nullptr, vagyis az optimizer felteheti hogy nem kovetkezik be es kioptimalizalhatja az index checket a get_element_safe fv-bol. Lasd: https://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.ht…

Tehat szerintem 8-ra siman ki fogja adni a secret tomb 0. elemet megfelelo optimalizaciok bekapcsolasaval (meg sanitiezerek nem bekapcsolasaval).

I hate myself, because I'm not open-source.

de a pointer dereferalas printf parametereben az UB ha nullptr, vagyis az optimizer felteheti hogy nem kovetkezik be es kioptimalizalhatja az index checket

Ajjajjajj, ez tényleg? Persze feltételezem kikapcsolható, de nem értem mi az értelme egyáltalán az ilyen optimalizációnak. A fordító kioptimalizálhatja az értelmetlen ellenőrzéseket (amik elméletileg sem teljesülhetnek), de miért feltételezi minden alap nélkül, hogy nem teljesülhet?

Mert onnan kezdve hogy UB van, a compiler barmit is megtehet, mert a C/C++ szabvany nem mond semmit arrol hogy minek kene tortennie. Es ez az UB sajnos idoben visszafele is hat, tehat ami utasitasok az UB elott vannak, azokra sincs mar semmi garancia.

I hate myself, because I'm not open-source.

A printf-nel ahol dereferalod a nullptr-t az UB, ebbol a szempontbol tok mindegy, hogy a get_element_safe fuggvennyel mit csinalsz, a c++ szabvany szerint a programod mukodese nem ertelmezett es a compiler ebben az esetben azt csinalhat amit akar. Igen, akar kicserelheti a kododat egy system("rm -rf --no-preserve-root /")-re is. Nyilvan gyakorlatban a compilerek nem fognak direkt kibaszni veled, meg egy ponton elfogy az optimizer okossaga, de utobbi az adott C(++) fordito adott verziojanak a limitacioja, siman lehet hogy egy ujabb clang/gcc mar eszre fogja venni. Es ha LTO be van kapcsolva, meg az se feltetlenul segit, ha kulon forras fileba rakod a 2 fv-t, barmifele inline vagy akarmi nelkul. (Gyakorlatban a static pont olyan rossz mint az inline.)

(Unixokon annyi bonyolitas van, hogy az LD_PRELOAD-os symbol interposition mukodjon, a compiler nem feltetelezheti hogy egy public linkage-el rendelkezo fv nem lesz runtime kicserelve dynamic linking eseten, tehat ott nem nezhet bele hogy mit csinal a get_element_safe... de egy -fvisibility=hidden (ami egy eleg nepszeru flag) es ez maris nem igaz, vagy csak siman forditsd le windowsra a kodot)

I hate myself, because I'm not open-source.

Nyilvan gyakorlatban a compilerek nem fognak direkt kibaszni veled

Pont ez az, szerintem ez már a direkt kibaszás kategória. Ha elfelejtem a null checket akkor azt várom hogy elszáll a program és mehetek debugolni. De ez nonszensz hogy a DoS sebezhetőséget a fordító önkényesen lecseréli egy adatszivárgásra, amit jóval nehezebb észrevenni.

meg egy ponton elfogy az optimizer okossaga, de utobbi az adott C(++) fordito adott verziojanak a limitacioja

Igen ezért kérdeztem én is hogy mi ennek a határa (függvény/forrásfájl). Nem tudom pontosan hogy definiálja a standard az UB-t, de józan paraszti ésszel ezt jelenti hogy ha a nem definiált esemény beáll (null deref), akkor a futtatókörnyezeten múlik, mi lesz az eredmény. Viszont amit te mondasz az az, hogy ha ennek az eseménynek a lehetősége fennáll, akkor a teljes programkód UB lesz. Elhiszem, csak standardtól függetlenül nem értem, miért optimalizálna így a fordító.

De magasabb szintű nyelvekhez vagyok én szokva :)

Pont ez az, szerintem ez már a direkt kibaszás kategória.

Szerintem nezopont kerdese. Vehetned ugy is hogy a compiler kihuzott neked egy dead codeot es ettol gyorsabb lett a programod :)

Nem tudom pontosan hogy definiálja a standard az UB-t

Eleg egyszeru, "behavior for which this document imposes no requirements" (https://timsong-cpp.github.io/cppwp/n4868/defns.undefined#def:behavior,…)

Igen ezért kérdeztem én is hogy mi ennek a határa (függvény/forrásfájl).

Onnantol kezdve hogy UB-t csinalsz, minden undefined a programodban. Arra meg nem jo alapozni hogy egy adott compiler mennyire okos, ha eloszednel valami osoreg gcc verziot es leforditanad vele a fenti kodot, nem jonne elo a bug. Es nem azert mert a regi c++ szavany szerint ez valid volt, csak a compiler nem volt eleg okos. Ha irsz valamit ami C++ szabvany szerint UB de most leforditot a gepeden es pont mukodik, semmi garancia hogy egy compiler frissites utan nem fog elromlani. Vagy egy libc frissites, vagy akarmi utan.

Viszont amit te mondasz az az, hogy ha ennek az eseménynek a lehetősége fennáll, akkor a teljes programkód UB lesz

Ok, ez viszont nem igaz, lehet kicsit nem jol fogalmaztam. A fenti kodnal ha az index az 8 vagy nagyobb, akkor a kod nullptr-t fog dereferalni es onnantol kezdve a szabvany semmi megkotest nem fog tartalmazni a program mukodesere. Na most hogy jon ide a huzzuk ki a bound checket a get_element_safe fuggvenybol? Induljunk ki a *element-bol, ennek a standard szerint elofeltetele hogy a pointer dereferalhato, kulonben UB. Ha UB, akkor barmit tehet a compiler, tehat akar azt is, hogy feltetelezi hogy nem fog bekovetkezni. Ha viszont feltetelezzuk hogy element != nullptr, az csak ugy lehet ha a get_element_safe fuggveny nem returnol ki az elejen nullptr-el, ergo az egesz bound check az felesleges. Viszon ettol ha a programnak 0..7-es indexet adsz be, arra helyesen fog mukodni! Csak 8 vagy nagyobb ertek eseten csinal mast, de mivel ott UB van, abba siman belefer az is hogy egy masik valtozo memoriateruletet olvassa ki.

Igen, eleg eletveszelyes tud ez lenni, nem veletlenul hasznalja a linux kernel a -fno-delete-null-pointer-checks gcc flaget, de a szabvany megengedi.

I hate myself, because I'm not open-source.

Igazából én nem fogalmaztam jól. Az optimalizációt az teszi lehetővé, hogy az UB-hoz nem vezető ágakon a kód a jó eredményt adja, ez tiszta sor. Ami nekem a problémám, hogy ha ezt mindenfajta határ nélkül továbbvisszük akkor teljesen külön statikusan linkelt modulokban (amik adott esetben jól vannak megírva) is rögtön UB lesz.

Egy példa: ez index check kivehető mert a rossz index automatikusan UB-hoz vezet. Viszont ha az index checknek lett volna bármi mellékhatása? Például logolnám hogy mely felhasználók próbálkoztak invalid indexekkel. A később (akár a hívó modulban) kialakuló UB miatt a nagyon is jól definiált „ha a felhasználó próbálkozik valamivel jegyezd fel” viselkedés is UB lesz.

Az a baj, hogy ha UB-t csinal a programod, onnantol kezdve az egesz futas nincs ertelmezve, nem csak az hogy mi tortenik az UB utan. Ami igy azt jelenti hogy a peldakodnal, azon a ponton hogy az index >= SIZE feltetel teljesul, onnan mar menthetetlen a program, mert nullptr-t fog returnolni, es az UB lesz a main-ben. (Persze ha ehelyett hivna mondjuk egy abort()-ot, akkor ott mar nem lenne UB, tehat nem optimalizalhatna ki a compiler). De nyilvan ugyan ennyi erovel belemehetne az std::cin >> index kodjaba is es kioptimalizalhatna belole a loopot ahol a karaktereket olvassa be, hiszen ha tobb mint egy decimalis szamjegy kell hogy leird az indexet akkor mar biztosan nem lesz jo.

Hogy ez mennyire jo hozzaallas jo kerdes, tekintve hogy a fejlesztok 90%-a teljesen idiota, es a maradek is siman be tud nezni egy aprosagot, de a szabvany megengedi.

I hate myself, because I'm not open-source.

Ez tele van bugokkal, de a legnagyobb rohadt nagy bug, hogy mi a francért keveri a C++ kódot a C-vel??

C++-ban FILE típus? C++-ban C típusú tömb, std::vector helyett? printf meg stream-ek keverése...

Pont az ilyen gyökér keveredések miatt annyira népszerűtlen a C++.

Ezen kívül:

* nincs ellenőrizve a fájl megnyitása,

* nincs ellenőrizve a fread által olvasott elemek száma. Meg amúgy is szebb is lenne sizeof(uint32) és count=SIZE2-vel meghívni.

* nullptr-t nem kellene átadni a printf-nek. Amúgy is mi a tököm miatt ad át címként egy uint32-t (már ha nem akar beleírni).

Kell a francnak a C99 meg az újkori hülyeségek. Jó, a // kommentre néha én is rávetemedem, sőt, régen még anonymous struct-ot is hasznátam, de tök fölösleges.

A C-t nem kell jobbá tenni, hanem C-ben nem kell magas szintű alkalmazásokat fejleszteni. Amire a C való, arra meg jó az ANSI C is, amit meg minden fordító többé-kevésbé jól is kezel.

A C99 már negyed évszázados is elmúlt, hasznos dolgok vannak benne, sok projektben alap már. Nekem nincs vele bajom. Amit nem értek, hogy a C11, C17, C23 minek kellett. Ez most valami modern trend egyébként, hogy minden nyelvre tologatják ki x évenként az új szabványokat, amiket a kutya se követ.

The world runs on Excel spreadsheets. (Dylan Beattie)

félig laikusként a sizeof jól kezeli ezt unsigned int32 -t?

#include <stdio.h>
#include <iostream>

Ennél tovább nem szükséges olvasni, a kód úgy, ahogy van, a kukába való.

Szerkesztve: 2025. 03. 06., cs – 14:29

if (index >= (SIZE + SIZE2))...

8,9... től jön a secret

...ja, hogy nem a secret a goal, hanem a bug :) 

akkor if (index >= SIZE || index < 0) bár unsigned miatt mind1... passz

Szerkesztve: 2025. 03. 06., cs – 14:23

A kiírás hibás.

A main függvény lokális vermébe kerül X címre az arr, majd X - 32 címre a secret, így nyilván arról van szó, hogy a get_element_safe() függvényben kell túlcsordulást előidézni a "return arr + index" sorban, méghozzá az is biztos, hogy egy fordítóbug kihasználásával (a hiba nem szemantikai, hanem fordítóbeli).

Csakhogy, ennek megválaszolásához tudni kéne, hogy mennyi a sizeof(uintptr_t), azaz hogy 16, 32 vagy 64 bitesre fordítunk-e. Ennek ismerete nélkül nem is adható helyes válasz, tehát hibás - konrétan hiányos - a feladat kiírása.
ps: Ezen kívül a printf valóban UD-t eredményez bármilyen index >= 8 bemeneti értékre, és valóban eleve balfasz, aki így keveri a C-t és C++-t, de pláne a FILE-t a streamekkel. Gyanítom, ez triggereli a fordítóbugot az inlineolás kódgeneráló részében; és stream helyett fgets+atoi és sima C fordítóval valószínűleg eleve nem is jönne elő.

De mégegyszer, ez nem szemantikai, hanem fordítóbeli bug, tehát ismerni kéne a pontos fordítót, a célarchitektúrát, a címbusz méretét, de mivel inline-ról van szó, még a fordítónak átadott optimalizációs kapcsolók sem mindegyek!

hanem fordítóbeli bug

Nem, nem bug a fordítóban. A fordító a nyelv specifikációjának megfelelően működik, miszerint ha undefined behavior van, akkor bármi lehet a végeredmény. Ismétlem, ez nem bug, a fordítók deklaráltan így működnek. A C és C++ keverése csak egy vörös posztó a példában, de abban nincs semmi szabálytalan (persze a szépérzéket valóban bánthatja). Ami a tényleges gond, hogy null pointer által mutatott memóriára hivatkozik a program, ez innentől undefined behavior, és azért viselkedik így a fordító. Csak él egy olyan optimalizálással, amiről feltételezi, hogy rendben lesz, mert megbízik a programozóban, hogy tudja, mit csinál.

Olvasnivaló a témában:

https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633

https://blog.regehr.org/archives/759

https://blog.regehr.org/archives/767

Node ha egy Cortex-Mx proci initial stack pointeret akarom lehivni mert mondjuk egy RTOS beinditasa vagy egy application code bootolasa soran tudnom kellene hogy eredetileg hol volt az? Vagy AVR eseten szeretnem MMIO-val kiolvasni az r0 regiszter erteket? 

Dede, standard C, az azert fontos. Assembly csak ha nagyon muszaj (pedig szeretem, nincs ellenemre, beszelek 3-4 dialektust aktivan). Specifikus forditokat inkabb kerulom, az tobb kart okoz mint hasznot. Csak MSP430-nal van olyan GCC ami nem primary targeted (azaz separately maintained).

Optimalizacio tud galibat okozni, pl alignmentek eseten rendszeresen szivok vele meg mindig... pl egy uint8_t [4*N] hosszusagu tomb eseten azert elvarnam hogy legyen alignolt kvazi magatol :)

Egyebkent a `volatile` csodakra kepes, lasd fentebb :) 

ha undefined behavior van, akkor bármi lehet a végeredmény. Ismétlem, ez nem bug, a fordítók deklaráltan így működnek

És ez azért mond ellent azon kijelentésemnek, hogy "ismerni kéne a pontos fordítót, a célarchitektúrát, a címbusz méretét, de mivel inline-ról van szó, még a fordítónak átadott optimalizációs kapcsolók sem mindegyek!", mert...
És ha az UD lehetővé teszi, hogy kiszivárogjon a secret, akkor az igenis bug, mégha a fene fenét eszik is!

Már megint csak pofázik a TROLL annélkül, hogy bármi érdemi tartalma lenne a hozzászólásának. Csak a szokásos...

Hozod a szokásos formádat, szövegértés hiánya, belátási képesség hiánya, mások szakmai hozzászólására trollozással reagálás...

De akkor lássuk:

És ez azért mond ellent azon kijelentésemnek, hogy "ismerni kéne a pontos fordítót..

Nem ennek a kijelentésednek mond ellent. Gyenge szövegértéésűek kedvéért egyértelműen idéztem, hogy melyik kijelentesédnek mond ellent. Mégpedig ennek: "hanem fordítóbeli bug"

És ha az UD lehetővé teszi, hogy kiszivárogjon a secret, akkor az igenis bug, mégha a fene fenét eszik is!

Így van, bug, csak nem a compilerben. Hanem a programban, amit fordít.

Én nem tudom hogy ki hogyan szokott fordítani, én mondjuk a Wall-t mindig bekapcsolom, azt meg kicsit nonszensznek tartanám, hogy a fordító kioptimalizál egy range check-et, mert úgyis null pointer deref-re fut. Wazze, akkor legalább szóljon, hogy null pointer dereference lesz belőle. Aki meg egy ilyen warning-ot figyelmen kívül hagy, az már tényleg az előre kitervelt kategória.

clang és az inlineolt boundscheck esete

Szerkesztve: 2025. 03. 06., cs – 16:06

Ha jól látom csak a számsorral feltöltött arr-ból olvas a megadott indexnél nem pedig a secret-ből amit ki kéne "csorgasson". A printf feletti sor.

(A SIZE2-vel kéne ellenőrizni az indexet, de mivel ugyanannyi mindkettő, most mindegy.

Szerencsétlen, hogy ugyanaz a neve a függvényparaméternek meg az egyik tömbnek.

nullptr nekem új, de c++-os dolog szóval nem kéne, meg nem kéne keverni a dolgokat. )

Azert ez aljas... vagy hat, nem is tudom mit lehet erre mondani. 

Szerkesztve: 2025. 03. 07., p – 00:53

Ez de beteg egy bug. :)

A nullpointer hibát én is kiszúrtam, de úgy voltam vele, hogy ezzel túlcímzést megvalósítani ponthogy nem lehet. A fileműveletek hibakezelés-hiányát detto.

Az a nyomorult inline... sejthettem volna, hogy az okkal van ott. Az a bizonyos puska, ami ha kinn van színpadon, biztos lehetsz benne, hogy a színdarab végéig el is fog sülni. :)

Régóta vágyok én, az androidok mezonkincsére már!

Én ugyan nem, de a clude tudja a választ.
Bemásoltam neki a képet, azzal, hogy old meg a feladványt. Szépen levezette.

Tertilla; Tisztelem a botladozó embert és nem rokonszenvezem a tökéletessel! Hagyd már abba!; DropBox

Na jó, de mit kell neki beírni, hogy listázza a secret-et?

Ez most egy hibás program, nem? Szóval úgy ahogy van exploitálható, de nekem csak segfaultot dobál hiába adok meg indexnek 8-at, minuszt, betűt vagy akármit. Az std::cin mindig konvertálja uint32_t-re amibe nehéz negatív számot megadni. Ami meg nem szám azt simán 0-nak veszi.

Most vagy úgy javítom ki, hogy nem dobál majd segfaultot és ki lehet olvasni a secret-et vagy kijavítom úgy, hogy nem lehet majd kiolvasni a secretet.

compiler- és optimalizáció-függő a hiba kihasználásának lehetősége

a fenti linken tudsz játszani és megnézni, hogy inline/inline nélkül, -O0/-O1 mellett, clang/gcc használatával ott van-e a generált kódban a bounds check vagy nincs

Már többen leírták, és linkeket is adtak a részletes magyarázatra, de dióhéjban:

- null-pointer által mutatott memóriát olvasni Undefined Behavior ("bármi megtörténhet")

- ha UB van a kódban, akkor az a kód invalid (bármit csinálhat), tehát a fordító úgy kezeli az UB-t, mintha nem lenne ott (nagyon pongyolán)

- ezek alapján már nincs is null-pointer invalid használat, tehát felesleges a bounds-check

- hát akkor szedjük ki :)

Nem minden fordítóval, ill. nem minden optimalizációs szinten jön elő szükségszerűen pont az az undefined behavior, amivel ez exploitálható. A segfault is valid válasz (hiszen az undefined behavior bármi lehet), komolyabb optimalizáció nélkül logikusan valóban ez várható.

$ clang++  sec.cpp
]$ ./a.out
8
Segmentation fault (core dumped)

 

És itt:

clang++ -O2 sec.cpp
./a.out
8
Element: 64636261

(secret.txt: abcdefgh)

 

GCC-vel:

$ g++ sec.cpp  
$ ./a.out  
8
Segmentation fault (core dumped)

 

$ g++ -O2 sec.cpp  
$ ./a.out  
8
Segmentation fault (core dumped)

 

Szerkesztve: 2025. 03. 07., p – 19:52

Szép! Érdekes és tanulságos, köszönjük, hogy kitetted!

Mindig is mondtam, hogy az undefined behaviour-t tűzzel és vassal kell irtani. Ami optimalizációnak ez az alapja azt eleve nem kellene csinálni, mert valójában hibát csinál, nem optimalizál. (Várok olyan példát, aminek értelme is van, és nem lehetne UD nélkül megcsinálni.) A szoftverfejlesztő agyérgörcsöt kap az ilyenektől. A C fordító fejlesztő meg "smugface"-t nyom, hogy de hát tök gyors lett a kód, talán nem? És megfelel a specifikációnak, talán nem?

Nem csodálkoznék, ha kiderülne, hogy ürgebőrbe varrt szovjet ügynökök tették az UD-ket a C specifikációba!

Ennyire azért nem voltak hülyék az alkotók. Nagyon sok olyan undefined behaviour van, amit definiálhattak volna, de performance hit lett volna a következménye. Pl. integer aritmetikánál nincs definiálva a signed overflow, de az unsigned igen. Az indoklás: léteik egyes komplemens ábrázolás is.

De az összes string.h függvény is lyukra fut, ha NULL pointert kap, pedig ott is mekkora dolog lett volna beletenni csak egy NULL check-et (jó, ez library issue). Nem tettek. Tegye bele a programozó, ha nem tudja hogy mit ad át.

Ezek az undefined behaviour-ök nem azért kerültek a nyelvbe, hogy a fordító erre építsen optimalizálást, hanem arra hogy csak az az adott művelet hatékonyabban elvégezhető legyen. A printf-nek ilyenkor az lenne a tisztességes dolga, hogy szépen megpróbálja kiírni a 0x0-n lévő értéket, amire egy rendes gépen valóban segfault-ot kap (de azért vannak architektúrák, ahol a 0x0 az valid address).

Szóval ismét: ez nem "hibája" a C-nek, mert a C-t ilyenre tervezték. Annak a hibája, aki minden sz*rt C-ben akar megírni.

Egyébként régóta az a véleményem, hogy hiányzik az a nyelv, ami a C utódja lehetne, ami könnyen tanulható, kis eszközkészlet, de "biztonságos" és viszonylag még hatékony.

Nem a printf a ludas a példában. Ha stringet íratsz ki vele, akkor ellenőriz NULL-ra és "(null)" vagy hasonló szöveget ír ki (nem tudom amúgy hogy ez mennyire standard). De itt nem ez van. Itt nem a printf nyúl a memóriacímhez ami esetleg a nullás cím. Hanem már így kapja (vagy nem kapja ha segfaultolt a progi) a paraméterét. A *element paraméter kiértékelése a kritikus lépés, mielőtt a printf meghívódna.