드림핵 System Hacking 로드맵 STAGE 1~4

Bae YuSeon·2023년 3월 16일
0

SystemHacking

목록 보기
1/1
post-thumbnail

STAGE 1 System Hacking Introduction

실습 환경

STAGE 2 Background - Computer Science

Linux Memory Layout

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

1. 코드 세그먼트

  • 실행 가능한 기계 코드가 위치하는 영역으로 텍스트 세그먼트(Text Segment)라고도 불린다.
  • 프로그램이 동작하려면 코드를 실행할 수 있어야 하므로 이 세그먼트에는 읽기 권한과 실행 권한이 부여 (악의적인 코드 삽입 방지를 위해 대부분의 현대 운영체제는 이 세그먼트의 쓰기권한 제거)

2. 데이터 세그먼트

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

3. BSS 세그먼트

  • 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 메모리 영역
  • 개발자가 선언만 하고 초기화하지 않은 전역변수 등이 포함
  • 이 세그먼트의 메모리 영역은 프로그램이 시작될 때, 모두 0으로 값이 초기화
    → C 코드를 작성할 때, 초기화되지 않은 전역 변수의 값은 0
  • C 코드를 작성할 때, 초기화되지 않은 전역 변수의 값은 0

4. 스택 세그먼트

  • 프로세스의 스택이 위치하는 영역
  • 함수의 인자나 지역 변수와 같은 임시 변수들이 실행중에 여기에 저장
  • 스택 세그먼트는 스택 프레임(Stack Frame) 이라는 단위로 사용.
    → 스택 프레임은 함수가 호출될 때 생성되고, 반환될 때 해제
    → 어떤 프로세스가 실행될 때, 이 프로세스가 얼마 만큼의 스택 프레임을 사용하게 될 지를 미리 계산하는 것은 일반적으로 불가능
    ⇒ 운영체제는 프로세스를 시작할 때 작은 크기의 스택 세그먼트를 먼저 할당해주고, 부족해 질 때마다 이를 확장(스택이 확장될 때 기존 주소보다 낮은주소로 확장)
  • 읽기와 쓰기 권한이 부여

5. 힙 세그먼트

  • 힙 데이터가 위치하는 세그먼트

  • 스택과 마찬가지로 실행중에 동적으로 할당될 수 있으며, 리눅스에서는 스택 세그먼트와 반대 방향으로 자람.

  • C언어에서 malloc(), calloc() 등을 호출해서 할당받는 메모리가 이 세그먼트에 위치

  • 일반적으로 읽기와 쓰기 권한이 부여

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


    Quiz 1. b가 위치하는 세그먼트는? - 읽기 전용 데이터 (rodata)
    Quiz 2. a가 위치하는 세그먼트는? - 데이터
    Quiz 3. e가 위치하는 세그먼트는? - 힙
    Quiz 4. foo가 위치하는 세그먼트는? - 코드
    Quiz 5. c가 위치하는 세그먼트는? - BSS
    Quiz 6. "d_str" 위치하는 세그먼트는? - 읽기 전용 데이터 (rodata)
    Quiz 7. d가 위치하는 세그먼트는? - 스택

Background: Computer Architecture
컴퓨터가 하나의 기계로서 작동할 수 있게 하는 기본 설계

CPU가 사용하는 명령어와 관련된 설계 → 명령어 집합구조 ISA (Instruction Set Archehitecture)

가장 대표적인 ISA 인텔의 x86-64아키텍쳐

컴퓨터 구조의 세부 분야

  • 기능 구조의 설계
    • 폰 노이만 구조
      → 연산과 제어를 위해 중앙처리장치( CPU)를,
      저장을 위해 기억장치(memory)를 사용.
      장치간에 데이터나 제어 신호를 교환할 수 있도록 버스(bus)라는 전자 통로를 사용
    • 하버드 구조
    • 수정된 하버드 구조
  • 명령어 집합구조
    : CPU가 해석하는 명령어의 집합
    • x86, x86-64
      → 고성능 프로세서 설계하기 위해 사용
      많은 전력 소모, 발열
    • ARM
    • MIPS
    • AVR
  • 마이크로 아키텍처
    : CPU의 하드웨어적 설계, CPU의 회로를 설계하는 분야
    • 캐시 설계
    • 파이프라이닝
    • 슈퍼 스칼라
    • 분기 예측
    • 비순차적 명령어 처리
  • 하드웨어 및 컴퓨팅 방법론
    • 직접 메모리 접근

x86-64 아키텍처
: 인텔의 32비트 CPU 아키텍처인 IA-32를 64비트 환경에서 사용할 수 있도록 확장한 것

  • n 비트 아키텍처
    • ‘64비트 아키텍처’, ‘32비트 아키텍처’에서 64, 32는 CPU가 한번에 처리할 수 있는 데이터의 크기
      → CPU가 이해할 수 있는 데이터 단위 = WORD
  • WORD가 크면 유리한 점
    • WORD가 클수록 제공하는 가상메모리의 크기도 커짐

x86-64 아키텍처: 레지스터

  • 범용 레지스터(General Register)
    • 주용도는 있으나, 그 외의 다양한 용도로 사용될 수 있는 레지스터
    • 각각의 범용 레지스터는 8바이트를 저장할 수 있으며, 부호 없는 정수를 기준으로 2^64 - 1까지의 수까지 표현 가능
  • 세그먼트 레지스터(Segment Register)
    • cs, ss, ds, es, fs, gs 총 6가지 세그먼트 레지스터가 존재
    • 각 레지스터의 크기는 16비트
    • 과거 IA-32, IA-16에서 세그먼트 레지스터를 이용하여 사용 가능한 물리 메모리의 크기 ↑
      x64에서는 사용 가능한 주소 영역이 굉장히 넓기 때문에 이런 용도로는 거의 사용 X
      현대의 x64에서 cs, ds, ss 레지스터는 코드 영역과 데이터, 스택 메모리 영역을 가리킬 때 사용
  • 명령어 포인터 레지스터(Instruction Pointer Register, IP)
    • CPU가 어느 부분의 코드를 실행할지 가리키는 역할
    • x64 아키텍처의 명령어 레지스터는 rip이며, 크기는 8바이트
  • 플래그 레지스터(Flag Register)
    • 프로세서의 현재 상태를 저장하고 있는 레지스터
    • x64 아키텍처에서는 RFLAGS라고 불리는 64비트 크기의 플래그 레지스터가 존재 (이는 과거 16비트 플래그 레지스터가 확장된 것)
    • RFLAGS는 64비트이므로 최대 64개의 플래그를 사용할 수 있지만, 실제로는 20여개의 비트만 사용

레지스터 호환

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비트를 의미

eax는 rax의 하위 32비트
al은 rax의 하위(가장 낮은) 8비트 부분
ah는 rax의 두 번째 낮은 8비트 부분
ax는 rax의 하위 16비트

Quiz 1. rax에서 rbx를 뺐을 때, ZF가 설정되었다. rax와 rbx의 대소를 비교하시오. rax == rbx

Quiz 2. rax = 0x0123456789abcdef 일 때, eax의 값은? 0x89abcdef

Quiz 3. rax = 0x0123456789abcdef 일 때, al의 값은? 0xef

Quiz 4. rax = 0x0123456789abcdef 일 때, ah의 값은? 0xcd

Quiz 5. rax = 0x0123456789abcdef 일 때, ax의 값은? 0xcdef

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

기계어가 여러 종류라면 어셈블리어도 여려 종류

X64 어셈블리 언어

동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성

명령어

피연산자(Operand)

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

x86-64 어셈블리 명령어

  • 데이터 이동 명령어: 어떤 값을 레지스터나 메모리에 옮기도록 지시
    • mov dst, src : src에 들어있는 값을 dst에 대입
    • lea dst, src : src의 유효 주소(Effective Address, EA)를 dst에 저장
  • 산술 연산 명령어: 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시
    • add dst, src : dst에 src의 값을 더하기.
    • sub dst, src: dst에서 src의 값을 빼기.
    • inc op: op의 값을 1 증가
    • dec op: op의 값을 1 감소
  • 논리 연산 명령어 : and, or, xor, neg 등의 비트 연산
    을 지시하는 명령어로 비트 단위를 통해 이루어짐.
    • and dst, src: dst와 src의 비트가 모두 1이면 1, 아니면 0
    • or dst, src: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
    • xor dst, src: dst와 src의 비트가 서로 다르면 1, 같으면 0
    • not op: op의 비트 전부 반전
  • 비교 명령어 : 두 피연산자의 값을 비교하고, 플래그를 설정
    • cmp op1, op2: op1과 op2를 비교
    • test op1, op2: op1과 op2를 비교
  • 분기 명령어 : rip를 이동시켜 실행 흐름을 바꾸는 역할
    • jmp addr: addr로 rip를 이동
    • je addr: 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
    • jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)
  • Opcode: 스택
    • push val : val을 스택 최상단에 쌓음
      rsp -= 8
      [rsp] = val
    • pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
      rsp += 8
      reg = [rsp-8]
  • Opcode: 프로시저
    • 컴퓨터 과학에서 프로시저(Procedure)는 특정 기능을 수행하는 코드 조각
    • 프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체할 수 있어서 전체 코드의 크기를 줄일 수 있고, 기능별로 코드에 이름을 붙이는 기능도 있어 코드의 가독성도 크게 높일 수 있음.
    • 호출(Call) : 프로시저를 부르는 행위.
      반환(Return) : 프로시저에서 돌아오는 것
    • 프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(return address, 반환주소)를 스택에 저장하고 프로시저로 rip을 이동
  • x64어셈블리어에서 프로시저 호출과 반환을 위한 명령어
    • call addr : addr에 위치한 프로시져 호출
    • leave : 스택프레임 정리
      • 스택은 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역. 이 스택 영역을 아무런 구분 없이 사용하게 된다면, 서로 다른 두 함수가 같은 메모리 영역을 사용할 수도 있음
        ⇒ 따라서 함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 스택프레임이 사용
    • ret : return address로 반환
  • Opcode: 시스템 콜
    운영체제는 모든 하드웨어 및 소프트웨어에 접근 가능
    ⇒ 이 기능을 보호하기 위해 커널 모드와 유저 모드로 권한을 나눈다.
    커널 모드 : 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한으로 모든 저수준의 작업은 커널 모드에서 진행.
    유저 모드 : 운영체제가 사용자에게 부여하는 권한.
    • 시스템 콜(system call, syscall)은 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작 수행을 요청하는 것
      • x64아키택쳐에서는 시스템콜을 위해 syscall 명령어 존재
    • 시스템 콜은 함수이므로, 필요한 기능과 인자에 대한 정보를 레지스터로 전달하면, 커널에서 이를 읽어서 요청을 처리
syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
read0x00unsigned int fdchar *bufsize_t count
write0x01unsigned int fdconst char *bufsize_t count
open0x02const char *filenameint flagsumode_t mode
close0x03unsigned int fd
mprotect0x0aunsigned long startsize_t lenunsigned long prot
connect0x2aint sockfdstruct sockaddr * addrint addrlen
execve0x3bconst char *filenameconst char const argvconst char const envp

STAGE 3 Tool Installation

Tool: gdb
버그: 컴퓨터 과학에서 실수로 발생한 프로그램의 결함을 의미
디버거: 이미 완성된 코드에서 버그를 찾아 없애주는 도구
gdb: 리눅스의 대표적인 디버거

설치 후 로드맵 따라 실습 진행

Tool: pwntools
익스플로잇(Exploit): 해킹 분야에서 상대 시스템을 공격하는 것
pwntools: 파이썬 모듈로, 파이썬으로 여러 개의 익스플로잇 스크립트를 작성하다 보면, 자주 사용하게 되는 함수들이 존재하는데 그 함수들을 집대성 한 것.

설치 후 로드맵 따라 실습 진행

STAFE 4 Shellcode

셸코드(Shellcode): 익스플로잇을 위해 제작된 어셈블리 코드 조각
orw 셸코드: 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드

orw 셸코드 작성을 위해 알아야 하는 syscall

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
read0x00unsigned int fdchar *bufsize_t count
write0x01unsigned int fdconst char *bufsize_t count
open0x02const char *filenameint flagsumode_t mode

“/tmp/flag”를 읽는 셸코드 ← c언어 형식 의사코드로 표현

1. int fd = open(“/tmp/flag”, O_RDONLY, NULL)
첫 번째로 해야 할 일: “/tmp/flag”라는 문자열을 메모리에 위치시키는 것
→ 스택에 0x616c662f706d742f67(/tmp/flag)를 push
→ rdi가 이를 가리키도록 rsp를 rdi로 옮김
O_RDONLY는 0이므로, rsi는 0으로 설정
→ 파일을 읽을 때, mode는 의미를 갖지 않으므로, rdx는 0으로 설정
rax를 open의 syscall 값인 2로 설정

  1. read(fd, buf, 0x30)

syscall의 반환 값은 rax로 저장
open으로 획득한 /tmp/flag의 fd는 rax에 저장
(파일 서술자(File Descriptor, fd): 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자)
read의 첫 번째 인자를 이 값(fd)으로 설정해야 하므로 rax를 rdi에 대입
→ 0x30만큼 읽을 것이므로, rsi에 rsp-0x30을 대입
(rsi: 파일에서 읽은 데이터를 저장할 주소)
rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정
→ read는 시스템콜을 호출하기 위해서 rax를 0으로 설정

  1. write(1, buf, 0x30)

출력은 stdout ⇒ rdi를 0x1로 설정
rsi와 rdx는 read에서 사용한 값을 그대로 사용
→ write 시스템콜을 호출하기 위해서 rax를 1로 설정
종합하면

이 셸코드는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF(Executable and Linkable Format)형식이 아니므로 리눅스에서 실행 불가능 ⇒ gcc 컴파일을 통해 ELF형식으로 변환

  • 어셈블리 코드를 컴파일하는 방법
    • 셸코드를 실행할 수 있는 스켈레톤 코드를 C언어로 작성하고, 거기에 셸코드를 탑재하는 방법

execve 셸코드

  • 셸 (shell, 껍질) ⇔ 커널 (kernel, 호두 속 내용물)
  • 운영체제에 명령을 내리기 위해 사용되는 인터페이스
    ⇔ 운영체제의 핵심 기능을 하는 프로그램

셸 획득 = 시스템 해킹 성공

execve 셸코드

  • 임의의 프로그램을 실행하는 셸코드
  • 이를 이용하면 서버의 셸을 획득 가능
  • 다른 언급없이 셸코드라고 하면 이를 의미하는 경우

execve 셸코드는 execve 시스템 콜만으로 구성

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
execve0x3bconst char *filenameconst char const argvconst char const envp

argv: 실행파일에 넘겨줄 인자

envp: 환경변수

sh만 실행하면 되므로 다른 값들은 전부 null로 설정

리눅스에서는 기본 실행 프로그램들이 /bin/ 디렉토리에 저장되어 있으며, 실행하려는sh도 여기에 저장

⇒ 따라서 execve(“/bin/sh”, null, null)을 실행하는 것을 목표로 셸 코드를 작성
리뷰퀴즈

Q1. 실습 환경에서 다음 파일 지정자들의 번호를 맞춰 보세요.

  • stderr = 2
  • stdin = 0
  • stdout = 1

리눅스에서 fd 0번은 표준 입력(stdin), 1번은 표준 출력(stdout), 2번은 표준 에러 출력(stderr)에 지정

Q2. 실습 환경에서 execve로 셸을 획득하려고 합니다. 이 때, 다음 셸코드의 빈칸을 채워 보세요

(a) 0x7fffffffc278

(b) 0x3b

리눅스 x86-64 아키텍처에서, 시스템 콜의 종류는 rax로 지정하고, 인자는 rdi, rsi, rdx, rcx, r8, r9 .. 순서로 전달.

execve의 번호는 59(0x3b)이고, 우리가 실행하려는 시스템 콜의 형태는 execve("/bin/sh", NULL, NULL) 이므로 rdi는 0x7fffffffc278

0개의 댓글

관련 채용 정보