• 🔥 Бесплатный курс от Академии Кодебай: «Анализ защищенности веб-приложений»

    🛡 Научитесь находить и использовать уязвимости веб-приложений.
    🧠 Изучите SQLi, XSS, CSRF, IDOR и другие типовые атаки на практике.
    🧪 Погрузитесь в реальные лаборатории и взломайте свой первый сайт!
    🚀 Подходит новичкам — никаких сложных предварительных знаний не требуется.

    Доступ открыт прямо сейчас Записаться бесплатно

Статья ASM. Секреты эксплуатации структуры PEB

PebLogo.webp

Аббревиатура РЕВ подразумевает «Process Environment Block», или блок окружения исполняемого процесса. Он издавна притягивал взоры исследователей кода, т.к. хранит недоступную с пользовательского уровня, ядерную часть нашей программы. Размер этого блока на 64-битных системах равен 0x380=896 байт, которые описывают 90 полей. В таком пространстве можно спрятать слона, а значит в РЕВ непременно должны существовать интересные значения и ссылки, которые и рассматриваются в данной статье.

Оглавление:
1. Вводная часть​
2. Сбор информации без вызовов Win32API​
3. Потенциальные атаки на консоль​
4. Формат структуры KUSER_SHARED_DATA​
5. Заключение​



1. Вводная часть

Для начала разберёмся, от куда появился и зачем вообще нужен «Блок окружения процесса».
Процесс – это объект ядра, а потому при запуске программы с диска или вызове пользовательской функции CreateProcess(), запрос на создание уходит в ядро Ntoskrnl.exe. При этом обслуживающий персонал создаёт основную структуру EPROCESS, через которую ОС и управляет в дальнейшем процессом. Поскольку процесс является просто боксом (а непосредственно исполнением кода занимается его поток Thread), то в ядре оформляется сразу и структура ETHREAD для основного потока процесса. Далее процесс может создавать себе дополнительные потоки, и для каждого из них ядро будет создавать свою ETHREAD. Таким образом, всякий процесс имеет единственную структуру\паспорт EPROCESS, и обязательно одну или несколько структур ETHREAD.

Однако часть операционной системы находится в адресном пространстве пользователя, например сервисы\службы, библиотеки программного интерфейса Win32API, и прочее. По роду своих служебных обязанностей, им тоже периодически нужно обращаться к структуре EPROCESS. Всё-бы хорошо, только вот переходы из юзера в кернел и обратно представляют дорогостоющую операцию, а когда это действие повторяется в секунду по несколько сотен раз, то производительность всей ОС уже проседает. Поэтому инженеры создали (в некотором смысле) копию ядерной EPROCESS, поместив её в пространство пользователя для своих подопечных. Это и есть герой данной статьи РЕВ, с жёстко привязанным к нему блоком потока ТЕВ.

Такой ход-конём позволил не только ускорить работу пользовательских приложений, но и создал плодородную почву для махинаций со-стороны малвари, т.к. конфиденциальная инфа ядра оказалась в открытом доступе. Здесь уже майкам пришлось садиться на шпагат – или отражение потенциальных угроз, или повышение производительности, что они и выбрали в конечном счёте. Авансом скажу, что в РЕВ не так много полей, модификация которых может нарушить работу процесса, но такие поля всё-же есть.

Указатель на РЕВ прописывается в каждой из структур ТЕВ процесса, а указатель на сам ТЕВ в скрытой части сегментого регистра fs на системах х32, и gs на системах x64. От сюда следует, что ОС выделяет для него не просто адрес в памяти текущего процесса, а специальный сегмент (хотя в защищённом режиме грань между ними тонка). Команда !address отладчика WinDbg возвращает информацию о любом регионе памяти (работает только в юм). Судя по логу это приватная память процесса (частная), размером в 4096 байт (одна вирт.страница с атрибутами Read\Write):

Код:
0:000> !address $peb  <------- псевдорегистр
--------------------------------------------
Usage:              PEB
Allocation Base:    000007ff`fffdf000
Base Address:       000007ff`fffdf000
End Address:        000007ff`fffe0000
Region Size:        00000000`00001000
Type:               00020000  MEM_PRIVATE
State:              00001000  MEM_COMMIT
Protect:            00000004  PAGE_READWRITE


2. Сбор информации о процессе без вызовов Win32API

Структура РЕВ – это золотая жила, где можно найти много полезных вещей. Основным преимуществом её прямого чтения является то, что изъятые данные позволяют писать позиционно-независимый шелл код, который будет исправно функционировать «и в снег, и в дождь» на любой версии Windows. Здесь главное взять базовый адрес в памяти из регистра gs, от которого потом смещаться к нужным полям. При этом в полях имеются не только значения Int, но и указатели Pointer на другие системные структуры далеко за пределами самой РЕВ, что в геометрической прогрессии расширяет наши возможности.

На схеме ниже представлена взаимосвязь доступных через РЕВ структур. Второстепенные поля я затенил в них серым, чтобы сделать акцент только на тех, которые будем дампить сразу на консоль. Всю структуру РЕВ найдёте в скрепке к статье, а на схеме лишь фрагмент первых её 30h мемберов:

PebLinks.webp


2.1. Вывод информации из основной РЕВ

Как упоминалось выше, в 64-битной Win указатель на структуру РЕВ лежит по смещению(60h) в дочерней её структуре ТЕВ, на которую (в свою очередь) смотрит всегда сегментный регистр gs. Таким образом, подобраться к РЕВ можно всего одной инструкцией ассемблера mov rsi,[gs:60h], после чего в регистре RSI получим линк на РЕВ.

Код:
0: kd> dt _teb Process*
nt!_TEB
   +0x060 ProcessEnvironmentBlock : Ptr64 _PEB    ;<--- адрес РЕВ в структуре ТЕВ
0: kd>

Ну а далее просто читаем поля по их смещениям, используя значение в регистре RSI в качестве базы.
При наличии описания структуры, проблем вообще не возникает, т.к. вместо Hex-смещения можно будет указывать имя поля – вот как это выглядит на практике:

C-подобный:
         mov     rsi, [gs:0x60]    ;//<---- указатель на РЕВ
         push    rsi
         movzx   eax, [rsi + PEB.BeingDebugged]
         mov     ebx, [rsi + PEB.NtGlobalFlag]
         mov     r10d,[rsi + PEB.NumberOfProcessors]

        cinvoke  printf,<10,' BeingDebugged.......: %d',\
                         10,' NtGlobalflag........: 0x%x',\
                         10,' ImageBase...........: 0x%016I64x',\
                         10,' ProcessHeap.........: 0x%016I64x',\
                         10,' ProcessorCores......: %d',10,0>,\
                                 rax,rbx,\
                                [rsi + PEB.ImageBaseAddress],\
                                [rsi + PEB.ProcessHeap],r10

На выходе имеем флаг отладки нашего приложения да\нет, далее глобальный флаг отладки системой 0x400 (в данном случае нет), базовый адрес загрузки образа в память 0x00400000, адрес статической памяти процесса Heap (в народе хип), ну и в последнем поле кол-во ядер у нашего ЦП. Обратите внимание, что для этого не потребовался вызов ни одной функции Win32API, хотя при обычных обстоятельствах нужна цепочка: IsDebuggerPresent(), GetModuleHandle(), GetProcessHeap() и cpuid.

peb_1.webp


Теперь по натоптанной читаем и остальные поля структуры РЕВ, принимая на грудь очередную дозу информации:

C-подобный:
         pop     rsi
         push    rsi
        cinvoke  printf,<10,' LdrLoaderData.......: 0x%016I64x',\
                         10,' SharedMemoryBase....: 0x%016I64x',\
                         10,' StaticServerData....: 0x%016I64x',\
                         10,' ProcessParameters...: 0x%016I64x',10,0>,\
                              [rsi + PEB.Ldr],\
                              [rsi + PEB.ReadOnlySharedMemoryBase],\
                              [rsi + PEB.ReadOnlyStaticServerData],\
                              [rsi + PEB.ProcessParameters]
         pop     rsi
         push    rsi
         mov     eax, [rsi + PEB.OSMajorVersion]
         mov     ebx, [rsi + PEB.OSMinorVersion]
         movzx   ebp, [rsi + PEB.OSBuildNumber]
         mov     r10d,[rsi + PEB.OSPlatformId]
         mov     r11, [rsi + PEB.CSDVersion]
        cinvoke  printf,<10,' OSMajorVersion......: %d',\
                         10,' OSMinorVersion......: %d',\
                         10,' OSBuildNumber.......: %d',\
                         10,' OSPlatformId........: %d',\
                         10,' CSDVersion..........: %ls',10,0>,\
                              rax,rbx,rbp,r10,r11

Если не считать версию операционной системы: NT-6.1 = Win7, номер сборки Build, , и сервис-пак,
интерес представляют поля LdrLoaderData + ProcessParameters, которые хранят указатели на структуры за периметром РЕВ – они рассматриваются далее:

peb_2.webp



2.2. Назначение структуры «PEB_LDR_DATA»

Значит первое поле из скрина выше смотрит на структуру «PEB_LDR_DATA». Поля этой структуры заполняет системный загрузчик образов так, что можно найти в них базовые адреса загруженных в наш процесс библиотек DLL, имя этих либ, точку-входа в каждую, занимаемый размер в памяти, и многое другое. Так выглядит прототип структуры:

Код:
struct PEB_LDR_DATA
   Length                   dd  0
   Initialized              dd  0
   SsHandle                 dq  0
   InLoadOrderModuleList    LIST_ENTRY
   InMemoryOrderModuleList  LIST_ENTRY
   InInitOrderModuleList    LIST_ENTRY
   EntryInProgress          dq  0
   ShutdownInProgress       dq  0
   ShutdownThreadId         dq  0
ends

Первые и последние три поля нам не интересны, а посередине заняли позиции связанные списки LIST_ENTRY. По сути, все три мембера несут в себе одинаковую инфо-нагрузку, просто имеем возможность перечислить библиотеки в порядке загрузки их системным лоадером, в порядке расположения в памяти, и в порядке инициализации. Обычно достаточно взять линк только из поля InLoaderOrderModuleList, который будет указателем на дочернюю структуру загрузчика LDR_DATA_TABLE_ENTRY.

Так мы в плотную подберёмся к описателю первой библиотеки DLL, а линк на следующую в цепочке будет лежать в первом-же поле InLoadOrderLinks. Чтобы найти последнюю структуру LDR_DATA_TABLE_ENTRY, нужно заранее сохранить указатель на первую, т.к. связанный список LIST_ENTRY является кольцевым. То-есть продвигаясь по линкам вперёд, мы всегда вернёмся туда, от куда пришли.

Код:
struct LDR_DATA_TABLE_ENTRY     ;<------------- LIST_ENTRY из “PEB_LDR_DATA” указывает сюда
   InLoadOrderLinks        LIST_ENTRY   ------> адрес сл.структуры “LDR_DATA_TABLE_ENTRY” в цепочке
   InMemoryOrderLinks      LIST_ENTRY
   InInitOrderLinks        LIST_ENTRY
   DllBase                 dq  0
   EntryPoint              dq  0
   SizeOfImage             dd  0
   Padding1                dd  0
   FullDllName             UNICODE_STRING
   BaseDllName             UNICODE_STRING
   Flags                   dd  0
   LoadCount               dw  0
   TlsIndex                dw  0
   SectionPointer          dq  0
   CheckSum                dd  0
   Padding2                dd  0
   TimeDateStamp           dq  0
   EntryPointActContext    dq  0
   PatchInformation        dq  0
   ForwarderLinks          LIST_ENTRY
   ServiceTagLinks         LIST_ENTRY
   StaticLinks             LIST_ENTRY
   ContextInformation      dq  0
   OriginalBase            dq  0
   LoadTime                dq  0
ends

Как правило, эти структуры загрузчика активно юзает малварь для поиска базы Ntdll, чтобы не вызывать API динамической загрузки библиотек LoadLibrary() + GetProcAddress(), поскольку для аверов они как красная тряпка. А так, с базой на руках можно будет вручную пропарсить секцию экспорта DLL, и найти таким образом адрес нужной функции в памяти процесса.

В своём примере я не концентрирую внимание на одной либе, а в цикле обхожу их все. Аналогичными по назначению функами являются или NtQuerySystemInformation() из Ntdll, или CreateToolhelp32Snapshot() + EnumProcessModules() из Kernel32.dll, ну и для полноты картины EnumerateLoadedModulesEx() из либы DbgHelp.dll. Ключом /i в отладчике можно запросить кол-во инструкций в листинге дизасма – как видим расход мин.230 инструкций (и это без вызова вложенных апи), в то время как мы потратили всего 5 (остальное цикл и печать printf). Вот и делаем выводы..

C-подобный:
;//  0: kd> uf /i NtQuerySystemInformation  ;<--- 253 instructions scanned
;//  0:000> uf /i CreateToolhelp32Snapshot  ;<--- 232 instructions scanned
;//-----------------------------------------------------------------------
         pop     rsi
         push    rsi
         mov     rsi,[rsi + PEB.Ldr]
         mov     rsi,[rsi + PEB_LDR_DATA.InLoadOrderModuleList]       ;//<--- линк на “LDR_DATA_TABLE_ENTRY”
         mov     rax,[rsi + LDR_DATA_TABLE_ENTRY.InLoadOrderLinks+8]

         mov     [firstEntry],rax     ;//<----- Запомнить линк на первую!!!

@@:      push    rsi
         mov     rbx,[rsi + LDR_DATA_TABLE_ENTRY.BaseDllName+8]
         mov     eax,[rsi + LDR_DATA_TABLE_ENTRY.SizeOfImage]
         shr     eax,10
        cinvoke  printf,<10,' %02d  %016I64x  %016I64x  %5d Kb  %ls',0>,[counter],\
                             [rsi + LDR_DATA_TABLE_ENTRY.DllBase],\
                             [rsi + LDR_DATA_TABLE_ENTRY.EntryPoint],rax,rbx
         pop     rsi
         inc     [counter]
         mov     rsi,[rsi + LDR_DATA_TABLE_ENTRY.InLoadOrderLinks]
         cmp     rsi,[firstEntry]     ;//<----- Это последняя запись Entry?
         jnz     @b

В процессе экспериментов выяснилось, что не во всех полях структуры LDR_DATA_TABLE_ENTRY лежат валидные значения, а потому слепо доверять им не стоит. Например на скрине ниже адрес точки-входа в Ntdll почему-то сброшен в нуль, хотя для остальных DLL он правильный (сравнивал с показанием WinDbg). Зато верными всегда будут: база модуля в памяти, его размер, время создания TimeDataStamp, а так-же имя Full\BaseDllName (всех тушканчиков проверять было лень).

peb_3.webp



2.3. Структура «RTL_USER_PROCESS_PARAMETERS»

Поверхностно ознакомившись со-структурой загрузчика LDR_DATA_TABLE_ENTRY, вернёмся обратно в РЕВ, и на этот раз заглянем в RTL_USER_PROCESS_PARAMETERS, которая находится под контролем уже исполнительной подсистемы ядра Executive. Прототип её зарыт в спойлере, а указатель лежит в РЕВ по смещению 0х20. Обратите внимание, что адрес смотрит в область хипа нашего процесса, и соответственно полностью доступен для записи:

Код:
0:000> dt _peb 000007fffffda000 Process*
ntdll!_PEB
   +0x020 ProcessParameters    : 0x00000000`00272080  _RTL_USER_PROCESS_PARAMETERS
   +0x030 ProcessHeap          : 0x00000000`00270000  Void
   +0x0f0 ProcessHeaps         : 0x00000000`77961c40  -> 0x00000000`00270000 Void
   +0x100 ProcessStarterHelper : (null)
   +0x300 ProcessStorageMap    : (null)

0:000> !address 00272080

Usage:             <unclassified>
Allocation Base:   00000000`00270000
Base Address:      00000000`00270000
End Address:       00000000`00276000
Region Size:       00000000`00006000
Type:              00020000  MEM_PRIVATE
State:             00001000  MEM_COMMIT
Protect:           00000004  PAGE_READWRITE  <---- Атрибуты R\W

Код:
0: kd> dt _RTL_USER_PROCESS_PARAMETERS –v

ntdll!_RTL_USER_PROCESS_PARAMETERS, 30 elements, 0x400 bytes
   +0x000 MaximumLength    : Uint4B
   +0x004 Length           : Uint4B
   +0x008 Flags            : Uint4B
   +0x00c DebugFlags       : Uint4B
   +0x010 ConsoleHandle    : Ptr64 to Void
   +0x018 ConsoleFlags     : Uint4B
   +0x020 StandardInput    : Ptr64 to Void
   +0x028 StandardOutput   : Ptr64 to Void
   +0x030 StandardError    : Ptr64 to Void
   +0x038 CurrentDirectory : struct  _CURDIR,         2 elements, 0x18 bytes
   +0x050 DllPath          : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x060 ImagePathName    : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x070 CommandLine      : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x080 Environment      : Ptr64 to Void
   +0x088 StartingX        : Uint4B
   +0x08c StartingY        : Uint4B
   +0x090 CountX           : Uint4B
   +0x094 CountY           : Uint4B
   +0x098 CountCharsX      : Uint4B
   +0x09c CountCharsY      : Uint4B
   +0x0a0 FillAttribute    : Uint4B
   +0x0a4 WindowFlags      : Uint4B
   +0x0a8 ShowWindowFlags  : Uint4B
   +0x0b0 WindowTitle      : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x0c0 DesktopInfo      : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x0d0 ShellInfo        : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x0e0 RuntimeData      : struct  _UNICODE_STRING, 3 elements, 0x10 bytes
   +0x0f0 CurrentDir       : [32] struct _RTL_DRIVE_LETTER_CURDIR, 4 elements, 0x18 bytes
   +0x3f0 EnvironmentSize  : Uint8B
   +0x3f8 EnvironmentVer   : Uint8B
0: kd>

Одним из интересных полей в этой структуре является Environment, через которое можно контробандным путём подобраться к глобальным переменным системы. Так практически без вызовов API мы получим например имя пользователя\компьютера, пути к профильным папкам, информацию о процессоре, и многое-многое другое. Здесь нужно учитывать, что текстовые строки в Environment хранятся не в привычном нам виде ansi, а как 2-байта на символ unicode. В своём примере я не стал выводить всю длинную портянку на консоль, и установил счётчик только на первые 10 строк (при желании можете изменить значение в регистре RCX).

C-подобный:
         mov     rsi,[gs:60h]
         mov     rsi,[rsi + PEB.ProcessParameters]
         mov     rsi,[rsi + RTL_USER_PROCESS_PARAMETERS.Environment]
         add     rsi,10h
         mov     rcx,10

@@:      push    rsi rcx     ;// RSI источник, RCX счётчик
         mov     rdi,buff    ;// RDI приёмник, для преобразования UnicodeToAnsi
@copy:   lodsw               ;// берём по 2 байта Word из источника,
         stosb               ;//    ...и сохраняем по одному Byte в приёмник.
         or      ax,ax       ;// это конец строки? (терминальный нуль)
         jnz     @copy       ;// нет – продолжить..

        cinvoke  CharToOem,buff,buff        ;//<---- для печати возможной кириллицы на консоль
        cinvoke  printf,<10,' %s',0>,buff

         pop     rcx rsi
         shl     eax,1       ;// коррекция указателя
         sub     rax,2       ;// ...^^^^^^
         add     rsi,rax     ;// следущая строка
         loop    @b

peb_5.webp


Помимо переменных, в структуре RTL_USER_PROCESS_PARAMETERS можно найти размер нашего окна в пикселях, строку его заголовка, полный путь до исполняемого файла, и если повезёт путь до системной оснастки PowerShell. Но в данном случае мы остановимся на таких атрибутах консольного окна, как стандартные дескрипторы ввода-вывода StdHandle, а так-же дескрипторе Handle самой консоли. В штатном режиме эту инфу возвращают функции GetStdHandle() + GetConsoleWindow():

C-подобный:
         pop     rsi
         mov     rsi,[rsi + PEB.ProcessParameters]
         mov     rax,[rsi + RTL_USER_PROCESS_PARAMETERS.ConsoleHandle]
         mov     rbx,[rsi + RTL_USER_PROCESS_PARAMETERS.StandardInput]
         mov     rdi,[rsi + RTL_USER_PROCESS_PARAMETERS.StandardOutput]
         mov     rbp,[rsi + RTL_USER_PROCESS_PARAMETERS.Environment]
         mov     r10,[rsi + RTL_USER_PROCESS_PARAMETERS.ImagePathName+8]
         mov     r11,[rsi + RTL_USER_PROCESS_PARAMETERS.ShellInfo+8]
        cinvoke  printf,<10,\
                         10,' Console   Handle....: 0x%04x <---- DANGER! Attacks possible!',\
                         10,' StdInput  Handle....: 0x%02x',\
                         10,' StdOutput Handle....: 0x%02x',\
                         10,' Environment.........: 0x%08I64x',\
                         10,' Window Title........: %ls',\
                         10,' PowerShell Path.....: %ls',10,0>,rax,rbx,rdi,rbp,r10,r11

peb_4.webp



3. Потенциальные атаки на консоль

До этого момента мы просто собирали информацию из РЕВ, без последующего применения её на практике.
Во втором раунде проведём небольшие эксперименты, в процессе которых попробуем найти уязвимость в консольном окне Win. Ошибка связана с дескриптором, который прописывается в поле ConsoleHandle структуры RTL_USER_PROCESS_PARAMETERS. Поэтому на скрине выше я и обозначил его как «Внимание, возможны атаки!».

Суть в том, что у любого процесса может быть только одна консоль, и при попытке запросить вторую функцией AllocConsole(), система возвращает ошибку «Доступ запрещён!» с кодом STATUS_ACCESS_DENIED=0xC0000022 (WinError=5). Если зайти на MSDN в поисках прототипа данной функции, можно найти предупреждение такого характера:

AllocConsole.webp


Поскольку эта API пользовательского режима из либы Kernel32, значит проверка на наличие у нас консольного окна должна осуществляться где-то рядом. Попробуем дизассемблировать функцию в отладчике.. Упс, и точно! Сначала код заходит в критическую секцию (чтобы ему не мешали другие потоки), после чего сразу видим тест на нуль поля ConsoleHandle. Если приложение с графическим интерфейсом GUI и консольного окна у него нет, то осуществляется прыжок на создание je kernel32!AllocConsole+0x54, иначе выход из критической секции, с ошибкой «Доступ запрещён!». Ну это уж совсем по ламерски..

Код:
0:000> uf /i AllocConsole  ; 49 instructions scanned

kernel32!AllocConsole:
00000000`76cf48a0 fff3             push    rbx
00000000`76cf48a2 4881ec80060000   sub     rsp,680h
00000000`76cf48a9 488b05a06a0800   mov     rax,qword ptr [kernel32!_security_cookie]
00000000`76cf48b0 4833c4           xor     rax,rsp
00000000`76cf48b3 4889842470060000 mov     qword ptr [rsp+670h],rax
00000000`76cf48bb 488d0d5e660800   lea     rcx,[kernel32!DllLock]
00000000`76cf48c2 ff1500830100     call    qword ptr [kernel32!RtlEnterCriticalSection]
;//------------------------------------------------------------------

00000000`76cf48c8 654c8b1c253000   mov     r11,qword ptr  gs:[30h]
00000000`76cf48d1 498b4360         mov     rax,qword ptr [r11+60h]     -----> получили линк на PEB
00000000`76cf48d5 488b5020         mov     rdx,qword ptr [rax+20h]     -----> PEB -> RTL_USER_PROCESS_PARAMETERS
00000000`76cf48d9 48837a1000       cmp         qword ptr [rdx+10h],0   -----> проверка поля “ConsoleHandle” на y/n
00000000`76cf48de 7414             je      kernel32!AllocConsole+0x54

;//------------------------------------------------------------------
kernel32!AllocConsole+0x40:
00000000`76cf48e0 488d0d39660800   lea     rcx,[kernel32!DllLock]
00000000`76cf48e7 ff15d3820100     call    qword ptr [kernel32!RtlLeaveCriticalSection]
00000000`76cf48ed b9220000c0       mov     ecx,0C0000022h              -----> STATUS_ACCESS_DENIED
00000000`76cf48f2 eb64             jmp     kernel32!AllocConsole+0xb8
...............

Таким образом, чтобы выделить себе запрещённую вторую консоль, достаточно сбросить в нуль поле ConsoleHandle структуры RTL_USER_PROCESS_PARAMETERS, и потянуть за AllocConsole() ещё раз. При этом что интересно, прежняя консоль превратится в зомби, поскольку дескриптора у её окна уже нет, и соответственно система не сможет найти пациента. Более того, ничто не мешает создать таким образом и третье\четвёртое консольное окно, или вообще хоть 1000 штук в цикле. Эти безхозные окна нельзя будет даже закрыть, и они будут висеть на рабочем столе вплоть до следующей перезагрузки системы. То-есть получим ничто иное, как уязвимость типа «Отказ в обслуживании» DoS. Правда баг пофиксили на 64-битных системах Win7 SP1, а потому работает он только в 32-битной WinXP.


4. Структура «KUSER_SHARED_DATA»

Под занавес хотелось-бы сказать пару слов об ещё одной структуре ядра – это промапленная в юзер-спейс KUSER_SHARED_DATA.
Так-же как и РЕВ, она призвана сократить кол-во переходов из юзера в ядро, чтобы по каждым пустякам системные службы не тревожили боса. В отличии от РЕВ, адрес этой структуры жёстко фиксирован значением 0x00000000`7ffe0000 как на системах х32, так и на системах х64 (см.отладчик x64dbg).

Из наиболее интересных полей стоит отметить: системные тики часов, которые возвращает функция GetTickCount(), размер установленной физ.памяти ОЗУ (указывается в 4К-байтных фреймах), кол-во физ.процессоров CPU с логическими их ядрами, ну и разрядность приложения х32\64. Ядро ОС тупо отображает\проецирует данную структуру из своей тушки в пространство пользователя, а потому модифицировать её безсмысленно – при следующем тике часов ядро всё-равно восстановит содержимое.

C-подобный:
         mov     rsi,0x7ffe0000
         movzx   eax, [rsi + KUSER_SHARED_DATA.ImageNumberLow]
         mov     ebx, [rsi + KUSER_SHARED_DATA.ActiveConsoleId]
         mov     ebp, [rsi + KUSER_SHARED_DATA.NumberOfPhysicalPages]
         imul    rbp, 4096
         shr     rbp, 20
         movzx   edi, [rsi + KUSER_SHARED_DATA.ActiveGroupCount]
         mov     r10d,[rsi + KUSER_SHARED_DATA.ActiveProcessorCount]
         mov     r11, [rsi + KUSER_SHARED_DATA.TickCount]
         imul    r11d,[rsi + KUSER_SHARED_DATA.TickCountMultiplier]
        cinvoke  printf,<10,\
                         10,' ******* KUSER_SHARED_DATA *******',\
                         10,' ImageNumber.........: 0x%04x',\
                         10,' ConsoleId...........: 0x%02x',\
                         10,' PhysicalMemory......: %u Mb',\
                         10,' TotalProcessors.....: %d',\
                         10,' ProcessorCores......: %d',\
                         10,' TickCount...........: %I64u',10,0>,rax,rbx,rbp,rdi,r10,r11

sharedData.webp



5. Заключение

Статья писалась с надеждой, что начинающий реверс-инженер почерпнёт из неё хоть что-то полезное. По сути нужно просто взять за правило, что если в листингах кода встречается конструкция типа mov rax,[gs:0x60] (чтение сегментных регистров fs\gs ), значит жди неприятностей. К примеру, на системах х32 таким способом малварь часто устанавливала свой обработчик исключений SEH, чтобы не вызывать палёную SetUnhandledExceptionFilter(). В скрепке найдёте полный исходник представленных выше блоков на ассемблере FASM, инклуд с описанием структур РЕВ64, а так-же готовое приложение х64 для тестов. Всем удачи, пока!
 

Вложения

Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab