x64 windows下的inline hook

來源:互聯網
上載者:User

以前做過ia32的inline hook,現在自然擴充em64t

x64中,虛擬位址變成64位,但大部分指令中的地址和立即數還是32位,執行時符號位擴充至64位
因此不能簡單的放一個帶位移的near jump在函數開頭,因為源地址和目標地址之間的差距很可能超過2G,這是32位有符號整數能表示的範圍.
為了能夠任意地跳轉,可以選擇兩種方法
1:mov GPR64,targetaddr
jmp GPR64
2:push targetaddrlow
mov [rsp-4],targetaddrhigh
ret
第一種的指令較短,佔12位元組,但會修改某個通用寄存器的值,第二種只依賴棧,佔14位元組
由調用者儲存的GPR有rax, rcx, rdx, r8-r11,第一種方法可以選擇這些,我使用的是第二種方法

在目標常式開頭安置轉移指令後,執行目標函數時即會轉移到指定的常式,當需要調用原來的常式時首先要執行被覆蓋的指令,此時又有兩種途徑
1:用原來的指令覆蓋目標常式開頭,然後調用該常式
2:在另外的位置執行被覆蓋的指令,然後轉移到被覆蓋的指令之後

顯然第一種方法只能用於不會重入的常式,否則很容易出問題。加鎖是不能解決問題的,因為被hook的常式可能本來允許多線程同時執行,加鎖就變更了這個特性。

因此選用第二個方法。為了備份完整的指令,需要知道每個指令的長度。
在32位中,許多可hotpatch的函數開頭是
mov edi,edi
push ebp
mov ebp,esp
這樣的prolog,佔5位元組,如果安置0xe9的jmp,正好5位元組,於是可以直接備份前5位元組,執行完後轉移到該函數的地址加5即可。但64位中安置的指令是12或14直接,函數的prolog不再能夠提供完整的12或14直接指令。於是要計算每個指令的長度。
初始的想法是利用單步中斷,但會受到分支指令以及指令的副作用的幹擾,因此不可行。
解決方案是自己分析指令的結構,指令的編碼可見intel提供的參考手冊。由於編程分析每個指令的編碼很麻煩,於是我從指令手冊複製了一張2進位編碼錶(需要少量的修正),然後把記憶體中的數轉成字串和這個表匹配,從而可以知道指令的長度。大致的步驟是:
從給定地址的開頭按位元組判斷是否是傳統的指令首碼,有段覆蓋、運算元尺寸覆蓋、地址尺寸覆蓋、lock、rep,直到出現其他值
然後判斷下一個位元組是否是REX首碼,REX首碼的W位,傳統的指令首碼和作業碼中的w、s位共同影響立即數的尺寸
與編碼錶進行比對

現在可以備份完整的指令了,但是備份的指令中可能有相對定址。
如果相對定址的目標地址在備份的指令當中,則不需修正,否則需要做出修正。
x64中相對定址的指令有帶直接的(位移在指令中)short jmp,near jmp,near call,short jcc,near jcc,jcxz和REX與ModR\M指定的rip相對定址。如果指令的displacement是4位元組且備份的指令處和指令引用地址的距離小於2G,這樣可以直接修正指令中的displacement(位移),新位移=原位移-原指令地址+新指令地址。如果displacement是1位元組,這樣的有short jmp,short jcc,和jcxz,則需要變換成near jmp或near
jcc或near jmp和jcxz的組合,這樣會導致指令長度的改變,這樣會影響到後續指令的位置,需要予以考慮。x64中沒有2位元組位移的分支指令。

如果原指令引用地址與備份的指令的地址相差超過2G,則需要進行更大的變化。
對於direct short jmp和direct near jmp,可以變換成
push targetaddrlow
mov [rsp-4],targetaddrhigh
ret
對於條件分支指令,可以變換成上述指令與條件分支指令的組合
對於direct near call指令,可以變換成
jmp rip+15
nop
nop
...
label a:(要保證a的地址是8的倍數,因為x64可以產生未對齊異常)
target address
...
nop
call [rip-b+a]
label b:
對於記憶體運算元的rip相對定址,我目前沒有找到比較好的修正方法
本來想變換成
mov rax,[moffset]
原指令運算元改為rax
mov [moffset],rax(如果指令會寫記憶體)
x64中只有作業碼為A0,A1,A2,A3的mov指令能夠接受8位元組的段位移,另一個運算元是累加寄存器
但是intel提供的表中不易判斷指令是否會寫記憶體,因此不易判斷是否應該加mov [moffset],rax
除此外,這樣做會修改寄存器的值,可以用push備份和pop還原,但不適用於間接分支指令.

我目前使用的方法是尋找一塊合適地址的記憶體,然後在這上面放置備份的指令。大致過程如下:
計算出原指令中rip相對定址所的目標地址的最小值LowRef和最大值HighRef
計算LowBound=HighRef-2G,HighBound=LowRef+2G
則區間[LowBound,HighBound]中的指令到原指令所有rip相對定址的運算元的距離小於2G
如果LowBound>=HighBound則無法繼續
用NtQueryVirtualMemory尋找一塊被[LowBound,HighBound]包含的空閑地區,然後用NtAllocateVirtualMemory在其中分配一塊記憶體,交給RtlCreateHeap建立一個堆,並記錄下堆的起始地址和堆的最大長度
然後即可用RtlAllocateHeap在堆上分配記憶體,安置備份的指令並進行修正

今後再需備份指令時,先從已建立的堆尋找合適的,沒有時再建立新的堆

原始碼見http://ishare.iask.sina.com.cn/f/24072861.html
這個僅作為學習資料,在正式場合請使用Detours(x64要錢)或免費的N-CodeHook

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.