Windows Internals: Dziwne parsowanie ścieżek

tech • 799 słów • 4 minuty czytania

Kilka miesięcy temu trafiłem na intrygujący problem z obsługą ścieżek w systemie Windows. Jak się okazało odkrycie to jest dobrze znane w środowisku deweloperów blisko związanych z bebechami tego systemu, ludziom zajmującym się inżynierią wsteczną, bezpieczeństwem i różnym badaczom… Można powiedzieć, że tak naprawdę to nic nadzwyczajnego, dlatego aż dziwne, że wcześniej nie trafiłem na coś podobnego, można wręcz powiedzieć zadziwiającego.

Te dziwne zachowanie (o jakim piszę) to nigdzie nie udokumentowane ignorowanie (usuwanie) znaku spacji i kropki na końcach poszczególnych elementów ścieżki. Co skutkuje tym, że poniższe ścieżki wskazują dokładnie na ten sam plik:

"c:\foo\bar.txt"
"c:\foo.\bar.txt."
"c:\foo.\bar.txt.  "
"c:\foo.\bar.txt.  ."

Łatwo można to udowodnić małym eksperymentem:

C:\>echo "dupa" > c:\foo\bar.txt

C:\>type c:\foo\bar.txt
"dupa"

C:\>type "c:\foo\bar.txt"
"dupa"

C:\>type "c:\foo.\bar.txt."
"dupa"

C:\>type "c:\foo.\bar.txt.  "
"dupa"

C:\>type "c:\foo..\bar.txt"
System nie może odnaleźć określonej ścieżki.

C:\>type "c:\foo \bar.txt"
System nie może odnaleźć określonej ścieżki.

C:\>type "c:\foo\bar.txt  . .. "
"dupa"

Ten mały eksperyment ukazuje tą niezwykłą funkcjonalność. Na pierwszy rzut oka można wywnioskować, ze parser zignoruje każdą pojedynczą kropkę z końca nazwy katalogu oraz wszelkie kropki i spacje w dowolnej ilości z końca nazwy pliku. Czy ktoś spodziewałby się takowego zachowania?

Sprawa ta dotyczy wszelkich ścieżek typu DOS/Win32 jakie są używane w systemie i przekazywane przez API podsystemu Win32. W WinAPI odpowiada za to funkcja GetFullPathName, którą można wykorzystać do prostej normalizacji ścieżek, czyli pobierania pełnej ścieżki do pliku lub katalogu.

Można napisać prosty program reparse zwracający znormalizowaną pełną ścieżkę:

#include <cstdio>
#include <windows.h>

int main(int argc, const char* argv[]) {

	if (argc != 2) {
		std::printf("Usage: %s [file]\n", argv[0]);
		return 1;
	}

	char buf[1024];
	if (!::GetFullPathNameA(argv[1], sizeof(buf), buf, nullptr)) {
		std::printf("GetFullPathName failed (%d)\n", GetLastError());
		return 1;
	}

	std::printf("Full path name: %s\n", buf);

	return 0;
}

Jego wyniki są podobne do tych jakie otrzymałem przy eksperymentach pod konsolą.

>reparse.exe "c:\foo.\bar.txt.  "
Full path name: c:\foo\bar.txt

>reparse.exe "c:\foo..\bar.txt"
Full path name: c:\foo..\bar.txt

>reparse.exe "c:\foo\bar.txt  . .. "
Full path name: c:\foo\bar.txt

Oczywiście realna implementacja tego parsowania znajduje się głęboko w module ntdll.dll, w funkcjach rodziny RtlGetFullPathNameXXX. Pod moim systemem Windows 7, funkcja GetFullPathName korzysta bezpośrednio z eksportowanej RtlGetFullPathName_UEx. A fizyczna implementacja parsowania w pomocniczym helperze RtlGetFullPathName_Ustr.

Żeby za bardzo nie zanudzać i nie nawrzucać za dużo zdiasemblowanych bebechów, bo funkcja ta jest całkiem rozbudowana (co widać na poniższych grafie), ale aby jednoznacznie udowodnić fakt, że rzeczywiście takie działanie zaszyto wewnątrz tej funkcji, posłużę się jedynie małym fragmentem kodu.

rtlgetfullpathname_ustr-graph

Zatem w mojej testowej wersji modułu ntdll, czyli 6.1.7601.18247 (win7sp1_gdr.130828-1532), sprawdzanie i eliminowanie z końca nazwy pliku znaków ' ' i '.' usytuowany jest pod adresem 0x7DEAA636:

7DEAA636 loc_7DEAA636:
7DEAA636	mov   word ptr [ebp+var_48], di
7DEAA63A	cmp   ecx, ebx
7DEAA63C	jbe   short loc_7DEAA658
7DEAA63E	lea   edx, [ecx-2]
7DEAA641	movzx esi, word ptr [edx]
7DEAA644	cmp   si, 20h		; si == '  '
7DEAA648	jz    loc_7DEEE627
7DEAA64E	cmp   si, 2Eh		; si == ' .'
7DEAA652	jz    loc_7DEEE627

Jest to ciało pętli, które Hex-Rays wyrzucił jako fragment podobny do tego:

for (j = (_WORD)v25 - (_WORD)v82; ; j = v81 - 2)
{
	LOWORD(v81) = j;
	if (v25 <= v28)
		break;
	v30 = v25 - 1;
	v31 = *(v25 - 1);
	if (v31 != ' ' && v31 != '.')
		break;
	--v25;
	v78 = v30;
	*v30 = 0;
}

Nie chciało mi się tutaj poprawiać i przetwarzać całego wyplutego kodu na bardziej ludzką formę. Ale mogę zachęcić do przejrzenia kodu projektu wine lub ReactOS-a, bo tam panowie deweloperzy również takowe zachowanie, na wzór orginału, zaimplementowali. Znaleźć tam można m.in taki fragment:

/* remove trailing spaces and dots (yes, Windows really does that, don't ask) */
while (p > path + mark && (p[-1] == ' ' || p[-1] == '.')) p--;

pochodzący z pliku wine/dlls/ntdll/path.c (funkcja collapse_path). Coś równie podobnego leży w pliku reactos/lib/rtl/path.c (funkcja RtlpCollapsePath).

A to nie wszystko, bo istnieje jeszcze mała ciekawostka, co prawda niezwiązana z tą implementacją wyżej, ale warto wiedzieć, że Explorer (lub Eksplorator) ma również niestandardową implementację parsowania i obsługi ścieżek. Na szczęście tylko na własny użytek dla potrzeb GUI i obsługi funkcji shell-owych. Aby się przekonać wystarczy w oknie Eksploratora spróbować wpisać niepoprawna ścieżkę, zawierająca te same magiczne ignorowane znaki odkryte wyżej.

win-explorer-path

Niezłe! Na początku myślałem, że jest to efekt implementacji tego opisanego wyżej zachowania w WinAPI, ale to całkowicie osobny przypadek. Kod zaszyty jest prawdopodobnie gdzieś w shell32.dll lub podobnym module, nie pamiętam dokładnie gdzie.

Moja pierwsza styczność z całym tym bardzo dziwnym zachowaniem nastąpiła w pracy, przy pewnym dziwnym błędzie związanym z nieoprawną implementacją mechanizmu obsługi ścieżek. Co skutkowało tym, że pewna aplikacja używając ścieżki składającej się z doklejonego pewnego fragmentu (nazwy pliku/katalogu) rozpoczynającego się od ciągu ".\name..." otrzymywała błąd dostępu do takiej lokalizacji. Mimo, że fizycznie pod normalnym systemem, wszystko działało oczywiście poprawnie. Moje zdziwienie było wielkie, gdy odkryłem, że to standardowe zachowanie Windowsa.

Niestety dopiero po tych kilku miesiącach sobie o tym przypomniałem i udokumentowałem tutaj.

Komentarze (2)

PiotrB avatar
PiotrB
20161223-225617-piotrb

Z takich ciekawostek: kiedyś w DOS można było zrobić cd … i to przechodziło 2 katalogi w górę.

A cały wywód jest kolejnym argumentem za tym, żeby używać systemowych funkcji do łączenia ścieżek.

Malcom avatar
Malcom
20161224-000734-malcom

O ciekawe z tym DOS-em, mam gdzieś źródła kilku wersji, można by poszukać ;)

Nie wiem czy jakiekolwiek funkcje do operacji na ścieżkach są dostępne w WinAPI i na poziomie niższym (ntdll). I tak w moim przypadku by nie pomogły. Szczególnie babrając się na niższych poziomach, gdzie nie zawsze można sobie pozwolić na wysokopoziomowo-dostepne ułatwiacze. A wstrzykując swoją warstwę pomiędzy WinAPI a kernelem, w celu przechwytywania różnych call-i, nie mając pojęcia o takim dziwnym zachowaniu wychodziło, że dostęp do pliku C:\foo.\bar.txt dotyczy dokładnie takiego właśnie pliku (i katalogu), a rzeczywistość okazała się zupełnie inna…

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/