venerdì 7 aprile 2017

Movfuscator

L'idea geniale di movfuscator è questa:

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 ...
       MOV ...
       MOV ...
       ...
       MOV ...
       MOV ...
       JMP start
Infatti tutto si può ridurre alla MOV, inclusi if/switch/loop e chiamate di funzioni.

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...
MOV R, [array + R]  ; ...indicizzando
Esempio 2: con l'esempio precedente il confronto tra X e Y (CMP x,y) si può ridurre a:
MOV [X], 0  ; scrivi all'indirizzo X un byte 0
MOV [Y], 1  ; scrivi all'indirizzo Y un byte 1
MOV R, [X]  ; leggi in R un byte dall'indirizzo Y
Se 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.

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
Con lo stesso metodo applicato più volte si possono usare più di due "rami", cioè costruire tutte le varianti if/else/switch.

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;
Questo significa che il codice del loop viene sempre eseguito, nel ramo da usare o nel ramo da ignorare (quest'ultimo scrive in una locazione che non verrà usata).

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