pwntools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,220 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
# TODO(Darkpi): Check if there should be special care for threading.
|
6
|
+
|
7
|
+
module Pwnlib
|
8
|
+
# Context module, store some platform-dependent informations.
|
9
|
+
module Context
|
10
|
+
# The type for context. User should never need to initialize one by themself.
|
11
|
+
class ContextType
|
12
|
+
DEFAULT = {
|
13
|
+
arch: 'i386',
|
14
|
+
bits: 32,
|
15
|
+
endian: 'little',
|
16
|
+
log_level: Logger::INFO,
|
17
|
+
newline: "\n",
|
18
|
+
os: 'linux',
|
19
|
+
signed: false,
|
20
|
+
timeout: Float::INFINITY
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
OSES = %w(linux freebsd windows).sort
|
24
|
+
|
25
|
+
BIG_32 = { endian: 'big', bits: 32 }.freeze
|
26
|
+
BIG_64 = { endian: 'big', bits: 64 }.freeze
|
27
|
+
LITTLE_8 = { endian: 'little', bits: 8 }.freeze
|
28
|
+
LITTLE_16 = { endian: 'little', bits: 16 }.freeze
|
29
|
+
LITTLE_32 = { endian: 'little', bits: 32 }.freeze
|
30
|
+
LITTLE_64 = { endian: 'little', bits: 64 }.freeze
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def longest(d)
|
34
|
+
Hash[d.sort_by { |k, _v| k.size }.reverse]
|
35
|
+
end
|
36
|
+
private :longest
|
37
|
+
end
|
38
|
+
|
39
|
+
ARCHS = longest(
|
40
|
+
'aarch64' => LITTLE_64,
|
41
|
+
'alpha' => LITTLE_64,
|
42
|
+
'avr' => LITTLE_8,
|
43
|
+
'amd64' => LITTLE_64,
|
44
|
+
'arm' => LITTLE_32,
|
45
|
+
'cris' => LITTLE_32,
|
46
|
+
'i386' => LITTLE_32,
|
47
|
+
'ia64' => BIG_64,
|
48
|
+
'm68k' => BIG_32,
|
49
|
+
'mips' => LITTLE_32,
|
50
|
+
'mips64' => LITTLE_64,
|
51
|
+
'msp430' => LITTLE_16,
|
52
|
+
'powerpc' => BIG_32,
|
53
|
+
'powerpc64' => BIG_64,
|
54
|
+
's390' => BIG_32,
|
55
|
+
'sparc' => BIG_32,
|
56
|
+
'sparc64' => BIG_64,
|
57
|
+
'thumb' => LITTLE_32,
|
58
|
+
'vax' => LITTLE_32
|
59
|
+
)
|
60
|
+
|
61
|
+
ENDIANNESSES = longest(
|
62
|
+
'be' => 'big',
|
63
|
+
'eb' => 'big',
|
64
|
+
'big' => 'big',
|
65
|
+
'le' => 'little',
|
66
|
+
'el' => 'little',
|
67
|
+
'little' => 'little'
|
68
|
+
)
|
69
|
+
|
70
|
+
SIGNEDNESSES = {
|
71
|
+
'unsigned' => false,
|
72
|
+
'no' => false,
|
73
|
+
'yes' => true,
|
74
|
+
'signed' => true
|
75
|
+
}.freeze
|
76
|
+
|
77
|
+
VALID_SIGNED = SIGNEDNESSES.keys
|
78
|
+
|
79
|
+
# XXX(Darkpi): Should we just hard-coded all levels here,
|
80
|
+
# or should we use Logger#const_defined?
|
81
|
+
# (This would include constant SEV_LEVEL, and exclude UNKNOWN)?
|
82
|
+
LOG_LEVELS = %w(DEBUG INFO WARN ERROR FATAL UNKNOWN).freeze
|
83
|
+
|
84
|
+
def initialize(**kwargs)
|
85
|
+
@attrs = DEFAULT.dup
|
86
|
+
update(**kwargs)
|
87
|
+
end
|
88
|
+
|
89
|
+
def update(**kwargs)
|
90
|
+
kwargs.each do |k, v|
|
91
|
+
next if v.nil?
|
92
|
+
public_send("#{k}=", v)
|
93
|
+
end
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
alias [] update
|
98
|
+
alias call update
|
99
|
+
|
100
|
+
def to_s
|
101
|
+
vals = @attrs.map { |k, v| "#{k} = #{v.inspect}" }
|
102
|
+
"#{self.class}(#{vals.join(', ')})"
|
103
|
+
end
|
104
|
+
|
105
|
+
# This would return what the block return.
|
106
|
+
def local(**kwargs)
|
107
|
+
raise ArgumentError, "Need a block for #{self.class}##{__callee__}" unless block_given?
|
108
|
+
# XXX(Darkpi): improve performance for this if this is too slow, since we use this in many
|
109
|
+
# places that has argument endian / signed / ...
|
110
|
+
old_attrs = @attrs.dup
|
111
|
+
begin
|
112
|
+
update(**kwargs)
|
113
|
+
yield
|
114
|
+
ensure
|
115
|
+
@attrs = old_attrs
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def clear
|
120
|
+
@attrs = DEFAULT.dup
|
121
|
+
end
|
122
|
+
|
123
|
+
# Getters here.
|
124
|
+
DEFAULT.each_key do |k|
|
125
|
+
define_method(k) { @attrs[k] }
|
126
|
+
end
|
127
|
+
|
128
|
+
def newline=(newline)
|
129
|
+
@attrs[:newline] = newline
|
130
|
+
end
|
131
|
+
|
132
|
+
# TODO(Darkpi): Timeout module.
|
133
|
+
def timeout=(timeout)
|
134
|
+
@attrs[:timeout] = timeout
|
135
|
+
end
|
136
|
+
|
137
|
+
# Difference from Python pwntools:
|
138
|
+
# We always change +bits+ and +endian+ field whether user have already changed them.
|
139
|
+
def arch=(arch)
|
140
|
+
arch = arch.downcase.gsub(/[[:punct:]]/, '')
|
141
|
+
defaults = ARCHS[arch]
|
142
|
+
raise ArgumentError, "arch must be one of #{ARCHS.keys.sort.inspect}" unless defaults
|
143
|
+
defaults.each { |k, v| @attrs[k] = v }
|
144
|
+
@attrs[:arch] = arch
|
145
|
+
end
|
146
|
+
|
147
|
+
def bits=(bits)
|
148
|
+
raise ArgumentError, "bits must be > 0 (#{bits} given)" unless bits > 0
|
149
|
+
@attrs[:bits] = bits
|
150
|
+
end
|
151
|
+
|
152
|
+
def bytes
|
153
|
+
bits / 8
|
154
|
+
end
|
155
|
+
|
156
|
+
def bytes=(bytes)
|
157
|
+
self.bits = bytes * 8
|
158
|
+
end
|
159
|
+
|
160
|
+
def endian=(endian)
|
161
|
+
endian = ENDIANNESSES[endian.downcase]
|
162
|
+
raise ArgumentError, "endian must be one of #{ENDIANNESSES.sort.inspect}" if endian.nil?
|
163
|
+
@attrs[:endian] = endian
|
164
|
+
end
|
165
|
+
|
166
|
+
def log_level=(value)
|
167
|
+
log_level = nil
|
168
|
+
case value
|
169
|
+
when String
|
170
|
+
value = value.upcase
|
171
|
+
log_level = Logger.const_get(value) if LOG_LEVELS.include?(value)
|
172
|
+
when Integer
|
173
|
+
log_level = value
|
174
|
+
end
|
175
|
+
raise ArgumentError, "log_level must be an integer or one of #{LOG_LEVELS.inspect}" unless log_level
|
176
|
+
@attrs[:log_level] = log_level
|
177
|
+
end
|
178
|
+
|
179
|
+
def os=(os)
|
180
|
+
os = os.downcase
|
181
|
+
raise ArgumentError, "os must be one of #{OSES.sort.inspect}" unless OSES.include?(os)
|
182
|
+
@attrs[:os] = os
|
183
|
+
end
|
184
|
+
|
185
|
+
def signed=(value)
|
186
|
+
signed = nil
|
187
|
+
case value
|
188
|
+
when String
|
189
|
+
signed = SIGNEDNESSES[value.downcase]
|
190
|
+
when true, false
|
191
|
+
signed = value
|
192
|
+
end
|
193
|
+
if signed.nil?
|
194
|
+
raise ArgumentError, "signed must be boolean or one of #{SIGNEDNESSES.keys.sort.inspect}"
|
195
|
+
end
|
196
|
+
@attrs[:signed] = signed
|
197
|
+
end
|
198
|
+
|
199
|
+
# TODO(Darkpi): #binary when we can read ELF.
|
200
|
+
end
|
201
|
+
|
202
|
+
@context = ContextType.new
|
203
|
+
|
204
|
+
class << self
|
205
|
+
attr_reader :context
|
206
|
+
end
|
207
|
+
|
208
|
+
# For include.
|
209
|
+
# @!visibility private
|
210
|
+
def context
|
211
|
+
::Pwnlib::Context.context
|
212
|
+
end
|
213
|
+
|
214
|
+
# @!visibility private
|
215
|
+
def self.included(base)
|
216
|
+
# XXX(Darkpi): Should we do this?
|
217
|
+
base.__send__(:private, :context)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/context'
|
4
|
+
require 'pwnlib/memleak'
|
5
|
+
require 'pwnlib/util/packing'
|
6
|
+
|
7
|
+
# TODO(hh): Use ELF datatype instead of magic offset
|
8
|
+
|
9
|
+
module Pwnlib
|
10
|
+
# DynELF class, resolve symbols in loaded, dynamically-linked ELF binaries.
|
11
|
+
# Given a function which can leak data at an arbitrary address,
|
12
|
+
# any symbol in any loaded library can be resolved.
|
13
|
+
class DynELF
|
14
|
+
PT_DYNAMIC = 2
|
15
|
+
DT_GNU_HASH = 0x6ffffef5
|
16
|
+
DT_HASH = 4
|
17
|
+
DT_STRTAB = 5
|
18
|
+
DT_SYMTAB = 6
|
19
|
+
|
20
|
+
attr_reader :libbase
|
21
|
+
|
22
|
+
def initialize(addr, &block)
|
23
|
+
@leak = ::Pwnlib::MemLeak.new(&block)
|
24
|
+
@libbase = @leak.find_elf_base(addr)
|
25
|
+
@elfclass = { "\x01" => 32, "\x02" => 64 }[@leak.b(@libbase + 4)]
|
26
|
+
@elfword = @elfclass / 8
|
27
|
+
@unp = ->(x) { Util::Packing.public_send({ 32 => :u32, 64 => :u64 }[@elfclass], x) }
|
28
|
+
@dynamic = find_dynamic
|
29
|
+
@hshtab = @strtab = @symtab = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def lookup(symb)
|
33
|
+
@hshtab ||= find_dt(DT_GNU_HASH)
|
34
|
+
@strtab ||= find_dt(DT_STRTAB)
|
35
|
+
@symtab ||= find_dt(DT_SYMTAB)
|
36
|
+
resolve_symbol_gnu(symb)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Function used to generated GNU-style hashes for strings.
|
42
|
+
def gnu_hash(s)
|
43
|
+
s.bytes.reduce(5381) { |acc, elem| (acc * 33 + elem) & 0xffffffff }
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_dynamic
|
47
|
+
e_phoff_offset = { 32 => 28, 64 => 32 }[@elfclass]
|
48
|
+
e_phoff = @libbase + @unp.call(@leak.n(@libbase + e_phoff_offset, @elfword))
|
49
|
+
phdr_size = { 32 => 32, 64 => 56 }[@elfclass]
|
50
|
+
loop do
|
51
|
+
ptype = @leak.d(e_phoff)
|
52
|
+
break if ptype == PT_DYNAMIC
|
53
|
+
e_phoff += phdr_size
|
54
|
+
end
|
55
|
+
offset = { 32 => 8, 64 => 16 }[@elfclass]
|
56
|
+
dyn = @unp.call(@leak.n(e_phoff + offset, @elfword))
|
57
|
+
# Sometimes this is an offset instead of an address
|
58
|
+
dyn += @libbase if (0...0x400000).cover?(dyn)
|
59
|
+
dyn
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_dt(tag)
|
63
|
+
dyn_size = @elfword * 2
|
64
|
+
ptr = @dynamic
|
65
|
+
loop do
|
66
|
+
tmp = @leak.n(ptr, @elfword * 2)
|
67
|
+
d_tag = @unp.call(tmp[0, @elfword])
|
68
|
+
d_addr = @unp.call(tmp[@elfword, @elfword])
|
69
|
+
break if d_tag.zero?
|
70
|
+
return d_addr if tag == d_tag
|
71
|
+
ptr += dyn_size
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def resolve_symbol_gnu(symb)
|
77
|
+
sym_size = { 32 => 16, 64 => 24 }[@elfclass]
|
78
|
+
# Leak GNU_HASH section header
|
79
|
+
nbuckets = @leak.d(@hshtab)
|
80
|
+
symndx = @leak.d(@hshtab + 4)
|
81
|
+
maskwords = @leak.d(@hshtab + 8)
|
82
|
+
|
83
|
+
l_gnu_buckets = @hshtab + 16 + (@elfword * maskwords)
|
84
|
+
l_gnu_chain_zero = l_gnu_buckets + (4 * nbuckets) - (4 * symndx)
|
85
|
+
|
86
|
+
hsh = gnu_hash(symb)
|
87
|
+
bucket = hsh % nbuckets
|
88
|
+
|
89
|
+
i = @leak.d(l_gnu_buckets + bucket * 4)
|
90
|
+
return nil if i.zero?
|
91
|
+
|
92
|
+
hsh2 = 0
|
93
|
+
while (hsh2 & 1).zero?
|
94
|
+
hsh2 = @leak.d(l_gnu_chain_zero + i * 4)
|
95
|
+
if ((hsh ^ hsh2) >> 1).zero?
|
96
|
+
sym = @symtab + sym_size * i
|
97
|
+
st_name = @leak.d(sym)
|
98
|
+
name = @leak.n(@strtab + st_name, symb.length + 1)
|
99
|
+
if name == (symb + "\x00")
|
100
|
+
offset = { 32 => 4, 64 => 8 }[@elfclass]
|
101
|
+
st_value = @unp.call(@leak.n(sym + offset, @elfword))
|
102
|
+
return @libbase + st_value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
i += 1
|
106
|
+
end
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/ext/helper'
|
4
|
+
require 'pwnlib/util/fiddling'
|
5
|
+
require 'pwnlib/util/packing'
|
6
|
+
|
7
|
+
module Pwnlib
|
8
|
+
module Ext
|
9
|
+
module Array
|
10
|
+
# Methods to be mixed into Array.
|
11
|
+
module InstanceMethods
|
12
|
+
extend ::Pwnlib::Ext::Helper
|
13
|
+
|
14
|
+
def_proxy_method ::Pwnlib::Util::Packing, %w(flat)
|
15
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(unbits)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Array.public_send(:include, ::Pwnlib::Ext::Array::InstanceMethods)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
module Pwnlib
|
4
|
+
module Ext
|
5
|
+
# Helper methods for defining extension
|
6
|
+
module Helper
|
7
|
+
def def_proxy_method(mod, *ms, **m2)
|
8
|
+
ms.flatten
|
9
|
+
.map { |x| [x, x] }
|
10
|
+
.concat(m2.to_a)
|
11
|
+
.each do |method, proxy_to|
|
12
|
+
class_eval <<-EOS
|
13
|
+
def #{method}(*args, &block)
|
14
|
+
#{mod}.#{proxy_to}(self, *args, &block)
|
15
|
+
end
|
16
|
+
EOS
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/ext/helper'
|
4
|
+
require 'pwnlib/util/packing'
|
5
|
+
|
6
|
+
module Pwnlib
|
7
|
+
module Ext
|
8
|
+
module Integer
|
9
|
+
# Methods to be mixed into Integer.
|
10
|
+
module InstanceMethods
|
11
|
+
extend ::Pwnlib::Ext::Helper
|
12
|
+
|
13
|
+
def_proxy_method ::Pwnlib::Util::Packing, %w(pack p8 p16 p32 p64)
|
14
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(bits bits_str), bitswap: 'bitswap_int'
|
15
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(hex)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Integer.public_send(:include, ::Pwnlib::Ext::Integer::InstanceMethods)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/ext/helper'
|
4
|
+
require 'pwnlib/util/fiddling'
|
5
|
+
require 'pwnlib/util/packing'
|
6
|
+
|
7
|
+
module Pwnlib
|
8
|
+
module Ext
|
9
|
+
module String
|
10
|
+
# Methods to be mixed into String.
|
11
|
+
module InstanceMethods
|
12
|
+
extend ::Pwnlib::Ext::Helper
|
13
|
+
|
14
|
+
def_proxy_method ::Pwnlib::Util::Packing, %w(unpack unpack_many u8 u16 u32 u64)
|
15
|
+
def_proxy_method ::Pwnlib::Util::Fiddling, %w(
|
16
|
+
enhex unhex urlencode urldecode bits bits_str unbits bitswap b64e b64d
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
::String.public_send(:include, ::Pwnlib::Ext::String::InstanceMethods)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/util/packing'
|
4
|
+
|
5
|
+
module Pwnlib
|
6
|
+
# MemLeak is a caching and heuristic tool for exploiting memory leaks.
|
7
|
+
class MemLeak
|
8
|
+
PAGE_SIZE = 0x1000
|
9
|
+
PAGE_MASK = ~(PAGE_SIZE - 1)
|
10
|
+
|
11
|
+
def initialize(&block)
|
12
|
+
@leak = block
|
13
|
+
@base = nil
|
14
|
+
@cache = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_elf_base(ptr)
|
18
|
+
ptr &= PAGE_MASK
|
19
|
+
loop do
|
20
|
+
return @base = ptr if n(ptr, 4) == "\x7fELF"
|
21
|
+
ptr -= PAGE_SIZE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Call the leaker function on address `addr`.
|
26
|
+
# Store the result to @cache
|
27
|
+
def do_leak(addr)
|
28
|
+
unless @cache.key?(addr)
|
29
|
+
data = @leak.call(addr)
|
30
|
+
data.bytes.each.with_index(addr) { |b, i| @cache[i] = b }
|
31
|
+
end
|
32
|
+
@cache[addr]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Leak `numb` bytes at `addr`.
|
36
|
+
# Returns a string with the leaked bytes.
|
37
|
+
def n(addr, numb)
|
38
|
+
(0...numb).map { |i| do_leak(addr + i) }.pack('C*')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Leak byte at ``((uint8_t*) addr)[ndx]``
|
42
|
+
def b(addr)
|
43
|
+
n(addr, 1)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Leak word at ``((uint16_t*) addr)[ndx]``
|
47
|
+
def w(addr)
|
48
|
+
Util::Packing.u16(n(addr, 2))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Leak dword at ``((uint32_t*) addr)[ndx]``
|
52
|
+
def d(addr)
|
53
|
+
Util::Packing.u32(n(addr, 4))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Leak qword at ``((uint64_t*) addr)[ndx]``
|
57
|
+
def q(addr)
|
58
|
+
Util::Packing.u64(n(addr, 8))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|