天天看點

系統調用捕獲和分析—必備的系統安全的知識點

本文為畢業設計過程中學習相關知識、動手實踐記錄下來的完整筆記,通過閱讀本系列文章,您可以從零基礎了解系統調用的底層原理并對系統調用進行攔截。由于本人能力有限,文章中可能會出現部分錯誤資訊,如有錯誤歡迎指正。另外,本系列所有内容僅作為個人學習研究的筆記,轉載請标明出處。感謝您的關注!

完整系列文章清單

​​​系統調用捕獲和分析—通過ptrace擷取系統調用資訊​​​​系統調用捕獲和分析—通過strace擷取系統調用資訊​​

文章目錄

  • ​​系統調用傳參規則​​
  • ​​尋找中斷服務程式入口位址​​
  • ​​中斷号與系統調用号​​
  • ​​系統調用相關資料結構​​
  • ​​系統調用号​​
  • ​​系統調用表​​
  • ​​應用程式、函數庫、核心、驅動程式關系​​
  • ​​裝置檔案、主裝置号、從裝置号​​
  • ​​linux下系統調用表分析​​
  • ​​kprobe、jprobe、kretprobe輕量級核心調試機制​​
  • ​​解開宏定義SYSCALL_DEFINE3​​

系統調用傳參規則

普通函數調用是通過将參數壓棧的方式傳遞的。系統調用從使用者态切換到核心态,在兩種執行模式下使用的是不同的堆棧,即程序的使用者态和程序的核心态堆棧,傳遞參數方法無法通過參數壓棧的方式,而是通過寄存器傳遞參數的方式。

在32位x86 Linux系統中,可用的系統調用定義在/usr/include/asm/unistd_32.h頭檔案中。

每個系統調用都對應一個編号以及若幹個參數。系統調用編号存到%eax,将參數依次存到%ebx, %ecx, %edx, %esi, %edi, %ebp中,然後再執行int $0x80指令。如果超過 6 個就把某一個寄存器作為指針指向記憶體,這樣就可以通過記憶體來傳遞更多的參數。

在64位x86_64 Linux系統中,系統調用定義在/usr/include/asm/unistd_64.h頭檔案中。

每個系統調用都對應一個編号以及若幹個參數。将系統調用編号存到%rax,将參數依次存到%rdi, %rsi, %rdx, %r10, %r8, %r9中,然後再執行syscall指令。如果超過 6 個就把某一個寄存器作為指針指向記憶體,這樣就可以通過記憶體來傳遞更多的參數。

尋找中斷服務程式入口位址

尋找中斷服務程式入口位址的方法有兩種:硬體向量法、軟體查詢法。

這裡隻記錄硬體向量法。

硬體向量法利用硬體産生向量位址,再由向量位址找到中斷服務程式的入口位址。

由向量位址找中斷服務程式的入口位址通常采用兩種方法:一是在向量位址存放一條無條件跳轉指令。二是設定向量位址表。

向量位址表設在存儲器内,存儲單元的位址為向量位址,存儲單元的内容為服務的入口位址。

中斷号與系統調用号

中斷号

系統調用是通過軟中斷機制實作的,并且一個作業系統的系統調用都通過同一個中斷入口來實作。在linux系統中,所有的系統調用都是通過中斷号(注意不是系統調用号) 80H來進入。

系統調用号

每個系統調用被賦予一個系統調用号。當使用者空間的程序執行一個系統調用的時候,這個系統調用号就用來指明要執行哪個系統調用。程序不會提及系統調用的名稱。

事實上,在​

​int 80h​

​​之前,需要先将系統調用号放到​

​eax​

​寄存器中。

結合ubuntu下linux核心檔案下arch/x86/entry/syscalls/syscall_64.tbl(低版本有),可以斷定ubuntu中的使用的是向量位址表。

系統調用相關資料結構

系統調用号

系統調用号所在檔案路徑​

​/usr/include/x86_64-linux-gnu/asm/​

​​路徑下的​

​unistd_64.h​

​(還有其他unistd頭檔案)。其中一部分如下。

#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1
\
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5      

系統調用表

linux利用函數指針數組儲存系統調用函數的位址,這個數組稱為系統調用表(sys_call_table)。

作業系統利用系統調用号為下标在系統調用表中找到相應的系統調用服務例程函數指針并跳轉到相應位址,實作系統調用服務例程的尋址與調用。

按理說找到sys_call_table的定義就可以看到其中的函數指針數組。但是事與願違。

首先下載下傳linux源碼

apt-cache search linux-source
apt install linux-source-5.3.0
cd /usr/src
tar -jxv -f linux-source-5.3.0.tar.bz2 -C /home/lab      

然後在​

​linux-source-5.3.0/arch/x86/um/sys_call_table_64.c​

​​中找到​

​sys_call_table​

​定義

const sys_call_ptr_t sys_call_table[] ____cacheline_aligned = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};      

​[0 ... __NR_syscall_max] = &sys_ni_syscall​

​​将數組内容全部初始化為未實作版本,然後包含​

​asm/syscalls_32.h​

​​當中逐項初始化的内容進行初始化。

asm/syscalls_32.h是編譯期間生成的一個頭檔案,之後就會将函數指針指派到數組中。(百度得到的結果)

補充

/usr/src/linux/include/linux是給編譯核心用的

/usr/include/linux是給編譯應用程式用的*

應用程式、函數庫、核心、驅動程式關系

應用程式調用一系列函數庫提供的封裝好的函數實作功能。

函數庫中部分函數不需要核心支援,隻是純高層代碼實作,直接完成相關功能。

函數庫中的另一部分函數需要核心支援,通過系統調用,由核心完成對應功能。

核心中的一部分需要直接操作硬體,這部分核心代碼叫驅動程式。

另一部分代碼不直接操作硬體,這部分隻是普通核心代碼。

裝置檔案、主裝置号、從裝置号

linux系統将一切抽象為檔案,将硬體裝置也抽象為檔案,放在/dev/目錄下,

主裝置号用于辨別哪一個驅動程式使用它,

從裝置号用于辨別一個驅動程式作用于自己。

例子:2440有三個序列槽,每隔序列槽的主裝置号相同,從裝置号用于區分具體哪個序列槽。

linux驅動開發基礎

linux下系統調用表分析

系統調用表位置​

​linux-4.13.10/arch/x86/entry/syscall_64.c​

#define __SYSCALL_64_QUAL_(sym)
#define __SYSCALL_64(nr, sym, qual) [nr] =##qual(sym)

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
  /*
   * Smells like a compiler bug -- it doesn't work
   * when the & below is removed.
   */
  [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};      

源碼的arch/x86/include/asm目錄下卻找不到 syscalls_64.h 的,

但在編譯kernel後的/usr/src/linux-4.13.10/arch/x86/include/generated/asm裡面有這個檔案

__SYSCALL_64(0, sys_read, )
__SYSCALL_64(1, sys_write, )
__SYSCALL_64(2, sys_open, )
__SYSCALL_64(3, sys_close, )
__SYSCALL_64(4, sys_newstat, )      

根據宏定義展開

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
  [0] = sys_read,
  [1] = sys_write,
  [2] = sys_open,
  [3] = sys_close,
  [4] = sys_newstat,
  ...
}      

說明:syscalls_64.h檔案是編譯過程中産生的,syscall_64.h檔案同目錄下的Makefile中使用其生成了syscalls_64.h檔案。

kprobe、jprobe、kretprobe輕量級核心調試機制

利用kprobe技術,使用者可以自定義自己的回調函數,可以再幾乎所有的函數中動态插入探測點。

當核心執行流程執行到指定的探測函數時,會調用該回調函數,使用者即可收集所需的資訊了,同時核心最後還會回到原本的正常執行流程。如果使用者已經收集足夠的資訊,不再需要繼續探測,則同樣可以動态的移除探測點。

kprobes技術包括的3種探測手段分别是kprobe、jprobe和kretprobe。

首先kprobe是最基本的探測方式,是實作後兩種的基礎,它可以在任意的位置放置探測點(就連函數内部的某條指令處也可以),它提供了探測點的調用前、調用後和記憶體通路出錯3種回調方式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函數将在被探測指令被執行前回調,post_handler會在被探測指令執行完畢後回調(注意不是被探測函數),fault_handler會在記憶體通路出錯時被調用;

jprobe基于kprobe實作,它用于擷取被探測函數的入參值;

kretprobe同樣基于kprobe實作,用于擷取被探測函數的傳回值。

參考

​https://lwn.net/Articles/132196/​

解開宏定義SYSCALL_DEFINE3

read函數聲明

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)      

SYSCALL_DEFINE3宏

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3,##name, __VA_ARGS__)      

展開

SYSCALL_DEFINEx(3, _read, unsigned int, fd, char __user *, buf, size_t, count)      

SYSCALL_DEFINEx宏定義

#define SYSCALL_DEFINEx(x, sname, ...)\
  SYSCALL_METADATA(sname, x, __VA_ARGS__)\
  __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)      

展開

SYSCALL_METADATA(_read, 3, unsigned int, fd, char __user *, buf, size_t, count)      \
  __SYSCALL_DEFINEx(3, _read, unsigned int, fd, char __user *, buf, size_t, count)      

__SYSCALL_DEFINEx宏定義

#define __SYSCALL_DEFINEx(x, name, ...)\
  asmlinkage long##name(__MAP(x,__SC_DECL,__VA_ARGS__))\
    __attribute__((alias(__stringify(SyS##name))));\
  static inline long##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
  asmlinkage long##name(__MAP(x,__SC_LONG,__VA_ARGS__));\
  asmlinkage long##name(__MAP(x,__SC_LONG,__VA_ARGS__))\
  {\
    long ret =##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
    __MAP(x,__SC_TEST,__VA_ARGS__);\
    __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));\
    return ret;\
  }\
  static inline long##name(__MAP(x,__SC_DECL,__VA_ARGS__))      
asmlinkage long sys_read(__MAP(3,__SC_DECL,unsigned int, fd, char __user *, buf, size_t, count))  \
    __attribute__((alias(__stringify(SyS_read))));    \
  static inline long SYSC_read(__MAP(3,__SC_DECL,unsigned int, fd, char __user *, buf, size_t, count));  \
  asmlinkage long SyS_read(__MAP(3,__SC_LONG,unsigned int, fd, char __user *, buf, size_t, count));  \
  asmlinkage long SyS_read(__MAP(3,__SC_LONG,unsigned int, fd, char __user *, buf, size_t, count))  \
  {                \
    long ret = SYSC_read(__MAP(3,__SC_CAST,unsigned int, fd, char __user *, buf, size_t, count));  \
    __MAP(3,__SC_TEST,unsigned int, fd, char __user *, buf, size_t, count);        \
    __PROTECT(3, ret,__MAP(x,__SC_ARGS,unsigned int, fd, char __user *, buf, size_t, count));  \
    return ret;            \
  }                \
  static inline long SYSC_read(__MAP(3,__SC_DECL,unsigned int, fd, char __user *, buf, size_t, count))      

繼續閱讀