scientific-method-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. scientific_method_engine/__init__.py +5 -0
  2. scientific_method_engine/__main__.py +3 -0
  3. scientific_method_engine/cli.py +75 -0
  4. scientific_method_engine/ghidra/ClearNoReturnFunctions.java +22 -0
  5. scientific_method_engine/ghidra/CreateFunctions.java +27 -0
  6. scientific_method_engine/ghidra/ExportBoundedFlow.java +59 -0
  7. scientific_method_engine/ghidra/ExportFunctionFingerprints.java +135 -0
  8. scientific_method_engine/ghidra/ExportFunctionInventory.java +50 -0
  9. scientific_method_engine/ghidra/MergeFallThroughFragment.java +63 -0
  10. scientific_method_engine/ghidra/RecoverCitedFunctions.java +93 -0
  11. scientific_method_engine/ghidra/RepairReturningCallers.java +76 -0
  12. scientific_method_engine/ghidra/ReportCallArguments.java +65 -0
  13. scientific_method_engine/ghidra/ReportCallPaths.java +106 -0
  14. scientific_method_engine/ghidra/ReportCallSitesWithScalars.java +95 -0
  15. scientific_method_engine/ghidra/ReportCallsToRange.java +67 -0
  16. scientific_method_engine/ghidra/ReportConstantFirstArgumentCalls.java +55 -0
  17. scientific_method_engine/ghidra/ReportDataBytes.java +36 -0
  18. scientific_method_engine/ghidra/ReportDecompileMatches.java +71 -0
  19. scientific_method_engine/ghidra/ReportDecompileWindow.java +61 -0
  20. scientific_method_engine/ghidra/ReportFilePatternInMemory.java +102 -0
  21. scientific_method_engine/ghidra/ReportFirstArgumentCallSummary.java +63 -0
  22. scientific_method_engine/ghidra/ReportFunctionScalarConstants.java +56 -0
  23. scientific_method_engine/ghidra/ReportFunctionSummary.java +64 -0
  24. scientific_method_engine/ghidra/ReportInstructionContext.java +64 -0
  25. scientific_method_engine/ghidra/ReportInstructionWindow.java +36 -0
  26. scientific_method_engine/ghidra/ReportMemoryBlockForFileOffset.java +105 -0
  27. scientific_method_engine/ghidra/ReportMemoryBlocks.java +52 -0
  28. scientific_method_engine/ghidra/ReportRandomnessCandidates.java +74 -0
  29. scientific_method_engine/ghidra/ReportReferences.java +42 -0
  30. scientific_method_engine/ghidra/ReportScalarConstants.java +55 -0
  31. scientific_method_engine/ghidra/ReportStringReferences.java +102 -0
  32. scientific_method_engine/ghidra/ReportSymbolReferences.java +72 -0
  33. scientific_method_engine/x86/__init__.py +0 -0
  34. scientific_method_engine/x86/dispatch.py +51 -0
  35. scientific_method_engine/x86/image.py +172 -0
  36. scientific_method_engine/x86/machine.py +659 -0
  37. scientific_method_engine/x86/pe.py +96 -0
  38. scientific_method_engine/x86/reports.py +1259 -0
  39. scientific_method_engine/x86/trace.py +547 -0
  40. scientific_method_engine/x86/values.py +123 -0
  41. scientific_method_engine-0.1.0.dist-info/METADATA +110 -0
  42. scientific_method_engine-0.1.0.dist-info/RECORD +45 -0
  43. scientific_method_engine-0.1.0.dist-info/WHEEL +4 -0
  44. scientific_method_engine-0.1.0.dist-info/entry_points.txt +2 -0
  45. scientific_method_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,659 @@
1
+ """Path-specific instruction effects. Unsupported semantics stop the path."""
2
+ from copy import deepcopy
3
+ from capstone.x86 import X86_OP_REG, X86_OP_IMM, X86_OP_MEM
4
+ from .values import Value, const, unknown, op, extract, join, resize, sources, address_parts
5
+
6
+ REGISTERS = ("eax", "ebx", "ecx", "edx", "esi", "edi", "ebp", "esp", "cs", "ds", "es", "ss", "fs", "gs")
7
+ ALIASES = {}
8
+ for root in REGISTERS:
9
+ ALIASES[root] = (root, 0, 16 if len(root) == 2 else 32)
10
+ if root.startswith("e") and len(root) == 3:
11
+ ALIASES[root[1:]] = (root, 0, 16)
12
+ for name in ("a", "b", "c", "d"):
13
+ ALIASES[name + "l"] = ("e" + name + "x", 0, 8)
14
+ ALIASES[name + "h"] = ("e" + name + "x", 8, 8)
15
+
16
+
17
+ class StopPath(Exception):
18
+ pass
19
+
20
+
21
+ def segment_register(ins, mem):
22
+ """Name the segment register an explicit memory operand uses (override or stack/data default)."""
23
+ if mem.segment:
24
+ return ins.reg_name(mem.segment)
25
+ base = ins.reg_name(mem.base) if mem.base else None
26
+ index = ins.reg_name(mem.index) if mem.index else None
27
+ return "ss" if base in ("bp", "sp", "ebp", "esp") or index == "bp" else "ds"
28
+
29
+
30
+ def alias(name):
31
+ # Registers outside the modeled set (control, debug, FPU, ...) stop the path instead of failing the report.
32
+ try:
33
+ return ALIASES[name]
34
+ except KeyError:
35
+ raise StopPath("Unsupported register: " + name) from None
36
+
37
+
38
+ class State:
39
+ def __init__(self, entry, image, config):
40
+ self.bits, self.flat, self.mask = image.bits, image.flat, image.mask
41
+ self.sp, self.bp = ("esp", "ebp") if self.flat else ("sp", "bp")
42
+ self.at = entry
43
+ self.regs = {r: unknown("initial:" + r, ALIASES[r][2]) for r in REGISTERS}
44
+ self.regs["esp"] = resize(unknown("entry:sp", self.bits), 32)
45
+ self.setreg(self.sp, unknown("entry:sp", self.bits), None)
46
+ self.segment_bases = {r: const(0, 32) if r in ("cs", "ds", "es", "ss") else unknown("initial-base:" + r, 32)
47
+ for r in ("cs", "ds", "es", "ss", "fs", "gs")}
48
+ self.setreg("cs", const(image.region(entry)["segment"], 16), None)
49
+ for r, n in config.get("registers", {}).items():
50
+ if r not in ALIASES or type(n) is not int or not 0 <= n < 1 << ALIASES[r][2]:
51
+ raise ValueError("Invalid initial register value")
52
+ self.setreg(r, const(n, ALIASES[r][2]), None)
53
+ self.memory = {}
54
+ # Keys grouped by (segment, base) so a write scans only groups that can alias it.
55
+ self.memory_groups = {}
56
+ self.memory_epoch = 0
57
+ self.events = []
58
+ self.guards = []
59
+ self.assumptions = {}
60
+ self.flags = None
61
+ # CF when an instruction sets it without leaving a comparable flag producer; None defers to flags.
62
+ self.carry = None
63
+ self.flag_serial = 0
64
+ self.flag_epoch = 0
65
+ self.direction_flag = unknown("initial:DF", 1)
66
+ self.interrupt_flag = unknown("initial:IF", 1)
67
+ self.saved_flags = {}
68
+ self.unknown_flag_site = None
69
+ self.frames = [{"entry": entry, "sp": self.reg(self.sp), "returnBytes": config.get("returnBytes", 4 if self.flat else 2)}]
70
+ self.steps = 0
71
+ self.visits = {}
72
+ self.path = []
73
+ self.conditional = []
74
+ flags = config.get("flags", {})
75
+ if not isinstance(flags, dict) or set(flags) - {"direction"}:
76
+ raise ValueError("Only an explicit starting direction flag is supported")
77
+ if "direction" in flags:
78
+ if type(flags["direction"]) is not int or flags["direction"] not in (0, 1):
79
+ raise ValueError("Starting direction flag must be zero or one")
80
+ self.direction_flag = const(flags["direction"], 1)
81
+ self.event("flag-assumption", flag="DF", value=self.direction_flag.report(),
82
+ evidence="explicit query starting hypothesis, not native state")
83
+
84
+ def forget_flags(self, keep_carry=False):
85
+ carry = self.carry_value() if keep_carry else None
86
+ self.flags = None
87
+ self.carry = carry
88
+ self.flag_serial += 1
89
+ self.flag_epoch = self.flag_serial
90
+ self.unknown_flag_site = self.at
91
+
92
+ def save_flags(self, bits):
93
+ word = unknown(f"saved-flags:{self.at}:{len(self.events)}", bits, self.at)
94
+ self.saved_flags[(bits, word.term)] = (self.flags, self.flag_epoch, self.unknown_flag_site,
95
+ self.direction_flag, self.interrupt_flag, self.carry)
96
+ self.push(word)
97
+ self.event("flags-save", width=bits // 8, value=word.report(),
98
+ direction=self.direction_flag.report(), interrupt=self.interrupt_flag.report())
99
+
100
+ def restore_flags(self, bits):
101
+ word = self.pop(bits // 8)
102
+ saved = self.saved_flags.get((bits, word.term))
103
+ if saved is None:
104
+ self.forget_flags()
105
+ self.carry = extract(word, 0, 1)
106
+ self.direction_flag = extract(word, 10, 1)
107
+ self.interrupt_flag = extract(word, 9, 1)
108
+ else:
109
+ self.flags, self.flag_epoch, self.unknown_flag_site, self.direction_flag, self.interrupt_flag, self.carry = saved
110
+ self.event("flags-restore", width=bits // 8, value=word.report(), intactLocalSnapshot=saved is not None,
111
+ direction=self.direction_flag.report(), interrupt=self.interrupt_flag.report())
112
+
113
+ def carry_value(self):
114
+ """CF as a one-bit value: from the last comparable flag producer, an explicit carry, or unknown."""
115
+ if self.flags is not None:
116
+ answer, _ = predicate(self, "jb")
117
+ if answer is not None:
118
+ return const(int(answer), 1, self.flags[3])
119
+ # Name the carry by its producer's operands, so every reading of one comparison shares an assumption.
120
+ a, b, operation, site = self.flags
121
+ return unknown(f"carry:{site}:{(operation, a.term, b.term)!r}", 1, site)
122
+ if self.carry is not None:
123
+ return self.carry
124
+ return unknown(f"carry:unresolved:{self.flag_epoch}", 1, self.unknown_flag_site)
125
+
126
+ def set_flags(self, a, b, operation):
127
+ self.flags = (a, b, operation, self.at)
128
+ self.carry = None
129
+
130
+ def clear_memory(self):
131
+ self.memory.clear()
132
+ self.memory_groups.clear()
133
+ self.memory_epoch += 1
134
+
135
+ def reg(self, name):
136
+ root, low, bits = alias(name)
137
+ return extract(self.regs[root], low, bits)
138
+
139
+ def setreg(self, name, value, site):
140
+ root, low, bits = alias(name)
141
+ value = resize(value, bits)
142
+ value = Value(bits, value.term, sources(value, site=site))
143
+ if bits == ALIASES[root][2]:
144
+ self.regs[root] = value
145
+ else:
146
+ old = self.regs[root]
147
+ chunks = [extract(old, n, 8) for n in range(0, old.bits, 8)]
148
+ chunks[low // 8:(low + bits) // 8] = [extract(value, n, 8) for n in range(0, bits, 8)]
149
+ self.regs[root] = join(chunks)
150
+
151
+ def event(self, kind, **fields):
152
+ event = {"kind": kind, "site": self.at, "entry": self.frames[-1]["entry"],
153
+ "depth": len(self.frames) - 1, "order": len(self.events), **fields}
154
+ self.events.append(event)
155
+ return event
156
+
157
+ def segment(self, name):
158
+ # PE selectors are not real-mode paragraph bases. FS/GS bases remain unknown.
159
+ return self.segment_bases[name] if self.flat else self.reg(name)
160
+
161
+ def location(self, segment, offset):
162
+ # Segment aliases resolve only when both components are concrete.
163
+ if segment.number is not None and offset.number is not None:
164
+ return ("linear",), ("absolute",), segment.number * (1 if self.flat else 16) + offset.number
165
+ base, delta = address_parts(offset)
166
+ return segment.term, base, delta
167
+
168
+ def keys(self, segment, offset, width):
169
+ if offset.number is not None and offset.number + width > 1 << self.bits:
170
+ raise StopPath("Memory access crosses the address boundary")
171
+ seg, base, delta = self.location(segment, offset)
172
+ return seg, base, delta, [(seg, base, (delta + i) if seg == ("linear",) else (delta + i) % (1 << self.bits)) for i in range(width)]
173
+
174
+ def peek(self, segment, offset, width):
175
+ """Inspect modeled memory without reporting an access the program never performed."""
176
+ _, _, _, keys = self.keys(segment, offset, width)
177
+ return join([self.memory.get(key, unknown(f"memory:{self.memory_epoch}:{key}", 8, self.at)) for key in keys])
178
+
179
+ def access(self, segment, offset, width, write=None, role=None, addressing_register=None):
180
+ seg, base, delta, keys = self.keys(segment, offset, width)
181
+ uncertain = []
182
+ if write is not None:
183
+ write = Value(write.bits, write.term, sources(write, site=self.at))
184
+ self.memory_epoch += 1
185
+
186
+ def domain(k):
187
+ if k[0] == ("linear",):
188
+ return k[2], k[2] + 1
189
+ if k[0][0] == "constant":
190
+ start = k[0][1] * (1 if self.flat else 16)
191
+ return start, start + (1 << self.bits)
192
+ return None
193
+ # A concrete write covers every byte it stores, not only its first byte.
194
+ written = (keys[0][2], keys[-1][2] + 1) if seg == ("linear",) else domain(keys[0])
195
+ for group, members in list(self.memory_groups.items()):
196
+ if group == (seg, base):
197
+ continue
198
+ for key in list(members):
199
+ # Different symbolic segments/bases may alias. Concrete linear locations do not.
200
+ a, b = domain(key), written
201
+ disjoint = a is not None and b is not None and (a[1] <= b[0] or b[1] <= a[0])
202
+ if not disjoint:
203
+ uncertain.append(key)
204
+ del self.memory[key]
205
+ members.discard(key)
206
+ if not members:
207
+ del self.memory_groups[group]
208
+ for i, key in enumerate(keys):
209
+ self.memory[key] = extract(write, i * 8, 8)
210
+ self.memory_groups.setdefault((seg, base), set()).update(keys)
211
+ value = write
212
+ missing = []
213
+ else:
214
+ missing = [i for i, key in enumerate(keys) if key not in self.memory]
215
+ value = join([self.memory.get(key, unknown(f"memory:{self.memory_epoch}:{key}", 8, self.at)) for key in keys])
216
+ relevant = []
217
+ for g in self.guards:
218
+ left = g.get("left", {}).get("expression")
219
+ right = g.get("right", {}).get("value")
220
+ if right == 0:
221
+ same = left == offset.term
222
+ relevant.append({"site": g["site"], "taken": g["taken"], "predicate": g["predicate"],
223
+ "samePointerValue": same,
224
+ "assessment": "same expression; inspect predicate polarity" if same else "checked value differs from this access"})
225
+ event = self.event("write" if write is not None else "read", segment=segment.report(), offset=offset.report(),
226
+ width=width, effectiveSegmentRegister=addressing_register, segmentInterpretation="base" if self.flat else "selector-paragraph", interval={"segment": seg, "base": base, "start": delta, "end": delta + width},
227
+ value=value.report(), missingByteProducers=missing,
228
+ byteProducers=[{"index": i, "producers": list(self.memory[key].sources) if key in self.memory else []} for i, key in enumerate(keys)],
229
+ guards=deepcopy(relevant), role=role,
230
+ uncertainAliasesInvalidated=len(uncertain))
231
+ f = self.frames[-1]
232
+ stack_base, stack_delta = address_parts(f["sp"])
233
+ mem_base, mem_delta = address_parts(offset)
234
+ relative = (mem_delta - stack_delta) % (1 << self.bits)
235
+ if write is None and segment.term == self.segment("ss").term and mem_base == stack_base and f["returnBytes"] <= relative < 1 << (self.bits - 1):
236
+ event["argument"] = {"offsetFromEntrySP": relative, "width": width, "returnFrameBytes": f["returnBytes"],
237
+ "pushProducers": list(value.sources), "grouping": role or "consumed width only"}
238
+ return value
239
+
240
+ def address(self, ins, operand):
241
+ mem = operand.mem
242
+ base = ins.reg_name(mem.base) if mem.base else None
243
+ index = ins.reg_name(mem.index) if mem.index else None
244
+ if ins.addr_size != self.bits // 8:
245
+ raise StopPath("Address-size override is outside the selected model")
246
+ offset = const(mem.disp, self.bits, self.at)
247
+ if base:
248
+ offset = op("add", self.reg(base), offset, self.at)
249
+ if index:
250
+ index_value = op("mul", self.reg(index), const(mem.scale, self.bits), self.at)
251
+ offset = op("add", offset, index_value, self.at)
252
+ register = segment_register(ins, mem)
253
+ return self.segment(register), offset, register
254
+
255
+ def get(self, ins, operand, image):
256
+ if operand.type == X86_OP_REG:
257
+ return self.reg(ins.reg_name(operand.reg))
258
+ if operand.type == X86_OP_IMM:
259
+ value = const(operand.imm, operand.size * 8, self.at)
260
+ fixup = image.fixups.get(self.at + ins.imm_offset)
261
+ if fixup and operand.size == 2:
262
+ value = const(fixup["segment"], 16, self.at)
263
+ self.event("relocated-immediate", provenance=fixup, value=value.report())
264
+ return value
265
+ if operand.type == X86_OP_MEM:
266
+ segment, offset, register = self.address(ins, operand)
267
+ return self.access(segment, offset, operand.size, addressing_register=register)
268
+ raise StopPath("Unsupported operand")
269
+
270
+ def put(self, ins, operand, value):
271
+ if operand.type == X86_OP_REG:
272
+ self.setreg(ins.reg_name(operand.reg), value, self.at)
273
+ elif operand.type == X86_OP_MEM:
274
+ segment, offset, register = self.address(ins, operand)
275
+ self.access(segment, offset, operand.size, resize(value, operand.size * 8), addressing_register=register)
276
+ else:
277
+ raise StopPath("Unsupported destination")
278
+
279
+ def push(self, value):
280
+ size = value.bits // 8
281
+ self.setreg(self.sp, op("sub", self.reg(self.sp), const(size, self.bits), self.at), self.at)
282
+ self.access(self.segment("ss"), self.reg(self.sp), size, value, role="push", addressing_register="ss")
283
+
284
+ def pop(self, size):
285
+ value = self.access(self.segment("ss"), self.reg(self.sp), size, role="pop", addressing_register="ss")
286
+ self.setreg(self.sp, op("add", self.reg(self.sp), const(size, self.bits), self.at), self.at)
287
+ return value
288
+
289
+
290
+ CARRY_BRANCHES = {"jb": True, "jc": True, "jnae": True, "jae": False, "jnb": False, "jnc": False}
291
+ # Branches taken when CF or OF is set; logic operations clear both whatever their operands.
292
+ CLEARED_BY_LOGIC = {**CARRY_BRANCHES, "jo": True, "jno": False}
293
+
294
+
295
+ def predicate(state, mnemonic):
296
+ flags = state.flags
297
+ if flags is None and state.carry is not None and mnemonic in CARRY_BRANCHES:
298
+ info = {"predicate": mnemonic, "flag": "CF", "carry": state.carry.report()}
299
+ if state.carry.number is None:
300
+ return None, {**info, "reason": "carry unresolved"}
301
+ return bool(state.carry.number) == CARRY_BRANCHES[mnemonic], info
302
+ if flags is None:
303
+ return None, {"predicate": mnemonic, "reason": "flag producer unresolved",
304
+ "flagProducer": state.unknown_flag_site, "flagGeneration": state.flag_epoch}
305
+ a, b, operation, site = flags
306
+ info = {"predicate": mnemonic, "flagProducer": site, "operation": operation,
307
+ "left": a.report(), "right": b.report()}
308
+ if a.number is not None and b.number is not None:
309
+ x, y = a.number, b.number
310
+ elif operation in ("cmp", "sub", "xor") and a.term == b.term:
311
+ # Any value compared with, subtracted from or XORed with itself yields zero.
312
+ x = y = 0
313
+ elif operation in ("test", "and", "or", "xor") and mnemonic in CLEARED_BY_LOGIC:
314
+ return not CLEARED_BY_LOGIC[mnemonic], info
315
+ else:
316
+ return None, info
317
+ bits = a.bits
318
+ if operation in ("cmp", "sub"):
319
+ raw = x - y
320
+ result = raw % (1 << bits)
321
+ cf = x < y
322
+ of = bool(((x ^ y) & (x ^ result)) & (1 << (bits - 1)))
323
+ elif operation == "add":
324
+ raw = x + y
325
+ result = raw % (1 << bits)
326
+ cf = raw >= 1 << bits
327
+ of = bool((~(x ^ y) & (x ^ result)) & (1 << (bits - 1)))
328
+ elif operation in ("test", "and", "or", "xor"):
329
+ result = {"test": x & y, "and": x & y, "or": x | y, "xor": x ^ y}[operation]
330
+ cf = of = False
331
+ else:
332
+ return None, info
333
+ zf, sf = result == 0, bool(result & (1 << (bits - 1)))
334
+ conditions = {"je": zf, "jz": zf, "jne": not zf, "jnz": not zf,
335
+ "jb": cf, "jc": cf, "jnae": cf, "jae": not cf, "jnb": not cf, "jnc": not cf,
336
+ "jbe": cf or zf, "jna": cf or zf, "ja": not cf and not zf, "jnbe": not cf and not zf,
337
+ "jl": sf != of, "jnge": sf != of, "jge": sf == of, "jnl": sf == of,
338
+ "jle": zf or sf != of, "jng": zf or sf != of, "jg": not zf and sf == of,
339
+ "jnle": not zf and sf == of, "js": sf, "jns": not sf, "jo": of, "jno": not of}
340
+ return conditions.get(mnemonic), info
341
+
342
+
343
+ def ordinary(state, ins, image):
344
+ m, operands = ins.mnemonic, ins.operands
345
+ if m in ("cld", "std", "cli", "sti"):
346
+ value = const(1 if m in ("std", "sti") else 0, 1, state.at)
347
+ flag = "DF" if m in ("cld", "std") else "IF"
348
+ if flag == "DF": state.direction_flag = value
349
+ else: state.interrupt_flag = value
350
+ state.event("flag-write", flag=flag, value=value.report(),
351
+ interpretation="local flag effect only; interrupts and timing are not simulated")
352
+ return
353
+ if m in ("pushf", "pushfd", "popf", "popfd"):
354
+ bits = 32 if (0x66 in ins.prefix) != state.flat else 16
355
+ if m.startswith("push"): state.save_flags(bits)
356
+ else: state.restore_flags(bits)
357
+ return
358
+ if m == "nop":
359
+ return
360
+ if m in ("mov", "movzx", "movsx"):
361
+ if state.flat and operands[0].type == X86_OP_REG and ins.reg_name(operands[0].reg) in state.segment_bases:
362
+ raise StopPath("Segment selector assignment requires a descriptor model")
363
+ value = state.get(ins, operands[1], image)
364
+ state.put(ins, operands[0], resize(value, operands[0].size * 8, signed=m == "movsx"))
365
+ return
366
+ if m == "xchg":
367
+ values = [state.get(ins, operand, image) for operand in operands]
368
+ addresses = [state.address(ins, operand) if operand.type == X86_OP_MEM else None for operand in operands]
369
+ for index, operand in enumerate(operands):
370
+ value = values[1-index]
371
+ if addresses[index] is None:
372
+ state.put(ins, operand, value)
373
+ else:
374
+ segment, offset, register = addresses[index]
375
+ state.access(segment, offset, operand.size, resize(value, operand.size * 8), addressing_register=register)
376
+ return
377
+ if m == "imul" and len(operands) in (2, 3):
378
+ left, right = (state.get(ins, operand, image) for operand in (operands if len(operands) == 2 else operands[1:]))
379
+ left = resize(left, operands[0].size * 8)
380
+ right = resize(right, left.bits, signed=True)
381
+ result = op("mul", left, right, state.at)
382
+ state.put(ins, operands[0], result)
383
+ state.forget_flags() # CF/OF require the full signed product; other flags are undefined.
384
+ state.event("arithmetic", operation="imul", left=left.report(), right=right.report(),
385
+ result=result.report(), modulus=1 << left.bits, flags="unresolved signed-product overflow")
386
+ return
387
+ if m == "lea":
388
+ segment, offset, register = state.address(ins, operands[1])
389
+ state.put(ins, operands[0], offset)
390
+ state.event("address-formation", value=offset.report(), addressingSegment=segment.report(),
391
+ addressingSegmentRegister=register, destinationRegister=ins.reg_name(operands[0].reg),
392
+ note="LEA does not access memory; this addressing default does not bind a later dereference")
393
+ return
394
+ if m in ("lds", "les"):
395
+ if state.flat:
396
+ raise StopPath("Descriptor loads are outside the PE32 flat model")
397
+ segment, offset, register = state.address(ins, operands[1])
398
+ if operands[0].size != 2:
399
+ raise StopPath("Only 16:16 pointer loads are supported")
400
+ value = state.access(segment, offset, 4, role="far-pointer", addressing_register=register)
401
+ state.put(ins, operands[0], extract(value, 0, 16))
402
+ state.setreg("ds" if m == "lds" else "es", extract(value, 16, 16), state.at)
403
+ return
404
+ if m == "push":
405
+ if state.flat and operands[0].type == X86_OP_REG and ins.reg_name(operands[0].reg) in state.segment_bases:
406
+ raise StopPath("Segment stack operations require a descriptor model")
407
+ state.push(state.get(ins, operands[0], image))
408
+ return
409
+ if m == "pop":
410
+ if state.flat and operands[0].type == X86_OP_REG and ins.reg_name(operands[0].reg) in state.segment_bases:
411
+ raise StopPath("Segment selector assignment requires a descriptor model")
412
+ state.put(ins, operands[0], state.pop(operands[0].size))
413
+ return
414
+ if m == "leave":
415
+ if 0x66 in ins.prefix:
416
+ raise StopPath("Operand-size override on LEAVE is unsupported")
417
+ state.setreg(state.sp, state.reg(state.bp), state.at)
418
+ state.setreg(state.bp, state.pop(state.bits // 8), state.at)
419
+ return
420
+ if m in ("cmp", "test"):
421
+ a, b = (state.get(ins, o, image) for o in operands)
422
+ state.set_flags(a, resize(b, a.bits), m)
423
+ state.event("compare", operation=m, left=a.report(), right=b.report())
424
+ return
425
+ if m in ("add", "sub", "and", "or", "xor", "shl", "sal", "shr", "sar"):
426
+ a, b = (state.get(ins, o, image) for o in operands)
427
+ b = resize(b, a.bits)
428
+ result = op("shl" if m == "sal" else m, a, b, state.at)
429
+ state.put(ins, operands[0], result)
430
+ if m in ("add", "sub", "and", "or", "xor"):
431
+ state.set_flags(a, b, m)
432
+ else:
433
+ shift_carry(state, m, a, b)
434
+ state.event("arithmetic", operation=m, left=a.report(), right=b.report(), result=result.report(), modulus=1 << a.bits)
435
+ return
436
+ if m in ("inc", "dec"):
437
+ a = state.get(ins, operands[0], image)
438
+ result = op("add" if m == "inc" else "sub", a, const(1, a.bits), state.at)
439
+ state.put(ins, operands[0], result)
440
+ # Carry is preserved; the other flags are not modeled for INC/DEC.
441
+ state.forget_flags(keep_carry=True)
442
+ return
443
+ if m in ("cbw", "cwde"):
444
+ # Capstone 5 names these inconsistently in 16-bit mode. Use effective size.
445
+ # The prefix toggles the mode's default operand size (16-bit real mode, 32-bit flat).
446
+ wide = (0x66 in ins.prefix) != state.flat
447
+ source, destination = ("ax", "eax") if wide else ("al", "ax")
448
+ value = resize(state.reg(source), 32 if wide else 16, True)
449
+ state.setreg(destination, value, state.at)
450
+ state.event("conversion", sourceRegister=source, destinationRegister=destination,
451
+ effectiveOperandBits=32 if wide else 16, decoderMnemonic=m,
452
+ mnemonicWidthMismatch=m != ("cwde" if wide else "cbw"), result=value.report())
453
+ return
454
+ if m in ("cwd", "cdq"):
455
+ wide = (0x66 in ins.prefix) != state.flat
456
+ source, destination = ("eax", "edx") if wide else ("ax", "dx")
457
+ bits = 32 if wide else 16
458
+ value = resize(extract(state.reg(source), bits-1, 1), bits, signed=True)
459
+ state.setreg(destination, value, state.at)
460
+ state.event("conversion", sourceRegister=source, destinationRegister=destination,
461
+ effectiveOperandBits=bits, decoderMnemonic=m,
462
+ mnemonicWidthMismatch=m != ("cdq" if wide else "cwd"), result=value.report())
463
+ return
464
+ if m in ("clc", "stc", "cmc"):
465
+ if m == "cmc":
466
+ value = op("xor", state.carry_value(), const(1, 1), state.at)
467
+ else:
468
+ value = const(int(m == "stc"), 1, state.at)
469
+ state.forget_flags()
470
+ state.carry = value
471
+ state.event("flag-write", flag="CF", value=value.report(), interpretation="local carry effect")
472
+ return
473
+ if m in ("not", "neg"):
474
+ a = state.get(ins, operands[0], image)
475
+ if m == "not":
476
+ state.put(ins, operands[0], op("xor", a, const((1 << a.bits) - 1, a.bits), state.at))
477
+ return
478
+ result = op("sub", const(0, a.bits), a, state.at)
479
+ state.put(ins, operands[0], result)
480
+ state.set_flags(const(0, a.bits, state.at), a, "sub")
481
+ state.event("arithmetic", operation="neg", left=a.report(), result=result.report(), modulus=1 << a.bits)
482
+ return
483
+ if m in ("adc", "sbb"):
484
+ a, b = (state.get(ins, o, image) for o in operands)
485
+ b = resize(b, a.bits)
486
+ carry = state.carry_value()
487
+ name = "add" if m == "adc" else "sub"
488
+ result = op(name, op(name, a, b, state.at), resize(carry, a.bits), state.at)
489
+ state.put(ins, operands[0], result)
490
+ state.forget_flags()
491
+ if None not in (a.number, b.number, carry.number):
492
+ raw = a.number + b.number + carry.number if m == "adc" else a.number - b.number - carry.number
493
+ state.carry = const(int(raw < 0 or raw >= 1 << a.bits), 1, state.at)
494
+ else:
495
+ state.carry = unknown(f"carry:{state.at}:{state.flag_serial}", 1, state.at)
496
+ state.event("arithmetic", operation=m, left=a.report(), right=b.report(), carryIn=carry.report(),
497
+ result=result.report(), carryOut=state.carry.report(), modulus=1 << a.bits)
498
+ return
499
+ if m in ("rol", "ror", "rcl", "rcr"):
500
+ a = state.get(ins, operands[0], image)
501
+ count = state.get(ins, operands[1], image) if len(operands) > 1 else const(1, 8)
502
+ if count.number is None:
503
+ raise StopPath("rotate count unresolved")
504
+ masked = count.number & 31
505
+ if masked == 0:
506
+ return # The value and flags are unchanged.
507
+ bits = a.bits
508
+ through = m in ("rcl", "rcr")
509
+ n = masked % (bits + 1 if through else bits)
510
+ if through and n == 0:
511
+ # A full rotation through CF restores the value and CF; OF is undefined.
512
+ state.forget_flags(keep_carry=True)
513
+ return
514
+ carry_in = resize(state.carry_value(), bits)
515
+ # Each form is an OR of shifted copies (positive shifts left), built once so the
516
+ # expression does not repeat the operand for every bit rotated.
517
+ parts = {"rol": [(a, n), (a, n - bits)],
518
+ "ror": [(a, -n), (a, bits - n)],
519
+ "rcl": [(a, n), (carry_in, n - 1), (a, n - bits - 1)],
520
+ "rcr": [(a, -n), (carry_in, bits - n), (a, bits + 1 - n)]}[m]
521
+ value = None
522
+ for part, shift in parts:
523
+ if abs(shift) >= bits:
524
+ continue # Every bit leaves the operand; op() would mask the count instead.
525
+ if shift:
526
+ part = op("shl" if shift > 0 else "shr", part, const(abs(shift), bits), state.at)
527
+ value = part if value is None else op("or", value, part, state.at)
528
+ # CF is the last bit rotated out: the result's low bit for ROL/RCL and its high bit for ROR/RCR.
529
+ out = {"rol": (bits - n) % bits, "ror": (n - 1) % bits, "rcl": bits - n, "rcr": n - 1}[m]
530
+ carry = extract(a, out, 1)
531
+ state.put(ins, operands[0], value)
532
+ state.forget_flags()
533
+ state.carry = Value(1, carry.term, sources(carry, site=state.at))
534
+ state.event("arithmetic", operation=m, left=a.report(), count=n, result=value.report(),
535
+ carryOut=state.carry.report(), modulus=1 << bits)
536
+ return
537
+ if m in ("mul", "imul") and len(operands) == 1:
538
+ source = state.get(ins, operands[0], image)
539
+ bits = source.bits
540
+ low_reg, high_reg = {8: ("al", "ah"), 16: ("ax", "dx"), 32: ("eax", "edx")}[bits]
541
+ signed = m == "imul"
542
+ multiplicand = state.reg(low_reg)
543
+ product = op("mul", resize(multiplicand, 2 * bits, signed), resize(source, 2 * bits, signed), state.at)
544
+ low = extract(product, 0, bits)
545
+ if bits == 8:
546
+ state.setreg("ax", product, state.at)
547
+ else:
548
+ state.setreg(low_reg, low, state.at)
549
+ state.setreg(high_reg, extract(product, bits, bits), state.at)
550
+ state.forget_flags()
551
+ if product.number is None:
552
+ state.carry = unknown(f"carry:{state.at}:{state.flag_serial}", 1, state.at)
553
+ else:
554
+ # CF and OF say whether the high half carries information beyond the low half.
555
+ state.carry = const(int(resize(low, 2 * bits, signed).number != product.number), 1, state.at)
556
+ state.event("arithmetic", operation=m, left=multiplicand.report(), right=source.report(),
557
+ result=product.report(), resultBits=2 * bits, carryOut=state.carry.report())
558
+ return
559
+ if m in ("div", "idiv"):
560
+ divisor = state.get(ins, operands[0], image)
561
+ bits = divisor.bits
562
+ signed = m == "idiv"
563
+ if bits == 8:
564
+ dividend = state.reg("ax")
565
+ else:
566
+ dividend = join([state.reg({16: "ax", 32: "eax"}[bits]), state.reg({16: "dx", 32: "edx"}[bits])])
567
+ quotient_reg, remainder_reg = {8: ("al", "ah"), 16: ("ax", "dx"), 32: ("eax", "edx")}[bits]
568
+ fault = None
569
+ if divisor.number == 0:
570
+ raise StopPath("divide by zero raises interrupt 0; its handler is not modeled")
571
+ if None not in (dividend.number, divisor.number):
572
+ x, y = dividend.number, divisor.number
573
+ if signed:
574
+ x -= (x >> (2 * bits - 1)) << (2 * bits)
575
+ y -= (y >> (bits - 1)) << bits
576
+ q = abs(x) // abs(y) * (1 if (x < 0) == (y < 0) else -1)
577
+ r = x - q * y
578
+ if not ((-(1 << (bits - 1)) <= q < 1 << (bits - 1)) if signed else q < 1 << bits):
579
+ raise StopPath("divide overflow raises interrupt 0; its handler is not modeled")
580
+ quotient, remainder = const(q, bits, state.at), const(r, bits, state.at)
581
+ else:
582
+ wide = resize(divisor, 2 * bits, signed)
583
+ origin = sources(dividend, divisor, site=state.at)
584
+ quotient = extract(Value(2 * bits, ("sdiv" if signed else "udiv", dividend.term, wide.term), origin), 0, bits)
585
+ remainder = extract(Value(2 * bits, ("smod" if signed else "umod", dividend.term, wide.term), origin), 0, bits)
586
+ fault = "possible divide error (interrupt 0) unresolved; this path assumes none"
587
+ state.conditional.append({"site": state.at, "assumption": "no divide error"})
588
+ state.setreg(quotient_reg, quotient, state.at)
589
+ state.setreg(remainder_reg, remainder, state.at)
590
+ state.forget_flags()
591
+ state.event("arithmetic", operation=m, dividend=dividend.report(), divisor=divisor.report(),
592
+ quotient=quotient.report(), remainder=remainder.report(), fault=fault)
593
+ return
594
+ raise StopPath("Unsupported instruction semantics: " + m)
595
+
596
+
597
+ def shift_carry(state, m, a, count):
598
+ """CF after SHL/SHR/SAR: the last bit shifted out, when the count is known."""
599
+ n = None if count.number is None else count.number & 31
600
+ if n == 0:
601
+ return # A zero count leaves every flag unchanged.
602
+ state.forget_flags()
603
+ if n is None or n > a.bits:
604
+ state.carry = unknown(f"carry:{state.at}:{state.flag_serial}", 1, state.at)
605
+ elif m in ("shl", "sal"):
606
+ state.carry = Value(1, extract(a, a.bits - n, 1).term, sources(a, site=state.at))
607
+ else:
608
+ state.carry = Value(1, extract(a, n - 1, 1).term, sources(a, site=state.at))
609
+
610
+
611
+ def string_instruction(ins):
612
+ # Match the one-byte opcode, not the last encoded byte: SSE MOVSD (F2 0F 10 /r) can end in A5.
613
+ return ins.opcode[0] in (0xA4, 0xA5, 0xAA, 0xAB, 0xAC, 0xAD) and ins.opcode[1] == 0
614
+
615
+
616
+ def string_count(state, ins):
617
+ return state.reg("ecx" if state.flat else "cx") if 0xF3 in ins.prefix else const(1, state.bits)
618
+
619
+
620
+ def check_string_form(state, ins):
621
+ if ins.addr_size != state.bits // 8:
622
+ raise StopPath("Address-size override on string operation is outside the selected model")
623
+ if 0xF2 in ins.prefix:
624
+ raise StopPath("REPNE string form is not supported")
625
+
626
+
627
+ def string_effect(state, ins, count, remaining):
628
+ """Apply a string form already accepted by check_string_form with its string_count."""
629
+ width = 1 if ins.opcode[0] % 2 == 0 else (4 if (0x66 in ins.prefix) != state.flat else 2)
630
+ operation = ins.mnemonic.split()[-1][:4]
631
+ state.event("string-operation", operation=operation, width=width, repetitions=count.report(),
632
+ direction=state.direction_flag.report(), repeat=0xF3 in ins.prefix,
633
+ interpretation="bounded memory effects only, not pixels or native input coverage")
634
+ if count.number is None:
635
+ raise StopPath("String repetition count unresolved; a bounded producer is required")
636
+ if count.number > remaining:
637
+ raise StopPath("String iteration budget exhausted; remaining effects unresolved")
638
+ if count.number == 0:
639
+ return 0
640
+ if state.direction_flag.number is None:
641
+ raise StopPath("Direction flag unresolved; conditional string paths required")
642
+ # MOVS/LODS decode their source as the second memory operand, carrying any segment override.
643
+ source_name = segment_register(ins, ins.operands[1].mem) if operation in ("movs", "lods") else None
644
+ si, di = ("esi", "edi") if state.flat else ("si", "di")
645
+ delta = -width if state.direction_flag.number else width
646
+ for _ in range(count.number):
647
+ if operation in ("movs", "lods"):
648
+ value = state.access(state.segment(source_name), state.reg(si), width, role="string-source", addressing_register=source_name)
649
+ state.setreg(si, op("add", state.reg(si), const(delta, state.bits), state.at), state.at)
650
+ else:
651
+ value = state.reg({1:"al",2:"ax",4:"eax"}[width])
652
+ if operation in ("movs", "stos"):
653
+ state.access(state.segment("es"), state.reg(di), width, value, role="string-destination", addressing_register="es")
654
+ state.setreg(di, op("add", state.reg(di), const(delta, state.bits), state.at), state.at)
655
+ else:
656
+ state.setreg({1:"al",2:"ax",4:"eax"}[width], value, state.at)
657
+ if 0xF3 in ins.prefix:
658
+ state.setreg("ecx" if state.flat else "cx", const(0, state.bits, state.at), state.at)
659
+ return count.number