Под термином «системный вызов» в программировании и вычислительной технике понимается обращение прикладной программы к ядру операционной системы (ОС) для выполнения какой-либо операции. Ввиду того что такое взаимодействие является основным, перехват системных вызовов представляется важнейшим этапом встраивания, т.к. позволяет осуществлять контроль ключевого компонента ядра ОС — интерфейса системных вызовов, что, в свою очередь, даёт возможность инспектировать запросы прикладного ПО к сервисам ядра.
Данная статья является продолжением анонсированного ранее цикла, посвящённого частным вопросам реализации наложенных средств защиты, и, в частности, встраиванию в программные системы.
I. Подходы к встраиванию
Существуют различные способы, позволяющие осуществить перехват системных вызовов ядра ОС Linux. Прежде всего, стоит отметить, что для перехвата единичных системных вызовов может быть использован рассмотренный ранее способ перехвата функций ядра. Действительно, в силу того что большинство системных вызовов представлены соответствующими функциями (например, sys_open), задача их перехвата равнозначна задаче перехвата этих функций. Однако с ростом числа перехватываемых системных вызовов и усложнением «бизнес-логики» такой подход может оказаться ограниченным.
Более универсальным является способ модификации записей в таблицах системных вызовов (подробнее о таблицах будет рассказано далее), содержащих указатели на функции, реализующие логику того или иного системного вызова. Таблицы используются ядром при диспетчеризации, когда по номеру запрашиваемого приложением системного вызова из соответствующей таблицы выбирается указатель на функцию-обработчик с последующим её исполнением. Замена такого указателя позволит изменить логику работы ядра в части обработки системных вызовов. Забегая вперёд, стоит отметить, что для успешной реализации данного метода сами таблицы необходимо будет как-то отыскать, т.к. они не экспортируются. В конечном счёте перехват системного вызова будет состоять в простом переопределении элемента таблицы.
Самым же универсальным из способов перехвата системных вызовов была и остаётся модификация кода диспетчера системных вызовов так, чтобы обеспечивалась пре- и постобработка контекста потока, запрашивающего какой-либо системный сервис. Такой вариант даёт большую гибкость в сравнении с предыдущими, т.к. вводит единые точки контроля состояния до и после функции-обработчика.
Далее на примере будет детально рассмотрено, каким образом осуществить встраивание в интерфейс системных вызовов ядра ОС Linux, модифицируя код диспетчеров.
II. Диспетчеризация системных вызовов в ядре Linux
Диспетчеризация системных вызовов является довольно сложным процессом со множеством нюансов, однако в рамках данной статьи многие детали будут опущены, т.к., за исключением собственно самого процесса диспетчеризации (выборки и исполнения соответствующей системному вызову функции), для осуществления встраивания знать более ничего не нужно.
Традиционно ядро Linux поддерживает следующие возможности осуществления системных вызовов для архитектуры x86:
- инструкция INT 80h (32-битный интерфейс, нативный вызов или эмуляция);
- инструкция SYSENTER (32-битный интерфейс, нативный вызов или эмуляция);
- инструкция SYSCALL (64-биный интерфейс, нативный вызов или эмуляция).
Ниже представлена заимствованная мной прекрасная иллюстрация осуществления системного вызова в зависимости от используемого варианта:
Как видно, 32-битные приложения осуществляют системные вызовы, используя механизмы INT 80h и SYSENTER, тогда как 64-битные — используя SYSCALL. При этом существует поддержка возможности выполнения 32-битного кода в 64-битном окружении (т.н. compatibility mode — режим эмуляции/совместимости; опция ядра CONFIG_IA32_EMULATION
). В связи с этим в ядре существуют 2 неэкспортируемые таблицы — sys_call_table
и ia32_sys_call_table
(доступна только для режима эмуляции), содержащие адреса функций — обработчиков системных вызовов.
В общем случае, когда в 64-битном ядре представлены все возможные механизмы, существует 4 точки входа, определяющих, какой будет логика работы соответствующего диспетчера:
- эмуляция INT 80h, ia32_syscall (x86/ia32/ia32entry.S);
- эмуляция SYSENTER, ia32_sysenter_target (x86/ia32/ia32entry.S);
- эмуляция SYSCALL32, ia32_cstar_target (x86/ia32/ia32entry.S);
- SYSCALL, system_call (x86/kernel/entry_64.S).
Так или иначе, при осуществлении приложением системного вызова ядро получает управление. Диспетчер системных вызовов для каждого из рассмотренных случаев имеет отличия от других, однако без потери общности их общая структура может быть рассмотрена на примере system_call:
0xffffffff81731670 <+0>: swapgs
0xffffffff81731673 <+3>: mov %rsp,%gs:0xc000
0xffffffff8173167c <+12>: mov %gs:0xc830,%rsp
0xffffffff81731685 <+21>: sti
0xffffffff81731686 <+22>: data32 data32 xchg %ax,%ax
0xffffffff8173168a <+26>: data32 xchg %ax,%ax
0xffffffff8173168d <+29>: sub $0x50,%rsp
0xffffffff81731691 <+33>: mov %rdi,0x40(%rsp)
0xffffffff81731696 <+38>: mov %rsi,0x38(%rsp)
0xffffffff8173169b <+43>: mov %rdx,0x30(%rsp)
0xffffffff817316a0 <+48>: mov %rax,0x20(%rsp)
0xffffffff817316a5 <+53>: mov %r8,0x18(%rsp)
0xffffffff817316aa <+58>: mov %r9,0x10(%rsp)
0xffffffff817316af <+63>: mov %r10,0x8(%rsp)
0xffffffff817316b4 <+68>: mov %r11,(%rsp)
0xffffffff817316b8 <+72>: mov %rax,0x48(%rsp)
0xffffffff817316bd <+77>: mov %rcx,0x50(%rsp)
0xffffffff817316c2 <+82>: testl $0x100801d1,-0x1f78(%rsp)
0xffffffff817316cd <+93>: jne 0xffffffff8173181e <tracesys>
0xffffffff817316d3 <+0>: and $0xbfffffff,%eax
0xffffffff817316d8 <+5>: cmp $0x220,%eax /* <-------- cmp $__NR_syscall_max,%eax */
0xffffffff817316dd <+10>: ja 0xffffffff817317a5 <badsys>
0xffffffff817316e3 <+16>: mov %r10,%rcx
0xffffffff817316e6 <+19>: callq *-0x7e7fec00(,%rax,8) /* <-------- call *sys_call_table(,%rax,8) */
0xffffffff817316ed <+26>: mov %rax,0x20(%rsp)
0xffffffff817316f2 <+0>: mov $0x1008feff,%edi
0xffffffff817316f7 <+0>: cli
0xffffffff817316f8 <+1>: data32 data32 xchg %ax,%ax
0xffffffff817316fc <+5>: data32 xchg %ax,%ax
0xffffffff817316ff <+8>: mov -0x1f78(%rsp),%edx
0xffffffff81731706 <+15>: and %edi,%edx
0xffffffff81731708 <+17>: jne 0xffffffff81731745 <sysret_careful>
0xffffffff8173170a <+19>: mov 0x50(%rsp),%rcx
0xffffffff8173170f <+24>: mov (%rsp),%r11
0xffffffff81731713 <+28>: mov 0x8(%rsp),%r10
0xffffffff81731718 <+33>: mov 0x10(%rsp),%r9
0xffffffff8173171d <+38>: mov 0x18(%rsp),%r8
0xffffffff81731722 <+43>: mov 0x20(%rsp),%rax
0xffffffff81731727 <+48>: mov 0x30(%rsp),%rdx
0xffffffff8173172c <+53>: mov 0x38(%rsp),%rsi
0xffffffff81731731 <+58>: mov 0x40(%rsp),%rdi
0xffffffff81731736 <+63>: mov %gs:0xc000,%rsp
0xffffffff8173173f <+72>: swapgs
0xffffffff81731742 <+75>: sysretq
Как видно, первой инструкцией (swapgs
) осуществляется переключение структур данных (с пользовательской на ядерную). Далее настраивается стек, разрешаются прерывания, а также на стеке формируется регистровый контекст потока (структура pt_regs
), необходимый в процессе обработки. Возвращаясь к представленному выше листингу, особое внимание следует уделить следующим командам:
0xffffffff817316d8 <+5>: cmp $0x220,%eax /* <-------- cmp $__NR_syscall_max,%eax */
0xffffffff817316dd <+10>: ja 0xffffffff817317a5 <badsys>
0xffffffff817316e3 <+16>: mov %r10,%rcx
0xffffffff817316e6 <+19>: callq *-0x7e7fec00(,%rax,8) /* <-------- call *sys_call_table(,%rax,8) */
0xffffffff817316ed <+26>: mov %rax,0x20(%rsp)
В первой строке осуществляется проверка соответствия номера запрашиваемого системного вызова (регистр %rax
), максимально допустимому значению (__NR_syscall_max
). В том случае, если проверка завершилась успехом, будет произведена диспетчеризация системного вызова, а именно — управление перейдёт к функции, реализующей соответствующую логику.
Таким образом, ключевой точкой процесса обработки системных вызовов является команда диспетчеризации, представляющая собой вызов функции (call *sys_call_table(,%rax,8)
). Дальнейшее встраивание будем осуществлять путём модификации этой команды.
III. Методика осуществления встраивания
Как было отмечено, универсальным способом встраивания в диспетчер будет являться модификация его кода таким образом, чтобы обеспечить возможность контроля контекста потока до выполнения им функции реализации логики системного вызова (преобработка), а также после её выполнения (постобработка).
В целях реализации встраивания описываемым способом предлагается слегка пропатчить диспетчер, модифицировав команду диспетчеризации (call *sys_call_table(,%rax,8)
), и записать поверх неё команду безусловного перехода (JMP REL32
) на обработчик service_stub
. При этом общая структура такого обработчика будет выглядеть следующим образом (далее псевдокод):
system_call:
swapgs
..
jmp service_stub /* <-------- ТУТ БЫЛ call *sys_call_table(,%rax,8) */
mov %rax,0x20(%rsp) /* <-------- СЮДА ОСУЩЕСТВЛЯЕТСЯ ВОЗВРАТ ИЗ service_stub */
...
swapgs
sysretq
service_stub:
...
call ServiceTraceEnter /* void ServiceTraceEnter(struct pt_regs *) */
...
call sys_call_table[N](args)
...
call ServiceTraceLeave(regs) /* void ServiceTraceLeave(struct pt_regs *) */
...
jmp back
Здесь ServiceTraceEnter()
и ServiceTraceLeave()
— функции пре- и пост- обработки соответственно. Их параметрами является указатель на pt_regs
— регистровую структуру, представляющую контекст потока. Завершающей инструкцией является команда передачи управления коду диспетчера системного вызова, откуда ранее был сделан вызов данного обработчика.
Ниже представлен код обработчика service_syscall64, используемый в качестве примера для осуществления перехвата system_call
(инструкция SYSCALL):
.global service_syscall64
service_syscall64:
SAVE_REST
movq %rsp, %rdi
call ServiceTraceEnter
RESTORE_REST
LOAD_ARGS 0
movq %r10, %rcx
movq ORIG_RAX - ARGOFFSET(%rsp), %rax
call *0x00000000(,%rax,8) // origin call
movq %rax, RAX - ARGOFFSET(%rsp)
SAVE_REST
movq %rsp, %rdi
call ServiceTraceLeave
RESTORE_REST
movq RAX - ARGOFFSET(%rsp), %rax
jmp 0x00000000
Как видно, он имеет рассматриваемую выше структуру. Точные значения указателей и смещений настраиваются в процессе загрузки модуля (об этом будет рассказано далее). Кроме того, в приведённом фрагменте присутствуют дополнительные элементы (SAVE_REST
, RESTORE_REST
, LOAD_ARGS
), назначение которых в основном заключается в формировании контекста потока (pt_regs
) перед вызовом функций ServiceTraceEnter
и ServiceTraceLeave
.
IV. Особенности осуществления встраивания
Осуществление встраивания в механизмы диспетчеризации системных вызовов ядра ОС Linux так или иначе предполагает необходимость решения следующих практических задач:
- определение адресов диспетчеров системных вызовов;
- определение адресов таблиц диспетчеризации системных вызовов;
- модификация кода диспетчеров;
- настройка обработчиков;
- выгрузка модуля.
Определение адресов диспетчеров системных вызовов
Наличие в системе нескольких диспетчеров предполагает необходимость определения их адресов. Выше было отмечено, что каждый диспетчер соответствует своему «способу» осуществления запроса на системный вызов. Поэтому для определения требуемых адресов будут использоваться соответствующие механизмы:
- INT 80h, чтение вектора таблицы IDT (подробнее);
- SYSENTER, чтение содержимого регистра MSR с номером MSR_IA32_SYSENTER_EIP (подробнее);
- SYSCALL32, чтение содержимого регистра MSR с номером MSR_CSTAR (подробнее);
- SYSCALL, чтение содержимого регистра MSR с номером MSR_LSTAR (подробнее).
Таким образом, легко определяется каждый из искомых адресов.
Определение адресов таблиц диспетчеризации системных вызовов
Как было отмечено выше, таблицы sys_call_table
и ia32_sys_call_table
не экспортируются. Существуют разные способы определения их адресов, однако определив адреса диспетчеров на предыдущем шаге, адреса таблиц определяются также просто — путём поиска инструкции диспетчеризации, имеющей вид call sys_call_table[N]
.
Для этих целей рационально использовать дизассемблер (udis86). Путём последовательного перебора инструкций, начиная с самой первой, можно дойти до искомой команды, аргументом которой будет адрес соответствующей таблицы. В связи с тем, что структура диспетчеров является устоявшейся, можно однозначно определить признаки искомой команды (CALL с длиной 7 байтов) и с высокой степенью надёжности получить из неё требуемое значение адреса таблицы.
В случае если по каким-то причинам этого не достаточно, можно усилить проверку полученного адреса. Для этого, например, можно проверить, является ли значение в ячейке с номером __NR_open
предполагаемой таблицы равным адресу функции sys_open. Однако, в рассматриваемом примере таких дополнительных проверок не производится.
Модификация кода диспетчеров
При осуществлении модификации кода диспетчеров системных вызовов нужно учитывать то, что их код является доступным только для чтения (ReadOnly). Кроме того, модификация кода на рабочей системе должна осуществляться атомарно, т.е. таким образом, чтобы в процессе модификации не было неопределённых состояний, когда какой-либо из потоков видит частично завершённую запись.
В одной из предыдущих статей рассмотрен корректный способ осуществления записи в защищённые от записи страницы, используя создание временных отображений. Нет необходимости здесь что-то повторять. Что касается атомарности, то этот вопрос также освещался ранее, когда рассматривалась тема перехвата функций ядра.
Таким образом, модификацию защищённого от записи кода целесообразно осуществлять с использованием временных отображений, а также специального интерфейса ядра ОС Linux — stop_machine
.
Настройка обработчиков
В соответствии с представленным методом встраивания код каждого из диспетчеров модифицируется таким образом, что 7-байтовая команда диспетчеризации CALL MEM32
заменяется на 5-байтовую команду безусловного перехода на соответствующий обработчик JMP REL32
. Вследствие этого накладываются определённые ограничения на дальность перехода. Обработчик должен быть расположен не далее, чем ± 2 Гб от места расположения команды JMP REL32
.
В соответствии со структурой обработчиков в них присутствуют команды (JMP и CALL), требующие указания точных аргументов (например, адреса возврата или адреса таблицы системных вызовов). Ввиду того что такие значения не доступны на этапе компиляции или загрузки модуля, они должны быть проставлены «вручную», после загрузки, до начала работы.
Ещё одной важной особенностью при настройке обработчиков является необходимость обеспечения возможности выгрузки модуля с сохранением работоспособности системы. Для этих целей код обработчиков должен оставаться в системе даже после выгрузки основного модуля (об этом далее).
Выгрузка модуля
Выгрузка модуля должна осуществляться с сохранением работоспособности системы. Это значит, что, после того как модуль был выгружен, система должна функционировать в обычном режиме. Данная задача не является тривиальной по причине того, что с выгрузкой модуля выгружается весь используемый в нём код.
Например, можно представить ситуацию, что некоторый поток, осуществляющий системный вызов, «уснул» в ядре. До момента, когда он проснётся, кто-то пытается выгрузить модуль. В принципе, ничто не мешает сделать это системе. В итоге, когда рассматриваемый поток проснётся и завершит запрашиваемый системный вызов, управление вернётся на соответствующий обработчик (именно поэтому он не должен выгружаться).
Однако невыгрузка кода обработчиков это не единственное условие сохранения работоспособности системы после выгрузки модуля. Стоит напомнить, что реальный системный вызов в обработчике был «обёрнут» в пару вызовов функций трассировки ServiceTraceEnter и ServiceTraceLeave, код которых располагался в выгружаемом модуле.
Поэтому для того чтобы не попасть в ситуацию, когда по возвращении из системного вызова поток попытался бы вызвать функцию, которой физически уже нет, необходимо повторно модифицировать код каждого обработчика, исключив оттуда недействительные более вызовы (проще говоря, забив их NOP'ами).
V. Особенности реализации модуля ядра
Далее рассмотрена структура модуля ядра, осуществляющего встраивание в механизм диспетчеризации системных вызовов ядра ОС Linux.
Ключевой структурой модуля является struct scentry — структура, содержащая необходимую для встраивания в соответствующий диспетчер информацию. В структуре представлены следующие поля:
typedef struct scentry {
const char *name;
const void *entry;
const void *table;
const void *pcall;
void *pcall_map;
void *stub;
const void *handler;
void (*prepare)(struct scentry *);
void (*implant)(struct scentry *);
void (*restore)(struct scentry *);
void (*cleanup)(struct scentry *);
} scentry_t;
Структуры объединены в массив, определяющий, как и с какими параметрами осуществлять встраивание:
scentry_t elist[] = {
...
{
.name = "system_call", /* SYSCALL: MSR(LSTAR), kernel/entry_64.S (1) */
.handler = service_syscall64,
.prepare = prepare_syscall64_1
},
{
.name = "system_call", /* SYSCALL: MSR(LSTAR), kernel/entry_64.S (2) */
.handler = service_syscall64,
.prepare = prepare_syscall64_2
},
...
};
За исключением обозначенных полей заполнение остальных элементов структуры происходит автоматически — за это отвечает функция prepare
. Ниже представлен пример реализации функции для подготовки к встраиванию в диспетчер команды SYSCALL:
extern void service_syscall64(void);
static void prepare_syscall64_1(scentry_t *se)
{
/*
* searching for -- 'call *sys_call_table(,%rax,8)'
* http://lxr.free-electrons.com/source/arch/x86/kernel/entry_64.S?v=3.13#L629
*/
se->entry = get_symbol_address(se->name);
se->entry = se->entry ? se->entry : to_ptr(x86_get_msr(MSR_LSTAR));
if (!se->entry) return;
se->pcall = ud_find_insn(se->entry, 512, UD_Icall, 7);
if (!se->pcall) return;
se->table = to_ptr(*(int *)(se->pcall + 3));
}
Как видно, прежде всего осуществляется попытка разрешения имени символа в соответствующий ему адрес (se->entry
). Если определить адрес таким способом не удаётся, вступают в дело специфичные для каждого диспетчера механизмы (в данном случае, чтение регистра MSR с номером MSR_LSTAR).
Далее для найденного диспетчера осуществляется поиск команды диспетчеризации (se->pcall
) и, в случае успеха, происходит определение адреса используемой диспетчером таблицы системных вызовов.
Завершением фазы подготовки является создание кода обработчика, используемого диспетчером после его модификации. Ниже представлена функция stub_fixup, которая это делает:
static void fixup_stub(scentry_t *se)
{
ud_t ud;
memset(se->stub, 0x90, STUB_SIZE);
ud_initialize(&ud, BITS_PER_LONG,
UD_VENDOR_ANY, se->handler, STUB_SIZE);
while (ud_disassemble(&ud)) {
void *insn = se->stub + ud_insn_off(&ud);
const void *orig_insn = se->handler + ud_insn_off(&ud);
memcpy(insn, orig_insn, ud_insn_len(&ud));
/* fixup sys_call_table dispatcher calls (FF.14.x5.xx.xx.xx.xx) */
if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 7) {
x86_insert_call(insn, NULL, se->table, 7);
continue;
}
/* fixup ServiceTraceEnter/Leave calls (E8.xx.xx.xx.xx) */
if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 5) {
x86_insert_call(insn, insn, orig_insn + (long)(*(int *)(orig_insn + 1)) + 5, 5);
continue;
}
/* fixup jump back (E9.xx.xx.xx.xx) */
if (ud.mnemonic == UD_Ijmp && ud_insn_len(&ud) == 5) {
x86_insert_jmp(insn, insn, se->pcall + 7);
break;
}
}
se->pcall_map = map_writable(se->pcall, 64);
}
Как видно, основная роль этой функции — создание копии обработчиков с последующей их настройкой на актуальные адреса. Здесь также активно используется дизассемблер. Простая структура обработчиков позволяет обходиться здесь без какой-либо сложной логики. Сигналом к выходу из цикла является обнаружение команды JMP REL32
, возвращающей управление диспетчеру.
За фазой подготовки следует фаза имплантации кода в код ядра. Эта фаза достаточно проста и заключается в записи одной единственной инструкции (JMP REL32
) в код каждого из системных сервисов.
При выгрузке модуля сперва отрабатывает фаза restore, заключающаяся в восстановлении кода диспетчеров системных вызовов, а также модификации кода обработчиков:
static void generic_restore(scentry_t *se)
{
ud_t ud;
if (!se->pcall_map) return;
ud_initialize(&ud, BITS_PER_LONG,
UD_VENDOR_ANY, se->stub, STUB_SIZE);
while (ud_disassemble(&ud)) {
if (ud.mnemonic == UD_Icall && ud_insn_len(&ud) == 5) {
memset(se->stub + ud_insn_off(&ud), 0x90, ud_insn_len(&ud));
continue;
}
if (ud.mnemonic == UD_Ijmp)
break;
}
debug(" [o] restoring original call instruction %p (%s)n", se->pcall, se->name);
x86_insert_call(se->pcall_map, NULL, se->table, 7);
}
Как видно, в коде обработчиков все найденные 5-байтные команды CALL будут заменены на последовательность NOP'ов, что исключит попытки выполнения несуществующего кода по возвращению из системного вызова. Речь об этом шла ранее.
Соответствующие фазам implant и restore функции выполняются в контексте stop_machine
, поэтому все используемые отображения должны быть подготовлены заранее.
Завершающей фазой при выгрузке является фаза clenup, где освобождаются внутренние ресурсы (pcall_map
).
Стоит ещё раз отметить, что после выгрузки модуля в памяти ядра навсегда остаётся область, содержащая код обработчиков. Как было отмечено ранее, это является необходимым условием корректной работы системы после выгрузки модуля.
Таким образом, на примере разобраны основные принципы встраивания в механизм системных вызовов ядра, а также проиллюстрирована возможность осуществления их перехвата.
VI. Тестирование и отладка
Для целей тестирования осуществим перехват системного вызова open(2)
. Ниже представлена функция trace_syscall_entry, реализующая данный перехват с использованием обработчика ServiceTraceEnter:
static void trace_syscall_entry(int arch, unsigned long major,
unsigned long a0, unsigned long a1, unsigned long a2, unsigned long a3)
{
char *filename = NULL;
if (major == __NR_open || major == __NR_ia32_open) {
filename = kmalloc(PATH_MAX, GFP_KERNEL);
if (!filename || strncpy_from_user(filename, (const void __user *)a0, PATH_MAX) < 0)
goto out;
printk("%s open(%s) [%s]n", arch ? "X86_64" : "I386", filename, current->comm);
}
out:
if (filename) kfree(filename);
}
void ServiceTraceEnter(struct pt_regs *regs)
{
if (IS_IA32)
trace_syscall_entry(0, regs->orig_ax,
regs->bx, regs->cx, regs->dx, regs->si);
#ifdef CONFIG_X86_64
else
trace_syscall_entry(1, regs->orig_ax,
regs->di, regs->si, regs->dx, regs->r10);
#endif
}
Сборка и загрузка модуля осуществляется стандартными средствами:
$ git clone https://github.com/milabs/kmod_hooking_sct
$ cd kmod_hooking_sct
$ make
$ sudo insmod scthook.ko
В результате в журнале ядра (команда dmesg
) должна появиться следующая информация:
[ 5217.779766] [scthook] # SYSCALL hooking module
[ 5217.780132] [scthook] # prepare
[ 5217.785853] [scthook] [o] prepared stub ffffffffa000c000 (ia32_syscall)
[ 5217.785856] [scthook] entry:ffffffff81731e30 pcall:ffffffff81731e92 table:ffffffff81809cc0
[ 5217.790482] [scthook] [o] prepared stub ffffffffa000c200 (ia32_sysenter_target)
[ 5217.790484] [scthook] entry:ffffffff817319a0 pcall:ffffffff81731a36 table:ffffffff81809cc0
[ 5217.794931] [scthook] [o] prepared stub ffffffffa000c400 (ia32_cstar_target)
[ 5217.794933] [scthook] entry:ffffffff81731be0 pcall:ffffffff81731c75 table:ffffffff81809cc0
[ 5217.797517] [scthook] [o] prepared stub ffffffffa000c600 (system_call)
[ 5217.797518] [scthook] entry:ffffffff8172fcb0 pcall:ffffffff8172fd26 table:ffffffff81801400
[ 5217.800013] [scthook] [o] prepared stub ffffffffa000c800 (system_call)
[ 5217.800014] [scthook] entry:ffffffff8172fcb0 pcall:ffffffff8172ff38 table:ffffffff81801400
[ 5217.800014] [scthook] # prepare OK
[ 5217.800015] [scthook] # implant
[ 5217.800052] [scthook] [o] implanting jump to stub handler ffffffffa000c000 (ia32_syscall)
[ 5217.800054] [scthook] [o] implanting jump to stub handler ffffffffa000c200 (ia32_sysenter_target)
[ 5217.800054] [scthook] [o] implanting jump to stub handler ffffffffa000c400 (ia32_cstar_target)
[ 5217.800055] [scthook] [o] implanting jump to stub handler ffffffffa000c600 (system_call)
[ 5217.800056] [scthook] [o] implanting jump to stub handler ffffffffa000c800 (system_call)
[ 5217.800058] [scthook] # implant OK
Корректная отработка перехвата open(2)
будет приводить к появлению в том же журнале сообщений типа:
[ 5370.999929] X86_64 open(/usr/share/locale-langpack/en_US.utf8/LC_MESSAGES/libc.mo) [perl]
[ 5370.999930] X86_64 open(/usr/share/locale-langpack/en_US/LC_MESSAGES/libc.mo) [perl]
[ 5370.999932] X86_64 open(/usr/share/locale-langpack/en.UTF-8/LC_MESSAGES/libc.mo) [perl]
[ 5370.999934] X86_64 open(/usr/share/locale-langpack/en.utf8/LC_MESSAGES/libc.mo) [perl]
[ 5370.999936] X86_64 open(/usr/share/locale-langpack/en/LC_MESSAGES/libc.mo) [perl]
[ 5371.001308] X86_64 open(/etc/login.defs) [cron]
[ 5372.422399] X86_64 open(/home/ilya/.cache/awesome/history) [awesome]
[ 5372.424013] X86_64 open(/dev/null) [awesome]
[ 5372.424682] I386 open(/etc/ld.so.cache) [skype]
[ 5372.424714] I386 open(/usr/lib/i386-linux-gnu/libXv.so.1) [skype]
[ 5372.424753] I386 open(/usr/lib/i386-linux-gnu/libXss.so.1) [skype]
[ 5372.424789] I386 open(/lib/i386-linux-gnu/librt.so.1) [skype]
[ 5372.424827] I386 open(/lib/i386-linux-gnu/libdl.so.2) [skype]
[ 5372.424856] I386 open(/usr/lib/i386-linux-gnu/libX11.so.6) [skype]
[ 5372.424896] I386 open(/usr/lib/i386-linux-gnu/libXext.so.6) [skype]
[ 5372.424929] I386 open(/usr/lib/i386-linux-gnu/libQtDBus.so.4) [skype]
[ 5372.424961] I386 open(/usr/lib/i386-linux-gnu/libQtWebKit.so.4) [skype]
[ 5372.425003] I386 open(/usr/lib/i386-linux-gnu/libQtXml.so.4) [skype]
[ 5372.425035] I386 open(/usr/lib/i386-linux-gnu/libQtGui.so.4) [skype]
[ 5372.425072] I386 open(/usr/lib/i386-linux-gnu/libQtNetwork.so.4) [skype]
[ 5372.425103] I386 open(/usr/lib/i386-linux-gnu/libQtCore.so.4) [skype]
[ 5372.425151] I386 open(/lib/i386-linux-gnu/libpthread.so.0) [skype]
[ 5372.425191] I386 open(/usr/lib/i386-linux-gnu/libstdc++.so.6) [skype]
[ 5372.425233] I386 open(/lib/i386-linux-gnu/libm.so.6) [skype]
[ 5372.425265] I386 open(/lib/i386-linux-gnu/libgcc_s.so.1) [skype]
[ 5372.425292] I386 open(/lib/i386-linux-gnu/libc.so.6) [skype]
[ 5372.425338] I386 open(/usr/lib/i386-linux-gnu/libxcb.so.1) [skype]
[ 5372.425380] I386 open(/lib/i386-linux-gnu/libdbus-1.so.3) [skype]
[ 5372.425416] I386 open(/lib/i386-linux-gnu/libz.so.1) [skype]
[ 5372.425444] I386 open(/usr/lib/i386-linux-gnu/libXrender.so.1) [skype]
[ 5372.425475] I386 open(/usr/lib/i386-linux-gnu/libjpeg.so.8) [skype]
[ 5372.425510] I386 open(/lib/i386-linux-gnu/libpng12.so.0) [skype]
[ 5372.425546] I386 open(/usr/lib/i386-linux-gnu/libxslt.so.1) [skype]
[ 5372.425579] I386 open(/usr/lib/i386-linux-gnu/libxml2.so.2) [skype]
Причём стоит отметить, что работа перехвата для 32-битных приложений (например, Skype) так же осуществляется корректно, подтверждением чему является наличие сообщений, начинающихся с I386, а не с X86_64. Таким образом, на примере open(2)
проиллюстрирована возможность осуществления перехвата системных вызовов.
VII. Заключение
Представленный в статье метод встраивания в механизмы диспетчеризации системных вызовов ядра ОС Linux позволяет решать задачи перехвата не только конкретных системных вызовов, но механизма диспетчеризации в целом. Предложенный подход к реализации загрузки и выгрузки модуля позволяет корректно встраиваться в систему, а также даёт возможность, что немаловажно, обеспечить работоспособность системы после выгрузки. Активное использование дизассемблера позволяет надёжно решать задачи поиска скрытых и неэкспортируемых символов.
Традиционно код модуля ядра, реализующий необходимые для перехвата функций действия, доступен на github.
Автор: milabs