Win7 and CreateRemoteThread

mai 26th, 2010 at 08:12 admin

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!

Entry Filed under: RE

18 Comments

  • 1. Eloo  |  mai 26th, 2010 at 21:24

    Petite question : malgré l’ASLR, LoadLibrary a la même adresse dans le processus injecteur et dans le processus injecté ? Je vois que tu utilises cette hypothèse pour définir l’adresse de départ du thread créé.


  • 2. mxatone  |  mai 27th, 2010 at 09:27

    L’ASLR est defini au boot ce qui fait que ntdll et la plus part des dll systems auront la meme base addresse dans tous les process. (Le reste depend des dlls loader et leur ordre)

    Ca facilite la vie des devs (user ou kernel) et ca ne reduit que des tres peu la couverture de la protection car remotely tu ne connais pas la base addresse choisi au boot.

    Nice post Ivan :)


  • 3. admin  |  mai 27th, 2010 at 10:29

    Merci mx je n’aurais pas mieux répondu :]

    Pour savoir pourquoi les DLLs ne sont pas remappées aléatoirement à chaque nouveaux process je cite Windows Internals 5h:

    For DLLs, computing the load offset begins with a per-boot, systemwide value called the image bias, which is computed by MiInitializeRelocations and stored in MiImageBias. This value corresponds to the time stamp counter (TSC) of the current CPU when this function was called during the boot cycle, shifted and masked into an 8-bit value, which provides 256 possible values. Unlike executables, this value is computed only once per boot and shared across the system to allow DLLs to remain shared in physical memory and relocated only once. Otherwise, if every DLL was loaded at a different location inside different processes, each DLL would have a private copy loaded in physical memory.


  • 4. Geo  |  mai 27th, 2010 at 20:30

    Encore un bon article de notre Ivanlef0u national.

    Merci beaucoup ! En plus j’ai bien compris le code. Continue comme ça. :)

    Geo


  • 5. falken  |  mai 28th, 2010 at 02:36

    @Ivan : Ça s’appelle le prelinking sur Linux.

    Sinon article sympa :-)
    Bonne continuation


  • 6. nameless  |  mai 28th, 2010 at 15:42

    @Ivan : Tu dis : « En fait rien ne nous oblige à notifier le subsystem lorsqu’on crée un nouveau thread. »

    Ma question est : Si on ne notifie pas le subsystem lors de la création d’un thread, esque le thread


  • 7. nameless  |  mai 28th, 2010 at 15:45

    // Oups, j’ai appuyer sur entrer :(

    Ma question est : Si on ne notifie pas le subsystem lors de la création d’un thread, esque le thread peut être hidden avec certain tools ?

    Sympa ton article :)


  • 8. admin  |  mai 30th, 2010 at 11:35

    @geo Merci dude :]

    @nameless
    En fait si ce tool se base uniquement sur la liste CsrRootProces pour scanner les processes et threads (comme le csrwalker), dans ce cas oui tu pourras te cacher de cet outil.


  • 9. ninjax  |  juin 29th, 2010 at 21:45

    Ty pour cet article fort intéressant :)

    Btw ayant migré sous win7 j’ai casé windbg et les symbols, cependant pour ta commande :

    !list -x « dt nt!_CLIENT_ID @$extret-8  » poi(CsrRootProcess)+8

    Il me retourne :
    Invalid expression in poi(CsrRootProcess)+8 .

    WTF?! Problème de symboles? (j’ai choppé les retails Win7 RTM).


  • 10. admin  |  juin 30th, 2010 at 13:01

    Yo,
    Une fois que t’es dans le context de csrss, fais un ‘.reload’ ensuite ‘x /v csrss!*’ puis : ? csrss!CsrRootProcess si tout ca est bon, retente.


  • 11. bla-bla-bla-blaker  |  mars 14th, 2011 at 15:12

    Bonjour,

    Bon article comme toujours.

    Je sais bien que je suis en retard de plusieurs mois, que l’eau a coulé sous les ponts, mais c’est aujourd’hui que j’ai mon soucis.

    J’ai étudié le code, la doc msdn… mais je n’arrive pas à injecter mon bout de code sous Windows7 x64.

    Je compile évidement tout en 64bits.

    J’ai un code d’erreur retourné par VirtualProtect(…) – 487L (ERROR_INVALID_ADDRESS).

    Y’a-t-il d’autres gens confrontés au même soucis ? Ou alors ça vient de moi, j’ai loupé le « truc » du magicien….

    Cordialement.

    Blabla.


  • 12. admin  |  mars 14th, 2011 at 23:49

    Yo,
    @bla-bla-bla-blaker on voit ca par mail :)


  • 13. User-space API monitor, P&hellip  |  décembre 25th, 2012 at 23:57

    [...] http://www.codeproject.com/Articles/2082/API-hooking-revealedhttp://www.ivanlef0u.tuxfamily.org/?p=395http://0x191unauthorized.blogspot.it/2011/08/reverse-shell-through-dll-injection.html – [...]


  • 14. clique  |  avril 26th, 2013 at 06:31

    i can’t download your file, it need password what’s it????!!!


  • 15. mekhalleh  |  mai 21st, 2013 at 11:03

    Salut Ivan On a plus accès au repository ?
    J’ai testé mais il semble avoir un bug quand on liste les fonctions de ntdll.dll !

    :)


  • 16. BB  |  novembre 13th, 2013 at 21:01

    I’m getting the following error Unhandled exception at 0x000000013f2013cc in InjectDll7.exe: 0xC0000005: Access violation reading location 0x00000000fd5db1e8.

    The error is thrown on the line OriginalCsrClientCallServer = *(PDWORD)ImportAddress;

    I’m running Win7x64. Any ideas why it’s crashing there?


  • 17. BB  |  novembre 13th, 2013 at 21:06

    After a little more looking, I see I’m getting the same error as the previous poster. The failure is actually on the previous line of VirtualProtect. The GetLastError() shows 487…which is invalid address.


  • 18. BB  |  novembre 13th, 2013 at 21:16

    By the way…this is very nice. I’ve never seen this technique before. Great job!


Trackback this post


Calendar

mars 2024
L Ma Me J V S D
« fév    
 123
45678910
11121314151617
18192021222324
25262728293031

Most Recent Posts