4-2.3) FAT32 분석 3 (특징과 분석 도구)
FAT32의 파일시스템의 기술적 특징을 정리하고, 간단한 분석도구를 만들어 봅시다.
3. FAT32 파일시스템의 특징
위 실습 등으로 충분히 FAT32의 기술적 접근을 진행하였습니다. 이제 아래 특징등이 충분히 이해가 될 것입니다.
FAT32의 기술적 주요 특징
VBR 예약영역, FAT1, FAT 복사본 그리고 데이터 영역으로 구분되어 있다. FAT 복사본이 있어 안정성이 조금 더 있다.
VBR의 복사본은 6번에 보통 위치한다. 이를 이용하여 파일시스템 구조를 복구할 수 있다. (디지털포렌식 2급 실기 단골 기출문제)
FAT 영역은 FAT 엔트리끼리 다음 클러스터 번호를 가리키고, 다음 클러스터를 가리키고 하는 링크드 리스트 구조를 가진다.
파일시스템의 최대 용량은 약 2T 미만이다. 이유는? FAT32 부트레코드의 전체 섹터 수를 저장하는 공간는 4바이트로 FF FF FF FF X 섹터 당 바이트 수 = 2,199,023,255,040 bytes = 약 1.99T Bytes 이다.
단일 파일의 최대 용량은 약 4G 미만이다. 이유는? 디렉토리 엔트리의 파일 사이즈를 나타내는 공간이 4바이트 FF FF FF FF = 4,294,967,295 bytes = 약 3.99 Gbytes 이다.
폴더의 크기는 디렉토리 엔트리 개수가 늘어날 수록 커진다. 만약 1클러스터로 부족하면 2클러스터를 새로 할당 받아 사용. (일반 파일과 동일한 구조로, 폴더는 데이터가 디렉토리 엔트리이다.)
FAT32는 LFN(Long File Name) 지원을 통해 최대 255자의 긴 파일 이름을 허용합니다. 총 LFN 엔트리 하나에 유니코드(2bytes)로 13자씩 저장이 되기 때문에 최대 20개의 LFN 엔트리 + 기본 디렉토리 엔트리를 해서 총 21개의 디렉토리 엔트리로 구성이 가능함. 13 X 20 = 260자 저장이 되나, 255자만 표현하도록 여러 시스템에서 제한하는 것은, 무제한으로 긴 파일명을 할 경우 디렉토리 엔트리공간 낭비가 될 수 있으므로 255자 정도로 제한함. 따라서 폴더 내 파일명이 긴 파일이 매우 많다면, 폴더의 크기도 커지게 될 것이다. (즉, 폴더의 크기는 파일명에 영향을 받는다.)
윈도우에서는 FAT32를 포맷하면 레이블이 생성되며, 레이블이 변경되지 않았다면 사실상 파일시스템 생성 또는 포맷시간의 근접한 시간을 확인할 수 있다. 레이블이 변경된다면, 레이블의 Write Date, Time 이 덮어써져 최초의 레이블 생성시간은 파악할 수 없다.
다만 다른 파일시스템에 비해 오류 등에 대한 데이터 복구가 어려울 수 있다. (이 부분은 다른 파일시스템 구조를 공부하면 이해가 될 것이다.)
이렇게 단순한 구조 때문에 많은 시스템 등에서 활용하고 있는 매우 대중적인 파일시스템입니다.
많은 시스템이라 하면 단순 컴퓨터 뿐만 아니라, 디지털카메라, 스마트TV 등 저장이라는 것을 하는 거의 모든 장치가 FAT32를 지원하고 있습니다. 즉, 파일시스템을 FAT32로 구성하여 파일을 저장할 경우 많은 곳에서 사용이 가능합니다.
대신 최근에는 점점 용량이 큰 파일들이 늘어나기 때문에 사용율이 점점 떨어지는 것도 사실입니다.
그러나 디지털포렌식에서 중요한 파일시스템을 공부하는 시작에 있어서 분석할만한 가치가 있는 파일시스템으로 사실 FAT32의 거의 모든 것을 분석해보았습니다.
이제 다른 파일시스템도 이런식로 하나하나 분석해 나갈 수 있을 것이다.
최근 파일시스템들은 다양한 옵션들의 추가로 속성들의 수가 매우 많기 때문에 모든 걸 분석하기에는 너무 많아 정말 필요한 부분만 공부해나가고, 추후에 다양한 속성들을 이용한 포렌식적 접근을 위해 여러가지 상황에 대해 장난치기 등을 통해 다양한 연구를 해보는 것을 추천합니다!
4. 간단한FAT32 분석 도구
우리가 공부한 것을 토대로 FAT32 간단 분석 도구를 만들어 보았습니다. 기본적으로 물리매체 접근하고, 파티션에 FAT32가 있으면 디렉토리 엔트리 분석 등을 하는 것과 논리적으로 운영체제에서 제공하는 기능으로 논리적인 접근을 하여 파일을 분석하는 것을 비교하는 도구입니다.
4-1) 사용법
필요한 모듈 먼저 아래 명령어를 입력하여 필요한 모듈을 설치해야 합니다.
pip install pywin32
이 후 관리자 권한으로 파이썬 소스를 실행합니다.
ChatGPT로 만든 전체 Python 소스 (714 줄)
import struct
import win32file
import subprocess
import tkinter as tk
from tkinter import ttk, simpledialog, messagebox
import os
selected_drive = None
root_dir_sector = None
sectors_per_cluster = None
def list_physical_drives():
drives = []
result = subprocess.run(['wmic', 'diskdrive', 'get', 'DeviceID,Model'], stdout=subprocess.PIPE, text=True)
lines = result.stdout.strip().split('\n')[1:]
for line in lines:
parts = line.strip().split()
if len(parts) >= 2:
device_id = parts[0]
model = ' '.join(parts[1:])
drives.append((device_id, model))
return drives
def open_drive(drive, access_mode):
return win32file.CreateFile(drive, access_mode, win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE, None, win32file.OPEN_EXISTING, 0, None)
def read_mbr(drive):
h = open_drive(drive, win32file.GENERIC_READ)
mbr = win32file.ReadFile(h, 512)[1]
h.close()
return mbr
def parse_partitions(mbr):
partitions = []
for i in range(4):
offset = 446 + i * 16
part_entry = mbr[offset:offset + 16]
boot_flag, start_chs, part_type, end_chs, start_lba, size = struct.unpack('<B3sB3sII', part_entry)
if part_type == 0x0B or part_type == 0x0C: # FAT32
partitions.append({'start_lba': start_lba, 'size': size})
return partitions
def read_sector(drive, sector):
try:
h = open_drive(drive, win32file.GENERIC_READ)
win32file.SetFilePointer(h, sector * 512, win32file.FILE_BEGIN)
data = win32file.ReadFile(h, 512)[1]
h.close()
print(f"Successfully read sector {sector}")
return data
except Exception as e:
print(f"Error reading sector {sector}: {e}")
def write_directory_entry(drive, sector, entry_offset, entry_data):
try:
h = open_drive(drive, win32file.GENERIC_WRITE)
win32file.SetFilePointer(h, sector * 512 + entry_offset, win32file.FILE_BEGIN)
win32file.WriteFile(h, entry_data)
h.close()
print(f"Successfully wrote directory entry to sector {sector}")
except Exception as e:
print(f"Error writing directory entry to sector {sector}: {e}")
def write_sector(drive, sector, data):
print('data len before writing:', len(data))
try:
if len(data) != 512:
raise ValueError("Data length must be exactly 512 bytes.")
h = open_drive(drive, win32file.GENERIC_WRITE)
win32file.SetFilePointer(h, sector * 512, win32file.FILE_BEGIN)
win32file.WriteFile(h, data)
h.close()
print(f"Successfully wrote sector {sector}")
except Exception as e:
print(f"Error writing sector {sector}: {e}")
def log_error(sector, data, error):
with open("write_error_log.txt", "a") as log_file:
log_file.write(f"Error writing to sector {sector}: {error}\n")
log_file.write(f"Data: {data.hex()}\n")
log_file.write("----\n")
def read_fat32_root(drive, start_lba):
boot_sector = read_sector(drive, start_lba)
bytes_per_sector = struct.unpack_from('<H', boot_sector, 11)[0]
global sectors_per_cluster
sectors_per_cluster = struct.unpack_from('<B', boot_sector, 13)[0]
reserved_sectors = struct.unpack_from('<H', boot_sector, 14)[0]
number_of_fats = struct.unpack_from('<B', boot_sector, 16)[0]
fat_size = struct.unpack_from('<I', boot_sector, 36)[0]
root_dir_sector = start_lba + reserved_sectors + number_of_fats * fat_size
root_dir_data = bytearray()
current_cluster = 2
while True:
for i in range(sectors_per_cluster):
sector = root_dir_sector + (current_cluster - 2) * sectors_per_cluster + i
cluster_data = read_sector(drive, sector)
root_dir_data.extend(cluster_data)
next_cluster = read_next_cluster(drive, start_lba, current_cluster, reserved_sectors, fat_size)
if next_cluster is None: # Check for the end of the chain
break
current_cluster = next_cluster
return root_dir_data, root_dir_sector, bytes_per_sector, sectors_per_cluster, reserved_sectors, fat_size
def read_next_cluster(drive, start_lba, cluster, reserved_sectors, fat_size):
fat_start_sector = start_lba + reserved_sectors
fat_offset = cluster * 4
fat_sector = fat_start_sector + (fat_offset // 512)
fat_entry_offset = fat_offset % 512
fat_sector_data = read_sector(drive, fat_sector)
# Print the raw FAT sector data for debugging
print(f"Reading FAT sector: {fat_sector}, FAT entry offset: {fat_entry_offset}")
fat_entry_data = fat_sector_data[fat_entry_offset:fat_entry_offset+4]
print(f"FAT entry data (hex): {fat_entry_data.hex()}")
next_cluster = struct.unpack_from('<I', fat_entry_data)[0] & 0x0FFFFFFF
# Check if next_cluster indicates the end of the cluster chain
if next_cluster >= 0x0FFFFFF8:
print(f"Next cluster: End of chain (0x{next_cluster:X})")
return None # Indicate the end of the chain
print(f"Next cluster: {next_cluster}")
return next_cluster
def parse_directory_entries(directory_data):
entries = []
lfn_entries = []
for i in range(0, len(directory_data), 32):
entry = directory_data[i:i+32]
if entry[0] == 0x00:
break
attrs = entry[11]
if attrs == 0x0F: # LFN entry
lfn_entries.append(entry)
else:
if lfn_entries:
lfn_entries.reverse()
lfn_name = ''.join(
[lfn_entry[1:11].decode('utf-16le', errors='ignore').rstrip('\x00').rstrip() +
lfn_entry[14:26].decode('utf-16le', errors='ignore').rstrip('\x00').rstrip() +
lfn_entry[28:32].decode('utf-16le', errors='ignore').rstrip('\x00').rstrip()
for lfn_entry in lfn_entries]
).rstrip()
lfn_entries = []
else:
lfn_name = entry[0:11].decode('ascii', errors='ignore').strip()
is_deleted = entry[0] == 0xE5
file_size = struct.unpack_from('<I', entry, 28)[0]
start_cluster_lo = struct.unpack_from('<H', entry, 26)[0]
start_cluster_hi = struct.unpack_from('<H', entry, 20)[0]
start_cluster = (start_cluster_hi << 16) + start_cluster_lo
is_directory = (attrs & 0x10) != 0
display_name = f"{lfn_name} (삭제됨)" if is_deleted else lfn_name
entries.append({
'name': display_name,
'is_deleted': is_deleted,
'is_directory': is_directory,
'size': file_size,
'start_cluster': start_cluster,
'raw_entry': entry,
'entry_offset': i
})
return entries
def display_directory_structure(tree, parent, entries):
for entry in entries:
if entry['is_directory']:
node = tree.insert(parent, 'end', text=entry['name'], values=(entry['start_cluster'],), open=True)
tree.insert(node, 'end', text='dummy')
else:
tree.insert(parent, 'end', text=entry['name'], values=(entry['start_cluster'],))
def on_folder_select(event, tree, file_tree, drive, root_dir_sector, details_text, data_text, start_lba, reserved_sectors, fat_size):
selected_item = tree.selection()[0]
selected_values = tree.item(selected_item, 'values')
if not selected_values:
return
selected_value = selected_values[0]
# 논리 드라이브 경로 처리
if ':' in selected_value:
selected_path = selected_value
# 디렉토리 엔트리 파싱
entries = []
for item in os.listdir(selected_path):
item_path = os.path.join(selected_path, item)
is_directory = os.path.isdir(item_path)
if not is_directory:
entries.append({
'name': item,
'is_directory': is_directory,
'path': item_path,
'is_deleted': False,
'size': os.path.getsize(item_path) if not is_directory else '',
'start_cluster': '', # 논리 드라이브에서는 클러스터 정보를 사용하지 않음
'raw_entry': '',
'entry_offset': ''
})
# 파일 목록 업데이트
for i in file_tree.get_children():
file_tree.delete(i)
for entry in entries:
file_tree.insert('', 'end', values=(entry['name'], entry['size'], entry['start_cluster'], 'No'))
# 파일 선택 시 세부 정보 표시
def on_file_select(event):
selected_file = file_tree.selection()[0]
selected_entry = file_tree.item(selected_file, 'values')
entry_data = next(e for e in entries if e['name'] == selected_entry[0])
details_text.delete(1.0, tk.END)
details_text.insert(tk.END, f"Name: {selected_entry[0]}\n")
details_text.insert(tk.END, f"Size: {selected_entry[1]}\n")
details_text.insert(tk.END, f"Path: {entry_data['path']}\n")
data_text.delete(1.0, tk.END)
try:
with open(entry_data['path'], 'r') as f:
data_text.insert(tk.END, f.read())
except Exception as e:
data_text.insert(tk.END, f"Error reading file: {e}")
file_tree.bind('<<TreeviewSelect>>', on_file_select)
# 물리 드라이브 처리
else:
try:
selected_cluster = int(selected_value)
except ValueError:
return
directory_data = bytearray()
current_cluster = selected_cluster
while True:
for i in range(sectors_per_cluster):
sector = root_dir_sector + (current_cluster - 2) * sectors_per_cluster + i
cluster_data = read_sector(drive, sector)
directory_data.extend(cluster_data)
next_cluster = read_next_cluster(drive, start_lba, current_cluster, reserved_sectors, fat_size)
if next_cluster is None: # Check for the end of the chain
break
current_cluster = next_cluster
entries = parse_directory_entries(directory_data)
for i in file_tree.get_children():
file_tree.delete(i)
for entry in entries:
file_tree.insert('', 'end', values=(entry['name'], entry['size'], entry['start_cluster'], 'Yes' if entry['is_deleted'] else 'No'))
def on_file_select(event):
selected_file = file_tree.selection()[0]
selected_entry = file_tree.item(selected_file, 'values')
entry_data = next(e for e in entries if e['name'] == selected_entry[0])
entry_sector = root_dir_sector + (entry_data['start_cluster'] - 2) * sectors_per_cluster
entry_offset = entry_data['entry_offset']
details_text.delete(1.0, tk.END)
details_text.insert(tk.END, f"Name: {selected_entry[0]}\n")
details_text.insert(tk.END, f"Size: {selected_entry[1]}\n")
details_text.insert(tk.END, f"Start Cluster: {selected_entry[2]}\n")
details_text.insert(tk.END, f"Deleted: {selected_entry[3]}\n")
details_text.insert(tk.END, f"Raw Entry: {entry_data['raw_entry']}\n")
details_text.insert(tk.END, f"Sector Number: {entry_sector}\n")
details_text.insert(tk.END, f"Entry Offset: {entry_offset}\n")
data_text.delete(1.0, tk.END)
if entry_data['is_directory']:
directory_data = bytearray()
current_cluster = entry_data['start_cluster']
while True:
for i in range(sectors_per_cluster):
sector = root_dir_sector + (current_cluster - 2) * sectors_per_cluster + i
cluster_data = read_sector(drive, sector)
directory_data.extend(cluster_data)
next_cluster = read_next_cluster(drive, start_lba, current_cluster, reserved_sectors, fat_size)
if next_cluster is None: # Check for the end of the chain
break
current_cluster = next_cluster
data_text.insert(tk.END, directory_data)
else:
sector_data = read_sector(drive, entry_sector)
data_text.insert(tk.END, sector_data)
file_tree.bind('<<TreeviewSelect>>', on_file_select)
def create_logical_file(tree, logical_drive):
selected_item = tree.selection()[0]
selected_path = tree.item(selected_item, 'values')[0]
file_name = simpledialog.askstring("File Name", "Enter the new file name:")
if file_name:
file_path = os.path.join(selected_path, file_name)
def save_file():
content = text.get(1.0, tk.END)
try:
with open(file_path, 'w') as f:
f.write(content)
print(f"File {file_name} created successfully at {selected_path}")
display_logical_directory(tree, logical_drive)
new_window.destroy()
except Exception as e:
messagebox.showerror("Error", f"Failed to create file: {e}")
new_window = tk.Toplevel()
new_window.title("Edit File Content")
text = tk.Text(new_window, width=80, height=20)
text.pack()
save_button = tk.Button(new_window, text="Save", command=save_file)
save_button.pack()
def create_logical_folder(tree, logical_drive):
selected_item = tree.selection()[0]
selected_path = tree.item(selected_item, 'values')[0]
folder_name = simpledialog.askstring("Folder Name", "Enter the new folder name:")
if folder_name:
folder_path = os.path.join(selected_path, folder_name)
try:
os.makedirs(folder_path)
print(f"Folder {folder_name} created successfully at {selected_path}")
display_logical_directory(tree, logical_drive)
except Exception as e:
messagebox.showerror("Error", f"Failed to create folder: {e}")
def delete_logical_file(tree, logical_drive):
selected_item = tree.selection()[0]
selected_path = tree.item(selected_item, 'values')[0]
selected_file_item = logical_file_tree.selection()
if selected_file_item:
selected_file = logical_file_tree.item(selected_file_item[0], 'values')[0]
file_path = os.path.join(selected_path, selected_file)
try:
os.remove(file_path)
print(f"File {file_path} deleted successfully.")
display_logical_directory(tree, logical_drive)
except Exception as e:
messagebox.showerror("Error", f"Failed to delete file: {e}")
else:
if os.path.isdir(selected_path):
if os.listdir(selected_path):
messagebox.showerror("Error", "Cannot delete a non-empty directory.")
else:
try:
os.rmdir(selected_path)
print(f"Folder {selected_path} deleted successfully.")
display_logical_directory(tree, logical_drive)
except Exception as e:
messagebox.showerror("Error", f"Failed to delete folder: {e}")
def edit_logical_file(tree, logical_drive):
selected_item = tree.selection()[0]
selected_path = tree.item(selected_item, 'values')[0]
selected_file_item = logical_file_tree.selection()
if not selected_file_item:
messagebox.showerror("Error", "No file selected to edit.")
return
selected_file = logical_file_tree.item(selected_file_item[0], 'values')[0]
file_path = os.path.join(selected_path, selected_file)
if not os.path.isdir(file_path):
def save_changes():
content = text.get(1.0, tk.END)
try:
with open(file_path, 'w') as f:
f.write(content)
print(f"File {file_path} edited successfully.")
display_logical_directory(tree, logical_drive)
edit_window.destroy()
except Exception as e:
messagebox.showerror("Error", f"Failed to edit file: {e}")
edit_window = tk.Toplevel()
edit_window.title("Edit File")
text = tk.Text(edit_window, width=80, height=20)
text.pack()
save_button = tk.Button(edit_window, text="Save", command=save_changes)
save_button.pack()
try:
with open(file_path, 'r') as f:
text.insert(tk.END, f.read())
except Exception as e:
messagebox.showerror("Error", f"Failed to read file: {e}")
def list_logical_drives():
drives = [f"{chr(d)}:\\" for d in range(65, 91) if os.path.exists(f"{chr(d)}:\\")]
drives = [drive for drive in drives if drive != "C:\\"] # C 드라이브 제외
return drives
def display_logical_directory(tree, path):
for i in tree.get_children():
tree.delete(i)
node = tree.insert('', 'end', text=path, values=(path,), open=True)
_display_logical_subdirectory(tree, node, path)
def _display_logical_subdirectory(tree, parent, path):
try:
for item in os.listdir(path):
abs_path = os.path.join(path, item)
if os.path.isdir(abs_path):
node = tree.insert(parent, 'end', text=item, values=(abs_path,))
_display_logical_subdirectory(tree, node, abs_path)
except Exception as e:
messagebox.showerror("Error", f"Failed to display directory: {e}")
def on_logical_tree_open(event, tree):
item = tree.selection()[0]
path = tree.item(item, 'values')[0]
children = tree.get_children(item)
if len(children) == 1 and tree.item(children[0], 'text') == 'dummy':
tree.delete(children[0])
_display_logical_subdirectory(tree, item, path)
def on_physical_tree_open(event, tree, file_tree, drive, root_dir_sector, details_text, data_text, start_lba, reserved_sectors, fat_size):
selected_item = tree.selection()[0]
selected_values = tree.item(selected_item, 'values')
if not selected_values:
return
selected_cluster = int(selected_values[0])
directory_data = bytearray()
current_cluster = selected_cluster
while True:
for i in range(sectors_per_cluster):
sector = root_dir_sector + (current_cluster - 2) * sectors_per_cluster + i
cluster_data = read_sector(drive, sector)
directory_data.extend(cluster_data)
next_cluster = read_next_cluster(drive, start_lba, current_cluster, reserved_sectors, fat_size)
if next_cluster is None: # Check for the end of the chain
break
current_cluster = next_cluster
entries = parse_directory_entries(directory_data)
for i in tree.get_children(selected_item):
tree.delete(i)
display_directory_structure(tree, selected_item, entries)
update_file_list(entries, file_tree, details_text, data_text)
def update_file_list(entries, file_tree, details_text, data_text):
for i in file_tree.get_children():
file_tree.delete(i)
for entry in entries:
file_tree.insert('', 'end', values=(entry['name'], entry['size'], entry['start_cluster'], 'Yes' if entry['is_deleted'] else 'No'))
def on_file_select(event):
selected_file = file_tree.selection()[0]
selected_entry = file_tree.item(selected_file, 'values')
entry_data = next(e for e in entries if e['name'] == selected_entry[0])
details_text.delete(1.0, tk.END)
details_text.insert(tk.END, f"Name: {selected_entry[0]}\n")
details_text.insert(tk.END, f"Size: {selected_entry[1]}\n")
details_text.insert(tk.END, f"Start Cluster: {selected_entry[2]}\n")
details_text.insert(tk.END, f"Deleted: {selected_entry[3]}\n")
details_text.insert(tk.END, f"Raw Entry: {entry_data['raw_entry']}\n")
data_text.delete(1.0, tk.END)
if entry_data['is_directory']:
data_text.insert(tk.END, "[Directory content]")
else:
data_text.insert(tk.END, "[File content]")
file_tree.bind('<<TreeviewSelect>>', on_file_select)
def on_logical_folder_select(event, tree, logical_file_tree, logical_details_text, logical_data_text):
selected_item = tree.selection()[0]
selected_path = tree.item(selected_item, 'values')[0]
entries = []
for item in os.listdir(selected_path):
item_path = os.path.join(selected_path, item)
is_directory = os.path.isdir(item_path)
entries.append({
'name': item,
'is_directory': is_directory,
'path': item_path,
'is_deleted': False,
'size': os.path.getsize(item_path) if not is_directory else '',
'start_cluster': '', # 논리 드라이브에서는 클러스터 정보를 사용하지 않음
'raw_entry': '',
'entry_offset': ''
})
for i in logical_file_tree.get_children():
logical_file_tree.delete(i)
for entry in entries:
logical_file_tree.insert('', 'end', values=(entry['name'], entry['size'], entry['start_cluster'], 'No'))
def on_logical_file_select(event):
selected_file = logical_file_tree.selection()[0]
selected_entry = logical_file_tree.item(selected_file, 'values')
entry_data = next(e for e in entries if e['name'] == selected_entry[0])
logical_details_text.delete(1.0, tk.END)
logical_details_text.insert(tk.END, f"Name: {selected_entry[0]}\n")
logical_details_text.insert(tk.END, f"Size: {selected_entry[1]}\n")
logical_details_text.insert(tk.END, f"Path: {entry_data['path']}\n")
logical_data_text.delete(1.0, tk.END)
try:
with open(entry_data['path'], 'r') as f:
logical_data_text.insert(tk.END, f.read())
except Exception as e:
logical_data_text.insert(tk.END, f"Error reading file: {e}")
logical_file_tree.bind('<<TreeviewSelect>>', on_logical_file_select)
def main():
global selected_drive, root_dir_sector, sectors_per_cluster, start_lba, reserved_sectors, fat_size, file_tree, logical_file_tree, selected_drive_label
drives = list_physical_drives()
if not drives:
print("No physical drives found.")
messagebox.showerror("Error", "No physical drives found.")
return
logical_drives = list_logical_drives()
if not logical_drives:
print("No logical drives found.")
messagebox.showerror("Error", "No logical drives found.")
return
print("Available drives:", drives)
root = tk.Tk()
root.title("FAT32 Explorer")
root.geometry("1100x800") # 전체 창 크기를 1100x800으로 설정
# Top frame for drive selections
top_frame = tk.Frame(root)
top_frame.pack(side=tk.TOP, fill=tk.X)
# Physical drive selection frame (left half)
physical_drive_frame = tk.Frame(top_frame, width=550)
physical_drive_frame.pack(side=tk.LEFT, fill=tk.X)
lbl = tk.Label(physical_drive_frame, text="물리디스크 선택:")
lbl.pack(side=tk.LEFT, padx=5, pady=5)
drive_var = tk.StringVar(physical_drive_frame)
drive_info = [f"{drive} - {model}" for drive, model in drives]
drive_var.set(drive_info[0]) # Default value
drive_menu = tk.OptionMenu(physical_drive_frame, drive_var, *drive_info)
drive_menu.pack(side=tk.LEFT, padx=5, pady=5)
btn = tk.Button(physical_drive_frame, text="물리드라이브 선택", command=lambda: on_select())
btn.pack(side=tk.LEFT, padx=5, pady=5)
# Logical drive selection frame (right half)
logical_drive_frame = tk.Frame(top_frame, width=550)
logical_drive_frame.pack(side=tk.RIGHT, fill=tk.X)
logical_drive_lbl = tk.Label(logical_drive_frame, text="논리드라이브 선택:")
logical_drive_lbl.pack(side=tk.LEFT, padx=5, pady=5)
logical_drive_var = tk.StringVar(logical_drive_frame)
logical_drive_var.set(logical_drives[0]) # Default value
logical_drive_menu = tk.OptionMenu(logical_drive_frame, logical_drive_var, *logical_drives)
logical_drive_menu.pack(side=tk.LEFT, padx=5, pady=5)
logical_drive_btn = tk.Button(logical_drive_frame, text="논리드라이브 선택", command=lambda: display_logical_directory(logical_tree, logical_drive_var.get()))
logical_drive_btn.pack(side=tk.LEFT, padx=5, pady=5)
# Splitter frame
splitter_frame = tk.PanedWindow(root, orient=tk.HORIZONTAL)
splitter_frame.pack(fill=tk.BOTH, expand=True)
frame_width = int(1100 / 4) # 4등분한 각 프레임의 너비
# Left tree view (physical drive structure)
physical_frame = tk.Frame(splitter_frame, width=frame_width)
splitter_frame.add(physical_frame, stretch="always")
physical_tree = ttk.Treeview(physical_frame, columns=('Start Cluster'), show='tree')
physical_tree.pack(fill=tk.BOTH, expand=True)
# Center file list and details frame
center_frame = tk.Frame(splitter_frame, width=frame_width)
splitter_frame.add(center_frame, stretch="always")
# File list tree view
file_tree = ttk.Treeview(center_frame, columns=('Name', 'Size', 'Start Cluster', 'Deleted'), show='headings')
file_tree.heading('Name', text='Name')
file_tree.heading('Size', text='Size')
file_tree.heading('Start Cluster', text='Start Cluster')
file_tree.heading('Deleted', text='Deleted')
file_tree.pack(fill=tk.BOTH, expand=True)
# File details and data frames
details_frame = tk.Frame(center_frame, width=frame_width, height=300)
details_frame.pack(fill=tk.BOTH, expand=True)
details_text = tk.Text(details_frame, height=10)
details_text.pack(fill=tk.BOTH, expand=True)
data_text = tk.Text(details_frame, height=10)
data_text.pack(fill=tk.BOTH, expand=True)
# Right frame (logical drive structure)
logical_frame = tk.Frame(splitter_frame, width=frame_width)
splitter_frame.add(logical_frame, stretch="always")
logical_tree = ttk.Treeview(logical_frame, columns=('Start Cluster'), show='tree')
logical_tree.pack(fill=tk.BOTH, expand=True)
# Far right frame (logical file details)
far_right_frame = tk.Frame(splitter_frame, width=frame_width)
splitter_frame.add(far_right_frame, stretch="always")
# Logical file list tree view
logical_file_tree = ttk.Treeview(far_right_frame, columns=('Name', 'Size', 'Start Cluster', 'Deleted'), show='headings')
logical_file_tree.heading('Name', text='Name')
logical_file_tree.heading('Size', text='Size')
logical_file_tree.heading('Start Cluster', text='Start Cluster')
logical_file_tree.heading('Deleted', text='Deleted')
logical_file_tree.pack(fill=tk.BOTH, expand=True)
# Logical file details and data frames
logical_details_frame = tk.Frame(far_right_frame, height=300)
logical_details_frame.pack(fill=tk.BOTH, expand=True)
logical_details_text = tk.Text(logical_details_frame, height=10)
logical_details_text.pack(fill=tk.BOTH, expand=True)
logical_data_text = tk.Text(logical_details_frame, height=10)
logical_data_text.pack(fill=tk.BOTH, expand=True)
# Bottom frame for physical and logical drive info
bottom_frame = tk.Frame(root)
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X)
# Left side of bottom frame (for physical drive info)
physical_drive_info_frame = tk.Frame(bottom_frame, width=550)
physical_drive_info_frame.pack(side=tk.LEFT, fill=tk.X)
selected_drive_label = tk.Label(physical_drive_info_frame, text="선택한 드라이브: None")
selected_drive_label.pack(side=tk.LEFT, padx=5, pady=5)
# Right side of bottom frame (for logical drive file operations)
logical_button_frame = tk.Frame(bottom_frame, width=550)
logical_button_frame.pack(side=tk.RIGHT, fill=tk.X)
# 논리 드라이브 폴더 및 파일 생성 버튼 추가
add_logical_folder_btn = tk.Button(logical_button_frame, text="폴더 추가(논리)", command=lambda: create_logical_folder(logical_tree, logical_drive_var.get()))
add_logical_folder_btn.pack(side=tk.LEFT, padx=5, pady=5)
add_logical_file_btn = tk.Button(logical_button_frame, text="파일 추가(논리)", command=lambda: create_logical_file(logical_tree, logical_drive_var.get()))
add_logical_file_btn.pack(side=tk.LEFT, padx=5, pady=5)
delete_logical_file_btn = tk.Button(logical_button_frame, text="파일 삭제", command=lambda: delete_logical_file(logical_tree, logical_drive_var.get()))
delete_logical_file_btn.pack(side=tk.LEFT, padx=5, pady=5)
edit_logical_file_btn = tk.Button(logical_button_frame, text="수정", command=lambda: edit_logical_file(logical_tree, logical_drive_var.get()))
edit_logical_file_btn.pack(side=tk.LEFT, padx=5, pady=5)
def on_select():
global selected_drive, root_dir_sector, sectors_per_cluster, start_lba, reserved_sectors, fat_size
selected_drive_info = drive_var.get()
selected_drive = selected_drive_info.split(' - ')[0]
print(f"선택한 드라이브: {selected_drive}")
mbr = read_mbr(selected_drive)
partitions = parse_partitions(mbr)
print(f"파티션: {partitions}")
if not partitions:
print("FAT32가 없습니다.")
messagebox.showerror("Error", "FAT32가 없습니다.")
return
#analysis_text = f"Selected Drive: {selected_drive_info}\n"
#analysis_text += f"Partitions found: {partitions}\n"
#analysis_label.config(text=analysis_text)
selected_drive_label.config(text=f"선택한 물리 디스크: {selected_drive_info}")
root_dir, root_dir_sector, bytes_per_sector, sectors_per_cluster, reserved_sectors, fat_size = read_fat32_root(selected_drive, partitions[0]['start_lba'])
start_lba = partitions[0]['start_lba']
entries = parse_directory_entries(root_dir)
for i in physical_tree.get_children():
physical_tree.delete(i)
root_node = physical_tree.insert('', 'end', text="Root Directory", values=(2,), open=True)
display_directory_structure(physical_tree, root_node, entries)
physical_tree.bind('<<TreeviewOpen>>', lambda event: on_physical_tree_open(event, physical_tree, file_tree, selected_drive, root_dir_sector, details_text, data_text, start_lba, reserved_sectors, fat_size))
physical_tree.bind('<<TreeviewSelect>>', lambda event: on_folder_select(event, physical_tree, file_tree, selected_drive, root_dir_sector, details_text, data_text, start_lba, reserved_sectors, fat_size))
logical_tree.bind('<<TreeviewOpen>>', lambda event: on_logical_tree_open(event, logical_tree))
logical_tree.bind('<<TreeviewSelect>>', lambda event: on_logical_folder_select(event, logical_tree, logical_file_tree, logical_details_text, logical_data_text))
root.mainloop()
if __name__ == "__main__":
main()
파티션에 FAT32 파일시스템이 있는 물리디스크 선택 시 좌측에 디렉토리 엔트리를 분석 후 보여줍니다. (삭제여부 등 포함) 반면 논리드라이브로 FAT32 인 것을 선택 후 논리드라이브 선택 시 논리방식으로 운영체제의 힘을 빌려 해당 드라이브를 분석하여 보여주고, 폴더 및 파일 수정, 삭제 등이 가능합니다. 참고로 이런식으로 활용이 가능하는구나.. 이렇게 복구 할 수 있구나.. 생각해볼 수 있습니다. 아래 도구는 파티션이 없는 경우, 파일(txt)내용에 한글은 제대로 지원하지 못하니 필요 시 수정하여 활용할 수 있을 것입니다.(필요하다면 추후에 업데이트 할 예정)
4-2) 프로그램 제작 의도와 기타 사항..
물리드라이브에서 디렉토리 엔트리를 분석하고 해당 디레토리 엔트리가 있는 클러스터를 찾은 뒤 비어 있는 영역에 디렉토리 엔트리를 직접 넣고 싶었으나, 윈도우 등에서 프로세스 사용 중 또는 권한문제 등으로 물리적으로 접근하여 섹터를 수정하는 것을 막고 있어 물리 분석 쪽에서 파일 추가, 수정 등을 할 수 없었습니다.(그렇다면 HxD는 어떻게 물리디스크는 hex 값을 수정하는 거지..?)
반면 논리적인 접근으로는 매우 쉽게 파일 구조 분석 및 파일 생성 수정이 가능합니다. 단순히 우리가 CMD 창에서 명령어 치는 것이 그대로 적용이 되기 때문이지요.
파일시스템 관점에서 우리가 직접 디렉토리 엔트리, FAT 영역, 데이터 영역에 데이터를 입력 할 수 있는 소스로 만들고 싶었으나 운영체제에서 이를 쉽게 허락하지 않습니다. 사실 이게 매우 쉽게 된다면.. 보안적 측면에서 매우 위험할 수 있습니다. 조금만 마음 먹고 파일시스템의 중요 부분에 물리적인 섹터 단위로 접근하여 덮어 써버리면 해당 파일시스템을 복구하기 전까지는 어떤 파일에도 접근이 제대로 되지 않는 위험한 경우가 발생 할 수 있기 때문입니다.
우리가 파일을 보고, 복사해서 다른 곳으로 이동하고, 폴더를 만들고 하는 부분이 이 파일시스템에 섹터 영역에 파일시스템 구조에 맞게 적절한 방법으로 쓰고하는 부분을 윈도우와 같은 운영체제에서 해주고 있는 것입니다.
따라서 포렌식 관점에서 생각해보면, 윈도우 버전마다 특정 파일 시스템에 대해서 어떤 행위를 하였을 때 어떤 변화를 하는지 파악하거나 분석할 수 있어야 할 것이며, 이를 통해 파일 관련 흔적, 파일 내용 복구 등이 가능하게 됩니다.(FAT32에서는 디렉토리 엔트리만 열심히 봐도 많은 것을 알 수 있습니다.)
파일 복구는 이미 위에서 삭제된 영역에 접근해서 추출하면 얼마든지 복구 가능하기 때문에 파일시스템 구조만 잘 알고 있으면, 파일 복구 기법을 생각해내고 도구로 만들어서 활용할 수 있는 것 입니다!
3줄 요약
처음 파일시스템 분석인데 뭔가 계산할게 많다.. 그러나 다 도구가 해주기 때문에 이번 한번정도 제대로 분석해봅시다.
FAT32 파일시스템은 비교적 단순한 파일시스템입니다. 이를 통해 FAT32 탐색기도 어떻게 만들 수 있는지 알게 되었을 것입니다.
파일시스템 구조를 파악했으니, 충분히 복구할 때 어떻게 접근해야 하는지 혹은 어떻게 접근해볼 수 있는지 아이디어가 생길 것입니다. 대부분의 도구는 여러분들이 생각한 아이디어와 유사한 방식으로 만들어서 복구를 해주고 있을것입니다. 물론 오 이런방법으로?? 하는 경우도 있겠지요. 우리가 아는 파일시스템에서 새로운 복구기법을 제시한 논문을 보면 우리는 해당 논문을 더자세히이해하고 나아가서 논문이 제시한 방법을 실제 구현도 가능할 것입니다. 또한 복구가 안되 될 경우 분석을 통해 왜 복구 안되는지도 충분히 설명할 수 있을 것입니다. (FAT Entry가 초기화 되있어!, 데이터 영역이 덮어 써져있어! 등등..)
Last updated