RNG Caller Mapping Workflow¶
Goal: recover exact native caller_static values for Python RNG draw sites.
Key Rule¶
caller_static is the return address after call crt_rand, not the function start and not the call instruction address.
For x86 PE code in Crimsonland:
call_addris the instruction that callscrt_randcaller_staticiscall_addr + instruction_length
Trial Functions¶
Start with small ports where the Python draw structure still matches native closely.
Good first targets:
bonus_pick_random_typefx_queue_add_random
Avoid large ownership-mixed functions first:
player_updateprojectile_updatecreature_spawn_template
BN Workflow¶
Use the stable target name from bn target list:
Do not copy the numeric selector from a live session into docs; it changes between sessions.
- Find the native function.
bn function search --target "$TARGET" bonus_pick_random_type
bn function info --target "$TARGET" bonus_pick_random_type
- Confirm semantics in decompile/disassembly.
bn decompile --target "$TARGET" bonus_pick_random_type
bn disasm --target "$TARGET" bonus_pick_random_type
- Enumerate
crt_randcallsites with LLIL and compute exactcaller_static.
bn py exec --target "$TARGET" --stdin <<'PY'
from binaryninja import LowLevelILOperation
CRT_RAND = 0x461746
name = "bonus_pick_random_type"
f = next(fn for fn in bv.functions if fn.symbol and fn.symbol.short_name == name)
rows = []
for block in f.low_level_il:
for insn in block:
if insn.operation != LowLevelILOperation.LLIL_CALL:
continue
dest = insn.dest
if dest.operation != LowLevelILOperation.LLIL_CONST_PTR or dest.constant != CRT_RAND:
continue
info = bv.arch.get_instruction_info(bv.read(insn.address, 16), insn.address)
rows.append((insn.address, insn.address + info.length))
print("\n".join(f"call={call:#x} caller_static={ret:#x}" for call, ret in rows))
PY
- Open the Python port and map each draw site by semantics and order.
- Record the mapping as a table before changing code.
Use:
- Python file and line
- native function
- native
call_addr - exact
caller_static - semantic meaning of the draw
Trial Mapping: bonus_pick_random_type¶
Native function:
0x412470 bonus_pick_random_type
Recovered callsites:
| Python | Meaning | Native call | caller_static |
|---|---|---|---|
src/crimson/bonuses/selection.py:60 |
main roll rand() % 162 + 1 |
0x4124a0 |
0x4124a5 |
src/crimson/bonuses/selection.py:70 |
energizer branch rand() & 0x3F |
0x4124d1 |
0x4124d6 |
Why this is a good trial:
- only two draws
- branch structure is obvious
- Python port still mirrors native control flow closely
Trial Mapping: fx_queue_add_random¶
Native function:
0x427740 fx_queue_add_random
Recovered callsites:
| Python | Meaning | Native call | caller_static |
|---|---|---|---|
src/crimson/effects.py:532 |
grayscale rand() & 0xF |
0x42775b |
0x427760 |
src/crimson/effects.py:533 |
width rand() % 24 - 12 |
0x427789 |
0x42778e |
src/crimson/effects.py:534 |
rotation rand() % 628 |
0x4277ab |
0x4277b0 |
src/crimson/effects.py:535 |
effect_id rand() % 5 + 3 |
0x427806 |
0x42780b |
Why this is a good second trial:
- multiple sequential draws
- draw meanings are explicit in both native and Python
- no helper ownership ambiguity inside the function
Practical Rules¶
Only map exact callers when all of these are true:
- the Python code is the semantic owner of the draw
- the native function is identified confidently
- the Python draw order is still aligned with native
- the draw is not being delegated to a helper that should really be parent-owned
If ownership is split, fix ownership first, then map callers.
Next Good Targets¶
weapon_pick_random_availableperk_select_randombonus_try_spawn_on_kill
These should be easier than:
player_fire_weaponplayer_updateprojectile_update