Одним из основных понятий в биологических дисциплинах является захват ареала обитания (области распространения таксонов). На подсознательном уровне все живые организмы пытаются занять свою нишу в общей сфере, любыми путями оберегая доступы к ней из вне. Аналогичной политики придерживаются и операционные системы, всё больше наделяемые интеллектом их разработчиками. Так, например, запущенный на исполнение процесс блокирует доступ ко-всем своим файлам и на запрос их копирования, система дипломатично посылает нас на материк:
Здесь и далее, в качестве кролика я выбрал системный файл SAM (Security Account Manager) – некий сейф с базой-данных учётных записей Win. Он хранит хэши паролей и приватную информацию о всех пользователях системы, а сами данные оформлены в виде древа структур настроек безопасности. На физическом уровне, база живёт в одноимённой ветке реестра HKLM\SAM, доступ к которой закрыт для всех, включая администратора. Система хранит жалкую копию валидного SAM-файла в своём дире ..\Windows\Repair, только создаётся он про запас в момент установки системы и в своей тушке не содержит ничего интересного (поэтому системный протекторат и позволяет его открывать).
Можно-ли программным способом утянуть реальный SAM-файл из под носа работающего мастдая? В документации Microsoft чёрным по-белому написано, что этим файлом оперировать нельзя – он доступен только ядру. Однако на заборе тоже написано, ..и если нельзя, но сильно хочется, то скопировать его всё-таки можно и данная статья подскажет как.
В сети встречаются много вариантов решения данной проблемы и все в унисон твердят, мол "загрузись с Live-CD, или линуха", т.е. советуют тупо остановить систему, после чего нажать F5 во-внешней оболочке сможет даже обезьяна. Но как быть, если кражей должен заниматься попавший на чужбину шелл-код? Он сам верхом на винде и не может рубить сук, на котором сидит. Вот об этом и поговорим..
-----------------------------------------------------
Системные объекты и их дескрипторы (handle)
Начнём с того, что в глазах систем класса Win-NT, мир существует в виде объектов. Объектно-ориентированный подход удобен тем, что при необходимости позволяет добавлять новые ресурсы, не затрагивая при этом код самого ядра Ntoskrnl.exe. На логическом уровне, объект – это просто структура данных, так-что если в обозримом будущем в системе появится какой-нибудь иноземный ресурс, под него достаточно будет выделить структуру и всё. Здесь нужно отдать должное инженерам, в пытливых умах которых нет-нет да и проскакивают гениальные идеи.
Под определение "ресурс" попадает всё-что шевелится, однако только разделяемым ресурсам выпала честь носить громкое имя "объект". На рисунке ниже, утилита "Object Explorer" от знаменитого хакера Four-F (прикреплена в скрепке). Когда-то он тусовался на васме и внёс большой вклад в область программирования драйверов. Утилита предоставляет исчерпывающую информацию о всех системных объектах, свойства которых можно просмотреть двойным кликом.
Мастдай обращается к объектам по их типу Type, и каждый тип имеет своё числовое значение. Например на семёрке и XP, объект типа "File" имеет значение
Когда мы запускаем процесс на исполнение, система создаёт для него окружение и сохраняет всю подноготную в своей ядерной структуре "EPROCESS" (у каждого процесса она своя). Внутри этой структуры нашлось место и для таблицы-объектов текущего процесса "ObjectTable", где хранятся дескрипторы Handle всех используемых нашим приложением объектов. Открывая объект по имени (например файл или устройство), процесс получает взамен его дескриптор, при помощи которого можно будет в дальнейшем ссылаться на этот объект. Ссылка по указателю работает быстрее чем по имени, т.к. при этом диспетчер может сразу найти его, не перебирая огромный список имён. Если запросить у отладчика WinDbg формат структуры _eprocess, то можно обнаружить в ней указатель на таблицу-объектов:
Самое обычное приложение типа "Hello World" может иметь до 50 объектов, большая часть из которых работает в фоне. Отметим, что объекты могут быть как именованные (тогда они находятся в пространстве имён), так и безымянные – например создаваемые нами объекты синхронизации типа Event, Semapfore и прочие (у них нет имени, но есть дескриптор). В демонстрационном примере я создал минимальное приложение и открыл его в утилите М.Руссиновича "Process Explorer". Она отображает тип объекта (жалко без числового значения), привязанный к нему дескриптор, флаги доступа к объекту Access, и адрес объекта в пространстве ядра:
На рисунке выше нас интересуют флаги доступа к объекту (столбец Access). Система имеет пул-объектов в виде готовых шаблонов, на которые слой-за-слоем композиционным образом (логическое сложение OR) накладываются различные атрибуты. Теперь система возвращает нам дескриптор, который представляет из-себя указатель на элемент в таблице-объектов.
Очевидно, что если к файлу нет доступа (в данном случае к SAM), значит он занят каким-то процессом и в своих закромах система должна хранить соответствующий флаг. Этот флаг как-раз и припрятан в значении столбца Access. Вот если-бы мы могли подобраться к нему и пропатчить доступ под себя, тогда можно было скопировать файл без всяких проблем. Но в столбце ObjectAddress к сожалению мы видим значение, которое указывает на область адресов ядра выше
Программируем вывод таблицы-объектов
Что можно сделать с пользовательского уровня, так это просто перечислить все активные объекты. Практической пользы от этого мало, но для повышения скила можно написать подобную утилиту, где соло будет играть всего одна функция NtQuerySystemInformation(). Если её аргументу "Info-Class" вскормить значение SystemHandleInformation, то за один выстрел она вернёт дескрипторы всех объектов, которые открыты на текущий момент в системе. В приёмном буфере, каждая из описывающих дескрипторы структур будет иметь такой формат, а в первом дворде буфера мы получим их кол-тво. Эти две структуры известны как SYSTEM_HANDLE_INFORMATION и SYSTEM_HANDLE:
Как видим, выхлоп данной функции достаточно прозрачный, и нам остаётся лишь через VirtualAlloc() выделить память под буфер, и в цикле пропарсить все структуры SYSTEM_HANDLE. Чтобы узнать размер требуемого буфера (величина не постоянная и зависит от числа активных процессов), нужно первый раз вызвать NtQuerySystemInformation() с заведомо кривым аргументом, и тогда функция вернёт требуемый размер. Вот код и результат его работы:
Можно модернизировать этот код, например под вывод объектов, которые принадлежат только нашему процессу по его PID, или-же вывести из общей кухни только объекты типа "File". Для этого достаточно внести условие, и переходить по нему. Вот пример фильтрации вывода по идентификатору процесса PID:
А вот с фильтрацией вывода только файлов может возникнуть проблема. Как упоминалось выше, объект типа-файл имеет разные константы на различных версия Win, поэтому нужно будет вычислять эту константу динамически. Например можно таскать с собой какой-нибудь файл и открыв, искать его тип в этом буфере по хэндлу. Другой вариант – открыть системное устройство NUL, которое представляет собой виртуальный файл. Этот файл поддерживает операцию Write, но при чтении всегда будет возвращать EOF (End of File), т.е. данные он не сохраняет. В любом случае, нужно перебирать тип по хэндлу.
Родственными к NtQuerySystemInformation() являются функции NtQueryObject() и NtQueryInformationFile(). Обе эти функции вплоть до структур описываются в бестцеллере Гарри Небета "Native_API_Reference". Судя по документации, они должны возвращать соответственно имя объекта, и имя файла по дескрипторам, однако у меня эти функции дают осечку (видят только имена файлов, которые открыл мой процесс), поэтому примеры их здесь я не привожу.
Объект типа "FILE"
Система создаёт данный объект, когда мы вызываем функцию CreateFile().
Посмотрим на её прототип:
Судя по описанию, для самовыражения здесь места достаточно. У аргументов этой функции наблюдается явный реверанс в сторону политики безопасности. Зомбируя нас кодилом, она предлагает кучу вариаций различных атрибутов, которые можно логически складывать посредством OR. Но что особенно важно, мы можем и наоборот выбрать только один атрибут из всех и это распахивает перед нами все двери.
Например, если посмотреть на столбец "Подразумевает", он предлагает атрибут доступа FILE_READ_ATTRIBUTES. Выставив в запросе только его, мы можем обмануть систему и получить таким образом долгожданный дескриптор закрытого объекта. Запросы с таким флагом позволяют открывать буквально любые системные файлы, кроме файла подкачки Pagefile.sys. А вот если мы укажем GENERIC_READ (в составе которого лежит и FILE_READ_ATTRIBUTES), то система обламает нас по-полной, т.к. примет запрос за попытку чтения-данных.
Посмотрим, что представляет из-себя объект типа File под именем SAM. Здесь видно, что флаги доступа к нему (в столбце Access) имеют значение
Теперь посмотрим на структуру FILE_OBJECT, которую любезно предоставил WinDbg. После того-как функция CreateFile() вернёт нам дескриптор открываемого (создаваемого) файла, этот дескриптор всегда будет указывать на данную структуру. Не будем ворошить это осиное гнездо, а рассмотрим лишь те его поля, которые имеют прямое отношение к проблеме чтения SAM-файла.
Значит со-смещения
Но на что стоит обратить особое внимание, так это на 8-байтный (64-бит) указатель позиции в файле "CurrentByteOffset". Он определён как LARGE_INTEGER, что означает два двойных слова dword, или одно 64-битное слово QuadPart (в зависимости от разрядности операционной системы). Этим полем оперирует функции перемещения указателя в файле типа SetFilePointer() или fseek():
К сожалению как и всё возвышенное, структура FILE_OBJECT находится в пространстве ядра и без драйвера мы не сможем программно подобраться к ней. Иначе можно было найти дескриптор файла SAM в таблице-объектов функцией NtQuerySystemInformation(), и напильником заточить его FILE_OBJECT под себя. Другими словами, из пользовательского режима возиться с атрибутами открытого файла – идея заранее провальная, и нужно применить смекалку с тяжёлой артиллерией.
Прямое чтение секторов диска
Теперь будем мыслить так..
Любой файл (в данном случае SAM) изначально хранится на диске и запускается с него только по требованию. А что если через драйвер файловой системы NTFS попытаться считать его, обратившись напрямую к кластерам диска? В этом случаем получим чтение не из памяти, а с носителя, и обойдя атрибуты-доступа наша лампада засияет ярче - этот вариант никогда не даёт осечек. Чтобы понять идею, рассмотрим поверхностно базовые принципы работы подсистемы ввода-вывода..
Операционная система, представляет устройства в виде файлов. В качестве устройства в нашем случае выступает диск. Как и обычный файл, диск можно открыть функцией CreateFile(), перемещать по его блинам файловый указатель и производить операции чтения-записи. Таким образом, при открытии девайсов создаётся такая-же структура FILE_OBJECT, только к ней привязывается дочерняя структура DEVICE_OBJECT, представляющая конкретный объект DEVICE. В этом объекте хранится ссылка на управляющий драйвер, а функции этого драйвера описываются во-вложенном в DEVICE_OBJECT объекте типа DRIVER_OBJECT. Пройдясь в отладчике по цепочке этих структур можно выстроить визуальную картину:
Для взаимодействия с драйверами из пользовательского режима, система имеет вполне легитимную функцию DeviceIoControl(), которая в своём аргументе ожидает код запрашиваемой операции IOCTL_xx. Кодов этих – как звёзд на небе, и среди них имеется
Обработав поступивший запрос, через этот-же DeviceIoControl() драйвер вернёт нам структуру RETRIEVAL_POINTERS_BUFFER, с полной картой кластеров, которые отведены на диске под запрашиваемый файл – нам остаётся только считать их. Структура STARTING_VCN_INPUT_BUFFER со-сброшенным в нуль полем "StartingVcn" так-же оборачивается в пакет-запроса, и характеризует номер начального виртуального кластера "Vcn":
Я не буду приводить здесь операцию чтения, т.к. код относится к классу вредоностных, но выведу только информацию о местоположении файла. Дальше функцией ReadFile() нужно будет считать эти кластеры и в буфере мы получим защищённый файл – вот код:
Здесь видно, что мой SAM на диске не фрагментирован и живёт в одном фрагменте. Можете в переменную "fName" подставить другой путь и посмотреть, из скольки фрагментов он состоит. Защититься от чтения сырых кластеров диска невозможно – диск всегда открыт за R\W, не уязвимость-ли это?
Здесь и далее, в качестве кролика я выбрал системный файл SAM (Security Account Manager) – некий сейф с базой-данных учётных записей Win. Он хранит хэши паролей и приватную информацию о всех пользователях системы, а сами данные оформлены в виде древа структур настроек безопасности. На физическом уровне, база живёт в одноимённой ветке реестра HKLM\SAM, доступ к которой закрыт для всех, включая администратора. Система хранит жалкую копию валидного SAM-файла в своём дире ..\Windows\Repair, только создаётся он про запас в момент установки системы и в своей тушке не содержит ничего интересного (поэтому системный протекторат и позволяет его открывать).
Можно-ли программным способом утянуть реальный SAM-файл из под носа работающего мастдая? В документации Microsoft чёрным по-белому написано, что этим файлом оперировать нельзя – он доступен только ядру. Однако на заборе тоже написано, ..и если нельзя, но сильно хочется, то скопировать его всё-таки можно и данная статья подскажет как.
В сети встречаются много вариантов решения данной проблемы и все в унисон твердят, мол "загрузись с Live-CD, или линуха", т.е. советуют тупо остановить систему, после чего нажать F5 во-внешней оболочке сможет даже обезьяна. Но как быть, если кражей должен заниматься попавший на чужбину шелл-код? Он сам верхом на винде и не может рубить сук, на котором сидит. Вот об этом и поговорим..
-----------------------------------------------------
Системные объекты и их дескрипторы (handle)
Начнём с того, что в глазах систем класса Win-NT, мир существует в виде объектов. Объектно-ориентированный подход удобен тем, что при необходимости позволяет добавлять новые ресурсы, не затрагивая при этом код самого ядра Ntoskrnl.exe. На логическом уровне, объект – это просто структура данных, так-что если в обозримом будущем в системе появится какой-нибудь иноземный ресурс, под него достаточно будет выделить структуру и всё. Здесь нужно отдать должное инженерам, в пытливых умах которых нет-нет да и проскакивают гениальные идеи.
Под определение "ресурс" попадает всё-что шевелится, однако только разделяемым ресурсам выпала честь носить громкое имя "объект". На рисунке ниже, утилита "Object Explorer" от знаменитого хакера Four-F (прикреплена в скрепке). Когда-то он тусовался на васме и внёс большой вклад в область программирования драйверов. Утилита предоставляет исчерпывающую информацию о всех системных объектах, свойства которых можно просмотреть двойным кликом.
Мастдай обращается к объектам по их типу Type, и каждый тип имеет своё числовое значение. Например на семёрке и XP, объект типа "File" имеет значение
0х1С
(28), хотя не факт, что на других системах оно будет совпадать:Когда мы запускаем процесс на исполнение, система создаёт для него окружение и сохраняет всю подноготную в своей ядерной структуре "EPROCESS" (у каждого процесса она своя). Внутри этой структуры нашлось место и для таблицы-объектов текущего процесса "ObjectTable", где хранятся дескрипторы Handle всех используемых нашим приложением объектов. Открывая объект по имени (например файл или устройство), процесс получает взамен его дескриптор, при помощи которого можно будет в дальнейшем ссылаться на этот объект. Ссылка по указателю работает быстрее чем по имени, т.к. при этом диспетчер может сразу найти его, не перебирая огромный список имён. Если запросить у отладчика WinDbg формат структуры _eprocess, то можно обнаружить в ней указатель на таблицу-объектов:
Самое обычное приложение типа "Hello World" может иметь до 50 объектов, большая часть из которых работает в фоне. Отметим, что объекты могут быть как именованные (тогда они находятся в пространстве имён), так и безымянные – например создаваемые нами объекты синхронизации типа Event, Semapfore и прочие (у них нет имени, но есть дескриптор). В демонстрационном примере я создал минимальное приложение и открыл его в утилите М.Руссиновича "Process Explorer". Она отображает тип объекта (жалко без числового значения), привязанный к нему дескриптор, флаги доступа к объекту Access, и адрес объекта в пространстве ядра:
C-подобный:
include 'win32ax.inc'
.data
capt db 'Тест',0
msg db 'Привет мир!',0
;//-------
.code
start: invoke MessageBox,0,msg,capt,0
invoke ExitProcess,0
.end start
На рисунке выше нас интересуют флаги доступа к объекту (столбец Access). Система имеет пул-объектов в виде готовых шаблонов, на которые слой-за-слоем композиционным образом (логическое сложение OR) накладываются различные атрибуты. Теперь система возвращает нам дескриптор, который представляет из-себя указатель на элемент в таблице-объектов.
Очевидно, что если к файлу нет доступа (в данном случае к SAM), значит он занят каким-то процессом и в своих закромах система должна хранить соответствующий флаг. Этот флаг как-раз и припрятан в значении столбца Access. Вот если-бы мы могли подобраться к нему и пропатчить доступ под себя, тогда можно было скопировать файл без всяких проблем. Но в столбце ObjectAddress к сожалению мы видим значение, которое указывает на область адресов ядра выше
0х80000000
, значит нужно будет писать драйвер, что нас совсем не вдохновляет. Соответственно способ с правкой атрибутов для доступа к файлу SAM нам не подходит и нужно искать обходные пути.Программируем вывод таблицы-объектов
Что можно сделать с пользовательского уровня, так это просто перечислить все активные объекты. Практической пользы от этого мало, но для повышения скила можно написать подобную утилиту, где соло будет играть всего одна функция NtQuerySystemInformation(). Если её аргументу "Info-Class" вскормить значение SystemHandleInformation, то за один выстрел она вернёт дескрипторы всех объектов, которые открыты на текущий момент в системе. В приёмном буфере, каждая из описывающих дескрипторы структур будет иметь такой формат, а в первом дворде буфера мы получим их кол-тво. Эти две структуры известны как SYSTEM_HANDLE_INFORMATION и SYSTEM_HANDLE:
C-подобный:
struct SYSTEM_HANDLE_INFORMATION
uCount dd 0 ;// кол-во структур 'SYSTEM_HANDLE' в буфере
aSH SYSTEM_HANDLE ;// массив структур..
ends
struct SYSTEM_HANDLE ;//<--- Size=16 (описывает один дескриптор)
uIdProcess dd 0 ;// PID процесса, которому принадлежит данный Handle
ObjectType db 0 ;// тип объекта (числовое значение)
Flags db 0 ;// флаги свойств дескриптора
Handle dw 0 ;// значение дескриптора
pAddress dd 0 ;// адрес объекта в пространстве ядра
GrantedAccess dd 0 ;// флаги доступа к объекту!!!
ends
Как видим, выхлоп данной функции достаточно прозрачный, и нам остаётся лишь через VirtualAlloc() выделить память под буфер, и в цикле пропарсить все структуры SYSTEM_HANDLE. Чтобы узнать размер требуемого буфера (величина не постоянная и зависит от числа активных процессов), нужно первый раз вызвать NtQuerySystemInformation() с заведомо кривым аргументом, и тогда функция вернёт требуемый размер. Вот код и результат его работы:
C-подобный:
format PE console
include 'win32ax.inc'
include 'ddk\ntdll.inc' ;//<-- смотри инклуд в скрепке
entry start
;//-------
.data
capt db 13,10,' OBJECT INFORMATION'
db 13,10,' =========================================='
db 13,10,' Current process PID.....: %04d'
db 13,10,' Total open object.......: %d',10
db 13,10,' PID Type Handle Obj_Addr Access'
db 13,10,' ==========================================',0
info db 13,10,' %04d 0x%02X 0x%04X 0x%08X 0x%08X',0
total db 13,10,' ========================='
db 13,10,' Total object found: %d',0
pid dd 0
found dd 0
count dd 0
frmt db '%s',0
align 16
buffSize dd 0
buffAddr dd 0
keyb db 0
;//-------
.code
start:
;//==== Заполняем буфер открытыми дескрипторами ==
;// первый выстрел холостой, чтобы получить требуемый размер буфа
invoke NtQuerySystemInformation,\
SystemHandleInformation,buffAddr,1024*2,buffSize
invoke VirtualAlloc,0,[buffSize],0x3000,4 ;// выделяем память под буф!
mov [buffAddr],eax ;// запомнить его адрес..
;// второй выстрел дробью, и в буфере получили все дескрипторы
invoke NtQuerySystemInformation,\
SystemHandleInformation,[buffAddr],[buffSize],0
;//==== Выводим шапку и общую информацию =========
mov eax,[buffAddr] ;// указатель на начало
mov eax,[eax] ;// берём первый дворд от туда
mov [count],eax ;// получили всего структур в буфере!
push eax ;//
invoke GetCurrentProcessId
mov [pid],eax ;// PID нашего процесса
pop ebx ;//
cinvoke printf,capt,eax,ebx ;// вывести инфу на консоль!!!
;//==== Полезная нагрузка Payload ================
mov esi,[buffAddr] ;// указатель на начало буфера
add esi,4 ;// пропускаем счётчик структур 'uCount'
mov ecx,[count] ;// возьмём этот счётчик в ECX для LOOP
@find: push esi ecx ;// запомнить!
and ebx,0 ;// три способа очистки регистров
xor ecx,ecx ;// ..^^^
sub edx,edx ;// ..^^^
;//---- парсим очередную структуру 'SYSTEM_HANDLE'
mov eax,[esi] ;// PID родителя
mov bl,[esi+4] ;// ObjectType
mov cx,[esi+6] ;// Handle
mov edx,[esi+8] ;// ObjTableAddr
mov edi,[esi+12] ;// Access
;//----
inc [found] ;// найдено +1
cinvoke printf,info,eax,ebx,ecx,edx,edi
pop ecx esi ;// восстановить счётчик и указатель
add esi,16 ;// переходим к следующей 'SYSTEM_HANDLE'
loop @find ;// промотать ECX-раз..
cinvoke printf,total,[found] ;// всего обработано структур
@exit: cinvoke scanf,frmt,keyb ;// ждём клаву..
cinvoke exit,0 ;// ..и на выход!
;//-------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',ntdll,'ntdll.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
import ntdll, NtQuerySystemInformation,'NtQuerySystemInformation'
include 'api\kernel32.inc'
Можно модернизировать этот код, например под вывод объектов, которые принадлежат только нашему процессу по его PID, или-же вывести из общей кухни только объекты типа "File". Для этого достаточно внести условие, и переходить по нему. Вот пример фильтрации вывода по идентификатору процесса PID:
C-подобный:
@find: push esi ecx ;// запомнить!
and ebx,0 ;// три способа очистки регистров
xor ecx,ecx ;// ..^^^
sub edx,edx ;// ..^^^
;// парсим очередную структуру 'SYSTEM_HANDLE'
mov eax,[esi] ;// PID родителя
mov bl,[esi+4] ;// ObjectType
mov cx,[esi+6] ;// Handle
mov edx,[esi+8] ;// ObjTableAddr
mov edi,[esi+12] ;// Access
cmp eax,[pid] ;// -----> сравнить PID с нашим
jne @fuck ;// -----> если не равно..
inc [found] ;// иначе: найдено +1
cinvoke printf,info,eax,ebx,ecx,edx,edi
@fuck: pop ecx esi ;// восстановить счётчик и указатель
add esi,16 ;// переходим к следующей 'SYSTEM_HANDLE'
loop @find ;// промотать ECX-раз..
А вот с фильтрацией вывода только файлов может возникнуть проблема. Как упоминалось выше, объект типа-файл имеет разные константы на различных версия Win, поэтому нужно будет вычислять эту константу динамически. Например можно таскать с собой какой-нибудь файл и открыв, искать его тип в этом буфере по хэндлу. Другой вариант – открыть системное устройство NUL, которое представляет собой виртуальный файл. Этот файл поддерживает операцию Write, но при чтении всегда будет возвращать EOF (End of File), т.е. данные он не сохраняет. В любом случае, нужно перебирать тип по хэндлу.
Родственными к NtQuerySystemInformation() являются функции NtQueryObject() и NtQueryInformationFile(). Обе эти функции вплоть до структур описываются в бестцеллере Гарри Небета "Native_API_Reference". Судя по документации, они должны возвращать соответственно имя объекта, и имя файла по дескрипторам, однако у меня эти функции дают осечку (видят только имена файлов, которые открыл мой процесс), поэтому примеры их здесь я не привожу.
Объект типа "FILE"
Система создаёт данный объект, когда мы вызываем функцию CreateFile().
Посмотрим на её прототип:
C-подобный:
handle CreateFile (
lpFileName ;// указатель на имя файла, или устройства,
dwDesiredAccess ;// тип доступа к объекту – наш клиент!!!
dwShareMode ;// расшаривание объекта (см.ниже),
lpSecurityAttributes ;// опционально (наследование),
dwCreationDisposition ;// открыть или создать объект?
dwFlagsAndAttributes ;// опционально (атрибуты),
hTemplateFile ); ;// опционально (шаблон).
;//---------------------------
;// Если dwShareMode = 0, объект НЕ доступен никому, кроме родителя.
;// Чтобы создать разделяемый объект, используйте комбинацию следующих флагов:
;//---------------------------
FILE_SHARE_DELETE - доступ закрыт для всех, кроме открывшего.
FILE_SHARE_READ - доступ на чтение.
FILE_SHARE_WRITE - доступ на запись.
dwDesiredAccess Подразумевает
------------------------------------------
GENERIC_EXECUTE FILE_EXECUTE
FILE_READ_ATTRIBUTES
STANDARD_RIGHTS_EXECUTE
SYNCHRONIZE
GENERIC_READ FILE_READ_ATTRIBUTES
FILE_READ_DATA
FILE_READ_EA
STANDARD_RIGHTS_READ
SYNCHRONIZE
GENERIC_WRITE FILE_APPEND_DATA
FILE_WRITE_ATTRIBUTES
FILE_WRITE_DATA
FILE_WRITE_EA
STANDARD_RIGHTS_WRITE
SYNCHRONIZE
Судя по описанию, для самовыражения здесь места достаточно. У аргументов этой функции наблюдается явный реверанс в сторону политики безопасности. Зомбируя нас кодилом, она предлагает кучу вариаций различных атрибутов, которые можно логически складывать посредством OR. Но что особенно важно, мы можем и наоборот выбрать только один атрибут из всех и это распахивает перед нами все двери.
Например, если посмотреть на столбец "Подразумевает", он предлагает атрибут доступа FILE_READ_ATTRIBUTES. Выставив в запросе только его, мы можем обмануть систему и получить таким образом долгожданный дескриптор закрытого объекта. Запросы с таким флагом позволяют открывать буквально любые системные файлы, кроме файла подкачки Pagefile.sys. А вот если мы укажем GENERIC_READ (в составе которого лежит и FILE_READ_ATTRIBUTES), то система обламает нас по-полной, т.к. примет запрос за попытку чтения-данных.
Посмотрим, что представляет из-себя объект типа File под именем SAM. Здесь видно, что флаги доступа к нему (в столбце Access) имеют значение
0х00000003
, т.е. почти все сброшены. Причём таким-же флагом наделены все файлы в папке system32\config, а значит вся эта братия одного поля ягоды. Я не искал константы атрибутов доступа Access, но видимо флаг(3) определяет полное эмбарго на ввод-вывод, разрешая читать только файловые атрибуты через FILE_READ_ATTRIBUTES:Теперь посмотрим на структуру FILE_OBJECT, которую любезно предоставил WinDbg. После того-как функция CreateFile() вернёт нам дескриптор открываемого (создаваемого) файла, этот дескриптор всегда будет указывать на данную структуру. Не будем ворошить это осиное гнездо, а рассмотрим лишь те его поля, которые имеют прямое отношение к проблеме чтения SAM-файла.
C-подобный:
lkd> dt _file_object
nt!_FILE_OBJECT
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 DeviceObject : Ptr32 _DEVICE_OBJECT
+0x008 Vpb : Ptr32 _VPB
+0x00c FsContext : Ptr32 Void
+0x010 FsContext2 : Ptr32 Void
+0x014 SectionObjPointer : Ptr32 _SECTION_OBJECT_POINTERS
+0x018 PrivateCacheMap : Ptr32 Void
+0x01c FinalStatus : Int4B
+0x020 RelatedFileObject : Ptr32 _FILE_OBJECT
+0x024 LockOperation : UChar
+0x025 DeletePending : UChar
+0x026 ReadAccess : UChar
+0x027 WriteAccess : UChar
+0x028 DeleteAccess : UChar
+0x029 SharedRead : UChar
+0x02a SharedWrite : UChar
+0x02b SharedDelete : UChar
+0x02c Flags : Uint4B
+0x030 FileName : _UNICODE_STRING
+0x038 CurrentByteOffset : _LARGE_INTEGER
+0x040 Waiters : Uint4B
+0x044 Busy : Uint4B
+0x048 LastLock : Ptr32 Void
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : Ptr32 _IO_COMPLETION_CONTEXT
Значит со-смещения
0х024
лежит массив байт UChar, где зарыты атрибуты доступа к данному объекту. Например в байте по смещению 0х026
"ReadAccess" хранится 8-битная маска, в которой перечислены возможные операции из столбца "Подразумевает" (см.прототип функции CreateFile() выше). В свою очередь со-смещения 0х029
начинаются атрибуты шары и т.д. Кроме того, этим полям вторит и поле "LockOperation" – запрещённые операции.Но на что стоит обратить особое внимание, так это на 8-байтный (64-бит) указатель позиции в файле "CurrentByteOffset". Он определён как LARGE_INTEGER, что означает два двойных слова dword, или одно 64-битное слово QuadPart (в зависимости от разрядности операционной системы). Этим полем оперирует функции перемещения указателя в файле типа SetFilePointer() или fseek():
C-подобный:
lkd> dt _large_integer
nt!_LARGE_INTEGER
+0x000 LowPart : Uint4B ;// младший dword позиции,
+0x004 HighPart : Int4B ;// старший dword.
+0x000 u : __unnamed
+0x000 QuadPart : Int8B ;// если система х64
;//------------------
;// Функция SetFilePointer() перемещает файловый указатель открытого файла.
;//------------------
SetFilePointer
hFile dd 0 ;// дескриптор файла или устройства
lDistanceToMove dd 0 ;// мл.слово указателя
lpDistanceToMoveHigh dd 0 ;// ст.слово
dwMoveMethod dd 0 ;// отправная точка:
;// ..начало, текущая позиция, или конец файла
К сожалению как и всё возвышенное, структура FILE_OBJECT находится в пространстве ядра и без драйвера мы не сможем программно подобраться к ней. Иначе можно было найти дескриптор файла SAM в таблице-объектов функцией NtQuerySystemInformation(), и напильником заточить его FILE_OBJECT под себя. Другими словами, из пользовательского режима возиться с атрибутами открытого файла – идея заранее провальная, и нужно применить смекалку с тяжёлой артиллерией.
Прямое чтение секторов диска
Теперь будем мыслить так..
Любой файл (в данном случае SAM) изначально хранится на диске и запускается с него только по требованию. А что если через драйвер файловой системы NTFS попытаться считать его, обратившись напрямую к кластерам диска? В этом случаем получим чтение не из памяти, а с носителя, и обойдя атрибуты-доступа наша лампада засияет ярче - этот вариант никогда не даёт осечек. Чтобы понять идею, рассмотрим поверхностно базовые принципы работы подсистемы ввода-вывода..
Операционная система, представляет устройства в виде файлов. В качестве устройства в нашем случае выступает диск. Как и обычный файл, диск можно открыть функцией CreateFile(), перемещать по его блинам файловый указатель и производить операции чтения-записи. Таким образом, при открытии девайсов создаётся такая-же структура FILE_OBJECT, только к ней привязывается дочерняя структура DEVICE_OBJECT, представляющая конкретный объект DEVICE. В этом объекте хранится ссылка на управляющий драйвер, а функции этого драйвера описываются во-вложенном в DEVICE_OBJECT объекте типа DRIVER_OBJECT. Пройдясь в отладчике по цепочке этих структур можно выстроить визуальную картину:
C-подобный:
lkd> dt _file_object
nt!_FILE_OBJECT
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 DeviceObject : Ptr32 _DEVICE_OBJECT --+
+0x008 Vpb : Ptr32 _VPB |
+0x00c FsContext : Ptr32 Void V
...... |
;//---------------- |
lkd> dt _device_object ;//<--------<<-------------+
nt!_DEVICE_OBJECT
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 ReferenceCount : Int4B
+0x008 DriverObject : Ptr32 _DRIVER_OBJECT -+
+0x00c NextDevice : Ptr32 _DEVICE_OBJECT |
+0x010 AttachedDevice : Ptr32 _DEVICE_OBJECT |
+0x014 CurrentIrp : Ptr32 _IRP |
+0x018 Timer : Ptr32 _IO_TIMER |
...... |
;//---------------- |
lkd> dt _DRIVER_OBJECT ;//<--------<<-------------+
nt!_DRIVER_OBJECT
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 DeviceObject : Ptr32 _DEVICE_OBJECT
+0x008 Flags : Uint4B
+0x00c DriverStart : Ptr32 Void
+0x010 DriverSize : Uint4B
+0x014 DriverSection : Ptr32 Void
+0x018 DriverExtension : Ptr32 _DRIVER_EXTENSION
+0x01c DriverName : _UNICODE_STRING ;//<--------//
+0x024 HardwareDatabase : Ptr32 _UNICODE_STRING
+0x028 FastIoDispatch : Ptr32 _FAST_IO_DISPATCH
+0x02c DriverInit : Ptr32 long
+0x030 DriverStartIo : Ptr32 void
+0x034 DriverUnload : Ptr32 void
+0x038 MajorFunction : [28] Ptr32 long ;//<--------//
Для взаимодействия с драйверами из пользовательского режима, система имеет вполне легитимную функцию DeviceIoControl(), которая в своём аргументе ожидает код запрашиваемой операции IOCTL_xx. Кодов этих – как звёзд на небе, и среди них имеется
FSCTL_GET_RETRIEVAL_POINTERS = 0x00090073
(коды файловой системы начинаются с приставки FS). Завернув его в пакет-запроса IRP (Interrupt_Request_Packet, см.структуру device_object) функция DeviceIoControl() передаёт его драйверу FS.Обработав поступивший запрос, через этот-же DeviceIoControl() драйвер вернёт нам структуру RETRIEVAL_POINTERS_BUFFER, с полной картой кластеров, которые отведены на диске под запрашиваемый файл – нам остаётся только считать их. Структура STARTING_VCN_INPUT_BUFFER со-сброшенным в нуль полем "StartingVcn" так-же оборачивается в пакет-запроса, и характеризует номер начального виртуального кластера "Vcn":
C-подобный:
BOOL DeviceIoControl (
HANDLE hDevice ;// имя или дескриптор устройство
DWORD dwIoControlCode ;// код операции для выполнения
LPVOID lpInBuffer ;// STARTING_VCN_INPUT_BUFFER (входные данные)
DWORD nInBufferSize ;// ..размер входного буфера = 8
LPVOID lpOutBuffer ;// RETRIEVAL_POINTERS_BUFFER (выходные данные)
DWORD nOutBufferSize ;// ..размер выходного буфера = ???
LPDWORD lpBytesReturned ;// переменная для получения кол-ва выходных байт
LPOVERLAPPED lpOverlapped ;// структура для асинхронной операции
);
struct STARTING_VCN_INPUT_BUFFER ;// Size = 8
StartingVcn dq 0 ;// Virtual Cluster Number
ends
struct RETRIEVAL_POINTERS_BUFFER ;// Size = 32
ExtentCount dd 0 ;// кол-во элементов 'Extents' в массиве
StartingVcn dq 0 ;// стартовый номер первой цепочки кластеров
struct ;// <---- массив элементов 'Extents'
NextVcn dq 0 ;// кол-во кластеров в цепочке
Lcn dq 0 ;// номер первого кластера в данной цепочке
ends
Extents dd 0 ;//
ends
Я не буду приводить здесь операцию чтения, т.к. код относится к классу вредоностных, но выведу только информацию о местоположении файла. Дальше функцией ReadFile() нужно будет считать эти кластеры и в буфере мы получим защищённый файл – вот код:
C-подобный:
format PE console
include 'win32ax.inc'
include 'ddk\ntdll.inc'
entry start
;//-------
.data
FSCTL_GET_RETRIEVAL_POINTERS = 0x00090073
struct STARTING_VCN_INPUT_BUFFER
StartingVcn dq 0 ; VCN = Virtual Cluster Number
ends
struct RETRIEVAL_POINTERS_BUFFER
ExtentCount dd 0 ;
StartingVcn dq 0 ;
struct
NextVcn dd 0,0 ;
Lcn dd 0,0 ;
ends
Extents dd 0 ;
ends
;buffPointer RETRIEVAL_POINTERS_BUFFER
capt db 13,10,' RAW-READ file: %s'
db 13,10,' ============================================='
db 13,10,' * Disk geometry'
db 13,10,' Sector size.....: %d'
db 13,10,' Cluster size.....: %d',10,0
fileInfo db 13,10,' * File info'
db 13,10,' Size.............: %d byte'
db 13,10,' Total fragments..: %d',10
db 13,10,' * Read address',0
fragment db 13,10,' Fragment #: %d'
db 13,10,' Start cluster....: %d'
db 13,10,' Data length......: %d clusters',10,0
fName db 'C:\WINDOWS\system32\config\SAM',0
Hndl dd 0
frmt db '%s',0
SecCls dd 0 ;// Число секторов на кластер.
BytesSec dd 0 ;// байт в секторе
FreeCls dd 0 ;// свободно кластеров
TotalClus dd 0 ;// всего кластеров
clsSize dd 0 ;// размер кластера
inpBuff dq 0 ;// передаваемые данные IRP
number dd 1 ;// счётчик фрагментов
outSize dd 0 ;// кол-во выходных байт
align 16
rpb RETRIEVAL_POINTERS_BUFFER ;// приёмный буфер
;//-------
.code
start:
;//==== Запрашиваем размеры кластера и сектора =====
invoke GetDiskFreeSpace,0,SecCls,BytesSec,FreeCls,TotalClus
mov eax,[SecCls] ;// секторов в кластере
mov ebx,[BytesSec] ;// байт в секторе
mul ebx ;// EAX = размер кластера!!!
mov [clsSize],eax ;// запомнить.
cinvoke printf,capt,fName,ebx,eax
;//==== Открываем SAM-файл на чтение атрибутов =====
invoke CreateFile,fName,FILE_READ_ATTRIBUTES,\
FILE_SHARE_READ,0,OPEN_EXISTING,0,0
mov [Hndl],eax ;// запомнить его дескриптор
;//==== Запрашиваем размер файла ===================
invoke GetFileSize,eax,0
push eax ;//
;//====
invoke DeviceIoControl,[Hndl],FSCTL_GET_RETRIEVAL_POINTERS,\
inpBuff,8,rpb,32,outSize,0
;//====
pop eax ;//
mov ebx,[rpb.ExtentCount] ;// число структур в буфере
cinvoke printf,fileInfo,eax,ebx
mov ecx,[rpb.ExtentCount] ;// ^^^^^
@findOffset: ;// цикл поиска карты кластеров..
push ecx ;//
mov eax,[rpb.Lcn+4] ;// номер первой цепочки кластеров файла
mov ebx,[rpb.NextVcn+4] ;// кол-во элементов Extents в структуре
cinvoke printf,fragment,[number],eax,ebx
pop ecx ;//
loop @findOffset ;// промотать по ECX..
@exit: cinvoke scanf,frmt,frmt ;//
cinvoke exit,0 ;// Game-Over!!!
;//-------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
include 'api\kernel32.inc'
Здесь видно, что мой SAM на диске не фрагментирован и живёт в одном фрагменте. Можете в переменную "fName" подставить другой путь и посмотреть, из скольки фрагментов он состоит. Защититься от чтения сырых кластеров диска невозможно – диск всегда открыт за R\W, не уязвимость-ли это?