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.
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.
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)
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.
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…