Security Briefs: Customizing GINA, Part 2 -- MSDN Magazine, June 2005

来源:百度文库 编辑:神马文学网 时间:2024/05/02 01:03:21
Customizing GINA, Part 2
Keith Brown
more ...
Print
E-mail
Add to Favorites
Rate
RSS (Security Briefs)
Add RSS to Any
Related Articles
Live Spaces
Digg This
BlogThis!
Slashdot
del.icio.us
Technorati
Explore the code.
Download the code.
Get the sample code for this article. NEW:Explore the sample code online!
- or -
Code download available at:SecurityBriefs0506.exe (274KB)
Contents
WlxNegotiate and WlxInitialize
WlxDisplaySASNotice
WlxLoggedOutSAS
Calling LsaLogonUser
Other WlxLoggedOutSAS Considerations
WlxActivateUserShell
WlxLoggedOnSAS and WlxDisplayLockedNotice
WlxWkstaLockedSAS
Trivial Functions
Supporting Remote Desktop
Conclusion
GINA,the Graphical Identification and Authentication component, is a part ofWinLogon that you can customize or replace. Last month I introducedGINA customization; this month, I‘m going to drill down to implementeach of the GINA entry points. If you have not read last month‘sSecurity Briefs column, I strongly suggest that you start there beforediving into this one (seeSecurity Briefs: Customizing GINA, Part 1).
Icovered the two simple functions WlxNegotiate and WlxInitialize in lastmonth‘s column. WlxNegotiate and WlxInitialize allow you to negotiateversions with WinLogon and exchange context handles for statemanagement. WinLogon gives GINA a handle (hWlx), and GINA givesWinLogon a pointer to its internal state (pWlxContext). Myimplementation uses a class called Gina to hold this state, whichdispatches each exported GINA function to member functions on the Ginaclass. I instantiate an instance of the Gina class during the second ofthese functions, WlxInitialize, so both Negotiate and Initialize areimplemented as static methods on the Gina class.
My sample GINAuses Ctrl+Alt+Del secure attention sequence (SAS) events just like thedefault GINA does. If you need this functionality, in WlxInitialize youshould call WlxSetOption and set the WLX_OPTION_USE_CTRL_ALT_DEL optionto TRUE. If you forget to do this, you will not get any SAS events whenthe user presses Ctrl+Alt+Del.

Thenext function normally called by WinLogon is WlxDisplaySASNotice, andas with all the rest of the functions you‘ll see, the export from myDLL simply takes the GINA context (pWlxContext), casts it to my Ginaclass, and calls a corresponding method, as shown here:
VOID WINAPI WlxDisplaySASNotice(PVOID pWlxContext) {LDB(L"-->WlxDisplaySASNotice");((Gina*)pWlxContext)->DisplaySASNotice();LDB(L"<--WlxDisplaySASNotice");}
 
Note the logging macros before and after the call. Thisis how each of my exported functions is implemented: I log that thefunction was called, I dispatch the call to the Gina class, then I logthat I‘m returning. LDB (which stands for Log Debug) compiles to NULLfor release builds. In debug builds, this generates a line of output ina log file on the local hard disk. Since debugging a deployed GINA istricky, I find that a liberal sprinkling of debug log messages coupledwith the debug entrypoint I discussed last month can help you avoid theneed for a symbolic debugger entirely during development.
Now let‘s look at the implementation of Gina::DisplaySASNotice:
void Gina::DisplaySASNotice() {NoticeDialog dlg(_pWinLogon, IDD_SASNOTICE);dlg.Show();}This really is a simple function. You should display a modal dialog boxthat tells the user how to log in. The odd thing about this dialog isthat it has no buttons on it, and no close box, so there‘s no way forthe user to dismiss it (see Figure 1).
 

Figure 1 The SAS Notice Dialog
AsI discussed last month, you‘ll call back into WinLogon whenever youwant to display modal dialogs. If you look at the NoticeDialog class,you‘ll see that it derives from a base class called GinaModalDialog,which provides a very basic framework for handling modal dialogs. Itcalls into WinLogon‘s WlxDialogBoxParam method to display the dialog,passing a pointer to itself as the parameter, which the dialogprocedure then grabs via WM_INITDIALOG and tucks away as part of thewindow state. This is coupled with a virtual method named DialogProcthat each derived dialog may implement however it likes, which makesdialog state management very simple. I just use member variables fordialogs that need to maintain state. Conceptually this is similar tothe way MFC worked, although it‘s considerably simpler.
Thenotice dialog doesn‘t do a thing. It just waits for WinLogon to dismissit, typically because the user pressed Ctrl+Alt+Del. If your GINA needsto listen for custom SAS events on another thread, your notice dialogshould watch for a custom window message that you define (WM_USER, forexample), and call WlxSasNotify when this message comes in. Yourbackground thread should post this message whenever it detects thecustom SAS. As I mentioned last month, this keeps all your interactionwith WinLogon on a single thread, as it should be.

WlxLoggedOutSAS
WlxLoggedOutSASis the next function WinLogon will call in your GINA, when the userpresses Ctrl+Alt+Del or you generate a custom SAS from your noticedialog. This is by far the most complicated function your GINA willimplement, although conceptually it‘s not that difficult. This is whereGINA collects credentials of some sort from the user, and either logsthe user in or rejects the logon attempt. My sample displays a dialogasking for a user name and password, as shown in Figure 2.
int WlxLoggedOutSAS(IN PVOID pWlxContext,IN DWORD dwSasType,OUT PLUID pAuthenticationId,OUT PSID pLogonSid,OUT PDWORD pdwOptions,OUT PHANDLE phToken,OUT PWLX_MPR_NOTIFY_INFO pNprNotifyInfo,OUT PVOID* pProfile);
 

Figure 2 The Logged Out SAS Dialog
Besidesyour context pointer (pWlxContext), WinLogon only passes in one otherparameter: dwSasType. Microsoft defines several SAS types, reservingthe range of values 0 to 127. If you define your own SAS type, makesure its value is 128 or greater. There are two SAS types my samplecares about:
WLX_SAS_TYPE_CTRL_ALT_DELWLX_SAS_TYPE_AUTHENTICATEDThe second SAS type (WLX_SAS_TYPE_AUTHENTICATED) is currentlyundocumented, but you must handle it if you want to fully support theWindows® XP Remote Desktop feature. I‘ll discuss it later.
 
The return value from WlxLoggedOutSAS tells WinLogon what GINA wants it to do:
WLX_SAS_ACTION_NONEWLX_SAS_ACTION_SHUTDOWNWLX_SAS_ACTION_LOGONYou should return NONE if you want to cancel the logon attempt (forexample, if the user presses the Cancel button or supplies invalidcredentials too many times). SHUTDOWN tells WinLogon to shut down thesystem. To demonstrate this, my sample allows any user to press theShutdown button on the logon dialog. However, in your own GINA, you maynot want an anonymous user to be able to do this.
 
LOGONindicates that GINA has successfully logged on the user. If you returnthis value, you must also supply the other out parameters fromWlxLoggedOutSAS that indicate the details of the logon. This includes atoken handle, profile path information, and other details. Your bestbet for obtaining these values comes from calling the low-levelfunction LsaLogonUser, which is complicated enough to warrant its ownsection.

Calling LsaLogonUser
TheLsaLogonUser function accepts credentials and produces a logon sessionif the credentials are valid. More specifically, this functiondispatches the request to an authentication package such as Kerberos orMSV1_0 (the Windows NT® logon provider),which will then verify the credentials and establish a session. Becausethere are many different forms of credentials, ranging from a simpleuser name and password to a Kerberos server ticket and authenticator,the input to this function is defined as a binary BLOB that will beinterpreted by the underlying authentication package. This BLOB ispassed via the AuthenticationInformation parameter shown inFigure 3.
Thereare a lot of parameters to this method, so I‘ve taken the liberty ofmarking the input and output parameters. I‘m not going to explain whateach and every one of these parameters is for; you can find thosedetails in the documentation. Instead I‘ll focus on what the currentdocumentation doesn‘t tell you, and give you tips on using thisfunction from a custom GINA.
My sample supports both domainlogons (Kerberos) and local workstation logons (MSV1_0); fortunatelythe input BLOB for both of these providers looks exactly the same.Here‘s the MSV1_0 structure definition:
typedef struct _MSV1_0_INTERACTIVE_LOGON {MSV1_0_LOGON_SUBMIT_TYPE MessageType;UNICODE_STRING LogonDomainName;UNICODE_STRING UserName;UNICODE_STRING Password;} MSV1_0_INTERACTIVE_LOGON;
 
Now here‘s the tricky part. The UNICODE_STRING fieldsare pointers to buffers that contain Unicode characters for the domain,user name, and password. You might be tempted to allocate these stringbuffers separately. If you do this, LsaLogonUser will fail. Instead youmust dynamically allocate a block of memory large enough to hold thedata structure shown inFigure 3,plus all the string buffers that go along with it. In other words, youmust serialize this data structure into a single contiguous bufferbefore calling LsaLogonUser. Rather than bore you with the code, I‘llsimply point you to the helper function that forms this request. It‘scalled _allocLogonRequest, and it can be found in theSecurityHelper.cpp file in the sample code available for download fromthe MSDN®Magazine Web site.
GINAshould specify a LogonType of "Interactive" when logging in a user fromWlxLoggedOutSAS, unless that user is logging in remotely via RemoteDesktop, in which case "RemoteInteractive" should be used. Specify aLogonType of "Unlock" when allowing a user to unlock the workstation.
Theonly other tricky parameter to LsaLogonUser is LocalGroups, whichallows the caller to specify any number of extra Security Identifiers(SIDs) that should be added to the resulting token as it‘s returned.Think about that for a moment. You have the power to make any user anadministrator, for example. Heck, your GINA could include a checkbox,"Make me an admin for this session." If checked, you could specify thewell-known SID for the local Administrators group via LocalGroups, andthe user would suddenly be an administrator for the length of thatlogon session.
Don‘t get me wrong; I‘m not suggesting that you dothis, just like I‘m not suggesting that you hardcode a backdoor intoyour GINA that allows anyone with a user name of "h@x0r" to log inusing the built-in administrator account without knowing the password.But GINA can do these sorts of things, which is why I mentioned lastmonth that you must put a strong access control list (ACL) on yourcustom GINA DLL file to ensure that a normal user doesn‘t overwrite itwith a malicious GINA. And your GINA had better be bulletproof. Youdon‘t want an attacker to exploit a buffer overflow in a GINA andsubsequently run arbitrary code there. Note that LsaLogonUser may onlybe called by code running in the trusted computing base, which GINA ispart of by virtue of running inside WinLogon, which runs as SYSTEM. Anormal user cannot call LsaLogonUser to elevate privileges.
My sample GINA passes NULL for LocalGroups, and unless you have a good reason not to, yours should as well.
Severaloutputs from LsaLogonUser are useful in your implementation ofWlxLoggedOutSAS. For example, GINA must cache the Token parameter aspart of its state. Also, the LogonId LUID is the unique 64-bitidentifier for the new logon session, and it maps directly to thepAuthenticationId out parameter for WlxLoggedOutSAS.
TheProfileBuffer output from LsaLogonUser is a BLOB whose format dependson the logon provider. Once again, the Kerberos and MSV1_0 providersagree on the format of this BLOB, which is shown inFigure 4.This structure contains a wealth of information that a fully featuredGINA needs. For example, the default GINA checks the PasswordMustChangefield to see if the user‘s password is due to expire soon, and givesthe user a chance to change it right then and there. Besidesconveniences like this, you‘ll need to look at the ProfilePath field tobe able to properly fill out the pProfile out parameter inWlxLoggedOutSAS, which is another BLOB that must be serialized.

Other WlxLoggedOutSAS Considerations
OnceLsaLogonUser returns, if you‘ve successfully established a logon, youcan cache the user name and domain as part of your state forconvenience, as they will be needed elsewhere. Another alternativewould be to simply look up the user name and domain whenever they areneeded, since you‘re already caching the user‘s token, but for domainaccounts that will require round-trips to a domain controller, so Isimply cache these values in member variables on the Gina class.
WinLogonnormally loads the user‘s profile after WlxLoggedOutSAS returns with asuccessful logon. To get this default behavior, my sample sets*pdwOptions to 0. Your GINA can customize how the profile is loaded bysetting this flag to WLX_LOGON_OPT_NO_PROFILE, which tells WinLogonthat GINA has already loaded the profile.
The pNprNotifyInfooutput parameter allows other network providers such as Novell Netwareto get a peek at the user name and password being used so they canautomatically log the user into their networks as well. This preventsthe user from having to log on multiple times.
The pLogonSidoutput parameter points to a fixed sized buffer provided by WinLogonthat is large enough to hold a logon SID of the form S-1-5-5-x-y, where x and yare unique values that identify the new logon session. Unfortunately,LsaLogonUser doesn‘t supply this SID, but you can find it by looking inthe token. Ask GetTokenInformation for the TokenGroups class ofinformation, then enumerate the group SIDs in the token until you findthe one with the SE_GROUP_LOGON_ID flag. You can then use the CopySIDfunction to copy this value into the buffer provided by pLogonSid.WinLogon uses this SID to grant permissions to the interactive windowstation and desktop. You can read more about this atWhat Is A Window Station.
Payclose attention to the return value from LsaLogonUser. If it fails, mysample is careful to check if the account requires an immediatepassword change or if the user‘s password has expired. If so, I pop upa password change dialog to give the user a chance to change herpassword. On any other failures I look up the corresponding errormessage by calling FormatMessage, display it to the user, and thenreturn WLX_SAS_ACTION_NONE.

WlxActivateUserShell
Once the user is logged on, GINA is asked to launch the shell.
BOOL WlxActivateUserShell(IN PVOID pWlxContext,IN PWSTR pszDesktopName,IN PWSTR pszMprLogonScript,IN PVOID pEnvironment);
 
My sample does what most GINAs should do: it launchesUSERINIT.EXE. Well, technically it looks in WinLogon‘s registry key fora named value called Userinit, and launches whatever programs arespecified in this comma-delimited string. Most machines will have avalue that looks something like this:
"C:\WINDOWS\system32\userinit.exe,"This little program is responsible for running logon scripts andcalling CreateProcess to start the user‘s shell, which is named inanother value aptly called "Shell". Most machines will have thefollowing value for Shell:"Explorer.exe"
 
The trick here is that you can‘t simply callCreateProcess to launch USERINIT.EXE. If you did, it would run asSYSTEM just like GINA! Instead, you must call CreateProcessAsUser,which takes one extra argument: the handle to the token that your GINAgot earlier from calling LsaLogonUser. This will cause USERINIT.EXE andconsequently the user‘s shell to run in the new logon session you‘vecreated.
Also, be careful to specify the desktop and environmentgiven to you by WinLogon. You can see my call to CreateProcessAsUser inthe SecurityHelper.cpp file in the sample code. Once you return fromthis function, your GINA won‘t be called again until somethinginteresting happens, such as the user pressing Ctrl+Alt+Del, in whichcase you‘ll see a call to WlxLoggedOnSAS.

WlxLoggedOnSAS and WlxDisplayLockedNotice
WinLogoncalls WlxLoggedOnSAS when a user is logged on and a SAS occurs. Theonly interesting argument is dwSasType. My sample watches forWLX_SAS_TYPE_CTRL_ALT_DEL and pops up the dialog shown in Figure 5.

Figure 5 The Logged-On SAS Dialog
Theimplementation of this function is simply an exercise in getting someuser input and returning a value to WinLogon indicating what the userneeds. The only significant functionality you need to implement is achange password dialog, which should call NetUserChangePassword toattempt a password change.
To give you an idea of how easy this function is to implement, here are the constants you can return from it:
WLX_SAS_ACTION_NONEWLX_SAS_ACTION_LOCK_WKSTAWLX_SAS_ACTION_LOGOFFWLX_SAS_ACTION_PWD_CHANGEWLX_SAS_ACTION_TASKLISTWLX_SAS_ACTION_SHUTDOWN_REBOOTWLX_SAS_ACTION_SHUTDOWNWLX_SAS_ACTION_SHUTDOWN_SLEEPWLX_SAS_ACTION_SHUTDOWN_HIBERNATEWLX_SAS_ACTION_SHUTDOWN_POWER_OFFSo if the user presses the Task Manager button, GINA simply returns theTASKLIST value, and WinLogon will launch the user‘s task manager. Tolock the workstation, return LOCK_WKSTA, and so on. If the user asks toshut down the computer, my sample asks for confirmation and thenreturns the SHUTDOWN value, but if you wanted to, you could call thepower management APIs to determine what other options are available,such as suspend or hibernate, and give the user further options.
 
TheWlxDisplayLockedNotice function is as simple as WlxDisplaySASNotice. Infact, my sample code uses the same dialog class to display this dialog;it just uses a different dialog resource to get an appropriateinterface. This dialog also has no buttons and will be dismissed onlywhen a SAS is detected.

WlxWkstaLockedSAS
WinLogoncalls WlxWkstaLockedSAS when a logged-on user has locked herworkstation and a SAS occurs. This function is similar toWlxLoggedOutSAS, except that it takes no out parameters:
int WlxWkstaLockedSAS(IN PVOID pWlxContext,IN DWORD dwSasType);
 
The trick to implementing this function is realizingthat there are two use cases to be considered. The normal case is wherethe user who locked her workstation is returning and unlocking it. Thecorner case is when someone other than the logged-on user tries tounlock the computer. If that person is an administrator, they should beallowed to forcefully log off the user in order to access theworkstation themselves.
Here‘s the approach I take in the sample.I pop-up my logon dialog, and populate the name and domain of thecurrently logged-on user that I cached earlier in WlxLoggedOutSAS. Ithen give keyboard fo-cus to the password edit box. This simplifies thenormal case where the user is simply unlocking her own workstation.
Oncethe password prompt returns, I attempt to log the user by callingLsaLogonUser with a logon type of Unlock, which is a special type oflogon designed just for GINA, and properly audits the attempt to unlockthe workstation.
If the logon succeeds, I look at the resultingtoken and compare the user SID with the SID of the currently logged-onuser. If these SIDs are the same, then I know the user has simplyreturned to unlock her workstation. I close the new token and returnWLX_SAS_ACTION_UNLOCK_WKSTA to WinLogon to indicate that theworkstation should be unlocked.
If the SIDs don‘t match, I checkto see if the new user is an administrator by callingCheckTokenMembership, looking for the well-known local Administratorsgroup SID.
If the user is an administrator, I returnWLX_SAS_ACTION_FORCE_LOGOFF, and WinLogon logs off the user and bringsGINA back around to display the logon prompt.

Trivial Functions
Thereare several trivial functions that my sample implements, so includingWlxIsLockOk, WlxIsLogoffOk, WlxLogoff, WlxShutdown, and others, I‘llmention them briefly here. Their names are usually self-explanatory.
InWlxIsLockOk and WlxIsLogoffOk, I simply return TRUE. My sample GINAnever stops anyone from logging off or locking the workstation,although yours might need to do this.
In WlxLogoff, I clear anyvariables that have to do with the currently logged-on user. This meansclosing the user‘s token and freeing the cached strings for the username and domain.
WlxShutdown is a pretty obvious notification. Ioriginally assumed that this was the last message I‘d ever receive fromWinLogon (that‘s my experience so far). But I‘ve been told by reliablesources that it may be possible to receive status notifications evenafter WlxShutdown has been called. With this in mind, I keep enough ofthe GINA alive to process any of these extra messages that happen tocome along.
WlxNetworkProviderLoad is obsolete, according tosources inside Microsoft. My GINA simply returns FALSE here, since theentry point is never actually called by WinLogon.
And finally,DisplayStatusMessage, RemoveStatusMessage, and GetStatusMessage allowWinLogon to give me messages for the user from time to time, which Idisplay in a modeless dialog box.

Supporting Remote Desktop
Inorder to support Terminal Services and the Windows XP Remote Desktopfeature, you need to make special considerations in your code. Thinkabout what happens when you use the remote desktop client, MSTSC.EXE,to log onto a remote workstation. What happens when you pressCtrl+Alt+Del? You‘re raising an interrupt on your local hardware, noton the remote machine.
If you want to be friendly to RemoteDesktop, the first thing to do is detect whether you‘re running in asession for a remote user. That information will be useful to youthroughout your GINA‘s lifetime, and it‘s easy to discover:
bool UserIsRemote() {return 0 != GetSystemMetrics(SM_REMOTESESSION);}My sample GINA uses this to skip the Ctrl+Alt+Del requirement forremote desktop users by calling WlxSasNotify to simulate Ctrl+Alt+Delon their behalf. You can see this in my implementation ofWlxDisplaySASNotice.
 
Whena user first logs on through Remote Desktop on Windows XP, somethingrather magical happens inside WinLogon, and GINA must help facilitateit. If you have two workstations side by side you can see this happen.Say workstation A just booted up and is waiting for someone to log on.Its GINA is waiting in WlxDisplaySASNotice. On workstation B, youconnect to A via remote desktop and log on. As soon as you enter yourlogon credentials via B‘s screen, you‘ll see something change back onA‘s screen. What‘s happening here is a shuffle of terminal servicessessions. In Remote Desktop, the operating system ensures that only oneuser is ever connected to the machine, either via the console or via aremote session. Part of this bookkeeping is ensuring that the logged-onuser is always working in Terminal Services session zero.
In thiscase, the GINA that was loaded on workstation A is running in session0, but the user is connecting via a new session. Session 1 is atemporary session with its own instance of WINLOGON.EXE, which has alsoloaded your GINA. This copy of GINA authenticates the user in session1, and then the magic shuffle starts. After you return fromWlxLoggedOutSAS with a successful logon, you‘ll see a call toWlxGetConsoleSwitchCredentials in session 1. This is the last chanceyour GINA has to communicate the results of the logon before itdisappears forever!
The data structure defined for this functionis pretty complicated, but you don‘t need to fill it all out. One thingyou absolutely need to pass is the token you just obtained fromLsaLogonUser. WinLogon also expects the UserName field (things won‘twork properly if you don‘t pass at least this field).
WinLogon insession 1 now marshals this data, pipes it over to session 0, and callsthe GINA‘s WlxLoggedOutSAS there with the special valueWLX_SAS_TYPE_AUTHENTICATED. This constant isn‘t in the documentation,but that‘s an oversight, not a secret. This is your signal to grab thedata from the session 1 GINA by callingWlxQueryConsoleSwitchCredentials, and return the user‘s token toWinLogon in session 0.
Interestingly enough, at this point if youcall the UserIsRemote function I described earlier, you‘ll see thatsession 0 has suddenly gone remote! The user on workstation B is nowusing session 0, and the login will proceed as normal. Session 1 hasbeen terminated. Back on workstation A‘s console, yet another temporaryTerminal Services session has been constructed, typically session 2 inthe scenario I‘ve described. This session exists to inform the userthat the workstation is in use, and allow the user to switch back tothe console if so desired.
Another thing you‘ll want to check foris whether the user supplied credentials via the Remote Desktop client.You can discover this by calling WinLogon‘s WlxQueryTsLogonCredentialsfunction. If the user has already provided credentials, you don‘t wantto prompt her to supply them again, so this is mainly a convenience.
Ifthis all sounds complicated, you‘re right, it really is. That‘s why theteam working on the next version of Windows (code-named "Longhorn") istrying to get rid of this rather messy interface once and for all. Butif you‘re customizing the logon experience on Windows XP and need tosupport Remote Desktop, you must jump through these hoops. The sampleaccompanying this article illustrates how it‘s done, including a numberof details that I didn‘t have room to discuss in these two columns.

Conclusion
Writinga custom GINA is not easy. MSGINA is a very complicated piece ofmachinery, and replacing it is not trivial. But a lot of folks havefound it necessary over the years. If you can‘t use the stub type GINAthat I discussed last month and therefore are forced to write a customGINA from scratch, I hope you find these columns with theiraccompanying samples helpful in your quest. Just remember that you areimplementing the heart of the interactive logon plumbing in Windows,and it‘s critical that your code be absolutely correct and bulletproof!To share knowledge with other GINA developers, please visit my wiki atCustomizing GINA.

Send your questions or comments for Keith at  briefs@microsoft.com.
NEW:Explore the sample code online! - or - Code download available at:SecurityBriefs0506.exe (274KB)