(pretend) No-restart UAC elevation

Share your advanced PureBasic knowledge/code with the community.
lexvictory
Addict
Addict
Posts: 1027
Joined: Sun May 15, 2005 5:15 am
Location: Australia
Contact:

(pretend) No-restart UAC elevation

Post by lexvictory »

I say pretend because it really does involve an app (re)start.
How it works: restarts the exe with a command line parameter (defined by a constant) as admin-mode, and sends it commands via a named pipe. (debugger compatible)
It also only requires ONE UAC elevation prompt no matter how many commands are sent (unless the sub process exits abnormally or something goes wrong with the pipe) - just add a ClosePipe() after sending a command to change this.
Unicode/x64 compatible.

All you need do is extract the code from inside the start/end include file blocks in the first file, and add your commands to the client file.
In debugger mode, the client exe will emit a debugger warning, so you can tell which debugger it is (in case you use the standalone debugger for everything like me)
The tab char is used in the communication to separate the command from its parameters (see client.pb)
Comments are included (including procedure pane ones for important bits), so there should be no difficulty understanding it
Included example runs cmd from the admin mode client when the button is clicked.

Save this somehere:

Code: Select all

EnableExplicit

;*** begin include file ***
#pipeName = "\\.\pipe\FakeUACNoRestart"
#admin_CommandFlag = "/admin"
#pipe_maxChars = 1024;- change this if you need more than 1024 chars per command
global  fconnected, Hpipe, pipe_hProcess

;Thanks to freak @ http://purebasic.fr/english/viewtopic.php?p=237513#237513 for this proc
#BCM_FIRST = $1600
#BCM_SETSHIELD = #BCM_FIRST + $000C
Procedure Button_SetElevationRequiredState(hwndButton, fRequired);Show/Hide the UAC shield icon. (set fRequired to #True or #False)
  SendMessage_(hwndButton, #BCM_SETSHIELD, 0, fRequired)
EndProcedure

#SEE_MASK_UNICODE = $00004000
Procedure RunProgramAdmin(windowid, exe.s, params.s, workingdir.s);returns a handle that you must CloseHandle_()!
  Protected shellex.SHELLEXECUTEINFO
  shellex\cbSize = SizeOf(SHELLEXECUTEINFO)
  shellex\fMask = #SEE_MASK_NOCLOSEPROCESS
  CompilerIf #PB_Compiler_Unicode
    shellex\fMask = shellex\fMask|#SEE_MASK_UNICODE
  CompilerEndIf
  shellex\hwnd = windowid
  shellex\lpVerb = @"runas"
  shellex\lpFile = @exe
  shellex\lpParameters = @params
  shellex\lpDirectory = @workingdir
  shellex\nShow = #SW_SHOW
  ShellExecuteEx_(shellex)
  ;debug shellex\hProcess
  ;debug Hex(shellex\hInstApp)
  ProcedureReturn shellex\hProcess
EndProcedure

Procedure ProcessRunning(hProcess)
  Protected exitcode.l
  GetExitCodeProcess_(hProcess, @exitcode)
  if exitcode = #STILL_ACTIVE
    ProcedureReturn 1
  else
    ProcedureReturn 0
  endif 
EndProcedure

Procedure PipeInit(pipeHandle)
  fConnected = ConnectNamedPipe_(pipeHandle, #Null)
EndProcedure

Procedure SendPipe(pipeHandle, tosend.s)
  Protected sentbytes, returnval
  returnval = WriteFile_(pipeHandle, tosend, StringByteLength(tosend), @sentbytes, 0) 
  FlushFileBuffers_(pipeHandle)
  ProcedureReturn returnval
EndProcedure

Procedure ClosePipe()
  if Hpipe 
    SendPipe(hPipe, "end")
    DisconnectNamedPipe_(Hpipe)
    CloseHandle_(Hpipe)
    Hpipe = 0
  Endif
  if pipe_hProcess
    CloseHandle_(pipe_hProcess)
  Endif
EndProcedure

Procedure DoAdminAction(parentwindow, action.s)
  Protected thid
  if Hpipe and (ProcessRunning(pipe_hProcess) = 0)
    ClosePipe()
    DoAdminAction(parentwindow, action)
    ProcedureReturn
  ElseIf not Hpipe
    Hpipe=CreateNamedPipe_(#pipeName, #PIPE_ACCESS_DUPLEX, #PIPE_TYPE_MESSAGE | #PIPE_READMODE_MESSAGE, 1, #pipe_maxChars* SizeOf(Character), #pipe_maxChars* SizeOf(Character), 3000, #Null) 
    thid = CreateThread(@PipeInit(), Hpipe)
    CompilerIf #PB_Compiler_Debugger;-this will interfere with the process running checks. disable it (put "0 or" in front of the constant) to let the check work
      pipe_hProcess = RunProgramAdmin(parentwindow, #PB_Compiler_Home+"compilers\pbdebuggerunicode.exe", #DQUOTE$+ProgramFilename()+#DQUOTE$+" "+#admin_CommandFlag, GetCurrentDirectory());we run the debugger, because otherwise if you start a console app, like cmd.exe, it will inherit the console debugger's console.
    CompilerElse
      pipe_hProcess = RunProgramAdmin(parentwindow, ProgramFilename(), #admin_CommandFlag, GetCurrentDirectory())
    CompilerEndIf
    fconnected = 0
    if pipe_hProcess
      WaitThread(thid);-instead of this you could make it timeout and call closepipe() if you dont want to wait forever.
    endif 
    If not fConnected : MessageRequester("Error", "Could not connect pipe") : end : endif ;- End keyword here!! (remove if you don't want to exit)
  endif 
  if Hpipe
    if SendPipe(hPipe, action) = 0;would return 0 if the pipe is damaged
      ClosePipe()
      DoAdminAction(parentwindow, action)
    endif 
  endif 
EndProcedure

if ProgramParameter(0) = #admin_CommandFlag
  IncludeFile "client.pb";- admin mode client file.
  end 
endif 
;*** end include file ***

Define EventID, quit.i
OpenWindow(0,0,0, 400,100, "Pipe Server", #PB_Window_SystemMenu|#PB_Window_ScreenCentered)
ButtonGadget(0, 50,35,300,25, "Perform Admin Action")
Button_SetElevationRequiredState(GadgetID(0), #True)
Repeat
    EventID = WaitWindowEvent()
    Select EventID
      case #PB_Event_Gadget
        DoAdminAction(WindowID(0), "cmd")
      Case #PB_Event_CloseWindow
        Quit = 1
    EndSelect
   
Until Quit = 1 

ClosePipe()
Save this as client.pb in the same dir as the other file

Code: Select all

CompilerIf #PB_Compiler_Debugger
  Import ""
    PB_DEBUGGER_SendWarning(Message.p-ascii)
  EndImport
  PB_DEBUGGER_SendWarning("I am the admin mode client instance");this is so you can tell which debugger is running the client
CompilerEndIf

define pipe_wait, pipe_file, pipe_readbuf.s, pipe_bytesread
pipe_wait=WaitNamedPipe_(#pipeName, #Null)
If pipe_wait
    pipe_file = CreateFile_(#pipeName, #GENERIC_READ |#GENERIC_WRITE, 0, #Null, #OPEN_EXISTING,0, #Null)
    If pipe_file
      Repeat
        pipe_readbuf.s=Space(#pipe_maxChars)
        if ReadFile_(pipe_file , @pipe_readbuf,StringByteLength(pipe_readbuf), @pipe_bytesread, 0) = 0
          CloseHandle_(pipe_file)
          end 
        endif 
        Select Trim(Lcase(StringField(pipe_readbuf, 1, chr(9))))
          Case "cmd"
            RunProgram("cmd.exe")
          case "end";DO NOT REMOVE THIS UNLESS YOU CHANGE THE EXITING METHOD.
            CloseHandle_(pipe_file)
            end 
        EndSelect
      ForEver
    EndIf     
Endif
Please let me know if you find a bug, as I (will) use it in my IDE.
Demonio Ardente

Currently managing Linux & OS X Tailbite
OS X TailBite now up to date with Windows!
SFSxOI
Addict
Addict
Posts: 2970
Joined: Sat Dec 31, 2005 5:24 pm
Location: Where ya would never look.....

Post by SFSxOI »

Thanks for the post. I'll play around with it some. I've just been using this by its self for a while now which seemed to work fine:

Code: Select all

Procedure Elevated_Cmd(app_dir.s, app_name.s)
AppVerb$ = "runas"
AppName$ = app_name
AppDir$ = app_dir

shExecInfo.SHELLEXECUTEINFO 
shExecInfo\cbSize=SizeOf(SHELLEXECUTEINFO) 
shExecInfo\fMask=#Null 
shExecInfo\hwnd=#Null 
shExecInfo\lpVerb=@AppVerb$
shExecInfo\lpFile=@AppName$ 
shExecInfo\lpDirectory=@AppDir$
shExecInfo\nShow=#SW_NORMAL

exe.i = ShellExecuteEx_(shExecInfo)

EndProcedure
lexvictory
Addict
Addict
Posts: 1027
Joined: Sun May 15, 2005 5:15 am
Location: Australia
Contact:

Post by lexvictory »

I'd be doing the same thing, apart from the fact that I can't really restart a whole IDE just to run one or two admin actions.
Plus it gives the one-prompt benefit.
(you'll also notice I use pretty much the same code as you to restart the app in admin-client mode)
Demonio Ardente

Currently managing Linux & OS X Tailbite
OS X TailBite now up to date with Windows!
freak
PureBasic Team
PureBasic Team
Posts: 5944
Joined: Fri Apr 25, 2003 5:21 pm
Location: Germany

Post by freak »

Why use a named pipe ? It just makes your program open to attack (if that is a concern). Somebody just has to open the pipe and he can control your admin program, or create the pipe before you do and by this deny your admin action. I would just use an anonymous pipe (with inheritable handle) and pass the handle on the commandline to the child process. Its not more complicated but eliminates some easy ways of attack.

But then again, security probably isn't a big issue in an IDE. Which brings me to the question: How many admin tasks does your IDE perform anyway ? Isn't its primary function to edit code ? ;)
quidquid Latine dictum sit altum videtur
lexvictory
Addict
Addict
Posts: 1027
Joined: Sun May 15, 2005 5:15 am
Location: Australia
Contact:

Post by lexvictory »

freak wrote:with inheritable handle
As far as I knew, handles aren't inheritable across the admin/limited user divide... (I remember reading something about it in the CodeProject article I read that does more or less the same thing as this - its closed source though)
Though if you can tell me otherwise, I'll look into it.
freak wrote:How many admin tasks does your IDE perform anyway ?
TailBite (as a DLL) :wink:
And possibly in the future UserLib management.
Demonio Ardente

Currently managing Linux & OS X Tailbite
OS X TailBite now up to date with Windows!
User avatar
Rescator
Addict
Addict
Posts: 1769
Joined: Sat Feb 19, 2005 5:05 pm
Location: Norway

Post by Rescator »

freak wrote:It just makes your program open to attack (if that is a concern).
Actually, any program whether it needs elevation or not is open to attack.
If the user is able to start it then any other program running under that user can manipulate the other program or exe.
This is why commercial software has signed executables.

Then again, if the users account has been compromised there are larger issues to worry about.

As far as pipes go I guess you are right in that another user account could compromise a different user, which is definitely a issue in multi user environments.
Post Reply