Taking screen shots to obtain images from the game is a crude method of obtaining some images whilst the original Direct Draw Surface (DDS) texture file sits right under our noses in our games data directories. It would be nice if we could just open up these image files and convert them to PNG images. However, Ndoors has seen fit to obfuscate the headers of these files so that this cannot be done easily. Screen shots are also inferior because they are typically of a significantly lower resolution than the source image and even displayed in the wrong aspect ratio in some cases, not to mention the wasted disk space the game consumes when printing the screen and wasted time deleting them afterwards.
The goal of this discussion is to dissect the scrambled DDS format that Ndoors ships with the game with the aim of unscrambling it again and, as a result, obtain usable images. Once this has been achieved manually, a program could be created to automate the procedure of deobfuscating all texture files.
For the purpose of this discussion we will use the file
NMap\Yggdrasill4\Yggdrasill4.dds as our test subject. Everything stated herein is nothing more than an assumption, unless otherwise stated, and should be treated as such when trying to figure out the mysteries of the obfuscation.
Header format[edit source]
Although the full header is 128 bytes in length, Ndoors only obfuscates the first 32 bytes, so we will only be looking at the first 32 bytes from the beginning of the file.
The header format of a DDS file is documented at MSDN. This only indicates what information is stored in the header structure when programming using the DirextX API, but it gives us a good idea of what sort of thing to expect to find in the DDS file. When comparing this information with the small C program designed to write a DDS header, we can determine what each DWORD in the first 32 bytes represents. This information is shown in the table below.
|"DDS "||Header length||Flags||Height||Width||Texture0 byte count||Depth||Mipmap count|
Obfuscated header[edit source]
The following is the how Ndoor's texture header appears in
An initial examination of this header shows that every 4th byte has the value 0xFF (after the first four bytes) and this is typically preceded by bytes with the values 0x10 and 0x77. Since we know that the first 32 bytes of the header are made up of eight DWORDs, and the file is stored in little-endian format, we can assume that these are numbers that only have the first byte filled (values less than or equal to 0xFF). From this we could field the assumption that one way in which these values are being obfuscated is that for each DWORD, the 2nd, 3rd and 4th bytes are being transformed by increasing the values by 0x77, 0x10 and 0xFF respectively. However, this is purely guess work at this stage and needs to be proven.
Comparing headers[edit source]
Starting with what we know to be fact, a DDS header always begins with the string "DDS " followed by a DWORD of the value 124 (0x7C).
Contrasting this normal header with the obfuscated header we can see that bytes 5, 6 and 7, which we know must start off as zero bytes, follow the rules we already established which is that the 2nd, 3rd and 4th bytes of each DWORD are being transformed by increasing the values by 0x77, 0x10 and 0xFF respectively. Since we know that the first byte of this word should be 0x7C but is in fact 0xC0, we might try the assumption that the first byte of the DWORD is being decreased by 0x44. Since a typical DDS header has many zero bytes in it, to check if this might be correct, we can see if there are any 0x44 bytes at the start of any other DWORDS in this header; this reveals that bytes 12, 20 and 24 are all set to 0x44 and, being multiples of 4, are all aligned at the start of each DWORD. This, however, is unexpected because the difference between the expected value and the obfuscated value is negative 68, and 0 minus 68 is not 68 unless the sign is dropped, so this is something to keep in mind going forward.
However, in the first DWORD, bytes 1 and 2 remain unchanged between headers. Instead of reading "DDS " it now reads "NDS@" in the obfuscated header. A bit of investigation revealed that the game will happily read regular DDS files in place of any obfuscated "NDS" files. Considering that this is the "legible" portion of the file, from this we safely assume that the obfuscation pattern is different for the first four bytes, or rather, that the first four bytes are hard coded instead of being subject to any mathematical transformation. In this way, the game can determine whether it is dealing with a normal or obfuscated file and successfully use either format interchangeably. This tells us that the obfuscation transformation is only applied to bytes 4-31, and the first four are actually hard coded to "NDS@" instead of "DDS ", so we do not need to consider how these bytes have changed between headers.
Blind deobfuscation[edit source]
So far we have determined that each DWORD may be being transformed by a series of offsets applied to each byte as shown in the following table:
Attempt 1[edit source]
Let's try to apply these offsets to the next DWORD, in reverse, to get back to the original value, starting at byte 8 which is the flags value.
Our resulting value is 0x8F100A00, or 10100001000010001111 in 32-bit binary. We can compare this value to the flags that would be switched on according to the MSDN documentation and see if this value seems to make sense. The table below shows each of the possible flags, its value in hexadecimal and which corresponding bit it represents.
From this we can see that the following flags DDSD_CAPS, DDSD_HEIGHT, DDSD_WIDTH, DDSD_PITCH, DDSD_PIXELFORMAT, DDSD_LINEARSIZE are switched on. This includes all of the required flags, plus DDSD_PITCH and DDSD_LINEARSIZE. This looks promising initially, until examining the MSDN documentation for the two additional flags which reveals that one is required for specifying the pitch in a compressed texture and the other in an uncompressed texture. Whether the texture data is compressed or not, these two flags are clearly conflicting and cannot be valid together. Also note that there is no known flag for the third bit, but I have examined a few different valid DDS files from different sources and it seems that it is normal for the third bit to be switched on for no known reason.
Attempt 2[edit source]
[Not by original author] Given that last hint at "End of discussion", the author is referring to DWORDs when he says "int" - 32-bit values, little-endian. A little playing with both the flags and the other values comes up with the following possible formula:
- Hardcoded start at "NDS@" instead of "DDS "
- All header fields increased by 0xff107744
In the example, we get height 512, width 255 (proper "computery" values which make us happy), texture byte count 522240, depth 0 (good, since dds_depth is not set) and mipmap-count 3. The length is 124, which turns out to be correct.
The flags are not as expected - bit 4, DDSD_WIDTH, is not set when calculating this way, while bit 3 is set. Assuming a mistake on ndoor's part, you could just binary OR your flags with 0x0b to correct that, as an extra "bit 3 set" does no harm. I thought about subtracting 0xff107740 instead, but playing through scenarios, that would mess up flags if ndoor does have their flags wrong, adds 0xff107744, and chooses to correct the flags in future. In contrast, if I assume the flags are wrong, subtract the 0xff107744 value and OR with 0xb, and it turns out the flags were right after all and ndoor adds 0xff107740, then I am doing no harm to the flags. To assume addition of 0xff107744 and OR with 0x0B is thus the safe way to go.
Looking at a few other DDS files in my folders, this seems to work. I can open converted files in WTV and The Compressonator as well as ddsview.
End of discussion[edit source]
I have already figured out how to decrypt the DDS header but have run out of energy to continue a discussion of this level of detail. Suffice it to say, there is still a lot of good information here, and for the interested reader it should not be hard to figure out now. Here's a tip, though: where I was going wrong was looking at bytes. Look at ints. Enjoy!