Tuesday, January 10, 2012

MS11-014: this is not the bug your are looking for …


It could be believed that patch management was an outdated topic for year 2011. However, I have still been asked by a client to challenge their internal patch management policy by delivering a working exploit faster than the XX-day period they waited before patch deployment (XX being somewhere between 10 and 99 - I love random figures like this ;).

This event occurred in February 2011. Having a look at the gorgeous monthly Microsoft release, we decided to target MS11-014 (“Vulnerability in Local Security Authority Subsystem Service Could Allow Local Elevation of Privilege ”), since local bugs are usually easier to exploit reliably.

As this story occurred one year ago, I assume that everybody had enough time to patch. And local exploits are not “wormable”, therefore releasing this information will not result in the end of Internet.

Patch analysis

The patch itself is piece of cake: only LSASRV.DLL has changed, and only 2 functions have changed within that DLL.

FMyPrimitiveHMACParam() only had a few extra NOPs added.

On the other hand, NegpMapLogonRequest() is an excellent candidate, for an extra size check has been added (as shown below in bold red) – sorry for providing unformatted Hex-Rays pseudo-code, I know it is lame ;)

signed int __stdcall NegpMapLogonRequest(char *a1, void *a2, unsigned int a3, struct _MSV1_0_INTERACTIVE_LOGON **a4) {
  if ( v6 > 0x100u || *(_WORD *)v5 > 0x100u
  || (v7 = *((_WORD *)a1 + 2), v7 > 0x1FEu) )
    return -1073741562;

a1 is not really a char* but rather a LSA_UNICODE_STRING, defined as such:

typedef struct _LSA_UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR Buffer;

NegpMapLogonRequest() has two direct callers: NegpCloneLogonSession() and NegpIsLocalOrNetworkService(), which is in turn called from NegLogonUserEx2().

Those APIs are internal to LSASS, and are exposed to other processes through loosely documented LPC calls. The easiest way to trigger that piece of code from any process is to rely on the official LogonUserEx API.

Debugging LSASS

Debugging LSASS locally can be tricky, for it is a critical system process that is involved in the debugging subsystem itself. Do not expect to be able to attach OllyDbg and go away with it … The easiest way to do it seems to rely on the Kernel Debugger itself, which has also the ability to debug any userland process.

That being done, WinDbg confirms that our target function is indirectly called from LogonUserEx() indeed, plus the offending string is the user-supplied domain name

kd> kv
ChildEBP RetAddr
00acfc8c 757434c5 LSASRV!NegpMapLogonRequest
00acfcb4 75742e41 LSASRV!NegpIsLocalOrNetworkService+0x2f
00acfcf8 75742891 LSASRV!NegLogonUserEx2+0xaa
00acfe98 757422ae LSASRV!LsapAuApiDispatchLogonUser+0x33b
00acfeac 75739481 LSASRV!LpcLsaLogonUser+0x22
00acfec4 757393a5 LSASRV!DispatchAPI+0x46
00acff50 75738cfa LSASRV!LpcHandler+0x153
00acff74 75738dbe LSASRV!SpmPoolThreadBase+0xb9
00acffb4 7c80b729 LSASRV!LsapThreadBase+0x91
00acffec 00000000 kernel32!BaseThreadStart+0x37

Wait … does it mean that calling LogonUserEx() with a domain name over 0x200 characters is enough to trigger that bug ? Unfortunately not: because there is no bug … The logon triplet (username, domain, password) will be rejected as invalid – which it is.

Where is the meat?

There is still one missing piece in the puzzle: where is the bug? No vulnerable string copy is to be found anywhere within LSASRV.DLL. And providing an oversized domain name will not result in any crash.

At this point, there are two possible ways to go: one is hard work. The other is laziness efficiency. As Jorge Moura (from Primavera BSS) is credited for the discovery, I emailed him. Not only did he respond over the night, but he also provided me with a test vector. Which turns to be something like:

  LOGON32_LOGON_NEW_CREDENTIALS, // defined as 9
  LOGON32_PROVIDER_DEFAULT, // defined as 0
  &hToken );
ImpersonateLoggedOnUser( hToken );
  NULL, // security attributes

The trick is to specify the LOGON32_LOGON_NEW_CREDENTIALS flag, which has the following effect:

This logon type allows the caller to clone its current token and specify new credentials for outbound connections. The new logon session has the same local identifier but uses different credentials for other network connections.

In that case, new credentials are not immediately checked, but rather stored “as is” in memory for future use. And the crash occurs when the authentication package – namely MSV1_0.DLL – makes use of those new credentials.

Vulnerable function is SspMapContext() in which lies an unbounded, inlined memcpy(). Destination is the local function stack … What else? ;)

Exploitation details

Despite being a “classical”, size-unlimited, Unicode stack overflow, generic exploitation of this bug can be tricky on an up-to-date Windows XP SP3 target.

LSASS process and all Microsoft-provided DLLs that are loaded by default within that process benefit from PEB and stack randomization, are flagged as /NXCOMPAT and /SAFESEH. The offending function is itself protected by a stack cookie (as a result of /GS option). Exploitation is one-shot, since LSASS process death will notoriously result in a forced system reboot.

During that particular assignment, it turned out that Symantec (ex-Sygate) personal firewall also loads SYSFER.DLL (version 1.0.0 at that time – if it means something) into LSASS address space. As this DLL has been compiled without any security option, and given that Windows XP does not provide any ASLR for code mappings, this DLL has been used as a gadget provider for ROP-like exploitation. After some MIASM magic, all client boxes were reliably 0wn3d – thanks to Symantec security products being installed ;)

Outro (a.k.a. TL;DR)

In summary:
  • The effective bug has not been fixed. Any other API that would allow passing an oversized domain name to LSASS could result in triggering the very same bug within MSV1_0.
  • Leave figures to risk managers and top-level management only. It makes no sense trying to define metrics such as “days before public exploit”, when unqualified exploit writers can provide a reliable attack vector within 2 days – not to mention all the people who had access to this flaw before public release. And after one year, this issue is still marked as “no public exploit available” on Microsoft summary page.
  • According to the original discoverer, such a trivial bug (think: oversized domain name provided during user authentication) was found by accident during a software QA session.
  • According to Microsoft security bulletin, this bug only affects Windows XP and Windows 2003. It is assumed (without checking) that memcpy() has eventually been defined as dangerous and replaced as much as possible by memcpy_s(). Is Microsoft aware of the number of security issues it killed? Who knows …


Anonymous said...

Using a third-party DLL (Symantec) to do ROP and own the machine is not a very good idea. Yeah, I know, you owned the machine but in the real world ... is there really a chance? I don't think so. This is just a PoC not a real world exploit.

Anonymous said...

I normally hate commenting, but i thought i would give it a go for this blog. Been reading your stuff for awhile.

Anonymous said...

Nice work, regards

KroKite said...

Does it only affect winXP SP3 ?

newsoft said...

@KroKite yes, Windows XP SP3 is affected (as stated by Microsoft).