Saltar a contenido

Windows APIs

WinAPI

El Windows API es un conjunto de interfaces de programación disponibles en el sistema operativo Windows. Son fácilmente accesibles en lenguajes C y C++, ya que exponen todas las estructuras de datos necesarias, pero también pueden usarse desde una variedad de lenguajes si el programador define esas estructuras de datos y convenciones de llamada (lo veremos cuando revisemos P/Invoke). A menudo verás que se hace referencia a “Windows API” como WinAPIs o Win32 APIs.

El uso de WinAPIs es prácticamente esencial para las operaciones ofensivas en Windows. Acciones como la enumeración de host, creación de procesos, process injection, manipulación de tokens y mucho más, se basan en estas APIs. El conjunto más comúnmente utilizado de WinAPIs son los base services (kernel32.dll) y los advanced services (advapi32.dll).

Además de las WinAPIs, existen las Native APIs. La mayoría de las Native API calls se implementan en ntoskrnl.exe (la Windows kernel image) y se exponen en modo de usuario mediante ntdll.dll. Estas APIs no están diseñadas estrictamente para ser llamadas directamente desde aplicaciones de usuario, y por ello no son tan accesibles como las WinAPIs. Las WinAPIs de nivel superior realmente llaman a estas Native APIs tras bambalinas. Por ejemplo, OpenProcess en kernel32.dll llama a NtOpenProcess en ntdll.dll.

Sin embargo, hay razones de OPSEC por las que nos interesaría llamar directamente a NtOpenProcess en lugar de OpenProcess — algo que descubriremos cuando veamos el userland API hooking. Por ahora, aprenderemos cómo llamar a Win32 APIs básicas.


MessageBox in C++

Para demostrar el uso básico de una Windows API, escribiremos una pequeña aplicación en C++. Abre Visual Studio, crea un nuevo proyecto de tipo C++ Console App y elimina el código de plantilla. Luego, añade #include <Windows.h> en la parte superior. Este archivo de encabezado contiene las declaraciones principales de la Windows API, macros y tipos de datos.

Podemos empezar a teclear "MessageBox" y Intellisense producirá algunas sugerencias.

Hay tantas opciones que puede resultar confuso — ¿quién hubiera pensado que hay tantas maneras de sacar un cuadro de mensaje...? Las sugerencias con íconos de cubo morado son functions y las de ícono azul/blanco de estilo “botón de reproducción” son macros. Estas macros son como atajos para funciones existentes — en este caso, la macro MessageBox apunta a MessageBoxW. También verás que existe MessageBoxA, ¿cuál es la diferencia?

Las funciones con la “A” usan cadenas ANSI y las que tienen “W” usan Unicode. Unicode es la codificación de caracteres preferida en Windows, por eso la macro MessageBox apunta a MessageBoxW por defecto. Si revisas la definición de la función para MessageBoxA y MessageBoxW, verás que MessageBoxA recibe LPCSTR y MessageBoxW recibe LPCWSTR. Si la API también retornara una cadena (MessageBox retorna un int), el tipo de retorno también sería ANSI o Unicode según la versión de la API invocada.

La mejor forma de entender una API, qué parámetros requiere y qué retorna (si aplica) es la documentación oficial de Microsoft en learn.microsoft.com.

El carácter ‘L’ denota un literal wchar_t, que es un tipo de carácter ancho.

Código completo:

#include <Windows.h>

int main()
{
    MessageBox(NULL, L"Hello World!", L"Alert", 0);
    return 0;
}

CreateProcess in C++

La API CreateProcessW requiere utilizar algunas estructuras de datos adicionales, concretamente STARTUPINFOW y PROCESS_INFORMATION. La estructura STARTUPINFOW puede proveer ciertos parámetros sobre cómo se inicia el proceso, y PROCESS_INFORMATION devuelve información sobre el nuevo proceso (por ejemplo, su PID). La mayoría de argumentos pueden ser NULL, con la excepción de los argumentos de línea de comandos del proceso, un apuntador a STARTUPINFO y uno a PROCESS_INFORMATION.

#include <windows.h>
#include <stdio.h>

int main()
{
    LPSTARTUPINFOW       si;
    PPROCESS_INFORMATION pi;
    BOOL                 success;

    si = new STARTUPINFOW();
    si->cb = sizeof(LPSTARTUPINFOW);

    pi = new PROCESS_INFORMATION();

    wchar_t cmd[] = L"notepad.exe\0";

    success = CreateProcess(
        NULL,
        cmd,
        NULL,
        NULL,
        FALSE,
        0,
        NULL,
        NULL,
        si,
        pi);

    if (!success) {
        printf("[x] CreateProcess failed.");
        return 1;
    }

    printf("dwProcessId : %d\n", pi->dwProcessId);
    printf("dwThreadId  : %d\n", pi->dwThreadId);
    printf("hProcess    : %p\n", pi->hProcess);
    printf("hThread     : %p\n", pi->hThread);

    CloseHandle(pi->hThread);
    CloseHandle(pi->hProcess);
}

Observa que CreateProcess es una macro de CreateProcessW.

Si la llamada no es exitosa, podemos imprimir un error y salir. De lo contrario, podemos imprimir los datos que ahora tiene la estructura PROCESS_INFORMATION. Parte de esa información incluye un handle al proceso y al hilo. Es importante recordar cerrar estos handles cuando ya no se necesiten, de lo contrario provocaremos handle leaks en nuestro código.


P/Invoke

Platform Invoke (P/Invoke) nos permite acceder a structs y funciones presentes en bibliotecas no administradas desde código administrado.

Las aplicaciones y bibliotecas escritas en C/C++ se compilan a machine code, y son ejemplos de unmanaged code. Los programadores deben gestionar aspectos como la memoria manualmente, por ejemplo, siempre que asignan memoria, deben recordar liberarla. En contraste, el managed code se ejecuta en un CLR (Common Language Runtime). Lenguajes como C# se compilan a un Intermediate Language (IL) que el CLR convierte más tarde a machine code en tiempo de ejecución. El CLR también maneja aspectos como la recolección de basura y varias verificaciones en tiempo de ejecución, de ahí el nombre managed code.

¿Por qué necesitamos P/Invoke? Tomemos .NET como ejemplo.

El .NET runtime ya usa P/Invoke tras bambalinas y nos provee abstracciones que se ejecutan encima de ello. Por ejemplo, para iniciar un proceso en .NET podemos usar el método Start de la clase System.Diagnostics.Process. Si rastreamos este método en el runtime, veremos que utiliza P/Invoke para llamar a la API CreateProcess. Sin embargo, no ofrece un medio para personalizar los datos que se pasan a la estructura STARTUPINFO; y esto nos impide poder, por ejemplo, iniciar el proceso en estado suspendido.

Existen otras WinAPIs útiles para nosotros (como VirtualAllocEx, WriteProcessMemory, CreateRemoteThread, etc.) que no se exponen en .NET; y la única forma de acceder a ellas es usar P/Invoke manualmente en nuestro código.

Otros lenguajes que soportan P/Invoke también abren un abanico de posibilidades para los atacantes. Por ejemplo, podemos usar P/Invoke en VBA, lo que da bastante potencia a documentos maliciosos de Office.


MessageBox in CSharp

Ahora creemos una nueva C# Console App (.NET Framework). Asegúrate de que sea .NET Framework y no .NET (Core). Por defecto, los proyectos de .NET Framework prefieren ejecutarse como 32 bits, algo que sospecho es una preferencia heredada de los tiempos en que las CPU de 64 bits no eran tan comunes.

Para cambiar esto, ve a Project > Properties > Build y desmarca la casilla Prefer 32-bit.

C# no tiene un archivo de encabezado de Windows como C++; por ello debemos declarar manualmente todas las WinAPIs y estructuras. La API MessageBoxW se declara así mediante el atributo DllImport. Esto le dice al CLR que una función llamada MessageBoxW está exportada desde user32.dll. Retorna un int y espera como parámetros un IntPtr y tres cadenas. Quizá notes que en la definición en C++ se declara el parámetro hWnd como HWND, en lugar de IntPtr. No todos los lenguajes comparten los mismos tipos de datos y, a menudo, necesitan cambiarse o “marshalled” cuando se pasa de managed code a unmanaged code. Veremos esto un poco más en la siguiente sección.

Código completo:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    internal class Program
    {
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        static extern int MessageBoxW(IntPtr hWnd, string lpText, string lpCaption, uint uType);

        static void Main(string[] args)
        {
            MessageBoxW(IntPtr.Zero, "Hello World!", "P/Invoke", 0);
        }
    }
}

Type Marshalling

“Marshalling” es el proceso de transformar un tipo de datos cuando cruza entre managed code y unmanaged code. A menos que se especifique otra cosa, el subsistema de P/Invoke intenta hacer automáticamente ese “marshalling” de datos por ti. En la lección anterior, llamamos a MessageBoxW que recibe dos parámetros de tipo string, y sabemos que necesitaban ser Unicode (LPCWSTR). No tuvimos que hacer nada especial, porque P/Invoke lo hizo por nosotros.

No obstante, puede haber situaciones en las que necesites ser más explícito, para lo cual existen dos métodos. El primero es usando el campo CharSet del atributo DllImport, que es una forma rápida y sencilla de definir cómo deben “marshallearse” todas las cadenas.

La segunda manera es usando el atributo MarshalAs en cada parámetro individual. Esto es más flexible porque no se limita a cadenas. Por ejemplo, podríamos forzar el parámetro uint a “marshallearse” como un entero sin signo de 1, 2, 4 u 8 bytes si fuera necesario.

También podemos “marshallear” cualquier dato retornado por un método P/Invoke usando la palabra clave return, seguida por un atributo MarshalAs.

En este caso, forzaría que los datos se “marshalleen” como un entero de 4 bytes con signo.

Esta página contiene una tabla útil de mapeos de tipos de datos administrados y no administrados. Ten en cuenta que P/Invoke maneja el 99% de los casos sin problema; esta información es más relevante cuando accedes a Native APIs sin P/Invoke (por ejemplo, D/Invoke).


CreateProcess in CSharp

Mover las P/Invoke signatures, structs y enums a su propia clase ayuda a mantener el código principal más limpio y legible. Crea una nueva clase en tu proyecto llamada Win32.cs y copia el siguiente código.

internal static class Win32
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern bool CreateProcessW(
        string applicationName,
        string commandLine,
        IntPtr processAttributes,
        IntPtr threadAttributes,
        bool inheritHandles,
        CREATION_FLAGS creationFlags,
        IntPtr environment,
        string currentDirectory,
        ref STARTUPINFO startupInfo,
        out PROCESS_INFORMATION processInformation);

    [DllImport("kernel32.dll")]
    public static extern bool CloseHandle(IntPtr hObject);

    [Flags]
    public enum CREATION_FLAGS : uint
    {
        DEBUG_PROCESS = 0x00000001,
        DEBUG_ONLY_THIS_PROCESS = 0x00000002,
        CREATE_SUSPENDED = 0x00000004,
        DETACHED_PROCESS = 0x00000008,
        CREATE_NEW_CONSOLE = 0x00000010,
        NORMAL_PRIORITY_CLASS = 0x00000020,
        IDLE_PRIORITY_CLASS = 0x00000040,
        HIGH_PRIORITY_CLASS = 0x00000080,
        REALTIME_PRIORITY_CLASS = 0x00000100,
        CREATE_NEW_PROCESS_GROUP = 0x00000200,
        CREATE_UNICODE_ENVIRONMENT = 0x00000400,
        CREATE_SEPARATE_WOW_VDM = 0x00000800,
        CREATE_SHARED_WOW_VDM = 0x00001000,
        CREATE_FORCEDOS = 0x00002000,
        BELOW_NORMAL_PRIORITY_CLASS = 0x00004000,
        ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000,
        INHERIT_PARENT_AFFINITY = 0x00010000,
        INHERIT_CALLER_PRIORITY = 0x00020000,
        CREATE_PROTECTED_PROCESS = 0x00040000,
        EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
        PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000,
        PROCESS_MODE_BACKGROUND_END = 0x00200000,
        CREATE_SECURE_PROCESS = 0x00400000,
        CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
        CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
        CREATE_DEFAULT_ERROR_MODE = 0x04000000,
        CREATE_NO_WINDOW = 0x08000000,
        PROFILE_USER = 0x10000000,
        PROFILE_KERNEL = 0x20000000,
        PROFILE_SERVER = 0x40000000,
        CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000,
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public int cb;
        public IntPtr lpReserved;
        public IntPtr lpDesktop;
        public IntPtr lpTitle;
        public int dwX;
        public int dwY;
        public int dwXSize;
        public int dwYSize;
        public int dwXCountChars;
        public int dwYCountChars;
        public int dwFillAttribute;
        public int dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public int dwProcessId;
        public int dwThreadId;
    }
}

Luego puedes acceder a esas estructuras usando Win32.xxx en tu clase principal de programa.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // create startup info
            var startupInfo = new Win32.STARTUPINFO();
            startupInfo.cb = Marshal.SizeOf(startupInfo);

            // create process
            var success = Win32.CreateProcessW(
                null,
                "notepad.exe",
                IntPtr.Zero,
                IntPtr.Zero,
                false,
                0,
                IntPtr.Zero,
                null,
                ref startupInfo,
                out var processInfo);

            // bail if it failed
            if (!success)
            {
                Console.WriteLine("[x] CreateProcessW failed");
                return;
            }

            // print process info
            Console.WriteLine("dwProcessId : {0}", processInfo.dwProcessId);
            Console.WriteLine("dwThreadId  : {0}", processInfo.dwThreadId);
            Console.WriteLine("hProcess    : 0x{0:X}", processInfo.hProcess);
            Console.WriteLine("hThread     : 0x{0:X}", processInfo.hThread);

            // close handles
            Win32.CloseHandle(processInfo.hThread);
            Win32.CloseHandle(processInfo.hProcess);
        }
    }
}


Error Handling

No es raro que tus llamadas a la API fallen, ya sea usando P/Invoke o no, así que es importante saber cómo obtener la causa del fallo para ayudarte a depurar. Por ejemplo, podemos forzar a CreateProcessW a fallar indicando una ruta de proceso que no existe.

#include <windows.h>
#include <stdio.h>

int main()
{
    LPSTARTUPINFOW      si;
    PROCESS_INFORMATION pi;
    BOOL                success;

    si = new STARTUPINFOW();
    si->cb = sizeof(LPSTARTUPINFOW);

    wchar_t cmd[] = L"this is a mistake.exe\0";

    success = CreateProcess(
        NULL,
        cmd,
        NULL,
        NULL,
        FALSE,
        0,
        NULL,
        NULL,
        si,
        &pi);

    if (!success) {
        printf("[x] CreateProcess failed with error code: %d\n", GetLastError());
        return 1;
    }

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
}

Después de verificar el valor booleano de retorno, obtenemos el código de error llamando a GetLastError(). Esto producirá la siguiente salida:

[x] CreateProcess failed with error code: 2

La utilidad net se puede usar para “traducir” el número de código a una cadena.

C:\>net helpmsg 2

The system cannot find the file specified.

O buscarlos en el sitio web de Microsoft.

Al usar P/Invoke en C#, debes asegurarte de establecer el campo SetLastError en true.

El código de error se recupera usando Marshal.GetLastWin32Error().

static void Main(string[] args)
{
    var si = new Win32.STARTUPINFO();
    si.cb = Marshal.SizeOf(si);

    var success = Win32.CreateProcessW(
        null,
        "this is a mistake.exe\0",
        IntPtr.Zero,
        IntPtr.Zero,
        false,
        0,
        IntPtr.Zero,
        null,
        ref si,
        out var pi);

    if (!success)
    {
        Console.WriteLine("[x] CreateProcess failed with error code: {0}", Marshal.GetLastWin32Error());
        return;
    }

    // close handles
    Win32.CloseHandle(pi.hThread);
    Win32.CloseHandle(pi.hProcess);
}

En realidad, en C# es bastante fácil obtener el mensaje de error como cadena creando una nueva Win32Exception y pasándole el código de error en el constructor. La propiedad Message de la excepción contendrá el mensaje descriptivo.

if (!success)
{
    var exception = new Win32Exception(Marshal.GetLastWin32Error());
    Console.WriteLine("[x] CreateProcess failed with error: {0}.", exception.Message);
    return;
}
[x] CreateProcess failed with error: The system cannot find the file specified.

NT APIs

Microsoft tiene una historia interesante con el Departamento de Justicia de EE. UU. en relación con sus APIs internas (nativas).

En 1998, Estados Unidos presentó una demanda contra Microsoft alegando violaciones de las leyes antimonopolio federales. Posteriormente se determinó que habían violado partes de la Ley Sherman al acoplar el sistema operativo a Internet Explorer y, por ende, mantener ilegalmente su monopolio en el mercado de navegadores. Si usabas Windows en aquellos días, puede que recuerdes que no era posible desinstalar IE por la ruta habitual del Panel de Control, y forzar su eliminación manualmente rompía funcionalidades vitales.

El tribunal concluyó que esto impedía a los OEMs preinstalar otros navegadores y disuadía a los consumidores de usarlos. Dado que no podían eliminar IE, instalar otro navegador significaba que el OEM incurriría en los costos de dar soporte a dos navegadores. Por tanto, los OEMs no tenían mucho incentivo para instalar un navegador rival. Además de los navegadores, también se planteó la cuestión de otros productos “middleware” como Microsoft Office.

La consecuencia para Microsoft fue que necesitaba permitir más competencia para productos rivales dentro de Windows, y se les impuso una divulgación obligatoria de APIs. La intención era eliminar cualquier tipo de “ventaja” para Microsoft a la hora de escribir software que se comunicara con el sistema operativo. El tribunal quería que los desarrolladores de software de terceros tuvieran el mismo acceso a la API del sistema operativo que Microsoft, para lograr paridad funcional.

Microsoft hizo público el header winternl, que define algunas NT APIs internas y estructuras de datos. Aunque la mayoría opina que ese contenido no encaja del todo con la intención del fallo judicial, este fue el resultado. Aún hay miles de APIs y estructuras que siguen sin documentación oficial (solo disponibles a través de ingeniería inversa de investigadores independientes).

Una de las APIs definidas en este header es NtQueryInformationProcess. Usemos esta API para obtener información sobre nuestro proceso actual.

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

int main()
{
    NTSTATUS                    status;
    PPROCESS_BASIC_INFORMATION  pbi;
    DWORD                       dwSize;

    // call once to get the size
    NtQueryInformationProcess(
        GetCurrentProcess(),
        ProcessBasicInformation,
        NULL,
        0,
        &dwSize);

    // allocate memory
    pbi = (PPROCESS_BASIC_INFORMATION)malloc(dwSize);
    RtlZeroMemory(pbi, dwSize);

    // call again
    status = NtQueryInformationProcess(
        GetCurrentProcess(),
        ProcessBasicInformation,
        pbi,
        sizeof(PROCESS_BASIC_INFORMATION),
        &dwSize);

    if (!NT_SUCCESS(status)) {
        printf("[x] NtQueryInformationProcess failed: %ld.\n", status);
        return 1;
    }

    printf("[+] PEB base address: 0x%p\n", pbi->PebBaseAddress);
}

No obstante, si tratamos de compilar este código fallará con un error de linker: “LNK2019: unresolved external symbol”, porque el Windows SDK no proporciona la import library para ntdll. Microsoft sugiere que lo manejes usando LoadLibrary y GetProcAddress para resolver la función en tiempo de ejecución. Para ello debemos copiar la definición de la función NtQueryInformationProcess como un typedef.

typedef NTSTATUS(NTAPI * NT_QUERY_INFORMATION_PROCESS)(
    IN HANDLE ProcessHandle,
    IN PROCESSINFOCLASS ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN ULONG ProcessInformationLength,
    OUT PULONG ReturnLength OPTIONAL);

Al llamar a GetProcAddress, convertimos el function pointer a nuestro tipo.

HMODULE hNtdll;
NT_QUERY_INFORMATION_PROCESS hNtQueryInformationProcess;

hNtdll = LoadLibrary(L"ntdll.dll\0");
hNtQueryInformationProcess = (NT_QUERY_INFORMATION_PROCESS) GetProcAddress(hNtdll, "NtQueryInformationProcess");

Luego usamos ese function pointer, hNtQueryInformationProcess, para invocar la API.

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS(NTAPI * NT_QUERY_INFORMATION_PROCESS)(
    IN HANDLE ProcessHandle,
    IN PROCESSINFOCLASS ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN ULONG ProcessInformationLength,
    OUT PULONG ReturnLength OPTIONAL);

int main()
{
    NTSTATUS                    status;
    PPROCESS_BASIC_INFORMATION  pbi;
    DWORD                       dwSize;

    HMODULE hNtdll;
    NT_QUERY_INFORMATION_PROCESS hNtQueryInformationProcess;

    hNtdll = LoadLibrary(L"ntdll.dll\0");
    hNtQueryInformationProcess = (NT_QUERY_INFORMATION_PROCESS) GetProcAddress(hNtdll, "NtQueryInformationProcess");

    // call once to get the size
    hNtQueryInformationProcess(
        GetCurrentProcess(),
        ProcessBasicInformation,
        NULL,
        0,
        &dwSize);

    // allocate memory
    pbi = (PPROCESS_BASIC_INFORMATION)malloc(dwSize);
    RtlZeroMemory(pbi, dwSize);

    // call again
    status = hNtQueryInformationProcess(
        GetCurrentProcess(),
        ProcessBasicInformation,
        pbi,
        sizeof(PROCESS_BASIC_INFORMATION),
        &dwSize);

    if (!NT_SUCCESS(status)) {
        printf("[x] NtQueryInformationProcess failed: %ld.\n", status);
        return 1;
    }

    printf("[+] PEB base address: 0x%p\n", pbi->PebBaseAddress);
}

Si tienes instalado el Windows Driver Kit (WDK), entonces puedes usar la import library de ntdll que incluye. Simplemente añade #pragma comment(lib, "ntdll") al inicio de tu archivo fuente y esto elimina la necesidad de hacer dynamic loading.


Ordinals

Abre el binario C# CreateProcess en pestudio y ve a la sección de imports. Verás una bandera de advertencia por un posible uso malicioso de la API CreateProcessW de kernel32.dll.

Referenciar una función exportada mediante ordinals puede engañar a algunas herramientas, como pestudio, para que el binario parezca menos malicioso. Para encontrar el número de ordinal correcto para CreateProcessW, carga C:\Windows\System32\kernel32.dll en PE-bear. Ve a la pestaña Exports y busca la función exportada.

En la columna Ordinal, vemos el valor E9, que en decimal es 233. Usa el modo de programador en una calculadora para convertir rápidamente entre ambos.

En lugar de usar CreateProcessW directamente en el atributo DllImport, podemos usar el número de ordinal con la propiedad EntryPoint y dar a la función un nombre más inocente. Aquí la llamé “TotallyLegitApi”.

Este código seguirá ejecutándose y lanzará notepad.exe en mi caso. Al abrir el nuevo binario en pestudio se verá que la presencia de CreateProcessW ha sido sustituida por TotallyLegitApi. Evidentemente, la herramienta no sabe qué es esta API, así que la bandera de malicioso desaparece.

Cualquier analista familiarizado con las Windows APIs sabrá que esto no es legítimo. Para mayor sigilo, usa una API que sí esté realmente exportada por kernel32.dll. Esto seguirá funcionando porque la propiedad EntryPoint tiene prioridad sobre el nombre de la función.


MessageBox in VBA

Las Invoke function signatures se declaran ligeramente distinto en VBA — en lugar de un atributo DllImport, usamos la directiva Declare. El resto es parecido, en cuanto a que declaramos los parámetros con sus tipos de dato en VBA y el tipo de retorno va al final.

Declare PtrSafe Function MessageBoxW Lib "user32.dll" (ByVal hWnd As LongPtr, ByVal lpText As String, ByVal lpCaption As String, ByVal uType As Integer) As Integer

Invocar esta función puede hacerse en un método de VBA. Puesto que llamamos a la versión Unicode, necesitamos StrConv para convertir las cadenas al formato adecuado.

Sub Demo()
    Dim result As Integer
    result = MessageBoxW(0, StrConv("Hello World", vbUnicode), StrConv("MS Word", vbUnicode), 0)
End Sub


CreateProcess in VBA

Al igual que en C#, primero debemos declarar las funciones y estructuras WinAPI. Una struct se define con la palabra clave Type.

Declare PtrSafe Function CreateProcessW Lib "kernel32.dll" (ByVal lpApplicationName As String, ByVal lpCommandLine As String, ByVal lpProcessAttributes As LongPtr, ByVal lpThreadAttributes As LongPtr, ByVal bInheritHandles As Boolean, ByVal dwCreationFlags As Long, ByVal lpEnvironment As LongPtr, ByVal lpCurrentDirectory As String, lpStartupInfo As STARTUPINFO, lpProcessInformation As PROCESS_INFORMATION) As Boolean

Type STARTUPINFO
    cb As Long
    lpReserved As String
    lpDesktop As String
    lpTitle As String
    dwX As Long
    dwY As Long
    dwXSize As Long
    dwYSize As Long
    dwXCountChars As Long
    dwYCountChars As Long
    dwFillAttribute As Long
    dwFlags As Long
    wShowWindow As Integer
    cbReserved2 As Integer
    lpReserved2 As LongPtr
    hStdInput As LongPtr
    hStdOutput As LongPtr
    hStdError As LongPtr
End Type

Type PROCESS_INFORMATION
    hProcess As LongPtr
    hThread As LongPtr
    dwProcessId As Long
    dwThreadId As Long
End Type

Luego llamamos la API como antes.

Sub Demo()
    Dim startup_info As STARTUPINFO
    Dim process_info As PROCESS_INFORMATION

    Dim nullStr As String

    Dim success As Boolean
    success = CreateProcessW(nullStr, StrConv("notepad.exe", vbUnicode), 0&, 0&, False, 0, 0&, nullStr, startup_info, process_info)
End Sub


D/Invoke

Dynamic Invoke (D/Invoke) es un proyecto de código abierto en C# que pretende ser un reemplazo directo para P/Invoke. Posee varias primitivas potentes que se pueden combinar para hacer cosas muy interesantes, incluyendo:

  • Invocar código no administrado sin P/Invoke.
  • Manually map binarios PE no administrados en memoria y llamar a su entry point o a una función exportada.
  • Generar syscall wrappers para Native APIs.

Por ahora, cambiemos el P/Invoke por D/Invoke.

Ve a Project > Add Reference > Browse y agrega una referencia a DInvoke.Data.dll y DInvoke.DynamicInvoke.dll en C:\Tools\DInvoke\DInvoke.DynamicInvoke\bin\Release\netstandard2.0.

Cambia el atributo DllImport a UnmanagedFunctionPointer y la palabra clave extern por delegate.

[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
public delegate bool CreateProcessWDelegate(
    string applicationName,
    string commandLine,
    IntPtr processAttributes,
    IntPtr threadAttributes,
    bool inheritHandles,
    CREATION_FLAGS creationFlags,
    IntPtr environment,
    string currentDirectory,
    ref STARTUPINFO startupInfo,
    out PROCESS_INFORMATION processInfo);

Los argumentos para la llamada a la API se ponen en un arreglo de objetos y todo se pasa al método DynamicApiInvoke.

object[] parameters =
{
    null, "notepad.exe", IntPtr.Zero, IntPtr.Zero, false, (uint)0,
    IntPtr.Zero, null, startupInfo, new Win32.PROCESS_INFORMATION()
};

var success = (bool)Generic.DynamicApiInvoke(
    "kernel32.dll",
    "CreateProcessW",
    typeof(Win32.CreateProcessWDelegate),
    ref parameters);

Un aspecto a cuidar son los tipos de dato por defecto. El parámetro dwCreationFlags es un DWORD, o uint en C#. Pero si pones 0 tal cual, el compilador asume un int, lo que provocará una excepción en tiempo de ejecución al llamar a la API. Por eso debe hacerse un cast explícito a uint.

Si la llamada tiene éxito, el PROCESS_INFORMATION estará en el índice 9 del arreglo parameters.

var processInfo = (Win32.PROCESS_INFORMATION)parameters[9];

D/Invoke & Ordinals

D/Invoke también es compatible con ordinals, lo cual sirve para ocultar cadenas fácilmente detectables como “CreateProcessW”. Para saber el ordinal correspondiente a una API, necesitamos abrir la DLL en una herramienta como PE-bear. Ve a la pestaña Exports y desplázate hasta que encuentres la API.

Aquí, podemos ver que el ordinal de CreateProcessW es E9 hex (233 en decimal). Después usamos ese valor con GetLibraryAddress para obtener un apuntador a la API y luego invocamos la API con DynamicFunctionInvoke.

var hLibrary = Generic.GetLibraryAddress("kernel32.dll", 233);

var success = (bool)Generic.DynamicFunctionInvoke(
    hLibrary,
    typeof(Win32.CreateProcessWDelegate),
    ref parameters);

D/Invoke API Hashing

Otra forma de evitar el uso de cadenas en D/Invoke es usar hashing. Esta técnica consiste en tomar una cadena, procesarla con una función hash y una clave predeterminada. Ese hash luego se usa en el código en lugar del literal de la DLL o el nombre de la API.

La forma más sencilla de generar estos hashes es con D/Invoke dentro de CSharpREPL. Simplemente importa DynamicInvoke.dll y llama a GetApiHash.

Al escribir tu código, GetLoadedModuleAddress y GetExportAddress tienen overloads que aceptan una cadena con hash y la clave usada.

La API luego se puede ejecutar con DynamicFunctionInvoke como en el ejemplo anterior.