# Shellcode x86 sur Windows

description : Article sur l'écriture de shellcode x86 sur Windows
categories : Coding; Hacking;
tags : Shellcode; Windows;

Écriture de shellcode x86 sur Windows

Dans cet article, nous allons voir comment écrire un shellcode x86 sur Windows.

Introduction

Un shellcode est un code arbitraire. Il est injecté dans la mémoire comme par exemple dans le cas d'un buffer overflow. Ils sont appelés shellcodes car ce type de code est souvent utilisé pour obtenir un shell ou une invite de commande dans le but de prendre le contrôle du pc vulnérable.

Les shellcodes sont constitués d'une suite d'instruction assembleur et peuvent donc faire tout ce que nous souhaitons.

L'écriture d'un shellcode Windows nécessite de connaître les API Windows, de savoir dans quelle DLL une fonction que nous souhaitons utiliser ce trouve, de charger cette DLL, puis d'appeler cette fonction par son adresse.

Généralement, pour qu'un shellcode soit entièrement copié dans la mémoire, comme par exemple dans le cas d'un Buffer Overflow, il ne doit absolument pas contenir de null byte, c'est à dire le caractère '\0', celui terminant une string en langage C.

Shellcode statique

Dans un premier temps, nous allons créer un shellcode statique, c'est à dire que ce shellcode ne fonctionnera plus lorsque vous aurez redémarré votre Windows. Ceci à cause de l'ASLR (Address Space Layout Randomization) introduit sur les OS de Microsoft depuis la sortie de Vista.

Notre shellcode sera tout simple, il affichera une MessageBox avec un titre et un message, puis quittera.

Nous avons donc besoin de connaître les adresses effective des fonctions suivantes:

  • LoadLibraryA qui se trouve dans kernel32.dll pour charger user32.dll
  • MessageBoxA contenue dans user32.dll pour afficher la boîte de dialogue
  • ExitProcess résidant dans kernel32.dll pour quitter proprement

Voici les prototypes de nos fonctions:

1
2
3
HMODULE WINAPI LoadLibraryA(
    __in LPCTSTR lpFileName
);
1
2
3
4
5
6
int WINAPI MessageBoxA(
    __in_opt HWND hWnd,
    __in_opt LPCTSTR lpText,
    __in_opt LPCTSTR lpCaption,
    __in UINT uType
);
1
2
3
VOID WINAPI ExitProcess(
    __in UINT uExitCode
);

Pour trouver ces adresses, nous allons utiliser les fonctions LoadLibrary() et GetProcAddress() réunis dans ce petit outil que j'ai appelé GetAddress et dont voici la source :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
    HMODULE handle;
    FARPROC address;

    if (argc != 3) {
        fprintf(stderr, "%s <dll> <function>\n", argv[0]);
        return 1;
    }

    handle = LoadLibrary(argv[1]);

    if (handle == NULL) {
        fprintf(stderr, "Could not load %s\n", argv[1]);
        return 1;
    }

    address = GetProcAddress(handle, argv[2]);

    if (address == NULL) {
        fprintf(stderr, "Could not find the address of %s\n", argv[2]);
        return 1;
    }

    printf("The address of %s in %s is: 0x%x\n", argv[2], argv[1], (unsigned int)address);

    return 0;
}

Nous obtenons l'adresse 0x76F74BC6 pour LoadLibraryA, 0x7782FEAE pour MessageBoxA et 0x76F7734E pour ExitProcess.

En ce qui concerne les chaînes de caractères, nous utiliserons une technique se servant des instructions assembleur jmp, call et pop pour obtenir un pointeur vers chacune de nos chaînes de caractères.

Voici un exemple de cette technique :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.386
.model flat, stdcall
option casemap:none
.code

start:

    jmp getString

    getStringReturn:
        pop ecx

    getString:
        call getStringReturn
        db "hello world",0

end start

Avec cet exemple, le registre ecx contient un pointeur vers la string "hello world".

Mais il y a un problème : le null byte utilisé pour terminer la chaîne 'h' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' '\0'. Pour y remédier, nous allons utiliser un autre registre, le mettre à 0 avec l'instruction XOR, ajouter un caractère superflu à la fin de notre string, puis écraser ce caractère par le byte de poids faible de ce registre.

Avec cette technique, notre shellcode sera sans null byte :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
section .text

global _start

_start:

    xor ebx,ebx     ; on met ebx à 0, donc bl aussi

    jmp getString   ; on jump sur getString

    getStringReturn:
        pop ecx         ; on pop dans ecx le haut de la stack,
                        ; soit le ptr sur notre string "hello world#"
        mov [ecx+11],bl ; on écrase le '#' par bl, donc 0

    getString:
        ; le call va pusher sur la stack l'adresse de l'instruction qui suit,
        ; soit l'adresse de notre string "hello world#"
        call getStringReturn
        db "hello world#"

Voici donc maintenant notre shellcode au complet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
.386
.model flat, stdcall
option casemap:none
.code

start:

    xor ebx,ebx ; Le registre ebx n'est jamais modifié.

    ; LoadLibraryA
    jmp getUser32LibString

    getUser32LibStringReturn:
        pop ecx
        mov [ecx+10],bl
        mov eax,76F74BC6h
        push ecx
        call eax

    ; MessageBoxA
    jmp getCaptionString

    getCaptionStringReturn:
        pop ecx
        mov [ecx+9],bl

    jmp getTextString

    getTextStringReturn:
        pop edx
        mov [edx+11],bl
        mov eax,7782FEAEh
        push ebx
        push ecx
        push edx
        push ebx
        call eax

    ; ExitProcess
    mov eax,76F7734Eh
    push ebx
    call eax

    getTextString:
        call getTextStringReturn
        db "hello world#"

    getCaptionString:
        call getCaptionStringReturn
        db "Shellcode#"

    getUser32LibString:
        call getUser32LibStringReturn
        db "user32.dll#"

end start

Pour essayer ce shellcode, il nous faut passer par un programme intermédiaire. Voici le programme que j'utilise, je l'ai appelé shellcodetest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <windows.h>

int main(void)
{
    char shellcode[] = "- Placez votre shellcode ici -";

    printf("Shellcode length: %u\n\n", strlen(shellcode));

    void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, shellcode, sizeof shellcode);
    ((void(*)())exec)();

    return 0;
}

Pour obtenir les bytes qui vont former le shellcode depuis l'exe généré par l'assembleur masm et ainsi le placer dans le buffer, nous utiliserons un outil appelé GetShellcode dont voici la source :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#include <windows.h>
#include <stdio.h>
#include <windowsx.h>

typedef FILE *PFILE;

#define APPLICATIONNAME "Get Shellcode\0"
#define CLASSNAME       "GetShellcode\0"

#define IDC_FILENAME_EDIT   101
#define IDC_LOAD_BUTTON     102
#define IDC_SHELLCODE_EDIT  103

#define IDA_SHELLCODE_EDIT 201

HINSTANCE hInst;
HWND hWnd;

ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
void                ResizeControls(HWND, HWND, HWND, HWND);
void                OnButtonClick(HWND, HWND, HWND);
void                GetShellcode(HWND, HWND, LPSTR);
void                DumpTextSegment(PFILE, IMAGE_SECTION_HEADER, HWND, HWND);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
    ACCEL accel;
    HACCEL haccel;

    MyRegisterClass(hInstance);

    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }

    accel.fVirt = FCONTROL | FVIRTKEY;
    accel.key = 0x41;
    accel.cmd = IDA_SHELLCODE_EDIT;

    haccel = CreateAcceleratorTable(&accel, 1);

    if (haccel == NULL)
    {
        return FALSE;
    }

    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(hWnd, haccel, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int)msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = CLASSNAME;
    wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    return RegisterClassEx(&wcex);
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // Stocke le handle d'instance dans la variable globale.

    hWnd = CreateWindow(CLASSNAME, APPLICATIONNAME, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, 800, 500, NULL, NULL, hInstance, NULL);

    if (!hWnd)
    {
        return FALSE;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId, wmEvent;
    PAINTSTRUCT ps;
    HDC hdc;

    static HWND hwndFilenameEdit;
    static HWND hwndLoadButton;
    static HWND hwndShellcodeEdit;

    switch (message)
    {
    case WM_CREATE:
        hwndFilenameEdit = CreateWindow("EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY | ES_LEFT,
            0, 0, 0, 0, hWnd, (HMENU)IDC_FILENAME_EDIT, hInst, NULL);
        hwndLoadButton = CreateWindow("BUTTON", "Load...", WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
            0, 0, 0, 0, hWnd, (HMENU)IDC_LOAD_BUTTON, hInst, NULL);
        hwndShellcodeEdit = CreateWindow("EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | ES_READONLY | ES_LEFT | ES_MULTILINE | ES_NOHIDESEL,
            0, 0, 0, 0, hWnd, (HMENU)IDC_SHELLCODE_EDIT, hInst, NULL);
        ResizeControls(hwndFilenameEdit, hwndLoadButton, hwndShellcodeEdit, hWnd);
        break;
    case WM_SIZE:
        ResizeControls(hwndFilenameEdit, hwndLoadButton, hwndShellcodeEdit, hWnd);
        break;
    case WM_COMMAND:
        wmId = LOWORD(wParam);
        wmEvent = HIWORD(wParam);
        // Analyse les sélections de menu :
        switch (wmId)
        {
        case IDC_LOAD_BUTTON:
            OnButtonClick(hwndFilenameEdit, hwndShellcodeEdit, hWnd);
            break;
        case IDA_SHELLCODE_EDIT:
            Edit_SetSel(hwndShellcodeEdit, 0, Edit_GetTextLength(hwndShellcodeEdit));
            SetFocus(hwndShellcodeEdit);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        break;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        // TODO : ajoutez ici le code de dessin...
        EndPaint(hWnd, &ps);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }

    return 0;
}

void ResizeControls(HWND hwndFilenameEdit, HWND hwndLoadButton, HWND hwndShellcodeEdit, HWND hWnd)
{
    RECT rcClient;

    GetClientRect(hWnd, &rcClient);

    MoveWindow(hwndFilenameEdit, 15, 15, rcClient.right - rcClient.left - (15 + 80 + 15 + 15), 25, TRUE);

    MoveWindow(hwndLoadButton, rcClient.right - (15 + 80), 15, 80, 25, TRUE);

    MoveWindow(hwndShellcodeEdit, 15, 15 + 25 + 15, rcClient.right - rcClient.left - (15 + 15),
        rcClient.bottom - rcClient.top - (15 + 25 + 15 + 15), TRUE);
}

void OnButtonClick(HWND hwndFilenameEdit, HWND hwndShellcodeEdit, HWND hWnd)
{
    OPENFILENAME ofn;
    TCHAR szFile[1024];

    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hWnd;
    ofn.lpstrFile = szFile;
    ofn.lpstrFile[0] = '\0';
    ofn.nMaxFile = sizeof(szFile);
    ofn.lpstrFilter = TEXT("Executable Files\0*.exe\0\0");
    ofn.nFilterIndex = 1;
    ofn.lpstrFileTitle = NULL;
    ofn.nMaxFileTitle = 0;
    ofn.lpstrInitialDir = NULL;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

    if (GetOpenFileName(&ofn))
    {
        Edit_SetText(hwndFilenameEdit, ofn.lpstrFile);
        return GetShellcode(hWnd, hwndShellcodeEdit, ofn.lpstrFile);
    }
}

void GetShellcode(HWND hWnd, HWND hwndShellcodeEdit, LPSTR lpstrFile)
{
    PFILE pfile = NULL;
    IMAGE_DOS_HEADER iDosHeader;
    IMAGE_NT_HEADERS iNtHeaders;
    IMAGE_SECTION_HEADER iSectionHeader;

    pfile = fopen(lpstrFile, "rb");

    if (pfile == NULL)
    {
        MessageBox(hWnd, "Impossible d'ouvrir le fichier.", "Erreur", MB_OK | MB_ICONERROR);
        return;
    }

    fread(&iDosHeader, sizeof(IMAGE_DOS_HEADER), 1, pfile);

    fseek(pfile, iDosHeader.e_lfanew, SEEK_SET);

    fread(&iNtHeaders, sizeof(IMAGE_NT_HEADERS), 1, pfile);

    for (WORD w = 0; w < iNtHeaders.FileHeader.NumberOfSections; w++)
    {
        fread(&iSectionHeader, sizeof(IMAGE_SECTION_HEADER), 1, pfile);

        if (!strcmp((char*)iSectionHeader.Name, ".text"))
        {
            return DumpTextSegment(pfile, iSectionHeader, hwndShellcodeEdit, hWnd);
        }
    }

    MessageBox(hWnd, "Impossible de trouver le segment text.", "Erreur", MB_OK | MB_ICONERROR);

    return;
}

void DumpTextSegment(PFILE pfile, IMAGE_SECTION_HEADER iSectionHeader, HWND hwndShellcodeEdit, HWND hWnd)
{
    BYTE by = 0;
    int nLength = 0;
    char szText[5];

    Edit_SetText(hwndShellcodeEdit, "");

    fseek(pfile, iSectionHeader.PointerToRawData, SEEK_SET);

    for (DWORD dw = 0; dw < iSectionHeader.Misc.VirtualSize; dw++)
    {
        fread(&by, sizeof(BYTE), 1, pfile);
        sprintf(szText, "\\x%.2X", by);

        nLength = Edit_GetTextLength(hwndShellcodeEdit);
        Edit_SetSel(hwndShellcodeEdit, nLength, nLength);
        Edit_ReplaceSel(hwndShellcodeEdit, szText);
    }

    fclose(pfile);

    Edit_SetSel(hwndShellcodeEdit, 0, Edit_GetTextLength(hwndShellcodeEdit));

    SetFocus(hwndShellcodeEdit);
}

Après compilation et exécution de shellcodetest nous obtenons :

Comme expliqué plus haut, après un reboot de votre PC, ce shellcode ne fonctionnera plus. L'adresse des fonctions écrites en dur ici auront changées. Il nous faut donc une autre approche.

Cette nouvelle approche est appelé "shellcode générique" et c'est ce que nous allons voir maintenant.

Shellcode générique

Au lieu d'utiliser un programme externe comme GetAddress pour obtenir les adresses des fonctions que nous souhaitons utiliser et de se contenter de copier/coller ces adresses dans le code assembleur. Nous les chercherons directement dans le shellcode.

Cette approche nécessite d'abord de connaître l'adresse de base de kernel32.dll. Il existe plusieurs technique pour trouver cette adresse dont une qui utilise la structure PEB.

Nous trouvons le pointeur sur ce PEB à fs:[0x30].

Une fois dans cette structure à 0x0C, nous tombons sur une autre structure nommée PEB_LDR_DATA.

Dans cette dernière, à 0x1C, il y a une liste chaînée qui contient les modules chargés en mémoire.

Cette liste chaînée porte le nom de InitializationOrderModule.

Selon la version de Windows, kernel32.dll est chargée en deuxième ou troisième position.

Une façon de rendre cette technique portable sur toutes les versions de Windows, est de regarder à la fin de chaque nom de module qui l'on parcourt, nom que l'on trouve à l'offset 0x20 sur chaque élément de InitializationOrderModule si le nom de la dll ou du module comporte les 12 caractères de "kernel32.dll".

Puis, enfin, à 0x08 sur chaque élément, nous trouvons l'adresse de base du module.

Voici le code assembleur qui nous permet de trouver l'adresse de kernel32.dll

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
.386
.model flat, stdcall
option casemap:none
assume fs:nothing
.code

start:

    xor ecx, ecx            ; ecx = 0
    mov esi, fs:[ecx + 30h] ; esi = &(PEB) (FS:[0x30])
    mov esi, [esi + 0ch]    ; esi = PEB->Ldr
    mov esi, [esi + 1ch]    ; esi = PEB->Ldr.InInitOrder

    next_module:
        mov eax, [esi + 08h]    ; eax = InInitOrder[X].base_address
        mov edi, [esi + 20h]    ; edi = InInitOrder[X].module_name (unicode)
        mov esi, [esi]          ; esi = InInitOrder[X].flink (module suivant)
        cmp [edi + 12 * 2], cl  ; module_name[12] == 0 ?
        jne next_module         ; Non : essayons le module suivant.

end start

Avec ce code, le registre EAX contient l'adresse de base du module kernel32.dll.

Il ne nous reste donc plus qu'à obtenir l'adresse de nos fonctions.

Nous allons refaire le même shellcode que dans l'exemple précédent, c'est à dire l'exemple du shellcode statique.

Nous allons y aller en deux fois :

  • Nous allons chercher les adresses de la fonction LoadLibraryA et de la fonction ExitProcess qui se trouvent toutes les deux dans kernel32.dll
  • Puis, nous chercherons l'adresse de la fonction MessageBoxA qui se trouve quant à elle dans user32.dll

Pour trouver les adresses de nos fonctions, nous allons utiliser le format PE et analyser les dll afin d'obtenir les adresses dans la table d'exportation.

Sachant que dans un shellcode nous ne pouvons pas lire toutes les en-têtes, ou utiliser fread, fseek, etc. Il nous faut donc connaître à l'avance les décalages ou offsets de tout ce que l'on a besoin:

  • PIMAGE_DOS_HEADER->e_lfanew = 0x3c
  • PIMAGE_NT_HEADERS->OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]->VirtualAddres = 0x78
  • PIMAGE_EXPORT_DIRECTORY->NumberOfNames = 0x18
  • PIMAGE_EXPORT_DIRECTORY->AddressOfNames = 0x20
  • PIMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals = 0x24
  • PIMAGE_EXPORT_DIRECTORY->AddressOfFunctions = 0x1c

Autre astuce, plutôt que de comparer dans le shellcode chaque nom de fonctions listé depuis la table d'exportation d'une dll avec un nom que nous recherchons, nous allons utiliser un système de hash.

C'est à dire, au départ, créer un hash pour chaque nom de fonction que nous souhaitons utiliser. Puis dans le shellcode à chaque nom de fonction parcouru nous générerons à nouveau ce hash pour le comparer avec celui que nous recherchons. Cette technique permet de diminuer la taille de notre shellcode.

Voici le code permettant de générer le hash d'une fonction:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    char *funcname = NULL;
    DWORD res = 0;

    if(argc != 2)
    {
        printf("USAGE: GenerateHash.exe <funcname>\n");
        printf("EXEMPLE: GenerateHash.exe MessageBoxA\n\n");
        system("PAUSE");
        exit(1);
    }

    else funcname = argv[1];

    __asm
    {
        prepare_hash:
        mov esi, funcname   // On fait pointer esi sur funcname.
        xor edi, edi        // On met edi à zéro.
        xor eax, eax        // On met eax à zéro.
        cld                 // Clear direction flag : pour être sûr que ça incrémente (de gauche à droite) pendant l'utilisation de lodsb.

        hash:
        lodsb               // Charge un byte de esi dans al et incrémente esi.
        test al, al         // On regarde si le byte est à zéro.

        jz hash_finished    // Si oui on a atteint la fin de la string (funcname).
        ror edi, 0xd        // Rotation de 13 bits vers la droite de la valeur courante.
        add edi, eax        // Ajout du caractère au hash.

        jmp hash            // On continue.

        hash_finished:      // On a finit.
        mov res, edi        // On met le hash dans la variable res.
    }

    printf("\n%s = %.8x\n", funcname, res);

    return 0;
}

Nous savons donc comment trouver l'adresse de kernel32.dll. Nous savons aussi qu'il faut parcourir la table d'exportation, et surtout comment la parcourir. Nous savons également qu'il faut générer un hash pour la comparaison des noms de fonctions.

On a donc tout ce qu'il nous faut pour rendre notre shellcode générique.

Voici le shellcode qui affiche la MessageBox mais de façon générique :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
.386
.model flat, stdcall
option casemap:none
assume fs:nothing
.code

start:

    jmp main

    find_kernel32:
        xor ecx, ecx            ; ecx = 0
        mov esi, fs:[ecx + 30h] ; esi = &(PEB) (FS:[0x30])
        mov esi, [esi + 0ch]    ; esi = PEB->Ldr
        mov esi, [esi + 1ch]    ; esi = PEB->Ldr.InInitOrder

    next_module:
        mov eax, [esi + 08h]    ; eax = InInitOrder[X].base_address
        mov edi, [esi + 20h]    ; edi = InInitOrder[X].module_name (unicode).
        mov esi, [esi]          ; esi = InInitOrder[X].flink (module suivant).
        cmp [edi + 12 * 2], cl  ; module_name[12] == 0 ?
        jne next_module         ; Non : essayons le module suivant.
        ret

    find_func_address:
        pushad
        mov ebp, [esp + 024h]       ; 24 = tous les registres push par le pushad (0x20) + l'adresse de base du module empilé avant l'appel de cette routine.
        mov eax, [ebp + 03ch]       ; PIMAGE_DOS_HEADER->e_lfanew
        mov edx, [ebp + eax + 078h] ; RVA de PIMAGE_NT_HEADERS->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]->VirtualAddres
        add edx, ebp                ; On y ajoute l'adresse de base de la dll.
        mov ecx, [edx + 018h]       ; PIMAGE_EXPORT_DIRECTORY->NumberOfNames
        mov ebx, [edx + 020h]       ; RVA de PIMAGE_EXPORT_DIRECTORY->AddressOfNames
        add ebx, ebp                ; On y joute l'adresse de base de la dll.

    find_func_address_loop:
        jecxz find_func_address_finished
        dec ecx                     ; Décrémente ecx.
        mov esi, [ebx + ecx * 4]    ; RVA d'un nom de fonction dans esi.
        add esi, ebp                ; On y ajoute l'adresse de base de la dll.

    prepare_hash:
        xor edi, edi    ; edi = 0
        xor eax, eax    ; eax = 0
        cld             ; Clear direction flag: pour être sûr que ça incrémente (de gauche à droite) pendant l'utilisation de lodsb.

    hash:
        lodsb               ; Charge un byte de esi (qui contient le nom d'une fonction) dans al et incrémente esi.
        test al, al         ; On regarde si le byte est à zéro.
        jz hash_finished    ; Si oui on a atteint la fin du nom de la fonction.
        ror edi, 0dh        ; Rotation de 13 bits vers la droite de la valeur courante (edi contient le hash).
        add edi, eax        ; Ajout du caractère au hash.
        jmp hash            ; On continue.

    hash_finished:
        compare_hash:
        cmp edi, [esp + 028h]       ; 28 = tous les registres push par le pushad (0x20) + l'adresse de base du module empilé avant l'appel de cette routine + le hash à trouver.
        jnz find_func_address_loop  ; Ce n'est pas le bon nom de fonction, on va au prochain.
        mov ebx, [edx + 024h]       ; RVA de PIMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals
        add ebx, ebp                ; On y ajoute l'adresse de base de la dll.
        mov cx, [ebx + 2 * ecx]     ; Ordinal de la fonction courante.
        mov ebx, [edx + 01ch]       ; RVA de PIMAGE_EXPORT_DIRECTORY->AddressOfFunctions
        add ebx, ebp                ; On y ajoute l'adresse de base de la dll.
        mov eax, [ebx + 4 * ecx]    ; RVA de l'adresse de la fonction.
        add eax, ebp                ; On y ajoute l'adresse de base de la dll = adresse effective.
        mov [esp + 01ch], eax

    find_func_address_finished:
        popad   ; Retrouve la valeur de tous les registres, eax contient l'adresse de la fonction grace au "mov [esp + 01ch], eax".
        ret

    main:
        sub esp, 12     ; On alloue l'espace sur la stack pour contenir l'adresse de LoadLibraryA, ExitProcess et MessageBoxA.
        mov ebp, esp    ; ebp devient notre frame pointeur. Ex :
                        ; call ebp+4 pour call LoadLibraryA.
                        ; call ebp+8 pour call ExitProcess.
                        ; call ebp+12 pour call MessageBoxA.

        call find_kernel32
        mov edx, eax ; On sauvegarde l'adresse de kernel32.dll dans edx.

        ; On cherche l'adresse de LoadLibraryA :
        push 0ec0e4e8eh     ; Le hash.
        push edx            ; L'adresse de base de la dll (kernel32.dll).
        call find_func_address
        mov [ebp+4], eax    ; ebp = Adresse de LoadLibraryA.

        ; On cherche l'adresse de ExitProcess :
        push 073e2d87eh     ; Le hash.
        push edx            ; L'adresse de base de la dll (kernel32.dll).
        call find_func_address
        mov [ebp+8], eax    ; ebp+4 = Adresse de ExitProcess.

        ; On get la string user32.dll :
        xor ebx, ebx
        jmp get_user32

        get_user32_return:
        pop eax
        mov [eax+10], bl    ; On termine la string sans null byte.

        ; On appel LoadLibraryA.
        push eax    ; la string user32.dll
        call dword ptr [ebp+4]
        mov edx, eax    ; edx contient maintenant l'adresse de base de user32.dll.

        ; On cherche l'adresse de MessageBoxA :
        push 0bc4da2a8h     ; Le hash.
        push edx            ; L'adresse base de la dll (user32.dll).
        call find_func_address
        mov [ebp+12], eax   ; ebp = adresse de MessageBoxA.

        xor ebx,ebx ; Le registre ebx n'est jamais modifié (convention d'appel stdcall).

        ; On get la string pour le titre :
        jmp get_caption
        get_caption_return:
        pop esi
        mov [esi+9], bl

        ; On get la string pour le message :
        jmp get_text
        get_text_return:
        pop edi
        mov [edi+11], bl

        ; On call MessageBoxA :
        push ebx
        push esi
        push edi
        push ebx
        call dword ptr [ebp + 12]

        ; On call ExitProcess :
        push ebx
        call dword ptr [ebp + 8]

        get_user32:
        call get_user32_return
        db "user32.dll#"

        get_caption:
        call get_caption_return
        db "Shellcode#"

        get_text:
        call get_text_return
        db "hello world#"

end start

En utilisant GetShellcode.exe pour obtenir les bytes, nous obtenons :

Conclusion

Nous avons donc d'abord créé un shellcode affichant une MessageBox qui fonctionnera uniquement sur notre pc et sans reboot, pour finir par un autre shellcode qui fonctionnera normalement en tout temps et sur n'importe quel Windows.