Смысл
В рамках этой статьи мы попробуем заглянуть "под капот" механизма SMB в Windows и написать бэкдор, используя его. Разбор на примере Windows 10 1809.
Немного теории
В Windows поддержку SMB обеспечивают несколько системных модулей. Драйвер ядра srvnet.sys регистрирует и управляет всеми SMB-серверами. В современной Windows 10 этих серверов два: srv.sys и srv2.sys. Первый отвечает за SMB v1, а второй за v2 и v3. Каждый из этих серверов принимает, обрабатывает и отправляет SMB-запросы (пакеты) в специальных callback-функциях. Указатель на структуру, содежающую указатели на функции, находится в таблице SrvNetDeviceExtension, которая определена в srvnet.sys.
SrvNetDeviceExtesion содержит некоторую информацию о зарегистрированном клиенте, в том числе и указатель на структуру. Но как же происходит заполнение этой таблицы? Заглянем во внутренности одного из клиентов srv.sys.
Практика
Забегая наперед, за конфигурацию в данном драйвере отвечает функция SrvConfigurationThread, в ней именно заполняется наша будущая структура инициализируется. Представляю фрагмент псевдокода:
Как можно заметить, используется некий массив v19, заполняемый указателями на функции и именем нашего сервера. Поясню о некоторых важных нам в будущем функциях, это SrvRegisterEndpointHandler и SrvNegotiateHandler. Первая функция регистрирует так называемый "эндпоинт", грубо говоря она вызывается при самой регистрации сервера. Опять же забегая наперед она должна возвращать значение не равное нулю, как знак об удачной регистрации. SrvNegotiateHandler - это функция, подготавливающая и начинающая "переговоры" между клиентом и сервером (srv.sys), именно здесь мы будем определять функционал бэкдора. Помимо этого мы также видим что 11 элементом устанавливается значение "0x3", оно также будет нужно. Далее вызывается функция SrvNetRegisterClient из srvnet.sys, передавая в качестве аргумента заполненную структуру и переменную HANDLE SrvNetHandler. Эта функция как раз таки и копирует указатель на структуру в таблицу SrvNetDeviceExtension, регистрируя новый сервер. Далее проверка и в случае успеха происходит вызов SrvNetStartClient с говорящим за себя названием, запускающая сервер. Думаю, стоит заглянуть в srvnet.sys.
Исследуем srvnet.sys
Как мы уже знаем, srvnet.sys принимает и передает smb-запросы в сервер, он явлется неким мостом между клиентом и сервером, распределяющим куда и какие пакеты надо слать. Этот процесс происходит в SrvNetCommonReceiveHandler, псевдокод которой приведен ниже.
В обозначенном мной буфере InputBuffer хранится весь пакет, отправленный клиентом серверу.
В цикле while происходит проверка буфера на валидность и определенные "метки", говорящий о том какую callback-функцию какого сервера вызывать для его обработки. Прошу обратить внимание на значение переменной v17, в ней находится адрес "экземпляра" зарегистрированного сервера (ресерчеры прозвали его srvnet_recv, на самом деле это структура ядра) в таблице SrvNetDeviceExtension, он получается по схеме &SrvNetDeiceExtension + 0x198 + v16, где 0x198 это оффсет к списку серверов, а v16 это 8*индекс сервера. Srv2 находится под индексом 0, Srv под 1, а наш бэкдор будет находится под индексом 2. Далее у нас проходит ряд побитовых проверок этих серверов, если он не соответствует какому либо условию проверка увеличивает v16 на 1 и запускается снова, таким образом происходит попытка "переслать" запрос всем доступным серверам. Помните, я обращал внимание на значение 0x3 при регистрации? Так вот, здесь оно проверяется и при его отсутствии сервер считается невалидным и пропускается. Я не понял для чего оно нужно, но если кто то узнает или уже знает, отпишитесь мне. Далее происходит последняя проверка, она проверяет наличие ненулевого байта по v19 + v67 + 1A0, где v19 это байт, устанавливаемый другой функцией (SrvNetNotifyClientsOfEndpoint), v67 это v17 + 0x118. Наверное возник логический вопрос, что за функция это такая SrvNetNotifyClientsOfEndpoint? Это функция, которая как бы устанавливает состояние "эндпоинта", изменяя тот самый байт по адресу в v19. Если 0x1, то "эндпоинт" находится в состоянии referenced, и в состоянии dereferenced если 0x0. Происходит это исключительно если функция регистрации/дерегистрации "эндпоинта" возвращает значение > 0, именно поэтому я просил обратить внимание на то, что SrvNetRegisterEndpoint возвращает ненулевое значение в случае успеха.
Вернемся к нашим баранам, проверка пройдена и последнее что нам здесь интересно это определение вызова
которая является нашим NegotiateHandler-ом. Как вы могли понять, 17+0xB0 это адрес нашей функции в callback-таблице. Это можно проверить в WinDbg:
Происходит вызов, где вторым аргументом передается размер буфера (пакета), а третьим сам буфер.
Выводы
На это первая часть заканчивается, и скоро будет часть 2, в которой будет описан процесс написания бэкдора. У меня большая просьба к читающим: я хочу услышать конструктивную критику по поводу этой статьи, так как это моя первая полноценная статья, я хочу узнать какие есть недочеты. Если что то не понятно, можете спрашивать, я постараюсь ответить.
В рамках этой статьи мы попробуем заглянуть "под капот" механизма SMB в Windows и написать бэкдор, используя его. Разбор на примере Windows 10 1809.
Немного теории
В Windows поддержку SMB обеспечивают несколько системных модулей. Драйвер ядра srvnet.sys регистрирует и управляет всеми SMB-серверами. В современной Windows 10 этих серверов два: srv.sys и srv2.sys. Первый отвечает за SMB v1, а второй за v2 и v3. Каждый из этих серверов принимает, обрабатывает и отправляет SMB-запросы (пакеты) в специальных callback-функциях. Указатель на структуру, содежающую указатели на функции, находится в таблице SrvNetDeviceExtension, которая определена в srvnet.sys.
SrvNetDeviceExtesion содержит некоторую информацию о зарегистрированном клиенте, в том числе и указатель на структуру. Но как же происходит заполнение этой таблицы? Заглянем во внутренности одного из клиентов srv.sys.
Практика
Забегая наперед, за конфигурацию в данном драйвере отвечает функция SrvConfigurationThread, в ней именно заполняется наша будущая структура инициализируется. Представляю фрагмент псевдокода:
Как можно заметить, используется некий массив v19, заполняемый указателями на функции и именем нашего сервера. Поясню о некоторых важных нам в будущем функциях, это SrvRegisterEndpointHandler и SrvNegotiateHandler. Первая функция регистрирует так называемый "эндпоинт", грубо говоря она вызывается при самой регистрации сервера. Опять же забегая наперед она должна возвращать значение не равное нулю, как знак об удачной регистрации. SrvNegotiateHandler - это функция, подготавливающая и начинающая "переговоры" между клиентом и сервером (srv.sys), именно здесь мы будем определять функционал бэкдора. Помимо этого мы также видим что 11 элементом устанавливается значение "0x3", оно также будет нужно. Далее вызывается функция SrvNetRegisterClient из srvnet.sys, передавая в качестве аргумента заполненную структуру и переменную HANDLE SrvNetHandler. Эта функция как раз таки и копирует указатель на структуру в таблицу SrvNetDeviceExtension, регистрируя новый сервер. Далее проверка и в случае успеха происходит вызов SrvNetStartClient с говорящим за себя названием, запускающая сервер. Думаю, стоит заглянуть в srvnet.sys.
Исследуем srvnet.sys
Как мы уже знаем, srvnet.sys принимает и передает smb-запросы в сервер, он явлется неким мостом между клиентом и сервером, распределяющим куда и какие пакеты надо слать. Этот процесс происходит в SrvNetCommonReceiveHandler, псевдокод которой приведен ниже.
В обозначенном мной буфере InputBuffer хранится весь пакет, отправленный клиентом серверу.
В цикле while происходит проверка буфера на валидность и определенные "метки", говорящий о том какую callback-функцию какого сервера вызывать для его обработки. Прошу обратить внимание на значение переменной v17, в ней находится адрес "экземпляра" зарегистрированного сервера (ресерчеры прозвали его srvnet_recv, на самом деле это структура ядра) в таблице SrvNetDeviceExtension, он получается по схеме &SrvNetDeiceExtension + 0x198 + v16, где 0x198 это оффсет к списку серверов, а v16 это 8*индекс сервера. Srv2 находится под индексом 0, Srv под 1, а наш бэкдор будет находится под индексом 2. Далее у нас проходит ряд побитовых проверок этих серверов, если он не соответствует какому либо условию проверка увеличивает v16 на 1 и запускается снова, таким образом происходит попытка "переслать" запрос всем доступным серверам. Помните, я обращал внимание на значение 0x3 при регистрации? Так вот, здесь оно проверяется и при его отсутствии сервер считается невалидным и пропускается. Я не понял для чего оно нужно, но если кто то узнает или уже знает, отпишитесь мне. Далее происходит последняя проверка, она проверяет наличие ненулевого байта по v19 + v67 + 1A0, где v19 это байт, устанавливаемый другой функцией (SrvNetNotifyClientsOfEndpoint), v67 это v17 + 0x118. Наверное возник логический вопрос, что за функция это такая SrvNetNotifyClientsOfEndpoint? Это функция, которая как бы устанавливает состояние "эндпоинта", изменяя тот самый байт по адресу в v19. Если 0x1, то "эндпоинт" находится в состоянии referenced, и в состоянии dereferenced если 0x0. Происходит это исключительно если функция регистрации/дерегистрации "эндпоинта" возвращает значение > 0, именно поэтому я просил обратить внимание на то, что SrvNetRegisterEndpoint возвращает ненулевое значение в случае успеха.
Вернемся к нашим баранам, проверка пройдена и последнее что нам здесь интересно это определение вызова
Код:
v21 = *(__int64 (__fastcall **)(__int64, _QWORD, __int64))(v17 + 0xB0);
Происходит вызов, где вторым аргументом передается размер буфера (пакета), а третьим сам буфер.
Выводы
На это первая часть заканчивается, и скоро будет часть 2, в которой будет описан процесс написания бэкдора. У меня большая просьба к читающим: я хочу услышать конструктивную критику по поводу этой статьи, так как это моя первая полноценная статья, я хочу узнать какие есть недочеты. Если что то не понятно, можете спрашивать, я постараюсь ответить.
Последнее редактирование: