시스템 스터디
컴퓨터 구조(폰노이만 구조)
: 최초의 프로그램이 내장된 컴퓨터 방식
- 연산의 수행과 관련된 명령어와, 연산에 필요하거나 결과로 나온 데이터를 저장장치(메모리)에 보관 → 순차적으로 메모리에 저장된 것을 끄집어내 지시대로 연산을 수행하는 방식
폰 노이만 구조 컴퓨터의 명령 주기
1. 명령어 가져오기
: 기억장치(메모리)부터 명령어를 가져오는 과정
2. 명령어 해석
: 앞서 가져온 명령어가 어떤 명령어인지 해석을 진행하는 과정
3. 피연산자 인출
: 명령의 실행에 필요한 정보를 기억장치로 접근해 가져오는 과정
4. 명령어 실행
: 앞서 가져온 연산자와 데이터를 가지고 연산을 수행하고 저장
5. 인터럽트 체크
중앙처리장치(CPU)
- 프로그램의 연산을 처리하고 시스템을 제어하는 컴퓨터의 두뇌
- 프로세스의 코드를 불러오고, 실행하고, 결과를 저장하는 일련의 모든 과정이 일어나는 곳
- 산술/논리 연산을 처리하는 산술논리장치(ALU)와 CPU에 필요한 데이터를 저장하는 레지스터 등으로 구성
기억장치(메모리)
- 컴퓨터가 동작하는데 필요한 여러 데이터를 저장하기 위해 사용
- 용도에 따라 주기억장치와 보조기억장치로 분류
- 주기억장치 : 프로그램 실행과정에서 필요한 데이터들을 임시로 저장하기 위해 사용 ex) 램
- 보조기억장치 : 운영 체제, 프로그램 등과 같은 데이터를 장기간 보관하고자 할 때 사용 ex) 하드드라이브, SSD
버스
- 컴퓨터 부품과 부품 사이 또는 컴퓨터와 컴퓨터 사이에 신호를 전송하는 통로
- 버스의 종류
- 데이터 버스 : 데이터가 이동
- 주소 버스 : 주소를 지정
- 제어 버스 : 읽기/쓰기를 제어
메모리 구조
segment
: 적재되는 데이터의 용도별로 메모리의 구획을 나눈 것
⇒ segment = 메모리 영역
- 프로세스의 메모리를 크게 5가지의 segment로 구분 → 각 용도에 맞게 적절한 권한 부여 가능
- 권한 : 읽기, 쓰기, 실행
- CPU는 메모리에 대한 권한이 부여된 행위만 가능
코드 세그먼트(Text Segment)
: 실행 가능한 기계 코드가 위치하는 영역
- 권한 : 읽기 및 실행 → 프로그램이 동작하려면 코드를 실행할 수 있어야 하기 때문
- +) 쓰기 권한이 존재할 경우 공격자가 악의적인 코드를 삽입하기 쉬워지기 때문에 쓰기 권한은 제거
int main( )
{
return 1;
}
main 함수가 컴파일되면 기계 코드로 변환 → 이 기계 코드가 코드 세그먼트에 위치
데이터 세그먼트
: 컴파일 시점에 값이 정해진 전역 변수 및 전역 상수
- 권한 : 읽기 권한 ⇒ CPU가 이 세그먼트의 Data를 읽을 수 있어야 하기 때문
- 쓰기 가능한 세그먼트 : 전역 변수와 같이 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 위치
- 쓰기 불가능한 세그먼트(read-only data) : 프로그램이 실행되면서 값이 변하면 안되는 데이터들이 위치 / ex) 전역 상수
int data_num = 31337; // data
char dta_rwstr[] = "writable_data"; // data
const char data_rostt[] = "readonly_data"; // rodata
char *str_ptr = "readonly"; // str_ptr은 data, 문자열은 rodata
int main(){ ... }
str_ptr은 “readonly”라는 문자열을 가리키고 있음 ⇒ 이 문자열은 상수 문자열로 취급
- 문자열 “readonly”는 rodata에 위치
- 이를 가리키는 str_ptr은 전역 변수로서 data에 위치
BSS 세그먼트
: 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치
- 권한 : 읽기 및 쓰기 권한
- 전역변수 등이 포함됨
- 프로그램이 시작될 때, 모두 0으로 값이 초기화됨
int bss_data;
int main() {
printf("%d\\n", bss_data);
return 0;
}
bss_data는 초기화되지 않은 전역 변수이기 때문에 0이며, BSS 세그먼트에 위치
스택 세그먼트
: 프로세스의 스택이 위치하는 영역
- 권한 : 읽기 및 쓰기 권한
- 함수의 인자나 지역 변수와 같은 임시 변수들이 실행중 여기에 저장됨
- 스택 프레임이라는 단위로 사용
- 스택 프레임은 함수가 호출될 때 생성되고, 반환될 때 해체됨
- 프로그램의 전체 실행 흐름은 사용자의 입력을 비롯한 여러 요인에 영향을 받음
→ 어떤 프로세스가 실행될 때, 이 프로세스가 얼만큼 스택 프레임을 사용하게 될지 예상하는 것은 일반적으로 불가능함.
- 운영체제는 프로세스 시작 시 작은 크기의 스택 세그먼트를 먼저 할당 / 부족해질 때마다 이를 확장함
- 스택은 확장될 때, 기존 주소보다 낮은 주소로 확장
void func() {
int choice = 0;
scanf("%d", &choice);
if(choice)
call_true();
else
call_false();
return 0;
}
choice의 값에 따라 call_ture 혹은 call_false가 결정됨 / choice가 스택에 저장됨
힙 세그먼트
: 힙 데이터가 위치하는 세그먼트
- 권한 : 읽기 및 쓰기 권한
- 스택과 마찬가지로 실행 중에 동적으로 할당될 수 있음
- C언어에서 malloc( ), calloc( ) 등을 호출해서 할당받는 메모리는 힙 세그먼트에 위치
int main(){
int *heap_data_ptr = malloc(sizeof(*heap_data_ptr)); // 동적 할당한 힙 영역의 주소를 가리킴
*heap_data_ptr = 31337; // 힙 영역에 값을 씀
printf("%d\\n", *heap_data_ptr); // 힙 영역의 값을 사용함
return 0;
}
heap_data_ptr에 malloc( )으로 동적 할당한 영역의 주소를 대입하고 이 영역에 값을 씀
heap_data_ptr은 지역변수 → 스택에 위치 / malloc으로 할당받은 힙 세그먼트의 주소를 가리킴
레지스터
: CPU가 요청을 처리하는데 필요한 데이터를 일시적으로 저장하는 기억 장치
- 메모리에서 값을 받아와 저장하고, CPU에게 전달하는 역할을 함.
x86 레지스터
: x86 아키텍처는 8개의 범용 레지스터, 6개의 세그먼트 레지스터, 1개의 플래그 레지스터를 가짐
범용 레지스터
: 작은 데이터의 임시 저장 공간
- 연산 처리 및 데이터의 주소를 지정하는 역할
- 컴퓨터의 장치들을 제어하는 역할
EAX | 산술 연산 및 논리 연산 수행 |
EBX | 메모리 주소 저장 |
ECX | - 반복문 사용시 반복 카운터로 사용 - 반복할 횟수를 지정하고 반복 작업을 수행 / 명령어를 사용할 때마다 값이 하나씩 줄어듬 |
EDX | - EAX 레지스터와 같이 쓰임 - 부호 확장 명령 등에 사용 - 큰 수의 곱셈 또는 나눗셈 연산 |
EDI | 복사할 때 목적지 주소 저장 |
ESI | 데이터를 조작하거나 복사할 때 데이터의 주소 저장 |
ESP | 메모리 스택의 끝 지점 주소 포인터 |
EBP | 메모리 스택의 첫 시작 주소 포인터 |
+) 명령어 포인터 EIP : 다음에 실행할 명령어의 메모리 주소를 저장하는 레지스터
세그먼트 레지스터
: 세그먼트에 대한 주소 지정을 제공함
- 자신의 주소 지정 능력을 제공함
- 세그먼트 레지스터는 16비트
CS 기계 명령을 포함한 코드 세그먼트의 시작 주소를 가리킴 DS - 프로그램에 정의된 데이터 세그먼트의 시작 주소를 가리킴
- 데이터의 오프셋을 DS 레지스터에 저장된 주소 값에 더해 데이터 세그먼트 내에 위치해 있는 데이터의 주소를 참조SS 실행 과정에서 필요한 데이터나 연산 결과 등을 임시로 저장하거나 삭제할 때 사용하는 스택 세그먼트의 시작 주소를 가리킴 ES 추가로 사용된 데이터 세그먼트의 주소를 가리킴 FS 사용처 미정, 여분 레지스터 GS 사용처 미정, 여분 레지스터
플래그 레지스터
: 마이크로프로세서에서 다양한 산술 연산 결과의 상태를 알려주는 플래그 비트들을 조정
- 조건문과 같은 실행 순서의 분기를 정할 때 주로 사용됨
- 32비트 레지스터
S | 상태 플래그 |
C | 제어 플래그 |
X | 시스템 플래그 |
CF | 부호 없는 수의 연산 결과가 할당된 비트의 범위를 넘을 경우 설정 |
OF | 오버플로우 / 산술 연산 후 상위(가장 왼쪽) 비트의 오버플로우를 나타냄 |
SF | 연산의 결과가 음수일 경우 1로 설정 그 외에는 0 |
ZF | 연산의 결과가 0일 경우 1로 설정 그 외에는 0 |
x64 레지스터
범용 레지스터
: x86 범용 레지스터를 64비트로 확정하고 8개의 새 레지스터 추가
RAX | 산술 연산 및 논리 연산 수행 및 함수의 반환 값 저장 |
RBX | 메모리 주소 저장 / 주된 용도 없음 |
RCX | - 반복문 사용시 반복 카운터로 사용 - 반복할 횟수를 지정하고 반복 작업을 수행 |
RDX | - EAX 레지스터와 같이 쓰임 - 부호 확장 명령 등에 사용 - 큰 수의 곱셈 또는 나눗셈 연산 |
RDI | 복사할 때 목적지 주소를 가르키는 포인터 |
RSI | 데이터를 조작하거나 복사할 때 원본을 가르키는 포인터 |
RSP | 메모리 스택의 끝 지점 주소 포인터 |
RBP | 메모리 스택의 첫 시작 주소 포인터 |
R8 ~ R15 | 범용 레지스터 |
+) 명령어 포인터 RIP : 명령어 포인터의 64비트 버전 / 8바이트
세그먼트 레지스터
: 세그먼트에 대한 주소 지정을 제공함
- 자신의 주소 지정 능력을 제공함
- 세그먼트 레지스터는 16비트
- x86과 동일한 세그먼트 레지스터가 사용
CS 기계 명령을 포함한 코드 세그먼트의 시작 주소를 가리킴 DS - 프로그램에 정의된 데이터 세그먼트의 시작 주소를 가리킴
- 데이터의 오프셋을 DS 레지스터에 저장된 주소 값에 더해 데이터 세그먼트 내에 위치해 있는 데이터의 주소를 참조SS 실행 과정에서 필요한 데이터나 연산 결과 등을 임시로 저장하거나 삭제할 때 사용하는 스택 세그먼트의 시작 주소를 가리킴 ES 추가로 사용된 데이터 세그먼트의 주소를 가리킴 FS 사용처 미정, 여분 레지스터 GS 사용처 미정, 여분 레지스터
플래그 레지스터
: 64비트 버전의 플래그 레지스터
- x86의 레지스터와 동일한 플래그를 유지하지만 64비트로 확장
- 현재 프로세스가 어떤 상태인지 알 수 있음
CF | 부호 없는 수의 연산 결과가 할당된 비트의 범위를 넘을 경우 설정 |
OF | 부호 있는 수의 연산 결과가 할당된 비트의 범위를 넘을 경우 설정 |
SF | 연산의 결과가 음수일 경우 1로 설정 그 외에는 0 |
ZF | 연산의 결과가 0일 경우 1로 설정 그 외에는 0 |
어셈블리
PUSH, POP
: 스택에 값을 넣는 것을 PUSH, 스택에 있는 값을 가져오는 것을 POP
+) 오퍼랜드는 push eax 또는 push 1과 같이 1개만 있으면 됨.
MOV
: 단지 값을 넣는 역할을 함.
ex) MOV eax 1 : eax에 1을 넣는다.
LEA
: 주소를 가져오라.
※ MOV는 값을 가져오라 / LEA는 주소를 가져오라
ADD
: 두 오퍼랜드의 값을 더해서 첫 번째 오퍼랜드에 담음.
add eax, ecx
※ 메모리끼리는 연산을 할 수 없음.
ex) add [esp+8], [esp+4] → 존재 X / [esp+8]과 [esp+4]에 들어 있는 값을 레지스터인 eax와 ecx에 담고 그것끼리 add 연산을 해야 함.
SUB
: 뺄셈 명령어
sub eax, 3 => eax - 3 한 후 eax에 담음
INT
: 인터럽트를 일으키는 명령어 / 뒤에 오퍼랜드로 어떤 숫자가 나오냐에 따라 각기 다른 처리가 일어남.
※ 가장 많이 만나는 INT 3 명령어 : 옵코드가 0xCC인 DebugBreak( )
CALL
: 함수를 호출하는 명령어 / CALL 뒤에 오퍼랜드로 번지가 붙음.
→ 해당 번지를 호출하고 작업이 끝나면 CALL 다음 번지로 되돌아옴. ⇒ CALL로 호출된 코드 안에서는 반드시 RET를 만나기 때문
INC와 DEC
- INC : i++;
- DEC : i- -;
AND, OR, XOR
: dest와 src를 연산함.
- AND : dest와 src 비트가 모두 1이면 1, 아니면 0
- OR : dest와 src 비트 중 하나라도 1이면 1, 아니면 0
- XOR : dest와 src의 비트가 서로 다르면 1, 같으면 0ex) XOR EAX, EAX ⇒ EAX = 0 ⇒ 같은 오퍼랜드 전달 시 변수 0으로 초기화
- ※ XOR : dest와 src를 동일한 오퍼랜드로 처리 가능
NOP
: 아무것도 하지 말라는 명령어
※ 해킹이나 리버스 엔지니어링에서 가장 많이 사용됨
CMP, JMP
: 비교해서 점프하는 명령어
- CMP
mov rax, 0xA
mov rbx, 0xA
cmp rax, rbx ; ZF=1
(cmp op1, op2;) => op1과 op2를 비교
: 두 피연산자를 빼서 대소 비교 / 연산의 결과를 op1에 대입 X / 결과가 0이 되면 ZF플래그가 설정됨 → CPU는 이 플래그를 보고 두 값이 같았는지 판단 가능
스택 프레임
: ESP(스택 포인터)가 아닌 EBP(베이스 포인터) 레지스터를 이용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소 등에 접근하는 기법
- ESP의 값은 프로그램 안에서 수시로 변하기 때문에 EBP를 함수 시작 전에 저장하고 유지하면 안전하게 변수, 파라미터, 복귀 주소 등에 접근이 가능함.
스택 프레임의 어셈블리 코드
PUSH EBP
MOV EBP, ESP
...
...
MOV ESP, EBP
POP EBP
RETN
- 함수가 호출되고 나면 PUSH EBP를 통해 기존 EBP값(함수 호출 이전의 EBP 주소)을 저장
- MOV EBP, ESP를 통해 ESP 값을 EBP에 저장 → 함수 내부의 로컬 변수는 EBP-4 등으로, 파라미터는 EBP+4 등으로 접근이 가능해짐
- 함수의 기능을 다 수행하면 MOV ESP, EBP를 통해 스택을 정리
- POP EBP를 통해 함수 호출 이전의 EBP 값을 복원
- RETN을 통해 스택에 저장되었던 복귀 주소로 돌아감
함수 호출 규약(Calling Convention)
: 읽고 관리하기 쉬운 코드를 작성하기 위한 일종의 코딩 스타일 규약
- 리버싱을 할 때 반드시 필요한 것 중 하나는 각 함수의 역할을 파악하는 것.
- → 함수가 어떻게 생겼고 인자가 몇 개인지 등에 대한 정보를 추출해 낼 수 있어야 함.
<대표적인 함수 호출 규약>
- __cdecl
- __stdcall
- __fastcall
- __thiscall
→ 디스어셈블된 코드를 보고 이것이 어떤 콜링 컨벤션에 해당하는지 파악해야 함 ⇒ call 문을 보고 이 함수의 인자가 몇 개이고 어떤 용도로 쓰이는지를 분석하기 위함.
x64에서는__fastcall로 통일됨
__cdecl
....
main :
push 2
push 1
call calling.00401000
add esp, 8
- call calling.00401000 : 함수를 호출하는 곳
※ 항상 call 문의 다음 줄을 살펴서 스택을 정리하는 곳이 있는지 체크해야 함.
- add esp, 8 : 스택을 보정하는 코드 → 가 존재하면__cdecl 방식의 함수임. (함수 밖에서 스택을 보정함)
+) 해당 스택의 크기로 함수 파라미터의 개수까지 확인 가능
- 인자는 4바이트씩 계산 → 스택을 8바이트까지 끌어올리면 파리미터가 2개인 함수라고 파악 가능.
__stdcall
- add esp, 8을 사용한 코드 존재 X → main( ) 안에 sum( )을 사용한 뒤 어떠한 스택 처리 X
- sum( ) 본체 후반부의 리턴문에 그냥 retn이 아닌 retn 8을 사용 → 함수 안에서 스택 처리
- ※ retn 뒤에 별도의 숫자가 존재하지 X → 파라미터가 없는 경우
__fastcall
- sub esp, 0ch : 스택 공간을 확보한 후 edx 레지스터 사용
- 파라미터가 2개 이하일 경우, 인자를 push로 넣지 않고 ecx와 edx 레지스터 이용
- ⇒ 함수 호출 전에 edx와 ecx 레지스터에 값을 넣는 것이 보이면 __fastcall 규약의 함수임.
__thiscall
- C++ 클래스에서 이용되는 방법
- 현재 객체의 포인터를 ecx에 전달함.: CPU가 요청을 처리하는데 필요한 데이터를 일시적으로 저장하는 기억 장치
- 메모리에서 값을 받아와 저장하고, CPU에게 전달하는 역할을 함
'리버싱' 카테고리의 다른 글
시스템 스터디 드램핵 문제 (0) | 2024.07.01 |
---|---|
NASM 익히기 (0) | 2024.01.24 |
역공학 분석(분기문, 반복문, 함수호출) (0) | 2024.01.22 |
pwntools 사용법 (0) | 2024.01.22 |
크랙미(CrackMe) 풀이 _abexcm1.exe (0) | 2023.08.01 |