Since OP didn't specify the addresses referred to by the hardcoded offsets, I'm gonna assume his intention. I'm assuming the values in find
match the two printf("gg")
statements and the two values in avoid
match the two printf("fail");
statements.
First of all, I believe your program has two unintentional considerable bugs in it, preventing the intended solutions from being needed:
- The first and more considerable of the two is the use of
lose
in your code. It is sufficient to only correctly provide one of the passwords to permanently set lose
to false
. Doing so using the first password means lose
will be false
for both, effectively reaching the two success criteria you specified. Simply put, because lose
is not being reset to the default of true
after the first password validation, is is enough to provide the first password to get two gg
s.
- This one does not really interfere with solving the challenge but is also probably a bug. We'll go over why it's an issue for angr in a few paragraphs but for now I'll just point out your string comparison is lacking any validation for the correct length, or that the input string is terminated when the hardcoded password is.
I find your example code quite interesting because those are two relatively common security vulnerabilities.
Ignoring the first logical bug above, I believe the issue is your configured success criteria; According to angr documentation (emphasis is mine):
When launching .explore()
with a find
argument, execution will run until a state is found that matches the find condition, which can be the address of an instruction to stop at, a list of addresses to stop at, or a function which takes a state and returns whether it meets some criteria.
When any of the states in the active stash match the find
condition, they are placed in the found stash, and execution terminates. You can then explore the found state, or decide to discard it and continue with the other ones. You can also specify an avoid
condition in the same format as find
. When a state matches the avoid
condition, it is put in the avoided stash, and execution continues. Finally, the num_find
argument controls the number of states that should be found before returning, with a default of 1
. Of course, if you run out of states in the active stash before finding this many solutions, execution will stop anyway.
Providing a sequence of addresses in explorer
's find
, like you did, means either one is an acceptable success condition. Because you did not provide any num_find
the default of 1
is assumed and execute
returns with the first success found.
Obviously, this is the success criteria for the first password. That is forced upon angr because the avoid
parameter you provided. If the avoid
condition is reached the current exploration path is terminated, preventing any additional exploration down that path. In your case, providing an avoid
of the failure of the first password means any paths failing the first password will be immediately discarded. Therefore, intentionally or not - thanks to your avoid
, no path is possible that does reach a success in the first password test.
There are several ways to properly solve this challenge and I'll go over them.
Increasing num_find
to more than one result
One might think a proper solution could be to increase num_find
to more than one, allowing more than one found
states to be reached before explorer
returns. That, however, is a mistake. Because your example code does not makes sure passwords are actually identical strings because there's no length validation and the null terminator is not compared, it appears any string starting with "first_pass"
should be considered a valid solution. Interestingly enough, this accidently allows more than one solution (and in reality, quite a lot), so increasing num_find
to any computationally feasible value will yield additional solutions to the first password, never reaching the second one.
btw, to fix the erroneous password verification condition, you can either verify lengths match, use a string comparison function (such as strcmp
and derivatives), or change the conditional from <
to <=
so it will include the null terminator.
separating the two tests
Since the two scenarios are completely independent of one another, you could run to executions. One for each password, setting a single find
and avoid
value for each, as well as setting the entry point for the second one. Splitting the two executions will effectively create a more simple set of constraints which should be executing faster. Of course, you can even run them in parallel to gain an additional performance advantage.
correctly specifying the success criteria
Although your values of find
and avoid
may initially seem correct, we've discussed several reasons why in reality they fail to accurately describe your intention. A more accurate specification of those parameters will be setting avoid
to both printf("fail");
statements, as you did. However you'll need to remove the first printf("gg")
and only keep the second, as it won't be reached without passing both passwords (assuming the logical bug mentioned above is fixed).