본문 바로가기

개념정리/Reverse Engineering

2. Binary

 

이번 시간에는 바이너리에 대하여 공부할 것이다.

 


1. 프로그램

 프로그램은 연산 장치가 수행해야 하는 동작을 정의한 일종의 문서이다. 프로그램을 연산 장치에 전달하면, CPU는 적혀있는 명령들을 처리하여 프로그래머가 의도한 동작을 수행한다.

 과거에는 프로그램을 내부 저장 장치에 저장할 수 없어서 사람이 전선을 연결하여 컴퓨터에 전달하거나, 천공 카드에 프로그램을 기록하여 재사용하는 방식을 사용했다. 현대 컴퓨터는 프로그램을 메모리에 전자적으로 저장하여 사용을 간편하게 했다. 

 프로그램을 바이너리라고도 부른다. 왜냐하면 프로그램이 저장 장치에 이진(Binary) 형태로 저장되기 때문이다. 텍스트가 아닌 다른 데이터들도 바이너리라고 불리지만, 바이너리라고 하면 프로그램을 의미한다.

 

2. 컴파일러와 인터프리터

1) 컴파일러

 CPU가 수행해야 할 명령들을 프로그래밍 언어로 작성한 것을 소스 코드라고 하며, 이러한 소스 코드를 컴퓨터가 이해할 수 있는 기계어 형식으로 번역하는 것을 컴파일이라고 한다. 컴파일러는 컴파일을 해주는 소프트웨어이다. 대표적으로 GCC, Clang, MSVC 등이 있다. 한번 컴파일되면 결과물이 프로그램으로 남기 때문에 언제든지 이를 실행하여 같은 명령을 처리할 수 있다.

2) 인터프리터

 Python, Javascript 등의 언어는 컴파일이 필요하지 않는다. 이 언어들은 사용자의 입력, 또는 사용자가 작성한 스크립트를 그때 그때 번역하여 CPU에 전달한다. 이 동작은 통역과 비슷하기 때문에 인터프리팅이라고도 불리며, 처리해주는 프로그램을 인터프리터라고 한다.

 

3. 컴파일 과정

 C언어로 작성된 코드는 일반적으로 1) 전처리, 2) 컴파일, 3) 어셈블, 4) 링크의 과정을 거쳐 바이너리로 번역된다.

1) 전처리

전처리는 컴파일러가 소스 코드를 어셈블리어로 컴파일하기 전에 필요한 형식으로 가공하는 과정이다. 컴파일 언어의 대부분은 3가지 순서로 전처리 과정을 거친다.

  1. 주석 제거 : 주석은 개발자가 자신과 개발자들의 코드 이해를 돕기위해 작성한 메모이기 때문에 프로그램의 동작과 상관이 없으므로 전처리 단계에서 모두 제거한다.

  2. 매크로 치환 : #define으로 정의한 매크로는 자주 쓰이는 코드나 상숫값을 정의한 것이다. 전처리 과정에서 매크로의 이름은 값으로 치환된다.

  3. 파일 병합 : 일반적으로 프로그램은 여러 개의 소스와 헤더 파일로 이루어져 있다. 컴파일러는 이를 따로 컴파일해 합치기도 하지만 전처리 단계에서 파일을 합치고 컴파일 하기도 한다.

2) 컴파일

 컴파일은 C로 작성된 소스 코드를 어셈블리어로 번역하는 과정이다. 이 때 컴파일러는 소스 코드의 문법을 검사하고 코드에 문법적 오류가 있다면 컴파일을 멈추고 에러를 출력한다.

 컴파일러는 코드를 번역할 때 최적화 기술을 적용하여 효율적으로 어셈블리 코드를 생성한다. 예를 들어, 컴파일러는 반복문을 어셈블리어로 옮기는 것이 아니라, 반복문의 결과로 가질 값을 직접 계산하여, 이를 대입하는 코드를 생성한다. 이를 통해 사용자는 더 짧고, 실행 시간도 단축되는 어셈블리 코드가 만들어지게 된다.

3) 어셈블

 어셈블은 컴파일로 생성된 어셈블리어 코드를 ELF형식의 목적 파일로 변환하는 과정이다. ELF는 리눅스의 실행파일 형식이다. 윈도우에서 어셈블한다면 목적 파일은 PE형식을 갖게 된다. 목적 파일로 변환되고 나면 어셈블리 코드가 기계어로 번역되므로 더이상 사람이 해석하기 어려워진다.

4) 링크

 링크는 여러 목적 파일들을 연결하여 실행 가능한 바이너리로 만드는 과정이다. printf 함수의 정의는 바이너리에 없으며 libc라는 공유 라이브러리에 존재한다. lib는 gcc의 기본 라이브러리 경로에 있는데 링커는 바이너리가 printf를 호출하면 libc의 함수가 실행될 수 있도록 연결해 준다. 링크를 거치고 나면 실행할 수 있는 프로그램이 완성된다.

 

4. 디스어셈블

 바이너리를 분석하려면 바이너리를 읽을 수 있어야 한다. 그런데 컴파일된 프로그램의 코드는 기계어로 작성되어 있으므로 이해할 수 없다. 그래서 분석가들은 이를 어셈블리어로 재번역하게 만들었다. 이 과정이 어셈블의 역과정인 디스어셈블이라고 부른다.

 

5. 디컴파일

규모가 큰 바이너리의 동작을 어셈블리 코드만으로 이해하기 어렵기 때문에 어셈블리어로 번역하는 것보다 고급 언어로 바이너리를 번역하는 디컴파일러를 개발하였다.

어셈블리어와 기계어는 거의 일대일 대응이기 때문에 오차없이 디스어셈블을 할 수 있다. 하지만 고급 언어와 어셈블리어 사이에는 대응 관계가 없다. 또한 코드를 작성할 때 사용했던 변수나 함수이름 등은 컴파일 과정에서 전부 사라지고 코드의 일부분은 최적화와 같은 이유로 컴파일에 의해 완전히 변형된다.

이러한 이유 때문에 디컴파일러는 바이너리의 소스 코드와 동일한 코드를 생성하지 못한다. 그러나, 이러한 오차가 바이너리의 동작을 왜곡하지 않으며, 디스어셈블러를 사용하는 것 보다 분석 효율을 높여준다.  

 


 

이번 시간에는 바이너리에 대하여 공부하였다.

다음시간은 정적 분석과 동적 분석에 대하여 공부할 것이다.

 

'개념정리 > Reverse Engineering' 카테고리의 다른 글

4. Windows Memory Layout  (0) 2022.08.17
3. 정적 분석 & 동적 분석  (0) 2022.08.02
1. 리버스 엔지니어링  (0) 2022.07.28