Response playbooks, investigation guides, and Atomic Red Team tests are Pro-only. Upgrade to unlock the full detection package for THREAT-Impact-FinancialLedgerTampering.
Upgrade to ProDetect Financial Ledger and Transaction Record Tampering via Direct Database Manipulation in Microsoft Sentinel
An insider with legitimate database credentials, or an attacker who has obtained them, can bypass an application's business logic entirely and manipulate financial data at the source: connecting directly to the ledger, invoice, or transaction database with an interactive client (SQL Server Management Studio, Azure Data Studio, DBeaver, psql, mysql CLI) instead of through the application's service account, then issuing UPDATE or DELETE statements against tables such as general_ledger, invoices, journal_entries, gl_entries, ap_transactions, or account_balances. This is functionally identical to APT38's DYEPACK tool altering SWIFT transaction records to conceal fraudulent transfers, and to the classic 'ghost employee' or 'check kiting' fraud pattern where a trusted insider edits posted transactions after the fact rather than through a reversing journal entry (which would leave the expected audit trail). Three behaviors distinguish malicious tampering from legitimate maintenance: (1) the write originates from an ad-hoc/interactive client tool or an identity other than the application's own connection pool account — legitimate corrections almost always flow back through the application, which posts compensating entries rather than editing history in place; (2) the activity clusters off-hours or on weekends, when fewer people are watching and reconciliation staff are not online; and (3) the write is preceded by disabling the database's own audit trail for that table or database — SQL Server 'ALTER SERVER AUDIT ... WITH (STATE = OFF)', temporal-table system-versioning disablement, change-tracking disablement, or a DISABLE TRIGGER on an audit trigger — which is a strong signal since audit trails exist specifically to survive this kind of tampering and disabling one immediately before writing to financial tables has essentially no legitimate business justification. This detection watches SQL platform audit logs (Azure SQL Auditing / SQL Server Audit, or equivalent extended-events output) for the write pattern, the audit-disablement pattern, and the two-step sequence where disablement precedes writes within a short window.
MITRE ATT&CK
- Tactic
- Impact
KQL Detection Query
// THREAT-Impact-FinancialLedgerTampering (T1565.001 Stored Data Manipulation - financial database)
let Lookback = 24h;
let BurstWindow = 15m;
let MinWriteCount = 5;
let AuditTamperWindow = 6h;
let BusinessAppNames = dynamic(["LedgerApp-Prod", "ERPService", "AccountingSvc", "BillingAPI", "GLPostingService"]);
let InteractiveClientTools = dynamic(["Microsoft SQL Server Management Studio", "Azure Data Studio", "DBeaver", "HeidiSQL", "psql", "pgAdmin", "DataGrip", "TablePlus", "mysql.exe", "sqlcmd"]);
let FinancialObjectPattern = @"(?i)(ledger|invoice|transaction|payment|journal_entr|gl_entr|account_balance|^ap_|^ar_|general_ledger)";
let AuditDisableStatementPattern = @"(?i)(DISABLE TRIGGER|SET CHANGE_TRACKING\s*=\s*OFF|ALTER SERVER AUDIT|SYSTEM_VERSIONING\s*=\s*OFF|IS_TEMPORAL_HISTORY_RETENTION)";
// Audit trail disablement events against the financial database platform
let AuditDisableEvents = AzureDiagnostics
| where TimeGenerated > ago(Lookback)
| where Category == "SQLSecurityAuditEvents"
| where action_name_s in ("AUDIT_CHANGE_GROUP", "SERVER_OBJECT_CHANGE_GROUP") or statement_s matches regex AuditDisableStatementPattern
| project TimeGenerated, DatabaseName = database_name_s, ServerPrincipal = server_principal_name_s, Statement = statement_s, ClientApp = application_name_s;
let AuditDisableByPrincipal = AuditDisableEvents
| summarize AuditDisabledAt = min(TimeGenerated) by ServerPrincipal, DatabaseName;
// Direct writes to financial tables
let FinancialWriteEvents = AzureDiagnostics
| where TimeGenerated > ago(Lookback)
| where Category == "SQLSecurityAuditEvents"
| where action_name_s in ("UPDATE", "DELETE")
| where object_name_s matches regex FinancialObjectPattern
| where succeeded_s == "true"
| extend HourOfDay = hourofday(TimeGenerated), DayOfWeek = dayofweek(TimeGenerated)
| extend IsOffHours = HourOfDay < 6 or HourOfDay >= 20 or DayOfWeek in (0d, 6d)
| extend IsInteractiveClient = application_name_s has_any (InteractiveClientTools) or not(application_name_s has_any (BusinessAppNames))
| project TimeGenerated, DatabaseName = database_name_s, ObjectName = object_name_s, ServerPrincipal = server_principal_name_s, ActionName = action_name_s, ClientApp = application_name_s, IsOffHours, IsInteractiveClient;
FinancialWriteEvents
| summarize
WriteCount = count(),
ObjectsTouched = dcount(ObjectName),
Objects = make_set(ObjectName, 20),
Actions = make_set(ActionName),
OffHoursCount = countif(IsOffHours),
InteractiveCount = countif(IsInteractiveClient),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by ServerPrincipal, DatabaseName, ClientApp, bin(TimeGenerated, BurstWindow)
| where InteractiveCount > 0 and (WriteCount >= MinWriteCount or OffHoursCount > 0)
| join kind=leftouter (AuditDisableByPrincipal) on ServerPrincipal, DatabaseName
| extend AuditTamperPrecursor = isnotempty(AuditDisabledAt) and AuditDisabledAt < FirstSeen and (FirstSeen - AuditDisabledAt) < AuditTamperWindow
| extend RiskScore = case(
AuditTamperPrecursor and InteractiveCount > 0, 95,
InteractiveCount > 0 and OffHoursCount > 0 and WriteCount >= 10, 85,
InteractiveCount > 0 and OffHoursCount > 0, 70,
InteractiveCount > 0 and WriteCount >= 10, 65,
50
)
| project FirstSeen, LastSeen, ServerPrincipal, DatabaseName, ClientApp, WriteCount, ObjectsTouched, Objects, Actions, OffHoursCount, InteractiveCount, AuditTamperPrecursor, AuditDisabledAt, RiskScore
| sort by RiskScore desc, WriteCount desc Detects direct manipulation of financial ledger, invoice, and transaction tables using Azure SQL / SQL Server Audit logs ingested into AzureDiagnostics (SQLSecurityAuditEvents category). Flags UPDATE/DELETE statements against financial-sounding tables issued by an interactive database client (SSMS, Azure Data Studio, DBeaver, psql, mysql CLI) or any identity other than the recognized application service accounts, grouping into 15-minute bursts per principal/database. Off-hours activity and burst volume raise the risk score, and the query separately tracks audit-trail disablement events (audit change group, disabled triggers, disabled change tracking or system-versioning) and joins them against the write summary — a disablement within 6 hours prior to interactive financial writes by the same principal/database is scored as the highest-confidence indicator, since disabling an audit trail immediately before editing financial records has essentially no legitimate use case.
Data Sources
Required Tables
False Positives & Tuning
- Database administrators running approved data-correction scripts through SSMS or Azure Data Studio during an off-hours maintenance window with a documented change ticket
- A newly deployed application version connecting under a different application_name than the allowlisted BusinessAppNames until the allowlist is updated, causing legitimate app traffic to be flagged as 'interactive'
- Scheduled batch reconciliation or month-end close jobs that run overnight and issue bulk UPDATE statements against ledger/journal tables from an ETL or reporting service account not yet in the allowlist
- Database migration or schema-change maintenance windows that legitimately disable change tracking or temporal versioning before a bulk data load, then re-enable it afterward
- QA or staging environment testing where testers use interactive clients against a copy of the financial schema — verify DatabaseName is a production database before escalating
Other platforms for THREAT-Impact-FinancialLedgerTampering
Testing Methodology
Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.
- Test 1Disable SQL Server Audit Then Update Ledger Table
Expected signal: SQL Server Audit / Azure SQL Auditing records: an AUDIT_CHANGE_GROUP event for test_ledger_audit_spec followed within minutes by an UPDATE action_name event against object_name dbo.general_ledger, both attributed to the same server_principal_name with application_name 'sqlcmd'.
- Test 2Off-Hours Bulk Update of Invoice Table via Interactive Client
Expected signal: SQL Server Audit / Azure SQL Auditing records: 10 UPDATE action_name events against object_name dbo.invoices within a single 15-minute window, application_name 'Azure Data Studio', succeeded = true.
- Test 3Direct Deletion of Journal Entry Rows via psql
Expected signal: Database audit log (pgaudit or equivalent forwarded into the SQLSecurityAuditEvents-equivalent schema): DELETE action_name events against object_name journal_entries, application_name 'psql', server_principal_name dbadmin, succeeded = true.
References (6)
- https://attack.mitre.org/techniques/T1565/001/
- https://attack.mitre.org/techniques/T1565/
- https://attack.mitre.org/tactics/TA0040/
- https://learn.microsoft.com/en-us/azure/azure-sql/database/auditing-overview
- https://learn.microsoft.com/en-us/sql/relational-databases/security/auditing/sql-server-audit-database-engine
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1565.001/T1565.001.md
Unlock playbooks & atomic tests with Pro
Get the full detection package for THREAT-Impact-FinancialLedgerTampering — response playbook and atomic red team tests, plus investigation guidance and hunting queries.
df00tech Pro — £29/user/month
Related Detections
Tactic Hub
Detection Variants (1)
Different telemetry and tradecraft for the same technique — pick the one that matches the data you collect.