ai security

Detecting Model Extraction Attacks

Detect model stealing, model inversion, and membership inference performed through inference-API abuse by monitoring query patterns, applying output perturbation, and red-teaming your own model's extractability.

ai-securityinference-apimembership-inferencemitre-atlasmlsecopsmodel-extractionmodel-inversionquery-monitoring
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Authorized Use Only: The extraction, inversion, and membership-inference techniques described here are intended for defenders testing their own models and for red teams operating under written authorization. Querying a third-party model to clone it, reconstruct its training data, or infer membership without permission may violate terms of service, copyright, and privacy law.

Overview

Model extraction is the family of attacks in which an adversary abuses a model's inference API to steal value that the model owner intended to keep private. MITRE ATLAS catalogs these under AML.T0024 — Exfiltration via AI Inference API, in the Exfiltration tactic, with three sub-techniques:

  • AML.T0024.000 — Infer Training Data Membership (membership inference): the adversary determines whether a specific record was part of the training set, a privacy violation that can expose, for example, whether a patient's record trained a medical model.
  • AML.T0024.001 — Invert AI Model (model inversion): the adversary reconstructs representative training inputs (e.g., faces, text) by exploiting confidence scores returned by the API.
  • AML.T0024.002 — Extract ML Model (model stealing): the adversary repeatedly queries the victim model, collects (input, prediction) pairs, and trains a surrogate model offline that mimics the victim's decision boundary — avoiding the per-query cost of a Machine-Learning-as-a-Service offering and stealing the owner's intellectual property.

All three share a common signal: an attacker must send many queries, often crafted to probe the decision boundary (high-entropy, near-boundary, synthetic, or systematically grid-sampled inputs), and frequently requests full confidence vectors / logits rather than just the top label. Detection therefore centers on per-principal query monitoring, input-distribution analysis, and confidence-exposure controls, while defense centers on rate limiting, output perturbation, and reducing the information returned per query. This skill follows the MITRE ATLAS technique definition for AML.T0024 (https://atlas.mitre.org/techniques/AML.T0024) and the NIST AI RMF MEASURE function (MEASURE-2.6, security and resilience of the AI system).

When to Use

  • When you operate a model behind a public or partner inference API and need to detect cloning, inversion, or membership inference.
  • When performing a pre-deployment AI red-team exercise to measure how many queries are needed to extract your own model.
  • When validating that rate limiting, output perturbation, and confidence-suppression controls actually reduce extractability.
  • When investigating anomalous billing/usage spikes that may indicate surrogate-model harvesting.
  • When responding to a privacy incident where membership inference against a model is suspected.

Prerequisites

  • Python 3.9+ environment.
  • Access to inference-API access logs (per-API-key/per-principal query counts, timestamps, input features or hashes, returned confidence vectors).
  • For self-assessment red-teaming, install the Adversarial Robustness Toolbox (ART), the reference framework for extraction/inference attacks and defenses:
    pip install adversarial-robustness-toolbox scikit-learn numpy
  • Optional: access to the target model object (white/grey-box) or only its API (black-box).
  • Authorization to test the target model.

Objectives

  • Instrument the inference API to record per-principal query volume, input diversity, and confidence-exposure.
  • Build a detector that scores principals for extraction-like behavior (volume, near-boundary sampling, full-vector requests).
  • Run an ART-based extraction attack against your own model to measure fidelity vs. query budget.
  • Run a membership-inference attack to quantify training-data leakage.
  • Apply and validate defenses: rate limiting, label-only responses, confidence rounding/perturbation, and prediction poisoning.

MITRE ATT&CK Mapping

ID Name (MITRE ATLAS) Tactic
AML.T0024 Exfiltration via AI Inference API Exfiltration
AML.T0024.000 Infer Training Data Membership Exfiltration
AML.T0024.001 Invert AI Model Exfiltration
AML.T0024.002 Extract ML Model Exfiltration

Workflow

1. Instrument the inference API for detection signals

Capture the fields a detector needs. Per request, log the principal (API key / IP / account), timestamp, an input fingerprint, and whether the caller requested probabilities/logits.

import hashlib, json, time
 
def log_inference(principal, features, returned_probs):
    record = {
        "ts": time.time(),
        "principal": principal,
        # hash inputs so logs don't store raw sensitive data
        "input_hash": hashlib.sha256(json.dumps(features, sort_keys=True).encode()).hexdigest(),
        "wants_probs": returned_probs,
        "n_features": len(features),
    }
    with open("inference_audit.jsonl", "a") as f:
        f.write(json.dumps(record) + "\n")

2. Detect extraction-like query patterns

Score each principal on the three signals that distinguish extraction from normal use: high query volume in a window, high unique-input ratio (attackers rarely repeat), and a high rate of full-probability requests.

import collections, json
 
def score_principals(audit_path="inference_audit.jsonl", window_qps_threshold=100):
    by_principal = collections.defaultdict(lambda: {"q": 0, "uniq": set(), "probs": 0})
    for line in open(audit_path):
        r = json.loads(line)
        p = by_principal[r["principal"]]
        p["q"] += 1
        p["uniq"].add(r["input_hash"])
        p["probs"] += int(r["wants_probs"])
    findings = []
    for principal, p in by_principal.items():
        uniq_ratio = len(p["uniq"]) / max(p["q"], 1)
        prob_ratio = p["probs"] / max(p["q"], 1)
        suspicious = p["q"] > window_qps_threshold and uniq_ratio > 0.9 and prob_ratio > 0.8
        findings.append({"principal": principal, "queries": p["q"],
                         "unique_ratio": round(uniq_ratio, 3),
                         "prob_request_ratio": round(prob_ratio, 3),
                         "suspected_extraction": suspicious})
    return sorted(findings, key=lambda x: -x["queries"])

3. Measure your model's extractability with ART (self red-team)

Use ART's CopycatCNN (or KnockoffNets) to train a surrogate from black-box queries and report fidelity at a given query budget. Low query budget + high agreement = high risk.

import numpy as np
from art.estimators.classification import SklearnClassifier
from art.attacks.extraction import KnockoffNets
from sklearn.ensemble import RandomForestClassifier
 
# victim is your already-trained model wrapped for ART
victim = SklearnClassifier(model=trained_model)            # your production model
thief_model = RandomForestClassifier(n_estimators=100)
thief = SklearnClassifier(model=thief_model)
 
attack = KnockoffNets(classifier=victim, batch_size_fit=64,
                      batch_size_query=64, nb_epochs=10, nb_stolen=2000)
stolen = attack.extract(x=x_pool, thief_classifier=thief)   # 2000-query budget
 
agreement = np.mean(stolen.predict(x_test).argmax(1) == victim.predict(x_test).argmax(1))
print(f"Surrogate fidelity (agreement with victim): {agreement:.2%} at 2000 queries")

4. Quantify training-data leakage with membership inference

Run ART's black-box membership-inference attack. An accuracy meaningfully above 50% indicates the model leaks membership (AML.T0024.000).

from art.attacks.inference.membership_inference import MembershipInferenceBlackBox
 
mia = MembershipInferenceBlackBox(victim, attack_model_type="rf")
# fit the attack on a labeled split of known members / non-members
mia.fit(x_train[:500], y_train[:500], x_test[:500], y_test[:500])
member_pred = mia.infer(x_train[500:1000], y_train[500:1000])
nonmember_pred = mia.infer(x_test[500:1000], y_test[500:1000])
acc = (member_pred.mean() + (1 - nonmember_pred.mean())) / 2
print(f"Membership-inference accuracy: {acc:.2%} (0.50 = no leakage)")

5. Apply and validate defenses

Reduce the information returned and the query economics. Re-run steps 3 and 4 after each control to confirm extractability drops.

# (a) Label-only responses: never return full probability vectors to untrusted callers.
def respond(probs, trusted):
    return int(probs.argmax()) if not trusted else probs.tolist()
 
# (b) Confidence rounding / output perturbation (raises queries needed for inversion):
def perturb(probs, decimals=2, noise=0.01):
    p = np.round(probs, decimals) + np.random.normal(0, noise, probs.shape)
    p = np.clip(p, 0, None)
    return p / p.sum()

Defense in depth combines these with strict per-principal rate limiting, anomaly alerting from step 2, ART's ReverseSigmoid / prediction-poisoning postprocessor, and watermarking so an extracted surrogate remains attributable.

6. Alert and respond

Wire step-2 findings into your SIEM. On a confirmed extraction pattern: throttle or revoke the API key, switch the principal to label-only responses, preserve the audit log as evidence, and assess membership-inference exposure for any sensitive training data.

Tools and Resources

Resource Link
MITRE ATLAS AML.T0024 — Exfiltration via AI Inference API https://atlas.mitre.org/techniques/AML.T0024
Adversarial Robustness Toolbox (ART) https://github.com/Trusted-AI/adversarial-robustness-toolbox
ART extraction attacks (CopycatCNN, KnockoffNets) https://adversarial-robustness-toolbox.readthedocs.io/
MITRE ATLAS Matrix https://atlas.mitre.org/matrices/ATLAS
NIST AI RMF (MEASURE function) https://www.nist.gov/itl/ai-risk-management-framework

Detection Signal Reference

Signal Normal use Extraction behavior
Query volume per principal Bounded, bursty Very high, sustained
Unique-input ratio Repeats common inputs Near-1.0 (rarely repeats)
Confidence-vector requests Mostly top label Demands full probs/logits
Input distribution In-distribution Near-boundary / synthetic / grid
Inter-query timing Human-paced Automated, regular

Validation Criteria

  • Inference API logs per-principal query volume, input fingerprint, and confidence-exposure.
  • Detector scores principals and flags high-volume, high-unique-ratio, full-vector callers.
  • ART extraction attack run against own model; surrogate fidelity vs. query budget reported.
  • Membership-inference accuracy measured and compared against the 50% baseline.
  • Label-only / confidence-perturbation defenses applied and re-tested.
  • Per-principal rate limiting enforced and validated.
  • Alerts routed to SIEM with response playbook (throttle, revoke, preserve evidence).
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md2.3 KB

Model Extraction Detection — API / Library Reference

Libraries

Library Install Purpose
adversarial-robustness-toolbox pip install adversarial-robustness-toolbox Extraction, inversion, and membership-inference attacks + defenses
scikit-learn pip install scikit-learn Surrogate / attack model training
numpy pip install numpy Confidence-vector math, perturbation

ART Extraction Attacks (art.attacks.extraction)

Class Key params Purpose
KnockoffNets nb_stolen, batch_size_query, nb_epochs, sampling_strategy Train surrogate from black-box queries (Knockoff Nets)
CopycatCNN nb_stolen, batch_size_fit, batch_size_query Copycat surrogate extraction for neural nets
attack.extract(x, thief_classifier=...) Run extraction; returns trained surrogate classifier

ART Inference Attacks (art.attacks.inference.membership_inference)

Class Key methods Purpose
MembershipInferenceBlackBox .fit(...), .infer(x, y) Black-box membership inference (AML.T0024.000)
MembershipInferenceBlackBoxRuleBased .infer(x, y) Rule-based MIA baseline (no shadow training)

ART Defenses (postprocessors)

Class Purpose
art.defences.postprocessor.ReverseSigmoid Perturb output probabilities to hinder extraction
art.defences.postprocessor.Rounded Round confidence values to reduce leaked precision
art.defences.postprocessor.HighConfidence Suppress low-confidence outputs

Estimator Wrappers

Class Purpose
art.estimators.classification.SklearnClassifier Wrap a scikit-learn model as an ART victim
art.estimators.classification.KerasClassifier / PyTorchClassifier Wrap DL models

Detection Signals (custom)

Signal Heuristic
Query volume Queries/principal/window above baseline
Unique-input ratio unique(input_hash)/queries → ~1.0
Confidence-request ratio Fraction of calls demanding full probability vectors

External References

standards.md1.6 KB

Standards and References — Detecting Model Extraction Attacks

MITRE ATLAS Techniques

ID Name Tactic Rationale
AML.T0024 Exfiltration via AI Inference API Exfiltration Parent technique: abusing the inference API to steal model value or training data.
AML.T0024.000 Infer Training Data Membership Exfiltration Membership inference — determine if a record was in the training set (privacy leak).
AML.T0024.001 Invert AI Model Exfiltration Model inversion — reconstruct training inputs from confidence scores.
AML.T0024.002 Extract ML Model Exfiltration Model stealing — train a surrogate from query/response pairs to clone the model.

NIST AI RMF

ID Function Rationale
MEASURE-2.6 AI system security and resilience are evaluated and documented Extraction/inference testing measures and documents the model's resilience to inference-API abuse.

Official Resources

Key Research

  • Tramèr et al., "Stealing Machine Learning Models via Prediction APIs" (USENIX Security 2016)
  • Shokri et al., "Membership Inference Attacks Against Machine Learning Models" (IEEE S&P 2017)
  • Orekondy et al., "Knockoff Nets: Stealing Functionality of Black-Box Models" (CVPR 2019)

Scripts 1

agent.py5.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
# For authorized AI red-teaming and defense of models you own or are permitted to test.
# Cloning a third-party model or inferring its training data without consent may
# violate terms of service, copyright, and privacy law.
"""Model-extraction detection helper.

Two modes:
  detect   - Parse an inference-API audit log (JSONL) and flag principals whose
             query behaviour matches MITRE ATLAS AML.T0024 (model extraction /
             inference). Pure stdlib, no external model needed.
  extract  - Self red-team: train an ART surrogate against your own scikit-learn
             model and report fidelity vs. query budget (requires ART).

Audit log format (one JSON object per line):
  {"ts": 1700000000.0, "principal": "key-123", "input_hash": "ab..",
   "wants_probs": true, "n_features": 12}
"""
import argparse
import collections
import json
import sys


def load_audit(path):
    records = []
    with open(path, "r", encoding="utf-8") as fh:
        for ln, line in enumerate(fh, 1):
            line = line.strip()
            if not line:
                continue
            try:
                records.append(json.loads(line))
            except json.JSONDecodeError as exc:
                print(f"[!] skip malformed line {ln}: {exc}", file=sys.stderr)
    return records


def score_principals(records, q_threshold, uniq_threshold, prob_threshold):
    agg = collections.defaultdict(lambda: {"q": 0, "uniq": set(), "probs": 0})
    for r in records:
        principal = r.get("principal", "unknown")
        a = agg[principal]
        a["q"] += 1
        a["uniq"].add(r.get("input_hash", id(r)))
        a["probs"] += int(bool(r.get("wants_probs", False)))

    findings = []
    for principal, a in agg.items():
        q = a["q"]
        uniq_ratio = len(a["uniq"]) / q if q else 0.0
        prob_ratio = a["probs"] / q if q else 0.0
        suspected = (q >= q_threshold and uniq_ratio >= uniq_threshold
                     and prob_ratio >= prob_threshold)
        findings.append({
            "principal": principal,
            "queries": q,
            "unique_ratio": round(uniq_ratio, 3),
            "prob_request_ratio": round(prob_ratio, 3),
            "suspected_extraction": suspected,
        })
    return sorted(findings, key=lambda x: (-x["suspected_extraction"], -x["queries"]))


def cmd_detect(args):
    records = load_audit(args.audit)
    if not records:
        print("[!] no usable records in audit log", file=sys.stderr)
        return 1
    findings = score_principals(records, args.min_queries,
                                args.min_unique_ratio, args.min_prob_ratio)
    flagged = [f for f in findings if f["suspected_extraction"]]
    print(f"[+] analysed {len(records)} requests across {len(findings)} principals")
    print(f"[+] {len(flagged)} principal(s) match AML.T0024 extraction pattern\n")
    for f in findings:
        mark = "[ALERT]" if f["suspected_extraction"] else "       "
        print(f"{mark} {f['principal']:<24} q={f['queries']:<7} "
              f"uniq={f['unique_ratio']:<6} probs={f['prob_request_ratio']}")
    if args.output:
        with open(args.output, "w", encoding="utf-8") as fh:
            json.dump(findings, fh, indent=2)
        print(f"\n[+] findings written to {args.output}")
    return 0


def cmd_extract(args):
    try:
        import numpy as np
        from sklearn.datasets import load_iris
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.model_selection import train_test_split
        from art.estimators.classification import SklearnClassifier
        from art.attacks.extraction import KnockoffNets
    except ImportError:
        print("[!] install: pip install adversarial-robustness-toolbox scikit-learn numpy",
              file=sys.stderr)
        return 1

    # demo victim trained on a public dataset (replace with your production model)
    data = load_iris()
    x_tr, x_te, y_tr, y_te = train_test_split(data.data, data.target,
                                              test_size=0.4, random_state=42)
    victim_model = RandomForestClassifier(n_estimators=100, random_state=0).fit(x_tr, y_tr)
    victim = SklearnClassifier(model=victim_model)

    thief = SklearnClassifier(model=RandomForestClassifier(n_estimators=100))
    attack = KnockoffNets(classifier=victim, batch_size_fit=16, batch_size_query=16,
                          nb_epochs=5, nb_stolen=args.budget, sampling_strategy="random")
    stolen = attack.extract(x=x_te, thief_classifier=thief)

    agreement = float(np.mean(stolen.predict(x_te).argmax(1) ==
                              victim.predict(x_te).argmax(1)))
    print(f"[+] query budget         : {args.budget}")
    print(f"[+] surrogate fidelity   : {agreement:.2%} agreement with victim")
    risk = "HIGH" if agreement > 0.9 else "MEDIUM" if agreement > 0.7 else "LOW"
    print(f"[+] extractability risk  : {risk}")
    return 0


def main():
    p = argparse.ArgumentParser(description="Model-extraction detection / self red-team")
    sub = p.add_subparsers(dest="cmd", required=True)

    d = sub.add_parser("detect", help="flag extraction-like principals in an audit log")
    d.add_argument("--audit", required=True, help="path to JSONL inference audit log")
    d.add_argument("--min-queries", type=int, default=100)
    d.add_argument("--min-unique-ratio", type=float, default=0.9)
    d.add_argument("--min-prob-ratio", type=float, default=0.8)
    d.add_argument("--output", help="write findings JSON")
    d.set_defaults(func=cmd_detect)

    e = sub.add_parser("extract", help="ART self red-team on a demo model")
    e.add_argument("--budget", type=int, default=2000, help="query budget for surrogate")
    e.set_defaults(func=cmd_extract)

    args = p.parse_args()
    sys.exit(args.func(args))


if __name__ == "__main__":
    main()
Keep exploring