Linux內核如何替換內核函數並調用原始函數

替換一個已經在內存中的函數,使得執行流流入咱們本身的邏輯,而後再調用原始的函數,這是一個很古老的話題了。好比有個函數叫作funcion,而你但願統計一下調用function的次數,最直接的方法就是 若是有誰調用function的時候,調到下面這個就行了 :linux

void new_function()
{
    count++;
    return function();
}

網上不少文章給出了實現這個思路的Trick,並且一直以來計算機病毒也都採用了這種偷樑換柱的伎倆來實現本身的目的。然而,當你親自去測試時,發現事情並不那麼簡單。golang

網上給出的許多方法均再也不適用了,緣由是在早期,這樣作的人比較少,處理器和操做系統大可沒必要理會一些不符合常規的作法,可是隨着這類Trick開始作壞事影響到正常的業務邏輯時,處理器廠商以及操做系統廠商或者社區便不得不在底層增長一些限制性機制,以防止這類Trick繼續起做用。服務器

常見的措施有兩點:架構

  • 可執行代碼段不可寫

這個措施便封堵住了你想經過簡單memcpy的方式替換函數指令的方案。tcp

  • 內存buffer不可執行

這個措施便封堵住了你想把執行流jmp到你的一個保存指令的buffer的方案。函數

  • stack不可執行

別看這些措施都比較low,一看誰都懂,它們卻避免了大量的緩衝區溢出帶來的危害。學習

那麼若是咱們想用替換函數的Trick作正常的事情,怎麼辦?測試

我來簡單談一下個人方法。首先我不會去HOOK用戶態的進程的函數,由於這樣意義不大,改一下重啓服務會好不少。因此說,本文特指HOOK內核函數的作法。畢竟內核從新編譯,重啓設備代價很是大。spa

咱們知道,咱們目前所使用的幾乎全部計算機都是馮諾伊曼式的統一存儲式計算機,即指令和數據是存在一塊兒的,這就意味着咱們必然能夠在操做系統層面隨意解釋內存空間的含義。操作系統

咱們在作正當的事情,因此我假設咱們已經拿到了系統的root權限而且能夠編譯和插入內核模塊。那麼接下來的事情彷佛就是一個流程了。

是的,修改頁表項便可,即使沒法簡單地經過memcpy來替換函數指令,咱們仍是能夠用如下的步驟來進行指令替換:

  1. 從新將函數地址對應的物理內存映射成可寫;
  2. 用本身的jmp指令替換函數指令;
  3. 解除可寫映射。

很是幸運,內核已經有了現成的 text_poke/text_poke_smp 函數來完成上面的事情。

一樣的,針對一個堆上或者棧上分配的buffer不可執行,咱們依然有辦法。辦法以下:

  1. 編寫一個stub函數,實現隨意,其代碼指令和buffer至關;
  2. 用上面重映射函數地址爲可寫的方法用buffer重寫stub函數;
  3. 將stub函數保存爲要調用的函數指針。

是否是有點意思呢?下面是一個步驟示意圖:

Linux內核如何替換內核函數並調用原始函數

下面是一個代碼,我稍後會針對這個代碼,說幾個細節方面的東西:

#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/cpu.h>
#include <linux/module.h>
#include <net/tcp.h>
#define OPTSIZE    5
// saved_op保存跳轉到原始函數的指令
char saved_op[OPTSIZE] = {0};
// jump_op保存跳轉到hook函數的指令
char jump_op[OPTSIZE] = {0};
static unsigned int (*ptr_orig_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);
static unsigned int (*ptr_ipv4_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);
// stub函數,最終將會被保存指令的buffer覆蓋掉
static unsigned int stub_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
    printk("hook stub conntrackn");
    return 0;
}
// 這是咱們的hook函數,當內核在調用ipv4_conntrack_in的時候,將會到達這個函數。
static unsigned int hook_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
    printk("hook conntrackn");
    // 僅僅打印一行信息後,調用原始函數。
    return ptr_orig_conntrack_in(ops, skb, in, out, state);
}
static void *(*ptr_poke_smp)(void *addr, const void *opcode, size_t len);
static __init int hook_conn_init(void)
{
    s32 hook_offset, orig_offset;
    // 這個poke函數完成的就是重映射,寫text段的事
    ptr_poke_smp = kallsyms_lookup_name("text_poke_smp");
    if (!ptr_poke_smp) {
        printk("err");
        return -1;
    }
    // 嗯,咱們就是要hook住ipv4_conntrack_in,因此要先找到它!
    ptr_ipv4_conntrack_in = kallsyms_lookup_name("ipv4_conntrack_in");
    if (!ptr_ipv4_conntrack_in) {
        printk("err");
        return -1;
    }
    // 第一個字節固然是jump
    jump_op[0] = 0xe9;
    // 計算目標hook函數到當前位置的相對偏移
    hook_offset = (s32)((long)hook_ipv4_conntrack_in - (long)ptr_ipv4_conntrack_in - OPTSIZE);
    // 後面4個字節爲一個相對偏移
    (*(s32*)(&jump_op[1])) = hook_offset;
    // 事實上,咱們並無保存原始ipv4_conntrack_in函數的頭幾條指令,
    // 而是直接jmp到了5條指令後的指令,對應上圖,應該是指令buffer裏沒
    // 有old inst,直接就是jmp y了,爲何呢?後面細說。
    saved_op[0] = 0xe9;
    // 計算目標原始函數將要執行的位置到當前位置的偏移
    orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE));
    (*(s32*)(&saved_op[1])) = orig_offset;
    get_online_cpus();
    // 替換操做!
    ptr_poke_smp(stub_ipv4_conntrack_in, saved_op, OPTSIZE);
    ptr_orig_conntrack_in = stub_ipv4_conntrack_in;
    barrier();
    ptr_poke_smp(ptr_ipv4_conntrack_in, jump_op, OPTSIZE);
    put_online_cpus();
    return 0;
}
module_init(hook_conn_init);
static __exit void hook_conn_exit(void)
{
    get_online_cpus();
    ptr_poke_smp(ptr_ipv4_conntrack_in, saved_op, OPTSIZE);
    ptr_poke_smp(stub_ipv4_conntrack_in, stub_op, OPTSIZE);
    barrier();
    put_online_cpus();
}
module_exit(hook_conn_exit);
MODULE_DESCRIPTION("hook test");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.1");

測試是OK的。

須要C/C++ Linux服務器架構師學習資料加羣812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享

Linux內核如何替換內核函數並調用原始函數

在上面的代碼中,saved_op中爲何沒有old inst呢?直接就是一個jmp y,這豈不是將原始函數中的頭幾個字節的指令給遺漏了嗎?

其實說到這裏,還真有個很差玩的Trick,起初我真的就是老老實實保存了前5個本身的指令,而後當須要調用原始ipv4_conntrack_in時,就先執行那5個保存的指令,也是OK的。隨後我objdump這個函數發現了下面的代碼:

0000000000000380 <ipv4_conntrack_in>:
      380:   e8 00 00 00 00          callq  385 <ipv4_conntrack_in+0x5>
      385:   55                      push   %rbp
      386:   49 8b 40 18             mov    0x18(%r8),%rax
      38a:   48 89 f1                mov    %rsi,%rcx
      38d:   8b 57 2c                mov    0x2c(%rdi),%edx
      390:   be 02 00 00 00          mov    $0x2,%esi
      395:   48 89 e5                mov    %rsp,%rbp
      398:   48 8b b8 e8 03 00 00    mov    0x3e8(%rax),%rdi
      39f:   e8 00 00 00 00          callq  3a4 <ipv4_conntrack_in+0x24>
      3a4:   5d                      pop    %rbp
      3a5:   c3                      retq
      3a6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
      3ad:   00 00 00

注意前5個指令: e8 00 00 00 00 callq 385 <ipv4_conntrack_in+0x5>

能夠看到,這個是能夠忽略的。由於無論怎麼說都是緊接着執行下面的指令。因此說,我就省去了inst的保存。

若是按照個人圖示中常規的方法的話,代碼稍微改一下便可:

char saved_op[OPTSIZE+OPTSIZE] = {0};
...
    // 增長一個指令拷貝的操做
    memcpy(saved_op, (unsigned char *)ptr_ipv4_conntrack_in, OPTSIZE);
    saved_op[OPTSIZE] = 0xe9;
    orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE + OPTSIZE));
    (*(s32*)(&saved_op[OPTSIZE+1])) = orig_offset;
...

可是以上的只是玩具。

有個很是現實的問題。在我保存原始函數的頭n條指令的時候,n究竟是多少呢?在本例中,顯然n是5,符合現在Linux內核函數第一條指令幾乎都是callq xxx的慣例。

然而,若是一個函數的第一條指令是下面的樣子:

op d1 d2 d3 d4 d5

即一個操做碼須要5個操做數,我要是隻保存5個字節,最後在stub中的指令將會是下面的樣子:

op d1 d2 d3 d4 0xe9 off1 off2 off3 off4

這顯然是錯誤的,op操做碼會將jmp指令0xe9解釋成操做數。

解藥呢?固然有咯。

咱們不能魯莽地備份固定長度的指令,而是應該這樣作:

curr = 0
if orig[0] 爲單字節操做碼
    saved_op[curr] = orig[curr];
    curr++;
else if orig[0] 攜帶1個1字節操做數
    memcpy(saved_op, orig, 2);
    curr += 2;
else if orig[0] 攜帶2字節操做數
    memcpy(saved_op, orig, 3);
    curr += 3;
...
saved_op[curr] = 0xe9; // jmp
offset = ...
(*(s32*)(&saved_op[curr+1])) = offset;

這是正確的作法。

相關文章
相關標籤/搜索