Skip to content

Commit 1d50b20

Browse files
authored
Support struct setting and getting in angr (callbacks wip) (#173)
* Support struct setting and getting in angr (callbacks wip) * working refresh
1 parent cb1a621 commit 1d50b20

3 files changed

Lines changed: 225 additions & 8 deletions

File tree

libbs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "3.2.0"
1+
__version__ = "3.3.0"
22

33

44
import logging

libbs/decompilers/angr/interface.py

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
DecompilerInterface,
1313
)
1414
from libbs.artifacts import (
15-
Function, FunctionHeader, Comment, StackVariable, FunctionArgument, Artifact, Decompilation, Context
15+
Function, FunctionHeader, Comment, StackVariable, FunctionArgument, Artifact, Decompilation, Context,
16+
Struct, StructMember
1617
)
1718
from .artifact_lifter import AngrArtifactLifter
1819

@@ -256,8 +257,9 @@ def _set_function(self, func: Function, **kwargs) -> bool:
256257
# re-decompile a function if needed
257258
decompilation = self.decompile_function(angr_func).codegen
258259
changes = super()._set_function(func, decompilation=decompilation, **kwargs)
259-
if not self.headless:
260-
self.refresh_decompilation(func.addr)
260+
if not self.headless and changes:
261+
# Use "retype_variable" event to trigger proper UI refresh including type reflow
262+
self.refresh_decompilation(func.addr, event="retype_variable")
261263

262264
return changes
263265

@@ -348,12 +350,34 @@ def _set_stack_variable(self, svar: StackVariable, decompilation=None, **kwargs)
348350
return changed
349351

350352
dec_svar = AngrInterface.find_stack_var_in_codegen(decompilation, svar.offset)
351-
if dec_svar and svar.name and svar.name != dec_svar.name:
352-
# TODO: set the types of the stack vars
353+
if not dec_svar:
354+
return changed
355+
356+
# Set the name if provided and different
357+
if svar.name and svar.name != dec_svar.name:
353358
dec_svar.name = svar.name
354359
dec_svar.renamed = True
355360
changed = True
356361

362+
# Set the type if provided
363+
if svar.type:
364+
try:
365+
from angr.sim_type import parse_type
366+
types_store = self.main_instance.project.kb.types
367+
arch = self.main_instance.project.arch
368+
369+
# Parse the type string into a SimType
370+
sim_type = parse_type(svar.type, predefined_types=types_store, arch=arch)
371+
sim_type = sim_type.with_arch(arch)
372+
373+
# Get the variable manager and set the type
374+
variable_kb = decompilation._variable_kb if hasattr(decompilation, '_variable_kb') else self.main_instance.project.kb
375+
variable_manager = variable_kb.variables[svar.addr]
376+
variable_manager.set_variable_type(dec_svar, sim_type, all_unified=True, mark_manual=True)
377+
changed = True
378+
except Exception as e:
379+
l.warning(f"Failed to set stack variable type for {svar.name}: {e}")
380+
357381
return changed
358382

359383
def _set_comment(self, comment: Comment, decompilation=None, **kwargs) -> bool:
@@ -382,6 +406,109 @@ def _set_comment(self, comment: Comment, decompilation=None, **kwargs) -> bool:
382406
func_addr = comment.func_addr or self.get_closest_function(comment.addr)
383407
return changed & self.refresh_decompilation(func_addr)
384408

409+
# structs
410+
def _structs(self) -> Dict[str, Struct]:
411+
"""
412+
Returns a dict of libbs.Struct that contain the name and size of each struct in the decompiler.
413+
"""
414+
from angr.sim_type import SimStruct, TypeRef
415+
structs = {}
416+
types_store = self.main_instance.project.kb.types
417+
418+
for type_ref in types_store.iter_own():
419+
if not isinstance(type_ref, TypeRef):
420+
continue
421+
sim_type = type_ref.type
422+
if isinstance(sim_type, SimStruct):
423+
structs[type_ref.name] = Struct(type_ref.name, sim_type.size // 8 if sim_type.size else 0, {})
424+
425+
return structs
426+
427+
def _get_struct(self, name) -> Optional[Struct]:
428+
"""
429+
Get a struct by name from the TypesStore.
430+
"""
431+
from angr.sim_type import SimStruct, TypeRef
432+
types_store = self.main_instance.project.kb.types
433+
434+
try:
435+
type_ref = types_store[name]
436+
except KeyError:
437+
return None
438+
439+
if not isinstance(type_ref, TypeRef):
440+
return None
441+
442+
sim_struct = type_ref.type
443+
if not isinstance(sim_struct, SimStruct):
444+
return None
445+
446+
return self._angr_struct_to_libbs(name, sim_struct)
447+
448+
def _set_struct(self, struct: Struct, header=True, members=True, **kwargs) -> bool:
449+
"""
450+
Create or update a struct in the TypesStore.
451+
"""
452+
from angr.sim_type import SimStruct, TypeRef, parse_type
453+
from collections import OrderedDict
454+
455+
types_store = self.main_instance.project.kb.types
456+
arch = self.main_instance.project.arch
457+
458+
# Build the fields OrderedDict from LibBS struct members
459+
fields = OrderedDict()
460+
if members and struct.members:
461+
sorted_members = sorted(struct.members.items(), key=lambda x: x[0])
462+
for offset, member in sorted_members:
463+
# Parse the member type string into a SimType
464+
try:
465+
sim_type = parse_type(member.type, predefined_types=types_store, arch=arch)
466+
except Exception:
467+
# Fallback to a simple int type with the right size if parsing fails
468+
from angr.sim_type import SimTypeInt
469+
sim_type = SimTypeInt(signed=False).with_arch(arch)
470+
471+
fields[member.name] = sim_type.with_arch(arch)
472+
473+
# Create the SimStruct
474+
sim_struct = SimStruct(fields, name=struct.name, pack=True)
475+
sim_struct = sim_struct.with_arch(arch)
476+
477+
# Wrap it in a TypeRef and store it
478+
type_ref = TypeRef(struct.name, sim_struct)
479+
types_store[struct.name] = type_ref
480+
481+
return True
482+
483+
def _del_struct(self, name) -> bool:
484+
"""
485+
Delete a struct from the TypesStore.
486+
"""
487+
types_store = self.main_instance.project.kb.types
488+
489+
if name in types_store.data:
490+
del types_store.data[name]
491+
return True
492+
493+
return False
494+
495+
@staticmethod
496+
def _angr_struct_to_libbs(name: str, sim_struct: "angr.sim_type.SimStruct") -> Struct:
497+
"""
498+
Convert an angr SimStruct to a LibBS Struct.
499+
"""
500+
members = {}
501+
if sim_struct._arch is not None:
502+
offsets = sim_struct.offsets
503+
for field_name, sim_type in sim_struct.fields.items():
504+
offset = offsets.get(field_name, 0)
505+
type_str = sim_type.c_repr() if sim_type else None
506+
size = sim_type.size // 8 if sim_type and sim_type.size else 0
507+
members[offset] = StructMember(field_name, offset, type_str, size)
508+
509+
size = sim_struct.size // 8 if sim_struct.size else 0
510+
return Struct(name, size, members)
511+
385512
#
386513
# Utils
387514
#
@@ -431,13 +558,16 @@ def addr_starts_instruction(self, addr) -> bool:
431558

432559
return addr in node.instruction_addrs
433560

434-
def refresh_decompilation(self, func_addr):
561+
def refresh_decompilation(self, func_addr, event=None):
435562
if self.headless:
436563
return False
437564

438565
self.workspace.jump_to(func_addr)
439566
view = self.workspace._get_or_create_view("pseudocode", CodeView)
440-
view.codegen.am_event()
567+
if event:
568+
view.codegen.am_event(event=event)
569+
else:
570+
view.codegen.am_event()
441571
view.focus()
442572
return True
443573

tests/test_decompilers.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,100 @@ def test_angr(self):
436436
headless=True,
437437
binary_path=self.FAUXWARE_PATH
438438
)
439+
self.deci = deci
439440
func_addr = deci.art_lifter.lift_addr(0x400664)
440441
main = deci.functions[func_addr]
441442
main.name = self.RENAMED_NAME
442443
deci.functions[func_addr] = main
443444
assert deci.functions[func_addr].name == self.RENAMED_NAME
444445
assert self.RENAMED_NAME in deci.main_instance.project.kb.functions
445446

447+
#
448+
# Struct support
449+
#
450+
451+
# test struct creation
452+
new_struct = Struct()
453+
new_struct.name = "my_angr_struct"
454+
new_struct.add_struct_member('char_member', 0, 'char', 1)
455+
new_struct.add_struct_member('int_member', 1, 'int', 4)
456+
deci.structs[new_struct.name] = new_struct
457+
458+
# verify struct was created
459+
updated = deci.structs[new_struct.name]
460+
assert updated is not None, "Struct was not created"
461+
assert updated.name == new_struct.name
462+
463+
# verify members are present
464+
assert 0 in updated.members, "First member not found"
465+
assert 1 in updated.members, "Second member not found"
466+
467+
# test struct listing
468+
struct_items = list(deci.structs.items())
469+
struct_names = [k for k, v in struct_items]
470+
assert new_struct.name in struct_names, "Struct not found in listing"
471+
472+
#
473+
# Stack variable type setting
474+
#
475+
476+
# Get the main function which has stack variables
477+
main_func_addr = deci.art_lifter.lift_addr(0x40071d)
478+
main_func = deci.functions[main_func_addr]
479+
480+
# Check that we have stack variables
481+
assert len(main_func.stack_vars) > 0, "No stack variables found in main function"
482+
483+
# Get the first stack variable and change its type to a primitive
484+
first_offset = list(main_func.stack_vars.keys())[0]
485+
original_svar = main_func.stack_vars[first_offset]
486+
487+
# Set a new type (change to int)
488+
original_svar.type = "int"
489+
deci.functions[main_func_addr] = main_func
490+
491+
# Verify the type was set by re-fetching the function
492+
updated_func = deci.functions[main_func_addr]
493+
updated_svar = updated_func.stack_vars.get(first_offset)
494+
assert updated_svar is not None, "Stack variable not found after update"
495+
# The type should contain "int" (may be formatted differently by angr)
496+
assert "int" in updated_svar.type.lower() if updated_svar.type else False, \
497+
f"Stack variable type was not updated to int, got: {updated_svar.type}"
498+
499+
#
500+
# Stack variable type setting with struct type
501+
#
502+
503+
# Re-fetch the function to get fresh stack variables
504+
main_func = deci.functions[main_func_addr]
505+
506+
# Get a stack variable (use the same one or another if available)
507+
svar_offsets = list(main_func.stack_vars.keys())
508+
struct_test_offset = svar_offsets[0] if len(svar_offsets) == 1 else svar_offsets[1]
509+
struct_test_svar = main_func.stack_vars[struct_test_offset]
510+
511+
# Set the type to a pointer to our struct
512+
struct_ptr_type = f"struct {new_struct.name} *"
513+
struct_test_svar.type = struct_ptr_type
514+
deci.functions[main_func_addr] = main_func
515+
516+
# Verify the struct type was set
517+
updated_func = deci.functions[main_func_addr]
518+
updated_svar = updated_func.stack_vars.get(struct_test_offset)
519+
assert updated_svar is not None, "Stack variable not found after struct type update"
520+
assert updated_svar.type is not None, "Stack variable type is None after struct type update"
521+
# The type should contain the struct name
522+
assert new_struct.name in updated_svar.type, \
523+
f"Stack variable type was not updated to struct pointer, got: {updated_svar.type}"
524+
525+
# Now test struct deletion (after we're done using it for stack var types)
526+
del deci.structs[new_struct.name]
527+
struct_items = list(deci.structs.items())
528+
struct_keys = [k for k, v in struct_items]
529+
assert new_struct.name not in struct_keys, "Struct was not deleted"
530+
531+
deci.shutdown()
532+
446533
def test_binja(self):
447534
deci = DecompilerInterface.discover(
448535
force_decompiler=BINJA_DECOMPILER,

0 commit comments

Comments
 (0)