磁盘分区表恢复原理
purpleroc · 2016/01/13 19:36
Author: [email?protected]
Email: [email?protected]
0x00 引子
實在是詞窮想不到要怎么寫題目了,就把vs中的工程名當題目吧。
這篇文,主要講講MBR、DBR、NTFS、FAT32結構等等諸如此類的東西,以及在數據恢復中,我們可以從現有的被破壞了的磁盤中獲取到哪些有利于我們進行數據恢復的信息。
不知道是最近沒休息好還是其他原因,總覺得靜不下心、集中不了注意力,也不知道從什么時候開始,瀏覽網頁只需要幾秒鐘,查找資料也從來不會耐心看完文章,總是一翻到底,用最快的速度去搜索、定位自己要找的內容。但通常來說,網上有的與你問題相同的解決方案并不多。而人們寫文章往往不是奔著主題去的,而是和寫論文、寫書一樣,先把種種后面要用到的概念堆砌起來,然后再來慢慢的說解決方案。當然,其實這樣也挺好的,但作為一個目的驅動者,我更喜歡看需要用到的時候再講的內容。
于是,這篇文就這么來寫吧,免得你看完NTFS和FAT32文件系統就不想看下去了。
0x01 背景
事情是這樣的,這幾天在測試TrueCrypt解密的時候,碰到這么一種情況:
用TrueCrypt做整盤加密系統時,TrueCrypt會重寫磁盤的MBR區域,將原本的MBR加密保存到其他位置,啟動過程中通過了TrueCrypt的密碼驗證后再在內存中恢復原先的MBR并引導進入系統
這么做沒問題,用我的解密程序解完后也能夠正常掛載并訪問,可另一種情況來了:
如果加密的磁盤中,并不是只有一個c盤,而還有其他分區
這時,我的解密工具便不能直接掛載了,雙擊出現:
圖1:
Winhex打開看了看,解密后的數據都是正確的,也看到了DBR:
圖2:
可還是打不開,為啥呢?猜測是分區表被破壞了。那怎么修復呢?瞬間想到了大一那會兒幫別人修電腦,搞壞分區表,花了一晚上找數據(而且還沒找全)的黑歷史,畢竟那會兒不知道用重建分區表的功能,也不理解原理,也就那次之后,呆圖書館看了蠻多數據恢復的書籍,去了解原理。
于是對解密后的文件做了個鏡像,用數據恢復軟件Diskgenuis打開,搜索并重建分區表,得到結果如下:
圖3:
發現他很神奇的把兩個分區找回來了,如果點保存,再去winhex中看,就能當磁盤來分析了。
所以,我的目的是想知道他是怎么恢復分區表的!
想自己寫個程序,能修復鏡像文件中損壞了的分區表,并且能夠當做vhd文件形式被win7以上的系統直接加載,并且在加載完之后,恢復到原有形態。
于是,憋了一周,邊寫邊找資料,終于把程序寫完來寫這文章了。
要恢復分區表,首先得從損壞了分區表的磁盤里找出分區信息,再用這些信息來生成分區表。
那問題是,磁盤中會有哪些信息呢?
0x02 尋址方式(CHS/LBA)
在用正常磁盤做講解前先來了解下磁盤的兩種尋址方式,一種是CHS(cylinder head sector)尋址方式、一種是LBA(Logical Block Addressing)邏輯塊尋址方式。其中CHS(尋址方式)在分區表中使用24個bit位(前10位表示cylinder、中間8位表示head、后6位表示sector),其最大尋址空間為8GB,后因為其滿足不了要求了,人們給他擴充到28bit,但尋址空間也只有128G,面對現有的動輒上TB的硬盤還是無能為力。LBA是用來取代CHS的,LBA是一個整數,常見有32bit和64bit,32bit最大尋址空間達到2TB。
不管CHS(尋址方式)也好,還是LBA(尋址方式)也好。磁盤存儲尋址都需要通過cylinder、head、sector這三個變量來實現。
從上面我們了解到的信息就是,有兩種尋址方式,而且可以相互轉換,再然后呢,歸根到底其實就是用的CHS方式。
這一塊的詳細的介紹以及轉換方式我就不說了,有興趣的可以百度百度,這里也提供一個鏈接:
blog.csdn.net/haiross/art…
0x03 主引導記錄MBR
接著,來看看正常的磁盤中的MBR,所謂MBR即Main Boot Record 主引導記錄區,位于整個硬盤的0磁道0柱面1扇區(也可以用LBA描述成0扇區)。總共占512字節,通常也就是1個扇區的大小。其重要作用就是負責從BIOS手中接過引導權,再去找可引導的分區,并將權限交給可引導的DBR(Dos Boot Record),完成系統啟動。
MBR雖然占了一個扇區,但其Boot_Code部分只占了446個字節。其余64個字節為DPT(Disk Partition Table硬盤分區表),對就是我們要恢復的東西,最后2個字節就是傳說中的標志位55AA了。于是,他的結構體大體如下:
#!cpp typedef struct MBR_T {UCHAR boot_code[446];PartTableRecord partition[4];UCHAR sign[2]; }MBR; 復制代碼對照著結構體,我們看看winhex中的截圖:
圖4:
黃色區域就是boot_code所占用的446個字節,紅色部分就是DPT,藍色就是標志位了。
0x04 磁盤分區表DPT
既然找到了DPT,那肯定是要分析清楚,它是干嘛用的,里面都有些什么信息呢?
直接用winhex的模板看看先:
圖5:
桌面太小,截圖不完全,但也大體知道了里面會有些什么信息,順便翻出結構體如下:
#!cpp typedef struct PartTableRecord_t {BYTE byIsBoot; //引導分區 1B 1B 80(引導分區),00(非引導分區)BYTE byStartHead; //起始磁頭 1B 2BBYTE byStartSector; //起始扇區 1B 3B BYTE byStartCylinder; //起始柱面 1B 4BBYTE byPartType; //分區類型 1B 5B 07(NTFS),05(擴展分區),0B(FAT32)BYTE byEndHead; //結束磁頭 1B 6BBYTE byEndSector; //結束扇區 1B 7BBYTE byEndCylinder; //結束柱面 1B 8BDWORD dwStartSector; //開始扇區 4B 12B DWORD dwTotalSector; //分區扇區數 4B 16B 最大2T Byte } PartTableRecord; 復制代碼恩,每個分區表占用16個字節,而MBR中只留了64個字節,這也是為什么一塊硬盤最多只能創建4個主分區的原因了。多了放不下。那,為啥我們可以看到比四個還多的分區呢?因為擴展分區里面可以創建邏輯分區,這里個數不定,只要你盤符夠,想創建多少就創建多少。
從結構體后的注釋語句也可以知道16字節中每一位分別代表什么含義。這里需要注意的是,表示分區位置和大小的地方有兩個,我們可以通過起始磁頭、扇區、柱面和結束磁頭、扇區、柱面來得到分區位置和大小,也可以直接通過LBA模式記錄的開始扇區和分區扇區數來獲取到分區的位置和大小。
那,問題來了,他們哪個是有用的?
在第二節中尋址方式里講過,CHS能記錄的最大的分區是8.4GB,超過這個大小,就無力了。那這時候32位的LBA自然就派上用場了。
我做了個實驗,把所有分區表中的CHS記錄全部清零,再用winhex加載,還是能夠正常識別。于是,我決定了,后面所有尋址方式均以LBA方式來說。
再回到winhex中看分區信息,來對照DPT一一理解。
圖6:
上圖可以看出,這塊硬盤總共有5個分區。其中主分區三個,擴展分區1個,邏輯分區2個(邏輯分區是在擴展分區里面的)。也就是MBR中64個字節除了主分區就是擴展分區。
根據DPT中的起始扇區以及扇區大小,就可以得到上圖中每個分區的大小、1st sector(起始扇區)了。主分區好說,我們對照著DPT中的其實位置和大小都能看得出,那擴展分區是怎么個形態呢?
我們直接看第四個DPT的信息:007A300B05FE3F1880D0020000600300,忽略掉CHS部分,并對照結構體來看,它告訴了我們這些信息:
#!bash byIsBoot = 0x00 // 非引導分區 byPartType = 0x05 // 擴展分區 dwStartSector = 0x0002D080 //起始扇區 184448 dwTotalSector = 0x00036000 //分區大小 221184 復制代碼這里可以看到,擴展分區的起始位置其實也就是三個主分區的總大小了,再加上自身的分區大小,就是整個磁盤的大小了。例如我這個磁盤是200MB的,現在大小應該是184448 + 221184 = 405632,注意單位是扇區,所以換算成MB應該是 405632/512 * 1024 * 1024 = 198.0625MB。為什么與文件總大小相比少了呢?因為,在分區表后面還有一些未被使用的空間。好奇的是,這個擴展分區中到底放了些什么呢?
在winhex中Ctrl+G輸入扇區號184448,跟隨過去:
圖7:
還是和剛才一樣,黃色部分為前446字節,這里全為0,因為不需要boot_code,而后64字節為擴展分區的分區表信息。在圖的左右下方分別標示出了現在所在的偏移扇區位置,以及總扇區個數和偏移位置(字節數表示)。還是來看這里的DPT信息吧,有兩個分區有信息,這次直接用winhex來看:
圖8:
第一個分區起始扇區是128,總大小為122880,類型是ntfs;第二個分區起始扇區是123008,總大小為94338,類型是擴展分區。需要注意的是,擴展分區的起始扇區是需要加上基地址(擴展分區偏移扇區位置)的。也就是說,我們看到的第一個分區,實際起始地址為:184448 + 128 = 184576,與圖6的partition4的起始位置是一樣的,那下一個呢?再來一個擴展分區的類型是怎么個意思,也還是算一下實際的起始地址:184448 + 123008 = 307456。
再次Ctrl + G跟隨過去:
圖9:
同樣的,再次找到一個DPT信息,這里面只有一項,也就是圖6中的第五個分區了,也來算一下吧:
起始扇區 = 307456 + 128 = 307584,與圖6中第五個分區起始位置一致。
從上面的實例中可以得出,整個磁盤大概是這么分布的:
圖10:
再看擴展分區的鏈接圖示:
圖11:
于是,回到背景里提到的目標,我們要做的就是,根據磁盤中存有的信息,來重建出這么一個分區表。
自然的,我們需要去知道分區表指向的內容是什么!
0x05 操作系統引導分區DBR
在上一節里面,提到了DPT,也提到了分區表的結構體,從結構體里我們可以看到偏移5的位置有鍵值byPartType,分區類型,去找了找資料,這里的取值非常多,常見類型大致如下:
#!bash 00H DOS或WINDOWS不允許使用,視為非法 01H FAT12 04H FAT16小于32MB 05H Extended 06H FAT16大于32MB 07H HPFS/NTFS OBH WINDOWS95 FAT32 OCH WINDOWS95 FAT32 0EH WINDOWS FAT16 0FH WINDOWS95 Extended(大于8G) 82H Linux swap 83H Linux 85H Linux extended 86H NTFS volume set 87H NTFS volume set 復制代碼在結合上一節的分區表,這里主要關注05、07、0B,即擴展分區、NTFS、FAT32三種。而05的在上一節介紹過了,那么,我們將目光投向NTFS與FAT32兩種類型。
5.1 NTFS(New Technology File System)
首先,從MBR中看到分區信息能知道,分區1是NTFS分區,在winhex中跟隨上一節中分區1的起始位置,可以看到如下信息,圖12:
已將每個數據對應的結構和數據都著色了,然后也是時候拿出NTFS文件系統中DBR的數據結構了:
#!cpp typedef struct ntfs_boot_sector_t {BYTE ignored[3]; /* 0x00 Boot strap short or near jump */char system_id[8]; /* 0x03 Name : NTFS */BYTE sector_size[2]; /* 0x0B bytes per logical sector */BYTE sectors_per_cluster; /* 0x0D sectors/cluster */WORD reserved; /* 0x0E reserved sectors = 0 */BYTE fats; /* 0x10 number of FATs = 0 */BYTE dir_entries[2]; /* 0x11 root directory entries = 0 */BYTE sectors[2]; /* 0x13 number of sectors = 0 */BYTE media; /* 0x15 media code (unused) */WORD fat_length; /* 0x16 sectors/FAT = 0 */WORD secs_track; /* 0x18 sectors per track */WORD heads; /* 0x1A number of heads */DWORD hidden; /* 0x1C hidden sectors (unused) */DWORD total_sect; /* 0x20 number of sectors = 0 */BYTE physical_drive; /* 0x24 physical drive number */BYTE unused; /* 0x25 */WORD reserved2; /* 0x26 usually 0x80 */LCN sectors_nbr; /* 0x28 total sectors nbr */QWORD mft_lcn; /* 0x30 Cluster location of mft data.*/QWORD mftmirr_lcn; /* 0x38 Cluster location of copy of mft.*/char clusters_per_mft_record; /* 0x40 */BYTE reserved0[3]; /* zero */char clusters_per_index_record; /* 0x44 clusters per index block */BYTE reserved1[3]; /* zero */LCN volume_serial_number; /* 0x48 Irrelevant (serial number). */DWORD checksum; /* 0x50 Boot sector checksum. */BYTE bootstrap[426]; /* 0x54 Irrelevant (boot up code). */WORD marker; /* 0x1FE */ }ntfs_boot_sector ; 復制代碼再次想想我們的目的,是要重構分區表,而且,我們可以看出,分區表中最重要的就是各分區的分區類型、起始位置以及
分區大小,三個信息了。當然,還有是否為活動分區,但這里我們主要是做數據恢復,所以,暫不考慮其是否作為引導分區了。是的,我們只要獲取到每一個分區的分區類型、起始位置、分區大小三個信息。
我們來看看這個結構體中能提供給我們一些什么:
- sectors_nbr:總扇區數,即分區大小
- system_id:文件系統類型,即分區類型
而在剛才的試驗中,我們也可以看到,分區表中每個起始位置對應的信息,都是指向DBR(Dos Boot Record操作系統引導分區)結構體的。于是,分區的起始位置也是可以獲取到的。
于是,我們的思路是從磁盤中去搜索各個DBR,然后獲取到分區表所需信息,再建立分區表。
既然聊到了NTFS,我們再來看看上面結構體中,還有哪些信息是我們需要的吧:
- sector_size:每扇區字節數,一般情況下是固定512字節的
- sectors_per_cluster:這個也挺重要的,需要引入一個新概念,“簇”。他在后面要用的MFT中起作用。先看看這個鍵值是干嘛用的,它表示的是每簇的扇區數量。
- hidden:字面意思是未使用的扇區數。這里表示的是從分區表到DBR的扇區數量。若為主分區,則hidden表示的是扇區的起始位置。若是擴展分區,則hidden表示的是從擴展分區表到該分區的扇區數。
mft_lcn:$MFT(主文件表)的偏移位置,這里的單位是簇,他LBA位置 = 分區offset + mft_lcn * sectors_per_cluster。以 圖:12 為例,分區起始位置為128,sectors_per_cluster = 8,mft_lcn = 853,所以LBA位置為:128 + 8 * 853 = 6952。
為了證實,我們在winhex中跟隨到6952扇區,如下:
圖13:
在這里,我們也可以用MFT作為判斷某個搜索到的DBR是否正確的條件。有想更詳細了解MFT的可以去找找資料繼續看文件管理方式,這里就不展開了,畢竟,我們這次主要目的是恢復分區表。
mftmirr_lcn:每個MFT表所占用的扇區數量。一般為2.
對我們有用的信息大概也就是上面所提到的了。所以對于恢復NTFS型分區,我們的策略是,全盤搜索,找到NTFS型DBR,之后通過MFT信息來判斷該分區是否正確。同時對于NTFS還需要提醒的是,其DBR扇區不僅僅在分區頭部存在,在分區的最后一個扇區中也有一個備份。所以,就算分區首部的DBR被破壞了,我們也可以通過分區尾部的DBR來恢復出分區表。
5.2 FAT32(32位文件分配表)
上一小節中,我們知道了怎么重建NTFS型分區,接著,來看看FAT32。回到圖6中,看看第三個分區。winhex跟過去,得到截圖如下,這次就不上色了,太累~~~
圖14:
找了找資料,翻出FAT32的結構體表示:
#!cpp typedef struct fat_boot_sector_t {BYTE ignored[3]; /* 0x00 Boot strap short or near jump */char system_id[8]; /* 0x03 Name - can be used to special casepartition manager volumes */BYTE sector_size[2]; /* 0x0B bytes per logical sector */BYTE sectors_per_cluster; /* 0x0D sectors/cluster */WORD reserved; /* 0x0E reserved sectors */BYTE fats; /* 0x10 number of FATs */BYTE dir_entries[2]; /* 0x11 root directory entries */BYTE sectors[2]; /* 0x13 number of sectors */BYTE media; /* 0x15 media code (unused) */WORD fat_length; /* 0x16 sectors/FAT */WORD secs_track; /* 0x18 sectors per track */WORD heads; /* 0x1A number of heads */DWORD hidden; /* 0x1C hidden sectors (unused) */DWORD total_sect; /* 0x20 number of sectors (if sectors == 0) *//* The following fields are only used by FAT32 */DWORD fat32_length; /* 0x24=36 sectors/FAT */WORD flags; /* 0x28 bit 8: fat mirroring, low 4: active fat */BYTE version[2]; /* 0x2A major, minor filesystem version */DWORD root_cluster; /* 0x2C first cluster in root directory */WORD info_sector; /* 0x30 filesystem info sector */WORD backup_boot; /* 0x32 backup boot sector */BYTE BPB_Reserved[12]; /* 0x34 Unused */BYTE BS_DrvNum; /* 0x40 */BYTE BS_Reserved1; /* 0x41 */BYTE BS_BootSig; /* 0x42 */BYTE BS_VolID[4]; /* 0x43 */BYTE BS_VolLab[11]; /* 0x47 */BYTE BS_FilSysType[8]; /* 0x52=82*//* */BYTE nothing[420]; /* 0x5A */WORD marker; }fat_boot_sector; 復制代碼前面是BIOS Parameter Block結構體的一些信息,是公用的,后面的才是單純的FAT32所需要的。用winhex的模板解析一下,得到下面的內容。
圖15:
首先,我們需要的文件類型是可以通過system_id來獲取到的,起始扇區也是可以通過DBR所在的位置得到的,相對NTFS來說FAT32中少了sectors_nbr鍵值,那我們應該如何去獲取到FAT32的總大小呢?
我采用的辦法是,用搜索到的它的后一個DBR的位置減去當前位置,如果后面沒有分區了,則用文件總大小減去當前偏移位置。
這樣,我們也能將分區表需要的三個信息得到。再然后,我們也要來說說這結構體里還有哪些對我們有用的信息。
- hidden:字面意思是未使用的扇區數。這里表示的是從分區表到DBR的扇區數量。若為主分區,則hidden表示的是扇區的起始位置。若是擴展分區,則hidden表示的是從擴展分區表到該分區的扇區數。
- total_sect:本來可以用來表示分區大小的,可只是個長整形變量,只能正確表示小分區的大小,對于大分區無力了。
reserved:保留扇區數,起始,他指的是第一個FAT1表所在的相對偏移位置,例如,在partition3中,起始地址是82048扇區,reserved扇區數量是6718扇區,可以得到,FAT1表起始扇區為82048 + 6718 = 88766。在winhex跟過去,得到信息如下:
圖16:
途中每一種顏色代表一個目錄表項,綠色為0號FAT項,淡黃色為1號FAT項。通常0號FAT項總為0x0ffffff8。于是,這一特征也可以被我們當做判斷分區表是否正確的標準。
fats:表示的是FAT表的個數,通常為2個,即FAT1和FAT2。
fat32_length:每個FAT表的扇區數。在上面我們定位到了FAT1,那么FAT2就是用FAT1的起始位置加上每個FAT表的大小。
info_sector:filesystem_info的起始位置,起始也可以作為FAT32分區是否正確的判斷標志。
backup_boot:FAT32的DBR備份文件存在的位置。上例中,backup_boot = 6,即DBR備份存放在分區起始扇區+6的地方。
有關FAT的一些結構和作用:
具體有關FAT的其他信息,還請自行收集資料,因為,有了前面的信息,我們也可以有辦法恢復FAT32分區對應的分區表數據了。
了解了前面的知識之后,我們就可以開始編寫程序來對分區表進行重建了。
0x06 FileMapping(文件映射)
首先需要從磁盤、磁盤鏡像中找到DBR存在的痕跡,就需要對整個磁盤或鏡像文件進行遍歷搜索,由于分區是線性擴展的,而且DBR所在的位置永遠的是扇區開頭,并且獨自占有一整個扇區。于是我們可以遍歷文件或磁盤中的每個扇區來快速完成搜索。
對于磁盤或者磁盤鏡像文件,肯定不能是通常的直接fopen、fread讀取整個文件了,因為,你沒那么多內存去讀,這時候就要對文件進行分片讀取,比如,每10M讀一次,如此循環將其遍歷一次。
但考慮到吞吐率的問題,這里引用的是FileMapping文件映射的方式,將文件直接映射到內存中進行操作。當文件比較小的時候,我們可以直接全文件映射起來,但通常不太建議這么做。
我采用的也是上面說的循環讀取的思路,對大的磁盤文件循環映射起來,代碼如下:
#!cpp #define MAPPING_SIZE 67108864 #define BYTE_PER_M 1024*1024// // big_file: 需要映射的文件路徑 // ll_file_size: 需要映射的文件總大小 // int ToMapping(char *big_file, unsigned __int64 ll_file_size) { LCN i = 0;//得到系統分配粒度SYSTEM_INFO sinf;GetSystemInfo(&sinf);DWORD dwAll = sinf.dwAllocationGranularity;printf ("Total %dM.\nSearching...\n",ll_file_size / BYTE_PER_M ;if (ll_file_size <= MAPPING_SIZE) //內存鏡像小于64M時,一次性掛載{Maping_file(big_file, 0, ll_file_size);}else{for (i = 0; i < (ll_file_size / MAPPING_SIZE) ; i++) //否則以64M為一個鏡像映射單位,循環掛載,直到全部映射完成{ if (i == 0){Maping_file(big_file, (i * (MAPPING_SIZE)) - (i * (MAPPING_SIZE) % dwAll), MAPPING_SIZE);}else {Maping_file(big_file, (i * (MAPPING_SIZE)) - ((i * (MAPPING_SIZE)) % dwAll), MAPPING_SIZE); }}if (ll_file_size > (i * MAPPING_SIZE)){ //最后一次可能并不是64M,需要根據實際大小來映射Maping_file(big_file, i * MAPPING_SIZE - ((i * MAPPING_SIZE) % dwAll), ll_file_size - i * MAPPING_SIZE);}}return 0; } 復制代碼ToMapping完成的是將文件分片交給Maping_file函數處理。
#!cpp int Maping_file(char* big_file, LCN lOffset, long lSize) {char* pPtr_File; //存放指向內存映射文件的首地址HANDLE hFile = CreateFileA(big_file, GENERIC_READ, FILE_SHARE_READ,NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);if ( hFile == INVALID_HANDLE_VALUE){ //Open the data file.ErrorOut("CreateFile() Error!");}HANDLE hFileMapping = CreateFileMapping(hFile, NULL, //Create the file-mapping object.PAGE_READONLY,0, 0,NULL);if (hFileMapping == INVALID_HANDLE_VALUE){ErrorOut("CreateFileMapping() Error!");}PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_READ,lOffset & 0xFFFFFFFF00000000, // Offset highlOffset & 0xFFFFFFFF, // Offset lowlSize); // bytes to mapif (pbFile == INVALID_HANDLE_VALUE){ErrorOut("MapViewOfFile() Error!");}/pPtr_File = (char*)pbFile;ToGetDBR(pPtr_File, lSize, lOffset);//UnmapViewOfFile(pbFile);CloseHandle(hFileMapping);CloseHandle(hFile);return 0; } 復制代碼這樣,就能通過CreateFileMapping將大的鏡像文件分塊為64M一小塊映射起來,之后調用ToGetDBR,來對磁盤中殘留的DBR信息進行搜索。
0x07 搜索DBR
這里的思路是,遍歷整個磁盤,而后檢測每個扇區,看是否滿足NTFS或FAT32型分區格式。若滿足,則鍵入到鏈表中。
#!cpp void ToGetDBR(char* p_file, long l_size, LCN offset) {long i = 0;char *buf = NULL;char *temp = NULL;LCN ll_offset = 0;do {buf = p_file + i * SECTOR_SIZE;if (!test_NTFS((ntfs_boot_sector*)buf, offset + i * SECTOR_SIZE)){ll_offset = offset + (i * SECTOR_SIZE);temp = (char*)malloc(512);memcpy(temp, buf, 512);if (InsertDBRList(g_dbr_list_head, temp, 1, g_n_dbr, ll_offset)) // NTFS type is 1{//printf ("Found NTFS! AT %lld sectors\n", ll_offset / SECTOR_SIZE);}ll_offset = 0;temp = NULL;}if(!test_FAT((fat_boot_sector*)buf, offset + i * SECTOR_SIZE)){ll_offset = offset + (i * SECTOR_SIZE);temp = (char*)malloc(512);memcpy(temp, buf, 512);if (InsertDBRList(g_dbr_list_head, temp, 2, g_n_dbr, ll_offset)) // FAT32 type is 2{//printf("Found FAT! AT %lld sectors\n", ll_offset / SECTOR_SIZE);}ll_offset = 0;temp = NULL;}i++;} while (i * SECTOR_SIZE < l_size); } 復制代碼檢測是否為NTFS與FAT32形式的磁盤可根據NTFS及FAT32結構體特征來判斷,這里判斷代碼如下:
#!cpp int test_NTFS(const ntfs_boot_sector*ntfs_header, LCN l_size) {LCN lba = l_size / SECTOR_SIZE;chs tmp;int verbose = 1;if(ntfs_header->marker!=0xAA55 ||(ntfs_header->reserved)>0 ||ntfs_header->fats>0 ||ntfs_header->dir_entries[0]!=0 || ntfs_header->dir_entries[1]!=0 ||ntfs_header->sectors[0]!=0 || ntfs_header->sectors[1]!=0 ||ntfs_header->fat_length!=0 || (ntfs_header->total_sect)!=0 ||memcmp(ntfs_header->system_id,"NTFS",4)!=0)return 1;switch(ntfs_header->sectors_per_cluster){case 1: case 2: case 4: case 8: case 16: case 32: case 64: case 128:break;default:return 1;}return 0;int test_FAT(const fat_boot_sector* fat_header, LCN l_size) {if(!(fat_header->marker==0xAA55&& (fat_header->ignored[0]==0xeb || fat_header->ignored[0]==0xe9)&& (fat_header->fats==1 || fat_header->fats==2)))return 1; /* Obviously not a FAT */switch(fat_header->sectors_per_cluster){case 1: case 2: case 4: case 8: case 16: case 32: case 64: case 128:break;default:return 1;}switch(fat_header->fats){case 1:break;case 2:break;default:return 1;}return 0; } 復制代碼而所用到的DBR鏈表結構體如下:
#!cpp typedef struct dbr_list_t {char* dbr; // DBR contentint n_type; // DBR type ntfs = 1 fat32 = 2int flag; // Is this DBR a Available?int n_is_org; // Is this DBR a orignal or copy.__int64 ll_offset; // DBR offset.__int64 ll_total_sector; // Partition offset.__int64 ll_start_sector; // Partition Size.dbr_list_t* p_next; // Point to next dbrdbr_list_t(){dbr = NULL;n_type = 0;p_next = 0;ll_offset = 0;n_is_org = 0;flag = 0;ll_total_sector = 0;ll_start_sector = 0;} }dbr_list; 復制代碼在搜索過中,對dbr、n_type、ll_offset、p_next四個鍵值進行賦值,得到整個磁盤中可能存在的分區信息。并將這些搜索到的DBR存放在鏈表中,方便進一步處理。
0x08 判斷DBR
在上一小節,我們完成了對整個磁盤中可能存在的分區信息的搜索。那,搜索到的結果肯定不會全部正確或者可用,為了使重建的分區表更加可靠,需要對搜索到的分區信息進行篩選,以及鏈表信息的填充。
在0x05中談到了NTFS和FAT32類型DBR的一些特性,這里就需要用到這些特性對去進行判斷。
對于NTFS型的DBR。
#!cpp if (p_dbr_temp->n_type == 1) // NTFS {p_ntfs_temp = (ntfs_boot_sector*)p_dbr_temp->dbr;if (p_ntfs_temp->sectors_nbr < (g_ll_file_size / SECTOR_SIZE)) // 獲取到的大小不能比總大小還大{flag = 0;flag = JudgeMFT(file_path, p_dbr_temp, p_ntfs_temp);if (flag){p_dbr_temp->flag = 1; // 設置dbr可用標志位p_dbr_temp->ll_total_sector = (LCN)p_ntfs_temp->sectors_nbr; // 設置分區總大小g_n_part++;}// 輸出信息printf("Type: NTFS.\tOffset: %I64u.\tSize %I64u.\t Hidden: %lu\tMFT at %I64u cluster.\t MFT is %s!\n",p_dbr_temp->ll_offset / SECTOR_SIZE, (LCN)p_ntfs_temp->sectors_nbr, p_ntfs_temp->hidden,p_ntfs_temp->mft_lcn.QuadPart,flag ? "Right" : "Wrong");} } 復制代碼若兩項都滿足,則判定其為正確的NTFS類型DBR,即可能為正確的分區信息。那該如何判斷MFT是否正確呢?在0x05中也提到過,定位到MFT的方法,定位過去之后,讀取文件看是否為滿足主文件記錄的格式。需要注意的是,對于NTFS搜索到的DBR有可能是分區起始扇區的,也有可能是分區結束扇區的,兩者都需要考慮,并且,在判斷MFT的同時,需要把dbr_list中的其他信息填充完整。
#!cpp // //file_path: 鏡像文件路徑 //p_dbr:dbr_list結構體 //ntfs:dbr // int JudgeMFT(char* file_path, dbr_list* p_dbr, ntfs_boot_sector* ntfs) {char sz_temp1[4] = {0};char sz_temp2[4] = {0};DWORD readsize;LARGE_INTEGER tmp1 = {0};LARGE_INTEGER tmp2 = {0};tmp1.QuadPart = p_dbr->ll_offset + (ntfs->mft_lcn.QuadPart * ntfs->sectors_per_cluster * SECTOR_SIZE);tmp2.QuadPart = p_dbr->ll_offset - (ntfs->sectors_nbr * SECTOR_SIZE) + (ntfs->mft_lcn.QuadPart * ntfs->sectors_per_cluster * SECTOR_SIZE);if (!ReadFileOffset(file_path, tmp1.QuadPart, 4, sz_temp1, FILE_BEGIN))ErrorOut("ReadFile Error!\n");if (!memcmp(sz_temp1, "FILE", 4)){p_dbr->ll_start_sector = p_dbr->ll_offset / SECTOR_SIZE;return 1;}else{if (!ReadFileOffset(file_path, tmp2.QuadPart, 4, sz_temp2, FILE_BEGIN))ErrorOut("ReadFile Error!\n");if (!memcmp(sz_temp2, "FILE", 4)){p_dbr->ll_start_sector = p_dbr->ll_offset / SECTOR_SIZE - ntfs->sectors_nbr;p_dbr->n_is_org = 1;return 1;}}return 0; } 復制代碼對于FAT32型,需要考慮的似乎只有是否滿足能正確找到FAT表,如0x05中所說,其大小是需要靠后一個分區的起始扇區或文件總大小來獲取的。
#!cpp if (p_dbr_temp->n_type == 2) // FAT {p_fat_temp = (fat_boot_sector*)p_dbr_temp->dbr;if (!memcmp(p_fat_temp->BS_FilSysType, "FAT32", 5)) // 只處理FAT32{flag = 0;flag = JudgeFAT(file_path, p_dbr_temp, p_fat_temp);if (flag){p_dbr_temp->flag = 1;g_n_part++;}} 復制代碼在對于FAT32型DBR,同樣,也需要考慮獲取的DBR是backup的情況:
#!cpp int JudgeFAT(char *file_path, dbr_list* p_dbr, fat_boot_sector* fat) {char sz_temp1[4] = {0};char sz_temp2[4] = {0};LARGE_INTEGER tmp1 = {0};LARGE_INTEGER tmp2 = {0};char flag[4] = {'\xf8', '\xff', '\xff', '\x0f'};DWORD readsize = 0;tmp1.QuadPart = p_dbr->ll_offset + (fat->reserved * SECTOR_SIZE);tmp2.QuadPart = p_dbr->ll_offset - ((fat->backup_boot + fat->reserved) * SECTOR_SIZE);if (!ReadFileOffset(file_path, tmp1.QuadPart, 4, sz_temp1, FILE_BEGIN))ErrorOut("ReadFile Error!\n");if (!memcmp(sz_temp1, flag, 4)){p_dbr->ll_start_sector = p_dbr->ll_offset / SECTOR_SIZE;return 1;}else{if (!ReadFileOffset(file_path, tmp2.QuadPart, 4, sz_temp2, FILE_BEGIN))ErrorOut("ReadFile Error!\n");if (!memcmp(sz_temp2, flag, 4)){p_dbr->ll_start_sector = p_dbr->ll_offset / SECTOR_SIZE - fat->backup_boot;p_dbr->n_is_org = 1; return 1;}}return 0; } 復制代碼完成這一步后,我們得到了篩選出了整個磁盤中所有可用的DBR信息,并且獲取到了重建分區表所需要的分區類型、起始扇區、分區大小三個信息。
可以將其輸出由用戶選擇需要恢復的分區:
#!cpp /*顯示DPT*/ int ShowDPT() {dbr_list* p_dbr_temp = NULL; __int64 tmp = 0;printf("\n\nChosse the partition you want to rebuild?\n");for(p_dbr_temp = g_dbr_list_head->p_next; p_dbr_temp != NULL;) {if (p_dbr_temp->flag) // 需要添加{p_dbr_temp->flag = 0; // 清空標志位置if (tmp < p_dbr_temp->ll_start_sector){printf("\nPartition with type %s.\tStart with %lld sectors.\t Size %lld sectors.\t End with %lld sectors.\nIs this partition you want to restore?(y/n)", (p_dbr_temp->n_type == 1?"NTFS":"FAT32"), p_dbr_temp->ll_start_sector,p_dbr_temp->ll_total_sector,p_dbr_temp->ll_start_sector + p_dbr_temp->ll_total_sector);if (getchar() == 'y'){p_dbr_temp->flag = 1;tmp = p_dbr_temp->ll_start_sector + p_dbr_temp->ll_total_sector;g_n_part++;getchar();}elsegetchar();}}p_dbr_temp = p_dbr_temp->p_next;}return 0; } 復制代碼0x09 重構DPT
重構DPT需要考慮的問題有以下幾個:
總分區數是否大于4個
a. 若不是,則可全寫入0扇區MBR中 b. 若是,則需要新建擴展分區完成擴展
獲取到的DBR是否為分區起始扇區DBR
a. 若是,則無需更改 b. 若不是,則需要將作為backup的DBR復制到分區起始扇區
分區表是線性一次擴展下去的,不存在分區交叉的情況,即各分區大小之和為文件總大小,各分區無公共部分。(這一步在0x08中的showdpt中做了處理)
創建擴展分區時,主分區DPT最后一條記錄是指向擴展DPT,擴展DPT的最后一條記錄繼續指向下一個擴展DPT。
MBR中boot_code部分信息可以不用考慮
考慮完了這些問題,就可以來編碼實現了。我的做法是,新建一個用于重構的鏈表,將所有要更改的內容、要更改的內容的大小、以及要更改的位置寫入到鏈表中,方便后面寫文件以及恢復文件。
#!cpp void ReBuildDPT(char* sz_file_path) {rebuild_content_t* rebuild_list = CreateReBuildHead();if (g_n_part <= 4) // 小于四個分區時,只需要建立主分區表{sz_tmp = (char*)malloc(4 * sizeof(PartTableRecord) + 2);memset(sz_tmp, 0, 4 * sizeof(PartTableRecord) + 2);for(p_dbr_temp = g_dbr_list_head->p_next; p_dbr_temp != NULL;) {if (p_dbr_temp->flag) // 是否需可用信息{*(sz_tmp + k * 16 + 4) = (p_dbr_temp->n_type == 1) ? 0x07 : 0x0B; // byPartTypememcpy(sz_tmp + k * 16 + 8, (char *)&(p_dbr_temp->ll_start_sector), sizeof(__int64)); // dwStartSectormemcpy(sz_tmp + k * 16 + 12, (char *)&(p_dbr_temp->ll_total_sector), sizeof(__int64)); // dwTotalSectork++;if (p_dbr_temp->n_is_org) // 是否起始扇區{InsertRebuildList(rebuild_list, p_dbr_temp->dbr, SECTOR_SIZE, p_dbr_temp->ll_start_sector, i++);}}p_dbr_temp = p_dbr_temp->p_next;}memcpy(sz_tmp + 64, sign, 2); InsertRebuildList(rebuild_list, sz_tmp, 4 * sizeof(PartTableRecord) + 2, 446, i++);}else // 否則考慮擴展分區的情況{sz_tmp = (char*)malloc(4 * sizeof(PartTableRecord) + 2);memset(sz_tmp, 0, 4 * sizeof(PartTableRecord) + 2);for(p_dbr_temp = g_dbr_list_head->p_next; p_dbr_temp != NULL;) {if (p_dbr_temp->flag) // 是否需可用信息{if (k < 3) // 主分區只能有三個,最后一個為擴展分區{if (k != 2){*(sz_tmp + k * 16 + 4) = (p_dbr_temp->n_type == 1) ? 0x07 : 0x0B; // byPartTypememcpy(sz_tmp + k * 16 + 8, (char *)&(p_dbr_temp->ll_start_sector), sizeof(__int64)); // dwStartSectortmp = p_dbr_temp->ll_total_sector + 1;memcpy(sz_tmp + k * 16 + 12, (char *)&tmp, sizeof(__int64)); // dwTotalSectork++;}else{*(sz_tmp + k * 16 + 4) = (p_dbr_temp->n_type == 1) ? 0x07 : 0x0B; // byPartTypememcpy(sz_tmp + k * 16 + 8, (char *)&(p_dbr_temp->ll_start_sector), sizeof(__int64)); // dwStartSectortmp = p_dbr_temp->ll_total_sector + 1;memcpy(sz_tmp + k * 16 + 12, (char *)&tmp, sizeof(__int64)); // dwTotalSectork++;for (p_dbr_temp_tmp = p_dbr_temp->p_next; p_dbr_temp_tmp != NULL;){if (p_dbr_temp_tmp->flag){*(sz_tmp + k * 16 + 4) = 0x05; // byPartTypetmp = p_dbr_temp_tmp->ll_start_sector - 1;memcpy(sz_tmp + k * 16 + 8, (char *)&tmp, sizeof(__int64)); // dwStartSectortmp = (g_ll_file_size/SECTOR_SIZE) - p_dbr_temp_tmp->ll_start_sector + 1;memcpy(sz_tmp + k * 16 + 12, (char *)&tmp, sizeof(__int64)); // dwTotalSectork++;memcpy(sz_tmp + 64, sign, 2); InsertRebuildList(rebuild_list, sz_tmp, 4 * sizeof(PartTableRecord) + 2, 446, i++);break;}p_dbr_temp_tmp = p_dbr_temp_tmp->p_next;}}}else{sz_tmp = NULL;sz_tmp = (char*)malloc(4 * sizeof(PartTableRecord) + 2);memset(sz_tmp, 0, 4 * sizeof(PartTableRecord) + 2);*(sz_tmp + 4) = (p_dbr_temp->n_type == 1) ? 0x07 : 0x0B; // byPartTypetmp = 1; // 擴展分區偏移地址從當前地址算起(相對地址)memcpy(sz_tmp + 8, (char *)&tmp, sizeof(__int64)); // dwStartSectormemcpy(sz_tmp + 12, (char *)&(p_dbr_temp->ll_total_sector), sizeof(__int64)); // dwTotalSectorif (p_dbr_temp->p_next != NULL){for (p_dbr_temp_tmp = p_dbr_temp->p_next; p_dbr_temp_tmp != NULL;){if (p_dbr_temp_tmp->flag){*(sz_tmp + 16 + 4) = 0x05; // byPartTypetmp = p_dbr_temp_tmp->ll_start_sector - p_dbr_temp->ll_start_sector;//tmp = 1;memcpy(sz_tmp + 16 + 8, (char *)&tmp, sizeof(__int64)); // dwStartSectortmp = (g_ll_file_size/SECTOR_SIZE) - p_dbr_temp_tmp->ll_start_sector;memcpy(sz_tmp + 16 + 12, (char *)&tmp, sizeof(__int64)); // dwTotalSectorbreak;}p_dbr_temp_tmp = p_dbr_temp_tmp->p_next;}}memcpy(sz_tmp + 64, sign, 2); InsertRebuildList(rebuild_list, sz_tmp, 66, (p_dbr_temp->ll_start_sector - 1) * SECTOR_SIZE + 446, i++);}if (p_dbr_temp->n_is_org) // 是否起始扇區{InsertRebuildList(rebuild_list, p_dbr_temp->dbr, SECTOR_SIZE, p_dbr_temp->ll_start_sector * SECTOR_SIZE, i++);}}p_dbr_temp = p_dbr_temp->p_next;}}HandleFile(sz_file_path, rebuild_list);FreeRebuildList(rebuild_list); } 復制代碼0x0A 文件處理
通過前面的操作,可以得到處理好了的rebuild_list。接下來要做的就是用它來完成重建分區表,恢復DBR的工作。
首先,我們的目的是,重建后的分區表后文件能作為VHD直接被win7以上系統加載。其次,希望能夠在我卸載VHD文件后,仍然恢復到原有狀態。意味著需要對更改的信息做一個備份,這沒問題,因為我們替換先前rebuild_list中的content就可以完成了。
之后,需要了解VHD文件格式。找了找資料。發現,VHD文件僅僅在文件尾部添加了一個扇區的內容,其結構如下:
#!cpp /*vhd尾部信息結構*/ typedef struct hd_ftr_t { char cookie[8]; /* Identifies original creator of the disk */ unsigned int features; /* Feature Support -- see below */ unsigned int ff_version; /* (major,minor) version of disk file */ unsigned __int64 data_offset; /* Abs. offset from SOF to next structure */ unsigned int timestamp; /* Creation time. secs since 1/1/2000GMT */ char crtr_app[4]; /* Creator application */ unsigned int crtr_ver; /* Creator version (major,minor) */ unsigned int crtr_os; /* Creator host OS */ unsigned __int64 orig_size; /* Size at creation (bytes) */ unsigned __int64 curr_size; /* Current size of disk (bytes) */ unsigned int geometry; /* Disk geometry */ unsigned int type; /* Disk type */ unsigned int checksum; /* 1's comp sum of this struct. */ unsigned char uu[16]; /* Unique disk ID, used for naming parents */ char saved; /* one-bit -- is this disk/VM in a saved state? */ char hidden; /* tapdisk-specific field: is this vdi hidden? */ char reserved[426]; /* padding */ }hd_ftr; 復制代碼這張表中,重要的就是orig_size、curr_size和checksum,通常情況下orig_size與curr_size相同,checksum是最后一個扇區所有字節相加后取反的值。我建了個模板來實現VHD標志位的添加。
所以,最終的文件處理模塊如下:
#!cpp void HandleFile(char* file_path, rebuild_content_t* p_rebuild_list) {char* sz_vhd_buf = (char*)malloc(SECTOR_SIZE);memset(sz_vhd_buf, 0, SECTOR_SIZE);rebuild_content_t* p_rebuild_tmp = NULL;char tmp[SECTOR_SIZE] = {0}; // Gen VHDhd_ftr* vhd;vhd = (hd_ftr*)data;LARGE_INTEGER offset = {0};DWORD readsize = 0;/*Set hd_ftr struct*/vhd->orig_size = 0; // clearvhd->orig_size = g_ll_file_size - SECTOR_SIZE;vhd->orig_size = INT64_TO_NET(vhd->orig_size);vhd->curr_size = vhd->orig_size;vhd->checksum = 0;/*calc checksum*/unsigned int temp = 0;for (int i = 0; i < 512; i++){temp += data[i];}vhd->checksum = htonl(~temp); //for(p_rebuild_tmp = p_rebuild_list->p_next; p_rebuild_tmp != NULL;) {if (!ReadFileOffset(file_path, p_rebuild_tmp->ll_offset, p_rebuild_tmp->n_size, tmp, FILE_BEGIN))ErrorOut("Backup Read Error!\n");if (!WriteFileOffset(file_path, p_rebuild_tmp->ll_offset, p_rebuild_tmp->n_size, p_rebuild_tmp->content, FILE_BEGIN))ErrorOut("Backup Write Error!\n");memcpy(p_rebuild_tmp->content, tmp, p_rebuild_tmp->n_size); // BackUp SECTORp_rebuild_tmp = p_rebuild_tmp->p_next;}/ BackUp VHDReadFileOffset(file_path, -SECTOR_SIZE, SECTOR_SIZE, sz_vhd_buf, FILE_END);/* */// Write VHDWriteFileOffset(file_path, -SECTOR_SIZE, SECTOR_SIZE, (char*)vhd, FILE_END);printf("WriteFile Success! You can mount it as vhd file now!\n");system("pause");// Restore SECTORfor(p_rebuild_tmp = p_rebuild_list->p_next; p_rebuild_tmp != NULL;) {if (!ReadFileOffset(file_path, p_rebuild_tmp->ll_offset, p_rebuild_tmp->n_size, tmp, FILE_BEGIN))ErrorOut("Restore Read Error!\n");if (!WriteFileOffset(file_path, p_rebuild_tmp->ll_offset, p_rebuild_tmp->n_size, p_rebuild_tmp->content, FILE_BEGIN))ErrorOut("Restore Write Error!\n");memcpy(p_rebuild_tmp->content, tmp, p_rebuild_tmp->n_size); // BackUp SECTORp_rebuild_tmp = p_rebuild_tmp->p_next;}/ Restore VHDWriteFileOffset(file_path, -SECTOR_SIZE, SECTOR_SIZE, sz_vhd_buf, FILE_END);printf("Restore File Success!\n"); } 復制代碼這樣,重建分區表的工作就完成了。
0x0B 程序效果
還是用前面例子中用到的VHD文件來做演示,該VHD現在的情況是:先前新建過兩個NTFS的分區,之后刪除了一個NTFS,并重新格式化成FAT32,再然后,我將其分區全部刪除,并新建了3個NTFS與兩個FAT32,最終效果如圖6所示。現在我們用剛寫好的工具來對它進行搜索分析。
圖17:
可以看到它總共搜索到了8個可用的DBR信息,但顯然其中有一些是前幾次分區留下的內容。選擇保留1.2.3.5.7五個分區(也就是圖6所示的五個分區),起始你選擇保留第三個的時候,就不有機會讓你選擇第四個了,因為他們是沖突的。結果如圖18:
提示寫文件成功,并已經可以直接當做VHD文件掛載了,我們用winhex打開處理過的文件,得到圖19:
可以看到,重構后的分區表幾乎與原來的分區表一致。
接著,我們試著來恢復前幾次分區留下的信息,看是否能夠成功。圖20:
這里我選擇恢復了一個NTFS和一個FAT32,再進winhex中查看,得到圖21:
可以看到第一個NTFS分區,是并不存在與圖6的五個分區中的。
再次,我們用圖1的那個TrueCrypt解密后的打不開的磁盤作為例子,看看這次能否恢復出正確的分區表,并且,實現加載。找到可用的DBR信息如圖22:
按照邏輯,選擇了第一個后,就只能選擇1和2。已提示寫文件成功,圖23:
Winhex解析如圖24:
正確完整恢復,所有文件也能正確解析,圖25:
接著,在做只留下尾部備份分區測試程序需找情況的時候,發現起始我們程序中并沒有必要將備份分區復制到起始扇區,因為,也能正常解析。
于是,完工!(不過,還有個小細節需要注意,如果起始位置是個DBR頭部的話,系統將不會當做MBR處理,而是當做DBR,所以,清除頭部信息也很重要!)
0x0C Summarize
加上寫代碼的時間和些這文章的時間,大概花費了近十天吧。各種找資料、調程序、做樣本,也算是把恢復分區表的原理弄清楚了。同時,也把忘得差不多的磁盤格式、文件系統什么的再撿起來看了看。當然文章其實并不詳細,因為關注點不一樣,就沒講太多關于文件系統的內容了。再往下,可以詳細到文件的恢復等等,當然,起始也沒必要去做,畢竟現有的工具一大堆,我只是突然感興趣就拿來實現了一把。
再者,本程序暫時只支持NTFS和FAT32兩種類型,若有其他類型的原理應該也是一致,大家仔細琢磨。
寫文章也比較倉促,也沒校稿習慣,若有勘誤,還請諒解并提醒更正。感謝!
源碼下載地址:github.com/purpleroc/h…
另推薦學習源碼:testdisk、ReadPartTable
——Tracy_梓朋
2016年1月6日21:31:52
總結
- 上一篇: Day8:盈利源泉是否可持续(1)
- 下一篇: vue3 + vite +ts 引入静态