ethical.blue Magazine

// Cybersecurity clarified.

Common Intermediate Language i kod generowany dynamicznie dla .NET

...
Assembly Language // // Dawid Farbaniec
Ostatnia modyfikacja:

Spis treści

Podstawowe pojęcia

Kod zarządzany (ang. managed) i niezarządzany (ang. unsafe)

Kod zarządzany (ang. managed code) nie jest tylko ładowany do pamięci i uruchamiany. Infrastruktura, która zarządza wykonaniem takiego kodu ma dostęp do metadanych opisujących metody, nadzoruje operacje na stosie czy zajmuje się obsługą wyjątków. Dane w kodzie zarządzanym też nie są pozostawione same sobie. Infrastruktura CLI zajmuje się rezerwacją pamięci oraz usuwaniem niepotrzebnych obiektów w procesie nazywanym odśmiecaniem pamięci (ang. garbage collection).

Typy danych w języku pośrednim CIL

Poniżej przedstawiono typy danych w języku CIL oraz ich odpowiedniki w wysokopoziomowej składni w języku C#.

Asembler IL
C#
Opis
bool
System.Boolean, bool
Typ logiczny
char
System.Char, char
Typ znakowy Unicode (16 bitów)
object
System.Object, object
Obiekt lub opakowane dane typu prostego
string
System.String, string
Napis Unicode
float32
System.Single, float
Liczba zmiennoprzecinkowa (32 bity, IEC 60559:1989)
float64
System.Double, double
Liczba zmiennoprzecinkowa (64 bity, IEC 60559:1989)
int8
System.SByte, sbyte
Liczba całkowita ze znakiem (8 bitów)
int16
System.Int16, short
Liczba całkowita ze znakiem (16 bitów)
int32
System.Int32, int
Liczba całkowita ze znakiem (32 bity)
int64
System.Int64, long
Liczba całkowita ze znakiem (64 bity)
native int
System.IntPtr, nint
Liczba całkowita ze znakiem (rozmiar natywny)
native unsigned int
System.UIntPtr, nuint
Liczba całkowita bez znaku (rozmiar natywny)
typedref
System.TypedReference
Wskaźnik określonego typu
unsigned int8
System.Byte, byte
Liczba całkowita bez znaku (8 bitów)
unsigned int16
System.UInt16, ushort
Liczba całkowita bez znaku (16 bitów)
unsigned int32
System.UInt32, uint
Liczba całkowita bez znaku (32 bity)
unsigned int64
System.UInt64, ulong
Liczba całkowita bez znaku (64 bity)

Typy danych bezpośrednio wspierane przez infrastrukture CLI

Poniżej przedstawiono typy danych bezpośrednio wspierane przez infrastrukture CLI.

Typ danych
Opis
int8
Liczba całkowita ze znakiem (U2, 8 bitów)
unsigned int8
Liczba całkowita bez znaku (8 bitów)
int16
Liczba całkowita ze znakiem (U2, 16 bitów)
unsigned int16
Liczba całkowita bez znaku (16 bitów)
int32
Liczba całkowita ze znakiem (U2, 32 bity)
unsigned int32
Liczba całkowita bez znaku (32 bity)
int64
Liczba całkowita ze znakiem (U2, 64 bity)
unsigned int64
Liczba całkowita bez znaku (64 bity)
float32
Liczba zmiennoprzecinkowa (32 bity, IEC 60559:1989)
float64
Liczba zmiennoprzecinkowa (64 bity, IEC 60559:1989)
native int
Liczba całkowita ze znakiem (U2, rozmiar natywny)
native unsigned int
Liczba całkowita bez znaku (rozmiar natywny) lub wskaźnik niezarządzany
F
Wewnętrzny typ dla liczb zmiennoprzecinkowych (rozmiar natywny)
O
Odwołanie do obiektu w pamięci zarządzanej (rozmiar natywny)
&
Wskaźnik do pamięci zarządzanej (rozmiar natywny)
*
Wewnętrzny wskaźnik do pamięci niezarządzanej używany tylko wewnątrz ciała danej metody

Narzędzie typu asembler dla języka pośredniego platformy .NET

W celu zbudowania pliku wykonywalnego *.exe lub *.dll z kodu źródłowego w Asemblerze CIL można użyć narzędzia ilasm.exe. Należy w tym celu uruchomić Developer PowerShell for Visual Studio.

...
Narzędzie ILAsm.exe (IL Assembler)

Kod źródłowy, który wyświetla proste okno dialogowe typu MessageBox przedstawiono poniżej.

Code:
.assembly extern mscorlib { }
.assembly Hello { }
.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .custom instance void [System.Runtime]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 8
    ldstr "Spreading knowledge like a virus."
    ldstr "ethical.blue Magazine"
    ldc.i4.0
    ldc.i4.s 64
    call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
        [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(
            string, string,
            valuetype [System.Windows.Forms]System.Windows.Forms.MessageBoxButtons,
            valuetype [System.Windows.Forms]System.Windows.Forms.MessageBoxIcon
        )
    pop
    ret
}
Download:

W celu zbudowania pliku wykonywalnego *.exe można użyć następującego polecenia:

ilasm hello.il /X64 /SUBSYSTEM=2

Narzędzie typu asembler to ilasm.exe, hello.il to nazwa pliku z kodem źródłowym, /X64 to docelowa architektura, a /SUBSYSTEM=2 oznacza program z interfejsem graficznym użytkownika (GUI). Stała o wartości dwa oznacza dokładnie ustawienie IMAGE_SUBSYSTEM_WINDOWS_GUI w słowie Subsystem w nagłówku _IMAGE_OPTIONAL_HEADER pliku wykonywalnego PE.

...
Przykładowy program w Asemblerze IL

Narzędzie typu dezasembler dla kodu bajtowego platformy .NET

W celu odzyskania kodu źródłowego w języku pośrednim CIL z pliku PE można użyć narzędzia typu dezasembler np. ildasm.exe.

...
Narzędzie ILDAsm.exe (IL Disassembler)

Lista kodów operacyjnych CIL (ang. opcode)

Kody operacyjne to binarna forma rozkazów dla maszyny wirtualnej VES. Poniżej przedstawiono listę kodów operacyjnych wraz z odpowiadającą im tekstową formą określaną terminem mnemonik.

Instrukcje CIL (0x00 – 0x0F)
Kod operacyjny Rozkaz
0x00 nop
0x01 break
0x02 ldarg.0
0x03 ldarg.1
0x04 ldarg.2
0x05 ldarg.3
0x06 ldloc.0
0x07 ldloc.1
0x08 ldloc.2
0x09 ldloc.3
0x0A stloc.0
0x0B stloc.1
0x0C stloc.2
0x0D stloc.3
0x0E ldarg.s
0x0F ldarga.s
Instrukcje CIL (0x10 – 0x1F)
Kod operacyjny Rozkaz
0x10 starg.s
0x11 ldloc.s
0x12 ldloca.s
0x13 stloc.s
0x14 ldnull
0x15 ldc.i4.m1
0x16 ldc.i4.0
0x17 ldc.i4.1
0x18 ldc.i4.2
0x19 ldc.i4.3
0x1A ldc.i4.4
0x1B ldc.i4.5
0x1C ldc.i4.6
0x1D ldc.i4.7
0x1E ldc.i4.8
0x1F ldc.i4.s
Instrukcje CIL (0x20 – 0x2F)
Kod operacyjny Rozkaz
0x20 ldc.i4
0x21 ldc.i8
0x22 ldc.r4
0x23 ldc.r8
0x25 dup
0x26 pop
0x27 jmp
0x28 call
0x29 calli
0x2A ret
0x2B br.s
0x2C brfalse.s
0x2D brtrue.s
0x2E beq.s
0x2F bge.s
Instrukcje CIL (0x30 – 0x3F)
Kod operacyjny Rozkaz
0x30 bgt.s
0x31 ble.s
0x32 blt.s
0x33 bne.un.s
0x34 bge.un.s
0x35 bgt.un.s
0x36 ble.un.s
0x37 blt.un.s
0x38 br
0x39 brfalse
0x3A brtrue
0x3B beq
0x3C bge
0x3D bgt
0x3E ble
0x3F blt
Instrukcje CIL (0x40 – 0x4F)
Kod operacyjny Rozkaz
0x40 bne.un
0x41 bge.un
0x42 bgt.un
0x43 ble.un
0x44 blt.un
0x45 switch
0x46 ldind.i1
0x47 ldind.u1
0x48 ldind.i2
0x49 ldind.u2
0x4A ldind.i4
0x4B ldind.u4
0x4C ldind.i8
0x4D ldind.i
0x4E ldind.r4
0x4F ldind.r8
Instrukcje CIL (0x50 – 0x5F)
Kod operacyjny Rozkaz
0x50 ldind.ref
0x51 stind.ref
0x52 stind.i1
0x53 stind.i2
0x54 stind.i4
0x55 stind.i8
0x56 stind.r4
0x57 stind.r8
0x58 add
0x59 sub
0x5A mul
0x5B div
0x5C div.un
0x5D rem
0x5E rem.un
0x5F and
Instrukcje CIL (0x60 – 0x6F)
Kod operacyjny Rozkaz
0x60 or
0x61 xor
0x62 shl
0x63 shr
0x64 shr.un
0x65 neg
0x66 not
0x67 conv.i1
0x68 conv.i2
0x69 conv.i4
0x6A conv.i8
0x6B conv.r4
0x6C conv.r8
0x6D conv.u4
0x6E conv.u8
0x6F callvirt
Instrukcje CIL (0x70 – 0x7F)
Kod operacyjny Rozkaz
0x70 cpobj
0x71 ldobj
0x72 ldstr
0x73 newobj
0x74 castclass
0x75 isinst
0x76 conv.r.un
0x79 unbox
0x7A throw
0x7B ldfld
0x7C ldflda
0x7D stfld
0x7E ldsfld
0x7F ldsflda
Instrukcje CIL (0x80 – 0x8F)
Kod operacyjny Rozkaz
0x80 stsfld
0x81 stobj
0x82 conv.ovf.i1.un
0x83 conv.ovf.i2.un
0x84 conv.ovf.i4.un
0x85 conv.ovf.i8.un
0x86 conv.ovf.u1.un
0x87 conv.ovf.u2.un
0x88 conv.ovf.u4.un
0x89 conv.ovf.u8.un
0x8A conv.ovf.i.un
0x8B conv.ovf.u.un
0x8C box
0x8D newarr
0x8E ldlen
0x8F ldelema
Instrukcje CIL (0x90 – 0x9F)
Kod operacyjny Rozkaz
0x90 ldelem.i1
0x91 ldelem.u1
0x92 ldelem.i2
0x93 ldelem.u2
0x94 ldelem.i4
0x95 ldelem.u4
0x96 ldelem.i8
0x97 ldelem.i
0x98 ldelem.r4
0x99 ldelem.r8
0x9A ldelem.ref
0x9B stelem.i
0x9C stelem.i1
0x9D stelem.i2
0x9E stelem.i4
0x9F stelem.i8
Instrukcje CIL (0xA0 – 0xC3)
Kod operacyjny Rozkaz
0xA0 stelem.r4
0xA1 stelem.r8
0xA2 stelem.ref
0xA3 ldelem
0xA4 stelem
0xA5 unbox.any
0xB3 conv.ovf.i1
0xB4 conv.ovf.u1
0xB5 conv.ovf.i2
0xB6 conv.ovf.u2
0xB7 conv.ovf.i4
0xB8 conv.ovf.u4
0xB9 conv.ovf.i8
0xBA conv.ovf.u8
0xC2 refanyval
0xC3 ckfinite
Instrukcje CIL (0xC6 – 0xDE)
Kod operacyjny Rozkaz
0xC6 mkrefany
0xD0 ldtoken
0xD1 conv.u2
0xD2 conv.u1
0xD3 conv.i
0xD4 conv.ovf.i
0xD5 conv.ovf.u
0xD6 add.ovf
0xD7 add.ovf.un
0xD8 mul.ovf
0xD9 mul.ovf.un
0xDA sub.ovf
0xDB sub.ovf.un
0xDC endfinally
0xDD leave
0xDE leave.s
Instrukcje CIL (0xDF – 0xFE 0x0E)
Kod operacyjny Rozkaz
0xDF stind.i
0xE0 conv.u
0xFE 0x00 arglist
0xFE 0x01 ceq
0xFE 0x02 cgt
0xFE 0x03 cgt.un
0xFE 0x04 clt
0xFE 0x05 clt.un
0xFE 0x06 ldftn
0xFE 0x07 ldvirtftn
0xFE 0x09 ldarg
0xFE 0x0A ldarga
0xFE 0x0B starg
0xFE 0x0C ldloc
0xFE 0x0D ldloca
0xFE 0x0E stloc
Instrukcje CIL (0xFE 0x0F – 0xFE 0x1E)
Kod operacyjny Rozkaz
0xFE 0x0F localloc
0xFE 0x11 endfilter
0xFE 0x12 unaligned.
0xFE 0x13 volatile.
0xFE 0x14 tail.
0xFE 0x15 Initobj
0xFE 0x16 constrained.
0xFE 0x17 cpblk
0xFE 0x18 initblk
0xFE 0x19 no.
0xFE 0x1A rethrow
0xFE 0x1C sizeof
0xFE 0x1D Refanytype
0xFE 0x1E readonly.
 
 

Opisy instrukcji języka pośredniego (CIL)

Opis każdej instrukcji CIL składa się z następujących elementów. Najpierw jest nazwa instrukcji z klasy OpCodes przestrzeni nazw System.Reflection.Emit, a dalej w nawiasie mnemonik z argumentami oraz po przecinku kod operacyjny z argumentami. Należy zaznaczyć, że nie wszystkie instrukcje przyjmują argumenty.

Nazwa (C#)
Mnemonik (IL)
Argumenty
Kod operacyjny
Nop
nop
-
0x00
Ldc_R8
ldc.r8
num
0x23 <float64>
Switch
switch
(IL_0001, IL_0002 ... IL_000N)
0x45 <unsigned int32> <int32>...<int32>
...
...
...
...

Lista instrukcji CIL wraz z opisem:

Generowanie kodu CIL z użyciem klasy ILGenerator

Dynamicznie generowany kod znajduje szerokie zastosowanie w wirusach komputerowych i złośliwym oprogramowaniu (ang. malware). Może on utrudniać analitykom badanie zachowania próbki, a programom obronnym identyfikację i oznaczenie próbki sygnaturą w postaci wzorca binarnego. Poniżej przedstawiono kod źródłowy przykładowego programu, który podczas działania tworzy dynamiczną metodę i umieszcza w niej instrukcje języka pośredniego CIL. Poszczególne rozkazy są dodawane do strumienia instrukcji za pomocą metody void ILGenerator.Emit(OpCode opcode);. Przykładowa metoda utworzona dynamicznie przyjmuje dwa argumenty w postaci liczb całkowitych, wykonuje dodawanie i zwraca w wyniku sumę tych liczb.

Po uruchomieniu przykładowego kodu na konsoli tekstowej powinno się wyświetlić  2 + 1 = 3.

Kod źródłowy przykładu tworzenia dynamicznej metody zaprezentowano poniżej.

Code:
using System.Reflection.Emit;

internal class Program
{
    private static void Main()
    {
        DynamicMethod add = new("Add", typeof(int), [typeof(int), typeof(int)]);
        ILGenerator generator = add.GetILGenerator();

        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldarg_1);
        generator.Emit(OpCodes.Add);
        generator.Emit(OpCodes.Ret);

        var c = add.Invoke(null, [2, 1]) as int?;

        Console.WriteLine($"2 + 1 = {c}");
    }
}
Download:

Dynamiczna kompilacja kodu źródłowego w języku C# za pomocą klasy CSharpCompilation

Podmioty zagrażające (ang. threat actor) mogą próbować dostarczać kod ładunku (ang. payload) w postaci nieskompilowanej w celu utrudnienia wykrycia. Metody zabezpieczeń ukierunkowane na pliki zawierające kod binarny mogą być wtedy nieskuteczne. Z tego powodu kolejny edukacyjny przykład to kompilacja kodu źródłowego w języku C# podczas działania aplikacji (ang. runtime). W celu przejrzystego zaprezentowania idei działania przykład nie zawiera zaciemnienia (ang. obfuscation). Przykładowa aplikacja kompiluje kod źródłowy C# do modułu (ang. assembly), który później jest ładowany do pamięci za pomocą Assembly.Load(...);, po czym następuje wywołanie metody głównej Main.

Code:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;

internal class Program
{
    private static void Main()
    {
        string code = "internal class Program { private static void Main() { System.Console.WriteLine(\"ethical.blue Magazine\"); } }";

        var root = Path.GetDirectoryName(typeof(object).Assembly.Location) ?? string.Empty;
        var tree = SyntaxFactory.ParseSyntaxTree(code);
        var compilation = CSharpCompilation.Create(Path.GetRandomFileName())
            .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication,
                optimizationLevel: OptimizationLevel.Release))
            .AddReferences(MetadataReference.CreateFromFile(Path.Combine(root, "System.dll")))
            .AddReferences(MetadataReference.CreateFromFile(Path.Combine(root, "System.Runtime.dll")))
            .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
            .AddReferences(MetadataReference.CreateFromFile(typeof(Console).Assembly.Location))
            .AddSyntaxTrees(tree);

        using MemoryStream ILCodeStream = new();
        var result = compilation.Emit(ILCodeStream);
        if (result.Success)
        {
            var assembly = Assembly.Load(ILCodeStream.ToArray());
            _ = assembly.EntryPoint?.Invoke(null, []);
        }

#if DEBUG
        foreach (var error in result.Diagnostics)
            Console.WriteLine(error.ToString());
#endif
    }
}
Download:

Wykonanie surowych bajtów poprzez utworzenie delegatu ze wskaźnika do pamięci niezarządzanej

Program dla platformy .NET oprócz kodu zarządzanego (ang. managed) posiada także możliwość wywoływania natywnych funkcji systemowych (np. Windows API). Może też uruchamiać ładunki (ang. payload), które są kodem maszynowym dla architektury sprzętowej na której działa aplikacja. W prosty i niegroźny sposób prezentuje to poniższy przykład.

Code:
using System.Runtime.InteropServices;

internal class Program
{
    internal static uint MEM_COMMIT = 0x1000;
    internal static uint PAGE_EXECUTE_READWRITE = 0x40;

    [DllImport("kernel32.dll")]
    static extern unsafe IntPtr VirtualAlloc(
        IntPtr lpAddress, int dwSize, uint flAllocationType, uint flProtect);

    internal delegate int BytesLauncher();

    static void Main()
    {
        byte[] rawBytes = [ 0x90, 0x90, 0xC3 ];

        var payload = VirtualAlloc(IntPtr.Zero, rawBytes.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

        Marshal.Copy(rawBytes, 0, payload, rawBytes.Length);

        if (Marshal.GetDelegateForFunctionPointer(
            payload, typeof(BytesLauncher)) is BytesLauncher ExecutePayload)
        {
            _ = ExecutePayload();
        }
    }
}
Download:

Wykaz literatury

  1. [1] https://ecma-international.org/publications-and-standards/standards/ecma-335/ [dostęp: 2024-04-03]
  2. [2] https://ecma-international.org/publications-and-standards/standards/ecma-334/ [dostęp: 2024-04-03]
  3. [3] https://standards.iso.org/ittf/PubliclyAvailableStandards/c075178_ISO_IEC_23270_2018.zip [dostęp: 2024-04-03]
  4. [4] https://standards.iso.org/ittf/PubliclyAvailableStandards/c058046_ISO_IEC_23271_2012(E).zip [dostęp: 2024-04-03]