<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      gohook 一個支持運行時替換 golang 函數(shù)的庫實現(xiàn)

      運行時替換函數(shù)對 golang 這類靜態(tài)語言來說并不是件容易的事情,語言層面的不支持導(dǎo)致只能從機器碼層面做些奇怪 hack,往往艱難,但如能成功,那掙脫牢籠帶來的成就感,想想就讓人興奮。

      gohook##

      gohook 實現(xiàn)了對函數(shù)的暴力攔截,無論是普通函數(shù),還是成員函數(shù)都可以強行攔截替換,并支持回調(diào)原來的舊函數(shù),效果如下(更多使用方式/接口等請參考 github 上的單元測試[1],以及 example 目錄下的使用示例):


                                                             圖-1

      以上代碼可以在 github 上找到[1],Linux/golang 1.4 1.12  下運行,輸出如下所示:


                                                         圖-2

      Hook() 函數(shù)原型很簡單:

      func Hook(target, replacement, trampoline interface{}) error {}

      該函數(shù)接受三個參數(shù),第一個參數(shù)是要 hook 的目標(biāo)函數(shù),第二個參數(shù)是替換函數(shù),第三個參數(shù)則比較神奇,它用來支持跳轉(zhuǎn)到舊函數(shù),可以理解函數(shù)替身,hook 完成后,調(diào)用 trampoline 則相當(dāng)于調(diào)用舊的目標(biāo)函數(shù)(target),第三個參數(shù)可以傳入 nil,此時表示不需要支持回調(diào)舊函數(shù)。

      gohook 不僅可以 hook 一般過程式函數(shù),也支持 hook 對象的成員函數(shù),如下圖。

      圖-3

      HookMethod 原型如下,其中參數(shù) instance 為對象,method 為方法名:

      func HookMethod(instance interface{}, method string, replacement, trampoline interface{}) error {}

      圖 3 運行結(jié)果如下:


                                                       圖-4

      目前 GitHub 上有類似功能的第三方實現(xiàn) go monkey[2],gohook 的實現(xiàn)受其啟發(fā),但 gohook 相較之有如下幾個明顯優(yōu)點:

      • 跳轉(zhuǎn)效率更高: 大部分情況下 gohook 通過五字節(jié)跳轉(zhuǎn),無棧操作,更可靠,且性能更好,實現(xiàn)上也更容易理解。
      • 更安全可靠:跳轉(zhuǎn)需要修改和拷貝指令,極容易影響 call/jmp/ret 等舊指令,本實現(xiàn)支持修復(fù)函數(shù)內(nèi) call/jmp 指令。
      • 支持回調(diào)舊函數(shù): 這是最大優(yōu)點,也是 gohook 實現(xiàn)的初衷。
      • 不依賴 runtime 內(nèi)部實現(xiàn): gomonkey 因為跳轉(zhuǎn)指令的原因依賴 reflect.value 來獲取 funval,而 value 內(nèi)部結(jié)構(gòu)并不開放,導(dǎo)致 go monkey  對 runtime 的內(nèi)部實現(xiàn)產(chǎn)生了依賴。

      實現(xiàn)解析##

      Hook 的原理是通過修改目標(biāo)函數(shù)入口的指令,實現(xiàn)跳轉(zhuǎn)到新函數(shù),這方面和 c/c++ 類似實踐的原理相同,具體可以參考[3]。原理好懂,實現(xiàn)上其實比較坎坷,源碼細(xì)節(jié)請參考[13],關(guān)鍵有幾點:

      1. 函數(shù)地址獲取###

      與 c/c++ 不同,golang 中函數(shù)地址并不直接暴露,但是可以利用函數(shù)對象獲取,通過將函數(shù)對象用反射的 Value 包裝一層,可以實現(xiàn)由 Value 的 Pointer() 函數(shù)返回函數(shù)對象中包含的真實地址,golang 文檔對此有特別說明[10]。

      2.跳轉(zhuǎn)代碼生成###

      跳轉(zhuǎn)指令取決于硬件平臺,對于 x86/x64 來說,有幾種方式,具體可以參考文檔[3],或者 intel 開發(fā)者手冊[4],gohook 的實現(xiàn)優(yōu)先選用 5 字節(jié)的相對地址跳轉(zhuǎn),該指令用四個字節(jié)表示位移,最多可以跳轉(zhuǎn)到半徑為 2 GB 以內(nèi)的地址。

      這對大部分的程序來說足夠了,如果程序的代碼段超出了 2GB(難以想像),gohook 則通過把目標(biāo)函數(shù)絕對地址壓到棧上,再執(zhí)行 ret 指令實現(xiàn)跳轉(zhuǎn)。

      這兩種跳轉(zhuǎn)方式的結(jié)合使得跳轉(zhuǎn)實現(xiàn)起來相對 gomonkey 簡單容易很多,gomonkey 選用了 indirect jump,該指令需要一個函數(shù)地址的中間變量存放到寄存器,因此這個變量必須保證不會被回收,還得注意該寄存器不會被目標(biāo)函數(shù)使用,導(dǎo)致實現(xiàn)上很別扭且不安全(跳轉(zhuǎn)代碼必須放到函數(shù)的最開始一段,不能放在中間),更嚴(yán)重的是,因為需要直接使用函數(shù)對象,gomonkey 必須猜測 value 對象的內(nèi)存布局來獲取其中的 function ptr,runtime 實現(xiàn)一改,這里就得跪。

      3.成員函數(shù)的處理###

      成員函數(shù)在 golang 中與普通函數(shù)幾乎一樣,唯一區(qū)別是成員函數(shù)的第一個參數(shù)是對象的引用,因此 hook 成員函數(shù)與 hook 一般函數(shù)本質(zhì)上是一樣的,無需特殊處理。

      值得注意到是子類調(diào)用基類函數(shù)這種場景,golang 編譯時會為子類生成一個基類函數(shù)的包裝(wrapper),這個包裝存在的目的是給通過接口調(diào)用基類函數(shù)時所使用,其作用從匯編角度看似乎是用于把對象的地址進行處理和傳遞,最后跳到基類函數(shù)中(具體原因沒深究)。

      所以在 hook 對象的成員函數(shù)時有兩種方式,一種是通過子類來 hook,一種是通過基類來 hook,前者只覆蓋通過接口調(diào)用函數(shù)這種場景,后者則能處理所有場景,對于 hook 第三方庫來說,經(jīng)?;惪赡苁遣婚_放的,這時 gohook 能發(fā)揮的作用就比較有限。當(dāng)然按 golang 開發(fā)的慣例來說,這種繼承(嚴(yán)格來說繼承也不存在)一般會配合接口來實現(xiàn)類似多態(tài)的功能,因此 hook 子類通常也能解決大部分場景了。

      如果上面的描述有些抽象,請參看 example 目錄下的 example3.go[12].

      4.回調(diào)舊函數(shù)###

      回調(diào)舊函數(shù)是很難的,很多問題需要處理,目標(biāo)函數(shù)因為入口地址要被修改,本質(zhì)上一部分指令會被破壞,因此如果想回調(diào)舊函數(shù),有幾種方式可以做到:

      1.將被損壞的指令拷貝出來,在需要回調(diào)舊函數(shù)時,先將指令恢復(fù)回去,再調(diào)用舊函數(shù)。
      2.將被損壞的指令拷貝到另一個地方,并在末尾加上跳轉(zhuǎn)指令轉(zhuǎn)回舊函數(shù)體中相應(yīng)的位置。
      3.將整個舊函數(shù)拷貝一份,并修復(fù)其中的跳轉(zhuǎn)指令。

      gohook 目前采用了第二種方案(后續(xù)會支持第三種),主要考慮有幾個:

      • 方案一無法重入,在 golang 協(xié)程環(huán)境下幾乎無法實際使用。
      • 拷貝整個函數(shù)消耗較大,且事先無法預(yù)測目標(biāo)函數(shù)的大小,函數(shù)替身難以準(zhǔn)備。

      無論是拷貝一部分指令還是全部指令,其中面臨一個問題必須解決,函數(shù)指令中的跳轉(zhuǎn)指令必須進行修復(fù)。

      跳轉(zhuǎn)指令主要有三類:call/jmp/conditional jmp,具體來說,是要處理這三類指令中的相對跳轉(zhuǎn)指令,gohook 已經(jīng)處理了所有能處理的指令,不能處理的主要是部分場景下的兩字節(jié)指令的跳轉(zhuǎn),原因是指令拷貝后,目標(biāo)地址和跳轉(zhuǎn)指令之間的距離很可能會超過一個字節(jié)所能表示,此時無法直接修復(fù),當(dāng)然同樣問題對四字節(jié)相對地址跳轉(zhuǎn)來說也可能會存在,只是概率小很多,gohook 目前能檢測這種情況的存在,如果無法修復(fù)就放棄(方案三理論上可以通過替換指令克服這個問題)。

      幸運的是,golang 為了實現(xiàn)棧的自動增長,會在每個函數(shù)的開頭加入指令對當(dāng)前的棧進行檢查,使得在需要時能對??臻g做擴充處理,無論是目前的 copy stack(contigious stack) 還是 split stack[5][6][7],函數(shù)入口的 prologue 都相當(dāng)長,參考下圖. 而 gohook 理想情況下只需要五字節(jié)跳轉(zhuǎn),最差情況 14 字節(jié)跳轉(zhuǎn),目前 golang 版本下,根本不會覆蓋正常的函數(shù)邏輯指令,因此指令修復(fù)大部分情況下只是修復(fù)函數(shù)末尾用于處理棧增長的跳轉(zhuǎn)指令,這種跳轉(zhuǎn)用近距離2字節(jié)指令的可能性相對小很多。

      圖-5

      5.遞歸處理###

      遞歸函數(shù)會自己調(diào)用自己,從匯編的角度看,通常就是一個五字節(jié)相對地址的 call 指令,如果我們替換當(dāng)前函數(shù),那么這個遞歸應(yīng)該調(diào)到哪里去才對呢?

      當(dāng)前 gohook 的實現(xiàn)是跳到新函數(shù),我個人認(rèn)為這樣邏輯上似乎合理些。另一方面,在不修復(fù)指令的情況下,遞歸默認(rèn)跳回函數(shù)開頭,執(zhí)行插入的跳轉(zhuǎn)指令也是走到新函數(shù),這樣行為反而一致。

      實現(xiàn)上為達(dá)到這個目的,在需要修復(fù)指令的情況下,就需要做些特殊處理,目前做法是當(dāng)看見是相對地址的 call 指令,就額外看看目的地址是不是跳到函數(shù)開頭,如果是就不修復(fù)。

      為什么只處理 Call,而不處理 jmp 呢?因為 Go 在函數(shù)末尾插入了處理棧增長的代碼,這部分代碼最后會跳轉(zhuǎn)回函數(shù)入口的地方,用的 JMP 指令,另外就是,函數(shù)體中也可能會有跳回函數(shù)開頭的理論性可能(可能性很小很小),因此如果所有跳回開頭的指令都不修復(fù),那么這部分邏輯就出問題了,想象一下,runtime 一幫你增長棧就跳到新函數(shù),場面太靈異。

      只處理相對地址的 Call 指令理論上也是不完全夠的,雖然大部分情況遞歸用五字節(jié) call 很經(jīng)濟實惠,但如果遞歸可以通過尾遞歸進行優(yōu)化,這時編譯器很可能可能就會用  jmp 指令來跳轉(zhuǎn),gcc 在這方面對 c 代碼有成熟的優(yōu)化案例,幸運的是目前 golang 沒聽說有尾遞歸優(yōu)化,所以以后再說了,畢竟這個優(yōu)化也不是那么容易的。

      注意事項##

      • 項目原意是用來輔助作測試,目前仍在初級階段,并未全面測試和生產(chǎn)驗證,可靠性有待驗證。
      • 特殊情況下通過 push/retn 跳轉(zhuǎn)時,需要臨時占用 8 字節(jié)棧空間,而這 8 字節(jié)空間不會被 golang 運行時提前感知,極端情況下,如果剛好處在棧的末尾理論上可能會有問題,但
      • 是根據(jù)[8][9]關(guān)于棧處理的描述,golang 對每個棧保留了幾百字節(jié)的額外空間用來作優(yōu)化,允許越過 stackmin 字節(jié)(通常是 128 bytes),因此可能也不會有問題,這個問題我目前還不確定。
      • 特殊情況下會因為某些指令因為距離溢出無法修復(fù),從而無法 hook。
      • 修復(fù)指令需要知道函數(shù)的大小,目前 gohook 通過 elf 導(dǎo)出的調(diào)試信息進行判斷,如果二進制 strip 過,則通過 function prologue 進行暴力搜索,對部分特殊庫函數(shù)可能無法成功。
      • 過小的函數(shù)有可能會被 inline,此時無法 hook(編譯時加上-gcflags='-m'選項可以查看哪些函數(shù)被 inline,另外就是如果自己寫的函數(shù)不希望被 inline,可以加上 // go:noline 來告訴編譯器不要對其進行 inline,gcflags 也可以指示編譯器不要對代碼進行內(nèi)聯(lián),如-gcflags=all='-l')。
      • 32 位環(huán)境下沒有完整驗證過,理論上可行,測試代碼也沒問題。

      引用##

      1、https://github.com/kmalloc/gohook

      2、https://github.com/bouk/monkey

      3、http://jbremer.org/x86-api-hooking-demystified/

      4、https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf

      5、https://agis.io/post/contiguous-stacks-golang/

      6、https://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite

      7、https://blog.cloudflare.com/how-stacks-are-handled-in-go/

      8、https://golang.org/src/runtime/stack.go

      9、http://blog.nella.org/?p=849
      10、https://golang.org/pkg/reflect/#Value.Pointer
      11、https://github.com/golang/go/blob/master/src/runtime/runtime2.go#L187
      12、https://github.com/kmalloc/gohook/blob/master/example/example3.go
      13、https://onedrive.live.com/View.aspx?resid=7804A3BDAEB13A9F!58083&authkey=!AKVlLS9s9KYh07s

      posted on 2019-06-04 15:10  twoon  閱讀(12106)  評論(1)    收藏  舉報

      主站蜘蛛池模板: 亚洲大尺度无码专区尤物| 本道久久综合无码中文字幕| av午夜福利一片免费看久久| 日韩熟妇中文色在线视频| 久久人与动人物a级毛片 | 免费观看羞羞视频网站| 色综合久久婷婷88| 九九热在线视频观看精品| 五月婷婷久久中文字幕| 日韩一欧美内射在线观看| 亚洲乱妇熟女爽到高潮的片| 久热天堂在线视频精品伊人| 老太脱裤子让老头玩xxxxx| 少妇爆乳无码专区| 亚洲熟女乱综合一区二区| 国产成人精品无码播放| 国产超高清麻豆精品传媒麻豆精品| 野花香视频在线观看免费高清版| 少妇夜夜春夜夜爽试看视频| 九九热免费在线播放视频| 国精品无码一区二区三区在线看 | 久久99精品久久久大学生| 国产综合色在线精品| 大尺度国产一区二区视频 | 亚洲精品久久久久国色天香| 亚洲av一区二区在线看| 国产69精品久久久久99尤物| 国产av国片精品一区二区| 欧美人与动牲交a免费| 中文字幕无码视频手机免费看 | 亚洲人成网站在线观看播放不卡| 国产精品毛片在线看不卡| 日韩V欧美V中文在线| 亚洲精品一区二区制服| 中文字幕精品亚洲字幕成| 天天躁日日躁狠狠躁中文字幕| 无码av永久免费专区麻豆| 中文字幕国产精品二区| 一区二区三区精品偷拍| 人人爽天天碰天天躁夜夜躁| 99久久精品久久久久久婷婷|