pwntools 1.0.1 → 1.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.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +4 -3
  3. data/Rakefile +3 -1
  4. data/lib/pwnlib/asm.rb +172 -2
  5. data/lib/pwnlib/constants/constants.rb +10 -3
  6. data/lib/pwnlib/context.rb +1 -3
  7. data/lib/pwnlib/elf/elf.rb +3 -3
  8. data/lib/pwnlib/errors.rb +30 -0
  9. data/lib/pwnlib/ext/helper.rb +1 -1
  10. data/lib/pwnlib/logger.rb +140 -2
  11. data/lib/pwnlib/pwn.rb +3 -0
  12. data/lib/pwnlib/reg_sort.rb +1 -1
  13. data/lib/pwnlib/shellcraft/generators/amd64/common/infloop.rb +9 -3
  14. data/lib/pwnlib/shellcraft/generators/amd64/common/pushstr_array.rb +6 -2
  15. data/lib/pwnlib/shellcraft/generators/amd64/common/setregs.rb +6 -2
  16. data/lib/pwnlib/shellcraft/generators/amd64/linux/cat.rb +23 -0
  17. data/lib/pwnlib/shellcraft/generators/amd64/linux/execve.rb +6 -4
  18. data/lib/pwnlib/shellcraft/generators/amd64/linux/exit.rb +23 -0
  19. data/lib/pwnlib/shellcraft/generators/amd64/linux/ls.rb +6 -2
  20. data/lib/pwnlib/shellcraft/generators/amd64/linux/open.rb +23 -0
  21. data/lib/pwnlib/shellcraft/generators/amd64/linux/sh.rb +6 -2
  22. data/lib/pwnlib/shellcraft/generators/amd64/linux/syscall.rb +6 -4
  23. data/lib/pwnlib/shellcraft/generators/i386/common/infloop.rb +9 -3
  24. data/lib/pwnlib/shellcraft/generators/i386/common/pushstr_array.rb +6 -2
  25. data/lib/pwnlib/shellcraft/generators/i386/common/setregs.rb +6 -2
  26. data/lib/pwnlib/shellcraft/generators/i386/linux/cat.rb +23 -0
  27. data/lib/pwnlib/shellcraft/generators/i386/linux/execve.rb +8 -4
  28. data/lib/pwnlib/shellcraft/generators/i386/linux/exit.rb +23 -0
  29. data/lib/pwnlib/shellcraft/generators/i386/linux/ls.rb +6 -2
  30. data/lib/pwnlib/shellcraft/generators/i386/linux/open.rb +23 -0
  31. data/lib/pwnlib/shellcraft/generators/i386/linux/sh.rb +6 -2
  32. data/lib/pwnlib/shellcraft/generators/i386/linux/syscall.rb +8 -4
  33. data/lib/pwnlib/shellcraft/generators/x86/linux/cat.rb +53 -0
  34. data/lib/pwnlib/shellcraft/generators/x86/linux/exit.rb +33 -0
  35. data/lib/pwnlib/shellcraft/generators/x86/linux/open.rb +46 -0
  36. data/lib/pwnlib/shellcraft/shellcraft.rb +3 -2
  37. data/lib/pwnlib/timer.rb +5 -2
  38. data/lib/pwnlib/tubes/process.rb +153 -0
  39. data/lib/pwnlib/tubes/serialtube.rb +112 -0
  40. data/lib/pwnlib/tubes/sock.rb +24 -25
  41. data/lib/pwnlib/tubes/tube.rb +191 -39
  42. data/lib/pwnlib/util/packing.rb +3 -9
  43. data/lib/pwnlib/version.rb +1 -1
  44. data/test/asm_test.rb +85 -2
  45. data/test/constants/constants_test.rb +2 -2
  46. data/test/data/echo.rb +2 -7
  47. data/test/elf/elf_test.rb +10 -15
  48. data/test/files/use_pwn.rb +2 -6
  49. data/test/logger_test.rb +38 -0
  50. data/test/shellcraft/linux/cat_test.rb +86 -0
  51. data/test/shellcraft/linux/syscalls/exit_test.rb +56 -0
  52. data/test/shellcraft/linux/syscalls/open_test.rb +86 -0
  53. data/test/shellcraft/shellcraft_test.rb +5 -4
  54. data/test/test_helper.rb +22 -2
  55. data/test/timer_test.rb +19 -1
  56. data/test/tubes/process_test.rb +99 -0
  57. data/test/tubes/serialtube_test.rb +165 -0
  58. data/test/tubes/sock_test.rb +20 -21
  59. data/test/tubes/tube_test.rb +86 -16
  60. metadata +75 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d329dcead1dc5c93633effddf3c627f5071cce80
4
- data.tar.gz: b5bf6ed6299056cbd1fd93ad6a37e04a992ce3e1
2
+ SHA256:
3
+ metadata.gz: 7c323ddeb8d8f4ea3d4d087fc69eb53ee7594f64c2ee01dfb57ada9e2a9eb111
4
+ data.tar.gz: b9177cb71feb8ecc038680cf850f0b654eea567c7f6774d8ad35cdb1b8949e93
5
5
  SHA512:
6
- metadata.gz: 71594da6ddc2572a11e2d41c641f04d494d1ea2d3e81ef1a6fd82d92edb3af3599e7099bbdc3393d56a5955fcfd983416dd8be787087fb06d3a586286eb13079
7
- data.tar.gz: b7cfba8de4134ea156f58244dff5d1a4e7431b376d1a75d9713503552c11911fb103187249e063baef93cac1f1ce651864f64815e5e2105fc539a6d8ef491df8
6
+ metadata.gz: 76cbcd91657ecaee5b8bfae49bfe378539937718a1eb46fbe517c8559919877a153cffb0e5beeb06dde68fe64cd7ab9e1fb30af507547771f49331e7bb90c93b
7
+ data.tar.gz: 1aedd2cb397654aa4bc4e13761a1587431f7a67ecec2150c5e6393f1275dfd14801089e0de7234ddd887b2fb99c9635c9bc438ba183c6fcf8d021331269d99b5
data/README.md CHANGED
@@ -57,7 +57,7 @@ gem install pwntools
57
57
  ```sh
58
58
  git clone https://github.com/peter50216/pwntools-ruby
59
59
  cd pwntools-ruby
60
- gem build pwntools.gemspec && gem install pwntools-*.gem
60
+ bundle install && bundle exec rake install
61
61
  ```
62
62
 
63
63
  ### optional
@@ -101,9 +101,10 @@ sudo make install
101
101
  - [x] elf
102
102
  - [x] dynelf
103
103
  - [x] logger
104
- - [ ] tube
104
+ - [x] tube
105
105
  - [x] sock
106
- - [ ] process
106
+ - [x] process
107
+ - [x] serialtube
107
108
  - [ ] fmtstr
108
109
  - [x] util
109
110
  - [x] pack
data/Rakefile CHANGED
@@ -7,8 +7,10 @@ require 'rake/testtask'
7
7
  require 'rubocop/rake_task'
8
8
  require 'yard'
9
9
 
10
+ import 'tasks/shellcraft/x86.rake'
11
+
10
12
  RuboCop::RakeTask.new(:rubocop) do |task|
11
- task.patterns = ['lib/**/*.rb', 'test/**/*.rb']
13
+ task.patterns = ['lib/**/*.rb', 'tasks/**/*.rake', 'test/**/*.rb']
12
14
  end
13
15
 
14
16
  task default: %i(install_git_hooks rubocop test)
@@ -1,8 +1,12 @@
1
1
  # encoding: ASCII-8BIT
2
2
 
3
+ require 'tempfile'
4
+
5
+ require 'elftools'
3
6
  require 'keystone_engine/keystone_const'
4
7
 
5
8
  require 'pwnlib/context'
9
+ require 'pwnlib/errors'
6
10
  require 'pwnlib/util/ruby'
7
11
 
8
12
  module Pwnlib
@@ -11,6 +15,15 @@ module Pwnlib
11
15
  module Asm
12
16
  module_function
13
17
 
18
+ # Default virtaul memory base address of architectures.
19
+ #
20
+ # This address may be different by using different linker.
21
+ DEFAULT_VMA = {
22
+ i386: 0x08048000,
23
+ amd64: 0x400000,
24
+ arm: 0x8000
25
+ }.freeze
26
+
14
27
  # Disassembles a bytestring into human readable assembly.
15
28
  #
16
29
  # {.disasm} depends on another open-source project - capstone, error will be raised if capstone is not intalled.
@@ -22,7 +35,7 @@ module Pwnlib
22
35
  # @return [String]
23
36
  # Disassemble result with nice typesetting.
24
37
  #
25
- # @raise [LoadError]
38
+ # @raise [Pwnlib::Errors::DependencyError]
26
39
  # If libcapstone is not installed.
27
40
  #
28
41
  # @example
@@ -85,6 +98,80 @@ module Pwnlib
85
98
  KeystoneEngine::Ks.new(ks_arch, ks_mode).asm(code)[0]
86
99
  end
87
100
 
101
+ # Builds an ELF file from executable code.
102
+ #
103
+ # @param [String] data
104
+ # Assembled code.
105
+ # @param [Integer?] vma
106
+ # The load address for the ELF file.
107
+ # If +nil+ is given, default address will be used.
108
+ # See {DEFAULT_VMA}.
109
+ # @param [Boolean] to_file
110
+ # Returns ELF content or the path to the ELF file.
111
+ # If +true+ is given, the ELF will be saved into a temp file.
112
+ #
113
+ # @return [String, Object]
114
+ # Without block
115
+ # - If +to_file+ is +false+ (default), returns the content of ELF.
116
+ # - Otherwise, a file is created and the path is returned.
117
+ # With block given, an ELF file will be created and its path will be yielded.
118
+ # This method will return what the block returned, and the ELF file will be removed after the block yielded.
119
+ #
120
+ # @yieldparam [String] path
121
+ # The path to the created ELF file.
122
+ #
123
+ # @yieldreturn [Object]
124
+ # Whatever you want.
125
+ #
126
+ # @raise [::Pwnlib::Errors::UnsupportedArchError]
127
+ # Raised when don't know how to create an ELF under architecture +context.arch+.
128
+ #
129
+ # @diff
130
+ # Unlike pwntools-python uses cross-compiler to compile code into ELF, we create ELFs in pure Ruby
131
+ # implementation. Therefore, we have higher flexibility and less binary dependencies.
132
+ #
133
+ # @example
134
+ # bin = make_elf(asm(shellcraft.sh))
135
+ # bin[0, 4]
136
+ # #=> "\x7FELF"
137
+ # @example
138
+ # path = make_elf(asm(shellcraft.cat('/proc/self/maps')), to_file: true)
139
+ # puts `#{path}`
140
+ # # 08048000-08049000 r-xp 00000000 fd:01 27671233 /tmp/pwn20180129-3411-7klnng.elf
141
+ # # f77c7000-f77c9000 r--p 00000000 00:00 0 [vvar]
142
+ # # f77c9000-f77cb000 r-xp 00000000 00:00 0 [vdso]
143
+ # # ffda6000-ffdc8000 rwxp 00000000 00:00 0 [stack]
144
+ # @example
145
+ # # no need 'to_file' parameter if block is given
146
+ # make_elf(asm(shellcraft.cat('/proc/self/maps'))) do |path|
147
+ # puts `#{path}`
148
+ # # 08048000-08049000 r-xp 00000000 fd:01 27671233 /tmp/pwn20180129-3411-7klnng.elf
149
+ # # f77c7000-f77c9000 r--p 00000000 00:00 0 [vvar]
150
+ # # f77c9000-f77cb000 r-xp 00000000 00:00 0 [vdso]
151
+ # # ffda6000-ffdc8000 rwxp 00000000 00:00 0 [stack]
152
+ # end
153
+ def make_elf(data, vma: nil, to_file: false)
154
+ to_file ||= block_given?
155
+ vma ||= DEFAULT_VMA[context.arch.to_sym]
156
+ vma &= -0x1000
157
+ # ELF header
158
+ # Program headers
159
+ # <data>
160
+ headers = create_elf_headers(vma)
161
+ ehdr = headers[:elf_header]
162
+ phdr = headers[:program_header]
163
+ entry = ehdr.num_bytes + phdr.num_bytes
164
+ ehdr.e_entry = entry + phdr.p_vaddr
165
+ ehdr.e_phoff = ehdr.num_bytes
166
+ phdr.p_filesz = phdr.p_memsz = entry + data.size
167
+ elf = ehdr.to_binary_s + phdr.to_binary_s + data
168
+ return elf unless to_file
169
+ path = Dir::Tmpname.create(['pwn', '.elf']) do |temp|
170
+ File.open(temp, 'wb', 0o750) { |f| f.write(elf) }
171
+ end
172
+ block_given? ? yield(path).tap { File.unlink(path) } : path
173
+ end
174
+
88
175
  ::Pwnlib::Util::Ruby.private_class_method_block do
89
176
  def cap_arch
90
177
  {
@@ -118,7 +205,7 @@ module Pwnlib
118
205
  def require_message(lib, msg)
119
206
  require lib
120
207
  rescue LoadError => e
121
- raise LoadError, e.message + "\n\n" + msg
208
+ raise ::Pwnlib::Errors::DependencyError, e.message + "\n\n" + msg
122
209
  end
123
210
 
124
211
  def install_crabstone_guide
@@ -140,6 +227,89 @@ https://github.com/keystone-engine/keystone/tree/master/docs
140
227
 
141
228
  EOS
142
229
  end
230
+
231
+ # build headers according to context.arch/bits/endian
232
+ def create_elf_headers(vma)
233
+ elf_header = create_elf_header
234
+ # we only need one LOAD segment
235
+ program_header = create_program_header(vma)
236
+ elf_header.e_phentsize = program_header.num_bytes
237
+ elf_header.e_phnum = 1
238
+ {
239
+ elf_header: elf_header,
240
+ program_header: program_header
241
+ }
242
+ end
243
+
244
+ def create_elf_header
245
+ header = ::ELFTools::Structs::ELF_Ehdr.new(endian: endian)
246
+ # this decide size of entries
247
+ header.elf_class = context.bits
248
+ header.e_ident.magic = ::ELFTools::Constants::ELFMAG
249
+ header.e_ident.ei_class = { 32 => 1, 64 => 2 }[context.bits]
250
+ header.e_ident.ei_data = { little: 1, big: 2 }[endian]
251
+ # Not sure what version field means, seems it can be any value.
252
+ header.e_ident.ei_version = 1
253
+ header.e_ident.ei_padding = "\x00" * 7
254
+ header.e_type = ::ELFTools::Constants::ET::ET_EXEC
255
+ header.e_machine = e_machine
256
+ # XXX(david942j): is header.e_flags important?
257
+ header.e_ehsize = header.num_bytes
258
+ header
259
+ end
260
+
261
+ def create_program_header(vma)
262
+ header = ::ELFTools::Structs::ELF_Phdr[context.bits].new(endian: endian)
263
+ header.p_type = ::ELFTools::Constants::PT::PT_LOAD
264
+ header.p_offset = 0
265
+ header.p_vaddr = vma
266
+ header.p_paddr = vma
267
+ header.p_flags = 4 | 1 # r-x
268
+ header.p_align = arch_align
269
+ header
270
+ end
271
+
272
+ # Not sure how this field is used, remove this if it is not important.
273
+ # This table is collected by cross-compiling and see the align in LOAD segment.
274
+ def arch_align
275
+ case context.arch.to_sym
276
+ when :i386, :amd64 then 0x1000
277
+ when :arm then 0x8000
278
+ end
279
+ end
280
+
281
+ # Mapping +context.arch+ to +::ELFTools::Constants::EM::EM_*+.
282
+ ARCH_EM = {
283
+ aarch64: 'AARCH64',
284
+ alpha: 'ALPHA',
285
+ amd64: 'X86_64',
286
+ arm: 'ARM',
287
+ cris: 'CRIS',
288
+ i386: '386',
289
+ ia64: 'IA_64',
290
+ m68k: '68K',
291
+ mips64: 'MIPS',
292
+ mips: 'MIPS',
293
+ powerpc64: 'PPC64',
294
+ powerpc: 'PPC',
295
+ s390: 'S390',
296
+ sparc64: 'SPARCV9',
297
+ sparc: 'SPARC'
298
+ }.freeze
299
+
300
+ def e_machine
301
+ const = ARCH_EM[context.arch.to_sym]
302
+ if const.nil?
303
+ raise ::Pwnlib::Errors::UnsupportedArchError,
304
+ "Unknown machine type of architecture #{context.arch.inspect}."
305
+ end
306
+ ::ELFTools::Constants::EM.const_get("EM_#{const}")
307
+ end
308
+
309
+ def endian
310
+ context.endian.to_sym
311
+ end
312
+
143
313
  include ::Pwnlib::Context
144
314
  end
145
315
  end
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  require 'pwnlib/constants/constant'
6
6
  require 'pwnlib/context'
7
+ require 'pwnlib/errors'
7
8
 
8
9
  module Pwnlib
9
10
  # Module containing constants.
@@ -36,17 +37,23 @@ module Pwnlib
36
37
  # @return [Constant]
37
38
  # The evaluated result.
38
39
  #
40
+ # @raise [Pwnlib::Errors::ConstantNotFoundError]
41
+ # Raised when undefined constant(s) provided.
42
+ #
39
43
  # @example
40
- # eval('O_CREAT')
44
+ # Constants.eval('O_CREAT')
41
45
  # #=> Constant('(O_CREAT)', 0x40)
42
- # eval('O_CREAT | O_APPEND')
46
+ # Constants.eval('O_CREAT | O_APPEND')
43
47
  # #=> Constant('(O_CREAT | O_APPEND)', 0x440)
48
+ # @example
49
+ # Constants.eval('meow')
50
+ # # Pwnlib::Errors::ConstantNotFoundError: Undefined constant(s): meow
44
51
  def eval(str)
45
52
  return str unless str.instance_of?(String)
46
53
  begin
47
54
  val = calculator.evaluate!(str.strip).to_i
48
55
  rescue Dentaku::UnboundVariableError => e
49
- raise NameError, e.message
56
+ raise ::Pwnlib::Errors::ConstantNotFoundError, "Undefined constant(s): #{e.unbound_variables.join(', ')}"
50
57
  end
51
58
  ::Pwnlib::Constants::Constant.new("(#{str})", val)
52
59
  end
@@ -274,9 +274,7 @@ module Pwnlib
274
274
  when true, false
275
275
  signed = value
276
276
  end
277
- if signed.nil?
278
- raise ArgumentError, "signed must be boolean or one of #{SIGNEDNESSES.keys.sort.inspect}"
279
- end
277
+ raise ArgumentError, "signed must be boolean or one of #{SIGNEDNESSES.keys.sort.inspect}" if signed.nil?
280
278
  @attrs[:signed] = signed
281
279
  end
282
280
 
@@ -1,5 +1,6 @@
1
- require 'elftools'
2
1
  require 'ostruct'
2
+
3
+ require 'elftools'
3
4
  require 'rainbow'
4
5
 
5
6
  require 'pwnlib/logger'
@@ -40,7 +41,7 @@ module Pwnlib
40
41
  # #=> #<Pwnlib::ELF::ELF:0x00559bd670dcb8>
41
42
  def initialize(path, checksec: true)
42
43
  path = File.realpath(path)
43
- @elf_file = ELFTools::ELFFile.new(File.open(path, 'rb')) # rubocop:disable Style/AutoResourceCleanup
44
+ @elf_file = ELFTools::ELFFile.new(File.open(path, 'rb'))
44
45
  load_got
45
46
  load_plt
46
47
  load_symbols
@@ -67,7 +68,6 @@ module Pwnlib
67
68
  [@got, @plt, @symbols].compact.each do |tbl|
68
69
  tbl.each_pair { |k, _| tbl[k] += val - old }
69
70
  end
70
- val
71
71
  end
72
72
 
73
73
  # Return the protection information, wrapper with color codes.
@@ -0,0 +1,30 @@
1
+ # encoding: ASCII-8BIT
2
+
3
+ module Pwnlib
4
+ # Generic {Pwnlib} exception class.
5
+ class Error < StandardError
6
+ end
7
+
8
+ # Pnwlib Errors
9
+ module Errors
10
+ # Raised by some IO operations in tubes.
11
+ class EndOfTubeError < ::Pwnlib::Error
12
+ end
13
+
14
+ # Raised when a dependent file fails to load.
15
+ class DependencyError < ::Pwnlib::Error
16
+ end
17
+
18
+ # Raised when a given constant is invalid or undefined.
19
+ class ConstantNotFoundError < ::Pwnlib::Error
20
+ end
21
+
22
+ # Raised when timeout exceeded.
23
+ class TimeoutError < ::Pwnlib::Error
24
+ end
25
+
26
+ # Raised when method doesn't support under current architecture.
27
+ class UnsupportedArchError < ::Pwnlib::Error
28
+ end
29
+ end
30
+ end
@@ -9,7 +9,7 @@ module Pwnlib
9
9
  .map { |x| [x, x] }
10
10
  .concat(m2.to_a)
11
11
  .each do |method, proxy_to|
12
- class_eval <<-EOS
12
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
13
13
  def #{method}(*args, &block)
14
14
  #{mod}.#{proxy_to}(self, *args, &block)
15
15
  end
@@ -1,7 +1,11 @@
1
1
  # encoding: ASCII-8BIT
2
2
 
3
3
  require 'logger'
4
+
5
+ require 'method_source/code_helpers' # don't require 'method_source', it pollutes Method/Proc classes.
4
6
  require 'rainbow'
7
+ require 'ruby2ruby'
8
+ require 'ruby_parser'
5
9
 
6
10
  require 'pwnlib/context'
7
11
 
@@ -13,6 +17,12 @@ module Pwnlib
13
17
  # The type for logger which inherits Ruby builtin Logger.
14
18
  # Main difference is using +context.log_level+ instead of +level+ in logging methods.
15
19
  class LoggerType < ::Logger
20
+ # To use method +expression_at+.
21
+ #
22
+ # XXX(david942j): move this extension if other modules need +expression_at+ as well.
23
+ extend ::MethodSource::CodeHelpers
24
+
25
+ # Color codes for pretty logging.
16
26
  SEV_COLOR = {
17
27
  'DEBUG' => '#ff5f5f',
18
28
  'INFO' => '#87ff00',
@@ -24,8 +34,8 @@ module Pwnlib
24
34
  # Instantiate a {Pwnlib::Logger::LoggerType} object.
25
35
  def initialize
26
36
  super(STDOUT)
27
- @formatter = proc do |severity, _datetime, _progname, msg|
28
- format("[%s] %s\n", Rainbow(severity).color(SEV_COLOR[severity]), msg)
37
+ @formatter = proc do |severity, _datetime, progname, msg|
38
+ format("[%s] %s\n", Rainbow(progname || severity).color(SEV_COLOR[severity]), msg)
29
39
  end
30
40
  end
31
41
 
@@ -43,6 +53,70 @@ module Pwnlib
43
53
  true
44
54
  end
45
55
 
56
+ # Log the arguments and their evalutated results.
57
+ #
58
+ # This method has same severity as +INFO+.
59
+ #
60
+ # The difference between using arguments and passing a block is the block will be executed if the logger's level
61
+ # is sufficient to log a message.
62
+ #
63
+ # @param [Array<#inspect>] args
64
+ # Anything. See examples.
65
+ #
66
+ # @yieldreturn [#inspect]
67
+ # See examples.
68
+ # Block will be invoked only if +args+ is empty.
69
+ #
70
+ # @return See ::Logger#add.
71
+ #
72
+ # @example
73
+ # x = 2
74
+ # y = 3
75
+ # log.dump(x + y, x * y)
76
+ # # [DUMP] (x + y) = 5, (x * y) = 6
77
+ # @example
78
+ # libc = 0x7fc0bdd13000
79
+ # log.dump libc.hex
80
+ # # [DUMP] libc.hex = "0x7fc0bdd13000"
81
+ #
82
+ # libc = 0x7fc0bdd13000
83
+ # log.dump { libc.hex }
84
+ # # [DUMP] libc.hex = "0x7fc0bdd13000"
85
+ # log.dump { libc = 12345678; libc.hex }
86
+ # # [DUMP] libc = 12345678
87
+ # # libc.hex = "0xbc614e"
88
+ # @example
89
+ # log.dump do
90
+ # meow = 123
91
+ # # comments will be ignored
92
+ # meow <<= 1 # this is a comment
93
+ # meow
94
+ # end
95
+ # # [DUMP] meow = 123
96
+ # # meow = (meow << 1)
97
+ # # meow = 246
98
+ #
99
+ # @note
100
+ # This method does NOT work in a REPL shell (such as irb and pry).
101
+ #
102
+ # @note
103
+ # The source code where invoked +log.dump+ will be parsed by using +ruby_parser+,
104
+ # therefore this method fails in some situations, such as:
105
+ # log.dump(&something) # will fail in souce code parsing
106
+ # log.dump { 1 }; log.dump { 2 } # 1 will be logged two times
107
+ def dump(*args)
108
+ severity = INFO
109
+ # Don't invoke the block if it's unnecessary.
110
+ return true if severity < context.log_level
111
+ caller_ = caller_locations(1, 1).first
112
+ src = source_of(caller_.absolute_path, caller_.lineno)
113
+ results = args.empty? ? [[yield, source_of_block(src)]] : args.zip(source_of_args(src))
114
+ msg = results.map { |res, expr| "#{expr.strip} = #{res.inspect}" }.join(', ')
115
+ # do indent if msg contains multiple lines
116
+ first, *remain = msg.split("\n")
117
+ add(severity, ([first] + remain.map { |r| '[DUMP] '.gsub(/./, ' ') + r }).join("\n"), 'DUMP')
118
+ end
119
+
46
120
  private
47
121
 
48
122
  def add(severity, message = nil, progname = nil)
@@ -51,6 +125,70 @@ module Pwnlib
51
125
  super(severity, message, progname)
52
126
  end
53
127
 
128
+ def source_of(path, line_number)
129
+ File.open(path) { |f| LoggerType.expression_at(f, line_number) }
130
+ end
131
+
132
+ # Find the content of block that invoked by log.dump { ... }.
133
+ #
134
+ # @param [String] source
135
+ #
136
+ # @return [String]
137
+ #
138
+ # @example
139
+ # source_of_block("log.dump do\n123\n456\nend")
140
+ # #=> "123\n456\n"
141
+ def source_of_block(source)
142
+ parse_and_search(source, [:iter, [:call, nil, :dump]]) { |sexp| ::Ruby2Ruby.new.process(sexp.last) }
143
+ end
144
+
145
+ # Find the arguments passed to log.dump(...).
146
+ #
147
+ # @param [String] source
148
+ #
149
+ # @return [Array<String>]
150
+ #
151
+ # @example
152
+ # source_of_args("log.dump(x, y, x + y)")
153
+ # #=> ["x", "y", "(x + y)"]
154
+ def source_of_args(source)
155
+ parse_and_search(source, [:call, nil, :dump]) do |sexp|
156
+ sexp[3..-1].map { |s| ::Ruby2Ruby.new.process(s) }
157
+ end
158
+ end
159
+
160
+ # This method do the following things:
161
+ # 1. Parse the source code to `Sexp` (using `ruby_parser`)
162
+ # 2. Traverse the sexp to find the block/arguments (according to target) when calling `dump`
163
+ # 3. Convert the sexp of block back to Ruby code (using gem `ruby2ruby`)
164
+ #
165
+ # @yieldparam [Sexp] sexp
166
+ # The found Sexp according to +target+.
167
+ def parse_and_search(source, target)
168
+ sexp = ::RubyParser.new.process(source)
169
+ sexp = search_sexp(sexp, target)
170
+ return nil if sexp.nil?
171
+ yield sexp
172
+ end
173
+
174
+ # depth-first search
175
+ def search_sexp(sexp, target)
176
+ return nil unless sexp.is_a?(::Sexp)
177
+ return sexp if match_sexp?(sexp, target)
178
+ sexp.find do |e|
179
+ f = search_sexp(e, target)
180
+ break f if f
181
+ end
182
+ end
183
+
184
+ def match_sexp?(sexp, target)
185
+ target.zip(sexp.entries).all? do |t, s|
186
+ next true if t.nil?
187
+ next match_sexp?(s, t) if t.is_a?(Array)
188
+ s == t
189
+ end
190
+ end
191
+
54
192
  include ::Pwnlib::Context
55
193
  end
56
194