Module: Pwnlib::Util::Packing

Included in:
Pwn, Shellcraft::Generators::Helper::Runner
Defined in:
lib/pwnlib/util/packing.rb

Overview

Methods for integer pack/unpack.

Examples:

Call by specifying full module path.

require 'pwnlib/util/packing'
Pwnlib::Util::Packing.p8(217) #=> "\xD9"

require 'pwn' and have all methods.

require 'pwn'
p8(217) #=> "\xD9"

Class Method Summary collapse

Class Method Details

.flat(*args, **kwargs, &preprocessor) ⇒ Object



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/pwnlib/util/packing.rb', line 254

def flat(*args, **kwargs, &preprocessor)
  ret = []
  p = make_packer(**kwargs)
  args.each do |it|
    if preprocessor && !it.is_a?(Array)
      r = preprocessor[it]
      it = r unless r.nil?
    end
    v = case it
        when Array then flat(*it, **kwargs, &preprocessor)
        when Integer then p[it]
        when String then it.dup.force_encoding('ASCII-8BIT') # dup in case 'it' is frozen
        else
          raise ArgumentError, "flat does not support values of type #{it.class}"
        end
    ret << v
  end
  ret.join
end

.pack(number, bits: nil, endian: nil, signed: nil) ⇒ String

Pack arbitrary-sized integer.

bits indicates number of bits that packed output should use. The output would be padded to be byte-aligned.

bits can also be the string ‘all’, indicating that the result should be long enough to hold all bits of the number.

Examples:

pack(0x34, bits: 8) #=> '4'
pack(0x1234, bits: 16, endian: 'little') #=> "4\x12"
pack(0xFF, bits: 'all', signed: false) #=> "\xFF"
pack(0xFF, bits: 'all', endian: 'big', signed: true) #=> "\x00\xFF"

Parameters:

  • number (Integer)

    Number to be packed.

  • bits (Integer, 'all') (defaults to: nil)

    Number of bits the output should have, or 'all' for all bits. Default to context.bits.

  • endian (String) (defaults to: nil)

    Endian to use when packing. Can be any value accepted by context (See Context::ContextType). Default to context.endian.

  • signed (Boolean, String) (defaults to: nil)

    Whether the input number should be considered signed when bits is 'all'. Can be any value accepted by context (See Context::ContextType). Default to context.signed.

Returns:

  • (String)

    The packed string.

Raises:

  • (ArgumentError)

    When input integer can't be packed into the size specified by bits and signed.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/pwnlib/util/packing.rb', line 52

def pack(number, bits: nil, endian: nil, signed: nil)
  if bits == 'all'
    bits = nil
    is_all = true
  else
    is_all = false
  end

  context.local(bits: bits, endian: endian, signed: signed) do
    bits = context.bits
    endian = context.endian
    signed = context.signed

    # Verify that bits make sense
    if signed
      bits = (number.bit_length | 7) + 1 if is_all

      limit = 1 << (bits - 1)
      unless -limit <= number && number < limit
        raise ArgumentError, "signed number=#{number} does not fit within bits=#{bits}"
      end
    else
      if is_all
        raise ArgumentError, "Can't pack negative number with bits='all' and signed=false" if number.negative?

        bits = number.zero? ? 8 : ((number.bit_length - 1) | 7) + 1
      end

      limit = 1 << bits
      unless 0 <= number && number < limit
        raise ArgumentError, "unsigned number=#{number} does not fit within bits=#{bits}"
      end
    end

    number &= (1 << bits) - 1
    bytes = (bits + 7) / 8

    out = []
    bytes.times do
      out << (number & 0xFF)
      number >>= 8
    end
    out = out.pack('C*')

    endian == 'little' ? out : out.reverse
  end
end

.unpack(data, bits: nil, endian: nil, signed: nil) ⇒ Integer

Unpack packed string back to integer.

bits indicates number of bits that should be used from input data.

bits can also be the string ‘all’, indicating that all bytes from input should be used.

Examples:

unpack('4', bits: 8) #=> 52
unpack("\x3F", bits: 6, signed: false) #=> 63
unpack("\x3F", bits: 6, signed: true) #=> -1

Parameters:

  • data (String)

    String to be unpacked.

  • bits (Integer, 'all') (defaults to: nil)

    Number of bits to be used from data, or 'all' for all bits. Default to context.bits

  • endian (String) (defaults to: nil)

    Endian to use when unpacking. Can be any value accepted by context (See Context::ContextType). Default to context.endian.

  • signed (Boolean, String) (defaults to: nil)

    Whether the output number should be signed. Can be any value accepted by context (See Context::ContextType). Default to context.signed.

Returns:

  • (Integer)

    The unpacked number.

Raises:

  • (ArgumentError)

    When data.size doesn't match bits.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/pwnlib/util/packing.rb', line 130

def unpack(data, bits: nil, endian: nil, signed: nil)
  bits = data.size * 8 if bits == 'all'

  context.local(bits: bits, endian: endian, signed: signed) do
    bits = context.bits
    endian = context.endian
    signed = context.signed
    bytes = (bits + 7) / 8

    raise ArgumentError, "data.size=#{data.size} does not match with bits=#{bits}" unless data.size == bytes

    data = data.reverse if endian == 'little'
    data = data.unpack('C*')
    number = 0
    data.each { |c| number = (number << 8) + c }
    number &= (1 << bits) - 1
    if signed
      signbit = number & (1 << (bits - 1))
      number -= 2 * signbit
    end
    number
  end
end

.unpack_many(data, bits: nil, endian: nil, signed: nil) ⇒ Array<Integer>

TODO:

Support bits not divisible by 8, if ever found this useful.

Split the data into chunks, and unpack each element.

bits indicates how many bits each chunk should be. This should be a multiple of 8, and size of data should be divisible by bits / 8.

bits can also be the string ‘all’, indicating that all bytes from input would be used, and result would be an array with one element.

Examples:

unpack_many('haha', bits: 8) #=> [104, 97, 104, 97]
unpack_many("\xFF\x01\x02\xFE", bits: 16, endian: 'little', signed: true) #=> [511, -510]
unpack_many("\xFF\x01\x02\xFE", bits: 16, endian: 'big', signed: false) #=> [65281, 766]

Parameters:

  • data (String)

    String to be unpacked.

  • bits (Integer, 'all') (defaults to: nil)

    Number of bits to be used for each chunk of data, or 'all' for all bits. Default to context.bits

  • endian (String) (defaults to: nil)

    Endian to use when unpacking. Can be any value accepted by context (See Context::ContextType). Default to context.endian.

  • signed (Boolean, String) (defaults to: nil)

    Whether the output number should be signed. Can be any value accepted by context (See Context::ContextType). Default to context.signed.

Returns:

  • (Array<Integer>)

    The unpacked numbers.

Raises:

  • (ArgumentError)

    When bits isn't divisible by 8 or data.size isn't divisible by bits / 8.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/pwnlib/util/packing.rb', line 190

def unpack_many(data, bits: nil, endian: nil, signed: nil)
  return [unpack(data, bits: bits, endian: endian, signed: signed)] if bits == 'all'

  context.local(bits: bits, endian: endian, signed: signed) do
    bits = context.bits

    # TODO(Darkpi): Support this if found useful.
    raise ArgumentError, 'bits must be a multiple of 8' if bits % 8 != 0

    bytes = bits / 8

    raise ArgumentError, "data.size=#{data.size} must be a multiple of bytes=#{bytes}" if data.size % bytes != 0

    ret = []
    (data.size / bytes).times do |idx|
      x1 = idx * bytes
      x2 = x1 + bytes
      # We already set local context, no need to pass things down.
      ret << unpack(data[x1...x2], bits: bits)
    end
    ret
  end
end