W poprzednim odcinku serii przybliżałem KTM oraz Transactional NTFS (TxF). Tym razem skupię się na nowościach związanych z tzw. Sesją 0 oraz zmianami w funkcjonowaniu serwisów interaktywnych.
Stacje okienkowe, pulpity i sesje
Zanim jednak przejdziemy do nowości w Viście spróbuję przybliżyć kilka pojęć, które są często mylone i wokół których występuje wiele nieporozumień.
W trakcie tworzenia, procesy Win32 są automatycznie kojarzone przez podsystem Win32 ze stacją okienkową (window station). Stacje okienkowe zawierają schowek, pulpit(y) i tablicę atomów, czyli obiektów typu event, mutex, semaphor, etc. Domyślnie interaktywna stacja okienkowa zawiera trzy pulpity: default, Winlogon oraz screen-saver. Domyślny pulpit tworzony jest w momencie uruchamiania procesu przez użytkownika i służy do interakcji użytkownika z systemem. Po wciśnięciu kombinacji ctrl+alt+del przenosimy się na pulpit Winlogon, na którym możemy np. zmienić hasło. Pulpit ten dostępny jest dla nas również przed zalogowaniem. Trochę informacji na ten temat można znaleźć na stronach MSDN.
Jedyną stacją okienkową, w której dostępny jest interfejs użytkownika oraz możliwa jest interakcja jest WinSta0.

Spy++ z uaktywnioną rejestracją niektórych komunikatów klawiatury pracujący na pulpicie Winlogon. Widoczna wspólna kolejka komunikatów wszystkich procesów znajdujących się na tym samym pulpicie.
Pulpity należące do jednej stacji okienkowej współdzielą schowek i tablicę atomów, jednak mają osobne kolejki komunikatów. Tym samym nie ma możliwości wysłania komunikatu z pulpitu domyślnego na Winlogon, etc. Oczywiście nie ma również takiej możliwości pomiędzy różnymi stacjami okienkowymi, dla których pozostałe elementy również nie są współdzielone (tzn schowek i tablica atomów).
Korzystając z API Win32 możemy tworzyć własne stacje okienkowe i pulpity, a także przełączać się między nimi (procesy między window stations a wątki między desktopami). Należy jednak pamiętać, że tylko stacja okienkowa o nazwie WinSta0 może być stacją interaktywną, pozostałe – nie.
Sesja jest pojęciem związanym z usługami terminalowymi. Sesja jest kontenerem, który zawiera stacje okienkowe oraz inne obiekty; stacja interaktywna dla danej sesji musi nazywać się WinSta0. Oczywiście, jak to już wcześniej napisałem, każda stacja zawiera domyślnie 3 pulpity.

Process Explorer z ustawioną kolumną informującą o numerze sesji oraz podgląd aktualnej stacji okienkowej na której pracuje zaznaczony proces (winlogon). Na fioletowo zaznaczone procesy, które pracują w kontekście tego samego użytkownika, który uruchomił procexpa.
Usługi
Dochodzimy do jednego z najważniejszych punktów tego tekstu.
Spośród wszystkich, Sesja 0 (Session 0) jest wyróżniona – jest to tak zwana konsola, czyli ta sesja, w której możemy pracować siedząc bezpośrednio przy komputerze (albo korzystając z narzędzi typu VNC). Wszystkie usługi skojarzone są z Sesją 0; jest to jedyna sesja, w której mogą pracować usługi.
Zaznaczając ‘Zezwalaj usłudze na współdziałanie z pulpitem’ powodujemy, że przy starcie proces usługi wiązany jest z desktopem Default stacji okienkowej WinSta0. Jeśli tego nie zrobimy, usługi wiązane są z nieinteraktywnymi stacjami okienkowymi. Usługi nieinteraktywne uruchomione w kontekście określonego użytkownika współdzielą stację okienkową, w przypadku usług pracujących na różnych kontach tworzone są kolejne stacje okienkowe. Nazwy stacji okienkowych w przypadku usług uruchomionych w kontekście różnych użytkowników mają postać ‘Service-0x0-id$‘, np. usługi uruchomione na Windows 2003 oraz Windows XP w kontekście LocalSystem współdzielą window station o nazwie ‘Service-0x0-3e7$’, LocalService - ‘Service-0x0-3e5$’, NetworkService - ‘Service-0x0-3e4$’, etc. Nazwy mogą się nieznacznie różnić w zależności od wersji (Windows 2000, Windows XP, Windows 2003) oraz dostępności odpowiedniego konta.

WinObj w akcji. Widoczne dwie sesje - konsola (0) oraz sesja terminalowa (1). W prawym oknie - lista stacji okienkowych powiązanych z Sesją 0. Zrzut wykonany pod Windows 2003.
Wyobraźmy sobie teraz sytuację, że usługa pragnie nam o czymś zakomunikować, ewentualnie domaga się naszej interakcji z oknem formularza wystawionym przez tę usługę. I tak: w przypadku usług interaktywnych odpowiednie okno wyświetlane jest w ramach Sesji 0 i pulpitu Default stacji WinSta0, bez względu na to, kto jest aktualnie zalogowany, a w przypadku usług nieinteraktywnych komunikat trafia również do Sesji 0, jednak na stację okienkową powiązaną z daną usługą i ‘wisi’ tam w oczekiwaniu na nasze zmiłowanie. Oczywiście, nie mogąc zobaczyć odpowiedniego komunikatu, jedyne co możemy zrobić to zresetować usługę.
Potencjalne problemy
Takie rozwiązanie ma swoje oczywiste słabości. Usługa pracująca w kontekście użytkownika mocno uprzywilejowanego (np. LocalSystem, albo – co gorsza – konta domenowego admistratora) może w pewnym momencie wyświetlić jakiś komunikat, albo okno konfiguracyjne użytkownikowi aktualnie korzystającemu z Sesji 0, który może w ten sposób uzyskać dodatkowe przywileje. Jest to podstawą ataku typu ‘shatter attack’.
Zanim jednak usługa interaktywna podłączy się do WinSta0, sprawdzana jest wartość HKLM\SYSTEM\CurrentControlSet\Control\Windows\NoInteractiveServices, którą administratorzy mogą ustawić na ‘1’, unikając wspomnianych problemów. I – do czasu Visty – jest to jedyny sposób na ochronę usług interaktywnych.
Vista
Skoro wyjaśniliśmy sobie już najważniejsze pojęcia, możemy przejść do zmian wprowadzonych w Viście w związku z obsługą Sesji 0.
Biorąc pod uwagę problemy, jakie mogą się pojawić, gdy procesy użytkownika współdzielą sesję wraz z serwisami, podjęto decyzję o całkowitej separacji procesów użytkownika od procesów usług. Usługi – tak jak do tej pory – działają wyłącznie w Sesji 0, natomiast pierwszą sesją dostępną do pracy interaktywnej jest Sesja 1. W przypadku, gdy zachodzi konieczność wyświetlenia komunikatu przez serwis interaktywny, usługa ‘Interactive Services Detection’ o wiele mówiącej nazwie pliku UI0Detect.exe odnajduje aktywną sesję dowolnego z administratorów i wyświetla na domyślnym pulpicie odpowiedni komunikat. Administrator może wówczas przenieść się na tytułowy błękitny pulpit i zareagować jakoś na okno wystawione przez usługę (pulpit istotnie jest psychodeliczny i nie zachęca do częstych odwiedzin :) !). Microsoft zaznacza jednak, że możliwość ‘teleportacji’ administratora z jego aktywnej sesji na Sesję 0 jest przejściowa i w następnej wersji systemu zostanie usunięta. Tym samym twórcy usług muszą przemyśleć sposób interakcji usługi z użytkownikiem i odpowiednio zmodyfikować swoje programy.
Przykładem znanej aplikacji, która zmusza nas do interakcji w ramach Sesji 0 jest RootkitRevealer w wersji 1.71 (do ściągnięcia z serwisu sysinternals w ramach Microsoft.com). Odbywa się to w ten sposób, że po uruchomieniu aplikacja wypakowuje plik .exe o przypadkowej nazwie, który następnie ustawiany jest jako usługa. Po uruchomieniu tej usługi wyświetlane jest właściwe okno aplikacji – stąd trafiamy na Sesję 0. Całe skanowanie i analiza odbywa się już w ramach Sesji 0.
Popływajmy chwilę w tym błękicie
Microsoft zaleca wykorzystanie Terminal Services API do przesyłania prostych komunikatów między usługą a użytkownikiem – w szczególności funkcji WTSSendMessage z biblioteki wtsapi32.dll. W przypadku konieczności bardziej rozbudowanej interakcji między usługą a użytkownikiem należy skorzystać z CreateProcessAsUser do utworzenia procesu działającego w sesji użytkownika i jego kontekście. Osobiście słabo widzę to rozwiązanie w praktyce, przy ciągłej tendencji do ustawiania serwisów na kontach o coraz niższych przywilejach. Ostatnie doniesienia o problemie z serwerem DNS Microsoftu (chodzi o usługę zarządzania DNS-em poprzez RPC i dziurę pozwalającą na wykonanie naszego (;)) kodu na maszynie z DNS-em) tylko pogłębiają tę tendencję (a właściwie czemu DNS musi pracować na koncie LocalSystem? W końcu integrację z AD można załatwić w inny sposób – na mój gust).
Wracając do sugerowanego rozwiązania - do komunikacji między nowoutworzonym procesem a usługą można skorzystać z dowolnego mechanizmu – RPC, potoki nazwane, Remoting, WCF etc.
Najczęściej jednak taką aplikację uruchomi sam użytkownik - zazwyczaj przy logowaniu; odpowiednia ikonka będzie sobie odpoczywać w trayu. Wówczas pozostaje wyłącznie komunikacja z usługą. To rozwiązanie wydaje mi się najsensowniejsze, głównie ze względu na jasną separację aplikacji zarządzającej usługą od samej usługi.
Jako przykład aplikacji ilustrującej poruszone tematy można rozważyć prosty serwis pracujący w trybie usługi interaktywnej, który koniecznie chce powiadomić o czymś użytkownika. Z jednej strony będzie to po prostu bezpośrednie wywołanie statycznej metody Show klasy System.Windows.Forms.MessageBox, z drugiej strony sięgniemy po P/Invoke i użyjemy wspomnianej metody WTSSendMessage. Serwis możemy zmusić do wykonania którejś z czynności na kilka sposobów – jednym z nich może być np. skorzystanie z FileSystemWatchera na określonym pliku, do którego zapiszemy transakcyjnie (!) odpowiednią informację. Dzięki zmianom w ReadDirectoryChanges uwzględniającym pojawienie się KTM możemy korzystać z FileSystemWatchera ciesząc się automatycznym wsparciem dla transakcji. Jest to niewątpliwa korzyść w korzystaniu z abstrakcji systemu, a nie bezpośrednio API systemowego.
Z drugiej strony – i to rozwiązanie poniżej – możemy wysłać sygnał kontrolny do usługi korzystając z narzędzia konsolowego sc.exe. Zależnie od wartości tego polecenia wywołujemy bezpieczną, lub też nie wersję MessageBoxa.
Oczywiście najlepsze rozwiązanie będzie wykorzystywać mechanizmy o których wcześniej wspomniałem, jednak dla prostoty rozwiązania pozostańmy przy tym co mamy teraz.
Usługę przygotowujemy w standardowy sposób – korzystając z kreatora projektu usługi dostępnego w Visual Studio.
W widoku projektu ustawiamy we właściwościach nazwę naszej usługi (ServiceName) i klikamy ‘Add Installer’. Pojawia nam się ProjectInstaller z dwoma elementami – serviceProcessInstaller1 oraz serviceInstaller1. Dla tego pierwszego ustawiamy konto, na którym nasza usługa ma pracować (Account – ja dla testu ustawiłem LocalSystem) oraz Parent ustawiamy na ProjectInstaller, w przypadku drugiego obiektu ustawiamy opis, nazwę usługi i Parent – również jako ProjectInstaller. StartType zostawiamy na manual.
Przechodzimy do kodu usługi i dodajemy override dla OnCustomCommand:
public partial class WinSta0TestService : ServiceBase
{
enum ServiceCommands
{
VistaReady = 129, VistaNOTReady = 130
}
//.ctor, OnStart, OnStop. W wersji z FileSystemWatcher
//dochodzi jeszcze obsluga zdarzen obiektu tej klasy
//nadpisana metoda do obslugi polecen service control
protected override void OnCustomCommand(int command)
{
int resp = 0;
int consoleSessionID = Win32.GetConsoleSession("");
string goodTitle = "Jestem gotowy na Viste!";
string good = "Ten komunikat jest ok! :) ";
string bad = "Ten komunikat oznacza klopoty na Viscie :(";
switch ((ServiceCommands)command)
{
case ServiceCommands.VistaNOTReady:
System.Windows.Forms.MessageBox.Show(bad);
//Process.Start("c:\\windows\\system32\\cmd.exe");
break;
case ServiceCommands.VistaReady:
Win32.WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, consoleSessionID,//WTS_CURRENT_SESSION,
goodTitle, goodTitle.Length,
good, good.Length,
0, 0, ref resp, false);
break;
default:
break;
}
}
}
Nasz kod korzysta z P/Invoke, więc importujemy odpowiednie metody:
public class Win32
{
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSSendMessage(
IntPtr hServer,
[MarshalAs(UnmanagedType.I4)] int SessionId,
String pTitle,
[MarshalAs(UnmanagedType.U4)] int TitleLength,
String pMessage,
[MarshalAs(UnmanagedType.U4)] int MessageLength,
[MarshalAs(UnmanagedType.U4)] int Style,
[MarshalAs(UnmanagedType.U4)] int Timeout,
[MarshalAs(UnmanagedType.U4)] ref int pResponse,
bool bWait);
//reszta importow
//Metoda wyciagajaca numer sesji dla Console – tam wyslemy komunikat
public static int GetConsoleSession(string ServerName)
{
IntPtr server = IntPtr.Zero;
int ret = 0;
server = OpenServer(ServerName);
try
{
IntPtr ppSessionInfo = IntPtr.Zero;
Int32 count = 0;
Int32 retval = WTSEnumerateSessions(server, 0, 1, ref ppSessionInfo, ref count);
Int32 dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
Int32 current = (int)ppSessionInfo;
if (retval != 0)
{
for (int i = 0; i < count; i++)
{
WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WTS_SESSION_INFO));
current += dataSize;
if (si.pWinStationName == "Console")
{
ret = si.SessionID;
break;
}
}
WTSFreeMemory(ppSessionInfo);
}
}
finally
{
CloseServer(server);
}
return ret;
}
}
Bardzo pomocne przy korzystaniu z P/Invoke jest posiłkowanie się gotowcami, które można znaleźć na stronie pinvoke.net. Ostatnio Red-Gate.com wypuścił dodatek do Visual Studio, dzięki któremu zabawa jest jeszcze prostsza.

Dodatkowy element menu Visual Studio z opcją PInvoke

Darmowy dodatek do Visual Studio 2005 ułatwiający korzystanie z Platform Invoke. Dostępny na stronie Red-Gate.com
Usługę instalujemy korzystając z narzędzia konsolowego InstallUtil:
installutil -i serwis.exe
Jeśli wszystko przebiegło prawidłowo, to na liście dostępnych usług (services.msc) pojawia się zdefiniowana przez nas usługa. Wchodzimy do jej właściwości i ustawiamy ją jako interaktywną. Zauważmy, że to ustawienie nie ma żadnego znaczenia w przypadku korzystania z ‘prawidłowej’ komunikacji serwisu z użytkownikiem i prawdopodobnie zostanie usunięte z przyszłych wersji systemu.

Zezwalanie usłudze na interakcję z desktopem użytkownika
Po uruchomieniu usługi możemy w procexpie upewnić się, że usługa przywiązana jest do Sesji 0, stacji WinSta0 oraz desktopu Default.
Na początku testujemy ‘prawidłową’ interakcję usługi z użytkownikiem. W tym celu wpisujemy w wierszu poleceń (zakładam, że usługa nazywa się MojSerwis)
sc control MojSerwis 129
Efekt wykonania tego polecenia przedstawiony jest na rysunku. Komunikat został poprawnie dostarczony do naszej sesji.

Komunikat wyświetlony w sesji Console na pulpicie administratora po wykorzystaniu API TS - WTSSendMessage. Nazwa nieco myląca, ale chodzi o MessageBox w ramach usług terminalowych.
Teraz spróbujemy zmusić usługę do nieprawidłowej interakcji z użytkownikiem:
sc control MojSerwis 130
Tym razem zareagował Interactive Services Detection Service i zaproponował nam teleportację na Sesję 0.

ISDS w akcji - komunikat o możliwości przeniesienia się do Sesji 0 w celu obejrzenia komunikatu wystawionego przez usługę interaktywną.
Przenosimy się tam i potwierdzamy wyświetlony komunikat.

Błękitny pulpit Sesji 0 - dla usług interaktywnych
Tu uwaga: jeśli przed zakończeniem pracy z oknem usługi wyświetlonym na Sesji 0 kliknęliśmy ‘Powróć teraz’, to zamknęliśmy sobie drogę powrotną. Żeby powrócić z naszej sesji do Sesji 0 musimy jeszcze raz zmusić serwis do próby interakcji z nami. Co gorsza – poprzednie okno ciągle tam wisi i czeka na nas. Co, jeśli musimy na chwilę powrócić do naszej sesji, a nie chcemy zamknąć sobie drogi powrotnej? Heh – nie znalazłem oficjalnego przejścia, ale ‘zupełnie przypadkowo’ ustawiłem fokus na oknie ‘Powróć teraz’ i wcisnąłem F1 :). W ten sposób wróciłem do swojej sesji, a okno z propozycją teleportacji ciągle na mnie czeka :). Drugie rozwiązanie to skorzystanie z opisanego wyżej prostego serwisu i w razie potrzeby przenoszenie się tam – w końcu okna wszystkich usług będą pojawiać się na tym samym desktopie Sesji 0.
Uwaga 2: Nic nie stoi na przeszkodzie, żeby sobie trochę pohasać w ramach ‘pulpitu serwisowego’: można uruchomić dowolny proces, w tym również explorer.exe, który utworzy nam wszystkie okna menu start, ustawi tapete, etc. (w dosyć okrojonej wersji).
Rozważania końcowe
Zapewne niektórzy zauważyli, że przy zrzucie z Windows 2003 obrazującym wspólną kolejkę komunikatów, spy++ uruchomiony jest w tym samym desktopie, w którym dostępne jest okno logowania. Co bardziej wnikliwi dostrzegą nawet hasło, które tak sumiennie wpisałem (jakie, hę ;) ? Czekam na odpowiedź w komentarzach!). Jeśli jesteście ciekawi jak to jest możliwe, to proszę o odpowiedni komentarz. W ciągu najbliższych dni uzupełnię treść postu o to, jak to można osiągnąć, teraz napiszę tylko, że potrzebne są przywileje:
SeChangeNotifyPrivilege
SeIncreaseQuotaPrivilege
SeAssignPrimaryTokenPrivilege
SeTcbPrivilege
SeCreateTokenPrivilege
SeTakeOwnershipPrivilege