[InterKosenCTF 2020] Tip Toe

Fall Bozos: Ultimate KosenCTF
The timer is of integer type.

解説

3Dゲームのexeファイルを渡された
10*10のタイルを越えて対岸に渡ればゴールらしい

だが、体感8割くらいの確立で踏んだタイルが崩落するので普通にゴールするのはつらそう
ゲーマーの威信にかけて根性でクリアしたら「もっとはやくクリアしやがれ」と言われた

仕方がないのでGhidraで解析することにする
Defined StringsにHSPという文字が散見されることからHSPで書かれたプログラムであると推測できる

「HSP exe 逆コンパイル」でggったらとてもよさそうなものが見つかった(https://github.com/YSRKEN/HSP-Decompiler)

投げてみた

#runtime "hsp3gp"

#struct modflag var prm_1
#regcmd "", ""

    goto *label_1
#deffunc __init modinit modflag, str prm_3, str prm_4, str prm_5
    var_0 = ginfo(16)
    var_1 = ginfo(17)
    var_2 = ginfo(18)
    sdim var_3, 4
    sdim var_4, 1
    sdim var_5, 4
    // /*09 20 004E*/ 6384761
    var_6 = ginfo(17)
    var_2 = ginfo(18)
    lpoke var_3, 0, ginfo(18) * 16777216 + ginfo(16) * 65536 + ginfo(17) * 256 + 112
    poke var_4, 0, ginfo(16)
    // /*09 20 004E*/ 6775149
    lpoke var_5, 0, 1694498816 + ginfo(18) * 65536 + ginfo(17) * 256 + ginfo(16)
    // /*09 20 004E*/ 4412486
    var_7 = ginfo(16)
    var_8 = ginfo(17)
    var_9 = ginfo(18)
    color var_0, var_1, var_2
    mes strf("KosenCTF{%s_%s_%s_%s_%s_%s_%c%c%c}", prm_3, var_3, var_4, var_5, prm_4, prm_5, var_7, var_8, var_9)
    // /*09 20 004E*/ 14593470
    return
*label_1
    setreq 50, 0
    title "Tamane Tip Toe"
    randomize
    // /*09 20 0060*/
    setcls 1, 15761568
    // /*09 20 00B5*/ 1048579 1 1 1
    // /*09 20 00B3*/ 1048579 0.5 0.5 0.5
    celload "res/text_eliminated.png", 4
    celload "res/text_qualified.png", 5
    buffer 1, ginfo(26), ginfo(27), 32 + 64
    var_10 = 1
    var_11 = 2
    var_12 = dirinfo(4)
    var_12 = strtrim(var_12, 0, 32)
    if ( var_12 == "debug" ) {
        var_13 = 1
        dialog "argv[1]='debug' : debug mode activated", 0, "Developer Mode"
    }
    else {
        var_13 = 0
    }
    var_14 = 16.0
    var_15 = 0.0
    var_16 = -20.0
    var_17 = 0.0
    var_18 = 0.0
    var_19 = 0.0
    var_20 = var_17
    var_21 = 12.0
    var_22 = 0
    var_23 = 0
    var_24 = 0
    var_25 = 0
    gosub *label_6
    var_26 = 0
*label_2
    gosub *label_5
    gosub *label_3
    gsel 1
    redraw 0
    gosub *label_4
    // /*09 20 0061*/
    redraw 1
    gsel 0
    redraw 0
    gmode 0
    pos 0, 0
    celput 1
    if ( var_13 ) {
        pos 16, 16
        color 0, 0, 0
        mes strf("(px, py, pz) = (%f, %f, %f)", var_15, var_27, var_16)
        mes strf("(cx, cy, cz) = (%f, %f, %f)", var_28, 10.0, var_29)
        mes strf("(dx, dz) = (%f, %f)", var_18, var_30)
        mes strf("animation = %d", var_22)
        mes strf("time = %f", var_26)
    }
    if ( var_25 == 1 ) {
        pos ginfo(26) / 2 - 268, ginfo(27) / 2 - 45
        gmode 2
        gcopy 4, 0, 0, 537, 90
    }
    else {
        if ( var_25 == 2 ) {
            pos ginfo(26) / 2 - 240, ginfo(27) / 2 - 45
            gmode 2
            gcopy 5, 0, 0, 480, 90
            pos ginfo(26) / 2 - 160, ginfo(27) / 2 + 60
            color 80, 240, 80
            if ( var_26 < 100 ) {
                newmod var_31, modflag, "Let's", "other", "than"
                delmod var_31
            }
            else {
                mes "Tips: Finish more quickly to get the flag!"
            }
        }
    }
    redraw 1
    await 1000 / 60
    if ( var_25 == 0 ) {
        var_26++
    }
    goto *label_2
*label_3
    // /*09 20 00D9*/ var_32 var_33 3.0
    if ( var_32 != (-1) ) {
        repeat 10
            var_34 = cnt
            repeat 10
                if ( var_35(var_34, cnt) == var_32 ) {
                    if ( var_36(var_34, cnt) == 0 ) {
                        // /*09 20 007D*/ var_35 ( var_34 cnt ) 1.0 0.0
                    }
                    else {
                        // /*09 20 0070*/ var_35 ( var_34 cnt )
                        var_35(var_34, cnt) = -1
                        // /*09 20 0078*/ var_37 4.8 15790208
                        // /*09 20 00B2*/ var_37 1.0 0.1 1.0
                        // /*09 20 00DA*/ var_37 var_10
                        // /*09 20 00B0*/ var_37 -22.0 cnt 5 * + -0.25 -12.0 var_34 5 * +
                    }
                }
            loop
        loop
    }
    return
*label_4
    // /*09 20 0080*/ var_33 var_38 var_27 var_39
    var_18 = var_38 - var_15
    var_30 = var_39 - var_16
    var_15 = var_38
    var_16 = var_39
    // /*09 20 00B0*/ var_40 var_15 var_27 1.0 - var_16
    // /*09 20 00B9*/ var_40 0 var_17
    // /*09 20 00B9*/ var_33 0 var_17
    if ( var_23 == 1 & var_27 > 1.2 ) {
        var_23 = 2
    }
    if ( var_23 == 2 & var_27 < 1.2 ) {
        var_23 = 0
        // /*09 20 00FA*/ var_40 "stop" 1
        var_22 = 0
    }
    if ( var_24 == 1 ) {
        if ( var_23 == 0 ) {
            // /*09 20 00F2*/ var_33 0 var_21 sin ( var_17 ) * 0.0 var_21 cos ( var_17 ) *
        }
        var_41 = var_18 * var_18 + var_30 * var_30
        // /*09 20 00F2*/ var_33 0 var_41 sin ( var_17 3.14159265358979 + ) * 0.0 var_41 cos ( var_17 3.14159265358979 + ) *
    }
    else {
        if ( var_23 == 0 ) {
            // /*09 20 00F2*/ var_33 0 0.0 -10.0 0.0
        }
    }
    if ( var_27 < (-5.0) & var_25 == 0 ) {
        // /*09 20 00F0*/ var_33 0 0
        // /*09 20 00F3*/ var_40 0.0
        var_25 = 1
    }
    if ( var_16 > 35.0 & var_25 == 0 ) {
        // /*09 20 00F0*/ var_33 0 0
        // /*09 20 00F3*/ var_40 0.0
        var_25 = 2
    }
    var_28 = var_15 - var_14 * sin(var_20)
    var_29 = var_16 - var_14 * cos(var_20)
    // /*09 20 00B0*/ 1048578 var_28 8 var_29
    // /*09 20 007C*/ 1048578 var_15 3.0 var_27 + var_16
    return
*label_5
    getkey var_42, 87
    getkey var_43, 65
    getkey var_44, 68
    if ( var_23 == 0 ) {
        if ( var_43 ) {
            var_17 += 0.04
        }
        else {
            if ( var_44 ) {
                var_17 -= 0.04
            }
        }
        if ( var_42 ) {
            if ( var_22 != 1 ) {
                // /*09 20 00FA*/ var_40 "run" 1
                var_22 = 1
            }
            var_24 = 1
        }
        else {
            var_24 = 0
            if ( var_22 == 1 ) {
                if ( var_43 == 0 & var_44 == 0 ) {
                    // /*09 20 00FA*/ var_40 "stop" 1
                    var_22 = 0
                }
                else {
                    // /*09 20 00FA*/ var_40 "walk" 1
                    var_22 = 2
                }
            }
            else {
                if ( var_43 == 0 & var_44 == 0 ) {
                    if ( var_22 != 0 ) {
                        // /*09 20 00FA*/ var_40 "stop" 1
                        var_22 = 0
                    }
                }
                else {
                    if ( var_22 == 0 ) {
                        // /*09 20 00FA*/ var_40 "walk" 1
                        var_22 = 2
                    }
                }
            }
        }
    }
    stick var_45, 16
    if ( var_45 & 16 ) {
        if ( var_23 == 0 ) {
            // /*09 20 00F2*/ var_33 1 0 6 0
            // /*09 20 00FA*/ var_40 "jump" 1
            var_23 = 1
        }
    }
    return
*label_6
    // /*09 20 0077*/ var_46 50 10 4227312
    // /*09 20 00B0*/ var_46 0.0 0.0 -20.0
    // /*09 20 007D*/ var_46 0 0.8
    // /*09 20 0077*/ var_47 50 10 4227312
    // /*09 20 00B0*/ var_47 0.0 0.0 40.0
    // /*09 20 007D*/ var_47 0 0.8
    // /*09 20 0075*/ var_40 "res/tamane"
    // /*09 20 00B2*/ var_40 0.01 0.01 0.01
    // /*09 20 00F7*/ var_40 "stop" 1000 3000
    // /*09 20 00F7*/ var_40 "run" 5000 5700
    // /*09 20 00F7*/ var_40 "walk" 4000 4800
    // /*09 20 00F7*/ var_40 "jump" 140 140
    // /*09 20 00FA*/ var_40 "stop" 1
    // /*09 20 0078*/ var_33 2.0 16711680
    // /*09 20 00B2*/ var_33 0.8 1.0 0.8
    // /*09 20 00B0*/ var_33 var_15 2.0 var_16
    // /*09 20 00DA*/ var_33 var_11 var_10
    // /*09 20 007D*/ var_33 1 0.8
    // /*09 20 00F0*/ var_33 5 0.0 -9.8 0.0
    if ( var_13 ) {
        // /*09 20 00F3*/ var_33 50.0
    }
    else {
        // /*09 20 00F3*/ var_33 0.0
    }
    dim var_35, 10, 10
    dim var_48, 10, 10
    dim var_36, 10, 10
    repeat 10
        var_34 = cnt
        repeat 10
            // /*09 20 0078*/ var_35 ( var_34 cnt ) 4.8 14708784 rnd ( 16 ) 65536 * + rnd ( 16 ) 256 * + rnd ( 16 ) +
            // /*09 20 00B2*/ var_35 ( var_34 cnt ) 1.0 0.1 1.0
            // /*09 20 00DA*/ var_35 ( var_34 cnt ) var_10
            // /*09 20 00B0*/ var_35 ( var_34 cnt ) -22.0 cnt 5 * + -0.25 -12.0 var_34 5 * +
            if ( rnd(3) == 0 ) {
                var_36(var_34, cnt) = 1
                // /*09 20 0077*/ var_48 ( var_34 cnt ) 4.8 4.8 65280
                // /*09 20 00B0*/ var_48 ( var_34 cnt ) -22.0 cnt 5 * + 0.0 -12.0 var_34 5 * +
                // /*09 20 007D*/ var_48 ( var_34 cnt ) 0 0.8
                if ( var_13 ) {
                    // /*09 20 00F3*/ var_48 ( var_34 cnt ) 50.0
                }
                else {
                    // /*09 20 00F3*/ var_48 ( var_34 cnt ) 0.0
                }
            }
        loop
    loop
    return

上記ツールから吐き出されたhspファイルを解析する
42-44行目を見るとコマンドラインにdebugを渡して実行するとデバッグモードに入れると分かる
デバッグモードでは座標や経過時間を確認することができる
ここで重要なのが経過時間が分かるようになるという点
経過時間の値が分かれば、デバッガでその値で検索することによって経過時間のアドレスを特定できる
そして、そのアドレスの値を書き換えることで経過時間を任意の値に設定し直せる

つまり、根性でゴール→経過時間書き換えとすればflagが得られる

経過時間のアドレスは 現在時刻よりちょっと高い値(操作時間による遅延のフォロー)以下で検索→現在時刻がさっき検索した値以上になったら条件を以上にして再検索→増加を複数回押す とすると候補が3つくらいに絞れるので、それらのアドレスの値を片っ端から書き換えてみてtimeの変化を見れば特定できる

あとは根性でゴールして特定したアドレスを0にすればflag

result

KosenCTF{Let's_play_a_game_other_than_CTF}

ちなみにゴールしたかどうかの値が leftTime+0x54の位置に入っており、そこの値を2にすればゴールしたのと実質的に一緒になるのでわざわざ根性でゴールする必要はなかったりする