天天看點

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

Hook技術基礎簡介

Hook中文的意思是鈎子,它的概念主要是,可以讓使用者有機會切入到 PG 資料庫的内部運作機制中,進行中斷、增加或者修改原來的程式邏輯,進而實作一些使用者自定義的功能。單獨看文字可能了解不是很直覺,我們來看看示意圖。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

這個藍色的箭頭方塊表示某一個軟體的原始的程式邏輯,預設情況下是無中斷的順序執行。但是我們原程式在設計的時候,可以在适當的位置定義一個鈎子,加入一些判斷機制,當滿足一定條件時,允許跳轉到一段使用者自定義的程式,使用者自定義程式運作完成之後再回到原程式繼續執行下去,這種技術就叫做 hook 機制。

當然,在設計這個鈎子的時候,也允許使用者使用自定義程式來替代原始的程式,以達到改變或優化原程式的業務邏輯的目的。這就是 hook 技術的感念。

要實作這個 hook 機制,需要滿足四方面的要素:

第一,原始程式中要設計有這麼一個鈎子,後續才能讓使用者有機會利用這個鈎子。這個是前提條件,如果原程式中根本就沒有設計這種機制,那麼我們也就無從談起利用這個鈎子了。

第二,要開發一段自定義的程式,用來完成某些特殊的功能,這點也就不用過多解釋。

第三,現在原程式的鈎子已經定義好了,自定義程式也已經實作,那麼什麼時候将這段自定義程式加入到原程式的執行流程中呢?預設情況下,原程式是不會理睬我們定義的這段程式的,需要我們人為的設定一下,這就是 hook 的安裝,隻有安裝好了這個鈎子,且滿足一定的條件,原程式才會流轉到我們自定義的這段程式中。

第四、有了鈎子的安裝,自然就有鈎子的解除安裝,就是在不需要再執行自定義程式的時候,将這個關聯關系解除安裝掉,讓程式恢複原狀,這就是鈎子的解除安裝。當然這個解除安裝的操作不一定是必須的,因為有些 hook 機制會一直使用下去,直到系統停機就自然而然的解除安裝掉了。也就不用我們特意的來實施這個解除安裝的操作。這點需要根據不同的業務場景而定。

hook機制實作方法

剛才我們大緻了解了 hook 技術的概念和基本原理,下面我們就用實際的代碼來說明 hook 機制的實作方法。這些是摘自 PG 資料庫的源碼,是用 C 語言開發的。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

首先,第一點是鈎子的定義,主要是在原程式中定義一個函數指針,并且預設情況下該函數指針賦一個空值。然後在原始代碼的合适的地方,增加一個if判斷,當原始程式在執行這段代碼時,會判斷這個指針函數是否為空,如果為空的話,就跳過,繼續執行後續的代碼。如果不為空那麼就執行這個函數指針所指向的那段代碼。那麼顯而易見,預設情況下,這個函數指針始終為空,是以原系統會不間斷的執行原始的代碼,就當作這個鈎子不存在。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

第二點,需要開發一段程式用來實作自定義的業務邏輯,這點不需要做過多解釋。大家注意到沒有,這段自定義代碼的函數中,還有另外一個判斷,這個待會兒再做解釋。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

第三點,hook 的安裝。就是在适當的時候,将原程式中的那個函數指針指向我們自定義的這個函數上,當然在修改原函數指針的時候,還要将原函數指針所指向的其他函數方儲存下來。

大家想想,原來的函數指針不是預設為空嗎?為什麼還要儲存原來的函數指針呢?這就涉及到 hook 的嵌套機制,因為當我們在安裝我們自己開發的插件的時候,有可能其他插件也在這個鈎子上安裝了他們的自定義程式,這就形成了一個鈎子鍊條。

為了不妨礙其他插件的運作,我們在修改函數指針之前要将原函數指針也儲存下來,然後在我們的自定義程式中再加上一個判斷,看看原函數指針是否為空,如果不為空還需要執行其他插件中自定義的程式。這點尤其重要,千萬不能忽略掉了。

另外,這個 hook 安裝的操作是在這個 _PG_int 函數中完成的。這個是 PG 插件開發的規範接口名稱。當資料庫在加載這個插件的時候,會預設調用這個函數做一些初始化的操作,是以我們就将 hook 的安裝放在這個函數中。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

第四點,就是鈎子的解除安裝,雖然這點不是必須的,我們也稍加解釋一下,就是在 _PG_fini 函數中,做一些收尾的操作,比如将之前修改過的原程式的指針還原。這個 _PG_fini 函數名也是約定好了的,在插件解除安裝的時候由資料庫自動調用。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

有了這 4 要素之外,還需要其他一些輔助的操作,才能讓這個插件真正的運作起來,比如要修改參數檔案,将這個插件名加入到 shared_preload_libraries 這個參數中,或者使用 load 指令手工加載這個插件。另外在必要的時候可能還需要重新開機資料庫執行個體。

注意事項

下面再來看看,我們在開發某個插件的時候,要注意哪些事項。雖然我們開發的插件與資料庫的核心程式是松耦合關系,我們很友善的安裝或者解除安裝這個插件,但是一旦這個插件被安裝上了之後,這段自定義程式就成為整個資料庫的一部分了,比如這段原始程式執行多少次,那麼我們的這段自定義程式就會執行多少次,這段自定義的程式品質的高低也會影響資料庫核心的原程式。是以也需要確定這段自定義程式的健壯性、性能以及容錯機制。同時還要檢查是否有記憶體洩漏的問題。另外如果這段程式需要占用大量記憶體,還需要做好記憶體消耗的評估等等。

Auth_delay案例

下面我們來介紹一個實際的插件的例子,這個例子是來自于随 PG 一道釋出的工程中的。叫做 auth_delay。

可能大家都知道這個插件的作用,就是當使用使用者名和密碼連接配接 PG 資料庫時,如果當使用者名和密碼錯誤,可以讓資料庫延遲幾秒再傳回給前端,這樣是防止利用窮舉密碼的方式來嘗試登陸資料庫。

我們就來看看這個插件的原理。首先第一點,在 PG 核心源碼中的使用者登陸驗證的程式中,定義了這麼一個函數指針,預設情況下該函數指針為空。然後在使用者名和密碼驗證結束的這個函數中增加了一個if判斷,如果這個函數指針不為空,則調用這個函數指針指向的程式。

第二點,開發了這個自定義函數,這個函數的邏輯非常簡單,首先判斷原函數指針是否為空,如果不為空則先執行這個函數指針指向的其他函數。然後就是運作這個插件要完成的主要任務,即判斷這個使用者名和密碼驗證的結果是否為OK 這個狀态,如果不是 OK 說明使用了錯誤的使用者名和密碼登陸,那麼此時将 sleep 幾秒,也就是延遲傳回。這個就是該插件的主要功能。

然後第三點就是在這個 PG_INIT 這個函數中安裝這個鈎子,即首先将原函數指針保留在一個函數指針變量中,然後将這個自定義的函數指派給原函數指針這個變量。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

這個插件非常簡單,簡短的幾行代碼就完成了密碼驗證錯誤後延遲傳回的這麼一個需求。是以我把它稱為史上最簡單的 PG 插件。

常用hook

剛才我們介紹了 HOOK 技術的第一個要素,就是 PG 原程式中要已經預定義了這個鈎子,如果原程式中根本就沒有定義這個鈎子,那我們就無能為力了。

那麼目前 PG 原程式代碼中到底有多少個這種鈎子呢?我之前在基于 10.0 這個新版本中大緻搜了一下,如果沒有遺漏的話大概有 26 個。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

我們可以根據這些 hook 名稱和所在的位置大緻猜到它們的作用,比如這個第4行這個鈎子 check_password_hook 是允許我們校驗密碼的複雜度。這 5、6、7、8 是在執行 SQL 語句的前後允許我們記錄一些什麼東西,比如 SQL 執行的開始、結束時間等等。還有第10行這個ClientAuthentication_hook就是我們剛才介紹的這個auth_delay插件所利用的鈎子。還有第19行這個shmem_startup_hook是在資料庫啟動過程中,當完成共享記憶體初始化之後,允許我們自定義一些共享記憶體,用來存儲自定義的資料。

這裡我就不一一介紹每一個hook的作用了,大家後續可以根據每一個hook所處的源碼位置來确定具體的作用,然後大家自由發揮,想想這些hook可以應用在哪些場景中,然後開發出對應的插件。

SQL執行過程

也許有朋友研究過SQL語句執行的過程,主要包括以下幾個階段:

SQL語句的詞法、文法解析、SQL語義分析生成查詢樹,然後對這個查詢樹進行優化重寫。接下來生成執行計劃,然後就根據這個執行計劃進行實際的執行,最後将執行結果傳回。

在這一整個SQL執行過程中,PG資料庫在如下階段安裝有HOOK,比如在查詢樹重寫之後有一個hook,允許我們對所生成的查詢樹進行分析。再比如在sql執行的前後均有幾個hook允許我們對SQL執行的前後做一些記錄或其他事情。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

Query Tree

下面就重點介紹一下SQL語義分析之後所生成的QUERY TREE,看看QUERY TREE是長什麼樣子,都記錄哪些資訊。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

query tree在C語言中就是一個結構體,該結構體中的每個變量也都有其特定的含義,每個結構體的變量也都包含其他結構體,這樣就現成了一個多叉樹的結構,這個多叉樹的每個節點都包含這個SQL語句的某一組成部分的詳細資訊。

上面的截圖是PG資料庫将某一個SQL語句解析後的query tree的各個節點的内容列印在背景日志中。

就拿這個簡單的查詢SQL為例,首先這個根節點中的commandType為1的屬性就表示這個SQL是一個SELECT語句。還有的表示是否有聚合函數,是否有視窗函數以及是否有子查詢,是否有distinct運算,是否有for update等等。

下面這個節點,用來描述from後面的表,從哪張表中查詢資料,該表有哪些字段等等描述都在這個節點中。

再往下個節點是關于表的join的資訊,後面跟的這個節點是where條件的表達式,再後面這個傳回列的資訊,這個SQL是總共傳回兩列,是以這裡有兩個節點。

再後來就是關于排序的節點資訊,這裡有一個order by ,是以這個排序的節點就不為空。下面就是傳回資料行的描述,比如limit offset和limit count,因為這個語句中沒有寫limit,是以這兩個節點都是為空。這個就是SQL語句語義解析後的這個QUERY TREE的大緻的樣子。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

這張圖是執行計劃生成出來之後的一個多叉樹,它記錄的資訊和查詢樹有些差別,比如其中的某個節點記錄了表的掃描方式,是全表還是根據索引。

SQL規範稽核插件

有了前面的HOOK技術和SQL解析的基礎之後,接下來就介紹本次議題的主角了。SQL規範稽核插件。

首先,我們需要定義一些SQL的稽核規則,這裡羅列的是部分規則,有些是已經實作了的,有些還在開發測試中。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

這些規則中有些是針對DML,還有些規則是針對于DCL和DDL的。表中二三行表示的是當一個UPDATE語句沒有where條件,或者where條件始終為true時,需要給出警告的稽核。再下面是關于授權的稽核,比如有些dml權限不能授權給查詢使用者。下面這些是關于命名方面的規範。

規範稽核案例

有了這些規則之後,我們就來針對某些規則介紹一下實作原理。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

首先看看這個規則,update語句必須出現where子句。還是回到這個SQL執行過程,在SQL語義解析後會生成QUERY TRUE,而且在這個位置又提供了一個hook,那麼我們就可以利用這個hook來分析這個查詢樹。針對這個語句,我們就可以找到查詢樹中關于where子句的這個節點,看看這個節點是否為空即可。

有時候可能對這個QUERY TREE結構不了解的初學者來說,一下子找不到相應的節點。那麼這裡有個小技巧,就是可以寫一個另外一個相似的SQL語句,一個帶where條件,一個不帶where條件,然後用類似于UE文本比較的工具,看看這兩個SQL語句列印出來的查詢樹有些什麼差異,通過這種方法可以快速定位到所需要的Node。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

再來看看下一個規則,剛才的這個規則是判斷where條件是否為空,接下來這個規則是update語句有寫where條件,但是有可能這個where條件的表達式始終為true,這樣就相當于沒有where條件,也會做全表update。比如這兩個語句:一個where條件為1=1,另一個where條件就直接是true。

如果我們這個使用仍然向上一個規則一樣來分析query true行不行呢?顯然是不行的,因為在這個查詢樹中,where條件這個節點并不為空,它是包含where條件的,是以我們需要換一種思路。

前面我們了解到,在執行計劃生成後也會産生一個執行計劃plan tree,我們可以看看這個plan tree中的這個where條件的節點長什麼樣子。

通過對比,我們可以發現如果where條件表達式的邏輯始終為true的話,在執行計劃樹中的這個節點也為空,這說明優化器在生成執行計劃的時候就直接忽略掉這個where條件。是以通過這種方法,我們也可以達到我們判斷業務邏輯的目的。當然這個規則的判斷需要安裝在SQL語句執行前的這個鈎子上。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

下面再來看看第三個規則,關于授權。這個規則的意思是不能将某些DML權限授權給查詢使用者,這些查詢使用者隻能授與這些白名單上的權限,比如select、usage等,類似與這個語句。

要稽核這個規則,我們就需要使用這個鈎子,這個鈎子是定義在執行類似于這種授權語句的代碼中。我們可以去分析這個授權SQL語句語義解析後的這個結構體。具體判斷這個結構體中授權對象是不是QRY角色,以及是不是有超出白名單的權限。進而判斷是否違規授權。

查詢樹周遊

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

前面我們介紹了三條規則稽核的例子。其中前兩個例子中都有提到那顆多叉樹。雖然我們人眼一眼就可以定位到我們關系的樹中的某個node,但是程式程式是如何定位的呢。這就涉及到樹的周遊的問題了。

還記得上圖圖中這一大段的内容是從哪裡來的嗎,對了就是從背景日志中擷取到的,PG程式肯定有地方也會周遊這課樹,把這個樹中的每個節點的内容列印出來。

在源碼的src/backend/nodes/outfuncs.c位置可以找到周遊樹方法。我們可以仿照着這個周遊的過程也寫一個周遊數的函數,當周遊某個節點時再根據規則名稱來判斷對應節點的内容是否符合違規條件。比如這個是周遊plan tree樹中對表進行全表掃描的節點,看看這個節點中過濾條件的節點是否為空,如果為空則說明該SQL語句會影響整表的資料。

下面顯示的就是稽核插件的最終效果。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

特點總結

下面總結一下稽核插件的特點:

第一點,這個插件非常輕量,因為它使用的是資料庫核心原生的SQL解析器,不需要額外開發的SQL解析器對SQL語句進行解析。是以對SQL語句執行的響應時間影響極小。

第二點隻讀、安全。因為整個過程對核心沒有任何修改操作,隻是讀取核心SQL解析器解析後的查詢樹等中間産物,是以是安全可靠的。

第三點,相容性強,這個插件對PG社群版的9.4、9.5、9.6以及最新的10.0都是相容的。對于企業版的EDB9.4、9.5、9.6版本也是相容的。第四點,靈活可控。我們可以很友善的啟用或禁用某些SQL稽核項。并且可以靈活的控制違規後的攔截級别,是僅僅警告還是攔截,都可以靈活設定。

SQL 稽核:基于PG資料庫插件hook的SQL規範稽核工具

未來展望

最後看看未來的展望。一方面是增加更多的SQL規範稽核項,另一方面是希望可以增加一些SQL語句優化的建議的功能,當出現違規SQL之後同時能給出優化後的SQL語句。再一個就是今後還會開發出更多使用的插件出來。

本次分享的主要内容講完了,謝謝大家!

原文釋出時間為:2018-07-11

本文作者:陳剛

本文來自雲栖社群合作夥伴“

資料和雲

”,了解相關資訊可以關注“

”。