Plugin C++ ida_medigate pour IDA Pro
[TOC]
L'ingénierie inverse du code C++ compilé n'est pas amusante. L'ingénierie inverse statique du code C++ compilé est frustrante. La principale raison qui rend les choses si difficiles sont les fonctions virtuelles. Contrairement au code C compilé, il n’y a pas de flux de code clair. Trop souvent, on peut passer beaucoup de temps à essayer de comprendre comment est appelée la prochaine fonction virtuelle, plutôt que de simplement voir la fonction comme dans le code C compilé.
Lorsqu'on étudie une fonction virtuelle, le temps qu'il lui faut pour trouver sa xréf n'a pas de sens.
Après trop de projets C++ RE, j'ai abandonné et j'ai décidé que j'avais besoin d'un outil flexible (Python) et stable (que je peux facilement maintenir) pour ce type de recherche. La majeure partie de ce plugin a été écrite en janvier 2018, et j'ai récemment décidé de nettoyer la poussière et d'ajouter la prise en charge de la nouvelle prise en charge des classes IDA (7.2).
Ce plugin n'est pas destiné à fonctionner toujours "prêt à l'emploi", mais à être un outil supplémentaire pour l'inverseur.
Le plugin se compose de deux parties :
Cette première partie ne dépend pas de la deuxième partie, il est donc possible d'utiliser le plugin pour rétro-ingénierier un binaire qui ne contient pas de RTTI, en définissant ces classes manuellement en fonction de l'API du plugin.
Ce qui rend le plugin unique est le fait qu'il utilise le même environnement que celui que le chercheur connaît déjà, et n'ajoute aucun nouveau menu ou objet, et est basé sur les éléments de base IDA connus (structure, union, type pour les membres de la structure, etc. ) - Cela permet au plugin de prendre en charge l'abstraction C++ pour chaque architecture prise en charge par IDA .
Remarque : L'analyseur RTTI analyse le RTTI x86/x64 g++, mais sa structure permet d'ajouter facilement la prise en charge de davantage d'architectures et de compilateurs.
plugins/
) Copiez medigate_cpp_plugin
dans le répertoire plugins
et ajoutez le chemin du code source à votre fichier idapythonrc.py
Copiez plugins/ida-referee/referee.py
dans le même répertoire.
En supposant que le code source binaire original est le suivant ( 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 ;
}
Le binaire est supprimé mais contient RTTI.
Lorsqu'on vient de charger le binaire, la fonction main
( sub_84D
dans la version 32 bits) ressemble à :
Lancez l'analyseur g++ RTTI et exécutez-le en utilisant :
from ida_medigate.rtti_parser import GccRTTIParser
GccRTTIParser.init_parser()
GccRTTIParser.build_all()
Actualisez maintenant la structure C (voir la section Remarques), convertissez v0
en C *
, décompilez à nouveau :
Dans les cas où il n'y a pas de RTTI, notre infrastructure permet toujours de définir manuellement la classe C++. Pour le même exemple (examples/a32_stripped), vous pouvez créer manuellement la structure B, puis sélectionner sa table virtuelle et taper
from ida_medigate import cpp_utils
cpp_utils.make_vtable("B")
make_vtable
peut également obtenir vtable_ea
et vtable_ea_stop
au lieu de la zone sélectionnée.
Créez ensuite la struct C et appliquez l'héritage :
cpp_utils.add_baseclass("C", "B")
Vous pouvez maintenant reconstruire la vtable de classe C en la sélectionnant et en tapant :
cpp_utils.make_vtable("C")
Ajoutez la structure Z, reconstruisez également sa table virtuelle, et voilà la partie intéressante :
cpp_utils.add_baseclass("C", "Z", 0x0c, to_update=True)
qui appliquent l'héritage C de Z au décalage 0x0c et actualisent également la structure (voir remarques).
Il ne reste plus qu'à mettre à jour la deuxième table virtuelle de C, celle qui implémente l'interface de Z. Marquez cette table virtuelle et tapez :
cpp_utils.make_vtable("C", offset_in_class=0x0c)
ida_medigate sait que cette vtable est la vtable de la classe Z et le résultat sera :
Le résultat final est le même que dans le cas RTTI :
Lorsqu'une nouvelle structure vtable est créée (par l'analyseur RTTI ou manuellement par l'utilisateur), chaque fonction qui n'a pas encore changé est renommée, décompilée et définit son premier argument sur this
.
Double-cliquez sur le membre d'une structure qui correspond à une telle fonction pour accéder à la fonction, afin que l'utilisateur puisse lire le flux de code C++ de manière pratique !
Chaque changement de nom ou de type d'une fonction ou de son membre pointeur de fonction correspondant dans les vtables est intégré et synchronisé entre tous. Cela signifie par exemple que l'utilisateur peut modifier le type de membre de la table virtuelle via la fenêtre du décompilateur, et ce nouveau type (prototype) sera également appliqué à la fonction cible.
À la ligne 15 de l'image précédente, il y a un appel à B::sub_9A8 (B::f_b dans le code source). Cet argument de fonction est B *
:
Mais cette fonction peut également être appelée par une instance C
(up-casting). nous voulons voir la fonction virtuelle que son instance appellerait. Supposons qu’il existe de nombreuses classes dérivées potentielles, donc convertir this
en C *
n’est pas toujours possible. Pour cette raison, nous implémentons une union pour chaque classe de base dont les fils ont une table virtuelle différente. On peut choisir d'afficher une table virtuelle dérivée différente des dérivées de B
en cliquant sur alt+y (le raccourci pour choisir un autre membre de l'union) :
donc en fin de compte, nous ne pouvons "caster" que des appels spécifiques vers différentes fonctions virtuelles :
Le Saint Graal des rétro-ingénieurs C++ frustrés. Nous maintenons les xréfs des fonctions virtuelles jusqu'aux membres de la structure vtable qui les représente !
Combiner cela avec ida-referee
nous permet de suivre toutes les xréfs des appels de fonctions virtuelles !
Une limitation : on ne peut tracer que les appels virtuels déjà décompilés. Heureusement, l'auto-analyse sait remplir un type d'argument entre les fonctions, donc avec le processus itératif de conversion de plus d'arguments -> décompilation de toutes les fonctions pertinentes -> lecture à nouveau du code et conversion de plus d'arguments (...) cette capacité devient vraiment puissant!
La façon dont nous marquons les membres de la structure en tant que sous-classes dans IDAPython n'est pas immédiatement synchronisée avec l'IDB. Le hack que nous faisons est de modifier la structure pour qu'une synchronisation soit déclenchée. Vous pouvez également utiliser
utils.refresh_struct(struct_ptr)
qui ajoute un champ factice à la fin de la structure, puis le définit.