Domanda Reversing Tecniche di offuscamento del codice

alu

Utente Electrum
22 Settembre 2013
285
37
73
152
Salve a tutti! Sto lavorando ad un progettino per lo sviluppo di una libreria in C che fornisca una serie di operazioni per l'offuscamento di determinate parti del codice.

L'offuscamento del codice sarebbe destinato alla protezione della proprietà intellettuale, quindi per rendere più difficile debugging / reversing.

Ho fatto un po' di ricerche e finora ho in mente di implementare:
  • Offuscamento di puntatori;
  • Offuscamento di chiamate a funzioni;
  • Offuscamento di stringhe;
  • Code flow obfuscation con automi per funzioni;
Un piccolo esempio concreto (offuscamento di stringhe):
C:
#define ENCRYPT_STRING(str)    ...

char* decrypt(char *);

int main(void) { 
    // viene salvata una stringa non in plaintext, così non sarà direttamente visibile nel file compilato
    char* var = ENCRYPT_STRING("Stringa di prova"); 

    // viene stampata la stringa "Stringa di prova", attraverso la funzione 'decrypt' riusciamo a risalire alla stringa originale
    printf("%d\n", decrypt(var));
    return 0;
}

A questo punto avrei diverse domande.

  1. Ha senso / è possiible, secondo voi, implementare questo tipo di funzionalità attraverso una libreria in C o sarebbe meglio agire direttamente sul codice sorgente attraverso uno script che con un parser fa le modifiche al codice? Come viene fatto di solito l'offuscamento a livello 'professionale'?;
  2. Ci sono altre tecniche che mi consigliate di guardare oltre a quelle che ho già descritto? Sono un novizio in questo campo e sto cercando di raccogliere più informazioni possibili;
  3. Per quanto riguarda l'offuscamento dei puntatori e delle chiamate a funzioni, non mi è chiarissimo ne il perché farlo, ne la sua possibile implementazione. Non ho trovato molta documentazione (probabilmente non ho cercato con le keyword giuste) e ho ancora molti dubbi. Avete qualche lettura da consigliarmi?
Grazie in anticipo a chi leggerà e proverà ad aiutarmi
 
Non puoi fare nessuna di quelle cose tramite una libreria su cui fai linking statico o dinamico. Per fare l’esempio delle stringhe, il risultato nell’eseguibile compilato sarebbe che la stringa e’ visibile in chiaro, poi viene cifrata e poi decifrata di nuovo, ma rimane visibile dall’inizio.

I protettori professionali agiscono direttamente sull’eseguibile compilato, spesso estrapolando codice e dati per poi ricombinarli, protetti, in un nuovo eseguibile. Questa soluzione ovviamente non e’ affatto semplice ma nonostante il grande lavoro e’ la soluzione migliore. Se invece vuoi offuscare a livello di sorgente puo’ essere buono come esercizio, ma anche fare bene un parser di C non e’ facile, per fortuna ci sono librerie che possono facilitare il compito.

Per quanto riguarda il punto 2 ci sono diverse feature extra che hanno i packer professionali: anti debug, anti tampering, anti dump e infine alcuni hanno anche la più potente ed anche complessa: la virtualizzazione.

Il punto 3, serve principalmente a non fare capire il control flow da analisi statica, se per esempio apri l’exe nel disassembler e ad un certo punto prende un valore dallo stack, lo mette in rdx e fa call rdx e’ difficile capire dove andra’ a finire quella chiamata senza fare partire in debug e piazzare un breakpoint, in questo modo li costringi anche a bypassare l’antidebug. Questo lo ottieni usando bene i puntatori a funzioni e facendo giri extra al posto di chiamare direttamente la funzione.
 
Innanzitutto grazie mille per la risposta.
Comincio facendo una premessa: il lavoro che sto sviluppando sarà il frutto di una collaborazione tra la mia università e un'azienda e ho un tempo relativamente limitato per studiare e sviluppare qualcosa di effettivo (circa 2 mesi). Per questo sto cercando un compromesso tra qualcosa di utilizzabile ma che non richieda troppo lavoro.
Non puoi fare nessuna di quelle cose tramite una libreria su cui fai linking statico o dinamico. Per fare l’esempio delle stringhe, il risultato nell’eseguibile compilato sarebbe che la stringa e’ visibile in chiaro, poi viene cifrata e poi decifrata di nuovo, ma rimane visibile dall’inizio.
Per quanto riguarda questo punto, ieri probabilmente non mi sono spiegato benissimo.

C:
#define KEY  55
#define ENCRYPT_STRING(str)    {str[0] ^ KEY, str[1] ^ KEY, ... str[n] ^ KEY, 0}
Per l'encrypt pensavo ad una cosa del genere: utilizzando una macro non salvo la stringa in chiaro nel file compilato. Ovviamente questo è un approccio naive e stavo pensando già a diverse varianti, ma l'idea di base è appunto utilizzare il preprocessore per fare questo tipo di sostituzioni in giro.


I protettori professionali agiscono direttamente sull’eseguibile compilato, spesso estrapolando codice e dati per poi ricombinarli, protetti, in un nuovo eseguibile. Questa soluzione ovviamente non e’ affatto semplice ma nonostante il grande lavoro e’ la soluzione migliore..
Un approccio di questo tipo però è limitato dall'architettura del processore? Penso sia infattibile per questioni di tempo.


Il punto 3, serve principalmente a non fare capire il control flow da analisi statica, se per esempio apri l’exe nel disassembler e ad un certo punto prende un valore dallo stack, lo mette in rdx e fa call rdx e’ difficile capire dove andra’ a finire quella chiamata senza fare partire in debug e piazzare un breakpoint, in questo modo li costringi anche a bypassare l’antidebug. Questo lo ottieni usando bene i puntatori a funzioni e facendo giri extra al posto di chiamare direttamente la funzione.
Ok, penso di aver capito il senso, ma effettivamente come è implementata questa cosa? Sinceramente non saprei da dove partire, si tratta di tecniche implementate in assembly o è possibile far generare al compilatore una chiamata a funzione di questo tipo?
 
Innanzitutto grazie mille per la risposta.
Comincio facendo una premessa: il lavoro che sto sviluppando sarà il frutto di una collaborazione tra la mia università e un'azienda e ho un tempo relativamente limitato per studiare e sviluppare qualcosa di effettivo (circa 2 mesi). Per questo sto cercando un compromesso tra qualcosa di utilizzabile ma che non richieda troppo lavoro.

Per quanto riguarda questo punto, ieri probabilmente non mi sono spiegato benissimo.

C:
#define KEY  55
#define ENCRYPT_STRING(str)    {str[0] ^ KEY, str[1] ^ KEY, ... str[n] ^ KEY, 0}
Per l'encrypt pensavo ad una cosa del genere: utilizzando una macro non salvo la stringa in chiaro nel file compilato. Ovviamente questo è un approccio naive e stavo pensando già a diverse varianti, ma l'idea di base è appunto utilizzare il preprocessore per fare questo tipo di sostituzioni in giro.



Un approccio di questo tipo però è limitato dall'architettura del processore? Penso sia infattibile per questioni di tempo.



Ok, penso di aver capito il senso, ma effettivamente come è implementata questa cosa? Sinceramente non saprei da dove partire, si tratta di tecniche implementate in assembly o è possibile far generare al compilatore una chiamata a funzione di questo tipo?

Per l’ultima cosa e’ sicuramente possibile farlo in entrambi i modi. Comunque se hai cosi’ poco tempo se il tuo obbiettivo e’ proteggere il codice ti consiglio di usare un packer gia’ pronto, se invece e’ lo scopo della tua ricerca allora va bene. Per quanto riguarda le stringhe esistono degli offuscatori in file header basati su template C++, in modo da gestire stringhe di differente lunghezza e scegliere la chiave xor differente per ogni stringa. Se devi fare tutto in C puro a questo punto è meglio che le cifri a mano e le metti gia’ in base64 nel codice, oppure le scrivi dinamicamente con il parser del sorgente.
 
  • Mi piace
Reazioni: DanyDollaro
Comincio facendo una premessa: il lavoro che sto sviluppando sarà il frutto di una collaborazione tra la mia università e un'azienda e ho un tempo relativamente limitato per studiare e sviluppare qualcosa di effettivo (circa 2 mesi). Per questo sto cercando un compromesso tra qualcosa di utilizzabile ma che non richieda troppo lavoro.

Purtroppo con queste premesse ti posso già garantire che non ti sarà possibile ottenere risultati soddisfacenti.
2 mesi non sono assolutamente sufficienti per fare quello che chiedi. Sicuramente non bastano per implementare delle protezioni in linea con quelle del mercato attuale.
I packers commerciali hanno lavorato con interi team di sviluppatori per anni prima di ottenere la protezione che riescono a fornire oggi.
Pertanto, nell'ottica di sviluppare qualcosa di accettabile e di imparare qualcosa, il mio consiglio è quello di limitare le features all'essenziale.

Come ti è stato già suggerito, ad oggi la protezione più complessa da gestire è quella della virtualizzazione, perchè rende inutile l'utilizzo dei normali debuggers ed impone l'analisi dello specifico contesto per risalire al codice originale.
In un paio di mesi potresti riuscire ad implementare un semplice meccanismo virtuale (anche se non con tutti gli opcodes).
 
  • Mi piace
Reazioni: DanyDollaro