对多态技术感兴趣朋友,可能多是钟情于代码混淆这块,无意中发现了这篇文章,可能大侠们早已看过,但偶觉得写的比较有趣,他从另一个角度揭示了一种代码变换技术。于是做了一个简单的翻译(水平有限,大家凑合看喽),原文链接:
http://mirror.sweon.net/z0mbie/pgames.txt
可肯能我们写多态时,更多的是考虑等效指令替换,指令乱序,随机指令生成,随机寄存器等这些因素,这篇文章倒是提了另外的思路,所以可以学习一下了,虽然不一定马上加入自己写的东东里,但留个思路日后琢磨吗,不废话了,下面就是. Polymorphic Games
=================
part 1 -- conditional execution w/o jxx
---------------------------------------
让我们考虑一些 C to asm 的转换示例吧。
我们设置 a 是 eax ,b 是 ebx ,c 是 ecx, d 是 edx,“condition” 是一个结果或者是些二进制的对比值, 例如一个位,0 或者 1
例子 1
---------
首先,我们要知道下面的语句如何反汇编:
c = condition ? -1 : 0;
这个看起来像:
; CF <-- condition
sbb ecx, ecx
or
; ecx.high_bit <-- condition
sar ecx, 31 例子 2
---------
让我们看一个更真实的情况:
c = a < b ? -1 : 0;
或者从这个例子:
if (a < b) c = -1; else c = 0;
因此,我们必选这样:
cmp eax, ebx
sbb ecx, ecx
或者
mov ecx, eax
sub ecx, ebx
sar ecx, 31
例子 3
---------
一些更复杂点的代码, 例如 a=MIN(a,b) function:
if (a > b) a = b;
这相当于:
a = a > b ? b : a;
也相当于:
a = a + ((a > b ? -1 : 0) & (b - a))
也相当于:
a += ((b - a) < 0 ? -1 : 0) & (b - a)
其中(例子1和例子2)也可以用如下的方式来表达:
sub ebx, eax
sbb ecx, ecx
and ecx, ebx
add eax, ecx
例子 4
---------
绝对值, abs() function:
a = abs(a)
if (a < 0) a = -a;
a = a < 0 ? -a : a;
但是,有了NEG操作,它如同 NOT + INC (我考虑典型的 x86 asm)
我可以通过如下的方式做:
a = (a ^ (a<0?-1:0)) - (a<0?-1:0)
反汇编看起来是这样:
mov edx, eax
sar edx, 31
xor eax, edx
sub eax, edx
example 5
---------
一些C的代码:
if (a != 0)
a = b;
else
a = c;
a = a ? b : c;
a = ((a != 0 ? -1 : 0) & b) | ((a != 0 ? 0 : -1) & c);
因此,它看起来像这样:
cmp eax, 1
sbb eax, eax
and ecx, eax
xor eax, -1
and eax, ebx
or eax, ecx
正如你所看到的,一些基本的操作码都可以被重新编码,除了条件跳转语句,
即便我们有些条件语句,那其实也可以避免使用jxx 指令来达到目的.
例如:
if (c) a >>= b
可以被转成
a >>= (c ? b : 0),
这相当于例5 + shr
等等.
但是,如果我们想调用子程序呢? okey,这有解决的办法:
func_addr = condition ? real_func : fake_func;
func_addr(arg1, arg2, ...);
int fake_func() { return 0; }
但是,如果我想初始化一些变量呢?让我们先这样做:
var_addr = condition ? &real_var : &fake_var;
*var_addr = ...;
因此,一下情况:
if (condition)
{
statement1;
statement2;
statementN;
}
你可以转换成:
if (condition) statement1;
if (condition) statement2;
if (condition) statementN;
每一行都可以单独编码,使用的技术,可以看上面的例子.
我们唯一不能改变,是直接跳转,和循环处理。
像 while() or for(;;).
然而,我们可以扩展到有条件的执行整个程序,
像这样所示:
while(1)
{
if (c) line1;
if (c) line2;
if (c) c = ...;
if (c) line3;
if (c) line4;
}
然后,我们仅需要一个jmp跳转这样的每一个程序上.
正如你所看到的,可以有很多条件,
if (c) if (d) if (e) lineN
- 3个嵌套循环,
或者,我们可以写成一个状态机的形式,例如
if (c==1) line1;
if (c>=2&&c<=3) line2;
if (c==4) line3;
等等,所有的程序操作都将可以编码成 w/o jmps
对于一个程序,编写者尽量最少的使用jxx,理由如下:
1 难以理解其逻辑
2. 更不容易理解其指令的排列顺序
ok, 可以想象,用相同的C的源代码,来产生不同的一些不同的独特的程序.
你可以写一些模板,通过脚本来处理这些模板到C的转换。
在这些模板中,你可以替换一些像下面的宏的一些定义,这些都是可以随机选择的. #define H0(x) (((signed)(x)) >> (sizeof((signed)(x))*8-1))
#define H1(a,b) H0((a)-(b))
#define MIN1(a,b) ((a)+(H1(b,a) & ((b)-(a))))
#define MIN2(a,b) ((a)-(H1(b,a) & ((a)-(b))))
#define MIN3(a,b) ((b)-(H1(a,b) & ((b)-(a))))
#define MIN4(a,b) ((b)+(H1(a,b) & ((a)-(b))))
//#define MIN5(a,b) ((a)<(b)?(a):(b))
//#define MIN6(a,b) ((a)>(b)?(b):(a))
//#define MIN7(a,b) ((b)>(a)?(a):(b))
//#define MIN8(a,b) ((b)<(a)?(b):(a))
#define MAX1(a,b) ((a)+(H1(a,b) & ((b)-(a))))
#define MAX2(a,b) ((a)-(H1(a,b) & ((a)-(b))))
#define MAX3(a,b) ((b)-(H1(b,a) & ((b)-(a))))
#define MAX4(a,b) ((b)+(H1(b,a) & ((a)-(b))))
//#define MAX5(a,b) ((a)<(b)?(b):(a))
//#define MAX6(a,b) ((a)>(b)?(a):(b))
//#define MAX7(a,b) ((b)>(a)?(b):(a))
//#define MAX8(a,b) ((b)<(a)?(a):(b))
#define ABS1(a) (((a)^H0(a))-H0(a))
//#define ABS2(a) ((a)>0?(a):-(a))
//#define ABS3(a) ((a)>=0?(a):-(a))
//#define ABS4(a) ((a)<0?-(a):(a))
//#define ABS5(a) ((a)<=0?-(a):(a))
所有的这些宏可以产生不同的代码,生成w/o jmps 类型的代码. part 2 -- generating code
-------------------------
在某些情况下,复杂的多态解密器是可以被 set-of-instructions 技术检测出来的.
这一技术可以被定义为以下方面:
if (一些代码已经包含了一些已知的指令)
{
return true
or
仿真这些指令.
}
因此,分析算法时当遇到非定义的指令,那将直接returns false.
可能对抗这种算法检测的方法是,用mistfall-like decryptor 技术注入到
程序代码中(入口模糊),使多态部分的解码器分多个小的代码片断,这些部分都是
间接的联系(例如,分散不同的代码节位置,并利用预先设计的程序进行这些空间的计算)
当你再产生代码时,就可以用下面的方法来改变他们了:
例子 6
---------
; at this step we know result of (r1 ? r2)
cmp r1, r2
jxx l2
l1:
; never executed really, but can be emulated
{random part of random .code segment}
l2:
; can be either executed or emulated
{part of our code} 例子 7
---------
; at this step we know that (r1 % (N+1)) == 2
and r1, N
<jmp|call> dword ptr [offset table1 + r1 * 4]
...
table1: dd l0
dd l1
dd l2
...
dd lN
l0:
; never executed really, but can be emulated
{random part of random .code segment}
l1:
; never executed really, but can be emulated
{random part of random .code segment}
l2:
; can be either executed or emulated
{part of our code}
lN:
...
正如您所看到的,例6/7所构造的方法将允许您产生这样的代码.
1 总体的指令效果是非常接近标志的指令(ps:猜想他的大意应该是说,构造的指令在效果上同真实的、人为写的程序很接近,也就是很
好的迷惑了扫描器)
2. 充分和正确的仿真是需要一个真正确定的指令指标(ps:应该就是指对set-of-instructions定义的那些指令)
给出的定理:如果一些算法检测将要分析每个有条件指令的跳转,如果仅选择一个条件的指令流(其中只以一些指令是用来定义的)进行分析,那么这将导致误报。
然而,在这里要应该指出:统计法或更先进的
检测算法被用来使用时,是所有其它的简单方法都失效时才会这样做。
那么唯一的办法是写在near-to-undetectable的代码是不变的,检测算法修正所有导致检测失败的错误。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)