PSReflect FTW16 minute(s) de lecture

Bien que les possibilités offertes par .NET sont extrêmement puissantes, il est parfois nécessaire d’avoir une interaction directe avec la WinAPI. En C#, le Pinvoke y est dédié. Dans cet article, nous verrons comment faire une implémentation discrète, stable et viable du Pinvoke. L’exemple principal de ce poste sera celui d’un outil que j’ai écrit, SweetBackup. Je dois vous avertir cependant qu’il ne s’agit pas d’un article aisé de compréhension, il vous faudra une bonne connaissance de PowerShell et de la réflexion.

PInvoke

Pour être tout à fait rigoureux, je devrais l’appeler P/Invoke mais je risque d’utiliser PInvoke. Le “platform Invoke” est une implémentation dans le framework .NET qui permet l’utilisation de code natif de dlls (aussi appelé code non managé). Autrement dit, l’accès à la WinAPI. En C#, on utilise l’espace de nom System.Runtime.InteropServices qui exporte un attribut [DllImport()]. Voici un exemple pour la fonction AdjustTokenPrivilege, en C#.

[DllImport("advapi32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(IntPtr TokenHandle,
   [MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges,
   ref TOKEN_PRIVILEGES NewState,
   UInt32 BufferLengthInBytes,
   ref TOKEN_PRIVILEGES PreviousState,
   out UInt32 ReturnLengthInBytes);

Ou encore possible en VB:

<DllImport("advapi32.dll", SetLastError:=True)> _
Private Function AdjustTokenPrivileges( _
    ByVal TokenHandle As IntPtr, _
    ByVal DisableAllPrivileges As Boolean, _
    ByRef NewState As TOKEN_PRIVILEGES, _
    ByVal Zero As Integer, _
    ByVal Null1 As IntPtr, _
    ByVal Null2 As IntPtr _
  ) As Boolean

Pinvoke est très pratique car il permet d’étendre les fonctionnalités de .NET, et puisqu’il est possible de l’utiliser en mémoire (sans toucher le disque) il devient d’autant plus puissant. En effet, tous les programmes qui nécessitaient d’être écrit en C++ ou en C peuvent désormais avoir une implémentation en code managé. En tant qu’attaquant, il y a donc tout intérêt à l’utiliser. Mais comment fonctionne ce fameux attribut ? Il va dans un premier temps chercher la dll, puis il va utiliser LoadLibrary() pour la charger en mémoire, et enfin utiliser la fonction GetProcAddress() pour charger la fonction. Bien que les dernières fonctions citées soient des fonctions natives, elles sont accessible via la dll mscoree.dll qui est la librairie où CLR est implémenté.

Équivalence de type

Avant de rentrer dans le cœur du sujet, il est nécessaire de parler objet. Les types qu’utilisent C# et PowerShell sont des objets, or ce n’est absolument pas le cas du C, où un type ne représente qu’un espace en mémoire. Il faut donc établir une équivalence entre les types .NET et les types non managés pour pouvoir appeler des fonctions natives. Petit point définition, on appelle “marshaling” l’opération de transformation d’un objet en un ensemble de type plus simple (on retrouve ainsi la définition de la sérialisation). Voici un tableau assez complet :

Nom dans la
documentation Microsoft
Nom du type en CNom du type en C#
VOIDvoidSystem.Void
HANDLEvoid *System.IntPtr
BYTEunsigned charSystem.Byte
SHORTshortSystem.Int16
WORDunsigned shortSystem.UInt16
INTintSystem.Int32
UINTunsigned intSystem.UInt32
LONGlongSystem.Int32
BOOLlongSystem.Bool
DWORDunsigned longSystem.UInt32
ULONGunsigned longSystem.UInt32
CHARcharSystem.Char
WCHARwchar_t System.Char
LPSTRchar *System.String
LPCSTRconst char * System.String
LPWSTRwchar_t *System.String
FLOATfloatSystem.Single
DOUBLEdoubleSystem.Double
LPCWSTRconst wchar_t *System.String

Les types System.String peuvent être remplacé par System.Text.StringBuilder
référence: https://docs.microsoft.com/fr-fr/dotnet/framework/interop/marshaling-data-with-platform-invoke

Si un pointeur est requis, il faudra simplement ajouter .MakeByRefType() après le type, et on utilisera [ref] devant la variable qui contient le contient. Lorsqu’il s’agira de structures ou d’énumérations, il sera alors nécessaire de les définir au préalable.

La manière la plus simple

Dans le but de faire du Pinvoke depuis PowerShell, le plus simple est d’ajouter du code .NET à la session actuelle. Vous vous rappelez peut-être alors que Add-Type, est une Cmdlet qui permet de répondre à ce besoin. L’avantage de cette méthode est sa simplicité. En effet, les signatures des fonctions à importer sont les mêmes que celles en C#. En cela, notre travail résidera seulement à aller sur pinvoke.net chercher notre fonction, et l’importer. Voici un exemple avec la MessageBox qui permet d’ouvrir une boite de dialogue contenant un message.

$Signature = @'
    [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "MessageBoxW", ExactSpelling = true)]
    public static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);
'@

Add-Type -MemberDefinition $Signature -Name User32 -Namespace Win32Functions

[Win32Functions.User32]::MessageBox([IntPtr]::Zero, "hello", "PInvoke", 0x40)

Cependant, Add-Type laisse des fichiers de compilation et appelle un processus non-trivial csc.exe qui n’est pas utilisé pour autre chose que cela. Pour importer son code, il va d’abord le compiler, et l’ajouter en utilisant System.Reflection. Ce qui, dans une perspective de discrétion, n’est clairement pas recommandable. Ainsi, on souhaite une meilleure méthode pour accéder à du code non managé. @Mattifestation nous a alors offert le fruit de son travail: PSReflect.

PSReflect

PSReflect est assurément la manière la plus élégante de faire du Pinvoke en PowerShell. Il se base principalement sur les pouvoirs de la réflexion puisqu’il va utiliser l’attribut [DllImport()]. L’idée générale de cette librairie est “d’émuler une syntaxe type C”. Ainsi beaucoup de fonctions ne sont pas des fonctions Cmdlets (aussi appelées fonctions avancées). Le module exporte les fonctions suivantes:

  • New-InMemoryModule qui permet de créer un nouvel espace de nom .NET ainsi qu’un module uniquement en mémoire.
  • func qui permet de faciliter la création d’objets utilisés par la fonction suivante.
  • Add-Win32Type qui permet de créer un type .NET représentant une fonction native. Ce dernier type devra être ajouté à un module pour pouvoir être utilisé.
  • psenum qui permet de créer une énumération à l’image des énumérations en C.
  • struct qui permet de créer des structures à l’image des structures en C.
  • field qui permet de simplifier la création de champ pour struct.

Dans la suite on explorera chacune de ces fonctions pour mieux les comprendre, et ainsi avoir une plus vision plus claire lors de leur utilisation. Mais dans un premier temps, essayons d’utiliser la réflexion pour manuellement charger une fonction. Nous prendrons par simplicité MessageBox qui ne nécessite point grand chose. On commence par créer un module en mémoire dynamique. L’idée étant de rendre accessible notre fonction grâce à ce module. Pour cela on créer un objet de classe System.Reflection.AssemblyName, que l’on passe à la méthode .DefineDynamicAssembly(). Puis on créer le module avec .DefineDynamicModule() auquel on vient finalement créer un une classe à l’aide de .DefineType().

$DynAssembly = New-Object System.Reflection.AssemblyName('Win32Lib')
$AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('Win32Lib', $False)
$TypeBuilder = $ModuleBuilder.DefineType('User32', 'Public, Class')

On ajoute alors une méthode à notre type, qui exhibera notre fonction. Pour cela, on se sert de notre variable $TypeBuilder et on utilise la méthode .DefineMethod(). Le premier paramètre sera le nom de la méthode, puis les attributs de cette méthode, qui ici seront Public et Static. On indique après le type de retour, et enfin un tableau [Type[]] qui donne les types des paramètres de notre méthode.

$PInvokeMethod = $TypeBuilder.DefineMethod(
	'MessageBox',
	[Reflection.MethodAttributes] 'Public, Static',
	[Int32],
	[Type[]] @([IntPtr], [String], [String], [Int32])
)

Nous devons ensuite créer un attribut [DllImport()] avec ses paramètres, exactement comme en C# (en version réflexion). On commence par créer un attribut à l’aide de son constructeur. Puis on charge les paramètres de cet attribut que l’on regroupe dans un tableau.

$DllImportConstructor = [System.Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$FieldArray = [System.Reflection.FieldInfo[]] @(
	[System.Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('SetLastError'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('CallingConvention'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('CharSet')
)

On créer un second tableau qui aura pour vocation de porter les valeurs aux paramètres importés.

$FieldValueArray = [Object[]] @(
	'MessageBoxW',
	$true,
	$true,
	[System.Runtime.InteropServices.CallingConvention]::Winapi,
	[System.Runtime.InteropServices.CharSet]::Unicode
)

Puis on créer un attribut personnalisé à l’aide de la classe System.Reflection.Emit.CustomAttributeBuilder

$SetLastErrorCustomAttribute = New-Object System.Reflection.Emit.CustomAttributeBuilder(
	$DLLImportConstructor,
	@('user32.dll'),
	$FieldArray,
	$FieldValueArray
)

Enfin on ajoute l’attribut à notre méthode et on créer le type pour que ce dernier soit accessible depuis PowerShell: $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute) puis $User32 = $TypeBuilder.CreateType(). Si vous voulez un exemple un peu plus complexe, vous pouvez consulter ceci.

On peut également créer des énumérations. En référence à mon article sur le contournement de sécurité PowerShell, nous construirons une énumération qui contient les codes de retours de la fonction AMSIScanContent(). Pour créer notre énumération, il faut, comme précédemment, créer un module en mémoire (ou la rajouter à un déjà existant) et utiliser la variable $ModuleBuilder, cette fois ci avec la méthode .DefineEnum(). Elle prend en argument le nom de l’énumération, puis les attributs de visibilité et enfin le type des éléments de l’énumération. Pour ajouter une entrée à une énumération, on utilise la méthode .DefineLiteral() qui prend en argument le nom du champ, puis sa valeur (attention, la méthode à un objet de retour, pour le masquer il faut le rediriger vers $null ou [void]). On a donc:

$TypeBuilder = $ModuleBuilder.DefineEnum(AMSI_RESULT, 'Public', [UInt32])
$null = $TypeBuilder.DefineLiteral('AMSI_RESULT_CLEAN', [UInt32]0)
$null = $TypeBuilder.DefineLiteral('AMSI_RESULT_NOT_DETECTED', [UInt32]1)
$null = $TypeBuilder.DefineLiteral('AMSI_RESULT_BLOCKED_BY_ADMIN_START', [UInt32]16384)
$null = $TypeBuilder.DefineLiteral('AMSI_RESULT_BLOCKED_BY_ADMIN_END', [UInt32]20479)
$null = $TypeBuilder.DefineLiteral('AMSI_RESULT_DETECTED', [UInt32]32768)

On finit simplement par créer le type: $AMSI_RESULT = $TypeBuilder.CreateType().

On peut enfin créer des structures. En référence à mon article sur l’abus d’ACL en Active Directory, nous allons construire la structure du header d’une ACL. Pour créer notre structure, il faut comme précédemment, créer un module en mémoire (ou la rajouter à un déjà existant) et utiliser la variable $ModuleBuilder, mais avec la méthode .DefineType() comme pour les fonctions. Il faut cependant ajouter plus d’attributs. Certains sont constants $StructAttributes = [System.Reflection.TypeAttributes] 'Class, Public, Sealed, BeforeFieldInit' d’autres non et sont en fonction des besoins (ils doivent être rajoutés). Le type de disposition qui peut être [System.Reflection.TypeAttribute]::SequencialLayout ou bien [System.Reflection.TypeAttribute]::ExplicitLayout. Le premier doit être utilisé dans les structures où il n’y a pas d’union présent, le second le cas contraire. Le charset, qui peut prendre les valeurs suivantes [System.Reflection.TypeAttribute]::AnsiClass/AutoClass/UnicodeClass. Pour ajouter des attributs à notre variable, il faudra utiliser l’opérateur -bor de la manière suivante $StructAttributes = $StructAttributes -bor [System.Reflection.TypeAttributes]::attribut. Le type spécifié devra être System.ValueType. Pour ajouter un champ à notre structure, il faudra alors utiliser la méthode .DefineField() qui prend en argument le nom du champ, son type et enfin son attribut (attention, la méthode à un objet de retour, pour le masquer il faut le rediriger vers $null ou [void]). On a donc:

$TypeBuilder = $ModuleBuilder.DefineType('ACL', [System.Reflection.TypeAttribute], [System.ValueType])
$null = $TypeBuilder.DefineField('AclRevision', [Byte], 'Public')
$null = $TypeBuilder.DefineField('Sbz1', [Byte], 'Public')
$null = $TypeBuilder.DefineField('AckSize', [UInt16], 'Public')
$null = $TypeBuilder.DefineField('AceCount', [UInt16], 'Public')
$null = $TypeBuilder.DefineField('Sbz2', [UInt16], 'Public')

On termine par la création de notre type: $ACL = $TypeBuilder.CreateType().

Par ailleurs, si l’on souhaite utiliser l’attribut MarshalAs() sur l’un des champs de notre structure, il faut commencer par obtenir un constructeur de l’attribut, ce qui peut être fait de 2 manières différentes (la seconde étant plus rapide que la première):

$ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
$ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0]

puis on créer un objet de la classe System.Reflection.Emit.CustomAttributeBuilder :

$AttribBuilder = New-Object System.Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, [Object[]] @([System.Runtime.InteropServices.UnmanagedType]::LPTStr))

ici, le type non managé est LPTStr, d’autres peuvent évidemment être utilisé. Simplement, le type doit appartenir à l’énumération [System.Runtime.InteropServices.UnmanagedType]. Puis on sauvegarde le retour de la création d’un champ et on lui applique la méthode .SetCustomAttribute() avec comme paramètre, notre variable $AttribBuilder :

$newField = $TypeBuilder.DefineField('pName', [String], 'Public')
$newField.SetCustomAttribute($AttribBuilder)

Bon c’est clairement trop long en terme de quantité de code lorsqu’un grand nombre d’énumération/structures/fonctions est utilisé. PSReflect regroupe toutes ces étapes dans plusieurs fonctions déjà nommées. On peut donc alors faire les mêmes implémentations en suivant les étapes suivantes pour la fonction MessageBox:

$InMemoryModule = New-InMemoryModule -ModuleName Win32Lib
$Type = Add-Win32Type -Namespace Win32Lib -DllName User32 -FunctionName MessageBox -EntryPoint MessageBoxW -ReturnType ([Int32]) -ParameterTypes ([IntPtr], [String], [String], [Int32]) -SetLastError -Module $InMemoryModule

L’énumération:

$AMSI_RESULT = psenum $InMemoryModule AMSI_RESULT UInt32 @{
    AMSI_RESULT_CLEAN                  = 0
    AMSI_RESULT_NOT_DETECTED           = 1
    AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384
    AMSI_RESULT_BLOCKED_BY_ADMIN_END   = 20479
    AMSI_RESULT_DETECTED               = 32768
}

La structure:

$ACL = struct $InMemoryModule ACL @{
    AclRevision = field 0 Byte
    Sbz1        = field 1 Byte
    AclSize     = field 2 UInt16
    AceCount    = field 3 UInt16
    Sbz2        = field 4 UInt16
}

Ce qui est vraiment plus rapide. De manière assez évidente, si des structures en appellent d’autres, il faudra simplement présenter la variable contenant le type en tant que type champ, et il en va de même si le type concerné se situe dans une énumération. Il existe un projet de @Jared Atkinson, PSReflect-Functions qui regroupe énormément de fonctions/énumérations/structures fréquemment utilisées. Ainsi lorsqu’il faut utiliser du P/Invoke avec PSReflect, le premier réflexe à s’imposer est d’aller voir si cette dernière est déjà écrite, ce qui provoque naturellement un certain gain de temps. En revanche, comment implémenter une fonction qui n’est pas déjà présente dans PSReflect-Functions. La première étape est d’aller sur pinvoke.net et de chercher une signature pour sa fonction ou alors de lire la documentation officielle de Microsoft sur cette dernière (sinon, il faut sortir les outils de rétro-conception). Prenons la fonction LookupPrivilegeValueA de advapi32.dll. Voici la définition de cette dernière:

BOOL LookupPrivilegeValueA( LPCSTR lpSystemName, LPCSTR lpName, PLUID lpLuid );

On commence par évaluer le type de retour, sans trop de difficulté, ici ce sera [bool]. Puis les types des arguments. Les deux premiers seront des [string], le dernier est un pointeur vers une structure. Pour cela il faut implémenter ladite structure (premier réflexe, PSReflect-Functions) puis on donne la variable de définition de la structure en tant que type de paramètre, enfin on ajoute .MakeByRefType() qui indique qu’il s’agit d’un pointeur. Notre fonction ressemblera donc à ceci:

(func advapi32 LookupPrivilegeValue ([bool]) @(
        [String],             # LPCSTR lpSystemName
        [String],             # LPCSTR lpName
        $LUID.MakeByRefType() # PLUID  lpLuid
) -EntryPoint LookupPrivilegeValue -SetLastError)

Pour s’en servir, on fait comme cela:

$LUIDObject = [Activator]::CreateInstance($LUID) # on créer une structure de LUID
$advapi32::LookupPrivilegeValue([String]::Empty, "SeDebugPrivilege", [ref]$LUIDObject)

Pour vous familiariser avec la programmation avec PSReflect je vous invite fortement à lire le code de PSReflect-Functions.

SweetBackup

Lorsque l’on souhaite utiliser certains droits, il est parfois nécessaire d’utiliser des fonctions de la WinAPI. C’est le cas lorsque l’on souhaite utiliser le droit SeBackupPrivilege qui doit absolument avoir un flag particulier lors de l’appelle de la fonction CreateFileA. Or ce flag est tellement particulier, qu’il n’est accessible dans la très grande majorité des utilitaires et des outils Windows. Les cmdlets comme Get-Item ou Get-Content ne l’utilisent pas, les commandes comme dir ou type ne l’implémentent pas. Seulement robocopy.exe (éventuellement xcopy.exe) le fait. Or il est parfois très utile de posséder des outils permettant de l’utiliser, par exemple dans le cas où un membre du groupe “Backup Operators” a été compromis. Jusqu’alors, la seule possibilité était de charger en mémoire deux dll .NET mais vous le savez, déposer des fichiers, c’est laisser des traces. Mon objectif était d’obtenir une implémentation en PowerShell qui s’affranchissait de cette restriction. Seulement, il est nécessaire de faire du P/Invoke pour utiliser le fameux flag. Mon choix s’est alors naturellement tourné vers PSReflect. J’ai donc besoin de:

  • RtlAdjustPrivilege
  • CreateFile
  • ReadFile
  • WriteFile
  • LookupPrivilegeDisplayName
  • LookupPrivilegeName
  • GetTokenInformation
  • OpenProcessToken
  • CloseHandle
  • GetCurrentProcess

Dans le but de créer les fonctions cmdlets suivantes:

  • Get-SeBackupPrivilege qui permet de savoir si le droit SeBackupPrivilege est activé ou non.
  • Set-SeBackupPrivilege qui permet de changer le statut du droit SeBackupPrivilege.
  • Read-FileContent qui permet de lire le contenu d’un fichier.
  • Set-FileContent qui permet de changer le contenu d’un fichier.
  • Copy-File qui permet de copier un fichier.

Je ne parlerais pas de la programmation des fonctions en elles-mêmes car ce n’est pas très intéressant. En revanche je vais me concentrer sur la partie P/Invoke. Il faut savoir que certaines de ces fonctions sont déjà implémenter dans la librairie PSReflect-Functions, je me suis donc allègrement servis (pourquoi s’en priver après tout). Il me restait alors qu’à implémenter Read/WriteFile. La première est définie ainsi:

BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped );

D’après le tableau de tout à l’heure, hFile sera un IntPtr, nNumberOfBytesToRead est un UInt32, lpNumverOfBytesRead un pointeur vers un UInt32. D’après la documentation de cette même fonction, le dernier paramètre correspond à une structure particulière qui doit être utilisé dans le cas où le HANDLE vers notre fichier a été créé avec le flag FILE_FLAG_OVERLAPPED, ce qui n’est guerre notre cas. On peut donc se permettre d’utiliser un IntPtr qui sera vraisemblablement nul. Le type LPVOID n’a pas de clair équivalent, il dépend de la situation dans lequel il est utilisé. En regardant sur pinvoke.net pour la signature de ReadFile, il semblerait que ce soit un tableau Byte qui doit être utilisé. Notre définition ressemblera donc à cela:

(func kernel32 ReadFile ([bool]) @(
        [IntPtr],                 # HANDLE       hFile
        [Byte[]],                 # LPVOID       lpBuffer
        [UInt32],                 # DWORD        nNumberOfBytesToRead
        [UInt32].MakeByRefType(), # LPDWORD      lpNumberOfBytesRead
        [IntPtr]                  # LPOVERLAPPED lpOverlapped
) -EntryPoint ReadFile -SetLastError)

La seconde fonction est définie ainsi:

BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped );

Les types des paramètres deviennent alors évident, hFile est un IntPtr, lpBuffer un Byte[], nNumberOfBytesToWrite est un UInt32, lpNumberOfBytesWritten est un pointeur vers un UInt32, lpOverlapped est un IntPtr qui sera vraisemblablement nul. Notre définition ressemblera donc à cela:

(func kernel32 WriteFile ([bool]) @(
        [IntPtr],                 # HANDLE       hFile
        [Byte[]],                 # LPCVOID      lpBuffer
        [UInt32],                 # DWORD        nNumberOfBytesToWrite
        [UInt32].MakeByRefType(), # LPDWORD      lpNumberOfBytesWritten
        [IntPtr]                  # LPOVERLAPPED lpOverlapped
) -EntryPoint WriteFile -SetLastError)

Or l’ensemble de ces fonctions nécessite un certain nombre d’énumérations et de structures que voici:

  • TOKEN_INFORMATION_CLASS
  • FILE_ACCESS
  • FILE_SHARE
  • CREATION_DISPOSITION
  • FILE_FLAGS_AND_ATTRIBUTES
  • TOKEN_ACCESS
  • SE_PRIVILEGE
  • SecurityEntity
  • TOKEN_PRIVILEGES et ses compléments

Qui sont par chance déjà définies dans PSReflect-Functions. On commence alors par créer notre module: $Module = New-InMemoryModule -ModuleName BackupMode Puis on ajoute nos énumérations et nos structures. Ensuite on crée une variable $FunctionDefinition qui est un tableau contenant toutes nos fonctions. On ajoute alors le type: $Types = $FunctionDefinition | Add-Win32Type -Module $Module -Namespace BackupMode Enfin, on récupère chaque dll et nous stockons son objet dans une variable pour rendre plus aisé l’utilisation de ses fonctions:

$advapi32 = $Types['advapi32']
$kernel32 = $Types['kernel32']
$ntdll = $Types['ntdll']

On peut alors accéder aux fonctions par leur variable comme ceci, en prenant l’exemple de la fonction GetCurrentProcess qui retourne un HANDLE pour notre processus (qui sera d’ailleurs -1 tout le temps, mais il est préférable, en accord avec la documentation Microsoft, d’utiliser cette fonction plutôt que d’utiliser une valeur codée en dure): $kernel32::GetCurrentProcess()

CVE-2021-1675

CVE-2021-1675 est une LPE/RCE utilisant le tristement célèbre PrintSpooler qui permet de gérer les systèmes d’impressions chez Windows. L’exploitation de la vulnérabilité PrintNightmare est un bon exemple pour utiliser le Pinvoke. Dans cette partie, nous allons voir comment utiliser ces méthodes de Pinvoke manuelle pour construire notre exploit (je me suis beaucoup inspiré du travail de @cube0x0, de @CalebStewart et @JohnHammond). Nous commençons par créer un module en mémoire:

$DynAssembly = New-Object System.Reflection.AssemblyName('BlueNightmareAssembly')
$AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('BlueNightmare', $False)

Cet exploit requière l’utilisation de deux fonctions de winspool.drv, AddPrinterDriverEx et EnumPrinterDrivers dont je vous invite à consulter la documentation pour voir les types des différents paramètres. On commence donc par définir les invariants de définition des fonctions, ainsi que le type à ajouter dans le module:

$TypeBuilder = $ModuleBuilder.DefineType('winsplool', 'Public, Class')

$DllImportConstructor = [System.Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$FieldArray = [System.Reflection.FieldInfo[]] @(
	[System.Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('SetLastError'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('CallingConvention'),
	[System.Runtime.InteropServices.DllImportAttribute].GetField('CharSet')
)

Ce qui nous permet d’ajouter la première fonction, dont le type de retour est un booléen:

$PInvokeMethod = $TypeBuilder.DefineMethod(
	'AddPrinterDriverEx',
	[Reflection.MethodAttributes] 'Public, Static',
	[Bool],
	[Type[]] @([String], [UInt32], [IntPtr], [UInt32])
)

$FieldValueArray = [Object[]] @(
	'AddPrinterDriverEx',
	$true,
	$true,
	[System.Runtime.InteropServices.CallingConvention]::Winapi,
	[System.Runtime.InteropServices.CharSet]::Unicode
)

$SetLastErrorCustomAttribute = New-Object System.Reflection.Emit.CustomAttributeBuilder(
	$DLLImportConstructor,
	@('winspool.drv'),
	$FieldArray,
	$FieldValueArray
)

$PinvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

Puis on fait de même avec la seconde fonction:

$PInvokeMethod = $TypeBuilder.DefineMethod(
	'EnumPrinterDrivers',
	[Reflection.MethodAttributes] 'Public, Static',
	[Bool],
	[Type[]] @([String], [String], [UInt32], [IntPtr], [UInt32], [UInt32].MakeByRefType(), [UInt32].MakeByRefType())
)

$FieldValueArray = [Object[]] @(
	'EnumPrinterDrivers',
	$true,
	$true,
	[System.Runtime.InteropServices.CallingConvention]::Winapi,
	[System.Runtime.InteropServices.CharSet]::Auto
)

$SetLastErrorCustomAttribute = New-Object System.Reflection.Emit.CustomAttributeBuilder(
	$DLLImportConstructor,
	@('winspool.drv'),
	$FieldArray,
	$FieldValueArray
)

$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

On finit par créer le type qui contiendra les fonctions avec $winspool = $TypeBuilder.CreateType(). La dernière fonction importée doit nous obtenir des informations qui devront se situer dans la structure DRIVER_INFO_2 que nous devons donc définir. D’après pinvoke.net tout les champs qui sont des string doivent être marshallé vers le type LPTStr. En reprenant les étapes vues plus haut:

$ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
$AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, [Object[]] @([System.Runtime.InteropServices.UnmanagedType]::LPTStr))

$TypeBuilder = $ModuleBuilder.DefineType('_DRIVER_INFO_2', 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit', [System.ValueType], [Reflection.Emit.PackingSize]::Unspecified)

$null = $TypeBuilder.DefineField('cVersion', [UInt32], 'Public')

$newField = $TypeBuilder.DefineField('pName', [String], 'Public')
$newField.SetCustomAttribute($AttribBuilder)

$newField = $TypeBuilder.DefineField('pEnvironment', [String], 'Public')
$newField.SetCustomAttribute($AttribBuilder)

$newField = $TypeBuilder.DefineField('pDriverPath', [String], 'Public')
$newField.SetCustomAttribute($AttribBuilder)

$newField = $TypeBuilder.DefineField('pDataFile', [String], 'Public')
$newField.SetCustomAttribute($AttribBuilder)

$newField = $TypeBuilder.DefineField('pConfigFile', [String], 'Public')
$newField.SetCustomAttribute($AttribBuilder)


$DRIVER_INFO_2 = $TypeBuilder.CreateType()

Et nous avons terminé pour le Pinvoke, je vous invite tout de même à lire le reste du programme pour vous faire une idée. Selon moi, il était préférable dans cette situation d’utiliser manuellement le Pinvoke, car l’exploit nécessitait très peu d’importation, et par conséquent, les 650 lignes de PSReflect en plus n’auraient pas été nécessaire. Le résultat, environ 500 lignes économisées. Évidemment, on peut utiliser PSReflect pour faire le même travail, mais cela me semble un peut moins pertinent, contrairement à SweetBackup où beaucoup d’importations sont faites, et donc nous économisons des lignes et du travail.

Bonnes pratiques de programmation

Dans cette section nous allons parler de bonnes pratiques à prendre dans le but d’avoir un code plus lisible, mais également d’assurer la compatibilité avec toutes les versions de PowerShell.

Pour les fonctions, il est d’usage d’indiquer après un paramètre, en commentaire, le type et le nom du paramètre conformément à la documentation de Microsoft. Ainsi, il devient plus aisé de comprendre pourquoi quel type. Cependant, dépendant de l’usage qui est fait de la fonction native, un paramètre n’a guerre l’obligation d’être du bon type. En effet, comme vous le verrez juste après, si un paramètre est dédiée à une certaine opération précise qui ne sera jamais accomplie, on peut le remplacer par le type IntPtr et le spécifié à [IntPtr]::Zero lors de l’appel. Pour généraliser cette remarque, lorsqu’un paramètre sera nul lors de l’appel dans tout les cas de son utilisation, il est mieux de remplacer ce dernier par un IntPtr qui sera toujours [IntPtr]::Zero. Dans le but d’assurer une bonne compatibilité de code, il est toujours préférable de ne pas appeler les fonctions chargées via l’espace de nom créé par le module en mémoire, mais plutôt par leur variable comme ci dessus. En effet PowerShell v2 est un peu ennuyant sur cet aspect.

Concernant les structures, de manière générale, puisque les structures sont des types dynamiques, les méthodes de la réflexion s’utilisent pour les instancier. Il faut également savoir que les structures générées par PSReflect sont fournies avec une méthode .GetSize() qui permet d’éviter de tout le temps utiliser [System.Runtime.InteropServices.Marshal]::SizeOf(). Il est également préférable, pour un code clair, d’aligner les éléments, au niveau du symbole “=”, lors de la définition de la structure, pour qu’il soit plus agréable de la lire. Le cas des unions est un peu particulier. Pour rappel, lorsqu’une structure contient un union, cela signifie qu’un même champ peut prendre de type différent mais pas les deux. Prenons exemple de la structure IO_STATUS_BLOCK :

typedef struct _IO_STATUS_BLOCK {
  union {
    NTSTATUS Status;
    PVOID    Pointer;
  };
  ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

Lors de l’utilisation de cette structure, le premier offset (donc 0), soit il contient un UInt32 (NTSTATUS) soit un pointeur mais pas les deux en même temps. Il est évidemment possible de réaliser de tels choses en C# il en découle naturellement que PowerShell est également apte à le faire. Pour cela, on doit changer le type de structure à créer en ajoutant le paramètre -ExplicitLayout à la fonction struct, et en précisant l’offset où vit chaque valeur. Ainsi nous devrions obtenir avec PSReflect la structure suivante pour IO_STATUS_BLOCK:

$IO_STATUS_BLOCK = struct $STRUCT IO_STATUS_BLOCK @{
    Status  = field 0 Int64  -Offset 0
    Pointer = field 1 IntPtr -Offset 0
} -ExplicitLayout

Si les paramètres de la structure doivent être marshallées, ils faut utiliser le flag -MarshalAs avec une liste contenant le type de destination, et éventuellement sa taille. Pour reprendre l’exemple en PInvoke manuel: pName = field 0 string -MarshalAs @("LPTStr")

Il faut voir les énumérations comme des classes où les méthodes sont les valeurs de l’énumération. Ainsi, on utilise :: après la variable de l’énumération. Il est à noter que lorsque l’on accède à une valeur d’une énumération, la valeur qui sera retournée sera le nom de la valeur plutôt que la définition de celle-ci. Pour y accéder, il faudra utiliser la propriété value__, mais quand on doit utiliser une énumération, il ne faut pas s’inquiéter et appeler la propriété dernièrement cité, simplement le nom de la valeur souhaité. Tout comme les structures, il faut aussi penser à bien aligner les valeurs au niveau du “=” pour plus de clarté.

Une dernière chose, PSReflect est très utile, mais gardez à l’esprit que s’il faut simplement importer une seule fonction, il est préférable d’économiser du code et utiliser le Pinvoke manuellement.

J’espère que cet article vous aura plu et vous donne une meilleur compréhension de PSReflect et du P/Invoke en PowerShell ! En revanche, je n’ai pas traité de toutes les techniques possibles pour faire du Pinvoke en PowerShell, mais si l’anglais ne vous effraie pas, je vous invite à consulter les documents de la formation Adversary Tactics PowerShell de SpecterOps qui y dédie une partie entière. SweetBackup est disponible ici et l’exploit pour PrintNightmare ici.

Leave a Reply