Skip to content

Commit e1fb03a

Browse files
committed
Initial commit
1 parent 46afeca commit e1fb03a

22 files changed

Lines changed: 2719 additions & 387 deletions

CLAUDE.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project
6+
7+
Python port of Pluto LLVM obfuscation passes using llvm-nanobind bindings. Six passes transform LLVM IR to obfuscate code: Substitution, MBA Obfuscation, Bogus Control Flow, Flattening, Global Encryption, and Indirect Call.
8+
9+
## Commands
10+
11+
```bash
12+
# Run all tests
13+
python -m uv run pytest tests/ -v
14+
15+
# Run a single test file
16+
python -m uv run pytest tests/test_substitution.py -v
17+
18+
# Run a single test by name
19+
python -m uv run pytest tests/test_substitution.py -k "test_add_substitution" -v
20+
21+
# Run UI (requires llvm-nanobind built)
22+
python -m uv run python -m shifting_codes.ui.app
23+
```
24+
25+
## Dependencies
26+
27+
- **llvm-nanobind**: Local editable dependency at `../llvm-nanobind/build/` (must be built separately before tests/UI work)
28+
- **z3-solver**: Constraint solving for MBA coefficient generation
29+
- **PyQt6**: GUI framework (UI not yet tested)
30+
- Python 3.12+ required, managed with UV + hatchling build backend
31+
32+
## Architecture
33+
34+
### Pass System
35+
36+
All passes inherit from `FunctionPass` or `ModulePass` (in `src/shifting_codes/passes/base.py`) and are auto-registered via `@PassRegistry.register` decorator. Each pass implements `run_on_function(func, ctx)` or `run_on_module(mod, ctx)` returning a bool indicating modification.
37+
38+
Passes are composed via `PassPipeline` (in `src/shifting_codes/passes/__init__.py`):
39+
```python
40+
pipeline = PassPipeline()
41+
pipeline.add(SubstitutionPass(rng=CryptoRandom(seed=42)))
42+
pipeline.run(mod, ctx)
43+
```
44+
45+
**FunctionPasses:** Substitution, MBAObfuscation, BogusControlFlow, Flattening
46+
**ModulePasses:** GlobalEncryption, IndirectCall
47+
48+
### Utilities (`src/shifting_codes/utils/`)
49+
50+
- **`crypto.py`**`CryptoRandom`: wraps `secrets` (production) or `random.Random(seed)` (testing). All passes accept an `rng` parameter for determinism.
51+
- **`mba.py`** — Z3-based MBA coefficient generation with result caching. Generates linear (15 truth tables) and univariate polynomial expressions.
52+
- **`ir_helpers.py`** — PHI/register demotion to stack (`demote_phi_to_stack`, `demote_regs_to_stack`), used by Flattening pass.
53+
54+
### XTEA (`src/shifting_codes/xtea/`)
55+
56+
Reference XTEA cipher implementation (pure Python) plus an LLVM IR builder that constructs the same cipher using the nanobind Builder API. Used for end-to-end testing: build IR → apply all passes → compile → execute via ctypes → verify against reference.
57+
58+
### Test Fixtures (`tests/conftest.py`)
59+
60+
- `ctx`: Fresh LLVM context per test
61+
- `rng`: Seeded `CryptoRandom(seed=42)` for deterministic tests
62+
- Helper functions: `make_add_function()`, `make_arith_function()`, `make_branch_function()`, `make_loop_function()`
63+
64+
## llvm-nanobind API Pitfalls
65+
66+
- `ctx.types.ptr`, `ctx.types.i32`, `ctx.types.void` are **properties** (not methods)
67+
- `ctx.create_module("name")` returns a context manager: `with ctx.create_module("name") as mod:`
68+
- `inst.block` for parent block (not `.parent`)
69+
- `gv.global_value_type` for content type (not `gv.type` which returns pointer type)
70+
- `call_inst.called_value` is read-only — to change call target, rebuild the call instruction
71+
- `builder.call(func, args, name)` for direct calls; `builder.call(func_ty, ptr, args, name)` for indirect calls
72+
- `mod.target_triple = "..."` (not `mod.triple`)
73+
- `func.dll_storage_class = llvm.DLLExport` required for Windows DLL exports
74+
- Integer constants must be masked to bit width: `key & ((1 << vtype.int_width) - 1)`
75+
- ConstantDataArray element access via `get_operand()` crashes — avoid array encryption
76+
- PHI nodes need `inst.add_incoming(value, pred_bb)` when new predecessors are added
77+
- Z3 non-determinism: bound coefficients (`-10 <= X[i] <= 10`) and set `smt.random_seed`

NANOBIND_ISSUES.md

Lines changed: 0 additions & 136 deletions
This file was deleted.

README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Shifting Codes
2+
3+
Python port of [Pluto](https://github.com/bluesadi/Pluto) LLVM obfuscation passes using [llvm-nanobind](https://github.com/nicovank/llvm-nanobind) bindings, with a PyQt6 visualization UI.
4+
5+
![](assets/UI-showcase.gif)
6+
7+
## Passes
8+
9+
Six obfuscation passes are available:
10+
11+
| Pass | Type | Description |
12+
|------|------|-------------|
13+
| **Substitution** | Function | Replaces arithmetic operations with equivalent but obscure sequences |
14+
| **MBA Obfuscation** | Function | Applies Mixed Boolean-Arithmetic transformations using Z3-generated coefficients |
15+
| **Bogus Control Flow** | Function | Inserts opaque predicates and dead code paths |
16+
| **Flattening** | Function | Transforms control flow into a switch-based dispatch loop |
17+
| **Global Encryption** | Module | XOR-encrypts global variable initializers with runtime decryption stubs |
18+
| **Indirect Call** | Module | Replaces direct function calls with indirect calls through function pointers |
19+
20+
## Prerequisites
21+
22+
- **Python 3.12+**
23+
- **[UV](https://docs.astral.sh/uv/)** package manager
24+
- **llvm-nanobind** — must be cloned and built separately (see below)
25+
26+
## Installation
27+
28+
1. **Install UV** (if not already installed):
29+
30+
```bash
31+
pip install uv
32+
```
33+
34+
2. **Clone and build llvm-nanobind** as a sibling directory:
35+
36+
```bash
37+
git clone https://github.com/LLVMParty/llvm-nanobind ../llvm-nanobind
38+
cd ../llvm-nanobind
39+
# Follow llvm-nanobind's build instructions to produce the build/ directory
40+
cd -
41+
```
42+
43+
The project expects the built package at `../llvm-nanobind/build/`.
44+
45+
3. **Install the project**:
46+
47+
```bash
48+
uv sync
49+
```
50+
51+
## Usage
52+
53+
```python
54+
import llvm
55+
from shifting_codes.passes import PassPipeline
56+
from shifting_codes.passes.substitution import SubstitutionPass
57+
from shifting_codes.passes.mba_obfuscation import MBAObfuscationPass
58+
from shifting_codes.passes.bogus_control_flow import BogusControlFlowPass
59+
from shifting_codes.passes.flattening import FlatteningPass
60+
from shifting_codes.passes.global_encryption import GlobalEncryptionPass
61+
from shifting_codes.passes.indirect_call import IndirectCallPass
62+
from shifting_codes.utils.crypto import CryptoRandom
63+
64+
rng = CryptoRandom(seed=42)
65+
66+
pipeline = PassPipeline()
67+
pipeline.add(SubstitutionPass(rng=rng))
68+
pipeline.add(MBAObfuscationPass(rng=rng))
69+
pipeline.add(BogusControlFlowPass(rng=rng))
70+
pipeline.add(FlatteningPass(rng=rng))
71+
pipeline.add(GlobalEncryptionPass(rng=rng))
72+
pipeline.add(IndirectCallPass(rng=rng))
73+
74+
# Apply to a module
75+
pipeline.run(mod, ctx)
76+
```
77+
78+
Passes are registered via `@PassRegistry.register` and can be looked up by name:
79+
80+
```python
81+
from shifting_codes.passes import PassRegistry
82+
83+
cls = PassRegistry.get("substitution")
84+
all_passes = PassRegistry.all_passes()
85+
```
86+
87+
## Running Tests
88+
89+
```bash
90+
# All tests
91+
python -m uv run pytest tests/ -v
92+
93+
# Single test file
94+
python -m uv run pytest tests/test_substitution.py -v
95+
96+
# Single test by name
97+
python -m uv run pytest tests/test_substitution.py -k "test_add_substitution" -v
98+
```
99+
100+
## UI
101+
102+
Launch the PyQt6 visualization GUI:
103+
104+
```bash
105+
python -m uv run python -m shifting_codes.ui.app
106+
```
107+
108+
## Project Structure
109+
110+
```
111+
src/shifting_codes/
112+
passes/ # Obfuscation passes (base classes, registry, pipeline)
113+
utils/ # Shared utilities (crypto RNG, MBA solver, IR helpers)
114+
xtea/ # XTEA cipher — pure Python reference + LLVM IR builder
115+
ui/ # PyQt6 GUI for visualizing pass transformations
116+
tests/ # pytest test suite
117+
```

assets/UI-showcase.gif

2.21 MB
Loading

src/shifting_codes/passes/__init__.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
from shifting_codes.passes.base import FunctionPass, ModulePass, PassInfo
88

9+
# Enum attribute kind IDs (stable within an LLVM version).
10+
# optnone: tells the optimizer to skip this function entirely.
11+
# noinline: required alongside optnone (LLVM verifier rejects optnone without it).
12+
_ATTR_NOINLINE = 32
13+
_ATTR_OPTNONE = 49
14+
915

1016
class PassRegistry:
1117
"""Registry of available obfuscation passes."""
@@ -36,13 +42,45 @@ def __init__(self, passes: list[FunctionPass | ModulePass] | None = None):
3642
def add(self, p: FunctionPass | ModulePass) -> None:
3743
self.passes.append(p)
3844

39-
def run(self, mod: llvm.Module, ctx: llvm.Context) -> bool:
45+
def run(
46+
self,
47+
mod: llvm.Module,
48+
ctx: llvm.Context,
49+
selected_functions: set[str] | None = None,
50+
) -> bool:
51+
"""Run all passes on the module.
52+
53+
Args:
54+
mod: The LLVM module to transform.
55+
ctx: The LLVM context.
56+
selected_functions: If None, all functions receive FunctionPasses.
57+
If a set, only named functions receive FunctionPasses.
58+
ModulePasses always apply to the entire module regardless.
59+
"""
4060
changed = False
61+
obfuscated_functions: set[str] = set()
4162
for p in self.passes:
4263
if isinstance(p, ModulePass):
4364
changed |= p.run_on_module(mod, ctx)
4465
elif isinstance(p, FunctionPass):
4566
for func in mod.functions:
46-
if not func.is_declaration:
47-
changed |= p.run_on_function(func, ctx)
67+
if func.is_declaration:
68+
continue
69+
if selected_functions is not None and func.name not in selected_functions:
70+
continue
71+
if p.run_on_function(func, ctx):
72+
changed = True
73+
obfuscated_functions.add(func.name)
74+
75+
# Stamp obfuscated functions with optnone + noinline so that
76+
# downstream compilers (e.g. clang -O2) cannot strip obfuscation.
77+
if obfuscated_functions:
78+
optnone = ctx.create_enum_attribute(_ATTR_OPTNONE, 0)
79+
noinline = ctx.create_enum_attribute(_ATTR_NOINLINE, 0)
80+
fn_idx = llvm.AttributeFunctionIndex
81+
for func in mod.functions:
82+
if func.name in obfuscated_functions:
83+
func.add_attribute(fn_idx, optnone)
84+
func.add_attribute(fn_idx, noinline)
85+
4886
return changed

0 commit comments

Comments
 (0)