Understanding CFGs and Decompilation with angr


Intro to angr.

This module moves from the GUI to angr's Python API. You will load binaries with angr.Project, build a control-flow graph with CFGFast, and then query the resulting knowledge base: functions, function graphs, the call graph, CFG edges, string references, cross-references, calling conventions, and decompiler output.

Each challenge asks you to write a short Python script that computes one answer about the binary.

Solving challenges

Every challenge ships a template.py that tells you exactly what you need to do: it already contains the boilerplate and marks the parts you have to fill in. Start from it.

Every challenge also ships a handout.py that implements a submit() function. Use submit(answer) to submit your answer; print() is not picked up by the checker. Then run:

/challenge/check /path/to/your/solve.py

If your answer matches the reference solution, check prints the flag.



Challenges

Level 0: Generate CFG & List Functions

Load the binary with angr.Project and generate a CFG using project.analyses.CFGFast().

Your task is to:

  1. Generate CFG for the binary.
  2. Count the total number of functions discovered by the CFG
  3. Count the number of PLT stubs among those functions

Submit both numbers as a JSON list: [total_functions, plt_count].

Interesting APIs

  • project.analyses.CFGFast()
  • project.kb.functions - the knowledge base of discovered functions
  • func.is_plt - whether a function is a PLT stub

Level 1: Count CFG Nodes

Load the binary and generate a CFG.

Your task is to count the total number of nodes in the CFG graph.

Submit the count as a single integer.

Interesting APIs

  • cfg.graph - the full CFG as a NetworkX DiGraph
  • cfg.graph.nodes() - all nodes in the CFG

Level 2: Function Graph - Max In-Degree and Out-Degree

Load the binary and generate a CFG. Then access the function graph for state_machine via func.graph.

The function implements a state machine inside a while(1) loop. Each basic block in the function graph has in-degree and out-degree edges.

Your task is to find:

  1. The hex address of the node with the highest out-degree
  2. The hex address of the node with the highest in-degree

Submit both as a JSON list: [hex_max_out_degree, hex_max_in_degree].

Interesting APIs

  • project.kb.functions - look up functions by name
  • func.graph - the function's CFG as a NetworkX DiGraph
  • func.graph.out_degree(node) - out-degree of a node
  • func.graph.in_degree(node) - in-degree of a node

Level 3: Callers of a Function

Load the binary and generate a CFG. Use the callgraph to find all user-defined functions that directly call the function printer.

Submit a sorted JSON list of the caller function names.

Interesting APIs

  • project.kb.callgraph - the call graph as a NetworkX DiGraph
  • Use NetworkX predecessors() to find callers

Level 4: Finding Callers via CFG Edges

Load the binary and generate a CFG.

Your task is to find the instruction addresses of every call to the printer function by querying the CFG graph edges.

Submit the list of instruction addresses.

Interesting APIs

  • cfg.model.get_any_node(addr) - get the CFG node for a given address
  • cfg.graph.in_edges(node, data=True) - get all incoming edges to a node with edge data

Level 5: Strings Referenced in a Function

Load the binary and generate a CFG with cross_references.

Your task is to find all constant strings referenced by the main function that are at least 8 bytes long. Submit them as a sorted JSON list.

Interesting APIs

  • project.analyses.CFGFast()
  • func.string_references() - get string references from a function

Level 6: Instruction Addresses Referencing a String

Load the binary and generate a CFG with cross_references=True.

Your task is to find all instruction addresses that reference a string which contains "angr_is_best". Search through cfg.model.memory_data to locate the string, then use cross-references to find which instructions access it.

Submit a sorted JSON list of instruction addresses (as integers).

If the string appears at multiple memory locations, collect xrefs for all of them.

Interesting APIs

  • cfg.model.memory_data - dictionary of memory addresses to MemoryData objects
  • from angr.knowledge_plugins.cfg import MemoryDataSort
  • md.sort == MemoryDataSort.String - check if memory data is a string
  • md.content - the string content (as bytes)
  • project.kb.xrefs.get_xrefs_by_dst(addr) - cross-references to an address
  • xref.ins_addr - the instruction address of the xref

Level 7: Calling Convention Analysis - Argument Count

Load the binary and generate a CFG. Then run CompleteCallingConventions analysis with recover_variables=True to recover function prototypes.

Your task is to determine how many arguments the function compute_result takes.

Submit the argument count as a single integer.

Interesting APIs

  • project.analyses.CompleteCallingConventions()
  • func.prototype - the recovered function prototype
  • func.prototype.args - list of recovered arguments

Level 8: Count Global Variables (Decompiler)

Load the binary, generate a CFG, and decompile the main function. Make sure you normalize the cfg before decompiling it.

Your task is to count the number of global variables (external symbols) referenced in the main function.

Submit the count as a single integer.

Interesting APIs

  • project.analyses.CFGFast()
  • project.analyses.Decompiler(func, cfg=cfg.model) - decompile a function
  • dec.codegen.cexterns - list of external (global) variables used in the function

Level 9: Count Local Variables (Decompiler)

Load the binary, generate a CFG, and decompile the main function.

Your task is to count the number of local variables in the decompiled main function.

Submit the count as a single integer.

Interesting APIs

  • project.analyses.CFGFast(normalize=True)
  • project.analyses.Decompiler(func, cfg=cfg.model)
  • dec.codegen.cfunc - the decompiled C function object
  • dec.codegen.cfunc.unified_local_vars - list of unified local variables

Variable unification is a decompiler process that reduces the number of variables to produce more readable output.

Level 10: Decompile & Count write() Calls

Load the binary, generate a CFG, and decompile the function logging_function.

Your task is to count how many times write() is called with file descriptor 4 as the first argument. Parse the decompiled text output to find these calls.

Submit the count as a single integer.

Interesting APIs

  • dec.codegen.text - the decompiled C code as a string

30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score