'll Hacker

Binary 개념 정리 본문

Hacking/Reverse Engineering

Binary 개념 정리

씨이오가 되자 2024. 4. 11. 01:10
728x90

프로그램과 컴파일

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

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

                    (컴퓨터의 대부분이 Stored-Program Computer의 형태로 개발)

               = 바이너리 

 

컴파일러와 인터프리터

CPU가 수행해야 할 명령들을 프로그래밍 언어(고급언어)로 작성 = 소스 코드

컴퓨터가 이해할 수 있는 기계어(저급언어)의 형식으로 번역 = 컴파일

컴파일을 해주는 소프트웨어 = 컴파일러 ex) GCC, Clang, MSVC 등

이외 파이썬이나 자바스크립트는 컴파일이 필요없음.

왜냐면 사용자의 입력 또는 사용자가 작성한 스크립트를 그때 그때 번역해서 CPU에 전달하기 때문에(이를 인터프린팅이라고 함, 똑같이 처리해주는 프로그램을 인터프린터라고 함)

 

컴파일 과정

in C Programming (Preprocess -> Compile -> Assemble -> Link -> Binary)

예제

// Name: add.c

#include "add.h"

#define HI 3

int add(int a, int b) { return a + b + HI; }  // return a+b

 

1. Preprocess

  • 주석 제거
  • 매크로 치환
  • 파일 병합
$ 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; }

cat 결과, 먼저 소스코드의 주석이었던 //return a+b 사라짐(주석제거)

HI가 3으로 치환(매크로 치환), add.h의 내용이 #include 에 의해 병합(파일 병합)

 

2. Compile

코드에 문법적 오류가 있을땐 에러 출력

컴파일러는 코드를 번역할 때, 몇몇 조건을 만족하면 최적화 기술을 적용하여 효율적인 어셈블리 코드를 생성해줌

gcc에서는  -O, -O0, -O1, -O2, -O3, -Os, -Ofast, -Og 등의 옵션을 사용하여 최적화 적용가능

// 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);
}
0x0000000000000560 <+0>:     lea    rsi,[rip+0x1bd]        ; 0x724
0x0000000000000567 <+7>:     sub    rsp,0x8
0x000000000000056b <+11>:    mov    edx,0x1356  ; hex((0+99)*50) = '0x1356' = sum(0,1,...,99) 
0x0000000000000570 <+16>:    mov    edi,0x1
0x0000000000000575 <+21>:    xor    eax,eax
0x0000000000000577 <+23>:    call   0x540 <__printf_chk@plt>
0x000000000000057c <+28>:    xor    eax,eax
0x000000000000057e <+30>:    add    rsp,0x8
0x0000000000000582 <+34>:    ret

 

아래와 같이 -S옵션을 이용하면 소스 코드를 어셈블리 코드로 컴파일 ㄱㄴ

$ 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

 

3. Assemble

컴파일로 생성된 어셈블리어 코드를 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..............|
...

 

4. Link

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

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

#include <stdio.h>

int main() { printf("Hello, world!"); }

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, ...

 

디스어셈블과 디컴파일

디스어셈블(어셈블의 역과정)

$ 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
...

 

디컴파일 (고급언어로 바이너리를 번역)

-> IDA Freeware 사용