Why waste time creating multiple functions, when you can just use one? Can you find the path to the flag in this angr-y binary?
Challenge Overview
The challenge provides a 32-bit ELF executable. The goal is to find the correct input that leads to the flag.
❯ file angr-y_binaryangr-y_binary: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=611e939f262f927b8515162283d36476df2d3244, not strippedAnalysis
Using radare2 to inspect the disassembly, we identify three key functions: print_flag, no_flag, and maybe_flag.
Disassembly
;-- print_flag: 0x08048596 55 push ebp 0x08048597 89e5 mov ebp, esp ... 0x080485db e830feffff call sym.imp.fgets ;[3] 0x080485e0 83c410 add esp, 0x10 0x080485e3 83ec0c sub esp, 0xc 0x080485e6 8d45b4 lea eax, [ebp - 0x4c] 0x080485e9 50 push eax 0x080485ea e841feffff call sym.imp.puts ;[4] ...
;-- no_flag: 0x08048609 55 push ebp 0x0804860a 89e5 mov ebp, esp 0x0804860c e8d0005e00 call sym.__x86.get_pc_thunk.ax ;[6] 0x08048611 05ef195e00 add eax, 0x5e19ef 0x08048616 c780300000.. mov dword [eax + 0x30], 0 0x08048620 90 nop 0x08048621 5d pop ebp 0x08048622 c3 ret
;-- maybe_flag: 0x08048623 55 push ebp 0x08048624 89e5 mov ebp, esp 0x08048626 53 push ebx 0x08048627 83ec04 sub esp, 4 0x0804862a e8b2005e00 call sym.__x86.get_pc_thunk.ax ;[6] 0x0804862f 05d1195e00 add eax, 0x5e19d1 0x08048634 8b9030000000 mov edx, dword [eax + 0x30] 0x0804863a 85d2 test edx, edx ┌─< 0x0804863c 7407 je 0x8048645 │ 0x0804863e e853ffffff call sym.print_flag ;[7] ┌──< 0x08048643 eb14 jmp 0x8048659 │└─> 0x08048645 83ec0c sub esp, 0xc ...The logic suggests we need to reach print_flag while avoiding the paths that lead to no_flag.
Solution
Since the binary’s path to the flag is complex, we use angr to perform symbolic execution and find the correct input.
Solver Script
import angrimport sys
def solve(bin_path): project = angr.Project(bin_path)
# Initialize the state at entry initial_state = project.factory.entry_state( add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY, angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS } )
simulation = project.factory.simgr(initial_state)
# Explore searching for the print_flag function and avoiding no_flag # print_flag: 0x08048596 # no_flag: 0x8048609 simulation.explore(find=0x08048596, avoid=0x8048609)
if simulation.found: solution_state = simulation.found[0] print(f"Found solution: {solution_state.posix.dumps(sys.stdin.fileno()).decode()}") else: raise Exception('Could not find the solution')
if __name__ == "__main__": solve('./angr-y_binary')Execution
Running the script gives us the password:
❯ python solve.pywgIdWOS6Df9sCzAfiKConnecting to the server with the found password:
❯ nc 0c4c28058a1f7a2f.247ctf.com 50230Enter a valid password:wgIdWOS6Df9sCzAfiKThe returned flag belongs to the launched service instance; no cross-instance invariance is established. Record the generation/retrieval method and use the current instance response instead of a fixed-answer spoiler.