Faciliter la création d’exploits avec DragonFFI : le cas de CVE-2018-0977

Spécialité(s)


Résumé

La recherche de vulnérabilités et la création d’exploits noyaux peuvent impliquer le fuzzing et l’appel des API C exposées par le noyau à l’utilisateur. Il peut ainsi être intéressant de pouvoir écrire un fuzzer d’API avec des langages de plus haut niveau tels que Python, et de pouvoir appeler directement les API à fuzzer depuis cedit langage.


Body

Le but de cet article est de démontrer les capacités de DragonFFI [1], et de montrer son utilisation dans le cas de la vulnérabilité CVE-2018-0977 présente dans le noyau Windows [2]. Nous présenterons ainsi dans un premier temps l’outil DragonFFI, ensuite succinctement cette vulnérabilité et comment DragonFFI simplifie l’implémentation d’une preuve de concepts.

Tous les exemples d’utilisation de DragonFFI, ainsi que le code l’exploit final en Python, sont disponibles dans un Jupyter Notebook accessible à cette URL : https://github.com/aguinet/dragonffi-misc.

1. Introduction à DragonFFI

L’installation du paquet Python de DragonFFI se fait simplement grâce à la commande suivante :

pip install pydffi

DragonFFI est un outil permettant d’utiliser des interfaces C à travers d’autres langages. Par exemple, il permet de faire ceci en Python :

import pydffi
CU = pydffi.FFI().cdef("int puts(const char* s);");
CU.funcs.puts("hello world!")

De manière plus avancée, DragonFFI peut aussi traiter des fichiers d’en-tête d’une librairie C, et appeler directement les fonctions associées. Voici un exemple avec libarchive [3] :

import pydffi
pydffi.dlopen("/path/to/libarchive.so")
CU = pydffi.FFI().cdef("#include <archive.h>")
a = funcs.archive_read_new()
assert(a)
...

De plus, ce qui nous intéressera, dans cet article, est sa capacité à pouvoir convertir des structures C en objet Python et vice-versa, afin de pouvoir (par exemple) interpréter des données brutes comme des données structurées.

1.1 Avantages et inconvénients par rapport à ctypes/pycparser/cffi

Le langage Python fournit la librairie standard ctypes [4], qui est basée sur libffi [5]. Elle permet elle aussi d’appeler des fonctions C depuis Python.

pycparser est un analyseur C99 écrit en pur Python. Il est utilisé par cffi afin de pouvoir définir les interfaces et les structures à exposer en Python. Le principal souci de cette approche est l’incapacité de pycparser à directement traiter des fichiers d’en-têtes, et qu’il faut souvent les adapter. Cela implique généralement une deuxième version des API à réécrire, qui peut être fastidieuse à réaliser et à maintenir.

Ceci étant dit, l’intérêt principal de libffi et ses usages, est que la librairie est très légère (quelques dizaines de Ko), a contrario de la vingtaine de Mo de la libraire de DragonFFI, qui embarque un compilateur C/C++ complet (clang).

1.2 Quel est le problème sous-jacent ?

Une Application Binary Interface (ABI) est une suite de spécifications décrivant, entre autres :

  • les conventions d’appels d’une fonction au niveau binaire (e.g. le premier argument scalaire d’une fonction passe par le registre RDI avec l’ABI SystemV) ;
  • le format de représentation des différents types et structures utilisés en C.

La façon dont l’état de la pile et des registres doivent être préparés, lors de l’appel une fonction C donnée, dépend ainsi de ces spécifications. Il faut ainsi implémenter toutes ces différentes procédures pour chaque couple architecture/système d’exploitation donné. libffi réimplémente toute cette logique, là où DragonFFI réutilise ce qui existe dans Clang/LLVM.

De plus, la représentation en mémoire d’une structure C dépend du système et du CPU pour lesquels cette structure a été compilée. Des différences sont ainsi visibles par exemple au niveau du padding de certains champs et de la taille de certains de ses membres.

1.3 Fonctionnement général de DragonFFI

DragonFFI se base sur Clang [6] pour traiter le langage C en entrée, et LLVM [7] afin de compiler le code qui fait glu entre le langage « haut niveau » utilisé (e.g. Python) et les API C ciblées.

Plus concrètement, Clang est utilisé pour compiler les différentes définitions fournies en IR LLVM, en incluant des métadonnées servant à générer les informations de debug. Ces informations de debug sont ensuite interprétées par DragonFFI, qui crée la liste des types et fonctions utilisés par cette unité de compilation. Pour chacune des fonctions à appeler, un wrapper est ensuite créé, qui permet de laisser Clang gérer les différentes ABI utilisées par les différentes plateformes. Par exemple, pour la fonction puts, ce wrapper ressemble à ceci :

void call_puts(void* Ret, void** Args) {
  *((int*)Ret) = puts((const char*) Args[0]);
}

Ce design permet d’utiliser les informations DWARF présentes dans un binaire afin d’en extraire les différentes interfaces et structures utilisées pour pouvoir les utiliser depuis un autre langage. Cette fonctionnalité est encore en cours de développement, et plus d’informations sont disponibles à ce sujet sur le blog du projet LLVM [8].

2. Exemples d’utilisation de DragonFFI

2.1 Utilisation d’une librairie C

Voici un exemple de code permettant d’utiliser libarchive [3] depuis Python afin de lister le contenu d’une archive passée en argument :

import pydffi
import sys
 
pydffi.dlopen("/path/to/libarchive.so")
D = pydffi.FFI()
CU=D.cdef('''
#include <archive.h>
#include <archive_entry.h>
''')
 
a = CU.funcs.archive_read_new()
CU.funcs.archive_read_support_filter_all(a)
CU.funcs.archive_read_support_format_all(a)
r = CU.funcs.archive_read_open_filename(a, sys.argv[1], 10240)
assert(r)
entry = pydffi.ptr(CU.types.archive_entry)()
while CU.funcs.archive_read_next_header(a, pydffi.ptr(entry)) == 0:
    pathname = CU.funcs.archive_entry_pathname_utf8(entry)
    print(pathname.cstr.tobytes().decode("utf8"))
    archive_read_data_skip(a)
funcs.archive_read_free(a)

Quelques remarques :

  • Il faut tout d’abord charger la librairie dynamiquement en mémoire. Cela est fait grâce à la fonction pydffi.dlopen, qui existe pour toutes les plateformes supportées (Linux/OSX/Windows).
  • Nous utilisons les en-têtes présents sur le système hôte, et ce sans aucune modification.
  • DragonFFI permet d’utiliser les chaînes de caractères C sans trop de difficultés, en les interprétant comme des objets bytes en Python.

2.2 Utilisation de structures C en Python

Cette section montre comment des structures C peuvent être utilisées directement en Python, afin de lire et/ou modifier les données de celles-ci. Nous verrons comment nous pouvons interpréter de manière cross-platform des données provenant d’un système différent du système hôte.

Pour les exemples, nous prendrons une structure de liste doublement chaînée, contenant un padding différent suivant les architectures :

typedef struct _Node {
  unsigned char val;
  long count;
  unsigned short n;
  struct _Node* prev;
  struct _Node* next;
} Node;

DragonFFI peut nous aider à faire deux opérations :

  • créer des objets Node directement en Python, et en obtenir une représentation native sous forme d’octets consécutifs ;
  • partir d’une représentation native d’un objet Node, et obtenir une structure Python permettant d’interpréter facilement les différents champs.

Nous allons voir trois façons de faire ces deux opérations. La première utilise DragonFFI directement, la deuxième le module struct, et la dernière le projet purectypes [9].

Sauf mention contraire, ces exemples fonctionnent sous Linux/x86-64.

2.2.1 Directement avec DragonFFI

Déclarons la structure suivante en Python :

import pydffi
FFI = pydffi.FFI()
CU = FFI.cdef('''
typedef struct _Node {
  unsigned char val;
  long count;
  unsigned short n;
  struct _Node* prev;
  struct _Node* next;
} Node;
''')
Node = CU.types.Node

Nous pouvons obtenir diverses informations sur le type Node, comme l’offset du membre n :

>>> print(Node.n.offset)
16

Nous pouvons créer une liste chaînée d’objets et obtenir leur représentation en mémoire. Il faut noter ici que tout ce que nous faisons se passe dans le processus Python courant. Les pointeurs obtenus pointent vers la mémoire de ce processus :

PtrNode = FFI.pointerType(Node)
ObjA = Node(val=0,count=10,n=1,prev=PtrNode(),next=PtrNode())
ObjB = Node(val=1,count=2,n=10,prev=pydffi.ptr(ObjA),next=PtrNode())

DragonFFI permet d’obtenir une memoryview [10] sur ces objets, afin de pouvoir les lire et/ou les modifier directement sans aucune copie mémoire :

>>> DataB = pydffi.view_as_bytes(ObjB)
>>> hexdump(bytes(DataB))
00000000: 01 ED E7 01 00 00 00 00 02 00 00 00 00 00 00 00 ................
00000010: 0A 00 00 00 00 00 00 00 50 7E E9 01 00 00 00 00 ........P~......
00000020: 00 00 00 00 00 00 00 00                           ........
 
>>> print(ObjB.val)
1
>>> DataA[0] = 20
>>> print(ObjB.val)
20

Dans l’autre sens, nous pouvons interpréter des données brutes comme un objet Node :

>>> import struct
>>> Data = struct.pack("<QQQQQ", 1,2,10,0xAA,0xBB)
>>> Obj = Node()
>>> ObjMem = pydffi.view_as_bytes(Obj)
>>> ObjMem[:] = Data
>>> print(Obj.count, Obj.n, Obj.val, hex(Obj.prev.value), hex(Obj.next.value))
2 10 1 0xaa 0xbb

2.2.2 En utilisant le module struct de Python

DragonFFI est capable de générer une chaîne de caractères représentant une structure au format utilisé par le module struct de Python :

>>> print(pydffi.portable_format(Node))
<BxxxxxxxqHxxxxxxQQ

Le padding est représenté par le caractère x, ce qui permet d’ignorer les octets associés.

L’API portable_format donne une représentation de la structure pour l’ABI en cours. Un des avantages est que celle-ci peut être utilisée sans aucun souci avec d’autres systèmes. Cela permet par exemple d’exporter une structure sous un système d’exploitation donné, et de pouvoir l’utiliser avec un autre.

L’utilisation du module struct peut convenir pour des structures simples. Cependant, il peut atteindre ses limites lors de l’utilisation de structures avec plusieurs dizaines de membres. Une autre solution est présentée dans la section suivante afin de pallier à ce problème.

2.2.3 En utilisant purectypes

La librairie pure Python purectypes [9] permet de packer/dépacker des structures C préalablement décrites avec son système de types. DragonFFI possède un générateur pour ce système de types.

Cela permet de générer des structures portables en Python à partir de structures C et ce, pour une ABI donnée.

import purectypes
# Récupération de la compilation unit DragonFFI contenant notre type Node
CU = FFI.cdef(''' ... ''')
# Création d'un "générateur de types" purectypes
G = purectypes.generators.pydffi()
NodeTy = G(CU.types.Node)
open("node.py","w").write(purectypes.dump(NodeTy))

Le fichier node.py contient du code dépendant seulement de purectypes, qui permet de définir notre type Node. Comme décrit précédemment, ce type dépend de l’ABI du système sur lequel nous l’avons généré.

Nous pouvons ainsi utiliser ce fichier node.py de manière portable afin d’interpréter et générer des données de notre structure Node :

>>> import purectypes
>>> from node import Node
>>> Data = bytes.fromhex("01 ... ")
>>> Obj = purectypes.unpack(Node, Data)
>>> print(Obj.val)
1
 
>>> Obj.val = 2
>>> hexdump(purectypes.pack(Node, Obj))
00000000: 02 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................
00000010: 0A 00 00 00 00 00 00 00 50 7E E9 01 00 00 00 00 ........P~......
00000020: 00 00 00 00 00 00 00 00                           ........

2.3 Intérêts pratiques de DragonFFI

Dans le cadre de la recherche de vulnérabilités et l’exploitation de ces dernières, DragonFFI présente aux moins deux avantages non négligeables :

  • le prototypage rapide de preuves de concept (PoC) ;
  • la dispense d’une chaîne de compilation.

Le premier point coule de source, si tant est que les personnes réalisant le prototypage soient un peu familières avec le langage Python (ce qui est souvent le cas dans le milieu de la sécurité informatique) et DragonFFI. In fine, l’utilisation de DragonFFI ne change pas, ni ne complexifie, outre mesure l’appel des API d’un système face par exemple à des langages compilés comme le C ou le C++. Bien évidemment, on bénéficie de la facilité et de la flexibilité d’utilisation du langage Python pour le reste.

Le deuxième point semble être moins prégnant à première vue, mais revêt un caractère important notamment avec les personnes peu au fait des subtilités et complexités d’une chaîne de compilation dédiée aux systèmes Windows. En effet, un interpréteur Python, le paquet DragonFFI et l’installation du SDK de Windows sont suffisants pour tester rapidement des appels aux API C exposées par le système, évitant la nécessité d’avoir une installation complète de Visual Studio.

2.4 Appel de fonctions C de l’API Windows

L’appel de fonctions Windows nécessite une certaine préparation. On devra tout d’abord :

  1. Faire une liste des API Windows appelées dans notre code ;
  2. Trouver les entêtes correspondants aux API appelées ;
  3. Trouver les bibliothèques dynamiques hébergeant les API et les charger ;
  4. Créer une unité de compilation (pydffi.CompilationUnit) ;
  5. Appeler les API.

Fort heureusement, les étapes 1 à 4 peuvent faire l’objet d’une petite bibliothèque évitant de recréer la roue à chaque fois.

Prenons un exemple concret et simple où nous souhaitons appeler l’API Windows GetProcessId() qui comme son nom l’indique renvoie l’identifiant du processus courant.

def main():
    # load library
    load_system_shared_lib("kernel32.dll")
    FFI = pydffi.FFI()
 
    # make a compilation unit out of the includes and defines
    CU = FFI.cdef('''
#include <Windows.h>
#include <Windef.h>
#include <processthreadsapi.h>
''')
 
    pid = CU.funcs.GetCurrentProcessId()
    print(f"Current Process Id: {pid.value:#x}")

Reprenons les premiers points de notre liste précédente :

  1. Nous n’appellerons que GetProcessId() ;
  2. La MSDN nous indique que l’entête associé est processthreadsapi.h [14] ;
  3. La MSDN nous indique que la fonction est exportée par la bibliothèque Kernel32.dll.

Le chargement de la bibliothèque dynamique est réalisé par la fonction load_system_shared_lib qui n’est qu’un emballage autour de la fonction pydffi.dlopen :

def load_system_shared_lib(shared_lib_name: str) -> None:
    """ Load a shared lib from the '/system32' folder.
 
    Args:
        shared_lib_name: name of the system shared lib. Must be in \system32.
    """
    comspec = os.environ.get("COMSPEC")
    if comspec:
        windows_base = comspec.parent
    else:
        windows_base = pathlib.Path(r"c:\windows\system32")
    dll_path = windows_base / shared_lib_name
    pydffi.dlopen(str(dll_path))

Cette fonction permet de trouver le chemin exact d’une librairie du système Windows. En effet, la fonction pydffi.dlopen a besoin d’un chemin absolu pour fonctionner, et laisse la responsabilité à l’utilisateur de trouver ce chemin.

Concernant les en-têtes du SDK Windows, DragonFFI récupère les chemins associés grâce aux mécanismes existants dans Clang. L’utilisation des variables d’environnements exportées par Visual Studio (à travers le script vcvarsall.bat) permet éventuellement de choisir un SDK Windows par rapport à un autre.

Notez que dans notre programme d’exemple nous incluons aussi, en plus de l’entête nécessaire pour GetProcessId(), l’entête windef.h qui contient les définitions pour les types de bases utilisés par la grande majorité des API Windows.

Afin d’utiliser les fonctions de ces en-têtes, on peut ainsi utiliser directement la fonction cdef de DragonFFI, pour définir une unité de compilation contenant toutes ces fonctions. À noter que, si besoin, une liste de répertoires, où trouver en plus des fichiers d’en-tête, peut être fournie grâce à l’option includeDirs de l’objet FFI :

FFI = pydffi.FFI(includeDirs=[])
CU = FFI.cdef('''
#include <Windows.h>
#include <Windef.h>
#include <processthreadsapi.h>
''')

Si l’on ne préfère pas inclure Windows.h, qui expose un certain nombre de types et fonctions dont nous n’avions pas forcément besoin, les définitions pour le préprocesseur C nécessaire peuvent se composer ainsi :

# see https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=vs-2019
PRE_DEFINES = [
    ("__STDC__", 1), # as C
    ("_M_AMD64 ", 100), # x64 (defined when targeting x64 processors.)
    ("_M_X64 ", 100), # x64 (defined when targeting x64 processors.)
    ("_MSC_FULL_VER ", 192227905), # compiler version (e.g. 19.22.27905 for VS 2019 version 16.2)
    ("_MSC_VER ", 1922), # compiler version
    ("_WIN32 ", 1), # defined for 32-bit ARM, 64-bit ARM, x86 or x64.
    ("_WIN64 ", 1), # defined for 64-bit ARM or x64.
    ("_AMD64_ ", 1), # (set internally in the headers)
    ("_WIN64 ", 1) # compiler defined.
]

Il faut ensuite déclarer ces en-têtes grâce à la directive #define dans la chaîne passée à la fonction cdef :

FFI.cdef("\n".join("#define %s %s" % d for d in PRE_DEFINES) + '''
#include <Windef.h>
#include <processthreadsapi.h>
''')

On pourra bien sûr ajuster ces mêmes définitions suivant le cas pratique qui nous intéresse. Cela dit, les définitions présentées ci-dessus fonctionnent dans une majorité des cas d’usage concernant l’exécution de code sous les systèmes d’exploitation Windows x64.

Voyons maintenant comment utiliser DragonFFI pour créer une preuve de concept pour la vulnérabilité CVE-2018-0977.

3. Introduction à la CVE-2018-0977

Cette vulnérabilité a fait l’objet d’un article détaillé dans MISC n°104 [11].

La CVE-2018-0977 est une vulnérabilité de type untrusted pointer dereference [12] affectant le pilote noyau BasicRender.sys, qui fait partie du composant graphique DirectX de Windows.

En tant que collection de bibliothèques pour développer des applications multimédias, DirectX offre plusieurs API. Parmi ces API, il existe une API bas niveau exposée par le fichier d’en-tête d3dkmthk.h, qui est surtout destinée aux développeurs de cartes graphiques. Cette API permet de gérer des objets DirectX comme les périphériques, contextes, allocations et commandes (Devices, Contexts, Allocations, Commands).

Windows fournit par défaut BasicRender.sys, un pilote générique remplaçant l’ancien driver VGA, qui fait office de carte graphique logicielle et qui est en même temps compatible avec DirectX. Afin d’interagir avec le pilote BasicRender.sys, il est nécessaire de configurer le contexte DirectX en appelant une séquence de fonctions de l’API bas niveau. L’initialisation de DirectX étant effectuée, il devient possible d’envoyer des tampons de Command au pilote en appelant la fonction D3DKMTSubmitCommand(), qui reçoit un argument de type D3DKMT_SUBMITCOMMAND.

En effet, la vulnérabilité est la suivante : la valeur du membre Commands de la structure D3DKMT_SUBMITCOMMAND passée en argument est interprétée comme un pointeur par le pilote BasicRender.sys. Ce driver déréférence deux fois le pointeur fourni depuis le mode utilisateur avant d’en appeler la fonction pointée, sans effectuer aucune vérification. Cela signifie qu’en fournissant une structure D3DKMT_SUBMITCOMMAND dont le membre Commands pointe sur une chaîne de pointeurs spécialement construite, il est bien possible de faire basculer l’exécution du noyau sur une adresse arbitraire.

4. Création de l’exploit pour la CVE-2018-0977

Dans la pratique, le développement de cet exploit nécessite l’installation du WDK (Windows Driver Kit) de Microsoft pour l’utilisation de certains entêtes, notamment d3dkmthk.h. De manière non intuitive, le WDK contient aussi des définitions de types et fonctions résidant uniquement en espace utilisateur. Notez que l’utilisation des entêtes du WDK ne nécessite pas de définitions particulières à passer au compilateur.

4.1 Fonctions bas niveau

Le développement d’un exploit, notamment d’un exploit noyau, nécessite souvent de faire appel à des fonctions internes du système ne disposant pas de prototypes « officiels ». Il sera nécessaire dans ce cas d’utiliser des entêtes tiers (comme le Native Development Kit d’A. Ionescu [13]) ou de définir les prototypes de fonctions soi-même.

Dans notre cas, l’exploit fait usage de la fonction interne NtAllocateReserveObject (exportée par ntdll.dll) :

_WINTERNALS_FUNC_DEFS: str = """
NTSTATUS
NTAPI
NtAllocateReserveObject(
    _Out_ PHANDLE hObject,
    _In_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ DWORD ObjectType
);
 
# ...
"""

On passera les prototypes de ces fonctions à la fonction cdef() de DragonFFI en sus des définitions pour le compilateur et les entêtes nécessaires. On pourra alors appeler ces fonctions sans plus de formalités, les fonctions internes du système étant ici limitées à la bibliothèque ntdll nécessairement chargée dans tous les processus.

4.2 Exemple d’appel aux fonctions DirectX

Ci-dessous un exemple d’appel à une fonction DirectX nommée D3DKMTEnumAdapters(). Cette fonction renvoie la liste des adaptateurs graphiques disponibles sur la machine cible :

# from <d3dkmt.h>
KMTQAITYPE_CPDRIVERNAME = ffi.types.KMTQUERYADAPTERINFOTYPE.KMTQAITYPE_CPDRIVERNAME
 
adapters = ffi.types.D3DKMT_ENUMADAPTERS()
padapters = pydffi.ptr(adapters)
ffi.funcs.memset(padapters, 0, pydffi.sizeof(adapters))
status = int(ffi.funcs.D3DKMTEnumAdapters(padapters))
if status != 0:
    print(f"D3DKMTEnumAdapters failed with NTSTATUS: {IntUtils.twos_complement(status, 32):#x}")
    return 0

Ici, ffi est une instance de la classe pydffi.FFI.

Décomposons le code d’appel à la fonction ; ici la première ligne :

# from <d3dkmt.h>
KMTQAITYPE_CPDRIVERNAME = ffi.types.KMTQUERYADAPTERINFOTYPE.KMTQAITYPE_CPDRIVERNAME

Le code ci-dessus obtient la valeur d’un membre (KMTQAITYPE_CPDRIVERNAME) d’une énumération DirectX (KMTQUERYADAPTERINFOTYPE).

Le membre types de la classe ffi est une simple propriété Python permettant d’obtenir n’importe quel type pourvu qu’il soit connu de l’unité de compilation :

@property
def types(self) -> pydffi.CUTypes:
    """Get all types from the compilation unit.
 
    Returns:
        An instance of `pydffi.CUTypes` containing all the know types.
    """
    if not self._compilation_unit:
        raise RuntimeError("No compilation unit. Call cdef() first.")
    return self._compilation_unit.types

L’allocation et l’utilisation d’une structure définie dans les entêtes de Windows ne sont pas plus complexes :

adapters = ffi.types.D3DKMT_ENUMADAPTERS()
padapters = pydffi.ptr(adapters)

La première ligne alloue la structure (ici de type D3DKMT_ENUMADAPTERS) et la ligne suivante crée un pointeur vers cette structure.

Vient ensuite la mise à zéro de la structure (memset) et le passage du pointeur de la structure en argument à la fonction D3DKMTEnumAdapters :

ffi.funcs.memset(padapters, 0, pydffi.sizeof(adapters))
status = int(ffi.funcs.D3DKMTEnumAdapters(padapters))
if status != 0:
    print(f"D3DKMTEnumAdapters failed with NTSTATUS: {IntUtils.twos_complement(status, 32):#x}")
    return 0

L’appel de la fonction utilise ici la propriété funcs de notre wrapper.

Notez que la majorité des fonctions bas niveau de Windows renvoie un type NTSTATUS qui est un type 32 bits signé.

Dans les faits, en imaginant qu’une erreur survienne, il est nécessaire de convertir l’entier renvoyé afin d’en faire une représentation hexadécimale sensée :

  • la fonction renvoie la valeur décimale -1073741811 ;
  • si on l’imprimait en hexadécimal via Python, celui-ci afficherait -0x3ffffff3 ;
  • on utilise un simple complément à deux pour afficher en fait la valeur 0xc000000d.

Cette dernière valeur est un NTSTATUS bien connu des développeurs Windows :

#define STATUS_INVALID_PARAMETER ((NTSTATUS) 0xC000000D)

4.3 Utilisation de Buffers

Ci-dessous un exemple en langage C d’allocation d’un buffer, remplissage, transtypage vers un autre type et finalement utilisation du buffer via le pointeur d’un second type :

BYTE *buffer = (BYTE*) malloc(0x60);
memset(buffer, 0x41, 0x60);
 
// transtypage vers DWORD*
DWORD *dw_buffer = (DWORD*) buffer;
dw_buffer[0] = 0xdeadbeef

L’utilisation d’un buffer de type donné est relativement simple via DragonFFI :

buffer_size = 0x60
 
# allocate a 0x60 long BYTE buffer and memset it with 0x41 ('A')
buffer = ffi.ffi.arrayType(ffi.types.BYTE, buffer_size)()
pbuffer = pydffi.ptr(buffer)
ffi.funcs.memset(pbuffer, 0x41, buffer_size)

Le transtypage via DragonFFI se fait comme suit :

# cast a BYTE buffer to a DWORD buffer. Set element at index 0 in the DWORD buffer.
buffer_size_in_dword = buffer_size // ffi.types.DWORD.size
buffer_dw = pydffi.cast(buffer, ffi.ffi.arrayType(ffi.types.DWORD, buffer_size_in_dword))
buffer_dw[0] = 0xdeadbeef

Notez ici que le transtypage n’est possible qu’en passant la taille du buffer après son transtypage. Dans les faits, il est impossible de transtyper un buffer vers un type dont la taille ne serait pas adéquate. DragonFFI propose ici une certaine sécurité.

À l’heure actuelle, la seule limitation potentiellement contraignante — pour le développement d’exploits notamment — est le manque de prise en charge par DragonFFI des structures contenant des bitfields.

Au final, la traduction, voire le développement à partir de rien, de preuves de concepts ou d’exploits système n’est pas plus compliquée que dans un langage compilé pour peu que l’on ait les quelques rudiments nécessaires pour démarrer avec DragonFFI. Bien évidemment, l’utilisation de Python rend tout un tas de manipulations contraignantes en langage compilé (qui a dit manipulation de chaînes de caractères ?) bien plus simples.

Conclusion

La possibilité de pouvoir appeler des API C « bas niveau » facilement depuis Python rend le développement de preuves de concept de certains exploits plus facile. En effet, cela permet de conjuguer la facilité de développement introduite par ce langage, avec la possibilité de descendre au niveau d’une API C de manière quasi-transparente.

DragonFFI est en évolution permanente, et d’autres fonctionnalités sont à prévoir telles que le support de la lecture des informations de debug afin de retrouver les types des fonctions exportées dans une librairie dynamique [8].

Références

[1] DragonFFI : https://github.com/aguinet/dragonffi

[2] CVE-2018-0977 : https://nvd.nist.gov/vuln/detail/CVE-2018-0977

[3] libarchive : https://www.libarchive.org/

[4] ctypes Python module : https://docs.python.org/3/library/ctypes.html

[5] libffi : https://sourceware.org/libffi/

[6] Clang : https://clang.llvm.org/

[7] LLVM : http://llvm.org/

[8] DragonFFI: FFI/JIT for the C language using Clang/LLVM : http://blog.llvm.org/2018/03/dragonffi-ffijit-for-c-language-using.html

[9] https://github.com/aguinet/purectypes

[10] https://www.geeksforgeeks.org/memoryview-in-python/

[11] Exploitation du CVE-2018-0977 dans le noyau Windows : https://connect.ed-diamond.com/MISC/MISC-104/Exploitation-du-CVE-2018-0977-dans-le-noyau-Windows

[12] CWE-822: Untrusted Pointer Dereference : https://cwe.mitre.org/data/definitions/822.html

[13] https://code.google.com/archive/p/native-nt-toolkit/

[14] https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocessid



Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous