驭龙 EventLog 读取模块的迭代历程
2019-08-31 00:34:56 Author: mp.weixin.qq.com(查看原文) 阅读量:60 收藏

关于驭龙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和简要介绍:

IDNameIntroduction In Chinese
4624Successful User Account Login大部分登录事件成功时会产生的日志
4625Failed User Account Login大部分登录时间失败时会产生的日志(解锁屏幕并不会产生这个日志)
4672Logon with Special Privs特权用户登录成功时会产生的日志,例如我们登录”administrator”,一般会看到一条4624和4672日志一起出现
4648Account 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 获取登录成功的日志:

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代码:

&{$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 输出。

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 获取用户登录信息:

logparser "SELECT EventLog,TimeGenerated,Strings,ComputerName FROM Security WHERE EventID=4624 ORDER BY TimeGenerated DESC"EventLog TimeGenerated Strings ComputerName
-------- ------------ ---------- ---------------
......

之后再从logparser的输出中提取我们想要的信息。

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 版本,且稳定运行了一段时间。不过确实也不算优雅的实现,后来讨论了下,其实2003目前还在用的厂商应该很少了,所以决定干脆不再支持 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 信息。

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 重新调研了一下, 决定参考并调用 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` 这两个库)。

import (

    "github.com/elastic/beats/winlogbeat/sys"

    win "github.com/elastic/beats/winlogbeat/sys/wineventlog"

)

这个就是驭龙获取 Windows 登录日志最终版本代码了,由wolf参考 beats 源码编写。

// 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

往期文章

巡风已支持检测 CVE-2017-3506 weblogic漏洞

moloch 网络流量回溯分析系统

Exploiting Python PIL Module Command Execution Vulnerability

同程旅游四层负载实战

基于ASM的java字符串混淆工具实现

为OLLVM添加字符串混淆功能

Jenkins 高危代码执行漏洞检测/开源漏洞靶场

Eat.Hack.Sleep.Repeat - YSRC 第二期夏日安全之旅

带你一程,守卫者们,集结了!

Django的两个url跳转漏洞分析:CVE-2017-7233&7234

应急不扎心 巡风现已支持检测S2-045漏洞

同程安全研究员获谷歌、腾讯安全致谢

Android 字符串及字典混淆开源实现

我可能是用了假的隐身模式

同程旅游 Hadoop 安全实践

点我的链接就能弹你一脸计算器

点我的链接我就能知道你用了哪些chrome插件

YSRC诚意之作,巡风-企业安全漏洞快速应急、巡航系统

基于文件特征的Android模拟器检测

Android逆向与病毒分析

F-Scrack 弱口令检测脚本

unsafe 模式下的 CSP Bypass

被动扫描器 GourdScan v2.0 发布!

Android App常见逆向工具和使用技巧

XSS Trap - XSS DNS防护的简单尝试

A BLACK PATH TOWARD THE SUN - HTTP Tunnel 工具简介

初探Windows Fuzzing神器----Winafl


文章来源: https://mp.weixin.qq.com/s/rHDJ2tQWEaZLikMt5bgCsw
如有侵权请联系:admin#unsafe.sh