14.3 ROP-lánc felismerése és miértje

14.3.1 Motiváció

Egy biztonsági audit első kérdése nem az, hogy „hogyan törjük fel a rendszert”, hanem az, hogy a támadónak milyen képessége marad, miután a védelmi rétegeink átengedték. A Védelmi mechanizmusok szakaszban láttuk, hogy az NX/DEP az egyetlen valódi megelőzés az újonnan injektált gépkód futtatása ellen, és hogy ez önmagában nem elegendő: a támadó a program saját, már jelen lévő, végrehajtható kódját használhatja fel. Az a technika, amely ezt rendszerezetté teszi, a return-oriented programming (ROP); a jelen alfejezet a felismerés és a kódreview eszközkészletét adja: mi a gadget, mi a lánc, és pontosan melyik védelmi réteg melyik lépést nehezíti meg. Ez az audit-készség a Verem-túlcsordulás mechanizmusa assembly szinten mechanizmusára épül, és a Biztonságos assembly- és C-interop gyakorlat szakaszban folytatódik.

A ROP-ismeret nem exploit-építési tudás, hanem defenzív elemzési készség: annak eldöntése, hogy egy adott kódbázisban van-e olyan primitív, amely a láncot elindíthatja, és hogy a védelmi rétegek mely része zárja le melyik lépcsőt.

14.3.2 Definíció és működés

ROP-gadget: olyan rövid, általában 2–6 utasításból álló gépi kódrészlet egy létező, végrehajtható memóriaterületen (a program saját szövegszegmensében vagy betöltött megosztott könyvtárban), amely egy meghatározott minimális műveletet végez, és RET-tel zárul. A „hasznos” rész jellemzően: regiszterbe töltés, regiszter-regiszter másolás, egyszerű aritmetika, vagy a veremmutató (SP) átállítása.

ROP-lánc: gadget-címek és a hozzájuk tartozó adatkonstansok sorozata a veremben, úgy elrendezve, hogy minden gadget a záró RET előtt a veremből az X30-ba tölti a következő gadget címét (és közben lépteti az SP-t), a RET pedig erre a regiszterben lévő címre ugrik. A verem lesz tehát a „program”, a gadget-címek pedig a „programutasítások”. A gépi kód, amit a processzor végrehajt, változatlanul a program saját, hiteles részeiből származik – ez az egész technika lényege.

JOP és COP: ha a gadget nem RET-tel, hanem regiszteres ugrással (BR Xn) zárul, a technika neve jump-oriented programming (JOP); a BLR Xn-nel záródó változaté call-oriented programming (COP). Ezeknél a következő cél nem a visszatérési útvonalon (X30) érkezik, hanem egy tetszőleges regiszterben, a láncot ezért jellemzően egy külön dispatcher gadget lépteti. A ROP, a JOP és a COP együtt a kód-újrafelhasználó (code-reuse) támadások családja; ez az alfejezet a ROP-ra összpontosít, a JOP/COP-ot csak ott említi, ahol a védelmek hatóköre eltér.

Miért működik mindez? Az A64 operandus nélküli RET (azaz RET X30) regiszteres visszatérés: a PC új értéke az X30 (LR) tartalma. A RET önmagában nem olvas memóriát, és az SP-t sem változtatja. Ez lényeges eltérés az x86 ret-jétől, amely a verem tetejéről veszi a címet, és lépteti a veremmutatót. A64-en a láncolást ezért nem a RET, hanem egy előtte álló betöltés valósítja meg: az az utasítás, amely a veremből az X30-ba tölti a következő gadget címét. A tipikus ilyen chain driver a szokásos epilógus:

// Chain driver - jellemző epilógus, amely a lánc egyik "lépcsője"
ldp x29, x30, [sp], #16   // (1) veremolvasás: X29 <- [SP], X30 <- [SP+8]
                          // (2) léptetés: SP <- SP + 16 (post-index visszaírás)
ret                       // (3) ugrás: PC <- X30; az SP és a memória változatlan
Lépés Utasítás Memória SP Regiszter / PC
(1) veremolvasás ldp olvassa [SP]-t és [SP+8]-at – X29 ← [SP], X30 ← [SP+8] (a következő gadget címe)
(2) léptetés ldp post-index #16 – SP ← SP + 16 –
(3) ugrás ret nem olvas változatlan PC ← X30

Az SP-t tehát egyedül az ldp post-index alakja változtatja meg; a ret előtti és utáni SP azonos. Az új célcím a veremből származik, de az ldp hozza át az X30-ba, és a ret már csak a regiszterből olvassa. Ha a lánc egyes lépcsői így néznek ki, a verem 16 bájtos [kísérő_adat][next_LR] párok ismételt sorozata lesz: az alacsonyabb címen lévő szó az X29-be, a magasabb címen lévő az X30-ba kerül.

Ebből egy A64-specifikus következmény is adódik: az a gadget, amely nem tölti újra az X30-at (például add x0, x0, #1; ret), nem lépteti a láncot. A ret ugyanarra az X30-ra ugrik, ahonnan a gadget indult, vagyis önmagára tér vissza. Ugyanezt a hibát mutatja be a Hívás és visszatérés alfejezet mentés nélküli RET-je. A64-en tehát a lánc csak akkor halad, ha a gadget hasznos részét X30-at betöltő epilógus követi. A támadó nem futtat új kódot, csak meglévő gépi sorozatokat használ fel – ezért lett a ROP az NX/DEP utáni domináns kontrolláram-támadási osztály.

Defenzív szempontból a lánc mindig négy fázisra bontható. Az auditnak ezeket a fázisokat egyenként kell lezárnia:

  1. Kezdeti hijack. Kell lennie egy első pontnak, ahonnan a vezérlés eltéríthető. Az a053 szakaszban láttuk: a mentett visszatérési cím felülírása a leggyakoribb primitív, de függvénymutató vagy hívóvisszatérési slot is ugyanide vezethet.
  2. Stack pivot (opcionális). Ha a lánc hosszabb, mint a hijack-puffer, egy pivot gadget átállítja SP-t a támadó által ellenőrzött területre (pl. mov sp, x0; ret vagy hasonló minta). Ettől kezdve a lánc ott fekszik, ahol a támadó szeretné.
  3. Gadget-feltérképezés. A binárisban minden RET-et megelőző néhány utasítás jelölt gadget; az ipari eszközök jellemzően több ezer kandidátust találnak egy tipikus dinamikusan linkelt Linux-binárisban.
  4. Lánc-összeállítás. A gadget-címek és adatok fontossági sorrendbe állítása konkrét, egymással összefüggő veremképbe.

A fázisok Turing-teljes számítást engednek meg: elég gadgetből tetszőleges műveletsor kirakható. Audit szempontból ez azt jelenti, hogy egyetlen kontrolláram-eltérítési primitív már elegendő egy súlyos következményhez; nem kell közvetlen injektálás.

Az alábbi ábra egy tipikus dinamikusan linkelt AArch64 bináris gadget-kategóriáinak illusztratív, hipotetikus megoszlását mutatja. Az értékek nem mérések, csak azt érzékeltetik, hogy melyik kategóriából szokott sok kandidátus lenni.

Illusztratív, hipotetikus gadget-kategória megoszlás - Hipotetikus, illusztratív értékek, nem tényleges mérés. Kategóriák: 1=LDP/STP epilógusok, 2=regiszter-töltés, 3=aritmetika/logika, 4=SP-átállítás (pivot), 5=BR/BLR-rel záródó (JOP/COP). Valós szám toolchain-, optimalizálási szint- és könyvtárfüggő.
# Kategória Tipikus alak Szerep az elemzésben
1 LDP/STP epilógus ldp x29, x30, [sp], #16; ret chain driver, a lánc „motorja”
2 Regiszter-töltés ldr x0, [x0]; ret argumentum-előkészítés
3 Aritmetika/logika add x0, x0, #1; ret értékmanipuláció
4 SP-átállítás mov sp, x0; ret pivot a támadó bufferébe
5 Branch-to-register (JOP/COP, nem ROP) ldp x16, x17, [sp], #16; br x16 a RET-hitelesítést (PAC-RET) nem érintő útvonal, ha elérhető

14.3.3 A védelmi rétegek hatása a ROP-lépésekre

Fontos tisztázni, melyik mechanizmus melyik fázist zárja. A teljes rendszerezést lásd a054-ben.

Mechanizmus Melyik ROP-lépést érinti Hatásmechanizmus
NX/DEP a kód-injektálás útját lezárja nem-végrehajtható adatoldal; pontosan emiatt kényszerül a támadó ROP-ra
Stack canary a kezdeti hijack-et detektálja mentett LR előtt elhelyezett őrérték; csak folytonos, őrértéket elérő túlírás esetén szólal meg
ASLR a gadget-cím feloldását nehezíti a szövegszegmens báziscíme véletlen; a láncépítéshez cím-szivárgás kell
PAC a visszatérési cím manipulálását nehezíti a mentett LR kriptográfiai címkét tartalmaz, a RET-hez tartozó változat ellenőrzi

Két dolgot érdemes kiemelni.

14.3.4 PAC hatása konkrétan

AArch64-en PAuth-támogatással a függvény-prológus és -epilógus így nézhet ki (a szintaxist és a szemantikát lásd az AAPCS64 szerződésében):

// PAC-kompatibilis prológus (ARMv8.3-A PAuth)
paciasp                        // X30 aláírása SP-vel mint modifierrel
stp x29, x30, [sp, #-16]!      // mentés
// ... függvénytest ...
ldp x29, x30, [sp], #16        // visszatöltés
autiasp                        // az aláírás ellenőrzése; hiba esetén "merge-elt" X30
ret

A paciasp/autiasp (illetve a kombinált retaa/retab) gondoskodnak róla, hogy a veremben tárolt LR-hez tartozó PAC-mező érvényes legyen. Ha a támadó a mentett LR-t gadget-címre cseréli, az autiasp érvénytelen címkét kap. Az architektúra szabályai szerint a hibás autentikáció rontott (merge-elt) mutatót ad vissza, és az azt követő ret nem a gadgetre, hanem fault-ra vezet. A ROP-lánc első lépcsője (mentett LR → gadget-cím) itt elakad. (FEAT_FPAC jelenlétében már maga az autiasp kivételt vált ki, nem csak a rákövetkező ret.)

Mindez csak akkor igaz, ha a PAuth ténylegesen működik, és hogy mi történik, ha nem, az az utasítás alakjától függ. A fenti paciasp/autiasp a HINT-térbe kódolt alak (HINT #25/HINT #29): Armv8.3-A előtti, FEAT_PAuth nélküli magon, illetve ha a kernel nem engedélyezte az APIA kulcsot, NOP-ként fut, a függvény hibátlanul visszatér, és a láncot semmi nem állítja meg. A retaa/retab (és a braa, blraa, ldraa, valamint a regiszteroperandusos pacia/autia) nem HINT-kódolású: PAuth nélküli magon definiálatlan utasításként SIGILL-t okoz. Ezért az alapértelmezett célra fordított kód a HINT-alakot használja, retaa-t csak legalább Armv8.3-A célra fordítva kapunk. A védelem meglétéhez három külön ellenőrzés kell (lásd a Védelmi mechanizmusok alfejezet kidolgozott példájának 4. lépését): a kód aláír-e (objdump), a hardver támogatja-e a címhitelesítést (HWCAP_PACA; a HWCAP_PACG a generikus PACGA-t jelzi, a visszatérési címhez nem elég), és a kernel engedélyezte-e a kulcsot (PR_PAC_GET_ENABLED_KEYS).

A védelem korlátai is audit-tények:

A tanulság: a PAC megdrágítja, de nem akadályozza meg a ROP-ot. Konkrétan a lánc első lépcsőjét nehezíti meg; a többi fázisra nem hat.

14.3.5 Kidolgozott példa: lánc és miért törik el PAC-cal

Munkakönyvtár: ~/arm-labor/a64/a055/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/a64/a055
cd ~/arm-labor/a64/a055

Vegyünk egy szándékosan sebezhető laborprogramot (a laborban csak a saját, lab- előtagú objektumainkon dolgozunk). A vizsgált függvény külön fájlba kerül, és a pufferét egy másik fordítási egységben definiált lab_sink olvassa. Erre két okból van szükség. Ha a puffer tartalmát senki nem olvassa, az optimalizáló a tárolásokat, sőt az egész törzset elhagyhatja: a lab_sink nélküli változatból már -O1-en is puszta ret lesz. Másodszor, a lab_sink hívása teszi a függvényt nem-levél függvénnyé, amely menti az LR-t. A -mbranch-protection=standard visszatérésicím-aláírása (pac-ret) alapértelmezésben csak ilyen függvényekre vonatkozik. Levélfüggvényre csak a pac-ret+leaf beállítással terjedne ki, ezért a lab_sink nélküli változatban nem keletkezne paciasp.

/* lab-a055-vuln.c — a vizsgált függvény; a puffert egy másik fordítási egység olvassa */
extern void lab_sink(const char *buf, unsigned long n);

void lab_vulnerable(const char *input, unsigned long len)
{
    char buf[16];
    unsigned long i;
    for (i = 0; i < len && input[i] != '\0'; i++)
        buf[i] = input[i];     /* hosszellenőrzés nincs: len > 16 esetén túlír */
    lab_sink(buf, i);          /* a puffer tartalma megfigyelhető: a tárolások maradnak */
}

A ciklus a lezáró nullánál is kilép. Emiatt a fordító nem alakítja memcpy-hívássá, így a bájtonkénti tárolás közvetlenül látszik a disassemblyben.

/* lab-a055-main.c — hívó és lab_sink; külön fordítási egység, ezért a
 * lab-a055-vuln.c fordításakor a lab_sink nem ágyazható be (LTO nélkül) */
#include <stdio.h>

void lab_vulnerable(const char *input, unsigned long len);

void lab_sink(const char *buf, unsigned long n)
{
    printf("lab_sink: %lu bajt: \"%.*s\"\n", n, (int)n, buf);
}

int main(void)
{
    lab_vulnerable("labor a055", 16);   /* 10 bájt + '\0': belefér a 16 bájtos pufferbe */
    return 0;
}

A bemenet szándékosan rövid: a labor a keret és a védelem felismeréséről szól, nem a túlírás kiváltásáról.

# target: cross-build x86_64 hoston, futtatás qemu-aarch64 user-mode-ban
# -fno-stack-protector: a canaryt a Védelmi mechanizmusok alfejezet tárgyalja; itt csak a PAC-
# hatást nézzük
# a -mbranch-protection mindig explicit, mert egyes disztribúciók GCC-je alapértelmezésben
# bekapcsolja
CF="-O2 -fno-stack-protector"                 # -O1-gyel is ugyanez a minta várható
aarch64-linux-gnu-gcc $CF -mbranch-protection=none \
    -c -o lab-a055-vuln-none.o lab-a055-vuln.c
aarch64-linux-gnu-gcc $CF -mbranch-protection=standard \
    -c -o lab-a055-vuln-pac.o lab-a055-vuln.c
aarch64-linux-gnu-gcc $CF -c -o lab-a055-main.o lab-a055-main.c
aarch64-linux-gnu-gcc -static -o lab-a055-none lab-a055-vuln-none.o lab-a055-main.o
aarch64-linux-gnu-gcc -static -o lab-a055-pac  lab-a055-vuln-pac.o  lab-a055-main.o
qemu-aarch64 ./lab-a055-none                  # várt kimenet: lab_sink: 10 bajt: "labor a055"
qemu-aarch64 ./lab-a055-pac                   # ugyanez
aarch64-linux-gnu-objdump -d lab-a055-vuln-none.o
aarch64-linux-gnu-objdump -d lab-a055-vuln-pac.o

A disassemblyben négy dolgot keresünk: a bájtonkénti tárolást (strb) a veremben lévő pufferbe; a keretrekord mentését és visszatöltését (stp/ldp x29, x30); a bl lab_sink hívást; valamint a PAC-os buildben a paciasp/autiasp párt (régebbi objdump alatt hint #25/hint #29). Ha ezek közül bármelyik hiányzik, a fordító mást optimalizált, mint amit a labor feltételez, és a további elemzés nem erre a forrásra vonatkozik.

Az alábbi két lista megfigyelés, nem előírás. A helyettesítő eszközlánc (zig cc/Clang, aarch64-linux-musl, -O2; -O1-en betűre ugyanez) lab_vulnerable-re adott kimenetét közli, a címeket elhagyva és megjegyzésekkel kiegészítve. A GNU GCC kimenete ettől eltérhet, különösen a keretrekord helyében (lásd lent). Kiemelt utasítássorozatról van szó, ezért nincs benne .cfi_* direktíva; a fordító a forrásszintű kimenetben (-S) a CFI-t maga generálja. Az alfejezet többi, kézzel írt gadget-részlete szemléltetés, nem C-ből hívható függvény.

Build A – PAC nélkül (-mbranch-protection=none), megfigyelt kimenet:

lab_vulnerable:
    sub   sp, sp, #32
    stp   x29, x30, [sp, #16]      // keretrekord: X29 -> [SP+16], X30 (LR) -> [SP+24]
    add   x29, sp, #16
    cbz   x1, 2f
    mov   x8, x1
    mov   x1, xzr                  // i = 0
    mov   x9, sp                   // buf = SP+0 .. SP+15
1:  ldrb  w10, [x0, x1]
    cbz   w10, 2f                  // lezáró nulla: kilépés
    strb  w10, [x9, x1]            // buf[i] = input[i]; i >= 16 esetén a keretrekordot írja
    add   x1, x1, #1
    cmp   x8, x1
    b.ne  1b
    mov   x1, x8
2:  mov   x0, sp
    bl    lab_sink                 // nem-levél függvény: ezért mentette az LR-t
    ldp   x29, x30, [sp, #16]
    add   sp, sp, #32
    ret                            // nincs autentikáció

Itt a mentett LR egyszerűen egy memóriaszó a [SP+24] címen, és a puffer (SP+0…SP+15) alatta fekszik. Az a053 szerinti, növekvő címek felé haladó túlírás ezért a 17. bájttól a mentett X29-et, a 25.-től a mentett LR-t éri el. Ez a megfigyelt keret. A GCC a keretrekordot gyakran a keret aljára teszi (stp x29, x30, [sp, #-N]!, a puffer fölötte; vö. a Védelmi mechanizmusok vázlatával), és akkor a túlírás nem a saját, hanem a hívó mentett LR-jét éri el. Hogy a saját buildben melyik eset áll fenn, azt mindig a saját objdump-kimenetből kell kiolvasni.

A verem képe a ret előtt (Build A, a fenti megfigyelt keret alapján):

Offszet (SP-hez képest) Szerep Kitöltés
+0 buf[0..15] a bemenet bájtjai (strb)
+16 mentett X29 a hívó keretmutatója
+24 mentett X30 (saved LR) (érintetlen esetben visszatérési cím; túlírás esetén a lánc első bejegyzése)
+32 hívó stack frame érintetlen

Build A esetén az elemző azt látja, hogy a mentett LR egy sima, hitelesítés nélküli memóriaszó. A lánc innen koncepcionálisan indítható: a +24 slotba egy pivot vagy chain-driver gadget címe kerülne, és a folytatás a +32-től induló pufferekben kapna helyet.

Ez a szerkezet fogalmi/illusztratív. Egy konkrét, működő payload összeállítása nem célja ennek a szakasznak, és a könyv más szakaszaiban sem szerepel receptként.

Build B – PAC-kal (-mbranch-protection=standard), megfigyelt kimenet; a Build A-tól eltérő sorok:

lab_vulnerable:
    paciasp                        // X30 aláírása, modifier: a belépéskori SP
    sub   sp, sp, #32
    stp   x29, x30, [sp, #16]      // már az aláírt X30 kerül a [SP+24] slotba
    // ... ugyanaz a ciklus és bl lab_sink, mint a Build A-ban ...
    ldp   x29, x30, [sp, #16]
    add   sp, sp, #32
    autiasp                        // ellenőrzés ugyanazzal az SP-vel, mint az aláíráskor
    ret

Ugyanaz a +24 slot itt aláírt érték. Ha a támadó ezt gadget-címre cseréli, az autiasp a címhez tartozó címkét ellenőrzi – és mivel a támadó nem ismeri a megfelelő PAC-kulcsot és az SP-alapú modifier kontextust – az ellenőrzés elbukik, és a ret nem a gadgetre ugrik, hanem fault-ot vált ki. A lánc első lépcsője tehát itt elakad, hacsak a támadó nem talál olyan primitívet (pl. BR Xn egy már hitelesített mutatóból), amely megkerüli a RET-hitelesítést.

Ez a különbség jól mutatja, hogy a mentett LR egyetlen memóriaszó pozíciót vesz igénybe, és hogy az NX + PAC együtt már komoly akadályt állít – de nem oldja meg a problémát önmagában.

14.3.6 Hibák és ellenpróbák

Hibás feltevés 1: „NX miatt nincs ROP.” Ellenpróba: az NX pontosan az injektálást zárja le; a ROP a meglévő kódot használja. Építs egy lab-binárist -z noexecstack-kel, listázd a szövegszegmensben a ret-et megelőző rövid sorozatokat (objdump -d | grep -B2 ret), és számold meg őket. Ha nem nulla, akkor van támadási felület, csak a támadás „hordozója” más.

Hibás feltevés 2: „PAC miatt nincs ROP.” Ellenpróba: a PAC csak a hitelesítő epilógust (autiasp + ret, illetve retaa) végrehajtó visszatérést hitelesíti, a sima ret nem; a BR Xn-nel záródó JOP-láncok és a data-only manipulációk hatókörén kívül esnek. A logikailag korrekt állítás az, hogy a PAC megnehezíti a mentett LR manipulálását, nem hogy kizárja a ROP-ot.

Hibás feltevés 3: „a canary kivédi a ROP-ot.” Ellenpróba: a canary csak detektál, és csak folytonos, az őrértéket elérő túlírás esetén. Nem akadályozza meg a mentett LR olvasását, sem a lánc más ponton (pl. függvénymutató cseréjével) való indítását.

Hibás feltevés 4: „az ASLR megoldja a problémát.” Ellenpróba: az ASLR entrópiája konfigurációfüggő, és egyetlen kiszivárgott cím (pl. egy formázott printf, egy részleges visszaírás, egy hibás hibakezelési útvonal) az egész réteget deaktiválhatja.

14.3.7 Feladatok

1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Fogalmazd meg 4–6 mondatban, miért volt az NX/DEP bevezetése után elkerülhetetlen technikai irányváltás a kód-újrafelhasználás (ROP) felé. Térj ki arra, mi az a primitív, amit az NX nem érint, és miért elég ez egy lánc elindításához.

2. (implementációs/elemző, középszint — cél: rutinépítés) Készíts egy rövid lab-C programot (lab-rop-scan.c), amelyben néhány függvény és egy lokális tömb szerepel. Fordítsd -O1 és -O2 opciókkal, majd objdump -d-vel keresd meg azokat az utasítássorozatokat, amelyek ret-tel zárulnak és legfeljebb 4 utasításból állnak. Készíts táblázatot: hány ilyen mintázat van -O1-ben és -O2-ben? Indokold a különbséget.

3. (mérési/debug, középszint — cél: rutinépítés) A kidolgozott példa lab-a055-vuln.c-jét (nem-levél függvény: a lab_sink hívása miatt menti az LR-t) és egy egysoros levélfüggvényt (lab-a055-leaf.c: unsigned long lab_leaf(unsigned long x) { return 3 * x + 1; }) fordítsd le -mbranch-protection=none, illetve -mbranch-protection=standard opcióval. Az ellenőrzést öt, egymástól független rétegre bontsd: (1) mit kér a fordító policyje; (2) mi áll ténylegesen a két függvény prológusában és epilógusában (objdump -d); (3) mit jelez az egyes objektumok .note.gnu.property jegyzete (readelf -n); (4) mit jelez a linkelt ELF jegyzete, ha a saját objektumok mellé egy jegyzet nélküli, kézzel írt kiegészítő objektumot is linkelsz (például egyetlen ret-tel záródó lab-a055-nonote.s-t); (5) támogatja-e a futtatókörnyezet a címhitelesítést. Melyik réteg eredményéből következik a többi?

14.3.8 Megoldások

1. megoldás — Segítség. Gondold át, mit zár ki pontosan az NX (adatterület végrehajtásának tiltása), és mi az a primitív (kontrolláram-eltérítés), ami egy memóriahibából önmagában még az NX ellenére is adódik.

1. megoldás — Teljes megoldás. Az NX/DEP a nem-végrehajtható adatlapokkal kizárja, hogy a program egy támadó-ellenőrzött puffert gépkódként futtasson. A támadónak azonban továbbra is szüksége van vezérlésátadásra: ha egyetlen memóriahiba (mentett LR felülírása, függvénymutató-felülírás) megvan, a vezérlés eltéríthető. Mivel új kódot nem tud befecskendezni, a program saját szövegszegmensében keres rövid, hasznos utasítássorozatokat. A64-en a RET regiszteres visszatérés: a PC-t az X30-ból tölti, a veremhez nem nyúl. Ha azonban a gadget egy olyan epilógussal zárul, mint az ldp x29, x30, [sp], #16; ret, akkor az ldp a támadó által írt veremből tölti az X30-ba a következő címet, és lépteti az SP-t, a ret pedig erre a címre ugrik. Így a támadó a verem tartalmával választja ki a következő végrehajtandó címet, és így láncolhatók a gadgetek. Az NX tehát nem érintette azt a primitívet (vezérlés-eltérítés), amelyre a ROP épül; csak a támadás hordozóját változtatta meg.

2. megoldás — Segítség. Gondold át, hogy a magasabb optimalizálási szint inline-olása és közös-kódrész-kiemelése tipikusan növeli vagy csökkenti a különböző, ret-tel végződő rövid utasítássorozatok számát egy adott bináris méretéhez képest.

2. megoldás — Teljes megoldás. Tipikus megfigyelés: -O1-en a függvények egyszerűbbek, kevesebb a beágyazás, ezért a ret-tel záródó 4 utasításos minták száma alacsonyabb, de a minták tisztábbak, könnyebben azonosíthatók. -O2-n a beágyazás, a ciklus-átalakítás és a közös kód-rész kiemelése növelheti a ret-ek számát és az epilógusok szórását, így a jelölt gadgetek száma is nőhet. A pontos szám erősen függ a GCC verziójától, a forrásmérettől és a hívási gráftól; a laboreredményt jelölten kell rögzíteni, nem általánosítani. Ellenőrzési igény: a konkrét darabszám ebben a munkafolyamatban nem ellenőrzött.

3. megoldás — Segítség. A standard policy alapértelmezett hatóköre nem minden függvény: gondold át, mi különbözteti meg a lab_vulnerable-t a lab_leaf-től az LR mentése szempontjából. A linkelt ELF jegyzetéről pedig gondold át: ha a linker a bemeneti objektumok feature-bitjeit ÉS-sel vonja össze, mi lesz az eredmény egyetlen jegyzet nélküli objektum mellett?

3. megoldás — Teljes megoldás. Az öt réteg külön kérdés, és egyik sem helyettesíti a másikat.

  1. Policy. A -mbranch-protection=standard a visszatérésicím-aláírást (pac-ret) és a BTI-t kéri. A pac-ret alapértelmezett hatóköre a nem-levél függvények köre, vagyis azoké, amelyek az LR-t elmentik; levélfüggvényekre csak a pac-ret+leaf terjeszti ki.
  2. Függvények. A standard buildben a lab_vulnerable prológusa paciasp-pal kezdődik (régebbi objdump alatt hint #25), az epilógusa autiasp + ret (hint #29). A lab_leaf-ben nincs paciasp: csak egy bti c landing pad, majd a számítás és a ret. pac-ret+leaf-fel fordítva már ott is megjelenik a paciasp. A záró ret helyett retaa/retab csak legalább Armv8.3-A célra fordítva (-march=armv8.3-a) jelenik meg, és az ilyen bináris PAuth nélküli magon SIGILL-lel áll le. A none buildben egyik függvényben sincs PAC-utasítás.
  3. Objektumjegyzet. A standard-dal fordított objektumok .note.gnu.property jegyzete (GNU_PROPERTY_AARCH64_FEATURE_1_AND) a PAC és a BTI bitet jelzi, a lab-a055-leaf.o-é is. A jegyzet tehát azt állítja, hogy az objektum a policy szerint készült, nem azt, hogy minden függvénye aláír. A none objektumban nincs PAC-bit, a kézzel írt lab-a055-nonote.o-ban pedig egyáltalán nincs jegyzet.
  4. Linkelt ELF. A linker a bemeneti objektumok feature-bitjeit ÉS-sel vonja össze: a végső ELF csak akkor kapja meg a PAC-bitet, ha minden linkelt objektum hordozza, a futtatókörnyezet startupfájljait és a statikusan linkelt könyvtárakat is beleértve. A saját, standard objektumokból linkelt ELF jegyzete PAC-ot és BTI-t mutat. Ha mellé linkeljük a jegyzet nélküli lab-a055-nonote.o-t, a végső jegyzet eltűnik, pedig a lab_vulnerable utasításai változatlanul aláírnak. A hiányzó végső jegyzet tehát nem fordítóhiba: azt jelenti, hogy legalább egy bemenet nem deklarálta a tulajdonságot. Ilyenkor a bemeneteket egyenként kell readelf -n-nel végignézni, a kézzel írt assembly fájlokat a megfelelő .note.gnu.property szekcióval kell ellátni, és a startupfájlok, valamint a könyvtárak jegyzetét is ellenőrizni kell.
  5. Futtatókörnyezet. Az előző négy réteg csak azt mutatja, mit tartalmaz a bináris. Hogy a paciasp/autiasp ténylegesen véd-e, vagy NOP-ként fut, az a hardver címhitelesítési képességétől (HWCAP_PACA) és attól függ, hogy a kernel engedélyezte-e a kulcsot. Ezt a Védelmi mechanizmusok kidolgozott példájának 4. lépése szerint kell ellenőrizni. A QEMU oldali PAuth-támogatás verziófüggő.

A következtetés iránya tehát egyirányú: a végső ELF PAC-bitje a függvények szintjén semmit sem bizonyít (a levélfüggvény bit mellett sem ír alá), a hiánya pedig nem bizonyítja, hogy a függvények nem írnak alá. A védelemről csak a policy, a kiválasztott nem-levél függvény tényleges utasításai, a bemenetek jegyzetei és a futtatókörnyezet képessége együtt mondanak valamit.

14.3.9 Rövid összefoglaló

A ROP-gadget egy rövid, RET-tel záródó, meglévő kódrészlet (a BR Xn/BLR Xn-nel záródó változat a JOP, illetve a COP). A ROP-lánc gadget-címek és adatok verembe rendezett sorozata. A64-en a láncot nem a RET lépteti: a RET csak az X30-ra ugrik, és az SP-t nem változtatja. A következő címet a RET előtti betöltés hozza a veremből az X30-ba (tipikusan ldp x29, x30, [sp], #16, amely egyben 16 bájttal lépteti az SP-t). A ROP az NX/DEP bevezetésének következménye: a kód-injektálás ellehetetlenítése után a támadó kód-újrafelhasználásra vált. A PAC a visszatérési cím kriptográfiai hitelesítésével a lánc első lépcsőjét nehezíti meg, de a BR-rel záródó JOP-láncokra és a data-only támadásokra nem hat. Audit szempontból a ROP-ismeret felismerési készség: egyetlen kontrolláram-eltérítési primitív már elég a súlyos következményhez.

14.3.10 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is fennmaradnak: