Linux LSM(Linux Security Modules) Hook Technology
目录
0. 引言 1. Linux Security Module Framework Introduction 2. LSM Sourcecode Analysis 3. LSMs Hook Engine:基于LSM Hook进行元数据的监控获取 4. LSM编程示例 5. Linux LSM stacking
0. 引言
从最佳实践的角度来说,在Linux Kernel中进行安全审计、Access Control(访问控制)最好的方式就是使用Linux提供的原生的框架机制,例如
1. Kprobe: Linux提供的原生的调试机制(Debug),允许向任意的内核函数注册Hook回调通知点,当执行到Hooked函数的时候,会触发我们注册的kprobe_handle 2. LSM(Linux Security Modules): Linux提供的原生的Hook框架,本质上来说,LSM是直接串行的插入到了Linux Kernel的代码路径中,是Linux本身对外提供了一套审计机制(Hook框架)
LSM框架的设计初衷是为了在Linux Kernel中实现一个MAC(Mandatory Access Control) Hook框架,LSM已经作为Linux Kernel的一部分随着内核一起发布了,使用框架的好处就在于,安全人员可以直接基于LSM框架进行上层审计模块的开发,专注于业务逻辑,而不需要关注底层的内核差异兼 容性和稳定性,这是相对于sys_call hook方式最大的优势
1. Linux Security Module Framework Introduction
The Linux Security Module (LSM) framework provides a mechanism for various security checks to be hooked by new kernel extensions. The name "module" is a bit of a misnomer since these extensions are not actually loadable kernel modules. Instead, they are selectable at build-time via CONFIG_DEFAULT_SECURITY and can be overridden at boot-time via the "security=..." kernel command line argument, in the case where multiple LSMs were built into a given kernel.
The primary users of the LSM interface are Mandatory Access Control (MAC) extensions which provide a comprehensive security policy. Examples include
1. SELinux 2. Smack 3. Tomoyo 4. AppArmor 5. other extensions can be built using the LSM to provide specific changes to system operation //Linux LSM是Linux下的一个框架标准,并不是一个特定的技术,类似于WINDOWS中的NDIS(Network Driver Interface Specification)概念,而SELINUX是在遵循针这一框架的基础上实现的具体Hook技术,SELINUX和LSM的关系是具体实现和框架标准的关系
0x1: Design
LSM inserts "hooks" (upcalls to the module) at every point in the kernel where a user-level system call is about to result in access to an important internal kernel object such as inodes and task control blocks.
The project is narrowly scoped to solve the problem of access control to avoid imposing a large and complex change patch on the mainstream kernel. It is not intended as a general "hook" or "upcall" mechanism, nor does it support Operating system-level virtualization.
0x2: LSM Principle
The Linux Security Modules (LSM) project has developed a lightweight, general purpose, access control framework for the mainstream Linux kernel that enables many different access control models to be implemented as loadable kernel modules.
A number of existing enhanced access control implementations, including
1. Linux Capabilities Model 2. Security-Enhanced Linux (SELinux) Model 3. Domain and Type Enforcement (DTE) Model
使用LSM Hook框架进行内核安全审计、元数据捕获,安全人员只需要按照既定的调用规范编写LKM模块,并加载进Linux内核,而不需要对system call lookup表进行任何修改
LSM allows modules to mediate access to kernel objects by placing hooks in the kernel code just ahead of the access, as shown in picture above
0x3: LSM Implementation Principle
The LSM kernel patch modifies the kernel in five primary ways
1. it adds opaque security fields to certain kernel data structures 1) task_struct: Task(Process) 2) linux_binprm: Program 3) super_block: Filesystem 4) inode: Pipe、File,、or Socket 5) file: Open File 6) sk_buff: Network Buffer(Packet) 7) net_device: Network Device 8) kern_ipc_perm: Semaphore、Shared Memory Segment、Message Queue 9) msg_msg: Individual Message /* The setting of these security fields and the management of the associated security data is handled by the security modules. LSM merely provides the fields and a set of calls to security hooks that can be implemented by the module to manage the security fields as desired LSM以串行地方式插入到Linux Kernel中,在进行串行Hook的同时,同时自身也需要维护一套数据结构来保存自身的Hook信息,这也是Hook Engine的通用思路 */ 2. the patch inserts calls to security hook functions at various points within the kernel code LSM在Linux Kernel中插入了两种"security hooks function call" 1) LSM安全策略判断 2) lSM自身数据结构维护 这些hook function 指针都保存在一个全局结构体: "security-ops"中(struct security_operations) 3. the patch adds a generic security system call LSM provides a general security system call that allows security modules to implement new calls for security-aware applications 4. the patch provides functions to allow kernel modules to register and unregister themselves as security modules When a security module is loaded, it must register itself with the LSM framework by calling the "register-security" function. This function sets the global "security_ops table" to refer to the module’s hook function pointers(即初始状态下,"security-ops"是指向NULL的指针), causing the kernel to call into the security module for access control decisions. The "register-security" function will not overwrite a previously loaded module. Once a security module is loaded, it becomes a policy decision whether it will allow itself to be unloaded. 5. the patch moves most of the capabilities logic into an optional security module, The Linux kernel currently provides support for a subset of POSIX.1e capabilities.
0x4: LSM Hook Method
1. Task Hooks LSM provides a set of task hooks that enable security modules to manage process security information and to control process operations 2. Program Loading Hooks LSM provides a set of programloading hooks that are called at critical points during the processing of an execve operation.
"linux_binprm" 3. IPC Hooks Security modules can manage security information and perform access control for System V IPC using the LSM IPC hooks. LSM inserts a hook into the existing ipcperms function so that a security module can perform a check for each existing Linux IPC permission check 4. Filesystem Hooks For file operations, three sets of hooks were defined: 1) filesystem hooks 2) inode hooks 3) file hooks 5. Network Hooks Application layer access to networking is mediated using a set of socket hooks. These hooks, which include interposition of all socket system calls, provide coarse mediation coverage of all socket-based protocols. Since active user sockets have an associated inode structure, a separate security field was not added to the socket structure or to the lower-level sock structure. As the socket hooks allow general mediation of network traffic in relation to processes, LSM significantly expands the kernel’s network access control framework (which is already handled at the network layer by Netfilter)(LSM对网络的访问控制和Netfilter保持兼容). For example, the sock rcv skb hook allows an inbound packet to be mediated in terms of its destination application, prior to being queued at the associated userspace socket. 6. Other Hooks LSM provides two additional sets of hooks: 1) module hooks Module hooks can be used to control the kernel operations that create, initialize, and delete kernel modules. 2) a set of top-level system hooks System hooks can be used to control system operations, such as setting the system hostname, accessing I/O ports, and configuring process accounting.
可以看出,LSM在向上层提供了一个统一的Hook的接口的同时,在下层进行了大量的兼容,对于Linux系统的各个子系统来说,要实现对它们的Hook,差异性是不言而喻的,LSM的Hook Point就像触手一样遍布在Linux Kernel的各个角落
0x5: 基于LSM框架的具体实现
LSM provides only the mechanism to enforce enhanced access control policies. Thus, it is the LSM modules that implement a specific policy and are critical in proving the functionality of the framework.
1. SELinux http://en.wikipedia.org/wiki/Security-Enhanced_Linux http://selinuxproject.org/page/NB_LSM A Linux implementation of the Flask flexible access control architecture and an example security server that supports Type Enforcement, Role-Based Access Control, and optionally MultiLevel Security. SELinux was originally implemented as a kernel patch and was then reimplemented as a security module that uses LSM. SELinux can be used to confine processes to least privilege, to protect the integrity and confidentiality of processes and data, and to support application security needs. 2. DTE Linux An implementation of Domain and Type Enforcement developed for Linux Like SELinux, DTE Linux was originally implemented as a kernel patch and was then adapted to LSM. With this module loaded, types can be assigned to objects and domains to processes. The DTE policy restricts access between domains and from domains to types. The DTE Linux project also provided useful input into the design and implementation of LSM. 3. LSM port of Openwall kernel patch 4. POSIX.1e capabilities
Relevant Link:
http://lxr.free-electrons.com/source/Documentation/security/LSM.txt https://www.usenix.org/legacy/event/sec02/wright.html http://en.wikipedia.org/wiki/Linux_Security_Modules https://www.usenix.org/legacy/event/sec02/wright.html https://www.usenix.org/legacy/event/sec02/full_papers/wright/wright.pdf https://www.kernel.org/doc/ols/2002/ols2002-pages-604-617.pdf http://se7so.blogspot.com/2012/04/linux-security-modules-framework-lsm.html http://www.kroah.com/linux/talks/ols_2002_lsm_paper/lsm.pdf
2. LSM Sourcecode Analysis
0x1: LSM的数据结构定义
/source/security/security.c
... static struct security_operations *security_ops; static struct security_operations default_security_ops = { .name = "default", }; ... /** * security_init - initializes the security framework * * This should be called early in the kernel initialization sequence. */ int __init security_init(void) { printk(KERN_INFO "Security Framework initialized\n"); security_fixup_ops(&default_security_ops); security_ops = &default_security_ops; do_security_initcalls(); return 0; } .. static void __init do_security_initcalls(void) { initcall_t *call; call = __security_initcall_start; while (call < __security_initcall_end) { (*call) (); call++; } }
内核代码声明了全局静态结构体变量: security_ops,并将结构体初始化为默认Hook结构体(default_security_ops)
LSM中最重要的数据结构,保存所有LSM Hook Point Function的: struct security_operations的结构定义如下
/source/include/linux/security.h
struct security_operations { //A string that acts as a unique identifier for the LSM char name[SECURITY_NAME_MAX + 1]; int (*ptrace_access_check) (struct task_struct *child, unsigned int mode); int (*ptrace_traceme) (struct task_struct *parent); int (*capget) (struct task_struct *target, kernel_cap_t *effective, kernel_cap_t *inheritable, kernel_cap_t *permitted); int (*capset) (struct cred *new, const struct cred *old, const kernel_cap_t *effective, const kernel_cap_t *inheritable, const kernel_cap_t *permitted); int (*capable) (const struct cred *cred, struct user_namespace *ns, int cap, int audit); int (*quotactl) (int cmds, int type, int id, struct super_block *sb); int (*quota_on) (struct dentry *dentry); int (*syslog) (int type); int (*settime) (const struct timespec *ts, const struct timezone *tz); int (*vm_enough_memory) (struct mm_struct *mm, long pages); /* 1. Security hooks for program execution operations 程序装载钩子,LSM(Linux Security Modules)提供了一系列程序装载钩子,用在一个execve()操作执行过程的关键点上 1) "linux_binprm"结构中的安全域允许安全模块维护程序装载过程中的安全信息 2) LSM提供了钩子用于允许安全模块在装载程序前初始化安全信息和执行访问控制 3) LSM提供了钩子允许模块在新程序成功装载后更新任务的安全信息 4) LSM提供了钩子用来控制程序执行过程中的状态继承,例如确认打开的文件描述符(不是可信启动) */ //Save security information in the bprm->security field, typically based on information about the bprm->file, for later use by the apply_creds hook int (*bprm_set_creds) (struct linux_binprm *bprm); //This hook mediates the point when a search for a binary handler will begin. This hook may be called multiple times during a single execve; and in each pass set_creds is called first. int (*bprm_check_security) (struct linux_binprm *bprm); int (*bprm_secureexec) (struct linux_binprm *bprm); //Prepare to install the new security attributes of a process being transformed by an execve operation, based on the old credentials pointed to by @current->cred and the information set in @bprm->cred by the bprm_set_creds hook void (*bprm_committing_creds) (struct linux_binprm *bprm); //Tidy up after the installation of the new security attributes of a process being transformed by an execve operation. void (*bprm_committed_creds) (struct linux_binprm *bprm); /* 2. Security hooks for filesystem operations. */ //Allocate and attach a security structure to the sb->s_security field. int (*sb_alloc_security) (struct super_block *sb); //Deallocate and clear the sb->s_security field. void (*sb_free_security) (struct super_block *sb); //Allow mount option data to be copied prior to parsing by the filesystem, so that the security module can extract security-specific mount options cleanly (a filesystem may modify the data e.g. with strsep()). int (*sb_copy_data) (char *orig, char *copy); //Extracts security system specific mount options and verifies no changes are being made to those options. int (*sb_remount) (struct super_block *sb, void *data); int (*sb_kern_mount) (struct super_block *sb, int flags, void *data); int (*sb_show_options) (struct seq_file *m, struct super_block *sb); //Check permission before obtaining filesystem statistics for the @mnt mountpoint. int (*sb_statfs) (struct dentry *dentry); //Check permission before an object specified by @dev_name is mounted on the mount point named by @nd. int (*sb_mount) (const char *dev_name, struct path *path, const char *type, unsigned long flags, void *data); //Check permission before the @mnt file system is unmounted. int (*sb_umount) (struct vfsmount *mnt, int flags); //Check permission before pivoting the root filesystem. int (*sb_pivotroot) (struct path *old_path, struct path *new_path); //Set the security relevant mount options used for a superblock int (*sb_set_mnt_opts) (struct super_block *sb, struct security_mnt_opts *opts, unsigned long kern_flags, unsigned long *set_kern_flags); //Copy all security options from a given superblock to another int (*sb_clone_mnt_opts) (const struct super_block *oldsb, struct super_block *newsb); //Parse a string of security data filling in the opts structure int (*sb_parse_opts_str) (char *options, struct security_mnt_opts *opts); //Compute a context for a dentry as the inode is not yet available since NFSv4 has no label backed by an EA anyway. int (*dentry_init_security) (struct dentry *dentry, int mode, struct qstr *name, void **ctx, u32 *ctxlen); #ifdef CONFIG_SECURITY_PATH int (*path_unlink) (struct path *dir, struct dentry *dentry); int (*path_mkdir) (struct path *dir, struct dentry *dentry, umode_t mode); int (*path_rmdir) (struct path *dir, struct dentry *dentry); int (*path_mknod) (struct path *dir, struct dentry *dentry, umode_t mode, unsigned int dev); int (*path_truncate) (struct path *path); int (*path_symlink) (struct path *dir, struct dentry *dentry, const char *old_name); int (*path_link) (struct dentry *old_dentry, struct path *new_dir, struct dentry *new_dentry); int (*path_rename) (struct path *old_dir, struct dentry *old_dentry, struct path *new_dir, struct dentry *new_dentry); int (*path_chmod) (struct path *path, umode_t mode); int (*path_chown) (struct path *path, kuid_t uid, kgid_t gid); int (*path_chroot) (struct path *path); #endif /* 3. Security hooks for inode operations. */ //Allocate and attach a security structure to @inode->i_security. int (*inode_alloc_security) (struct inode *inode); void (*inode_free_security) (struct inode *inode); //Obtain the security attribute name suffix and value to set on a newly created inode and set up the incore security field for the new inode. int (*inode_init_security) (struct inode *inode, struct inode *dir, const struct qstr *qstr, const char **name, void **value, size_t *len); int (*inode_create) (struct inode *dir, struct dentry *dentry, umode_t mode); int (*inode_link) (struct dentry *old_dentry, struct inode *dir, struct dentry *new_dentry); int (*inode_unlink) (struct inode *dir, struct dentry *dentry); int (*inode_symlink) (struct inode *dir, struct dentry *dentry, const char *old_name); int (*inode_mkdir) (struct inode *dir, struct dentry *dentry, umode_t mode); int (*inode_rmdir) (struct inode *dir, struct dentry *dentry); int (*inode_mknod) (struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev); int (*inode_rename) (struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry); int (*inode_readlink) (struct dentry *dentry); int (*inode_follow_link) (struct dentry *dentry, struct nameidata *nd); int (*inode_permission) (struct inode *inode, int mask); int (*inode_setattr) (struct dentry *dentry, struct iattr *attr); int (*inode_getattr) (struct vfsmount *mnt, struct dentry *dentry); int (*inode_setxattr) (struct dentry *dentry, const char *name, const void *value, size_t size, int flags); void (*inode_post_setxattr) (struct dentry *dentry, const char *name, const void *value, size_t size, int flags); int (*inode_getxattr) (struct dentry *dentry, const char *name); int (*inode_listxattr) (struct dentry *dentry); int (*inode_removexattr) (struct dentry *dentry, const char *name); int (*inode_need_killpriv) (struct dentry *dentry); int (*inode_killpriv) (struct dentry *dentry); int (*inode_getsecurity) (const struct inode *inode, const char *name, void **buffer, bool alloc); int (*inode_setsecurity) (struct inode *inode, const char *name, const void *value, size_t size, int flags); int (*inode_listsecurity) (struct inode *inode, char *buffer, size_t buffer_size); void (*inode_getsecid) (const struct inode *inode, u32 *secid); /* 4. Security hooks for file operations */ //Check file permissions before accessing an open file. This hook is called by various operations that read or write files. int (*file_permission) (struct file *file, int mask); //Allocate and attach a security structure to the file->f_security field int (*file_alloc_security) (struct file *file); //Deallocate and free any security structures stored in file->f_security. void (*file_free_security) (struct file *file); //Check permission for an ioctl operation on @file int (*file_ioctl) (struct file *file, unsigned int cmd, unsigned long arg); //Check permissions for a mmap operation at @addr. int (*mmap_addr) (unsigned long addr); //Check permissions for a mmap operation. int (*mmap_file) (struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags); //Check permissions before changing memory access permissions. int (*file_mprotect) (struct vm_area_struct *vma, unsigned long reqprot, unsigned long prot); //Check permission before performing file locking operations. int (*file_lock) (struct file *file, unsigned int cmd); //Check permission before allowing the file operation specified by @cmd from being performed on the file @file. int (*file_fcntl) (struct file *file, unsigned int cmd, unsigned long arg); int (*file_set_fowner) (struct file *file); int (*file_send_sigiotask) (struct task_struct *tsk, struct fown_struct *fown, int sig); int (*file_receive) (struct file *file); //Save open-time permission checking state for later use upon file_permission, and recheck access if anything has changed since inode_permission int (*file_open) (struct file *file, const struct cred *cred); /* 5. Security hooks for task operations. LSM(Linux Security Modules)提供了一系列的任务钩子使得安全模块可以管理进程的安全信息并且控制进程的操作 1) LSM可以使用"task_struct"结构中的安全域来维护进程安全信息 2) 任务钩子提供了控制进程间通信的钩子,例如kill() 3) LSM提供了控制对当前进程进行特权操作的钩子,例如setuid() 4) LSM提供了对资源管理操作进行细粒度控制的钩子,例如setrlimit()和nice() */ //Check permission before creating a child process int (*task_create) (unsigned long clone_flags); //@task task being freed Handle release of task-related resources. (Note that this can be called from interrupt context.) void (*task_free) (struct task_struct *task); //Only allocate sufficient memory and attach to @cred such that cred_transfer() will not get ENOMEM. int (*cred_alloc_blank) (struct cred *cred, gfp_t gfp); //eallocate and clear the cred->security field in a set of credentials. void (*cred_free) (struct cred *cred); //Prepare a new set of credentials by copying the data from the old set. int (*cred_prepare)(struct cred *new, const struct cred *old, gfp_t gfp); //Transfer data from original creds to new creds void (*cred_transfer)(struct cred *new, const struct cred *old); //Set the credentials for a kernel service to act as (subjective context). int (*kernel_act_as)(struct cred *new, u32 secid); int (*kernel_create_files_as)(struct cred *new, struct inode *inode); int (*kernel_module_request)(char *kmod_name); int (*kernel_module_from_file)(struct file *file); //Update the module's state after setting one or more of the user identity attributes of the current process. int (*task_fix_setuid) (struct cred *new, const struct cred *old, int flags); int (*task_setpgid) (struct task_struct *p, pid_t pgid); int (*task_getpgid) (struct task_struct *p); int (*task_getsid) (struct task_struct *p); void (*task_getsecid) (struct task_struct *p, u32 *secid); int (*task_setnice) (struct task_struct *p, int nice); int (*task_setioprio) (struct task_struct *p, int ioprio); int (*task_getioprio) (struct task_struct *p); int (*task_setrlimit) (struct task_struct *p, unsigned int resource, struct rlimit *new_rlim); int (*task_setscheduler) (struct task_struct *p); int (*task_getscheduler) (struct task_struct *p); int (*task_movememory) (struct task_struct *p); int (*task_kill) (struct task_struct *p, struct siginfo *info, int sig, u32 secid); int (*task_wait) (struct task_struct *p); //Check permission before performing a process control operation on the current process. int (*task_prctl) (int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5); void (*task_to_inode) (struct task_struct *p, struct inode *inode); int (*ipc_permission) (struct kern_ipc_perm *ipcp, short flag); void (*ipc_getsecid) (struct kern_ipc_perm *ipcp, u32 *secid); int (*msg_msg_alloc_security) (struct msg_msg *msg); void (*msg_msg_free_security) (struct msg_msg *msg); int (*msg_queue_alloc_security) (struct msg_queue *msq); void (*msg_queue_free_security) (struct msg_queue *msq); int (*msg_queue_associate) (struct msg_queue *msq, int msqflg); int (*msg_queue_msgctl) (struct msg_queue *msq, int cmd); int (*msg_queue_msgsnd) (struct msg_queue *msq, struct msg_msg *msg, int msqflg); int (*msg_queue_msgrcv) (struct msg_queue *msq, struct msg_msg *msg, struct task_struct *target, long type, int mode); int (*shm_alloc_security) (struct shmid_kernel *shp); void (*shm_free_security) (struct shmid_kernel *shp); int (*shm_associate) (struct shmid_kernel *shp, int shmflg); int (*shm_shmctl) (struct shmid_kernel *shp, int cmd); int (*shm_shmat) (struct shmid_kernel *shp, char __user *shmaddr, int shmflg); int (*sem_alloc_security) (struct sem_array *sma); void (*sem_free_security) (struct sem_array *sma); int (*sem_associate) (struct sem_array *sma, int semflg); int (*sem_semctl) (struct sem_array *sma, int cmd); int (*sem_semop) (struct sem_array *sma, struct sembuf *sops, unsigned nsops, int alter); /* 6. Security hooks for Netlink messaging. */ //Save security information for a netlink message so that permission checking can be performed when the message is processed. int (*netlink_send) (struct sock *sk, struct sk_buff *skb); void (*d_instantiate) (struct dentry *dentry, struct inode *inode); int (*getprocattr) (struct task_struct *p, char *name, char **value); int (*setprocattr) (struct task_struct *p, char *name, void *value, size_t size); int (*ismaclabel) (const char *name); int (*secid_to_secctx) (u32 secid, char **secdata, u32 *seclen); int (*secctx_to_secid) (const char *secdata, u32 seclen, u32 *secid); void (*release_secctx) (char *secdata, u32 seclen); int (*inode_notifysecctx)(struct inode *inode, void *ctx, u32 ctxlen); int (*inode_setsecctx)(struct dentry *dentry, void *ctx, u32 ctxlen); int (*inode_getsecctx)(struct inode *inode, void **ctx, u32 *ctxlen); #ifdef CONFIG_SECURITY_NETWORK /* 7. Security hooks for Unix domain networking. */ //Check permissions before establishing a Unix domain stream connection between @sock and @other. int (*unix_stream_connect) (struct sock *sock, struct sock *other, struct sock *newsk); //Check permissions before connecting or sending datagrams from @sock to @other. int (*unix_may_send) (struct socket *sock, struct socket *other); int (*socket_create) (int family, int type, int protocol, int kern); int (*socket_post_create) (struct socket *sock, int family, int type, int protocol, int kern); int (*socket_bind) (struct socket *sock, struct sockaddr *address, int addrlen); //Check permission before socket protocol layer connect operation attempts to connect socket @sock to a remote address, int (*socket_connect) (struct socket *sock, struct sockaddr *address, int addrlen); int (*socket_listen) (struct socket *sock, int backlog); int (*socket_accept) (struct socket *sock, struct socket *newsock); int (*socket_sendmsg) (struct socket *sock, struct msghdr *msg, int size); int (*socket_recvmsg) (struct socket *sock, struct msghdr *msg, int size, int flags); int (*socket_getsockname) (struct socket *sock); int (*socket_getpeername) (struct socket *sock); int (*socket_getsockopt) (struct socket *sock, int level, int optname); int (*socket_setsockopt) (struct socket *sock, int level, int optname); int (*socket_shutdown) (struct socket *sock, int how); int (*socket_sock_rcv_skb) (struct sock *sk, struct sk_buff *skb); int (*socket_getpeersec_stream) (struct socket *sock, char __user *optval, int __user *optlen, unsigned len); int (*socket_getpeersec_dgram) (struct socket *sock, struct sk_buff *skb, u32 *secid); int (*sk_alloc_security) (struct sock *sk, int family, gfp_t priority); void (*sk_free_security) (struct sock *sk); void (*sk_clone_security) (const struct sock *sk, struct sock *newsk); void (*sk_getsecid) (struct sock *sk, u32 *secid); void (*sock_graft) (struct sock *sk, struct socket *parent); int (*inet_conn_request) (struct sock *sk, struct sk_buff *skb, struct request_sock *req); void (*inet_csk_clone) (struct sock *newsk, const struct request_sock *req); void (*inet_conn_established) (struct sock *sk, struct sk_buff *skb); int (*secmark_relabel_packet) (u32 secid); void (*secmark_refcount_inc) (void); void (*secmark_refcount_dec) (void); void (*req_classify_flow) (const struct request_sock *req, struct flowi *fl); int (*tun_dev_alloc_security) (void **security); void (*tun_dev_free_security) (void *security); int (*tun_dev_create) (void); int (*tun_dev_attach_queue) (void *security); int (*tun_dev_attach) (struct sock *sk, void *security); int (*tun_dev_open) (void *security); void (*skb_owned_by) (struct sk_buff *skb, struct sock *sk); #endif /* CONFIG_SECURITY_NETWORK */ #ifdef CONFIG_SECURITY_NETWORK_XFRM /* 8. Security hooks for XFRM operations. */ int (*xfrm_policy_alloc_security) (struct xfrm_sec_ctx **ctxp, struct xfrm_user_sec_ctx *sec_ctx, gfp_t gfp); int (*xfrm_policy_clone_security) (struct xfrm_sec_ctx *old_ctx, struct xfrm_sec_ctx **new_ctx); void (*xfrm_policy_free_security) (struct xfrm_sec_ctx *ctx); int (*xfrm_policy_delete_security) (struct xfrm_sec_ctx *ctx); int (*xfrm_state_alloc) (struct xfrm_state *x, struct xfrm_user_sec_ctx *sec_ctx); int (*xfrm_state_alloc_acquire) (struct xfrm_state *x, struct xfrm_sec_ctx *polsec, u32 secid); void (*xfrm_state_free_security) (struct xfrm_state *x); int (*xfrm_state_delete_security) (struct xfrm_state *x); int (*xfrm_policy_lookup) (struct xfrm_sec_ctx *ctx, u32 fl_secid, u8 dir); int (*xfrm_state_pol_flow_match) (struct xfrm_state *x, struct xfrm_policy *xp, const struct flowi *fl); int (*xfrm_decode_session) (struct sk_buff *skb, u32 *secid, int ckall); #endif /* CONFIG_SECURITY_NETWORK_XFRM */ /* key management security hooks */ #ifdef CONFIG_KEYS /* 9. Security hooks affecting all Key Management operations */ int (*key_alloc) (struct key *key, const struct cred *cred, unsigned long flags); void (*key_free) (struct key *key); int (*key_permission) (key_ref_t key_ref, const struct cred *cred, key_perm_t perm); int (*key_getsecurity)(struct key *key, char **_buffer); #endif /* CONFIG_KEYS */ #ifdef CONFIG_AUDIT int (*audit_rule_init) (u32 field, u32 op, char *rulestr, void **lsmrule); int (*audit_rule_known) (struct audit_krule *krule); int (*audit_rule_match) (u32 secid, u32 field, u32 op, void *lsmrule, struct audit_context *actx); void (*audit_rule_free) (void *lsmrule); #endif /* CONFIG_AUDIT */ };
0x2: LSM在Linux Kernel函数的Hook点:security_file_mmap
LSM以串行插入的方式集成到了Linux Kernel Function中的正常函数中,以"security_file_mmap"为例
source/security/security.c
int security_file_mmap( struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags, unsigned long addr, unsigned long addr_only) { int ret; //LSM Hook Function嵌入到了原始的内核函数的执行流程中 ret = security_ops->file_mmap(file, reqprot, prot, flags, addr, addr_only); if (ret) return ret; return ima_file_mmap(file, prot); }
Relevant Link:
http://lxr.free-electrons.com/source/security/security.c http://wenku.baidu.com/view/df89fe235901020207409c49.html
3. LSMs Hook Engine:基于LSM Hook进行元数据的监控获取
1. sys_fork 2. sys_execve、sys_connect 4. sys_init_module
0x1: sys_fork
Linux下进程创建的第一步就是执行fork,用该系统调用时,子进程复制父进程的全部资源。由于要复制父进程进程描述符给子进程(进程描述的结构很大),这一过程会消耗很大的性能开销。linux采用了"写时复制技术"(copy on write,COW),使子进程先共享父进程的物理页,只有子进程进行写操作时,再复制对应的物理页,避免了无用的复制开销,提高了系统的性能
source/arch/x86/kernel/process.c
int sys_fork(struct pt_regs *regs) { return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL); }
在do_fork中的LSM Hook Point如下
1. retval = security_task_create(clone_flags); 在copy_process中被调用 int security_task_create(unsigned long clone_flags) { return security_ops->task_create(clone_flags); }
在这个LSM Hook中,当前新创建的进程的命令行参数并没有传入进来
0x2: sys_execve hook
当用户态发起一个sys_execve()的系统调用,在内核态需要经过一系列的调用流程,我们逐一学习它的LSM Hook点
1. do_execve
/source/fs/exec.c
/* * sys_execve() executes a new program. */ int do_execve(char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs) { //将运行可执行文件时所需的信息组织到一起 struct linux_binprm *bprm; struct file *file; struct files_struct *displaced; bool clear_in_exec; int retval; retval = unshare_files(&displaced); if (retval) goto out_ret; retval = -ENOMEM; bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); if (!bprm) goto out_files; /* 1. LSM Hook Point 1: retval = prepare_bprm_creds(bprm); prepare_exec_creds->security_prepare_creds int security_prepare_creds(struct cred *new, const struct cred *old, gfp_t gfp) { return security_ops->cred_prepare(new, old, gfp); } */ retval = prepare_bprm_creds(bprm); if (retval) goto out_free; retval = check_unsafe_exec(bprm); if (retval < 0) goto out_free; clear_in_exec = retval; current->in_execve = 1; //找到并打开给定的可执行程序文件,open_exec()返回file结构指针,代表着读入可执行文件的上下文 file = open_exec(filename); //强制转换 retval = PTR_ERR(file); //判断open_exec()返回的是否是无效指针 if (IS_ERR(file)) goto out_unmark; sched_exec(); //要执行的文件 bprm->file = file; //要执行的文件的名字 bprm->filename = filename; bprm->interp = filename; retval = bprm_mm_init(bprm); if (retval) goto out_file; //统计命令汗参数的个数 bprm->argc = count(argv, MAX_ARG_STRINGS); if ((retval = bprm->argc) < 0) goto out; //统计环境变量参数的个数 bprm->envc = count(envp, MAX_ARG_STRINGS); if ((retval = bprm->envc) < 0) goto out; //可执行文件中读入开头的128个字节到linux_binprm结构brmp中的缓冲区,用于之后内核根据这头128字节判断应该调用哪个解析引擎来处理当前文件 retval = prepare_binprm(bprm); if (retval < 0) goto out; //文件名拷贝到新分配的页面中 retval = copy_strings_kernel(1, &bprm->filename, bprm); if (retval < 0) goto out; bprm->exec = bprm->p; //将环境变量拷贝到新分配的页面中 retval = copy_strings(bprm->envc, envp, bprm); if (retval < 0) goto out; //将命令行参数拷贝到新分配的页面中 retval = copy_strings(bprm->argc, argv, bprm); if (retval < 0) goto out; //所有准备工作已经完成,所有必要的信息都已经搜集到了linux_binprm结构中的bprm中 current->flags &= ~PF_KTHREAD; //调用search_binary_handler()装入并运行目标程序,根据读入数据结构linux_binprm内的二进制文件128字节头中的关键字,决定调用哪种加载函数 /* 2. LSM Hook Point 2: retval = security_bprm_check(bprm); int security_bprm_check(struct linux_binprm *bprm) { return security_ops->bprm_check_security(bprm); } */ retval = search_binary_handler(bprm, regs); if (retval < 0) goto out; /* execve succeeded */ current->fs->in_exec = 0; current->in_execve = 0; acct_update_integrals(current); free_bprm(bprm); if (displaced) put_files_struct(displaced); return retval; out: if (bprm->mm) { acct_arg_size(bprm, 0); mmput(bprm->mm); } out_file: //发生错误,返回inode,并释放资源 if (bprm->file) { //调用allow_write_access()防止其他进程在读入可执行文件期间通过内存映射改变它的内容 allow_write_access(bprm->file); //递减file文件中的共享计数 fput(bprm->file); } out_unmark: if (clear_in_exec) current->fs->in_exec = 0; current->in_execve = 0; out_free: free_bprm(bprm); out_files: if (displaced) reset_files_struct(displaced); out_ret: return retval; }
在do_execve中(search_binary_handler也包含在了do_execve中),包含了2个LSM Hook Point
1. prepare_bprm_creds:cred初始化过程的LSM Hook点 2. search_binary_handler:根据待执行文件的头128字节判断对应的解析处理引擎
对于这两个点,我们对比一下我们需要获取的元数据
1. current 2. argv
current(即task_struct)这个时候还未完全初始化完毕,例如"current->cred"还保存的是父进程的信息(因为linux下的所有进程都是通过父进程去"复制"出来的),代码在"execve_lsm_hook.c"
所以在这个LSM Hook Point上,不能直接获取current进行使用,是不准确的
另一方面,argv:retval = copy_strings(bprm->envc, envp, bprm);完成了将当前启动进程的命令行参数复制到内核栈的内存页中,将页的偏移地址保存在了"linux_binprm->p"指针中,这个p指针保存的是这个内存页的偏移,需要将参数从这块虚拟页内存中copy出来,如果直接打印printk会导致kernel panic(可以将那行注释去掉测试)
综上所述,这两个Hook点都不能满足我们进行execve进程执行元数据收集的目的,我们继续往下寻找
2. load_elf_binary
在do_execve()函数的准备阶段,已经从可执行文件头部读入128字节存放在bprm的缓冲区中,而且运行所需的参数和环境变量也已收集在bprm中
search_binary_handler()函数就是逐个扫描formats队列,直到找到一个匹配的可执行文件格式,运行的事就交给它
1. 如果在这个队列中没有找到相应的可执行文件格式,就要根据文件头部的信息来查找是否有为此种格式设计的可动态安装的模块 2. 如果找到对应的可执行文件格式,就把这个模块安装进内核,并挂入formats队列,然后再重新扫描
在linux_binfmt数据结构中,有三个函数指针
1. load_binary load_binary就是具体的ELF程序装载程序,不同的可执行文件其装载函数也不同 1) a.out格式的装载函数为: load_aout_binary() 2) elf的装载函数为: load_elf_binary() 2. load_shlib 3. core_dump
我们的Hook对象是ELF可执行程序的执行,因此,我们继续深入研究load_elf_binary这个函数
... #ifdef CONFIG_MODULES } else { #define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e)) if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && printable(bprm->buf[2]) && printable(bprm->buf[3])) break; /* -ENOEXEC */ request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2])); #endif ...
内核会根据bprm结构体中判断得到的文件格式类型,加载对应的解析函数,对于ELF文件来说就是binfmt_elf.c
source/fs/binfmt_elf.c
在load_elf_binary()函数中,有两个LSM Hook Point
1. file_permission(interpreter, MAY_READ) int file_permission(struct file *file, int mask) { return inode_permission(file->f_path.dentry->d_inode, mask); } 2. install_exec_creds(bprm); void install_exec_creds(struct linux_binprm *bprm) { security_bprm_committing_creds(bprm); commit_creds(bprm->cred); bprm->cred = NULL; /* * cred_guard_mutex must be held at least to this point to prevent * ptrace_attach() from altering our determination of the task's * credentials; any time after this it may be unlocked. */ security_bprm_committed_creds(bprm); mutex_unlock(¤t->cred_guard_mutex); } .. void security_bprm_committed_creds(struct linux_binprm *bprm) { security_ops->bprm_committed_creds(bprm); }
在load_elf_binary()函数中,有一行代码需要重点关注
retval = create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr);
跟进函数进行分析
..... p = current->mm->arg_end = current->mm->arg_start; while (argc-- > 0) { size_t len; if (__put_user((elf_addr_t)p, argv++)) { return -EFAULT; } len = strnlen_user((void __user *)p, MAX_ARG_STRLEN); if (!len || len > MAX_ARG_STRLEN) { return -EINVAL; } p += len; } if (__put_user(0, argv)) { return -EFAULT; } ....
内核代码在这里将当前进程的参数保存在current->mm->arg_start和current->mm->arg_end之间的内存空间中,这也意味着在调用了create_elf_tables之后,我们就可以在current当中获取到当前进程的命令行参数了
3. do_mmap
从源代码中可以看到,在load_elf_binary中的do_mmap之前,调用了create_elf_tables,所以我们一定可以在do_mmap中寻找到一个hook点,在这个hook点当中可以通过current->mm->arg_start和current->mm->arg_end可以拿到进程的参数
\linux-2.6.32.63\include\linux\mm.h
/* file:表示要映射的文件 addr:虚拟空间中的一个地址,表示从这个地址开始查找一个空闲的虚拟区 len:要映射的文件部分的长度 prot:这个参数指定对这个虚拟区所包含页的存取权限。可能的标志有 1) PROT_READ 2) PROT_WRITE 3) PROT_EXEC 4) PROT_NONE 前三个标志与标志VM_READ、VM_WRITE 及VM_EXEC的意义一样。PROT_NONE表示进程没有以上三个存取权限中的任意一个 Flag:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体 1) MAP_FIXED: 使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上 2) MAP_SHARED: 与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新 3) MAP_PRIVATE: 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个 4) MAP_DENYWRITE: 这个标志被忽略 5) MAP_EXECUTABLE: 这个标志被忽略 6) MAP_NORESERVE: 不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号 7) MAP_LOCKED: 锁定映射区的页面,从而防止页面被交换出内存 8) MAP_GROWSDOWN: 用于堆栈,告诉内核VM系统,映射区可以向下扩展 9) MAP_ANONYMOUS: 匿名映射,映射区不与任何文件关联 10) MAP_ANON: MAP_ANONYMOUS的别称,不再被使用。 11) MAP_FILE: 兼容标志,被忽略 12) MAP_32BIT: 将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持 13) MAP_POPULATE: 为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞 14) MAP_NONBLOCK: 仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。 off:文件内的偏移量,因为我们并不是一下子全部映射一个文件,可能只是映射文件的一部分,off就表示那部分的起始位置 */ static inline unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) { unsigned long ret = -EINVAL; if ((offset + PAGE_ALIGN(len)) < offset) goto out; if (!(offset & ~PAGE_MASK)) //do_mmap()函数对参数offset的合法性检查后,就调用do_mmap_pgoff()函数,该函数才是内存映射的主要函数 ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT); out:1 return ret; }
\linux-2.6.32.63\mm\mmap.c
在do_mmap_pgoff中可以找到的LSM Hook Point如下
1. error = security_file_mmap(NULL, 0, 0, 0, addr, 1); int security_file_mmap(struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags, unsigned long addr, unsigned long addr_only) { return security_ops->file_mmap(file, reqprot, prot, flags, addr, addr_only); }
在这个LSM Hook点:security_ops->file_mmap中,代码截获到的是当前系统所有的文件映射请求,如果需要专门针对ELF可执行文件镜像的file_mmap,需要针对ELF镜像头作判断,针对ELF可执行文件的file_mmap进行,数据量很大,会对性能产生影响,
Relevant Link:
http://blog.csdn.net/edwardlulinux/article/details/7998433
4. 在通用的LSM Hook点都无法直接获取参数的情况下如何通过Hacking方式获取进程当前命令行参数
LSM Hook框架是一种访问控制的审计型框架,它的主要目的是对进程执行的身份权限、文件名等信息进行权限审计,对进程命令行参数这些元数据的捕获并不是那么方便,目前遇到的难点情况如下
1. 直接通过最靠近do_execve的LSM Hook接口:bprm_check_security进行Hook,这个时候拿到的arg和current都是不准确的 2. 将Hook点下移,通过do_mmap中的LSM Hook接口:file_mmap进行Hook,参数arg和current是可以取到且也是准确的,但是这个函数的上层调用频率太高,需要ELF镜像头部进行区分,对系统运行性能影响较大
解决这个问题的思路可以朝这个方向思考
1. 将LSM Hook点尽量上移,在执行流支的尽量上游进行Hook,减少冗余调用频率,提高Hook命中率 2. 因为Hook上移,不可避免带来的问题就是,原始Linux内核代码的执行在这个点还未进行,很多变量还没有赋值、或者设置好 3. 我们的代码需要针对我们所需的变量,将从当前代码逻辑位置后面的和这些变量相关的内核代码拷贝过来,我们在代码中进行模拟执行,即将原本内核做的事情,我们提前做了 4. 但是需要注意的是,我们模拟执行的代码不能对正常的内核函数的执行流支产生任何影响,从程序的角度来说,就是只能拷贝模拟执行那种只获取(get),而不设置(set)的代码逻辑
基于以上思考,我们有了下面的Hacking思路
1. 选取"security_ops->bprm_committed_creds"作为LSM Hook点 2. "security_ops->bprm_committed_creds"在load_elf_binary的install_exec_creds中被调用,而install_exec_creds在create_elf_tables的前面,这个时候current->mm->arg_end还未被设置成命令行参数的结束位置、current->mm->arg_start已经是命令行参数的起始位置了,我们需要仿照create_elf_tables中对current->mm->arg_end的计算代码进行模拟执行,得到arg_end,要注意的是,为了保持系统状态一致性,不能去修改current->mm->arg_end 3. 计算好arg_start、arg_end之后,我们继续模拟proc_pid_cmdline这个函数从arg_start、arg_end中获取到当前进程的命令行参数
按照这个思路,我们进行代码DEMO实现
我们来关注load_elf_binary这个函数中,看到这行代码:
retval = create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr);
source/fs/binfmt_elf.c
.... p = current->mm->arg_end = current->mm->arg_start; while (argc-- > 0) { size_t len; if (__put_user((elf_addr_t)p, argv++)) return -EFAULT; len = strnlen_user((void __user *)p, MAX_ARG_STRLEN); if (!len || len > MAX_ARG_STRLEN) return -EINVAL; p += len; } if (__put_user(0, argv)) return -EFAULT; /* current->mm->arg_start指向的已经是当前进程命令行参数的起始地址 但是未将current->mm->arg_end设置到命令行参数的终止位置(即arg_start + arg_len) */ current->mm->arg_end = current->mm->env_start = p; .....
看这段代码,我们要明白的是,Linux将当前进程的命令行参数和环境变量参数都统一放在了一整块内核内存空间中了,而使用arg_start + offset、arg_len来进行区分它们,所以,如果我们想从中取出命令行参数,就需要仿照Linux内核的代码,对arg_start、arg_end进行计算,并得到len,这样才能从current->mm指向的内存中拷贝出我们想要的
int fake_cmdline(int argc) { char buffer[2048] = {0}; unsigned long start ,end, p; start = current->mm->arg_start; end = current->mm->arg_end; p = current->mm->arg_start; printk("argc = %d, start =%p, end=%p, p =%p\n", argc, start, end, p); while (argc-- > 0) { size_t len; len = strnlen_user((void __user *)p, MAX_ARG_STRLEN); if (!len || len > MAX_ARG_STRLEN) return -EINVAL; p += len; } end = p; printk("argc = %d, start =%p, end=%p, p =%p\n", argc, start, end, p); proc_cmdline(start, end , buffer); return 0; }
拷贝出来之后,我们需要根据arg_start、arg_end从内核内存地址空间中直接复制出参数字符串,模拟proc_pid_cmdline这个函数的代码逻辑
\linux-2.6.32.63\fs\proc\base.c
static int proc_pid_cmdline(struct task_struct *task, char * buffer) { int res = 0; unsigned int len; struct mm_struct *mm = get_task_mm(task); if (!mm) goto out; if (!mm->arg_end) goto out_mm; /* Shh! No looking before we're done */ len = mm->arg_end - mm->arg_start; if (len > PAGE_SIZE) len = PAGE_SIZE; res = access_process_vm(task, mm->arg_start, buffer, len, 0); // If the nul at the end of args has been overwritten, then // assume application is using setproctitle(3). if (res > 0 && buffer[res-1] != '\0' && len < PAGE_SIZE) { len = strnlen(buffer, res); if (len < res) { res = len; } else { len = mm->env_end - mm->env_start; if (len > PAGE_SIZE - res) len = PAGE_SIZE - res; res += access_process_vm(task, mm->env_start, buffer+res, len, 0); res = strnlen(buffer, res); } } out_mm: mmput(mm); out: return res; }
我们只要完成以上步骤,理论上应该是可以拿到进程的命令行参数的,在进行实际编码的时候,从最佳实践的角度来说,我们的代码中应该注意以下几点
1. 尽量少的使用拷贝的源代码,而是采用系统原生提供的函数去实现功能,但是要注意的是,如果这个使用到的系统原生函数是会"改变当前系统运行状态"(例如设置全局变量、改变全局指针),就绝对不能使用,而只能采用拷贝源代码自己实现的方式,将其中会对当前系统状态产生影响的代码去掉,而只保留那些"取值型"、"临时变量计算型"的非核心代码
2. 我们拷贝的执行代码一定要特别注意对全局变量、全局指针的操作,不能因为我们的模拟执行,而在当前LSM Hook点影响到了Linux内核的状态,否则这样往下执行系统就要处于不一致状态了
3. Linux下进程都是通过父进程复制出来的,在bash下执行指令是通过bash复制出来的,而current值(即新进程的task_struct)是在整个sys_execve过程中不断完成初始化填充的,我们需要确认我们的hook点取到的current值已经是新进程的current值
代码示例见: "4. LSM编程示例->0x3: 针对sys_execve使用LSM Hook方式获取进程命令行参数"
到目前为止,我们已经解决了针对sys_execve进程执行的LSM Hook,并获取到了进程启动时的命令行参数,接下来继续研究一下如何Hook监控当前系统的网络连接状态,即对socket的LSM Hook
5. 借助remove_arg_zero直接从内核栈的内存页中拷贝获得进程参数
/source/fs/exec.c
/* Arguments are '\0' separated strings found at the location bprm->p points to; chop off the first by relocating brpm->p to right after the first '\0' encountered. */ int remove_arg_zero(struct linux_binprm *bprm) { int ret = 0; unsigned long offset; char *kaddr; struct page *page; if (!bprm->argc) return 0; do { offset = bprm->p & ~PAGE_MASK; page = get_arg_page(bprm, bprm->p, 0); if (!page) { ret = -EFAULT; goto out; } kaddr = kmap_atomic(page, KM_USER0); for (; offset < PAGE_SIZE && kaddr[offset]; offset++, bprm->p++) ; kunmap_atomic(kaddr, KM_USER0); put_arg_page(page); if (offset == PAGE_SIZE) free_arg_page(bprm, (bprm->p >> PAGE_SHIFT) - 1); } while (offset == PAGE_SIZE); bprm->p++; bprm->argc--; ret = 0; out: return ret; } EXPORT_SYMBOL(remove_arg_zero);
remove_arg_zero(bprm)是把argv[0]中的'\0'从bprm->page中清除,我们在bprm_check_security进行Hook的时候,通过调用这个内核函数之后,可以直接从bprm->page中获取到进程的启动参数,从而调用copy函数进行拷贝
0x3: sys_connect
使用LSM(Linux Security Modules)框架的一个最大的好处就是Linux Kernel帮助屏蔽了底层的差异性,在Linux 32bit/64bit和socket系统调用的差异性在LSM Hook这一层都不存在了,而取而代之的是统一的Hook接口
.... int (*socket_create) (int family, int type, int protocol, int kern); int (*socket_post_create) (struct socket *sock, int family, int type, int protocol, int kern); int (*socket_bind) (struct socket *sock, struct sockaddr *address, int addrlen); int (*socket_connect) (struct socket *sock, struct sockaddr *address, int addrlen); int (*socket_listen) (struct socket *sock, int backlog); int (*socket_accept) (struct socket *sock, struct socket *newsock); int (*socket_sendmsg) (struct socket *sock, struct msghdr *msg, int size); int (*socket_recvmsg) (struct socket *sock, struct msghdr *msg, int size, int flags); ....
我们以socket_connect作为研究对象
关于和socket相关的数据结构定义,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:6. 系统网络状态相关的数据结构
在我们这个LSM Hook Engine的业务场景下,我们如果需要针对socket_connect动作进行Hook监控,我们要解决的问题有
1. struct socket、inet_sk((struct socket)->(struc sock))、struct sockaddr这3中结构体中,要从中筛选出准确保存当前socket连接动作的IP、PORT等信息的字段 2. LSM的security_socket_connect()函数会在一次socket连接过程中被多次调用,包括从初始化到最后建立完整连接,我们需要选择相关type字段,对当前的状态进行过滤,过滤出TCP Connect完整建立的那次调用
1. LSM Hook点被多次调用的问题
\linux-2.6.32.63\net\socket.c
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen) {
.../* 调用该BSD socket对应的connect调用 这里的sock是在前面的socket调用的时候就初始化的,对应于TCP的BSD socket的操作符集是inet_stream_ops 要注意的是: 网络子模块在内核初始化的时候(linux-2.6.32.63\net\ipv4\af_inet.c->inet_init()中)就注册了TCP,UDP和RAW3中协议 linux-2.6.32.63\net\ipv4\af_inet.c const struct proto_ops inet_stream_ops = { .family = PF_INET, .owner = THIS_MODULE, .release = inet_release, .bind = inet_bind, .connect = inet_stream_connect, .socketpair = sock_no_socketpair, .accept = inet_accept, .getname = inet_getname, .poll = tcp_poll, .ioctl = inet_ioctl, .listen = inet_listen, .shutdown = inet_shutdown, .setsockopt = sock_common_setsockopt, .getsockopt = sock_common_getsockopt, .sendmsg = tcp_sendmsg, .recvmsg = sock_common_recvmsg, .mmap = sock_no_mmap, .sendpage = tcp_sendpage, .splice_read = tcp_splice_read, #ifdef CONFIG_COMPAT .compat_setsockopt = compat_sock_common_setsockopt, .compat_getsockopt = compat_sock_common_getsockopt, #endif }; 从上面的结构中可以看到connect对应的函数是inet_stream_connect,我们继续分析inet_stream_connect函数 */ err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); out_put: ... }
\linux-2.6.32.63\net\ipv4\af_inet.c
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags) { struct sock *sk = sock->sk; ... switch (sock->state) { default: err = -EINVAL; goto out; /* 该BSD socket已连接 */ case SS_CONNECTED: err = -EISCONN; goto out; /* 该BSD socket正在连接 */ case SS_CONNECTING: err = -EALREADY; /* Fall out of switch with err, set for this state */ break; case SS_UNCONNECTED: err = -EISCONN; if (sk->sk_state != TCP_CLOSE) goto out; err = sk->sk_prot->connect(sk, uaddr, addr_len); if (err < 0) goto out; /* 上面的调用完成后,连接并没有完成 */ sock->state = SS_CONNECTING; /* Just entered SS_CONNECTING state; the only * difference is that return value in non-blocking * case is EINPROGRESS, rather than EALREADY. */ err = -EINPROGRESS; break; } ... } EXPORT_SYMBOL(inet_stream_connect);
从源代码里可以看到,security_socket_connect()在每次sys_connect被调用时都会被调用,而我们的目的只是针对TCP Connect这个动作捕获一次包含实际数据的socket动作即可,因此需要对security_socket_connect进行过滤,可以用于过滤的字段有
1. struct inet_sock->is_icsk 2. struct socket->state 3. struct socket->type 4. struct sockaddr->sa_family
除此之外,针对socket中的IP、Port等信息的获取也存在问题,代码实现请参阅"0x4: 针对sys_connect使用LSM Hook方式获取socke TCP Connect连接的IP、Port参数"
参考一下TOMOYO Linux的做法,相关知识请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/4149509.html
我们可以模仿TOMOYO从SOCKET相关数据结构中获取IP、PORT的做法,在编程中,我们需要注意以下几点
1. socket_connect的被调用频率是很高的,需要针对TCP Connect(IPv4、IPv6)进行过滤 1) struct socket->type 1.1) case SOCK_STREAM: stream (connection) socket 1.2) case SOCK_SEQPACKET: sequential packet socket //从所有sys_connect动作中过滤出TCP Socket之后,继续下一层过滤 2) struct socket->state 2.1) case SS_UNCONNECTED: 只有在这个状态下,内核的SOCKET代码才会调用inet_stream_connect发起TCP连接 //过滤出TCP connect连接之后,继续下一层过滤 3) struct sockaddr->sa_family 3.1) AF_INET6 3.2) AF_INET 2. 从网络的角度理解,SOCKET的建立本质上一次网络状态的建立过程,它有很多次的握手交互子过程组成,每一次获取和设置的信息都似乎不同的 1) 对于kretprobe来说,它是在整个sys_socketcall调用结束之后才获得回调触发的,此时SOCKET的相关信息已经全部填充完成,是可以获取到参数的 2) 而对于LSM Hook框架来说,针对sys_connect的LSM Hook点是在函数的入口就被触发调用的,这个时候,TCP Connect的建立还未完成,有很多的元数据是拿不到的
翻阅sys_connect的内核代码可以得出以下结论
1. 目的(对端)SOCKET信息(IP、PORT) 在sys_connect传入的"struct sockaddr *uaddr"中保存,这个变量中保存的是最原始的数据,内核代码中其他的变量都是从这里开始获取的 /* ... struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; daddr = usin->sin_addr.s_addr; dport = usin->sin_port; ... */ 2. 源(本端)SOCKET信息(IP、PORT) 严格来说,源SOCKET信息应该是在sys_bind,即socket初始化建立的时候由用户传入配置的,这些源SOCKET信息在整个socket变量生命周期中都一直有效,所以,我们获取源SOCKET信息的来源应该是"struct sock *sk"变量 /* ... struct inet_sock *inet = inet_sk(sk); inet->sport inet->saddr */
所以,我们的LSMs代码中对源、目的SOCKET信息的获取应该分别从"struct sock *sk"、"struct sockaddr *uaddr"中获取
相关的代码,请参阅"0x4: 针对sys_connect使用LSM Hook方式获取socke TCP Connect连接的IP、Port参数"
对于使用LSMs Hook方式获取SOCKET相关信息,存在一点局限性,即sport无法获取到
\linux-2.6.32.63\net\ipv4\tcp_ipv4.c
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { ... /* Socket identity is still unknown (sport may be zero). * However we set state to SYN-SENT and not releasing socket * lock select source port, enter ourselves into the hash tables and * complete initialization after this. */ tcp_set_state(sk, TCP_SYN_SENT); err = inet_hash_connect(&tcp_death_row, sk); if (err) goto failure; err = ip_route_newports(&rt, IPPROTO_TCP, inet->sport, inet->dport, sk); if (err) goto failure; /* OK, now commit destination to socket. */ sk->sk_gso_type = SKB_GSO_TCPV4; sk_setup_caps(sk, &rt->u.dst); if (!tp->write_seq) tp->write_seq = secure_tcp_sequence_number(inet->saddr, inet->daddr, inet->sport, usin->sin_port); ...
在内核代码的注释中对这一现象作出了解释,因为TCP连接中的"源端口"是一个系统稀缺资源,内核在收到TCP Connect动作的时候,需要从本地"端口池(/proc/sys/net/ipv4/ip_local_port_range 中配置)"中选取一个可用的端口,并分配给当前的socket,并填充路由表,然后才能发起真正的连接
关于内核中SOCKET端口选取的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3875451.html //搜索:2. connect() API原理
那现在问题来了,我们能不能像之前获取sys_execve的命令行参数一样,通过拷贝内核源代码,模拟这个端口选取的过程,在我们的LSMs代码中进行模拟执行,从而确定这个sport呢?答案是不行,原理还是一样的,我们要拷贝模拟执行的操作会对当前Linux内核的状态进行"改变",会破坏当前Kernel的一致性
1. 会改变当前socket的state状态 2. 会在hash表中增加entry 3. 这两个操作涉及到的内核代码很逗,且如果模拟执行后无法进行逆向回滚
暂时没有解决方案
Relevant Link:
0x4: sys_init_module
\linux-2.6.32.63\security\security.c
int security_kernel_create_files_as(struct cred *new, struct inode *inode) { return security_ops->kernel_create_files_as(new, inode); } int security_kernel_module_request(void) { return security_ops->kernel_module_request(); }
待研究
4. LSM编程示例
0x1: 获取当前执行进程的绝对路径
execve_lsm_hook.c
#include <linux/security.h> #include <linux/sysctl.h> #include <linux/ptrace.h> #include <linux/prctl.h> #include <linux/ratelimit.h> #include <linux/workqueue.h> #include <linux/file.h> #include <linux/fs.h> #include <linux/dcache.h> #include <linux/path.h> #include <linux/kprobes.h> #include <linux/module.h> static int (*register_security_p)(struct security_operations *ops); static int (*unregister_security_p)(struct security_operations * ops); // do nothing int kprobe_pre(struct kprobe *p, struct pt_regs *regs) { return 0; } static void* acquire_func_by_kprobe(char* func_name) { void *symbol_addr=NULL; struct kprobe kp; do { memset(&kp, 0, sizeof(kp)); kp.symbol_name = func_name; kp.pre_handler = kprobe_pre; if(register_kprobe(&kp) != 0) { symbol_addr=(void*)kp.addr; break; } //this is the address of "symbol_name" symbol_addr = (void*)kp.addr; //now kprobe is not used any more,so unregister it unregister_kprobe(&kp); }while(false); return symbol_addr; } int execve_lsm_hook(struct linux_binprm *bprm) { struct cred *currentCred; if (bprm->filename) { printk("file: %s\n", bprm->filename); //printk("file argument: %s\n",bprm->p); } else { printk("file exec\n"); } currentCred = current->cred; printk(KERN_INFO "uid = %d\n", currentCred->uid); printk(KERN_INFO "gid = %d\n", currentCred->gid); printk(KERN_INFO "suid = %d\n", currentCred->suid); printk(KERN_INFO "sgid = %d\n", currentCred->sgid); printk(KERN_INFO "euid = %d\n", currentCred->euid); printk(KERN_INFO "egid = %d\n", currentCred->egid); printk("comm: %s\n", current->comm); return 0; } static struct security_operations test_security_ops = { .name = "test", .bprm_check_security = execve_lsm_hook, }; static __init int test_init(void) { int ret_val = -1; //获取register_security的导出地址 register_security_p = acquire_func_by_kprobe("register_security"); printk("register_security:%p\n", register_security_p); if (!register_security_p) { printk("get register_security error\n"); } //获取unregister_security的导出地址 unregister_security_p = acquire_func_by_kprobe("unregister_security"); printk("unregister_security:%p\n", unregister_security_p); if (!unregister_security_p) { printk("get unregister_security error\n"); } //注册LSMsbas //if (register_security_p && unregister_security_p) if (register_security_p) { ret_val = register_security_p(&test_security_ops); printk("register_security:%d\n", ret_val); } return 0; } static void test_cleanup(void) { int ret_val=-1; //if (register_security_p && unregister_security_p) if (unregister_security_p) { ret_val = register_security_p(&test_security_ops); printk("register_security:%d\n", ret_val); } return; } module_init(test_init); module_exit(test_cleanup); //一定要有这个声明,否则会有unknown module symbol error MODULE_LICENSE("GPL");
Makefile
obj-m := execve_lsm_hook.o PWD := $(shell pwd) all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: rm -rf *.o *~ core .*.cmd *.mod.c ./tmp_version *.ko modules.order Module.symvers clean_omit: rm -rf *.o *~ core .*.cmd *.mod.c ./tmp_version modules.order Module.symvers
Relevant Link:
http://hi.baidu.com/wzt85/item/275a9651277866aaacc857d5
0x2: 通过arg_start、arg_end对应的内存获取进程参数
get_arc_from_current.c
#include <linux/module.h> // included for all kernel modules #include <linux/kernel.h> // included for KERN_INFO #include <linux/init.h> // included for __init and __exit macros #include <linux/cred.h> #include <linux/sched.h> #include <linux/mm_types.h> #include <linux//vmalloc.h> #include <linux/cache.h> #include <asm/cacheflush.h> #include <linux/pagemap.h> static void util_get_task_name(struct task_struct* arg_task) { struct task_struct *task = arg_task; char *name = NULL; char *argv = NULL; if(task != NULL) { name = kmalloc(PATH_MAX, GFP_ATOMIC); argv = kmalloc(PATH_MAX, GFP_ATOMIC); char *buffer = NULL; struct mm_struct *mm = get_task_mm(task); int len = 0; unsigned long address = 0; if(!name || !mm) goto out; down_read(&mm->mmap_sem); len = mm->arg_end - mm->arg_start; address = mm->arg_start; buffer = name; while(len) { void *maddr = NULL; struct page *page = NULL; struct vm_area_struct *vma = NULL; int bytes = 0, offset = 0; int ret = get_user_pages(task, mm, address, 1, 0, 1, &page, &vma); if(ret <= 0) { bytes = ret; goto next; } bytes = len; offset = address & (PAGE_SIZE-1); if (bytes > PAGE_SIZE-offset) { bytes = PAGE_SIZE - offset; } maddr = kmap(page); copy_from_user_page(vma, page, address, buffer, maddr + offset, bytes); kunmap(page); page_cache_release(page); next: len -= bytes; address += bytes; buffer += bytes; } up_read(&mm->mmap_sem); mmput(mm); printk("name = %s, len = %d\n", name, len); //这里需要调整一下参数,因为进程的参数是以空格作为分隔的,需要将以空格分隔的参数串分离出来分别打印 name += 6; printk("arg = %s, len = %d\n", name, len); } out: if(name) { kfree(name); } return; } static int __init hello_init(void) { struct cred *currentCred; currentCred = current->cred; printk(KERN_INFO "uid = %d\n", currentCred->uid); printk(KERN_INFO "gid = %d\n", currentCred->gid); printk(KERN_INFO "suid = %d\n", currentCred->suid); printk(KERN_INFO "sgid = %d\n", currentCred->sgid); printk(KERN_INFO "euid = %d\n", currentCred->euid); printk(KERN_INFO "egid = %d\n", currentCred->egid); util_get_task_name(current); printk(KERN_INFO "Hello world!\n"); return 0; // Non-zero return means that the module couldn't be loaded. } static void __exit hello_cleanup(void) { printk(KERN_INFO "Cleaning up module.\n"); } module_init(hello_init); module_exit(hello_cleanup); MODULE_LICENSE("GPL");
Makefile
obj-m := get_arc_from_current.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean
Relevant Link:
http://laokaddk.blog.51cto.com/368606/694037/
0x3: 针对sys_execve使用LSM Hook方式获取进程命令行参数
get_arc_by_hacking.c
#include <linux/module.h> // included for all kernel modules #include <linux/kernel.h> // included for KERN_INFO #include <linux/init.h> // included for __init and __exit macros #include <linux/cred.h> #include <linux/sched.h> #include <linux/mm_types.h> #include <linux//vmalloc.h> #include <linux/cache.h> #include <asm/cacheflush.h> #include <linux/pagemap.h> #ifndef MODULE #define MODULE #endif #ifndef __KERNEL__ #define __KERNEL__ #endif #include <linux/fs.h> #include <linux/file.h> #include <linux/kallsyms.h> #include <linux/syscalls.h> #include <asm/unistd.h> #include <linux/time.h> #include <linux/timer.h> #include <linux/kprobes.h> #include <linux/security.h> #include <linux/binfmts.h> #include <asm/current.h> #include <linux/threads.h> #include <linux/mm.h> #include <asm/page.h> static struct security_operations sec_ops; static int (*register_security_p)(struct security_operations *ops); static int (*unregister_security_p)(struct security_operations * ops); static int (*access_process_vm_p)(struct task_struct *tsk, unsigned long addr, void *buf, int len, int write); // do nothing int kprobe_pre(struct kprobe *p, struct pt_regs *regs) { return 0; } //使用kprobe获取内核中任意函数的地址 static void* acquire_func_by_kprobe(char* func_name) { void *symbol_addr = NULL; struct kprobe kp; do { memset(&kp,0,sizeof(kp)); kp.symbol_name=func_name; kp.pre_handler=kprobe_pre; if(register_kprobe(&kp)!=0) { symbol_addr=(void*)kp.addr; break; } symbol_addr=(void*)kp.addr; //now kprobe is not used any more,so unregister it unregister_kprobe(&kp); }while(false); return symbol_addr; } //根据arg_start、arg_end从内核内存地址空间中直接复制出参数字符串,模拟proc_pid_cmdline这个函数的代码逻辑 static int proc_cmdline(unsigned long start, unsigned long end, char * buffer) { int res = 0, i; unsigned int len; struct task_struct * task = current; struct mm_struct *mm = get_task_mm(task); //struct mm_struct *mm = bprm->mm; printk("proc_cmdline, mm = %p, arg_start = %d , arg_end=%d\n", mm, mm->arg_start, mm->arg_end); if (!mm) goto out; if (!mm->arg_end) { if(end <= start) goto out_mm; } len = end - start; printk("args len = %d\n", len); if (len > PAGE_SIZE) len = PAGE_SIZE; if (access_process_vm_p) { res = access_process_vm_p(task, mm->arg_start, buffer, len, 0); } //res = access_vm(task, mm->arg_start, buffer, len, 0); // If the nul at the end of args has been overwritten, then // assume application is using setproctitle(3). if (res > 0 && buffer[res-1] != '\0' && len < PAGE_SIZE) { len = strnlen(buffer, res); if (len < res) { res = len; } else { len = mm->env_end - mm->env_start; if (len > PAGE_SIZE - res) { len = PAGE_SIZE - res; } if (access_process_vm_p) { res += access_process_vm_p(task, mm->env_start, buffer+res, len, 0); } //res += access_vm(task, mm->env_start, buffer+res, len, 0); res = strnlen(buffer, res); } } printk("arg 1 :%s\n", buffer); //命令行参数是以空格结尾的,这里临时跳过这段空格,真正编码的时候需要针对空格作"replace"处理,才能正常打印出来 printk("arg 2 : %s\n", buffer+6); out_mm: mmput(mm); printk("out_mm\n"); out: printk("out\n"); return res; } void print_current() { struct cred *currentCred; currentCred = current->cred; printk(KERN_INFO "uid = %d\n", currentCred->uid); printk(KERN_INFO "gid = %d\n", currentCred->gid); //printk(KERN_INFO "suid = %d\n", currentCred->suid); //printk(KERN_INFO "sgid = %d\n", currentCred->sgid); printk(KERN_INFO "euid = %d\n", currentCred->euid); printk(KERN_INFO "egid = %d\n", currentCred->egid); printk(KERN_INFO "tid = %d\n", task_tgid_vnr(current)); printk(KERN_INFO "sid = %d\n", task_session_vnr(current)); printk(KERN_INFO "pid = %d\n", current->pid); printk(KERN_INFO "tgid = %d\n", current->tgid); printk(KERN_INFO "tty name: %s\n", current->signal->tty); printk(KERN_INFO "programe name: %s\n", current->comm); return; } //模拟内核代码对arg_start、arg_end的计算 int fake_cmdline(int argc) { char buffer[2048] = {0}; unsigned long start ,end, p; start = current->mm->arg_start; end = current->mm->arg_end; p = current->mm->arg_start; printk("argc = %d, start =%p, end=%p, p =%p\n", argc, start, end, p); while (argc-- > 0) { size_t len; len = strnlen_user((void __user *)p, MAX_ARG_STRLEN); if (!len || len > MAX_ARG_STRLEN) return -EINVAL; //将p指针指向当前进程参数的结束地址内存区 p += len; } end = p; printk("argc = %d, start =%p, end=%p, p =%p\n", argc, start, end, p); //根据arg_start、arg_end从内核内存地址空间中直接复制出参数字符串,模拟proc_pid_cmdline这个函数的代码逻辑 proc_cmdline(start, end , buffer); //打印current信息,验证当前LSM Hook点是否已经完成了current的赋值,因为Linux中所有的进程都是父进程"复制"出来的,在新进程创建的过程中,新进程的current是逐步填充设置为新进程的相关信息 print_current(); return 0; } void security_bprm_committed_creds(struct linux_binprm *bprm) { printk("security_bprm_committed_creds\n"); do { if (bprm->filename) { printk("in committed cred pid = %d, mm:%p, file:%s\n",current->pid, current->mm, bprm->filename); }else { printk("file exec\n"); } //模拟内核代码对arg_start、arg_end的计算 fake_cmdline(bprm->argc); } while (false); return; } static int __init hello_init(void) { int ret_val = -1; do { memset(&sec_ops, 0, sizeof(sec_ops)); sprintf(sec_ops.name,"aegis_lsm"); sec_ops.bprm_committed_creds = security_bprm_committed_creds; //获取access_process_vm函数的内核地址 access_process_vm_p = acquire_func_by_kprobe("access_process_vm"); printk("access_process_vm:%p\n", access_process_vm_p); if (!access_process_vm_p) { printk("get access_process_vm error\n"); } //获取LSM注册函数register_security的内核地址 register_security_p = acquire_func_by_kprobe("register_security"); printk("register_security:%p\n", register_security_p); if (!register_security_p) { printk("get register_security error\n"); } //获取LSM注销函数unregister_security的内核地址 unregister_security_p = acquire_func_by_kprobe("unregister_security"); printk("unregister_security:%p\n", unregister_security_p); if (!unregister_security_p) { printk("get unregister_security error\n"); } //注册LSM Hook模块 if (register_security_p) { ret_val = register_security_p(&sec_ops); printk("register_security successfully: %d\n", ret_val); break; } } while(false); printk(KERN_INFO "Hello world!\n"); return ret_val; } static void __exit hello_cleanup(void) { if (unregister_security_p) { unregister_security_p(&sec_ops); } printk(KERN_INFO "Cleaning up module.\n"); } module_init(hello_init); module_exit(hello_cleanup); MODULE_LICENSE("GPL");
0x4: 针对sys_connect使用LSM Hook方式获取socke TCP Connect连接的IP、Port参数
get_ip_by_lsm.c
#ifndef MODULE #define MODULE #endif #ifndef __KERNEL__ #define __KERNEL__ #endif #include <linux/kernel.h> #include <linux/init.h> #include <linux/kallsyms.h> #include <linux/syscalls.h> #include <asm/unistd.h> #include <linux/time.h> #include <linux/timer.h> #include <linux/security.h> #include <asm/current.h> #include <linux/threads.h> #include <linux/sched.h> #include <asm/page.h> #include <asm/cacheflush.h> #include <linux/pagemap.h> #include <linux/inet.h> #include <linux/ipv6.h> #include <linux/skbuff.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/string.h> #include <linux/version.h> #include <linux/module.h> #include <linux/binfmts.h> #include <linux/kprobes.h> #include <linux/slab.h> #include <linux/tty.h> //thread header #include <linux/kthread.h> #include <linux/mm.h> //modinfo header #include <linux/vmalloc.h> #include <net/ipv6.h> #if LINUX_VERSION_CODE < KERNEL_VERSION(3, 0, 25) #define SADDR saddr #define DADDR daddr #define SPORT sport #define DPORT dport #else #define SADDR inet_saddr #define DADDR inet_daddr #define SPORT inet_sport #define DPORT inet_dport #endif typedef void* (*look_up_symbol_t)(char*); static void* (*look_up_symbol)(char*); static struct security_operations org_sec_ops_back_up; static struct security_operations *org_sec_ops_p = NULL; static struct security_operations sec_ops; static int (*register_security_p)(struct security_operations *ops); static int (*unregister_security_p)(struct security_operations * ops); static unsigned int wpoff_cr0_32(void) { unsigned int cr0 = 0; unsigned int ret=0; asm volatile ("movl %%cr0, %%eax":"=a"(cr0)); //汇编代码,用于取出CR0寄存器的值 ret = cr0; cr0 &= 0xfffeffff; asm volatile ("movl %%eax, %%cr0": :"a"(cr0));//汇编代码,将修改后的CR0值写入CR0寄存器 return ret; } static unsigned long int wpoff_cr0_64(void) { unsigned long int cr0 = 0; unsigned long int ret=0; asm volatile ("movq %%cr0, %%rax":"=a"(cr0)); //汇编代码,用于取出CR0寄存器的值 ret = cr0; cr0 &= 0xfffffffffffeffff; asm volatile ("movq %%rax, %%cr0": :"a"(cr0));//汇编代码,将修改后的CR0值写入CR0寄存器 return ret; } static unsigned long int wpoff_cr0(void) { if(sizeof(void*)==4) { return wpoff_cr0_32(); }else { return wpoff_cr0_64(); } } static void set_cr0_32(unsigned int val) { asm volatile ("movl %%eax, %%cr0": :"a"(val)); return; } static void set_cr0_64(unsigned long int val) { asm volatile ("movq %%rax, %%cr0": :"a"(val)); return; } static void set_cr0(unsigned long int val) { if(sizeof(void*)==4) { set_cr0_32((unsigned int)val); }else { set_cr0_64(val); } } static void SLEEP_MILLI_SEC(int nMilliSec) { long timeout = (nMilliSec) * HZ / 1000; while(timeout > 0) { timeout = schedule_timeout(timeout); } } // do nothing int kprobe_pre(struct kprobe *p, struct pt_regs *regs) { return 0; } //使用kprobe获取内核中任意函数的地址 static void* acquire_func_by_kprobe(char* func_name) { void *symbol_addr = NULL; struct kprobe kp; do { memset(&kp,0,sizeof(kp)); kp.symbol_name=func_name; kp.pre_handler=kprobe_pre; if(register_kprobe(&kp)!=0) { symbol_addr=(void*)kp.addr; break; } symbol_addr=(void*)kp.addr; //now kprobe is not used any more,so unregister it unregister_kprobe(&kp); }while(false); return symbol_addr; } static look_up_symbol_t init_lookup_func(void) { if (!look_up_symbol) { look_up_symbol = (look_up_symbol_t)acquire_func_by_kprobe("kallsyms_lookup_name"); } return look_up_symbol; } //判断当前socket连接是否为"SS_UNCONNECTED"状态,只有在这个状态下,才是TCP Connect最初发起连接的那一次 int is_inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags) { int ret = 0; int err = 0; struct sock *sk = sock->sk; const unsigned int type = sock->type; //1. struct socket->type switch (type) { case SOCK_DGRAM: case SOCK_RAW: ret = 0; break; case SOCK_STREAM: case SOCK_SEQPACKET: ret = 1; //2. struct socket->state if (uaddr->sa_family == AF_UNSPEC) { goto out; } if (sk->sk_state == TCP_CLOSE) { goto sock_error; } switch (sock->state) { case SS_CONNECTED: ret = 0; break; case SS_CONNECTING: ret = 0; break; //我们真正需要捕获的TCP Conncet动作 case SS_UNCONNECTED: ret = 1; //3. struct sockaddr->sa_family switch (uaddr->sa_family) { case AF_INET6: case AF_INET: ret = 1; break; default: ret = 0; } break; default: ret = 0; } break; default: ret = 0; } out: return ret; sock_error: goto out; } void socket_connect_log(struct socket *sock, struct sockaddr *addr, int addr_len) { int ret = 0; const __be32 *daddr = NULL; __be16 dport = 0; __be16 sport = 0; const __be32 *saddr = NULL; pid_t pid = current->pid; do { ret = 0; //过滤冗余事件 ret = is_inet_stream_connect(sock, (struct sockaddr *)&addr, addr_len, sock->file->f_flags); if (ret != 1) { goto skip; } //获取待连接的对端的IP、PORT信息 switch (addr->sa_family) { case AF_INET6: if (addr_len < SIN6_LEN_RFC2133) { goto skip; } daddr = (__be32 *) ((struct sockaddr_in6 *) addr)->sin6_addr.s6_addr; dport = ((struct sockaddr_in6 *) addr)->sin6_port; saddr = &inet_sk(sock->sk)->SADDR; //sport = ntohs(inet_sk(sock->sk)->SPORT); sport = inet_sk(sock->sk)->SPORT; break; case AF_INET: if (addr_len < sizeof(struct sockaddr_in)) { goto skip; } daddr = (__be32 *) &((struct sockaddr_in *) addr)->sin_addr; dport = ((struct sockaddr_in *) addr)->sin_port; saddr = &inet_sk(sock->sk)->SADDR; //sport = ntohs(inet_sk(sock->sk)->SPORT); sport = inet_sk(sock->sk)->SPORT; break; default: daddr = NULL; dport = 0; saddr = NULL; sport = 0; goto skip; } //printk("process: %d, daddr: %s, dport: %d\n", pid, daddr, dport); printk("process: %d, saddr: %d:%d, daddr: %d:%d\n", pid, saddr, sport, daddr, dport); } while (false); skip: return; } static int __init hello_init(void) { int ret_val = -1; unsigned long int cr0 = 0; do { memset(&sec_ops, 0, sizeof(sec_ops)); //sprintf(sec_ops.name,"aegis_lsm"); //针对TCP CONNECT连接动作进行Hook sec_ops.socket_connect = socket_connect_log; if (!init_lookup_func()) { break; } //获取LSM注册函数register_security的内核地址 register_security_p = acquire_func_by_kprobe("register_security"); printk("register_security:%p\n", register_security_p); if (!register_security_p) { printk("get register_security error\n"); } //获取LSM注销函数unregister_security的内核地址 unregister_security_p = acquire_func_by_kprobe("unregister_security"); printk("unregister_security:%p\n", unregister_security_p); if (!unregister_security_p) { printk("get unregister_security error\n"); } //注册LSM Hook模块 if (register_security_p) { ret_val = register_security_p(&sec_ops); printk("register_security successfully: %d\n", ret_val); break; } memset(&org_sec_ops_back_up, 0, sizeof(org_sec_ops_back_up)); //获取内核中security_ops(LSM Table)的地址 org_sec_ops_p = look_up_symbol("security_ops"); if (!org_sec_ops_p) { break; } org_sec_ops_p = *(void**)org_sec_ops_p; printk("org_sec_ops:%p\n", org_sec_ops_p); //保存内核中的"security_ops" memcpy(&org_sec_ops_back_up, org_sec_ops_p, sizeof(org_sec_ops_back_up)); cr0 = wpoff_cr0(); org_sec_ops_p->socket_connect = socket_connect_log; set_cr0(cr0); ret_val = 0; } while(false); printk(KERN_INFO "Hello world!\n"); return ret_val; } static void __exit hello_cleanup(void) { unsigned long int cr0 = 0; if (unregister_security_p) { unregister_security_p(&sec_ops); } else if(org_sec_ops_p) { cr0 = wpoff_cr0(); org_sec_ops_p->socket_connect = org_sec_ops_back_up.socket_connect; set_cr0(cr0); SLEEP_MILLI_SEC(100); } printk(KERN_INFO "Cleaning up module.\n"); } module_init(hello_init); module_exit(hello_cleanup); MODULE_LICENSE("GPL");
5. Linux LSM stacking
我们知道,LSM Hook Function被保存在内核内存的一个全局数组(security_ops)中,在正常流程中,调用register_security()向系统注册一个LSM回调函数,如果当前已经开启了SELINUX,即当前挂载点已经注册了一个LSM回调函数,则后来的register_security()会失败,即默认情况下,每个LSM挂载点最多只能注册一个回调函数
LSM的一个Patch Project可以使得LSM支持"模块栈式函数挂载"
Linux kernel patches to support LSM (security module) stacking, plus a module which actively stacks LSMs.
LSM Stacking Patch使用了一个内核标准双链表将Stacking链式的LSM模块进行存储,采用FIFO的形式进行链式存储,这样,多个LSM模块可以堆叠式地注册到系统中,
Relevant Link:
http://lsm-stacker.sourceforge.net/ http://sourceforge.net/projects/lsm-stacker/files/ http://t75040.security-selinux.securetalk.info/current-future-plans-to-support-stacking-lsm-modules-t75040-20.html
Copyright (c) 2014 LittleHann All rights reserved