关于驭龙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和简要介绍:
runas /user
驭龙会更加重视 4625 和 4624 两个ID的日志,以集中收集并分析系统的异常登录情况。PS. 巡风(https://github.com/ysrc/xunfeng) 会定期扫描内网资产中的SMB弱口令(见: crack_smb插件),如果同时使用这两个系统,为避免产生多余的告警信息,可将巡风的地址添加到驭龙的白名单内。
v1.0 使用 powershell 获取 EventLog
如果说要写程序实现获取Windows登录日志,那么作为web安全选手,可能第一个想到的就是 powershell。这个也是我们最开始的思路。
Get-WinEvent
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='http://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 采用至今的格式。
C:\Windows\System32\winevt\Logs\Security.evtx
在 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 windows event log file with evtx format after win2005
winodwsEvtxFile = "C:\\Windows\\System32\\winevt\\Logs\\Security.evtx"
winodwsEvtxFilex32 = "C:\\Windows\\Sysnative\\winevt\\Logs\\Security.evtx"
// timeFormat starttime format
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 5:Service(by Scheduled Tasks or services)
needlessLogonType = []string{"5"}
)
...
// Regular "winodwsEvtxFile"
evtxf, err := evtx.New(loginFile)
if err != nil {
log.Println(err.Error())
return nil
}
start, _ := time.Parse(timeFormat, starttime)
for event := range evtxf.FastEvents() {
// If before start It was the data we had
createTime := event.TimeCreated()
if starttime != "all" && createTime.Before(start) && createTime.Equal(start) {
continue
}
eventlog := make(map[string]string)
// only need login data
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)
}
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
EvtQuery
go getgo 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
// code by wolf
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
}
// GetLoginLog 获取系统登录日志
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 {
// 不支持2003
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 {
// rec.EventData.Pairs[10].Value != "5" &&
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 {
// rec.EventData.Pairs[8].Value != "5" &&
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"