SECCON13 Quals writeup
SECCON13 QualsにKUDoSで出場
全体24位、国内4位でした
自分はpwn3問とrev2問を解いたのでそのwriteupを載せます
rev
packed
パッキングされているバイナリのようだが、UPXを使ってアンパックすると
Flagcheckerの挙動をしないバイナリになるみたいなので、アンパック前のバイナリでgdbで解析を行う。
すると大体以下のことがわかる。
0x44ee1f:
syscall(read)の返り値を0x31と比較していることからflagは0x31文字1
20x44ee1d 0f05 <NO_SYMBOL> syscall
*-> 0x44ee1f 83f831 <NO_SYMBOL> cmp eax, 0x310x44ee34~0x44ee3a:
stack上のデータ(ユーザの入力値)に対してループでxorの処理をしている。
xorする値の値が格納されている開始番地はrsi。1
2
3
4
50x44ee2c 488dbc2470ffffff <NO_SYMBOL> lea rdi, [rsp - 0x90]
-> 0x44ee34 ac <NO_SYMBOL> lods al, BYTE PTR ds:[rsi]
0x44ee35 3007 <NO_SYMBOL> xor BYTE PTR [rdi], al
0x44ee37 48ffc7 <NO_SYMBOL> inc rdi
0x44ee3a e0f8 <NO_SYMBOL> loopne 0x44ee340x44ee82~0x44ee8d
xorした入力値とメモリ上の値をループで一文字ずつ比較している1
2
3
4
5
6*-> 0x44ee82 ac <NO_SYMBOL> lods al, BYTE PTR ds:[rsi]
0x44ee83 3807 <NO_SYMBOL> cmp BYTE PTR [rdi], al
0x44ee85 0f95c0 <NO_SYMBOL> setne al
0x44ee88 08c2 <NO_SYMBOL> or dl, al
0x44ee8a 48ffc7 <NO_SYMBOL> inc rdi
0x44ee8d e0f3 <NO_SYMBOL> loopne 0x44ee82
上記からループで使用しているxorの値と比較文字列をメモリ上から撮ってきて、以下のソルバを作成。
1 | a = [ |
Jump
aarch64のバイナリ
とりあえずghidraのデコンパイラで開いてみると、以下のような数値との比較を行う関数や
1 | void FUN_0040090c(int param_1) |
何らかの値との演算後の数値を比較している関数が見つかる。
1 | void FUN_00400964(long param_1) |
前者に出てきた0x43434553とかは’SECC’のASCIIなので、flagの一部を比較や演算している雰囲気を感じる。
一応qemuのデバッグ環境を用意し、前述の関数にブレークポイントを貼ったりしたもののそう簡単には引っ掛からず。
コンテスト終盤で体力が厳しくなってきたので、比較している数値の演算の組み合わせでASCII文字列になるようなものを探す手法に乗り換える。
以下が最終的なコード。
1 | import struct |
pwn
Paragraph
数行のソースコードがコンパイルされたバイナリ
1 |
|
printf()によるFSBがあり、その後もう一回printfを呼んでいる。
またscanfで読み込めるのは23bytesであるため、FSBもこの文字数の制約を受ける。
ここで配布された環境のlibc内ではscanfとprintfがかなり近い場所にあるので、printfのgot領域の下位2bytesをscanfのアドレスに書き換えた場合、
2回目のprintfで変数nameに対して%sで読み込むことができるのでBOFが引き起こせそうである。
1 | $ objdump -d -M intel ./libc.so.6 |
libcリークはしていないので、4bitのbruteforceで(1/16の確率)うまくprintfをscanfに書き換えることができる。
が、この解法を思いついたときにはすでにlibcリークをしながら2回目のmain関数に飛ぶことができていた。
それが以下のpayloadである。
(以下のwriteupを参考にした、見つけきてくれた@k1_zuna氏ありがとう)
https://project-euphoria.dev/problems/imaginary-ctf-2022-format-string-fun/
1 | payload = b'%*38$p%8$n%33$hn' # just 16 bytes! |
上記ペイロードを送った際ののprintf実行時のstackは以下のような状況である。
(環境によって若干違うと思われるがリモートでも刺さったので主要なところは問題ないはず)
1 | gef> x/40gx $rsp |
‘%*38$p%8$n’で0x4010b0(_startのアドレス)を0x0404ec8のアドレスに書き込みながら(理由は後述)、第一引数を%pで出力している。
この時のrsiはlibc内のアドレスをたまたま指しているのでlibcリークもできる。
残りの部分の’%33$hn’では0x10b0を0x7ffff7ffe2e0に書き込んでいる。
さて0x7ffff7ffe2e0には何があるかというと、_rtld_globalが指すlink_map->l_addrである。
https://elixir.bootlin.com/glibc/glibc-2.40.9000/source/include/link.h#L95
1 | gef> x/10gx &_rtld_global |
l_addrを書き換えると何が起きるかというと、_dl_call_fini内で呼ぶfini_arrayをずらすことができる。
1 | ElfW(Dyn) *fini_array = map->l_info[DT_FINI_ARRAY]; |
https://elixir.bootlin.com/glibc/glibc-2.39.9000/source/elf/dl-call_fini.c#L23
今回のfini_arrayは0x403e18なので0x10b0を足すと0x404ec8になる。
0x404ec8にはFSBで_startのアドレスを書き込んでいるので、2回目のmain関数が実行可能である。
1 | $readelf -S ./chall |
2回目のmainでは先述のprintfのgot領域をscanfに変える手法を使う。
libcリークをすることにより、scanfとprintfの下位3byte目が一致しない場合を除いてexploitが刺さるようになった。
(理論上15/16の確率だが、実際には%cで出力する文字数が多すぎると失敗しているような感じがする)
1 | #!/usr/bin/python3 |
Make ROP Great Again
getsがあるので自明なBOF、ROPを組みたいが単純なgadgetがないのでどうにかする問題。
頑張ってどうにかできたので以下ざっくりとした流れ。
- bssの固定アドレス領域にstack pivot(stackアドレスが既知だと色々やりやすくなるので)
- _startからmain関数を実行すると_IO_file_underflow+357(
pop rbx; ...; ret;
が存在するいい感じのgadget)のアドレスがstackに残る add dword[rbp-0x3d]; ebx; ret;
のgadgetを使い、これまたstack上に落ちている_libc_start_main+139に加算を行うことでstack上にone_gadgetのアドレスを用意する。- 用意したone_gadgetにretする
1 | => 0x7ffff7e3d795 <_IO_file_underflow+357>: test rax,rax |
使用するone_gadgetは以下
1 | $ one_gadget ./libc.so.6 |
最終的なexploit
1 | #!/usr/bin/python3 |
free-free free
いわゆるnote問っぽいheap exploit。release関数があるがfree()が呼ばれていない。
脆弱性はalloc関数でData構造体を確保するときに適切なサイズで確保されていないので、edit時に8bytesのheap overflowが発生する。
free()がない、heap overflowが存在するの2条件からtop chunkのサイズを書き換えて無理やりfreedなchunkをheap上に作成するテクニック(house of orangeという手法の1パートだった気がする)を思いつく。
またalloc時に構造体を初期化していないので、Data->nextの位置にlibcのアドレスがある状態を作れる。
例えば以下を実行するとhead変数はlibcのアドレスを指すようになる。
1 | id_x = alloc(0x400) |
1 | gef> x/2gx &head |
0x7ffff7faeb40はlibc内のアドレス(small bin)であり、また0x7ffff7faeb40をData構造体としてみると、bufに当たる0x7ffff7faeb50は自身を指しているので、この状態でeditを行うと(id=0x7fff, size=0xf7faeb30)、nextを編集することができてAAWが作れる。
show関数的なものがないが、edit&release時に存在しないIDを指定すると”Not found”が出力するoracleやedit時にprintf("data(%u): ",...)
を実行してくれているので、ここからlibcリーク&heapリークができる。
AAWができるのでFSOPをしてシェルを取得する。
1 | #!/usr/bin/python3 |
終わりに
運営陣のみなさま、いつも良いCTFを本当にありがとうございます。
本戦参加は2年ぶりで、前回あまり本線振るわなかったので頑張りたい所存。