[드림핵] Reverse Engineering

큐가·2025년 3월 16일
1

드림핵

목록 보기
3/5

1. Introduction

엔지니어링 vs 리버스 엔지니어링

  • 엔지니어링: 완성품과 이를 구성하는 부품들의 기능과 설계를 고안하고, 제작하는 과정.
  • 리버스 엔지니어링(리버싱): 완성품을 해체하고 분석하여 구조, 기능, 디자인을 파악. 엔지니어링 과정을 거꾸로라고 생각하면 됨.

2. Binary & analsis

프로그램

  • 컴파일러: 컴파일(기계어로 번역)을 해주는 소프트웨어
  • 인터프리터: 인터프리팅(사용자가 작성한 스크립트를 그때그때 번역하여 CPU에 저장)을 해주는 소프트웨어

전처리(Preprocessing)

  • 컴파일러가 소스코드를 어셈블리어로 컴파일하기 전에, 필요한 형식으로 가공하는 과정.
  • 과정: 1. 주석 제거 -> 2. 매크로(#define으로 정의함) 치환 -> 3. 파일 병합(GCC에서는 -E 옵션을 사용하여 전처리 결과를 확인 함)

컴파일

  • C로 작성된 소스 코드를 어셈블리어로 번역
  • 컴파일 과정에서 컴파일러는 소스코드 문법 검사를 하고 오류가 있다면 컴파일을 멈춘 후 에러를 출력.
  • gcc에서 최적화를 적용하는 옵션: -O -O0 -O1 -O2 -O3 -Os -Ofast -Og
  • 옵션 -S: 소스코드를 어셈블리 코드로 컴파일.

어셈블

  • 컴파일로 생성된 어셈블리어코드를 ELF형식의 목적 파일로 변환하는 과정.
  • 목적파일로 변환되고 나면 기계어로 번역되므로 더이상 사람이 해석하기 어려워진다.

링크

  • 여러 목적 파일들을 연결하여 실행 가능한 바이너리로 만드는 과정.

디스어셈블

  • 기계어를 어셈블리어로 재번역하는 과정. (어셈블의 역과정)

디컴파일러

  • 어셈블리어보다 고급 언어로 바이너리를 번역하는 컴파일러. 그러나 오차가 있어서 바이너리의 소스코드와 동일한 코드를 생성하지는 못한다.
  • 그럼에도 디스어셈블러보다 디컴파일러가 압도적으로 분석 효율이 높다. 그 오차가 바이너리의 동작을 왜곡하지는 않는다.

정리

  • 전처리: 소스 코드가 컴파일에 필요한 형식으로 가공되는 과정.
  • 컴파일: 소스 코드를 어셈블리어로 번역하는 과정.
  • 어셈블: 어셈블리 코드를 기계어로 번역하고, 실행 가능한 형식으로 변형하는 과정.
  • 링크: 여러 개의 목적 파일을 하나로 묶고, 필요한 라이브러리와 연결해주는 과정.
  • 디스어셈블: 바이너리를 어셈블리어로 번역하는 과정
  • 디컴파일: 바이너리를 고급 언어로 번역하는 과정.

정적 분석

  • 프로그램을 실행시키지 않고 분석하는 방법, 관찰을 통해 분석.
  • 장점
    1. 전체 구조를 파악하기 쉽다
    2. 프로그램을 실행하지 않아도 되므로 분석 환경의 제약에서도 비교적 자유롭다.
    3. 바이러스 등을 실행할 필요가 없으므로 악성 프로그램의 위협으로부터 안전하다.
  • 단점
    1. 프로그램의 코드를 변형하는 난독화가 적용되면 분석이 매우 어려워진다.
    2. 프로그램의 실행 흐름이 복잡할수록 정적 분석만으로는 다양한 동적 요소를 고려하기 어렵다.
  • 정적 분석 도구의 예: IDA(어셈블리 코드, 디컴파일 된 코드, 상호 참조 기능(문자열이나 함수를 어디에서 사용하는지 보여줌), 제어 흐름 그래프(함수의 실행 흐름을 보기 쉽게 해줌) 등의 기능이 있음.

동적 분석

  • 프로그램을 실행시키면서 분석하는 방법.
  • 장점

    코드를 자세히 분석해보지 않고도 프로그램의 개략적인 동작을 파악할 수 있다. 동적 분석을 통해 출력값들을 기반으로 동작을 추론하곤 한다. (대개의 프로그램은 많은 함수로 구성되어 있어 정적 분석만으로는 파악하는데 한계가 있다.)

  • 단점
    1. 동적 분석은 프로그램을 실행하면서 분석하는 것이므로 분석 환경을 구축하기 어려울 수 있다.
    2. 디버깅(동적 분석의 일종)을 방해하는 안티 디버깅에 의해 분석이 어려워질 수 있다.
  • 동적 분석 도구의 예: 디버거(프로그램의 버그를 찾고 제거하기 위해 사용되는 도구) 중 하나인 x64dbg(윈도우의 대표적인 동적 분석 도구, CPU의 레지스터 상태, 메모리와 스택의 값을 확인하며 분석을 진행함.)

3. Computer Science

서론

  • 컴퓨터 구조: 컴퓨터에 대한 기본 설계
  • 명령어 집합구조(ISA): CPU가 사용하는 명령어와 관련된 설계, 가장 널리 사용되는 ISA는 x86-64 아키텍처

컴퓨터 구조

  • 컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고, 이들을 구성하는 방법.
  • 세부 분야
    1. 기능 구조에 대한 설계: 컴퓨터가 연산을 효율적으로 하기 위해 어떤 기능들이 컴퓨터에 필요한지 고민하고, 설계하는 분야
      • 폰노이만 구조, 하버드 구조, 수정된 하버드 구조 등이 있다.
    2. 명령어 집합구조: CPU가 처리해야하는 명령어를 설계하는 분야
      • x86, x86-64, ARM, MIPS, AVR 등이 있다.
    3. 마이크로 아키텍처: CPU의 하드웨어적 설계를 하는 분야.
      • 캐시 설계, 파이프라이닝, 슈퍼 스칼라, 분기 예측, 비순차적 명령어 처리
    4. 하드웨어 및 컴퓨팅 방법론
      • 직접 메모리 접근 등이 있다.
  • 폰 노이만 구조, x86-64 아키텍처에 대해 알아봄

폰 노이만 구조

  • 중앙처리장치(CPU): 산술논리장치(ALU), 제어장치(Control Unit), 레지스터(cpu에 필요한 데이터를 저장) 등으로 구성됨
  • 기억장치: 주기억장치(RAM), 보조기억 장치(하드 드라이브(HDD), SSD)
  • 버스(컴퓨터 부품과 부품 사이 또는 컴퓨터와 컴퓨터 사이에 신호를 전송하는 통로): 데이터 버스(데이터가 이동하는 통로), 주소 버스(주소를 지정), 제어 버스(읽기/쓰기를 제어), 소프트웨어, 프로토콜 등이 있음.

명령어 집합 구조(ISA)

  • CPU가 해석하는 명령어의 집합.
  • 모든 컴퓨터가 동일한 수준의 연산 능력을 요구하지 않기 때문에 다양한 ISA가 개발되고 사용됨.
  • x86-64를 위주로 공부.(x86 기반 CPU의 점유율이 압도적으로 높음)

x86-64(amd64) 아키텍처

  • n비트 아키텍쳐: n은 CPU가 한번에 처리할 수 있는 데이터의 크기 단위인 WORD를 의미함
  • 전문 소프트웨어나 고사양 게임 등을 실행할 땐 64비트 아키텍쳐를 추천함.

레지스터

  • CPU 내부의 저장장치
  • 범용 레지스터: 주용도는 있으나, 그 외 임의의 용도로도 사용될 수 있는 레지스터. 8바이트를 저장할 수 있음.
    x64에서는 rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, r8-r15가 있다.
  • 세그먼트 레지스터: 과거에는 메모리 공간의 확장을 위해 사용했으나, 현재는 주로 메모리 보호를 위해 사용되는 레지스터이다. 6가지 세그먼트 레지스터가 존재하며 각각은 16비트이다.
    cs, ds, ss 레지스터는 코드 영역과 데이터, 스택 메모리 영역을 가리킬 때 사용되고, es, fs, gs는 운영체제 별로 용도를 결정할 수 있도록 범용적인 용도로 제작되었다.
  • 명령어 포인터 레지스터: 포인터로서의 역할을 함.x64에서는 rip가 있다.
  • 플래그 레지스터: CPU(프로세서)의 현재 상태를 저장하고 있는 레지스터.

레지스터 호환

  • x86-64 아키텍처는 IA-32(32비트)의 64비트 확장 아키텍처이고 호한이 가능하다.
  • eax, ebx, ecx, edx, esi, edi, esp, ebp는 rax, rbx, rcx, rdx, rsi, rsp, rbp의 하위 32비트를 가리킨다.
  • 또한, 과거 16비트 아키텍처인 IA-16과도 호환한다.
  • ax, bx, cx, dx, si, di, sp, bp는 eax, ebx, ecx, edx, esi, edi, esp, ebp의 하위 16비트를 가리킨다.

4. 프로세스 메모리 구조

섹션

  • 윈도우의 PE파일은 PE 헤더와 1개 이상의 섹션으로 구성되어 있다.
  • 섹션: 유사한 용도로 사용되는 데이터가 모여있는 영역.
  • 섹션에 대한 정보 중 중요한 것은 섹션의 이름과 크기, 섹션이 로드될 주소의 오프셋, 섹션의 속성과 권한이다.
  • PE에는 일반적으로 ".text", ".data", ".rdata"섹션이 일반적으로 사용된다.

.text 섹션

  • 실행 가능한 기계 코드가 위치하는 영역.
  • 읽기 권한과 실행 권한이 부여된다.
  • 쓰기 권한이 있으면 공격 받을 가능성이 있으므로 쓰기 권한은 제한된다.
  • main() 등의 함수 코드에서 사용된다.

.data 섹션

  • 컴파일 시점에 값이 정해진 전역 변수들이 위치함.
  • CPU가 읽고 쓸 수 있어야 하므로 읽기와 쓰기 권한이 부여된다.
  • 초기화된 전역변수, 전역 상수에서 사용된다.

.rdata

  • 컴파일 시점에 값이 정해진 전역 상수와 참조할 DLL(여러 프로그램에서 동시에 사용할 수 있는 코드와 데이터를 포함하는 동적 라이브러리) 및 외부 함수들의 정보가 저장된다.
  • CPU가 읽을 수 있어야 하므로 읽기 권한이 부여되지만 쓰기는 불가능하다.

섹션이 아닌 메모리

  • 스택: 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역.
    윈도우즈 프로세스의 각 쓰레드는 지역 변수나 함수의 인자(함수의 리턴 주소가 저장)되는 자신만의 스택 공간을 가지고 있다.
    • 자유롭게 읽고 쓸 수 있어야 하므로 읽기, 쓰기 권한이 부여된다.
    • 스택이 확장될 때, 기존 주소보다 낮은 주소로 확장돼서 '아래로 자란다'는 표현을 사용한다.
  • 힙: 프로그램이 여러 용도로 사용하기 위해 할당바든 공간으로 모든 종류의 데이터가 저장될 수 있다.
    • 스택과는 달리 큰 데이터도 저장할 수 있고, 전역적으로 접근이 가능하도록 설계되었다. 또, 실행중 동적으로 할당받는다는 차이가 있다.
    • 데이터를 읽고 쓰기만 하기 때문에 읽기, 쓰기 권한만을 가지지만 상황에 따라 실행 권한을 가지는 경우도 존재한다.
    • malloc(), calloc() 등으로 할당 받은 메모리에서 사용된다.

5. x86 Assembly

서론

  • 0과 1로만 구성된 기계어는 해석하기 힘들기 때문에 어셈블리 언어어셈블러(어셈블리 언어를 기계어로 번역)가 고안됨.

  • 이후 기계어를 어셈블리 언어로 번역하는 역어셈블러를 개발했다.

  • 어셈블리어만 이해할 수 있다면 역어셈블러를 사용하여 소프트웨어를 분석할 수 있다.

어셈블리 언어

  • 컴퓨터의 기계어와 치환되는 언어.
  • 기계어와 CPU에 사용되는 명령어 집합구조(ISA)는 여러 종류이므로 어셈브리어도 여러종류임.

x64 어셈블리 언어

  • 명령어(동사에 해당)와 피연산자(목적어)의 문법 구조로 구성됨.
  • 명령어
    • 데이터 이동: mov, lea
    • 산술 연산: inc, dec, add, sub
    • 논리 연산: and, or, xor, not
    • 비교: cmp, test
    • 분기: jmp, je, jg
    • 스택: push, pop
    • 프로시져: call, ret, leave
    • 시스템 콜: syscall
  • 피연산자: 상수, 레지스터, 메모리가 올 수 있음.
    • 메모리는 타입 \[]로 표현. 타입에는 Byte, Word, DWord, QWord가 올 수 있으며, 각각 1, 2 4, 8바이트의 크기를 지정한다.

x86-64 어셈블리 명령어

  • 데이터 이동 명령어: 어떤 값을 레지스터나 메모리에 옮기도록 지시한다.
    • mov rdi, rsi: rsi의 값을 rdi에 대입
    • mov QWORD PTR[rdi], rsi: rsi의 값을 rdi가 가리키는 주소에 대입.
    • lea rsi, DWORD PTR[rdi]: rid가 가리키는 주소를 rsi에 대입.
  • 산술 연산: 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다.
    • add dst, src: dst += src
    • sub dst, src: dst -= src
    • inc op: op += 1
    • dec op: op -= 1
  • 논리 연산: and, or, xor, neg 등의 비트 연산을 지시한다. 따라서 비트 단위로 연산이 이루어진다.
    • and dst, src: dst와 src가 모두 1이여야 1
    • or dst, src: dst와 src 중 하나라도 1이면 1
    • xor dst, src: dst와 src가 서로 다르면 1
    • not op: op의 비트 전부 반전
  • 비교 명령어: 두 피연산자의 값을 비교하고 플래그를 설정한다.
    • cmp op1, op2: op1과 op2를 빼서 대소를 비교.
    • test op1, op2: op1과 op2에 AND 비트 연산을 취하여 비교한다.
  • 분기 명령어: rip 레지스터를 이동시켜 실행 흐름을 바꾼다.
    • jmp addr: addr로 rip를 이동.
    • je addr: 직전에 비교한 두 연산자가 같으면 점프
    • jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프

스택과 관련된 명령어

  • push val: rsp를 8만큼 빼고, val을 스택 최상단에 쌓음.
  • pop reg: 스택 최상단의 값을 꺼내서 reg에 대입히고 rsp를 8만큼 더함.

프로시저의 호출과 반환을 위한 명령어

  • call addr: addr에 위치한 프로시져 호출
  • leave: 스택프레임 정리
    • 스택프레임: 스택의 영역을 명확히 구분함.
  • ret: return address로 반환
profile
대학교 2학년, 컴퓨터학과

0개의 댓글