기계어(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)의 과정을 거쳐 바이너리로 번역됨
→ 컴파일러가 소스 코드를 어셈블리어로 컴파일하기 전에, 필요한 형식으로 가공하는 과정
#define 으로 단어와 상수를 매칭해 논 것을 단어⇒상수로 바꾸는 작업→ 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; }
→ 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
→ 컴파일로 생성된 어셈블리어 코드를 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 라는 함수가 호출이 되지만 해당 파일에 정의되어 있지 않음
→ printf 는 libc 라는 공유 라이브러리에 존재, 이는 gcc의 기본 라이브러리 경로에 있음
→ 링커는 바이너리가 printf 를 호출하면 libc의 함수가 실행될 수 있도록 연결해줌
→ 이 단계를 지나면 실행 가능한 프로그램이 완성
→ 위 어셈블 과정에서 나온 add.o 파일을 링크하는 명령어는 gcc add.o -o add --unresolved-symbols=ignore-in-object-files 이다.
→ --unresolved-symbols 컴파일 옵션은 이 파일 안에 main 함수가 존재하지 않기 때문에 에러를 방지하기 위해 넣어준 옵션
→ 바이너리를 분석하기 위해 바이너리를 읽을 수 있어야함
→ 하지만 기계어를 이해하기 어렵기 때문에 이를 다시 어셈블리어로 번역하고자하는 기법
→ 즉, Binary ⇒ Assemble Code ⇒ Source Code 가 리버싱의 절차
→ 규모가 크면 어셈블리 코드만으로 이해하기 힘듦
→ 어셈블리어보다 고급언어로 바이너리를 번역하는 것이 디컴파일
→ 디컴파일을 진행해주는 프로그램이 디컴파일러(Decompiler)
→ 최적화와 같은 과정에 의해 완벽하게 소스코드와 동일한 코드를 생성은 불가능에 가까움
→ 하지만 이 오차가 바이너리의 동작을 왜곡하지는 않기 때문에 디스어셈블러를 사용하는 것보다 압도적으로 효율적인 디컴파일러를 사용하는게 유리함
외적인 관찰만을 통해 정보를 알아내는 것을 의미
실행을 통해 동작을 분석하는 것을 의미
→ 어느 하나의 방법만 고수하는 것이 아닌 상황에 따라 적절한 방법을 탱해야 함
[Reference : Dreamhack]