Вбудовування в ядро Linux: перехоплення системних викликів

Під терміном «системний дзвінок» програмування та обчислювальної техніки розуміється звернення з прикладної програми до ядра операційної системи (ОС) для виконання якої-небудь операції. Зважаючи на те, що така взаємодія є основним, перехоплення системних викликів представляється найважливішим етапом вбудовування, т. до. дозволяє здійснювати контроль ключового компонента ядра ОС — інтерфейс системних викликів, що, в свою чергу, дає можливість інспектувати запити прикладного ПЗ до сервісів ядра.

Дана стаття є продовженням анонсованого раніше циклу, присвяченого приватним питань реалізації накладених засобів захисту, і, зокрема, запровадження в програмні системи.



I. Підходи до вбудовуванню



Існують різні способи, що дозволяють здійснити перехоплення системних викликів ядра ОС Linux. Насамперед, варто зазначити, що для перехоплення одиничних системних викликів може бути використаний розглянутий раніше спосіб перехоплення функцій ядра. Дійсно, в силу того, що більшість системних викликів представлені відповідними функціями (наприклад, sys_open), завдання їх перехоплення рівнозначна завдання перехоплення цих функцій. Однак з зростанням числа перехоплених системних викликів і ускладненням «бізнес-логіки» такий підхід може виявитися обмеженим.

Більш універсальним є спосіб модифікації записів в таблицях системних викликів (докладніше про таблицях буде розказано далі), що містять покажчики на функції, які реалізують логіку того чи іншого системного виклику. Таблиці використовуються ядром при диспетчеризації, коли за номером запитуваної додатком системного виклику з відповідної таблиці вибирається вказівник на функцію-обробник з подальшим її виконанням. Заміна такого покажчика дозволить змінити логіку роботи ядра у частині обробки системних викликів. Забігаючи вперед, варто відзначити, що для успішної реалізації цього методу самі таблиці необхідно буде відшукати, оскільки вони не експортуються. У кінцевому рахунку перехопленні системного виклику буде полягати в простому перевизначенні елемента таблиці.

Найбільш універсальним із способів перехоплення системних викликів була і залишається модифікація коду диспетчера системних викликів так, щоб забезпечувалася пре — і постобробка контексту потоку, запитувача який-небудь системний сервіс. Такий варіант дає більшу гнучкість у порівнянні з попередніми, оскільки вводить єдині точки контролю стану до і після функції-обробника.

Далі на приклад буде детально розглянуто, яким чином здійснити вбудовування в інтерфейс системних викликів ядра ОС Linux, модифікуючи код диспетчерів.

II. Диспетчеризація системних викликів в ядрі Linux



Диспетчеризація системних викликів є досить складним процесом з безліччю нюансів, проте в рамках даної статті багато деталі будуть опущені, т. к., за винятком власне самого процесу диспетчеризації (вибірки і виконання відповідної системного виклику функції), для здійснення вбудовування знати більше нічого не потрібно.

Традиційно ядро Linux підтримує наступні можливості здійснення системних викликів для архітектури x86:

  • інструкція INT 80h (32-бітний інтерфейс, нативний виклик або емуляція);
  • інструкція SYSENTER (32-бітний інтерфейс, нативний виклик або емуляція);
  • інструкція SYSCALL (64-биный інтерфейс, нативний виклик або емуляція).


Нижче представлена запозичена мною прекрасна ілюстрація здійснення системного виклику в залежності від використовуваного варіанти:

image

Як видно, 32-бітні програми здійснюють системні виклики, використовуючи механізми INT 80h і SYSENTER, тоді як 64-бітові — використовуючи SYSCALL. При цьому існує підтримка можливості виконання 32-бітного коду в 64-бітному оточенні (т. зв. compatibility mode — режим емуляції/сумісності; опція ядра
CONFIG_IA32_EMULATION
). У зв'язку з цим в ядрі існують 2 неекспортіруемие таблиці
sys_call_table
та
ia32_sys_call_table
(доступна тільки для режиму емуляції), що містять адреси функцій — обробників системних викликів.

У загальному випадку, коли в 64-бітному ядрі представлені всі можливі механізми, існує 4 точки входу, визначають, якою буде логіка роботи відповідного диспетчера:



Так чи інакше, при здійсненні додатком системного виклику ядро отримує управління. Диспетчер системних викликів для кожного з розглянутих випадків має відмінності від інших, проте без втрати спільності їх загальна структура може бути розглянута на прикладі 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.