취약점분석/Pwnable

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

poiri3r 2025. 6. 16. 21:24

오늘은 ORW 셸코드에 대해 작성해보도록 하겠습니다.

 

ORW는 Open, Read, Write라는 시스템 콜의 약자로 특정 파일을 열고 메모리로 읽은 뒤 읽은 내용을 출력(stdout)하는 코드 조각을 일컫습니다.

보통 CTF 문제에서 flag를 얻을 때 쉘 권한 없이 출력해야 할 경우 사용합니다.

 

먼저 구현하고자 하는 쉘코드를 C언어로 표현하면 다음과 같습니다.

#include <fcntl.h>  
#include <unistd.h>

int main() {
    char buf[0x30];

    int fd = open("/tmp/flag", O_RDONLY); 
    read(fd, buf, 0x30);
    write(1, buf, 0x30);

    return 0;
}

 

<fcntl.h>와 <unistd.h>는 각각 open과 read,write를 사용하기 위한 헤더파일입니다.

먼저 파일에서 읽을 내용을 저장하기 위해 char형식의 배열을 만들어줍니다.

그 다음에 syscall이 3가지 나오는데 표로 정리해 보았습니다

syscall number syscall rax rdi rsi rdx
0 sys_read 0x0 unsigned int fd char *buf size_t count
1 sys_write 0x1 unsigned int fd const char *buf size_t coun
2 sys_open 0x2 const char 
*filename
int flags int mode

 

먼저 뼈대를 작성해보겠습니다.

section .text
global _start

 

저번에 했던 대로 코드가 저장될 영역을 지정하고, 프로그램의 Entry Point를 _start로 지정합니다.

이제 Open - Read - Write 순서대로 Syscall 코드에 대해 알아보겠습니다.

 

 1.Open

_start:

    push 0x67                        ; 'g'
    mov rax, 0x616c662f706d742f      ; '/tmp/fla'
    push rax
    mov rdi, rsp                     
    xor rsi, rsi                    
    xor rdx, rdx                     
    mov rax, 2                       
    syscall

 

저번에도 한번 분석했었기 때문에 빠르게 훑고 넘어가보겠습니다.

push 0x67
push 0x616c662f7076d742f
push rax

 

/tmp/flag라는 문자열(출력을 원하는 경로)을 리틀엔디안 기준으로 스택에 푸쉬합니다.

mov rdi, rsp

 

rdi에 스택에 저장된 문자열을 넣습니다 (sys_open의 filename 인자)

xor rsi,rsi
xor rdx,rdx

 

open에 들어가는 인자인 arg1(rsi) (*O_RDONLY의 값이 0) 와 arg2를 0으로 설정합니다.

mov rax, 2
syscall

마지막으로 rax를 open의 syscall 번호인 2를 넣은 뒤 syscall을 해주면 open은 끝입니다

 

여기서 syscall의 반환값은 rax에 저장되므로,  rax에 open으로 획득한 /tmp/flag의 내용 fd는 rax에 저장됩니다. 이 fd는 read와 write의 인자로 들어가는 값인데 쉽게 말해 리눅스의 syscall을 통해 열리는 파일의 핸들입니다.

이제 두번째 syscall인 read에 대해 살펴보겠습니다.

 

2.read

    mov rdi,rax
    mov rsi,rsp
    sub rsp, 0x30
    mov rdx, 0x30
    mov rax, 0
    syscall

 

먼저 아까 open의 반환값인 fd가 저장된 rax의 값을 rdi로 옮겨줍니다.

그 뒤 0x30만큼의 데이터를 저장하기 위한 공간을 마련해줍니다.

mov rsi, rsp
sub rsi, 0x30

 

먼저 현재 스택포인터 값인 rsp를 rsi에 복사합니다. 

현재까지의 rsp 상태

그 뒤 sub rsi,0x30으로 0x30만큼의 공간을 확보해줍니다.

새로 공간을 확보함

 

그 뒤 rdx와 rax 값을 인자에 맞게 각각 설정해주고 syscall을 진행합니다.

mov rdx, 0x30
mov rax, 0
syscall

 

마지막으로 남은 부분에 대해 살펴보겠습니다.

3.write

    mov rdi, 1
    mov rax, 1
    syscall

 

먼저 write의 인자에 들어가는 fd는 stdout이므로 fd값을 1로 설정해주어야합니다. 그래서 mov rdi,1을 통해 인자값을 넣어주고,

write의 syscall number가 1이기 때문에 rax에도 1을 넣어줍니다.

rsi와 rdx에는 이미 원하는 값(read에서 사용한 rsp-0x30, 0x30)이 들어가있기 때문에 따로 넣을 필요가 없습니다.

 

전체코드는 다음과 같습니다.

section .text
global _start

_start:
    push 0x67
    mov rax, 0x616c662f706d742f 
    push rax

    push rax
    mov rdi, rsp
    xor rsi, rsi
    xor rdx,rdx
    mov rax, 2
    syscall

    mov rdi,rax
    mov rsi,rsp
    sub rsi, 0x30
    mov rdx, 0x30
    mov rax, 0
    syscall

    mov rdi, 1
    mov rax, 1
    syscall

    mov rax,60
    xor rdi,rdi
    syscall

 

문자열을 push하는데 nasm에서 warning이 떠서 나눠서 push했습니다.

저번에 했던 것처럼 그대로 링킹한 뒤 컴파일 해보겠습니다.

 

그리고 저희는 /tmp/flag에 문자열을 출력해야되므로 /tmp/flag 파일을 새로 생성해야 합니다

echo 'flag{dustin_poiri3r_is_diam0nd!'} > /tmp/flag

 

입력해준뒤 컴파일한 코드를 실행해주면 우리가 입력한 flag가 나오게 됩니다!

중간중간 헷갈리는 과정이 있었지만 .. 그래도 잘 완성이 됐고, 이제 gdb로 분석하는 걸 연습해보겠습니다.

 

orw 동적 분석

먼저 orw를 gdb로 열고, _start부분에 브레이크 포인트를 설정해줍니다.

작성한 셸코드에 rip가 위치한것을 확인할 수 있습니다.

 

syscall : open 분석

 

여기서 첫번째 syscall인 0x40101b 즉 <_start+27>지점에 bp를 걸어보겠습니다.

그 뒤 c(continue)를 입력해 bp지점까지 이동해보면

지점에서 파일명이 제대로 입력되었고, 인자도원하는값이 들어가 있습니다. 

이 뒤에는 ni로 함수 안으로 들어가서 이동해보겠습니다.

 

왼쪽은 ni를 입력하기 전 레지스터 상태고 오른쪽은 ni를 입력한 후의 레지스터 상태입니다.

syscall : open을 실행한 결과로 RAX에 3이 저장됨을 확인했습니다.

 

syscall : read 분석

 

그 다음으로 syscall read지점에다 bp를 걸고 분석해보겠습니다.

start+49지점에 위치해서 bp를 걸고 이동해주었습니다.

fd값으로 3이들어갔고 나머지 인자들도 원하는 값이 들어가 있습니다.

 

왼쪽은 ni실행 전 레지스터고 오른쪽은 ni실행 후 레지스터상태입니다.

flag값에 flag의 문자열이 성공적으로 저장된 것을 확인할 수 있습니다.

 

syscall : write 분석

마지막으로 write입니다. buf안에 flag가 적혀있는 메모리 주소값이 들어가있고 인자값도 잘 들어가 있습니다.

ni로 한 줄 실행해보면

플래그가 쉘에 출력되는걸 확인할 수 있습니다.

 


어셈블리 코드 짜보겟다고 지피티랑 할때는 짜증이 많이 났는데 디버거로 연습해보니까 재밌네요. 디버거로 실습해보는게 제일 재미있는 것 같습니다 .. odjdump를 이용한 쉘코드 추출도 같이 작성하고 싶었는데, orw쉘코드를 추출하는것은 크게 유용하지 않아서 다음 포스팅할때 execve 쉘코드를 추출하는 걸 작성해보도록 하고 오늘은 여기까지 포스팅을 해보도록 하겠습니다.

읽어주셔서 감사합니다~