Lorsqu’un programme à la capacité d’accéder à ses propres métadonnées, de magnifiques perspectives s’offrent alors à nous. Modifier son comportement en RunTime
ou ajouter des types s’inscrivent dans ce que l’on appelle la réflexion et .NET le permet, particulièrement grâce au C#. Puisque PowerShell est entièrement construit sur ce dernier, nombres de possibilités s’ouvrent alors. Dans cet article, nous étudierons les manières d’utiliser la réflexion en PowerShell qui est l’une des clés du PInvoke.
Pour des questions de lisibilité, je définis “méthode” de 2 manières différentes puisque je risque de souvent l’utiliser:
- méthode: qui désigne un ensemble de démarches raisonnées pour atteindre un but (merci le Robert).
- méthode: fonction s’appliquant sur et depuis un type sous-jacent (un type étant une classe ou un objet .NET).
L’architecture .NET
Avant de commencer, j’ai besoin de vous expliquer certains concepts et d’établir un ensemble de définition afin de ne pas vous perdre. Le framework .NET est composé de plusieurs éléments, chacun permettant d’achever une tâche particulière. L’ensemble de ces composants créé une sorte de structure pour .NET. Tout en haut, nous avons le code dans sa représentation la plus brute : C#, F#, ou encore VB.NET. Lorsque de tels programmes sont compilés, le code est transformé en CIL/IL pour Common Intermediate Language (aussi connu sous le nom de MSIL pour Microsoft Intermediate Language). Ce dernier ressemble d’une certaine façon à l’assembleur sans toutefois intégrer des appels directement au système. Voila un exemple de code en MSIL:
.class private auto ansi beforefieldinit SharpSample.Program extends [mscorlib]System.Object { // Methods .method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2050 // Code size 113 (0x71) .maxstack 4 .entrypoint .locals init ( [0] class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.LdapConnection connection, [1] class [System]System.Net.NetworkCredential cred, [2] string 'filter', [3] string[] attributesToReturn, [4] class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchRequest searchRequest, [5] class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResponse searchResponse, [6] class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResultAttributeCollection attributes ) IL_0000: nop IL_0001: ldstr "testlab.local" IL_0006: newobj instance void [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.LdapConnection::.ctor(string) IL_000b: stloc.0 IL_000c: ldstr "admin" IL_0011: ldstr "passw0rd1!" IL_0016: ldstr "testlab.local" IL_001b: newobj instance void [System]System.Net.NetworkCredential::.ctor(string, string, string) IL_0020: stloc.1 IL_0021: ldloc.0 IL_0022: ldloc.1 IL_0023: callvirt instance void [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.DirectoryConnection::set_Credential(class [System]System.Net.NetworkCredential) IL_0028: nop IL_0029: ldstr "cn=WS01" IL_002e: stloc.2 IL_002f: ldc.i4.1 IL_0030: newarr [mscorlib]System.String IL_0035: dup IL_0036: ldc.i4.0 IL_0037: ldstr "msDS-SupportedEncryptionTypes" IL_003c: stelem.ref IL_003d: stloc.3 IL_003e: ldstr "" IL_0043: ldloc.2 IL_0044: ldc.i4.2 IL_0045: ldloc.3 IL_0046: newobj instance void [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchRequest::.ctor(string, string, valuetype [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchScope, string[]) IL_004b: stloc.s 4 IL_004d: ldloc.0 IL_004e: ldloc.s 4 IL_0050: callvirt instance class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.DirectoryResponse [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.DirectoryConnection::SendRequest(class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.DirectoryRequest) IL_0055: castclass [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResponse IL_005a: stloc.s 5 IL_005c: ldloc.s 5 IL_005e: callvirt instance class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResultEntryCollection [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResponse::get_Entries() IL_0063: ldc.i4.0 IL_0064: callvirt instance class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResultEntry [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResultEntryCollection::get_Item(int32) IL_0069: callvirt instance class [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResultAttributeCollection [System.DirectoryServices.Protocols]System.DirectoryServices.Protocols.SearchResultEntry::get_Attributes() IL_006e: stloc.s 6 IL_0070: ret } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20cd // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class SharpSample.Program
Qui en C# est:
using System.Net; using System.DirectoryServices.Protocols; namespace SharpSample { class Program { static void Main(string[] args) { LdapConnection connection = new LdapConnection("testlab.local"); NetworkCredential cred = new NetworkCredential("admin", "passw0rd1!", "testlab.local"); connection.Credential = cred; string filter = "cn=" + "WS01"; string[] attributesToReturn = new string[] { "msDS-SupportedEncryptionTypes" }; SearchRequest searchRequest = new SearchRequest("", filter, SearchScope.Subtree, attributesToReturn); SearchResponse searchResponse = (SearchResponse)connection.SendRequest(searchRequest); SearchResultAttributeCollection attributes = searchResponse.Entries[0].Attributes; } } }
Pour cela, le code est passé par le compilateur au CLI pour Common Language Infrastructure, qui est l’un des deux cœurs de .NET. Ce dernier se divise en plusieurs axes. Nous avons le CTS pour Common Type System qui permet d’établir une manière commune aux différents langages compatible .NET, de définir leurs types. Ainsi, CTS définit les types valeurs et les types références. Les premiers sont des objets représentés par leur valeur (comme les entiers ou les chaînes de caractères), contrairement aux seconds qui agissent similairement aux pointeurs en C/C++. CTS regroupe également ces types dans plusieurs catégories. Par exemple, les classes, les structures, ou bien les énumérations. Chacune de ces catégories possède une syntaxe propre et un ensemble de propriétés propres qui sont également définies. On peut retrouver ces types primitifs dans l’espace de nom commun à tout langage du framework .NET System
(situé dans la dll cœur de .NET mscorlib.dll
). Pourtant, qu’importe le langage, il existera des points communs entre ces types primitifs. Ces points communs sont définis grâce à CLS pour Common Language Specification et doivent être appliqué par CLI pour pouvoir obtenir une “inter-opérabilité” parmi les langages compatibles. Ce code, construit à partir de la CLI, est appelé code managé et est transformé en code machine lors de son exécution par le CLR pour Common Language Runtime et son compilateur JIT (Just In Time). En C#, si le code n’est pas directement compilé en langage Machine, cela permet plusieurs choses. En premier lieu, les binaires générés sont multi-systèmes d’exploitation. En second lieu, cela permet l’intégration de code dynamique grâce au compilateur JIT. Enfin, la construction du langage se rapproche beaucoup de Java, qui a énormément inspiré C#, ce qui rend la transition entre les deux langages plus aisée.
Par ailleurs, les types sont des objets et non simplement un espace en mémoire !
La Réflexion en C#
Pour commencer, il est nécessaire de préciser que les assemblys contiennent des modules, qui contiennent des types qui contiennent des membres. Chacun de ces blocs intervient dans la réflexion. Commençons par les assemblys. Les assemblys (ou assemblage en français, oui c’est moche) sont fondamentales pour .NET et sont le minimum à déployer (penser à la place de la cellule dans la théorie cellulaire, pour les aficionados de biologie: la cellule est l’unité de base du vivant, pourtant elle possède des composants). Ce sont usuellement des fichiers exe, dll (mais pas que) et contiennent des collections de modules (et par conséquent de types …) et de ressources. Elles possèdent un certain nombre de propriétés qui sont retenues dans un manifeste qui exhibe, le nom de l’assembly, la version de l’assembly, une liste des références à d’autres assemblys notamment. La plupart des dlls de référence sont situées dans le GAC pour Global Assembly Cache qui est par défaut en C:\Windows\Assembly
(pour .NET de 1.0 à 3.5) ou en C:\Windows\Microsoft.NET\Assembly
(à partir de .NET 4.0). Le but n’étant pas l’exhaustivité, vous pouvez trouver plus de détails ici. Un module, quant à lui, est simplement une assembly (structurellement parlant) mais qui ne contient pas de manifeste. Ainsi, il expose les métadonnées de type, et le code compilé. Fait amusant, puisque toutes les assemblys .NET doivent supporter CTS et CLS, il est possible de joindre à une assembly plusieurs modules qui ne proviennent pas du même langage ! Concernant les types et les membres. Ils ont brièvement été introduits plus haut. Cependant, il faut apporter quelques précisions. Un type peut représenter une classe et peut donc contenir des “sous-types”. Par conséquent on peut également utiliser le vocabulaire des classes pour les types. Un champ Field
est une valeur nommée dans une classe. Une propriété Property
est un type particulier de méthode qui permet d’interagir avec un champ. Les constructeurs Constructor
sont des méthodes spécifiques qui aident à initialiser les classes. On désigne par membre tout ce qui vient d’être mentionné…
Pour manipuler ces blocs, on utilise plusieurs espaces de noms et classes. Le premier est System.Reflection
qui contient deux classes très importantes: Assembly
et Module
. System.Type
qui permet, comme son nom l’indique, de manipuler les types (et représente par ailleurs les types du CTS). Pour interagir dynamiquement avec les assemblys/types, on utilise System.Reflection.Emit
. De manière générale, la réflexion s’articule autour de 2 étapes, on obtient d’abord le type au travers d’une assembly, puis on accède à ses membres.
Interagir avec les types et les méthodes
Pour obtenir le type d’un objet, rien de plus simple. En PowerShell il n’existe pas d’opérateur typeof
, cependant l’utilisation de la méthode .GetType()
permet d’obtenir le même résultat; mais on peut également, dans les cas où cela est possible, accéder au type grâce “au chemin de sa classe” (e.g [System.DirectoryServices.DirectoryEntry]
). Il faut également mentionner qu’il peut être utilisé pour obtenir un type particulier contenu dans une assembly spécifique. Pour cela, il faut obtenir un objet de type RuntimeAssembly
issu de l’assembly en question. C’est dans ce but que l’on utilise la propriété .Assembly
auquel on applique la méthode
.GetType()
avec en paramètre une chaîne de caractère décrivant le type. Un exemple issu du bypass de l’article sur le contournement de protection PowerShell:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
Ou pour faire écho à l’exemple ci-dessus:
[System.DirectoryServices.ActiveDirectoryRights].Assembly.GetType('System.DirectoryServices.DirectoryEntry')
Cette méthode est très pratique puisqu’elle permet d’accéder aux types non exposés (ou privés) lors de leur déclaration à l’image du premier exemple. Grâce à ce type obtenu, on peut alors énumérer ses membres grâce à l’utilisation de la cmdlet Get-Member
(ou la méthode .GetMembers()
attention beaucoup d’informations seront retournées). La cmdlet s’organise autour d’un nombre assez faible de paramètres. Le flag MemberType
indique le type de membre qu’il faut retourner, par exemple les propriétés (Property
) ou les méthodes (Method
). Pour obtenir l’accès à un membre en particulier on utilise la méthode .GetMember()
qui prend en paramètre le nom du membre, et éventuellement les caractéristiques de sa définition, les BindingFlags
. Il existe des méthodes similaires pour les Fields
les Constructors
… Pour lire/modifier ces membres, on peut utiliser les méthodes .GetValue()
et .SetValue()
. La première prend en paramètre un objet ou une liste d’objets qui seront retournées, la seconde prend en paramètre un objet ou une liste d’objet qui seront la nouvelle valeur. On peut alors combiner ces actions pour modifier le comportement de certaines fonctions accessibles, comme l’alphabet d’encodage de base64 (exemple issu de at-ps) :
[Byte[]] $DataToEncode = 0..255 # donnés encodé avant la modification $EncodedData1 = [Convert]::ToBase64String($DataToEncode) # On obtient en premier lieu un accès au champ base64Table que l’on peut trouver dans DnSpy $Base64TableField = [Convert].GetField('base64Table', [Reflection.BindingFlags] 'NonPublic, Static') $OriginalBase64Alphabet = $Base64TableField.GetValue($null) # Ce qui nous permet de changer les deux premiers caractères $OriginalBase64Alphabet[0] = [Char] 'B' $OriginalBase64Alphabet[1] = [Char] 'A' # On peut utiliser la variable Base64TableField pour lui appliquer la méthode SetValue() avec notre nouvel alphabet $Base64TableField.SetValue($null, $OriginalBase64Alphabet) # On encode et on peut alors comparer $EncodedData2 = [Convert]::ToBase64String($DataToEncode) $EncodedData1 $EncodedData2
L’instanciation d’un type est universelle et nécessaire (a priori pour les amateurs de philosophie). En PowerShell on dispose principalement de 3 manières de procéder. La première est spécifique à PowerShell, c’est la cmdlet New-Object
. Un paramètre ArgumentList
permet d’ajouter des informations au type créé. Seulement des méthodes plus élémentaires existent. La première provient de la classe System.Activator
avec la méthode ::CreateInstance()
qui prend en paramètre le type à instancier, puis une liste d’objets éventuelle qui sera complétée par la liste en question dans le paramètre suivant. La dernière méthode utilise les constructeurs avec la méthode .GetConstructor()
qui prend en paramètre une liste de types qui représente l’ordre des types des arguments fournis. Le résultat de cette méthode (peut être placé dans une variable pour plus de lisibilité de code) est passé à la méthode .Invoke()
– qui s’applique à la classe System.Reflection.ConstructorInfo
– qui prend en paramètre une liste qui contient les arguments fournis. Voilà un exemple pour illustrer mon propos, avec la classe System.DirectoryServices.DirectoryEntry
:
New-Object System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://CN=AdminSDHolder,CN=System,DC=contoso,DC=local" [System.Activator]::CreateInstance([System.DirectoryServices.DirectoryEntry], [Object[]], @("LDAP://CN=AdminSDHolder,CN=System,DC=contoso,DC=local")) [System.DirectoryServices.DirectoryEntry].GetConstructor([Type[]] @([String])).Invoke([Object[]] @("LDAP://CN=AdminSDHolder,CN=System,DC=contoso,DC=local"))
Il est important de comprendre que New-Object
ne permet pas d’accéder aux types privés tout comme la méthode naturelle pour PowerShell (e.g [System.DirectoryServices.Directory]
). Pourtant les types privés peuvent tout de même être instanciés grâce aux 2 autres méthodes vues plus haut
$Type = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils') $Instance = [Activator]::CreateInstance($Type)
De plus, il est souvent plus simple d’utiliser la troisième méthode lorsque nous souhaitons déclarer des attributs.
Désormais en possession de types, il faut maintenant déterminer les moyens d’exécuter des méthodes. En utilisant la réflexion, il est possible, évidemment, d’accéder aux méthodes d’un type en utilisant .GetMethod()
. Elle prend plusieurs arguments, en premier le nom de la méthode, puis des BindingFlags
, puis un binder (très souvent nul), ensuite un tableau de type représentant les types des arguments, et enfin un tableau de ParameterModifier
(très souvent nul également). .GetMethod()
retourne un System.Reflection.MethodInfo
qui nous permet d’appeler .Invoke()
qui prend en paramètre l’objet sur lequel on applique la méthode, puis la liste des arguments. Voici un exemple issu de at-ps pour convertir un entier en hexadécimal:
$IntToConvert = 1094795585 $ToStringMethod = [Int32].GetMethod('ToString',[Reflection.BindingFlags] 'Public, Instance', $null, [Type[]] @([String]), $null) $ToStringMethod.Invoke($IntToConvert, [Object[]] @('X8'))
Au travers de cette section, vous avez pu apercevoir l’importance des espaces de noms présentés dans la seconde section qui sont très utilisés.
Interagir avec les Assembly
Très souvent lorsque l’on utilise PowerShell, on souhaite utiliser du code C# qui n’est pas natif. Une façon de procéder très connu dans ces situations est d’utiliser la cmdlet Add-Type
qui compile le code qui lui est fournit et utilise les fonctionnalités, que nous allons explorer dans cette partie, pour l’ajouter à notre session. Comme nous pouvons le voir sur cette capture d’écran (faite avec procmon64.exe
), l’appel de Add-Type
provoque la création d’un dossier d’un fichier temporaire et la création d’un processus csc.exe
.
L’ensemble de ces artefacts rend l’opération facilement détectable. Pourtant l’espace de nom System.Reflection
possède de très puissantes capacités qui nous aiderons à contourner ce problème. Charger un assembly est une tâche très aisée. Pour cela, deux possibilités. Si l’assembly se trouve sur le disque, on utilise la commande suivante: [System.Reflection.Assembly]::LoadFile("Chemin Complet vers le fichier")
. Si l’assembly n’est pas sur le disque, il est possible de la charger depuis un tableau de bytes qui le représente. Pour obtenir ce dernier vous pouvez, sur votre machine, utiliser la méthode .ReadAllBytes()
qui prend en paramètre le chemin complet vers l’assembly, de la classe System.IO.File
: $assemblyBytes = [System.IO.File]::ReadAllBytes("Chemin Complet vers le fichier")
. Une possibilité autre est d’exhiber l’assembly sur un serveur web et d’utiliser la méthode .DownloadData()
qui prend en argument l’URL où se situe le binaire: $assemblyBytes = (New-Object System.Net.WebClient).DownloadData("http://contoso.com/assembly.exe")
. Une fois en possession du tableau de bytes, on peut utiliser la méthode ::Load()
: $assembly = [System.Reflection.Assembly]::Load($assemblyBytes)
S’il s’agît d’un exécutable .NET, on peut alors invoquer la méthode Main()
en utilisant la propriété .EntryPoint
et lui appliquer la méthode .Invoke()
; à l’image de tout à l’heure avec les paramètres suivants $null, [Object[]] @(@(,([String[]] @())))
où le dernier tableau contient les arguments à passer. Si l’assembly n’est pas un exécutable, .EntryPoint
retourne $null
. Sinon il faut connaître les espaces de noms/classes/méthodes de l’assembly chargée pour les appeler de la même manière que les types habituels. Petit exemple qui charge Rubeus.exe
:
$assembly = [System.Reflection.Assembly]::LoadFile("D:\tools\Rubeus\Rubeus\bin\debug\Rubeus.exe") $assembly.EntryPoint.Invoke($null, [Object[]] @(@(,([String[]] @("triage")))))
Attention tout de même, les méthodes de chargements d’assembly sont supportées par l’AMSI à partir de .NET 4.8, vos outils offensifs préférés seront sûrement détectés !
Une manière de contourner ce problème est d’ajouter dynamiquement du code à notre session. Pour cela il faut dans un premier temps créer un module en mémoire. Ainsi, nous devons ajouter à notre domaine d’exécution – c’est à dire l’ensemble des informations de l’environnement dans lequel l’application est exécutée –, une assembly dynamique. Pour en créer une, nous utilisons la classe System.Reflection.AssemblyName
avec en paramètre le nom de l’assembly. Puis on ajoute l’assembly au domaine en utilisant la méthode .DefineDynamicAssembly()
qui prend en argument la classe instanciée précédemment, et le droit d’accès à l’assembly qui sera issu de l’énumération AssemblyBuilderAccess
de la classe System.Reflection.Emit
. La plupart du temps, il faut choisir le flag Run
. A partir de là, si vous avez bien suivis, vous savez qu’il faut ajouter un module à notre assembly dynamique. Ce faisant, on utilise la méthode .DefineDynamicModule()
qui prend en paramètre le nom du module.
$DynAssembly = New-Object System.Reflection.AssemblyName('InMemoryAssembly') $AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run) $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $False)
On peut alors définir de nouveaux types, comme les classes avec la méthode .DefineType()
qui prend en paramètre le nom du type, puis ses attributs de type contenu dans l’énumération System.Reflection.TypeAttributes
, le type parents et une liste de types des interfaces implémentées. Une fois la méthode créée on peut lui ajouter des instructions. Pour cela il faut directement lui fournir du code MSIL. Dans un premier temps nous obtenons un objet System.Reflection.Emit.ILGenerator
grâce à la méthode .GetILGenerator()
. Puis on adjoint les instructions MSIL avec la méthode .Emit() qui prend en paramètre l’instruction MSIL de la structure System.Reflection.Emit.OpCode
et l’éventuel paramètre de cette instruction. Pour appeler une méthode, il est nécessaire d’obtenir un objet .MethodeInfo()
qui représente la méthode donnée avec l’instruction Call
. Pour ajouter un point d’entré à notre assembly, on utilise .SetEntryPoint()
avec en paramètre la variable contenant le type MethodBuilder
qui nous a permis de construire cette dernière. Enfin, on créer le type avec la méthode .CreateType()
. La création de code dynamique est permise par l’utilisation d’un compilateur JIT qui transforme le bytecode IL en code machine. Voici un exemple pour un programme qui affiche Hello, world!
(merci SpecterOps) :
$Domain = [AppDomain]::CurrentDomain $DynAssembly = New-Object System.Reflection.AssemblyName('HelloWorld') $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run) $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('HelloWorld.exe') $TypeBuilder = $ModuleBuilder.DefineType('MyClass', [Reflection.TypeAttributes]::Public) $MethodBuilder = $TypeBuilder.DefineMethod('Main', [Reflection.MethodAttributes] 'Public, Static', [Void], @([String[]])) $Generator = $MethodBuilder.GetILGenerator() $WriteLineMethod = [Console].GetMethod('WriteLine', [Type[]] @([String])) # Recreate the MSIL from the disassembly listing. $Generator.Emit([Reflection.Emit.OpCodes]::Ldstr, 'Hello, world!') $Generator.Emit([Reflection.Emit.OpCodes]::Call, $WriteLineMethod) $Generator.Emit([Reflection.Emit.OpCodes]::Ret) $AssemblyBuilder.SetEntryPoint($MethodBuilder) $TypeBuilder.CreateType() [MyClass]::Main(@())
Et un autre provenant de PSReflect qui ajoute une méthode ::GetSize()
à une structure:
$SizeMethod = $StructBuilder.DefineMethod('GetSize', 'Public, Static', [Int], [Type[]] @()) $ILGenerator = $SizeMethod.GetILGenerator() $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, [Type].GetMethod('GetTypeFromHandle')) $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, [Runtime.InteropServices.Marshal].GetMethod('SizeOf', [Type[]] @([Type]))) $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ret)
La possibilité d’émettre du code dynamiquement est très utile pour faire du Pinvoke. Il est assez rare d’utiliser cette façon de procéder pour ajouter du code dynamiquement, mais il faut toujours garder en mémoire qu’elle existe, pour ajouter de courtes méthodes à des objets personnalisés. Pour des assemblys plus conséquente, il va de soi que l’on préférera utiliser un chargement d’assembly, d’autant plus que programmer en CIL est très inconfortable.
Les assemblys, modules classes et autres méthodes peuvent parfois nécessiter l’ajout d’attributs qui augmentent le contenu des métadonnées. Il est possible de fournir à du code dynamique de tels objets. Pour cela, on commence par obtenir un constructeur de l’attribut en question. Puis il faut établir l’ensemble des champs/propriétés qu’il doit contenir (si ces données vous sont inconnues, il vous reste .GetProperties()
et .GetFields()
ou la documentation de Microsoft de l’assembly en question, ultimement utiliser DnSpy/IlSpy). Pour cela on utilise les méthodes .GetProperty()
et .GetField()
avec en argument le nom de la propriété/champ. On construit alors un tableau contenant ces objets, et un autre tableau contenant leurs valeurs. On utilise alors la classe System.Reflection.Emit.CustromAttributeBuilder
avec en paramètre le constructeur, puis les éventuels paramètres, ensuite le tableau précédemment construit et enfin les valeurs associés.On applique finalement la méthode .SetCustomAttribute()
avec en paramètre l’objet créé. L’exemple le plus parlant sera dans l’article qui suit celui-ci, sur le PInvoke.
Notre étude de la réflexion touche à sa fin, j’espère que cet article vous aura plu !