Share this
Dynamic Binary Instrumentation with Intel Pin
by Reflare Research Team on Feb 21, 2026 2:29:31 PM
A Powerful Tool for Reverse Engineers and Security Researchers
Some programs only make sense once they’re in motion. Reverse engineering gets easier when you can follow every step, not just guess the next one, and turn runtime behaviour into evidence you can trust.
.jpg?width=1200&height=800&name=Dynamic%20Binary%20Instrumentation%20with%20Intel%20Pin%20(1200).jpg)
Precision cuts tough targets
When you're reverse engineering a suspicious binary or solving a challenging CTF problem, sometimes static analysis just doesn't cut it. You need to see what's actually happening when the code runs, trace every instruction, monitor memory accesses, and understand the program's runtime behaviour without modifying the original executable. This is where Intel Pin Tool becomes an invaluable asset in your security toolkit.
What is Intel Pin?
Intel Pin is a dynamic binary instrumentation (DBI) framework that allows you to insert arbitrary code into running programs. Think of it as a sophisticated microscope for executable files that lets you observe and analyse program behaviour at an incredibly granular level without needing the source code or even recompiling the binary.
The framework works by intercepting a program's execution and inserting instrumentation code at various points, whether that's before specific instructions, after function calls, or during memory operations. What makes Pin particularly powerful is that it operates at the binary level, meaning you can analyse compiled executables regardless of the programming language they were written in or whether debug symbols are available.
Pin consists of two main components: the Pin runtime system itself, which handles the heavy lifting of code injection and program manipulation, and Pintools, which are the instrumentation plugins you write to define what you want to observe or modify. Intel provides Pin for Linux, Windows, and macOS platforms, making it versatile across different operating systems commonly encountered in security research.
The Architecture Behind the Magic
Understanding how Pin works helps appreciate its capabilities. When you run a program under Pin, the framework doesn't directly execute the original binary. Instead, it performs just-in-time (JIT) compilation on the executable code. Pin fetches sequences of instructions from the original program, adds your instrumentation code where specified, and then executes this instrumented version.
This approach provides several advantages. First, the original binary remains unchanged on disk, which is crucial when analysing malware that might detect modifications. Second, the instrumentation overhead, while not negligible, is generally manageable for most analysis tasks. Third, you get complete control over what aspects of execution you want to monitor without being limited by what a debugger typically exposes.
The Pin API is designed around the concept of instrumentation routines and analysis routines. Instrumentation routines run once when Pin encounters new code, allowing you to decide where to insert your analysis code. Analysis routines are the callbacks that execute during program runtime, collecting the actual data you're interested in. This two-phase approach enables efficient instrumentation by separating the "where to instrument" decision from the "what to collect" execution.
A Practical Example: Building Our First Pintool
Let's dive into a concrete example. Consider this simple target program that might represent a basic CTF crackme challenge:
// target.c
#include <stdio.h>
#include <string.h>
int check_password(char *input) {
char secret[] = "sup3r_s3cr3t";
return strcmp(input, secret);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <password>\n", argv[0]);
return 1;
}
printf("Checking password...\n");
if (check_password(argv[1]) == 0) {
printf("Access granted! Flag: CTF{you_found_me}\n");
} else {
printf("Access denied!\n");
}
return 0;
}
Compile this with MSVC:
cl /Od /MD target.c
Now, let's write a Pintool to trace all instruction executions and see what the program is doing:
// inscount.cpp - Simple instruction counter
#include "pin.H"
#include <iostream>
#include <fstream>
std::ofstream outFile;
UINT64 icount = 0;
// Analysis routine - called for every instruction
VOID docount() {
icount++;
}
// Instrumentation routine - called once per instruction
VOID Instruction(INS ins, VOID *v) {
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}
// Called when program exits
VOID Fini(INT32 code, VOID *v) {
outFile << "Total instructions executed: " << icount << std::endl;
outFile.close();
}
INT32 Usage() {
std::cerr << "This tool counts the number of instructions executed" << std::endl;
return -1;
}
int main(int argc, char *argv[]) {
if (PIN_Init(argc, argv)) return Usage();
outFile.open("inscount.out");
INS_AddInstrumentFunction(Instruction, 0);
PIN_AddFiniFunction(Fini, 0);
PIN_StartProgram();
return 0;
}
To compile the Pintool on Windows, copy the code above to:
source\tools\MyPinTool
Then run make:
C:\Users\ctfman\Documents\Reflare\pintool\source\tools\MyPinTool>make TARGET=intel64
mkdir -p obj-intel64/
make objects
make[1]: Entering directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make[1]: Nothing to be done for 'objects'.
make[1]: Leaving directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make libs
make[1]: Entering directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make[1]: Nothing to be done for 'libs'.
make[1]: Leaving directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make dlls
make[1]: Entering directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make[1]: Nothing to be done for 'dlls'.
make[1]: Leaving directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make apps
make[1]: Entering directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make[1]: Nothing to be done for 'apps'.
make[1]: Leaving directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
make tools
make[1]: Entering directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
../../../intel64/pinrt/bin/pin-clang-cl++ -Wno-non-c-typedef-for-linkage -Wno-microsoft-include -Wno-unicode -I../../../source/include/pin -I../../../source/include/pin/gen -I../../../intel64/pinrt/include/adaptor -I../../../extras/components/include -I../../../extras/xed-intel64/include/xed -I../../../source/tools/Utils -MD -O2 -c -Foobj-intel64/inscount.obj inscount.cpp
../../../intel64/pinrt/bin/pin-ld -dll -c++ -EXPORT:main -INCREMENTAL:NO -IGNORE:4210 -IGNORE:4049 -DYNAMICBASE -NXCOMPAT -OPT:REF -out:obj-intel64/inscount.dll obj-intel64/inscount.obj -LIBPATH:../../../intel64/lib -LIBPATH:../../../extras/xed-intel64/lib pin.lib pinrt-adaptor-static.lib xed.lib kernel32.lib
make[1]: Leaving directory '/cygdrive/c/Users/ctfman/Documents/Reflare/pintool/source/tools/MyPinTool'
C:\Users\ctfman\Documents\Reflare\pintool\source\tools\MyPinTool>
Please note that you must have GNU make (from Cygwin) and clang-cl already installed. You can download those tools at the following links:
- https://www.cygwin.com/
- https://github.com/llvm/llvm-project/releases
Also, don't forget to edit the makefile.rules and change the TEST_TOOL_ROOTS line to:
TEST_TOOL_ROOTS := inscount
Run it:
pin.exe -t source\tools\MyPinTool\obj-intel64\inscount.dll -- target.exe wrongpassword
C:\Users\ctfman\Documents\Reflare>pintool\pin.exe -t pintool\source\tools\MyPinTool\obj-intel64\inscount.dll -- target.exe wrongpasswd
Checking password...
Access denied!
C:\Users\ctfman\Documents\Reflare>type inscount.out
Total instructions executed: 2156764
C:\Users\ctfman\Documents\Reflare>pintool\pin.exe -t pintool\source\tools\MyPinTool\obj-intel64\inscount.dll -- target.exe sup3r_s3cr3t
Checking password...
Access granted! Flag: CTF{you_found_me}
C:\Users\ctfman\Documents\Reflare>type inscount.out
Total instructions executed: 2158778
C:\Users\ctfman\Documents\Reflare>
This basic example counts instructions, but let's create something more useful for CTF analysis.
Advanced Example: Tracing String Comparisons
For the strcmp tracer to work, we need the target to dynamically link the C runtime. This is critical: on modern Windows with MSVC, the /MD flag links against ucrtbase.dll (the Universal CRT), which Pin can see and instrument. Without /MD, the compiler may statically embed strcmp into the binary, making it invisible to function-level hooks. Note that Pin can still intercept inlined comparisons through instruction-level instrumentation (e.g., hooking the repe cmpsb instructions that MSVC emits for inlined strcmp), but we use function-level hooking here to keep things simple.
cl /Od /MD target.c
Where:
/Od: disables optimisation (prevents inlining)/MD: links against the dynamic CRT (ucrtbase.dll), ensuring strcmp is an actual DLL call that Pin can intercept
Now, here's a more powerful Pintool that specifically targets string comparison functions - exactly what we need to crack our password checker:
// strcmp_tracer.cpp
#include "pin.H"
#include <iostream>
#include <fstream>
#include <string>
std::ofstream outFile;
std::string SafeReadString(ADDRINT addr, size_t maxLen = 256) {
std::string result;
char c;
for (size_t i = 0; i < maxLen; i++) {
if (PIN_SafeCopy(&c, reinterpret_cast<const void*>(addr + i), 1) != 1)
break;
if (c == '\0') break;
result += c;
}
if (result.empty()) result = "<unreadable>";
return result;
}
std::string SafeReadWString(ADDRINT addr, size_t maxLen = 256) {
std::string result;
wchar_t wc;
for (size_t i = 0; i < maxLen; i++) {
if (PIN_SafeCopy(&wc, reinterpret_cast<const void*>(addr + i * sizeof(wchar_t)), sizeof(wchar_t)) != sizeof(wchar_t))
break;
if (wc == L'\0') break;
result += (char)wc;
}
if (result.empty()) result = "<unreadable>";
return result;
}
VOID BeforeStrCmp(ADDRINT arg1, ADDRINT arg2, const char *funcName) {
outFile << funcName << " called:" << std::endl;
outFile << " Arg1: \"" << SafeReadString(arg1) << "\"" << std::endl;
outFile << " Arg2: \"" << SafeReadString(arg2) << "\"" << std::endl;
outFile << std::endl;
outFile.flush();
}
VOID BeforeMemCmp(ADDRINT arg1, ADDRINT arg2, ADDRINT size, const char *funcName) {
outFile << funcName << " called (size=" << size << "):" << std::endl;
outFile << " Arg1: \"" << SafeReadString(arg1, (size_t)size) << "\"" << std::endl;
outFile << " Arg2: \"" << SafeReadString(arg2, (size_t)size) << "\"" << std::endl;
outFile << std::endl;
outFile.flush();
}
VOID BeforeWcsCmp(ADDRINT arg1, ADDRINT arg2, const char *funcName) {
outFile << funcName << " called:" << std::endl;
outFile << " Arg1: \"" << SafeReadWString(arg1) << "\"" << std::endl;
outFile << " Arg2: \"" << SafeReadWString(arg2) << "\"" << std::endl;
outFile << std::endl;
outFile.flush();
}
VOID HookStrCmp(IMG img, const char *name) {
RTN rtn = RTN_FindByName(img, name);
if (RTN_Valid(rtn)) {
RTN_Open(rtn);
RTN_InsertCall(rtn, IPOINT_BEFORE, (AFUNPTR)BeforeStrCmp,
IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
IARG_FUNCARG_ENTRYPOINT_VALUE, 1,
IARG_ADDRINT, (ADDRINT)name,
IARG_END);
RTN_Close(rtn);
outFile << "[*] Instrumented " << name << " in " << IMG_Name(img) << std::endl;
}
}
VOID HookMemCmp(IMG img, const char *name) {
RTN rtn = RTN_FindByName(img, name);
if (RTN_Valid(rtn)) {
RTN_Open(rtn);
RTN_InsertCall(rtn, IPOINT_BEFORE, (AFUNPTR)BeforeMemCmp,
IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
IARG_FUNCARG_ENTRYPOINT_VALUE, 1,
IARG_FUNCARG_ENTRYPOINT_VALUE, 2,
IARG_ADDRINT, (ADDRINT)name,
IARG_END);
RTN_Close(rtn);
outFile << "[*] Instrumented " << name << " in " << IMG_Name(img) << std::endl;
}
}
VOID HookWcsCmp(IMG img, const char *name) {
RTN rtn = RTN_FindByName(img, name);
if (RTN_Valid(rtn)) {
RTN_Open(rtn);
RTN_InsertCall(rtn, IPOINT_BEFORE, (AFUNPTR)BeforeWcsCmp,
IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
IARG_FUNCARG_ENTRYPOINT_VALUE, 1,
IARG_ADDRINT, (ADDRINT)name,
IARG_END);
RTN_Close(rtn);
outFile << "[*] Instrumented " << name << " in " << IMG_Name(img) << std::endl;
}
}
VOID Image(IMG img, VOID *v) {
outFile << "[IMG] Loaded: " << IMG_Name(img) << std::endl;
outFile.flush();
// Standard string compares
HookStrCmp(img, "strcmp");
HookStrCmp(img, "_stricmp");
HookStrCmp(img, "_strcmpi");
HookStrCmp(img, "lstrcmpA");
HookStrCmp(img, "lstrcmpiA");
// Bounded compares (3 args)
HookMemCmp(img, "strncmp");
HookMemCmp(img, "_strnicmp");
HookMemCmp(img, "memcmp");
// Wide-char compares
HookWcsCmp(img, "wcscmp");
HookWcsCmp(img, "_wcsicmp");
HookWcsCmp(img, "lstrcmpW");
HookWcsCmp(img, "lstrcmpiW");
HookMemCmp(img, "wcsncmp");
HookMemCmp(img, "_wcsnicmp");
}
VOID Fini(INT32 code, VOID *v) {
outFile.close();
}
INT32 Usage() {
std::cerr << "This tool traces string/memory compare calls" << std::endl;
return -1;
}
int main(int argc, char *argv[]) {
PIN_InitSymbols();
if (PIN_Init(argc, argv)) return Usage();
outFile.open("strcmp_trace.out");
IMG_AddInstrumentFunction(Image, 0);
PIN_AddFiniFunction(Fini, 0);
PIN_StartProgram();
return 0;
}
C:\Users\ctfman\Documents\Reflare>pintool\pin.exe -t pintool\source\tools\MyPinTool\obj-intel64\strcmp_tracer.dll -- target.exe sup3r_scr3t
Checking password...
Access denied!
C:\Users\ctfman\Documents\Reflare>type strcmp_trace.out
[IMG] Loaded: C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented strcmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented _stricmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented strncmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented _strnicmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented memcmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented wcscmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented _wcsicmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented wcsncmp in C:\WINDOWS\System32\ucrtbase.dll
[*] Instrumented _wcsnicmp in C:\WINDOWS\System32\ucrtbase.dll
[IMG] Loaded: C:\WINDOWS\SYSTEM32\VCRUNTIME140.dll
[*] Instrumented memcmp in C:\WINDOWS\SYSTEM32\VCRUNTIME140.dll
strcmp called:
Arg1: "sup3r_scr3t"
Arg2: "sup3r_s3cr3t"
[IMG] Loaded: C:\WINDOWS\SYSTEM32\kernel.appcore.dll
[IMG] Loaded: C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented strcmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented _stricmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented _strcmpi in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented strncmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented _strnicmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented memcmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented wcscmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented _wcsicmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented wcsncmp in C:\WINDOWS\System32\msvcrt.dll
[*] Instrumented _wcsnicmp in C:\WINDOWS\System32\msvcrt.dll
And just like that, the secret password is right there in the trace output. No manual disassembly required.
CTF Example: Tracing a Serial Key Validator
Let's create a more challenging CTF challenge that uses multiple validation steps:
// serial_checker.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int validate_format(char *serial) {
// Serial must be in format: XXXX-YYYY-ZZZZ
if (strlen(serial) != 14) return 0;
if (serial[4] != '-' || serial[9] != '-') return 0;
return 1;
}
int validate_checksum(char *serial) {
int sum = 0;
for (int i = 0; i < 14; i++) {
if (serial[i] != '-') {
sum += serial[i];
}
}
// Secret checksum
return (sum == 1242); // Valid serial: C0D3-~~~~-~~~~
}
int validate_magic(char *serial) {
// First 4 chars must be "C0D3"
return (strncmp(serial, "C0D3", 4) == 0);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <serial-key>\n", argv[0]);
return 1;
}
printf("Validating serial key...\n");
if (!validate_format(argv[1])) {
printf("Invalid format!\n");
return 1;
}
if (!validate_magic(argv[1])) {
printf("Invalid magic bytes!\n");
return 1;
}
if (!validate_checksum(argv[1])) {
printf("Invalid checksum!\n");
return 1;
}
printf("Valid serial! Flag: CTF{d1d_y0u_us3_p1n?}\n");
return 0;
}
Compile:
cl /Od /MD serial_checker.c
Now let's write a Pintool to trace function calls and see the validation flow:
// calltrace.cpp
#include "pin.H"
#include <iostream>
#include <fstream>
#include <string>
std::ofstream outFile;
UINT32 callDepth = 0;
VOID BeforeCall(ADDRINT target, ADDRINT sp) {
for (UINT32 i = 0; i < callDepth; i++) {
outFile << " ";
}
outFile << "CALL -> 0x" << std::hex << target << std::dec << std::endl;
callDepth++;
}
VOID AfterCall() {
if (callDepth > 0) callDepth--;
}
VOID Instruction(INS ins, VOID *v) {
if (INS_IsCall(ins)) {
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)BeforeCall,
IARG_BRANCH_TARGET_ADDR,
IARG_REG_VALUE, REG_STACK_PTR,
IARG_END);
if (INS_HasFallThrough(ins)) {
INS_InsertCall(ins, IPOINT_AFTER, (AFUNPTR)AfterCall, IARG_END);
}
}
}
VOID Fini(INT32 code, VOID *v) {
outFile.close();
}
int main(int argc, char *argv[]) {
if (PIN_Init(argc, argv)) return -1;
outFile.open("calltrace.out");
INS_AddInstrumentFunction(Instruction, 0);
PIN_AddFiniFunction(Fini, 0);
PIN_StartProgram();
return 0;
}
Compile:
copy calltrace.cpp source\tools\MyPinTool\
cd source\tools\MyPinTool
make TARGET=intel64
Run it:
pin.exe -t source\tools\MyPinTool\obj-intel64\calltrace.dll -- serial_checker.exe AAAA-BBBB-CCCC
strings calltrace.out | head -n 10
CALL -> 0x7ff95331a2a0
CALL -> 0x7ff9534658e0
CALL -> 0x7ff953307900
CALL -> 0x7ff953396380
CALL -> 0x7ff95330d880
CALL -> 0x7ff953395ea0
CALL -> 0x7ff95336eae8
CALL -> 0x7ff95333c190
CALL -> 0x7ff9533f7970
CALL -> 0x7ff9533f7970
This reveals the exact sequence of validation functions being called, helping you understand the program flow. Be warned: calltrace logs every single CALL instruction in the entire process, including deep within system DLLs. Even for a trivial program, expect the output file to be hundreds of megabytes. Ours was close to 700MB.
CTF Example: Memory Access Tracking for XOR Decryption
Many CTF challenges use XOR encoding. Here's an example that decodes a flag at runtime:
// xor_decoder.c
#include <stdio.h>
#include <string.h>
void decode_flag(char *input) {
char encoded[] = {0x32, 0x2b, 0x2c, 0x1d, 0x2b, 0x31, 0x1d, 0x23, 0x35,
0x27, 0x31, 0x2d, 0x2f, 0x27, 0x63, 0x63};
char decoded[20];
int key = 0x42;
// XOR decode
for (int i = 0; i < 16; i++) {
decoded[i] = encoded[i] ^ key;
}
decoded[16] = '\0';
// Check if input matches decoded flag
if (strcmp(input, decoded) == 0) {
printf("Correct! Flag: CTF{%s}\n", decoded);
} else {
printf("Wrong!\n");
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <flag>\n", argv[0]);
return 1;
}
decode_flag(argv[1]);
return 0;
}
Compile:
cl /Od /MD xor_decoder.c
Now let's track memory writes to see where the decoded flag appears. There are a couple of subtleties to get right here. First, a naive approach that instruments every write in the process will drown in noise. System DLLs alone can produce hundreds of thousands of writes before your code even runs. We use IMG_IsMainExecutable to grab the target binary's address range and filter out everything else. Second, if we read the written value at IPOINT_BEFORE (before the instruction executes), we'll see the old contents of memory, not the new value. We solve this by saving the write address before the instruction and reading the value after it completes:
// memtrace.cpp
#include "pin.H"
#include <iostream>
#include <fstream>
std::ofstream outFile;
UINT32 writeCount = 0;
ADDRINT mainLow = 0, mainHigh = 0;
static TLS_KEY tls_key;
struct WriteInfo {
VOID *ip;
VOID *addr;
UINT32 size;
};
VOID RecordMemWriteBefore(VOID *ip, VOID *addr, UINT32 size, THREADID tid) {
ADDRINT rip = (ADDRINT)ip;
if (rip < mainLow || rip > mainHigh) return;
WriteInfo *info = static_cast<WriteInfo*>(PIN_GetThreadData(tls_key, tid));
info->ip = ip;
info->addr = addr;
info->size = size;
}
VOID RecordMemWriteAfter(THREADID tid) {
WriteInfo *info = static_cast<WriteInfo*>(PIN_GetThreadData(tls_key, tid));
if (!info->addr) return;
writeCount++;
unsigned char buf[64];
UINT32 len = info->size > 64 ? 64 : info->size;
PIN_SafeCopy(buf, info->addr, len);
outFile << "WRITE at " << info->ip
<< " addr=" << info->addr
<< " size=" << std::dec << info->size << " ";
for (UINT32 i = 0; i < len; i++) {
outFile << (char)((buf[i] >= 0x20 && buf[i] <= 0x7E) ? buf[i] : '.');
}
outFile << std::endl;
info->addr = NULL;
}
VOID Instruction(INS ins, VOID *v) {
if (INS_IsMemoryWrite(ins) && INS_HasFallThrough(ins)) {
INS_InsertCall(
ins, IPOINT_BEFORE, (AFUNPTR)RecordMemWriteBefore,
IARG_INST_PTR,
IARG_MEMORYWRITE_EA,
IARG_MEMORYWRITE_SIZE,
IARG_THREAD_ID,
IARG_END);
INS_InsertCall(
ins, IPOINT_AFTER, (AFUNPTR)RecordMemWriteAfter,
IARG_THREAD_ID,
IARG_END);
}
}
VOID Image(IMG img, VOID *v) {
if (IMG_IsMainExecutable(img)) {
mainLow = IMG_LowAddress(img);
mainHigh = IMG_HighAddress(img);
}
}
VOID ThreadStart(THREADID tid, CONTEXT *ctxt, INT32 flags, VOID *v) {
WriteInfo *info = new WriteInfo();
info->addr = NULL;
PIN_SetThreadData(tls_key, info, tid);
}
VOID ThreadFini(THREADID tid, const CONTEXT *ctxt, INT32 code, VOID *v) {
WriteInfo *info = static_cast<WriteInfo*>(PIN_GetThreadData(tls_key, tid));
delete info;
}
VOID Fini(INT32 code, VOID *v) {
outFile << "\nTotal memory writes (main exe only): " << writeCount << std::endl;
outFile.close();
}
int main(int argc, char *argv[]) {
PIN_InitSymbols();
if (PIN_Init(argc, argv)) return -1;
tls_key = PIN_CreateThreadDataKey(NULL);
outFile.open("memtrace.out");
IMG_AddInstrumentFunction(Image, 0);
INS_AddInstrumentFunction(Instruction, 0);
PIN_AddFiniFunction(Fini, 0);
PIN_AddThreadStartFunction(ThreadStart, 0);
PIN_AddThreadFiniFunction(ThreadFini, 0);
PIN_StartProgram();
return 0;
}
Compile:
copy memtrace.cpp source\tools\MyPinTool\
cd source\tools\MyPinTool
make TARGET=intel64
Run it:
pin.exe -t source\tools\MyPinTool\obj-intel64\memtrace.dll -- xor_decoder.exe test
Here's the output (trimmed for brevity):
WRITE at 0x7ff792561018 addr=0xdfb66ff900 size=1 2
WRITE at 0x7ff79256101d addr=0xdfb66ff901 size=1 +
WRITE at 0x7ff792561022 addr=0xdfb66ff902 size=1 ,
WRITE at 0x7ff792561027 addr=0xdfb66ff903 size=1 .
WRITE at 0x7ff79256102c addr=0xdfb66ff904 size=1 +
WRITE at 0x7ff792561031 addr=0xdfb66ff905 size=1 1
WRITE at 0x7ff792561036 addr=0xdfb66ff906 size=1 .
WRITE at 0x7ff79256103b addr=0xdfb66ff907 size=1 #
WRITE at 0x7ff792561040 addr=0xdfb66ff908 size=1 5
WRITE at 0x7ff792561045 addr=0xdfb66ff909 size=1 '
WRITE at 0x7ff79256104a addr=0xdfb66ff90a size=1 1
WRITE at 0x7ff79256104f addr=0xdfb66ff90b size=1 -
WRITE at 0x7ff792561054 addr=0xdfb66ff90c size=1 /
WRITE at 0x7ff792561059 addr=0xdfb66ff90d size=1 '
WRITE at 0x7ff79256105e addr=0xdfb66ff90e size=1 c
WRITE at 0x7ff792561063 addr=0xdfb66ff90f size=1 c
WRITE at 0x7ff792561068 addr=0xdfb66ff8f4 size=4 B...
WRITE at 0x7ff792561070 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff910 size=1 p
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff911 size=1 i
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff912 size=1 n
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff913 size=1 _
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff914 size=1 i
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff915 size=1 s
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff916 size=1 _
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff917 size=1 a
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff918 size=1 w
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff919 size=1 e
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff91a size=1 s
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff91b size=1 o
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff91c size=1 m
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff91d size=1 e
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff91e size=1 !
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
WRITE at 0x7ff79256109e addr=0xdfb66ff91f size=1 !
WRITE at 0x7ff792561080 addr=0xdfb66ff8f0 size=4 ....
Total memory writes (main exe only): 130
The interesting part starts at instruction address 0x7ff79256109e. Notice how it repeats. A single byte written to consecutive stack addresses (0xdfb66ff910 through 0xdfb66ff91f). The same instruction address repeating confirms this is a loop body. Reading the ASCII column straight down gives us the decoded flag: pin_is_awesome!!. The interleaved 4-byte writes at 0xdfb66ff8f0 are the loop counter i incrementing each iteration.
By filtering to only the main executable's address range, we cut out all the noise from system DLLs and are left with just 130 writes, all from our target's own code.
Pin in CTF Competitions
The examples above demonstrate exactly why Pin is so valuable in CTF competitions:
- Password crackers: The strcmp tracer reveals passwords instantly
- Serial key validators: Call tracing shows the validation logic flow
- Encoded flags: Memory tracing catches XOR decoding and other transformations
- Anti-debugging: Pin often bypasses these since it's not a traditional debugger
- Obfuscated comparisons: Even if the comparison logic is complex, Pin sees the actual values at runtime
Consider other CTF scenarios where Pin excels:
Timing-based challenges: You can instrument timing-related functions to understand delays or race conditions.
Custom encryption: By tracking memory operations, you can watch data transform through encryption routines without understanding the algorithm.
Input validation: Trace where your input goes and what transformations it undergoes.
Practical Considerations
Writing effective Pintools requires balancing comprehensive instrumentation against performance. Instrumenting every instruction creates massive overhead. Instead, instrument selectively - focus on specific functions, memory regions, or instruction types relevant to your analysis.
The compilation process requires the Pin toolkit installed on your system. Each Pintool compiles against Pin's headers and links with its libraries.
One limitation to understand: Pin introduces timing differences. Instrumented code runs slower than native execution, which can affect malware using timing-based anti-analysis or causing race conditions to behave differently. However, for most CTF scenarios, the insights gained far outweigh these limitations.
The Competitive Landscape
Pin isn't alone in the DBI space. DynamoRIO offers similar capabilities with different performance characteristics, while Frida provides a more scripting-friendly approach using JavaScript. Pin's advantages include mature documentation, excellent instruction-level granularity, and strong Intel support. Its C++ API provides maximum control and performance for complex instrumentation tasks.
Conclusion
The examples we've explored, from simple instruction counting to targeted strcmp tracing to memory access tracking, demonstrate how Pin enables rapid analysis of binaries that would otherwise require tedious manual disassembly. Start with simple Pintools like the ones shown here, then build more sophisticated instrumentation as you encounter harder challenges. The code examples provided here serve as a foundation. Modify them, combine them, and build your own custom analysis tools tailored to your specific needs.
Share this
- January 2026 (1)
- December 2025 (1)
- November 2025 (1)
- October 2025 (1)
- September 2025 (1)
- August 2025 (1)
- July 2025 (1)
- June 2025 (1)
- May 2025 (1)
- April 2025 (1)
- March 2025 (1)
- February 2025 (1)
- January 2025 (1)
- December 2024 (1)
- November 2024 (1)
- October 2024 (1)
- September 2024 (1)
- August 2024 (1)
- July 2024 (1)
- June 2024 (1)
- April 2024 (2)
- February 2024 (1)
- January 2024 (1)
- December 2023 (1)
- November 2023 (1)
- October 2023 (1)
- September 2023 (1)
- August 2023 (1)
- July 2023 (1)
- June 2023 (2)
- May 2023 (2)
- April 2023 (3)
- March 2023 (4)
- February 2023 (3)
- January 2023 (5)
- December 2022 (1)
- November 2022 (2)
- October 2022 (1)
- September 2022 (11)
- August 2022 (5)
- July 2022 (1)
- May 2022 (3)
- April 2022 (1)
- February 2022 (4)
- January 2022 (3)
- December 2021 (2)
- November 2021 (3)
- October 2021 (2)
- September 2021 (1)
- August 2021 (1)
- June 2021 (1)
- May 2021 (14)
- February 2021 (1)
- October 2020 (1)
- September 2020 (1)
- July 2020 (1)
- June 2020 (1)
- May 2020 (1)
- April 2020 (2)
- March 2020 (1)
- February 2020 (1)
- January 2020 (3)
- December 2019 (1)
- November 2019 (2)
- October 2019 (3)
- September 2019 (5)
- August 2019 (2)
- July 2019 (3)
- June 2019 (3)
- May 2019 (2)
- April 2019 (3)
- March 2019 (2)
- February 2019 (3)
- January 2019 (1)
- December 2018 (3)
- November 2018 (5)
- October 2018 (4)
- September 2018 (3)
- August 2018 (3)
- July 2018 (4)
- June 2018 (4)
- May 2018 (2)
- April 2018 (4)
- March 2018 (5)
- February 2018 (3)
- January 2018 (3)
- December 2017 (2)
- November 2017 (4)
- October 2017 (3)
- September 2017 (5)
- August 2017 (3)
- July 2017 (3)
- June 2017 (4)
- May 2017 (4)
- April 2017 (2)
- March 2017 (4)
- February 2017 (2)
- January 2017 (1)
- December 2016 (1)
- November 2016 (4)
- October 2016 (2)
- September 2016 (4)
- August 2016 (5)
- July 2016 (3)
- June 2016 (5)
- May 2016 (3)
- April 2016 (4)
- March 2016 (5)
- February 2016 (4)


