14.2 Védelmi mechanizmusok: canary, ASLR, NX/DEP, PAC

14.2.1 Motiváció: a védelem pontos garanciája auditálható állítás

Egy biztonsági audit során ritkán elég annyit mondani, hogy „a bináris védelemmel fordított”. A kérdés mindig az: mit véd, melyik támadási lépést nehezíti, és mi az, amit kifejezetten nem garantál. A gyakorló mérnök számára ez nem elméleti finomság: ha egy kernelmodul vagy beágyazott szolgáltatás fordítói kapcsolóit kell dokumentálni, a „van canary” állítás önmagában megtévesztő lehet, ha a folyamat például nem indít új címtérrel, vagy a hardver nem támogatja a mutatóhitelesítést.

Ebben az alfejezetben négy mechanizmust rendszerezünk: a stack canary-t, az ASLR-t, az NX/DEP-et és az AArch64-specifikus PAC-ot. Mindegyiknél megadjuk a pontos garanciát, és külön kiemeljük a garancia határát. Az előző alfejezet megmutatta, hogyan lesz a mentett visszatérési címből (X30 verembeli másolata) a vezérlésátvétel kitüntetett pontja; itt azt vizsgáljuk, milyen rétegek állnak közte és a RET utasítás között. A keret végig defenzív: a cél a mechanizmusok felismerése és helyes értékelése, nem a megkerülésük receptje. A támadási lépéseket csak annyiban nevezzük meg, amennyiben a védelem hatókörének megértéséhez szükséges.

14.2.2 Definíció: mit jelent egy védelmi mechanizmus garantálni

Védelmi mechanizmus (mitigation): a program végrehajtásának olyan tulajdonsága vagy a fordító/linker/kernel által bevezetett szerkezeti elem, amely egy adott támadási lépés sikerességének valószínűségét csökkenti, vagy annak következményét észlelhetővé teszi.

Három fogalmat érdemes élesen szétválasztani, mert a mindennapi szóhasználat összemossa őket:

Tulajdonság Jelentés Példa
Megelőzés (prevention) a hibás művelet létre sem jön NX: az adatlap nem végrehajtható
Detektálás (detection) a hibás művelet megtörténik, de később kiderül canary: a túlírás észlelhető a visszatérés előtt
Nehezítés (hardening) a siker valószínűsége csökken ASLR: a cím nem ismert előre

Ez a hármas tagolás adja a fejezet gerincét. Fontos következmény: a detektálás nem előzi meg a memóriamódosítást. A canary csak azt jelzi, hogy a puffer túlírása megtörtént; a támadó által beírt adat a memóriában van, legfeljebb a kihasználás nem folytatódik tovább. Hasonlóképp az ASLR nem tilt semmit, csak valószínűséget módosít. A mélységi védelem (defense in depth) elve épp abból indul ki, hogy egyik mechanizmus sem ad abszolút garanciát, ezért egymásra rétegzik őket: a támadónak minden réteget külön kell legyőznie, és ha bármelyik réteg kiesik (például egy nem PASLR-alapú rendszerben), a többi még fog.

14.2.3 A négy mechanizmus és garanciájuk

14.2.3.1 Stack canary: detektálás a visszatérés előtt

A stack canary (veremőr, stack guard) egy véletlen érték, amelyet a fordító a helyi pufferek mellé helyez; a konkrét sorrend a veremkeret elrendezésétől függ. A függvény prológusában beíródik, az epilógusban pedig a RET előtt összehasonlítódik az eredetivel.

Pontos garancia: ha a puffer túlírása folytonosan, egy irányban terjed, és eléri a canary által elfoglalt bájtokat úgy, hogy azokat nem pontosan az eredeti értékkel írja felül, akkor a függvény visszatérése előtt a __stack_chk_fail hibakezelő lefut, és a folyamat hibával leáll.

Amit nem garantál:

A canary nem akadályozza meg a memóriamódosítást; csak a vezérlésátvétel lépését vágja el.

14.2.3.2 ASLR: cím-véletlenítés

Az ASLR (Address Space Layout Randomization) a folyamat egyes memóriaterületeinek (verem, heap, mmap-elt szakaszok, megosztott könyvtárak, PIE esetén maga a programkép) kezdőcímét véletleníti el minden új folyamatindításnál.

Pontos garancia: a támadó számára a célszerkezet címe nem ismert előre; a sikerhez vagy cím-információra van szüksége, vagy nagyszámú találgatásra.

Amit nem garantál:

Az ASLR tehát nehezítés, nem megelőzés; és a védelme részleges, mert egyetlen kiszivárgott cím deaktiválhatja.

14.2.3.3 NX/DEP: nem-végrehajtható adatterület

Az NX (No-eXecute), más néven DEP (Data Execution Prevention) a lapok lapasztal-bejegyzésében jelöli, hogy egy adott memórialapon lévő bájtok adatként értelmezendők, nem utasításként. AArch64-en a könyv Linux-céljában ezt a lapleírók végrehajtási jogosultságbitjei adják. A program az EL0 kivételszinten (végrehajtási szinten) fut; az EL0 és az EL1 címei egyaránt az EL1&0 címfordítási rezsimhez tartoznak, amelynek lapasztalait a kernel kezeli, és az MMU érvényesíti. A lapleíróban két külön bit van: az EL0-beli végrehajtásra az UXN (Unprivileged eXecute Never) vonatkozik, ez a felhasználói program NX-védelme; a PXN (Privileged eXecute Never) a privilegizált, EL1-beli végrehajtást tiltja, például hogy a kernel ne futtasson felhasználói lapon lévő kódot. Fogalmi megjegyzés: az EL0…EL3 kivételszintek, vagyis a jogosultsági szintek, és a táblabejárás L0…L3 szintjei két külön dolog. Az utóbbiak egyetlen címfordítás egymásba ágyazott laptábláinak mélységét jelölik, és nincs „EL0-s laptábla”. A Linux általában W^X (write XOR execute) szabályt követ: egy lap vagy írható, vagy végrehajtható, de nem mindkettő.

Pontos garancia: a veremre, heapre vagy .data szakaszba írt bájtok nem hajtódnak végre. A klasszikus kód-injektálás (beírni a gépi kódot a pufferbe, majd a visszatérési címet a pufferre állítani) ettől meghiúsul: a vezérlés átvevődhet, de az első utasításnál hardveres laphiba keletkezik (SIGSEGV).

Amit nem garantál:

14.2.3.4 PAC: visszatérési cím kriptográfiai jelölése (AArch64)

A PAC (Pointer Authentication) az ARMv8.3-A architektúra-kiterjesztés része, tehát AArch64-specifikus. Lényege: a mutató felső, a virtuális címtérben nem használt bitjeibe egy rövid kriptográfiai aláírás (PAC-mező) kerül, amelyet a processzor egy, az operációs rendszer által beállított kulccsal (jellemzően APIA/APIB az utasítás-mutatókhoz, APDA/APDB az adatmutatókhoz, valamint APGA generikus kulcs) számít ki.

A tipikus pac-ret minta a veremkeretben:

Fenyegetési modell. A garancia egy pontosan körülhatárolt helyzetre szól: a támadó a veremben tárolt, aláírt visszatérési címet módosítja; a program a függvény hitelesítő epilógusán át tér vissza; és a megfelelő kulcs a futtatókörnyezetben aktív (lásd a példa 4. lépését).

Melyik utasítás hitelesít? A disassemblyben három alakot kell megkülönböztetni:

Alak Mit csinál Hitelesít?
AUTIASP (hint #29), utána sima RET az AUTIASP ellenőrzi az X30 aláírását, és sikeres esetben eltávolítja; a RET csak ugrik igen, az AUTIASP
RETAA / RETAB egyetlen utasítás: hitelesít és visszatér igen, maga a RETAA
sima RET, előtte AUTIASP nélkül csak PC ← X30 nem

Pontos garancia: ebben a modellben, ha a támadó a mentett visszatérési címet érvényes aláírás előállítása nélkül megváltoztatja, a hitelesítő utasítás (AUTIASP vagy RETAA) meghiúsul. A processzor ilyenkor a címet jellemzően „mérgezett” értékké alakítja, ami a vezérlésátadásnál azonnali laphibát okoz (FEAT_FPAC esetén már maga a hitelesítés kivételt vált ki). Az ellenőrzést azonban csak az az útvonal végzi el, amely a hitelesítő utasítást ténylegesen végrehajtja. Egy AUTIASP nélküli RET-útvonal nem hitelesít, és egy olyan vezérlésátadás sem, amely nem a hitelesítő epilóguson át megy (például egy BR Xn egy nem aláírt mutatóval). A pac-ret tehát a visszatérési címet védi a szabályos epilógusban, nem az összes közvetett vezérlésátadás célját, és nem általános kontrollfolyam-integritás (CFI).

Amit nem garantál:

Kiegészítő, kapcsolódó hardverelem a BTI (Branch Target Identification), saját feltételekkel. A BTI a BR/BLR közvetett elágazások célpontjait korlátozza a BTI landing pad utasításokra (a paciasp implicit landing padként is szolgál), de a RET célját nem érinti. Csak akkor hat, ha a lap védett lapként (guarded page) van leképezve. Ehhez a FEAT_BTI-t támogató hardver, az ELF BTI-jegyzete minden linkelt objektumban (lásd a ROP-alfejezet 3. megoldását) és az operációs rendszer támogatása kell. A GCC -mbranch-protection=standard kapcsolója a pac-ret és a bti kombinációját kéri. A kettő együtt sem teljes CFI: a közvetett hívások érvényes célpontjai között nem tesz különbséget.

14.2.4 Levezetés: entrópia, és mit jelent a „nehezítés” számszerűen

Feltételek. Tegyük fel, hogy a védelmi mechanizmus NN bitnyi független, egyenletes eloszlású véletlent használ, és a támadó egyetlen „vak” próbálkozást tesz, amelynek sikere pontosan akkor következik be, ha a helyes értéket találja el. (Ez erős egyszerűsítés: a valóságban részleges kiszivárgás, ismételt próbák és a hibás próba következményei is számítanak.)

Ekkor egyetlen próba siker-valószínűsége:

p=2−N p = 2^{-N}

ASLR-entrópia levezetése. Ha egy SS méretű, lapkeretre igazított régiót egy RR méretű tartományon belül véletlenítünk, a lehetséges pozíciók száma legfeljebb R/SR/S, így

N≈log2RS[bit] N \approx \log_2 \frac{R}{S} \quad [\text{bit}]

Példa: 32 bites címtér (R=232R = 2^{32} B = 4 GiB), 8 MiB-os (S=223S = 2^{23} B) veremrégió esetén N≈log229=9N \approx \log_2 2^{9} = 9 bit. Ez lényegesen kevesebb, mint amennyit a „32 bites ASLR” kifejezés sugall — a valós rendszerekben a kernel elrendezése és további korlátok miatt a tényleges érték ettől eltérhet, ezért a helyi gépen kell ellenőrizni.

Az alábbi ábra és táblázat illusztratív, levezetett/irodalmi nagyságrendeket mutat; egyik érték sem tekintendő ebben a munkafolyamatban mért adatnak, és a tényleges érték kernelkonfiguráció-, illetve hardverfüggő.

Illusztratív entrópia-nagyságrendek védelmi mechanizmusonként - Illusztratív, levezetett/irodalmi nagyságrendek: 1=canary (7 véletlen bájt), 2=32 bites verem-ASLR (R=2^32, S=2^23 levezetés), 3=64 bites PIE+mmap ASLR tipikus konfigurációban, 4=PAC-mező reprezentatív értéke. Nem mért adat; a tényleges érték kernel- és hardverfüggő, helyben ellenőrizendő.
Mechanizmus Illusztratív entrópia NN Egy vak próba siker-valószínűsége 2−N2^{-N} Jelleg
Stack canary (8 bájt, alsó bájt kötött 0x00) 56 bit 1,4⋅10−171{,}4\cdot10^{-17} detektálás
Verem-ASLR, 32 bites címtér (levezetve: R=232R=2^{32}, S=223S=2^{23}) 9 bit 2,0⋅10−32{,}0\cdot10^{-3} nehezítés
ASLR, 64 bites PIE + mmap (tipikus konfiguráció) ~28 bit 3,7⋅10−93{,}7\cdot10^{-9} nehezítés
PAC-mező (reprezentatív érték, címtér-függő) ~11 bit 4,9⋅10−44{,}9\cdot10^{-4} megelőzés/detektálás

A levezetés tanulsága. A négy mechanizmus nem ugyanazt a támadási lépést zárja el. Az ASLR alacsony entrópiája önmagában nem elég: 9 bit azt jelenti, hogy néhány ezer próbálkozás matematikailag elég lehet — ezért van szükség arra, hogy a hibás próba összeomoljon (ne lehessen folytatni), és ezért kritikus, hogy egyetlen kiszivárgott cím ne adja meg a teljes elrendezést. A canary magas entrópiája viszont csak a folytonos, felfelé terjedő írás ellen hatásos, és csak akkor, ha a canary értéke nem szivárog ki.

14.2.5 Kidolgozott példa: a canary felismerése a generált kódban és a mechanizmusok állapotának ellenőrzése

Munkakönyvtár: ~/arm-labor/a64/a054/ — 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/a054
cd ~/arm-labor/a64/a054

Cél. Egy adott binárison auditálható módon állapítsuk meg, melyik mechanizmus van jelen, és milyen bizonyíték támasztja alá. Target: aarch64-linux-gnu-gcc + qemu-aarch64 (user-mode), illetve opcionálisan natív AArch64 host.

1. lépés — forrás és build. A vizsgált függvény egy külön fájlban van; a hívó main és a sink definíciója a második fájlba kerül, így a 3–4. lépés futtatható állománya csak a két, itt látható forrásból épül.

/* lab-a054-vuln.c */
#include <string.h>

extern void sink(const char *);

void lab_vuln(const char *in) {
    char buf[32];            /* helyi puffer */
    strcpy(buf, in);         /* határellenőrzés nélküli másolás */
    sink(buf);
}
/* lab-a054-main.c – hívó és sink a lab_vuln vizsgálatához */
#include <stdio.h>

void lab_vuln(const char *in);

void sink(const char *s)            /* a lab-a054-vuln.c extern sink-je */
{
    printf("sink: \"%s\"\n", s);
}

int main(void)
{
    int local = 0;
    lab_vuln("labor a054");          /* 10 bájt + '\0': belefér a 32 bájtba */
    /* betöltési címek az ismételt futtatásos ASLR-vizsgálathoz */
    printf("main:  %p\n", (void *)main);
    printf("verem: %p\n", (void *)&local);
    return 0;
}
# target: cross-build x86_64 hoston, futtatás qemu-aarch64 user-mode-ban
aarch64-linux-gnu-gcc -O1 -fPIE -fstack-protector-strong -c -o lab-a054-vuln.o lab-a054-vuln.c
aarch64-linux-gnu-gcc -O1 -fPIE -c -o lab-a054-main.o lab-a054-main.c
aarch64-linux-gnu-gcc -pie -Wl,-z,relro,-z,now \
    -o lab-a054-vuln lab-a054-vuln.o lab-a054-main.o
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-a054-vuln

A program a sink: "labor a054" sort, majd a main és egy veremváltozó címét írja ki. A bemenet szándékosan rövid: ez a labor a védelmek felismeréséről szól, nem a túlírás kiváltásáról. A 2. lépés a lab-a054-vuln.o objektumfájlt, a 3–4. lépés a linkelt lab-a054-vuln állományt vizsgálja.

2. lépés — a canary-minta felismerése a disassemblyben.

aarch64-linux-gnu-objdump -d lab-a054-vuln.o | sed -n '/<lab_vuln>:/,/^$/p'

AArch64 glibc mellett a globális __stack_chk_guard betöltését keressük; a TPIDR_EL0 + 40 nem hordozható canary-cím. Az alábbi felismerési vázlat kihagyja a függvénytörzset; a fordító konkrét regiszter- és keretkiosztása eltérhet:

    stp  x29, x30, [sp, #-64]!      // keret + mentett LR
    mov  x29, sp
    adrp x9, :got:__stack_chk_guard
    ldr  x9, [x9, :got_lo12:__stack_chk_guard]
    ldr  x10, [x9]                 // a globális guard értéke
    str  x10, [sp, #56]            // buf[32]: sp+24..sp+55; guard: sp+56
    // ... a függvény törzse, benne a strcpy és a sink hívása ...
    adrp x9, :got:__stack_chk_guard // a hívások után újra képezzük a címet
    ldr  x9, [x9, :got_lo12:__stack_chk_guard]
    ldr  x10, [x9]
    ldr  x11, [sp, #56]
    cmp  x10, x11
    b.ne .Lfail
    ldp  x29, x30, [sp], #64
    ret
.Lfail:
    bl   __stack_chk_fail          // detektálás: a folyamat leáll

A felismerési minta a guard betöltése és verembe mentése, majd a visszatérés előtti összehasonlítás és a __stack_chk_fail hibája. A fenti GOT-címzés a globális __stack_chk_guard szimbólumhoz vezet. A pontos utasítássor libc-, fordító- és kapcsolófüggő; egyetlen MRS vagy adott TLS-eltolás hiányából nem következik a védelem hiánya.

3. lépés — ASLR, NX, RELRO, PIE állapotának ellenőrzése.

Az alábbi parancsok egyetlen konkrét állományra, az 1. lépésben linkelt lab-a054-vuln ELF-re vonatkoznak, nem az objektumfájlra. Egyik sor sem bizonyít önmagában semmit; az állítás a fejléc, a szegmensek, a dinamikus bejegyzések és a futásidejű címek együttes értékeléséből születik. A parancsokat két csoportra bontjuk: az első a fájl tulajdonságait olvassa (bárhol futtatható, a host readelf-je is jó), a második a futó vendégprogramot vizsgálja.

# (A) a fájl statikus tulajdonságai – host-eszközzel olvasható
readelf -hW lab-a054-vuln | grep Type                          # EXEC vagy DYN
readelf -lW lab-a054-vuln | grep -E 'INTERP|LOAD|GNU_RELRO|GNU_STACK'
readelf -dW lab-a054-vuln | grep -E 'FLAGS'                    # BIND_NOW, FLAGS_1

# (B) a vendég futásidejű viselkedése – ugyanabban a környezetben kétszer futtatva
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-a054-vuln
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-a054-vuln

# (C) a HOST beállításai – csak a host folyamataira vonatkoznak
cat /proc/sys/kernel/randomize_va_space                        # host: 2 = teljes ASLR
cat /proc/sys/vm/mmap_rnd_bits                                 # host: mmap-entrópia

A helyettesítő eszközlánccal (zig cc/Clang, glibc-cél, ugyanezekkel a kapcsolókkal) linkelt állományon az (A) csoport kimenete:

  Type:  DYN (Position-Independent Executable file)
  INTERP     ...  R    [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
  LOAD       ...  R
  LOAD       ...  R E
  LOAD       ...  RW
  LOAD       ...  RW
  GNU_RELRO  ...  R
  GNU_STACK  ...  RW
 (FLAGS)    BIND_NOW
 (FLAGS_1)  Flags: NOW PIE

Az értékelés tulajdonságonként, a szükséges bizonyítékok együttesével:

Állítás Szükséges bizonyítékok együtt Mit nem jelent önmagában
PIE futtatható állomány Type: DYN és INTERP szegmens (vagy FLAGS_1-ben PIE) a DYN önmagában megosztott könyvtár (.so) is lehet
teljes RELRO GNU_RELRO szegmens és azonnali kötés (FLAGS: BIND_NOW vagy FLAGS_1: NOW) GNU_RELRO BIND_NOW nélkül csak részleges RELRO: a lusta kötésű GOT-bejegyzések írhatók maradnak
a verem nem végrehajtható GNU_STACK RW (nem RWE) csak a verem végrehajthatóságára vonatkozó kérés; a betöltő és a kernel teljesíti, nem a fájl
egyetlen betöltött szegmens sem írható és végrehajtható egyszerre minden LOAD sor jelzője: R, R E vagy RW, egyik sem RWE a futás közbeni mmap/mprotect hívásokat (például JIT) a fájl nem mutatja
a programkép címe futásonként változik két futás main: sora eltér (B csoport) a PIE csak lehetővé teszi a véletlenítést; hogy megtörténik-e, a betöltőn múlik

A (B) és a (C) csoportot nem szabad összemosni. Natív AArch64 gépen a kettő ugyanaz a rendszer: ott a randomize_va_space is a vizsgált program környezetét írja le. Cross-hoston viszont a /proc a host kernel beállításait mutatja, és csak a host folyamataira (köztük magára a qemu-aarch64 folyamatra) érvényes. A vendégprogram betöltési címét a QEMU user-mode betöltője választja ki, ezért a host-beállításból nem következik, hogy a vendég programképe és verme futásonként máshová kerül. A vendégre vonatkozó állítás forrása csak a (B) csoport két futásának összevetése lehet, és ez is csak az adott QEMU-verzióra és kapcsolókra érvényes. A jegyzőkönyvben ezért minden sor mellé oda kell írni, hogy host- vagy vendégmegfigyelés. Ha statikus, fix címre linkelt (Type: EXEC) binárist vizsgálunk, az ASLR a programképre biztosan nem hat, legfeljebb a veremre és az mmap-régiókra.

4. lépés — PAC jelenlétének, hardvertámogatásának és tényleges alkalmazásának ellenőrzése.

Ez a lépés három külön kérdést tesz fel, és mindhárom válaszát külön kell rögzíteni:

Kérdés Bizonyíték Mit nem dönt el
1. A kód kéri-e a PAC-ot? objdump: paciasp/autiasp (régebbi objdump alatt hint #25/hint #29), illetve retaa/retab; readelf -n: GNU_PROPERTY_AARCH64_FEATURE_1_PAC hogy a futtató mag ismeri-e az utasítást
2. A hardver támogatja-e? getauxval(AT_HWCAP): HWCAP_PACA hogy a kernel bekapcsolta-e a kulcsot ennél a folyamatnál
3. Érvényesül-e a futó folyamatban? prctl(PR_PAC_GET_ENABLED_KEYS): az APIA kulcs engedélyezett hogy a program minden függvénye aláírt-e (ezt az 1. pont dönti el, objektumonként)

A HWCAP_PACA a címhitelesítést jelzi (az APIA/APIB/APDA/APDB kulcsok és a PACIA*/AUT* utasításcsalád); a pac-ret ehhez kötődik. A HWCAP_PACG a generikus hitelesítést jelzi (PACGA utasítás, APGA kulcs), amely tetszőleges adatból számít aláírást. A visszatérési cím védelméhez nem kell, és nem is helyettesíti a HWCAP_PACA-t; a kettő külön képesség, külön bitben.

aarch64-linux-gnu-gcc -O1 -fPIE -fstack-protector-strong -mbranch-protection=standard \
    -c -o lab-a054-vuln-pac.o lab-a054-vuln.c
aarch64-linux-gnu-gcc -O1 -fPIE -mbranch-protection=standard \
    -c -o lab-a054-main-pac.o lab-a054-main.c
aarch64-linux-gnu-gcc -pie -Wl,-z,relro,-z,now \
    -o lab-a054-pac lab-a054-vuln-pac.o lab-a054-main-pac.o
aarch64-linux-gnu-objdump -d lab-a054-pac \
    | grep -E 'paci|auti|retaa|retab|hint[[:space:]]+#(25|29)'
readelf -n lab-a054-pac | grep -i 'AArch64 feature'

A helyettesítő eszközlánccal (Clang) fordítva a lab_vuln alapértelmezett célon a prológus elején hint #25 (paciasp), az epilógusban hint #29 (autiasp) utasítást kap, utána sima ret áll. Armv8.3-A célon (Clangnál -mcpu=generic+v8_3a, GCC-nél -march=armv8.3-a) a prológus paciasp, a záró utasítás viszont retaa: ez a bináris PAuth nélküli magon már nem NOP-ként fut, hanem SIGILL-lel leáll. A Clang-gal -mbranch-protection=standard mellett fordított objektum .note.gnu.property bejegyzésében a GNU_PROPERTY_AARCH64_FEATURE_1_AND értéke 7 lett (BTI | PAC | GCS bit; más fordító vagy verzió más bitkombinációt adhat); a bitek kiosztását a használt binutils readelf -n kimenete nevesítve mutatja.

A 2. és 3. kérdést a vizsgált környezetben futó próba válaszolja meg:

/* lab-a054-pac-probe.c – PAuth: hardvertámogatás és engedélyezett kulcsok */
#include <stdio.h>
#include <sys/auxv.h>
#include <sys/prctl.h>

#ifndef HWCAP_PACA                  /* Linux arm64 uapi: asm/hwcap.h */
#define HWCAP_PACA (1UL << 30)
#endif
#ifndef HWCAP_PACG
#define HWCAP_PACG (1UL << 31)
#endif
#ifndef PR_PAC_GET_ENABLED_KEYS     /* Linux 5.13+ */
#define PR_PAC_GET_ENABLED_KEYS 61
#endif
#ifndef PR_PAC_APIAKEY
#define PR_PAC_APIAKEY (1UL << 0)
#endif

int main(void)
{
    unsigned long hw = getauxval(AT_HWCAP);
    printf("AT_HWCAP = 0x%lx\n", hw);
    printf("HWCAP_PACA (címhitelesítés, pac-ret ehhez kell): %s\n",
           (hw & HWCAP_PACA) ? "igen" : "nem");
    printf("HWCAP_PACG (generikus hitelesítés, PACGA):      %s\n",
           (hw & HWCAP_PACG) ? "igen" : "nem");

    long keys = prctl(PR_PAC_GET_ENABLED_KEYS, 0, 0, 0, 0);
    if (keys < 0)
        printf("engedélyezett kulcsok: nem lekérdezhető (régi kernel vagy nincs PAuth)\n");
    else
        printf("engedélyezett kulcsok: 0x%lx, APIA %s\n", keys,
               (keys & PR_PAC_APIAKEY) ? "be" : "ki");
    return 0;
}
aarch64-linux-gnu-gcc -O1 -o lab-a054-pac-probe lab-a054-pac-probe.c
# vendég, alapértelmezett CPU-modell
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-a054-pac-probe
# vendég, PAuth-ot is tartalmazó modell
qemu-aarch64 -cpu max -L /usr/aarch64-linux-gnu ./lab-a054-pac-probe

QEMU alatt a próba az emulált CPU-modell és a QEMU által szimulált kernelfelület képességeit mutatja, a hostét nem; az eredményt ezért a használt -cpu értékkel és QEMU-verzióval együtt kell rögzíteni. A PAC-védelemről akkor állíthatjuk, hogy érvényesül, ha mindhárom kérdésre igen a válasz: a kód aláír, a HWCAP_PACA be van állítva, és az APIA kulcs engedélyezett. Ha a kód csak a HINT-alakot használja, a hardver vagy a kulcs hiányában a program hiba nélkül fut, csak védelem nélkül. Ez a leggyakoribb félreértés ezen a területen.

Ellenőrzési feltétel. A példa sikeres, ha (a) a lab_vuln disassemblyjében azonosítható a canary-minta, (b) a lab-a054-vuln állományról a PIE, a teljes RELRO és a nem végrehajtható verem állítása a fenti táblázat összes szükséges bizonyítékával alá van támasztva, (c) a két futás betöltési címe rögzítve van, és minden ASLR-megfigyelés host- vagy vendégcímkét kapott, (d) a PAC három kérdésére (kódkérés, HWCAP_PACA, engedélyezett kulcs) külön-külön van válasz, a futtatási környezet megnevezésével.

14.2.6 Tipikus hibák és ellenpróbák

Hiba 1 — „A canary megvédi a puffert.” Tünet: a canary jelen van, mégis egy másik lokális változó sérül. Hipotézis: a túlírás nem érte el a canary-t, vagy nem folytonosan írt. Adatgyűjtés: objdump -d a veremkeret elrendezéséhez, gdb-vel a sp-hez viszonyított címek vizsgálata. Egy változtatás: a puffer méretének és a szomszédos lokálisok sorrendjének megváltoztatása a forrásban, újrafordítás. Ellenőrzés: a canary-minta pozíciója elmozdul-e, és a hibás írás most már detektálódik-e. Tanulság: a canary a keret végére őrködik, nem a keret minden mezőjére.

Hiba 2 — „Van PAC, tehát védett.” Tünet: a binárisban paciasp található, a folyamat mégsem hibázik hamis visszatérési címen. Hipotézis: a hardver nem támogatja a PAuth-ot, vagy a kernel nem engedélyezte a kulcsot, így a HINT-kódolású paciasp/autiasp NOP-ként fut; vagy QEMU-modell, amely nem emulálja. (Ha a kódban retaa is van, PAuth nélkül nem csendes futást, hanem SIGILL-t látnánk.) Adatgyűjtés: a lab-a054-pac-probe kimenete a célrendszeren (HWCAP_PACA, engedélyezett kulcsok), QEMU -cpu beállítás és a modell dokumentációja. Egy változtatás: a QEMU CPU-modell cseréje PAuth-támogató modellre, vagy natív AArch64 hardveren futtatás. Ellenőrzés: a HWCAP_PACA bit és az APIA kulcs megjelenik-e, és a hamis cím most hibát okoz-e. Megjegyzés: a QEMU user-mode és a -cpu modellek PAuth-támogatása verziófüggő, ellenőrizendő.

Hiba 3 — „Az ASLR miatt a cím kitalálhatatlan.” Tünet: a program fix címre hivatkozik, vagy a címtér töredéke véletlenített. Hipotézis: nem PIE bináris, vagy randomize_va_space=0 a konténerben. Adatgyűjtés: readelf -hW (DYN vs. EXEC), /proc/sys/kernel/randomize_va_space, /proc/self/maps ismételt kiolvasása. Egy változtatás: -fPIE -pie hozzáadása a linkeléshez. Ellenőrzés: a betöltési bázis két egymást követő futás között eltér-e. Tanulság: az ASLR egy rendszer- és build-szintű tulajdonság, nem a forráskódé.

14.2.7 Feladatok

1. feladat (magyarázó, alapszint — cél: megértés-ellenőrzés). A fejezet négy mechanizmusához rendeld hozzá, hogy a verem-túlcsordulás melyik támadási lépését nehezítik meg: (a) a puffer felülírása, (b) a visszatérési cím eltalálása, (c) a beírt bájtok végrehajtása, (d) a visszatérési cím megváltoztatása. Minden mechanizmusnál írj egy mondatot arról, mit nem garantál.

2. feladat (implementációs, középszint — cél: rutinépítés). Készíts egy lab-a054-probe.c programot, amely futásidőben kiírja: a getauxval(AT_HWCAP) értékét hexadecimálisan, és a readelf-ből kiolvasott GNU_STACK és Type mezőt. Fordítsd háromféleképp: (i) alapértelmezett, (ii) -fstack-protector-strong, (iii) -mbranch-protection=standard. Mindhárom binárison dokumentáld a canary-minta, a PIE és az NX állapotát, valamint a PAC-utasítások jelenlétét.

3. feladat (hibakeresési/mérési, középszint — cél: rutinépítés). Egy binárison a GNU_STACK sor RWE. Fogalmazz hipotézist arról, melyik build-lépés okozhatja (például hiányzó .note.GNU-stack egy assembly-objektumban), és egyetlen változtatással igazítsd ki. Igazold readelf -lW-vel, és írd le, miért nem elég ehhez a forráskód vizsgálata.

14.2.8 Megoldások

1. megoldás — Segítség. Nézd meg a fejezet “Pontos garancia” / “Amit nem garantál” bontását minden mechanizmusnál — melyik lépést éri el közvetlenül, és melyiket csak közvetve?

1. megoldás — Teljes megoldás. (a) A puffer felülírását egyik mechanizmus sem akadályozza meg közvetlenül; a canary csak utólag detektálja, ha a túlírás eléri. (b) A visszatérési cím eltalálását az ASLR nehezíti (a cím nem ismert előre), és a PAC védi (a módosított cím nem hitelesíthető). (c) A beírt bájtok végrehajtását az NX/DEP akadályozza meg; ez az egyetlen valódi megelőzés a négy közül arra a lépésre. (d) A visszatérési cím megváltoztatását a canary detektálja (ha a túlírás folytonos és eléri), a PAC pedig megelőzi az érvényesítést. Amit nem garantálnak: a canary nem észlel nem folytonos írást és nem véd kiszivárgott érték ellen; az ASLR nem véd kiszivárgott cím ellen és entrópiája konfigurációfüggő; az NX nem véd meglévő kód újrafelhasználása (ROP) ellen; a PAC nem véd a nem aláírt mutatókra, a hardver-támogatás hiányában hatástalan, és csak az aláírt mutatók integritását garantálja.

2. megoldás — Segítség. Mindhárom fordítási változatnál külön kell rögzítened, hogy a bináris mit kér (a kód jelenléte) és a futó rendszer mit tud ténylegesen biztosítani (HWCAP) — ez a két dolog a (iii) esetben válik külön élesen.

2. megoldás — Teljes megoldás. A mérési jegyzőkönyv három blokkból áll. (i) Alapértelmezett: jellemzően nincs canary-minta, Type: DYN (a disztribúció PIE-t linkel alapból), GNU_STACK: RW. (ii) -fstack-protector-strong: a helyi puffert használó függvényekben megjelenik a guard betöltése (glibc alatt a __stack_chk_guard GOT-on keresztül), a verembe mentése, majd a LDR/CMP/B.NE és a __stack_chk_fail hívás; a konkrét címzés libc-függő. (iii) -mbranch-protection=standard: megjelennek a paciasp/autiasp (régebbi objdump-nál hint #25/hint #29) és bti utasítások. Kulcs: a (iii) esetben a védelem csak akkor tényleges, ha a futásidejű HWCAP_PACA jelzi a címhitelesítés támogatását és az APIA kulcs engedélyezett; a HWCAP_PACG erről nem dönt. Ezt minden futtatási környezetben külön kell rögzíteni, és a QEMU-modell támogatása ellenőrizendő. Az objdump és a readelf adatai a bináris képességét írják le, nem a futó rendszer védelmi állapotát.

3. megoldás — Segítség. A GNU_STACK jelző a linkelés eredménye, és az ÖSSZES bemeneti objektum jelzéseinek kombinációjából áll elő — melyik objektumtípusnál (kézzel írt vagy fordítóból származó) szokott hiányozni az erre vonatkozó direktíva?

3. megoldás — Teljes megoldás. A GNU_STACK: RWE gyakori oka, hogy valamelyik linkelt objektumfájl nem tartalmaz .note.GNU-stack szakaszt (tipikusan kézzel írt assembly), és a linker ezt – célarchitektúrától és verziótól függően – végrehajtható veremigényként értelmezi; a másik ok egy explicit -z execstack. Fordítva viszont nem következtethetünk: a direktíva hiánya nem jelent automatikusan RWE-t (egy AArch64 Binutils 2.40-es próbában a direktíva nélkül is RW lett a verem), ezért mindig a kész ELF-et kell ellenőrizni. Hipotézis → adatgyűjtés: readelf -SW minden objektumfájlon, a .note.GNU-stack szakasz keresése; a hiányzó elem azonosítja a felelős objektumot. Egy változtatás: a kérdéses assembly-forrásba .section .note.GNU-stack,"",%progbits direktíva felvétele (ez üres, nem allokált szakasz, csak jelzi a szándékot). Ellenőrzés: readelf -lW a végleges binárison RW-t mutat. Miért nem elég a forráskód: a GNU_STACK jelző a linkelési lépésben, az összes bemeneti objektum jelzéseinek kombinálásából keletkezik, ezért egyetlen forrásfájl olvasásából nem következtethetünk rá.

14.2.9 Rövid összefoglaló

Négy mechanizmust rendszereztünk. A stack canary a puffer és a mentett visszatérési cím közé szúrt véletlen érték: csak detektál, a visszatérés előtt, és csak folytonos, a canary-t elérő túlírás esetén. Az ASLR a memóriaterületek kezdőcímét véletleníti: nehezíti a cím eltalálását, de entrópiája konfigurációfüggő, és egyetlen kiszivárgott cím deaktiválhatja. Az NX/DEP az adatlapokat nem-végrehajthatóvá teszi: ez az egyetlen valódi megelőzés a kód-injektálás ellen, de a ROP-ra nem hat. A PAC az AArch64-en a visszatérési címet hardveresen, kriptográfiailag hitelesíti, de csak azon az úton, amely a hitelesítő utasítást (AUTIASP vagy RETAA) ténylegesen végrehajtja; a sima RET nem hitelesít, és a pac-ret nem általános CFI; hatékonysága a hardvertámogatástól (HWCAP_PACA), a kulcs engedélyezésétől és a PAC-mező méretétől függ; a HINT-kódolású paciasp/autiasp támogatás nélkül csendben NOP, a retaa viszont SIGILL. Egyik sem abszolút: a mélységi védelem lényege, hogy a támadónak minden réteget külön kell legyőznie.

14.2.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. A gyorsan változó tények (QEMU CPU-modellek PAuth-támogatása, glibc TLS-eltolás, kernel ASLR-entrópia, HWCAP-bitek) továbbra sem ellenőrzöttek — ezeket a dokumentumellenőrzés nem tudja pótolni, mert rendszer- és verziófüggők.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak (külön-külön kezelendők):

  1. A readelf/objdump kimenetek és a glibc TLS-eltolás (tpidr_el0 + 40) verziófüggő; minden konkrét rendszeren ellenőrizendő.
  2. A qemu-aarch64 user-mode és a qemu-system-arm CPU-modellek PAuth- és BTI-támogatása nem ellenőrzött ebben a munkafolyamatban; a PAC-példa QEMU-ban csak a támogató modell kiválasztásával érvényes.
  3. Az ASLR tényleges entrópiája (mmap_rnd_bits, randomize_va_space) kernelkonfiguráció-függő; a fejezetben szereplő 9/28 bit illusztratív, levezetett, nem mért érték.
  4. A PAC-mező mérete a virtuális címtér méretétől függ; a ~11 bit reprezentatív, nem általános érvényű.
  5. A stack canary alsó bájtjának 0x00 kötöttsége és a __stack_chk_fail viselkedése glibc-implementációfüggő.
  6. A helyettesítő eszközlánccal (zig cc/Clang) ténylegesen ellenőrizve: a két forrásból a lab-a054-vuln linkelhető, a statikus musl-változat Unicorn-alapú emulátorban lefut és a sink sort kiírja; a glibc-célú PIE fejlécében a közölt Type/INTERP/LOAD/GNU_RELRO/GNU_STACK/FLAGS sorok szerepelnek; alapértelmezett célon hint #25/hint #29 + ret, Armv8.3-A célon paciasp + retaa keletkezik. A lab-a054-pac-probe lefordul és lefut, de az emulátor nem ad AT_HWCAP-ot és nem ismeri a prctl hívást, ezért ott „nem”/„nem lekérdezhető” a válasz; ez az emulátorról szól, nem a PAuth-ról. A GNU-eszközlánccal, qemu-aarch64-gyel és natív hardveren semmit nem futtattunk; az ismételt betöltési címeket és a HWCAP-biteket ott kell rögzíteni.