我是靠谱客的博主 傻傻裙子,最近开发中收集的这篇文章主要介绍Linux 内核中的 static_key 机制问题来源:恶意程序检测DO_ONCE函数解析STATIC KEYSGOTOJUMP TABLE__do_once_doneCONFIG 问题解决:,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

目录

 

问题来源:恶意程序检测

DO_ONCE函数解析

STATIC KEYS

GOTO

JUMP TABLE

__do_once_done

CONFIG

 问题解决:


问题来源:恶意程序检测

最近,主要由于在研讨一些关于LINUX被恶意程序ROOT后,可能会被修改代码段中的数据。为了防止代码段被修改,采用几种特殊的机制来保护代码段的数据不被篡改,当有恶意程序试图修改代码段或只读数据段中的数据,特殊保护机制会自动忽略这种恶意操作,并追溯该操作的PC地址,栈指针,进程号等,从而找到恶意程序的源头,从而处理恶意程序。

问题就是来源这种特殊的保护机制启动后,当安装一些官方验证后的APK后,我们发现保护机制居然真的报警了,这意味着有程序在修改代码段。起初我们怀疑该APK自带ROOT工具,但是后来通过特殊机制进行回追出错的地址,回溯栈指针,发现是由于LINUX系统引入了Static_Keys机制,该机制中的部分函数的实现利用了代码段修改来实现的,而不是每次都是通过变量判断,从而直接改变代码段。而修改代码段的工作就交给了一个函数就是DO_ONCE中的__do_once_done中,那么我们就先来分析一下代码。

DO_ONCE函数解析

DO_ONCE机制想法很简单,有些函数只应该调用一次,那么这些函数调用一次后,如果下次再调用就应该直接返回。以前我们要实现这种功能总是需要判断一个变量,而使用DO_ONCE机制,自动帮你完成判断,保证函数只会在第一次调用的时候被执行,以后都直接返回。很自然,DO_ONCE机制就采用了Static_Keys来实现。使用static_key外 ,另一个关键在__do_once_done中。

#define DO_ONCE(func, ...)						     
	({								     
		bool ___ret = false;					     
		static bool ___done = false;				     
		static DEFINE_STATIC_KEY_TRUE(___once_key);		     
		if (static_branch_unlikely(&___once_key)) {		     
			unsigned long ___flags;				     
			___ret = __do_once_start(&___done, &___flags);	     
			if (unlikely(___ret)) {				     
				func(__VA_ARGS__);			     
				__do_once_done(&___done, &___once_key,	     
					       &___flags);		     
			}						     
		}							     
		___ret;							     
	})

如果有一个func只能被调用一次,例如初始化函数的话,则可以用这个宏,如下例所示foo这个函数即使被调用两次,也只会运行一次:

/* Call a function exactly once. The idea of DO_ONCE() is to perform
 * a function call such as initialization of random seeds, etc, only
 * once, where DO_ONCE() can live in the fast-path. After @func has
 * been called with the passed arguments, the static key will patch
 * out the condition into a nop. DO_ONCE() guarantees type safety of
 * arguments!
 *
 * Not that the following is not equivalent ...
 *
 *   DO_ONCE(func, arg);
 *   DO_ONCE(func, arg);
 *
 * ... to this version:
 *
 *   void foo(void)
 *   {
 *     DO_ONCE(func, arg);
 *   }
 *
 *   foo();
 *   foo();
 *
 * In case the one-time invocation could be triggered from multiple
 * places, then a common helper function must be defined, so that only
 * a single static key will be placed there!
 */

STATIC KEYS

简单的说,如果你代码对性能很敏感,而且大多数情况下分支路径是确定的,可以考虑使用Static Keys。Static Keys可以代替使用普通变量进行分支判断,目的是用来优化频繁使用if-else判断的问题,这里涉及到指令分支预取的一下问题。简单地说,现代cpu都有预测功能,变量的判断有可能会造成硬件预测失败,影响流水线性能。虽然有likely和unlikely,但还是会有小概率的预测失败。

下面使用例子说明一下:

//定义一个Static Keys,并且默认这个值是false。
DEFINE_STATIC_KEY_FALSE(key);…
//代码使用Static Keys代替普通变量进行判断,static_branch_unlikely是一个宏,展开后不会有真正的判断,而是直接执行false分支,即 do likely code。
if (static_branch_unlikely(&key))
    do unlikely code
else
    do likely code… 

这样的好处是,上述代码的性能和没有分支判断的性能差不多,具体可能只差一个nop指令的执行时间。 当然,如果某种情况发生了,需要改变分支的执行路径,可以调用下面的接口:
static_branch_enable(&key);
执行static_branch_enable(&key)后,底层通过gcc提供的goto功能,再结合c代码编写的动态更改内存功能,就可以让使用key的代码从执行false分支变为执行true分支。当然这个更改代价是比较昂贵的,不是所有情况都适用。可以改变分支的函数参照如下

#define static_branch_inc(x)		static_key_slow_inc(&(x)->key)
#define static_branch_dec(x)		static_key_slow_dec(&(x)->key)
#define static_branch_inc_cpuslocked(x)	static_key_slow_inc_cpuslocked(&(x)->key)
#define static_branch_dec_cpuslocked(x)	static_key_slow_dec_cpuslocked(&(x)->key)

/*
 * Normal usage; boolean enable/disable.
 */

#define static_branch_enable(x)			static_key_enable(&(x)->key)
#define static_branch_disable(x)		static_key_disable(&(x)->key)
#define static_branch_enable_cpuslocked(x)	static_key_enable_cpuslocked(&(x)->key)
#define static_branch_disable_cpuslocked(x)	static_key_disable_cpuslocked(&(x)->key)

GOTO

gcc4.5提供了一个特性用于嵌入式汇编,那就是asm goto,其实这个特性没有什么神秘之处,就是在嵌入式汇编中go to到c代码的label,其最简单的用法如下(来自gcc的文档):
int frob(int x)
{
    int y;
    asm goto ("frob %%r5, %1; jc %l[error]; mov (%2), %%r5"
                 : : "r"(x), "r"(&y) : "r5", "memory" : error);
    return y;
error:
    return -1;
}

按照原理来说"asm goto"其实就是在outputs,inputs,registers-modified之外提供了嵌入式汇编的第四个“:”,后面可以跟一系列的c语言的label,然后你可以在嵌入式汇编中goto到这些label中一个。然而使用"asm goto"可以巧妙地将"运行时修改载入内存的二进制代码”规范化,就是说你只需要调用一个统一的接口宏,编译器就将你想实现的东西给实现了,要不然代码写起来会很麻烦,这点上,编译器不嫌麻烦。具体为什么要动态修改二级制代码的原因还是与前面介绍的指令分支预取有关,为了极大的优化系统性能。

JUMP TABLE

jump_lable屏蔽不同体系更改机器代码的不同,向上提供一个统一接口。不同体系会提供给jump_lable一个体系相关的实现。
jump_lable的实现原理很简单,就是通过替换内存中机器代码的"nop"空指令为"b"指令,或者替换机器代码的“b”指令为“nop”空指令,实现分支的切换.

static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
{
	asm goto("1: nopnt"
		 ".pushsection __jump_table,  "aw"nt"
		 ".align 3nt"
		 ".quad 1b, %l[l_yes], %c0nt"
		 ".popsectionnt"
		 :  :  "i"(&((char *)key)[branch]) :  : l_yes);

	return false;
l_yes:
	return true;
}

static __always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)
{
	asm goto("1: b %l[l_yes]nt"
		 ".pushsection __jump_table,  "aw"nt"
		 ".align 3nt"
		 ".quad 1b, %l[l_yes], %c0nt"
		 ".popsectionnt"
		 :  :  "i"(&((char *)key)[branch]) :  : l_yes);

	return false;
l_yes:
	return true;
}

__do_once_done

void __do_once_done(bool *done, struct static_key_true *once_key,
		    unsigned long *flags)
	__releases(once_lock)
{
	*done = true;
	spin_unlock_irqrestore(&once_lock, *flags);
	once_disable_jump(once_key);
}
EXPORT_SYMBOL(__do_once_done

这个函数实在执行完第一次—do_once_start后需要修改代码段将jump修改成nop,最后还是调用了static_branch_disable(work->key); 具体修改代码段的代码追溯后到arch_jump_label_transform:

void arch_jump_label_transform(struct jump_entry *entry,
			       enum jump_label_type type)
{
	void *addr = (void *)entry->code;
	u32 insn;

	if (type == JUMP_LABEL_JMP) {
		insn = aarch64_insn_gen_branch_imm(entry->code,
						   entry->target,
						   AARCH64_INSN_BRANCH_NOLINK);
	} else {
		insn = aarch64_insn_gen_nop();
	}

	aarch64_insn_patch_text(&addr, &insn, 1);
}

 再次调用aarch64_insn_patch_text_nosync函数,

int __kprobes aarch64_insn_patch_text_nosync(void *addr, u32 insn)
{
	u32 *tp = addr;
	int ret;

	/* A64 instructions must be word aligned */
	if ((uintptr_t)tp & 0x3)
		return -EINVAL;

	ret = aarch64_insn_write(tp, insn);
	if (ret == 0)
		flush_icache_range((uintptr_t)tp,
				   (uintptr_t)tp + AARCH64_INSN_SIZE);

	return ret;
}

最后调用到__aarch64_insn_write,进行代码段的重映射,然后将jump tabel的头地址“b ”替换成“nop”,取消映射,返回。

static int __kprobes __aarch64_insn_write(void *addr, __le32 insn)
{
	void *waddr = addr;
	unsigned long flags = 0;
	int ret;

	raw_spin_lock_irqsave(&patch_lock, flags);
	waddr = patch_map(addr, FIX_TEXT_POKE0);

	ret = probe_kernel_write(waddr, &insn, AARCH64_INSN_SIZE);

	patch_unmap(FIX_TEXT_POKE0);
	raw_spin_unlock_irqrestore(&patch_lock, flags);

	return ret;
}

作为安全工程师,linux提供这种可以修改的代码段的函数,如果是用于调试还可以理解,但是如果公布的版本还是提供,是不是给一些专家提供了便捷的途径,还是那句话,"安全是一个平衡,性能和安全从来就是相对的,具体看应用的场景吧“。

CONFIG

如果启动功能,需要在开启线面的两个开关,目前Linux在x86上是强制开启的,ARM上还是根据需求来开启。

#if defined(CC_HAVE_ASM_GOTO) && defined(CONFIG_JUMP_LABEL)
# define HAVE_JUMP_LABEL
#endif

 问题解决:

最后,我先关掉了这个开关,特殊代码段检测程序已经不会再检测到有人修改的异常了。稍后,我会对这种修改做特殊处理,但是需要特殊识别,我的程序又要复杂了。

最后

以上就是傻傻裙子为你收集整理的Linux 内核中的 static_key 机制问题来源:恶意程序检测DO_ONCE函数解析STATIC KEYSGOTOJUMP TABLE__do_once_doneCONFIG 问题解决:的全部内容,希望文章能够帮你解决Linux 内核中的 static_key 机制问题来源:恶意程序检测DO_ONCE函数解析STATIC KEYSGOTOJUMP TABLE__do_once_doneCONFIG 问题解决:所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(66)

评论列表共有 0 条评论

立即
投稿
返回
顶部