Linux 核心中的 static_key 機制
目錄
問題來源:惡意程式檢測
最近,主要由於在研討一些關於LINUX被惡意程式ROOT後,可能會被修改程式碼段中的資料。為了防止程式碼段被修改,採用幾種特殊的機制來保護程式碼段的資料不被篡改,當有惡意程式試圖修改程式碼段或只讀資料段中的資料,特殊保護機制會自動忽略這種惡意操作,並追溯該操作的PC地址,棧指標,程序號等,從而找到惡意程式的源頭,從而處理惡意程式。
問題就是來源這種特殊的保護機制啟動後,當安裝一些官方驗證後的APK後,我們發現保護機制居然真的報警了,這意味著有程式在修改程式碼段。起初我們懷疑該APK自帶ROOT工具,但是後來通過特殊機制進行回追出錯的地址,回溯棧指標,發現是由於LINUX系統引入了Static_Keys機制,該機制中的部分函式的實現利用了程式碼段修改來實現的,而不是每次都是通過變數判斷,從而直接改變程式碼段。而修改程式碼段的工作就交給了一個函式就是DO_ONCE中的__do_once_done中,那麼我們就先來分析一下程式碼。
DO_ONCE函式解析
DO_ONCE機制想法很簡單,有些函式只應該呼叫一次,那麼這些函式呼叫一次後,如果下次再呼叫就應該直接返回。以前我們要實現這種功能總是需要判斷一個變數,而使用DO_ONCE機制,自動幫你完成判斷,保證函式只會在第一次呼叫的時候被執行,以後都直接返回。很自然,DO_ONCE機制就採用了Static_Keys來實現。使用static_key外 ,另一個關鍵在__do_once_done中。
#define DO_ONCE(func, ...) \ ({ \ bool ___ret = false; \ static bool ___done = false; \ static DEFINE_STATIC_KEY_TRUE(___once_key); \ if (static_branch_unlikely(&___once_key)) { \ unsigned long ___flags; \ ___ret = __do_once_start(&___done, &___flags); \ if (unlikely(___ret)) { \ func(__VA_ARGS__); \ __do_once_done(&___done, &___once_key, \ &___flags); \ } \ } \ ___ret; \ })
如果有一個func只能被呼叫一次,例如初始化函式的話,則可以用這個巨集,如下例所示foo這個函式即使被呼叫兩次,也只會執行一次:
/* Call a function exactly once. The idea of DO_ONCE() is to perform * a function call such as initialization of random seeds, etc, only * once, where DO_ONCE() can live in the fast-path. After @func has * been called with the passed arguments, the static key will patch * out the condition into a nop. DO_ONCE() guarantees type safety of * arguments! * * Not that the following is not equivalent ... * * DO_ONCE(func, arg); * DO_ONCE(func, arg); * * ... to this version: * * void foo(void) * { * DO_ONCE(func, arg); * } * * foo(); * foo(); * * In case the one-time invocation could be triggered from multiple * places, then a common helper function must be defined, so that only * a single static key will be placed there! */
STATIC KEYS
簡單的說,如果你程式碼對效能很敏感,而且大多數情況下分支路徑是確定的,可以考慮使用Static Keys。Static Keys可以代替使用普通變數進行分支判斷,目的是用來優化頻繁使用if-else判斷的問題,這裡涉及到指令分支預取的一下問題。簡單地說,現代cpu都有預測功能,變數的判斷有可能會造成硬體預測失敗,影響流水線效能。雖然有likely和unlikely,但還是會有小概率的預測失敗。
下面使用例子說明一下:
//定義一個Static Keys,並且預設這個值是false。
DEFINE_STATIC_KEY_FALSE(key);…
//程式碼使用Static Keys代替普通變數進行判斷,static_branch_unlikely是一個巨集,展開後不會有真正的判斷,而是直接執行false分支,即 do likely code。
if (static_branch_unlikely(&key))
do unlikely code
else
do likely code…
這樣的好處是,上述程式碼的效能和沒有分支判斷的效能差不多,具體可能只差一個nop指令的執行時間。 當然,如果某種情況發生了,需要改變分支的執行路徑,可以呼叫下面的介面: static_branch_enable(&key); 執行static_branch_enable(&key)後,底層通過gcc提供的goto功能,再結合c程式碼編寫的動態更改記憶體功能,就可以讓使用key的程式碼從執行false分支變為執行true分支。當然這個更改代價是比較昂貴的,不是所有情況都適用。可以改變分支的函式參照如下
#define static_branch_inc(x) static_key_slow_inc(&(x)->key)
#define static_branch_dec(x) static_key_slow_dec(&(x)->key)
#define static_branch_inc_cpuslocked(x) static_key_slow_inc_cpuslocked(&(x)->key)
#define static_branch_dec_cpuslocked(x) static_key_slow_dec_cpuslocked(&(x)->key)
/*
* Normal usage; boolean enable/disable.
*/
#define static_branch_enable(x) static_key_enable(&(x)->key)
#define static_branch_disable(x) static_key_disable(&(x)->key)
#define static_branch_enable_cpuslocked(x) static_key_enable_cpuslocked(&(x)->key)
#define static_branch_disable_cpuslocked(x) static_key_disable_cpuslocked(&(x)->key)
GOTO
gcc4.5提供了一個特性用於嵌入式彙編,那就是asm goto,其實這個特性沒有什麼神祕之處,就是在嵌入式彙編中go to到c程式碼的label,其最簡單的用法如下(來自gcc的文件): int frob(int x) { int y; asm goto ("frob %%r5, %1; jc %l[error]; mov (%2), %%r5" : : "r"(x), "r"(&y) : "r5", "memory" : error); return y; error: return -1; }
按照原理來說"asm goto"其實就是在outputs,inputs,registers-modified之外提供了嵌入式彙編的第四個“:”,後面可以跟一系列的c語言的label,然後你可以在嵌入式彙編中goto到這些label中一個。然而使用"asm goto"可以巧妙地將"執行時修改載入記憶體的二進位制程式碼”規範化,就是說你只需要呼叫一個統一的介面巨集,編譯器就將你想實現的東西給實現了,要不然程式碼寫起來會很麻煩,這點上,編譯器不嫌麻煩。具體為什麼要動態修改二級制程式碼的原因還是與前面介紹的指令分支預取有關,為了極大的優化系統性能。
JUMP TABLE
jump_lable遮蔽不同體系更改機器程式碼的不同,向上提供一個統一介面。不同體系會提供給jump_lable一個體系相關的實現。 jump_lable的實現原理很簡單,就是通過替換記憶體中機器程式碼的"nop"空指令為"b"指令,或者替換機器程式碼的“b”指令為“nop”空指令,實現分支的切換.
static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
{
asm goto("1: nop\n\t"
".pushsection __jump_table, \"aw\"\n\t"
".align 3\n\t"
".quad 1b, %l[l_yes], %c0\n\t"
".popsection\n\t"
: : "i"(&((char *)key)[branch]) : : l_yes);
return false;
l_yes:
return true;
}
static __always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)
{
asm goto("1: b %l[l_yes]\n\t"
".pushsection __jump_table, \"aw\"\n\t"
".align 3\n\t"
".quad 1b, %l[l_yes], %c0\n\t"
".popsection\n\t"
: : "i"(&((char *)key)[branch]) : : l_yes);
return false;
l_yes:
return true;
}
__do_once_done
void __do_once_done(bool *done, struct static_key_true *once_key,
unsigned long *flags)
__releases(once_lock)
{
*done = true;
spin_unlock_irqrestore(&once_lock, *flags);
once_disable_jump(once_key);
}
EXPORT_SYMBOL(__do_once_done
這個函式實在執行完第一次—do_once_start後需要修改程式碼段將jump修改成nop,最後還是呼叫了static_branch_disable(work->key); 具體修改程式碼段的程式碼追溯後到arch_jump_label_transform:
void arch_jump_label_transform(struct jump_entry *entry,
enum jump_label_type type)
{
void *addr = (void *)entry->code;
u32 insn;
if (type == JUMP_LABEL_JMP) {
insn = aarch64_insn_gen_branch_imm(entry->code,
entry->target,
AARCH64_INSN_BRANCH_NOLINK);
} else {
insn = aarch64_insn_gen_nop();
}
aarch64_insn_patch_text(&addr, &insn, 1);
}
int __kprobes aarch64_insn_patch_text_nosync(void *addr, u32 insn)
{
u32 *tp = addr;
int ret;
/* A64 instructions must be word aligned */
if ((uintptr_t)tp & 0x3)
return -EINVAL;
ret = aarch64_insn_write(tp, insn);
if (ret == 0)
flush_icache_range((uintptr_t)tp,
(uintptr_t)tp + AARCH64_INSN_SIZE);
return ret;
}
最後呼叫到__aarch64_insn_write,進行程式碼段的重對映,然後將jump tabel的頭地址“b ”替換成“nop”,取消對映,返回。
static int __kprobes __aarch64_insn_write(void *addr, __le32 insn)
{
void *waddr = addr;
unsigned long flags = 0;
int ret;
raw_spin_lock_irqsave(&patch_lock, flags);
waddr = patch_map(addr, FIX_TEXT_POKE0);
ret = probe_kernel_write(waddr, &insn, AARCH64_INSN_SIZE);
patch_unmap(FIX_TEXT_POKE0);
raw_spin_unlock_irqrestore(&patch_lock, flags);
return ret;
}
作為安全工程師,linux提供這種可以修改的程式碼段的函式,如果是用於除錯還可以理解,但是如果公佈的版本還是提供,是不是給一些專家提供了便捷的途徑,還是那句話,"安全是一個平衡,效能和安全從來就是相對的,具體看應用的場景吧“。
CONFIG
如果啟動功能,需要在開啟線面的兩個開關,目前Linux在x86上是強制開啟的,ARM上還是根據需求來開啟。
#if defined(CC_HAVE_ASM_GOTO) && defined(CONFIG_JUMP_LABEL)
# define HAVE_JUMP_LABEL
#endif
問題解決:
最後,我先關掉了這個開關,特殊程式碼段檢測程式已經不會再檢測到有人修改的異常了。稍後,我會對這種修改做特殊處理,但是需要特殊識別,我的程式又要複雜了。