閱讀124 返回首頁    go 阿裏雲 go 技術社區[雲棲]


內核級sandbox設計原理與實現

作者:王智通

 

Index
1 – 背景
1.1 – 現有的技術方法
1.1.1 – selinux/apparmor
1.1.2 – Hook sys_call_table
1.2 – LKM or Patch
2 – 原理
2.1 – 截獲中斷處理程序
2.2 – Protect kernel from kernel
3 – 源碼
——[ 1 - 背景
飛天係統會運行來自第三方的不可信二進製程序, 黑客可以隨意提交後門, 蠕蟲, rootkit,掃描, 溢出攻擊等等惡意程序,因此需要一些防護措施來減小這些代碼對係統安全造成的損失。Sandbox 的目的在於控製程序對係統的控製能力。但 SandboxP1 僅僅實現了降低用戶uid 來控製用戶行為的能力, 這遠遠不夠。
黑客可以利用操作係統漏洞來達到權限提升的目的,從而突破 SandboxP1 的防護。本文提供了一種利用 LKM 內核模塊來實現 Sandbox 的技術方法, 通過使用 System call hook 的技術來達到監視用戶 api 和防禦內核攻擊的能力。
---[ 1.1 - 現有的技術方法
-[ 1.1.1 - selinux/apparmor
它們通過使用 LSM(linux security module)提供的安全鉤子函數來實現對用戶 api 的監控, 包括進程/進程間通訊,文件係統, 網絡係統都提供了足夠的鉤子函數:
struct security_operations {
int (*ptrace) (struct task_struct * parent, struct task_struct * child);
...
int (*bprm_alloc_security) (struct linux_binprm * bprm);
...
int (*sb_alloc_security) (struct super_block * sb);
...
int (*sb_mount) (char *dev_name, struct nameidata * nd,
char *type, unsigned long flags, void *data);
...
int (*inode_rmdir) (struct inode *dir, struct dentry *dentry);
...
int (*task_setuid) (uid_t id0, uid_t id1, uid_t id2, int flags);
...
int (*shm_shmctl) (struct shmid_kernel * shp, int cmd);
...
int (*netlink_send) (struct sock * sk, struct sk_buff * skb);
...
int (*socket_listen) (struct socket * sock, int backlog);
}
selinux/apparmor 通過像 struct security_operations 結構注冊相應鉤子函數, 完成監控。
-[ 1.1.2 - Hook sys_call_table
通過替換 sys_call_table[]數組中的函數地址,來截獲係統調用, 這在 windows 下的安全軟件中常常使用。 如果要監控所有的 api, 那麼需要重新編寫所有 api 的替代函數。linuxkernel2.6.18 中大概有 300 多個係統調用函數, 編寫新函數是非常浪費時間的, 而且會使
sandbox 變得非常龐大,不好管理。
—[ 1.2 - LKM or Patch
本文提出一種新的 system call hook 的技術方法, 通過 int $0x80 中斷處理程序來達到監視所有 api 的目的。如果采用內核補丁的方式, 將會很容易實現, 因為可以隨意的給 kernel 打補丁。
但是為此付出的代價就是要重新編譯 kernel,對於飛天係統來說不是很容易維護。 采用 LKM 的方式可以動態加載內核模塊,不用時又可以動態卸載,非常方便, 但編寫難度很大, LKM 模塊在加載到內核後,需要一係列的 hack 技巧來完成中斷處理程序的截獲, 下文會有詳細的介紹。
------ [ 2 - 原理
--- [ 2.1 - 截獲中斷處理程序
我們的目的是劫持中斷處理程序, 通過應用層成功傳遞的係統調用號來達到監視某個 API的作用,具體的調用號在 incude/asm-i386/unistd.h 中有定義:
#define __NR_restart_syscall
0
#define __NR_exit
#define __NR_fork
1
2
#define __NR_read
3
...
#define __NR_move_pages
317
#ifdef __KERNEL__
#define NR_syscalls
318
先看下內核是如何實現 int $0x80 處理程序的:
arch/i386/kernel/entry.S:
ENTRY(system_call)
RING0_INT_FRAME
pushl %eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL
GET_THREAD_INFO(%ebp)
testl $TF_MASK,EFLAGS(%esp)
jz no_singlestep
orl $_TIF_SINGLESTEP,TI_flags(%ebp)
no_singlestep:
testw
$(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%eb
p)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
pushl %eax 保存的就是係統調用號, SAVE_ALL 保存所有寄存器的值, 應用程序從 ring3 進入 ring0 的時候要進行堆棧切換,將使用內核堆棧進行操作。在經過信號檢查後, 判斷此次係統調用是否需要跟蹤,使用 ptrace 進行 api 監控的 sandbox 就是這個原理。然後判斷係統調用號是否超出了係統提供的最大值。
然後通過 call 指令根據寄存器號執行對應的係統調用函數。sys_call_table 是個指針數組,裏麵的每一項都保存著係統調用函數的地址,通過%eax*4 就可以得到數組中的偏移,取值就是這個係統調用函數的地址。通過替換數組中的地址,可以完成係統調用的截獲:
orig_sys_read = sys_call_table[__NR_read];
sys_call_table[__NR_read] = new_sys_read;
這種方法的缺點就是要編寫很多函數的替代品, 2.6.18 有 317 個函數, 比較麻煩。
我們現在需要一種方法來進行統一的判斷, 就是想法讓所有 api 的截獲都到一個函數裏去判斷,然後又可以調用不同的原始函數, 這樣管理起來非常方便。
通過前麵的代碼,我們知道 ptrace這個係統調用就可以完成類似的事情, 但是它要跟蹤這個係統調用兩次, 係統調用執行前跟蹤一次,調用執行後在跟蹤一次, 而且在跟蹤的時候, 這個係統調用本身都已經運行完了。 看樣子 ptrace 的代碼可以完成我們之前提到的統一入口函數功能上, 去看下它怎麼實現的:
)
syscall_trace_entry:
movl $-ENOSYS,EAX(%esp)
movl %esp, %eax
xorl %edx,%edx
call do_syscall_trace
cmpl $0, %eax
jne resume_userspace
# ret != 0 -> running under PTRACE_SYSEMU,
# so must skip actual syscall
movl ORIG_EAX(%esp), %eax
cmpl $(nr_syscalls), %eax
jnae syscall_call
jmp syscall_exit
首先設置一個錯誤的值給堆棧中的 eax 值, eax 是所有函數的返回值。 在用 eax 保存堆棧指 針 。 等會 看到 do_syscall_trace 的 參數 是 通過 堆 棧方 式 來傳 遞 的。 然 後 開始 調 用do_syscall_trace , 開 始 跟 蹤 過 程 , 當 其 執 行 完 後 , 如 果 不 成 功 , 就 直 接 掉 轉 到resume_userspace 出退出本次係統調用。如果成功, 還得繼續讓其把這個係統調用執行完,ORIG_EAX 保存的是係統調用號,再次判斷係統調用號是否大於 nr_syscalls, 如果大於就跳到 syscall_exit,退出本次係統調用, 否則跳到 syscall_call 出完成本次係統調用的執行:
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,EAX(%esp)
syscall_exit 跟 resume_userspace 的不同之處在於, 在係統調用退出的時候還要在進行一次跟蹤。現在看下 do_syscall_trace 具體是怎麼實現的:
arch/i386/kernel/ptrace.c:
__attribute__((regparm(3)))
int do_syscall_trace(struct pt_regs *regs, int entryexit)
{

}
到目前為止,我們知道 ORIG_EAX 保存的是係統調用號, 又有 do_syscall_trace 這樣的函數可以完成統一監控的入口函數。 那麼我們隻需要編寫一個類似 do_syscall_trace 的函數, 根據 eax 值完成基於 sandbox 規則的判斷,對其放行或拒絕。 編寫一個 sandbox_syscall_trace的 函 數 比 較 簡 單 , 難 的 是 我 們 還 需 要 修 改 system_call 函 數 , 讓 其 能 跳 轉 到sandbox_syscall_trace 中。 由於不能給內核打補丁,所以需要一係列 hack 手段來完成system_call 函數的修改, 將赤裸裸的 rootkit 技術用於其中。
1、 模塊加載後找到 system_call 函數的地址。
2、 從 system_call 開始, 搜索 jae syscall_badsys 的機器碼。
3、 替換掉 cmpl $(nr_syscalls), %eax 和 jae syscall_badsy 的機器碼, 讓其跳轉到我們的新處理函數 asbox_idt_handler 中。
4、 asbox_idt_handler 完成係統調用的監控。
1、 怎樣得到 system_call 函數的地址
idtr 寄存器保存的就是內核使用的 idt 表的地址,可以使用 sidt 執行來取得 idt 的地址。
__asm__ volatile (“sidt %0″: “=m” (idt48));
system_call 處理程序保存在 idt 表的第 0×80 選項上, 通過解析它即可得到 system_call 的地址。
pIdt80 = (struct descriptor_idt *)(idt48.base + 8 * 0×80);
addr = (pIdt80->offset_high << 16 | pIdt80->offset_low);
if (!addr) {
DbgPrint(“oh, shit! can’t find system_call address.\n”);
return 0;
}
2、怎樣得到 sys_call_table 的地址
將 vmlinux 反匯編看看:
c1003cc4 <system_call>:
c1003cc4: 50
push
%eax
c1003cc5: fc cld c1003cc6: 06 push %es
c1003cc7: 1e push %ds
c1003cc8: 50 push %eax
c1003cc9: 55 push %ebp
c1003cca: 57 push %edi
c1003ccb: 56 push %esi
c1003ccc: 52 push %edx
c1003ccd: 51 push %ecx
c1003cce: 53 push %ebx
c1003ccf: ba 7b 00 00 00 mov $0x7b,%edx
c1003cd4: 8e da movl %edx,%ds
c1003cd6: 8e c2 movl %edx,%es
c1003cd8: bd 00 f0 ff ff mov $0xfffff000,%ebp
c1003cdd: 21 e5 and
c1003cdf: f7 44 24 30 00 01 00 %esp,%ebp
testl $0×100,0×30(%esp)
c1003ce6: 00 je
c1003ce7: 74 04 orl
c1003ce9: 83 4d 08 10
c1003ced <no_singlestep>
$0×10,0×8(%ebp)
c1003ced <no_singlestep>:
c1003ced: 66 f7 45 08 c1 01 testw $0x1c1,0×8(%ebp)
c1003cf3: 0f 85 bf 00 00 00 jne
c1003cf9: 3d 3e 01 00 00 cmp
c1003db8 <syscall_trace_entry>
$0x13e,%eax
c1003cfe:
0f 83 27 01 00 00
jae call
mov
c1003d04 <syscall_call>:
c1003d04:
ff 14 85 e0 e4 1e c1
c1003d0b:
89 44 24 18
call
c1003e2b <syscall_badsys>
*0xc11ee4e0(,%eax,4)
%eax,0×18(%esp)
*0xc11ee4e0(,%eax,4) 這 條 指 令 調 用 具 體 的 係 統 調 用 函 數 , 0xc11ee4e0 就 是sys_call_table 的地址。 ff 14 85 e0 e4 1e c1 是其機器碼, 於是我們可以從 system_call 地址,開始向下搜索 0xff0x140x85, 後麵便是 sys_call_table 的地址了。
void *get_sct_addr(void)
{
unsigned char *p;
unsigned int sct;
p = (unsigned char *)system_call_addr;
while (!((*p == 0xff) && (*(p + 1) == 0×14) && (*(p + 2) == 0×85)))
p++;
syscall_start = (unsigned long)p;
p += 3;
sct = *((unsigned int *)p);
p += 4;
syscall_end = (unsigned long)p;

}
注意這個函數不僅把 sys_call_table 的地址記錄下來了, 還把 syscall_call, syscall_exit 標號出的地址也記錄下來了。 後麵 sandbox 處理函數還會用到這些地址。
3、 搜索並替換 jae syscall_badsys 的機器碼
c1003cf9:
c1003cfe:
3d 3e 01 00 00
0f 83 27 01 00 00
cmp
$0x13e,%eax
jae
c1003e2b <syscall_badsys>
從 system_call 地址開始向後搜索 0x0f0x83,
找到後向前定位 5 個字節,便是 cmp 指令地址。
用 push xxxx;ret 機器碼替換掉它們。這樣當執行到這個地址的時候, push 指令會把 sandbox的處理函數 asbox_idt_handler 地址壓入堆棧,在通過 ret 指令將其彈出,完成函數的跳轉。
void set_idt_handler(void)
{
unsigned char buf[4] = “\x00\x00\x00\x00″;
unsigned int offset = 0;
unsigned char *p, *p1;
unsigned long *p2;
p = (unsigned char *)system_call_addr;
while (!((*p == 0x0f) && (*(p + 1) == 0×83)))
p++;
//printk(“found opcode.\n”);
// found syscall_badsys addr.
p1 = p + 2;
buf[0] = p1[0];
buf[1] = p1[1];
buf[2] = p1[2];
buf[3] = p1[3];
offset = *(unsigned int *)buf;
printk(“offset: 0x%08x\n”, offset);
syscall_badsys = offset + (unsigned int)p1 + 4;
printk(“jmp_badsys_addr: 0x%08x\n”, syscall_badsys);
// found resume_userspace addr.
p1 = (unsigned char *)syscall_badsys;
while (!(*p1 == 0xe9))
p1++;
printk(“found opcode 0xe9.\n”);
p1++;
buf[0] = p1[0];
buf[1] = p1[1];
buf[2] = p1[2];
buf[3] = p1[3];
offset = *(unsigned int *)buf;
printk(“offset: 0x%08x\n”, offset);
resume_userspace = offset + (unsigned int)p1 + 4;
printk(“resume_userspace_addr: 0x%08x\n”, resume_userspace);
p -= 5;
*p++ = 0×68;
p2 = (unsigned long *)p;
*p2++ = (unsigned long)((void *)asbox_idt_handler);
p = (unsigned char *)p2;
*p = 0xc3;

}
4、 asbox_idt_handler 完成係統調用的監控。
asbox_idt_handler 先完 成係 統調用 號是 否超 出內 核提 供的最 大值 , 超出 則直 接跳到syscall_exit 地址出, 這個地址前麵我們已經找到了。 否則跳到 asbox_syscall_hook 出繼續執行。
void asbox_idt_handler(void)
{
__asm__(“cmp %0, %%eax\n”
“jae asbox_syscall_bad\n”
“jmp asbox_syscall_hook\n”
“asbox_syscall_bad:\n”
“jmp syscall_exit\n”
::”i”(NR_syscalls));
}
asbox_syscall_hook 調用 asbox_syscall_trace,並使係統調用繼續執行或退出。
void asbox_syscall_hook(void)
{
__asm__(“movl %esp, %eax\n”
“call asbox_syscall_trace\n”
“cmpl $0, %eax\n”
“jne asbox_resume_userspace\n”
“movl 0×24(%esp), %eax\n”
“pushl syscall_start\n”
“ret\n”
“pushl syscall_end\n”
“ret\n”
“asbox_resume_userspace:\n”
“pushl syscall_badsys\n”
“ret\n”
);
}
asbox_syscall_trace 是我們的主監控函數:
考慮到係統上的 sandbox 用戶非常多, 為了加快監控速度, 我們將為每個 sandbox 用戶提供一個哈希表, 根據用戶 uid 算出哈希表中對應的地址, 這個地址是一個雙向鏈表,鏈接著要監控的係統調用號。
struct asbox_uid_node {
int uid;
int sys_bit[MAX_BIT];
struct list_head list;
};
struct asbox_uid {
struct list_head uid_list[ASBOX_MAX_UID];
spinlock_t uid_list_lock[ASBOX_MAX_UID];
}asbox_uid_cache;
asbox_uid_cache 用來管理哈希表, 通過 uid 來得到哈希表中的索引。
int asbox_uid_hash(int uid)
{
return uid;
}
SandboxP2 中直接返回 uid 值。
通過 asbox_uid_cache.uid_list[uid_hash]來訪問 asbox_uid_node 結構。
每個係統調用都有一個 asbox_uid_node 的結構。 sys_bit 是個位圖數組, 通過移位運算就可以快速的算出這個係統調用號該不該被監控。
asbox_uid_cache
|
V +————————–+ +————————–+ +————————-+
+———–+ | asbox_uid_node1 | -> | asbox_uid_node2 | -> … -> | asbox_uid_nodeN |
| uid1 | ->
+———–+ +————————–+ +————————–+ +————————-+
+———–+ +————————–+ +————————–+ +————————-+
| uid2 | -> | asbox_uid_node1 | -> | asbox_uid_node2 | -> … -> | asbox_uid_nodeN |
+———–+ +————————–+ +————————–+ +————————-+

+———–+
+————————–+ +————————–+ +————————-+
| uidN | -> | asbox_uid_node1 | -> | asbox_uid_node2 | -> … -> | asbox_uid_nodeN |
+———–+ +————————–+ +————————–+ +————————-+
模塊通過 init_asbox_hash_list()來初始化嘻哈表,通過/proc 接口來動態增加哈希表項。
check_sys_bit 通過移位運算快速判斷一個係統調用是不是需要被監控:
int check_sys_bit(int idx, struct asbox_uid_node *node)
{
int tmp;
if (idx >= NR_syscalls) {
return -1;
}
if ((idx / 32) >= MAX_BIT || (idx / 32) < 0) {
return -1;
}
//sys_bit[idx / 32] >>= (idx % 32);
tmp = (node->sys_bit[idx / 32]) >> (idx % 32);
if (0×1 & tmp) {
printk(“idx: %d was set.\n”, idx);
return 0;
}
return -1;
}
通過用戶程序 setproc.c 來完成係統調用的設置。
— [ 2.1 – Protect kernel from kernel
通過監控用戶 api, 可以完成 sandbox 功能。 但是不能阻止 zero day 的攻擊。 黑客可以使用未公開的係統漏洞來獲得權限提升, 然後可以安裝 rootkit 來破壞 sandbox 程序, 或完成進程,網絡,文件的隱藏等等功能。因此在 sandbox 之後, 還要加入內核自身防護的功
能。 當模塊加載的時候,開啟一個內核線程,每隔 5 秒中作一次防禦。
static int asbox_protect_thread(void *arg)
{
daemonize(“asbox_thread”);
allow_signal(SIGTERM);
while (!signal_pending(current)) {
if (!check_syscall_addr()) {
printk(“syscall table is ok.\n”);
}
if (!check_inline_hook()) {
printk(“syscall opcode is ok.\n”);
}
if (!check_idt_handler()) {
printk(“idt handler is ok.\n”);
}
if (!check_asbox_idt_hook()) {
printk(“asbox idt hook is ok.\n”);
}
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(5 * HZ);
}
complete_and_exit(&thread_exited, 1);
}
check_syscall_addr 用來檢查 system_call 地址是否改變, check_inline_hook 用來檢查係統調用函數是否被植 入了 inline hook 鉤 子, check_idt_handler 用來完 成 idt 表 的檢查,check_asbox_idt_hook 用來完成自身 hook 的防禦, 看其有沒有被其他模塊修改掉。 詳細代碼可參加 protect.c。
——[ 3 – 源碼
附件中提供了 SandboxP2 demo 的完整實現,名為 asbox = as a sandbox, 又為 apsara sandbox。
使用 git 進行版本控製, 有興趣的同學可以一起來開發:)

最後更新:2017-04-03 07:57:04

  上一篇:go Arale 背後的一些設計理念
  下一篇:go ttf設置文字字體