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,31 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'pwnlib/constants/constants'
|
5
|
+
require 'pwnlib/context'
|
6
|
+
|
7
|
+
class ConstantsTest < MiniTest::Test
|
8
|
+
include ::Pwnlib::Context
|
9
|
+
Constants = ::Pwnlib::Constants
|
10
|
+
|
11
|
+
def test_amd64
|
12
|
+
context.local(arch: 'amd64') do
|
13
|
+
assert_equal('Constant("SYS_read", 0x0)', Constants.SYS_read.inspect)
|
14
|
+
assert_equal('__NR_arch_prctl', Constants.__NR_arch_prctl.to_s)
|
15
|
+
assert_equal('Constant("(O_CREAT)", 0x40)', Constants.eval('O_CREAT').inspect)
|
16
|
+
# TODO(david942j): implement 'real' Constants.eval
|
17
|
+
# assert_equal('Constant("(O_CREAT | O_WRONLY)", 0x41)', Constants.eval('O_CREAT | O_WRONLY').inspect)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_i386
|
22
|
+
context.local(arch: 'i386') do
|
23
|
+
assert_equal('Constant("SYS_read", 0x3)', Constants.SYS_read.inspect)
|
24
|
+
assert_equal('__NR_prctl', Constants.__NR_prctl.to_s)
|
25
|
+
assert_equal('Constant("(O_CREAT)", 0x40)', Constants.eval('O_CREAT').inspect)
|
26
|
+
assert_equal(0x40, Constants.method(:O_CREAT).call.to_i)
|
27
|
+
# 2 < 3
|
28
|
+
assert_operator(2, :<, Constants.SYS_read)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'pwnlib/context'
|
5
|
+
|
6
|
+
class ContextTest < MiniTest::Test
|
7
|
+
include ::Pwnlib::Context
|
8
|
+
|
9
|
+
def test_update
|
10
|
+
context.update(arch: 'arm', os: 'windows')
|
11
|
+
assert_equal('arm', context.arch)
|
12
|
+
assert_equal('windows', context.os)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_local
|
16
|
+
context.timeout = 1
|
17
|
+
assert_equal(1, context.timeout)
|
18
|
+
|
19
|
+
context.local(timeout: 2) do
|
20
|
+
assert_equal(2, context.timeout)
|
21
|
+
context.timeout = 3
|
22
|
+
assert_equal(3, context.timeout)
|
23
|
+
end
|
24
|
+
|
25
|
+
assert_equal(1, context.timeout)
|
26
|
+
|
27
|
+
assert_raises(RuntimeError) do
|
28
|
+
context.local(timeout: 3) { raise 'QQ failed in block' }
|
29
|
+
end
|
30
|
+
|
31
|
+
assert_equal(1, context.timeout)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_clear
|
35
|
+
default_arch = context.arch
|
36
|
+
context.arch = 'arm'
|
37
|
+
context.clear
|
38
|
+
assert_equal(default_arch, context.arch)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_arch
|
42
|
+
context.arch = 'mips'
|
43
|
+
assert_equal('mips', context.arch)
|
44
|
+
|
45
|
+
err = assert_raises(ArgumentError) { context.arch = 'shik' }
|
46
|
+
assert_match(/arch must be one of/, err.message)
|
47
|
+
assert_equal('mips', context.arch)
|
48
|
+
|
49
|
+
context.clear
|
50
|
+
assert_equal(32, context.bits)
|
51
|
+
context.arch = 'powerpc64'
|
52
|
+
assert_equal(64, context.bits)
|
53
|
+
assert_equal('big', context.endian)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_bits
|
57
|
+
context.bits = 64
|
58
|
+
assert_equal(64, context.bits)
|
59
|
+
|
60
|
+
err = assert_raises(ArgumentError) { context.bits = 0 }
|
61
|
+
assert_match(/bits must be > 0/, err.message)
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_bytes
|
65
|
+
context.bytes = 8
|
66
|
+
assert_equal(64, context.bits)
|
67
|
+
assert_equal(8, context.bytes)
|
68
|
+
|
69
|
+
context.bits = 32
|
70
|
+
assert_equal(4, context.bytes)
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_endian
|
74
|
+
context.endian = 'le'
|
75
|
+
assert_equal('little', context.endian)
|
76
|
+
|
77
|
+
context.endian = 'big'
|
78
|
+
assert_equal('big', context.endian)
|
79
|
+
|
80
|
+
err = assert_raises(ArgumentError) { context.endian = 'SUPERBIG' }
|
81
|
+
assert_match(/endian must be one of/, err.message)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_log_level
|
85
|
+
context.log_level = 'error'
|
86
|
+
assert_equal(Logger::ERROR, context.log_level)
|
87
|
+
|
88
|
+
context.log_level = 514
|
89
|
+
assert_equal(514, context.log_level)
|
90
|
+
|
91
|
+
err = assert_raises(ArgumentError) { context.log_level = 'BOOM' }
|
92
|
+
assert_match(/log_level must be an integer or one of/, err.message)
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_os
|
96
|
+
context.os = 'windows'
|
97
|
+
assert_equal('windows', context.os)
|
98
|
+
|
99
|
+
err = assert_raises(ArgumentError) { context.os = 'deepblue' }
|
100
|
+
assert_match(/os must be one of/, err.message)
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_signed
|
104
|
+
context.signed = true
|
105
|
+
assert_equal(true, context.signed)
|
106
|
+
|
107
|
+
context.signed = 'unsigned'
|
108
|
+
assert_equal(false, context.signed)
|
109
|
+
|
110
|
+
err = assert_raises(ArgumentError) { context.signed = 'partial' }
|
111
|
+
assert_match(/signed must be boolean or one of/, err.message)
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_timeout
|
115
|
+
context.timeout = 123
|
116
|
+
assert_equal(123, context.timeout)
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_newline
|
120
|
+
context.newline = "\r\n"
|
121
|
+
assert_equal("\r\n", context.newline)
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_to_s
|
125
|
+
assert_match(/\APwnlib::Context::ContextType\(.+\)\Z/, context.to_s)
|
126
|
+
end
|
127
|
+
|
128
|
+
def teardown
|
129
|
+
context.clear
|
130
|
+
end
|
131
|
+
end
|
data/test/data/victim.c
ADDED
data/test/data/victim32
ADDED
Binary file
|
data/test/data/victim64
ADDED
Binary file
|
data/test/dynelf_test.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
require 'tty-platform'
|
6
|
+
|
7
|
+
require 'test_helper'
|
8
|
+
require 'pwnlib/dynelf'
|
9
|
+
|
10
|
+
class DynELFTest < MiniTest::Test
|
11
|
+
def test_lookup
|
12
|
+
skip 'Only tested on linux' unless TTY::Platform.new.linux?
|
13
|
+
[32, 64].each do |b|
|
14
|
+
# TODO(hh): Use process instead of popen2
|
15
|
+
Open3.popen2(File.expand_path("../data/victim#{b}", __FILE__)) do |i, o, t|
|
16
|
+
main_ra = Integer(o.readline)
|
17
|
+
libc_path = nil
|
18
|
+
IO.readlines("/proc/#{t.pid}/maps").map(&:split).each do |s|
|
19
|
+
st, ed = s[0].split('-').map { |x| x.to_i(16) }
|
20
|
+
next unless main_ra.between?(st, ed)
|
21
|
+
libc_path = s[-1]
|
22
|
+
break
|
23
|
+
end
|
24
|
+
refute_nil(libc_path)
|
25
|
+
|
26
|
+
# TODO(hh): Use ELF instead of objdump
|
27
|
+
# Methods in libc might have multi-versions, so we record and check if
|
28
|
+
# we can find one of them.
|
29
|
+
h = Hash.new { |hsh, key| hsh[key] = [] }
|
30
|
+
symbols = `objdump -T #{libc_path}`.lines.map(&:split).select { |a| a[2] == 'DF' }
|
31
|
+
symbols.map { |a| h[a[-1]] << a[0].to_i(16) }
|
32
|
+
|
33
|
+
mem = open("/proc/#{t.pid}/mem", 'rb')
|
34
|
+
d = ::Pwnlib::DynELF.new(main_ra) do |addr|
|
35
|
+
mem.seek(addr)
|
36
|
+
mem.getc
|
37
|
+
end
|
38
|
+
|
39
|
+
assert_nil(d.lookup('pipi_hao_wei!'))
|
40
|
+
h.each do |sym, off|
|
41
|
+
assert_includes(off, d.lookup(sym) - d.libbase)
|
42
|
+
end
|
43
|
+
|
44
|
+
i.write('bye')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/test/ext_test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'pwnlib/ext/array'
|
5
|
+
require 'pwnlib/ext/integer'
|
6
|
+
require 'pwnlib/ext/string'
|
7
|
+
|
8
|
+
class ExtTest < MiniTest::Test
|
9
|
+
# Thought that test one method in each module for each type is enough, since it's quite
|
10
|
+
# stupid (and meaningless) to copy the list of proxied functions to here...
|
11
|
+
def test_ext_string
|
12
|
+
assert_equal(0x4142, 'AB'.u16(endian: 'be'))
|
13
|
+
assert_equal([1, 1, 0, 0, 0, 1, 0, 0], "\xC4".bits)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_ext_integer
|
17
|
+
assert_equal('AB', 0x4241.p16)
|
18
|
+
assert_equal([0, 0, 1, 1, 0, 1, 0, 0], 0x34.bits)
|
19
|
+
assert_equal(2**31, 1.bitswap)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_ext_array
|
23
|
+
assert_equal("\xfe", [1, 1, 1, 1, 1, 1, 1, 0].unbits)
|
24
|
+
assert_equal("XX\xef\xbe\xad\xdeXX", ['XX', 0xdeadbeef, 'XX'].flat)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
# Make sure we're using local copy for local testing.
|
4
|
+
$LOAD_PATH.unshift File.expand_path(File.join(__FILE__, '..', '..', '..', 'lib'))
|
5
|
+
|
6
|
+
require 'pwn'
|
7
|
+
|
8
|
+
context[arch: 'amd64']
|
9
|
+
|
10
|
+
raise 'pack fail' unless pack(1) == "\x01\0\0\0\0\0\0\0"
|
11
|
+
unless ::Pwnlib::Util::Fiddling.__send__(:context).object_id == context.object_id
|
12
|
+
raise 'not unique context'
|
13
|
+
end
|
14
|
+
unless ::Pwnlib::Context.context.object_id == context.object_id
|
15
|
+
raise 'not unique context'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Make sure things aren't polluting Object
|
19
|
+
begin
|
20
|
+
1.__send__(:context)
|
21
|
+
raise 'context polluting Object.'
|
22
|
+
rescue NoMethodError
|
23
|
+
puts 'good'
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
'1'.__send__(:context)
|
28
|
+
raise 'context polluting Object.'
|
29
|
+
rescue NoMethodError
|
30
|
+
puts 'good'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Make sure we can use Util::xxx::yyy directly
|
34
|
+
raise 'pack fail' unless Util::Packing.pack(1) == "\x01\0\0\0\0\0\0\0"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
# Make sure we're using local copy for local testing.
|
4
|
+
$LOAD_PATH.unshift File.expand_path(File.join(__FILE__, '..', '..', '..', 'lib'))
|
5
|
+
|
6
|
+
# TODO(Darkpi): Should we make sure ALL module works? (maybe we should).
|
7
|
+
require 'pwnlib/util/packing'
|
8
|
+
|
9
|
+
raise 'call from module fail' unless ::Pwnlib::Util::Packing.p8(0x61) == 'a'
|
10
|
+
|
11
|
+
include ::Pwnlib::Util::Packing::ClassMethods
|
12
|
+
raise 'include module and call fail' unless p8(0x61) == 'a'
|
13
|
+
|
14
|
+
begin
|
15
|
+
::Pwnlib::Util::Packing.context
|
16
|
+
raise 'context public in Pwnlib module'
|
17
|
+
rescue NoMethodError
|
18
|
+
puts 'good'
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
require 'test_helper'
|
6
|
+
|
7
|
+
class FullFileTest < MiniTest::Test
|
8
|
+
parallelize_me!
|
9
|
+
Dir['test/files/*.rb'].each do |f|
|
10
|
+
fn = File.basename(f, '.rb')
|
11
|
+
define_method("test_#{fn}") do
|
12
|
+
_, stderr, status = Open3.capture3('ruby', f, binmode: true)
|
13
|
+
assert(status.success?, stderr)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
require 'tty-platform'
|
6
|
+
|
7
|
+
require 'test_helper'
|
8
|
+
require 'pwnlib/memleak'
|
9
|
+
|
10
|
+
class MemLeakTest < MiniTest::Test
|
11
|
+
def setup
|
12
|
+
@victim = IO.binread(File.expand_path('../data/victim32', __FILE__))
|
13
|
+
@leak = ::Pwnlib::MemLeak.new { |addr| @victim[addr] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_find_elf_base_basic
|
17
|
+
assert_equal(0, @leak.find_elf_base(@victim.length * 2 / 3))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_find_elf_base_running
|
21
|
+
skip 'Only tested on linux' unless TTY::Platform.new.linux?
|
22
|
+
[32, 64].each do |b|
|
23
|
+
# TODO(hh): Use process instead of popen2
|
24
|
+
Open3.popen2(File.expand_path("../data/victim#{b}", __FILE__)) do |i, o, t|
|
25
|
+
main_ra = o.readline[2...-1].to_i(16)
|
26
|
+
realbase = nil
|
27
|
+
IO.readlines("/proc/#{t.pid}/maps").map(&:split).each do |s|
|
28
|
+
st, ed = s[0].split('-').map { |x| x.to_i(16) }
|
29
|
+
next unless main_ra.between?(st, ed)
|
30
|
+
realbase = st
|
31
|
+
break
|
32
|
+
end
|
33
|
+
refute_nil(realbase)
|
34
|
+
mem = open("/proc/#{t.pid}/mem", 'rb')
|
35
|
+
l2 = ::Pwnlib::MemLeak.new do |addr|
|
36
|
+
mem.seek(addr)
|
37
|
+
mem.getc
|
38
|
+
end
|
39
|
+
assert_equal(realbase, l2.find_elf_base(main_ra))
|
40
|
+
mem.close
|
41
|
+
i.write('bye')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_n
|
47
|
+
assert_equal("\x7fELF", @leak.n(0, 4))
|
48
|
+
assert_equal(@victim[0xf0, 0x20], @leak.n(0xf0, 0x20))
|
49
|
+
assert_equal(@victim[514, 0x20], @leak.n(514, 0x20))
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_b
|
53
|
+
assert_equal(@victim[0x100], @leak.b(0x100))
|
54
|
+
assert_equal(@victim[514], @leak.b(514))
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_w
|
58
|
+
assert_equal(::Pwnlib::Util::Packing.u16(@victim[0x100, 2]), @leak.w(0x100))
|
59
|
+
assert_equal(::Pwnlib::Util::Packing.u16(@victim[514, 2]), @leak.w(514))
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_d
|
63
|
+
assert_equal(::Pwnlib::Util::Packing.u32(@victim[0, 4]), @leak.d(0))
|
64
|
+
assert_equal(::Pwnlib::Util::Packing.u32(@victim[0x100, 4]), @leak.d(0x100))
|
65
|
+
assert_equal(::Pwnlib::Util::Packing.u32(@victim[514, 4]), @leak.d(514))
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_q
|
69
|
+
assert_equal(::Pwnlib::Util::Packing.u64(@victim[0x100, 8]), @leak.q(0x100))
|
70
|
+
assert_equal(::Pwnlib::Util::Packing.u64(@victim[514, 8]), @leak.q(514))
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
require 'test_helper'
|
3
|
+
require 'pwnlib/reg_sort'
|
4
|
+
|
5
|
+
class RegSortTest < MiniTest::Test
|
6
|
+
include ::Pwnlib::RegSort::ClassMethods
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@regs = %w(a b c d x y z)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_normal
|
13
|
+
assert_equal([['mov', 'a', 1], ['mov', 'b', 2]], regsort({ a: 1, b: 2 }, @regs))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_post_mov
|
17
|
+
assert_equal([['mov', 'a', 1], %w(mov b a)], regsort({ a: 1, b: 1 }, @regs))
|
18
|
+
assert_equal([%w(mov c a), ['mov', 'a', 1], %w(mov b a)], regsort({ a: 1, b: 1, c: 'a' }, @regs))
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_pseudoforest
|
22
|
+
# only one connected component
|
23
|
+
assert_equal([%w(mov b a), ['mov', 'a', 1]], regsort({ a: 1, b: 'a' }, @regs))
|
24
|
+
assert_equal([['mov', 'c', 3], %w(xchg a b)], regsort({ a: 'b', b: 'a', c: 3 }, @regs))
|
25
|
+
assert_equal([%w(mov c b), %w(xchg a b)], regsort({ a: 'b', b: 'a', c: 'b' }, @regs))
|
26
|
+
assert_equal([%w(mov x 1), %w(mov y z), %w(mov z c), %w(xchg a b), %w(xchg b c)],
|
27
|
+
regsort({ a: 'b', b: 'c', c: 'a', x: '1', y: 'z', z: 'c' }, @regs))
|
28
|
+
|
29
|
+
# more than one connected components
|
30
|
+
assert_equal([%w(xchg a b), %w(xchg c d)], regsort({ a: 'b', b: 'a', c: 'd', d: 'c' }, @regs))
|
31
|
+
assert_equal([%w(mov c b), %w(mov d b), %w(mov z x), %w(xchg a b), %w(xchg x y)],
|
32
|
+
regsort({ a: 'b', b: 'a', c: 'b', d: 'b', x: 'y', y: 'x', z: 'x' }, @regs))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_raise
|
36
|
+
err = assert_raises(ArgumentError) do
|
37
|
+
regsort({ a: 1 }, ['b'])
|
38
|
+
end
|
39
|
+
assert_match(/Unknown register!/, err.message)
|
40
|
+
end
|
41
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'codeclimate-test-reporter'
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
5
|
+
[SimpleCov::Formatter::HTMLFormatter, CodeClimate::TestReporter::Formatter]
|
6
|
+
)
|
7
|
+
SimpleCov.start do
|
8
|
+
add_filter '/test/'
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'minitest/autorun'
|
12
|
+
require 'minitest/unit'
|
13
|
+
require 'minitest/hell'
|