3-3) 파티션 분석 도구 만들기(기초)
Last updated
Last updated
Python 을 이용한 소스 코드 작성 및 실행 방법
우리가 알고 있는 것
0번 섹터에서 파티션 테이블에서, MBR 파티션 테이블, GPT 파티션 테이블을 확인 가능
파티션테이블은 MBR에서 확인 가능하며, GPT 파티션테이블의 경우 GPT 헤더 이 후 파티션테이블을 저장하는 공간이 따로 있다.
파티션 분석 주요 요구사항
물리 드라이브를 리스트를 표시하고 선택한 물리디스크의 파티션을 분석한다.
MBR을 분석하여 GPT 파티션테이블을 포함하고 있는지 여부를 확인하고 분석 한다.
파티션테이블을 분석하고, 파티션별 시작 섹터, 마지막섹터, 총 섹터 수, 파티션별 용량을 GUI로 표시하자.
우선 업로드한 실행파일을 받아서 사용할 경우 제가 공유한 프로그램이 정확한지 확인 후 사용
먼저 제가 ChatGPT와 같이 작성한 파이썬 소스를 이용하여 물리이미징 획득 도구를 제작하였고, 공유하니 참고로 활용하도록 합시다.
import ctypes
import subprocess
import logging
import tkinter as tk
from tkinter import messagebox, ttk
# Windows 상수 정의
GENERIC_READ = 0x80000000
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
OPEN_EXISTING = 3
IOCTL_DISK_GET_DRIVE_GEOMETRY = 0x00070000
IOCTL_DISK_GET_LENGTH_INFO = 0x0007405C
# ctypes.wintypes 불러오기
from ctypes import wintypes
# DISK_GEOMETRY 구조체
class DISK_GEOMETRY(ctypes.Structure):
_fields_ = [
("Cylinders", wintypes.LARGE_INTEGER),
("MediaType", wintypes.DWORD),
("TracksPerCylinder", wintypes.DWORD),
("SectorsPerTrack", wintypes.DWORD),
("BytesPerSector", wintypes.DWORD)
]
# GET_LENGTH_INFORMATION 구조체
class GET_LENGTH_INFORMATION(ctypes.Structure):
_fields_ = [("Length", wintypes.LARGE_INTEGER)]
# MBR 파티션 항목 구조체
class PARTITION_ENTRY(ctypes.Structure):
_fields_ = [
("boot_flag", ctypes.c_ubyte),
("start_chs", ctypes.c_ubyte * 3),
("partition_type", ctypes.c_ubyte),
("end_chs", ctypes.c_ubyte * 3),
("start_sector", ctypes.c_uint32),
("num_sectors", ctypes.c_uint32)
]
# MBR 구조체
class MBR(ctypes.Structure):
_fields_ = [
("boot_code", ctypes.c_ubyte * 446),
("partitions", PARTITION_ENTRY * 4),
("signature", ctypes.c_ushort)
]
_pack_ = 1 # 패딩을 사용하지 않도록 설정
# GPT 헤더 구조체
class GPT_HEADER(ctypes.Structure):
_fields_ = [
("signature", ctypes.c_char * 8),
("revision", ctypes.c_uint32),
("header_size", ctypes.c_uint32),
("header_crc32", ctypes.c_uint32),
("reserved", ctypes.c_uint32),
("current_lba", ctypes.c_uint64),
("backup_lba", ctypes.c_uint64),
("first_usable_lba", ctypes.c_uint64),
("last_usable_lba", ctypes.c_uint64),
("disk_guid", ctypes.c_byte * 16),
("partition_entry_lba", ctypes.c_uint64),
("num_partition_entries", ctypes.c_uint32),
("sizeof_partition_entry", ctypes.c_uint32),
("partition_entry_array_crc32", ctypes.c_uint32)
]
# GPT 파티션 엔트리 구조체
class GPT_PARTITION_ENTRY(ctypes.Structure):
_fields_ = [
("partition_type_guid", ctypes.c_byte * 16),
("unique_partition_guid", ctypes.c_byte * 16),
("starting_lba", ctypes.c_uint64),
("ending_lba", ctypes.c_uint64),
("attributes", ctypes.c_uint64),
("partition_name", ctypes.c_wchar * 36)
]
# OVERLAPPED 구조체 정의
class OVERLAPPED(ctypes.Structure):
_fields_ = [
("Internal", ctypes.c_ulong),
("InternalHigh", ctypes.c_ulong),
("Offset", ctypes.c_uint32),
("OffsetHigh", ctypes.c_uint32),
("hEvent", wintypes.HANDLE)
]
# Kernel32 DLL 로드
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
# CreateFile 함수 정의
def create_file(filename, access, mode, creation, flags):
handle = kernel32.CreateFileW(
ctypes.c_wchar_p(filename),
ctypes.c_ulong(access),
ctypes.c_ulong(mode),
None,
ctypes.c_ulong(creation),
ctypes.c_ulong(flags),
None
)
if handle == wintypes.HANDLE(-1).value:
error_code = ctypes.get_last_error()
logging.error(f"Failed to open {filename}: {ctypes.WinError(error_code)}")
raise ctypes.WinError(error_code)
logging.info(f"Successfully opened {filename} with handle {handle}")
return handle
# DeviceIoControl 함수 정의
def device_io_control(device, io_control_code, in_buffer, out_buffer):
bytes_returned = wintypes.DWORD(0)
status = kernel32.DeviceIoControl(
wintypes.HANDLE(device),
wintypes.DWORD(io_control_code),
None,
0,
ctypes.byref(out_buffer),
wintypes.DWORD(ctypes.sizeof(out_buffer)),
ctypes.byref(bytes_returned),
None
)
return status
# ReadFile 함수 정의
def read_file(handle, num_bytes, offset=0):
logging.info(f"Reading {num_bytes} bytes from handle {handle} at offset {offset}")
if handle == wintypes.HANDLE(-1).value:
logging.error(f"Invalid handle: {handle}")
raise ValueError("Invalid handle")
data = ctypes.create_string_buffer(num_bytes)
bytes_read = wintypes.DWORD(0)
overlapped = OVERLAPPED()
overlapped.Offset = offset & 0xFFFFFFFF
overlapped.OffsetHigh = offset >> 32
success = kernel32.ReadFile(
wintypes.HANDLE(handle),
data,
num_bytes,
ctypes.byref(bytes_read),
None # 동기적 호출에서는 overlapped 매개변수를 None으로 설정
)
logging.info(f"Success: {success}")
if not success:
error_code = ctypes.get_last_error()
logging.error(f"Failed to read file: {ctypes.WinError(error_code)}")
raise ctypes.WinError(error_code)
logging.info(f"Successfully read {num_bytes} bytes from file")
return data.raw
def analyze_partition(disk_number):
drive_path = f"\\\\.\\PhysicalDrive{disk_number}"
try:
h_drive = create_file(drive_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, 0)
except Exception as e:
logging.error(f"Failed to open handle for {drive_path}: {e}")
return []
try:
# MBR을 읽음
logging.info(f"Reading MBR from {drive_path}")
mbr_data = read_file(h_drive, 512)
mbr = MBR.from_buffer_copy(mbr_data)
# MBR signature 확인
if mbr.signature != 0xAA55:
raise ValueError("Invalid MBR signature")
# GPT 확인
logging.info(f"Reading GPT header from {drive_path}")
gpt_header_data = read_file(h_drive, 512, 512)
gpt_header = GPT_HEADER.from_buffer_copy(gpt_header_data)
if gpt_header.signature != b'EFI PART':
# MBR 파티션 분석
partitions = []
for partition in mbr.partitions:
if partition.partition_type != 0:
start_sector = partition.start_sector
num_sectors = partition.num_sectors
end_sector = start_sector + num_sectors - 1
size = num_sectors * 512
size_gb = size / (1024 ** 3)
size_display = f"{size_gb:.2f} GB" if size_gb >= 1 else f"{size / (1024 ** 2):.2f} MB"
partitions.append({
"Start Sector": start_sector,
"End Sector": end_sector,
"Total Sectors": num_sectors,
"Size": size_display
})
return partitions
# GPT 파티션 분석
partitions = []
partition_entries_data = read_file(h_drive, gpt_header.num_partition_entries * gpt_header.sizeof_partition_entry, gpt_header.partition_entry_lba * 512)
for i in range(gpt_header.num_partition_entries):
offset = i * gpt_header.sizeof_partition_entry
partition_entry = GPT_PARTITION_ENTRY.from_buffer_copy(partition_entries_data[offset:offset + gpt_header.sizeof_partition_entry])
if partition_entry.starting_lba != 0:
start_sector = partition_entry.starting_lba
end_sector = partition_entry.ending_lba
num_sectors = end_sector - start_sector + 1
size = num_sectors * 512
size_gb = size / (1024 ** 3)
size_display = f"{size_gb:.2f} GB" if size_gb >= 1 else f"{size / (1024 ** 2):.2f} MB"
partitions.append({
"Start Sector": start_sector,
"End Sector": end_sector,
"Total Sectors": num_sectors,
"Size": size_display
})
return partitions
finally:
kernel32.CloseHandle(wintypes.HANDLE(h_drive))
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
# 물리 드라이브 정보를 가져오는 함수
def get_physical_drives():
logging.info("Fetching physical drives")
try:
result = subprocess.check_output("wmic diskdrive get DeviceID, Model, Size", shell=True).decode('cp949').strip()
logging.debug(f"Raw wmic output: {result}")
lines = result.split('\n')[1:] # 첫 번째 줄은 헤더이므로 제외
drives_info = []
for line in lines:
parts = line.strip().split()
if len(parts) < 3:
continue
drive = parts[0]
model = ' '.join(parts[1:-1])
try:
# 마지막 섹터 번호를 기반으로 크기 계산
disk_number = int(drive[-1])
last_sector_number = get_last_sector_number(disk_number)
bytes_per_sector = 512 # 일반적으로 섹터 크기는 512 바이트입니다.
total_size = (last_sector_number + 1) * bytes_per_sector
# Size 변환 및 표시 단위 결정
size_in_gb = total_size / (1024 ** 3) # bytes to gigabytes
if size_in_gb < 1:
size_in_mb = size_in_gb * 1024 # Convert GB to MB
display_size = f"{size_in_mb:.2f} MB"
else:
display_size = f"{size_in_gb:.2f} GB"
except Exception as e:
logging.error(f"Failed to get size for {drive}: {e}")
display_size = "Unknown"
drives_info.append((drive, model, display_size))
logging.debug(f"Drive: {drive}, Model: {model}, Size: {display_size}")
return drives_info
except subprocess.CalledProcessError as e:
messagebox.showerror("오류", "물리 드라이브를 조회하는 중 오류가 발생했습니다.")
logging.error("Failed to get physical drives: %s", str(e))
return []
except Exception as e:
messagebox.showerror("오류", "예상치 못한 오류가 발생했습니다.")
logging.error("Unexpected error: %s", str(e))
return []
# 마지막 섹터 번호를 계산하는 함수
def get_last_sector_number(disk_number):
drive_path = f"\\\\.\\PhysicalDrive{disk_number}"
try:
h_drive = create_file(drive_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, 0)
except Exception as e:
logging.error(f"Failed to open handle for {drive_path}: {e}")
return -1
try:
length_info = GET_LENGTH_INFORMATION()
if not device_io_control(h_drive, IOCTL_DISK_GET_LENGTH_INFO, None, length_info):
raise ctypes.WinError(ctypes.get_last_error())
disk_geometry = DISK_GEOMETRY()
if not device_io_control(h_drive, IOCTL_DISK_GET_DRIVE_GEOMETRY, None, disk_geometry):
raise ctypes.WinError(ctypes.get_last_error())
total_length = length_info.Length
bytes_per_sector = disk_geometry.BytesPerSector
last_sector_number = (total_length // bytes_per_sector) - 1
return last_sector_number
finally:
kernel32.CloseHandle(wintypes.HANDLE(h_drive))
class DiskAnalyzerApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("Disk Analyzer")
self.geometry("600x400")
self.create_widgets()
def create_widgets(self):
self.drive_listbox = tk.Listbox(self)
self.drive_listbox.pack(fill=tk.BOTH, expand=True)
self.analyze_button = tk.Button(self, text="Analyze Disk", command=self.analyze_selected_disk)
self.analyze_button.pack()
self.partition_info_text = tk.Text(self)
self.partition_info_text.pack(fill=tk.BOTH, expand=True)
self.load_drives()
def load_drives(self):
drives = get_physical_drives()
for drive in drives:
self.drive_listbox.insert(tk.END, f"{drive[0]} - {drive[1]} - {drive[2]}")
def analyze_selected_disk(self):
selection = self.drive_listbox.curselection()
if not selection:
messagebox.showwarning("경고", "분석할 디스크를 선택하세요.")
return
drive = self.drive_listbox.get(selection[0]).split()[0]
disk_number = int(drive[-1])
partitions = analyze_partition(disk_number)
self.partition_info_text.delete(1.0, tk.END)
for partition in partitions:
self.partition_info_text.insert(tk.END, f"Start Sector: {partition['Start Sector']}\n")
self.partition_info_text.insert(tk.END, f"End Sector: {partition['End Sector']}\n")
self.partition_info_text.insert(tk.END, f"Total Sectors: {partition['Total Sectors']}\n")
self.partition_info_text.insert(tk.END, f"Size: {partition['Size']}\n")
self.partition_info_text.insert(tk.END, "\n")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
if not is_admin():
messagebox.showerror("오류", "이 프로그램을 실행하려면 관리자 권한이 필요합니다.")
else:
app = DiskAnalyzerApp()
app.mainloop()
위 소스로 실행파일 형태로 만들어 보았습니다. 아래 프로그램 다운로드 후 해당 파일의 해시값(md5, sha256)을 비교해보고 정확하지 않으면 어떤 소스인지 검증할 수 없으니 꼭 잘 알아보고 사용하도록 하자. 아래 실습 화면을 이어서 참고
다운로드 파일 정상 파일인지 확인하기!
먼저 다운로드한 프로그램의 압축해제 후 "물리이미징 도구.exe"가 있는 폴더에 빈공간에 [Shift] 키를 누른 상태에서 우클릭 후 [여기에 PowerShell 창 열기] 를 클릭
아래 명령어를 입력하여 hash값을 확인하여 봅시다. > get-filehash "Partition 분석.exe" -algorithm md5 / 띄워쓰기가 있는 경우 " ~~ " 를 사용하자. > get-filehash "Partition 분석.exe"
아래의 해시값이 맞는지 확인해봅시다. 아래 해시값이 아니면, 제대로 된 파일이 아니라는 뜻임을 잘 알 수 있습니다. md5 : D70636A3FD5164086601D44E5BD0DD25 sha256 : 3C9DD390BA905F1D10908F27FE95920BF32678CEBC338C81DFEBB61B9FAFD211
사용법
프로그램을 실행하면, 관리자 권한이 아닌 경우 아래와 같이 표시됩니다.
프로그램 우클릭 후 [관리자 권한으로 실행]해봅시다. 사용법은 매우 간단합니다. 해당 물리매체를 선택 후 Analyze Disk 를 선택하면 시작 섹터, 마지막 섹터 번호, 전체 섹터 수 그리고 전체 용량이 표시됩니다. 우리가 GPT 로 파티션 나눠둔 영역 그리고 그냥 MBR 파티션 영역도 모두 확인이 가능합니다.
FTK Imager에서 디스크를 인식 후 비교해 봅시다. 우리가 만든 도구에서는 섹터관련 정보만 있으나, 우리는 GUID 등의 정보도 어떤 값을 통해 만들었는지 알 수 있기 때문에 소스를 수정하면 다른 영역의 값도 충분히 추가하여 도구를 만들 수 있습니다.
3줄 요약
저장매체에서 파티션 별로 시작 섹터와 마지막 섹터번호, 파티션의 총 섹터 수를 가진다.
대부분 파티션의 상세한 정보를 이미 많이 알려져 있기 때문에 우리가 모든 영역을 일일히 직접 만들 수고는 많이 줄었습니다. 대신 물리적으로 0번 섹터부터 접근하는 경우는 거의 없기 때문에 소스에서 그 부분을 잘 하고 있는지, 어떤 방법으로 하고 있는지 잘 알아둘 필요가 있습니다.
물리이미징 수집에서 활용했던 소스도 잘 활용해보고, 이번에 파티션에 사용했던 소스도 추후에 잘 이용해보도록 합시다.