Every identity-compromise runbook I have written, read or inherited has the same step near the top: revoke the tokens. Reset the password, kill the sessions, invalidate the refresh tokens, then go hunting. It is the right instinct. Against adversary-in-the-middle phishing, where the whole prize is a stolen session cookie, revocation is the move that ends the incident. Then I spent a few days taking apart a backdoor where that step buys you nothing at all. The reason sits in one function, and it is the last place most of us would think to look. The sample is GraphWorm, a custom implant tied to the China-nexus APT group Webworm. One thing I found there rewrote a line in my own incident response procedure, and I think it belongs in yours. The C2 channel is somebody’s OneDrive GraphWorm has no C2 domain, no beacon to a rented VPS, no hardcoded address to block. It authenticates to Microsoft Graph as an OAuth application and uses a OneDrive account as a dead drop. The layout is simple enough to sketch on a napkin. The operator writes an encrypted task file into a job folder. The implant polls that folder, runs the task, then uploads the encrypted result into a result folder. A heartbeat file gets a fresh timestamp every interval and a fingerprint file carries the victim’s system details. The command set covers shell execution, file upload and download, sleep, kill and a key exchange pair that upgrades the deployment key to per-session keys. All of it rides graph.microsoft.com over TLS, from a host that is already talking to Microsoft 365 all day. There is no unusual destination for a firewall to flag, no freshly registered domain for a reputation engine to penalize, no odd port. The network layer has nothing to say. That is the design goal, and it is the same shift I have written about before, where operators stop building their own infrastructure and start running on somebody else’s. The credentials are all sitting in the binary in cleartext: a client ID, a client secret, a tenant ID and a refresh token more than 1,300 characters long. The operator also shipped a debug build with the full PDB path intact. One more detail matters for anyone planning a response. The implant does not identify its victims by hostname or address. It builds an identifier by hashing the network adapter’s hardware address together with CPU and disk serial numbers pulled through WMI. Rename the machine, move it to a different subnet or put it behind a new egress address, and the operator still recognizes it as the same box. Any containment plan that leans on changing the victim’s network identity is solving a problem this implant does not have. So far this is a good story but a familiar one. Cloud-hosted C2 is not new. The part I did not expect was in the tasking handler. The function that breaks the runbook Among the commands the implant accepts is one called upgrade. The flow is short, and it is worth walking through. The handler parses a configuration blob out of the incoming task. It then destroys all five credential strings held in the beacon object, copies five new ones in, rebuilds the OAuth scope structure, tests a connection against the new OneDrive account, writes the replacement configuration to disk and swaps the live API instance. One task. Entire identity replaced. Sit with the consequence for a second. Your investigation reaches the point where you have identified the application, you file with the platform and the tokens get revoked. The implant’s next poll fails. If the operator is watching, and an operator writing a heartbeat file every interval is watching, they queue an upgrade task pointing at a second OneDrive account they registered months ago. The implant collects it, rotates and resumes. Nothing on the endpoint changed. No new binary, no new persistence mechanism, no new process. Same file on disk, different identity behind it. Revocation removed a credential. It did not remove access. This does not appear in the public reporting on the family, which is not a criticism. Vendor writeups are scoped to the campaign they observed, and this is the kind of detail you only see with the decompiler open on a function nobody had a reason to prioritize. I confirmed it twice before I was willing to write it down, once from the extracted strings and once from the decompiled function itself. The credential field offsets I recovered from the strings had to line up with the offsets the constructor actually reads from, and they did, which is the difference between a finding and a guess. What I changed afterward Three things came out of this that I would put into an identity response procedure tomorrow. Treat revocation as a delay rather than an eviction whenever the C2 rides an application identity. The durable object is the registration, not the tokens it issues. Tokens are leaves, the registration is the root, and containment that stops at revoking has started a timer rather than closed a door. If the registration lives in
Revoking the token didn’t kill the backdoor
Every identity-compromise runbook I have written, read or inherited has the same step near the top: revoke the tokens. Reset the password, kill the sessions, invalidate the refresh tokens, then go hunting. It is the right instinct. Against adversary-in-the-middle phishing, where the whole prize is…
Source: CSO Online