Sesión no iniciada

API - Emular "RunAs"
General
Autor: Lluis Franco, 01/04/2005

Leído: 213 veces
Puntuación: 7,85          
Votar este artículo: 1 2 3 4 5 6 7 8 9 10

   

Como ejecutar una aplicación con otro usuario

Como ejecutar una aplicación con permisos de otro usuario (Emular un "Run As")

 

Introducción

En ocasiones resulta de utilidad poder ejecutar una aplicación con permisos de otro usuario (usualmente como administrador), ya que en una red Windows bien configurada, es decir con políticas de grupos y usuarios, raramente los usuarios llamados "normales" poseerán privilegios de administrador de la red.

Esto está muy bien para asegurar que no se producen "desgracias" por parte de un usuario, ya no de forma malintencionada sino lo que vulgarmente se conoce como el "yonohesido" :-D, ya que si un usuario no tiene permiso para borrar una carpeta o cambiar la configuración del Internet Explorer, seguros que nuestra red vivirá más tiempo sin sobresaltos.

Ahora bien, también existe la parte negativa, y es que existen algunas (que digo algunas, un montón) aplicaciones que deben forzosamente ejecutarse con privilegios de usuario administrador para funcionar correctamente. Esto habitualmente es fuente de pesadillas para los administradores de red, que afortunadamente cuentan con la posibilidad de crear accesos directos a aplicaciones que se ejecutan con permisos de otro usuario, basta con activar la casilla 'Ejecutar como usuario diferente" (fig. 1).


(fig. 1)

De esta forma cuando el usuario quiere ejecutar la aplicación se le presentará una ventana dónde pueda introducir sus "credenciales", es decir: Usuario, contraseña y dominio en el que iniciar la sesión (fig. 2). Así solventamos algunas de las situaciones, pero ¿que sucede cuando es una aplicación la que debe llamar a otra y hacer que esta segunda se ejecute con otros privilegios? Pues la respuesta es que no es tan sencillo... o no lo sería si no tuviéramos la función "CreateProcessWithLogon" del API de Windows.


(fig. 2)

 

¿CreateProcessWithLogon? Examinemos atentamente esta función:

Private Declare Function CreateProcessWithLogon Lib _
      "Advapi32" Alias "CreateProcessWithLogonW" _
      (ByVal lpUsername As Long, ByVal lpDomain As Long, ByVal _
      lpPassword As Long, ByVal dwLogonFlags As Long, ByVal _
      lpApplicationName As Long, ByVal lpCommandLine As Long, _
      ByVal dwCreationFlags As Long, ByVal lpEnvironment As Long, _
      ByVal lpCurrentDirectory As Long, ByVal lpStartupInfo As STARTUPINFO, _
      ByVal lpProcessInfo As PROCESS_INFORMATION) As Long

Básicamente crea un nuevo proceso con su thread primario, dentro del cual ejecuta la aplicación solicitada en el contexto de seguridad especificado (usuario, contraseña y dominio) que es suministrado a la función en forma de parámetros, y aunque nos suene extraño las cadenas que contienen el usuario, contraseña y dominio no se pasan como String, sino como puntero a un String usando la función StrPtr de VB

lpUsername
Puntero a un String que contiene el nombre del usuario que ejecutará la aplicación. Si usamos la convención UPN (usuario@dominio), el argumento lpDomain deberá ser NULL. Se conceden permisos a todos los usuarios en todos los clientes y servidores, pero sólo a los administradores en los controladores de dominio.

lpDomain
Puntero a un String que contiene el dominio en el que nos autentificaremos, en Windows XP si el valor es "." se asume que estamos usando una cuenta local.

lpPassword
Puntero a un String que contiene la contraseña del usuario que ejecutará la aplicación.

dwLogonFlags
Define las opciones de la autentificación, sus valores pueden ser:
- LOGON_WITH_PROFILE: Carga el perfil del usuario, es la mejor opción siempre que tengamos acceso al mismo, aunque consume un poco más de tiempo.
- LOGON_NETCREDENTIALS_ONLY: El nuevo proceso usará el token de seguridad que el proceso llamante, pero el sistema creará una nueva sesión mediante LSA, se acostumbra a usar en entornos multidominio en los cuales no esté definida relación de confianza.

lpApplicationName
Puntero a un String que contiene la ruta del ejecutable. Si únicamente se suministra el nombre, por defecto se buscará en la carpeta actual.

lpCommandLine
Puntero a un String que contiene los argumentos de línea de comandos suministrado a la aplicación. En caso que la combinación de nombre de aplicación más los argumentos suministrados permitan diferentes interpretaciones el orden de búsqueda será:

- El directorio de la aplicación que deseamos ejecutar.
- El directorio actual del proceso llamador.
- El directorio %System (independientemente de si estamos en un sistema 16 o 32 bits).
- El directorio Windows.
- Los directorios listados en la variable de entorno PATH.

dwCreationFlags
Permite especificar "como" se va a crear el nuevo proceso, por defecto los flags CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, y CREATE_NEW_PROCESS_GROUP están activados, y admite los siguientes valores:
- CREATE_DEFAULT_ERROR_MODE: El nuevo proceso no heredará el modo de errores del proceso "llamador".
- CREATE_NEW_CONSOLE: El nuevo proceso creará su propia consola en lugar de heredar la consola del proceso "llamador".
- CREATE_NEW_PROCESS_GROUP: Permite la creación de un proceso "raíz" de un grupo de procesos, el identificador de este nuevo proceso será devuelto en el valor del parámetro lpProcessInfo.
- CREATE_SEPARATE_WOW_VDM: Válido sólo en entornos 16 bits.
- CREATE_SUSPENDED: Crea el proceso pero establece el estado del thread principal a suspendido, y sólo se activará si llamamos a la función ResumeThread.
- CREATE_UNICODE_ENVIRONMENT: Permite el uso de carácteres Unicode en el entorno lpEnvironment.

Si además deseamos establecer la prioridad del nuevo proceso podemos usar las constantes habituales, enumeradas de menor a mayor prioridad (por defecto NORMAL_PRIORITY_CLASS ):
- IDLE_PRIORITY_CLASS
- BELOW_NORMAL_PRIORITY_CLASS
- NORMAL_PRIORITY_CLASS
- ABOVE_NORMAL_PRIORITY_CLASS
- HIGH_PRIORITY_CLASS
- REALTIME_PRIORITY_CLASS

lpEnvironment
Puntero al entorno del nuevo proceso. Si el valor es NULL se usará el entorno del proceso "llamador".

lpCurrentDirectory
Puntero a una cadena que contiene la unidad y directorio del nuevo proceso. Si el valor es NULL se usarán los valores del proceso "llamador".

lpStartupInfo
Puntero a una estructura STARTUPINFO que permite especificar la apariencia de la ventana principal del nuevo proceso.

lpProcessInfo
Parámetro de salida. Puntero a una estructura PROCESS_INFORMATION que recibe información de identificación del nuevo proceso, incluyendo el handle del nuevo proceso.

 

Ejemplo de implementación (código VB 6.0):

Una vez conocemos cómo funciona vamos a ver un ejemplo de cómo implementarla en una aplicación VB 6.0. Esta aplicación recibirá tres parámetros que son: el path de la aplicación, usuario@dominio (observar que usaremos convención UPN), y la contraseña del usuario.

El código está completo e incluye las declaraciones necesarias para la llamada, además de las funciones "GetLastError" y "FormatMessage" para los posibles valores de retorno en caso de error. Sólo es necesario un modulo *.BAS con el siguiente código y establecer "Sub Main" en el modo de inicio en las propiedades del proyecto:

    Option Explicit

    ' ///
    ' ///  Ejecutar una aplicación con permisos de otro usuario ("Run As")
 
   ' ///  Lluís Franco i Montanyés (MVP-MCP-VB) 07/06/2004
    ' ///  Ejemplo: fRunAsEx "NOTEPAD.EXE", "<usuario>@<dominio>", "<password>"
   
' ///

    Private Const LOGON_WITH_PROFILE = &H1&
    Private Const CREATE_DEFAULT_ERROR_MODE = &H4000000
    Private Const CREATE_NEW_CONSOLE = &H10&
    Private Const CREATE_NEW_PROCESS_GROUP = &H200&
    Private Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
    Private Const LANG_NEUTRAL = &H0

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

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

    Private Declare Function CreateProcessWithLogon Lib _
      "Advapi32" Alias "CreateProcessWithLogonW" _
      (ByVal lpUsername As Long, ByVal lpDomain As Long, ByVal _
      lpPassword As Long, ByVal dwLogonFlags As Long, ByVal _
      lpApplicationName As Long, ByVal lpCommandLine As Long, _
      ByVal dwCreationFlags As Long, ByVal lpEnvironment As Long, _
      ByVal lpCurrentDirectory As Long, ByVal lpStartupInfo As STARTUPINFO, _
      ByVal lpProcessInfo As PROCESS_INFORMATION) As Long

    Private Declare Function CloseHandle Lib "kernel32" _
      (ByVal hObject As Long) As Long
 

    Private Declare Function FormatMessage Lib "kernel32" Alias "FormatMessageA" _
      (ByVal dwFlags As Long, ByVal lpSource As Any, ByVal dwMessageId As Long, _
      ByVal dwLanguageId As Long, ByVal lpBuffer As String, ByVal nSize As Long, _
      ByVal Arguments As Long) As Long

    Private Declare Function GetLastError Lib "kernel32" () As Long

    Public sError As String 

    Public Sub Main()

        Dim sParams() As String, sParam, i As Long, fReturn As Long
        Dim sApp As String, sUser As String, sPwd As String

        'Verificación de argumentos correctos (/path aplicación /usuario@dominio /contraseña)

        If Trim(Command$) = "" Then
            MsgBox("Error! No se han suministrado argumentos para ejecutar RunAs:" & _
            vbNewLine & vbNewLine & _
            "Parametros= /<path aplicación> /<usuario> /<password>" & _
            vbNewLine & vbNewLine & _
            "Ejemplo= /NOTEPAD.EXE /administrador@dominio.com /mypassword", vbExclamation)
        
    Exit Sub
        End If

        sParams = Split(Command$, "/")

        For Each sParam In sParams
            If Trim(sParam) <> "" Then
                i = i + 1
                If i = 1 Then sApp = Trim(sParam)
                If i = 2 Then sUser = Trim(sParam)
                If i = 3 Then sPwd = Trim(sParam)
            End If
        Next

        fReturn = fRunAsEx(sApp, sUser, sPwd)
        If fReturn = 0 Then MsgBox(sError, vbExclamation)

    End Sub

    Public Function fRunAsEx(ByVal sFileName As String, _
      ByVal
sUserName As String, ByVal sUserPwd As String) As Long

        'Declaració vars
        Dim lpBuffer As String * 200
        Dim fReturn As Long
        Dim lpUsername As String, lpDomain As String
        Dim lpPassword As String, lpApplicationName As String
        Dim lpCommandLine As String, lpCurrentDirectory As String
        Dim StartInfo As STARTUPINFO, ProcessInfo As PROCESS_INFORMATION
       
lpApplicationName = sFileName
        lpUsername = sUserName
        lpPassword = sUserPwd
        lpCommandLine = vbNullString
        lpCurrentDirectory = vbNullString
        StartInfo.cb = LenB(StartInfo)
        StartInfo.dwFlags = 0&

        fReturn = CreateProcessWithLogon(StrPtr(lpUsername), StrPtr(lpDomain), _
          StrPtr(lpPassword), LOGON_WITH_PROFILE, StrPtr(lpApplicationName), _
          StrPtr(lpCommandLine), CREATE_DEFAULT_ERROR_MODE Or CREATE_NEW_CONSOLE _
          Or CREATE_NEW_PROCESS_GROUP, ByVal 0&, StrPtr(lpCurrentDirectory), _
          StartInfo, ProcessInfo)

        If fReturn = 0 Then
            If GetLastError = 0 Then
               
sError = "La operación no se ha podido completar con éxito."
           
Else
   
            FormatMessage FORMAT_MESSAGE_FROM_SYSTEM, ByVal 0&, _
                  GetLastError, LANG_NEUTRAL, lpBuffer, 200, ByVal 0&
                sError = lpBuffer
            End If
        End If

        fRunAsEx = fReturn
        CloseHandle(ProcessInfo.hThread)
       
CloseHandle(ProcessInfo.hProcess)

    End Function

 

Ejecutarlo en un caso real

Una vez compilado el código obtenemos un ejecutable de Windows (RunAsEx.exe) que para más comodidad copiaremos en el directorio %system (así no tenemos que especificar el path completo cada vez que hagamos una llamada), y a continuación podemos elegir ejecutarlo desde la línea de comandos (fig. 3), o bien crear otra aplicación VB muy sencilla que realice la llamada a través del comando "Shell" (fig. 4):


(fig. 3)


(fig. 4)

 

Bueno, pues ya hemos terminado con esto. Si quieres puedes descargarte el proyecto de ejemplo y ya me contarás...


Un saludo y hasta pronto!

Lluís Franco

Principat d'Andorra, May 2005

:-)