│ Category: Reverse Engineering
│ Author: Imattas aka Zemi
│ Flag: byuctf{g3t_w1th_th3_c0ntr01_fl0w}
│ Source Path: rev/angr_management
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
-- text --
I built a maze using goto statements! Navigate through it successfully to get the flag.
Note: the provided file contains a dummy flag.
────────────────────────────────────────────────────────────────────────────────
--[ Provided Materials ]--
- Repository folder
https://github.com/BYU-CSA/BYUCTF-2026/tree/main/rev/angr_management
- rev/angr_management/description.txt https://github.com/BYU-CSA/BYUCTF-2026/blo
b/main/rev/angr_management/description.txt
- rev/angr_management/writeup.md
https://github.com/BYU-CSA/BYUCTF-2026/blob/main/rev/angr_management/writeup.md
- rev/angr_management/challenge.zip https://github.com/BYU-CSA/BYUCTF-2026/blob/
main/rev/angr_management/challenge.zip
- rev/angr_management/solve.py
https://github.com/BYU-CSA/BYUCTF-2026/blob/main/rev/angr_management/solve.py
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
This page imports the BYUCTF 2026 source material for the Reverse Engineering
challenge Angr Management. The prompt is kept separate from the solve notes, and
upstream artifacts are linked so the challenge can be replayed from the original
repository.
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
The useful observation comes from the imported solve notes below. I kept the
original technical path intact while normalizing the page metadata, challenge
grouping, and author attribution for the Volume 2 writeup archive.
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
Source: rev/angr_management/writeup.md
https://github.com/BYU-CSA/BYUCTF-2026/blob/main/rev/angr_management/writeup.md
The user must traverse through the GOTO maze in order to get the flag.
The intended solve is to extract the program's control flow graph using a tool
like angr and then to use nx.shortest_path (Dijkstra's algorithm) to compute the
solve path from the start of the maze to the end.
Using angr, we can grab the address at the start of the main function and an
address near the end of main. We can then obtain the CFGNode object
corresponding to each of these locations in the program. The control flow graph
itself is a graph of CFGNodes, so this prepares us to use a shortest path
algorithm on the graph.
-- python --
# Create an angr Project object with auto_load_libs turned off
p = angr.Project(binary_name, auto_load_libs=False)
loader = p.loader
# Generate the control flow graph
cfg = p.analyses.CFGFast()
cfg_graph = cfg.model.graph
# Get the start and end of the main function
main_func = cfg.kb.functions['main']
main_start = main_func.addr
main_end = 0
for i, block in enumerate(list(main_func.blocks)):
main_end = max(main_end, block.instruction_addrs[0])
source_node = cfg.model.get_any_node(main_start)
sink_node = cfg.model.get_any_node(main_end)
Wait a second! If we call nx.shortest_path(cfg_graph, source_node, sink_node) we
will get a very short path since calls to library functions like printf, puts,
and get_input connect the source node and sink node prematurely. So we need to
manually remove these destinations from our CFG before proceeding:
-- python --
# Remove calls to printf, puts and get_input from the CFG
printf_addr = cfg.kb.functions['printf'].addr
puts_addr = cfg.kb.functions['puts'].addr
get_input_addr = cfg.kb.functions['get_input'].addr
addrs_to_remove = [printf_addr, puts_addr, get_input_addr]
for addr in addrs_to_remove:
node = cfg.model.get_any_node(addr)
cfg_graph.remove_node(node)
Now we can determine the shortest path:
-- python3 --
# Find a path from the start to the end of main which doesn't enter any disallowed functions
cfg_path = nx.shortest_path(cfg_graph, source_node, sink_node)
There is still work to do... we need to actually extract the correct destination
names from the code. With some luck and consulting the angr documentation, we
can examine the types of jumps angr has computed between the CFGNodes in our
cfg_path. Turns out that jumps of the type Ijk_Boring are what we want. We also
require stmt_idx to not equal -2 for each of the jumps. See the following code:
-- python --
# Parse the sequence of inputs required to realize the aforementioned path
# The following loop finds the responsible cmp instructions and extracts the immediate value from each
computed_path = list()
for i, cfgnode in enumerate(cfg_path):
try:
cfgnode_attrs = cfg.graph.get_edge_data(cfgnode, cfg_path[i+1])
except:
pass
# Verify experimentally-determined conditions for the code to branch to the next goto are met
if cfgnode_attrs['jumpkind'] == 'Ijk_Boring' and cfgnode_attrs['stmt_idx'] > -2:
for instruction in cfgnode.block.capstone.insns:
# Find the cmp instruction responsible and convert the immediate value to decimal; store it
if instruction.mnemonic == 'cmp':
poss = instruction.op_str.split("], ", 1)
desired_branch = int(poss[-1], 16)
computed_path.append(desired_branch)
break
Input each of the numbers in computed_path in succession, and we get the flag.
byuctf{g3t_w1th_th3_c0ntr01_fl0w}
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
- rev/angr_management/solve.py
https://github.com/BYU-CSA/BYUCTF-2026/blob/main/rev/angr_management/solve.py
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- The BYUCTF 2026 challenge material is preserved with local archive formatting.
- The page author is normalized to Imattas aka Zemi.
- The source repository remains linked for handouts, services, and solve
artifacts.