기본 개념

Hyeon-Stone·2022년 6월 27일

Reversing

목록 보기
1/1

기계어(Machine Language):
→ 컴퓨터에 명령을 내리기 위한 컴퓨터의 언어

어셈블리어(Assembly Language):
→ 사람이 이해하기 쉬운 언어

어셈블러(Assembler):
→ 기계어로 번역해주는 도구

컴파일러(Compiler):
→ C, C++, Go 등을 비롯해 어셈블리어 보다 이해하기 쉬운 언어들을 기계어로 번역해주는 도구

프로그램(Program):
→ 연산 장치가 수행해야 하는 동작을 정의한 일종의 문서

programmable:
사용자가 정의한 프로그램을 해성하여 명렁어를 처리할 수 있는 연산 장치
→ ex. 컴퓨터

바이너리(Binary):
→ 프로그램이 저장 장치에 binary 형태로 저장됨
→ 즉, 프로그램을 의미


컴파일러와 인터프리터

프로그래밍 언어(Programing Language):
→ 프로그램을 개발하기 위해 사용하는 언어
→ C,C++, GO와 같은 고급 언어와 어셈블리어, 기계어와 같은 저급 언어가 존재

소스코드(Source Code):
→ CPU가 수행해야 할 명령들을 프로그래밍 언어로 작성한 것

컴파일 및 컴파일러(Comoplie/Compiler):
→ 소스코드를 컴퓨터가 이해할 수 있는 기계어의 형식으로 변역하는 것/
→ 컴파일 해주는 도구를 컴파일러라고 함
→ 컴파일의 정확한 의미는 Source Code를 Object Code로 번역하는 것

인터프리팅 및 인터프리터(Interpreting/Interpreter):
→ Python, Javascripts 등의 언어는 사용자의 입력 또는 사용자가 작성한 스크립트를 그때 그때 번역하여 CPU에 전달하는데, 이 동작이 통역과 비슷해 인터프리팅이라 불림
→ 이를 처리해주는 프로그램을 인터프리터라 함


컴파일 과정

→ C언어는 일반적으로 전처리(Preprocess), 컴파일(Compile), 어셈블(Assemble), 링크(Link)의 과정을 거쳐 바이너리로 번역됨

전처리(Preprocess)

→ 컴파일러가 소스 코드를 어셈블리어로 컴파일하기 전에, 필요한 형식으로 가공하는 과정

  1. 주석 제거
  2. 매크로 치환
    #define 으로 단어와 상수를 매칭해 논 것을 단어⇒상수로 바꾸는 작업
  3. 파일 병합
    → 여러개의 소스(.c)와 헤더(.h)를 합치는 작업

gcc를 사용해 -e옵션을 주면 전처리 과정의 결과를 화면에 보여줌

gcc -E add.c > add.i

// Name: add.h
int add(int a, int b);
// Name: add.c

#include "add.h"
#define HI 3
int add(int a, int b) { return a + b + HI; }  // return a+b
// Name: add.h
int add(int a, int b);
# 1 "add.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "add.c"
# 1 "add.h" 1
int add(int a, int b);
# 2 "add.c" 2
int add(int a, int b) { return a + b + 3; }

컴파일(Compile)

→ C로 작성된 소스 코드를 어셈블리어로 번역하는 것
→ 이 과정에서 소스코드의 문법을 검사하고, 오류가 있다면 컴파일을 멈추고 에러를 출력
→ gcc에서 -O -O0 -O1 -O2 -O3 -Os -Ofast -Og 등의 옵션으로 최적화 가능
-S 옵션으로 소스크드를 어셈블리 코드로 변환 가능

gcc -S add.i -o add.S

.file   "add.c"
        .intel_syntax noprefix
        .text
        .globl  add
        .type   add, @function
add:
.LFB0:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        mov     DWORD PTR -4[rbp], edi
        mov     DWORD PTR -8[rbp], esi
        mov     edx, DWORD PTR -4[rbp]
        mov     eax, DWORD PTR -8[rbp]
        add     eax, edx
        add     eax, 3
        pop     rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   add, .-add
        .ident  "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
        .section        .note.GNU-stack,"",@progbits

어셈블(Asemble)

→ 컴파일로 생성된 어셈블리어 코드를 ELF 형식의 Object file(.o)로 변환하는 과정
→ 변환되고 나면 기계어로 번역되어 더 이상 해석하기 어려움
-c 옵션으로 컴파일 후 나온 add.S 파일을 Object file로 변환 가능

file add.o
add.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ hexdump -C add.o
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  10 02 00 00 00 00 00 00  |................|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 0b 00 0a 00  |....@.....@.....|
00000040  55 48 89 e5 89 7d fc 89  75 f8 8b 55 fc 8b 45 f8  |UH...}..u..U..E.|
00000050  01 d0 5d c3 00 47 43 43  3a 20 28 55 62 75 6e 74  |..]..GCC: (Ubunt|
00000060  75 20 37 2e 35 2e 30 2d  33 75 62 75 6e 74 75 31  |u 7.5.0-3ubuntu1|
00000070  7e 31 38 2e 30 34 29 20  37 2e 35 2e 30 00 00 00  |~18.04) 7.5.0...|
00000080  14 00 00 00 00 00 00 00  01 7a 52 00 01 78 10 01  |.........zR..x..|
00000090  1b 0c 07 08 90 01 00 00  1c 00 00 00 1c 00 00 00  |................|
000000a0  00 00 00 00 14 00 00 00  00 41 0e 10 86 02 43 0d  |.........A....C.|
000000b0  06 4f 0c 07 08 00 00 00  00 00 00 00 00 00 00 00  |.O..............|
...

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

// Name: hello-world.c
// Compile: gcc -o hello-world hello-world.c
#include <stdio.h>
int main() { printf("Hello, world!"); }

printf 라는 함수가 호출이 되지만 해당 파일에 정의되어 있지 않음
printflibc 라는 공유 라이브러리에 존재, 이는 gcc의 기본 라이브러리 경로에 있음
→ 링커는 바이너리가 printf 를 호출하면 libc의 함수가 실행될 수 있도록 연결해줌
→ 이 단계를 지나면 실행 가능한 프로그램이 완성

→ 위 어셈블 과정에서 나온 add.o 파일을 링크하는 명령어는 gcc add.o -o add --unresolved-symbols=ignore-in-object-files 이다.
--unresolved-symbols 컴파일 옵션은 이 파일 안에 main 함수가 존재하지 않기 때문에 에러를 방지하기 위해 넣어준 옵션


디스어셈블과 디컴파일

디스어셈블(Disassemble)

→ 바이너리를 분석하기 위해 바이너리를 읽을 수 있어야함
→ 하지만 기계어를 이해하기 어렵기 때문에 이를 다시 어셈블리어로 번역하고자하는 기법

→ 즉, Binary ⇒ Assemble Code ⇒ Source Code 가 리버싱의 절차

디컴파일(Decompile)

→ 규모가 크면 어셈블리 코드만으로 이해하기 힘듦
→ 어셈블리어보다 고급언어로 바이너리를 번역하는 것이 디컴파일
→ 디컴파일을 진행해주는 프로그램이 디컴파일러(Decompiler)

→ 최적화와 같은 과정에 의해 완벽하게 소스코드와 동일한 코드를 생성은 불가능에 가까움
→ 하지만 이 오차가 바이너리의 동작을 왜곡하지는 않기 때문에 디스어셈블러를 사용하는 것보다 압도적으로 효율적인 디컴파일러를 사용하는게 유리함



Static & Dynamic Analysis

정적 분석(Static Analysis)

외적인 관찰만을 통해 정보를 알아내는 것을 의미

장점

  1. 전체 구조 파악이 쉬움
    → 프로그램이 어떤 함수로 구성되었는지
    → 함수들은 서로 어떤 호출 관계를 갖는지
    → 어떤 API를 사용하고 어떤 문자열을 포함하는 지
  2. 분석 환경의 제약에서 비교적 자유로움
    → apk와 같은 파일은 별도의 소프트웨어 없이는 시스템에서 실행할 수 없음
  3. 바이러스와 같은 악성 프로그램의 위협으로부터 안전함

단점

  1. 난독화(Obfuscation)가 적용되면 분석이 매우 어려워짐
  2. 다양한 동적 요소를 고려하기 어려움
    → 인자 입력같은 경우

동적 분석(Dynamic Analysis)

실행을 통해 동작을 분석하는 것을 의미

장점

  1. 코드를 자세히 분석해보지 않고도 프로그램의 개략적인 동작 파악 가능

단점

  1. 분석환경을 구축하기 어려울 수 있음
  2. 안티 디버깅(Anti Debugging)을 사용해 디버깅을 방해하는 기법이 있음

→ 어느 하나의 방법만 고수하는 것이 아닌 상황에 따라 적절한 방법을 탱해야 함


[Reference : Dreamhack]

0개의 댓글