[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;
}
- 왼쪽 위 : 어셈블리
- 왼쪽 아래 : 메모리
- 오른쪽 위 : 레지스터
- 오른쪽 아래 : 스택 정보
1. 현재 코드는 0x31337이라는 상수값을 스택에 저장하고 있다. 소스 코드의 int n = 0x31337에 대응된다.
2. 1의 코드를 실행한 직후, 이미지 오른쪽 아래를 보면 스택에 0x31337이 저장되어 있다.
3. printf 함수를 호출한다.
이미지 오른쪽 위의 레지스터를 보면, rcx에 Hello World 0x%x\n 문자열이, rdx에 0x31337이 저장되어 있음을 확인할 수 있다.
4. 3의 코드를 실행한 직후, 프로그램을 보면 Hello World 0x31337이 출력된 것을 확인할 수 있다.
- 이처럼 동적 분석은 프로그램을 실제로 실행하여 시스템의 변화를 관찰하는 것이 특징
- 함수를 실행할 때, 어떤 인자가 함수로 전달되며, 그 때의 메모리 상태는 어떠한지 등을 직관적으로 확인할 수 있음
'Security > Reversing' 카테고리의 다른 글
[어셈블리어 분석 실습] example 1 (0) | 2022.05.08 |
---|---|
[어셈블리어 분석 실습] 기본 내용 (0) | 2022.05.08 |
[Dreamhack Reverse Engineering] STAGE 4 (0) | 2022.05.06 |
[Dreamhack Reverse Engineering] STAGE 3 (0) | 2022.04.28 |
[Dreamhack Reverse Engineering] STAGE 1 (0) | 2022.04.28 |
댓글