Po przydługowamy opisie tego co daje nam Windows przyszła chwila na trochę kodu. Jako, że jednym z celów tego projektu jest poznanie nieznanego postanowiłem wykorzystać F#-a (http://fsharp.org/), czyli funkcjyny język z rodziny .NET.

W czasie pracy staram się wykorzystywać TDD i nie inaczej będzie w przypadku Pathera. Jeśli chodzi o technologię to wybór padł na xUnit oraz FsUnit. Oprócz bibliotek potrzebować będziemy także programu, którego zmienne środowiskowe będą zmieniane. W tym celu napisałem malutki program (również w F#), pozwoli sobą sterować (poprzez standardowe wejście/wyjście) dzięki czemu możliwe będzie napisanie testów:

Komunikacja wygląda następująco:

  1. Program odczytuje jeden znak ozaczający polecenie do wykonania (E - echo, S -> ustawienie zmiennej, R -> odczytanie zmiennej)
  2. W zależności od polecenia odczytywane są kolejne parametry - każdy w oddzielnej linii
  3. Odpowiedź jest odsyłana w formie pojedynczej linii

Po stronie testów korzystne będzie napisanie kilku funkcji, które pozwolą na wygodne sterowanie programem pomocniczym:

Te kilka funkcji pozwoli na napisanie pierwszych testów w bardzo przejrzysty sposób:

Szczęśliwe po uruchomieniu uzyskujemy dwa zielone testy potwierdzające, że nasz proces pomocniczy działa i będziemy mogli przetestować ustawianie zmiennych środowiskowych z innego procesu.

Ten post zakończy się dwoma testami przedstawiającymi końcową funkcjonalność:

Jako, ze implementacja funkcji RemoteProcess.readPath i RemoteProcess.setPath wykonujących to czego potrzebujemy jest rozbudowana, będą one tematem następnego posta.

W poprzednim poście opisałem gdzie w pamięci procesu znajdują się zmienne środowiskowe. Niestety bezpośrednie ich modyfikowanie nie przyniosło oczekiwanego efektu ze względu cache wprowadzony w podstawowych bibliotekach systemowych. Na szczęście nie oznacza to końca projektu a dopiero początek wspaniałej przygody ;).

Jedynym pewnym sposobem korzystania ze zmiennych środwiskowych jest korzystanie z dostarczonych funkcji, które operują na własnej przestrzeni adresowej. Można je jednak połączyć z mechanizmami systemowi pozwalającymi na wykonanie kodu w kontekście innego procesu poprzez stworzenie w nim nowego wątku (technika ta powinna być znana tym, którzy tworzyli hooki funkcji systemowych). Dzięki temu możliwe będzie wykonanie dowolnego kodu, w tym zmiana zmiennych środowiskowych. Oczywiście rozwiązanie problemu wymaga pokanania kilku przeszkód natury technicznej.

Pierwszym z nich jest konieczność podania adresu entrypointa wątku, która musi się znajdować już w przestrzeni adresowej docelowego procesu. Niesety, większość programistów nie będzie na tyle wspmaniałomyślna, żeby uwzględnić w swoim kodzie funkcję przewidzanią do współpracy z Patherem. Problem można rozwiązać na dwa sposoby: dynamiczne wygenerowanie funkcji oraz załadowanie DLL-ki wskazując funkcję LoadLibrary z kernel32.dll jako entrypoint wątku, która po wczytniu zacznie wykonywać swoją funkcję DllMain, czyli nasz kod. Dynamiczne wygenerowanie funkcji jest interesującym zagadnieniem jednak wiąże się z kolejnymi kłopotami: poprawność kodu maszynowego, ochrona pamięci, itp. Na potrzeby tego projektu wykorzystanie DLL-ki powinno okazać się wystarczające.

Po załadowaniu DLL-ki możemy zrobić co tylko nam się podoba ze stanem docelowego procesu. Powstaje jednak pytanie skąd wziąć nową wartość zmiennej PATH lub jak przesłać jej aktualną wartość do Pathera? Z pomocą przychodzą nazwane potoki (named pipes) pozwalające na dwukierunkową komunikację między procesami poprzez plikopodoby interfejs. Dzięki temu możliwa będzie wymiana dowolnych danych między Patherem a procesem docelowym.

Cały pomysł najlepiej przedstawi schemat:

Wykorzystanie wątku z docelowym procesie do zmiany zmiennej PATH

W następnym poście pojawi się (nareszcie!) trochę kodu i implementacja opisanego podejścia.

Głównym zadaniem Pathera jest zmienianie wartości zmiennych środowiskowych, a żeby to osiągnąć trzeba dowiedzieć się, gdzie są one przechowywane. W Windowsie znajdują się trzy zestawy zmiennych środowiskowych:

  1. Domyślne zmienne systemu
  2. Domyślne zmienne użytkownika
  3. Zmienne procesu

Domyślne zmienne

Dwa pierwsze zestawy wykorzystywane są przez powłokę (explorer.exe) podczas tworzenia procesu i są łatwo dostępne poprzez ustawienia systemowe:

Domyślne zmienne środowiskowe

Zmienne systemowe mogą być zmieniane jedynie przez administratora i są wspólne dla wszystkich korzystających z systemu. Zmienne użytkownika pozwalają na nadpisanie (lub dopisanie w przypadku zmiennej PATH) ustawień systemowych przez poszczególnych użytkowników.

Informacji o tym gdzie znaleźć (i zmienić) domyślne wartości zmiennych dostacza TechNet: zmienne użytkownika przechowywane są w HKCU\Environment, a systemowe w HKLM\System\CurrentControlSet\Control\Session Manager\Environment. Na szczęście zmiana w rejestrze jest wystarczająca, aby Explorer wykorzystał nowe wartości.

Zmienne procesu

W przypadku procesów sprawa dostępu do zmiennych środowiskowych nie jest taka oczywista. Sam proces ma możliwość ich ustawiania (funkcje WinAPI: SetEnvironmentVariable, GetEnvironmentVariable) jednak MSDN nic nie wspomina o ustawianiu ich przez inny proces, co oznacza, że trzeba ich poszukać.

Poszukiwania zaczniemy od uruchomienia cmd.exe i podpięcia się do niego w WinDbg. Pierwszym podejrzanym w którym mogą siedzieć zmienne procesu jest PEB (Process Environment Block). Szybkie sprawdzenie w WinDbg:

> !peb
PEB at 0000000e8dd78000
    InheritedAddressSpace:    No
    ReadImageFileExecOptions: No
    BeingDebugged:            Yes
    ImageBaseAddress:         00007ff7df810000
    Ldr                       00007ffc55905200
    Ldr.Initialized:          Yes
    Ldr.InInitializationOrderModuleList: 000001624c582c80 . 000001624c5860c0
    Ldr.InLoadOrderModuleList:           000001624c582de0 . 000001624c5860a0
    Ldr.InMemoryOrderModuleList:         000001624c582df0 . 000001624c5860b0
                    Base TimeStamp                     Module
            7ff7df810000 5632d733 Oct 30 03:34:27 2015 C:\WINDOWS\SYSTEM32\cmd.exe
            7ffc557c0000 56cbf9dd Feb 23 07:19:09 2016 C:\WINDOWS\SYSTEM32\ntdll.dll
            7ffc55710000 5632d5aa Oct 30 03:27:54 2015 C:\WINDOWS\system32\KERNEL32.DLL
            7ffc51e70000 56a8489c Jan 27 05:33:32 2016 C:\WINDOWS\system32\KERNELBASE.dll
            7ffc53610000 5632d79e Oct 30 03:36:14 2015 C:\WINDOWS\system32\msvcrt.dll
                7e110000 56d69d20 Mar 02 08:58:24 2016 D:\Tools\ConEmu\ConEmu\ConEmuHk64.dll
            7ffc55060000 565423d2 Nov 24 09:46:10 2015 C:\WINDOWS\system32\USER32.dll
            7ffc53420000 568b2035 Jan 05 02:45:25 2016 C:\WINDOWS\system32\GDI32.dll
            7ffc531b0000 5632d48d Oct 30 03:23:09 2015 C:\WINDOWS\system32\IMM32.DLL
            7ffc4b3d0000 5632d813 Oct 30 03:38:11 2015 C:\WINDOWS\SYSTEM32\winbrand.dll
    SubSystemData:     0000000000000000
    ProcessHeap:       000001624c580000
    ProcessParameters: 000001624c582490
    CurrentDirectory:  'C:\Users\Novakov\'
    WindowTitle:  'ConEmu'
    ImageFile:    'C:\WINDOWS\SYSTEM32\cmd.exe'
    CommandLine:  '"C:\WINDOWS\SYSTEM32\cmd.exe"'
    DllPath:      '< Name not readable >'
    Environment:  000001624c58b920
        =::=::\
        =C:=C:\Users\Novakov
        ALLUSERSPROFILE=C:\ProgramData
        ANSICON=170x9999 (170x41)
        ANSICON_DEF=7
        APPDATA=C:\Users\Novakov\AppData\Roaming
        CARBON_MEM_DISABLE=1
        ChocolateyPath=C:\Chocolatey
        CodeContractsInstallDir=C:\Program Files (x86)\Microsoft\Contracts\
        CommonProgramFiles=C:\Program Files\Common Files
        CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
        CommonProgramW6432=C:\Program Files\Common Files
        <dużo innych zmiennych>
        Path=D:\Tools\ConEmu\ConEmu\Scripts<dużo innych ścieżek>
        <jeszcze więcej zmiennych>

Bingo! PEB w jakiś sposób wskazuje na blok zmiennych środowiskowych, pytanie brzmi: jak się dostać konkretnego adresu w pamięci? To kolejna sytuacja w której MSDN nam nie pomoże, ponieważ większa część struktury PEB jest nieudokumentowana, jednak z tego co wiadomo wraz z kolejnymi wersjami systemu jest jedynie rozszerzana o następne pola. Wspaniałym przewodnikiem po wnętrznościach Windowsa jest http://terminus.rewolf.pl/terminus/. Dowiadujemy się, że ścieżka prowadząca do zmiennych środowiskowych to PEB->ProcessParameters->Environment. Oglądając pamieć pod tym adresem okaże się, że mamy doczynienia z listą stringów zakończonych znakiem 0, a dodatkowo cała lista kończy się kolejnym znakiem 0. Sprawdźmy zatem czy modyfikując pamięć wskazywaną przez to pole zmienimy wartość zmiennej PATH procesu. Chwila kombinowania z oknem Memory i zmieniamy D:\Tools na Y:\Tools. Sprawdźmy poleceniem !peb:

> !peb
PEB at 0000000e8dd78000
    <to samo co wcześniej>
        Path=Y:\Tools\ConEmu\ConEmu\Scripts;...

Sukces! Znaleźliśmy sposób na zmianę zmiennej bez korzystania z funkcji WinAPI, a operowanie na pamięci innego procesu jest stosunkowo prostą (i dla odmiany - udokumentowaną) czynnością otwierając drogę do implementacji podstawowej funkcji Pathera.

Dla pewności wywołajmy jeszcze polecenie set PATH w cmd:
C:\>set PATH Path=D:\Tools\ConEmu\ConEmu\Scripts;...

Ups…

Dwa dni debugowania WinAPI później…

Modyfikacja pamięci wskazanej przez PEB nie wystarczy. Funkcje systemowe oprócz budowania nowego bloku przechowują go także w buforze (ntdll!RtlpQueryEnvironmentCache). Po głębszym zastanowieniu ma to nawet sens, gdyż zmienne środowiskowe mogą wykorzystywać inne, a odczytując wartość którejś z nich chcielibyśmy uzyskać wartość ostateczną. Wyznaczanie jej za każdym razem byłoby niepotrzebym kosztem zwłaszcza, że jedyny (oficjalny) sposób na zmianę zmiennej to SetEnvironmentVariable.

Sprawa komplikuje się jeszcze bardziej jeśli weźmiemy pod uwagę aplikacje 32-bitowe uruchomione w 64-bitowym systemie (WoW64). Eksperymenty pokazały, że blok zmiennych środowiskowych wskazyny przez PEB nie jest tym z którego korzystają funkcje systemowe. Proces WoW64 taki ma zdublowane niektóre struktury systemowe, np. PEB i o ile możliwe jest uzyskanie adresu 64-bitowego PEBa, to jego 32-bitowy odpowiednik (w którym powinien być wskaźnik na rzeczywiste zmienne środowiskowe) nie jest taki łatwy do znalezienia, zwłaszcza, że w Windows 10 zmienił swoje położenie względem PEB64 (było to 4MB różnicy).

Problemy te wskazują, że pierwotnie wybrana droga okazała się ślepa. Oczywiście nie oznacza to, że nie da się osiągnąć tego samego celu innymi.

Ciąg dalszy nastąpi…

No i stało się… Zapisałem się do Daj się poznać z projektem nad którym spędzam (prawie) zimowe wieczory - Pather.

Jego zadania są dwa: poznać technologie, których jeszcze nie miałem okazji używać oraz ułatwić sobie utrzymanie porządku w zmiennej PATH. Możliwe, że jestem dziwnym człowiekiem, ale nie lubię dodawać kolejnych i kolejnych ścieżek do systemowego PATH, wolę utrzymywać tam ścieżki systemowe i narzędzia globalne (np. wget, git) a resztę (np. Windows SDK) dodawać do konsoli w której akurat jest coś potrzebne.

O ile ręczne dopisanie czegoś do zmiennej PATH w konsoli jest wydaje się być proste, jednak jest z tym związane kilka problemów. Przedewszystkim musimy znać konkretną ścieżkę, którą chcemy dodać, co nie zawsze jest takie proste, na przykład Windows SDK występuje w wielu wersjach, a każda z nich jest w innym folderze. Jeszcze gorzej jeżeli potrzebujemy wielu narzędzi, np. kiedy pracuję z ARMami i potrzebuję GCC, CMake, QEmu i J-Link.

A gdyby tak ścieżki zapisać w pliku a potem załadować jednym prostym poleceniem? A czy niewspaniale by było, gdyby nie trzeba było podawać dokładnych ścieżek a jedynie “wskazówki” jak powinny wyglądać?

W ten sposób narodził się pomysł na projekt Pather, którego celem jest zarządzanie zmienną PATH zarówno na poziomie systemu jak i pojedynczych procesów przy wykorzystaniu plików w których ścieżki mogą być budowane dynamicznie na podstawie wartości w rejestrze, najwyższej dostępnej wersji i wielu innych.