Неполный патч Dirty COW

Уязвимость Dirty COW (CVE-2016–5195) - одна из самых разрекламированных и известных уязвимостей. Каждая версия Linux за последнее десятилетие, включая Android, настольные компьютеры и серверы, была уязвима. Воздействие было огромным - миллионы пользователей могли быть легко и надежно скомпрометированы, минуя обычные средства защиты от эксплойтов.

Об уязвимости было опубликовано много информации, но ее патч не анализировался подробно.

Мы в Bindecy были заинтересованы в изучении патча и всех его последствий. Удивительно, но, несмотря на широкую огласку, полученную об ошибке, мы обнаружили, что исправление было неполным.

Резюме «Грязной коровы»

Во-первых, нам нужно полное представление об исходном эксплойте Dirty COW. Мы предполагаем базовое понимание менеджера памяти Linux. Мы не вернем исходные кровавые детали, потому что талантливые люди это уже сделали.

Изначально уязвимость была в функции get_user_pages. Эта функция используется для получения физических страниц за виртуальными адресами в пользовательских процессах. Вызывающий должен указать, какие действия он намеревается выполнять на этих страницах (касание, запись, блокировка и т. Д.), Чтобы менеджер памяти мог подготовить страницы соответствующим образом. В частности, при планировании выполнения действия записи на странице внутри частного сопоставления, странице может потребоваться пройти цикл COW (копирование при записи) - исходная страница «только для чтения» копируется на новую страницу. который доступен для записи. Исходная страница может быть «привилегированной» - она ​​также может быть отображена в других процессах и даже может быть записана обратно на диск после ее изменения.

Теперь посмотрим на соответствующий код в __get_user_pages:

Цель цикла while - получить каждую страницу из запрошенного диапазона страниц. Каждая страница должна быть проверена до тех пор, пока наши требования не будут удовлетворены - для этого используется метка retry.

Роль follow_page_mask заключается в сканировании таблиц страниц, чтобы получить физическую страницу для данного адреса (с учетом разрешений PTE), или сбой, если запрос не может быть удовлетворен. Во время работы follow_page_mask устанавливается спин-блокировка PTE - это гарантирует, что физическая страница не будет освобождена до того, как мы получим ссылку.

faultin_page запрашивает у диспетчера памяти обработку ошибки по данному адресу с указанными разрешениями (также в рамках спин-блокировки PTE). Обратите внимание, что после успешного вызова faultin_page блокировка снимается - нет гарантии, что follow_page_mask будет успешным при следующей попытке; другой фрагмент кода мог испортить нашу страницу.

Исходный уязвимый код находился в конце faultin_page:

Причина удаления флагаFOLL_WRITE состоит в том, чтобы принять во внимание случай, когда флаг FOLL_FORCE применяется к VMA только для чтения (когда в VMA установлен флаг VM_MAYWRITE). В этом случае функция pte_maybe_mkwrite не установит бит записи, однако страница с ошибкой действительно готова к записи.

Если страница прошла цикл COW (отмеченный флагом VM_FAULT_WRITE) при выполнении faultin_page и VMA не доступен для записи, FOLL_WRITE flag удаляется при следующей попытке доступа к странице - будут запрошены только разрешения на чтение.

Если первая follow_page_mask выйдет из строя, потому что страница была доступна только для чтения или отсутствовала, мы попытаемся исправить это. Теперь давайте представим, что за это время, до следующей попытки получить страницу, мы избавимся от COW версия (например, используя madvise(MADV_DONTNEED)).

Следующий вызов faultin_page будет выполнен без флага FOLL_WRITE, поэтому мы получим доступную только для чтения версию страницы из кеша страниц. Теперь следующий вызов follow_page_mask также произойдет без флага FOLL_WRITE, поэтому он вернет привилегированную страницу, доступную только для чтения, в отличие от исходного запроса вызывающей стороны на доступную для записи версию страницы.

По сути, вышеупомянутый поток - это уязвимость Dirty COW - она ​​позволяет нам писать в привилегированную версию страницы только для чтения. В faultin_page было внесено следующее исправление:

И была добавлена ​​новая функция, которую вызывает follow_page_mask:

Вместо того, чтобы уменьшать запрашиваемые разрешения, get_user_pages теперь запоминает тот факт, что мы прошли цикл COW. На следующей итерации мы сможем получить страницу только для чтения для операции записи, только если указаны флаги FOLL_FORCE и FOLL_COW и что PTE помечен как грязный.

Этот патч предполагает, что привилегированная копия страницы, доступная только для чтения, никогда не будет иметь PTE, указывающего на нее с включенным грязным битом - разумное предположение… или нет?

Прозрачные огромные страницы (THP)

Обычно Linux обычно использует страницы длиной 4096 байт. Чтобы система могла управлять большими объемами памяти, мы можем либо увеличить количество записей в таблице страниц, либо использовать страницы большего размера. Мы остановимся на втором способе, который реализован в Linux с использованием огромных страниц.

Огромная страница - это страница размером 2 МБ. Одним из способов использования этой функции является механизм прозрачных огромных страниц. Хотя есть и другие способы получения огромных страниц, они выходят за рамки нашей компетенции.

Ядро будет пытаться удовлетворить соответствующие распределения памяти, используя огромные страницы. THP могут быть заменены и «разбиты» (т.е. могут быть разделены на обычные страницы размером 4096 байт) и могут использоваться в сопоставлениях anonymous, shmem и tmpfs (последние два верны только в более новых версиях ядра).

Обычно (в зависимости от флагов компиляции и конфигурации компьютера) поддержка THP по умолчанию предназначена только для анонимного сопоставления. Поддержка Shmem и tmpfs может быть включена вручную, и в целом поддержку THP можно включать и выключать во время работы системы путем записи в некоторые специальные файлы ядра.

Важная возможность оптимизации - объединить обычные страницы в огромные страницы. Специальный демон, называемый khugepaged, постоянно сканирует возможные страницы-кандидаты, которые можно было бы объединить в огромные страницы. Очевидно, что для того, чтобы быть кандидатом, VMA должна охватывать весь выровненный диапазон памяти размером 2 МБ.

THP реализуется путем включения бита _PAGE_PSE в PMD (Page Medium Directory, на один уровень выше уровня PTE). Таким образом, PMD указывает на физическую страницу размером 2 МБ, а не на каталог PTE. Каждый раз, когда сканируются таблицы страниц, PMD должны проверяться с помощью функции pmd_trans_huge, чтобы мы могли решить, указывает ли PMD на pfn или на каталог PTE. На некоторых архитектурах также существуют огромные PUD (верхний каталог страниц), что приводит к страницам размером 1 ГБ.

THP поддерживается начиная с ядра 2.6.38. На большинстве устройств Android подсистема THP отключена.

Ошибка 🐞

Углубляясь в патч кода Dirty COW, который имеет дело с THP, мы видим, что та же логика, что и can_follow_write_pte, была применена к огромным PMD. Была добавлена ​​функция сопоставления с именем can_follow_write_pmd:

Однако в случае огромного PMD страницу можно пометить как грязную, не проходя цикл COW, используя функцию touch_pmd:

К этой функции обращается follow_page_mask, который будет вызываться каждый раз, когда get_user_pages пытается получить огромную страницу. Очевидно, что комментарий неверен, и в настоящее время грязный бит НЕ бессмысленен. В частности, при использовании get_user_pages для чтения огромной страницы эта страница будет помечена как грязная без прохождения цикла COW, и логика can_follow_write_pmd теперь нарушена.

На этом этапе использовать ошибку очень просто - мы можем использовать аналогичный шаблон исходной гонки Dirty COW. На этот раз, после того, как мы избавимся от скопированной версии страницы, мы должны указать исходную страницу дважды - сначала, чтобы сделать ее присутствующей, а затем включить грязный бит.

Возникает неизбежный вопрос - насколько это плохо?

Последствия ошибки

Чтобы воспользоваться ошибкой, мы должны выбрать интересную огромную страницу только для чтения в качестве цели для записи. Единственное ограничение заключается в том, что мы должны иметь возможность получить его после того, как он был удален с помощью madvise(MADV_DONTNEED). Анонимные огромные страницы, унаследованные от родительского процесса после fork, являются ценной целью, однако после их удаления они навсегда теряются - мы не можем получить их снова.

Мы обнаружили две интересные цели, в которые не следует записывать:

  • Огромная нулевая страница
  • Запечатанные (только для чтения) огромные страницы

Нулевая страница

При выдаче ошибки чтения в анонимном отображении до того, как оно было записано, мы получаем специальную физическую страницу, называемую нулевой страницей. Эта оптимизация избавляет систему от необходимости выделять в системе несколько обнуленных страниц, на которые никогда не будет производиться запись. Таким образом, одна и та же нулевая страница отображается во многих различных процессах, которые имеют разные уровни безопасности.

Тот же принцип применим и к огромным страницам - нет необходимости создавать еще одну огромную страницу, если еще не произошло ошибки записи - вместо этого будет отображена специальная страница, называемая огромной нулевой страницей. Обратите внимание, что эту функцию также можно отключить.

THP, shmem и запечатанные файлы

Файлы shmem и tmpfs также могут быть отображены с помощью THP. Файлы shmem могут быть созданы с помощью системного вызова memfd_create или mmap путем установки анонимных общих сопоставлений. Файлы tmpfs могут быть созданы с использованием точки монтирования tmpfs (обычно /dev/shm). Оба могут быть сопоставлены с огромными страницами, в зависимости от конфигурации системы.

Файлы shmem могут быть запечатаны - запечатывание файла ограничивает набор операций, разрешенных с данным файлом. Этот механизм позволяет процессам, которые не доверяют друг другу, обмениваться данными через общую память без необходимости принимать дополнительные меры для борьбы с неожиданными манипуляциями с областью общей памяти (дополнительную информацию см. man memfd_create()). Существуют три типа уплотнений -

  • F_SEAL_SHRINK: размер файла нельзя уменьшить
  • F_SEAL_GROW: размер файла нельзя увеличить
  • F_SEAL_WRITE: содержимое файла не может быть изменено

Эти печати можно добавить в файл shmem с помощью системного вызова fcntl.

POC

Наш POC демонстрирует перезапись огромной нулевой страницы. Перезапись shmem должна быть в равной степени возможной и приведет к альтернативному пути эксплойта.

Обратите внимание, что после первой ошибки записи на нулевую страницу она будет заменена новым свежим (и обнуленным) THP. Используя этот примитив, мы успешно завершаем несколько процессов. Вероятное последствие перезаписи огромной нулевой страницы - неправильные начальные значения внутри больших разделов BSS. Распространенным уязвимым шаблоном будет использование нулевого значения в качестве индикатора того, что глобальная переменная еще не инициализирована.

Следующий пример сбоя демонстрирует этот шаблон. В этом примере поток JS Helper Firefox создает NULL-deref, вероятно, потому, что логическое значение, указанное %rdx, ошибочно сообщает, что объект был инициализирован:

Thread 10 "JS Helper" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffe2aee700 (LWP 14775)]
0x00007ffff13233d3 in ?? () from /opt/firefox/libxul.so
(gdb) i r
rax            0x7fffba7ef080 140736322269312
rbx            0x0 0
rcx            0x22 34
rdx            0x7fffba7ef080 140736322269312
rsi            0x400000000 17179869184
rdi            0x7fffe2aede10 140736996498960
rbp            0x0 0x0
rsp            0x7fffe2aede10 0x7fffe2aede10
r8             0x20000 131072
r9             0x7fffba900000 140736323387392
r10            0x7fffba700000 140736321290240
r11            0x7fffe2aede50 140736996499024
r12            0x1 1
r13            0x7fffba7ef090 140736322269328
r14            0x2 2
r15            0x7fffe2aee700 140736996501248
rip            0x7ffff13233d3 0x7ffff13233d3
eflags         0x10246 [ PF ZF IF RF ]
cs             0x33 51
ss             0x2b 43
ds             0x0 0
es             0x0 0
fs             0x0 0
gs             0x0 0
(gdb) x/10i $pc-0x10
   0x7ffff13233c3: mov    %rax,0x10(%rsp)
   0x7ffff13233c8: mov    0x8(%rdx),%rbx
   0x7ffff13233cc: mov    %rbx,%rbp
   0x7ffff13233cf: and    $0xfffffffffffffffe,%rbp
=> 0x7ffff13233d3: mov    0x0(%rbp),%eax
   0x7ffff13233d6: and    $0x28,%eax
   0x7ffff13233d9: cmp    $0x28,%eax
   0x7ffff13233dc: je     0x7ffff1323440
   0x7ffff13233de: mov    %rbx,%r13
   0x7ffff13233e1: and    $0xfffffffffff00000,%r13
(gdb) x/10w $rdx
0x7fffba7ef080: 0x41414141 0x00000000 0x00000000 0x00000000
0x7fffba7ef090: 0xeef93bba 0x00000000 0xda95dd80 0x00007fff
0x7fffba7ef0a0: 0x778513f1 0x00000000

Это еще один пример сбоя - gdb аварийно завершает работу при загрузке символов для сеанса отладки Firefox:

(gdb) r
Starting program: /opt/firefox/firefox 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x0000555555825487 in eq_demangled_name_entry (a=0x4141414141414141, b=<optimized out>) at symtab.c:697
697   return strcmp (da->mangled, db->mangled) == 0;
(gdb) i s
#0  0x0000555555825487 in eq_demangled_name_entry (a=0x4141414141414141, b=<optimized out>) at symtab.c:697
#1  0x0000555555955203 in htab_find_slot_with_hash (htab=0x555557008e60, element=element@entry=0x7fffffffdb00, hash=4181413748, insert=insert@entry=INSERT) at ./hashtab.c:659
#2  0x0000555555955386 in htab_find_slot (htab=<optimized out>, element=element@entry=0x7fffffffdb00, insert=insert@entry=INSERT) at ./hashtab.c:703
#3  0x00005555558273e5 in symbol_set_names (gsymbol=gsymbol@entry=0x5555595b3778, linkage_name=linkage_name@entry=0x7ffff2ac5254 "_ZN7mozilla3dom16HTMLTableElement11CreateTHeadEv", len=len@entry=48, 
    copy_name=copy_name@entry=0, objfile=<optimized out>) at symtab.c:818
#4  0x00005555557d186f in minimal_symbol_reader::record_full (this=0x7fffffffdce0, this@entry=0x1768bd6, name=<optimized out>, 
    name@entry=0x7ffff2ac5254 "_ZN7mozilla3dom16HTMLTableElement11CreateTHeadEv", name_len=<optimized out>, copy_name=copy_name@entry=48, address=24546262, ms_type=ms_type@entry=mst_file_text, 
    section=13) at minsyms.c:1010
#5  0x00005555556959ec in record_minimal_symbol (reader=..., name=name@entry=0x7ffff2ac5254 "_ZN7mozilla3dom16HTMLTableElement11CreateTHeadEv", name_len=<optimized out>, copy_name=copy_name@entry=false, 
    address=<optimized out>, address@entry=24546262, ms_type=ms_type@entry=mst_file_text, bfd_section=<optimized out>, objfile=0x555557077860) at elfread.c:209
#6  0x0000555555696ac6 in elf_symtab_read (reader=..., objfile=objfile@entry=0x555557077860, type=type@entry=0, number_of_symbols=number_of_symbols@entry=365691, 
    symbol_table=symbol_table@entry=0x7ffff6a6d020, copy_names=copy_names@entry=false) at elfread.c:462
#7  0x00005555556970c4 in elf_read_minimal_symbols (symfile_flags=<optimized out>, ei=0x7fffffffdcd0, objfile=0x555557077860) at elfread.c:1084
#8  elf_symfile_read (objfile=0x555557077860, symfile_flags=...) at elfread.c:1194
#9  0x000055555581f559 in read_symbols (objfile=objfile@entry=0x555557077860, add_flags=...) at symfile.c:861
#10 0x000055555581f00b in syms_from_objfile_1 (add_flags=..., addrs=0x555557101b00, objfile=0x555557077860) at symfile.c:1062
#11 syms_from_objfile (add_flags=..., addrs=0x555557101b00, objfile=0x555557077860) at symfile.c:1078
#12 symbol_file_add_with_addrs (abfd=<optimized out>, name=name@entry=0x55555738c1d0 "/opt/firefox/libxul.so", add_flags=..., addrs=addrs@entry=0x555557101b00, flags=..., parent=parent@entry=0x0)
    at symfile.c:1177
#13 0x000055555581f63d in symbol_file_add_from_bfd (abfd=<optimized out>, name=name@entry=0x55555738c1d0 "/opt/firefox/libxul.so", add_flags=..., addrs=addrs@entry=0x555557101b00, flags=..., 
    parent=parent@entry=0x0) at symfile.c:1268
#14 0x000055555580b256 in solib_read_symbols (so=so@entry=0x55555738bfc0, flags=...) at solib.c:712
#15 0x000055555580be9b in solib_add (pattern=pattern@entry=0x0, from_tty=from_tty@entry=0, readsyms=1) at solib.c:1016
#16 0x000055555580c678 in handle_solib_event () at solib.c:1301
#17 0x00005555556f9db4 in bpstat_stop_status (aspace=0x555555ff5670, bp_addr=bp_addr@entry=140737351961185, ptid=..., ws=ws@entry=0x7fffffffe1d0) at breakpoint.c:5712
#18 0x00005555557ad1ef in handle_signal_stop (ecs=0x7fffffffe1b0) at infrun.c:5963
#19 0x00005555557aec8a in handle_inferior_event_1 (ecs=0x7fffffffe1b0) at infrun.c:5392
#20 handle_inferior_event (ecs=ecs@entry=0x7fffffffe1b0) at infrun.c:5427
#21 0x00005555557afd57 in fetch_inferior_event (client_data=<optimized out>) at infrun.c:3932
#22 0x000055555576ade5 in gdb_wait_for_event (block=block@entry=0) at event-loop.c:859
#23 0x000055555576aef7 in gdb_do_one_event () at event-loop.c:322
#24 0x000055555576b095 in gdb_do_one_event () at ./common/common-exceptions.h:221
#25 start_event_loop () at event-loop.c:371
#26 0x00005555557c3938 in captured_command_loop (data=data@entry=0x0) at main.c:325
#27 0x000055555576d243 in catch_errors (func=func@entry=0x5555557c3910 <captured_command_loop(void*)>, func_args=func_args@entry=0x0, errstring=errstring@entry=0x555555a035da "", 
    mask=mask@entry=RETURN_MASK_ALL) at exceptions.c:236
#28 0x00005555557c49ae in captured_main (data=<optimized out>) at main.c:1150
#29 gdb_main (args=<optimized out>) at main.c:1160
#30 0x00005555555ed628 in main (argc=<optimized out>, argv=<optimized out>) at gdb.c:32
(gdb) list
692   const struct demangled_name_entry *da
693     = (const struct demangled_name_entry *) a;
694   const struct demangled_name_entry *db
695     = (const struct demangled_name_entry *) b;
696 
697   return strcmp (da->mangled, db->mangled) == 0;
698 }
699 
700 /* Create the hash table used for demangled names.  Each hash entry is
701    a pair of strings; one for the mangled name and one for the demangled
(gdb)

Ссылка на наш POC

Резюме

Эта ошибка демонстрирует важность аудита исправлений в жизненном цикле разработки системы безопасности. Как показывает случай с Dirty COW и другие прошлые случаи, даже разрекламированные уязвимости могут получить неполные исправления. Эта ситуация не предназначена только для программного обеспечения с закрытым исходным кодом; Программное обеспечение с открытым исходным кодом страдает не меньше.

Не стесняйтесь комментировать любой вопрос или идею по проблеме ☺

График раскрытия

Первоначальный отчет был отправлен 22.11.17 в списки рассылки ядра и дистрибутивов. Ответ последовал незамедлительно и профессионально: заплатка была готова через несколько дней. Патч исправляет функцию touch_pmd , устанавливая грязный бит записи PMD только тогда, когда вызывающий абонент запрашивает доступ для записи.

Спасибо команде безопасности и дистрибутивам за их время и усилия по поддержанию высокого стандарта безопасности.

22.11.17 - Первоначальный отчет для [email protected] и [email protected]
22.11.17 - Назначена CVE-2017–1000405
27.11.17 - Патч был переведен на основное ядро ​​
29.11.17 - Публичное объявление