본문 바로가기

3. Pwnable (포너블)/2) 개념 정리

[2025.03.21] 1주차 활동 _ Pwnabless

참고 강의: 드림핵(DreamHack) - System Hacking

 

System Hacking

 

dreamhack.io


STAGE 1.   System  Hacking  Introduction

[환경 구축]

해당 강의는 Ubuntu 22.04(x86-64), x86-64 아키텍처를 기반으로 학습할 예정

VMware, Ubuntu 22.04 ISO 다운 -> 리눅스 가상환경 구축

[용어]

가상 머신 컴퓨터를 에뮬레이팅(emulating, 모방)한 것
호스트(Host) 가상 머신을 작동시키는 컴퓨터
게스트(Guest) 가상 머신 안에서 작동하는 컴퓨터

* 대표적인 가상화 소프트웨어: VMware, VirtualBox, Parallels, QEMU 등


STAGE 2.   Background  -  Computer  Science

시스템 해킹 기술은 컴퓨터 과학에 뿌리를 두고 있음

따라서 해킹 기술뿐만 아니라, 컴퓨터 과학의 전반을 이해하기 위한 노력이 필요

[컴퓨터 구조(Computer  Architecture)]

컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어 기능 고안 및 구성하는 방법

 

CPU 연산(컴퓨터의 작동에 핵심) 처리
저장장치 데이터 저장
GPU 그래픽 데이터 처리
랜카드 네트워크 통신 처리
사운드 카드 소리 데이터 처리

▶ 서로 다른 부품들이 모여 컴퓨터라는 하나의 기계로 작동

컴퓨터 구조는 아래를 모두 포함

1. 컴퓨터의 기능 구조에 대한 설계
2. 명령어 집합구조
3. 마이크로 아키텍처
4. 기타 하드웨어 및 컴퓨팅 방법에 대한 설계 등

[명령어 집합 구조(Instruction  Set  Architecture,  ISA)]

CPU가 사용하는 명령어와 관련된 설계 구조 (CPU가 해석하는 명령어의 집합)

 

가장 널리 사용되는 ISA

▶ x86-64 아키텍처

 

(그 외 ISA: ARM, MIPS, AVR, x86 등)

※ 다양한 ISA가 개발되고 사용되는 이유?
모든 컴퓨터가 동일한 수준의 연산 능력을 요구하지 않으며, 컴퓨팅 환경도 다양하기 때문

[마이크로 아키텍처(Micro  Architecture)]

명령어 집합을 효율적으로 처리할 수 있도록 CPU의 회로(하드웨어적) 설계

[폰 노이만 구조]

컴퓨터의 3가지 핵심 기능

• 연산

• 제어

• 저장

 

중앙처리장치(CPU) 연산, 제어 기능 수행
기억장치(memory) 저장 기능 수행
버스(Bus) 장치간에 데이터나 제어 신호 교환을 위한 전자 통로

 

중앙처리장치(Central Processing Unit, CPU)

 

프로세스의 코드를 불러오고, 실행하고, 결과를 저장하는 일련의 모든 과정이 CPU에서 발생

 

구성 요소 내용
산술논리장치(ALU) 산술/논리 연산 처리
제어장치(Control Unit) CPU 제어
레지스터(Register) CPU에 필요한 데이터를 저장 및 사용할 때 이용하는 보관소

 

기억장치(memory)

 

주기억장치 프로그램 실행에 필요한 데이터 임시 저장 (e.g., 램(RAM))
보조기억장치 운영체제, 프로그램 등의 데이터를 장기간 보관 (e.g., 하드 드라이브(HDD), SSD)

 

버스(Bus)

 

컴퓨터-부품, 컴퓨터-컴퓨터 사이의 신호 전송 통로

 

데이터 버스
(Data Bus)
데이터 이동
주소 버스
(Address Bus)
주소 지정
제어 버스
(Control Bus)
읽기/쓰기 제어

[x86-64 아키텍처]

대다수 개인용 컴퓨터는 x86-64 아키텍처 기반의 CPU를 탑재

 

워드(WORD)

CPU가 이해할 수 있는 데이터의 단위 (크기는 CPU가 어떻게 설계됐느냐에 따라 상이)

WORD가 크면 유리한 점

16엑사 바이트(=16,777,216 테라바이트)의 가상메모리를 제공
→ 가용한 메모리 자원이 부족해서 소프트웨어 실행 불가능한 상황이 발생하지 않음

[레지스터]

산술 연산에 필요한 데이터와 주소를 저장 및 참조하는 등 다양한 용도로 사용

 

범용 레지스터(General Register)

 

주 용도 외에 다양한 용도로 사용 가능

 

세그먼트 레지스터(Segment Register)
  과거 → 메모리 세그멘테이션, 가용 메모리 공간 확장 목적 사용
  현재 → 메모리 보호를 위해 사용

 

  • cs, ss, ds, es, fs, gs 존재 (각 레지스터의 크기 = 16비트)
  • x64에서는 cs, ds, ss → 코드 영역과 데이터, 스택 메모리 영역 가리킬 때 사용
  • 나머지 → 운영체제 별로 용도 결정
명령어 포인터 레지스터(Instruction Pointer Register, IP)

 

CPU가 실행해야 할 코드 지칭

 

  • x64에서는 rip 사용
플래그 레지스터(Flag Register)

 

CPU의 상태 저장

 

  • x64에서는 REFLAGS 존재 (64비트 중 20여개의 비트만 사용)
※ CPU의 동작과 메모리 사이에는 밀접한 연관이 존재

IF, 공격자가 메모리를 악의적으로 조작한다면?
조작된 메모리 값에 의해 CPU도 잘못된 동작 수행 가능성 有 → 메모리 오염(Memory Corruption)
메모리 오염을 유발하는 취약점 → 메모리 오염 취약점

 

 메모리 오염 취약점 종류

1. Stack Buffer Overflow

2. Format String Bug

3. Use After Free

4. Double Free Bug

[리눅스 메모리 구조(Memory  Layout)]

리눅스에서의 프로세스 메모리는 세그먼트(Segment)로 구분.

 

세그먼트

적재되는 데이터의 용도별로 메모리의 구획을 나눈 것

→ 각 용도에 맞게 적절한 권한 부여 가능

 

권한

읽기 권한, 쓰기 권한, 실행 권한

 

세그먼트 역할 일반적인 권한 사용 예
코드 세그먼트 실행 가능한 코드가 저장된 영역 읽기, 실행 main() 등의 함수 코드
데이터 세그먼트 초기화된 전역 변수, 상수가 위치하는 영역 읽기, 쓰기
(data 세그먼트)
초기화된 전역 변수
읽기
(rodata(read-only data)
세그먼트)
초기화된 전역 상수
BSS 세그먼트 초기화되지 않은 데이터가 위치하는 영역 읽기, 쓰기 초기화되지 않은 전역 변수
스택 세그먼트 임시 변수가 저장되는 영역 읽기, 쓰기 지역 변수, 함수의 인자 등
힙 세그먼트 실행 중에 동적으로 사용되는 영역 읽기, 쓰기 malloc(), calloc() 등으로 할당받은 메모리

 

✓ 코드 세그먼트의 쓰기 권한이 없는 이유?
공격자가 악의적인 코드를 삽입하기 쉬워지기 때문

■ BSS 세그먼트
프로그램 시작 시 값이 모두 0으로 초기화
→ C코드 작성 시 초기화되지 않은 전역 변수의 값 = 0

 스택 세그먼트
스택 프레임(Strack Frame) 단위 사용
스택이 확장될 때 기존 주소보다 낮은 주소로 확장되기 때문에 ‘아래로 자란다’ 표현 사용
→ 함수 호출 시 생성, 반환 시 해제

 힙 세그먼트
스택 세그먼트와 반대 방향으로 할당

※ 힙과 스택 세그먼트가 자라는 방향이 반대인 이유?
두 세그먼트가 동일한 방향으로 자라게 되는 경우, 힙 세그먼트를 모두 사용 후 확장 과정에서 스택 세그먼트와 충돌하기 때문

So, 스택을 메모리 끝에 위치시키고, 힙과 스택을 반대로 자라게 함
힙과 스택은 메모리를 최대한 자유롭게 사용 + 충돌 문제 해결 가능

[어셈블리어]

컴퓨터의 공통 언어 = 기계어(Machine Code)

 

시스템 해커는 컴퓨터의 언어로 작성된 소프트웨어에서 취약점을 발견해야 함

 

컴퓨터의 언어인 기계어는 0과 1로만 구성돼 있어서, 인간으로서는 이해하기가 매우 어려움

 

이를 해결하기 위해 David Wheeler는 EDSAC을 개발하면서 어셈블리 언어(Assembly Language)와 어셈블러(Assembler) 라는 것을 고안함

 

어셈블러 작동 방식

인간언어 어셈블리 기계어

 

역어셈블러 작동 방식

인간언어 ←  어셈블리 ←  기계어 

 

x64 어셈블리어

 

문법 구조
명령어(동사) 피연산자(목적어)
mov eax, 3
대입해라 eax에 3을

 

피연산자

 

[]으로 표현

 

■ 피연산자 종류

 상수

 레지스터

메모리

 

■ 앞에 크기 지정자인 TYPE PTR 추가 가능

BYTE: 1바이트

WORD: 2바이트

DWORD: 4바이트

QWORD: 8바이트

 

(e.g., QWORD PTR[0x8048000] -> 0x8048000의 데이터를 8바이트 만큼 참조)

 

명령어

 

명령코드
데이터 이동 mov dst, src src 값을 dst에 대입
lea dst, src src의 유효 주소를 dst에 대입
산술 연산 add eax, 3 eax += 3
sub eax, 3 eax -= 3
inc eax eax += 1
dec eax eax -= 1
논리 연산 AND 비트가 모두 1이면 1, 아니면 0
OR 비트 중 하나라도 1이면 1, 아니면 0
XOR 비트가 서로 다르면 1, 같으면 0
NOT 비트 전부 반전
비교 cmp op1, op2 op1에서 op2를 빼서 비교 후 플래그 설정
test op1, op2 op1과 op2에 AND 연산 후 플래그 설정
분기 jmp addr addr로 rip 이동 (jump)
je addr 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
jg add 직전에 비교한 두 피연산자 중 전자가 더 크면 점프 (jump if greater)
스택 push val rsp를 8만큼 빼고, 스택 최상단에 val 쌓음
pop reg 스택 최상단의 값을 reg에 대입 후 rsp를 8만큼 더함
프로시저 call addr addr의 프로시저 호출
leave 프로시저가 반환되기 전, 스택 프레임 정리
ret 함수에서 반환해 원래 실행하던 코드로 돌아옴
시스템 콜 syscall 유저 모드에서 커널 모드의 시스템 소프트웨어에게 동작 요청

 

* 스택

- LIFO (Last In, First Out, 후입선출) 방식으로 동작

- x86 아키텍처에서는 데이터를 추가할 때마다 메모리 주소가 감소하며, 데이터를 제거하면 다시 증가 하는 특성

 

* rsp (스택 포인터 레지스터)

- 항상 스택의 가장 위(Top)를 가리킴

 

✓ 시스템 콜(System call, syscall)

 사용자 모드(User Mode)
운영체제가 사용자에게 부여하는 권한
→ 접근할 수 있는 메모리 영역 및 권한이 한정적 + 하드웨어에 직접적인 접근 불가능

 커널 모드(Kernel Mode)
운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한
→ 모든 메모리 영역 및 하드웨어에 접근 가능
 
※ 모드를 구분하는 이유?
사용자가 권한 없이 운영체제 내부의 데이터를 읽거나 쓰는 경우 시스템에 악의적인 영향을 끼칠 수 있기 때문