Assignment: OBFUSCATE

Changelog:

  • 17 Feb 2025: Note explicit that your changes should still make the game playable, and recommend testing the inputting a number for a move works.
  • 20 Feb 2025: clarify that inputting a number into the game counts as working; mention explicit that bugs in determining when there are ties are in the orignal
  • 21 Feb 2025: correct “memory dump” to “dump memory” to match actual GDB commands

Your Task

  1. For this assignment, we have three different versions of a password-protected tic-tac-toe-like program. The three versions vary in how the code is obscured to prevent analysis. For each of the versions, your task is to produce a modified version that disables the password check, either by removing it or by making it accept the password “password”.

    [added 17 Feb 2025, clarified 20 Feb 2025:] Your changes should still allow a row/column number to be inputted into the corresponding game. The original game has a bug in determining when there are ties; you are not expected to fix/change this.

  2. The first version is ttt1.exe, submit a modified executable called ttt1-modified.exe.

  3. Do the same for the second and third versions, except with appropriate filenames.

  4. Submit each of your modified versions to the submission site. Submit C code for the versions provided as C code and binaries for the versions provided as binaries.

  5. In a text file, readme.txt briefly describe how you produced each of the modified versions. Submit this file.

How the versions are produced

  1. The first version is a compiled and stripped (debugging information/function names removed) version, with no attempt at obfuscation.

  2. The second version is run through the following Tigress transformations:

    • Merge (to combine most functions, including a cryptographic hashing library)
    • Flatten (to obscure control flow in the combined function)
    • CleanUp (rename functions and variables to hide details)
  3. The third version is compiled normally, but then code is “encrypted”. The start location of the executable runs the “decrypter”.

Hints

General Hints

  1. You do not need to understand a vast majority of the code in order to change the password check.

    If the program is running code like

    result = check_password(...);
    if (result) {
        fail;
    } else {
        play game;
    }
    

    and you can make it accept the password “password” by editing the compare and jump instructions that make up the if statement (for example, to ignore the value of result), by changing the result value just before if statement or changing what function is called, or by moving the play game code elsewhere or probably in some other ways.

  2. We’ve avoided any transformations that would add unnecessary conditional jumps/if statements. In real obfuscated, it’s common to add extra conditionals that are always true or always false in order to complicate analysis. (If we had done this, you’d most likely need to spend extra time eliminating these added conditionals, probably by paying careful attention to where the entered password is used.)

  3. You can identify interesting places in the executables by looking for calls to standard library functions like fgets, and printf.

First Program

  1. I recommend identifying where input is read from. There is a relevant call to fgets. Alternately, you can look for where relevant error messages are output and work backwards.

  2. You can look at the strings in the executable to find relevant parts of the code.

    Notably, you can look for strings you see printed out, and use them to identify where the relevant parts of the code are. (This is telling reason why “encrypting” strings is a common practice.)

    Most interesting strings are usually in the .rodata section. Once you figure out the address of a string, you can search for references for it in the disassembly.

    (Note that the real password does not appear as a constant string.)

  3. We’ve stripped out information from the executable about where functions are, but you can idenitfy where they are by looking at call and return instructions.

  4. Some helpful GDB commands:

    • b *0x122345 — set a breakpoint at a particular instruction

    • info registers — output all registers

    • x/i $pc — disassemble the current instruction

    • x/10i 0x123456 — disassemble 10 instructions at address 0x123456

    • x/10bx 0x123456 — print out the values of 10 memory bytes starting at address 0x123456

  5. You can overwrite code you don’t want to execute with nop instructions (opcode 90 hexdecimal).

    You can also use Ghidra’s “patch” feature (right-click in the “Listing” window, then select “Patch instruction”). Then you can export the program from the file menu, being sure to select “original file” as the format to use for the export.

  6. You can use Ghidra, or a program like ghex (available on department machines via NoMachine) to edit the executable, or a technique similar to what you did for TRICKY.

Second Program

  1. In the second program, Tigress’s Flatten transformation converts the function containing the if statement to a loop like:

    int place = 0;
    while (true) {
        switch (place) {
        case 0:
            ...; place = ...;
        case 1:
            ...; place = ...;
        ...
        }
    }
    

    This structure should be apparent in the C code. You can look for the assignment(s) to place that correspond to the original if statement and change them.

  2. To compute the new value of place, the optimizer often avoids conditionals. For example, to do the equivalent of

    if (x == 0)
        place = 12;
    else
        place = 7;
    

    the generated code might look like

    place = ((x == 0) * 5) + 7
    

    or similar. This is something the compiler’s optimizer is doing (not actually Tigress).

  3. We haven’t done a transformation that obscures strings, so you can figure out where the prompt for the password is output by searching or strings in the binary. If we had obscured strings, you could have done this using a debugger instead.

  4. You should be able to identify the interesting cases of the large switch statement and use this to avoid analyzing a vast majority of it.

Third Program

  1. Examine the decryptor code via objdump and/or Ghidra.

  2. Get a copy of the decrypted machine code:

    1. One way to do this is to figure out what the decrypter code does and write your own version that acts on the executable file. You could then either make a modified version of the executable file to get something that’s easier to analyze or you could create a raw binary file containing machine code.

      To do this approach, you would need to know how to find the code+data that is loaded into memory in the executable that the decrypter code “decrypts”. Like with the TRICKY assignment, you can do this by looking at the headers (as in with objdump --all-headers) and checking the LOAD program header entries.

    2. Another way to do this is to run the decrypter in a debugger (setting a breakpoint before it jumps to the decrypted code) then use a command like (in GDB):

      dump memory out.bin 0x123456 0x234567
      

      to dump the contents of memory from address 0x123456 through 0x234567 to out.bin.

    If you end up getting a raw binary file containing machine code, see below regarding importing it into Ghidra.

  3. To actually produce the modified binary:

    1. One option is to figure what part of the machine code to modify, then edit those bytes of the binary, taking into account the “encryption”.

      The “encryption” in ttt3.exe is based on xor’ing data with a “key”. It’s helpful to know that (x XOR y) XOR y = x. This means that the decryption operation can also be used as a (re-)encryption operation.

    2. Another option is to produce a version of the binary with the “encryption” removed — overwrite the encrypted bytes with their decrypted version and replace the “decrypter” with nops.

importing raw machine code + executable into Ghidra

If you have the machine code as a raw binary file and you want to load it alongside the origianl exceutable, Ghidra supports this operation, but it’s not obvious how to do this. After loading the original executable, you can add a binray file by selecting File > Add to Program and loading the additional file as “Raw binary”. Then select “options” and choose an appropriate base address to load the file into.

This, however, won’t let you load the binary file at overlapping addresses with the original executable. One way to deal with this is to go into the “Memory map” view and delete (using the red X) the memory mappings corresponding to the overlapping addresses, then import the corrected memory image.

After you import into Ghidra, you may want to rerun its analysis with “Analyze all open” from the “Analysis” menu.