GP-807 add support for HFS volumes and switch ISO9660 to 7zip

Both file systems are handled by existing 7zip, but HFS needed code to
recognize the volume header.

Remove ghidra implementation of ISO9660 as it lacked support for long
file names and defer to the 7zip implementation.
This commit is contained in:
dev747368
2021-06-17 14:15:17 -04:00
parent 54bbbcf44b
commit 89edc1594e
5 changed files with 301 additions and 275 deletions

View File

@@ -0,0 +1,28 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.ios.hfs;
import ghidra.file.formats.sevenzip.SevenZipFileSystem;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
@FileSystemInfo(type = "hfsplus", description = "Apple HFS+ Disk Volume", factory = HFSPlusFileSystemFactory.class)
public class HFSPlusFileSystem extends SevenZipFileSystem {
public HFSPlusFileSystem(FSRLRoot fsrl, FileSystemService fsService) {
super(fsrl, fsService);
}
}

View File

@@ -0,0 +1,53 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.ios.hfs;
import java.io.IOException;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class HFSPlusFileSystemFactory
implements GFileSystemFactoryByteProvider<HFSPlusFileSystem>, GFileSystemProbeByteProvider {
@Override
public boolean probe(ByteProvider byteProvider, FileSystemService fsService,
TaskMonitor monitor) throws IOException, CancelledException {
return HFSPlusVolumeHeader.probe(byteProvider);
}
@Override
public HFSPlusFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
HFSPlusFileSystem fs = new HFSPlusFileSystem(targetFSRL, fsService);
try {
fs.mount(byteProvider, monitor);
return fs;
}
catch (IOException ioe) {
fs.close();
throw ioe;
}
}
}

View File

@@ -0,0 +1,145 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.ios.hfs;
import java.io.IOException;
import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.ByteProvider;
/**
* Apple HFS+ volume header.
* <p>
* See https://developer.apple.com/library/archive/technotes/tn/tn1150.html#VolumeHeader
* <p>
* Fields are BigEndian
*/
public class HFSPlusVolumeHeader {
//@formatter:off
// Offset (hex) Length Comment
private short signature; // 0 2 48 2b, "H+"
private short version; // 2 2 4=HFS+, 5=HFSX
private int attributes; // 4 4
private int lastMountedVersion; // 8 4 '10.0' string
private int journalInfoBlock; // C 4
private int createDate; // 10 4
private int modifyDate; // 14 4
private int backupDate; // 18 4
private int checkedDate; // 1C 4
private int fileCount; // 20 4
private int folderCount; // 24 4
private int blockSize; // 28 4 0x1000=4096
private int totalBlocks; // 2C 4 blockSize*totalBlocks should equal vol size
private int freeBlocks; // 30 4
private int nextAllocation; // 34 4
private int rsrcClumpSize; // 38 4
private int dataClumpSize; // 3C 4
private int nextCatalogID; // 40 4
private int writeCount; // 44 4
private long encodingsBitmap; // 48 8
private int[] finderInfo; // 50 32 uint32[8]
private byte[] rawForkData; // 70 400
//HFSPlusForkData allocationFile; // 70 80
//HFSPlusForkData extentsFile; // C0 80
//HFSPlusForkData catalogFile; // 110 80
//HFSPlusForkData attributesFile; // 160 80
//HFSPlusForkData startupFile; // 1B0 80
//@formatter:on
private static final int HFSPLUS_SIGNATURE_MAGIC = 0x482b; // "H+"
private static final int HFSX_SIGNATURE_MAGIC = 0x4858; // "HX"
private static final int HFSPLUS_VERSION = 4;
private static final int HFSX_VERSION = 5;
private static final int SIZEOF_HEADER = 512;
private static final int DEFAULT_OFFSET = 1024;
public static boolean probe(ByteProvider provider) {
try {
if (provider.length() < DEFAULT_OFFSET + SIZEOF_HEADER) {
return false;
}
HFSPlusVolumeHeader header = read(provider);
return header.isValid() && header.hasGoodVolumeInfo(provider);
}
catch (IOException e) {
return false;
}
}
public static HFSPlusVolumeHeader read(ByteProvider provider) throws IOException {
return read(provider, DEFAULT_OFFSET);
}
public static HFSPlusVolumeHeader read(ByteProvider provider, long offset) throws IOException {
BinaryReader reader = new BinaryReader(provider, false /*BE*/);
reader.setPointerIndex(offset);
HFSPlusVolumeHeader result = new HFSPlusVolumeHeader();
result.signature = reader.readNextShort();
result.version = reader.readNextShort();
result.attributes = reader.readNextInt();
result.lastMountedVersion = reader.readNextInt();
result.journalInfoBlock = reader.readNextInt();
result.createDate = reader.readNextInt();
result.modifyDate = reader.readNextInt();
result.backupDate = reader.readNextInt();
result.checkedDate = reader.readNextInt();
result.fileCount = reader.readNextInt();
result.folderCount = reader.readNextInt();
result.blockSize = reader.readNextInt();
result.totalBlocks = reader.readNextInt();
result.freeBlocks = reader.readNextInt();
result.nextAllocation = reader.readNextInt();
result.rsrcClumpSize = reader.readNextInt();
result.dataClumpSize = reader.readNextInt();
result.nextCatalogID = reader.readNextInt();
result.writeCount = reader.readNextInt();
result.encodingsBitmap = reader.readNextLong();
result.finderInfo = reader.readNextIntArray(8);
result.rawForkData = reader.readNextByteArray(400);
return result;
}
public boolean isValid() {
return signature == HFSPLUS_SIGNATURE_MAGIC && version == HFSPLUS_VERSION &&
isGoodBlockSize(blockSize);
}
private static boolean isGoodBlockSize(int bs) {
return bs > 0 && bs % 512 == 0;
}
public boolean hasGoodVolumeInfo(ByteProvider bp) throws IOException {
long calculatedSize = blockSize * totalBlocks;
// NOTE: can't compare with exact equals-to provider size because an extra 16 bytes
// are present in examples extracted from firmware images
return bp.length() >= calculatedSize;
}
}

View File

@@ -15,283 +15,15 @@
*/
package ghidra.file.formats.iso9660;
import java.io.IOException;
import java.util.*;
import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.*;
import ghidra.file.formats.sevenzip.SevenZipFileSystem;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory;
import ghidra.util.Msg;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
@FileSystemInfo(type = "iso9660", description = "ISO 9660", factory = GFileSystemBaseFactory.class)
public class ISO9660FileSystem extends GFileSystemBase {
@FileSystemInfo(type = "iso9660", description = "ISO 9660", factory = ISO9660FileSystemFactory.class)
public class ISO9660FileSystem extends SevenZipFileSystem {
//Possible locations for magic number
private static final long[] SIGNATURE_PROBE_OFFSETS = new long[] { 0x8000L, 0x8800L, 0x9000L };
//Location where the magic number was found
private long signatureOffset;
//Set true if the root level directory has been processed
private boolean lookedAtRoot = false;
private short logicalBlockSize;
private ISO9660Header header;
private Map<GFile, ISO9660Directory> fileToDirectoryMap = new HashMap<>();
public ISO9660FileSystem(String fileSystemName, ByteProvider provider) {
super(fileSystemName, provider);
public ISO9660FileSystem(FSRLRoot fsrl, FileSystemService fsService) {
super(fsrl, fsService);
}
@Override
public boolean isValid(TaskMonitor monitor) throws IOException {
for (long probeOffset : SIGNATURE_PROBE_OFFSETS) {
if (isMagicSignatureAt(probeOffset + 1)) {
// signature is at +1 offset from the start of the volume offset
signatureOffset = probeOffset;
return true;
}
}
return false;
}
private boolean isMagicSignatureAt(long offset) throws IOException {
int magicLen = ISO9660Constants.MAGIC_BYTES.length;
long providerLen = provider.length();
return (providerLen > offset + magicLen) &&
Arrays.equals(provider.readBytes(offset, magicLen), ISO9660Constants.MAGIC_BYTES);
}
@Override
public void open(TaskMonitor monitor) throws IOException, CryptoException, CancelledException {
BinaryReader reader = new BinaryReader(provider, true);
//Set start of pointer index of beginning of primary volume descriptor
reader.setPointerIndex(signatureOffset);
header = new ISO9660Header(reader);
ISO9660VolumeDescriptor pvd = header.getPrimaryVolumeDescriptor();
logicalBlockSize = pvd.getLogicalBlockSizeBE();
ISO9660Directory rootDir = header.getPrimaryDirectory();
//Get the list containing all directories at the root level of the file system
List<ISO9660Directory> topLevel =
createDirectoryList(reader, rootDir, pvd.getLogicalBlockSizeLE(), monitor);
try {
//Entry point for the this recursive function to process all nested
//directories
createDirectories(reader, topLevel, pvd.getLogicalBlockSizeLE(), monitor);
}
catch (Exception e) {
Msg.showError(this, null, "Directory Creation Error",
"Failed to create archive directories");
}
}
@Override
public void close() throws IOException {
super.close();
header = null;
fileToDirectoryMap.clear();
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
if (directory == null || directory.equals(root)) {
List<GFile> roots = new ArrayList<>();
for (GFile file : fileToDirectoryMap.keySet()) {
if (file.getParentFile() == root || file.getParentFile().equals(root)) {
roots.add(file);
}
}
return roots;
}
List<GFile> tmp = new ArrayList<>();
for (GFile file : fileToDirectoryMap.keySet()) {
if (file.getParentFile() == null) {
continue;
}
if (file.getParentFile().equals(directory)) {
tmp.add(file);
}
}
return tmp;
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
ISO9660Directory dir = fileToDirectoryMap.get(file);
return dir.getByteProvider(provider, logicalBlockSize, file.getFSRL());
}
/*
* From a given parent directory create each child directory
* under that parent directory and add them to a list
*/
private List<ISO9660Directory> createDirectoryList(BinaryReader reader,
ISO9660Directory parentDir, long blockSize, TaskMonitor monitor) throws IOException {
List<ISO9660Directory> directoryList = new ArrayList<>();
ISO9660Directory childDir = null;
long dirIndex;
long endIndex;
//Get location from parent into child directory
dirIndex = parentDir.getLocationOfExtentLE() * blockSize;
endIndex = dirIndex + parentDir.getDataLengthLE();
//while there is still more data in the current directory level
while (dirIndex < endIndex) {
reader.setPointerIndex(dirIndex);
//If the next byte is not zero then create the directory
if (reader.peekNextByte() != 0) {
if (!lookedAtRoot) {
childDir = new ISO9660Directory(reader);
addAndStoreDirectory(monitor, directoryList, childDir);
}
//Root level has already been looked at
else {
if (parentDir.getName() != null) {
childDir = new ISO9660Directory(reader, parentDir);
addAndStoreDirectory(monitor, directoryList, childDir);
}
}
}
//Otherwise there is a gap in the data so keep looking forward
//while still under the end index and create directory when data is
//reached
else {
readWhileZero(reader, endIndex);
//Create the data once the reader finds the next position
//and not reached end index
if (reader.getPointerIndex() < endIndex) {
if (!lookedAtRoot) {
childDir = new ISO9660Directory(reader);
addAndStoreDirectory(monitor, directoryList, childDir);
dirIndex = childDir.getVolumeIndex();
}
else {
if (parentDir.getName() != null) {
childDir = new ISO9660Directory(reader, parentDir);
addAndStoreDirectory(monitor, directoryList, childDir);
dirIndex = childDir.getVolumeIndex();
}
}
}
}
dirIndex += childDir.getDirectoryRecordLength();
}
lookedAtRoot = true;
return directoryList;
}
private void readWhileZero(BinaryReader reader, long endIndex) throws IOException {
while (reader.peekNextByte() == 0) {
//keep reading if all zeros until non zero is met or
//end index reached
if (reader.getPointerIndex() < endIndex) {
reader.readNextByte();
}
else {
break;
}
}
}
private void addAndStoreDirectory(TaskMonitor monitor, List<ISO9660Directory> directoryList,
ISO9660Directory childDir) {
directoryList.add(childDir);
if (childDir.getName() != null) {
storeDirectory(childDir, monitor);
}
}
/*
* Recurses though each level of a directory structure
* in a depth-first manner
* and creates each directory also marking them in the binary
*/
private void createDirectories(BinaryReader reader, List<ISO9660Directory> directoryList,
long blockSize, TaskMonitor monitor) throws DuplicateNameException, Exception {
for (ISO9660Directory dir : directoryList) {
//If the directory is a new level of directories
//recurse down into the next level
if (dir.isDirectoryFlagSet() && dir.getName() != null) {
List<ISO9660Directory> dirs;
dirs = createDirectoryList(reader, dir, blockSize, monitor);
createDirectories(reader, dirs, blockSize, monitor);
}
}
return;
}
/*
* Stores a gFile after finding its parent and its matching directory
*/
private void storeDirectory(ISO9660Directory directory, TaskMonitor monitor) {
String dirName = directory.getName();
boolean isDirectory = directory.isDirectoryFlagSet();
int length = directory.getDataLengthLE();
GFileImpl gFile = null;
//Map does not contain entries yet since root level needs to be processed
if (!lookedAtRoot) {
gFile = GFileImpl.fromFilename(this, root, dirName, isDirectory, length, null);
storeFile(gFile, directory);
}
else {
//Root has been processed, all other entries must have a parent
String parentDirName = directory.getParentDirectory().getName();
for (GFile currGFile : fileToDirectoryMap.keySet()) {
//Find the parent and store the file
if (parentDirName.equals(currGFile.getName())) {
gFile =
GFileImpl.fromFilename(this, currGFile, dirName, isDirectory, length, null);
storeFile(gFile, directory);
break;
}
}
}
}
private void storeFile(GFile file, ISO9660Directory directory) {
if (file == null) {
return;
}
if (file.equals(root)) {
return;
}
if (!fileToDirectoryMap.containsKey(file) || fileToDirectoryMap.get(file) == null) {
fileToDirectoryMap.put(file, directory);
}
GFile parentFile = file.getParentFile();
storeFile(parentFile, null);
}
}

View File

@@ -0,0 +1,68 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.iso9660;
import java.io.IOException;
import java.util.Arrays;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class ISO9660FileSystemFactory
implements GFileSystemFactoryByteProvider<ISO9660FileSystem>, GFileSystemProbeByteProvider {
private static final long[] SIGNATURE_PROBE_OFFSETS = new long[] { 0x8000L, 0x8800L, 0x9000L };
@Override
public boolean probe(ByteProvider byteProvider, FileSystemService fsService,
TaskMonitor monitor) throws IOException, CancelledException {
for (long probeOffset : SIGNATURE_PROBE_OFFSETS) {
if (isMagicSignatureAt(byteProvider, probeOffset + 1)) {
return true;
}
}
return false;
}
private boolean isMagicSignatureAt(ByteProvider provider, long offset) throws IOException {
int magicLen = ISO9660Constants.MAGIC_BYTES.length;
long providerLen = provider.length();
return (providerLen > offset + magicLen) &&
Arrays.equals(provider.readBytes(offset, magicLen), ISO9660Constants.MAGIC_BYTES);
}
@Override
public ISO9660FileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
ISO9660FileSystem fs = new ISO9660FileSystem(targetFSRL, fsService);
try {
fs.mount(byteProvider, monitor);
return fs;
}
catch (IOException ioe) {
fs.close();
throw ioe;
}
}
}