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

Техника модификации


Техника модификации системных вызовов операционной системы известна давно и использовалась в самых разных операционных системах. Это любимая техника писателей вирусов начиная с системы MS-DOS — системы, которая просто провоцировала на такие эксперименты. Но мы будем использовать эту технику в мирных целях… (В разных публикациях такие действия называют по-разному: модификация, встраивание, имплементация, подмена, перехват — они имеют свои нюансы, но в нашем обсуждении могут использоваться как синонимы.)

Если любой системный вызов в ядре операционной системы Linux вызывается косвенно через адрес в таблице (массиве) системных вызовов sys_call_table, то подменив адрес в этой селекторной таблице на собственную функцию-обработчик, мы тем самым и подменим обработчик системного вызова. В этом, собственно, и состоит техника модификации. На практике радикализм до такой степени никогда не требуется, реально нам бывает необходимо выполнять оригинальный системный вызов, но проделав некоторые собственные действия либо до него (предобработка), либо после окончания (постобработка) его (либо комбинация того и другого).

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

Реально схема будет чуть сложнее — необходимо прежде сохранить старое (оригинальное) значение системного обработчика:
— Для вызова оригинального обработчика из собственной функции обработки перед или/и после выполнения модифицированного кода;
— Для восстановления оригинального обработчика при выгрузке модуля.

Реализация


Реализация, как это всегда и бывает, несколько сложнее теории. Первая, незначительная, сложность состоит в том как записать прототип собственной функции обработки конкретного системного вызова. Малейшая некорректность прототипа с большой вероятностью приведёт просто к краху операционной системы. Решение состоит в том, чтобы просто подсмотреть и списать (как двоечник) прототип функции обработки этого системного вызова из заголовочного файла ядра <linux/syscalls.h>.

Следующая, гораздо более существенная, трудность здесь состоит в том, что селекторная таблица sys_call_table, в процессорных архитектурах которые это позволяют (а I386 и X86_64 в их числе), размещается в страницах памяти, разрешённых исключительно для чтения (readonly). Это контролируется аппаратно (средствами MMU — Memory Management Unit) и при нарушении прав доступа возбуждается исключение. Поэтому нам нужно снять флаг запрета записи на время модификации элемента sys_call_table и восстановить его после.

В архитектурах I386 и X86_64 флаг разрешения записи определяется битовым флагом в скрытом регистре состояния процессора CR0. Для выполнения нужных нам действий мы используем соответствующе функции, для 32-бит архитектуры, например, они будут выглядеть так (файл CR0.c, этот код написан на инлайновых ассемблерных вставках — расширение компилятора GCC):
// page write protect - on
#define rw_enable()              asm( "cli \n"                         "pushl %eax \n"                  "movl %cr0, %eax \n"             "andl $0xfffeffff, %eax \n"      "movl %eax, %cr0 \n"             "popl %eax" );

// page write protect - off
#define rw_disable()             asm( "pushl %eax \n"                  "movl %cr0, %eax \n"             "orl $0x00010000, %eax \n"       "movl %eax, %cr0 \n"             "popl %eax \n"                   "sti " );

P.S. Различные варианты техники записи в защищённые от записи страницы обсуждались, например, в WP: Safe or Not? и Кошерный способ модификации защищённых от записи областей ядра Linux.

Теперь мы готовы заменить любой системный вызов Linux (man(2)) на свою собственную функцию-обработчик — а это и есть то, к чему мы и стремились. Для иллюстрации работоспособности метода мы заменим (расширим) системный вызов write( 1, … ) — вывод на терминал, задублируем поток вывода в системный журнал (подобно тому что делает команда tee):
#define PREFIX "! " 
#define DEB2(...) if( debug > 1 ) printk( KERN_INFO PREFIX " ---- " __VA_ARGS__ ) 
#define LOG(...) printk( KERN_INFO PREFIX __VA_ARGS__ ) 
#define ERR(...) printk( KERN_ERR PREFIX __VA_ARGS__ ) 

static int debug = 0;                    // debug output level: 0, 1, 2 
module_param( debug, uint, 0 ); 

asmlinkage long (*old_sys_write) ( unsigned int fd, const char __user *buf, size_t count ); 

#define LEN 250 
asmlinkage long new_sys_write ( unsigned int fd, const char __user *buf, size_t count ) { 
   if( 1 == fd ) { 
      char msg[ LEN + 1 ]; 
      int n = count < LEN ? count : LEN, r; 
      if( ( r = copy_from_user( msg, (void*)buf, n ) ) != 0 ) return -EINVAL; 
      if( '\n' == msg[ n - 1 ] ) msg[ n - 1 ] = '\0'; 
      else msg[ n ] = '\0'; 
      if( strchr( msg, '!' ) != NULL ) goto rec; // to prevent recursion 
      LOG( "{%04d} %s\n", count, msg ); 
   } 
rec: 
   return old_sys_write( fd, buf, count );       // original write() 
};

static void **taddr;                             // address of sys_call_table 

static int __init wrchg_init( void ) { 
   void *waddr; 
   if( NULL == ( taddr = find_sym( "sys_call_table" ) ) ) { 
      ERR( "sys_call_table not found\n" ); return -EINVAL; 
   } 
   old_sys_write = (void*)taddr[ __NR_write ]; 
   if( NULL == ( waddr = find_sym( "sys_write" ) ) ) { 
      ERR( "sys_write not found\n" ); return -EINVAL; 
   } 
   if( old_sys_write != waddr ) { 
      ERR( "Oooops! : addresses not equal\n" ); return -EINVAL; 
   } 
   LOG( "set new sys_write syscall [%p]\n", &new_sys_write ); 
   show_cr0(); 
   rw_enable(); 
   taddr[ __NR_write ] = new_sys_write; 
   show_cr0(); 
   rw_disable(); 
   show_cr0(); 
   return 0; 
} 

static void __exit wrchg_exit( void ) { 
   rw_enable(); 
   taddr[ __NR_write ] = old_sys_write; 
   rw_disable(); 
   LOG( "restore old sys_write syscall [%p]\n", (void*)taddr[ __NR_write ] ); 
   return; 
} 

module_init( wrchg_init ); 
module_exit( wrchg_exit ); 

Функцию поиска символа ядра find_sym(), использующую вызов API ядра kallsyms_on_each_symbol(), мы видели в предыдущей части обсуждения. Кроме того, мы делаем контроль (больше для иллюстрации) совпадение адреса имени оригинального sys_write() с этим же адресом, находящимся в позиции __NR_write таблицы sys_call_table.

Теперь мы можем исполнять систему с параллельным журналированием всего, что выводится на терминал (выбор для экспериментов write() не особо эстетично, но очень иллюстративно и, кроме того, безопасно на ранних стадиях экспериментирования в сравнении с другими системными вызовами Linux):
$ sudo insmod wrlog.ko debug=2 
$ ls 
CR0.c  find.c  Makefile  Modi.hist  wrlog.0.c  wrlog.1.c  wrlog.2.c  wrlog.3.c  wrlog.c  wrlog.hist  wrlog.ko 
$ sudo rmmod wrlog 
$ dmesg | tail -n31 
[ 1594.231242] ! set new sys_write syscall [f8854000] 
[ 1594.231248] !  ---- CR0 = 80050033 
[ 1594.231250] !  ---- CR0 = 80040033 
[ 1594.231252] !  ---- CR0 = 80050033 
[ 1594.232737] ! {0052} /home/olej/2015_WORK/own.BOOK/SysCalls/Modi/examles 
[ 1594.233368] ! {0078} \x1b[01;32molej@nvidia\x1b[01;34m ~/2015_WORK/own.BOOK/SysCalls/Modi/examles $\x1b[00m 
[ 1596.866659] ! {0001} l 
[ 1597.154675] ! {0001} s 
[ 1597.644985] ! {0110} CR0.c  find.c  Makefile  Modi.hist  wrlog.0.c  wrlog.1.c  wrlog.2.c  wrlog.3.c  wrlog.c  wrlog.hist  wrlog.ko 
[ 1597.645196] ! {0113} 
[ 1597.645196] CR0.c  find.c  Makefile  Modi.hist  wrlog.0.c  wrlog.1.c  wrlog.2.c  wrlog.3.c  wrlog.c  wrlog.hist  wrlog.ko 
[ 1597.645321] ! {0052} /home/olej/2015_WORK/own.BOOK/SysCalls/Modi/examles 
[ 1597.645951] ! {0078} \x1b[01;32molej@nvidia\x1b[01;34m ~/2015_WORK/own.BOOK/SysCalls/Modi/examles $\x1b[00m 
[ 1600.226651] ! {0001} s 
[ 1600.346587] ! {0001} u 
[ 1600.522683] ! {0001} d 
[ 1601.026667] ! {0001} o 
[ 1602.170701] ! {0001} 
[ 1602.426522] ! {0001} r 
[ 1603.218682] ! {0001} m 
[ 1603.682677] ! {0001} m 
[ 1603.906615] ! {0001} o 
[ 1604.338566] ! {0001} d 
[ 1606.442570] ! {0001} 
[ 1606.946670] ! {0001} w 
[ 1607.226667] ! {0001} r 
[ 1607.834662] ! {0001} l 
[ 1608.106672] ! {0001} o 
[ 1608.842694] ! {0001} g 
[ 1612.003059] ! {0002} 
[ 1612.014102] ! restore old sys_write syscall [c1179f70] 

Обсуждение


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

Показанный код заметно упрощён. Реальный модуль должен был бы предпринимать ряд страховочных действий для гарантий целостности. Например, новая функция-обработчик могла бы увеличить счётчик ссылок модуля вызовом try_module_get( THIS_MODULE ), чтобы предотвратить выгрузку модуля на время выполнения функции (что возможно с исчезающе малой, но всё-таки конечной вероятностью). Перед возвратом функция тогда проделает обратное действие: module_put( THIS_MODULE ). Могут понадобится и другие предосторожности, на время загрузки и выгрузки модуля, например. Но это достаточно обычная техника модулей ядра, и она не обсуждается дабы не усложнять принцип.

Некоторые дополнительные нюансы и особые случаи показанной техники мы увидим в седующей части обсуждения.

Архив кода для экспериментов можно взять здесь или здесь (из-за несущественности примеров я не размещаю их на GitHub).

P.S. Всё показанное работает в неизменном виде в 32-бит. В 64-бит архитектуре картина становится несколько сложнее за счёт необходимости эмуляции 32-битных приложений. Чтобы не усложнять картину, этот вариант сознательно не затрагивался (возможно пока, и к нему стоит вернуться позже).

Комментарии (32)


  1. d_olex
    27.09.2015 08:51

    Какой изврат, однако. Для патча sys_call_table на стоковых x86_64 ядрах я написал вот такую функцию которая ищет нужный символ по сигнатуре (пояснения в комментах к коду): https://gist.github.com/Cr4sh/fe910f0d1b0559efd43d
    Работает на всех версиях начиная с 2.6.18 или около того с любой конфигурацией и любыми настройками компилятора.


    1. Olej
      27.09.2015 09:26

      Как-раз поиска по сигнатуре и хотелось избежать. Поиск по сигнатуре многократно описан, например здесь же рядом: Встраивание в ядро Linux: перехват системных вызовов (уж лучше так). Но зачем искать по сигнатуре, которая в любой момент может поменяться, если достаточно простого kallsyms_lookup_name( «sys_call_table» ). В вашем коде сигнатура сохраняется только для X86_64 (даже для I386 всё не так), а kallsyms_lookup_name() работает на любой платформе.


      1. d_olex
        27.09.2015 09:35

        Поиск по сигнатуре многократно описан, например здесь же рядом

        Выглядит громоздко и ненадежно.

        Но зачем искать по сигнатуре, которая в любой момент может поменяться

        Вообще говоря, в ядре Linux даже публичный экспортируемый API может в любой момент поменяться. Но в целом, я согласен с тем что это дело вкуса.


    1. Olej
      27.09.2015 10:09

      Для патча sys_call_table на стоковых x86_64 ядрах я написал вот такую функцию

      На 64-бит архитектуре всё становится куда интереснее не в том, чтобы патчить sys_call_table (дело не хитрое), а в том, чтобы перехватить в своих целях все варианты выполнения системного вызова. что становится горомоздко из-за вариантов выполнения как 32-бит, так и 64-бит приложений.
      Повторю схему, которую здесь уже рядом статье показывали:
      image


      1. d_olex
        27.09.2015 10:19

        Я бы запилил для этого сверхтонкий гипервизор хендлящий sysenter, syscall и int 80h — мне кажется, что для отслеживания произвольного системного вызова этот вариант будет всяко проще и надежнее чем отпатчивание всего этого добра в образе ядра.


        1. jcmvbkbc
          27.09.2015 13:22

          Почему для перехвата системных вызовов не использовать ptrace?


          1. d_olex
            27.09.2015 13:31

            Не подходит по условиям задачи которая была сформулирована в комменте выше (http://habrahabr.ru/post/267773/#comment_8592429).


            1. jcmvbkbc
              27.09.2015 13:45

              Каким образом не подходит-то? Приложению трассирующему другое приложение через ptrace вообще не надо заморачиваться на тему того, как именно выполняется системный вызов.


              1. d_olex
                27.09.2015 13:50

                Задача изначально была о том, как перехватывать _все_ системные вызовы из контекста _всех_ user-mode процессов.


                1. jcmvbkbc
                  27.09.2015 13:52

                  Всех user-mode процессов я, честно говоря, не вижу.


                  1. d_olex
                    27.09.2015 13:56

                    Я так понял, что это подразумевается, т.к. в случае с одним конкретным процессом задача решается тривиально и обсуждать тут особо и нечего.


                1. jcmvbkbc
                  27.09.2015 14:10

                  Я вообще постановки задачи не вижу.


          1. Olej
            27.09.2015 13:41

            в юзерспэйс?


            1. jcmvbkbc
              27.09.2015 13:45

              Конечно.


              1. Olej
                27.09.2015 14:16

                Цель ведь не в том, чтобы выловить системные вызовы из одного конкретного процесса… да ещё запущенного каким-то особым образом, под трасировщиком. Цель в том, чтобы контролировать все системные вызовы от всех запускаемых процессов, а потом уже решать что с этим системным вызовом делать… или ничего не делать.


                1. jcmvbkbc
                  27.09.2015 15:25

                  да ещё запущенного каким-то особым образом, под трасировщиком

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

                  И это можно сделать с помощью ptrace, так делает, например, strace -f.


                  1. Olej
                    27.09.2015 16:13

                    В этом нет необходимости, к нужному процессу можно присоединиться после его запуска.

                    Это не важно, запущен особым образом, или к нему подключиться после запуска… речь о том, чтобы контролировать системные запросы от любого процесса (всех!), запущенного стандартным образом, командной строкой… или вообще от любого процесса, запущенного как угодно: демон, сервис… без разницы.


      1. d_olex
        27.09.2015 10:28

        … или, как вариант для систем не поддерживающих аппаратную виртуализацию, модификация IDT + IA32_SYSENTER_EIP_MSR + IA32_LSTAR_MSR. Это в любом случае будет лучше и стабильнее, т.к. патчинг ядра для подобного рода задач это полная упячка — слишком много зависимостей от недокументированных системных потрохов которые могут меняться от версии к версии.


  1. d_olex
    27.09.2015 09:08

    Олсо, в листинге номер два есть одна хоть и незначительная, но все же проблема: на системах с несколькими процессорами/ядрами нет совершенно никаких гарантий что код который сбрасывает WP-бит и код который его устанавливает обратно будут исполнены на одном и том же ядре (регистр CR0 у каждого ядра свой собственный). Так делать нельзя, короче, если вам нужно модифицировать read only страницу памяти — то для этого следует выставлять атрибуты доступа в PTE описывающей эту страницу, а не вырубать защиту памяти глобально.


    1. Olej
      27.09.2015 09:43

      в листинге номер два есть одна хоть и незначительная, но все же проблема:

      Нет такой проблемы ;-)
      Во-первых, проблема эта известна, описана и ссылка показана в тексте: WP: Safe or Not?.
      Во-вторых, проблема в SMP не возникнет из-за cli / sti, хотя можно её оградить и более сложным образом, испольуя макросы preempt_disable() и preempt_enable_no_resched(). Лучше ли это? Не знаю…
      В-третьих, когда пишется текст, хотелось бы его упростить «до нельзя», не акцентируясь на детали: описывать схему — чтобы кто-то из читающих смог ним оспользоваться, а не показывать насколько пишущий умён ;-)

      Так делать нельзя, короче, если вам нужно модифицировать read only страницу памяти — то для этого следует выставлять атрибуты доступа в PTE описывающей эту страницу

      В-четвёртых, этот способ хорошо известен, показан, например, вот здесь: Кошерный способ модификации защищённых от записи областей ядра Linux (в комментариях). Там же (в статье) предложен ещё один способ… лучший, наверное, по сравнению… и с вашим и с нашим ;-). Способов много. Какой из них лучше трудно судить.


      1. d_olex
        27.09.2015 09:52

        Хаки которые опираются на хардверные фичи архитектуры IA-32 мне нравятся больше чем хаки которые опираются на какой-либо софтовый API — меньше вероятность что в следующих версиях что-либо поломают :) Хотя тот факт что оно будет прибито гвоздями к одной конкретной платформе — это, конечно, минус.


  1. bitterman
    27.09.2015 10:01

    Полагаю, для таких целей (разработчику хочется знать, что сейчас происходит в системе) целесообразнее использовать стандартные механизмы вроде SystemTap, DTrace, Kprobes

    Хотя безусловно, полезно знать, как оно работает на низком уровне.


    1. d_olex
      27.09.2015 10:48

      SystemTap/DTrace заведется из коробки далеко не везде, нужно что бы ядро было собранно с CONFIG_DEBUG_INFO, CONFIG_KPROBES, CONFIG_RELAY, CONFIG_DEBUG_FS, CONFIG_UPROBES и возможно еще какими-то опциями. Плюс, в случае с ним разработчик ограничен возможностями тамошнего DSL который несколько беднее и менее выразителен чем связка С + inline ASM.


      1. bitterman
        27.09.2015 11:02

        Дык если речь не про руткиты, а про свою машину, то делать с ней можно что угодно.

        И если «выяснить, что происходит» — не основная задача, то специализированный язык намного быстрее доведёт до результата — полно готовых примеров на все случаи жизни. А код надо писать как серьёзный проект уже.

        Ну это всё, если не руткиты :-D


        1. d_olex
          27.09.2015 11:12

          Не всегда есть возможность пересобрать ядро из сорцов: есть embedded и программно-аппаратная проприетарщина с кастомными патчами, не все так однозначно, в общем :)


          1. bitterman
            27.09.2015 11:14

            и такое пишем. Да, там не все современные средства отладки бывают доступны, да :-)

            Сам не так давно свои боевые исходники ядра, внедрённого в эксплуатацию, потерял :-) Спасло то, что когда-то догадался поставить галочку на конфиге ядра в /proc/config.gz. Получилось всё восстановить. Ну и поскольку 3.8, все SystemTap при желании доступны. OProfile ещё когда-то пробовал, но не очень понравилось.


          1. jcmvbkbc
            27.09.2015 13:35

            Не всегда есть возможность пересобрать ядро из сорцов

            Поставщик такого ядра не предоставляющий его исходники нарушает условия лицензии ядра.


            1. qw1
              27.09.2015 13:52

              Интрересно, что лицензия не предусматривает сроки, в которые поставщик обязан предоставить исходники. Возможна ситуация — запрос исходников, ответ — ждите, делаем. И тишина.

              На практике я сталкивался с этим у компании HTC, поддержка которой на вопросы по исходникам отвечает «мониторьте портал для разработчиков, там скоро выложат». Задержка от выхода устройства на рынок до появления исходников может быть и полгода, и год.


  1. zim32
    08.10.2015 00:42

    А можно вместо глобального отключения страничной защиты найти pte страницы где лежаж векторы и поправить там флаги?


    1. Olej
      08.10.2015 00:51
      +1

      Конечно можно.
      Здесь в обсуждении и по ссылкам названным показано как минимум 4 разных способа (с фрагментами кода) которыми можно сделать запись в защищённую страницу.
      Придётся мне, наверное, написать отдельное дополнение о этих 4-х способах.

      P.S. я не знаю какой из них лучше и чем.


      1. zim32
        08.10.2015 01:03

        За материал спасибо. Просто выше верно подметили про smp и стремность такого отлкючения. Совсем недавно писал модуль для работы с mmu там много неэкспортируемых методов. Ваш материал очень кстати.


  1. Olej
    08.10.2015 01:09
    +1

    Просто выше верно подметили про smp и стремность такого отлкючения.

    Для SMP (а это общий на сегодня случай) нужно обеспечить, чтобы выполнение на критическом фрагменте не было перенесено на другой процессор.
    Пожалуй, этот вопрос заслуживает отдельного рассмотрения.