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.
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:
mov sp, x0; ret vagy hasonló
minta). Ettől kezdve a lánc ott fekszik, ahol a támadó szeretné.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.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.
| # | 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ő |
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.
-z noexecstack mellett „ROP nem lehetséges”, pontosan azt a
támadási osztályt hagyja nyitva, amit az NX kikényszerített.RET-útvonalra épül (például a BR Xn-nel záródó
JOP-láncok, vagy a pusztán adatmanipuláló támadások), kimarad a
hatóköréből.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
retA 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:
RET-alapú lánc helyett más útvonal.
Ha egy támadó olyan kódhoz jut, amely aláírt mutatót ad ki neki
(egyfajta signing orákulum), a PAC védelme megkerülhető. Ez az
oka annak, hogy a PAC nem helyettesíti sem a canaryt,
sem az ASLR-t, sem a W^X-et.RET-et.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.
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/a055Vegyü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.oA 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
retUgyanaz 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.
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.
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?
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.
-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.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.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.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.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.
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.
RET-tel záródik, minimális műveletet végez; a
BR Xn/BLR Xn-nel záródó változat a
JOP/COP.ldp … x30, [sp], #16
olvassa a vermet, és lépteti az SP-t, a ret pedig a PC-t az
X30-ból tölti, az SP-t változatlanul hagyva.autiasp/retaa ellenőrzi.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.
PAC*,
AUT*, RETAA/RETAB, valamint a
RET viselkedésének elsődleges forrása.
Ellenőrizve: a link él, de a fejezetszám és a
kiadásbetű az Arm-oldalak közös korlátja miatt nem ellenőrzött.PACIASP/AUTIASP mintázatok hivatalos
kontextusa. Ellenőrizve: aktuális kiadás 2025Q4
(2026-01-23), Release minőségű (csak az SME-vonatkozású részek
Beta).objdump, readelf és a
.note.gnu.property értelmezéséhez.
Ellenőrizve: aktuális kiadás binutils 2.47.-mbranch-protection opciócsalád.
Ellenőrizve: legújabb listázott kiadás GCC 16.2.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is fennmaradnak:
qemu-aarch64 PAuth-támogatása – különösen az
autiasp hibához vezető „merging/poisoning” szemantikája –
build- és CPU-modell-függő.aarch64-linux-musl, ld.lld) ellenőriztük. A
standard buildben a lab_vulnerable
paciasp-pal kezdődött, a lab_leaf csak
bti c-vel (pac-ret+leaf-fel
paciasp-pal). Mindhárom standard objektum
jegyzete BTI, PAC-ot mutatott, a levélfüggvényé is. A saját
objektumokból linkelt ELF jegyzete megmaradt; a jegyzet nélküli
kiegészítő objektummal linkelve eltűnt. A musl futtatókörnyezettel
végzett teljes statikus linkelésnél a végső ELF-ben szintén nem volt
jegyzet, mert a helyettesítő eszközlánc startupfájljai nem hordozzák; a
program ettől függetlenül helyesen futott.-mbranch-protection=standard pontos hatása az Ubuntu
24.04-beli GCC verziójában ellenőrzést igényel. A helyettesítő
eszközlánccal (Clang) ellenőrizve: alapértelmezett célon
hint #25/hint #29 + ret,
Armv8.3-A célon paciasp + retaa
keletkezik.lab-a055-vuln.c fordítói kimenetét a helyettesítő
eszközlánccal (zig cc/Clang, aarch64-linux-musl,
-fno-stack-protector) -O1 és -O2
mellett, -mbranch-protection=none és standard
beállítással is megnéztük. Mind a négy esetben megjelent a
strb-ciklus, az stp/ldp x29, x30
és a bl lab_sink, a standard buildben pedig a
paciasp/autiasp. A lab_sink
nélküli korábbi változat ugyanezekkel a beállításokkal puszta
ret-re (PAC-kal bti c; ret-re) egyszerűsödött,
tehát paciasp sem keletkezett benne. A két statikus build
az Unicorn-alapú emulátorban a
lab_sink: 10 bajt: "labor a055" sort írta ki. A GNU GCC
kimenete (keretrekord helye, ciklusforma) a hallgatói laborban
ellenőrizendő.ldp x29, x30, [sp], #16; ret)
regiszterhatását a helyettesítő eszközlánccal (Unicorn AArch64-emuláció,
utasításonkénti nyomkövetés) mértük, nem GNU/qemu alatt: az
ldp az X30-at a [SP+8]-ról tölti, és SP-t
16-tal növeli; a ret előtt és után az SP azonos, a PC az
X30 értékét kapja; az X30-at újra nem töltő
add x0, x0, #1; ret gadget önmagára ugrik vissza.