2021-10-21 13:10:21 -04:00

618 lines
25 KiB
Java
Executable File

/*
*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
*
* Copyright 2012 42six Solutions.
* Contact: aebadirad <at> 42six <dot> com
* Project Contact/Architect: carrier <at> sleuthkit <dot> org
*
* 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 org.sleuthkit.autopsy.recentactivity;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.BufferUnderflowException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import org.joda.time.Instant;
import org.openide.util.NbBundle.Messages;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.services.FileManager;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress;
import org.sleuthkit.autopsy.ingest.IngestJobContext;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Blackboard.BlackboardException;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_DELETED;
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH;
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_NAME;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.FsContent;
import org.sleuthkit.datamodel.OsAccount;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* This module is based on the RecycleBin python module from Mark McKinnon.
*
* @see
* <a href="https://github.com/markmckinnon/Autopsy-Plugins/blob/master/Recycle_Bin/Recycle_Bin.py">Recycle_Bin.py</a>
*
*/
final class ExtractRecycleBin extends Extract {
private static final Logger logger = Logger.getLogger(ExtractRecycleBin.class.getName());
private static final String RECYCLE_BIN_ARTIFACT_NAME = "TSK_RECYCLE_BIN"; //NON-NLS
private static final String RECYCLE_BIN_DIR_NAME = "$RECYCLE.BIN"; //NON-NLS
private static final int V1_FILE_NAME_OFFSET = 24;
private static final int V2_FILE_NAME_OFFSET = 28;
@Messages({
"ExtractRecycleBin_module_name=Recycle Bin"
})
ExtractRecycleBin() {
super(Bundle.ExtractRecycleBin_module_name());
}
@Override
void process(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar) {
// At this time it was decided that we would not include TSK_RECYCLE_BIN
// in the default list of BlackboardArtifact types.
try {
createRecycleBinArtifactType();
} catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format("%s may not have been created.", RECYCLE_BIN_ARTIFACT_NAME), ex);
}
BlackboardArtifact.Type recycleBinArtifactType;
try {
recycleBinArtifactType = tskCase.getBlackboard().getArtifactType(RECYCLE_BIN_ARTIFACT_NAME);
} catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format("Unable to retrive custom artifact type %s", RECYCLE_BIN_ARTIFACT_NAME), ex); // NON-NLS
// If this doesn't work bail.
return;
}
// map SIDs to user names so that we can include that in the artifact
Map<String, String> userNameMap;
try {
userNameMap = makeUserNameMap(dataSource);
} catch (TskCoreException ex) {
logger.log(Level.WARNING, "Unable to create OS Account user name map", ex);
// This is not the end of the world we will just continue without
// user names
userNameMap = new HashMap<>();
}
FileManager fileManager = Case.getCurrentCase().getServices().getFileManager();
// Collect all of the $R files so that we can later easily map them to corresponding $I file
Map<String, List<AbstractFile>> rFileMap;
try {
rFileMap = makeRFileMap(dataSource);
} catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format("Unable to create $R file map for dataSource: %s", dataSource.getName()), ex);
return; // No $R files, no need to continue;
}
// Get the $I files
List<AbstractFile> iFiles;
try {
iFiles = fileManager.findFiles(dataSource, "$I%", RECYCLE_BIN_DIR_NAME); //NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.WARNING, "Unable to find recycle bin I files.", ex); //NON-NLS
return; // No need to continue
}
String tempRARecycleBinPath = RAImageIngestModule.getRATempPath(Case.getCurrentCase(), "recyclebin", context.getJobId()); //NON-NLS
// cycle through the $I files and process each.
for (AbstractFile iFile : iFiles) {
if (context.dataSourceIngestIsCancelled()) {
return;
}
processIFile(context, recycleBinArtifactType, iFile, userNameMap, rFileMap, tempRARecycleBinPath);
}
(new File(tempRARecycleBinPath)).delete();
}
/**
* Process each individual iFile. Each iFile ($I) contains metadata about files that have been deleted.
* Each $I file should have a corresponding $R file which is the actuall deleted file.
*
* @param context
* @param recycleBinArtifactType Module created artifact type
* @param iFile The AbstractFile to process
* @param userNameMap Map of user ids to names
* @param tempRARecycleBinPath Temp directory path
*/
private void processIFile(IngestJobContext context, BlackboardArtifact.Type recycleBinArtifactType, AbstractFile iFile, Map<String, String> userNameMap, Map<String, List<AbstractFile>> rFileMap, String tempRARecycleBinPath) {
String tempFilePath = tempRARecycleBinPath + File.separator + Instant.now().getMillis() + iFile.getName();
try {
try {
ContentUtils.writeToFile(iFile, new File(tempFilePath));
} catch (IOException ex) {
logger.log(Level.WARNING, String.format("Unable to write %s to temp directory. File name: %s", iFile.getName(), tempFilePath), ex); //NON-NLS
// if we cannot make a copy of the $I file for later processing
// move onto the next file
return;
}
// get the original name, dates, etc. from the $I file
RecycledFileMetaData metaData;
try {
metaData = parseIFile(tempFilePath);
} catch (IOException ex) {
logger.log(Level.WARNING, String.format("Unable to parse iFile %s", iFile.getParentPath() + iFile.getName()), ex); //NON-NLS
// Unable to parse the $I file move onto the next file
return;
}
// each user has its own Recyle Bin folder. Figure out the user name based on its name .
String userID = getUserIDFromPath(iFile.getParentPath());
String userName = "";
if (!userID.isEmpty()) {
userName = userNameMap.get(userID);
} else {
// If the iFile doesn't have a user ID in its parent
// directory structure then it is not from the recyle bin
return;
}
// get the corresponding $R file, which is in the same folder and has the file content
String rFileName = iFile.getName().replace("$I", "$R"); //NON-NLS
List<AbstractFile> rFiles = rFileMap.get(rFileName);
if (rFiles == null) {
return;
}
SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase();
for (AbstractFile rFile : rFiles) {
if (context.dataSourceIngestIsCancelled()) {
return;
}
if (iFile.getParentPath().equals(rFile.getParentPath())
&& iFile.getMetaFlagsAsString().equals(rFile.getMetaFlagsAsString())) {
try {
postArtifact(createArtifact(rFile, recycleBinArtifactType, metaData.getFullWindowsPath(), userName, metaData.getDeletedTimeStamp()));
// If we are processing a disk image, we will also make a deleted file entry so that the user
// sees the deleted file in its original folder. We re-use the metadata address so that the user
// can see the content.
if (rFile instanceof FsContent) {
// if the user deleted a folder, then we need to recusively go into it. Note the contents of the $R folder
// do not have corresponding $I files anymore. Only the $R folder does.
if (rFile.isDir()) {
AbstractFile directory = getOrMakeFolder(Case.getCurrentCase().getSleuthkitCase(), (FsContent) rFile, metaData.getFullWindowsPath());
popuplateDeletedDirectory(Case.getCurrentCase().getSleuthkitCase(), directory, rFile.getChildren(), metaData.getFullWindowsPath(), metaData.getDeletedTimeStamp());
} else {
AbstractFile folder = getOrMakeFolder(Case.getCurrentCase().getSleuthkitCase(), (FsContent) rFile.getParent(), Paths.get(metaData.getFullWindowsPath()).getParent().toString());
addFileSystemFile(skCase, (FsContent)rFile, folder, Paths.get(metaData.getFullWindowsPath()).getFileName().toString(), metaData.getDeletedTimeStamp());
}
}
} catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format("Unable to add attributes to artifact %s", rFile.getName()), ex); //NON-NLS
}
}
}
} finally {
(new File(tempFilePath)).delete();
}
}
/**
* Add the children of recycled $R folder to the folder.
*
* @param skCase The current Sleuthkit case
* @param parentFolder The folder to folder the deleted files are to be
* added.
* @param children The recycled children of the $R folder
* @param parentPath String path to the directory the children were
* deleted from
* @param deletedTimeStamp The time at which the files were deleted,
* inherited from the $R file.
*
* @throws TskCoreException
*/
private void popuplateDeletedDirectory(SleuthkitCase skCase, AbstractFile parentFolder, List<Content> recycledChildren, String parentPath, long deletedTimeStamp) throws TskCoreException {
if (recycledChildren == null) {
return;
}
for (Content child : recycledChildren) {
if (child instanceof FsContent) {
FsContent fsContent = (FsContent) child;
if (fsContent.isFile()) {
addFileSystemFile(skCase, fsContent, parentFolder, fsContent.getName(), deletedTimeStamp);
} else if (fsContent.isDir()) {
String newPath = parentPath + "\\" + fsContent.getName();
AbstractFile childFolder = getOrMakeFolder(skCase, fsContent, parentPath);
popuplateDeletedDirectory(skCase, childFolder, fsContent.getChildren(), newPath, deletedTimeStamp);
}
}
}
}
/**
* Parse the $I file. This file contains metadata information about deleted files
*
* File format prior to Windows 10:
* Offset Size Description
* 0 8 Header
* 8 8 File Size
* 16 8 Deleted Timestamp
* 24 520 File Name
*
* File format Windows 10+
* Offset Size Description
* 0 8 Header
* 8 8 File Size
* 16 8 Deleted TimeStamp
* 24 4 File Name Length
* 28 var File Name
*
* For versions of Windows prior to 10, header = 0x01. Windows 10+ header ==
* 0x02
*
* @param iFilePath Path to local copy of file in temp folder
*
* @throws IOException
*/
private RecycledFileMetaData parseIFile(String iFilePath) throws IOException {
try {
byte[] allBytes = Files.readAllBytes(Paths.get(iFilePath));
ByteBuffer byteBuffer = ByteBuffer.wrap(allBytes);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
long version = byteBuffer.getLong();
long fileSize = byteBuffer.getLong();
long timestamp = byteBuffer.getLong();
// Convert from windows FILETIME to Unix Epoch seconds
timestamp = Util.filetimeToMillis(timestamp) / 1000;
byte[] stringBytes;
if (version == 1) {
stringBytes = Arrays.copyOfRange(allBytes, V1_FILE_NAME_OFFSET, allBytes.length);
} else {
int fileNameLength = byteBuffer.getInt() * 2; //Twice the bytes for unicode
stringBytes = Arrays.copyOfRange(allBytes, V2_FILE_NAME_OFFSET, V2_FILE_NAME_OFFSET + fileNameLength);
}
String fileName = new String(stringBytes, "UTF-16LE"); //NON-NLS
return new RecycledFileMetaData(fileSize, timestamp, fileName);
} catch (IOException | BufferUnderflowException | IllegalArgumentException | ArrayIndexOutOfBoundsException ex) {
throw new IOException("Error parsing $I File, file is corrupt or not a valid I$ file", ex);
}
}
/**
* Create a map of userids to usernames from the OS Accounts.
*
* @param dataSource
*
* @return A Map of userIDs and userNames
*
* @throws TskCoreException
*/
private Map<String, String> makeUserNameMap(Content dataSource) throws TskCoreException {
Map<String, String> userNameMap = new HashMap<>();
for(OsAccount account: tskCase.getOsAccountManager().getOsAccounts(((DataSource)dataSource).getHost())) {
Optional<String> userName = account.getLoginName();
userNameMap.put(account.getName(), userName.isPresent() ? userName.get() : "");
}
return userNameMap;
}
/**
* Get a list of files that start with $R and create a map of the file to
* their name.
*
* @param dataSource
*
* @return File map
*
* @throws TskCoreException
*/
private Map<String, List<AbstractFile>> makeRFileMap(Content dataSource) throws TskCoreException {
FileManager fileManager = Case.getCurrentCase().getServices().getFileManager();
List<AbstractFile> rFiles = fileManager.findFiles(dataSource, "$R%");
Map<String, List<AbstractFile>> fileMap = new HashMap<>();
for (AbstractFile rFile : rFiles) {
String fileName = rFile.getName();
List<AbstractFile> fileList = fileMap.get(fileName);
if (fileList == null) {
fileList = new ArrayList<>();
fileMap.put(fileName, fileList);
}
fileList.add(rFile);
}
return fileMap;
}
/**
* Helper functions to get the user ID from the iFile parent path. User ids
* will be of the form S-<more characters>.
*
* @param iFileParentPath String parent path of the iFile
*
* @return String user id
*/
private String getUserIDFromPath(String iFileParentPath) {
int index = iFileParentPath.indexOf('-') - 1;
if (index >= 0) {
return (iFileParentPath.substring(index)).replace("/", "");
} else {
return "";
}
}
/**
* Gets the attribute for the given type from the given artifact.
*
* @param artifact BlackboardArtifact to get the attribute from
* @param type The BlackboardAttribute Type to get
*
* @return BlackboardAttribute for given artifact and type
*
* @throws TskCoreException
*/
private BlackboardAttribute getAttributeForArtifact(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE type) throws TskCoreException {
return artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.fromID(type.getTypeID())));
}
@Messages({
"ExtractRecycleBin_Recyle_Bin_Display_Name=Recycle Bin"
})
/**
* Create TSK_RECYCLE_BIN artifact type.
*
* @throws TskCoreException
*/
private void createRecycleBinArtifactType() throws TskCoreException {
try {
tskCase.getBlackboard().getOrAddArtifactType(RECYCLE_BIN_ARTIFACT_NAME, Bundle.ExtractRecycleBin_Recyle_Bin_Display_Name()); //NON-NLS
} catch (BlackboardException ex) {
throw new TskCoreException(String.format("An exception was thrown while defining artifact type %s", RECYCLE_BIN_ARTIFACT_NAME), ex);
}
}
/**
* Create the new artifact for the give rFile
*
* @param rFile AbstractFile to create the artifact for
* @param type Type of artifact to create
* @param fileName The original path of the deleted file
* @param userName The name of the user that deleted the file
* @param dateTime The time in epoch seconds that the file was deleted
*
* @return Newly created artifact
*
* @throws TskCoreException
*/
private BlackboardArtifact createArtifact(AbstractFile rFile, BlackboardArtifact.Type type, String fileName, String userName, long dateTime) throws TskCoreException {
List<BlackboardAttribute> attributes = new ArrayList<>();
attributes.add(new BlackboardAttribute(TSK_PATH, getName(), fileName));
attributes.add(new BlackboardAttribute(TSK_DATETIME_DELETED, getName(), dateTime));
attributes.add(new BlackboardAttribute(TSK_USER_NAME, getName(), userName == null || userName.isEmpty() ? "" : userName));
return createArtifactWithAttributes(type, rFile, attributes);
}
/**
* Returns a folder for the given path. If the path does not exist the
* the folder is created. Recursively makes as many parent folders as needed.
*
* @param skCase
* @param dataSource
* @param path
*
* @return AbstractFile for the given path.
*
* @throws TskCoreException
*/
private AbstractFile getOrMakeFolder(SleuthkitCase skCase, FsContent dataSource, String path) throws TskCoreException {
String parentPath = getParentPath(path);
String folderName = getFileName(path);
List<AbstractFile> files = null;
if (parentPath != null) {
if (!parentPath.equals("/")) {
parentPath = parentPath + "/";
}
files = skCase.findAllFilesWhere(String.format("fs_obj_id=%s AND parent_path='%s' AND name='%s'",
dataSource.getFileSystemId(), SleuthkitCase.escapeSingleQuotes(parentPath), folderName != null ? SleuthkitCase.escapeSingleQuotes(folderName) : ""));
} else {
files = skCase.findAllFilesWhere(String.format("fs_obj_id=%s AND parent_path='/' AND name=''", dataSource.getFileSystemId()));
}
if (files == null || files.isEmpty()) {
AbstractFile parent = getOrMakeFolder(skCase, dataSource, parentPath);
return skCase.addVirtualDirectory(parent.getId(), folderName);
} else {
return files.get(0);
}
}
/**
* Adds a new file system file that is unallocated and maps to the original
* file in recycle bin directory.
*
* @param skCase The current case.
* @param recycleBinFile The file from the recycle bin.
* @param parentDir The directory that the recycled file was deleted.
* @param fileName The name of the file.
* @param deletedTime The time the file was deleted.
*
* @throws TskCoreException
*/
private void addFileSystemFile(SleuthkitCase skCase, FsContent recycleBinFile, Content parentDir, String fileName, long deletedTime) throws TskCoreException {
skCase.addFileSystemFile(
recycleBinFile.getDataSourceObjectId(),
recycleBinFile.getFileSystemId(),
fileName,
recycleBinFile.getMetaAddr(),
(int) recycleBinFile.getMetaSeq(),
recycleBinFile.getAttrType(),
recycleBinFile.getAttributeId(),
TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC,
(short) (TskData.TSK_FS_META_FLAG_ENUM.UNALLOC.getValue() | TskData.TSK_FS_META_FLAG_ENUM.USED.getValue()),
recycleBinFile.getSize(),
recycleBinFile.getCtime(), recycleBinFile.getCrtime(), recycleBinFile.getAtime(), deletedTime,
true, parentDir);
}
/**
* Clean up the windows path string to match what the autopsy db uses.
*
* @param path The file\folder path to normalize
*
* @return New path string with the root removed (ie X:) and the slashes
* changed from windows to unix.
*/
String normalizeFilePath(String pathString) {
if (pathString == null || pathString.isEmpty()) {
return null;
}
Path path = Paths.get(pathString);
int nameCount = path.getNameCount();
if(nameCount > 0) {
String rootless = "/" + path.subpath(0, nameCount);
return rootless.replace("\\", "/");
} else {
return "/";
}
}
/**
* Helper function get from the given path either the file name or
* the last directory in the path.
*
* @param filePath The file\directory path
*
* @return If file path, returns the file name. If directory path the
* The last directory in the path is returned.
*/
String getFileName(String filePath) {
Path fileNamePath = Paths.get(filePath).getFileName();
if (fileNamePath != null) {
return fileNamePath.toString();
}
return filePath;
}
/**
* Returns the parent path for the given path.
*
* @param path Path string
*
* @return The parent path for the given path.
*/
String getParentPath(String path) {
Path parentPath = Paths.get(path).getParent();
if (parentPath != null) {
return normalizeFilePath(parentPath.toString());
}
return null;
}
/**
* Stores the data from the $I files.
*/
final class RecycledFileMetaData {
private final long fileSize;
private final long deletedTimeStamp;
private final String fileName;
/**
* Constructs a new instance.
*
* @param fileSize Size of the deleted file.
* @param deletedTimeStamp Time the file was deleted.
* @param fileName Name of the deleted file.
*/
RecycledFileMetaData(Long fileSize, long deletedTimeStamp, String fileName) {
this.fileSize = fileSize;
this.deletedTimeStamp = deletedTimeStamp;
this.fileName = fileName;
}
/**
* Returns the size of the recycled file.
*
* @return Size of deleted file
*/
long getFileSize() {
return fileSize;
}
/**
* Returns the time the file was deleted.
*
* @return deleted time in epoch seconds.
*/
long getDeletedTimeStamp() {
return deletedTimeStamp;
}
/**
* Returns the full path to the deleted file or folder. This path will
* include the drive letter, ie C:\
*
* @return String name of the deleted file
*/
String getFullWindowsPath() {
return fileName.trim();
}
}
}