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

Marylin

Mod.Assembler
Red Team
05.06.2019
340
1 500
BIT
971
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 для тестов. Всем удачи, пока!
 

Вложения

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

Обучение наступательной кибербезопасности в игровой форме. Начать игру!