Skip to content

È valido scrivere sotto ESP?

Se ti imbatti in qualcosa che ti fa dubitare, puoi commentarlo e faremo del nostro meglio per aiutarti il ​​più rapidamente possibile.

Soluzione:

TL:DR: no, ci sono alcuni casi limite di SEH che possono renderlo non sicuro nella pratica, oltre a essere documentato come non sicuro. @Raymond Chen ha recentemente scritto un post sul blog che probabilmente dovreste leggere al posto di questa risposta.

Il suo esempio di un errore di I/O di page-fetch del codice che può essere "risolto" invitando l'utente a inserire un CD-ROM e a riprovare è anche la mia conclusione sull'unico errore praticamente recuperabile se non ci sono altre istruzioni potenzialmente dannose tra store e reload sotto ESP/RSP.

Oppure se si chiede a un debugger di chiamare una funzione nel programma in fase di debug, questo utilizzerà anche lo stack del processo di destinazione.

Questa risposta contiene un elenco di alcune cose che si pensa possano potenzialmente utilizzare la memoria al di sotto di ESP, ma che in realtà non lo fanno, il che potrebbe essere interessante. Sembra che solo SEH e i debugger possano essere un problema nella pratica.



Prima di tutto, se vi interessa l'efficienza, non potete evitare x87 nella vostra convenzione di chiamata? movd xmm0, eax è un modo più efficiente per restituire un oggetto float che si trovava in un registro intero. (E spesso si può evitare di spostare i valori FP nei registri interi, utilizzando le istruzioni integer SSE2 per separare esponente/mantissa per un valore log(x)o l'aggiunta di un intero 1 per nextafter(x).) Ma se dovete supportare hardware molto vecchio, allora avete bisogno di una versione x87 a 32 bit del vostro programma e di una versione efficiente a 64 bit.

Ma ci sono altri casi d'uso per piccole quantità di spazio sullo stack, dove sarebbe bello salvare un paio di istruzioni che compensano ESP/RSP.


Sto cercando di raccogliere la saggezza combinata di altre risposte e discussioni nei commenti sotto di esse (e su questa risposta):

È esplicitamente documentato come non sicuro da Microsoft: (per il codice a 64 bit, non ho trovato una dichiarazione equivalente per il codice a 32 bit, ma sono sicuro che c'è)

Uso dello stack (per x64)

Tutta la memoria oltre l'indirizzo corrente di RSP è considerata volatile: Il sistema operativo, o un debugger, può sovrascrivere questa memoria durante una sessione di debug dell'utente o un gestore di interrupt.

Questa è la documentazione, ma il motivo dell'interruzione indicato non ha senso per lo stack dello spazio utente, ma solo per lo stack del kernel. La parte importante è che viene documentata come non garantito sicuro, non le ragioni addotte.

Gli interrupt hardware non possono usare lo stack utente; ciò permetterebbe allo spazio utente di mandare in crash il kernel con mov esp, 0o, peggio, di prendere il controllo del kernel facendo in modo che un altro thread nel processo dello spazio utente modifichi gli indirizzi di ritorno mentre è in esecuzione un gestore di interrupt. Questo è il motivo per cui i kernel configurano sempre le cose in modo che il contesto dell'interrupt venga spinto sullo stack del kernel.

I moderni debugger vengono eseguiti in un processo separato e non sono "intrusivi". Ai tempi del DOS a 16 bit, senza un sistema operativo multi-tasking a memoria protetta che desse a ogni task il proprio spazio di indirizzamento, i debugger utilizzavano lo stesso stack del programma in fase di debug, tra due istruzioni qualsiasi, durante il single-stepping.

@RossRidge fa notare che un debugger potrebbe voler permettere di chiamare una funzione nel contesto del thread corrente, per esempio con SetThreadContext. Questo verrebbe eseguito con ESP/RSP appena sotto il valore corrente. Questo potrebbe ovviamente avere effetti collaterali per il processo in fase di debug (intenzionali da parte dell'utente che esegue il debugger), ma l'intromissione delle variabili locali della funzione corrente al di sotto di ESP/RSP sarebbe un effetto collaterale indesiderato e inaspettato. (Quindi i compilatori non possono metterle lì).

(In una convenzione di chiamata con una zona rossa al di sotto di ESP/RSP, un debugger potrebbe rispettare tale zona rossa decrementando ESP/RSP prima di effettuare la chiamata di funzione).

Esistono programmi che si rompono intenzionalmente quando vengono sottoposti a debug, e si considera questa una caratteristica (per difendersi dai tentativi di reverse-engineering).


Correlato: l'ABI del sistema V x86-64 (Linux, OS X, tutti gli altri non Windows sistemi non Windows) definisce una zona rossa per il codice dello spazio utente (solo a 64 bit): 128 byte al di sotto della RSP che è garantita per non essere clobbata in modo asincrono. I gestori di segnali Unix possono essere eseguiti in modo asincrono tra due istruzioni dello spazio utente, ma il kernel rispetta la zona rossa lasciando uno spazio di 128 byte sotto la vecchia RSP dello spazio utente, nel caso in cui fosse in uso. Senza gestori di segnali installati, si ha una red-zone effettivamente illimitata anche in modalità a 32 bit (dove l'ABI fa non garantisce una zona rossa). Il codice generato dal compilatore o dalle librerie non può presumere che nessun altro programma (o libreria richiamata dal programma) abbia installato un gestore di segnali.

Quindi la domanda diventa: esiste qualcosa su Windows che possa eseguire codice in modo asincrono utilizzando lo stack dello spazio utente tra due istruzioni arbitrarie? (cioè un equivalente di un gestore di segnali Unix).

Per quanto ne sappiamo, SEH (Structured Exception Handling) è l'unico vero ostacolo a ciò che proponete per il codice in spazio utente su corrente Windows a 32 e 64 bit. (Ma i futuri Windows potrebbero includere una nuova funzione).
E credo che il debugger possa essere utile se si chiede al debugger di chiamare una funzione nel processo/thread di destinazione, come menzionato sopra.

In questo caso specifico, non toccando nessun'altra memoria oltre allo stack, o facendo qualsiasi altra cosa che potrebbe avere un errore, è probabilmente sicuro anche da SEH.


SEH (Structured Exception Handling) consente al software dello spazio utente di avere eccezioni hardware, come la divisione per zero, in modo simile alle eccezioni del C++. Queste non sono veramente asincrone: sono per le eccezioni innescate da istruzioni eseguite dall'utente, non per eventi che si sono verificati dopo un'istruzione a caso.

Ma a differenza delle normali eccezioni, una cosa che un gestore SEH può fare è riprendere da dove si è verificata l'eccezione. (@RossRidge ha commentato: I gestori SEH sono inizialmente chiamati nel contesto dello stack non avvolto e possono scegliere di ignorare l'eccezione e continuare l'esecuzione dal punto in cui si è verificata l'eccezione).

Quindi questo è un problema anche se non c'è un'eccezione catch() nella funzione corrente.

Normalmente le eccezioni HW possono essere innescate solo in modo sincrono, ad esempio da una clausola div o da un accesso alla memoria che potrebbe avere un errore con STATUS_ACCESS_VIOLATION (l'equivalente Windows di un errore di segmentazione SIGSEGV di Linux). È possibile controllare le istruzioni da utilizzare, in modo da evitare istruzioni che potrebbero errore.

Se si limita il codice ad accedere solo alla memoria dello stack tra la memorizzazione e il ricaricamento e si rispetta la pagina di protezione della crescita dello stack, il programma non avrà un errore se accede a [esp-4]. (A meno che non si sia raggiunta la dimensione massima dello stack (Stack Overflow), nel qual caso push eax e non è possibile riprendersi da questa situazione, perché non c'è spazio in pila da utilizzare per SEH).

Quindi possiamo escludere STATUS_ACCESS_VIOLATION come problema, perché se si verifica quando si accede alla memoria di stack, siamo comunque fregati.

Un gestore SEH per STATUS_IN_PAGE_ERROR potrebbe essere eseguito prima di qualsiasi istruzione di caricamento. Windows può disimpegnare qualsiasi pagina e reimpaginarla in modo trasparente se serve di nuovo (paginazione della memoria virtuale). Ma se si verifica un errore di I/O, Windows tenta di lasciare che il processo gestisca l'errore consegnando un messaggio STATUS_IN_PAGE_ERROR

Ancora una volta, se questo accade allo stack corrente, siamo fregati.

Ma il code-fetch potrebbe causare STATUS_IN_PAGE_ERRORe si potrebbe plausibilmente recuperare. Ma non riprendendo l'esecuzione nel punto in cui si è verificata l'eccezione (a meno che non si possa in qualche modo rimappare quella pagina su un'altra copia in un sistema altamente tollerante ai guasti?), quindi potremmo ancora essere a posto qui.

Un errore di I/O nel codice che vuole leggere ciò che è stato memorizzato sotto ESP esclude qualsiasi possibilità di lettura. Se non si aveva intenzione di farlo comunque, è tutto a posto. Un gestore SEH generico che non conosce questo specifico pezzo di codice non cercherebbe comunque di farlo. Penso che di solito un STATUS_IN_PAGE_ERROR cercherebbe al massimo di stampare un messaggio di errore o magari di registrare qualcosa, non di portare avanti qualsiasi calcolo stia avvenendo.

L'accesso ad altra memoria tra l'archiviazione e il ricaricamento della memoria sotto ESP potrebbe innescare un'anomalia STATUS_IN_PAGE_ERROR per che memoria. Nel codice di libreria, probabilmente non si può presumere che qualche altro puntatore passato non sia strano e che il chiamante si aspetti di gestire STATUS_ACCESS_VIOLATION o PAGE_ERROR.

I compilatori attuali non sfruttano lo spazio al di sotto di ESP/RSP su Windows, anche se fanno sfruttano la zona rossa in x86-64 System V (nelle funzioni foglia che devono versare/ricaricare qualcosa, esattamente come si sta facendo per int -> x87). Questo perché MS dice che non è sicuro e non sa se esistano gestori SEH che potrebbero tentare di riprendere dopo un SEH.


Cose che si pensa possano essere un problema nell'attuale Windows e perché non lo sono:

  • La pagina di guardia al di sotto dell'ESP: finché non si va troppo al di sotto dell'ESP corrente, si tocca la pagina di guardia e si attiva l'allocazione di altro spazio in pila anziché l'errore. Questo va bene finché il kernel non controlla l'ESP dello spazio utente e scopre che si sta toccando lo spazio in pila senza averlo prima "riservato".

  • recupero da parte del kernel delle pagine al di sotto di ESP/RSP: a quanto pare Windows attualmente non lo fa. Quindi, se si usa molto spazio in pila una volta sola, quelle pagine rimarranno allocate per il resto della vita del processo, a meno che non si usi manualmente VirtualAlloc(MEM_RESET) manualmente. (Il kernel sarebbe consentito Non è possibile fare questo, però, perché i documenti dicono che la memoria al di sotto di RSP è volatile. Il kernel potrebbe effettivamente azzerarla in modo asincrono, se vuole, mappandola copy-on-write in una pagina zero invece di scriverla nel pagefile sotto pressione di memoria).

  • APC (Chiamate di procedura asincrone): Possono essere eseguite solo quando il processo si trova in uno "stato di allerta", cioè solo quando si trova all'interno di un file call a una funzione come SleepEx(0,1). callUna funzione utilizza già una quantità sconosciuta di spazio al di sotto di E/RSP, quindi si deve già assumere che ogni call si accaparri tutto ciò che si trova al di sotto del puntatore allo stack. Quindi questi callback "asincroni" non sono veramente asincroni rispetto alla normale esecuzione, come lo sono i gestori di segnali Unix. (curiosità: POSIX async io usa i gestori di segnali per eseguire i callback).

  • Callback per applicazioni console per ctrl-C e altri eventi (SetConsoleCtrlHandler). Questo sembra esattamente come la registrazione di un gestore di segnali Unix, ma in Windows il gestore viene eseguito in un thread separato con il proprio stack. (Vedere il commento di RbMm)

  • SetThreadContextUn altro thread potrebbe cambiare il nostro EIP/RIP in modo asincrono mentre questo thread è sospeso, ma l'intero programma deve essere scritto in modo speciale perché ciò abbia senso. A meno che non sia un debugger a usarlo. Normalmente la correttezza non è richiesta quando un altro thread si occupa dell'EIP, a meno che le circostanze non siano molto controllate.

E apparentemente non ci sono altri modi in cui un altro processo (o qualcosa che questo thread ha registrato) possa innescare l'esecuzione di qualcosa in modo asincrono rispetto all'esecuzione di codice in spazio utente su Windows.

Se non ci sono gestori SEH che possano tentare di riprendere, Windows ha più o meno una zona rossa di 4096 byte sotto ESP (o forse di più se la si tocca in modo incrementale?), ma RbMm dice che nessuno la sfrutta in pratica. Questo non sorprende perché MS dice di non farlo e non si può sempre sapere se i chiamanti potrebbero aver fatto qualcosa con SEH.

Ovviamente tutto ciò che potrebbe in modo sincrono lo colpirebbe (come un call) deve essere evitato, come quando si usa la zona rossa nella convenzione di chiamata del System V x86-64. (Si veda ). (Vedere https://stackoverflow.com/tags/red-zone/info per maggiori informazioni).

nel caso generale (x86/x64 ) - l'interrupt può essere eseguito in qualsiasi momento, sovrascrivendo la memoria sotto il puntatore di stack (se viene eseguito sullo stack corrente). per questo motivo, anche se si salva temporaneamente qualcosa sotto il puntatore di stack, non è valido in modalità kernel - l'interrupt utilizzerà lo stack corrente del kernel. ma in modalità utente la situazione è un'altra - windows costruisce la tabella degli interrupt (IDT) in modo tale che quando l'interrupt viene sollevato - sarà sempre eseguito in modalità kernel e nello stack del kernel. Come risultato, lo stack in modalità utente (sotto il puntatore allo stack) non sarà influenzato. e sarà possibile utilizzare temporaneamente un po' di spazio nello stack sotto il puntatore, fino a quando non si effettuerà alcuna chiamata di funzione. se si verificherà un'eccezione (ad esempio per l'accesso a un indirizzo non valido), anche lo spazio sotto il puntatore allo stack sarà sovrascritto - l'eccezione della cpu inizierà ovviamente a essere eseguita in modalità kernel e nello stack del kernel, ma poi il kernel eseguirà il callback nello spazio utente via ntdll.KiDispatchExecption già nello spazio dello stack corrente. quindi in generale questo è valido in modalità utente di windows (nell'implementazione attuale), ma è necessario capire bene cosa si sta facendo. comunque questo è usato molto raramente, credo.


Naturalmente, come è corretto notare nei commenti che possiamo, in windows modalità utente scrivere sotto il puntatore allo stack - è solo il comportamento dell'implementazione corrente. questo non è documentato o garantito.

ma questo è molto fondamentale - è improbabile che venga cambiato: gli interrupt saranno sempre eseguiti solo in modalità kernel privilegiata. e la modalità kernel userà solo lo stack della modalità kernel. il contesto della modalità utente non è affatto affidabile. cosa succederà se il programma della modalità utente imposta un puntatore allo stack errato? diciamo da
mov rsp,1 o mov esp,1 e subito dopo questa istruzione viene sollevata un'interruzione. Cosa succede se inizia a essere eseguita su tale esp/rsp non valido? Tutto il sistema operativo si blocca. Proprio perché questa interruzione viene eseguita solo sullo stack del kernel e non sovrascrive lo spazio dello stack dell'utente.

Bisogna anche notare che lo stack è uno spazio limitato (anche in modalità utente), l'accesso al di sotto di 1 pagina (4Kb) è già un errore (bisogna sondare lo stack pagina per pagina, per spostare la pagina di guardia verso il basso).

e infine non c'è bisogno di accedere a [ESP-4], EAX - in quale problema decrementare ESP anche se abbiamo bisogno di accedere allo spazio dello stack in un ciclo enorme di tempo - il decremento del puntatore dello stack è necessario solo una volta - 1 istruzione aggiuntiva (non nel ciclo) non cambia nulla nelle prestazioni o nella dimensione del codice.

quindi, nonostante l'ufficialità, questo sarà il lavoro corretto in modalità utente di Windows, meglio (e non necessario) usare questo


Naturalmente la documentazione formale dice:

Uso della pila

Tutta la memoria oltre l'indirizzo attuale di RSP è considerata volatile

ma questo è per il caso comune, compresa la modalità kernel. Ho scritto della modalità utente e in base all'attuale implementazione


è possibile che in futuro windows aggiunga un apc "diretto" o alcuni segnali "diretti" - un po' di codice verrà eseguito tramite callback subito dopo l'ingresso del thread nel kernel (durante il solito interrupt hardware). dopo di che tutti gli esp sottostanti saranno indefiniti. ma fino a quando questo non esisterà. fino a quando questo codice funzionerà sempre (nelle build attuali) correttamente.

In generale (non specificamente in relazione a qualsiasi sistema operativo), non è sicuro scrivere sotto ESP se:

  • È possibile che il codice venga interrotto e che il gestore dell'interrupt venga eseguito allo stesso livello di privilegio. Nota: questo è tipicamente molto improbabile per il codice "user-space", ma estremamente probabile per il codice del kernel.

  • Si chiama qualsiasi altro codice (dove il parametro call o lo stack utilizzato dalla routine chiamata può cestinare i dati memorizzati sotto ESP)

  • Qualcosa di diverso dipende dall'uso "normale" dello stack. Questo può includere la gestione dei segnali, lo svolgimento di eccezioni (basate sul linguaggio), i debugger, i "protettori dello stack".

È sicuro scrivere sotto l'ESP se non è "non sicuro".

Si noti che per il codice a 64 bit, la scrittura al di sotto di RSP è incorporata nell'ABI x86-64 ("zona rossa"); ed è resa sicura dal supporto di catene di strumenti/compilatori e quant'altro.

Recensioni e valutazioni degli articoli

Vi invitiamo a difendere la nostra ricerca mostrando un commento e valutandolo, vi ringraziamo.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.