An article by Amit Basu
This document presents a method of tamper detection of sensitive data. SQL Server provides many mechanisms to authenticate and authorize users to perform certain activities in the database. These mechanisms ensure that other than those with proper authentication, no one else can view or modify data. However, this still doesn't protect the system when these authorized users tamper with data - modify or delete them in any way. The suggested method illustrates a way how such tampering by an authorized user can be detected. While this method doesn't provide tamper-prevention measures, but as there is no such thing as ultimate security, detection of such tampers will help maintaining the integrity of information in a great way.
Consider the scenario where you have an Audit Trail database. All the activities from your application get logged onto that database along with the name of the application user who has performed that operation. This is a standard approach for any application supporting government regulated Compliance. Typically, this Audit Trail database is never updated and always have rows serially updated. Different mechanisms exist to insert record to the Audit Trail database like direct inserts from Applications (perhaps through a data access layer), through database triggers etc. Once the data is inserted, the data is supposed to remain intact should there come any forensic requirement in future. This database, if separate from the application database, or at least the set of tables which comprise of Audit Trail information, are usually protected by authentication and authorization schemes provided by SQL Server.
Every application will have at least one SQL Server login which can have write-access to this data - right? This is the user who will be impersonated at the required time to write to the Audit information. The question is now, what if I know the credentials of that user like password and login, and then use this credential to get into the system, delete or modify a particular row from the Audit information because I don’t want anyone to see that I have actually done something wrong in the main application, intentionally or by mistake? You may ask that how in the first place I will know those credentials. Well, I am the administrator of that application and I myself have messed up something which I want to cover up now. See - I told you that there is no such thing as ultimate security!
Therefore, we need some tool with the help of which we can at least find out, from time to time, that whether the data that exists, is it really clean and un-tampered? Or, the data that we see - is that all? Or was there some other data that have disappeared - has someone deleted one or more rows?
One of the solutions (please note there could be other solutions also) that I am presenting here can boast of the following features: it can detect modifications in any row of a table
Like any good thing in life - the solution also comes with some disadvantages! They are:
We will go by our example of Audit Trail database. This is a database having two independent tables - AuditLog and AuditUser. These two tables, in reality, can reside in the same application database also. The objective will be to protect AuditLog table so that any tampering in that can be detected.
For this purpose, we will have two special columns called HReserved and VReserved (they should have been RowHash and ColHash ideally, but ain't we love to underestimate the thieves by trying to fool them?). The algorithm involving these two columns are:
That's all there to it! Yup - that simple but effective!
Now that we have a basic idea of how we are going to do this, let's now do it step by step. Here we go:
Run the following script to create the two tables:
CREATE TABLE AuditLog ( AuditId BIGINT IDENTITY (1, 1) NOT NULL, AuditSummary NVARCHAR (3000) NOT NULL, AuditUserId NVARCHAR(200) NOT NULL, Details XML NULL, RecordTimeStamp DATETIME NOT NULL, HReserved INT NULL, VReserved BIGINT NULL CONSTRAINT PK_AuditLog PRIMARY KEY CLUSTERED (AuditId DESC) ) CREATE TABLE AuditUser ( AuditUserId INT IDENTITY (1, 1) NOT NULL, AuditUser NVARCHAR (256) NOT NULL, RecordTimeStamp DATETIME NOT NULL CONSTRAINT PK_AuditUser PRIMARY KEY CLUSTERED ( AuditUserId DESC ) )
The first table contains all Audit logging information like a textual summary, audit log details in XML format so that later on queries can be made on that.
The second table, AuditUser exists to provide normalization in the AuditLog table. While the columns AuditSummary and Details log the actual operation within the application, AuditUserId is used to keep track of who made the operation. The columns HReserved and VReserved have already been explained. Please note that the primary key of the table, AuditId has a descending index on it to ensure a faster way for the VReserved calculation.
Because we need to calculate horizontal and vertical row hashes, insert operations in the AuditLog table is preferably done through a stored procedure. Before it actually insert the new row, the stored procedure carries out three tasks:
CREATE PROCEDURE upAddAudit ( @AuditUser NVARCHAR(256), @AuditSummary NVARCHAR(3000), @Details XML = NULL ) AS BEGIN SET NOCOUNT ON BEGIN TRANSACTION IF (@AuditUser IS NULL) OR @AuditUser = '' SET @AuditUser = 'Anonymous' --does this user already exist? DECLARE @UserId INT SELECT @UserId = AuditUserId FROM AuditUser WHERE AuditUser = @AuditUser -- if he is a new user, insert her record IF @UserId IS NULL BEGIN INSERT INTO AuditUser (AuditUser, RecordTimeStamp) VALUES ( @AuditUser, GETUTCDATE() ) SET @UserId = SCOPE_IDENTITY() END IF @@ERROR <> 0 GOTO ErrorExit --the user is found, so first insert the Audit record INSERT INTO AuditLog ( AuditUserId, AuditSummary, Details, RecordTimeStamp ) VALUES ( @UserId, @AuditSummary, @Details, GETUTCDATE() ) IF @@ERROR <> 0 GOTO ErrorExit -- now is the time to make this record tamper-proof DECLARE @CurrentId INT DECLARE @HReserved INT DECLARE @VReserved BIGINT SET @CurrentId = SCOPE_IDENTITY() -- calculate the row hash SELECT @HReserved = BINARY_CHECKSUM( AuditId, RecordTimeStamp, AuditUserId, AuditSummary, Details ) FROM AuditLog WHERE AuditId = @CurrentId -- Calculate column hash SELECT TOP 2 @VReserved = CHECKSUM_AGG( ISNULL(HReserved,0)) FROM AuditLog WHERE AuditId < @CurrentId SET @VReserved = ISNULL(@VReserved, 0) --update the row with the calculated values UPDATE AuditLog SET VReserved = @VReserved + @HReserved, HReserved = @HReserved WHERE AuditId = @CurrentId IF @@ERROR <> 0 GOTO ErrorExit --commit data if all the transactions above are successful COMMIT TRANSACTION --return the current AuditId in case it is required RETURN @CurrentId --handle errors ErrorExit: ROLLBACK TRANSACTION RETURN -1 END
Let&single;s add some data now, which we can test later.
EXEC upAddAudit
'Brishti',
'Added new customer',
'13 '
EXEC upAddAudit
'Amit',
'Added new customer',
'14 '
EXEC upAddAudit
'Brishti',
'Added new customer',
'15 '
EXEC upAddAudit
'Brishti',
'Added new product',
'59 '
EXEC upAddAudit
'Amit',
'Modified product details',
'81 '
After adding data through the stored procedure, this two tables will have the following data:

Now that we have some data in place, let's try tampering them and then we will see how the detection works.
Let's modify one row and delete one row from the AuditLog table.
UPDATE AuditLog SET AuditUserId = 3 WHERE AuditId = 4 DELETE AuditLog WHERE AuditId = 2
We need a stored procedure now for the purpose of detection:
CREATE PROCEDURE upCheckAudit
(@StartDate DATETIME = NULL,
@EndDate DATETIME = NULL
)
AS
BEGIN
SET NOCOUNT ON
DECLARE @Result TABLE (AuditId INT, TamperType VARCHAR(50))
DECLARE @AuditId INT
DECLARE @HReserved INT
DECLARE @VReserved BIGINT
DECLARE @Temp BIGINT
-- if start date is passed as NULL, set to one week back
IF @StartDate IS NULL
SET @StartDate = dbo.ufStartDateTime
( DATEADD(ww, -1, GETUTCDATE()))
--and also make sure start date has time stamp from 00:00:00
SET @StartDate = dbo.ufStartDateTime ( @StartDate)
--if end date is passed as null, set it to current date
IF @EndDate IS NULL -- set to current date
SET @EndDate = GETUTCDATE()
--and make sure end date has time stamp upto 23:59:59
SET @EndDate = dbo.ufEndDateTime ( @EndDate)
--find all modified rows in the specified date range and
--put them into temporary table
INSERT INTO @Result (
AuditId,
TamperType
)
(SELECT AuditId,
'Modified'
FROM AuditLog
WHERE HReserved <> BINARY_CHECKSUM(
AuditId,
RecordTimeStamp,
AuditUserId,
AuditSummary,
Details
)
AND
DATEADD( s, DATEDIFF(s, GETUTCDATE(), GETDATE() ), RecordTimeStamp )
BETWEEN @StartDate AND @EndDate
)
--find all deleted rows in the specified date range and
--put them into temporary table
DECLARE cRow CURSOR LOCAL FORWARD_ONLY KEYSET READ_ONLY
FOR SELECT AuditId, HReserved, VReserved
FROM AuditLog
WHERE DATEADD( s, DATEDIFF(s, GETUTCDATE(), GETDATE() ), RecordTimeStamp )
BETWEEN @StartDate AND @EndDate
ORDER BY AuditId ASC
OPEN cRow
FETCH NEXT FROM cRow INTO @AuditId, @HReserved, @VReserved
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT TOP 2
@Temp = CHECKSUM_AGG( ISNULL(HReserved,0))
FROM AuditLog
WHERE AuditId < @AuditId
SET @Temp = ISNULL(@Temp, 0)
SET @Temp = @Temp + @HReserved
IF @Temp <> @VReserved
BEGIN
INSERT INTO @Result (
AuditId,
TamperType
)
(SELECT @AuditId - 1,
'Deleted' AS 'TamperType' )
BREAK
END
FETCH NEXT FROM cRow INTO @AuditId, @HReserved, @VReserved
END
CLOSE cRow
DEALLOCATE cRow
--check for the last row deletion
IF NOT EXISTS
(SELECT *
FROM AuditLog
WHERE AuditId = ident_current('AuditLog'))
INSERT INTO @Result (AuditId, TamperType)
VALUES (ident_current('AuditLog'), 'Deleted')
--we have all tamper-detections done - spit out the result!!!
SELECT * FROM @Result
END
Wait! Don't yet - execute it now. You may have noticed that there are two user-defined functions used in the procedure above. Apart from formatting the dates, these two functions correctly modify the time-component of the dates to 00:00:00 and 23:59:59 depending on if the passed parameter is a starting day or ending day. Here they are
CREATE FUNCTION ufStartDateTime (@StartDate DATETIME) RETURNS DATETIME AS BEGIN RETURN CAST(YEAR( @StartDate ) AS VARCHAR(4)) + '-' + CAST(MONTH( @StartDate ) AS VARCHAR(2)) + '-' + CAST(DAY( @StartDate ) AS VARCHAR(2)) + ' 00:00:00' END GO CREATE FUNCTION ufEndDateTime (@EndDate DATETIME) RETURNS DATETIME AS BEGIN RETURN CAST(YEAR( @EndDate ) AS VARCHAR(4)) + '-' + CAST(MONTH( @EndDate ) AS VARCHAR(2)) + '-' + CAST(DAY( @EndDate ) AS VARCHAR(2)) + ' 23:59:59.99' END GO
The upCheckAudit procedure accepts default parameters - starting day and ending day of the time period we want to track. In case they are not passed, they default to the last seven days from the current date.
Let's see now how it responds when we tell it to discover the tampers (we have already tampered the data, in case you have forgotton!)
EXEC upCheckAudit
And voila:
About the Author: Amit Basu has an extensive experience on Microsoft based technologies. Apart from his current work as Project Manager with Gamcom Solutions Ltd. UK, he is busy with writing a book on Chess Programming. He loves photography, long drives, and music. He also programs poetry, in his own words!