Un motore sarebbe non individuabile se la routine di decifratura fosse indistinguibile dal resto del codice senza una funzione di decifrazione ed il codice cifrato fosse indistinguibile da dati casuali. Fortunatamente, le restrizioni imposte ai programmi dal set di istruzioni del processore non permette ciò.
Gli attacchi si presentano con varie sfaccettature, ma sono in sostanza analoghe. Tali attacchi cercano frammenti di codice che potrebbero essere stati generati dal motore. Questo tipo di attacco è imperfetto poiché parti di un programma ``legale'' possono benissimo essere state generate da un motore. Inoltre, nascondere codice generato polimorficamente nel mezzo di un programma legale rende questo tipo di attacchi virtualmente impossibili.
L'attacco alla routine di cifratura più sciocco traccia il codice di decifrazione, cercando i codici operativi (opcodes) del processore che il motore non può aver generato. Utilizzando delle tecniche di anti-debugging esterne al motore, questo tipo di attacco viene bloccato. La maggior parte degli anti-virus più semplici possono essere bloccati se un virus utilizza tali tecniche.
Un attacco più sofisticato implicherebbe una analisi statistica, ma al momento gli attacchi reali sono differenti e si focalizzano sul codice cifrato. Potrebbe non esserci una sequenza di unità prevedibile, ma le unità hanno relazioni matematiche distinte tra loro poiché sono state tutte trasformate usando la stessa operazione. Se l'operazione inversa è nota, se ne può trarre un vantaggio.
E' possibile trasformare parti di codice cifrato e non cifrato, indipendentemente, in una forma comune. Per utilizzare questo fatto nel rilevamento supponiamo che una stringa sia la versione cifrata di una stringa non cifrata: calcoliamo cosa dovrebbe essere la loro forma comune, una volta per il plaintext ed una volta per il ciphertext, ed infine vediamo se le due forme corrispondono. Le manipolazioni necessarie per giungere a tale forma comune sono differenti a seconda del tipo di cifratura, ma per un singolo loop sono facilmente ottenibili come segue:
dove A e B sono unità plaintext, ed A' e B' sono il loro ciphertext. Ora possiamo derivare:
Quindi si vede che:
In particolare: se il codice era stato cifrato con un loop XOR.
Effettuiamo uno XOR sia su coppie consecutive di unità cifrate, sia su coppie di unità plaintext, e
confrontiamo i risultati. Si procede analizzando la coppia (D[n],D[n+1]), seguita dalla coppia
(D[n+1],D[n+2]), (D[n+2],D[n+3], e così via, con n=0,1,... e D è la dimensione dell'unità
(per esempio D=1 per i byte, D=2 per le word).
Vediamo un esempio che spiega meglio tutto ciò.
Sia una porzione del ciphertext di cui vogliamo ricavare la
corrispondente porzione del plaintext
(detta signature). Supponiamo che
l'operatore implicato nella codifica sia
. Dalla (1) si ottiene che:
Si ricava che:
Risolvendo col metodo di sostituzione si ottiene:
Quindi si ottiene un sistema di equazioni parametriche nel parametro . Se tentiamo con tutti
i possibili valori per
(nel caso fosse un byte sono 256) troveremo sicuramente una sequenza
che ha senso. A questo punto si può facilmente ricavare il valore della chiave K
perché sappiamo che
; se, applicando K all'intero ciphertext, otteniamo un
plaintext corretto, allora vuol dire che il valore di
che abbiamo scelto è quello
giusto.
Naturalmente tutto questo funziona ancora anche se utilizziamo un loop leggermente più complesso, come una somma in cui K è incrementato di 1 ogni iterazione, e viene trasformata una unità per iterazione.
Deriviamo che:
Quindi si vede che A' - B' = A - B - 1
Analogamente al caso precedente possiamo computare A' - B' e A - B - 1 e confrontarne i risultati. Come si vede chiaramente, le operazioni necessarie per trasformare il ciphertext ed il plaintext in una forma comune non sono uguali, mentre lo sono i risultati.
Tale attacco è chiamato crittoanalisi. Il numero degli errori che si possono commettere
nello scegliere il parametro ( nell'esempio precedente), diminuisce rapidamente al crescere
della lunghezza della signature con la seguente relazione:
Ora proviamo a cambiare dinamicamente il valore della chiave K aggiungendogli l'indice dell'iterazione che chiamiamo C.
Deriviamo che:
Non possiamo eliminare la chiave K! Quindi non possiamo più ricavare la forma comune come prima. La soluzione è ricostruire K usando l'attacco crittografico known plaintext.
Questo tipo di attacco ha la fortuna di essere facile su cifrature come queste:
A questo punto abbiamo una chiave K, non necessariamente quella del primo byte cifrato, ma che si riferisce ad una certa iterazione. Quindi quando c'era un semplice loop potevamo utilizzare la chiave K per decifrare tutto il codice, mentre ora dobbiamo prima ricostruire il contatore C e poi utilizzarlo con la chiave K.
Sottraendo membro a membro la (2) dalla (3), otteniamo:
Ora che abbiamo anche C possiamo decifrare l'N-esima unità (l'indice parte da 0) utilizzando la relazione:
Confrontando U con l'unità corrispondente del plaintext vediamo se la chiave K che ci siamo ricavati era quella giusta.