리버스 엔지니어링(Reverse Engineering) : 역공학
•
기계어(Machine Language) : 컴퓨터의 언어 →
•
어셈블러(번역장치) →
•
어셈블리어(Assembly Language) : 번역된 저급 언어
•
← 컴파일러(Compiler)
•
← 고급 언어(C, C++, C#…)
•
바이너리 = 프로그램
컴파일러와 인터프리터
•
프로그래밍 언어
◦
고급 언어 : C, C++, Go
◦
저급 언어 : 어셈블리어, 기계어
•
컴파일 : CPU가 수행해야할 명령들을 프로그래밍 언어로 작성한 것(소스코드)를 기계어 형식으로 번역하는 것
•
인터프리팅 : 사용자의 입력, 또는 사용자가 작성한 스크립트를 그때 그때 번역하여 CPU에 전달하는 것
컴파일 과정
전처리(Preprocessing)
•
컴파일러가 소스 코드를 어셈블리어로 컴파일하기 전에, 필요한 형식으로 가공하는 과정
1.
주석 제거
•
주석 : 개발자가 자신과 개발자들의 코드 이해를 돕기위해 작성하는 메모
•
주석은 프로그램의 동작과 상관이 없으므로 전처리 단계에서 모두 제거
2.
매크로 치환
•
#define으로 정의한 매크로 : 자주 쓰이는 코드나 상숫값을 단어로 정의한 것
•
전처리 과정에서 매크로의 이름은 값으로 치환
3.
파일 병합
•
일반적인 프로그램은 여러 개의 소스와 헤더 파일로 이루어져 있습니다.
•
컴파일러는 이를 따로 컴파일해 합치기도 하지만, 어떠한 경우는 전처리 단계에서 파일을 합치고 컴파일하기도 함.
// Name: add.c
#include "add.h"
#define HI 3
int add(int a, int b) { return a + b + HI; }
// return a+b
C
복사
// Name: add.h
int add(int a, int b);
C
복사
전처리
$ gcc -E add.c > add.i
$ cat add.i
Bash
복사
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; }
Assembly
복사
컴파일(Compile)
C로 작성된 소스 코드를 어셈블리어로 번역하는 것.
이 과정에서 컴파일러는 소스 코드의 문법을 검사하는데, 코드에 문법적 오류가 있다면 컴파일을 멈추고 에러를 출력
// 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);
}
C
복사
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
Assembly
복사
어셈블리 코드 컴파일 (add.S)
$ gcc -S add.i -o add.S
$ cat add.S
Bash
복사
.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
Assembly
복사
어셈블(Assemble)
•
컴파일로 생성된 어셈블리어 코드를 ELF형식(윈도우는 PE형식)의 목적 파일(Object file)로 변환하는 과정
•
*ELF : 리눅스의 실행파일 형식
•
목적파일로 변환 (add.o)
$ 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..............|
...
Bash
복사
링크(Link)
•
여러 목적 파일들을 연결하여 실행 가능한 바이너리로 만드는 과정
// Name: hello-world.c
// Compile: gcc -o hello-world hello-world.c
#include <stdio.h>
int main() { printf("Hello, world!"); }
C
복사
$ 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, ...
Bash
복사