代码保护技术:控制流混淆

文章大部分内容来源自《软件加密与解密》,本人新手小白只负责学习和整理。

代码混淆的目的是在不改变源程序的功能的同时让程序代码可读性大大降低,使其反编译成本超过通过反编译所带来的利益。根据Collberg等人将代码混淆分为布局混淆,数据混淆,控制流混淆和预防混淆,其中研究较为广泛的是控制流混淆,如控制流平展化算法和不透明谓词混淆等。


1. 控制流

控制流是程序在运行时指令(或陈述、子程序)运行或求值的顺序。无论是面向对象的编程语言还是面向过程的编程语言,都会在程序中使用到控制流程。 

1.1 控制流分析

控制流分析(Control flow analysis,CFA)是一种对程序控制流程的静态代码分析技术。不同的编程语言提供的控制流指令也不同,但大体上可以分为几种: 

  • 无条件分支指令,继续运行处于不同位置的一段指令
  • 有条件分支指令,特定条件成立则运行一段指令(如C语言中的switch指令和for指令)
  • 运行在不同位置的一段指令,完成后返回原指令处继续运行(如子程序)
  • 无条件终止指令

常用的控制流结构有条件判断,循环,异常处理等。 

1.2 控制流图  

控制流图(Control Flow Graph,CFG)代表了一个程序执行过程中会遍历到的所有的路径。它用图的形式表示一个过程中的所有基本块执行的可能的流向,也可以反映一个过程的实时执行过程。如下是《软件加密与解密》书中给出的模n取幂函数。   

int modexp(int y, int x[], int w, int n)
{
	int R, L;
	int k = 0;
	int s = 1;
	while(k < w)
	{
		if(x[k] == 1)
			R = s * y % n;
		else
			R = s;
		s = R * R % n;
		L = R;
		k++;
	}
	return L;
}

根据函数中使用到的各跳转指令,可以构建出如图1-1所示的控制流程图,通过控制流图可以清晰地看到程序的走向。

代码保护技术:控制流混淆_第1张图片 图1-1 模n取幂函数控制流图

 2. 控制流混淆

控制流混淆的目的是改变控制流或将程序原有控制流复杂化,使程序更难破译。通常程序中代码块都是按照逻辑顺序有序划分与组合,并且将相关的代码放在一起。在不改变程序功能的前提下,通过拆分重组程序代码等方式打破这种常规逻辑,使代码间的关系变得模糊,以此来保护程序的源码。

控制流混淆是代码混淆技术研究中最为广泛的一种,控制流混淆的方法非常多,常用的包括不透明表达式,压扁控制流,插入多余控制流等。

2.1 不透明谓词

在混淆时,如果一个表达式的值已知,但是破解者却很难通过表达式本身推断它的值,那么它就是一个不透明表达式。最常见的不透明表达式即不透明谓词。不透明谓词是永真或永假或时真时假的布尔表达式。

图2-1左侧为两个顺序执行的基本块,通过构造恒真或恒假的不透明谓词,使程序中多了一条伪分支,这样使控制流的结构更为复杂,也可以构造如图2-2所示的可为真也可为假的不透明谓词,在程序中添加等价基本块,使程序执行结果无论为真还是为假都能正确执行,这样也可以使控制流复杂化。

代码保护技术:控制流混淆_第2张图片 图2-1 恒真或恒假不透明谓词

 

代码保护技术:控制流混淆_第3张图片 图2-2 永真或永假不透明谓词

在如下程序段中,我们构造了永假的不透明谓词(x²+x)%2,无论x为何值,此表达式结果永为false,即程序永远不会执行到a * b这一基本块。 

int add(int a, int b, double x)
{
	if ((int)(pow(x, 2.0) + x) % 2 == 0)
		return a + b;
	else
		return a * b;
}

2.2 压扁控制流

OBFWHKD算法是常用的混淆工具,利用它可以将程序中原有的嵌套循环和条件转移语句平展开。压扁控制流的通常过程有两步:

(1)把控制流图中的各个基本块全部放到switch语句中;

(2)把switch语句封装到死循环中。

算法在每个基本块中添加next变量以在switch结构中维护正确的控制流结构。这样控制流仍然会正确的执行但是控制流图的结构已经被彻底改变了。各个基本块中已经失去了明确记载控制流流向的基本信息,在逆向分析的过程中也只能一步步记录哪些基本块被执行过。

对于1.2节中算法,我们可以分离出其中的所有基本块,在每一个基本块中添加next值用于指向下一个基本块,接着用switch-case结构封装这些基本块,得到如下代码:

int modexp(int y, int x[], int w, int n)
{
	int R, L, k, s;
	int next = 0;
	for(;;)
	{
		switch(next)
		{
			case 0 : k = 0; s = 1; next = 1; break;
			case 1 : if(k < w) next = 2; else next = 6; break;
			case 2 : if(x[k] == 1) next = 3; else next = 4; break;
			case 3 : R = s * y % n; next = 5; break;
			case 4 : R = s; next = 5; break;
			case 5 : s = R * R % n; L = R; k++; next = 1; break;
			case 6 : return L;
		}
	}
}

 根据以上程序再次构造出图2-3所示控制流图。

代码保护技术:控制流混淆_第4张图片 图2-3 被压扁后模n取幂函数控制流图

我们通过next的值对程序的正确流程进行维护,在不断更新next变量的值的过程中使程序正确执行。不过OBFWHKD压扁控制流算法的开销较高,需要更多优化。同时该算法的混淆强度也不算太高,可以选择构造不透明表达式去计算next值以加强混淆力度。

2.3 插入多余控制流

压扁控制流算法OBFWHKD是通过重新组织控制流,使静态分析工具无法构建出原有控制流。插入多余控制流算法OBFCTJbogus是通过向程序中插入多余控制流的方法来实现控制流的复杂化。这一算法的主要实现方法是分离程序中的基本块并插入不透明表达式。

除了2.1节中所示的三种方式,还可以通过图2-4所示方法向程序中插入多余控制流。

代码保护技术:控制流混淆_第5张图片 图2-4 循环条件中插入不透明谓词

在循环条件P中加入不透明谓词,使程序看上去必须要在P和PT都为真的时候才能继续执行。

按照结构化的编程规则,用嵌套的判断和循环语句编程,程序的控制流图就是可归约的。这一类程序的控制流相对清晰。如果在程序中加上直接跳转到循环内部的语句,那么这个循环就会多出一个入口。这样生成的控制流图就是不可归约的例如某程序段:

while(1)
{
	y = 20;
	x = y + 10;
	return x;
}

在循环开始前插入条件判断语句,让其跳转到循环内部,修改后代码如下:

if(PF) goto b;
while(1)
{
	x = y + 10;
	return x
	b : y = 20;
}

构造出图2-5所示控制流图后可以很明显看出它的分析难度加大了。

代码保护技术:控制流混淆_第6张图片 图2-5 不可归约控制流图

2.4 通过函数跳转执行无条件转移指令

另一种控制流混淆的基本方法是通过跳转函数来执行无条件转移指令。下图2-6所示即采用了这种思想实现的混淆算法OBFLDK的基本过程。

OBFLDK算法基本过程 图2-6 OBFLDK算法基本过程

用函数bf实现原本无条件跳转指令jmp b的功能,用一个哈希函数计算返回值a的hash值,再用这个hash值查表T来实现跳转。这一做法可以有效对抗静态分析,但是抗动态分析能力不足。

你可能感兴趣的:(软件保护技术)