本文首發先知社群:https://xz.aliyun.com/t/14542
在上篇文章 https://xz.aliyun.com/t/14487 中,我們討論了靜態堆疊欺騙,那是關於 hook sleep ,在睡眠期間改變堆疊行為的欺騙,這篇文章我們來一起討論一下主動欺騙,允許任意函式發起時的堆疊欺騙。
相關的基礎知識在上篇文章已經介紹,並且給出了推薦閱讀的連結,這裡就不再多說,接下來讓我們一起動起手來進行除錯。
手動進行堆疊欺騙
我們先在 x64dbg 中手動進行堆疊欺騙,這對我們理解接下來的專案有很大的幫助。
我隨便找了一個程式,我們的想法是在棧底再偽造相同的兩幀,都是RtlUserThreadStart +0x28,因為這是我係統上常見的偏移量,你在下面的截圖中也可以發現相同的幀。
我們首先找到這個函式
可以看到第一條指令是 sub rsp 78,這意味著它需要的幀棧大小為 0x78,注意這裡是 16 進位制,我們只需要在當前棧底向下移動 15 次(0x78=0x08*15
, 十六進位制滿 16 進 1),然後就可以在這個位置建立新棧了。
x64dbg 自動標註的範圍也證實了我們的理論
我們把這個位置改成想要的幀棧
然後再次重複即可完成第二幀的偽造
此時在 Process Hacker 中檢視(注意以管理員許可權開啟),可以看到兩個幀棧已經成功偽造。
LoudSunRun
第一個要介紹的專案 https://github.com/susMdT/LoudSunRun,這是作者在學習另一個專案https://github.com/klezVirus/SilentMoonwalk 時的產物,由於原專案有點大,作者在這個專案較小的程式碼庫、間接系統呼叫支援和多引數支援。
這個 Poc 實現了 pPrintf,NtAllocateVirtualMemory 的直接呼叫,以及pNtAllocateVirtualMemory 的間接系統呼叫,我們接下來看一下專案:
一個引數的 Printf 的呼叫
首先先獲取 Printf 的地址,這是我們要去呼叫的函式,另外又獲取了 BaseThreadInitThunk+0x14 和RtlUserThreadStart+0x21 的位置,這是我們要去偽造的棧幀,因為這是作者電腦上一個執行緒中常見的棧底,當然在不同 windows 版本下偏移量是不一樣的,我的 windows 版本下偏移量是 RtlUserThreadStart+0x28。還有一個FindGadget 函式,這是為了幫助我們尋找一個 jmp [rbx] 小工具的,我們後面會講到。
也許還有人注意到了 CalculateFunctionStackSizeWrapper 函式,這個函式是用來計算幀棧大小的,就像我們上面手動偽造 0x78,在當前棧底向下移動 15 次一樣,這個函式是根據 UnwindOp 來進行計算的,想要深入理解的話可以閱讀一下:https://codemachine.com/articles/x64_deep_dive.html
緊接著就來到了 Spoof 函式,這是最關鍵的函式,是我們的欺騙函式,這個函式的引數是可變的,但是 Spoof的前七個引數是相對固定的,前四個引數是我們想要去呼叫的函式的前四個引數,第五個引數是一個重要的結構體,裡面儲存著程序上下文,如果需要間接系統呼叫的 SSN 以及要偽造的棧幀,第六個引數是要呼叫的函式的地址,第七個引數用來指示是否還有別的引數,假如為 2 的話在 Spoof 裡面就會想辦法獲取後面兩個引數。
下面是第五個引數的結構體。
在這裡我還想再多說一句 x64 下引數的傳遞,前四個引數是放在Rcx,Rdx,R8,R9四個暫存器中,後面的引數就要放在棧上了,如圖(圖源 Windows x64 呼叫約定 - 堆疊框架):
準備和呼叫階段
ok,現在讓我們進入彙編看看到底發生了什麼,
首先是一些準備操作,先將棧上的引數分別給 rdi 和 rsi,rdi 就是我們前面提到的結構體,為了便於恢復所以要先將當前暫存器的值給儲存起來,rsi 就是要呼叫的函式的地址。
在下圖的最後一行我們將 rax 給到了 r12,而之前 pop rax 則將原始的返回值給到了 rax,這樣 r12 就儲存了函式的返回值,這是因為 rax 是易失性暫存器,而 r12 是非易失性暫存器,也就是說即使被別的函式呼叫,r12 也會被 push 保護起來,最後再 pop 出來。
在x64的呼叫約定中規定易失性暫存器RAX, RCX, RDX, R8, R9, R10, R11, XMM0-XMM5 為易失性暫存器,RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, XMM6-XMM15為非易失性暫存器
這段程式碼是處理引數的準備工作,r11 和 r13 分別儲存了需要額外處理的引數的個數和已經額外處理的引數的個數,透過比較這兩個暫存器的值就可以處理完所有的額外的引數了。由於 printf 是不需要額外的引數的,所以我們之後再分析
下面是一個迴圈,和我們上面說的一樣,比較兩個暫存器的值來判斷是否還需要處理,等下我們再說是如何處理的,先跟著程式碼除錯
然後棧上分配一塊空間,200h,然後 push 0,將之前的幀棧截斷,剩下的就是我們自己要偽造的操作了。
接下來就是在偽造棧幀了,透過上面手動偽造應該很容易可以理解
現在看一下我們的棧幀,成功偽造
接下來是為了跳轉和 fixup 做準備的,syscall 的程式碼等下再講。將返回地址,rbx 暫存器值,fixup 的值給到前面那個欺騙的結構體,然後將 fixup 的值給到 rbx,因為它也是個非易失性暫存器,最後 jmp11。
此時看一下堆疊,十分乾淨
返回階段
然後就是返回階段了,當執行完 printf 後會進入到我們前面找到 jmp [rbx] 小工具,而我們的 rbx 存的是 fixup 函式地址,所以就會跳轉到 fixup 函式
下面是我們的fixup 函式,主要就是恢復幀棧和前面儲存的暫存器工作,最後 jmp 回到我們最初保持的返回點。
恢復之後的棧幀又是正常的
多引數呼叫
我們看一下多引數是怎麼處理的
先將 rsp+30h 儲存到 r10 裡面,這樣 r10+0x08 就可以找到下一個引數
這裡 r14 是為了獲取額外引數應該在的位置的,是我們需要壓入棧中的資料的偏移量,首先加上 200h,這是我們在棧上分配的假棧的空間,然後是加 8,這對應著 push 0 指令,然後再加上要偽造的三個幀棧大小,這樣就到了我們要呼叫的函式的幀棧了,然後以此為基礎,第一個引數在 r14+0x28 位置處,然後每個引數依次加 0x08 即可
下面上一張圖幫助大家理解
我們先找到引數需要移動到的位置,然後再將 r10+0x08 的值給到相應位置就可以了,相應位置是透過 rsp-r14 的值計算出來的,r14 是我們上面說的偏移量
間接系統呼叫
這個實現起來就很簡單了,我們 jmp 去的時候先將 ssn 號存到 rax,然後直接跳轉到 syscall 指令就可以了
注意這裡跳轉的函式直接就是 syscall 指令,Poc 裡面作者是手動找到 syscall 指令的
當然獲取 syscall 指令可以自動化獲取,這裡不再展開