컴퓨터는 크게 CPU 와 메모리로 구성되어 있으며, CPU는 실행할 명령어와 명령어 처리에 필요한 데이터를 메모리에서 읽고, Instruction Set Architecture(ISA) 에 따라 이를 처리
만약 공격자가 메모리를 악의적으로 조작할 수 있다면 조작된 메모리 값에 의해 CPU 도 잘못된 동작을 할 수 있음
리눅스에서는 프로세스의 메모리를 크게 5가지의 세그먼트 로 구분한다.
코드, 데이터, BSS, 힙, 스택 세크먼트 가 있다.
운영체제가 메모리를 용도별로 나누면, 각 용도에 맞게 적절한 권한을 부여할 수 있다는 장점이 있다.
권한은 읽기, 쓰기, 실행 권한이 존재한다.
실행 가능한 기계 코드가 위치하는 영역으로 텍스트 세그먼트라고도 불린다.
프로그램이 동작하려면 코드를 실행할 수 있어야 하므로 읽기 권한과 실행 권한이 부여된다.
쓰기 권한 이 있으면 공격자가 악이적인 코드를 삽입하기 쉬워짐로 현대 운영체제는 이 세그먼트에 쓰기 권한을 제거힌다.
int main() { return 31337; }
컴파일 시점에 값이 정해진 전역 변수 및 전역 상수 들이 위치 함.
CPU 가 이 세그먼트의 데이터를 읽을 수 있어야 하므로, 읽기 권한 이 부여된다.
데이터 세그먼트는 쓰기가 가능한 세그먼트와 쓰기가 불가능한 세그먼트로 다시 분류된다.
char *str_ptr = "readonly";
// str_ptr 은 포인터 변수로써 "주소"를 가지고 있다. 변수이므로 data 세그먼트
// "readonly" 는 변하지 않는 상수로써 rodata 세그먼트이다. str_ptr 은 "readonly" 의 주소를 가지고 있다.
int main() {... }
BSS 세그먼트(Block started By Symbol Segment) 는 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 영역이다.
해당 세그먼트의 메모리 영역은 프로그램이 시작될 대 모두 0으로 초기화된다.
이 세그먼트에는 읽기 권한 및 쓰기 권한이 부여된다.
int bss_data;
int main() { ... }
스택 세그먼트는 프로세스의 스택이 위치하는 영역이다.
함수의 인자 나 지역변수 와 같은 임시 변수들이 실행중에 여기에 저장된다.
스택 세그먼트는 스택 프레임 이라는 단위로 사용된다. 스택 프레임은 함수가 호출될 때 생성되고, 반환될 때 해제된다.
스택은 다른 세그먼트와 달리 높은 주소에서 낮은 주소 방향으로 확장된다.
CPU 가 자유롭게 값을 읽고 쓸 수 있어야 하므로 읽기, 쓰기 권한이 부여된다.
힙 세그먼트는 힙 데이터가 위치하는 세그먼트이다.
스택과 마찬가지로 실행중에 동적으로 할당될 수 있으며, 리눅스에서는 스택 세그먼트와 반대 방향으로 자란다.
전퓨터에서 CPU 가 사용하는 명령어와 관련된 설계를 명령어 집합 구조(Instruction set Architecture 라고 한다.
가장 널리 사용되는 ISA 중 하나는 인텔의 x86-64 아키텍처 이다.
컴퓨터 구조 란 컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고, 이들을 구성하는 방법을 말한다.
CPU 명령어에 대한 설계는 명령어 집합구조(Instruction Set Architecture) 라고 하며, CPU 가 처리해야 하는 명령어를 설계하는 분야이다.
대표적으로 ARM, MIPS, AVR, 인텍의 x86 및 x86-64 등이 있다.
초기 컴퓨터 설계에 있어서 중요한 부분은 연산, 제어 저장 이었다.
근대의 컴퓨터는 연산과 제어를 위해 중앙처리장치(CPU) 를, 저장을 위해 기억장치 를 사용한다.
또한 장치간 데이터난 제어 신호를 교환할 수 있도록 버스 라는 전자 통로를 사용한다.
CPU 는 프로그램의 연산을 처리하고 시스템을 관리하는 컴퓨터의 두뇌이다.
산술/논리 연산을 처리하는 산술논리장치 와 CPU 를 제어하는 제어장치, 필요한 데이터를 저장하는 레지스터 등으로 구성된다.
기억장치는 컴퓨터가 동작하는데 필요한 여러 데이터를 저장하기 위해 사용되며, 용도에 따라 주기억장치 와 보조기억장치 로 분류된다.
버스는 컴퓨터 부품과 부품 사이 또는 컴퓨터와 컴퓨터 사이에 신호를 전송하는 통로를 말한다.
명령어 집합 구조 는 CPU 가 해석하는 명령어의 집합을 의미한다.
프로그램은 기계어로 이루어져 있는데, 프로그램을 실행하면 이 명령어들을 CPU 가 읽고 처리한다.
모든 컴퓨터가 동일한 수준의 연산 능력을 요구하지 않으며, 컴퓨팅 환경도 다양하기 때문에 여러 ISA 가 존재한다.
x64 아키텍처는 인텔의 64bit CPU 아키텍처이다.
32bit, 64bit 에서 32, 64 의 숫자는 CPU 가 한번에 처리할 수 있는 데이터의 크기이다.
컴퓨터 과학에서는 CPU 가 이해할 수 있는 데이터의 단위라는 의미에서 WORD 라고 부른다.
32bit CPU PC 에서는 WORD 가 32bit 이며, 64bit CPU PC 에서는 WORD 가 64bit 이다.
32bit CPU PC 는 최대 4GB 용량이 램만 사용가능하지만 64bit CPU PC 는 이론상으로 16엑사바이트 의 메모리를 사용할 수 있다.(실제로는 운영체제 레벨에서 인식을 못해서 128GB 정도까지만 사용가능하다)
레지스터는 CPU 가 이터를 빠르게 저장하고 사용할 때 이용하는 보관소이며, 산술 연산에 필요한 데이터를저장하거나 주소를 저장하고 참조하는 등 다양한 용도로 사용된다.
x64 아키텍처에서 레지스터 종류는 다음과 같다.
범용 레지스터, 세그먼트 레지스터, 명령어 포인터 레지스터, 플래그 레지스터
범용 레지스터는 주용도는 있으나, 그 외의 다양한 용도로 사용될 수 있는 레지스터이다.
x86-64 에서 각각의 범용 레지스터는 8바이트를 저장할 수 있다.
이름 | 주용도 |
---|---|
rax | 함수의 반환 값 |
rbx | x64 에서는 주된 용도 없음 |
rcx | 반복문의 반복 횟수, 각종 연산의 시행 횟수 |
rdx | x64 에ㅅ는 주된 용도 없음 |
rsi | 데이터를 옮길 때 원본을 가리키는 포인터 |
rdi | 데이터를 옮길 때 목적지를 가리키는 포인터 |
rsp | 사용중인 스택의 위치를 가리키는 포인터 |
rbp | 스택의 바닥을 가리키는 포인터 |
x64 아키텍처에는 cs, ss, ds, es, fs, gs 총 6 가지 세그먼트 레지스터가 존재한다.
각 레지스터의 크기는 16비트이다.
프로그램은 인련의 기계어 명령어 집합으로 이루어져 있다.
CPU가 해당 명령어 집합에서 어느 부분의 코드를 실행할지 가리키는게 명령어 포인터 레지스터의 역할이다.
x64 아키텍처의 명령어 레지스터는 rip 이며, 크기는 8바이트이다.
플래그 레지스터는 프로세서의 현재 상태를 저장하고 있는 레지스터이다.
x64 아키텍처에서는 RFLAGS 라고 불리는 64 비트 크기의 플래스 레지스터가 존재한다.
RFLAGS 는 64bit 이므로 최대 64개의 플래그를 사용할 수 있지만, 실제로는 20여개의 비트만 사용한다. 아래는 몇개의 예시이다
플래그 | 의미 |
---|---|
CF | 부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 설정 |
ZF | 연산의 결과가 0일 경우 설정 |
SF | 연산의 결과가 음수일 경우 설정 |
OF | 부호 있는 수의 연산 결과가 비트 범위를 넘을 경우 설정 |
컴퓨터의 동작은 기계어를 통해서 이루어지며, 해커가 하는 일은 해당 컴퓨터 구조의 허점을 공격하여 시스템을 장악하는 것이다.
소프트웨어를 역어셈블러 를 통해 어셈블리 코드로 변환시키면 소프트웨어를 분석할 수 있으며, 해당 분석 내용을 바탕으로 취약점을 찾을 수 있다.
어셈블리 언어는 컴퓨터의 기계어와 치환되는 언어이다.
이는 기계어가 여러 종류라면 어셈블리어도 여러 종류여야함을 의미한다.
현재는 IA-32, x86-64, ARM, MIPS 등 여러 ISA 에 해당하는 기계어와 그에 대응되는 어셈블리어가 있다.
어셈블리 언어는 명령어(opcode) 와 피연산자(operand) 로 이루어져 있다.
동작 | 명령 코드 |
---|---|
데이터 이동 | mov, lea |
산술 연산 | inc, dec, add, sub |
논리 연산 | and, or, xor, not |
비교 | cmp, test |
분기 | jmp, je, jg |
스택 | push, pop |
프로시저 | call, ret, leave |
시스템 콜 | syscall |
피연산자에는 총 3가지 종류가 올 수 있다.
메모리 피연산자는 [] 으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자 TYPE PTR 이 추가될 수 있다. 크기 지정자의 타입으로는 BYTE, WORD, DWORD, QWORD 가 올 수 있다.
각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다.
크기 지정자 | 설명 |
---|---|
QWORD PTR [0x8048000] | 해당 주소의 데이터를 8바이트만큼 참조 |
DWORD PTR [0x8048000] | 해당 주소의 데이터를 4바이트만큼 참조 |
WORD PTR [rax] | rax 가 가르키는 주소에서 데이터를 2바이트만큼 참조 |
현대 운영체제는 컴퓨터 자원의 효율적인 사용을 위해 내부적으로 매우 복잡한 동작을 한다.
운영체제는 연결된 모든 하드웨어 및 소프트웨어에 접근할 수 있으며, 이들을 제저할 수도 있다.
이러한 운영체제의 막강한 권한을 해커로부터 보호하기 위해 영역을 커널모드 와 유저모드 로 권한을 나눈다.
운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어 부여하는 권한
파일시스템, 입출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 커널 모드에서 진행된다.
유저모드는 운영체제가 사용자에게 부여하는 권한이다.
여러 소프트웨어를 사용하는 동작을 포함하며, 유저 모드에서는 해킹이 발생해도, 해커가 커널 모드에 접근할 수 없기 떄문에 해커로부터 커널의 권한을 보호할 수 있다.
시스템 콜은 유저모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용된다.
유저모드가 저수준의 작업을 하기 위해서는 커널에 요청 신호를 보내야하고, 이러한 신호를 시스템 콜이라고 한다.
유저 모드에서 도움을 요청하면 커널이 요청한 동작을 수행하고 결과를 유저에게 반환한다.
시스템 콜은 함수이며, 필요한 기능과 인자에 대한 정보를 레지스터 로 전달하면, 커널이 이를 읽어서 요청을 처리한다.
리눅스에서는 x64 아키텍처에서 rax 레지스터로 무슨 요청인지 나타내고, 아래의 순서대로 필요한 인자를 전달한다.
시스템 콜 함수들은 각자 고유의 번호를 가지고 있으며, 해당 시스템 콜을 실행하기 위해서는 rax 레지스터에 해당 번호를 집어넣고 syscall 명령을 실행하면 된다.