LoginSignup
1
1

More than 1 year has passed since last update.

TSG CTF 2021 writeup (UB#)

Last updated at Posted at 2021-10-04

TSG CTF 2021にチームで参加しました。CTFの参加は人生で2回目です。
1問だけですが解けたので、解き方と解答を書きます。

[Misc]UB#

278pts(10 solves)

問題

Exception of C# is perfect, isn't it?
nc 34.146.161.123 2000
ubsharp.tar.gz

解答だけ見る

解き方

C#の問題です。最初にProof of Workが求められるので、表示されたコマンドを打って得た結果をそのまま送ります。

$ nc 34.146.161.123 2000
 [Proof of Work]
 Submit the token generated by `hashcash -mb27 mnbavtuokb`
 1:27:211004:mnbavtuokb::
 matched token: 1:27:211004:mnbavtuokb::
 check: ok
 read until 'END\n'

サーバ側で動いているプログラムのソースコードはubsharp.tar.gzにあるので、中身を見てみます。
start.shが最初に呼び出されます。start.shではProof of Workが終わった後にserver.pyを呼び出しているので、その中身を見てみます。

server.py(抜粋)
    template_path = "./template.cs"
    with open(template_path, 'r') as f:
        template_txt = f.read()
    print(hello_output)
    tmp = ""
    while tmp != "END":
        user_input += tmp+'\n'
        tmp = input()
        if len(user_input) > PROGRAM_MAX_LEN:
            print("too long!!")
            sys.exit()
    if not sanitizer(user_input):
        print("Evil input")
        sys.exit()
    s = Template(template_txt)
    user_output = (s.substitute(user_input=user_input, password=password))

上記の部分で、C#のプログラムが書かれたtemplate.csを読み込んで、\$user_inputをユーザが入力した文字列、\$passwordを"予め生成したランダムな文字列"に置き換えています。server.pyは、上記の処理の後、template.csをコンパイル・実行し、実行結果として"予め生成したランダムな文字列"が出力されれば、フラグを得られます。
template.csは次のような中身になっています。

template.cs
namespace test
{
    class Program
    {
        public readonly System.Collections.Generic.List<int> a = new System.Collections.Generic.List<int>();
        public Program(){
            a.Add(1);
            a.Add(2);
        }
        public void Run(){
            System.Action<int>  b = x=>{
$user_input
            };
            try {
                a.ForEach(b);
                if(a.Count!=2||(a.Count>0&&a[0]!=1)){
                    System.Console.WriteLine("$password");
                }
            } catch (System.Exception e) {
                System.Console.WriteLine("failed");
            }
        }
        static void Main(string[] args)
        {
            var p = new Program();
            p.Run();
        }
    }
}

List型の変数aがあり、ForEachが行われている間にaのアイテムの数を変えるか、a[0]を1以外にすれば、$passwordが出力され、フラグが得られます。Foreach(Action action)とすることで、各要素ごとにActionが実行されます。つまり、

            System.Action<int>  b = x=>{
$user_input
            };

要素1つにつき1回$user_inputに書かれたプログラムが実行されるということです。(xに要素の中身が代入される)


単純に$user_inputを

$user_input
a[0]=2;

のようにすれば良いのではという考えが最初に浮かびますが、C#ではForeachをしているときにそのListの要素を変更するとInvalidOperationExceptionが発生してしまいます。(template.csではこの例外エラーがtry-catchで補足され、パスワードの代わりにfailedが出力されてしまう)
そのため、この例外を発生させずにaを変更するように、$user_inputに書くプログラムを考えないといけません。
また、server.pyは入力した$user_inputに対して、以下の単語が入っているとrejectするようにサニタイジングが入っています。

blacklist_words = ['`', '@', '$', '\\', '#', '"', "'", "//", "/*", "using", "unsafe",
                   "Reflection", "IO", "File", "Console", "Net",
                   "Accessibility", "Microsoft", "System", "UI"]

そのため、$user_inputを
System.Console.WriteLine("$password");にしたり、メモリを直接触ったり、Refrectionでprivate変数を触ったり、というのは難しそうです。


では、どのようにListの変更を検知してInvalidOperationExceptionを出しているのか見ていきましょう。
ググってみるとListのソースコードがあったのでそれを見てみます。
Foreachの中身は以下のようになっていました。

list.cs(抜粋)
       public void ForEach(Action<T> action) {
            if( action == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            Contract.EndContractBlock();

            int version = _version;

            for(int i = 0 ; i < _size; i++) {
                if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5) {
                    break;
                }
                action(_items[i]);
            }

            if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
                ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
        }

最初に_versionという変数をversionに記録しておいて、各要素に対してactionを呼び出した後、_versionversionが一致していないとInvalidOperationExceptionを出すようになってました。_versionはint型なんですね。
では、_versionがどのように設定されてるのかを調べていきます。

list.cs(抜粋)
    public T this[int index] {
            get {
                // Following trick can reduce the range check by one
                if ((uint) index >= (uint)_size) {
                    ThrowHelper.ThrowArgumentOutOfRangeException();
                }
                Contract.EndContractBlock();
                return _items[index]; 
            }

            set {
                if ((uint) index >= (uint)_size) {
                    ThrowHelper.ThrowArgumentOutOfRangeException();
                }
                Contract.EndContractBlock();
                _items[index] = value;
                _version++;
            }
        }

        public void Add(T item) {
            if (_size == _items.Length) EnsureCapacity(_size + 1);
            _items[_size++] = item;
            _version++;
        }
        public void Clear() {
            if (_size > 0)
            {
                Array.Clear(_items, 0, _size); // Don't need to doc this but we clear the elements so that the gc can reclaim the references.
                _size = 0;
            }
            _version++;
        }

Listを変更するメソッドをいくつか抜粋しました。いずれも_versionを単純にインクリメントさせていました。
ということは、_versionをオーバーフローさせて、Foreachが始まる前に記録されたversionに合わせれば変更の検知を回避できそうです。
なので、$user_inputを以下のようにしてみます。

解答

$user_input
if(x==1) for (long i = 0; i <= uint.MaxValue; i++) { a[0] = 2; }

uintの最大値+1だけa[0]=2するプログラムです。これだけでも結構実行に時間がかかるので、念のためif(x==1)を追加して、Listの最初の要素に対してだけ実行するようにしました。

ログ
$ nc 34.146.161.123 2000
[Proof of Work]
Submit the token generated by `hashcash -mb27 mnbavtuokb`
1:27:211004:mnbavtuokb::
matched token: 1:27:211004:mnbavtuokb::
check: ok
read until 'END\n'
if(x==1) for (long i = 0; i <= uint.MaxValue; i++) { a[0] = 2; }
END
following program is run
namespace test
{
    class Program
    {
        public readonly System.Collections.Generic.List<int> a = new System.Collections.Generic.List<int>();
        public Program(){
            a.Add(1);
            a.Add(2);
        }
        public void Run(){
            System.Action<int>  b = x=>{

if(x==1) for (long i = 0; i <= uint.MaxValue; i++) { a[0] = 2; }

            };
            try {
                a.ForEach(b);
                if(a.Count!=2||(a.Count>0&&a[0]!=1)){
                    System.Console.WriteLine("hdzybrdhxxdaoggnymbjbntkxuymcnzaxslxubxxwdyvmbwbdqmduhovxpnj");
                }
            } catch (System.Exception e) {
                System.Console.WriteLine("failed");
            }
        }
        static void Main(string[] args)
        {
            var p = new Program();
            p.Run();
        }
    }
}

Congratulations! TSGCTF{eXc6ptiOn_1s_w0rk1ng_ExcePt_uNdeFineD_beh@vi0uR}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1