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.
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.
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.
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:
fork-alapú szerver esetén a címek
utódokra öröklődnek).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.
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:
SIGSEGV a támadónak is visszajelzés.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:
X30 (LR) aláíródik:
PACIASP — az aláírás az SP-t is bevonja a
módosítóba (context), így egy másik verempozícióra átvitt érték is
érvénytelenné válik;AUTIASP ellenőrzi és eltávolítja az
aláírást, vagy a RETAA/RETAB utasítás az
ellenőrzést és a visszatérést egy lépésben végzi.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:
PACIASP/AUTIASP esetén az aláíráskori,
illetve a hitelesítéskori SP). Egy más SP-jű
keretbe átvitt aláírt LR ezért nem megy át, egy azonos
SP-vel futó másik visszatérési ponton viszont igen. Ezért
fontos az SP-függő módosító, és ezért van jelentősége a
veremkeret pontos elrendezésének.FEAT_PAuth) az Armv8.3-A része; régebbi magokon nincs.
Hogy ilyenkor mi történik, az az utasítás kódolásától
függ, nem általában „a PAC-utasításoktól”:
PACIASP, AUTIASP (és a
PACIBSP/AUTIBSP, XPACLRI) a
HINT-térbe kódolt alakok (HINT #25,
HINT #29 stb.). PAuth nélküli magon NOP-ként futnak le, a
program működik, de védelem nincs. Ugyanez a helyzet,
ha a mag ismeri a PAuth-ot, de az operációs rendszer nem engedélyezte a
kulcsot;RETAA/RETAB, a
BRAA/BLRAA, a LDRAA, valamint a
regiszteroperandusos PACIA Xd, Xn és
AUTIA Xd, Xn nem HINT-kódolású. PAuth
nélküli magon definiálatlan utasításként SIGILL-lel
leállítják a programot.armv8-a) célon a
-mbranch-protection=standard mellett csak a HINT-alakot
használja. RETAA-t csak akkor ad ki, ha a cél legalább
Armv8.3-A (például -march=armv8.3-a), és az ilyen bináris
régebbi magon nem fut. Az, hogy a védelem érvényes-e, futásidőben
ellenőrizendő (lásd a példát).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.
Feltételek. Tegyük fel, hogy a védelmi mechanizmus 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:
ASLR-entrópia levezetése. Ha egy méretű, lapkeretre igazított régiót egy méretű tartományon belül véletlenítünk, a lehetséges pozíciók száma legfeljebb , így
Példa: 32 bites címtér ( B = 4 GiB), 8 MiB-os ( B) veremrégió esetén 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ő.
| Mechanizmus | Illusztratív entrópia | Egy vak próba siker-valószínűsége | Jelleg |
|---|---|---|---|
Stack canary (8 bájt, alsó bájt kötött 0x00) |
56 bit | detektálás | |
| Verem-ASLR, 32 bites címtér (levezetve: , ) | 9 bit | nehezítés | |
ASLR, 64 bites PIE + mmap (tipikus konfiguráció) |
~28 bit | nehezítés | |
| PAC-mező (reprezentatív érték, címtér-függő) | ~11 bit | 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.
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/a054Cé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-vulnA 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állA 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ópiaA 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-probeQEMU 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.
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é.
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.
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á.
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.
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.
PACIASP/AUTIASP/RETAA,
UXN/PXN lapbitek. Ellenőrizve: a link él; a
fejezetszám és a kiadásbetű az Arm-oldalak közös korlátja miatt nem
ellenőrzött.-fstack-protector-strong, -mbranch-protection,
PIE-kapcsolók pontos szemantikája. Tartalmilag
ellenőrizve: a legújabb listázott kiadás GCC 16.2.objdump, readelf,
.note.GNU-stack szakasz jelentése. Tartalmilag
ellenőrizve: a jelenlegi kiadás binutils 2.47.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):
readelf/objdump kimenetek és a glibc
TLS-eltolás (tpidr_el0 + 40) verziófüggő;
minden konkrét rendszeren ellenőrizendő.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.mmap_rnd_bits,
randomize_va_space)
kernelkonfiguráció-függő; a fejezetben szereplő 9/28
bit illusztratív, levezetett, nem mért érték.0x00 kötöttsége és a
__stack_chk_fail viselkedése
glibc-implementációfüggő.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.