| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
import os |
|---|
| 7 |
from config import conf |
|---|
| 8 |
from base_classes import BasePacket,BasePacketList |
|---|
| 9 |
from packet import Padding |
|---|
| 10 |
|
|---|
| 11 |
from utils import do_graph,hexdump,make_table,make_lined_table,make_tex_table |
|---|
| 12 |
|
|---|
| 13 |
import arch |
|---|
| 14 |
if arch.GNUPLOT: |
|---|
| 15 |
Gnuplot=arch.Gnuplot |
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
class PacketList(BasePacketList): |
|---|
| 24 |
res = [] |
|---|
| 25 |
def __init__(self, res=None, name="PacketList", stats=None): |
|---|
| 26 |
"""create a packet list from a list of packets |
|---|
| 27 |
res: the list of packets |
|---|
| 28 |
stats: a list of classes that will appear in the stats (defaults to [TCP,UDP,ICMP])""" |
|---|
| 29 |
if stats is None: |
|---|
| 30 |
stats = conf.stats_classic_protocols |
|---|
| 31 |
self.stats = stats |
|---|
| 32 |
if res is None: |
|---|
| 33 |
res = [] |
|---|
| 34 |
if isinstance(res, PacketList): |
|---|
| 35 |
res = res.res |
|---|
| 36 |
self.res = res |
|---|
| 37 |
self.listname = name |
|---|
| 38 |
def _elt2pkt(self, elt): |
|---|
| 39 |
return elt |
|---|
| 40 |
def _elt2sum(self, elt): |
|---|
| 41 |
return elt.summary() |
|---|
| 42 |
def _elt2show(self, elt): |
|---|
| 43 |
return self._elt2sum(elt) |
|---|
| 44 |
def __repr__(self): |
|---|
| 45 |
|
|---|
| 46 |
stats = dict(map(lambda x: (x,0), self.stats)) |
|---|
| 47 |
other = 0 |
|---|
| 48 |
for r in self.res: |
|---|
| 49 |
f = 0 |
|---|
| 50 |
for p in stats: |
|---|
| 51 |
if self._elt2pkt(r).haslayer(p): |
|---|
| 52 |
stats[p] += 1 |
|---|
| 53 |
f = 1 |
|---|
| 54 |
break |
|---|
| 55 |
if not f: |
|---|
| 56 |
other += 1 |
|---|
| 57 |
s = "" |
|---|
| 58 |
ct = conf.color_theme |
|---|
| 59 |
for p in self.stats: |
|---|
| 60 |
s += " %s%s%s" % (ct.packetlist_proto(p.name), |
|---|
| 61 |
ct.punct(":"), |
|---|
| 62 |
ct.packetlist_value(stats[p])) |
|---|
| 63 |
s += " %s%s%s" % (ct.packetlist_proto("Other"), |
|---|
| 64 |
ct.punct(":"), |
|---|
| 65 |
ct.packetlist_value(other)) |
|---|
| 66 |
return "%s%s%s%s%s" % (ct.punct("<"), |
|---|
| 67 |
ct.packetlist_name(self.listname), |
|---|
| 68 |
ct.punct(":"), |
|---|
| 69 |
s, |
|---|
| 70 |
ct.punct(">")) |
|---|
| 71 |
def __getattr__(self, attr): |
|---|
| 72 |
return getattr(self.res, attr) |
|---|
| 73 |
def __getitem__(self, item): |
|---|
| 74 |
if isinstance(item,type) and issubclass(item,BasePacket): |
|---|
| 75 |
return self.__class__(filter(lambda x: item in self._elt2pkt(x),self.res), |
|---|
| 76 |
name="%s from %s"%(item.__name__,self.listname)) |
|---|
| 77 |
if type(item) is slice: |
|---|
| 78 |
return self.__class__(self.res.__getitem__(item), |
|---|
| 79 |
name = "mod %s" % self.listname) |
|---|
| 80 |
return self.res.__getitem__(item) |
|---|
| 81 |
def __getslice__(self, *args, **kargs): |
|---|
| 82 |
return self.__class__(self.res.__getslice__(*args, **kargs), |
|---|
| 83 |
name="mod %s"%self.listname) |
|---|
| 84 |
def __add__(self, other): |
|---|
| 85 |
return self.__class__(self.res+other.res, |
|---|
| 86 |
name="%s+%s"%(self.listname,other.listname)) |
|---|
| 87 |
def summary(self, prn=None, lfilter=None): |
|---|
| 88 |
"""prints a summary of each packet |
|---|
| 89 |
prn: function to apply to each packet instead of lambda x:x.summary() |
|---|
| 90 |
lfilter: truth function to apply to each packet to decide whether it will be displayed""" |
|---|
| 91 |
for r in self.res: |
|---|
| 92 |
if lfilter is not None: |
|---|
| 93 |
if not lfilter(r): |
|---|
| 94 |
continue |
|---|
| 95 |
if prn is None: |
|---|
| 96 |
print self._elt2sum(r) |
|---|
| 97 |
else: |
|---|
| 98 |
print prn(r) |
|---|
| 99 |
def nsummary(self,prn=None, lfilter=None): |
|---|
| 100 |
"""prints a summary of each packet with the packet's number |
|---|
| 101 |
prn: function to apply to each packet instead of lambda x:x.summary() |
|---|
| 102 |
lfilter: truth function to apply to each packet to decide whether it will be displayed""" |
|---|
| 103 |
for i in range(len(self.res)): |
|---|
| 104 |
if lfilter is not None: |
|---|
| 105 |
if not lfilter(self.res[i]): |
|---|
| 106 |
continue |
|---|
| 107 |
print conf.color_theme.id(i,fmt="%04i"), |
|---|
| 108 |
if prn is None: |
|---|
| 109 |
print self._elt2sum(self.res[i]) |
|---|
| 110 |
else: |
|---|
| 111 |
print prn(self.res[i]) |
|---|
| 112 |
def display(self): |
|---|
| 113 |
"""deprecated. is show()""" |
|---|
| 114 |
self.show() |
|---|
| 115 |
def show(self, *args, **kargs): |
|---|
| 116 |
"""Best way to display the packet list. Defaults to nsummary() method""" |
|---|
| 117 |
return self.nsummary(*args, **kargs) |
|---|
| 118 |
|
|---|
| 119 |
def filter(self, func): |
|---|
| 120 |
"""Returns a packet list filtered by a truth function""" |
|---|
| 121 |
return self.__class__(filter(func,self.res), |
|---|
| 122 |
name="filtered %s"%self.listname) |
|---|
| 123 |
def make_table(self, *args, **kargs): |
|---|
| 124 |
"""Prints a table using a function that returs for each packet its head column value, head row value and displayed value |
|---|
| 125 |
ex: p.make_table(lambda x:(x[IP].dst, x[TCP].dport, x[TCP].sprintf("%flags%")) """ |
|---|
| 126 |
return make_table(self.res, *args, **kargs) |
|---|
| 127 |
def make_lined_table(self, *args, **kargs): |
|---|
| 128 |
"""Same as make_table, but print a table with lines""" |
|---|
| 129 |
return make_lined_table(self.res, *args, **kargs) |
|---|
| 130 |
def make_tex_table(self, *args, **kargs): |
|---|
| 131 |
"""Same as make_table, but print a table with LaTeX syntax""" |
|---|
| 132 |
return make_tex_table(self.res, *args, **kargs) |
|---|
| 133 |
|
|---|
| 134 |
def plot(self, f, lfilter=None,**kargs): |
|---|
| 135 |
"""Applies a function to each packet to get a value that will be plotted with GnuPlot. A gnuplot object is returned |
|---|
| 136 |
lfilter: a truth function that decides whether a packet must be ploted""" |
|---|
| 137 |
g=Gnuplot.Gnuplot() |
|---|
| 138 |
l = self.res |
|---|
| 139 |
if lfilter is not None: |
|---|
| 140 |
l = filter(lfilter, l) |
|---|
| 141 |
l = map(f,l) |
|---|
| 142 |
g.plot(Gnuplot.Data(l, **kargs)) |
|---|
| 143 |
return g |
|---|
| 144 |
|
|---|
| 145 |
def diffplot(self, f, delay=1, lfilter=None, **kargs): |
|---|
| 146 |
"""diffplot(f, delay=1, lfilter=None) |
|---|
| 147 |
Applies a function to couples (l[i],l[i+delay])""" |
|---|
| 148 |
g = Gnuplot.Gnuplot() |
|---|
| 149 |
l = self.res |
|---|
| 150 |
if lfilter is not None: |
|---|
| 151 |
l = filter(lfilter, l) |
|---|
| 152 |
l = map(f,l[:-delay],l[delay:]) |
|---|
| 153 |
g.plot(Gnuplot.Data(l, **kargs)) |
|---|
| 154 |
return g |
|---|
| 155 |
|
|---|
| 156 |
def multiplot(self, f, lfilter=None, **kargs): |
|---|
| 157 |
"""Uses a function that returns a label and a value for this label, then plots all the values label by label""" |
|---|
| 158 |
g=Gnuplot.Gnuplot() |
|---|
| 159 |
l = self.res |
|---|
| 160 |
if lfilter is not None: |
|---|
| 161 |
l = filter(lfilter, l) |
|---|
| 162 |
|
|---|
| 163 |
d={} |
|---|
| 164 |
for e in l: |
|---|
| 165 |
k,v = f(e) |
|---|
| 166 |
if k in d: |
|---|
| 167 |
d[k].append(v) |
|---|
| 168 |
else: |
|---|
| 169 |
d[k] = [v] |
|---|
| 170 |
data=[] |
|---|
| 171 |
for k in d: |
|---|
| 172 |
data.append(Gnuplot.Data(d[k], title=k, **kargs)) |
|---|
| 173 |
|
|---|
| 174 |
g.plot(*data) |
|---|
| 175 |
return g |
|---|
| 176 |
|
|---|
| 177 |
|
|---|
| 178 |
def rawhexdump(self): |
|---|
| 179 |
"""Prints an hexadecimal dump of each packet in the list""" |
|---|
| 180 |
for p in self: |
|---|
| 181 |
hexdump(self._elt2pkt(p)) |
|---|
| 182 |
|
|---|
| 183 |
def hexraw(self, lfilter=None): |
|---|
| 184 |
"""Same as nsummary(), except that if a packet has a Raw layer, it will be hexdumped |
|---|
| 185 |
lfilter: a truth function that decides whether a packet must be displayed""" |
|---|
| 186 |
for i in range(len(self.res)): |
|---|
| 187 |
p = self._elt2pkt(self.res[i]) |
|---|
| 188 |
if lfilter is not None and not lfilter(p): |
|---|
| 189 |
continue |
|---|
| 190 |
print "%s %s %s" % (conf.color_theme.id(i,fmt="%04i"), |
|---|
| 191 |
p.sprintf("%.time%"), |
|---|
| 192 |
self._elt2sum(self.res[i])) |
|---|
| 193 |
if p.haslayer(conf.raw_layer): |
|---|
| 194 |
hexdump(p.getlayer(conf.raw_layer).load) |
|---|
| 195 |
|
|---|
| 196 |
def hexdump(self, lfilter=None): |
|---|
| 197 |
"""Same as nsummary(), except that packets are also hexdumped |
|---|
| 198 |
lfilter: a truth function that decides whether a packet must be displayed""" |
|---|
| 199 |
for i in range(len(self.res)): |
|---|
| 200 |
p = self._elt2pkt(self.res[i]) |
|---|
| 201 |
if lfilter is not None and not lfilter(p): |
|---|
| 202 |
continue |
|---|
| 203 |
print "%s %s %s" % (conf.color_theme.id(i,fmt="%04i"), |
|---|
| 204 |
p.sprintf("%.time%"), |
|---|
| 205 |
self._elt2sum(self.res[i])) |
|---|
| 206 |
hexdump(p) |
|---|
| 207 |
|
|---|
| 208 |
def padding(self, lfilter=None): |
|---|
| 209 |
"""Same as hexraw(), for Padding layer""" |
|---|
| 210 |
for i in range(len(self.res)): |
|---|
| 211 |
p = self._elt2pkt(self.res[i]) |
|---|
| 212 |
if p.haslayer(Padding): |
|---|
| 213 |
if lfilter is None or lfilter(p): |
|---|
| 214 |
print "%s %s %s" % (conf.color_theme.id(i,fmt="%04i"), |
|---|
| 215 |
p.sprintf("%.time%"), |
|---|
| 216 |
self._elt2sum(self.res[i])) |
|---|
| 217 |
hexdump(p.getlayer(Padding).load) |
|---|
| 218 |
|
|---|
| 219 |
def nzpadding(self, lfilter=None): |
|---|
| 220 |
"""Same as padding() but only non null padding""" |
|---|
| 221 |
for i in range(len(self.res)): |
|---|
| 222 |
p = self._elt2pkt(self.res[i]) |
|---|
| 223 |
if p.haslayer(Padding): |
|---|
| 224 |
pad = p.getlayer(Padding).load |
|---|
| 225 |
if pad == pad[0]*len(pad): |
|---|
| 226 |
continue |
|---|
| 227 |
if lfilter is None or lfilter(p): |
|---|
| 228 |
print "%s %s %s" % (conf.color_theme.id(i,fmt="%04i"), |
|---|
| 229 |
p.sprintf("%.time%"), |
|---|
| 230 |
self._elt2sum(self.res[i])) |
|---|
| 231 |
hexdump(p.getlayer(Padding).load) |
|---|
| 232 |
|
|---|
| 233 |
|
|---|
| 234 |
def conversations(self, getsrcdst=None,**kargs): |
|---|
| 235 |
"""Graphes a conversations between sources and destinations and display it |
|---|
| 236 |
(using graphviz and imagemagick) |
|---|
| 237 |
getsrcdst: a function that takes an element of the list and return the source and dest |
|---|
| 238 |
by defaults, return source and destination IP |
|---|
| 239 |
type: output type (svg, ps, gif, jpg, etc.), passed to dot's "-T" option |
|---|
| 240 |
target: filename or redirect. Defaults pipe to Imagemagick's display program |
|---|
| 241 |
prog: which graphviz program to use""" |
|---|
| 242 |
if getsrcdst is None: |
|---|
| 243 |
getsrcdst = lambda x:(x['IP'].src, x['IP'].dst) |
|---|
| 244 |
conv = {} |
|---|
| 245 |
for p in self.res: |
|---|
| 246 |
p = self._elt2pkt(p) |
|---|
| 247 |
try: |
|---|
| 248 |
c = getsrcdst(p) |
|---|
| 249 |
except: |
|---|
| 250 |
|
|---|
| 251 |
continue |
|---|
| 252 |
conv[c] = conv.get(c,0)+1 |
|---|
| 253 |
gr = 'digraph "conv" {\n' |
|---|
| 254 |
for s,d in conv: |
|---|
| 255 |
gr += '\t "%s" -> "%s"\n' % (s,d) |
|---|
| 256 |
gr += "}\n" |
|---|
| 257 |
return do_graph(gr, **kargs) |
|---|
| 258 |
|
|---|
| 259 |
def afterglow(self, src=None, event=None, dst=None, **kargs): |
|---|
| 260 |
"""Experimental clone attempt of http://sourceforge.net/projects/afterglow |
|---|
| 261 |
each datum is reduced as src -> event -> dst and the data are graphed. |
|---|
| 262 |
by default we have IP.src -> IP.dport -> IP.dst""" |
|---|
| 263 |
if src is None: |
|---|
| 264 |
src = lambda x: x['IP'].src |
|---|
| 265 |
if event is None: |
|---|
| 266 |
event = lambda x: x['IP'].dport |
|---|
| 267 |
if dst is None: |
|---|
| 268 |
dst = lambda x: x['IP'].dst |
|---|
| 269 |
sl = {} |
|---|
| 270 |
el = {} |
|---|
| 271 |
dl = {} |
|---|
| 272 |
for i in self.res: |
|---|
| 273 |
try: |
|---|
| 274 |
s,e,d = src(i),event(i),dst(i) |
|---|
| 275 |
if s in sl: |
|---|
| 276 |
n,l = sl[s] |
|---|
| 277 |
n += 1 |
|---|
| 278 |
if e not in l: |
|---|
| 279 |
l.append(e) |
|---|
| 280 |
sl[s] = (n,l) |
|---|
| 281 |
else: |
|---|
| 282 |
sl[s] = (1,[e]) |
|---|
| 283 |
if e in el: |
|---|
| 284 |
n,l = el[e] |
|---|
| 285 |
n+=1 |
|---|
| 286 |
if d not in l: |
|---|
| 287 |
l.append(d) |
|---|
| 288 |
el[e] = (n,l) |
|---|
| 289 |
else: |
|---|
| 290 |
el[e] = (1,[d]) |
|---|
| 291 |
dl[d] = dl.get(d,0)+1 |
|---|
| 292 |
except: |
|---|
| 293 |
continue |
|---|
| 294 |
|
|---|
| 295 |
import math |
|---|
| 296 |
def normalize(n): |
|---|
| 297 |
return 2+math.log(n)/4.0 |
|---|
| 298 |
|
|---|
| 299 |
def minmax(x): |
|---|
| 300 |
m,M = min(x),max(x) |
|---|
| 301 |
if m == M: |
|---|
| 302 |
m = 0 |
|---|
| 303 |
if M == 0: |
|---|
| 304 |
M = 1 |
|---|
| 305 |
return m,M |
|---|
| 306 |
|
|---|
| 307 |
mins,maxs = minmax(map(lambda (x,y): x, sl.values())) |
|---|
| 308 |
mine,maxe = minmax(map(lambda (x,y): x, el.values())) |
|---|
| 309 |
mind,maxd = minmax(dl.values()) |
|---|
| 310 |
|
|---|
| 311 |
gr = 'digraph "afterglow" {\n\tedge [len=2.5];\n' |
|---|
| 312 |
|
|---|
| 313 |
gr += "# src nodes\n" |
|---|
| 314 |
for s in sl: |
|---|
| 315 |
n,l = sl[s]; n = 1+float(n-mins)/(maxs-mins) |
|---|
| 316 |
gr += '"src.%s" [label = "%s", shape=box, fillcolor="#FF0000", style=filled, fixedsize=1, height=%.2f,width=%.2f];\n' % (`s`,`s`,n,n) |
|---|
| 317 |
gr += "# event nodes\n" |
|---|
| 318 |
for e in el: |
|---|
| 319 |
n,l = el[e]; n = n = 1+float(n-mine)/(maxe-mine) |
|---|
| 320 |
gr += '"evt.%s" [label = "%s", shape=circle, fillcolor="#00FFFF", style=filled, fixedsize=1, height=%.2f, width=%.2f];\n' % (`e`,`e`,n,n) |
|---|
| 321 |
for d in dl: |
|---|
| 322 |
n = dl[d]; n = n = 1+float(n-mind)/(maxd-mind) |
|---|
| 323 |
gr += '"dst.%s" [label = "%s", shape=triangle, fillcolor="#0000ff", style=filled, fixedsize=1, height=%.2f, width=%.2f];\n' % (`d`,`d`,n,n) |
|---|
| 324 |
|
|---|
| 325 |
gr += "###\n" |
|---|
| 326 |
for s in sl: |
|---|
| 327 |
n,l = sl[s] |
|---|
| 328 |
for e in l: |
|---|
| 329 |
gr += ' "src.%s" -> "evt.%s";\n' % (`s`,`e`) |
|---|
| 330 |
for e in el: |
|---|
| 331 |
n,l = el[e] |
|---|
| 332 |
for d in l: |
|---|
| 333 |
gr += ' "evt.%s" -> "dst.%s";\n' % (`e`,`d`) |
|---|
| 334 |
|
|---|
| 335 |
gr += "}" |
|---|
| 336 |
open("/tmp/aze","w").write(gr) |
|---|
| 337 |
return do_graph(gr, **kargs) |
|---|
| 338 |
|
|---|
| 339 |
|
|---|
| 340 |
def _dump_document(self, **kargs): |
|---|
| 341 |
import pyx |
|---|
| 342 |
d = pyx.document.document() |
|---|
| 343 |
l = len(self.res) |
|---|
| 344 |
for i in range(len(self.res)): |
|---|
| 345 |
elt = self.res[i] |
|---|
| 346 |
c = self._elt2pkt(elt).canvas_dump(**kargs) |
|---|
| 347 |
cbb = c.bbox() |
|---|
| 348 |
c.text(cbb.left(),cbb.top()+1,r"\font\cmssfont=cmss12\cmssfont{Frame %i/%i}" % (i,l),[pyx.text.size.LARGE]) |
|---|
| 349 |
if conf.verb >= 2: |
|---|
| 350 |
os.write(1,".") |
|---|
| 351 |
d.append(pyx.document.page(c, paperformat=pyx.document.paperformat.A4, |
|---|
| 352 |
margin=1*pyx.unit.t_cm, |
|---|
| 353 |
fittosize=1)) |
|---|
| 354 |
return d |
|---|
| 355 |
|
|---|
| 356 |
|
|---|
| 357 |
|
|---|
| 358 |
def psdump(self, filename = None, **kargs): |
|---|
| 359 |
"""Creates a multipage poscript file with a psdump of every packet |
|---|
| 360 |
filename: name of the file to write to. If empty, a temporary file is used and |
|---|
| 361 |
conf.prog.psreader is called""" |
|---|
| 362 |
d = self._dump_document(**kargs) |
|---|
| 363 |
if filename is None: |
|---|
| 364 |
filename = "/tmp/scapy.psd.%i" % os.getpid() |
|---|
| 365 |
d.writePSfile(filename) |
|---|
| 366 |
os.system("%s %s.ps &" % (conf.prog.psreader,filename)) |
|---|
| 367 |
else: |
|---|
| 368 |
d.writePSfile(filename) |
|---|
| 369 |
print |
|---|
| 370 |
|
|---|
| 371 |
def pdfdump(self, filename = None, **kargs): |
|---|
| 372 |
"""Creates a PDF file with a psdump of every packet |
|---|
| 373 |
filename: name of the file to write to. If empty, a temporary file is used and |
|---|
| 374 |
conf.prog.pdfreader is called""" |
|---|
| 375 |
d = self._dump_document(**kargs) |
|---|
| 376 |
if filename is None: |
|---|
| 377 |
filename = "/tmp/scapy.psd.%i" % os.getpid() |
|---|
| 378 |
d.writePDFfile(filename) |
|---|
| 379 |
os.system("%s %s.pdf &" % (conf.prog.pdfreader,filename)) |
|---|
| 380 |
else: |
|---|
| 381 |
d.writePDFfile(filename) |
|---|
| 382 |
print |
|---|
| 383 |
|
|---|
| 384 |
def sr(self,multi=0): |
|---|
| 385 |
"""sr([multi=1]) -> (SndRcvList, PacketList) |
|---|
| 386 |
Matches packets in the list and return ( (matched couples), (unmatched packets) )""" |
|---|
| 387 |
remain = self.res[:] |
|---|
| 388 |
sr = [] |
|---|
| 389 |
i = 0 |
|---|
| 390 |
while i < len(remain): |
|---|
| 391 |
s = remain[i] |
|---|
| 392 |
j = i |
|---|
| 393 |
while j < len(remain)-1: |
|---|
| 394 |
j += 1 |
|---|
| 395 |
r = remain[j] |
|---|
| 396 |
if r.answers(s): |
|---|
| 397 |
sr.append((s,r)) |
|---|
| 398 |
if multi: |
|---|
| 399 |
remain[i]._answered=1 |
|---|
| 400 |
remain[j]._answered=2 |
|---|
| 401 |
continue |
|---|
| 402 |
del(remain[j]) |
|---|
| 403 |
del(remain[i]) |
|---|
| 404 |
i -= 1 |
|---|
| 405 |
break |
|---|
| 406 |
i += 1 |
|---|
| 407 |
if multi: |
|---|
| 408 |
remain = filter(lambda x:not hasattr(x,"_answered"), remain) |
|---|
| 409 |
return SndRcvList(sr),PacketList(remain) |
|---|
| 410 |
|
|---|
| 411 |
|
|---|
| 412 |
|
|---|
| 413 |
class SndRcvList(PacketList): |
|---|
| 414 |
def __init__(self, res=None, name="Results", stats=None): |
|---|
| 415 |
PacketList.__init__(self, res, name, stats) |
|---|
| 416 |
def _elt2pkt(self, elt): |
|---|
| 417 |
return elt[1] |
|---|
| 418 |
def _elt2sum(self, elt): |
|---|
| 419 |
return "%s ==> %s" % (elt[0].summary(),elt[1].summary()) |
|---|
| 420 |
|
|---|
| 421 |
|
|---|
| 422 |
|
|---|
| 423 |
|
|---|
| 424 |
|
|---|
| 425 |
|
|---|
| 426 |
|
|---|