Forensic Inspection of Hard Disks



Click here for a larger image.

Environment: VC6, MFC, DLL

This is a systems-side programming project. The forensic analysis tool helps find unique strings of various kinds to be found on a given hard disk. This project involves finding the hard disks and writing out the string occurrences in the searched hard disks. If the string is found, it will write out files to the destination drive with many segments and the pattern of the results file is as follows:

  • System Information
  • Computer Name
  • Logged User Name
  • Other details of the machine
  • Chunks of the words found with number of bytes before and after

The utility uses only a single thread during the process of searching and it uses PeekMessage to monitor activities. The project uses a modeless dialog box to display messages. For example, if the destination drive happens to be a floppy drive, the utility displays a modeless dialog box and waits for the user to insert the floppy disk and continues normally without the touch of the mouse or the keyboard.

Reading and Writing Disks in Windows 95+

There are many methods for reading disks in Win 95+; I have chosen the thunking of DLLs to read the disks. This is nothing but executing the real-mode BIOS Int 13h from within the protected mode. This way, we can read the disks as if we were in real-mode DOS. However, it requires two DLLs called the thunk DLLs. One is a 32-bit DLL that thunks down to a 16-bit DLL; it will communicate to the real-mode BIOS and fetch the required buffer of data from the hard disks. The 32-bit in the protected mode translates the linear pointers and sends them down to the 16-bit DLL, which in turn calls the real-mode BIOS using the DOS Protected Mode Interface (DPMI). We have to set up a part of real-mode memory for fetching the buffer, which is mapped to the memory of the 16-bit DLL in the protected mode; then it is copied to the linear memory by the routine that handles reading of the buffer. For writing, the process is just the reverse; first, the buffer in the linear memory is copied to the protected mode 16-bit memory, which is mapped to the real mode memory which is written to the disk.

In Windows 95+ there are no restrictions on how the system is being used.The Kernel and Application levels (Ring 0 and Ring 3) are seamlessly integrated where the memories can be allocated by anything and accessed by anything.

Reading and Writing Disks in Windows NT+

In NT the reading and writing mechanisms are entirely different. Because of the security restrictions, we cannot call the BIOS or the real-mode interrupts but only access the disks using the APIs that NT offers. The API for accessing the disks is CreateFile, which opens a disk as if it were a file; the disk is then read or written as if it were a file except that it can be done only in chunks of sectors. The disk reads and writes are treated just like files except that it does direct disk IO.

Int 13h Extensions

BIOS offers Int 13h extensions in the latest motherboards starting some years back. This is necessary because in the old BIOS it is not possible to read beyond 8 Gb of hard disk due to register restrictions, which is the mechanism for reading and writing disks. I have handled int 13h extensions in the thunk 16-bit DLL. Certain BIOS interrupts are added; this allows up to two-power, 64-bit number of sectors to be accessed. This is way beyond the imagination and can be used for decades without altering the BIOS any more. NT internally handles int 13h extensions, so there is no need to handle the extensions separately.

Disk Organization in the FAT File System

In the FAT file system, the disk organization is straightforward and simple. The organization of the files ensures that the files are accessible through a simple walkthrough of pointers in a random fashion. This way, any file anywhere in the disk can be located with a simple mechanism. FAT12, or 12-bit FAT, is the lowest member of the FAT family, where the file pointers are just 1.5 bytes; FAT, or 16-bit FAT, is most widely used in hard disks that have 16-bits or 2 bytes per pointer. VFAT is used in Win95 on; it is the FAT with long file name support. FAT32, or 32-bit FAT, is the highest member that can handle huge hard disks as a single partition; they are used from Win95 OSR 2 on. NT 4 does not manipulate FAT32; hence, FAT is the lowest common member of the FAT file system. Starting from Win2000, the OS handles FAT32; hence we also manipulate this file system.

The boot sector comes first in FAT, followed by hidden and reserved sectors. Then come the FAT tables. The FAT tables consist of pointers to the next cluster occupied by a file. The pointers point to the next clusters, finally terminated by a word or a double word, depending on the file system. The bad clusters are marked with a unique word or dword; thus they are skipped during the assigning process. FATs are stored in two copies for security reasons because this is the most important part of the file system. If any FAT sectors become unusable, they are skipped and the second copy is used automatically. The FAT is present in a fixed location of the disk at the start of the partition. Then, after the two copies of FAT, comes the root directory. This is a 32-byte packet with file information coded in it. The Root directory is fixed in FAT12 and FAT16, but is just another sub-directory in FAT32. It is fixed; only a certain number of files can be stored in the directory. Otherwise, if as in FAT32, it can store enormous numbers of files.

Then come the files and sub-directories, which are organized only with the help of the FAT tables. The starting cluster number is stored in the directory entry; it points to the next cluster in the FAT table that is occupied by the file. This phenomenon continues until the end-of-file mark is reached.

System Information

This routine gets the system information from the Registry. The information is different in 95 and NT breeds. The querying is done using the Reg APIs. The RegEnumKeyEx searches for the hardware installed in the system.


long KeyRes;
DWORD TmpVal;
HKEY hKey, hKeyTmp, hKeyTmp1;
FILETIME Ft;
char ClassStr[500] = “SYSTEM\\CurrentControlSet\\Control\\
Class\\”;
char ClassStr95[500]= “SYSTEM\\CurrentControlSet\\Services\\
Class\\”;
char ClassStrTmp[500], ClassStrTmp1[500];
SysInfo[0]=0;
SysInfoTmp = SysInfo;
strcpy(SysInfo, “\nSystem Information\n\t”);

if(!gOSWin95)
KeyRes = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
“SYSTEM\\CurrentControlSet\\Control\\Class”, 0, KEY_READ,
&hKey);
else
KeyRes = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
“SYSTEM\\CurrentControlSet\\Services\\Class”, 0, KEY_READ,
&hKey);
if(KeyRes==ERROR_SUCCESS){
i=0;
while(KeyRes != ERROR_NO_MORE_ITEMS){
TmpVal = sizeof(TmpStr);
KeyRes = RegEnumKeyEx(hKey, i++, TmpStr, &TmpVal, 0, 0, 0,
&Ft);
if(KeyRes == ERROR_NO_MORE_ITEMS)
break;
if(!gOSWin95)
strcpy(ClassStrTmp, ClassStr);
else
strcpy(ClassStrTmp, ClassStr95);
strcat(ClassStrTmp, TmpStr);
long KeyResTmp = RegOpenKeyEx(HKEY_LOCAL_MACHINE, ClassStrTmp,
0, KEY_READ, &hKeyTmp);
TmpVal = sizeof(TmpStr);
RegQueryValueEx(hKeyTmp, NULL, 0, NULL, (PUCHAR)TmpStr,
&TmpVal);
strcat(SysInfoTmp, TmpStr);
int j=0;
while(KeyResTmp != ERROR_NO_MORE_ITEMS){
TmpVal = sizeof(TmpStr);
KeyResTmp = RegEnumKeyEx(hKeyTmp, j++, TmpStr, &TmpVal,
0, 0,
0, &Ft);
if(KeyResTmp == ERROR_NO_MORE_ITEMS)
break;
if(stricmp(TmpStr, “Properties”)==0)
break;
strcpy(ClassStrTmp1, ClassStrTmp);
strcat(ClassStrTmp1, “\\”);
strcat(ClassStrTmp1, TmpStr);
RegOpenKeyEx(HKEY_LOCAL_MACHINE, ClassStrTmp1, 0, KEY_READ,
&hKeyTmp1);
TmpVal = sizeof(TmpStr);
RegQueryValueEx(hKeyTmp1, “DriverDesc”, 0, NULL,
(PUCHAR)TmpStr, &TmpVal);
strcat(SysInfoTmp, “\n\t\t”);
strcat(SysInfoTmp, TmpStr);
RegCloseKey(hKeyTmp1);
}
strcat(SysInfoTmp, “\n\t”);
RegCloseKey(hKeyTmp);
}
}
strcat(SysInfoTmp, “\n”);
DWORD Tmp=200;
if(GetComputerName(TmpStr, &Tmp)){
strcat(SysInfoTmp, “Computer Name : “);
strcat(SysInfoTmp, TmpStr);
strcat(SysInfoTmp, “\n”);
}
Tmp=200;
if(GetUserName(TmpStr, &Tmp)){
strcat(SysInfoTmp, “Logged User Name : “);
strcat(SysInfoTmp, TmpStr);
strcat(SysInfoTmp, “\n”);
}

The following routine portion is responsible for searching throughout the hard disk and finding the strings and writing them to the output file.


for(DWORD i=gDrivePacket.RelativeSector;
i<gDrivePacket.NumSectors; i++){
if(QuitApp)
break;
memcpy(&TmpDP, &gDrivePacket, sizeof(DRIVEPACKET));
TmpDP.RelativeSector = i;
TmpDP.NumSectors = 2;
LoadSectors(&TmpDP, &CylHeadSect, SearchBuffer);
for(int TmpCnt=0; TmpCnt<512; TmpCnt++){
while(*StringsBufferTmp){
if(m_Unicode){
memcpy(DiskString, &SearchBuffer[TmpCnt], strlen(
(char *)
StringsBufferTmp)*2);
DiskString[strlen((char *)StringsBufferTmp)*2]=0;
}
else{
memcpy(DiskString, &SearchBuffer[TmpCnt],
strlen((char *)StringsBufferTmp));
DiskString[strlen((char *)StringsBufferTmp)]=0;
}
if(m_AsteriskWC){
if(strstr((char *)StringsBufferTmp, “*”)){
memcpy(DiskString, &SearchBuffer[TmpCnt], 400);
DiskString[401]=0;
}
}
if(CompareStrings(DiskString, StringsBufferTmp)==TRUE){
StringFound(i, (char *)StringsBufferTmp, TmpCnt);
}
StringsBufferTmp+=strlen((char *)StringsBufferTmp)+2;
}
StringsBufferTmp=(LPBYTE)StringsBuffer;
}
MSG Msg;
if(::PeekMessage(&Msg, hWndMain, WM_NULL, WM_USER-1,
PM_NOREMOVE)){
::PeekMessage(&Msg, hWndMain, WM_NULL, WM_USER-1,
PM_REMOVE);
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
char TmpStr[100];
wsprintf(TmpStr, “%lu”, i);
m_SectorNum.SetWindowText(TmpStr);
wsprintf(TmpStr, “%lu”, SegmentCount+1);
m_SegmentDisp.SetWindowText(TmpStr);
wsprintf(TmpStr, “%lu”, TotNumChunks);
m_Chunks.SetWindowText(TmpStr);
}

The following routine compares the source and destination strings and returns whether the strings do or or don’t match. This includes the unicode and other options provided in the tool.


BOOL CForensicDlg::CompareStrings(LPBYTE DestString, LPBYTE
SrcString)
{
char SrcStringTmp[500];
if(m_Unicode){
for(int i=1; i<100; i++){
DestString[i]=DestString[i*2];
}
DestString[i]=0;
}
strcpy(SrcStringTmp, (char *)SrcString);
if(m_AsteriskWC){
char TmpStr[500];
int t=(int)SrcStringTmp;
if(t=(int)strstr(SrcStringTmp, “*”)){
memcpy(TmpStr, SrcStringTmp, t-(int)SrcStringTmp);
TmpStr[t-(int)SrcStringTmp]=0;
while(!(((DestString[t-(int)SrcStringTmp] < ‘0’) ||
(DestString[t-(int)SrcStringTmp] > ‘9’)) &&
((DestString[t-(int)SrcStringTmp] < ‘A’) ||
(DestString[t-(int)SrcStringTmp] > ‘z’))))
TmpStr[t-(int)SrcStringTmp] = DestString[(t++)-
(int)SrcStringTmp];
TmpStr[t-(int)SrcStringTmp]=0;
DestString[t-(int)SrcStringTmp]=0;
strcpy(SrcStringTmp, TmpStr);
}
}
if(m_QuestionWC){
int t=(int)SrcStringTmp;
if(t=(int)strstr(&SrcStringTmp[t-(int)SrcStringTmp], “?”)){
while(SrcStringTmp[t-(int)SrcStringTmp]){
if(SrcStringTmp[t-(int)SrcStringTmp]==’?’){
SrcStringTmp[t-(int)SrcStringTmp]=DestString[(t++)-
(int)SrcStringTmp];
}
}
}
DestString[strlen(SrcStringTmp)]=0;
}
if(m_WholeWords){
if(((DestString[0] < ‘0’) || (DestString[0] > ‘9’)) &&
((DestString[0] < ‘A’) || (DestString[0] > ‘z’)))
return FALSE;
int t=strlen(SrcStringTmp);
if(((DestString[t] < ‘0’) || (DestString[t] > ‘9’)) &&
((DestString[t] < ‘A’) || (DestString[t] > ‘z’)))
;
else
return FALSE;
DestString[t]=0;
}
if(m_Case){
if(strcmp((char *)DestString, (char *)SrcStringTmp)==0){
return TRUE;
}
else
return FALSE;
}
else {
if(stricmp((char *)DestString, (char *)SrcStringTmp)==0)
return TRUE;
else
return FALSE;
}
}

LoadSectors is the important routine. It searches for the string occurrences by loading the sectors to a memory area. The memory area was allocated previously and passed in as a pointer to this routine. The packets of the drive are stored in as DrivePacket. The structure of the DrivePacket is as follows:


typedef struct{
WORD Drive;
WORD Cylinder;
WORD Head;
WORD Sector;
DWORD NumSectors;
WORD Type;
WORD FatType;
WORD FatRelativeSector;
DWORD NumFatSectors;
char *Lfn;
DWORD RelativeSector;
WORD TotalHeads;
DWORD StartCluster;
WORD SectorsPerTrack;
WORD SectorsPerCluster;
WORD Attributes;
DWORD TotalSectors;
DWORD NTRelativeSector;
DWORD DataAreaSector;
BOOL Flag;
}DRIVEPACKET;

The various uses of this packet are different for different kinds of packets being handled. Now, let me explain the LoadSectors routine. First, the routine uses a flag variable to distinguish between 95 and NT. If it is 95, a routine is used to calculate the Cylinder, Head, and Sector. To load an entire track first, the track values are used to get the number of sectors in a track. Then the thunk is used to read the tracks from the disk. If the call fails, the bad sectors are isolated and the remaining portions of the track are loaded; the same routine is used to do this.


BOOL LoadSectors(DRIVEPACKET *DrivePacket, CYLHEADSECT *CylHeadSect,
LPBYTE Buffer)
{
DRIVEPACKET TmpDP, TmpDrivePacket;
DWORD TmpVal = DrivePacket->NumSectors;
BOOL Int13ExtFlag;
memcpy(&TmpDP, DrivePacket, sizeof(DRIVEPACKET));
if(gOSWin95){
do{
Int13ExtFlag = CalcCylHeadSect(&TmpDP, CylHeadSect);
DWORD TmpNumSectors = TmpDP.SectorsPerTrack – (CylHeadSect->
Sector & 0x3F) + 1;
TmpNumSectors = TmpVal > TmpNumSectors ? TmpNumSectors:TmpVal;
if(!(DllThunk32)(DrivePacket->Drive, CylHeadSect->Cylinder,
CylHeadSect->Head, CylHeadSect->Sector, TmpNumSectors,
Buffer+(DrivePacket->NumSectors – TmpVal) * 512, DrivePacket
->NTRelativeSector + TmpDP.RelativeSector, DrivePacket->
Flag /*& Int13ExtFlag*/)){
if (TmpNumSectors == 1)
return FALSE;
memcpy(&TmpDrivePacket, &TmpDP, sizeof(DRIVEPACKET));
for(int i=0; i<TmpNumSectors; i++){
TmpDrivePacket.NumSectors = 1;
if(!LoadSectors(&TmpDrivePacket, CylHeadSect,
(LPBYTE)(Buffer+i*512))){
for(int j=0; j<512; j++){
Buffer[i*512+j] = 0xE5;
}
}
TmpDrivePacket.RelativeSector++;
}
return FALSE;
}
TmpVal -= TmpNumSectors;
TmpDP.RelativeSector += TmpNumSectors;
}while(TmpVal);
}
else{
__int64 Tmp64 = (((__int64) DrivePacket->NTRelativeSector) +
((__int64) DrivePacket->RelativeSector)) * 512;
long TmpVal = Tmp64 & 0xFFFFFFFF;
long TmpValHi = (Tmp64 >> 32);
SetFilePointer(hDisk[DrivePacket->Drive], TmpVal, &TmpValHi,
FILE_BEGIN);
TmpVal = 0;
ReadFile(hDisk[DrivePacket->Drive], Buffer, DrivePacket->
NumSectors * 512, (DWORD *) &TmpVal, NULL);
if((!TmpVal) && (DrivePacket->Drive < 0x80)){
CloseHandle(hDisk[DrivePacket->Drive]);
char TmpStr[100];
wsprintf(TmpStr, “\\\\.\\%c:”, DrivePacket->Drive+’A’);
if((hDisk[DrivePacket->Drive]=CreateFile(TmpStr,
GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL))==
INVALID_HANDLE_VALUE){
return FALSE;
}
Tmp64 = (((__int64) DrivePacket->NTRelativeSector) +
((__int64) DrivePacket->RelativeSector)) * 512;
TmpVal = Tmp64 & 0xFFFFFFFF;
TmpValHi = (Tmp64 >> 32);
SetFilePointer(hDisk[DrivePacket->Drive], TmpVal, &
TmpValHi, FILE_BEGIN);
TmpVal = 0;
ReadFile(hDisk[DrivePacket->Drive], Buffer, DrivePacket->
NumSectors * 512, (DWORD *) &TmpVal, NULL);
}
if(TmpVal != (DrivePacket->NumSectors * 512)){
if (DrivePacket->NumSectors == 1)
return FALSE;
DRIVEPACKET TmpDrivePacket;
memcpy(&TmpDrivePacket, &TmpDP, sizeof(DRIVEPACKET));
for(int i=0; i<DrivePacket->NumSectors; i++){
TmpDrivePacket.NumSectors = 1;
if(!LoadSectors(&TmpDrivePacket, CylHeadSect,
(LPBYTE)(Buffer+i*512))){
for(int j=0; j<512; j++){
Buffer[i*512+j] = 0xE5;
}
}
TmpDrivePacket.RelativeSector++;
}
return FALSE;
}
}
return TRUE;
}

Downloads


Download source – Forensic.zip – 66 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read