2016-03-28:

Zagadka zmniejszającej się konsoli

winapi:windows:re
ExpressCard z Firewire używany do debuggowania Windows 10Podczas jednego ze streamów bawiłem się wczytywaniem rozpakowanych plików .xp (REXPaint), czyli ASCII-artów, w których zarówno kolory czcionek, jak i kolory teł mogły być dowolnie zdefiniowane (w rozumieniu RGB). Ich wyświetlenie wymagało zmiany domyślnej palety kolorów w Windowsowej konsoli, co jest jak najbardziej możliwe. Nieprzewidzianym efektem ubocznym zmiany palety kolorów była zmniejszająca się konsola - każde wywołanie pary GetConsoleScreenBufferInfoEx + SetConsoleScreenBufferInfoEx powodowało utratę jednego wiersza i jednej kolumny (ew. czasem, w zależności od ustawień, jedynie jednego wiersza). Z efektem tym zresztą spotkałem się zresztą kilka dni wcześniej, podczas gdy siedzieliśmy z masakrą (znanym Wam pewnie jako moderator na livestreamie) nad Pythono'wym ctypes i wywoływaniem wspomnianych wcześniej funkcji, jednak ani wtedy, ani podczas streamu nie zagłębiłem się w temat na tyle, żeby dojść co jest problemem. Wczoraj jednak znalazłem trochę czasu.



Zacząłem trochę na około - od ustalenia jak działa API konsoli pod Windows 10 - wiedziałem, że trochę się tu pozmieniało od kiedy siedziałem nad tym tematem w okolicach Windows XP/Vista/7 (tak na marginesie, strona 38). Z dużych zmian (w skrócie): za okno konsoli nie jest już odpowiedzialny csrss.exe (Client/Server Runtime Subsystem), a, na spółkę, nowy program uruchamiany z uprawnieniami danego użytkownika zajmujący się oknem konsoli - conhost.exe, oraz sterownik odpowiedzialnym za uruchamianie conhost (*cough*) i transport pakietów (RPC) pomiędzy aplikacjami konsolowymi a conhostem - condrv.sys (pseudo-urządzenia \Device\ConDrv\*). To jak dokładnie działają wywołania "konsolowego RPC" jest tematem na oddzielny post - na ten moment wystarczy wiedzieć, że coś takiego ma miejsce.

Otwarte pseudo-urządzenia \Device\ConDrv\* w cmd.exe oraz urządzenie \Device\ConDrv w conhost.exe.

Dodam jeszcze tylko, że przy analizie condrv.sys niezastąpiony okazał się setup, który podrzucił mi j00ru, czyli połączenie dwóch komputerów (w moim wypadku PCta z laptopem) za pomocą firewire - debugowanie (jądra/sterowników) systemu Windows nigdy nie było tak przyjemne; co prawda mój laptop firewire nie posiada, ale ten problem udało się rozwiązać ExpressCardem kupionym w lokalnym Conradzie (patrz też fotka na początku posta).

Wracając do tematu, po przejrzeniu consys.drv, conhost.exe wraz z ConhostV2.dll (na który już niedawno zresztą patrzyłem, przy okazji zabawy z ANSI escape codes pod Windows 10), a także kernel32.dll i KernelBase.dll, problem okazał się leżeć w tym ostatnim.

Jak się okazuje, że conhost nie posiada funkcji GetConsoleScreenBufferInfoEx wypełniającej strukturę CONSOLE_SCREEN_BUFFER_INFOEX - zamiast tego jest funkcja SrvGetConsoleScreenBufferInfo (numer funkcji: 0x2000007), która wypełnia strukturę CONSOLE_SCREENBUFFERINFO_MSG:


 struct CONSOLE_SCREENBUFFERINFO_MSG {
   /* 8 bajtów nagłówka tutaj */
   COORD dwSize;
   COORD dwCursorPosition;
   COORD srWindow_TopLeft;
   WORD wAttributes;
   COORD srWindow_WidthHeight;
   COORD dwMaximumWindowSize;
   WORD wPopupAttributes;
   BYTE bFullScreenSupported;
   BYTE _padding[3];
   DWORD ColorTable[16];
 };

Obie wspomniane struktury są podobne, ale nie identyczne. W kontekście omawianego problemu główna różnica dotyczy pola srWindow, które (w CONSOLE_SCREEN_BUFFER_INFOEX) zawiera koordynaty górnego-lewego i dolnego-prawego rogu bufora tekstowego (w "znakach"), a konkretniej, koordynat znaku, który jest widoczny na samej górze konsoli po prawej (zazwyczaj 0,0), oraz koordynat znaku, który jest widoczny na samym dole po prawej (zazwyczaj o jeden mniej niż wynosi szerokość i wysokość konsoli w znakach). Pole to nie występuje CONSOLE_SCREENBUFFERINFO_MSG; zamiast tego są tam dwa inne pola - jedno (oznaczone przeze mnie srWindow_TopLeft) zawiera koordynat górnego lewego rogu, a drugie (srWindow_WidthHeight) mówi o szerokości o raz wysokości konsoli (w znakach). Oczywiście mając jeden zestaw informacji trywialnie jest wyliczyć drugi; w funkcji GetConsoleScreenBufferInfoEx jest to realizowane przez poniższy kod:


 if ( lpCSBIEx->cbSize == 96 )
 {
   // Wywołanie RPC.
   v3 = ConsoleCallServer(hConOutput, &v16 /* m.in. CSBIMsg */, 0x2000007, 92);
   if ( v3 >= 0 )
   {
     // Przepisanie informacji (fragment):
     // ...
     lpCSBIEx->srWindow.Right = CSBIMsg.srWindow_TopLeft.X + CSBIMsg.srWindow_WidthHeight.X - 1;
     lpCSBIEx->srWindow.Bottom = CSBIMsg.srWindow_TopLeft.Y + CSBIMsg.srWindow_WidthHeight.Y - 1;
     // ...
     return result;
   }
   // ...

Warto zwrócić uwagę na oba wystąpienia - 1, które w powyższym zastosowaniu są jak najbardziej prawidłowe.

Analogicznie do powyższego przypadku, conhost nie posiada funkcji SetConsoleScreenBufferInfoEx; zamiast niej istnieje inna, analogiczna, o nazwie SrvSetScreenBufferInfo (numer funkcji: 0x2000008), które również operuje na wspomnianej wcześniej strukturze CONSOLE_SCREENBUFFERINFO_MSG. Oznacza to, że również funkcja SetConsoleScreenBufferInfoEx musi dokonać konwersji, tyle że w drugą stronę. Jest to realizowane przez poniższy kod:


 if ( lpConsoleScreenBufferInfoEx->cbSize == 96 )
 {
   // Przepisanie informacji (fragment):
   // ...
   CSBIMsg.srWindow_WidthHeight.X = lpCSBIEx->srWindow.Right - lpCSBIEx->srWindow.Left;
   CSBIMsg.srWindow_WidthHeight.Y = lpCSBIEx->srWindow.Bottom - lpCSBIEx->srWindow.Top;
   // ...
   // Wywołanie RPC.
   v8 = ConsoleCallServer(hConOutput, &v10 /* m.in. CSBIMsg */, 0x2000008, 92);
   if ( v8 >= 0 )
     return 1;
   // ...

I tak oto trafiliśmy na rozwiązanie zagadki - czyżby ktoś zapomniał o + 1 rekompensującym poprzednie - 1? Ups.

Rozwiązaniem problemu jest więc inkrementacja obu pól, których wartości są źle wyliczane, przed samym wywołaniem SetConsoleScreenBufferInfoEx:


 CONSOLE_SCREEN_BUFFER_INFOEX bi;
 bi.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
 GetConsoleScreenBufferInfoEx(hcon, &bi);
 
 // Różne operacje na bi.
 
 bi.srWindow.Right += 1;
 bi.srWindow.Bottom += 1;
 SetConsoleScreenBufferInfoEx(hcon, &bi);

Podsumowując: wywołując parę Get/SetConsoleScreenBufferInfoEx musimy pamiętać o kompensacji wartości w polach srWindow.Right oraz srWindow.Bottom.

I tyle :)

Comments:

2016-03-28 19:50:05 = pajadam
{
Czyli ładnie obszedłem problem. Ale wow. Muszę nauczyć się debugować swoje aplikacje.
}
2016-03-30 11:09:54 = WhiteLightning
{
Zrobiliscie moze na VolgaCTF quiz za 10p? Strasznie mnie ciekawi jaka odpowiedz, a nigdzie nie ma.
}

Add a comment:

Nick:
URL (optional):
Math captcha: 3 ∗ 7 + 4 =