Skip to content

Instantly share code, notes, and snippets.

@kms70847
Last active May 8, 2021 02:36
Show Gist options
  • Save kms70847/d09f991dc9394f2feb3aa3910f935447 to your computer and use it in GitHub Desktop.
Save kms70847/d09f991dc9394f2feb3aa3910f935447 to your computer and use it in GitHub Desktop.
A reverse-engineering of Aran-Fey's obfuscated script
#copied from https://chat.stackoverflow.com/transcript/message/52153354#52153354
class folly(zip(), zip(__name__), zip(), zip(__file__), zip()):
hel = help
locals()[r""u'''d
'''fr''] = __qualname__
exc = exit
def Tel(f, a, k):
return getattr(a[0], f)(*a[1:], **dict(tuple(k.items())[2:]))
class to_bytes(0x6f20776f726c, 0o6, f'b'"ig"u'', metaclass=Tel):
list(zip(folly, [{()}]))
class print(__builtins__, next(folly)[2], to_bytes.decode(), metaclass=Tel):
try:
folly(next)
except Exception as exc:
([sep, exc, end], __qualname__) = (folly)
#Simplification 1. identification of red herrings. Anything annotated here with a "v" or "^" is something that
#could be replaced with a placeholder without affecting the behavior of the program.
#the contents of these objects do not matter, as long as they are all zip objects.
# vvvvv vvvvvvvvvvvvv vvvvv vvvvvvvvvvvvv vvvvv
class folly(zip(), zip(__name__), zip(), zip(__file__), zip()):
#Doesn't matter what value is bound to `hel`
# vvvv
hel = help
locals()[r""u'''d
'''fr''] = __qualname__
# ^^^^^^^^^^^^
#value doesn't matter
#doesn't matter what name is bound, as long as a name is bound.
#`exc` was probably chosen to create confusion with the `exc`s in the final class.
#also doesn't matter what value is bound.
#vv vvvv
exc = exit
#not a red herring per se, but feel free to change the names in this function
def Tel(f, a, k):
return getattr(a[0], f)(*a[1:], **dict(tuple(k.items())[2:]))
class to_bytes(0x6f20776f726c, 0o6, f'b'"ig"u'', metaclass=Tel):
#contents of second argument to `zip` don't matter as long as the argument is a list of length one
# vvvv
list(zip(folly, [{()}]))
class print(__builtins__, next(folly)[2], to_bytes.decode(), metaclass=Tel):
try:
#can be any statement, as long as it raises an exception
#vvvvvvvvv
folly(next)
except Exception as exc:
#Can be any name that is automatically bound to a class' namespace. So, __module__ or __qualname__.
#The value that gets bound doesn't matter.
# vvvvvvvvvvvv
([sep, exc, end], __qualname__) = (folly)
#^ ^ ^ ^
#unnecessary parens
#Simplification 2. Some integer and string literals are obfuscated. Let's identify what they are.
z = zip()
class folly(z,z,z,z,z):
hel = None
#consecutive string literals, like r"" u'''d\n''', get concatenated into a single string literal.
#so this string is equivalent to r'' + u'''d\n''' + fr'', which is just "d\n".
# vvvvvvvv
locals()[r""u'''d
'''fr''] = None
cabbage = None
def Tel(f, a, k):
return getattr(a[0], f)(*a[1:], **dict(tuple(k.items())[2:]))
# The ordinal values of the bytes object b"o worl". We'll leave it like this for now.
# |
# | octal 6, which is just regular 6.
# | |
# | | "big", obfuscated with the same literal concatenation as folly's "d\n"
# | | |
# vvvvvvvvvvvvvv vvv vvvvvvvvvvv
class to_bytes(0x6f20776f726c, 0o6, f'b'"ig"u'', metaclass=Tel):
list(zip(folly, [None]))
class print(__builtins__, next(folly)[2], to_bytes.decode(), metaclass=Tel):
try:
1/0
except Exception as exc:
[sep, exc, end], __qualname__ = folly
#Simplification 3. Revealing some of the underlying mechanisms of class creation.
#(but don't worry, I'm skipping over the really boring parts that don't matter)
#One of the most unusual elements of this code is the classes that seemingly inherit from objects that are not types.
#This is usually impossible to do, because the `type(name, bases, namespace)` function is typically responsible for
#creating types from class definitions, and it crashes if any element of `bases` isn't a type.
#But `type` is never called here, because each class has a metaclass that does type's job.
z = zip()
#Let's use `folly` as our example class illustrating how class creation works.
# The class' name will be passed to the metaclass later.
# |
# | The base types* are evaluated, gathered into a tuple, and passed to the metaclass later.
# | (*but they don't _have_ to be types if the metaclass doesn't care, so we can use zip instances here)
# | |
# vvvvv vvvvvvvvv
class folly(z,z,z,z,z):
# ^
#If no metaclass is specified, Python chooses one based on the metaclasses of the class' bases.
#Normal classes usually supply types for bases, and most types have a metaclass of "type".
#but here our bases are zip instances. That means our metaclass is the `zip` type.
#The contents of the class block are executed.
#The locals() collection initially starts out as a two element dict containing the module's name and the class' name:
#{'__module__': '__main__', '__qualname__': 'folly'}
hel = None
locals()["d\n"] = None
cabbage = None
#locals is now {'__module__': '__main__', '__qualname__': 'folly', 'hel': None, 'd\n': None, 'cabbage': None}.
#this dict will be passed to the metaclass later, as its "namespace".
#The class block is over, and it is now "later". The class' name, bases tuple, and namespace dict are passed to the metaclass,
#and the result is bound to the class' name. In other words, it does:
#folly = zip(
# "folly",
# (z,z,z,z,z),
# {'__module__': '__main__', '__qualname__': 'folly', 'hel': None, 'd\n': None, 'cabbage': None})
def Tel(f, a, k):
return getattr(a[0], f)(*a[1:], **dict(tuple(k.items())[2:]))
#Same deal as with `folly`, except Python doesn't have to deduce the metaclass, because it's specified right here.
class to_bytes(0x6f20776f726c, 6, "big", metaclass=Tel):
list(zip(folly, [None]))
#this class definition is essentially equivalent to:
#list(zip(folly, [None]))
#to_bytes = Tel("to_bytes", (0x6f20776f726c, 6, "big"), {'__module__': '__main__', '__qualname__': 'to_bytes'})
#Same deal as the other classes, but watch out for the surprising behavior of Exception variables.
class print(__builtins__, next(folly)[2], to_bytes.decode(), metaclass=Tel):
try:
1/0
except Exception as exc:
[sep, exc, end], __qualname__ = folly
# ^^^
#despite being assigned to, `exc` doesn't show up in the final namespace that's passed to the metaclass.
#this is because an except block's exception variable is always deleted at the end of the block.
#Taking exception name trickery into account, this class definition is equivalent to:
#bases = (__builtins__, next(folly)[2], to_bytes.decode())
#[sep, _, end], __qualname__ = folly
#print = Tel("print", bases, {'__module__': '__main__', '__qualname__': __qualname__, 'sep': sep, 'end': end})
#notice that this is the only class where I broke out `bases = ...` onto its own line.
#This is because this is the only class whose base list has a side effect -- the next() call.
#It is important that this next() call occurs before `folly` is unpacked in the body.
#Simplification 4. Analysis of the behavior of Tel.
z = zip()
folly = zip(
"folly",
(z,z,z,z,z),
{'__module__': '__main__', '__qualname__': 'folly', 'hel': None, 'd\n': None, 'cabbage': None}
)
#now that we can see where and how Tel is being called, we can deduce its behavior.
def Tel(f, a, k):
#since this is only called twice, we may as well just substitute in the arguments for both calls.
return getattr(a[0], f)(*a[1:], **dict(tuple(k.items())[2:]))
#The first call is equivalent to:
#return getattr(0x6f20776f726c, "to_bytes")(6, "big", **{})
#or:
#return (0x6f20776f726c).to_bytes(6, "big")
#the second call is equivalent to:
#return getattr(__builtins__, "print")(next(folly)[2], to_bytes.decode(), **{'sep': sep, 'end': end})
#or:
#return print(next(folly)[2], to_bytes.decode(), sep=sep, end=end)
#(but remember that next(folly)[2] must still occur before `folly` is unpacked for the last time)
#So essentially Tel just unscrambles its arguments into a method, an argument list, and a keyword argument dict,
#and calls the method with the arguments.
list(zip(folly, [None]))
to_bytes = Tel("to_bytes", (0x6f20776f726c, 6, "big"), {'__module__': '__main__', '__qualname__': 'to_bytes'})
bases = (__builtins__, next(folly)[2], to_bytes.decode())
[sep, _, end], __qualname__ = folly
print = Tel("print", bases, {'__module__': '__main__', '__qualname__': __qualname__, 'sep': sep, 'end': end})
#Simplification 5. Analysis of the `folly` iterable
z = zip()
folly = zip(
"folly",
(z,z,z,z,z),
{'__module__': '__main__', '__qualname__': 'folly', 'hel': None, 'd\n': None, 'cabbage': None}
)
#if you ignore the distinction between an iterable and a sequence, `folly` is essentially a 3x5 matrix that starts out as:
#[
# ["f", z, "__module__"],
# ["o", z, "__qualname__"],
# ["l", z, "hel"],
# ["l", z, "d\n"],
# ["y", z, "cabbage"]
#]
#the only salient difference is that a zip object can only be iterated over once. So whenever you call `next`,
#or unpack it on the RHS of an assignment statement, etc, then the top of of the matrix permanently pops off.
list(zip(folly, [None]))
#this line exists purely to pop the first two rows out of `folly`.
#The first iteration of this `zip` call pops out the first element of `folly` and [None]`,
#and joins them together into `(["f", z, "__module__"], None)`.
#The second iteration pops out the next element of `folly`. It tries to pop out the next element of `[None]`,
#but there's nothing left to pop, so it raises a StopIteration and the zip is finished.
#`["f", z, "__module__"]` is discarded because the list call is not assigned to anything.
#`["o", z, "__qualname__"]` is discarded because there was nothing to zip it with.
#The final result: `folly` is effectively equivalent to:
#[
# ["l", z, "hel"],
# ["l", z, "d\n"],
# ["y", z, "cabbage"]
#]
to_bytes = (0x6f20776f726c).to_bytes(6, "big")
#You can probably tell by now that this is just b"o worl".
x = next(folly)[2]
#pops ["l", z, "hel"] out of `folly` and assigns "hel" to x.
#`folly` is now effectively equivalent to:
#[
# ["l", z, "d\n"],
# ["y", z, "cabbage"]
#]
[sep, _, end], __qualname__ = folly
#`[...], __qualname__ = folly` pops out the final two elements of `folly`, assigning the first one to the list,
#and the second one to `__qualname__`.
#So this is effectively equivalent to:
#sep, _, end = ["l", z, "d\n"]
#__qualname__ = ["y", z, "cabbage"]
#we don't care about the value of `__qualname__` at this point, but we have to unpack folly's final value into something.
#so the assignments we actually care about are:
#sep = "y"
#end = "d\n"
print(x, to_bytes.decode(), sep=sep, end=end)
#Simplification 6. Assembling the final output.
to_bytes = b"o worl"
x = "hel"
sep = "l"
end = "d\n"
print(x, to_bytes.decode(), sep=sep, end=end)
#now we know the value of print's arguments, so we can substitute in the literals:
#print("hel", "o worl", sep="l", end="d\n")
#which is effectively equivalent to:
#print("hel" + "l" + "o worl" + "d")
#which is equivalent to... <please scroll to the next file dramatically>
print("hello world")
#thank you for coming to my TED talk.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment