Reverse d’API non documentée

Salut ce forum est dédié à comprendre les entrailles de l'ordinateur au plus bas niveau bienvenue dans le monde du reverse et de l'assembleur

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Reverse d’API non documentée

Post by Hydraxx »

Reverse d’API non documentée

Objectif du chapitre

Ce chapitre montre comment partir d’une fonction inconnue ou peu documentée et reconstruire progressivement :
- son rôle ;
- ses paramètres ;
- ses structures internes ;
- ses callbacks ;
- ses boucles ;
- sa logique générale ;
- un pseudo-code C/C++ compréhensible.

L’idée importante est qu’en reverse engineering on ne cherche pas à lire de l’assembleur pour lire de l’assembleur. On cherche à transformer des instructions machine en concepts connus : pointeurs, structures, boucles, tableaux, arbres, callbacks et appels d’API.

1. Repérer une API intéressante

Une API non documentée peut être repérée dans les exports d’une DLL, dans Ghidra, dans x64dbg, avec dumpbin ou en observant les appels effectués par un programme.

Code: Select all

dumpbin /exports ntdll.dll
Si on trouve :

Code: Select all

RtlInitializeGenericTable
RtlInsertElementGenericTable
RtlLookupElementGenericTable
RtlDeleteElementGenericTable
on peut déjà deviner qu’il existe une structure interne appelée Generic Table.

2. Commencer par les paramètres

En x64 Windows, les quatre premiers paramètres entiers/pointeurs sont généralement passés dans RCX, RDX, R8 et R9.

Si RCX est utilisé comme base d’accès mémoire :

Code: Select all

mov eax, [rcx+14h]
alors RCX est probablement un pointeur vers une structure.

Pseudo-code :

Code: Select all

eax = *(int*)((char*)rcx + 0x14);
3. MOV, crochets et pointeurs

Code: Select all

mov rax, rcx
Copie simplement la valeur de RCX dans RAX.

Code: Select all

mov rax, [rcx]
lit la mémoire située à l’adresse contenue dans RCX.

Code: Select all

mov eax, [rcx+10h]
signifie :

Code: Select all

eax = *(uint32_t*)((char*)rcx + 0x10);
En reverse, [registre + offset] est essentiel pour reconstruire les champs d’une structure.

4. LEA : calculer une adresse sans lire la mémoire

Code: Select all

lea rax, [rcx+20h]
RAX reçoit l’adresse RCX + 0x20. Il n’y a pas de lecture de la valeur contenue à cette adresse.

Equivalent C :

Code: Select all

rax = (char*)rcx + 0x20;
Comparaison :

Code: Select all

mov rax, [rcx+20h]   ; lit la valeur en mémoire
lea rax, [rcx+20h]   ; calcule seulement l'adresse
LEA est donc très souvent utilisé pour fabriquer un pointeur.

5. Déduire une structure à partir des offsets

Supposons :

Code: Select all

mov rax, [rcx]
mov rbx, [rcx+8]
mov edx, [rcx+10h]
mov eax, [rcx+14h]
On peut proposer :

Code: Select all

struct UNKNOWN
{
    void* field0;      // +0x00
    void* field1;      // +0x08
    uint32_t field2;   // +0x10
    uint32_t field3;   // +0x14
};
Au début ce ne sont que des hypothèses. On les confirme en regardant comment chaque champ est utilisé.

6. Identifier un compteur

Code: Select all

mov eax, [rcx+14h]
inc eax
mov [rcx+14h], eax
Pseudo-code :

Code: Select all

table->Count++;
Si une autre fonction fait seulement :

Code: Select all

mov eax, [rcx+14h]
ret
et qu’elle est censée retourner le nombre d’éléments, l’hypothèse est presque confirmée.

7. Identifier un pointeur

Code: Select all

mov rax, [rcx+8]
test rax, rax
je empty
Equivalent :

Code: Select all

if (table->Root == nullptr)
{
    ...
}
TEST registre, registre est très courant pour vérifier NULL.

8. Reconnaître une condition

Code: Select all

cmp eax, ebx
je equal
jg greater
jl lower
Equivalent :

Code: Select all

if (eax == ebx) { ... }
else if (eax > ebx) { ... }
else { ... }
9. Reconnaître une boucle

Code: Select all

loop_start:
    test rcx, rcx
    je done

    mov eax, [rcx]
    mov rcx, [rcx+8]

    jmp loop_start

done:
Ici il n’y a pas besoin de INC : l’avancement se fait avec le pointeur suivant stocké dans la structure.

Pseudo-code :

Code: Select all

while (node != nullptr)
{
    int value = node->value;
    node = node->next;
}
10. Reconnaître un tableau

Code: Select all

mov eax, [rcx+r8*4]
inc r8
cmp r8, rdx
jb loop_start
Pseudo-code :

Code: Select all

for (size_t i = 0; i < count; i++)
{
    int value = array[i];
}
Le facteur *4 suggère des éléments de 4 octets. Pour des pointeurs 64 bits, on voit souvent *8.

11. Reconnaître un callback

Code: Select all

mov rax, [rcx+20h]
call rax
Le champ [rcx+20h] contient probablement l’adresse d’une fonction.

Pseudo-code :

Code: Select all

table->CompareCallback(...);
Autre forme fréquente :

Code: Select all

call qword ptr [rcx+20h]
12. Reconstruire un prototype

Code: Select all

mov rax, [rcx+20h]
mov rcx, rbx
mov rdx, rsi
call rax
Le callback reçoit au moins deux paramètres :

Code: Select all

typedef int (*COMPARE_CALLBACK)(void* a, void* b);
Si EAX est ensuite comparé à zéro, on peut penser à une fonction de comparaison.

13. Cas pratique : Generic Table

Le chapitre analyse plusieurs fonctions de la Generic Table de NTDLL. Le but n’est pas seulement de comprendre ces fonctions, mais d’apprendre une méthode réutilisable sur n’importe quelle API inconnue.

14. RtlInitializeGenericTable

Une fonction d’initialisation révèle souvent directement la disposition interne d’une structure.

Exemple simplifié :

Code: Select all

mov [rcx+00h], rdx
mov [rcx+08h], r8
mov [rcx+10h], r9
mov dword ptr [rcx+18h], 0
On pourrait reconstruire :

Code: Select all

struct TABLE
{
    void* CompareRoutine;
    void* AllocateRoutine;
    void* FreeRoutine;
    uint32_t Count;
};
15. RtlNumberGenericTableElements

Code: Select all

mov eax, [rcx+18h]
ret
Si cette fonction retourne le nombre d’éléments, l’offset +0x18 est confirmé comme compteur.

16. Recherche dans une structure

Code: Select all

mov rbx, [rcx+8]

search:
    test rbx, rbx
    je not_found

    mov eax, [rbx+10h]
    cmp edx, eax
    je found
    jl go_left

    mov rbx, [rbx+8]
    jmp search

go_left:
    mov rbx, [rbx]
    jmp search
Pseudo-code :

Code: Select all

NODE* node = table->Root;
while (node)
{
    if (value == node->value) return node;
    if (value < node->value) node = node->left;
    else node = node->right;
}
On vient de reconnaître un arbre binaire.

17. Reconstruire la structure NODE

À partir des accès [rbx], [rbx+8] et [rbx+10h] :

Code: Select all

struct NODE
{
    NODE* left;
    NODE* right;
    int value;
};
Puis on complète au fur et à mesure que d’autres offsets apparaissent.

18. Comprendre les arbres binaires

Code: Select all

        50
       /  \
     20    80
    / \    / \
   10 30  60 90
Recherche de 60 : 60 > 50 donc droite, 60 < 80 donc gauche, puis 60 == 60.

19. Splay Tree

Un splay tree peut réorganiser ses nœuds après un accès pour rapprocher l’élément utilisé de la racine.

Avant :

Code: Select all

        100
       /
      50
     /
    20
Après un accès à 20 :

Code: Select all

      20
        \
         50
           \
            100
20. RtlLookupElementGenericTable

Une recherche repose souvent sur un pointeur vers le nœud courant, une comparaison et un déplacement vers le fils gauche ou droit.

Code: Select all

loop:
    test rbx, rbx
    je not_found

    mov rcx, searchValue
    mov rdx, [rbx+DataOffset]
    call compareCallback

    test eax, eax
    je found
    jl left

right:
    mov rbx, [rbx+RightOffset]
    jmp loop

left:
    mov rbx, [rbx+LeftOffset]
    jmp loop
21. RtlInsertElementGenericTable

Une insertion doit généralement rechercher si l’élément existe, trouver le futur parent, allouer un nœud, copier les données, relier le nœud, incrémenter le compteur et éventuellement réorganiser l’arbre.

Pseudo-code simplifié :

Code: Select all

NODE* Insert(TABLE* table, void* data)
{
    NODE* parent = FindInsertionPoint(table, data);
    NODE* node = table->AllocateRoutine(sizeof(NODE));
    node->left = nullptr;
    node->right = nullptr;
    memcpy(node->data, data, table->ElementSize);

    if (parent == nullptr) table->Root = node;
    else if (table->CompareRoutine(data, parent->data) < 0) parent->left = node;
    else parent->right = node;

    table->Count++;
    return node;
}
22. Reconnaître une copie mémoire

Code: Select all

copy_loop:
    mov eax, [rsi]
    mov [rdi], eax
    add rsi, 4
    add rdi, 4
    sub rcx, 4
    jne copy_loop
Pseudo-code :

Code: Select all

memcpy(destination, source, size);
Il faut apprendre à reconnaître les comportements, pas seulement les instructions.

23. RtlDeleteElementGenericTable

Une suppression implique généralement de trouver le nœud, réparer les liens, éventuellement réorganiser l’arbre, appeler le callback de libération et décrémenter le compteur.

24. Déduire le type d’un champ

Code: Select all

mov eax, [rcx+10h]      ; lecture 32 bits
mov rax, [rcx+10h]      ; lecture 64 bits
movzx eax, byte ptr [rcx+10h] ; 1 octet
movzx eax, word ptr [rcx+10h] ; 2 octets
La taille de l’accès donne un indice sur le type du champ.

25. Workflow x64dbg

1. Charger le programme.
2. Placer un breakpoint sur l’API intéressante.
3. Lancer le programme.
4. Observer RCX/RDX/R8/R9.
5. Suivre les pointeurs dans le dump mémoire.
6. Avancer instruction par instruction.
7. Noter chaque offset.
8. Noter les appels indirects.
9. Donner un rôle aux registres.
10. Réécrire progressivement en pseudo-code.

Exemple de notes :

Code: Select all

RCX = table
RDX = element
[RCX+08] = root
[RCX+18] = count
[RCX+20] = compareCallback
26. Workflow Ghidra

Dans Ghidra : ouvrir la fonction, regarder le désassemblage et le décompilateur, renommer les variables, créer une structure, appliquer la structure au pointeur et renommer les fonctions selon leur comportement.

Avant :

Code: Select all

FUN_140001230(param_1, param_2)
Après analyse :

Code: Select all

FindNode(table, key)
27. Reconstituer une structure dans Ghidra

Si on observe :

Code: Select all

[RCX+00]
[RCX+08]
[RCX+10]
[RCX+18]
[RCX+20]
on peut créer une structure provisoire, puis la corriger au fur et à mesure.

28. Reconstruire du C à partir de l’ASM

Code: Select all

mov eax, [rcx+18h]
test eax, eax
je empty
mov rax, [rcx+8]
ret
empty:
xor eax, eax
ret
Pseudo-code :

Code: Select all

void* GetRoot(TABLE* table)
{
    if (table->Count == 0) return nullptr;
    return table->Root;
}
29. Méthode générale pour une fonction inconnue

Étape 1 : identifier les paramètres.
Étape 2 : repérer les pointeurs.
Étape 3 : noter tous les accès [registre+offset].
Étape 4 : identifier les callbacks avec CALL registre ou CALL [mémoire].
Étape 5 : reconstruire les conditions avec CMP, TEST et Jcc.
Étape 6 : reconstruire les boucles.
Étape 7 : identifier tableaux, listes, arbres ou structures.
Étape 8 : écrire du pseudo-code C.
Étape 9 : confirmer avec d’autres fonctions utilisant la même structure.

30. Exercice pratique complet

Code: Select all

#include <iostream>

struct Node
{
    int value;
    Node* left;
    Node* right;
};

Node* Find(Node* node, int value)
{
    while (node)
    {
        if (node->value == value)
            return node;

        if (value < node->value)
            node = node->left;
        else
            node = node->right;
    }

    return nullptr;
}

int main()
{
    Node a{50, nullptr, nullptr};
    Node b{20, nullptr, nullptr};
    Node c{80, nullptr, nullptr};

    a.left = &b;
    a.right = &c;

    Node* result = Find(&a, 80);

    if (result)
        std::cout << result->value << std::endl;
}
Compile en Release sans PDB. Dans x64dbg, retrouve le pointeur du nœud courant, l’offset de value, left et right, la boucle, les comparaisons, la condition de direction et la valeur retournée.

Structure attendue en x64 :

Code: Select all

struct Node
{
    int value;      // +0x00
    padding         // +0x04
    Node* left;     // +0x08
    Node* right;    // +0x10
};
31. Ce qu’il faut retenir

Face à une fonction inconnue, le but est de transformer progressivement :

Code: Select all

mov eax, [rcx+14h]
en :

Code: Select all

table->Count
puis :

Code: Select all

call qword ptr [rcx+20h]
en :

Code: Select all

table->CompareCallback(...)
puis :

Code: Select all

cmp eax, ebx
jl left
jg right
en une logique C/C++ compréhensible.

Le reverse engineering consiste surtout à reconnaître des motifs, construire des hypothèses, puis les confirmer avec d’autres fonctions et avec l’exécution réelle du programme.

Who is online

Users browsing this forum: No registered users and 1 guest