关于驭龙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='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 采用至今的格式。

我们所需的登录日志属于 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 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)
}

这里经 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
// 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 日志解析的发展过程了,也算是迭代了好几个版本。其中有很多方式和思路在渗透测试过程中也可以使用,希望能给大家带来点帮助。

如果大家有更好的建议和实现方法,期待来自大家的交流和反馈。

本文URL: https://blog.neargle.com/2018/01/21/yulong-hids-windows-eventlog-iteration/index.html