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.

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.

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.



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



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

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.

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”





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.
Leave a comment