1 /*
2 * Copyright (C) 2013-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
3 * Copyright (C) 2013 Sourcefire, Inc.
4 *
5 * Authors: David Raynor <draynor@sourcefire.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License version 2 as
9 * published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 * MA 02110-1301, USA.
20 */
21 /**
22 * Documentation:
23 * - https://digital-forensics.sans.org/media/FOR518-Reference-Sheet.pdf
24 * - https://github.com/sleuthkit/sleuthkit/blob/develop/tsk/fs/tsk_hfs.h
25 * - https://github.com/unsound/hfsexplorer/tree/master/src/java/org/catacombae/hfs
26 */
27
28 #if HAVE_CONFIG_H
29 #include "clamav-config.h"
30 #endif
31
32 #include <fcntl.h>
33
34 #include "clamav.h"
35 #include "others.h"
36 #include "hfsplus.h"
37 #include "scanners.h"
38 #include "entconv.h"
39
40 #define DECMPFS_HEADER_MAGIC 0x636d7066
41 #define DECMPFS_HEADER_MAGIC_LE 0x66706d63
42
43 static void headerrecord_to_host(hfsHeaderRecord *);
44 static void headerrecord_print(const char *, hfsHeaderRecord *);
45 static void nodedescriptor_to_host(hfsNodeDescriptor *);
46 static void nodedescriptor_print(const char *, hfsNodeDescriptor *);
47 static void forkdata_to_host(hfsPlusForkData *);
48 static void forkdata_print(const char *, hfsPlusForkData *);
49
50 static int hfsplus_volumeheader(cli_ctx *, hfsPlusVolumeHeader **);
51 static int hfsplus_readheader(cli_ctx *, hfsPlusVolumeHeader *, hfsNodeDescriptor *,
52 hfsHeaderRecord *, int, const char *);
53 static cl_error_t hfsplus_scanfile(cli_ctx *, hfsPlusVolumeHeader *, hfsHeaderRecord *,
54 hfsPlusForkData *, const char *, char **, char *);
55 static int hfsplus_validate_catalog(cli_ctx *, hfsPlusVolumeHeader *, hfsHeaderRecord *);
56 static int hfsplus_fetch_node(cli_ctx *, hfsPlusVolumeHeader *, hfsHeaderRecord *,
57 hfsHeaderRecord *, hfsPlusForkData *, uint32_t, uint8_t *);
58 static cl_error_t hfsplus_walk_catalog(cli_ctx *, hfsPlusVolumeHeader *, hfsHeaderRecord *,
59 hfsHeaderRecord *, hfsHeaderRecord *, const char *);
60
61 /* Header Record : fix endianness for useful fields */
headerrecord_to_host(hfsHeaderRecord * hdr)62 static void headerrecord_to_host(hfsHeaderRecord *hdr)
63 {
64 hdr->treeDepth = be16_to_host(hdr->treeDepth);
65 hdr->rootNode = be32_to_host(hdr->rootNode);
66 hdr->leafRecords = be32_to_host(hdr->leafRecords);
67 hdr->firstLeafNode = be32_to_host(hdr->firstLeafNode);
68 hdr->lastLeafNode = be32_to_host(hdr->lastLeafNode);
69 hdr->nodeSize = be16_to_host(hdr->nodeSize);
70 hdr->maxKeyLength = be16_to_host(hdr->maxKeyLength);
71 hdr->totalNodes = be32_to_host(hdr->totalNodes);
72 hdr->freeNodes = be32_to_host(hdr->freeNodes);
73 hdr->attributes = be32_to_host(hdr->attributes); /* not too useful */
74 }
75
76 /* Header Record : print details in debug mode */
headerrecord_print(const char * pfx,hfsHeaderRecord * hdr)77 static void headerrecord_print(const char *pfx, hfsHeaderRecord *hdr)
78 {
79 cli_dbgmsg("%s Header: depth %hu root %u leafRecords %u firstLeaf %u lastLeaf %u nodeSize %hu\n",
80 pfx, hdr->treeDepth, hdr->rootNode, hdr->leafRecords, hdr->firstLeafNode,
81 hdr->lastLeafNode, hdr->nodeSize);
82 cli_dbgmsg("%s Header: maxKeyLength %hu totalNodes %u freeNodes %u btreeType %hhu attributes %x\n",
83 pfx, hdr->maxKeyLength, hdr->totalNodes, hdr->freeNodes,
84 hdr->btreeType, hdr->attributes);
85 }
86
87 /* Node Descriptor : fix endianness for useful fields */
nodedescriptor_to_host(hfsNodeDescriptor * node)88 static void nodedescriptor_to_host(hfsNodeDescriptor *node)
89 {
90 node->fLink = be32_to_host(node->fLink);
91 node->bLink = be32_to_host(node->bLink);
92 node->numRecords = be16_to_host(node->numRecords);
93 }
94
95 /* Node Descriptor : print details in debug mode */
nodedescriptor_print(const char * pfx,hfsNodeDescriptor * node)96 static void nodedescriptor_print(const char *pfx, hfsNodeDescriptor *node)
97 {
98 cli_dbgmsg("%s Desc: fLink %u bLink %u kind %d height %u numRecords %u\n",
99 pfx, node->fLink, node->bLink, node->kind, node->height, node->numRecords);
100 }
101
102 /* ForkData : fix endianness */
forkdata_to_host(hfsPlusForkData * fork)103 static void forkdata_to_host(hfsPlusForkData *fork)
104 {
105 int i;
106
107 fork->logicalSize = be64_to_host(fork->logicalSize);
108 fork->clumpSize = be32_to_host(fork->clumpSize); /* does this matter for read-only? */
109 fork->totalBlocks = be32_to_host(fork->totalBlocks);
110 for (i = 0; i < 8; i++) {
111 fork->extents[i].startBlock = be32_to_host(fork->extents[i].startBlock);
112 fork->extents[i].blockCount = be32_to_host(fork->extents[i].blockCount);
113 }
114 }
115
116 /* ForkData : print details in debug mode */
forkdata_print(const char * pfx,hfsPlusForkData * fork)117 static void forkdata_print(const char *pfx, hfsPlusForkData *fork)
118 {
119 int i;
120 cli_dbgmsg("%s logicalSize " STDu64 " clumpSize " STDu32 " totalBlocks " STDu32 "\n", pfx,
121 fork->logicalSize, fork->clumpSize, fork->totalBlocks);
122 for (i = 0; i < 8; i++) {
123 if (fork->extents[i].startBlock == 0)
124 break;
125 cli_dbgmsg("%s extent[%d] startBlock " STDu32 " blockCount " STDu32 "\n", pfx, i,
126 fork->extents[i].startBlock, fork->extents[i].blockCount);
127 }
128 }
129
130 /* Read and convert the HFS+ volume header */
hfsplus_volumeheader(cli_ctx * ctx,hfsPlusVolumeHeader ** header)131 static int hfsplus_volumeheader(cli_ctx *ctx, hfsPlusVolumeHeader **header)
132 {
133 hfsPlusVolumeHeader *volHeader;
134 const uint8_t *mPtr;
135
136 if (!header) {
137 return CL_ENULLARG;
138 }
139
140 /* Start with volume header, 512 bytes at offset 1024 */
141 if (ctx->fmap->len < 1536) {
142 cli_dbgmsg("hfsplus_volumeheader: too short for HFS+\n");
143 return CL_EFORMAT;
144 }
145 mPtr = fmap_need_off_once(ctx->fmap, 1024, 512);
146 if (!mPtr) {
147 cli_errmsg("hfsplus_volumeheader: cannot read header from map\n");
148 return CL_EMAP;
149 }
150
151 volHeader = cli_malloc(sizeof(hfsPlusVolumeHeader));
152 if (!volHeader) {
153 cli_errmsg("hfsplus_volumeheader: header malloc failed\n");
154 return CL_EMEM;
155 }
156 *header = volHeader;
157 memcpy(volHeader, mPtr, 512);
158
159 volHeader->signature = be16_to_host(volHeader->signature);
160 volHeader->version = be16_to_host(volHeader->version);
161 if ((volHeader->signature == 0x482B) && (volHeader->version == 4)) {
162 cli_dbgmsg("hfsplus_volumeheader: HFS+ signature matched\n");
163 } else if ((volHeader->signature == 0x4858) && (volHeader->version == 5)) {
164 cli_dbgmsg("hfsplus_volumeheader: HFSX v5 signature matched\n");
165 } else {
166 cli_dbgmsg("hfsplus_volumeheader: no matching signature\n");
167 return CL_EFORMAT;
168 }
169 /* skip fields that will definitely be ignored */
170 volHeader->attributes = be32_to_host(volHeader->attributes);
171 volHeader->fileCount = be32_to_host(volHeader->fileCount);
172 volHeader->folderCount = be32_to_host(volHeader->folderCount);
173 volHeader->blockSize = be32_to_host(volHeader->blockSize);
174 volHeader->totalBlocks = be32_to_host(volHeader->totalBlocks);
175
176 cli_dbgmsg("HFS+ Header:\n");
177 cli_dbgmsg("Signature: %x\n", volHeader->signature);
178 cli_dbgmsg("Attributes: %x\n", volHeader->attributes);
179 cli_dbgmsg("File Count: " STDu32 "\n", volHeader->fileCount);
180 cli_dbgmsg("Folder Count: " STDu32 "\n", volHeader->folderCount);
181 cli_dbgmsg("Block Size: " STDu32 "\n", volHeader->blockSize);
182 cli_dbgmsg("Total Blocks: " STDu32 "\n", volHeader->totalBlocks);
183
184 /* Block Size must be power of 2 between 512 and 1 MB */
185 if ((volHeader->blockSize < 512) || (volHeader->blockSize > (1 << 20))) {
186 cli_dbgmsg("hfsplus_volumeheader: Invalid blocksize\n");
187 return CL_EFORMAT;
188 }
189 if (volHeader->blockSize & (volHeader->blockSize - 1)) {
190 cli_dbgmsg("hfsplus_volumeheader: Invalid blocksize\n");
191 return CL_EFORMAT;
192 }
193
194 forkdata_to_host(&(volHeader->allocationFile));
195 forkdata_to_host(&(volHeader->extentsFile));
196 forkdata_to_host(&(volHeader->catalogFile));
197 forkdata_to_host(&(volHeader->attributesFile));
198 forkdata_to_host(&(volHeader->startupFile));
199
200 if (cli_debug_flag) {
201 forkdata_print("allocationFile", &(volHeader->allocationFile));
202 forkdata_print("extentsFile", &(volHeader->extentsFile));
203 forkdata_print("catalogFile", &(volHeader->catalogFile));
204 forkdata_print("attributesFile", &(volHeader->attributesFile));
205 forkdata_print("startupFile", &(volHeader->startupFile));
206 }
207
208 return CL_CLEAN;
209 }
210
211 /* Read and convert the header node */
hfsplus_readheader(cli_ctx * ctx,hfsPlusVolumeHeader * volHeader,hfsNodeDescriptor * nodeDesc,hfsHeaderRecord * headerRec,int headerType,const char * name)212 static int hfsplus_readheader(cli_ctx *ctx, hfsPlusVolumeHeader *volHeader, hfsNodeDescriptor *nodeDesc,
213 hfsHeaderRecord *headerRec, int headerType, const char *name)
214 {
215 const uint8_t *mPtr = NULL;
216 off_t offset;
217 uint32_t minSize, maxSize;
218
219 /* From TN1150: Node Size must be power of 2 between 512 and 32768 */
220 /* Node Size for Catalog or Attributes must be at least 4096 */
221 maxSize = 32768; /* Doesn't seem to vary */
222 switch (headerType) {
223 case HFS_FILETREE_ALLOCATION:
224 offset = volHeader->allocationFile.extents[0].startBlock * volHeader->blockSize;
225 minSize = 512;
226 break;
227 case HFS_FILETREE_EXTENTS:
228 offset = volHeader->extentsFile.extents[0].startBlock * volHeader->blockSize;
229 minSize = 512;
230 break;
231 case HFS_FILETREE_CATALOG:
232 offset = volHeader->catalogFile.extents[0].startBlock * volHeader->blockSize;
233 minSize = 4096;
234 break;
235 case HFS_FILETREE_ATTRIBUTES:
236 offset = volHeader->attributesFile.extents[0].startBlock * volHeader->blockSize;
237 minSize = 4096;
238 break;
239 case HFS_FILETREE_STARTUP:
240 offset = volHeader->startupFile.extents[0].startBlock * volHeader->blockSize;
241 minSize = 512;
242 break;
243 default:
244 cli_errmsg("hfsplus_readheader: %s: invalid headerType %d\n", name, headerType);
245 return CL_EARG;
246 }
247 mPtr = fmap_need_off_once(ctx->fmap, offset, volHeader->blockSize);
248 if (!mPtr) {
249 cli_dbgmsg("hfsplus_readheader: %s: headerNode is out-of-range\n", name);
250 return CL_EFORMAT;
251 }
252
253 /* Node descriptor first */
254 memcpy(nodeDesc, mPtr, sizeof(hfsNodeDescriptor));
255 nodedescriptor_to_host(nodeDesc);
256 nodedescriptor_print(name, nodeDesc);
257 if (nodeDesc->kind != HFS_NODEKIND_HEADER) {
258 cli_dbgmsg("hfsplus_readheader: %s: headerNode not header kind\n", name);
259 return CL_EFORMAT;
260 }
261 if ((nodeDesc->bLink != 0) || (nodeDesc->height != 0) || (nodeDesc->numRecords != 3)) {
262 cli_dbgmsg("hfsplus_readheader: %s: Invalid headerNode\n", name);
263 return CL_EFORMAT;
264 }
265
266 /* Then header record */
267 memcpy(headerRec, mPtr + sizeof(hfsNodeDescriptor), sizeof(hfsHeaderRecord));
268 headerrecord_to_host(headerRec);
269 headerrecord_print(name, headerRec);
270
271 if ((headerRec->nodeSize < minSize) || (headerRec->nodeSize > maxSize)) {
272 cli_dbgmsg("hfsplus_readheader: %s: Invalid nodesize\n", name);
273 return CL_EFORMAT;
274 }
275 if (headerRec->nodeSize & (headerRec->nodeSize - 1)) {
276 cli_dbgmsg("hfsplus_readheader: %s: Invalid nodesize\n", name);
277 return CL_EFORMAT;
278 }
279 /* KeyLength must be between 6 and 516 for catalog */
280 if (headerType == HFS_FILETREE_CATALOG) {
281 if ((headerRec->maxKeyLength < 6) || (headerRec->maxKeyLength > 516)) {
282 cli_dbgmsg("hfsplus_readheader: %s: Invalid cat maxKeyLength\n", name);
283 return CL_EFORMAT;
284 }
285 if (headerRec->maxKeyLength > (headerRec->nodeSize / 2)) {
286 cli_dbgmsg("hfsplus_readheader: %s: Invalid cat maxKeyLength based on nodeSize\n", name);
287 return CL_EFORMAT;
288 }
289 } else if (headerType == HFS_FILETREE_EXTENTS) {
290 if (headerRec->maxKeyLength != 10) {
291 cli_dbgmsg("hfsplus_readheader: %s: Invalid ext maxKeyLength\n", name);
292 return CL_EFORMAT;
293 }
294 }
295
296 /* hdr->treeDepth = rootnode->height */
297 return CL_CLEAN;
298 }
299
300 /**
301 * @brief Read and dump a file for scanning.
302 *
303 * If the filename pointer is provided, the file name will be set and the
304 * resulting file will __NOT__ be scanned. The returned pointer must be freed
305 * by the caller. If the pointer is NULL, the file will be scanned and,
306 * depending on the --leave-temps value, deleted or not.
307 *
308 * @param ctx The current scan context
309 * @param volHeader Volume header
310 * @param extHeader Extent overflow file header
311 * @param fork Fork Data
312 * @param dirname Temp directory name
313 * @param[out] filename (optional) temp file name
314 * @param orig_filename (optiopnal) Original filename
315 * @return cl_error_t
316 */
hfsplus_scanfile(cli_ctx * ctx,hfsPlusVolumeHeader * volHeader,hfsHeaderRecord * extHeader,hfsPlusForkData * fork,const char * dirname,char ** filename,char * orig_filename)317 static cl_error_t hfsplus_scanfile(cli_ctx *ctx, hfsPlusVolumeHeader *volHeader, hfsHeaderRecord *extHeader,
318 hfsPlusForkData *fork, const char *dirname, char **filename, char *orig_filename)
319 {
320 hfsPlusExtentDescriptor *currExt;
321 const uint8_t *mPtr = NULL;
322 char *tmpname = NULL;
323 int ofd;
324 cl_error_t ret = CL_CLEAN;
325 uint64_t targetSize;
326 uint32_t outputBlocks = 0;
327 uint8_t ext;
328
329 UNUSEDPARAM(extHeader);
330
331 /* bad record checks */
332 if (!fork || (fork->logicalSize == 0) || (fork->totalBlocks == 0)) {
333 cli_dbgmsg("hfsplus_scanfile: Empty file.\n");
334 return CL_CLEAN;
335 }
336
337 /* check limits */
338 targetSize = fork->logicalSize;
339 #if SIZEOF_LONG < 8
340 if (targetSize > ULONG_MAX) {
341 cli_dbgmsg("hfsplus_scanfile: File too large for limit check.\n");
342 return CL_EFORMAT;
343 }
344 #endif
345 ret = cli_checklimits("hfsplus_scanfile", ctx, (unsigned long)targetSize, 0, 0);
346 if (ret != CL_CLEAN) {
347 return ret;
348 }
349
350 /* open file */
351 ret = cli_gentempfd(dirname, &tmpname, &ofd);
352 if (ret != CL_CLEAN) {
353 cli_dbgmsg("hfsplus_scanfile: Cannot generate temporary file.\n");
354 return ret;
355 }
356 cli_dbgmsg("hfsplus_scanfile: Extracting to %s\n", tmpname);
357
358 ext = 0;
359 /* Dump file, extent by extent */
360 do {
361 uint32_t currBlock, endBlock, outputSize = 0;
362 if (targetSize == 0) {
363 cli_dbgmsg("hfsplus_scanfile: output complete\n");
364 break;
365 }
366 if (outputBlocks >= fork->totalBlocks) {
367 cli_dbgmsg("hfsplus_scanfile: output all blocks, remaining size " STDu64 "\n", targetSize);
368 break;
369 }
370 /* Prepare extent */
371 if (ext < 8) {
372 currExt = &(fork->extents[ext]);
373 cli_dbgmsg("hfsplus_scanfile: extent %u\n", ext);
374 } else {
375 cli_dbgmsg("hfsplus_scanfile: need next extent from ExtentOverflow\n");
376 /* Not implemented yet */
377 ret = CL_EFORMAT;
378 break;
379 }
380 /* have extent, so validate and get block range */
381 if ((currExt->startBlock == 0) || (currExt->blockCount == 0)) {
382 cli_dbgmsg("hfsplus_scanfile: next extent empty, done\n");
383 break;
384 }
385 if ((currExt->startBlock & 0x10000000) && (currExt->blockCount & 0x10000000)) {
386 cli_dbgmsg("hfsplus_scanfile: next extent illegal!\n");
387 ret = CL_EFORMAT;
388 break;
389 }
390 currBlock = currExt->startBlock;
391 endBlock = currExt->startBlock + currExt->blockCount - 1;
392 if ((currBlock > volHeader->totalBlocks) || (endBlock > volHeader->totalBlocks) || (currExt->blockCount > volHeader->totalBlocks)) {
393 cli_dbgmsg("hfsplus_scanfile: bad extent!\n");
394 ret = CL_EFORMAT;
395 break;
396 }
397 /* Write the blocks, walking the map */
398 while (currBlock <= endBlock) {
399 size_t to_write = MIN(targetSize, volHeader->blockSize);
400 size_t written;
401 off_t offset = currBlock * volHeader->blockSize;
402 /* move map to next block */
403 mPtr = fmap_need_off_once(ctx->fmap, offset, volHeader->blockSize);
404 if (!mPtr) {
405 cli_errmsg("hfsplus_scanfile: map error\n");
406 ret = CL_EMAP;
407 break;
408 }
409 written = cli_writen(ofd, mPtr, to_write);
410 if (written != to_write) {
411 cli_errmsg("hfsplus_scanfile: write error\n");
412 ret = CL_EWRITE;
413 break;
414 }
415 targetSize -= to_write;
416 outputSize += to_write;
417 currBlock++;
418 if (targetSize == 0) {
419 cli_dbgmsg("hfsplus_scanfile: all data written\n");
420 break;
421 }
422 if (outputBlocks >= fork->totalBlocks) {
423 cli_dbgmsg("hfsplus_scanfile: output all blocks, remaining size " STDu64 "\n", targetSize);
424 break;
425 }
426 }
427 /* Finished the extent, move to next */
428 ext++;
429 } while (ret == CL_CLEAN);
430
431 /* if successful so far, scan the output */
432 if (filename) {
433 *filename = tmpname;
434 } else {
435 if (ret == CL_CLEAN) {
436 ret = cli_magic_scan_desc(ofd, tmpname, ctx, orig_filename);
437 }
438
439 if (!ctx->engine->keeptmp) {
440 if (cli_unlink(tmpname)) {
441 ret = CL_EUNLINK;
442 }
443 }
444 free(tmpname);
445 }
446
447 if (ofd >= 0) {
448 close(ofd);
449 }
450
451 return ret;
452 }
453
454 /* Calculate true node limit for catalogFile */
hfsplus_validate_catalog(cli_ctx * ctx,hfsPlusVolumeHeader * volHeader,hfsHeaderRecord * catHeader)455 static int hfsplus_validate_catalog(cli_ctx *ctx, hfsPlusVolumeHeader *volHeader, hfsHeaderRecord *catHeader)
456 {
457 hfsPlusForkData *catFork;
458
459 UNUSEDPARAM(ctx);
460
461 catFork = &(volHeader->catalogFile);
462 if (catFork->totalBlocks >= volHeader->totalBlocks) {
463 cli_dbgmsg("hfsplus_validate_catalog: catFork totalBlocks too large!\n");
464 return CL_EFORMAT;
465 }
466 if (catFork->logicalSize > (catFork->totalBlocks * volHeader->blockSize)) {
467 cli_dbgmsg("hfsplus_validate_catalog: catFork logicalSize too large!\n");
468 return CL_EFORMAT;
469 }
470 if (catFork->logicalSize < (catHeader->totalNodes * catHeader->nodeSize)) {
471 cli_dbgmsg("hfsplus_validate_catalog: too many nodes for catFile\n");
472 return CL_EFORMAT;
473 }
474
475 return CL_CLEAN;
476 }
477
478 /* Check if an attribute is present in the attribute map */
hfsplus_check_attribute(cli_ctx * ctx,hfsPlusVolumeHeader * volHeader,hfsHeaderRecord * attrHeader,uint32_t expectedCnid,const uint8_t name[],uint32_t nameLen,int * found,uint8_t record[],unsigned * recordSize)479 static cl_error_t hfsplus_check_attribute(cli_ctx *ctx, hfsPlusVolumeHeader *volHeader, hfsHeaderRecord *attrHeader, uint32_t expectedCnid, const uint8_t name[], uint32_t nameLen, int *found, uint8_t record[], unsigned *recordSize)
480 {
481 uint16_t nodeSize, recordNum, topOfOffsets;
482 uint16_t recordStart, nextDist, nextStart;
483 uint8_t *nodeBuf = NULL;
484 uint32_t thisNode, nodeLimit, nodesScanned = 0;
485 cl_error_t ret = CL_SUCCESS;
486 int foundAttr = 0;
487
488 if (found) {
489 *found = 0;
490 }
491
492 if (!attrHeader) {
493 return CL_EARG;
494 }
495
496 nodeLimit = MIN(attrHeader->totalNodes, HFSPLUS_NODE_LIMIT);
497 thisNode = attrHeader->firstLeafNode;
498 nodeSize = attrHeader->nodeSize;
499
500 /* Need to buffer current node, map will keep moving */
501 nodeBuf = cli_malloc(nodeSize);
502 if (!nodeBuf) {
503 cli_dbgmsg("hfsplus_check_attribute: failed to acquire node buffer, "
504 "size " STDu32 "\n",
505 nodeSize);
506 return CL_EMEM;
507 }
508
509 /* Walk catalog leaf nodes, and scan contents of each */
510 /* Because we want to scan them all, the index nodes add no value */
511 while (ret == CL_CLEAN && !foundAttr) {
512 hfsNodeDescriptor nodeDesc;
513
514 if (thisNode == 0) {
515 cli_dbgmsg("hfsplus_check_attribute: reached end of leaf nodes.\n");
516 break;
517 }
518 if (nodesScanned++ > nodeLimit) {
519 cli_dbgmsg("hfsplus_check_attribute: node scan limit reached.\n");
520 break;
521 }
522
523 /* fetch node into buffer */
524 ret = hfsplus_fetch_node(ctx, volHeader, attrHeader, NULL, &(volHeader->attributesFile), thisNode, nodeBuf);
525 if (ret != CL_CLEAN) {
526 cli_dbgmsg("hfsplus_check_attribute: node fetch failed.\n");
527 break;
528 }
529 memcpy(&nodeDesc, nodeBuf, 14);
530
531 /* convert and validate node */
532 nodedescriptor_to_host(&nodeDesc);
533 nodedescriptor_print("leaf attribute node", &nodeDesc);
534 if ((nodeDesc.kind != HFS_NODEKIND_LEAF) || (nodeDesc.height != 1)) {
535 cli_dbgmsg("hfsplus_check_attribute: invalid leaf node!\n");
536 ret = CL_EFORMAT;
537 break;
538 }
539 if ((nodeSize / 4) < nodeDesc.numRecords) {
540 cli_dbgmsg("hfsplus_check_attribute: too many leaf records for one node!\n");
541 ret = CL_EFORMAT;
542 break;
543 }
544
545 /* Walk this node's records and scan */
546 recordStart = 14; /* 1st record can be after end of node descriptor */
547 /* offsets take 1 u16 per at the end of the node, along with an empty space offset */
548 topOfOffsets = nodeSize - (nodeDesc.numRecords * 2) - 2;
549 for (recordNum = 0; recordNum < nodeDesc.numRecords; recordNum++) {
550 uint16_t keylen;
551 hfsPlusAttributeKey attrKey;
552 hfsPlusAttributeRecord attrRec;
553
554 /* Locate next record */
555 nextDist = nodeSize - (recordNum * 2) - 2;
556 nextStart = nodeBuf[nextDist] * 0x100 + nodeBuf[nextDist + 1];
557 /* Check record location */
558 if ((nextStart > topOfOffsets - 1) || (nextStart < recordStart)) {
559 cli_dbgmsg("hfsplus_check_attribute: bad record location %x for %u!\n", nextStart, recordNum);
560 ret = CL_EFORMAT;
561 break;
562 }
563 recordStart = nextStart;
564 if (recordStart + sizeof(attrKey) >= topOfOffsets) {
565 cli_dbgmsg("hfsplus_check_attribute: Not enough data for an attribute key at location %x for %u!\n",
566 nextStart, recordNum);
567 ret = CL_EFORMAT;
568 break;
569 }
570
571 memcpy(&attrKey, &nodeBuf[recordStart], sizeof(attrKey));
572 attrKey.keyLength = be16_to_host(attrKey.keyLength);
573 attrKey.cnid = be32_to_host(attrKey.cnid);
574 attrKey.startBlock = be32_to_host(attrKey.startBlock);
575 attrKey.nameLength = be16_to_host(attrKey.nameLength);
576
577 /* Get record key length */
578 keylen = nodeBuf[recordStart] * 0x100 + nodeBuf[recordStart + 1];
579 keylen += keylen % 2; /* pad 1 byte if required to make 2-byte align */
580 /* Validate keylen */
581 if (recordStart + attrKey.keyLength + 4 >= topOfOffsets) {
582 cli_dbgmsg("hfsplus_check_attribute: key too long for location %x for %u!\n",
583 nextStart, recordNum);
584 ret = CL_EFORMAT;
585 break;
586 }
587
588 if (recordStart + sizeof(hfsPlusAttributeKey) + attrKey.nameLength >= topOfOffsets) {
589 cli_dbgmsg("hfsplus_check_attribute: Attribute name is longer than expected: %u\n", attrKey.nameLength);
590 ret = CL_EFORMAT;
591 break;
592 }
593
594 if (attrKey.cnid == expectedCnid && attrKey.nameLength * 2 == nameLen && memcmp(&nodeBuf[recordStart + 14], name, nameLen) == 0) {
595 memcpy(&attrRec, &(nodeBuf[recordStart + sizeof(hfsPlusAttributeKey) + attrKey.nameLength * 2]), sizeof(attrRec));
596 attrRec.recordType = be32_to_host(attrRec.recordType);
597 attrRec.attributeSize = be32_to_host(attrRec.attributeSize);
598
599 if (attrRec.recordType != HFSPLUS_RECTYPE_INLINE_DATA_ATTRIBUTE) {
600 cli_dbgmsg("hfsplus_check_attribute: Unexpected attribute record type 0x%x\n", attrRec.recordType);
601 continue;
602 }
603
604 if (found) {
605 *found = 1;
606 }
607
608 if (attrRec.attributeSize > *recordSize) {
609 ret = CL_EMAXSIZE;
610 break;
611 }
612
613 memcpy(record, &(nodeBuf[recordStart + sizeof(hfsPlusAttributeKey) + attrKey.nameLength * 2 + sizeof(attrRec)]), attrRec.attributeSize);
614 *recordSize = attrRec.attributeSize;
615
616 ret = CL_SUCCESS;
617 foundAttr = 1;
618 break;
619 }
620 }
621 }
622
623 if (nodeBuf != NULL) {
624 free(nodeBuf);
625 nodeBuf = NULL;
626 }
627 return ret;
628 }
629
630 /* Fetch a node's contents into the buffer */
hfsplus_fetch_node(cli_ctx * ctx,hfsPlusVolumeHeader * volHeader,hfsHeaderRecord * catHeader,hfsHeaderRecord * extHeader,hfsPlusForkData * catFork,uint32_t node,uint8_t * buff)631 static int hfsplus_fetch_node(cli_ctx *ctx, hfsPlusVolumeHeader *volHeader, hfsHeaderRecord *catHeader,
632 hfsHeaderRecord *extHeader, hfsPlusForkData *catFork, uint32_t node, uint8_t *buff)
633 {
634 int foundBlock = 0;
635 uint64_t catalogOffset;
636 uint32_t startBlock, startOffset;
637 uint32_t endBlock, endSize;
638 uint32_t curBlock;
639 uint32_t extentNum = 0, realFileBlock;
640 uint32_t readSize;
641 size_t fileOffset = 0;
642 uint32_t searchBlock;
643 uint32_t buffOffset = 0;
644
645 UNUSEDPARAM(extHeader);
646
647 /* Make sure node is in range */
648 if (node >= catHeader->totalNodes) {
649 cli_dbgmsg("hfsplus_fetch_node: invalid node number " STDu32 "\n", node);
650 return CL_EFORMAT;
651 }
652
653 /* Need one block */
654 /* First, calculate the node's offset within the catalog */
655 catalogOffset = (uint64_t)node * catHeader->nodeSize;
656 /* Determine which block of the catalog we need */
657 startBlock = (uint32_t)(catalogOffset / volHeader->blockSize);
658 startOffset = (uint32_t)(catalogOffset % volHeader->blockSize);
659 endBlock = (uint32_t)((catalogOffset + catHeader->nodeSize - 1) / volHeader->blockSize);
660 endSize = (uint32_t)(((catalogOffset + catHeader->nodeSize - 1) % volHeader->blockSize) + 1);
661 cli_dbgmsg("hfsplus_fetch_node: need catalog block " STDu32 "\n", startBlock);
662 if (startBlock >= catFork->totalBlocks || endBlock >= catFork->totalBlocks) {
663 cli_dbgmsg("hfsplus_fetch_node: block number invalid!\n");
664 return CL_EFORMAT;
665 }
666
667 for (curBlock = startBlock; curBlock <= endBlock; ++curBlock) {
668
669 foundBlock = 0;
670 searchBlock = curBlock;
671 /* Find which extent has that block */
672 for (extentNum = 0; extentNum < 8; extentNum++) {
673 hfsPlusExtentDescriptor *currExt = &(catFork->extents[extentNum]);
674
675 /* Beware empty extent */
676 if ((currExt->startBlock == 0) || (currExt->blockCount == 0)) {
677 cli_dbgmsg("hfsplus_fetch_node: extent " STDu32 " empty!\n", extentNum);
678 return CL_EFORMAT;
679 }
680 /* Beware too long extent */
681 if ((currExt->startBlock & 0x10000000) && (currExt->blockCount & 0x10000000)) {
682 cli_dbgmsg("hfsplus_fetch_node: extent " STDu32 " illegal!\n", extentNum);
683 return CL_EFORMAT;
684 }
685 /* Check if block found in current extent */
686 if (searchBlock < currExt->blockCount) {
687 cli_dbgmsg("hfsplus_fetch_node: found block in extent " STDu32 "\n", extentNum);
688 realFileBlock = currExt->startBlock + searchBlock;
689 foundBlock = 1;
690 break;
691 } else {
692 cli_dbgmsg("hfsplus_fetch_node: not in extent " STDu32 "\n", extentNum);
693 searchBlock -= currExt->blockCount;
694 }
695 }
696
697 if (foundBlock == 0) {
698 cli_dbgmsg("hfsplus_fetch_node: not in first 8 extents\n");
699 cli_dbgmsg("hfsplus_fetch_node: finding this node requires extent overflow support\n");
700 return CL_EFORMAT;
701 }
702
703 /* Block found */
704 if (realFileBlock >= volHeader->totalBlocks) {
705 cli_dbgmsg("hfsplus_fetch_node: block past end of volume\n");
706 return CL_EFORMAT;
707 }
708 fileOffset = realFileBlock * volHeader->blockSize;
709 readSize = volHeader->blockSize;
710
711 if (curBlock == startBlock) {
712 fileOffset += startOffset;
713 } else if (curBlock == endBlock) {
714 readSize = endSize;
715 }
716
717 if (fmap_readn(ctx->fmap, buff + buffOffset, fileOffset, readSize) != readSize) {
718 cli_dbgmsg("hfsplus_fetch_node: not all bytes read\n");
719 return CL_EFORMAT;
720 }
721 buffOffset += readSize;
722 }
723
724 return CL_CLEAN;
725 }
726
hfsplus_seek_to_cmpf_resource(int fd,size_t * size)727 static cl_error_t hfsplus_seek_to_cmpf_resource(int fd, size_t *size)
728 {
729 hfsPlusResourceHeader resourceHeader;
730 hfsPlusResourceMap resourceMap;
731 hfsPlusResourceType resourceType;
732 hfsPlusReferenceEntry entry;
733 int i;
734 int cmpfInstanceIdx = -1;
735 int curInstanceIdx = 0;
736 size_t dataOffset;
737 uint32_t dataLength;
738 cl_error_t ret = CL_SUCCESS;
739
740 if (!size) {
741 ret = CL_ENULLARG;
742 goto done;
743 }
744
745 if (cli_readn(fd, &resourceHeader, sizeof(resourceHeader)) != sizeof(resourceHeader)) {
746 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to read resource header from temporary file\n");
747 ret = CL_EREAD;
748 goto done;
749 }
750
751 resourceHeader.dataOffset = be32_to_host(resourceHeader.dataOffset);
752 resourceHeader.mapOffset = be32_to_host(resourceHeader.mapOffset);
753 resourceHeader.dataLength = be32_to_host(resourceHeader.dataLength);
754 resourceHeader.mapLength = be32_to_host(resourceHeader.mapLength);
755
756 //TODO: Need to get offset of cmpf resource in data stream
757
758 if (lseek(fd, resourceHeader.mapOffset, SEEK_SET) != resourceHeader.mapOffset) {
759 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to seek to map in temporary file\n");
760 ret = CL_ESEEK;
761 goto done;
762 }
763
764 if (cli_readn(fd, &resourceMap, sizeof(resourceMap)) != sizeof(resourceMap)) {
765 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to read resource map from temporary file\n");
766 ret = CL_EREAD;
767 goto done;
768 }
769
770 resourceMap.resourceForkAttributes = be16_to_host(resourceMap.resourceForkAttributes);
771 resourceMap.typeListOffset = be16_to_host(resourceMap.typeListOffset);
772 resourceMap.nameListOffset = be16_to_host(resourceMap.nameListOffset);
773 resourceMap.typeCount = be16_to_host(resourceMap.typeCount);
774
775 for (i = 0; i < resourceMap.typeCount + 1; ++i) {
776 if (cli_readn(fd, &resourceType, sizeof(resourceType)) != sizeof(resourceType)) {
777 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to read resource type from temporary file\n");
778 ret = CL_EREAD;
779 goto done;
780 }
781 resourceType.instanceCount = be16_to_host(resourceType.instanceCount);
782 resourceType.referenceListOffset = be16_to_host(resourceType.referenceListOffset);
783
784 if (memcmp(resourceType.type, "cmpf", 4) == 0) {
785 if (cmpfInstanceIdx != -1) {
786 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: There are several cmpf resource types in the file\n");
787 ret = CL_EFORMAT;
788 goto done;
789 }
790
791 cmpfInstanceIdx = curInstanceIdx;
792 cli_dbgmsg("Found compressed resource type!\n");
793 }
794
795 curInstanceIdx += resourceType.instanceCount + 1;
796 }
797
798 if (cmpfInstanceIdx < 0) {
799 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Didn't find cmpf resource type\n");
800 ret = CL_EFORMAT;
801 goto done;
802 }
803
804 if (lseek(fd, cmpfInstanceIdx * sizeof(hfsPlusReferenceEntry), SEEK_CUR) < 0) {
805 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to seek to instance index\n");
806 ret = CL_ESEEK;
807 goto done;
808 }
809
810 if (cli_readn(fd, &entry, sizeof(entry)) != sizeof(entry)) {
811 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to read resource entry from temporary file\n");
812 ret = CL_EREAD;
813 goto done;
814 }
815
816 dataOffset = (entry.resourceDataOffset[0] << 16) | (entry.resourceDataOffset[1] << 8) | entry.resourceDataOffset[2];
817
818 if (lseek(fd, resourceHeader.dataOffset + dataOffset, SEEK_SET) < 0) {
819 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to seek to data offset\n");
820 ret = CL_ESEEK;
821 goto done;
822 }
823
824 if (cli_readn(fd, &dataLength, sizeof(dataLength)) != sizeof(dataLength)) {
825 cli_dbgmsg("hfsplus_seek_to_cmpf_resource: Failed to read data length from temporary file\n");
826 ret = CL_EREAD;
827 goto done;
828 }
829
830 *size = be32_to_host(dataLength);
831 done:
832 return ret;
833 }
834
hfsplus_read_block_table(int fd,uint32_t * numBlocks,hfsPlusResourceBlockTable ** table)835 static int hfsplus_read_block_table(int fd, uint32_t *numBlocks, hfsPlusResourceBlockTable **table)
836 {
837 uint32_t i;
838
839 if (!table || !numBlocks) {
840 return CL_ENULLARG;
841 }
842
843 if (cli_readn(fd, numBlocks, sizeof(*numBlocks)) != sizeof(*numBlocks)) {
844 cli_dbgmsg("hfsplus_read_block_table: Failed to read block count\n");
845 return CL_EREAD;
846 }
847
848 *numBlocks = le32_to_host(*numBlocks); //Let's do a little little endian just for fun, shall we?
849 *table = cli_malloc(sizeof(hfsPlusResourceBlockTable) * *numBlocks);
850 if (!*table) {
851 cli_dbgmsg("hfsplus_read_block_table: Failed to allocate memory for block table\n");
852 return CL_EMEM;
853 }
854
855 if (cli_readn(fd, *table, *numBlocks * sizeof(hfsPlusResourceBlockTable)) != *numBlocks * sizeof(hfsPlusResourceBlockTable)) {
856 cli_dbgmsg("hfsplus_read_block_table: Failed to read table\n");
857 free(*table);
858 return CL_EREAD;
859 }
860
861 for (i = 0; i < *numBlocks; ++i) {
862 (*table)[i].offset = le32_to_host((*table)[i].offset);
863 (*table)[i].length = le32_to_host((*table)[i].length);
864 }
865
866 return CL_SUCCESS;
867 }
868
869 /* Given the catalog and other details, scan all the volume contents */
hfsplus_walk_catalog(cli_ctx * ctx,hfsPlusVolumeHeader * volHeader,hfsHeaderRecord * catHeader,hfsHeaderRecord * extHeader,hfsHeaderRecord * attrHeader,const char * dirname)870 static cl_error_t hfsplus_walk_catalog(cli_ctx *ctx, hfsPlusVolumeHeader *volHeader, hfsHeaderRecord *catHeader,
871 hfsHeaderRecord *extHeader, hfsHeaderRecord *attrHeader, const char *dirname)
872 {
873 cl_error_t ret = CL_SUCCESS;
874 unsigned int has_alerts = 0;
875 uint32_t thisNode, nodeLimit, nodesScanned = 0;
876 uint16_t nodeSize, recordNum, topOfOffsets;
877 uint16_t recordStart, nextDist, nextStart;
878 uint8_t *nodeBuf = NULL;
879 const uint8_t COMPRESSED_ATTR[] = {0, 'c', 0, 'o', 0, 'm', 0, '.', 0, 'a', 0, 'p', 0, 'p', 0, 'l', 0, 'e', 0, '.', 0, 'd', 0, 'e', 0, 'c', 0, 'm', 0, 'p', 0, 'f', 0, 's'};
880 char *tmpname = NULL;
881 uint8_t *uncompressed = NULL;
882 int ofd = -1;
883 char *name_utf8 = NULL;
884 size_t name_utf8_size = 0;
885
886 nodeLimit = MIN(catHeader->totalNodes, HFSPLUS_NODE_LIMIT);
887 thisNode = catHeader->firstLeafNode;
888 nodeSize = catHeader->nodeSize;
889
890 /* Need to buffer current node, map will keep moving */
891 nodeBuf = cli_malloc(nodeSize);
892 if (!nodeBuf) {
893 cli_dbgmsg("hfsplus_walk_catalog: failed to acquire node buffer, "
894 "size " STDu32 "\n",
895 nodeSize);
896 return CL_EMEM;
897 }
898
899 /* Walk catalog leaf nodes, and scan contents of each */
900 /* Because we want to scan them all, the index nodes add no value */
901 while (ret == CL_SUCCESS) {
902 hfsNodeDescriptor nodeDesc;
903
904 if (thisNode == 0) {
905 cli_dbgmsg("hfsplus_walk_catalog: reached end of leaf nodes.\n");
906 break;
907 }
908 if (nodesScanned++ > nodeLimit) {
909 cli_dbgmsg("hfsplus_walk_catalog: node scan limit reached.\n");
910 break;
911 }
912
913 /* fetch node into buffer */
914 ret = hfsplus_fetch_node(ctx, volHeader, catHeader, extHeader, &(volHeader->catalogFile), thisNode, nodeBuf);
915 if (ret != CL_SUCCESS) {
916 cli_dbgmsg("hfsplus_walk_catalog: node fetch failed.\n");
917 break;
918 }
919 memcpy(&nodeDesc, nodeBuf, 14);
920
921 /* convert and validate node */
922 nodedescriptor_to_host(&nodeDesc);
923 nodedescriptor_print("leaf node", &nodeDesc);
924 if ((nodeDesc.kind != HFS_NODEKIND_LEAF) || (nodeDesc.height != 1)) {
925 cli_dbgmsg("hfsplus_walk_catalog: invalid leaf node!\n");
926 ret = CL_EFORMAT;
927 break;
928 }
929 if ((nodeSize / 4) < nodeDesc.numRecords) {
930 cli_dbgmsg("hfsplus_walk_catalog: too many leaf records for one node!\n");
931 ret = CL_EFORMAT;
932 break;
933 }
934
935 /* Walk this node's records and scan */
936 recordStart = 14; /* 1st record can be after end of node descriptor */
937 /* offsets take 1 u16 per at the end of the node, along with an empty space offset */
938 topOfOffsets = nodeSize - (nodeDesc.numRecords * 2) - 2;
939 for (recordNum = 0; recordNum < nodeDesc.numRecords; recordNum++) {
940 uint16_t keylen;
941 int16_t rectype;
942 hfsPlusCatalogFile fileRec;
943 name_utf8 = NULL;
944
945 /* Locate next record */
946 nextDist = nodeSize - (recordNum * 2) - 2;
947 nextStart = nodeBuf[nextDist] * 0x100 + nodeBuf[nextDist + 1];
948 /* Check record location */
949 if ((nextStart > topOfOffsets - 1) || (nextStart < recordStart)) {
950 cli_dbgmsg("hfsplus_walk_catalog: bad record location %x for %u!\n", nextStart, recordNum);
951 ret = CL_EFORMAT;
952 break;
953 }
954 recordStart = nextStart;
955 /* Get record key length */
956 keylen = nodeBuf[recordStart] * 0x100 + nodeBuf[recordStart + 1];
957 keylen += keylen % 2; /* pad 1 byte if required to make 2-byte align */
958 /* Validate keylen */
959 if (recordStart + keylen + 4 >= topOfOffsets) {
960 cli_dbgmsg("hfsplus_walk_catalog: key too long for location %x for %u!\n",
961 nextStart, recordNum);
962 ret = CL_EFORMAT;
963 break;
964 }
965 /* Collect filename */
966 if (keylen >= 6) {
967 uint16_t name_length = (nodeBuf[recordStart + 2 + 4] << 8) | nodeBuf[recordStart + 2 + 4 + 1];
968 char *index = (char *)&nodeBuf[recordStart + 2 + 4 + 2];
969 if ((name_length > 0) && (name_length * 2 <= keylen - 2 - 4)) {
970 /*
971 * The name is contained in nodeBuf[recordStart + 2 + 4 + 2 : recordStart + 2 + 4 + 2 + name_length * 2] encoded as UTF-16BE.
972 */
973 if (CL_SUCCESS != cli_codepage_to_utf8((char *)index, name_length * 2, CODEPAGE_UTF16_BE, &name_utf8, &name_utf8_size)) {
974 cli_errmsg("hfsplus_walk_catalog: failed to convert UTF-16BE to UTF-8\n");
975 name_utf8 = NULL;
976 }
977 cli_dbgmsg("hfsplus_walk_catalog: Extracting file %s\n", name_utf8);
978 }
979 }
980 /* Copy type (after key, which is after keylength field) */
981 memcpy(&rectype, &(nodeBuf[recordStart + keylen + 2]), 2);
982 rectype = be16_to_host(rectype);
983 cli_dbgmsg("hfsplus_walk_catalog: record %u nextStart %x keylen %u type %d\n",
984 recordNum, nextStart, keylen, rectype);
985 /* Non-file records are not needed */
986 if (rectype != HFSPLUS_RECTYPE_FILE) {
987 if (NULL != name_utf8) {
988 free(name_utf8);
989 name_utf8 = NULL;
990 }
991 continue;
992 }
993 /* Check file record location */
994 if (recordStart + keylen + 2 + sizeof(hfsPlusCatalogFile) >= topOfOffsets) {
995 cli_dbgmsg("hfsplus_walk_catalog: not enough bytes for file record!\n");
996 ret = CL_EFORMAT;
997 break;
998 }
999 memcpy(&fileRec, &(nodeBuf[recordStart + keylen + 2]), sizeof(hfsPlusCatalogFile));
1000
1001 /* Only scan files */
1002 fileRec.fileID = be32_to_host(fileRec.fileID);
1003 fileRec.permissions.fileMode = be16_to_host(fileRec.permissions.fileMode);
1004 if ((fileRec.permissions.fileMode & HFS_MODE_TYPEMASK) == HFS_MODE_FILE) {
1005 int compressed = 0;
1006 uint8_t attribute[8192];
1007 unsigned attributeSize = sizeof(attribute);
1008
1009 /* Convert forks and scan */
1010 forkdata_to_host(&(fileRec.dataFork));
1011 forkdata_print("data fork:", &(fileRec.dataFork));
1012 forkdata_to_host(&(fileRec.resourceFork));
1013 forkdata_print("resource fork:", &(fileRec.resourceFork));
1014
1015 if (hfsplus_check_attribute(ctx, volHeader, attrHeader, fileRec.fileID, COMPRESSED_ATTR, sizeof(COMPRESSED_ATTR), &compressed, attribute, &attributeSize) != CL_SUCCESS) {
1016 cli_dbgmsg("hfsplus_walk_catalog: Failed to check compressed attribute, assuming no compression\n");
1017 }
1018
1019 if (compressed) {
1020 hfsPlusCompressionHeader header;
1021 cli_dbgmsg("hfsplus_walk_catalog: File is compressed\n");
1022
1023 if (attributeSize < sizeof(header)) {
1024 cli_warnmsg("hfsplus_walk_catalog: Error: Compression attribute size is less than the compression header\n");
1025 ret = CL_EFORMAT;
1026 break;
1027 }
1028
1029 memcpy(&header, attribute, sizeof(header));
1030 //In the sample I had (12de189078b1e260d669a2b325d688a3a39cb5b9697e00fb1777e1ecc64f4e91), this was stored in little endian.
1031 //According to the doc, it should be in big endian.
1032
1033 if (header.magic == DECMPFS_HEADER_MAGIC_LE) {
1034 header.magic = cbswap32(header.magic);
1035 header.compressionType = cbswap32(header.compressionType);
1036 header.fileSize = cbswap64(header.fileSize);
1037 }
1038
1039 if (header.magic != DECMPFS_HEADER_MAGIC) {
1040 cli_dbgmsg("hfsplus_walk_catalog: Unexpected magic value for compression header: 0x%08x\n", header.magic);
1041 ret = CL_EFORMAT;
1042 break;
1043 }
1044
1045 /* open file */
1046 ret = cli_gentempfd(dirname, &tmpname, &ofd);
1047 if (ret != CL_SUCCESS) {
1048 cli_dbgmsg("hfsplus_walk_catalog: Cannot generate temporary file.\n");
1049 break;
1050 }
1051
1052 cli_dbgmsg("Found compressed file type %u size %" PRIu64 "\n", header.compressionType, header.fileSize);
1053 switch (header.compressionType) {
1054 case HFSPLUS_COMPRESSION_INLINE: {
1055 size_t written;
1056 if (attributeSize < sizeof(header) + 1) {
1057 cli_dbgmsg("hfsplus_walk_catalog: Unexpected end of stream, no compression flag\n");
1058 ret = CL_EFORMAT;
1059 break;
1060 }
1061
1062 if ((attribute[sizeof(header)] & 0x0f) == 0x0f) { //Data is stored uncompressed
1063 if (attributeSize - sizeof(header) - 1 != header.fileSize) {
1064 cli_dbgmsg("hfsplus_walk_catalog: Expected file size different from size of data available\n");
1065 free(tmpname);
1066 ret = CL_EFORMAT;
1067 break;
1068 }
1069
1070 written = cli_writen(ofd, &attribute[sizeof(header) + 1], header.fileSize);
1071 } else {
1072 z_stream stream;
1073 int z_ret;
1074
1075 if (header.fileSize > 65536) {
1076 cli_dbgmsg("hfsplus_walk_catalog: Uncompressed file seems too big, something is probably wrong\n");
1077 ret = CL_EFORMAT;
1078 break;
1079 }
1080
1081 uncompressed = malloc(header.fileSize);
1082 if (!uncompressed) {
1083 cli_dbgmsg("hfsplus_walk_catalog: Failed to allocate memory for the uncompressed file contents\n");
1084 ret = CL_EMEM;
1085 break;
1086 }
1087
1088 stream.zalloc = Z_NULL;
1089 stream.zfree = Z_NULL;
1090 stream.opaque = Z_NULL;
1091 stream.avail_in = attributeSize - sizeof(header);
1092 stream.next_in = &attribute[sizeof(header)];
1093 stream.avail_out = header.fileSize;
1094 stream.next_out = uncompressed;
1095
1096 z_ret = inflateInit2(&stream, 15 /* maximum windowBits size */);
1097 if (z_ret != Z_OK) {
1098 switch (z_ret) {
1099 case Z_MEM_ERROR:
1100 cli_dbgmsg("hfsplus_walk_catalog: inflateInit2: out of memory!\n");
1101 break;
1102 case Z_VERSION_ERROR:
1103 cli_dbgmsg("hfsplus_walk_catalog: inflateinit2: zlib version error!\n");
1104 break;
1105 case Z_STREAM_ERROR:
1106 cli_dbgmsg("hfsplus_walk_catalog: inflateinit2: zlib stream error!\n");
1107 break;
1108 default:
1109 cli_dbgmsg("hfsplus_walk_catalog: inflateInit2: unknown error %d\n", ret);
1110 break;
1111 }
1112
1113 ret = CL_EFORMAT;
1114 break;
1115 }
1116
1117 z_ret = inflate(&stream, Z_NO_FLUSH);
1118 if (z_ret != Z_OK && z_ret != Z_STREAM_END) {
1119 cli_dbgmsg("hfsplus_walk_catalog: inflateSync failed to extract compressed stream (%d)\n", ret);
1120 ret = CL_EFORMAT;
1121 break;
1122 }
1123
1124 z_ret = inflateEnd(&stream);
1125 if (z_ret == Z_STREAM_ERROR) {
1126 cli_dbgmsg("hfsplus_walk_catalog: inflateEnd failed (%d)\n", ret);
1127 }
1128
1129 written = cli_writen(ofd, uncompressed, header.fileSize);
1130 free(uncompressed);
1131 uncompressed = NULL;
1132 }
1133 if (written != header.fileSize) {
1134 cli_errmsg("hfsplus_walk_catalog: write error\n");
1135 ret = CL_EWRITE;
1136 break;
1137 }
1138
1139 ret = CL_SUCCESS;
1140 break;
1141 }
1142 case HFSPLUS_COMPRESSION_RESOURCE: {
1143 //FIXME: This is hackish. We're assuming (which is
1144 //correct according to the spec) that there's only
1145 //one resource, and that it's the compressed data.
1146 //Ideally we should check that there is only one
1147 //resource, that its type is correct, and that its
1148 //name is cmpf.
1149 char *resourceFile = NULL;
1150 int ifd = -1;
1151 size_t written = 0;
1152
1153 //4096 is an approximative value, there should be
1154 //at least 16 (resource header) + 30 (map header) +
1155 //4096 bytes (data that doesn't fit in an
1156 //attribute)
1157 if (fileRec.resourceFork.logicalSize < 4096) {
1158 cli_dbgmsg("hfsplus_walk_catalog: Error: Expected more data in the compressed resource fork\n");
1159 ret = CL_EFORMAT;
1160 break;
1161 }
1162
1163 if ((ret = hfsplus_scanfile(ctx, volHeader, extHeader, &(fileRec.resourceFork), dirname, &resourceFile, name_utf8)) != CL_SUCCESS) {
1164 cli_dbgmsg("hfsplus_walk_catalog: Error while extracting the resource fork\n");
1165 if (resourceFile) {
1166 free(resourceFile);
1167 }
1168 break;
1169 }
1170
1171 if (NULL == resourceFile) {
1172 cli_dbgmsg("hfsplus_walk_catalog: Error: hfsplus_scanfile returned no resource file\n");
1173 ret = CL_EFORMAT;
1174 break;
1175 }
1176
1177 if ((ifd = safe_open(resourceFile, O_RDONLY | O_BINARY)) == -1) {
1178 cli_dbgmsg("hfsplus_walk_catalog: Failed to open temporary file %s\n", resourceFile);
1179 ret = CL_EOPEN;
1180 } else {
1181 size_t resourceLen;
1182 if ((ret = hfsplus_seek_to_cmpf_resource(ifd, &resourceLen)) != CL_SUCCESS) {
1183 cli_dbgmsg("hfsplus_walk_catalog: Failed to find cmpf resource in resource fork\n");
1184 } else {
1185 hfsPlusResourceBlockTable *table = NULL;
1186 uint32_t numBlocks;
1187 uint32_t dataOffset = lseek(ifd, 0, SEEK_CUR);
1188
1189 if ((ret = hfsplus_read_block_table(ifd, &numBlocks, &table)) != CL_SUCCESS) {
1190 cli_dbgmsg("hfsplus_walk_catalog: Failed to read block table\n");
1191 } else {
1192 uint8_t block[4096];
1193 uint8_t uncompressed[4096];
1194 unsigned curBlock;
1195
1196 for (curBlock = 0; ret == CL_SUCCESS && curBlock < numBlocks; ++curBlock) {
1197 off_t blockOffset = dataOffset + table[curBlock].offset;
1198 size_t curOffset;
1199 size_t readLen;
1200 z_stream stream;
1201 int streamBeginning = 1;
1202 int streamCompressed = 0;
1203
1204 cli_dbgmsg("Handling block %u of %" PRIu32 " at offset %" PRIi64 " (size %u)\n", curBlock, numBlocks, (int64_t)blockOffset, table[curBlock].length);
1205
1206 if (lseek(ifd, blockOffset, SEEK_SET) != blockOffset) {
1207 cli_dbgmsg("hfsplus_walk_catalog: Failed to seek to beginning of block\n");
1208 ret = CL_ESEEK;
1209 break;
1210 }
1211
1212 for (curOffset = 0; curOffset < table[curBlock].length;) {
1213 readLen = table[curBlock].length - curOffset;
1214 if (readLen > sizeof(block)) {
1215 readLen = sizeof(block);
1216 }
1217
1218 if (cli_readn(ifd, block, readLen) != readLen) {
1219 cli_dbgmsg("hfsplus_walk_catalog: Failed to read block from temporary file\n");
1220 ret = CL_EREAD;
1221 break;
1222 }
1223
1224 if (streamBeginning) {
1225 streamCompressed = (block[0] & 0x0f) != 0x0f;
1226
1227 if (streamCompressed) {
1228 cli_dbgmsg("Current stream is compressed\n");
1229 stream.zalloc = Z_NULL;
1230 stream.zfree = Z_NULL;
1231 stream.opaque = Z_NULL;
1232 stream.avail_in = readLen;
1233 stream.next_in = block;
1234 stream.avail_out = sizeof(uncompressed);
1235 stream.next_out = uncompressed;
1236
1237 if ((ret = inflateInit2(&stream, 15)) != Z_OK) {
1238 cli_dbgmsg("hfsplus_walk_catalog: inflateInit2 failed (%d)\n", ret);
1239 ret = CL_EFORMAT;
1240 break;
1241 }
1242 }
1243 }
1244
1245 if (streamCompressed) {
1246 stream.avail_in = readLen;
1247 stream.next_in = block;
1248 stream.avail_out = sizeof(uncompressed);
1249 stream.next_out = uncompressed;
1250
1251 while (stream.avail_in > 0) {
1252 ret = inflate(&stream, Z_NO_FLUSH);
1253 if (ret != Z_OK && ret != Z_STREAM_END) {
1254 cli_dbgmsg("hfsplus_walk_catalog: Failed to extract (%d)\n", ret);
1255 ret = CL_EFORMAT;
1256 break;
1257 }
1258 ret = CL_SUCCESS;
1259 if (cli_writen(ofd, &uncompressed, sizeof(uncompressed) - stream.avail_out) != sizeof(uncompressed) - stream.avail_out) {
1260 cli_dbgmsg("hfsplus_walk_catalog: Failed to write to temporary file\n");
1261 ret = CL_EWRITE;
1262 break;
1263 }
1264 written += sizeof(uncompressed) - stream.avail_out;
1265 stream.avail_out = sizeof(uncompressed);
1266 stream.next_out = uncompressed;
1267 }
1268 } else {
1269 if (cli_writen(ofd, &block[streamBeginning ? 1 : 0], readLen - (streamBeginning ? 1 : 0)) != readLen - (streamBeginning ? 1 : 0)) {
1270 cli_dbgmsg("hfsplus_walk_catalog: Failed to write to temporary file\n");
1271 ret = CL_EWRITE;
1272 break;
1273 }
1274 written += readLen - (streamBeginning ? 1 : 0);
1275 }
1276
1277 curOffset += readLen;
1278 streamBeginning = 0;
1279 }
1280
1281 if (ret == CL_SUCCESS) {
1282 if ((ret = inflateEnd(&stream)) != Z_OK) {
1283 cli_dbgmsg("hfsplus_walk_catalog: inflateEnd failed (%d)\n", ret);
1284 ret = CL_EFORMAT;
1285 } else {
1286 ret = CL_SUCCESS;
1287 }
1288 }
1289 }
1290
1291 cli_dbgmsg("hfsplus_walk_catalog: Extracted compressed file from resource fork to %s (size %zu)\n", tmpname, written);
1292
1293 if (table) {
1294 free(table);
1295 table = NULL;
1296 }
1297 }
1298 }
1299 }
1300
1301 if (!ctx->engine->keeptmp) {
1302 if (cli_unlink(resourceFile)) {
1303 ret = CL_EUNLINK;
1304 }
1305 }
1306 free(resourceFile);
1307 resourceFile = NULL;
1308
1309 cli_dbgmsg("hfsplus_walk_catalog: Resource compression not implemented\n");
1310 break;
1311 }
1312 default:
1313 cli_dbgmsg("hfsplus_walk_catalog: Unknown compression type %u\n", header.compressionType);
1314 break;
1315 }
1316
1317 if (tmpname) {
1318 if (ret == CL_SUCCESS) {
1319 cli_dbgmsg("hfsplus_walk_catalog: Extracted to %s\n", tmpname);
1320
1321 /* if successful so far, scan the output */
1322 ret = cli_magic_scan_desc(ofd, tmpname, ctx, name_utf8);
1323
1324 if (ret == CL_VIRUS) {
1325 has_alerts = 1;
1326 if (SCAN_ALLMATCHES) {
1327 /* Continue scanning in SCAN_ALLMATCHES mode */
1328 cli_dbgmsg("hfsplus_walk_catalog: Compressed file alert, continuing");
1329 ret = CL_SUCCESS;
1330 }
1331 }
1332 }
1333
1334 if (!ctx->engine->keeptmp) {
1335 if (cli_unlink(tmpname)) {
1336 ret = CL_EUNLINK;
1337 }
1338 }
1339
1340 free(tmpname);
1341 tmpname = NULL;
1342 }
1343 if (ofd >= 0) {
1344 close(ofd);
1345 ofd = -1;
1346 }
1347
1348 if (ret != CL_SUCCESS) {
1349 break;
1350 }
1351 }
1352
1353 if (fileRec.dataFork.logicalSize) {
1354 ret = hfsplus_scanfile(ctx, volHeader, extHeader, &(fileRec.dataFork), dirname, NULL, name_utf8);
1355 }
1356 /* Check return code */
1357 if (ret == CL_VIRUS) {
1358 has_alerts = 1;
1359 if (SCAN_ALLMATCHES) {
1360 /* Continue scanning in SCAN_ALLMATCHES mode */
1361 cli_dbgmsg("hfsplus_walk_catalog: data fork alert, continuing");
1362 ret = CL_CLEAN;
1363 }
1364 }
1365 if (ret != CL_SUCCESS) {
1366 cli_dbgmsg("hfsplus_walk_catalog: data fork retcode %d\n", ret);
1367 break;
1368 }
1369 /* Scan resource fork */
1370 if (fileRec.resourceFork.logicalSize) {
1371 ret = hfsplus_scanfile(ctx, volHeader, extHeader, &(fileRec.resourceFork), dirname, NULL, name_utf8);
1372 }
1373 /* Check return code */
1374 if (ret == CL_VIRUS) {
1375 has_alerts = 1;
1376 if (SCAN_ALLMATCHES) {
1377 /* Continue scanning in SCAN_ALLMATCHES mode */
1378 cli_dbgmsg("hfsplus_walk_catalog: resource fork alert, continuing");
1379 ret = CL_CLEAN;
1380 }
1381 }
1382 if (ret != CL_SUCCESS) {
1383 cli_dbgmsg("hfsplus_walk_catalog: resource fork retcode %d", ret);
1384 break;
1385 }
1386 } else {
1387 cli_dbgmsg("hfsplus_walk_catalog: record mode %o is not File\n", fileRec.permissions.fileMode);
1388 }
1389
1390 if (NULL != name_utf8) {
1391 free(name_utf8);
1392 name_utf8 = NULL;
1393 }
1394 }
1395 /* if return code, exit loop, message already logged */
1396 if (ret != CL_SUCCESS) {
1397 break;
1398 }
1399
1400 /* After that, proceed to next node */
1401 if (thisNode == nodeDesc.fLink) {
1402 /* Future heuristic */
1403 cli_warnmsg("hfsplus_walk_catalog: simple cycle detected!\n");
1404 ret = CL_EFORMAT;
1405 break;
1406 } else {
1407 thisNode = nodeDesc.fLink;
1408 }
1409 }
1410
1411 free(nodeBuf);
1412 if (NULL != name_utf8) {
1413 free(name_utf8);
1414 }
1415
1416 if (has_alerts) {
1417 ret = CL_VIRUS;
1418 }
1419 return ret;
1420 }
1421
1422 /* Base scan function for scanning HFS+ or HFSX partitions */
cli_scanhfsplus(cli_ctx * ctx)1423 cl_error_t cli_scanhfsplus(cli_ctx *ctx)
1424 {
1425 char *targetdir = NULL;
1426 cl_error_t ret = CL_SUCCESS;
1427 hfsPlusVolumeHeader *volHeader = NULL;
1428 hfsNodeDescriptor catFileDesc;
1429 hfsHeaderRecord catFileHeader;
1430 hfsNodeDescriptor extentFileDesc;
1431 hfsHeaderRecord extentFileHeader;
1432 hfsNodeDescriptor attributesFileDesc;
1433 hfsHeaderRecord attributesFileHeader;
1434 int hasAttributesFileHeader = 0;
1435
1436 if (!ctx || !ctx->fmap) {
1437 cli_errmsg("cli_scanhfsplus: Invalid context\n");
1438 return CL_ENULLARG;
1439 }
1440
1441 cli_dbgmsg("cli_scanhfsplus: scanning partition content\n");
1442 /* first, read volume header contents */
1443 ret = hfsplus_volumeheader(ctx, &volHeader);
1444 if (ret != CL_SUCCESS) {
1445 goto freeHeader;
1446 }
1447
1448 /*
1449 cli_dbgmsg("sizeof(hfsUniStr255) is %lu\n", sizeof(hfsUniStr255));
1450 cli_dbgmsg("sizeof(hfsPlusBSDInfo) is %lu\n", sizeof(hfsPlusBSDInfo));
1451 cli_dbgmsg("sizeof(hfsPlusExtentDescriptor) is %lu\n", sizeof(hfsPlusExtentDescriptor));
1452 cli_dbgmsg("sizeof(hfsPlusExtentRecord) is %lu\n", sizeof(hfsPlusExtentRecord));
1453 cli_dbgmsg("sizeof(hfsPlusForkData) is %lu\n", sizeof(hfsPlusForkData));
1454 cli_dbgmsg("sizeof(hfsPlusVolumeHeader) is %lu\n", sizeof(hfsPlusVolumeHeader));
1455 cli_dbgmsg("sizeof(hfsNodeDescriptor) is %lu\n", sizeof(hfsNodeDescriptor));
1456 */
1457
1458 /* Get root node (header node) of extent overflow file */
1459 ret = hfsplus_readheader(ctx, volHeader, &extentFileDesc, &extentFileHeader, HFS_FILETREE_EXTENTS, "extentFile");
1460 if (ret != CL_SUCCESS) {
1461 goto freeHeader;
1462 }
1463 /* Get root node (header node) of catalog file */
1464 ret = hfsplus_readheader(ctx, volHeader, &catFileDesc, &catFileHeader, HFS_FILETREE_CATALOG, "catalogFile");
1465 if (ret != CL_SUCCESS) {
1466 goto freeHeader;
1467 }
1468
1469 /* Get root node (header node) of attributes file */
1470 ret = hfsplus_readheader(ctx, volHeader, &attributesFileDesc, &attributesFileHeader, HFS_FILETREE_ATTRIBUTES, "attributesFile");
1471 if (ret == CL_SUCCESS) {
1472 hasAttributesFileHeader = 1;
1473 } else {
1474 hasAttributesFileHeader = 0;
1475 ret = CL_SUCCESS;
1476 }
1477
1478 /* Create temp folder for contents */
1479 if (!(targetdir = cli_gentemp_with_prefix(ctx->sub_tmpdir, "hfsplus-tmp"))) {
1480 cli_errmsg("cli_scanhfsplus: cli_gentemp failed\n");
1481 ret = CL_ETMPDIR;
1482 goto freeHeader;
1483 }
1484 if (mkdir(targetdir, 0700)) {
1485 cli_errmsg("cli_scanhfsplus: Cannot create temporary directory %s\n", targetdir);
1486 ret = CL_ETMPDIR;
1487 goto freeDirname;
1488 }
1489 cli_dbgmsg("cli_scanhfsplus: Extracting into %s\n", targetdir);
1490
1491 /* Can build and scan catalog file if we want ***
1492 ret = hfsplus_scanfile(ctx, volHeader, &extentFileHeader, &(volHeader->catalogFile), targetdir);
1493 */
1494 if (ret == CL_SUCCESS) {
1495 ret = hfsplus_validate_catalog(ctx, volHeader, &catFileHeader);
1496 if (ret == CL_SUCCESS) {
1497 cli_dbgmsg("cli_scanhfsplus: validation successful\n");
1498 } else {
1499 cli_dbgmsg("cli_scanhfsplus: validation returned %d : %s\n", ret, cl_strerror(ret));
1500 }
1501 }
1502
1503 /* Walk through catalog to identify files to scan */
1504 if (ret == CL_SUCCESS) {
1505 ret = hfsplus_walk_catalog(ctx, volHeader, &catFileHeader, &extentFileHeader, hasAttributesFileHeader ? &attributesFileHeader : NULL, targetdir);
1506 cli_dbgmsg("cli_scanhfsplus: walk catalog finished\n");
1507 }
1508
1509 /* Clean up extracted content, if needed */
1510 if (!ctx->engine->keeptmp) {
1511 cli_rmdirs(targetdir);
1512 }
1513
1514 freeDirname:
1515 free(targetdir);
1516 freeHeader:
1517 free(volHeader);
1518 return ret;
1519 }
1520