In this blog post, i’ll explain how to trace and manipulate a program with DynamoRIO. I’ll use a simple program to explain the concepts (Source code is below this post).
Table Of Contents
1. DynamoRIO Basics 1.1 What Is DynamoRIO 1.2 How It Works 1.2.1 Code Cache and Basic Block 2. Call Tracing With DynamoRIO 2.1 Libraries 2.2 Interface Functions 3. Manipulating Anti-Detection Techniques 3.1 Create Test Application 3.2 Manipulate with DrWrap
What Is DynamoRIO
DynamoRIO is a dynamic binary instrumentation framework. It is a tool that can be used to very purposes but we’ll use it for tracing and manipulating and It’s open source, can be found on GitHub
from DynamoRIO’s website:
DynamoRIO is a runtime code manipulation system that supports code transformations on any part of a program, while it executes. DynamoRIO exports an interface for building dynamic tools for a wide variety of uses: program analysis and understanding, profiling, instrumentation, optimization, translation, etc. Unlike many dynamic tool systems, DynamoRIO is not limited to insertion of callouts/trampolines and allows arbitrary modifications to application instructions via a powerful IA-32/AMD64/ARM/AArch64 instruction manipulation library. DynamoRIO provides efficient, transparent, and comprehensive manipulation of unmodified applications running on stock operating systems (Windows, Linux, or Android, with experimental Mac support) and commodity IA-32, AMD64, ARM, and AArch64 hardware.
DynamoRIO presented by Derek L. Bruening with a whitepaper at 2004 September. It’s a very old project but still maintained and developed. This whitepaper title is “Efficient, Transparent, and Comprehensive Runtime Code Manipulation”. At the beginning of the article they explain their goals and then they explain how DynamoRIO simply works. We need to use this library when writing any client. It contains the main functions that DynamoRIO will run on the client.
How It Works
DynamoRIO interposes itself between an application and the underlying operating system and hardware. It executes a copy of the application’s code out of a code cache to avoid emulation overhead.
We will do all of our work in the Code Cache section. Therefore, that is the point I want to specifically mention. With the “Code Cache” technology specially designed in DynamoRIO, we can monitor and modify each instruction before it runs. DynamoRIO creates a special section called Code Cache. DynamoRIO has got full authority over this section.
The code cache enables native execution to replace emulation, bringing performance down from a several hundred times slowdown for pure emulation to an order of magnitude.
Instrumented application’s instructions is keep in Code Cache as “basic block”.
In fact, every structure in DynamoRIO is very detailed. But I want to go directly to the application part without explaining the details too much.
Call Tracing With DynamoRIO
We have got a very basic example application for tracing. Source code:
Let’s make a tracer with DynamoRIO’s basic functions. Firstly, we need DynamoRIO’s libraries so i’ll include these on my project
utils.h need for logging operations never mind it.
dr_api.h we need to use this library when writing any client. It contains the main functions that DynamoRIO will run on the client.
drmgr.h multi instrumentation library that contains functions to manage basic block operations and instruction operations.
Now we will define the 4 functions we need to define in the basic structure of the client. Thread init and exit, client exit (event exit) and the function that manages basic blocks.
Ok, now we can write the dr_client_main function which is the EntryPoint of the application. In this function we will save the event functions we defined before.
event_app_instruction: This function is intercept all instruction at runtime. Our goal is to show the user ONLY where the CALL instructions in the target application make calls (except system dlls). For this, we need to write a function that will execute every time a CALL instruction is caught.
After writing the function that will be executed during each CALL instruction (at_call_ind), I write the print_address function for a different logging operation. This function presents the symbols of the requesting and received address to the user.
Now let’s see how it comes out.
%utku> .\drrun.exe -c maestro.dll -- "API Test Dynamo.exe" Scope: API Test Dynamo.exe Client maestro is running Data file utku\Desktop\maestro.API Test Dynamo.exe.09628.0000.log created Hello World! Wrote 7 bytes to "utku\Desktop\merhaba.txt" successfully.
CALL INDIRECT @ 0x00007ff642a810e3 API Test Dynamo.exe!main to 0x00007ffa20f47c40 KERNEL32.dll!IsDebuggerPresent ??:0 CALL INDIRECT @ 0x00007ff642a812ab API Test Dynamo.exe!std::operator<<<> to 0x00007ff9f20c6770 MSVCP140.dll!std::ios_base::_Init ??:0 CALL INDIRECT @ 0x00007ff642a8134e API Test Dynamo.exe!std::operator<<<> to 0x00007ff9f20c8fb0 MSVCP140.dll!std::ctype<>::toupper ??:0 CALL INDIRECT @ 0x00007ff642a813bc API Test Dynamo.exe!std::operator<<<> to 0x00007ff9f20c8d80 MSVCP140.dll!std::basic_ios<>::setstate ??:0 CALL INDIRECT @ 0x00007ff642a813c3 API Test Dynamo.exe!std::operator<<<> to 0x00007ff9f20d3ec0 MSVCP140.dll!std::uncaught_exception ??:0 CALL INDIRECT @ 0x00007ff642a813d0 API Test Dynamo.exe!std::operator<<<> to 0x00007ff9f20cb290 MSVCP140.dll!std::basic_ostream<>::_Osfx ??:0 CALL INDIRECT @ 0x00007ff642a813eb API Test Dynamo.exe!std::operator<<<> to 0x00007ff9f20c69e0 MSVCP140.dll!std::ios_base::_Tidy ??:0 CALL INDIRECT @ 0x00007ff642a81184 API Test Dynamo.exe!main to 0x00007ffa20f50180 KERNEL32.dll!CreateFileW ??:0 CALL INDIRECT @ 0x00007ff642a811c3 API Test Dynamo.exe!main to 0x00007ffa20f50610 KERNEL32.dll!WriteFile ??:0 CALL INDIRECT @ 0x00007ff642a81038 API Test Dynamo.exe!wprintf to 0x00007ffa1f6a7d40 ucrtbase.dll!_acrt_iob_func ??:0 CALL INDIRECT @ 0x00007ff642a81057 API Test Dynamo.exe!wprintf to 0x00007ffa1f682330 ucrtbase.dll!_stdio_common_vfwprintf ??:0 CALL INDIRECT @ 0x00007ff642a81200 API Test Dynamo.exe!main to 0x00007ffa20f4ff00 KERNEL32.dll!CloseHandle ??:0 CALL INDIRECT @ 0x00007ff642a81d6a API Test Dynamo.exe!__scrt_is_managed_app to 0x00007ffa20f46580 KERNEL32.dll!GetModuleHandleW ??:0
Manipulating Anti-Detection Techniques
In this section we will manipulate the anti-debug (IsDebuggerPresent) and anti-vmware technique. Of course, I start by writing a test application first.
Create Test Application
My test application is checking debugger with IsDebuggerPresent API and checking VMWare tool reg key.
Manipulate with DrWrap
I mentioned that DynamoRIO has a lot of authority over the virtual memory it generates and gives us a lot of possibilities. DrWrap is one of those possibilities… Function Wrapping and Replacing
A library that allows us to run before and after the target function and to analyze the instructions before the function runs. I will show the structures that we need to use specially in the application section.
Now, let’s write main and register our functions. There are several functions and structures we will meet here. I will explain them after writing.
drmgr_register_module_load_event: This function is called when the module is loaded. In order to search for the functions we want to hook, we need to run an event every time a module is loaded. So we create a function.
Firstly, we need to examine wrap func and its args:
DR_EXPORT bool drwrap_wrap ( app_pc func, void(*)(void *wrapcxt, OUT void **user_data) pre_func_cb, void(*)(void *wrapcxt, void *user_data) post_func_cb )
Our bypass method here is to bypass the checks by treating the value returned by the function as harmless. We can specify individual functions to run before and after the target function. But according to our bypass method, there is no need to run anything else before.
%utku> .\drrun.exe -c wrap.dll -- "API Test Dynamo.exe" Client maestro2 is running RegOpenKeyExW post wrap RegOpenKeyExW retval is 1 IsDebuggerPresent post wrap IsDebuggerPresent retval is 0 RegOpenKeyExW post wrap RegOpenKeyExW retval is 1 IsDebuggerPresent post wrap IsDebuggerPresent retval is 0 debugger not detected Clear Hello World! Wrote 5 bytes to "hello.txt" successfully.