Skip to content

Commit

Permalink
An option to use filename-based decryption key
Browse files Browse the repository at this point in the history
Previously, libmpq tried to brute-force the decryption key
based on know contents, and only for packed files.

This caused issues, such as
mbroemme#13.

Adds a new function, `libmpq__block_open_offset_with_filename`,
that uses the filename to obtain the decryption key.
  • Loading branch information
glebm committed Oct 31, 2021
1 parent 8a08290 commit 1d65740
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 0 deletions.
20 changes: 20 additions & 0 deletions libmpq/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ int32_t libmpq__decrypt_block(uint32_t *in_buf, uint32_t in_size, uint32_t seed)
return LIBMPQ_SUCCESS;
}

int32_t libmpq__encryption_key_from_filename(const char *filename, uint32_t *key) {
const char *basename = strrchr(filename, '\\');
if (*basename == '\0')
basename = strrchr(filename, '/');
if (*basename == '\0')
basename = filename;
if (*basename == '\0')
return LIBMPQ_ERROR_DECRYPT;
*key = libmpq__hash_string(basename, 0x300);
return LIBMPQ_SUCCESS;
}

int32_t libmpq__encryption_key_from_filename_v2(const char *filename, uint32_t offset, uint32_t unpacked_size, uint32_t *key) {
int32_t result = libmpq__encryption_key_from_filename(filename, key);
if (result < 0)
return result;
*key = (*key + offset) ^ unpacked_size;
return LIBMPQ_SUCCESS;
}

/* function to detect decryption key. */
int32_t libmpq__decrypt_key(uint8_t *in_buf, uint32_t in_size, uint32_t block_size, uint32_t *key) {

Expand Down
14 changes: 14 additions & 0 deletions libmpq/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ int32_t libmpq__decrypt_key(
uint32_t *key
);

/* function to obtain the decryption key given a filename. */
int32_t libmpq__encryption_key_from_filename(
const char *filename,
uint32_t *key
);

/* function to obtain the decryption key given a filename for files with LIBMPQ_FLAG_ENCRYPTION_KEY_V2. */
int32_t libmpq__encryption_key_from_filename_v2(
const char *filename,
uint32_t offset,
uint32_t unpacked_size,
uint32_t *key
);

/* function to decompress or explode block from archive. */
int32_t libmpq__decompress_block(
uint8_t *in_buf,
Expand Down
1 change: 1 addition & 0 deletions libmpq/mpq-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#define LIBMPQ_FLAG_COMPRESS_PKZIP 0x00000100 /* compression made by pkware data compression library. */
#define LIBMPQ_FLAG_COMPRESS_MULTI 0x00000200 /* multiple compressions. */
#define LIBMPQ_FLAG_COMPRESS_NONE 0x00000300 /* no compression (no blizzard flag used by myself). */
#define LIBMPQ_FLAG_ENCRYPTION_KEY_V2 0x00020000 /* File decryption key is additionally derived from file offset and unpacked size. */
#define LIBMPQ_FLAG_SINGLE 0x01000000 /* file is stored in one single sector, first seen in world of warcraft. */
#define LIBMPQ_FLAG_CRC 0x04000000 /* compressed block offset table has CRC checksum. */

Expand Down
174 changes: 174 additions & 0 deletions libmpq/mpq.c
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,180 @@ int32_t libmpq__file_read(mpq_archive_s *mpq_archive, uint32_t file_number, uint
return LIBMPQ_SUCCESS;
}

/* opens a file and calculates the filename-based decryption key if needed. */
int32_t libmpq__block_open_offset_with_filename(mpq_archive_s *mpq_archive, uint32_t file_number, const char *filename) {

/* some common variables. */
uint32_t i;
uint32_t packed_size;
int32_t result = 0;

/* check if given file number is not out of range. */
CHECK_FILE_NUM(file_number, mpq_archive)

if (mpq_archive->mpq_file[file_number]) {

/* file already opened, so increment counter */
mpq_archive->mpq_file[file_number]->open_count++;
return LIBMPQ_SUCCESS;
}

mpq_block_s *mpq_block = &mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices];

/* check if file is not stored in a single sector. */
if ((mpq_block->flags & LIBMPQ_FLAG_SINGLE) == 0) {

/* get packed size based on block size and block count. */
packed_size = sizeof(uint32_t) * (((mpq_block->unpacked_size + mpq_archive->block_size - 1) / mpq_archive->block_size) + 1);
} else {

/* file is stored in single sector and we need only two entries for the packed block offset table. */
packed_size = sizeof(uint32_t) * 2;
}

/* check if data has one extra entry. */
if ((mpq_block->flags & LIBMPQ_FLAG_CRC) != 0) {

/* add one uint32_t. */
packed_size += sizeof(uint32_t);
}

/* allocate memory for the file. */
if ((mpq_archive->mpq_file[file_number] = calloc(1, sizeof(mpq_file_s))) == NULL) {

/* memory allocation problem. */
result = LIBMPQ_ERROR_MALLOC;
goto error;
}

/* allocate memory for the packed block offset table. */
if ((mpq_archive->mpq_file[file_number]->packed_offset = calloc(1, packed_size)) == NULL) {

/* memory allocation problem. */
result = LIBMPQ_ERROR_MALLOC;
goto error;
}

/* initialize counter to one opening */
mpq_archive->mpq_file[file_number]->open_count = 1;

/* check if we need to load the packed block offset table, we will maintain this table for unpacked files too. */
if ((mpq_block->flags & LIBMPQ_FLAG_COMPRESSED) != 0 &&
(mpq_block->flags & LIBMPQ_FLAG_SINGLE) == 0) {

/* seek to block position. */
if (fseeko(mpq_archive->fp, mpq_block->offset + (((long long)mpq_archive->mpq_block_ex[mpq_archive->mpq_map[file_number].block_table_indices].offset_high) << 32) + mpq_archive->archive_offset, SEEK_SET) < 0) {

/* seek in file failed. */
result = LIBMPQ_ERROR_SEEK;
goto error;
}

/* read block positions from begin of file. */
if (fread(mpq_archive->mpq_file[file_number]->packed_offset, 1, packed_size, mpq_archive->fp) != packed_size) {

/* something on read from archive failed. */
result = LIBMPQ_ERROR_READ;
goto error;
}

/* check if the archive is protected some way, sometimes the file appears not to be encrypted, but it is.
* a special case are files with an additional sector but LIBMPQ_FLAG_CRC not set. we don't want to handle
* them as encrypted. */
if (mpq_archive->mpq_file[file_number]->packed_offset[0] != packed_size &&
mpq_archive->mpq_file[file_number]->packed_offset[0] != packed_size + 4) {

/* file is encrypted. */
mpq_block->flags |= LIBMPQ_FLAG_ENCRYPTED;
}

/* check if packed offset block is encrypted, we have to decrypt it. */
if (mpq_block->flags & LIBMPQ_FLAG_ENCRYPTED) {

/* get the file seed. */
uint32_t seed;
if (mpq_block->flags & LIBMPQ_FLAG_ENCRYPTION_KEY_V2) {
result = libmpq__encryption_key_from_filename_v2(filename, mpq_block->offset, mpq_block->unpacked_size, &seed);
} else {
result = libmpq__encryption_key_from_filename(filename, &seed);
}
if (result < 0) {
result = LIBMPQ_ERROR_DECRYPT;
goto error;
}
mpq_archive->mpq_file[file_number]->seed = seed;

/* decrypt block in input buffer. */
if (libmpq__decrypt_block(mpq_archive->mpq_file[file_number]->packed_offset, packed_size, seed - 1) < 0 ) {

/* something on decrypt failed. */
result = LIBMPQ_ERROR_DECRYPT;
goto error;
}

/* check if the block positions are correctly decrypted. */
if (mpq_archive->mpq_file[file_number]->packed_offset[0] != packed_size) {

/* sorry without seed, we cannot extract file. */
result = LIBMPQ_ERROR_DECRYPT;
goto error;
}
}
} else {

/* check if file is not stored in a single sector. */
if ((mpq_block->flags & LIBMPQ_FLAG_SINGLE) == 0) {

/* loop through all blocks and create packed block offset table based on block size. */
for (i = 0; i < ((mpq_block->unpacked_size + mpq_archive->block_size - 1) / mpq_archive->block_size + 1); i++) {

/* check if we process the last block. */
if (i == ((mpq_block->unpacked_size + mpq_archive->block_size - 1) / mpq_archive->block_size)) {

/* store size of last block. */
mpq_archive->mpq_file[file_number]->packed_offset[i] = mpq_block->unpacked_size;
} else {

/* store default block size. */
mpq_archive->mpq_file[file_number]->packed_offset[i] = i * mpq_archive->block_size;
}
}
} else {

/* store offsets. */
mpq_archive->mpq_file[file_number]->packed_offset[0] = 0;
mpq_archive->mpq_file[file_number]->packed_offset[1] = mpq_block->packed_size;
}

if (mpq_block->flags & LIBMPQ_FLAG_ENCRYPTED) {
/* get the file seed. */
uint32_t seed;
if (mpq_block->flags & LIBMPQ_FLAG_ENCRYPTION_KEY_V2) {
result = libmpq__encryption_key_from_filename_v2(filename, mpq_block->offset, mpq_block->unpacked_size, &seed);
} else {
result = libmpq__encryption_key_from_filename(filename, &seed);
}
if (result < 0) {
result = LIBMPQ_ERROR_DECRYPT;
goto error;
}
mpq_archive->mpq_file[file_number]->seed = seed;
}
}

/* if no error was found, return zero. */
return LIBMPQ_SUCCESS;

error:

/* free packed block offset table and file pointer. */
free(mpq_archive->mpq_file[file_number]->packed_offset);
free(mpq_archive->mpq_file[file_number]);

/* return error constant. */
return result;
}

/* this function open a file in the given archive and caches the block offset information. */
int32_t libmpq__block_open_offset(mpq_archive_s *mpq_archive, uint32_t file_number) {

Expand Down
1 change: 1 addition & 0 deletions libmpq/mpq.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ extern LIBMPQ_API int32_t libmpq__file_read(mpq_archive_s *mpq_archive, uint32_t

/* generic block processing functions. */
extern LIBMPQ_API int32_t libmpq__block_open_offset(mpq_archive_s *mpq_archive, uint32_t file_number);
extern LIBMPQ_API int32_t libmpq__block_open_offset_with_filename(mpq_archive_s *mpq_archive, uint32_t file_number, const char *filename);
extern LIBMPQ_API int32_t libmpq__block_close_offset(mpq_archive_s *mpq_archive, uint32_t file_number);
extern LIBMPQ_API int32_t libmpq__block_size_unpacked(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, libmpq__off_t *unpacked_size);
extern LIBMPQ_API int32_t libmpq__block_read(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, uint8_t *out_buf, libmpq__off_t out_size, libmpq__off_t *transferred);
Expand Down

0 comments on commit 1d65740

Please sign in to comment.