ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# CTF AI/ML - Model Attacks
|
|
2
|
+
|
|
3
|
+
Techniques for attacking ML models directly: weight manipulation, model inversion, encoder collision, LoRA adapter exploitation, model extraction, and membership inference. For adversarial example generation and data poisoning, see [adversarial-ml.md](adversarial-ml.md). For LLM-specific attacks, see [llm-attacks.md](llm-attacks.md).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [ML Model Weight Perturbation Negation (DiceCTF 2026)](#ml-model-weight-perturbation-negation-dicectf-2026)
|
|
7
|
+
- [ML Model Inversion via Gradient Descent (BSidesSF 2025)](#ml-model-inversion-via-gradient-descent-bsidessf-2025)
|
|
8
|
+
- [Neural Network Encoder Collision (RootAccess2026)](#neural-network-encoder-collision-rootaccess2026)
|
|
9
|
+
- [LoRA Adapter Weight Merging (ApoorvCTF 2026)](#lora-adapter-weight-merging-apoorvctf-2026)
|
|
10
|
+
- [Model Extraction via Query API](#model-extraction-via-query-api)
|
|
11
|
+
- [Membership Inference Attack](#membership-inference-attack)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ML Model Weight Perturbation Negation (DiceCTF 2026)
|
|
16
|
+
|
|
17
|
+
**Pattern:** A GPT-2 model has been fine-tuned to suppress a specific behavior (e.g., generating a flag). The challenge provides both the original base model and the fine-tuned (suppressed) model. By computing the weight delta and negating it, you reverse the suppression into amplification.
|
|
18
|
+
|
|
19
|
+
The key mathematical insight: if `W_chal = W_orig + delta` where `delta` was learned to suppress output, then `W_recovered = W_orig - delta = 2*W_orig - W_chal` amplifies the original behavior instead.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import torch
|
|
23
|
+
from transformers import GPT2LMHeadModel, GPT2Tokenizer
|
|
24
|
+
|
|
25
|
+
# Load both models
|
|
26
|
+
original = GPT2LMHeadModel.from_pretrained("gpt2")
|
|
27
|
+
challenge = GPT2LMHeadModel.from_pretrained("./challenge_model")
|
|
28
|
+
|
|
29
|
+
# Compute negated weights: W_recovered = 2*W_orig - W_chal
|
|
30
|
+
recovered = GPT2LMHeadModel.from_pretrained("gpt2")
|
|
31
|
+
orig_sd = original.state_dict()
|
|
32
|
+
chal_sd = challenge.state_dict()
|
|
33
|
+
rec_sd = recovered.state_dict()
|
|
34
|
+
|
|
35
|
+
for key in orig_sd:
|
|
36
|
+
rec_sd[key] = 2 * orig_sd[key] - chal_sd[key]
|
|
37
|
+
|
|
38
|
+
recovered.load_state_dict(rec_sd)
|
|
39
|
+
|
|
40
|
+
# Generate with recovered model
|
|
41
|
+
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
|
|
42
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
43
|
+
|
|
44
|
+
prompt = "The flag is"
|
|
45
|
+
inputs = tokenizer(prompt, return_tensors="pt")
|
|
46
|
+
with torch.no_grad():
|
|
47
|
+
output = recovered.generate(
|
|
48
|
+
**inputs,
|
|
49
|
+
max_new_tokens=100,
|
|
50
|
+
temperature=0.7,
|
|
51
|
+
do_sample=True,
|
|
52
|
+
num_return_sequences=5,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for seq in output:
|
|
56
|
+
print(tokenizer.decode(seq, skip_special_tokens=True))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Key insight:** Fine-tuning adds a delta to weights. Negating that delta reverses suppression into amplification. Check which layers differ most with `(orig_sd[k] - chal_sd[k]).abs().max()` to confirm fine-tuning targeted specific layers. If only certain layers changed, the delta is sparse and negation is even more effective.
|
|
60
|
+
|
|
61
|
+
**Variations:**
|
|
62
|
+
- **Partial negation with scaling:** Sometimes `W_orig + alpha * (W_orig - W_chal)` with `alpha > 1` works better than pure negation. Try `alpha` values from 1.0 to 3.0.
|
|
63
|
+
- **Layer-selective negation:** Only negate layers that show significant delta (threshold by L2 norm of difference).
|
|
64
|
+
- **LoRA-aware negation:** If fine-tuning used LoRA, the delta is low-rank. Extract and negate only the LoRA component.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## ML Model Inversion via Gradient Descent (BSidesSF 2025)
|
|
69
|
+
|
|
70
|
+
**Pattern:** Given a trained model and a target output (e.g., a specific class label or embedding vector), recover the input by optimizing a random input tensor using gradient descent to minimize the distance between the model's output and the target.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import torch
|
|
74
|
+
import torch.nn as nn
|
|
75
|
+
import torch.optim as optim
|
|
76
|
+
from torchvision import transforms
|
|
77
|
+
from PIL import Image
|
|
78
|
+
|
|
79
|
+
# Load the challenge model
|
|
80
|
+
model = torch.load("challenge_model.pt", map_location="cpu")
|
|
81
|
+
model.eval()
|
|
82
|
+
|
|
83
|
+
# Target: the output we want to invert (e.g., a specific embedding or class)
|
|
84
|
+
target_output = torch.load("target_embedding.pt") # shape depends on model
|
|
85
|
+
|
|
86
|
+
# Initialize random input (e.g., 3x224x224 image)
|
|
87
|
+
input_tensor = torch.randn(1, 3, 224, 224, requires_grad=True)
|
|
88
|
+
|
|
89
|
+
optimizer = optim.Adam([input_tensor], lr=0.01)
|
|
90
|
+
mse_loss = nn.MSELoss()
|
|
91
|
+
|
|
92
|
+
for step in range(2000):
|
|
93
|
+
optimizer.zero_grad()
|
|
94
|
+
output = model(input_tensor)
|
|
95
|
+
loss = mse_loss(output, target_output)
|
|
96
|
+
|
|
97
|
+
# Optional: add total variation regularization for smoother images
|
|
98
|
+
tv_loss = (
|
|
99
|
+
torch.sum(torch.abs(input_tensor[:, :, :, :-1] - input_tensor[:, :, :, 1:])) +
|
|
100
|
+
torch.sum(torch.abs(input_tensor[:, :, :-1, :] - input_tensor[:, :, 1:, :]))
|
|
101
|
+
)
|
|
102
|
+
total_loss = loss + 1e-4 * tv_loss
|
|
103
|
+
|
|
104
|
+
total_loss.backward()
|
|
105
|
+
optimizer.step()
|
|
106
|
+
|
|
107
|
+
# Clamp to valid image range
|
|
108
|
+
with torch.no_grad():
|
|
109
|
+
input_tensor.clamp_(0, 1)
|
|
110
|
+
|
|
111
|
+
if step % 200 == 0:
|
|
112
|
+
print(f"Step {step}: loss={loss.item():.6f}")
|
|
113
|
+
|
|
114
|
+
# Save recovered image
|
|
115
|
+
recovered = input_tensor.squeeze(0).detach()
|
|
116
|
+
img = transforms.ToPILImage()(recovered)
|
|
117
|
+
img.save("recovered_input.png")
|
|
118
|
+
print("Recovered input saved to recovered_input.png")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Key insight:** Neural networks are differentiable, so you can backpropagate through them to optimize the input. Total variation regularization produces more natural-looking images. If the model has batch normalization, set it to eval mode (`model.eval()`) to use running statistics rather than batch statistics.
|
|
122
|
+
|
|
123
|
+
**Variations:**
|
|
124
|
+
- **Feature visualization:** Maximize a specific neuron's activation instead of matching a target output.
|
|
125
|
+
- **Deep Dream style:** Use layer activations as optimization targets for artistic reconstruction.
|
|
126
|
+
- **Gradient-free inversion:** If gradients are unavailable (black-box), use CMA-ES or other evolutionary strategies.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Neural Network Encoder Collision (RootAccess2026)
|
|
131
|
+
|
|
132
|
+
**Pattern:** Given a neural network encoder, find two distinct inputs that produce identical (or nearly identical) output embeddings. This exploits the dimensionality reduction inherent in encoders — the mapping from high-dimensional input to lower-dimensional embedding space is not injective.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import torch
|
|
136
|
+
import torch.nn as nn
|
|
137
|
+
import torch.optim as optim
|
|
138
|
+
|
|
139
|
+
# Load the encoder model
|
|
140
|
+
encoder = torch.load("encoder.pt", map_location="cpu")
|
|
141
|
+
encoder.eval()
|
|
142
|
+
|
|
143
|
+
# Initialize two random inputs
|
|
144
|
+
input_a = torch.randn(1, 3, 64, 64, requires_grad=True)
|
|
145
|
+
input_b = torch.randn(1, 3, 64, 64, requires_grad=True)
|
|
146
|
+
|
|
147
|
+
optimizer = optim.Adam([input_a, input_b], lr=0.005)
|
|
148
|
+
|
|
149
|
+
for step in range(5000):
|
|
150
|
+
optimizer.zero_grad()
|
|
151
|
+
|
|
152
|
+
emb_a = encoder(input_a)
|
|
153
|
+
emb_b = encoder(input_b)
|
|
154
|
+
|
|
155
|
+
# Minimize distance between embeddings
|
|
156
|
+
collision_loss = nn.MSELoss()(emb_a, emb_b)
|
|
157
|
+
|
|
158
|
+
# Maximize distance between inputs (so they are distinct)
|
|
159
|
+
input_diff = nn.MSELoss()(input_a, input_b)
|
|
160
|
+
diversity_loss = -input_diff # negative because we want to maximize
|
|
161
|
+
|
|
162
|
+
# Regularize to valid range
|
|
163
|
+
range_penalty = (
|
|
164
|
+
torch.relu(-input_a).sum() + torch.relu(input_a - 1).sum() +
|
|
165
|
+
torch.relu(-input_b).sum() + torch.relu(input_b - 1).sum()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
loss = collision_loss + 0.1 * diversity_loss + 0.01 * range_penalty
|
|
169
|
+
loss.backward()
|
|
170
|
+
optimizer.step()
|
|
171
|
+
|
|
172
|
+
with torch.no_grad():
|
|
173
|
+
input_a.clamp_(0, 1)
|
|
174
|
+
input_b.clamp_(0, 1)
|
|
175
|
+
|
|
176
|
+
if step % 500 == 0:
|
|
177
|
+
dist = (emb_a - emb_b).norm().item()
|
|
178
|
+
inp_dist = (input_a - input_b).norm().item()
|
|
179
|
+
print(f"Step {step}: emb_dist={dist:.8f}, input_dist={inp_dist:.4f}")
|
|
180
|
+
|
|
181
|
+
# Verify collision
|
|
182
|
+
with torch.no_grad():
|
|
183
|
+
final_a = encoder(input_a)
|
|
184
|
+
final_b = encoder(input_b)
|
|
185
|
+
print(f"Final embedding distance: {(final_a - final_b).norm().item():.10f}")
|
|
186
|
+
print(f"Final input distance: {(input_a - input_b).norm().item():.4f}")
|
|
187
|
+
print(f"Embeddings equal: {torch.allclose(final_a, final_b, atol=1e-6)}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Key insight:** Encoders compress information, so collisions must exist by the pigeonhole principle. The optimization simultaneously minimizes embedding distance while maximizing input distance. Starting with very different random initializations helps avoid trivial solutions.
|
|
191
|
+
|
|
192
|
+
**Variations:**
|
|
193
|
+
- **Targeted collision:** Force both inputs to map to a specific target embedding.
|
|
194
|
+
- **Near-collision with hamming constraint:** Find inputs that differ in only a few pixels but produce identical embeddings.
|
|
195
|
+
- **Hash-like collision:** If the encoder output is discretized (e.g., binary hash), collision search is easier via relaxation.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## LoRA Adapter Weight Merging (ApoorvCTF 2026)
|
|
200
|
+
|
|
201
|
+
**Pattern:** A LoRA (Low-Rank Adaptation) adapter is provided alongside a base model. The adapter encodes hidden information in its low-rank weight matrices. Merging the adapter into the base model and generating output (or visualizing weight patterns) reveals the flag.
|
|
202
|
+
|
|
203
|
+
LoRA modifies weights as: `W_merged = W_base + alpha * (B @ A)` where A and B are the low-rank matrices and alpha is the scaling factor.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import torch
|
|
207
|
+
from safetensors import safe_open
|
|
208
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
209
|
+
|
|
210
|
+
# Load base model
|
|
211
|
+
base_model = AutoModelForCausalLM.from_pretrained("gpt2")
|
|
212
|
+
tokenizer = AutoTokenizer.from_pretrained("gpt2")
|
|
213
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
214
|
+
|
|
215
|
+
# Inspect LoRA adapter structure
|
|
216
|
+
adapter = safe_open("adapter_model.safetensors", framework="pt")
|
|
217
|
+
print("LoRA keys:", list(adapter.keys()))
|
|
218
|
+
# Typical keys: base_model.model.transformer.h.0.attn.c_attn.lora_A.weight
|
|
219
|
+
# base_model.model.transformer.h.0.attn.c_attn.lora_B.weight
|
|
220
|
+
|
|
221
|
+
# Manual merge: for each LoRA pair, compute W_merged = W_base + alpha * (B @ A)
|
|
222
|
+
alpha = 1.0 # Check adapter_config.json for lora_alpha and r values
|
|
223
|
+
# Effective alpha = lora_alpha / r
|
|
224
|
+
|
|
225
|
+
lora_a_keys = [k for k in adapter.keys() if "lora_A" in k]
|
|
226
|
+
lora_b_keys = [k for k in adapter.keys() if "lora_B" in k]
|
|
227
|
+
|
|
228
|
+
base_sd = base_model.state_dict()
|
|
229
|
+
|
|
230
|
+
for a_key in lora_a_keys:
|
|
231
|
+
b_key = a_key.replace("lora_A", "lora_B")
|
|
232
|
+
# Map LoRA key back to base model key
|
|
233
|
+
# e.g., "base_model.model.transformer.h.0.attn.c_attn.lora_A.weight"
|
|
234
|
+
# -> "transformer.h.0.attn.c_attn.weight"
|
|
235
|
+
base_key = a_key.replace("base_model.model.", "").replace(".lora_A.weight", ".weight")
|
|
236
|
+
|
|
237
|
+
A = adapter.get_tensor(a_key) # shape: (r, in_features)
|
|
238
|
+
B = adapter.get_tensor(b_key) # shape: (out_features, r)
|
|
239
|
+
|
|
240
|
+
delta = alpha * (B @ A) # shape: (out_features, in_features)
|
|
241
|
+
|
|
242
|
+
if base_key in base_sd:
|
|
243
|
+
base_sd[base_key] = base_sd[base_key] + delta
|
|
244
|
+
print(f"Merged {base_key}: delta norm = {delta.norm():.4f}")
|
|
245
|
+
|
|
246
|
+
base_model.load_state_dict(base_sd)
|
|
247
|
+
|
|
248
|
+
# Generate with merged model
|
|
249
|
+
prompt = "The secret is"
|
|
250
|
+
inputs = tokenizer(prompt, return_tensors="pt")
|
|
251
|
+
with torch.no_grad():
|
|
252
|
+
output = base_model.generate(
|
|
253
|
+
**inputs,
|
|
254
|
+
max_new_tokens=100,
|
|
255
|
+
temperature=0.7,
|
|
256
|
+
do_sample=True,
|
|
257
|
+
)
|
|
258
|
+
print(tokenizer.decode(output[0], skip_special_tokens=True))
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Alternative: using PEFT library for automatic merging:**
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from peft import PeftModel
|
|
265
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
266
|
+
|
|
267
|
+
base = AutoModelForCausalLM.from_pretrained("gpt2")
|
|
268
|
+
model = PeftModel.from_pretrained(base, "./lora_adapter_dir")
|
|
269
|
+
model = model.merge_and_unload() # Merge LoRA into base weights
|
|
270
|
+
|
|
271
|
+
tokenizer = AutoTokenizer.from_pretrained("gpt2")
|
|
272
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
273
|
+
inputs = tokenizer("The flag is", return_tensors="pt")
|
|
274
|
+
output = model.generate(**inputs, max_new_tokens=100)
|
|
275
|
+
print(tokenizer.decode(output[0], skip_special_tokens=True))
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Key insight:** LoRA adapters modify only a small subset of weights via low-rank matrices. The `adapter_config.json` file contains `lora_alpha`, `r` (rank), and `target_modules` which tell you exactly which layers were modified. Sometimes the hidden content is not in the model's text output but in the weight matrices themselves — try visualizing `B @ A` as an image.
|
|
279
|
+
|
|
280
|
+
**Variations:**
|
|
281
|
+
- **Weight visualization:** Reshape `B @ A` delta matrices and render as images; flags may be encoded visually in the weight pattern.
|
|
282
|
+
- **Multi-adapter stacking:** Multiple LoRA adapters applied sequentially; merge in correct order.
|
|
283
|
+
- **Quantized adapters:** QLoRA uses 4-bit quantization; dequantize before merging.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Model Extraction via Query API
|
|
288
|
+
|
|
289
|
+
**Pattern:** A challenge exposes a model via an API endpoint. By sending carefully crafted inputs and observing outputs (predictions, confidence scores, logits), you can reconstruct the model's parameters or decision boundary. This is especially effective against simple models (linear, decision tree, small neural networks).
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
import numpy as np
|
|
293
|
+
import requests
|
|
294
|
+
from sklearn.linear_model import LogisticRegression
|
|
295
|
+
|
|
296
|
+
API_URL = "http://challenge:8080/predict"
|
|
297
|
+
|
|
298
|
+
def query_model(x):
|
|
299
|
+
"""Send input to model API and get prediction/confidence."""
|
|
300
|
+
resp = requests.post(API_URL, json={"input": x.tolist()})
|
|
301
|
+
return resp.json() # e.g., {"class": 1, "confidence": 0.87}
|
|
302
|
+
|
|
303
|
+
# Strategy 1: Decision boundary mapping for 2D models
|
|
304
|
+
# Sample a grid of points to map the decision boundary
|
|
305
|
+
xs = np.linspace(-5, 5, 100)
|
|
306
|
+
ys = np.linspace(-5, 5, 100)
|
|
307
|
+
X_grid = np.array([[x, y] for x in xs for y in ys])
|
|
308
|
+
labels = []
|
|
309
|
+
confidences = []
|
|
310
|
+
|
|
311
|
+
for point in X_grid:
|
|
312
|
+
result = query_model(point)
|
|
313
|
+
labels.append(result["class"])
|
|
314
|
+
confidences.append(result["confidence"])
|
|
315
|
+
|
|
316
|
+
labels = np.array(labels)
|
|
317
|
+
confidences = np.array(confidences)
|
|
318
|
+
|
|
319
|
+
# Fit a surrogate model to the extracted labels
|
|
320
|
+
surrogate = LogisticRegression()
|
|
321
|
+
surrogate.fit(X_grid, labels)
|
|
322
|
+
print(f"Extracted weights: {surrogate.coef_}")
|
|
323
|
+
print(f"Extracted bias: {surrogate.intercept_}")
|
|
324
|
+
|
|
325
|
+
# Strategy 2: Exact weight extraction for linear models
|
|
326
|
+
# For a linear model f(x) = sigmoid(w*x + b), query with basis vectors
|
|
327
|
+
dim = 10 # input dimensionality
|
|
328
|
+
weights = np.zeros(dim)
|
|
329
|
+
# Query with zero vector to get bias term
|
|
330
|
+
base_result = query_model(np.zeros(dim))
|
|
331
|
+
base_logit = np.log(base_result["confidence"] / (1 - base_result["confidence"]))
|
|
332
|
+
|
|
333
|
+
for i in range(dim):
|
|
334
|
+
e_i = np.zeros(dim)
|
|
335
|
+
e_i[i] = 1.0
|
|
336
|
+
result = query_model(e_i)
|
|
337
|
+
logit = np.log(result["confidence"] / (1 - result["confidence"]))
|
|
338
|
+
weights[i] = logit - base_logit
|
|
339
|
+
|
|
340
|
+
print(f"Extracted weights: {weights}")
|
|
341
|
+
print(f"Extracted bias: {base_logit}")
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Key insight:** Linear models can be extracted exactly with `dim + 1` queries (one per basis vector plus zero vector). For neural networks, use model distillation: train a student network on input-output pairs from the API. Decision trees can be extracted by probing decision boundaries with binary search.
|
|
345
|
+
|
|
346
|
+
**Variations:**
|
|
347
|
+
- **Logit extraction:** If API returns only class labels (not confidence), use inputs near decision boundaries with binary search.
|
|
348
|
+
- **Functionally equivalent extraction:** Train a neural network on API responses; fidelity > 99% is often achievable with 10K-100K queries.
|
|
349
|
+
- **Side-channel extraction:** Timing differences in API responses can leak model architecture (deeper = slower).
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Membership Inference Attack
|
|
354
|
+
|
|
355
|
+
**Pattern:** Determine whether a specific data sample was part of the model's training set. Training data members typically produce higher confidence predictions and lower loss values than non-members, because the model has memorized them to some degree.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
import torch
|
|
359
|
+
import torch.nn.functional as F
|
|
360
|
+
import numpy as np
|
|
361
|
+
from sklearn.metrics import roc_auc_score
|
|
362
|
+
|
|
363
|
+
# Load challenge model
|
|
364
|
+
model = torch.load("target_model.pt", map_location="cpu")
|
|
365
|
+
model.eval()
|
|
366
|
+
|
|
367
|
+
def get_prediction_metrics(model, x, true_label):
|
|
368
|
+
"""Compute metrics that distinguish members from non-members."""
|
|
369
|
+
with torch.no_grad():
|
|
370
|
+
logits = model(x.unsqueeze(0))
|
|
371
|
+
probs = F.softmax(logits, dim=1)
|
|
372
|
+
confidence = probs[0, true_label].item()
|
|
373
|
+
loss = F.cross_entropy(logits, torch.tensor([true_label])).item()
|
|
374
|
+
entropy = -(probs * torch.log(probs + 1e-10)).sum().item()
|
|
375
|
+
return {
|
|
376
|
+
"confidence": confidence,
|
|
377
|
+
"loss": loss,
|
|
378
|
+
"entropy": entropy,
|
|
379
|
+
"top1_margin": (probs.max() - probs.topk(2).values[0, 1]).item(),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Method 1: Simple threshold attack
|
|
383
|
+
# Members typically have higher confidence and lower loss
|
|
384
|
+
def threshold_attack(metrics, threshold=0.9):
|
|
385
|
+
"""Predict membership based on confidence threshold."""
|
|
386
|
+
return metrics["confidence"] > threshold
|
|
387
|
+
|
|
388
|
+
# Method 2: Shadow model attack (more sophisticated)
|
|
389
|
+
# Train shadow models on known in/out splits to learn the membership signal
|
|
390
|
+
def shadow_model_attack(target_model, candidate_samples, candidate_labels):
|
|
391
|
+
"""Use multiple metrics to predict membership."""
|
|
392
|
+
results = []
|
|
393
|
+
for x, y in zip(candidate_samples, candidate_labels):
|
|
394
|
+
m = get_prediction_metrics(target_model, x, y)
|
|
395
|
+
# High confidence + low loss + low entropy = likely member
|
|
396
|
+
score = m["confidence"] - 0.5 * m["entropy"]
|
|
397
|
+
results.append({
|
|
398
|
+
"sample_label": y,
|
|
399
|
+
"member_score": score,
|
|
400
|
+
**m,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
# Sort by membership likelihood
|
|
404
|
+
results.sort(key=lambda r: r["member_score"], reverse=True)
|
|
405
|
+
return results
|
|
406
|
+
|
|
407
|
+
# Example usage
|
|
408
|
+
candidate = torch.randn(3, 224, 224) # single candidate image
|
|
409
|
+
label = 5 # true class
|
|
410
|
+
metrics = get_prediction_metrics(model, candidate, label)
|
|
411
|
+
print(f"Confidence: {metrics['confidence']:.4f}")
|
|
412
|
+
print(f"Loss: {metrics['loss']:.4f}")
|
|
413
|
+
print(f"Entropy: {metrics['entropy']:.4f}")
|
|
414
|
+
print(f"Likely member: {threshold_attack(metrics)}")
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Key insight:** Models overfit to training data, producing measurably different behavior on seen vs. unseen samples. The gap between training and test confidence is the core signal. More sophisticated attacks train a binary classifier on (metrics, member/non-member) pairs from shadow models trained on similar data distributions.
|
|
418
|
+
|
|
419
|
+
**Variations:**
|
|
420
|
+
- **Label-only attack:** When only the predicted class is returned (no confidence), use perturbation sensitivity: members are more robust to small perturbations.
|
|
421
|
+
- **Augmentation attack:** Apply data augmentations; members maintain consistent predictions across augmentations.
|
|
422
|
+
- **LiRA (Likelihood Ratio Attack):** Train multiple shadow models with/without the target sample; compare loss distributions.
|