0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何用替換函數(shù)的Trick做正常的事情

Linux閱碼場(chǎng) ? 來(lái)源:未知 ? 作者:胡薇 ? 2018-11-23 11:00 ? 次閱讀

浙江溫州皮鞋濕,下雨進(jìn)水不會(huì)胖。周六的雨夜,期待明天的雨會(huì)更大更冷。

已經(jīng)多久沒有編程了?很久了吧…其實(shí)我本來(lái)就不怎么會(huì)寫代碼,時(shí)不時(shí)的也就是為了驗(yàn)證一個(gè)系統(tǒng)特性,寫點(diǎn)玩具而已,工程化的代碼,對(duì)于我而言,實(shí)在是吃力。

最近遇到一些問題,需要特定的解法,也就有機(jī)會(huì)手寫點(diǎn)代碼了。其實(shí)這個(gè)話題記得上一次遇到是在8年前,時(shí)間過(guò)得好快。

替換一個(gè)已經(jīng)在內(nèi)存中的函數(shù),使得執(zhí)行流流入我們自己的邏輯,然后再調(diào)用原始的函數(shù),這是一個(gè)很古老的話題了。比如有個(gè)函數(shù)叫做funcion,而你希望統(tǒng)計(jì)一下調(diào)用function的次數(shù),最直接的方法就是 如果有誰(shuí)調(diào)用function的時(shí)候,調(diào)到下面這個(gè)就好了 :

void new_function()

{

count++;

return function();

}

網(wǎng)上很多文章給出了實(shí)現(xiàn)這個(gè)思路的Trick,而且一直以來(lái)計(jì)算機(jī)病毒也都采用了這種偷梁換柱的伎倆來(lái)實(shí)現(xiàn)自己的目的。然而,當(dāng)你親自去測(cè)試時(shí),發(fā)現(xiàn)事情并不那么簡(jiǎn)單。

網(wǎng)上給出的許多方法均不再適用了,原因是在早期,這樣做的人比較少,處理器操作系統(tǒng)大可不必理會(huì)一些不符合常規(guī)的做法,但是隨著這類Trick開始做壞事影響到正常的業(yè)務(wù)邏輯時(shí),處理器廠商以及操作系統(tǒng)廠商或者社區(qū)便不得不在底層增加一些限制性機(jī)制,以防止這類Trick繼續(xù)起作用。

常見的措施有兩點(diǎn):

可執(zhí)行代碼段不可寫

這個(gè)措施便封堵住了你想通過(guò)簡(jiǎn)單memcpy的方式替換函數(shù)指令的方案。

內(nèi)存buffer不可執(zhí)行

這個(gè)措施便封堵住了你想把執(zhí)行流jmp到你的一個(gè)保存指令的buffer的方案。

stack不可執(zhí)行

別看這些措施都比較low,一看誰(shuí)都懂,它們卻避免了大量的緩沖區(qū)溢出帶來(lái)的危害。

那么如果我們想用替換函數(shù)的Trick做正常的事情,怎么辦?

我來(lái)簡(jiǎn)單談一下我的方法。首先我不會(huì)去HOOK用戶態(tài)的進(jìn)程的函數(shù),因?yàn)檫@樣意義不大,改一下重啟服務(wù)會(huì)好很多。所以說(shuō),本文特指HOOK內(nèi)核函數(shù)的做法。畢竟內(nèi)核重新編譯,重啟設(shè)備代價(jià)非常大。

我們知道,我們目前所使用的幾乎所有計(jì)算機(jī)都是馮諾伊曼式的統(tǒng)一存儲(chǔ)式計(jì)算機(jī),即指令和數(shù)據(jù)是存在一起的,這就意味著我們必然可以在操作系統(tǒng)層面隨意解釋內(nèi)存空間的含義。

我們?cè)谧稣?dāng)?shù)氖虑?,所以我假設(shè)我們已經(jīng)拿到了系統(tǒng)的root權(quán)限并且可以編譯和插入內(nèi)核模塊。那么接下來(lái)的事情似乎就是一個(gè)流程了。

是的,修改頁(yè)表項(xiàng)即可,即便無(wú)法簡(jiǎn)單地通過(guò)memcpy來(lái)替換函數(shù)指令,我們還是可以用以下的步驟來(lái)進(jìn)行指令替換:

重新將函數(shù)地址對(duì)應(yīng)的物理內(nèi)存映射成可寫;

用自己的jmp指令替換函數(shù)指令;

解除可寫映射。

非常幸運(yùn),內(nèi)核已經(jīng)有了現(xiàn)成的 text_poke/text_poke_smp 函數(shù)來(lái)完成上面的事情。

同樣的,針對(duì)一個(gè)堆上或者棧上分配的buffer不可執(zhí)行,我們依然有辦法。辦法如下:

編寫一個(gè)stub函數(shù),實(shí)現(xiàn)隨意,其代碼指令和buffer相當(dāng);

用上面重映射函數(shù)地址為可寫的方法用buffer重寫stub函數(shù);

將stub函數(shù)保存為要調(diào)用的函數(shù)指針。

是不是有點(diǎn)意思呢?下面是一個(gè)步驟示意圖:

下面是一個(gè)代碼,我稍后會(huì)針對(duì)這個(gè)代碼,說(shuō)幾個(gè)細(xì)節(jié)方面的東西:

#include

#include

#include

#include

#include

#define OPTSIZE5

// saved_op保存跳轉(zhuǎn)到原始函數(shù)的指令

char saved_op[OPTSIZE] = {0};

// jump_op保存跳轉(zhuǎn)到hook函數(shù)的指令

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函數(shù),最終將會(huì)被保存指令的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 conntrack\n");

return 0;

}

// 這是我們的hook函數(shù),當(dāng)內(nèi)核在調(diào)用ipv4_conntrack_in的時(shí)候,將會(huì)到達(dá)這個(gè)函數(shù)。

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 conntrack\n");

// 僅僅打印一行信息后,調(diào)用原始函數(shù)。

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;

// 這個(gè)poke函數(shù)完成的就是重映射,寫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;

}

// 第一個(gè)字節(jié)當(dāng)然是jump

jump_op[0] = 0xe9;

// 計(jì)算目標(biāo)hook函數(shù)到當(dāng)前位置的相對(duì)偏移

hook_offset = (s32)((long)hook_ipv4_conntrack_in - (long)ptr_ipv4_conntrack_in - OPTSIZE);

// 后面4個(gè)字節(jié)為一個(gè)相對(duì)偏移

(*(s32*)(&jump_op[1])) = hook_offset;

// 事實(shí)上,我們并沒有保存原始ipv4_conntrack_in函數(shù)的頭幾條指令,

// 而是直接jmp到了5條指令后的指令,對(duì)應(yīng)上圖,應(yīng)該是指令buffer里沒

// 有old inst,直接就是jmp y了,為什么呢?后面細(xì)說(shuō)。

saved_op[0] = 0xe9;

// 計(jì)算目標(biāo)原始函數(shù)將要執(zhí)行的位置到當(dāng)前位置的偏移

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");

測(cè)試是OK的。

在上面的代碼中,saved_op中為什么沒有old inst呢?直接就是一個(gè)jmp y,這豈不是將原始函數(shù)中的頭幾個(gè)字節(jié)的指令給遺漏了嗎?

其實(shí)說(shuō)到這里,還真有個(gè)不好玩的Trick,起初我真的就是老老實(shí)實(shí)保存了前5個(gè)自己的指令,然后當(dāng)需要調(diào)用原始ipv4_conntrack_in時(shí),就先執(zhí)行那5個(gè)保存的指令,也是OK的。隨后我objdump這個(gè)函數(shù)發(fā)現(xiàn)了下面的代碼:

0000000000000380 :

380: e8 00 00 00 00 callq 385

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

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個(gè)指令: e8 00 00 00 00 callq 385

可以看到,這個(gè)是可以忽略的。因?yàn)椴还茉趺凑f(shuō)都是緊接著執(zhí)行下面的指令。所以說(shuō),我就省去了inst的保存。

如果按照我的圖示中常規(guī)的方法的話,代碼稍微改一下即可:

char saved_op[OPTSIZE+OPTSIZE] = {0};

...

// 增加一個(gè)指令拷貝的操作

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;

但是以上的只是玩具。

有個(gè)非常現(xiàn)實(shí)的問題。在我保存原始函數(shù)的頭n條指令的時(shí)候,n到底是多少呢?在本例中,顯然n是5,符合如今Linux內(nèi)核函數(shù)第一條指令幾乎都是callq xxx的慣例。

然而,如果一個(gè)函數(shù)的第一條指令是下面的樣子:

op d1 d2 d3 d4 d5

即一個(gè)操作碼需要5個(gè)操作數(shù),我要是只保存5個(gè)字節(jié),最后在stub中的指令將會(huì)是下面的樣子:

op d1 d2 d3 d4 0xe9 off1 off2 off3 off4

這顯然是錯(cuò)誤的,op操作碼會(huì)將jmp指令0xe9解釋成操作數(shù)。

解藥呢?當(dāng)然有咯。

我們不能魯莽地備份固定長(zhǎng)度的指令,而是應(yīng)該這樣做:

curr = 0

if orig[0] 為單字節(jié)操作碼

saved_op[curr] = orig[curr];

curr++;

else if orig[0] 攜帶1個(gè)1字節(jié)操作數(shù)

memcpy(saved_op, orig, 2);

curr += 2;

else if orig[0] 攜帶2字節(jié)操作數(shù)

memcpy(saved_op, orig, 3);

curr += 3;

...

saved_op[curr] = 0xe9; // jmp

offset = ...

(*(s32*)(&saved_op[curr+1])) = offset;

這是正確的做法。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11123

    瀏覽量

    207914
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4237

    瀏覽量

    61969

原文標(biāo)題:Linux內(nèi)核如何替換內(nèi)核函數(shù)并調(diào)用原始函數(shù)

文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    何用labview啟動(dòng)

    何用labview啟動(dòng)
    發(fā)表于 03-13 16:07

    何用Primff()函數(shù)打印串行端口信息?

    ”);// Uart可以打印串口信息?,F(xiàn)在,我想這樣:PrtTF(“Helon Test\r\n”);//“Helon Test\R\n”由串行工具恢復(fù)。所以,我的問題是如何用Primff()函數(shù)打印串行端口
    發(fā)表于 09-26 08:35

    GUI函數(shù)里面做了哪些事情,對(duì)顯示界面有何作用

    請(qǐng)問 GUI_TOUCH_Exec()函數(shù)里面做了哪些事情,對(duì)顯示界面有何作用?該函數(shù)的資料很難找到,請(qǐng)大家?guī)兔ΑVx謝!
    發(fā)表于 06-03 04:35

    何用Matlab去實(shí)現(xiàn)FFT函數(shù)和IFFT函數(shù)

    Matlab的FFT函數(shù)和IFFT函數(shù)有什么用法嗎?如何用Matlab去實(shí)現(xiàn)FFT函數(shù)和IFFT函數(shù)呢?
    發(fā)表于 11-18 07:05

    何用printf()函數(shù)代替串口發(fā)送數(shù)據(jù)?

    何用printf()函數(shù)代替串口發(fā)送數(shù)據(jù)?
    發(fā)表于 12-01 08:01

    何用STM32通用定時(shí)器微秒延時(shí)函數(shù)?

    何用STM32通用定時(shí)器微秒延時(shí)函數(shù)
    發(fā)表于 12-01 06:37

    何用__write函數(shù)替換掉原先的fputc函數(shù)

    何用__write函數(shù)替換掉原先的fputc函數(shù)呢?
    發(fā)表于 12-01 06:55

    何用2SC2539替換2SC1971

    何用2SC2539替換2SC1971
    發(fā)表于 12-22 11:40 ?3122次閱讀

    替換數(shù)組子集函數(shù)

    Labview之替換數(shù)組子集函數(shù),很好的Labview資料,快來(lái)下載學(xué)習(xí)吧。
    發(fā)表于 04-19 10:43 ?0次下載

    何用AD電路板邊框

    何用AD電路板邊框,感興趣的小伙伴們可以看看。
    發(fā)表于 07-26 10:43 ?0次下載

    matlab升級(jí)2021a版本后有哪些函數(shù)需要替換?

    使用新的函數(shù)進(jìn)行替換!先到互聯(lián)網(wǎng)上搜索一下該函數(shù)的使用方法吧!因?yàn)闊o(wú)法在matlab里面運(yùn)行此函數(shù)了,所以老函數(shù)的用法只能靠搜索來(lái)給大家
    的頭像 發(fā)表于 06-10 16:44 ?9312次閱讀

    在C++中如何用函數(shù)實(shí)現(xiàn)多態(tài)

    01 — C++虛函數(shù)探索 C++是一門面向?qū)ο笳Z(yǔ)言,在C++里運(yùn)行時(shí)多態(tài)是由虛函數(shù)和純虛函數(shù)實(shí)現(xiàn)的,現(xiàn)在我們看下在C++中如何用函數(shù)實(shí)現(xiàn)
    的頭像 發(fā)表于 09-29 14:18 ?1618次閱讀

    C語(yǔ)言內(nèi)聯(lián)函數(shù)

    函數(shù)B很小,又被頻繁的調(diào)用,可能函數(shù)調(diào)用的切換時(shí)間比函數(shù)內(nèi)代碼的執(zhí)行時(shí)間還長(zhǎng),這樣明顯劃不來(lái),那么我們就可以將這個(gè)函數(shù)聲明為內(nèi)聯(lián)(加上 inline ),編譯器在編譯時(shí),會(huì)把內(nèi)聯(lián)
    的頭像 發(fā)表于 02-21 16:55 ?842次閱讀
    C語(yǔ)言內(nèi)聯(lián)<b class='flag-5'>函數(shù)</b>

    何用兩種不同的方法列寫雙容水槽傳遞函數(shù)

    何用兩種不同的方法列寫雙容水槽傳遞函數(shù)
    的頭像 發(fā)表于 03-10 16:20 ?3280次閱讀
    如<b class='flag-5'>何用</b>兩種不同的方法列寫雙容水槽傳遞<b class='flag-5'>函數(shù)</b>

    MySQL替換字符串函數(shù)REPLACE

    MySQL是目前非常流行的開源數(shù)據(jù)庫(kù)管理系統(tǒng)之一,它具有強(qiáng)大的功能和性能。其中之一的字符串函數(shù)REPLACE,可以用于替換字符串中的指定字符或字符串。在本文中,我們將詳細(xì)討論MySQL替換字符串
    的頭像 發(fā)表于 11-30 10:44 ?1252次閱讀