Аппараттық және бағдарламалық қамтамасыз етуді орнату

Linux - жүйелік қоңыраулар. Linux жүйесінде жүйелік қоңыраулар

Бұл материал «System Administrator» журналында жарияланған Владимир Мешковтың аттас мақаласының модификациясы болып табылады.

Бұл материал Владимир Мешковтың «Жүйелік әкімші» журналындағы мақалаларының көшірмесі болып табылады. Бұл мақалаларды төмендегі сілтемелерден табуға болады. Сондай-ақ, бағдарламаның бастапқы мәтіндерінің кейбір мысалдары өзгертілді - жетілдірілді, аяқталды. (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 Орталықта (сол жақта) жүйенің ядросы орналасқан. Ядро қолданбалы бағдарламаларды архитектуралық мүмкіндіктерден оқшаулай отырып, компьютердің аппараттық құралдарымен тікелей әрекеттеседі. Ядрода қолданбалы бағдарламаларға ұсынылатын қызметтер жиынтығы бар. Ядро қызметтеріне енгізу-шығару операциялары (файлдарды ашу, оқу, жазу және басқару), процестерді құру және басқару, оларды синхрондау және процестер аралық байланыс кіреді. Барлық қолданбалар жүйелік қоңыраулар арқылы ядро ​​қызметтерін сұрайды.

Екінші деңгей қолданбалардан немесе тапсырмалардан тұрады, жүйенің функционалдығын анықтайтын жүйелік және Linux пайдаланушы интерфейсін қамтамасыз ететін қолданбалы. Дегенмен, қосымшалардың сыртқы гетерогенділігіне қарамастан, ядромен әрекеттесу схемалары бірдей.

Ядромен әрекеттесу стандартты жүйелік шақыру интерфейсі арқылы жүзеге асады. Жүйелік қоңырау интерфейсі ядро ​​қызметтерінің жиынтығы болып табылады және қызметтерге сұраныстардың пішімін анықтайды. Процесс кәдімгі кітапхана функциясын шақыру сияқты көрінетін белгілі бір ядро ​​​​ процедурасына жүйелік қоңырау шалу арқылы қызметті сұрайды. Ядро процесс атынан сұранысты орындайды және қажетті деректерді процесске қайтарады.

Жоғарыда келтірілген мысалда бағдарлама файлды ашады, одан деректерді оқиды және файлды жабады. Бұл жағдайда файлды ашу (ашу), оқу (оқу) және жабу (жабу) операциясы тапсырманың сұранысы бойынша ядро ​​арқылы орындалады, ал ашу (2), оқу (2) және жабу (2) ) функциялар жүйелік шақырулар болып табылады.

/* 1.0 көзі */ #include main () ( int fd; char buf; /* Файлды ашу – сілтемені алу (файл дескрипторы) fd */ fd = open("file1",O_RDONLY); /* буфер буфіне 80 таңбаны оқу */ оқу( 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 жүйесінде жүйелік шақыруды орындау үшін /usr/src/linux/arch/i386/kernel/entry.S файлында анықталған (осы жағдайда i386 архитектурасына байланысты) system_call функциясы пайдаланылады. Бұл функция барлық жүйелік қоңыраулар үшін кіру нүктесі болып табылады. Ядро 0x80 үзуіне system_call функциясын шақыру арқылы жауап береді, ол 0x80 үзу өңдеушісі болып табылады.

Дұрыс жолда екенімізге көз жеткізу үшін libc жүйелік кітапханасындағы open() функциясының кодын қарастырайық:

# 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 регистрлеріне беріледі, ал соңғы EAX регистрі жүйенің шақыру нөмірімен толтырылады, ол бізге бұрыннан белгілі, 5. .

Енді жүйелік шақыру механизміне оралайық. Сонымен, ядро ​​0x80 үзу өңдеушісін - system_call функциясын шақырады. System_call SAVE_ALL макросы арқылы шақыру параметрлері бар регистрлердің көшірмелерін стекке итереді және шақыру пәрменімен қажетті жүйе функциясын шақырады. Жүйелік шақыруларды жүзеге асыратын ядро ​​функцияларына көрсеткіштер кестесі sys_call_table массивінде орналасқан (arch/i386/kernel/entry.S файлын қараңыз). EAX регистрінде орналасқан жүйелік шақыру нөмірі осы массивтің индексі болып табылады. Осылайша, егер EAX құрамында 5 мәні болса, sys_open() ядро ​​функциясы шақырылады. SAVE_ALL макросы не үшін қажет? Мұндағы түсініктеме өте қарапайым. Ядро жүйесінің барлық функциялары дерлік Си тілінде жазылғандықтан, олар өз параметрлерін стектен іздейді. Ал параметрлер SAVE_ALL көмегімен стекке итеріледі! Жүйелік шақырудың қайтару мәні EAX регистрінде сақталады.

Енді жүйелік қоңырауды қалай ұстау керектігін анықтайық. Бұл бізге жүктелетін ядро ​​модульдерінің механизмі көмектеседі.

2. Жүктелетін ядро ​​модулі

Loadable Kernel Module (LKM - Loadable Kernel Module) — ядро ​​кеңістігінде жұмыс істейтін код. Негізгі мүмкіндік 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 Hello World # 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 ... open("file1", 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 *жол); /* Жеке жүйелік қоңырауды жасаңыз. Біздің қоңырауымыз ештеңе жасамайды, тек 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 moved back) \n "); ) /* EOF */ Нысан модулін алу үшін келесі пәрменді орындаңыз және жүйеде кейбір эксперименттерді орындаңыз: # gcc -c -DMODULE -I/usr/src/linux/include/src-3.1. c # dmesg | tail -n 1 sys_mkdir ауыстырылды # mkdir сынағы # ls -ald test ls: сынақ: Мұндай файл немесе каталог жоқ # rmmod src-3.1 # dmesg | tail -n 1 sys_mkdir артқа жылжытылды # mkdir сынағы # ls -ald сынағы drwxr-xr-x 2 түбірлік түбір 4096 2003-12-23 03:46 сынақ Көріп отырғаныңыздай, «mkdir» пәрмені жұмыс істемейді, дәлірек айтсақ, ештеңе жоқ. орын алады. Жүйе функционалдығын қалпына келтіру үшін модульді түсіру жеткілікті. Жоғарыда не істелді.

4.2 Каталогтағы файл жазбасын жасыру

Каталогтың мазмұнын оқу үшін қандай жүйелік шақыру жауапты екенін анықтайық. Мұны істеу үшін біз басқа жазамыз сынақ үзіндісі, ол ағымдағы каталогты оқиды: /* Source 4.2.1 */ #include #қосу int main() ( DIR *d; struct dirent *dp; d = opendir("."); dp = readdir(d); қайтару 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, 0x4014C2C0, 0x4014C2C0, 3, 0x4014C2C0) = 0x_fcntl64 (3, 2, 1, 1, 1, 1, 1, 0x4014c2c0) = 0 sys_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 жүйелік шақыруын ұстап, қабылданған құрылымдар тізімінен сәйкес жазбаны тауып, оны жою керек. Осы операцияны орындайтын кодты қарастырайық (түпнұсқа кодтың авторы - Михал Залевски): /* Source 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; unclned short d_re 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; /* Біздің файлды іздеуді бастаңыз */ while (t) >0) ( /* Бірінші жазбаның ұзындығын оқыңыз және каталогтағы жазбалардың қалған ұзындығын анықтаңыз */ n = dirp3->d_reclen; t -= n; /* Ағымдағы жазбадағы файл атауының сәйкес келетінін тексеріңіз біз іздейтін біреу */ if (strstr((char *)&(dirp3->d_name), (char) *)&жасыру) != 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_gets; this */commands/fun) коды, «file1» қалай жоғалып кететініне назар аударыңыз, бұл біз дәлелдегіміз келген нәрсе.

5. /dev/kmem ядросының мекенжай кеңістігіне тікелей қатынау әдісі

Алдымен ядроның адрестік кеңістігіне тікелей қол жеткізу әдісімен ұстап алу қалай жүзеге асырылатынын теориялық тұрғыдан қарастырайық, содан кейін практикалық іске асыруға көшейік.

Ядроның мекенжай кеңістігіне тікелей қол жеткізу /dev/kmem құрылғы файлы арқылы қамтамасыз етіледі. Бұл файл барлық қол жетімді виртуалды мекенжай кеңістігін, соның ішінде своп бөлімін (своп-аумақ) көрсетеді. Kmem файлымен жұмыс істеу үшін стандартты жүйе функциялары пайдаланылады - open(), read(), write(). /dev/kmem стандартты түрде ашылғанда, біз жүйедегі кез келген мекенжайға оны осы файлда офсет ретінде орнату арқылы сілтеме жасай аламыз. Бұл әдісті Сильвио Чезаре жасаған.

Жүйе функцияларына функция параметрлерін процессор регистрлеріне жүктеп, содан кейін бағдарламалық құралды үзу 0x80 шақыру арқылы қол жеткізуге болады. Осы үзілістің өңдеушісі, system_call функциясы, шақыру параметрлерін стекке итереді, sys_call_table кестесінен шақырылатын жүйе функциясының мекенжайын шығарып алады және басқаруды сол мекенжайға тасымалдайды.

Ядроның мекенжай кеңістігіне толық қол жеткізу арқылы біз жүйелік шақырулар кестесінің барлық мазмұнын ала аламыз, яғни. барлық жүйе функцияларының адрестері. Кез келген жүйелік қоңыраудың мекенжайын өзгерту арқылы біз оны ұстап аламыз. Бірақ ол үшін кестенің адресін немесе басқаша айтқанда, осы кесте орналасқан /dev/kmem файлындағы ығысуды білу керек.

sys_call_table кестесінің мекенжайын анықтау үшін алдымен жүйелік_шақыру функциясының мекенжайын есептеу керек. Бұл функция үзу өңдеушісі болғандықтан, қорғалған режимде үзілістердің қалай өңделетінін қарастырайық.

Нақты режимде үзуді тіркеу кезінде процессор үзіліс векторының кестесіне қол жеткізеді, ол әрқашан жадтың ең басында тұрады және үзіліс өңдеушілердің екі сөзден тұратын адрестерін қамтиды. Қорғалған режимде үзілістердің векторлық кестесінің аналогы қорғалған режим операциялық жүйесінде орналасқан үзу дескрипторының кестесі (IDT) болып табылады. Процессордың осы кестеге қол жеткізуі үшін оның мекенжайы үзіліс дескрипторы кестесі регистріне (IDTR) жүктелуі керек. IDT кестесінде үзу өңдегішінің дескрипторлары бар, олар, атап айтқанда, олардың адрестерін қамтиды. Бұл дескрипторлар шлюздер (қақпалар) деп аталады. Процессор үзуді тіркей отырып, оның нөмірі бойынша IDT шлюзін шығарып, өңдеушінің адресін анықтайды және оған басқаруды береді.

IDT кестесінен жүйелік_шақыру функциясының адресін есептеу үшін int $0x80 үзу қақпасын, ал одан сәйкес өңдеушінің адресін, яғни. жүйелік_шақыру функциясының мекенжайы. Жүйелік_шақыру функциясында жүйелік_шақыру_кестесіне шақыру пәрмені арқылы қол жеткізіледі<адрес_таблицы>(,%ең,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 басыңыз : жылжыту $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 : жоқ Ассемблер қоқысының соңы. "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 Осылайша, /dev/kmem файлындағы \xff\x14\x85 тізбегін тауып, одан кейінгі 4 байтты оқи отырып, sys_call_table жүйесінің шақыру кестесінің адресін аламыз. Оның адресін біле отырып, біз осы кестенің мазмұнын (барлық жүйелік функциялардың адрестерін) ала аламыз және кез келген жүйелік шақырудың адресін оны ұстап алу арқылы өзгерте аламыз.

Ұстау операциясын орындайтын псевдокодты қарастырайық:

readaddr(eski_syscall, scr + SYS_CALL*4, 4); writeaddr(new_syscall, scr + SYS_CALL*4, 4); Readaddr функциясы жүйелік шақырулар кестесінен жүйелік шақыру мекенжайын оқиды және оны old_syscall айнымалысында сақтайды. sys_call_table кестесіндегі әрбір жазба 4 байт алады. Қажетті адрес /dev/kmem файлында sct + SYS_CALL*4 ығысуында орналасқан (мұнда sct — sys_call_table кестесінің мекенжайы, SYS_CALL — жүйелік шақырудың сериялық нөмірі). writeaddr функциясы SYS_CALL жүйелік шақыруының мекенжайын new_syscall функциясының мекенжайымен қайта жазады және SYS_CALL жүйелік шақыруына барлық шақыруларға осы функция қызмет көрсетеді.

Барлығы қарапайым, мақсат орындалған сияқты. Дегенмен, біз пайдаланушының мекенжай кеңістігінде жұмыс істеп жатқанымызды еске түсірейік. Егер біз осы мекенжай кеңістігіне жаңа жүйе функциясын орналастырсақ, онда бұл функцияны шақырған кезде біз әдемі қате туралы хабар аламыз. Бұдан шығатын қорытынды – ядроның адрестік кеңістігіне жаңа жүйелік шақыруды орналастыру керек. Ол үшін сізге қажет: ядро ​​кеңістігінде жад блогын алу, осы блокта жаңа жүйелік шақыруды орналастыру.

kmalloc функциясын пайдаланып, ядро ​​кеңістігінде жадты бөлуге болады. Бірақ сіз пайдаланушының мекенжай кеңістігінен ядро ​​​​функциясын тікелей шақыра алмайсыз, сондықтан біз келесі алгоритмді қолданамыз:

  • sys_call_table кестесінің мекенжайын біле отырып, біз кейбір жүйелік қоңыраудың мекенжайын аламыз (мысалы, sys_mkdir)
  • kmalloc функциясына шақыруды орындайтын функцияны анықтаймыз. Бұл функция ядроның мекенжай кеңістігіндегі жад блогына көрсеткішті қайтарады. Бұл функцияны get_kmalloc деп атаймыз
  • sys_mkdir жүйелік шақыруының бірінші N байттарын сақтаңыз, мұнда N - get_kmalloc функциясының өлшемі
  • get_kmalloc функциясымен sys_mkdir қоңырауының бірінші N байтының үстінен жазыңыз
  • біз sys_mkdir жүйелік қоңырауына шақыруды орындаймыз, осылайша орындау үшін get_kmalloc функциясын іске қосамыз
  • sys_mkdir жүйелік шақыруының бірінші N байттарын қалпына келтіріңіз
Нәтижесінде бізде ядро ​​кеңістігінде орналасқан жад блогы болады.

Бірақ бұл алгоритмді жүзеге асыру үшін бізге kmalloc функциясының мекенжайы қажет. Сіз оны бірнеше жолмен таба аласыз. Ең қарапайымы - бұл мекенжайды System.map файлынан оқу немесе оны gdb отладкасының көмегімен анықтау (басып шығару &kmalloc). Ядрода модуль қолдауы қосылған болса, kmalloc мекенжайын get_kernel_syms() функциясы арқылы анықтауға болады. Бұл опция одан әрі талқыланады. Егер ядро ​​модульдері үшін қолдау болмаса, kmalloc функциясының мекенжайын kmalloc шақыру пәрменінің операциялық коды бойынша іздеу керек болады - 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 басыңыз : қосалқы $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 функциясының мекенжайын есептей аламыз. Бір қарағанда, бұл функцияның мекенжайы шақыру нұсқауының аргументі болып табылады, бірақ бұл мүлдем дұрыс емес. Жүйелік_шақыру функциясынан айырмашылығы, мұнда нұсқау 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->өлшем, k->жалаулар); қайтару 0 ; ) /* Функцияның мекенжайын қайтаратын функция (kalloc іздеу үшін қажет) */ ulong get_sym(char *n) ( struct kernel_sym қойындысы; int numsyms; int i; numsyms = get_kernel_syms(NULL); if (numsyms > MAX_SYMS) || нумсимдер< 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_kmallo;e=0c 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 ядросында тексерілді. Есепті дайындау кезінде сайт материалдары пайдаланылды

Көбінесе __NR_xxx нөмірлі жүйелік қоңырау коды анықталған /usr/include/asm/unistd.h, функциядағы Linux ядросының бастапқы кодынан табуға болады sys_xxx(). (i386 үшін қоңыраулар кестесін мына жерден табуға болады /usr/src/linux/arch/i386/kernel/entry.S.) Бұл ережеден көптеген ерекшеліктер бар, негізінен ескі жүйелік қоңыраулардың көпшілігі жаңаларына ауыстырылады және ешқандай жүйесіз. parisc, sparc, sparc64 және alpha сияқты меншікті эмуляция платформаларында көптеген қосымша жүйелік қоңыраулар бар; mips64-те 32-биттік жүйелік қоңыраулардың толық жиынтығы бар.

Уақыт өте қажет болған жағдайда кейбір жүйелік қоңыраулардың интерфейсіне өзгерістер енгізілді. Бұл өзгерістердің себептерінің бірі құрылымдардың өлшемін немесе жүйелік шақыруға берілетін скаляр мәндерді ұлғайту қажеттілігі болды. Осы өзгерістерге байланысты кейбір архитектураларда (мысалы, ескі 32-биттік i386-да) ұқсас жүйелік қоңыраулардың әртүрлі топтары пайда болды (мысалы, қысқарту(2) және қысқарту64(2)), олар бірдей тапсырмаларды орындайды, бірақ аргументтерінің өлшемі бойынша ерекшеленеді. (Айтылғандай, қолданбалар әсер етпейді: glibc орауыштары дұрыс жүйелік шақыруды іске қосу үшін біраз жұмыс жасайды және бұл ескі екілік файлдар үшін ABI үйлесімділігін қамтамасыз етеді.) Бірнеше нұсқасы бар жүйелік қоңыраулардың мысалдары:

*Қазіргі уақытта үшеуі бар әртүрлі нұсқалар стат(2): sys_stat() (орын __NR_oldstat), sys_newsstat() (орын __NR_stat) және sys_stat64() (орын __NR_stat64), соңғысы қазіргі уақытта қолданылуда. Осыған ұқсас жағдай lstat(2) және fstat(2). * Дәл осылай анықталған __NR_oldolduname, __NR_oldunameжәне __NR_nameқоңыраулар үшін sys_olduname(), sys_name() және sys_newuname(). * Linux 2.0 жаңа нұсқасы бар vm86(2), жаңа және ескі нұсқасыядролық процедуралар деп аталады sys_vm86old() және sys_vm86(). * Linux 2.4 жаңа нұсқасы бар алу шегі(2) ядролық процедуралардың жаңа және ескі нұсқалары деп аталады sys_old_getrlimit() (орын __NR_getrlimit) және sys_getrlimit() (орын __NR_ugetrlimit). * Linux 2.4 жүйесінде пайдаланушы мен топ идентификаторы өрісінің өлшемі 16 биттен 32 битке дейін ұлғайтылды. Бұл өзгерісті қолдау үшін бірнеше жүйелік қоңыраулар қосылды (мысалы, Чон32(2), getuid32(2), getgroups32(2), setresuid32(2)), бұрынғы атаулары бар, бірақ "32" жұрнағы жоқ бұрынғы қоңырауларды пайдаланудан бас тарту. * Linux 2.4 кіруге қолдауды қосты үлкен файлдар(өлшемдері мен ығысулары 32 битке сәйкес келмейтін) 32 биттік архитектуралардағы қолданбаларда. Бұл файл өлшемдерімен және ығысулармен жұмыс істейтін жүйелік қоңырауларға өзгертулерді талап етті. Келесі жүйелік қоңыраулар қосылды: fcntl64(2), getdents64(2), stat64(2), statfs64(2), қысқарту64(2) және олардың файл дескрипторларын немесе символдық сілтемелерді өңдейтін аналогтары. Бұл жүйелік шақырулар "stat" шақыруларын қоспағанда, бірдей аталатын, бірақ "64" жұрнағы жоқ ескі жүйе шақыруларын жояды.

Тек 64 биттік файлға қатынасы және 32 биттік UID/GID (мысалы, alpha, ia64, s390x, x86-64) бар жаңа платформаларда UID/GID және жүйенің бір ғана нұсқасы бар файлға қол жеткізу. *64 және *32 қоңыраулары бар платформаларда (әдетте 32-биттік платформалар), басқа нұсқалар ескірген.

* Қиындықтар rt_sig*нақты уақыттағы қосымша сигналдарды қолдау үшін 2.2 ядросына қосылған (қараңыз сигнал(7)). Бұл жүйелік шақырулар бірдей атпен, бірақ "rt_" префиксі жоқ ескі жүйе қоңырауларын ескіреді. * Жүйе қоңырауларында таңдаңыз(2) және mmap(2) i386 жүйесінде аргументтердің қалай жіберілгенін анықтауда қиындықтар тудыратын бес немесе одан да көп аргумент пайдаланылады. Нәтижесінде, басқа архитектураларда шақырылады sys_select() және sys_mmap() сәйкес келеді __NR_таңдаужәне __NR_mmmap, i386-де олар сәйкес келеді old_select() және ескі_мап() (аргументтер блогына көрсеткішті пайдаланатын процедуралар). Қазіргі уақытта бес аргументтен артық айту мәселесі жоқ және бар __NR__жаңалық таңдау, ол дәл сәйкес келеді sys_select() және сол жағдай __NR_mmap2.


Жүйелік қоңыраулар

Осы уақытқа дейін біз жасаған барлық бағдарламалар /proc файлдарын және құрылғы драйверлерін тіркеу үшін жақсы анықталған ядро ​​​​механизмдерін пайдалану керек болды. Бұл құрылғы драйверін жазу сияқты ядро ​​​​бағдарламашылары ұсынған нәрсені жасағыңыз келсе өте жақсы. Бірақ егер сіз сәнді нәрсе жасағыңыз келсе, жүйенің әрекетін қандай да бір жолмен өзгерткіңіз келсе ше?

Дәл осы жерде ядролық бағдарламалау қауіпті болады. Төмендегі мысалды жазу кезінде мен ашық жүйелік қоңырауды жойдым. Бұл ешбір файлды аша алмайтынымды, ешқандай бағдарламаны іске қоса алмайтынымды және жүйені өшіру пәрменімен өшіре алмайтынымды білдірді. Мен оны тоқтату үшін қуатты өшіруім керек. Бақытымызға орай, ешбір файл жойылмады. Ешбір файлды жоғалтпау үшін insmod және rmmod пәрмендерін шығармас бұрын синхрондауды орындаңыз.

/proc файлдары мен құрылғы файлдарын ұмытыңыз. Олар жай ғана ұсақ бөлшектер. Барлық процестер пайдаланатын нақты ядролық байланыс процесі жүйелік шақырулар болып табылады. Процесс ядродан қызметті сұрағанда (мысалы, файлды ашу, жаңа процесті бастау немесе қосымша жадты сұрау) бұл механизм пайдаланылады. Егер сіз ядроның әрекетін қызықты жолдармен өзгерткіңіз келсе, бұл дұрыс орын. Айтпақшы, бағдарлама қандай жүйе шақырғанын көргіңіз келсе, іске қосыңыз: strace .

Жалпы, процесс ядроға қол жеткізе алмайды. Ол ядро ​​жадына қол жеткізе алмайды және ядро ​​функцияларын шақыра алмайды. Орталық процессордың аппараттық құралы осы жағдайды қамтамасыз етеді (белгілі бір себептермен ол «қорғалған режим» деп аталады). Жүйелік қоңыраулар осы жалпы ережеден ерекшелік болып табылады. Процесс регистрлерді сәйкес мәндермен толтырады, содан кейін арнайы нұсқауды шақырады ядродағы алдын ала анықталған орын (әрине, оны пайдаланушы процестері оқиды, бірақ олар қайта жазбайды.) Intel процессорларында бұл 0x80 үзіліс арқылы орындалады. Аппараттық құрал осы орынға өткеннен кейін жұмыс істемейтінін біледі. жылы шектеулі режимпайдаланушы. Оның орнына сіз өзек ретінде жұмыс жасайсыз операциялық жүйе, сондықтан сізге не істегіңіз келсе, жасауға рұқсат беріледі.

Ядродағы процесс өтуі мүмкін орын system_call деп аталады. Онда орналасқан процедура жүйенің қоңырау нөмірін тексереді, ол ядроға процесс нені қалайтынын нақты айтады. Содан кейін ол шақырылатын ядро ​​функциясының мекенжайын табу үшін жүйелік шақыру кестесін (sys_call_table) іздейді. Содан кейін қажетті функция шақырылады және ол мәнді қайтарғаннан кейін жүйеде бірнеше тексерулер жүргізіледі. Содан кейін нәтиже процеске (немесе процесс тоқтатылған болса, басқа процеске) қайтарылады. Мұның бәрін жасайтын кодты көргіңіз келсе, ол бастапқы файл arch/< architecture >/kernel/entry.S , ENTRY(жүйелік_шақыру) жолынан кейін.

Сонымен, егер кейбір жүйелік шақырулардың жұмысын өзгерткіміз келсе, бірінші кезекте тиісті нәрсені орындау үшін өз функциямызды жазуымыз керек (әдетте кейбір кодты қосып, содан кейін бастапқы функцияны шақырамыз), содан кейін көрсеткішті өзгертіңіз. функциямызды көрсету үшін sys_call_table параметріне. Біз кейінірек жойылуы мүмкін болғандықтан және жүйені тұрақсыз күйде қалдырғымыз келмейтіндіктен, cleanup_module үшін кестені бастапқы күйіне қайтару маңызды.

Мұнда берілген бастапқы код осындай модульдің мысалы болып табылады. Біз кейбір пайдаланушыға «тыңшылық» жасағымыз келеді және кез келген уақытта басып шығару хабарламасын жібергіміз келеді берілген пайдаланушыфайлды ашады. Біз файлды ашық жүйе шақыруын our_sys_open деп аталатын өз функциямызбен ауыстырамыз. Бұл функция ағымдағы процестің uid идентификаторын (пайдаланушы идентификаторы) тексереді және ол біз тыңшылық жасап жатқан uid-ге тең болса, ашылатын файлдың атын көрсету үшін printk шақырады. Содан кейін ол файлды ашатын бірдей параметрлермен бастапқы ашық функцияны шақырады.

init_module функциясы sys_call_table ішіндегі сәйкес орынды өзгертеді және бастапқы көрсеткішті айнымалы мәнде сақтайды. Cleanup_module функциясы осы айнымалы мәнді барлығын қалыпты жағдайға қайтару үшін пайдаланады. Бұл тәсіл екі модульдің бір жүйелік шақыруды өзгерту мүмкіндігіне байланысты қауіпті. Бізде екі модуль бар деп елестетіңіз, А және В. А модулінің ашық жүйелік шақыруы A_open деп аталады, ал сол В модулінің шақыруы B_open деп аталады. Енді ядро ​​​​инъекцияланған жүйе шақыруы A_open ауыстырылды, ол бастапқы sys_open қажет нәрсені орындаған кезде шақырады. Содан кейін, B ядроға кірістіреді және жүйелік шақыруды B_open деп ауыстырады, ол бастапқы жүйелік шақыру деп санайтын нәрсені шақырады, бірақ шын мәнінде A_open.

Енді, егер алдымен B жойылса, бәрі жақсы болады: бұл түпнұсқаны шақыратын A_open жүйесінде жүйелік қоңырауды қалпына келтіреді. Дегенмен, егер А жойылса, содан кейін В жойылса, жүйе құлайды. A жою жүйелік қоңырауды бастапқыға қалпына келтіреді, sys_open, В циклден қиып тастайды. Содан кейін, B жойылған кезде, ол жүйелік қоңырауды бастапқы деп санайтын нәрсені қалпына келтіреді.Шын мәнінде, қоңырау енді жадта жоқ A_open-ге бағытталады. Бір қарағанда, жүйенің біздің ашық функциямызға тең екендігін тексеру және егер солай болса, сол шақырудың мәнін өзгертпеу арқылы (B жойылған кезде жүйені өзгертпеуі үшін) осы мәселені шешуге болатын сияқтымыз. бірақ бұл әлі де ең нашар проблема деп атайды. A жойылған кезде, ол жүйе шақыруы B_open болып өзгертілгенін көреді, осылайша ол енді A_open мәнін көрсетпейді, сондықтан ол жадтан жойылмас бұрын көрсеткішті sys_open күйіне қалпына келтірмейді. Өкінішке орай, B_open әлі де жадта жоқ A_open қызметіне қоңырау шалуға тырысады, сондықтан B жойылмаса да, жүйе әлі де істен шығады.

Мен бұл мәселені болдырмаудың екі жолын көремін. Біріншіден: қоңырауды sys_open бастапқы мәніне қалпына келтіріңіз. Өкінішке орай, sys_open /proc/ksyms ішіндегі жүйелік ядролар кестесінің бөлігі емес, сондықтан оған қол жеткізе алмаймыз. Тағы бір шешім - модульді түсіруге жол бермеу үшін сілтеме санауышын пайдалану. Бұл кәдімгі модульдер үшін жақсы, бірақ «білім беру» модульдері үшін нашар.

/* syscall.c * * Жүйелік қоңыраудың «ұрлау» үлгісі */ /* Авторлық құқық (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 #егер LINUX_НҰСҚАСЫНЫҢ_КОДЫ >= ЯДРДЫҢ_НҰСҚАУЫ(2,2,0) #қосу #endif /* Жүйені шақыру кестесі (функциялар кестесі). Біз * мұны жай ғана сыртқы деп анықтаймыз, ал ядро ​​* инsmod» енгізілген кезде біз үшін оны толтырады */ 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 ағымдағы->uid маған * нөл берді, нақты пайдаланушы идентификаторы емес. Мен ненің қате кеткенін анықтауға тырыстым, бірақ оны қысқа уақытта жасай алмадым, және * мен жалқаумын - сондықтан * uid, процесс қалай болады. * * Қандай да бір себептермен, ядроны қайта құрастырғаннан кейін бұл * мәселе жойылды. */ asmlinkage int (*getuid_call)(); /* Біз "sys_open функциясын (ашық жүйе шақыруын шақырған кезде шақырылатын функция) ауыстырамыз. * Нақты прототипті табу үшін * аргументтердің саны мен түрі * бар, біз алдымен бастапқы функцияны табамыз * (ол") s fs/open.c). * * Теориялық тұрғыдан, бұл ядроның * ағымдағы нұсқасына байланысты екенімізді білдіреді. Іс жүзінде * жүйелік шақырулар ешқашан өзгермейді (ол бүлінуге әкеледі * және бағдарламаларды қайта құрастыруды талап етеді, өйткені жүйе * қоңыраулары ядро мен * процестер арасындағы интерфейс).*/ asmlinkage int our_sys_open(const char *файл аты, int жалаулары, int режимі) ( int i = 0; char ch; /* Бұл біз тыңшылық жасап жатқан пайдаланушы екенін тексеріңіз. */ if (uid == getuid_call()) ( /* getuid_call — getuid жүйелік шақыруы, * ол жүйеге қоңырау шалған процесті орындаған * пайдаланушының uid идентификаторын береді * біз алған қоңырау */ /* Файлды хабарлау, сәйкес болса */ printk("%d арқылы ашылған файл: ", uid); істеу ( #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, файл аты+i); #else ch = get_user(файл аты+ i) ); #endif i++; printk("%c", ch); ) while (ch != 0); printk("\n"); ) /* Түпнұсқа sys_open деп атаймыз - әйтпесе * ашу мүмкіндігін жоғалтамыз. файлдар */ бастапқы_қоңырауды қайтарады (файл аты, жалаушалар, режим); ) /* Модульді инициализациялаңыз - жүйелік шақыруды ауыстырыңыз */ int init_module() ( /* Ескерту - бұл үшін қазір тым кеш, бірақ келесі жолы * болуы мүмкін. .. */ printk("Мен қауіптімін. Сіз жасадыңыз деп үміттенемін "); printk("insmod"мені жасамас бұрын синхрондау.\n"); printk("Менің әріптесім, cleanup_module(), жұп"); printk("қауіптірек. Егер\n"); printk("сіз файлдық жүйеңізді бағалайсыз, ол солай болады"); printk("be \"синхрондау; rmmod\" \n"); printk("осы модульді жойғанда.\n"); /* * original_call ішінде бастапқы функцияға көрсеткішті сақтаңыз, содан кейін жүйелік шақыру кестесіндегі * жүйелік шақыруды our_sys_open */ original_call = sys_call_table[__NR_open] дегенмен ауыстырыңыз; sys_call_table[__NR_open] = our_sys_open; /* * call foo жүйесіне арналған функцияның мекенжайын алу үшін sys_call_table[__NR_foo] бетіне өтіңіз. */ printk("UID тыңшылық:%d\n", uid); /* getuid жүйесіне қоңырау шалу */ getuid_call = sys_call_table[__NR_getuid]; қайтару 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] = бастапқы_шақыру; )

Мақала ұнады ма? Достарыңызбен бөлісіңіз!
Бұл мақала пайдалы болды ма?
Иә
Жоқ
Пікіріңізге рахмет!
Бірдеңе дұрыс болмады және сіздің дауысыңыз есептелмеді.
Рақмет сізге. Сіздің хабарламаңыз жіберілді
Мәтіннен қате таптыңыз ба?
Оны таңдаңыз, басыңыз Ctrl+Enterжәне біз оны түзетеміз!