pwntools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +49 -0
- data/Rakefile +40 -0
- data/lib/pwn.rb +24 -0
- data/lib/pwnlib/constants/constant.rb +45 -0
- data/lib/pwnlib/constants/constants.rb +82 -0
- data/lib/pwnlib/constants/linux/amd64.rb +1558 -0
- data/lib/pwnlib/constants/linux/i386.rb +1340 -0
- data/lib/pwnlib/context.rb +220 -0
- data/lib/pwnlib/dynelf.rb +110 -0
- data/lib/pwnlib/ext/array.rb +21 -0
- data/lib/pwnlib/ext/helper.rb +21 -0
- data/lib/pwnlib/ext/integer.rb +21 -0
- data/lib/pwnlib/ext/string.rb +23 -0
- data/lib/pwnlib/memleak.rb +61 -0
- data/lib/pwnlib/pwn.rb +26 -0
- data/lib/pwnlib/reg_sort.rb +147 -0
- data/lib/pwnlib/util/cyclic.rb +120 -0
- data/lib/pwnlib/util/fiddling.rb +262 -0
- data/lib/pwnlib/util/hexdump.rb +145 -0
- data/lib/pwnlib/util/packing.rb +284 -0
- data/lib/pwnlib/version.rb +5 -0
- data/test/constants/constant_test.rb +24 -0
- data/test/constants/constants_test.rb +31 -0
- data/test/context_test.rb +131 -0
- data/test/data/victim.c +8 -0
- data/test/data/victim32 +0 -0
- data/test/data/victim64 +0 -0
- data/test/dynelf_test.rb +48 -0
- data/test/ext_test.rb +26 -0
- data/test/files/use_pwn.rb +34 -0
- data/test/files/use_pwnlib.rb +19 -0
- data/test/full_file_test.rb +16 -0
- data/test/memleak_test.rb +72 -0
- data/test/reg_sort_test.rb +41 -0
- data/test/test_helper.rb +13 -0
- data/test/util/cyclic_test.rb +36 -0
- data/test/util/fiddling_test.rb +106 -0
- data/test/util/hexdump_test.rb +179 -0
- data/test/util/packing_test.rb +168 -0
- metadata +231 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
# TODO(Darkpi): Check if there should be special care for threading.
|
6
|
+
|
7
|
+
module Pwnlib
|
8
|
+
# Context module, store some platform-dependent informations.
|
9
|
+
module Context
|
10
|
+
# The type for context. User should never need to initialize one by themself.
|
11
|
+
class ContextType
|
12
|
+
DEFAULT = {
|
13
|
+
arch: 'i386',
|
14
|
+
bits: 32,
|
15
|
+
endian: 'little',
|
16
|
+
log_level: Logger::INFO,
|
17
|
+
newline: "\n",
|
18
|
+
os: 'linux',
|
19
|
+
signed: false,
|
20
|
+
timeout: Float::INFINITY
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
OSES = %w(linux freebsd windows).sort
|
24
|
+
|
25
|
+
BIG_32 = { endian: 'big', bits: 32 }.freeze
|
26
|
+
BIG_64 = { endian: 'big', bits: 64 }.freeze
|
27
|
+
LITTLE_8 = { endian: 'little', bits: 8 }.freeze
|
28
|
+
LITTLE_16 = { endian: 'little', bits: 16 }.freeze
|
29
|
+
LITTLE_32 = { endian: 'little', bits: 32 }.freeze
|
30
|
+
LITTLE_64 = { endian: 'little', bits: 64 }.freeze
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def longest(d)
|
34
|
+
Hash[d.sort_by { |k, _v| k.size }.reverse]
|
35
|
+
end
|
36
|
+
private :longest
|
37
|
+
end
|
38
|
+
|
39
|
+
ARCHS = longest(
|
40
|
+
'aarch64' => LITTLE_64,
|
41
|
+
'alpha' => LITTLE_64,
|
42
|
+
'avr' => LITTLE_8,
|
43
|
+
'amd64' => LITTLE_64,
|
44
|
+
'arm' => LITTLE_32,
|
45
|
+
'cris' => LITTLE_32,
|
46
|
+
'i386' => LITTLE_32,
|
47
|
+
'ia64' => BIG_64,
|
48
|
+
'm68k' => BIG_32,
|
49
|
+
'mips' => LITTLE_32,
|
50
|
+
'mips64' => LITTLE_64,
|
51
|
+
'msp430' => LITTLE_16,
|
52
|
+
'powerpc' => BIG_32,
|
53
|
+
'powerpc64' => BIG_64,
|
54
|
+
's390' => BIG_32,
|
55
|
+
'sparc' => BIG_32,
|
56
|
+
'sparc64' => BIG_64,
|
57
|
+
'thumb' => LITTLE_32,
|
58
|
+
'vax' => LITTLE_32
|
59
|
+
)
|
60
|
+
|
61
|
+
ENDIANNESSES = longest(
|
62
|
+
'be' => 'big',
|
63
|
+
'eb' => 'big',
|
64
|
+
'big' => 'big',
|
65
|
+
'le' => 'little',
|
66
|
+
'el' => 'little',
|
67
|
+
'little' => 'little'
|
68
|
+
)
|
69
|
+
|
70
|
+
SIGNEDNESSES = {
|
71
|
+
'unsigned' => false,
|
72
|
+
'no' => false,
|
73
|
+
'yes' => true,
|
74
|
+
'signed' => true
|
75
|
+
}.freeze
|
76
|
+
|
77
|
+
VALID_SIGNED = SIGNEDNESSES.keys
|
78
|
+
|
79
|
+
# XXX(Darkpi): Should we just hard-coded all levels here,
|
80
|
+
# or should we use Logger#const_defined?
|
81
|
+
# (This would include constant SEV_LEVEL, and exclude UNKNOWN)?
|
82
|
+
LOG_LEVELS = %w(DEBUG INFO WARN ERROR FATAL UNKNOWN).freeze
|
83
|
+
|
84
|
+
def initialize(**kwargs)
|
85
|
+
@attrs = DEFAULT.dup
|
86
|
+
update(**kwargs)
|
87
|
+
end
|
88
|
+
|
89
|
+
def update(**kwargs)
|
90
|
+
kwargs.each do |k, v|
|
91
|
+
next if v.nil?
|
92
|
+
public_send("#{k}=", v)
|
93
|
+
end
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
alias [] update
|
98
|
+
alias call update
|
99
|
+
|
100
|
+
def to_s
|
101
|
+
vals = @attrs.map { |k, v| "#{k} = #{v.inspect}" }
|
102
|
+
"#{self.class}(#{vals.join(', ')})"
|
103
|
+
end
|
104
|
+
|
105
|
+
# This would return what the block return.
|
106
|
+
def local(**kwargs)
|
107
|
+
raise ArgumentError, "Need a block for #{self.class}##{__callee__}" unless block_given?
|
108
|
+
# XXX(Darkpi): improve performance for this if this is too slow, since we use this in many
|
109
|
+
# places that has argument endian / signed / ...
|
110
|
+
old_attrs = @attrs.dup
|
111
|
+
begin
|
112
|
+
update(**kwargs)
|
113
|
+
yield
|
114
|
+
ensure
|
115
|
+
@attrs = old_attrs
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def clear
|
120
|
+
@attrs = DEFAULT.dup
|
121
|
+
end
|
122
|
+
|
123
|
+
# Getters here.
|
124
|
+
DEFAULT.each_key do |k|
|
125
|
+
define_method(k) { @attrs[k] }
|
126
|
+
end
|
127
|
+
|
128
|
+
def newline=(newline)
|
129
|
+
@attrs[:newline] = newline
|
130
|
+
end
|
131
|
+
|
132
|
+
# TODO(Darkpi): Timeout module.
|
133
|
+
def timeout=(timeout)
|
134
|
+
@attrs[:timeout] = timeout
|
135
|
+
end
|
136
|
+
|
137
|
+
# Difference from Python pwntools:
|
138
|
+
# We always change +bits+ and +endian+ field whether user have already changed them.
|
139
|
+
def arch=(arch)
|
140
|
+
arch = arch.downcase.gsub(/[[:punct:]]/, '')
|
141
|
+
defaults = ARCHS[arch]
|
142
|
+
raise ArgumentError, "arch must be one of #{ARCHS.keys.sort.inspect}" unless defaults
|
143
|
+
defaults.each { |k, v| @attrs[k] = v }
|
144
|
+
@attrs[:arch] = arch
|
145
|
+
end
|
146
|
+
|
147
|
+
def bits=(bits)
|
148
|
+
raise ArgumentError, "bits must be > 0 (#{bits} given)" unless bits > 0
|
149
|
+
@attrs[:bits] = bits
|
150
|
+
end
|
151
|
+
|
152
|
+
def bytes
|
153
|
+
bits / 8
|
154
|
+
end
|
155
|
+
|
156
|
+
def bytes=(bytes)
|
157
|
+
self.bits = bytes * 8
|
158
|
+
end
|
159
|
+
|
160
|
+
def endian=(endian)
|
161
|
+
endian = ENDIANNESSES[endian.downcase]
|
162
|
+
raise ArgumentError, "endian must be one of #{ENDIANNESSES.sort.inspect}" if endian.nil?
|
163
|
+
@attrs[:endian] = endian
|
164
|
+
end
|
165
|
+
|
166
|
+
def log_level=(value)
|
167
|
+
log_level = nil
|
168
|
+
case value
|
169
|
+
when String
|
170
|
+
value = value.upcase
|
171
|
+
log_level = Logger.const_get(value) if LOG_LEVELS.include?(value)
|
172
|
+
when Integer
|
173
|
+
log_level = value
|
174
|
+
end
|
175
|
+
raise ArgumentError, "log_level must be an integer or one of #{LOG_LEVELS.inspect}" unless log_level
|
176
|
+
@attrs[:log_level] = log_level
|
177
|
+
end
|
178
|
+
|
179
|
+
def os=(os)
|
180
|
+
os = os.downcase
|
181
|
+
raise ArgumentError, "os must be one of #{OSES.sort.inspect}" unless OSES.include?(os)
|
182
|
+
@attrs[:os] = os
|
183
|
+
end
|
184
|
+
|
185
|
+
def signed=(value)
|
186
|
+
signed = nil
|
187
|
+
case value
|
188
|
+
when String
|
189
|
+
signed = SIGNEDNESSES[value.downcase]
|
190
|
+
when true, false
|
191
|
+
signed = value
|
192
|
+
end
|
193
|
+
if signed.nil?
|
194
|
+
raise ArgumentError, "signed must be boolean or one of #{SIGNEDNESSES.keys.sort.inspect}"
|
195
|
+
end
|
196
|
+
@attrs[:signed] = signed
|
197
|
+
end
|
198
|
+
|
199
|
+
# TODO(Darkpi): #binary when we can read ELF.
|
200
|
+
end
|
201
|
+
|
202
|
+
@context = ContextType.new
|
203
|
+
|
204
|
+
class << self
|
205
|
+
attr_reader :context
|
206
|
+
end
|
207
|
+
|
208
|
+
# For include.
|
209
|
+
# @!visibility private
|
210
|
+
def context
|
211
|
+
::Pwnlib::Context.context
|
212
|
+
end
|
213
|
+
|
214
|
+
# @!visibility private
|
215
|
+
def self.included(base)
|
216
|
+
# XXX(Darkpi): Should we do this?
|
217
|
+
base.__send__(:private, :context)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/context'
|
4
|
+
require 'pwnlib/memleak'
|
5
|
+
require 'pwnlib/util/packing'
|
6
|
+
|
7
|
+
# TODO(hh): Use ELF datatype instead of magic offset
|
8
|
+
|
9
|
+
module Pwnlib
|
10
|
+
# DynELF class, resolve symbols in loaded, dynamically-linked ELF binaries.
|
11
|
+
# Given a function which can leak data at an arbitrary address,
|
12
|
+
# any symbol in any loaded library can be resolved.
|
13
|
+
class DynELF
|
14
|
+
PT_DYNAMIC = 2
|
15
|
+
DT_GNU_HASH = 0x6ffffef5
|
16
|
+
DT_HASH = 4
|
17
|
+
DT_STRTAB = 5
|
18
|
+
DT_SYMTAB = 6
|
19
|
+
|
20
|
+
attr_reader :libbase
|
21
|
+
|
22
|
+
def initialize(addr, &block)
|
23
|
+
@leak = ::Pwnlib::MemLeak.new(&block)
|
24
|
+
@libbase = @leak.find_elf_base(addr)
|
25
|
+
@elfclass = { "\x01" => 32, "\x02" => 64 }[@leak.b(@libbase + 4)]
|
26
|
+
@elfword = @elfclass / 8
|
27
|
+
@unp = ->(x) { Util::Packing.public_send({ 32 => :u32, 64 => :u64 }[@elfclass], x) }
|
28
|
+
@dynamic = find_dynamic
|
29
|
+
@hshtab = @strtab = @symtab = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def lookup(symb)
|
33
|
+
@hshtab ||= find_dt(DT_GNU_HASH)
|
34
|
+
@strtab ||= find_dt(DT_STRTAB)
|
35
|
+
@symtab ||= find_dt(DT_SYMTAB)
|
36
|
+
resolve_symbol_gnu(symb)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Function used to generated GNU-style hashes for strings.
|
42
|
+
def gnu_hash(s)
|
43
|
+
s.bytes.reduce(5381) { |acc, elem| (acc * 33 + elem) & 0xffffffff }
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_dynamic
|
47
|
+
e_phoff_offset = { 32 => 28, 64 => 32 }[@elfclass]
|
48
|
+
e_phoff = @libbase + @unp.call(@leak.n(@libbase + e_phoff_offset, @elfword))
|
49
|
+
phdr_size = { 32 => 32, 64 => 56 }[@elfclass]
|
50
|
+
loop do
|
51
|
+
ptype = @leak.d(e_phoff)
|
52
|
+
break if ptype == PT_DYNAMIC
|
53
|
+
e_phoff += phdr_size
|
54
|
+
end
|
55
|
+
offset = { 32 => 8, 64 => 16 }[@elfclass]
|
56
|
+
dyn = @unp.call(@leak.n(e_phoff + offset, @elfword))
|
57
|
+
# Sometimes this is an offset instead of an address
|
58
|
+
dyn += @libbase if (0...0x400000).cover?(dyn)
|
59
|
+
dyn
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_dt(tag)
|
63
|
+
dyn_size = @elfword * 2
|
64
|
+
ptr = @dynamic
|
65
|
+
loop do
|
66
|
+
tmp = @leak.n(ptr, @elfword * 2)
|
67
|
+
d_tag = @unp.call(tmp[0, @elfword])
|
68
|
+
d_addr = @unp.call(tmp[@elfword, @elfword])
|
69
|
+
break if d_tag.zero?
|
70
|
+
return d_addr if tag == d_tag
|
71
|
+
ptr += dyn_size
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def resolve_symbol_gnu(symb)
|
77
|
+
sym_size = { 32 => 16, 64 => 24 }[@elfclass]
|
78
|
+
# Leak GNU_HASH section header
|
79
|
+
nbuckets = @leak.d(@hshtab)
|
80
|
+
symndx = @leak.d(@hshtab + 4)
|
81
|
+
maskwords = @leak.d(@hshtab + 8)
|
82
|
+
|
83
|
+
l_gnu_buckets = @hshtab + 16 + (@elfword * maskwords)
|
84
|
+
l_gnu_chain_zero = l_gnu_buckets + (4 * nbuckets) - (4 * symndx)
|
85
|
+
|
86
|
+
hsh = gnu_hash(symb)
|
87
|
+
bucket = hsh % nbuckets
|
88
|
+
|
89
|
+
i = @leak.d(l_gnu_buckets + bucket * 4)
|
90
|
+
return nil if i.zero?
|
91
|
+
|
92
|
+
hsh2 = 0
|
93
|
+
while (hsh2 & 1).zero?
|
94
|
+
hsh2 = @leak.d(l_gnu_chain_zero + i * 4)
|
95
|
+
if ((hsh ^ hsh2) >> 1).zero?
|
96
|
+
sym = @symtab + sym_size * i
|
97
|
+
st_name = @leak.d(sym)
|
98
|
+
name = @leak.n(@strtab + st_name, symb.length + 1)
|
99
|
+
if name == (symb + "\x00")
|
100
|
+
offset = { 32 => 4, 64 => 8 }[@elfclass]
|
101
|
+
st_value = @unp.call(@leak.n(sym + offset, @elfword))
|
102
|
+
return @libbase + st_value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
i += 1
|
106
|
+
end
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/ext/helper'
|
4
|
+
require 'pwnlib/util/fiddling'
|
5
|
+
require 'pwnlib/util/packing'
|
6
|
+
|
7
|
+
module Pwnlib
|
8
|
+
module Ext
|
9
|
+
module Array
|
10
|
+
# Methods to be mixed into Array.
|
11
|
+
module InstanceMethods
|
12
|
+
extend ::Pwnlib::Ext::Helper
|
13
|
+
|
14
|
+
def_proxy_method ::Pwnlib::Util::Packing, %w(flat)
|
15
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(unbits)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Array.public_send(:include, ::Pwnlib::Ext::Array::InstanceMethods)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
module Pwnlib
|
4
|
+
module Ext
|
5
|
+
# Helper methods for defining extension
|
6
|
+
module Helper
|
7
|
+
def def_proxy_method(mod, *ms, **m2)
|
8
|
+
ms.flatten
|
9
|
+
.map { |x| [x, x] }
|
10
|
+
.concat(m2.to_a)
|
11
|
+
.each do |method, proxy_to|
|
12
|
+
class_eval <<-EOS
|
13
|
+
def #{method}(*args, &block)
|
14
|
+
#{mod}.#{proxy_to}(self, *args, &block)
|
15
|
+
end
|
16
|
+
EOS
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/ext/helper'
|
4
|
+
require 'pwnlib/util/packing'
|
5
|
+
|
6
|
+
module Pwnlib
|
7
|
+
module Ext
|
8
|
+
module Integer
|
9
|
+
# Methods to be mixed into Integer.
|
10
|
+
module InstanceMethods
|
11
|
+
extend ::Pwnlib::Ext::Helper
|
12
|
+
|
13
|
+
def_proxy_method ::Pwnlib::Util::Packing, %w(pack p8 p16 p32 p64)
|
14
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(bits bits_str), bitswap: 'bitswap_int'
|
15
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(hex)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Integer.public_send(:include, ::Pwnlib::Ext::Integer::InstanceMethods)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/ext/helper'
|
4
|
+
require 'pwnlib/util/fiddling'
|
5
|
+
require 'pwnlib/util/packing'
|
6
|
+
|
7
|
+
module Pwnlib
|
8
|
+
module Ext
|
9
|
+
module String
|
10
|
+
# Methods to be mixed into String.
|
11
|
+
module InstanceMethods
|
12
|
+
extend ::Pwnlib::Ext::Helper
|
13
|
+
|
14
|
+
def_proxy_method ::Pwnlib::Util::Packing, %w(unpack unpack_many u8 u16 u32 u64)
|
15
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(
|
16
|
+
enhex unhex urlencode urldecode bits bits_str unbits bitswap b64e b64d
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
::String.public_send(:include, ::Pwnlib::Ext::String::InstanceMethods)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/util/packing'
|
4
|
+
|
5
|
+
module Pwnlib
|
6
|
+
# MemLeak is a caching and heuristic tool for exploiting memory leaks.
|
7
|
+
class MemLeak
|
8
|
+
PAGE_SIZE = 0x1000
|
9
|
+
PAGE_MASK = ~(PAGE_SIZE - 1)
|
10
|
+
|
11
|
+
def initialize(&block)
|
12
|
+
@leak = block
|
13
|
+
@base = nil
|
14
|
+
@cache = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_elf_base(ptr)
|
18
|
+
ptr &= PAGE_MASK
|
19
|
+
loop do
|
20
|
+
return @base = ptr if n(ptr, 4) == "\x7fELF"
|
21
|
+
ptr -= PAGE_SIZE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Call the leaker function on address `addr`.
|
26
|
+
# Store the result to @cache
|
27
|
+
def do_leak(addr)
|
28
|
+
unless @cache.key?(addr)
|
29
|
+
data = @leak.call(addr)
|
30
|
+
data.bytes.each.with_index(addr) { |b, i| @cache[i] = b }
|
31
|
+
end
|
32
|
+
@cache[addr]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Leak `numb` bytes at `addr`.
|
36
|
+
# Returns a string with the leaked bytes.
|
37
|
+
def n(addr, numb)
|
38
|
+
(0...numb).map { |i| do_leak(addr + i) }.pack('C*')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Leak byte at ``((uint8_t*) addr)[ndx]``
|
42
|
+
def b(addr)
|
43
|
+
n(addr, 1)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Leak word at ``((uint16_t*) addr)[ndx]``
|
47
|
+
def w(addr)
|
48
|
+
Util::Packing.u16(n(addr, 2))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Leak dword at ``((uint32_t*) addr)[ndx]``
|
52
|
+
def d(addr)
|
53
|
+
Util::Packing.u32(n(addr, 4))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Leak qword at ``((uint64_t*) addr)[ndx]``
|
57
|
+
def q(addr)
|
58
|
+
Util::Packing.u64(n(addr, 8))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|