55Supports multiple CTF sources:
66 - DeFiVulnLabs (56 isolated vulnerability tests by SunWeb3Sec)
77 - Paradigm CTF 2021/2022/2023 (competitive security challenges)
8+ - 2025 CTFs: R3CTF 2025 + HTB Cyber Apocalypse 2025
89
910Usage:
1011 python3 ctf_benchmark.py # DeFiVulnLabs benchmark
1112 python3 ctf_benchmark.py --paradigm # Paradigm CTF benchmark
13+ python3 ctf_benchmark.py --ctf2025 # 2025 CTF benchmark
1214 python3 ctf_benchmark.py --all # All benchmarks
1315 python3 ctf_benchmark.py --dry-run # Show mapping only
1416 python3 ctf_benchmark.py --repo-path /tmp/X # Use existing clone
773775}
774776
775777
778+ # ─── 2025 CTF → ETH pattern mapping ───────────────────────────────────────
779+ # R3CTF 2025 (r3kapig) + HTB Cyber Apocalypse 2025 (HackTheBox)
780+ # Maps challenge_name → { source, files_dir, files, eth_ids, category, static }
781+
782+ CTF_2025_REPOS = {
783+ "r3ctf-2025" : "https://github.com/r3kapig/r3ctf-2025.git" ,
784+ "cyber-apocalypse-2025" : "https://github.com/hackthebox/cyber-apocalypse-2025.git" ,
785+ }
786+
787+ CTF_2025_MAP = {
788+ # ═══════════════════════════════════════════════════════════════
789+ # R3CTF 2025 (r3kapig — Blockchain "Blackchain" category)
790+ # ═══════════════════════════════════════════════════════════════
791+
792+ "r3ctf-2025/miniagent" : {
793+ "source" : "r3ctf-2025" ,
794+ "files_dir" : "Blackchain/miniagent/attachment/src" ,
795+ "files" : ["Arena.sol" , "Boss.sol" , "Challenge.sol" , "Randomness.sol" ],
796+ "eth_ids" : ["ETH-037" ], # Weak randomness (block.prevrandao as seed)
797+ "category" : "Logic" ,
798+ "static" : True ,
799+ "description" : "Battle arena with predictable randomness (block.prevrandao seed)" ,
800+ },
801+ "r3ctf-2025/signin" : {
802+ "source" : "r3ctf-2025" ,
803+ "files_dir" : "Blackchain/signin/attachment/src" ,
804+ "files" : ["Vault.sol" , "Setup.sol" , "LING.sol" ],
805+ "eth_ids" : ["ETH-057" ], # Vault share inflation / first depositor attack
806+ "category" : "DeFi" ,
807+ "static" : True ,
808+ "description" : "ERC4626 vault with first-depositor inflation vulnerability" ,
809+ },
810+
811+ # NOTE: socpcl and socpclv2 are Solana/Rust challenges — not applicable
812+
813+ # ═══════════════════════════════════════════════════════════════
814+ # HTB Cyber Apocalypse 2025 (HackTheBox)
815+ # Source: github.com/hackthebox/cyber-apocalypse-2025
816+ # NOTE: Repo has compiled JSON only, no .sol source. We use
817+ # reconstructed contracts from official writeups.
818+ # ═══════════════════════════════════════════════════════════════
819+
820+ "htb-ca-2025/Eldorion" : {
821+ "source" : "htb-ca-2025-embedded" ,
822+ "files_dir" : None , # Embedded test contract
823+ "files" : [],
824+ "eth_ids" : ["ETH-036" ], # Timestamp-based state reset
825+ "category" : "Logic" ,
826+ "static" : True ,
827+ "description" : "Health regeneration via block.timestamp comparison — batch attack in single block" ,
828+ "embedded_code" : {
829+ "Eldorion.sol" : '''// SPDX-License-Identifier: MIT
830+ pragma solidity ^0.8.28;
831+ contract Eldorion {
832+ uint256 public health = 300;
833+ uint256 public lastAttackTimestamp;
834+ uint256 private constant MAX_HEALTH = 300;
835+ event EldorionDefeated(address slayer);
836+ modifier eternalResilience() {
837+ if (block.timestamp > lastAttackTimestamp) {
838+ health = MAX_HEALTH;
839+ lastAttackTimestamp = block.timestamp;
840+ }
841+ _;
842+ }
843+ function attack(uint256 damage) external eternalResilience {
844+ require(damage <= 100, "Mortals cannot strike harder than 100");
845+ require(health >= damage, "Overkill is wasteful");
846+ health -= damage;
847+ if (health == 0) { emit EldorionDefeated(msg.sender); }
848+ }
849+ }''' ,
850+ },
851+ },
852+ "htb-ca-2025/HeliosDEX" : {
853+ "source" : "htb-ca-2025-embedded" ,
854+ "files_dir" : None ,
855+ "files" : [],
856+ "eth_ids" : ["ETH-016" ], # Rounding mode inconsistency
857+ "category" : "Arithmetic" ,
858+ "static" : True ,
859+ "description" : "DEX with inconsistent Math.mulDiv rounding modes (Floor vs Expand)" ,
860+ "embedded_code" : {
861+ "HeliosDEX.sol" : '''// SPDX-License-Identifier: MIT
862+ pragma solidity ^0.8.28;
863+ import "@openzeppelin/contracts/utils/math/Math.sol";
864+ contract HeliosDEX {
865+ uint256 public immutable exchangeRatioELD = 2;
866+ uint256 public immutable exchangeRatioMAL = 4;
867+ uint256 public immutable exchangeRatioHLS = 10;
868+ uint256 public immutable feeBps = 25;
869+ mapping(address => bool) public hasRefunded;
870+ // Floor rounding — favors contract
871+ function swapForELD() external payable {
872+ uint256 grossELD = Math.mulDiv(msg.value, exchangeRatioELD, 1e18, Math.Rounding(0));
873+ uint256 fee = (grossELD * feeBps) / 10_000;
874+ uint256 netELD = grossELD - fee;
875+ require(netELD > 0, "Zero tokens");
876+ }
877+ // Ceil rounding — favors user slightly
878+ function swapForMAL() external payable {
879+ uint256 grossMal = Math.mulDiv(msg.value, exchangeRatioMAL, 1e18, Math.Rounding(1));
880+ uint256 fee = (grossMal * feeBps) / 10_000;
881+ uint256 netMAL = grossMal - fee;
882+ require(netMAL > 0, "Zero tokens");
883+ }
884+ // Expand rounding — strongly favors user (1 wei -> 1 HLS)
885+ function swapForHLS() external payable {
886+ uint256 grossHLS = Math.mulDiv(msg.value, exchangeRatioHLS, 1e18, Math.Rounding(3));
887+ uint256 fee = (grossHLS * feeBps) / 10_000;
888+ uint256 netHLS = grossHLS - fee;
889+ require(netHLS > 0, "Zero tokens");
890+ }
891+ function oneTimeRefund(address item, uint256 amount) external {
892+ require(!hasRefunded[msg.sender], "Refund already bestowed");
893+ uint256 grossEth = Math.mulDiv(amount, 1e18, exchangeRatioHLS);
894+ hasRefunded[msg.sender] = true;
895+ payable(msg.sender).transfer(grossEth);
896+ }
897+ }''' ,
898+ },
899+ },
900+ "htb-ca-2025/EldoriaGate" : {
901+ "source" : "htb-ca-2025-embedded" ,
902+ "files_dir" : None ,
903+ "files" : [],
904+ "eth_ids" : ["ETH-013" ], # Integer overflow in assembly (uint256→uint8)
905+ "category" : "Arithmetic" ,
906+ "static" : True ,
907+ "description" : "Assembly integer overflow — uint256 add truncated to uint8 in storage" ,
908+ "embedded_code" : {
909+ "EldoriaGate.sol" : '''// SPDX-License-Identifier: MIT
910+ pragma solidity ^0.8.28;
911+ contract EldoriaGateKernel {
912+ bytes4 private eldoriaSecret;
913+ uint8 private constant ROLE_SERF = 1;
914+ struct Villager { uint256 id; bool authenticated; uint8 roles; }
915+ mapping(address => Villager) public villagers;
916+ uint256 public villagerCount;
917+ constructor(bytes4 _secret) { eldoriaSecret = _secret; }
918+ function authenticate(address _unknown, bytes4 _passphrase) external returns (bool) {
919+ return _passphrase == eldoriaSecret;
920+ }
921+ function evaluateIdentity(address _unknown, uint8 _contribution) external {
922+ uint256 id = ++villagerCount;
923+ uint8 roles;
924+ assembly {
925+ let defaultRolesMask := ROLE_SERF
926+ roles := add(defaultRolesMask, _contribution)
927+ if lt(roles, defaultRolesMask) { revert(0, 0) }
928+ }
929+ villagers[_unknown] = Villager(id, true, roles);
930+ }
931+ }
932+ contract EldoriaGate {
933+ EldoriaGateKernel public kernel;
934+ constructor(bytes4 _secret) { kernel = new EldoriaGateKernel(_secret); }
935+ function enter(bytes4 passphrase) external payable {
936+ bool isAuthenticated = kernel.authenticate(msg.sender, passphrase);
937+ require(isAuthenticated, "Authentication failed");
938+ uint8 contribution = uint8(msg.value);
939+ kernel.evaluateIdentity(msg.sender, contribution);
940+ }
941+ function checkUsurper(address _villager) external view returns (bool) {
942+ (uint256 id, bool authenticated, uint8 rolesBitMask) = kernel.villagers(_villager);
943+ return authenticated && (rolesBitMask == 0);
944+ }
945+ }''' ,
946+ },
947+ },
948+ }
949+
950+
776951@dataclass
777952class BenchmarkResult :
778953 contract : str
@@ -858,6 +1033,126 @@ def run_benchmark(repo_path: str) -> list[BenchmarkResult]:
8581033 return results
8591034
8601035
1036+ def ensure_2025_repos () -> dict :
1037+ """Ensure 2025 CTF repos are cloned, return {name: path}."""
1038+ paths = {}
1039+ for name , url in CTF_2025_REPOS .items ():
1040+ dest = os .path .join (tempfile .gettempdir (), name )
1041+ if os .path .isdir (dest ):
1042+ print (f"Using existing clone at { dest } " )
1043+ else :
1044+ if not clone_repo (dest , url ):
1045+ continue
1046+ paths [name ] = dest
1047+ return paths
1048+
1049+
1050+ def run_2025_benchmark (repo_paths : dict ) -> list [BenchmarkResult ]:
1051+ """Run scan_patterns against each mapped 2025 CTF challenge."""
1052+ import shutil
1053+ results = []
1054+
1055+ for challenge_key , mapping in sorted (CTF_2025_MAP .items ()):
1056+ eth_ids = mapping ["eth_ids" ]
1057+ source = mapping ["source" ]
1058+
1059+ with tempfile .TemporaryDirectory () as tmpdir :
1060+ found_any = False
1061+
1062+ if source == "htb-ca-2025-embedded" :
1063+ # Write embedded code to temp dir
1064+ for fname , code in mapping .get ("embedded_code" , {}).items ():
1065+ with open (os .path .join (tmpdir , fname ), "w" ) as f :
1066+ f .write (code )
1067+ found_any = True
1068+ elif source in repo_paths :
1069+ # Copy files from cloned repo
1070+ repo = repo_paths [source ]
1071+ files_dir = mapping .get ("files_dir" , "" )
1072+ for fname in mapping .get ("files" , []):
1073+ src = os .path .join (repo , files_dir , fname ) if files_dir else None
1074+ if src and os .path .exists (src ):
1075+ shutil .copy2 (src , os .path .join (tmpdir , fname ))
1076+ found_any = True
1077+
1078+ if not found_any :
1079+ print (f" [SKIP] { challenge_key } — no files found" )
1080+ result = BenchmarkResult (contract = challenge_key , expected_ids = eth_ids , detected_ids = [])
1081+ result .compute ()
1082+ results .append (result )
1083+ continue
1084+
1085+ findings = scan_patterns (tmpdir )
1086+ detected_ids = list (set (f .id for f in findings ))
1087+
1088+ result = BenchmarkResult (
1089+ contract = challenge_key ,
1090+ expected_ids = eth_ids ,
1091+ detected_ids = detected_ids ,
1092+ )
1093+ result .compute ()
1094+ results .append (result )
1095+
1096+ status = "PASS" if result .detected else "MISS"
1097+ print (f" [{ status } ] { challenge_key } : expected { eth_ids } , detected { detected_ids } " )
1098+
1099+ return results
1100+
1101+
1102+ def print_2025_report (results : list [BenchmarkResult ]):
1103+ """Print 2025 CTF benchmark summary."""
1104+ with_patterns = [r for r in results if r .expected_ids ]
1105+ total = len (results )
1106+ total_static = len (with_patterns )
1107+ detected_static = sum (1 for r in with_patterns if r .detected )
1108+ all_tp = sum (len (r .true_positives ) for r in with_patterns )
1109+ all_expected = sum (len (r .expected_ids ) for r in with_patterns )
1110+
1111+ print ("\n " + "=" * 70 )
1112+ print ("CTF BENCHMARK REPORT — 2025 CTFs (R3CTF + HTB Cyber Apocalypse)" )
1113+ print ("=" * 70 )
1114+ print (f"\n Total challenges: { total } " )
1115+ print (f"Challenges with patterns: { total_static } " )
1116+ print (f"\n Detection rate: { detected_static } /{ total_static } "
1117+ f"({ 100 * detected_static // total_static if total_static else 0 } %)" )
1118+ print (f"Pattern matches: { all_tp } /{ all_expected } "
1119+ f"({ 100 * all_tp // all_expected if all_expected else 0 } %)" )
1120+
1121+ missed = [r for r in with_patterns if not r .detected ]
1122+ if missed :
1123+ print ("\n --- Missed Challenges ---" )
1124+ for r in missed :
1125+ mapping = CTF_2025_MAP .get (r .contract , {})
1126+ print (f" { r .contract } : expected { r .expected_ids } — { mapping .get ('description' , '' )} " )
1127+
1128+ print ("\n --- Detection by Source ---" )
1129+ for source_name in ["R3CTF 2025" , "HTB CA 2025" ]:
1130+ prefix = "r3ctf" if "R3CTF" in source_name else "htb"
1131+ src_results = [r for r in with_patterns if prefix in r .contract ]
1132+ if not src_results :
1133+ continue
1134+ det = sum (1 for r in src_results if r .detected )
1135+ tot = len (src_results )
1136+ pct = 100 * det // tot if tot else 0
1137+ bar = "#" * (pct // 5 ) + "." * (20 - pct // 5 )
1138+ print (f" { source_name :<20} [{ bar } ] { det } /{ tot } ({ pct } %)" )
1139+
1140+ print ("\n --- Detection by Category ---" )
1141+ categories = {}
1142+ for r in with_patterns :
1143+ cat = CTF_2025_MAP .get (r .contract , {}).get ("category" , "Unknown" )
1144+ if cat not in categories :
1145+ categories [cat ] = {"total" : 0 , "detected" : 0 }
1146+ categories [cat ]["total" ] += 1
1147+ if r .detected :
1148+ categories [cat ]["detected" ] += 1
1149+
1150+ for cat , counts in sorted (categories .items ()):
1151+ pct = 100 * counts ["detected" ] // counts ["total" ] if counts ["total" ] else 0
1152+ bar = "#" * (pct // 5 ) + "." * (20 - pct // 5 )
1153+ print (f" { cat :<20} [{ bar } ] { counts ['detected' ]} /{ counts ['total' ]} ({ pct } %)" )
1154+
1155+
8611156def ensure_paradigm_repos () -> dict :
8621157 """Ensure Paradigm CTF repos are cloned, return {year: path}."""
8631158 paths = {}
@@ -1101,8 +1396,10 @@ def main():
11011396 help = "Show mapping without cloning/scanning" )
11021397 parser .add_argument ("--paradigm" , action = "store_true" ,
11031398 help = "Run Paradigm CTF benchmark (2021+2022+2023)" )
1399+ parser .add_argument ("--ctf2025" , action = "store_true" ,
1400+ help = "Run 2025 CTF benchmark (R3CTF + HTB Cyber Apocalypse)" )
11041401 parser .add_argument ("--all" , action = "store_true" ,
1105- help = "Run all benchmarks (DeFiVulnLabs + Paradigm CTF )" )
1402+ help = "Run all benchmarks (DeFiVulnLabs + Paradigm + 2025 CTFs )" )
11061403 parser .add_argument ("--repo-path" , help = "Path to existing DeFiVulnLabs clone" )
11071404 parser .add_argument ("--output" , help = "Write JSON results to file" )
11081405
@@ -1112,8 +1409,9 @@ def main():
11121409 print_dry_run ()
11131410 return
11141411
1115- run_defi = not args .paradigm or args .all
1412+ run_defi = not ( args .paradigm or args . ctf2025 ) or args .all
11161413 run_paradigm = args .paradigm or args .all
1414+ run_2025 = args .ctf2025 or args .all
11171415
11181416 all_results = {}
11191417
@@ -1149,6 +1447,17 @@ def main():
11491447 print_paradigm_report (paradigm_results )
11501448 all_results ["ParadigmCTF" ] = paradigm_results
11511449
1450+ # ── 2025 CTF benchmark ──
1451+ if run_2025 :
1452+ print ("\n " + "=" * 70 )
1453+ print ("2025 CTF BENCHMARK (R3CTF + HTB Cyber Apocalypse)" )
1454+ print ("=" * 70 )
1455+ repo_paths_2025 = ensure_2025_repos ()
1456+ print ("\n Running 2025 CTF benchmark..." )
1457+ ctf2025_results = run_2025_benchmark (repo_paths_2025 )
1458+ print_2025_report (ctf2025_results )
1459+ all_results ["CTF2025" ] = ctf2025_results
1460+
11521461 # ── Combined summary ──
11531462 if args .all and len (all_results ) > 1 :
11541463 print ("\n " + "=" * 70 )
0 commit comments