From 229ad358ec2f096018c9d49de2904454225d8d7a Mon Sep 17 00:00:00 2001 From: Sinatras Date: Fri, 1 May 2026 16:17:18 +0300 Subject: [PATCH] fix(detector): flag chr-concat string construction as obfuscated dispatch Targets red #17 ('chr-concat getattr dispatch') under 'Direction - Obfuscated exec / dynamic execution'. Live KG (kernelguard==0.2.2) called it valid because: def custom_kernel(data): op = chr(99) + chr(108) + chr(111) + chr(110) + chr(101) return getattr(data, op)() contains no literal 'eval' / 'exec' / 'compile' / '__import__' calls, so the existing detect_dynamic_execution Call-walking detector misses it. The string 'clone' is built character-by-character at runtime, which hides the literal attribute name from grep-based filters and hands a constructed string to getattr. Approach: in detect_dynamic_execution, after the existing Call-walking pass, do a single BinOp walk over the AST. For each top-level Add BinOp, count chr()-call leaves in the chain via a small recursive helper: - Add(BinOp, BinOp): sum left and right - Call(Name('chr'), ...): 1 - anything else: -1 (disqualifies the chain) If the count is >=3, emit an OBFUSCATED_EXEC match. Below 3 chr leaves or any non-chr leaf disqualifies the chain - this keeps the rule from firing on incidental two-character chr concats, on plain string-literal concatenation ('a' + 'b' + 'c'), and on chr-call-mixed-with-literal shapes that aren't pure obfuscation. Verified: chr_concat_clone (red #17) and chr_concat_exec_call are flagged; two_chr_only, plain_kernel, plain_string_concat_legit stay valid; classic_obfuscated_exec (lzma/b64) still fires through the existing Call-walking pass. --- kernelguard.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/kernelguard.py b/kernelguard.py index f086847..a285f93 100644 --- a/kernelguard.py +++ b/kernelguard.py @@ -2786,6 +2786,56 @@ def detect_dynamic_execution(code: str | SubmissionFacts) -> list[dict]: "evidence": "importlib.reload() (module state reset/manipulation)", }) + # chr()-concatenation obfuscation: building strings character-by-character + # via chr() calls chained with + is essentially never legitimate in a + # compute kernel; it shows up when an attacker is hiding a literal name + # ("clone", "exec", "open", an import path) from grep-based filters and + # then handing the constructed string to getattr / __import__ / exec / eval. + if "CHR_CONCAT_OBFUSCATION" not in seen: + def _count_chr_leaves(expr: ast.AST) -> int: + """Count chr() call leaves in an Add-chain BinOp tree. + + ``chr(99) + chr(108) + ... + chr(101)`` shows up as a left-leaning + BinOp tree of Add ops; this walks it and counts how many leaves + are bare chr(int) calls. Any non-chr leaf or non-Add op makes the + chain disqualifying. + """ + if isinstance(expr, ast.BinOp) and isinstance(expr.op, ast.Add): + left = _count_chr_leaves(expr.left) + if left < 0: + return -1 + right = _count_chr_leaves(expr.right) + if right < 0: + return -1 + return left + right + if ( + isinstance(expr, ast.Call) + and isinstance(expr.func, ast.Name) + and expr.func.id == "chr" + ): + return 1 + return -1 + + for expr in ast.walk(tree): + if not isinstance(expr, ast.BinOp) or not isinstance(expr.op, ast.Add): + continue + # Only look at the outermost Add chain — skip when this BinOp is + # itself a child of an Add BinOp (we'll see the parent first). + count = _count_chr_leaves(expr) + if count < 3: + continue + seen.add("CHR_CONCAT_OBFUSCATION") + matches.append({ + "pattern": "OBFUSCATED_EXEC", + "severity": "critical", + "evidence": ( + f"string built character-by-character via chr() concatenation " + f"({count} chr() leaves) — used to hide a literal name from " + f"grep-based filters before dynamic dispatch" + ), + }) + break + return matches