본문 바로가기
Security/Reversing

[Dreamhack Reverse Engineering] STAGE 2

by 단월໒꒱ 2022. 4. 28.

[Binary]

1. 프로그램과 컴파일

 1) 프로그램

   - 연산 장치가 수행해야 하는 동작을 정의한 일종의 문서

   - 프로그램을 연산 장치에 전달하면, CPU는 적혀있는 명령들을 처리하여 프로그래머가 의도한 동작을 수행함

   - 소프트웨어 개발자, 해커 등 많은 정보 분야의 엔지니어들이 프로그램을 '바이너리'라고 부름

      Stored-Program Computer에서 프로그램이 저장 장치에 이진 형태로 저장되기 때문

 

 2) 컴파일러

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

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

   - 컴파일러 : 컴파일을 해주는 소프트웨어

   - 대표적인 컴파일러 : GCC, Clang, MSVC

 

 3) 인터프리터

   - 모든 언어가 컴파일을 필요로 하는 게 아님  ex. Python, Javascript

   - 위의 언어들은 사용자의 입력, 또는 사용자가 작성한 스크립트를 그때 그때 번역하여 CPU에 전달

   - 위의 동작이 통역과 비슷하기 때문에 인터프리팅이라고 함

   - 인터프리터 : 인터프리팅을 처리해주는 프로그램

 

 4) 컴파일 과정

   - C언어로 작성된 코드는 일반적으로 전처리, 컴파일, 어셈블, 링크의 과정을 거쳐 바이너리로 번역됨

   - 예제 코드

 

 

// 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);

 

 

     ① 전처리

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

         - 언어마다 조금씩 다르지만, 컴파일 언어의 대부분은 아래의 전처리 과정을 거침

             ① 주석 제거

             ② 매크로 치환 (#define으로 정의된 매크로는 값으로 치환됨)

             ③ 파일 병합

 

 

$ gcc -E add.c > add.i
$ cat add.i

 

 

# 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; }

 

 

         - 위는 add.c를 전처리한 결과

         - gcc에서 -E 옵션을 사용하여 소스 코드의 전처리 결과를 확인할 수 있음

         - 주석이었던 //return a+b가 사라지고, HI가 3으로 치환되고, add.h가 #include에 의해 병합되었음

 

     ② 컴파일

         - 컴파일 : C로 작성된 소스 코드를 어셈블리어로 번역하는 것

         - gcc에서 -O -O0 -O1 -O2 -O3 -Os -Ofast -Og 등의 옵션으로 최적화를 할 수 있음

       

 

$ gcc -S add.i -o add.S
$ cat 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

 

 

         - -S 옵션을 이용하면 소스 코드를 어셈블리 코드로 컴파일할 수 있음

         - 최적화하여 컴파일하면, 반복문을 어셈블리어로 옮기는 것이 아니라, 반복문의 결과로 가질 값을 직접 계산하고, 이를 대입하는 코드를 생성하기도 함

         - 결과적으로 전보다 더 짧고, 실행 시간도 단축되는 어셈블리 코드가 만들어짐

 

     ③ 어셈블

         - 어셈블 : 컴파일로 생성된 어셈블리어 코드를 ELF 형식의 목적 파일로 변환하는 과정

         - ELF는 리눅스 실행파일 형식으로, 윈도우에서 어셈블한다면 목적 파일은 PE 형식을 갖게 됨

         - 목적 파일로 변환되고 나면 어셈블리 코드가 기계어로 번역되므로 더이상 사람이 행석하기 어려워짐

         

 

$ gcc -c add.S -o add.o
$ 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..............|
...

 

 

         - 위는 gcc의 -c 옵션을 통해 add.S를 목적 파일로 변환하고, 결과로 나온 파일을 16진수로 출력한 것

 

     ④ 링크 

         - 링크 : 여러 목적 파일들을 연결하여 실행 가능한 바이너리로 만드는 과정

 

         - 아래의 코드로 링크가 필요한 이유 설명

 

 

// Name: hello-world.c
// Compile: gcc -o hello-world hello-world.c

#include <stdio.h>
int main() { printf("Hello, world!"); }

 

 

         - 위의 코드에서 prinf 함수를 호출하지만 printf 함수의 정의는 hello-world.c에 없으며, libc라는 공유 라이브러리에 존재

         - libc는 gcc의 기본 라이브러리 경로에 있는데, 링커는 바이너리가 printf를 호출하면 libc의 함수가 실행될 수 있도록 연결

         - 링크를 거치고 나면 실행할 수 있는 프로그램이 완성됨

 

 

$ gcc add.o -o add -Xlinker --unresolved-symbols=ignore-in-object-files
$ file add
add: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, ...

 

 

         - 위는 add.o를 링크하는 명령어

         - 링크 과정에서 링커는 main 함수를 찾는데, add의 소스 코드에는 main 함수의 정의가 없으므로 에러가 발생할 수 있음

         - 이를 방지하기 위해 --unresolved-symbols를 컴파일 옵션에 추가

 

 

 

2. 디스어셈블과 디컴파일

 1) 디스어셈블

   - 바이너리를 분석하려면 바이너리를 읽을 수 있어야 하는데, 컴파일된 프로그램의 코드는 기계어라 자체적으로 이해하기 어려움

   - 이를 어셈블리어로 재번역하고자 햇는데, 이는 어셈블의 역과정이므로 디스어셈블이라고 부름

 

 

$ objdump -d ./add -M intel
...
000000000000061a <add>:
 61a:   55                      push   rbp
 61b:   48 89 e5                mov    rbp,rsp
 61e:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
 621:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
 624:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 627:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 62a:   01 d0                   add    eax,edx
 62c:   5d                      pop    rbp
 62d:   c3                      ret
 62e:   66 90                   xchg   ax,ax
...

 

 

   - 위의 명령어로 쉽게 디스어셈블된 결과를 확인할 수 있음

 

 

 

 

 2) 디컴파일

   - 디스어셈블 기술로 바이너리를 분석하기 쉬워졌지만, 규모가 큰 바이너리의 동작을 어셈블리 코드만으로 이해하기 어려움

   - 따라서 어셈블리어보다 고급 언어로 바이너리를 번역하는 디컴파일러를 개발함

   - 어셈블리어와 기계어는 거의 1:1로 대응되어 오차없는 디스어셈블러를 개발할 수 있었지만, 고급 언어와 어셈블리어 사이에는 이런 대응 관계 x

   - 또한 코드를 작성할 때 사용했던 변수나 함수의 이름 등은 컴파일 과정에서 전부 사라지고, 코드 일부분은 최적화와 같은 이유로 컴파일러에 의해 완전히 변형되기도 함

   - 이런 어려움으로 인해 디컴파일러는 일반적으로 바이너리의 소스 코드와 동일한 코드 생성이 불가능

   - 그러나 이 오차가 바이너리의 동작을 왜곡하지는 않으며, 디스어셈블러를 사용하는 것보다 압도적으로 부석 효율을 높여주기 때문에, 디컴파일러를 사용할 수 있다면 반드시 디컴파일러를 사용하는 것이 유리

   - 이 커리큘럼에서는 IDA Freeware를 사용할 것.

 

[동적분석과 정적분석]

1. 정적 분석

 1) 정적 분석

   - 프로그램을 실행시키지 않고 분석하는 방법

 

 2) 정적 분석의 장점

   - 프로그램의 전체구조를 파악하기 쉬움

   - 분석 환경의 제약에서도 비교적 자유로움

   - 바이러스와 같은 악성 프로그램의 위협으로부터 안전

 

 3) 정적 분석의 단점

   - 난독화(Obfuscation)가 적용되면 분석이 매우 어려워짐

   - 정적 분석만으로는 다양한 동적 요소를 고려하기 어려움

 

 4) 정적 분석의 예

   - 대표적인 정적 분석 도구 중 하나인 IDA를 이용하여 HelloWorld.exe 프로그램을 열면 아래와 같은 모습을 볼 수 있음

 

 

 

 

   - 위의 이미지 가운데 부분에서 프로그램의 어셈블리 코드를 살펴볼 수 있음

   - 우측에 위치하는 코드는 디컴파일된 코드 -> 기계어를 사람이 이해하기 쉬운 언어로 나타내어 분석을 더 쉽게 할 수 있게 됨

   - 좌측에서는 프로그램을 구성하는 여러 함수와 프로그램과 관련된 각종 정보를 살펴볼 수 있음

   - 이외에도 IDA는 문자열이나 함수를 어디에서 사용하는지를 보여주는 상호 참조 기능이나 함수의 실행 흐름을 보기 쉽게 해주는 제어 흐름 그래프 등을 통해 프로그램을 쉽게 이해할 수 있도록 함

 

   - 제어 흐름 그래프

 

 

 

2. 동적 분석

 1) 동적 분석

   - 프로그램을 실행시키면서 분석하는 방법

 

 2) 동적 분석의 장점

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

       - 어떤 입력에 대한 개별 함수 또는 프로그램의 출력을 빠르게 확인할 수 있으므로, 이 출력값들을 기반으로 동작 추론 가능

 

 3) 동적 분석의 단점

   - 분석 환경을 구축하기 어려울 수 있음

       - 프로그램을 실행하면서 분석하는 것이므로, 프로그램을 실행하지 못하면 동적 분석을 진행할 수 없음

   - '난독화'처럼 동적 분석에 대해서 이를 어렵게 하는 기법이 존재

       - 대표적인 것 : 안티 디버깅 (디버깅 방해)

     

 

if (is_debugging()) // 디버깅인지 확인
  exit(-1); // 프로그램 종료
Func();

 

 

       - 디버깅 당하고 있는지 검사하고, 디버깅 중이면 프로그램을 강제로 종료시키는 코드이다.

 

 

 4) 동적 분석의 예

   - 윈도우의 대표적인 동적 분석 도구로는 디버거 중 하나인 x64dbg가 있음

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

      이를 이용하면 실행 중인 어셈블리 코드, CPU의 레지스터 상태, 메모리와 스택 값을 확인할 수 있음

 

   - 아래는 helloworld.c 코드를 컴파일한 HelloWorld.exe를 64dbg로 동적 분석한 예

 

 

//helloworld.c
#include <stdio.h>
int main()
{
  int n = 0x31337;
  printf("Hello World 0x%x\n", n);
  return 0;
}

 

 

 

01234

 

   - 왼쪽 위 : 어셈블리

   - 왼쪽 아래 : 메모리

   - 오른쪽 위 : 레지스터

   - 오른쪽 아래 : 스택 정보

   

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

    2. 1의 코드를 실행한 직후, 이미지 오른쪽 아래를 보면 스택에 0x31337이 저장되어 있다.

    3. printf 함수를 호출한다.

        이미지 오른쪽 위의 레지스터를 보면, rcx에 Hello World 0x%x\n 문자열이, rdx에 0x31337이 저장되어 있음을 확인할 수 있다.

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

 

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

   - 함수를 실행할 때, 어떤 인자가 함수로 전달되며, 그 때의 메모리 상태는 어떠한지 등을 직관적으로 확인할 수 있음

 

 

 

 

 

댓글