Active Directory 更改跟踪






4.73/5 (5投票s)
Active Directory 更改审核解决方案。一个 Windows 服务,它将所有域控制器安全日志中选定的 AD 更改事件存储到 SQL 数据库。
也在 GitHub 上:https://github.com/snorrikris/ADchangeTracker
引言
对于任何使用 Microsoft Active Directory (AD) 进行身份验证的公司来说,能够很好地了解该环境中的情况是强制要求或非常有必要的。管理 AD 对系统管理员来说可能是一个巨大的挑战——这里提出的解决方案旨在对此有所帮助。
该软件为 Active Directory 提供安全审核解决方案。在 Microsoft Windows 计算机系统环境中,找出谁在 Active Directory 中何时更改了什么可能有点挑战。当然,前提是 AD 的更改已记录到所有域控制器服务器上的安全日志中。
此解决方案有三个主要部分;
- 存储 AD 更改事件的 SQL Server 数据库。
- 运行在所有可写域控制器上的 Windows 服务。此服务订阅安全日志中记录的所有事件,过滤 AD 更改事件,并将它们发送到 SQL 服务器进行处理。此服务对域控制器服务器的负载非常低——运行时内存占用不到 3MB,CPU 时间占用很少。它也尽可能不打扰,因为它只订阅安全日志中的事件——它根本不以任何方式与 Active Directory 本身通信。
- SQL Server Reporting Services (SSRS) 报表——它提供数据库中 AD 事件的查看和搜索。点击下面的图片了解详情。
注意——安装此软件不需要任何编程经验——只需下载 ADchangeTracker_release.zip 文件并按照提供的详细说明进行操作。当然,您需要有权访问 Active Directory 域控制器(作为域管理员)、SQL 服务器(作为 SysAdmin)和报表服务器(作为系统管理员)。
在我撰写本文时,此软件已在我生产环境的两个域控制器(Windows Server 2012 R2)上运行了一个多月,没有出现任何问题。
系统要求
- Active Directory 2008 R2 或更高版本。
- SQL Server 2008 R2 或更高版本。
- SQL Server Reporting Services 2008 R2 或更高版本。
此软件已在 Windows Server 2012 R2 Active Directory、SQL Server 2012 标准版上进行了测试——它应该可以在 2008 R2 上运行,但这尚未经过测试。
您应该可以使用 SQL Express 版本,但该版本不包含 Reporting Services(或 SQL Agent)。可以使用 SQL Management Studio 代替 SSRS 来查看和搜索数据库中的数据。
背景
尽管市场上存在许多优秀的 Active Directory 审计软件解决方案——但我发现除此([^])之外,没有免费的,因此我决定自己创建一个。经过一些研究,我得出结论,处理记录到安全日志中的 AD 更改事件 [^] 将提供我所需的关于 Active Directory 更改的信息。
工作原理
当 ADchangeTracker 服务启动时,它首先会读取其配置文件(ADchangeTracker.cfg 文件与 ADchangeTracker.exe 位于同一文件夹中)。配置文件中包含服务所需的不多的设置;SqlConnString - SQL 服务器连接字符串,AcceptedEventIDs - 已接受事件 ID 的列表,IgnoredEvents - 事件(ID 在 5136...5141 范围内)的对象类别列表,这些事件将被忽略(即不存储在数据库中),VerboseLogging - 开启或关闭(开启时——所有接收到的事件都将被记录),以及 DaysToKeepOldLogFiles - 保留日志文件的天数。
ADchangeTracker 服务(在域控制器上运行时)调用 EvtSubscribe
函数 [^] 来注册一个订阅,订阅(其正在运行的域控制器)安全日志中记录的所有事件。操作系统会在我们的代码中为每个记录的事件调用一个回调函数。注意——当服务第一次运行时,它将接收到已记录到安全日志中的所有事件,这可能需要几分钟时间,事件的数量取决于安全日志的大小——100MB 大约包含 500,000 个事件,服务标记每个已处理的事件,以便它可以从上次停止的地方继续。
接收到的每个事件都呈现为 XML 数据。然后我们提取 EventRecordID(事件日志生成的顺序号)和 EventID [^](使用了 pugixml [^] 库代码进行 XML 解析,因为它非常快速且占用空间小)。如果事件数据中提供了对象类别信息,我们也需要它。我们使用 EventID 和对象类别来过滤要处理的事件。EventRecordID 仅在写入服务日志文件时使用。
如果事件通过了过滤,我们通过调用 AD_DW 数据库中的 usp_ADchgEventEx 存储过程,将事件 XML 作为唯一的参数数据传递,将其发送到 SQL 服务器。如果调用成功,我们将此事件保存为安全日志中的书签。如果服务在此时间点停止或与 SQL 服务器断开连接,它可以从上次处理的事件继续。书签以文件形式保存在 %PROGRAMDATA% 文件夹中。(例如 C:\ProgramData\ADchangeTracker\Bookmark.bin)。
服务日志文件也保存在该文件夹中。每天都会创建一个新的日志文件,并保留最近 15 天(在配置文件中设置)的日志。
SQL 服务器中的 usp_ADchgEventEx 存储过程将从 XML 代码中提取信息,并将其存储在 ADevents 表中的单个行中的列里。XQuery 用于从事件 XML 代码中提取数据。 [^] [^] [^] ADevents 表将 SourceDC 和 EventRecordID 作为主键——防止重复事件插入表中。不同寻常的是,主键不是聚集索引,而是我们使用 EventTime 作为聚集索引键——这样做是为了提高查询性能,因为查询(几乎)总是会限制在某个时间段内。usp_ADchgEventEx 存储过程首先会检查它正在处理的事件是否已存在于表中,如果存在,则简单地向调用者返回成功,表示该事件已处理。
下表解释了我们处理的每个 EventID 的 ADevents 表的每个列中应期望的数据。
列名 | 事件 XML 数据 XPath |
SourceDC |
/Event/System/Computer 例如:DC1 |
EventRecordID |
/Event/System/EventRecordID 例如:143358864 |
EventTime |
/Event/System/TimeCreated/@SystemTime 例如:2015-07-13T08:31:36 |
EventID |
/Event/System/EventID 例如:5136 |
ObjClass |
= 'user' 当 EventID = 4738, 4740, 4720, 4725, 4724, 4723, 4722, 4767。 = 'group' 当 EventID = 4728, 4732, 4733, 4756。 = 'unknown' 当 EventID = 4781。 = 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="ObjectClass"] 当 EventID = 5136, 5137, 5139, 5141。 例如:user |
目标 | = 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="TargetDomainName"] + '\' +/Event/EventData/Data[@Name="TargetUserName"] 当 EventID = 4738, 4740, 4725, 4724, 4723, 4722, 4720, 4732, 4733, 4781, 4728, 4756, 4767。 = 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="ObjectDN"] 当 EventID = 5136, 5137, 5141。 = 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="OldObjectDN"] 当 EventID = 5139。 = 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="SubjectDomainName"] + '\' + /Event/EventData/Data[@Name="SubjectDomainName"] 当 EventID = 4740。 例如:contoso\john |
变更 | = 'NewTargetUserName: ' + 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="NewTargetUserName"] 当 EventID = 4781。 = 'MemberName: ' + 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="MemberName"] 当 EventID = 4728, 4756。 = 'MemberSID: ' + 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="MemberSid"] 当 EventID = 4732, 4733。 = '(Value Added) ' OR '(Value Deleted) ' + 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="AttributeLDAPDisplayName"] + ': ' + /Event/EventData/Data[@Name="AttributeValue"] 当 EventID = 5136。 = 'NewObjectDN: ' + 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="NewObjectDN"] 当 EventID = 5139。 = 'Calling computer: ' + 来自 XML 的数据,XPath:/Event/EventData/Data[@Name="TargetDomainName"] 当 EventID = 4740。 例如:(Value Added) msTSExpireDate: 20150911080051.0Z |
ModifiedBy |
/Event/EventData/Data[@Name="SubjectDomainName"] + '\' + /Event/EventData/Data[@Name="SubjectUserName"] 例如:contoso\admin |
EventXML | 未更改地存储事件 XML。 |
重要的是要知道 EventXML 列存储了相当多的数据。平均每个事件 2K。一些 AD 更改事件可能包含 50K 以上的 XML 数据,但我们过滤掉了(大部分?)这些事件。为防止数据库过大,我们需要定期删除旧数据。这可以通过计划一个 SQL Agent 作业来完成,例如每周运行一次,删除旧数据。或者只是偶尔手动进行。无论如何,您需要决定要保留数据的时长。保留 XML 数据的目的是,如果您需要比 ADevents 表列中提供更多信息时,可以查看它。您也可以考虑保留列数据,但丢弃 XML 数据。例如,您可以保留一年的列数据,但只保留一个月的 XML 数据。
-- Delete old XML data
UPDATE [AD_DW].[dbo].[ADevents] SET EventXml = NULL WHERE EventTime < '2015-05-01'
-- Delete old AD events data
DELETE [AD_DW].[dbo].[ADevents] WHERE EventTime < '2015-01-01'
关于代码
提供的源代码是使用 Visual Studio 2013 用 C++ 创建的,还包含一个 Installshield LE 安装项目。
此软件是一个标准的 NT 服务应用程序,对于服务代码,我使用了微软的这个示例 [^] 作为参考。
文件名 | 目的 |
---|---|
ADchangeTracker.cpp | 主入口点,服务代码和读取配置文件代码。 |
EventProcessing.cpp | 订阅安全日志事件,过滤事件。 |
AdoSqlServer.cpp | SQL Server ADO 代码。 |
LogSys.cpp | 写入文件日志。 |
当您阅读以下部分时——建议将项目在 Visual Studio 中打开 :)
ADchangeTracker.cpp
_tmain
函数
当服务控制管理器(SCM) 启动 ADchangeTracker 服务或从命令行运行时,会调用 main 函数。我们首先要做的是初始化文件日志系统,读取配置文件信息并将其存储在 EventProcessing.cpp 中声明的全局 theService
对象中。
如果存在命令行参数-install 或-uninstall,则会处理这些参数,然后退出。
接下来,我们调用 StartServiceCtrlDispatcher
函数来启动服务的主函数。此调用将一直到服务停止才会返回。如果该调用因错误代码 ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
而失败,我们则假定应用程序是从命令行运行的,显示一条消息并退出。
EventProcessing.cpp
此文件包含 CEventProcessing
类。只有一个此对象的实例——即全局 theService
对象。服务主入口点和服务控制处理程序入口点在此文件中。
ServiceMain
成员函数
当 ADchangeTracker 服务启动时,会调用 ServiceMain 成员函数。我们首先做的是向服务控制管理器注册一个服务控制处理程序。然后,我们将状态报告为 SERVICE_START_PENDING
给 SCM。接着,我们将 COM 初始化为多线程。现在,我们检查是否满足最低必需设置(来自配置文件)。然后,我们创建两个信号事件,用于在服务需要停止(m_hEvent_ServiceStop
)或我们丢失 SQL 服务器连接(m_hEvent_SqlConnLost
)时发出信号。此时初始化 ADO SQL 连接对象。最后,我们向 SCM 发出 SERVICE_RUNNING
信号,并调用 Start 成员函数。除非在启动阶段出现任何问题,否则我们需要发出 SERVICE_STOPPED
信号并返回。请注意,对 Start 的调用将直到服务停止才会返回。
Start
成员函数
如果初始化成功,则从 ServiceMain 函数调用 Start 成员函数。首先,我们尝试连接到 SQL 服务器,如果成功,则启动安全日志事件订阅。但是,如果无法连接到 SQL 服务器,我们将不会开始订阅事件。我们需要等到连接恢复。在这里,我们进入一个 while(true) 循环——等待其中一个信号事件(m_hEvent_ServiceStop
、m_hEvent_SqlConnLost
)被发出信号。
如果我们丢失了 SQL 连接,我们需要停止事件订阅。我们将等待大约 60 秒,然后重试连接到 SQL 服务器。一旦连接重新建立,我们将再次开始事件订阅——从上次在日志中中断的地方继续。
ProcessEvent
成员函数
此函数处理操作系统对安全日志中记录的每个事件的回调。首先,我们将事件呈现为 XML 数据。然后调用 FilterAndSendEventToSql
函数。之后,我们检查 SQL 连接是否已丢失。
FilterAndSendEventToSql
成员函数
此函数从 XML 代码中提取 EventRecordID、EventID 和 ObjectClass。然后,我们检查 EventID 是否在接受的 ID 列表中,如果在,我们则检查 ObjectClass 是否在忽略列表中——如果不在,则将此事件转发给 SQL 服务器。
关注点
在开发此项目过程中,我学到了很多东西——我想最重要的就是学习了 XQuery 和 XPath。正如您可能注意到的,我不太喜欢在本文中复制粘贴代码——但为了好玩,我想在这里放上 usp_ADchgEventEx 存储过程的代码——因为它是我写过的最难的部分。
CREATE PROCEDURE [dbo].[usp_ADchgEventEx]
@XmlData nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @x XML = @XmlData;
-- Get EventRecordID and SourceDC from XML data.
DECLARE @EventRecordID int;
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @EventRecordID = @x.value('(/Event/System/EventRecordID)[1]', 'int');
DECLARE @SourceDC nvarchar(128);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @SourceDC = @x.value('(/Event/System/Computer)[1]', 'nvarchar(128)');
-- Early exit if event already processed (exists in table).
IF EXISTS(SELECT EventRecordID FROM dbo.ADevents
WHERE EventRecordID = @EventRecordID AND SourceDC = @SourceDC)
RETURN;
DECLARE @EventID int;
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @EventID = @x.value('(/Event/System/EventID)[1]', 'int'); -- AS EventID
DECLARE @ObjClass nvarchar(128), @Target nvarchar(256) = '', @Changes nvarchar(256) = '';
-- Set @ObjClass depending on EventID:
SELECT @ObjClass = 'user' WHERE @EventID IN (4740, 4738, 4725, 4724, 4723, 4722, 4720, 4767);
SELECT @ObjClass = 'unknown' WHERE @EventID IN (4781);
SELECT @ObjClass = 'group' WHERE @EventID IN (4728, 4732, 4733, 4756);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @ObjClass = @x.value('(/Event/EventData/Data[@Name="ObjectClass"])[1]', 'nvarchar(64)')
WHERE @EventID IN (5136, 5137, 5139, 5141);
-- Set @Target depending on EventID:
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Target = @x.value('(/Event/EventData/Data[@Name="SubjectDomainName"])[1]', 'nvarchar(64)') + '\'
+ @x.value('(/Event/EventData/Data[@Name="TargetUserName"])[1]', 'nvarchar(64)')
WHERE @EventID IN (4740);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Target = @x.value('(/Event/EventData/Data[@Name="TargetDomainName"])[1]', 'nvarchar(64)') + '\'
+ @x.value('(/Event/EventData/Data[@Name="TargetUserName"])[1]', 'nvarchar(64)')
WHERE @EventID IN (4738, 4725, 4724, 4723, 4722, 4720, 4728, 4732, 4733, 4756, 4767);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Target = @x.value('(/Event/EventData/Data[@Name="TargetDomainName"])[1]', 'nvarchar(64)') + '\'
+ @x.value('(/Event/EventData/Data[@Name="OldTargetUserName"])[1]', 'nvarchar(64)')
WHERE @EventID IN (4781);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Target = @x.value('(/Event/EventData/Data[@Name="ObjectDN"])[1]', 'nvarchar(128)')
WHERE @EventID IN (5136, 5137, 5141);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Target = @x.value('(/Event/EventData/Data[@Name="OldObjectDN"])[1]', 'nvarchar(128)')
WHERE @EventID IN (5139);
-- Set @Changes depending on EventID:
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Changes = 'Calling computer: '
+ @x.value('(/Event/EventData/Data[@Name="TargetDomainName"])[1]', 'nvarchar(128)')
WHERE @EventID IN (4740);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Changes = 'NewTargetUserName: '
+ @x.value('(/Event/EventData/Data[@Name="NewTargetUserName"])[1]', 'nvarchar(128)')
WHERE @EventID IN (4781);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Changes = 'MemberName: '
+ @x.value('(/Event/EventData/Data[@Name="MemberName"])[1]', 'nvarchar(128)')
WHERE @EventID IN (4728, 4756);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Changes = 'MemberSID: '
+ @x.value('(/Event/EventData/Data[@Name="MemberSid"])[1]', 'nvarchar(128)')
WHERE @EventID IN (4732, 4733);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Changes = 'NewObjectDN: '
+ @x.value('(/Event/EventData/Data[@Name="NewObjectDN"])[1]', 'nvarchar(128)')
WHERE @EventID IN (5139);
IF @EventID = 5136
BEGIN
DECLARE @OpType nvarchar(32);
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @OpType = @x.value('(/Event/EventData/Data[@Name="OperationType"])[1]', 'nvarchar(32)');
IF @OpType = '%%14674'
SET @OpType = 'Value Added';
ELSE IF @OpType = '%%14675'
SET @OpType = 'Value Deleted';
WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/win/2004/08/events/event')
SELECT @Changes = '(' + @OpType + ') '
+ @x.value('(/Event/EventData/Data[@Name="AttributeLDAPDisplayName"])[1]', 'nvarchar(128)')
+ ': ' + @x.value('(/Event/EventData/Data[@Name="AttributeValue"])[1]', 'nvarchar(128)');
END
-- Insert new row into ADevents table.
;WITH XMLNAMESPACES (
default 'http://schemas.microsoft.com/win/2004/08/events/event'
)
,[Event] AS
(
SELECT @x.value('(/Event/System/TimeCreated/@SystemTime)[1]', 'datetime2') AS EventTime
,@x.value('(/Event/EventData/Data[@Name="SubjectDomainName"])[1]', 'nvarchar(64)') + '\'
+ @x.value('(/Event/EventData/Data[@Name="SubjectUserName"])[1]', 'nvarchar(64)') AS ModifiedBy
,@x AS EventXml
)
INSERT INTO dbo.ADevents
SELECT @SourceDC, @EventRecordID, e.EventTime, @EventID AS EventID,
@ObjClass AS ObjClass, @Target AS [Target], @Changes AS [Changes], e.ModifiedBy,
e.EventXml
FROM [Event] e
END
历史
V1.0 首次发布。
2016 年 1 月 9 日 添加了小型错误修复。