Windows: cómo separar en el código «solo los procesos que necesitan permisos de administrador»

· Actualizado el: · · Desarrollo Windows, Seguridad, UAC, C# / .NET, Win32

En el artículo anterior, «Lista de comprobación para mantener un mínimo de seguridad en el desarrollo de apps Windows», planteé la línea de mantener asInvoker como base y separar únicamente los procesos que necesitan permisos de administrador.

En este artículo profundizo en esa parte hasta llegar a cómo escribirlo realmente.

En una app de Windows, no es posible «ejecutar como administrador» de forma selectiva solo una parte del código dentro del mismo proceso. La elevación es un asunto de límites de proceso, así que lo que se necesita es un diseño que extraiga solo ese proceso a otra unidad de ejecución.

En este artículo avanzo en el siguiente orden.

  1. Primero, los supuestos de partida
  2. Qué modelo de separación elegir
  3. La forma más práctica: asInvoker + un helper EXE de administrador
  4. Trampas que no conviene pasar por alto al implementarlo
  5. Ejemplos de código concretos

Los ejemplos de código parten de una app de escritorio de .NET 8 para Windows. El framework de UI puede ser WPF, WinForms o WinUI; la única diferencia notable estaría en los controladores de eventos del lado de la UI.

El código que aparece en este artículo está publicado en GitHub como un conjunto de muestras que se puede compilar y ejecutar (la biblioteca de contrato común, las demos de la UI y del helper de administrador, y pruebas unitarias que también se pueden ejecutar en Linux).

windows-admin-broker-deep-dive - komurasoft-blog-samples (GitHub)

Cómo leer este artículo

Como es un artículo largo, primero dejo una guía de navegación.

Lo que quiere saber Dónde leer
Solo cómo elegir el modelo de separación capítulos 1 a 4 (conclusión, comparación de los 4 modelos, la forma recomendada)
Las decisiones de diseño y sus motivos capítulo 5 (allowlist, rutas fijas, runas, ACL del pipe, verificación de PID)
El código de la implementación capítulos 7 a 14 (estructura, manifiestos, contrato común, lado de la UI, lado del helper)
Cómo comprobar que la separación es correcta 15.6
Formas que no se deben usar capítulo 16

El código completo también está en la muestra de GitHub, igual que aquí. Si solo quiere quedarse con la parte de diseño, puede leer los capítulos 1 a 5 y 15 a 16; si quiere llegar hasta la implementación, puede leer todo de corrido.

Qué preparar antes de probarlo

Para ejecutar esta muestra realmente, necesita lo siguiente.

  • Una máquina Windows (el prompt de elevación de UAC, la ACL explícita mediante PipeSecurity, GetNamedPipeClientProcessId y la escritura en HKLM son todas exclusivas de Windows)
  • .NET 8 SDK o posterior
  • Una cuenta que pueda aprobar la elevación. Con una cuenta de administrador aparece un consent prompt; con un usuario estándar, un credential prompt. Si quiere comprobar ambas rutas, prepare las dos cuentas
  • Una máquina en la que se pueda modificar HKLM. La muestra crea HKLM\SOFTWARE\Classes\*\shell\MyApp.Open a nivel de máquina. Es más seguro probarlo en una VM de evaluación que en su equipo de desarrollo habitual

Los comandos concretos de compilación y ejecución están recogidos en el README de la muestra. Tenga en cuenta de antemano solo este punto: hay que publicar (publish) la UI y el helper en la misma carpeta antes de ejecutarlos (porque el helper resuelve de forma fija el MyApp.exe situado en su misma carpeta).

1. La conclusión primero

Antes que nada, enumero el punto de equilibrio práctico.

  • La app de UI normal se ejecuta siempre en asInvoker
  • Los procesos que necesitan permisos de administrador se extraen a un EXE aparte
  • Ese helper EXE se configura como requireAdministrator
  • El inicio se realiza con runas
  • La comunicación con el helper usa un pipe con nombre u otro IPC, no la entrada/salida estándar, que se lleva mal con runas
  • Lo que se pasa al helper no es una «cadena de comando en bruto», sino únicamente solicitudes tipadas
  • En el lado del helper se vuelve a validar el contenido de la solicitud
  • El origen de la conexión IPC se restringe por el SID del usuario que llama y el PID esperado

“Ejecutar todo como administrador es cómodo” solo lo es la primera vez. Más adelante, en temas como el UAC, arrastrar y soltar, el diseño del registro de logs, las entradas externas, la operación de soporte, la carga de DLL o dónde se guarda la configuración, casi siempre acaba trayendo problemas.

2. Aclarando el punto de partida: no se puede convertir en administrador solo una parte del mismo proceso

El UAC de Windows no se controla por «elevación a nivel de función», sino según «con qué token/nivel de integridad se ejecuta el proceso». Las apps que necesitan un token de acceso de administrador son objeto del prompt de elevación, y los procesos hijos heredan el token con el mismo nivel de integridad que el padre. Es decir, no se puede diseñar un sistema en el que, dentro de un proceso de UI no elevado, de repente un método concreto se ejecute con permisos de administrador. Cuando se necesite, hay que usar otra unidad de ejecución: un proceso independiente, un servicio, una tarea o un objeto COM elevado.

Si se ignora esta premisa, se termina en una consulta de diseño un poco desafortunada: «quiero ser administrador solo en el instante en que se pulsa este botón». Windows no resuelve eso por arte de magia.

2.1 Fijar primero la correspondencia de los integrity level (niveles de integridad)

En el resto del artículo aparecerán las expresiones medium integrity / high integrity. Fijemos antes su correspondencia.

Nivel de integridad Significado en este artículo Ejemplo
medium Proceso que se ejecuta como usuario estándar App de UI en asInvoker
high Proceso elevado Helper EXE en requireAdministrator

El Mandatory Integrity Control de Windows define cuatro niveles -low, medium, high y system-, y el usuario estándar recibe medium mientras que el usuario elevado recibe high. Es decir, el diseño de este artículo consiste en trazar una línea explícita entre «el proceso de UI en medium» y «el proceso helper en high» y hacer que se comuniquen a través de ella.

Si tiene clara esta correspondencia, tanto la explicación de CurrentUserOnly en 5.6 como la de 16.4 se leerán con naturalidad.

3. Qué modelo de separación elegir

Microsoft Learn menciona principalmente los siguientes cuatro métodos para separar las apps que necesitan permisos de administrador.

Modelo Forma general Escenario adecuado
Administrator Broker Model App de UI de usuario estándar + helper EXE de administrador Las operaciones de administrador son esporádicas y basta con mostrar el UAC en el momento necesario
Operating System Service Model UI de usuario estándar + servicio en segundo plano Funciones administrativas que operan constantemente, monitorización en segundo plano, procesos desatendidos
Elevated Task Model UI de usuario estándar + tarea programada con permisos de administrador Procesos rutinarios breves que terminan de una sola vez
Administrator COM Object Model UI de usuario estándar + COM elevado Cuando ya existe un diseño COM y la funcionalidad está bastante limitada

Estas son las pautas para elegir.

3.1 Lo primero que conviene considerar es un broker EXE

El broker EXE encaja bien con operaciones como estas.

  • Registrar o eliminar la integración con el Explorador de archivos
  • Cambios de configuración a nivel de máquina bajo HKLM
  • Registrar o eliminar el servicio de la propia app
  • Añadir o eliminar reglas del firewall
  • Operaciones de administrador bajo Program Files

Estas operaciones normalmente no son necesarias y suelen requerirse solo cuando se pulsa un botón concreto de la pantalla de configuración. En este caso, en lugar de recurrir a un servicio en segundo plano, es más natural iniciar un helper EXE de administrador una sola vez y que termine ahí.

3.2 Se elige un servicio cuando es «constante», «desatendido» y «frecuente»

El servicio es un modelo en el que la app de usuario estándar se comunica con él mediante RPC u otros mecanismos. Su ventaja es que puede recibir procesos administrativos sin prompt de elevación, pero a cambio aumenta la responsabilidad de operar un proceso en segundo plano.

El servicio encaja con usos como estos.

  • Monitorización constante
  • Recopilación de logs
  • Actualizaciones en segundo plano
  • Integración constante con dispositivos o daemons
  • Funciones administrativas compartidas entre varias sesiones de UI

3.3 La tarea programada conviene para «procesos rutinarios breves»

El Elevated Task Model consiste en que la app de usuario estándar inicia una tarea programada que se ejecuta con permisos de administrador. Es más ligero que un servicio y se cierra al terminar, por lo que encaja con trabajos rutinarios que se ejecutan una vez cada vez.

3.4 El COM elevado es bastante limitado

El COM elevation moniker parece útil a primera vista, pero sus casos de uso son limitados. Microsoft Learn también indica que la UI capaz de controlar el COM elevado debe presentarla el propio lado COM, por lo que no está pensado para que «una UI no elevada haga lo que quiera con un COM elevado».

4. La recomendación de este artículo: UI en asInvoker + helper EXE en requireAdministrator

A partir de aquí, concreto la forma más práctica de usar.

Frontera de elevación entre la UI y el helper de administradorDiagrama que muestra cómo la UI en medium integrity permanece sin elevar y lanza con runas un helper en high integrity, que crea el pipe con nombre, valida el SID y el PID de origen, y solo entonces opera sobre el objetivo fijo que requiere permisos de administrador.high integrity — proceso elevado de vida cortamedium integrity — no se eleva en ningún momentoInicia con ruta absoluta + Verb=runas (aquí aparece el prompt de UAC)Solicitud tipada (no se pasa una cadena de comando en bruto)MyApp.AdminBroker.exe (requireAdministrator)Punto de recepción del pipe con nombre: solo permite conectar al SID del usuario de la UI y también coteja el PID de origenDistribuye según la allowlist de operaciones y vuelve a validar los argumentos en el helperMyApp.exe (asInvoker): solo recibe la interacción del usuario y arma la solicitudObjetivo fijo que requiere permisos de administrador: claves bajo HKLM, registro de servicios o reglas de firewall

Figura 1: Hacer coincidir el límite de elevación con el límite de proceso. El lado de la UI permanece en medium, y solo el helper se ejecuta en high durante un tiempo breve.

Hay tres puntos clave.

  1. El proceso de la UI permanece sin elevar hasta el final
  2. El helper de administrador tiene una vida corta
  3. Las operaciones que acepta el helper se limitan a una allowlist fija

Con solo respetar estos tres puntos, el diseño se ordena bastante.

5. Reglas que no conviene pasar por alto en la implementación

Aquí conviene decidir antes de escribir el código.

5.1 No convertir al helper en un «todoterreno»

Este es un mal ejemplo.

  • Pasar de la UI al helper un reg add ... entero como cadena
  • Pasar de la UI al helper un sc.exe ... entero como cadena
  • Pasar de la UI al helper una ruta de registro o de EXE arbitraria

Si hace esto, cuando la UI se rompa, el helper se romperá con ella. El helper de administrador está dentro del límite de elevación. Crear ahí «un punto por el que se puede ejecutar cualquier cosa» es bastante peligroso.

Una buena forma es esta.

  • set-explorer-context-menu
  • install-service
  • add-firewall-rule

Fijar la propia operación de este modo, y limitar los argumentos necesarios a bool / enum / números / cadenas restringidas.

5.2 La path que se pasa al helper debe ser absolute, y sin que la UI decida de más

El propio helper EXE que se inicia con runas se especifica con ruta absoluta. Se evita depender de la búsqueda por PATH o de rutas relativas.

Además, el objetivo que ejecuta el helper también se resuelve de forma fija, en la medida de lo posible, en el propio helper. En esta muestra, el EXE de destino que se registra en el menú contextual del Explorador se fija al MyApp.exe situado en la misma carpeta que el helper.

5.3 Si usa Verb="runas", especifique explícitamente UseShellExecute=true

En .NET, ProcessStartInfo.Verb solo funciona cuando UseShellExecute=true. Además, el valor predeterminado de UseShellExecute difiere entre .NET Framework y .NET Core / .NET. Si deja esto en manos del valor predeterminado, más adelante puede aparecer el molesto accidente de «funciona en un entorno y en otro no».

Por eso, aquí hay que especificarlo siempre de forma explícita.

5.4 runas se lleva mal con la redirección de la entrada/salida estándar

Al establecer UseShellExecute=true, la comunicación basada en la redirección de la entrada/salida estándar deja de ser cómoda de usar. Por eso, para el intercambio con el helper es más natural usar otro IPC, como un named pipe.

5.5 El pipe con nombre no debe depender de la ACL predeterminada

Con el descriptor de seguridad predeterminado, un pipe con nombre concede por defecto permiso de lectura a Everyone o a usuarios anónimos. Usar eso tal cual para el IPC de un helper de administrador es bastante descuidado.

Es mejor configurar siempre un PipeSecurity explícito.

5.6 PipeOptions.CurrentUserOnly no se usa en este caso

A primera vista parece cómodo. Pero en Windows, CurrentUserOnly comprueba no solo la cuenta de usuario, sino también el nivel de elevación. Es decir, no es adecuado para la comunicación entre una UI no elevada y un helper elevado.

Además, quién se conecta al pipe y con qué token depende del tipo de prompt de UAC. Conviene fijar esto en una tabla para poder seguir por qué hace falta una ACL explícita.

Cuenta con la que se ejecuta la UI Prompt de UAC que aparece Cuenta con la que se ejecuta el helper WindowsIdentity.GetCurrent() del helper que crea el pipe SID de la UI que se conecta
Cuenta de administrador (no elevada) consent prompt (basta con pulsar «Sí») El token elevado de el mismo usuario El mismo usuario que la UI El usuario de la UI
Usuario estándar credential prompt (se introducen credenciales de otra cuenta) La otra cuenta de administrador que se introdujo Un usuario distinto al de la UI El usuario de la UI

Así se leen las filas.

  • En la fila superior, «el usuario actual del helper = el usuario de la UI», así que aunque el helper construya la ACL mirando solo su propio SID, por casualidad se conecta bien
  • En la fila inferior, el usuario actual del helper y el usuario de la UI son personas distintas. Si se construye la ACL mirando solo WindowsIdentity.GetCurrent(), el usuario original de la UI queda sin poder enviar su propia solicitud
  • En ambas filas, CurrentUserOnly se rechaza por la diferencia de nivel de elevación entre «la UI en medium» y «el helper en high»

Es decir, la única forma que funciona en ambas filas es «que la UI pase su SID y se conceda el permiso de conexión a ese SID».

Por eso, en este caso se hace lo siguiente.

  • La UI obtiene su propio SID y se lo pasa al helper
  • El helper concede el permiso de conexión al pipe solo al SID del usuario de la UI
  • Además, se verifica el PID de origen con GetNamedPipeClientProcessId

5.7 La verificación del PID es una defensa adicional para reducir «intrusiones descuidadas»

Con solo un nombre de pipe aleatorio ya se mejora bastante, pero no es nula la posibilidad de que otro proceso que se ejecuta con el mismo usuario se conecte antes. Por eso, en el lado del helper se usa GetNamedPipeClientProcessId para comprobar si coincide con el PID esperado del proceso de la UI.

Por supuesto, no significa que se pueda confiar en cualquier cosa solo porque el PID coincida. Si la UI está comprometida, también llegarán al helper solicitudes peligrosas. Por eso mismo hacen falta la allowlist de operations y la validación de argumentos en el lado del helper.

Si se ordenan estas reglas desde el inicio hasta el final, quedan así. Basta con captar que cada movimiento de la UI tiene su verificación correspondiente en el lado del helper.

Secuencia de arranque, conexión y solicitud entre la UI y el broker de administradorDiagrama de secuencia que muestra cómo la UI decide el pipe y prepara su SID y PID, cómo Windows/UAC aprueba la elevación del helper, cómo el helper crea el pipe con una ACL restringida y verifica el PID de origen, y cómo procesa y responde a la solicitud tipada antes de terminar sin dejar estado elevado.Objetivo que requiere permisos de administradorAdminBroker.exe(high)Windows / UACMyApp.exe(medium)Objetivo que requiere permisos de administradorAdminBroker.exe(high)Windows / UACMyApp.exe(medium)Decide el nombre del pipe y prepara su propio SID y PIDInicia con ruta absoluta y Verb=runasSi se aprueba, se inicia con el token elevadoCrea el pipe con una ACL que solo permite ese SIDSe conecta al pipeCoteja el PID de origen de la conexiónSolicitud tipada (nombre de la operation y argumentos)Rechaza lo que está fuera de la allowlist y los argumentos inesperadosOpera únicamente sobre el objetivo fijadoDevuelve el resultadoTermina sin dejar ningún estado elevado

Figura 2: A cada paso de inicio, conexión y solicitud le corresponde una verificación en el lado del helper. Si se omite alguno, ese paso queda sin control.

6. El tema de la muestra

En este caso, uso como ejemplo registrar o eliminar el menú contextual del botón derecho del Explorador de archivos a nivel de máquina.

El motivo es sencillo:

  • Requiere permisos de administrador
  • El límite de la operación está claro
  • Permite evitar pasar cadenas de comando arbitrarias al helper
  • Es una situación que también se da normalmente en la práctica

Estas son las razones.

Las claves fijas de destino del registro son las siguientes.

  • HKLM\SOFTWARE\Classes\*\shell\MyApp.Open
  • HKLM\SOFTWARE\Classes\*\shell\MyApp.Open\command

La UI solo tiene la casilla «Registrar en el menú contextual del Explorador de archivos»; la operación real del registro la realiza el lado del helper.

7. Estructura de la solución

MyApp/
  MyApp/                         App de UI (asInvoker)
    app.manifest
    ElevationBrokerClient.cs
    SettingsPage.xaml.cs
  MyApp.AdminBroker/             Helper de administrador (requireAdministrator)
    app.manifest
    Program.cs
    BrokerLaunchOptions.cs
    ExplorerContextMenuRegistration.cs
  MyApp.BrokerProtocol/          Contrato común
    BrokerProtocol.cs

Si se mantiene el contrato común en un proyecto aparte,

  • El nombre de las operations
  • Los tipos de request/response
  • El formato de los mensajes del pipe

resulta más fácil mantenerlos alineados entre la UI y el helper.

8. Manifiestos

8.1 Lado de la UI (MyApp/app.manifest)

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApp.app" />
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

8.2 Lado del helper (MyApp.AdminBroker/app.manifest)

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApp.AdminBroker.app" />
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

La UI se mantiene siempre en asInvoker. Solo el helper está en requireAdministrator. Si se invierte esto, desaparece el sentido de haberlos separado.

9. Código del contrato común

9.1 MyApp.BrokerProtocol/BrokerProtocol.cs

using System.Buffers.Binary;
using System.Text.Json;

namespace MyApp.BrokerProtocol;

public static class BrokerJson
{
    public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
}

public static class BrokerOperations
{
    public const string SetExplorerContextMenu = "set-explorer-context-menu";
}

public sealed record BrokerRequest(string Operation, JsonElement Payload);

public sealed record BrokerResponse(bool Success, string? ErrorCode, string? Message)
{
    public static BrokerResponse Ok(string? message = null) => new(true, null, message);

    public static BrokerResponse Fail(string errorCode, string message) =>
        new(false, errorCode, message);
}

public sealed record SetExplorerContextMenuRequest(bool Enabled);

public static class PipeMessageSerializer
{
    private const int MaxPayloadBytes = 256 * 1024;

    public static async Task WriteAsync<T>(Stream stream, T value, CancellationToken cancellationToken)
    {
        byte[] payload = JsonSerializer.SerializeToUtf8Bytes(value, BrokerJson.Options);
        if (payload.Length > MaxPayloadBytes)
        {
            throw new InvalidDataException($"Payload is too large: {payload.Length} bytes.");
        }

        byte[] header = new byte[sizeof(int)];
        BinaryPrimitives.WriteInt32LittleEndian(header, payload.Length);

        await stream.WriteAsync(header.AsMemory(0, header.Length), cancellationToken);
        await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancellationToken);
        await stream.FlushAsync(cancellationToken);
    }

    public static async Task<T> ReadAsync<T>(Stream stream, CancellationToken cancellationToken)
    {
        byte[] header = await ReadExactAsync(stream, sizeof(int), cancellationToken);
        int payloadLength = BinaryPrimitives.ReadInt32LittleEndian(header);

        if (payloadLength <= 0 || payloadLength > MaxPayloadBytes)
        {
            throw new InvalidDataException($"Invalid payload length: {payloadLength}");
        }

        byte[] payload = await ReadExactAsync(stream, payloadLength, cancellationToken);

        return JsonSerializer.Deserialize<T>(payload, BrokerJson.Options)
            ?? throw new InvalidDataException($"Failed to deserialize {typeof(T).FullName}.");
    }

    private static async Task<byte[]> ReadExactAsync(Stream stream, int length, CancellationToken cancellationToken)
    {
        byte[] buffer = new byte[length];
        int offset = 0;

        while (offset < length)
        {
            int read = await stream.ReadAsync(buffer.AsMemory(offset, length - offset), cancellationToken);
            if (read == 0)
            {
                throw new EndOfStreamException("Pipe was closed before the expected number of bytes was read.");
            }

            offset += read;
        }

        return buffer;
    }
}

El punto clave es enviar el JSON al pipe con un prefijo de longitud, en lugar de volcarlo sin más. Mantener el protocolo simple -una solicitud, una respuesta- reduce el riesgo de errores.

10. Lado de la UI: inicio y comunicación con el helper

10.1 MyApp/ElevationBrokerClient.cs

using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Security.Principal;
using System.Text.Json;
using MyApp.BrokerProtocol;

namespace MyApp;

public sealed class ElevationBrokerClient
{
    private readonly string _helperExePath;

    public ElevationBrokerClient(string helperExePath)
    {
        _helperExePath = Path.GetFullPath(helperExePath);

        if (!Path.IsPathRooted(_helperExePath))
        {
            throw new ArgumentException("Helper executable path must be absolute.", nameof(helperExePath));
        }

        if (!File.Exists(_helperExePath))
        {
            throw new FileNotFoundException("Helper executable was not found.", _helperExePath);
        }
    }

    public async Task SetExplorerContextMenuEnabledAsync(bool enabled, CancellationToken cancellationToken = default)
    {
        string pipeName = $"myapp-broker-{Guid.NewGuid():N}";
        int clientPid = Environment.ProcessId;
        string clientSid = GetCurrentUserSid();

        StartHelper(pipeName, clientPid, clientSid);

        using var pipe = new NamedPipeClientStream(
            serverName: ".",
            pipeName: pipeName,
            direction: PipeDirection.InOut,
            options: PipeOptions.Asynchronous);

        using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        connectCts.CancelAfter(TimeSpan.FromSeconds(30));

        await pipe.ConnectAsync(connectCts.Token);

        BrokerRequest request = new(
            BrokerOperations.SetExplorerContextMenu,
            JsonSerializer.SerializeToElement(
                new SetExplorerContextMenuRequest(enabled),
                BrokerJson.Options));

        await PipeMessageSerializer.WriteAsync(pipe, request, cancellationToken);

        BrokerResponse response = await PipeMessageSerializer.ReadAsync<BrokerResponse>(pipe, cancellationToken);

        if (!response.Success)
        {
            throw new InvalidOperationException(
                $"Admin broker returned an error. Code={response.ErrorCode}, Message={response.Message}");
        }
    }

    private void StartHelper(string pipeName, int clientPid, string clientSid)
    {
        string workingDirectory = Path.GetDirectoryName(_helperExePath)
            ?? throw new InvalidOperationException("Helper executable directory could not be resolved.");

        var startInfo = new ProcessStartInfo
        {
            FileName = _helperExePath,
            Arguments = BuildArguments(pipeName, clientPid, clientSid),
            WorkingDirectory = workingDirectory,
            UseShellExecute = true,
            Verb = "runas"
        };

        try
        {
            Process.Start(startInfo)
                ?? throw new InvalidOperationException("The helper process could not be started.");
        }
        catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
        {
            throw new OperationCanceledException("La aprobación de los permisos de administrador fue cancelada.", ex);
        }
    }

    private static string GetCurrentUserSid()
    {
        using WindowsIdentity identity = WindowsIdentity.GetCurrent();
        return identity.User?.Value
            ?? throw new InvalidOperationException("Current user SID could not be resolved.");
    }

    private static string BuildArguments(string pipeName, int clientPid, string clientSid)
    {
        return string.Join(
            " ",
            "--pipe",
            QuoteArgument(pipeName),
            "--client-pid",
            clientPid.ToString(CultureInfo.InvariantCulture),
            "--client-sid",
            QuoteArgument(clientSid));
    }

    private static string QuoteArgument(string value)
    {
        return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
    }
}

Aquí, lo que se pasa al helper es solo el nombre del pipe y la información mínima necesaria para verificar el origen de la conexión. La operación de administrador en sí queda encerrada en la request tipada que se envía dentro del pipe. Este QuoteArgument es una implementación mínima que asume valores simples como el nombre del pipe, el PID y el SID que se pasan en esta muestra. Si necesita pasar como argumento de línea de comandos una ruta de Windows arbitraria o una cadena de entrada libre, sustitúyalo por un tratamiento de escape específico que respete las reglas de análisis de argv de Windows.

11. Lado del helper: análisis de los argumentos de inicio

11.1 MyApp.AdminBroker/BrokerLaunchOptions.cs

namespace MyApp.AdminBroker;

internal sealed class BrokerLaunchOptions
{
    public required string PipeName { get; init; }
    public required int ExpectedClientProcessId { get; init; }
    public required string ClientUserSid { get; init; }

    public static BrokerLaunchOptions Parse(string[] args)
    {
        string? pipeName = null;
        int? clientPid = null;
        string? clientSid = null;

        for (int i = 0; i < args.Length; i++)
        {
            switch (args[i])
            {
                case "--pipe":
                    pipeName = ReadNextValue(args, ref i, "--pipe");
                    break;
                case "--client-pid":
                    string pidText = ReadNextValue(args, ref i, "--client-pid");
                    if (!int.TryParse(pidText, out int pid) || pid <= 0)
                    {
                        throw new ArgumentException($"Invalid client PID: {pidText}");
                    }

                    clientPid = pid;
                    break;
                case "--client-sid":
                    clientSid = ReadNextValue(args, ref i, "--client-sid");
                    break;
                default:
                    throw new ArgumentException($"Unknown argument: {args[i]}");
            }
        }

        if (string.IsNullOrWhiteSpace(pipeName))
        {
            throw new ArgumentException("--pipe is required.");
        }

        if (clientPid is null)
        {
            throw new ArgumentException("--client-pid is required.");
        }

        if (string.IsNullOrWhiteSpace(clientSid))
        {
            throw new ArgumentException("--client-sid is required.");
        }

        return new BrokerLaunchOptions
        {
            PipeName = pipeName,
            ExpectedClientProcessId = clientPid.Value,
            ClientUserSid = clientSid
        };
    }

    private static string ReadNextValue(string[] args, ref int index, string optionName)
    {
        if (index + 1 >= args.Length)
        {
            throw new ArgumentException($"A value is required after {optionName}.");
        }

        index++;
        return args[index];
    }
}

El lado del helper genera un error en cuanto faltan argumentos o sobran argumentos. Dentro del límite de elevación, es mejor no intentar «interpretar como sea» lo que llega.

12. Lado del helper: creación del pipe, verificación del PID de origen y dispatch

12.1 MyApp.AdminBroker/Program.cs

using System.ComponentModel;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;
using MyApp.BrokerProtocol;

namespace MyApp.AdminBroker;

internal static class Program
{
    public static async Task<int> Main(string[] args)
    {
        BrokerLaunchOptions options = BrokerLaunchOptions.Parse(args);

        using var brokerCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using NamedPipeServerStream pipe = CreatePipeServer(options);

        await pipe.WaitForConnectionAsync(brokerCts.Token);

        VerifyClientProcessId(pipe, options.ExpectedClientProcessId);

        BrokerRequest request = await PipeMessageSerializer.ReadAsync<BrokerRequest>(pipe, brokerCts.Token);
        BrokerResponse response = await DispatchAsync(request);

        await PipeMessageSerializer.WriteAsync(pipe, response, brokerCts.Token);

        return response.Success ? 0 : 2;
    }

    private static Task<BrokerResponse> DispatchAsync(BrokerRequest request)
    {
        try
        {
            return request.Operation switch
            {
                BrokerOperations.SetExplorerContextMenu => HandleSetExplorerContextMenuAsync(request.Payload),
                _ => Task.FromResult(
                    BrokerResponse.Fail(
                        "unsupported_operation",
                        $"Unsupported operation: {request.Operation}"))
            };
        }
        catch (JsonException ex)
        {
            return Task.FromResult(BrokerResponse.Fail("invalid_payload", ex.Message));
        }
        catch (Exception ex)
        {
            return Task.FromResult(BrokerResponse.Fail("broker_failure", ex.Message));
        }
    }

    private static NamedPipeServerStream CreatePipeServer(BrokerLaunchOptions options)
    {
        var pipeSecurity = new PipeSecurity();
        var clientSid = new SecurityIdentifier(options.ClientUserSid);
        SecurityIdentifier helperSid = WindowsIdentity.GetCurrent().User
            ?? throw new InvalidOperationException("Helper user SID could not be resolved.");

        pipeSecurity.AddAccessRule(new PipeAccessRule(
            clientSid,
            PipeAccessRights.ReadWrite,
            AccessControlType.Allow));

        pipeSecurity.AddAccessRule(new PipeAccessRule(
            helperSid,
            PipeAccessRights.FullControl,
            AccessControlType.Allow));

        pipeSecurity.AddAccessRule(new PipeAccessRule(
            new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
            PipeAccessRights.FullControl,
            AccessControlType.Allow));

        return NamedPipeServerStreamAcl.Create(
            options.PipeName,
            PipeDirection.InOut,
            maxNumberOfServerInstances: 1,
            transmissionMode: PipeTransmissionMode.Byte,
            options: PipeOptions.Asynchronous | PipeOptions.WriteThrough,
            inBufferSize: 0,
            outBufferSize: 0,
            pipeSecurity: pipeSecurity);
    }

    private static void VerifyClientProcessId(NamedPipeServerStream pipe, int expectedClientProcessId)
    {
        if (!GetNamedPipeClientProcessId(
                pipe.SafePipeHandle.DangerousGetHandle(),
                out uint actualClientProcessId))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        if (actualClientProcessId != (uint)expectedClientProcessId)
        {
            throw new InvalidOperationException(
                $"Unexpected pipe client PID. Expected={expectedClientProcessId}, Actual={actualClientProcessId}");
        }
    }

    private static Task<BrokerResponse> HandleSetExplorerContextMenuAsync(JsonElement payload)
    {
        SetExplorerContextMenuRequest request = payload.Deserialize<SetExplorerContextMenuRequest>(BrokerJson.Options)
            ?? throw new JsonException("Payload could not be parsed.");

        ExplorerContextMenuRegistration.Apply(request.Enabled);
        return Task.FromResult(BrokerResponse.Ok("Explorer context menu setting was updated."));
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetNamedPipeClientProcessId(
        IntPtr pipe,
        out uint clientProcessId);
}

Lo que realmente importa aquí es lo siguiente.

  • La ACL del pipe se construye de forma explícita
  • La ACL se concede no solo al SID del usuario actual del helper, sino también al SID del usuario de la UI que hace la llamada
  • Tras la conexión, se verifica el PID del cliente
  • Incluso después de recibir la request, se hace dispatch por el nombre de la operation

Mantener switch (request.Operation) para dejar pasar únicamente operaciones fijas hace que sea más difícil que el helper se convierta en una «caja elevada que hace de todo».

13. El cuerpo de la operación de administrador: registro del menú contextual del Explorador

13.1 MyApp.AdminBroker/ExplorerContextMenuRegistration.cs

using System;
using System.IO;
using Microsoft.Win32;

namespace MyApp.AdminBroker;

internal static class ExplorerContextMenuRegistration
{
    private const string MenuKeyPath = @"SOFTWARE\Classes\*\shell\MyApp.Open";
    private const string CommandKeyPath = @"SOFTWARE\Classes\*\shell\MyApp.Open\command";
    private const string MenuText = "Open with MyApp";
    private const string ClientExecutableName = "MyApp.exe";

    public static void Apply(bool enabled)
    {
        string clientExePath = ResolveClientExecutablePath();

        using RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, GetRegistryView());

        if (enabled)
        {
            using RegistryKey menuKey = hklm.CreateSubKey(MenuKeyPath)
                ?? throw new InvalidOperationException($"Failed to create registry key: {MenuKeyPath}");

            menuKey.SetValue(null, MenuText, RegistryValueKind.String);
            menuKey.SetValue("Icon", $"\"{clientExePath}\",0", RegistryValueKind.String);

            using RegistryKey commandKey = hklm.CreateSubKey(CommandKeyPath)
                ?? throw new InvalidOperationException($"Failed to create registry key: {CommandKeyPath}");

            commandKey.SetValue(null, $"\"{clientExePath}\" \"%1\"", RegistryValueKind.String);
        }
        else
        {
            hklm.DeleteSubKeyTree(@"SOFTWARE\Classes\*\shell\MyApp.Open", throwOnMissingSubKey: false);
        }
    }

    private static string ResolveClientExecutablePath()
    {
        string clientExePath = Path.GetFullPath(
            Path.Combine(AppContext.BaseDirectory, ClientExecutableName));

        if (!File.Exists(clientExePath))
        {
            throw new FileNotFoundException("Client executable was not found.", clientExePath);
        }

        return clientExePath;
    }

    private static RegistryView GetRegistryView()
    {
        return Environment.Is64BitOperatingSystem
            ? RegistryView.Registry64
            : RegistryView.Registry32;
    }
}

La clave de este código está en lo que no recibe de la UI.

  • No recibe de la UI una ruta de registro arbitraria
  • No recibe de la UI una cadena de comando arbitraria
  • El EXE de destino del registro se resuelve de forma fija en el lado del helper
  • El contenido de la request es únicamente Enabled

Es decir, el helper está hecho para tener un único significado: «alternar el estado de registro del menú contextual del Explorador».

14. Ejemplo de llamada desde la UI

14.1 MyApp/SettingsPage.xaml.cs

using System.Windows;

namespace MyApp;

public partial class SettingsPage
{
    private readonly ElevationBrokerClient _broker = new(
        Path.Combine(AppContext.BaseDirectory, "MyApp.AdminBroker.exe"));

    private async void ExplorerMenuCheckBox_Click(object sender, RoutedEventArgs e)
    {
        bool enabled = ExplorerMenuCheckBox.IsChecked == true;

        try
        {
            await _broker.SetExplorerContextMenuEnabledAsync(enabled);
            MessageBox.Show("Setting has been updated.", "MyApp");
        }
        catch (OperationCanceledException)
        {
            MessageBox.Show("The administrator approval prompt was canceled.", "MyApp");
            ExplorerMenuCheckBox.IsChecked = !enabled;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "Failed to update the setting.");
            ExplorerMenuCheckBox.IsChecked = !enabled;
        }
    }
}

El lado de la UI es sencillo.

  • Lee el estado de la casilla
  • Llama al broker client
  • Si falla, revierte la UI

Nada más que eso. No toca el registro directamente. Eso es la separación.

15. Lo que esta implementación garantiza

Estas son las líneas que esta muestra respeta en la práctica.

15.1 Separación de responsabilidades entre la UI y el helper

  • La UI solo recibe la interacción del usuario
  • El helper solo ejecuta operaciones de administrador fijas

15.2 No se crea un «punto de ejecución arbitraria» en el helper

  • No recibe una ruta de registro arbitraria
  • No recibe una línea de comandos arbitraria
  • No recibe una ruta de EXE arbitraria

15.3 La ruta de inicio es fija

  • El helper EXE usa una ruta absoluta
  • runas se especifica explícitamente
  • UseShellExecute = true se especifica explícitamente

15.4 El origen de la conexión IPC está restringido

  • La ACL del pipe se limita al SID del usuario de la UI
  • Tras la conexión, se comprueba el PID del cliente

15.5 El objetivo de la operación de administrador también es fijo

  • El hive y la ruta del registro son fijos
  • El EXE de destino del registro también se resuelve de forma fija

Llegar hasta este punto aleja bastante del estado en el que «si la UI se ve comprometida, se puede hacer cualquier cosa con el helper».

15.6 Comprobar que la separación funciona realmente

Hasta aquí todo ha sido cuestión de diseño. Si lo que se ha escrito realmente está separado, solo se sabe ejecutándolo y comprobándolo. Que «toda la UI terminó elevada sin darse cuenta» es un accidente difícil de detectar con solo leer el código.

La comprobación consiste en revisar los siguientes cuatro puntos en orden.

1. Si el proceso de la UI permanece sin elevar

Es el punto más importante. Se comprueba después de iniciar la UI y ejecutar una vez una operación de administrador.

  • Administrador de tareas: en la pestaña «Detalles», haga clic con el botón derecho en las columnas y muestre la columna «Elevado». Si MyApp.exe aparece como «No» y solo MyApp.AdminBroker.exe aparece como «Sí», es lo esperado
  • Process Explorer: muestre la columna Integrity. Si la UI aparece como Medium y el helper como High, es correcto (según 2.1, los niveles de integridad de Windows son medium = usuario estándar, high = elevado)

Si quiere verlo desde el código, basta con comprobarlo una sola vez justo después de iniciar la UI.

using System.Security.Principal;

using WindowsIdentity identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);

// En el proceso de la UI, esto debería ser false
bool isElevatedAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);

2. Si el prompt de UAC aparece solo al iniciar el helper

  • Si el prompt aparece al iniciar la UI -> el manifiesto del lado de la UI no está en asInvoker
  • Si aparece justo al pulsar la casilla de configuración -> es lo esperado
  • Si la configuración cambia sin que aparezca ningún prompt -> es posible que el helper esté siempre elevado por otra vía

Con una cuenta de administrador aparece un consent prompt; con un usuario estándar, un credential prompt (tabla de 5.6). Probando ambas se puede comprobar incluso si el paso del SID es correcto.

3. Si la operación de administrador realmente surtió efecto

Para el registro del menú del Explorador, lo más rápido es mirar directamente el registro.

reg query "HKLM\SOFTWARE\Classes\*\shell\MyApp.Open" /s

Compruebe la eliminación de la misma manera. Si solo prueba el registro y no la eliminación, puede quedar un error en el lado de DeleteSubKeyTree.

4. Si «falla cuando debería fallar»

Si no se comprueba esto, no se sabe si la separación realmente funciona.

  • Cancelar el prompt de elevación -> la configuración no debe cambiar y la casilla de la UI debe volver a su estado anterior (tratamiento de ERROR_CANCELLED = 1223; capítulo 10)
  • Iniciar el helper directamente -> aunque se ejecute a mano algo como MyApp.AdminBroker.exe --pipe x --client-pid 1 --client-sid S-1-5-18, el proceso no debe avanzar por la verificación del PID de origen y el timeout
  • Enviar una operation que no está en la allowlist -> debe rechazarse con unsupported_operation (DispatchAsync del capítulo 12)

Los comandos concretos de estos pasos están recogidos con el mismo flujo en la sección «Procedimiento de verificación en Windows» del README de la muestra.

16. Errores frecuentes

16.1 Poner toda la UI en requireAdministrator

Aunque solo un botón de la pantalla de configuración necesita permisos de administrador, se inicia todo elevado. Esto va en la dirección de borrar de forma descuidada el límite de permisos.

16.2 Pasar al helper un comando en forma de cadena en bruto

Por ejemplo, un diseño como este.

UI -> le pasa al helper "reg add HKLM\\.... /v ... /d ..."

Esto convierte al helper en un ejecutor de comandos. Es mejor evitarlo.

16.3 Usar tal cual la ACL predeterminada del pipe con nombre

“Como es IPC local, seguro que está bien” es un poco arriesgado. Como el pipe está sujeto a la seguridad de Windows, es mejor construir la ACL correctamente.

16.4 Recurrir sin más a CurrentUserOnly

Parece cómodo, pero no es adecuado para la comunicación entre la UI en medium integrity y el helper en high integrity de este caso. Aquí es más manejable una ACL explícita.

16.5 Que el helper reciba una ruta arbitraria y opere sobre ella

Por ejemplo, cosas como estas.

  • Copiar un archivo arbitrario en Program Files
  • Escribir una clave arbitraria en HKLM
  • Eliminar un nombre de servicio arbitrario
  • Añadir una regla de firewall con un comando arbitrario

Si el helper acepta eso, el propio helper se convierte en un punto de ejecución genérico con permisos de administrador. Las operaciones siempre deben fijarse.

17. Resumen

Que «solo una parte del proceso necesita permisos de administrador» en una app de Windows no es una situación excepcional. Sin embargo, la solución no es «poner todo en requireAdministrator», sino trazar un límite de ejecución.

La forma más fácil de adoptar al principio es esta.

  • La UI está en asInvoker
  • El proceso de administrador se separa en un helper EXE
  • El helper está en requireAdministrator
  • El inicio se hace con runas
  • La comunicación usa un named pipe
  • El helper solo acepta operations fijas
  • El origen de la conexión se restringe con la ACL del pipe y el PID del cliente
  • Los argumentos se vuelven a validar en el lado del helper

Mantener esta forma facilita la migración más adelante si se quiere convertir en un servicio. Si se separa bien el contrato de las operations, el límite entre la UI y el proceso de administrador se convierte directamente en un activo de diseño.

En cuestión de seguridad, no dejar límites descuidados funciona mejor que añadir funciones vistosas. Con los permisos de administrador ocurre lo mismo. En lugar de darlo todo junto, hay que entregar solo lo necesario, de la forma más restringida posible. Ese tipo de sobriedad es lo que da resultado más adelante.

Referencias

Tenga en cuenta que algunos de los siguientes enlaces incluyen en la URL una indicación de versión como view=net-10.0. Esto especifica qué versión de .NET muestra la documentación en Microsoft Learn, y no significa que haya una discrepancia con .NET 8, la base de este artículo. PipeOptions, NamedPipeServerStreamAcl y RegistryView, que se usan aquí, están todos disponibles en .NET 8. Si quiere que la visualización coincida con .NET 8, cámbiela con el selector de versión de la parte superior de la página.

  • El conjunto de código de muestra de este artículo (biblioteca de contrato común, demo y pruebas unitarias) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-admin-broker-deep-dive
  • Artículo original: Lista de comprobación para mantener un mínimo de seguridad en el desarrollo de apps Windows https://comcomponent.com/es/blog/windows-app-security-minimum-checklist/
  • Administrator Broker Model - Win32 apps https://learn.microsoft.com/ja-jp/windows/win32/secauthz/administrator-broker-model
  • Developing Applications that Require Administrator Privilege https://learn.microsoft.com/en-us/windows/win32/secauthz/developing-applications-that-require-administrator-privilege
  • Operating System Service Model - Win32 apps https://learn.microsoft.com/ja-jp/windows/win32/secauthz/operating-system-service-model
  • Elevated Task Model - Win32 apps https://learn.microsoft.com/en-us/windows/win32/secauthz/elevated-task-model
  • Administrator COM Object Model - Win32 apps https://learn.microsoft.com/ja-jp/windows/win32/secauthz/administrator-com-object-model
  • The COM Elevation Moniker https://learn.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker
  • How User Account Control works https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works
  • Mandatory Integrity Control - Win32 apps https://learn.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control
  • Process Explorer - Sysinternals https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer
  • WindowsPrincipal.IsInRole Method https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsprincipal.isinrole
  • ProcessStartInfo.UseShellExecute https://learn.microsoft.com/ja-jp/dotnet/fundamentals/runtime-libraries/system-diagnostics-processstartinfo-useshellexecute
  • Named Pipe Security and Access Rights https://learn.microsoft.com/ja-jp/windows/win32/ipc/named-pipe-security-and-access-rights
  • PipeOptions Enum https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.pipeoptions?view=net-10.0
  • NamedPipeServerStreamAcl.Create https://learn.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstreamacl.create?view=net-10.0
  • GetNamedPipeClientProcessId https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-getnamedpipeclientprocessid
  • RegistryView Enum https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.win32.registryview?view=net-8.0

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Se puede ejecutar solo una parte del código dentro del mismo proceso con permisos de administrador?
No es posible. El UAC de Windows no controla la elevación por función, sino según con qué token y nivel de integridad se ejecuta el proceso. Los procesos hijos heredan el token con el mismo nivel de integridad que el proceso padre, por lo que es imposible diseñar un sistema en el que solo un método determinado se ejecute con permisos de administrador dentro de un proceso de UI no elevado. El proceso que requiere esos permisos debe extraerse a otra unidad de ejecución, como un proceso independiente, un servicio, una tarea programada o un objeto COM elevado.
¿Qué opciones existen para separar los procesos que necesitan permisos de administrador?
Microsoft Learn menciona principalmente cuatro modelos. El Administrator Broker Model, que combina una UI de usuario estándar con un helper EXE de administrador; el Operating System Service Model, que usa un servicio en segundo plano; el Elevated Task Model, que usa una tarea programada con permisos de administrador; y el Administrator COM Object Model, que usa un objeto COM elevado. Si las operaciones de administrador son esporádicas y basta con mostrar el UAC en el momento necesario conviene un broker EXE; si son constantes, desatendidas y frecuentes conviene un servicio; y si son procesos rutinarios breves que terminan de una sola vez conviene una tarea programada.
¿Se puede usar la entrada/salida estándar para comunicarse con un helper EXE iniciado con runas?
Es difícil de usar, así que conviene evitarlo. En .NET, ProcessStartInfo.Verb solo funciona cuando UseShellExecute=true, y al establecer UseShellExecute=true deja de estar disponible la comunicación basada en redirección de la entrada/salida estándar. Por eso lo más natural es comunicarse con el helper mediante IPC, como un pipe con nombre. El pipe no debe depender de la ACL predeterminada: hay que configurar un PipeSecurity explícito, limitar el permiso de conexión al SID del usuario que realiza la llamada y, además, verificar el PID de origen de la conexión con GetNamedPipeClientProcessId.
¿No es seguro usar PipeOptions.CurrentUserOnly en un pipe con nombre?
No es adecuado para la comunicación entre una UI no elevada y un helper elevado. En Windows, CurrentUserOnly comprueba no solo la cuenta de usuario, sino también el nivel de elevación, por lo que no permite conectar entre procesos con distinto nivel de integridad. Además, en un entorno de usuario estándar el UAC se convierte en un credential prompt, y el helper puede llegar a ejecutarse con otra cuenta de administrador distinta. Es más manejable que la UI obtenga su propio SID y se lo pase al helper, y que el helper conceda el permiso de conexión al pipe únicamente a ese SID mediante una ACL explícita.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog