Einleitung
Die wenigsten Spieleentwickler möchten, dass die Daten ihres Spiels (Texturen, Modelle, Sounds, Levels …) für jeden einsehbar und veränderbar sind. Dieses Tutorial zeigt, wie man mit einfachen Mitteln Dateien aus verschlüsselten Zip-Archiven laden kann.
Natürlich wäre es auch möglich, sich sein eigenes Archivformat und die benötigten Tools zum Packen und Entpacken zu schreiben, aber wozu das Rad neu erfinden? Die Kompressionsrate von Zip reicht in den meisten Fällen aus, und es gibt gute Tools wie Sand am Meer.
Was wird gebraucht?
Alles, was zusätzlich zu einem Compiler noch benötigt wird, ist die völlig kostenlose zlib. Diese ist hier zu bekommen: http://www.zlib.net/ . Entweder lädst du dir eine bereits vorkompilierte Version der zlib herunter, oder du kompilierst sie dir selbst. Die Packages kommen zusammen mit Projektdateien für alle gängigen Compiler inklusive Visual C++.
In der zlib enthalten ist auch gleichzeitig minizip, was wir später noch brauchen werden.
Nur eine Funktion
In diesem Tutorial soll lediglich eine einzige Funktion programmiert werden. Diese zeigt, wie eine Datei aus einem verschlüsselten Zip-Archiv gelesen werden kann, und du kannst sie später beispielsweise in deinem selbstprogrammierten virtuellen Dateisystem einsetzen. Da dies nur der Demonstration dient, verzichte ich auf den Einsatz von Klassen. Unsere Funktion soll wie folgt aussehen:
|
C-/C++-Quelltext
|
1
2
3
4
5
|
int readArchivedFile(const std::string& archiveFilename,
const std::string& filename,
const std::string& password,
void** pp_dataOut,
unsigned int* p_dataSizeOut);
|
Im ersten Parameter werden wir den Dateinamen des Zip-Archivs übergeben (z.B. „data.zip“), im zweiten den Namen der darin archivierten Datei (z.B. „sprites.png“) und im dritten ein Passwort, falls das Archiv verschlüsselt ist. Im vierten Parameter übergeben wir die Adresse eines Zeigers, den die Funktion ausfüllt, so dass er anschließend auf die gelesenen Daten zeigt, und der letzte Parameter ist die Adresse eines ganzzahligen Werts, den die Funktion mit der Größe der gelesenen Daten ausfüllt. Für das spätere Löschen der gelesenen Daten mit delete[] ist der Programmierer selbst zuständig. Der Rückgabewert der Funktion zeigt an, ob alles glatt gelaufen ist, oder ob es einen Fehler gab.
Es geht zur Sache
Zuerst müssen wir das Archiv öffnen. Dazu verwenden wir die Funktion unzOpen, die – wie alle im folgenden verwendeten Funktionen – in der minizip-Library zu finden ist. Die minizip-Library befindet sich im Ordner contrib/minizip des zlib-Packages. Insbesondere benötigen wir die Funktionen aus der Datei unzip.c. In unzip.h befindet sich die Dokumentation der hier verwendeten Funktionen. Diese Datei muss per #include eingebunden werden. Du solltest neben unzip.c auch die anderen .c-Dateien aus dem minizip-Verzeichnis zum Projekt hinzufügen – außer minizip.c und miniunz.c, denn dabei handelt es sich um eigenständige Programme (zum Packen und Entpacken per Kommandozeile).
unzOpen erwartet lediglich den Dateinamen des zu öffnenden Archivs und liefert einen Rückgabewert vom Typ unzFile. Dies ist ein Handle, das wir für alle weiteren Operationen mit dem geöffneten Archiv brauchen werden. Gab es einen Fehler, ist der Rückgabewert null.
Als nächstes müssen wir den internen Lesezeiger des geöffneten Archivs auf die angeforderte archivierte Datei setzen. Dazu verwenden wir unzLocateFile. Diese Funktion erwartet als Parameter das Archiv-Handle und den Namen der archivierten Datei, die wir später lesen möchten. Der dritte Parameter gibt an, ob beim Suchen der Datei die Groß-/Kleinschreibung beachtet werden soll. Ein Rückgabewert von UNZ_OK signalisiert, dass es keine Fehler gab, also die Datei gefunden wurde.
Jetzt sind wir schon fast so weit, dass wir die Datei tatsächlich lesen können. Aber eines fehlt noch, denn zuvor sollten wir wissen, wie groß die Datei eigentlich ist, damit wir die richtige Speichermenge anfordern können. Diese und andere Informationen erhalten wir mit Hilfe der Funktion unzGetCurrentFileInfo. Diese erwartet viele Parameter, wovon für uns aber nur die ersten beiden interessant sind: das Archiv-Handle (wie immer) und einen Zeiger auf eine Variable vom Typ unz_file_info. Die Funktion wird diese Variable ausfüllen, und dort finden wir dann im Element uncompressed_size die Größe der entpackten Datei.
Nun können wir den benötigten Speicher reservieren und sind bereit für den entscheidenden Schritt: das Öffnen und Lesen der Datei. Hierzu verwenden wir entweder unzOpenCurrentFile oder unzOpenCurrentFilePassword, abhängig davon, ob wir es mit einer verschlüsselten Datei zu tun haben oder nicht. Als Parameter brauchen wir nur das Archiv-Handle und bei der zweiten Variante noch das Passwort zum Entschlüsseln zu übergeben. Auch hier zeigt ein Rückgabewert von UNZ_OK an, dass alles geklappt hat.
Das eigentliche Lesen der Daten erfolgt nun mit Hilfe der Funktion unzReadCurrentFile. Dieser übergeben wir das Archiv-Handle, die Adresse zum Ablegen der gelesenen Daten und die Anzahl der Bytes, die wir gerne lesen würden. Stimmt der Rückgabewert mit der Anzahl der angeforderten Bytes überein, war alles in Ordnung.
Nun müssen wir nur noch aufräumen, wozu wir unzCloseCurrentFile und unzClose aufrufen. Das war’s! Und hier kommt die Funktion, die genau das macht, was in den letzten Absätzen beschrieben wurde.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
int readArchivedFile(const std::string& archiveFilename,
const std::string& filename,
const std::string& password,
void** pp_dataOut,
unsigned int* p_dataSizeOut)
{
// Archiv öffnen
unzFile archive = unzOpen(archiveFilename.c_str());
if(!archive)
{
// Fehler! Die Archivdatei existiert wahrscheinlich nicht oder ist beschädigt.
return -1;
}
// die archivierte Datei aufspüren, Groß-/Kleinschreibung ignorieren
int result = unzLocateFile(archive, filename.c_str(), 0);
if(result != UNZ_OK)
{
// Fehler! Die Datei wurde wohl nicht gefunden.
unzClose(archive);
return -2;
}
// Dateiinformationen abfragen
unz_file_info info;
unzGetCurrentFileInfo(archive, &info, 0, 0, 0, 0, 0, 0);
// die entsprechende Menge an Speicher reservieren
unsigned int fileSize = static_cast<unsigned int>(info.uncompressed_size);
char* p_data = new char[fileSize];
if(!p_data)
{
// Fehler! Nicht genug Speicher. Die Datei müsste stückweise gelesen werden.
unzClose(archive);
return -3;
}
// Datei öffnen - mit Passwort oder ohne
if(password.empty()) result = unzOpenCurrentFile(archive);
else result = unzOpenCurrentFilePassword(archive, password.c_str());
if(result != UNZ_OK)
{
// Fehler! Das Passwort könnte falsch sein.
delete[] p_data;
unzClose(archive);
return -4;
}
// die komplette Datei lesen
unsigned int numBytesRead = unzReadCurrentFile(archive, p_data, fileSize);
if(numBytesRead != fileSize)
{
// Fehler! Das Archiv könnte beschädigt sein.
delete[] p_data;
unzCloseCurrentFile(archive);
unzClose(archive);
return -5;
}
// aufräumen
unzCloseCurrentFile(archive);
unzClose(archive);
// Daten und Größe zurückliefern
if(pp_dataOut) *pp_dataOut = p_data;
if(p_dataSizeOut) *p_dataSizeOut = fileSize;
// Alles OK!
return 0;
}
|
Anwendungsbeispiel
Ich will hier noch ein kleines Beispiel für die Verwendung unserer Funktion zeigen. Wir lesen die Datei „test.txt“ aus dem Archiv „test.zip“, die mit dem Passwort „1337“ verschlüsselt wurde.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
void* p_text = 0;
unsigned int textSize = 0;
// verschlüsselte Datei "test.txt" aus dem Archiv "test.zip" lesen, Passwort "1337"
int result = readArchivedFile("test.zip", "test.txt", "1337", &p_text, &textSize);
if(result)
{
// Fehler!
printf("Fehler! (Code: %d)\n", result);
}
else
{
// Text ausgeben
std::string text(static_cast<char*>(p_text), textSize);
printf("Der gelesene Text: %s\n", text.c_str());
// Speicher wieder freigeben
delete[] p_text;
}
|
Und was ist mit Schreiben?
Die minizip-Bibliothek ermöglicht nicht nur das Lesen aus passwortverschlüsselten Zip-Archiven, sondern unterstützt auch Schreibvorgänge darin. Die entsprechenden Funktionen befinden sich in der Datei zip.c (Dokumentation in zip.h) und lassen sich ganz analog zu den hier benutzten Lesefunktionen verwenden.
Aufwändiger wird es, wenn eine bereits vorhandene Datei in einem Archiv überschrieben werden soll. Dann ist es zunächst erforderlich, die Datei komplett aus dem Archiv zu löschen. Leider bietet die minizip-Bibliothek hierzu keine Funktion. Darum möchte ich hier eine solche zur Verfügung stellen. Die Funktion deleteArchivedFile erwartet als Parameter den Dateinamen des Archivs und den Namen der zu löschenden Datei, die darin archiviert ist. Dazu wird das Archiv Datei für Datei in ein neues Archiv kopiert, wobei die zu löschende Datei einfach ausgelassen wird. Für den Fall, dass die zu löschende Datei die einzige Datei in dem Archiv ist, wird das gesamte Archiv gelöscht, da ein leeres Archiv beim Öffnen zu Problemen führt.
Die Informationen zum Aufbau des Zip-Dateiformats habe ich
diesem Text entnommen.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
#pragma pack(push, 1)
int deleteArchivedFile(const std::string& archiveFilename,
const std::string& objectName)
{
int result = 0;
struct LocalFileHeader
{
unsigned int signature;
unsigned short versionNeeded;
unsigned short flags;
unsigned short method;
unsigned short modTime;
unsigned short modDate;
unsigned int crc;
unsigned int compressedSize;
unsigned int uncompressedSize;
unsigned short filenameLength;
unsigned short extraFieldLength;
};
struct EndOfCentralDirectory
{
unsigned int signature;
unsigned short thisDisk;
unsigned short centralRecordDisk;
unsigned short entriesOnThisDisk;
unsigned short totalEntries;
unsigned int centralDirectorySize;
unsigned int centralDirectoryOffset;
unsigned short globalCommentLength;
};
struct CentralDirectoryEntry
{
unsigned int signature;
unsigned short versionMadeBy;
unsigned short versionNeeded;
unsigned short flags;
unsigned short method;
unsigned short modTime;
unsigned short modDate;
unsigned int crc;
unsigned int compressedSize;
unsigned int uncompressedSize;
unsigned short filenameLength;
unsigned short extraFieldLength;
unsigned short commentLength;
unsigned short diskNumber;
unsigned short intAttribs;
unsigned int extAttribs;
unsigned int localHeaderOffset;
};
FILE* p_in = fopen(archiveFilename.c_str(), "rb");
FILE* p_out = fopen((archiveFilename + "_").c_str(), "wb");
// zentrales Verzeichnis suchen
while(true)
{
unsigned int signature;
unsigned int pos = ftell(p_in);
fread(&signature, 1, 4, p_in);
fseek(p_in, pos, SEEK_SET);
if(signature == 0x04034B50)
{
LocalFileHeader lfh;
fread(&lfh, 1, sizeof(lfh), p_in);
fseek(p_in, lfh.filenameLength + lfh.extraFieldLength + lfh.compressedSize, SEEK_CUR);
}
else if(signature == 0x02014B50)
{
CentralDirectoryEntry cde;
fread(&cde, 1, sizeof(cde), p_in);
fseek(p_in, cde.filenameLength + cde.extraFieldLength + cde.commentLength, SEEK_CUR);
}
else if(signature == 0x06054B50)
{
// Danach haben wir gesucht!
break;
}
else
{
return false;
}
}
EndOfCentralDirectory ecd, ecdOut;
fread(&ecd, 1, sizeof(ecd), p_in);
char* p_globalComment = 0;
if(ecd.globalCommentLength) p_globalComment = new char[ecd.globalCommentLength];
ecdOut = ecd;
fseek(p_in, ecd.centralDirectoryOffset, SEEK_SET);
std::vector<CentralDirectoryEntry> cdOut;
std::vector<char*> filenameOut, extraFieldOut, commentOut;
// Einträge lesen und schreiben
for(unsigned int i = 0; i < ecd.totalEntries; i++)
{
CentralDirectoryEntry cde, cdeOut;
fread(&cde, 1, sizeof(cde), p_in);
cdeOut = cde;
char* p_filename = new char[cde.filenameLength + 1];
fread(p_filename, 1, cde.filenameLength, p_in);
p_filename[cde.filenameLength] = 0;
char* p_extraField = 0;
if(cde.extraFieldLength)
{
p_extraField = new char[cde.extraFieldLength];
fread(p_extraField, 1, cde.extraFieldLength, p_in);
}
char* p_comment = 0;
if(cde.commentLength)
{
p_comment = new char[cde.commentLength];
fread(p_comment, 1, cde.commentLength, p_in);
}
// merken, wo es nachher weitergeht
unsigned int nextCDE = ftell(p_in);
// Stimmt der Dateiname mit dem zu löschenden Dateinamen überein?
if(!_stricmp(objectName.c_str(), p_filename))
{
ecdOut.entriesOnThisDisk--;
ecdOut.totalEntries--;
ecdOut.centralDirectorySize -= sizeof(cde) + cde.filenameLength + cde.extraFieldLength + cde.commentLength;
result = 1;
delete[] p_filename;
delete[] p_extraField;
delete[] p_comment;
}
else
{
// Diese Datei soll kopiert werden. Zuerst lesen wir ihren lokalen Header.
LocalFileHeader lfh;
fseek(p_in, cde.localHeaderOffset, SEEK_SET);
fread(&lfh, sizeof(lfh), 1, p_in);
// Dateiname und Extrafeld überspringen
fseek(p_in, lfh.filenameLength + lfh.extraFieldLength, SEEK_CUR);
// Daten lesen
char* p_fileData = new char[lfh.compressedSize];
fread(p_fileData, 1, lfh.compressedSize, p_in);
// lokalen Header schreiben
cdeOut.localHeaderOffset = ftell(p_out);
fwrite(&lfh, 1, sizeof(lfh), p_out);
fwrite(p_filename, 1, lfh.filenameLength, p_out);
if(lfh.extraFieldLength) fwrite(p_extraField, 1, lfh.extraFieldLength, p_out);
fwrite(p_fileData, 1, lfh.compressedSize, p_out);
delete[] p_fileData;
// Eintrag für das zentrale Verzeichnis merken
cdOut.push_back(cdeOut);
filenameOut.push_back(p_filename);
extraFieldOut.push_back(p_extraField);
commentOut.push_back(p_comment);
}
fseek(p_in, nextCDE, SEEK_SET);
}
// zentrales Verzeichnis schreiben
ecdOut.centralDirectoryOffset = ftell(p_out);
for(unsigned int i = 0; i < cdOut.size(); i++)
{
CentralDirectoryEntry& cde = cdOut[i];
fwrite(&cde, 1, sizeof(cde), p_out);
char* p_filename = filenameOut[i];
char* p_extraField = extraFieldOut[i];
char* p_comment = commentOut[i];
fwrite(p_filename, 1, cde.filenameLength, p_out);
if(cde.extraFieldLength) fwrite(p_extraField, 1, cde.extraFieldLength, p_out);
if(cde.commentLength) fwrite(p_comment, 1, cde.commentLength, p_out);
delete[] p_filename;
delete[] p_extraField;
delete[] p_comment;
}
// Ende schreiben
fwrite(&ecdOut, 1, sizeof(ecdOut), p_out);
if(ecdOut.globalCommentLength) fwrite(p_globalComment, 1, ecdOut.globalCommentLength, p_out);
delete[] p_globalComment;
fclose(p_in);
fclose(p_out);
// altes Archiv löschen
remove(archiveFilename.c_str());
if(ecdOut.totalEntries)
{
// neue Datei umbenennen
rename((archiveFilename + "_").c_str(), archiveFilename.c_str());
}
else
{
// neue Datei löschen
remove((archiveFilename + "_").c_str());
result = -1;
}
return result;
}
#pragma pack(pop)
|
Weitere Anregungen
Die hier gezeigten Funktionen sind natürlich noch recht rudimentär. Richtig praktisch wird das ganze erst, wenn man es in ein virtuelles Dateisystem integriert. Dort könnte man Zip-Archive wie Verzeichnisse behandeln und beispielsweise Dateinamen wie „data.zip[passwort]/sprites.png“ verwenden. Das Dateisystem würde beim Analysieren des Pfads erkennen, dass es sich um ein verschlüsseltes Zip-Archiv handelt und dafür sorgen, dass die Datei „sprites.png“ mit dem Passwort „passwort“ entpackt wird. Das alles würde ganz transparent geschehen. Dem Benutzer (Programmierer) könnte es egal sein, ob sein Pfad auf eine gewöhnliche Datei oder auf eine archivierte Datei zeigt, denn das Dateisystem würde sich um alle nötigen Schritte kümmern.
Das ursprüngliche Ziel war ja, dass niemand einfach so in die Dateien des Spiels bzw. der Anwendung hereinsehen kann. Wenn das Passwort jedoch im Klartext, also unverschlüsselt, im Programmcode steht, dann wird es ohne weitere Maßnahmen auch unverschlüsselt in der ausführbaren Datei (.exe) stehen und könnte so herausgefischt werden. Darum könnte man alle Passwörter noch einmal separat verschlüsseln.