Archive for mai 26th, 2010

Win7 and CreateRemoteThread

Depuis Vista et 7 le fonctionnement de l’API CreateRemoteThread à quelque peu changé. En fait il n’est plus possible d’aller injecter des threads dans des processus qui ne nous appartiennent pas ou pour être plus précis ceux qui sont dans d’autres sessions. Cela pose problème lorsqu’on veut jouer avec l’OS pour par exemple injecter des DLLs dans les processus d’autres utilisateurs. Après avoir regardé de plus près ce qui ne marche pas, je vous propose une petite solution simple mais efficace pour contourner cette restriction.

On se place dans le contexte d’un user loggé sous 7 appartenant au groupe ‘Administrateurs’ avec niveau d’UAC par défaut.

Avant de pouvoir faire un CreateRemoteThread sur le processus d’un autre utilisateur il faut pouvoir ouvrir un handle sur le process visé avec OpenProcess vu qu’on appartient au groupe Administrateurs on peut activer le SeDebugPrivilege et lancer notre process avec le token Administrateur qu’on a obtenu lorsqu’on s’est loggé à l’aide de la commande runas. Par défaut avec l’UAC même lorsqu’on se logge en admin on ne travaille pas directement avec le vrai token administrateur mais avec un token utilisateur. Lorsqu’une action administrative est nécessaire, l’UAC (si activé) prompt un avertissement pour lancer l’application avec le token administrateur.

La doc de CreateRemoteThread spécifie que :

‘Terminal Services isolates each terminal session by design. Therefore, CreateRemoteThread fails if the target process is in a different session than the calling process.’

Au moins ce comportement est documenté :]

Lorsqu’on essaye d’appeler CreateRemoteThread avec un handle sur le process d’un autre utilisateur la fonction nous renvoie NULL et GetLastError donne 8 (ERROR_NOT_ENOUGH_MEMORY). Déjà on se dit WTF?!. Traçons la fonction pour voir d’ou provient l’erreur. Avant de plonger plus loin on n’oublie pas que depuis 7 les fonctions de kernel32.dll ont été découpées dans plusieurs DLLs virtuelles. Même si la table des imports de kernel32.dll indique que CreateRemoteThread se situe dans API-MS-Win-Core-ProcessThreads-L1-1-0.dll on arrive au dans final kernelbase.dll et c’est pareil pour le reste des fonctions de kernel32.dll.

Au final on tombe dans kernelbase!CreateRemoteThreadEx. En traçant cette fonction l’erreur ne provient pas du syscall NtCreateThreadEx mais de CsrClientCallServer qui renvoie 0xC0000001 (STATUS_UNSUCCESSFUL). Plus loin CreateRemoteThreadEx transforme cette erreur en 0xC00000017 (STATUS_NO_MEMORY) et BaseSetLastNTError met à jour le GetLastError à 8. La bonne nouvelle c’est que ce n’est pas le syscall en lui même qui échoue mais un appel au subsystem csrss qui fait tout foirer. En fait le thread a été créé avec le CREATE_SUSPENDED, il attend donc un appel à ResumeThread ou (ZwResumeThread) pour être lancé. Il y effectivement cet appel dans CreateRemoteThreadEx mais il ne s’exécute qu’en cas de réussite de CsrClientCallServer.

Petit description du process csrss.exe sous 7 (from Windows Internals 5th) :

Session space contains information global to each session.A session consists of the processes and other system objects (such as the window station, desktops, and windows) that represent a single user’s logon session. Each session has a session-specific paged pool area used by the kernel-mode portion of the Windows subsystem (Win32k.sys) to allocate session-private GUI data structures. In addition, each session has its own copy of the Windows subsystem process (Csrss.exe) and logon process (Winlogon.exe). The session manager process (Smss.exe) is responsible for creating new sessions, which includes loading a session-private copy of Win32k.sys, creating the sessionprivate object manager namespace, and creating the session-specific instances of the Csrss and Winlogon processes.

En fait chaque utilisateur a donc son process csrss.exe. Voici le prototype de CsrClientCallServer et le code d’appel dans CreateRemoteThreadEx :

NTSTATUS
NTAPI
CsrClientCallServer(
    struct _CSR_API_MESSAGE *Request,
    struct _CSR_CAPTURE_BUFFER *CaptureBuffer OPTIONAL,
    ULONG ApiNumber,
    ULONG RequestLength
);


kernelbase.dll
7597BD24    6A 0C           PUSH 0C
7597BD26    68 01000100     PUSH 10001
7597BD2B    53              PUSH EBX
7597BD2C    8D85 F0FDFFFF   LEA EAX, DWORD PTR SS:[EBP-210]
7597BD32    50              PUSH EAX
7597BD33    FF15 00129775   CALL NEAR DWORD PTR DS:[<&ntdll.CsrClientCallServer>]      ; ntdll.CsrClientCallServer
7597BD39    8B85 10FEFFFF   MOV EAX, DWORD PTR SS:[EBP-1F0]
7597BD3F    8985 E8FDFFFF   MOV DWORD PTR SS:[EBP-218], EAX
7597BD45    399D E8FDFFFF   CMP DWORD PTR SS:[EBP-218], EBX
7597BD4B    0F8C 13D80100   JL KERNELBA.75999564

On s’intéresse au paramètre ApiNumber qui est défini par la macro :

#define CSR_MAKE_API_NUMBER( DllIndex, ApiIndex ) \
    (CSR_API_NUMBER)(((DllIndex) << 16) | (ApiIndex))

Ici on a ApiIndex qui vaut 1 et DllIndex qui vaut 1. On sort les tables faites par j00ru et on voit que CsrClientCallServer va appeler
basesrv!BaseSrvCreateThread. Après debug on s'aperçoit que c'est csrsrv!CsrLockProcessByClientId qui pose problème. Hop on sort IDA et on analyse CsrLockProcessByClientId

unsigned int __stdcall CsrLockProcessByClientId(int a1, int ret)
{
  int v2; // ebx@1
  int v3; // edi@1
  int v4; // esi@1
  int v6; // edx@6
  unsigned int v7; // [sp+18h] [bp+Ch]@1

  v3 = ret;
  *(_DWORD *)ret = 0;
  v2 = CsrRootProcess + 8;
  v7 = 0xC0000001u;
  v4 = CsrRootProcess + 8;
  RtlEnterCriticalSection(&CsrProcessStructureLock);
  while ( *(_DWORD *)(v4 - 8 ) != a1 )
  {
    v4 = *(_DWORD *)v4;
    if ( v4 == v2 )
    {
      RtlLeaveCriticalSection(&CsrProcessStructureLock);
      return v7;
    }
  }
  v7 = 0;
  CsrLockedReferenceProcess(v4 - 8);
  *(_DWORD *)v3 = v6;
  return v7;
}

Directement en voyant CsrRootProcess je me suis rappelé le CsrWalker. Pour faire simple sous XP le process csrss maintient une liste de tous les processes et threads lancés sur le système. Ainsi si on extrait cette liste on peut s'en servir comme d'un anti-rootkit (ou pour mieux cacher un rootkit :]).

Sous 7 cela change. Ce n'est plus la liste de tous les process qu'on a dans csrss mais bien ceux qui sont dans la même session (comprendre même user) que ce processus. Ainsi CsrLockProcessByClientId est appelé par BaseSrvCreateThread pour justement mettre à jour la liste des threads qui appartiennent au process cible. Or vu que le process existe dans une autre session csrsrv!CsrLockProcessByClientId échoue et renvoie 0xC0000001 (STATUS_UNSUCCESSFUL) relayé par basesrv!BaseSrvCreateThread. Voila l'origine de la valeur de retour de CsrClientCallServer.

Pendant que j'y suis il est possible de dumper les CLIENT_ID des processes de la CsrRootProcess à l'aide du kernel debugger:

1: kd> p
eax=024df7a0 ebx=00000000 ecx=000003b0 edx=00000008 esi=024df7f0 edi=003f7ea0
eip=75884458 esp=024df770 ebp=024df7a4 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
basesrv!BaseSrvCreateThread+0x3f:
001b:75884458 ff1504108875    call    dword ptr [basesrv!_imp__CsrLockProcessByClientId (75881004)] ds:0023:75881004={CSRSRV!CsrLockProcessByClientId (75895ad5)}

1: kd> !list -x "dt nt!_CLIENT_ID @$extret-8 " poi(CsrRootProcess)+8 
   +0x000 UniqueProcess    : 0x000001a8 
   +0x004 UniqueThread     : 0x000001ac 

   +0x000 UniqueProcess    : 0x000001e4 
   +0x004 UniqueThread     : 0x000001e8 

   +0x000 UniqueProcess    : 0x000000f0 
   +0x004 UniqueThread     : 0x0000006c 

   +0x000 UniqueProcess    : 0x00000550 
   +0x004 UniqueThread     : 0x0000078c 

   +0x000 UniqueProcess    : 0x00000480 
   +0x004 UniqueThread     : 0x0000043c 

   +0x000 UniqueProcess    : 0x00000258 
   +0x004 UniqueThread     : 0x000004dc 

   +0x000 UniqueProcess    : 0x0000019c 
   +0x004 UniqueThread     : 0x00000388 

   +0x000 UniqueProcess    : 0x00000b20 
   +0x004 UniqueThread     : 0x00000b24 

   +0x000 UniqueProcess    : 0x00000b28 
   +0x004 UniqueThread     : 0x00000b2c 

   +0x000 UniqueProcess    : 0x00000d1c 
   +0x004 UniqueThread     : 0x00000d20 

   +0x000 UniqueProcess    : 0x000006d0 
   +0x004 UniqueThread     : 0x00000288 

   +0x000 UniqueProcess    : 0x00000120 
   +0x004 UniqueThread     : 0x00000428 

   +0x000 UniqueProcess    : 0x00000f5c 
   +0x004 UniqueThread     : 0x000001d8 

En fait rien ne nous oblige à notifier le subsystem lorsqu'on crée un nouveau thread. Par exemple un des rôles du process csrss est de killer tous les processes avant un shutdown, c'est pour cela qu'il maintient la liste CsrRootProcess de chaque user sous 7. Donc ne pas le prévenir qu'un nouveau thread a été crée ne pose pas vraiment de problème pour la stabilité du système. Justement on ne va pas le faire :]

Plusieurs solutions existent pour pouvoir créer un thread dans le process d'une autre session. La première et la plus bourrin serait de directement passer par l'API native ZwCreateThreadEx. Pourquoi pas mais cela demande de manipuler une API et des structures non documentées.

Une autre possibilité serait de passer par ntdll!RtlCreateUserThread comme montré ici. Pareil dans ce cas on manipule toujours des structures et API non documentées.

De mon coté j'ai choisi de faire plus simple. Comme CsrClientCallServer est importé par kernelbase.dll. Il suffit de hooker son IAT pour remplacer temporairement CsrClientCallServer par notre fonction qui renvoie toujours une valeur de succès. Au moins on peut se baser sur les headers du SDK pour le faire. J'ai envie de dire que c'est une solution comme une autre et qu'elle ne propose rien de vraiment innovant mais au moins on travaille avec des APIs documentées et donc fiables. La seule chose bancale c'est qu'il ne suffit pas de modifier la valeur de retour de CsrClientCallServer mais aussi le champ ReturnValue de la structure CSR_API_MESSAGE passée en argument. Pour ça on modifie directement en fonction du disass, c'est à dire qu'on met à jour la valeur pointée par ebp-0x1F0.

Le POC permet donc d'injecter une DLL dans n'importe quel processus sous 7. Tant qu'on l'exécute depuis un shell administrateur. La DLL qui est injectée fait appel à OutputDebugString donc le comportement a changé aussi sous 7. Donc mettez à jour la valeur de HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter.

Les codes et binaires ici :
http://ivanlef0u.fr/repo/CreateRemoteThread7.rar

Enjoy!

18 comments mai 26th, 2010


Calendar

mai 2010
L Ma Me J V S D
« avr   juil »
 12
3456789
10111213141516
17181920212223
24252627282930
31  

Posts by Month

Posts by Category