Аббревиатура РЕВ подразумевает «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 мемберов:
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
.Теперь по натоптанной читаем и остальные поля структуры РЕВ, принимая на грудь очередную дозу информации:
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
, которые хранят указатели на структуры за периметром РЕВ – они рассматриваются далее: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
(всех тушканчиков проверять было лень).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
Помимо переменных, в структуре 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
3. Потенциальные атаки на консоль
До этого момента мы просто собирали информацию из РЕВ, без последующего применения её на практике.
Во втором раунде проведём небольшие эксперименты, в процессе которых попробуем найти уязвимость в консольном окне Win. Ошибка связана с дескриптором, который прописывается в поле
ConsoleHandle
структуры RTL_USER_PROCESS_PARAMETERS. Поэтому на скрине выше я и обозначил его как «Внимание, возможны атаки!».Суть в том, что у любого процесса может быть только одна консоль, и при попытке запросить вторую функцией AllocConsole(), система возвращает ошибку «Доступ запрещён!» с кодом
STATUS_ACCESS_DENIED=0xC0000022
(WinError=5). Если зайти на MSDN в поисках прототипа данной функции, можно найти предупреждение такого характера:Поскольку эта 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
5. Заключение
Статья писалась с надеждой, что начинающий реверс-инженер почерпнёт из неё хоть что-то полезное. По сути нужно просто взять за правило, что если в листингах кода встречается конструкция типа
mov rax,[gs:0x60]
(чтение сегментных регистров fs\gs ), значит жди неприятностей. К примеру, на системах х32 таким способом малварь часто устанавливала свой обработчик исключений SEH, чтобы не вызывать палёную SetUnhandledExceptionFilter(). В скрепке найдёте полный исходник представленных выше блоков на ассемблере FASM, инклуд с описанием структур РЕВ64, а так-же готовое приложение х64 для тестов. Всем удачи, пока!