pwntools 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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