Artykuły

A A A
Drukuj Ekportuj do PDF
Opublikowane: 2008.10.21 21:46 | Michał Grzegorzewski | Aktualizacja: 2011.10.06 16:38

Zabawy z LSA - wydłubywanie haseł usług

tagi: Windows
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.

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

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

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, okreś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.

Do tekstu dołączony jest prosty program, który 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 też 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!)

Attachment(s): SAPD

Michał Grzegorzewski (mgrzeg)

Autor: Michał Grzegorzewski (mgrzeg)

Michał prowadzi darmowy magazyn Zine.NET - polski magazyn o .NET. Autor serwisu blogerskiego zine.net.pl. Były lider Warszawskiej Grupy .NET, aktualnie jej aktywny uczestnik. Aktywny użytkownik serwisów wss.pl oraz codeguru.pl

Komentarze 2 Masz uwagi do tej strony? Napisz

Użytkownik usunięty 2008.11.02 16:36
0 oceń pozytywnie   oceń negatywnie 0
avatar
 
Nie to, że się czepiam.
Malutka literówka tutaj ?

"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. "

Przeczytany, coś na oderwanie, dzięki.
basiaw7 2008.11.03 8:19
0 oceń pozytywnie   oceń negatywnie 0
avatar VIP
 
poprawione :) - od strony redakcyjnej współodpowiedzialna ;)


ps.
takim (administratorom :) jak ja nie bardzo orientują się w kodzie i dlatego myślą że to artykuł nie bardzo dla nich polecam uruchomienie dołączonego programu, bo jak na mnie efekt robi wrażenie ;)

pozdrawiam,
Basia

-------------------------------------
http://basiaw7.spaces.live.com/

pozdrawiam,
Basia

-------------------------------------
blog

Dodaj komentarz

avatar

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.

Autor Michał Grzegorzewski
avatar
 

Załóż konto
WSS to serwis, który łączy dziesiątki tysięcy specjalistów IT w Polsce, zajmujących się szeroko pojętymi technologiami Microsoft. Portal działa od 2003 roku, i oprócz setek publikacji technicznych, rozwijającego się forum - portal to ludzie, którzy go tworzą. To właśnie z myślą o nich warto codziennie nas odwiedzać.

Dowiedz się więcej o WSS

vGuru - Zostań Guru Wirtualizacji

 

MetroOne

Idź na górę strony