Analisi dell'offuscamento di Apple FairPlay

28 agosto 2023 – 38 min – 7904 parole

FairPlay comprende una serie di algoritmi creati da Apple per la gestione dei diritti digitali (anche chiamato DRM, digital rights management). FairPlay è attualmente utilizzato per gestire la decrittazione delle applicazioni per iOS durante l’installazione delle stesse sui dispositivi Apple. Sappiamo infatti che Apple distribuisce tutte le applicazioni dell’Apple Store attraverso il formato file IPA. Il formato file IPA contiene al suo interno le informazioni crittate che verranno poi utilizzate dal sistema operativo per installare una applicazione. Tutte le informazioni crittate vengono gestite tramite FairPlay che si occupa di mantenere sicura la chiave di decrittazione e tutto il processo per evitare che, in mani sbagliate, sia possibile decrittare il contenuto dei file .ipa per condividere il contenuto di un app (magari pagata).

In questo articolo andremo a riassumere alcune misure di protezione statiche che sono riuscito a trovare all’interno di demoni user-space che gestiscono FairPlay, il sistema DRM utilizzato da Apple. Tutte le informazioni si ritengono aggiornate alla data dell’articolo; il sistema operativo dal quale sono stati estratti i binari è macOS 13.5.1.

I sistemi DRM e la protezione delle applicazioni Apple

La protezione della proprietà intellettuale nel digitale è da sempre un obiettivo da parte delle aziende che distribuiscono materiale con copyright. Come poter distribuire il contenuto senza che gli utenti riescano a copiarlo, visionarlo, editarlo e ridistribuirlo? Gran parte dei sistemi attuali utilizza una tecnologia DRM. In modo molto riassuntivo, i sistemi DRM funzionano nel seguente modo: ricevono in input un segreto (può essere un film, una immagine, ma anche un algoritmo) che non è leggibile, ad esempio file grezzi, elaborano l’informazione e trasmettono come output il contenuto originale. Tutto questo cercando di mantenere più nascosto possibile il procedimento attraverso cui si è riusciti ad ottenere l’informazione originale. I sistemi DRM sono molto utilizzati soprattutto per la visione di contenuto protetto da copyright: l’utente sottoscrive un contratto con un gestore di servizi che promette di inviare le informazioni richieste (film, serie tv, libri). Per evitare la copia dei contenuti (il contratto prevede un solo utente autorizzato alla visione con quel contratto), le informazioni devono essere accessibili in modo che non sia prevista alcuna copia o esportazione del contenuto.

In ottica Apple, ipotizziamo di voler scaricare un’applicazione o un nuovo gioco a pagamento tramite l’Apple Store. Effettuiamo la transazione e il nostro dispositivo ottiene un archivio standalone, pronto per essere installato. Tecnicamente parlando, se non ci fosse alcuna misura di protezione, una persona potrebbe copiare l’installer1 delle applicazioni e passarla ad una qualsiasi altra persona. Risultato? Perdita di guadagni da parte di Apple e dello sviluppatore che ha pubblicato l’applicazione dal momento che la copia dell’archivio è gratuita. Paradossalmente, trattando il formato file IPA come una sorta di contenitore non ci sarebbe alcuna misura tecnica per fermare la condivisione delle applicazioni (gli archivi sono standalone). È qui che la tecnologia FairPlay™ entra in gioco.

Come possiamo proteggere le informazioni contenute all’interno dell’archivio? È chiaro che bisogna in qualche modo nascondere il contenuto dell’archivio: così facendo, anche se dovessero estrarre dall’iPhone l’archivio IPA, gli attaccanti non potrebbero accedere al contenuto. Le informazioni andrebbero essere lette soltanto dai processi di sistema che sono adibiti all’installazione dell’applicazione. La successiva domanda sarebbe la seguente: come possiamo nascondere il contenuto? Qui si apre un vaso di Pandora senza eguali. Una delle idee più semplici e meno costose dal punto di vista dell’archivio è crittografare le informazioni tramite una chiave segreta. L’informazione verrebbe resa di nuovo leggibile tramite una semplice chiamata a decrypt(content, key).

La scelta della chiave segreta però non è un problema banale. Se pensiamo al processo di installazione, sappiamo che l’informazione ad un certo punto andrà decriptata, ovvero resa di nuovo leggibile. Possiamo impostare una chiave statica che vale per tutti gli iPhone venduti? Certamente no. La scelta di una chiave statica (ovvero hardcoded) pone svariate seccature: gli attaccanti avrebbero solo da trovare un’unica chiave per poter accedere a tutti gli archivi creati dall’Apple Store. Tuttavia, una volta scoperta la chiave, il resto verrebbe naturale: gli installer verrebbero decrittati in poco tempo e la rete si troverebbe inondata di archivi “crackati”. La scelta della chiave statica potrebbe sembrare vantaggiosa da applicare, magari suddividendo la “visibilità” di una serie di chiavi per dispositivo (iPhone 12 avrà una certa chiave, iPhone 13 una diversa) sfruttando l’hardware2. Ma anche questa inventiva non funziona: la condivisione della chiave è su più dispositivi!

La soluzione in realtà include la generazione di chiavi “dinamiche”, ovvero che dipendano esclusivamente dal dispositivo di installazione, dall’account dell’utente che ha pagato le applicazioni e da una serie di metadata scambiati durante la transazione (per evitare lo spoofing). Apple provvede a crittare il contenuto del file IPA nel momento in cui riceve una nuova richiesta da parte dell’Apple Store: la crittazione avviene con una chiave pubblica associata all’account Apple. Una volta ricevuto, l’archivio viene decompresso e il singolo binario viene decrittato utilizzando la chiave privata all’interno del dispositivo. L’applicazione viene così installata in chiaro e rimane in chiaro all’interno del dispositivo Apple.

Il punto in cui avviene la decrittazione costituisce un grande punto di centralizzazione che potrebbe essere utile da analizzare per cercare di ottenere l’IPA decrittato. Se gli attaccanti potessero capire come viene generata la chiave privata a partire dal disposito e dall’account associato, il sistema di protezione di Apple fallirebbe all’istante. Rispetto al rischio di spoofing, Apple include all’interno dei suoi dispositivi alcuni stratagemmi per dichiarare ai servizi cloud (Apple Store, iCloud, Apple Signing) che effettivamente lo scambio di pacchetti ha origine da un dispositivo Apple e non da un dispositivo Apple emulato o simulato. Sfortunatamente per una serie di limiti tecnici non è possibile provare al 100% “non sono un dispositivo simulato, dammi il binario, sono [myaccount@icloud.com]”.

L’analisi statica e le tecniche di offuscamento

FairPlay costituisce una parte tecnologia molto importante della competenza tecnica di Apple in materia di protezione della proprietà intellettuale. Diversi brevetti sono stati sviluppati per descrivere questa tecnologia e proteggerla: US8934624B2: Decoupling rights in a digital content unit from download, ES2373131T3: Safe distribution of content using descifrado keys. Proteggere come il processo di decrittazione funziona è il principale obiettivo di una altra serie di tecnologie di “anti-reverse engineering” che prendono il nome di offuscamento del software.

Sappiamo infatti che tra la vasta gamma di tecniche che un qualsiasi addetto del settore può mettere a punto per analizzare un software, esiste una procedura chiamata analisi statica che consente di indagare più nello specifico alcune parti di un software senza mandarlo in esecuzione. Tecnica principale dell’analisi statica è il reverse engineering, ovvero la ricostruzione del codice originale inferendo le informazioni dal codice grezzo binario.

I binari grezzi infatti constano di due principali informazioni per funzionare: dati e istruzioni. Attraverso una moltetudine di passaggi, programmi come Ghidra, IDA o Binary Ninja possono ricostruire gran parte del codice sorgente originale. Seppur non perfettamente, l’analista del software è in grado di desumere gran parte della semantica del software: come funziona, quali metodi chiama e quali informazioni utilizza del sistema operativo sono alcuni degli esempi di domande a cui possiamo rispondere attraverso l’ingegneria inversa.

L’ingegneria inversa permette di ricavare con un buon grado di approssimazione gli algoritmi che FairPlay utilizza per poter decrittare il contenuto. Attraverso i dati è possibile poi cercare di capire come viene costruita la chiave segreta e con abbastanza sforzi un attaccante potrebbe capire come copiare il metodo di decrittazione per sviluppare uno strumento di decrittazione. Derivare l’algoritmo sarebbe un problema per Apple e per i suoi investitori. La soluzione per proteggere le istruzioni e i dati consiste nell’applicare alcune forme di offuscamento che rendono più difficile il processo di analisi del reverse engineering.

Le forme di offuscamento fanno parte di una disciplina dell’informatica chiamata Sicurezza del Software che si prefigge di proteggere il codice e le informazioni contenute all’interno di un programma. Esempi di applicazioni dell’offuscamento includono: la protezione della proprietà intellettuale, rendere più difficile il reverse engineering per evitare il rinvenimento di vulnerabilità, la mitigazione di exploit. Queste tecniche sono applicate alla sintassi di un programma (ovvero alle istruzioni grezze) e consentono di nascondere gran parte della semantica originale: manipolare codice grezzo è molto complesso e per questo è necessario avere delle solide basi per poter modificare il codice senza effetti collaterali.

Come vedremo nei paragrafi successivi, FairPlay è stato costruito in maniera da nascondere gran parte delle istruzioni e dati. Queste tecniche tuttavia sono realmente efficaci? Tenteremo di rispondere a fine articolo. Ricordiamo, infatti, che le tecniche di offuscamento e la protezione del codice sono delle tecniche che devono essere sempre cambiate di rilascio in rilascio dal momento che non esiste una soluzione definitiva per proteggere un software. Attraverso l’offuscamento siamo in grado di complicare tutti i tentativi di reverse engineering, ma non possiamo certamente prevenire o impossibilitare l’analisi del binario.

Il binario fairplayd

Nei prossimi paragrafi tenteremo di analizzare in dettaglio un demone lato utente che è possibile trovare all’interno della famiglia di sistemi operativi della Apple. L’utilizzo di FairPlay non è circoscritto solo alla piattaforma mobile iOS: FairPlay viene anche adoperato da macOS per gestire un canale sicuro attraverso cui veicolare il contenuto digitale da proteggere (film, serie TV..). Questa sotto-tecnologia prende il nome di FairPlay Streaming e consente di distribuire contenuto protetto da copyright crittando il contenuto. Un documento che riassume ad alto livello il funzionamento è FairPlay Streaming Overview.

Vogliamo quindi scoprire di più sull’utilizzo di FairPlay all’interno di macOS e vedere se gli algoritmi sono stati protetti tramite offuscamenti. Scopriamo che gran parte della gestione di FairPlay viene assegnata ad un framework chiamato CoreFP.Framework (Core Fair Play) presente all’interno della cartella /System/Library/PrivateFrameworks. I private frameworks sono un insieme di librerie adibite ad alcune specifiche funzionalità di macOS, considerate private, ovvero non sono state rilasciate per essere utilizzate pubblicamente e tutti i metodi contenuti all’interno sono da ritenersi validi solo per le applicazioni Apple.

CoreFP.Framework viene attualmente utilizzato da alcune applicazioni e demoni come Safari e AMPLibraryAgent. Nota a parte prima di proseguire: se avete un private framework e siete curiosi dove attualmente viene utilizzato, è possibile eseguire il comando lsof | grep -i [nome_framework] dove nome_framework è il nome della libreria framework. Come risultato vedremo una serie di processi attivi che hanno aperto il private framework; in questo caso, ci soffermiamo brevemente su AMPLibraryAgent. AMPLibraryAgent è un demone dello spazio utente che viene utilizzato per gestire i media dell’utente (TV.app e Music.app). AMPLibraryAgent è una sorta di processo intermedio tra il contenuto crittato proveniente dai server di Apple e l’utente finale che interagisce con il contenuto decrittato attraverso i client TV.app e Music.app.

FairPlayd è il demone che viene richiamato da AMPLibraryAgent ed è utilizzato nella pratica per decrittare il contenuto. Incominciamo ad analizzare quindi il contenuto della cartella CoreFP.Framework. Seppur i privateframework siano contenuti all’interno di una cache dinamica chiamata dyld, i binari di CoreFP.Framework sono disponibili senza particolari accorgimenti che l’utente deve effettuare sulla cache dinamica. All’interno di CoreFP, possiamo trovare: CoreFP e fairplayd. CoreFP è il binario che viene utilizzato dai processi di sistema e costituisce la libreria, mentre fairplayd è il demone spazio utente. Il demone spazio utente utilizza la componente del kernel chiamato FairPlayIOKit.

Estraiamo la versione x86_64 di fairplayd e salviamolo in una cartella a nostra scelta tramite il comando lipo -extract x86_64 fairplayd ~/fairplayd. Il file fairplayd è un classico file Mach-O eseguibile, non presenta particolari campi all’interno dell’intestazione degni di nota. Quindi importiamolo dentro uno dei nostri strumenti di reverse engineering (a piacimento possiamo utilizzare Ghidra, IDA, Binary Ninja oppure Hopper). Utilizziamo per praticità in questo articolo IDA, anche se dobbiamo avere particolare attenzione nell’importare il binario in altri strumenti (a fine articolo spiegheremo il perché). Lasciamo che IDA lavori per ricostruire le informazioni all’interno del binario (attraverso il parsing delle sezioni, disassembling, decompilazione).

Screenshot di IDA

Di default IDA apre il binario posizionando il lettore sul simbolo principale, ovvero _main, il punto di ingresso del demone fairplayd. Come possiamo notare, tocchiamo subito con mano la potenza dell’offuscamento e dopo poco ci rendiamo conto che l’intero binario in realtà è stato costruito in modo da offuscare tutte le istruzioni. Conferma di questo è data anche dal decompilatore, di seguito un breve estratto:

v298 = &v297;
v297 = (((unsigned int) v298 &0x52491520 | (2 *(_DWORD) v298) &0x80120A40) ^ 0x40090520) + ((-1704077140 - ((unsigned int) v298 &0x8924A850)) &0x8924A854) + ((((unsigned int) v298 &0x24924288) + 689062540) &0x24924288 | (2 *(_DWORD) v298) &0x506D9510);
v302 = 62;
((void(__fastcall*)(__int64, _QWORD, _QWORD, _QWORD))((char*) *(&off_1002B0BF0 + (unsigned int)(unsigned __int8)((unsigned __int8) v298 ^ byte_100237430[byte_1002AEEF0[(unsigned __int8) v298] ^ 0x3A]) +944) -790860942))(31 LL,0 LL,0 LL,0 LL);
LODWORD(v303) = 1312628203 *((unsigned int) &v303 ^ 0x4EBE92AB) + 8;
((void(__fastcall*)(unsigned __int64 *))((char*) *(&off_1002B0BF0 +(unsigned int)(unsigned __int8)(byte_100237330[byte_1002AEDF0[(unsigned__int8) &v297] ^ 0x18] ^ (unsigned __int8) &v297) +	532) -1051853286))(&v303);
LODWORD(v303) = 2064956458 - 1106503637 *(((unsigned int) &v303 - 2 *((unsigned int) &v303 &0x6F01D10) + 116399381) ^ 0x92F4EBC6);
sub_10015D450(&v303);
v21 = HIDWORD(v303);
v22 = (HIDWORD(v303) == 1923298241) | 2;

Panico! L’analista che si trova di fronte ad un codice simile ha poche scelte: abbandonare il reverse engineering oppure provare ad indagare più a fondo. La maggior parte delle persone che applicano le tecniche di offuscamento sperano che il più delle volte l’analista scelga la prima strada. Se è troppo complicato analizzare il comportamento di un’applicazione tramite il reverse engineering, non ne vale la pena. Tuttavia, è necessario che l’offuscamento sia ben realizzato prima che ci possano essere dei grandi scivoloni. In questo articolo cercheremo di scoprire che in realtà basta una semplice occhiata al codice grezzo per identificare alcuni pattern comuni di offuscamento. Nei prossimi paragrafi introdurremo alcune tecniche di offuscamento e come sono state implementate.

Prima di proseguire, dobbiamo menzionare che esistono tecniche di offuscamento diverse in relazione alla risorsa da proteggere. Come vedremo successivamente, tecniche di offuscamento diverse hanno un costo diverso: la tecnica dei predicati opachi ha tutt’altro costo rispetto al control flow flattening. Detto questo iniziamo!

Consiglio utile: con la presenza di offuscamenti applicati alle istruzioni e ai dati, è bene cercare di utilizzare la vista del decompilatore il meno possibile. Il decompilatore infatti deduce alcune informazioni di alto livello da come le istruzioni macchina sono state poste all’interno del binario e su quali dati lavorano. Dal momento che gran parte delle istruzioni sono volte a complicare il risultato del decompilatore, abbandonare il risultato del decompilatore risulta sempre un’ottima idea.

Mixed Boolean Arithmetic Expression

La prima tecnica di offuscamento che affrontiamo è la tecnica dell’offuscamento dei dati attraverso le espressioni miste con operatori algebrici (somma, sottrazione, moltiplicazione, divisione) e booleani (operazioni logiche). Questo tipo di espressioni vengono utilizzate quando vi è la necessità di nascondere un dato costante numerico all’interno di un programma. Un dato costante potrebbe rappresentare un valore utilizzato in un algoritmo di crittazione (“Nothing-up-my-sleeve-number”), ma anche una stringa, un indirizzo di memoria e molto altro. Ma non solo! Se durante l’elaborazione, ad esempio durante la decrittazione, un algoritmo crittografico esegue una semplice addizione è possibile rendere più complessa l’espressione aritmetica.

Esempi di espressioni aritmetiche-booleane sono: v298 & 0x52491520 | (2 *(_DWORD) v298) & 0x80120A40) ^ 0x40090520) + ((-1704077140 - (v298 & 0x8924A850)) & 0x8924A854) + ((((unsigned int) v298 & 0x24924288) + 689062540) &0 x24924288 | (2 * v298) & 0x506D9510). Ho scritto già in un post precedente come poter creare queste espressioni, applicando delle regole di trasformazione. Il procedimento che Apple ha utilizzato è lo stesso: si prenda una costante del codice, si riscrive la costante utilizzando degli operatori aritmetici e si applicano successivamente le trasformazioni. Abbiamo già una espressione? Continuiamo ad applicare le regole di trasformazioni. Nota che solo alcune trasformazioni possono essere applicate dal momento che non modificano la semantica dell’espressione originale. Alla fine del processo, l’espressione viene tradotta nuovamente in linguaggio macchina per poter essere reinserita all’interno del binario.

Nel caso di fairplayd, le costanti numeriche rappresentano la maggior parte delle volte gli indirizzi a cui saltare da un blocco elementare ad un altro. Indirizzi fanno riferimento ad altri blocchi elementari oppure agli stub utilizzati per chiamare metodi di altre librerie. Parleremo più in dettaglio di questa opzione nello offuscamento del flusso di controllo.

Possiamo notare in realtà due tipi di offuscamento delle espressioni booleane a seconda del tipo di istruzione assembly utilizzata. Possiamo identificare un classico esempio di offuscamento tramite una espressione booleana:

*(_BYTE *)(a1 + v2) = -13 * (-71 * (59 * *(_BYTE *)(a1 + v2) - 107) + 71 * (v2 & 0xF ^ 0x9C)) - 111;

Questo in assembly è tradotto come:

cdqe
movzx   ecx, byte ptr [rdi+rax]
imul    ecx, 0x3B
add     cl, 0x95
movzx   ecx, cl
imul    ecx, 0xB9
mov     edx, eax
and     edx, 0x0F
xor     edx, 0x9C
imul    edx, 0x47
add     edx, ecx
imul    ecx, edx, 0x0D
add     cl, 0x91
mov     [rdi+rax], cl

Notiamo che queste istruzioni assembly sono abbastanza comuni da trovare all’interno del set di istruzioni Intel x86_64 e con un po’ di sforzi è possibile ridurre l’espressione in una espressione simplificata attraverso alcuni framework, come Msynth. Queste istruzioni sono anche classificate storicamente come SISD: una singola espressione che agisce su un singolo dato (la somma tra il registro cl e 0x95 non ha effetti collaterali sugli altri registri).

Un altro tipo di espressione logico aritmetica che attualmente ho trovato riguarda un altro tipo di istruzioni. Il set di istruzioni Intel x86_64 utilizza un sottosistema chimato MMX che fa parte della famiglia di istruzioni SIMD (Single Instruction, Multiple Data) e consente di operare con una singola istruzione su più dati. Ad esempio un blocco elementari di queste istruzioni sono:

movdqu  	xmm0, xmmword ptr [rdi+rdx]
pmovzxbw 	xmm3, xmm0
punpckhbw 	xmm0, xmm0
movdqa  	xmm1, cs:xmmword_100216450
pmullw  	xmm0, xmm1
pand    	xmm0, xmm6
pmullw  	xmm3, xmm1
pand    	xmm3, xmm6
packuswb 	xmm3, xmm0
paddb   	xmm3, cs:xmmword_100216470
pshufd  	xmm0, xmm3, 0EEh
pmovzxbd 	xmm8, xmm0
pshufd  	xmm0, xmm3, 0FFh
pmovzxbd 	xmm13, xmm0
pshufd  	xmm0, xmm3, 55h ; 'U'
pmovzxbd 	xmm9, xmm0
pmovzxbd 	xmm10, xmm3
movdqa  	xmm0, cs:xmmword_100216600
pmulld  	xmm10, xmm0
pmulld  	xmm9, xmm0
pmulld  	xmm13, xmm0
pmulld  	xmm8, xmm0
movdqa  	xmm0, xmm14
movdqa  	xmm4, cs:xmmword_100216480
pand    	xmm0, xmm4

Questo tipo di operazioni sono complesse da tradurre in codice ad alto livello perché sono dipendenti dall’architettura e dal fatto che una istruzione possa essere convertita in più istruzioni ad alto livello (ad esempio: paddb consente di fare l’addizione a 128 bit tra il registro xmm3 e il dato posizionato all’indirizzo xmmword_100216470). IDA risolve il problema definendo delle funzioni come mmu_addb(xmm3, xmmword_100216470) ma non sappiamo tuttavia cosa succede all’interno.

Idea: Il calcolo SIMD apre una serie di opportunità per l’offuscamento basato sulle espressioni MBA: è possibile utilizzare la tecnologia per il calcolo matriciale/parallelo per offuscare una certa costante? L’idea sarebbe avere una serie di espressioni MBA da calcolare in parallelo per trovare il risultato di una costante. Questo abbatterebbe l’overhead dato dall’introduzione della complicazione di una espressione (e dal momento che ho un overhead più basso, posso aumentare il grado di illegibilità dell’espressione). In più sarebbe dipendente dall’architettura di Apple Silicon, rendendo più difficile l’emulazione del calcolo. Eventuali problemi di questo approccio includono: trovare delle equazioni che possono essere eseguite in parallelo, verificare come interagire con i core paralleli (tramite astrazione software?), verificare la semantica delle espressioni.

La tecnica di offuscamento tramite le espressioni mixed boolean arithmetic è stata ampiamente utilizzata nel binario fairplayd. È possibile trovare un ottimo esempio di come questo offuscamento sia stato utilizzato nella funzione sub_1001F5D32: la più grande funzione al cui interno si trovano espressioni aritmetico booleane. Esistono al momento diversi metodi per cercare di riassumere le espressioni aritmetiche booleane, ma la maggior parte si basa su un’unica tecnica chiamata esecuzione simbolica e prevedono l’utilizzo di Proof Solver come z3 per verificare la semantica tra l’espressione offuscata e l’espressione sintetizzata.

Il miglior strumento che al momento è possibile utilizzare per risolvere le espressioni aritmetico booleane è goomba, sviluppato da HexRays e disponibile di default nelle versioni 8.3 di IDA Pro e IDA Teams. Un’ottima alternativa è il framework Msynth sviluppato da Tim Blazytko che riesce ad ottenere buoni risultati. In definitiva, se da un lato l’offuscamento MBA rappresenta una buona alternativa per rendere più complicate le espressioni, dall’altra parte esistono molti strumenti che consentono di risolvere e di riscrivere le espressioni.

Opaque Predicates

I predicati opachi sono un’altra tecnica molto “economica” per introdurre offuscamento all’interno delle istruzioni. Questa tecnica consiste nell’introduzione di alcune condizioni sempre vere o sempre false che portano il decompilatore ad esplorare blocchi di istruzioni con utilità pari a zero. Le condizioni sempre vere o sempre false includono un salto diretto o indiretto a blocchi elementari che non verranno mai eseguiti: non presentano funzionalità aggiuntive, aggiungono solamente complessità alle funzioni da analizzare.

Ipotizziamo ad esempio di avere il seguente codice sorgente:

int sum(int a, int b){
	int result = a + b + c;
	return result;
}

Per proteggere la parte data del risultato possiamo riscrivere il codice in questo modo:

int sum(int a, int b){
	int result = a + b;

	if(a == 0 && a == 1 && a - 4 >= 55 && (a * 4 - 36 * 0xff - 0xc) < 2){
		result += 1 * 4 << 2 - 0x5c;
	} else if (a == 5 && a != 5){
		if(b > 4 && b < 4 && b != 4 && 352610 == 122){
			result += 50 * 0xf5 * 352610;
			result += decrypt(key);
		}
	} else {
		return result + decrypt(key);
	}
}

Con nostro stupore scopriremo che il decompilatore faticherebbe a ricostruire il check originale, pertanto non si accorgerebbe di alcuni controlli sempre veri o sempre falsi. La difesa data da questi predicati opachi è proporzionale al grado di illegibilità e complessità data dal controllo effettuato tra gli if (ad alto livello) o le varie istruzioni algebrico aritmetiche date prima dell’istruzione cmp. Il codice all’interno degli if viene presentato come dead code ovvero codice che non verrà mai eseguito: in fase di decompilazione tuttavia non è possibile riconoscere tra il codice che andrà in esecuzione e codice morto. I predicati opachi quindi aggiungono gradi di “confusione” all’analisi binaria.

Un tipico esempio di questa trasformazione è la procedura sub_100005FC0: dopo il classico prologo, possiamo trovare una chiamata a funzione e poi due diversi rami d’esecuzione. La condizione per saltare dentro un ramo oppure proseguire è data dall’istruzione test al, 2: in alto livello la condizione sarebbe uguale a if (al % 2 == 0), ovvero se il numero memorizzato all’interno di al è pari prosegui con l’esecuzione, altrimenti salta a loc_10000505F. L’istruzione test in realtà esegue l’AND tra il registro al e il valore immediato 0x2: eseguire un AND con un valore immediato vuol dire controllare il resto della divisione del registro con il valore immediato al & 2 == al % 2.

Visualizzando il graph tree della funzione possiamo vedere come il blocco elementare appena sotto la condizione è un blocco molto corposo.

Graph tree della funzione su IDA

Intuitivamente verrebbe da pensare che un blocco elementare molto corposo sia una parte integrante dell’elaborazione. Se il programma non fosse offuscato, è un’ipotesi molto concreta. Tuttavia dobbiamo immaginare che gli sviluppatori della Apple hanno complicato le istruzioni proprio per mettere in difficoltà gli strumenti automatici di analisi binaria. In questo caso, il blocco elementare più corposo (branch TRUE della condizione al % 2 == 0) rappresenta un chiaro esempio di predicato opaco. Infatti, se vedessimo il blocco elementare più consistente, potremo vedere una serie di operazioni assembly inutili.

Graph tree della funzione su IDA

Notare che lo screenshot è stato tagliato per evitare di riempire l’intero articolo con l’immagine del blocco elementare. Ci chiediamo: è esattamente un blocco elementare che risulta inutile ai fini della computazione? La condizione al % 4 == 0 è sempre vera o sempre falsa? Il compito dell’offuscamento sarebbe quello di prevenire qualsiasi tipo di deduzione all’interno della condizione. Ed è così: non riusciamo a prevedere se l’istruzione avrà sempre un risultato costante oppure no.

Quello che però possiamo fare è vedere il blocco elementare presente al di sotto del blocco elementare della condizione TRUE al % 4 == 0. Il blocco elementare che stiamo guardando rappresenta quindi la condizione FALSE (ovvero seguendo jnz loc_100005F2F, arriviamo ad analizzare la condizione loc_100005F2F). Un buon metodo per verificare se il blocco elementare più grande è effettivamente utile o meno è vedere se esistono delle dipendenze tra i dati utilizzati nel blocco elementare sospetto e gli altri blocchi elementari.

In questo caso, possiamo notare che il blocco elementare carica l’indirizzo di xmmword_1002B6460, utilizzato precedentemente nel blocco elementare “sospetto”.

Blocco elementare condizione FALSE dell’if

Nel blocco elementare sospetto la locazione di memoria xmmword_1002B6460 viene riscritta da un dato presente in un’altra locazione di memoria. Risulta difficile quindi verificare la complessa dipendenza tra le istruzioni! Tuttavia in questo caso, è semplice verificare la dipendenza dal momento che il codice più “importante” per questa funzione può essere tradotto così:

xmmword_1002B5460 = 0x0EC0C7C941423B0F77D59F9E25CFAC016;
xmmword_1002142F0 = 0x6432B8A1D491C1746754EB00EF0F9478;

// prologo
[... omissis ...]
// eax = 
if (al % 4 == 0){
	// dead code
	xmmword_1002B5460 = xmmword_1002142F0;
}
memcpy(v6, &xmmword_1002B5460, 0x1000);

// epilogo

Quale valore assume la variabile xmmword_1002B5460? Molto dipende dalla condizione dell’if. Tuttavia anche nell’incertezza, l’offuscamento dato dal predicato opaco risulta essere poco potente: abbiamo due possibilità di scelta, questo significa che possiamo propagare gran parte delle modifiche in modo parallelo (verificando cosa succede se entriamo dentro al blocco della condizione TRUE o dentro al blocco della condizione FALSE). Altre domande che potrebbero venire in mente:

Per rispondere alla domanda “quindi sub_100005FC0 cosa fa nella pratica?”, basta vedere il blocco elementare appena dopo la chiamata a funzione memcpy. Ricordiamoci che la signature (ovvero la dichiarazione della funzione) è la seguente: sub_100005FC0(int64 a1, int a2).

for ( i = 0; i != a2; ++i ){
	v4 = i + 15;
	if ( i >= 0 )
		v4 = i;

	*(_BYTE *)(a1 + i) = -13 * v6[256 * (__int64)(int)(i - (v4 & 0xFFFFFFF0))
	 	+ (unsigned __int8)(59 * *(_BYTE *)(a1 + i) - 107)] - 111;
}

La procedura sub_100005FC0 procede quindi a iterare sul buffer v6 per deoffuscare il contenuto della parola xmmword_1002B5460. L’istruzione più onerosa, computazionalmente parlando, è una espressione MBA da deoffuscare. Si rimanda alla sottosezione MBA per capire come poter de-offuscare l’espressione e riscriverla. Se avete problemi con i puntatori, posso riscrivere l’espressione come:

a1[i] = -13 * v6[256 * (i - (v4 & 0xFFFFFFF0)) + 59 * a1[i] - 107] - 111;

La misura di offuscamento in questo caso non è efficace e consente di recuperare gran parte delle informazioni originarie. Ci sono numerosi casi della tendenza ad includere predicati opachi “inutili” (come questo analizzato) all’interno del programma fairplayd. I predicati opachi introdotti da Apple non fanno venire il mal di testa: un buon analista del software riuscirebbe a distinguere tra rami morti e istruzioni che non possono essere eseguite. Bisogna rivedere la costruzione dei predicati opachi per rendere ancora più complessi i blocchi elementari.

Idea: per rendere ancora più efficace l’offuscamento, servirebbe rendere più complesso ogni blocco elementare (cercando magari di rendere più complicato il flusso di controllo). Un esempio chiaro di come sia possibile rendere arduo il compito del decompilatore è presentato nella sottosezione control flow flattening.

Spostare lo stack

Prima di proseguire con la nostra trattazione, rimaniamo saldi sull’analisi della funzione sub_100005FC0 e visualizziamo il prologo della procedura prima dell’istruzione test al, 0x2.

Screen del prologo della procedura sub_100005FC0

Notiamo niente di strano? Confesso che non è stato semplice da analizzare, ma dopo l’analisi di IDA si capisce un’altra tecnica di “offuscamento” che Apple ha applicato per proteggere fairplayd. In modo molto subdolo lo stack viene spostato verso l’alto (o il basso, dipende da come si vuole costruire lo stack). Perché un programma dovrebbe voler spostare lo stack?

Tutti i software di analisi delle istruzioni prevedono, tra i tanti passaggi di analisi, un tipo di approfondimento particolare chiamato “analisi dello stack”. Questa tecnica consente di recuperare come è composto lo stack e le variabili locali: è fondamentale riuscire a dedurre correttamente come è composto lo stack, altrimenti il software di analisi non riesce più a capire il controllo di flusso e delle varie chiamate a funzione.

Per questa procedura, il tipo di errore che IDA segnala è sp-analysis failed ovvero non riesce a tracciare come viene modificato lo stack pointer. Nel nostro caso, IDA non riesce a rendersi conto che lo spostamento viene effettuato proprio per confondere l’analisi. Infatti con l’istruzione mov eax, 1010h e sub rsp, rax3, IDA è indotto a pensare che sia presente un buffer locale chiamato var_1020 di dimensioni 0x1010 byte. Questo è solo uno dei tanti esempi in cui IDA fallisce a ricostruire lo stack originale.

Altri esempi sono reperibili in funzioni con più istruzioni: la procedura sub_10000F620 presenta un parametro sullo stack che occupa 0x2CE7AB73 byte (753380211 byte = circa 753 MB). Un parametro così grande sullo stack sappiamo che non può esistere: lo stack può venire aumentato di dimensioni, ma solitamente raggiunge massimo 65520kb (valori più alti causano errori in macOS). Il valore 753 MB è dato da un blocco elementare in cui viene utilizzata l’istruzione sub esp, 0x2CE7AB73, blocco elementare non raggiungibile, considerabile quindi codice morto. IDA però non ha gli strumenti per rendersi conto che un tale valore è troppo grande per poter esistere: IDA non riconosce che la sua analisi è inesatta, al contrario si rifiuta di decompilare la funzione perché la dimensione dello stack frame è troppo elevato. Con una singola istruzione, Apple riesce a mettere un grosso ostacolo per chiunque voglia decompilare la funzione.

È così difficile risolvere questa situazione? Per un principiante, sì. Per un motivato studente di reverse engineering, no. Fortunatamente IDA consente all’analista di poter modificare parte delle informazioni che ha dedotto. Tramite la procedura di ridefinizione dello stack, è possibile ignorare gran parte dei 753 MB di variabile. Una strada alternativa è segnalare un certo percorso come codice morto (basta selezionare i byte di tale blocco e non definirlo tramite l’opzione chiamata Undefine). Nel caso dello spostamento verso l’alto dello stack pointer, quando cerchiamo di rendere complessa l’analisi del software, non parliamo proprio di offuscamento: non agiamo sulla leggibilità del codice. Agiamo piuttosto su alcuni limiti che hanno questi strumenti per recuperare le informazioni. Un buon articolo che raggruppa alcune tecniche per mandare in confusione le tecniche di analisi applicate da IDA è stato scritto da Markus Gaasedelen: Dangers of the Decompiler.

Control Flow Flattening

Il Control Flow Flattening è un’altra tecnica di offuscamento che troviamo all’interno del binario FairPlay ed è forse la più potente utilizzata da Apple in ambito della protezione del codice per fairplayd. Il controllo di flusso indica come il programma evolve nel tempo dal punto di vista del flusso delle istruzioni. Quali funzioni una procedura chiama, che tipo di salti fa il controllo (se condizionati o incondizionati), il collegamento tra i vari blocchi elementari, le condizioni per cui l’esecuzione si sposta e molte altre informazioni sono desunte dal controllo di flusso.

Le istruzioni ad alto livello del controllo di flusso sono le tipiche istruzioni che consentono di suddividere l’esecuzione di un programma in uno o più casi: if, if else, if else if else, switch, do while, for e molti altri. La maggior parte di queste condizioni si traducono in istruzioni linguaggio macchina che per Intel diventano cmp, test, jmp, jcc (jz, jnz, je, jg). Se il cambiamento del flusso ad alto livello è dettato da istruzioni “logiche”, a basso livello il cambiamento del controllo di flusso è cambiare l’indirizzo della prossima istruzione, o per meglio dire, effettuare un salto ad una certa istruzione.

Tra le informazioni che possiamo dedurre dal controllo di flusso, possiamo ricavare i vari collegamenti tra i blocchi base e procedere ad effettuare l’analisi intraprocedurale, ovvero cercare di ricostruire il flusso d’esecuzione del programma tra le varie procedure. Il flusso di controllo inoltre permette di recuperare importanti informazioni (come i loop) per procedere successivamente alla traduzione da codice macchina a pseudo codice C.

Per mostrare più chiaramente la potenza dell’offuscamento tramite il control flow flattening, prendiamo una funzione d’esempio: sub_10003FE60. Da IDA, selezioniamo la funzione d’esempio, tasto destro e scegliamo la visualizzazione ad albero. La visualizzazione ad albero è una funzionalità di IDA che consente di capire: come i blocchi elementari sono connessi, le dipendenze tra i blocchi e i branch d’esecuzione. Spesso tramite questa visualizzazione è possibile farsi un’idea più precisa di come sarà lo pseudo codice tradotto dal decompilatore.

L’obiettivo della tecnica del control flow flattening è quella di appiattire il control flow, ovvero di trasformare il flusso di controllo tramite alcuni salti condizionali e incondizionali. La tecnica è stata sviluppata da Chenxi Wang all’interno della sua tesi di dottorato dal titolo A Security Architecture for Survivability Mechanisms. Normalmente il flusso di controllo di una procedura è pressoché sviluppata in verticale, come è possibile vedere nell’immagine riportata qui sotto.

IDA controllo di flusso verticale

Con l’applicazione del control flow flattening, il grafo della procedura viene pesantemente modificato rendendo il grafo “più orizzontale”, ovvero appiattito o flatten. Ecco qui un esempio tipico di come può essere modificata una funzione applicando il control flow flattening (semplice):

IDA controllo di flusso verticale applicando il control flow flattening

Possiamo vedere quindi come i blocchi elementari siano stati portati tutti allo stesso livello di fatto estendendo orizzontalmente il grafo dei blocchi elementari. Il caso di control flow flattening portato all’estremo fa impazzire l’analista, come questa funzione rappresentata qui sotto:

IDA controllo di flusso verticale

Come è possibile osservare dall’immagine appena qui sopra, il control flow flattening può essere applicato per costruire blocchi elementari anche fasulli che possono mettere in confusione il decompilatore. Il risultato finale è un flusso di controllo estremamente complesso da analizzare. Ad alto livello la tecnica di trasformazione può essere tradotta tramite un semplice switch. Poniamo caso di avere una procedura:

int sum(int a, int b){
	int s = 4;
	int c;
	if ( a > 500 ){
		c = 2;
	} else {
		c = 0;
	}
	int result = s + a + b + c;
	return result;
}

Il control flow di questa semplice funzione è data dal seguente grafo:

Control flow graph

Ora vogliamo che questo grafo verticale sia il più appiattito possibile! Per farlo utilizziaom un altro potente costrutto dei linguaggi di programmazione: l’istruzione switch. L’istruzione switch consente di suddividere l’esecuzione in più rami, creando dei blocchi elementari disposti orizzontalmente. Per passare però da un ramo ad un altro nello switch, possiamo utilizzare una variabile ausiliaria, chiamata state, che memorizza lo stato attuale in cui siamo. Vi ricorda qualcosa? Questa tecnica sembra molto simile alla sintesi di una macchina a stati finiti! La variabile ausiliaria aiuta a cambiare stato senza preoccuparsi di alcuni side effect (o senza l’utilizzo di etichette e goto). Passiamo quindi alla trasformazione vera e propria! Per ora ipotizziamo di non aggiungere alcun blocco elementare inutile.

I passi che eseguiamo sono i seguenti:

  1. Creazione del blocco “start”: contiene l’inizializzazione della variabile state e tutte le variabili che verranno utilizzate all’interno dei blocchi elementari dello switch. È possibile spostare le dichiarazioni all’interno dei rami degli switch, tuttavia dobbiamo prestare attenzione ad eventuali effetti collaterali dell’utilizzo di variabili locali interni ad un costrutto.

  2. Creazione del dispatcher: questo blocco elementare verificherà in che stato saltare. È un semplice while con all’interno una istruzione switch: questo per evitare che dopo l’entrata in uno dei rami dello switch, il salto fuori raggiunga l’ultima istruzione della funzione.

  3. Spostamento dei blocchi elementari: scelgo un nuovo numero per un nuovo stato. Inserisco il codice all’interno del blocco elementare individuato includendo anche la modifica dello stato. La variabile state dovrà puntare al blocco elementare successivo che voglio eseguire. Ricordo che posso creare nuovi blocchi elementari anche suddividendo blocchi elementari più grandi o aggiungendo istruzioni che non verranno mai eseguite. Il limite per l’offuscamento è la fantasia :- )

  4. Verifica della trasformazione: verifico che effettivamente la funzione possa terminare, prestando attenzione all’ultimo blocco che viene eseguito. Tutte le trasformazioni degli offuscamenti andrebbero in realtà controllati attraverso alcuni proof solver e equazioni matematiche per evitare che la semantica originale venga modificata durante il processo di offuscamento.

Applicando il control flow flattening:

int sum(int a, int b){
	int state = 0;
	int s, c, result;
	while(1){
		switch(state){
			case 2:
				if (a > 500){
					state = 4;
				} else {
					state = -1;
				}
				break;
			case 0:
				s = 4;
				state = 2;
				break;
			case -1:
				c = 0;
				state = 5;
				break;
			case 5:
				result = s + a + b + c;
			case 0x54292639:
				return result;
			case 4:
				c = 2;
				state = 5;
				break;
		}
	}
	return -542926392;
}

Il risultato:

Control flow graph

Alcune note per punti:

Giusto per curiosità, proviamo a compilare un programma che utilizza entrambe le funzioni sum per vedere quanto è efficace questo tipo di tecnica. Il programma infatti ha due funzioni sum_1, la funzione offuscata e sum_2, la funzione originale. La funzione sum_2 viene decompilata correttamente:

__int64 __fastcall sum_2(int a1, int a2){
  int v3; // [rsp+4h] [rbp-10h]

  if ( a1 <= 500 )
    v3 = 0;
  else
    v3 = 2;
  return (unsigned int)(v3 + a2 + a1 + 4);
}

La funzione è molto simile all’originale. In aggiunta, il control flow graph costruito da IDA non desta alcun sospetto e risulta uguale al grafo costruito da noi manualmente. Diverso è il discorso per sum_1:

__int64 __fastcall sum_1(int a1, int a2){
  int v3; // [rsp+8h] [rbp-14h]
  int v4; // [rsp+Ch] [rbp-10h]
  int i; // [rsp+10h] [rbp-Ch]

  for ( i = 0; ; i = 5 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( i == -1 )
        {
          v3 = 0;
          i = 5;
        }
        if ( i )
          break;
        v4 = 4;
        i = 2;
      }
      if ( i != 2 )
        break;
      if ( a1 <= 500 )
        i = -1;
      else
        i = 4;
    }
    if ( i != 4 )
      break;
    v3 = 2;
  }
  return (unsigned int)(v3 + a2 + a1 + v4);
}

Intanto possiamo dire che siamo riusciti a confondere il decompilatore di IDA. Notiamo infatti che viene costruito un for, poi 3 cicli while(1) uno dentro l’altro. Seppur il risultato del decompilatore possa non essere esattamente corretto ed è diverso dal codice originale, il programma funziona. Un attaccante riuscirebbe quindi a prelevare la logica del programma. Questo è un tipico esempio di offuscamento debole: l’attaccante potrebbe avere qualche difficoltà nel ricostruire il control flow originale, ma ci sono buone probabilità che si riesca a recuperare la logica iniziale. Per rendere ancora più complesso il tutto, possiamo utilizzare le altre tecniche: inserimento dei predicati opachi, trasformazioni delle costanti, trasformazioni delle espressioni aritmetico booleane.

Ritornando a fairplayd, la tecnica del control flow flattening viene utilizzata pesantemente per cercare di offuscare l’evoluzione del flusso di controllo che fairplay ha durante l’esecuzione. Apriamo con IDA la funzione sub_10003FE60 ed estriamo la logica del funzionamento tramite la vista del decompilatore. In particolare possiamo notare che il decompilatore riesce a comprendere l’utilizzo di una struttura di alto livello simile ad uno switch:

switch ( (v7 == 0) + v2 ){
    case 0:
      JUMPOUT(0x100035E2CLL);
    case 1:
      JUMPOUT(0x10004359ELL);
    case 2:
      JUMPOUT(0x10005BECDLL);
    case 3:
      v46 = v3;
      v45 = a1;
      v8 = v2 ^ 6u;
      v9 = v8 - 5;
      v10 = ((_DWORD)v8 - 5) | 2u;
      ....
}

Esaminando tuttavia tutti i casi all’interno dello switch, avremo delle istruzioni senza senso! Questo perché gli sviluppatori di Apple sono riusciti ad offuscare pesantemente la variabile di stato, ovvero v2 e v7. D’altra parte, il decompilatore riconosce il costrutto switch solo dalla presenza di jump table, particolari tabelle le cui entry sono composte da indirizzi di memoria a cui saltare. Poniamo caso di avere 5 rami di uno switch, è possibile memorizzare in memoria 5 indirizzi diversi, ognuno associato ad un ramo diverso dello switch. Durante la comparazione, basterà fare un salto all’indirizzo puntato nella cella corretta della tabella. Queste tabelle sono memorizzate in specifici punti del binario e con un po’ di fortuna è possibile ripristinarle tutte.

Per alzare ancora l’asticella della difficoltà dell’analisi, gli sviluppatori hanno deciso che per alcune funzioni la jump table venisse ricreata dinamicamente. In modo molto riassuntivo, il programma alloca uno spazio di memoria dove poter immettere degli indirizzi che successivamente verranno utilizzati come rami di uno switch. Il calcolo degli indirizzi viene eseguito tramite le abituali espressioni MBA e per una serie di limiti tecnici dell’analisi statica non è possibile determinare esattamente gli indirizzi. Questo in poche parole distrugge completamente ogni possibilità per IDA e per l’analista di recuperare la logica originale del programma e la sua struttura.

Esempio tipico di costruzione degli indirizzi viene dall’ultima porzione di codice di ogni blocco elementare (il fatto che la costruzione degli indirizzi avvenga in modo inline aggiunge complessità al codice):

lea     r12, jpt_10003FF12								; address where to fetch the next address
movsxd  rax, ds:(jpt_10003FF12 - 100218890h)[r12+rax*4] ; move the address referred by jpt_10003FF12 - 100218890h
lea     rcx, loc_10005A140 								; load the address of loc_10005A140 (base address of jump table)
add     rcx, rax										; add the rax value
mov     r14, [rbp+var_58]								; save previous variable on stack
jmp     rcx												; jump to next switch case

La tecnica del control flow flattening risulta quindi essere una buona tecnica di offuscamento con cui Apple riesce a prevenire l’analisi statica da parte degli attaccanti. La costruzione delle tabelle in modo dinamico, unita alla serie di tecniche menzionate nelle sottosezioni precedenti (predicati opachi, MBA) sono la chiave dell’offuscamento di fairplayd.

Sono possibili ulteriori misure di offuscamento?

All’interno di questo articolo abbiamo provato ad identificare alcuni pattern comuni di offuscamento che è possibile trovare in fairplayd, un demone eseguito nello spazio utente, utilizzato da Apple per proteggere il contenuto dietro copyright all’interno di macOS. Alcune misure si sono rivelate efficaci: effettivamente complicano ogni tentativo di effettuare un preciso reverse engineering del programma. La misura che di più è complice di questa complicazione è data dal control flow flattening che azzera le possibilità per un attaccante di recuperare la struttura originale del programma. Gli attaccanti si devono scontrare con tabelle di salto costruite dinamicamente, switch non più recuperabili e codice generato dinamicamente.

Altre tecniche di offuscamento invece sembrano essere buone idee, ma solo in teoria: i predicati opachi sono abbastanza semplici da analizzare e attraverso un controllo più accurato dei blocchi base è possibile distinguere tra il codice originale e il codice modificato per introdurre l’offuscamento.

Esiste una misura perfetta per poter offuscare il codice? È possibile ideare una tecnica in grado di produrre una protezione inattaccabile? No. Si tratterà sempre di una continua evoluzione tra strumenti che cercano di deoffuscare il contenuto nascosto e nuove tecniche per poter aggirare questi strumenti e applicare offuscamenti più pesanti. Questo è dato dal limite della gestione del contenuto digitale: le informazioni alla fine devono poter essere visibili e quindi deoffuscabili, al contrario di altri tipi di informazioni che possono essere nascoste per sempre all’interno dei dispositivi. È probabile che dopo l’ennesimo articolo sugli offuscamenti presenti all’interno di fairplayd vengano di nuovo aggiornate le misure di protezione, portando il contenuto dell’articolo a non essere più aggiornato.

Una possibile via che Apple potrebbe perseguire per applicare offuscamenti più potenti è quella di spostare gran parte dell’esecuzione su una tecnica di offuscamento chiamata “virtualizzazione”. Questa tecnica prevede la scrittura di un interprete e un instruction set architecture completamente ad-hoc per eseguire le operazioni necessarie per la decrittazione del contenuto. Al momento la tecnica non sembra essere utilizzata dentro fairplayd, ma sono presenti alcuni segnali (come switch con 13-14 casi) che sembrano presagire un utilizzo di valutazione dei dati e produzione dinamica di codice. Onestamente non ho avuto ancora il tempo di analizzare in dettaglio questa parte.

Come sempre, se avete critiche, suggerimenti o qualsiasi altro commento, potete scrivere un’e-mail a seekbytes@protonmail.com4. Non voglio mai essere voce di verità, per cui segnalazioni d’errori o imprecisioni sono sempre ben accettate.


  1. È bene precisare che con il termine “installer” si intende il pacchetto IPA che contiene i file eseguibili, le risorse e i metadata firmati per poter avviare l’applicazione. ↩︎

  2. Questa tecnica in realtà è già utilizzata all’interno del mondo Apple per crittare e decrittare il contenuto del firmware. La chiave prende il nome di GID Key ed è una chiave AES 256-bit condivisa da tutti i dispositivi con lo stesso processore. Parte di questa chiave può essere utilizzata per generare dinamicamente la chiave del proprio account iCloud per la verifica degli acquisti su Apple Store. ↩︎

  3. Vedendo il codice originale, possiamo notare che c’è una chiamata a sub_100008B80, tuttavia non influisce sul risultato originale. La procedura è un semplice caricamento dello stack canary all’interno dello stack. ↩︎

  4. Un ringraziamento particolare va al talk “Svelare i segreti nei binari utilizzando strategie di rilevamento del codice” di Tim Blazytko che ha permesso di velocizzare gran parte dell’analisi binaria utilizzando le informazioni date dalla lunghezza dei blocchi, la disposizione di essi all’interno del control flow graph. ↩︎