Friday 4 November 2016

CTF Writeup - Flare-On 2016 - 03: unknown


  • Name - unknown
  • Category - Reverse Engineering
  • Points - 3
  • Description - n/a
  • Binary - Download here

The binary for challenge 3 lacks an extension; exiftool reveals it's a standard win32 binary:

root@kali: ~/Desktop
root@kali:~/Desktop# exiftool unknown ExifTool Version Number : 9.74 File Name : unknown Directory : . File Size : 89 kB File Modification Date/Time : 2016:09:24 12:34:41-04:00 File Access Date/Time : 2016:10:16 07:49:40-04:00 File Inode Change Date/Time : 2016:10:16 07:49:40-04:00 File Permissions : rwxrw-rw- File Type : Win32 EXE MIME Type : application/octet-stream Machine Type : Intel 386 or later, and compatibles Time Stamp : 2016:07:31 20:00:00-04:00 PE Type : PE32 Linker Version : 12.0 Code Size : 62464 Initialized Data Size : 35840 Uninitialized Data Size : 0 Entry Point : 0x3771 OS Version : 5.1 Image Version : 0.0 Subsystem Version : 5.1 Subsystem : Windows command line root@kali:~/Desktop#


Add the .exe extension and run with the argument "some_random_input":

Command Prompt
C:\>unknown.exe some_random_input No rite arguhments! C:\>



Load it in IDA and search for the string "No rite arguhments!". This leads us to function sub_4027A0, at the bottom of which we have the following:


Now that we know where we need to end up, let's start tackling this function top-down:


The first basic block ensures that we have supplied 1 argument to the binary. The second basic block calls function __LDint with values 0x72 ('r') and the path to the executable, and returns a substring of the input path starting from the last 'r' encountered. For example if the path is C:\somefolder\unknown.exe, the returned string is r\unknown.exe. If the character 'r' is not present, the execution flow jumps to the "No rite arguhments!" basic block. This suggests that the path and/or name of the binary have to be some specific values, and hence why the binary was called unknown and lacked an extension.

Running strings on the binary we get an interesting entry:

root@kali: ~/Desktop
root@kali:~/Desktop# strings unknown | grep -i pdb C:\extraspecial.pdb root@kali:~/Desktop#

Change the name of the binary to extraspecial.exe; the location doesn't matter since the name contains an 'r', and the result of __LDint will always return raspecial.exe.

The binary then removes the first character of this string (which is always 'r') and passes it to sub_402760 which does some computation on it and returns an integer. As this function is used quite a few times throughout the binary, from now on I will be calling it singlify and can be implemented in the following way:


    def singlify (input_string):
        temp_ans = 0
        for char in input_string:
            temp_ans = temp_ans * 37 + ord(char)
            temp_ans = temp_ans & 0xffffffff
        return temp_ans


In essence, the binary then does the following:
  1. Singlifies the subpath (described above)
  2. Singlifies the argument we passed
  3. Computes length of argument we passed, and increments it
  4. Allocates memory of size 0x40000 and copies itself in this region
  5. Searches for string "RSDS" in this region (finds it twice)
  6. Copies the string located next to "RSDS"; let's call this the key
  7. Frees previously-allocated memory region

The retrieved key will always be the same as it is not dependent on our input. This key is then hashed X amount of times using MD5 and the resultant key used to RC4 encrypt/decrypt a fixed buffer in memory; where X is computed in the following manner:
  • Byte 1 : Always 0x00
  • Byte 2 : Length (argv[1]) + 1
  • Bytes 3-4 : (Singlify ( __LDint(binary path) ) & 0xFFFF) - 1

This might look complicated at first but essentially the number of iterations depend on only 2 things, the path of the binary and the length of argv[1]; and we already know what the former should be. Let's skip this section for the time being and check the next basic block that comes after the RC4:


If (length of argv[1] + 1) is equal to 0x1B, this basic block is skipped, else it will be execute and 0x1B will overwrite our original entry. This value is later used as a counter to loop over each character of argv[1]. And there we have it, (len(argv[1]) + 1) should be equal to 27, and hence len(argv[1]) should be equal to 26. So now we have all the ingredients to encrypt/decrypt the buffer to its intended state; as long as the binary is called extraspecial.exe and len(argv[1]) == 26, the buffer is going to remain the same. Let's move onto the last validation routine.

As we're right at the bottom let's take a bottom-up approach for a moment:


The yellow section is the part that is executed 0x1B times and is where the execution flow comes in from. We'll take a better look at it later. When the loop has ended, the control flow shifts to the purple box (loc_402C6C) at the top left.

To arrive at the green section and display the desired message we need to make sure that [ebp+singlify_path] is not zero (loc_402C97), hence xor eax, 0x0B019815A should not produce zero (purple block). Since eax at this point contains [ebp+singlify_path], we have to make sure that the blue box is avoided 0x1B times, else [ebp+singlify_path] will take the value of 0x0B019815A and we end up with the wrong message.

So now everything boils down to the following basic block which is repeated 0x1B times, once for each char of our input:


As a reminder, we need to avoid the bottom blue basic block. The algorithm in the yellow block does the following for each of the chars in argv[1]:
  1. Concatenates it with "[`abcdefghijklmnopqrstuvwxyz]Flare On!"
    • Chooses the next char from []; 1st round = `, 2nd round = a, 3rd = b, etc..
  2. Singlifies result
  3. Compares result with part of encrypted/decrypted buffer that was computed earlier

As the search space is tiny, we can brute-force each character individually:


    import string
    import struct
    import sys

    result = ["2F3E61EE","45EB79DE","3D2F1BAF","D7BB4787",
    "9CC49A73","AEF5A4C9","C1C53246","249B02A0",
    "595016D6","5194B7A6","BA239DE7","CE92AE8A",
    "181A9985","9958E0FE","94790C43","6FF3B91A",
    "8124C470","CF27BD05","6F6EFFC4","7C84775A",
    "B37792DD","FF3C8425","44A9DC5F","9628E48E",
    "C761E92A","DA3177A7"]

    nonce = "`abcdefghijklmnopqrstuvwxyz"

    def singlify(input_string):
        temp_ans = 0
        for char in input_string:
            temp_ans = temp_ans * 37 + ord(char)
            temp_ans = temp_ans & 0xffffffff
        return temp_ans


    for id in range(len(result)):
        for x in string.printable:
            test_string = x + nonce[id] + "FLARE On!"
            singlified_string = singlify(test_string)
            endian_string = format(struct.unpack("<I", struct.pack(">I", singlified_string))[0], 'x')
            if endian_string.upper()  == result[id]:
                sys.stdout.write(x)
                break


Testing the result:

Command Prompt
C:\>extraspecial.exe Ohs0pec1alpwd@flare-on.com yOU MAKE GOOD Arguhments! C:\>

3 comments:

  1. Hi, Apologies for my n00bness but I don't understand how did you get the hash for result in your script above?

    ReplyDelete
  2. Hey .. god question .. I didn't explain it fully. The 'result' string is basically the decrypted buffer desribed earlier. You can either get it

    1) straight after it has been properly decrypted (which happens when name of the binary = extraspecial.exe and len(argv[1]) == 26; or

    2) you can get it byte by byte from the compare statement which is the 2nd to last instruction in the yellow basic block

    ReplyDelete
  3. hi.. nice post.
    can you explain this part in the code?
    http://i.imgur.com/QwzsgsW.png

    ReplyDelete