Obfuscation Techniques: No More Type Hints

Obfuscation Techniques: No More Type Hints Featured Image

Type hinting is a nice tool that dynamic typing languages employ to make code more readable. As you can probably imagine, readability is not the goal with obfuscating code, so we ought to get rid of type hints when possible.

Table of Contents

The Benefits of Type Hinting Code

Many programming languages enforce some form of static typing. In those cases, declaring the type of a variable is required. However, many other programming languages have a dynamic typing system where the types are inferred through evaluation.

In the latter case, dynamic type systems are often easier to use because there’s less boilerplate and more flexibility. Of course, the cost associated with the lack of boilerplate is maintenance. How is anyone supposed to know the type of the variable without the type being declared explicitly? The answer: type hints.

One programming language with type hinting that comes to mind for me is Python, as I use it everyday. In Python, variable types are inferred, so there is no need to state the type of a variable. Prior to type hinting, the community often opted for explicit documentation to label the types of their variables. Alternatively, folks might have adopted naming conventions for specific data types to leave out ambiguity.

Eventually, Python began supporting type hints, so you can now specify the types of your variables explicitly. Here’s an example:

def bob_do_something(p: Player): pass

In other words, now parameters (among other data types) can include a type hint alongside the parameter name. These are handy for a variety of reasons, but they help a ton with autocompletion in IDEs. In addition, they show up nicely in documentation, so folks know exactly what your API needs (e.g., my add_block methodOpens in a new tab.).

Abandon All Types

Ultimately, if type hints help use make sense of our code, surely removing them will make code harder to read. Without type hints, how do we know what type a method returns? And in fact, languages like Python don’t enforce a particular return type, so a method could return anything. To make matters worse, a method can return nothing (i.e., void), and the lack of type hints removes this clarity from the code. Similarly, without type hints, how do we know what types a method will accept? The result is an incredibly aggravating sequence of trial and error until the method does something of value.

All of that is to say that we can really get some mileage out of our obfuscation by explicitly leaving out type hints. And I doubt you would get called out for it as tons of Python folks don’t use them to begin with.

To start, let’s look at an API I put together for a project; it generates markdown from Python code. For example, here’s a bit of the API showing how to create a document and begin editing it:

import snakemd

doc = snakemd.new_doc()
doc.add_heading("Why Use SnakeMD?")
doc.dump("README")

From this example, there are no type hints. However, if you’re reading the documentation, you’ll know that new_doc() returns a Document object. You will also know that the add_heading() method takes a string and appends it to the document as a heading. And because the API is open source, you can even dig into the source code to see how it’s done:

def add_heading(self, text: str, level: int = 1) -> Heading:
    """
    A convenience method which adds a heading to the document:

    .. doctest:: document

        >>> doc = snakemd.new_doc()
        >>> doc.add_heading("Welcome to SnakeMD!")
        Heading(text=[Inline(text='Welcome to SnakeMD!',...)], level=1)
        >>> print(doc)
        # Welcome to SnakeMD!

    :param str text:
        the text for the heading
    :param int level:
        the level of the heading from 1 to 6
    :return:
        the :class:`Heading` added to this Document
    """
    heading = Heading(Inline(text), level)
    self._elements.append(heading)
    logger.info("Added heading to document: %r", heading)
    return heading

Now, this particular sample code is fully documented, includes doctests, contains logging, and has type hints. We aren’t really focused on most of those, so I will strip them away and leave the type hints:

def add_heading(self, text: str, level: int = 1) -> Heading:
    heading = Heading(Inline(text), level)
    self._elements.append(heading)
    return heading

With the clutter out of the way, it’s clear that this method takes a string called “text” and an optional “level.” In addition, it returns a “Heading”. We can strip these type hints, so it’s quite a bit more difficult to figure out what the method is doing:

def add_heading(self, text, level = 1):
    heading = Heading(Inline(text), level)
    self._elements.append(heading)
    return heading

It’s subtle, but with the type hints gone, it’s unclear what the parameters are expecting and if the method returns anything at all. Combining this technique with some of the others we’ve included in this series would result in some truly diabolical code.

Layering Obfuscation Techniques

Since we’re already five deep in this series, I figured I’d take a moment to really scramble the example code above using some of the techniques we’ve already discussed. For example, now that the type hints are gone, we can go ahead and start adding some malicious comments:

def add_heading(self, text, level = 1):
    # adds a heading to the current table
    heading = Heading(Inline(text), level)
    self._elements.append(heading)
    return heading

In this case, heading refers to one of the six main HTML heading tags (e.g., H1, H2, H3, etc.). Instead, we’ve suggested that actually this method adds a “heading” to a table, which I believe are actually called “headers,” so this will just add to the confusion.

While we’re at it, I figure we can take a moment to shadow the built-in string function with the text parameter:

def add_heading(self, str, level = 1):
    # adds a heading to the current table
    heading = Heading(Inline(str), level)
    self._elements.append(heading)
    return heading

Then, we can stir up the readability by littering the code with visually similar characters:

def adcl_heading(self, str, leve1 = 1):
    # adds a heading to the current table
    l1ll = Heading(Inline(str), level)
    self._elements.append(l1ll)
    return l1ll

And surely we could take this even further, as I’ve done in the past, but we still have the rest of a series to write. Therefore, if you want to see me massacre some more code, feel free to keep reading any of the following related articles:

Otherwise, you’re welcome to contribute a bit more by visiting my list of ways to grow the site. In either case, take care! I’ll hopefully see you back soon.

Obfuscation Techniques (6 Articles)—Series Navigation

In our field, everyone likes to talk about best practices, but it’s sometimes difficult to make the case for what is and isn’t a best practice. On the other hand, I think it’s very easy to come up with ways to make code worse, but who would want to read about ways to make their code bad? I’ll tell you who! People who want to obfuscate their code, or at least that’s what I tell myself. Regardless, that’s my cover story for putting together this new series, and I figure it’ll be a lot of fun.

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife, playing Overwatch and Phantasy Star Online 2, practicing trombone, watching Penguins hockey, and traveling the world.

Recent Posts