プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

Beginners CTF 2019 write-up

Beginners CTF 2019に、一人チーム「rotation」で参加しました。 CTFはksnctfに取り組んでいましたが、コンテストへの参加は初めてです。 普段C#を書いているので、ちょっとしたことにはC#を使いました。

環境: Win10 Version 1809 + Windows Subsystem for Linux(Ubuntu 18.04.2 LTS)

成績

  • 123rd place
  • 824 points

解けた問題

Reversing [warmup] Seccompare

  • fileで調べると ELF 64-bit LSB executable
  • コマンドライン引数で与える文字列がフラグかどうかを判定するらしい
  • 最初はobjdump -dでアセンブリを読もうと思っていましたが、IDAでもELFを読めたのでそれをもとに解析
  • main()でスタック領域にbyte単位でmvしてstrcmpしているのが見えたので、そのbyteの内容をASCIIにするとflagが出ました: ctf4b{5tr1ngs_1s_n0t_en0ugh}

Reversing Leakage

  • これもSeccompareと同様にELF 64-bit LSB executableで、コマンドライン引数で与える文字列がフラグかどうかを判定するものらしい
  • main()からis_correct()を呼び出して、is_correct()が非0を返すとcorrectになる
  • is_correct()冒頭でstrlenで文字数チェックしている、22h=34文字がflagの文字数になる
  • その後、内部で持っているflagを1文字ずつconvert()して、コマンドライン引数と一致するかのループに入る
  • convert()の中身は難しくて分からない
  • convert()の戻り値がflagになるので、 gdbで不一致時の分岐にbreakpointを貼って[rbp-5]を出力させれば良さそう

との道筋に、いくつかの試行錯誤の末に到達したので

b *0x400648
command 1
printf "%c\n", *(char*)($rbp-5)
end

の初期設定をした後に

set args (34文字固定、判明したものを順次反映)
r

で1文字ずつ確定していきました: ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

履歴をさかのぼって引数設定、rで最初から実行、をひたすら手入力していたので腕が痛くなりそうでした。

Crypto [warmup] So Tired

  • tar.gzを展開するとBASE64の長いものが出てきた
  • base64 -d fileするとzlib圧縮のものが出てきた
  • zlib-flate -uncompress <fileするとまたBASE64の長いものが出てきた
  • 更にbase64 -d fileしようとするとbase64: write error: Input/output errorエラーで処理できず、しばらく嵌まる
  • Base64の文字数ではないのか?と末尾に=を足したり、先頭に適当な文字を足したりしたけれど、同様にエラーになる
  • 試しにC#プログラムでBase64のデコードを書いたらエラーなしにデコード成功、なぜbase64ではエラーになったのか……(未調査)
  • 改めて、base64の長いものをデコードするとzlib圧縮のが出てきた
  • (ここで20回ほど、baze64とzlibのデコードを繰り返す)
  • 流石に手作業でのデコードに疲れてきたのでスクリプトを書くことに決める
  • 最初から数えて、zlibとbase64のデコードを各500回した時点でフラグが出てきました: ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}
// usingは省略
namespace MyCtfSharp
{
    internal class Program
    {
        private static void Main()
        {
            var bytes = Convert.FromBase64String(Console.ReadLine());
            using (var writer = new BinaryWriter(Console.OpenStandardOutput()))
            {
                writer.Write(bytes, 0, bytes.Length);
            }
        }
    }
}
#!/bin/sh

# 途中まで手動で進めていたので、途中からの番号になっています
index=20
while :
do
    next=`expr $index + 1`
    ./MyCtfSharp.exe <"$index.bin" >"$next.bin" || exit
    # read key

    index=`expr $next + 1`
    zlib-flate -uncompress <"$next.bin" >"$index.bin" || exit
    # read key
done

Misc [warmup] Welcome

  • IRCにログインする問題
  • LimeChatというソフトがIRCの定番らしいですが、インストールしてみるも使い方が分からず
  • どうしようかと考えていたら ルールのページ のIRC箇所にリンクが張ってあるのに気づく
  • https://webchat.freenode.net/?channels=seccon-beginners-ctf にアクセスしてログイン成功
  • 上の方に説明とフラグが書かれていました: #seccon-beginners-ctf: 競技に関する質問等はこちらで受け付けます FLAG: ctf4b{welcome_to_seccon_beginners_ctf}

Misc containers

  • DLした内容をfileするとdataの一言
  • binwalk fileするとPNG画像がいっぱいあるらしい
  • binwalkの結果をもとにddするスクリプトを書いて抽出
#!/bin/sh
index=0
for begin in 16 738 1334 1914 2856 3666 4354 5156 5846 6722 7757 8338 9243 10319 11042 12118 12809 13845 14592 15535 16440 17313 18218 19123 19926 20869 21742 22465 23408 23989 24810 25753 26788 27599 28504 29085 29808 30844 31524
do
index=`expr $index + 1`
# echo "$index.png $begin"
`dd if=e35860e49ca3fa367e456207ebc9ff2f_containers of=$index.png bs=1 skip=$begin`
done

count引数を指定していないので各png画像に余分な内容も含まれることになりますが、画像内容としては問題ないので気にせず作成しました。 作成した画像をexplorerのサムネイル表示で並べて、フラグを読みました。(flagはランダムな英数字のようなので省略)

他の方のwrite-upを読んで知りましたが、foremostというコマンドを使えば画像展開まで一括で全部やってくれます。

Misc Dump

  • DLした内容をfileするとtcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)とのこと
  • Wiresharkで開くとHTTP通信が見える
  • No.28のパケットにhttp://192.168.75.230/webshell.php?cmd=hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flagのリクエストがある
  • No.3193のパケットにレスポンスが乗っている
  • 8進数の復元方法どうしよう→C#プログラム書こう
// usingやらMain()やら省略
string oct = (レスポンスの内容の8進数部分の文字列);
using (var stream = File.Create("ctf4b.bin"))
{
    foreach (var x in oct.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s, fromBase: 8)))
    {
        stream.Write(new byte[] { (byte)x }, 0, 1);
    }
}

復元した内容をfileで調べるとgzip compressed dataだったので展開するとflag.jpgが出てきました: ctf4b{hexdump_is_very_useful}

Misc Sliding puzzle

  • 3x3のスライドパズルを解く問題
  • ソルバーを書く。盤面の状態を4bit*9個の36bitの1整数にまとめましたが、4bit単位で扱う箇所で盛大にバグらせました
  • ncで接続して盤面を目視確認してソルバーに入力し、ソルバーの出力を目視でncに入力、しても何も出てこない、しばらく嵌まる
  • しばらく試していると、接続して短時間で(5秒~10秒くらい?)で回答すると、次の問題や[-] Incorrect answer.の出力をもらえることに気づく
  • クリップボード経由でncとソルバーをやり取りしようとしましたが、どうにもnc(=ubuntu=WSL)側への貼り付けの際に10秒ほどフリーズする、そしてタイムアウトする
  • 「nc→ソルバー」はリダイレクト、「ソルバー→nc」はソルバー出力を手動で入力、で挑んでいると、数十問進んだあたりでタイプミスして悲しみに包まれる
  • coprocという機能を使えばncとソルバーの標準入出力をループさせられるらしいのですが、使い方が分からず
  • ソルバー側にSendKeys.SendWait()を仕込んでキーボードの自動入力をさせることで、「nc→ソルバー」はリダイレクト、「ソルバー→nc」は自動入力でする、という荒業を思いつく
  • 数十問解いたあたりでフラグが出てきました(内容はランダム英数字のようなので省略)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace MyCtfSharp
{
    internal class Program
    {
        private static void Main()
        {
            while (true)
            {
                var list = new List<int>();
                while (true)
                {
                    var line = Console.ReadLine();
                    Console.Error.WriteLine(line);
                    if (line.Contains("-")) { continue; }
                    if (line.Length == 0) { break; }

                    list.AddRange(line.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse));
                }
                var moves = new SlidingPuzzleSolver().Solve(list.ToArray());
                var res = string.Join(",", moves.Select(x => (int)x)) + Environment.NewLine;
                Console.Write(res);

                foreach (var key in moves
                    .Select(x => ((int)x).ToString())
                    .SelectMany(x => new[] { ",", x })
                    .Skip(1)
                    .Select(key => "{" + key + "}")
                    .Concat(new[] { "{ENTER}" })) // これが何故か効いてなかったのでEnterは自分で入力
                {
                    SendKeys.SendWait(key);
                }
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace MyCtfSharp
{
    public enum Movement
    {
        Up,
        Right,
        Down,
        Left,
    }

    public class SlidingPuzzleSolver
    {
        public Movement[] Solve(int[] initialState)
        {
            // 00 01 02
            // 03 04 05
            // 06 07 08
            var dx = new[] { 0, 1, 0, -1, };
            var dy = new[] { -1, 0, 1, 0, };
            // 0~8は4bitでエンコード出来て、4bit*9マスの36bitあれば事足りる
            var dict = new Dictionary<ulong, ImmutableStack<Movement>>();
            var expectedState = Encode(Enumerable.Range(0, 9).ToArray());

            var queue = new Queue<ulong>();
            queue.Enqueue(Encode(initialState));
            while (queue.Count > 0)
            {
                var current = queue.Dequeue();
                if (!dict.TryGetValue(current, out var moves))
                {
                    moves = ImmutableStack.Create<Movement>();
                    dict.Add(current, moves);
                }

                if (current == expectedState) { return moves.Reverse().ToArray(); } // stackなので最後の操作が最初に来ているので裏返す

                int zeroPos = GetZeroPosition(current);
                int cx = zeroPos % 3;
                int cy = zeroPos / 3;
                for (int i = 0; i < 4; i++)
                {
                    int nx = cx + dx[i];
                    int ny = cy + dy[i];
                    if (nx < 0 || ny < 0 || nx >= 3 || ny >= 3) { continue; }

                    ulong next = Swap(current, zeroPos, ny * 3 + nx);
                    if (dict.ContainsKey(next)) { continue; }
                    dict.Add(next, moves.Push((Movement)i));
                    queue.Enqueue(next);
                }
            }

            throw new InvalidOperationException("解けませんでした。");

            ulong Encode(int[] field)
            {
                ulong result = 0;
                foreach (var x in field)
                {
                    result <<= 4;
                    var unsined = (uint)x;
                    result |= unsined;
                }
                return result;
            }

            int GetZeroPosition(ulong state)
            {
                for (int i = 0; i < 9; ++i)
                {
                    if (((state >> (i * 4)) & 0xF) == 0) { return 8 - i; }
                }
                throw new ArgumentException(nameof(state));
            }

            ulong Swap(ulong state, int p1, int p2)
            {
                p1 = 8 - p1;
                p2 = 8 - p2;
                ulong x1 = (state >> (p1 * 4)) & 0xF;
                ulong x2 = (state >> (p2 * 4)) & 0xF;
                return state
                    & (~((ulong)0xF << (p1 * 4)))
                    & (~((ulong)0xF << (p2 * 4)))
                    | (x1 << (p2 * 4))
                    | (x2 << (p1 * 4));
            }
        }
    }
}

解けなかった問題

Reversing Linear Operation

  • これもELF 64-bit LSB executableだけれど、文字列の与え方がコマンドライン引数ではなく標準入力になっている
  • main()からis_correct()を呼び出して、is_correct()が非0を返すと"correct"になる
  • is_correct()のGraph overviewがものすごく斜めな感じになってる、複雑そう
  • 最初の方を見るとctf4b{}の判定をしている、'}'の位置からflagが63文字だとわかる
  • その後はflag内の特定2文字を延々とビット操作などして特定の数値になるかを判定している、まともに読みたくない
  • 「特定2文字をあれこれして特定の数値になるなら次へ、そうでないなら外れルートへjmp」なので、次へ行くところでブレークさせて2文字を特定できれば、と考える
  • IDAの「Edit/Patch Program/Change byte...」からINT 3の0xCCを適宜入れて判定できるようにしよう

英数字36文字+"_"の37文字が候補だとして、2文字全探索は1369通りなので自動化したくなりました:

#!/bin/sh
flags="
ctf4b{393..l1C.30..u.ix..c._.......ve.........0....0.0...Nt..0}
ctf4b{393..l1C.30..u.ix..c._.......ve.........0....1.0...Nt..0}
(snip)
ctf4b{393..l1C.30..u.ix..c._.......ve.........z....y.0...Nt..0}
ctf4b{393..l1C.30..u.ix..c._.......ve.........z....z.0...Nt..0}
"

for flag in ${flags};
do
echo $flag
echo $flag | ./linear_operation || exit
done

候補文字列のリストはC#プログラムで適当に出力させました。 それで、このようにすれば少しずつflag文字列が特定できる……と思っていたのですが、どうにも途中の分岐から先に進めなくなりました。

  • 使った自動化スクリプトは、次へ進める入力値が1つになる前提で書いていましたが、実際は複数の入力値で突破できたのではないか?
  • 複数の候補があるにもかかわらず、1つ見つかっただけで打ち切っていたので、誤ったflag文字列を構築していたのではないか?

という点に終了後に気づきました。

angrというツールを使えば解けて、正しいフラグは ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n} とのことです。とっても便利そう……。

Pwnable [warmup] shellcoder

warmupなのに319ptもある……。 とりあえずIDAで見ると、入力に"binsh"のいずれかが含まれるとInvalid扱いされる、という点までは判明。ただPwnの知識が全く無いのでそれ以降何も進められず。

Crypto Party

2次式3つの連立方程式になることまでは分かりましたが、数が巨大であるのも相まって思考が止まって何も出来ず。