Cobertura de código, herramientas y ejemplos
Hola a todos,
en este post os paso algunas herramientas para realizar cobertura de código que me han sido bastante útiles, así como su documentación y unos ejemplos que intentan ser bastante claros sobre como usarlas.
A la hora de buscar vulnerabilidades o hacer reversing de un ejecutable, sobretodo si es grande, es muy útil realizarle un análisis previo de cobertura de código que nos de una primera idea de por donde anda cada cosa. También nos será útil para probar nuestros fuzzers y ver si realmente nuestros datos están haciendo que se ejecute una buena parte del código.
Hay muchas otras herramientas de cobertura de código, algunas se apoyan en IDA u otros desensambladores/depuradores, otras en frameworks como PaiMei, otras funcionan por si mismas sin apoyarse en otras, etc… Así que seguramente no encontraréis conceptos nuevos por aquí que no hayáis visto por otros sitios.
Las herramientas que os paso son scripts en python principalmente para IDA (aunque también va un script para realizar la cobertura con pydbg). Son fruto de varios cambios y mejoras que he ido metiendo según he ido necesitando. También he quitado cosas que pensé que iban a ser útiles y que finalmente no me ayudaban. Así que espero que aunque no aporte ideas nuevas, estas herramientas sean lo suficientemente prácticas para ser usadas en casos reales, ya que son fruto de eso mismo, de su uso en casos reales.
Aunque he usado bastante estos scripts no es seguro que no vayan a tener bugs o cosas que se puedan mejorar, asi que si alguien se anima a retocarlos o mejorarlos, adelante, y si luego nos pasa las mejoras, mucho mejor 😉
Respecto a los ejemplos he intentado que sean bastante claros, con varias imágenes, etc… para que se puedan ir siguiente bien y se vea el potencial que tiene realizar cobertura de código a la hora de hacer reversing.
Espero que lo disfrutéis y a alguien le sean útiles estas herramientas.
Documentación de las herramientas para cobertura de código
Este paquete de herramientas para cobertura de código comprende los siguientes scripts en python:
Getfuncs.py:
script para ida python que extrae a un fichero todas las direcciones de las funciones del ejecutable analizado (que después podrá ser usado con los demás scripts).
Getbblocks.py:
script para ida python que extrae a un fichero todas las direcciones de los basic blocks del ejecutable analizado (que después podrá ser usado con los demás scripts).
Funcs2bblocks.py:
script para ida python al que se le especifica un fichero de entrada con direcciones de funciones y extrae a otro fichero especificado todas las direcciones de los basic blocks.
Hits2ida.py:
script para ida python al que se le especifica un fichero de entrada con direcciones de hits de basic blocks. El script colorea y comenta el código en ida. Por defecto el color es 0xAAAAAA y el comentario es el nombre del fichero de hits. Es posible nombrar el fichero de esta forma:
Color.0xABCDEF.comment.hola.file.txt
El script tomará el dato después de “color.” para colorear con dicho valor, y el dato después de “comment.” para usarlo como comentario.
Cuando se ejecuta el script también preguntará si se desea abrir también todos los ficheros en el mismo directorio de manera que se pueda hacer un coloreado/comentado múltiple.
Cover.py:
usage: cover.py covermodname path2exe/attachid path2bblocks path2res
Script en python que utiliza el framework paimei (pydbg) para realizar la cobertura de código de un ejecutable dado.
En la línea de comandos debe especificarse el nombre del ejecutable (puede ser dll), el path al exe para ejecutar o id del proceso al que engancharse, el path al fichero de direcciones donde monitorizar los hits, y el path del fichero donde almacenar los resultados (los hits ocurridos).
Después de varias pruebas haciendo la cobertura de código con pydbg he podido ver que ocurren bastantes errores que no ocurren si se realiza con el depurador de ida (y un plugin de monitizaración de los hits).
Idacover.py:
script para ida python que recibe un fichero de direcciones, setea los breakpoints para estas direcciones, y se instala como hook de debug. Cuando se depure el ejecutable analizado, el script monitoriza los hits (eliminando los breakpoints según pasa por ellos).
Al finalizar la depuración el script pregunta si se desean realizar varias acciones:
- Coloreado de los Basic blocks de los hits.
- Coloreado de las funciones enteras de los hits.
- En ambos casos el valor numérico del color (0xAABBCC).
- Si se desea guardar los hits a un fichero.
Ccadd.py:
usage: ccadd.py file/dir1 file/dir2 …file/dirN fileresults
script para crear un fichero de direcciones nuevo con todas los direcciones en los ficheros especificados previamente en la línea de comandos.
Ccsub.py:
usage: ccsub.py fileorig subfile/dir1 … subfile/dirN fileresults
script para crear un fichero de direcciones nuevo quitando a fileorig todas las direcciones en los demás ficheros especificados en la línea de comandos.
Ccand.py:
usage: ccand.py file/dir1 file/dir2 … file/dirN fileresults
script para crear un fichero de direcciones nuevo solamente con las direcciones que estén en todos los ficheros especificados en la línea de comandos.
Ccxor.py:
usage: ccxor.py file/dir1 file/dir2 … file/dirN fileresults
script para crear un fichero de direcciones nuevo, para el cual primero se realiza un “and” (como con ccand.py) y luego se meten al fichero nuevo todas las direcciones que estén en algún fichero pero no en todos.
Ejemplos:
Caso 1. Cobertura de código del parser de HTML en mshtml.dll.
Introducción:
Para realizar esta prueba podríamos usar cualquier programa que tirara de mshtml.dll, por ejemplo, Internet Explorer.
Ya que vamos a tener más versatilidad para tocar cosas, vamos a tirar de un script en python que use la dll para crear un diálogo que muestre una web:
import tempfile
import sys # From urlmon.h # Dialog box properties # HTML content # Create temp HTML file def show_html_dialog(url, dlg_options): # Helper for Python 2.6 and 3.0 compatibility moniker = POINTER(IUnknown)() windll.mshtml.ShowHTMLDialog(None, moniker, None, wstr(dlg_options), None) |
Este script creará un diálogo en el que mostrará el código html que va en el propio script.
Estrategia a seguir:
- Primero vamos a hacer la cobertura a nivel de funciones. Extraemos las funciones con getfuncs.py a un fichero funcs.txt.
- Vamos a quitarnos bloques de código que se ejecutan siempre, inicializaciones, gestor de mensajes de la ventana, etc… Para ello primero realizamos la cobertura de código con un fichero de direcciones con todas las funciones del ejecutable y en el script metemos un trozo de código html muy básico:
# HTML content
HTML_DLG = """\
<html>
<head>
<title>ttttttttttttttttt</title>
</head>
<body>
bbbbbbbbbbbbbbbbbbbbbbbbb
</body>
</html>""" - Guardamos todos los hits sin colorear nada a un fichero, lo llamamos init.txt.
- Restamos al fichero con todas las funciones func.txt lo que se ha ejecutado con el html más básico usando ccsub.py y lo guardamos en noinit.txt
- Ahora podemos ir probando otros tags html haciendo la cobertura con noinit.txt, de manera que lo nuevo que se ejecute estará relacionado con los tags introducidos.
Desarrollo de la prueba:
Tras analizar mshtml.dll con IDA lanzamos el script getfuncs.py.
Y nos guardamos todas las funciones a un fichero, funcs.txt:
A continuación realizamos una primera cobertura de código para quitarnos del medio todas las funciones de inicialización, etc… y otras funciones al margén de las que van a gestionar los tags, que es lo que queremos localizar.
Para lanzar el script de python que hemos dicho antes, configuramos IDA:
Posteriormente ejecutamos idacover.py:
Nos va a pedir el fichero de direcciones sobre las que monitorizar. Usaremos todas las funciones, que previamente habíamos guardado en funcs.txt:
Idacover.py pondrá breakpoints en todas las direcciones dadas y se instalará para monitorizar los hits. Cada vez que ocurra un breakpoint, lo quitará y se guardará la dirección.
Empezamos la depuración y le dejamos que saque la web en el diálogo, movemos la ventana, pasamos el ratón, toquiteamos, etc… veremos como van saltando breakpoints a medida que se pasa por distintos sitios. La idea es quitarnos todo el código posible de en medio (por eso es interesante hacer cosas con la ventana, pasarla al fondo, traerla, moverla, sacar un trozo de la pantalla, etc…).
Al finalizar el proceso se nos preguntarán varias cosas:
En este caso no queremos colorear nada en IDA, no nos interesan esas zonas de código. Pero sí que salvaremos los hits para hacernos un subconjunto de funciones para continuar con la cobertura en las que no estén todas las que nos hemos quitado en el paso anterior.
Salvamos todas estas direcciones en un fichero init.txt:
Restamos init.txt a funcs.txt y lo guardamos en noinit.txt:
Para continuar con la cobertura de código usamos noinit.txt.
Quitamos todos los breakpoints de IDA y volvemos a ejecutar idacover.py, esta vez seleccionando noinit.txt.
Además vamos a modificar el trozo html:
# HTML content
HTML_DLG = """\ <html> <head> <title>ttttttttttttttttt</title> </head> <body> bbbbbbbbbbbbbbbbbbbbbbbbb<font size=3>sksksksk</font> </body> </html>""" |
Como vemos es como el de antes, pero hemos añadido texto entre el tag <font>.
En la cobertura que vamos a realizar ahora no van a saltar ninguno de los hits de antes porque nos los hemos quitado ya (por eso usamos noinit.txt).
Repetimos los pasos de antes, ejecutamos el programa y esperamos a ver que nuevos hits ocurren. Cuando acaba la ejecución nos pregunta si queremos colorear el código:
Le decímos que sí queremos aplicar los colores, le ponemos el valor del color, y le decímos que coloree toda la función donde esté la dirección del hit (ya que hemos ido haciendo la cobertura por funciones).
Si queremos podemos salvar los hits a un fichero también (hits.font.txt por ejemplo).
Vemos algunas de las zonas coloreadas:
CFontElement::CreateElement:
CFontElement:: CFontElement:
CFontElement::ApplyDefaultFormat:
ApplyFontSize:
Vemos como el tag que hemos añadido: <font size=3>sksksksk</font>
Ha provocado que se ejecuten nuevas funciones:
CFontElement::CreateElement
CFontElement:: CFontElement
CFontElement::ApplyDefaultFormat
ApplyFontSize
Etc…
En este caso concreto tenemos los símbolos de microsoft y no nos es tán útil los resultados de la cobertura de código, pero en otros casos puede darnos una cantidad de información muy útil.
Repitiendo este proceso con otros tags podemos ir cubriendo más zonas de código y teniendo una idea aproximada de donde buscar cuando queramos encontrar algo. Si tenemos un casque con un documento html generado aleatoriamente por un fuzzer, analizando el casque, y con la información que saquemos de la cobertura de código, también podremos tener una idea más concreta de que parte del documento ocasionó el casque.
Opcionalmente, si queremos información más detallada, podemos convertir las funciones en las que ocurrieron hits para <font> a basic blocks con funcs2bblocks.py. Una vez convertidas a basic blocks, usamos el resultado para repetir el code coverage de manera que obtendremos lo mismo pero sabremos exactamente los basic blocks que se han ejecutado en la función y cuales no.
Caso 2. Localización del checkeo de password de Filezilla Server.
Introducción:
En esta prueba vamos a localizar la zona en la que se checkea el password en FileZilla Server mediante cobertura de código.
Estrategia a seguir:
- Lanzamos el servidor y nos attacheamos, conectamos a él pero no introducimos password, etc… es decir intentamos quitarnos todas las funciones posibles del medio que no tengan que ver con el checkeo de password.
- Lanzamos el servidor, nos attacheamos, y esta vez nos logeamos correctamente, coloreando las partes por las que se pasa y guardándonos los hits.
- Lanzamos de nuevo el servidor y nos attacheamos, pero esta vez nos logeamos con password no válido y vemos los nuevos hits, los coloreamos también.
Desarrollo de la prueba:
En primer lugar vamos a obtener directamente todos los basic blocks con getbblocks.py.
Guardamos las direcciones de los basic blocks y lanzamos idacover.py con ellos.
Una vez seteados todos los breakpoints:
Lanzamos el servidor de ftp y vamos “trasteando” sin llegar a hacer nada que tenga que ver con el checkeo de password, para quitarnos de en medio todas las funciones que podamos que no nos interesen. Introducimos el usuario, pero no llegamos a introducir el password ni bueno ni malo. Y paramos el proceso quitando todos esos hits de en medio.
Con esto practicamente nos hemos quedado con la zona de checkeo de password. Ahora lanzamos el server e introducimos la clave buena.
E inmediatamente paramos, esta vez cuando nos pregunte si deseamos colorear, coloreamos de verde por ejemplo (esta vez no le decimos que coloree la función entera, solo el basic block del hit). Repetimos el mismo proceso para el password inválido y coloreamos de otro color los basic blocks.
Vamos comparando y revisando las funciones coloreadas y vemos los sitios por los que ha pasado para el caso válido y caso inválido:
La zona verde es el recorrido para password válido y la zona azul para inválido.
Si vamos exactamente al punto donde se bifurca la ejecución para password bueno y malo:
Ahora tenemos los símbolos y vemos claramente que esa es la función que comprueba el pass y ese jnz es el salto que bifurca si es pass bueno o malo (de hecho si metemos pass malo y forzamos a saltar para el otro lado nos logearemos bien). Pero en un caso en que no tuvieramos los símbolos más o menos podríamos intuir por donde anda toda la historia del checkeo.
Código fuente:
Getfuncs.py:
from idaapi import *
from idc import * from idautils import * fs = AskFile(1, "*.txt", "Select file to save basic block start addresses…") ea=0 f.write(("0x%08x"%get_imagebase())+",modbase\n") for ea in Functions(ea, MaxEA()): f.close() |
Getbblocks.py:
from idaapi import *
from idc import * from idautils import * ############################################### @type ea: DWORD @rtype: List if is_call_insn(ea): xrefsgen = CodeRefsFrom(ea, 1) # if the only xref from ea is next ea, then return nothing. return xrefs ############################################### @type ea: DWORD @rtype: List xrefs = [] if prev_ea!=0xffffffff: for xref in CodeRefsTo(ea, 1): return xrefs fs = AskFile(1, "*.txt", "Select file to save basic block start addresses…") ea=0 all_basic_blocks = [] for ea in Functions(ea, MaxEA()): print "Function:" + hex(ea) function_instructions = list() while status: basic_blocks = set() l=[ea] lastwasret=0 for address in function_instructions: if is_ret_insn(ea): basic_blocks = filter(lambda i: i in function_instructions, basic_blocks) for bb in basic_blocks: f.write(("0x%08x"%get_imagebase())+",modbase\n") all_basic_blocks.sort() f.close() |
Funcs2bblocks.py:
from idaapi import *
from idc import * from idautils import * ############################################### @type ea: DWORD @rtype: List if is_call_insn(ea): xrefsgen = CodeRefsFrom(ea, 1) # if the only xref from ea is next ea, then return nothing. return xrefs ############################################### @type ea: DWORD @rtype: List xrefs = [] if prev_ea!=0xffffffff: for xref in CodeRefsTo(ea, 1): return xrefs funcsfname = AskFile(0, "*.*", "Select functions file.") all_basic_blocks = [] f=open(funcsfname) ffs=""+fs for ea in all_funcs: print "Function:" + hex(ea) function_instructions = list() while status: basic_blocks = set() l=[ea] lastwasret=0 for address in function_instructions: if is_ret_insn(ea): basic_blocks = filter(lambda i: i in function_instructions, basic_blocks) for bb in basic_blocks: f.write(("0x%08x"%get_imagebase())+",modbase\n") all_basic_blocks.sort() f.close() |
Hits2ida.py:
from idaapi import *
from idautils import * from idc import * import os ############################################### def _branches_from (ea): @type ea: DWORD @rtype: List if is_call_insn(ea): xrefsgen = CodeRefsFrom(ea, 1) # if the only xref from ea is next ea, then return nothing. return xrefs ############################################### def _branches_to (ea): @type ea: DWORD @rtype: List xrefs = [] while not isCode(GetFlags(prev_code_ea)): for xref in CodeRefsTo(ea, 1): return xrefs def dofile(dir,fname): if len(lfname)>=2: f=open(dir+"\\"+fname) return fname = AskFile(0, "*.*", "Select hits file.") allf = AskYN(1,"Do you want to load all files in the same directory?") if allf: |
cover.py:
import sys
import utils import pida import pydbg ############################################### if len(sys.argv)<5: reloc=0 f=open(path2bblocks) attachid=0 print "attachid="+str(attachid) dbg = pydbg.pydbg() if not attachid: dbg.run() f=open(path2res,"w+b") |
Idacover.py:
from idaapi import *
############################################### def _branches_from (ea): ############################################### def _branches_to (ea): ############################################### class MyDbgHook(DBG_Hooks): def dbg_process_start(self, pid, tid, ea, name, base, size): def dbg_process_attach(self, pid, tid, ea, name, base, size): def dbg_process_exit(self, pid, tid, ea, code): if AskYN(1,"Do you want to apply colors?"): docolor=True if AskYN(1,"Do you want to color entire functions?"): if colorfuncs: if AskYN(1,"Do you want to save hits?"): def dbg_process_detach(self, pid, tid, ea): def dbg_library_load(self, pid, tid, ea, name, base, size): def dbg_bpt(self, tid, ea): def dbg_trace(self, tid, ea): def dbg_step_into(self): def dbg_step_over(self): ############################################### # Install the debug hook hits=[] path2bblocks = AskFile(0, "*.*", "Select basic blocks file.") f=open(path2bblocks) # Start debugging |
Ccadd.py:
import sys
import os def readfilehits(fname,hits): def readhits(path,hits): def savehits(fname,hits): sum=[] if len(sys.argv)<4: for i in range(1,len(sys.argv)-1): savehits(sys.argv[len(sys.argv)-1],sum) print "Done." |
Ccsub.py:
import sys
import os def readfilehits(fname,hits): def readhits(path,hits): def savehits(fname,hits): orig=[] if len(sys.argv)<4: print "1" orig=readhits(sys.argv[1],orig) print "2" for i in range(2,len(sys.argv)-1): print "3" savehits(sys.argv[len(sys.argv)-1],orig) print "Done." |
Ccand.py:
import sys
import os def readfilehits(fname,hits): def readhits(path,hits): def savehits(fname,hits): first=[] if len(sys.argv)<4: first=readhits(sys.argv[1],first) for i in range(1,len(sys.argv)-1): for e in first: savehits(sys.argv[len(sys.argv)-1],res) print "Done." |
Ccxor.py:
import sys
import os def readfilehits(fname,hits): def readhits(path,hits): def savehits(fname,hits): first=[] if len(sys.argv)<4: first=readhits(sys.argv[1],first) for i in range(1,len(sys.argv)-1): for e in first: for l in all: savehits(sys.argv[len(sys.argv)-1],res) print "Done." |