실습 환경
Linux Memory Layout
1. 코드 세그먼트
2. 데이터 세그먼트
3. BSS 세그먼트
4. 스택 세그먼트
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아키텍쳐
컴퓨터 구조의 세부 분야
x86-64 아키텍처
: 인텔의 32비트 CPU 아키텍처인 IA-32를 64비트 환경에서 사용할 수 있도록 확장한 것
x86-64 아키텍처: 레지스터
레지스터 호환
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)
x86-64 어셈블리 명령어
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 char const envp |
Tool: gdb
버그: 컴퓨터 과학에서 실수로 발생한 프로그램의 결함을 의미
디버거: 이미 완성된 코드에서 버그를 찾아 없애주는 도구
gdb: 리눅스의 대표적인 디버거
설치 후 로드맵 따라 실습 진행
Tool: pwntools
익스플로잇(Exploit): 해킹 분야에서 상대 시스템을 공격하는 것
pwntools: 파이썬 모듈로, 파이썬으로 여러 개의 익스플로잇 스크립트를 작성하다 보면, 자주 사용하게 되는 함수들이 존재하는데 그 함수들을 집대성 한 것.
설치 후 로드맵 따라 실습 진행
셸코드(Shellcode): 익스플로잇을 위해 제작된 어셈블리 코드 조각
orw 셸코드: 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드
orw 셸코드 작성을 위해 알아야 하는 syscall
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 |
“/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로 설정
syscall의 반환 값은 rax로 저장
⇒ open
으로 획득한 /tmp/flag의 fd는 rax에 저장
(파일 서술자(File Descriptor, fd): 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자)
read의 첫 번째 인자를 이 값(fd)으로 설정해야 하므로 rax를 rdi에 대입
→ 0x30만큼 읽을 것이므로, rsi에 rsp-0x30을 대입
(rsi: 파일에서 읽은 데이터를 저장할 주소)
→ rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정
→ read는 시스템콜을 호출하기 위해서 rax를 0으로 설정
출력은 stdout ⇒ rdi를 0x1로 설정
→ rsi와 rdx는 read에서 사용한 값을 그대로 사용
→ write 시스템콜을 호출하기 위해서 rax를 1로 설정
종합하면
이 셸코드는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF(Executable and Linkable Format)형식이 아니므로 리눅스에서 실행 불가능 ⇒ gcc 컴파일을 통해 ELF형식으로 변환
execve 셸코드
셸 획득 = 시스템 해킹 성공
execve 셸코드
execve 셸코드는 execve 시스템 콜만으로 구성
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
---|---|---|---|---|
execve | 0x3b | const char *filename | const char const argv | const char const envp |
argv: 실행파일에 넘겨줄 인자
envp: 환경변수
sh만 실행하면 되므로 다른 값들은 전부 null로 설정
리눅스에서는 기본 실행 프로그램들이 /bin/ 디렉토리에 저장되어 있으며, 실행하려는sh도 여기에 저장
⇒ 따라서 execve(“/bin/sh”, null, null)을 실행하는 것을 목표로 셸 코드를 작성
리뷰퀴즈
Q1. 실습 환경에서 다음 파일 지정자들의 번호를 맞춰 보세요.
리눅스에서 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