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.
- Primero, los supuestos de partida
- Qué modelo de separación elegir
- La forma más práctica:
asInvoker+ un helper EXE de administrador - Trampas que no conviene pasar por alto al implementarlo
- 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,GetNamedPipeClientProcessIdy 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.Opena 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.
flowchart TB
accTitle: Frontera de elevación entre la UI y el helper de administrador
accDescr: Diagrama 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.
subgraph MED["medium integrity — no se eleva en ningún momento"]
UI["MyApp.exe (asInvoker): solo recibe la interacción del usuario y arma la solicitud"]
end
subgraph HIGH["high integrity — proceso elevado de vida corta"]
BR["MyApp.AdminBroker.exe (requireAdministrator)"]
PIPE["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 origen"]
DISP["Distribuye según la allowlist de operaciones y vuelve a validar los argumentos en el helper"]
end
TGT["Objetivo fijo que requiere permisos de administrador: claves bajo HKLM, registro de servicios o reglas de firewall"]
UI -->|"Inicia con ruta absoluta + Verb=runas (aquí aparece el prompt de UAC)"| BR
BR --> PIPE
UI -->|"Solicitud tipada (no se pasa una cadena de comando en bruto)"| PIPE
PIPE --> DISP
DISP --> TGT
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.
- El proceso de la UI permanece sin elevar hasta el final
- El helper de administrador tiene una vida corta
- 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-menuinstall-serviceadd-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,
CurrentUserOnlyse 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.
sequenceDiagram
accTitle: Secuencia de arranque, conexión y solicitud entre la UI y el broker de administrador
accDescr: Diagrama 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.
participant UI as MyApp.exe(medium)
participant OS as Windows / UAC
participant BR as AdminBroker.exe(high)
participant TGT as Objetivo que requiere permisos de administrador
UI->>UI: Decide el nombre del pipe y prepara su propio SID y PID
UI->>OS: Inicia con ruta absoluta y Verb=runas
OS->>BR: Si se aprueba, se inicia con el token elevado
BR->>BR: Crea el pipe con una ACL que solo permite ese SID
UI->>BR: Se conecta al pipe
BR->>BR: Coteja el PID de origen de la conexión
UI->>BR: Solicitud tipada (nombre de la operation y argumentos)
BR->>BR: Rechaza lo que está fuera de la allowlist y los argumentos inesperados
BR->>TGT: Opera únicamente sobre el objetivo fijado
BR-->>UI: Devuelve el resultado
BR->>BR: Termina 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.OpenHKLM\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
runasse especifica explícitamenteUseShellExecute = truese 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.exeaparece como «No» y soloMyApp.AdminBroker.exeaparece como «Sí», es lo esperado - Process Explorer: muestre la columna Integrity. Si la UI aparece como
Mediumy el helper comoHigh, 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(DispatchAsyncdel 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 relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Almacenamiento de información confidencial en aplicaciones Windows - Cómo evitar la configuración en texto plano con DPAPI
Para no guardar en texto plano las cadenas de conexión ni los tokens de API en aplicaciones Windows, se repasan DPAPI / ProtectedData, la...
Cuándo se necesita el privilegio de administrador en Windows - UAC, áreas protegidas y cómo distinguirlo por diseño
Analizamos cuándo Windows requiere privilegios de administrador: UAC, áreas protegidas, servicios, controladores y diseño per-user/per-ma...
Lista de verificación mínima de seguridad para el desarrollo de aplicaciones de Windows
Lista de verificación para aplicaciones empresariales WPF, WinForms, WinUI, C++ y C#: permisos, firma, actualizaciones, secretos, HTTPS, ...
Las profundidades del I/O en Windows (6.ª entrega, final) ── Controladores de filtro y minifiltros: por qué Procmon y el análisis antivirus pueden interceptar el I/O
Última entrega de la serie sobre controladores de filtro y minifiltros de Windows: el Filter Manager, las altitudes, los callbacks pre/po...
Cuando su aplicación Windows de desarrollo propio es tratada como virus — cómo abordar los falsos positivos de Microsoft Defender y su impacto en el rendimiento
Procedimiento oficial para resolver falsos positivos de Microsoft Defender en apps Windows propias: por qué ocurren, cómo reportarlos, re...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Este tema abarca el diseño de permisos de toda una app de Windows -UAC, helper EXE, la decisión de convertirlo en servicio y cambios de configuración a nivel de máquina-, por lo que encaja bien con Desarrollo de apps Windows.
Consultoría técnica y revisión de diseño
Si desea revisar el uso permanente de `requireAdministrator` en una app existente y reordenar el diseño del broker y los límites de IPC, es un tema que se presta bien a una consultoría técnica y revisión de diseño.
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.