바이너리 계측의 두가지 방식
1.
정적 바이너리 계측(SBI, Static Binary Instrumentation)
2.
동적 바이너리 계측(DBI, Dynamic Binary Instrumentation)
해당 장에서 인텔에서 제작한 유명한 DBI 체계인 Pin을 사용해 직접 바이너리 계측 도구를 구현할 예정
9.1 바이너리 계측이란 무엇인가?
주어진 바이너리 내부의 임의의 지점에 새로운 코드를 삽입함으로써 해당 바이너리의 행위를 관찰하거나 수정할 수 있는 기법을 바이너리 계측(Binary Instrumentation)이라고 한다.
새로운 코드를 삽입하는 지점을 계측 지점(instrumentation point)라고 하고 해당 추가 코드를 계측 코드(instrumentation code) 라고 한다
행위 관찰(observe)
•
함수 호출 카운팅을 하고 싶으면 모든 call 명령어를 계측한다.
•
호출되는 해당 함수의 정보를 기록하면 해당 바이너리 실행 시 계측 결과로 호출 된 함수를 확인할 수 있다.
행위 조작(modify)
•
바이너리 내의 모든 간접 호출 명령(call rax, ret 등)을 계측해 제어권 이양이 발생할 때 보증된 곳을 목적지로 하는지를 검사하는 작업 - CFI (control flow integrity)
•
Microsoft에서는 Control Flow Guard 라고 부르는 CFI를 컴파일 단계에서 적용하고 있음.
9.1.1 바이너리 계측 API
Microsoft Pin 플랫폼을 이용하여 해당 장을 수행 할 것이며, 해당 플랫폼은 API를 제공하며 callback식으로 수행된다.
바이너리 계측은 위에서도 설명 하였지만 정적 바이너리 계측과 동적 바이너리 계측 두가지가 존재한다.
9.1.2 정적 및 동적 바이너리 계측 비교
1.
정적 바이너리 계측
a.
바이너리 내용을 수정
b.
9.2에서 확인
2.
동적 바이너리 계측
a.
실행 시점에 CPU의 명령어 처리 스트림에 새로운 명령어를 일시적으로 끼워 넣는 방식을 사용
b.
기존 바이너리의 구조를 건드리지 않음
c.
하지만 실시간 작업의 특성 상 컴퓨터의 계산량이 월등히 높아짐
9.2 정적 바이너리 계측
정적 바이너리 계측은
1.
주어진 바이너리를 디스어셈블하고
2.
필요한 위치에 계측을 위한 코드를 삽입한 후
3.
디스크에 영구적으로 저장한 후 체크한다
주로 알려진 SBI 플랫폼으로는
1.
PEBIL
2.
Dyninst
가 있다. 단 연구 목적으로 쓰이기에 자료 정리가 되어있진 않다.
SBI를 구현할 때 가장 중요한 것은 바이너리에 계측 코드를 삽입할 때 기존의 코드 및 데이터의 상호 참조를 깨트리지 말아야 한다.
이를 위한 방법으로
1.
int 3 방법
2.
trampoline 방법
이 존재한다
9.2.1 int 3 방법
int 3 instruction은 x86아키텍쳐에서 디버거가 소프트웨어 방식의 중단점(breakpoint)를 구현할 때 사용된다.
단순한 SBI 구현
재배치된 코드에 대한 모든 참조를 전부 알맞게 수정하는 것은 실질적으로 불가능하다.
기존 코드 섹션에 새로운 코드 일부를 추가할 공간은 존재하지 않을 것이므로 (SBI기준)
SBI를 수행하려면 다른 섹션이나 공유 라이브러리 등 별도의 위치에 계측 코드를 삽입해야만 한다.
그 후 계측 지점에 도달하면 계측 코드가 수행될 수 있게 제어권을 넘겨줘야 한다.
1.
5바이트를 사용하는 mov edx,0x1을 5바이트 패치를 수행하여 제어권을 이전하는 jmp로 수정한다.
2.
해당 jmp 구문을 수행하여 계측 코드를 수행하며 (모든 레지스터 및 eflags 백업)
3.
이후 기존의 코드를 수행
4.
백업 한 레지스터와 eflags를 복원하고
5.
제어권을 다시 전달해야 할 rip로 점프를 수행한다.
해당 방식의 문제점은 최소 5바이트를 무조건 사용해야 하는 점이다. (e9 00 00 00 00 / e9의 경우 32bit jump opcode)
이러한 문제점에 대한 해결책으로 나온것이 int 3 명령어이다.
int3 방법으로 멀티 바이트 점프 문제 해결하기
1.
계측을 수행할 첫 바이트를 int 3 (0xcc)로 수정한다
2.
인터럽트가 발생하면 해당 주소값에 맞는 계측 코드를 호출한다.
단점
1.
소프트웨어 인터럽트 방식이라 느리다
2.
해당 소프트웨어가 이미 디버깅 중 일경우 호환되지 않는다.
9.2.2 트램펄린 방법
jmp 를 이용하여 원본 코드가 존재하는 곳의 코드를 모두 복사 한 후, 다른 섹션에 저장하고 계측 함수를 기존 코드에 추가하는 방법
기존 코드가 존재하는 곳은 jmp 이후 NOP 처리하여 실행 될 가능성이 존재하지 않음.
또한 SBI엔진은
1.
모든 상대 Jmp를 패치한다. 또한 2바이트 상대 jmp를 32비트 오프셋이 있는 5바이트 점프 오프셋으로 패치한다.
2.
함수 내부에서 호출하는 다른 함수 또한 복사본을 만들어 수행한다.
간접 제어 흐름 처리하기
1.
간접 함수 호출
a.
간접 함수 호출의 경우 호출하는 레지스터 값을 조작하지 않기 때문에 그대로 유지된다.
2.
간접 점프
a.
간접 점프의 경우 특정한 상황에 부합하는 간접 점프를 점프 테이블로 만들어 해당 테이블을 참조하여 점프한다.
b.
그럴 경우 점프하였을 때 기존 함수의 중간부분에서 실행 될 수도 있기 때문에 해당 상황의 경우 원본 코드에 트램펄린을 다시 삽입해서 작업해줘야 한다.
트램펄린 기법의 신뢰성
1.
위의 간접 제어 흐름 처리방법의 문제로 상용으로 사용하기에는 부담이 있다.
9.3 동적 바이너리 계측
동적 바이너리 계측(DBI, Dynamic Binary Instrumentation) 엔진은 바이너리를 실행 한 후 CPU에서 각 명령어들의 스트림이 진행되는 과정을 모니터링한다.
시스템 구조 예시.
향후 추가 정리 할 것.
9.3.2 Pin 개요
9.4 Pin을 사용한 프로파일링
9.4.1 프로파일링 도구의 자료 구조 및 초기 설정을 위한 코드
#include <stdio.h>
#include <map>
#include <string>
#include <asm-generic/unistd.h>
#include "pin.H" //1. 프로파일링을 위한 pintool을 구현하려면 반드시 사용해야 함. 대문자 H는 CPP 해더를 의미함. 처음알았네;
KNOB<bool> ProfileCalls(KNOB_MODE_WRITEONCE, "pintool", "c", "0", "Profile function calls"); // 2
KNOB<bool> ProfileSyscalls(KNOB_MODE_WRITEONCE, "pintool", "s", "0", "Profile syscalls");
std::map<ADDRINT, std::map<ADDRINT, unsigned long> > cflows; //3 프로파일링에 사용되는 map 구조체들
std::map<ADDRINT, std::map<ADDRINT, unsigned long> > calls;
std::map<ADDRINT, unsigned long> syscalls;
std::map<ADDRINT, std::string> funcnames;
unsigned long insn_count = 0;
unsigned long cflow_count = 0;
unsigned long call_count = 0;
unsigned long syscall_count = 0;
int
main(int argc, char *argv[])
{
PIN_InitSymbols(); // symbol이 존재한다면 심볼 정보를 기반으로 함수가 얼마나 호출 되었는지 통계를 제공한다.
if(PIN_Init(argc,argv)) { // Init 함수. Pin과 관련된 모든것들을 초기화한다.
print_usage();
return 1;
}
IMG_AddInstrumentFunction(parse_funcsyms, NULL); //계측시 이미지 단위로 처리
INS_AddInstrumentFunction(instrument_insn, NULL); // 인스트럭션 단위로 처리
TRACE_AddInstrumentFunction(instrument_trace, NULL); // trace 단위로 처리
if(ProfileSyscalls.Value()) {
PIN_AddSyscallEntryFunction(log_syscall, NULL); // 시스콜 로깅 함수. ProfileSyscalls가 참일 경우만 실행
}
PIN_AddFiniFunction(print_results, NULL); // Pin 종료함수
/* Never returns */
PIN_StartProgram(); // 분석 대상 어플리케이션 수행.
return 0;
}
C++
복사
9.4.2 함수 심볼 분석하기
이미지 내에 존재하는 모든 심볼을 확인하여 최종 결과물에서 심볼이 있는 경우 함수 주소를 심볼로 치환하여 출력해준다.
static void
parse_funcsyms(IMG img, void *v)
{
if(!IMG_Valid(img)) return; //1. 이미지 유효 검사
for(SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec)) { //2 섹션 헤더가 정상일 경우 다음 섹션으로 넘겨준다.
for(RTN rtn = SEC_RtnHead(sec); RTN_Valid(rtn); rtn = RTN_Next(rtn)) { //3 섹션 내에서 루틴을 통해 함수 이름을 순회한다.
funcnames[RTN_Address(rtn)] = RTN_Name(rtn); //4. 함수 이름을 펑션네임 배열에 저장한다.
}
}
}
C++
복사
9.4.3 기본 블록 계측하기
이 파트에서는 대상 프로그램이 수행한 명령어의 갯수를 프로파일링 한다
실행되는 기본 블록의 명령어 갯수만큼 명령 카운터(ins_count)를 증가시킨다.
Pin의 기본 블록 관련 참고 // 블록의 개념이 정확이 무엇인지? 함수 실행하는 블록을 의미하는듯
Pin의 경우 큰 블록을 찾고, 블록 내에서 점프가 이루어 지는 경우 블록이 2개인 것으로 간주
instruction 하나하나 체크할 수 있지만 성능의 저하가 매우 심하기 때문에 추천하진 않음.
기본 블록 계측 구현하기