{"slug": "first-action-after-compromise-blind-the-audit", "title": "First Action After Compromise: Blind the Audit", "summary": "A developer detailed how attackers can blind AWS audit trails by calling `cloudtrail stop-logging`, which halts event delivery while leaving the trail's configuration looking healthy. The post highlights that detection requires checking `get-trail-status` for `IsLogging: false`, and recommends monitoring this field as a first-class asset property to close the audit blind window.", "body_md": "✓ Human-authored analysis; AI used for formatting and proofreading.\n\nIf you're an attacker who's just landed in an AWS account, the most expensive thing about your future is detection. Every API call you make leaves a trail in CloudTrail, gets forwarded to a SIEM, becomes evidence the team uses to boot you out and reconstruct what you touched. The cost of your campaign rises linearly with the number of events on record.\n\nSo you make one call first:\n\n```\naws cloudtrail stop-logging --name org-audit-trail\n```\n\nThe trail still exists. The console still shows it. `aws cloudtrail describe-trails`\n\nstill returns it. The S3 bucket is still configured as the destination. To anyone glancing at the configuration, the audit infrastructure looks healthy.\n\nOnly `aws cloudtrail get-trail-status`\n\nreveals that `IsLogging`\n\nis `false`\n\n. New events stop landing in the bucket. The audit clock pauses. Whatever the attacker does next happens unobserved, and remains unobserved until somebody notices.\n\nThis pattern is established enough to have its own MITRE ATT&CK identifier: **T1562.008** (*Disable or Modify Cloud Logs*). It appears in cloud incident write-ups across Mandiant's catalogue, AWS Security Bulletin guidance, and pen-test reports against organisations whose detection budget went into SIEM and not into CloudTrail-config monitoring.\n\nA stopped trail is structurally healthy. Every other configuration field is normal:\n\n```\n{\n  \"Name\": \"org-audit-trail\",\n  \"S3BucketName\": \"acme-cloudtrail-archive\",\n  \"IsMultiRegionTrail\": true,\n  \"IsOrganizationTrail\": true,\n  \"EventSelectors\": [...],\n  \"InsightSelectors\": [],\n  \"KmsKeyId\": \"arn:aws:kms:us-east-1:...\",\n  \"LogFileValidationEnabled\": true\n}\n```\n\nThe only field that flips is the one `get-trail-status`\n\nreturns separately:\n\n```\n{\n  \"IsLogging\": false,\n  \"LatestDeliveryTime\": \"2026-01-01T00:00:00Z\",\n  \"StartLoggingTime\": \"2025-09-15T00:00:00Z\",\n  \"StopLoggingTime\": \"2026-01-01T00:00:00Z\"\n}\n```\n\nTwo timestamps and a boolean. That's the entire signal. Detection requires reading the right API — `GetTrail`\n\nand `DescribeTrails`\n\nboth return the configuration but not the running state.\n\nThe audit blind window is the difference between a 30-day incident response and a 6-week one. Concrete things the attacker does inside that window:\n\n`AdministratorAccess`\n\n, an access key for offline use, a Lambda function with a privileged execution role that the attacker invokes via API Gateway.None of these leave a CloudTrail event. All of them fail audit review *because there is no audit*. The investigator's first question: \"what did this principal do between Jan 1 and Jan 8?\" has no answer.\n\nOperators who built the CloudTrail configuration usually checked their work. The bucket is encrypted, the trail is multi-region, log file validation is on, the bucket policy denies external principals. Every static configuration field passes audit review.\n\nThe mistake is treating \"trail exists and is configured\" as equivalent to \"trail is running.\" They are not the same. The IAM permission that creates the trail (`cloudtrail:CreateTrail`\n\n) is different from the permission that toggles its state (`cloudtrail:StopLogging`\n\n, `cloudtrail:StartLogging`\n\n). Roles permitted to do one routinely have permission to do the other — incident-response runbooks frequently include \"start logging if it stopped\" without restricting the inverse.\n\nEvery CloudTrail trail must have`IsLogging: true`\n\n,\n\nand at least one trail in the account must be\n\nmulti-region.\n\nIn Stave's observation schema, the trail's running state is a first-class field on the asset:\n\n```\n{\n  \"id\": \"arn:aws:cloudtrail:us-east-1:111122223333:trail/org-audit-trail\",\n  \"type\": \"aws_cloudtrail_trail\",\n  \"properties\": {\n    \"trail\": {\n      \"name\": \"org-audit-trail\",\n      \"is_logging\": false,\n      \"is_multi_region_trail\": true,\n      \"stopped_at\": \"2026-01-01T00:00:00Z\",\n      \"stopped_by\": \"arn:aws:sts::111122223333:assumed-role/incident-actor/session\"\n    }\n  }\n}\n```\n\nThe collector that produces this observation calls `GetTrailStatus`\n\nfor every trail, picks up the `IsLogging`\n\nboolean, and surfaces it directly. The diagnostic fields (`stopped_at`\n\n, `stopped_by`\n\n) carry forensic context the responder uses next; the predicate only reads the booleans.\n\n```\nid: CTL.CLOUDTRAIL.STOP.DETECT.001\nname: CloudTrail Trails Must Be Actively Logging in All Regions\nseverity: critical\nunsafe_predicate:\n  any:\n    - field: properties.trail.is_logging\n      op: eq\n      value: false\n    - field: properties.trail.is_multi_region_trail\n      op: eq\n      value: false\n```\n\n`any`\n\n— either the trail is stopped, or the trail is single-region (which leaves the other regions blind even when the configured one is running). Severity is `critical`\n\nbecause the trail being stopped is the configuration that determines whether *every other finding* in the account is investigable; the impact multiplies across the rest of the security posture.\n\nThis is a **presence check**, not a reachability question. The collector observes two booleans; the predicate is a two-leaf disjunction. There is no search space. There is no witness to enumerate.\n\nZ3 earns its complexity when the question is \"what specific request does this configuration admit?\" — that's reachability over a permission set. The question here is \"is the audit infrastructure running?\" — a flat read on a status field.\n\nA self-contained example is in the repo: `stave/examples/cloudtrail-stop-logging/`\n\n:\n\n```\ngo run ./examples/cloudtrail-stop-logging before\n```\n\nCaptured stdout:\n\n```\n=== before (trail stopped) ===\n  status: NON_COMPLIANT   total_assets=1   violations=1\n  CTL.CLOUDTRAIL.STOP.DETECT.001 fired on 1 asset(s):\n    - arn:aws:cloudtrail:us-east-1:111122223333:trail/org-audit-trail   severity=critical   exposure_score=100.00\n  assertion: fires=true (expected) ✓\n```\n\nAfter `aws cloudtrail start-logging`\n\nruns:\n\n```\n=== after  (trail re-started) ===\n  status: COMPLIANT   total_assets=1   violations=0\n  CTL.CLOUDTRAIL.STOP.DETECT.001: no findings\n  assertion: fires=false (expected) ✓\n```\n\nThe before-fixture carries `stopped_by`\n\n— `assumed-role/incident-actor/session`\n\n— which is the forensic breadcrumb the responder uses next. CloudTrail itself recorded the `StopLogging`\n\nevent before stopping; EventBridge can route that event to the SOC even after the trail goes silent, so the *first* unauthorised stop is detectable. The risk is detection latency. A six-hour latency is enough window for a determined attacker.\n\nThe remediation is a single command:\n\n```\naws cloudtrail start-logging --name org-audit-trail\n```\n\nA follow-up question every IR runbook should answer: *what was logged in the audit-blind window?* The answer is \"nothing through this trail.\" Other data sources fill in the gap to varying degrees:\n\nNone of these are a substitute for the trail. Together they shorten the audit-blind window's effective length.\n\nThree layers, in order of leverage:\n\n**Service Control Policy denying StopLogging for any non-bootstrap principal.** The strongest layer:\n\n```\n{\n  \"Sid\": \"DenyCloudTrailStopLogging\",\n  \"Effect\": \"Deny\",\n  \"Action\": [\n    \"cloudtrail:StopLogging\",\n    \"cloudtrail:DeleteTrail\",\n    \"cloudtrail:UpdateTrail\",\n    \"cloudtrail:PutEventSelectors\"\n  ],\n  \"Resource\": \"*\",\n  \"Condition\": {\n    \"ArnNotLike\": {\n      \"aws:PrincipalArn\": [\n        \"arn:aws:iam::*:role/CloudTrailAdministrator\"\n      ]\n    }\n  }\n}\n```\n\nThe deny applies to every principal except the explicit admin role. An attacker who lands on a compromised Lambda or compromised user has no path to `StopLogging`\n\n; the SCP rejects the call before it reaches CloudTrail's API.\n\n**EventBridge alarm on StopLogging events.** The trail records the\n\n`StopLogging`\n\nevent before going silent. Route that specific event to the SOC channel with a high-priority page:\n\n```\n{\n  \"source\": [\"aws.cloudtrail\"],\n  \"detail-type\": [\"AWS API Call via CloudTrail\"],\n  \"detail\": {\n    \"eventSource\": [\"cloudtrail.amazonaws.com\"],\n    \"eventName\": [\"StopLogging\", \"DeleteTrail\"]\n  }\n}\n```\n\nThe alarm catches the *first* unauthorised stop. Every moment the attacker waits is a moment the page rings.\n\n** stave apply in CI** against the post-deploy observation snapshot. The example shipped with this article is the template. PRs that cause a trail's\n\n`is_logging`\n\nto go false produce exit code 3.`cloudtrail:StopLogging`\n\n,\n`cloudtrail:DeleteTrail`\n\n,\n`cloudtrail:UpdateTrail`\n\n,\n`cloudtrail:PutEventSelectors`\n\nfor any non-bootstrap principal`StopLogging`\n\n, `DeleteTrail`\n\nevents from the management trail and pages the on-call SOC`stave apply`\n\nruns in CI against post-deploy observations; PRs that introduce a stopped trail failThe attacker's first action is `StopLogging`\n\n. The defender's first action should be making `StopLogging`\n\nhard to call without paging the on-call. The window between the call and the page is the budget the attacker has to spend; closing the window to seconds is how the rest of the security posture stays investigable.\n\nThe control above asks one question: *is the trail running?* It's a presence check on `is_logging`\n\nand `is_multi_region_trail`\n\n. The CEL predicate is two booleans ANDed together. That's the headline-grabbing case where the attacker who calls `StopLogging`\n\nto evade detection.\n\nThe Bybit / Safe{WALLET} writeup (March 2025) makes a quieter point: \"modifying objects in an S3 bucket can often be hard to detect because, by default, CloudTrail logging for S3 object activity is disabled.\" That is a different gap. Management events log `CreateBucket`\n\n, `PutBucketPolicy`\n\n, `DeleteBucket`\n\n. Data events log `GetObject`\n\n, `PutObject`\n\n, `DeleteObject`\n\n. Most production trails enable management events and stop there — data events are high-volume, expensive, and opt-in.\n\nThe trail in `data-events-before/`\n\nis a normal production CloudTrail:\n\n```\ntrail_name: org-management-trail\nis_logging: true\nis_multi_region_trail: true\ninclude_global_service_events: true\nlog_file_validation_enabled: true\nevent_selectors:\n  - read_write_type: All\n    include_management_events: true\n    data_resources: []         # ← empty\n```\n\nFive booleans say \"logging is healthy.\" The compliance dashboard shows green. The CEL control above (`CTL.CLOUDTRAIL.STOP.DETECT.001`\n\n) reports no findings — because, by its definition, there is nothing wrong: `is_logging=true`\n\nand `is_multi_region_trail=true`\n\n. The check passes.\n\nBut the account contains three production buckets — `company-frontend-prod`\n\n(serves JavaScript via CloudFront), `customer-data-prod`\n\n(PII), and `financial-records`\n\n(financial data) — and none of them appear in any trail's `data_resources`\n\n. An attacker who modifies `app.js`\n\nin the frontend bucket leaves no CloudTrail record. The security team's dashboard stays green.\n\nThe earlier section \"Why Z3 Doesn't Help Here\" is honest about the original control: presence checks have no search space, no witnesses to enumerate. CEL is the right shape.\n\nThe data-events question is different. It asks: *is there a sensitive bucket whose object operations are not covered by any trail's data_resources?* That has the shape Z3 is built for — quantifier over buckets, predicate combining tag classification and trail coverage. The prover at\n\n`examples/cloudtrail-stop-logging/z3prove/`\n\nruns two queries:**Query 1 — coverage gap.** ∃ bucket *b*: *b* is sensitive ∧ no trail covers *b*.\n\n```\n=== data-events-before (mgmt logging on, data events off) ===\n  trails observed:  1\n    - org-management-trail   is_logging=true   data_resource_patterns=0\n  buckets observed: 4\n    - company-frontend-prod   environment=production   data_classification=public   sensitive=true\n    - customer-data-prod      environment=production   data_classification=pii      sensitive=true\n    - financial-records       environment=production   data_classification=financial sensitive=true\n    - company-frontend-dev    environment=development  data_classification=          sensitive=false\n\n  --- S3 Data Event Logging Gap ---\n  verdict: SAT\n  witness: arn:aws:s3:::company-frontend-prod\n           (sensitive=true, classification=public, environment=production,\n            data_event_coverage=NONE)\n  rationale: 3 of 3 sensitive buckets fall outside every trail's data_resources\n```\n\nThree of three sensitive buckets are uncovered. Z3 picks one as the witness. The prover doesn't care which; any unconvered sensitive bucket discharges the existential.\n\n**Query 2 — compound: write access without audit.** This is the conjunction that makes the gap matter: *write_admitted ∧ ¬covered*. The `write_admitted`\n\nhalf is discharged against an IAM policy with a prefix wildcard; the `¬covered`\n\nhalf is discharged against the trail's event selectors. The compound is the supply chain attack path Bybit took: modify production JavaScript, leave no CloudTrail record, $1.5B redirected.\n\nDon't enable data events on every bucket. That's cost-prohibitive and probably useless. Enable data events on the buckets that contain PII, serve production frontends, or hold financial records. The remediated fixture is a one-statement change:\n\n```\nevent_selectors:\n  - read_write_type: All\n    include_management_events: true\n    data_resources:\n      - type: AWS::S3::Object\n        values:\n          - arn:aws:s3:::company-frontend-prod/\n          - arn:aws:s3:::customer-data-prod/\n          - arn:aws:s3:::financial-records/\n```\n\nThree ARN prefixes. Z3 reports UNSAT on both queries:\n\n```\n=== data-events-after  (data events scoped to sensitive buckets) ===\n  --- S3 Data Event Logging Gap ---\n  verdict: UNSAT\n  rationale: every sensitive bucket is covered by at least one trail's data_resources\n\n  --- Compound: Write Access + No Audit Trail ---\n  verdict: UNSAT\n  rationale: no logging gap → compound trivially UNSAT\n```\n\nThe teaching beat: management events answer \"is logging running?\" Data events answer \"is logging *covering the buckets that matter?*\" The first question is one boolean. The second is a quantifier over a sensitivity-tagged bucket inventory and a coverage relation. Different question, different prover.\n\nThe CEL control above is correct as written. It catches the attacker who runs `StopLogging`\n\n. The data-event question is the one a heuristic scanner can't ask — it requires reasoning over which sensitive buckets fall outside which trail's `data_resources`\n\npatterns, across the join of two asset types. Z3 closes that gap.\n\n*The example at stave/examples/cloudtrail-stop-logging/ is a self-contained Go program that loads two fixture snapshots, runs pkg/stave.Apply, asserts that CTL.CLOUDTRAIL.STOP.DETECT.001 fires on the trail-stopped fixture and is silent on the running one, and exits zero when both assertions hold. Stave detects this pattern and 31 other H1-grounded scenarios from local AWS configuration snapshots, with no cloud credentials.*", "url": "https://wpnews.pro/news/first-action-after-compromise-blind-the-audit", "canonical_source": "https://dev.to/bala_paranj_059d338e44e7e/first-action-after-compromise-blind-the-audit-2fmm", "published_at": "2026-09-01 11:05:00+00:00", "updated_at": "2026-09-01 11:23:16.767468+00:00", "lang": "en", "topics": ["ai-safety", "ai-policy", "ai-ethics"], "entities": ["AWS", "CloudTrail", "MITRE ATT&CK", "Mandiant", "Stave"], "alternates": {"html": "https://wpnews.pro/news/first-action-after-compromise-blind-the-audit", "markdown": "https://wpnews.pro/news/first-action-after-compromise-blind-the-audit.md", "text": "https://wpnews.pro/news/first-action-after-compromise-blind-the-audit.txt", "jsonld": "https://wpnews.pro/news/first-action-after-compromise-blind-the-audit.jsonld"}}