
Deobf Indirect
- 80 installs
- 133 repo stars
- Updated May 21, 2026
- p4nda0s/bin-deobf-skills
Helps with ai & agent building tasks during AI-assisted development.
About
deobf-indirect is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- deobf-indirect
- AI & Agent Building
- AI-coding skill
Deobf Indirect by the numbers
- 80 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,222 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/p4nda0s/bin-deobf-skills --skill deobf-indirectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 80 |
|---|---|
| repo stars | ★ 133 |
| Last updated | May 21, 2026 |
| Repository | p4nda0s/bin-deobf-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Deobfuscate Indirect Branches
Part 1: CSEL + BR Indirect Branch Pattern
Pattern Recognition
The obfuscator converts conditional branches into indirect jumps, where CSEL and BR instructions appear in pairs. CSEL selects one of two target addresses based on a condition, followed by some junk instructions, then BR jumps to the selected address.
Typical instruction sequence:
CMP W10, W11
CSEL W10, W13, W12, LS ; select W13 or W12 based on LS condition
...... ; junk code in between (address calculations, etc.)
ADD X8, X8, X14
BR X8 ; indirect jump to the computed targetKey characteristics:
CSELselects one of two register values, representing two branch targets- Code between
CSELandBRis address calculation or junk code BRperforms the final indirect jump
Analysis Approach
Symbolic execution: when encountering a CSEL instruction, force different branch selections to obtain two different BR target addresses.
Steps: 1. Start two symbolic execution runs from the same basic block entry 2. First run: force CSEL to select the first register (condition-true branch) 3. Second run: force CSEL to select the second register (condition-false branch) 4. Each run reaches BR and yields a different target address (A and B)
Traversal Strategy
BFS traversal starting from the function entry block:
1. Add the function entry address to the work queue 2. Dequeue an address, run symbolic execution with both CSEL selections until BR 3. Record patch info (CSEL address, condition, two target addresses) 4. Add both target addresses to the work queue 5. Use a visited set to prevent revisiting 6. Mark blocks ending with RET as return blocks — do not continue from them
Patching Strategy
Code between CSEL and BR is junk — patch a conditional branch directly at the CSEL location.
For example, CSEL W11, W8, W9, CC: W8 is the target for the CC-true branch (A), W9 is the target for the CC-false branch (B).
Patch as:
BCC A ; if condition met, jump to A
B B ; otherwise jump to BTwo instructions = 8 bytes, overwriting the CSEL (4 bytes) and the next junk instruction (4 bytes).
Part 2: CSET + BR Indirect Branch Pattern (Jump Table Variant)
Pattern Recognition
Unlike the CSEL variant in Part 1, the CSET variant uses a 0/1 index to look up a jump table for computing the target address. The code between CSET and BR is NOT all junk — it contains useful instructions that subsequent basic blocks depend on.
Typical instruction sequence:
CMP X27, X8
CSET W8, EQ ; W8 = 0 or 1 (index)
STR W8, [SP, #offset] ; store index (junk)
LDR X9, [SP, #tbl_off] ; load jump table pointer (junk)
LDR X8, [X9, W8, UXTW#3] ; table[index] (junk)
ADRP X9, #page ; load encrypted constant (junk)
LDR W9, [X9, #off] ; (junk)
MOV W10, #imm ; XOR key (junk)
MOVK W10, #imm, LSL#16 ; (junk)
EOR W9, W9, W10 ; decrypt offset (junk)
NEG W9, W9 ; negate (junk)
ADD X8, X8, W9, SXTW ; final target address (junk, boundary)
; --- useful code below ---
ADRP X25, #0x100004000 ; register setup for successor blocks
ADD X25, X25, #0x250
MOV W28, #0xF065 ; constant init
MOVK W28, #0x611A, LSL#16
LDR X23, [SP, #0x50] ; load state for successor
BR X8 ; indirect jump (junk)Junk Code Identification
ADD Xn, Xn, Wm, SXTW is the boundary between junk and useful code. Everything from CSET to ADD (inclusive) is junk:
| Instruction | Purpose | Classification |
|---|---|---|
CSET Wd, cond | Set 0/1 index | junk (replaced by patch) |
STR Wd, [SP, #off] | Store index value | junk (only used by jump table) |
LDR Xn, [base, #off] | Load jump table pointer | junk |
LDR Xm, [Xn, Wd, UXTW#3] | Table lookup table[index] | junk |
ADRP + LDR Wn | Load encrypted constant | junk |
MOV + MOVK Wm | XOR decryption key | junk |
EOR Wn, Wn, Wm | Decrypt offset | junk |
NEG Wn, Wn | Negate | junk |
ADD Xm, Xm, Wn, SXTW | Compute final address | junk (boundary) |
| Subsequent MOV/LDR/STR/ADRP+ADD | Register and stack state init | useful |
BR Xm | Indirect jump | junk (replaced by patch) |
Analysis Approach
Same as the CSEL variant: symbolic execution forces CSET to take 1 and 0 respectively, runs until BR to obtain two target addresses.
One difference: a single basic block may contain multiple CSEL/CSET instructions (e.g., a data-selection CSEL followed by a branch-controlling CSET). The script forces selection on every CSEL/CSET encountered; the recorded csel_addr is the last one (the one that controls the BR target).
Patching Strategy
Cannot patch directly at the CSET location (would skip useful code). Correct approach:
1. Locate ADD Xn, Xn, Wm, SXTW (the boundary) 2. Extract useful code bytes between ADD+4 and BR 3. Move useful code up to the CSET location 4. Append Bcond A; B B immediately after
Before: [CSET][junk...][ADD][useful code][BR]
After: [useful code][Bcond A][B B][... dead code ...]Prerequisite: useful code must be position-independent (SP-relative addressing, immediate assignments, or same-page ADRP). Moving ADRP within the same 4KB page requires no immediate adjustment.
Reference Implementation
Two script variants for different obfuscation sub-patterns:
script/deinbr-v3-csel.py — CSEL variant (ELF)
For binaries where CSEL directly selects between two target addresses and the code between CSEL and BR is pure address calculation junk. Patches 8 bytes at the CSEL location (Bcond + B), overwriting CSEL and the next junk instruction.
script/deinbr-v3-cset.py — CSET variant (Mach-O / ELF)
For binaries where CSET sets a 0/1 index and the code between CSET and BR contains useful side-effect instructions (register setup, stack stores for subsequent blocks) interleaved with address calculation junk.
Patching strategy: find ADD Xn, Xn, Wm, SXTW (last address calculation step), move the useful code (between ADD and BR) up to the CSET location, then append Bcond + B. This preserves register/memory state that successor blocks depend on. The moved instructions must be position-independent (SP-relative, immediates, or same-page ADRP).
Both scripts share the same core workflow:
1. analyze_br(proj, func_start) — BFS traversal, collects all patch points 2. run_until_br(proj, state, csel_selector) — execute from a given state until BR, forcing CSEL/CSET selection 3. do_patch(binary_path, save_path, proj, patch_list) — assemble patches with keystone and write to binary
Key angr options:
CALLLESS— ignore function calls to prevent analysis divergenceLAZY_SOLVES— defer constraint solving for performanceZERO_FILL_UNCONSTRAINED_MEMORY— fill unconstrained memory with zerosZERO_FILL_UNCONSTRAINED_REGISTERS— fill unconstrained registers with zeros
Script Execution Rules
All non-trivial Python code (both analysis scripts and final patching scripts) MUST be written to a file and executed via python script.py. Never use python -c "..." inline in bash. This avoids shell quoting/escaping issues and makes scripts easier to debug and reuse.
Common Errors and Debugging
BR target is an invalid address
block: 41fe20, next_1: <SimState @ 0x2908f8c3>, next_2: <SimState @ 0x2908f8c3>
SimEngineError: No bytes in memory for block starting at 0x2908f8c3.Cause: The user-provided address is not the function entry but an internal basic block. Stack-based jump table base addresses and offset constants have not been initialized (filled with zeros by ZERO_FILL), causing BR to compute an invalid target.
Solution: Confirm the address is the function start. Use the symbol table or IDA/Ghidra to find the function entry:
for sym in proj.loader.main_object.symbols:
if sym.rebased_addr <= target_addr < sym.rebased_addr + sym.size:
print(sym.name, hex(sym.rebased_addr))Both branch targets are identical
block: XXXXX, next_1: <SimState @ 0xABCD>, next_2: <SimState @ 0xABCD>Same cause as above — starting execution from a non-entry point means the two registers selected by CSEL hold the same value (both zero or the same uninitialized value), producing the same BR target after address calculation.
Solution: Use the correct function entry address.
Encountering non-CSEL conditional select instructions
block XXXXX: expected 1 successor, got 0Or the script reaches BR without detecting CSEL, so csel_addr is missing from globals.
Cause: The basic block uses CSET instead of CSEL. CSET is a special form of CSEL, equivalent to CSEL Rd, WZR, WZR, invert(cond), selecting 1 or 0 as an index.
Handle the same way as CSEL — identify the condition and force both branch selections. The script must handle both csel and cset:
if insn.mnemonic == 'cset':
# cset Wd, cond is equivalent to csel Wd, #1, #0, cond (condition met=1, not met=0)
dst, cond = parse_cset(insn)
val = 1 if csel_selector == 1 else 0
setattr(state.regs, dst, val)
...import angr
import claripy
import keystone
from pwn import *
import logging
logger = logging.getLogger("deinbr")
logger.setLevel(logging.INFO)
def parse_csel(insn):
"""解析 csel 指令,返回 (dst_reg, condition, reg1, reg2)"""
if insn.mnemonic != 'csel':
return None
ops = insn.op_str.replace(' ', '').split(',')
return ops[0], ops[3], ops[1], ops[2]
def parse_cset(insn):
"""解析 cset 指令,返回 (dst_reg, condition)"""
if insn.mnemonic != 'cset':
return None
ops = insn.op_str.replace(' ', '').split(',')
return ops[0], ops[1]
def run_until_br(proj, entry_state, csel_selector=1):
"""
从 entry_state 执行到 BR 指令,遇到 CSEL 时按 csel_selector 强制选择。
返回 BR 后的 state,遇到 RET 返回 None。
"""
state = entry_state.copy()
state.options.update({
angr.options.CALLLESS,
angr.options.LAZY_SOLVES,
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
})
while True:
insn = state.block().capstone.insns[0]
if insn.mnemonic == 'ret':
return None
if insn.mnemonic == 'csel':
dst, cond, reg1, reg2 = parse_csel(insn)
val = state.regs.get(reg1) if csel_selector == 1 else state.regs.get(reg2)
setattr(state.regs, dst, val)
state.globals['csel_addr'] = insn.address
state.globals['csel_condition'] = cond
logger.info("execute %x csel %s select: %d" % (insn.address, insn.op_str, csel_selector))
state.regs.pc += 4
continue
if insn.mnemonic == 'cset':
dst, cond = parse_cset(insn)
val = 1 if csel_selector == 1 else 0
setattr(state.regs, dst, claripy.BVV(val, 32))
state.globals['csel_addr'] = insn.address
state.globals['csel_condition'] = cond
logger.info("execute %x cset %s select: %d" % (insn.address, insn.op_str, csel_selector))
state.regs.pc += 4
continue
successors = proj.factory.successors(state, num_inst=1).successors
if len(successors) != 1:
raise RuntimeError("block %x: expected 1 successor, got %d" % (state.addr, len(successors)))
state = successors[0]
if insn.mnemonic == 'br':
return state
def analyze_br(proj, func_start):
"""BFS 遍历函数,收集所有 (csel_addr, condition, true_target, false_target) patch 点。"""
patch_list = []
visited = set()
work_list = [proj.factory.blank_state(addr=func_start)]
while work_list:
init_state = work_list.pop(0)
if init_state.addr in visited:
continue
visited.add(init_state.addr)
s1 = run_until_br(proj, init_state, csel_selector=1)
s2 = run_until_br(proj, init_state, csel_selector=2)
logger.info("block: %x, next_1: %s, next_2: %s" % (init_state.addr, s1, s2))
if s1 is None and s2 is None:
continue
if s1.globals['csel_addr'] != s2.globals['csel_addr']:
raise RuntimeError("block %x: inconsistent csel addresses" % init_state.addr)
patch_list.append((
s1.globals['csel_addr'],
s1.globals['csel_condition'],
s1.addr,
s2.addr,
))
work_list.append(s1)
work_list.append(s2)
# 去重:同一个 csel 地址只保留一条 patch
seen = set()
deduped = []
for item in patch_list:
if item[0] not in seen:
seen.add(item[0])
deduped.append(item)
return deduped
def do_patch(binary_path, save_path, proj, patch_list):
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
image_base = proj.loader.main_object.mapped_base
elf = ELF(binary_path)
elf_base = elf.address
for csel_addr, condition, true_addr, false_addr in patch_list:
asm_code = "b%s 0x%x; b 0x%x" % (condition, true_addr, false_addr)
logger.info("patch %x: %s" % (csel_addr, asm_code))
opcode, _ = ks.asm(asm_code, csel_addr)
file_offset = csel_addr - image_base + elf_base
elf.write(file_offset, bytes(opcode))
elf.save(save_path)
if __name__ == "__main__":
import sys
logging.basicConfig(level=logging.INFO)
binary_path = sys.argv[1] if len(sys.argv) > 1 else './tests/goron-indbr-miniz-example2'
save_path = binary_path + '.patched'
proj = angr.Project(binary_path, auto_load_libs=False)
image_base = proj.loader.main_object.mapped_base
funcs = [int(x, 16) + image_base for x in sys.argv[2:]] if len(sys.argv) > 2 else [image_base + 0x4BBA0]
patch_list = []
for func in funcs:
patch_list += analyze_br(proj, func)
print("patch list (%d):" % len(patch_list))
for csel_addr, condition, true_addr, false_addr in patch_list:
print(" %x: b%s %x / b %x" % (csel_addr, condition, true_addr, false_addr))
do_patch(binary_path, save_path, proj, patch_list)
print("saved:", save_path)
import angr
import claripy
import keystone
from pwn import *
import logging
logger = logging.getLogger("deinbr")
logger.setLevel(logging.INFO)
def parse_csel(insn):
"""解析 csel 指令,返回 (dst_reg, condition, reg1, reg2)"""
if insn.mnemonic != 'csel':
return None
ops = insn.op_str.replace(' ', '').split(',')
return ops[0], ops[3], ops[1], ops[2]
def parse_cset(insn):
"""解析 cset 指令,返回 (dst_reg, condition)"""
if insn.mnemonic != 'cset':
return None
ops = insn.op_str.replace(' ', '').split(',')
return ops[0], ops[1]
def run_until_br(proj, entry_state, csel_selector=1):
"""
从 entry_state 执行到 BR 指令,遇到 CSEL 时按 csel_selector 强制选择。
返回 BR 后的 state,遇到 RET 返回 None。
"""
state = entry_state.copy()
state.options.update({
angr.options.CALLLESS,
angr.options.LAZY_SOLVES,
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
})
while True:
insn = state.block().capstone.insns[0]
if insn.mnemonic == 'ret':
return None
if insn.mnemonic == 'csel':
dst, cond, reg1, reg2 = parse_csel(insn)
val = state.regs.get(reg1) if csel_selector == 1 else state.regs.get(reg2)
setattr(state.regs, dst, val)
state.globals['csel_addr'] = insn.address
state.globals['csel_condition'] = cond
logger.info("execute %x csel %s select: %d" % (insn.address, insn.op_str, csel_selector))
state.regs.pc += 4
continue
if insn.mnemonic == 'cset':
dst, cond = parse_cset(insn)
val = 1 if csel_selector == 1 else 0
setattr(state.regs, dst, claripy.BVV(val, 32))
state.globals['csel_addr'] = insn.address
state.globals['csel_condition'] = cond
logger.info("execute %x cset %s select: %d" % (insn.address, insn.op_str, csel_selector))
state.regs.pc += 4
continue
successors = proj.factory.successors(state, num_inst=1).successors
if len(successors) != 1:
raise RuntimeError("block %x: expected 1 successor, got %d" % (state.addr, len(successors)))
state = successors[0]
if insn.mnemonic == 'br':
return state
def analyze_br(proj, func_start):
"""BFS 遍历函数,收集所有 (csel_addr, condition, true_target, false_target) patch 点。"""
patch_list = []
visited = set()
work_list = [proj.factory.blank_state(addr=func_start)]
while work_list:
init_state = work_list.pop(0)
if init_state.addr in visited:
continue
visited.add(init_state.addr)
s1 = run_until_br(proj, init_state, csel_selector=1)
s2 = run_until_br(proj, init_state, csel_selector=2)
logger.info("block: %x, next_1: %s, next_2: %s" % (init_state.addr, s1, s2))
if s1 is None and s2 is None:
continue
if s1.globals['csel_addr'] != s2.globals['csel_addr']:
raise RuntimeError("block %x: inconsistent csel addresses" % init_state.addr)
patch_list.append((
s1.globals['csel_addr'],
s1.globals['csel_condition'],
s1.addr,
s2.addr,
))
work_list.append(s1)
work_list.append(s2)
# 去重:同一个 csel 地址只保留一条 patch
seen = set()
deduped = []
for item in patch_list:
if item[0] not in seen:
seen.add(item[0])
deduped.append(item)
return deduped
def addr_to_file_offset(proj, addr):
"""Convert virtual address to file offset using segment info."""
obj = proj.loader.main_object
for seg in obj.segments:
if seg.vaddr <= addr < seg.vaddr + seg.memsize:
return addr - seg.vaddr + seg.offset
return addr - obj.mapped_base
def do_patch(binary_path, save_path, proj, patch_list):
"""
Patch strategy: move useful code (between ADD SXTW and BR) up to CSET location,
then append Bcond + B. This preserves register/memory setup that subsequent blocks need.
"""
import capstone
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
cs = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)
with open(binary_path, 'rb') as f:
data = bytearray(f.read())
image_base = proj.loader.main_object.mapped_base
for csel_addr, condition, true_addr, false_addr in patch_list:
file_off_csel = addr_to_file_offset(proj, csel_addr)
# Scan forward from CSET to find ADD Xn,Xn,Wm,SXTW and BR
off = file_off_csel + 4
br_off = None
add_sxtw_off = None
while off < file_off_csel + 0x300:
for insn in cs.disasm(bytes(data[off:off+4]), image_base + off):
if insn.mnemonic == 'br':
br_off = off
if insn.mnemonic == 'add' and 'sxtw' in insn.op_str:
add_sxtw_off = off
if br_off:
break
off += 4
if br_off is None or add_sxtw_off is None:
logger.warning("patch %x: cannot find ADD/BR, falling back to simple patch" % csel_addr)
asm_code = "b%s 0x%x; b 0x%x" % (condition, true_addr, false_addr)
opcode, _ = ks.asm(asm_code, csel_addr)
data[file_off_csel:file_off_csel+8] = bytes(opcode)
continue
useful_start = add_sxtw_off + 4
useful_end = br_off
useful_bytes = bytes(data[useful_start:useful_end])
useful_len = len(useful_bytes)
branch_addr = csel_addr + useful_len
asm_code = "b%s 0x%x; b 0x%x" % (condition, true_addr, false_addr)
opcode, _ = ks.asm(asm_code, branch_addr)
data[file_off_csel:file_off_csel + useful_len] = useful_bytes
data[file_off_csel + useful_len:file_off_csel + useful_len + 8] = bytes(opcode)
logger.info("patch %x: moved %d bytes, b%s %x / b %x" % (
csel_addr, useful_len, condition, true_addr, false_addr))
with open(save_path, 'wb') as f:
f.write(data)
if __name__ == "__main__":
import sys
logging.basicConfig(level=logging.INFO)
binary_path = sys.argv[1] if len(sys.argv) > 1 else './tests/goron-indbr-miniz-example2'
save_path = binary_path + '.patched'
proj = angr.Project(binary_path, auto_load_libs=False)
image_base = proj.loader.main_object.mapped_base
funcs = [int(x, 16) + image_base for x in sys.argv[2:]] if len(sys.argv) > 2 else [image_base + 0x4BBA0]
patch_list = []
for func in funcs:
patch_list += analyze_br(proj, func)
print("patch list (%d):" % len(patch_list))
for csel_addr, condition, true_addr, false_addr in patch_list:
print(" %x: b%s %x / b %x" % (csel_addr, condition, true_addr, false_addr))
do_patch(binary_path, save_path, proj, patch_list)
print("saved:", save_path)