構建 Go 語言的動態插裝 Agent

我們在 Sqreen (譯註:一家安全公司)一直致力於讓的安全保護透明無感、接入平滑。最近,我們發佈了 Sqreen for Go,它可以在不需要任何代碼改動的情況下檢測和阻止 Go 應用中的安全問題。爲了實現這一點,我們利用了動態插裝(dynamic instrumentation)在運行時向程序中插入額外的安全邏輯。作爲我們動態插裝系列的最新發文,這篇文章將要討論動態插裝,Sqreen 的 Go Agent,以及我們是如何把他們結合在一起的。

這種檢測和保護是基於 microagent 的,microagent 是一個從運行的程序中獲取數據,並與外部組件通信以報告統計信息或特定數據的組件。這裏描述的 的,microagent 是一個 Go 包,它與 Sqreen 的後端服務通信。它專門設計用於安全地插裝到生產系統中,因此我們用了精妙的(sophisticated)方法來最大化其穩定性,同時讓它對性能的影響儘可能小。

運行中的 Go 程序與 Sqreen 概覽:Agent 協程自動啓動,並根據我們面板中的配置對程序進行插裝。

Sqreen 的動態插裝有很多優點:

Agent 可以主要分爲三個部分:

  1. 插裝引擎,處理讓函數(function)增加額外業務邏輯的底層機制;

  2. 安全規則引擎,通過調用插裝引擎來爲函數增加安全防護邏輯。它遵循由 Sqreen 後端下發的高級(high-level)邏輯描述,例如保護 SQL 語句受到 SQL 注入攻擊等;

  3. 數據記錄機制,異步收集安全防護的數據,並定期發送到 Sqreen 後端。

接下來的章節詳細介紹了我們如何在 Go 語言中解決這個非常有挑戰的問題。Go 語言中的標準庫 database/sql 將被用作 SQL 注入工具防護組件的示例。

插裝 Go 代碼

運行時插裝在動態語言中有廣泛的使用,通常稱爲 Monkey Patching。但是 Go 是一門強類型的靜態語言,用它構建的代碼由 Go 編譯器變異成包含二進制機器碼的程序文件:

在運行時,操作系統和硬件加載並執行這個二進制程序文件:

因此,運行中的 Go 程序其實就是一個運行中的二進制程序。我們通常認爲,運行時插裝去修改它的二進制代碼是不安全、有風險的,在生產環境某些場景中甚至是不可能的。

選擇正確的插裝方案

前面的討論已經告訴我們所有能做插裝的地方了:由開發者手動使用的工具進行源碼級別的插裝,到針對運行中的程序的硬件級別的插裝。這意味着我們有很多方案可以選擇。而對於 Sqreen 來說,就是要選擇最適合 Sqreen Agent 場景的方案:

由此可得出以下的表格:

ChNV3p

長話短說,我們選擇編譯時插裝技術:

而其他的插裝技術:

基於以上的分析,我們決定使用編譯時插裝的技術來爲 Go 語言程序添加運行時插裝能力。

鉤子策略(Hooking strategy)

我們需要編譯器爲 Go 程序中的函數添加插裝鉤子點位(hook points),以便讓 Sqreen 的 microagent 監控和保護函數執行。例如,我們想在 SQL 執行函數添加鉤子,檢測函數參數中的 SQL 語句是否存在注入情況,並在檢測到(注入)攻擊時中止函數調用。

爲此,我們的鉤子策略允許我們通過以下方式監控和保護函數的執行:

SQL 執行函數鉤子啓用時,SQL 注入保護的示例:當語句中檢測到注入時,函數需要馬上中止,返回一個不爲 nil 的錯誤。

以下代碼段展示了一個被插裝的 Go 函數,它使用了之前描述的鉤子策略:

// 這是一個函數的示例,返回給定參數的json序列化結果。
//
// 該函數插裝了一個演示的鉤子,監控函數調用,以實現保護
// 控制流程,並在必要時中止調用。
//
// 爲此,以下插裝代碼塊由兩部分組成:
//
// 1. The prolog: 它是插裝的起點,掛鉤在正常代碼執行之前,
//    對應其輸入參數。它會返回一個布爾值和 epilog 鉤子,
//    當函數調用必須中止時布爾值爲 true。
//
// 2. The epilog: 它是由 prelog 返回的函數,用於掛鉤在函
//    數返回時以及對應函數返回值。它由 defer 執行,以保證在
//    任何返回情況下均會執行到。
//
func myInstrumentedFunction(a int) (result []byte, err error) {
 // 插裝代碼段
 {
  // prelog 鉤子啓用。
  // 注意這個示例不是線程安全的,但我們的實際實現中加載函數值
  // 是原子(atomically)的。
  if myInstrumentedFunctionHook != nil {
   // 用函數參數調用 prelog 鉤子,並檢查返回值
   epilog, abort := myInstrumentedFunctionHook(a)
   
   // 不管 abort 值如何,epilog 方法總會由 defer 執行以
   // 觀測所有返回情況。
   if epilog != nil {
    defer epilog(&result, &err)
    
   }
   
   // 如果 abort 值爲 true,則立即從函數中返回。返回前可能
   // 執行 epilog 函數。
   if abort {
    return
   }
  }
 }

 // 正常代碼
 return json.Marshal(a)
}

// 插裝函數的 prolog 鉤子
var myInstrumentedFunctionHook myInstrumentedFunctionPrologHookType

// 函數的 prelog 和 epilog 鉤子定義,強類型並與函數簽名保持一致。
type (
 myInstrumentedFunctionPrologHookType func(a int) (myInstrumentedFunctionEpilogHookType, bool)
 myInstrumentedFunctionEpilogHookType func(result *[]byte, err *error)
)

Copy

你可以在 Go Playground 運行完整的示例代碼:https://play.golang.org/p/zAQaf_rGaRs

編譯時(Compile-Time)插裝:向 Go 程序添加鉤子點位

Go 編譯器默認不提供這種鉤子點位插裝。但是,代碼生成已經很常見,並且在 Go 語言中也有廣泛使用。例如,Go 編譯器在代碼覆蓋率或競態檢測中都有使用到這種技術。Go 語言標準庫有解析、修改、重新生成 Go 代碼所需的所有工具。因此,我們選擇在一個獨立的插裝工具中實現安全的源碼層面插裝,使其能與 Go 編譯器集成,生成插裝後的 Go 程序文件。

集成 Sqreen 插裝工具到 Go 編譯器中並生成插裝後的 Go 程序文件。

這個工具會在每次編譯時被 Go 編譯器調用,並插裝所有定義了的函數。

SQL 執行函數源碼層級的插裝示例:接收 Go 源碼文件作爲輸入,輸出增加了鉤子點位插裝的新源碼。

原有的源碼文件不會被修改,插裝後的源碼會生成到編譯器的構建目錄(build directory)中。因此,可以使用 Go 構建選項 -work 來查看插裝後的代碼,它會打印結果並保留構建目錄。

在編譯器層級處理插裝的另一個好處是,可以完整地對所有 Go 語言涉及到的組件進行插裝。我們只對少量的 Go 庫包進行了插裝,這些庫包記錄在文檔中:https://docs.sqreen.com/go/instrumentation/#list-of-instrumented-packages

運行時插裝:掛載安全防護邏輯到鉤子點位

基於上述的編譯時插裝方案,Sqreen 的 Go Agent 現在可以通過鉤子表(一個由工具生成的 Go 數組)找到鉤子點位,並在需要時進行插裝。在 SQL 注入防護的例子中,當我們在面板上啓用該功能後,Agent 會收到 SQL 注入安全的規則。它會包括插裝 SQL 執行函數(當前一共有 3 個:prepareexecquery)所需的信息,以及 SQL 注入防護邏輯。

藉助標準庫中的 reflection,鉤子點位的 Go 類型(譯註:此處應指函數位置、函數簽名等信息)也用於檢查和掛載防護邏輯。掛載原子操作,在併發使用場景下也是安全的。

在這個示例中,Agent 收到 Sqreen 後端的 SQL 注入防護指令。當應用設置中的防護開啓,Agent 檢索鉤子表中的鉤子,檢驗是否和將要掛載的函數類型信息匹配,最後原子地掛載到鉤子點位上。

掛載之後,函數的執行就能被觀察到,更重要的是,能在檢測到攻擊時中止執行。安全防護可以在函數調用上下文進行它的檢測,並藉助 Go 函數的簽名,安全地中止函數調用,返回一個不爲 nil 的錯誤。

我們的安全防護還可以自動通過取消(cancel) Go request context 和根據面板配置響應 HTTP 請求,來中止 HTTP 調用處理。

HTTP 請求由 Sqreen 自動管理:根據面板配置進行響應,以及通過取消 Go request context 來中止請求,並正確地傳播中止信息。

數據傳輸

所有加在請求處理流程中的操作都是設計成異步的,以規避對響應時間的影響。當一個新的 HTTP 請求開始時,我們的中間件函數添加了一些數據結構到請求中,它們可以在掛載的防護中被獲取到,用以收集安全相關的信息,例如攻擊詳情、指標和事件。當請求中止時,請求的數據結構通過非阻塞的 Go channel 異步送往 Agent,這樣不會時阻塞到 HTTP 請求的 handler 把它發出去。

Agent 協程大部分時間都在休眠,僅在 Go channel 有數據時被 Go 調度器喚醒。Agent 收集數據,暫存攢批,最後定時發送至我們的後端服務(默認 20 秒一次)。我們的安全防護不收集敏感數據,Agent 會強制清除所有已發送至後端的數據。總而言之,這個實現就是個簡單的異步 Go 程序。

性能和魯棒性

Sqreen Go Agent 只使用了標準的 Go 語言技術,但是它所充當的角色需要我們仔細考慮以下幾方面:

Agent 還有針對通信失敗、大量請求流量、內部發生非預期的行爲的魯棒性設計,其工作不依賴於後端服務,並且可以重啓或者停止工作來保證最大程度的安全可靠。

展望未來

我們在這篇博客中描述了生產級插裝 Agent 的上層概念。動態插裝 Agent 可以用於處理很多不同的任務,例如性能監控、錯誤監控或安全防護。

在 Sqreen,我們的 Go Agent 利用動態插裝技術來保護應用,並且提高 Go 應用的可觀測性。如果對 Sqreen 感興趣或者想動手試試 Go Agent,可以來 Sqreen 官網轉一轉。

原文地址:https://jiekun.dev/posts/dynamic_instrumentation_agent/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vliRmGoGWJVJRUM3t0dLLA