Files
ZZ0R0 33393fe258 Initial release: Proteus — Rust Mythic C2 agent
Rust implementation of a Mythic Framework agent (wrapper=False, raw shellcode
output). Features per-build function shuffle, ChaCha20 data-section obfuscation,
dynamic API resolution via PEB walk, and a full Python Mythic Payload Type service.
2026-05-14 01:33:48 +02:00

25 KiB

STATE_OF_WORK.md — Proteus avancement

Ce fichier sert de carnet de bord pour ne pas se perdre entre sessions. À mettre à jour à chaque session significative.

Ne pas confondre avec :

  • ROADMAP.md — plan macro par phases (réf. opérationnelle long-terme)
  • DATA_SECTIONS_CIPHERING.md — design de la Phase 1.5 (figé)
  • docs/ARCHITECTURE.md — schéma global du système

1. Ligne du temps brève

Date Session Précis
2026-05-05 — Phase 1.5 design validé (ChaCha20 sur les data-sections + nonces per-build).
2026-05-07 — Reprise du debug sample.exe qui s'arrête après [init] winhttp OK. Mise en place de logs granulaires + fix d'un bug d'init introduit en session précédente (null_mut() non importé).
2026-05-08 this Restructuration interne des deux crates Rust (win/ regroupe l'ancien instance/ + rustic/ + utils/), split de tasking.rs en tasking.rs + messages.rs + commands.rs, dead-code dbj2_hash_seeded supprimé, OUT_DIR/hashes.rs renommé en OUT_DIR/build_config.rs. End-to-end cargo make mythic-payload revérifié.

1.6. Build seed + déterminisme + split builder.py (2026-05-08, suite)

Quoi

  • BuildParameter("build_seed", String, default="auto") — l'opérateur choisit une seed (decimal ou 0x…) ou laisse auto pour qu'on en pioche une fresh via secrets.randbits(64), echo'd dans le build log.
  • La seed est passée par builders/common.make_env() à la fois en PROTEUS_BUILD_SEED (lu par build.rs) et en SHUFFLE_SEED (lu par tools/gen-shuffled-linker.py), donc UNE valeur pilote les deux couches en lockstep.
  • build.rs (proteus-agent + loaders) : remplace thread_rng() par match seed { Some(s) => StdRng::seed_from_u64(s), None => StdRng::from_entropy() }.
  • Makefile.toml::SHUFFLE_SEED : passe en { value="0", condition={ env_not_set=["SHUFFLE_SEED"] } } pour ne plus écraser l'override env.
  • tools/link-shuffled.sh (loader) : ajout de --no-insert-timestamp et --build-id=none.
  • Makefile.toml::shuffle-strip (proteus-agent + loaders) : env = { SOURCE_DATE_EPOCH = "0" } pour que binutils strip ne réinsère pas un timestamp PE après ld.
  • builder.py (Mythic builder) : split en 3 modules sous proteus/mythic/builders/ :
    • common.py — stage_crates, make_env, run (helpers asyncio + paths)
    • shellcode.py — build_shellcode(tmp, cfg_json, seed, log) -> Path
    • loader.py — build_loader(tmp, cfg_json, seed, technique, log) -> Path Le Proteus(PayloadType) ne fait plus que parser les paramètres + dispatch.

Vérification (cargo make mythic-payload, depuis la racine)

Commande proteus-agent sha256 loader sha256
PROTEUS_BUILD_SEED=42 SHUFFLE_SEED=42 cargo make mythic-payload (run 1) 9e334330… 5a289291…
PROTEUS_BUILD_SEED=42 SHUFFLE_SEED=42 cargo make mythic-payload (run 2) 9e334330… ✓ identique 5a289291… ✓ identique
PROTEUS_BUILD_SEED=12345 SHUFFLE_SEED=12345 … 8dfa43a4… ≠ différent
cargo make mythic-payload (auto) différent à chaque run ✓ différent à chaque run ✓

cmp -l entre deux builds avec la même seed retourne 0 — byte-pour-byte identique, des octets de la DOS stub jusqu'à la fin du PE.


1.5. Restructuration interne des crates (2026-05-08)

Pourquoi

L'arbo intra-crate accumulait des modules grab-bag :

  • rustic/ (nom vide de sens) mélangeait PEB walking, allocator, mem* shims, et un faux hashes.rs qui n'était plus qu'un include!() du fichier généré par build.rs.
  • instance/ mélangeait le struct Instance central et tous les wrappers Win32 par DLL.
  • utils/ était lui aussi un grab-bag (flags PE + UnicodeString + helpers nocrt).
  • tasking.rs faisait 702 lignes mêlant : main-loop, JSON builders Mythic, command handlers, parsers (ascii_trim, parse_u32, push_u64), sleep, RNG.

Quoi

Pour chacun des deux crates (proteus-agent, loaders), trois sous-arbres disparaissent (instance/, rustic/, utils/) au profit d'un seul win/ clair :

win/
├── peb.rs        # NT structures + find_peb + get_cstr_len + UnicodeString
├── resolver.rs   # find_module_by_name_w + ExportsIter + ascii_eq_ci(_w)
├── allocator.rs  # #[global_allocator] (proteus-agent only)
├── nocrt.rs      # mem* shims
├── instance.rs   # Instance + INSTANCE_MAGIC + get_instance + load_dll
└── <dll>.rs      # ntdll, kernel32, advapi32, winhttp, bcrypt, crypt32

Côté agent/, tasking.rs est splité en trois :

  • tasking.rs — main loop (run, do_initial_checkin_loop, do_tasking_loop, exchange_message).
  • messages.rs — JSON builders (build_checkin_message, parse_tasking_response, …).
  • commands.rs — cmd_* handlers + dispatch + sleep_with_jitter + parsers.

Côté build :

  • OUT_DIR/hashes.rs → OUT_DIR/build_config.rs (le nom legacy n'avait plus rien à voir avec son contenu).
  • Dans proteus-agent, crate::rustic::hashes → crate::build_config.
  • Dans loaders, le hashes.rs qui n'incluait qu'un fichier vide est carrément supprimé (le build.rs du loader n'écrit plus de hashes.rs).

Dead code supprimé

  • loaders/src/utils/utils.rs::dbj2_hash_seeded — la dernière fonction djb2 vivante. build.rs::djb2_hash_seeded avait disparu lors du refacto PEB du 2026-05-07 mais le côté runtime du loader n'avait pas été nettoyé.
  • Re-exports inutilisés dans win/mod.rs (init_crypt32, load_dll — toujours utilisés, mais via le path complet crate::win::instance::*, pas via la re-exportation top-level).

Confirmation que le compile-time hashing est mort

Recherche git grep -in 'djb2' après nettoyage : seul résultat est ce paragraphe + les notes historiques dans build.rs. Plus aucun appel à runtime, plus aucune table émise par build.rs. Toute résolution Win32 passe par crate::win::resolver + comparaison ASCII-CI directe contre les noms obf_bytes!-encryptés.

Vérif end-to-end

$ cargo make mythic-payload
[shuffle] seed=4151969711624575314 fns=322 rdata_prx=247 -> Linker.shuffled.ld
[link] -> Proteus.bin (34232 bytes)
[shuffle] seed=4643722448869141158 fns=61 rdata_prx=33 pe=True
[link] -> proteus-loader.exe (41984 bytes)
[mythic-payload] tech=apc-self
$ strings -n 6 Proteus.bin | grep -iE 'checkin|action|tasking|kernel32' | wc -l
0

Aucun literal Mythic-protocole / Win32 ne fuit ; Phase 1.5 toujours intacte après le refacto.


2. Bug résolu (2026-05-07) — sample.exe silencieux après winhttp OK

Cause racine

Collision de INSTANCE_MAGIC entre loader et proteus-agent. Les deux crates définissaient INSTANCE_MAGIC = 0x17171717 et registraient leur propre Instance dans PEB.ProcessHeaps. Le loader's slot venait avant celui du shellcode dans le tableau ; quand proteus-agent::get_instance() walkait PEB.ProcessHeaps, il trouvait LE LOADER en premier et retournait son Instance (avec un AgentState tout zéro — le loader n'a pas d'AgentState). Résultat : running lu comme false, le do-while du checkin loop sautait, l'agent quittait silencieusement via NtTerminateProcess.

À noter : crates/loaders/src/utils/ était un symlink vers crates/proteus-agent/src/utils/, donc impossible de tenir deux valeurs distinctes. Le symlink est maintenant cassé, et chaque crate a son propre flags.rs.

Fix appliqué

  • proteus-agent/src/utils/flags.rs : INSTANCE_MAGIC = 0x17171717 (inchangé)
  • loaders/src/utils/flags.rs : INSTANCE_MAGIC = 0x4C4F4144 ("LOAD")
  • Symlink loaders/src/utils → proteus-agent/src/utils supprimé, fichiers indépendants
  • null_mut importé dans main.rs (compile-error qui bloquait la rebuild)
  • dbg_log! self-guarded (null-check sur les fp kernel32) — safe avant init_kernel32
  • Logs granulaires ajoutés dans main.rs::initialize() et instance/advapi32.rs

Vérification

End-to-end après fix :

[task] instance ptr: 1975572695136          ← matches alloc returned ptr
[task] running: 1                            ← TRUE
[task] sleep_seconds: 10                     ← from CFG_SLEEP_SECONDS
[task] entering initial checkin loop
[checkin] attempt                            ← effectivement tente
[seal] plaintext bytes: 171

L'agent essaie maintenant de checkin contre 127.0.0.1:7443 (config par défaut). Pour valider end-to-end, il faut un serveur Mythic réel.

Lecture pour reprendre le contexte plus tard

  • crates/loaders/src/utils/flags.rs — commentaire explicite sur la distinction
  • crates/proteus-agent/src/utils/flags.rs — idem
  • crates/loaders/src/main.rs::initialize() — où le loader register son Instance (slot K)
  • crates/proteus-agent/src/main.rs::initialize() — où proteus-agent register la sienne (slot K+1)
  • crates/proteus-agent/src/instance/instance.rs::get_instance() — la fonction qui matchait au mauvais Instance

3. Refacto résolution PEB (livrée 2026-05-07)

Avant

Pour chaque module, ldr_module(MODULE_HASH) walkait PEB.LoaderData en hashant chaque nom rencontré. Pour chaque fonction, ldr_function(base, FUNC_HASH) itérait toute la table d'exports en hashant chaque nom.

  • N=30 fonctions résolues ⇒ 30 walks d'export tables
  • Build.rs émettait une table MODULE_SEED + FUNCTION_SEED + *_HASH régénérée à chaque cargo build
  • OPSEC : justifiable AVANT Phase 1.5, redondant APRÈS

Après

  • crates/proteus-agent/src/rustic/peb_enum.rs (NEW) — find_module_by_name_w (un walk) + ExportsIter (itérateur direct sur les exports d'un module).
  • Même fichier dupliqué côté loader : crates/loaders/src/rustic/peb_enum.rs.
  • Chaque init_* :
    1. let target_w = obf_utf16!(b"<module>.dll") — décodé sur la stack
    2. let base = find_module_by_name_w(&target_w) — un seul walk PEB
    3. Pré-décode TOUTES les fonctions cibles avec obf_bytes!
    4. Une seule itération for (name, addr) in ExportsIter::new(base)
    5. Cascade if ascii_eq_ci(name, &target) { instance.X.field = transmute(addr); } pour chaque cible
  • Comparaison ASCII-CI directe via peb_enum::ascii_eq_ci{,_w} — pas de djb2.
  • Pour les modules pas encore mappés (winhttp/advapi32) : crate::instance::load_dll(&path_z) (kernel32!LoadLibraryW) puis re-walk.

Suppressions

  • crates/proteus-agent/src/rustic/ldrapi.rs — supprimé
  • crates/loaders/src/rustic/ldrapi.rs (était symlink vers le précédent) — supprimé
  • crates/proteus-agent/build.rs — toute la machinerie MODULE_SEED, FUNCTION_SEED, libraries[], functions[], *_HASH retirée. Conservé : émission CFG_* + master_key + nonce_salt.
  • crates/loaders/build.rs — idem (et émet désormais master_key + nonce_salt pour proteus-obf, qu'il n'utilisait pas avant).
  • crates/proteus-agent/src/utils/utils.rs — dbj2_hash_seeded retirée (inutile).
  • Symlinks loaders/src/utils → proteus-agent/src/utils et loaders/src/rustic/{hashes,nocrt,ntpeb}.rs → proteus-agent/... cassés : chaque crate a maintenant ses propres copies indépendantes.

Validation end-to-end (run réussi sur la VM lab)

[init] ntdll + kernel32 OK
[init] heap OK
[init] winhttp OK
[init] advapi32 OK
[init] agent state populated
[init] -> run()
[task] tasking::run entry
[task] runtime config loaded
[task] callback port: 7443
[task] get_uri: /api/v1.4/agent_message
[task] post_uri: /api/v1.4/agent_message
[task] entering initial checkin loop
[checkin] attempt
[seal] plaintext bytes: 171

L'agent boote, peuple son AgentState (running=true, sleep_seconds=10), tente le checkin contre 127.0.0.1:7443. Pour aller plus loin, il faut un Mythic réel.

3.5 Layout du repo (refacto 2026-05-07)

L'arborescence des Rust crates a été déplacée de Proteus/crates/ (top-level) vers Proteus/Payload_Type/proteus/crates/. Raison : mythic-cli install folder ne suit pas les symlinks de directories — l'ancien layout reposait sur trois symlinks Payload_Type/proteus/proteus/{agent_code,loader_code,layout_manifest_code} qui pointaient vers le top-level crates/, et mythic-cli les copiait comme fichiers vides de 0 octet. Le shutil.copytree du builder.py crashait ensuite avec NotADirectoryError.

Layout actuel :

Proteus/
├── config.json                     # gateway mythic-cli
├── Makefile.toml                   # délègue vers Payload_Type/proteus/crates/*
├── README.md, ROADMAP.md, docs/    # doc projet
└── Payload_Type/
    └── proteus/
        ├── Dockerfile              # COPY crates crates (build context = ce dir)
        ├── main.py
        ├── requirements.txt        # mythic-container == 0.6.16
        ├── crates/                 # ← canonical location, plus de symlinks
        │   ├── proteus-agent/
        │   ├── loaders/
        │   ├── layout-manifest/
        │   ├── proteus-obf/
        │   ├── proteus-obf-macros/
        │   └── mini-loader/
        └── proteus/                # python package
            ├── __init__.py
            └── mythic/
                ├── __init__.py     # auto-import des agent_functions/
                └── agent_functions/
                    ├── builder.py
                    ├── exit.py, pwd.py, sleep.py, whoami.py
                    └── ...

Conséquences :

  • cargo make shuffle au top-level marche : root Makefile.toml délègue vers Payload_Type/proteus/crates/proteus-agent.
  • sudo mythic-cli install folder <path-to-repo> -f marche directement : pas de staging, pas de tmp dir, pas de symlinks.
  • La règle cargo make install-mythic (staging hack) a été supprimée — n'avait plus lieu d'être.

4. Conventions de l'architecture (post-refacto)

Le principe single-pass à respecter

Ne jamais ajouter une nouvelle fonction Win32 à résoudre par hash + walk dédié. Ajouter à la place :

  1. Le type Win32 + le champ dans la struct du module
  2. La déclaration dans Module::new() (transmute null)
  3. Un nouveau let n_xxx = obf_bytes!(b"FuncName"); dans le init_* correspondant
  4. Une nouvelle branche else if ascii_eq_ci(name, &n_xxx) { instance.module.xxx = transmute(addr); }

Loader vs proteus-agent — code partiellement dupliqué

Les helpers peb_enum, nocrt, ntpeb existent en deux copies indépendantes (loader + proteus-agent). C'est volontaire après le bug INSTANCE_MAGIC : les symlinks ont été cassés pour permettre des magic distincts. Si on veut éviter la divergence, extraire en crate partagée — mais à coût de refacto et out-of-scope ici.

INSTANCE_MAGIC — TOUJOURS distincts entre loader et proteus-agent

  • crates/loaders/src/utils/flags.rs : 0x4C4F4144 ("LOAD")
  • crates/proteus-agent/src/utils/flags.rs : 0x17171717 Tout changement doit garder cette distinction. Lire les commentaires dans les deux fichiers.

Diagnostic

Trois bugs distincts ont été identifiés dans crates/proteus-agent/src/main.rs/log.rs :

Bug A — null_mut() utilisé sans import (compile-error)

La session précédente a introduit un reentrancy guard :

(*peb).sub_system_data = null_mut();

…sans ajouter use core::ptr::null_mut;. Donc le code actuel ne compile pas. Le binaire que l'opérateur a lancé date d'avant ce changement. Fix : ajouter l'import.

Bug B — dbg_log!("[init] ntdll OK") appelé avant init_kernel32()

La macro dbg_log! appelle instance.kernel32.write_file(...) et instance.kernel32.get_std_handle(...). Avant init_kernel32(), ces pointeurs sont transmute(null_mut()) — appeler un function pointer NULL en x64 = access violation.

Fix défense en profondeur : write_raw doit guard les pointeurs avant de les déréférencer, pour qu'on puisse appeler dbg_log! à n'importe quel moment du boot sans risque. Bonus : si on perd un log précoce, le programme survit.

Bug C — Pas de log granulaire dans le segment qui crashe

Entre [init] winhttp OK et le premier log de agent::tasking::run ([task] tasking::run entry), il y a au minimum :

  1. init_advapi32()
  2. Layout::new::<Instance>() + alloc(layout)
  3. copy_nonoverlapping(temp → permanent)
  4. *process_heaps.add(N) = permanent_instance_ptr (swap PEB slot)
  5. (*permanent_instance_ptr).agent.populate() (decode CFG_UUID via ChaCha20)
  6. run() → agent::tasking::run()

Sans logs intermédiaires, impossible de savoir lequel échoue. Fix : un dbg_log! autour de chaque étape.

Hypothèses ordonnées (à invalider une par une après rebuild)

  1. alloc(layout) retourne NULL. NT_HEAPALLOCATOR.initialize() a peut-être échoué silencieusement (RtlCreateHeap → 0). On a pas vérifié son retour.
  2. PEB slot écrasé. LoadLibraryW("winhttp.dll") charge winhttp → DllMain peut faire HeapCreate() → écrit dans le slot adjacent. Notre slot Instance reste valable mais la séquence *process_heaps.add(number_of_heaps) = … (re-write avec le permanent ptr) écrit à un index qui peut-être occupé par autre chose maintenant.
  3. init_advapi32 échoue silencieusement. Hash mismatch (peu probable), ou obf_utf16_z! qui décode mal le path (très peu probable, c'est de l'arithmétique pure).
  4. agent.populate() panic. cfg_decode fait unreachable_unchecked si dst.len() < blob.len(). Tous les call-sites passent des buffers MAX_*_LEN donc ça ne devrait pas se déclencher.

3. État du code (par crate)

Payload_Type/proteus/crates/proteus-agent/

Agent core, no_std, x86_64-pc-windows-gnu, output ~34KB flat binary (Proteus.bin) après Phase 1.5. Layout après refacto 2026-05-08 :

Sous-arbre Contenu
src/main.rs _start, initialize, run, _Unwind_Resume
src/log.rs dbg_log! family (gated par feature debug-log)
src/obf.rs MASTER_KEY static + re-exports obf!* macros
src/build_config.rs include!() du fichier généré par build.rs
src/win/{peb,resolver,allocator,nocrt,instance}.rs OS surface — PEB, single-pass resolver, NT heap allocator, mem* shims, Instance + INSTANCE_MAGIC + get_instance
src/win/{ntdll,kernel32,winhttp,advapi32,bcrypt,crypt32}.rs Wrappers Win32 par DLL avec init_* single-pass
src/agent/{config,state}.rs AgentConfig + AgentState
src/agent/{tasking,messages,commands}.rs Loop principale + JSON builders + handlers de commandes (split du tasking.rs original 702 lignes)
src/comms/{http,crypto,envelope,json}.rs Stack comms (WinHTTP + BCrypt + Mythic envelope + JSON)
build.rs Émet master_key + nonce_salt + build_config.rs (CFG_*) par build

Payload_Type/proteus/crates/loaders/

Phase 0.5 done. Embed Proteus.bin via include_bytes!, applique une technique d'injection (tech-self-rwx / tech-self-rw-to-rx / tech-apc-self / tech-local-thread). Layout symétrique à proteus-agent mais réduit (pas de comms/, pas de agent/, win/ sans allocator) :

Sous-arbre Contenu
src/main.rs _start, initialize (sans heap step)
src/obf.rs MASTER_KEY static + re-exports obf!* macros
src/shellcode.rs pub static SHELLCODE = include_bytes!("../../proteus-agent/Proteus.bin")
src/win/{peb,resolver,nocrt,instance,ntdll,kernel32}.rs Subset OS surface (pas d'allocator, pas de winhttp/bcrypt/advapi32/crypt32)
src/techniques/{self_rwx,self_rw_to_rx,apc_self,local_thread}.rs Une technique par fichier, gated par feature tech-<name>

Payload_Type/proteus/crates/proteus-obf + proteus-obf-macros

Phase 1.5 livrée. ChaCha20 keystream + nonce salt per-build. Aucun changement structurel le 2026-05-08.

Payload_Type/proteus/crates/layout-manifest

Outil parse-COFF (host-side, dev tool) qui sort un manifest JSON. Utilisé pré-link pour valider les sections / relocations. Indépendant des autres crates.

Payload_Type/proteus/proteus/ (Mythic side, Python style thanatos — pas Go)

Skeleton en place : Dockerfile, builder.py, main.py. Auto-import des agent_functions/. Pas testé contre un vrai serveur Mythic.


4. Phase 1.5 — État final

Acceptance partielle :

  • CFG_* migrés sur ChaCha20 (build.rs::emit_chacha)
  • obf! / obf_bytes! / obf_utf16_z! couvrent toutes les literals Mythic-protocol
  • Pas de format!() dans le code agent
  • unwrap_unchecked() au lieu de unwrap() pour drop le panic-format de .rdata
  • Per-build nonce salt → samples non-corrélables byte-à-byte
  • strings -n 6 Proteus.bin ne montre plus aucun literal Mythic
  • Reste un run de 643 bytes commun entre 2 builds — probablement le chacha20_block pur arithmétique. À traiter par inline+specialize ou par instruction substitution. Reporté.

5. Conventions et pièges connus

Boot sequence — fragile, lire avant de modifier

_start (asm) → initialize() ──► find_peb()
                            ├─► reentrancy guard via peb.sub_system_data
                            ├─► register temp Instance dans PEB.ProcessHeaps
                            ├─► init_ntdll       (résolution hash de RtlCreateHeap, etc.)
                            ├─► init_kernel32    ⚠ DOIT venir avant tout dbg_log!
                            ├─► NT_HEAPALLOCATOR.initialize()
                            ├─► init_winhttp     (LoadLibraryW + ldr_module)
                            ├─► init_advapi32    (idem)
                            ├─► alloc Instance permanent + copy_nonoverlapping
                            ├─► swap PEB slot vers le permanent
                            ├─► agent.populate() (decode CFG_UUID etc.)
                            └─► run() → tasking

Règles de vie pour dbg_log!

  • Active uniquement avec la feature debug-log (cargo make shuffle avec PROTEUS_FEATURES=debug-log ou similaire).
  • Sûr d'usage à tout moment dès que log.rs a la garde sur les fp (cf. fix 2026-05-07).
  • Les literals texte cassent l'objectif Phase 1.5 — production builds = feature OFF.

Règles de vie pour obf! / obf_bytes! / obf_utf16_z!

  • Toute literal Mythic-protocole, header HTTP, ou nom de DLL DOIT passer par obf*!.
  • Pas de format!() côté agent — format! fait revenir les tables core::fmt::num en clair.
  • Ne jamais logger une obf-decoded value ; ça réintroduit le plaintext temporairement dans .rdata via le format-string du log.

Règle de vie pour la mémoire

  • Instance vit dans PEB.ProcessHeaps[N] identifié par magic = 0x17171717.
  • get_instance() walk le tableau et match sur magic.
  • Ne jamais ajouter un champ avant magic dans la struct Instance sans réfléchir.

6. Prochaines étapes immédiates

  1. ✅ Cartographier le code (sessions 2026-05-07 + 2026-05-08)
  2. ✅ Diagnostiquer + fixer les 3 bugs main.rs/log.rs (livré 2026-05-07)
  3. ✅ Refacto interne des crates (livré 2026-05-08, voir §1.5)
  4. ⬜ Test end-to-end checkin contre un vrai serveur Mythic sur le lab
  5. ⬜ Continuer Phase 2 (filesystem ops : cd, ls, cat, cp, mv, rm, mkdir) — sites canoniques sont agent/commands.rs pour les handlers et win/kernel32.rs pour ajouter les imports Win32 (single-pass dans init_kernel32).
  6. ⬜ Phase 1.5 §6 (Phase 2 ROADMAP) — si possible casser le run de ~643 bytes commun entre 2 builds (probablement chacha20_block — inline+specialize per call site ou instruction substitution).

7. Comment reprendre cette session si elle est interrompue

  1. Lire ce fichier en entier (notamment §1.5 pour le layout actuel).
  2. Lire les memory entries (MEMORY.md).
  3. Lire crates/proteus-agent/src/main.rs pour le boot order.
  4. Lire crates/proteus-agent/src/agent/tasking.rs pour la main loop.
  5. cargo make mythic-payload depuis la racine Proteus/ — doit produire un Proteus.bin ~34KB et un proteus-loader.exe ~42KB sans warning.