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.
@@ -0,0 +1,145 @@
1
+ # encoding: ASCII-8BIT
2
+ require 'rainbow'
3
+
4
+ module Pwnlib
5
+ module Util
6
+ # Method for output a pretty hexdump.
7
+ # Since this may be used in log module, to avoid cyclic dependency, it is put in a separate module as {Fiddling}
8
+ # See {ClassMethods} for method details.
9
+ # @todo Control coloring by context?
10
+ # @example Call by specifying full module path.
11
+ # require 'pwnlib/util/hexdump'
12
+ # Pwnlib::Util::HexDump.hexdump('217')
13
+ # #=> "00000000 32 31 37 │217│\n00000003"
14
+ # @example require 'pwn' and have all methods.
15
+ # require 'pwn'
16
+ # hexdump('217')
17
+ # #=> "00000000 32 31 37 │217│\n00000003"
18
+ module HexDump
19
+ # @note Do not create and call instance method here. Instead, call module method on {HexDump}.
20
+ module ClassMethods
21
+ MARKER = "\u2502".freeze
22
+ HIGHLIGHT_STYLE = ->(s) { Rainbow(s).bg(:red) }
23
+ DEFAULT_STYLE = {
24
+ 0x00 => ->(s) { Rainbow(s).red },
25
+ 0x0a => ->(s) { Rainbow(s).red },
26
+ 0xff => ->(s) { Rainbow(s).green },
27
+ marker: ->(s) { Rainbow(s).dimgray },
28
+ printable: ->(s) { s },
29
+ unprintable: ->(s) { Rainbow(s).dimgray }
30
+ }.freeze
31
+
32
+ # @!macro [new] hexdump_options
33
+ # Color is provided using +rainbow+ gem and only when output is a tty.
34
+ # To force enable/disable coloring, call <tt>Rainbow.enabled = true / false</tt>.
35
+ # @param [Integer] width
36
+ # The max number of characters per line.
37
+ # @param [Boolean] skip
38
+ # Whether repeated lines should be replaced by a +"*"+.
39
+ # @param [Integer] offset
40
+ # Offset of the first byte to print in the left column.
41
+ # @param [Hash{Integer, Symbol => Proc}] style
42
+ # Color scheme to use.
43
+ #
44
+ # Possible keys are:
45
+ # * <tt>0x00..0xFF</tt>, for specified byte.
46
+ # * +:marker+, for the separator in right column.
47
+ # * +:printable+, for printable bytes that don't have style specified.
48
+ # * +:unprintable+, for unprintable bytes that don't have style specified.
49
+ # The proc is called with a single argument, the string to be formatted.
50
+ # @param [String] highlight
51
+ # Convenient argument to highlight (red background) some bytes in style.
52
+
53
+ # Yields lines of a hexdump-dump of a string. Unless you have massive
54
+ # amounts of data you probably want to use {#hexdump}.
55
+ # Returns an Enumerator if no block given.
56
+ #
57
+ # @param [#read] io
58
+ # The object to be dumped.
59
+ # @!macro hexdump_options
60
+ # @return [Enumerator<String>]
61
+ # The resulting hexdump, line by line.
62
+ def hexdump_iter(io, width: 16, skip: true, offset: 0, style: {}, highlight: '')
63
+ Enumerator.new do |y|
64
+ style = DEFAULT_STYLE.merge(style)
65
+ highlight.bytes.each { |b| style[b] = HIGHLIGHT_STYLE }
66
+ (0..255).each do |b|
67
+ next if style.include?(b)
68
+ style[b] = (b.chr =~ /[[:print:]]/ ? style[:printable] : style[:unprintable])
69
+ end
70
+
71
+ styled_bytes = (0..255).map do |b|
72
+ left_hex = format('%02x', b)
73
+ c = b.chr
74
+ right_char = (c =~ /[[:print:]]/ ? c : "\u00b7")
75
+ [style[b].call(left_hex), style[b].call(right_char)]
76
+ end
77
+
78
+ marker = style[:marker].call(MARKER)
79
+ spacer = ' '
80
+
81
+ byte_index = offset
82
+ skipping = false
83
+ last_chunk = ''
84
+
85
+ loop do
86
+ # We assume that chunk is in ASCII-8BIT encoding.
87
+ chunk = io.read(width)
88
+ break unless chunk
89
+ chunk_bytes = chunk.bytes
90
+ start_byte_index = byte_index
91
+ byte_index += chunk_bytes.size
92
+
93
+ # Yield * once for repeated lines.
94
+ if skip && last_chunk == chunk
95
+ y << '*' unless skipping
96
+ skipping = true
97
+ next
98
+ end
99
+ skipping = false
100
+ last_chunk = chunk
101
+
102
+ hex_bytes = ''
103
+ printable = ''
104
+ chunk_bytes.each_with_index do |b, i|
105
+ left_hex, right_char = styled_bytes[b]
106
+ hex_bytes << left_hex
107
+ printable << right_char
108
+ if i % 4 == 3 && i != chunk_bytes.size - 1
109
+ hex_bytes << spacer
110
+ printable << marker
111
+ end
112
+ hex_bytes << ' '
113
+ end
114
+
115
+ if chunk_bytes.size < width
116
+ padded_hex_length = 3 * width + (width - 1) / 4
117
+ hex_length = 3 * chunk_bytes.size + (chunk_bytes.size - 1) / 4
118
+ hex_bytes << ' ' * (padded_hex_length - hex_length)
119
+ end
120
+
121
+ y << format("%08x %s #{MARKER}%s#{MARKER}", start_byte_index, hex_bytes, printable)
122
+ end
123
+
124
+ y << format('%08x', byte_index)
125
+ end
126
+ end
127
+
128
+ # Returns a hexdump-dump of a string.
129
+ #
130
+ # @param [String] str
131
+ # The string to be hexdumped.
132
+ # @!macro hexdump_options
133
+ # @return [String]
134
+ # The resulting hexdump.
135
+ def hexdump(str, width: 16, skip: true, offset: 0, style: {}, highlight: '')
136
+ hexdump_iter(StringIO.new(str),
137
+ width: width, skip: skip, offset: offset, style: style,
138
+ highlight: highlight).to_a.join("\n")
139
+ end
140
+ end
141
+
142
+ extend ClassMethods
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,284 @@
1
+ # encoding: ASCII-8BIT
2
+
3
+ require 'pwnlib/context'
4
+
5
+ module Pwnlib
6
+ module Util
7
+ # Methods for integer pack/unpack.
8
+ # See {ClassMethods} for method details.
9
+ # @example Call by specifying full module path.
10
+ # require 'pwnlib/util/packing'
11
+ # Pwnlib::Util::Packing.p8(217) #=> "\xD9"
12
+ # @example require 'pwn' and have all methods.
13
+ # require 'pwn'
14
+ # p8(217) #=> "\xD9"
15
+ module Packing
16
+ # @note Do not create and call instance method here. Instead, call module method on {Packing}.
17
+ module ClassMethods
18
+ # Pack arbitrary-sized integer.
19
+ #
20
+ # +bits+ indicates number of bits that packed output should use.
21
+ # The output would be padded to be byte-aligned.
22
+ #
23
+ # +bits+ can also be the string 'all',
24
+ # indicating that the result should be long enough to hold all bits of the number.
25
+ #
26
+ # @param [Integer] number
27
+ # Number to be packed.
28
+ # @param [Integer, 'all'] bits
29
+ # Number of bits the output should have,
30
+ # or +'all'+ for all bits.
31
+ # Default to +context.bits+.
32
+ # @param [String] endian
33
+ # Endian to use when packing.
34
+ # Can be any value accepted by context (See {Context::ContextType}).
35
+ # Default to +context.endian+.
36
+ # @param [Boolean, String] signed
37
+ # Whether the input number should be considered signed when +bits+ is +'all'+.
38
+ # Can be any value accepted by context (See {Context::ContextType}).
39
+ # Default to +context.signed+.
40
+ # @return [String]
41
+ # The packed string.
42
+ # @raise [ArgumentError]
43
+ # When input integer can't be packed into the size specified by +bits+ and +signed+.
44
+ #
45
+ # @example
46
+ # pack(0x34, bits: 8) #=> '4'
47
+ # pack(0x1234, bits: 16, endian: 'little') #=> "4\x12"
48
+ # pack(0xFF, bits: 'all', signed: false) #=> "\xFF"
49
+ # pack(0xFF, bits: 'all', endian: 'big', signed: true) #=> "\x00\xFF"
50
+ def pack(number, bits: nil, endian: nil, signed: nil)
51
+ if bits == 'all'
52
+ bits = nil
53
+ is_all = true
54
+ else
55
+ is_all = false
56
+ end
57
+
58
+ context.local(bits: bits, endian: endian, signed: signed) do
59
+ bits = context.bits
60
+ endian = context.endian
61
+ signed = context.signed
62
+
63
+ # Verify that bits make sense
64
+ if signed
65
+ bits = (number.bit_length | 7) + 1 if is_all
66
+
67
+ limit = 1 << (bits - 1)
68
+ unless -limit <= number && number < limit
69
+ raise ArgumentError, "signed number=#{number} does not fit within bits=#{bits}"
70
+ end
71
+ else
72
+ if is_all
73
+ if number < 0
74
+ raise ArgumentError, "Can't pack negative number with bits='all' and signed=false"
75
+ end
76
+ bits = number.zero? ? 8 : ((number.bit_length - 1) | 7) + 1
77
+ end
78
+
79
+ limit = 1 << bits
80
+ unless 0 <= number && number < limit
81
+ raise ArgumentError, "unsigned number=#{number} does not fit within bits=#{bits}"
82
+ end
83
+ end
84
+
85
+ number &= (1 << bits) - 1
86
+ bytes = (bits + 7) / 8
87
+
88
+ out = []
89
+ bytes.times do
90
+ out << (number & 0xFF)
91
+ number >>= 8
92
+ end
93
+ out = out.pack('C*')
94
+
95
+ endian == 'little' ? out : out.reverse
96
+ end
97
+ end
98
+
99
+ # Unpack packed string back to integer.
100
+ #
101
+ # +bits+ indicates number of bits that should be used from input data.
102
+ #
103
+ # +bits+ can also be the string +'all'+,
104
+ # indicating that all bytes from input should be used.
105
+ #
106
+ # @param [String] data
107
+ # String to be unpacked.
108
+ # @param [Integer, 'all'] bits
109
+ # Number of bits to be used from +data+,
110
+ # or +'all'+ for all bits.
111
+ # Default to +context.bits+
112
+ # @param [String] endian
113
+ # Endian to use when unpacking.
114
+ # Can be any value accepted by context (See {Context::ContextType}).
115
+ # Default to +context.endian+.
116
+ # @param [Boolean, String] signed
117
+ # Whether the output number should be signed.
118
+ # Can be any value accepted by context (See {Context::ContextType}).
119
+ # Default to +context.signed+.
120
+ # @return [Integer]
121
+ # The unpacked number.
122
+ # @raise [ArgumentError]
123
+ # When +data.size+ doesn't match +bits+.
124
+ #
125
+ # @example
126
+ # unpack('4', bits: 8) #=> 52
127
+ # unpack("\x3F", bits: 6, signed: false) #=> 63
128
+ # unpack("\x3F", bits: 6, signed: true) #=> -1
129
+ def unpack(data, bits: nil, endian: nil, signed: nil)
130
+ bits = data.size * 8 if bits == 'all'
131
+
132
+ context.local(bits: bits, endian: endian, signed: signed) do
133
+ bits = context.bits
134
+ endian = context.endian
135
+ signed = context.signed
136
+ bytes = (bits + 7) / 8
137
+
138
+ unless data.size == bytes
139
+ raise ArgumentError, "data.size=#{data.size} does not match with bits=#{bits}"
140
+ end
141
+
142
+ data = data.reverse if endian == 'little'
143
+ data = data.unpack('C*')
144
+ number = 0
145
+ data.each { |c| number = (number << 8) + c }
146
+ number &= (1 << bits) - 1
147
+ if signed
148
+ signbit = number & (1 << (bits - 1))
149
+ number -= 2 * signbit
150
+ end
151
+ number
152
+ end
153
+ end
154
+
155
+ # Split the data into chunks, and unpack each element.
156
+ #
157
+ # +bits+ indicates how many bits each chunk should be.
158
+ # This should be a multiple of 8,
159
+ # and size of +data+ should be divisible by +bits / 8+.
160
+ #
161
+ # +bits+ can also be the string +'all'+,
162
+ # indicating that all bytes from input would be used,
163
+ # and result would be an array with one element.
164
+ #
165
+ # @param [String] data
166
+ # String to be unpacked.
167
+ # @param [Integer, 'all'] bits
168
+ # Number of bits to be used for each chunk of +data+,
169
+ # or +'all'+ for all bits.
170
+ # Default to +context.bits+
171
+ # @param [String] endian
172
+ # Endian to use when unpacking.
173
+ # Can be any value accepted by context (See {Context::ContextType}).
174
+ # Default to +context.endian+.
175
+ # @param [Boolean, String] signed
176
+ # Whether the output number should be signed.
177
+ # Can be any value accepted by context (See {Context::ContextType}).
178
+ # Default to +context.signed+.
179
+ # @return [Array<Integer>]
180
+ # The unpacked numbers.
181
+ # @raise [ArgumentError]
182
+ # When +bits+ isn't divisible by 8 or +data.size+ isn't divisible by +bits / 8+.
183
+ #
184
+ # @todo
185
+ # Support +bits+ not divisible by 8, if ever found this useful.
186
+ #
187
+ # @example
188
+ # unpack_many('haha', bits: 8) #=> [104, 97, 104, 97]
189
+ # unpack_many("\xFF\x01\x02\xFE", bits: 16, endian: 'little', signed: true) #=> [511, -510]
190
+ # unpack_many("\xFF\x01\x02\xFE", bits: 16, endian: 'big', signed: false) #=> [65281, 766]
191
+ def unpack_many(data, bits: nil, endian: nil, signed: nil)
192
+ return [unpack(data, bits: bits, endian: endian, signed: signed)] if bits == 'all'
193
+
194
+ context.local(bits: bits, endian: endian, signed: signed) do
195
+ bits = context.bits
196
+
197
+ # TODO(Darkpi): Support this if found useful.
198
+ raise ArgumentError, 'bits must be a multiple of 8' if bits % 8 != 0
199
+
200
+ bytes = bits / 8
201
+
202
+ if data.size % bytes != 0
203
+ raise ArgumentError, "data.size=#{data.size} must be a multiple of bytes=#{bytes}"
204
+ end
205
+ ret = []
206
+ (data.size / bytes).times do |idx|
207
+ x1 = idx * bytes
208
+ x2 = x1 + bytes
209
+ # We already set local context, no need to pass things down.
210
+ ret << unpack(data[x1...x2], bits: bits)
211
+ end
212
+ ret
213
+ end
214
+ end
215
+
216
+ { 8 => 'c', 16 => 's', 32 => 'l', 64 => 'q' }.each do |sz, ch|
217
+ define_method("p#{sz}") do |num, **kwargs|
218
+ context.local(**kwargs) do
219
+ c = context.signed ? ch : ch.upcase
220
+ arrow = context.endian == 'little' ? '<' : '>'
221
+ arrow = '' if sz == 8
222
+ [num].pack("#{c}#{arrow}")
223
+ end
224
+ end
225
+
226
+ define_method("u#{sz}") do |data, **kwargs|
227
+ context.local(**kwargs) do
228
+ c = context.signed ? ch : ch.upcase
229
+ arrow = context.endian == 'little' ? '<' : '>'
230
+ arrow = '' if sz == 8
231
+ data.unpack("#{c}#{arrow}")[0]
232
+ end
233
+ end
234
+ end
235
+
236
+ # TODO(Darkpi): pwntools-python have this for performance reason,
237
+ # but current implementation doesn't offer that much performance
238
+ # relative to what pwntools-python do. Maybe we should initialize
239
+ # those functions (p8lu, ...) like in pwntools-python?
240
+ [%w(pack p), %w(unpack u)].each do |v1, v2|
241
+ define_method("make_#{v1}er") do |bits: nil, endian: nil, signed: nil|
242
+ context.local(bits: bits, endian: endian, signed: signed) do
243
+ bits = context.bits
244
+ endian = context.endian
245
+ signed = context.signed
246
+
247
+ if [8, 16, 32, 64].include?(bits)
248
+ ->(num) { public_send("#{v2}#{bits}", num, endian: endian, signed: signed) }
249
+ else
250
+ ->(num) { public_send(v1, num, bits: bits, endian: endian, signed: signed) }
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ def flat(*args, **kwargs, &preprocessor)
257
+ ret = []
258
+ p = make_packer(**kwargs)
259
+ args.each do |it|
260
+ if preprocessor && !it.is_a?(Array)
261
+ r = preprocessor[it]
262
+ it = r unless r.nil?
263
+ end
264
+ v = case it
265
+ when Array then flat(*it, **kwargs, &preprocessor)
266
+ when Integer then p[it]
267
+ when String then it.force_encoding('ASCII-8BIT')
268
+ else
269
+ raise ArgumentError, "flat does not support values of type #{it.class}"
270
+ end
271
+ ret << v
272
+ end
273
+ ret.join
274
+ end
275
+
276
+ # TODO(Darkpi): fit! Which seems super useful.
277
+
278
+ include ::Pwnlib::Context
279
+ end
280
+
281
+ extend ClassMethods
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: ASCII-8BIT
2
+
3
+ module Pwnlib
4
+ VERSION = '0.1.0'.freeze
5
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: ASCII-8BIT
2
+
3
+ require 'test_helper'
4
+ require 'pwnlib/constants/constant'
5
+
6
+ class ConstantTest < MiniTest::Test
7
+ Constant = ::Pwnlib::Constants::Constant
8
+
9
+ def test_methods
10
+ a1 = Constant.new('a', 1)
11
+ assert_equal(1, a1.to_i)
12
+ assert_equal(3, a1 | 2)
13
+ assert_operator(a1, :==, 1)
14
+ # test coerce
15
+ assert_operator(1, :==, a1)
16
+ assert_operator(a1, :==, Constant.new('b', 1))
17
+ refute_operator(a1, :==, Constant.new('a', 3))
18
+ assert_equal(3, a1 | Constant.new('a', 2))
19
+ assert_equal(3, 2 | a1)
20
+ assert_equal(1, 2 - a1)
21
+
22
+ assert_equal(a1.method(:chr).call, "\x01")
23
+ end
24
+ end