关于驭龙HIDS 驭龙HIDS (https://github.com/ysrc/yulong-hids ) 是一款由 YSRC 开发的入侵检测系统,集异常检测、监控管理为一体,拥有异常行为发现、快速阻断、高级分析等功能,可从多个维度行为信息中发现入侵行为。当前,驭龙agent主要会收集系统信息,计划任务,存活端口,登录日志,进程信息,服务信息,启动项,用户列表,web路径等信息,其中Windows用户登录信息读取了Windows的EventLog,经历了几个版本,更换了好几个方法,最终成型。这篇文章就稍微说一下这几种方法的思路和实现。如果有更好的思路或实现,期待各位交流指教。
Windows事件查看器与日志ID 我们知道Windows会把系统登录日志记录在Windows安全日志里,我们可以在事件查看器里筛选查看这些系统日志。
那如果要看登录相关的系统日志呢?可以用事件ID进行筛选,其中登录失败的事件ID为 4625, 而成功的登录ID为 4624。大部分的登录信息会包含在这两个事件ID里面。
贴一个我自己整理的比较重要的Windows登录相关日志的事件ID和简要介绍:
ID
Name
Introduction In Chinese
4624
Successful User Account Login
大部分登录事件成功时会产生的日志
4625
Failed User Account Login
大部分登录时间失败时会产生的日志(解锁屏幕并不会产生这个日志)
4672
Logon with Special Privs
特权用户登录成功时会产生的日志,例如我们登录”administrator”,一般会看到一条4624和4672日志一起出现
4648
Account Login with Explicit Credentials
一些其他的登录情况,如使用 runas /user
登录除当前以外的其他用户运行程序时,会产生这样的日志。(不过runas命令执行时同时也会产生一条4624日志)
驭龙会更加重视 4625 和 4624 两个ID的日志,以集中收集并分析系统的异常登录情况。PS. 巡风(https://github.com/ysrc/xunfeng ) 会定期扫描内网资产中的SMB弱口令(见: crack_smb插件),如果同时使用这两个系统,为避免产生多余的告警信息,可将巡风的地址添加到驭龙的白名单内。
v1.0 使用 powershell 获取 EventLog 如果说要写程序实现获取Windows登录日志,那么作为web安全选手,可能第一个想到的就是 powershell。这个也是我们最开始的思路。
powershell里可以用 Get-WinEvent
这个 cmdlet 获取 EventLog,例如:
使用 Get-WinEvent
获取登录成功的日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 PS C:\Windows\system32> Get-WinEvent -FilterHashtable @{'ProviderName' ='Microsoft-Windows-Security-Auditing' ;Id=4624 } ProviderName:Microsoft-Windows-Security-Auditing TimeCreated Id LevelDisplayName Message ----------- -- ---------------- ------- 2018 /1 /20 15 :44 :47 4624 信息 已成功登录帐户。...2018 /1 /20 15 :32 :01 4624 信息 已成功登录帐户。...2018 /1 /20 15 :29 :36 4624 信息 已成功登录帐户。...2018 /1 /20 15 :29 :36 4624 信息 已成功登录帐户。...2018 /1 /20 15 :27 :34 4624 信息 已成功登录帐户。...2018 /1 /20 15 :24 :57 4624 信息 已成功登录帐户。...
获取登录详细信息并格式化成xml代码:
1 2 3 4 5 &{$reslist =Get-WinEvent -FilterHashtable @{'ProviderName' ='Microsoft-Windows-Security-Auditing' ;Id=4624 };If ($reslist .length){For ($index =0 ;$index -le $reslist .length-1 ;++$index ){Write-Host $reslist [$index ].toxml()}}Else {Write-Host $res .toxml();}} <Event xmlns='https://schemas.microsoft.com/win/2004/08/events/event' ><System><Provider Name='Microsoft-Windows-Security-Auditing' ...
之后再用 golang 解析xml输出,提取我们想要的信息。这里的实现方式就比较多了。可以用正则或者 encoding/xml package 处理 xml 输出。
1 var RegexWindowsEvt = regexp.MustCompile(`<TimeCreated SystemTime='(?P<time>[\w\-\:]+)\.\w+'\/>.*<Data Name='TargetUserName'>(?P<username>[^<]+)</Data>.*<Data Name='TargetDomainName'>(?P<hostname>([^<]*))</Data><Data Name='Status'>(?P<status>\w+)</Data>.*<Data Name='IpAddress'>(?P<ip>[^<]+)</Data>` )
后来考虑到服务器上可能没有 powershell,且当时还想支持 Windows2003, 另外使用 golang 调用 powershell 代码,解析输出的方式也算不上优雅(可以说比较恶心了)。考虑到以上及其他一些原因我在以这种方式实现之后,又重构了这一部分的代码。
v2.0 使用 logparser 提取 EventLog logparser 是微软官方提供的小程序,支持解析 Windows2003 之前的日志格式 evt, 也支持 Windows2008 之后的格式 evtx。 当时我们也考虑到了接下来两种比较优雅的实现方式,在尽快实现,简单直接的目的下,我依赖于 logparser 实现了 v2.0 版本。
logparser 支持像sql语句一样的搜索方式,筛选 EventLog。例如:
使用 logparser 获取用户登录信息:
1 2 3 4 5 logparser "SELECT EventLog,TimeGenerated,Strings,ComputerName FROM Security WHERE EventID=4624 ORDER BY TimeGenerated DESC" EventLog TimeGenerated Strings ComputerName -------- ------------ ---------- --------------- ......
之后再从logparser的输出中提取我们想要的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 out, _ := res.Output() outstr := string (out) lines := strings.Split(outstr, "\n" ) for _, line := range lines { if strings.HasPrefix(line, "Security," ) { result := make (map [string ]string ) infolist := strings.Split(line, "," ) lpStringsList := strings.Split(infolist[2 ], "|" ) result["time" ] = strings.TrimSpace(infolist[1 ]) ... if common.InArray([]string {"-" , "127.0.0.1" , "::1" }, result["remote" ], false ) || common.InArray(common.Config.Filter.IP, result["remote" ], false ) { continue } loglist = append (loglist, result) } }
虽然这个版本不算好,但是毕竟支持了比较多的 Windows 版本,且稳定运行了一段时间。不过确实也不算优雅的实现,其实现在同程内部并没有 Windows2003 的服务器,所以接下来的实现我们决定不再支持 Windows2003,选择较为优雅可靠的实现方式。
v3.0 使用解析日志文件的方式提取 EventLog 之前的方式多多少少依赖于一些其他的工具,并没有直接读取 EventLog 的源文件。我们知道,Windows 的系统日志格式有两种,evtx和evt。其中 evt 是 Windows2003 所采用的格式,而 Evtx 是之后 Windows 采用至今的格式。
我们所需的登录日志属于 Security Event Log,日志文件路径为 C:\Windows\System32\winevt\Logs\Security.evtx
。解析这个格式需要了解 evtx 的格式规范,感兴趣的同学可以参考: https://github.com/libyal/libevtx/blob/master/documentation/Windows%20XML%20Event%20Log%20(EVTX).asciidoc .
在 v3.0 里面我使用了 golang-evtx 对evtx文件进行解析,调用内部接口读取 EventLog 信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 var ( winodwsEvtxFile = "C:\\Windows\\System32\\winevt\\Logs\\Security.evtx" winodwsEvtxFilex32 = "C:\\Windows\\Sysnative\\winevt\\Logs\\Security.evtx" timeFormat = "2006-01-02T15:04:05Z07:00" successEventID = int64 (4624 ) failedEventID = int64 (4625 ) usernamePath = evtx.Path("/Event/EventData/TargetUserName" ) ipAddressPath = evtx.Path("/Event/EventData/IpAddress" ) logonTypePath = evtx.Path("/Event/EventData/LogonType" ) localAddress = []string {"-" , "127.0.0.1" , "::1" } needlessLogonType = []string {"5" } ) ... evtxf, err := evtx.New(loginFile) if err != nil { log.Println(err.Error()) return nil } start, _ := time.Parse(timeFormat, starttime) for event := range evtxf.FastEvents() { createTime := event.TimeCreated() if starttime != "all" && createTime.Before(start) && createTime.Equal(start) { continue } eventlog := make (map [string ]string ) eventID := event.EventID() .... logonType, _ := event.GetString(&logonTypePath) ipAddress, _ := event.GetString(&ipAddressPath) eventlog["remote" ] = ipAddress eventlog["time" ] = createTime.Format(timeFormat) eventlog["username" ], _ = event.GetString(&usernamePath) loglist = append (loglist, eventlog) }
这里经 wolf 提示还有一个大坑需要注意一下,如果把golang的代码编译成 32 位的程序的话,在64位的操作系统下是不能访问读取 C:\\Windows\\System32\\
路径下的文件的,需要去访问 C:\\Windows\\Sysnative\\winevt\\Logs\\Security.evtx
路径才行。所以才需要定义两个路径。
虽然我很喜欢 golang-evtx 的接口规范和实现,但是这个库毕竟小众。和 wolf 重新调研了一下, wolf 决定参考并调用 https://github.com/elastic/beats 的代码,以调用 Windows 系统动态链接库的方式再次重构这段代码。
v4.0 调用 wevtapi.dll 获取 Event Log wevtapi.dll 是 windows2008 之后内置于系统 path 的动态链接库,内置如 EvtQuery
等多个函数可处理 Windows 系统日志。 elastic/beats 也是调用了 wevtapi.dll 监控并读取 Event Log的。
golang 的库有一个特点: 即使是项目里面编写的子 package,也可以做单独的 package 处理, 调用的时候可以避免把整个大项目编译到程序里,只编译固定的子 package。这里我们只需要 beats 的两个子 package 就好了。(但是 go get
的时候,还是会把整个项目 clone 到 $GOPATH 里。 驭龙的源码把第三方依赖全部放在 vendor 下,实际编译的时候无需 go get
这两个库)。
1 2 3 4 import ( "github.com/elastic/beats/winlogbeat/sys" win "github.com/elastic/beats/winlogbeat/sys/wineventlog" )
这个就是驭龙获取 Windows 登录日志最终版本代码了。由wolf大大参考 beats 源码编写,应该是目前最优的实现方式之一了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 func newWinEventLog (eventID string ) (EventLog, error) { var ignoreOlder time.Duration if first { ignoreOlder = time.Hour * 17520 first = false } else { ignoreOlder = time.Second * 60 } query, err := win.Query{ Log: "Security" , IgnoreOlder: ignoreOlder, Level: "" , EventID: eventID, Provider: []string {}, }.Build() if err != nil { return nil , err } l := &winEventLog{ query: query, channelName: "Security" , maxRead: 1000 , renderBuf: make ([]byte , renderBufferSize), outputBuf: sys.NewByteBuffer(renderBufferSize), } l.render = func (event win.EvtHandle, out io.Writer) error { return win.RenderEvent(event, 0 , l.renderBuf, nil , out) } return l, nil } func GetLoginLog () (resultData []map [string ]string ) { var loginFile string var timestamp int64 if common.Config.Lasttime == "all" { timestamp = 615147123 } else { ti, _ := time.Parse("2006-01-02T15:04:05Z07:00" , common.Config.Lasttime) timestamp = ti.Unix() } if runtime.GOARCH == "386" { loginFile = winodwsEvtxFilex32 } else { loginFile = winodwsEvtxFile } if _, err := os.Stat(loginFile); err != nil { log.Println(err.Error()) return } resultData = getSuccessLog(timestamp) resultData = append (resultData, getFailedLog(timestamp)...) return } func getSuccessLog (timestamp int64 ) (resultData []map [string ]string ) { l, err := newWinEventLog("4625" ) if err != nil { return } err = l.Open(0 ) if err != nil { return } reList, _ := l.Read() for _, rec := range reList { if rec.TimeCreated.SystemTime.Local().Unix() > timestamp { if common.InArray(localAddress, rec.EventData.Pairs[19 ].Value, false ) { continue } m := make (map [string ]string ) m["status" ] = "true" m["username" ] = rec.EventData.Pairs[5 ].Value m["remote" ] = rec.EventData.Pairs[19 ].Value m["time" ] = rec.TimeCreated.SystemTime.Local().Format("2006-01-02T15:04:05Z07:00" ) resultData = append (resultData, m) } } return } func getFailedLog (timestamp int64 ) (resultData []map [string ]string ) { l, err := newWinEventLog("4624" ) if err != nil { return } err = l.Open(0 ) if err != nil { return } reList, _ := l.Read() for _, rec := range reList { if rec.TimeCreated.SystemTime.Local().Unix() > timestamp { if common.InArray(localAddress, rec.EventData.Pairs[18 ].Value, false ) { continue } m := make (map [string ]string ) m["status" ] = "false" m["username" ] = rec.EventData.Pairs[5 ].Value m["remote" ] = rec.EventData.Pairs[18 ].Value m["time" ] = rec.TimeCreated.SystemTime.Local().Format("2006-01-02T15:04:05Z07:00" ) resultData = append (resultData, m) } } return }
后日谈 这就是驭龙实现 Windows 日志解析的发展过程了,也算是迭代了好几个版本。其中有很多方式和思路在渗透测试过程中也可以使用,希望能给大家带来点帮助。
如果大家有更好的建议和实现方法,期待来自大家的交流和反馈。
LINK
本博客所有内容只用于安全研究,请勿用于恶意攻击。 本文URL: "https://blog.neargle.com/2018/01/21/yulong-hids-windows-eventlog-iteration/index.html"