Linux的系统调用、网络连接状态、磁盘I/O;可疑行为监控/日志收集、SHELL命令执行流程

简介: http://man7.org/linux/man-pages/man7/capabilities.


http://man7.org/linux/man-pages/man7/capabilities.7.html

http://www.cnblogs.com/LittleHann/p/3850653.html


相关学习资料

复制代码
《The Linux Kernel API Book》
http://blog.csdn.net/orange_os/article/details/7485069
http://www.cnblogs.com/LittleHann/p/3850655.html
http://blog.csdn.net/yeyuangen/article/details/6858062
http://linux.chinaunix.net/techdoc/develop/2008/01/15/976651.shtml
http://www.cnblogs.com/LittleHann/p/3850653.html
http://www.cnblogs.com/LittleHann/p/3632646.html
www.cnblogs.com/LittleHann/p/3558685.html
复制代码

 

目录

复制代码
0. 引言
1. 用户编程接口(API)、系统命令、系统调用、和内核函数的关系
2. Linux的系统调用
3. Linux Shell命令执行
4. Linux系统调用日志收集
5. Linux网络连接状态日志收集
6. Linux磁盘I/O事件日志收集
7. 后记
复制代码

 

0. 引言

对系统调用、网络连接、磁盘文件操作、恶意rootkit的动态监控,是进行主机入侵恶意行为实时检测的一个很好的思考方向,作为纵深防御的一环,当黑客实际入侵了一台机器之后,我们怎样通过监控机制实时地发现入侵行为,显然,这需要在安全攻防的规律的基础上,结合操作系统的事件监控进行采集维度确定、事件数据采集、黑白名单数据源过滤、数据集中通道上传、基于规则/行为模式的入侵判别/分析。

一般来说,我们可以从服务器上采集的信息维度有:

复制代码
1. 系统调用
    1.1 Bash命令
    黑客在获取了linux服务器的ssh、webshell之后,必然会执行一些bash命令进行提权、建立账户等猥琐操作
    我们知道,对于Bash命令,大部分情况下是通过"do_execve系统调用(6个exec执行函数的总称)"执行环境变量PATH下的某个应用程序(有少部分的bash命令属于内建命令,不走系统调用流程)
    关于shell命令执行,我们之后会学习到
    1.2 网络连接
        1) 正向连接
        黑客很可能使用黑下来的主机充当DDOS、暴力破解的肉鸡,这些行为都会建立正向的网络连接,在这个过程中必然会调用到linux系统提供的特定系统调用。我们可以从中发现"以系统调用为数据源的行为轨迹"
        2) 反向连接
        反向连接一般被用在黑客建立"网络后门端口"时使用,方便2次入侵或者进行内网渗透。或者当被渗透的目标主机处在内网之中,而内网的边界路由器、防火墙又阻止外部客户端主动连接目标主机,这个时候,黑客就可以利用反
向连接(例如nc)主动从内网向外部客户端发起连接,从而bypass目标系统边界的防火墙(基于路由规则的防火墙,例如iptables)
        3) 隧道连接
        隧道连接和反向连接很像,区别在于隧道连接是一个加密后的反向连接

2. 磁盘操作
通过监控指定敏感目录的磁盘操作行为(例如www、tmp目录),可以防止从系统层面防止黑客对WEB漏洞的攻击和利用
    1) webshell入侵
    黑客可以通过cms的注入漏洞进行webshell的入侵
        1.1) 通过修改update现有文件的getshell
        1.2) 童鞋写入write新文件的getshell
    2) 读写敏感文件
    建立黑名单机制,对系统中关键的敏感路径进行监控,例如:
        2.1) /etc/passwd
        2.2) /etc/shadow
        2.3) /etc/httpd/..
    3) 可执行文件
        3.1) 通过在/tmp/目录上传exp、poc进行渗透提权
        3.2) 通过上传DDOS、暴力密码破解工具进行对外肉鸡攻击
对于攻击者来说,不管是上传exp、还是新建webshell后门、异或是读取机密文件,都会涉及到磁盘I/O操作,所以磁盘I/O操作的这块行为分析,也是我们的分析重点
关于磁盘I/O操作的可疑行为分析,请参阅另一篇文章
www.cnblogs.com/LittleHann/p/3558685.html

3. Rootkit
黑客在获取一台服务器的完全控制权之后,往往会在磁盘上留下rootkit,为了下次2次入侵、或者隐藏自己的入侵痕迹。我们可以通过:
    1) 基于磁盘文件事件方式监控
    通过对磁盘的inotify进行注册,每当有文件/文件夹创建的时候就自动进行检测
    2) 每天定时监控
复制代码

 

1. 用户编程接口(API)系统命令系统调用、和内核函数的关系

在研究恶意入侵行为检测的时候,我们可能会问自己这样的一个问题,黑客上传的exp使用的可能是函数库中的API、而在Bash中执行的指令又由shell负责解析执行,难道我们要分别对它们进行Hook监控吗?

答案是否定的,为了理解这个原因,我们需要先理清用户编程接口(API)、系统命令、系统调用、和内核函数的关系

复制代码
1. 内核函数
Linux系统内核由许多的代码模块组成,每个模块都会提供一些相应的的调用接口,我们在进行内核编程的时候,可以把这些接口看成是linux提供的SDK来使用。
Linux系统中存在许多内核函数,有些是内核文件中自己使用的,有些则是可以export出来供内核其他部分共同使用的。内核公开的内核函数,export出来的可以使用命令ksyms 或 cat /proc/ksyms来查看 

2. 系统调用
操作系统(内核态)提供给用户程序(用户态)调用的一组"特殊"接口(属于一种内部中断)。用户程序(ring3)可以通过这组"特殊"接口来获得操作系统内核(ring0)提供的服务
系统调用是一层用户进入内核的接口,它本身并非内核函数,进入内核后,不同的系统调用会找到对应到各自的内核函数(系统调用服务例程)。实际上针对请求提供服务的是内核函数而非调用接口
比如系统调用 getpid实际上就是调用内核函数sys_getpid
asmlinkage long sys_getpid(void)
{
    return current->tpid;
}
关于系统调用和中断的相关知识,请参阅另一篇文章:
http://www.cnblogs.com/LittleHann/p/3850655.html

3. 用户编程接口(API)
用户编程接口其实是一个函数定义,就是我们常说的C库,说明了如何获得一个给定的服务,比如read( )、malloc( )、free( )、abs( )等。它有可能和系统调用形式上一致,比如read()接口就和read系统调用对应,但这种对应并
非一一对应,往往会出现:
    1) 几种不同的API内部用到同一个系统调用,比如malloc( )、free( )内部利用brk( )系统调用来扩大或缩小进程的堆
    2) 一个API利用了好几个系统调用组合完成服务
    3) 有些API甚至不需要任何系统调用——因为它并不是必需要使用内核服务,如计算整数绝对值的    abs()接口
值得注意的是Linux的用户编程接口遵循了在Unix世界中最流行的应用编程界面标准——POSIX标准,这套标准定义了一系列API。在Linux中(Unix也如此),这些API主要是通过C库(libc)实现的,它除了定义的一些标准的C函数外,一个
很重要的任务就是提供了一套封装例程(wrapper routine)将系统调用在用户空间包装后供用户编程使用

4. 系统命令
系统命令即Bash Shell命令,系统管理员通过bash输入命令,然后由bash通过API调用系统调用,最后通过内核函数完成相应功能。
例如pwd、uname等系统命令
复制代码

总体来说,它们的先后顺序是这样的:

BASH系统命令    ->API接口    ->系统调用    ->内核函数
            ->系统调用    ->内核函数
//程序员可以选择使用C库进行编程,或者直接调用系统调用进行编程

们看个例子来对比一下通过C库调用和直接调用的区别

复制代码
#include <syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>

int main(void) 
{
    long ID1, ID2;
    /*-----------------------------*/
    /* 直接系统调用*/
    /* SYS_getpid (func no. is 20) */
    /*-----------------------------*/
    ID1 = syscall(SYS_getpid);
    printf ("syscall(SYS_getpid)=%ld\n", ID1);
    
    /*-----------------------------*/
    /* 使用"libc"封装的系统调用 */
    /* SYS_getpid (Func No. is 20) */
    /*-----------------------------*/
    ID2 = getpid();
    printf ("getpid()=%ld\n", ID2);
    
    return(0);
}
复制代码

总结来说,不管是执行程序(exec)、还是bash命令执行,最终都需要通过系统调用来实现,所以我们只需要对do_execve()进行Hook监控就可以实现对命令、程序执行这块的监控

 

2. Linux的系统调用

0x1: 系统调用基本概念

从逻辑上来说,系统调用可被看成是一个内核(ring0)与用户空间(ring3)程序交互的接口,它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。
系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行"保护",因为我们知道Linux的运行空间分为内核空间(ring0)和用户空间(ring3)

1) 内核空间(ring0)
2) 用户空间(ring3)

它们各自运行在不同的级别中,逻辑上相互隔离,所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间函数

但是很多情况下,用户进程需要获得系统服务(调用系统内核函数),这时就必须利用系统提供给用户的"特殊接口",即系统调用。
它的特殊性主要在于规定了用户进程进入内核的具体位置,换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核(即陷入点trap),而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全

0x2: 系统调用实现

系统调用是一个内部陷入(trap)中断,系统调用的中断号是0x80,int 0x80指令的目的是产生一个编号为0x80的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项,也就是对应的系统门描述符。
门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:system_call(),很显然,所有的系统调用都会统一地转到这个地址,但Linux一共有2、3百个系统调用都从这里进入内核后又该如何派发到它们到各自的服务程序去呢?
要理解这个问题,我们就必须明白我们在使用系统调用时都要遵循哪些步骤(一般是C库会帮我们处理好系统调用的细节,程序员面对的只是相对简单的C库API)

复制代码
1. 程序员在编程中的使用的API实现了对系统调用的封装和包装,例如write()就是对sys_write()的包装,每个包装了系统调用的API都对应有一个"系统调用号"
2. 从应用层ring3进入系统调用需要借助linux系统提供的中断机制
3. Linux中实现系统调用利用了0x86体系结构中的软件中断。int 0x80指令的目的是产生一个编号为128的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项,也就是对应的系统门描述符
4. 门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序: system_call()(注意,这是"所有"系统调用的总入口,别和系统调用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)
5. 首先Linux为每个系统调用都进行了编号(0~NR_syscall),同时在内核中保存了一张系统调用表,该表中保存了系统调用编号和其对应的服务例程
6. 在系统调入通过系统门陷入内核前,需要把系统调用号一并传入内核,在x86上,这个传递动作是通过在执行int 0x80前把调用号装入eax寄存器实现的
7. 这样系统调用处理程序一旦运行,就可以从eax中得到数据,然后再去系统调用表中寻找相应服务例程了。
8. 除了需要传递系统调用号以外,许多系统调用还需要传递一些参数到内核,比如
sys_write(unsigned int fd, const char * buf, size_t count)
调用就需要传递文件描述符fd、要写入的内容buf、以及写入字节数count等几个内容到内核。
碰到这种情况,Linux会有6个寄存器可被用来传递这些参数:eax(存放系统调用号)、 ebx、ecx、edx、esi及edi来存放这些额外的参数(以字母递增的顺序)。具体做法是在system_call( )中使用SAVE_ALL宏把这些寄存器的值保
存在内核态堆栈中
9. 有始便有终,当服务例程结束时,system_call()从eax获得系统调用的返回值,并把这个返回值存放在曾保存用户态eax寄存器栈单元的那个位置上。然后跳转到ret_from_sys_call(),终止系统调用处理程序的执行
10. 当进程恢复它在用户态的执行前,RESTORE_ALL宏会恢复用户进入内核前被保留到堆栈中的寄存器值。其中eax返回时会带回系统调用的返回码(负数说明调用错误,0或正数说明正常完成)
复制代码

完成了以上的各个步骤后,我们在写C代码时编写的一个个C库API调用就通过中断机制被系统正确地解析成一个个对应的系统调用,调用相应的系统子程序,完成我们要求的功能。

0x3: 系统调用分类

系统调用主要分为以下几类:

复制代码
1. 控制硬件
系统调用往往作为硬件资源和用户空间的抽象接口 

2. 设置系统状态或读取内核数据
系统调用是用户空间和内核的唯一通讯手段,所以用户设置系统状态,比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过系统调用。比如getpgid、getpriority、setpriority、sethostname
    2.1 系统控制  
        1) ioctl: I/O总控制函数
        2) _sysctl: 读/写系统参数
        3) acct    启用或禁止进程记账
        4) getrlimit: 获取系统资源上限
        5) setrlimit: 设置系统资源上限
        6) getrusage: 获取系统资源使用情况
        7) uselib: 选择要使用的二进制函数库
        8) ioperm: 设置端口I/O权限
        9) iopl    改变进程I/O权限级别
        10) outb: 低级端口操作
        11) reboot: 重新启动
        12) swapon: 打开交换文件和设备
        13) swapoff: 关闭交换文件和设备
        14) bdflush: 控制bdflush守护进程
        15) sysfs: 取核心支持的文件系统类型
        16) sysinfo: 取得系统信息
        17) adjtimex: 调整系统时钟
        18) alarm: 设置进程的闹钟
        19) getitimer: 获取计时器值
        20) setitimer: 设置计时器值
        21) gettimeofday: 取时间和时区
        22) settimeofday: 设置时间和时区
        23) stime: 设置系统日期和时间
        24) time: 取得系统时间
        25) times: 取进程运行时间
        26) uname: 获取当前UNIX系统的名称、版本和主机等信息
        27) vhangup: 挂起当前终端
        28) nfsservctl: 对NFS守护进程进行控制
        29) vm86: 进入模拟8086模式
        30) create_module: 创建可装载的模块项
        31) delete_module: 删除可装载的模块项
        32) init_module: 初始化模块
        33) query_module: 查询模块信息
        34) *get_kernel_syms: 取得核心符号,已被query_module代替
    2.2 用户管理
        1) getuid: 获取用户标识号
        2) setuid: 设置用户标志号
        3) getgid: 获取组标识号
        4) setgid: 设置组标志号
        5) getegid: 获取有效组标识号
        6) setegid: 设置有效组标识号
        7) geteuid: 获取有效用户标识号
        8) seteuid: 设置有效用户标识号
        9) setregid: 分别设置真实和有效的的组标识号
        10) setreuid: 分别设置真实和有效的用户标识号
        11) getresgid: 分别获取真实的,有效的和保存过的组标识号
        12) setresgid: 分别设置真实的,有效的和保存过的组标识号
        13) getresuid: 分别获取真实的,有效的和保存过的用户标识号
        14) setresuid: 分别设置真实的,有效的和保存过的用户标识号
        15) setfsgid: 设置文件系统检查时使用的组标识号
        16) setfsuid: 设置文件系统检查时使用的用户标识号
        17) getgroups: 获取后补组标志清单
        18) setgroups: 设置后补组标志清单

3. 进程管理
系统调用接口是用来保证系统中进程能以多任务在虚拟内存环境下得以运行。比如 fork、clone、execve、exit等
    1) fork: 创建一个新进程
    2) clone: 按指定条件创建子进程
    3) execve: 运行可执行文件(重要)
    4) exit: 中止进程
    5) _exit: 立即中止当前进程
    6) getdtablesize: 进程所能打开的最大文件数
    7) getpgid: 获取指定进程组标识号
    8) setpgid: 设置指定进程组标志号
    9) getpgrp: 获取当前进程组标识号
    10) setpgrp: 设置当前进程组标志号
    11) getpid: 获取进程标识号
    12) getppid: 获取父进程标识号
    13) getpriority: 获取调度优先级
    14) setpriority: 设置调度优先级
    15) modify_ldt: 读写进程的本地描述表
    16) nanosleep: 使进程睡眠指定的时间
    17) nice: 改变分时进程的优先级
    18) pause: 挂起进程,等待信号
    19) personality: 设置进程运行域
    20) prctl: 对进程进行特定操作
    21) ptrace: 进程跟踪
    22) sched_get_priority_max: 取得静态优先级的上限
    23) sched_get_priority_min: 取得静态优先级的下限
    24) sched_getparam: 取得进程的调度参数
    25) sched_getscheduler: 取得指定进程的调度策略
    26) sched_rr_get_interval: 取得按RR算法调度的实时进程的时间片长度
    27) sched_setparam: 设置进程的调度参数
    28) sched_setscheduler: 设置指定进程的调度策略和参数
    29) sched_yield: 进程主动让出处理器,并将自己等候调度队列队尾
    30) vfork: 创建一个子进程,以供执行新程序,常与execve等同时使用
    31) wait: 等待子进程终止
    32) wait3: 参见wait
    33) waitpid: 等待指定子进程终止
    34) wait4: 参见waitpid
    35) capget: 获取进程权限
    36) capset: 设置进程权限
    37) getsid: 获取会晤标识号
    38) setsid: 设置会晤标识号

4. 文件/文件系统
    4.1 文件操作
        1) fcntl: 文件控制
        2) open: 打开文件
        3) creat: 创建新文件
        4) close: 关闭文件描述字
        5) read: 读文件
        6) write: 写文件
        7) readv: 从文件读入数据到缓冲数组中
        8) writev: 将缓冲数组里的数据写入文件
        9) pread: 对文件随机读
        10) pwrite: 对文件随机写
        11) lseek: 移动文件指针
        12) _llseek: 在64位地址空间里移动文件指针
        13) dup: 复制已打开的文件描述字
        14) dup2: 按指定条件复制文件描述字
        15) flock: 文件加/解锁
        16) poll: I/O多路转换
        17) truncate: 截断文件
        18) ftruncate: 参见truncate
        19) umask: 设置文件权限掩码
        20) fsync: 把文件在内存中的部分写回磁盘
    4.2 文件系统操作
        1) access: 确定文件的可存取性
        2) chdir: 改变当前工作目录
        3) fchdir: 参见chdir
        4) chmod: 改变文件方式
        5) fchmod: 参见chmod
        6) chown: 改变文件的属主或用户组
        7) fchown: 参见chown
        8) lchown: 参见chown
        9) chroot: 改变根目录
        10) stat: 取文件状态信息
        11) lstat: 参见stat
        12) fstat: 参见stat
        13) statfs: 取文件系统信息
        14) fstatfs: 参见statfs
        15) readdir: 读取目录项
        16) getdents: 读取目录项
        17) mkdir: 创建目录
        18) mknod: 创建索引节点
        19) rmdir: 删除目录
        20) rename: 文件改名
        21) link: 创建链接
        22) symlink: 创建符号链接
        23) unlink: 删除链接
        24) readlink: 读符号链接的值
        25) mount: 安装文件系统
        26) umount: 卸下文件系统
        27) ustat: 取文件系统信息
        28) utime: 改变文件的访问修改时间
        29) utimes: 参见utime
        30) quotactl: 控制磁盘配额
            
5. 内存操作
    1) brk: 改变数据段空间的分配
    2) sbrk: 参见brk
    3) mlock: 内存页面加锁
    4) munlock: 内存页面解锁
    5) mlockall: 调用进程所有内存页面加锁
    6) munlockall: 调用进程所有内存页面解锁
    7) mmap: 映射虚拟内存页
    8) munmap: 去除内存页映射
    9) mremap: 重新映射虚拟内存地址
    10) msync: 将映射内存中的数据写回磁盘
    11) mprotect: 设置内存映像保护
    12) getpagesize: 获取页面大小
    13) sync: 将内存缓冲区数据写回硬盘
    14) cacheflush: 将指定缓冲区中的内容写回磁盘

6. 网络管理
    1) getdomainname: 取域名
    2) setdomainname: 设置域名
    3) gethostid: 获取主机标识号
    4) sethostid: 设置主机标识号
    5) gethostname: 获取本主机名称
    6) sethostname: 设置主机名称
    7) socketcall: socket系统调用
    8) socket: 建立socket
    9) bind: 绑定socket到端口
    10) connect: 连接远程主机
    11) accept: 响应socket连接请求
    12) send: 通过socket发送信息
    13) sendto: 发送UDP信息
    14) sendmsg: 参见send
    15) recv: 通过socket接收信息
    16) recvfrom: 接收UDP信息
    17) recvmsg: 参见recv
    18) listen: 监听socket端口
    19) select: 对多路同步I/O进行轮询
    20) shutdown: 关闭socket上的连接
    21) getsockname: 取得本地socket名字
    22) getpeername: 获取通信对方的socket名字
    23) getsockopt: 取端口设置
    24) setsockopt: 设置端口参数
    25) sendfile: 在文件或端口间传输数据
    26) socketpair: 创建一对已联接的无名socket

7. 进程间通信
1) ipc: 进程间通信总控制调用
    7.1 信号
        1) sigaction: 设置对指定信号的处理方法
        2) sigprocmask: 根据参数对信号集中的信号执行阻塞/解除阻塞等操作
        3) sigpending: 为指定的被阻塞信号设置队列
        4) sigsuspend: 挂起进程等待特定信号
        5) signal: 参见signal
        6) kill: 向进程或进程组发信号
        7) *sigblock: 向被阻塞信号掩码中添加信号,已被sigprocmask代替
        8) *siggetmask: 取得现有阻塞信号掩码,已被sigprocmask代替
        9) *sigsetmask: 用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替
        10) *sigmask: 将给定的信号转化为掩码,已被sigprocmask代替
        11) *sigpause: 作用同sigsuspend,已被sigsuspend代替
        12) sigvec: 为兼容BSD而设的信号处理函数,作用类似sigaction
        13) ssetmask: ANSI C的信号处理函数,作用类似sigaction
    7.2 消息
        1) msgctl: 消息控制操作
        2) msgget: 获取消息队列
        3) msgsnd: 发消息
        4) msgrcv: 取消息
    7.3 管道
        1) pipe: 创建管道
    7.4 信号量
        1) semctl: 信号量控制
        2) semget: 获取一组信号量
        3) semop: 信号量操作
    7.5 共享内存
        1) shmctl: 控制共享内存
        2) shmget: 获取共享内存
        3) shmat: 连接共享内存
        4) shmdt: 拆卸共享内存系统调用是中断中的一种,属于陷入内中断(或者叫软中断)
复制代码

关于中断和异常的相关知识,请参阅另一篇文章:

http://www.cnblogs.com/LittleHann/p/3850655.html

 

3. Linux Shell命令执行

0x1: Shell简介

Linux系统的shell作为操作系统的外壳,为用户提供使用操作系统的接口。它是命令语言、命令解释程序及程序设计语言的统称。 

复制代码
1. 命令语言
从编程的角度来看,shell是一种命令导向的可编程语言

2. 命令解释语言
shell是一个命令语言解释器,它拥有自己内建的shell命令集,shell也能被系统中其他应用程序所调用。用户在提示符下输入的命令都由shell先解释然后传给Linux核心。  
shell命令的可分为:
    1) 内部命令
    内部命令是shell内建的命令,比如改变工作目录命令cd 
    2) PATH系统变量路径下的某个应用程序(大多数情况下都是这种情况)
    对于这类命令,shell只是起一个传达和启动的作用,shell负责寻找并以子进程的形式启动这些程序

3. 程序设计语言
shell程序设计语言支持绝大多数在高级语言中能见到的程序元素,如函数、变量、数组和程序控制结构。shell编程语言简单易学,任何在提示符中能键入的命令都能放到一个可执行的shell程序中。 
复制代码

0x2: Shell执行程序步骤

它的执行过程基本上按如下步骤:

复制代码
1. 读取用户由键盘输入的命令行
2. 分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve()内部处理所要求的形式。 
3. 终端进程调用fork()建立一个子进程,这只是创建一个进程的空壳,包括一些必要的内核对象句柄,并没有实际运行进程
4. 终端进程本身用系统调用wait4()来等待子进程完成(如果是后台命令,则不等待)。
当子进程运行时调用execve(),子进程根据文件名(即命令名)到目录中查找有关文件(这是命令解释程序构成的文件),将它调入内存,执行这个程序(解释这条命令) 
5. 如果命令末尾有&号(后台命令符号),则终端进程不用系统调用wait4()等待,立即发提示符,让用户输入下一个命令,转⑴。
如果命令末尾没有&号,则终端进程要一直等待。
当子进程(即运行命令的进程)完成处理后终止,向父进程(终端进程)报告,此时终端进程醒来,在做必要的判别等工作后,终端进程发提示符,让用户输入新的命令,重复上述处理过程。
复制代码

用户在登录到Linux时由/etc/passwd文件来决定要使用哪个shell 

fgrep zhenghan /etc/passwd
zhenghan:x:500:500:Red Hat Enterprise Linux 6 32位:/home/zhenghan:/bin/bash
//表明"zhenghan"这个用户的默认shell是/bin/bash

0x3: Linux Shell分类

Linux中的shell有多种类型

复制代码
1. Bourne shell(sh)
Bourne shell是UNIX最初使用的shell,并且在每种UNIX上都可以使用。Bourne shell在shell编程方面相当优秀,但在处理与用户的交互方面做得不如其他几种shell

2. Bourne Again shell(Bash)(默认)
Linux操作系统缺省的shell是Bourne Again shell,它是Bourne shell的扩展,简称Bash,与Bourne shell完全向后兼容,并且在Bourne shell的基础上增加、增强了很多特性,可以提供如:
    1) 命令补全
    2) 命令编辑
    3) 命令历史表
    4) 包含了很多C shell和Korn shell中的优点
    5) 有灵活和强大的编程接口
    6) 同时又有很友好的用户界面 

3. C shell(csh)
C shell是一种比Bourne shell更适于编程的shell,它的语法与C语言很相似。 Linux为喜欢使用C shell的人提供了Tcsh。
Tcsh是C shell的一个扩展版本。Tcsh包括:
    1) 命令行编辑
    2) 可编程单词补全
    3) 拼写校正
    4) 历史命令替换
    5) 作业控制和类似C语言的语法
    6) 它不仅和Bash shell是提示符兼容,而且还提供比Bash shell更多的提示符参数

4. Korn shell(ksh)
Korn shell集合了C shell和Bourne shell的优点并且和Bourne shell完全兼容。
Linux系统提供了pdksh(ksh的扩展),它支持任务控制,可以在命令行上挂起、后台执行、唤醒或终止程序 
复制代码

 

4. Linux系统调用日志收集

监控系统调用就是对"do_execve系统调用"进行注册监控回调,在本例中,我们监控的事件维度有:

复制代码
1. path: 文件路径
2. uid: 执行当前系统调用的用户的UID
3. tid: 当前系统调用的TID(任务)
4. pid: 调用当前系统调用的进程的PID
5. tty: 当前用户所连接的tty
6. filename: 指令名
复制代码

do_execvelog.c

复制代码
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/slab.h>
#include <linux/tty.h>


static int execlog_do_execve(char *filename, char __user *__user *__argv, char __user *__user *__envp, struct pt_regs *not_used)
{
    char tty[64];
    char __user *__user *__temp_argv;
    char __user *__argv_part;
    size_t argv_size = 2;
    char *argv, *write_argv;
    long written_argv;

    __temp_argv = __argv;
    while (get_user(__argv_part, __temp_argv) == 0 && __argv_part != NULL) 
    {
        argv_size += strlen_user(__argv_part) + 1;
        ++__temp_argv;
    }

    argv = kmalloc(argv_size, GFP_ATOMIC);
    if (unlikely(argv == NULL))
    {
        goto out;
    } 

    __temp_argv = __argv;
    write_argv = argv;
    while (get_user(__argv_part, __temp_argv) == 0 && __argv_part != NULL) 
    {
        written_argv = strncpy_from_user(write_argv, __argv_part, argv_size);
        if (unlikely(written_argv < 0)) 
        {
            goto free;
        }
        write_argv += written_argv;
        *(write_argv++) = ' ';
        argv_size -= (written_argv + 1);
        ++__temp_argv;
    }
    *write_argv = '\0';

    printk(KERN_DEBUG KBUILD_MODNAME ": [path:%s uid:%d tid:%d pid:%d tty:%s filename:%s]: %s\n",
   
    current->comm, current_uid(), current->pid, task_session_vnr(current), tty_name(current->signal->tty, tty), filename, argv);
free:
    kfree(argv);
out:
    jprobe_return();
    return 0;
}

static int signal_that_will_cause_exit(int trap_number)
{
        switch(trap_number)
        {
            case SIGABRT:
            case SIGSEGV:
            case SIGQUIT:
            //TODO Other signals that we need to handle?
                return 1;
                break;
            default:
                return 0;  
                break;
        }
}

static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trap_number)
{
    if(signal_that_will_cause_exit(trap_number))
    {
        printk(KERN_ERR KBUILD_MODNAME ": fault handler: Detected fault %d from inside probes.", trap_number);
    }

    return 0;
}


static struct jprobe execve_jprobe =
{
    .entry = (kprobe_opcode_t *) execlog_do_execve,
    .kp =
    {
        .symbol_name = "do_execve",
        .fault_handler = handler_fault,
    },
};


static int __init plant_probes(void)
{
    int err;

    err = register_jprobe(&execve_jprobe); 
    if(err < 0)
    {
        printk(KERN_ERR KBUILD_MODNAME ":\t[-] Failed to plant execve pre handler\n");
        return -1;
    }

    printk(KERN_INFO KBUILD_MODNAME ":\t[+] Planted execve pre handler\n");  
    printk(KERN_INFO KBUILD_MODNAME ":\t[+] Deployed\n");

    return 0;
}


static void __exit unplant_probes(void)
{
    unregister_jprobe(&execve_jprobe);
    printk(KERN_INFO KBUILD_MODNAME ":\t[+] Unplanted execve pre handler probe\n");
}


MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alibaba");
MODULE_DESCRIPTION("execve monitor module");

//初始化内核模块
module_init(plant_probes);
module_exit(unplant_probes);
复制代码

Makefile

复制代码
#
# Variables needed to build the kernel module
#
name      = do_execvelog

obj-m += $(name).o

all: build

.PHONY: build install clean

build:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules CONFIG_DEBUG_SECTION_MISMATCH=y

install: build
    -mkdir -p /lib/modules/`uname -r`/kernel/arch/x86/kernel/
    cp $(name).ko /lib/modules/`uname -r`/kernel/arch/x86/kernel/
    depmod /lib/modules/`uname -r`/kernel/arch/x86/kernel/$(name).ko

clean:
    [ -d /lib/modules/$(shell uname -r)/build ] && \
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
复制代码

编译并加载内核模块

make
insmod do_execvelog.ko

测试效运行果

编写的内核模块成功地对所有的"do_execve系统调用(程序、指令执行)"进行了监控

 

5. Linux网络连接状态日志收集

对linux网络连接状态的监控就是对"inet_stream_connect系统调用"进行注册监控回调,在本例中,我们监控的事件维度有:

1. uid: 执行当前系统调用的用户的UID
2. tty: 当前用户所连接的tty
3. path: 文件路径
4. src_ip: 发起网络连接的源IP
5. dst_ip: 发起网络连接的目的IP

netlog.c

复制代码
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/slab.h>
#include <linux/tty.h>
#include <linux/file.h>
#include <linux/in.h>
#include <linux/init.h>
#include <linux/ipv6.h>
#include <linux/kallsyms.h>
#include <linux/net.h>
#include <linux/socket.h>
#include <linux/syscalls.h>
#include <linux/unistd.h>
#include <net/ip.h>
#include <linux/version.h>
#define MAX_EXEC_PATH 950
#define CONNECT_PROBE_FAILED -1
#define ACCEPT_PROBE_FAILED -2
#define CLOSE_PROBE_FAILED -3
#define BIND_PROBE_FAILED -4 
#define PROBE_CONNECTION_CLOSE 1
#define PROBE_UDP 0
#define MAX_ACTIVE 100
#define DEFAULT_PROBES 0xFFFFFFFF
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 7, 0)
#ifndef __must_hold
#define __must_hold(x)
#endif
#ifndef __acquires
#define __acquires(x)
#endif
#ifndef __releases
#define __releases(x)
#endif
#endif  
#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
#ifdef CONFIG_X86
#ifdef CONFIG_X86_64
/* Calling conventions: RDI, RSI, RDX */
#define GET_ARG_1(regs) regs->di
#define GET_ARG_2(regs) regs->si
#define GET_ARG_3(regs) regs->dx
#else /* !CONFIG_X86_64 */
/* Calling conventions: AX, DX, BX */
#define GET_ARG_1(regs) regs->ax
#define GET_ARG_2(regs) regs->dx
#define GET_ARG_3(regs) regs->bx
#endif /* CONFIG_X86_64 ? */
#else
#error Unsupported architecture
#endif
#define MOD_AUTHORS "Alibaba" 
#define MOD_DESC "netlog logs module"
#define MOD_LICENSE "GPL"
static u8 initialized;
static u32 loaded_probes;
static DEFINE_SPINLOCK(probe_lock);

enum secure_log_protocol 
{
    PROTO_TCP = 0,
    PROTO_UDP,
};
enum secure_log_action 
{
    ACTION_CONNECT,
    ACTION_ACCEPT,
    ACTION_CLOSE,
    ACTION_BIND,
};

static char *path_from_mm(struct mm_struct *mm, char *buffer, int length)
{
    char *p = NULL;
    if (unlikely(mm == NULL))
    {
        return NULL;
    } 
    down_read(&mm->mmap_sem);
    if (unlikely(mm->exe_file == NULL)) 
    {
        p = NULL;
    } 
    else 
    {
        p = d_path(&mm->exe_file->f_path, buffer, length);
        if (IS_ERR(p))
        {
            p = NULL;
        } 
    } 
    up_read(&mm->mmap_sem);
    return p;
}

static void logdata(struct socket *sock, u8 protocol, u8 action)
{
    char buffer[MAX_EXEC_PATH + 1], *path;
    unsigned short family;
    const void *dst_ip;
    const void *src_ip;
    int dst_port;
    int src_port;

    path = path_from_mm(current->mm, buffer, MAX_EXEC_PATH);
    buffer[MAX_EXEC_PATH] = '\0';
    if (unlikely(path == NULL))
    {
        return;
    } 

    family = sock->sk->sk_family;
    dst_port = ntohs(inet_sk(sock->sk)->DPORT);
    src_port = ntohs(inet_sk(sock->sk)->SPORT);
    switch (family) {
    case AF_INET:
        dst_ip = &inet_sk(sock->sk)->DADDR;
        src_ip = &inet_sk(sock->sk)->SADDR;
        break;
    case AF_INET6:
#if LINUX_VERSION_CODE > KERNEL_VERSION(3, 13, 0)
        dst_ip = &sock->sk->sk_v6_daddr;
#else /* LINUX_VERSION_CODE < KERNEL_VERSION(3, 13, 0) */
        dst_ip = &inet6_sk(sock->sk)->daddr;
#endif /* LINUX_VERSION_CODE ? KERNEL_VERSION(3, 13, 0) */
        src_ip = &inet6_sk(sock->sk)->saddr;
        break;
    default:
        dst_ip = NULL;
        src_ip = NULL;
        break;
    }
    char tty[64];
    char sip[16],dip[16];
    sprintf(sip,NIPQUAD_FMT,NIPQUAD(inet_sk(sock->sk)->SADDR));
    sprintf(dip,NIPQUAD_FMT,NIPQUAD(inet_sk(sock->sk)->DADDR));                                          
    printk("uid:%d - tty:%s - path:%s - src_ip:%s:%d - dst_ip:%s:%d\n", current_uid(),tty_name(current->signal->tty, tty),path,sip,src_port,dip,dst_port);
          
}

int handler_fault(struct kprobe *p, struct pt_regs *regs, int trap_number)
{
    switch (trap_number) 
    {
        case SIGABRT:
        case SIGSEGV:
        case SIGQUIT:
            pr_err(" fault handler: Detected fault %d from inside probes.", trap_number);
            return 0;
        default:
            return 0;
    }
}
struct probe_data 
{
    struct socket *sock;
};
void unplant_kretprobe(struct kretprobe *probe) __must_hold(probe_lock)
{
    pr_info("[+] Unplanting kretprobe on %s\n", probe->kp.symbol_name);
    unregister_kretprobe(probe);
    pr_info("[+] Unplanted kretprobe on %s\n", probe->kp.symbol_name);
    probe->kp.addr = NULL;
}

static int pre_handler_store_sock(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    struct probe_data *priv = (struct probe_data*)ri->data;

    if (likely(current != NULL)) 
    {
        priv->sock = (struct socket*)GET_ARG_1(regs);
        return 0;
    }
    return 1;
}

static int enter_inet_stream_connect(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    struct probe_data *priv = (struct probe_data*)ri->data;
    struct socket *sock = priv->sock;

    if (likely(current != NULL) &&
        likely(sock != NULL) &&
        likely(sock->sk != NULL) &&
        likely(sock->sk->sk_family == AF_INET ||
           sock->sk->sk_family == AF_INET6) &&
        likely(sock->sk->sk_protocol == IPPROTO_TCP))
    {
        logdata(sock, PROTO_TCP, ACTION_CONNECT);
    } 
    return 0;
}  

static struct kretprobe tcpconn_kretprobe =
{
    .entry_handler = pre_handler_store_sock,
    .handler = enter_inet_stream_connect,
    .data_size = sizeof(struct socket*),
    .maxactive = 16 * NR_CPUS,
    .kp = {
        .symbol_name = "inet_stream_connect",
        .fault_handler = handler_fault,
    },
};
static void unplant_probes(u32 removed_probes) __must_hold(probe_lock)
{
    unplant_kretprobe(&tcpconn_kretprobe);
}

void unplant_all(void)
{
    unsigned long flags; 
    spin_lock_irqsave(&probe_lock, flags); 
    unplant_probes(loaded_probes); 
    spin_unlock_irqrestore(&probe_lock, flags);
}

int plant_kretprobe(struct kretprobe *probe) __must_hold(probe_lock)
{
        int err;
        pr_info("[+] Planting kretprobe on %s\n", probe->kp.symbol_name);
        err = register_kretprobe(probe);
        if (err < 0)
        {
            pr_err("[-] Failed to planted kretprobe on %s: %i\n", probe->kp.symbol_name, err);
        }    
        else
        {
            pr_info("[+] Planted kretprobe on %s\n", probe->kp.symbol_name);
        } 
        return err;
}

static int plant_probes(u32 new_probes) __must_hold(&probe_lock)
{
    int err = 0;
    err = plant_kretprobe(&tcpconn_kretprobe);
    if (err < 0)
    {
        return -CONNECT_PROBE_FAILED;
    } 
    return err;
}

int probes_init(void)
{
    unsigned long flags;
    int ret = 0;
    spin_lock_irqsave(&probe_lock, flags);
    initialized=2;
    if (initialized != 0) 
    {
        pr_info("Start plant_probes\n");
        ret = plant_probes(DEFAULT_PROBES);
        if (ret >= 0)
        {
            initialized = 1;  
        } 
    }
    spin_unlock_irqrestore(&probe_lock, flags);
    return ret;
}

static int __init netlog_init(void)
{
    int ret;
    pr_info("Net Monitor Start....\n");
    ret = probes_init();
    if (ret != 0) 
    {
        unplant_all();
    }
    else
    {
        pr_info("Net Monitor Start Ok!ret is %d\n",ret);
    }
    return ret;
}


static void __exit netlog_exit(void)
{
    unplant_all();
}
module_init(netlog_init);
module_exit(netlog_exit);

MODULE_LICENSE(MOD_LICENSE);
MODULE_AUTHOR(MOD_AUTHORS);
MODULE_DESCRIPTION(MOD_DESC);
复制代码

Makefile

复制代码
#
# Variables needed to build the kernel module
#
name      = netlog

obj-m += $(name).o

all: build

.PHONY: build install clean

build:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules CONFIG_DEBUG_SECTION_MISMATCH=y

install: build
    -mkdir -p /lib/modules/`uname -r`/kernel/arch/x86/kernel/
    cp $(name).ko /lib/modules/`uname -r`/kernel/arch/x86/kernel/
    depmod /lib/modules/`uname -r`/kernel/arch/x86/kernel/$(name).ko

clean:
    [ -d /lib/modules/$(shell uname -r)/build ] && \
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
复制代码

编译并加载内核模块

make
insmod netlog.ko

测试运行效果

 

 

6. Linux磁盘I/O事件日志收集

对linux磁盘I/O状态的监控就是对"sys_open系统调用"进行注册监控回调,在本例中,我们监控的事件维度有:

1. 操作当前文件的进程,即什么程序打开了当前文件
2. 打开文件的绝对路径
3. 打开文件的标志
4. 打开文件的模式

openlog.c

复制代码
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/slab.h>
#include <linux/kallsyms.h>
#include <linux/fs.h>
#include <asm/uaccess.h>


static struct jprobe jp;

asmlinkage long jprobe_sys_open(const char __user *filename, int flags, int mode)
{
    int len = PATH_MAX;
    char * tmpfilename = NULL;

    if (TASK_SIZE - (unsigned long) filename < PATH_MAX) 
    {
        len = TASK_SIZE - (unsigned long) filename;
    }

    tmpfilename = kmalloc(len, GFP_ATOMIC);
    if (tmpfilename == NULL) 
    {
        return 0;
    } 

    /*
    copy_from_user可能导致睡眠,而kprobe并不允许在探测点处理函数中这么做
    if (copy_from_user(tmpfilename, filename, len)) 
    {
        return 0;
    }
    printk("process '%s' call open('%s', %d, %d)\n", current->comm, tmpfilename, flags, mode);
    */

    printk("process '%s' call open('%s', %d, %d)\n", current->comm, filename, flags, mode);
    jprobe_return();
    return 0;
}

int init_module(void)
{
    int ret;

    jp.entry = (kprobe_opcode_t *) jprobe_sys_open;
    jp.kp.addr = (kprobe_opcode_t *)kallsyms_lookup_name("sys_open");
    if (!jp.kp.addr) 
    {
        printk("Couldn't find the address of sys_open\n");
        return -1;
    }

    if ((ret = register_jprobe(&jp)) <0) 
    {
        printk("register_jprobe failed, returned %d\n", ret);
        return -1;
    }
    printk("Registered a jprobe.\n");
    return 0;
}

void cleanup_module(void)
{
    unregister_jprobe(&jp);
    printk("jprobe unregistered\n");
}

MODULE_LICENSE("GPL"); 
复制代码

Makefile

复制代码
#
# Variables needed to build the kernel module
#
name      = openlog

obj-m += $(name).o

all: build

.PHONY: build install clean

build:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules CONFIG_DEBUG_SECTION_MISMATCH=y

install: build
    -mkdir -p /lib/modules/`uname -r`/kernel/arch/x86/kernel/
    cp $(name).ko /lib/modules/`uname -r`/kernel/arch/x86/kernel/
    depmod /lib/modules/`uname -r`/kernel/arch/x86/kernel/$(name).ko

clean:
    [ -d /lib/modules/$(shell uname -r)/build ] && \
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
复制代码

编译并加载内核模块

make
insmod openlog.ko

测试运行效果

 

 

7. 后记

对于主机恶意行为检测来说,对于事件信息的捕获只是第一步,真正的重点在于:

1. 我们在系统中建立哪些监控Hook点,要监控哪些系统调用、哪些内核函数
2. 我们需要在监控回调注册点中获取哪些维度的信息
3. 搜集到相应的信息之后,我们怎样对这些信息进行建模,从而找出恶意行为轨迹
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
9天前
|
安全 Linux 虚拟化
网络名称空间在Linux虚拟化技术中的位置
网络名称空间(Network Namespaces)是Linux内核特性之一,提供了隔离网络环境的能力,使得每个网络名称空间都拥有独立的网络设备、IP地址、路由表、端口号范围以及iptables规则等。这一特性在Linux虚拟化技术中占据了核心位置🌟,它不仅为构建轻量级虚拟化解决方案(如容器📦)提供了基础支持,也在传统的虚拟机技术中发挥作用,实现资源隔离和网络虚拟化。
网络名称空间在Linux虚拟化技术中的位置
|
9天前
|
网络协议 安全 Linux
Linux网络名称空间之独立网络资源管理
Linux网络名称空间是一种强大的虚拟化技术🛠️,它允许用户创建隔离的网络环境🌐,每个环境拥有独立的网络资源和配置。这项技术对于云计算☁️、容器化应用📦和网络安全🔒等领域至关重要。本文将详细介绍在Linux网络名称空间中可以拥有的独立网络资源,并指出应用开发人员在使用时应注意的重点。
|
9天前
|
安全 网络协议 Linux
Linux网络名称空间概述
Linux网络名称空间是操作系统级别的一种虚拟化技术🔄,它允许创建隔离的网络环境🌐,使得每个环境拥有自己独立的网络资源,如IP地址📍、路由表🗺️、防火墙规则🔥等。这种技术是Linux内核功能的一部分,为不同的用户空间进程提供了一种创建和使用独立网络协议栈的方式。本文旨在全方面、多维度解释Linux网络名称空间的概念、必要性和作用。
Linux网络名称空间概述
|
10天前
|
Web App开发 Java Linux
Linux之Shell基本命令篇
Linux之Shell基本命令篇
Linux之Shell基本命令篇
|
7天前
|
存储 算法 Linux
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
20 6
|
9天前
|
网络协议 Linux
在Linux中,管理和配置网络接口
在Linux中管理网络接口涉及多个命令,如`ifconfig`(在新版本中被`ip`取代)、`ip`(用于网络设备配置)、`nmcli`(NetworkManager的CLI工具)、`nmtui`(文本界面配置)、`route/ip route`(处理路由表)、`netstat/ss`(显示网络状态)和`hostnamectl/systemctl`(主机名和服务管理)。这些命令帮助用户启动接口、设置IP地址、查看连接和路由信息。不同发行版可能有差异,建议参考相应文档。
19 4
|
11天前
|
Shell Linux
【Linux】12. 模拟实现shell
【Linux】12. 模拟实现shell
27 2
|
12天前
|
域名解析 监控 网络协议
Linux网卡与IP地址:通往网络世界的通行证 🌐
探索Linux网卡与IP地址关系,理解网卡作为网络通信的关键。Linux网卡需配置IP地址以实现唯一标识、通信、路由、安全管理和网络服务。无IP地址时,网卡在特定情况如局域网服务、网络监控、无线认证和网络启动可有限工作,但通用功能受限。配置IP地址通常通过`ifconfig`(传统)或`ip`(现代)命令,永久配置需编辑网络配置文件。配置错误如IP冲突、子网掩码错误、默认网关和DNS配置不当可能导致服务中断、网络拥堵、安全漏洞和数据丢失。重视网络配置的正确与安全至关重要。
Linux网卡与IP地址:通往网络世界的通行证 🌐
|
6月前
|
Unix Shell Linux
|
30天前
|
Shell Linux C语言
Linux中执行Shell的函数(popen,system,exec)介绍:分享一些常用的执行Shell的函数及其相关编程技巧和经验
Linux中执行Shell的函数(popen,system,exec)介绍:分享一些常用的执行Shell的函数及其相关编程技巧和经验
30 0