ホットパッチ原理 - Linuxカーネルの機能の実装を交換する方法

昨夜はアップ誓いました。その後、クラフトは、バイナリフックを書き、今日のユーザーは技術的な詳細を相談し、最終的に抵抗することができませんでしたがあるではないでしょう...

宣誓に違反しないためにも、何気なくエッジ、今日はC言語で書かれた、今日はバイナリフックを書いていない、バイナリだけのディップを言います!

件名を見て、Linuxカーネルの機能の代替の実施、何?そのkpatchのものではありません!それは我々が呼んでホットパッチ私たちは、誰も組み立て、それを書かないためにカーネルのホットパッチを行う、誰もが、それは、我々が直接、その後、カーネル関数のCコードを修正するパッチファイルを形成し、その後...そして一般的にされているバイナリロジックスクリプトを綴るないように...文書は、それのkpatchをお読みください。

本論文では、私はより多くの任意のソースkpatchテクニカル分析上よりも、むしろHOWTOのkpatchを使用する方法よりも、熱パッチの原理を説明します。

3.10カーネルの実用的な例とバグ修正ホットパッチは、私たちの話を始めます。

この例では、set_next_buddyの実現を変更しました:

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
...
@@ -4537,8 +4540,11 @@ static void set_next_buddy(struct sched_entity *se)
    if (entity_is_task(se) && unlikely(task_of(se)->policy == SCHED_IDLE))
        return;

-   for_each_sched_entity(se)
+   for_each_sched_entity(se) {
+       if (!se->on_rq)
+           return;
        cfs_rq_of(se)->next = se;
+   }
 }

これは既知のバグを修正するために、我々はコードset_next_buddy機能の数行を追加する必要があり、そうです、簡単であることは明らかです。

これは、彼らが新たな機能アップを実行できるようにするために、新しいset_next_buddy機能を形成し、そして今、私たちは三つの問題に直面している、後に数行のコードを追加します。

  • どのように我々は、バイナリにコンパイルし、この新しい関数set_next_buddyを使用できますか?
  • どのように我々はこの新しいset_next_buddyバイナリコードが実行中のカーネルに注入機能していますか?
  • どのように我々は新しい機能set_next_buddy set_next_buddyバイナリと古いものを置き換えるのですか?

我々は問題を参照してください。

まず、最初の質問は解決するのは非常に簡単です。

我々は、コンパイル時の依存関係を解決するために、あなたが実行中のカーネルのソースツリーをコンパイルすることができますにだけ形成した後に変更するパッチファイルをCファイルカーネル/ SCHED / fair.cを修正し、objdumpのことで同様メカニズムは、我々はOBJファイルset_next_buddyコンパイルされたバイナリ形式の引き抜き、その後、KOは難しいことではありません作ることができます。これは、コアモジュールを形成し、同様のkpatch-y8u59dkv.ko

次に、2番目の質問を見て、KOファイルには、どのようにそれのコアに注入set_next_buddyバイナリでの最初の質問を形成するには?

それはkpatchモジュールローディング機構はそれをやっている、難しいことではありません。カーネルへのホットパッチが2つのset_next_buddy機能が存在します。

crash> dis set_next_buddy
dis: set_next_buddy: duplicate text symbols found:
// 老的set_next_buddy
ffffffff810b9450 (t) set_next_buddy /usr/src/debug/kernel-3.10.0/linux-3.10.0.x86_64/kernel/sched/fair.c: 4536
// 新的set_next_buddy
ffffffffa0382410 (t) set_next_buddy [kpatch_y8u59dkv]

3番目の質問、少し問題によって。どのように新しいset_next_buddyバイナリは、それset_next_buddy古いバイナリを置き換えるのでしょうか?

カーネル関数のレイアウトは非常にコンパクトで、連続しているので、明らかに、カバーは、使用することができない、カーネル空間の各機能がセットに形成されたときに、古い機能よりもはるかに大きい新機能は、それが他のクロスボーダーを上書きする場合機能。

私の以前の記事を使用すると説明したバイナリフック技術は、次の資料方法として、実現可能である:
https://blog.csdn.net/dog250/article/details/105206753
バイナリdiffを、場所を変更するには、次にコンパクト突くの必要性これは間違いなくクーデターです!しかし、この方法では、エレガントな賢いが、無用の完全ではないが、その最大の問題は、逆マネージャです。

最も通常の方法は、使用フック次にftrace、すなわちにある「JMP /新しい関数を呼び出す」、およびスタブ関数の古いスタックフレームスキップ機能を指示するように変更され、古いバイト次にftraceスタブ5の初めに修正機能。このように完全に古い機能のバイパス。

私たちは、上記のバイナリの2 set_next_buddyを見て:

// 老的set_next_buddy:
crash> dis ffffffff810b9450 4
// 注意,老函数的ftrace stub已经被替换
0xffffffff810b9450 <set_next_buddy>:    callq  0xffffffff81646df0 <ftrace_regs_caller>
// 后面这些如何被绕过呢?ftrace_regs_caller返回后如何被skip掉呢?这需要平衡堆栈的技巧!
// 后面通过实例来讲如何平衡堆栈,绕过老的函数。
0xffffffff810b9455 <set_next_buddy+5>:  push   %rbp
0xffffffff810b9456 <set_next_buddy+6>:  cmpq   $0x0,0x150(%rdi)
0xffffffff810b945e <set_next_buddy+14>: mov    %rsp,%rbp
// 新的set_next_buddy:
crash> dis ffffffffa0382410 4
// 新函数则是ftrace_regs_caller最终要调用的函数
0xffffffffa0382410 <set_next_buddy>:    nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffffa0382415 <set_next_buddy+5>:  push   %rbp
0xffffffffa0382416 <set_next_buddy+6>:  cmpq   $0x0,0x150(%rdi)
0xffffffffa038241e <set_next_buddy+14>: mov    %rsp,%rbp

これは、熱パッチの原理です。

ここにこの記事では、これが、私はこれを説明するための実用的な例を使用し、当惑と後悔の終わりになり、紙の話です。この例は非常に簡単で、わずか数手配し、最大実行し、効果を見ることができます。

私は、私たちの労働者は、物事の本質を理解することは非常に有益である、私はコメントftrace_regs_callerのソースコードを通勤行くことはありませんので、私は同様のニーズを達成するために、私自身の方法を使用し、ソースコードの解析を比較するために嫌い、とはるかに簡単。

私の例では、両方の私の例のパッチをカーネル関数にパッチを適用する行くことはありません、次のようにモジュールのコードは、私は関数の中で書いた簡単なカーネルモジュールです。

#include <linux/module.h>
#include <linux/proc_fs.h>

// 下面的sample_read就是我将要patch的函数
static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	int n = 0;
	char kb[16];

	if (*ppos != 0) {
		return 0;
	}

	n = sprintf(kb, "%d\n", 1234);
	memcpy(ubuf, kb, n);
	*ppos += n;
	return n;
}

static struct file_operations sample_ops = {
	.owner = THIS_MODULE,
	.read = sample_read,
};

static struct proc_dir_entry *ent;
static int __init sample_init(void)
{
	ent = proc_create("test", 0660, NULL, &sample_ops);
	if (!ent)
		return -1;

	return 0;
}

static void __exit sample_exit(void)
{
	proc_remove(ent);
}

module_init(sample_init);
module_exit(sample_exit);
MODULE_LICENSE("GPL");

私たちは、それをロードし、その後に/ proc /テストについて読ん行きます:

[root@localhost test]# insmod sample.ko
[root@localhost test]# cat /proc/test
1234

OK、そうするすべてのもの。この時点で、我々はsample_read 5バイト目の前で見て:

crash> dis sample_read 1
0xffffffffa038c000 <sample_read>:       nopl   0x0(%rax,%rax,1) [FTRACE NOP]

すでにロードsample.ko前提に来て、我々はそれをパッチ。私の目標は、それが4321の代わりに1234を返すことsample_read機能を修正することです。

以下は、完全なコードポイントがコメントしている次のとおりです。

// hijack.c
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/cpu.h>

char *stub;
char *addr = NULL;

// 可以用JMP模式,也可以用CALL模式
//#define JMP	1

// 和sample模块里同名的sample_read函数
static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	int n = 0;
	char kb[16];

	if (*ppos != 0) {
		return 0;
	}
	// 这里我们把1234的输出给fix成4321的输出
	n = sprintf(kb, "%d\n", 4321);
	memcpy(ubuf, kb, n);
	*ppos += n;
	return n;
}

// hijack_stub的作用就类似于ftrace kpatch里的ftrace_regs_caller
static ssize_t hijack_stub(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	// 用nop占位,加上C编译器自动生成的函数header代码,这么大的函数来容纳stub应该够了。
	asm ("nop; nop; nop; nop; nop; nop; nop; nop;");
	return 0;
}

#define FTRACE_SIZE   	5
#define POKE_OFFSET		0
#define POKE_LENGTH		5
#define SKIP_LENGTH		8

static unsigned long *(*_mod_find_symname)(struct module *mod, const char *name);
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;
unsigned char saved_inst[POKE_LENGTH];
struct module *mod;

static int __init hotfix_init(void)
{
	unsigned char jmp_call[POKE_LENGTH];
	unsigned char e8_skip_stack[SKIP_LENGTH];
	s32 offset, i = 5;

	mod = find_module("sample");
	if (!mod) {
		printk("没加载sample模块,你要patch个啥?\n");
		return -1;
	}
	_mod_find_symname = (void *)kallsyms_lookup_name("mod_find_symname");
	if (!_mod_find_symname) {
		printk("还没开始,就已经结束。");
		return -1;
	}
	addr = (void *)_mod_find_symname(mod, "sample_read");
	if (!addr) {
		printk("一切还没有准备好!请先加载sample模块。\n");
		return -1;
	}
	_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
	_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
	if (!_text_poke_smp || !_text_mutex) {
		printk("还没开始,就已经结束。");
		return -1;
	}

	stub = (void *)hijack_stub;

	offset = (s32)((long)sample_read - (long)stub - FTRACE_SIZE);

	// 下面的代码就是stub函数的最终填充,它类似于ftrace_regs_caller的作用!
	e8_skip_stack[0] = 0xe8;
	(*(s32 *)(&e8_skip_stack[1])) = offset;
#ifndef JMP	// 如果是call模式,则需要手工平衡堆栈,跳过原始函数的栈帧
	e8_skip_stack[i++] = 0x41; // pop %r11
	e8_skip_stack[i++] = 0x5b; // r11寄存器为临时使用寄存器,遵循调用者自行保护原则
#endif
	e8_skip_stack[i++] = 0xc3;
	_text_poke_smp(&stub[0], e8_skip_stack, SKIP_LENGTH);

	offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);

	memcpy(&saved_inst[0], addr, POKE_LENGTH);
#ifndef JMP
	jmp_call[0] = 0xe8;
#else
	jmp_call[0] = 0xe9;
#endif
	(*(s32 *)(&jmp_call[1])) = offset;
	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr[POKE_OFFSET], jmp_call, POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();

	return 0;
}

static void __exit hotfix_exit(void)
{
	mod = find_module("sample");
	if (!mod) {
		printk("一切已经结束!\n");
		return;
	}
	addr = (void *)_mod_find_symname(mod, "sample_read");
	if (!addr) {
		printk("一切已经结束!\n");
		return;
	}
	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr[POKE_OFFSET], &saved_inst[0], POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();
}

module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");

OK、今我々はそれをロードした後、再読み込みに/ proc /テストについて:

[root@localhost test]# insmod ./hijack.ko
[root@localhost test]# cat /proc/test
4321

すでに正常にパッチ見ることができます。最終的に何が起こったのか?我々逆アセンブルを見て:

crash> dis sample_read
dis: sample_read: duplicate text symbols found:
ffffffffa039d000 (t) sample_read [sample]
ffffffffa03a2020 (t) sample_read [hijack]
crash>

ああ、同じ名前の2つのsample_read機能シンボル、機能が古いサンプルモジュールがあった、と機能モジュールのハイジャック新しい修正プログラムがあります。私たちは、それぞれを見て:

// 先看老的sample_read,它的ftrace stub已经被改成了call hijack_stub
crash> dis ffffffffa039d000 1
0xffffffffa039d000 <sample_read>:       callq  0xffffffffa03a2000 <hijack_stub>
// 再看新的sample_read,它就是最终被执行的函数
crash> dis ffffffffa03a2020 1
0xffffffffa03a2020 <sample_read>:       nopl   0x0(%rax,%rax,1) [FTRACE NOP]
crash>

新しいsample_readが終了したら、hijack_stubを返した後、もしポップ%のR11がそれを完了するので、必要に直接RET缶後、古いスタックフレームsample_read機能をオフにスキップするCALLモード、JMPモードであれば、 JMP命令はプッシュしないため、直接RETは、スタックフレームをスキップする必要はありません。

まあ、これは私が話を言いたいです。端的に言えば、私たちはここに記載さクラフトまだ生きている、私はちょうど原則の比較的複雑な熱パッチを達成するために表示するために、誰もが理解できることが最も簡単な方法を使用します。私は労働者が基本的な原理の深い知識を持っている必要があると思います。

管理者はまた、唐辛子を食べるのが大好きではなく、非常に、管理者が水を振りかけることができないことは明らかです。


温州は濡れた靴、雨水太っていません!

发布了1583 篇原创文章 · 获赞 5118 · 访问量 1114万+

おすすめ

転載: blog.csdn.net/dog250/article/details/105254739