Report-home: knowing when a protected .NET build is attacked in the field
Anti-tamper and anti-debug guards react on the user's machine — but you never find out they fired. Nebula.NET's report-home (tamperReportUrl, Pro) POSTs a minimal event to your own endpoint the moment a guard trips, so an attack on a shipped build becomes a signal you can act on. Here is how it works and how to build the receiver.
You harden a .NET build well. Control-flow flattening, string and method encryption, anti-tamper, anti-debug, environment checks — the works. You ship it. And then you hear nothing, which you tell yourself is good news. But “silence” and “nobody is attacking my software” are not the same thing. Every one of those guards fires on the customer’s machine, reacts there, and leaves no trace you ever see. A cracker spends a weekend stepping through your license check in a debugger, your anti-debug guard trips a dozen times, and you learn exactly none of it.
Nebula.NET’s report-home (tamperReportUrl, a Pro feature) closes that gap. When a guard fires, the protected app also POSTs a tiny event to an endpoint you run — so an attack on a build in the field becomes a signal on your side instead of a shrug. This post is a deep dive on what it sends, what it deliberately doesn’t, and how to build the receiver that turns those events into something you act on.
The blind spot in client-side protection
Be honest about what a runtime guard is. Anti-tamper computes a SHA-256 of the written image at load and reacts if the file was modified after obfuscation. Anti-debug asks the managed and native runtime whether a debugger is attached. The environment checks look for a CLR profiler (CORECLR_ENABLE_PROFILING) or the Harmony runtime patcher. Each is a genuinely useful tripwire — but a tripwire whose only output is a local reaction: terminate, throw, exit, or call a method you nominated.
That reaction protects the one machine it runs on. What it does not do is tell you anything. The guard and the attacker are alone in a room on someone else’s computer. You find out your software is being cracked when a keygen shows up on a forum, not when the first antiDebug guard trips.
What report-home actually does
Set one config key and every injected guard gains a second action: before it runs its reaction, it POSTs a minimal event to your URL.
{
"obfuscate": true,
"controlFlow": true,
"stringEncryption": true,
"antiTamper": true,
"antiDebug": true,
"environmentChecks": true,
"tamperReportUrl": "https://telemetry.example.com/nebula-events"
}
The body is deliberately tiny — just the event name:
{ "event": "antiTamper" }
Four properties define the behavior, and each one is a design decision worth understanding:
- It goes to your endpoint, never to us. The POST hits the URL you set in
tamperReportUrl. Delta1 Labs never receives it. This is your telemetry on your server. - Only the event name leaves the machine. No source, no assembly bytes, no file paths, no personal or device data — just which guard fired. There is nothing sensitive to intercept and nothing that would make a privacy review unhappy.
- It is bounded and self-swallowing. The send cannot block your app and cannot throw into it. If your endpoint is slow or unreachable, the report is dropped and the guard’s reaction proceeds unchanged. Report-home never becomes a new failure mode.
- It is separate from Nebula’s own analytics. Nebula has a build-time anonymous tool-usage channel you can silence with
NEBULA_NO_TELEMETRY. Report-home has nothing to do with it — it’s a runtime channel from your app to your server, controlled only by whethertamperReportUrlis set.
Because it adds outbound traffic from the protected app, report-home is explicitly not for air-gapped deployments. Leave the URL unset there and the guards still react locally — they just can’t phone home.
Which guards report, and what each one means
Every runtime guard Nebula injects reports under its own event name, which is what makes the signal useful rather than a single undifferentiated “something happened”:
| Event | Guard | What it usually means |
|---|---|---|
antiTamper | Load-time image integrity (SHA-256 of the written image) | The binary was modified after obfuscation — a patched/cracked copy, or a corrupted download |
antiDebug | Managed + native debugger detection | Someone attached a debugger — the classic first move of dynamic reverse engineering |
environment | CLR profiler / Harmony patcher detection | A profiler or the Harmony runtime patcher is present — automated hooking of your methods |
expiry | Expiry / time-bomb guard | A beta, evaluation or embargoed build is running past its expiryUtc |
Seeing antiDebug spike on a single build version tells you something very different from a steady trickle of antiTamper from one customer ID’s machines. One is active reverse engineering; the other might be a corrupted install or a redistribution ring. Distinguishing them is the whole point of giving each guard its own name.
Making the signal louder: continuous checks
Report-home is most useful paired with checks that run more than once. By default environmentChecks and antiDebug are load-time guards — they fire at startup. A patient attacker simply attaches after launch. Turning on continuous checks changes that:
{
"antiTamper": true,
"antiDebug": true,
"environmentChecks": true,
"environmentChecksContinuous": true,
"environmentChecksIntervalSeconds": 45,
"autoProtectLicensing": true,
"tamperReportUrl": "https://telemetry.example.com/nebula-events"
}
With environmentChecksContinuous, detection no longer runs only at startup. It subscribes to AppDomain.AssemblyLoad, so a profiler or patcher attaching after launch is caught the moment it loads; it re-checks at the entry of the methods that call your licensing SDK (the same ones autoProtectLicensing finds and protects); and with environmentChecksIntervalSeconds it re-checks on a jittered background timer. Each of those additional firings can report home — so instead of one event at startup you get a picture of an attack as it unfolds across a session.
Building the receiver
The endpoint is ordinary. It takes a small JSON POST and records it; everything interesting happens in how you aggregate, not in the handler. A minimal ASP.NET Core receiver:
app.MapPost("/nebula-events", async (HttpContext ctx, TamperEvent evt) =>
{
// Attribute the hit to a build/customer using signals you already have:
// the User-Agent you set, a header you add at your CDN/gateway, or the
// source address. The body itself carries only the event name by design.
var record = new TamperHit(
Event: evt.Event, // "antiTamper" | "antiDebug" | "environment" | "expiry"
AtUtc: DateTime.UtcNow,
Source: ctx.Connection.RemoteIpAddress?.ToString(),
Agent: ctx.Request.Headers.UserAgent.ToString());
await _hits.AppendAsync(record); // your store of choice
return Results.NoContent(); // keep it cheap; the client ignores the response anyway
});
record TamperEvent(string Event);
record TamperHit(string Event, DateTime AtUtc, string? Source, string? Agent);
Two things to keep in mind on the receiving side. First, the client ignores your response and never retries — it’s fire-and-forget by design — so your handler should be cheap and must never expect an acknowledgement protocol. Second, treat the body as untrusted input: it arrives from software running on a machine an attacker may control, so validate the event name against your known set, rate-limit per source, and never let an unexpected payload do anything but get logged and dropped. The value of report-home is in the aggregate — a jump in antiDebug after a release, a cluster of antiTamper from one region — not in trusting any single event.
What report-home is, and isn’t
Report-home does not stop a crack. The guards do the stopping — anti-tamper refuses to run a modified binary, anti-debug and the environment checks react to dynamic attacks — and none of that is absolute, because every client-side protection ultimately runs on a machine the attacker controls. What report-home changes is your visibility. Today an attack on a shipped build is invisible to you; with tamperReportUrl set, it’s a dated, named event on your own server, attributable to a build and often to a customer. That turns reverse-engineering from something you discover months later into something you watch happen — and respond to, by shipping a harder build, revoking a leaked license, or reaching out to a customer whose machines are lighting up.
Set it in one line, point it at an endpoint you control, and keep it unset for air-gapped targets. See configuration → anti-tamper & anti-debug for the full set of guard and reaction options, resisting deobfuscation for how the guards fit the larger hardening story, or the Nebula.NET product page for the whole picture.
Try Nebula.NET
Harden your .NET code in minutes — start with the free edition.