首页
论坛
专栏
课程

[原创]代码混淆之我见(一)

三十二变
2
2018-10-4 14:46 3895
前言:
本人只是一个中学生,属于业余爱好者,没有经过系统学习,如有错误,望指正。
  • 混淆技术概论 
究竟是什么是混淆?如果仔细思考一下这个问题,就会发现混淆其实是打破思维惯性的一种方式。不可否认惯性思维带来的方便,但是同样有其局限性。我们一般对反汇编代码进行还原时,默认CALL就是对一个函数的调用,碰到RET就是函数返回,条件分支两侧的代码都有可能被执行。而代码混淆就是打破了这种思维惯性,让逆向工程变得更加复杂。
说到混淆,就不得不提到编译原理。编译器在把中间代码翻译为目标程序时,会先经过一个代码优化器来处理。
其过程可以表示为:
源程序 - > 前端 - > 中间代码 - > 代码优化器 - > 中间代码 - > 代码生成器 - > 目标程序
而混淆,就是代码优化器的逆过程。
相应的,混淆技术也可以分为两类,基于控制流的混淆,基于数据流的混淆。而实际应用中,这两类技术都会被紧密结合在一起。
所以如果一个人要进行解混淆操作,而这个人恰好对编译优化技术一窍不通 (   比如我  :)    ),可以说,他能做到的最好的解混淆也就是搜一搜特征码,做模式替换。 
  • 基于数据流的混淆技术 
  • (1)常量展开(常量合并的逆操作)
编译器在编译时,会把那些在每次运行时总是得到相同常量值的表达式替换为该常量值。
int a;
a = 5 * 7 + 10;
比如上面这段代码,在现代编译器中是不可能把"5 * 7 + 10"这个计算过程编译进目标程序的,因为这个值在编译时就可以推算出来。
对代码进行混淆时,我们可以提取出一些指令的立即数,对其进行展开。
push 2
push 1
inc dword ptr[esp],1

这两段代码是完全等价的,如果忽略标志位的变化的话。在VMProtect 1.X(早期版本)中,该混淆技术被大量运用。
  • (2)恒等运算
x - 1 == ~-x
x + 1 == -x
ror x,y == x >> y | (x << (lenbite(x) - y))
rol x,y ==x << y | (x >> (lenbite(x) - y))

lenbite指取位数,比如DWORD取位数是32 。

not reg32
xor reg32,-1

上面两条指令是完全等价的,但将not reg32替换为xor reg32,-1,足以使一些人摸不着头脑。

  • (3)模式替换(窥孔优化的逆操作)
push x
lea esp,[esp - 4]
mov [esp],x
lea esp,[esp - 4]
push reg32
mov reg32,esp
xchg [reg32],esp
pop esp
上述第1段代码与第2段代码是完全等价的,而第3段代码与第4段代码又是完全等价的。
这项技术的可怕在于,这个过程是可以不断重复的。混淆了一次代码,可能又会有新的可以混淆的代码出现。
这项技术在Themida的虚拟机里运用地非常成熟。
当然,尽管其变形后的代码往往让人读了之后有想吐的感觉,但其实去混淆非常简单。你放心,我绝不会编这么一个无聊的小孩子谎话来骗你,只要搜集到足够多的模板,对其替换回来就好了。唯一要考虑的难题就是,你的编码能力能不能支撑你写完一个高阶的模式匹配器。

  • 基于控制流的混淆技术
  • (1)插入死代码(消除死代码的逆操作)
如果一个变量在程序的某一点上的值以后可能会被用到,则该变量在改点上是活的,否则,它在该点上就是死的。与此相关的一个概念就是死代码,即永远不会被使用的语句。
基于该项技术,还可以延伸出伪造基本块等混淆技术。
其思路就是在两个基本块之间,或者我们可以在一个基本块里强行再随机选择首指令划出两个基本块,这两个新基本块本应该有一条无条件转移指令连接,我们可以伪造一条条件跳转指令,而有效的,永远是两侧分支中的一支。如果更进一步,对于那些永远不会执行的代码,我们可以进行巧妙构造来对抗静态反汇编工具。
如何不让破解者轻易的发现这是伪造的基本块呢?
我们可以引入一些必然成立的数学公式,或者在某个范围上必然成立的数学公式。
比如,贯穿整个高中函数题的基本不等式。
a^2 + b^2 >= 2(ab)^(1/2)
或者柯西不等式。
(a^2+b^2)(c^2 + d^2)≥(ac+bd)^2
再阴险一点的话,可以考虑去数论里面找一些灵感,比如费马小定理。
  • (2)流图展平
流图是用来表示中间代码的一种方式。流图就是一个有向图,基本块构成图的节点,基本块之间以转移指令构造出来的联系就是图的边。
借助IDA等工具,我们可以清楚地看到一段代码的流图,而这对我们逆向工程的进行是很有益处的。有了控制流,我们就知道往哪里入手,如果走控制流行不通,我们就只能走数据流,比如常见的我们对内存中的关键信息下内存断点。如果数据流和控制流都走不通,那么唯有设计一个解混淆工具,这是背水一战。 
所谓流图展平,其实和虚拟机是差不多的一项技术。

如下图所示,就是一个非常典型的虚拟机。
  


而流图展平,和虚拟机一样,都是对程序的控制流进行了处理。唯一不同的就是,虚拟机里的Handle,变成了基本块。

  • (3)虚拟机
没什么多说的,大名鼎鼎的一项技术。
简单来说就是将部分代码重新编译一次,并打包解释器进程序,运行的时候动态解释执行。
但这种技术会带来很多额外的CPU开销,不能用来,或者说不会有人用来保护算法性质的代码。

  • (4)打乱代码顺序局部性
同一基本块中的指令基本都是按顺序排列的,而不同基本块之间也基本上是按照一定顺序排列的,因为这可以减少跳转指令的数量。
在十年前,或者说更早的时候,用JMP IMM32来打乱代码顺序这种技术的运用可以说是数不胜数。
但以现在的眼光来看,这种技术完全是幼儿园五岁小孩的把戏。有太多办法可以把它KILL掉,这里不多赘述。

  • (5)特殊的控制流模糊化
我们可以采取异常处理机制,设置程序的下一步状态。但这也是基于系统的,你不能指望在Linux上面还能使用VEH。
实现思路就很简单了,现在也还有一些壳会用到这种技术。
比较常见的应用就是,安装一个SEH,然后触发一个异常,在异常处理Handle里面将程序设置回正常的状态。
如果说阴险的话,我首推PESpin壳,该壳的v1.33版本会使用Debug机制接管程序,并转移一部分指令进虚拟机,异常触发时由调试程序来执行。

总结:
如果把上述的混淆技术,单独提出一两种来运用,则其效果是非常差的。一个好的混淆引擎,应该设计成可重用的,支持多次轮环加密的。

  • 尝试实现一个混淆器之流图展平 
源程序里面用到的PE操作库,反汇编引擎,汇编引擎,我都是直接在Github上面找的,它们分别是,PEFile、BeaEngine、XDEParser。
如果要实现流图展平的话,首先我们得理出程序的基本块。

基本块的定义如下。
(1)控制流只能从基本块的第一条指令进入该块。也就是说,没有跳转到该基本块中间的指令。
(2)除了基本块尾指令,否则不会有机会离开基本块。 

如果要划分基本块的话,首先需要确定首指令。
首指令的选择规则如下。
(1)待混淆的所有代码的第一条指令是首指令。
(2)任意一条转移指令的目标指令是一个首指令。
(3)紧跟在一条转移指令后的指令是一个首指令。
每个首指令对应的基本块包括了从它开始,直到下一个首指令(不含)或者结尾指令的所有指令。 

以一段汇编代码为例。
如下图所示。



首先,我们需要选择首指令,根据首指令的选择规则,标出首指令。



接着选择基本块。
按照基本块划分规则,我们可以划分出。
1>
mov eax,2018h
cmp eax,7DFh
jbe Label1

2>
mov ecx,1
jmp Label2

3>
mov ecx,0

4>
ret

实际处理的时候,如果尾指令是流程转移指令,我们应该把这条指令抹掉,因为每个代码块之间的联系不再是由流程转移指令维护了,而是隐藏在对Dispatch的调用中,而对于尾指令不是流程转移指令的,我们可以向其尾部附加一条无条件转移指令,当做尾指令是流程转移指令的情况来处理。

Dispatch的设计如下。
pushad
pushfd
mov		edx,dword ptr[esp]			;edx - > eflags
mov		ecx,dword ptr[esp + 02Ch]	;ecx - > BranchType
cmp		ecx,0						;JO
jnz		@F
push	edx
popfd
mov		edx,0
mov		eax,1
cmovo	edx,eax
jmp		end5
@@:
cmp		ecx,1						;JC | JB
jnz		@F
push	edx
popfd
mov		edx,0
mov		eax,1
cmovc	edx,eax
jmp		end5
@@:
cmp		ecx,2						;JE
jnz		@F
push	edx
popfd
mov		edx,0
mov		eax,1
cmove	edx,eax
jmp		end5
@@:
cmp		ecx,3						;JS
jnz		@F
push	edx
popfd
mov		edx,0
mov		eax,1
cmovs	eax,edx
jmp		end5
@@:
cmp		ecx,4						;JP
jnz		@F
push	edx
popfd
mov		edx,0
mov		eax,1
cmovp	eax,edx	
jmp		end5
@@:
cmp		ecx,5						;JL
jnz		@F
mov		eax,1
push	edx
popfd
mov		edx,0
cmovl	edx,eax
jmp		end5
@@:
cmp		ecx,7						;JG
jnz		@F
mov		eax,1
push	edx
popfd
mov		edx,0
cmovg	edx,eax
jmp		end5
@@:
cmp		ecx,6						;JA
jnz		@F
mov		eax,1
push	edx
popfd
mov		edx,0
cmova	edx,eax
@@:
mov		eax,dword ptr[esp + 01Ch]	;JECXZ
cmp		eax,0
mov		ebx,1
cmovz	edx,ebx
jmp		end5
end5:
push	dword ptr[esp + edx * 4 + 024h]
add		esp,4
popfd
popad
sub		esp,28h
ret		30h

调用方式很简单。其实是我为我自己的懒惰找了个借口 :) 

push	BranchType
push	Branch2		//跳转成立的目标代码块
push	Branch1		//跳转不成立的目标代码块

对于JX和JNX这种类型的跳转,我只实现了JX一种。
其实这两者是可以进行转换的。
如下。

JNX Label1

Label2:

JX Label2

Label1:

这两段代码其实是完全等价的。因为我们设计的Dispatch可以同时指明Label1与Label2,所以把JNX转化为JX是一件很方便的事,只需要把两个目标互换一下即可。

而对于无条件转移指令,可以随便用一条条件转移指令来替代,只要把两个目标设置成一样的即可。在ZProtect里面就大量运用到了这一技巧。

	for (unsigned int i = 0; i < vAsm.size(); i++)	//对所有跳转指令的下一条指令,跳转指令目的地,以及最后一条指令做标记
	{
		if (IsBranch(vAsm[i].stAsm.Instruction.BranchType) && vAsm[i].stAsm.Instruction.AddrValue != 0)
		{
			vAsm[i + 1].states = true;
			for (unsigned int x = 0; x < vAsm.size(); x++)
			{
				if (vAsm[x].stAsm.VirtualAddr == vAsm[i].stAsm.Instruction.AddrValue)
				{
					vAsm[x].states = true;
				}
			}
		}
	}
	vAsm[vAsm.size() - 1].states = true;
	
	CodeBlock stBlock;
	for (unsigned int i = 0; i < vAsm.size(); i++)
	{
		if (vAsm[i].states == true)
		{
			stBlock.Entry = (int)stBlock.vAsm[0].VirtualAddr;
			if (!IsBranch(vAsm[i - 1].stAsm.Instruction.BranchType) || vAsm[i - 1].stAsm.Instruction.AddrValue == 0) //如果尾指令不是跳转指令
			{
				stBlock.iBranch1 = (int)vAsm[i].stAsm.VirtualAddr;
				stBlock.iBranch2 = (int)vAsm[i].stAsm.VirtualAddr;
				srand(unsigned int(time(NULL)));
				stBlock.nBrType = rand() % 8;	//当成JMP指令来处理
			}
			else if (IsBranch(vAsm[i - 1].stAsm.Instruction.BranchType) && vAsm[i - 1].stAsm.Instruction.AddrValue != 0) //如果尾指令是跳转指令
			{
				stBlock.nBrType = tranbr(vAsm[i - 1].stAsm.Instruction.BranchType);
				stBlock.iBranch1 = (int)vAsm[i].stAsm.VirtualAddr;
				stBlock.iBranch2 = (int)vAsm[i - 1].stAsm.Instruction.AddrValue;
				if (stBlock.nBrType == 10)	//如果是JMP指令,随便找一条跳转指令模拟JMP
				{
					srand(unsigned int(time(NULL)));
					stBlock.nBrType = rand() % 8;
					stBlock.iBranch1 = stBlock.iBranch2;
				}
				if (stBlock.nBrType < 0)	//对于JNC Label \ Label2: 指令,将其转换为JC Label2 \ Label1:
				{
					stBlock.nBrType = -stBlock.nBrType;
					int k;
					k = stBlock.iBranch1;
					stBlock.iBranch1 = stBlock.iBranch2;
					stBlock.iBranch2 = k;
				}
				stBlock.vAsm.pop_back();	//删除尾指令
			}
			vBlocks.push_back(stBlock);
			stBlock.vAsm.clear();
			stBlock.vAsm.push_back(vAsm[i].stAsm);
		}
		else
		{
			stBlock.vAsm.push_back(vAsm[i].stAsm);
		}
	}	
	vBlocks[vBlocks.size() - 1].vAsm.push_back(vAsm[vAsm.size() - 1].stAsm);	//将代码切割成基本块

接下来,因为要把代码全部移到新添加的区块,所以要正所有基本块的后继区块的地址(VA地址)。

	for (unsigned int i = 0; i < vBlocks.size() - 1; i++)
	{
		srand(unsigned int(time(NULL)));
		chanblock(vBlocks, i, rand() % (unsigned int)vBlocks.size());
	}
	//先将基本块随机交换位置,打乱代码空间局部性

	int pBase = (int)pe.getaddr(pCopy, vt_offset, vt_va);
	vector<int> vOldEntry;
	const int jmpsize = 17; //每个基本块后都会添加一段指令,以跳转到Dispatch,需要用到该字段计算新的Entry
	for (unsigned int i = 0; i < vBlocks.size(); i++)
	{
		vOldEntry.push_back(vBlocks[i].Entry);
	}
	//记录所有基本块的原入口
	
	vBlocks[0].Entry = pBase;
	for (unsigned int i = 1; i < vBlocks.size(); i++)
	{
		static int len = 0;
		for (unsigned int x = 0; x < vBlocks[i - 1].vAsm.size(); x++)
		{
			memcpy(xde.instr, vBlocks[i - 1].vAsm[x].CompleteInstr, 64);
			xde.cip = 0;
			XEDParseAssemble(&xde);
			len += xde.dest_size; //这里出现设计失误了,本来应该记录一下指令长度的
		}
		len += jmpsize; //基本块尾部跳转到Dispatch的指令长度
		vBlocks[i].Entry = pBase + len;
	}
	//更新所有基本块的入口

	for (unsigned int i = 0; i < vBlocks.size(); i++)
	{
		for (unsigned int x = 0; x < vOldEntry.size(); x++)
		{
			if (vBlocks[i].iBranch1 == vOldEntry[x])
			{
				vBlocks[i].iBranch1 = vBlocks[x].Entry;
			}
			
			if (vBlocks[i].iBranch2 == vOldEntry[x])
			{
				vBlocks[i].iBranch2 = vBlocks[x].Entry;
			}
		}
	}
	//修正所有区块的后继区块

然后就是让原来的代码跳转到混淆后的代码,同时抹掉原代码了,或者说,也可以用一些小技巧实现流程转移。比如,利用栈溢出,不易让攻击者轻易察觉。同时,原来的代码所占的空间也不用浪费,可以随便填一点进去。至于填进去的代码究竟是什么意图?Who TM cares?

参考代码源代码打包已打包。但是据朋友反映说,用的那个PE库(PEFile)操作过的PE文件,在WIN7上打不开,初步排查是资源出了问题。如果确实无法打开,请使用LordPE把经过混淆的程序的.lcz区块保存到硬盘上,然后再打开原程序,手工把区块附加到尾部。找到开始的基本块(注意,是第一个开始执行的基本块,因为把基本块给随机打乱了,不是按照原来的顺序写入的),手工修改一下源代码,跳转过去即可。

以下是一段示例程序,以及混淆前与混淆后的代码的对比。

int _tmain(int argc, _TCHAR* argv[])
{
	int i = 20;
	scanf("%d", &i);
	if (i > 50)
	{
		printf("i > 50\n");
	}
	else
	{
		printf("i < 50\n");
	}
	for (i = 0; i < 20; i++)
	{
		printf("i = %d\n", i);
	}
	getchar();
	getchar();
	return 0;
}

混淆前:







混淆后:











[推荐]十年磨一剑!《加密与解密(第4版)》上市发行

上传的附件:
本主题帖已收到 1 次赞赏,累计¥5.00
最新回复 (34)
kanxue 8 2018-10-4 15:31
2

0

中学就这么厉害!学习能力强
junkboy 2018-10-4 16:19
3

0

支持下
xiaohang 3 2018-10-4 21:12
4

0

现在的中学生nb啊
hzmslx 1 2018-10-5 05:29
5

0

腻害腻害
xinpoo 1 2018-10-5 09:05
6

0

好厉害!不过大神应该是大人了吧,因为你说业余爱好,哈哈
三十二变 2 2018-10-5 09:25
7

0

xinpoo 好厉害!不过大神应该是大人了吧,因为你说业余爱好,哈哈
我是高中学生,职业是学习,业余爱好是逆向。
pitily 2018-10-5 10:22
8

0

有没有大大逆向一下这个样本的感染原理,
分析出来写个报告大家看一下,微云下载链接
https://share.weiyun.com/5k4bAQP

最后于 2018-10-5 10:23 被pitily编辑 ,原因:
xinpoo 1 2018-10-5 13:54
9

0

三十二变 我是高中学生,职业是学习,业余爱好是逆向。[em_20][em_20]
小小年纪就学到这种程度了,好历害。
新月之铭 2018-10-5 19:18
10

0

感觉和十一ctf有关额
黑手鱼 1 2018-10-6 12:36
11

0

我高中还是天天跟朋友跑网吧玩DNF呢
x - 1 == ~-x
x + 1 == -x   是不是应该是x+1 ==-~x ?

下面这几句应该就是这样
    x = x ^ 1;
008D1AAC 8B 45 F8             mov         eax,dword ptr [x] 
008D1AAF 83 F0 01             xor         eax,1 
008D1AB2 89 45 F8             mov         dword ptr [x],eax 
    b = -~b;
008D1AB5 8B 45 EC             mov         eax,dword ptr [b] 
008D1AB8 F7 D0                not         eax 
008D1ABA F7 D8                neg         eax 
008D1ABC 89 45 EC             mov         dword ptr [b],eax 


最后于 2018-10-6 12:43 被黑手鱼编辑 ,原因:
不知世事 1 2018-10-6 17:20
12

0

总结的不错,期待(二)
VCKFC 2018-10-7 10:46
13

0

中学生厉害了,前途无量
icesnowwise 2018-10-7 11:17
14

0

楼主是自学的吗?
yuzhouheike 2018-10-7 17:36
15

0

中学生收徒吗,我是小学生
Reynoldkk 2018-10-8 10:27
16

0

中学生????厉害厉害.....自配不如,期待(二) 
TaoMjkTx 2018-10-8 21:44
17

0

小伙不错,考虑来阿里不?
hkfans 3 2018-10-9 14:03
18

0

好牛逼
kwanhua 2018-10-9 18:09
19

0

楼主牛逼呀,无论是内容还是描述,都很好,感觉楼主都能去做培训了
严启真 2018-10-9 18:26
20

0

这中学生的水平令人恐惧…
kajimazzZ 2018-10-9 18:37
21

0

牛逼,收徒吗高二一枚哈哈哈
老刘NoOne 2018-10-9 19:24
22

0

感谢分享,收藏慢慢研究
ilyzqe 2018-10-10 12:48
23

0

乖乖 收大三的徒弟么?
foyjog_127675 2018-10-10 18:16
24

0

给中学生大佬掉头
挽梦雪舞 2018-10-12 14:05
25

0

高三狗飘过\(〇_o)/
何不敢 2018-10-15 01:45
26

0

现在中学生这么厉害吗...
holing 12 2018-10-15 01:56
27

0

五本失败人士给跪了
菜鸟级X 2018-10-15 14:29
28

0

我感觉我生下来就是凑人数的
小南y 2018-11-7 16:47
29

0

高中生这么厉害,跪了,工作5年了还是小白怎么办?
大C滑稽 2018-12-6 04:08
30

0

楼主能不能留个联系方式一起交流,我也是中学生也想学符号执行
三十二变 2 2018-12-6 22:28
31

0

就我个人觉得。不学编译原理只去学一个逆向工程框架怎么用是并没有什么卵用的。而要玩通逆向,所需要的知识框架不是一个中学生能掌握的。

综上所述:作业写完了吗?
三十二变 2 2018-12-6 22:29
32

0

大C滑稽 楼主能不能留个联系方式一起交流,我也是中学生也想学符号执行[em_48]
个人觉得。不学编译原理只去学一个逆向工程框架怎么用是并没有什么卵用的。而要玩通逆向,所需要的知识框架不是一个中学生能掌握的。

综上所述:作业写完了吗?
大C滑稽 2018-12-7 21:58
33

0

我对angr的原理很好奇,不单单只是使用
大C滑稽 2018-12-7 22:03
34

0

也许不需要实现像Python这样复杂的语言的东西,实战一个类似Scheme的东西
大C滑稽 2018-12-7 22:05
35

0

还有,我天天逃课去网吧学逆向不容易
返回