Angr, qira & PIN vs. ptrace anti-debug & anti-symbolic-execution
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:
- that the
ptrace
check for-1
fails and - if the first check is patched, we don't know if the subsequent
ptrace
calls return the same value they would if not run underQEMU
Now, 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 ./executable
Tracing 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:
- the address where we want to 'hook'
- what we want to do at that address
- the length of the instruction(s) we want to skip in bytes
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 ],EAX
So 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 = 0
p.hook(0x400948, hook_ptrace1, length=5)
Will do the following:
- Whenever address
0x400948
is executed, "ptrace1 hooked
" is printed andrax
is set to0
. - The actual instruction,
CALL ptrace
, is skipped
We 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 = 0
def hook_ptrace2(state):
print("ptrace2 hooked")
state.regs.rax = -1
def hook_ptrace3(state):
print("ptrace3 hooked")
state.regs.rax = -1
p.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.....