Few days ago Xjun briefly mentioned a new feature of VMProtect 3.1 - it uses direct syscalls to check if software is running under debugger. I decided to take a quick look myself and figure out how exactly it works.
I'll give you 2 targets for the research:
oppo_flash_tool.exe (https://mega.nz/#!ZgJzjQxR!cNEHMwM-jKnLVgPXf4OUupyk1DNt69FYB2rEfY-5AlA) - it was given as an example by Xjun;
asshurt.dll (https://mediafire.com/?3xyc0ugc2hxervn) - some sort of a cheat for Roblox. I don't care about the cheat itself, it just happened to have the syscall feature enabled. And it uses different syscalls than Oppo.
In addition to that, I'll provide a very simple demo executable which replicates part of the VMProtect protection, so that you don't waste time looking at obfuscated code.
As a debugger in 32-bit OS you can use anything you like. On 64-bit OS you will really need to use WinDbg - as far as I know, it's the only debugger that can handle those tricks..
32bit OS
Let's start by debugging oppo_flash_tool.exe. First, we need to get past the usual tricks like IsDebuggerPresent and CheckRemoteDebuggerPresent. If you're reading this, I'm sure you know how to do that.
Few moments later we'll arrive here:
[Asm] 纯文本查看复制代码
00dcfd8e f744250000800000 test dword ptr [ebp],8000h
00dcfd96 e9f6bdfdff jmp 00dabb91
...
00dabb91 0f84841a0b00 je 00e5d61b
Remember this conditional jump. It's taken on 32bit OSes and not taken on 64-bit OS.
Let's look at 32bit OS version first. Now VMProtect prepares to call sysenter.
Since the code is obfuscated, here comes a cleaned-up version. Please note that in other applications, different registers can be used.
[Asm] 纯文本查看复制代码
;input:
; ECX = number of parameters for the syscall
; [EBP] = syscall id. See https://github.com/tinysec/windows-syscall-table
; [EBP+4] .. [EBP+X] = params for the syscall
; [EBP-4] .. [EBP-8] = free space to save registers
push esi
push edi
push ebx
; save register values for later
lea eax, [ebp+ecx*4]
mov dword ptr [ebp-4], eax
mov dword ptr [ebp-8], esp
; set up stack frame for syscall
setupParams:
mov eax, dword ptr [ebp+ecx*4]
push eax
sub ecx, 1
jnz setupParams
; put syscall number in EAX
mov eax, dword ptr [ebp]
; the actual call
call trampoline1
; restore stack and frame pointers
mov esp, dword ptr [ebp-8]
mov ebp, dword ptr [ebp-4]
; save result
mov dword ptr [ebp], eax
; restore registers
pop ebx
pop edi
pop esi
jmp 00de21de
trampoline1:
call trampoline2
retn
trampoline2:
mov edx, esp
sysenter
retn
// -- continue VM execution as usual --
00de21de 8b06 mov eax,dword ptr [esi]
00de21e2 8db604000000 lea esi,[esi+4]
00de21ea 33c3 xor eax,ebx
00de21ec 8d807cc2efb1 lea eax,[eax-4E103D84h]
00de21f6 f7d0 not eax
00de21fd 35ee613a76 xor eax,763a61ee
00df9a28 48 dec eax
00df9a29 f8 clc
00df9a2a c1c002 rol eax,2
00df9a2e 33d8 xor ebx,eax
00df9a32 03f8 add edi,eax
00df9a34 e9a0eafbff jmp 00db84d9
00db84d9 ffe7 jmp edi
...
00ddaeba 8b542500 mov edx,dword ptr [ebp]
64-bit OS
Here it is getting interesting! smile You cannot use sysenter instruction from 32-bit code in 64-bit Windows. But, as ReWolf described few years ago, one can mix x86 code with x64 code in the same process. And that's exactly what VMProtect 3.1 is doing.
Let's go back to that conditional jump and see what happens in 64-bit OS. The jump will not be taken:
x64 code does pretty much the same thing as x86 code - sets up a stack frame, sets up registers and then executes syscall instruction. Cleaned-up and shortened version follows:
[Asm] 纯文本查看复制代码
;input:
; ECX = number of parameters for the syscall
; [EBP] = encoded syscall id.
; High order byte = special handling info
; Lowest 15 bits = syscall id
; [EBP+4] .. [EBP+X] = params for the syscall
; [EBP-8] .. [EBP-10] = free space to save registers
; [EBP-..] = free space to use in specific syscalls
push rsi
push rdi
push rbx
mov ebx,ecx
mov edx,ebx
xor ecx,ecx
; calculate new stack frame pointer
cmp ebx,4
jbe @F
lea ecx,[rbx-4]
@@:
shl ecx,3
shl edx,2
mov rax,rbp
add rax,rdx
; save registers
mov qword ptr [rbp-8],rax
mov qword ptr [rbp-10h],rsp
; adjust RSP
sub rsp,rcx
and rsp,0FFFFFFFFFFFFFFF0h
add rsp,rcx
; useless?
mov r10d,dword ptr [rbp]
shr r10d,9
; set up params for syscall
test ebx,ebx
je doneSettingParams
loopSetParams:
mov eax,dword ptr [rbp+rbx*4]
cmp ebx,1
jne @F
mov rcx,rax
jmp nextParam
@@:
cmp ebx,2
jne @F
mov rdx,rax
jmp nextParam
@@:
cmp ebx,3
jne @F
mov r8,rax
jmp nextParam
@@:
cmp ebx,4
jne @F
mov r9,rax
jmp nextParam
@@:
push rax
nextParam:
sub ebx,1
jne loopSetParams
doneSettingParams:
; check if syscall needs special handling
mov rax,qword ptr [rbp]
mov r10d,eax
shr r10d,18h
; 3 = NtQueryInformationProcess
cmp r10b,3
jne doSyscall
; fix current process pseudo-handle, if it's there
cmp ecx,0FFFFFFFFh
jne @F
movsx rcx,cl
@@:
; is this ProcessDebugObjectHandle request?
cmp edx,1Eh
jne @F
; if so, fix buffer and size for ProcessDebugObjectHandle request.
; It should be 8-bytes long.
lea r10,[rbp-18h]
mov r8,r10
mov r9d,8
@@:
jmp doSyscall
doSyscall:
and eax,7FFFh
sub rsp,20h
call trampoline
jmp processResult
trampoline:
mov r10,rcx
syscall
ret
processResult:
; check for special handling again
mov r10d,dword ptr [rbp]
shr r10d,18h
; 3 = NtQueryInformationProcess
cmp r10b,3
jne returnToX86
; is this ProcessDebugObjectHandle ?
cmp dword ptr [rbp+8],1Eh
jne returnToX86
; were 2 buffers the same in original call?
mov ecx,dword ptr [rbp+0Ch]
cmp ecx,dword ptr [rbp+14h]
je @F
; if not, copy returned DebugObjectHandle back to original buffer
mov r10d,dword ptr [rbp-18h]
mov dword ptr [rcx],r10d
@@:
jmp returnToX86
returnToX86:
nop
mov rsp,qword ptr [rbp-10h]
mov rbp,qword ptr [rbp-8]
mov dword ptr [rbp],eax
pop rbx
pop rdi
pop rsi
retf
You'll notice that x64 version is slightly more complex due to the way parameters are passed (registers vs. stack). It also includes a special treatment for 8 special edge cases - it will modify syscall parameters to adjust buffers and pointer sizes to satisfy requirements for 64-bit code.
NOTE - to keep code simple, I only showed the part which deals with NtQueryInformationProcess but other cases are similar.
As you can see, return back from x64 to the x86 world is a simple retf instruction. x86 code continues right where it left off:
Instruction at address 0x00ddaeba is the same for both x86 and x64 OS-es and VM continues as usual.
Different protection modes and syscalls
I provided you with 2 real-world test executables. Oppo seems to be simpler and use just 3 syscalls:
NtQueryInformationProcess with ProcessDebugObjectHandle class
NtSetInformationThread with ThreadHideFromDebugger class
NtProtectVirtualMemory to set protection attributes for each section in original executable
Asshurt doesn't have antidebug trick with NtQueryInformationProcess but it uses additional syscalls for some purposes:
Since VMProtect is using undocumented Windows features, it somehow needs to ensure that the protection will work on each and every Windows version. That's VMProtect's biggest strength and also the biggest weakness.
Windows' syscall numbers change in each version and also between major builds. Use the wrong syscall number and you're guaranteed to receive unexpected results. So, VMProtect developers had to hardcode a table with Windows build numbers and corresponding syscall id's in the executable.
To obtain Windows build number, VMProtect uses information from PEB (Process Environment Block). The method is already described in The MASM Forum, so I'll just reproduce the (ugly) code from their page:
[Asm] 纯文本查看复制代码
print "Read From Process Environment Block:",13,10
ASSUME FS:Nothing
mov edx,fs:[30h] ;PEB.InheritedAddressSpace
ASSUME FS:ERROR
mov eax,[edx+0A4h] ;eax = Major Version
push eax
push edx
print ustr$(eax),'.'
pop edx
push edx
mov eax,[edx+0A8h] ;eax = Minor Version
print ustr$(eax),'.'
pop edx
mov eax,[edx+0ACh] ;eax = build
and eax,0FFFFh ;because win 7 collapses
print ustr$(eax),13,10,13,10
pop eax
VMProtect checks only the build number and picks the corresponding syscall number. However, if the build number is not in the internal database, it will not use direct syscall and fall back to standard protection. Bingo, problem solved - no need for ugly hacks like Xjun's SharpOD plugin!
Hint: VMProtect 3.1 doesn't support Windows 10 Creators Update (build number 15063).
Demo time
As promised, here is a download link for the test application: https://mediafire.com/?niqqbs0fqcq8n23
Note: it should support most common builds of Windows XP/7/8.1/10. Windows 2003/Vista and other rare systems are not supported!
If it shows "OK" message, you've hidden your debugger well. If it shows "Debugger detected", you have a problem.
Have fun!
kao.
EDIT: Updated download link for Oppo. Mediafire's antivirus tends to have plenty of False Positives..