Дослідження можливостей нестандартного використання модулів ядра ОС Linux

Неекспортовані символи ядра. Оптимальний підхід до реалізації пошуку символів у ядрі. Виконання, підміна, додавання та приховання системних викликів. Завантаження модуля ядра із програмного коду та з коду іншого модуля. Робота з UNIX-сигналами.

Рубрика Программирование, компьютеры и кибернетика
Вид курсовая работа
Язык украинский
Дата добавления 23.05.2013
Размер файла 84,0 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

Размещено на http://www.allbest.ru/

Міністерство освіти і науки, молоді та спорту України

Тернопільський національний технічний університет імені Івана Пулюя

Комп'ютерні системи та мережі

КУРСОВИЙ ПРОЕКТ

З Системного програмного забезпечення

на тему:

Дослідження можливостей нестандартного використання модулів ядра ОС Linux

Студента (ки) 4 курсу, групи CI-42

напряму підготовки 6.050102

“Комп'ютерна інженерія”

Спеціальності 7.05010201

“Комп'ютерні системи та мережі”

Керівник: Луцків А.М

м. Тернопіль - 2013

Міністерство освіти і науки, молоді та спорту України

Тернопільський національний технічний університет імені Івана Пулюя

Кафедра Комп'ютерні системи та мережі

Дисципліна Системне програмне забезпечення

Напрям підготовки 6.050102 “Комп'ютерна інженерія”

Курс 4 Група CI-42 Семестр 8

ЗАВДАННЯ

на курсову роботу

Студентові

Никорчуку Володимиру Романовичу

1. Тема роботи

Дослідження можливостей нестандартного використання модулів ядра ОС Linux

2. Термін здачі студентом закінченої роботи 16.05.2013

3. Вихідні дані до роботи

Згідно стандартам 4.4BSD і POSIX.

4. Зміст розрахунково-пояснювальної записки (перелік питань, які підлягають розробці)

1.1 Робота з UNIX-сигналами 2.1 Пошук символів

2.2 Неекспортовані символи ядра

2.3 Оптимальний підхід до реалізації пошуку символів у ядрі

3.1 Виконання системних викликів 3.2 Підміна системного виклику

3.3 Додавання системного виклику 3.4 Приховання системного виклику

4.1 Завантаження модуля ядра із програмного коду

4.2 Завантаження модуля ядра з коду іншого модуля

5. Перелік графічного (ілюстративного) матеріалу, якщо передбачено

6. Дата видачі завдання

5.03.2013 р.

КАЛЕНДАРНИЙ ПЛАН

п/п

Назва етапів курсової роботи

Термін виконання етапів роботи

Примітка

1

Отримання завдання на КР

1.03.2013 - 5.03.213

2

Ознайомлення з вихідними даними

6.03.2013 - 8.03.2013

3

Огляд літературних джерел

9.03.2013 - 15.03.213

4

Опис теоретичної частини

16.03.2013 - 30.03.2013

5

Реалізація практичної частини

1.04.2013 - 18.04.2013

6

Оформлення коду програм

19.04.2013 - 23.04.2013

7

Оформлення ПЗ КР

24.04.2013 - 5.05.2013

8

Попередній захист КР

6.05.2013 - 10.05.2013

9

Захист КР

11.05.2013 - 16.02.2013

Студент

Керівник роботи Лукців А.М

ЗМІСТ

ВСТУП

1. РОБОТА З СИГНАЛАМИ

1.1 Робота з UNIX-сигналами

2. СИМВОЛИ В ЯДРІ

2.1 Пошук символів

2.2 Неекспортовані символи ядра

2.3 Оптимальний підхід до реалізації пошуку символів у ядрі

3. СИСТЕМНІ ВИКЛИКИ

3.1 Виконання системних викликів

3.2 Підміна системного виклику

3.3 Додавання системного виклику

3.4 Приховання системного виклику

4. ЗАВАНТАЖЕННЯ МОДУЛІВ ЯДРА

4.1 Завантаження модуля ядра із програмного коду

4.2 Завантаження модуля ядра з коду іншого модуля

4.3 Підключаючі плагіни

ВИСНОВКИ

ПЕРЕЛІК ВИКОРИСТАНОЇ ЛІТЕРАТУРИ

ВСТУП

У Linux є функціональність, доступну при програмуванні модулів ядра, але виходить за рамки традиційного використання модулів і слабо освітлену в документації. Можливості ядра в такій традиційно консервативної області UNIX-програмування, як сигнали.

Сигнали одночасно є одним з «наріжних каменів» і характерною рисою операційних систем сімейства UNIX. Сигнали UNIX (саме така повна назва застосовується для них у літературі) часто непомітні для кінцевого користувача, але відіграють значну роль усередині системи, тому що без допомоги сигналів ми не змогли б завершити жоден процес у системі (натискаючи ^C або виконуючи команди kill, killall). Як звичайно, ми не будемо заглиблюватися в докладне вивчення сигналів, але зосередимося на їхньому спільному використанні з модулями ядра.

1. РОБОТА З СИГНАЛАМИ

1.1 Робота з UNIX-сигналами

В Linux існує можливість відправлення з ядра сигналу будь-якому процесу простору користувача або будь-якому іншому потоку простору ядра. У ядрі Linux (починаючи з версії 2.5) може існувати багато незалежних потоків виконання, більше того, ядро Linux є що витісняє (preemptive): код ядра в стані витиснути інші завдання, що виконуються, навіть коли ті працюють у режимі ядра. До речі, тільки далеко не всі комерційні реалізації UNIX мають витісняючим ядро, наприклад, Solaris або AIX.

В користувальницькому просторі звичайно виконується командою kill (або викликом kill() із програмного коду), але тепер аналогічні дії можна виконати й з коду модуля ядра. Можливість організації взаємодії в ядрі за допомогою сигналів UNIX стає очевидної при уважному вивченні викликів API ядра, які переводять поточний потік виконання в блокований стан. Всі ці виклики присутні у двох формах:

· безумовної, що очікує настання фінальної умови;

· що перериває, що допускає переривання очікування одержанням сигналу.

Нам залишається тільки з'ясувати: як реалізувати таку можливість? Для рішення цього завдання підготуємо тестовий проект із архіву signal.tgz. Наш приклад буде видозміненою реалізацією ідеї зі згадуваної книги "Writing Linux Device Drivers". Цей об'ємний тест складається із трьох програмних компонентів:

· користувальницький додаток sigreq, що реєструє одержувані сигнали;

· модуль ядра ioctl_signal.ko, що буде відправляти зазначений сигнал певному користувальницькому процесу;

· діалоговий користувальницький процес ioctl, що вказує модулю ядра ioctl_signal.ko, який саме сигнал відсилати (по номері) і якому процесу (у нашому випадку -- це sigreq).

Процес ioctl буде передавати команди для ioctl_signal.ko за допомогою викликів ioctl(), які вже розглядалися в статтях, присвячених драйверам символьних пристроїв. Загальні визначення, необхідні для команд ioctl(), винесені в окремий файл ioctl.h, наведений у лістингу 1.

Лістинг 1. Визначення кодів команд ioctl().

#define MYIOC_TYPE 'k'

#define MYIOC_SETPID _IO(MYIOC_TYPE,1) //установити PID процесу,

якому буде відправлений сигнал

#define MYIOC_SETSIG _IO(MYIOC_TYPE,2) //установити номер сигналу, що відсилає

#define MYIOC_SENDSIG _IO(MYIOC_TYPE,3) //відправити сигнал

#define SIGDEFAULT SIGKILL

У лістингу 2 наведений код модуля ioctl_signal.ko, що виконує відправлення сигналів.

Лістинг 2. Модуль, що посилає сигнал UNIX додаткам.

#include <linux/module.h>

#include "ioctl.h"

#include "lab_miscdev.h"

static int sig_pid = 0;

static struct task_struct *sig_tsk = NULL;

static int sig_tosend = SIGDEFAULT;

static inline long mycdrv_unlocked_ioctl( struct file *fp,

unsigned int cmd, unsigned long arg ) {

int retval;

switch( cmd ) {

case MYIOC_SETPID:

sig_pid = (int)arg;

printk( KERN_INFO "Setting pid to send signals to, sigpid = %d\n",

sig_pid );

sig_tsk = pid_task( find_vpid( sig_pid ), PIDTYPE_PID );

break;

case MYIOC_SETSIG:

sig_tosend = (int)arg;

printk( KERN_INFO "Setting signal to send as: %d \n",

sig_tosend );

break;

case MYIOC_SENDSIG:

if( !sig_tsk ) {

printk( KERN_INFO "You haven't set the pid; using current\n" );

sig_tsk = current;

sig_pid = (int)current->pid;

}

printk( KERN_INFO "Sending signal %d to process ID %d\n",

sig_tosend, sig_pid );

retval = send_sig( sig_tosend, sig_tsk, 0 );

printk( KERN_INFO "retval = %d\n", retval );

break;

default:

printk( KERN_INFO " got invalid case, CMD=%d\n", cmd );

return -EINVAL;

}

return 0;

}

static const struct file_operations mycdrv_fops = {

.owner = THIS_MODULE,

.unlocked_ioctl = mycdrv_unlocked_ioctl,

.open = mycdrv_generic_open,

.release = mycdrv_generic_release

};

module_init( my_generic_init );

module_exit( my_generic_exit );

У цьому файлі присутній оброблювач, що цікавить нас, функцій ioctl(), а всі інші операції модуля (створення символьного пристрою /dev/mycdrv, open(), і т.д.) винесені у включає файл, що, miscdev.h, загальний для багатьох прикладів і докладно вивчений раніше при розгляді операцій із символьним пристроєм.

Остаємо на групі функцій API ядра, що виконують пошук процесу по його PID. Нижче наведені описи декількох таких функцій із заголовних файлів ядра.

#include <linux/sched.h>

struct task_struct *find_task_by_vpid( pid_t nr );

#include <linux/pid.h>

struct pid *find_vpid( int nr );

struct task_struct *pid_task( struct pid *pid, enum pid_type );

enum pid_type {

PIDTYPE_PID,

PIDTYPE_PGID,

PIDTYPE_SID,

PIDTYPE_MAX

};

У лістингу 3 наведений додаток, що одержує інформацію від користувача й виконує серію викликів ioctl() для взаємодії з модулем: установка PID процесу, установка номера сигналу й відправлення сигналу:

Лістинг 3. Програма для керування модулем

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <sys/ioctl.h>

#include <signal.h>

#include "ioctl.h"

static void sig_handler( int signo ) {

printf( "-і-> signal %d\n", signo );

}

int main( int argc, char *argv[] ) {

int fd, rc;

unsigned long pid, sig;

char *nodename = "/dev/mycdrv";

pid = getpid();

sig = SIGDEFAULT;

if( argc > 1) sig = atoi( argv[ 1 ] );

if( argc > 2) pid = atoi( argv[ 2 ] );

if( argc > 3) nodename = argv[ 3 ];

if( SIG_ERR == signal( sig, sig_handler ) )

printf( "set signal handler error\n" );

/* відкриття пристрою */

fd = open( nodename, O_RDWR );

printf( "I opened the device node, file descriptor = %d\n", fd );

/* виклик IOCTL для установки PID процесу */

rc = ioctl( fd, MYIOC_SETPID, pid );

printf("rc from ioctl setting pid is = %d\n", rc );

/* виклик IOCTL для установки номера сигналу */

rc = ioctl( fd, MYIOC_SETSIG, sig );

printf("rc from ioctl setting signal is = %d\n", rc );

/* виклик IOCTL для ініціації відправлення сигналу */

rc = ioctl( fd, MYIOC_SENDSIG, "anything" );

printf("rc from ioctl sending signal is = %d\n", rc );

/* закриття пристрою */

close( fd );

printf( "FINISHED, TERMINATING NORMALLY\n");

exit( 0 );

}

І, нарешті, простий додаток, що є кінцевим приймачем сигналів, що відправляють:

Лістинг 4. Процес, що реєструє одержання сигналу.

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include "ioctl.h"

static void sig_handler( int signo ) {

printf( "-і-> signal %d\n", signo );

}

int main( int argc, char *argv[] ) {

unsigned long sig = SIGDEFAULT;

printf( "my own PID is %d\n", getpid() );..

sig = SIGDEFAULT;

if( argc > 1) sig = atoi( argv[ 1 ] );

if( SIG_ERR == signal( sig, sig_handler ) )

printf( "set signal handler error\n" );

while( 1) pause();

exit( 0 );

}

У цьому додатку (як й у попередньому) для установки оброблювача сигналу використається стара, так називана "ненадійна модель" обробки сигналів c використанням виклику signal(), але в цьому випадку це ніяк не впливає на вірогідність одержуваних результатів. Для перевірки функціонування можна скористатися (майже) кожним з набору сигналів UNIX, тому ми виберемо не має спеціального призначення сигнал SIGUSR1 (сигнал номер 10):

$ ./sigreq 10

my own PID is 10737

-і-> signal 10

$ kill -n 10 10737

Як видно процес-реєстратор відреагував на одержання сигналу, тобто сигнал був доставлений до додатка. А тепер виконаємо повноцінне тестування, змусивши процес ioctl виконати серію викликів ioctl() до завантаженого модуля ядра, щоб він, у свою чергу, відправив зазначений сигнал процесу sigreq:

$ sudo insmod ioctl_signal.ko

$ lsmod | head -n2

Module Size Used by

ioctl_signal 2053 0.

$ dmesg | tail -n2

Succeeded in registering character device mycdrv

$ cat /sys/devices/virtual/misc/mycdrv/dev

10:56

$ ls -l /dev | grep my

crw-rw-і-і 1 root root 10, 56 Травень 6 17:15 mycdrv

$ ./ioctl 10 11684

I opened the device node, file descriptor = 3

rc from ioctl setting pid is = 0

rc from ioctl setting signal is = 0

rc from ioctl sending signal is = 0

FINISHED, TERMINATING NORMALLY

$ dmesg | tail -n14

Succeeded in registering character device mycdrv

attempting to open device: mycdrv:

MAJOR number = 10, MINOR number = 56

successfully open device: mycdrv:

I have been opened 1 times since being loaded

ref=1

Setting pid to send signals to, sigpid = 11684

Setting signal to send as: 10.

Sending signal 10 to process ID 11684

retval = 0

closing character device: mycdrv:

$ ./sigreq 10

my own PID is 11684

-і-> signal 10

^C

Відправлення сигналу в цій реалізації виконуються при виклику send_sig(), що разом з іншими функціями API, пов'язаними з відправленням сигналів, визначені у файлі <linux/sched.h>:

int send_sig_info( int signal, struct siginfo *info, struct task_struct *task );

int send_sig( int signal, struct task_struct *task, int priv );

int kill_pid_info( int signal, struct siginfo *info, struct pid *pid );

...

Опис досить складної структури siginfo перебуває в заголовному файлі простору користувача</usr/include/asm-generic/siginfo.h>:

typedef struct siginfo {

int si_signo;

int si_errno;

int si_code;

...

}

2. СИМВОЛИ В ЯДРІ

2.1 Пошук символів

ядро символ системний виклик

Експорт символів - це один із самих складних аспектів функціонування ядра. Для того, щоб ім'я із простору ядра можна було «зв'язати» у модулі, повинні виконуватися дві умови:

· ім'я повинне мати глобальну область видимості (у коді такі імена не повинні оголошуватися як static);

· ім'я повинне бути явно оголошено експортованим, тобто записано параметром макровиклику EXPORT_SYMBOL (илиEXPORT_SYMBOL_GPL).

Візьмемо для порівняння два подібних системних виклики: sys_open й sys_close.

$ cat /proc/kallsyms | grep ' T ' | grep sys_open

c04deb28 T do_sys_open

c04dec0c T sys_openat

c04dec35 T sys_open

$ cat /proc/kallsyms | grep ' T ' | grep sys_close

c04dea99 T sys_close

Обоє імені sys_open й sys_close відомі в таблиці символів ядра як глобальні імена в секції коду (T). Підготуємо найпростіший модуль ядра, Лістинг 1. Експортований символ sys_close

#include <linux/module.h>

extern int sys_close( int fd );

static int __init sys_init( void ) {

void* Addr;

Addr = (void*)sys_close;

printk( "sys_close address: %p\n", Addr );

return -1;

}

module_init( sys_init );

Перевіримо, що в нас вийшло.

$ sudo insmod md_0c.ko

insmod: error inserting 'md_0c.ko': -1 Operation not permitted

$ dmesg

sys_close address: c04dea99

Усе спрацювало, як й очікувалася: адреса оброблювача системного виклику sys_close (експортований ядром й отриманий у ході виконання) збігається зі значенням, прочитаним раніше з /proc/kallsyms. Тепер підготуємо аналогічний модуль md_0o.c для обробки симетричного системного виклику sys_open.

Лістинг 2. Неекспортований символ sys_open.

#include <linux/module.h>

extern int sys_open( int fd );

static int __init sys_init( void ) {

void* Addr;

Addr = (void*)sys_open;

printk( KERN_INFO "sys_open address: %p\n", Addr );

return -1;

}

module_init( sys_init );

Прототип sys_open(), описаний у листинге 2, не відповідає реальному формату виклику оброблювача системного виклику для open(), але це не має значення, тому що ми не плануємо робити виклик, а тільки хочемо одержати адресу для зв'язування. Однак одержати цю адресу виявляється неможливо, як показано нижче.

$ make

...

MODPOST 2 modules

WARNING: "sys_open"

[/home/olej/2011_WORK/LINUX-books/examples.DRAFT/sys_call_table/md_0o.ko]

undefined!

...

$ sudo insmod md_0o.ko

insmod: error inserting 'md_0o.ko': -1 Unknown symbol in module

$ dmesg

md_0o: Unknown symbol sys_open

Як видно, на етапі зв'язування в модулі був виявлений невизначений символ. Такий модуль не може бути завантажений, тому що він суперечить правилам цілісності ядра: містить недозволений зовнішній символ (тобто цей символ не експортується ядром для зв'язування). Посилатися по іменах до об'єктів у коді модуля можна тільки на ті імена, які вже були експортовані (ядром або кожним раніше завантаженим модулем). Як можна довідатися, які із символів є експортованими, а які ні, тим більше що в ядрі присутні порядку декількох десятків тисяч символів?

$ cat /proc/kallsyms | wc -l

69698

Кожен рядок у файлі Module.symvers відповідає опису одного експортованого символу й має наступний формат:

· ім'я символу (2-ий стовпчик);

· модуль, що експортує символ, із вказівкою шляху до файлу модуля, або vmlinux, якщо символ експортується безпосередньо ядром;

· тип експортування, наприклад, EXPORT_SYMBOL або EXPORT_SYMBOL_GPL.

$ cat /lib/modules/'uname -r'/build/Module.symvers | grep sys_

0x00000000 sys_close vmlinux EXPORT_SYMBOL

0x00000000 sys_copyarea vmlinux EXPORT_SYMBOL

0x00000000 fb_sys_write vmlinux EXPORT_SYMBOL_GPL

0x00000000 nfnetlink_subsys_register net/netfilter/nfnetlink EXPORT_SYMBOL_GPL

...

Як було показано вище, число експортованих символів майже на порядок (69698:9594 ~ 10:1) менше загального числа імен ядра, наприклад, sys_close експортується самим ядром (vmlinux), а sys_open -- немає. Але як джерело експортованих символів можуть виступати й модулі (net/netfilter/nfnetlink).

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

2.2 Неекспортовані символи ядра

Чи означає показане вище, що для нашого модуля доступні тільки експортовані символи ядра? Ні, це означає тільки, що рекомендує способ, що, зв'язування по імені застосуємо тільки до експортованих імен. Експортування забезпечує додатковий рівень контролю для забезпечення цілісності ядра, тому що мінімальна некоректність приводить до повного краху операційної системи, іноді при цьому вона навіть не встигає вивести фінальне повідомлення. Особливо це ставиться до модулів, подібним тим, до розгляду яких ми приступаємо. Файл call_table.tgz з вихідним кодом модулів можна знайти в розділі "Матеріали для скачивания".

Модуль також може використати й всі інші імена (функцій, змінних, структур), перераховані в /proc/kallsyms, якщо зможе сам уважати їх. У найпростішому випадку це могло б виглядати так:

1. модуль повинен уважати файл /proc/kallsyms;

2. знайти в ньому адреса його імені, що цікавить;

3. скористатися знайденим ім'ям.

Правда, ця схема настільки поверхнева, як буде показано далі, що, скоріше, її можна розглядати як модель, що пояснює загальний принцип. Хоча така модель не дуже ефективна й має більше вдалі альтернативи, але вона дуже добре пояснює суть, і тому ми почнемо розгляд з її. Повна реалізація даного підходу наведена у файлі mod_rct.c, якому можна знайти в архіві call_table.tgz. У листинге приводиться тільки центральна частина коду, що зчитує й перебирає символи ядра з /proc/kallsyms у пошуках символу sys_call_table (адреса таблиці системних викликів).

Лістинг 3. Перебір імен ядра з файлу /proc/kallsyms.

...

static char* file = "/proc/kallsyms";

...

char buff[ BUF_LEN + 1 ] = "";

f = filp_open( file, O_RDONLY, 0 );

while( 1 ) {

char *p = buff, *find;

int k;

*p = '\0';

do {

if( ( k = kernel_read( f, n, p++, 1 ) ) < 0 ) {

printk( "+ failed to read\n" );

return -EIO;

};

*p = '\0';

if( 0 == k ) break;

n += k;

if( '\n' == *( p - 1 ) ) break;

} while( 1 );

if( ( debug != 0 ) && ( strlen( buff ) > 0 ) ) {

if( '\n' == buff[ strlen( buff ) - 1 ] ) printk( "+ %s", buff );

else printk( "+ %s|\n", buff );

}

if( 0 == k ) break; // EOF

if( NULL == ( find = strstr( buff, "sys_call_table" ) ) ) continue;

put_table( buff );

}

printk( "+ close file: %s\n", file );

filp_close( f, NULL );

...

Очевидно, що це вкрай ускладнений підхід, але проте він добре ілюструє основну ідею принципу. Спробуємо виконати наш модуль і скористаємося для цього командою time, щоб оцінити скільки часу займає перебір десятків тисяч імен ядра.

$ time sudo insmod mod_rct.ko

insmod: error inserting 'mod_rct.ko': -1 Operation not permitted

real 0m0.728s

user 0m0.003s

sys 0m0.719s

$ dmesg | tail -n4

[57478.736476] + openning file: /proc/kallsyms

[57478.912136] + sys_call_table address = c07c2438

[57478.912140] + sys_call_table : c044e80c c0443af8 c0408a04

c04e39e3 c04e3a45 c04e2e59 c04e1e59 c0443dce c04e2ead c04ed654 ...

[57479.453508] + close file: /proc/kallsyms

Формат висновку dmesg і всі адреси оброблювачів у таблиці sys_call_table відрізняються від показаних раніше, тому що ця демонстрація виробляється на іншій версії ядра (причину використання декількох версій ядра ми розберемо пізніше). Крім адреси таблиці sys_call_table модуль виводить для контролю 10 перших крапок входу цієї таблиці. Перевіримо деякі із цих адрес зворотним пошуком в /proc/kallsyms:

$ cat /proc/kallsyms | grep c044e80c

c044e80c T sys_restart_syscall

$ cat /proc/kallsyms | grep c0443af8

c0443af8 T sys_exit

$ cat /proc/kallsyms | grep c0408a04

c0408a04 t ptregs_fork

$ cat /proc/kallsyms | grep c04e39e3

c04e39e3 T sys_read

$ cat /proc/kallsyms | grep c04e3a45

c04e3a45 T sys_write

...

Виведена інформація в точності відповідає початку масиву адрес оброблювачів системних викликів Linux, індекси якого ми розглядали в одній з попередніх частин:

$ cat /usr/include/asm/unistd_32.h

...

#define __NR_restart_syscall 0

#define __NR_exit 1

#define __NR_fork 2

#define __NR_read 3

#define __NR_write 4

...

2.3 Оптимальний підхід до реалізації пошуку символів у ядрі

Ще один недолік рішення, що обговорювалося в попередній статті, полягає в тому, що воно вийшло громіздким иненатуральным. Псевдофайл /proc/kallsyms (таблиця) - це ні що інше, як відображення в зовнішнє оточення деякої внутрішньої структури ядра. Ядро надає користувачам (тобто модулям) вызовkallsyms_lookup_name(), що дозволяє виконувати пошук у цій внутрішній структурі. У лістингу 1 представлений новий варіант рішення завдання.

Лістинг 1. Перебір імен за допомогою kallsyms_lookup_name()

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/init.h>

#include <linux/kallsyms.h>

static int __init ksys_call_tbl_init( void ) {

void** sct = (void**)kallsyms_lookup_name( "sys_call_table" );

printk( "+ sys_call_table address = %p\n", sct );

if( sct ) {

int i;

char table[ 120 ] = "sys_call_table : ";

for( i = 0; i < 10; i++ )

sprintf( table + strlen( table ), "%p ", sct[ i ] );

printk( "+ %s ...\n", table );

}

return -EPERM;

}

module_init( ksys_call_tbl_init );

MODULE_LICENSE( "GPL" );

Як видно, кількість коду скоротилося, а його складність знизилася. Але це ще не всі, тому що виконання даного приклада для одержання таких же результатів займає в 30 разів менше часу в порівнянні з попередньою реалізацією.

$ time sudo insmod mod_kct.ko

insmod: error inserting 'mod_kct.ko': -1 Operation not permitted

real 0m0.022s

user 0m0.005s

sys 0m0.013s

$ dmesg | tail -n2

[59595.918185] + sys_call_table address = c07c2438

[59595.918193] + sys_call_table : c044e80c c0443af8 c0408a04 c04e39e3

c04e3a45 c04e2e59 c04e1e59 c0443dce c04e2ead c04ed654 ...

Але в кожного методу обов'язково присутні свої побічні ефекти, і щоб вони виявилися, запустимо наш приклад на ледве більше ранній версії ядра:

$ uname -r

2.6.32.9-70.fc12.i686.PAE

$ make

...

WARNING: "kallsyms_lookup_name"

[/home/olej/2011_WORK/LINUX-books/examples.DRAFT/sys_call_table/call_table/mod_kct.ko]

undefined!

Як видно, ім'я kallsyms_lookup_name є присутнім у версії ядра 2.6.32.

$ cat /proc/kallsyms | grep kallsyms_ | grep T

c046e815 T module_kallsyms_lookup_name

c0471764 T kallsyms_on_each_symbol

c04717f2 T kallsyms_lookup_name

...

Цей виклик ядра став експортованим у проміжку між версіями ядра 2.6.32 й 2.6.35 (або приблизно між пакетними дистрибутивами Linux, випущеними влітку 2010р. і навесні 2011р.). В усіх більше ранніх дистрибутивах скористатися цим викликом не вдасться.

Однак цю проблему можна обійти, якщо скористатися іншим експортованим ім'ям kallsyms_on_each_symbol, що також є присутнім у таблиці символів. Цей виклик забезпечує виконання зазначеної користувальницької функції послідовно для всіх імен ядра. Правда, алгоритм його використання більше складний і тому вимагає додаткових пояснень. Виклик kallsyms_on_each_symbol має такий прототип, оголошений у файлеlinux/kallsyms.h:

int kallsyms_on_each_symbol( int

(*fn)(void *, const char *, struct module *, unsigned long),

void *data );

У першому параметрі fn передається покажчик на вашу користувальницьку функцію, що буде последовательновызываться для всіх символів у таблиці ядра, а в другому параметрі data -- покажчик на довільний блок даних (параметрів), що буде передаватися в кожен виклик функції fn. Така передача даних (параметрів виклику) -- це стандартна практика, подібний підхід, наприклад, також застосовується при створенні нового потоку (як потоку ядра, так і потоку користувальницького простору в POSIX API). Прототип користувальницької функції fn, що циклічно викликається для кожного імені:

int func( void *data, const char *symb, struct module *mod, unsigned long addr );

де:

· data -- блок параметрів, заповнений у зухвалій одиниці й переданий з виклику функцииkallsyms_on_each_symbol() (2-й параметр виклику), як це було описано вище, тут звичайно передається ім'я розшукуваного символу;

· symb -- символьне зображення (рядок) імені з таблиці імен ядра, що обробляється на текущемвызове func (природно, це значення змінюється при кожному виклику func);

· mod -- модуль ядра, до якого ставиться оброблюваний символ;

· addr -- адреса символу в адресному просторі ядра (те, що нам потрібно знайти).

З міркувань ефективності перебір імен таблиці ядра можна перервати на поточному кроці, якщо необхідні символи вже були оброблені, і тоді користувальницька функція func повинна повернути ненульове значення.

Незважаючи на те, що принцип використання виклику kallsyms_on_each_symbol() складніше, ніж у виклику, що з'явився пізніше, kallsyms_lookup_name(), в "старого" варіанта набагато більше можливостей. З їхньою допомогою ми створимо третій варіант модуля для здійснення пошуку в таблиці імен ядра.

Лістинг 2. Перебір імен за допомогою kallsyms_on_each_symbol().

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/init.h>

#include <linux/kallsyms.h>

static int nsym = 0;

static unsigned long taddr = 0;

int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) {

nsym++;

if( 0 == strcmp( (char*)data, sym ) ) {

printk( "+ sys_call_table address = %lx\n", addr );

taddr = addr;

return 1;

}

return 0;

};

static int __init ksys_call_tbl_init( void ) {

int n = kallsyms_on_each_symbol( symb_fn, (void*)"sys_call_table" );

if( n != 0 ) {

int i;

char table[ 120 ] = "sys_call_table : ";

printk( "+ find in position %d\n", nsym );

for( i = 0; i < 10; i++ ) {

unsigned long sa = *( (unsigned long*)taddr + i );

sprintf( table + strlen( table ), "%p ", (void*)sa );

}

printk( "+ %s ...\n", table );

}

else printk( "+ symbol not found\n" );

return -EPERM;

}

module_init( ksys_call_tbl_init );

MODULE_LICENSE( "GPL" );

MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" );

Для переконливості повернемося й виконаємо цей код у ядрі 2.6.32, у якому попередній приклад не працював:

$ uname -r

2.6.32.9-70.fc12.i686.PAE

$ time sudo insmod mod_koes.ko

insmod: error inserting 'mod_koes.ko': -1 Operation not permitted

real 0m0.042s

user 0m0.005s

sys 0m0.027s

$ dmesg | tail -n30 | grep +

+ sys_call_table address = c07ab3d8

+ find in position 25239

+ sys_call_table : c044ec61 c0444f63 c040929c c04e149d c04e12fc c04dec35

c04dea99 c0444767 c04dec60

$ cat /proc/kallsyms | wc -l

69423

$ cat /proc/kallsyms | grep c04dec35

c04dec35 T sys_open

Як можна помітити:

· це ті ж результати, що й у найпершому прикладі (безпосередньо читаючому /proc/kallsyms);

· ноб час виконання скоротилося в ~20 разів;

· для знаходження необхідного символу читалася не вся таблиця імен ядра (69423 символів), а тільки біля (25239) однієї її третини;

3. СИСТЕМНІ ВИКЛИКИ

3.1 Виконання системних викликів

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

Лістинг 1. Висновок рядка на термінал виконанням системного виклику.

#include <linux/module.h>

#include <linux/kallsyms.h>

#include <linux/uaccess.h>

static unsigned long waddr = 0;

static char buf[ 80 ];

static int len;

int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) {

if( 0 == strcmp( (char*)data, sym ) ) {

waddr = addr;

return 1;

}

else return 0;

}

/* запозичимо з <linux/syscalls.h>, тут дуже важливо - asmlinkage:

asmlinkage long sys_write(unsigned int fd, const char __user *buf,

size_t count); */

static asmlinkage long (*sys_write) (

unsigned int fd, const char __user *buf, size_t count );

static int do_write( void ) {

int n;

mm_segment_t fs = get_fs();

set_fs( get_ds() );

sys_write = (void*)waddr;

n = sys_write( 1, buf, len );

set_fs(fs);

return n;

}

static int __init wr_init( void ) {

int n = kallsyms_on_each_symbol( symb_fn, (void*)"sys_write" );

if( n != 0 ) {

sprintf( buf, "адреса системного оброблювача sys_write = %lx\n", waddr );

len = strlen( buf ) + 1;

printk( "+ [%d]: %s", len, buf );

n = do_write();

printk( "+ write return : %d\n", n );

}

else

printk( "+ %d: symbol not found\n", n );

return -EPERM;

}

module_init( wr_init );

MODULE_LICENSE( "GPL" );

MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" );

Хоча в даному прикладі ми не виконуємо виклик printf(), але, як говорилося в попередніх статтях, бібліотечний виклик printf() виконується через бібліотечний виклик sprintf(), що, у свою чергу, викликає системныйвызов write( 1, ... ). Тому ми фактично вирішили поставлене завдання, відтворивши зазначену послідовність викликів. Виконаємо перевірку створеного модуля.

$ sudo insmod mod_wrc.ko

адреса системного оброблювача sys_write = c04e12fc

insmod: error inserting 'mod_wrc.ko': -1 Operation not permitted

$ dmesg | tail -n30 | grep +

+ [77]: адреса системного оброблювача sys_write = c04e12fc

+ write return : 77

Видно, що висновок рядка відбувається до висновку повідомлення про завершення виконання коду модуля на графічний термінал (X11). З подібним поводженням ми вже зустрічалися при розгляді системних викликів. Природно, виклик write() був виконаний з функції ініціалізації модуля, і тому здійснює висновок на керуючий термінал зухвалого процесу, а в цьому випадку -- це процес insmod. Щоб остаточно переконатися, перевіримо адресу оброблювача sys_write у файлі /proc/kallsyms.

$ cat /proc/kallsyms | grep T | grep sys_write

c04e12fc T sys_write

c04e196b T sys_writev

c05f99fc T fb_sys_write

У коді модуля немає нічого складного, за винятком питання: звідки був узятий точний прототип для власної нової функції sys_write(), що пізніше був привласнений адреса системного оброблювача виклику sys_write? От визначення, про яке мова йде:

asmlinkage long sys_write( unsigned int fd, const char __user *buf,

size_t count );

Очевидно, що визначення для нашої функції було скопійовано із заголовного файлу linux/syscalls.h (про це є явне згадування в коментарі). Я рекомендую надходити так й у відношенні всіх інших системних викликів, тому що в цьому випадку будь-яка помилка у визначенні прототипу функції приводить до негайного краху всієї системи. Зокрема, ключовим фактором для деяких системних оброблювачів й інших неекспортованих функцій ядра буде наявність або відсутність визначення: asmlinkage;. При його наявності параметри виклику будуть по черзі (від першого до останнього) заноситися в регістри процесора, а при його відсутності - міститися в стек (від останнього до першого) зухвалим процесом, відповідно до угод про виклик, установленими в мові C.

Цей приклад породжує ще ряд цікавих питань, як, наприклад, чи спрацює такий виклик тільки для висновку вграфический термінал (X11) або й у текстову консоль (<Ctrl><Alt><F2>)? Так, спрацює, але перевірку цього твердження я залишаю читачеві.

Інше питання полягає в тому, навіщо як рядок висновку використалися російськомовний рядок, хоча це й не вітається в програмуванні ядра? Тому що в повному тракті проходження повідомлень від ядра задіяно занадто багато послідовних шарів і компонентів (реалізація printk(), демон журналу, файл журналу, термінальна система UNIX, термінал, визуализатор dmesg, і т.д.), те висновок такого рядка може стати гарною перевіркою для погодженості роботи всіх цих компонентів. Попередньо варто перевірити установки системи:

$ echo $LANG

ru_RU.UTF-8

Установимо рівень діагностики нижче порога висновку (але тільки з термінала root, а не за допомогою sudo), інакше запускати модуль із консолі просто безглуздо, і повторимо установку нашого модуля:

# cat /proc/sys/kernel/printk

3 4 1 7

# echo 8 > /proc/sys/kernel/printk

# cat /proc/sys/kernel/printk

8 4 1 7

# sudo insmod mod_wrc.ko

...

Ми побачимо в консолі досить дивну картину:

· висновок виклику write() (який виконується модулем) виведе очікуваний рядок: "адреса …";

· висновок команд dmesg й cat /var/log/messages (виконуваних у консолі!) виведе той же рядок: "адреса …";

· тестова програма hello_world виведе в консолі російськомовний рядок: "Привіт мир!";

· а от діагностичний висновок у консоль виклику printk() з ядра покаже рядок дивного змісту й довжиною в 77 символів.

Виходить, що реалізація виклику printk() у ядрі:

1. виводить діагностику не через системний журнал, а паралельно демонові журналирования;

2. намагається інтерпретувати потік UNICODE символів і перетворювати їх побайтно в ASCII;

3. у результаті символи UNICODE одержують «необоротні» ушкодження й рядок виводиться в такому дивному виді.

У результаті проведених експериментів ми можемо заявити, що програмістові модулів ядра, крім інструментів програмування, присутніх у ядрі, доступний і набір всіх системних викликів POSIX API простору користувача. Природно, тлумачення результатів деяких з таких системних викликів у контексті ядра може бути досить двозначним, а іноді навіть і безглуздим. Але проте, самі виклики можна виконувати й використати для рішення окремих завдань!

3.2 Підміна системного виклику

Всі системні виклики з користувальницьких процесів, як говорилося в попередніх статтях, проходять через таблицуsys_call_table (якийсь case-селектор, що передає керування в оброблювач необхідного запиту). Індекс адреси оброблювача кожного системного виклику, що втримується в цьому масиві, визначається номером виклику, як показано нижче:

$ cat /usr/include/asm/unistd_32.h

...

#define __NR_restart_syscall 0

#define __NR_exit 1

#define __NR_fork 2

...

Ця таблиця використається для зв'язку системних викликів простору користувача з оброблювачами цих викликів у просторі ядра (у випадку з 64-х бітною системою ця інформація втримується у файлі unistd_64.h).

Іноді потрібно підмінити існуючий або додати нова адреса оброблювача в цю таблицю. Подібна техніка, відома ще із часів MS-DOS, застосовується в різних операційних системах. Нижче наведений далеко не повний список ситуацій, коли необхідно виконати подібну операцію:

· для моніторингу й нагромадження статистики по якому-небудь існуючому системному виклику;

· для додавання власного оброблювача нового системного виклику, що буде використатися прикладними програмами простору користувача цільового пакета;

· для перехоплення керування комп'ютером (не дуже гарний приклад, але це звичайне поводження вірусів і шкідливих програм).

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

При cтатическом підході необхідно додати свій файл реалізації arch/i386/kernel/new_calls.c у дерево вихідних кодів ядра Linux і відповідний рядок-запис у таблицю системних викликів arch/i386/kernel/syscall_table.S, а також включити свою реалізацію в зборку ядра, дописавши в arch/i386/kernel/Makefile рядок виду:

· obj-y += new_calls.o

· Після буде потрібно зібрати ядро заново, щоб одержати його модифіковану версію, у якій реалізований необхідний новий системний виклик у просторі ядра. Однак цей підхід виходить за рамки нашого циклу й тому далі розглядатися не буде.

· При динамічному підході під час виконання в таблицю sys_call_table[] додається посилання на код власного модуля, що і реалізує новий системний виклик (і зробити цю дію в просторі ядра може, природно, тільки код модуля ядра).

До версії 2.6 ядро експортувало адресу таблиці системних викликів sys_call_table[]. У поточних версіях цей символ може бути присутнім у таблиці імен ядра (/proc/kallsyms), але він уже не експортується для використання модулями:

$ cat /proc/kallsyms | grep 'sys_call'

c052476b t proc_sys_call_handler

c07ab3d8 R sys_call_table

Однак ядро завжди експортує символ sys_close, що перебуває в початкових позиціях таблиці sys_call_table[]:

$ cat /proc/kallsyms | grep sys_close

c04dea99 T sys_close

Спеціальні програми, обговорювані на профільних форумах, розшукують це відоме значення в сегменті коду ядра, зворотним відліком (__NR_close) і визначають по ньому місце розташування таблиці sys_call_table[], після чого можуть динамічно додавати нові або підмінювати існуючі системні виклики.

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

$ sudo insmod mod_wrchg_1.ko

...

Message from syslogd@notebook at Dec 31 01:56:41 ...

kernel:CR2: 00000000c07ab3e8

$ dmesg | tail -n100 | grep -v audit

! адреса sys_call_table = c07ab3d8

! адреса в позиції 4[__NR_write] = c04e12fc

! адреса sys_write = c04e12fc

! адреса нового sys_write = fe1f1024

! CR0 = 8005003b

BUG: unable to handle kernel paging request at c07ab3e8

IP: [<fe1f40b6>] wrchg_init+0xb6/0xd4 [mod_wrchg_1]

*pdpt = 0000000000a8c001 *pde = 0000000036881063 *pte = 00000000007ab161

Oops: 0003 [#1] SMP

...

У результаті вийшов аварійно встановлений модуль, що неможливо видалити без перезавантаження системи. Помилка пов'язана з тим, що таблиця адрес системних викликів перебуває в сегменті тільки для читання, і спроба запису в неї приводить до помилки захисту пам'яті (звертаємо увагу на символ R):

$ cat /proc/kallsyms | grep sys_call_table

c07ab3d8 R sys_call_table

Це обумовлено нововведеннями апаратної архітектури процесора Intel x86, які використаються у відносно свіжих версіях ядра. Це обмеження можна обійти, тому що ми виконуємо код модуля в режимі супервізора, у нульовому кільці захисту процесора x86, де всі припустимо. Для цього на час перезапису крапки входу в таблицеsys_call_table скасуємо контроль запису в сегмент, оголошений доступним тільки для читання, а потім відновимо його назад. Варто відзначити, що цей приклад буде працювати тільки для архітектури x86, тому що захист запису присутня саме в x86, а для інших платформ існують рішення аналогічного плану. Так, в архітектурі ia-32 за контроль запису відповідає 16-й біт у керуючому регістрі процесора CR0 (називаний WP біт), а в архітектурі ia-64 застосовується інший підхід.

Лістинг 1. Керування захистом запису.

// 16 біт WP: |

// V

// 3 2 2 1 1 1 0 0 0

// 1 7 3 9 5 1 7 3 0

// 0000 0000 0000 0001 0000 0000 0000 0000 => 0x00010000

// 1111 1111 1111 1110 1111 1111 1111 1111 => 0xfffeffff

// показати керуючий регістр CR0

#define show_cr0() \

{ register unsigned r_eax asm ( "eax" ); \

asm( "pushl %eax" ); \

asm( "movl %cr0, %eax" ); \

printk( "! CR0 = %x\n", r_eax ); \

asm( "popl %eax"); \

}

//код вимикання захисту запису:

#define rw_enable() \

asm( "pushl %eax \n" \

"movl %cr0, %eax \n" \

"andl $0xfffeffff, %eax \n" \

"movl %eax, %cr0 \n" \

"popl %eax" );

//код включення захисту запису:

#define rw_disable() \

asm( "pushl %eax \n" \

"movl %cr0, %eax \n" \

"orl $0x00010000, %eax \n" \

"movl %eax, %cr0 \n" \

"popl %eax" );

У коментарі на початку листинга 2 показаний формат керуючого регістра cr0 для 32-розрядних процесорів і кілька макросів, що оперують із цим регістром. Макроси rw_enable() (розв'язний запис у сегмент для читання) і rw_disable() (восстанавливающий контроль запису) реалізовані як инлайновые ассемблерные вставки, причёмбез параметрів, а тому регістри в них можна вказати і як %eax й %cr0, (з одним префіксом %, а не подвійним).

Цей приклад буде працювати тільки на 32-розрядній платформі, для 64-розрядної платформи все принципиально залишиться так само, але

1. повинні використатися 64-розрядні операції (суфікс q у записі мнемоник команд AT&T),

2. замість регістра %eax повинен визначатися регістр %rax,

3. за захист запису відповідає біт CR іншого формату:

asm( "andq $0xfffffffffffeffff, %rax" );

...

asm( "orq $0x0000000000001000, %rax" );

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

Лістинг 2. Функція для пошуку символу в ядрі.

static void* find_sym( const char *sym ) {

static unsigned long faddr = 0; // static!!!

// -і-і-і-і-і- вкладена функція - розширення GCC -і-і-і-і-

int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) {

if( 0 == strcmp( (char*)data, sym ) ) {

faddr = addr;

return 1;

}

else return 0;

};

// -і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і-і

kallsyms_on_each_symbol( symb_fn, (void*)sym );

return (void*)faddr;

}

Ми вже розглядали подібні функції, але в коді функції find_sym() використається таке синтаксичне розширення gcc ( допускаєне стандартами мови З, але часто зустрічається в мовах групи PASCAL), як визначення вкладеної функції symb_fn(), локальної стосовно зухвалій. Незважаючи на свою «вычурность», такий підхід дозволяє описати повернення адреси будь-якого імені sym з таблиці /proc/kallsyms, не прибігаючи ні до яким глобальним змінного для загального використання.

В лістингу 3 розглядається код самого модуля

Лістинг 3. Модуль, що підмінює оброблювач sys_write.

#include <linux/module.h>

#include <linux/kallsyms.h>

#include <linux/uaccess.h>

#include <linux/unistd.h>

#include "../find.c"

#include "../CR0.c"

asmlinkage long (*old_sys_write) (

unsigned int fd, const char __user *buf, size_t count );

asmlinkage long new_sys_write (

unsigned int fd, const char __user *buf, size_t count ) {

int n;

if( 1 == fd ) {

static const char prefix[] = ":-) ";

mm_segment_t fs = get_fs();

set_fs( get_ds() );

n = old_sys_write( 1, prefix, strlen( prefix ) );

set_fs(fs);

}

n = old_sys_write( fd, buf, count );

return n;

};

EXPORT_SYMBOL( new_sys_write );

static void **taddr; // адреса таблиці sys_call_table

static int __init wrchg_init( void ) {

void *waddr;

if( ( taddr = find_sym( "sys_call_table" ) ) != NULL )

printk( "! адреса sys_call_table = %p\n", taddr );

else {

printk( "! sys_call_table не знайдений\n" );

return -EINVAL;

}

old_sys_write = (void*)taddr[ __NR_write ];

printk( "! адреса в позиції %d[__NR_write] = %p\n", __NR_write, old_sys_write );

if( ( waddr = find_sym( "sys_write" ) ) != NULL )

printk( "! адреса sys_write = %p\n", waddr );

else {

printk( "! sys_write не знайдений\n" );

return -EINVAL;

}

if( old_sys_write != waddr ) {

printk( "! незрозуміло! : адреси не збігаються\n" );

return -EINVAL;

}

printk( "! адреса нового sys_write = %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 ) {

printk( "! адреса sys_write при вивантаженні = %p\n", (void*)taddr[ __NR_write ] );

rw_enable();

taddr[ __NR_write ] = old_sys_write;

rw_disable();

return;

}

module_init( wrchg_init );

module_exit( wrchg_exit );

MODULE_LICENSE( "GPL" );

MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" );

Це найбільший і складний приклад із встречавшихся раніше, тому його варто розглянути у всіх подробицях. З елементів, що заслуговують детального розгляду, можна виділити:

· новий оброблювач new_sys_write() для системного виклику sys_write при висновку на SYSOUT делаетпредшествующий виведеному рядку висновок власного префікса (рядка ":-) "), причому для цього йому необхідно спочатку вважати сегмент даних в адресному просторі ядра (взяти дані рядки висновку з області ядра), після чого, відновити контроль приналежності адреси сегменту дані простори користувача (для висновку оригінального переданого рядка);

· при вивантаженні модуля обов'язково потрібно відновити колишню функцію оброблювач old_sys_write();

· при такому відновленні можливе виникнення критичної помилки для ядра, якщо деякий інший модуль ще раз підмінить адреса оброблювача після нашої заміни; тому показаний приклад можна використати тільки як ілюстрація, але не в реальних завданнях;

· робота в ядрі (і з таблицею системних викликів) - украй ризиковане заняття, тому в коді виконується подвійний повторний огляд: адреса оброблювача виклику, знайдена як символ ядра sys_write, рівняється з адресою в позиції __NR_write у таблиці sys_call_table;

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

$ sudo insmod mod_wrchg.ko

:-) $ :-) e:-) c:-) h:-) o:-) :-) з:-) т:-) р:-) про:-) до:-) а:-)

:-) рядок

:-) $ lsmod | head -n4

:-) :-) Module Size Used by

:-) mod_wrchg 1382 0

:-) fuse 48375 2

:-) ip6table_filter 2227 0

:-) $ sudo rmmod mod_wrchg

$

Тому що ми підмінили один із самих використовуваних при роботі з терміналом системних викликів Linux, те можна чекати, що робота із системою в командному рядку трохи ускладниться. Але після видалення модуля все повинне повернутися в норму. Тепер можна подивитися на дії модуля з погляду системного журналу.

$ dmesg | tail -n120 | grep -v audit

! адреса sys_call_table = c07ab3d8

! адреса в позиції 4[__NR_write] = c04e12fc

! адреса sys_write = c04e12fc

! адреса нового sys_write = fd8ae024

! CR0 = 8005003b

! CR0 = 8004003b

! CR0 = 8005003b

! адреса sys_write при вивантаженні = fd8ae024

У цьому прикладі спеціально показується отладочный висновок умісту керуючого регістра cr0 процесора.

3.3 Додавання системного виклику

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

На відміну від приклада, що обговорювався раніше, з підміною системного виклику це завдання, при всій її подібності, має деякі особливості:

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

· константа, що задає розмірність цієї таблиці (відома в ядрі як __NR_syscall_max), являетсяпрепроцессорной константою періоду компіляції й невідома під час виконання (принаймні, мені не вдалося вважати її значення).

· намагаючись додати власний системний виклик, ми не маємо права вийти за межі існуючої таблиці.

Розмір таблиці sys_call_table досить великий і може мінятися в різних версіях ядра:

$ cat /proc/kallsyms | grep ' sys_' | grep T | wc -l

345

Вище наведена зразкова оцінка тільки порядку величини, тому що деякі оброблювачі в сучасних версіях ядра підмінені на інші їхні форми, наприклад, оброблювач ptregs_fork виклику fork() в одній з початкових (__NR_fork == 2) позицій sys_call_table:

$ cat /proc/kallsyms | grep ptregs_fork

c040929c t ptregs_fork

$ cat /proc/kallsyms | grep sys_fork

c040ee13 T sys_fork

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

$ cat /proc/kallsyms | grep sys_ni_syscall

c045b9a8 T sys_ni_syscall

Випливаючи таким шляхом, можна додати новий оброблювач системного виклику в будь-яку невикористовувану позицію таблицыsys_call_table. Статично структуру таблиці можна детально розглянути в дереві вихідних кодів ядра для цільової платформи. Для приклада візьмемо дерево ядра 3.0. 9 (у листинге показані тільки деякі невикористовувані позиції, а коментарі виду __NR_# наприкінці рядків, що підказують номер системного виклику, були додані спеціально).

$ cat /usr/src/linux-3.0.9/arch/x86/kernel/syscall_table_32.S

ENTRY(sys_call_table)

.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */

.long sys_exit

.long ptregs_fork

...

.long sys_ni_syscall/* old break syscall holder *///17

.long sys_ni_syscall/* old stty syscall holder *///31

.long sys_ni_syscall/* old gtty syscall holder *///32

.long sys_ni_syscall/* 35 - old ftime syscall holder */ //35

...

.long sys_ni_syscall/* reserved for TUX *///222

.long sys_ni_syscall//223

.long sys_ni_syscall//251

.long sys_ni_syscall/* sys_vserver *///273

.long sys_ni_syscall/* 285 */ /* available *///285

...

.long sys_setns//346

Видно, що в цій версії ядра таблиця містить 347 позицій, з яких 21 не задіяна (у листинге наведені не всі вільні позиції). Ці позиції краще не використати повторно, тому що призначити новий системний виклик застарілий старому занадто ризиковано з погляду наступного використання системи: ніхто не гарантує, що на системі не будуть виконуватися старі програми, а наслідку їхніх звертань до «старого-нового» викликам не виявляться фатальними для ядра.

Динамічний аналіз невикористовуваних позицій у таблиці системних викликів виконується в представленому модулі ni-test.

Лістинг 1. Пошук невикористовуваних системних викликів.

#include <linux/module.h>

#include <linux/kallsyms.h>

#include <linux/uaccess.h>

static unsigned long asct = 0, anif = 0;

int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) {

if( 0 == strcmp( "sys_call_table", sym ) )

asct = addr;

else if( 0 == strcmp( "sys_ni_syscall", sym ) )

anif = addr;

return 0;

}

#define SYS_NR_MAX_OLD 340

// - цей розмір таблиці взятий досить довільно з ядра 2.6.37

static void show_entries( void ) {

int i, ni = 0;

char buf[ 200 ] = "";

for( i = 0; i <= SYS_NR_MAX_OLD; i++ ) {

unsigned long *taddr = ((unsigned long*)asct) + i;

if( *taddr == anif ) {

ni++;

sprintf( buf + strlen( buf ), "%03d, ", i );

}

}

printk( "! знайдено %d входів: %s\n", ni, buf );

}

static int __init init_driver( void ) {

kallsyms_on_each_symbol( symb_fn, NULL );

printk( "! адреса таблиці системних викликів = %lx\n", asct );

printk( "! адреса не реалізованих викликів = %lx\n", anif );

if( 0 == asct || 0 == anif ) {

printk( "! не знайдені символи ядра\n" );

return -EFAULT;

}

show_entries();

return -EPERM;

}

module_init( init_driver );

MODULE_DESCRIPTION( "test unused entries" );

MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" );

MODULE_LICENSE( "GPL" );

Тому що код досить прост і зрозумілий, і не вимагає докладного обговорення, то відразу перейдемо до його виконання на версії ядра 2.6.32:

$ sudo insmod ni-test.ko

insmod: error inserting 'ni-test.ko': -1 Operation not permitted


Подобные документы

  • Структура мережевої підсистеми Linux. Створення мережевого інтерфейсу. Передача пакетів та аналіз поведінки інтерфейсу. Протокол транспортного рівня. Використання модулів ядра. Вплив маршрутизації на процес розробки і налагодження мережевих модулів.

    курсовая работа [56,2 K], добавлен 23.05.2013

  • История создания, архитектура операционной системы и перечень возможностей, реализуемых в Linux. Инструментальные средства и цикл разработки новой версии ядра. Жизненный цикл патча. Структура принятия решений при добавлении новых функций (патчей) в ядро.

    лекция [303,8 K], добавлен 29.07.2012

  • UNIX - одна з найпопулярніших в світі операційних систем. Ключеві риси Linux. Порівняльні характеристики 32-розрядних операційних систем. Поверхневий огляд характеристик ядра Linux. Програмні характеристики: базові команди і утиліти, мови програмування.

    курсовая работа [33,3 K], добавлен 07.12.2010

  • Дослідження внутрішньої структури операційної системи Windows. Архітектура NT і структура ядра. Методи перехоплення функцій у режимі ядра та режимі користувача. Поняття драйверу. Пакети вводу-виводу. Оцінка стабільності та безпеки системи Windows.

    курсовая работа [239,3 K], добавлен 02.01.2014

  • Огляд засобів створення програмного забезпечення сучасних мікроконтролерів. Аналіз методів та налаштувань контролерів. Засоби генерації коду налаштувань. Детальний опис розробки програми генератора налаштувань ядра Cortex M4 та методики її тестування.

    курсовая работа [1,3 M], добавлен 20.05.2015

  • Несколько определений ERP системы. Происхождение, развитие, признаки. Что дает внедрение. Особенности разработки программ на Java. Проектирование и реализация модуля ERP системы. Экономическая схема торговой деятельности. Пример реализации схемы.

    курсовая работа [1,1 M], добавлен 10.09.2008

  • Переваги і проблеми дистанційної освіти на прикладі корпорації Microsoft. Створення власного web-додатку. Розробка технічних умов програмної системи, модуля пошуку та бронювання авіаквитків. Інтеграція модуля з сайтом. Використання javascript фреймворків.

    курсовая работа [1,0 M], добавлен 31.08.2014

  • Мониторинг системных вызовов. Системные вызовы shmget, shmat, shmctl, shmdt. Написание и внедрение модуля ядра. Выбор языка программирования. Структура программного обеспечения. Реализация мониторинга управления и удаления сегментов разделяемой памяти.

    курсовая работа [91,4 K], добавлен 24.06.2009

  • Особенности архитектуры MIPS компании MIPS Technology. Иерархия памяти. Обработка команд перехода. Адресная очередь. Переименование регистров. Обоснование выбора операционной системы. Perl-эмулятор и сборка ядра. Электрическая и пожарная безопасность.

    дипломная работа [180,2 K], добавлен 06.03.2013

  • История развития и отличительные признаки UNIX-системы. Основы информационной безопасности и особенности настройки исследуемой операционной системы, ее достоинства, недостатки и базовые права доступа. Общая характеристика безопасности ядра UNIX.

    реферат [599,5 K], добавлен 18.09.2013

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.