Привет, Хабр! Меня зовут Денис Копырин, и сегодня я хочу рассказать о том, как мы решили проблему резервного копирования по требованию на macOS. Фактически интересная проблема, с которой я столкнулся в институте, со временем переросла в большой исследовательский проект по работе с файловой системой.
Все детали под катом.
Не буду начинать издалека, скажу лишь, что все началось с проекта в МФТИ, который я разрабатывал вместе со своим научным руководителем на базовом отделе Acronis. Перед нами стояла задача организовать удаленное хранение файлов, а точнее, поддержание текущего состояния их резервных копий.
Для обеспечения безопасности данных мы используем расширение ядра macOS, которое собирает информацию о событиях в системе.
KPI для разработчиков имеет интерфейс KAUTH API, который позволяет получать уведомления об открытии и закрытии файла — и не более того.
Если вы используете KAUTH, вы должны сохранить весь файл при его открытии для записи, поскольку события записи в файл недоступны разработчикам.
Для наших целей такой информации было недостаточно.
Ведь чтобы навсегда пополнить резервную копию данных, нужно понимать, где именно пользователь (или вредоносная программа:) записала в файл новые данные.
Но какой разработчик испугался ограничений ОС? Если API ядра не позволяет получить информацию об операциях записи, то нужно придумать свой способ ее перехвата через другие инструменты ядра.
Сначала мы не хотели патчить ядро и его структуры.
Вместо этого мы попытались создать целый виртуальный том, который позволил бы нам перехватывать все проходящие через него запросы на чтение и запись.
Но при этом выяснилась одна неприятная особенность работы macOS: операционная система считает, что у нее не 1, а 2 флешки, два диска и так далее.
А поскольку второй том меняется при работе с первым, macOS начинает некорректно работать с дисками.
С этим методом было так много проблем, что от него пришлось отказаться.
Находим другое решение
Несмотря на ограничения KAUTH, этот KPI позволяет получать уведомление об использовании файла для записи перед всеми операциями.Разработчикам предоставляется доступ к абстракции файла BSD в ядре — vnode. Как ни странно, оказалось, что патчить vnode проще, чем использовать фильтрацию тома.
Структура vnode содержит таблицу функций, обеспечивающих работу с реальными файлами.
Поэтому нам пришла в голову идея заменить эту таблицу.
Идея сразу была расценена как хорошая, но для ее реализации нужно было найти саму таблицу в структуре vnode, поскольку Apple нигде не документирует ее расположение.
Для этого нужно было изучить машинный код ядра, а также разобраться, можно ли было написать по этому адресу, чтобы система после этого не умерла.
Если таблица найдена, мы просто копируем ее в память, заменяем указатель и вставляем ссылку на новую таблицу в существующую vnode. Благодаря этому все файловые операции будут проходить через наш драйвер, и мы сможем логировать все запросы пользователей, включая чтение и запись.
Поэтому поиск заветного стола стал нашей главной целью.
Учитывая, что Apple не очень-то хочет этого делать, решение проблемы — попытаться «угадать» расположение таблицы с помощью эвристики относительного размещения полей, либо взять уже известную функцию, разобрать ее и искать отклонение от этой информации.
Как искать офсет: простой способ Самый простой способ найти смещение таблицы во vnode — это эвристика, основанная на расположении полей в структуре ( ссылка на гитхаб ).
Давайте воспользуемся предположением, что необходимое нам поле v_op находится ровно в 8 байтах от v_mount. Значение последнего можно получить с помощью общедоступного KPI ( ссылка на гитхаб ):struct vnode { .
int (**v_op)(void *); /* vnode operations vector */ mount_t v_mount; /* ptr to vfs we are in */ .
}
mount_t vnode_mount(vnode_t vp);
Зная значение v_mount, мы начнем искать «иголку в стоге сена» — значение указателя на vnode ‘vp’ мы будем воспринимать как uintptr_t*, значение vnode_mount(vp) как uintptr_t. За этим следуют итерации до «разумного» значения i, пока не будет выполнено условие «haystack[i]==needle».
И если предположение о расположении полей верно, то смещение v_op равно i-1. void* getVOPPtr(vnode_t vp)
{
auto haystack = (uintptr_t*) vp;
auto needle = (uintptr_t) vnode_mount(vp);
for (int i = 0; i < ATTEMPTCOUNT; i++)
{
if (haystack[i] == needle)
{
return haystack + (i - 1);
}
}
return nullptr;
}
Как искать офсет: разборка
Несмотря на свою простоту, первый метод имеет существенный недостаток.
Если Apple изменит порядок полей в структуре vnode, простой метод сломается.
Более универсальный, но менее тривиальный метод — динамический дизассемблирование ядра.
Например, рассмотрим дизассемблированную функцию ядра VNOP_CREATE ( ссылка на гитхаб ) в macOS 10.14.6. Интересующие нас инструкции отмечены стрелочкой -> .
_VNOP_CREATE:
1 push rbp
2 mov rbp, rsp
3 push r15
4 push r14
5 push r13
6 push r12
7 push rbx
8 sub rsp, 0x48
9 mov r15, r8
10 mov r12, rdx
11 mov r13, rsi
-> 12 mov rbx, rdi
13 lea rax, qword [___stack_chk_guard]
14 mov rax, qword [rax]
15 mov qword [rbp+-48], rax
-> 16 lea rax, qword [_vnop_create_desc] ; _vnop_create_desc
17 mov qword [rbp+-112], rax
18 mov qword [rbp+-104], rdi
19 mov qword [rbp+-96], rsi
20 mov qword [rbp+-88], rdx
21 mov qword [rbp+-80], rcx
22 mov qword [rbp+-72], r8
-> 23 mov rax, qword [rdi+0xd0]
-> 24 movsxd rcx, dword [_vnop_create_desc]
25 lea rdi, qword [rbp+-112]
-> 26 call qword [rax+rcx*8]
27 mov r14d, eax
28 test eax, eax
….
errno_t
VNOP_CREATE(vnode_t dvp, vnode_t * vpp, struct componentname * cnp, struct vnode_attr * vap, vfs_context_t ctx)
{
int _err;
struct vnop_create_args a;
a.a_desc = &vnop;_create_desc; a.a_dvp = dvp; a.a_vpp = vpp;
a.a_cnp = cnp; a.a_vap = vap; a.a_context = ctx;
_err = (*dvp->v_op[vnop_create_desc.vdesc_offset])(&a;);
…
Отсканируем инструкцию по сборке, чтобы найти сдвиг в vnode dvp. «Цель» ассемблерного кода — вызвать функцию из таблицы v_op. Для этого процессору необходимо выполнить следующие действия:
- Загрузить dvp в реестр
- Разыменуйте его, чтобы получить v_op (строка 23)
- Получите vnop_create_desc.vdesc_offset (строка 24)
- Вызов функции (строка 26)
Как понять в какой регистр загрузился двп? Для этого мы использовали метод эмуляции функции, отслеживающей перемещения искомого указателя.
Согласно соглашению о вызовах System V x86_64, первый аргумент передается в регистре rdi. Поэтому мы решили мониторить все регистры, содержащие rdi. В моем примере это регистры rbx и rdi. Также копию регистра можно сохранить в стеке, который находится в отладочной версии ядра.
Зная, что регистры rbx и rdi хранят dvp, мы знаем, что в строке 23 разыменовывается vnode для получения v_op. Итак, мы получаем предположение, что смещение в конструкции равно 0xd0. Для подтверждения правильности решения продолжаем сканирование и убеждаемся в правильности вызова функции (строки 24 и 26).
Этот метод более безопасен, но, к сожалению, имеет и недостатки.
Нам приходится полагаться на то, что шаблон функции (а именно 4 шага, о которых мы говорили выше) будет одинаковым.
Однако вероятность изменения структуры функции на порядок меньше вероятности изменения порядка полей.
Поэтому мы решили пойти по второму способу.
Замена указателей в таблице
После нахождения v_op возникает вопрос, как использовать этот указатель? Есть два разных способа - перезаписать функцию в таблице (третья стрелка на картинке) или перезаписать таблицу во vnode (вторая стрелка на картинке).На первый взгляд кажется, что первый вариант выгоднее, поскольку нам достаточно заменить один указатель.
Однако у этого подхода есть 2 существенных недостатка.
Во-первых, таблица v_op одинакова для всех vnode данной файловой системы (v_op для HFS+, v_op для APFS, .
), поэтому необходима фильтрация по vnode, что может оказаться очень затратным — придется отфильтровывать лишние vnodes при каждой операции записи.
Во-вторых, таблица записывается на странице «Только для чтения».
Это ограничение можно обойти, используя запись IOMappedWrite64 в обход системных проверок.
Также, если поставляется кекст с драйвером файловой системы, то будет сложно разобраться, как удалить патч.
Второй вариант оказывается более точным и безопасным — перехватчик будет вызываться только для нужного vnode, а память vnode изначально допускает операции чтения-записи.
Поскольку заменяется вся таблица, необходимо выделить немного больше памяти (80 функций вместо одной).
А поскольку количество таблиц обычно равно количеству файловых систем, то ограничение памяти совершенно незначительно.
Именно поэтому kext использует второй метод, хотя, повторюсь, на первый взгляд кажется, что этот вариант хуже.
В итоге наш драйвер работает по следующей схеме:
- KAUTH API предоставляет vnode
- Заменяем таблицу vnode. При необходимости перехватываем операции только для «интересных» внод, например пользовательских документов.
- При перехвате проверяем какой процесс записывает и отсеиваем «своих»
- Мы отправляем клиенту синхронный запрос UserSpace, который решает, что именно нужно сохранить.
Что случилось
Сегодня у нас есть экспериментальный модуль, который является расширением ядра macOS и учитывает любые изменения файловой системы на детальном уровне.Стоит отметить, что в macOS 10.15 Apple представила новый фреймворк ( ссылка на EndpointSecurity ) для получения уведомлений об изменениях файловой системы, которую планируется использовать в Active Protection, поэтому решение, описанное в статье, объявляется устаревшим.
Теги: #macOS #Разработка для MacOS #Резервное копирование #Apple #файловая система #восстановление #Acronis #KAUTH
-
Как Выбрать 3-Фазный Онлайн-Ибп
19 Oct, 24 -
Как Написать Грамотный Отзыв На Вакансию?
19 Oct, 24 -
Lumia 920 И Lumia 820 В Москве
19 Oct, 24 -
Инфовис Инструментарий
19 Oct, 24 -
«Вебалта» Представила «Оптимисту»
19 Oct, 24 -
Мультимедийный Центр «Коди» И Yocto Project
19 Oct, 24 -
Супермаркет 2.0
19 Oct, 24 -
Видеоотчеты С Moscowjs Meetup
19 Oct, 24