1

I just wonder to know, is there any change to manipulate it or break it on Windows?

For example ivgot a code like that:

while (1)
        {
            auto v6 = __rdtsc();
            switch (v6 & 0xF)
            {
                case 1ui64:
                    something();
                    break;
                case 2ui64:
                    something2();
                    break;
                default:
                    break;
            }
        }

and the request is simple as breaking the only 2ui64 case.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
ayya
  • 71
  • 5
  • why do you want to do that? For unit tests a mocking framework would be better – phuclv Oct 10 '22 at 17:12
  • Because I got a binary code which one hard to read. If i could do it, i can bypass the something2 function. – ayya Oct 10 '22 at 17:15
  • then what you want is patching. Replace that `rdtsc` with a call to your function that you can control – phuclv Oct 10 '22 at 17:20
  • @phuclv the point is before this replacement i can not put my code in process. – ayya Oct 10 '22 at 17:23
  • 1
    Run inside a VM; a hypervisor can configure it so `rdtsc` causes a VM exit, so it can put whatever it wants into EDX:EAX before resuming. Otherwise, modify the binary. You can't change how the CPU executes a `rdtsc` instruction, and `__rdtsc()` is an intrinsics that inlines that instruction, not calling anywhere you could hook. – Peter Cordes Oct 10 '22 at 18:09
  • I thought find another instruction to return zero, and patch the byte code with calling it – ayya Oct 10 '22 at 18:12
  • 3
    @ayya: If you can change instructions, just overwrite the entire `case 2:` part with `nop`. Way simpler. From the outside, you indeed end up with hypervisor hacks. There's no middle ground here. – MSalters Oct 10 '22 at 18:45
  • do you know case instruction ? because i only see jmp (switch table) – ayya Oct 10 '22 at 20:09

2 Answers2

5

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

  1. Install CMake. It's needed to build the DynamoRIO client (you can, of course, technically avoid using CMake by manually crafting your make file).

  2. 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.

  3. Download DynamoRIO and unpack it wherever you want. I'll use C:\dynamorio in the following.

  4. 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)
    
  5. 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 . 
    
  6. 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.

  7. 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[])
    {
    }
    
  8. 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:

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:

Fake timestamp

Margaret Bloom
  • 41,768
  • 5
  • 78
  • 124
2

Run inside a VM; a hypervisor can configure it so rdtsc causes a VM exit, so it can put whatever it wants into EDX:EAX before resuming.

You can't change how the CPU executes a rdtsc instruction, and __rdtsc() is an intrinsics that inlines that instruction, not calling anywhere you could hook.

So your only other option is to modify the binary, e.g. to replace rdtsc (2 bytes) with xor eax,eax (2 bytes), so the RDTSC return value is always 0.

Or mov al, 7 so __rdtsc() % 256 is always 7, if you want to make that switch take the default case. The full RDTSC output in EDX:EAX will include whatever values were in EDX and the higher bytes of EAX, but your code only depends on the low byte (in fact low nibble) of that integer value. (&0xF zeros the high bits.)

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Thank you for this awesome explanation. Returning 0 distressingly was not my solution, because it also made unuseful the other cases. And also it made same result with just nopping switch jmp – ayya Oct 10 '22 at 18:25
  • you can replace `rdtsc` with `JMP somewhere` and return the desired value from that faked function – phuclv Oct 11 '22 at 08:10
  • 1
    @phuclv: Yeah, if you can modify arbitrary instructions, not just `rdtsc` itself, it's trivial to make this function do what you want without ever running `rdtsc`. I guess I took the question more literally than I needed to, but it was kind fun coming up with a way to get the job done while actually replacing only the instruction corresponding to the `__rdtsc()` intrinsic. In case there was any value in that, or just as an exercise in creative thinking. – Peter Cordes Oct 11 '22 at 08:16
  • @PeterCordes could we check instructions on private message? – ayya Oct 11 '22 at 11:47
  • If you have a new question, you can ask it on Stack Overflow. I'm not interested in spending more time on this by email, especially not details of Windows executables since I don't use Windows. – Peter Cordes Oct 11 '22 at 12:00
  • When i get a view of your profile i thought you would interest it. But explaining it on sof is harder then you see how it works . Question is not about the executables, it is about compilers. – ayya Oct 11 '22 at 12:05