是否可以在没有 SSMS 的情况下编辑未加密的 SQL Server 备份文件 (*.bak) 中的数据?

lak*_*han 2 security sql-server visual-studio-2017

我发现我的数据库中的一项存在差异错误(剩余数量 <>(采购 - 销售))。到目前为止,有超过5000个项目,超过100,000笔交易。对于此计算,每次都运行相同的过程。

但只有一个项目有错误,而且只发生过一次。它似乎没有从应用程序方面发生,因为它已经运行了超过 100,000 次。数据库服务器没有受到物理保护,也没有连接到 Internet。只有我可以登录到服务器。自从我记录错误以来,程序中没有发生任何异常。我不明白上述错误是如何发生的。

我已经看到可以使用记事本打开未加密的 SQL Server 备份文件并读取其数据。以下场景是否可能?

  1. 使用Live CD启动服务器并复制数据库 .mdf 文件
  2. 编辑和更改它的数据(例如:一些数值)
  3. 用被黑的数据库替换服务器上的原始 .mdf 文件

如果上述情况不可能发生,是否有可能是由于 SQL Server 端的错误或通信错误(而不是应用程序代码端)导致的。

Dan*_*man 10

根据我的经验,这些症状的根本原因通常是应用程序和/或 T-SQL 代码中的竞争条件,而不是由于硬件、网络或恶意活动造成的数据损坏。竞争条件是阴险的,因为不希望的结果是偶然发生的,难以重现,而且可能很少发生。

必须使用事务以及适当的隔离级别(或锁定提示),以确保项目表中的诸如剩余数量之类的聚合值反映相关的采购和销售事务。

编辑

测试并发问题是一项挑战,因为需要多个会话,而且通常也需要多次迭代。下面是一个使用多个 SSMS 查询窗口的示例技术,我用来重现生产问题以及在发布前主动识别可疑代码。请注意,竞争条件是一个时间问题,因此即使对测试进行尽职调查也可能无法发现罕见的情况。

--example setup script with race condition vulnerability
USE YourDatabase;
DROP TABLE dbo.Transactions;
DROP TABLE dbo.Item;
CREATE TABLE dbo.Item(
      ItemNumber int NOT NULL
          CONSTRAINT PK_Item PRIMARY KEY 
    , RemainingQTY int NOT NULL
    );
CREATE TABLE dbo.Transactions(
      TransactionID int NOT NULL IDENTITY
        CONSTRAINT PK_Traansactions PRIMARY KEY 
    , ItemNumber int NOT NULL
          CONSTRAINT FK_Traansactions_Item FOREIGN KEY REFERENCES dbo.Item(ItemNumber)
          INDEX idx_Transactions_ItemNumber
    , TransactionType varchar(10) NOT NULL 
        CONSTRAINT CK_Transactions_TransactionType CHECK (TransactionType IN('Purchase', 'Sale'))
    , QTY int
);
--load 5000 items
WITH 
     t10 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
    ,t10k AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS num  FROM t10 AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c CROSS JOIN t10 AS d)
INSERT INTO dbo.Item(ItemNumber, RemainingQTY)
SELECT num, 1000
FROM t10k
WHERE num <= 5000;
--load 5000 purchase transactions
WITH 
     t10 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
    ,t10k AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS num  FROM t10 AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c CROSS JOIN t10 AS d)
INSERT INTO dbo.Transactions(ItemNumber, TransactionType, QTY)
SELECT ItemNumber, 'Purchase', 1000
FROM dbo.Item;
GO

CREATE OR ALTER PROC dbo.usp_InsertTransaction
      @ItemNumber int
    , @TransactionType varchar(10)
    , @QTY int
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @RemainingQTY int;
BEGIN TRY

    BEGIN TRAN;

    SELECT @RemainingQTY = RemainingQTY
    FROM dbo.Item
    WHERE ItemNumber = @ItemNumber;

    IF @TransactionType = 'Purchase' 
        SET @RemainingQTY = @RemainingQTY + @QTY
    ELSE
        SET @RemainingQTY = @RemainingQTY - @QTY;

    UPDATE dbo.Item
    SET RemainingQTY = @RemainingQTY
    WHERE ItemNumber = @ItemNumber;

    INSERT INTO dbo.Transactions VALUES
        (@ItemNumber, @TransactionType, @QTY);

    COMMIT;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK;
    THROW;
END CATCH
GO
Run Code Online (Sandbox Code Playgroud)

打开 3 个 SSMS 查询窗口并按照说明运行这些脚本:

--Step 1: Run script in SSMS query window 1 to acquire exclusive lock to sync start of test executions
DECLARE @return_code int;
EXEC @return_code = sp_getapplock
     @Resource = 'concurrency_test'
    ,@LockMode = 'exclusive'
    ,@LockOwner = 'session';
RAISERROR('sp_getapplock return code is %d', 0, 0, @return_code) WITH NOWAIT;
GO

--Step 2: Run this script in SSMS query windows 2 and 3 to acquire a shared lock on same resource as session 1.
--These will block until session 1 lock is released to start test.
DECLARE @return_code int;
EXEC @return_code = sp_getapplock
     @Resource = 'concurrency_test'
    ,@LockMode = 'shared'
    ,@LockOwner = 'session';
RAISERROR('sp_getapplock return code is %d', 0, 0, @return_code) WITH NOWAIT;
GO
--execute queries to insert transactions and update RemainingQTY 100 times
DECLARE @IterationCount int = 0;
WHILE @IterationCount < 100
BEGIN
    EXEC dbo.usp_InsertTransaction
          @ItemNumber = 1000
        , @TransactionType = 'Sale'
        , @QTY = 1;
    SET @IterationCount +=  1;
END;
GO
--release lock after test completes
DECLARE @return_code int;
EXEC @return_code = sp_releaseapplock
     @Resource = 'concurrency_test'
    ,@LockOwner = 'session';
RAISERROR('sp_releaseapplock return code is %d', 0, 0, @return_code) WITH NOWAIT;
GO

--Step 3: Run script in SSMS query window 1 to release lock to start test
DECLARE @return_code int;
EXEC @return_code = sp_releaseapplock
     @Resource = 'concurrency_test'
    ,@LockOwner = 'session';
RAISERROR('sp_releaseapplock return code is %d', 0, 0, @return_code) WITH NOWAIT;
GO

--Step 4: Run this script to validate RemainingQTY after test completes
WITH transaction_summary AS (
    SELECT
          t.ItemNumber
        , SUM(CASE WHEN t.TransactionType = 'Purchase' THEN t.QTY END) AS Purchase
        , SUM(CASE WHEN t.TransactionType = 'Sale' THEN t.QTY END) AS Sale
    FROM dbo.Transactions AS t
    WHERE t.ItemNumber = 1000
    GROUP BY t.ItemNumber
)
SELECT 
      i.ItemNumber
    , i.RemainingQTY AS ItemRemainingQTY
    , t.Purchase
    , t.Sale
    , t.Purchase - t.Sale AS ActualTramsactionsRemainingQTY
FROM dbo.Item AS i
JOIN transaction_summary AS t
    ON t.ItemNumber = i.ItemNumber
WHERE i.RemainingQTY <> t.Purchase - t.Sale;
GO
Run Code Online (Sandbox Code Playgroud)

下面是在我的盒子上进行测试后第 4 步验证查询的输出,显示项目 RemainingQTY 无效。这在我的测试机器上发生了 200 次总 proc 调用中的 31 次。原因是不同的会话SELECT几乎同时执行查询并读取相同的 RemainingQTY 值。一个会话根据需要减少了 RemainingQTY,但该值随后被另一个会话基于陈旧数据覆盖。此测试在紧密循环中运行,因此与常见的生产工作负载相比,它更有可能发生。

项目编号 物品剩余数量 购买 销售 实际交易剩余数量
1000 831 1000 200 800

修复此代码错误的一种方法是重构 proc 以避免使用局部变量。这将序列化对项目行的更新,并通过消除SELECT查询执行得稍微好一点:

CREATE OR ALTER PROC dbo.usp_InsertTransaction
      @ItemNumber int
    , @TransactionType varchar(10)
    , @QTY int
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @RemainingQTY int;
BEGIN TRY

    BEGIN TRAN;

    UPDATE dbo.Item
    SET RemainingQTY = RemainingQTY +
        CASE @TransactionType WHEN 'Purchase' THEN @RemainingQTY ELSE @RemainingQTY * -1 END
    WHERE ItemNumber = @ItemNumber;

    INSERT INTO dbo.Transactions VALUES
        (@ItemNumber, @TransactionType, @QTY);

    COMMIT;

END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK;
    THROW;
END CATCH
GO
Run Code Online (Sandbox Code Playgroud)


Dav*_*ett 5

是否可以在 SQL Server 实例停止时编辑数据文件:当然,页面布局和格式已被充分记录。虽然听起来不太可能,但如果有人故意这样做,他们可能会使用更简单的方法来影响数据。

是否有可能以这种方式影响备份:是的。普通备份本质上是数据库文件中使用过的页面的副本。但同样不太可能。

更有可能的是,在我们之间通过网络传输的过程中,磁盘或内存中发生了位翻转。这些是十亿分之一(警告:数字是从无到有的,尽管我相信你会发现对这种可靠性的研究你想要一个实数)但考虑到我们在我们的机器中读写位的次数所有人都会偶尔受到他们的影响。这就是为什么存在 ECC RAM 之类的东西。

假设您的数据库不够古老(在 2005 之前的 SQL Server 版本中创建)默认情况下没有打开页面校验和,并且您没有明确关闭该功能,它会在每个页面上保留一个校验和值,可以用于检测以这种方式引起的多种形式的损坏。您是否运行完整DBCC CHECKDB(请参阅https://docs.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-checkdb-transact-sql)以验证您没有损坏这样检测到?

正如 Dan 在他的回答中所讨论的那样,如果事务没有被正确使用(根本没有使用,或者在隔离级别设置不足的情况下使用),那么竞争条件可能是导致问题的原因。

  • @lakshithadilhan - 如果有人有权修改数据库或备份文件,那么他们已经获得了对文件系统的访问权限,写访问权限不少。这在许多情况下可能意味着他们具有管理员级别的访问权限,因此可以比手动编辑数据库/备份更容易造成*很多*的损害。这不是特定于 SQL Server 的,它可能是操作系统或网络级别的违规行为,因此请研究最佳实践服务器强化技术以大大降低风险。 (2认同)