Linux описание на системните извиквания на ядрото. Man syscalls (2): Системни повиквания на Linux

Linux описание на системните извиквания на ядрото. Man syscalls (2): Системни повиквания на Linux

Най-често кодът за системно извикване с номер __NR_xxx, дефиниран в /usr/include/asm/unistd.h, може да се намери в програмен кодядрото на linux във функция sys_xxx(). (Таблицата за повиквания за i386 може да бъде намерена в /usr/src/linux/arch/i386/kernel/entry.S.) Има много изключения от това правило, главно поради факта, че повечето от старите системни повиквания се заменят с нови и без никаква система. На платформи със собствена емулация на OS, като parisc, sparc, sparc64 и alpha, има много допълнителни системни извиквания; mips64 също има пълен набор от 32-битови системни извиквания.

С течение на времето е имало промени в интерфейса на някои системни повиквания, ако е необходимо. Една от причините за тези промени беше необходимостта от увеличаване на размера на структурите или скаларните стойности, предавани на системно повикване. Поради тези промени на някои архитектури (а именно на стария 32-битов i386) се появиха различни групи от подобни системни извиквания (например, съкращавам(2) и truncate64(2)), които изпълняват едни и същи задачи, но се различават по размера на техните аргументи. (Както беше отбелязано, приложенията не са засегнати: обвивките на glibc вършат известна работа, за да задействат правилното системно извикване и това гарантира ABI съвместимост за по-стари двоични файлове.) Примери за системни извиквания, които имат множество версии:

*В момента има три различни версии статистика(2): sys_stat() (място __NR_oldstat), sys_newstat() (място __NR_stat) И sys_stat64() (място __NR_stat64), последното се използва в този момент. Подобна ситуация с lstat(2) и fstat(2). * Определено по подобен начин __NR_старо име, __NR_старо имеИ __NR_unameза разговори sys_старо име(), sys_uname() И sys_ново име(). * В Linux 2.0 се появи нова версия vm86(2) се наричат ​​новата и старата версия на ядрените процедури sys_vm86old() И sys_vm86(). * Linux 2.4 има нова версия getrlimit(2) извикват се новата и старата версия на ядрените процедури sys_old_getrlimit() (място __NR_getrlimit) И sys_getrlimit() (място __NR_ugetrlimit). * В Linux 2.4 размерът на полето за идентификатор на потребител и група е увеличен от 16 на 32 бита. Няколко системни извиквания са добавени в подкрепа на тази промяна (напр. chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), отхвърляйки по-ранни извиквания със същите имена, но без наставката "32". * Linux 2.4 добави поддръжка за достъп до големи файлове (чиито размери и отмествания не се побират в 32 бита) в приложения на 32-битова архитектура. Това изисква промени в системните извиквания, които работят с размери на файлове и отмествания. Добавени са следните системни повиквания: fcntl64(2), getdents64(2), статистика64(2), statfs64(2), truncate64(2) и техните двойници, които обработват файлови дескриптори или символни връзки. Тези системни повиквания премахват старите системни повиквания, които, с изключение на повикванията "stat", се наричат ​​по същия начин, но нямат суфикс "64".

На по-новите платформи, които имат само 64-битов достъп до файлове и 32-битов UID/GID (напр. alpha, ia64, s390x, x86-64), има само една версия на системните извиквания за UID/GID и достъп до файлове. На платформи (обикновено 32-битови платформи), които имат *64 и *32 повиквания, другите версии са остарели.

* Предизвикателства rt_sig*добавен към ядрото 2.2 за поддръжка на допълнителни сигнали в реално време (вижте сигнал(7)). Тези системни извиквания отхвърлят старите системни извиквания със същото име, но без префикса "rt_". * При системни повиквания изберете(2) и mmap(2) използвани са пет или повече аргумента, което създава проблеми при определяне на това как аргументите са били предадени на i386. В резултат на това, докато на други архитектури разговори sys_select() И sys_mmap() съвпада __NR_изборИ __NR_mmap, на i386 отговарят на old_select() И old_mmap() (процедури, използващи указател към блок от аргументи). В момента вече няма проблем с предаването на повече от пет аргумента и има __NR__новоизбор, което съвпада точно sys_select(), и същата ситуация с __NR_mmap2.

ВЛАДИМИР МЕШКОВ

Прихващане на системни повиквания в Linux OS

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

Системни повиквания

Да започнем с определение. Системните повиквания са набор от функции, реализирани в ядрото на ОС. Всяка заявка от приложението на потребителя в крайна сметка се превежда в системно повикване, което изпълнява исканото действие. Пълен списък на системните извиквания на Linux OS може да бъде намерен във файла /usr/include/asm/unistd.h. Нека да разгледаме общия механизъм за извършване на системни повиквания с пример. Нека източникът на приложението извика функцията creat(), за да създаде нов файл. Компилаторът, след като е срещнал извикване на тази функция, го преобразува в код на асемблер, зареждайки номера на системното извикване, съответстващ на тази функция, и нейните параметри в регистрите на процесора и след това извиква прекъсване 0x80. Следните стойности се зареждат в регистрите на процесора:

  • към EAX регистър– номер на системно повикване. Така че, за нашия случай, номерът на системното повикване ще бъде 8 (вижте __NR_creat);
  • към EBX регистър– първият параметър на функцията (за creat това е указател към низ, съдържащ името на създадения файл);
  • към ECX регистъра– втори параметър (права за достъп до файл).

Третият параметър се зарежда в EDX регистъра, в този случай го нямаме. За да се изпълни системно повикване в Linux OS, се използва функцията system_call, която е дефинирана във файла /usr/src/liux/arch/i386/kernel/entry.S. Тази функция е входната точка за всички системни повиквания. Ядрото отговаря на прекъсването 0x80, като извиква функцията system_call, която по същество е манипулатор на прекъсване 0x80.

За да сме сигурни, че сме на прав път, нека напишем малък тестов фрагмент на асемблер. В него ще видим в какво се превръща функцията creat() след компилация. Нека именуваме файла test.S. Ето съдържанието му:

Globl_start

Текст

начало:

Заредете номера на системното повикване в EAX регистъра:

movl $8, %eax

Към регистъра EBX - първият параметър, указател към низ с името на файла:

movl $ име на файл, %bx

В регистъра ECX - вторият параметър, права за достъп:

movl $0, %ecx

Ние наричаме прекъсване:

int $0x80

Излизаме от програмата. За да направите това, извикайте функцията exit(0):

movl $1, %eax movl $0, %ebx int $0x80

В сегмента с данни посочете името на файла, който ще бъде създаден:

Данни

име на файл: .string "file.txt"

Компилиране:

gcc -c тест.S

ld -s -o тест тест.о

Тестът на изпълнимия файл ще се появи в текущата директория. Изпълнявайки го, ние ще създаваме нов файлс име file.txt.

Сега да се върнем към механизма за системни повиквания. И така, ядрото извиква манипулатора на прекъсвания 0x80 - функцията system_call. System_call избутва копия на регистрите, съдържащи параметри за повикване, в стека с помощта на макроса SAVE_ALL и извиква желаната системна функция с командата за извикване. Таблицата с указатели към функции на ядрото, които изпълняват системни извиквания, се намира в масива sys_call_table (вижте файла arch/i386/kernel/entry.S). Номерът на системното повикване, който е в EAX регистъра, е индексът в този масив. Така, ако стойността 8 е в EAX, ще бъде извикана функцията на ядрото sys_creat(). Защо е необходим макросът SAVE_ALL? Обяснението тук е много просто. Тъй като почти всички системни функции на ядрото са написани на C, те търсят своите параметри в стека. И параметрите се изпращат в стека с помощта на макроса SAVE_ALL! Върнатата стойност на системното извикване се съхранява в EAX регистъра.

Сега нека разберем как да прихванем системното повикване. Механизмът на зареждаемите модули на ядрото ще ни помогне с това. Въпреки че по-рано обсъдихме разработването и използването на модули на ядрото, от съображения за последователност, ще разгледаме накратко какво представлява модулът на ядрото, от какво се състои и как взаимодейства със системата.

Зареждаем модул на ядрото

Зареждаемият модул на ядрото (нека го обозначим с LKM - Зареждаем модул на ядрото) е програмен код, който се изпълнява в пространството на ядрото. Основната характеристика на LKM е възможността за динамично зареждане и разтоварване без необходимост от рестартиране на цялата система или повторно компилиране на ядрото.

Всеки LKM се състои от две основни функции (минимум):

  • функция за инициализация на модула. Извиква се, когато LKM се зареди в паметта:

int init_module(void) ( ... )

  • функция за разтоварване на модула:

void cleanup_module(void) ( ... )

Ето пример за прост модул:

#define МОДУЛ

#включи

init_module(void)

printk("Здравей свят");

връщане 0;

void cleanup_module(void)

printk("от");

Компилирайте и заредете модула. Зареждането на модул в паметта се извършва от командата insmod:

gcc -c -O3 helloworld.c

insmod helloworld.o

Информация за всички модули, заредени в момента в системата, се намира във файла /proc/modules. За да сте сигурни, че модулът е зареден, напишете cat /proc/modules или lsmod. Командата rmmod разтоварва модул:

rmmod здравей свят

Алгоритъм за прихващане на системни повиквания

За да се реализира модул, който прихваща системно повикване, е необходимо да се дефинира алгоритъм за прихващане. Алгоритъмът е следният:

  • запишете указател към оригиналното (оригиналното) повикване, за да може да бъде възстановено;
  • създаване на функция, която реализира ново системно извикване;
  • замени повикванията в таблицата на системните повиквания sys_call_table, т.е. настройте подходящ указател към новото системно извикване;
  • в края на работата (когато модулът е разтоварен), възстановете оригиналното системно повикване, като използвате предварително запазения указател.

Проследяването ви позволява да разберете кои системни извиквания са включени, когато потребителското приложение работи. Чрез проследяване можете да определите кое системно повикване трябва да бъде прихванато, за да поемете контрола над приложението. Пример за използване на програмата за проследяване ще бъде разгледан по-долу.

Сега имаме достатъчно информация, за да започнем да изучаваме примери за реализации на модули, които прихващат системни повиквания.

Примери за прихващане на системни повиквания

Забранете създаването на директории

Когато се създаде директория, се извиква функцията на ядрото sys_mkdir. Параметърът е низ, съдържащ името създадена директория. Помислете за кода, който прихваща съответното системно извикване.

#включи

#включи

#включи

Експортиране на таблицата на системните повиквания:

extern void *sys_call_table;

Нека дефинираме указател за съхраняване на оригиналното системно извикване:

int (*orig_mkdir)(const char *path);

Нека създадем собствено системно извикване. Нашето извикване не прави нищо, само връща нула:

int own_mkdir(const char *path)

връщане 0;

По време на инициализацията на модула запазваме указателя към оригиналното повикване и заместваме системното повикване:

int init_module()

orig_mkdir=sys_call_table;

sys_call_table=собствен_mkdir; връщане 0;

При разтоварване възстановяваме оригиналното повикване:

void cleanup_module()

Sys_call_table=orig_mkdir;

Ще запазим кода във файла sys_mkdir_call.c. За да получите обектния модул, нека създадем Makefile със следното съдържание:

CC=gcc

CFLAGS=-O3 -Wall -fomit-frame-pointer

sys_mkdir_call.o: sys_mkdir_call.c

$(CC) -c $(CFLAGS) $(MODFLAGS) sys_mkdir_call.c

Командата make ще създаде модул на ядрото. След като го заредим, ще се опитаме да създадем директория с командата mkdir. Както можете да видите, нищо не се случва. Командата не работи. За да възстановите функционалността му, е достатъчно да разтоварите модула.

Предотвратяване на четене на файл

За да прочетете файл, той първо трябва да бъде отворен с функцията за отваряне. Лесно е да се досетите, че тази функция съответства на системното извикване sys_open. Чрез прихващането му можем да защитим файла от четене. Помислете за внедряването на модула за прихващане.

#включи

#включи

#включи

#включи

#включи

#включи

#включи

extern void *sys_call_table;

Указател за запазване на оригиналното системно повикване:

int (*orig_open)(const char *pathname, int флаг, int режим);

Първият параметър на функцията за отваряне е името на файла за отваряне. Новото системно извикване трябва да сравни този параметър с името на файла, който искаме да защитим. Ако имената съвпадат, ще бъде симулирана грешка при отваряне на файл. Нашето ново системно повикване изглежда така:

int own_open(const char *pathname, int флаг, int режим)

Тук поставяме името на файла, който ще отворим:

char *път на_ядрото;

Името на файла, който искаме да защитим:

char hide="test.txt"

Разпределете памет и копирайте там името на файла, който ще отворите:

kernel_path=(char *)kmalloc(255,GFP_KERNEL);

copy_from_user(път на_ядрото, пътека, 255);

Сравнете:

if(strstr(kernel_path,(char *)&hide) != NULL) (

Освобождаваме памет и връщаме код за грешка, ако имената съвпадат:

безплатен (път_на_ядрото);

връщане -ENOENT;

иначе(

Ако имената не съвпадат, извикваме оригиналното системно извикване, за да изпълним стандартната процедура за отваряне на файл:

безплатен (път_на_ядрото);

връщане orig_open(път, флаг, режим);

int init_module()

orig_open=sys_call_table;

sys_call_table=own_open;

връщане 0;

void cleanup_module()

sys_call_table=orig_open;

Нека запазим кода във файла sys_open_call.c и създадем Makefile, за да получим обектния модул:

CC=gcc

CFLAGS=-O2 -Wall -fomit-frame-pointer

MODFLAGS = -D__KERNEL__ -DMODULE -I/usr/src/linux/include

sys_open_call.o: sys_open_call.c

$(CC) -c $(CFLAGS) $(MODFLAGS) sys_open_call.c

В текущата директория създайте файл, наречен test.txt, заредете модула и въведете командата cat test.txt. Системата ще съобщи, че няма файл с това име.

Честно казано, такава защита е лесна за заобикаляне. Достатъчно е да преименувате файла с командата mv и след това да прочетете съдържанието му.

Скриване на запис на файл в директория

Нека да определим кое системно повикване отговаря за четенето на съдържанието на директорията. За да направим това, ще напишем друг тестов фрагмент, който чете текущата директория:

/* Файл dir.c*/

#включи

#включи

int main()

DIR*d;

struct директория *dp;

d = opendir(".");

dp = readdir(d);

връщане 0;

Вземете изпълнимия модул:

gcc -o dir dir.c

и го проследи:

strace ./реж

Нека да разгледаме предпоследния ред:

getdents(6, /* 4 записа*/, 3933) = 72;

Съдържанието на директорията се чете от функцията getdents. Резултатът се съхранява като списък от struct dirent структури. Вторият параметър на тази функция е указател към този списък. Функцията връща дължината на всички записи в директорията. В нашия пример функцията getdents определи, че има четири записа в текущата директория – „.“, „..“ и нашите два файла, изпълнимия модул и изходния код. Дължината на всички записи в директорията е 72 байта. Информацията за всеки запис се съхранява, както казахме, в структурата struct dirent. Ние се интересуваме от две полета на тази структура:

  • d_reclen– рекорден размер;
  • d_name- име на файл.

За да скриете запис във файл (с други думи, да го направите невидим), трябва да прихванете системното извикване sys_getdents, да намерите съответния запис в списъка с получени структури и да го изтриете. Помислете за кода, който изпълнява тази операция (авторът на оригиналния код е Михал Залевски):

extern void *sys_call_table;

int (*orig_getdents)(u_int, struct dirent *, u_int);

Нека дефинираме нашето системно извикване.

int own_getdents(u_int fd, struct dirent *dirp, u_int count)

неподписан int tmp, n;

int t;

Присвояването на променливите ще бъде показано по-долу. Освен това се нуждаем от структури:

struct dirent *dirp2, *dirp3;

Името на файла, който искаме да скрием:

char hide="нашия.файл";

Определете дължината на записите в директорията:

tmp=(*orig_getdents)(fd,dirp,count);

ако (tmp>0)(

Нека отделим памет за структурата в пространството на ядрото и да копираме съдържанието на директорията в нея:

dirp2=(struct dirent *)kmalloc(tmp,GFP_KERNEL);

копиране_от_потребител(dirp2,dirp,tmp);

Нека използваме втората структура и да запазим стойността на дължината на записите в директорията:

dirp3=dirp2;

t=tmp;

Нека започнем да търсим нашия файл:

докато (t>0) (

Прочитаме дължината на първия запис и определяме оставащата дължина на записите в директорията:

n=dirp3->d_reclen;

t=n;

Проверяваме дали името на файла от текущия запис съвпада с това, което търсим:

if(strstr((char *)&(dirp3->d_name),(char *)&hide) != NULL) (

Ако е така, презаписваме записа и изчисляваме нова стойност за дължината на записите в директорията:

memcpy(dirp3,(char *)dirp3+dirp3->d_reclen,t);

tmp-=n;

Позиционирайте показалеца към следващия запис и продължете да търсите:

dirp3=(struct dirent *)((char *)dirp3+dirp3->d_reclen);

Връщаме резултата и освобождаваме паметта:

copy_to_user(dirp,dirp2,tmp);

безплатно (dirp2);

Връщаме стойността на дължината на записите в директорията:

връщане tmp;

Функциите за инициализация и разтоварване на модула имат стандартна форма:

init_module(void)

orig_getdents=sys_call_table;

sys_call_table=own_getdents;

връщане 0;

void cleanup_module()

sys_call_table=orig_getdents;

Нека запазим изходния текст във файла sys_call_getd.c и създадем Makefile със следното съдържание:

CC=gcc

модул = sys_call_getd.o

CFLAGS = -O3 -стена

linux=/usr/src/linux

MODFLAGS = -D__KERNEL__ -DMODULE -I$(LINUX)/include

sys_call_getd.o: sys_call_getd.c $(CC) -c

$(CFLAGS) $(MODFLAGS) sys_call_getd.c

Нека създадем our.file в текущата директория и да заредим модула. Файлът изчезва, което трябваше да се докаже.

Както разбирате, не е възможно да се разгледа пример за прихващане на всяко системно повикване в рамките на една статия. Ето защо, за тези, които се интересуват от този въпрос, препоръчвам да посетят сайтовете:

Там можете да намерите по-сложни и интересни примери за прихващане на системни повиквания. Пишете за всички коментари и предложения във форума на списанието.

При изготвянето на статията са използвани материали от сайта

Този материал е модификация на едноименната статия на Владимир Мешков, публикувана в списание "Системен администратор"

Този материал е копие на статии на Владимир Мешков от списание "Системен администратор". Тези статии могат да бъдат намерени на връзките по-долу. Също така някои примери за изходни текстове на програмата бяха променени - подобрени, финализирани. (Пример 4.2 е силно модифициран, тъй като трябваше да бъде прихванато малко по-различно системно повикване) URL адреси: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/ качен/a3.pdf

Имате въпроси? Тогава сте тук: [имейл защитен]

  • 2. Зареждаем модул на ядрото
  • 4. Примери за прихващане на системни повиквания, базирани на LKM
    • 4.1 Деактивирайте създаването на директория

1. Общ изглед на Linux архитектурата

Най-общият изглед ни позволява да видим двустепенен модел на системата. ядро<=>progs В центъра (отляво) е ядрото на системата. Ядрото взаимодейства директно с компютърния хардуер, като изолира приложните програми от архитектурните характеристики. Ядрото има набор от услуги, предоставени на приложните програми. Услугите на ядрото включват I/O операции (отваряне, четене, писане и управление на файлове), създаване и управление на процеси, тяхното синхронизиране и междупроцесна комуникация. Всички приложения изискват услуги на ядрото чрез системни повиквания.

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

Взаимодействието с ядрото се осъществява чрез стандартния интерфейс за системни повиквания. Интерфейсът за системни повиквания е набор от услуги на ядрото и определя формата на заявките за услуги. Процес изисква услуга чрез системно извикване на конкретна процедура на ядрото, което изглежда като обикновено извикване на библиотечна функция. Ядрото изпълнява заявката от името на процеса и връща необходимите данни на процеса.

В горния пример програмата отваря файл, чете данни от него и затваря файла. В този случай операцията за отваряне (open), четене (read) и затваряне (close) на файл се извършва от ядрото по искане на задачата, а отварянето (2), четенето (2) и затварянето (2 ) функциите са системни повиквания.

/* Източник 1.0 */ #include main () ( int fd; char buf; /* Отворете файла - вземете връзка (дескриптор на файл) fd */ fd = open("file1",O_RDONLY); /* Прочетете 80 знака в буфера buf */ read( fd, buf, sizeof(buf)); /* Затваряне на файла */ close(fd); ) /* EOF */ Пълен списък на системните извиквания на OS Linux може да се намери в /usr/include/asm/unistd.h . Нека сега да разгледаме механизма за извършване на системни повиквания този пример. Компилаторът, след като е срещнал функцията open() за отваряне на файл, го преобразува в код на асемблер, зареждайки номера на системното извикване, съответстващ на тази функция и нейните параметри, в регистрите на процесора и след това извиква прекъсване 0x80. Следните стойности се зареждат в регистрите на процесора:

  • към регистъра EAX - номерът на системното повикване. И така, за нашия случай номерът на системното повикване е 5 (вижте __NR_open).
  • към регистъра EBX - първият параметър на функцията (за open() е указател към низ, съдържащ името на файла, който се отваря.
  • към регистъра ECX - вторият параметър (права за достъп до файл)
Третият параметър се зарежда в EDX регистъра, в този случай го нямаме. За извършване на системно повикване в OS Linux се използва функцията system_call, която е дефинирана (в зависимост от архитектурата в този случай i386) във файла /usr/src/linux/arch/i386/kernel/entry.S. Тази функция е входната точка за всички системни повиквания. Ядрото отговаря на прекъсването 0x80, като извиква функцията system_call, която по същество е манипулатор на прекъсване 0x80.

За да сме сигурни, че сме на прав път, нека да разгледаме кода за функцията open() в системната библиотека на libc:

# gdb -q /lib/libc.so.6 (gdb) disas open Дъмп на асемблерния код за отворена функция: 0x000c8080 : извикайте 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : добавете $0x6423b,%ecx 0x000c808b : cmpl $0x0.0x1a84(%ecx) 0x000c8092 : jne 0xc80b1 0x000c8094 : натиснете %ebx 0x000c8095 : mov 0x10(%esp,1),%edx 0x000c8099 : mov 0xc(%esp,1),%ecx 0x000c809d : mov 0x8(%esp,1),%ebx 0x000c80a1 : mov $0x5,%eax 0x000c80a6 : int $0x80 ... Както можете да видите в последните редове, параметрите се подават към регистрите EDX, ECX, EBX, а номерът на системното повикване, както вече знаем, 5, се поставя в последния EAX регистър.

Сега да се върнем към механизма за системни повиквания. И така, ядрото извиква манипулатора на прекъсвания 0x80 - функцията system_call. System_call избутва копия на регистри, съдържащи параметри за повикване, в стека с помощта на макроса SAVE_ALL и извиква желаната системна функция с командата за извикване. Таблицата с указатели към функции на ядрото, които изпълняват системни извиквания, се намира в масива sys_call_table (вижте файла arch/i386/kernel/entry.S). Номерът на системното повикване, който се намира в EAX регистъра, е индексът в този масив. Така, ако EAX съдържа стойност 5, ще бъде извикана функцията на ядрото sys_open(). Защо е необходим макросът SAVE_ALL? Обяснението тук е много просто. Тъй като почти всички системни функции на ядрото са написани на C, те търсят своите параметри в стека. И параметрите се избутват в стека с SAVE_ALL! Върнатата стойност на системното извикване се съхранява в EAX регистъра.

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

2. Зареждаем модул на ядрото

Зареждаем модул на ядрото (LKM - зареждаем модул на ядрото) е код, който се изпълнява в пространството на ядрото. Основната характеристика на LKM е възможността за динамично зареждане и разтоварване без необходимост от рестартиране на цялата система или повторно компилиране на ядрото.

Всеки LKM се състои от две основни функции (минимум):

  • функция за инициализация на модула. Извиква се, когато LKM се зареди в паметта: int init_module(void) ( ... )
  • функция за разтоварване на модул: void cleanup_module(void) ( ... )
Ето пример за най-простия модул: /* Source 2.0 */ #include int init_module(void) ( printk("Hello World\n"); return 0; ) void cleanup_module(void) ( printk("Bye\n"); ) /* EOF */ Компилирайте и заредете модула. Зареждането на модул в паметта се извършва с командата insmod, а прегледът на заредените модули с командата lsmod: # gcc -c -DMODULE -I /usr/src/linux/include/ src-2.0.c # insmod src-2.0.o Предупреждение: зареждането на src-2.0 .o ще опетни ядрото: няма лиценз Модулът src-2.0 е зареден, с предупреждения # dmesg | tail -n 1 Здравей свят # lsmod | grep src src-2.0 336 0 (неизползван) # rmmod src-2.0 # dmesg | опашка -n 1 Чао

3. Алгоритъм за прихващане на системно повикване на базата на LKM

За да се реализира модул, който прихваща системно повикване, е необходимо да се дефинира алгоритъм за прихващане. Алгоритъмът е следният:
  • запишете указател към оригиналното (оригиналното) повикване, за да може да бъде възстановено
  • създайте функция, която реализира новото системно извикване
  • заместване на повикванията в таблицата на системните повиквания sys_call_table, т.е. задаване на съответния указател към ново системно извикване
  • в края на работата (когато модулът е разтоварен), възстановете оригиналното системно повикване, като използвате предварително запазения указател
Проследяването ви позволява да разберете кои системни извиквания участват в работата на потребителското приложение. Чрез проследяване можете да определите кое системно повикване трябва да бъде прихванато, за да поемете контрола над приложението. # ltrace -S ./src-1.0 ... отворено ("файл1", 0, 01 SYS_open("file1", 0, 01) = 3<... open resumed>) = 3 четене (3, SYS_read(3, "123\n", 80) = 4<... read resumed>"123\n", 80) = 4 затваряне(3 SYS_close(3) = 0<... close resumed>) = 0 ... Сега имаме достатъчно информация, за да започнем да изучаваме примери за реализации на модули, които прихващат системни повиквания.

4. Примери за прихващане на системни повиквания, базирани на LKM

4.1 Деактивирайте създаването на директория

Когато се създаде директория, се извиква функцията на ядрото sys_mkdir. Параметърът е низ, съдържащ името на директорията, която ще бъде създадена. Помислете за кода, който прихваща съответното системно извикване. /* Източник 4.1 */ #include #включи #включи /* Експортиране на таблицата на системните повиквания */ extern void *sys_call_table; /* Дефиниране на указател за съхраняване на оригиналното повикване */ int (*orig_mkdir)(const char *path); /* Създаване на собствено системно извикване. Нашето извикване не прави нищо, просто връща null */ int own_mkdir(const char *path) ( return 0; ) /* По време на инициализацията на модула, запишете указателя към оригиналното извикване и заменете системното извикване */ int init_module(void) ( orig_mkdir =sys_call_table; sys_call_table=own_mkdir; printk("sys_mkdir заменен\n"); return(0); ) /* При разтоварване, възстановяване на оригиналното повикване */ void cleanup_module(void) ( sys_call_table=orig_mkdir; printk("sys_mkdir преместен обратно) \n "); ) /* EOF */ За да получите обектния модул, изпълнете следната команда и изпълнете някои експерименти в системата: # gcc -c -DMODULE -I/usr/src/linux/include/ src-3.1. c # dmesg | tail -n 1 sys_mkdir заменен # mkdir тест # ls -ald тест ls: тест: Няма такъв файл или директория # rmmod src-3.1 # dmesg | опашка -n 1 sys_mkdir преместен назад # mkdir тест # ls -ald тест drwxr-xr-x 2 корен корен 4096 2003-12-23 03:46 тест Както можете да видите, командата "mkdir" не работи или по-скоро нищо случва се. Разтоварването на модула е достатъчно за възстановяване на функционалността на системата. Какво е направено по-горе.

4.2 Скриване на запис на файл в директория

Нека да определим кое системно повикване отговаря за четенето на съдържанието на директорията. За да направим това, ще напишем друг тестов фрагмент, който чете текущата директория: /* Източник 4.2.1 */ #include #включи int main() ( DIR *d; struct dirent *dp; d = opendir("."); dp = readdir(d); return 0; ) /* EOF */ Вземете изпълнимия файл и трасирайте: # gcc -o src -3.2.1 src-3.2.1.c # ltrace -S ./src-3.2.1 ... opendir("." SYS_open(".", 100352, 010005141300) = 3 SYS_fstat64(3, 0xbffff79c, 0x4014c2c0, 3, 0xbffff874) = 0 SYS_fcntl64(3, 2, 1, 1, 0x4014c2c0) = 0 SY S _brk(NULL) = 0x080495f4 SYS_brk(0x0806a5f4 ) = 0x0806a5f4 SYS_brk(NULL) = 0x0806a5f4 SYS_brk(0x0806b000) = 0x0806b000<... opendir resumed>) = 0x08049648 readdir(0x08049648 SYS_getdents64(3, 0x08049678, 4096, 0x40014400, 0x4014c2c0) = 528<... readdir resumed>) = 0x08049678 ... Обърнете внимание на последния ред. Съдържанието на директорията се чете от функцията getdents64 (getdents е възможна в други ядра). Резултатът се съхранява като списък от структури от тип struct dirent, а самата функция връща дължината на всички записи в директорията. Ние се интересуваме от две полета на тази структура:
  • d_reclen - рекорден размер
  • d_name - име на файл
За да скриете файлов запис за файл (с други думи, да го направите невидим), трябва да прихванете системното извикване sys_getdents64, да намерите съответния запис в списъка с получени структури и да го изтриете. Помислете за кода, който изпълнява тази операция (автор оригинален код- Михал Залевски): /* Източник 4.2.2 */ #include #включи #включи #включи #включи #включи #включи #включи extern void *sys_call_table; int (*orig_getdents)(u_int fd, struct dirent *dirp, u_int count); /* Дефинирайте нашето системно извикване */ int own_getdents(u_int fd, struct dirent *dirp, u_int count) ( unsigned int tmp, n; int t; struct dirent64 ( int d_ino1,d_ino2; int d_off1,d_off2; unsigned short d_reclen; unsigned char d_type; char d_name; ) *dirp2, *dirp3; /* Името на файла, който искаме да скрием */ char hide = "file1"; /* Определяне на дължината на записите в директорията */ tmp = (*orig_getdents )(fd,dirp ,count); if (tmp>0) ( /* Разпределете памет за структурата на пространството на ядрото и копирайте съдържанието на директорията в нея */ dirp2 = (struct dirent64 *)kmalloc(tmp,GFP_KERNEL) ; copy_from_user(dirp2,dirp,tmp) ; /* Извикване на втората структура и запазване на стойността на дължината на записите в директорията */ dirp3 = dirp2; t = tmp; /* Започнете да търсите нашия файл */ докато (t >0) ( /* Прочетете дължината на първия запис и определете оставащата дължина на записите в директорията */ n = dirp3->d_reclen; t -= n; /* Проверете дали името на файла от текущия запис съвпада с този, който търсим */ if (strstr((char *)&(dirp3->d_name), (char * )&hide) != NULL) ( /* Ако е така, презапишете записа и изчислете нова стойност за дължина на записите в директорията */ memcpy(dirp3, (char *)dirp3+dirp3->d_reclen, t); tmp -=n; ) /* Позиционирайте показалеца към следващия запис и продължете търсенето */ dirp3 = (struct dirent64 *)((char *)dirp3+dirp3->d_reclen); ) /* Връща резултат и освобождава памет */ copy_to_user(dirp,dirp2,tmp); безплатно (dirp2); ) /* Връща стойността на дължината на записите в директорията */ return tmp; ) /* Функциите за инициализация и разтоварване на модула имат стандартна форма */ int init_module(void) ( orig_getdents = sys_call_table; sys_call_table=own_getdents; return 0; ) void cleanup_module() ( sys_call_table=orig_getdents; ) /* EOF */ След компилиране на това код, забележете как "file1" изчезва, което искахме да докажем.

5. Метод за директен достъп до адресното пространство на ядрото /dev/kmem

Нека първо да разгледаме теоретично как се извършва прихващането чрез метода на директен достъп до адресното пространство на ядрото и след това да преминем към практическо изпълнение.

Директният достъп до адресното пространство на ядрото се осигурява от файла на устройството /dev/kmem. Този файл показва цялото налично виртуално адресно пространство, включително суап дяла (swap-област). За работа с kmem файл се използват стандартни системни функции - open(), read(), write(). Отваряйки /dev/kmem по стандартния начин, можем да се обърнем към всеки адрес в системата, като го зададем като отместване в този файл. Този методе проектиран от Силвио Чезаре.

Системните функции се осъществяват чрез зареждане на функционалните параметри в регистрите на процесора и след това извикване на софтуерно прекъсване 0x80. Манипулаторът за това прекъсване, функцията system_call, избутва параметрите на извикването в стека, извлича адреса на извиканата системна функция от таблицата sys_call_table и прехвърля контрола на този адрес.

С пълен достъп до адресното пространство на ядрото можем да получим цялото съдържание на таблицата на системните повиквания, т.е. адреси на всички системни функции. Променяйки адреса на всяко системно повикване, ние го прихващаме. Но за това трябва да знаете адреса на таблицата или, с други думи, отместването във файла /dev/kmem, където се намира тази таблица.

За да определите адреса на таблицата sys_call_table, първо трябва да изчислите адреса на функцията system_call. Тъй като дадена функцияе манипулатор на прекъсвания, нека да разгледаме как се обработват прекъсванията в защитен режим.

В реален режим, когато регистрира прекъсване, процесорът има достъп до векторната таблица на прекъсванията, която винаги е в самото начало на паметта и съдържа двусловни адреси на манипулатори на прекъсвания. В защитен режим аналогът на векторната таблица на прекъсванията е Interrupt Descriptor Table (IDT), намираща се в операционната система на защитения режим. За да може процесорът да има достъп до тази таблица, неговият адрес трябва да бъде зареден в регистъра на таблицата с дескриптори на прекъсвания (IDTR). Таблицата IDT съдържа дескриптори на манипулатори на прекъсвания, които по-специално включват техните адреси. Тези дескриптори се наричат ​​шлюзове (шлюзове). Процесорът, регистрирайки прекъсване, извлича шлюза от IDT по неговия номер, определя адреса на манипулатора и прехвърля управлението към него.

За да се изчисли адреса на функцията system_call от таблицата IDT, е необходимо да се извлече gate за прекъсване int $0x80, а от него адреса на съответния манипулатор, т.е. адрес на функцията system_call. Във функцията system_call таблицата system_call_table е достъпна чрез командата call<адрес_таблицы>(,%eax,4). След като намерихме кода на операцията (сигнатурата) на тази команда във файла /dev/kmem, ще намерим и адреса на таблицата на системните повиквания.

За да определим кода на операцията, нека използваме дебъгера и разглобим функцията system_call:

# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call Дъмп на асемблерния код за функция system_call: 0xc0194cbc : натиснете %eax 0xc0194cbd : cld 0xc0194cbe : натиснете %es 0xc0194cbf : натиснете %ds 0xc0194cc0 : натиснете %eax 0xc0194cc1 : натиснете %ebp 0xc0194cc2 : натиснете %edi 0xc0194cc3 : натиснете %esi 0xc0194cc4 : натиснете %edx 0xc0194cc5 : натиснете %ecx 0xc0194cc6 : натиснете %ebx 0xc0194cc7 : mov $0x18,%edx 0xc0194ccc : mov %edx,%ds 0xc0194cce : mov %edx,%es 0xc0194cd0 : mov $0xffffe000,%ebx 0xc0194cd5 : и %esp,%ebx 0xc0194cd7 : testb $0x2.0x18(%ebx) 0xc0194cdb : jne 0xc0194d3c 0xc0194cdd : cmp $0x10e,%eax 0xc0194ce2 : jae 0xc0194d69 0xc0194ce8 : извикване на *0xc02cbb0c(,%eax,4) 0xc0194cef : mov %eax,0x18(%esp,1) 0xc0194cf3 : nop Край на дъмп на асемблер. Редът "call *0xc02cbb0c(,%eax,4)" е извикване на таблицата sys_call_table. Стойността 0xc02cbb0c е адресът на таблицата (най-вероятно вашите числа ще бъдат различни). Вземете операционния код на тази команда: (gdb) x/xw system_call+44 0xc0194ce8 : 0x0c8514ff Открихме операционния код на командата sys_call_table. Равно е на \xff\x14\x85. 4-те байта след него са адреса на таблицата. Можете да проверите това, като въведете командата: (gdb) x/xw system_call+44+3 0xc0194ceb : 0xc02cbb0c По този начин, намирайки последователността \xff\x14\x85 във файла /dev/kmem и четейки 4-те байта след нея, получаваме адреса на таблицата за системни повиквания sys_call_table. Знаейки неговия адрес, можем да получим съдържанието на тази таблица (адресите на всички системни функции) и да променим адреса на всяко системно извикване, като го прихванем.

Помислете за псевдокода, който изпълнява операцията по прихващане:

readaddr(old_syscall, scr + SYS_CALL*4, 4); writeaddr(new_syscall, scr + SYS_CALL*4, 4); Функцията readaddr чете адреса на системното повикване от таблицата на системните повиквания и го съхранява в променливата old_syscall. Всеки запис в таблицата sys_call_table отнема 4 байта. Необходимият адрес се намира на отместването sct + SYS_CALL*4 във файла /dev/kmem (тук sct е адресът на таблицата sys_call_table, SYS_CALL е серийният номер на системното повикване). Функцията writeaddr презаписва адреса на системното извикване SYS_CALL с адреса на функцията new_syscall и всички извиквания към системното извикване SYS_CALL ще бъдат обслужвани от тази функция.

Изглежда, че всичко е просто и целта е постигната. Все пак нека помним, че работим в адресното пространство на потребителя. Ако поставим нова системна функция в това адресно пространство, когато извикаме тази функция, ще получим красиво съобщение за грешка. Оттук и заключението - ново системно извикване трябва да бъде поставено в адресното пространство на ядрото. За да направите това, трябва да: получите блок памет в пространството на ядрото, да поставите ново системно извикване в този блок.

Можете да разпределите памет в пространството на ядрото с помощта на функцията kmalloc. Но не можете директно да извикате функция на ядрото от адресното пространство на потребителя, затова използваме следния алгоритъм:

  • знаейки адреса на таблицата sys_call_table, получаваме адреса на някакво системно повикване (например sys_mkdir)
  • ние дефинираме функция, която извършва извикване на функцията kmalloc. Тази функция връща указател към блок памет в адресното пространство на ядрото. Нека наречем тази функция get_kmalloc
  • съхранява първите N байта от системното извикване sys_mkdir, където N е размерът на функцията get_kmalloc
  • презапишете първите N байта на извикването sys_mkdir с функцията get_kmalloc
  • ние изпълняваме повикването към системното повикване sys_mkdir, като по този начин стартираме функцията get_kmalloc за изпълнение
  • възстановяване на първите N байта от системното извикване sys_mkdir
В резултат на това ще имаме блок памет, разположен в пространството на ядрото.

Но за да реализираме този алгоритъм, се нуждаем от адреса на функцията kmalloc. Можете да го намерите по няколко начина. Най-простият е да прочетете този адрес от файла System.map или да го определите с помощта на gdb дебъгера (print &kmalloc). Ако ядрото има активирана поддръжка на модули, kmalloc адресът може да бъде определен с помощта на функцията get_kernel_syms(). Тази опция ще бъде обсъдена допълнително. Ако няма поддръжка за модули на ядрото, адресът на функцията kmalloc ще трябва да се търси чрез кода на операцията на командата kmalloc call - подобно на това, което беше направено за таблицата sys_call_table.

Функцията kmalloc приема два параметъра: размера на заявената памет и GFP спецификатора. За да намерим кода на операцията, ще използваме програмата за отстраняване на грешки и ще разглобим всяка функция на ядрото, която съдържа извикване на функцията kmalloc.

# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register Дъмп на асемблерния код за функция inter_module_register: 0xc01a57b4 : натиснете %ebp 0xc01a57b5 : натиснете %edi 0xc01a57b6 : натиснете %esi 0xc01a57b7 : натиснете %ebx 0xc01a57b8 : sub $0x10,%esp 0xc01a57bb : mov 0x24(%esp,1),%ebx 0xc01a57bf : mov 0x28(%esp,1),%esi 0xc01a57c3 : mov 0x2c(%esp,1),%ebp 0xc01a57c7 : movl $0x1f0,0x4(%esp,1) 0xc01a57cf : movl $0x14,(%esp,1) 0xc01a57d6 : обадете се на 0xc01bea2a ... Без значение какво прави функцията, основното в нея е това, от което се нуждаем - извикване на функцията kmalloc. Обърнете внимание на последните редове. Първо, параметрите се зареждат в стека (регистърът esp сочи към върха на стека), след което следва извикването на функцията. Спецификаторът на GFP се зарежда първо в стека ($0x1f0,0x4(%esp,1). За версии на ядрото 2.4.9 и по-нови тази стойност е 0x1f0. Намерете кода за операция за тази команда: (gdb) x/xw inter_module_register+ 19 0xc01a57c7 : 0x042444c7 Ако намерим този код за операция, можем да изчислим адреса на функцията kmalloc. На пръв поглед адресът на тази функция е аргумент на инструкцията за извикване, но това не е съвсем вярно. За разлика от функцията system_call, тук инструкцията не е адресът kmalloc, а отместването към него спрямо текущия адрес. Ще проверим това, като дефинираме кода на операцията на извикването на командата 0xc01bea2a: (gdb) x/xw inter_module_register+34 0xc01a57d6 : 0x01924fe8 Първият байт е e8, който е кодът на операцията на инструкцията за повикване. Намерете стойността на аргумента на тази команда: (gdb) x/xw inter_module_register+35 0xc01a57d7 : 0x0001924f Сега, ако добавим текущия адрес 0xc01a57d6, отместването 0x0001924f и 5 байта от командата, получаваме необходимия адрес на функцията kmalloc - 0xc01bea2a.

Това приключва теоретичните изчисления и, използвайки горната техника, ще прихванем системното извикване sys_mkdir.

6. Пример за прихващане с помощта на /dev/kmem

/* източник 6.0 */ #include #включи #включи #включи #включи #включи #включи #включи /* Номер на системно повикване за прихващане */ #define _SYS_MKDIR_ 39 #define KMEM_FILE "/dev/kmem" #define MAX_SYMS 4096 /* IDTR описание на формата на регистъра */ struct ( unsigned short limit; unsigned int base; ) __attribute__ ((packed) ) idtr; /* IDT таблица прекъсване на врата описание формат */ struct ( unsigned short off1; unsigned short sel; unsigned char none, флагове; unsigned short off2; ) __attribute__ ((packed)) idt; /* Описание на структурата за функцията get_kmalloc */ struct kma_struc ( ulong (*kmalloc) (uint, int); // - адрес на функцията kmalloc int size; // - размер на паметта за разпределяне на int флагове; // - флаг, за ядра > 2.4.9 = 0x1f0 (GFP) ulong mem; ) __attribute__ ((packed)) kmalloc; /* Функция, която разпределя само блок памет в адресното пространство на ядрото */ int get_kmalloc(struct kma_struc *k) ( k->mem = k->kmalloc(k->size, k->flags); return 0 ; ) /* Функция, която връща адреса на функцията (необходима за kmalloc търсене) */ ulong get_sym(char *n) ( struct kernel_sym tab; int numsyms; int i; numsyms = get_kernel_syms(NULL); if (numsyms > MAX_SYMS || numsyms< 0) return 0; get_kernel_syms(tab); for (i = 0; i < numsyms; i++) { if (!strncmp(n, tab[i].name, strlen(n))) return tab[i].value; } return 0; } /* Наша новая системная функция, ничего не делает;) */ int new_mkdir(const char *path) { return 0; } /* Читает из /dev/kmem с offset size данных в buf */ static inline int rkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset){ printf("lseek err\n"); return 0; } if (read(fd, buf, size) != size) return 0; return size; } /* Аналогично, но только пишет в /dev/kmem */ static inline int wkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } /* Читает из /dev/kmem данные размером 4 байта */ static inline int rkml(int fd, uint offset, ulong *buf) { return rkm(fd, offset, buf, sizeof(ulong)); } /* Аналогично, но только пишет */ static inline int wkml(int fd, uint offset, ulong buf) { return wkm(fd, offset, &buf, sizeof(ulong)); } /* Функция для получения адреса sys_call_table */ ulong get_sct(int kmem) { ulong sys_call_off; // - адрес обработчика // прерывания int $0x80 (функция system_call) char *p; char sc_asm; asm("sidt %0" : "=m" (idtr)); if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0; sys_call_off = (idt.off2 << 16) | idt.off1; if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0; p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3; printf("call for sys_call_table at %08x\n",p); if (p) return *(ulong *)p; return 0; } /* Функция для определения адреса функции kmalloc */ ulong get_kma(ulong pgoff) { uint i; unsigned char buf, *p, *p1; int kmemz; ulong ret; ret = get_sym("kmalloc"); if (ret) { printf("\nZer gut!\n"); return ret; } kmemz = open("/dev/kmem", O_RDONLY); if (kmemz < 0) return 0; for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){ if (!rkm(kmemz, i, buf, sizeof(buf))) return 0; p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4); if(p1) { p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1; if (p) { close(kmemz); return *(unsigned long *)p+i+(p-buf)+4; } } } close(kmemz); return 0; } int main() { int kmem; // !! - пустые, нужно подставить ulong get_kmalloc_size; // - размер функции get_kmalloc !! ulong get_kmalloc_addr; // - адрес функции get_kmalloc !! ulong new_mkdir_size; // - размер функции-перехватчика!! ulong new_mkdir_addr; // - адрес функции-перехватчика!! ulong sys_mkdir_addr; // - адрес системного вызова sys_mkdir ulong page_offset; // - нижняя граница адресного // пространства ядра ulong sct; // - адрес таблицы sys_call_table ulong kma; // - адрес функции kmalloc unsigned char tmp; kmem = open(KMEM_FILE, O_RDWR, 0); if (kmem < 0) return 0; sct = get_sct(kmem); page_offset = sct & 0xF0000000; kma = get_kma(page_offset); printf("OK\n" "page_offset\t\t:\t0x%08x\n" "sys_call_table\t:\t0x%08x\n" "kmalloc()\t\t:\t0x%08x\n", page_offset,sct,kma); /* Найдем адрес sys_mkdir */ if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) { printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_); perror("er: "); return 1; } /* Сохраним первые N байт вызова sys_mkdir */ if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Cannot save old %d syscall!\n", _SYS_MKDIR_); return 1; } /* Перепишем первые N байт, функцией get_kmalloc */ if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) { printf("Can"t overwrite our syscall %d!\n",_SYS_MKDIR_); return 1; } kmalloc.kmalloc = (void *) kma; //- адрес функции kmalloc kmalloc.size = new_mkdir_size; //- размер запращевоемой // памяти (размер функции-перехватчика new_mkdir) kmalloc.flags = 0x1f0; //- спецификатор GFP /* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */ mkdir((char *)&kmalloc,0); /* Востановим оригинальный вызов sys_mkdir */ if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Can"t restore syscall %d !\n",_SYS_MKDIR_); return 1; } if (kmalloc.mem < page_offset) { printf("Allocated memory is too low (%08x < %08x)\n", kmalloc.mem, page_offset); return 1; } /* Оторбразим результаты */ printf("sys_mkdir_addr\t\t:\t0x%08x\n" "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n" "our kmem region\t\t:\t0x%08x\n" "size of our kmem\t:\t0x%08x (%d bytes)\n\n", sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size, kmalloc.mem, kmalloc.size, kmalloc.size); /* Разместим в пространстве ядра наш новый сис. вызво */ if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) { printf("Unable to locate new system call !\n"); return 1; } /* Перепишем таблицу sys_call_table на наш новый вызов */ if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) { printf("Eh ..."); return 1; } return 1; } /* EOF */ Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump: # gcc -o src-6.0 src-6.0.c # objdump -x ./src-6.0 >dump Нека отворим дъмп файла и да намерим данните, които ни интересуват: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir Сега нека добавим тези стойности към нашата програма: ulong get_kmalloc_size=0x32; ulong get_kmalloc_addr=0x080485a4 ; ulong new_mkdir_size=0x0a; ulong new_mkdir_addr=0x080486b1; Сега нека прекомпилираме програмата. След като го стартираме за изпълнение, ще прихванем системното извикване sys_mkdir. Всички извиквания към sys_mkdir вече ще се обработват от функцията new_mkdir.

Край на хартията/EOP

Ефективността на кода от всички секции беше тествана на ядрото 2.4.22. При изготвянето на доклада са използвани материали от сайта
Системни повиквания

Досега всички програми, които направихме, трябваше да използват добре дефинирани механизми на ядрото, за да регистрират /proc файлове и драйвери на устройства. Това е чудесно, ако искате да направите нещо, което вече е предоставено от програмистите на ядрото, като например да напишете драйвер за устройство. Но какво, ако искате да направите нещо фантастично, да промените поведението на системата по някакъв начин?

Точно тук програмирането на ядрото става опасно. Докато пишех примера по-долу, унищожих отвореното системно повикване. Това означаваше, че не мога да отварям никакви файлове, не мога да стартирам никакви програми и не мога да изключа системата с командата за изключване. Трябва да изключа захранването, за да го спра. За щастие няма унищожени файлове. За да сте сигурни, че няма да загубите никакви файлове, моля, направете синхронизиране, преди да подадете командите insmod и rmmod.

Забравете за /proc файловете и файловете на устройството. Те са само малки детайли. Истинският комуникационен процес на ядрото, използван от всички процеси, са системните повиквания. Когато процес поиска услуга от ядрото (като отваряне на файл, стартиране на нов процес или искане на повече памет), се използва този механизъм. Ако искате да промените поведението на ядрото по интересни начини, това е правилното място. Между другото, ако искате да видите какви системни извиквания е използвала дадена програма, изпълнете: strace .

По принцип даден процес не може да получи достъп до ядрото. Той няма достъп до паметта на ядрото и не може да извиква функции на ядрото. Хардуерът на процесора налага това състояние на нещата (то се нарича „защитен режим" с причина). Системните повиквания са изключение от това общо правило. Процесът попълва регистрите с подходящите стойности и след това извиква специална инструкция, която прескача към предварително дефинирано местоположение в ядрото (разбира се, то се чете от потребителските процеси, но не се презаписва от тях.) При процесорите на Intel това се прави чрез прекъсване 0x80.Хардуерът знае, че след като преминете към това местоположение, вие вече не работите в ограничен потребителски режим Вместо това вие работите като ядро ​​на операционната система и следователно ви е позволено да правите каквото искате.

Мястото в ядрото, до което процесът може да прескочи, се нарича system_call. Процедурата, която се намира там, проверява за номера на системното повикване, който казва на ядрото какво точно иска процесът. След това търси таблицата на системните повиквания (sys_call_table), за да намери адреса на функцията на ядрото, която да извика. След това се извиква желаната функция и след като върне стойност, се правят няколко проверки на системата. След това резултатът се връща обратно към процеса (или към друг процес, ако процесът е прекратен). Ако искате да видите кода, който прави всичко това, той е вътре изходен файларка/< architecture >/kernel/entry.S , след реда ENTRY(system_call).

Така че, ако искаме да променим начина, по който работи някое системно извикване, първото нещо, което трябва да направим, е да напишем собствена функция, за да направим подходящото нещо (обикновено добавяме част от нашия собствен код и след това извикваме оригиналната функция), след което променим указателя към sys_call_table, за да посочи нашата функция. Тъй като може да бъдем изтрити по-късно и не искаме да оставяме системата в непостоянно състояние, важно е cleanup_module да възстанови таблицата в първоначалното й състояние.

Предоставеният тук изходен код е пример за такъв модул. Искаме да "шпионираме" някой потребител и да изпратим printk съобщение винаги даден потребителотваря файла. Заменяме системното повикване, отваряйки файл с нашия собствена функция, наречено our_sys_open . Тази функция проверява uid (потребителски идентификатор) на текущия процес и ако той е равен на uid, който шпионираме, извиква printk, за да покаже името на файла, който ще бъде отворен. След това извиква оригиналната отворена функция със същите параметри, като всъщност отваря файла.

Функцията init_module променя подходящото местоположение в sys_call_table и съхранява оригиналния указател в променлива. Функцията cleanup_module използва тази променлива, за да възстанови всичко обратно към нормалното. Този подход е опасен, поради възможността два модула да променят едно и също системно извикване. Представете си, че имаме два модула, A и B. Отвореното системно извикване на модул A ще се нарича A_open, а извикването на същия модул B ще се нарича B_open. Сега, когато инжектираният от ядрото syscall е заменен с A_open, който ще извика оригиналния sys_open, когато свърши това, което трябва да направи. След това B ще вмъкне в ядрото и ще замени системното извикване с B_open, което ще извика това, което смята, че е оригиналното системно извикване, но всъщност е A_open.

Сега, ако първо се премахне B, всичко ще бъде наред: това просто ще възстанови системното извикване на A_open, което извиква оригинала. Въпреки това, ако A бъде премахнато и след това B е премахнато, системата ще се срине. Премахването на A ще възстанови системното извикване към оригинала, sys_open, изрязвайки B от цикъла. След това, когато B бъде премахнат, той ще възстанови системното извикване до това, което смята за оригинално.Всъщност извикването ще бъде насочено към A_open, което вече не е в паметта. На пръв поглед изглежда, че можем да разрешим този конкретен проблем, като проверим дали системното извикване е равно на нашата отворена функция и ако е така, не променяме стойността на това извикване (така че B да не променя системното извикване, когато бъде премахнато), но това пак би било най-лошият проблем. Когато A се премахне, той вижда, че системното повикване е променено на B_open, така че вече да не сочи към A_open, така че няма да възстанови указателя към sys_open, преди да бъде премахнат от паметта. За съжаление, B_open все още ще се опитва да извика A_open, който вече не е в паметта, така че дори и без премахване на B, системата пак ще се срине.

Виждам два начина за предотвратяване на този проблем. Първо: възстановете повикването до оригиналната стойност на sys_open. За съжаление, sys_open не е част от таблицата на системното ядро ​​в /proc/ksyms, така че нямаме достъп до него. Друго решение е да използвате брояч на връзки, за да предотвратите разтоварването на модула. Това е добре за редовните модули, но е лошо за "образователните" модули.

/* syscall.c * * Пример за "кражба" на системно повикване */ /* Copyright (C) 1998-99 от Ori Pomerantz */ /* Необходимите заглавни файлове */ /* Стандарт в модулите на ядрото */ #include /* Ние работим върху ядрото */ #include /* По-конкретно, модул */ /* Справяне с CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #include /* Списъкът със системни повиквания */ /* За текущата (процес) структура имаме нужда * от това, за да знаем кой е текущият потребител. */ #включи /* Във 2.2.3 /usr/include/linux/version.h включва * макрос за това, но 2.0.35 не го включва - така че го добавям * тук, ако е необходимо. */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a ,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* Таблицата на системните повиквания (таблица с функции). Ние * просто дефинираме това като външно и ядрото ще * го попълни вместо нас, когато сме insmod"ed */ extern void *sys_call_table; /* UID, който искаме да шпионираме - ще бъде попълнен от * командния ред */ int uid; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* Указател към оригиналното системно извикване. Причината * запазваме това, вместо да извикваме оригиналната функция * (sys_open), е защото някой друг може * да е заменил системното извикване преди нас функцията в този модул - и тя * може да бъде премахната преди нас. * Това е статична променлива, така че не се експортира. */ asmlinkage int (*original_call)(const char *, int, int); /* По някаква причина в 2.2.3 current->uid ми даде * нула, не истинският потребителски идентификатор. Опитах се да намеря какво * се е объркало, но не можах да го направя за кратко време и * съм мързелив - така че просто ще използвам системното извикване, за да получа * uid, начин, по който един процес би. * * По някаква причина, след като прекомпилирах ядрото, този * проблем изчезна. */ asmlinkage int (*getuid_call)(); /* Функцията, с която "ще заменим sys_open (функцията *, извикана, когато извикате отвореното системно повикване). За да * намерим точния прототип, с броя и типа * на аргументите, първо намираме оригиналната функция * (тя" s при fs/open.c). * * На теория това означава, че сме обвързани с * текущата версия на ядрото. На практика * системните извиквания почти никога не се променят (това ще доведе до хаос * и ще изисква програмите да бъдат прекомпилирани, тъй като системните * извиквания са интерфейса между ядрото и * процесите).*/ asmlinkage int our_sys_open(const char *filename, int flags, int mode) ( int i = 0; char ch; /* Проверете дали това е потребителят, когото шпионираме */ if (uid == getuid_call()) ( /* getuid_call е getuid системното извикване, * което дава uid на потребителя, който * е изпълнил процеса, извикал системното * повикване, което получихме */ /* Докладвайте файла, ако е приложимо */ printk("Отворен файл от %d: ", uid); do ( #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, filename+i); #else ch = get_user(filename+ i ); #endif i++; printk("%c", ch); ) while (ch != 0); printk("\n"); ) /* Извикване на оригиналния sys_open - в противен случай губим * възможността за отваряне files */ return original_call(filename, flags, mode); ) /* Инициализиране на модула - заместване на системното повикване */ int init_module() ( /* Предупреждение - твърде късно за това сега, но може би за * следващия път. ... printk("Моят аналог, cleanup_module(), е четен"); printk("по-опасно. Ако\n"); printk("цените своя файлова система, ще "); printk("да бъде \"sync; rmmod\" \n"); printk("когато премахнете този модул.\n"); /* Запазете указател към оригиналната функция в * original_call и след това заменете системното повикване * в таблицата на системното повикване с our_sys_open */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open; /* За да получите адреса на функцията за системно * повикване foo, отидете на sys_call_table[__NR_foo] */ printk("Шпиониране на UID:%d\n", uid); /* Вземете системното извикване за getuid */ getuid_call = sys_call_table[__NR_getuid]; return 0; ) /* Почистване - дерегистрирайте съответния файл от / proc */ void cleanup_module() ( /* Връща системното извикване обратно към нормалното */ if (sys_call_table[__NR_open] != our_sys_open) ( printk("Някой друг също е играл с "); printk("отворено системно повикване\n " ); printk("Системата може да бъде оставена в "); printk("нестабилно състояние.\n"); ) sys_call_table[__NR_open] = original_call; )