Ouch my eye!

Do not look at LASER with remaining eye!


No REServations

In my last post we took on the .RES Container File format from Nova Logic. In that post I mentioned that there was a 2nd variant of the format that we see with F-22 Lightning II” (1996) from Nova Logic. While working on the original format we found with “Comanche 3”, I took a quick peek and saw this version is quite similar, but there are some notable differences. In this post we will take a deeper look at this other variant and see if we can figure it out fully as well.

First Look

Just like the last time, we’ll start by looking at the file in a hex viewer. But first just as a reminder, below is what we saw with the variant we see with “Comanche 3” and “Armored Fist 2”

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
00000000  52 45 53 4F 55 52 43 45  31 0D 0A 1A DD 04 00 00  RESOURCE1.......
00000010  E9 9B A0 E3 83 93 A4 FF  AD DE ED AC F0 4D 00 00  .............M..
00000020  E9 9B A0 E3 83 93 A3 F9  AD DE ED AC B1 62 00 00  .............b..
00000030  E9 9B A0 E3 83 8C A8 EF  AD DE ED AC 68 82 00 00  ............h...
00000040  E7 91 B4 FC E2 8C B9 82  E9 8C BB AC 6A AC 00 00  ............j...
00000050  E9 9B A0 E3 83 8E A2 FF  AD DE ED AC 3E AE 00 00  ............>...
00000060  E9 9B A0 E3 83 97 A3 EA  AD DE ED AC 82 D1 00 00  ................
00000070  E9 9B A0 E3 83 94 A4 E1  AD DE ED AC 7E D3 00 00  ............~...

Header:
"RESOURCE1": Identifier string
0D 0A:       CR+LF
1A:          SUB (DOS end of text stream marker)
DD 04 00 00: 0x000004DD (1245) Count

Resource Records:
XX XX XX XX XX XX XX XX XX XX XX XX: Obfuscated resource name (12 bytes)
XX XX XX XX: Offsets

Now that we’ve refreshed our memories, let’s take a look at the new variant. First we’ll start with the header.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F 
00000000  52 45 53 4F 55 52 43 45  32 78 78 78 DC 03 00 00  RESOURCE2xxx....
00000010  6E EE DF E1 9D EB C3 E0  EF 92 ED AC 00 50 00 00  n............P..
00000020  00 00 00 00 7C 9D A6 E1  E4 8D DF 82 E1 9C A1 AC  ....|...........
00000030  00 68 3B 00 00 00 00 00  7C 9D A6 E1 E4 8D DE 82  .h;.....|.......
00000040  E1 9C A1 AC 00 78 6C 00  00 00 00 00 7C 9D A6 E1  .....xl.....|...
00000050  E4 8D D9 82 E1 9C A1 AC  00 58 98 00 00 00 00 00  .........X......
00000060  7C 9D A6 E1 E4 8D D8 82  E1 9C A1 AC 00 88 CD 00  |...............
00000070  00 00 00 00 6E EE DC E1  9D EF C3 E0 EF 92 ED AC  ....n...........
00000080  00 88 09 01 00 00 00 00  6E EE DC E1 9D EC C3 E0  ........n.......
00000090  EF 92 ED AC 00 18 34 01  00 00 00 00 6E EE DC E1  ......4.....n...
000000A0  9D ED C3 E0 EF 92 ED AC  00 98 67 01 00 00 00 00  ..........g.....
000000B0  6E EE DC E1 9D EA C3 E0  EF 92 ED AC 00 78 9C 01  n............x..
000000C0  00 00 00 00 6E EE DC E1  9D EB C3 E0 EF 92 ED AC  ....n...........
000000D0  00 18 C9 01 00 00 00 00  6E EE DC E1 9D E8 C3 E0  ........n.......
000000E0  EF 92 ED AC 00 E0 F6 01  00 00 00 00 6E EE DC E1  ............n...
000000F0  9D E9 C3 E0 EF 92 ED AC  00 A8 2A 02 00 00 00 00  ..........*.....

"RESOURCE2xxx": Identifier String (12 bytes)
DC 03 00 00:    0x000003DC (988) Count?

Well for starters the header sees to be in the same basic format. We have 12 bytes of identifier, same as version 1 (if we were to include the CRLF and SUB as part of the string). After the string identifier we have a 32 bit value that looks to be a record count. So far so good. However what follows does not appear to be the same structure as what we see in version 1. With the first version we saw everything line up on a 16 bit boundary, but here we see a pattern, but it seems to rotate a bit, meaning that the record size is likely different, and is not 16 bytes anymore. Let’s take a closer look to see what we can figure out.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F 
00000000  52 45 53 4F 55 52 43 45  32 78 78 78 DC 03 00 00  RESOURCE2xxx....
00000010  6E EE DF E1 9D EB C3 E0  EF 92 ED AC 00 50 00 00  n............P..
00000020  00 00 00 00 7C 9D A6 E1  E4 8D DF 82 E1 9C A1 AC  ....|...........
00000030  00 68 3B 00 00 00 00 00  7C 9D A6 E1 E4 8D DE 82  .h;.....|.......
00000040  E1 9C A1 AC 00 78 6C 00  00 00 00 00 7C 9D A6 E1  .....xl.....|...
00000050  E4 8D D9 82 E1 9C A1 AC  00 58 98 00 00 00 00 00  .........X......
00000060  7C 9D A6 E1 E4 8D D8 82  E1 9C A1 AC 00 88 CD 00  |...............
00000070  00 00 00 00 6E EE DC E1  9D EF C3 E0 EF 92 ED AC  ....n...........
00000080  00 88 09 01 00 00 00 00  6E EE DC E1 9D EC C3 E0  ........n.......
00000090  EF 92 ED AC 00 18 34 01  00 00 00 00 6E EE DC E1  ......4.....n...
000000A0  9D ED C3 E0 EF 92 ED AC  00 98 67 01 00 00 00 00  ..........g.....
000000B0  6E EE DC E1 9D EA C3 E0  EF 92 ED AC 00 78 9C 01  n............x..
000000C0  00 00 00 00 6E EE DC E1  9D EB C3 E0 EF 92 ED AC  ....n...........
000000D0  00 18 C9 01 00 00 00 00  6E EE DC E1 9D E8 C3 E0  ........n.......
000000E0  EF 92 ED AC 00 E0 F6 01  00 00 00 00 6E EE DC E1  ............n...
000000F0  9D E9 C3 E0 EF 92 ED AC  00 A8 2A 02 00 00 00 00  ..........*.....

XX … XX: 12 Byte obfuscated Resource name?
ED AC: Null bytes (obfuscated) / Leaked Obfuscation Key Bytes?
XX XX XX XX: Offset? 
00 00 00 00: Padding Bytes?

Assuming the records start right after the header like with version 1, we start off with the same pattern, 12 bytes of what look to be obfuscated name data. Looks to be the same as version 1 leaking the key with the trailing null values and the key appears to be the same. After the first we bytes, we have what could be an offset. However Instead of starting with the next record after the offset, we appear to have 32bits of 00‘s. If we look at the pattern formed by the 32 bit zero values we can see it shifts over by 4 bytes each time, meaning we likely have a record size of 20 bytes. Using the updated length of 20 bytes, we can see the pattern does appear to hold now. So the record appears to be the same as version 1, except there is an extra 32 bits appended onto the record. So far all we see is 00 in that field, so might be padding, or an unused field. Let’s take our count from the header and multiply that by our record size and see what is at that location to see if that appears to be the end of the index.

988 * 20 = 19760 + 16 (header length) ==> 19776 (0x004D40)

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
00004CF0  FA 8A BF 9C 9C F0 BD EF  F5 DE ED AC 00 88 5A 18  ..............Z.
00004D00  6A 17 00 00 FA 8A BF 9C  9F F0 BD EF F5 DE ED AC  j...............
00004D10  00 A0 5A 18 24 40 00 00  FA 8A BF 9C 9E F0 BD EF  ..Z.$@..........
00004D20  F5 DE ED AC 00 E8 5A 18  77 07 00 00 FA 8A BF 9C  ......Z.w.......
00004D30  99 F0 BD EF F5 DE ED AC  00 F0 5A 18 1B 06 00 00  ..........Z.....
00004D40  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00004D50  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00004D60  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
         ⋮                        ⋮                        ⋮
00004FE0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00004FF0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00005000  EB EC DF 82 FD 9F A6 AC  AD DE ED AC 14 50 00 00  .............P..
00005010  D8 51 02 00 EB EC DF F3  E9 9F BE E4 83 8E AE F4  .Q..............
00005020  00 A8 02 00 01 0F 00 00  33 44 50 4B 46 2D 32 32  ........3DPKF-22

00005000: offset of first data payload as indicated by first record in index

Well the count and size appear to work out. The first thing we see though is that unlike version 1, we do not have a “KYLE” end of index record, instead we see a large block of null data until we get to the offset where the first record indicated its data was located. We also see more of the obfuscation key bytes in the data at this point, so it look that it very likely is the same key as in version 1. The next thing we can see is that the additional 32 bit field in our record is no longer 00‘s, meaning that it is not likely to be padding or unused. My first thought would be that this field is the data size, but that doesn’t really hold with those first records that have a value of zero for that field. We can also see that adding that value to the offset does not result in the next offset. Although, the offsets all seem to have the least significant byte set to 00 indicating possibly that the data exists on at least a 256 byte boundary (possibly larger, if the next byte has some bits constantly zero as well). I guess it’s possible that these particular entries all have a size that is a multiple of 256, but scrolling back for quite a ways in ImHex with a pattern configured for the records, it appears to always be zero. This is a bit of a challenge because if files are padded out to some boundary, we would need to know what the true length is.


The Code

Well now that we think we have a good structure, we can write some code to parse the index, and then maybe we can figure out what is up with that extra 32bit field. The first thing is to define our structures. Borrowing from the v1 structures we already created in the last post, we simply need to add the additional 32bit value to the record, and to update our values for the header signature.

#define OBFUSCATE_KEY 0xACEDDEAD // 32 bit XOR key used to hide the filenames

typedef struct {
    union {
        uint32_t raw32[3];
        char name[12];
    };
    uint32_t offset;
    uint32_t unknown;
} nl_resource_v2_entry_t;

typedef struct {
    char sig[8];    // "RESOURCE"
    char ver;       // "2"
    char term[3];   // "xxx"
    uint32_t count; // number of entries in the file
    nl_resource_v2_entry_t entries[];
} nl_resource_v2_file_t;

The rest of the code, at this stage, is largely the same as what we already wrote for version 1. So with a quick update to the print statements, let’s see what we get when we try to parse the index. Like with Version 1 size is calculated based on the offset of the next item, with the exception of the last one, in which case we use the size of the RES file itself (EOF position).

NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE.RES'    File Size: 408614463 bytes
Version 2 resource catalog found
Resource catalog contains 988 entries
 Idx:     Resource     Size  Unknown  Offset
   1:   ?02M05.LBL  3872768        0  [@00005000]
   2:  ?CKMIS2.LBL  3215360        0  [@003b6800]
   3:  ?CKMIS3.LBL  2875392        0  [@006c7800]
   4:  ?CKMIS4.LBL  3485696        0  [@00985800]
   5:  ?CKMIS5.LBL  3932160        0  [@00cd8800]
   6:   ?01M01.LBL  2789376        0  [@01098800]
   7:   ?01M02.LBL  3375104        0  [@01341800]
   8:   ?01M03.LBL  3465216        0  [@01679800]
   9:   ?01M04.LBL  2924544        0  [@019c7800]
  10:   ?01M05.LBL  3000320        0  [@01c91800]
    ⋮              ⋮        ⋮         ⋮  
  50:         ?OOT       20        0  [@0b137800]
  51:    BIN01.BIN  2103276  2102796  [@0b137814]
  52:    BIN02.BIN  2103296  2102796  [@0b339000]
  53:    BIN03.BIN  2103296  2102796  [@0b53a800]
  54:    BIN04.BIN  2103296  2102796  [@0b73c000]
  55:   C01M01.ORF     4096     3390  [@0b93d800]
  56:   C01M01.REF    26624    24576  [@0b93e800]
  57:   C01M01.TXT     2048     1228  [@0b945000]
  58:   C01M02.ORF     6144     4666  [@0b945800]
  59:   C01M02.REF    26624    24576  [@0b947000]
  60:   C01M02.TXT     2048      730  [@0b94d800]
    ⋮              ⋮        ⋮         ⋮  
 980: USGREEN4.PCX    20380    18608  [@18591800]
 981:  VTNM_04.PAK     8192     6468  [@18596800]
 982:   VTNM_T.PCX     8192     7427  [@18598800]
 983: WHLWELL3.PCX    12288    10737  [@1859a800]
 984:   WTOWER.PAK    45056    44276  [@1859d800]
 985:    WTR01.PCX     6144     5994  [@185a8800]
 986:    WTR02.PCX    18432    16420  [@185aa000]
 987:    WTR03.PCX     2048     1911  [@185ae800]
 988:    WTR04.PCX     1599     1563  [@185af000]

?: Invalid Character

The first thing of concern is the invalid characters that are showing up with the first 50 entries. We’ll have to take a closer look as to what is going on there. The next thing that is clear is that the offsets seem to fall on a 2K (2048 byte [0x800]) boundary, with the exception of “BIN01.BIN” And is the first of the entries that does not have an invalid character at the start. It also appears that the “unknown” might actually be the size, as in each case the calculated size falls on the next 2048 byte boundary larger than the “unknown” value. So it’s probably safe to say that the unknown value is indeed the actual payload size. Now we just need to figure out what is going on with those first 50 entries. Why is their size 0, and why is the first character invalid. (the last entry does not have a calculated size that falls on a 2048 byte boundary, because it is terminated by the EOF of the .RES file)


Flag on the play

So how could we be getting an invalid character in the resource name strings? For the most part this is an indicator that the most significant bit of the character is set. All of the printable characters exist in the lower 127 possible values, meaning that the most significant bit (MSbit) should always be a zero. Let’s go back and look at the first few entries to see if we can figure out how this could be happening.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F 
00000010  6E EE DF E1 9D EB C3 E0  EF 92 ED AC 00 50 00 00  n............P..
00000020  00 00 00 00 7C 9D A6 E1  E4 8D DF 82 E1 9C A1 AC  ....|...........

XX … XX: 12 Byte obfuscated Resource name?
XX: First byte of obfuscated name
XX XX XX XX: Offset 
00 00 00 00: Size field

Now that is interesting. In these first entries, the first byte always has the MSbit clear, and all the other characters have it set (when obfuscated). If you recall the obfuscation key was 0xACEDDEAD, where in each byte of the key the MSbit is set, so when we XOR it it would be clear. Thus we would expect that all of the obfuscated bytes would have the MSbit set. Now in this case, because it is cleared in the encoded form, that means it would become set pushing it into the extended ASCII range, which are unprintable characters. (Yes in DOS these generally were printable as additional graphic symbols, but these generally would never be used in a filename). Since this bit should always be zero, I wonder if it isn’t being used as some sort of flag here. The fact that all the sizes for these files is zero as well, would indicate something special is going on here. So let’s update our code to mask off that bit, and note when it was set.

NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE.RES'    File Size: 408614463 bytes
Version 2 resource catalog found
Resource catalog contains 988 entries
 Idx:     Resource     Size  Unknown  Offset
   1:   C02M05.LBL  3872768        0  [@00005000] Flagged
   2:  QCKMIS2.LBL  3215360        0  [@003b6800] Flagged
   3:  QCKMIS3.LBL  2875392        0  [@006c7800] Flagged
   4:  QCKMIS4.LBL  3485696        0  [@00985800] Flagged
   5:  QCKMIS5.LBL  3932160        0  [@00cd8800] Flagged
   6:   C01M01.LBL  2789376        0  [@01098800] Flagged
   7:   C01M02.LBL  3375104        0  [@01341800] Flagged
   8:   C01M03.LBL  3465216        0  [@01679800] Flagged
   9:   C01M04.LBL  2924544        0  [@019c7800] Flagged
  10:   C01M05.LBL  3000320        0  [@01c91800] Flagged
    ⋮              ⋮        ⋮         ⋮  
  50:         ROOT       20        0  [@0b137800] Flagged
  51:    BIN01.BIN  2103276  2102796  [@0b137814]
  52:    BIN02.BIN  2103296  2102796  [@0b339000]
  53:    BIN03.BIN  2103296  2102796  [@0b53a800]
  54:    BIN04.BIN  2103296  2102796  [@0b73c000]
  55:   C01M01.ORF     4096     3390  [@0b93d800]
  56:   C01M01.REF    26624    24576  [@0b93e800]
  57:   C01M01.TXT     2048     1228  [@0b945000]
  58:   C01M02.ORF     6144     4666  [@0b945800]
  59:   C01M02.REF    26624    24576  [@0b947000]
  60:   C01M02.TXT     2048      730  [@0b94d800]
    ⋮              ⋮        ⋮         ⋮  
 980: USGREEN4.PCX    20380    18608  [@18591800]
 981:  VTNM_04.PAK     8192     6468  [@18596800]
 982:   VTNM_T.PCX     8192     7427  [@18598800]
 983: WHLWELL3.PCX    12288    10737  [@1859a800]
 984:   WTOWER.PAK    45056    44276  [@1859d800]
 985:    WTR01.PCX     6144     5994  [@185a8800]
 986:    WTR02.PCX    18432    16420  [@185aa000]
 987:    WTR03.PCX     2048     1911  [@185ae800]
 988:    WTR04.PCX     1599     1563  [@185af000]

Well that seems to have done the trick, we get reasonable characters back by simply masking that bit off, so I think it is safe to say that that bit is being used as a flag to indicate something. Now the “ROOT” entry has me thinking if this isn’t some sort of filesystem like structure here. Perhaps the flag is indicating these are sub-directories? At a quick glance I don’t see a recursive .RES like structure at the offsets for the flagged entries. Though some of the data does like like it could be more record entries. I just have no feel for how e can determine how many entries are there. I’ll stick a pin in that for a moment, and turn my attention to the actual data-files, just to make sure we don’t have any surprises there.


Data Inception

Since we can see that the last few files are .PCX image files, let’s take a peek at what the data looks like there. For simplicity I’ll look at the last entry, as it will be the easiest to locate.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
185AF000  EF 97 A3 9C 9C F0 AF E5  E3 DE ED AC 14 78 13 0B  .............x..
185AF010  0C 16 20 00 0A 05 01 08  00 00 00 00 1F 00 1F 00  .. .............
185AF020  20 00 20 00 00 00 00 00  00 00 00 00 00 00 00 00   . .............
185AF030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
185AF040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
185AF050  00 00 00 00 00 01 20 00  00 00 00 00 00 00 00 00  ...... .........
185AF060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
185AF070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
185AF080  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
185AF090  00 00 00 00 E0 00 CF 00  67 6C CF 00 C7 00 C2 0A  ........gl......

EF … 00: (20 bytes) Resource index record entry?
0A: PCX Magic
05: PCX SW Version (05 = Version 3.0)
01: PCX Encoding (01 = RLE)
08: Bits per Pixel

Now that is interesting. For a PCX we would expect 0x0A to be the first byte, followed by the version number, most commonly 0x05. However we don’t see that sequence until 20 bytes into the data. The fact that it is a 20 byte offset, and the data looks like it could be an index record is curious. Why would they replicate the data again here?

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
185AF000  EF 97 A3 9C 9C F0 AF E5  E3 DE ED AC 14 78 13 0B  .............x..
185AF010  0C 16 20 00 0A 05 01 08  00 00 00 00 1F 00 1F 00  .. .............

EF … AC: Obfuscated filename?
14 78 13 0B: Offset? 0x0b137814
0C 16 20 00: Size? 0x0020160c (2102796)

Neither the offset or the size make sense for this entry. However looking at the offset value, that is much earlier in the file. Looking at our index earlier that is actually the offset for “BIN01.BIN” The size value matches that for BIN01 as well, strange, let’s look at the previous entry.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
185AE800  FA 8A BF 9C 99 F0 BD EF  F5 DE ED AC 00 F0 5A 18  ..............Z.
185AE810  1B 06 00 00 0A 05 01 08  00 00 00 00 1F 00 1F 00  ................

FA … AC: Obfuscated filename of next entry ==> "WTR04.PCX"
00 F0 5A 18: Offset of next entry
1B 06 00 00: Size of next entry

Okay now it is all coming together now, It seems that each data payload is prefixed with the name, offset, and size of the next file in the list, and it appears to be circular with the last item pointing back to the first. I wonder if this doesn’t have something to do with the sub-directory structure that the flags are indicating. Still curious that we would have two different, but redundant, data structures overlayed on top of each other. let’s take a closer look at that “ROOT” entry, as it appears right before BIN01, which is the first in the list.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
0B137800  EF 97 A3 9C 9C F0 AF E5  E3 DE ED AC 14 78 13 0B  .............x..
0B137810  0C 16 20 00 EF 97 A3 9C  9F F0 AF E5 E3 DE ED AC  .. .............
0B137820  00 90 33 0B 0C 16 20 00  80 00 00 00 00 00 00 07  ..3... .........
0B137830  00 40 00 07 00 80 00 07  00 C0 00 07 00 00 01 07  .@..............

EF … AC: Obfuscated filename of first entry ==> "BIN01.BIN"
14 78 13 0B: Offset of first entry 0x0B137814
0C 16 20 00: Size of first entry

EF … AC: Obfuscated filename of next entry ==> "BIN02.BIN"
00 90 33 0B: Offset of next entry
0C 16 20 00: Size of next entry
80 00 00 00: Payload data for first (current) entry

The structure is all starting to make sense now. The purpose for the prefixed records is to facilitate the sub-directory structure. Though curious why they didn’t just do a nested structure, similar to the primary index. Either way, we appear to have a single forward linked list for each “directory”, with the last entry pointing back to the first. There appears to be no way to determine the number of entries in a given directory list, short of walking it. To prevent an infinite loop the fist entry needs to be tracked, so that when we see the “next” value point back to it, we know we need to stop.

Now lets go back and look at the ROOT entry data in the primary index which is located at 0x000003E4. Adding the new context as we now understand it.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
000003E0  00 00 00 00 7F 91 A2 F8  AD DE ED AC AD DE ED AC  ................
000003F0  00 78 13 0B 00 00 00 00  EF 97 A3 9C 9C F0 AF E5  .x..............

7F … AC: Obfuscated entry name ==> "ROOT" Flagged as directory
00 78 13 0B: Offset of First Directory Entry
00 00 00 00: Size Field, unused for directories

The structures all make sense now. The only oddity is the redundancy of the index/name data. The primary index could have been just the directory entries, without the replication of the all of the “ROOT” file name data.


Walking the tree

Now that we’ve determined the overlayed structure, we can update our code accordingly.

        for(int i = 0; i < hdr.count; i++) {
            // de-obfuscate the entry name
            for(int j = 0; j < 3; j++) {
                dir2[i].raw32[j] ^= OBFUSCATE_KEY;
            }
            
            // determine if we have a directory or not
            bool is_dir = false;
            if( 0 != (dir2[i].name[0] & 0x80)) {
                dir2[i].name[0] &= ~0x80; // clear the dir flag bit
                is_dir = true;
            }

            fseek(fi, dir2[i].offset, SEEK_SET); // seek to the position of the entry
            // read in the prefixed directory entry data
            nl_resource_v2_entry_t chain;
            nr = fread(&chain, sizeof(nl_resource_v2_entry_t), 1, fi);
            if(1 != nr) {
                printf("Error reading file index\n");
                ERREXIT(-1);
            }
            for(int j = 0; j < 3; j++) {
                chain.raw32[j] ^= OBFUSCATE_KEY;
            }

In the main index, for non-directory entries, we can carry on the same as we did with v1, and safely ignore the chain data. The only other difference from v1 here is that we have the size in the structure, instead of needing to compute it. However if it is a directory entry we need to follow the linked list to see all the files.

            if(true == is_dir) {
                uint32_t first = chain.offset; // track the first entry
                int n = 0;
                do {
                    fseek(fi, chain.offset, SEEK_SET); // seek to the position of the entry

                    // preserve any needed data for the current entry here (size/name)

                    // read in the next entry
                    nr = fread(&chain, sizeof(nl_resource_v2_entry_t), 1, fi);
                    if(1 != nr) {
                        printf("Error reading file index\n");
                        ERREXIT(-1);
                    }
                    for(int j = 0; j < 3; j++) {
                        chain.raw32[j] ^= OBFUSCATE_KEY;
                    }
                  
                    // work with the current file data here
 
                } while(first != chain.offset);
            }

And that’s pretty much it for parsing the structure.


Anything worth doing, is worth doing 50 times

All that’s left now is to run our code against the data and see what we get.

NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE.RES'    File Size: 408614463 bytes
Version 2 resource catalog found
Resource catalog contains 988 entries
   1:   C02M05.LBL        0  [@00005000] Directory
           F22.PAK   152024  [@00005014]
      F22_DASH.PCX     3841  [@0002a800]
       F22SCRN.PCX     8608  [@0002b800]
       F22TOPS.PCX     7550  [@0002e000]
      F22TOPSD.PCX     8215  [@00030000]
       F22SCRS.PCX     9371  [@00032800]
      DASHDARK.PCX    17434  [@00035000]
          DASH.PCX    17434  [@00039800]
      F22SCRBT.PCX     9371  [@0003e000]
       SIDEPL2.PCX    12517  [@00040800]
    ⋮               ⋮       ⋮  
       PAL0204.PCX    75125  [@00393800]
          SKY5.BMP    66616  [@003a6000]
   2:  QCKMIS2.LBL        0  [@003b6800] Directory
         ABA_H.PAK    32712  [@003b6814]
         ABA03.PCX    18569  [@003be800]
    ⋮               ⋮       ⋮  
         SPLAT.PAK      744  [@0b123000]
        SPLAT1.PCX     5182  [@0b123800]
        SPLAT2.PCX     7465  [@0b125000]
          SKY5.BMP    66616  [@0b127000]
  50:         ROOT        0  [@0b137800] Directory
         BIN01.BIN  2102796  [@0b137814]
         BIN02.BIN  2102796  [@0b339000]
         BIN03.BIN  2102796  [@0b53a800]
         BIN04.BIN  2102796  [@0b73c000]
    ⋮               ⋮       ⋮  
         WTR01.PCX     5994  [@185a8800]
         WTR02.PCX    16420  [@185aa000]
         WTR03.PCX     1911  [@185ae800]
         WTR04.PCX     1563  [@185af000]
  51:    BIN01.BIN  2102796  [@0b137814] 
  52:    BIN02.BIN  2102796  [@0b339000] 
  53:    BIN03.BIN  2102796  [@0b53a800] 
  54:    BIN04.BIN  2102796  [@0b73c000] 
    ⋮               ⋮       ⋮  
 984:   WTOWER.PAK    44276  [@1859d800] 
 985:    WTR01.PCX     5994  [@185a8800] 
 986:    WTR02.PCX    16420  [@185aa000] 
 987:    WTR03.PCX     1911  [@185ae800] 
 988:    WTR04.PCX     1563  [@185af000] 

Well that seemed to work perfectly. Technically we could have stopped parsing at the end of “ROOT” as the remaining main index entries are the same as those in ROOT. One thing I did notice is that most files have the same name and size in each directory, though they have a different data offset. I wonder if the data is identical or not, or is it just common names. Easy enough to check, we can add some code to perform a MD5 sum on the payload data for each resource, then we can compare the similarly named entries against each other.

NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE.RES'    File Size: 408614463 bytes
Version 2 resource catalog found
Resource catalog contains 988 entries
   1:   C02M05.LBL        0  [@00005000] Directory
        1:      F22.PAK   152024  [@00005014] 977326045e9b4611781750ce11404ea8
         ⋮               ⋮        ⋮           ⋮
      346:     SKY5.BMP    66616  [@003a6000] 5f42ab847e398333181f0d2fd2a98ca6
   2:  QCKMIS2.LBL        0  [@003b6800] Directory
        1:    ABA_H.PAK    32712  [@003b6814] 05d094a239660df84672e4af7849676b
         ⋮               ⋮        ⋮           ⋮
      284:     SKY5.BMP    66616  [@006b7000] 5f42ab847e398333181f0d2fd2a98ca6
   3:  QCKMIS3.LBL        0  [@006c7800] Directory
        1:       C5.PAK   113004  [@006c7814] 027b9f79330f9f6a6b2267e5c7d0e63b
         ⋮               ⋮        ⋮           ⋮
      247:     SKY5.BMP    66616  [@00975000] 5f42ab847e398333181f0d2fd2a98ca6
   4:  QCKMIS4.LBL        0  [@00985800] Directory
        1:    ABA_H.PAK    32712  [@00985814] 05d094a239660df84672e4af7849676b
         ⋮               ⋮        ⋮           ⋮
      284:     SKY5.BMP    66616  [@00cc8000] 5f42ab847e398333181f0d2fd2a98ca6
   5:  QCKMIS5.LBL        0  [@00cd8800] Directory
    ⋮               ⋮       ⋮  
  50:         ROOT        0  [@0b137800] Directory
        1:    BIN01.BIN  2102796  [@0b137814] 84df83cb2311ae262d3bfd6fcd629b55
         ⋮               ⋮        ⋮           ⋮
      850:     SKY5.BMP    66616  [@1849e000] 5f42ab847e398333181f0d2fd2a98ca6
         ⋮               ⋮        ⋮           ⋮
      938:    WTR04.PCX     1563  [@185af000] 6ffbf8286264057fd1e3993a9e38a3dc
  51:    BIN01.BIN  2102796  [@0b137814] (Pad:  480) 84df83cb2311ae262d3bfd6fcd629b55
    ⋮               ⋮       ⋮  
 900:     SKY5.BMP    66616  [@1849e000] (Pad:  968) 5f42ab847e398333181f0d2fd2a98ca6
    ⋮               ⋮       ⋮  
 988:    WTR04.PCX     1563  [@185af000] (Pad:   36) 6ffbf8286264057fd1e3993a9e38a3dc

Best I can tell is that all the files with the same names contain identical data, and some files are replicated up to 50 times. Not a particularly efficient storage. When discussing this with some friends, a couple of ideas came up. The first was for copy protection by making the file enormous. The second, and most likely option, was data grouping to minimize seeking on the CD-ROM for faster loading. Wasn’t something I considered, but certainly makes sense for the era the game is from.


Searching for Treasure

Now that we have the ability to extract the contents, we can start to look at the contents that are packaged within the .RES file. None of the PCX files have names that look terribly interesting. So picking one from random based on size, choosing a large size as it is more likely to be a larger image.

PAL0401.PCX

Not sure why there appears to be some corruption at the bottom of the image, otherwise it seems to decode fine. From the name I expected these just to be palettes, but they are HUGE (for a palette). Looking at the raw data for the file, it seems fine. And the fact that the palette comes AFTER the image data in a PCX file, and the colours look correct, it must be either an error in the RLE encoding, or perhaps a decoding error by Gimp. Nonetheless, it’s a nice image, let’s look at a few more of the “PAL” images, as they all seem to be reasonably large.

PAL0405.PCX

Once Again we see some corruption, but clearly this is data coming from 2 different images. I suspect this is at the source, and not a decode error. Perhaps these images were just intended for the palettes, but the image came along as a stowaway? Most of them decode fine, but several are corrupt like this one. Several are also the same base image data, but with different palettes.

PAL0102.PCX
PAL0103.PCX
PAL0105.PCX

Here’s a few more from the “PAL” images that looked interesting.

PAL0104.PCX
PAL0304.PCX
PAL0305.PCX

One last one from the “PAL” series, I like this shot with the HUD.

PAL0404.PCX

Not all the images are PCX, it appears they also used some Windows BMP files, as you may have noticed from our file comparison example. Here is the file we looked at.

SKY5.BMP

File Inception

Of course the .RES file doesn’t just hold images, but that is our primary interest. While scanning the files, I noticed there are actually two .RES files (though with custom extensions) inside our main “RESOURCE.RES” file.

 NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE.RES'    File Size: 408614463 bytes
Version 2 resource catalog found
Resource catalog contains 988 entries
   1:   C02M05.LBL        0  [@00005000] Directory
    ⋮               ⋮       ⋮  
 295: RESOURCE._01  8609484  [@15375000] 
 296: RESOURCE._02 32869830  [@15bab000] 

Of coarse we need to look into those to see what they might contain. I looked at the data and they both have the “RESOURCE2xxx” signature. Both seem to contain only a single ROOT sub-directory entry.

“RESOURCE._02” (I know I’m going backwards here) seems to contain mostly game play assets. Lot’s of images, but mostly small. It does contain some larger ones as well, but they appear to be repeats of what is in “RESOURCE._01”

NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE._02'    File Size: 32869830 bytes
Version 2 resource catalog found
Resource catalog contains 1067 entries
   1:         ROOT        0  [@00005380] Directory
        1:  22WINFM.MID     5710  [@00005394] 
        2: A3INTRFM.MID     3342  [@00006a00] 
         ⋮               ⋮       ⋮           
     1065: PLYRSTAT.PCX   171551  [@01f005e0] 
     1066:    VOTER.PCX   190850  [@01f2a420] 
   2:  22WINFM.MID     5710  [@00005394]  
   3: A3INTRFM.MID     3342  [@00006a00]  
    ⋮               ⋮       ⋮  
1066: PLYRSTAT.PCX   171551  [@01f005e0]  
1067:    VOTER.PCX   190850  [@01f2a420]  

“RESOURCE._01” seems to contain mostly assets for the game menus and configuration. Though just like with “RESOURCE._02” there is some crossover.

NovaLogic RES Extractor (v1)
Opening: 'F22II/RESOURCE._01'    File Size: 8609484 bytes
Version 2 resource catalog found
Resource catalog contains 215 entries
   1:         ROOT        0  [@000010e0] Directory
        1:  BUTTONS.PCX    30297  [@000010f4] 
        2: CALLSIGN.PCX   234697  [@00008780] 
        3:  CC_LOGO.PCX   322681  [@00041c60] 
        4:   Q_LOGO.PCX   211348  [@00090900] 
        5:   CURSOR.PCX     1147  [@000c42c0] 
         ⋮               ⋮       ⋮           
      210:   OCV031.WAV    26264  [@00801980] 
      211:   OCV046.WAV    56070  [@00808040] 
      212:   OCV038.WAV    43196  [@00815b60] 
      213:   OCV047.WAV    29588  [@00820440] 
      214:   OCV030.WAV    59048  [@00827800] 
   2:  BUTTONS.PCX    30297  [@000010f4] 
   3: CALLSIGN.PCX   234697  [@00008780] 
   4:  CC_LOGO.PCX   322681  [@00041c60] 
   5:   Q_LOGO.PCX   211348  [@00090900] 
   6:   CURSOR.PCX     1147  [@000c42c0] 
    ⋮               ⋮       ⋮  
 210:    FLYBY.WAV    31660  [@007f9dc0] 
 211:   OCV031.WAV    26264  [@00801980] 
 212:   OCV046.WAV    56070  [@00808040] 
 213:   OCV038.WAV    43196  [@00815b60] 
 214:   OCV047.WAV    29588  [@00820440] 
 215:   OCV030.WAV    59048  [@00827800] 

Here are some extracts from “RESOURCE._01”

LOADING.PCX
MAIN.PCX
ENEMY.PCX
OPT_LOGO.PCX
MLOGO.PCX

I think that’s pretty much a wrap for this variant of the .RES file format. In the next post on this format we’ll cover the mechanics of creating/modifying a .RES file, and ultimately release the code we’ve created here.

By Thread



Leave a comment