Skip to content

Commit e858e45

Browse files
committed
big update
1 parent 004fbbf commit e858e45

16 files changed

Lines changed: 1308 additions & 15 deletions
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"""Alias Access Pass — port of Polaris AliasAccess.cpp.
2+
3+
Obscures local variable access through pointer aliasing and multi-level
4+
struct indirection. Original allocas are hidden inside randomly-built
5+
struct types and accessed via a graph of transition nodes with getter
6+
functions, making static analysis of stack variable access much harder.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from dataclasses import dataclass, field
12+
13+
import llvm
14+
15+
from shifting_codes.passes import PassRegistry
16+
from shifting_codes.passes.base import FunctionPass, PassInfo
17+
from shifting_codes.utils.crypto import CryptoRandom
18+
19+
BRANCH_NUM = 6 # Slots per transition node struct
20+
21+
22+
@dataclass
23+
class ElementPos:
24+
struct_type: llvm.Type
25+
index: int
26+
27+
28+
@dataclass
29+
class ReferenceNode:
30+
alloca: llvm.Value # struct alloca for this node
31+
is_raw: bool
32+
node_id: int
33+
raw_insts: dict = field(default_factory=dict) # {orig_alloca: ElementPos}
34+
edges: dict = field(default_factory=dict) # {slot_idx: ReferenceNode}
35+
path: dict = field(default_factory=dict) # {orig_alloca: [slot_indices]}
36+
37+
38+
@PassRegistry.register
39+
class AliasAccessPass(FunctionPass):
40+
41+
def __init__(self, rng: CryptoRandom | None = None):
42+
self.rng = rng or CryptoRandom()
43+
self._counter = 0
44+
45+
@classmethod
46+
def info(cls) -> PassInfo:
47+
return PassInfo(
48+
name="alias_access",
49+
description="[Polaris] Obscure stack variable access via struct indirection",
50+
)
51+
52+
def run_on_function(self, func: llvm.Function, ctx: llvm.Context) -> bool:
53+
# Step 1: Collect allocas
54+
allocas = []
55+
for bb in func.basic_blocks:
56+
for inst in bb.instructions:
57+
if inst.opcode == llvm.Opcode.Alloca:
58+
allocas.append(inst)
59+
60+
if not allocas:
61+
return False
62+
63+
self._counter += 1
64+
tag = self._counter
65+
mod = func.module
66+
ptr_ty = ctx.types.ptr
67+
i32 = ctx.types.i32
68+
i64 = ctx.types.i64
69+
70+
# Step 2: Randomly distribute allocas into buckets
71+
n = len(allocas)
72+
buckets: list[list] = [[] for _ in range(n)]
73+
for a in allocas:
74+
idx = self.rng.get_range(n)
75+
buckets[idx].append(a)
76+
77+
# Step 3: Build raw nodes from non-empty buckets
78+
raw_nodes: list[ReferenceNode] = []
79+
entry_bb = list(func.basic_blocks)[0]
80+
first_inst = list(entry_bb.instructions)[0]
81+
82+
for bucket in buckets:
83+
if not bucket:
84+
continue
85+
86+
# Create struct with alloca types at random positions + pointer padding
87+
field_count = 2 * len(bucket) + 1
88+
field_types = [ptr_ty] * field_count
89+
positions: dict = {}
90+
91+
for alloca in bucket:
92+
# Pick random unused position
93+
while True:
94+
pos = self.rng.get_range(field_count)
95+
if pos not in positions:
96+
break
97+
field_types[pos] = alloca.allocated_type
98+
positions[alloca] = pos
99+
100+
struct_ty = ctx.types.struct(field_types)
101+
102+
# Allocate the struct in entry block
103+
with entry_bb.create_builder() as builder:
104+
builder.position_before(first_inst)
105+
struct_alloca = builder.alloca(struct_ty, name=f"aa.raw.{tag}.{len(raw_nodes)}")
106+
107+
node = ReferenceNode(
108+
alloca=struct_alloca,
109+
is_raw=True,
110+
node_id=len(raw_nodes),
111+
)
112+
for alloca, pos in positions.items():
113+
node.raw_insts[alloca] = ElementPos(struct_type=struct_ty, index=pos)
114+
raw_nodes.append(node)
115+
116+
# Step 4: Build transition nodes
117+
trans_struct_ty = ctx.types.struct([ptr_ty] * BRANCH_NUM)
118+
trans_count = len(raw_nodes) * 3
119+
all_nodes: list[ReferenceNode] = list(raw_nodes)
120+
trans_nodes: list[ReferenceNode] = []
121+
122+
for t in range(trans_count):
123+
with entry_bb.create_builder() as builder:
124+
builder.position_before(first_inst)
125+
trans_alloca = builder.alloca(
126+
trans_struct_ty, name=f"aa.trans.{tag}.{t}",
127+
)
128+
129+
node = ReferenceNode(
130+
alloca=trans_alloca,
131+
is_raw=False,
132+
node_id=len(all_nodes),
133+
)
134+
135+
# Randomly connect to existing nodes
136+
num_edges = self.rng.get_range(BRANCH_NUM) + 1
137+
available_slots = list(range(BRANCH_NUM))
138+
for _ in range(min(num_edges, len(all_nodes))):
139+
if not available_slots:
140+
break
141+
slot = available_slots.pop(self.rng.get_range(len(available_slots)))
142+
target = all_nodes[self.rng.get_range(len(all_nodes))]
143+
node.edges[slot] = target
144+
145+
# Store pointer to target node in the transition struct
146+
with entry_bb.create_builder() as builder:
147+
builder.position_before(first_inst)
148+
slot_ptr = builder.gep(
149+
trans_struct_ty, trans_alloca,
150+
[i32.constant(0), i32.constant(slot)],
151+
f"aa.edge.{tag}.{t}.{slot}",
152+
)
153+
builder.store(target.alloca, slot_ptr)
154+
155+
# Propagate paths
156+
if target.is_raw:
157+
for orig_alloca in target.raw_insts:
158+
if orig_alloca not in node.path:
159+
node.path[orig_alloca] = [slot]
160+
# else: already have a path, keep first
161+
else:
162+
for orig_alloca, path in target.path.items():
163+
if orig_alloca not in node.path:
164+
node.path[orig_alloca] = [slot] + path
165+
166+
trans_nodes.append(node)
167+
all_nodes.append(node)
168+
169+
# Step 5: Create getter functions (one per slot index)
170+
getter_funcs: dict[int, llvm.Function] = {}
171+
for slot_idx in range(BRANCH_NUM):
172+
getter_name = f"__obfu_aa_getter_{tag}_{slot_idx}"
173+
getter_fn_ty = ctx.types.function(ptr_ty, [ptr_ty])
174+
getter = mod.add_function(getter_name, getter_fn_ty)
175+
getter.linkage = llvm.Linkage.Private
176+
177+
getter_entry = getter.append_basic_block("entry")
178+
with getter_entry.create_builder() as builder:
179+
gep = builder.gep(
180+
trans_struct_ty, getter.get_param(0),
181+
[i32.constant(0), i32.constant(slot_idx)],
182+
"aa.getter.gep",
183+
)
184+
loaded = builder.load(ptr_ty, gep, "aa.getter.load")
185+
builder.ret(loaded)
186+
187+
getter_funcs[slot_idx] = getter
188+
189+
# Step 6: Replace operand references
190+
for orig_alloca in allocas:
191+
uses = list(orig_alloca.uses)
192+
for use in uses:
193+
user_inst = use.user
194+
operand_idx = use.operand_index
195+
196+
# Find a node that has a path to this alloca
197+
# Prefer transition nodes (more indirection)
198+
source_node = None
199+
for node in trans_nodes:
200+
if orig_alloca in node.path:
201+
source_node = node
202+
break
203+
if source_node is None:
204+
# Fall back to raw node
205+
for node in raw_nodes:
206+
if orig_alloca in node.raw_insts:
207+
source_node = node
208+
break
209+
if source_node is None:
210+
continue
211+
212+
with user_inst.block.create_builder() as builder:
213+
builder.position_before(user_inst)
214+
215+
if source_node.is_raw:
216+
# Direct GEP to the element
217+
elem = source_node.raw_insts[orig_alloca]
218+
replacement = builder.gep(
219+
elem.struct_type, source_node.alloca,
220+
[i32.constant(0), i32.constant(elem.index)],
221+
"aa.direct",
222+
)
223+
else:
224+
# Walk the path: call getters to traverse graph
225+
path = source_node.path[orig_alloca]
226+
current_ptr = source_node.alloca
227+
for slot_idx in path:
228+
getter = getter_funcs[slot_idx]
229+
current_ptr = builder.call(
230+
getter, [current_ptr], "aa.walk",
231+
)
232+
233+
# current_ptr is now the raw node; GEP to element
234+
# Find which raw node we ended up at
235+
target_node = source_node
236+
for slot_idx in path:
237+
target_node = target_node.edges[slot_idx]
238+
239+
if target_node.is_raw and orig_alloca in target_node.raw_insts:
240+
elem = target_node.raw_insts[orig_alloca]
241+
replacement = builder.gep(
242+
elem.struct_type, current_ptr,
243+
[i32.constant(0), i32.constant(elem.index)],
244+
"aa.elem",
245+
)
246+
else:
247+
# Fallback: shouldn't happen with correct paths
248+
continue
249+
250+
user_inst.set_operand(operand_idx, replacement)
251+
252+
# Step 7: Erase original allocas (all uses should be replaced)
253+
for alloca in allocas:
254+
if not alloca.has_uses:
255+
alloca.erase_from_parent()
256+
257+
return True

src/shifting_codes/passes/bogus_control_flow.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
from shifting_codes.utils.crypto import CryptoRandom
1515

1616

17+
def _safe_remap_operands(inst, value_map: dict):
18+
"""Remap operands using try/except for nanobind cross-type __eq__ safety."""
19+
for i in range(inst.num_operands):
20+
op = inst.get_operand(i)
21+
try:
22+
if op in value_map:
23+
inst.set_operand(i, value_map[op])
24+
except TypeError:
25+
pass
26+
27+
1728
def _clone_basic_block(body_bb: llvm.BasicBlock, func: llvm.Function,
1829
ctx: llvm.Context, tag: int) -> llvm.BasicBlock:
1930
"""Clone a basic block's instructions into a new block.
@@ -35,10 +46,7 @@ def _clone_basic_block(body_bb: llvm.BasicBlock, func: llvm.Function,
3546

3647
# Remap operands: replace references to original values with cloned ones
3748
for inst in clone_bb.instructions:
38-
for i in range(inst.num_operands):
39-
op = inst.get_operand(i)
40-
if op in value_map:
41-
inst.set_operand(i, value_map[op])
49+
_safe_remap_operands(inst, value_map)
4250

4351
return clone_bb
4452

@@ -79,7 +87,7 @@ def __init__(self, rng: CryptoRandom | None = None):
7987
def info(cls) -> PassInfo:
8088
return PassInfo(
8189
name="bogus_control_flow",
82-
description="Insert opaque predicates and bogus branches",
90+
description="[Pluto] Insert opaque predicates and bogus branches",
8391
)
8492

8593
def run_on_function(self, func: llvm.Function, ctx: llvm.Context) -> bool:
@@ -113,7 +121,7 @@ def run_on_function(self, func: llvm.Function, ctx: llvm.Context) -> bool:
113121
continue
114122

115123
# If block has only a terminator, skip (nothing to split)
116-
if first_non_phi.is_terminator_inst:
124+
if first_non_phi.is_terminator:
117125
continue
118126

119127
# Split: headBB → bodyBB (at first non-PHI)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Custom Calling Convention Pass — port of Polaris CustomCC.cpp.
2+
3+
Randomly assigns non-standard calling conventions to internal functions
4+
and their call sites, breaking decompiler assumptions about register
5+
usage, stack frame layout, and argument passing.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import llvm
11+
12+
from shifting_codes.passes import PassRegistry
13+
from shifting_codes.passes.base import ModulePass, PassInfo
14+
from shifting_codes.utils.crypto import CryptoRandom
15+
16+
# Pool of standard non-C calling conventions that change ABI behaviour.
17+
_CC_POOL = [
18+
llvm.CallConv.Fast,
19+
llvm.CallConv.Cold,
20+
llvm.CallConv.PreserveMost,
21+
llvm.CallConv.PreserveAll,
22+
llvm.CallConv.X86RegCall,
23+
llvm.CallConv.X86_64_SysV,
24+
llvm.CallConv.Win64,
25+
]
26+
27+
28+
@PassRegistry.register
29+
class CustomCCPass(ModulePass):
30+
31+
def __init__(self, rng: CryptoRandom | None = None):
32+
self.rng = rng or CryptoRandom()
33+
34+
@classmethod
35+
def info(cls) -> PassInfo:
36+
return PassInfo(
37+
name="custom_cc",
38+
description="[Polaris] Randomly assign non-standard calling conventions",
39+
is_module_pass=True,
40+
)
41+
42+
def run_on_module(self, mod: llvm.Module, ctx: llvm.Context) -> bool:
43+
# Collect internal/private functions with bodies
44+
internal_funcs = []
45+
for func in mod.functions:
46+
if func.is_declaration:
47+
continue
48+
if func.linkage in (llvm.Linkage.Internal, llvm.Linkage.Private):
49+
internal_funcs.append(func)
50+
51+
if not internal_funcs:
52+
return False
53+
54+
for func in internal_funcs:
55+
cc = _CC_POOL[self.rng.get_range(len(_CC_POOL))]
56+
func.calling_conv = cc
57+
58+
# Fix all call sites targeting this function
59+
func_name = func.name
60+
for caller in mod.functions:
61+
for bb in caller.basic_blocks:
62+
for inst in bb.instructions:
63+
if inst.opcode == llvm.Opcode.Call:
64+
called = inst.get_operand(inst.num_operands - 1)
65+
if hasattr(called, 'name') and called.name == func_name:
66+
inst.instruction_call_conv = cc
67+
68+
return True

src/shifting_codes/passes/flattening.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self, rng: CryptoRandom | None = None):
2424
def info(cls) -> PassInfo:
2525
return PassInfo(
2626
name="flattening",
27-
description="Control flow flattening via switch dispatcher",
27+
description="[Pluto] Control flow flattening via switch dispatcher",
2828
)
2929

3030
def run_on_function(self, func: llvm.Function, ctx: llvm.Context) -> bool:

src/shifting_codes/passes/global_encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def __init__(self, rng: CryptoRandom | None = None):
2323
def info(cls) -> PassInfo:
2424
return PassInfo(
2525
name="global_encryption",
26-
description="XOR-encrypt internal global variables",
26+
description="[Pluto] XOR-encrypt internal global variables",
2727
is_module_pass=True,
2828
)
2929

0 commit comments

Comments
 (0)