Skip to content

Commit

Permalink
Verify pk3 hash and integrity after completing the download
Browse files Browse the repository at this point in the history
Engine does not verify CRCs when loading pk3, only after downloading it.
This check asserts server/download sanity and protects against bitrot (but not spoofing).
  • Loading branch information
aufau committed Apr 2, 2024
1 parent bfdcc8c commit 3d0b200
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 7 deletions.
9 changes: 2 additions & 7 deletions src/client/cl_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1598,7 +1598,6 @@ void CL_BeginDownload( const char *localName, const char *remoteName ) {
if (cls.downloadBlacklist[i].checksum == clc.downloadChksums[clc.downloadIndex]) {
// file is blacklisted
Com_Printf("Skipping download for blacklisted file %s\n", remoteName);
clc.downloadIndex++;
CL_NextDownload();
return;
}
Expand Down Expand Up @@ -1638,19 +1637,14 @@ Stores it in the current game directory.
void CL_ContinueCurrentDownload(dldecision_t decision) {
if (decision == DL_ABORT) {
// user disallowed the file
clc.downloadIndex++;
CL_NextDownload();
} else if (decision == DL_ABORT_BLACKLIST) {
// user disallowed the file and never wants to be asked again
Com_DPrintf("Blacklisted file with checksum %i", clc.downloadChksums[clc.downloadIndex]);
CL_BlacklistCurrentFile();

clc.downloadIndex++;
CL_NextDownload();
} else {
// user accepted the file
clc.downloadIndex++;

Com_Printf("^1****** ^7File Download ^1******^7\n"
"Localname: %s\n"
"Remotename: %s\n"
Expand Down Expand Up @@ -1726,6 +1720,7 @@ void CL_NextDownload(void) {
// move over the rest
memmove( clc.downloadList, s, strlen(s) + 1);

clc.downloadIndex++;
CL_BeginDownload(localNameCpy, remoteNameCpy);

return;
Expand All @@ -1745,7 +1740,7 @@ and determine if we need to download them
void CL_InitDownloads(void) {
char missingfiles[1024];

clc.downloadIndex = 0;
clc.downloadIndex = -1;

if (cls.ignoreNextDownloadList) {
cls.ignoreNextDownloadList = qfalse;
Expand Down
14 changes: 14 additions & 0 deletions src/client/cl_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,13 @@ void CL_ParseUDPDownload ( msg_t *msg ) {
FS_FCloseFile( clc.download );
clc.download = 0;

int checksum;
if (FS_SV_VerifyZipFile(clc.downloadTempName, &checksum)) {
Com_Error(ERR_DROP, "Download Error: pk3 archive corrupted");
}
if (clc.downloadChksums[clc.downloadIndex] != checksum) {
Com_Error(ERR_DROP, "Download Error: pk3 checksum does not match");
}
FS_SV_Rename(clc.downloadTempName, clc.downloadName);
}
*clc.downloadTempName = *clc.downloadName = 0;
Expand Down Expand Up @@ -619,6 +626,13 @@ HTTP download ended
*/
void CL_EndHTTPDownload(dlHandle_t handle, qboolean success, const char *err_msg) {
if (success) {
int checksum;
if (FS_SV_VerifyZipFile(clc.downloadTempName, &checksum)) {
Com_Error(ERR_DROP, "Download Error: pk3 archive corrupted");
}
if (clc.downloadChksums[clc.downloadIndex] != checksum) {
Com_Error(ERR_DROP, "Download Error: pk3 checksum does not match");
}
FS_SV_Rename(clc.downloadTempName, clc.downloadName);
} else {
Com_Error(ERR_DROP, "Download Error: %s", err_msg);
Expand Down
107 changes: 107 additions & 0 deletions src/qcommon/files.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,113 @@ static pack_t *FS_LoadZipFile( char *zipfile, const char *basename, qboolean ass
return pack;
}

/*
=================
FS_SV_VerifyZipFile
Verify zip data integrity with CRCs
Calculate checksum for zip file the same way as in pack->checksum
This is just a hash of CRCs from ZIP central directory
It does not check the actual content and zip metadata
=================
*/
qboolean FS_SV_VerifyZipFile( const char *zipfile, int *checksum )
{
const char *ospath;
unzFile uf;
int err;
unz_global_info gi;
unz_file_info file_info;
ZPOS64_T i;
int fs_numHeaderLongs;
int *fs_headerLongs = NULL;
int chksum;
char *read_buffer = NULL;
const int read_buffer_size = 16384; // UNZ_BUFSIZE

if ( !fs_searchpaths ) {
Com_Error( ERR_FATAL, "Filesystem call made without initialization" );
}

ospath = FS_BuildOSPath( fs_homepath->string, zipfile );

uf = unzOpen(ospath);
if (uf == NULL)
goto unzip_error;

if (unzGetGlobalInfo(uf, &gi))
goto unzip_error;

fs_numHeaderLongs = 0;
fs_headerLongs = (int *)Hunk_AllocateTempMemory(gi.number_entry * sizeof(int));
read_buffer = (char *)Hunk_AllocateTempMemory(read_buffer_size);

if (unzGoToFirstFile(uf))
goto unzip_error;

for (i = 0; i < gi.number_entry; i++)
{
if (unzGetCurrentFileInfo(uf, &file_info, NULL, 0, NULL, 0, NULL, 0))
goto unzip_error;

if (file_info.uncompressed_size > 0) {
fs_headerLongs[fs_numHeaderLongs++] = LittleLong(file_info.crc);
}

if (unzOpenCurrentFile(uf))
goto unzip_error;

// read whole file to make minizip calculate CRC
do {
err = unzReadCurrentFile(uf, read_buffer, read_buffer_size);

if (err < 0) {
unzCloseCurrentFile(uf);
goto unzip_error;
}
} while (err != UNZ_EOF);

// decompression may fail early due to bitrot
// unzCloseCurrentFile() does not verify CRC unless unzeof() returns 1
if (unzeof(uf) != 1) {
unzCloseCurrentFile(uf);
goto unzip_error;
}

// returns UNZ_CRCERROR if CRC does not match
if (unzCloseCurrentFile(uf))
goto unzip_error;

unzGoToNextFile(uf);
}

if (checksum) {
chksum = Com_BlockChecksum( fs_headerLongs, 4 * fs_numHeaderLongs );
chksum = LittleLong( chksum );
*checksum = chksum;
}

unzClose(uf);

Hunk_FreeTempMemory(read_buffer);
Hunk_FreeTempMemory(fs_headerLongs);

return qfalse;

unzip_error:
if (uf)
unzClose(uf);

if (read_buffer)
Hunk_FreeTempMemory(read_buffer);

if (fs_headerLongs)
Hunk_FreeTempMemory(fs_headerLongs);

return qtrue;
}

/*
=================================================================================
Expand Down
1 change: 1 addition & 0 deletions src/qcommon/qcommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ qboolean FS_ComparePaks(char *neededpaks, int len, int *chksums, size_t maxchksu
qboolean FS_Rename( const char *from, const char *to );

const char *FS_MV_VerifyDownloadPath(const char *pk3file);
qboolean FS_SV_VerifyZipFile( const char *zipfile, int *checksum );

int FS_GetDLList(dlfile_t *files, int maxfiles);
qboolean FS_RMDLPrefix(const char *qpath);
Expand Down

0 comments on commit 3d0b200

Please sign in to comment.