1

I'm new to reverse engineering and I've just recently learned about angr, a framework that uses symbolic execution to get the input for a given output. I'm trying to complete a crackme using angr. My code so far is:

import angr
import claripy

strlen = 10
base_addr = 0x00000000

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(strlen)]
flag = claripy.Concat(*flag_chars)

proj = angr.Project("./d4rkfl0w-Crackme-4", main_opts={'base_addr': base_addr})
state = proj.factory.full_init_state(args=["./d4rkfl0w-Crackme-4"], add_options=angr.options.unicorn, stdin=flag)
state.memory.store(state.regs.rbp-0x8, flag)

for k in flag_chars:
    state.solver.add(k > 0x20)
    state.solver.add(k < 0x7e)

sm = proj.factory.simulation_manager(state)
sm.run()

for de in sm.deadended:
    print(de.posix.dumps(0), de.posix.dumps(1))

The output is:

> python3 test.py 
WARNING | 2020-08-24 18:55:24,821 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2020-08-24 18:55:24,822 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2020-08-24 18:55:24,822 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-08-24 18:55:24,823 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2020-08-24 18:55:24,823 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-08-24 18:55:24,823 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffefff8 with 64 unconstrained bytes referenced from 0x38e0d0 (strlen+0x0 in libc.so.6 (0x8e0d0))
b'((0$0(0(0(' b'\nPlease enter the password: \nSorry that is incorrect.\n'
b'0(0$0(:(((' b'\nPlease enter the password: \nSorry that is incorrect.\n'
b'U($>"(:($(' b'\nPlease enter the password: \nSorry that is incorrect.\n'
b'U(6>$(:(0(' b'\nPlease enter the password: \nSorry that is incorrect.\n'
b'U(6>-(:($(' b'\nPlease enter the password: \nSorry that is incorrect.\n'
b'U(6$-(:(Y(' b'\nPlease enter the password: \nSorry that is incorrect.\n'

This should be a relatively easy crackme (I had solved it with static analysis already and I want to get the flag a different way). Am I doing something wrong in the code?

The actual flag should be:

U6-:YL."+

2
  • What I notice that angr is not the best at handling c++ objects like cin & cout (esp. the former). I would expect that's the culprit for not finding the solution. Aug 28, 2020 at 7:16
  • I vaguely remember hearing that too... Thanks for the reminder- that might be a reason.
    – V. Zed
    Aug 29, 2020 at 15:20

1 Answer 1

1

Looking at your solution, and your spoiler of the intended flag, it looks like you are halfway there (literally).

angr is solving every other character and matching half of the flag, as if the flag is being treaded as a wide character string/utf16. I modified your script slightly to have a 20 character string, with every other byte null, ending in a newline, and angr successfully found the flag.

There are lots of other ways to do this with Angr, but this seemed close to how you were originally using it.

Modified Angr script:

import angr
import claripy

strlen = 20
base_addr = 0x00000000

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(strlen)]
flag = claripy.Concat(*flag_chars)

proj = angr.Project("./d4rkfl0w-Crackme-4", main_opts={'base_addr': base_addr})
state = proj.factory.full_init_state(args=["./d4rkfl0w-Crackme-4"], add_options=angr.options.unicorn, stdin=flag)
# The script functions the same with or without this
# Presumably symbolic stdin handles storing the flag 
# in memory for us
# state.memory.store(state.regs.rbp-0x8, flag)

# It seems like the program is reading in widechars/utf16
# this code will constrain every other byte to be printable, 
# with zero bytes in between
zero_char = False
for k in flag_chars[:-2]:
    if not zero_char:
        state.solver.add(k > 0x20)
        state.solver.add(k < 0x7e)
    else:
        state.solver.add(k == 0x0)
    zero_char = not zero_char

# Flag ends in widechar newline (not strictly needed)
state.solver.add(flag_chars[-2] == 0xa)
state.solver.add(flag_chars[-1] == 0x00)

sm = proj.factory.simulation_manager(state)
sm.run()

for de in sm.deadended:
    print(de.posix.dumps(0), de.posix.dumps(1))
    print("UTF16 flag: %s" % str(de.posix.dumps(0), encoding='utf=16'))
~                                                                        

Truncated Output:

b'U\x006\x00-\x00:\x00Y\x00L\x00.\x00!\x00+\x00\n\x00' b'\nPlease enter the password: \nSorry that is incorrect.\n' UTF16 flag: U6-:YL.!+
b'U\x006\x00-\x00:\x00Y\x00L\x00.\x00"\x00+\x00\n\x00' b"\nPlease enter the password: \nThat's CORRECT, Well Done.\n" UTF16 flag: U6-:YL."+

1
  • That's very interesting! Thank you! I'm not sure why angr is seeing the input as a wide character string. I'll be sure to try out different encoding next time I fall into this scenario. Thank you!!
    – V. Zed
    Aug 31, 2020 at 23:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.