리버싱, 포너블 전에 알아야 할 기본 지식들을 정리 [일부 Dreamhack ]
Architecture
설명
CPU의 내부 구성 요소, 레지스터, 명령어 등을 포함하는 개념
아키텍처를 알아야 분석 시 어셈블리어 분석 및 POC 작성을 잘 할 수 있다. 그 이유는 비트 별로 레지스터의 명과 크기가 다르며 종류 별로 작동 가능한 OS가 다르기 때문이다. [그 외 다양한 이유가 있을 수 있음]
아키텍처의 비트는 CPU가 한번에 처리할 수 있는 데이터의 크기로 WORD라고 부른다.
x86-64 > WORD > 8byte > 64bit
종류
x86
- Intel 기반 32bit CPU
- x86은 32bit CPU의 대표명사처럼 불림
- Windows, Linux, Mac OS (BigSur까지) 지원
x86_64 (amd64)
- Intel 기반 64bit CPU, x86과 호환됨.
- Windows, Linux, Mac OS (BigSur까지) 지원
arm
- RISC(Reduced Instruction Set Computer) 아키텍처
- 저전력 소비, 높은 효율성
- arm 기반 32bit CPU
- x86과 아예 달라서 호환 안 됨.
- Linux, Mac OS (Monterey부 터), Android, iOS
arm64
- arm 기반 64bit CPU
- 32bit arm과 호환됨
- Linux, Mac OS (Monterey부터), Android, iOS
Register
여기선 64비트 위주로 정리를 해놓을 예정
x86-64는 x86의 64비트 확장 아키텍처이며 호환이 가능함 [레지스터의 호환이 가능하다는 것임 -> 32비트 프로그램을 64비트 환경에서 돌리는게 가능하다는 건 아니다. -> 물론 Windows 환경에선 일부분 지원을 해주기 때문에 돌아가긴 하는데 이게 레지스터 호환 때문만이라곤 보기 힘듦]
General Register
주용도는 있으나, 다양한 용도로 사용될 수 있음.
이름 | 용도 |
---|---|
rax (accumulator register) | 함수의 반환 값 |
rbx (base register) | x64에서는 주된 용도 없음 |
rcx (counter register) | 반복문의 반복 횟수, 각종 연산의 시행 횟수 |
rdx (data register) | x64에서는 주된 용도 없음 |
rsi (source index) | 데이터를 옮길 때 원본을 가리키는 포인터 |
rdi (destination index) | 데이터를 옮길 때 목적지를 가리키는 포인터 |
rsp (stack pointer) | 사용중인 스택의 위치를 가리키는 포인터 |
rbp (stack base pointer) | 스택의 바닥을 가리키는 포인터 |
Segment Register
Code, Stack, Data 메모리 영역을 가리킬 때 사용되는 CS, SS, DS 레지스터와 운영체제 별로 용도를 결정할 수 있는 ES, FS, GS 레지스터
Flag Register
가장 많이 접할 플래그들은 아래 표와 같음 [다른게 보인다면 추후에 검색해서 찾아볼 것]
플래그 | 의미 |
---|---|
CF(Carry Flag) | 부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 설정 됩니다. |
ZF(Zero Flag) | 연산의 결과가 0일 경우 설정 됩니다. |
SF(Sign Flag) | 연산의 결과가 음수일 경우 설정 됩니다. |
OF(Overflow Flag) | 부호 있는 수의 연산 결과가 비트 범위를 넘을 경우 설정 됩니다. |
Instruction Pointer Register
CPU가 처리할 명령어의 주소를 나타내는 레지스터이다. JMP, CALL, RET 등으로 변경된다.
Segment
데이터 용도별로 메모리의 구획을 나눈 것이다. 각 용도에 맞게 적절한 권한(읽기, 쓰기, 실행)을 부여할 수 있음.
세그먼트 | 역할 | 일반적인 권한 | 사용 예 |
---|---|---|---|
코드 세그먼트 | 실행 가능한 코드가 저장된 영역 | 읽기, 실행 | main() 등의 함수 코드 |
데이터 세그먼트 | 초기화된 전역 변수 또는 상수가 위치하는 영역 | 읽기와 쓰기 또는 읽기 전용 | 초기화된 전역 변수, 전역 상수 |
BSS 세그먼트 | 초기화되지 않은 데이터가 위치하는 영역 | 읽기, 쓰기 | 초기화되지 않은 전역 변수 |
스택 세그먼트 | 임시 변수가 저장되는 영역 | 읽기, 쓰기 | 지역 변수, 함수의 인자 등 |
힙 세그먼트 | 실행중에 동적으로 사용되는 영역 | 읽기, 쓰기 | malloc(), calloc() 등으로 할당 받은 메모리 |
Code Segment
실행 가능한 코드가 위치하는 영역으로 Text Segment라고도 부른다. 아래와 같은 함수가 해당 부분에 위치함.
int main() {
...
}
Data Segment
컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 위치하는 영역이다. 상수는 rodata, 가변하는 변수는 data에 위치한다.
아래와 같이 변수들이 해당 부분에 위치하며, 포인터의 경우 문자열은 rodata에 변수는 data에 위치하게 된다.
int data1 = 1234; // data
char data2[] = "writable"; // data
const char data3[] = "readable"; // rodata
char *ptr_data4 = "readable"; // data/rodata
int main() {
...
}
BSS Segment
컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 영역이다. 선언만하고 초기화하지 않은 변수도 포함되며 해당 변수들은 프로그램 시작 시 0으로 초기화됨.
int BSS;
int main() {
printf("%d\n", BSS); // 0
return 0;
}
Stack Segment
함수의 인자나 지역 변수와 같은 임시 변수들이 위치하는 영역이다. 스택 프레임이라는 단위를 사용하며 함수 시작과 끝에 프롤로그, 에필로그 과정을 통해 생성 및 반환된다.
int stack() {
int stack_data = 0;
scanf("%d", &stack_data);
return 0;
}
Heap Segment
C언어에서 malloc(), calloc() 등을 호출하여 할당받은 메모리가 위치하는 영역이다. 아래에서 heap_data_ptr
은 스택에 위치하며, 해당 값은 heap 주소를 가리킨다.
int heap() {
int *heap_data_ptr = malloc(sizeof(*heap_data_ptr));
*heap_data_ptr = 1234
return 0;
}
Assembly
opcode operand operand
와 같은 문법 구조를 지닌다.
Opcode
다양한 명령 코드들 중 일부 정리
각 명령 코드별 자세한 움직임은 분석 시에 찾아보면서 익히면 됨.
명령 코드 | |
---|---|
데이터 이동(Data Transfer) |
mov , lea
|
산술 연산(Arithmetic) |
inc , dec , add , sub
|
논리 연산(Logical) |
and , or , xor , not
|
비교(Comparison) |
cmp , test
|
분기(Branch) |
jmp , je , jg
|
스택(Stack) |
push , pop
|
프로시져(Procedure) |
call , ret , leave
|
시스템 콜(System call) | syscall |
Operand
피연산자는 []
로 둘러싸인 것으로 DWORD PTR[0x1234567]
와 같이 표기됨.
WORD가 2바이트 - 16비트인 이유는 제일 초기 16비트 아키텍처를 개발하였기 때문이다.
피연산자 타입 | 크기 |
---|---|
BYTE | 1byte |
WORD | 2byte |
DWORD | 4byte |
QWORD | 8byte |
호출 규약
호출 규약을 적용하는 것은 컴파일러의 몫이며, 따로 명시하지 않으면 CPU 아키텍처에 적합한 호출 규약을 알아서 적용시킴.
x86
|**함수호출규약**|**사용 컴파일러**|**인자 전달 방식**|**스택 정리**|**적용**| |—|—|—|—|—| |stdcall|MSVC|Stack|Callee|WINAPI| |cdecl|GCC, MSVC|Stack|Caller|일반 함수| |fastcall|MSVC|ECX, EDX|Callee|최적화된 함수| |thiscall|MSVC|ECX(인스턴스), Stack(인자)|Callee|클래스의 함수| x86-64
함수호출규약 | 사용 컴파일러 | 인자 전달 방식 | 스택 정리 | 적용 |
---|---|---|---|---|
MS ABI | MSVC | RCX, RDX, R8, R9 | Caller | 일반 함수, Windows Syscall |
System ABI | GCC | RDI, RSI, RDX, RCX, R8, R9, XMM0–7 | Caller | 일반 함수 |
x86-64 : SYSV
SYSV에서 정의한 함수 호출 규약은 다음의 특징을 갖는다.
- 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달합니다. 더 많은 인자를 사용해야 할 때는 스택을 추가로 이용합니다.
- Caller에서 인자 전달에 사용된 스택을 정리합니다.
- 함수의 반환 값은 RAX로 전달합니다.
x86 : cdecl
x86아키텍처는 레지스터의 수가 적기에 스택을 통해 인자를 전달함. 또한 Caller(호출자)가 스택을 정리함.