VB.NETは解析されやすいのか?(その1)
はじめに
C#やVBなどの.NET言語はリバースエンジニアリングに弱いと言われています。 ちょっとした用事でソースコードを紛失したライブラリを解析してその事を実感したので今回はその事です。
イントロ
それではまず、VB*1のコンパイルの過程をおさらいしましょう。
- VBでコードを書く
- VBコンパイラでVBのコードからCIL(Common Intermediate Language)に変換される
- コンパイル結果がファイルに格納される(ここまでがコンパイル時)
- プログラムがメモリにロードされる
- メソッド単位で初めて呼び出されるタイミングでCPU命令に変換される
という流れになっています。
また、自己反映計算《†リフレクション†》に対応するため3の時点で生成されるファイルにはクラス名はメソッド名などの情報が埋め込まれています。 そのため、メソッド名をそのまま読み取ることが可能であり、読みやすいコードを書いている場合はクラス名やメソッド名から役割や機能を類推する事も出来ます。
このブログは取り合えずやってみようぜの精神なので、とりあえずテキトーなアプリケーションを解析してみましょう。
ここで想定するアプリケーションは以下の条件を満たすものとします。
- すべてマネージで構成されている
- 難読化されていない
機能をP/Invokeに委譲してたりみんな大好き異形言語C++/CLIで実装されてたりとネイティブが含まれていると難易度が一気に跳ね上がります。 また、難読化されていると文字列が暗号化されて格納されたり、メソッドのオーバーロードをこれでもかと言わんばかりに突っ込まれたり、識別子を紛らわしい文字に置き換えらえたり、フローを書き換えて逆コンパイラを殺しにかかったりとこちらも鬼畜な感じになるのでやるなら気合を入れてとりかかってくだしあ。
世の中にはC#向け(一部VBにも対応)している逆コンパイラがあるので、弊社の知っている範囲ですが一応紹介します。今回は使いませんが
- ILSpy
- dotPeek
- .NET Refactor
- RedGate社の逆コンパイラ
- 有償。
- 使ったことがないので正直よくわからない。
解析
とりあえずテキトーなアプリケーションとしてSuperUsefulApplication.exe
というコンソールアプリケーションを用意しました。
パスワードを入力してください >password パスワードが違います
まず、起動するとパスワードを求められます。初めにこれを突破してみましょう。
逆アセンブルしておきましょう。
>ildasm SuperUsefulApplication.exe /utf8 /out=SuperUsefulApplication.il
すると以下のファイルが生成されるはずです。
SuperUsefulApplication.il SuperUsefulApplication.res SuperUsefulApplication.Resources.resources
ここで興味があるのはSuperUsefulApplication.il
だけですので、他のはとりあえず気にしない方向で。
上記の動作例を見るとパスワードを入力させ判定するコードの周辺に「パスワードを入力してください」という文字があるはずです。 .NET Frameworkの内部コードはUTF-16 LEですので、
D1 30 B9 30 EF 30 FC 30 C9 30 92 30 65 51 9B 52 57 30 66 30 4F 30 60 30 55 30 44 30
というバイトアレイがリテラルとして近くにあればいいな~という願望を抱きながら検索します。
.entrypoint .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) // コード サイズ 188 (0xbc) .maxstack 4 .locals init (class [System.Core]System.Security.Cryptography.SHA1Cng V_0, class SuperUsefulApplication.SuperUsefulClass`1<string> V_1, string V_2) IL_0000: newobj instance void [System.Core]System.Security.Cryptography.SHA1Cng::.ctor() IL_0005: stloc.0 // パスワードを入力してください IL_0006: ldstr bytearray (D1 30 B9 30 EF 30 FC 30 C9 30 92 30 65 51 9B 52 57 30 66 30 4F 30 60 30 55 30 44 30 ) IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: ldstr ">" IL_0015: call void [mscorlib]System.Console::Write(string) IL_001a: ldstr "Z454hURH5p7ckn+/aqZMFTQ7J/o=" IL_001f: ldloc.0 IL_0020: call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8() IL_0025: call string [mscorlib]System.Console::ReadLine() IL_002a: callvirt instance uint8[] [mscorlib]System.Text.Encoding::GetBytes(string) IL_002f: callvirt instance uint8[] [mscorlib]System.Security.Cryptography.HashAlgorithm::ComputeHash(uint8[]) IL_0034: call string [mscorlib]System.Convert::ToBase64String(uint8[]) IL_0039: ldc.i4.0 IL_003a: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string, string, bool) IL_003f: brfalse.s IL_0051 IL_0041: ldstr bytearray (D1 30 B9 30 EF 30 FC 30 C9 30 4C 30 55 90 44 30 // .0.0.0.0.0L0U.D0 7E 30 59 30 ) // ~0Y0 IL_0046: call void [mscorlib]System.Console::WriteLine(string) IL_004b: ldc.i4.0 IL_004c: call void [mscorlib]System.Environment::Exit(int32) IL_0051: ldstr bytearray (88 30 46 30 53 30 5D 30 ) // .0F0S0]0 IL_0056: call void [mscorlib]System.Console::WriteLine(string)
ありました。今回はツイてます。
ILコードの詳細は皆さんの脳内評価スタックを信じて詳しくは解説しませんが、以下の流れでパスワードを検証していることが分かります。
パスワードがプログラム中に直接埋め込まれていた場合、この時点でパスワードが判明します。まぁ、普通はそんなことはあり得ませんが。 SHA1を突破するのはあきらめてこの検証ロジック自体を迂回する事とします。
IL_004c
までが検証ロジックのようですので、プログラムの先頭で直接IL_0051
にジャンプすれば何とかなりそうです。
ということでほい。
.entrypoint .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) // コード サイズ 188 (0xbc) .maxstack 4 .locals init (class [System.Core]System.Security.Cryptography.SHA1Cng V_0, class SuperUsefulApplication.SuperUsefulClass`1<string> V_1, string V_2) br.s IL_0051 // ここを追加 IL_0000: newobj instance void [System.Core]System.Security.Cryptography.SHA1Cng::.ctor() IL_0005: stloc.0
アセンブルして確認してみましょう。
>ilasm SuperUsefulApplication.il /exe /debug=impl /resource=SuperUsefulApplication.res /output=SuperUsefulApplication2.exe
一応peverify
で生成されたプログラムを確認しておきましょう。
>peverify SuperUsefulApplication2.exe Microsoft (R) .NET Framework PE Verifier バージョン 4.0.30319.0 Copyright (c) Microsoft Corporation. All rights reserved. 含まれるすべてのクラスおよびメソッドSuperUsefulApplication2.exe 確認しました。
問題なさそうですね。それでは起動してみましょう。
ようこそ >
期待通りに検証ロジックを迂回できています。
終わりに
今回は割と長くなってしまったので、いったんここで区切っておきます。 気が向いたらこの続きについて書こうと思います。
余談ですが、サンプルプログラムで使用しているCryptography API: Next GenerationはWindows Vista以降でしか動きません。 XP? なにそれ? おいしいの?
つづく
VB13以前でもNull条件演算子を使いたい
はじめに
ちょっと前にVB14の新機能についての話をしたじゃないですか。 その中にNull条件演算子なんてものがあったと思います。
弊社はEntity FrameworkみたいなライブラリでFirstOrDefault
みたいなクエリを割合ぽいぽい投げ返ってきた値がNothing
かそうでないかで処理を分岐させることが多いのですが、
Dim hoge = db.Hoges.Where(Function(it) it.UniqueKey = "HOGE").FirstOrDefault() Dim hogeName As String If Not hoge Is Nothing Then hogeName = hoge.Name Else hogeName = Nothing End If
みたいなコードをほぼ毎回のように書かなくてはならず非常にげんなりします。 まぁ、設計がクソなのも一理ありますが。
そこでNull条件演算子を使いたいのですが、現在使用している環境では使えないという最高にクソな感じに仕上がっており*1日夜冗長でクソなコードを生成している次第でございます。うおォン 俺はまるでクソコードジェネレータだ
あああああ
かくなる上はメタプログラミングを用いてごり押しで解決したいところなのですが、さすがに構文に介入することはできません。 コンパイル時に介入してASTを弄れれば話は別かもしれませんが。
Roslyn? なにそれ? おいしいの?
というわけで拡張メソッドでそれっぽく仕上げてお茶を濁すほかありません。
というわけでほい。
Module NullableExtension <Extension> Public Function N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult)) As TResult If value Is Nothing Then Return Nothing Else Return expr(value) End If End Function <Extension> Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult)) As Nullable(Of TResult) If value Is Nothing Then Return Nothing Else Return expr(value) End If End Function <Extension> Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult))) As Nullable(Of TResult) If value Is Nothing Then Return Nothing Else Return expr(value) End If End Function End Module
とくに解説することも無いのですが、
N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult))
と
N(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))
がオーバーロード不可なのはなんとなくわかりますが、
Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))
と
Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult)))
はオーバーロードできるんですね。驚きです。
んで、こんな感じに使います。
Dim a = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = 0} Dim b = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = Nothing} Dim c As Hoge Console.WriteLine(a.Ns(Function(it) it.Id)) Console.WriteLine(a.N(Function(it) it.Name)) Console.WriteLine(a.Ns(Function(it) it.ParentId)) Console.WriteLine(b.Ns(Function(it) it.Id)) Console.WriteLine(b.N(Function(it) it.Name)) Console.WriteLine(b.Ns(Function(it) it.ParentId)) Console.WriteLine(c.Ns(Function(it) it.Id)) Console.WriteLine(c.N(Function(it) it.Name)) Console.WriteLine(c.Ns(Function(it) it.ParentId))
おわりに
なんというか、拡張メソッドの乱用ですよね。
Optional
みたいなアプローチではなくnull
だろうがなんだろうがゴリ押しで値を取得するみたいなアプローチ、私は結構好きです。
ただ、実際に他人のコードでこんなのを見たら『うわぁ・・・』は必至ですね。
取り合えず設計をもう少しどうにかしろよ的な。
余談ですが、VBのラムダってFunction(it) it...
みたいに冗長なのが嫌いなんですよね。
同じ意味を持つC#のそれと比較してもit => it.Name
とFunction(it) it.Name
の差の大きいこと。
まぁ、そもそもC#/VBののラムダの内部に可変ステートを抱えられる仕様はちょっとどうかな~と思います。 シングルスレッドならともかく、マルチスレッドでうっかり可変ステートを外部に出してしまうとスレッドセーフの要件の一つである『不変なステートは常にスレッドセーフ』が途端に崩壊してしまうのでマルチスレッドで使うときはハラハラします。
とにかく、Decimalの内部構造を取り出して桁数を解析する以来のバッドノウハウの再来ということで。
おわり
*1:そもそもYield Returnが使えない。複数行ラムダはぎりぎり使える。あっ(察し)
C#でもSigilでデリゲートをダイナミックに生成したい
はじめに
飽きもせずに.NET黒魔術シリーズです。
SigilといってもEPUBの方のSigilではないです。そっちの情報を求めていた人はまわれ右してお帰りください。
やる気が起きなかった時にネットサーフィンしていたところ、こんな記事を見つけました。
このライブラリはILGenerator
をラップし、ILの誤りを可能な限り早く検出しわかりやすいエラーメッセージを提供してくれます。
というよりもILGenerator
は実行時でないと誤りがあるかわからないのと、誤りがあります程度のメッセージしか提供してくれずかなりつらぽよポイントは高いです。
今回はSigilを用いて動的にToString
をするデリゲートを作成してみましょう。好きですね。ToString
を動的に生成するの。
どうでもいいのですが、作者の笑顔がいいですね。
お手本
とりあえず、ベースとなるILをコンパイラに生成してもらいましょう。
class Target { public int Id { set; get; } public virtual string Name { set; get; } public DateTime Hoge { set; get; } }
こんなクラスがあって、
class Program { static void Main(string[] args) { var target = new Target() { Id = 1, Name = "あああああ", Hoge = DateTime.Now }; Console.WriteLine(ToString(target)); } static string ToString(object value) { var target = (Target)value; var builder = new StringBuilder(); builder .Append(nameof(Target) + ":{") .Append(nameof(Target.Id) + "=") .Append(target.Id) .Append(",") .Append(nameof(Target.Name) + "=") .Append(target.Name) .Append(",") .Append(nameof(Target.Hoge) + "=") .Append(target.Hoge) .Append("}"); return builder.ToString(); } }
こんな感じにToString
するとしましょう。
アセンブリをビルドし、ildasmで逆アセンブルすると以下のコードを生成すればいいことがわかります。
.maxstack 2 .locals init ([0] class Samplejunk.Target target, [1] class [mscorlib]System.Text.StringBuilder builder, [2] string V_2) IL_0000: nop IL_0001: ldarg.0 IL_0002: castclass Samplejunk.Target IL_0007: stloc.0 IL_0008: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() IL_000d: stloc.1 IL_000e: ldloc.1 IL_000f: ldstr "Target:{" IL_0014: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_0019: ldstr "Id=" IL_001e: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_0023: ldloc.0 IL_0024: callvirt instance int32 Samplejunk.Target::get_Id() IL_0029: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) IL_002e: ldstr "," IL_0033: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_0038: ldstr "Name=" IL_003d: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_0042: ldloc.0 IL_0043: callvirt instance string Samplejunk.Target::get_Name() IL_0048: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_004d: ldstr "," IL_0052: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_0057: ldstr "Hoge=" IL_005c: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_0061: ldloc.0 IL_0062: callvirt instance valuetype [mscorlib]System.DateTime Samplejunk.Target::get_Hoge() IL_0067: box [mscorlib]System.DateTime IL_006c: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(object) IL_0071: ldstr "}" IL_0076: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string) IL_007b: pop IL_007c: ldloc.1 IL_007d: callvirt instance string [mscorlib]System.Object::ToString() IL_0082: stloc.2 IL_0083: br.s IL_0085 IL_0085: ldloc.2 IL_0086: ret
共通中間言語
C#とILのコードを比較しながら見てみましょう。
共通中間言語(Common Intermediate Language、略してCIL)はスタックベースの言語であり、オペランドをスタックへプッシュし命令を実行し計算結果をスタックから受け取ります。
ここではスタック遷移を以下の書式で表現します。
- スタックは左から右に伸びていく。つまり古い値が左で新しい値が右である。
- 左側のスタックが命令の実行前のスタックを表し、右側のスタックが命令の実行後のスタックを表す。
試しにやたら目につくcallvirt
を見てみましょう。
形式
callvirt <メソッド>
スタック遷移
..., <オブジェクト>, <引数1>, ..., <引数N> → ..., <返り値>(返り値のないメソッドの場合は無い)
callvirt
はメソッドを呼び出します。
また、virt
とついているあたり、コンパイル時のクラスに基づいたメソッド呼び出しではなく、実際のクラスに基づき呼び出すメソッドを決定します。
まぁ、要するにオーバーライドされている場合はオーバーライドしている派生先のメソッドが呼び出されるってことです。
callvirt
で呼び出す場合は最初に呼び出すオブジェクトをスタックへプッシュし、次に順に引数をスタックへプッシュしていきます。
呼び出し先のメソッドから制御が返されると、返り値がある場合はスタックの先頭に返り値が置かれます。
と、いうわけで先ほどのコードを先頭から見ていきましょう。
var target = (Target)value; // IL_0001: ldarg.0 // IL_0002: castclass Samplejunk.Target // IL_0007: stloc.0
ldarg.0
は0番目の引数をスタックへプッシュ(空) → object
castclass
でスタックの先頭のオブジェクトをキャストしスタックにプッシュobject → Target
stloc.0
でスタックの先頭のオブジェクトを0番目のローカル変数に代入Target → (空)
var builder = new StringBuilder(); // IL_0008: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() // IL_000d: stloc.1
newobj
でStringBuilder
のインスタンスを生成(空) → StringBuilder
stloc.1
でスタックの先頭のオブジェクトを1番目のローカル変数に代入StringBuilder → (空)
builder .Append(nameof(Target) + ":{") // IL_000e: ldloc.1 // IL_000f: ldstr "Target:{" // IL_0014: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
ldloc.1
で1番目のローカル変数をスタックにプッシュ(空) → StringBuilder
ldstr
で文字列定数"Target:{"をスタックにプッシュStringBuilder → StringBuilder, string
callvirt
でStringBuilder::Append(string)
を呼び出すStringBuilder
をスタックにプッシュするStringBuilder, string → StringBuilder
あとは似たようなコードなので割愛しますが、上のコードから以下のことが読み取れます。
StringBuilder::Append
の適切なオーバーロードがある場合はそのメソッドを呼び出す- 参照型で適切なオーバーロードがない場合は
StringBuilder::Append(object)
を呼び出す - 値型で適切なオーバーロードがない場合は
box
でボックス化を施したのちにStringBuilder::Append(object)
を呼び出す
つまりC#で書く場合はコンパイラがやってくれているオーバーロードの解決を自分でやらないといけないってことです。
Sigil
前半はSigilというよりはILそのものについてのお話しでしたが、ここからはSigilでどのように生成しましょうかというお話になります。
その前に、String::Append
のオーバーロードを解決するヘルパメソッドを作っておきましょう。
static bool IsSupportedType(Type type) { var appends = typeof(StringBuilder) .GetMethods(Public | Instance) .Where(it => it.Name == "Append") .Where(it => it.GetParameters().Length == 1) .ToDictionary(it => it.GetParameters()[0].ParameterType); return appends.ContainsKey(type); } static MethodInfo GetAppend(Type type) { var appends = typeof(StringBuilder) .GetMethods(Public | Instance) .Where(it => it.Name == "Append") .Where(it => it.GetParameters().Length == 1) .ToDictionary(it => it.GetParameters()[0].ParameterType); if (appends.ContainsKey(type)) return appends[type]; else return appends[typeof(object)]; }
IsSupportedType
はType
を引数に取り、その型の引数をとるオーバーロードがあればtrue
を返し、なければfalse
を返します。
同様にGetAppend
はType
を引数に取り、その型の引数をとるオーバーロードがあればそのMethodInfo
を返し、なければobject
のMethodInfo
を返します。
static Func<object, string> BuildDelegate(Type targetType) { var targetProperties = targetType.GetMembers(Public | Instance) .Where(it => it is PropertyInfo) .Select(it => (PropertyInfo)it) .Where(it => it.CanRead && it.GetIndexParameters().Length == 0); var e = Emit<Func<object, string>>.NewDynamicMethod(); using (var target = e.DeclareLocal(targetType)) using (var builder = e.DeclareLocal<StringBuilder>()) { // target = (Target)value; e.LoadArgument(0); e.CastClass(targetType); e.StoreLocal(target); // builder = new StringBuilder(); e.NewObject<StringBuilder>(); e.StoreLocal(builder); // builder.Append(nameof(Target) + ":{") e.LoadLocal(builder); e.LoadConstant(targetType.Name + "{"); e.CallVirtual(GetAppend(typeof(string))); var isNotFirst = false; foreach (var it in targetProperties) { if (isNotFirst) { // builder.Append(",") e.LoadConstant(","); e.CallVirtual(GetAppend(typeof(string))); } isNotFirst = true; // builder.Append(nameof(Target.Property) + "=") e.LoadConstant(it.Name + "="); e.CallVirtual(GetAppend(typeof(string))); // builder.Append(target.Property) e.LoadLocal(target); e.CallVirtual(it.GetGetMethod()); if (it.PropertyType.IsValueType && !IsSupportedType(it.PropertyType)) e.Box(it.PropertyType); e.CallVirtual(GetAppend(it.PropertyType)); } // builder.Append("}") e.LoadConstant("}"); e.CallVirtual(GetAppend(typeof(string))); // return builder.ToString() var toString = typeof(object).GetMethod("ToString"); e.CallVirtual(toString); e.Return(); } Console.WriteLine($".maxstack {e.MaxStackSize}"); Console.WriteLine(e.Instructions()); return e.CreateDelegate(); }
static void Main(string[] args) { var target = new Target() { Id = 1, Name = "あああああ", Hoge = DateTime.Now }; var d = BuildDelegate(typeof(Target)); Console.WriteLine(); Console.WriteLine(d(target)); }
using (var target = e.DeclareLocal(targetType)) using (var builder = e.DeclareLocal<StringBuilder>()) { // ... }
上記の構文で変数の宣言を行っています。
本来ILレベルでは変数のスコープはメソッドが最小単位なのですが、IDisposable
を用いた機構でブロックレベルでの変数スコープを表現しています。
これは面白いと思います。
あとはほとんどILと一対一で対応している命令を放り込んで最後に
e.CreateDelegate();
でデリゲートは完成です。
実行結果はこちら。
.maxstack 2 ldarg.0 castclass SigilCSharp.Target stloc.0 // SigilCSharp.Target _local0 newobj Void .ctor() stloc.1 // System.Text.StringBuilder _local1 ldloc.1 // System.Text.StringBuilder _local1 ldstr 'Target{' callvirt System.Text.StringBuilder Append(System.String) ldstr 'Id=' callvirt System.Text.StringBuilder Append(System.String) ldloc.0 // SigilCSharp.Target _local0 callvirt Int32 get_Id() callvirt System.Text.StringBuilder Append(Int32) ldstr ',' callvirt System.Text.StringBuilder Append(System.String) ldstr 'Name=' callvirt System.Text.StringBuilder Append(System.String) ldloc.0 // SigilCSharp.Target _local0 callvirt System.String get_Name() callvirt System.Text.StringBuilder Append(System.String) ldstr ',' callvirt System.Text.StringBuilder Append(System.String) ldstr 'Hoge=' callvirt System.Text.StringBuilder Append(System.String) ldloc.0 // SigilCSharp.Target _local0 callvirt System.DateTime get_Hoge() box System.DateTime callvirt System.Text.StringBuilder Append(System.Object) ldstr '}' callvirt System.Text.StringBuilder Append(System.String) callvirt System.String ToString() ret Target{Id=1,Name=あああああ,Hoge=2016/04/30 17:36:16}
ちゃんと動いていますね。
おわりに
どちらかと言うとSigilというよりはILそのものの解説になってしまいました。
とは言っても、Sigilの命令はILと一対一で対応しており、ILを生成するためのDSLを提供しているわけではないのでどうしてもILの知識は必要になってしまいます。
最後にサンプルコードをのっけて終わりにしたいと思います。
おわり