TSG CTF 2021 writeup (UB#)

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が求められるので、表示されたコマンドを打って得た結果をそのまま送ります。

```shell
$ 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`を呼び出しているので、その中身を見てみます。


```python: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`は次のような中身になっています。

```csharp: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<T> action)とすることで、各要素ごとにActionが実行されます。つまり、

```csharp
            System.Action<int>  b = x=>{
$user_input
            };
```
要素1つにつき1回$user_inputに書かれたプログラムが実行されるということです。(`x`に要素の中身が代入される)

-----

単純に$user_inputを

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

```python
blacklist_words = ['`', '@', '$', '\\', '#', '"', "'", "//", "/*", "using", "unsafe",
                   "Reflection", "IO", "File", "Console", "Net",
                   "Accessibility", "Microsoft", "System", "UI"]
```
そのため、$user_inputを
`System.Console.WriteLine("$password");`にしたり、メモリを直接触ったり、Refrectionでprivate変数を触ったり、というのは難しそうです。

------
では、どのようにListの変更を検知して`InvalidOperationException`を出しているのか見ていきましょう。
ググってみると[Listのソースコード](https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs)があったのでそれを見てみます。
Foreachの中身は以下のようになっていました。

```csharp: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`を呼び出した後、`_version`と`version`が一致していないと`InvalidOperationException`を出すようになってました。`_version`はint型なんですね。
では、`_version`がどのように設定されてるのかを調べていきます。

```csharp: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を以下のようにしてみます。

###解答
```csharp:$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}
```