The Filefix Professional 2009 (wizard.exe) demo

version will uncorrupt (read: decrypt) one file. Which means that

I can learn everything I need to know to decrypt all files from analyzing

just this binary itself.

So, where to start looking? Well a file decryption routine is going to

need to read and write files, so search for calls to ReadFile.

Almost the first thing I find is a loop that calls ReadFile,

has an inner loop that XOR's over each byte in the buffer, and

then calls WriteFile. Hmmm… (See appendix.)

Now all I need are some encrypted files. Filefix Pro doesn't encrypt

anything itself, and I didn't have a sample of the malware which did.

Fortunately (for me), we were in contact with some of the victims, so as

soon as I had some samples it confirmed my suspicion about the encryption just being

ECB-XOR. The only thing which took me more than a minute to figure out was

that the crypto key was stored at the end of the file. (Since I had already

figured out how to decrypt it without knowing the key.)

Spending a little more time reading the binary, I also found the routine

which checks for valid keys at the ends of files. This allows Filefix to tell

corrupt and non-corrupt files apart when scanning the disk. There is a

strict mathematical relationship between the four bytes of the key.

Implemented as three simple boolean tests. If you do the math, this

also means that there are only 256 possible valid keys.



In English


Using the key 0x6622CDAB as an example:




  1. Divide 0xCD by two, and compare to 0x66

  3. XOR 0xAB by 0x66 and compare to 0xCD

  5. AND 0xAB by 0x66 and compare to 0x22



In Perl



@key = (0xAB, 0xCD, 0x22, 0x66);
print "Key Check: First Test Pass\n" if ( ($key[1]>>1) == $key[3] );
print "Key Check: Second Test Pass\n" if ( ($key[0]^$key[3]) == $key[1] );
print "Key Check: Third Test Pass\n" if ( ($key[0]&$key[3]) == $key[2] );



In the original assembly


I have to wonder if the original was written in assembly too.

Compilers generally don't generate code like this.


     ; I hope you can understand this...
mov eax, [esp+10h+Buffer]  ; pointer to four byte result from ReadFile()
     ; Pretend it read 0x6622CDAB from the file
test ah, ah    ; EAX=0x6622CDAB so AH is not 0
jz short loc_405C16  ; loc_405C16 calls CloseHandle() and returns 0
mov cl, byte ptr [esp+10h+Buffer+3] ; CL=0x66 The last byte from the input buffer.
mov dl, ah    ; DL=0xCD
shr dl, 1    ; DL=0x66
cmp cl, dl    ; if (0x66==0x66)
jnz short loc_405C16  ; Yes equal, keep going...
mov dl, cl    ; DL=0x66 and CL=0x66 so this
     ; MOV is completely pointless
xor dl, ah    ; DL=0xAB AH=0x66
cmp al, dl    ; if (0xCD==0xCD)
jnz short loc_405C16  ; Yep, it does, so keep going...
and al, cl    ; AL=0x66&0xAB -> EAX = 0x6622CD22
cmp byte ptr [esp+10h+Buffer+2], al ; if (0x22==0x22) Next to last buffer byte
jnz short loc_405C16  ; So, if we get past here it was a valid key.


By shuffling the math around a bit, you can generate all of the possible valid keys like this (in Perl)


# Mathematically 0x00000000 is a valid key, 
# but there's a test for that, so it's not.
for ($B=1; $B<=0xFF; $B++) {
 $D = $B >> 1;
 $A = $B ^ $D;
 $C = $A & $D;
 print sprintf("%02x%02x%02x%02x\n", $A, $B, $C, $D);


With a keyspace of only 255 valid keys out of 4,294,967,296, and assuming a

perfectly random distribution (YMMV), there is a 1 in 16,843,009 chance that

a non-encrypted file will be decrypted, and corrupted for-reals, since the

last four bytes will be truncated. The file will also be (re-)encrypted,

since this crypto function is its own inverse. (Well, almost, the missing

key will stop Filefix from re-re-decrypting the file, except for one chance

in 4,294,967,296, when the last four bytes of the encrypted file are the same

as the last four bytes of the original file. There are 253 chances out of

4,294,967,296 that it will re-re-re-encrypt the file again with a new key (with

just the XOR's of the two wrong keys.) (If the keys form an Algebraic Group

then this doesn't matter.) (Each time you go through this composition,

you'll loose eight bytes from the end of the file.) (Ok my quick analysis of

the keyspace leads me to believe that the XOR operator does not form a

group over this set.)


Rationale Tangent

The second/third byte of the key… ok

the $C or $key[2] byte from the above examples,

can only have one of 34 possible values, and 0x03 is not

one of them. But 0x01 and 0x02 are valid, so if you

XOR'd them together the result would be 0x03, which is

not valid. Therefore this keyspace is not closed under XOR and is not an Algebraic Group. (So you now have at most 2,147,483,648 possible keys, as

the first byte (big-endian) will never have the high-bit set.)

But anyway, yeah, there is a non-zero chance that Filefix will chop one of

255 possible byte patterns off the end of your perfectly good file, and

then be unable to re-decrypt it itself. (If this happens to you, you can

XOR the first four bytes of the now for-reals corrupt file with the

magic-number of that filetype's signature (i.e. "JFIF" for JPEGs, "%PDF"

for PDFs, 0xD0CF11E0 for Office Documents.) and use that as

the key, and put those four bytes back onto the end of the file.

(I've never actually tried any of this, it's all purely hypothetical.)

Anyway, you don't really need to know any of the above in order to decrypt a file…


The Decryption Algorithm





  1. Filefix searches the entire filesystem for files with one of the following (28) extensions:



    "doc", "xls", "ppt", "pdf", "jpg", "jpeg", "png", "mp3",

    "wma", "mdb", "pst", "docx", "docm", "dotx", "dotm", "xlsx",

    "xlsm", "xltx", "xltm", "xlsb", "xlam", "pptx", "pptm", "potx",

    "potm", "ppam", "ppsx", "ppsm"




  3. It opens each file, and reads the last four bytes. If these four bytes


    satisfy a particular boolean formula (see above), then the file is corrupt,

    and the file is added to the to-decrypt list.


  5. Create a new file, which is this four byte key repeatedly XOR'd over the


    victim-file. Think of it as XOR in ECB mode with a 32-bit key.


  7. Don't write the key back out to the new file. The newly created file will


    be four bytes shorter than the encrypted file. Delete the encrypted file,

    and give the newly decrypted file the same filename as the original.

    (Filesystem metadata is not preserved.)



Yeah, that's it.

I don't have a sample of the trojan which performs this encryption, but

based upon other people's reports, these are my deductions and speculations:




  1. The installer for this malware [Vundo?] has the MD5 hash of

    d54340d03ebf417ac29ce5a087e944c2 (UPX packed) and injects

    C:\WINDOWS\SYSTEM32\fpfstb.dll into the explorer.exe process using

    InjectLibraryRemote. [Information via Norman Sandbox (Mar-08-2009) and

    Bleeping Computer] There may be other variants of this malware.

    See Also:



  3. From the decryption algorithm above, we can deduce the encryption algorithm.



    • The malware generates a random (maybe?) key like this (except not using


      Perl obviously):


       $B = 255 * rand(); # There's probably a test for (0==$B) in the malware
       $D = $B >> 1;
       $A = $B ^ $D;
       $C = $A & $D;
       print sprintf("%02x%02x%02x%02x\n", $A, $B, $C, $D);




    • Create a new file, which is this key repeatedly XOR'd over the


      victim-file. Think of it as XOR in ECB mode with a 32-bit key.


    • Append the four byte key to the end of the new corrupted file, and


      delete the original. The new file grows by four bytes longer than the

      original. Apparently the filenames match up, but no other metadata.



  5. According to victim reports, the trojan encrypts all of the files


    (matching above extension list) within the My Documents directory, and

    then files in other locations, only when opened. This makes me wonder if

    the trojan has hooked a function or two in explorer.exe and is only

    encrypting on file open or close, and that everything ending up encrypted

    under My Documents is a side effect of another benign system process

    scanning through the files. (This is pure speculation on my part.)


  7. Puts an icon into the Windows System Tray, which pops up balloons about


    fixing corrupt files. Apparently directs you to the Filefix Professional

    2009 website.





That's nice, but how do I get my files back?


Please be aware, I can not vouch for any of these, as I have not tested

them at all. Use at your own risk; if it

breaks you get to keep the pieces, [include standard disclaimers here].

Boban Spasic [] wrote a batch decryptor in Delphi (I think, but

it's a Windows EXE so you don't have to care), which is probably the easiest

option currently if you don't know how to program.

Aaron Atwater [Dalhousie University] wrote a batch decrptor in Java. Available here:

There's my own hastily written proof-of-concept Perl script [See

previous blog

entry], which you probably shouldn't use if you don't know Perl.




Update Mar 25


I should have mentioned, the instructions for Bobby's tool (linked above) are at:



Thanks to everyone I mentioned, and anyone I forgot.

Thanks also go to Dan and Travis (via Devshed) who sent me some encrypted files.




Ok, you can stop reading now.

The Filefix Pro 2009 executable I examined has the following properties:

Filename: wizard.exe

Build Time: Wed Mar 18 09:03:10 2009

MD5sum: e1827fbbf959d7c5f3219a1f0b0c35fc

Size: 626688


; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦ S U B R O U T I N E ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦

; int __cdecl Decryption_Routine(LPCSTR lpFileName)
Decryption_Routine proc near  ; CODE XREF: StartAddress+130↑p

var_D  = byte ptr -0Dh
lDistanceToMove = dword ptr -0Ch
NumberOfBytesWritten= dword ptr -8
Buffer  = dword ptr -4
lpFileName = dword ptr 4

  sub esp, 10h
  push ebx
  push esi
  mov esi, [esp+18h+lpFileName]
  xor bl, bl
  push esi  ; lpFileName
  mov [esp+1Ch+var_D], bl
  call Key_Check_Routine
  add esp, 4
  test al, al
  jz loc_405E95
  push 0  ; hTemplateFile
  push 0  ; dwFlagsAndAttributes
  push 3  ; dwCreationDisposition
  push 0  ; lpSecurityAttributes
  push 1  ; dwShareMode
  push 0C0000000h ; dwDesiredAccess
  push esi  ; lpFileName
  call ds:CreateFileA
  mov esi, eax
  cmp esi, 0FFFFFFFFh
  jz loc_405E95
  push ebp
  push edi
  push 0  ; lpFileSizeHigh
  push esi  ; hFile
  call ds:GetFileSize
  mov ebx, eax
  cmp ebx, 40h
  jbe loc_405E88
  mov ebp, ds:SetFilePointer
  push 0  ; dwMoveMethod
  push 0  ; lpDistanceToMoveHigh
  lea eax, [ebx-4]
  push eax  ; lDistanceToMove
  push esi  ; hFile
  mov [esp+30h+lDistanceToMove], 0
  call ebp ; SetFilePointer
  push 0  ; lpOverlapped
  lea ecx, [esp+24h+lDistanceToMove]
  push ecx  ; lpNumberOfBytesRead
  push 4  ; nNumberOfBytesToRead
  lea edx, [esp+2Ch+Buffer]
  push edx  ; lpBuffer
  push esi  ; hFile
  call ds:ReadFile
  push 10000h  ; size_t
  call _malloc
  mov edi, eax
  add esp, 4
  test edi, edi
  jz loc_405E88
  xor eax, eax
  add ebx, 0FFFFFFFCh
  mov [esp+20h+lDistanceToMove], eax
  jz short loc_405E56

loc_405DE7:    ; CODE XREF: Decryption_Routine+114↓j
  push 0  ; dwMoveMethod
  push 0  ; lpDistanceToMoveHigh
  push eax  ; lDistanceToMove
  push esi  ; hFile
  mov [esp+30h+NumberOfBytesWritten], 0
  call ebp ; SetFilePointer
  push 0  ; lpOverlapped
  lea eax, [esp+24h+NumberOfBytesWritten]
  push eax  ; lpNumberOfBytesRead
  push 10000h  ; nNumberOfBytesToRead
  push edi  ; lpBuffer
  push esi  ; hFile
  call ds:ReadFile
  mov ecx, [esp+20h+NumberOfBytesWritten]
  xor eax, eax
  test ecx, ecx
  jbe short loc_405E26

main_crypto_loop:   ; CODE XREF: Decryption_Routine+E4↓j
  mov edx, eax
  and edx, 3
  mov dl, byte ptr [esp+edx+20h+Buffer]
  xor [eax+edi], dl ; The "Decryption" operator.
  inc eax
  cmp eax, ecx
  jb short main_crypto_loop

loc_405E26:    ; CODE XREF: Decryption_Routine+D3↑j
  mov eax, [esp+20h+lDistanceToMove]
  push 0  ; dwMoveMethod
  push 0  ; lpDistanceToMoveHigh
  push eax  ; lDistanceToMove
  push esi  ; hFile
  call ebp ; SetFilePointer
  mov edx, [esp+20h+NumberOfBytesWritten]
  push 0  ; lpOverlapped
  lea ecx, [esp+24h+NumberOfBytesWritten]
  push ecx  ; lpNumberOfBytesWritten
  push edx  ; nNumberOfBytesToWrite
  push edi  ; lpBuffer
  push esi  ; hFile
  call ds:WriteFile
  mov eax, [esp+20h+lDistanceToMove]
  add eax, [esp+20h+NumberOfBytesWritten]
  cmp eax, ebx
  mov [esp+20h+lDistanceToMove], eax
  jb short loc_405DE7

loc_405E56:    ; CODE XREF: Decryption_Routine+A5↑j
  push edi  ; void *
  call _free
  mov eax, [esp+24h+lDistanceToMove]
  add esp, 4
  push 0  ; dwMoveMethod
  push 0  ; lpDistanceToMoveHigh
  add eax, 0FFFFFFFCh
  push eax  ; lDistanceToMove
  push esi  ; hFile
  call ebp ; SetFilePointer
  push esi  ; hFile
  call ds:SetEndOfFile
  push esi  ; hObject
  mov bl, 1
  call ds:CloseHandle
  pop edi
  pop ebp
  pop esi
  mov al, bl
  pop ebx
  add esp, 10h
; –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

loc_405E88:    ; CODE XREF: Decryption_Routine+51↑j
     ; Decryption_Routine+96↑j
  mov bl, [esp+20h+var_D]
  push esi  ; hObject
  call ds:CloseHandle
  pop edi
  pop ebp

loc_405E95:    ; CODE XREF: Decryption_Routine+1A↑j
     ; Decryption_Routine+3B↑j
  pop esi
  mov al, bl
  pop ebx
  add esp, 10h
Decryption_Routine endp




Julia Wolf @ FireEye Malware Intelligence Lab

Questions/Comments to research [@] fireeye [.] com