본문 바로가기

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

[2023.05.20] '드림핵-시스템 해킹' 강의 수강

드림핵 시스템 해킹 강의 STAGE 1,2 를 수강하였다.


STAGE 1 - System Hacking Introduction

 

<리눅스 환경 구축>

 

본 로드맵은 Ubuntu 22.04(x86-64)를 기반으로 작성되었다. 해당 운영체제 사용하고 있지 않으면 가상 환경을 구축해야 원환할 실습이 가능하다.---> 이를 위해 가상 머신을 사용해야 함!

 

윈도우에서는 VMware, VirtualBox 또는 WSL2를 이용하여 환경 구축 가능.

 

맥에서는 VMware, VirtualBox나 맥에 최적화된 Parallels Desktop을 통해 환경 구축 가능.

 

STAGE 2 - Background - Computer Science

<컴퓨터 구조>

 

컴퓨터 구조(Computer Architecture)

  • 컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고, 이들을 구성하는 방법.
  • 컴퓨터의 기능 구조에 대한 설계, 명령어 집합구조, 마이크로 아키텍처, 그리고 기타 하드웨어 및 컴퓨팅 방법에 대한 설계 등이 포함됨
    • 컴퓨터의 기능 구조에 대한 설계 : 컴퓨터가 연산을 효율적으로 하기 위해 어떤 기능들이 컴퓨터에 필요한지 고민하고 설계하는 분야. 대표적으로 폰 노이만 구조, 하버드 구조, 수정된 하버드 구조가 있음.
      • 폰 노이만 구조 : 근대의 컴퓨터는 연산과 제어를 위해 중앙처리장치(CPU)를, 저장을 위해 기억장치(memory)를, 장치간에 데이터나 제어 신호를 교환할 수 있도록 버스(bus)라는 전자 통로를 사용
      • 중앙처리장치(CPU) : 프로그램의 연산을 처리하고 시스템을 관리하는 컴퓨터의 두뇌 역할로, 산술논리장치, 제어장치, 레지스터 등으로 구성됨
      • 기억장치(memory) : 컴퓨터가 동작하는데 필요한 여러 데이터를 저장하기 위해 사용되며 용도에 따라 주기억장치와 보조기억장치로 분류됨
      • 버스(bus) : 컴퓨터 부품과 부품 사이 또는 컴퓨터와 컴퓨터 사이에 신호를 전송하는 통로이며 대표적으로 데이터 버스, 주소 버스, 제어 버스가 있음
    • CPU의 명령어에 대한 설계 : 명령어 집합구조라고 불리며, CPU가 처리해야하는 명령어를 설계하는 분야
      • 명령어 집합 구조(ISA) : CPU가 해석하는 명령어의 집합을 의미함. 모든 컴퓨터가 동일한 수준의 연산 능력을 요구하지 않으며, 컴퓨팅 환경도 다양하기 때문에 ISA는 IA-32 , x86-64(x64), MIPS, AVR 등 다양하게 존재
    • CPU의 하드웨어적 설계 : 마이크로 아키텍처라고 불리며 정의된 명령어 집합을 효율적으로 처리할 수 있도록, CPU의 회로를 설계하는 분야

x86-64 아키텍처

 

x86-64 아키텍처 : 인텔의 64비트 CPU 아키텍처

n 비트 아키텍처에서의 n은 CPU가 한번에 처리할 수 있는 데이터의 크기. 이를 컴퓨터과학에서는 CPU가 이해할 수 있는 데이터의 단위라는 의미에서 WORD라고 부름

 

x64 아키텍처에는 범용 레지스터세그먼트 레지스터명령어 포인터 레지스터플래그 레지스터가 존재

 

범용 레지스터 

  • 주용도는 있으나 그외의 다양한 용도로 사용될 수 있는 레지스터
이름 주용도
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) 스택의 바닥을 가리키는 포인터

세그먼트 레지스터

  • x64 아키텍처에는 cs, ss, ds, es, fs, gs 총 6가지 세그먼트 레지스터가 존재
  • 각 레지스터의 크기는 16비트

명령어 포인터 레지스터

  • 프로그램은 일련의 기계어 코드들로 이루어져 있는데 이 중에서 CPU가 어느 부분의 코드를 실행할지 가리키는 역할

플래그 레지스터

  • 프로세서의 현재 상태를 저장하고 있는 레지스터
  • x64 아키텍처에서는 RFLAGS라고 불리는 64비트 크기의 플래그 레지스터가 존재
  • 자신을 구성하는 여러 비트들로 CPU의 현재 상태를 표현

레지스터 호환

IA-32에서 CPU의 레지스터들은 32비트 크기를 가지며, 이들의 명칭은 각각 eax, ebx, ecx, edx, esi, edi, esp, ebp 인데, 호환성을 위해 이 레지스터들은 x86-64에서도 그대로 사용이 가능

 rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp가 이들의 확장된 형태이며, eax, ebx 등은 확장된 레지스터의 하위 32비트를 가르킴

마찬가지로 과거 16비트 아키텍처인 IA-16과의 호환을 위해 ax, bx, cx, dx, si, di, sp, bp는 eax, ebxecxedx, esi, edi, esp ,ebp의 하위 16비트를 가르킴

<리눅스 메모리 구조>

 

리눅스에서는 프로세스의 메모리를 크게 5가지의 세그먼트(Segment)로 구분.

 

세그먼트 : 적재되는 데이터의 용도별로 메모리의 구획을 나눈 것. 크게 코드 세그먼트, 데이터 세그먼트, BSS 세그먼트, 힙 세그먼트, 그리고 스택 세그먼트로 구분

 

코드 세그먼트

  • 실행 가능한 기계 코드가 위치하는 영역
  • 텍스트 세그먼트라고도 불림
  • 읽기 권한과 실행 권한 부여됨 

데이터 세그먼트

  • 컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 위치
  • 읽기 권한 부여됨
  • 쓰기가 가능한 세그먼트와 쓰기가 불가능한 세그먼트로 다시 분류됨.
    • data 세그먼트(쓰기가 가능한 세그먼트) : 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 위치(ex. 전역변수)
    • rodata 세그먼트(쓰기가 불가능한 세그먼트) : 프로그램이 실행되면서 값이 변하면 안되는 데이터들이 위치(ex. 전역으로 선언된 상수)

BSS 세그먼트

  • 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 메모리 영역
  • 개발자가 선언만 하고 초기화하지 않은 전역변수 등이 포함
  • BSS 세그먼트의 메모리 영역은 프로그램이 시작될 때, 모두 0으로 값이 초기화됨.
  • 읽기 권한과 쓰기 권한이 부여됨

스택 세그먼트

  • 프로세스의 스택이 위치하는 영역
  • 함수의 인자나 지역 변수와 같은 임시 변수들이 실행중에 여기에 저장됨.
  • 읽기 권한과 쓰기 권한이 부여됨
  • 스택 세그먼트는 스택 프레임 단위로 사용

힙 세그먼트

  • 힙 데이터가 위치하는 세그먼트
  • 스택과 마찬가지로 실행 중에 동적으로 할당 가능
  • 리눅스에서는 스택 세그먼트와 반대 방향으로 자람.
  • C언어에서 malloc(), calloc() 등을 호출해서 할당받는 메모리가 위치함
  • 읽기 권한과 쓰기 권한 부여됨

 

어셈블리 언어 : 컴퓨터의 기계어와 치환되는 언어

x64 어셈블리 언어 : 명령어 피연산자로 구성됨.

인텔의 x64에는 매우 많은 명령어가 존재

명령코드  
데이터 이동(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

x86-64 어셈블리 명령어

데이터 이동 명령어 : 어떤 값을 레지스터나 메모리에 옮기도록 지시

산술 연산 명령어 : 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시

논리 연산 명령어 : and, or, xor, neg 등의 비트 연산을 지시함. 이 연산은 비트 단위로 이루어짐

비교 명령어 : 두 피연산자의 값을 비교하고, 플래그를 설정

분기 명령어는 rip를 이동시켜 실행 흐름을 바꿈

 

피연산자에는 총 3가지 종류가 올 수 있음

  • 상수(Immediate Value)
  • 레지스터(Register)
  • 메모리(Memory)

메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자 TYPE PTR이 추가될 수 있다.여기서 타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정

 

Opcode: 스택

x64 아키텍쳐에서는 다음의 명령어로 스택을 조작 가능

push val : val을 스택 최상단에 쌓음

pop reg : 스택 최상단의 값을 꺼내서 reg에 대입

 

Opcode : 프로시저

프로시저(Procedure) : 컴퓨터 과학에서는 프로시저를 특정 기능을 수행하는 코드 조각을 말함

호출(Call): 프로시저를 부르는 행위

반환(Return) : 프로시저에서 돌아오는 것

프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(return address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동시킴

x64어셈블리언어에서 프로시저의 호출과 반환을 위한 명령어에는 call, leave, ret 가 있음.

 

Opcode: 시스템 콜

운영체제는 연결된 모든 하드웨어 및 소프트웨어에 접근 가능하므로 해킹으로부터 막강한 권한을 보호하기 위해 커널 모드와 유저 모드로 권한을 나눔

커널 모드는 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한. 저수준의 작업은 사용자 모르게 커널모드에서 진행됨

유저 모드는 운영체제가 사용자에게 부여하는 권한.

시스템 콜은 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용된다.

x64아키텍처에서는 시스템콜을 위해 syscall 명령어 존재