Tekst ten dedykuję Grzesiowi Tworkowi i Pauli Januszkiewicz, których tegoroczna sesja na MTS natchnęła mnie do tego, żeby opisać parę poruszonych tam tematów na blogu.
Na pewno wielokrotnie zastanawialiście się nad tym, w jaki sposób zapisać jakąś tajemną informację tak, żeby nikt niepowołany nie mógł się do niej dobrać. Weźmy dla przykładu automatyczne logowanie do systemu.
Winlogon jakiego nie znamy?
W ramach swojego wsparcia technicznego, Microsoft proponuje rozwiązanie polegające na edycji zawartości klucza rejestru, a mianowicie HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon i ustawieniu:
- DefaultUserName - nazwa użytkownika, dla którego ma zachodzić automatyczne logowanie;
- DefaultPassword - hasełko użytkownika wpisanego w DefaultUserName;
- AutoAdminLogon - 1 oznacza, że ma zachodzić automatyczne logowanie, 0 - nie.
I wszystko w porządku, gdyby nie to, że hasełko w DefaultPassword jest wpisane jawnym tekstem i dostępne bez większego problemu dla każdego, kto może odczytać zawartość tego klucza. Bezpieczeństwo takiego rozwiązania pozostawię każdemu do samodzielnej oceny. Moment! - ktoś może powiedzieć - przecież jest bezpieczniejsze rozwiązanie!
A i owszem!
Podczas swojego startu, proces Winlogon sprawdza zawartość wspomnianego wyżej klucza i na jego podstawie podejmuje decyzję o automatycznym logowaniu. Jednak chwilę wcześniej Winlogon sięga do LSA po DefaultPassword i jeśli coś dostanie w odpowiedzi, to nie omieszka z tego skorzystać, zamiast wpisu w rejestrze. Prawdę powiedziawszy, Microsoft zaleca tę właśnie metodę jako sposób ochrony hasła DefaultPassword.
LsaStorePrivateData - odsłona pierwsza
Jednak co to znaczy, że Winlogon sięga do LSA po DefaultPassword? Otóż upraszczając, wywoływana jest funkcja LsaRetrievePrivateData z biblioteki advapi32.dll z próbą odczytania wartości Sekretu dla klucza 'DefaultPassword'.
Sekretu? A cóż to takiego???
Podsystem Lsass udostępnia mechanizm umożliwiający zapisywanie wrażliwych danych do 'zasobnika LSA' w postaci zaszyfrowanej oraz na żądanie dostęp do tych danych. I tak: żeby zapisać interesujące nas dane, wystarczy użyć funkcji LsaStorePrivateData, gdzie w postaci parametrów przekazujemy parę klucz - wartość, czyli np. 'DefaultPassword' i 'Pa$$w0rd', natomiast do wyłuskania zapisanych danych należy skorzystać ze wspomnianej wcześniej funkcji LsaRetrievePrivateData z parametrem określającym klucz.
Microsoft podzielił dane, które możemy w ten sposób zapisywać na 4 grupy:
- dane lokalne - nazwy z tej grupy rozpoczynają się prefiksem 'L$'. Dane lokalne można odczytać tylko na komputerze, na którym te dane są przechowywane. Dodatkowo wpadają tu dane, których nazwy zaczynają się od '$machine.acc', 'SAC', 'SAI', 'SANSC', żeby wymienić tylko kilka;
- dane globalne - tu nazwy rozpoczynają się od prefiksu 'G$'. Dane globalne utworzone na kontrolerze domeny są replikowane na pozostałe kontrolery domeny;
- dane komputera - ich nazwy rozpoczynają się od 'M$'. Zgodnie z dokumentacją dostęp do nich ma wyłącznie system operacyjny. Do tej grupy trafiają również nazwy zaczynające się od 'NL$', lub '_SC_';
- dane prywatne - te nie wymagają żadnego przedrostka. Są dostępne z zewnętrznych komputerów, ale nie są replikowane w domenie.
Warto zwrócić uwagę na dane komputera (machine data), które możemy zapisywać korzystając z LsaStorePrivateData, natomiast nie możemy odczytywać przy wykorzystaniu LsaRetrievePrivateData.
Poniższe fragmenty kodu pokazują, jak korzystać z obu funkcji przy użyciu P/Invoke. Jest to właściwie przeniesienie przykładowego kodu do zmiany DefaultPassword do środowiska .NET. Można również skorzystać z przykładowego kodu do funkcji LsaRetrievePrivateData w ramach pinvoke.net.
public static string RetrieveData(string secretName)
{
string secretValue = "";
long retcode = 0;
IntPtr zero = Marshal.AllocHGlobal(0);
Win32.LSA_UNICODE_STRING systemName = new Win32.LSA_UNICODE_STRING();
IntPtr policyHandle = IntPtr.Zero;
Win32.LSA_OBJECT_ATTRIBUTES objectAttributes = new Win32.LSA_OBJECT_ATTRIBUTES();
objectAttributes.Length = 0;
objectAttributes.RootDirectory = IntPtr.Zero;
objectAttributes.Attributes = 0;
objectAttributes.SecurityDescriptor = IntPtr.Zero;
objectAttributes.SecurityQualityOfService = IntPtr.Zero;
retcode = Win32.LsaNtStatusToWinError(Win32.LsaOpenPolicy(ref systemName, ref objectAttributes, (int) Win32.LSA_AccessPolicy.POLICY_CREATE_SECRET, out policyHandle));
if (retcode == 0)
{
IntPtr secretData;
Win32.LSA_UNICODE_STRING[] lsa_unicode_stringArray = new Win32.LSA_UNICODE_STRING[] { new Win32.LSA_UNICODE_STRING() };
lsa_unicode_stringArray[0].Buffer = Marshal.StringToHGlobalUni(secretName);
lsa_unicode_stringArray[0].Length = (ushort)(secretName.Length * 2);
lsa_unicode_stringArray[0].MaximumLength = (ushort)((secretName.Length + 1) * 2);
Win32.LsaRetrievePrivateData(policyHandle, lsa_unicode_stringArray, out secretData);
if (secretData != IntPtr.Zero)
{
Win32.LSA_UNICODE_STRING lsa_unicode_string2 = (Win32.LSA_UNICODE_STRING)Marshal.PtrToStructure(secretData, typeof(Win32.LSA_UNICODE_STRING));
secretValue = Marshal.PtrToStringAuto(lsa_unicode_string2.Buffer);
}
Win32.LsaClose(policyHandle);
}
Win32.FreeSid(zero);
return secretValue;
}
public static long StoreData(string secretName, string secretData)
{
long retcode = 0;
IntPtr zero = Marshal.AllocHGlobal(0);
Win32.LSA_UNICODE_STRING systemName = new Win32.LSA_UNICODE_STRING();
IntPtr policyHandle = IntPtr.Zero;
Win32.LSA_OBJECT_ATTRIBUTES objectAttributes = new Win32.LSA_OBJECT_ATTRIBUTES();
objectAttributes.Length = 0;
objectAttributes.RootDirectory = IntPtr.Zero;
objectAttributes.Attributes = 0;
objectAttributes.SecurityDescriptor = IntPtr.Zero;
objectAttributes.SecurityQualityOfService = IntPtr.Zero;
retcode = Win32.LsaNtStatusToWinError(Win32.LsaOpenPolicy(ref systemName, ref objectAttributes, (int) Win32.LSA_AccessPolicy.POLICY_CREATE_SECRET, out policyHandle));
if (retcode == 0)
{
Win32.LSA_UNICODE_STRING[] lsa_unicode_stringArray = new Win32.LSA_UNICODE_STRING[] { new Win32.LSA_UNICODE_STRING() };
lsa_unicode_stringArray[0].Buffer = Marshal.StringToHGlobalUni(secretName);
lsa_unicode_stringArray[0].Length = (ushort)(secretName.Length * 2);
lsa_unicode_stringArray[0].MaximumLength = (ushort)((secretName.Length + 1) * 2);
Win32.LSA_UNICODE_STRING[] privateData = new Win32.LSA_UNICODE_STRING[] { new Win32.LSA_UNICODE_STRING() };
privateData[0].Buffer = Marshal.StringToHGlobalUni(secretData);
privateData[0].Length = (ushort)(secretData.Length * 2);
privateData[0].MaximumLength = (ushort)((secretData.Length + 1) * 2);
Win32.LsaStorePrivateData(policyHandle, lsa_unicode_stringArray, privateData);
Win32.LsaClose(policyHandle);
}
Win32.FreeSid(zero);
return retcode;
}
Do tego niezbędne importy na potrzeby P/Invoke
internal static class Win32
{
[DllImport("advapi32")]
public static extern IntPtr FreeSid(IntPtr pSid);
[DllImport("advapi32.dll")]
public static extern uint LsaClose(IntPtr ObjectHandle);
[DllImport("advapi32.dll")]
public static extern uint LsaNtStatusToWinError(uint status);
[DllImport("advapi32.dll")]
public static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, int DesiredAccess, out IntPtr PolicyHandle);
[DllImport("advapi32.dll")]
public static extern uint LsaRetrievePrivateData(IntPtr PolicyHandle, LSA_UNICODE_STRING[] KeyName, out IntPtr PrivateData);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern uint LsaStorePrivateData(IntPtr PolicyHandle, LSA_UNICODE_STRING[] KeyName, LSA_UNICODE_STRING[] PrivateData);
public enum LSA_AccessPolicy : long
{
POLICY_AUDIT_LOG_ADMIN = 0x200L,
POLICY_CREATE_ACCOUNT = 0x10L,
POLICY_CREATE_PRIVILEGE = 0x40L,
POLICY_CREATE_SECRET = 0x20L,
POLICY_GET_PRIVATE_INFORMATION = 4L,
POLICY_LOOKUP_NAMES = 0x800L,
POLICY_NOTIFICATION = 0x1000L,
POLICY_SERVER_ADMIN = 0x400L,
POLICY_SET_AUDIT_REQUIREMENTS = 0x100L,
POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x80L,
POLICY_TRUST_ADMIN = 8L,
POLICY_VIEW_AUDIT_INFORMATION = 2L,
POLICY_VIEW_LOCAL_INFORMATION = 1L
}
[StructLayout(LayoutKind.Sequential)]
public struct LSA_OBJECT_ATTRIBUTES
{
public int Length;
public IntPtr RootDirectory;
public Win32.LSA_UNICODE_STRING ObjectName;
public uint Attributes;
public IntPtr SecurityDescriptor;
public IntPtr SecurityQualityOfService;
}
[StructLayout(LayoutKind.Sequential)]
public struct LSA_UNICODE_STRING
{
public ushort Length;
public ushort MaximumLength;
public IntPtr Buffer;
}
}
I przykładowe użycie może wyglądać następująco:
static void Main(string[] args)
{
StoreData("alamakota", "ala ma kota");
string s = RetrieveData("alamakota");
Console.WriteLine(s);
}
Uzbrojeni w maszynkę do odczytywania i zapisywania sekretów możemy już swobodnie ustawiać sobie DefaultPassword i usunąć odpowiedni element w kluczu Winlogon w rejestrze. Nareszcie bezpieczni!
Lsa(Store | Retrieve)PrivateData internals
Po pierwszej euforii nadchodzi chwila refleksji: 'A gdzie właściwie zapisywane są te dane?'. W dokumentacji do obu funkcji, Microsoft twierdzi, że:
"The data stored by the LsaStorePrivateData function is not absolutely protected. However, the data is encrypted before being stored, and the key has a DACL that allows only the creator and administrators to read the data."
Z jednej strony mamy zaufanie do systemu, że zadba o nasze dane, z drugiej - okazuje się, że wcale nie jest tak różowo. Otóż okazuje się, że dane przekazane przez LSASS zapisywane są w rejestrze, w gałęzi HKLM\Security\Policy\Secrets. Standardowo tylko system ma dostęp do tego klucza, więc jeśli chcecie się do niego dobrać, to musicie to zrobić w kontekście konta systemowego. W systemach przed Vistą najprościej skorzystać z polecenia systemowego at:
>at 10:23 /interactive regedit.exe
albo, już niezależnie od systemu i jeszcze prościej, z sysinternalsowego psexec:
>psexec -i -s regedit.exe
Przykładowa zawartość sekretu zapisanego wcześniej opisanymi metodami wygląda tak, jak to przedstawia Rysunek 1.

Rysunek 1. Przykładowy sekret :)
Przeglądając zawartość klucza HKLM\SECURITY\Policy\Secrets możemy znaleźć wiele podkluczy, których nazwy zaczynają się od L$, M$, ale także w formacie SCM:{GUID}, o których nie będę się rozpisywał.
Naszą uwagę przykują jednak klucze o nazwach zaczynających się na _SC_.
Sekrety usług
Jeśli postanowimy, żeby któraś z usług uruchamiana była z konta innego niż systemowe, to zostaniemy poproszeni o wprowadzenie nazwy użytkownika i hasła, w ramach którego usługa ma startować. Jest to jednorazowa czynność i musimy tylko pamiętać o tym, że gdy zmienimy hasło dla tego użytkownika, to musimy ponownie odwiedzić zakładkę 'Logowanie', gdzie ponownie będziemy musieli wprowadzić nasze dane. Zapewne zastanawialiście się kiedyś, gdzie przechowywane są te informacje i czy można się do nich jakoś dobrać. No właśnie, ciekawe, nie? :)
Weźmy dla przykładu usługę Telnet (jeśli jeszcze ją mamy w systemie, a w przypadku braku, możemy wybrać sobie inną ofiarę, której zepsucie nic nam nie zaszkodzi ;)) i ustawmy logowanie z dowolnego konta.

Rysunek 2. Zakładka logowanie dla usługi Telnet.
Zajrzyjmy teraz na chwilę do książki Marka Russinovicha i Davida Solomona o internalsach windowsów, a dokładniej do rozdziału poświęconemu uruchamianiu usług. Interesuje nas w szczególności fragment:
"SCM [Service Control Manager] zapisuje dane o usługach uruchomionych na koncie innym niż systemowe poprzez wywoływanie funkcji Lsass LsaLogonUser. Funkcja LsaLogonUser standardowo wymaga hasła, ale SCM sygnalizuje dla Lsass, że hasło jest przechowywane 'sekretnie' w kluczu HKLM\SECURITY\Policy\Secrets w rejestrze. [..] Kiedy SCM wywołuje LsaLogonUser, ukreśla typ logowania jako logowanie usługi, tak by Lsass szukał hasła w podkluczu Secrets o nazwie _SC_<nazwa usługi>. SCM poleca Lsass, by ten przechowywał hasło logowania w sekrecie, podczas gdy SCP konfiguruje informacje logowania usługi." [2]
Aha! Tu Cię mamy!
Mr Jingle podpowiada mi w tym momencie, że czuje już zapach hasła.
Po modyfikacji ustawień logowania dla usługi Telnet, w sekretach w rejestrze pojawił się dodatkowy podklucz, _SC_TlntSvr. To by potwierdzało słowa Wielkich Autorów, więc nie czekając długo na dalsze zachęty, łapię szybko za kod do wydłubywania sekretów i dopisuje w mainie krótką linijkę, dla testu:
static void Main(string[] args)
{
string s = RetrieveData("_SC_TlntSvr");
Console.WriteLine(s);
}
Kompiluję, uruchamiam i... nic :(. Po prostu pusta linia. No tak, przecież sekrety zaczynające się od _SC_ należą do kategorii systemowych, a te można tylko zapisywać, odczytywanie nie działa. Upewniam się jeszcze, czy sam mogę utworzyć sekret systemowy i później go odczytać:
static void Main(string[] args)
{
StoreData("M$alamakota", "ala ma kota");
string s = RetrieveData("M$alamakota");
Console.WriteLine(s);
}
i oczywiście znowu puściutko. Dla pewności sprawdzam jeszcze wcześniejszy kod i przy zapisie do 'alamakota', odczyt działa prawidłowo. A więc tak, jest bezpiecznie!
W tym momencie Mr Jingle trąca mnie nosem. 'Nie, to nie może być takie proste!' - odpowiadam. Sięgam jednak po otwarty regedit, eksportuję do pliku zawartość klucza HKLM\SECURITY\Policy\Secrets\_SC_TlntSvr\CurrVal, zmieniam '_SC_TlntSvr' na 'alamakota' i importuję. Potem uruchamiam wcześniej przygotowany kod i ... na konsoli wypisuje się hasełko, wklepane wcześniej sumiennie w oknie ustawień logowania usługi Telnet.
Uwagi końcowe
Podsystem LSA umożliwia zapisanie zaledwie 4096 sekretów, z czego połowa zarezerwowana jest dla systemu operacyjnego. Przechowywanie sekretów z udziałem LSA dostępne jest od Windows NT 4.0 i obecnie Microsoft nie zaleca korzystania z opisanych metod do zapisywania swoich sekretów. Począwszy od Windows 2000 dostępny jest interfejs ochrony danych DPAPI z metodami CryptProtectData i CryptUnprotectData, jednak w tym przypadku to programista musi zadbać o przechowywanie wrażliwych danych, co - jak widać - może być jednak lepszym rozwiązaniem. Trzeba również pamiętać, że większość z opisanych metod wymaga odpowiednich przywilejów, dostępnych praktycznie rzecz biorąc wyłącznie dla administratorów systemów.
Z oczywistych względów nie udostępniam gotowego narzędzia, a jedynie opisuję technikę.
[Edit: Postanowiłem jednak udostępnić prostą wersję programu, ktory wykonuje opisane w tekście czynności. Wszystko oczywiście wyłącznie do celów 'edukacyjnych'. W przyszłości zamierzam rozwijać to narzędzie o kolejne elementy, które postaram się sumiennie opisywać na blogu. Program wymaga zainstalowanego .NET Framework w wersji 2.0 oraz uruchamianie z konta systemowego.]
Od dawien dawna dostępne jest narzędzie, które wyłuskiwało sekrety systemowe korzystając z dll injection, jednak od pewnego czasu ta technika zastosowana do Lsass powoduje załamanie tego procesu i wymusza reset komputera.
Sprawdziłem opisaną metodę na Windows 2000 Server, Windows XP oraz Windows Vista, wszędzie z tym samym rezultatem.
Źródła (podaję polskie tłumaczenia, dostępne jeszcze na rynku - są tańsze od oryginałów i znośnie przetłumaczone):
1. 'Bezpieczny kod. Tworzenie i zastosowanie' - Michael Howard i David LeBlanc. (Ech, czas kupić kolejne wydanie tej rewelacyjnej książki!)
2. 'Microsoft Windows 2000 od środka' - David A. Solomon, Mark E. Russinovich. (Tu też już jest kolejne wydanie i naprawdę nie wiem, czemu go jeszcze nie mam w swojej biblioteczce!)