Commit 3abd184e authored by liming6's avatar liming6
Browse files

feature 添加scp监视功能

parent 4c295cf4
# readme # Readme
sshd-tool用于收集sshd的日志,过滤出用户的登录和退出ssh信息 file-minitor是一个监控sftp和scp上传文件动作并对上传文件进行病毒扫描的工具
依赖: ## 工作方式
- sshd (1) 针对sftp,它解析实时sftp日志,识别上传文件行为并使用clamdscan扫描文件
- rsyslog
工作流程: 需要rsyslog和sshd服务同时启用
配置sshd和rsyslog,将sshd日志转发到机器上的一个unix socket或者远端的tcp或udp a. 修改rsyslog配置,将日志发送到unix socket中
sshd-tool监听这个unix socket,过滤出需要的信息 ```bash
# 写入配置文件
cat > /etc/rsyslog.d/sftp.conf <<EOF
\$ModLoad omuxsock
\$OMUxSockSocket /tmp/rsyslog.sock
authpriv.* :omuxsock:
EOF
除此之外,sshd-tool需要解析各个系统用户家目录下的.ssh/authorized_keys文件的内容 # 重启rsyslog服务,启用上述配置
systemctl restart rsyslog
```
最终,sshd-tool提供查询服务 b. 修改 sshd_config 配置,让sftp将日志写入rsyslog的AUTHPRIV事件队列中
## todo ```
# Subsystem sftp /usr/libexec/openssh/sftp-server
Subsystem sftp /usr/libexec/openssh/sftp-server -l INFO -f AUTHPRIV
```
重启sshd服务:`systemctl restart sshd`
经过上述配置后,即可在unix socket `/tmp/rsyslog.sock` 监听到sftp日志了
(2) 针对scp,它利用auditd中的审计规则判识别写文件动作,需要系统auditd服务,并添加以下审计规则:
```bash
# 监控scp创建文件动作
-a always,exit -F arch=b64 -S openat -F exe=/usr/bin/scp -F a2&0x40 -F key=scp_create_file
-a always,exit -F arch=b64 -S open -F exe=/usr/bin/scp -F a1&0x40 -F key=scp_create_file
# 监控scp关闭文件动作
-a always,exit -F arch=b64 -S close -F exe=/usr/bin/scp -F key=scp_close_file
```
可以将上述规则写到 `/etc/audit/rules.d/audit.rules` 中并通过 `service auditd reload`命令持久化
- 适配ubuntu系统,对于Ubuntu,系统who -u中的pid是ssh日志中pid的子进程,需要处理一下
- 能查询出以前的,没有被sshd-tool记录的在线情况
## 注意 一旦上传文件完成,file-monitor会立即修改文件名,添加 `.scanning` 后缀,若扫描到文件,删除文件,否则会恢复文件名
扫描病毒文件的时间会根据文件类型、文件大小而变化,最长扫描时间为5分钟
同样大小下,`.zip`文件扫描比较慢,建议压缩包格式为 `.tar.gz`
## 日志
日志会记录在 `/var/log/file-monitor.<启动时间>.log` 文件里
## todo
- sftp登录在sshd日志里有记录,而who -u的输出是没有记录的 添加白名单:
- who -u的输出里,可能有多个pid相同的数据,那是同一个ssh连接的多个虚拟终端,由于没有登录动作,所以sshd日志里没有对应的日志条目
## auditd日志分析 - 用户白名单:不扫描指定用户上传的文件
- 路径白名单:不扫描上传到指定路径的文件
增加可识别文件类型功能,对于文本类型,直接跳过扫描
针对压缩包,使用流式扫描以加速扫描
\ No newline at end of file
cd cmd/daemon cd cmd/file-monitor
go clean go clean
go build -ldflags="-s -w" go build
if type upx > /dev/null; then \ No newline at end of file
upx daemon
fi
\ No newline at end of file
...@@ -44,7 +44,7 @@ func TestScanFile(t *testing.T) { ...@@ -44,7 +44,7 @@ func TestScanFile(t *testing.T) {
} }
func TestParseInt(t *testing.T) { func TestParseInt(t *testing.T) {
i, err := ParseInt("abc") i, err := ParseInt("-321")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
......
...@@ -2,28 +2,24 @@ package logic ...@@ -2,28 +2,24 @@ package logic
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/alphadose/haxmap" "github.com/elastic/go-libaudit/v2"
"github.com/elastic/go-libaudit/v2/aucoalesce" "github.com/elastic/go-libaudit/v2/aucoalesce"
"github.com/elastic/go-libaudit/v2/auparse" "github.com/elastic/go-libaudit/v2/auparse"
"github.com/shirou/gopsutil/v4/process"
) )
var ( var (
// 接收审计事件的管道 // 接收审计事件的管道
EventChan = make(chan *aucoalesce.Event, 1024) EventChan = make(chan *aucoalesce.Event, 8192)
// 记录有效事件的map
EventMap = haxmap.New[string, []*TaggedEvent]()
// 记录需要注意的可执行文件路径
ExecPath = map[string]bool{
"/usr/bin/scp": true,
"/usr/libexec/openssh/sftp-server": true,
}
// 记录需要注意的系统调用 // 记录需要注意的系统调用
Syscalls = map[string]FileAction{ Syscalls = map[string]FileAction{
...@@ -39,17 +35,12 @@ var ( ...@@ -39,17 +35,12 @@ var (
"unlinkat": FA_Rename, "unlinkat": FA_Rename,
} }
SCP = "/usr/bin/scp" SCP = "/usr/bin/scp"
SFTP = "/usr/libexec/openssh/sftp-server"
)
// FileTransformExec 传输文件的程序类型 EventMap = make(map[string]*EventSet)
type FileTransformExec uint8 EventMapLock = sync.RWMutex{} // 保护EventMap的锁
const ( ErrArgNil = errors.New("error arg is nil")
FTE_UNKNOWN FileTransformExec = iota
FTE_SFTP // sftp
FTE_SCP // scp
) )
type FileAction uint8 type FileAction uint8
...@@ -61,92 +52,270 @@ const ( ...@@ -61,92 +52,270 @@ const (
FA_Delete FA_Delete
) )
type TaggedEvent struct { func CleanSCP() {
Event *aucoalesce.Event EventMapLock.Lock()
Exec FileTransformExec defer EventMapLock.Unlock()
IsTabby bool // 标记是否为tabby客户端 toDelete := make([]string, len(EventMap))
Fd *int // 记录文件描述符 for k, v := range EventMap {
alive, _ := v.IsAlive()
if !alive {
toDelete = append(toDelete, k)
}
}
for _, v := range toDelete {
delete(EventMap, v)
}
} }
func FromEvent(event *aucoalesce.Event) *TaggedEvent { type EventHandler struct{}
if event == nil {
return nil func (h *EventHandler) ReassemblyComplete(msgs []*auparse.AuditMessage) {
} event, err := aucoalesce.CoalesceMessages(msgs)
result := TaggedEvent{ if err != nil {
Event: event, log.Printf("coalesce messages error: %v \n", err)
} }
switch event.Process.Exe { EventChan <- event
case SCP: }
result.Exec = FTE_SCP
case SFTP: func (h *EventHandler) EventsLost(count int) {
result.Exec = FTE_SFTP log.Printf("event lost: %d \n", count)
default: }
result.Exec = FTE_UNKNOWN
func StartAuditMonitor(wg *sync.WaitGroup) {
defer wg.Done()
cli, err := libaudit.NewMulticastAuditClient(nil)
if err != nil {
log.Printf("failed to create audit client: %v \n", err)
return
} }
// 如果是sftp,检查是否为 defer cli.Close()
if result.Exec == FTE_SFTP {
handler := &EventHandler{}
rea, err := libaudit.NewReassembler(8192, time.Second*180, handler)
if err != nil {
log.Printf("%v", err)
return
} }
// 如果是open、openat、close,会有文件描述符 defer rea.Close()
// 定期维护事件池
go func() {
ticker := time.NewTicker(time.Second * 15)
defer ticker.Stop()
for range ticker.C {
if rea.Maintain() != nil {
break
}
}
}()
go FiltAuditMsg()
return &result for {
rawMsg, err := cli.Receive(false)
if err != nil {
break
}
err = rea.Push(rawMsg.Type, slices.Clone(rawMsg.Data))
if err != nil {
log.Printf("push audit event error: %v\n", err)
}
}
close(EventChan)
} }
// EventSet 事件集,一个事件集记录了同一个进程的审计事件 // EventSet 事件集,一个事件集记录了同一个进程的审计事件
type EventSet struct { type EventSet struct {
Pid, PPID int // pid和ppid Pid, PPID, Uid int32 // pid、ppid和Uid
Events []*aucoalesce.Event // 记录所有事件 Events []*aucoalesce.Event // 记录所有事件
Fds map[int64][]*aucoalesce.Event // 记录可以获取文件描述符的事件 Fds map[int64][]*aucoalesce.Event // 记录可以获取文件描述符的事件
IsTabby *bool // 记录是否为tabby客户端 Lock sync.RWMutex // 锁,用于保护Fds和Events的读写
Exec FileTransformExec // 记录文件传输工具 }
Lock sync.RWMutex // 锁,用于保护Fds和Events的读写
} // PushEvent 向事件集中插入事件,并判断是否需要触发文件扫描
func (es *EventSet) PushEvent(event *aucoalesce.Event, syscall string) {
func NewEventSet() *EventSet { if event == nil {
return &EventSet{ return
Events: make([]*aucoalesce.Event, 0, 16), }
Fds: make(map[int64][]*aucoalesce.Event), switch syscall {
IsTabby: nil, case "open", "openat":
Exec: FTE_UNKNOWN, fd, err := getExitFd(event)
Lock: sync.RWMutex{}, if err != nil {
} log.Println("error: ", err)
} return
}
// func (es *EventSet) AddEvent(e *aucoalesce.Event) error { if fd == nil {
// if e == nil { log.Println("error: open(at) syscall, but can't get fd")
// return nil return
// } }
// es.Events = append(es.Events, e) es.Lock.Lock()
// sc := e.Data["syscall"] el, have := es.Fds[*fd]
// switch sc { if have && len(el) > 0 {
// case "close": // 已经用了这个fd,有问题,要么是没有删除旧fd,要么是有bug
bb, _ := json.Marshal(el)
// case "openat": cc, _ := json.Marshal(event)
log.Println("error: fd repeat, old: ", string(bb), "new: ", string(cc))
// case "": clear(el)
// } el = append(el, event)
es.Events = append(es.Events, event)
// if sc == "openat" { es.Lock.Unlock()
// i, err := ParseInt(e.Data["exit"]) } else {
// if err != nil { // ok,正常的
// return nil el = make([]*aucoalesce.Event, 0, 16)
// } el = append(el, event)
// } es.Fds[*fd] = el
es.Events = append(es.Events, event)
// return nil es.Lock.Unlock()
// } }
case "close":
// GenKey 为事件生成字符串id,格式为 pid-ppid // 获取close的第一个参数,那就是fd
func GenKey(event *aucoalesce.Event) string { if event.Data == nil {
return fmt.Sprintf("%s-%s", event.Process.PID, event.Process.PPID) return
}
fdStr, have := event.Data["a0"]
if !have {
log.Println("error: syscall close, but can't find the first arg")
return
}
fd, err := ParseInt(fdStr)
if err != nil {
log.Println("error: ", err)
return
}
es.Lock.Lock()
// 判断fd是否存在,存在就扫描文件
es.Events = append(es.Events, event)
el, have := es.Fds[fd]
if have {
delete(es.Fds, fd)
es.Lock.Unlock()
l := len(el)
if l == 0 {
// bug
log.Println("error: fds slices's length is 0")
return
}
filePath, err := getFilePath(el[l-1])
if err != nil {
log.Println("error: ", err)
return
}
if filePath == nil {
log.Println("error: find file path is nil")
return
}
go es.ScanFile(*filePath)
} else {
// 不存在就返回
es.Events = append(es.Events, event)
es.Lock.Unlock()
}
}
}
func (es *EventSet) ScanFile(path string) {
log.Printf("user UID(%d) upload file: %s, scanning...\n", es.Uid, path)
v, _ := ScanFile(path)
if v {
log.Printf("user UID(%d) upload file %s containing viruses \n", es.Uid, path)
} else {
log.Printf("user UID(%d) upload file %s not find virus \n", es.Uid, path)
}
}
// NewEventSet 从open、openat系统调用创建事件集
func NewEventSet(event *aucoalesce.Event, _ string) (*EventSet, error) {
info, err := getBaseInfo(event)
if err != nil {
return nil, err
}
result := EventSet{
Events: make([]*aucoalesce.Event, 0, 16),
Fds: make(map[int64][]*aucoalesce.Event),
Lock: sync.RWMutex{},
}
result.Events = append(result.Events, event)
if info[0] != nil {
result.Pid = *info[0]
}
if info[1] != nil {
result.PPID = *info[1]
}
if info[2] != nil {
result.Uid = *info[2]
}
if event.Data != nil {
exit, have := event.Data["exit"]
if !have {
return nil, errors.New("create file syscall, but not find exit code")
}
fd, err := ParseInt(exit)
if err != nil {
return nil, err
}
el := make([]*aucoalesce.Event, 0, 16)
el = append(el, event)
result.Fds[fd] = el
}
return &result, nil
}
// CheckAlive 检查进程是否存活
func (es *EventSet) IsAlive() (bool, error) {
p, err := process.NewProcess(es.Pid)
if err != nil {
// 进程已经结束
return false, nil
}
ppid, err := p.Ppid()
if err != nil {
return false, nil
}
if ppid == es.PPID {
return true, nil
}
return false, nil
} }
// FiltMsg 过滤出有用的事件,存放到map中,并触发处理程序 // PushEvent 向EventMap中放入事件
func FiltMsg() { func PushEvent(syscall string, event *aucoalesce.Event) {
if event == nil {
return
}
key := genKey(event)
switch syscall {
case "open", "openat":
// 新建文件
EventMapLock.Lock()
es, have := EventMap[key]
if !have {
ies, err := NewEventSet(event, syscall)
if err != nil {
log.Println(err)
EventMapLock.Unlock()
return
}
es = ies
EventMap[key] = es
}
EventMapLock.Unlock()
case "close":
// 关闭文件
EventMapLock.Lock()
es, have := EventMap[key]
if !have {
EventMapLock.Unlock()
return
}
EventMapLock.Unlock()
es.PushEvent(event, syscall)
}
}
// FiltAuditMsg 过滤出有用的事件,存放到map中,并触发处理程序
func FiltAuditMsg() {
for i := range EventChan { for i := range EventChan {
v, have := ExecPath[i.Process.Exe] if i.Process.Exe != SCP {
if !(v && have) {
// 不是指定的exe跳过
continue continue
} }
if i.Category != aucoalesce.EventTypeAuditRule { if i.Category != aucoalesce.EventTypeAuditRule {
...@@ -157,10 +326,6 @@ func FiltMsg() { ...@@ -157,10 +326,6 @@ func FiltMsg() {
// 不是系统调用,跳过 // 不是系统调用,跳过
continue continue
} }
// if i.Data == nil {
// // Data里没有数据的跳过
// continue
// }
sc, have := i.Data["syscall"] sc, have := i.Data["syscall"]
if !have { if !have {
continue continue
...@@ -174,50 +339,29 @@ func FiltMsg() { ...@@ -174,50 +339,29 @@ func FiltMsg() {
if i.Result != "success" { if i.Result != "success" {
continue continue
} }
printEvent(i) // printEvent(i)
PushEvent(sc, i)
} }
}
// HandleEvent 处理事件
func HaneldEvent(events []*TaggedEvent) {
// todo
panic("unimplemented")
}
// needHandle 判断是否需要进入处理流程
func needHandle(events []*TaggedEvent) bool {
// todo
panic("unimplemented")
} }
func printEvent(e *aucoalesce.Event) { func printEvent(e *aucoalesce.Event) {
// pid,ppid,syscall,
// open/openat:file,fd
// close:fd
if e == nil { if e == nil {
return return
} }
fmt.Println("-----")
switch e.Data["syscall"] { switch e.Data["syscall"] {
case "open", "openat": case "open", "openat":
fmt.Printf("open(at) pid: %s, ppid: %s, open %s, fd: %s \n", e.Process.PID, e.Process.PPID, e.File.Path, e.Data["exit"]) fd, _ := getExitFd(e)
fp, _ := getFilePath(e)
log.Printf("open(at): pid: %s ,fd: %d, file: %s \n", e.Process.PID, *fd, *fp)
case "close": case "close":
log.Printf("close: pid: %s ,fd: %s \n", e.Process.PID, e.Data["a0"])
fmt.Printf("close pid: %s, ppid: %s, fd: %s \n", e.Process.PID, e.Process.PPID, e.Data["a0"])
case "rename", "renameat", "renameat2":
fmt.Println(json.MarshalIndent(e, "", " "))
case "link", "linkat":
fmt.Printf("link(at): paths: %+v\n", e.Paths)
case "unlink", "unlinkat":
fmt.Printf("unlink(at): paths: %+v\n", e.Paths)
case "dup2", "dup3":
fmt.Println("dup")
default: default:
return log.Println("unknown syscall: ", e.Data["syscall"])
} }
// bb, err := json.MarshalIndent(e, "", " ")
// if err == nil {
// fmt.Println(string(bb))
// }
} }
func ParseInt(str string) (int64, error) { func ParseInt(str string) (int64, error) {
...@@ -231,10 +375,73 @@ func ParseInt(str string) (int64, error) { ...@@ -231,10 +375,73 @@ func ParseInt(str string) (int64, error) {
} }
} else { } else {
// 不是16进制数 // 不是16进制数
n, err := strconv.Atoi(str) n, err := strconv.ParseInt(str, 10, 64)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return int64(n), err return n, err
}
}
// genKey 为事件生成字符串id,格式为 pid-ppid
func genKey(event *aucoalesce.Event) string {
return fmt.Sprintf("%s-%s", event.Process.PID, event.Process.PPID)
}
// getBaseInfo 获取事件的基本信息,pid、ppid、uid
func getBaseInfo(event *aucoalesce.Event) ([3]*int32, error) {
result := [3]*int32{nil, nil, nil}
if event == nil {
return result, ErrArgNil
}
var pid, ppid, uid int32
if event.User.IDs != nil {
u, have := event.User.IDs["uid"]
if have {
uu, err := ParseInt(u)
if err == nil {
uid = int32(uu)
result[2] = &uid
}
}
}
p, err := ParseInt(event.Process.PID)
if err == nil {
pid = int32(p)
result[0] = &pid
}
p, err = ParseInt(event.Process.PPID)
if err == nil {
ppid = int32(p)
result[0] = &ppid
}
return result, nil
}
func getFilePath(event *aucoalesce.Event) (*string, error) {
if event == nil {
return nil, ErrArgNil
}
if event.File == nil {
return nil, nil
}
return &event.File.Path, nil
}
// getExitFd 获取系统调用的返回值
func getExitFd(event *aucoalesce.Event) (*int64, error) {
if event == nil {
return nil, ErrArgNil
}
if event.Data != nil {
exit, have := event.Data["exit"]
if have {
e, err := ParseInt(exit)
if err != nil {
return nil, err
}
return &e, nil
}
} }
return nil, nil
} }
...@@ -30,6 +30,21 @@ var ( ...@@ -30,6 +30,21 @@ var (
RegForceCloseFile = regexp.MustCompile(`(?i)^forced close "(.*)" bytes read (\d+) written (\d+)$`) RegForceCloseFile = regexp.MustCompile(`(?i)^forced close "(.*)" bytes read (\d+) written (\d+)$`)
) )
func CleanSftp() {
SftpLogLock.Lock()
defer SftpLogLock.Unlock()
toDelete := make([]int32, len(SftpLogMap))
for k, v := range SftpLogMap {
alive, _ := v.CheckAlive()
if !alive {
toDelete = append(toDelete, k)
}
}
for _, v := range toDelete {
delete(SftpLogMap, v)
}
}
type GetSLA interface { type GetSLA interface {
GetFfileAction() SftpLogAction GetFfileAction() SftpLogAction
SetPid(pid int32) SetPid(pid int32)
...@@ -454,9 +469,9 @@ func InsertAction(action GetSLA) { ...@@ -454,9 +469,9 @@ func InsertAction(action GetSLA) {
finfo.LogLock.Unlock() finfo.LogLock.Unlock()
} else { } else {
// 发生重复了??? // 发生重复了???
log.Println("warn: sftp file path repeat")
ls.Lock.Unlock() ls.Lock.Unlock()
} }
return return
case *SftpLogClose: case *SftpLogClose:
if num, have := act.Write.Get(); have && num == 0 { if num, have := act.Write.Get(); have && num == 0 {
...@@ -553,7 +568,6 @@ func InsertAction(action GetSLA) { ...@@ -553,7 +568,6 @@ func InsertAction(action GetSLA) {
} }
} }
// todo 需要适应文件名有空格的情况
func ParseSftpLog(s string) (GetSLA, error) { func ParseSftpLog(s string) (GetSLA, error) {
if !strings.Contains(s, "sftp-server") { if !strings.Contains(s, "sftp-server") {
// 不是sftp日志 // 不是sftp日志
...@@ -594,7 +608,8 @@ func ParseSftpLog(s string) (GetSLA, error) { ...@@ -594,7 +608,8 @@ func ParseSftpLog(s string) (GetSLA, error) {
return fta, nil return fta, nil
} }
func StartSftpMonitor() { func StartSftpMonitor(wg *sync.WaitGroup) {
defer wg.Done()
regRemove := regexp.MustCompile(`^<\d+>(.*)$`) regRemove := regexp.MustCompile(`^<\d+>(.*)$`)
os.Remove("/tmp/rsyslog.sock") os.Remove("/tmp/rsyslog.sock")
conn, err := net.ListenPacket("unixgram", "/tmp/rsyslog.sock") conn, err := net.ListenPacket("unixgram", "/tmp/rsyslog.sock")
...@@ -614,7 +629,7 @@ func StartSftpMonitor() { ...@@ -614,7 +629,7 @@ func StartSftpMonitor() {
continue continue
} }
if strings.Contains(items[1], "sftp-server") { if strings.Contains(items[1], "sftp-server") {
fmt.Println(items[1]) // fmt.Println(items[1])
l, err := ParseSftpLog(items[1]) l, err := ParseSftpLog(items[1])
if err != nil { if err != nil {
log.Println(err) log.Println(err)
......
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"os" "os"
"sshd-tool/cmd/file-monitor/logic" "sshd-tool/cmd/file-monitor/logic"
"sync"
"time" "time"
"github.com/gofrs/flock" "github.com/gofrs/flock"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
// import (
// "fmt"
// "log"
// "os"
// "sshd-tool/cmd/file-monitor/logic"
// "time"
// "github.com/elastic/go-libaudit/v2"
// "github.com/elastic/go-libaudit/v2/aucoalesce"
// "github.com/elastic/go-libaudit/v2/auparse"
// )
// type EventHandler struct{}
// func (h *EventHandler) ReassemblyComplete(msgs []*auparse.AuditMessage) {
// event, err := aucoalesce.CoalesceMessages(msgs)
// if err != nil {
// fmt.Printf("coalesce messages error: %v", err)
// }
// logic.EventChan <- event
// }
// func (h *EventHandler) EventsLost(count int) {
// fmt.Fprintf(os.Stderr, "=== event lost: %d \n", count)
// }
// func main() {
// cli, err := libaudit.NewMulticastAuditClient(nil)
// if err != nil {
// log.Fatalf("failed to create audit client: %v", err)
// }
// defer cli.Close()
// handler := &EventHandler{}
// rea, err := libaudit.NewReassembler(1024, time.Second*60, handler)
// if err != nil {
// log.Printf("%v", err)
// return
// }
// defer rea.Close()
// go func() {
// ticker := time.NewTicker(time.Second * 15)
// defer ticker.Stop()
// for range ticker.C {
// if rea.Maintain() != nil {
// break
// }
// }
// }()
// go logic.FiltMsg()
// for {
// rawMsg, err := cli.Receive(false)
// if err != nil {
// break
// }
// _ = rea.Push(rawMsg.Type, rawMsg.Data)
// }
// close(logic.EventChan)
// }
var ( var (
logfile *os.File logfile *os.File
flagDebug = pflag.Bool("debug", false, "debug mode, print log to stdout, not file") flagDebug = pflag.Bool("debug", false, "debug mode, print log to stdout, not file")
...@@ -109,6 +47,24 @@ func main() { ...@@ -109,6 +47,24 @@ func main() {
defer logFile.Close() defer logFile.Close()
} }
} }
wg := sync.WaitGroup{}
logic.StartSftpMonitor() wg.Add(2)
go logic.StartSftpMonitor(&wg)
go logic.StartAuditMonitor(&wg)
ctx, cancel := context.WithCancel(context.Background())
go func(c context.Context) {
ticker := time.NewTicker(time.Minute * 5)
defer ticker.Stop()
for {
select {
case <-ticker.C:
logic.CleanSCP()
logic.CleanSftp()
case <-c.Done():
return
}
}
}(ctx)
wg.Wait()
cancel()
} }
# readme # Readme
file-minitor是一个监控sftp和scp上传文件动作并对上传文件进行病毒扫描的工具 file-minitor是一个监控sftp和scp上传文件动作并对上传文件进行病毒扫描的工具
## 工作方式 ## 工作方式
针对sftp,它解析实时sftp日志,识别上传文件行为并使用clamdscan扫描文件 (1) 针对sftp,它解析实时sftp日志,识别上传文件行为并使用clamdscan扫描文件
针对scp,它利用auditd中的审计规则判识别写文件动作 需要rsyslog和sshd服务同时启用
a. 修改rsyslog配置,将日志发送到unix socket中
```bash
# 写入配置文件
cat > /etc/rsyslog.d/sftp.conf <<EOF
\$ModLoad omuxsock
\$OMUxSockSocket /tmp/rsyslog.sock
authpriv.* :omuxsock:
EOF
# 重启rsyslog服务,启用上述配置
systemctl restart rsyslog
```
b. 修改 sshd_config 配置,让sftp将日志写入rsyslog的AUTHPRIV事件队列中
```
# Subsystem sftp /usr/libexec/openssh/sftp-server
Subsystem sftp /usr/libexec/openssh/sftp-server -l INFO -f AUTHPRIV
```
重启sshd服务:`systemctl restart sshd`
经过上述配置后,即可在unix socket `/tmp/rsyslog.sock` 监听到sftp日志了
(2) 针对scp,它利用auditd中的审计规则判识别写文件动作,需要系统auditd服务,并添加以下审计规则:
```bash
# 监控scp创建文件动作
-a always,exit -F arch=b64 -S openat -F exe=/usr/bin/scp -F a2&0x40 -F key=scp_create_file
-a always,exit -F arch=b64 -S open -F exe=/usr/bin/scp -F a1&0x40 -F key=scp_create_file
# 监控scp关闭文件动作
-a always,exit -F arch=b64 -S close -F exe=/usr/bin/scp -F key=scp_close_file
```
可以将上述规则写到 `/etc/audit/rules.d/audit.rules` 中并通过 `service auditd reload`命令持久化
一旦上传文件完成,file-monitor会立即修改文件名,添加 `.scanning` 后缀,若扫描到文件,删除文件,否则会恢复文件名
一旦上传文件完成,file-monitor会立即修改文件名,添加 `.scanning` 后缀,若扫描到文件,删除文件,并在原地留下一个警示文件 `.virus`
如果没有病毒,会恢复文件名
扫描病毒文件的时间会根据文件类型、文件大小而变化,最长扫描时间为5分钟 扫描病毒文件的时间会根据文件类型、文件大小而变化,最长扫描时间为5分钟
同样大小下,`.zip`文件扫描比较慢,建议压缩包格式为 `.tar.gz` 同样大小下,`.zip`文件扫描比较慢,建议压缩包格式为 `.tar.gz`
## 日志
日志会记录在 `/var/log/file-monitor.<启动时间>.log` 文件里
## todo ## todo
添加白名单: 添加白名单:
- 用户白名单:不扫描指定用户上传的文件 - 用户白名单:不扫描指定用户上传的文件
- 路径白名单:不扫描上传到指定路径的文件 - 路径白名单:不扫描上传到指定路径的文件
\ No newline at end of file
增加可识别文件类型功能,对于文本类型,直接跳过扫描
针对压缩包,使用流式扫描以加速扫描
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment