A határellenőrzés nélküli másolás a C-programok egyik legrégebbi és
legveszélyesebb hibaosztálya. A strcpy, a gets
vagy a sprintf korlátlanul ír, ha a célpuffer mérete nem
elegendő. A C-fejlesztő számára ez „csak” egy memóriahiba; assembly
szinten viszont pontosan látszik, hogy a másolás a verem szomszédos
tartalmát írja felül. Ebben az alfejezetben kontrollált demonstrációval
megmutatjuk, hogyan íródik felül a mentett visszatérési cím (return
address), és miért lesz ebből vezérlésátvétel. A cél nem egy működő
exploit elkészítése, hanem a mechanizmus és a védelem megértése. A példa
a saját, jelölt labor-binárisunkra épül, fix bemenettel, és a
túlcsordulást még a RET előtt detektálja.
Verem-túlcsordulás (stack buffer overflow): olyan írási művelet, amely egy verembeli puffer határán túlra ír, és ezzel a szomszédos veremtartalmat — lokális változókat, mentett keretmutatót (frame pointer, FP) vagy mentett visszatérési címet (link register, LR) — módosítja. Nem azonos a veremkimerüléssel (stack exhaustion), amikor végtelen rekurzió miatt elfogy a veremterület. Itt a verem mérete elegendő, de egy másolás túlnyúlik a kijelölt pufferen.
Az AAPCS64 szerinti verem lefelé növekszik: a függvény az
SP-t kisebb címre állítja. Ebben a példában a keret alján
(alacsonyabb címen) helyezkedik el a lokális puffer, felette a mentett
X29/X30 pár. Egy felfelé, növekvő címre író
másolás először a puffert, majd a mentett FP-t, végül a mentett LR-t éri
el. Pontosan ez a sebezhető elrendezés.
| Veremterület | Offset az SP-hez képest |
Tartalom |
|---|---|---|
| Lokális puffer | 0–31 | 32 bájtos tömb |
Mentett X29 (FP) |
32–39 | hívó keretmutatója |
Mentett X30 (LR) |
40–47 | visszatérési cím |
| Kihasználatlan igazítás | 48–63 | 16 bájtos igazítás |
A táblázat a példabeli 64 bájtos keretre vonatkozik. A konkrét elrendezés fordító- és optimalizálási szintfüggő; a lényeg, hogy a puffer és a mentett LR között nincs hardveres határ.
A másoló ciklus minden bájtot egymás után ír a puffer kezdetétől. A
0-tól számozott indexek közül a 32. már a mentett X29, a
40. a mentett X30 első bájtját éri el: ehhez rendre 33,
illetve 41 bájtos másolás kell. A szokásos epilógus az LDP
utasítással tölti vissza a mentett X30-at; a
RET ezután a regiszterben lévő címre ugrik. Ha a felülírt
mentett értéket ellenőrzés nélkül töltenénk vissza, a RET
nem a hívóba térne vissza, hanem az így kapott címre. Ez a
vezérlésátvétel (control-flow hijack) fogalmi szintje:
a támadó által befolyásolt érték a program számlálójába (PC) kerül. A
példában ezt nem érjük el, mert a RET előtt ellenőrizzük a
mentett LR épségét, és szándékosan leállítjuk a programot.
A mentett FP felülírása önmagában nem ad vezérlést, de a láncolt keretlistát és a hibakeresést megzavarja. A lokális változók sérülése pedig akár a másolás méretét vagy egy mutatót is módosíthat, ami további hibákhoz vezet.
Munkakönyvtár: ~/arm-labor/a64/a053/ —
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/a053
cd ~/arm-labor/a64/a053Az alábbi lab-overflow.S egyetlen fájl, amely
_start belépési ponttal fut. CFI nélküli
demonstráció: a lab_vuln_copy keretet épít, de
szándékosan nem kap .cfi_* direktívákat — a vizsgálat
tárgya éppen a veremben lévő keretrekord, ezért a hívási lánc itt csak a
keretlánc (X29) alapján, és a felülírás után már úgy sem
követhető (a CFI-szabályt lásd a Prológus és
epilógus alfejezetben). A lab_vuln_copy függvény 32
bájtos puffert feltételez, de a hosszt nem ellenőrzi. A forrás 48 bájt:
32 bájt minta, 8 bájt a mentett FP helyére, 8 bájt hamis LR érték. A
hamis LR (0xdeadbeefcafebabe) szándékosan nem érvényes cím,
és soha nem hajtódik végre, mert a detektálás megelőzi a
RET-et.
// lab-overflow.S — A64 ISA, AArch64, little-endian
.section .data
src:
.ascii "AAAAAAAAAAAAAAAA" // 16 bájt
.ascii "BBBBBBBBBBBBBBBB" // 16 bájt -> puffer 32 bájt
.quad 0x1122334455667788 // mentett X29 felülírása
.quad 0xdeadbeefcafebabe // mentett X30 felülírása
src_end:
.equ SRC_LEN, src_end - src // 48
.section .text
.global _start
_start:
adr x0, src
mov x1, #SRC_LEN
bl lab_vuln_copy
// Ide normál esetben vissza kellene térni.
mov x0, #0
mov x8, #93 // exit
svc #0
// x0 = forrás, x1 = hossz
lab_vuln_copy:
sub sp, sp, #64
stp x29, x30, [sp, #32] // mentett FP/LR a puffer felett
add x29, sp, #32
mov x2, #0
1:
cmp x2, x1
b.ge 2f
ldrb w3, [x0, x2]
strb w3, [sp, x2] // nincs határellenőrzés
add x2, x2, #1
b 1b
2:
// Defenzív ellenőrzés: a mentett LR érintetlen?
ldr x3, [x29, #8] // mentett X30
cmp x3, x30 // X30 = eredeti visszatérési cím
b.ne lab_overflow_detected
ldp x29, x30, [sp, #32]
add sp, sp, #64
ret
lab_overflow_detected:
mov x0, #1 // jelzett kilépési kód
mov x8, #93
svc #0
.section .note.GNU-stack,"",%progbits // nem kér végrehajtható vermetBuild és futtatás (qemu-user, AArch64):
aarch64-linux-gnu-as -o lab-overflow.o lab-overflow.S
aarch64-linux-gnu-ld -o lab-overflow lab-overflow.o
readelf -lW lab-overflow | grep GNU_STACK # elvárt jelző: RW
qemu-aarch64 ./lab-overflow
echo $?A GNU_STACK ellenőrzése a kész ELF-re vonatkozik. A
forrásbeli direktíva a szándékot rögzíti, de egy hiányzó direktívából
önmagában nem következik végrehajtható verem: hogy a linker jelzés
nélküli objektumból mit állít elő, az eszközlánc- és verziófüggő.
Várható kimenet: a program az
lab_overflow_detected ágra kerül, ezért a kilépési kód
1. A 0xdeadbeefcafebabe hamis LR nem hajtódik
végre. A qemu-aarch64 itt statikus binárist futtat, külső
-L rendszerkönyvtár nélkül.
Ellenőrzés GDB-vel (gdb-multiarch):
qemu-aarch64 -g 1234 ./lab-overflow # az egyik terminálban
# A másik terminálban:
gdb-multiarch -q ./lab-overflow
(gdb) set architecture aarch64
(gdb) break lab_overflow_detected
(gdb) target remote localhost:1234
(gdb) continue
(gdb) info registers x29 x30 sp
(gdb) x/8gx $sp
A töréspontnál a sp a keret aljára mutat. A
[sp+32] és [sp+40] tartományban a felülírt
mentett FP és LR látható. A $x30 ekkor még az eredeti
visszatérési cím, a [x29, #8] pedig a hamis érték; a
cmp mutatja az eltérést.
C-ben a tipikus minta:
void f(const char *input) {
char buf[32];
strcpy(buf, input); // nincs méretellenőrzés
}A strcpy addig másol, amíg nullát nem lát. Ha az
input hosszabb 31 bájtnál, ugyanaz a veremfelülírás
történik, mint a példában. A -fstack-protector-strong
kapcsolóval a fordító a védelemre kijelölt, ténylegesen verembeli
objektumot tartalmazó függvényekben canary (stack
canary) értéket helyez el; a függvény epilógusa ellenőrzi annak épségét.
A tényleges generált kódot kell vizsgálni: az optimalizálás a fel nem
használt lokális puffert el is tüntetheti. A
_FORTIFY_SOURCE megfelelő fordítási feltételek és ismert
célobjektumméret mellett ellenőrzött változatot, például
__strcpy_chk-t rendelhet a strcpy hívásához.
Ezek a védelmek a Védelmi mechanizmusok: canary,
ASLR, NX/DEP, PAC alfejezetben részletesen szerepelnek. A mostani
példa canary nélküli, ezért a mentett LR közvetlenül elérhető.
X29 képzését, a puffer kezdőcímét és a visszatöltést együtt
kell módosítani; egyetlen prológusutasítás cseréje nem elég.X30 változhat. A
cmp x3, x30 csak akkor érvényes, ha a függvény nem hív más
függvényt. Ha bl-t használunk, az X30
felülíródik, és a detektálás hibás. Ilyenkor a mentett LR-t kell
referenciaként elmenteni.SP 16 bájtos igazítása
miatt a keretméret 16 többszöröse. A 64 bájtos keret ezt teljesíti.RET mást is használhat. A
RET xN alakban a megadott regiszterből tér vissza; a példa
a RET alapértelmezett X30 viselkedését
mutatja.Ellenpróbák a példán (a _start
mov x1, #SRC_LEN sorát mov x1, #32-re, illetve
#40-re cserélve): 32 bájtos bemenet → nincs túlcsordulás, a
program 0 kóddal lép ki. 40 bájtos bemenet → a mentett FP
felülíródik, de a mentett LR nem; a detektálás nem jelez, a
ret a helyes címre tér vissza, de a X29 hamis.
48 bájtos bemenet → a mentett LR is felülíródik; a detektálás
1 kóddal leáll.
X29 és a mentett X30 offsetjét az
SP-hez képest. Melyik másolási hossznál íródik felül a
mentett LR első bájtja?lab-overflow-canary.S néven. A lab_vuln_copy a
puffer utáni 32–39. offsetre írja ki a 0x0123456789abcdef
értéket a másolás előtt, majd a másolás után hasonlítsa
össze az eredeti értékkel. Eltéréskor a
lab_overflow_detected út állítsa le a programot. Adja meg a
teljes keretelrendezést, az X29 képzését, a mentett FP/LR
helyét és a sikeres epilógust is. Az eredeti 48 bájtos forrás 0, 32, 33,
40 és 48 bájtos prefixével végezzen ellenpróbát: az első két eset térjen
vissza, a többi jelezzen canary-sérülést. A hívó a sikeres visszatérés
után külön ellenőrizze az SP és az X29 eredeti
értékét.lab-overflow.S programban GDB-vel állítson töréspontot a
strb utasításra. Figyelje az x2 regisztert és
a [sp, x2] címet. Melyik iterációnál íródik felül a mentett
LR? Adja meg az x2 értékét és a strb által írt
bájt címét.1. megoldás — Segítség. Nézd meg a fejezet eleji
offset-táblázatot, és gondold át: a másolás a 0. indextől indul — hány
bájtot kell írni ahhoz, hogy az index elérje a
SP+40-et?
1. megoldás — Teljes megoldás. Puffer:
SP+0–SP+31. Mentett X29:
SP+32–SP+39. Mentett X30:
SP+40–SP+47. A mentett LR első bájtja a
SP+40 címen van. Mivel a másolás a 0. indextől indul, a 40.
indexű bájt írásához 41 bájtos másolás szükséges. A 40 bájtos másolás a
0–39. indexeket fedi, így a SP+40-et még nem éri el.
2. megoldás — Segítség. A canary a puffer és a
mentett FP/LR közé kerüljön. Előbb rajzold fel a teljes keretet, majd
egyeztesd vele a mentést, az X29 értékét és a
visszatöltést. Mikor kell a jelzőértéket a verembe írni? Mely regiszter
őrizheti az összehasonlítás referenciáját, ha a másoló ciklus nem hív
más függvényt?
2. megoldás — Teljes megoldás. Az alábbi
önálló, teljes programot mentsd
lab-overflow-canary.S néven. Az alapdemonstráció
forrásadatait használja, és a canary alapján dönt; az eredeti
lab-overflow.S változatlanul megtartható
összehasonlításra.
CFI nélküli demonstráció: az alapváltozathoz
hasonlóan a lab_vuln_copy itt is keretet épít
.cfi_* direktívák nélkül, mert a vizsgálat tárgya maga a
verembeli keretrekord (a CFI-szabályt lásd a Prológus és epilógus alfejezetben). A választott
keret 80 bájtos, így az SP 16 bájtos igazítása megmarad. A
függvény törzsében az offsetek a keret lefoglalása utáni
SP-hez képest értendők:
| Offset | Tartalom |
|---|---|
| 0–31 | 32 bájtos puffer |
| 32–39 | Canary: 0x0123456789abcdef |
| 40–47 | Kitöltés |
| 48–55 | Mentett X29 |
| 56–63 | Mentett X30 |
| 64–79 | Kitöltés |
Az X29 = SP + 48 a mentett FP/LR párra mutat. A canary
első bájtját 33, az új helyen lévő mentett FP első bájtját 49, a mentett
LR első bájtját 57 bájtos másolás érné el. A mostani 48 bájtos forrás
tehát a canary-t és az utána álló kitöltést írja felül, a feljebb
helyezett FP/LR párt még nem éri el.
// lab-overflow-canary.S — A64 ISA, AArch64 Linux, little-endian
.section .data
src:
.ascii "AAAAAAAAAAAAAAAA"
.ascii "BBBBBBBBBBBBBBBB"
.quad 0x1122334455667788
.quad 0xdeadbeefcafebabe
src_end:
.equ SRC_LEN, src_end - src // 48 bájt, mint az alapdemóban
// A --defsym COPY_LEN=N az N bájtos prefixet választja ki.
.ifndef COPY_LEN
.equ COPY_LEN, SRC_LEN
.endif
.if COPY_LEN < 0
.error "COPY_LEN must be nonnegative"
.endif
.if COPY_LEN > SRC_LEN
.error "COPY_LEN exceeds source size"
.endif
.section .text
.global _start
_start:
// Hívói keret: érvényes, nullával lezárt keretrekord.
sub sp, sp, #16
stp xzr, xzr, [sp]
mov x29, sp
mov x19, sp // elvárt SP és X29; a hívott nem írja
adr x0, src
mov x1, #COPY_LEN
bl lab_vuln_copy
// Csak a sikeres visszatérési út jut ide.
mov x4, sp
cmp x4, x19
b.ne lab_frame_error
cmp x29, x19
b.ne lab_frame_error
add sp, sp, #16 // a hívó saját keretének felszabadítása
mov x0, #0
b lab_exit
// x0 = forrás, x1 = hossz; a laborban 0 <= x1 <= SRC_LEN.
// A másolás szándékosan nem vizsgálja a 32 bájtos pufferhatárt.
lab_vuln_copy:
sub sp, sp, #80
stp x29, x30, [sp, #48]
add x29, sp, #48
movz x6, #0xcdef
movk x6, #0x89ab, lsl #16
movk x6, #0x4567, lsl #32
movk x6, #0x0123, lsl #48
str x6, [sp, #32] // inicializálás a másolás ELŐTT
mov x2, #0
1:
cmp x2, x1
b.hs 2f // hossz és index: előjel nélküli
ldrb w3, [x0, x2]
strb w3, [sp, x2]
add x2, x2, #1
b 1b
2:
ldr x5, [sp, #32]
cmp x5, x6 // x6-ot a ciklus nem módosította
b.ne lab_overflow_detected
add x7, sp, #48
cmp x29, x7 // az X29 a saját keretrekordra mutat?
b.ne lab_frame_error
ldp x29, x30, [sp, #48]
add sp, sp, #80
ret
lab_overflow_detected:
mov x0, #1 // a canary megváltozott
b lab_exit
lab_frame_error:
mov x0, #2 // hibás X29-képzés, vagy SP/X29 nem állt helyre
lab_exit:
mov x8, #93 // Linux SYS_exit; nem tér vissza
svc #0
.section .note.GNU-stack,"",%progbitsAz X6 a 0x0123456789abcdef
referenciaértéket tartja, a STR ugyanezt írja a guard
helyére. Little-endian memóriában a bájtsor
ef cd ab 89 67 45 23 01. A ciklus csak az X2
és W3 regisztert módosítja, nem hív más függvényt, ezért az
X6-ban lévő referencia megmarad. Ha a rutin később
hívásokat kap, az X6 caller-saved szerepe miatt a
referencia megőrzését újra kell tervezni.
A sikeres út előbb a hívott oldalán ellenőrzi, hogy az
X29 valóban a saját keretrekordjára (SP + 48)
mutat; ez a képzést vizsgálja. Ezután az epilógus a
párt a 48-as offsetről tölti vissza, és 80
bájtot ad az SP-hez. A hívó az
X19-ben megőrzött címhez hasonlítja az SP-t és
az X29-et; ez a visszaállítást vizsgálja.
Bármelyik eltérésnél 2 a program kilépési állapota.
Canary-sérüléskor 1 az állapot: ez az ág befejezi az egész
demonstrációt, ezért nem hajt végre epilógust és
RET-et.
Build és egy futás (GNU eszközlánc + QEMU):
aarch64-linux-gnu-as -o lab-overflow-canary.o lab-overflow-canary.S
aarch64-linux-gnu-ld -o lab-overflow-canary lab-overflow-canary.o
readelf -lW lab-overflow-canary | grep GNU_STACK # elvárt: RW
# Az alapértelmezett COPY_LEN = 48 esetben 1 az elvárt állapot.
if qemu-aarch64 ./lab-overflow-canary; then rc=0; else rc=$?; fi
printf 'exit=%s\n' "$rc"Önellenőrzés az öt bemenetre (bash): a
--defsym csak a másolási hosszt változtatja; a forrásadat
és a függvény ugyanaz marad. Az if az elvárt 1
állapotot is kezeli akkor, ha a shellben a set -e be van
kapcsolva. A ciklus alhéjban fut, így az exit 1 hibánál
csak az alhéjat állítja le, interaktív terminálban sem zárja be a
shellt.
(
for n in 0 32 33 40 48; do
aarch64-linux-gnu-as --defsym "COPY_LEN=$n" \
-o lab-overflow-canary.o lab-overflow-canary.S || exit 1
aarch64-linux-gnu-ld -o lab-overflow-canary \
lab-overflow-canary.o || exit 1
if qemu-aarch64 ./lab-overflow-canary; then rc=0; else rc=$?; fi
if [ "$n" -le 32 ]; then want=0; else want=1; fi
printf 'n=%s exit=%s elvart=%s\n' "$n" "$rc" "$want"
[ "$rc" -eq "$want" ] || exit 1
done
)Elvárt eredmény:
n=0 exit=0 elvart=0
n=32 exit=0 elvart=0
n=33 exit=1 elvart=1
n=40 exit=1 elvart=1
n=48 exit=1 elvart=1
A 33 bájtos prefix utolsó bájtja a forrásban 0x88, amely
az SP+32 címen lévő 0xef canary-bájtot
változtatja meg. A sérülés így nem a verem korábbi tartalmától függ. A 0
és 32 bájtos futás csak akkor ad 0-t, ha a jelző sértetlen,
és az SP/X29 is helyreállt.
Ez a fordítói stack protector alapelvét szemlélteti: a mentett vezérlési adatok felhasználása előtt ellenőrizzük a közéjük és a puffer közé tett jelzőt. A canary nem akadályozza meg a túlírást, és az egyezése nem bizonyítja a másolás szabályosságát. A fix, nyilvános érték determinisztikus oktatási jelző: azért választottuk, hogy a próba ismételhető legyen, és változatlan bájtokkal felül is lehet írni. A valódi védelemhez megfelelő fenyegetési modell, rendszerint kiszámíthatatlan guard és a biztonságos bemenethossz-kezelés is szükséges; a mostani ellenpróbák a guardot biztosan megváltoztató mintát használják.
3. megoldás — Segítség. A ciklus x2-t
0-tól indexeli; ugyanazzal a logikával, mint az 1. feladatban, keresd
meg, melyik x2 érték esik egybe a mentett LR első bájtjának
offsetjével.
3. megoldás — Teljes megoldás. A strb
már az első iterációban (x2 = 0) ír, és minden iterációban
pontosan egy bájtot. A mentett LR a [sp+40] címen kezdődik.
Az x2 = 40 értéknél a strb w3, [sp, x2] a
[sp+40] címre ír, ez az első bájt, amely a mentett
X30-at érinti. Mivel az index 0-tól indul, ez a
41. végrehajtott írás: a GDB-ben a 41. iteráció után
változik meg a [sp+40] tartalma. A detektálás a másolás
után, a 2: címkénél fut, és a cmp jelzi az
eltérést.
A verem-túlcsordulás akkor jön létre, ha egy határellenőrzés nélküli
másolás túlnyúlik a pufferen, és felülírja a szomszédos veremtartalmat.
A mentett visszatérési cím a RET miatt kitüntetett célpont:
felülírása vezérlésátvételhez vezethet. A példa ezt kontrolláltan,
detektálással állítja meg a RET előtt. A C-s
strcpy-hez hasonló hiba ugyanezt a mechanizmust idézi elő;
a canary, a _FORTIFY_SOURCE és a fordítói védelmek ezt
hivatottak kivédeni.
BL által
az X30-ba írt visszatérési cím verembeli másolata.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.
STP,
LDP, RET, SP-igazítás.
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,
-fstack-protector-strong: a guard belépéskori beállítása és
kilépéskori ellenőrzése._FORTIFY_SOURCE,
ismert célobjektumméret és ellenőrzött könyvtári hívások.A canary-megoldás ellenőrzési állapota (2026-10-20).
A fenti teljes lab-overflow-canary.S programot a
helyettesítő eszközlánccal (zig cc 0.17.0: Clang 22.1.8 +
LLD) fordítottuk, és a tools/lab-run.py 1.1 futtatóban
(Unicorn 2.1.4) ellenőriztük; a próba a kísérőcsomag
tests/run.sh készletének része (tools/a053/).
A 0/32/33/40/48 bájtos esetek állapota rendre 0/0/1/1/1 volt. A sikeres
utakon a program saját SP/X29-vizsgálata is teljesült. A guard
inicializáló tárolásának elhagyása a sértetlen bemenetet is hibára
vitte; a sikeres epilógus hibás, 64 bájtos SP-felszabadítása és a hibás,
SP + 32-ből képzett X29 egyaránt
2 állapotot adott. Ezek a hibás kontrollok a teszt
érzékenységét ellenőrzik. A helyettesítő futtató eredménye funkcionális
bizonyíték, nem QEMU- vagy hardverfuttatás.
Fennmaradó ellenőrzések: a fenti GNU
assembler/linker + qemu-aarch64 parancsokat és a
GDB-lépéseket a fő laborúton is végre kell hajtani. A két kézzel írt
demonstráció offsetjeit a saját utasításaik rögzítik: az alapváltozat
kerete 64, a canary-változaté 80 bájt. Egy C-fordító által létrehozott
keret elrendezése ettől függetlenül fordító- és optimalizálásfüggő. A
fordítói stack protector és a glibc fortifikációja külön mechanizmus;
konkrét támogatásuk és generált kódjuk a labor eszközverzióival
ellenőrizendő.