컴퓨터 구조와 명령어 집합 구조
컴퓨터 구조(Computer Architecture)
컴퓨터의 기본 설계. 서로 다른 부품들이 이 설계에 맞춰서 개발되고 조립되어 컴퓨터가 완성됨.
컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고, 구성하는 방법.
명령어 집합구조(Instruction Set Architecture, ISA)
CPU가 사용하는 명령어와 관련된 설계
CPU의 하드웨어적 설계 = 마이크로 아키텍처(Micro Architecture)
-> 정의된 명령어 집합을 효율적으로 처리할 수 있도록, CPU의 회로를 설계하는 분야.
인텔 x86-64 아키텍처 = 가장 널리 사용되는 ISA 중 하나.
<세부 분야>
- 기능 구조의 설계
폰 노이만 구조
하버드 구조
수정된 하버드 구조
- 명령어 집합구조
x86, x86-64
ARM
MIPS
AVR
- 마이크로 아키텍처
캐시 설계
파이프라이닝
슈퍼 스칼라
분기 예측
비순차적 명령어 처리
- 하드웨어 및 컴퓨팅 방법론
직접 메모리 접근
폰 노이만 구조
컴퓨터에 연산, 제어, 저장의 세 가지 핵심 기능이 필요하다. - 노이만
+ 중앙처리장치(Central Processing Unit, CPU) -> 연산과 제어
CPU = 컴퓨터의 두뇌
- 프로그램 연산 처리, 시스템 제어
- 구성
산술논리장치(Arithmetic Logic Unit, ALU) : 산술/논리 연산 처리
제어장치(Control Unit) : CPU 제어
레지스터(Register) : CPU에 필요한 데이터 저장
+ 기억장치(Memory) -> 저장
주기억장치 : 프로그램 실행과저에서 필요한 데이터 임시 저장.-> 램(Random-Access Memory, RAM)
보조기억장치 : 운영 체제, 프로그램 등과 같은 데이터 장기 보관 -> 하드드라이브(HDD), SSD(Solid State Drive)
+ 버스(bus) -> 장치 간 데이터 or 제어 신호 교환용 전자 통로.
컴퓨터 부품과 부품 사이 / 컴퓨터 간 신호 전송 통로.
데이터 버스(Data Bus) : 데이터 이동
주소 버스(Address Bus) : 주소 지정
제어 버스(Control Bus) : 읽기/쓰기 제어
그 외) 랜선, 데이터 전송 목적 소프트웨어, 프로토콜 등
CPU에 저장장치가 필요한 이유 : CPU는 굉장히 빠른 속도로 연산을 처리하므로 데이터 교환속도를 주기억장치 및 보조기억장치가 따라가지 못함. -> 병목현상 발생 가능.
데이터 교환 속도 획기적 단축 목적 -> 레지스터와 캐시 존재
*CPU가 필요한 데이터를 빠르게 공급하고 반출할 수 있어야 자신의 효율을 제대로 발휘할 수 있음.
명령어 집합 구조(Instruction Set Architecture, ISA)
CPU가 해석하는 명령어의 집합
프로그램의 코드는 기계어로 작성되어 있고, 실행하면 이 명령어들을 CPU가 읽고 처리함.
ISA는 IA-32, x86-64(x64), MIPS, AVR 등 다양하게 존재함. -> 모든 컴퓨터가 동일한 수준의 연산 능력을 요구하지 않으며, 컴퓨팅 환경도 다양하기 때문에 다양한 ISA가 개발되고 사용됨.
인텔 x86-64 : 고성능 프로세서 설계용. 많은 전력 소비, 발열 -> 데스크톱, 랩탑에 적합함. 점유율 압도적임.
ARM, MIPS, AVR 사용 : 드론, 임베디드 기기(공유기, 인공지능 스피커 등), 스마트폰 등은 발열 문제에 민감하고, 배터리 작동으로 많은 전력 공급이 어려움.
x86-64 아키텍처
인텔의 64비트 CPU 아키텍처. 인텔의 IA-32를 64비트 환경에서 사용할 수 있도록 확장한 것.
n 비트 아키텍처
64비트 아키텍처, 32비트 아키텍처
64, 32 = CPU가 한번에 처리 가능한 데이터의 크기 => WORD : CPU가 이해할 수 있는 데이터의 단위라는 의미.
32비트 아키텍처
ALU = 한번에 32비트 크기의 값 연산 가능
레지스터의 용량 및 각종 버스 대역폭 = 32비트 크기
-> 설계 상 32비트의 데이터까지만 처리 가능.
WORD가 크면 유리한 점
32비트 아키텍처는 가상메모리(Vitual Architecture) 크기가 작음. 최대 4기가 바이트. -> 전문 소프트웨어나 고사양 게임 돌리기엔 부족함.
가상 메모리 : CPU가 프로세스에게 제공하는 가상의 메모리 공간.
BUT 64비트 아키텍처는 이론상 16엑사 바이트의 가상메모리를 제공할 수 있음.
-> 완전한 사용이 불가능할 정도로 큰 크기.
가용한 메모리 자원이 부족해서 소프트웨어의 최고 성능을 낼 수 없다거나 실행이 불가능한 상황은 거의 발생하지 않음.
x86-64 아키텍처 : 레지스터
레지스터 : CPU 내부 저장장치, 산술 연산에 필요한 데이터 저장 or 주소 저장하고 참조 등 다양한 용도.
범용 레지스터(General Register)
주용도는 있는데 그 외 용도로도 사용 가능.
8바이트 저장 가능.
부호 없는 정수 기준 2^64 -1 까지 나타낼 수 있음.
이름 | 주용도 |
rax(accumulator register) | 함수의 반환 값 |
rbx (base register) | x64에서는 주된 용도 x |
rcx (counter register) | 반복문의 반복 횟수, 각종 연산의 시행 횟수 |
rdx (data register) | x64에서는 주된 용도 x |
rsi (source index) | 데이터를 옮길 때 원본을 가리키는 포인터 |
rdi (destination index) | 데이터를 옮길 때 목적지를 가리키는 포인터 |
rsp (stack pointer) | 사용중인 스택의 위치를 가리키는 포인터 |
rbp (stack base pointer) | 스택의 바닥을 가리키는 포인터 |
세그먼트 레지스터
cs, cc, ds, es, fs, gs -> 세그먼트 레지스터. 각각 16비트 크기.
- x64로 아키텍처가 확장되면서 용도에 큰 변화가 생긴 레지스터.
과거
IA-32, IA-16 : 세그먼트 레지스터를 이용해서 사용 가능한 물리 메모리의 크기를 키우려 함.
ex) cs:offset -> 실제 주소 = cs<<4 + offset
= 16비트에 범위에서 접근할 수 없는 주소 접근 가능.
당시에는 범용 레지스터 크기 작았지만 지금 x64에서는 사용 가능한 주소 영역이 굉장히 넓기 때문에 이런 용도로는 거의 사용 x.
현대
cs, ds, ss 레지스터 : 코드 영역과 데이터, 스택 메모리 영역을 가리킬 때 사용됨.
es, fs, gs : 운영체제 별로 용도 결정. -> 범용적 세그먼트 레지스터.
명령어 포인터 레지스터
프로그램의 기계어 코드 중 어느 부분의 코드를 실행할지 가리키는 역할.
rip, 크기 = 8바이트.
플래그 레지스터
프로세서의 현재 상태 저장.
REFLAGS, 64비트 크기.
깃발을 올리고 내리듯이 자신을 구성하는 여러 비트들로 CPU의 현재 상태 표현.
플래그 | 의미 |
CF (Carry Flag) | 부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 설정됨. |
ZF (Zero Flag) | 연산의 결과가 0일 경우 |
SF(Sign Flag) | 연산의 결과가 음수일 경우 |
OF(Overflow Flag) | 부호 있는 수의 연산 결과가 비트의 범위를 넘을 경우 |
a=3, b=5일 때 a에서 b를 빼면 연산의 결과가 음수이므로 SF 설정.
CPU는 SF를 통해 a가 b보다 작았음을 알 수 있음.
-> 실제 이런 원리를 이용해 대소 비교하는 어셈블리 명령어가 있음.
레지스터 호환
x86-64 아키텍처는 IA-32의 64비트 확장 아키텍처임. 호환 가능.
IA-32 레지스터들 : eax, ebx, ecx, edx, esi, edi, edp, ebp -> 확장된 레지스터의 하위 32비트.
예) eax = rax의 하위 32비트
IA-16 레지스터 : ax, bx, cx, dx, si, di, sp bp -> IA-32의 하위 16비트를 가리킴
또 다시 상위 8비트와 하위 8비트로 나뉘기도 함.
rax -> eax -> ax 순으로 내려간다고 보면 됨.
퀴즈
rax = 0x0123456789abcdef 일 때, eax, ax, ah, al의 값.
1) eax = 0x89abcdef
-> rax는 16바이트 즉, 64비트로 이루어짐. eax는 rax의 하위 32비트이므로 0123456789abcdef의 절반 중 뒷부분인
0x89abcdef이다.
2) ax = 0xcdef
3) ah = 0xcd
4) al = 0xef
rax에서 rbx를 뻈을 때, ZF 설정 -> rax == rbx이다.
-> ZF= 제로 플래그. 연산의 결과가 0일 경우 설정됨.
리눅스 프로세스의 메모리 구조
리눅스에서는 프로세스의 메모리를 크게 5가지의 세그먼트로 구분한다.
세그먼트(Segment) : 적재되는 데이터의 용도별로 메모리의 구획을 나눈 것.
- 코드 세그먼트
- 데이터 세그먼트
- BSS 세그먼트
- 힙 세그먼트
- 스택 세그먼트
장점- 운영체제가 메모리를 용도별로 나누면, 각 용도에 맞게 적절한 권한 부여 가능.권한 : 읽기, 쓰기, 실행 -> CPU는 메모리에 대해 권한이 부여된 행위만 할 수 있음.
ex) 데이터 세그먼트에는 프로그램이 실행되면서 사용하는 데이터가 적재된다. CPU는 이곳의 데이터를 읽을 수 있어야 하기 때문에 이 영역에는 읽기 권한이 부여되나, 실행 대상 데이터가 아니기 때문에 실행 권한은 부여되지 않는다.
코드 세그먼트(Code Segment)
실행 가능한 기계 코드가 위치하는 영역. 텍스트 세그먼트(Text Segment)라고도 불린다. 프로그램이 동작하려면 코드를 실행할 수 있어야 함 -> 읽기 권한, 실행 권한 부여. but 쓰기 권한 x : 공격자가 악의적인 코드를 삽입하기 쉬워지기 때문에.
정수 31337을 반환하는 main함수가 컴파일 되면 554889e5b8697a00005dc3라는 기계 코드로 변환
이 기계코드가 코드 세그먼트에 위치하게 된다.
데이터 세그먼트(Data Segment)
컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 위치하는 세그먼트.
읽기 권한 부여 -> CPU가 이 세그먼트의 데이터를 읽을 수 있어야 하기 때문에.
- 쓰기 가능 세그먼트
전역 변수와 같이 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 위치한다.
-> data 세그먼트
- 쓰기 불가능 세그먼트
프로그램이 실행되면서 값이 변하면 안되는 데이터들이 위치한다.
예) 전역으로 선언된 상수.
-> rodata(read-only-data) 세그먼트
str_ptr은 "readonly"라는 문자열을 가리키고 있는데, 이는 상수 문자열로 취급되어 rodata 세그먼트에 위치하며, str_ptr은 전역 변수로서 data 세그먼트에 위치한다.
BSS 세그먼트(BSS Segment, Block Started By Symbol Segment)
데이터 세그먼트와 달리, 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 메모리 영역.
ex) 개발자가 선언만 하고 초기화하지 않은 전역변수 등
BSS 세그먼트의 메모리 영역 : 프로그램이 시작될 때, 모두 0으로 값이 초기화된다.
-> C코드를 작성할 때, 초기화되지 않은 전역 변수의 값은 0이 된다.
읽기, 쓰기 권한 부여.
스택 세그먼트(Stack Segment)
프로세스의 스택이 위치하는 영역.
임시 변수(함수의 인자나 지역 변수)들이 실행 중에 저장됨.
단위 : 스택 프레임(Stack Frame)
함수 호출 시 생성, 반환 시 해제.
but 프로그램의 전체 실행 흐름은 사용자의 입력을 비롯한 여러 요인에 영향을 받음.
아래 코드에서 사용자가 choice에 뭘 입력하느냐에 따라 호출될 함수가 달라짐.
따라서, 프로세스 실행 시, 이게 사용할 스택 프레임 양을 미리 계산하는 것은 불가능.
- 프로세스 시작 시 작은 크기의 스택 세그먼트 먼저 할당해주고 부족해질 때마다 확장.
'아래로 자란다' -> 스택 확장 시 기존 주소보다 낮은 주소로 확장됨.
읽기, 쓰기 권한 부여 -> CPU가 자유롭게 값을 읽고 쓸 수 있어야 함.
힙 세그먼트(Heap Segment)
힙 데이터가 위치하는 세그먼트. 실행 중에 동적 할당 가능(스택처럼).
실행 중에 동적으로 사용되는 영역.
리눅스에서는 스택 세그먼트와 반대 방향으로 자란다.
C언어에서 malloc(), calloc() 등을 호출해서 할당받는 메모리가 위치한다.
읽기, 쓰기 권한 부여.
heap_data_ptr에 malloc()으로 동적 할당한 영역의 주소 대입. 이 영역에 31337이라는 값을 쓴다.
heap_data_ptr은 지역변수이므로 스택에 위치하며, malloc으로 할당받은 힙 세그먼트의 값을 가리킨다.
* 힙과 스택 세그먼트가 자라는 방향이 반대인 이유 *
두 세그먼트가 동일한 방향으로 자라고, 연속된 메모리 주소에 각각 할당된다고 가정.
기존의 힙 세그먼트 모두 사용 이후에는 이를 확장하는 과정에서 스택 세그먼트와 충돌하게 된다.
이를 해결하기 위해 리눅스에서는 스택을 메모리의 끝에 위치시키고 힙과 스택을 반대로 자라게 한다.
-> 힙과 스택이 메모리를 최대한 자유롭게 사용할 수 있고, 충돌 문제로부터도 비교적 자유롭게 됨.
1번 답 : BSS 세그먼트
풀이 : c는 초기화 되지 않은 전역변수이므로 BSS 세그먼트에 위치한다.
2번 답 : 데이터 세그먼트
풀이 : 상수 문자열 "d_str"을 저장하고 있는 전역변수 b.
3번 답 : 데이터 세그먼트
풀이 : a는 0xa로 초기화된 전역변수임.
4번 답 : 코드 세그먼트
풀이 : foo는 arg를 인자로 받는 함수 코드이므로 코드 세그먼트에 들어감.
5번 답 : 힙 세그먼트
풀이 : malloc으로 할당 된 값을 가리키고 있음. malloc으로 할당된 값은 힙 세그먼트에 저장됨.
6번 답 : rodata 세그먼트
풀이 : 프로그램 실행 하면서 값이 변하면 안되는 데이터들은 rodata에 저장됨.
7번 답 : 스택 세그먼트
풀이 : 지역변수는 스택에 저장됨.
x86 Assembly
해커의 언어 : 어셈블리
컴퓨터 세계에서 통용되는 언어 기계어(Machine Code).
해커는 컴퓨터 언어 지식을 습득해야함. -> 컴퓨터 언어로 작성된 소프트웨어에서 취약점을 발견해야하기 때문.
기계어의 단점 : 0과 1로만 구성돼있어서 인간이 이해하기 어려움.
David Wheeler 어셈블리 언어와 어셈블러 고안.
어셈블러 = 일종의 통역사. 어셈블리어 코드를 기계어 코드로 치환해줌.
기계어가 여러 종류라면 어셈블리어도 여러 종류여야 함.
CPU에 사용되는 ISA는 종류가 매우 다양하고 이들의 종류만큼 많은 수의 어셈블리어가 존재한다.
해당 커리큘럼에서는 x64 아키텍처를 대상으로 하기 때문에 x64 어셈블리어를 공부.
x64 어셈블리 언어
기본 구조
명령어(Operation Code) + 피연산자(Operand)
명령어는 동사 역할, 피연산자는 목적어 역할
mov eax, 3
opecode = 대입해라
operand1 = eax에operand2 = 3을
주요 명령어 21가지
산술 연산(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 |
피연산자 종류
- 상수(Immediate Value)
- 레지스터(Register)
- 메모리(Memory) -> []으로 둘러싸인 것으로 표현됨.
TYPE PTR [메모리 피연산자]
*크기 지정자 TYPE PTR
메모리 피연산자의 예
TYPE에는 BYTE, WORD, DWORD, QWORD가 올 수 있음.
각각 1, 2, 4, 8 바이트.
QWORD PTR [0x8048000] | 0x8048000의 데이터를 8바이트만큼 참조 |
DWORD PTR [0x8048000] | 0x8048000의 데이터를 4바이트만큼 참조 |
WORD PTR [rax] | rax가 가리키는 주소에서 데이터를 2바이트 만큼 참조 |
Opcode: 데이터 이동
어떤 값을 레지스터나 메모리에 올리도록 지시함.
mov dst, src : src에 들어있는 값을 dst에 대입
mov rdi, rsi | rsi의 값을 rdi에 대입 |
mov QWORD PTR[rdi], rsi | rsi의 값을 rdi가 가리키는 주소에 대입 |
mov QWORD PTR[rdi+8*rcx], rsi | rsi의 값을 rdi+8*rcx가 가리키는 주소에 대입 |
lea dst, src : src의 유효 주소(Effective Address, EA)를 dst에 저장함.
lea rsi, [rbx+8*rcx] => rbx+8*rcx를 rsi에 대입
Opcode: 산술 연산
add = 더하기, sub = 뺴기, inc = 1 증가, dec = 1 감소
add dst, src : dst에 src의 값을 더함.
add eax, 3 | eax += 3 |
add ax, WORD PTR[rdi] | ax += *(WORD *)rdi |
sub dst, src : dst에서 src의 값을 뺌.
inc op : op += 1
dec op : op -= 1\
Opcode: 논리 연산
and, or, xor, neg 등
and dst, src : dst와 src의 비트가 모두 1이면 1, 아니면 0
or dst, src : dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
계산방식은 이진수로 바꾸어서 and 연산, or 연산 하고 다시 16진수로 바꾼다.
xor = 서로 다르면 1, 같으면 0
[Register]
eax = 0xffffffff
ebx = 0xcafebabe -> 각 문자를 이진수 형태로 바꿔서 xor 연산을 함.
[Code]
xor eax, ebx
[Result]
eax = 0x35014541
not op : op의 비트 전부 반전
[Register]
eax = 0xffffffff
[Code]
not eax
[Result]
eax = 0x00000000
-> 비트가 이진수로 바꾸면 전부 1이니까 전부 반전 시켜서 0만 나옴
XOR 연산을 동일한 값으로 두 번 실행할 경우, 원래 값으로 돌아감.
-> XOR Cipher : 이런 성격을 이용한 단순 암호.
비교 : 두 피연산자의 값을 비교하고, 플래그 설정.
cmp op1, op2 : op1과 op2를 비교
두 피연산자를 빼서 대소를 비교하고, 연산의 결과는 op1에 대입하지 않음.
예를 들어 같은 두 수끼리 빼면 값이 0이니까 플래그가 ZF(Zero Flag)가 되는데, 이후에 CPU는 이 플래그를 보고 두 값이 같았는지 판단할 수 있음.
[Code]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
test op1, op2 : 두 피연산자에 AND 비트연산을 취함.
역시나 연산의 결과는 op1에 대입하지 않음.
[Code]
1: xor rax, rax
2: test rax, rax ; ZF=1
위 코드처럼 rax로 xor 연산 이후에 test를 하면 결과가 0이므로 ZF 플래그 설정됨.
-> rax끼리 xor 하면 비트가 같으니까 전부 0이 되고, test에서 and 연산을 할 경우에도 0이 나오기 때문에.
분기
rip(명령어 포인터 레지스터)을 이동시켜 실행 흐름을 바꿈.
jmp addr : addr로 rip을 이동시킴
[Code]
1: xor rax, rax
2: jmp 1 ; jump to 1
je addr : 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
[Code]
1: mov rax, 0xcafrbabe
2: mov rbx, 0xcafebabe
3: cmp rax, rbx ; rax == rbx
4: je 1; jump to 1 -> rax와 rbx가 같으면 1번 코드로 점프한다.
jg addr : 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)
[Code]
1: mov rax, 0x31337
2: mov rbx, 0x13337
3: cmp rax, rbx ; rax > rbx
4: jg 1; jump to 1 -> rax가 rbx보다 크면 1번 코드로 점프한다.
Opcode : 스택 (LIFO, Last In First Out)
push val : val을 스택 최상단에 쌓음
0x31337을 스택 최상단에 쌓음. rsp는 사용중인 스택의 위치를 가리키는 포인터임.
pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
pop rax 명령어를 통해서 스택 최상단에 있던 0x31337 값이 스택이란 바구니 안에서 꺼내지고 rax로 대입됨.
rsp가 가리키는 주소는 0x31337을 저장하고 있던 주소 하나 아래에 있던 0x7fffffffc400으로 바뀜.
Opcode : 프로시저
프로시저 (Procedure) : 특정 기능을 수행하는 코드 조각
- 반복되는 연산을 프로시저 호출로 대체, 전체 코드 크기를 줄일 수 있음.
- 기능별로 코드 조각에 이름을 붙일 수 있어 코드의 가독성 좋아짐.
- 호출(Call) : 프로시저를 부름.
- 반환(Return) : 프로시저에서 돌아오는 것.
프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(return address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동시킴
call addr : addr에 위치한 프로시저 호출
rip는 어느 부분의 코드를 실행할지 가리키는 역할. rip가 0x40000에서 0x401000으로 바뀜.
0x401000 주소를 호출해서 그 부분의 코드를 실행하는 것 같음.
이 [Code] 부분에서 0x40000, 0x40005 이런 부분은 주소인것 같은데 무슨 주소인걸까?
rip가 어느 코드를 실행할지 가리키는 역할이랬는데 push rbp 명령이 들어왔으니까 rip가 바뀐게 아닐까 하는 생각을 해본다.
leave : 스택 프레임 정리
연산
mov rsp, rbp
pop rbp
rsp는 사용 중인 스택의 위치, rbp는 스택의 바닥을 가리킴. leave 명령어로 현재 사용 중인 스택의 위치를 가리키게 하고, 스택의 바닥을 가리키도록 스택 프레임을 정리한 것으로 보임.
* 스택 프레임 *
스택 영역을 아무런 구분 없이 사용하게 된다면, 서로 다른 두 함수가 같은 메모리 영역을 사용할 수 있게 된다.
a라는 함수가 b라는 함수를 호출하는데 둘이 같은 스택 영역을 사용한다면, b에서 a의 지역 변수를 모두 오염시킬 수 있고, b에서 반환한 뒤 a는 정상적인 연산을 수행할 수 없게 된다.
따라서 함수 별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 스택 프레임이 사용된다.
ret : return address로 반환
ip(instruction pointer) : 다음 수행 명령어의 위치를 가리킴.
sp(stack pointer) : 현재 스택 최상단 스택의 위치를 가리킴.
bp(base pointer) : 스택의 상대주소 계산용.
sp는 계속 움직이지만 bp로 주소를 잡아주면 아무리 sp가 움직여도 원하는 주소 값을 bp에
저장해 놓음.
기존의 스택 프레임을 저장하는 이유는 sp가 계속 바뀌어도 원하는 주소값을 저장해놓기 위해서?
새로운 스택 프레임을 만들기 위해 mov rbp rsp를 해준다.
스택에서 0x30 바이트만큼 공간을 확보한다.
rsp가 가리키는 주소에 0x3 대입. 아래 이미지를 보면 Stack에서 rsp가 가리키는 주소에 0x3이 들어간걸 볼 수 있음.
실행 코드가 call func에서 mov esi, eax로 바뀜.
Opcode : 시스템 콜
컴퓨터 자원의 효율적인 사용을 위해, 그리고 사용자에게 편리한 경험을 제공하기 위해, 내부적으로 매우 복잡한 동작을 하는 운영체제.
연결된 모든 하드웨어 및 소프트웨어에 접근 가능하고, 이들을 제어할 수도 있다.
해킹으로부터 이 권한을 보호하기 위해 커널 모드와 유저 모드로 권한을 나눈다.
커널 모드 : 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한.
파일시스템, 입출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행된다.
시스템의 모든 부분을 제어할 수 있다.
유저 모드 : 운영체제가 사용자에게 부여하는 권한.
유튜브 시청, 게임, 프로그래밍, 리눅스에서 루트 권한으로 사용자 추가, 패키지 다운로드 행위.
해킹이 발생해도 유저모드 권한 밖에 획득 못하기 때문에 커널의 권한 보호 가능함.
시스템 콜(System Call, syscall)
유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용됨.
ex) cat flag
flag라는 파일을 읽어서 출력해주려면 파일시스템에 접근할 수 있어야 함. 커널의 도움 필요.
도움 요청 = 시스템 콜. 커널이 요청 동작을 수행해서 유저에게 결과를 반환함.
syscall은 함수이다. 필요한 기능과 인자에 대한 정보를 레지스터로 전달하면, 커널이 이를 읽어서 요청을 처리한다.
rax가 0x1일 때, 커널에 write 시스템콜 요청.
rdi, rst, rdx가 0x1, 0x401000, 0xb이므로 커널은 write(0x1, 0x401000, 0xb)를 수행하게 된다.
-> write 함수 안에 들어가는 인자가 저 셋인것같음.
write 함수 각 인자는 출력 스트림, 출력 버퍼, 출력 길이를 나타냄.
0x1 = stdout, 일반적으로 화면을 의미함.
0x401000 = Hello World가 저장됨.
0xb = 길이 지정(10진수로 11)
rld는 0x401008에 들어가 있는데 왜 0x401000에 저장돼있다고 하는 걸까.
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
read | 0x00 | unsigned int fd | char *buf | size_t count |
write | 0x01 | unsigned int fd | const char* buf | size_t count |
open | 0x02 | const char *filename | int flags | umode_t mode |
close | 0x03 | unsigned int fd | ||
mprotect | 0x0a | unsigned long start | size_t len | unsigned long prot |
connect | 0x2a | int sockfd | struct sockaddr * addr | int addrlen |
execve | 0x3b | const char *filename | const char *const *argv |
const chat *const *envp
|
'Dreamhack' 카테고리의 다른 글
웹 해킹 Stage 2 (0) | 2022.07.22 |
---|---|
시스템 해킹 Stage 3 (0) | 2022.07.19 |
시스템 해킹 Stage 1 (0) | 2022.07.18 |
리버싱 09 - 혼자 실습 (0) | 2022.07.12 |
리버싱 08 - Patch (0) | 2022.07.12 |