보안/드림핵 강의

[Reverse Engineering]Binary & Analysis 정리

haerim9.9 2025. 3. 31. 18:12

Introduction: Reverse Engineering

  • 엔지니어링 : 완성품과 이를 구성하는 부품들의 기능과 설계를 고안하고, 제작하는 과정.
  • 리버스 엔지니어링(Reverse Engineering) : 엔지니어링의 역과정.

 

리버싱의 용도

  • 좋은 예 : 개발을 중단한 프로그램에 대한 패치가 필요할 때 / 각종 프로그램의 보안성 평가 / 악성코드를 분석할 때
  • 나쁜 예 : 상용 프로그램을 구매 없이 무료로 이용하기 위해 쓰는 키젠 프로그램 등의 불법 프로그램 제작할 때 / 게임핵을 만들 때

리버싱 시 프로그램의 전체적인 작동 원리를 알아낼 수 있음. -> 지적 재산권 침해의 위험성.

테스팅, 연구, 학습 등의 리버싱 : 저작권을 침해하지 않는 한, 제한적으로 허용.

 


Background: Binary

프로그램을 연산 장치에 전달하면 CPU는 적혀있는 명령들을 처리한다.

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

  • programmable : 사용자가 정의한 프로그램을 해석하여 명령어를 처리할 수 있는 연산 장치. 예시로는 컴퓨터가 있다.
  • non-programmable : 예시로 일반 계산기가 있다.

 

과거의 컴퓨터 : 프로그램을 내부 저장 장치에 저장할 수 없었음. 프로그램이 바뀔 때마다 배선을 재배치해야 해서 비효율적. 크기가 큰 프로그램 사용이 어려움.

Stored-Program Coumputer : 위의 단점을 해결한 컴퓨터. 프로그램을 메모리에 저장할 수 있음.

 

*바이너리(Binary) : Stored-Program Coumpter에서 프로그램이 저장 장치에 이진(Binary) 형태로 저장된다. 따라서 이를 프로그램을 지칭하는 말로 부른다. 즉, 보통의 경우에 바이너리=프로그램.

 

 

프로그래밍 언어(Programming Language) : 프로그램을 개발하기 위해 사용하는 언어.

  • 고급 언어 : C, C++ 등
  • 저급 언어 : 어셈블리어, 기계어등

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

컴파일(Compile) : 소스 코드를 기계어(=컴퓨터가 이해할 수 있는 형식)의 형식으로 번역하는 것. 

 

*컴파일이 필요없는 언어 : Python, Javascript 등.

해당 언어들은 입력 또는 스크립트를 그때 그때 번역해서 CPU에 전달한다. 이 동작을 인터프리팅(Interpreting)이라 부른다. 이를 처리해주는 프로그램을 인터프리터(Interpreter)라고 한다.

 

 

C언어의 컴파일 과정

  1. 전처리(Preprocess) : 컴파일러가 소스 코드를 어셈블리어로 컴파일하기 전에 필요한 형식으로 가공하는 과정.
    (1)주석 제거 / (2)매크로 치환 : #define과 같은 것으로 정의한 매크로의 이름을 값으로 치환한다. / (3)파일 병합 : 일반적인 프로그램은 여러 개의 소스와 헤더 파일로 이루어져 있다. 이를 컴파일러가 합치기도 하지만 전처리 단계에서 합치고 컴파일하기도 한다.
  2. 컴파일(Compile) : C로 작성된 소스 코드를 어셈블리어로 번역하는 것.
    몇몇 조건을 만족하면 최적화 기술을 적용해 효율적인 어셈블리 코드를 생성한다. 예시로, 최적화 시 반복문을 그대로 번역하는 게 아니라 반복문의 결과를 대입하는 코드를 생성한다.
  3. 어셈블(Assemble) : 어셈블리어 코드를 ELF형식의 목적 파일(Object file)로 변환하는 과정. ELF는 리눅스의 실행파일 형식이고, 윈도우에서 어셈블 시 목적 파일은 PE형식이다.
    목적 파일로 변환되고 나면 어셈블리 코드가 기계어로 번역된다.
  4. 링크(Link) : 목적 파일들을 연결하여 실행 가능한 바이너리로 만드는 과정.

 

바이너리 분석을 하려면 바이너리를 읽을 수 있어야 한다. 기계어를 이해하기란 어려우므로 어셈블리어로 번역해야 한다.

디스어셈블(Disassemble) : 기계어로 작성된 코드를 어셈블리어로 재번역하는 과정.

 

어셈블리 코드만으로는 바이너리의 동작을 이해하기 어렵다. 어셈블리어보다 고급 언어로 바이너리를 번역해야 한다.

디컴파일러(Decompiler) : 어셈블리어로 작성된 코드를 고급 언어로 재번역하는 과정.

-> 코드를 작성할 때 사용했던 변수나 함수의 이름은 컴파일 과정에서 사라지고, 최적화 등의 이유로 완전히 변형되기도 한다. 따라서 디컴파일러로는 바이너리의 소스 코드와 동일한 코드를 생성하진 못한다.

 

디컴파일러 : IDA Freeware(무료), Hex Rays, Ghidra 등

 

 


 

요약

더보기

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


Background: Static Analysis vs. Dynamic Analysis

리버스 엔지니어링에서 소프트웨어를 분석하기 위해 사용하는 분석 방법

  • 정적 분석(Static Analysis) : 외적인 관찰만을 통해 정보를 알아내는 것. 프로그램을 실행시키지 않고 분석하는 방법이다.
  • 동적 분석(Dynamic Analysis) : 실행을 통해 동작을 분석하는 것.

 

 

정적 분석

  1. 장점 :
    프로그램의 전체구조를 파악하기 쉽다. 이 정보들을 바탕으로 프로그램을 큰 관점에서 이해할 수 있다.
  2. 분석 환경의 제약에서 비교적 자유롭다. 분석을 지원하는 적절한 도구만 갖추면 시도할 수 있는 것이다.
    바이러스와 같은 악성 프로그램의 위협으로부터 안전하다. 프로그램을 실행하지 않고 분석하므로 감염될 우려가 없다.

단점 :

  1. 프로그램에 난독화(Obfuscation)가 적용되면 분석이 어려워진다. 난독화가 적용되면 프로그램의 코드가 심하게 변형돼서 이를 읽고 흐름을 파악하기 어려워진다. 상용 난독화 서비스들을 무력화하는 방법은 알려지지 않았다.
  2. 다양한 동적 요소를 고려하기 어렵다. 프로그램은 실행중에 영향을 주고 받는 여러 함수로 구성된다. 따라서 어떤 함수가 특정 시점에 정확히 어떤 인자와 전역 변수를 가지고 실행될지, 알기 어렵다.

 

IDA 설명 : 정적 분석 도구 중 하나.

이미지 출처 : 드림핵 / IDA로 연 Helloworld.exe

이미지 중앙 : 어셈블리 코드

이미지 우측 : 디컴파일(Decompile)된 코드

이미지 좌측 : 프로그램을 구성하는 여러 함수, 프로그램과 관련된 각종 정보

 

상호 참조(Cross Reference)기능 : 문자열이나 함수를 어디에서 사용하는지 보여주는 기능

제어 흐름 그래프(Control Flow Graph) : 함수의 실행 흐름을 보기 쉽게 해주는 그래프

 

 

동적 분석

장점 : 

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

단점 : 

  1. 분석 환경을 구축하기 어려울 수 있다. 가상 머신을 구축하거나 프로그램을 실행할 수 있는 장치의 구매가 번거로울 수 있기 때문이다.
  2. 동적 분석의 일종인 디버깅을 방해하는, 안티 디버깅(Anti Debugging)이 있다. 예시로 디버깅 중인지 확인하고 디버깅 중이면 프로그램을 강제로 종료시키는 방법이 있다.

 

x64dbg 설명 : 윈도우의 대표적 동적 분석 도구. 디버거 중 하나.

*디버거 : 프로그램의 버그를 찾아내고 제거하기 위해 사용되는 도구.

출처 : 드램핵 / x64dbg의 각 패널

이미지 좌측 위 : 어셈블리

이미지 좌측 아래 : 메모리

이미지 우측 위 : 레지스터

이미지 우측 아래 : 스택 정보

 

 

출처 : 드림핵 / 1. int n = 0x31337에 대응되는 명령어.

1. 현재 코드는 0x01337이라는 상숫값을 스택에 저장한다. 소스 코드의 int n = 0x31337에 대응된다.

출처 : 드림핵 / 2. 명령어 실행 이후 스택에 0x31337이 저장된 모습. / 3. printf() 호출 직전 인자가 세팅된 모습.

2. 1의 코드를 실행한 직후, 스택에 0x31337이 저장된 걸 확인할 수 있다.

3. printf()함수를 호출한다. 레지스터를 보면, rcx에 Hello World 0x%x\n문자열이, rdx에 0x31337이 저장된 것을 확인할 수 있다.

출처 : 드림핵 / 4. printf() 호출 결과

4. 3의 코드를 실행한 후 , 프로그램을 보면 Hello World 0x31337이 출력된 걸 확인할 수 있다.

 

동적 분석은 이처럼 프로그램을 실제로 실행하며 시스템의 변화를 관찰하는 것이 특징이다.