Neat testing technique (Part Two)

| | Comments (3)

Last time we looked into the theory behind running an application as a restricted process for testing purposes. Today we're going to look at the code needed to make everything work. I apologize in advance for the formatting of the code. I've still yet to get MT figured out and have no idea how to get the formatting correct (or the RSS feed, according to Steve!). But there will be a downloadable project at the end of the post, so that should clear up most points of confusion.

The first thing to look at are the declares we're going to be using. Thankfully, they're all pretty straight-forward. This came as quite the surpise to me since most of the security APIs rely on obscure structures which require a lot of patience to get right in a BASIC environment.

Declare Function GetCurrentProcess Lib "Kernel32" () as Integer
This is used as a way for us to get a pseudo-handle to the current process for use with the OpenProcessToken API. This is one of the very few Win32 APIs which returns a handle that does not need to be closed.

Declare Function OpenProcessToken Lib "AdvApi32" ( handle as Integer, access as Integer, ByRef token as Integer ) as Boolean
This API is the way in which we get the token for the currently executing process. The basic idea is that you pass in a handle to the process you wish to open, and what access rights you need on that token. The API passed back (ByRef) the process token handle.

Declare Sub CloseHandle Lib "Kernel32" ( handle as Integer )
Used to close handles for cleanup purposes. Most handles need to be cleaned up eventually, so you'll see calls to this scattered around.

Declare Function CreateRestrictedToken Lib "AdvApi32" ( existing as Integer, flags as Integer, disableCount as Integer, _
disable as Ptr, deleteCount as Integer, delete as Ptr, restrictCount as Integer, restrict as Ptr, ByRef newToken as Integer ) as Boolean

This API is the guts behind the "old" style restricted token access. It is what takes the existing token and make a new (and modified) clone of the original.

Declare Function DuplicateTokenEx Lib "AdvApi32" ( handle as Integer, access as Integer, attribs as Integer, _
securityLevel as Integer, type as Integer, ByRef newToken as Integer ) as Boolean

When we're not making a restricted token directly, we still need to be able to clone the existing token. This API is the correct way to clone an existing token, so that's why we're using it.

Declare Function SetTokenInformation Lib "AdvApi32" ( handle as Integer, infoClass as Integer, infoBuffer as Ptr, infoBufferLen as Integer ) as Boolean
The SetTokenInformation API is how we assign a new integrity level to a token on Windows Vista.

Declare Function CreateProcessAsUserW Lib "AdvApi32" ( token as Integer, appName as WString, cmdLine as Integer, procAttribs as Integer, _
threadAttribs as Integer, inheritsHandles as Boolean, createFlags as Integer, env as Integer, currentDir as Integer, startup as Ptr, procInfo as Ptr ) as Boolean

Regardless of how we make the restricted token, we end up calling this API to create the process that we want to launch.

(Note: I'm removing error checking and extraneous declarations from my code examples for brevity.)

The way in which we create a restricted token on Vista is to string together calls to GetProcessHandle, OpenProcessToken, DuplicateTokenEx and SetTokenInformation, like so:

OpenProcessToken( GetCurrentProcess, TOKEN_DUPLICATE, tokenHandle )
DuplicateTokenEx( tokenHandle, MAXIMUM_ALLOWED, 0, SecurityAnonymous, _
TokenPrimary, restrictedToken )

mandatoryLabel.Ptr( 0 ) = GetIntegrityLevelSID( SECURITY_MANDATORY_LOW_RID )
mandatoryLabel.Long( 4 ) = SE_GROUP_INTEGRITY
SetTokenInformation( restrictedToken, TokenIntegrityLevel, mandatoryLabel, labelSize )

If we were instead going to use the restricted token approach (instead of integrity level), then we'd be stringing together calls to GetCurrentProcess, OpenProcess and CreateRestrictedToken, like this:


OpenProcessToken( GetCurrentProcess, _
TOKEN_DUPLICATE Or TOKEN_ASSIGN_PRIMARY Or TOKEN_QUERY, tokenHandle )

sidsToDisable.Ptr( 0 ) = GetAdminSID
sidsToDisable.Long( 4 ) = 0
CreateRestrictedToken( tokenHandle, 0, 1, sidsToDisable, 0, _
nil, 0, nil, restrictedToken )

Finally, once we've gotten a token with restricted access, it's a simple matter of calling CreateProcessAsUser, like such:


CreateProcessAsUserW( restrictedToken, path, 0, 0, 0, false, _
0, 0, 0, startup, procInfo )

I've thrown together a comment-less example project which demonstrates how to piece all of these APIs together to allow you to launch any executable with restricted rights. Note that this project isn't using soft declares (and, obviously, it only works on Windows), so you'll have to be running Windows 2000 or higher (at least, I think that's the lowest version of Windows you can use for this). Also, I'm making use of some newer constructs in REALbasic (such as private structures), so you should be using REALbasic 2007r3 to test the project out (though, with minor tweaks, I'm sure you can use earlier versions).

The example project can be found here: http://ramblings.aaronballman.com/hosted_files/RestrictedRun.rbp

3 Comments

Wonderful Aaron.
Logged in as Administrator (XP)
Created small test project to write to HKEY_CLASSES_ROOT
Built version works standalone, debug version works from IDE.

Launch built app from RestrictedRun, I get a RegistryAccessErrorException
Launch IDE from RestrictedRun and run debug version, I get a RegistryAccessErrorException

Perfect for testing!

Excellent! I was worried that people weren't interested in the topic because of the lack of noise.

This was quite useful for me as well.

I've been developing Windows apps for many years but had never thought to use these APIs this way.

We've done testing in limited user accounts before- but if it had been done it was a part of the testing process rather than something a developer easily did- because many things developers did "required" admin rights. So testers would just log in as a user with low privileges and then run the app, and see if it died. It was helpful a few times, but that back and forth got old pretty quick.

This method is much more effective and lets developers test straight away. Thanks again!

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.