L'idea geniale di movfuscator è questa:
compilare un intero programma usando solo istruzioni MOV, poiché la MOV è nientemeno che Turing-complete.
compilare un intero programma usando solo istruzioni MOV, poiché la MOV è nientemeno che Turing-complete.
L'intero programma compilato avrà un lunghissimo listato assembler di questo tipo:
start: MOV ...Infatti tutto si può ridurre alla MOV, inclusi if/switch/loop e chiamate di funzioni.
MOV ...
MOV ...
...
MOV ...
MOV ...
JMP start
Sarà più lento da eseguire e richiederà più memoria ma praticamente tentare di disassemblarlo sarà impossibile, perché le funzioni e i calcoli non si distingueranno più - è solo una fastidiosissima e lunghissima sequenza di MOV (vedi per esempio un banale programmino in C di quindici righe che calcola numeri primi può essere "compilato" ad un listato di 5744 istruzioni MOV).
Vediamo come si fa.
Esempio 1: selezionare un valore da un array (cioè le singole variabili del programma possono essere indicate non con l'indirizzo di memoria ma con un numero, anche piccolo):
MOV R, 1 ; scegli la variabile 1...Esempio 2: con l'esempio precedente il confronto tra X e Y (CMP x,y) si può ridurre a:
MOV R, [array + R] ; ...indicizzando
MOV [X], 0 ; scrivi all'indirizzo X un byte 0Se X e Y sono lo stesso numero, allora la seconda MOV sovrascrive quello zero con un 1, e quindi alla fine R=1; altrimenti la terza istruzione leggerà il valore zero scritto dalla prima.
MOV [Y], 1 ; scrivi all'indirizzo Y un byte 1
MOV R, [X] ; leggi in R un byte dall'indirizzo Y
Più variabili (vere o temporanee) servono, e più si può allargare l'array.
Esempio 3: il costrutto IF x==y THEN z=100 si può ridurre a:
- una "CMP" come da esempio 2 (che restituirà 0 oppure 1 a seconda di x==y)
- usare il risultato 0/1 come da esempio 1, cioè come locazione in cui scrivere il valore 100
- ignorare la locazione 0 (scritta solo se "x==y" non risulta verificato)
- usare la locazione 1 (scritta solo se "x==y" risulta verificato) per scrivervi 100
Esempio 4: i costrutti while/loop si possono ridurre a un calcolo che viene eseguito solo se la condizione era verificata:
- una IF condizioneloop==1 THEN z=100 ELSE ignoraz=100;
Esempio 5: le chiamate di funzioni e l'allocazione dinamica della memoria si possono fare usando qualche array come "stack", e i costrutti indicati negli esempi 1,2,3.
In tal modo può funzionare perfino la ricorsione.
Esempio 6: le operazioni matematiche si possono eseguire usando tabelle (array) già calcolate. Per esempio, quando un programma vorrebbe eseguire X = Y * 2, basterà avere:
array Raddoppio = [ 0, 2, 4, 6, 8 ...]
MOV X, [ Raddoppio + Y ] ; se Y=3, troverà 6, ecc.Il codice compilato si riempirà di tabelle precalcolate, ma non saranno troppe visto che ogni volta che hai bisogno di moltiplicare per due puoi sfruttare la stessa tabella.
Anche le operazioni logiche AND OR NOT si possono ridurre a tabelle (array) da cui estrarre risultati usando MOV.
Le operazioni in virgola mobile possono essere emulate a suon di MOV grazie a una libreria soft-float scritta in C che una volta compilata diventa di appena mezzo milione di istruzioni MOV.
Infine: anche l'istruzione assembler XOR è Turing-completa. Quindi si può fare uno Xorfuscator. Anche la ADD è Turing-completa. Anche SBB, anche CMPXCHG/XCHG, eccetera. Caos!! ☺
Nella directory poc/crackme c'è un piccolo esempio: un programmino crackme che chiede una password prima di scrivere OK. Ai bei tempi bastava usare PCTOOLS per trovare il singolo byte del JUMP da modificare per far accettare qualsiasi password. Ora invece bisogna scovare tutte le MOV interessate all'emulazione di quel JUMP, all'interno di un file eseguibile che contiene centinaia di migliaia di MOV....
Nessun commento:
Posta un commento