Is it possible to edit data inside an unencrypted SQL Server backup file (*.bak) without SSMS?
In my experience, the root cause of these symptoms is most commonly a race condition in the application and/or T-SQL code rather than data corruption due to hardware, network, or malicious activity. Race conditions are insidious because the undesired outcome occurs by happenstance, difficult to reproduce, and may rarely occur.
One must use transactions, along with the appropriate isolation level (or locking hints), to ensure aggregated values like Remaining Qty in the items table reflects the related Purchase and Sales transactions.
EDIT
Testing concurrency problems is a challenge because multiple sessions are required and often many iterations too. Below is an example technique using multiple SSMS query windows I use to reproduce production issues as well as proactively identify suspect code before release. Be aware that race conditions are a matter of timing so even due diligence with testing may not catch rare occurrences.
--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 &lt;= 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 &gt; 0 ROLLBACK;
    THROW;
END CATCH
GO

Open 3 SSMS query windows and run these scripts as instructed:
--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 &lt; 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 &lt;&gt; t.Purchase - t.Sale;
GO

Below is the output of the step 4 validation query after a test on my box that shows item RemainingQTY is invalid. This happened 31 times of 200  total proc calls on my test machine. The cause is different sessions both executed the SELECT query at about the same time and read the same RemainingQTY value. One session reduced the RemainingQTY as desired but the value was subsequently overwritten by the other session based on stale data. This test ran in a tight loop so it's more likely to occur than in a common prod workload.




ItemNumber
ItemRemainingQTY
Purchase
Sale
ActualTramsactionsRemainingQTY




1000
831
1000
200
800




One way to fix this code bug is to refactor the proc to avoid the local variable. This will serialize updates to the item row and perform slightly better too by eliminating the SELECT query:
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 &gt; 0 ROLLBACK;
    THROW;
END CATCH
GO