I stumbled across a very interesting anti-debugging technique using ptrace that also trips up Angr unless special ptrace behavior is explained to it. The executable under test produces a double ptrace call:
The first call is familiar. It's just a standard call with a 0/-1 check to see if the executable is being traced. But the second call return value is used in a calculation, and then again from a third call later. From Ghidra:
lVar2 = ptrace(PTRACE_TRACEME,0,0,0); local_c = (int)lVar2; if (local_c == -1) { FUN_00400786(); } lVar2 = ptrace(PTRACE_TRACEME,0,0); [...SNIP...] lVar2 = ptrace(PTRACE_TRACEME,0,0,0); local_50 = (uint)lVar2; local_c = local_50 + local_c + local_10 + local_14; if (local_c == 0) { puts("You win!"); }So what does a repeated ptrace(TRACEME,....) return in a non-debugged application? I did not know. And I could not find any documentation about this either. It could be an error, because we are already attached. Or it could be smart enough to see that it's the same PID attaching and simply succeed a second time. And not knowing, we can not just patch out any of the ptrace calls, because we need to retain the behavior. The first call may affect the second, and there is a third call later in the program as well. Interesting. We can invert or completely patch the jnz instruction after the -1 check, of course. To analyze a program's behavior, I like to use qira, and the standard qira uses QEMU in user mode. Apparently, this utilizes some sort of ptrace interface, and that implies:
ptrace check for -1 fails and ptrace calls return the same value they would if not run under QEMUNow, we could just write a small program that doubles up an ptrace calls and prints out the return values and then use that information to patch the executable. However, there is a provision to use a PIN tracing tool in qira, and that avoids the problem:
$ qira --pin ./executableTracing the program yields the very interesting information that a repeated call to ptrace(TRACEME,....) in a program that is not being debugged returns 0 for the first call and -1 for all subsequent calls.
Nice. Knowing that it was just the usual work flow of using qira-IDA-Ghidra-retdec-radare2-etc. to understand the program logic. The program applies some complicated formula to the input and requires some sort of constraint solver to get at the solution. And this brings us to the anti-symbolic-execution effect of the repeated ptrace calls.
This is a known issue; see this pull request discussion:
https://github.com/angr/simuvex/pull/78
and here:
https://github.com/angr/simuvex/pull/78/commits/90af0227e0c5d61a0756625a5d0e6c638363652e
For now, ptrace simply returns an unconstrained value.
Testing using the following hooks (more on hooks later):
def hook_ptrace1_before_call(state): print('rax before ptrace call 1: ' + str(state.regs.rax))def hook_ptrace1_after_call(state): print('rax after ptrace call 1: ' + str(state.regs.rax))results in:
rax before ptrace call 1: <SAO <BV64 0x0>>rax after ptrace call 1: <SAO <BV64 unconstrained_ret_ptrace_15_64{UNINITIALIZED}>>However, the right return value of ptrace is critical to yielding a properly constrained system. Without it we get an explosion of the solution space.
Do do this, what we can do is 'hook' the execution of the ptrace calls. A hook is a mechanism allowing the change the behavior of the program flow. We need 3 pieces of information:
Lets look at it. From Ghidra:
00400943 b8 00 00 MOV EAX ,0x0 00 00 00400948 e8 03 fd CALL ptrace long ptrace(__ptrace_request __r ff ff 0040094d 89 45 fc MOV dword ptr [RBP + local_c ],EAXSo the first call to ptrace is at address 0x00400948. and the instruction is 5 bytes long. Lets take the test hook from above as an example:
p = angr.Project('./executable',auto_load_libs=False)def hook_ptrace1_before_call(state): print('rax before ptrace call 1: ' + str(state.regs.rax))p.hook(0x400948, hook_ptrace1_before_call,0)The first piece of information, the address, is the first parameter to the hook() call. The second piece of information, what we want to do, is the function hook_ptrace1_before_call(), which is the second parameter to the hook() call. The third piece of information, the length of instructions to skip, is zero in this case, because we don't want to skip any instructions. We are just printing out the state of rax.
However, to solve this challenge, we do want to skip the actual ptrace call and furthermore simulate it's effect on rax. The X86 instuction set has variable length instructions, but we can see from the Ghidra snippet above that 5 bytes are used for the ptrace call. So,
def hook_ptrace1(state): print("ptrace1 hooked") state.regs.rax = 0p.hook(0x400948, hook_ptrace1, length=5)Will do the following:
0x400948 is executed, "ptrace1 hooked" is printed and rax is set to 0.CALL ptrace, is skippedWe can then combine that with further hooks for the other ptrace calls to force the behavior we learned from the PIN tracer:
def hook_ptrace1(state): print("ptrace1 hooked") state.regs.rax = 0def hook_ptrace2(state): print("ptrace2 hooked") state.regs.rax = -1def hook_ptrace3(state): print("ptrace3 hooked") state.regs.rax = -1p.hook(0x400948, hook_ptrace1, length=5)p.hook(0x400979, hook_ptrace2, length=5)p.hook(0x400BEB, hook_ptrace3, length=5)And after that the rest is just standard angr solving for the flag.....