Complemento ida_medigate C++ para IDA Pro
[TOC]
La ingeniería inversa del código C++ compilado no es divertida. La ingeniería inversa estática del código C++ compilado es frustrante. La razón principal que lo hace tan difícil son las funciones virtuales. A diferencia del código C compilado, no existe un flujo de código claro. Muchas veces uno puede pasar mucho tiempo tratando de entender cómo se llama la siguiente función virtual, en lugar de simplemente ver la función como en el código C compilado.
Cuando uno investiga una función virtual, la cantidad de tiempo que necesita esforzarse para encontrar su referencia externa no tiene sentido.
Después de demasiados proyectos de C++ RE, me di por vencido y decidí que necesitaba una herramienta flexible (Python) y estable (que pueda mantener fácilmente) para este tipo de investigación. La mayor parte de este complemento se escribió en enero de 2018 y recientemente decidí limpiar el polvo y agregar soporte para las nuevas clases IDA (7.2).
Este complemento no está destinado a funcionar siempre "listo para usar", sino a ser otra herramienta para el inversor.
El complemento consta de dos partes:
Esta primera parte no depende de la segunda parte, por lo que es posible usar el complemento para realizar ingeniería inversa en un binario que no contiene RTTI, definiendo esas clases manualmente según la API del complemento.
Lo que hace que el complemento sea único es el hecho de que utiliza el mismo entorno con el que el investigador ya está familiarizado, y no agrega ningún menú u objeto nuevo, y se basa en los bloques de construcción conocidos de IDA (estructura, unión, tipo para los miembros de la estructura, etc.). ): esto permite que el complemento admita la abstracción de C++ para cada arquitectura que admite IDA .
Nota: El analizador RTTI analiza RTTI x86/x64 g++, pero su estructura permite agregar soporte para más arquitecturas y compiladores fácilmente.
plugins/
). Copie medigate_cpp_plugin
al directorio plugins
y agregue la ruta del código fuente a su archivo idapythonrc.py
Copie plugins/ida-referee/referee.py
al mismo directorio.
Suponiendo que el código fuente binario original es el siguiente ( examples/a.cpp
):
using namespace std ;
class A {
public:
int x_a;
virtual int f_a ()=0;
};
class B : public A {
public:
int x_b;
int f_a (){x_a = 0 ;}
virtual int f_b (){ this -> f_a ();}
};
class Z {
public:
virtual int f_z1 (){cout << " f_z1 " ;}
virtual int f_z2 (){cout << " f_z2 " ;}
};
class C : public B , public Z {
public:
int f_a (){x_a = 5 ;}
int x_c;
int f_c (){x_c = 0 ;}
virtual int f_z1 (){cout << " f_z3 " ;}
};
int main ()
{
C *c = new C ();
c-> f_a ();
c-> f_b ();
c-> f_z1 ();
c-> f_z2 ();
return 0 ;
}
El binario está eliminado pero contiene RTTI.
Cuando simplemente cargamos el binario, la función main
( sub_84D
en la versión de 32 bits) se ve así:
Inicie el analizador g++ RTTI y ejecútelo usando:
from ida_medigate.rtti_parser import GccRTTIParser
GccRTTIParser.init_parser()
GccRTTIParser.build_all()
Ahora actualice la estructura C (consulte la sección Comentarios), convierta v0
para que sea C *
, descompile nuevamente:
Para los casos en los que no hay RTTI, nuestra infraestructura aún permite definir manualmente la clase C++. Para el mismo ejemplo (examples/a32_stripped), puede crear manualmente la estructura B, luego seleccionar su tabla virtual y escribir
from ida_medigate import cpp_utils
cpp_utils.make_vtable("B")
make_vtable
también puede obtener vtable_ea
y vtable_ea_stop
en lugar del área seleccionada.
Luego crea la estructura C y aplica la herencia:
cpp_utils.add_baseclass("C", "B")
Ahora puedes reconstruir la clase C vtable seleccionándola y escribiendo:
cpp_utils.make_vtable("C")
Agregue la estructura Z, reconstruya su vtable también y ahora viene la parte interesante:
cpp_utils.add_baseclass("C", "Z", 0x0c, to_update=True)
que aplica la herencia C de Z en el desplazamiento 0x0c y también actualiza la estructura (ver comentarios).
Lo último que queda es actualizar la segunda vtable de C, la que implementa la interfaz de Z. Marca esta vtable y escribe:
cpp_utils.make_vtable("C", offset_in_class=0x0c)
ida_medigate sabe que esta vtable es la vtable de clase Z y el resultado será:
El resultado final es el mismo que en el caso RTTI:
Cuando se crea una nueva estructura vtable (mediante el analizador RTTI o manualmente por el usuario), se cambia el nombre de cada función que aún no ha cambiado, se descompila y se establece su primer argumento en this
.
Haga doble clic en el miembro de una estructura que corresponde a dicha función para navegar a la función, de modo que el usuario pueda leer el flujo de código C++ de una manera conveniente.
Cada cambio de nombre o tipo de una función o su correspondiente miembro puntero de función en vtables se engancha y sincroniza entre todos ellos. Esto significa, por ejemplo, que el usuario podría cambiar el tipo de miembro de vtable a través de la ventana del descompilador y este nuevo tipo (prototipo) también se aplicará a la función de destino.
En la línea 15 de la imagen anterior, hay una llamada a B::sub_9A8 (B::f_b en el código fuente). El argumento de esta función es B *
:
Pero esta función también podría ser llamada por una instancia C
(up-casting). queremos ver la función virtual que llamaría su instancia. Supongamos que hay muchas clases derivadas potenciales, por lo que no siempre es posible convertir this
a C *
. Por esa razón, implementamos una unión para cada clase base que tenga hijos que tengan una mesa virtual diferente. Se puede optar por mostrar una tabla virtual derivada diferente de las derivadas de B
haciendo clic en alt+y (el acceso directo para elegir un miembro diferente del sindicato):
por lo que, en última instancia, solo podemos "transmitir" llamadas específicas a diferentes funciones virtuales:
El santo grial de los frustrados ingenieros inversos de C++. ¡Mantenemos referencias externas desde funciones virtuales hasta los miembros de la estructura vtable que las representa!
¡Combinar esto con ida-referee
nos permite rastrear todas las referencias externas de llamadas a funciones virtuales!
Una limitación: sólo podemos rastrear llamadas virtuales que ya hayan sido descompiladas. Afortunadamente, el análisis automático sabe completar un tipo de argumento entre funciones, por lo que con el proceso iterativo de emitir más argumentos->descompilar todas las funciones relevantes-> leer el código nuevamente y emitir más argumentos (...) esta capacidad se vuelve realmente ¡poderoso!
La forma en que marcamos los miembros de la estructura como subclases en IDAPython no se sincroniza inmediatamente con el BID. El truco que hacemos es editar la estructura para que se active una sincronización. También puedes usar
utils.refresh_struct(struct_ptr)
que agrega un campo ficticio al final de la estructura y luego lo deja sin definir.