I had a similar case with GuLoader (a malware packer) that used rdtsc
to profile the execution of CPUID.1
to tell if it's running inside a VM.
Changing the hypervisor was not an option for me, because rdtsc
is used by many other legitimate programs.
Patching the binary was also not an option because the packer was a shellcode encoded (multiple times) in a host program and making a patch for every possible polymorphic shellcode was too much work.
So I went for Dynamic Binary Instrumentation with DynamoRIO.
Installing DynamoRIO and compiling your own client is a bit tricky the first time but it's all documented.
You can use instr_get_opcode
to test if an instruction is OP_rdtsc
and then insert two (meta) instructions that override edx:eax
:
instrlist_meta_postinsert(bb, inst, INSTR_CREATE_mov_imm(drcontext, opnd_create_reg(DR_REG_XDX), opnd_create_immed_uint(0xBADF00D, OPSZ_4)));
instrlist_meta_postinsert(bb, inst, INSTR_CREATE_mov_imm(drcontext, opnd_create_reg(DR_REG_XAX), opnd_create_immed_uint(0xBADF00D, OPSZ_4)));
Installing the toolchain
Install CMake. It's needed to build the DynamoRIO client (you can, of course, technically avoid using CMake by manually crafting your make file).
Install a C compiler. For Windows this is a bit tricky. Visual Studio comes with MSVC, otherwise you can use MinGW-w64 through MSYS2 or Cygwin.
Download DynamoRIO and unpack it wherever you want. I'll use C:\dynamorio
in the following.
Make a new folder rdtsc_patcher and create a new file named CMakeList.txt inside of it. Paste this into the file:
project(rdtsc_patcher)
cmake_minimum_required(VERSION 3.0)
add_library(rdtsc_patcher SHARED rdtsc_patcher.c)
find_package(DynamoRIO)
if (NOT DynamoRIO_FOUND)
message(FATAL_ERROR "DynamoRIO package required to build")
endif(NOT DynamoRIO_FOUND)
configure_DynamoRIO_client(rdtsc_patcher)
Run CMake to make the Makefile. Open a command prompt, go to the rdtsc_patcher folder and run CMake passing it the DynamoRIO cmake directory:
rdtsc_patcher> cmake -DDynamoRIO_DIR=C:\dynamorio\cmake .
If CMake was able to find a build toolchain correctly you should name be able to run make
or nmake
. I have a MSVC++ 6.0 ('98 era) toolchain in this VM so I use nmake
.
Write an empty client to test the build process. Paste this code to a new file named rdtsc_patcher.c:
#include "dr_api.h"
DR_EXPORT void dr_client_main(client_id_t id, int argc, const char *argv[])
{
}
Run make
/nmake
. If everything is fine, a rdtsc_patcher.dll will be built. From now on you only need to repeat this step to build the DLL (unless you later need to add some DynameRIO extension, like drmgr or drutil).
Making the client
Read the DynamoRIO documentation to get the gist of it. It basically works by walking the program's basic blocks and giving you a chance of altering them before executing them. Please note that each BB is translated only once and then cached.
DynamoRIO is a quite involved ecosystem but it's very powerful, we don't need things such as DrManager to orchestrate multiple clients. In fact, our client is pretty straightforward.
Write this to the rdtsc_patcher.c file and then run make
/nmake
:
#include "dr_api.h"
/* This function is called for each basic block */
static dr_emit_flags_t
event_basic_block(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating)
{
instr_t *instr, *next_instr;
/* Scan each BB's instruction */
for (instr = instrlist_first(bb); instr != NULL; instr = next_instr)
{
next_instr = instr_get_next(instr);
/* Skip invalid instructions (should not happen) */
if (!instr_opcode_valid(instr))
continue;
/* Ignore any non RDTSC */
if (instr_get_opcode(instr) != OP_rdtsc)
continue;
dr_printf("Found rdtsc at %x\n", instr_get_app_pc(instr));
/* You can perform additional checks if needed:
next_instr is the next instruction. instr_get_next(next_instr) is the next next instruction and so on.
next_instr can be NULL if instr is the last instruction in a BB!
instr_get_app_pc(instr) is the EIP/RIP of the instruction. Programs are relocated but the least 12-bit of the
address are fixed. This gives 1/4096 chance of targeting the wrong RDTSC instruction (if we tested the EIP/RIP too)
*/
/* Insert mov eax, imm32 */
instrlist_meta_postinsert(bb, instr, INSTR_CREATE_mov_imm(drcontext, opnd_create_reg(DR_REG_XAX), opnd_create_immed_uint(0xBADF00D, OPSZ_4)));
/* Insert mov edx, imm32 */
instrlist_meta_postinsert(bb, instr, INSTR_CREATE_mov_imm(drcontext, opnd_create_reg(DR_REG_XDX), opnd_create_immed_uint(0xCAFEBABE, OPSZ_4)));
}
return DR_EMIT_DEFAULT;
}
DR_EXPORT void dr_client_main(client_id_t id, int argc, const char *argv[])
{
/* Register for the BB event */
dr_register_bb_event(event_basic_block);
/*
If you want to print to the console used to run the client, call
dr_enable_console_printing();
here in main and use
dr_printf("Example %x\n", instr_get_app_pc(instr));
For logging use:
dr_log(NULL, DR_LOG_ALL, 1, "Example\n");
but you need to enable logging when using drrun.
*/
dr_enable_console_printing();
}
Run and test the client
To run the client use drrun
:
rdtsc_patcher> c:\dynamorio\bin32\drrun -c32 rdtsc_patcher.dll -- <target program>
Since I was limited to a '98 compiler I could only make a 32-bit client (thus the -c32
instead of -c
/-c64
).
I made a simple program that shows the timestamp counter (again, remember this is MSVC++ 98 code):
#include <windows.h>
#include <stdio.h>
__int64 __rdtsc()
{
_asm rdtsc
}
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
)
{
__int64 count = __rdtsc();
char buffer[20] = {0};
sprintf(buffer, "%I64x", count);
MessageBox(NULL, buffer, "Title", MB_ICONINFORMATION);
}
When run normally, it will show the real timestamp:

When run with:
rdtsc_patcher> c:\dynamorio\bin32\drrun -c32 rdtsc_patcher.dll -- ..\rdtsc_test\Debug\rdtsc_test.exe
it will show the fake timestamp:
