通過系統調用,內核斷點方法定位用戶進程被內核踩內存的問題


請看我的上一篇博客,https://www.cnblogs.com/xingmuxin/p/11287935.html 介紹了具體的踩內存的問題。下面我來介紹下如何通過一些手段和方法,定位內核踩內存的問題。

1、系統調用攔截

    系統調用攔截的目的其實就是把系統真正要執行的系統調用替換為我們自己寫的內核函數,這里有一篇博客,對此作了介紹,https://blog.csdn.net/zhangyifei216/article/details/49872861

系統調用攔截的兩個問題,一個是找到sys_call_table地址,一個是修改內存頁的屬性,讓其變為可寫。

1)找sys_call_table地址

    我們可以通過兩種方法來查找sys_call_table地址:

一、使用kallsyms_lookup_name

    使用kallsyms_lookup_name函數,來讀取對應的sys_call_table的地址,但是kallsyms_lookup_name這個函數能否可以被我們使用,能否在我們寫的內核模塊中導出。這要看內核代碼中是否有加入EXPORT_SYMBOL。

    EXPORT_SYMBOL標簽內定義的函數或者符號對全部內核代碼公開,不用修改內核代碼就可以在您的內核模塊中直接調用,即使用EXPORT_SYMBOL可以將一個函數以符號的方式導出給其他模塊使用。

使用方法
   第一、在模塊函數定義之后使用EXPORT_SYMBOL(函數名)
   第二、在掉用該函數的模塊中使用extern對之聲明
   第三、首先加載定義該函數的模塊,再加載調用該函數的模塊

    通過查看內核代碼,我們看到,我們自己的內核中,是有對kallsyms_lookup_name這個函數導出符號的。

EXPORT_SYMBOL_GPL(kallsyms_lookup_name);

    使用的是EXPORT_SYMBOL_GPL,_GPL版本的宏定義只能使符號對GPL許可的模塊可用,如果你的module的協議不是GPL, 那么EXPORT_SYMBOL_GPL導出的那些符號,你就不能用。

綜上,我們要查找到sys_call_table的地址,可以這樣來寫代碼。

MODULE_LICENSE("GPL"); /* 這個必須要加,否則不可以使用EXPORT_SYMBOL_GPL導出的符號,加載模塊時,會提示unknown symbol */
extern unsigned long kallsyms_lookup_name(const char *name); /* 將kallsyms_lookup_name符號導出 */
static
int a_init(void) { unsigned long p = 0UL; p = kallsyms_lookup_name("sys_call_table"); if (!p) { printk("can not find sys_call_table"); return -1; }return 0; }

二、讀取讀取/proc/kallsyms,再把值作為參數傳入內核模塊

/proc/kallsyms是一個特殊的文件,它並不是存儲在磁盤上的文件。這個文件只有被讀取的時候,才會由內核產生內容。因為這些內容是內核動態生成的,所以可以保證其中讀到的地址是正確的。可以在加載模塊時用腳本獲取符號的地址。命令:

#cat /proc/kallsyms | grep "\<sys_call_table\>" | awk '{print $1}'

2)、在修改系統調用地址時,需要修改內存內的屬性

為了可以對sys_call_table所在內存地址處,進行讀寫,需要重新設置該地址對應的頁表項的屬性。物理地址本來是沒有什么讀寫屬性的,內核只通過修改物理地址對應的頁表項的一些屬性位來設置該物理地址的讀寫屬性而已。下面是具體修改的過程。

static void set_addr_rw(unsigned long addr)
{
    unsigned int level;
    pte_t *pte;

    pte = lookup_address(addr, &level); /* 查找虛擬地址所在的頁表地址 */
    pte->pte |= _PAGE_RW; /* 設置頁表讀寫屬性 */
}

static void set_addr_ro(unsigned long addr)
{
    unsigned int level;
    pte_t *pte;

    pte = lookup_address(addr, &level);
    pte->pte = pte->pte &~_PAGE_RW; /* 設置只讀屬性 */
}

下面我們來看下,如何設置系統調用,是系統調用到我們自己寫的內核函數。在模塊載入的時候,先保存原有的系統調用的地址,然后再修改該地址所在頁表的屬性,為可寫的,然后再賦值為新的我們自己的系統調用地址。

static int a_init(void)
{
    unsigned long p = 0UL;

    call_table_p = kallsyms_lookup_name("sys_call_table");
    if (!p) {
        printk("can not find sys_call_table");
        return -1;
    }

    /* afs_syscall 183 is not use */
    syscall_p =(unsigned long*)(p + 8*183); /* 在sys_call_table中有很多系統調用是沒有實現的,我們可以占用 */
    save_syscall_val = *syscall_p;
    set_syscall_ptr((unsigned long)my_syscall);

    return 0;
}
set_syscall_ptr這個函數的具體實現為:
 
         

static unsigned long *syscall_p = 0UL;
static unsigned long save_syscall_val = 0UL;

static void set_syscall_ptr(unsigned long val)
{
    unsigned long flags;
    if (save_syscall_val == 0UL || call_table_p == 0UL) {
        return; 
    }

    preempt_disable();     /* 關閉內核搶占*/
    local_irq_save(flags);  /*  對 local_irq_save的調用將把當前中斷狀態保存到flags中,然后禁用當前處理器上的中斷發送 */

    set_addr_rw((unsigned long)call_table_p);  /* 設置sys_cal_table地址所在的內存頁面為可讀寫*/
    mb();
    *syscall_p = val; 
    mb();
    set_addr_ro((unsigned long)syscall_p);   /* 設置sys_cal_table地址所在的內存頁面為寫保護*/
local_irq_restore(flags); 
preempt_enable();
printk(
"new syscall 183 is %lx\n", val);
}

2、hardware breakpoint

    內核由於共享內存地址空間,如果沒有合適的工具,很多踩內存的問題即使復現,也無法快速定位;在新的內核版本中引入了一個新工具hardware breakpoint,其能夠監視對指定的地址的特定類型(讀/寫)的數據訪問,有利於該類問題的定位;https://blog.csdn.net/phenix_lord/article/details/41415559
下面,我們再來看一下我們替換的這個my_syscall的具體實現。

static unsigned long g_pid;
static unsigned long g_addr;

struct perf_event * __percpu *sample_hbp;

static void sample_hbp_handler(struct perf_event *bp, struct perf_sample_data *data, struct pt_regs *regs)
{
    printk("%lu, %016lx value is changed\n", g_pid, g_addr);
    dump_stack();

}

asmlinkage long my_syscall(unsigned long addr)  /* 通過堆棧而不是通過寄存器傳遞參數 */
{
    int ret = 0;
    struct perf_event_attr attr;

    g_pid = (unsigned long)task_tgid_vnr(current);
    g_addr = addr;

    hw_breakpoint_init(&attr);
    attr.bp_addr = addr;      /* 待監視的地址 */
    attr.bp_len = HW_BREAKPOINT_LEN_4;
    attr.bp_type = HW_BREAKPOINT_W; /* 待監視的訪問類型 */

    if (sample_hbp)
        unregister_wide_hw_breakpoint(sample_hbp);  /* 內核對斷點數量有限制,為4,所以要及時釋放 */
    sample_hbp = register_wide_hw_breakpoint(&attr, sample_hbp_handler, NULL);
    if (IS_ERR((void __force *)sample_hbp)) {
        ret = PTR_ERR((void __force *)sample_hbp);
        sample_hbp = NULL;
        printk("Breakpoint registration failed\n");
        return ret;
    }
    printk("%lu, %016lx write installed\n", g_pid, g_addr);
    return 0; 
}

最后,在模塊卸載時要做如下操作:

static void a_exit(void)
{
    set_syscall_ptr(save_syscall_val);

    if (sample_hbp)
        unregister_wide_hw_breakpoint(sample_hbp);
}

module_init(a_init);
module_exit(a_exit);

 

 

 
        


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM