취약점분석/Pwnable

[Pwnable] 셸코드 (Shellcode) - execve 셸코드

poiri3r 2025. 6. 16. 03:02

이번 포스팅은 포너블에서 가장 기본이 되는 셸코드에 대해 작성해보도록 하겠습니다.

 

먼저 익스플로잇(Exploit)이란 컴퓨터 시스템, 네트워크, 소프트웨어에서 취약점을 이용하여 원래 의도되지 않은 방식으로 동작하게 하는것을 의미합니다.

셀코드는 익스플로잇을 위해 제작된 코드 조각인데, 공격자가 취약점을 이용해 시스템에 주입하고 실행시키는 목적으로 사용됩니다. 이름에 셸(shell)이 들어간 이유는 원래 목적이 공격자가 시스템 셸(리눅스의 bin/sh, 윈도우의 cmd.exe)를 실행하기 위한 코드였기 때문입니다. 

*해킹하고싶은 컴퓨터의 셸에 접속할 수 있으면 원하는 모든 명령어를 내릴수 있기 때문입니다.

 

셀코드에는 크게 두가지 종류가 있는데 첫번째로 execve 셸코드이고 두번째로 ORW 셸코드입니다. 오늘은 execve에 대해 포스팅 해보고 다음에 ORW 셀코드에 대해 포스팅해보겠습니다.

 

execve란 ?

 

먼저 execve란 리눅스/유닉스 계열 운영체제에서 프로세스를 다른 프로그램으로 완전히 교체하는 syscall입니다.

현재 프로세스를 완전히 대체 하기 때문에 execve가 등장한 이후 코드는 실행되지 않습니다. 

익스플로잇에서 공격자는 취약점을 통해 execve("/bin/sh")를 호출하는 셸코드를 주입하는 방식으로, 원격 명령 실행 권한을 얻는 방식으로 사용됩니다. 

 

execve의 가장 기본적인 코드를 C언어로 작성하면 다음과 같습니다.

#include <unistd.h>

int main() {
    char *argv[] = { "/bin/sh", NULL };
    execve("/bin/sh", argv, NULL); 
    return 0;                        
    }

 

핵심이 되는 execve함수는 다음과 같습니다.

#include <unistd.h> /execve를 사용하기위한 헤더파일
int execve(const char *filename, char *const argv[], char *const envp[]);

 

const char *filename : 실행할 프로그램의 절대 경로 또는 상대경로를 나타내는 문자열입니다 (/bin/sh나 /usr/bin/python)

 

char *const argv[] : 새 프로그램에 전달할 인자 목록으t로 첫번째 요소 argv[0]에는 보통 프로그램 이름이 들어갑니다.

 

char *const envp[] : 실행할 새 프로그램에 전달되는 환경 변수 목록으로, 마지막은 반드시 NULL로 끝나야 합니다. 저희가 작성한 쉘코드에는 환경변수가 없으므로 NULL만 작성해주었습니다.

 

이제 쉘코드를 작성해보겠습니다. 쉘코드를 작성할때는 어셈블리로 작성해야 합니다. 어셈블리어로 작성하고 컴파일하는 과정에서 nasm이 필요하기 때문에 먼저 nasm을 깔아줬습니다.

sudo apt update
sudo apt install nasm

nasm -v를 입력해주면 설치가 잘 되었는지 확인할 수 있습니다.

nasm은 정확하게 컴파일러는 아니고 어셈블리어를 기계어로 바꿔주는 어셈블러입니다. Intel형식의 어셈블리어를 사용합니다. 

gcc를 이용해서도 컴파일이 가능하긴 하지만 gcc를 이용해서 쉘코드를 작성하고 컴파일을 하면 execve호출 앞뒤에 여러 보조 코드들이 붙어 쉘코드로 사용하긴 불필요하게 커지고 구조가 복잡해집니다.

nasm을 사용해야 직접 쓴 _start함수에서 필요한 명령만 기계어로 변환하기 때문에 nasm이 원래 목적에 더 적합합니다.

 

다시 돌아가서 execve호출하는 어셈블리어 코드를 작성하겠습니다.

section .text
global _start

_start:
    xor rax, rax  
    mov al, 59 
    xor rsi, rsi              
    xor rdx, rdx 
    mov rdi, 0x68732f6e69622f 
    push rdi                  
    mov rdi, rsp                                         
    syscall

 

처음보면 굉장히 뭘까싶은 코드가 나왔는데요.하나하나씩 살펴보도록 하겠습니다.

section .text
global _start

 

section .text는 코드가 저장될 영역을 지정하는 명령어입니다.

global _start는 _start부분을 전역을 선언한다는 뜻입니다. C언어 프로그램은 main함수가 시작점이지만 어셈블리에서는 직접 지정해야 합니다. _start는 보통 프로그램의 EP로 사용됩니다.

 

syscall 번호 syacall rax rdi rsi rdx
56 execve 0x3B file name argv[] envp[]

 

execve에 간단하게 표로 정리했습니다. 

rax에는 0x3B값이 rdi에는 filename이 rsi에는 argv값(=0) rdx에는 envp값(=0)이들어가야합니다.

xor rax, rax
mov al, 59

 

해당 두 줄을 통해 rax값을 0으로 초기화 한뒤 59값을 넣습니다. (rax를 초기화 안하면 쓰레기값이 들어가있을 수도 있음)

xor rsi, rsi
xor rdx, rdx

 

해당 두 줄은 각각 rsi, rdx를 0으로 초기화합니다. (argv, envp)

 

mov rdi, 0x68732f6e69622f 
push rdi                  
mov rdi, rsp

 

/bin/sh를 리틀엔디안 방식으로 ASCII코드표를 통해 변환한 뒤 rdi에 넣고 스택에 push합니다.

 그 뒤 rdi에 rsp(스택포인터)값을 넣습니다.

 

syscall

 

래지스터 값을 다 설정한 뒤 syscall을 하면 실행됩니다!

 

이제 컴파일하는 과정을 설명드리겠습니다. 먼저 저는 execve.asm으로 해당 코드를 저장해뒀습니다.

해당 코드가 들어있는 파일에서 wsl을 실행한뒤 다음 명령어를 입력해줍니다.

nasm -f elf64 execve.asm -o execve.o

 

해당 명령어는 64비트 ELF 형식의 출력 오브젝트 파일을 생성해줍니다.

그 다음 링커로 실행할 수 있는 파일을 만들어줘야합니다.

ld execve.o -o execve

 

chmod +x를 통해 파일에 실행권한을 부여합니다 

chmod +x execve

 

.o파일이나 NASM으로 만든 실행파일은 기본적으로 실행권한이 없기 때문에 실행 가능하게 만들어주어야 합니다.

 

실행해주면 #으로 바뀌면서 루트 권한으로 실행중인 셸이 뜨게됩니다.

 

이상으로 execve에 대한 포스팅을 마치겠습니다.

생각보다 너무 어려우면서도 좀 재미있는 포인트들이 있던 것 같네요.

다음 포스팅은 ORW쉘코드 작성과 gdb로 디버깅하는 내용까지 담아보도록 하겠습니다!

읽어주셔서 감사합니다~