Pintools 기능 분석 (1)
0. 개요
Side Channel Attack을 소프트웨어에 적용하기 위해서는 pintools이나 Valgrind와 같은 Dynamic Binary Instruction와 같은 tool을 사용해야한다. DCA와 같은 공격을 하기 위해서는 해당 바이너리의 특정 주소의 메모리를 trace할 수 있는 기능을 갖추는 코드를 제작 해야한다. 2016년 DCA가 등장하면서, SideChannelMarvel이라는 github에 White box 암호의 DCA공격을 할 수 있게 다양한 기능을 만들어놨다. 그러나 해당 github은 너무 오래되서 현재 2023년에는 라이브러리가 맞지 않은 경우가 많다. 따라서 앞으로의 연구를 위해서 Pintools를 이용해서 memory 하나하나의 값을 추적할 수 있는 프로그램을 제작해야한다. 따라서 이에 앞서, pintools에 기능을 하나하나 분석하는 기능을 만들어 보려고 한다. 참고로 해당 분석은 철저하게 부채널 분석적인 면에서 분석 됐고, 내가 필요한 기능을 확인하려고 하기 때문에, 많은 사람에게 도움이 안될 가능성도 있다.
https://github.com/SideChannelMarvels/Tracer
1. 시작
pintools를 다운 받으면 다음과 같은 경로에 간단한 tools가 있다.

각각의 cpp 파일은 다양한 기능들을 제공하고 해당기능을 이용해서 패치를 해서 기능을 활용할 수 있다.
2. 세부 기능
2-1. Instruction Address Trace (Instruction Instrumentation)
해당 기능을 분석하기 위해서는 IARG_TYPE를 먼저 알고, 분석을 시도해야한다. 하지만 걱정말어라.. 같이 분석을 해보도록 하자.
해당 기능은 아래의 그림과 같은 기능 제공한다. 아래의 기름을 확인하면, 프로그램이 실행될 때, 명령어 실행의 정확한 주소를 확인할 수 있다.

해당 기능은 부채널 분석을 하기 위해서 반드시 필요한 기능이기 때문에 확인해보도록 하자.
/*
* Copyright (C) 2004-2021 Intel Corporation.
* SPDX-License-Identifier: MIT
*/
#include <stdio.h>
#include "pin.H"
FILE* trace;
// This function is called before every instruction is executed
// and prints the IP
VOID printip(VOID* ip) { fprintf(trace, "%p\n", ip); }
// Pin calls this function every time a new instruction is encountered
VOID Instruction(INS ins, VOID* v)
{
// Insert a call to printip before every instruction, and pass it the IP
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END);
}
// This function is called when the application exits
VOID Fini(INT32 code, VOID* v)
{
fprintf(trace, "#eof\n");
fclose(trace);
}
/* ===================================================================== */
/* Print Help Message */
/* ===================================================================== */
INT32 Usage()
{
PIN_ERROR("This Pintool prints the IPs of every instruction executed\n" + KNOB_BASE::StringKnobSummary() + "\n");
return -1;
}
/* ===================================================================== */
/* Main */
/* ===================================================================== */
int main(int argc, char* argv[])
{
trace = fopen("itrace.out", "w");
// Initialize pin
if (PIN_Init(argc, argv)) return Usage();
// Register Instruction to be called to instrument instructions
INS_AddInstrumentFunction(Instruction, 0);
// Register Fini to be called when the application exits
PIN_AddFiniFunction(Fini, 0);
// Start the program, never returns
PIN_StartProgram();
return 0;
}
실로 끔찍헌 코드임에는 틀림없다. 걍 함수로 도배되어 있으면 어캐 분석을 하라는 건지, 초보 개발자는 다 뒤지라는 말인거 같다. 하나한 분석을 시도 해보자. (물론 영어 번역이 거의 맞다..)
일단 해당 프로그램은 itrace.out이라는 출력물을 뱉고, itrace.out은 instrucment의 주소를 하나하나 출력하는 프로그램이다. 일단 pintools을 실행하려고면 이런식으로 실행을 해줘야한다.
if (PIN_Init(argc, argv)) return Usage();
/*
필요 코드 추가
*/
PIN_StartProgram();
따라서 앞으로 해당 코드에 대한 설명을 생략하도록 하겠다.
// Register Instruction to be called to instrument instructions
INS_AddInstrumentFunction(Instruction, 0);
// Register Fini to be called when the application exits
PIN_AddFiniFunction(Fini, 0);
다음 해당기능의 핵심인 다음 두코드
해당 기능을 확인하면 다음과 같다
VOID printip(VOID* ip) { fprintf(trace, "%p\n", ip); }
// Pin calls this function every time a new instruction is encountered
VOID Instruction(INS ins, VOID* v)
{
// Insert a call to printip before every instruction, and pass it the IP
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END);
}
간단하게 설명을 하면, 명령어가 실행될 때마다, printip라는 기능을 통해서 파일 포인터인 trace에 명령어의 주소를 적어주는 간단한 프로그램이다. 여기서 인수는 시작할 때랑 안할 때를 구분하는 인자를 추가로 INS_InsertCall다음에 넣어주는 것을 확인할 수 있다.
2-2.Memory Reference Trace (Instruction Instrumentation)
해당 기능도 DCA의 특성상, 해당 byte의 비트를 정확하게 알아야 두개의 그룹으로 분리하고 해당 그룹을 통해서 차분을 구하기 때문에 memory trace를 만드는 것이 중요하다. 해당 기능은 명령어가 접근한 memory 주소를 뽑는데 중요한 역할을 하며, 해당 주소 접근을 확인하여 WB-AES table이 언제 사용하여 해당 주소를 분석하는 것도 중요하다.
코드를 살펴보자
/*
* Copyright (C) 2004-2021 Intel Corporation.
* SPDX-License-Identifier: MIT
*/
/*
* This file contains an ISA-portable PIN tool for tracing memory accesses.
*/
#include <stdio.h>
#include "pin.H"
FILE* trace;
// Print a memory read record
VOID RecordMemRead(VOID* ip, VOID* addr) { fprintf(trace, "%p: R %p\n", ip, addr); }
// Print a memory write record
VOID RecordMemWrite(VOID* ip, VOID* addr) { fprintf(trace, "%p: W %p\n", ip, addr); }
// Is called for every instruction and instruments reads and writes
VOID Instruction(INS ins, VOID* v)
{
// Instruments memory accesses using a predicated call, i.e.
// the instrumentation is called iff the instruction will actually be executed.
//
// On the IA-32 and Intel(R) 64 architectures conditional moves and REP
// prefixed instructions appear as predicated instructions in Pin.
UINT32 memOperands = INS_MemoryOperandCount(ins);
// Iterate over each memory operand of the instruction.
for (UINT32 memOp = 0; memOp < memOperands; memOp++)
{
if (INS_MemoryOperandIsRead(ins, memOp))
{
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemRead, IARG_INST_PTR, IARG_MEMORYOP_EA, memOp,
IARG_END);
}
// Note that in some architectures a single memory operand can be
// both read and written (for instance incl (%eax) on IA-32)
// In that case we instrument it once for read and once for write.
if (INS_MemoryOperandIsWritten(ins, memOp))
{
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemWrite, IARG_INST_PTR, IARG_MEMORYOP_EA, memOp,
IARG_END);
}
}
}
VOID Fini(INT32 code, VOID* v)
{
fprintf(trace, "#eof\n");
fclose(trace);
}
/* ===================================================================== */
/* Print Help Message */
/* ===================================================================== */
INT32 Usage()
{
PIN_ERROR("This Pintool prints a trace of memory addresses\n" + KNOB_BASE::StringKnobSummary() + "\n");
return -1;
}
/* ===================================================================== */
/* Main */
/* ===================================================================== */
int main(int argc, char* argv[])
{
if (PIN_Init(argc, argv)) return Usage();
trace = fopen("pinatrace.out", "w");
INS_AddInstrumentFunction(Instruction, 0);
PIN_AddFiniFunction(Fini, 0);
// Never returns
PIN_StartProgram();
return 0;
}
코드는 다음과 같이 되어 있으며, 위에서 설명한 것처럼
- INS_AddInstrumentFunction(Instruction, 0);
- PIN_AddFiniFunction(Fini, 0);
해당 두 함수만, 역할을 잘 분석하면 된다.
VOID RecordMemRead(VOID* ip, VOID* addr) { fprintf(trace, "%p: R %p\n", ip, addr); }
// Print a memory write record
VOID RecordMemWrite(VOID* ip, VOID* addr) { fprintf(trace, "%p: W %p\n", ip, addr); }
// Is called for every instruction and instruments reads and writes
VOID Instruction(INS ins, VOID* v)
{
// Instruments memory accesses using a predicated call, i.e.
// the instrumentation is called iff the instruction will actually be executed.
//
// On the IA-32 and Intel(R) 64 architectures conditional moves and REP
// prefixed instructions appear as predicated instructions in Pin.
UINT32 memOperands = INS_MemoryOperandCount(ins);
// Iterate over each memory operand of the instruction.
for (UINT32 memOp = 0; memOp < memOperands; memOp++)
{
if (INS_MemoryOperandIsRead(ins, memOp))
{
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemRead, IARG_INST_PTR, IARG_MEMORYOP_EA, memOp,
IARG_END);
}
// Note that in some architectures a single memory operand can be
// both read and written (for instance incl (%eax) on IA-32)
// In that case we instrument it once for read and once for write.
if (INS_MemoryOperandIsWritten(ins, memOp))
{
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemWrite, IARG_INST_PTR, IARG_MEMORYOP_EA, memOp,
IARG_END);
}
}
}
위의 함수의 기능의 핵심은 다음의 함수들이다. 일단 간단하게 Read : R , Write : W로 분석할 수 있으며,
- RecordMemRead
- RecordMemWrite
해당 두함수는 단순히 명령어 point와 주소를 적는 함수이다.
Instruction 함수의 경우, INS_MemoryOperandCount로 operand의 개수를 센 다음에 INS_MemoryOperandIsRead 나 INS_MemoryOperandIsWritten를 확인해서 적는 역할을 한다. 그러면 명령어가 접근하는 주소를 알수 있고, 명령어가 쓰는 역할을 하는지 구분할 수있다. 이때 해당 명령어가 동작이 되는지 확인하는 것을 INS_InsertPredicatedCall() 다음의 함수를 통해 알 수 있다. 동작을 하면 메모리에 구동.