Domanda:
Perché i decompilatori del codice macchina sono meno capaci, ad esempio, di quelli per CLR e JVM?
Rolf Rolles
2013-03-27 15:12:24 UTC
view on stackexchange narkive permalink

I decompilatori Java e .NET possono (di solito) produrre un codice sorgente quasi perfetto, spesso molto vicino all'originale.

Perché non si può fare lo stesso per il codice nativo? Ne ho provati alcuni, ma non funzionano o producono un pasticcio di gotos e cast con i puntatori.

È fantastico che tu abbia scritto questo post, tuttavia deve ancora essere sotto forma di domande e risposte. Se potessi trasformare questo in una serie di domande, allora sarebbe ancora meglio :)
Va meglio?
Ti esponi davvero a come rendere difficile il ripristino del codice di alto livello? Salterei quella parte della domanda e mi limiterò a parlare della decompilazione. La tua risposta è molto buona anche se imo.
@IgorSkochinsky Hai appena definito il tuo decompilatore Hex-Rays schifoso con quella modifica? : P
Bene, stavo seguendo il sentimento generale che puoi leggere in molte di queste domande :)
Ho provato a renderlo più carino. Non sei sicuro che catturi ancora lo spirito della domanda Rolf?
Sì, funziona. Fondamentalmente l'ho scritto in modo che possa essere menzionato in futuro, quindi non mi interessa davvero quale sia il titolo. Tuttavia, il tuo titolo cattura perfettamente lo spirito dell'interrogatorio e della risposta, quindi mi sembra fantastico.
Due risposte:
#1
+40
Rolf Rolles
2013-03-27 15:12:24 UTC
view on stackexchange narkive permalink

TL; DR: i decompilatori del codice macchina sono molto utili, ma non aspettarti gli stessi miracoli che forniscono per i linguaggi gestiti. Per citare diverse limitazioni: il risultato generalmente non può essere ricompilato, manca di nomi, tipi e altre informazioni cruciali dal codice sorgente originale, è probabile che sia molto più difficile da leggere rispetto al codice sorgente originale meno i commenti e potrebbe lasciare strano artefatti specifici del processore nell'elenco di decompilazione.

  1. Perché i decompilatori sono così popolari?

    I decompilatori sono strumenti di reverse engineering molto interessanti perché hanno il potenziale per risparmiare molto lavoro. In effetti, sono così irragionevolmente efficaci per linguaggi gestiti come Java e .NET che "Java e .NET reverse engineering" è praticamente inesistente come argomento. Questa situazione fa sì che molti principianti si chiedano se lo stesso sia vero per il codice macchina. Purtroppo non è così. Esistono decompilatori di codice macchina e sono utili per risparmiare tempo all'analista. Tuttavia, sono solo un aiuto per un processo molto manuale. Il motivo per cui questo è vero è che i decompilatori del linguaggio bytecode e del codice macchina devono affrontare una serie diversa di sfide.

  2. Vedrò i nomi delle variabili originali nel sorgente decompilato codice?

    Alcune sfide sorgono dalla perdita di informazioni semantiche durante il processo di compilazione. Le lingue gestite spesso conservano i nomi delle variabili, come i nomi dei campi all'interno di un oggetto. Pertanto, è facile presentare all'analista umano i nomi creati dal programmatore che si spera siano significativi. Ciò migliora la velocità di comprensione del codice macchina decompilato.

    D'altra parte, i compilatori per programmi in codice macchina di solito distruggono la maggior parte di tutte queste informazioni durante la compilazione del programma (forse lasciandone alcune sotto forma di informazioni di debug). Pertanto, anche se un decompilatore di codice macchina fosse perfetto in ogni altro modo, renderebbe comunque nomi di variabili non informativi (come "v11", "a0", "esi0", ecc.) Che rallenterebbero la velocità della comprensione umana .

  3. Posso ricompilare il programma decompilato?

    Alcune sfide riguardano il disassemblaggio del programma. Nei linguaggi bytecode come Java e .NET, i metadati associati all'oggetto compilato descriveranno generalmente le posizioni di tutti i byte di codice all'interno dell'oggetto. Cioè, tutte le funzioni avranno una voce in una tabella in un'intestazione dell'oggetto.

    Nel linguaggio macchina d'altra parte, per prendere il disassemblaggio di Windows x86, ad esempio, senza l'aiuto di pesanti informazioni di debug come un PDB il disassemblatore non sa dove si trova il codice all'interno del binario. Vengono forniti alcuni suggerimenti come il punto di ingresso del programma. Di conseguenza, i disassemblatori del codice macchina sono costretti a implementare i propri algoritmi per scoprire le posizioni del codice all'interno del binario. In genere utilizzano due algoritmi: scansione lineare (scansione attraverso la sezione di testo alla ricerca di sequenze di byte note che di solito denotano l'inizio di una funzione) e attraversamento ricorsivo (quando si incontra un'istruzione di chiamata a una posizione fissa, considerare quella posizione come contenente codice ).

    Tuttavia, questi algoritmi generalmente non rileveranno tutto il codice all'interno del binario, a causa delle ottimizzazioni del compilatore come l'allocazione dei registri interprocedurali che modificano i prologhi delle funzioni causando il guasto del componente di sweep lineare e a causa del flusso di controllo indiretto naturale cioè chiamata tramite il puntatore a funzione) causando il fallimento dell'attraversamento ricorsivo. Pertanto, anche se un decompilatore di codice macchina non incontrasse problemi diversi da quello, non potrebbe generalmente produrre una decompilazione per un intero programma, e quindi il risultato non potrebbe essere ricompilato.

    Il codice / Il problema di separazione dei dati sopra descritto rientra in una categoria speciale di problemi teorici, chiamati problemi "indecidibili", che condivide con altri problemi impossibili come il problema dell'arresto. Pertanto, abbandona la speranza di trovare un decompilatore automatico del codice macchina che produca un output che può essere ricompilato per ottenere un clone del binario originale.

  4. Avrò informazioni su gli oggetti usati dal programma decompilato?

    Ci sono anche sfide relative alla natura del modo in cui linguaggi come C e C ++ vengono compilati rispetto ai linguaggi gestiti; Discuterò le informazioni sul tipo qui. Nel bytecode Java, c'è un'istruzione dedicata chiamata 'new' per allocare gli oggetti. Accetta un argomento intero che viene interpretato come un riferimento nei metadati del file .class che descrive l'oggetto da allocare. Questi metadati a loro volta descrivono il layout della classe, i nomi e i tipi dei membri e così via. Questo rende molto facile decompilare i riferimenti alla classe in un modo gradito all'ispettore umano.

    Quando un programma C ++ viene compilato, d'altra parte, in assenza di informazioni di debug come RTTI, la creazione di oggetti non viene condotta in modo pulito e ordinato. Chiama un allocatore di memoria specificabile dall'utente e quindi passa il puntatore risultante come argomento alla funzione di costruzione (che può anche essere inline, e quindi non una funzione). Le istruzioni che accedono ai membri della classe sono sintatticamente indistinguibili da riferimenti a variabili locali, riferimenti ad array, ecc. Inoltre, il layout della classe non è memorizzato da nessuna parte nel binario. In effetti, l'unico modo per scoprire le strutture dati in un binario spogliato è attraverso l'analisi del flusso di dati. Pertanto, un decompilatore deve implementare la propria ricostruzione del tipo per far fronte alla situazione. In effetti, il popolare decompilatore Hex-Rays lascia per lo più questo compito all'analista umano (sebbene offra anche l'utile assistenza umana).

  5. Fondamentalmente la decompilazione assomigliano al codice sorgente originale in termini di struttura del flusso di controllo?

    Alcune sfide derivano dal fatto che le ottimizzazioni del compilatore sono state applicate al binario compilato. La popolare ottimizzazione nota come "tail merging" fa sì che il flusso di controllo del programma venga mutilato rispetto ai compilatori meno aggressivi, il che di solito si manifesta con molte istruzioni goto all'interno della decompilazione. La compilazione di istruzioni switch sparse può causare problemi simili. D'altra parte, i linguaggi gestiti spesso hanno istruzioni di istruzione switch.

  6. Il decompilatore fornirà un output significativo quando sono coinvolti aspetti oscuri del processore?

    Alcune sfide derivano dalle caratteristiche architettoniche del processore in questione. Ad esempio, l'unità a virgola mobile integrata su x86 è un incubo di un calvario. Non ci sono "registri" in virgola mobile, c'è uno "stack" in virgola mobile, e deve essere tracciato con precisione affinché il programma venga decompilato correttamente. Al contrario, i linguaggi gestiti spesso hanno istruzioni specializzate per trattare i valori in virgola mobile, che sono essi stessi variabili. (Hex-Rays gestisce perfettamente l'aritmetica in virgola mobile.) Oppure si consideri il fatto che ci sono molte centinaia di tipi di istruzioni legali su x86, la maggior parte dei quali non sono mai prodotti da un compilatore regolare senza che l'utente specifichi esplicitamente che dovrebbe farlo tramite un intrinseco. Un decompilatore deve includere un'elaborazione speciale per quelle istruzioni che supporta in modo nativo, quindi la maggior parte dei decompilatori include semplicemente il supporto per quelli più comunemente generati dai compilatori, utilizzando assembly in linea o (nella migliore delle ipotesi) intrinseci per quelli che non supporta.

Questi sono solo alcuni degli esempi accessibili di sfide che affliggono i decompilatori del codice macchina. Possiamo aspettarci che i limiti rimarranno per il prossimo futuro. Pertanto, non cercare una bacchetta magica efficace quanto i decompilatori di linguaggio gestito.

preferisci una nuova risposta per ulteriori aspetti o modificarli nella tua risposta? Generalmente mi sento a disagio con l'editing a questo livello di ripetizione (forse è diverso per le beta private?), Perché finisce in una coda e così via. Ma comunque. Allora qual è? :)
Puoi sentirti libero di modificarlo o suggerire nuovi argomenti e io lo modificherò.
On 6. Quando il codice è passato attraverso l '* ottimizzazione della pipeline *, una sequenza logica di singole operazioni potrebbe essere mescolata con il blocco logico precedente e / o successivo di operazioni.
#2
+7
Ed McMan
2013-03-27 22:48:57 UTC
view on stackexchange narkive permalink

La decompilazione è difficile perché i decompilatori devono recuperare le astrazioni del codice sorgente che mancano dal target binario / bytecode.

Esistono diversi tipi di astrazioni:

  • Funzioni: L'identificazione del codice corrispondente a una funzione alta, con il suo ingresso, argomenti, valori di ritorno e uscita.
  • Variabili: le variabili locali in ogni funzione e qualsiasi variabile globale o statica.
  • Tipi: il tipo di ogni variabile e gli argomenti e il valore restituito di ciascuna funzione.
  • Flusso di controllo di alto livello: lo schema del flusso di controllo di un programma, ad esempio while (. ..) {if (...) {...} else {...}}

La decompilazione del codice nativo è difficile perché nessuna di queste astrazioni è rappresentata esplicitamente nel codice nativo. Quindi, per produrre un buon codice decompilato (cioè, non usando goto s ovunque), i decompilatori devono reinferire queste astrazioni in base al comportamento del codice nativo. Questo è un processo difficile e molti articoli sono stati scritti su come dedurre queste astrazioni. Vedi Balakrishnan e Lee per i principianti.

Al contrario, il bytecode è più facile da decompilare perché di solito contiene informazioni sufficienti per consentire il controllo del tipo . Di conseguenza, bytecode contiene tipicamente astrazioni esplicite per funzioni (o metodi), variabili e il tipo di ogni variabile. L'astrazione principale mancante nel bytecode è il flusso di controllo di alto livello.



Questa domanda e risposta è stata tradotta automaticamente dalla lingua inglese. Il contenuto originale è disponibile su stackexchange, che ringraziamo per la licenza cc by-sa 3.0 con cui è distribuito.
Loading...