티스토리 뷰

개발/해킹

리버스 엔지니어링 (1)

리오's 2024. 1. 19. 03:52

리버스 란?

리버스 엔지니어링이란 '리버스'라는 용어가 의미하듯이 과정을 거꾸로 하는 행위를 말합니다.

 

위의 케이스에서 리버싱이 사용될 수 있습니다

 - 판매 된지 너무 오래되어 개발이 중단된 프로그램에 패치가 필요할떄

 - 프로그램의 보안성을 평가하거나 악성코드를 분석할떄

 

컴퓨터 언어의 발전

1. 컴퓨터과학자들은 컴퓨터에 명령을 내리기 위해 기계어(Machine Language) 라는 컴퓨터 언어를 정의했고 이것은 0과 1로 이루어져있어 사람이 이해하기 어려웠고 명령을 내리기에 비효율적이 었음

2. 사람이 이해하기 쉬운 새로운 언어로 어셈블리어(Assembly Language)를 고안했고 이를 기계어로 번역해주는 어셈블러(Assembler)를 개발함

3. 어셈블리어는 기계어에 비하면 효율적이었으나, 큰 큐모의 프로젝트에선 부족했습니다 그래서 C언어와 같은 사람들이 이해하기 쉬운 언어들이 만들어졌고 이것을 기계어로 번역해주는 컴파일러(Compiler)를 개발함

 

고급 언어(High-Level Language) = C, C++, Rust

저급 언어(Low-Level Language) = 기계어, 어셈블리어

 

 

과거에는 프로그램을 내부 저장 장치에 저장할 수 없어 천공 카드(Punched card)에 프로그램을 기록하여 재사용하는 방식을 사용했지만 매우 비효율적이었음

이러한 단점을 해결한 Stored-Program Computer가 1950년경에 최초로 상용화 되었고 이로인해 프로그램을 메모리에 전자적, 광학적으로 저장할 수 있어 기존의 컴퓨터 보다 많은 프로그램을 저장할 수 있었음 이로서 오늘까지 Stored-Program Computer의 형태로 개발이 이루어지고있음

많은 엔지니어들은 프로그램을 바이너리(Binary)라고 부르는데 이유는 프로그램이 저장 장치에 이진(Binary) 형태로 저장 되기 떄문이다. 다른데이터 값도 바이너리라고 부르긴 하지만 통상적으로 바이너리는 프로그램을 의미한다.

 

 

소스코드를 컴퓨터가 이해할 수 있는 기계어 형식으로 번역하는 것을 컴파일(Compile)이라 하는데 대표적으로 GCC, Clang, MSVC등이 있다. 하지만 모든언어가 컴파일이 필요로 하는건 아니고 Python, Javascript등은 사용자가 작성한 스크립트들을 그때 번역하여 CPU에 전달하므로 이 동작이 통역과 비슷합니다 이를 인터프리팅(Interpreting)이라 부르고 이를 처리해주는 프로그램 역시 인터프리터(Interpreter)라고 부릅니다

컴파일(Compile)

 - 장점 : 결과물이 남아서 언제든 다시 읽을 수 있다

 - 단점 : 한 번 번역하는데 시간이 많이 필요하다

인터프리터(Interperter)

 - 장점 : 스크립트의 실행결과를 빠르게 볼 수 있다

 - 단점 : 같은 코드여도 매번 인터프리팅을 거쳐야한다

 

컴파일 과정

 

C언어로 작성된 코드는 위 과정을 통해 바이너리로 번역됩니다

전처리(Preprocess) > 컴파일(Compile) > 어셈블(Assemble) > 링크(Link)

// 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. 전처리

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

 - 주석은 프로그램의 동작과 상관이 없으므로 전처리 단계에서 모두 제거됨

 - 코드에서 #define 으로 정의한 메크로의 이름은 값으로 치환됨

 - 여러 개의 소스와 헤더 파일은 전처리 단계에서 모두 합쳐집니다

 

2. 컴파일

 = 소스 코드를 어셈블리어로 번역하는 것입니다. 이 과정에서 소스 코드의 문법을 검사하는데, 코드에 문법적 오류가 있다면 컴파일을 멈추고 에러를 출력합니다

 - 컴파일러는 코드를 번역할 떄, 몇몇 조건을 만족하면 최적화 기술을 적용하여 효율적인 어셈블리 코드를 생성해줍니다. gcc에서는 -O -O0 -O1 -O2 -O3 -Os -Ofast -Og 등의 옵션을 사용하여 최적화를 적용할 수 있습니다.

 ex) 아래 코드를 최적화하여 컴파일 하면, 컴파일러는 반복문을 어셈블리어로 옮기는 것이 아니라, 반복문의 결과로 x가 가질 값을 직접 계산하여, 이를 대입하는 코드를 생성합니다.

// Name: opt.c
// Compile: gcc -o opt opt.c -O2

#include <stdio.h>

int main() {
  int x = 0;
  for (int i = 0; i < 100; i++) x += i; // x에 0부터 99까지의 값 더하기
  printf("%d", x);
}

 

3. 어셈블

 = 컴파일로 생성된 어셈블리어 코드를 해당 실행 환경에 목적 파일(Object file)로 변환하는 과정입니다

 - ELF형식 : 리눅스 / PE형식 : 윈도우

 - 목적 파일로 변환되고 나서는 어셈블리 코드가 기계어로 번역되므로 더이상 사람이 해석하기 어려워집니다

 

4. 링크

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

 - 만약 printf 함수를 호출하게되면 위 함수는 libc라는 공유 라이브러리에 존재합니다

 - libc은 gcc의 기본 라이브러리 경로에 있는데, 링커는 바이너리라 printf를 호출하면 함수가 실행될 수 있도록 링크하는 역할을 합니다.

 

 

디스어셈블

바이너리를 분석하려면 바이너리를 읽을 수 있어야 하지만 컴파일된 프로그램은 기계어로 작성되어 이를 이해하기가 어렵습니다

그래서 분석가들은 이를 어셈블리어로 재번역합니다. 이를 디스어셈블(Disassemble) 이라고 부릅니다

 

디컴파일

디스어셈블로 바이너리를 분석하기는 쉬워졌지만, 여전히 큰 바이너리의 동작을 어셈블리 코드만으로 이해하기 어려웠습니다

그래서 리버스 엔지니어들은 어셈블리어보다 고급 언어로 바이너리를 번역하는 디컴파일러(Decompiler) 를 개발하였습니다.

 

어셈블리어와 기계어는 1대1로 매칭되어 오차없는 디스어셈블을 진행 할 수 있었지만, 고급 언어와 어셈블리어 사이에는 많은 과정을 통해 사용했던 변수나 함수의 이름등이 사리지거나, 코드의 일부분은 최적화와 같은 이유로 컴파일러에 의해 완전히 변형됩니다.

 

이런 어려움으로 원래와 동일한 소스 코드를 제공하지는 못하지만, 이 오차가 바이너리의 동작을 왜곡하지는 않습니다.

 

최근에는 Hex Rays, Ghidra 등의 뛰어난 디컴파일러들이 개발되어 분석의 효율을 더욱 높여주고 있습니다.

 

 

정적 분석

정적 분석(Static Analysis)은 프로그램을 실행시키지 않고 분석하는 방법입니다.

 

장점

1. 전체구조를 파악하기 쉽습니다

 - 바이너리가 어떤 함수로 구성됐고 함수들은 서로 어떤 호출 관계를 갖는지, 어떤 API를 사용하고 어떤 문자열을 포함하는지 등을 종합적으로 살펴볼 수 있습니다.

2. 분석 환경의 제약에서도 비교적 자유롭습니다

 - 실행을 전제로 하는 동적 분석을 윈도우 환경에서 apk를 대상으로 하기에는 다소 번거롭지만, 정적분석은 프로그램을 실행하지 않아도 되므로 분석을 지원하는 적절한 도구만 갖춘다면 시도할 수 있습니다.

3. 바이러스와 같은 악성 프로그램의 위협으로부터 안전합니다

 - 만약 바이러스를 동적 분석할 경우, 바이러스를 실제러 실행해야 하므로 자신의 컴퓨터가 감염될 우려가 있습니다.

 

단점

1. 난독화(Obfuscation) 가 적용되면 분석이 매우 어려워집니다

 - 리버스 엔지니어링으로부터 보호하기 위해 난독화 기법을 적용합니다. 난독화가 적용되면 프로그램의 코드가 심하게 변형돼서 이를 읽고 실행 흐름을 파악하기 어려워집니다.

2. 다양한 동적 요소를 고려하기 어렵습니다.

 - 프로그램은 실행중에 어떤 함수가 특정 시점에 정확히 어떤 인자와 어떤 전역 변수를 가지고 실행될지는 정적으로 알기 어렵습니다.

 

동적 분석

동적 분석(Dynamic Analysis)은 프로그램을 실행시키면서 분석하는 방법입니다

 

장점

1. 코드를 자세히 분석해보지 않고도 프로그램의 개략적인 동적을 파악할 수 있습니다.

 - 동적 분석은 어떤 입력에 대한 개별 함수 또는 프로그램의 출력을 빠르게 확인할 수 있으므로, 이 출력값들을 기반으로 동작을 추론해 볼 수 있습니다.

 

단점

1. 분석 환경을 구축하기 어려울 수 있습니다

 - 동적 분석은 프로그램을 실행하면서 분석하는 것이므로, 프로그램을 실행하지 못하면 동적 분석을 진행할 수 없습니다. 그래서 다른 환경의 프로그램을 동적 분석할때에는 그때에 맞는 환경을 구축해야하는데 과정에 따라 매우 번거롭고 어려울 수 있습니다.

 - 또한 난독화 처럼 동적 분석에서도 이를 어렵게 하는 기법이 있는데 일종의 디버깅을 방해하는 안티 디버깅(Anti Debugging)입니다. 프로그램이 자신이 디버깅 당하고 있는지 검사하고, 디버깅 중이면 프로그램을 강제로 종료시키는 방법이 있습니다.

 

 

 

 

 

* 참고

컴파일(Compile) : 어떤 언어로 작성된 소스 코드(Source Code)를 다른 언어의 목적 코드(Object Code)로 번역하는 것

 

'개발 > 해킹' 카테고리의 다른 글

리버스 엔지니어링 (3)  (0) 2024.01.29
리버스 엔지니어링 (2)  (0) 2024.01.29
리눅스 명령어 정리  (0) 2024.01.14
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
글 보관함